CH 10 · セッションログ=真実の源
この章の目標
前章では「帳簿」(session)を扱いました。この章ではそれをほどきます:なぜそれが機械全体の真実の源なのか —— モデルが覚えていること、見える軌跡、書き出せる文字起こし、開けるフォーク、すべてがこの 1 つのログから派生し、ログ自身は追記のみで決して編集されません。
1 行で:セッションログ = Agent の帳簿、記憶、アーカイブ
Session の公式定義は強烈で、3 点に分解できます:
- 追記専用:末尾に記録を足すだけ、修正しない、過去のものを削除しない。
- 型付きイベント:各行はフリーフォーマットのテキストではなく「イベント」の一種 ——
turn/start、user/message、assistant/message、tool/result、turn/end... - 単一の真実の源:Agent のインタラクション履歴全体、これが唯一の実在;その他はすべてその投影。
前章とつなげると実にスムーズです:前章で見た「メッセージの流れ」、各ステップは実際にはこのログへイベントを書き込んでいる —— turn/start でターンが開き、step/start でステップが始まり、user/message はあなたが送ったものを記録し、assistant/message はモデルの返答を記録し、tool/result はツールの実行を記録し、turn/end でターンが閉じる。
記憶法:流れは「起きていること」、ログは「起きたことの記録」、1 対 1 対応。
なぜ「真実の源」なのか
公式の言葉:
モデルのメッセージ履歴はログから派生し、決して別途保存しない。
意味:dsh では会話記録の 2 つ目のコピーがない。「モデルは前に言われたことを覚えている」と思うかもしれませんが、モデルが見る履歴はログから投影されたもの;CH 04 で見た軌跡ビュー、書き出せる文字起こし、開けるフォーク —— すべてこの同じログから描画されています:
- モデル会話履歴:
deriveMessages()がモデルに見せるMessage[]をログから投影 —— 「モデルが覚えているもの」=「ログにあるもの」; - 軌跡ビュー:CH 04 で見た
ASSISTANT / TOOLのタイムライン、ログの可視化に過ぎない; - 文字起こし/エクスポート:完全会話テキスト、ログから再生;
- fork ブランチ:ある歴史的ノードから新しいセッションを伸ばす;
- テレメトリ/統計:トークン使用量、所要時間、ログから算出;
- 永続化ファイル:
$DSH_HOME/sessionsのsession.jsonl.zstd。
なぜこう設計しなければならないのか?真実の源が 1 つだからこそ、「UI に表示されているもの、モデルが覚えているもの、エクスポートされるもの —— 3 つのコピーが食い違う」が絶対に起きない。 他のビューはすべて同じログの投影で、ルールは一貫、永久に一貫。
「モデルに見えたものが記録されている」:設計の軸
これは前章でも触れた dsh の厳しいルールで、ここで展開します:
モデルリクエストに到達するあらゆるものは、ログから再構成可能でなければならず、ランタイムはこれを不変条件としてチェックする。
ここから直接の帰結が 2 つ:
- モデルに見せる新しいものを足したいなら、新しいイベント型を足す必要がある。 例えば Agent に注入された文脈チャンクを見せたいなら、ログを迂回して直接リクエストに詰め込むことはできず、代わりに新しいセッションイベントを定義し、それをログに書き込み、ログから投影して出します。これによりすべてのステップが追跡可能になり、ホームページの「すべての実行が追跡可能」の下層保証になります。
- ログはロスレス。 モデルが返した生のストリーミングチャンクすら保存され(
assistant/chunk)、再生はトークン単位で忠実に再現でき、UI は正確にそれを復元できます。
1 行で:この設計は「追跡可能」をスローガンではなくアーキテクチャ上の必然にします。
CH 04 との簡単な相互参照:コンテキスト圧縮に使う /compact コマンドは、水面下では単に「圧縮した」というアクションをログに記録し(compaction/* イベント)、ログからモデルにより洗練された形を投影するだけです。履歴は書き換えない —— 元のイベントはログに残り続け、モデルに見せる投影だけが並べ替えられます。だから圧縮後も Agent が「洗練された版」を覚えているように見えても、元の記録は依然として完全なのです。
本当の姿:ローカルのセッションログ
自分のマシンに戻ります —— この PC を例に(CH 05 で headless を走らせた後、ここにあります):
C:\Users\mortal\.dsh\
├─ profiles\ ← profile 一覧(CH 08 の「メニューカード」)
├─ sessions\ ← セッションログはここ
│ ├─ --E-software-workspace-DeepSeek~0020harness~0020demo--\
│ │ └─ session-307edce2-...\session.jsonl.zstd ← CH 05 の headless 実行記録
│ └─ --E-software-workspace-doubaowork-DeepSeekHarnessGuide--\
│ └─ session-f417b4dd-...\session.jsonl.zstd ← このプロジェクトで使用したセッション
├─ storages\
├─ settings.yaml
└─ .credentials.yamlポイントいくつか:
- 作業ディレクトリ別のディレクトリ:ディレクトリ名は作業ディレクトリパスのエスケープ版(スペースは
~0020)、どこで作業中に生成されたセッションか一目で分かる; - 1 セッション 1 フォルダ、中には
session.jsonl.zstd—— JSONL 1 行ずつ追記 + zstd 圧縮の永続化ファイル; - CH 05 の「未分類」と呼応:これらのファイルは作業ディレクトリ別に保存されますが、Web UI のセッション一覧は headless 実行を未分類にまとめます —— 2 つの次元を混同しないでください;
- これらのファイルはセッションの「記憶アーカイブ」、気軽に削除しないでください。 削除すれば、そのセッションの「記憶」は本当に消えます。
fork:ログから新しいセッションを伸ばす
ログが完全に再生できるので、当然「ある位置から再生し直す」も可能 —— 公式チームはこの機能を fork と呼びます:安定した位置より前の全イベントを新しいセッションのオープニングとして複製し、そこから別々に行きます。使い方は直接的:元のセッションを触らずにある歴史的ノードで新しいパスを試したいとき。使い方の詳細は後ほどの実操作章で扱います;今は「存在する、そして存在できる理由はまさにログが完全再生できるからだ」とだけ覚えてください。
この章で学んだこと
- [ ] セッションログの 3 つの特徴(追記専用 / 型付きイベント / 単一の真実の源)を言える
- [ ] 前章のメッセージフローをログイベントと 1 対 1 対応させられる(turn/start、user/message、assistant/message、tool/result、turn/end)
- [ ] 「モデル履歴はログから派生し、別途保存されない」を説明でき、なぜこれが「3 つのコピーが食い違う」を防げるか言える
- [ ] 「モデルに見えたものが記録されている」の 2 つの帰結を言える(新しいものを足す = 新しいイベント型を足す;ロスレスログはトークン単位再生可能)
- [ ] ローカルのセッションログの場所($DSH_HOME/sessions の作業ディレクトリ別ディレクトリ、session.jsonl.zstd)を知り、Web UI の「未分類」との違いを知っている
