Skip to content

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
fujibee:mainfrom
hakohund:fix/codex-tui-thread-affinity
Open

fix(codex): open the TUI on the role's recorded thread, and let the bridge attach to it#960
hakohund wants to merge 1 commit into
fujibee:mainfrom
hakohund:fix/codex-tui-thread-affinity

Conversation

@hakohund

@hakohund hakohund commented Aug 22, 2026

Copy link
Copy Markdown

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 codex in that project can never show the conversation the bridge delivers to:

codex-monitor.sh:247 a plain codex is codex --remote <url> — a fresh thread
codex-bridge-launcher.sh:667-680 the bridge is bound to the role's recorded thread, nothing else (#350)
codex-record-session.sh:75-87 an existing seat is never replaced by inference; only CODEX_THREAD_ID may, 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-runs actas on 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:

codex resume --remote <url> <thread> [the operator's own args]

The gate is agmsg_role_resume_uuid from lib/boot-command.sh — the one spawn and resurrect already 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 explicit codex resume … is untouched — it already carries the operator's choice.

codex-bridge.js--wait-for-tui-thread. With an explicit --thread, poll thread/loaded/list (up to --loaded-timeout) until the TUI has the thread loaded, then attach to it without a thread/resume of our own: turn/start needs 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 the already has an active writer collision 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 the mode monitor enable 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

Tests (measured)

New, all against the fake codex / fake app-server the existing suites use:

file new what
test_codex_monitor.bats 6 resumes the seat (argv shape and the operator's args after it); stays fresh with no seat, a gone rollout, a foreign-project seat, several roles (with the message); explicit resume passed through as given
test_codex_bridge.bats 2 attaches after the TUI's load shows up (≥3 thread/loaded/list, then process/spawn, never thread/resume/thread/start); gives up when it never does (no resume, no spawn, exit ≠ 0)
test_codex_bridge_launcher.bats 1 --thread rec-thread-1 --wait-for-tui-thread reaches the bridge

Control, 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.

suite result
test_codex_monitor ok=16 (1 platform skip)
test_codex_bridge_launcher ok=17 (1 platform skip)
test_codex_bridge ok=47 (4 sandbox skips: unix-socket listen)
test_codex_shim ok=25
test_codex_resume ok=33
check-enforced-assertions.sh baseline 638, unchanged — the new assertions use grep / refute, not [[ ]] / ! cmd
check-private-names.mjs clean

test_delivery.bats has 4 failures in my sandbox (unsignalable watcher, two watch.sh subscription cases, single-quoted hook commands) — identical on unmodified main in 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, then wakeup N … started turn on thread … with the turn visible in the TUI.

Out of scope, deliberately

…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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant