Repository navigation
fix: stop remote adapter turns through the shared cancellation owner - #835
AlexanderZ-Band merged 12 commits into
Conversation
1d5e54a to
3a65127
Compare
AlexanderZ-Band
left a comment
There was a problem hiding this comment.
Full-PR review: no verified new findings across all six review groups. Converged in cycle 1. The suspected interrupted Copilot eviction leak reproduces on the base as well; the new owned stop improves abort completion. Full unit validation is still running.
3a65127 to
c81118e
Compare
There was a problem hiding this comment.
🟡 Changes recommended
Replacement tools become accessible before cancellation settles, and new A2A tasks can escape cancellation before their IDs arrive.
4 open issues
What changed in this PR
Extends the shared backend cancellation foundation to Copilot SDK, OpenCode, Letta, and A2A, aiming to stop remote work before replacement turns or teardown.
Changes:
- Integrates four adapters with
BackendTurns. - Adds recording probes for cancellation, timeouts, continuation, and shutdown ordering.
- Documents OpenCode’s stopped-turn behavior.
| File | Description |
|---|---|
| tests/integrations/a2a/kit.py | Adds a recording A2A client. |
| tests/framework_conformance/test_backend_stop.py | Registers four adapters’ stop probes. |
| tests/framework_conformance/stopprobes/opencode.py | Covers aborts, timeouts, and cleanup. |
| tests/framework_conformance/stopprobes/letta.py | Covers cancellation modes and failures. |
| tests/framework_conformance/stopprobes/copilotsdk.py | Covers aborts, session reuse, and shutdown. |
| tests/framework_conformance/stopprobes/a2a.py | Covers task cancellation and bootstrap continuation. |
| tests/adapters/opencode/test_lifecycle.py | Waits for the asynchronously owned abort. |
| tests/adapters/lettakit.py | Adds a recording Letta client. |
| tests/adapters/copilot_sdk/fakes.py | Records lifecycle calls and supports held aborts. |
| src/band/integrations/a2a/adapter.py | Adds task cancellation ownership and settlement waits. |
| src/band/adapters/opencode/adapter.py | Coordinates watchers, aborts, and timeout reporting. |
| src/band/adapters/letta.py | Adds owned run cancellation and cleanup waits. |
| src/band/adapters/copilot_sdk.py | Adds owned session aborts and shutdown waits. |
| docs/adapters/opencode.md | Documents cancellation ordering. |
Review effort: Balanced
c81118e to
4651e01
Compare
…ation Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YNMWPLGSHPnZpadgLr4sPA
AlexanderZ-Band
left a comment
There was a problem hiding this comment.
Blocking — would be REQUEST_CHANGES on a PR by someone else.
Full-PR review of fae4bf310, cycle 1: 6 issues, 13 nits, 5 suggestions. The issues are a STOP that cancels an already-finished A2A task, and five stop/teardown probes that stay green when the ordering they claim to guard is removed.
Not repeated here because they are already open as threads: replacement tools published before the previous turn settles (Copilot, Letta, OpenCode) and the A2A stop before the first task event.
Findings whose line is outside the diff hunks:
src/band/adapters/opencode/adapter.py:502— nit: The comment says readingroom_stateafter the prompt await "would wire this turn's watch task to the wrong turn's future and usage dict". Watcher creation now sits above it (lines 495-498) and takesturndirectly, so no code after the await wires a watch task. Only therelease_future/turn_futuresnapshot is still used afterwards. Trim the comment to the reason that still holds: a message racing in duringprompt_asynccalls_begin_turnagain, soroom_state.turnwould point at the next turn.tests/adapters/opencode/test_lifecycle.py:463— nit: The diff touches this 679-line file, which holds 13 module-levelasync def test_...functions and noclass Test.... AGENTS.md says one test class per file with no module-level test functions, and 'split a file you touch that breaks it'. The edited test istest_interrupting_a_turn_stops_the_reply_and_frees_the_room.
Move the edited interrupt test into its own class TestInterruptedTurn file in tests/adapters/opencode/ (with a conftest or helpers module for shared fixtures) as part of this change; splitting the rest of the file can follow.
Copilot SDK, Letta and OpenCode published the replacement turn's tools before waiting for the stopped turn's abort or cancel, so a late tool call from the stopped turn ran against the replacement turn. Each now settles the stop first (Copilot and Letta inside running(), OpenCode at the top of on_message), as Codex and Claude already do. OpenCode also keeps its turn watcher only on the turn and drops a snapshot of the turn's futures. Its probes, and the Copilot and Letta ones, now guard the stop ordering: the next message, teardown and a stopped watcher's silence. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YNMWPLGSHPnZpadgLr4sPA
A STOP before a new task's first event reached no task, and a STOP while a finished task was being delivered sent tasks/cancel for it. The first event is now read through BackendTurns.started(), the handle carries its own finished state, and a cancel releases the task through the single finalize path before it cancels the task remotely. Without streaming the first event is the final answer, so a STOP before it still cannot cancel the task; the setting's documentation says so. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YNMWPLGSHPnZpadgLr4sPA
conversations.cancel first ships in 1.7.1 and the conversations resource in 1.6.5, while the floor was 1.0.0, so a shared-mode STOP raised on older releases. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YNMWPLGSHPnZpadgLr4sPA
|
Answers to the two findings in the cycle-1 review body, which have no thread:
|
AlexanderZ-Band
left a comment
There was a problem hiding this comment.
Blocking — would be REQUEST_CHANGES on a PR by someone else.
Full-PR review of bcb647de0, cycle 2: 6 issues, 7 nits, 2 suggestions. The issues are a Copilot message that queues behind a STOP and can return on the stopped turn's aborted idle (reproduced against the real CLI), an OpenCode STOP that does not silence a released turn's timeout report, a test that no longer tests what it claims, a probe module that breaks collection in lanes without the Copilot extra, and two A2A guards no probe covers.
No thread from earlier cycles is open.
The CLI emits the aborted turn's idle just after acknowledging the abort, and send_and_wait ends on any idle, so a message queued behind a STOP could return on it with no reply. The stop now waits for that idle while a prompt is in flight. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YNMWPLGSHPnZpadgLr4sPA
… next-message ordering Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YNMWPLGSHPnZpadgLr4sPA
…stop docs Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YNMWPLGSHPnZpadgLr4sPA
Cancelling on_message only schedules the watcher's cancellation, so drain() could return before the abort was spawned; on Python 3.11 it did. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01YNMWPLGSHPnZpadgLr4sPA
…rns-throug-INT-1842

Why
Remote backend work can survive STOP or INTERRUPT, and unfinished cancellation can overlap replacement work or teardown.
What
Integrate Copilot SDK, OpenCode, Letta and A2A with BackendTurns. Cover timeout-abort ownership, bootstrap rollover, next-turn isolation and shutdown ordering through recording backend probes.
Scope
Second layer of the backend cancellation stack split from #791, built on the merged cancellation foundation (#834). Closes INT-1842; contributes to INT-1133. Parlant engine child ownership is the final layer: #791 adds Parlant and the complete inventory guard. Independent fixes: #832 and #833.
Not covered here: STOP before a new A2A task answers when the remote does not stream (INT-1862), and splitting the OpenCode test package into one-class files (INT-1863).