Skip to content

fix: reliable pure-API CR/LF submit + ack verification for simple dispatch() - #66

Merged
CondorCommodore merged 3 commits into
mainfrom
fix-tab-dispatch-api-only
Aug 11, 2026
Merged

fix: reliable pure-API CR/LF submit + ack verification for simple dispatch()#66
CondorCommodore merged 3 commits into
mainfrom
fix-tab-dispatch-api-only

Conversation

@CondorCommodore

Copy link
Copy Markdown
Owner

Summary

  • The simple --tty/--text CLI dispatch path (dispatch()) did a single async_send_text(text + "\n") write and returned ok:true unconditionally. Against a real Claude Code TUI, bracketed-paste mode can absorb the trailing \n as 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 through send_prompt_with_crlf (prompt/CR/LF as three separate pure-API writes, same mechanism dispatch_registered already relies on) and polls for an observed state transition (_simple_dispatch_acknowledged) before reporting success, instead of trusting the write.
  • Added a screen-content-diff fallback signal alongside the existing _ack_transitioned variable checks, because a target session without the c2 runtime hook / shell-integration wiring has none of session.isProcessing/currentCommand/user.worker* populated -- confirmed live (see below).
  • If no ack is observed, falls back once to the existing AppleScript character-id-13/10 keystroke path (new 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), launched claude fresh in it, and dispatched two trivial /goal prompts through the fixed pure-API path with allow_applescript_fallback=False:

  • Both submits landed and were fully processed by Claude (confirmed via async_get_screen_contents transcript dump, not just the return value).
  • The first run showed ok:false/observed_ack:false even though the dispatch plainly worked, because the ack check only looked at session variables that were empty in this unhooked tab.
  • After adding the screen-diff fallback signal, the second run returned 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/ttys015 was never touched.

Test plan

  • python3 -m pytest tests/test_cos_tab_dispatch.py -q — 66 passed / 20 pre-existing failures (identical failure set on main, unrelated coord_agent_id contract issue)
  • python3 -m pytest tests/ -q — 670 passed vs 668 on main (net +2 new passing tests, zero regressions; 29 pre-existing failures match baseline exactly)
  • New tests: CR/LF-split write + ack success (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)
  • Live smoke test against a real Claude Code TUI in a disposable tab (see above)

🤖 Generated with Claude Code

https://claude.ai/code/session_01UzK8G6r2KLZ48fbcjn7jmZ

…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

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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".

Comment on lines +549 to +551
current_screen = await _screen_text(session)
if current_screen and current_screen != before_screen:
return True, latest

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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 👍 / 👎.

Comment on lines +362 to +364
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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge 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))

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge 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.
@CondorCommodore
CondorCommodore merged commit 6894ba2 into main Aug 11, 2026
5 checks passed
@CondorCommodore
CondorCommodore deleted the fix-tab-dispatch-api-only branch August 11, 2026 12:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant