Confirm managed OpenCode interrupts settle
What
Managed OpenCode steps now confirm that an interrupted turn has stopped before they report it, and before a retry replaces the session.
- Cancellation and Stop. A cancelled, timed-out, or stopped managed run interrupts its turn. It then waits, up to a 10s budget that cancellation cannot cut short, until the host reports the session idle (
/session/statuson v1,/api/session/activeon v2). The event isabortedwhen that is confirmed. Otherwise it isabort-unverified,lastErrorholds the cause, and the step error joins it with the cancellation error.Stopcancels the run so it takes the same path, andStopitself still returns at once. - Turn start. OpenCode can take a prompt before its turn starts, and an interrupt in that gap does nothing. A started prompt submission therefore completes despite cancellation. A run cancelled before submission submits nothing and interrupts nothing. A run whose prompt was taken waits for its turn to start before interrupting it. If the start is never confirmed, the prompt is redelivered to the same session on retry instead of the session being replaced.
- Sessions the user supplied. In a session given with
session:, only a reply to Dagu's prompt shows that its turn started, because the active turn may be the user's own. Such a session is not interrupted until Dagu's turn is confirmed, and a retry only waits on it. - Retry and restart. A retry, a restart, or a DAG retry first confirms the discarded session is idle, interrupting it first when Dagu created it. If it does not settle, no replacement session is created and the next attempt checks again. This check also outlives a
Stop. - V2 commands. A v2 command carries no message ID, so once its request begins it counts as sent and is never submitted twice.
- OpenCode 2.1.4 check.
TestRealV2Runnow cancels a turn that is still streaming on OpenCode 2.1.4 and expectsaborted.
Why
OpenCode accepts an interrupt before its cleanup settles. In 2.1.4 a session stays in /api/session/active until its execution settles. Dagu sent a fire-and-forget abort, recorded aborted, and on retry created a new session in the same working directory, so two agents could edit one checkout at once.
Two related bugs are also fixed:
- Fallbacks after Stop. A graceful
Stopdoes not cancel the step context, andRunskipped fallback providers after a stop only for ACP. Stopping a CLI or managed attempt that has fallbacks therefore started the next provider. - Legacy CLI after Stop. A
Stopthat landed before the managed session started maderunOncefall back to the legacy OpenCode CLI.
A flaky test is fixed as well. TestStagehandWorksInAWindowThatStays/dialog failed on this PR's CI with "target closed before CDP response". Its Register button closes the window, which can happen before the click reports back. Replay's contract allows that error, but the test required none. It now replays through the operator, which judges a lost page by its document.
Not included
Left out deliberately, as discussed in #3448: a separate requested/acknowledged/settled state model, sending resume=false (omitting it does not resume), decoding the v2 interrupted field (status polling decides instead), cross-host or fencing guarantees (settlement is scoped to the host that owns the session), and the experimental /api/experimental/session/{id}/wait endpoint.
Testing
go test -race ./internal/runtime/builtin/harness/...passes.- The new harness tests pass at
-count=20. TestStagehandWorksInAWindowThatStayspasses at-count=10 -race.DAGU_TEST_OPENCODE_V2_EXECUTABLE=<opencode 2.1.4> go test -run '^TestRealV2Run$' ./internal/runtime/builtin/harness/passes.golangci-lintreports 0 issues on the changed packages for linux, darwin, and windows.
Closes #3448
Summary by CodeRabbit
- Bug Fixes
- Stopping a managed run now prevents fallback runs, including when the stop is requested before startup.
- Canceled runs now report cancellation instead of continuing through a fallback.
- The system waits for a running session to become idle before confirming an interruption.
- Runs stopped before prompt submission no longer interrupt an existing session.
- Retry handling now accounts for whether a prompt was submitted, avoiding unnecessary prompt redelivery or replacement sessions.