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.
2.1 KiB
RFC 008: Deep-readonly public surfaces
Status: implemented (revised) — the pervasive DeepReadonly<T> type flip was rejected in favor of an always-on deriveMessages clone plus dev-mode Object.freeze + invariants. See ADR 0012.
Problem
The session log is append-only by contract, but session.events returns readonly SessionEvent[] whose elements are mutable: a plugin can reach in and rewrite history (events[0].data.content.push(...)), silently breaking replay equivalence and the derived-history guarantee. The same applies to derived messages and prompt assemblies passed through waterfalls — mutation is sometimes the intended idiom (waterfall middleware mutates the request) and sometimes corruption (mutating a logged event), and the types don't distinguish.
Proposal
Make immutability part of the type where mutation is corruption:
SessionEventdata becomesDeepReadonlyon the way OUT of a session (events,session/eventlisteners);append()keeps taking plain mutable input. ADeepReadonly<T>utility type lands in dsh-llm next to the brand/never helpers.deriveMessages()returns deep-readonly messages; the loop clones before handing a mutable request to theagent/requestwaterfall (mutation there is sanctioned — the clone makes the boundary explicit and cheap, once per step).PromptAssemblystays mutable through its waterfall (sanctioned) but the registry's internal section list is cloned per assembly (already true).- Optionally, dev-mode
Object.freezeof event data behind the RFC 005 invariants flag, so sanctioned-mutation violations throw in tests rather than corrupting silently.
Plan
Introduce DeepReadonly, flip the session read paths, fix resulting compile errors in consumers (expected: a handful in tests), add the freeze-in-dev option alongside RFC 005's invariants plugin.
Risks
DeepReadonly types can produce noisy errors at waterfall boundaries where mutation IS the API — keep the mutable/readonly boundary exactly at "logged vs in-flight" and document it in the session README.