`/side` chats are expiring too quickly; expiration behavior is opaque
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_
11 Comments
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.
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:
Occurrence time:
2026-06-03 15:51:55 +08:00Asia/Shanghai2026-06-03 07:51:55 UTC019e8c77-b217-7fd2-818b-79100d753122.Context:
019e8794-5b50-72f2-ae70-4458a5fa5265Local checks:
~/.codex/sessions/2026/06/02/rollout-2026-06-02T17-05-17-019e8794-5b50-72f2-ae70-4458a5fa5265.jsonl~/.codex/sessions/2026/06/03/directory.thread not loaded: 019e8c77-b217-7fd2-818b-79100d753122prompt-history.019e8c77-b217-7fd2-818b-79100d753122composer-prompt-drafts-v1.local:019e8c77-b217-7fd2-818b-79100d753122This 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.
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?
Adding a current-build macOS datapoint.
Environment:
/sidechatObserved:
After returning to an existing
/sidewindow and sending a query, Codex showed:Local validation:
codex_app.list_threadsreturns no thread for that id.~/.codex/sessions.Expected:
If
/sidechats expire, the UI should say that directly and offer to start a new side chat. It should not surface the internal rollout error.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_configsetsfork_config.ephemeral = true(tui/src/app/side.rs:579-592), andephemeralshort-circuitsthread 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_DELAYis 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
ephemeralanywhere — 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_threadreturnsThreadNotFound (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
ephemeralaccessor already used atthread_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.
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.
Constantly running into this, would really enjoy a fix
gotchu, lemme look in a little bit
Adding a current Windows regression datapoint.
Environment:
26.727.6591.0(Microsoft Store package)26100, x64/sideside chatObserved behavior:
/side.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.
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 !
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_messagematches"no rollout found for thread id"and substitutesSIDE_NO_STARTED_CONVERSATION_MESSAGErather than surfacing the raw error.tui/src/app/session_lifecycle.rs:222-232—is_terminal_thread_read_errormatches"thread not loaded:", andclosed_state_for_thread_read_errormarks 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.
/sidethreads are created withephemeral = true(tui/src/app/side.rs:579-592), andephemeralshort-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-memoryArc<CodexThread>that the unload dropped.which leaves two genuinely different requests, and they have very different costs:
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_DELAYis 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"atthread_lifecycle.rs:411— if that appears shortly before the pane goes blank, the mechanism is confirmed and the only open question is what sethas_subscribers=falseand when.