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

@os-project-manager
os-project-manager marked this pull request as ready for review September 13, 2026 16:12
@os-project-manager
os-project-manager added this pull request to the merge queue Sep 13, 2026
Merged via the queue into main with commit e248c4d Sep 13, 2026
38 checks passed
@os-project-manager
os-project-manager deleted the claude/issue-18045-board-snapshot-delta-first branch September 13, 2026 16:45
akarma-synetal pushed a commit to akarma-synetal/framework that referenced this pull request Sep 17, 2026
…bjectstack-ai#18266)

Fixes objectstack-ai#18137

Scope is the card's **shape 3 only**, per the skills seat's correction
(comment 5673977209): shape 1 (delta-first) landed as objectstack-ai#18054, shape 2
(more runs) and any budget raise are the maintainer's, shape 4 (an
objectui port) is a separate card. This PR changes no cron, not the
800-request cap and not the 300-request delta slice.

## The defect, measured rather than argued

`count_check: pending` is the tool's own "could not check" — but the
exit code is built from the count check's `ok`, and `pending` leaves
that `null`. So a run that archived days-old state exited 0, and the
workflow step that reacts to `steps.snapshot.outputs.exit_code != '0'`
never fired.

Read off this repository's own archive — every manifest `board-archive`
has ever carried (21 commits touching `board/manifest.json`, `2731e47a6`
back to `16259758b`) against this workflow's run history:

| manifest `generated_at` | `count_check.verdict` | `walk.delta.cursor`
| run conclusion |
|---|---|---|---|
| 2026-09-10T15:40Z | `ok` (580/580) | — (the run that finished the open
set) | success |
| 2026-09-10T20:23Z … 2026-09-13T15:30Z (13 runs) | `pending` — "the
board's own count was not read this run" | (no delta block) | success
×13 |
| 2026-09-13T20:23Z | `pending` — "this run's delta did not catch up" |
2026-09-11T02:47Z | success |
| **2026-09-14T02:37Z** | **`pending`** (expected 512 / archived 485) |
**2026-09-12T00:40Z, two days behind** | **success** |
| 2026-09-14T08:37Z / 14:28Z / 20:24Z | `pending` | 2026-09-12T14:32Z /
2026-09-13T11:19Z / 2026-09-14T04:46Z | success ×3 |
| 2026-09-15T02:34Z | `surplus` (expected 519 / archived 536) |
2026-09-15T02:30Z | **failure** (exit 2) |

Nineteen consecutive `pending` runs, 2026-09-10T20:23Z through
2026-09-14T20:24Z, every one of them green — including the run on the
morning an account suspension destroyed cards the archive was two days
too far behind to hold. That is the card.

The same manifest replayed through `renderRun`, before and after this
change (measured, not transcribed — the "before" leg imports the file as
of `cb648cb1`):

```text
========== BEFORE — exit 0 ==========
  count check  PENDING — archived 485 open issue(s); the board reports 512.
               Why not a verdict: the open set is complete but was enumerated in an earlier run and this run's delta did not catch up with the live board, and the closed history is still walking — cards closed or opened since then are archived in their old state
  paused       the per-run budget of 800 requests is spent — this is a planned stop, not a failure.
               The next run resumes the history walk at 2026-08-06T13:26:08Z.

========== AFTER — exit 5 ==========
  (every line above, unchanged, then:)
  freshness    STALE — no run has reached a count-check verdict since 2026-09-10T20:23:11.768Z (78 h ago); the window is 24 h.
               This archive is behind the live board by an unknown amount, so the run is RED rather than green: a backup that is
               behind while reporting success is the one state this row exists to end. Whatever this run DID read is committed
               before the step that reads this exit code — a stale archive is better than none.
               It clears on the first run whose delta catches up with the live board and whose count check reports a verdict again.
```

## What the row is, and the two spellings it deliberately does not use

A new manifest block, `freshness`, with three verdicts — `building`,
`fresh`, `stale` — and a new exit code `EXIT_STALE = 5`, distinct from
the count check's 2 (that one says the archive and the board *disagree*
about a count both sides were read for; this one says no such reading
has happened at all for a day).

**The anchor is the streak's start stamp, ⛔ not the delta cursor — and
that is a falsification of the literal reading, not a preference.**
`walk.delta.cursor` is the `updated_at` of the newest row the delta
archived, not the instant the board was last read: an empty page is a
short page, `nextWalkStep` ends the walk returning the cursor it was
handed, so on a quiet board the delta completes every run, the archive
is exactly level, and that cursor sits as far in the past as the last
card anyone touched. Ageing it reds a board for being quiet. This is
driven in `--self-test` rather than argued (a five-day-quiet board:
delta complete, `count_check: ok`, cursor five days old, verdict
`fresh`, exit 0) and ablated below.

**It is a stamp, ⛔ not a counter of consecutive runs.** The file's own
`materialManifest` exists because a value that moves on every run
commits a manifest-only diff on every scheduled run and buries the real
ones. A streak start is written once when the streak opens, carried
forward byte-identical while it lasts, and cleared when a verdict
returns: two manifest writes per incident, and the pinned idempotence
property survives untouched.

**The window: 24 h, measured from the first UNCONFIRMED run** — which is
itself one schedule interval after the last confirmed one, so a red
lands a day plus one interval after the archive was last known level. On
the four-a-day cron that is the 30 h the card asked for, and the script
carries no cadence constant at all, so the maintainer's cadence decision
touches nothing here. The calibration comes from the same 21 manifests:
the one legitimate catch-up in this archive's history — a bounded delta
slice closing a three-day gap after objectstack-ai#18045 — ran 2026-09-13T20:23Z to
2026-09-15T02:34Z, 30 h 04 m, and every run inside it held an archive
that did not have the newest cards. So the window never reds a single
stalled run, never reds a day-long gap, reds the tail of a worst-case
catch-up, and reds every run of the four-day streak this card was filed
on.

**`building` is why a PR run stays green.** The workflow's
`pull_request` leg walks into a temp dir with no manifest under a
`--limit`, so its open set never completes; without that verdict every
PR touching this tool would go red on a tool working perfectly.

## How the red surfaces, and what it costs the archive

Nothing. The step order is measured and kept: `Run the snapshot` →
`Commit and push the archive` → `Upload the archive to R2` → `Publish
the run to the summary` → the step that reads the exit code, which is
last in the job. A stale archive is better than none, so whatever a red
run managed to read is committed and mirrored before the run is allowed
to fail. The workflow's error line now names code 5 alongside 2/3/4, and
its step is renamed to say what it now judges.

⛔ The row prescribes no remedy: cadence, budget and slice are named
nowhere in the verdict. What it prints is what *clears* it — the first
run whose delta catches up and whose count check reports a verdict
again. A self-test case pins that: the stale block contains none of
"cadence", "budget", "slice" or "more runs".

## Tests

`--self-test`: **125 cases / 10 batteries → 145 cases / 11 batteries**,
all green; the 125 existing cases keep their outcomes. The new battery
("the freshness row: a stale archive is never a green run") is 18 cases
plus 2 added to the driven battery, including the card's own manifest
replayed to `stale` + exit 5, the correction's caught-up manifest
replayed to `fresh` + exit 2 (the count mismatch keeps its own code),
the boundary at exactly the window and one millisecond past it, and two
end-to-end driven runs.

Reverse verification — the fix committed first, each leg proving its
mutation reached the disk (block-occurrence counts, not a `grep -c` over
a multi-line block, which is what a first attempt got wrong and the
guard caught), restored with `git checkout HEAD -- path` and verified by
blob hash against `HEAD`:

| ablation | on disk | self-test |
|---|---|---|
| delete the `EXIT_STALE` leg from `renderRun`'s exit selection |
removed-block 1 → 0, marker 1 | **RED** — 2 of 145 fail: the card replay
and the driven stale run |
| age the delta cursor instead of the streak (the literal reading of the
card's condition (a)) | removed-block 1 → 0, marker 1 | **RED** — 2 of
145 fail, one of them the quiet-board case: the false alarm is real, and
the case that names it is an instrument and not a decoration |

Restore verified both times: blob `1ead53c1…` equals
`HEAD:scripts/pm/board-snapshot.mjs` and `git diff HEAD` is empty; the
tree re-runs 145/145 green.

Gates: `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` with no paths, on the merged head `d53749c2`
— 51 families derived, 51 run in the foreground with exit codes captured
by redirect (`cmd > log 2>&1; EXIT=$?`), 51 exit 0, reconciled with
`--ran`. `check:pm-dispatch-gates` was run the way its own header
prescribes (detached, log tailed) because its battery outruns a
foreground command cap.

⛔ Not measured here, and named rather than implied: the workflow file
cannot be executed locally (no runner, no `GITHUB_TOKEN` with this
scope), so its change is validated by the repo's own workflow gates —
`check:ci-filter-parity`, `check:workflow-status-functions`,
`check:workflow-step-name-quoting`, `check:self-test-workflow-commands`,
`check:step-collectors` — and by reading the step order. The 52
artifact-roster families, the 11 wide-population families and the one
path-scheduled CI job the derivation names are outside the runnable
total and are CI's.

## Acceptance notes

- `skip-changeset`: nothing published moves. `scripts/pm/**` and
`.github/workflows/**` ship in no package's `files[]`.
- Noted, not filed — the workflow header calls this script's self-test
"70-case"; it was 125 before this PR and is 145 after. A stale number in
a comment is a doc nit, not one of the three filing classes, and fixing
it is outside this card's defect class. Carrier: the next PR that edits
that paragraph.
- Noted, not filed — the latest scheduled run (2026-09-15T02:31Z, run
46) is already **red** at exit 2 with a `surplus` of 17 (board 519,
archive 536). That is this tool's destruction signature doing exactly
its job, and it is consistent with the 2026-09-14 suspension destroying
cards the archive still holds; `--restore=N` prints them back, and a
number deliberately left gone belongs in `gone.json`. Acting on it is
the seat's and the maintainer's, not this PR's.

---
_Generated by [Claude
Code](https://claude.ai/code/session_01HZfg2AwVX191qCizp88gQr)_

Co-authored-by: Claude <noreply@anthropic.com>
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