PiのSessionは、主に「分岐した履歴を現在のContextへどう復元するか」という問題を軸に設計されています。DeepSeek Harness(DSH)は、より強い永続化の境界を選びました。Sessionそのものがappend-onlyな型付きSessionEventログとなり、モデルのメッセージ、UI、Replay、復元状態はすべて同じログから派生します。[1]
公式ドキュメントは、このログをAgentとのすべてのやり取りの履歴におけるsingle source of truthと定義しています。モデルメッセージは別の権威ある状態として個別に保持されるのではなく、Session LogからderiveMessages()によって投影されます。[1]
この設計が最初に解決するのは、複数ある状態間の整合性問題です。
Messageと実行履歴を別々に永続化する場合
よく使われるAgentのストレージモデルは、次の形になります
messages table
runs table
tool_calls table
stream_events table
どのテーブルももっともな設計ですが、1回の実行では複数の状態を同時に更新します。
たとえば、モデルが応答し始めると、次の順序で処理が進みます。
创建 Run
追加 streaming chunks
生成 Assistant Message
写 Tool Call
写 Tool Result
更新 Run 状态
そのいずれかの書き込みが失敗した場合、「どの状態がコミット済みか」を処理する必要があります。復元ロジックも、どのテーブルのほうが信頼できるかを判断しなければなりません。
DSHは、こうした事実をまず一つの順序あるイベントストリームに統一します。
turn/start
step/start
user/message
request/header
assistant/chunk
assistant/chunk
assistant/message
tool/call
tool/result
step/end
turn/end
各イベントには単調増加するseqが付きます。通常のログではseqが連続している必要があるため、イベントはSession内で確定した位置を持ちます。[1]
すると、復元問題は次の形になります。
读取 SessionEvent[]
↓
按 seq Replay
↓
重新构造需要的 Projection
中核状態のソースはひとつだけです。
SessionEventが記録する範囲はMessageより広い
DSHが使うイベントには、複数種類の事実が含まれます。[1]
このイベント群から、DSHのSessionの定義はConversation Transcriptを超えていることがわかります。
たとえば、assistant/chunkは次のLLM Historyには直接組み込まれません。ただし、元のストリーミング出力の軌跡は保持されます。turn/startとstep/startは実行の境界を表し、request/headerには、そのリクエストで使用したモデル設定、System Prompt、Tool schemasが保存されます。
同じEvent Logは複数のコンシューマーを支えます。
これがAgent用途でEvent Sourcingの主な価値十足な点—.Tool、Stream、モデルのメッセージ、実行境界は、そのまま時系列になります。
Surface:Event Logとモデル履歴の間の中間層
すべてのSessionEventを直接モデルメッセージに変換すると、 明らかな問題が生じます。
モデルに見せる必要がないのは次のようなものです。
turn/start
step/end
assistant/chunk
request/context
そこでDSHは、完全なEvent Logの上にSurfaceという層を定義しています。
現在のコアでは、次の3種類のイベントだけがモデルから見えるSurfaceへ追加されます。[2]
deriveEventMessage()のロジックも直接的です。
user/message → UserMessage
assistant/message → AssistantMessage
tool/result → ToolResultMessage
其他事件 → null
全体像は次の形になります。
Full Session Log
↓
Surface Fold
↓
Surface Nodes
↓
deriveEventMessage()
↓
Model Messages
この層には重要な意味があります。SessionにはContextより豊かな事実を保存できる一方、Model Historyの構造は安定させられます。
surfaceOpによってCompactionもappend-onlyを維持できる
通常のメッセージがSurfaceに追加されるときはappendを使います。
M1 → M2 → M3 → M4
Compactionには特殊な問題があります。その後のモデルには、ある範囲の古い履歴の代わりにSummaryを見せる必要があります。しかしEvent Logから古いイベントを削除してはいけません。
DSHのSurfaceには、置換操作がサポートされています。[1][2]
現在のSurfaceが次の状態だとします。
10 15 21 30 35
新しいSummary Eventは次の形で宣言できます。
replace 10..30
新しいSurfaceは次の形になります。
42(summary) 35
ただしSession Logには元々の10、15、21、30が残っています。
したがって、責務は次のようになります。
Log
只追加事实
Surface
允许改变模型可见顺序和范围
PiのCompactionの考え方と似ています。どちらでも元の履歴は保持し、以降のモデルContextの構築方法だけを変更します。DSHはさらにこの原則を、汎用的なSurface Operationとして形式化しています。
sourceEventSeqsが派生関係を保存する
DSHのSurface EventにはsourceEventSeqsも記録できます。[1]
最も直感的な例がStreamingです。
assistant/chunk seq=20
assistant/chunk seq=21
assistant/chunk seq=22
↓ assemble
assistant/message seq=23
sourceEventSeqs=[20,21,22]
こうすると、完全なMessageと元のChunkの両方が同時に存在しても、両者の由来関係は失われません。
このフィールドはSurface Replaceでも同様に重要です。Summaryが複数の古いSurface Nodeを置き換える場合、置き換え対象として参照したノードを明示する必要があります。Surface foldはこれらのprovenance関係を検証します。[2]
この結果、Event Logは「時刻順にJSONを保存する」だけでなく、派生グラフの一部も保持します。
Raw Events
↓ provenance
Derived Surface Event
Debug、Replay、監査では、最終的なMessageだけを残すより信頼できます。
なぜRequest Headerまで記録するのか
DSHのアーキテクチャ文書では、モデルに送るリクエストの入力をログから再構築できなければならないという、厳しい要件が示されています。[3]
Messageだけを保存しても不十分です。実際のLLM Requestには次の情報も含まれます。
provider / model
reasoning config
sampling config
System Prompt
Tool schemas
これらはrequest/headerに保存されます。[1]
現在のEpochHeaderは、おおむね次の内容を持っています。
各Loopインスタンスの開始時に完全なHeaderを記録し、設定が変更されるたびに新しいSnapshotを追加します。復元時は最新のHeaderを読み取ればリクエスト環境を再構築できます。[1]
これはよく見落とされる問題です。
消息完全相同
+
System Prompt / Tools不同
=
实际模型语义可能完全不同
「正確なReplay」を保証するシステムなら、Messageだけを保存するだけでは不十分です。
DSHは「モデルから見える状態を再構築できること」をSessionのランタイム不変条件としています。[3]
記録の完全性によってログは大きくなりschemaも複雑になりますが、Resume、Fork、Replayの意味はより明確になります。
Persistenceは同じログを永続化する役割だけ
Event Sourcingには、メモリ側のEventとデータベース側で定義されたPersistence Eventが別のものになる、という複雑さが生じます。
DSHは現時点でこの状態を意図的に避けています。Persistence seamは既存のSessionEventをそのまま永続化し、並列するイベントモデルを持ちません。[4]
実行時にEventを追加すると、session/event通知が同期的に発行されます。Persistence PluginはそのEventをper-session bufferへ複製し、後からまとめてバックエンドへ書き込みます。Eventを生成するAgent Loopは、1件ごとのディスク書き込みを待つ必要はありません。[4]
フローはおおむね次のようになります。
Session.append(event)
↓
In-memory log committed
↓
session/event
↓
Persistence buffer
↓ batch
JSONL / SQLite
永続化の境界を確定したい場合は、session/flushでバッファを空にします。
ここで2つの概念は明確に分かれています。
Session
定义事实和顺序
Persistence
决定这些事实如何耐久化
そのためJSONLとSQLiteは差し替え可能なBackendになり、上位のEvent Modelを変更する必要はありません。[4]
Crash Recoveryは中断中のTurnを削除しない
AgentプロセスがTurnの途中でクラッシュする可能性があります。
ディスクにはすでに次のようなものが存在する可能性があります。
turn/start
step/start
user/message
assistant/message
tool/call
ただし、次は存在しません。
tool/result
step/end
turn/end
未完了の最後のTurnを切り詰める方法もあります。DSHは現時点で、耐久化済みの事実は保持し、コールドリカバリ時にsyntheticイベントを追加する方針を選びました。
turn/end {
reason: interrupted
}
未対応のTurnを閉じるために追加します。[4]
復元後のログも、次の事実を明確に示します。
这个 Turn 曾经发生
其中一部分工作已经完成
进程在结束前被中断
Turn全体を削除するより、このほうがappend-onlyな事実モデルに合っています。
長時間の処理では特に重要です。1つのTurnに多数のStepと大量のTool Resultが含まれていても、それまでに生成された事実が、最後のクラッシュをきっかけにまとめて失われることはありません。
Session HeaderをEvent Logの外に置く理由
DSHも、すべてのデータをEventとして表現しているわけではありません。
Sessionの保存メタデータは、別のSessionHeaderに保持します。たとえば次の形です。[4]
version
id
createdAt
cwd
parentSession
seedLength
origin
delegationDepth
agentPreset
これらのフィールドはSessionそのものとその保存系統を示し、相互作用のタイムラインには属しません。
たとえば次のとおりです。
parentSession
这份 Session 从哪个 Session fork
seedLength
前多少条 Event 属于继承前缀
agentPreset
恢复这个 Session 应使用哪种 Agent 组合
ここには、保持すべき価値のある境界があります。Event Logは「セッション内で何が起きるか」を記録し、Headerは「このセッション記録が何を表すか」を保存します。
Event Sourcingは、すべてのMetadataをイベントに変換する必要はありません。
DSHのForkはSession Lineageに近い
Piは、1つのSessionファイル内に木構造を作れます。
DSHのSession Log自体は線形のままです。
seq 0
seq 1
seq 2
...
Fork時には新しいSessionを作り、安定した接頭辞をSeedとして引き継ぎます。HeaderのparentSessionとseedLengthが系譜を記録します。[4]
次の形で表現できます。
Session A
0 1 2 3 4 5
│
└──── fork at prefix
↓
Session B
0 1 2 3 | 4' 5' 6'
したがって、PiとDSHは分岐の扱いについて、代表的な2つのモデルを示しています。
Pi
一个 Session 内保存 Entry Tree
DSH
每个 Session 保持线性 Event Log
分支形成 Session Lineage
前者は、1つのCoding Session内で履歴を行き来する操作に向いています。後者はEvent seqの線形性とReplayモデルの simplicityを保ちます。その代償として、Forkは新しいSession Identityを生みます。
実際の製品挙動を見ないと、どちらが優れているかは判断しにくいです。
Event Logのコスト
DSHの設計は強い再構築能力を得る代わりに、明確なコストも抱えています。
第一に、イベント数 bloodstreamALOG 증가. Streaming Chunkもログに入るため、1回の応答で大量のEventが生成されることがあります。
第二に、Event Schemaは永続化プロトコルです。一度公開されると、型を変更する際には古いログへの影響を考える必要があります。現在のPersistenceは、形式バージョンと未知の必須イベントを厳格に検証し、再構築の意味に影響するイベントを黙って無視することを防ぎます。[4]
第三に、すべてのProjectionは決定論的でなければなりません。Surface、Message、UI、Transcriptで解釈のルールが異なれば、一貫性の問題は再発します。
第四に、Event LogにはCompactionやSurface Replaceなどの仕組みが必要ですが、元のログは長期的に増え続けます。
第五に、開発者は次を区別しなければなりません。
事实 Event
实时 Runtime Event
模型 Surface Event
UI-only Projection
DSHのアーキテクチャ文書は、Session Event、Agent Event、capability eventを明確に分離しています。リロードをまたいで保持すべき事実はSession Eventに入り、実行中に介入する処理にはagent/*を使います。[3]
この境界は重要です。すべてのcallbackを永続Eventとして書くと、ログは低レベルのdebug Traceになります。一方、すべての実行事実をメモリ上のcallbackだけにすると、復元できなくなります。
このような設計を採用すべき場面
単純なChat Botなら、turn/start、Raw Chunk、Request Headerを保存する必要は通常ありません。
次の要件が同時にそろうと、Event Logの効果が大きくなります。
必要なのが次の2つだけなら、messages[] + metadataのほうが低コストです。
展示聊天历史
把最近消息发送给模型
DSHの価値は「Event Sourcingそのもの」ではなく、Agentの中で分断されやすい複数の状態源を統一したことです。
システム全体が同じ事実の流れを軸に動きます。
次の記事はこのモデルを実行可能なDemoまで縮小します。DemoではDSHのすべてのイベントを複製したり、PiのTreeを実装したりはせず、append-only Event、連続したseq、Message Projection、Crash Repairという4つの重要な仕組みだけを残します。検証したい問いは1つです。Event Logだけを保存した場合、プロセスの再起動後にConversationと次のModel Contextを決定論的に復元できるかどうかです。
参考資料
[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