fix(pm): board-snapshot walks a bounded delta before the backfill, so the live board is re-read every run - #18054
Conversation
…hot re-reads the live board every run Once the open set completed, `selectWalkPlan` handed every run to the history backfill and `incremental` was unreachable until that backfill finished. The 2026-09-13T14:22Z run measured the result on `board-archive` tip b7c5f57: `walk_phase: history`, `resume.stopped_by: budget`, history cursor at 2026-08-03, `next_since: null`, `board.read_at: null`. The highest archived number was 17460 — #18010, #18020, #18025 and the twelve cards an account suspension destroyed that morning were in no snapshot at all, and at 800 requests a run the backfill was weeks from reaching them. The archive exists to answer one question after a loss; on the day of a loss it answered nothing. Maintainer ruling, verbatim and untranslated, in the skills seat's chat at 2026-09-13T14:58Z: 「现在就派发处理 备份缺口」 A DELTA walk now runs first on every run once the open set is complete: `state=all&sort=updated&direction=asc&since=<the archive's high-water mark over the live board>`, archiving every row it sees, open or closed, issue or pull request. It is bounded to DELTA_REQUEST_SLICE (300) of the 800-request budget and the backfill takes the remaining 500, so a quiet window costs a page and a busy one cannot starve the backfill. A spent slice is not a run stop: the delta writes its cursor, the backfill continues, and the next run resumes the delta there. The delta's first anchor is `walk.open_set.completed_at` minus DELTA_SKEW_MS rather than the previous run's `generated_at` — that stamp names a window this tool never walked, and on the measured manifest it would have skipped three days of board activity with the very walk added to stop skipping it. The manifest carries `walk.delta` beside `walk.history`, `next_since` becomes the delta's high-water mark, and the board's own open count is read right after the delta, so `count_check` stops being permanently `pending` in the history phase. `--self-test` grows an offline harness that drives `snapshot()` against an injected fetch — the walk order and the budget split exist nowhere but inside that function, so they are pinned by running it rather than by reading its source. 89 cases across 8 batteries before; 124 across 10 after. Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8 Co-authored-by: Claude <noreply@anthropic.com>
…board has The regression case passed on a tree with the delta step ablated: on a four-row board the backfill reaches today inside one page, so the row landed on disk whether or not a delta ran. A 900-row closed backlog with a comment thread each reproduces the pressure the real archive is under, and the case now names something only the delta can do. Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8 Co-authored-by: Claude <noreply@anthropic.com>
An absent archived card makes `readArchivedCard().issue` undefined, so an ablated tree threw at the first such assertion and the remaining cases never ran. Optional chaining turns each into a named red instead of one stack trace. Claude-Session: https://claude.ai/code/session_01DAcomhvR9kKizeYgg89Vo8 Co-authored-by: Claude <noreply@anthropic.com>
Contract reviewHead: ① derived judgments — the archiver re-reads the live board every run, bounded, before the backfill:
Seat measurements on the head tree ( ② semver: ③ boundary flags: Implemented-by: Verdict: PASS — the live board is re-read every run, bounded, anchored on the last full enumeration, with the regression pinned by a driven harness; the maintainer's direct order is delivered. Generated by Claude Code |
Fixes #18045
Maintainer ruling, verbatim and untranslated (skills seat chat, 2026-09-13T14:58Z): 「现在就派发处理 备份缺口」
Premises, all four checked against
origin/mainc185d08 before any editselectWalkPlanreachesincrementalonly oncehistorycompletes, so while the backfill runs no walk ever carries a recentsince. Confirmed onboard-archivetipb7c5f578:walk_phase: history,next_since: null,resume.stopped_by: budget, history cursor2026-08-03T11:51:21Z,board.read_at: null,count_check: pending.board/issues/, highest number 17460; 18010, 18020 and 18025 are absent. Read withgit ls-treeover a shallow fetch of the branch into a private ref — the shared checkout was never switched.scripts/pm/board-snapshot.mjs. (feat(pm): two report-only patrol rows — the decision card's four-facet block, andfindingoutliving its grade #18033 touchesscripts/pm/check-half-states.mjs, which this file imports from; no overlap in this diff.).github/workflows/board-snapshot.ymlis unchanged — same cron, same 800-request default, oneGITHUB_TOKEN, no retry loop.The fix, in
scripts/pm/board-snapshot.mjsaloneA bounded delta walk now runs first on every run once the open set is complete:
state=all&sort=updated&direction=asc&since=..., archiving every row it sees — open or closed, issue or pull request. The history backfill then continues from its own cursor with the remaining budget, its resume semantics untouched. A spent slice is not a run stop; only the run budget and a rate-limit refusal stop a run, exactly as before.The budget split: 300 of 800, and 500 to the backfill. A quiet six-hour window moves a few dozen numbers, so a steady-state delta costs one listing page plus a comment read per changed number and the backfill keeps very nearly the whole budget. A catch-up delta buys around 250 numbers a run, closing a multi-day gap in a day of scheduled runs rather than in the weeks the backfill needs to reach the same rows. In the worst case the backfill still gets 500 — five eighths of its former pace, slowed but never starved. An unbounded delta could starve it for good, which is why
DELTA_REQUEST_SLICEexists at all.New manifest keys.
walk.delta = { complete, cursor, since, slice }sits besidewalk.historyand is never merged into it.next_sincebecomes the delta's high-water mark instead of the backfill's February cursor.resume.phasenow names the WALK that stopped (open/delta/history/incremental) rather than the phase.rungainsdelta_ran,delta_completed_hereanddelta_requests— inrun, whichmaterialManifeststrips, so a per-run counter cannot commit a manifest-only diff every run. The board's own open count is bought right after the delta and before the backfill, socount_checkstops being permanentlypendingin the history phase.One declared deviation from the card's fix shape
The card spells the delta anchor as "the previous manifest's
generated_at, minus a small skew". Implemented instead as: the delta's own stored cursor; elsewalk.open_set.completed_atminus the skew; elsenext_since; elsegenerated_atminus the skew. On the measured manifestgenerated_atis 2026-09-13T14:22Z while the board was last enumerated in full at 2026-09-10T15:39Z, so anchoring on the stamp would have declared a window this tool never walked and skipped the exact three-day gap the card was filed about. The skew is 30 minutes, two runs' worth of the workflow's own 15-minute job timeout.Verification
node scripts/pm/board-snapshot.mjs --self-test: 89 cases across 8 batteries before, 125 across 10 after, exit 0 both. Two new batteries; the count-check battery's floor moves 15 to 17 and the roster floor 8 to 10.snapshot()end to end against an injectedfetch— no network, no token, restored in afinally. The card assumed one existed; it did not, so the walk order and the budget split had no instrument at all.f3723530toe0cdbd7b, restored tof3723530,git diff HEADempty). A first attempt stayed green: over a four-row board the backfill reaches "today" inside one page, so the row landed on disk whether or not a delta ran. The harness now carries a 900-row closed backlog, so the backfill provably cannot reach that day inside 800 requests — that fix is its own commit.origin/main(226970b) is merged in;node scripts/pm/dispatch-gates.mjs --commands --repo objectstack-ai/objectstackderives the same 33 commands on the merged head as before it. Every one runs in the foreground with$?captured before any pipe, and--ranreconciles the list; the verdicts are in theos-dev-reportcomment on the card.skip-changeset: the root package is private and no published package'sfiles[]carriesscripts/, so this diff publishes nothing.Generated by Claude Code