Skip to content

fix: stop remote adapter turns through the shared cancellation owner - #835

Merged
AlexanderZ-Band merged 12 commits into
mainfrom
fix/fixpython-stop-remote-adapter-backend-turns-throug-INT-1842
Oct 20, 2026
Merged

AlexanderZ-Band merged 12 commits into
mainfrom
fix/fixpython-stop-remote-adapter-backend-turns-throug-INT-1842

Conversation

@AlexanderZ-Band

@AlexanderZ-Band AlexanderZ-Band commented Oct 18, 2026 •

Copy link
Copy Markdown
Collaborator

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).

@linear-code

linear-code Bot commented Oct 18, 2026 •

Copy link
Copy Markdown

INT-1842

INT-1133

@AlexanderZ-Band AlexanderZ-Band left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

@AlexanderZ-Band
AlexanderZ-Band force-pushed the fix/fixpython-stop-remote-adapter-backend-turns-throug-INT-1842 branch from 3a65127 to c81118e Compare October 18, 2026 05:49
@AlexanderZ-Band
AlexanderZ-Band requested a balanced review from Copilot October 19, 2026 08:15

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 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

Comment thread src/band/adapters/copilot_sdk.py
Comment thread src/band/adapters/letta.py
Comment thread src/band/adapters/opencode/adapter.py Outdated
Comment thread src/band/integrations/a2a/adapter.py Outdated
Base automatically changed from fix/fixpython-own-backend-turn-cancellation-across-acp-INT-1841 to main October 19, 2026 08:39
@AlexanderZ-Band
AlexanderZ-Band force-pushed the fix/fixpython-stop-remote-adapter-backend-turns-throug-INT-1842 branch from c81118e to 4651e01 Compare October 19, 2026 08:39

@AlexanderZ-Band AlexanderZ-Band left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 reading room_state after 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 takes turn directly, so no code after the await wires a watch task. Only the release_future/turn_future snapshot is still used afterwards. Trim the comment to the reason that still holds: a message racing in during prompt_async calls _begin_turn again, so room_state.turn would point at the next turn.
  • tests/adapters/opencode/test_lifecycle.py:463 — nit: The diff touches this 679-line file, which holds 13 module-level async def test_... functions and no class 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 is test_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.

Comment thread src/band/adapters/opencode/adapter.py Outdated
Comment thread src/band/adapters/letta.py
Comment thread src/band/adapters/letta.py
Comment thread src/band/integrations/a2a/adapter.py Outdated
Comment thread src/band/integrations/a2a/adapter.py Outdated
Comment thread tests/framework_conformance/stopprobes/letta.py
Comment thread tests/framework_conformance/stopprobes/letta.py Outdated
Comment thread tests/framework_conformance/stopprobes/a2a.py
Comment thread tests/framework_conformance/stopprobes/copilotsdk.py Outdated
Comment thread tests/integrations/a2a/kit.py
AlexanderZ-Band and others added 3 commits October 19, 2026 19:52
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
@AlexanderZ-Band

Copy link
Copy Markdown
Collaborator Author

Answers to the two findings in the cycle-1 review body, which have no thread:

  • src/band/adapters/opencode/adapter.py:502 (stale comment): confirmed. The watcher is created above it and takes turn directly, so nothing after the await wires a watch task. Fixed in 71b9852: the comment is gone, along with the two snapshot locals, which are now plain reads of turn.
  • tests/adapters/opencode/test_lifecycle.py:463 (file breaks the one-class-per-file rule): true, but out of scope here. A compliant split moves the whole file (679 lines; the package has 116 module-level tests) and does not belong in a change about stopping remote backend turns. Tracked in INT-1863.

@AlexanderZ-Band AlexanderZ-Band left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Comment thread src/band/adapters/copilot_sdk.py Outdated
Comment thread src/band/adapters/opencode/adapter.py
Comment thread tests/framework_conformance/stopprobes/copilotsdk.py Outdated
Comment thread tests/adapters/opencode/test_lifecycle.py Outdated
Comment thread tests/framework_conformance/stopprobes/a2a.py Outdated
Comment thread src/band/adapters/opencode/adapter.py
Comment thread tests/framework_conformance/stopprobes/copilotsdk.py Outdated
Comment thread tests/framework_conformance/stopprobes/letta.py
Comment thread tests/framework_conformance/stopprobes/letta.py Outdated
Comment thread tests/framework_conformance/stopprobes/opencode.py Outdated
AlexanderZ-Band and others added 3 commits October 19, 2026 22:12
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
AlexanderZ-Band and others added 2 commits October 20, 2026 05:51
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
@AlexanderZ-Band
AlexanderZ-Band added this pull request to the merge queue Oct 20, 2026
Merged via the queue into main with commit ecd2b1b Oct 20, 2026
42 checks passed
@AlexanderZ-Band
AlexanderZ-Band deleted the fix/fixpython-stop-remote-adapter-backend-turns-throug-INT-1842 branch October 20, 2026 04:13
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.

2 participants