Skip to content

fix(pm): board-snapshot walks a bounded delta before the backfill, so the live board is re-read every run - #18054

Merged
os-project-manager merged 5 commits into
mainfrom
claude/issue-18045-board-snapshot-delta-first
Sep 13, 2026
Merged

os-project-manager merged 5 commits into
mainfrom
claude/issue-18045-board-snapshot-delta-first

Conversation

@claude

@claude claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor

Fixes #18045

Maintainer ruling, verbatim and untranslated (skills seat chat, 2026-09-13T14:58Z): 「现在就派发处理 备份缺口」

Premises, all four checked against origin/main c185d08 before any edit

  • P1 holds. selectWalkPlan reaches incremental only once history completes, so while the backfill runs no walk ever carries a recent since. Confirmed on board-archive tip b7c5f578: walk_phase: history, next_since: null, resume.stopped_by: budget, history cursor 2026-08-03T11:51:21Z, board.read_at: null, count_check: pending.
  • P2 holds. 5232 records under board/issues/, highest number 17460; 18010, 18020 and 18025 are absent. Read with git ls-tree over a shallow fetch of the branch into a private ref — the shared checkout was never switched.
  • P3 holds. 16 open pull requests, none touching scripts/pm/board-snapshot.mjs. (feat(pm): two report-only patrol rows — the decision card's four-facet block, and finding outliving its grade #18033 touches scripts/pm/check-half-states.mjs, which this file imports from; no overlap in this diff.)
  • P4 holds. .github/workflows/board-snapshot.yml is unchanged — same cron, same 800-request default, one GITHUB_TOKEN, no retry loop.

The fix, in scripts/pm/board-snapshot.mjs alone

A 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_SLICE exists at all.

New manifest keys. walk.delta = { complete, cursor, since, slice } sits beside walk.history and is never merged into it. next_since becomes the delta's high-water mark instead of the backfill's February cursor. resume.phase now names the WALK that stopped (open / delta / history / incremental) rather than the phase. run gains delta_ran, delta_completed_here and delta_requests — in run, which materialManifest strips, 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, so count_check stops being permanently pending in 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; else walk.open_set.completed_at minus the skew; else next_since; else generated_at minus the skew. On the measured manifest generated_at is 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.
  • The self-test gains an offline harness that drives snapshot() end to end against an injected fetch — no network, no token, restored in a finally. The card assumed one existed; it did not, so the walk order and the budget split had no instrument at all.
  • Ablation. Deleting the 25-line delta step turns 10 of 125 cases red, THE REGRESSION among them (blob f3723530 to e0cdbd7b, restored to f3723530, git diff HEAD empty). 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/objectstack derives the same 33 commands on the merged head as before it. Every one runs in the foreground with $? captured before any pipe, and --ran reconciles the list; the verdicts are in the os-dev-report comment on the card.
  • skip-changeset: the root package is private and no published package's files[] carries scripts/, so this diff publishes nothing.

Generated by Claude Code

…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>
@claude claude Bot added the skip-changeset PR has no user-facing published change; bypasses the changeset gate label Sep 13, 2026
@claude

claude Bot commented Sep 13, 2026

Copy link
Copy Markdown
Contributor Author
  • Served-tier: 1230/1230 claude-fable-5-1 — harness model stamp counted over this seat's own transcript (non-sidechain assistant messages a model served; <synthetic> harness notices excluded) at 2026-09-13T16:11Z; get_session external_metadata.last_served_model read claude-fable-5-1 at 2026-09-13T16:11Z.

Contract review

Head: c2a54670 (PR #18054, card #18045) — read at 2026-09-13T16:12Z by the skills seat at the contract-review tier. NOT GOVERNED, measured: scripts/pm/board-snapshot.mjs only (+588 −34; the workflow untouched) ⇒ in-seat review, then ready + auto-merge by this seat. Maintainer direct dispatch 「现在就派发处理 备份缺口」 is the ruling of record.

① derived judgments — the archiver re-reads the live board every run, bounded, before the backfill:

  1. Delta first (selectDeltaPlanwalk('delta', 'all', budget.slice(300))): state=all&sort=updated&direction=asc&since=…, every row seen is archived (open or closed, issue or PR); a spent slice raises SliceExhausted inside the delta only — the run continues into the backfill with the remaining 500. The 300/500 split is stated with its reason (a quiet window costs a page; a catch-up buys ~250 numbers a run; the backfill is slowed to five eighths, never starved). Accepted.
  2. The anchor, deviation accepted: the delta's own cursor; else walk.open_set.completed_at minus a 30-minute skew; else next_since; else generated_at minus the skew. The card's literal anchor (generated_at) would have skipped the measured three-day gap (open set enumerated 09-10T15:39Z, stamp 09-13T14:22Z) — the dev falsified the card's spelling with the live manifest and the chain is the right order. --full and --since= switch the delta off (they ARE the delta); a completed history walk switches it off (the incremental phase is the delta).
  3. Manifest: walk.delta = { complete, cursor, since, slice } beside walk.history; next_since = the delta's high-water mark; resume.phase names the walk that stopped (open / delta / history / incremental); per-run counters live under run (stripped by materialManifest, so no manifest-only commits); the board's open count is bought after the delta so count_check leaves pending. The live b7c5f578 manifest is a driven fixture, so the compatibility with today's archive is measured, not assumed.
  4. The harness: the card's premise that --self-test already drove snapshot() was FALSE (pure functions only); the dev built driveSnapshot() / fakeBoard() over an injected fetch (no network, counter zeroed and restored) rather than pinning the walk order by source text. The regression is pinned against a 900-row closed backlog after a first ablation stayed green on a four-row board — the phantom was caught and fixed in its own commit.
  5. Constraints kept: one GITHUB_TOKEN, no retry loop, budget 800, cron and workflow unchanged (P4); no .claude/**. origin/main 226970bb merged in; the changed blob is identical across the merge.
  6. Two 「noted, not filed」 items stand as notes: the workflow comment's stale 「70-case」 count (already false on main; next editor of the workflow), and the module-global requestCount shared by two runs in one process (no shipping caller does that).

Seat measurements on the head tree (git archive c2a54670): board-snapshot.mjs --self-test → 125 cases pass across 10 batteries, exit 0 (base 226970bb: 89 across 8, exit 0 — the control). --pair 18054 → exit 0 at 2026-09-13T16:11Z. Checks on c2a54670 at 2026-09-13T16:11Z: 36 runs, 0 red, 1 running (Lint & Repo Gates) — the queue's own gate holds the merge until it is green. Dev's ablation: the delta step deleted → 10 of 125 red including the regression, restored by blob hash.

② semver: scripts/pm/** publishes nothing; skip-changeset is right.

③ boundary flags: open_questions empty. Landing consequence: from the next scheduled run (20:07Z today) every card created or updated since 09-10 enters board-archive within the delta's slice per run; the R2 copy still waits on the maintainer's four secrets.

Implemented-by: claude/issue-18045-board-snapshot-delta-first
Reviewed-by: session_01DAcomhvR9kKizeYgg89Vo8

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/l skip-changeset PR has no user-facing published change; bypasses the changeset gate

Projects

None yet

2 participants