Repository navigation
wiki: /stats AI-review freshness, recent run counts, public GET /api/runs (counts only) - #50
Conversation
'Review fresh (<=30d)' read moderation_state.last_reviewed_at, which every bot save stamps, including Wikipedia re-pins and unchanged no-op saves, so it stayed high through the AI review outage. The three stamp-based rows are relabeled as bot-touch counts, and two new rows read pipeline revisions commented ai-moderate:review:<runId>: distinct articles AI-reviewed in the last 30 days, and the date of the most recent AI review revision. Render-cache key page:stats:v5 -> v6.
…ate labels - GET /api/runs?kind=&limit= (public, read-only, newest first, 400 on an unknown kind or a limit outside 1..50, Cache-Control public max-age=300) serves per-run reports including the runner's sanitized notes. Notes are re-redacted server-side (Bearer, sk-ant-, gh*_ and github_pat_ tokens, emails, control bytes) and capped at 2000 chars. - /stats gains a 'Recent pipeline runs' table (last 10 runs: started UTC, kind, articles, errors, tokens, escaped + redacted notes). - The stamp rows now name the stamp (bot-save stamp), since AI proposal decisions and revert-runs do not stamp it; the footer no longer calls a windowed 0 a bug; the latest-review date is labelled UTC. - Test the comment contract through a real bearer save (changed counts, no-op does not) and fix a midnight flake. page:stats stays at v6 (one bump for the whole PR).
…er copy - An out-of-range runner started_at (> 8.64e15 ms) rendered with toISOString() threw and would 500 /stats until the row was deleted; dates now render as a dash when invalid. - Note redaction also replaces the Worker's own secrets verbatim, matches bearer/basic case-insensitively (incl. %20-encoded), high-entropy alphanumeric runs (the runner's rule), Unicode emails, and home paths. - run_id is the final sort key; notes wrap; the date cell does not. - The footer names the rows that are 0 by design instead of calling every all-time 0 a bug. - CLAUDE.md/AGENTS.md render-key list: page:stats:v5 -> v6.
|
Ready to merge after one docs row and a fresh CI run (re-reviewed at Posted by the WikiLean PR swarm (lead); updated 2026-10-17. |
Status review (2026-10-14): ❓ Needs an owner decisionThe code is sound: correct SQL, safe escaping, bounded validation and good tests. In the combined build, #59's The open question is the one the PR raises: should run notes be public? Decision: public run notes
Nits
Conflicts: #55 conflicts in the To ship: Worker change, so it needs the next Brain release and promotion. Merge after #55. 🤖 Automated status pass by Claude Code over all 18 open PRs (#46–#63), posted for the repo owner. An independent verifier re-checked each finding. All 18 heads were also merged and tested together in a scratch tree; the only test failure that comes from combining them is #47 × #55 (three other pairs had mechanical text conflicts). Reviewed head: |
Conflict in the AGENTS.md/CLAUDE.md render-cache key list: take main's line (page:articles:v3 and page:about:v2 from #55) with this branch's page:stats:v5 -> v6 bump. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- GET /api/runs and the /stats Recent pipeline runs table no longer serve pipeline_runs.notes, and the Worker-side note redaction is gone with them. Notes carry free error text, and pattern redaction on a public page publishes any secret shape it does not know. The runner already means its error sample to be read from D1 with wrangler (site/moderate.py), and status.py prints notes only when present, so it needs no change. The counts are what detection reads. - Recent runs sort by created_at (server receipt), then run_id. started_at and finished_at are runner-reported and unbounded, so one future-dated row (a runner sending microseconds, say) stayed on top and hid a failing night from status.py, which reads runs[0] as newest. - /stats: "Pending (awaiting a human)" -> "Pending (undecided)", matching /about (unmatched proposals stay pending until a patroller or a later nightly run decides them). Still inside the page:stats:v6 bump. - docs/OPERATIONS.md: per-run counts at GET /api/runs once released; notes stay on wrangler d1 execute or the host logs. - Tests: no notes key or text in either response; a future-dated run does not take the top row; redaction tests removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fields, when each is null, the always-200 no-store response, and that a brain- version tag is evidence of a promoter deploy, not proof. The row sits after /api/auth/* so it doesn't collide with #50's /api/runs row at the end of the table.
|
Recommendation: merge (at
The maintainer's call: expose Posted by the WikiLean PR swarm (lead); updated 2026-10-17. |
Kinds ('all' is the full-nightly run kind, not a wildcard), the 1..50 limit with 400 instead of clamping, newest-received-first order, the counts-only fields, and the 5-minute public cache.
|
Updated verdict at
Not done, and optional: KV-caching the public GET. When merging, note that the release that carries this needs to be built at the new main. Earlier verdict at 37aea3cVerdict: merge after small doc fixes. I reviewed
This PR is release-affecting ( |
…rix covers the public GET The nightly runs moderate.py wp-update and review as separate commands, and each run records kind=args.command, so nightly rows are wp-update and review and ?kind=all returns none of them. The README row, the route comment and the runs-api test comment now say 'all' is the kind a combined moderate.py all run records. authz.test.ts's endpoint notes still said /api/runs needs a bot bearer; GET /api/runs now has a public row in the matrix. docs/research-plan.md notes the /stats relabel and the v6 cache key that arrive with this release.
Fields, when each is null, the always-200 no-store response, and that a brain- version tag is evidence of a promoter deploy, not proof. The row sits after /api/auth/* so it doesn't collide with #50's /api/runs row at the end of the table.
wiki: /stats AI-review freshness, recent run counts, public GET /api/runs (counts only)
Problem
moderation_state.last_reviewed_at >= now-30d. Every bot save stamps that field: AI review saves, Wikipedia stage-0 re-pins (wp-update:stage0:<run>) and unchanged no-op saves.POST /api/runsstores per-run counts, but /stats shows only all-time sums per kind.manage/status.py(status.py health alerts #59) diffs those sums as a fallback, and its own docstring says that cannot see a failing night between two healthy ones.Change
Review freshness (/stats)
kind='pipeline'revision commentedai-moderate:review:<runId>in the last 30 days.site/moderate.pywrites for review saves.kind='pipeline'keeps free-text session comments out.Run counts
GET /api/runs?kind=<review|wp-update|new|all>&limit=<1..50>:created_at DESC, run_id DESC;Cache-Control: public, max-age=300;run_id, kind, started_at, finished_at, articles_processed, errors, tokens, cost_usd_equiv, model, prompt_sha. Nonotes.kindmatches the run's own kind exactly:allis the kind a combinedmoderate.py allrun records, not a wildcard. The nightly posts separatewp-updateandreviewrows. Omitkindto get every kind.Datecan't represent (above 8.64e15 ms) render as "—". Without that,toISOString()throws and /stats returns 500 for as long as the row exists. A microsecond value is still in range and renders as a far-future date, which is cosmetic.wiki/README.md: the Routes table documentsGET /api/runs: the kinds (allis the combinedmoderate.py allkind, not a wildcard), the 1..50 limit with 400 instead of clamping, the newest-received-first order, the counts-only fields and the 5-minute public cache.docs/OPERATIONS.md: once this is released, per-run counts are atGET /api/runsand on /stats. Notes stay in D1 (wrangler d1 execute, now ordered bycreated_at) and in the host logs.Render-cache key
page:stats:v5→page:stats:v6, bumped once for the whole PR. The CLAUDE.md/AGENTS.md key list is updated to match.Merged
main(through #53). The only conflict was that key list: it now has main'spage:articles:v3andpage:about:v2pluspage:stats:v6.Why counts only
pipeline_runs.notesis free text. It holds the runner's error summary and, since Surface review failures without host access #52, a sample of distinct agent error messages. Making it public would depend on pattern redaction, and pattern redaction publishes any secret shape the patterns don't know. It would also leave two regex sets, the runner's Python and the Worker's TypeScript, to keep in step.site/moderate.pysays the sample makes a failing night diagnosable "from D1 (wrangler d1 execute --remote) without access to the host". Counts only matches that design.status.pyflags a run as failing when errors equal articles or tokens are 0. It prints notes only when the field is present, so it works unchanged against this endpoint and keeps its/statsfallback until the endpoint is deployed.Ordering fix
started_atandfinished_atare reported by the runner, andPOST /api/runsaccepts any non-negative integer for them. The earlier version sorted bystarted_atfirst. One future-dated row, for example from a runner that reported microseconds, would then stay on top until real time caught up with it.status.pyreadsruns[0]as the newest run. A row like that hides a failing streak behind it, and its negative age suppresses the staleness alert too. Sorting by the Worker's own receipt time keeps such a row from pinning the top of the list. It doesn't fix the age check:manage/status.pystill computes the newest run's age from its runner-reportedfinished_at, and/api/runsdoesn't returncreated_at, so a future-dated newest row still suppresses the staleness alert. Exposingcreated_atand using it instatus.pywould close that; it is not included. Rejecting far-future timestamps at POST is an optional extra and is not included.Removed compared with the earlier version of this PR
notesinGET /api/runsand the Notes column on /stats, plus the footnote about sanitized notes. The footnote now says only "newest reported first" and links to /api/runs.NOTE_REDACTIONS,redactRunNotesand their tests.started_atas the first sort key.How tested
npx vitest run test/stats.test.ts test/runs-api.test.ts test/runs.test.ts: 29 pass. Fullnpx vitest run: 942 pass in 40 files.npx tsc --noEmit -p .is clean.started_at/finished_atin microseconds and the higherrun_iddoes not take the top row of/api/runs, with or without?kind=review. The real failing run stays on top. This test fails under the old ordering./api/runsnor/statscontains anoteskey, a Notes column or any posted notes text./api/runsshape, receipt-time ties broken byrun_id, the kind filter (includingall) and the limit bounds;Datecan't represent renders as "—";manage/test_status.py: 23 pass.review_from_runson the trimmed row shape (nonoteskey) still raises the failing-streak alert and prints no notes line.Risks
GET /api/runsis an unauthenticated D1 read of a small table. It has no rate limiter, the same as/recent-changes.docs/OPERATIONS.mddescribes.Release-affecting: Worker change. It ships only through the next Brain release and promotion. No migration, no new secret and no
wrangler.jsoncchange.What Jack must do
Nothing beyond the release.
🤖 Generated with Claude Code