fix(codex): open the TUI on the role's recorded thread, and let the bridge attach to it - #960
Open
hakohund wants to merge 1 commit into
Open
fix(codex): open the TUI on the role's recorded thread, and let the bridge attach to it#960hakohund wants to merge 1 commit into
hakohund wants to merge 1 commit into
Conversation
…ridge attach to it A plain monitored `codex` starts a FRESH thread (`codex --remote <url>`), while the bridge is bound to the role's RECORDED thread (fujibee#350) and a seat that exists is never replaced by inference (fujibee#579). Together: once a role has a seat, the conversation the operator opens can never become the seat again. The bridge keeps delivering into the recorded thread, every turn is marked read there, and the TUI shows a different conversation. codex-monitor.sh now opens the TUI on the recorded thread (`codex resume --remote <url> <thread>`) when that is unambiguous: one Codex role registered for the project, seated in this project, rollout still on disk (agmsg_role_resume_uuid -- the same fail-open gate spawn and resurrect use, so a gone rollout cannot take the TUI down). Several roles, no seat, a foreign-project seat: fresh, as before, with a line saying why. An explicit `codex resume ...` is never rewritten. codex-bridge.js gains --wait-for-tui-thread: with an explicit --thread, poll thread/loaded/list until the TUI has it loaded, then attach without a thread/resume of our own (turn/start needs only the id, as the no-rollout fallback has relied on since fujibee#276; a bridge-side resume is at best redundant and at worst the "already has an active writer" collision of fujibee#906). A bridge that never sees its TUI on the thread exits instead of delivering into a thread no one is looking at. The launcher passes the flag. Tests: six codex-monitor cases (resumes the seat; stays fresh with no seat, a gone rollout, a foreign-project seat, or several roles; explicit resume untouched), two bridge cases (attach after the TUI's load, no resume; give up when it never loads), one launcher case (the flag is passed). Docs: README, README.ja, docs/codex-monitor-beta.md, and the `mode monitor` enable message. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UF5bLjYkTJmnF4bAR8i83A
hakohund
force-pushed
the
fix/codex-tui-thread-affinity
branch
from
August 22, 2026 14:21
b984366 to
919b065
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #959. On
main(rebased over #943, which touched the same launcher hunk).Problem
Once a Codex role has a recorded seat, a plain monitored
codexin that project can never show the conversation the bridge delivers to:codex-monitor.sh:247codexiscodex --remote <url>— a fresh threadcodex-bridge-launcher.sh:667-680codex-record-session.sh:75-87CODEX_THREAD_IDmay, and codex 0.141+ does not export it (#579, #170)Seat = T1, operator relaunches, TUI = T2, bridge delivers into T1, every turn marked read where no one is looking, and the seat cannot move to T2 — not on SessionStart, not on
actas. #353 closed #350 assuming "a role re-runsactason resume, which rewrites the record"; #579 made that rewrite impossible, on purpose and for good reason. The TUI half was never done.The change
codex-monitor.sh— a plain launch makes the same choice the launcher makes. When exactly one Codex role is registered for the project, its seat is recorded for this project, and the rollout still exists, the TUI is opened on it:The gate is
agmsg_role_resume_uuidfromlib/boot-command.sh— the onespawnandresurrectalready use (record + transcript, else fresh), so a seat whose rollout is gone cannot take the TUI down with it (codex resume <gone-uuid>refuses to start). Several roles: fresh, with a line saying a plain launch cannot choose and how to (codex resume <thread-id>). A seat in another project: fresh, saying so. An explicitcodex resume …is untouched — it already carries the operator's choice.codex-bridge.js—--wait-for-tui-thread. With an explicit--thread, pollthread/loaded/list(up to--loaded-timeout) until the TUI has the thread loaded, then attach to it without athread/resumeof our own:turn/startneeds only the id, which the no-rollout fallback has relied on since #276, so a bridge-side resume is at best redundant and at worst thealready has an active writercollision of #906. If the TUI never loads it, the bridge exits instead of delivering into a thread no one is watching; the launcher relaunches it once the TUI is there.codex-bridge-launcher.sh— passes the flag. Every thread it launches on is a live TUI's (recorded seat, or the legacy request-file thread).Docs — README / README.ja (one paragraph in the Codex section),
docs/codex-monitor-beta.md(a section under Bridge Mechanics), and themode monitorenable message, which said "the bridge starts on your first turn" and now says what happens to a seated role.No data layout, driver interface or manifest changes; no new dependency. No ADR.
Why it is safe
codex-monitor.shfalls through to exactly today'scodex --remote <url>launch; only the single-role, seated-here, rollout-present case changes. The gate is the existing fail-open one.--wait-for-tui-threadis opt-in on the bridge; the launcher is its only caller.--thread loadedand the default (resume) path are unchanged.threadIdle = true,turnActive = false, no resume) is the state the fix(codex): grace fallback for thread/resume failure on Codex 0.142+ #276 fallback already leaves the bridge in; the watchdogs that rescue an externally-active thread (Codex bridge: thread stuck in waitingOnApproval deadlocks delivery forever (bridge can't answer approval requests; launcher never replaces the hung bridge) #299) apply as before.Tests (measured)
New, all against the fake codex / fake app-server the existing suites use:
test_codex_monitor.batstest_codex_bridge.batsthread/loaded/list, thenprocess/spawn, neverthread/resume/thread/start); gives up when it never does (no resume, no spawn, exit ≠ 0)test_codex_bridge_launcher.bats--thread rec-thread-1 --wait-for-tui-threadreaches the bridgeControl, same tests against unmodified
main: the monitor "resumes the seat" / "foreign project" / "several roles" cases and both bridge cases fail; the three monitor cases that document unchanged behaviour pass on both, as they should.test_codex_monitortest_codex_bridge_launchertest_codex_bridgetest_codex_shimtest_codex_resumecheck-enforced-assertions.shgrep/refute, not[[ ]]/! cmdcheck-private-names.mjstest_delivery.batshas 4 failures in my sandbox (unsignalable watcher, twowatch.shsubscription cases,single-quoted hook commands) — identical on unmodifiedmainin the same sandbox, so not from this change; the sharded CI is the gate there.Running locally since 2026-08-10 (an earlier form of this on v1.1.13): the bridge log reads
TUI loaded thread …; attaching without a competing resume, thenwakeup N … started turn on thread …with the turn visible in the TUI.Out of scope, deliberately
serverRequest/resolved, bounded decline as fallback) is a follow-up, not folded in here.