The cache now owns its storage root (dshHomePath('projections')), so the
mount moves from the web-app overlay to base with that root declared, next
to the other base session layers. web-app inherits it; its overlay mount is
removed.
Store each session's projection_cache.json under the cache's own root tree
(<root>/<session-id>/projection_cache.json, wired to dshHomePath('projections')
in the base bundle) instead of beside the session log via
sessionPersistence.locate(). The cache owns its directory layout, keys
directories by the code-generated session id, and never consults the
persistence layer; the service now injects only sessionProjections and
sessions.
Drop the coldSnapshot method and its readFrom-tail fold ladder: every cold
consumer refolds from the log itself, so the cache only serves the listing
read (cachedSnapshot, one async file read per session) and the write side.
Fail-soft durability, per-path write serialization, in-flight drain, and
atomic 0600 writes are unchanged; the chain cleanup now observes its own
rejection so a failed write cannot surface as an unhandled error.
dsh-session-persistence leaves peer/dev dependencies and the tsconfig
reference; dsh-atomic-write moves to peerDependencies. Config gains a
required root.
- Write through @deepseek-ai/dsh-atomic-write with { mode: 0o600,
dirMode: 0o700 } instead of exporting a second atomic-write primitive
from dsh-storage-json; the session tree stays owner-only like the jsonl
backend's own directories.
- Serialize atomic replacements per cache path so an older cut can never
overwrite a newer one; track in-flight writes and drain them on
disposal so a late flush cannot land after teardown.
- Detect the absent per-session directory before the checkpoint cut and
durability flush: sqlite-style backends no-op the write entirely.
- Cold-read write-back path and identity both come from the stored log
header (tail.meta), so a stale caller header cannot mint an orphan
cache file.
- Add no-path coverage (write no-op, cachedSnapshot undefined, cold
fallback to the full-log rung) and a concurrent-write ordering test;
the package now holds 100% statement/branch/function/line coverage.
- Sync README.md/zh (inject list, coldSnapshot signature, per-session
file read wording), package description, the Agent Note alternatives,
and the superseded proposed/implemented notes (EN/ZH); add the
concurrent-checkpoint Known Limitation.
Replace the single global session_projcache domain with a per-session
cache file inside the session's own persistence directory, resolved
through sessionPersistence.locate(meta) — the persistence backend owns
the session-directory layout, the cache service keeps every checkpoint
and cold-read responsibility.
- cachedSnapshot(meta) becomes async (one file read per session);
coldSnapshot takes the session header so it can locate the file, with
the stored log header remaining the identity witness.
- Backends without a per-session directory (sqlite) disable the durable
cache: writes no-op and cold reads fall to the full-log rung. An
obsolete global cache is never read — derived data refolds on first
cold read (no migration).
- writeAtomic is exported from dsh-storage-json as the shared atomic
whole-file replace primitive; api-proxy listing and subagent cold
reads await the now-async cachedSnapshot.
- READMEs and a new Agent Note document the per-session medium.
Drop the persist?: boolean opt-in: every unit's state — client-visible and
host-only alike — is now written to the projection cache. A unit can no
longer silently skip the durable cache, host-only units no longer need an
explicit flag to participate in cold restore, and the persist-sharing
conflict check disappears with the field.
- ProjectionDefinition/ErasedDefinition lose persist; register overloads
simplify; checkpoint/restoreFloor/restore fold every registered unit.
- Registry and cache tests drop the persist:true fixtures and the
persistence-policy sharing test.
- READMEs and the state-and-client-views note record the uniform rule;
cordis API catalog and subsystem signatures regenerated.
The upstream-type proof only ran covariantly, so it caught a profile
field wider than pi-ai's but not one narrower — the direction its own
JSDoc claimed to guard. It now asserts both directions; the reverse holds
today because every field is either derived from upstream or a boolean.
`ModelCompat` gained `BedrockCompat`, which the gate had already started
serving, and the per-field protocol lists now match what the gates
resolve: `docs/config-catalog.md` pastes that JSDoc verbatim, so a stale
list there contradicted the README in the same change. The interface
header names the Responses grouping, since a catalog reader never sees
the README passage that explains it.
Valueless keys are judged after the name, so a withheld or misspelled key
written bare is refused for being that name rather than sent back for a
value it would be refused with anyway; the remedy no longer promises an
installed catalog value that a hand-declared route does not have. The
check covers `undefined` beside `null`: schemastery keeps the key either
way, and a cordis.yml entry reaches that state through `!!js undefined`,
so it is a config boundary rather than a typed one.
Coverage follows the composed path: the rejection is asserted through a
written settings section, and a switch is carried from that section onto
the wire a provider receives.
Refs #2646
The message-feedback note editor previously expanded inline inside the
assistant message's shared IconActions row, overflowing the row at every
viewport and pushing the branch action and the clock out of the column.
Rework it into a portable popover: the note editor is a fixed-position panel
portaled to document.body and anchored to the note trigger, so the row keeps
its single 28px line whether the editor is open or not and the panel escapes
the column's overflow clip.
The like/dislike buttons and the note trigger stay in the row unchanged; the
trigger toggles the popover (textarea + Save/Cancel + note-save failure) which
auto-focuses the input and closes on Escape or an outside click, returning
focus to the trigger. Rating/list-load failures surface in the row, note-save
failures inside the popover. Reuse the Menu portal surface tokens and add
@types/react-dom so the createPortal usage typechecks.
Layout e2e sweeps six viewports and pins that opening the editor leaves the
row's overflow, line count, and out-of-column items unchanged, and that the
panel is outside the column, within the viewport, and anchored to its trigger.
Unit tests cover the popover's portal-to-body, toggle, Escape/outside-click
dismissal, and unmount early-returns.
The full switch list already exists and is generated from source, so the
guide needs a way in rather than a copy: `config-catalog.md` carries 107
plugin sections, and linking the whole file leaves a reader to find the
one that configures the page they are on. Both mentions now deep-link the
`dsh-llm-pi-ai` anchor.
Refs #2646
The Models page has no field for `compat`, and the symptom it addresses —
a gateway holding a working key at a reachable address while refusing
every request — reads as a credential or connectivity problem. Give it
the same treatment `input` already has: name the symptom, show the two
switches that account for most of it, and state the resolution order.
Refs #2646
Review found two live defects in the compat surface.
A valueless key (`supportsDeveloperRole:`) survives schemastery as null,
and resolution carried it forward as a configured value. It landed on
`Model.compat` as null, which replaced the installed catalog entry's
value and left pi-ai's `??` reaching for its baseURL detection — the
catalog layer skipped entirely, and the switch written but not applied.
The vocabulary check now refuses it where it is written, matching the
`reasoningEfforts` precedent in the same file.
The gates were keyed by protocol name, but pi-ai keys compat by type:
`openai-responses`, `azure-openai-responses`, and `openai-codex-responses`
share one `OpenAIResponsesCompat`, so two shipped catalog routes were
refused the fields their own models declare. Gates now group by compat
type, `bedrock-converse-stream` gains its own, and the protocol set is
derived from `Model.compat`'s conditional so a release that gives a
further protocol a compat type fails the gate list by name.
Field types are derived from upstream rather than restated, with a proof
pinning the profile assignable to the upstream types, so a widened value
union cannot silently narrow what configuration accepts.
The `undefined` filter stays removed: `exactOptionalPropertyTypes` keeps
a typed caller from writing one, and schemastery never materializes one,
so it was validation for a value the static interface already excludes.
Refs #2646