Skip to content

feat(agent): admin lane, final run limits and anonymous limit telemetry - #12507

Merged
MarkusNeusinger merged 11 commits into
mainfrom
feat/agent-admin-lane
Nov 2, 2026
Merged

MarkusNeusinger merged 11 commits into
mainfrom
feat/agent-admin-lane

Conversation

@MarkusNeusinger

@MarkusNeusinger MarkusNeusinger commented Nov 1, 2026 •

Copy link
Copy Markdown
Owner

Summary

  • The owner's run limits (decisions of 2026-11-01). The run queue runs 5 runs at once for the whole instance (AGENT_RUN_CONCURRENCY, was 1) and starts at most one every 10 s, in every lane (AGENT_RUNS_PER_MINUTE 6, with the starts spaced 60/6 s apart, RunQueue.start_gap_s), so it holds 60 waiting entries. Lanes differ only in their per-user cap and order: public 2 (AGENT_USER_MAX_RUNS, was 3), admin 5 (AGENT_ADMIN_USER_MAX_RUNS), and premium stays reserved and goes first. A public user's day is 50 runs and 10,000,000 weighted tokens (about $1). The global budget is 100,000,000 (about $10) and binds admins too. Admins are exempt from the per-user day caps and the message minute; the attack strikes bind everyone. The values are the settings.py defaults, the pinned production values, and Cloud Run --concurrency rises to 80 for the 60 waiting streams.
  • Who is admin: X-Anyplot-Tier: admin, set in one place of the BFF (agent_context) from the identity its admin gate verified, never from the client's source: fanout. The agents service trusts it only behind check_caller. A missing or other value is public and never a 422, so the API and the agents service deploy in either order. /v1/status reports the caller's own user_max_runs and tier, and the panel reads "at most 5 at a time".
  • The fan-out survives the cap. The admin's cap counts every window, so a run_limit in the panel is no failure (nextFanOut in fanoutRules.ts): the library goes back to waiting and the panel shrinks to the runs the server took. A freed place or a 15 s retry tries one library again, and a library gives up only after 30 minutes, which its card says. A single chat's run_limit message is unchanged.
  • Anonymous limit telemetry on pipeline_run: lane, queue_wait_ms, queue_position, user_runs_today (1, 2_5, 6_10, 11_20, 21_30, 31_40, 41_plus) and user_tokens_today (lt25, 25_50, 50_75, 75_90, 90_100, over, against the public budget for every tier). They are closed sets and bounded integers, with no user id, no hash and no key. Limit refusals stay in chat_turn.refusal and chat_turn.error; the admin's land there too (tested). The views SQL is updated, and a test pins every model field to its view column. The views are not applied in BigQuery: that write needs the owner's OK after the merge.
  • Also:
    • The gates run in a worker thread.
    • Claude root, adapter and reviewer retry 4 times instead of 2; the judge stays at 0.
    • A 429 or 529 that outlasts the retries ends a run as busy, and pipeline_result names the innermost error class.
    • pipeline_render logs the agents-side render slot wait.

Live measurement (local stack, real renderer, Claude Haiku 5.5 in eu, final values)

One 15-library fan-out of scatter-basic as admin, by scripted HTTP through the local BFF, 5 at a time from /status:

First design (355 s run, superseded) Final values
Wall time 355 s 402 s (6.7 min); before this PR about 20 min
Result 15 of 15 ok 15 of 15 ok
capacity / run_limit errors 0 / 0 0 / 0
429s none none (Vertex shows only 200 for Haiku eu in the window)
Per library median 89 s, max 136 s median 82 s, max 191 s (muix, the last to start)
Queue wait (queue_wait_ms) 0 s p50 1.9 s, max 39 s: the 10 s gap between starts
Render slot wait, agents side (slot_wait_s) p50 6.9 s, p95 24.0 s p50 0.0 s, p95 7.7 s, max 17.4 s
Render slot wait, renderer telemetry p95 0.001 s (the agents' slot serializes first)
Cost 3.05 M weighted (about $0.30) 3.12 M weighted (about $0.31)

The 10 s gap costs about 40 s at the start of a fan-out, and it spreads the renders: the p95 render slot wait drops from 24 s to 7.7 s. All 15 pipeline_run lines carried lane=admin, and none contained an id.

Decisions taken in the brief's scope (decide and document)

  • "1 start every 10 s" is a minimum gap between starts (start_gap_s = 60 / AGENT_RUNS_PER_MINUTE), not just a sliding window of 6. The window alone would let a fan-out start five runs together, and their renders would meet in the one render slot. One setting stays the knob, and the capacity formula gives 60.
  • The telemetry fields sit on pipeline_run, because user_runs_today is a pipeline-run ordinal and the queue entry is the run's. A not_ready run carries lane only.
  • Fan-out retry policy: shrink to the runs the server took, then one retry per freed place or per 15 s, and give up after 30 minutes. This keeps three panels from burning their libraries without hammering the root model. A simulation of three 15-library panels sharing a cap of 5 finishes all 45 plots in 13.5 to 18 minutes with no library lost.
  • Strikes still bind admins. Pastes and conversions keep their per-user limits (memory and sandbox time, not model spend). The fan-out's copies bind no admin: three windows copy 45 datasets within a minute, past the 30 a minute and 150 a day, and the store's byte cap still refuses a copy it has no room for.
  • A root call refused for quota or overload stays capacity, and the page's capacity text now names that cause.

Open points

  • anyplot-api concurrency (40, one instance). Every chat stream passes through the BFF. At the queue's new capacity, a full queue could hold about 65 BFF streams and crowd out the website's API. Today the admin panel opens at most 5 per window. Before the public launch, raise the API's concurrency or route the chat through its own service. This touches the site's infrastructure, so I left it out of this PR.
  • A public user's render can wait behind a fan-out's renders. That is now lower (p95 7.7 s), but a public-first render slot would remove it.

Plan

N/A (owner-approved plan of 2026-11-01 and the owner's follow-ups of the same day).

Test plan

  • ruff check, ruff format --check, mypy api core agents
  • pytest tests/unit tests/integration (25,900+ passed):
    • the start gap and the shared lanes;
    • TestFanOut: 15 sessions of one admin at the new values all start, none refused, also beside three public users;
    • tier trust, including the production caller check, the exemptions, and an admin's strike lockout;
    • the status payload, the limit telemetry's buckets and validation, and that no new field can carry an id;
    • that an admin's run_limit lands in chat_turn, and the view-column pin;
    • busy versus error, the gates in a worker thread, and the retries.
  • App: yarn lint, fm:check, type-check, test (1,362 passed; nextFanOut, the three-panel simulation, and a component test where a refused library waits and runs at the next freed place), build
  • Live fan-out above. The panel was checked earlier in the browser ("at most 5 at a time", desktop and mobile, light and dark, no console errors).
  • Gemini 3.8 Flash pre-check:
    • First design: 4 findings fixed.
    • Follow-ups (diff against c5ccfa1): the first run stopped at its output cap with no defect found so far; the rerun said "sound", and its one low finding (.catch on plotTurn) is fixed.
  • Copilot round on the first design: 2 medium findings fixed and resolved. No second round, by the rule for medium findings.

https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8

MarkusNeusinger and others added 2 commits November 1, 2026 22:55
The admin's runs get limits of their own: five in flight, no rate window,
five per admin, and no per-user day caps or message minute. The BFF asserts
the tier with X-Anyplot-Tier from the verified admin identity; anything else
is the public tier, which keeps today's limits. The global day budget rises
to 80 M and binds every tier. /v1/status reports the caller's own cap, the
gates run off the event loop, Claude calls retry four times, and a 429 that
outlasts them ends a run as busy.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8
From the Gemini Flash pre-check: model_busy (was rate_limited) also
counts Anthropic's 529 OverloadedError, and the pipeline_result line
names the innermost error class instead of ADK's DynamicNodeFailError.
An implicit __context__ is still not followed. A route-level test pins
that the attack strikes lock an admin out too.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8
Copilot AI lite review requested due to automatic review settings November 1, 2026 22:03
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8

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

Two moderate active findings remain: Cloud Run concurrency can reject admitted streams before the queue’s documented capacity response, and model-service exhaustion is misreported as queue saturation.

2 open issues
What changed in this PR

This pull request adds a trusted administrator execution lane for agent fan-outs while preserving public-user limits. It also improves model-busy handling, render telemetry, frontend status messaging, deployment capacity, documentation, and test coverage.

Changes:

  • Adds tier-aware queue lanes, quotas, daily-budget exemptions, and BFF-to-agents tier propagation.
  • Classifies exhausted model retries as busy, increases Claude retries, and moves render gates to worker threads.
  • Updates fan-out UI behavior, status reporting, Cloud Run concurrency, documentation, changelog, and tests.

Review findings:

  • Moderate (2 votes): agents/cloudbuild.yaml may admit more streams than Cloud Run concurrency can handle; aggregate admin waiting streams or increase capacity accordingly.
  • Moderate (2 votes): Some pre-pipeline model 429/529 failures still surface as capacity rather than busy; preserve the distinction in the stream error and UI.
  • Nit (1 vote): Clarify in docs/concepts/agent-network.md that the global token budget is only the admin’s remaining daily model-related cap.
  • Nit (1 vote): Rename the grammatically incorrect test_an_admins... test.
File Description
tests/​unit/​api/​test_agent_router.py Verifies trusted admin-tier headers and protection from client-supplied values.
tests/​unit/​agents/​test_settings.py Tests admin lane settings and the increased global budget.
tests/​unit/​agents/​test_image.py Updates the expected Cloud Run concurrency setting.
tests/​unit/​agents/​runtime/​test_telemetry.py Updates telemetry tests for tier-aware message handling.
tests/​unit/​agents/​runtime/​test_run_queue.py Tests lane limits, capacity, ordering, isolation, and per-user behavior.
tests/​unit/​agents/​runtime/​test_admin_lane.py Covers admin tiers, exemptions, busy failures, lane isolation, and worker-thread gates.
tests/​unit/​agents/​runtime/​fakes.py Allows model fakes to raise configured exceptions.
docs/​reference/​api.md Documents tier headers, status fields, limits, and busy errors.
docs/​concepts/​agent-network.md Describes admin lanes, quotas, rendering contention, and deployment capacity.
changelog.d/​agent-admin-lane.md Records the admin lane and related model retry changes.
app/​src/​sections/​agent-chat/​messages.ts Adds busy and dynamic run-limit messaging.
app/​src/​sections/​agent-chat/​fanoutRules.ts Displays detailed plot failure messages in fan-out cards.
app/​src/​sections/​agent-chat/​fanoutRules.test.ts Tests busy failure presentation.
app/​src/​sections/​agent-chat/​FanOut.tsx Documents caller-specific fan-out concurrency.
app/​src/​sections/​agent-chat/​ChatThread.test.tsx Tests dynamic run-limit and busy messaging.
app/​src/​pages/​AgentChatPage.tsx Uses the caller’s reported run limit for fan-out.
app/​src/​lib/​agent.ts Extends agent status and stream types with tier-aware fields.
api/​routers/​agent.py Asserts and forwards the verified admin tier.
agents/​README.md Documents admin settings, limits, and deployment concurrency.
agents/​main.py Applies tiers to status, ledgers, queue lanes, and busy errors.
agents/​cloudbuild.yaml Raises agents service Cloud Run concurrency.
agents/​anyplot/​tools/​session.py Applies tier-aware data-run limits.
agents/​anyplot/​theme_render.py Moves theme-render gates off the event loop.
agents/​anyplot/​telemetry.py Adds busy as a pipeline failure reason.
agents/​anyplot/​settings.py Adds admin lane settings and raises the global token budget.
agents/​anyplot/​schemas.py Adds busy to the public failure-reason schema.
agents/​anyplot/​run_queue.py Implements independent admin, premium, and normal queue pools.
agents/​anyplot/​render/​serial.py Records render-slot wait time.
agents/​anyplot/​render/​contract.py Carries render-slot wait telemetry.
agents/​anyplot/​plugins/​scope_guard.py Applies tier-aware daily budget checks.
agents/​anyplot/​plugins/​ledger.py Implements admin exemptions and tier-aware usage accounting.
agents/​anyplot/​plugins/​budget.py Applies tier-aware pipeline-run limits.
agents/​anyplot/​pipeline.py Runs gates in worker threads and classifies model overloads as busy.
agents/​anyplot/​models.py Adds Claude retry configuration and model-busy detection.

Review effort: Lite

Comment thread agents/cloudbuild.yaml Resolved
Comment thread agents/main.py Resolved
@codecov

codecov Bot commented Nov 1, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.42932% with 3 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
app/src/sections/agent-chat/FanOut.tsx 89.28% 3 Missing ⚠️

📢 Thoughts on this report? Let us know!

MarkusNeusinger and others added 2 commits November 1, 2026 23:17
From the Copilot review of #12507: a lane without a rate window now
holds as many waiting entries as it has slots (5 for the admin lane, 50
before), so both lanes at their fullest hold 21 streams, within Cloud
Run's concurrency of 30. The page's capacity text now also names an
overloaded model service, which a root call refused after its retries
ends in. The docs say the global budget is an admin's only daily cap
on model spend.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8
Owner decisions of 2026-11-01, superseding the first admin lane: five runs
in flight for the whole instance and one start every 10 s for every lane
(AGENT_RUNS_PER_MINUTE 6, the starts spaced 60/6 s apart), so the queue
holds 60. The admin lane keeps only its cap of 5 runs (public 2) and its
exemptions. Public day caps are 50 runs and 10 M weighted tokens (about
$1); the global budget is 100 M (about $10). Cloud Run concurrency goes to
80 for the queue's 60 waiting streams.

pipeline_run gains lane, queue_wait_ms, queue_position, user_runs_today
and user_tokens_today: closed sets and bounded integers, no user id, no
hash. The views SQL reads them (not applied in BigQuery), and a test pins
every model field to its view column. A queue test runs a 15-library
fan-out of one admin at the new values: all start, none is refused.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8
@MarkusNeusinger MarkusNeusinger mentioned this pull request Nov 1, 2026
5 of 6 tasks
MarkusNeusinger and others added 2 commits November 2, 2026 00:00
The admin's cap of five runs counts every window, while each fan-out panel
counts only its own: with .plot_all() open in three windows the server
took five runs and refused the rest, and the panels marked those libraries
failed. A run_limit now sends the library back to waiting and shrinks the
panel to the runs the server took; a freed place or a 15 s retry tries one
library again, and a library gives up only after 30 minutes. A simulation
of three 15-library panels sharing one cap of five finishes all 45 plots.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8
From the Gemini Flash pre-check of the follow-ups: FanOutRun clears its
in-flight mark and frees the place also when plotTurn rejects.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8
@MarkusNeusinger MarkusNeusinger changed the title feat(agent): admin lane in the run queue for the fan-out feat(agent): admin lane, final run limits and anonymous limit telemetry Nov 1, 2026
MarkusNeusinger added a commit that referenced this pull request Nov 1, 2026
## Summary
- Cuts v3.6.0 "All fifteen": `tools.changelog release` folds the 95
fragments of the window into `## [3.6.0] — 2026-11-01 — All fifteen`
(170 entries: Added 63, Changed 57, Removed 2, Fixed 42, Security 6),
repoints the compare links and bumps `pyproject.toml`, `uv.lock` and
`app/package.json` to 3.6.0.
- The window is 253 squash-merged PRs since v3.5.0 (#12160 to #12506).
The milestone: the agent chat ("Use with my data") serves all 15
libraries for admins, plus today's user-test fixes #12502 to #12506.
#12507 and the open sample-data, combo-request, date-convert and
data-grid work are not on main and not in this release.
- Hand-written aggregate lines: the `**Dependencies:**` bullet (14
Dependabot PRs; four security updates close 60 advisories, four critical
and 32 high) and the italic `*Catalog:*` line (no new implementations or
specs; 138 regenerations, 127 of them the MUI X 9 migration, 11
daily-regen improvements; 3 stored re-scores; coverage unchanged at 315
of 325 specs).

Routine decisions, documented here:
- Ten fragment bullets had no PR reference (`/pull_request` step 7 never
ran for #12404, #12408, #12409, #12235, #12458, #12461, #12462). Before
the cut, their real numbers were appended (found with `git log
--diff-filter=A` on each fragment), and the two Jules fragments (#12408,
#12409) were wrapped to the file's line width without changing their
wording.
- Completeness: every non-bot commit in `v3.5.0..origin/main` has an
entry except #12469, #12471 and #12472 (`skip-changelog`; a temporary
DEBUG diagnosis and its revert, net zero) and #12298 (`plots/`-only
metadata attribution, exempt). No fragment was added.
- Catalog count: implementation-file changes in the window, classified
by whether the file existed at `v3.5.0` (all did, so no new
implementations), cross-checked against the merged pipeline PRs' labels
on GitHub (127 `regen:forced`, 11 `regen:improved`, 3
`review-writeback`); the MUI X split comes from `library_version` and
`generated_by` in `plots/*/metadata/javascript/muix.yaml`. Advisory
counts are unique GHSA ids of the Dependabot alerts fixed after the
v3.5.0 tag; the nanoid and `@vitest/mocker` alerts closed by #12235 and
#12249 belong to those PRs' own entries and are not counted.

## Plan
`agentic/commands/release.md`, steps 1 to 5. The tag and the GitHub
release (steps 6 and 7) follow after the merge.

## Test plan
- [x] `uv run python -m tools.changelog release 3.6.0 --title "All
fifteen" --dry-run`, then the real run
- [x] The diff touches only `CHANGELOG.md`, `pyproject.toml`, `uv.lock`,
`app/package.json` and the 95 deleted fragments
- [x] `uv lock --check` passes; the lock changes only the project's own
version line
- [x] `uv run pytest tests/unit/test_version_sync.py` passes (2 tests)
- [x] `uv run python -m tools.changelog check --base origin/main` passes
- [ ] CI green

https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
MarkusNeusinger and others added 2 commits November 2, 2026 00:53
Three windows of .plot_all() copy 45 datasets within a minute, past the copy
limits of 30 a minute and 150 a day. The store's byte cap still refuses a copy
it has no room for, and public users keep both limits.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8
@MarkusNeusinger
MarkusNeusinger merged commit fecea42 into main Nov 2, 2026
16 checks passed
@MarkusNeusinger
MarkusNeusinger deleted the feat/agent-admin-lane branch November 2, 2026 06:58
MarkusNeusinger added a commit that referenced this pull request Nov 2, 2026
Carries the caller's tier into the pause: `day_refusal` takes it, so an admin,
who has no own day budget, hears `service_paused` from the route, the scope
guard, the Budget plugin and the paste routes. The usage book learns who came in
the admin tier from `_stamp`, which every spending route passes, and the live
view counts an admin only at the caps that bind every tier. The per-day table's
queue wait reads `pipeline_run.queue_wait_ms` from the line itself, so it fills
from #12507's deploy on, before the `pipeline_runs` view is applied again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8
MarkusNeusinger added a commit that referenced this pull request Nov 2, 2026
## Summary
- `/debug` gets a table of the last 14 UTC days (new chats, plots, cost,
fan-out cost, limit hits by code, p95 queue wait), read read-only from
the BigQuery telemetry.
- `/debug` gets a live line for today from the agents service: the
service's spend against its day budget and how many users are at their
own limits, counts only, for the instance's lifetime.
- A spent service budget now has its own refusal, `service_paused`,
apart from a user's own limit. It names the reset at 00:00 UTC, the page
adds the user's own time, and `GET /v1/status` tells the chat before the
user types.

## Plan
N/A: owner request of 2026-11-02 ("What happens when the $10 daily limit
is reached? We definitely need a few more numbers in debug: dollars per
day, how many new chats."). This takes over the work of a killed
session.

## Decisions
- **Window.** The brief asked for a window floored to 10 minutes. The
table's window is floored to the day instead, so its oldest row is a
whole day. The query text and parameter stay the same all day, and the
API keeps each answer for 10 minutes as the card does.
- **`budget` counts too.** Limit hits count `budget` as well as the
codes the brief named, because a user's own spent token budget is a
limit hit too. `locked_out` is a safety lockout, so it stays in the
card's refusal list.
- **Source handling.** `live` lines fill every column, `fanout` lines
fill only a separate `fan-out` cost column (they spend the same day
budget), and `harness` lines are never read.
- **A user with both budgets spent keeps `budget`.** The pause text says
their own limit is untouched, which would be false for them.
- **The dollars carry `eu`'s 10 %.** The live line prices weighted
tokens at the main model's input list price in the service's location,
so the default of 100 M tokens reads `$11.00`, not `$10.00`.
- **The refusal text stays in UTC on the server.** The server does not
know the user's time zone, so the page adds the local time next to it.
`/debug` shows both.
- **A pipeline halted mid-run keeps `plot.reason = budget`.** That is
the `plot` event's closed failure reason, not a refusal, and it reads
"the usage limit is reached", which is true of either budget. The root's
next call then sends `service_paused` through `day_refusal`.

## Flash pre-check
- First pass: stopped at its token limit without a verdict, after
confirming that `daily_sql` prices and deduplicates as
`05_prices_cost.sql` does.
- Second pass, narrowed to the remaining questions: "sound with
changes", 3 findings, none applied. The pipeline finding claims the
refusal stays `budget`, which the code does not do (see above). The
double-count finding refutes itself in its own text. The client-side
reset time is always the next 00:00 UTC, so passing the server's value
would change nothing for a correct clock. It found no gap in the
consumers of the new code, no exposure beyond counts and times, and no
bug in the pause note.

## Copilot review
Three findings, one applied:
- **Applied:** the page's pause note claimed "your own limit is not
affected", which `/status` cannot know. The note now says only that the
chat is paused and until when. The server's refusal keeps the sentence,
because `day_refusal` sends it only to users with their own budget left.
- **Skipped:** the fragment's PR references. `/pull_request` step 7
appends them and the changelog check passes.
- **Done after #12507 merged:** the admin tier in `snapshot` and
`day_refusal`; see the next section.

## Merged with #12507
- **`day_refusal` takes the caller's tier.** An admin has no own day
budget, so the route, the scope guard, the Budget plugin and the paste
routes all answer an admin `service_paused` during a pause, never
`budget`.
- **The live line counts an admin only at the caps that bind every
tier** (pastes, conversions, strikes). The usage book holds no tier, so
it learns who came in the admin tier from `_stamp`, which every spending
route passes (`UsageBook.mark_admin`).
- **The queue-wait column reads `pipeline_run.queue_wait_ms` from the
line itself**, not from the `pipeline_runs` view, so it fills from
#12507's deploy on, whether or not the view is applied again, and reads
`—` on every earlier day. A test pins the field name to the telemetry
model.

## Owner steps
None. No view changes, no setting changes, no writes to BigQuery.

## Verification
- BigQuery, read-only: per-day cost matches the `cost` view for every
day of the window (10 day and source rows, to the rounding).
`read_daily` over the real REST path decodes 14 rows.
- Scan: one load processes 62,676 bytes in 9 partitions in 0.6 s (a real
uncached job; BigQuery bills its 10 MiB minimum). Dry runs of the same
query: 0 bytes for a window in the future, 50,674 for 3 days, 62,676 for
the 14 days, 70,906 for the whole bucket.
- Browser, own headless playwright-core, 1440x900 and 390x844, light and
dark, normal and paused, with the real rows: no console errors, no page
overflow. The mobile check found the limit-hits cell wrapping and a
mixed UTC and local clock; both are fixed.
- Gates after the #12507 merge: ruff, ruff format, mypy, 26,034 unit
tests, and the app's lint, fm:check, type-check, 1,379 tests and build.

## Test plan
- [x] Unit tests: the query builder, the aggregates, the live status
payload, the pause refusal against the personal one, and the paste's
`429`.
- [x] Per-day cost matches the `cost` view in BigQuery.
- [x] Dry run shows that one load scans only the window's partitions.
- [x] `/debug` and the chat's pause note in the browser, both viewports
and both themes (the note's shorter final wording is covered by unit
tests).
- [ ] After the deploy: open `/debug` and check that the table and the
live line load.

https://claude.ai/code/session_01H7HSkGyea1k2v293XiAMo8

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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