Skip to content

Claude prompt submit still inconsistent — detection logic falls behind under load #90

Closed

Description

@Juliusolsson05

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:

Repro

Non-deterministic. Most reliably reproduced by:

  1. Open a Claude pane.
  2. Paste a medium-to-large prompt (a few KB, more than just a few lines).
  3. Hit submit — sometimes via Enter, sometimes via Cmd+Enter — depending on the composer state.
  4. 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:

  1. 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.
  2. 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.
  3. 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

  1. added 2 commits that reference this issue on Jun 14, 2026
  2. Juliusolsson05 commented on Jul 8, 2026

    @Juliusolsson05
    OwnerAuthor

    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:

    1. Our routing decision — a prompt is "paste-like" if input.includes('\n') || input.length > 100, so it takes the bracketed-paste route.
    2. Claude's rendering decision — Claude's TUI only renders the [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 \r that raced the accumulator, or (runtime) withheld \r entirely. Either way: prompt sits in the composer, Claude looks busy, no turn starts.

    We characterized this against a real claude PTY (harness in tmp/paste-repro/), driving pastes across lengths/shapes and watching the live composer buffer:

    paste Claude renders it as [Pasted text #N]? old behavior
    single-line ≤ ~800 chars inline never timeout → stuck
    short multi-line (2–3 lines) inline never timeout → stuck
    single-line > ~800 / multi-line ≥ ~4 lines collapse yes (~10–20ms) worked

    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

    The fix (#498)

    • Consolidated the detection into one shared module (@shared/claude/pasteConfirm) used by both the desktop composer and the runtime delivery path, so they can't drift again.
    • Confirm on new-placeholder OR newly-inlined-tail, against a baseline captured before the paste (the baseline also makes paste Fix post-refactor permission and feed rendering regressions #2 immune to paste refactor: reshape tree, break up god-files, go all-absolute imports #1's stale placeholder).
    • The runtime path reads a live in-process screen snapshot (AgentSession.snapshotScreen → snapshotPlain), which is load-independent — no debounced/IPC'd screen to fall behind.
    • Verified: promptDelivery.test.ts pins 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 real claude — 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 \r if 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.)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions