Keep a pane's Codex conversation while tool output fills the screen - #16
Conversation
devswha
left a comment
There was a problem hiding this comment.
The per-pane binding approach is sound: two panes in the same cwd, process restarts, and Codex-home checks all behave. Three things before merge:
- The new contract test is timing-dependent.
codex-binding.contract.test.tswaits a fixed 700ms after typing. On a slow-starting shell (zsh here),pane.process_infostill shows only the shell. Both new tests fail both in the full run and alone, and pass with longer waits. Also, in the matched test the first match comes from the typed command line (SAY='<answer>'), not from Codex's output. Please poll until acodexprocess appears, and keep the answer text out of the typed command. - The resume fallback can show the wrong conversation for a long time. After
codex resume A, then/new, the command line still names A. So every poll returns A's rollout until B produces a matching answer of at least 64 characters. With short answers or a long tool run, the chat shows A and the user's new prompts never appear. On main this showed "Conversation unavailable", which was honest. Suggestion: only useresumedPathwhile A is the newest thread for that cwd, or drop it once a newer thread row appears after the process started. - Rebase onto main. #5 is merged and conflicts with this PR in
server/codex.ts(paneRead/readTail→codexHistoryTail) and in thecodex.test.tsimports.
Minor: the 64-entry cache evicts in insertion order, not least recently used. Re-set the key on each hit to keep active panes.
672017c to
c51a517
Compare
|
Thanks. Addressed in c51a517. The branch is rebased onto main, which resolves the
Minor. The binding is re-inserted on each hit, so the cache evicts least recently used. |
|
One more follow-up (b84e6be). Your point 2 applied to a screen match as well: after |
b84e6be to
48bdfbb
Compare
devswha
left a comment
There was a problem hiding this comment.
Thanks. The contract test, the stale fallback, the rebase and the LRU point are all done. One real issue with the new "newer thread begins" check:
- The pane's own subagents and
codex execruns count as a newer thread. The check (server/codex.ts~405–423) readsthreadsrows withcwd = ? AND archived = 0 AND agent_role IS NULL, and nothing ties a row to this pane's process.agent_role IS NULLdoesn't keep subagents out: a localstate_5.sqlitehere has 429 subagent threads with a NULLagent_role, all in their parent's cwd, and 13,058source='exec'threads. Scenario: the pane's Codex spawns a subagent during a long tool run, which is exactly the case this PR targets. The binding is marked superseded and dropped, and the chat shows "Conversation unavailable". Anycodex execin the same repo does the same, including the Codex plugin run from Claude Code. Please filter to interactive sources (source IN ('cli','vscode')). Better still, on Linux use the rollout file descriptors you already collect as the proof that the process moved on. - Two Codex panes in one repo. A thread started in one pane drops the other pane's binding while its answer is off screen. It never flips to the wrong conversation, but it shows "unavailable" until its own answer is back on screen. The fd-based check would fix this too.
Minor: the "kept through the flood" step still uses a fixed Bun.sleep(3500) and never checks that the answer actually left the screen, so it would pass even if the flood didn't happen. Also, #18 is merged: there's a trivial conflict in the server/codex.test.ts import line.
…the screen Codex does not say which rollout it writes, so the chat recognises it by a recent answer on screen. A long run of exec/wait output pushes that answer past the 400 lines read, and the chat flipped to "Conversation unavailable" until Codex answered again. Now a TUI started as `codex resume <thread>` reads that thread by id, and a pane whose Codex was matched once keeps that rollout while the same process runs; a later match to another rollout (after /new) replaces it, and another process starts over. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…dy binding test Review follow-ups: - After `codex resume A` then /new, the command line still names A, and the fallback showed A's conversation until the new thread produced a matching answer. It now holds only while no other thread in the pane's cwd began after that Codex process started (Linux: its start time from /proc); a later thread, from /new or another pane, makes the chat say it cannot tell, as main did. Without a start time it holds only while A is the cwd's newest. - The contract test waits until the stand-in codex process runs instead of a fixed 700ms, and the matched answer comes from the process's own output, never from the typed command line. It also covers the newer-thread case. - A pane's binding is re-inserted on use, so the 64 kept are the recent ones. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
The review's /new case applied to a screen match as well: after /new the same process writes a new thread, and the pane kept showing the matched one until the new thread produced a matching answer. A binding now holds only while no thread in the pane's cwd began since the match; otherwise the chat says it cannot tell, as main did. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s /new Review follow-ups for the "newer thread begins" guard: - Subagents (many with a NULL agent_role) and `codex exec` runs share their parent's cwd, so one spawned during a long tool run dropped the binding: the exact case this PR targets. Only interactive threads (source cli or vscode, when the store has that column) count now, for both the resume fallback and a screen match. - A thread another pane is bound to is that pane's, and no longer counts. A superseded binding is kept rather than deleted, so a pane left unsure by a second Codex in the same repo holds again once that thread is bound to its own pane. - The contract test waits until the flood has pushed the answer out of the 400 lines the match reads, instead of a fixed sleep. It covers a subagent and an exec thread (kept), an interactive one (dropped), and another pane's thread (unsure until bound there, then kept). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
48bdfbb to
42138b1
Compare
|
Thanks. Addressed in 42138b1, rebased onto main (the import conflict is resolved):
Minor. The test now waits until the flood has pushed the answer out of the 400 lines the match reads, instead of sleeping. New cases:
Each case fails without its part of the fix. |
… as /new A second Codex in the same repo starting a thread left this pane unsure (chat "unavailable" while its answer was off screen) until someone opened the other pane's chat and bound it. When this pane's own screen cannot tell and a newer interactive thread exists, the other Codex panes in the cwd are now resolved on the spot, without looking further. A thread one of them shows is theirs (and they are bound to it), not a /new of this pane. This runs at most once per 5s per set of panes, and only while such a thread is unclaimed. A /new in this pane shows in no other pane, so it still reads as unsure. The two-pane contract test no longer reads the other pane's chat first. It fails without this change. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Follow-up on point 2, two Codex panes in one repo (0454317). The binding no longer waits for someone to open the other pane's chat. When this pane's own screen can't tell and a newer interactive thread exists, the server resolves the other Codex panes in the same cwd on the spot, without them looking further. A thread one of them shows is that pane's, and that pane gets bound to it. It is not read as a The check runs at most once per 5s per set of panes, and only while such a thread is unclaimed. A The two-pane contract test no longer reads the other pane's chat first. It fails without the change. |
The check of the other Codex panes resolved each one in full: its processes, open files, the store and up to 32 rollout tails matched against its screen. It now reads each pane's screen once and matches it against the unclaimed newer threads only, usually one. A pane that shows one is bound to it. The 5s limit is kept per set of threads and panes, so a pane that appears since is looked at at once. Checked with two real Codex 0.156.1 panes in one folder. A was matched, then its answer was pushed off screen. B was opened afterwards and answered. A kept its conversation without anyone reading B's chat (119 ms for that read, 12 ms once B was bound). With this check disabled, the same run left A "unavailable". A /new in A afterwards was not taken for its old conversation. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
|
Follow-up (3f3aa6e): the other-pane check now reads each other Codex pane's screen once and matches it only against the unclaimed newer threads, instead of resolving each pane in full. Checked with two real Codex 0.156.1 panes in one folder:
One thing I learned about the store: Codex 0.156.1 writes a TUI's thread row at its first message, with |
|
Merged at 3f3aa6e. Thanks. The source filter and the cross-pane claim work, and a
|
|
Thanks. The follow-ups are in #20:
On |
From the merge reviews: - #19: codexHistoryTail read the remembered chain as it was. After a backtrack the live file's tail reached an archived parent, the read threw, and an app-server TUI without an open rollout lost its screen match until the entry was evicted. Both codexHistoryTail and codexHistorySegments now use the chain only while every file in it exists, and look it up again otherwise. transcriptStream's own check is folded into that. - #16: the 32 candidates for a screen match are interactive threads only, so a burst of `codex exec` runs no longer pushes a pane's own thread out. - #16: another pane now claims a newer thread only when its screen shows that thread's first message as well as its answer. An answer of this pane quoted or pasted there claims nothing. Its screen is matched against this pane's own thread and its own binding too, so a match is unique across all of them. - #16: the conversation route hands its session snapshot down, so an unsure poll takes no extra snapshot, and closed panes' bindings are dropped with it. Not changed: narrowing `source='vscode'`. On this machine, TUIs attached to a Codex Desktop app-server record originator "Codex Desktop" too, so the store cannot tell a Desktop or plugin thread from a TUI's. A thread no pane shows keeps reading as unsure, never as another conversation. Tests: the archived-parent tail and the exec burst each fail without their change. A pane showing only the other thread's answer leaves this pane unsure; the pane that typed it claims it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Summary
Codex doesn't say which rollout it writes to, so the chat recognises a Codex pane's rollout by a recent answer on screen, matched within the last 400 lines. A long run of exec/wait output pushes that answer past the 400 lines. The chat then flipped to "Conversation unavailable — show terminal output" until Codex answered again.
codex resume <thread>reads that thread by id (threads.idinstate_5.sqlite), whatever the screen shows./newthe command line still names the resumed thread and the process is the same, so each fallback holds only while no other thread in the pane's cwd has begun since. For the resume fallback, "since" means since the Codex process started (its start time from/proc; without that, the resumed thread must be the cwd's newest). For a match, it means since the match. Only interactive threads count (sourcecliorvscode): subagents andcodex execruns share the cwd but never replace the TUI's conversation. A thread another Codex pane in the same cwd shows is that pane's, and doesn't count either: when this pane's screen can't tell, those panes are resolved on the spot (at most once per 5s), whether or not anyone opened their chat. Once a newer thread appears, the chat says it can't tell, as main does, until the new thread's answer matches.One trade-off: a new thread in another Codex pane in the same cwd counts as newer until that pane shows an answer of it. Until then, this pane says it can't tell while its own answer is off screen. It never shows the wrong conversation.
Testing
bun run typecheckis clean;bun test: 272 pass.codex resumecommand line.codexprocess that prints its own answer (never on the typed command line) and then floods the screen. The test waits for the process to appear rather than sleeping. It covers:main'scodex.ts, and each newer-thread test fails without its guard.codex resume <id>pane that is mostly exec/wait output resolves its conversation on every poll.🤖 Generated with Claude Code