DSHarness Systems Dissection Pinned baseline 47f943859b · 36 verified / 0 in progress / 36 chapters
中文
Sessions and Persistence·Chapter 09

Internal Sessions and the Event Model

How append-only SessionEvents become the source of truth for model context

VerifiedUpstream 47f943859bScope: Catalog envelopes, event types, sequence numbers, scopes, origin links, format versions, and model-visible invariants.

Conclusion: the complete log preserves facts; the Surface decides what the model sees now

A DeepSeek Harness Session is neither a message array nor a Provider SDK transcript. It is an immutable, contiguously numbered, extensible SessionEvent log, with a replaceable model-visible Surface folded over it. Compaction, tool-result pruning, and other context rewrites only shadow old nodes on the Surface; the original events remain in the complete log.

This design preserves three semantics at once: original facts for audit and replay, the current Surface for the next model request, and append-origin events for the human transcript that must not be rewritten by compaction. These three layers are the prerequisite for understanding persistence, compaction, recovery, and the UI.

1. SessionHeader and the SessionEvent Log have different jobs

LayerWhat it storesHow it changesSemantic owner
SessionHeaderFormat version, session ID, createdAt, cwd, parent, seedLength, subagent origin/depth, and agent presetSnapshotted at creation, outside the event logLifecycle identity and the composition required for recovery SESSION-FORMAT-HEADER
SessionEvent[]Conversation, request, tool, policy, compaction, orchestration, and diagnostic factsAppend only; seq grows contiguously from 0The logical source of truth for Agent interaction
SessionSurfaceThe current ordered sequence of model-visible event seqsAppend or positional replaceInput to the next deriveMessages()

parentSession and seedLength in the Header are durable fork lineage; delegationDepth prevents a resumed child Agent from being mistaken for a root; agentPreset binds the tool and prompt composition needed on recovery. None needs to masquerade as a conversation event.

2. The event envelope is small, but its constraints are strong

Fixed envelope

Every event belongs to a union discriminated by type: {type, seq, time, data}. Its only optional fields are surfaceOp, sourceEventSeqs, and ignorable: true. Seed and load reject every additional envelope key.SESSION-EVENT-ENVELOPE

  • seq is the only ordering authority within a Session. Live append takes log.length; a seed must be contiguous from 0 with no gaps.
  • time records Date.now() wall-clock time and does not promise strict monotonic order. Same-millisecond events or a system-clock rollback do not change seq order.
  • data must be lossless JSON, not merely something that JSON.stringify can approximately process.
  • Only user/message, assistant/message, and tool/result may carry Surface metadata.
  • An unknown required event cannot be silently ignored. Only ignorable: true explicitly states that losing it cannot change reconstruction.
Mechanism-level conclusion

Every cross-event relation is expressed with seq rather than timestamp, array identity, or database row ID. JSONL, SQLite, in-memory replay, and browser transport can therefore share one logical coordinate system.

3. The pinned baseline recognizes 44 event types

Generated catalog

The current build generates its known-event catalog from every SessionEventMap declaration in the repository and contains 44 types. The core package declares 13; package owners add the rest through TypeScript module augmentation.SESSION-CORE-EVENTSSESSION-KNOWN-EVENTS

Ownership groupEvents, with none omittedCount
Core Sessionturn/start, turn/end, step/start, step/end, user/message, assistant/chunk, assistant/message, tool/call, tool/result, todo/write, request/header, request/context, session/end-seed13
Control and policyagent-preset/selected, agent/inbox/spliced, approval/asked, approval/decided, approval/policy, permission/preset, sandbox/mode, plan/mode, goal/change, schedule/change10
Compaction and retrycompaction/start, compaction/prune, compaction/summary, compaction/end, llm/retry, llm/retry-started6
Auxiliary models and metadatasession/title, session/title-llm-request, web/deepseek-search-llm-request, feedback/record4
Commands and Hookscommand/run, command/done, hook/invoked, hook/result4
Subagents, Workflow, and Code Modesubagent/descriptor, tool-workflow/run-start, tool-workflow/run-end, tool-workflow/agent-start, tool-workflow/agent-end, tool/code-dispatch, tool/code-dispatch-start7

The catalog says which vocabulary this build understands, not which events the model sees. Most are log-only audit or control facts; only three Surface event types can derive a Message.

4. Lossless JSON is an admission boundary, not a storage repair

One-pass snapshot

snapshotJsonValue() walks the graph with an explicit task stack and validates while copying from the same property read. A stateful getter cannot swap values between a separate “check” and “copy” pass, and nesting depth is limited by memory rather than the JavaScript call stack.SESSION-LOSSLESS-JSON

AcceptedRejectedReason
null, boolean, string, finite number-0, NaN, InfinityA JSON round trip must be byte-semantically equivalent
Dense plain ArraySparse Array, extra own property, Array subclassJSON would insert null or lose properties or prototypes
Plain or null-prototype objectDate, Map, Set, class instance, forged prototypeNo toJSON call and no hidden conversion
Own enumerable string keysSymbol key, non-enumerable key, function, undefined, BigInt, cycleThese values are dropped, reshaped, or impossible to encode
Immutability tests

Events and nested Messages are deep-frozen when admitted; derived history shares those frozen objects. Tests require mutations of content and tool results to throw TypeError, and an invalid graph to be rejected without changing log length.SESSION-IMMUTABILITY-TEST

5. Session.append() is a two-phase local transaction

1

Own the input

Snapshot data and Surface metadata; later caller mutation is irrelevant.

2

Construct a candidate

Allocate seq = log.length, read wall time, and deep-freeze the complete event.

3

Plan the Surface

Validate only against the candidate; do not mutate the committed node list yet.

4

Precommit dispatch

internal/dispatch lets invariants and diagnostics validate without mutation; every veto happens before the log changes.

5

Commit

log.push(event) is the logical commit point and invalidates the cached events array.

6

Postcommit publish

Notify a snapshot of session/event listeners; record and isolate each synchronous throw and asynchronous rejection.

Production implementation

The same frozen candidate crosses precommit validation, commit, and observation. A reentrant append during observer notification is rejected so a nested commit cannot reorder later listeners.SESSION-APPEND-COMMIT

Atomicity tests

The tests require an internal-dispatch veto to leave the log, publication, and Surface unchanged. After commit, a hostile observer can neither roll back the event nor prevent later observers from seeing it.SESSION-APPEND-BOUNDARY-TEST

6. The Surface is a small replacement calculus over an append-only log

The only visible types

The runtime recognizes only user/message, assistant/message, and tool/result as Surface-eligible. They must declare a surfaceOp; a boundary, chunk, request, or plugin audit event carrying Surface metadata fails.SESSION-SURFACE-PROJECTIONSESSION-SURFACE-PROVENANCE

OperationChange to the current node listChange to the complete logMain use
appendAdd the current event seq at the tailAppend the same eventNormal user, assistant, or tool result
{op:'replace', start, end}Atomically replace an inclusive range in current Surface order with the new event seqAppend only the replacement event; never delete old eventsCompaction summaries and tool-result content rewrites

start and end identify current Surface nodes by event seq; they are not a contiguous slice of the raw log. Both endpoints must still exist on the current Surface in legal order, while shadowed nodes may be separated by log-only events.SESSION-SURFACE-REPLACE

Narrow privilege for tool results

A replacement tool/result may target exactly one current tool result and may change only the nested result block's content. Call ID, error flag, message identity, turn and step, and meta must remain structurally equivalent. Spill or pruning can shorten output without forging another tool execution.

7. sourceEventSeqs is explicit provenance, not complete lineage

Validation rules

When present, the source list must be dense, duplicate-free, composed of non-negative safe integers, and strictly earlier than the current event. A Surface replacement must include at least every shadowed node, or it cannot establish what the new node replaced.SESSION-SURFACE-PROVENANCE

ProducerTypical sourceMeaning
Assistant assemblerEvery assistant/chunk seq from that attemptWhich raw stream facts produced the final Message
Tool resultThe tool/call seqWhich recorded side-effect intent the result answers
Compaction replacementAll shadowed Surface nodes, optionally plus summary-input factsThe source set of the model-visible rewrite
Ordinary user or plugin messageUsually absentIt is a new fact itself and declares no derivation source

8. deriveMessages() is a pure projection of the Surface

Rules per node

user/message returns its user message unchanged; a nonempty assistant/message returns the model message; tool/result returns its user-role result. An assistant with empty content carries usage but does not enter the transcript. The other 41 event types all return null.SESSION-SURFACE-PROJECTION

Incremental cache

Without replacement, the projector folds only Surface nodes added since the prior call, making the work O(new nodes). When replace generation changes, it clears and rebuilds from the current node list. Each API call returns a fresh array snapshot while the inner Message objects remain shared frozen event data.SESSION-DERIVED-CACHE

Replay-oracle tests

After ordinary append, an empty assistant, and replacement, the tests construct a fresh Session from the complete events and require its derived result to deep-equal the live incremental cache. An earlier returned array must not grow when later events append. Dependencies are not installed, so this study reviewed but did not run the tests.SESSION-DERIVED-ORACLE-TEST

Assessment

Producers write Message framing into content; the Surface projector does not wrap content again by source. This reduces hidden prompt mutation, but every context producer must maintain correct model-facing framing itself.

9. One log supports three different read views

ViewWhat it containsWhat it must not represent
Complete event logAll 44 fact types, raw chunks, shadowed messages, and control markersIt is not a Provider message array
Current model SurfaceCurrent node order after replacement, then message projectionIt is not the permanent transcript of what the user once saw
Append-origin transcriptOnly message events that originally entered with surfaceOp:'append'It is not the context of the next post-compaction request

The Surface module explicitly warns that a human transcript built from the current Surface would make already-seen conversation disappear as soon as compaction lands; the replacement copy is a model-only view. Conversely, building the next request only from the append-origin transcript would ignore compaction entirely.

10. A seed separates the recovered prefix from this lifecycle

Admission path

Every seed event is detached or adopted, frozen, and checked for envelope shape, request header shape, contiguous seq, and a legal Surface transition. Failure cannot leave half a Surface. Construction records seed length as firstLiveSeq and appends session/end-seed when the tail does not already contain it.SESSION-SEED-ACCEPTANCE

Idempotence tests

A fresh Session has no marker; an explicitly empty seed receives one; reopening a seed that already ends with the marker does not append another. The seed prefix remains unchanged and derived Messages match the original Session.SESSION-SEED-REPLAY-TEST

The marker is neither a lock proving that no other writer exists nor a crash checkpoint. It only marks the boundary between constructor seed and events added in this lifecycle, letting forks, resumes, and test replay distinguish inherited facts from new work.

11. Version 0 uses fail-closed compatibility

Format rule

SESSION_FORMAT_VERSION is fixed at 0 in this baseline. Breaking changes to the Header, envelope, core semantics, or Surface mechanism should bump it; this baseline has no upgrade chain or migration and rejects a different version. An ordinary new event theoretically need not bump the format because the envelope has ignorable.SESSION-FORMAT-HEADER

Unknown-event gate

After persistence normalization, the loader checks the whole log against the generated known catalog. An unknown required event produces SessionFormatUnsupportedError; an unknown event marked ignorable:true remains in the loaded log so a consumer that understands it can still read it.SESSION-UNKNOWN-EVENT-GATESESSION-UNKNOWN-EVENT-TEST

Philosophy

Required-by-default may reject too much, but it prevents an older runtime from silently reconstructing the wrong conversation when it does not understand new semantics. Forward-compatibility risk is biased toward visible downtime instead of silent corruption.

12. Always-on storage guards are separate from optional relational invariants

Always-on Session rootOptional dsh-session/invariant
Lossless JSON, snapshot, and freezeTurns are contiguous from 1 and do not overlap
Envelope shape and contiguous seed seqSteps are contiguous from 1 within the current Turn and close properly
Surface eligibility, range, and source coverageAssistant, chunk, tool, and other execution events occur within the current Step
A tool-result replacement may change only contentTool calls and results pair within the same Step
Limited compatibility validation of request-header shapeEach owner checks its own plugin-extended events
Relational checking

The companion replays a trace from the existing log. On internal/dispatch, a new candidate creates only a staged transition; state advances only when the real session/event arrives. A later precommit-listener veto therefore cannot move invariant state ahead of the log.SESSION-RELATIONAL-INVARIANTSESSION-INVARIANT-COMMIT

13. Three integrity gaps and two deliberate tradeoffs

Gap 1: end-seed writer authority is not enforced

The type comment calls the constructor the only legitimate writer of session/end-seed and explicitly admits that a plugin append would incorrectly reclassify the preceding live bracket. Yet public append() accepts this type and the optional invariant deliberately leaves it unconstrained. Safety currently relies on in-process plugin discipline, not a capability or runtime guard.SESSION-CORE-EVENTSSESSION-APPEND-COMMIT

Gap 2: ignorable is a reader capability, not a usable writer API

The envelope defines ignorable?: true and the loader implements its forward-compatible gate, but Session.append(type, data, surfaceIntent?) accepts no envelope options and production source never appends ignorable:true. Ordinary new event producers in this baseline therefore cannot use the documented “ignorable without a version bump” mechanism through the public path.SESSION-EVENT-ENVELOPESESSION-APPEND-COMMIT

Gap 3: known payloads have no unified runtime schema

The always-on root deeply validates JSON, the three Message shapes, request headers, and the Surface, but it does not run one schema over all 44 payloads. Relational validation lives in an optional companion. This is a lightweight extension point for trusted code, not a complete defense if third-party plugins are treated as untrusted writers.

  • Deliberate tradeoff: ordinary appends may omit sourceEventSeqs, avoiding expensive lineage maintenance for every new fact at the cost of incomplete provenance.
  • Deliberate tradeoff: live commit is separate from disk durability, keeping the hot path synchronous and I/O-free at the cost of requiring callers to use the flush barrier before external side effects.

14. Strengths, costs, and verification status

Design choiceMain benefitMain cost
Complete append-only log plus replaceable SurfacePreserves audit facts while permitting context governanceEvery consumer must choose the correct view
Lossless JSON plus deep-freezeStable replay across backends and no caller-alias mutationCannot directly record rich runtime values such as Date, Map, or typed objects
seq as a shared coordinateLog, Surface, source lineage, and client dedup share one identityCross-Session causality needs separate Header or event links
Module-augmented event vocabularyPackage owners can extend independentlyGenerated catalog, version gate, and owner invariants must remain synchronized
Precommit validation plus contained observersClear semantics for vetoes and notification failuresObservers can compensate but cannot roll back a committed event
Verification status

This chapter rechecked the types, production append, Surface, and invariant control flow, the generated vocabulary, and test source. Upstream dependencies are not installed, so every test section describes the contract encoded by tests rather than a local execution result. The next chapter follows raw logs, forks, repair, history, and client replay on top of this event model.

My Learning Notes

Autosaved only in this browser. Nothing is uploaded or committed. Export Markdown whenever you want to keep a copy.