A crash can leave a durable log whose final turn never closed. The old
behavior truncated everything after the last turn/end as a "crash tail".
But a single turn can be HUGE in a long-horizon task (many steps, large
tool output), so truncating it silently destroys real, durably-written
work — truncating a turn is wrong.
New crash recovery (ADR 0018): load() PRESERVES the interrupted turn's
events and CLOSES the orphaned turn by durably appending synthetic
boundary events — a step/end if a step was open, then a turn/end carrying
the new merge-extensible TurnEndReason {kind:'interrupted'}. load()
returns the balanced log, so a resumed session is immediately usable. Only
a never-fully-written TORN tail fragment is discarded; corruption in the
committed region is still unloadable.
- dsh-session: TurnEndReason {kind:'interrupted'} + shared
interruptedTurnClosers() repair helper.
- JSONL backend: scanLog preserves the longest contiguous prefix
(including a partial final turn); loadCore truncates a torn fragment and
durably writes the closers, returning the balanced log.
- runPersistenceContract gains a crash-recovery test (both backends + mock).
- Docs: ADR 0018/0017, architecture.md, package READMEs.
Also (review #33): RFC 013 records the "move event vocabulary to Zod"
question (merge-extensible maps → runtime schema registry) + blast radius;
deferred, not done here.
Add the durable session-persistence capability seam (ADR 0016): an
abstract SessionPersistence service (dsh-session-persistence,
ctx.sessionPersistence) defining create/append/load/list/has/delete/
update over the existing SessionEvent — no parallel persisted type — and
a first implementation (dsh-session-persistence-jsonl): an append-only
JSONL log per session with crash-safe atomic writes, truncation-repair
of a never-committed crash tail, and a read/replay path. SessionMeta
(format version, cwd, lineage) travels out-of-log via session.header.
A shared runPersistenceContract suite holds every backend to the same
append-only / contiguous-seq / lazy-materialization / serializability
semantics.
Config-driven create() now uses a per-run ${id}-session-<uuid> session
id so a fixed name no longer collides with an on-disk log once a durable
backend is loaded; each run is a new session (a demo simplification). The
examples drop their hand-rolled session-jsonl.ts and load the JSONL
backend via cordis.yml; CI smoke-loads it too.
The agent-facing create/resume factory that consumes load() is a
separate seam, deferred to a follow-up; this change stops at the load
primitive and does not reach into the loop.
Clarify that the CodeRuntime seam can host backends differing by
language/runtime, not just trust level — e.g. an AssemblyScript/WASM
backend (naturally sandboxed) and a Python backend over CPython or a
more controllable/embeddable interpreter. Note the execution contract
is language-agnostic while SDK codegen/prompt presentation is per-
language, and add these backends to the deferred follow-up list.
- Add an Alternatives section comparing Code Mode against the narrower
result-elision/summarization route over native tool-calling (solves
context-bloat but not composition/round-trips) and against parallel native
dispatch (a core-loop change that still lacks composition); states why
Code Mode is chosen and why the new code-execution surface is the price.
- Fix the Problem-section framing: it said the model "can run independent
calls concurrently," which contradicted the serialize-by-default decision.
Reworded to "express fan-out, initially serialized until concurrency-safety
metadata exists" — early win is composition + fewer round-trips, not parallelism.
- Concurrency: change from "may serialize" to mandatory serialize-by-default
via a per-run dispatch queue in the SDK bindings, with a non-overlap test as
a hard acceptance criterion (the binding shape otherwise makes Promise.all
dispatch concurrently before the tool contract has concurrency-safety metadata).
- node:vm guard: make it enforceable, not a README warning — CodeRuntime exposes
safe:boolean, the VM stub throws unless constructed { unsafe:true }, and
code-mode refuses to register run_code over an unsafe runtime unless separately
acknowledged (allowUnsafeRuntime); refusal path is tested.
- Prompt budget: drop the "zero prompt tokens" claim (the SDK .d.ts is injected
into the system prompt, so types do consume context) and add the explicit
budget/caching tradeoff — Code Mode's saving is on output/round-trips, not the
input-side tool description.
Proposes an optional Code Mode where the model writes a TypeScript program
against a generated SDK wrapping every registered tool, instead of emitting
one native tool-call per step. Implemented Cordis-style as a capability-seam
trio (code-runtime interface / code-runtime-vm node:vm reference stub /
code-mode consumer plugin) with zero core-package changes; the hardened
execution substrate is deferred to a follow-up RFC.
- 009: the crash-tail "overwrite" contradicted the append-only contract.
Name it explicitly as a one-time truncation-repair (ftruncate+fsync to
the last complete turn/end byte offset) that removes only the
never-committed crash tail; committed events are never rewritten.
Qualify the append/impl/ADR wording to match.
- 010: remove the remaining concrete-loop references — the session/new
and session/load table rows now point at the dsh-agent create/resume
factory, and the Risks disposal line uses the interface-level settle
signal (agent/status) instead of LoopAgent-only agent.done.
Resolve the inline review feedback on PR #18 (all verified against the
codebase, the published @agentclientprotocol/sdk@0.25.1 tarball, and
Cordis fiber semantics):
- 009: dsh-session owns SessionMeta (persistence re-exports) to avoid a
package cycle; split mutable summary into a sidecar so the event log
stays append-only and list/load can return it; pick one load-repair
rule (resume from the last complete turn/end, overwrite the orphan).
- 010: SDK has a zod peer dep + runtime zod/v4 import (drop "zero runtime
deps"); session/new needs a create seam taking {sessionId, meta};
propose an abstract create/resume factory on dsh-agent so the bridge
depends on the interface not the loop, and observe agent/status for
quiescence since agent.done is LoopAgent-only; add the explicit
TurnEndReason -> ACP StopReason wire mapping + test; reject non-empty
additionalDirectories for the MVP; remove the EOF blank line.
- 011: ctx.extend() does not create a disposable fiber — use a real
per-session disposer scope.
Three proposal documents, numbered in dependency order:
- RFC 009: an abstract, append-only, event-based SessionPersistence
service over the existing SessionEvent log (no parallel persisted
type), a JSONL impl, a SessionMeta header seam, and an async
AgentLoop.resume path. Design informed by Codex/Claude Code/
opencode/pi. Core design point; unblocks resume + ACP session/load.
- RFC 010: ACP (Agent Client Protocol) support as a dsh-acp
client-driver plugin on @agentclientprotocol/sdk, mapping ACP onto
the agent/* events and the tools/execute permission seam. Builds on
009 for session/load; single active session.
- RFC 011: multiplex concurrent ACP sessions over one connection
(bridge-layer change; downstream of 010).
Introduce HarnessError in dsh-llm (the leaf package): a stable machine-routable
code distinct from the message, cause chaining, name from the subclass, plus
isHarnessError. LlmError, ToolArgsError, and InvariantError now extend it.
Tool failures carry the structure end-to-end: ToolExecutionResult gains
error: { name, code } (populated from a thrown HarnessError), and the loop
forwards it onto the tool/result session event (which gained the same optional
field) for retry/sandbox plugins and replay. The loop's toError wraps non-Error
throws in a HarnessError(code: UNKNOWN, cause) instead of a bare Error.
Landed last and in isolation so it's a pure upgrade over the plain Error+code
the earlier PRs used — independently revertible. Graduates RFC 005 pt 2 ->
ADR 0015; RFC 005 now fully implemented.
Two tsx CI gates make doc/code drift fail fast:
- doc-typecheck extracts every fenced ts block from README/docs/package READMEs,
compiles them with tsc --noEmit against a temp project (vendor->lib, harness->src
paths from tsconfig.typecheck.json), and fails on errors. Deliberate sketches opt
out with ```ts ignore-check; the opt-out ratio is reported and capped.
- verify-event-taxonomy asserts the docs/architecture.md taxonomy table names
exactly the events declared in the interface Events blocks. This surfaced three
events the table had been missing (tools/change, llm/adapter-change,
system-prompt/change), now added.
Doc snippets made compilable with stub imports/declares (1 genuine sketch ignored).
Wired into CI after typecheck. API reports (RFC 006 pt 3) deferred. Graduates RFC
006 pts 1-2 -> ADR 0014.
Adds fast-check + one tests/properties.spec.ts per protocol-shaped package
(llm/BlockAssembler, session, tools/schema DSL, agent-loop scheduling). The
tools suite includes the RFC 001<->005 composition property (generated args
satisfying a spec pass validateArgs), closing the validator/InferArgs drift
risk from ADR 0011. Loop properties are deterministic (settle on agent/status,
no sleeps).
The BlockAssembler suite found a real bug on first run: a duplicate block-end
at the same index overwrote an already-flushed block, so the streamed prefix
disagreed with final blocks(). Fixed (first close wins, matching the existing
straggler rule) + regression test. Graduates RFC 001 -> ADR 0013.
- HMR state soundness: inject sessions, rebuild per-session trace by replaying
each existing session's log at (re-)apply, so a reload mid-turn no longer
falsely rejects the next event
- tighten nesting: turn/end rejects an open step; step/start rejects an open
step; chunk/message/tool events must name the open turn+step; pendingCalls
clears at step/end so a cross-step tool/result can't satisfy a stale call
- drop the default export (it stripped the inject metadata when loaded by
name; functional plugins expose named exports only — matches tool-bash)
- document deepFreeze's top-down precondition; sync RFC 005/008 bodies to the
as-implemented decision
New @deepseek-ai/dsh-invariants plugin (pure listeners, off in prod) asserts
the event taxonomy at runtime — seq monotonicity, turn/step nesting, a
tool/result needs a prior tool/call (NOT the converse), legal agent/status
transitions — and deep-freezes logged event data so mutating history throws.
Seeded sessions are checked + frozen on session/created.
The real RFC 008 fix is always-on: deriveMessages now structured-clones the
content it emits, so the loop's sanctioned request/adapter mutation can no
longer reach back and rewrite the append-only log. The pervasive
DeepReadonly<T> type flip is rejected (compile-only, high-noise, castable) —
recorded in ADR 0012, which folds in RFC 008. Wired into both demos.
defineTool now runs validateArgs against the SchemaSpec before execute, so a
malformed model call returns a self-correctable isError result listing the
violations instead of reaching the typed body untyped-in-practice. The
validator mirrors schemaSpecToJsonSchema semantics exactly (required from
required:true only, extra keys allowed, default not applied, object/array
without properties/items only type-checks, enum membership).
tool-bash's hand-rolled type/required checks (carrying the TODO(RFC 005)
stopgap note) are slimmed to just the value constraints the DSL can't express
(non-empty strings, positive timeout). Graduates RFC 005 pt 1 to ADR 0011.
Hard line breaks mid-paragraph make docs harder to edit and diff — a
one-word change reflows and re-diffs the whole paragraph. Reflow all
tracked non-vendor Markdown (plus vendor/AGENTS.md) so each prose
paragraph is a single line; soft-wrapping is the editor's job. Fenced
code, tables, and list structure are preserved (wrapped list items fold
to one line per bullet). Documents the convention in AGENTS.md.
Eight proposals grouped by category, each with problem statement,
concrete plan, and risks: property-based testing over the
protocol-shaped core (chunk streams, event logs, schema DSL);
mutation testing as the counterweight to the 100%-coverage gate;
deterministic tests + a universal replay-invariant fixture + nightly
race stress; architectural conformance (dependency-cruiser rules and
the LlmAdapter conformance kit); runtime arg validation at the model
boundary with a structured error taxonomy and dev-mode invariants;
doc-sync enforcement (typechecked doc snippets, API reports);
supply-chain checks and nightly vendor-drift verification against the
manifest; and deep-readonly public surfaces (logged-vs-in-flight
mutability boundary). AGENTS.md points at docs/adr and docs/rfc.