SherlockLiu Logo SherlockLiu
Back to all posts
Engineering

The Session Log: DeepSeek Harness's Enforced Invariant (Part 6)

SL
Aug 14, 2026 7 min read
The Session Log: DeepSeek Harness's Enforced Invariant (Part 6)

Series: Inside the DeepSeek Harness — Part 6 of 16


Every post so far has mentioned, in passing, that dsh’s session log is “the source of truth.” This post is where that phrase stops being a slogan and becomes a specific, readable piece of code — one that runs on every single model request and throws if it catches a divergence. If you only read one post in this series for the “wait, that’s clever” reaction, this is probably the one.


The log is a discriminated union, appended once, frozen forever

Session (packages/core/session/src/index.ts) is a plain class — not even a Cordis Service — created through ctx.sessions.create(). Its log is readonly SessionEvent[], and each event is deep-frozen the moment it’s accepted. SessionEventMap is merge-extensible through the same TypeScript declaration-merging trick from Part 1: compaction adds its own compaction/start/summary/end types, a hook-protocol package adds hook/invoked/hook/result, and the core log has no idea any of that exists ahead of time.

SESSION_FORMAT_VERSION = 0 (packages/core/session/src/types.ts:56) is enforced with zero tolerance: packages/core/session/src/index.ts:101-102 throws on load if the stored version doesn’t match exactly. There’s no migration path pre-1.0 — a mismatch is a hard refusal, not an upgrade attempt.

The most interesting small detail here is the ignorable envelope on every event. Absent means required. If a build encounters an event type it doesn’t recognize, and that event isn’t explicitly marked ignorable: true, it must refuse to reconstruct the session rather than silently skip the event. That sounds strict until you think about why: an unrecognized required event might change how every event after it should be interpreted. Silently dropping it wouldn’t fail — it would resume the session on a subtly wrong basis, which is worse than failing loudly. A forgotten ignorable marker over-refuses, which is safe; the alternative under-refuses, which isn’t.


The invariant, in the actual code

Here’s the claim from every prior post in this series, made concrete. packages/core/agent-loop/src/invariant.ts registers a listener directly on the llm/stream waterfall — the same dispatch point Part 8 will cover in depth — and it runs before every other listener in the chain:

// packages/core/agent-loop/src/invariant.ts (abridged, real)
ctx.on('llm/stream', (options, next) => {
  if (!isAgentLoopRequest(options)) return next()
  if (!Object.isFrozen(options)) fail('a loop-built request must be frozen')
  if (options.sessionId === undefined) fail('a loop-built request must carry a session id')

  const session = ctx.sessions.get(options.sessionId)
  if (!session) fail(`... must carry a live session id ...`)
  if (!Object.isFrozen(options.messages)) fail('... must carry a frozen messages array')
  // ... (a step/start-presence check and a header-fold existence check omitted here) ...

  const expected = session.deriveMessages()
  if (JSON.stringify(options.messages) !== JSON.stringify(expected)) {
    fail(`llm request for session "${session.id}" diverges from the dispatch-time durable derivation (log-reconstruction desync)`)
  }
  // ...plus a field-by-field check on model, system, temperature, maxTokens, stop, tools
  return next()
}, { global: true, prepend: true })
agent-loop dispatches llm/stream waterfall invariant listener { global: true, prepend: true } runs FIRST — can't be silenced by a short-circuiting listener session.deriveMessages() pure projection of the log JSON.stringify equal? pass -> next() · fail -> throw

Figure: not a design principle in a doc — a listener that actually runs, on every request, and throws on mismatch.

A few things worth being precise about, because it’s easy to oversell this. It’s a JSON.stringify equality check, not a literal byte comparison — “byte-equal” is a good intuition but not the literal mechanism. And { prepend: true } is doing real work, not just convenience: it guarantees this listener runs before everything else in the waterfall chain, including a replay listener that might otherwise short-circuit the dispatch before the check ever gets a chance to run. Without prepend, a well-meaning but misordered listener could silently prevent the safety check from ever executing. The comment in source calls this out directly — this is a case where listener ordering itself is load-bearing for correctness, not just for behavior.


The surface: only three event types the model ever sees

The log has around 15 durable event types (turn/*, step/*, chunks, calls, results, and more). Exactly three of them are eligible to become part of what a model request actually contains: user/message, assistant/message, tool/result. Everything else is structural — it tells you when something happened, not what the model saw.

user/msg assistant/msg tool/result turn/start, step/start, assistant/chunk — structural, dimmed deriveMessages() walks the log once, projects each surface node once, caches until a "replace" op bumps the replaceGeneration

Figure: the surface is a filter over the log, not a separate store — three types, cached, projected once each.

There’s a genuinely subtle detail in how compaction (Part 7’s subject) interacts with this. A replace surface op shadows a range of the log — but after several compactions, a later replace’s shadowed range can end up with start greater than end. That’s not a bug; SurfaceOp ranges are positions on the surface, not numeric intervals, and the authoritative record of what’s actually shadowed is a separate shadowedSeqs list, not the range itself. A naive reimplementation that treated {start, end} as “the numbers you’d expect from a normal range” would get this wrong.

One more concrete rule worth knowing: an assistant/message with empty content — the kind you get from a max-tokens cutoff that produced nothing but still needs to log usage numbers — is logged, but skipped during derivation. It exists in the log to hold accounting data; it must never enter what the model actually sees as its own prior turn.


One invariant mechanism, applied everywhere

The model-visible⟺logged check isn’t a one-off special case bolted onto the agent loop. It’s one instance of a general pattern: dsh-invariants is a registry service, and every workspace package publishes its own ./invariant companion module. A regex allowlist/blocklist decides which are active; each enabled installer runs inside its own dedicated child Cordis fiber, so one bad invariant check fails independently without taking others down with it. A mechanical CI gate (pnpm run verify-package-invariants) rejects any package that has no companion at all, or whose installer is a generated stub that never checks anything real — a package with genuinely nothing to check still has to write an empty installer whose comment explicitly starts No runtime invariant: and explains why.

That last requirement is worth sitting with. The system doesn’t let a package silently opt out of having any runtime self-check — it forces every package to either have one, or explain in writing why it doesn’t need one. The agent-loop’s request-derivation check is simply the most dramatic example of a discipline the whole repository is held to.


Compare: Claude Code

Claude Code’s memory system is a dedicated subsystem: a closed four-type taxonomy (user/feedback/project/reference), a MEMORY.md index capped at 200 lines and 25KB, and a background fork agent that extracts new memories after each turn, sharing the main conversation’s prompt cache. It’s purpose-built, and the payoff shows — three trust levels, deliberate capacity limits, a whole apparatus for deciding what’s worth remembering across sessions.

dsh has no memory subsystem at all, in that sense. It has this post’s invariant instead: the append-only session log is the memory, and “what the model can see” is derived from it by construction rather than curated into it by a separate extraction pass. That’s a narrower claim than Claude Code’s — dsh doesn’t decide what’s worth remembering, it just guarantees nothing the model saw is ever missing from the record. The two aren’t solving the same problem. Claude Code is answering “what should persist across sessions”; dsh is answering “can the model’s context ever silently diverge from what’s logged” — and its answer is a runtime check that throws the moment it does.


Coming up in this series

(All 16 parts are live today — no daily drip.)

Part Title What it covers
1 DeepSeek Harness: Inside the Open-Source Claude Code Rival The launch, the comparison, no privileged core, the four Cordis primitives
2 Composing an App From YAML, Not Code Profiles, bundles, five patch layers, boot, live HMR, --dump-config
3 Scope: Why a Live Agent Is the Key of Its Own Registration Shadowing, restriction, lineage vs. scope
4 Tool Execution in DeepSeek Harness: Guards and Approval Pre-execute, monotonic guards, post-execute, approval
16 DeepSeek Harness: Agent Presets as Data, Not Code Agent presets, PTC/Code Mode, the plugin ecosystem, compared to Claude Code’s agent types
5 Capability Seams: Making Bash Swappable for Sandboxed Bash The 3-role pattern end-to-end, across ~85 real seams
6 The Session Log: DeepSeek Harness’s Enforced Invariant (this post) The real invariant-checking code, surface projection
7 Persistence and Compaction: Crash-Safe by Construction JSONL/SQLite, torn-tail repair, the compaction lock bracket
8 Waterfalls: The One Event Pattern That Runs Everything Five dispatch modes, durable vs. live events, retry
9 The Agent Loop: Turns, Steps, and a Real Cancellation Bug The phase state machine, the inbox, a dated bug fix
10 The LLM Layer: One Message Format, Every Surface Message vocabulary, streaming, retry-via-log-replay
11 Subagents and Workflows: Composing Agents From Agents Provider kinds, continuable children, Ralph rounds
12 Defense in Depth: Sandboxing and Four Real Incidents bwrap/Landlock/Seatbelt/ACL, real production postmortems
13 Three Surfaces, One Spine: Web, Typert RPC, and SDK/ACP Typert codegen, the four-quadrant RPC envelope
14 Engineering Rigor: DeepSeek Harness’s Verification Gate The 100% coverage gate, real engineering-culture quotes
15 What a Second Production Harness Teaches dsh vs. the Agent Harness series, side by side

Part 16 shipped after this series’ initial 15 posts, once Agent Presets and Code Mode landed — it reads best right after Part 4, which is why its row sits there instead of at the end.

Next: Part 7 asks what happens when the process holding this log crashes mid-write — and how compaction shrinks a huge conversation without ever rewriting a single byte of the log itself.


References

Comments