fix(vm-agent): bind prompt-cancel grace watchdog to the cancelled attempt - #2180
Conversation
|
Important Review skippedAuto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Repository: raphaeltm/simple-agent-manager/.coderabbit.yaml Review profile: CHILL Plan: Advanced Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
@coderabbitai review |
1 similar comment
|
@coderabbitai review |
|
@coderabbitai review |
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Pure move, no behavior change: cancel, prompt-attempt arbiter, session settings, stderr, and MCP server builder blocks move to their own files. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… attempt
The 5s cancel-grace watchdog was keyed by a numeric prompt ID and never
disarmed. When the next prompt was accepted inside the window, the stale
timer fabricated an attempt for the old ID, classified the stop as fatal,
killed the new prompt's agent and failed the task ("Prompt cancel grace
elapsed after 5s"). Production 08-30..09-29: 4 of 4 force-stops were this
false positive, 0 true positives.
- Arm the watchdog with the exact *promptAttempt; disarm on attempt.done
or host teardown.
- Force-stop acts only while that attempt is current and non-terminal.
- Delete promptAttemptForID's fabricate-an-attempt branch.
- A genuinely stuck requested cancel now finishes "cancelled" and restarts
the agent via the intentional prompt-cancel stop, never HostError/fatal.
- Lifecycle logs carry promptId (and deliveryId for control-plane prompts).
Idea 01M31M9G3T4SEWT9ZW1BM4QKZ3 (section A).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ight restarts Review follow-ups: - New test drives a wedged viewer (WS) cancel through the real monitorProcessExit restart and a follow-up prompt on the new agent. - When the process is already cleared by a concurrent restart, the stuck cancel settles "cancelled" and leaves the host transition to that owner instead of claiming a second restart. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-to-attempt Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
a1ac51d to
6d47303
Compare
|



Summary
Fixes the VM agent's prompt-cancel grace watchdog, which killed the next prompt and failed its task (
Prompt cancel grace elapsed after 5s). This is Fix Plan section A of idea01M31M9G3T4SEWT9ZW1BM4QKZ3. The control-plane amplifier (section B: per-session delivery single-flight, one stop per urgent delivery) is a separate follow-up PR.cancelPrompt(true)armed a 5 s timer keyed only by a numeric prompt ID and never disarmed it. If a follow-up prompt was accepted inside the window, the timer fired against it:promptAttemptForIDfabricated an attempt for the stale ID and overwroteh.promptAttempt, and the stop was classifiedfatal_errorbecauseactivePromptIDhad moved on. That killed the new prompt's agent and failed the task. This is a regression from PR feat: add durable session sleep and recovery for Claude Code and Codex #1785.*promptAttemptand selects onattempt.done, host teardown, or the timer.cancelledand restarts the agent through the existing intentional prompt-cancel stop. It never producesHostError/Agent prompt failed. If a concurrent restart already cleared the process, it defers to that owner.Prompt cancel requested,ACP Prompt started/cancelled/completedandACP prompt force-stoppednow carrypromptId. Control-plane prompts also carrydeliveryId(AcceptPromptgains adeliveryIDarg). Force-stop addscurrentPromptIdandcancelRequested.579d8f6ca) is a pure move that splitssession_host.gofrom 1295 lines to 744 (cancel, attempt arbiter, settings, stderr and MCP builders get their own files).Validation
pnpm lint— N/A for Go-only change;gofmt -lclean andgo vet ./...cleanpnpm typecheck— N/A (no TypeScript changed)pnpm test—go test -race ./...forpackages/vm-agent: all packages ok (CI runs the same command)go test -race -count=10 ./internal/acp/— the new tests passed every run. One pre-existing, unrelated debounce test flaked once (TestHarnessActivityReportCoalescesACPToolCallBursts) and is filed astasks/backlog/2026-09-29-flaky-harness-activity-coalesce-test.mdDiscrimination (rule 62):
attempt.donedisarm; force-stop re-pointed at the current attempt) madeTestCancelGraceWatchdogNeverTouchesTheNextPrompt/{ws,http}fail with prompt B completingfatal_error, the incident signature. It also madeTestCancelGraceWatchdogDisarmsWhenAttemptSettlesfail.TestCancelGraceWatchdogSettlesAGenuinelyStuckCancel/{ws,http}reportfatal_error.TestCancelGraceStuckViewerCancelRestartsAgentBackToReadytime out, because the host never returns to Ready.Staging Verification (REQUIRED for all code changes — merge-blocking)
POST /api/auth/token-loginand drove the same endpoints the app's chat UI calls (task submit, session cancel, session prompt, session messages). See Exceptions.Staging Verification Evidence
01M3NYCH7N2JK1QAACABAKF4NJreportedagent_version = 4ef45be85(this branch) with healthy heartbeats. The test was a Claude Code VM task (01M3NYC8SHR8N7JFR0HY137SV1).06:52:44.101;06:52:48.976, 4.875 s later, inside the 5 s grace;platform_errors: 0 warn/error rows, 0ACP prompt force-stopped, and 0 grace or "did not settle" rows. The task finishedin_progress/awaiting_followup, and the last follow-up answeredPONG.Prompt cancel requested {"deliveryId":"01M3NYZ8B19D7DBGQSCPJ521JT","promptId":10}./api/nodesreturns[].UI Compliance Checklist (Required for UI changes)
N/A: no UI changes.
UI Screenshot Evidence
N/A: no UI changes.
End-to-End Verification (Required for multi-component changes)
Data Flow Trace
apps/api/src/routes/chat-cancel.ts(POST /:sessionId/cancel) →cancelAgentSessionOnNode→ VMhandleCancelAgentSession(internal/server/workspaces.go) →SessionHost.CancelPromptFromControlPlane(internal/acp/session_host_cancel.go) →cancelPrompt(true)→watchPromptCancelGrace(attempt).Gateway.handleMessagesession/cancel(gateway.go) →CancelPrompt→ the same watchdog.routes/chat-prompt-route.tsdurable delivery → VM prompt-delivery handler →AcceptPrompt(..., deliveryID, observer)→beginPromptForDelivery→AcceptedPrompt.Run.triggerPromptForceStopIfStuck(attempt)(session_host_prompt_state.go) no-ops unlessh.promptAttempt == attemptand the attempt is non-terminal. A stuck requested cancel goes tosettleStuckPromptCancel→StopProcessForPromptCancel→monitorProcessExitrestart (session_host_process.go).Untested Gaps
session/cancelpath is covered by Go tests throughGateway.handleMessage, but was not exercised on staging. Staging exercised the HTTP control-plane path, which is the one in all 4 production incidents.handleCancelAgentSession) is a thinIsPrompting()guard in front ofCancelPromptFromControlPlane. The Go tests call the latter directly; staging exercised the full HTTP path.Post-Mortem (Required for bug fix PRs)
What broke
A stop or urgent message sent to a busy agent could kill the agent's next turn and mark the task
failedwith "Prompt cancel grace elapsed after 5s". Claude Code restarts in about 4.2–6.2 s, so this was roughly a coin flip per urgent message.Root cause
PR #1785 (
00169b016, 2026-08-12) replaced the force-stop'sactivePromptID != promptIDearly return with an ID-based attempt lookup. That lookup fabricated an attempt for any unknown ID while a prompt was in flight, and the grace timer was never disarmed when its own attempt finished.Class of bug
A watchdog keyed by a reusable, moving identity (a sequence ID interpreted against "current state") instead of the owned object it guards. Once the guarded thing ended, the watchdog outlived it and applied its verdict to a successor.
Why it wasn't caught
The tests hand-built
promptInFlight=truewith no real attempt, so they depended on the fabricate seam and never ran the "next prompt accepted inside the grace window" ordering. No test owned the timer, so the load-bearing midpoint was never reached (rule 62). Logs carried no prompt identity, so the 09-21 report could only guess at the cause.Process fix included in this PR
AcceptPrompt/ cancel paths, with an injectable grace timer that owns the ordering.platform_errorsalone..claude/rules/62-tests-must-observe-the-real-trigger.mdalready covers this class, and the tests now comply with it.Post-mortem file
tasks/archive/2026-09-29-bind-prompt-cancel-watchdog-to-attempt.mdand idea01M31M9G3T4SEWT9ZW1BM4QKZ3.Specialist Review Evidence (Required for agent-authored PRs)
needs-human-reviewlabel added and merge deferred to human — N/A: all completedTestCancelGraceStuckViewerCancelRestartsAgentBackToReady(realmonitorProcessExit→ Ready → follow-up; discriminating). MED: nil-process settle → now defers to the in-flight restart, plus a test (4ef45be85). One observed full-suite failure coincided with the test-engineer's concurrent revert experiment; a clean re-run of 10× passed every cancel-grace test.-race -count=10clean. LOW: cross-reference comment added.CodeRabbit Review Evidence (Required for agent-authored PRs)
coderabbit-reviewlabel applied after local review, staging, and CI gates passedCodeRabbit Notes
CodeRabbit was requested and given substantially more than the required 15-minute wait, but it did not review this PR.
Review skipped/Bot user detectedand submitted no review.219e1c05), this recorded skip satisfies the CodeRabbit gate; no waiver orneeds-human-reviewlabel is required.Exceptions (If any)
Agent Preflight (Required)
Classification
External References
github.com/coder/acp-go-sdk@v0.13.5source (client_gen.goPrompt,connection.gowaitForResponse/sendMessage), read to confirm thatPromptreturns on ctx cancel but first writessession/cancelwithcontext.Background(). That write can block on a wedged agent's stdin, which is the real stuck-cancel case.Codebase Impact Analysis
packages/vm-agent/internal/acp:session_host_cancel.go,session_host_prompt_state.go,session_host_attempt.go,session_host_prompt.go,gateway.go(comment), and the split files.packages/vm-agent/internal/server/workspaces.go: passesdeliveryIDtoAcceptPrompt.packages/vm-agent/internal/config/config.go: comment only.cancelledalready maps to a cancellation, notfailed(server.gomakeTaskCompletionCallback).Documentation & Specs
.claude/skills/env-reference/SKILL.md:ACP_PROMPT_CANCEL_GRACE_PERIODsemantics.DefaultPromptCancelGracePeriod,GatewayConfig.PromptCancelGracePeriodandconfig.ACPPromptCancelGrace.Constitution & Risk Check
ACP_PROMPT_CANCEL_GRACE_PERIOD); no new hardcoded values.🤖 Generated with Claude Code