From 35c15923db63a11fc155750f08b53c9733f661e3 Mon Sep 17 00:00:00 2001 From: Hare Date: Wed, 20 May 2026 04:07:44 +0900 Subject: [PATCH] =?UTF-8?q?ticket:=20=E6=B0=B8=E7=B6=9A=E5=8C=96=E6=95=B4?= =?UTF-8?q?=E7=90=86=E3=82=92=208=20=E5=80=8B=E3=81=AB=E5=88=86=E5=89=B2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 に切り出した。 --- TODO.md | 12 +- tickets/entry-hash-abolish.md | 42 ++++++ tickets/live-fork-marker.md | 40 ++++++ tickets/persistence-semantics.md | 188 -------------------------- tickets/pod-name-resume.md | 37 +++++ tickets/pod-persistent-state.md | 84 ------------ tickets/pod-state-backend.md | 44 ++++++ tickets/pod-state-write-points.md | 36 +++++ tickets/segment-rename.md | 35 +++++ tickets/session-grouping-introduce.md | 48 +++++++ tickets/spawned-registry-persist.md | 37 +++++ 11 files changed, 329 insertions(+), 274 deletions(-) create mode 100644 tickets/entry-hash-abolish.md create mode 100644 tickets/live-fork-marker.md delete mode 100644 tickets/persistence-semantics.md create mode 100644 tickets/pod-name-resume.md delete mode 100644 tickets/pod-persistent-state.md create mode 100644 tickets/pod-state-backend.md create mode 100644 tickets/pod-state-write-points.md create mode 100644 tickets/segment-rename.md create mode 100644 tickets/session-grouping-introduce.md create mode 100644 tickets/spawned-registry-persist.md diff --git a/TODO.md b/TODO.md index a7cd0bfa..6388aff1 100644 --- a/TODO.md +++ b/TODO.md @@ -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) diff --git a/tickets/entry-hash-abolish.md b/tickets/entry-hash-abolish.md new file mode 100644 index 00000000..e4085928 --- /dev/null +++ b/tickets/entry-hash-abolish.md @@ -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` (後続) diff --git a/tickets/live-fork-marker.md b/tickets/live-fork-marker.md new file mode 100644 index 00000000..9796132e --- /dev/null +++ b/tickets/live-fork-marker.md @@ -0,0 +1,40 @@ +# session-store: live auto-fork の marker 形式確定 + +## 背景 + +`entry-hash-abolish` で `ensure_head_or_fork` は末尾 seq 比較ベースに置換されたが、最小実装で繋いだだけで marker 形式の本決定は保留している。 + +live auto-fork(concurrent writer 検知)と過去 fork(UI から 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` diff --git a/tickets/persistence-semantics.md b/tickets/persistence-semantics.md deleted file mode 100644 index 5e0ab325..00000000 --- a/tickets/persistence-semantics.md +++ /dev/null @@ -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 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 比較に置換(形式は実装時に決める)。 - -### 廃止前の足場 (前提) - -本セクションを実装に移すタイミングでは、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` は単に `Arc + 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` を、新 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` diff --git a/tickets/pod-name-resume.md b/tickets/pod-name-resume.md new file mode 100644 index 00000000..ec36f602 --- /dev/null +++ b/tickets/pod-name-resume.md @@ -0,0 +1,37 @@ +# Pod state: Pod 名単位の resume / attach 導線 + +## 背景 + +`pod-state-write-points` で Pod state が active session を保持するようになる。本チケットでは pod-cli / TUI 側から **Pod 名で resume / attach できる入口**を確定し実装する。 + +既存の `--session ` resume は引き続き使えること。 + +## 要件 + +- pod-cli の引数仕様を確定: + - 例: `pod --pod ` で 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 ` 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/` diff --git a/tickets/pod-persistent-state.md b/tickets/pod-persistent-state.md deleted file mode 100644 index ccf7f895..00000000 --- a/tickets/pod-persistent-state.md +++ /dev/null @@ -1,84 +0,0 @@ -# Pod: セッションログをバックエンドにした Pod 単位の永続化 - -## 背景 - -現在の永続化の主軸は session-store の append-only JSONL ログで、`SessionId` 単位に会話履歴・設定・scope snapshot・usage・拡張 payload を復元できる。一方で Pod 単位のランタイム状態は `/{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 は `/pods/` 配下など、`sessions/` と同じ data_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 の registry(pod 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 ` resume は壊さない。 -- 新しい Pod 名単位 resume / attach の入口を決めること。 - - 例: `pod --pod-state ` ではなく、既存 `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 切り替え設計との整合が必要。 diff --git a/tickets/pod-state-backend.md b/tickets/pod-state-backend.md new file mode 100644 index 00000000..aa3adb86 --- /dev/null +++ b/tickets/pod-state-backend.md @@ -0,0 +1,44 @@ +# Pod state: session-store backend と FsStore 実装 + +## 背景 + +Pod 単位のランタイム状態は現状 `/{pod_name}/` 配下 (`status.json` / `history.json` / `spawned_pods.json`) に write-through されているのみで、Pod プロセスの寿命を超える復元ソースとして扱えない。 + +`session-grouping-introduce` で Session(Segment 群の 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 は `/pods//` 配下に置く。`` は引き続き 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 で確認できる。 +- `/pods//` の 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` diff --git a/tickets/pod-state-write-points.md b/tickets/pod-state-write-points.md new file mode 100644 index 00000000..2a87a88c --- /dev/null +++ b/tickets/pod-state-write-points.md @@ -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 ` resume を壊さない。 +- `cargo check --workspace` および `cargo test -p pod` が通る。 + +## 範囲外 + +- Pod 名単位 resume の CLI 引数導線(別チケット `pod-name-resume`)。 +- spawned children の永続化(別チケット `spawned-registry-persist`)。 +- ordered session history の audit(Pod state 側に持たせるか、session log だけで足りるかは本チケットで判断。**持つ必要が無いなら持たないこと**を優先する)。 + +## 関連 + +- `tickets/pod-state-backend.md` (前提) +- `tickets/pod-name-resume.md` (後続) +- `crates/pod/src/pod.rs` +- `crates/pod-registry/` diff --git a/tickets/segment-rename.md b/tickets/segment-rename.md new file mode 100644 index 00000000..85afb1be --- /dev/null +++ b/tickets/segment-rename.md @@ -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 ` 系 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` diff --git a/tickets/session-grouping-introduce.md b/tickets/session-grouping-introduce.md new file mode 100644 index 00000000..76d0dbb7 --- /dev/null +++ b/tickets/session-grouping-introduce.md @@ -0,0 +1,48 @@ +# session-store: Session(Segment 群の 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 = ?` 相当で引けること)。形式は `/sessions//.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 単位 metadata(Phase 2 一連のチケット)。 +- TUI からの Session/Segment 選択 UI。 +- DB backend 実装。 + +## 関連 + +- `tickets/segment-rename.md` (前提) +- `tickets/live-fork-marker.md` +- `tickets/pod-state-backend.md` +- `crates/session-store/` diff --git a/tickets/spawned-registry-persist.md b/tickets/spawned-registry-persist.md new file mode 100644 index 00000000..355cd515 --- /dev/null +++ b/tickets/spawned-registry-persist.md @@ -0,0 +1,37 @@ +# Pod state: SpawnedPodRegistry の永続化と復元 + +## 背景 + +`SpawnedPodRegistry` (`crates/pod/src/spawn/registry.rs`) は spawner の子 Pod 一覧を保持しているが、現状 `/{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 のままで良い)。 + - 到達不能になっている child(socket が消えている等)は 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`