Repository navigation
Claude prompt submit still inconsistent — detection logic falls behind under load #90
Description
What happened
Sending a prompt to Claude is still unreliable. Sometimes the text reaches the CLI and submits cleanly, sometimes it gets typed but never submitted, sometimes it gets partially typed. The submit-detection logic that watches the screen / paste accumulator can fall behind, especially after large pastes or under heavy IPC load.
This persists despite three prior fixes:
- PR feat(paste): event-driven submit + per-paste debug dump + headless quirks docs #71: event-driven submit + per-paste debug dump
- PR fix(paste): lower threshold to 100 — Claude accumulator engages well below 800 #72: lowered CLAUDE_PASTE_THRESHOLD to 100 (accumulator engages well below 800)
- PR fix: hardcode event-driven paste submit #80: hardcoded event-driven paste submit (removed the 125 ms setting that never worked)
Repro
Non-deterministic. Most reliably reproduced by:
- Open a Claude pane.
- Paste a medium-to-large prompt (a few KB, more than just a few lines).
- Hit submit — sometimes via Enter, sometimes via Cmd+Enter — depending on the composer state.
- Observe: enter occasionally fails to commit, or commits but the screen-parser hasn't transitioned to streaming yet so the UI still shows the composer as un-sent.
Happens more frequently when many sessions are active and IPC is busy.
Where
- `src/renderer/src/workspace/tile-tree/TileLeaf/claudePaste.ts` (renderer paste handler / accumulator)
- `src/renderer/src/workspace/tile-tree/TileLeaf/useClaudeImagePaste.ts` (image-paste pipeline)
- `packages/claude-code-headless/src/screen/...` (TUI submit-detection parsers — exact file TBD)
- `packages/claude-code-headless/src/proxy/ClaudeProxyAdapter.ts` (semantic-event boundaries)
Output
No fatal trace — this is a UX-reliability issue, not a crash. Per-paste debug dumps under `~/.config/agent-code/debug-bundles/` capture the relevant chunk/threshold/timestamps when reproduced.
State
Seen with 5–20 active agents, both fresh and resumed sessions, both small (a few hundred chars) and large (tens of KB) pastes. Worse when AGENT_CODE_PERF is on (more IPC traffic = more chance for the screen-parser to lag).
Sketch
The recurring failure mode suggests detection is still racing against the screen state rather than coupled to a determinate event. Options:
- Detect submit completion off the JSONL transcript tailer rather than the screen parser. Claude writes the user turn as a JSONL entry the instant it commits — that's the authoritative signal. Screen-parsing is a fallback for environments where JSONL isn't available, but in Agent Code we always have it.
- Add a watchdog: if submit was issued but no JSONL entry / screen transition appears within ~2 s, surface a "submit may have failed — retry?" affordance in the composer rather than leaving the user wondering.
- Capture timing pairs (submit-issued → submit-detected) in the existing per-paste debug log so we can see the actual latency distribution before tuning further.
Activity
Merge pull request #278 from Juliusolsson05/feat/claude-paste-detecti…
Merge pull request #280 from Juliusolsson05/fix/paste-submit-confirma…
Juliusolsson05 commentedon Jul 8, 2026
Resolved by #498 (merged to
main). Closing with a full writeup of what was actually wrong, since this one resisted three prior fixes precisely because it was mis-diagnosed as a timing/load problem when the core was a detection-signal mismatch.Root cause (finally characterized, not guessed)
There are two independent decisions in Claude paste-submit, and they disagreed:
input.includes('\n') || input.length > 100, so it takes the bracketed-paste route.[Pasted text #N]placeholder for pastes it collapses, and its collapse threshold is much higher than our 100.The old confirm step waited for the
[Pasted text #N]placeholder only. So for the entire band between "our route threshold (100)" and "Claude's collapse threshold", Claude inlines the paste as raw text with no placeholder ever — the detector timed out and (renderer) fired a blind\rthat raced the accumulator, or (runtime) withheld\rentirely. Either way: prompt sits in the composer, Claude looks busy, no turn starts.We characterized this against a real
claudePTY (harness intmp/paste-repro/), driving pastes across lengths/shapes and watching the live composer buffer:[Pasted text #N]?Consistent across repeated runs. The collapse boundary is Claude's, not ours, and unrelated to the 100 we route on.
Why it kept coming back / was so hard to diagnose
\ris byte-identical to a successful one — we loggedwrite:submit-crandsubmit:returnedbut never verified a turn actually started. The user-facing failure ("had to press Enter twice") produced no event.claudePaste.ts) had been fixed to confirm on placeholder OR inline-tail (waitForPasteAbsorbed), which closed Claude paste submit: inlined (non-placeholder) pastes race the accumulator — blind \r gets swallowed, no turn starts #279 — but the runtime/remote delivery path (promptDelivery.ts) still hard-required the placeholder. So the remote/mobile companion hit the original bug on every short prompt, and on every dictated prompt (the<stt>…</stt>wrapper adds newlines → always the paste route). "The desktop breaks too" reports were people seeing that withheld paste sitting unsent in the desktop composer.[Pasted text #N]with no baseline — so once paste refactor: reshape tree, break up god-files, go all-absolute imports #1 left a stale[Pasted text #1]on screen, paste Fix post-refactor permission and feed rendering regressions #2 false-confirmed on it at 0ms and fired\rprematurely. That's the "first prompt works, second fails" pattern.The fix (#498)
@shared/claude/pasteConfirm) used by both the desktop composer and the runtime delivery path, so they can't drift again.AgentSession.snapshotScreen→snapshotPlain), which is load-independent — no debounced/IPC'd screen to fall behind.promptDelivery.test.tspins the inline-tail path, the stale-placeholder sequence guard, and Enter-withheld-on-timeout; and the actual shared detector was run end-to-end against realclaude— short single-line, short multi-line, and dictated<stt>prompts all now confirm in ~11–24ms (were 2000ms timeouts).Not in scope of #498 (deliberately)
The original sketch also floated JSONL-transcript-based submit detection and a watchdog + auto-retry. #498 didn't need them — confirming on the live inline/placeholder signal is deterministic and fast — so the historic "prompt won't send" symptom is fixed on both paths without them. If we ever want belt-and-suspenders (e.g. auto-resend
\rif no turn-start evidence within ~300ms as an extra safety net), that's a small, focused follow-up rather than a reason to keep this umbrella open.Closing as resolved by #498. (#279 was the isolated renderer sub-case, already closed; #498 completed the same idea on the runtime/remote path and unified both.)