Repository navigation
[pi/0.49.12] NUL-corrupted served-ledger JSONL files permanently block a session with a false 'storage is busy' refusal (Windows/Bun) #723
Description
[pi/0.49.12] NUL-corrupted served-ledger JSONL files permanently block a session with a false "storage is busy" refusal (Windows/Bun)
Short description
Corrupt trailing NUL bytes in pi-served-array-digests/*.jsonl / pi-served-tag-numbers/*.jsonl make getPiServedTagNumbers() throw a raw JSON Parse error: Unrecognized token '\u0000' on every turn in one session; the pi fallback wraps it into PiStorageBusyError, so the host displays a misleading, non-transient "storage is busy" refusal that persists until the files are manually repaired.
Environment
- Plugin:
@cortexkit/pi-magic-context0.49.12 (dist, Bun backend) - Harness: OMP (
omp/18.12.1,--harness pi), Codex/Responses provider - Platform: Windows x64 (MSYS2 shell for tooling; omp itself runs on Bun)
- Affected session: one long-lived Pi session with a large history; only 2 of 874 ledger files across all sessions were affected — the corruption is per-session, not global
- Storage: default data dir (
~/.local/share/cortexkit/magic-context/) — the SQLite store was ruled out (schema in sync:database=v95, supported_fence=v95; noSQLITE_BUSYin the log at failure times; "SQLite boot busy timeout" is a separate, unrelated boot line)
What happened
From a user's perspective:
Error: Magic Context could not safely prepare this turn; send your message again.
stage=recovery elapsed=394ms recovery=no managed result; refused (Magic Context storage is busy; send your message again)
Every retry from 2026-10-17 12:09 UTC onward (6+ refusals across Days 1–2) produced the same refusal. Resending never helped — the error is deterministic, not transient.
The plugin log shows the real error, DEGRADED PASS JSON Parse error: Unrecognized token '\u0000': LKG unavailable (lkg_miss); refusing unreduced 107-message input, immediately before each turn refused line.
Evidence chain
- Corrupt files. Two sidecar JSONL files for the affected session had trailing NUL (0x00) bytes:
pi-served-array-digests/<session>-<hash>.jsonl: 4,911,606 bytes on disk, 1,584 trailing NULs (first NUL at offset 4,910,022)pi-served-tag-numbers/<session>-<hash>.jsonl: 241,535 bytes on disk, 87 trailing NULs (first NUL at offset 241,448)
All NULs sit strictly at EOF; no mid-file NULs. Every NUL was a position with no record content behind it — the files had grown (or been written) without content, i.e. a hole-padded tail, not a truncated parse artifact.statmtimes froze at the corruption moment (10:58 Oct 17), hours before the first refusal (12:09), and never advanced again — consistent with the capture path failing on its own eventual read loop (the ledger identity is also re-read, so the corrupt write is only consumed later when the stamp cache invalidates).
- Reader-side blast radius.
getPiServedTagNumbers()(served-array-ledger.ts) reads the identity file, doestext.split("\n"), andJSON.parse(line)-es each non-empty line. The NUL tail becomes a final "line" that is non-empty, so it is parsed → rawSyntaxError: JSON Parse error: Unrecognized token '\u0000'(message exactly reproduced by running the same parse in Bun against the corrupt bytes). - Unhandled propagation. That
SyntaxErroris not aPiServedEnvelopeUnavailable, so the db-path wrapper can't catch it; it escapes to the context handler as a plain error → the transform pass is marked degraded. - Misleading fail-closed wrap. In the pi raw fallback (
registerPiGuardedContext), when the LKG snapshot isn't available (lkg_miss, nolkg_slotsrow for the session), the non-transient path wraps any error intoPiStorageBusyError("\u201cMagic Context storage is busy; send your message again\u201d"). So a deterministic corrupt-file parse error is surfaced as a "busy, resend" refusal — a message with no valid remediation for this cause. This is the same catch-all surface noted in [Pi] PiStorageBusyError on SQLite lock contention triggers fail-closed ctx.abort(), blocking interactive turns #601. - Related. [Pi Agent] Conflicting served tool tag numbers permanently block a session, reported as storage busy (0.47.0) #650 is the same visible symptom ("served tag numbers permanently block a session, reported as storage busy") with a different root cause — further evidence this error surface is fragile to any per-session state anomaly.
Repro condition
- Windows + Bun runtime (omp/pi harness), plugin writing these ledger/identity
.jsonlfiles viawriteStorageFileSync(path, data, { flag: "a" })(and timer-flushed appends viaappendPendingLines). - One session's files got a hole-padded tail (mechanism not directly captured — likely a Bun/Windows append-mode write race; both files corrupted in the same window). Every subsequent turn in that session fails at
getPiServedTagNumbers, every other session is unaffected.
Minimal repro once the file is corrupt: JSON.parse on the file's final \n-split segment (the NUL run) throws SyntaxError: JSON Parse error: Unrecognized token '\u0000' under Bun; the refusal then repeats on every turn.
Suggested fixes
- Fail open on a NUL/garbage tail (reader): in
getPiServedTagNumbers, trim trailing NUL bytes (or skip trailing lines that fail to parse and contain no{) before splitting. Losing a corrupt tail record is strictly better than permanently blocking the session; the DB-backed committed-served-numbers path remains the authoritative fallback. - Wrap, don't leak: convert file-read
SyntaxErrors intoPiServedEnvelopeUnavailable(orPiServedIdentityError) so existing recoverability handling applies instead of a raw parse error escaping the transform. - Stop using
PiStorageBusyErroras a catch-all: only throw it for genuinely transient causes (SQLITE_BUSY/SQLITE_LOCKED); for everything else, surface the real cause in the CONTEXT REFUSED text — "send your message again" is actively misleading for a persistent error and cost a full day of misdirected debugging here. - Writer hardening: write these appends through
writeStorageFileDurableAtomicSync-style temp-file + rename (or hold oneO_APPENDfd andfsync), instead ofwriteFileSync(..., { flag: "a" })per flush on Windows/Bun, to eliminate the hole-padded-tail mechanism.
Verification performed
- Byte-level audit of both files (NUL positions strictly at EOF).
- Reader-semantics simulation: all 874 ledger files across both directories validated line-by-line under the exact reader rules before repair; exactly the two affected files failed.
- After repair (strip trailing NULs only, originals kept): all 874 files parse under the reader's full record validation (
version===1, matchingsession_id, array of safe positive integers intag_numbers); the repaired digest ledger's last record validatesversion/SHA256/message_count/sequence.
Activity
magic-alfonso commentedon Oct 18, 2026
Thanks for this report. The byte-level audit, the reader rules and the full-validation check after repair made it easy to follow, and your diagnosis matches the code: a zero-filled tail in one of those sidecar files makes the reader throw a raw parse error, and the Pi fallback then reports it as "storage is busy", which no amount of resending can fix.
The fix is being worked on now and goes into the next patch release. It covers three things:
Your repair (stripping the trailing zero bytes while keeping the originals) is the right workaround until then.
pi-plugin: verify served-ledger appends and strip hole tails before w…
magic-alfonso commentedon Oct 20, 2026
This shipped in Magic Context 0.49.15. The 0.49.14 Pi package never became installable on npm, so 0.49.15 is the first Pi release that carries it. A damaged session record, including a zero-filled tail after a crash, no longer blocks the session: the unreadable tail is skipped without changing the file, and refusals now name their real cause instead of reporting that storage is busy. Thank you @Zireael for the report, the evidence and the fixtures. Please reopen if you still see it after updating.