Interactive codex spawn never connects: rollout is born at first turn, queued first message deadlocks on the transcript gate #96

Closed
opened 2026-07-10 14:08:16 +02:00 by dominik.polakovics · 2 comments

This was generated by AI during triage.

Symptom

Starting a Codex instance from the New-run page never "connects": the chat page shows "Waiting for the transcript…" indefinitely, and a first message typed into the New-run composer stays parked forever ("Sends when Codex is ready"). The codex TUI itself is healthy — it sits idle at its greeting in the run's tmux session.

Root cause (live-diagnosed 2026-07-10)

  1. codex-cli 0.133.0 (TUI) creates its rollout transcript ($CODEX_HOME/sessions/YYYY/MM/DD/rollout-*.jsonl) lazily at first-turn start, not at session init. Evidence: an issue-#87 spike session has session_meta.payload.timestamp 01:25:38Z but a file birth of 01:27:40Z — the rollout appeared only when the first message was sent; a 30-min-idle interactive instance had no rollout file at all and no rollout fd open. codex exec writes immediately (the turn starts at once), which is why the Tier-2 spike never surfaced this.
  2. Interactive spawns carry no argv prompt — only the AFK launcher populates SpawnSpec.InitialPrompt.
  3. Lab's only "connected" signal for codex is LocateTranscript's cwd-match over the rollout tree → it misses forever for an idle TUI.
  4. The queued first message (issue #41) auto-sends only once the transcript is available — a gate that exists as a TUI-readiness proxy and is valid for claude-code, whose transcript exists from process start. Codex inverts the dependency: the message that would create the transcript is waiting for the transcript. Deadlock.
  5. The parked text lives only in browser memory — a tab reload loses it.

Verified against ADR-0038 (#92/#94) and #93

The ReadChat refactor is behavior-neutral here: codex's ReadChat with an empty path composes the same idle empty chat core used to synthesize, the frontend gate and placeholder copy are unchanged, and the spawn path is untouched. The diagnosis holds on current main. The #93 dead-session sweep is orthogonal (the session is alive).

Workaround until fixed

Type into the run chat composer despite the "Waiting for the transcript…" placeholder — sends work in that state; the first send births the rollout and the chat connects on the next poll. Or attach to the tmux session directly.

Proposed direction (maintainer-approved)

  1. Ride the New-run composer text on SpawnSpec.InitialPrompt — the AFK-proven trailing positional for both providers. Retires the client-side queue and its gate: kills the deadlock by construction, no cold-start keystroke race, text survives reloads.
  2. Frontend: for an active run whose adapter-composed state is idle with no messages, trust that state (ADR-0038) and show "ready — your first message starts the conversation" copy instead of deriving "Waiting for the transcript…" from the missing transcript identity. No new seam surface.
  3. Hygiene: pin the lazy rollout-birth timing in the codex Tier-2 compat record; add "when is the transcript born?" to the ADR-0036 live checklist.

Agent brief follows in a comment.

> *This was generated by AI during triage.* ## Symptom Starting a Codex instance from the New-run page never "connects": the chat page shows "Waiting for the transcript…" indefinitely, and a first message typed into the New-run composer stays parked forever ("Sends when Codex is ready"). The codex TUI itself is healthy — it sits idle at its greeting in the run's tmux session. ## Root cause (live-diagnosed 2026-07-10) 1. **codex-cli 0.133.0 (TUI) creates its rollout transcript (`$CODEX_HOME/sessions/YYYY/MM/DD/rollout-*.jsonl`) lazily at first-turn start, not at session init.** Evidence: an issue-#87 spike session has `session_meta.payload.timestamp` 01:25:38Z but a file birth of 01:27:40Z — the rollout appeared only when the first message was sent; a 30-min-idle interactive instance had no rollout file at all and no rollout fd open. `codex exec` writes immediately (the turn starts at once), which is why the Tier-2 spike never surfaced this. 2. Interactive spawns carry no argv prompt — only the AFK launcher populates `SpawnSpec.InitialPrompt`. 3. Lab's only "connected" signal for codex is `LocateTranscript`'s cwd-match over the rollout tree → it misses forever for an idle TUI. 4. The queued first message (issue #41) auto-sends only once the transcript is available — a gate that exists as a TUI-readiness proxy and is valid for claude-code, whose transcript exists from process start. Codex inverts the dependency: **the message that would create the transcript is waiting for the transcript.** Deadlock. 5. The parked text lives only in browser memory — a tab reload loses it. ## Verified against ADR-0038 (#92/#94) and #93 The ReadChat refactor is behavior-neutral here: codex's `ReadChat` with an empty path composes the same idle empty chat core used to synthesize, the frontend gate and placeholder copy are unchanged, and the spawn path is untouched. The diagnosis holds on current main. The #93 dead-session sweep is orthogonal (the session is alive). ## Workaround until fixed Type into the run chat composer despite the "Waiting for the transcript…" placeholder — sends work in that state; the first send births the rollout and the chat connects on the next poll. Or attach to the tmux session directly. ## Proposed direction (maintainer-approved) 1. Ride the New-run composer text on `SpawnSpec.InitialPrompt` — the AFK-proven trailing positional for both providers. Retires the client-side queue and its gate: kills the deadlock by construction, no cold-start keystroke race, text survives reloads. 2. Frontend: for an active run whose adapter-composed state is idle with no messages, trust that state (ADR-0038) and show "ready — your first message starts the conversation" copy instead of deriving "Waiting for the transcript…" from the missing transcript identity. No new seam surface. 3. Hygiene: pin the lazy rollout-birth timing in the codex Tier-2 compat record; add "when is the transcript born?" to the ADR-0036 live checklist. Agent brief follows in a comment.
Author
Owner

This was generated by AI during triage.

Agent Brief

Category: bug
Summary: An interactive Codex spawn never connects because its transcript is only born at the first turn — deliver the operator's first message on the spawn argv, make the pre-transcript chat state honest, and pin the transcript-birth timing in the conformance bar.

Current behavior:
The New-run composer's text is parked client-side (issue #41) and auto-sent only once the run's transcript is located. codex-cli 0.133.0 creates its rollout transcript only when the first turn starts, so an interactive codex run with no message yet has no transcript: the locate misses forever, the chat page shows "Waiting for the transcript…", and the parked message never sends — a deadlock (the message that would create the transcript waits for the transcript). The parked text is lost on a tab reload. claude-code is unaffected only because its transcript file exists from process start, which made transcript availability double as a TUI-readiness proxy.

Desired behavior:

  • A manual (non-AFK) spawn started with text in the New-run composer delivers that text as the spawn's initial prompt via provider.SpawnSpec.InitialPrompt, which both adapters already emit as the trailing positional argument — present before the process starts, so there is no post-spawn keystroke and no cold-start race. The message therefore starts the first turn immediately, the transcript is born, and the chat connects without any operator intervention.
  • The client-side queued-message path (issue #41) is retired along with its transcript-availability gate: after this change no code path may wait on a transcript that only the waiting message itself would create. (If any queue remnant is kept for other reasons, it must not gate on transcript availability.)
  • A spawn with an empty composer stays exactly as today: no positional argument is appended (the existing conformance obligation for an empty InitialPrompt stands).
  • For an ACTIVE run whose adapter-composed state is idle with no messages and no located transcript, the chat page trusts the adapter's composed state (ADR-0038) and shows ready-to-start copy (e.g. "Ready — your first message starts the conversation") with a usable composer, instead of deriving a "Waiting for the transcript…" placeholder from the missing transcript identity. Sending from that composer must work (it already does — only the copy misleads).
  • The AFK path is unchanged — it already rides InitialPrompt.
  • The codex Tier-2 compat record (ADR-0036/ADR-0037) documents the live-pinned finding: the 0.133.0 TUI births its rollout at first-turn start, not session init, while codex exec writes it immediately. The ADR-0036 live-spike checklist gains a "when is the provider's transcript born?" question so the next adapter cannot hit this silently.

Key interfaces:

  • provider.SpawnSpec.InitialPrompt — already the trailing-positional seed prompt in both adapters' SpawnArgv; today only the AFK launcher populates it. The manual-launch path (launch spec + its HTTP create-instance surface) must be able to carry the operator's first message into it. Watch the existing invariant that injected spawn flags land BEFORE the trailing prompt.
  • The web New-run composer and the queued-message module (issue #41) — the producer and consumer of the parked text; the run chat view's auto-send effect and its transcript gate are the deadlock site.
  • The run chat view's empty/placeholder presentation — currently keyed off the transcript identity being absent; should key off the adapter-composed idle state per ADR-0038.
  • The codex Tier-2 compat record and the ADR-0036 checklist — where the rollout-birth pin and the new checklist question live.

Acceptance criteria:

  • Spawning a codex instance with text in the New-run composer produces a spawn argv whose trailing positional is that text, and the chat page shows the message and connects without any manual keystroke into the TUI.
  • Spawning a claude-code instance with composer text still delivers the text exactly once (no double-send from any queue remnant).
  • Spawning a codex instance with NO text shows ready-to-start copy and a usable composer; typing the first message there connects the chat.
  • No frontend or backend path waits on transcript availability before delivering the operator's first message.
  • An empty composer appends no positional argument (existing Tier-1 conformance for empty InitialPrompt stays green).
  • Tier-1 conformance passes for both registered adapters; the codex compat record documents the lazy rollout birth and the ADR-0036 checklist asks when the transcript is born.

Out of scope:

  • Changing LocateTranscript's cwd-match or reading codex's internal state DB to locate sessions earlier.
  • Any LiveSignals capability for codex (ADR-0037's honest-degradation posture stands).
  • claude-code hook-edge liveness (issue #62) and the dead-session sweep (#93).
  • Durability of an UNSENT drafted message across reloads beyond what argv delivery already provides.
> *This was generated by AI during triage.* ## Agent Brief **Category:** bug **Summary:** An interactive Codex spawn never connects because its transcript is only born at the first turn — deliver the operator's first message on the spawn argv, make the pre-transcript chat state honest, and pin the transcript-birth timing in the conformance bar. **Current behavior:** The New-run composer's text is parked client-side (issue #41) and auto-sent only once the run's transcript is located. codex-cli 0.133.0 creates its rollout transcript only when the first turn starts, so an interactive codex run with no message yet has no transcript: the locate misses forever, the chat page shows "Waiting for the transcript…", and the parked message never sends — a deadlock (the message that would create the transcript waits for the transcript). The parked text is lost on a tab reload. claude-code is unaffected only because its transcript file exists from process start, which made transcript availability double as a TUI-readiness proxy. **Desired behavior:** - A manual (non-AFK) spawn started with text in the New-run composer delivers that text as the spawn's initial prompt via `provider.SpawnSpec.InitialPrompt`, which both adapters already emit as the trailing positional argument — present before the process starts, so there is no post-spawn keystroke and no cold-start race. The message therefore starts the first turn immediately, the transcript is born, and the chat connects without any operator intervention. - The client-side queued-message path (issue #41) is retired along with its transcript-availability gate: after this change no code path may wait on a transcript that only the waiting message itself would create. (If any queue remnant is kept for other reasons, it must not gate on transcript availability.) - A spawn with an empty composer stays exactly as today: no positional argument is appended (the existing conformance obligation for an empty `InitialPrompt` stands). - For an ACTIVE run whose adapter-composed state is idle with no messages and no located transcript, the chat page trusts the adapter's composed state (ADR-0038) and shows ready-to-start copy (e.g. "Ready — your first message starts the conversation") with a usable composer, instead of deriving a "Waiting for the transcript…" placeholder from the missing transcript identity. Sending from that composer must work (it already does — only the copy misleads). - The AFK path is unchanged — it already rides `InitialPrompt`. - The codex Tier-2 compat record (ADR-0036/ADR-0037) documents the live-pinned finding: the 0.133.0 TUI births its rollout at first-turn start, not session init, while `codex exec` writes it immediately. The ADR-0036 live-spike checklist gains a "when is the provider's transcript born?" question so the next adapter cannot hit this silently. **Key interfaces:** - `provider.SpawnSpec.InitialPrompt` — already the trailing-positional seed prompt in both adapters' `SpawnArgv`; today only the AFK launcher populates it. The manual-launch path (launch spec + its HTTP create-instance surface) must be able to carry the operator's first message into it. Watch the existing invariant that injected spawn flags land BEFORE the trailing prompt. - The web New-run composer and the queued-message module (issue #41) — the producer and consumer of the parked text; the run chat view's auto-send effect and its transcript gate are the deadlock site. - The run chat view's empty/placeholder presentation — currently keyed off the transcript identity being absent; should key off the adapter-composed idle state per ADR-0038. - The codex Tier-2 compat record and the ADR-0036 checklist — where the rollout-birth pin and the new checklist question live. **Acceptance criteria:** - [ ] Spawning a codex instance with text in the New-run composer produces a spawn argv whose trailing positional is that text, and the chat page shows the message and connects without any manual keystroke into the TUI. - [ ] Spawning a claude-code instance with composer text still delivers the text exactly once (no double-send from any queue remnant). - [ ] Spawning a codex instance with NO text shows ready-to-start copy and a usable composer; typing the first message there connects the chat. - [ ] No frontend or backend path waits on transcript availability before delivering the operator's first message. - [ ] An empty composer appends no positional argument (existing Tier-1 conformance for empty `InitialPrompt` stays green). - [ ] Tier-1 conformance passes for both registered adapters; the codex compat record documents the lazy rollout birth and the ADR-0036 checklist asks when the transcript is born. **Out of scope:** - Changing `LocateTranscript`'s cwd-match or reading codex's internal state DB to locate sessions earlier. - Any LiveSignals capability for codex (ADR-0037's honest-degradation posture stands). - claude-code hook-edge liveness (issue #62) and the dead-session sweep (#93). - Durability of an UNSENT drafted message across reloads beyond what argv delivery already provides.
Author
Owner

This was generated by AI while landing a PR.

Land-PR audit — PR #97PASS

Verification signal relied on: the green ci / native check run (exit 0, success, 4m48s). That single job exercises the full stack this diff touches — SPA eslint/prettier/vitest + vite build, then the ui-tagged go build/go test, then untagged golangci-lint 2.12.2 — so the green light covers both the Go and web paths. Not re-run (it already vouches).

Checks:

  • AFK contract — head afk/96, body carries a working Closes #96.
  • Convention lint — title fix(spawn): … is Conventional Commits.
  • Mergeableafk/96 contains the main tip; no conflict.
  • Diff review vs. issue intent:
    • Backend: first_message added to the create request with shape-only validation mirroring the afk_prompt precedent (whitespace-only → none; afkPromptMaxBytes cap → 400 verbatim otherwise), threaded instanceCreateRequest → StartParams.FirstMessage → LaunchSpec.SeedPrompt onto the trailing spawn positional. --settings still lands before the prompt; SeedPrompt carries no run-kind semantics. Covered by TestAPI_InstanceStartFirstMessage + TestStart_firstMessageIsTrailingSpawnPositional (zero keystroke batches asserted).
    • Frontend: req.first_message is set before startInstance (correct ordering — no dependence on a post-spawn run.id); empty box omits the key. The queuedMessage module, its transcript-availability gate, the auto-send effect, the pending bubble, and the draftSeed plumbing are fully deleted with all consumers removed (eslint-green CI confirms no dangling refs).
    • The new <Match> for the ready-to-start copy is correctly ordered ahead of the generic locating placeholder and is tightly guarded (!ended() && transcript()==="locating" && state()==="idle" && messages().length===0), so it cannot fire for a mid-conversation run or an ended run — verified by the rewritten RunChat.test.tsx cases (state:"" and ended run both keep the locating placeholder).
    • Hygiene: codex compat record pins the lazy rollout birth with a when-this-breaks recipe; ADR-0025 amendment + ADR-0036 checklist question present.

Verdict: PASS. No blockers. Awaiting the human's free-text go-ahead to merge.

> *This was generated by AI while landing a PR.* ## Land-PR audit — PR #97 → **PASS** **Verification signal relied on:** the green `ci / native` check run (exit 0, `success`, 4m48s). That single job exercises the full stack this diff touches — SPA `eslint`/`prettier`/`vitest` + `vite build`, then the `ui`-tagged `go build`/`go test`, then untagged `golangci-lint 2.12.2` — so the green light covers both the Go and web paths. Not re-run (it already vouches). **Checks:** - **AFK contract** — head `afk/96`, body carries a working `Closes #96`. ✅ - **Convention lint** — title `fix(spawn): …` is Conventional Commits. ✅ - **Mergeable** — `afk/96` contains the `main` tip; no conflict. - **Diff review vs. issue intent:** - Backend: `first_message` added to the create request with shape-only validation mirroring the `afk_prompt` precedent (whitespace-only → none; `afkPromptMaxBytes` cap → 400 verbatim otherwise), threaded `instanceCreateRequest → StartParams.FirstMessage → LaunchSpec.SeedPrompt` onto the trailing spawn positional. `--settings` still lands before the prompt; `SeedPrompt` carries no run-kind semantics. Covered by `TestAPI_InstanceStartFirstMessage` + `TestStart_firstMessageIsTrailingSpawnPositional` (zero keystroke batches asserted). - Frontend: `req.first_message` is set **before** `startInstance` (correct ordering — no dependence on a post-spawn `run.id`); empty box omits the key. The `queuedMessage` module, its transcript-availability gate, the auto-send effect, the pending bubble, and the `draftSeed` plumbing are fully deleted with all consumers removed (eslint-green CI confirms no dangling refs). - The new `<Match>` for the ready-to-start copy is correctly ordered ahead of the generic locating placeholder and is tightly guarded (`!ended() && transcript()==="locating" && state()==="idle" && messages().length===0`), so it cannot fire for a mid-conversation run or an ended run — verified by the rewritten `RunChat.test.tsx` cases (`state:""` and ended run both keep the locating placeholder). - Hygiene: codex compat record pins the lazy rollout birth with a when-this-breaks recipe; ADR-0025 amendment + ADR-0036 checklist question present. **Verdict: PASS.** No blockers. Awaiting the human's free-text go-ahead to merge.
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/coding-lab#96
No description provided.