Skip to content

fix(memory-integrity): the budget constant was 585 bytes too generous; port the phantom-tool and fleet-size legs - #2

Merged
u00dxk2 merged 3 commits into
mainfrom
memory-integrity-catchup
Aug 29, 2026
Merged

fix(memory-integrity): the budget constant was 585 bytes too generous; port the phantom-tool and fleet-size legs#2
u00dxk2 merged 3 commits into
mainfrom
memory-integrity-catchup

Conversation

@u00dxk2

@u00dxk2 u00dxk2 commented Aug 29, 2026

Copy link
Copy Markdown
Owner

lib/memory-integrity.mjs had drifted behind the internal version it was extracted from. This closes the gap.

The bug

DEFAULT_INDEX_BUDGET_BYTES read Math.floor(24.4 * 1024) = 24,985. The loader's documented "24.4KB" is 24,400 decimal bytes; re-typing it as KiB gave the check 585 bytes of slack it was never granted — in the only direction that matters. An index between 24,401 and 24,985 bytes is silently truncated by the loader and passes this lint clean.

The existing test asserted DEFAULT_INDEX_BUDGET_BYTES + 1, which passes just as happily on the wrong constant. That is why it never caught it. The replacement pins the literal and both sides of the boundary.

Also ported from origin

  • phantom-tool (opt-in via a new toolResolver option) — an index line that NAMES a tool or script which resolves to no file. The live instance: an index naming an "open-items projection" tool that never existed, because a compression pass kept the name and dropped the inline recipe. Unfollowable for eleven days, and every reader saw a capability the fleet didn't have. Extraction (extractToolReferences) is pure; existence is judged by a caller-supplied resolver, so the file stays fs-free. No resolver → the check doesn't run. Resolver returns null or throws → can't judge → no finding.
  • classifyMemoryIndexSizes + MEMORY_SIZE_STATES — the fleet-wide read, which has a failure the single-dir lint does not: an agent whose index you could not measure. MISSING is fail-closed and a finding (the dir exists, so sessions run there with no index at all); a dir that never existed is declared, not swept, and not a finding; sweptCount counts only measured agents, so a run that measured nothing returns "nothing-swept" rather than "clean". Findings dominate — a one-agent fleet whose index is missing has zero measured agents, and calling that "nothing-swept" would bury the loudest instance of the exact failure the classifier exists to catch.
  • LOADER_TRUNCATION_SENTENCE — one sentence, used by both budget messages, so every surface says the same thing about why the byte count matters: nothing errors, nothing warns, the tail is just gone.

Deliberately not ported

mungeProjectPath and stripWorktreeSuffix. Both encode Claude-Code-specific path shapes (~/.claude/projects name munging, .claude/worktrees suffixes), and both are about locating files — which this library does not do.

Checks

188 tests pass, 0 skipped. Five new: the pinned budget boundary, extractToolReferences dedup/URL/placeholder handling, the phantom-tool resolver contract across all five resolver outcomes, and the two classifyMemoryIndexSizes verdict cases.

Independent of #1 — branched from main.

🤖 Generated with Claude Code

u00dxk2 and others added 3 commits August 29, 2026 12:27
DEFAULT_INDEX_BUDGET_BYTES read `Math.floor(24.4 * 1024)` = 24,985 — the
loader's documented "24.4KB" re-typed as KiB where the number is decimal.
The one check that can catch a truncating memory index carried 585 bytes of
slack it was never granted, in the only direction that matters: it let a
truncated index pass. Pinned to the literal 24,400.

The existing budget test asserted against `DEFAULT_INDEX_BUDGET_BYTES + 1`,
so it passed just as happily on the wrong constant. The new one pins the
literal and the boundary: clean at 24,400, WARN at 24,401.

Catches the file up with its origin on two more legs:

- phantom-tool (opt-in): an index line NAMING a tool or script that resolves
  to no file. The live instance was an index naming an "open-items
  projection" tool that never existed — a compression pass kept the name and
  dropped the recipe, and the line stayed unfollowable for eleven days.
  Extraction is pure; existence is judged by a caller-supplied `toolResolver`
  so the file stays fs-free. Absent resolver, the check does not run; `null`
  or a throw means can't-judge, which yields no finding.

- classifyMemoryIndexSizes: the fleet read, whose failure the single-dir lint
  doesn't have — an agent whose index you could not measure. MISSING is
  fail-closed and a finding; a dir that never existed is declared, not swept;
  `sweptCount` counts only what was measured, so a run that measured nothing
  returns "nothing-swept" rather than "clean", and findings dominate.

Deliberately NOT ported: mungeProjectPath and stripWorktreeSuffix. Both
encode Claude-Code-specific path shapes (`~/.claude/projects` name munging,
`.claude/worktrees` suffixes) and both are about LOCATING files, which this
library does not do.

188 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CkTF1JryMkGt4Qx2A2vHEh
The bus listener writes tmp/.bus-events.jsonl in whatever repo it runs from,
and those events carry portfolio internals. This is a public repo; one stray
`git add -A` is all it would take.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CkTF1JryMkGt4Qx2A2vHEh
…gs, 6 as written, 1 adapted

Every one reproduced against 3cfff31 before it was touched; the tests carry
the numbers each returned.

1. The README quickstart measured the index with `statSync().size`, which on
   a CRLF working tree is +1 byte per line — the documented integration put a
   valid 24,400-byte index over the cap it had just been pinned to. Measure
   the text you read, LF-normalized. JSDoc said "on disk"; it now says what
   the number is.
2. `scripts\foo.py` extracted as `foo.py`, and `file:bar.py` as `bar.py`: the
   extractor invented tokens the index never contained, and a resolver handed
   the basename could "prove" absent a file that exists. Backslash is now a
   separator; a token preceded by `:` is skipped.
3. ADAPTED. `bytes: -1` classified OK with 24,401 bytes of headroom; 1.5 and
   NaN likewise. A measurement is now a non-negative safe integer; anything
   else is MISSING, fail-closed. Zero stays a measurement — an empty index is
   under the cap — and the JSDoc says how that differs from the lint's
   `indexRead`, which asks whether there was anything to lint.
4. `dirExists: false` alongside a real `bytes` discarded the measurement and
   hid an over-budget index. The measurement wins; it is what was observed.
5. Two rows named `a` counted as two agents; an empty name became a clean
   "(unnamed agent)"; a null row manufactured a MISSING finding whose message
   asserted a directory exists. Rows with no identity, and duplicates, are
   now `malformedCount` — skipped, never measured, never a finding.
6. `TOOL_REF_RE` was quadratic on a long line with no match: 30,000 hyphens
   took 2.5 s, on an input the size of the budget this library polices.
   Segments are bounded and a linear pre-check skips lines with no candidate
   extension. 50,000 hyphens: 0 ms.
7. The README claimed the lint finds phantom tools while its quickstart passed
   no resolver, so the check never ran; and an async resolver returned a
   Promise, never `false`, so it silently never fired. The row now says
   opt-in and synchronous, the quickstart shows a resolver, and a thenable
   throws a TypeError — a caller error, not a data ambiguity.

194 tests pass.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CkTF1JryMkGt4Qx2A2vHEh
@u00dxk2

u00dxk2 commented Aug 29, 2026

Copy link
Copy Markdown
Owner Author

Codex adversarial round — 7 findings, 6 as written, 1 adapted

All seven reproduced against the first commit before anything was touched. Folded in as one commit; each test names the number the old code returned.

# Finding Verdict
1 README quickstart used statSync().size; on CRLF that's +1/line, so the documented integration over-reported at the boundary it had just pinned as written — measure the read text, LF-normalized
2 scripts\foo.pyfoo.py; file:bar.pybar.py. The extractor invented tokens the index didn't contain as written — \ is a separator; a token after : is skipped
3 bytes: -1 → OK with 24,401 bytes of headroom adapted — non-negative safe integers only, else MISSING (fail-closed). Zero stays a measurement: an empty index is under the cap, and the JSDoc now says how that differs from the lint's indexRead
4 dirExists:false + real bytes discarded the measurement as written — the measurement wins
5 Duplicate agents counted twice; empty name → clean row; null row → MISSING finding about a dir nothing is known about as written — malformedCount, skipped, never measured, never a finding
6 TOOL_REF_RE quadratic: 30k hyphens = 2.5 s as written — bounded segments + linear pre-check; 50k = 0 ms
7 README said the lint finds phantom tools; quickstart passed no resolver; an async resolver silently never fired as written — row says opt-in + synchronous, quickstart shows one, a thenable throws TypeError

194 tests pass. The spawn EPERM Codex hit running npm test was its sandbox refusing node:test's child processes; --test-isolation=none gave the same 188 it was reviewing against.

@u00dxk2
u00dxk2 merged commit 57b9e72 into main Aug 29, 2026
4 checks passed
@u00dxk2
u00dxk2 deleted the memory-integrity-catchup branch August 29, 2026 22:07
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.

1 participant