Skip to content

fix(claude): seal an Esc-severed stream, and keep a sleep a sleep (#1040) - #1057

Merged
Juliusolsson05 merged 4 commits into
mainfrom
fix/claude-esc-seal-app
Sep 20, 2026
Merged

Juliusolsson05 merged 4 commits into
mainfrom
fix/claude-esc-seal-app

Conversation

@Juliusolsson05

Copy link
Copy Markdown
Owner

Closes #1040. Bumps claude-code-headless to its merged fix (#61) and wires the app half.

The bug

Press Esc while Claude is streaming and Claude Code closes the HTTP connection. mitmproxy reports that only through its error hook — the stream tap's end-of-stream call, which becomes response-end, runs on a normal end of message. The addon had no error hook, so the adapter never tore the flow down and kept the phase it last published.

What that looked like:

  • a pane saying Thinking until the next prompt;
  • a goal loop whose turn ended by Esc had no idle edge to resume from — pinned as a known limit in GoalLoopService, because the fix belonged in the package.

The package half (#61, merged)

error(flow) emits response-error — allowed hosts only, flow id and mitmproxy's message and nothing else, because this hook fires for flows that never reached request() and a probe captured a full URL with a query token from a host we do not proxy for. The adapter seals the flow as its stale-flow watchdog would, but immediately: turn stopped, finishTurn with whatever text arrived, phase idle, flow released so a retry drives the UI.

Four review rounds also established:

  • a turn that already stopped keeps its phase — a tool message ends cleanly and leaves the pane awaiting-tool while the tool runs locally, and an upstream socket failure says nothing about whether that tool is still running;
  • the interruption is neutral: mitmproxy hands the hook Client disconnected. for its own inactivity timeout too (reproduced with tcp_timeout=1), so attributing it to the user would be a guess the UI displays as fact.

The app half (this PR)

ClaudeSession.noteSystemSuspension now tells the adapter the suspension instant immediately, rather than only when the 60 s seal grace expires (#963). A sleep-severed stream usually reports its own death first — a transport error seconds after wake — and whichever signal arrives first decides what the user is told. Without this the turn was reported as a transport failure, the later seal found no flow left to attribute, and the shared desktop/phone ledger lost "Interrupted while asleep."

Tests

  • Package: ClaudeProxyAdapter.clientDisconnect.test.ts (7 cases) and mitmAddon.error.test.ts, which runs the Python hook itself against a stub mitmproxy and asserts both the payload shape and the silence for a non-allowed host.
  • App: claudeSession.suspension.test.ts — the adapter is told before the grace, and a session with no proxy still works. Fails without the call.

tsc -b clean.

🤖 Generated with Claude Code

Juliusolsson05 and others added 4 commits September 19, 2026 21:59
)

Bumps claude-code-headless to its merged fix (#61) and wires the app half.

The package: mitmproxy reports a client disconnect only through its `error`
hook, which the addon never implemented, so an Esc mid-stream left the flow
open holding its phase — a pane stuck on `Thinking` until the next prompt,
and a goal loop with no idle edge to resume from. The addon now emits
`response-error` (allowed hosts only, flow id and message, nothing else) and
the adapter seals the flow: turn stopped, phase idle, flow released.

The app: ClaudeSession tells the adapter the suspension instant as soon as it
learns one, instead of only when the seal grace expires a minute later. A
sleep-severed stream usually reports its own death first, seconds after wake,
and whichever signal arrives first decides what the user reads — so without
this the turn was reported as a transport failure and the feed lost
"Interrupted while asleep".

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…limit

Review findings.

The fold kept only `system-suspended`, so a stream that died mid-answer —
an Esc, a proxy timeout, an upstream failure — left a half-response with no
explanation on desktop and phone, which share this pipeline. The cause now
survives into the turn, the ledger and a marker row of its own:
"Interrupted before the response finished". The wording claims no cause,
because mitmproxy's error text cannot establish one.

The goal loop's #1040 KNOWN LIMIT is gone with the thing it described. Its
test asserted that an Esc-ended turn could only be recovered by typing; it
now drives the sealed sequence and asserts Resume recovers the loop, and the
comment in `control()` says where the fix lives.

The new suspension test's fixtures also failed typechecking
(`SystemSuspension` has `source`, not `durationMs`).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Round-2 review. Why a turn stopped is a closed enum — `system-suspended` or
`transport-error` — and the feed paints a different marker row for each.
Structure-only extraction turned it into `⟨text:15⟩`, the fold then dropped
the unrecognised value, and an extracted recording could not reproduce what
the user saw.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@Juliusolsson05
Juliusolsson05 merged commit 3303bc6 into main Sep 20, 2026
2 checks passed
@Juliusolsson05
Juliusolsson05 deleted the fix/claude-esc-seal-app branch September 20, 2026 06:03
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.

Claude proxy: an Esc mid-stream never ends the flow, so the phase stays busy (stuck Thinking, goal-loop Resume)

1 participant