`/side` chats are expiring too quickly; expiration behavior is opaque

Open 💬 11 comments Opened May 30, 2026 by miraclebakelaser

What version of the Codex App are you using (From “About Codex” dialog)?

Version 26.527.31326 (3390)

What subscription do you have?

Pro

What platform is your computer?

Darwin 25.5.0 arm64 arm

What issue are you seeing?

Ephemeral /side chats expire too quickly in Codex App (not tested for CLI). Currently, side chats seem to expire after a very short period, even when the app is open and the side chat is active. Chat expiration seems to even more aggressive after recent updates. In my loose estimation, it feels like side chats expire after ~15 minutes without any way to recover them.

How I use /side: I use side chats to perform research, the conclusions of which I feed back into the main chat. Some of these conversations end up becoming very long. Unfortunately, I often get deep into a side conversation only to realize that it has been expired and unrecoverable.

Furthermore, expiration behavior is not transparent. After when is a chat supposed to expire? My expectation is that side chats not expire so long as the side chat window is open.

What steps can reproduce the bug?

Use /side in Codex App. Leave the computer for a little while and resume the conversation. Get error message "Error creating task no rollout found for thread id [..]"

What is the expected behavior?

Immediate fix: Don't expire open side chats. If open side chats must remain on a timer, then please make that timer visible to users.

Stretch goals: allow users to turn side chats into a durable fork https://github.com/openai/codex/issues/20262

Additional information

_No response_

View original on GitHub ↗

11 Comments

alik-git · 2 months ago
Stretch goals: allow users to turn side chats into a durable fork https://github.com/openai/codex/issues/20262

imho this should be a first class feature, not being able to turn side chats into full chats are a pain point in my workflow. actually, I would even consider folding the side chat functionality into fork, because it really is a different kind of fork, and I don't see the point of deleting these chats.

Thank you for the amazing work on codex btw, its fantastic.

Nishijujuba · 2 months ago

Adding another data point, from Codex Desktop on Windows.

This is the same visible failure mode: an open side chat accepted a message while it still appeared usable, then task creation failed with:

创建任务时出错
no rollout found for thread id 019e8c77-b217-7fd2-818b-79100d753122

Occurrence time:

  • 2026-06-03 15:51:55 +08:00 Asia/Shanghai
  • 2026-06-03 07:51:55 UTC
  • This timestamp is decoded from the UUIDv7 prefix of the failed side-thread id 019e8c77-b217-7fd2-818b-79100d753122.

Context:

  • Parent thread: 019e8794-5b50-72f2-ae70-4458a5fa5265
  • The side chat came from that parent thread.
  • At the time the message was submitted, the side chat did not yet show as expired in the UI.
  • Later, the UI showed the side chat as expired.

Local checks:

  • The parent rollout exists locally:
  • ~/.codex/sessions/2026/06/02/rollout-2026-06-02T17-05-17-019e8794-5b50-72f2-ae70-4458a5fa5265.jsonl
  • No rollout file exists for the failed side-thread id under the local ~/.codex/sessions/2026/06/03/ directory.
  • Reading the failed side-thread id through the app thread API returns:
  • thread not loaded: 019e8c77-b217-7fd2-818b-79100d753122
  • The failed side-thread id appears only in local UI state under prompt history / composer draft keys:
  • prompt-history.019e8c77-b217-7fd2-818b-79100d753122
  • composer-prompt-drafts-v1.local:019e8c77-b217-7fd2-818b-79100d753122
  • It does not appear as a recoverable thread entry such as a projectless thread id, workspace-root hint, queued follow-up, or output directory mapping.

This makes it look like the renderer/composer accepted and retained the user's side-chat input, while the backing ephemeral side-thread had no materialized rollout and was no longer loadable by the app-server path used to create the task.

Expected behavior: if a side chat is still visible and accepts input, submitting should either start the task successfully or the UI should block submission with an explicit expired-state message before accepting the input. If side chats are intentionally ephemeral, the expiry window should be visible or the side chat should be promotable/persistable before input is lost.

I am avoiding full rollout files, DB dumps, or the exact user prompt content because they may contain private project context.

gclayburg · 2 months ago

can someone explain why side chats have an expiry date at all? It seems like codex goes out of its way to prevent you from having more than one chat thread on the same worktree. why? I can kind of get around this by creating a new thread with a new worktree for the first chat. For the second chat you can create a brand new project, navigate through the arbitrary hidden filename that codex chose for the worktree folder name and eventually create a new thread with that same worktree folder. but this is a mess to do. It seems like this should just be a single button on the UI. I thought 'side chat' was that button, but I was surprised to see that codex decides on its own when you are done using that side chat and deletes it completely. This is frustrating.

Cursor has a '+' button, why can't codex?

alibrohde · 2 months ago

Adding a current-build macOS datapoint.

Environment:

  • Codex Desktop: 26.616.81150
  • CFBundleVersion: 4306
  • macOS: 15.6.1 build 24G90, arm64
  • Area: /side chat

Observed:
After returning to an existing /side window and sending a query, Codex showed:

Error creating task
no rollout found for thread id 019eff38-a286-7f82-8dae-08edcc632c5

Local validation:

  • codex_app.list_threads returns no thread for that id.
  • No matching rollout file exists under ~/.codex/sessions.

Expected:
If /side chats expire, the UI should say that directly and offer to start a new side chat. It should not surface the internal rollout error.

PranavMishra28 · 25 days ago

The forensics in this thread line up with a specific mechanism: side chats aren't expiring
on a timer designed for them, they're being garbage-collected by a cache-eviction policy
written for threads that can be reloaded — and they can't be. Source is main @ bb5054fe.

/side threads are created with persistence off: side_fork_config sets
fork_config.ephemeral = true (tui/src/app/side.rs:579-592), and ephemeral short-circuits
thread persistence entirely (core/src/session/session.rs:618-621, and the state DB at
:704-714). So no rollout file is ever written — which is exactly the "no rollout file exists
under ~/.codex/sessions" observation above, and it's by design. A side chat's whole existence
is one Arc<CodexThread> in the in-memory ThreadManager.

The generic idle-unload path then removes it. THREAD_UNLOADING_DELAY is 30 minutes
(app-server/src/request_processors/thread_lifecycle.rs:5), firing when a thread has no
subscribers and is not Active, measured from the later of the two transitions (:56-63); the
unload removes it from the manager at :411-423. That file contains no reference to
ephemeral anywhere — I grepped app-server/src, and the hits are all in thread_delete.rs,
thread_goal_processor.rs, thread_processor.rs and message_processor.rs, none in the
lifecycle path. So an ephemeral thread is evicted like a normal one; for a normal thread
that's a cheap reclaim because the next request re-reads the rollout, and for an ephemeral
one it's irreversible deletion of the only copy.

That produces both error strings reported here. Once out of the map, get_thread returns
ThreadNotFound (core/src/thread_manager.rs:1285-1291) and the fallbacks hit the store, which
has nothing — "no rollout found for thread id ..." comes from
thread-store/src/local/read_thread.rs:83-87 and model_context.rs:39-43, and "thread not
loaded: <id>" from app-server thread_processor.rs:2346-2350. The TUI already treats both as
known conditions (tui/src/app/side.rs:604-614, session_lifecycle.rs:224); the desktop app
apparently doesn't, which is why the raw internal error shows.

One thing I can't reconcile and don't want to assert: the constant is 30 minutes and the
floor is 30 min after the client unsubscribes, but several reports here say ~15. Candidates
I couldn't check: the desktop app unsubscribing from a background side pane as soon as it
loses focus (so the user's clock starts earlier than the server's), a desktop-side unload
trigger outside this repo, an app-server restart (which kills every ephemeral thread
instantly, no timer involved), or an explicit discard — the TUI discards the side thread when
you switch threads (side.rs:414, :436, :543, :637-650) and desktop likely has an analogue.

Two checks that would settle it. The decisive log line is
"thread {id} has no subscribers and is idle; shutting down" (thread_lifecycle.rs:411) — if it
appears for the side thread shortly before the failure, the mechanism is confirmed and the
only open question is what set has_subscribers=false and when. And to separate the constant
from client behaviour: open a /side chat, keep the pane focused, idle 35 minutes, then send.
Surviving means the server timer isn't the trigger and the client is unsubscribing early;
dying at ~30 min with the pane focused means the subscription bookkeeping is wrong.

On fixes: exempting ephemeral threads from idle unload is the smallest real change (one
check in UnloadingState using the ephemeral accessor already used at
thread_manager.rs:685/719) but needs a companion cap or LRU, or it trades data loss for
unbounded memory. Materializing on evict would make them recoverable and would deliver most
of what's being asked for here, but it contradicts the stated contract —
app-server-protocol thread_data.rs:198-199 says ephemeral threads "should not be materialized
on disk" — and would make side chats appear in session lists and search, which is a product
call. At minimum, emitting a notification on ephemeral unload so the client can disable the
composer would stop the current behaviour of accepting typed input into a thread that no
longer exists.

felix021 · 24 days ago

Adding a related Desktop observation:

I understand that an expired side chat may no longer be able to accept new messages. However, after expiration, its existing transcript is no longer shown either.

Previously, I could still read the past messages in an expired side chat even though I could not continue the conversation. I do not know which app version changed this behavior.

Losing both the ability to continue and the ability to view the existing conversation makes the side chat effectively inaccessible and removes useful notes and context.

Expected behavior: expiration should disable new messages, but preserve the transcript in a read-only view, or provide an explicit way to open/export it.

aryanetched · 23 days ago

Constantly running into this, would really enjoy a fix

PranavMishra28 · 23 days ago

gotchu, lemme look in a little bit

z-stefanie · 23 days ago

Adding a current Windows regression datapoint.

Environment:

  • Codex Desktop: 26.727.6591.0 (Microsoft Store package)
  • Windows 11 24H2, build 26100, x64
  • Area: /side side chat

Observed behavior:

  1. Create and use a side chat through /side.
  2. Let the side chat expire.
  3. Reopen or return to the expired side chat.
  4. The entire side pane is blank/white, and none of the existing conversation transcript is visible.

Previous behavior:

In earlier Codex App versions, a side chat could also expire and stop accepting new messages, but its existing transcript remained readable. The current blank-screen behavior is therefore a regression: expiration now removes access to both continuation and history.

Expected behavior:

An expired side chat should render its existing transcript in a read-only state and clearly indicate that no new messages can be sent. At minimum, it should show an explicit expired-state message instead of a blank pane.

This matches the transcript-loss observation in https://github.com/openai/codex/issues/25233#issuecomment-5175913592, with the additional current-build detail that the affected Desktop pane renders as a completely blank white screen.

theophanemayaud · 23 days ago

Yeah same as @z-stefanie : I've recently started having side threads "become completely blank" when they expire though I'm on codex app macOS, which is really making side threads unreliable and I think I'll just end up forking all the time instead !

PranavMishra28 · 22 days ago

followed up on this. the blank-pane reports from @z-stefanie and @theophanemayaud look like a client rendering gap rather than a change in the eviction behaviour, and I think that matters for where this gets routed.

on current main (c4f42d1) the TUI still has explicit handling for exactly these two failures:

  • tui/src/app/side.rs:604-613 — side_start_error_message matches "no rollout found for thread id" and substitutes SIDE_NO_STARTED_CONVERSATION_MESSAGE rather than surfacing the raw error.
  • tui/src/app/session_lifecycle.rs:222-232 — is_terminal_thread_read_error matches "thread not loaded:", and closed_state_for_thread_read_error marks the thread closed off the back of it.

so the TUI degrades to "closed, with a message". a blank pane means the desktop client is hitting the same two errors without an equivalent branch. that would explain why the behaviour differs by client while the timer and the eviction path are common, and why it reads as a regression even though nothing about the expiry itself needs to have changed.

the harder part is the ask itself, and I don't think it can be served the way it's phrased. /side threads are created with ephemeral = true (tui/src/app/side.rs:579-592), and ephemeral short-circuits thread persistence entirely (core/src/session/session.rs:618-621, state DB at :704-714). there is no rollout on disk, by design — that is the same reason nobody found one under ~/.codex/sessions. so "preserve the transcript in a read-only view" cannot be satisfied from storage after eviction: the only copy was the in-memory Arc<CodexThread> that the unload dropped.

which leaves two genuinely different requests, and they have very different costs:

  1. render what the client already has. if the desktop app still holds the transcript it rendered, it can keep showing it read-only and just disable the composer. that is a client change and needs nothing from core.
  2. survive a restart or a cold open. that needs the thread to be materialized, which contradicts app-server-protocol/thread_data.rs:198-199 ("ephemeral threads should not be materialized on disk") and would put side chats into session lists and search. that is a product call, not a bug fix.

if the goal is just "stop losing my visible conversation", (1) is the whole fix and it is a lot cheaper.

one thing I could not settle: several reports here say ~15 minutes while THREAD_UNLOADING_DELAY is 30 (app-server/src/request_processors/thread_lifecycle.rs:5), measured from the later of the two transitions (:56-63). candidates I can't check from outside are the desktop app unsubscribing from a background pane as soon as it loses focus (starting the user's clock earlier than the server's), or an app-server restart, which kills every ephemeral thread instantly with no timer involved. the decisive log line is "thread {id} has no subscribers and is idle; shutting down" at thread_lifecycle.rs:411 — if that appears shortly before the pane goes blank, the mechanism is confirmed and the only open question is what set has_subscribers=false and when.