The persisted row (sessionId, key, stateVersion, observedSeq, state)
becomes (sessionId, key, ver, seq, val) — the cache medium repeats these
three names for every unit of every session, so the long forms dominated
the JSON payload. ProjectionCheckpointRow and the checkpointRow zod spec
rename together; the domain spec bumps to v3 (cache semantics: the old
medium is discarded, not migrated). The unit-facing declaration keeps
stateVersion — only the persisted/checkpoint row shape changes.
eslint --fix removed the no-unnecessary-type-assertion hits the review
knives introduced; the two await-thenable errors were vi.runAllTicks()
awaits in the cache spec, replaced with advanceTimersByTimeAsync(0) (the
fake-timer-safe microtask drain). Repo-wide eslint, dual-aggregate tsc,
and the focused 100% coverage set all green.
Static: the cache package.json files array matches the workspace
constraint shape, the unused dsh-storage-json devDependency is dropped
(tests run on the memory backend), and docs/module-graph.md is
regenerated for the new package edge.
Coverage: two unreachable branches deleted rather than tested —
coldSnapshot's floor-0 tail reuse (a baseSeq-0 restore never throws and
an unrelated record still carries a usable watermark) and flushSoft's
non-mandatory clean-skip (throttle triggers only fire dirty). New tests
close the real gaps: write() on a never-dirty session and the non-JSON
unit-state rejection, plugin disposal clearing armed interval timers,
cachedSnapshot's all-version-mismatched and cwd-identity arms, the
zero-units empty-log cut, the coordinator seek-hook ladder (suffix /
not-found / plain failure / abort-reason relay), and the superseded-
retirement race proving forget()'s exact-entry guard.
Review finding (PR #791): rows carried only version/watermark/state, so a
recreated session id, or a persistence store replaced under a surviving
cache, could pass every watermark check and seed state folded from an
unrelated log; a checkpoint racing ahead of an eager log flush could
likewise expose values no stored log contains. Records now store the
header identity (createdAt, cwd) they were folded from — reads validate it
against the live header (listing) or the tail's stored header (cold read)
and discard unrelated records whole (domain version 2 discards v1 media by
the pre-release stance). A live checkpoint additionally flushes the
session's buffered events durably before the cache row lands: the cache
can trail the log, never lead it. cachedValues is reshaped into
cachedSnapshot(meta): the identity witness plus the {asOfSeq, values} cut
the list carrier serves.
Review finding (PR #791): with no projection definitions registered,
restoreFloor() is undefined and the fast path returned a successful empty
snapshot without touching persistence — a nonexistent session 'succeeded',
violating the documented not-found contract in that supported topology.
The no-unit branch now probes readFrom(id, 0): an absent log rejects with
the seam's not-found, a present one dates the empty cut at its stored end.
gen-cordis-catalog/api, config and persistence catalogs, and doc graphs
regenerated over the new sessionProjectionCache service and the registry's
checkpoint faces. Classifications: ProjectionCheckpoint joins the type-link
exemptions (owned by the projection package source), Partial joins the
foundation names, the cache service gets its capability-seam role row, and
the package takes the one-sentence Model Experience contract (host-side
read-model accelerator, no model surface).
The base's bilingual gate now covers this branch's new README: add the
Chinese counterpart of the session-projection-cache package README, the
cache row on the group README's Chinese side, and re-record the touched
pairs (group, cache, session-persistence — whose English side gained the
readFrom rows earlier on this branch). verify-translation-pairing: 541
pairs consistent.
The watermark cache is the registry's authoritative mutable state; a
checkpoint consumer holding the live reference could corrupt every
subsequent snapshot and frame through it. structuredClone at the read face
(total, by the unit plain-JSON contract) pins the boundary; a mutation test
proves the cache is unreachable through handed-out rows. restore and
viewCheckpoint only touch caller-owned rows — no other leak path.
SessionSummary grows an optional projections column (whole value per key,
same passthrough posture as the history-tail block): attached rows cut the
live registry watermark cache; cold rows view the persisted projection
cache's stored rows via the new registry viewCheckpoint face (version-
matching keys only, zero I/O) — the RFC's motivating scenario, every
session's title across a listing without loading one event log. The column
is fail-soft and absence-coded: no registry, no cache row, or a throwing
read serve the row without the column, never breaking the listing.
New package on the domain data form: one session_projcache record per
session (key → {stateVersion, observedSeq, state}), landing beside
workspace.json under the shipped json backend. Write policy: two mandatory
points (turn/end + session disposal) with count/interval throttling between
them (both Config fields required — flush cadence is a deployment choice);
every background write is fail-soft (log + stay stale, self-heal on the
next write or cold read). coldSnapshot(id) runs the read ladder — cached
rows + persistence readFrom from the registry's anchored restore floor +
registry restore + fail-soft write-back — detecting crash-repair-shrunk
logs via the one-below anchor and degrading to a single full re-read.
Mounted in apps/cli/cordis.yml (writeEveryEvents 200 / writeIntervalMs
5000).
The registry grows the state-level read ladder the persisted cache builds
on: checkpoint(session) snapshots every unit's {stateVersion, observedSeq,
state} row from the watermark cache; restoreFloor(checkpoint) anchors the
tail read one event below the lowest usable watermark (so a shrunk log is
provable); restore(checkpoint, events, baseSeq) refolds each unit from its
usable row (or from init over a full read), rejects rows a tail read cannot
fix (version mismatch / overreach with baseSeq > 0 => re-read from 0), and
returns both the snapshot and the refreshed rows for durable write-back.
ProjectionCheckpointRow/ProjectionCheckpoint are the persisted-row types
minus the record keys.
Lint: eslint --fix over the merge-crossed projection/command files (arrow
parens, trailing commas, unnecessary assertions), Extract<> replaces the
keyof-map & string intersections no-redundant-type-constituents rejects,
the fold-adapter's merge loop drops its non-null assertions for a
bounds-carrying cursor, one JSDoc line wrapped under max-len (api-catalog
regenerated). Snapshots: the four TUI goldens re-recorded for the merged
event-count shift (the durable command lifecycle adds one event to the
seeded diagnostics log). The headless advanced-toolchain snapshot passes
on CI and fails locally in this sandbox both with and without these
changes (30s child timeout — environment-bound, tracked in the ledger).
New zh pairs for the session-projection group and package READMEs (both
gained their missing language-switcher lines); the packages/README rows,
apiproxy README, and tool-todo README zh sides catch up with their edited
English; the group README's stale ProjectionProvider name becomes
ProjectionDefinition. RFC precision from review: asOfSeq is the last
event's seq (session.seq - 1, -1 empty; subscribed.lastSeq vocabulary) and
a new risk names the accepted dev-only staleness window when registry
churn changes the key set mid-session. Pairing re-recorded; 540 pairs
consistent.