Internal Sessions and the Event Model
How append-only SessionEvents become the source of truth for model context
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
| Layer | What it stores | How it changes | Semantic owner |
|---|---|---|---|
SessionHeader | Format version, session ID, createdAt, cwd, parent, seedLength, subagent origin/depth, and agent preset | Snapshotted at creation, outside the event log | Lifecycle identity and the composition required for recovery SESSION-FORMAT-HEADER |
SessionEvent[] | Conversation, request, tool, policy, compaction, orchestration, and diagnostic facts | Append only; seq grows contiguously from 0 | The logical source of truth for Agent interaction |
SessionSurface | The current ordered sequence of model-visible event seqs | Append or positional replace | Input 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
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
seqis the only ordering authority within a Session. Live append takeslog.length; a seed must be contiguous from 0 with no gaps.timerecordsDate.now()wall-clock time and does not promise strict monotonic order. Same-millisecond events or a system-clock rollback do not change seq order.datamust be lossless JSON, not merely something thatJSON.stringifycan approximately process.- Only
user/message,assistant/message, andtool/resultmay carry Surface metadata. - An unknown required event cannot be silently ignored. Only
ignorable: trueexplicitly states that losing it cannot change reconstruction.
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
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 group | Events, with none omitted | Count |
|---|---|---|
| Core Session | turn/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-seed | 13 |
| Control and policy | agent-preset/selected, agent/inbox/spliced, approval/asked, approval/decided, approval/policy, permission/preset, sandbox/mode, plan/mode, goal/change, schedule/change | 10 |
| Compaction and retry | compaction/start, compaction/prune, compaction/summary, compaction/end, llm/retry, llm/retry-started | 6 |
| Auxiliary models and metadata | session/title, session/title-llm-request, web/deepseek-search-llm-request, feedback/record | 4 |
| Commands and Hooks | command/run, command/done, hook/invoked, hook/result | 4 |
| Subagents, Workflow, and Code Mode | subagent/descriptor, tool-workflow/run-start, tool-workflow/run-end, tool-workflow/agent-start, tool-workflow/agent-end, tool/code-dispatch, tool/code-dispatch-start | 7 |
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
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
| Accepted | Rejected | Reason |
|---|---|---|
| null, boolean, string, finite number | -0, NaN, Infinity | A JSON round trip must be byte-semantically equivalent |
| Dense plain Array | Sparse Array, extra own property, Array subclass | JSON would insert null or lose properties or prototypes |
| Plain or null-prototype object | Date, Map, Set, class instance, forged prototype | No toJSON call and no hidden conversion |
| Own enumerable string keys | Symbol key, non-enumerable key, function, undefined, BigInt, cycle | These values are dropped, reshaped, or impossible to encode |
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
Own the input
Snapshot data and Surface metadata; later caller mutation is irrelevant.
Construct a candidate
Allocate seq = log.length, read wall time, and deep-freeze the complete event.
Plan the Surface
Validate only against the candidate; do not mutate the committed node list yet.
Precommit dispatch
internal/dispatch lets invariants and diagnostics validate without mutation; every veto happens before the log changes.
Commit
log.push(event) is the logical commit point and invalidates the cached events array.
Postcommit publish
Notify a snapshot of session/event listeners; record and isolate each synchronous throw and asynchronous rejection.
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
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 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
| Operation | Change to the current node list | Change to the complete log | Main use |
|---|---|---|---|
append | Add the current event seq at the tail | Append the same event | Normal user, assistant, or tool result |
{op:'replace', start, end} | Atomically replace an inclusive range in current Surface order with the new event seq | Append only the replacement event; never delete old events | Compaction 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
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
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
| Producer | Typical source | Meaning |
|---|---|---|
| Assistant assembler | Every assistant/chunk seq from that attempt | Which raw stream facts produced the final Message |
| Tool result | The tool/call seq | Which recorded side-effect intent the result answers |
| Compaction replacement | All shadowed Surface nodes, optionally plus summary-input facts | The source set of the model-visible rewrite |
| Ordinary user or plugin message | Usually absent | It is a new fact itself and declares no derivation source |
8. deriveMessages() is a pure projection of the Surface
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
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
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
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
| View | What it contains | What it must not represent |
|---|---|---|
| Complete event log | All 44 fact types, raw chunks, shadowed messages, and control markers | It is not a Provider message array |
| Current model Surface | Current node order after replacement, then message projection | It is not the permanent transcript of what the user once saw |
| Append-origin transcript | Only 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
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
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
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
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
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 root | Optional dsh-session/invariant |
|---|---|
| Lossless JSON, snapshot, and freeze | Turns are contiguous from 1 and do not overlap |
| Envelope shape and contiguous seed seq | Steps are contiguous from 1 within the current Turn and close properly |
| Surface eligibility, range, and source coverage | Assistant, chunk, tool, and other execution events occur within the current Step |
| A tool-result replacement may change only content | Tool calls and results pair within the same Step |
| Limited compatibility validation of request-header shape | Each owner checks its own plugin-extended events |
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
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
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
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 choice | Main benefit | Main cost |
|---|---|---|
| Complete append-only log plus replaceable Surface | Preserves audit facts while permitting context governance | Every consumer must choose the correct view |
| Lossless JSON plus deep-freeze | Stable replay across backends and no caller-alias mutation | Cannot directly record rich runtime values such as Date, Map, or typed objects |
| seq as a shared coordinate | Log, Surface, source lineage, and client dedup share one identity | Cross-Session causality needs separate Header or event links |
| Module-augmented event vocabulary | Package owners can extend independently | Generated catalog, version gate, and owner invariants must remain synchronized |
| Precommit validation plus contained observers | Clear semantics for vetoes and notification failures | Observers can compensate but cannot roll back a committed event |
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.