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:
parent
be5e413b55
commit
35c15923db
12
TODO.md
12
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)
|
||||
|
|
|
|||
42
tickets/entry-hash-abolish.md
Normal file
42
tickets/entry-hash-abolish.md
Normal 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` (後続)
|
||||
40
tickets/live-fork-marker.md
Normal file
40
tickets/live-fork-marker.md
Normal file
|
|
@ -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`
|
||||
|
|
@ -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<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`
|
||||
37
tickets/pod-name-resume.md
Normal file
37
tickets/pod-name-resume.md
Normal 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/`
|
||||
|
|
@ -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 の 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 <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 切り替え設計との整合が必要。
|
||||
44
tickets/pod-state-backend.md
Normal file
44
tickets/pod-state-backend.md
Normal 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` で 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 は `<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`
|
||||
36
tickets/pod-state-write-points.md
Normal file
36
tickets/pod-state-write-points.md
Normal 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 の audit(Pod 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
35
tickets/segment-rename.md
Normal 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`
|
||||
48
tickets/session-grouping-introduce.md
Normal file
48
tickets/session-grouping-introduce.md
Normal file
|
|
@ -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 = ?` 相当で引けること)。形式は `<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 単位 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/`
|
||||
37
tickets/spawned-registry-persist.md
Normal file
37
tickets/spawned-registry-persist.md
Normal 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 のままで良い)。
|
||||
- 到達不能になっている 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`
|
||||
Loading…
Reference in New Issue
Block a user