Repository navigation
feat(agents): parallel chat windows, a per-user cap on sandbox runs - #12220
Conversation
Owner decision of 2026-10-21: the admin agent chat's browser windows work
in parallel, and only sandbox work queues.
- Every exclusion in the agents service is per session now. `_active`
checks the session's registry entry (`Runtime.active`) and, unless the
route passes `paste=False` (the language chip), the session's paste in
flight; `Runtime.pasting` and `Runtime.converting` hold session ids, not
user ids. `_data_route_free` folds into `_active`. A turn, a paste, a
theme toggle and the data routes still exclude each other within one
session, and the user's other sessions never block them.
- `RunQueue.submit` refuses a user's entry past `AGENT_USER_MAX_RUNS`
(default 3, waiting and running together, across sessions) with the new
`QueueUserLimit`, checked before the queue's length, so a user at the cap
learns that from their own runs. `TurnSlot.take` fails the turn with
`run_limit` (stream `error{code:"run_limit"}`), `run_data_code` answers
the same code, and the `chat_turn` telemetry's `ErrorCode` gains
`run_limit`. The queue's capacity and order are unchanged.
- BFF: `run_limit` joins the pass-through allowlist of stream error codes.
- Page: `run_limit` has its own text ("you already have 3 plots or data
runs in progress in other windows; ..."), and the `run_active` texts now
name this window.
Decisions:
- Why the cross-session paste exclusion existed: a paste holds the view it
read before its judges ran and writes the next dataset from it, so a
version stored meanwhile would be lost. That hazard is per session (the
stores are keyed by session id, no ADK state is user-scoped), and the
guards are keyed by session id, so they hold whoever sends the request,
also two tabs sharing one id. The page opens one session per window and
keeps its id in memory only, so its tabs never share one.
- Conversions: one per session; each waits for the render slot at most
`AGENT_CONVERT_SLOT_WAIT_S`, and the per-user conversion counter still
bounds them.
- Budgets under parallel turns (comment in `post_message`): message, paste,
conversion and pipeline-run counts are checked and booked with no
suspending await between, so they never overshoot. The daily token
budget is checked before every model call and booked after, so parallel
turns overshoot it by the calls in flight. Daily data runs are checked
before the queue wait and counted after it, so they can overshoot by at
most `AGENT_USER_MAX_RUNS` - 1.
- `drop_stale`, `finish` and the idle sweep work per registry entry and
release each entry's own ticket, so several entries per user need no
change.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdMSqLR5ww4ji74EUSmk9M
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdMSqLR5ww4ji74EUSmk9M
There was a problem hiding this comment.
🔵 Needs a closer look
Session concurrency and shared-budget behavior require final human validation, including the pending live multi-window check.
No open issues 🎉
What changed in this PR
Enables parallel agent-chat sessions while limiting each user’s queued and running sandbox work.
Changes:
- Makes activity locks session-specific.
- Adds a configurable per-user sandbox cap, defaulting to three, with
run_limitreporting. - Updates concurrency tests, browser messages, and documentation.
| File | Description |
|---|---|
| tests/unit/api/test_agent_router.py | Tests run_limit forwarding. |
| tests/unit/agents/test_settings.py | Checks the default cap. |
| tests/unit/agents/runtime/test_turn_limits.py | Tests session-specific paste protection. |
| tests/unit/agents/runtime/test_service_flow.py | Tests independent sessions. |
| tests/unit/agents/runtime/test_run_queue.py | Tests cap enforcement and release. |
| tests/unit/agents/runtime/test_run_queue_flow.py | Tests parallel windows and queue limits. |
| tests/unit/agents/runtime/test_data_code_tool.py | Tests data-run limit errors. |
| tests/unit/agents/runtime/test_convert_route.py | Tests parallel file conversions. |
| docs/reference/api.md | Documents session locks and errors. |
| docs/concepts/agent-network.md | Updates concurrency and resource bounds. |
| changelog.d/agent-parallel-windows.md | Records the behavior change. |
| app/src/sections/agent-chat/messages.ts | Adds actionable limit messages. |
| app/src/sections/agent-chat/DataPanel.test.tsx | Updates busy-message expectations. |
| app/src/sections/agent-chat/ChatThread.test.tsx | Tests session and cap messages. |
| app/src/lib/agent.ts | Accepts run_limit events. |
| app/src/lib/agent.test.ts | Tests error parsing. |
| app/src/hooks/useAgentSession.ts | Updates session-lock commentary only. |
| app/src/hooks/useAgentSession.test.ts | Tests limit-error completion. |
| api/routers/agent.py | Allows run_limit through the proxy. |
| agents/stream.py | Documents the new stream error. |
| agents/README.md | Documents parallel sessions and configuration. |
| agents/main.py | Applies session locks and handles cap errors. |
| agents/anyplot/tools/session.py | Propagates data-run cap errors. |
| agents/anyplot/theme_render.py | Updates session-exclusion documentation. |
| agents/anyplot/telemetry.py | Recognizes run_limit telemetry. |
| agents/anyplot/settings.py | Defines the configurable user cap. |
| agents/anyplot/run_queue.py | Enforces per-user queue limits. |
Review effort: Balanced
Codecov Report✅ All modified and coverable lines are covered by tests. 📢 Thoughts on this report? Let us know! |
Gemini 4 Argon review of #12220: a run of the user that goes stale during another turn's root call would still count toward AGENT_USER_MAX_RUNS until the next request swept it. TurnSlot.take now sweeps right before it submits. A flow test pins that a stale run in another window never causes run_limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01GdMSqLR5ww4ji74EUSmk9M
First review: Gemini 4 ArgonVerdict: sound with changes. Gemini confirmed that every single-session lock is kept, that the counts which are checked and booked without an await (messages, pastes, conversions) cannot overshoot, and that the cap is checked before the queue length. (Its file reads were refused, so it reviewed from the diff and marked unverified claims; I verified the ones below.) Applied (
Checked, not a problem:
Accepted for now (admin-only; listed for the public launch):
Fable was not involved: no finding was serious, and the chat is admin-only. Copilot is still to come. |
Summary
409 run_active, which read as "a run or another paste is in progress". Now paste, chat turns, data steps, file conversion and the theme toggle run in every session immediately. Only sandbox work (plot_pipeline,run_data_code) goes through the existing run queue, with itsqueued, position Nstatus. Within one session it stays one thing at a time.AGENT_USER_MAX_RUNSsandbox runs (default 3, owner's choice) may be queued or running per user, across all sessions. A 4th run ends with the new stream errorrun_limit, shown as "you already have 3 plots or data runs in progress in other windows; wait until one finishes, then try again". The cap lives inRunQueue.submit(QueueUserLimit). Queue capacity and ordering are unchanged._active(runtime, sid, *, paste=True)is checked on every route,_data_route_freeis folded into it, andRuntime.pastingandRuntime.convertinghold session ids. Each per-session protection is kept, including the paste-versus-data-route race inside one session. Only the cross-session locks are gone.run_limitis added to telemetryErrorCode, to the BFF allowlist_ERROR_CODES(otherwise it would reach the browser asinternal), toSTREAM_ERROR_CODESand to the page texts.run_activetexts now say "in this window".docs/reference/api.md,docs/concepts/agent-network.mdandagents/README.mdare updated.Plan
N/A. These are owner decisions from the session of 2026-10-21, implemented by a delegate. Gemini 4 Argon reviewed it first; its verdict is in a comment.
Test plan
uv run pytest tests/unit tests/integration -n 4: 12,239 passed, 3 skipped.run_limitand no pipeline model call, while another user is unaffected;run_limitpasses through. Frontend: event parsing, hook and page texts.ruff,mypy api core,tools.changelog check; frontendlint,fm:check,type-check,test(1,166) andbuild.queued.🤖 Generated with Claude Code
https://claude.ai/code/session_01GdMSqLR5ww4ji74EUSmk9M