fix: reliable pure-API CR/LF submit + ack verification for simple dispatch() - #66
Conversation
…patch() The simple --tty/--text CLI path (dispatch()) did one async_send_text(text + "\n") write and returned ok:true unconditionally. Against a real Claude Code TUI target, bracketed-paste mode can absorb that trailing "\n" as pasted content instead of a submit keystroke, so text lands but Enter never registers -- while the API write still succeeds, making ok:true misleading. Fix: - Route the simple path through send_prompt_with_crlf (already used by dispatch_registered): prompt, CR, and LF are written as three separate iTerm2 API calls, defeating paste-mode absorption without osascript. - Verify the submit actually landed by polling for an observed state transition before reporting ok:true, instead of trusting the write. Added _simple_dispatch_acknowledged, which checks the same _ack_transitioned session-variable signals dispatch_registered uses, plus a screen-content-diff fallback -- needed because a bare/unhooked target has none of the user.worker*/session.isProcessing variables populated (confirmed live against a disposable Claude Code TUI tab: the CR/LF submit fully worked and Claude answered, but variable-only ack falsely reported failure until the screen-diff signal was added). - If no ack is observed, fall back once to the existing AppleScript character-id-13/10 keystroke path (build_applescript_by_tty, new: the simple path has no registered session id to target by), with the fallback's use and any error surfaced explicitly in the response (fallback_used/fallback_error/submit_method) rather than silently. Experimental finding: pure iTerm2-API CR/LF submission is fully reliable against a real Claude Code TUI session (tested live: two /goal prompts sent to a disposable tab running `claude`, both submitted and processed, confirmed via screen dump). It does not require true OS-level keystrokes -- the earlier osascript-only success was masking that the ack signal, not the submit mechanism, was the actual gap. osascript is kept only as a logged last-resort fallback per operator direction to minimize but not remove it. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UzK8G6r2KLZ48fbcjn7jmZ
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 1f520a7e89
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| current_screen = await _screen_text(session) | ||
| if current_screen and current_screen != before_screen: | ||
| return True, latest |
There was a problem hiding this comment.
Do not treat any screen change as a submission ack
On an unhooked TUI—the exact context this fallback targets—the prompt appearing in the input buffer changes the screen even when CR/LF was absorbed and the prompt was never submitted. This condition therefore returns observed_ack: true and suppresses the AppleScript fallback for the original failure mode; screen changes from cursor movement, spinners, or unrelated output can produce the same false positive. The acknowledgement must distinguish submitted/processed output from merely rendered input.
Useful? React with 👍 / 👎.
| tell s to write text "{payload}" without newline | ||
| tell s to write text (character id 13) without newline | ||
| tell s to write text (character id 10) without newline |
There was a problem hiding this comment.
Avoid re-sending the payload during submit recovery
When acknowledgement is unavailable or times out, the pure-API attempt has already written request.text, but this fallback writes the entire payload again. If the first CR/LF left the original prompt queued, the buffer becomes two concatenated prompts; if the first submit actually succeeded but its transition was missed, the goal is executed twice. Recovery should submit the existing buffer without replaying the payload, or otherwise establish that the buffer is empty before sending it again.
Useful? React with 👍 / 👎.
| request, | ||
| allow_applescript_fallback=not args.no_applescript_fallback, | ||
| ) | ||
| print(json.dumps(result, indent=2, sort_keys=True)) |
There was a problem hiding this comment.
Propagate dispatch failure to the CLI exit status
When the newly added acknowledgement check returns ok: false—for example with --no-applescript-fallback, an osascript error, or no observable transition—_run only prints that result and main() still returns 0. Shell callers therefore treat a reported dispatch failure as success, defeating reliable automation around this new failure reporting; carry the result out of the callback and return a nonzero status when ok is false.
Useful? React with 👍 / 👎.
Split error message across two string literals to keep line under 100 chars. The message content is unchanged, just reflowed for formatting compliance.
Summary
--tty/--textCLI dispatch path (dispatch()) did a singleasync_send_text(text + "\n")write and returnedok:trueunconditionally. Against a real Claude Code TUI, bracketed-paste mode can absorb the trailing\nas pasted content rather than a submit keystroke, so the API write succeeds but Enter never registers -- exactly the silent-failure bug reported in production.dispatch()now routes throughsend_prompt_with_crlf(prompt/CR/LF as three separate pure-API writes, same mechanismdispatch_registeredalready relies on) and polls for an observed state transition (_simple_dispatch_acknowledged) before reporting success, instead of trusting the write._ack_transitionedvariable checks, because a target session without the c2 runtime hook / shell-integration wiring has none ofsession.isProcessing/currentCommand/user.worker*populated -- confirmed live (see below).build_applescript_by_tty, since the simple path has no registered session id to target by unique ID). The fallback's use and any error are surfaced explicitly in the response (fallback_used/fallback_error/submit_method) rather than silently, and can be disabled via--no-applescript-fallback.Experimental finding (live test, not just eyeballing)
Created a disposable iTerm2 tab via the Python API (never touched the live coordination tab on
/dev/ttys015), launchedclaudefresh in it, and dispatched two trivial/goalprompts through the fixed pure-API path withallow_applescript_fallback=False:async_get_screen_contentstranscript dump, not just the return value).ok:false/observed_ack:falseeven though the dispatch plainly worked, because the ack check only looked at session variables that were empty in this unhooked tab.ok:true, observed_ack:true, fallback_used:false— the pure-API CR/LF path did not need osascript at all.Conclusion: pure iTerm2-API CR/LF submission fully replaces the osascript character-id-13/10 keystroke path for a Claude Code TUI target. The earlier operator success with osascript was masking a gap in the ack signal, not a limitation of the submit mechanism itself. osascript is retained only as an explicit, logged last-resort fallback per the operator's request to minimize (not remove) its use.
Disposable tab was closed after testing; production tab
/dev/ttys015was never touched.Test plan
python3 -m pytest tests/test_cos_tab_dispatch.py -q— 66 passed / 20 pre-existing failures (identical failure set onmain, unrelatedcoord_agent_idcontract issue)python3 -m pytest tests/ -q— 670 passed vs 668 onmain(net +2 new passing tests, zero regressions; 29 pre-existing failures match baseline exactly)test_dispatch_sends_crlf_separately_and_confirms_ack), AppleScript fallback firing + visibility (test_dispatch_falls_back_to_applescript_when_no_ack_observed), failure reported without silent fallback (test_dispatch_reports_failure_without_fallback)🤖 Generated with Claude Code
https://claude.ai/code/session_01UzK8G6r2KLZ48fbcjn7jmZ