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.
Architecture Decision Records
Short, immutable records of the why behind decisions that shape this codebase. Code and docs say what the system does; ADRs say why it does it that way and what we gave up.
Format: one file per decision, numbered, with Status / Context / Decision / Consequences. An ADR is never edited into a different decision — supersede it with a new one and cross-link.
When to write an ADR
Write one when a decision is all three of: durable (it shapes the codebase beyond a single function or package), contested (there was a real alternative you rejected, and a reasonable engineer might have chosen it), and surprising (a future reader would otherwise ask "why on earth is it done this way?"). The ADR captures the why and what we gave up — the parts code and docs can't.
Do NOT write an ADR for: a mechanical or local choice (a variable name, a one-file refactor); anything already enforced and explained by a gate or a convention in AGENTS.md; or a still-provisional decision tagged TODO(...) in the code — record those as TODOs and promote to an ADR only once they settle. When in doubt, the test is the "why on earth" question: if the code alone would mislead a careful reader about intent, write the ADR.
| # | Title | Status |
|---|---|---|
| 0001 | Vendor Cordis as source, not npm dependencies | accepted |
| 0002 | Microkernel: extension via Cordis event taxonomy, one concrete loop | accepted |
| 0003 | Event-sourced sessions with derived message history | accepted |
| 0004 | Provider-neutral content-block vocabulary owned by dsh-llm | accepted |
| 0005 | Custom typed tool-schema DSL instead of schemastery | accepted |
| 0006 | Tool schemas are part of the system-prompt assembly | accepted |
| 0007 | Mechanical quality gates over prose guidelines | accepted |
| 0008 | tsdown for JS bundling instead of dumble | accepted |
| 0009 | Capability seams — interface / implementation / consumer split | accepted |
| 0010 | Two LLM adapters as a design-verification twin | accepted |
| 0011 | Runtime arg validation at the model boundary | accepted |
| 0012 | Dev-mode invariants over compile-time deep-readonly | accepted |
| 0013 | Property-based testing for protocol-shaped code | accepted |
| 0014 | Doc-sync enforcement (doc code blocks + event taxonomy) | accepted |
| 0015 | Structured error taxonomy (HarnessError base) | accepted |
| 0016 | pnpm as the package manager instead of Yarn 4 | accepted |
| 0017 | Every session event is enclosed in a turn | accepted |
| 0018 | Session persistence as an abstract service over SessionEvent |
accepted |