Repository navigation
Surface review failures without host access - #52
Conversation
…eviews Failed reviews carry their error text only in the nightly host's site/cache logs, and /stats shows only totals, so a night where every article fails is invisible without host access. run_jobs now returns per-job outcomes; main() folds the distinct errors (counts, median time, example slugs, secrets/paths/emails stripped) into the existing pipeline_runs notes field, capped to the Worker's 2000-unit limit. A failed review never stamps last_reviewed_at, so /api/work hands the same articles out every night. Review mode now keeps a host-local site/cache/.review_failures.json and moves articles that failed in the last 3 days behind the rest of the queue (over-fetching by that count so the run keeps its size); success clears an entry, dry runs never write it.
…mple Review findings on the first cut: - Account-wide failures (login, limits, CLI, network, Worker writes) stamped the record too, so a retry after a Max reset, or the night after a login fix, deferred the highest-priority articles. Only fetch/extract/agent-output/render/final-read/422/404 errors stamp now, and an aborted run (exit 3) records nothing. - The retry delay is measured in nights (3 days minus 12 h), since the stamp is taken at run end and checked at the next run's start. - Failures are keyed by the work-queue slug, not annotate_one's make_slug(title); '?' is skipped. - The record is reloaded and merged just before saving, with a per-pid tmp file; a malformed n is coerced; any record error only warns. - No extra stdout line: retry-lib greps the log tail on exit 3. - The email rule needs an alphabetic TLD so package@version survives; the bearer token is matched across line breaks and escape codes; one distinct error gets up to 600 chars of the notes budget. - Tests drive several nights through run_mode, assert the dry run never saves, and cover the aborted and account-wide cases.
|
Review status: merged in Update 2026-10-14 (re-checked at
Original review (2026-10-14)Independent adversarial review of Verdict: merge as the error-sample half only (per-job outcomes, Required before merge
Decisions for the maintainer
Merge order: after #56; in the same release-affecting batch as #50 and the other Worker PRs, before the next Brain release build. Fixes are in progress; this comment will be updated. Posted by the WikiLean PR swarm (lead); updated 2026-10-14. |
Status review (2026-10-14): 🔧 Fix before mergeThis is a runner-only, fail-soft change. Recording errors only warns, dry and aborted runs never update Fix before merge
Nits
Needs a decision alongside #63. This PR's host-local deferral ( To take effect: the nightly host has to run this code. 🤖 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: |
annotate_one started its clock before `async with sem:`, so a job's elapsed_s included its wait behind the other queued articles, and the error sample's ~Ns median mixed queue position into a per-job time. - Start t0 after the acquire; nothing in annotate_one returns or fails before it, and an acquire that raises (AbortSkip) yields no rec. - error_sample's docstring describes ~Ns as per-job time excluding queue wait, and as a hint rather than a clean login/post-agent split. - main()'s comment points at wrangler d1 execute --remote for reading the notes. - Test: a job queued behind a held slot reports only its own time. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
"Not logged in · Please run /login" and "Failed to authenticate. API Error: 401 OAuth access token has expired" fail every article the same way, but neither contains "authentication" or "credit balance", so the run kept handing out articles instead of aborting on the first one. - The account-wide abort also matches "failed to authenticate" and "not logged in" (lowercase). - Test: each error aborts run_jobs (exit 3) after only the jobs that already held a slot. - The per-job outcome test and the no-rotation night test use errors that do not abort, so they still exercise a completed run. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- An expired or revoked token reaches the runner as "Failed to authenticate. API Error: 401 ...", which lacks the abort keyword "authentication", so it gives 15 of 15 on a current checkout too. The "What the code says" note and ranked items 1 and 2 no longer claim it aborts; PR #52 extends the abort keywords. - elapsed_s starts before annotate_one waits for a concurrency slot, so queued articles count the wait. nightly_health reports the fastest failing article instead of the median and no longer infers quota spend from slow failures; the outcome table and "Confirms it" lines drop their timing claims. - Classify "Control request timeout: initialize" (claude CLI startup timeout) and "API Error: Stream idle timeout" (API stream timeout), in nightly_health and the outcome table, with tests. - Step 1 writes the launchctl print dump to a private mktemp file. The setup token goes only in nightly.local.env, never a plist, and steps 5 and 7 show how to run their checks with it. - The fresh-checkout commands pass --job moderate to nightly-launchd.py. - backup-d1.yml runs its own wrangler export, not backup-d1.sh. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…kouts without #52 #52 adds the CLI's login failure strings to the abort keywords and starts elapsed_s once a concurrency slot is acquired, so "15 of 15 on any checkout" and "the clock includes the queue wait" now hold only for checkouts without it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Fixes pushed (2026-10-14)This addresses the "Status review (2026-10-14)" comment and the PR swarm's review (the What changed
Verification
Not changed / still open
Merge notes
🤖 Posted by Claude Code for the repo owner. Each fix was checked by an independent verifier before it was pushed. Head: |
…tempt stamps Drop the per-queue attempt stamps (migration 0014, the two moderation_state columns, POST /api/runs `attempted` and the /api/work ordering on them). Review rotation already lives in the runner since #52, which defers only article-specific failures; a second, looser server-side rotation would fight it. The columns were also declared on the drizzle table, so any later Worker deploy without 0014 applied first would fail every moderation_state write and read while the canary still passed. Nothing sends `attempted` yet, so removing it changes no behavior. The wp-update problem the stamps were for is simpler to fix by size. A failed stage-0 re-pin writes nothing, so a 100-row page let the same failures hold its head every night. The corpus is under 1000 articles, so /api/work now serves wp-update up to 1000 rows (review stays at 100) and every drifted article gets a zero-token attempt each night. The runner clamps per mode to match, the nightly asks for 1000, and run_wp_update now paces read-only articles as update_from_upstream's own loop does, since the sweep is several times longer. nightly_health warns once a single wp-update run reaches 800 articles, the point to revisit rotation. The articles.slug tiebreak stays, so ties on last_reviewed_at (every never-reviewed article) come back in a stable order. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- An expired or revoked token reaches the runner as "Failed to authenticate. API Error: 401 ...", which lacks the abort keyword "authentication", so it gives 15 of 15 on a current checkout too. The "What the code says" note and ranked items 1 and 2 no longer claim it aborts; PR #52 extends the abort keywords. - elapsed_s starts before annotate_one waits for a concurrency slot, so queued articles count the wait. nightly_health reports the fastest failing article instead of the median and no longer infers quota spend from slow failures; the outcome table and "Confirms it" lines drop their timing claims. - Classify "Control request timeout: initialize" (claude CLI startup timeout) and "API Error: Stream idle timeout" (API stream timeout), in nightly_health and the outcome table, with tests. - Step 1 writes the launchctl print dump to a private mktemp file. The setup token goes only in nightly.local.env, never a plist, and steps 5 and 7 show how to run their checks with it. - The fresh-checkout commands pass --job moderate to nightly-launchd.py. - backup-d1.yml runs its own wrangler export, not backup-d1.sh.
Surface review failures without host access
…tempt stamps Drop the per-queue attempt stamps (migration 0014, the two moderation_state columns, POST /api/runs `attempted` and the /api/work ordering on them). Review rotation already lives in the runner since #52, which defers only article-specific failures; a second, looser server-side rotation would fight it. The columns were also declared on the drizzle table, so any later Worker deploy without 0014 applied first would fail every moderation_state write and read while the canary still passed. Nothing sends `attempted` yet, so removing it changes no behavior. The wp-update problem the stamps were for is simpler to fix by size. A failed stage-0 re-pin writes nothing, so a 100-row page let the same failures hold its head every night. The corpus is under 1000 articles, so /api/work now serves wp-update up to 1000 rows (review stays at 100) and every drifted article gets a zero-token attempt each night. The runner clamps per mode to match, the nightly asks for 1000, and run_wp_update now paces read-only articles as update_from_upstream's own loop does, since the sweep is several times longer. nightly_health warns once a single wp-update run reaches 800 articles, the point to revisit rotation. The articles.slug tiebreak stays, so ties on last_reviewed_at (every never-reviewed article) come back in a stable order.
…be like the nightly Step 1 masks TOKEN/KEY/SECRET/PASSWORD values from launchctl's environment block. Step 5's token check runs in a subshell, and the launchd probe runs like the nightly's plist (Background, a /bin/bash parent kept alive) and reads runs and the last exit code before bootout. Step 7 says a run registers either way and names the real confirmation. The outcome table splits extract_failed/fetch_failed into host-wide (every article) and article-specific rows, and gains a Full Disk Access row. site/moderate.py line citations, shifted by #52 and #63, become function names. The fresh-checkout note lists what a clone must recreate. site/ops/README.md links the runbook.
…are rows; fresh-checkout facts The probe's -c script ends in 'exit $?' instead of an echo, so bash still runs the CLI as a child (as for the nightly) and launchd's last exit code is the CLI's, which makes the 126 sentence true. Step 7's token form runs in step 5's subshell. The checklist says to type bash first: interactive zsh does not treat # as a comment. Outcome rows: extract_failed and fetch_failed are host-wide when every article that reached that stage shows them; the article-specific row covers cached HTML and PR #52 scoping; the FDA row lists 'exit code 126' and the older wrapper's limit. The lower-bound note is described for a full old page. Fresh checkout: the wrapper's preflight is what needs wiki/.dev.vars, the job's two KV pushes run npx wrangler, and pending checkpoints must be flushed or copied.
Runner change: takes effect when the nightly host's checkout is updated. Nothing here ships through the Brain release or promotion path;
site/moderate.pyandsite/batch_annotate.pyrun from the host's checkout. The release-neutral-paths policy (site/ops/brain_release_authority.py) still classifies both as release-affecting, since it lists neutral paths and treats everything else as affecting. So after this merges, a Brain release frozen at an earlier commit cannot be promoted frommainwithout a rebuild at the new commit.site/test_moderate.pyis neutral.Problem
The nightly AI review has failed on every article since the 2026-09-18 night. Live
/statsread 117 review runs / 1,761 articles / 843 errors on 2026-10-12 22:17 UTC and 118 / 1,776 / 858 on 2026-10-13, with tokens unchanged at 26,345,645. The newestai-moderate:reviewrevision is from 2026-09-17 09:05 UTC (https://wikilean.jackmccarthy.org/Generalized_hypergeometric_function/history). Two runner gaps let this persist:site/cache/.moderate_run.log.POST /api/runsalready accepts a free-textnotesfield (wiki/src/index.ts, up toMAX_TEXT_LEN= 2000), but the runner sends onlymodes=review(site/moderate.py, end ofmain()).process_reviewreturns before the bot POST on any agent error (site/moderate.py:934-935on main). Only bot saves stampmoderation_state.last_reviewed_at./api/workorders by flags, drift, human edits, then oldestlast_reviewed_at. So an article that fails for its own reasons is handed out again every night, ahead of everything behind it.Change
notes.run_jobsnow also returns per-job{slug, error, elapsed_s}, keyed by the work-queue slug.main()folds the distinct errors intonotes, most common first:modes=review; 15/15 errors: 15x ~2s RuntimeError: agent_error: … (e.g. A, B) | …, with up to 5 distinct errors.~Nsis the median per-job time for that error: the article's own time inannotate_one, excluding the wait for a concurrency slot. Errors raised beforeannotate_one(get_failed_*) have no time. It is a hint about where a job failed, not a classification: seconds suggest a login or launch failure, minutes suggest the agents ran. An error inside the agent pipeline reports 0 tokens either way, so the token count can't tell them apart.sanitize_errorremoves the bearer token, including when it is wrapped across lines or split by escape codes. It also removessk-ant-/gh*_tokens, long alphanumeric runs containing a digit, emails,/Users/<name>(replaced by~), and control and ANSI bytes.package@versionstrings survive.notesstring is cut to 2000 UTF-16 units, because the Worker 413s anything longer and drops the row.annotate_one(site/batch_annotate.py) now starts its clock after acquiring the semaphore. It used to start before, soelapsed_sincluded the wait behind other queued jobs. This also changeselapsed_sin both runners' run logs.run_jobsnow also matchesfailed to authenticateandnot logged in(case-insensitive), next tocredit balanceandauthentication.Not logged in · Please run /loginandFailed to authenticate. API Error: 401 OAuth access token has expired. Re-authenticate to continue.matched neither, so a night worked through the whole batch with every article failing.modes=review aborted; 2/2 errors: 2x ~2s RuntimeError: agent_error: Not logged in · Please run /login (e.g. A, B).site/ops/retry-lib.shretries an exit 3 only when the log tail carries the Max limit signature, which these messages don't.site/cache/.review_failures.json.fetch_failed,extract_failed,agent1_no_json,agent2_no_json,render_failed,final_read_failed,human_lost_422,get_failed_404. A success clears the stamp./api/workcall over-fetches by the number of slugs stamped in the last 3 nights (capped at 100) and moves them behind the others, oldest failure first. The run never shrinks: when nothing else is left, they fill the remaining slots.GET /api/runsand a recent-runs table on/stats, readingnotestakeswrangler d1 execute --remote(query below).How tested
site/test_moderate.py: 142 passed, 0 failed, 0 skipped (Python 3.12, withclaude-agent-sdkinstalled so thebatch_annotatetests run). 18 tests are new. They cover:elapsed_s;run_jobs(exit 3) after at most the jobs holding a slot;run_mode("review")across nights with fakes: rotation pastfetch_failed, no rotation after a night of Worker-write failures, nothing recorded from an aborted run, and nothing saved on a dry run.annotate_one's slug, dropping either new abort keyword, and starting the timer before the semaphore again.site/test_parity.py: 8 passed.site/eval_moderation.py --offline --require-all: 7 passed.blocking_changeson this branch's diff listssite/batch_annotate.pyandsite/moderate.py.Risks
fetch_failedbecause Wikipedia is unreachable, orextract_failedbecausepython3is broken, about 15 articles a night are stamped. After the fix, the top articles wait up to 3 nights. A login, limit or CLI failure records nothing.authenticationbefore, any error text containingfailed to authenticateornot logged inaborts the night after the jobs already holding a slot./api/workwindow, so it can't rotate the wp-update queue; that needs a Worker-side attempt stamp.What Jack must do
mainthat includes this change. Each night'spipeline_runs.notesthen carries the error sample.site/ops/nightly-runtime.sh. Its preflight stops the moderation nightly unlessWIKILEAN_MATHLIBnames an absolute mathlib4 checkout. Set it in the gitignoredsite/ops/nightly.local.env(template:site/ops/nightly.local.env.example) before the first night on the new checkout.GET /api/runs, read the notes withwrangler d1 execute <db> --remote --command "SELECT run_id, started_at, errors, articles_processed, notes FROM pipeline_runs WHERE kind='review' ORDER BY started_at DESC LIMIT 5".🤖 Generated with Claude Code