Skip to content

bug(claude): an orphaned prompt write deadlocks the session as a permanent 'human draft' #679

Closed

Description

@Juliusolsson05

Expected behaviour

A prompt delivery that fails should leave the session able to accept the next prompt.

Actual behaviour

After one failed delivery, every subsequent submit is refused forever with prompt input is occupied by a human draft. The session never recovers on its own. Reported symptom: "on every prompt I submitted in the application it said that a human draft occupied it and it did not submit any prompt."

Root cause — the "human" is Agent Code itself

Three individually-defensible decisions compose into a deadlock:

  1. deliverClaudePrompt writes the prompt body into Claude's PTY composer, then waits up to 2s (CONFIRM_TIMEOUT_MS) for Claude to visibly absorb it, detected by screen diff.
  2. On timeout it returns promptWritten: true, enterWritten: false, do-not-retry — deliberately leaving the bytes in the composer. Its own comment: "Prompt bytes are already editable in Claude even when our detector times out. Automatic retry is unsafe: it would append/duplicate those bytes."
  3. Nothing clears them. There is no composer-clearing code path anywhere in the repo.
  4. The composer now genuinely holds text, so parseClaudeComposerState correctly returns drafted.
  5. derivePromptGateState returns occupied / human-draft, and that state is deliberately not time-bounded — a 10s bound was tried and removed because it destroyed real user input mid-sentence.
  6. Every later delivery is refused at before-write in ~32ms.

Nothing is wrong with any single step. The system writes bytes it cannot confirm, abandons them by design, then treats its own abandoned bytes as a human draft it must never overwrite — with no exit.

Evidence

From the lifecycle journal (incidents/runs/*/events.jsonl), the two-event signature:

20:19:40  ca7dbc9c  absorption-timeout  bodyWritten:TRUE  enterWritten:FALSE  2017ms
20:19:59  ca7dbc9c  not-ready           before-write      bodyWritten:false     32ms

Aggregates across all recorded runs:

  • submit.result failures: 88 — not-ready 57, acceptance-timeout 20, absorption-timeout 10
  • deliveries that wrote the body but never sent Enter (orphaned writes): 10
  • gate.eval events: 2,319 — of which 833 (36%) are occupied / human-draft, every one a claude session
  • occupied duration: median 0.3s, but p90 27s, 56 entries >60s, 24 >300s, max 47h
  • 19 of 216 sessions ended with their last gate state occupied — never recovered

Why it was invisible

readiness.ts collapses every non-ready verdict to one string:

'provider-not-ready': 'waiting for agent'

replay-pending, composer-unpainted and human-draft are indistinguishable on screen. Both readiness.ts and session-runtime/state.ts flag this as a known deliberate gap. A latched gate and a slow boot look identical, which is why this needed a full journal dig to identify.

Manual recovery today

Focus the agent's raw terminal view and press Ctrl+U to clear the line. The gate re-evaluates on the next frame and returns ready.

Acceptance criteria

  • A delivery that writes the body but cannot send Enter must not leave the session permanently unable to accept prompts.
  • Whatever the recovery, the prompt text must not be silently destroyed without the user being told.
  • A latched occupied state must be visible in the UI as something other than "waiting for agent".
  • Genuine human drafts must still block automated delivery — the protection this gate exists for cannot regress. The 10s-bound approach is out of scope: it was tried, and it destroys user input.

Related but separate

parseClaudeComposerState(screen, null) returns drafted for Claude's dim placeholder text (e.g. Try "how does CommandPalette.tsx work?"), because the string fallback fails closed. Reproduced against a real captured screen: identical screen returns empty when attrs are supplied, drafted when they are not. That accounts for the ~52% of human-draft events that fire nowhere near a submit. Filed separately if it is not fixed here.

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