Context
Discovered during the operator-directed 2026-06-26 #13999 Memory Core recovery. After the partial-promotion repair restored canonical exportability, npm run ai:backup ran clean through all 7 export steps — the vector-loss blocker is fixed — then crashed in the post-export integrity verification:
Verifying bundle integrity (row-count parity)...
❌ Backup failed: Error: Cannot create a string longer than 0x1fffffe8 characters
at Buffer.toString (node:buffer:925:14)
code: 'ERR_STRING_TOO_LONG'This is a latent bug exposed by two things at once: (1) the export only now started completing (it previously died earlier at the Memory Core export step, so verification was never reached), and (2) the corpus grew — the just-written exports are 1.2 GB (mc/memory-backup-*.jsonl) and 1.0 GB (kb/knowledge-base-backup-*.jsonl).
The Problem
verifyBundleIntegrity counts each exported JSONL's rows (for source/bundle parity) by reading the whole file into a single string and splitting on newlines:
const content = await fs.readFile(path.join(dir, file), 'utf8');
bundleCount += content.split('\n').filter(line => line.trim()).length;V8's maximum string length is 0x1fffffe8 (~512 MB). The 1.2 GB Memory Core export and 1.0 GB Knowledge Base export exceed it, so the internal Buffer.toString() throws ERR_STRING_TOO_LONG. The bundle data is fully written and complete; only the self-verification crashes — which means the canonical backup can never certify as a usable recovery source for a normally-sized corpus. That directly undermines the parent ticket's "verify restorability" goal and the v13.1 "verified-restorable backup" release-gate clause.
The Architectural Reality
ai/scripts/maintenance/backup.mjs:299-302 — the verifyBundleIntegrity per-file row-count loop (the fs.readFile(..., 'utf8') + content.split('\n')).
- The same function gained its
empty status in #14048 (sibling slice); this is the second latent defect in the same parity counter.
node:readline streaming is already the established pattern in this exact directory: ai/scripts/maintenance/restore.mjs, ingestTenant.mjs, plus ai/services/knowledge-base/VectorService.mjs / DatabaseService.mjs. The fix lifts that precedent.
The Fix
Replace the whole-file read with a streaming line count — readline.createInterface({input: fs.createReadStream(file), crlfDelay: Infinity}), incrementing for each non-empty trimmed line. This is the exact semantic equivalent of the current split('\n').filter(line => line.trim()), with bounded memory and no string-size ceiling. All four verdict statuses (pass / empty / fail / skipped) are preserved unchanged.
Contract Ledger Matrix
| Target Surface |
Source of Authority |
Proposed Behavior |
Fallback |
Docs |
Evidence |
verifyBundleIntegrity() row counter (backup.mjs:299-302) |
#14048 parity contract |
Count bundle rows via streamed reader; identical non-empty-line semantics; no whole-file .toString() |
n/a — pure mechanism swap |
function JSDoc |
Unit: large stream counted without throwing; small-file parity verdicts identical |
verifyBundleIntegrity() return shape |
unchanged |
{subsystem, status, sourceCount, bundleCount, reason} — no change |
n/a |
@returns JSDoc (already documents 4 statuses) |
No consumer change (additive-free) |
Decision Record impact
aligned-with #14030 (backup-substrate trustworthiness) and ADR-0025/ADR-0026 (the backup feeds the immune system's recovery-source-of-last-resort). No ADR amended — pure mechanism fix.
Acceptance Criteria
Out of Scope
- Reducing export size / not serializing full embeddings into the JSONL (
#14079 — the reason the files are 1+ GB).
- The empty-parity semantics (
#14048, shipped) and the alert-on-failure / restorability-import / retention-SLA slices (#14030 AC1-3, shipped).
Avoided Traps
- "Just raise the string limit."
0x1fffffe8 is a hard V8 cap, not configurable — streaming is the only correct fix.
- Naive
\n-byte counting. A raw byte scan would mishandle the existing empty-line filter and the trailing-newline case; readline preserves the exact non-empty-line semantics the parity check relies on.
Related
#14030 (parent — backup reliability / verify restorability).
#14048 (sibling — the empty-parity fix in the same verifyBundleIntegrity function).
#13999 (the Memory Core recovery whose now-completing export exposed this).
#14079 (Memory Core store bloat — why the exported files exceed 512 MB).
Handoff Retrieval Hints
query_raw_memories("backup verifyBundleIntegrity ERR_STRING_TOO_LONG 512MB streaming line count")
- Exact anchors:
verifyBundleIntegrity (ai/scripts/maintenance/backup.mjs:299-302), node:readline precedent in restore.mjs.
Authored by Vega (Claude Opus 4.8) — surfaced by @tobiu during the 2026-06-26 live #13999 recovery; premise reproduced via npm run ai:backup on dev.
Context
Discovered during the operator-directed 2026-06-26
#13999Memory Core recovery. After the partial-promotion repair restored canonical exportability,npm run ai:backupran clean through all 7 export steps — the vector-loss blocker is fixed — then crashed in the post-export integrity verification:Verifying bundle integrity (row-count parity)... ❌ Backup failed: Error: Cannot create a string longer than 0x1fffffe8 characters at Buffer.toString (node:buffer:925:14) code: 'ERR_STRING_TOO_LONG'This is a latent bug exposed by two things at once: (1) the export only now started completing (it previously died earlier at the Memory Core export step, so verification was never reached), and (2) the corpus grew — the just-written exports are 1.2 GB (
mc/memory-backup-*.jsonl) and 1.0 GB (kb/knowledge-base-backup-*.jsonl).The Problem
verifyBundleIntegritycounts each exported JSONL's rows (for source/bundle parity) by reading the whole file into a single string and splitting on newlines:const content = await fs.readFile(path.join(dir, file), 'utf8'); // backup.mjs:300 bundleCount += content.split('\n').filter(line => line.trim()).length;V8's maximum string length is
0x1fffffe8(~512 MB). The 1.2 GB Memory Core export and 1.0 GB Knowledge Base export exceed it, so the internalBuffer.toString()throwsERR_STRING_TOO_LONG. The bundle data is fully written and complete; only the self-verification crashes — which means the canonical backup can never certify as a usable recovery source for a normally-sized corpus. That directly undermines the parent ticket's "verify restorability" goal and the v13.1 "verified-restorable backup" release-gate clause.The Architectural Reality
ai/scripts/maintenance/backup.mjs:299-302— theverifyBundleIntegrityper-file row-count loop (thefs.readFile(..., 'utf8')+content.split('\n')).emptystatus in#14048(sibling slice); this is the second latent defect in the same parity counter.node:readlinestreaming is already the established pattern in this exact directory:ai/scripts/maintenance/restore.mjs,ingestTenant.mjs, plusai/services/knowledge-base/VectorService.mjs/DatabaseService.mjs. The fix lifts that precedent.The Fix
Replace the whole-file read with a streaming line count —
readline.createInterface({input: fs.createReadStream(file), crlfDelay: Infinity}), incrementing for each non-empty trimmed line. This is the exact semantic equivalent of the currentsplit('\n').filter(line => line.trim()), with bounded memory and no string-size ceiling. All four verdict statuses (pass/empty/fail/skipped) are preserved unchanged.Contract Ledger Matrix
verifyBundleIntegrity()row counter (backup.mjs:299-302)#14048parity contract.toString()verifyBundleIntegrity()return shape{subsystem, status, sourceCount, bundleCount, reason}— no change@returnsJSDoc (already documents 4 statuses)Decision Record impact
aligned-with#14030(backup-substrate trustworthiness) and ADR-0025/ADR-0026 (the backup feeds the immune system's recovery-source-of-last-resort). No ADR amended — pure mechanism fix.Acceptance Criteria
verifyBundleIntegritycounts bundle JSONL rows via a streaming reader (no whole-file.toString()), so files >512 MB verify withoutERR_STRING_TOO_LONG.pass/empty/fail/skippedverdicts are identical to before for small files.npm run ai:backupcompletes the verification step on the current ~1.2 GB MC / ~1.0 GB KB exports (manual evidence in the PR).Out of Scope
#14079— the reason the files are 1+ GB).#14048, shipped) and the alert-on-failure / restorability-import / retention-SLA slices (#14030AC1-3, shipped).Avoided Traps
0x1fffffe8is a hard V8 cap, not configurable — streaming is the only correct fix.\n-byte counting. A raw byte scan would mishandle the existing empty-line filter and the trailing-newline case;readlinepreserves the exact non-empty-line semantics the parity check relies on.Related
#14030(parent — backup reliability / verify restorability).#14048(sibling — the empty-parity fix in the sameverifyBundleIntegrityfunction).#13999(the Memory Core recovery whose now-completing export exposed this).#14079(Memory Core store bloat — why the exported files exceed 512 MB).Handoff Retrieval Hints
query_raw_memories("backup verifyBundleIntegrity ERR_STRING_TOO_LONG 512MB streaming line count")verifyBundleIntegrity(ai/scripts/maintenance/backup.mjs:299-302),node:readlineprecedent inrestore.mjs.Authored by Vega (Claude Opus 4.8) — surfaced by @tobiu during the 2026-06-26 live
#13999recovery; premise reproduced vianpm run ai:backupondev.