Skip to content

feat(agents): queue only sandbox work; per-user message limits - #12175

Merged
MarkusNeusinger merged 3 commits into
mainfrom
feat/queue-pipeline-only
Oct 14, 2026
Merged

MarkusNeusinger merged 3 commits into
mainfrom
feat/queue-pipeline-only

Conversation

@MarkusNeusinger

Copy link
Copy Markdown
Owner

Summary

  • Only sandbox work queues now. In the owner's live test of 2026-10-14, plain chat answers and even scope refusals waited about 60 s each for the run queue's rate window. Owner decision: "queue only the pipeline … bei sowas muss schnell die antwort kommen".
    • A /messages turn now starts at once. The ADK run goes into its own task, and the stream reads an outbox that carries the ADK events and the queue positions.
    • plot_pipeline and run_data_code take the turn's run slot through TurnSlot.take (ledger.run_slot, which replaces feat(agents): generate data from code in the sandbox (run_data_code) #12172's rate_start). The slot books the one start per turn in the rate window and is held until the turn ends.
    • The request deadline pauses while the turn is queued and re-arms in full when the slot is granted.
    • A full queue or an expired wait ends the turn with error{code:"capacity"}.
    • The turn-level rated = … gate and RunQueue.rate are removed.
    • The theme toggle was already outside the queue and is unchanged.
  • What a user sees:
    • A plain question: ready → message → done, with no wait, even while another user's pipeline holds the slot.
    • A plot: ready → status{queued} (only when it must wait) → the pipeline steps → plot → message → done.
  • Per-user message limits replace the rate window as the chat's abuse guard.
    • AGENT_USER_MESSAGES_PER_MINUTE (10) and AGENT_USER_MESSAGES_PER_DAY (200) are checked before the scope judge, so a flood costs no model call.
    • Action turns count; refused turns do not.
    • Fixed refusals rate_limited and daily_limit, in all 11 refusal languages.
  • A session closes after 3 scope refusals (AGENT_SESSION_REFUSALS).
    • The count comes from the session's own out_of_scope placeholders: attack stores the same placeholder, and needs_data has its own and never counts.
    • The third refusal answers session_closed, and every later message is refused before the judge.
    • The per-user attack strike lockout is unchanged.
  • Plausible. The three new refusal codes are recorded under their own agent_guardrail_block reason.

Notes for the owner

  • A closed session can still paste data. The dataset route keeps its own judge and per-user strikes. Blocking pastes too would be a small follow-up.
  • There is no "new session" button: the session_closed text asks the user to reload.
  • A pipeline call that is then refused with capacity still counts against the daily pipeline runs, as before.
  • The new refusal texts in 10 languages were written by the implementer and deserve a native-speaker glance.

Plan

N/A

Test plan

  • pytest tests/unit tests/integration: 10,506 passed. The new test_turn_limits.py covers:
    • a chat turn answering at once while another pipeline runs;
    • the queued status mid-turn;
    • one rated start per pipeline turn;
    • the per-minute and per-day limits;
    • the session closing after 3 refusals, with needs_data not counted.
  • ruff, ruff format, mypy (api core agents) and the changelog check pass.
  • Frontend: lint, Prettier, type-check, 1,023 Vitest tests and build pass.
  • After deploy: in the live chat, a refusal and a data question answer within seconds, and a plot run shows queued only when another run holds the slot.

…ssion close

Owner decision of 2026-10-14 ("queue only the pipeline, not every chat
message"): a /messages turn now starts at once. Only a plot_pipeline call
(after its readiness check) or a run_data_code call takes the run queue's
slot and the turn's start in the rate window, mid-turn, through the ledger's
run_slot (TurnSlot.take in agents/main.py). The route runs the ADK invocation
in its own task and multiplexes its events with the queue positions, so the
stream still sends status{step:"queued"} while the tool waits. The request
deadline pauses during the wait and starts again in full. A full queue or an
expired wait fails the turn with capacity and aborts the run; the slot is held
until the turn ends, so a second sandbox run in a turn books nothing more.
RunQueue loses the unrated entries and rate(); RequestLedger.rate_start
becomes run_slot.

Chat turns are bounded instead by per-user message limits counted in the
usage book (AGENT_USER_MESSAGES_PER_MINUTE 10, AGENT_USER_MESSAGES_PER_DAY
200), checked before the scope judge and answered with the fixed rate_limited
or daily_limit refusal, and a session closes at its third out_of_scope or
attack refusal (AGENT_SESSION_REFUSALS; needs_data never counts), counted
from the session's withheld messages: that refusal and every later turn
answer session_closed. The page records the three new refusal codes as their
own guardrail reasons.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdMSqLR5ww4ji74EUSmk9M
Copilot AI balanced review requested due to automatic review settings October 14, 2026 15:13
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdMSqLR5ww4ji74EUSmk9M
@codecov

codecov Bot commented Oct 14, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 99.27007% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
agents/main.py 98.18% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟡 Changes recommended

End-to-end timeout caps conflict with the new deadline behavior, and attack-closed sessions can return the wrong refusal.

3 open findings
What changed in this PR

This PR moves queueing from whole chat turns to sandbox work and adds per-user message and session-refusal limits.

Changes:

  • Queues only plot and data-code sandbox operations.
  • Adds message rate limits and session closure after repeated scope refusals.
  • Updates analytics, documentation, and tests for the new behavior.
File Description
agents/​main.py Implements turn slots, deadlines, and pre-judge limits.
agents/​anyplot/​run_queue.py Simplifies queue entries to sandbox runs.
agents/​anyplot/​pipeline.py Acquires a slot before pipeline execution.
agents/​anyplot/​tools/​session.py Queues data-code sandbox runs.
agents/​anyplot/​plugins/​ledger.py Adds per-user message accounting.
agents/​anyplot/​plugins/​scope_guard.py Counts refusals and closes sessions.
agents/​anyplot/​settings.py Adds message and refusal settings.
agents/​anyplot/​prompts/​refusals.yaml Adds localized refusal messages.
agents/​stream.py Documents updated stream behavior.
agents/​README.md Documents new queue and limits.
app/​src/​hooks/​useAgentSession.ts Tracks new guardrail reasons.
app/​src/​hooks/​useAgentSession.test.ts Tests new refusal analytics.
docs/​concepts/​agent-network.md Updates queue architecture and bounds.
docs/​reference/​api.md Updates SSE and endpoint contracts.
docs/​reference/​plausible.md Documents new analytics reasons.
changelog.d/​queue-pipeline-only.md Records the user-visible changes.
tests/​unit/​agents/​runtime/​test_turn_limits.py Tests message and refusal limits.
tests/​unit/​agents/​runtime/​test_run_queue.py Removes obsolete unrated-run tests.
tests/​unit/​agents/​runtime/​test_run_queue_flow.py Tests mid-turn sandbox queueing.
tests/​unit/​agents/​runtime/​test_policy.py Tests localized refusal coverage.
tests/​unit/​agents/​runtime/​test_data_code_tool.py Tests data-code slot acquisition.

🧠 Review effort: Balanced


💡 Configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread agents/main.py Resolved
Comment thread agents/main.py Resolved
Comment thread docs/reference/api.md Resolved
…osed session first

Copilot round 1 on #12175:
- The request deadline resumed in full after a queue wait, so a turn could
  run 540 s + 600 s queued + 540 s, past the BFF's 1,190 s turn cap. It now
  resumes with the time it had left.
- A session closed by three `attack` refusals also hits the strike lockout,
  which answered `budget` before the `session_closed` check. The closure is
  checked first now.
- docs/reference/api.md names `attack` as a closing refusal too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01GdMSqLR5ww4ji74EUSmk9M

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🔵 Needs a closer look

The asynchronous queue, deadline, cancellation, and abuse-control changes warrant final human review.

0 open findings

3 resolved since last review

🧠 Review effort: Balanced

@MarkusNeusinger
MarkusNeusinger merged commit d6ad0de into main Oct 14, 2026
16 checks passed
@MarkusNeusinger
MarkusNeusinger deleted the feat/queue-pipeline-only branch October 14, 2026 16:00
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