ticket: 永続化整理を 8 個に分割

persistence-semantics と pod-persistent-state を実装可能な粒度に分割。
Storage 層 (Phase 1) を entry-hash-abolish / segment-rename /
session-grouping-introduce / live-fork-marker に、Pod 単位永続化
(Phase 2) を pod-state-backend / pod-state-write-points /
pod-name-resume / spawned-registry-persist に切り出した。
This commit is contained in:
Keisuke Hirata 2026-05-20 04:07:44 +09:00
parent 72c1d04cf2
commit 3d091acacd
11 changed files with 329 additions and 274 deletions

12
TODO.md
View File

@ -7,8 +7,16 @@
- Pod: 空応答ターン (Submit 後 AI 応答ゼロで Pause/Cancel) を自動巻き戻し → [tickets/pod-empty-turn-rollback.md](tickets/pod-empty-turn-rollback.md)
- Pod: 任意ターンからの Fork複数ターン巻き戻しを汎用化 → [tickets/pod-session-fork.md](tickets/pod-session-fork.md)
- Pod: Inbound PodEvent ハンドリングの重複を統合 → [tickets/pod-inbound-pod-event-dedup.md](tickets/pod-inbound-pod-event-dedup.md)
- Pod: セッションログをバックエンドにした Pod 単位の永続化 → [tickets/pod-persistent-state.md](tickets/pod-persistent-state.md)
- 永続化層のセマンティック整理 → [tickets/persistence-semantics.md](tickets/persistence-semantics.md)
- 永続化層整理 (Storage)
- Entry hash chain 廃止 → [tickets/entry-hash-abolish.md](tickets/entry-hash-abolish.md)
- SessionId → SegmentId リネーム → [tickets/segment-rename.md](tickets/segment-rename.md)
- Session (Segment 群の grouping) 導入 → [tickets/session-grouping-introduce.md](tickets/session-grouping-introduce.md)
- live auto-fork の marker 形式確定 → [tickets/live-fork-marker.md](tickets/live-fork-marker.md)
- Pod 単位永続化
- Pod state backend と FsStore 実装 → [tickets/pod-state-backend.md](tickets/pod-state-backend.md)
- Pod lifecycle 各点での write 配線 → [tickets/pod-state-write-points.md](tickets/pod-state-write-points.md)
- Pod 名単位の resume / attach 導線 → [tickets/pod-name-resume.md](tickets/pod-name-resume.md)
- SpawnedPodRegistry の永続化と復元 → [tickets/spawned-registry-persist.md](tickets/spawned-registry-persist.md)
- llm-worker のエラー耐性
- ストリーム途中失敗時の継続 → [tickets/llm-worker-stream-continuation.md](tickets/llm-worker-stream-continuation.md)
- llm-worker: history append を callback 経由の単一経路に閉じる → [tickets/worker-history-append-contract.md](tickets/worker-history-append-contract.md)

View File

@ -0,0 +1,42 @@
# session-store: Entry hash chain の廃止
## 背景
session-store の各 entry は SHA-256 hash chain (`prev_hash` → `hash`) で連結されており、`HashedEntry` として JSONL に 1 行ずつ書かれる。実際に効いている用途は以下の 2 つだけ:
1. `ensure_head_or_fork` (`crates/pod/src/pod.rs:1591`) — store 末尾と Pod 保持の `head_hash` を比較して auto-fork。**末尾識別子があれば良い**。
2. `fork_at(source_id, at_hash)` (`crates/session-store/src/session.rs:425`) — 過去 entry からの fork。`pod-session-fork` の入口仕様は turn 番号であり、entry hash は内部 pointer に過ぎず turn boundary (TurnEnd entry の index) で代替可能。
宣伝されている改竄検知 (tamper-evident chain) は walk して verify するルートがコード上に存在せず、削除しても regression にならない。
write 経路は既に sync 化済み (`6e5b148`)。前提足場は揃っている。
## 要件
- `HashedEntry` 廃止、JSONL は 1 行 1 `LogEntry`
- `compute_hash` / `build_chain` / `EntryHash` の撤去(外部に公開している場合は呼び出し元を含めて)。
- `SessionOrigin.at_hash``at_turn_index` (TurnEnd entry 由来の turn 番号) に置換。
- `ensure_head_or_fork` の検知ロジックを末尾 seq 比較ベースに置換。形式は実装時に決めるterminal marker entry / 末尾 seq の何れか)。
- **`session_head` mutex の撤去**。hash chain が無くなる結果として "head_hash を直前 entry から取得して次へ渡す" という serialize 必須の依存が消える。1 行 < `PIPE_BUF` (Linux 4KB) の `O_APPEND` write は kernel が atomic に直列化するため user space で mutex 不要。
- 既存 JSONL の読み込み互換は不要(プロジェクト方針として後方互換性は作らない)。
## 完了条件
- `HashedEntry` / `prev_hash` / `compute_hash` / `build_chain` / `EntryHash` がコードベースから消えている。
- `SessionOrigin``at_turn_index` を保持し、`fork_at` も同 API になっている。
- `session_head` mutex への参照が無く、`SessionLogWriter` 系は writer ハンドルを `Arc` で持つだけになっている。
- `cargo check --workspace` および `cargo test -p session-store -p pod` が通る。
- 既存 session を新規再生成して JSONL が 1 行 1 `LogEntry` になっていることを目視確認できる。
## 範囲外
- `SessionId``SegmentId` のリネーム(別チケット `segment-rename`)。
- 新 `SessionId` (Segment 群の grouping) 導入(別チケット `session-grouping-introduce`)。
- live auto-fork の marker 形式の最終決定(別チケット `live-fork-marker`、ここでは末尾 seq 比較相当の最小実装で繋ぐ)。
## 関連
- `crates/session-store/src/session_log.rs`
- `crates/session-store/src/session.rs`
- `crates/pod/src/pod.rs:1591` `ensure_head_or_fork` 経路
- `tickets/segment-rename.md` (後続)

View File

@ -0,0 +1,40 @@
# session-store: live auto-fork の marker 形式確定
## 背景
`entry-hash-abolish``ensure_head_or_fork` は末尾 seq 比較ベースに置換されたが、最小実装で繋いだだけで marker 形式の本決定は保留している。
live auto-forkconcurrent writer 検知)と過去 forkUI から turn 選択)は性質が違う:
- **live auto-fork**: 元 Segment の末尾に terminal marker (例: `Forked { to: SegmentId }`) を append する CoW semantics。以降の writer は marker を見て新 Segment に自動移動。
- **過去 fork**: 元 Segment は無変更で、replay して新 Segment を生やすだけ。
両者を同じ marker で扱うと、過去 fork から更に過去で fork した場合に元 Segment への marker 位置解釈が複雑化して破綻する。**過去 fork は元 Segment に触れない方針を固定**した上で、live auto-fork 側の marker 形式を確定する。
## 要件
- live auto-fork 検知の形式を以下のどちらかに確定して実装:
- (a) **terminal marker entry**: 元 Segment 末尾に `Forked { to: SegmentId }` 等の LogEntry を 1 行 append する
- (b) **末尾 seq 比較**: 元 Segment に書き込みは行わず、writer の保持する末尾 seq と store の末尾 seq の差分のみで検知する
- 選択基準:
- (a) は読み手側が fork チェーンを log だけから辿れる利点。書き手競合時に 1 write 増えるコスト。
- (b) は元 Segment が完全 immutable で、過去 fork との semantics 統一が綺麗。fork 関係を引くには別の metadata index が要る。
- 過去 fork 側は引き続き元 Segment を touch しないことを invariant として明文化。
- `pod.rs``ensure_head_or_fork` を確定後の形式に合わせて書き直す。
## 完了条件
- live auto-fork の marker 形式が決まり、実装に反映されている。
- 過去 fork からの nested 過去 fork が正しく動くtest で確認)。
- 並行 writer による live auto-fork が正しく検知され、新 Segment に分岐するtest で確認)。
## 範囲外
- 過去 fork の物理コピー方式(既に `fork_at``SessionStart` seed 1 回書き込みの方針で固定)。
- Fork tree の可視化 UI。
## 関連
- `tickets/entry-hash-abolish.md` (前提)
- `tickets/session-grouping-introduce.md` (前提、Session 単位の lineage と整合)
- `tickets/pod-session-fork.md`

View File

@ -1,188 +0,0 @@
# 永続化層のセマンティック整理
## 背景
現在の永続化は `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 Segmentlineage: `compacted_from`
- **fork** (live / 過去いずれも) = 同 Session, new Segmentlineage: `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 比較に置換(形式は実装時に決める)。
### 廃止前の足場 (前提)
本セクションを実装に移すタイミングでは、log writer が既に sync 化されていることを前提にする (`tickets/log-entry-singular-and-direct-commit.md`)。具体的には:
- `Store::append` / `read_all` 等が `std::fs` ベースの sync API
- `SessionLogWriter::append_entry()` が sync 関数
- `session_head` mutex は `parking_lot::Mutex` / `std::sync::Mutex`
- `LogCommand` / drain task / Flush バリアは既に撤廃済み
この状態で hash chain を廃止すると追加で取れる単純化:
- **`session_head` mutex そのものを撤去できる**。 hash chain が無いので「`head_hash` を直前 entry から取得して次に渡す」 という serialize 必須の依存が消える。 1 行 < `PIPE_BUF` (Linux 4KB) の `O_APPEND` write は kernel 側で atomic に直列化されるので、 user space で mutex を持つ必要が無い
- `session_head` が消えると Pod / interceptor / worker callback が writer ハンドルだけ持てば良くなる。 `Arc<SessionLogWriter>` は単に `Arc<Store> + sink` を抱えるだけの値で、 hot-path の競合がない
- `compute_hash` 呼び出しが消える分、 append が serialize + open + write + close の 3 syscall まで詰まる
つまり「sync 化」 が先に来て、 「hash 廃止」 で mutex まで消える、 という 2 段階の単純化になる。
## 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<Item>` を、新 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`

View File

@ -0,0 +1,37 @@
# Pod state: Pod 名単位の resume / attach 導線
## 背景
`pod-state-write-points` で Pod state が active session を保持するようになる。本チケットでは pod-cli / TUI 側から **Pod 名で resume / attach できる入口**を確定し実装する。
既存の `--session <UUID>` resume は引き続き使えること。
## 要件
- pod-cli の引数仕様を確定:
- 例: `pod --pod <name>` で manifest cascade から同名 Pod state を引いて resume
- `--session` との同時指定時の優先順位 / エラーを明示
- 解決順序: Pod 名 → Pod state → active `(SessionId, SegmentId)` → session restore。
- Pod state が存在しない pod 名で起動した場合: 新規 Pod として作成 (initial Pod 起動と同じパス)。
- TUI 側の入口は本チケットでは「最小限の resume / attach 導線」のみ。Pod 一覧 UI や history UX は別チケット。
- 衝突検出: Pod 名が既に live で running なら pod-registry が検知して reject する既存挙動を維持。
## 完了条件
- pod-cli の `--pod` (名称は実装時確定) で resume できる。
- TUI から Pod 名で attach する最小経路が動作する。
- `--session <UUID>` resume が壊れていない。
- `cargo check --workspace` および `cargo test -p pod-cli -p pod` が通る。
## 範囲外
- TUI 上の Pod 一覧 UI / fork tree 可視化。
- spawn された子 Pod 一覧の復元(別チケット `spawned-registry-persist`)。
- Session 単位 / Segment 単位の resume 引数(本チケットでは Pod 名から内部解決のみ)。
## 関連
- `tickets/pod-state-backend.md` (前提)
- `tickets/pod-state-write-points.md` (前提)
- `crates/pod-cli/`
- `crates/pod-registry/`

View File

@ -1,84 +0,0 @@
# Pod: セッションログをバックエンドにした Pod 単位の永続化
## 背景
現在の永続化の主軸は session-store の append-only JSONL ログで、`SessionId` 単位に会話履歴・設定・scope snapshot・usage・拡張 payload を復元できる。一方で Pod 単位のランタイム状態は `<runtime_dir>/{pod_name}/` 配下の `status.json` / `history.json` / `spawned_pods.json` などに write-through されているが、runtime dir は再起動で消えてよい領域であり、Pod プロセスの寿命を超える復元ソースとしては扱えない。
特に spawned Pod の管理情報は `SpawnedPodRegistry` のコメントにもある通り、現状は runtime dir への write-through のみで、再起動した spawner が子 Pod 一覧を rebuild する future work になっている。
このチケットでは、既存の session-store を物理バックエンドとして利用しつつ、Pod 名をキーにした永続状態を追加し、Pod 単位で「最後にどの session を保持していたか」「spawned children をどう復元するか」を扱えるようにする。
## 方針
- session log は引き続き会話状態の唯一の復元ソースにする。
- `history.json` や runtime dir の snapshot を永続正本にはしない。
- LLM context に載せる新規 input は、既存方針通り先に worker history / session log に commit されている必要がある。
- Pod 単位の永続化は「Pod identity → session / child registry などへの参照」を保存する薄いメタデータ層として設計する。
- 会話本文を二重保存しない。
- active session だけでなく、compaction / fork / resume によってその Pod が辿ってきた過去 session を順序付きで保持する。これは UI の履歴表示、直近以前への復元、active session 変更の監査に使う。
- session-store の `Store` trait を拡張するか、隣接 trait / module を追加して、FsStore 以外の backend でも同じ形で実装できるようにする。
- FsStore のデフォルト layout は `<data_dir>/pods/` 配下など、`sessions/` と同じ data_dir 管理下に置く。
- runtime dir (`<runtime_dir>/{pod_name}/`) は引き続き socket / pid / status など一時状態専用。
- Pod lifecycle 上の write point を明確にする。
- Pod 作成時: pod name と allocated session id を記録。
- first run で `SessionStart` が materialize された後: active session / head を更新できる状態にする。
- compaction / fork / resume で active session が変わる場合: Pod state も同時に更新。
- `SpawnPod` / callback / `StopPod` による child registry 変更時: runtime dir だけでなく persistent Pod state にも write-through。
- 復元時は Pod state から active session を解決し、その session log を `restore_from_manifest` 相当の経路で復元する。
- session id を明示した resume は既存通り session を直接指定できる。
- Pod 名 resume は Pod state → active session → session restore の順に解決する。
- live writer 衝突は既存の pod-registry / session_id collision check を維持する。
## データ粒度の考え方
- ユーザー視点の会話継続単位と、内部の append-only log 単位を分けて扱う。
- ユーザー視点: Pod / thread / conversation のような安定 ID。compaction しても同じ会話として継続する。
- 内部 log 視点: session segment / revision / epoch のような履歴再構築単位。compaction や fork で新しい log root が必要なら新 ID になる。
- 現状の `SessionId` は内部 log 単位の性質が強い。compaction は履歴を要約済み prefix に置き換えて新しい append-only chain を始めるため、低レベルには「新 session」として扱うのは自然。ただし UX / データモデル上は「同じ Pod conversation の新 revision」と見せる。
- 将来 DB backend を追加する場合も、`Conversation/PodState` と `SessionSegment` を分ける形に寄せる。
- `pod_state.active_session_id` は現在 append 先の segment を指す。
- `pod_state.session_history[]` は Pod 視点で active だった segment の順序付き履歴。
- compaction / fork の構造的 lineage は session log の `SessionOrigin` または DB の relation として保持し、Pod state は「この Pod がどれを active にしたか」の操作履歴に留める。
## 要件
- Pod 名をキーに、少なくとも以下を永続化できること:
- active `SessionId`
- ordered session history: その Pod が active として保持してきた `SessionId` の時系列リスト
- 各 entry には最低限 `session_id` と遷移理由new / resume / compact / fork など)を持たせる
- compaction / fork の構造的な出自は session log の `SessionOrigin` を正本とし、Pod state 側は Pod 視点の active session 遷移履歴として扱う
- Pod manifest / scope 復元に必要な参照または snapshot の扱い(既存 session log の `pod.scope` snapshot と責務を重複させない)
- spawned children の registrypod name, socket path, delegated scope, callback address, child session id が必要なら含める)
- `SpawnedPodRegistry` が runtime dir の `spawned_pods.json` だけでなく、Pod 永続状態から初期化できること。
- `ListPods` / `SendToPod` / `ReadPodOutput` / `StopPod` は、復元後の spawner でも永続化された child registry を基に動作できること。
- ただし `ReadPodOutput` の read cursor は session-lifetime / in-memory のままでよい。永続化対象にしない。
- Pod の compaction により active session id が変わった場合、Pod 永続状態と pod-registry の session id が整合すること。
- 既存の `--session <UUID>` resume は壊さない。
- 新しい Pod 名単位 resume / attach の入口を決めること。
- 例: `pod --pod-state <name>` ではなく、既存 `pod.name` と manifest cascade から同名 Pod state を探す形など。
- CLI / TUI の最小導線を本チケット内で確定する。
## 完了条件
- `session-store` に Pod 単位メタデータを扱う backend API と FsStore 実装がある。
- Pod state が active session と ordered session history を保持し、new / resume / compaction / fork の遷移が順序付きで記録される。
- 新規 Pod 起動、resume、compaction、spawn / stop の各タイミングで Pod 永続状態が更新される。
- Pod プロセス再起動後、Pod 名から active session を復元し、会話を継続できる。
- spawner Pod の再起動後、永続化された spawned children 一覧から `ListPods` が復元され、到達可能な child に対して comm tools が使える。
- runtime dir は引き続き一時状態として扱われ、永続正本に依存しない。
- live writer の二重起動は既存 pod-registry / session lock と同等以上に防止される。
## 範囲外
- 会話履歴そのものの保存形式変更。
- session log の DB 化や remote backend 実装。
- Pod state の自動 GC / retention policy。
- TUI 上の高度な Pod 一覧 UI。最小限の resume / attach 導線を超える UX は別チケット。
- `ReadPodOutput` cursor の永続化。
## 関連
- `crates/session-store/`: 既存の session append-only backend。
- `crates/pod/src/runtime/dir.rs`: runtime dir の `history.json` / `spawned_pods.json`
- `crates/pod/src/spawn/registry.rs`: spawned children registry。現状は write-through のみで復元未実装。
- `tickets/pod-session-fork.md`: active session 切り替え設計との整合が必要。

View File

@ -0,0 +1,44 @@
# Pod state: session-store backend と FsStore 実装
## 背景
Pod 単位のランタイム状態は現状 `<runtime_dir>/{pod_name}/` 配下 (`status.json` / `history.json` / `spawned_pods.json`) に write-through されているのみで、Pod プロセスの寿命を超える復元ソースとして扱えない。
`session-grouping-introduce` で SessionSegment 群の groupingが導入されたあとは、Pod は **「どの Session の、どの leaf Segment が現在 active か」を指す軽量メタデータ**を持てば良い。会話本文は session log を唯一の正本とし、Pod state は references のみを保持する。
このチケットは Pod metadata の **backend trait と FsStore 実装の追加**だけに範囲を絞る。lifecycle hook の配線や CLI 導線は後続チケットで扱う。
## 要件
- Pod metadata trait を session-store crate に追加(`Store` 拡張 or 隣接 trait / module。実装時に決定
- 保持する内容:
- active `(SessionId, SegmentId)` 参照
- Pod 名key
- manifest / scope の snapshot 参照(既存 session log の `pod.scope` snapshot と責務を重複させない範囲で。最低限 latest segment への pointer のみで足りる可能性が高い)
- FsStore のデフォルト layout は `<data_dir>/pods/<pod_name>/` 配下に置く。`<runtime_dir>` は引き続き socket / pid / status など一時状態専用。
- write は session-store の他 write と同じ sync API で揃える。
- read は冪等で、Pod state が無ければ `None` を返すだけinitial Pod 起動時に作成される)。
- `--pod` resume の入口に必要な `read_by_name(pod_name)` API を提供する。
## 完了条件
- Pod metadata trait と FsStore 実装が `session-store` にあり、minimal CRUD が unit test で確認できる。
- `<data_dir>/pods/<pod_name>/` の layout が決まっている。
- ordered session history のような時系列 audit は本チケットには含めないwrite point 配線時に必要なら追加)。
- `cargo check --workspace` および `cargo test -p session-store` が通る。
## 範囲外
- Pod lifecycle の write point 配線(別チケット `pod-state-write-points`)。
- Pod 名単位 resume / attach の CLI 導線(別チケット `pod-name-resume`)。
- SpawnedPodRegistry の永続化(別チケット `spawned-registry-persist`)。
- DB backend 実装。
## 関連
- `tickets/session-grouping-introduce.md` (前提)
- `tickets/pod-state-write-points.md` (後続)
- `tickets/pod-name-resume.md` (後続)
- `tickets/spawned-registry-persist.md` (並行可)
- `crates/session-store/`
- `crates/pod/src/runtime/dir.rs`

View File

@ -0,0 +1,36 @@
# Pod state: lifecycle 各点での write 配線
## 背景
`pod-state-backend` で Pod metadata の永続 backend が用意された。本チケットでは Pod の lifecycle 各点で **active `(SessionId, SegmentId)` の更新を Pod state に write-through する** ことを実装する。
## 要件
- Pod state の write point を以下に配置:
- Pod 作成時: pod name と allocated `SessionId` を初期化。`SegmentId` は first `SessionStart` materialize で確定するので未確定 marker を許容する。
- first run で `SessionStart` が materialize された後: active `(SessionId, SegmentId)` を確定。
- compaction: 新 `SegmentId` に切り替わる (`SessionId` は据え置き)。
- fork (live auto / 過去): 新 `SegmentId` に切り替わる。
- resume: 起動時に Pod state から `(SessionId, SegmentId)` を解決し、session log を `restore_from_manifest` 相当の経路で復元する。
- session log の `SessionOrigin` を Pod state 側に重複保持しないこと。compaction / fork の構造的 lineage は session-store 側に正本。
- live writer の二重起動は既存の pod-registry / session lock と同等以上に防止するPod state にも lock 役割を持たせるかは実装時に判断、ただし pod-registry の責務を歪めない)。
## 完了条件
- 上記 write point で Pod state が更新され、Pod プロセスを再起動しても Pod 名から active session に復元できる。
- compaction / fork により active segment が変わった場合、Pod state と pod-registry の session id が整合する。
- 既存の `--session <UUID>` resume を壊さない。
- `cargo check --workspace` および `cargo test -p pod` が通る。
## 範囲外
- Pod 名単位 resume の CLI 引数導線(別チケット `pod-name-resume`)。
- spawned children の永続化(別チケット `spawned-registry-persist`)。
- ordered session history の auditPod state 側に持たせるか、session log だけで足りるかは本チケットで判断。**持つ必要が無いなら持たないこと**を優先する)。
## 関連
- `tickets/pod-state-backend.md` (前提)
- `tickets/pod-name-resume.md` (後続)
- `crates/pod/src/pod.rs`
- `crates/pod-registry/`

35
tickets/segment-rename.md Normal file
View File

@ -0,0 +1,35 @@
# session-store: SessionId → SegmentId へのリネーム
## 背景
永続化層の現 `SessionId` は、append-only log の物理的 / 復元上の単位を指しているcompaction や fork で新 ID が発行される)。一方ユーザー視点では「同じ会話の継続」が compaction / fork を跨いで成立しており、ここに**ユーザー視点の会話の家系 = `Session`** と **物理 append-only 単位 = `Segment`** の 2 階層を導入したい。
本チケットは 2 階層導入のうち、**先に物理単位側のリネームだけ**を機械的に済ませる。新 `SessionId`grouping 概念)は次チケットで導入する。
## 要件
- `crates/session-store/` 内の `SessionId` 型・関連関数 (`SessionLogWriter` / `Store::append` / `fork` / `fork_at` 等) を `SegmentId` に統一。
- 同様に `pod` / `pod-registry` / `pod-cli` / `llm-worker` 関連の呼び出し元の参照を `SegmentId` に追従。
- `SessionLogWriter` / `SessionStart` / `SessionOrigin` 等の `Session` プレフィックス型のうち、**segment レベルを指しているもの**は同時に `Segment` プレフィックスへ変える。
- 線引きの基準: そのデータが「fork / compaction で新規生成される log 1 本」に紐づくなら `Segment`。会話の家系全体に紐づく概念は何も無いはずなので(次チケットで初めて出てくる)、本チケット内では全て Segment 系に倒す。
- crate 名 (`session-store`) の rename 要否は本チケットでは扱わず保留。中身が Segment 中心になった事実のみで判断材料を残す。
- `--session <UUID>` 系 CLI 引数は内部実装としては `SegmentId` を受けるが、外部表記は変更しない(ユーザー向け ID 体系の整理は `session-grouping-introduce` で扱う)。
## 完了条件
- `SessionId` 型がコードベースに残らない(後続の Session grouping で導入する新 `SessionId` は無関係)。
- `cargo check --workspace` および全テストが通る。
- 既存 session の JSONL を Segment 中心の API で読み書きできる。
## 範囲外
- 新 `SessionId` (Segment 群の grouping) の導入(次チケット `session-grouping-introduce`)。
- session-store crate 名の rename。
- 外部公開 CLI 引数の rename。
## 関連
- `tickets/entry-hash-abolish.md` (前提)
- `tickets/session-grouping-introduce.md` (後続)
- `crates/session-store/`
- `crates/pod/src/pod.rs`

View File

@ -0,0 +1,48 @@
# session-store: SessionSegment 群の grouping導入
## 背景
`segment-rename` で物理 append-only 単位を `Segment` に揃えた。続けて、ユーザー視点の「同じ会話の家系」を表す **`Session`** を導入する。
`Session` は fork tree 全体を 1 つにまとめる grouping で、compaction / fork は同 Session 内に新 Segment を生やす操作になる。これにより:
- Session 数が fork で爆発せず、`WHERE session_id = ?` だけで fork tree 全体が取れる。
- resume の指定は `(SessionId, SegmentId)` の組で行える。
- pod-persistent-state 側で Pod ↔ Session の対応関係を扱いやすくなる。
## 要件
- 新 `SessionId` 型を `session-store` に追加。Segment は `parent_session_id` を持つ。
- Segment 生成パスでの session_id 決定:
- new session: 新 `SessionId` を発行
- compaction: 元 Segment と同 `SessionId` を継承
- fork (live auto / 過去 fork いずれも): 元 Segment と同 `SessionId` を継承
- `SessionOrigin` を以下のいずれかに整理:
- `compacted_from { segment_id, at_turn_index }`
- `forked_from { segment_id, at_turn_index }`
- どちらも同 Session 内 segment への参照であることを型レベルで保証。
- restore API を `(SessionId, SegmentId)` を取る形に。`SegmentId` のみを取る既存経路は内部で `SessionId` を解決する shim を一段噛ませる。
- FsStore layout に Session 単位の index を追加Session → Segment 列挙が `WHERE session_id = ?` 相当で引けること)。形式は `<data_dir>/sessions/<session_id>/<segment_id>.jsonl` または別ファイル index、実装時に決定。
- Session 内の leaf Segment 列挙 API を提供pod-name resume 等から使う)。
## 完了条件
- `SessionId` 型と Session 単位 metadata の永続表現が決まり、FsStore で読み書きできる。
- compaction / fork が同 Session 内 Segment 増分として記録される。
- `(SessionId, SegmentId)` での restore が動作し、leaf 以外を指定した read-only restore も実装上は可能。
- 既存 session を Session 単位に grouping する migration 戦略が決まっている(プロジェクト方針として後方互換は作らないため、既存 sessions ディレクトリは破棄して良いかどうかをここで明示)。
- `cargo check --workspace` および全テストが通る。
## 範囲外
- live auto-fork の marker 形式(別チケット `live-fork-marker`)。
- Pod 単位 metadataPhase 2 一連のチケット)。
- TUI からの Session/Segment 選択 UI。
- DB backend 実装。
## 関連
- `tickets/segment-rename.md` (前提)
- `tickets/live-fork-marker.md`
- `tickets/pod-state-backend.md`
- `crates/session-store/`

View File

@ -0,0 +1,37 @@
# Pod state: SpawnedPodRegistry の永続化と復元
## 背景
`SpawnedPodRegistry` (`crates/pod/src/spawn/registry.rs`) は spawner の子 Pod 一覧を保持しているが、現状 `<runtime_dir>/{pod_name}/spawned_pods.json` への write-through のみで、再起動した spawner が子 Pod 一覧を rebuild する経路が無い(コメントに future work と明記)。
`pod-state-backend` で Pod metadata の永続 backend が用意されたあと、本チケットで spawned children registry も同じ永続層に乗せる。
## 要件
- spawned children registry の永続化:
- 各 child について最低限: pod name, socket path, delegated scope, callback address。child の session id を含めるかは実装時判断Pod 名から `pod-name-resume` で引けるなら冗長になる)。
- 書き込みタイミング: `SpawnPod` / callback 受領 / `StopPod` の各点。runtime dir への write-through と同期して永続層にも書く。
- 読み込みタイミング: spawner Pod の起動時に Pod state から initial load。
- 復元後の spawner で `ListPods` / `SendToPod` / `StopPod` が機能すること。
- `ReadPodOutput` の read cursor は **永続化対象外**session-lifetime / in-memory のままで良い)。
- 到達不能になっている childsocket が消えている等)は registry から除外しつつ、最低限のログを残す。
- `pod-state-backend` で追加した backend trait を再利用し、専用層を増やさない(同じ Pod state の一部として持つか、隣接 entry として持つかは実装時判断)。
## 完了条件
- spawner Pod を再起動した後、永続化された child 一覧から `ListPods` が復元される。
- 復元された child に対して `SendToPod` / `StopPod` が到達可能なものに限って成功する。
- `cargo check --workspace` および `cargo test -p pod` が通る。
- runtime dir の `spawned_pods.json` は引き続き存在しても良いが、永続正本ではない(消えても永続層から復元できる)ことを test で確認。
## 範囲外
- `ReadPodOutput` cursor の永続化。
- 高度な child Pod 監視 / 自動再起動。
- TUI 上の Pod ツリー UI。
## 関連
- `tickets/pod-state-backend.md` (前提)
- `crates/pod/src/spawn/registry.rs`
- `crates/pod/src/runtime/dir.rs`