# 永続化層のセマンティック整理 ## 背景 現在の永続化は `SessionId` 単位の append-only JSONL log を中心に構成されている。これは実装上は扱いやすい一方で、今後 Pod 単位永続化、compaction、fork、DB backend 追加などを進めるにあたり、以下の概念が混ざり始めている。 - ユーザー視点の「同じ会話 / 作業の継続単位」 - Pod 視点の「現在 active な会話状態」 - append-only log の物理的 / 復元上の単位 - compaction によって生成される新しい履歴系列 - fork の起点となる履歴中の境界 - runtime dir に置かれる一時状態と、data dir / DB に置く永続正本 特に、現在は compaction によって新しい `SessionId` が発行される。これは append-only log の低レベル単位としては自然だが、ユーザー視点では「同じ会話が継続している」とも見えるため、`Session` という名称・粒度が今後の設計上あいまいになり得る。 このチケットでは、実装変更に入る前に、永続化層のドメイン概念・名称・責務境界を整理する。 ## 目的 - 永続化層で扱う概念を、ユーザー視点 / Pod 視点 / storage 視点に分けて定義する。 - `SessionId` が今後も適切な中心概念か、あるいは別概念に分解すべきかを判断する。 - compaction / fork / resume / Pod state / spawned child registry が、どの粒度のデータに属するかを決める。 - 将来 DB backend を追加しても歪みにくいデータ構造を設計する。 - 既存の session-store JSONL 実装から段階的に移行できる命名・API 境界を決める。 ## 結論: Session / Segment / Entry の 3 階層 ``` Session ← ユーザー視点の会話の家系(fork tree 全体。Segment 群の grouping) └── Segment ← Compaction / Fork で切れる物理 append-only 単位(現在の SessionId 相当) └── Entry ← 1 永続化イベント ``` セマンティクスの対応: - **resume** = 同 Session 内の指定 leaf Segment に append - **compaction** = 同 Session, new Segment(lineage: `compacted_from`) - **fork** (live / 過去いずれも) = 同 Session, new Segment(lineage: `forked_from`) - **branching は Session 内で完結**する。Segment 間が DAG、Segment 内は完全 linear。 - **Session は分岐ツリーを持つ静的な構造**。「今どの Segment に書いているか」(current active Segment) は Pod state 側が動的に保持する。 文脈合成 (merge / cherry-pick) は今後の要件として想定しないため、entry レベルの DAG(任意の親 pointer)は採用しない。Segment 内 linear + Segment 間 DAG の 2 階層に閉じることで、同 Segment 内は `(segment_id, seq)` の連番 PK で直線探索でき、Segment 間の lineage は粗粒度な DAG として軽量に管理できる。 fork で新しい Session を切らないのは、compaction で Segment を切るのと fork で Segment を切るのが対称な操作であり、ユーザー視点でも「同じ起源から派生した枝」として 1 つの単位で扱う方が自然なため。これにより Session 数が分岐で爆発せず、`WHERE session_id = ?` だけで fork tree 全体が取れる。 ### Restore / Resume の API Session が分岐を含むため、resume の指定は **(SessionId, leaf SegmentId) の組** で行う: - TUI 経路: ユーザーは **Session を選択 → その Session 内の leaf を選択**。 - 内部 API: restore は `(SessionId, SegmentId)` を取り、指定 Segment から replay する。leaf 以外を指定すれば read-only な過去状態の参照も可能。 - 「最後に active だった leaf」は Pod state が保持するので、TUI が初期選択候補として使える。 ### 既存コードとの対応 | 既存 | 新名称 | |---|---| | `SessionId` | `SegmentId` にリネーム | | (なし) | `SessionId` 新設(Segment 群をまとめる、ユーザー視点 ID) | `session-store` crate の型・関数は Segment 中心の命名に揃える。crate 名自体を変えるかは別論点(中身は引き続き Segment 単位の append-only log を扱う)。 ### llm-worker への影響 llm-worker は session 概念を持たない(`Worker` は `history` / `turn_count` / `RequestConfig` を持つだけで永続化への hook も無い)。Session / Segment 階層の導入は llm-worker 層に染み出さず、影響範囲は `session-store` / `pod` / `pod-registry` / `pod-cli` に閉じる。 ## Entry hash の廃止 現状、各 entry は SHA-256 hash chain (`prev_hash` → `hash`) を持つが、実際に効いている用途は 2 つだけ: 1. `ensure_head_or_fork` (pod.rs:1348) — store の末尾と Pod の保持する `head_hash` を比較し、不一致なら auto-fork。**末尾識別子があれば良い**(hash chain そのものは要らない)。 2. `fork_at(source_id, at_hash)` (session.rs:425) — 過去 entry pointer から fork。`pod-session-fork.md` の入口仕様は turn number で、entry hash は内部 pointer に過ぎない。turn boundary (TurnEnd entry の index) で代替可能。 改竄検知 (tamper-evident chain) は宣伝されているがコード上は walk して verify するルートが無いため、削除しても regression にならない。 廃止に伴う対応: - `HashedEntry` 廃止、JSONL は 1 行 1 `LogEntry`。 - `SessionOrigin.at_hash` → `at_turn_index` (TurnEnd 由来) に置換。 - `ensure_head_or_fork` の検知ロジックは、Segment 末尾の terminal marker entry または末尾 seq 比較に置換(形式は実装時に決める)。 ## Fork: 2 種類の書き込み方 Session 境界の話ではなく **元 Segment への marker 書き込みの有無**で 2 種類を分ける。Session はどちらの場合も同じで、新 Segment が同 Session 内に生える。 - **live auto-fork**(concurrent writer 検知) - 元 Segment の末尾に terminal marker (`Forked { to: SegmentId }` 等) を append → 以降の writer は marker を見て新 Segment へ自動移動。 - CoW semantics: 元 Segment は immutable、生まれた Segment 同士は対等な兄弟。 - **過去 fork**(UI で turn 選択) - 元 Segment は **無変更**、replay して新 Segment を生やすだけ。 - 起点は `(source_segment_id, at_turn_index)`。 - 元 Segment に書き込まないため、過去 fork を nested に重ねても解釈が単純。 両者を同じ marker で扱おうとすると、過去 fork から更に過去で fork した場合に元 Segment への marker 位置解釈が複雑化して破綻するため、過去 fork 側は元 Segment に触れない方針で固定する。 ### fork の物理操作(物理コピーは不要) 「fork = 同 Session 内に Segment を増やす」と言っても、過去 Segment を丸ごとコピーする必要は無い: - source segment の seq=N までを replay して得られた `Vec` を、新 Segment の seed entry (`SessionStart` 相当) に 1 回書き込む。 - compaction の transitive な圧縮効果がここに乗る(source segment の `SessionStart.history` に過去 Segment の compaction 結果が既に埋まっている)ので、**source segment 1 本だけ replay すれば fork seed が作れる**。さらに過去の Segment は touch しない。 - 新 Segment が持つ lineage 情報は `(parent_segment_id, fork_at_turn)` のメタデータのみ。 これは現状の `fork_at` (`session.rs:430-456`) の挙動と同じで、Session 階層が乗っても操作は変わらない。 ## RDB backend を想定した概念モデル 将来 DB backend を追加しても歪みにくい形として、以下の関係を仮定する: ``` sessions ( id PK, ... -- ユーザー視点 metadata ) segments ( id PK, session_id FK, parent_segment_id FK NULL, -- compaction / fork の元 fork_at_turn INT NULL, origin_kind ENUM (new | compact | fork), lineage_path ltree, -- 祖先・子孫の逆引き用 (materialized path) ... ) entries ( segment_id FK, seq INT, kind, payload, ts, PRIMARY KEY (segment_id, seq) -- 同 Segment 内 linear scan ) ``` - 同 Segment 内 entry は `(segment_id, seq)` PK で linear scan、surrogate identity なので hash 不要。 - Segment 間 lineage は `parent_segment_id` chain。深さは compaction / fork 回数のみ(Session あたり数〜数十)。 - Segment lineage の祖先・子孫逆引きは `lineage_path` の `<@` / `@>` で 1 index 引き。entry 単位の DAG ではないため materialized path のメンテコストも軽い。 - 通常 append は `lineage_path` 更新を伴わない(Segment 生成時に確定)。 - 「Session の fork tree 全体」は `WHERE session_id = ?` で取れる。Session 単位の listing / GC が自然。 FsStore 実装はこの構造のサブセット相当として位置付ける(1 Segment = 1 jsonl、`session_id` は Segment の metadata に持たせるか別ファイルに index する)。 ## 残る検討事項 - pod.scope extension entry を Pod state 側に寄せるか、Segment log に残すか(`tickets/pod-persistent-state.md` 側と合わせて決定)。 - 撤廃の選択肢: (a) Segment log から削除し Pod state を唯一の正本にする / (b) snapshot 保持責務だけ Pod state に寄せ、scope 変更 event は Segment log に残す / (c) 現状維持で Pod state は Segment への参照のみ。 - live auto-fork の marker 形式 (terminal entry vs Segment 末尾 seq 比較)。 - pod-cli / TUI の `--session` 引数を Session 単位にするか Segment 単位にするか。debug 用 ID とユーザー向け ID の分離。 - `session-store` crate 名のリネーム要否。 ## 完了条件 - 永続化層の主要概念と名称が文書化されている。 - compaction / fork / resume / Pod state のデータ粒度が決まっている。 - 現在の `SessionId` / session-store API をどう扱うか、維持・alias・rename・段階移行の方針が決まっている。 - DB backend を追加する場合の概念モデルが、最低限テーブル / relation 相当で説明できる。 - `tickets/pod-persistent-state.md` や fork 関連チケットに反映すべき前提が整理されている。 ## 範囲外 - このチケット単体での大規模 rename 実装。 - DB backend の実装。 - UI の履歴表示 / branch 表示の詳細 UX。 - GC / retention policy の実装。 - Session を跨ぐ merge / cherry-pick。 ## 関連 - `tickets/pod-persistent-state.md` - `tickets/pod-session-fork.md` - `crates/session-store/` - `crates/pod/src/pod.rs`