Skip to content

puffer-cli: cancel takes precedence in classify_turn_error (follow-up to #110)#112

Open
n-WN wants to merge 1 commit into
masterfrom
fix/cancel-classify-precedence
Open

puffer-cli: cancel takes precedence in classify_turn_error (follow-up to #110)#112
n-WN wants to merge 1 commit into
masterfrom
fix/cancel-classify-precedence

Conversation

@n-WN
Copy link
Copy Markdown
Collaborator

@n-WN n-WN commented May 13, 2026

Summary

Follow-up to #110.

When a user cancels a turn mid tool call, the runner WebSocket gets dropped and the agent loop's ? chain bubbles up as internal runner error: transport error. The substring matcher landed in #110 classified this as runner_unreachable, so the SSE turn-error told the user "retry, the sandbox is still warming up" — which is wrong, because the user just pressed cancel on purpose.

This PR threads the spawning turn's CancelToken into the worker thread and passes cancel.is_cancelled() into classify_turn_error. The function short-circuits to cancelled whenever the flag is set; the raw chain is still preserved as errorRaw for debug.

How I caught this

Real wire-capture test on hv5 against a stack built off #109 + #110:

  1. CreatePuffer + SendMessage (turn starts a Bash chain with a sleep)
  2. CancelTurn mid-flight
  3. Kill manager + all daemon children, restart manager, WakePuffer (same puffer_id + session_id)
  4. Send a follow-up message through an HTTP proxy that captures the outbound /v1/responses body verbatim

The captured input[2] from #109's marker said:

[turn-aborted] The previous turn was ended with error. No tool calls
completed before the abort. ... Error: internal runner error: transport error

instead of the cancel-specific wording

[turn-aborted] The previous turn was cancelled by user. ...

i.e. the cancel-induced transport interruption was being classified as a transport failure rather than a user cancel. With was_cancelled precedence the wording reflects what the user actually did.

Test plan

  • cargo test -p puffer-cli classify_turn_error (2 tests, both pass):
    • classify_turn_error_distinguishes_runner_unreachable — existing, updated for the new (err, was_cancelled) signature
    • classify_turn_error_cancel_shadows_transport_when_token_fired — new, exercises precedence with the exact "internal runner error: transport error" chain captured on hv5
  • cargo build -p puffer-cli clean
  • cargo fmt -p puffer-cli -- --check clean
  • Re-run the hv5 wire-capture flow against this branch and verify the marker text says "cancelled by user" not "ended with error" (will do after this lands or you ask)

Backward compatibility

The classify_turn_error function is fn-not-pub, so the signature change is internal. Downstream consumers (SSE clients) only see the externally serialized category string and errorRaw chain, both of which keep their existing meaning.

Related

Follow-up to #110.

When a user cancels a turn mid tool call, the runner WebSocket gets
dropped and the agent loop's `?` chain bubbles up as
`internal runner error: transport error`. The substring matcher in
#110 classified this as `runner_unreachable` and the SSE turn-error
told the user "retry, the sandbox is still warming up" — misleading,
because the user just pressed cancel on purpose.

Thread the spawning turn's `CancelToken` into the std::thread closure
and pass `cancel.is_cancelled()` into `classify_turn_error`. The
function short-circuits to `cancelled` whenever the flag is set, with
the raw chain still preserved as `errorRaw` for debug.

I caught this with a real wire-capture test on hv5 against a stack
built off #109 + #110: the captured outbound LLM payload had a
[turn-aborted] marker reading "previous turn was ended with error...
Error: internal runner error: transport error" instead of the
"previous turn was cancelled by user" wording that the cancel path
should produce. With this change the wording reflects what the user
actually did.

Tests:
- existing `classify_turn_error_distinguishes_runner_unreachable`
  updated for the new `(err, was_cancelled)` signature
- new `classify_turn_error_cancel_shadows_transport_when_token_fired`
  exercises the precedence with the exact "internal runner error:
  transport error" chain we captured
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