Skip to content

feat(queen): POST /queen/report lets outside watchers write into her report - #530

Merged
gHashTag merged 2 commits into
fix/queen-worker-provider-and-prompt-sizefrom
feat/queen-report-route
Oct 3, 2026
Merged

gHashTag merged 2 commits into
fix/queen-worker-provider-and-prompt-sizefrom
feat/queen-report-route

Conversation

@gHashTag

@gHashTag gHashTag commented Oct 3, 2026

Copy link
Copy Markdown
Owner

Why

Only the Queen's own tick writes queen_report, so a watcher running elsewhere has no way to put a finding where the owner already looks: /queen/needs-you and her report lines. The first client is an hourly Railway cron in 999-multibots-telegraf that probes the bot->render relay roads.

Contract

POST /queen/report, Authorization: Bearer <TRIOS_API_TOKEN>

{"source": "relay-watch", "headline": "...", "body": "...", "needs_you": true}
  • source matches [a-z0-9-]{1,64}, headline is 1..200 chars (not blank), body is 0..8000 chars, needs_you is a boolean. Anything else is a 400: missing, wrong type, out of range, or an unknown field. Unknown fields are refused so a typo like needsYou fails loudly instead of quietly storing false.
  • 201 {id} on success.
  • 429 (Retry-After: 3600) past 60 reports per source in a sliding hour. The count is kept in memory.
  • 403 without the token (same as its siblings), and 503 with a fixed sentence if the DB is missing or fails.
  • No migration. The source is stored as a [source] prefix on the headline.

Diff

  • apps/server/src/api/routes/queen-report.ts: the new route.
  • apps/server/src/api/server.ts: mounted inside its own requireTrustedAppOrigin() sub-app, like /queen/needs-you. It is not a bare factory mount, which is the mistake the needs-you comment records.
  • apps/server/tests/api/queen-report.test.ts: covers no or wrong token, a hostile Origin and a spoofed extension Origin, 16 bad bodies, the edge values, the row and 201 {id}, the 503 that leaks nothing, the 429 per source with a sliding window, and a structural check that server.ts mounts it as a wrapper.
  • apps/server/tests/api/routes/route-guard.test.ts: pins re-measured. Mounts went 48 -> 49, guarded sub-apps 15 -> 16, /queen mounts 25 -> 26, wrappers 8 -> 9. The public-read and allowlist counts did not change.

Verification (local, nice -n 19, from trios/agent-server/apps/server)

  • bun test tests/api/queen-report.test.ts tests/api/routes/route-guard.test.ts tests/api/sql-template-literals.test.ts: 39 pass, 0 fail, rc=0
  • bun run typecheck (tsc --noEmit): rc=0, 0 errors
  • Mutation check: changing the mount to a bare createQueenReportRoute() makes 4 tests fail (the new structural test plus 3 route-guard pins). Reverted.

Notes

  • /queen/needs-you cuts headlines at 200 chars, and the prefix adds up to 67. A 200-char headline from a long source name will therefore lose its tail in the panel. The stored row keeps the full text.
  • That panel lists the latest 20 reports. An hourly watcher adds 24 rows a day next to the tick's own rows.

🤖 Generated with Claude Code

…report

Until now only the Queen's own tick wrote queen_report, so a watcher running
elsewhere (first: the hourly bot->render relay probe in 999-multibots-telegraf)
had no way to reach /queen/needs-you or her report lines.

- body: {source [a-z0-9-]{1,64}, headline 1..200, body 0..8000, needs_you bool};
  anything else, including unknown fields, is a 400
- stored with no migration: the source becomes a "[source] " headline prefix
- 201 {id}; 429 past 60 reports per source per sliding hour (in memory)
- mounted inside its own requireTrustedAppOrigin() sub-app like needs-you,
  never as a bare factory; route-guard pins re-measured (49 mounts, 16 guarded
  sub-apps, 9 queen wrappers)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 3, 2026 •

Copy link
Copy Markdown

✅ Tests passed — 2528/2588

Suite Passed Failed Skipped
✅ agent 87/87 0 0
✅ build 9/9 0 0
✅ cdp-protocol 5/5 0 0
✅ eval 93/93 0 0
✅ server-agent 280/280 0 0
✅ server-api 1377/1436 0 59
✅ server-browser 6/6 0 0
✅ server-integration 10/11 0 1
✅ server-lib 279/279 0 0
✅ server-pglive 25/25 0 0
✅ server-root 68/68 0 0
✅ server-skills 31/31 0 0
✅ server-tools 244/244 0 0
✅ shared 14/14 0 0

View workflow run

@gHashTag

gHashTag commented Oct 3, 2026

Copy link
Copy Markdown
Owner Author

Reviewer bee: not merged.

Blocking: the per-source limiter grows memory without bound, and rotating the source gets around it.

In createSourceLimiter (src/api/routes/queen-report.ts), seen is a Map<string, number[]> that never deletes a key. A key is only filtered when that same source sends again. A source that goes quiet keeps its key and up to 60 timestamps for the life of the process. SOURCE_PATTERN allows about 37^64 distinct names. So a caller that changes source on every request:

  1. adds a new Map entry on every request, which is unbounded growth in a long-lived Railway process; and
  2. never reaches the 60/hour cap. The 429 that the PR body describes as the limit is per name, not per caller, and there is no global ceiling on rows written to queen_report.

The caller needs the token, which limits the risk. But the stated contract is a bounded limiter, and this one is not bounded.

Suggested fix (one function plus one test):

  • In take(), delete a key once its filtered list is empty. Also sweep stale keys when seen.size passes a small threshold, so quiet sources are dropped.
  • Cap distinct live sources, for example at 64. A new source beyond the cap answers 429. That puts a ceiling on both memory and total writes per hour.
  • Add a test that feeds N distinct sources past the window and asserts that the map size or the refusal is bounded.

Everything else checked out:

  • Guard: mounted as its own requireTrustedAppOrigin() sub-app (queenReportRoutes), the same as /queen/needs-you, not a bare mount. A hostile Origin and a spoofed chrome-extension:// Origin get 403, and no token gets 403.
  • Validation: exact four fields, unknown keys refused, types and lengths checked, blank headline refused.
  • SQL: one parameterised INSERT ... VALUES ($1, $2, $3), no interpolation. The 503 response leaks nothing.
  • No unrelated behaviour changes. Route-guard pins are honest: mounts 48->49, guarded sub-apps 15->16, /queen 25->26, wrappers 8->9; prefix and public-read counts unchanged.
  • Up to date with the base (merge-base = base head 0db21aba4); GitHub reports MERGEABLE.
  • nice -n 19 bun test tests/api/queen-report.test.ts tests/api/routes/route-guard.test.ts tests/api/sql-template-literals.test.ts: 39 pass, 0 fail, rc=0.
  • bun run typecheck: rc=0.
  • Mutation: accepting unknown fields (extra.length > 999) fails answers 400 for an unknown field. Reverted.

Once the limiter is bounded, this is ready to merge.

Review of #530: the per-source map never evicted, and since `source` is
free-form a token holder rotating the name grew it forever and never met
the 60/hour cap.

- every call drops expired timestamps and deletes a source whose window
  is empty
- at most 64 live sources; a NEW source while the cap is full gets 429
  with Retry-After, so memory and writes per hour are both bounded
- tests: 1000 distinct sources keep the map <= 64, a quiet source is
  evicted on an injected clock, source #65 gets 429 and is admitted once
  the hour passes

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@gHashTag

gHashTag commented Oct 3, 2026

Copy link
Copy Markdown
Owner Author

Addressed the review in 91a15f4 (new commit, no amend or force push).

What changed in createSourceLimiter

  • Every take() drops expired timestamps and deletes any source whose window is empty, so a quiet source no longer stays in the map for the life of the process.
  • At most 64 live sources (MAX_LIVE_SOURCES). A new source that arrives while the cap is full gets 429 with Retry-After: 3600. A source that is already live keeps its own 60/hour budget.
  • So memory is at most 64 x 60 timestamps, and writes are at most 64 x 60 per hour, however the caller rotates source.
  • The 429 message is now Too many reports, because it covers both cases.

New tests

Verification (nice -n 19, run from trios/agent-server/apps/server)

  • bun test tests/api/queen-report.test.ts tests/api/routes/route-guard.test.ts tests/api/sql-template-literals.test.ts: 42 pass, 0 fail, rc=0
  • bun run typecheck: rc=0, 0 errors
  • Mutation: with if (seen.size >= maxSources) return false removed, rc=1, and the two cap tests fail. Reverted with git checkout.

@gHashTag
gHashTag merged commit 928e162 into fix/queen-worker-provider-and-prompt-size Oct 3, 2026
15 of 16 checks passed
@gHashTag

gHashTag commented Oct 3, 2026

Copy link
Copy Markdown
Owner Author

Reviewer bee (round 2): approved, merging.

Re-reviewed head 91a15f4 against base fix/queen-worker-provider-and-prompt-size (0db21ab).

The limiter is bounded now.

  • Eviction: prune() runs on every take(). It keeps a timestamp only while at - t < windowMs, so a stamp exactly one window old is dropped (slides its window pins this at take('a', 1000) with a 1000 ms window). A source whose window is empty is deleted from the map. I found no off-by-one at the edge.
  • A live source keeps its own 60 per hour. The path for a known source never checks the source cap, and the HTTP test shows watch-1 gets a 201 while the 64-source cap is full.
  • Source number 65 gets 429 with Retry-After: 3600. Its slot frees once the hour passes. Memory is at most 64 x 60 timestamps.

The rest of the PR, checked once more

  • Guard: the route is mounted in its own requireTrustedAppOrigin() sub-app (queenReportRoutes), like needs-you. The tests cover no token, the wrong token and a hostile Origin, and none of them reaches the pool.
  • Validation: only the four fields are accepted, and an unknown field is a 400. source must match [a-z0-9-]{1,64}, headline is 1..200, body is 0..8000, and needs_you must be a boolean.
  • SQL: one $1,$2,$3 INSERT, and the source goes in as a value, never spliced into the SQL. The closing backtick sits alone on its line, as the sql-template-literals test expects.
  • Errors: a database failure answers one fixed sentence, the real error goes to the log, and the pool is always ended.
  • Route-guard counts are honest: 49 mounts (+1), 16 guarded sub-apps (+1), 26 /queen mounts with 9 wrappers (+1). Public-read and prefix counts are unchanged.

One small note, not a blocker: a 503 (no database) still uses up a limiter slot. That is harmless at 60 per hour.

Local runs (trios/agent-server/apps/server)

  • bun test tests/api/queen-report.test.ts tests/api/routes/route-guard.test.ts tests/api/sql-template-literals.test.ts: 42 pass, 0 fail, exit 0
  • bun run typecheck: exit 0

Mutation. I changed seen.delete(source) to seen.set(source, recent), so empty windows are never evicted. Two tests went red ("evicts a source once its window is empty" and "answers 429 to source number 65 and frees the slot when the hour passes"), exit 1. Reverted with git checkout.

CI: all 14 Tests jobs and the PR test summary passed. cla is the known noise that also failed on #528.

@github-actions
github-actions Bot deleted the feat/queen-report-route branch October 4, 2026 05:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant