Skip to content

fix(channel): cancel queued work under the agent-scoped session key - #3316

Merged
zhayujie merged 1 commit into
zhayujie:masterfrom
Lesereingrape:fix/cancel-session-queue-key
Sep 29, 2026
Merged

zhayujie merged 1 commit into
zhayujie:masterfrom
Lesereingrape:fix/cancel-session-queue-key

Conversation

@Lesereingrape

Copy link
Copy Markdown
Contributor

Fixes #3315

ChatChannel.produce() files a session's queue under the routed Agent's key (bridge/agent_bridge.py:1205-1207 -- "keep legacy token keys for the default agent, namespace the rest"), while cancel_session() and cancel_message() looked the queue up with the bare session id (channel/chat_channel.py:636, :666). For any Agent other than the default one -- a channel instance created with an agent_id in the console stamps bound_agent_id on every inbound message (app.py:193, agent/routing.py:57-78) -- the lookup missed, so #reset (plugins/godcmd/godcmd.py:340) left the queued messages in place to be processed after the memory was cleared, and recalling a queued Feishu message (channel/feishu/feishu_channel.py:697) still got answered. The futures half of cancel_session missed for the same reason, since consume() records futures under the queue key (:614-617).

What changed

channel/chat_channel.py -- both cancel paths now resolve the key the same way the enqueue path does:

  • a new _queue_key(session_id, agent_id=None) goes through AgentBridge.scoped_session_key, the helper bridge/agent_bridge.py:673-684 already provides for exactly this ("cancel and steer lookups must be scoped"); if the bridge cannot be reached it falls back to the bare session id, the same way produce() falls back when routing fails, so a default-Agent install behaves byte-for-byte as before,
  • cancel_session(session_id, agent_id=None) and cancel_message(session_id, message_id, agent_id=None) use it for self.sessions and self.futures.

plugins/godcmd/godcmd.py -- #reset passes e_context["context"].get("agent_id"), which is the value route_context() wrote onto the context on the way into the queue.

channel/feishu/feishu_channel.py -- _message_sessions records (session_id, agent_id) and is written after produce() has resolved the route, so a recall can rebuild the key; the two halves are passed to cancel_message. Reading context["agent_id"] before produce() would store the pre-routing value (or nothing), and a bound Agent that has since been disabled falls back to the default route inside produce() (agent/routing.py:71-77), so the value after the call is the only one that can match the key.

How it was verified

  • Redone on the pristine tip 04eb97cf: with the queue filed under team-a::web:u1 -- which is what produce() does, pinned by its own test case -- cancel_session("web:u1") left qsize() == 1, and cancel_message("web:u1", ...) reported queued=0 while the message stayed in the queue. Running the two test files as submitted against that pristine checkout gives 10 failed, 10 passed; on this branch the same two files give 20 passed (adding tests/test_robustness_fixes.py, which already covers the default-Agent path, gives 25 passed).
  • Whole suite, same tip, twice (pytest tests/ -q): pristine master 32 failed, 2285 passed; this branch 32 failed, 2294 passed. The FAILED lines are the same set in both runs (diff of the sorted lists is empty), so nothing regressed -- those are pre-existing environment-dependent failures on this Windows host, not caused by this diff. The nine extra passes are the new cases.
  • ruff check channel/chat_channel.py channel/feishu/feishu_channel.py plugins/godcmd/godcmd.py tests/test_cancel_session_agent_scope.py tests/test_feishu_message_recall.py --select F --ignore F403,F405 -- clean.

ChatChannel.produce() files a session's queue under the routed Agent's key
("agent::session"), but cancel_session() and cancel_message() looked the bare
session id up, so for any Agent other than the default one they found no queue
and left the queued messages running: #reset on a bound channel instance, or
recalling a queued Feishu message, reset nothing.

Both now resolve the key the same way produce() does, through the AgentBridge's
own scoped_session_key, and the two callers pass the Agent that accepted the
turn.
@zhayujie
zhayujie merged commit 3c9b7d5 into zhayujie:master Sep 29, 2026
3 checks passed
@zhayujie

Copy link
Copy Markdown
Owner

Merged, thanks @Lesereingrape! Routing both cancel paths through scoped_session_key is the right fix, and recording the agent after produce() has resolved the route is a nice detail.

One follow-up on master (3e74cd0): the web console's session delete handler had the same gap. It called channel.cancel_session(session_id) and scoped_session_key(session_id) without the agent, so deleting a non-default agent's session left its queued messages and in-flight run alone. It now passes the request's agent_id to both. This will ship in v2.2.0.

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.

cancel_session/cancel_message look the queue up with the bare session id, so a non-default Agent cancels nothing

2 participants