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.
dsh-invariants
Dev-mode event-contract invariants and session-log freeze. A pure-listener plugin (everything is a plugin) that asserts the harness event contract at runtime and, optionally, freezes logged session-event data so any code that mutates history throws instead of corrupting silently.
Off in production. Enable it in tests and the demos, where a contract violation should fail loudly. It costs nothing when not registered, and doubles as executable documentation of the event taxonomy — the assertions are the contract.
Plugin
import Invariants from '@deepseek-ai/dsh-invariants'
await ctx.plugin(Invariants) // freeze on (default)
await ctx.plugin(Invariants, { freeze: false }) // assert contract, don't freeze
inject: none required — it listens on session/created, session/event, and agent/status, all emitted by services it does not depend on directly.
Config
| Key | Default | Meaning |
|---|---|---|
freeze |
true |
Deep-freeze each logged event's data so mutating a logged event throws. Set false to assert the contract without freezing. |
Invariants asserted
Session log (per session):
seqstrictly increases — the spine of replay equivalence.- turns pair and nest —
turn/startopens a turn,turn/endcloses the matching one; no overlapping turns. - steps nest in turns —
step/startopens a step in the open turn;step/endcloses the matching step. - chunks belong to an open step —
step/startprecedes itsassistant/chunks. - a
tool/resultneeds a priortool/call— but NOT the converse: atool/callmay have no result (a throwntools/executewaterfall ends the step with notool/result, which is legal).
Agent status (per agent):
- legal transitions only —
idle↔runningand(idle|running)→disposed. A no-op transition (setStatusdedups, so it never fires) and leaving the terminaldisposedstate are violations.
On any violation it throws InvariantError (code: 'INVARIANT').
Why runtime, not deep-readonly types
A DeepReadonly<SessionEvent> is high type-noise across every log consumer, and a plugin can cast straight through it. A dev-mode freeze plus these assertions catch real corruption at zero production cost and zero type noise. The always-on half of that defense — cloning derived messages so request/adapter mutation can't reach back into the log — lives in dsh-session's deriveMessages. This package is the dev-mode tripwire. See ADR 0012.
Seeded sessions
A seeded/forked session arrives with events already in its log (the Session constructor copies the seed without emitting session/event). On session/created the plugin replays the existing log through the checker and freezes those entries, so seeded history is held to the same contract.