Repository navigation
fix: apply agent.control signals off the agent_control topic processor - #870
Conversation
AlexanderZ-Band
left a comment
There was a problem hiding this comment.
Blocking — would be REQUEST_CHANGES on a PR by someone else.
Cycle-1 review of #870 (control lane). Blocking:
- a hung hook now stalls the lane permanently, and reconnect no longer recovers it (link.py:447, proved on a real wire against
main); - a queued-signal cancel path no test covers.
Outside the diff: the PR makes four band-bridge comments wrong. They still say the control handler runs on the WebSocket receive task, and it now runs on BandLink's control lane:
band-bridge/bridge_core/control.py:43, which also names_seen_control_ids, nowControlDeduplicator;band-bridge/bridge_core/control.py:79;band-bridge/bridge_core/bridge.py:124;band-bridge/bridge_core/bridge.py:592.
The design behind them still holds: the lane is ordered, so an awaited play nudge would still hold up a later stop. Only the stated reason is wrong.
|
Review-body item (stale |
|
Cycle-2 review of #870 (head a246b25). Most findings were about the cycle-1 adapter deadline, so I stopped the cycle instead of adding a fix on top of that fix:
The deadline is reverted in be3a864 and deferred to INT-1864. The remaining non-deadline findings:
|
AlexanderZ-Band
left a comment
There was a problem hiding this comment.
Blocking — would be REQUEST_CHANGES on a PR by someone else.
Cycle-3 review of #870 (head be3a864).
issue (outside the diff's hunks): band-bridge/bridge_core/bridge.py:237. close() cancels _control_tasks before await self._link.disconnect(). With the control lane, a PLAY queued behind a running agent-scope STOP still applies during disconnect()'s agent_control leave round trip, while the STOP's _resolve_rooms REST fetch returns. That PLAY runs _spawn_play_nudge after the cancel loop. Nothing cancels the new _nudge_room task, so it calls /next and forwards to the forwarder close() has already closed. On main the PLAY sat in the topic queue behind the inline STOP and _handle_leave_mode dropped it once the leave was requested.
- Flow: SIGTERM →
BandBridge._shutdown→AgentRunner.close()→ cancel loop (empty) →link.disconnect()→ leave awaits its reply. The STOP's REST returns, the PLAY applies and spawns a nudge, then_forwarder.close()runs and the nudge forwards m1 to the closed forwarder. - Proof:
/private/tmp/claude-501/-Users-zaikman-repo-band-sdk-python-copy/8bebec4c-0a5e-4e5a-9eb2-2741d4957c52/scratchpad/review870/skeptic3/h1_bridge_close_nudge.py, using a realAgentRunner,BandLinkand PHX client againstFakePhoenixServer, with only the REST boundary gated.- Head:
control tasks alive after close(): 1 / forward m1 (forwarder closed=True). - Main:
control tasks alive after close(): 0, with no/next.
- Head:
- Fix: cancel and await
_control_tasksafter the link is disconnected. The lane can't spawn a nudge oncedisconnect()has returned.
Settled items re-raised without new evidence were dropped: a lane task escaping disconnect when the leave fails mid-reconnect, the test that disconnect awaits cancelled tasks, and the direct _on_control calls in the disconnect tests. A SIGTERM-path variant was dropped: the signal path now matches the host-stop behaviour settled in cycle 1.
|
Cycle-3 review-body item ( |
BandLink awaited the on_control hook inline on the agent_control topic processor. A STOP drains every room's turn and can take seconds, and a supersede on the same topic queued behind it until the hook returned. The hook now runs on an ordered task lane: each signal applies after the one before it, a failed signal is logged at ERROR with its correlation and room ids without blocking the next, and disconnect() cancels what is still pending and logs how many signals it abandoned. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PBt2TLCc2YD1AXxmmo61Hx
test_link_control.py and test_link_control_lane.py each defined the same `link` fixture; it now lives once in tests/platform/conftest.py. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PBt2TLCc2YD1AXxmmo61Hx
…lane Signals are applied one at a time, so an adapter on_interrupt that never returns held back every later interrupt/stop/play until disconnect. The adapter notification is now abandoned after BACKEND_CANCEL_TIMEOUT_SECONDS with a WARNING naming the room and mode. disconnect() names the control signals it cancels. Tests cover a signal queued behind the running one at disconnect and the lane emptying once a signal applies. The control hook type has one alias, and the band-bridge comments state where control now runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PBt2TLCc2YD1AXxmmo61Hx
The per-room deadline on the adapter's on_interrupt bounded only cooperative hooks, left band-bridge hooks unbounded, and made scenario tests that hold a control hook depend on wall time. BandLink.on_control now states the contract instead: signals apply one at a time, so a hook that never returns holds the later ones until disconnect(). A lane-level stall policy is tracked separately. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PBt2TLCc2YD1AXxmmo61Hx
BandLink applies control signals until disconnect() returns, so a PLAY queued behind a running STOP could spawn a nudge after close() had already cancelled the nudge tasks; that nudge then forwarded to a closed forwarder. close() now cancels and awaits the nudges once the link has disconnected. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PBt2TLCc2YD1AXxmmo61Hx
44bfa5f to
74a015e
Compare
Why
BandLinkawaited the runtime'sagent.controlhandling inline on theagent_controltopic processor. Topic callbacks run one at a time, andsupersedeshares that topic. An agent-wide STOP drains every room's turn, up to a second per room, one room after another, so asupersedearriving behind it waited until the STOP returned. Today the platform's 1001 close happens to end such a session anyway. Once a live agent reconnects after 1000/1001 (INT-1808, stacked on this PR), that delay lets the evicted agent reconnect and evict the newer session.What
BandLink._on_controlhands each signal to theon_controlhook on its own task, chained after the previous one, so signals still apply in arrival order and asupersedeis handled at once.disconnect()cancels every unfinished signal, the running one and any queued behind it, and logs each by mode and correlation id.BandLink.on_controlstates the hook contract: signals apply one at a time, so a hook that never returns holds the later ones untildisconnect().ControlHookalias for the control hook type.AgentRunner.close()cancels play nudges after the link has disconnected, because the link applies control signals until then.band-bridgecomments that said control signals run "from the WebSocket receive task" now say what holds.tests/platform/test_link_control_lane.py);tests/platform/linkclose/test_disconnect_cancels_control.py);close()(tests/bridge/test_close_after_control.py);tests/platform/linkclose/test_supersede_during_control.py);AgentRuntimescenarios: a supersede behind an agent-wide STOP whose adapter is slow to cancel, and an agent stop cancelling a control that is still applying (tests/scenarios/test_session_end_during_control.py).Scope
band-sdk-python only, with no behavior change for a signal that applies promptly. A stall policy for a hook that never returns is out of scope, tracked in INT-1864. First PR of the INT-1808 stack: #869 (reconnect after 1000/1001) builds on it, and a test-structure PR follows.
Linear: INT-1808
🤖 Generated with Claude Code
https://claude.ai/code/session_01PBt2TLCc2YD1AXxmmo61Hx