Repository navigation
Borrowed designs: the session log is a mini-WAL — five textbook answers, and the closed channel in between #5169
MedicineKing
started this conversation in
Ideas
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Uh oh!
There was an error while loading. Please reload this page.
Borrowed designs: the session log is a mini-WAL — four corruption shapes, one write-path characteristic, and the closed channel in between
The observation
session.jsonl.zstdis already written like a transaction log: one checksummed zstd frame per append batch, a crash window between the final write and its durability point. What it is missing is the lifecycle protocol every real transaction log has — the part that defines "what is committed, and who wrote it". Today the read path answers those questions with symptom-level heuristics:Each heuristic is a symptom-level answer to a question that has a definition-level answer elsewhere:
seq < index+ end-seed foldThe fifth mapped item — fsync per batch → PostgreSQL group commit (
handle.sync()per append) — is a write-path characteristic, not a corruption shape; it composes with frame merging under #4949 and is returned to there.The closed channel (release note vs code — rev. after 0.1.2-alpha.2)
The obvious carrier for protocol metadata is a new JSON record type, e.g.
{"type":"log-commit","epoch":3,"endSeq":187}. Which is where I found a discrepancy — and, four days later, its reversal:@deepseek-ai/dsh-session0.1.1-rc.2, the official read path maintained in the Regression gate for the session layer — a corruption corpus that would have caught this entire bug family #4945 gate): the vocabulary contract note says unknown types are refused "unless the event carries the envelope'signorablemarker … such a log was likely written by a newer harness". Theignorableflag is still validated (vendoredlib/index.js— the seed check acceptsignorable:true).ignorablefield was removed. "每个会话事件类型都是读取必需项" (every session event type is a read-required item) —PersistenceCoordinatorcompares every event type against the generatedKNOWN_SESSION_EVENT_TYPESand rejects any unknown type withSessionFormatUnsupportedError, listing the index and pointing at "likely written by a newer writer". The note explicitly records that the skip-declaration was dropped because it had no production writer and added surface to every envelope.ignorableis back end-to-end:packages/core/session/src/types.ts(ignorable?: true),known-event-types.ts(docstring now points at the 08-30 note),index.ts:224(case 'ignorable'), and the SQLite path (oneignorablecolumn per event row, so the field survives both the JSONL and the SQLite encodings).So the "two truths" conflict was a five-day window (08-25 → 08-30), not a stable fork: rc.2 semantics and the main/a2 line are aligned again — the escape-hatch channel exists. What is still missing is not permission to add a record type; it is the semantics of commitment.
ignorable: truebuys a reader's tolerance for an unknown type; it does not define "last intact commit" or "who wrote this batch".On the 08-25 deletion rationale — the two objections were no production writer and added surface to every envelope — both are answered by the shape of this proposal: a
log-commitline is one record per batch, not per event (surface cost O(batches), not O(events)), and its production writer is the coordinator itself, i.e. this proposal supplies the writer the note said was missing. The flag is the carrier, not the protocol; the protocol below is the net addition.Two channels
log-commitline at end of each batchsession.jsonl.zstd.metamanifest, atomic renameignorable:trueevent type or a named non-event record category (both are listable entities on the guard's known-type check, like thesessionheader record)Either way the protocol rules are the same:
The protocol (shared by both channels)
Read-side rules (4 rules: 3 replace the heuristics, 1 conserves today's behavior)
partial-tail-after-crashshape now yields the identical outcome as a corollary of HW, not of a string test.maxEpoch; metadata with a lower epoch marks its batch as stale-writer residue and is skipped. This replaces both theseq < indexskip and the stale-closer case — including the case the seq-based heuristic is weakest at, where the stale seqs coincide with the resumer's new tail. Epoch decides by who wrote it, not what seq it carries.endSeq. This is the "recycled tail → ACCEPT" source: stale closers fenced, new tail resumes at seq 10 under the new epoch, contiguity follows without the end-seed fold.seq 10 expected 13.) Rule 4 is a conservation clause, not a new capability: it requires no new reader behavior, so the prototype below implements rules 1-3 (+fallback) and rule 4 is simply today's behavior stated explicitly and carried through.Compatibility rule (stability guarantee): if neither channel is present in a log, a protocol-aware reader MUST use today's heuristics. Old artifacts are never judged more strictly.
Expected rows (per shape, under the protocol)
The two-writer row is a deliberate softening of the #4942 expectation table, and it is explicitly on top of, not instead of the write-side lease fix: write-side prevents the interleave from occurring; this reader rule turns a leftover interleave into a no-op. The rc.2/alpha.2 ground truth for these rows is the measurement table below.
Measurement (prototype): the protocol reader vs the unpatched referee
I built a ~260-line zero-install prototype (same tooling style as the repair repo): a writer that emits the P1 batch layout, and a reader implementing the rules (plus the fallback). The four shapes from #4945 were generated as P1 variants (synthetic, content-free, deterministic) and run through both paths — the referee (
verify-frame-format.mjs, unpatched official rc.2 read path, frame-level) and the P1 reader:seq 0…9thenseq undefinedat thelog-commitlinefenced=5rows, A stays contiguous)seq>=10— the 3 stale closers)seq 10 expected 13(rule 4; same failure as today)Re-run against 0.1.2-alpha.2 (published npm build, 2026-08-30, via
Session.fromRestore+ officialdecodeStorageRecord— event-level):seq 4 expected 10(event-level stop; the rc.2 frame-level stop differs only by observation layer:seq undefinedat the record line)seq 10 expected 13The two runs disagree in the failed row's exit text but not in its semantics: the rc.2 column is the frame-level scanner contract (stops when a record line has no category), the alpha.2 column is the event-level
fromRestore(reports the first conflicting seq) — both are REJECT for the same shapes, and both address the same question ("would the official reader accept this artifact?"). Where the tables report different counts for the same sample (5 events contiguous vs 6 log events / liveSeq 5), the accounting basis is the observation layer again: scanner-contiguous events vsSession.fromRestoreloaded totals (header-derived state counted).The negative datum: the published read path has not moved between rc.2 and alpha.2 — two consecutive releases, same REJECT rows. The alpha.2 release changed the vocabulary escape hatch (
ignorableback), not the lifecycle semantics. The write-side lease gap (#4942 fourth point) and the reader rules here are still the live open items, and the #4945 gate remains unfilled for exactly these rows.Three properties the table demonstrates, not asserts:
epoch < maxEpoch; no sequence inspection was needed.later writer wins(epoch-switch truncation: on a switch, prior-epoch events atseq >=the new batch's first seq are dropped).One referee-level artifact worth stating: the unpatched path stops at the
log-commitline itself (seq undefined) — no released version has a category for it (the vendored reader evidently does not extend itsignorabletolerance to a record with no type category — the same dispatch-branch uncertainty as fn.3). That is the empirical form of the "closed channel" section (now: was closed for five days, reopened withignorable): P1 artifacts are unreadable by today's reader by construction, so P1 ships together with the reader change, and only P2 can land on an old codebase unfelt.The prototype script (
prototype-wal.mjs) and the four artifacts are in my local tooling — I will attach them to the gate with the PR, alongside the extended verify contract.Where this sits (and why it is not a duplicate)
Out of scope
ignorablefor external plugins, and the P1log-commitline ships on top of it (one per batch, not per event). The flag is the carrier; the protocol rules (HW / fencing / epoch-switch / same-epoch fail-closed) are the real addition.Cost
ignorable:trueevent type or a named non-event record category (~10 lines, both listable on the guard's known-type check) + write-side epoch counter.Footnotes
lib/index.js:1064-1072; seed validation still acceptsignorable:true(lib/index.js:1229)..agents/notes/implemented/simplification/2026-08-25-fail-closed-session-event-vocabulary.mdexists in the alpha.1 tree; absent in alpha.2, replaced by.agents/notes/implemented/architecture/2026-08-30-retain-ignorable-external-session-events.md(code:core/sessiontypes.ts/known-event-types.ts/index.ts:224,237). The revert commit (revert of PR 自定义调整会话压缩阈值 #3087, remove-ignorable-session-events) is merged into the alpha.2 release branch.@deepseek-ai/dsh-session0.1.2-alpha.2) viaSession.fromRestore+ the officialdecodeStorageRecord; the scanner-identity claim is from a line-level diff ofzstd.ts:48-105between the alpha.1 and alpha.2 tags.All reactions