The runtime's new outward session contract exposes projections as a
ProjectionsFace (faceOf only); the ui-goal verb closure reads the current
value through faceOf('goal').getSnapshot() instead of the store-level get.
Catalogs regenerated after the merge.
The sparkle glyph gained a second consumer (GoalBar beside the Others
tool-row variant), which the duplication gate correctly flags as a clone —
the package-local-until-shared rule its comment recorded has run its
course. Both copies collapse into the shared ic_ds_* family; the icon-set
count test grows to 57.
First consumer of the runtime's single-slot mounting: the real apply mounts
on its own fiber, renderSlot('sidebar', ...) captures exactly the sidebar
slot's output, and update() re-renders the collapsed rail in place. The
.snap files carry semantic class names and svg fingerprints only.
The five ui-conversation machinery specs (apply-inject, chat-apply,
chat-toolview-slot, service-orchestration, selection-survival — now .tsx)
and the web app/app-shell specs assemble through the runtime instead of
hand-built Context + SlotsService + fake-session scaffolding per suite.
Session behavior mocks are typed against ISession, so incomplete fakes
fail at compile time.
A jsdom slot test runtime for feature specs: a real Cordis Context, the
production SlotsService and web-react renderer, and typed session/workspace
doubles (TestSessions implements ISessions with FixtureSession sessions;
TestWorkspaces implements IWorkspaces), so the compiler flags fixture drift
when a production face changes. Fixtures feed plain data: list rows,
conversation snapshots, and ISession-typed behavior stubs; provide-bundle
materialization runs the shared SessionProvideChannel.
DOM snapshot support: declare()/renderSlot() mount a single slot inside a
data-slot wrapper for local .snap capture, and a snapshot serializer folds
CSS-module class hashes to their semantic locals and collapses svg internals
to a content fingerprint. The typed provide() constrains declared-service
fakes to Partial of the service's outward face.
- projection.spec covers applyGoalProjection's non-message early return and
the foreign-change-kind arm directly (the pure-function posture the unit
requires — a throwing apply would tear down the registry drive).
- New ui-goal browser-plugin spec exercises the plugin body over a real
cordis Context with fake faces: dock registration (id/order), verbs
reading the CAS ref from the current projected value, the
no-current-goal short circuit, RPC-error mapping, HMR disposal, the
GoalDock adapter's three projection states, and the inert node half.
Feature packages now reach these domains through interface types only:
ISession/SessionFace (identity + prompt/cancel/loadOlder + the useSession
snapshot source), ISessions, IWorkspaces, ILayout, IConversation, and the
existing SlashServiceContract now actually mounted on Context.slash. The
concrete services implement their face; wire-pump and assembly entry
points stay on the classes. The provide-channel materialization and
current-projection logic moves into SessionProvideChannel so the
production service and the client test runtime share one implementation.
The workspaces service consumes sessions through the narrow SessionsPort.
- Regenerate the cordis/config catalogs, doc graphs, and module graph for
the four newly mounted plugins and the goal projection key.
- ui-goal README pair (Model Experience indirect + Known Limitations) with
its i18n record; sentence-allowlist entry for the indirect form.
- knip workspace entry for ui-goal (tsx test pattern).
- type-equiv manifest follows the seven host-coupled goal symbols to
domain.ts.
- The web-slash-command-dispatch note documents the dropped prompt
interception and leaves with it; the goal-bar note's code paths follow the
component into ui-goal (pairing re-recorded).
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): the column carried bare values (no seq), so the
client could not seed its value store without risking a stale list block
outranking newer push frames — and nothing consumed the column at all,
leaving cold titles absent after a restart. SessionSummary.projections is
now the same SessionProjectionsBlock as the history tail (values +
asOfSeq; attached rows cut the live registry, cold rows serve the cache's
identity-checked cachedSnapshot whose asOfSeq is the lowest served-row
watermark). SessionManager.refreshList seeds each row's block into the
per-session projection store via per-key apply — partial-baseline
semantics: an absent key never clears, and higher-seq-wins keeps stale
list blocks beneath push frames and tail baselines — so cold titles
surface in the sidebar without opening a session.
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.
The projection cache surfaced two substrate gaps, both properties of the
domain-KV stack it landed on. (1) The json backend's root is relative and
joined per-open against process.cwd(): sessions are patched global
(~/.dsh/sessions) but workspace.json/session_projcache.json land under
<launch dir>/.storages, splitting derived media from their source of
truth. Proposal: patch storage-json.root to ~/.dsh/storages beside the
session root (profile key storageRoot, mirroring persistenceRoot) and
resolve-once at backend construction, adopting the JSONL backend's
recorded rationale. (2) A damaged cache medium (truncated / version-bumped
/ schema-drifted) bricks fail-loud boot even though its content is fully
rebuildable from session logs. Proposal: DomainSpec grows
recovery: 'reject' | 'reset'; the facility, on exactly the damage-class
errors, destroys a declared-reset medium once and reopens empty —
workspace stays authoritative and loud. Bilingual pair recorded.
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.
SessionPersistence grows readFrom(id, fromSeq, signal?): the non-mutating
read-from-seq primitive for checkpoint consumers (the persisted projection
cache folds only the tail past its watermark). Coordinator owns validation,
per-id serialization, and the sequential fallback (loadStored + forward
skip); SQLite implements the optional seek-capable loadStoredFrom hook
(WHERE seq >= ?), JSONL stays sequential by contract. Contract suite covers
suffix exactness, empty-tail, non-mutation, and cancellation; seam README
(both languages) documents the method and the hook.
goal (GoalService + the 'goal' projection unit), goal-session (automatic
continuation), command-goal (/goal — lands in the materialized effective
catalog and the slash menu with zero client changes), and ui-goal (the
GoalBar dock surface). apps/cli deps updated accordingly.