Model-visible ⟺ logged: An Invariant That Crashes in Your Face
Everything the model sees must be reconstructable from the log; before sending a request, it rebuilds and checks on the spot—fail the check, then crash.
Play first, then talk. On the left: an append-only event log. On the right: the message array rebuilt from the log, and the request about to go to the model. Hit Play to watch events stream in. When it finishes, play the villain: delete a log entry, or bypass the log and mutate the request, then hit “Send next request”—watch the check tick items one by one, plant a red X at the fork, then crash in your face.
The lab’s check order matches the source; the red banner keeps the original source error text: comparison logic in packages/core/agent-loop/src/invariant.ts lines 31–42; error-prefix assembly in packages/runtime-diagnostics/invariants/src/index.ts line 62. Verified on 2026-08-13.
What problem it solves.Most chat programs keep two copies of the conversation: an in-memory array and an on-disk archive, each written on its own. One day the process crashes; you restore from the archive, and the recovered history is missing a tool result the model actually saw. Everything the model says next is off, and you can’t prove why—neither copy can vouch for the other. When truth has two homes, they drift.
What the idea is.DSH collapses truth to one copy. The rule sits in the repo-root AGENTS.md line 107:
AGENTS.md line 107, verified on 2026-08-13
Unpack it. The session log is an append-only event stream; anything you want the model to see must first become an event written into the log. Message history is derived from the log—the official docs say it is “never stored separately.” So in DSH, the log is the conversation itself; there is no second conversation state.
Source: docs/subsystems/session.zh.md line 5, verified on 2026-08-13.
Then the bidirectional arrow ⟺ in the title. The log must imply the request, and the request must be explainable by the log. Writing the log alone isn’t enough—someone has to stand guard at the outbound gate. Before every request leaves, DSH re-derives the expected message array from the log and does a full-string compare against what’s actually on the request; System Prompt, model name, sampling params, and the tool list must also match the request-header snapshot in the log field by field. Only a full match gets through.
The guard’s core is four lines—short enough to tape on the wall. It proves one thing: before every dispatch, DSH really re-derives messages from the log, full-string-compares them to the actual request, and fails in place on mismatch:
const expected = session.deriveMessages()
if (JSON.stringify(options.messages) !== JSON.stringify(expected)) {
fail(`llm request for session "${String(session.id)}" diverges from the dispatch-time durable derivation (log-reconstruction desync)`)
}
deepseek-harness-master repo; verified against packages/core/agent-loop/src/invariant.ts, verified on 2026-08-13. Code blocks keep the original source text.One more detail. The derive function used for the check is the same public set used for restore and replay—checker and checked share one rebuild rule; nobody has a private path. Request-header rebuild is just a seven-line pure function: scan events, take the last snapshot.
Source: full check order in packages/core/agent-loop/src/invariant.ts lines 22–52; request-header rebuild in packages/core/session/src/request-header.ts lines 65–71. Verified on 2026-08-13.
Why it lasts.Single source of truth is a fifty-year-old database rule: one ledger only; everything else is a view of that ledger—views can be dropped and rebuilt at will. DSH simply brings that discipline into agent conversation management. Rewrite the source in Rust, triple the event types—still one ledger.
What problem it solves.Imagine the guard finds a mismatch and only logs a warning. A warning means the bad request already reached the model: some plugin bypassed the log and quietly mutated messages; from that moment, what the model sees and what the log records are two different stories. The log keeps writing—wrong books. Three days later someone replays that log to debug and can’t reproduce the weird production behavior. Silent drift is scarier than a crash; it quietly dumps the investigation cost on the future.
What the idea is.So DSH takes the hardest path: fail the compare, throw, void this request on the spot—it never leaves. Design notes explicitly rejected the soft option; the verdict on “compare consecutive requests, warn on divergence” was “rejected because violations must be inexpressible at the interface.”
Source: .agents/notes/implemented/architecture/2026-07-05-reconstructable-requests.zh.md, “Alternatives considered” section, verified on 2026-08-13.
Two small mechanisms ride along. The checker registers at the head of the event-listener queue so no other listener can short-circuit and silently skip the check—the guard sees the request first. Then the request object and message array must be deep-frozen, closing the back door of pass-then-mutate.
Source: head-of-queue registration in invariant.ts lines 20–21 and 54; freeze and session checks in lines 22–29. Verified on 2026-08-13.
Why it lasts.That’s fail-fast. A crash locks loss at zero: no poisoned turns in the log—fix the bug and rerun. Keeping running while sick compounds: the longer you go, the more bad data, until you can’t even find when it started. One crash at the boundary is cheaper than archaeology three days into weird symptoms. That judgment doesn’t care about language or framework.
The first two ideas each cost effort; the payoff lands here together. Once the log is the only truth, a whole capability set comes free around it:
- Restore: process crashed—re-derive from the log and keep chatting.
- Fork: branch from any event into a parallel session.
- Replay: re-derive the log and you get the original request—replay tests need no API key.
- Audit: the UI trail is what the model saw, backed by a runtime assertion.
Even Compaction (compressing a too-long history into a summary) sits under this invariant: the summary is written as an event too; post-compaction requests still face the outbound compare; a buggy Compaction implementation crashes on the spot—no escape.
Why it lasts.This pattern has a name—event sourcing—used for years in banking and accounting: don’t store the balance, store the ledger; the balance is always computed from the flow. DSH just swaps trade events for conversation events. As long as the event stream is the only truth, these capabilities stay free byproducts.
Session persistence is almost table stakes for coding agents; the difference is direction. Grok Build (xAI’s open-source Rust coding agent) persists one-way: in-memory conversation state is primary, disk is secondary. Each message copies the in-memory entry into the persist channel; send results are dropped; persist failure doesn’t interrupt the chat; Compaction can even replace the whole on-disk history in one shot. Reasonable product trade-off—but the persistence layer doesn’t own correctness: no path reverse-derives the log and compares it to the outbound request.
Source: grok-build-main repo crates/codegen/xai-grok-shell/src/session/chat_persistence.rs lines 30–38 (persist_message and replace_history), verified on 2026-08-13.
Claude Code is closed-source; what’s public is .jsonl session logs under ~/.claude plus restore. On published evidence, that’s after-the-fact record persistence; nothing public shows a runtime assertion that rebuilds the request from the log at dispatch time and compares—whether an equivalent exists internally is unknown.
So compare mechanism direction only: Grok Build and Claude Code treat persistence as a restore tool; DSH elevates the log to a first principle that must be proven at runtime.
Walk a trigger path
A plugin listens for outbound request events and wants to stuff a system prompt into the message array before send. Walk both cases with this lesson’s check order: what happens if it mutates the frozen array directly? If it clones the whole request, mutates the clone, then forwards—where does it get stopped? Hint: what do the freeze check and the byte-for-byte compare each guard?