Type-only change (brands are zero-cost casts; no runtime/wire impact). Closes the two gaps in the "brand ids that cross package boundaries" policy and fixes the dependency direction so a capability package never pulls in an unrelated one. - Extract the `Branded<B>` primitive into a new standalone type-only package `@deepseek-ai/dsh-brand` (packages/util/brand) with no harness-package deps. dsh-llm keeps its owned CallId but imports Branded from dsh-brand; dsh-session, dsh-agent, and dsh-bash all import Branded from there. dsh-bash depends on dsh-brand ALONE — never on dsh-llm or dsh-session (the architectural fix: a generic execution backend must not couple to the LLM or session vocabulary). - Mint BashTaskId + OwnerToken in dsh-bash and thread them through BashTask.id, the get/ownerOf/list/readOutput/kill seam, the bash-local generation site, and the dsh-tool-bash validate/access surface. OwnerToken is a DISTINCT brand from SessionId so the seam stays decoupled; dsh-tool-bash is the single boundary that casts SessionId -> OwnerToken. - Brand at the SOURCE, not via mid-pipeline casts: agent-loop's Config types agents[].id as AgentId and resumeSessionId as SessionId, so the brand enters at the config boundary and the inner create()/resume casts disappear (only the genuinely-new per-run session-id string is cast). - Stop brand erosion: propagate CallId/SessionId/AgentId to the registry/store Map keys and public params/exports (SessionStore, AgentRegistry + factory options, the ACP session-id surface + ToolPresenter CallId map, the persistence coordinator, invariants pendingCalls, the pi-ai tool-call maps). - Docs: document BashTaskId/OwnerToken in bash.md (type-equiv re-pasted), point the Branded type-equiv at dsh-brand, fix stale param types in the session/ agent/bash READMEs, regenerate the cordis catalog + module graph. Implements docs/rfc/proposed/architecture/2026-06-20-branded-ids.md
@deepseek-ai/dsh-session-persistence-sqlite
A SQLite durable session-persistence backend — a second SessionPersistence implementation (session persistence), built to validate that the abstract seam and the shared runPersistenceContract suite are genuinely backend-agnostic. It satisfies the SAME contract as dsh-session-persistence-jsonl (append-only, contiguous-seq, lazy materialization, interrupted-turn close on load), expressed over node:sqlite rows instead of file bytes.
TODO: this backend talks to
node:sqlitedirectly. If a cordis database service (cordis/db/ a@cordisjsSQL driver plugin) is adopted, route through that instead of holding a rawDatabaseSynchere — the contract surface (SessionPersistence) would not change, only the storage driver.
Storage model
Each SessionEvent maps 1:1 onto a row in an events table (session_id, seq, type, time, data) — data is the event payload as JSON text, so the row shape is the event verbatim (including assistant/chunk, keeping seq contiguous). Out-of-log metadata (SessionHeader) lives in a sessions row. A sessions row is written only by the first append — its existence is the lazy-materialization signal (list reports exactly the sessions that have a row), so no separate column is needed.
The repo targets Node ≥ 24 (the root engines field), which includes the stable node:sqlite module. The database opens with foreign_keys = ON (so ON DELETE CASCADE drops a session's events with its row) and journal_mode = WAL. The table-layout version is stored in PRAGMA user_version and checked on open: a fresh database is stamped with the current SCHEMA_VERSION; a database written by any other, incompatible build (a non-current user_version, older or newer) is rejected rather than opened against an unknown layout — there is no migration (unreleased software).
Contract semantics over rows
- Append = a transaction.
appendrunsBEGIN/COMMITaround the batch: it materializes thesessionsrow (if still lazy) and INSERTs every event, asserting the contiguous-seq contract first (the first event'sseqmust equal the stored next-seq). A mid-batch failure (a UNIQUE violation on a duplicated seq) rolls back entirely, so the stored log and the in-memory cursor stay consistent. (load()already balanced the stored log, soappendnever has to repair a crash tail.) - Lazy materialization.
create()records intent in memory only — no row is written until the firstappend. A created-but-never-appended session has nosessionsrow, so it is absent fromlist()(which reports exactly the sessions that have a row). - Interrupted-turn close on load.
load()reads every stored event ordered byseqand finds the longest seq-contiguous, parseable prefix — INCLUDING the real events of an interrupted final turn after the lastturn/end(the loop only flushes atturn/end, so a process killed mid-turn leaves real, fully-written rows past it). A single turn can be huge in a long-horizon task, so those events are preserved, never truncated:load()CLOSES the orphaned turn by durably appending the minimal synthetic boundary events (an errortool/resultfor every assistant tool call left unanswered, astep/endif a step was open, then aturn/endcarrying{ kind: 'interrupted' }), inside one transaction that also DELETEs any never-fully-written torn tail row.load()is therefore mutating — after it the stored rows are balanced and the cursor is truthful, so the nextappendcontinues cleanly. The boundary (lastturn/end, torn-tail detection) is computed from theseq/typecolumns so a malformeddatain a torn tail row is never parsed (discarded, not unloadable). A parse error orseqgap inside the committed region (at or before the last realturn/end) makes the session unloadable. A session whose only turn never closed keeps its metadata row and stays present inlist()— the same as the JSONL backend, whose file likewise survives a first append that never reachedturn/end.
Configuration (schemastery)
interface Config {
path: string // SQLite database file path, or ':memory:' for an in-process DB
}
Write path
Like the JSONL backend, the plugin also installs the session/event → buffer → session/flush drain: it snapshots each event when buffered (the live session.events object is mutable), persists a fork's seed once on session/created, keeps a per-session write cursor so a resumed session never re-appends stored events, and seeds existing live sessions on apply (HMR does not replay session/created). Dispose awaits every in-flight init + final drain and then closes the database, so no write lands after teardown.