Skip to content

fix(pm): post-stamped counts a same-second later comment as after the acknowledged one - #18297

Merged
claude[bot] merged 1 commit into
mainfrom
claude/issue-18295-post-stamped-after-count-same-second
Sep 15, 2026
Merged

claude[bot] merged 1 commit into
mainfrom
claude/issue-18295-post-stamped-after-count-same-second

Conversation

@claude

@claude claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor

Fixes #18295

The defect

unreadComments in scripts/pm/post-stamped.mjs picked newest as the last element of a list sorted by created_at with an id tiebreak, but derived after — the comments that landed after the acknowledged one — from a strictly-later comparison on created_at alone. Two comments written inside one second (a batch, a bot) with --ack-through naming the lower id: the refresh was rightly refused as ack-not-newest and the remedy line named the right id, but the refusal's head read 0 comment(s) landed after it and listed nothing — a count contradicting the refusal it sits in.

Two orderings answering one question is the whole defect, so the fix is one ordering, not a second comparison bolted onto the filter.

The fix

One comparator, byCommentOrder (created_at, then id), declared once beside commentCreatedMs and read by both the newest pick and the "landed after" set. id breaks only an exact tie, so it never outranks the clock; comments whose created_at is unreadable share one instant and are ordered by id — the same tie the sort already resolved that way. The window filter keeps reading the instant alone, because it compares a comment against a stamp, not against another comment.

Which refusal fires on which input is unchanged; only the count and the listing move.

Reverse verification — ablation from the committed state, hash-proved

Blobs: HEAD 0e6d55abf5a895c285bdb20879fb2024915543cb, pre-fix 48275456ba4f55f10422aa25c76cba7e45015e73.

Leg A — the unpatched derivation (pre-fix blob restored on disk; on-disk hash re-read and matched, byCommentOrder occurrences 0). The reproducer, called on the pure core:

REPRO kind=ack-not-newest after.length=0 after.ids=[] newest.id=101
REPRO head: post-stamped: REFUSED — --ack-through=100 is not the newest comment on #18295: 0 comment(s) landed after it. Nothing was written.

Restored with git checkout HEAD --; on-disk hash back to 0e6d55ab, git diff HEAD and git status --porcelain both empty. After the restore:

REPRO kind=ack-not-newest after.length=1 after.ids=[101] newest.id=101
REPRO head: post-stamped: REFUSED — --ack-through=100 is not the newest comment on #18295: 1 comment(s) landed after it. Nothing was written.
REPRO listing-names-101: true

Leg A says nothing about the new self-test cases — a whole-file revert takes them with it, so its --self-test is green at 204 cases. Recorded as observed, not as a pass.

Leg B — the derivation alone reverted, the new cases kept (mutation on top of HEAD; mutated blob a577b24bb6afa543d4db105f44cc8c9121e4e956, proved different from the HEAD blob and proved on disk by occurrence counts: new form 0, old form 1). --self-test exits 1, 4 of 212 case(s) failed:

✗ …and the later id COUNTS as after it — the count no longer contradicts the refusal it sits in (after=[])
✗ …so the refusal head reads 1, not 0
✗ …and lists that comment, so the seat reads it rather than a count
✗ …the newest pick and the after set read ONE order: whoever is newest is in the set the refusal lists

The fifth new refusal case stays green there on purpose: the refusal itself was always right, only its count was not. Restore: on-disk hash back to 0e6d55ab, git diff HEAD and git status --porcelain empty, --self-test back to 212 passing. Both legs run a trap … EXIT INT TERM restore against an absolute path; neither leaves a temporary file in the tree.

Self-test

204 → 212 cases across 10 batteries; the unread-knock battery's floor 41 → 49, the measured count. Added: the filed reproducer (after = [101], the head reads 1, the listing names 101), the newest/after agreement invariant, the ordinary later-second control restated as unchanged, the same-second pair with the ack naming the higher id still cleared, and the ordinary ack-the-newest case still cleared.

Gates

node scripts/pm/dispatch-gates.mjs --commands scripts/pm/post-stamped.mjs derived 32 commands; all 32 ran, all exit 0. Reconciliation on the same tree:

✓ dispatch-gates --ran: 32 derived famil(ies) accounted for — 32 run, 0 NOT-MEASURED

Among them pnpm check:pm-dispatch-gates (the gate source's own battery, run detached: 1730 cases, exit 0, 439.0s on this box), pnpm check:pm-post-stamped, pnpm check:nul-bytes, node scripts/check-self-test-wired.mjs. Plus, outside the derivation: node --stack-size=4000 node_modules/eslint/bin/eslint.js scripts/pm/post-stamped.mjs — clean; and a control-character self-scan over the file — no hit.

Acceptance notes


🤖 Generated with Claude Code

https://claude.ai/code/session_01HZfg2AwVX191qCizp88gQr


Generated by Claude Code

… acknowledged one

`unreadComments` picked `newest` with a `created_at` sort that tiebreaks on
`id`, but derived `after` — the comments that landed after the acknowledged one
— from a strictly-later comparison on `created_at` alone. Two comments written
inside one second (a batch, a bot) with `--ack-through` naming the lower id:
the refresh was rightly refused as `ack-not-newest` and the remedy line named
the right id, but the refusal's head read "0 comment(s) landed after it" and
listed nothing — a count contradicting the refusal it sits in.

Both now read ONE comparator, `byCommentOrder` (`created_at`, then `id`), so
the newest pick and the "landed after" set cannot disagree about which comment
follows which. No refusal changes which route it fires on; only the count and
the listing move.

Self-test: the filed reproducer (after = [101], head 1, the listing names it),
the newest/after agreement invariant, the ordinary later-second control
unchanged, and the same-second pair with the ack naming the higher id still
cleared. Unread-knock battery floor 41 → 49 (measured).

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HZfg2AwVX191qCizp88gQr
@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

Contract review

Served-tier: CONTRACT_REVIEW_TIER
Head-sha: ab9c45d24a61f82e5f8658beb606e822ebc87fb9

Reviewing seat: domain:skills, in-seat review at the contract-review tier — get_session read at 2026-09-15T07:15Z: configured_model = session_context.model = last_served_model, the constant's tier, no fallback notice this round. Card #18295, claim 5679281273, report 5679465556 (posted through the tool itself); Clause-②: no on the claim; default-tier build, seat review at tier.

① Derived judgments

  • Accept set / public surface: none moves — scripts/pm/post-stamped.mjs (+37/−4) against merge-base 5a18f32e; nothing published; skip-changeset read back (size/s). Non-governed ⇒ in-seat landing.
  • The change: one comparator (byCommentOrder: created_at, then id) declared once and read by both the newest sort and the ack-not-newest refusal's after set, so the two can no longer disagree about which comment follows which; which refusals fire is unchanged. The seat took the pure-core reading on both trees with the reproducer (two comments sharing one created_at, the ack naming the lower id): origin/mainack-not-newest, after empty, newest 101 — the filed contradiction; the head → ack-not-newest, after = [101]. Controls on both trees: the ack naming the higher id clears (acknowledged); an ordinary later-second comment is after the acknowledged one exactly as before. Self-test 204 / 10 on main → 212 / 10 on the head (the unread-knock battery's floor 41 → 49).
  • Gates 32 / 32 / 0 / 0 by --ran; the battery 1730 cases pass, 439.0 s detached, exit captured; --pair 18297 exit 0. Seat re-run on a detached worktree of the head: self-test 212, ESLint 0/0, check-self-test-wired OK, check-watch-hint-literal OK, 32 derived. Reverse verification by the dev, two legs with hash proofs: a whole-file revert (which also removes the new cases, so it demonstrates the defect reading only — observed and said) and a derivation-only revert on top of HEAD with the cases kept (4 of 212 red — the count, the head, the listing, the newest/after agreement; the refusal case stays green on purpose because the refusal itself always fired), restored and hash-proved.

② Semver level

Not applicable — nothing published moves; skip-changeset measured.

③ Boundary flags

open_questions: none. Three declared deviations, each answered: (1) a second ablation leg added because a whole-file revert cannot prove the new cases have teeth — accepted, the honest form. (2) the reproducer driven from a scratchpad probe, nothing left in the tree — accepted. (3) no 速读 section, scripts/pm/** being non-governed — accepted. Out of scope: none. Landing: in-seat once every check on this head is green — ready through the CCR route, auto-merge SQUASH, the enqueue event and the queue ref read, provenance below; #18296 (SERIAL, the same file) dispatches after the landing.

Implemented-by: claude/issue-18295-post-stamped-after-count-same-second
Reviewed-by: session_01HZfg2AwVX191qCizp88gQr

VERDICT: PASS


Generated by Claude Code

@claude

claude Bot commented Sep 15, 2026

Copy link
Copy Markdown
Contributor Author

Landing provenance — skills seat, session session_01HZfg2AwVX191qCizp88gQr, 2026-09-15T11:52Z. Non-governed landing (scripts/pm/**): review of record 5679503885 PASS on head ab9c45d24a61f82e5f8658beb606e822ebc87fb9, ACCEPT on #18295; every check on that head read green at 2026-09-15T11:51Z (24 success / 11 skipped / running none / failed none). The seat flipped the PR ready through POST …/pulls/18297/ccr/ready_for_review and armed auto-merge SQUASH through PUT …/pulls/18297/ccr/auto_merge; the added_to_merge_queue event reads 2026-09-15T11:51Z; the queue ref refs/heads/gh-readonly-queue/main/pr-18297-* read 1 fifteen seconds after the enqueue. Landing is confirmed by the three readings (merged_at + git log origin/main carrying (#18297) single-parent + the queue ref gone) at the next wake; then #18295's residue is stripped, the landing record posted, and #18296 (same file, SERIAL) dispatches.


Generated by Claude Code

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

Labels

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

Projects

None yet

0 participants