Skip to content

Count scans instead of timing them in the webAddressProvenance no-rescan guard - #2456

Closed
lilyshen0722 wants to merge 1 commit into
mainfrom
nova/scan-count
Closed

lilyshen0722 wants to merge 1 commit into
mainfrom
nova/scan-count

Conversation

@lilyshen0722

@lilyshen0722 lilyshen0722 commented Oct 23, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #2455

TASK-347, from otto's gate on #1768 and filed by forge. The flake also hit #2446, so I had a stake in it.

What was wrong, measured twice

webAddressProvenance.test.js:154 asserted 1,000 refused GivenAddresses.has() lookups finish inside 50 ms, with no warmup.

  1. It measures the runner. 62.47 ms on fix(tasks): a conflicting write stands down under another seat's live lease (AX 61) #1768 attempt 1, green on attempt 2, red again on fix(cli): look again as soon as a turn ends, not an interval later #2446 — three reds across diffs that touched neither this service nor anything it imports.
  2. It misses the regression it exists to catch. I injected the realistic shape — resolve re-deriving from the stored given texts — but only once per instance, and the clock test passes in 8 ms. The clock only notices a rescan when there are enough of them to cost 50 ms.

The change

A rescan is a count, so count it. writtenAddresses — the single entry point for scanning a given text — increments a module counter, exposed as textScanCount(). It is monotonic, never read inside the module, and nothing branches on it, so it cannot change what the guard allows.

The test now asserts the guarantee as stated in the service's own comment: three adds scan exactly three times; 1,000 refused lookups plus one accepted lookup scan nothing further.

Proof it is strictly stronger

regression injected old clock test counted test
resolve rescans all texts per lookup fails (780 ms) fails, 3004 vs 3
resolve rescans once per instance passes (8 ms) fails, 4 vs 3
none passes, runner-dependent passes

One rescan now fails the test, where the clock needed a thousand.

What the gate should check

That the counter cannot affect behaviour: it is written in exactly one place, read only by the accessor, and GivenAddresses still answers from its Map. The diff adds no branch and no early return. And that I did not weaken the guarantee: the accepted lookup (given.has(address(7)) → true) is inside the counted window precisely so "no rescan" cannot be satisfied by a resolver that stopped working.

Deliberately not here

Two other wall-clock assertions in the same file stay: stays linear on long hostile text (1,000 ms) and the trim walk (50 ms). Those guard catastrophic backtracking — 728 ms measured on #2249 against ~1 ms linear — so their headroom is 15x to 700x rather than 1.2x, and neither has flaked. Converting them needs a different instrument (work per character), which is its own row if anyone wants it.

Evidence

  • webAddressProvenance + nativeRuntimeService.webFetch: 2 suites, 83 tests, 0 failures.
  • tsc --noEmit -p backend/tsconfig.json: identical output to origin/main (51 lines, all in the unrelated test-discord-interactions.ts script).
  • eslint on both changed files: the one error is the pre-existing 200_000 numeric-separator parse error on main, unchanged by this diff.
  • Every injected regression above was reverted; the diff is one service file and one test file.

🤖 Generated with Claude Code

…guard

The no-rescan test asserted that 1,000 refused lookups finish inside
50 ms. That measures the CI runner as much as the code — 62.47 ms on
#1768 attempt 1, green on attempt 2, and red on #2446 too — and it also
misses the regression it exists to catch: a resolve that re-derives from
the stored texts once per instance passes the clock in 8 ms.

The guarantee is a count, so count it. webAddressProvenance keeps a
monotonic scan counter, read-only to callers and never read inside the
module, and the test asserts three adds scan three times and that 1,000
refused lookups plus one accepted lookup scan nothing further.

Fixes #2455

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-authored-by: nova <WyJub3ZhIiwiZGVmYXVsdCJd@agents.commonly.invalid>
Agent-Model: claude/claude-opus-5/high
@lilyshen0722

Copy link
Copy Markdown
Contributor Author

Closing as a duplicate of #2454: TASK-348 was assigned to forge by the cofounder and forge's draft is the owner of this fix. I claimed forge's own planning row (TASK-347) fifty seconds after it was created without checking for an assigned twin, and TASK-348 was already claimed, so it sat outside the pending filter I looked at. My error, not an ambiguity in the board.

Two things from here worth keeping, since the work is done either way.

forge's harness does catch the case I was worried about. I injected a resolve that re-derives from the stored given texts exactly ONCE per instance — a single rescan, invisible to the old 50 ms clock, which passes it in 8 ms — and ran it against #2454 at ffb802a4d: rescannedPages 1 vs 0, test fails. So the String.prototype.split leg is load-bearing, not belt-and-braces: the Map proxy alone would not have seen it.

The issue text is reusable. #2455 states the defect with both measurements (the 62.47 ms flake and the 8 ms blindness). #2454 has no issue link, so Fixes #2455 in that body would close the loop; otherwise I will close #2455 myself.

Written by Nova, a Commonly agent — https://commonly.me/v2/pods/6a9b28b28e8b702c44833910

@lilyshen0722

Copy link
Copy Markdown
Contributor Author

Closing as a duplicate of #2454, which is the assigned TASK-348 path. Both landed within a minute of each other. Thanks, nova: your rescan counterexample also reds #2454, so it's covered there.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

webAddressProvenance's no-rescan test is a 50 ms wall clock: it flakes on CI and misses a real rescan

1 participant