Pi's Session is primarily organized around "how branchable history recovers to the current Context." DeepSeek Harness (DSH) chose a stronger persistence boundary: the Session itself is an append-only typed SessionEvent log, from which model messages, UI, Replay, and recovery state are all derived. [1]
The official documentation defines this log directly as the single source of truth for all agent interaction history. Model messages are not maintained separately as another authoritative state; instead, they are projected from the Session Log via deriveMessages(). [1]
This design first solves the consistency problem between multiple states.
When Message and Execution History are Persisted Separately
A common Agent storage model is:
messages table
runs table
tool_calls table
stream_events table
Each table is reasonable, but a single execution modifies multiple states simultaneously.
For example, when the model starts responding:
Create Run
Append streaming chunks
Generate Assistant Message
Write Tool Call
Write Tool Result
Update Run status
If one of these writes fails, you need to handle "which state has already been committed." Recovery logic must also decide which table is more trustworthy.
DSH unifies these facts into a single ordered event stream first:
turn/start
step/start
user/message
request/header
assistant/chunk
assistant/chunk
assistant/message
tool/call
tool/result
step/end
turn/end
Each event has a monotonically increasing seq. Normal logs require seq to be continuous, so events have a deterministic position within the Session. [1]
Thus the recovery problem becomes:
Read SessionEvent[]
↓
Replay by seq
↓
Reconstruct needed Projection
The source of core state is only one.
SessionEvent Records a Wider Range than Message
DSH's event vocabulary includes multiple types of facts: [1]
Lifecycle
turn/start
turn/end
step/start
step/end
Model-visible messages
user/message
assistant/message
tool/result
Execution trace
assistant/chunk
tool/call
Request state
request/header
request/context
Other persistent state
todo/write
session/end-seed
This set of events shows that DSH's definition of Session extends beyond Conversation Transcript.
For example, assistant/chunk doesn't directly form the next LLM History, but it preserves the original streaming output trace; turn/start and step/start express execution boundaries; request/header saves the model configuration, System Prompt, and Tool schemas used for a request.
Therefore, the same Event Log can serve multiple consumers:
SessionEvent Log
│
├── Model History
├── UI Replay
├── Transcript
├── Telemetry
├── Crash Recovery
└── Fork / Resume
This is also the main value of Event Sourcing in the Agent scenario: Tools, Streams, model messages, and execution boundaries are naturally time series.
Surface: The Middle Layer Between Event Log and Model History
If all SessionEvents were directly converted to model messages, obvious problems would emerge.
The model doesn't need to see:
turn/start
step/end
assistant/chunk
request/context
So DSH defines a Surface layer on top of the complete Event Log.
Currently, the core only allows three types of events into the model-visible Surface: [2]
type SurfaceEventType =
| 'user/message'
| 'assistant/message'
| 'tool/result'
The logic for deriveEventMessage() is also straightforward:
user/message → UserMessage
assistant/message → AssistantMessage
tool/result → ToolResultMessage
Other events → null
This forms:
Full Session Log
↓
Surface Fold
↓
Surface Nodes
↓
deriveEventMessage()
↓
Model Messages
This layer is important because it allows Session to preserve richer facts than Context while keeping the Model History structure stable.
surfaceOp Lets Compaction Remain Append-only
Normal messages enter Surface using append:
M1 → M2 → M3 → M4
Compaction creates a special problem. The model needs to see a Summary instead of a segment of old history, but the Event Log shouldn't delete old events.
DSH's Surface supports replacement operation: [1][2]
type SurfaceOp =
| 'append'
| { op: 'replace'; start: number; end: number }
Assume the current Surface is:
10 15 21 30 35
A new Summary Event can declare:
replace 10..30
The new Surface becomes:
42(summary) 35
But the Session Log still preserves the original 10, 15, 21, 30.
Therefore:
Log
Only appends facts
Surface
Allows changing the model's visible order and scope
This is similar to Pi Compaction's idea: both preserve original history, what changes is how the subsequent model Context is constructed. DSH further formalizes this principle into a general Surface Operation.
sourceEventSeqs Preserves Derivation Relationships
DSH's Surface Events can also record sourceEventSeqs. [1]
The most intuitive example is Streaming:
assistant/chunk seq=20
assistant/chunk seq=21
assistant/chunk seq=22
↓ assemble
assistant/message seq=23
sourceEventSeqs=[20,21,22]
This way, the complete Message and original Chunk both exist, but the source relationship between them isn't lost.
This field is also important in Surface Replace. When a Summary replaces several old Surface Nodes, it needs to declare which obscured nodes it references. Surface fold verifies these provenance relationships. [2]
This makes the Event Log more than just "storing JSON in time order"; it also preserves part of the derivation graph:
Raw Events
↓ provenance
Derived Surface Event
For Debug, Replay, and Audit, this is more reliable than only saving the final Message.
Why Even Request Header is Recorded
DSH's architecture document提出了一个很严格的要求:抵达模型请求的输入必须能够从日志重建。[3]
Saving Message alone isn't enough, because a real LLM Request also includes:
provider / model
reasoning config
sampling config
System Prompt
Tool schemas
This information is saved by request/header. [1]
The current EpochHeader roughly contains:
interface EpochHeader {
config: LlmCallConfig
adapterDefaults?: ...
system?: string
tools?: ToolSchema[]
}
Each Loop instance records the complete Header at start; when configuration changes, it continues appending new Snapshots. Recovery reads the latest Header to reconstruct the request environment. [1]
This solves a commonly overlooked problem:
Identical messages
+
Different System Prompt / Tools
=
Actual model semantics can be completely different
If a system promises "precise Replay", saving Message alone isn't sufficient.
DSH therefore treats "model-visible state being reconstructible" as a runtime invariant of Session. [3]
This completeness increases log size and schema complexity, but it makes Resume, Fork, and Replay semantics more explicit.
Persistence Only Responsible for Making the Same Log Durable
Event Sourcing can easily introduce another kind of complexity: one set of Events in memory, another set of Persistence Events defined in the database.
DSH currently deliberately avoids this. The Persistence seam directly persists existing SessionEvent, without a parallel event model. [4]
After runtime appends an Event, it synchronously emits a session/event notification; the Persistence Plugin copies the event to a per-session buffer, then batch writes to the backend. The Agent Loop producing Events doesn't wait for each disk write. [4]
The flow is approximately:
Session.append(event)
↓
In-memory log committed
↓
session/event
↓
Persistence buffer
↓ batch
JSONL / SQLite
When you need to determine the persistence boundary, flush the buffer via session/flush.
Here the two concepts are clearly separated:
Session
Defines facts and order
Persistence
Decides how these facts are made durable
Therefore JSONL and SQLite can be replaceable backends, while the upper-layer Event Model doesn't need to change. [4]
Crash Recovery Doesn't Delete Interrupted Turns
Agent process may crash in the middle of a Turn.
The disk may already have:
turn/start
step/start
user/message
assistant/message
tool/call
But not:
tool/result
step/end
turn/end
One recovery approach is to truncate the last unfinished Turn. DSH currently chooses to preserve already-durable facts, and adds a synthetic event during cold recovery:
turn/end {
reason: interrupted
}
To close the unbalanced Turn. [4]
This way the recovered log still clearly expresses:
This Turn did happen
Part of the work was completed
The process was interrupted before ending
This is more aligned with the append-only fact model than deleting the entire Turn.
This is especially important for long tasks. A Turn may contain many Steps and large amounts of Tool Results; facts already produced shouldn't all disappear because of a crash at the end.
Why Session Header is Outside the Event Log
DSH still doesn't make everything an Event.
Session's storage metadata is placed in a separate SessionHeader, for example: [4]
version
id
createdAt
cwd
parentSession
seedLength
origin
delegationDepth
agentPreset
These fields describe the Session itself and storage lineage, not belonging to the interaction timeline.
For example:
parentSession
Which Session this Session was forked from
seedLength
How many Events belong to the inherited prefix
agentPreset
Which Agent combination should be used to restore this Session
This reflects a boundary worth preserving: Event Log records "what happened in the conversation", Header保存"what this conversation record is".
Event Sourcing doesn't require all Metadata to be converted to events.
DSH's Fork is Closer to Session Lineage
Pi can form a tree within a single Session file.
DSH's Session Log itself remains linear:
seq 0
seq 1
seq 2
...
When forking, a new Session is created and the stable prefix becomes the Seed. Header uses parentSession and seedLength to record lineage. [4]
Can be represented as:
Session A
0 1 2 3 4 5
│
└──── fork at prefix
↓
Session B
0 1 2 3 | 4' 5' 6'
So Pi and DSH form two representative models for Branching:
Pi
One Session preserves an Entry Tree
DSH
Each Session maintains a linear Event Log
Branching forms Session Lineage
The former suits frequent historical navigation within a Coding Session; the latter keeps Event seq linear and Replay model simple, at the cost of Fork producing a new Session Identity.
It's hard to judge which is better without product behavior.
Costs of Event Log
DSH's design gains strong reconstruction ability, but also bears obvious costs.
First, large number of events. Streaming Chunks also enter the log; one response may generate many Events.
Second, Event Schema belongs to the persistent protocol. Once released, type changes need to consider old logs. Currently Persistence performs strict validation on format versions and unknown required events, avoiding silently ignoring events that affect reconstruction semantics. [4]
Third, all Projections must be deterministic. Surface, Message, UI, Transcript if using different interpretation rules will re-create consistency problems.
Fourth, Event Log needs Compaction or Surface Replace mechanisms to control model Context, but the original log may still grow long-term.
Fifth, developers must distinguish:
Fact Events
Real-time Runtime Events
Model Surface Events
UI-only Projections
DSH's architecture documents also clearly separate Session Event, Agent Event, and capability event. Facts needing to persist across reloads go into Session Event; intercepting work during execution uses agent/*. [3]
This boundary is very important. Writing all callbacks as persistent Events would make the log a low-level debug trace; keeping all execution facts as only in-memory callbacks would make recovery impossible.
When This Design is Worth Adopting
A simple Chat Bot usually doesn't need to save turn/start, Raw Chunk, and Request Header.
Event Log's benefits significantly increase when these requirements appear simultaneously:
Execution process needs recovery
UI needs complete Replay
One fact needs to produce multiple Projections
Need precise audit of Tool behavior
Need Fork / Resume
Need to explain what complete state a model request saw at that time
Need cross-process observation of Agent work
If the system only needs:
Display chat history
Send recent messages to model
messages[] + metadata is still a lower-cost design.
The value DSH provides isn't in "Event Sourcing itself", but in unifying several state sources that easily split in Agents:
SessionEvent Log
↓
Surface
↓
Model Messages
SessionEvent Log
↓
UI / Replay / Transcript
SessionEvent Log
↓
Persistence / Recovery
The entire system works around the same fact stream.
The next article will shrink this model into a runnable Demo. The Demo won't replicate all of DSH's events or implement Pi's Tree, but will preserve the four most critical mechanisms: append-only Event, continuous seq, Message Projection, Crash Repair. The goal is to verify one question: when only Event Log is saved, can the process deterministically recover Conversation and the next Model Context after restart?
References
[1] DeepSeek Harness Session: https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/subsystems/session.md
[2] DSH Session Surface: https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/core/session/src/surface.ts
[3] DeepSeek Harness Architecture: https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/architecture.zh.md
[4] DeepSeek Harness Persistence: https://github.com/deepseek-ai/deepseek-harness/blob/master/docs/subsystems/persistence.zh.md
[5] DSH Session implementation: https://github.com/deepseek-ai/deepseek-harness/blob/master/packages/core/session/src/index.ts