Repository navigation
Full-review fixes: sitemap walk safety, probe gaps, queue priority, serve path, admin (plugin 0.97.0, console 0.22.0) - #226
Merged
Conversation
…v0.97.0 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… filed instead of throwing v0.94.0 moved the write to `fileDueNow` and deleted the `nextRenderTime` local the response still returned, so every revalidate threw a ReferenceError after filing and Harper rolled the filing back. `fileDueNow` now returns the due time it wrote, which the handler reports. `no-undef` is now on for packages/plugin, with Harper's own declared globals (its dist/index.d.ts) and the Node runtime globals the plugin uses. That undefined local was the only hit. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…che statuses in the schema - `ingress.report.interval`, `management.backlogSnapshotInterval`, `render.reconcile.interval` and `queue.statusSyncInterval` now max out at 2147483647 ms. Past it Node fires a timer every millisecond, so "30 days = effectively off" was a hot loop. - `management.analytics.maxRange` (max 7 days), `management.analytics.scanCap` (max 3,000,000), `management.scanCap` (max 1,000,000) and `management.pageSize` (max 500) are console-editable and had no ceiling. - `render.negative.statuses` entries must be integers 400-499 other than 429. A 5xx or 429 would store and replay an origin failure, and a string such as "404" stored nothing. This is enforced through a new schema field, `items` (a per-entry rule for a list of numbers), which rejects the whole list at apply time and at the override door, like `itemEnum`. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… racing a cross-node delete leaves a readable row Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e loads, not on first request After a deploy that does not restart Harper, the new package.json is on disk under workers still running the old code, and a lazy read on the first overview request reported the staged version as the running one. Read at module load, it names the code the worker loaded. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rs run outside the caller's request
The override subscription's listener may run in the async context of the request whose write rang
it, and a timer armed there inherits that request's transaction. So the re-read, the apply and every
timer the apply re-arms (probe, reconcile, backlog, the backstop poll) could run on a transaction
Harper closes after the response ("Database closed during transaction get"). The debounce timer is
now armed through `runDetached`, and `applyOptions` runs each listener through a new synchronous
`runInLoadContext` (util/detach.js). That covers every listener whatever called the apply, and a
listener's throw is still caught by the apply's own guard.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… due time apart, instead of re-repairing them every pass Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…s only rows that make a request The freshness skip keyed on `probedAt`, which moves only when a baseline is written, so it skipped exactly the URLs that had recently changed: a change caught by an off-schedule pass was skipped by the next anchored pass and its reprice waited a further day. A pass now skips only rows written since it (or the pass it resumes) began; `changeProbe.reprobeAfter` is retired (accepted, read by nothing). Rows skipped as fresh no longer consume the rate window. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…the staleness window is not taken over by a second one Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…red something and agreed `claimsDisagree` answered false both on agreement and when no dimension was comparable (an unrecognised page availability beside a null endpoint price), and the verification proof for the price/availability pair was only that a stored claim existed. `compareClaims` now answers agreed / disagreed / nothing comparable, and the proof requires agreed — as mapped fields already required an armed field that agreed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… and a changed page's at lease scale Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… window cache is bounded - `bucketize` and `systemSeries` are split into accumulate (`add`) and `finish` (`createSeriesFold` / `createSystemFold`), and the scan folds each row inside the walk. Before, it held every kept row until the walk ended: about 250k objects at a 300k scan cap, on a worker that also serves bot traffic. Memory now scales with combos x buckets. The output is identical: a golden file produced by the pre-change code on a seeded fixture pins it, and a walk that poisons each row after yielding it proves nothing is buffered. - The window cache was keyed by the exact requested range and never evicted, so a script walking the range parameter grew it for the life of the worker. `clampRange` now rounds to a whole minute, and the cache keeps at most eight windows (the console offers six), evicting the oldest. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…n is retried once after the walk `probe_*` pass counters were one emit per finished pass: lost whole to a dropped analytics window, lost entirely when the pass threw (the emit sat after the walk). They are now emitted per probed batch as increments, and the error path reports what it reached. New series: `probe_errors`, `probe_caught_up`, `probe_ignored`. A change whose action threw used to wait for the next pass, because the cursor had moved past it; the first 1,000 failures of a pass are retried once when the walk ends (pass record: retried, recovered, unacted). The console guard waives the new series. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… not a table-row edit Growing the prerender_ops table cell re-pads every row of the table under prettier; a note beside the entity-gate one says the same without touching lines other changes edit. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ot on walk progress The heartbeat rode the walk's yields (every 200 rows examined), so a pass inside one long await — a Retry-After wait capped at 5 minutes (the staleness window), a slow action slot — read as dead, and a console "Run sweep" started a second full-rate pass beside it. A pass-long unref'd interval now beats through the walk, the drain and the retries, and is stopped before the claim is released. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… one never releases or overwrites the next lease's, and one granted before a change mark is not stored A lease slot now carries its latest grant's instant and due minute (buffer v3). A result whose render provably began before the key's latest grant is dropped whole and its release is generation-checked; a result whose grant predates the row's changedAt stores nothing and leaves the row due and marked; and a released key re-granted at the same due minute counts a miss, so a result whose commit failed after the release engages the wedge guard instead of looping. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…use lists the compiler would drop A config override row holds a whole option, and a list is one option. So one route edited from the console stored a copy of every route, and later config.yaml edits to any route deployed and were merged away silently. - Each ConfigOverride row now records `fileHash` (new schema attribute), a stable hash of the file layer's value at its path when it is written. `GET /prerender_admin/config` layers carry `masking` (the override is in effect and the file has changed since the row was written; null for a row written before this change, which recorded nothing) and `redundant` (the override equals the file now). A masking row is logged at every apply and is a `warn` finding in `warnings`. A redundant one is an `info` finding. Re-saving a row changes its fingerprint, so every worker picks it up. - `POST /prerender_admin/config-override`: a set of `ingress.routes` or `changeProbe.rules` whose compiler would drop entries is listed in `rejected` (with `dropped` and the compiler's reasons) and refused with 409 on apply. Before, the apply answered `applied: true`. The check inspects the value being set, so a bad entry already in the file never blocks an unrelated edit. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… masking/redundant + dropped-entry refusal, streaming analytics fold, negative statuses validation, version at load, admin read maxima, detached config doorbell Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ep or a restart
The anchor timer awaited runProbeSweepOnce, which returns {skipped} whenever any sweep holds the node
(a reseed, a manual pass, a resume), and re-armed for tomorrow with no log or metric; a restart
spanning the anchor also lost the night (the timer only looks forward, and the boot check resumed
only a claim that was running). Now the anchor asks a measuring pass (dry run or reseed) to stand
down — through the claim row, so it reaches another worker's pass — and waits for an acting pass and
runs after it; at boot, a node that has started no acting pass since the most recent anchor
(DST-correct) catches that anchor's pass up, and a dead claim that predates the anchor is replaced by
a whole pass instead of resumed. New series probe_anchor, detail on_time / interrupted / chained /
caught_up / skipped.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…eue.ready.urgentHeadStart), and head starts are in boosted units Every fileDueNow caller (render-now, revalidate, admin rejoin) and Target.put given an explicit current-minute time (an adopted redirect destination, a sitemap revalidate) marks the row urgentAt. Filed at the current minute such a row was zero cadences late: behind every overdue row, and past the ready set's capacity not published at all. The keeper holds urgent rows as their own class and ranks them queue.ready.urgentHeadStart (default 1) ahead. Both head starts are now multiplied by sitemapBoost, so a fresh changed or urgent row outranks routine sitemap lateness under a cadence instead of under half of one. Touches Target.put (sitemap agent's file) for the explicit-time mark: one derived field. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…idation does not cover A trip replaced the rest of the night with a DRY-RUN reseed, but the invalidation epoch refuses only pages rendered before it: a page re-rendered after the trip that then changed (or disagreed with its claim) was baselined and never expired, and served wrong. The reseed now runs armed and acts on a change unless every page of the URL predates the epoch and no verification exempts it — those it baselines without acting (counted `covered`), so clearing a false trip still restores them. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
A truncated gzip body decodes leniently to a well-formed-looking prefix (1,972 of 5,000 entries, 200, no throw), which the walk read as a shorter sitemap: every URL past the cut was unlinked, departed and later rejoined. The parse now refuses a document whose root is never closed, so it is a failed child. Targeted rather than full XML validation, which refuses real sitemaps' tolerated errors (an unescaped & in a loc). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…erdict is gone A canonical-mismatch or noindex row answers 200, so deleting it lets the next crawl re-mint it: a delete, a mint, a render and a re-suppression per URL. Suppression clears demandInterval, so skipVisited never spared them. Gone rows are still purged (nothing re-mints a 404). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…-created stubs The key-rule orphan sweep used one unconstrained projected scan, which can end silently at an unreadable row and report no orphans; it now uses walkUrlRange. A patch landing on a just-deleted key creates a row with only the patched fields and no primary-key attribute: walkUrlRange reports where such a row sat, recoverKeylessKeys finds its key in the primary store, and the Target and page sweeps delete it. The page sweep also deletes a content-less stub (no statusCode, no lastCached) that its age test kept forever. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…dropped children, first listings - A walk with a failed child acts on no departure and re-links what it unlinked (departure_relinked), so shear into a failed child is neither rendered as a departure nor, next walk, as a rejoin. - sitemap.shrinkGuard (maxRatio 0.5, minUrls 1000) refuses a prune that would unlink most of a child; the child fails instead. - A child the index stops listing is pruned after the walk, behind the guard, and its row dropped once the walk is clean. - A URL the owning child drops but a 304'd child still lists is re-attached to it (departure_listed_unchanged). - A discovered URL's first listing, or its non-gone suppression recheck, is filed due now inside newTargets.maxPerRun (listedSoon). - Departures and rejoins file through fileDueNow with a change mark, so they keep a probe mark and get the changed head start. - The bypass token goes only to the root's host or config.domains, re-decided on every manually followed redirect hop. - Every Target/page patch names its primary key; urlset rows store a 500-entry sample; the blocking REST writers run via runDetached. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e went unseen No per-URL "last seen unchanged" is stored, so each change a sweep detects emits two upper bounds, labelled by rule: since the start of the pass that found it (for an anchored pass, since the anchor it serves) and since the start of the previous pass. A duration series — read its percentiles. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… view, not two reads per request Every cache-servable request made two Invalidation point reads (~10M/day at current traffic; the module header had priced it at <=5,800). Each worker now holds the table in memory and resolves synchronously; a listener-form subscription (omitCurrent) is the doorbell that re-reads it on any committed change, and invalidation.syncInterval (1 min) is the backstop against delivery stopping silently. Until the view has loaded under a live subscription — at boot, after subscribe() fails or the subscription closes, after a read fails — resolution falls back to the per-request reads and LKG exactly as before. Re-reads run detached (a doorbell can fire inside the writer's request context). The Invalidation database stays single-table because a subscription is priced per database. New invalidation_error kinds: view-read-error, view-subscribe-error. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…he bypass token to Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…e owner, so an earlier due time and a change mark survive
A node that does not own the row (about three calls in four) read nothing locally, and its whole-row put
replaced the owner's row: an overdue row pushed back to the current minute, changedAt and demandPeriod
wiped. It now asks the owner over POST /prerender_peer/due-now (peer token, a leaf that never forwards
onward) and writes nothing itself; if the owner cannot be asked or refuses, it files locally as before,
and a failing owner is not asked again for 30s. queue.dueNowForward.{enabled,timeoutMs}; counted as
prerender_ops due_now_forward.
Touches http_handlers/index.js (serve agent's file) to route the new peer path: one import, two lines.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rops a stored 404 Now that a HEAD goes upstream as a HEAD, its status is the origin's HEAD handler's — and many answer 200 to any path. A HEAD may still confirm a stored 404 (as the background HEAD re-check does), but reopening a gone-suppressed target from traffic, dropping a stored 404, and the dry run's would-serve-live count all take a GET's status. The origin resource now carries its `method` (added with the upstream changes that follow). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…off by default; drained 304 bodies - A proxy that found a PrerenderedPage row it would not serve (stale — which includes probe hard-expired pages — blob faults, a render-now fallback over a page) now strips the crawler's validators upstream AND skips the local conditional, as only `invalidated` did. The crawler's If-Modified-Since is our render time; compared with an origin Last-Modified that does not track content it answered 304 locally, and the crawler kept the pre-change snapshot. A true miss and a raw serve keep ordinary conditional handling. - A snapshot carries `ETag: W/"<lastCachedMs>"` beside Last-Modified, so If-None-Match names the exact render (no one-second HTTP-date granularity, no cross-node clock). - `page.serveLinkHeader` defaults to FALSE (the pre-0.97.0 behaviour) until stored rows are sampled. Newline-joined stored headers (puppeteer joins repeats with `\n`) are appended as separate values — which also restores repeated x-robots-tag/vary/cache-control on cache serves — and a value no response may carry is counted (`serve_error` `bad-header`) with a rate-limited log instead of an error log per serve. - A GET body released by a local 304 is drained (up to 1 MiB) so the pooled connection is reused; past that, and for HEAD, the undici stream is destroyed. Cancelling the Readable.toWeb wrapper is still avoided: re-verified, 10 of 20 cancels throw ERR_INVALID_STATE against an origin that sends its body in one write. - A relative origin `Location` is resolved against the public request URL. - The peer token header (`peerRescue.header`) is never forwarded to the origin. Tests: botServePath, response, upstream. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… snapshot ETag, Link off by default + split joined headers, invalidation own-write visibility, drained local-304 bodies, HEAD can't reopen or drop, absolute Location, peer token never forwarded Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…robe before expiring it
renderCheck acted on the stored baseline's word, so it expired correct renders whenever the origin had
moved since the probe looked: an intra-day availability change, an off-schedule reprice before the canary
trips, a false trip's glitch baselines, and — in interval or continuous mode with no invalidation — against
a baseline of any age. A disagreeing render is now suspect: one confirming probe of the URL (detached,
paced node-wide at ratePerSecond via a shared-buffer slot, shed past a minute) decides. Still disagreeing:
expired and re-filed (confirmed); agreeing: the baseline is updated and nothing expires (cleared). No
request when the origin is known to have moved since the probe last observed the URL (untrusted, left to
the pass); one confirmation per stored observation (bounded). recordPageClaim still resolves { stale: true }
for a suspect render, so RenderQueue's change_lag guard keeps its meaning.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ld holds back only its route - shrinkGuard measures the DOCUMENT: refuse a urlset whose entry count fell below (1 - maxRatio) of the stored entryCount by at least minUrls, never would-be-unlinked / attributed (which counted forward shear and re-keying as shrink, tripped every walk and froze departures). An identical shorter document is accepted after shrinkGuard.acceptAfter (default 3) refusals. New sitemap_shrink_refused / _accepted series. - An index that stops listing more than maxRatio of its children (by count or by the entries they held) is refused: nothing is pruned. - A failed child re-links only the held URLs on a route its stored entry sample covers (siblings when it was never stored); every other departure of the walk proceeds. - The 304 re-read fetches only for held URLs still unattributed after the walk on a route with a departure/arrival action, only children whose sample shares that route, with one retry. - The sitemap bypass token needs https (or a local origin). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Stubs are found on this node's replica only, and the cascading delete replicated page, schedule and probe-state deletes cluster-wide, including where the row is whole. Now a stub is deleted only if it is still url-less on re-read with no owner-local schedule row and no rendered page, and only through the raw table. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…al only for newly dropped entries
- The admin sitemap detail reports entriesStored beside entryCount, and
the console pages over the stored sample and says so ("showing the
first 500 of N") instead of offering empty pages past it.
- A route or probe-rule list set is refused only for entries it NEWLY
drops relative to the running value, so a bad entry the file layer
already drops no longer blocks every whole-list edit from the console.
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ccept, failed children hold back only their route, narrowed 304 re-read, index-level dropped-child guard, https-only token, safe stub delete, console paging past stored entries, 409 only for newly dropped entries Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…o longer advises reprobeAfter Plugin v0.97.0 emits probe_* per probed batch (not once per finished pass) and retired changeProbe.reprobeAfter. The pass card is now "Probe passes — counts, not a live gauge", its help and empty state describe both emit shapes, the skip note reads a large skip share as resumed passes re-walking ground (a node restarting mid-pass), and the settings table and settings-card description drop reprobeAfter; the plugin's status settings stop reporting it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rmed-pass record dry runs can't overwrite, render check confirms by one paced re-probe, dry-run anchor serving, per-row fault isolation + resumable thrown passes, re-asserted stand-down, coverage in every sweep, resume cursor, stale-retry skip, console text, direct epoch read, PK on probe patches Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> # Conflicts: # packages/plugin/src/metrics.js
…p detail paging over stored entries); v0.22.0 Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rst outcomes Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
… a leftDue release carries the miss count A mark filed due now is typically granted 0.2-1.5s later, often in the same second; floored to its second the grant read as earlier than the mark and a correct first render of a changed page was dropped as changed-during-render. The slot gains a grantedMs word (8 words, 32 B). A leftDue release now carries the stored miss count instead of resetting it. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…the miss count Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…commits A clear inside a request transaction was removed from the worker's view before the delete committed; if the transaction aborted, that worker served pages still invalidated — failing open. A clear now lands with the post-commit reload (late is the safe direction); an apply is still patched, since it can only raise an epoch. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…rded origin body stream - The snapshot ETag is `W/"<lastCachedMs>-<deviceType>"`: one render job stamps the same lastCached on every device variant, so desktop and mobile shared a tag. The device comes from the resource, the request, or the stored row's cache key. - A local 304's drain has its own 5 s deadline, then destroys the source, instead of holding a socket until undici's 300 s bodyTimeout on a stalled origin body. - The proxied origin body is now `originBodyStream`, a small adapter whose enqueue is guarded, in place of `Readable.toWeb`. Characterized in test/originBodyStream.test.js (child process, tolerant): on Node 24.15 a toWeb cancel landing in the stream's creation microtask phase throws ERR_INVALID_STATE as an uncaughtException 20/20 (0/20 synchronous, next tick, or after a read — why a quick repro can miss it). Harper's own cancel of a body for a departed client, and the capture's over-cap cancel, now reach a cancel that cannot throw. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…for the commit, drain deadline, originBodyStream replaces Readable.toWeb on proxied bodies Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ld; dropped children judged by what they still hold - A: a child its index stops listing is refused when, after the walk, it still holds at least minUrls URLs listed nowhere else and more than maxRatio of its stored entryCount; refused, it is a failed child. - B: a refused shrink is accepted only once refused acceptAfter times (default 2) AND it survived a new origin version (Last-Modified or content; a dropped child uses its index's) or shrinkGuard.acceptAge (default 48h) passed. An accepted shrink unlinks at most shrinkGuard.releasePerWalk (default 5000) a walk; the rest release on later walks (sitemap_shrink_held_back). - C/D/F: the index guard runs after the walk on URLs listed nowhere, not child names, with the minUrls floor; each refused omitted child is a failed child, so the route hold-back covers shear into it. - E: each urlset row stores the full set of routes its entries fall on, which scopes the hold-back instead of the 500-entry sample. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…red rows count as covered, verified rescue, healed needs every changed slot shown - N1: the render check's confirming re-probes draw from the sweep's node-wide budget (the headroom a running sweep publishes, none at the ceiling or under origin backoff), so the node never probes past ratePerSecond; mayBeStale's invalidation term trusts only the baseline, not a covered walk-past. - N2: a pre-epoch page rendered by the render whose claim agrees with the new origin is verified (while verification is armed) and so served, instead of staying refused until re-rendered. - N3: 'healed' only when every changed slot is in the claim pair or on an armed mapped field that agreed; an unmapped slot that moved may postdate the heal render, so the pass acts. - N4: coverage is decided in the pass: a covered change is counted covered (probe_covered), not changed, with no detection lag, every pass; healed is a caught-up change. - N5: a confirming re-probe stands down (superseded) when a newer render's claim landed meanwhile, and clears only the claim it judged. - N6: a thrown reseed or boot resume is resumed from its cursor, bounded like the anchored pass. - RV1b noted beside anchorServedSince (one catch-up on the first boot after upgrade; accepted). Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ill hold, shrink accepted only after a new origin version or acceptAge with releasePerWalk, refused omissions count as failed children, route sets per child, minUrls floor everywhere Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…, verified rescue for agreeing pre-epoch pages, healed only when every changed slot is checked, covered changes counted apart, re-probes never wipe a newer claim, bounded resume for reseeds and boot resumes Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
harper-joseph
marked this pull request as ready for review
September 30, 2026 09:46
…ce, visible hold-back, stale route keys - sitemap.departure.maxPerWalk (default 5000, -1 none): one budget per walk across every child, from normal prunes and accepted shrinks alike. Departures past it are re-linked to the child that unlinked them, whose validator is cleared, and depart on later walks (departure_deferred) — lossless, unlike maxActions. Replaces the per-child sitemap.shrinkGuard.releasePerWalk / Sitemap.shrinkRelease (never released). - sitemap.shrinkGuard.newVersionAfter (default 20h): a new origin version confirms a refused shrink only if it arrives that long after the first refusal; a touch or re-publish of the same bad build is not a rebuild. - The refresh result and progress row carry holdBack: each child departures are held back for, why (refused-shrink / failed-child / budget), since when, and when a refusal can be accepted. - A failed child whose stored route keys the current config no longer produces also holds back its siblings' URLs, so a route edit cannot turn its hold-back into false departures. - Nested sub-index accounting (child-sitemap units) documented: it fails closed. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…shared round-robin, off when departures are off; route-table fingerprint for stale keys; drained dropped children not re-accepted - The walk budget counts only departures the executor will act on (decideDeparture on the post-walk target): suppressed targets and routes with no departure action no longer push real departures back. - Shared round-robin across the children that unlinked them, so one child's mass event cannot starve the rest walk after walk. - No metering when departures are off or no route departs (arrival-only deployments). - Per-URL error handling in the budget read and the re-link: one failure no longer aborts the rest. - A child's stored route keys are trusted only under the route table they were computed with (Sitemap.routesFingerprint, routeClass.js routeTableFingerprint): a route added above another or an ingress-mode switch now widens a failed child's hold-back to its siblings. - A dropped child accepted once (Sitemap.shrinkAcceptedAt) drains without being refused and re-accepted every walk; a refusal with no recorded first time records one. - holdBack entries: failed-child carries heldBack and only children that held something back are named; budget carries deferred; per-walk entries carry at, refusals since. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…nge to cache, page size, filing, departures - Change probe view, Change safety: action errors (the G2 'every change was expired' check), caught up, ignored, covered, anchored-pass outcomes (skipped flagged), detection lag from pass start and from the previous pass (median, ≈p95, per rule), and the render check's outcomes. - Queue view, Change to cache: render change_lag_ms per route — median, ≈p95, mean, trend. - Queue view, Page size: render_size bands, share over 1 MB (one decimal) by route and device. - Queue view, Filing: due_now_forward outcomes (fallbacks flagged) and keeper_unschedulable. - Sitemaps view, Departures: the departure family grouped by what it means for a page (acted, shear, held back, deferred, skipped, lost), shrink refusals/acceptances, and the live hold-back list from each root's run row. - The metric guard no longer waives any of these series. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…tes, a 304 clears stale shrink state, one route key per held URL; console Since cells, lost-URL counts, inconclusive re-probes Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Plugin 0.97.0, console 0.22.0. Fixes from a full review of the plugin at
b312c1d, built in five areas, merged here, then reviewed again as one branch in three more rounds; those findings are fixed too. Every fix has a regression test that fails without it (plugin 1,814 tests, console 484, lint and format clean).Deploying this needs a worker restart (schema changes, a renamed lease buffer) — see Deploy notes.
What it fixes
Sitemap walk
sitemap.shrinkGuard): a urlset whose entry count falls below(1 − maxRatio) ×its stored count by at leastminUrlsis refused and counts as a failed child. A refused shrink is accepted only afteracceptAfterrefusals and either a new origin version (changed Last-Modified or content digest) arriving at leastnewVersionAfterafter the first refusal, oracceptAge. A child the index stops listing is judged after the walk by the URLs it still holds (listed nowhere else), and an index is judged the same way, so renamed children are not a partial index. Every check has theminUrlsfloor.domains, only over https, and redirects are followed manually with the decision re-made per hop (it used to follow redirects to any host with the token attached).sitemap.departure.maxPerWalk, 5000): across every child, from normal prunes and accepted shrinks alike, spent only on departures that get an action, and shared round-robin across the children so one child's mass event can't starve the rest. Departures past it are re-linked to the child that unlinked them (validator cleared) and depart on later walks — deferred, not dropped (departure_deferred). A well-formed partial build that loses less thanmaxRatiocan no longer depart everything it dropped in one walk.holdBack— each child departures are held back for, why (refused shrink, failed child, budget), since when, and when a refusal can be accepted. A failed child's stored route keys are trusted only under the route table they were computed with (a fingerprint on the row), so a route edit — including a route added above another or an ingress-mode switch — widens its hold-back to its siblings instead of producing false departures.http-gone(they were re-minted and re-rendered in a loop).entryCountkeeps the true count); the console pages over what is stored.DELETE /sitemaps/<url>, collection revalidate,POST /sitemapswithbackground: false) run detached from the request transaction.Change probe
reprobeAfterold — so a change caught by a daytime or manual sweep hid that URL from the next night's pass.reprobeAfteris retired (accepted, unused).probe_anchorcounts each outcome.probe_covered, notchanged, and a pre-epoch page whose claim agrees gets a verification so it serves. "Healed" requires every changed slot to be one the page check compares — a change to an unchecked slot (e.g. a strikethrough price) is acted on.changeProbe.renderCheck, default on): a new render whose page claim disagrees with the probe's stored observation gets one detached confirming re-probe, paced from the headroom the sweep leaves underratePerSecond(the total origin rate never exceeds it); the page is hard-expired and re-filed only if the origin still disagrees, and a re-probe never clears a newer claim.probe_errors,probe_caught_up,probe_ignored. The first 1,000 failed actions of a pass are retried once after the walk, unless the row moved since.changeProbe.canary.schedule, default off).probe_detection_lag: an upper bound on detection lag at every detected change.Render queue
RenderSchedule.urgentAt,queue.ready.urgentHeadStart0.5): they used to score 0 and sit behind every overdue row. A detected change still outranks an equally late ask; bulk re-files (collection revalidate, sitemaprevalidate: true) are not marked. Both head starts scale withsitemapBoost.fileDueNowoff the row's owner forwards to the owner (POST /prerender_peer/due-now, peer token;queue.dueNowForward) instead of a whole-row write that replaced the owner's row and its change mark. Falls back to the local write on refusal or connect failure; a plain ask is left to a slow owner.supersededoutcome). An ask filed mid-render survives the render.keeper_unschedulable).render change_lag_ms(change mark → render landed) andrender_size(decoded size bands).Serve path
Location(resolved to absolute) andContent-Language— a proxied 3xx used to reach the crawler with no target.W/"<lastCached ms>-<device>"andLast-Modifiedfrom the render time instead of the origin document's validators.originBodyStreaminstead ofReadable.toWeb: cancelling atoWebstream in the window after creation throwsERR_INVALID_STATEas an uncaught exception (reproduced 20/20 in that window). Measured equal or cheaper per body; backpressure, truncation errors and the capture tee are tested.recordInvalidationreturns, and a clear lands with its commit (an aborted clear can't unguard a worker).\n-joined stored headers are appended as separate values (repeatedvary/x-robots-tag/cache-controlwere dropped on cache serves).page.serveLinkHeadermakes the storedLinkheader servable — default off, as before, until stored rows are sampled.Admin, config, metrics
POST /prerender_admin/revalidateno longer throws (nextRenderTimewas undefined since 0.94.0, and the throw rolled back the render it filed);no-undefis enabled for the plugin with Harper's declared globals.ConfigOverride.fileHash): layers reportmasking(the file changed since) andredundant(it now matches). An override that would newly drop route or probe-rule entries is refused (409).onConfigAppliedlistener run detached from the committing request's context.render.negative.statuses4xx integers except 429, admin read limits.host.pluginVersionis read at module load.Console (0.22.0)
Every series this release adds is charted — the metric guard waives none of them:
change_lag_msper route — median, ≈p95, mean, trend. Page size:render_sizebands and the share over 1 MB by route and device. Filing:due_now_forwardoutcomes (fallbacks flagged) andkeeper_unschedulable.New config options
sitemap.shrinkGuard.maxRatio/.minUrls/.acceptAfter/.acceptAge/.newVersionAftersitemap.departure.maxPerWalkchangeProbe.renderCheckchangeProbe.canary.schedule[](fixed interval)changeProbe.reprobeAfterqueue.ready.urgentHeadStartqueue.dueNowForward.enabled/.timeoutMspeerRescue.tokenis set)page.serveLinkHeaderinvalidation.syncIntervalNew metrics
probe_errors,probe_caught_up,probe_ignored,probe_covered,probe_anchor,probe_detection_lag,probe_render_mismatch;renderchange_lag_msand outcomesuperseded;render_size;queue_healthkeeper_unschedulable;prerender_opsdue_now_forward(incl.timed-out),sitemap_shrink_refused,sitemap_shrink_accepted; sitemap departure outcomesrelinked,deferred,listed-unchanged,unknown-child;invalidation_errorview-read-error/view-subscribe-error;serve_errorbad-header.probe_*rows are now per batch: sum values, don't count rows. Documented inMETRICS.md.Deploy notes
RenderSchedule.urgentAt,ProbeState.renderRefiledAt,ConfigOverride.fileHash, and onSitemaproutes,routesFingerprint,shrinkRefusals,shrinkRefusedVersion,shrinkRefusedAt,shrinkAcceptedAt. The lease buffer is renamed (render_queue_v3): expect the usual one-time duplicate-render burst on restart.ConfigOverrideis sealed; a row carryingfileHashreplicating to a not-yet-restarted node is untested).domains, or onhttp://, is now fetched without the bypass token (and fails if the edge requires it).masking: nulluntil re-saved; rows now identical to the file showredundant: true— safe to clear.sitemap.departure.maxActions: -1: a large legitimate removal (a clearance event) departs over several walks instead of one. Setsitemap.departure.maxPerWalk: -1to restore the old behaviour.acceptAge); the change probe still catches availability changes meanwhile.Not in this release
A truncated origin body leaves the crawler's connection open (Harper pipes without an error listener; predates this branch). Own-code failures that answer 500 instead of falling back to the origin; a cap on UA-derived analytics bot labels; expiring non-default device pages on a probe trip; malformed URLs in prefix mode (500 → 400); the probe's swrTtl-relative hard expiry (don't raise
page.swrTtluntil it is fixed).🤖 Generated with Claude Code