Context
Follow-up to #12.
The queue-vs-submit rule adopted there is directionally correct, but downstream dogfood exposed a second state-model bug: recent transcript activity is a reachability/liveness signal, not proof that a Codex turn is currently running.
A target can have a completed turn while its transcript is still inside the freshness window. If the adapter treats that effective active value as current turn activity, it sends Tab to an idle Codex composer. The envelope is written into the input box but is not submitted to the transcript.
The inverse failure also occurs: for a genuinely busy turn, a successful Tab queue may remain absent from the transcript until the turn finishes. An early grep miss is therefore not non-delivery evidence. Retrying at that point can produce duplicate decisions when the original queue later lands.
Root cause
Two independent axes were collapsed into one state:
- Reachability/liveness — registry declaration plus transcript freshness; answers whether to use a live pane or stopped-session resume.
- Current submit activity — whether the Codex TUI is currently executing a turn; answers whether to send Tab or Enter.
A second collapse happened in the result model: one queued-unverified value covered actions with opposite caller behavior (wait, inspect, recover an existing composer, or safely retry).
Proposed contract
1. Separate reachability from submit activity
For a reachable Codex pane, derive submit activity from the latest complete Codex turn-boundary event:
- latest
task_started → active → send Tab once;
- latest
task_complete → idle → send Enter;
- no trustworthy boundary → do not guess.
A non-empty, unterminated JSONL tail must be treated as a concurrent-write/unknown state. Do not skip it and reuse an older boundary.
Refresh submit activity after any pane settle delay, immediately before the key. If an unverified Enter would be retried, refresh again and retry only while still idle.
2. Report outcomes by observed action
Do not use names such as will-land or stranded, which predict a future result without evidence. Return an explicit caller action instead:
delivery |
Meaning |
Caller action |
pre-injection-rejected |
turn state unknown; zero pane commands |
retry after state becomes readable (retry_safe=true) |
delivered |
this envelope nonce appears after the pre-send transcript boundary |
await semantic peer ACK |
queued-for-next-turn |
active Codex Tab command executed; nonce not yet present |
wait for turn boundary; do not resend |
submit-unverified |
Enter executed; nonce not observed |
inspect target or await ACK; do not blindly resend |
composer-unsubmitted |
pane text written; refreshed state unknown; no key sent |
recover the existing composer; do not rewrite envelope |
write-unverified |
pane write command returned non-zero or timed out |
inspect target; do not resend |
submit-command-unverified |
submit-key command returned non-zero or timed out |
inspect target or await ACK; do not blindly resend |
Every structured pane result should include:
submit_action
recommended_action
retry_safe
verification_guidance
- the unique envelope nonce
- message-specific evidence (
envelope-nonce-found or none)
- semantic
acknowledged=false until a peer reply
Only a mechanically proven zero-pane-command path may set retry_safe=true.
For queued-for-next-turn, explicitly state that an early transcript miss is not non-delivery evidence. A caller must not use one early grep miss to justify reinjection.
3. Define fail-closed behavior by phase
fail closed is ambiguous unless the transport side effect is stated:
- unknown before pane write → zero pane commands, structured
pre-injection-rejected, non-zero exit;
- unknown after pane write → no submit key, structured
composer-unsubmitted, non-zero exit;
- target becomes active/unknown after an Enter attempt → no second Enter,
submit-unverified;
- pane command non-zero/timeout → preserve nonce and attempted stage, structured uncertain result, non-zero exit, explicit do-not-resend action.
Dogfood evidence
The downstream adopter reproduced both directions:
- A recently completed target was still derived as reachable/active by freshness. Tab left the envelope in the idle composer. One Enter submitted the existing envelope and immediately unblocked the waiting workflow.
- A truly busy target had no nonce in an early transcript check, but the original Tab queue appeared after the turn ended. A premature reinjection produced duplicate content.
The adopter implementation now has 34 focused AgentTUI tests and passed an independent challenge review covering:
- effective active + latest
task_complete → Enter;
- latest
task_started → Tab;
- active→idle transition during settle;
- partial JSONL tail → unknown;
- no second Enter after active/unknown transition;
- nonce-only delivery evidence;
- every outcome and caller action above;
- pane write/submit non-zero and timeout paths.
Acceptance criteria
Context
Follow-up to #12.
The queue-vs-submit rule adopted there is directionally correct, but downstream dogfood exposed a second state-model bug: recent transcript activity is a reachability/liveness signal, not proof that a Codex turn is currently running.
A target can have a completed turn while its transcript is still inside the freshness window. If the adapter treats that
effective activevalue as current turn activity, it sends Tab to an idle Codex composer. The envelope is written into the input box but is not submitted to the transcript.The inverse failure also occurs: for a genuinely busy turn, a successful Tab queue may remain absent from the transcript until the turn finishes. An early grep miss is therefore not non-delivery evidence. Retrying at that point can produce duplicate decisions when the original queue later lands.
Root cause
Two independent axes were collapsed into one state:
A second collapse happened in the result model: one
queued-unverifiedvalue covered actions with opposite caller behavior (wait, inspect, recover an existing composer, or safely retry).Proposed contract
1. Separate reachability from submit activity
For a reachable Codex pane, derive submit activity from the latest complete Codex turn-boundary event:
task_started→ active → send Tab once;task_complete→ idle → send Enter;A non-empty, unterminated JSONL tail must be treated as a concurrent-write/unknown state. Do not skip it and reuse an older boundary.
Refresh submit activity after any pane settle delay, immediately before the key. If an unverified Enter would be retried, refresh again and retry only while still idle.
2. Report outcomes by observed action
Do not use names such as
will-landorstranded, which predict a future result without evidence. Return an explicit caller action instead:deliverypre-injection-rejectedretry_safe=true)deliveredqueued-for-next-turnsubmit-unverifiedcomposer-unsubmittedwrite-unverifiedsubmit-command-unverifiedEvery structured pane result should include:
submit_actionrecommended_actionretry_safeverification_guidanceenvelope-nonce-foundornone)acknowledged=falseuntil a peer replyOnly a mechanically proven zero-pane-command path may set
retry_safe=true.For
queued-for-next-turn, explicitly state that an early transcript miss is not non-delivery evidence. A caller must not use one early grep miss to justify reinjection.3. Define fail-closed behavior by phase
fail closedis ambiguous unless the transport side effect is stated:pre-injection-rejected, non-zero exit;composer-unsubmitted, non-zero exit;submit-unverified;Dogfood evidence
The downstream adopter reproduced both directions:
The adopter implementation now has 34 focused AgentTUI tests and passed an independent challenge review covering:
task_complete→ Enter;task_started→ Tab;Acceptance criteria