What happened: Every peer-chat.py --to codex send is refused before anything is typed — target composer prompt is not recognisable (shell mode, disabled input, a trailing modal or status row, or an unknown prompt glyph); nothing was typed, five times at 10 s, then exit 1 — while the Codex pane is idle with an empty composer. Codex 0.154.0 draws an ambient animated particle field across its composer box: scattered Braille dots on the prompt row itself and on the rows immediately above and below it, drifting continuously. Sends to Claude Code are unaffected.
This is the same class as #540, #553 and #564, but from the Codex side rather than the Claude side — the first one where the agent decorates the rows around the prompt rather than the prompt's content.
Expected vs actual: Expected: an idle Codex pane whose composer holds only the Ask Codex to do anything placeholder is recognised, and the send goes through. Actual: refused pre-write, the message is never typed.
Where it breaks: trailing_input_block (peer-chat.py:462) pops trailing blank lines, pops one footer row, then walks up while lines are non-blank. The particle rows are no longer blank, so the block starts one row too high and CODEX_PROMPT_RE.match(block[0]) in codex_live_prompt_text (:476) sees a row of dots instead of ›. live_prompt_text returns None and send (:933) raises PromptBlocked.
Pane tail at the moment of refusal, blank rows marked:
4: [BLANK]
5: [⠁ ⠈ ⠄ ⠁ ⠁ ⠐ ⠐ ] <- particles
6: [› Ask Codex to do anything⡀ ⠂ ⠁] <- prompt
7: [ ⠠⢀ ⠠ ⠄ ⢀ ⡀ ] <- particles
8: [ gpt-6-astra medium · ~/Code/overgear-vpn · main · ...] <- footer
Three separate breakages, and the first one is the least important. Restoring only the block boundary would move the failure later, not remove it:
- Block boundary, above.
composer_is_empty (:521) compares against the exact string Ask Codex to do anything. Particles land on the prompt row too (row 6 above), so the content check fails even once the block starts in the right place.
wait_for_composer_change (:543) requires the composer state to hold still for settle_delay within PROBE_TIMEOUT 3.0 s. The animation redraws about every 150 ms (measured below), so the 0.5 s window this function is given before submit can never be met.
(3) is on the body-write path, not the preflight, and it is the reason a preflight-only relaxation would make things worse rather than better. type_body (:611) uses CHUNK_SETTLE_DELAY 0.1 s after every chunk but COMPOSER_SETTLE_DELAY 0.5 s after the last one. A ~150 ms redraw period satisfies 0.1 s, so every intermediate chunk would verify normally and the send would run to the final step — then fail at message marker after chunk N/N was not confirmably removed; submit withheld, with the whole message typed into Codex's composer and only the cleanup backspaces standing between that and a stranded draft. That is the #502 failure mode, reached at the last possible moment. Whatever shape the fix takes, it has to cover the verification path, not just the gate.
Steps to reproduce:
- Codex 0.154.0 in the right pane, Claude Code in the left.
- Let Codex finish a turn and leave the pane idle. The particle animation runs on and around the composer row (it is not a splash-screen effect; it is still there after a completed turn).
- From the Claude pane,
peer-chat.py --to codex --stdin <<'CHAT' … CHAT.
- Refused with
target composer prompt is not recognisable …; nothing was typed, five attempts, exit 1.
On a possible discriminator: the particle glyphs are a clean signal. Sampling the composer box repeatedly (12 reads of the three rows above the footer) yields exactly one non-ASCII glyph that is not a particle — U+203A, the › prompt itself — and otherwise only U+2801, U+2802, U+2804, U+2808, U+2810, U+2820, U+2840 and U+2880: all eight single-dot Braille patterns, nothing else from the block, and nothing outside it. Dropping that block from the pane text before composer state is derived would restore all three checks at once. I am not proposing that relaxation: it would make any message body containing Braille invisible to the same checks, and that trade is yours.
The animation does not stop for a draft, and its period is measured. With ыв typed into the composer by hand and left there, particles keep drifting across the row, including into the prompt-glyph column itself:
'›⠁ыв ⠠ ⠁ ⡀ ⠈ ⠂ ⠁'
'›⠁ыв ⠠ ⠁ ⠈ ⠂ ⠁'
'›⠁ыв ⠠ ⠁'
Polling session text as fast as it returns — 526 reads in 6 s, ~11 ms each — the three composer rows changed 39 times, with a gap between redraws of min 141 ms, median 151 ms, max 163 ms. So the state is never still for 0.5 s, and breakage (3) is real rather than hypothetical.
No user-side workaround. #564 had "promptSuggestionEnabled": false on the Claude side. Codex has no equivalent that I can find: codex features list shows no flag matching ambient|anim|particle|splash|idle|composer — the only near hit is prevent_idle_sleep, which is unrelated.
Environment: agterm 0.28.0 (6305191), macOS 26.6.2 (25G83), Codex CLI 0.154.0, Claude Code 2.1.268, Python 3.14.3, peer-chat.py byte-identical to cookbook/two-agent-chat/peer-chat.py at 9dd5294.
What happened: Every
peer-chat.py --to codexsend is refused before anything is typed —target composer prompt is not recognisable (shell mode, disabled input, a trailing modal or status row, or an unknown prompt glyph); nothing was typed, five times at 10 s, then exit 1 — while the Codex pane is idle with an empty composer. Codex 0.154.0 draws an ambient animated particle field across its composer box: scattered Braille dots on the prompt row itself and on the rows immediately above and below it, drifting continuously. Sends to Claude Code are unaffected.This is the same class as #540, #553 and #564, but from the Codex side rather than the Claude side — the first one where the agent decorates the rows around the prompt rather than the prompt's content.
Expected vs actual: Expected: an idle Codex pane whose composer holds only the
Ask Codex to do anythingplaceholder is recognised, and the send goes through. Actual: refused pre-write, the message is never typed.Where it breaks:
trailing_input_block(peer-chat.py:462) pops trailing blank lines, pops one footer row, then walks up while lines are non-blank. The particle rows are no longer blank, so the block starts one row too high andCODEX_PROMPT_RE.match(block[0])incodex_live_prompt_text(:476) sees a row of dots instead of›.live_prompt_textreturnsNoneandsend(:933) raisesPromptBlocked.Pane tail at the moment of refusal, blank rows marked:
Three separate breakages, and the first one is the least important. Restoring only the block boundary would move the failure later, not remove it:
composer_is_empty(:521) compares against the exact stringAsk Codex to do anything. Particles land on the prompt row too (row 6 above), so the content check fails even once the block starts in the right place.wait_for_composer_change(:543) requires the composer state to hold still forsettle_delaywithinPROBE_TIMEOUT3.0 s. The animation redraws about every 150 ms (measured below), so the 0.5 s window this function is given before submit can never be met.(3) is on the body-write path, not the preflight, and it is the reason a preflight-only relaxation would make things worse rather than better.
type_body(:611) usesCHUNK_SETTLE_DELAY0.1 s after every chunk butCOMPOSER_SETTLE_DELAY0.5 s after the last one. A ~150 ms redraw period satisfies 0.1 s, so every intermediate chunk would verify normally and the send would run to the final step — then fail atmessage marker after chunk N/N was not confirmably removed; submit withheld, with the whole message typed into Codex's composer and only the cleanup backspaces standing between that and a stranded draft. That is the #502 failure mode, reached at the last possible moment. Whatever shape the fix takes, it has to cover the verification path, not just the gate.Steps to reproduce:
peer-chat.py --to codex --stdin <<'CHAT' … CHAT.target composer prompt is not recognisable …; nothing was typed, five attempts, exit 1.On a possible discriminator: the particle glyphs are a clean signal. Sampling the composer box repeatedly (12 reads of the three rows above the footer) yields exactly one non-ASCII glyph that is not a particle — U+203A, the
›prompt itself — and otherwise only U+2801, U+2802, U+2804, U+2808, U+2810, U+2820, U+2840 and U+2880: all eight single-dot Braille patterns, nothing else from the block, and nothing outside it. Dropping that block from the pane text before composer state is derived would restore all three checks at once. I am not proposing that relaxation: it would make any message body containing Braille invisible to the same checks, and that trade is yours.The animation does not stop for a draft, and its period is measured. With
ывtyped into the composer by hand and left there, particles keep drifting across the row, including into the prompt-glyph column itself:Polling
session textas fast as it returns — 526 reads in 6 s, ~11 ms each — the three composer rows changed 39 times, with a gap between redraws of min 141 ms, median 151 ms, max 163 ms. So the state is never still for 0.5 s, and breakage (3) is real rather than hypothetical.No user-side workaround. #564 had
"promptSuggestionEnabled": falseon the Claude side. Codex has no equivalent that I can find:codex features listshows no flag matchingambient|anim|particle|splash|idle|composer— the only near hit isprevent_idle_sleep, which is unrelated.Environment: agterm 0.28.0 (6305191), macOS 26.6.2 (25G83), Codex CLI 0.154.0, Claude Code 2.1.268, Python 3.14.3,
peer-chat.pybyte-identical tocookbook/two-agent-chat/peer-chat.pyat 9dd5294.