fix(spawn): manual first message rides SpawnSpec.InitialPrompt — retire the queued-message transcript gate (#96) #97

Merged
dominik.polakovics merged 1 commit from afk/96 into main 2026-07-10 14:41:59 +02:00

Problem

An interactive codex spawn never connects: codex-cli 0.133.0's TUI births its rollout transcript at first-turn start, not session init. The issue-#41 client-side queued first message auto-sent only once the transcript was located — so the message that would create the transcript waited for the transcript. Deadlock by construction, and the parked text lived only in browser memory (lost on reload). claude-code was unaffected only because its transcript exists from process start, letting transcript availability double as a TUI-readiness proxy.

Fix (maintainer-approved direction from the issue)

  1. BackendPOST /api/v1/repos/{id}/instances gains optional first_message: shape-only validation per the ADR-0027 precedent (whitespace-only → none; 16 KiB argv cap → 400, reusing afkPromptMaxBytes; otherwise verbatim), threaded instanceCreateRequestStartParams.FirstMessageLaunchSpec.SeedPrompt — the AFK-proven spawn-argv trailing positional, present before the process starts. No post-spawn keystroke, no cold-start race; the first message starts the first turn, which births the rollout, and the chat connects on the next poll. Injected flags (--settings) still land before the trailing prompt. SeedPrompt carries no run-kind semantics; Kind alone decides AFK vs manual (comments updated).
  2. Frontend — the New-run composer text rides the create call; the queued-message module (web/src/lib/queuedMessage.ts), its transcript-availability gate, the auto-send effect, the "Sends when … is ready" pending bubble, and the draft-seed plumbing are deleted. Repo-wide grep for queue identifiers is zero-hit — no path waits on a transcript to deliver the operator's first message.
  3. Frontend — an ACTIVE run whose adapter-composed state is idle with no messages and an unlocated transcript trusts the adapter state (ADR-0038) and shows "Ready — your first message starts the conversation." with a usable composer (verified ungated in that state), instead of deriving "Waiting for the transcript…" from the missing transcript identity. The locating placeholder still stands for state === '' (pre-first-poll) and ended runs.
  4. Hygiene — the codex compat record §5 pins the lazy rollout birth (idle TUI ⇒ no file, no open fd; codex exec writes at once; live evidence from the #87 spike, with a when-this-breaks recipe); the ADR-0036 Tier-2 transcript spike now asks when is the transcript born; ADR-0025's queued-first-message decision carries a dated retirement amendment in the house square-bracket style.

Acceptance criteria → evidence

  • Composer text → trailing positional, no manual keystroke: TestAPI_InstanceStartFirstMessage (verbatim InitialPrompt via recorded SpawnSpecs) + TestStart_firstMessageIsTrailingSpawnPositional (trailing positional, --settings before it, zero keystroke batches).
  • claude-code delivers exactly once: argv-only delivery; queue deleted; NewRun.test.tsx asserts exactly one POST carrying first_message.
  • No text → ready-to-start copy + usable composer: RunChat.test.tsx (active-idle shows the copy, composer present, no reply POST; state '' keeps the locating placeholder; an ended run never shows it).
  • Empty composer appends no positional: Tier-1 spawn-argv obligation untouched; whitespace-only normalizes to "" (test); empty box sends no first_message key (test).
  • Suite: go test ./... green except the pre-existing sandbox-environmental internal/tmuxx integration failures (fail identically on clean main — no runnable tmux here); go build ./... and go build -tags ui ./cmd/... green; golangci-lint run (2.12.2, CI's pin) 0 issues; gofmt clean; web: 557/557 vitest, eslint clean, prettier clean, vite build green.

Notes for review

  • Browsers cannot run in this sandbox, so the ready-to-start copy and New-run flow were verified through jsdom component tests, not a live browser — worth an eyeball on / (New-run) and /runs/:id for an idle codex instance.
  • Durability of an unsent draft across reloads is out of scope per the issue (argv delivery already covers the spawn-time message).

Closes #96

🤖 Generated with Claude Code

https://claude.ai/code/session_01Poub8fVRSFNrYq1PgMAtWt

## Problem An interactive codex spawn never connects: codex-cli 0.133.0's TUI births its rollout transcript at **first-turn start**, not session init. The issue-#41 client-side queued first message auto-sent only once the transcript was located — so the message that would create the transcript waited for the transcript. Deadlock by construction, and the parked text lived only in browser memory (lost on reload). claude-code was unaffected only because its transcript exists from process start, letting transcript availability double as a TUI-readiness proxy. ## Fix (maintainer-approved direction from the issue) 1. **Backend** — `POST /api/v1/repos/{id}/instances` gains optional `first_message`: shape-only validation per the ADR-0027 precedent (whitespace-only → none; 16 KiB argv cap → 400, reusing `afkPromptMaxBytes`; otherwise verbatim), threaded `instanceCreateRequest` → `StartParams.FirstMessage` → `LaunchSpec.SeedPrompt` — the AFK-proven spawn-argv trailing positional, present before the process starts. No post-spawn keystroke, no cold-start race; the first message starts the first turn, which births the rollout, and the chat connects on the next poll. Injected flags (`--settings`) still land before the trailing prompt. `SeedPrompt` carries no run-kind semantics; `Kind` alone decides AFK vs manual (comments updated). 2. **Frontend** — the New-run composer text rides the create call; the queued-message module (`web/src/lib/queuedMessage.ts`), its transcript-availability gate, the auto-send effect, the "Sends when … is ready" pending bubble, and the draft-seed plumbing are **deleted**. Repo-wide grep for queue identifiers is zero-hit — no path waits on a transcript to deliver the operator's first message. 3. **Frontend** — an ACTIVE run whose adapter-composed state is idle with no messages and an unlocated transcript trusts the adapter state (ADR-0038) and shows **"Ready — your first message starts the conversation."** with a usable composer (verified ungated in that state), instead of deriving "Waiting for the transcript…" from the missing transcript identity. The locating placeholder still stands for `state === ''` (pre-first-poll) and ended runs. 4. **Hygiene** — the codex compat record §5 pins the lazy rollout birth (idle TUI ⇒ no file, no open fd; `codex exec` writes at once; live evidence from the #87 spike, with a when-this-breaks recipe); the ADR-0036 Tier-2 transcript spike now asks *when is the transcript born*; ADR-0025's queued-first-message decision carries a dated retirement amendment in the house square-bracket style. ## Acceptance criteria → evidence - Composer text → trailing positional, no manual keystroke: `TestAPI_InstanceStartFirstMessage` (verbatim `InitialPrompt` via recorded SpawnSpecs) + `TestStart_firstMessageIsTrailingSpawnPositional` (trailing positional, `--settings` before it, **zero** keystroke batches). - claude-code delivers exactly once: argv-only delivery; queue deleted; `NewRun.test.tsx` asserts exactly one POST carrying `first_message`. - No text → ready-to-start copy + usable composer: `RunChat.test.tsx` (active-idle shows the copy, composer present, no reply POST; `state ''` keeps the locating placeholder; an ended run never shows it). - Empty composer appends no positional: Tier-1 `spawn-argv` obligation untouched; whitespace-only normalizes to `""` (test); empty box sends no `first_message` key (test). - Suite: `go test ./...` green except the pre-existing sandbox-environmental `internal/tmuxx` integration failures (fail identically on clean main — no runnable tmux here); `go build ./...` and `go build -tags ui ./cmd/...` green; `golangci-lint run` (2.12.2, CI's pin) 0 issues; gofmt clean; web: 557/557 vitest, eslint clean, prettier clean, `vite build` green. ## Notes for review - Browsers cannot run in this sandbox, so the ready-to-start copy and New-run flow were verified through jsdom component tests, not a live browser — worth an eyeball on `/` (New-run) and `/runs/:id` for an idle codex instance. - Durability of an *unsent* draft across reloads is out of scope per the issue (argv delivery already covers the spawn-time message). Closes #96 🤖 Generated with [Claude Code](https://claude.com/claude-code) https://claude.ai/code/session_01Poub8fVRSFNrYq1PgMAtWt
An interactive codex spawn never connected (issue #96): codex-cli 0.133.0's
TUI births its rollout transcript at first-turn start, not session init, so
the issue-#41 client-side queued first message — auto-sent only once the
transcript was located — deadlocked by construction: the message that would
create the transcript waited for the transcript. The parked text also lived
only in browser memory, lost on reload. claude-code was unaffected only
because its transcript exists from process start, which let transcript
availability double as a TUI-readiness proxy.

- httpapi: POST /repos/{id}/instances gains optional first_message —
  shape-only validation per the ADR-0027 precedent (whitespace-only → none,
  16 KiB argv cap → 400, otherwise verbatim), threaded through
  StartParams.FirstMessage into LaunchSpec.SeedPrompt, the AFK-proven
  spawn-argv trailing positional (present before the process starts — no
  post-spawn keystroke, no cold-start race). An empty message appends
  nothing; the Tier-1 empty-InitialPrompt obligation stays pinned.
- web: the New-run composer text rides the create call; the queued-message
  module, its transcript-availability gate, auto-send effect, pending
  bubble, and draft-seed plumbing are deleted. No code path waits on a
  transcript to deliver the operator's first message.
- web: an ACTIVE run whose adapter-composed state is idle with no messages
  and an unlocated transcript trusts the adapter state (ADR-0038) and shows
  "Ready — your first message starts the conversation." instead of the
  transcript-derived "Waiting for the transcript…" placeholder; the
  composer is usable from that state.
- compat/docs: the codex compat record pins the lazy rollout birth (idle
  TUI ⇒ no file; codex exec writes at once) with a when-this-breaks recipe;
  the ADR-0036 Tier-2 transcript spike now asks when the transcript is
  born; ADR-0025's queued-first-message decision carries a retirement
  amendment.

Closes #96

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Poub8fVRSFNrYq1PgMAtWt
Sign in to join this conversation.
No reviewers
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!97
No description provided.