Repository navigation
[PR] fix(storage): align the envelope tail leg to the row tail by suffix, so folded-envelope verification can converge (Closes #6349) - #6350
Conversation
The sampled read-back verifier compared the two legs of a session's tail
positionally from index 0, but they intentionally hold different SLICES of the
same log:
- the rows hold the raw tail interned at save time (`tailSeqs`, oldest-first,
the whole resident window), and
- the envelope holds `boundedFoldedSnapshot`, which prunes from the FRONT and
keeps the NEWEST messages under `persist.tailTokens` (ranxianglei#2960 (5), caveat 2a).
Measured on live sessions the envelope is an exact element-wise SUFFIX of the row
tail (tail=1095/envelope=10 and tail=752/envelope=22, zero id and zero content
mismatches inside the overlap, stable across a six-sample sweep). Walking both
legs from index 0 therefore compared the oldest row against the oldest RETAINED
message — off by `tail.length - messages.length` — so `want` was undefined for
every slot outside the envelope window and `changed("lastMessages")` fired for
every tailSeq on every sampled save.
The sampled digest could then never converge: the streak latched (ranxianglei#5677), the
delta fast path was disabled for that session by design, and every save paid a
FULL ingest. Measured on one affected session: 44 full ingests, avg 4522 ms, max
9363 ms, 170.8 s of stalled event loop in a single episode.
Fix: align the envelope to its suffix position and compare only the overlap. A
genuine divergence inside the window still reports; a leg that is not a suffix
produces mismatches rather than silence. Row PRESENCE is still enforced for every
seq (ranxianglei#4390) — a recorded tail pointer with no row now also raises `changed()`,
which it previously did not, and the loop advances its index on that branch so the
suffix offset cannot drift when a gap is present.
Absence of a whole leg stays with the header digest, which already distinguishes
`undefined` from `[]` via `withoutBulk`; it is not re-litigated here.
Tests: tests/folded-tail-suffix-alignment.test.ts pins both halves — a row tail
longer than the envelope with a matching overlap verifies clean, and a content
change inside the overlap still latches drift.
80bdaf8 to
3a196c7
Compare
[bot] 🏷 Review of PR #6350 (Closes #6349) — mechanism concernI've traced the code path carefully and have a concern about the mechanism claim. I'll lay out what I found, then ask for clarification. The verifier's
|
[bot] 🏷 Follow-up: CI green, tests pass locallyI've verified:
I'm still waiting for clarification on the mechanism. The core question remains: what is
In both cases, the verifier should report clean. So I cannot construct a scenario where the verifier reports drift in the current master. Could you provide a log or trace that shows the verifier reporting drift, with the full context:
This will help me understand the actual failure mode. |
[bot] 🏷 Review verdict — PR #6350 (external, @dimas623)PR: What I did:
Key finding — the PR's mechanism claim is incorrect: The PR claims the verifier's
I traced all code paths (raw save, folded save, delta, full, restart, The code changes are reasonable (suffix alignment + missing-row drift detection) and the tests are valid, but they fix a mechanism I cannot confirm exists in the current master. The performance improvement reported in the PR body (24×8-9s → single cold-start) is real but not explained by the stated mechanism. Waiting on: PR author's response to my clarification request (posted above): what is Verdict: Hold — mechanism unconfirmed. The code is safe to merge (no regression risk for clean cases: when lengths are equal, offset=0 and behavior is unchanged), but the bug it claims to fix is not reproduced in my code trace. |
What
BatchedViewVerification.compareMessagescompared the two storage legs positionally from index 0. They do not hold the same slice of the conversation:tail_json.tailSeqs— the raw resident tail, oldest-first, the wholelastMessageswindow;boundedFoldedSnapshot(session)— a bounded suffix of that tail, becausebuildRecordalways writesmessagesFolded: trueandboundedFoldedSnapshotdrops the oldest messages untilpersist.tailTokens(default 16384) is satisfied.One leg is a truncated copy of the other with the same tail end, so the correct mapping is
expectedIndex = index - (seqs.length - expected.length). Comparing from 0 makes every slot mismatch as soon as the tail outgrows the envelope budget.That mismatch is deterministic, so the sampled verification fails on every sample,
driftedSessionslatches,#5677forces the full ingest path on every save, and the envelope gets frozen atDRIFT_ESCALATE_STREAK. On a ~2 200-message session the full path is a synchronous 8–9 s block; one measured episode blocked the event loop for 170.8 s across 24 consecutive ingests. Nothing is corrupted —integrity.okstaystrueand the rows, refs and block ledger are intact. It is a digest-shape defect in the verifier.Measured on two live sessions (SQLite read-only + envelope JSON, 6 samples at 5 s; offset stable, overlap element-wise identical in every sample):
tailSeqsrowsmessagesForcing the old positional comparison over one such sample yields
25/25mismatching slots plus 1127 "tail-only" slots on A,38/38plus 794 on B — biased toward permanent failure by construction.The fix
contentrow inside the overlap and asserts the drift is reported, so this is not a blanket mute.expected.length > tailLen: the envelope cannot hold more messages than the tail it claims.tailSeqspointer with no row now reports drift (tailGaps++andchanged()andindex++) instead of only bumping a counter. That means the table leg is shorter than the tail it claims (#4390) — a real defect, not an alignment artifact. Without this,tests/storage-batched-verification.test.ts("missing and retired tail rows remain visible as drift") regresses, which is how it was caught.Why not gate on
tailFoldedTried first:
tail_json.tailFoldedis written fromsession.lastMessagesFolded(src/storage/ingest.ts), which onlyfoldResidentTailsets, while the envelope marksmessagesFoldedfrombuildRecordwhenever a snapshot exists at all. On both live sessionstailFoldedisfalsein every row while the envelope is genuinely folded — the guard would never fire on real data. The suffix alignment does not depend on that flag.Verification
tests/folded-tail-suffix-alignment.test.ts(2 cases): a folded envelope that is a suffix of the row tail verifies clean; a real content divergence inside the overlap still reports drift.storage-batched-verification,issue5677-drift-field-diag,issue4323-drift-escalation,issue5408-frozen-recovery,issue5412-async-drift-latch-parity,issue4986-digest-drift-ref-order,storage-bounded-load,storage-canonical-digest,issue4392-canonical-key-order,folded-tail-suffix-alignment.tsc --noEmit --project tsconfig.jsonclean;node scripts/check-test-import-graph.mjsclean;node scripts/check-unused-exports.mjsclean.offset = 0fails the new test withtail slot 0: expect raw-32/user len=70 sha=45d5582d, got raw-0/user len=68 sha=853761edwhile the divergence case still passes.DIGEST DRIFT = 0,ESCALATED = 0,envelope writes FROZEN = 0,driftedSessions = 0;slow-ingest path=fullwent from 24 × 8–9 s to a single cold-start ingest, everything elsepath=deltaat 530–932 ms. Cache hit stayed 97–99 %.origin/masterc26c31ac;git rev-list --left-right --count origin/master...HEAD=0 1, and no upstream commit in between touchedsrc/storage/shadow.tsorsrc/storage/ingest.ts.No
versionbump — perAGENTS.md§4 that only happens on*_release-v*branches.Closes #6349