Route messages to all mentioned agents so tasks run in parallel - #575
Open
QuanCheng-QC wants to merge 3 commits into
Open
Route messages to all mentioned agents so tasks run in parallel#575QuanCheng-QC wants to merge 3 commits into
QuanCheng-QC wants to merge 3 commits into
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
Assigning tasks to different agents in one channel used to funnel every
message through single-target routing — the fallback kept only the first
@mention, the LLM router truncated multi-name answers to one agent, and
in dynamic mode explicit mentions never bypassed the router at all. All
tasks landed on one agent whose per-channel queue then serialized them,
showing the 'message queued' status in the UI instead of parallel work.
- honor ALL explicit @mentions (deduped, self-mentions filtered) in
_fallback_targets instead of just the first one
- human messages with explicit mentions now skip the LLM router and
target the mentioned agents directly in dynamic/workflow modes
(master mode keeps its star topology)
- the LLM router now accepts multi-name answers ('next; a, b') and
targets every valid name, dropping only unknowns and self-loops
- cloud agents targeted by one message are invoked concurrently with
per-task DB sessions instead of a sequential await loop
- bound cloud agent fan-out with a semaphore (new
CLOUD_AGENT_MAX_CONCURRENCY config, default 4) and split DB access
into short-lived sessions released before every provider API call,
so a large mention list can no longer pin pool connections for the
duration of external model round-trips
- dedupe cloud agent targets before the gather and dedupe master-mode
delegation mentions at the source, so duplicate mentions can't invoke
the same agent twice with duplicate billable requests
- teach the router prompt the multi-target output format ('next' with
comma-separated names) for independent tasks, matching the parser;
raise the router max_tokens from 30 to 64 so multi-name answers
don't get truncated
- new tests for fan-out dedupe, sentinel filtering, concurrency cap,
depth limit, and master-mode delegation dedupe
While several agents worked in parallel on tasks a human handed them by
name, each worker's own output ("running the command now…") had no
@mention, so it fell through to the LLM router — which cannot tell a
progress note from a handoff and picked a bystander agent. That agent
then answered a task it was never given, and only after its own 60s job
drained off the per-channel queue, so it read as duplicate, late replies.
With N agents working at once this produced N-1 spurious turns every run.
- new _human_assignment_holds(): walks the channel's recent human
messages newest → oldest — one that @mentions the sender means the
assignment stands, one with no @mention at all means free-form chat
reopened routing, one that @mentions only others keeps looking back
(parallel assignments arrive as separate messages, one per agent, so
a newer task for someone else must not cancel this agent's own)
- dynamic mode: an agent message with no peer @mention whose assignment
still holds targets nobody and skips the router entirely — the mirror
image of the human-mention bypass. Explicit handoffs (@peer) still go
through the router, and master mode's star topology is untouched
- router prompt: progress narration about work in flight is "stop",
covering workflow mode and free-form chat where the rule above
does not apply
- 6 tests for the new class
QuanCheng-QC
force-pushed
the
bugfix/parallel-mention-routing
branch
from
August 4, 2026 09:33
355bf19 to
c027149
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The problem, and where it came from
Reported during manual multi-agent testing: assigning a different task to each
agent in the same channel did not run them in parallel. The UI showed a
"message queued"badge and results came back one after another. The channelwas in
dynamicorchestration mode with tasks assigned by explicit human@mention.Two independent routing defects produce that symptom. They have different
repro steps, so they are described separately below.
Defect 1 — explicit assignments collapsed to a single target
Repro: in a
dynamicchannel with two agents, send one message namingboth:
Before: only one agent was targeted. Three separate causes, all
single-target:
_fallback_targetsreturnedmentions[0]and discarded the rest;dynamicmode a human message with explicit@mentionsstill wentthrough the LLM router, so the router could override a target the user had
already chosen;
.split(",")[0], so even a correctmulti-name decision collapsed to one name.
Both tasks then landed on one agent, whose per-channel adapter queue
(
BaseAdapter._channelBusy) serialized them. That queue is what surfaces inthe UI as the
"message queued"badge.Defect 2 — peer progress messages woke agents that were already working
Repro: in a
dynamicchannel with three agents, send three messagesback-to-back, one ~60s task each:
Each command was equivalent to:
python -c "import time,datetime; print('START', datetime.datetime.now().isoformat()); time.sleep(60); print('END', datetime.datetime.now().isoformat())"Before: each agent posts a progress message when it starts ("Starting the
command now…", "Executing command…"). Those messages
@mentionnobody, so theyfell through to the LLM router, which cannot distinguish progress narration
from a handoff and picked a bystander agent as the next target.
Observed with
claude-test-001/codex-test-001/kimi-test-001(A→claude, B→codex, C→kimi):
claude-test-001answered PARALLEL-A threetimes — once legitimately, then twice more after being handed
codex's andkimi's progress messages. Both spurious turns queued behind its own runningjob, so they surfaced ~60s late as duplicate answers.
In a second run one of those spurious replies opened with "you seem to be
describing the PARALLEL-B run" — the agent was visibly reacting to a message
addressed to someone else.
This is systematic, not flaky: N agents working in parallel produce up to N−1
spurious turns per round, so the symptom gets worse the more parallelism works.
After
Re-tested in a
dynamicchannel with threeclaude-adapter agents (all threecan actually execute shell commands, unlike the original mixed set):
claude-test-001,claude-dp-002,claude-dp-003. Three separate messages,one task each.
claude-test-00117:17:23.24732617:18:23.248152claude-dp-00217:17:48.76543217:18:48.766026claude-dp-00317:18:04.49697717:19:04.497673(2026-08-04, times UTC as reported by each agent.)
The decisive number:
PARALLEL-Bstarts at 17:17:48, whilePARALLEL-Aisstill running until 17:18:23. Under the old behaviour B could not have
started until A's turn had finished. Same for C, which starts at 17:18:04.
All three intervals overlap between
17:18:04.496977and17:18:23.248152—18.75s with three agents executing concurrently. Pairwise overlap is larger
(A∩B = 34.5s, B∩C = 44.3s).
The staggered starts are the human send interval plus per-agent startup, not
queueing.
Also confirmed in the same run:
The fix
Routing —
workspace_mod.py_fallback_targetsreturns all explicit@mentions(deduplicated,sender's self-mention filtered) instead of
mentions[0].@mentionsskips the LLM router — the user hasalready chosen the targets.
next:agent-a,agent-b); each name isvalidated against the candidate set, and unknown names and self-loops are
dropped.
_ROUTER_PROMPTdocuments the format andmax_tokensgoes 30 → 64so a multi-name answer is not truncated.
Direct-assignment hold —
workspace_mod.pyNew
_human_assignment_holds(). When all of these hold:@mentionsno peer,workflowmode,the message targets nobody and skips the router entirely. An assigned agent can
post progress notes and status without creating work for a peer.
The assignment window ends when a human posts a message with no
@mentionat all. A newer assignment naming a different agent does not end it —
parallel assignments arrive as separate messages (
@a …,@b …,@c …), soending the window on any later assignment would defeat the rule for exactly the
case it exists to handle.
Explicit agent-to-agent handoffs still route normally: a message that mentions
a peer is not progress narration and goes through the router as before.
_ROUTER_PROMPTadditionally treats progress narration asstop— a secondline of defence for the paths the deterministic rule does not cover
(
workflowmode, free-form chat).Cloud agents —
cloud_agent.pyawaitloop.CLOUD_AGENT_MAX_CONCURRENCYconfig,default
4.— or bill — the same agent twice. Sentinel targets are filtered before
invocation; depth-limit behaviour is unchanged.
call, so a large mention list can no longer pin pool connections for the
duration of multiple external round-trips. Image/audio file records and their
response message still commit in one transaction.
Blast radius
len(real_participants) < 2path is untouched.mastermodedynamicmodeworkflowmode@mentionnow bypasses the plan-steered router. The agent-side hold is deliberately scoped tomode != "workflow", so the plan still drives agent→agent hops.max_tokens30 → 64 per call, but two paths now skip the call entirely, so net router calls go down.["__no_response__"]sentinel semantics unchanged.Worth a reviewer's attention
workflow-mode ordering. Explicit human mentions now take precedenceover the plan-steered router. Deliberate — the human picked the target — but
it is a routing-policy decision, not a mechanical fix.
_ASSIGNMENT_LOOKBACK = 20._human_assignment_holds()scans at most 20recent human messages. How far back a direct assignment stays relevant is a
judgement call.
The termination rule. An unaddressed human message reopens routing; a
later assignment for another agent does not. Required for back-to-back
parallel assignments, but still a policy choice.
The
cloud_agent.pysession split. Correctness depends on provider-callcode never touching unloaded attributes on a detached ORM object. This is the
part that most deserves a close read — it combines concurrent provider calls,
transaction boundaries, file records and response-message creation.
CLOUD_AGENT_MAX_CONCURRENCY = 4. An operational default; may needtuning per deployment.
Intentionally unchanged
One agent still processes one task at a time within a channel. The per-channel
adapter queue stays, because a single agent's session context must stay
ordered. This PR makes different agents run concurrently; it does not make one
agent run two tasks at once. The fix stops unrelated assignments from being
misrouted into the same queue — it does not remove the queue.
Tests
Router suite: 25 → 39 tests (+14) covering explicit-mention fan-out,
dedupe, self-mention filtering, human-mention router bypass, master-mode
preservation, multi-name router answers, unknown-target and self-loop
filtering, and the six direct-assignment cases (progress note stops; assignment
survives a later task for another agent; explicit handoff still routes;
unaddressed human message reopens routing; never-assigned agent unaffected;
master mode unaffected).
New
test_cloud_agent.py(5 tests): duplicate targets invoked once;sentinel-only produces no invocation; sentinel mixed with real targets is
dropped; concurrency cap enforced; depth limit skips all.
Full backend suite, rebased onto
origin/developc3a18260:c3a18260c027149c548 − 529 = 19— exactly the tests added here. The 64 failures arebyte-identical between the two revisions: pre-existing environmental
failures (filesystem-permission errors) in
test_skill_install,test_knowledge,test_browser_contexts,test_events,test_login_session_authandtest_migration_schema.CI's other gate,
test_onm_addressing/test_onm_events/test_onm_pipeline: 65 passed.Known CI noise — pre-existing, not from this PR
CI lints only changed Python files, and the files this PR touches were never
ruff-clean, so the lint job will go red. Onorigin/developthose same filesalready produce 17
ruff checkerrors; on this branch they produce 14 —a strict subset (rewriting
cloud_agent.pyremoved threeF841s). The newtest_cloud_agent.pyis clean.ruff format --checkalready fails on thosefiles on
developtoo.No new violation is introduced here. Auto-fixing would add ~660 lines of pure
formatting churn on top of a 623-line functional diff, so it is deliberately
left for a separate formatting PR.