diff --git a/TODO.md b/TODO.md index 9df94035..538e7625 100644 --- a/TODO.md +++ b/TODO.md @@ -14,6 +14,7 @@ - [ ] ユーザーマニフェストのモデル設定 wizard → [tickets/tui-user-model-setup.md](tickets/tui-user-model-setup.md) - [ ] サブミット入力 - [ ] TUI 補完 + 型付き atom 化 → [tickets/submit-tui-completion.md](tickets/submit-tui-completion.md) + - [ ] セッションログの Item 依存切り離し → [tickets/session-log-decouple-item.md](tickets/session-log-decouple-item.md) - [ ] セッションログの Segment 保持 → [tickets/session-log-segments.md](tickets/session-log-segments.md) - [ ] メモリ機構 - [ ] Phase 2 consolidation → [tickets/memory-phase2-consolidation.md](tickets/memory-phase2-consolidation.md) diff --git a/tickets/session-log-decouple-item.md b/tickets/session-log-decouple-item.md new file mode 100644 index 00000000..cbf9e967 --- /dev/null +++ b/tickets/session-log-decouple-item.md @@ -0,0 +1,82 @@ +# Session log の Item 依存を切り離す + +## 背景 + +`crates/session-store` の `LogEntry` は、history 構成パートをすべて llm-worker の `Item` enum でそのままシリアライズしている: + +- `UserInput.item: Item` +- `AssistantItems.items: Vec` +- `ToolResults.items: Vec` +- `HookInjectedItems.items: Vec` +- `SessionStart.history: Vec` + +これは worker 内部型と永続フォーマットを結合しているため、`Item` / `ContentPart` / `Reasoning` のフィールド追加・名称変更が即ログ非互換になる。永続データは llm-worker の進化と独立に安定したスキーマを持つべき。 + +`tickets/session-log-segments.md`(user message を `Vec` で残す)の前提として、まず session-store が自分のスキーマを持つように剥がす。 + +後方互換は持たない(既存 jsonl は捨てる)。新スキーマで一新する。 + +## 方針 + +### session-store に独自の logged 型を置く + +llm-worker の `Item` をそのまま流用せず、session-store 内に永続用の型を切り出す: + +```rust +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum LoggedItem { + Message { role: LoggedRole, content: Vec, .. }, + ToolCall { call_id, name, arguments, .. }, + ToolResult { call_id, summary, content, .. }, + Reasoning { text, summary, encrypted_content, .. }, +} +``` + +具体形は実装で決める。要件: + +- replay に必要な field のみ持つ(`ItemId` や `ItemStatus` のように LLM 送信 / round-trip に効かないものは原則持たない。ただし ZDR 用 `encrypted_content` のような stateless 再送に必要なものは保持する) +- worker `Item` のフィールド追加でログ非互換を起こさない +- snake_case JSON、既存 LogEntry の他フィールドと整合 + +### 変換器 + +- `From<&Item> for LoggedItem`(worker → logged、save 経路) +- `LoggedItem::into_item()`(logged → worker、replay 経路) +- session-store crate 内の責務。worker / pod は変換を意識しない + +worker → logged の変換で落ちる field が出るのは構わない(永続化に不要なら捨てる)が、replay → 再 save で wire-equivalent な Item が再生される構造にする。 + +### LogEntry の差し替え + +- `AssistantItems.items` / `ToolResults.items` / `HookInjectedItems.items` / `SessionStart.history` を `Vec` に置換 +- `collect_state` は logged → Item 変換を通して `RestoredState.history` を組む +- `save_delta` は Item → logged 変換を通して書き込む + +`UserInput.item` は触らない。直後の `session-log-segments` で segments に置き換わるため、ここで logged 化しても 1 ステップで再変更になる。 + +### ログの hash chain + +`compute_hash` は `LogEntry` を JSON シリアライズして SHA-256 を取る。スキーマが変わるのでハッシュ値は別物になる。新規セッションから新スキーマで書き始める前提(既存ログを読まないので問題ない)。 + +## 範囲外 + +- 後方互換(既存 jsonl ログの読み込み) +- `UserInput.item` の差し替え(`session-log-segments` で対応) +- ログフォーマットのバージョニング機構 — 必要になったら追加 +- llm-worker の `Item` 構造変更 +- compaction / fork / restore 経路自体の再設計 + +## 完了条件 + +- session-store crate が `LoggedItem` 系の独自型を export し、`Item` への依存が UserInput を除いて消える +- `collect_state` で組まれた `RestoredState.history` が従来と同じ Item 列を返す(既存の worker / pod テストが通る) +- `save_delta` の外側 API は変えず、内部で Item → LoggedItem 変換を通す +- session-store の単体テストを新スキーマに合わせて書き換え、すべて合格 +- round-trip テストを 1 本追加: `Item → LoggedItem → JSON → LoggedItem → Item` で意味的に等価 +- 既存のビルド・全テストが新スキーマで合格 + +## 参照 + +- 後続: `tickets/session-log-segments.md` +- 影響範囲: `crates/session-store/src/session_log.rs`, `crates/session-store/src/session.rs`, `crates/session-store/tests/*` +- 不変: `crates/llm-worker/src/llm_client/types.rs`(`Item` / `ContentPart` 等)、`crates/pod`(`save_delta` の呼び出し側) diff --git a/tickets/session-log-segments.md b/tickets/session-log-segments.md index 1f0e9c11..8a051943 100644 --- a/tickets/session-log-segments.md +++ b/tickets/session-log-segments.md @@ -8,13 +8,26 @@ LLM 側の入力は flatten 済み文字列のままで良い(worker / llm-client 層は変更しない)。永続化と client 復元経路にだけ segment を残す。 +## 前提チケット + +- `tickets/session-log-decouple-item.md` — session-store が llm-worker `Item` から独立した永続スキーマを持つようにする。本チケットはその上で `UserInput` を `Item` から `Vec` に置き換える。 + ## 要件 -- セッションログに user message の元 `Vec` を残す。worker の `Item` 表現や LLM 送信 payload は変更しない(`flatten_segments` の結果を引き続き食わせる)。 +- セッションログに user message を `Vec` として残す。worker の `Item` 表現や LLM 送信 payload は変更しない(`flatten_segments` の結果を引き続き食わせる)。 +- `LogEntry::UserInput` から `item: Item` を取り除き、`segments: Vec` のみ持つ形に置き換える。replay 時には `flatten_segments` で 1 文字列にして `Item::user_message(text)` を派生させ worker history に積む。 - 復元経路で client が segments を取り戻せるようにする。最低限、TUI の `restore_history` が paste segment を典型の magenta `[Clipboard #N | ...]` チップとして再構築できること。 -- forward compat: 旧フォーマット(segments を持たないログ行)を読んでも panic / parse error にならず、従来通り text 1 個の segment として復元できること。 +- 後方互換は持たない。既存 jsonl ログは捨てる前提。 - 現行の compaction / fork / restore のフローを壊さない。 +### Pod と save_delta の責務分割 + +`save_delta` は worker history の差分を分類して `LogEntry::UserInput` を書いていたが、segments は worker history に存在しない。Pod 側で `Method::Run` 入口直後(`flatten_segments` 直前)に segments で `LogEntry::UserInput` を直接書き、`save_delta` からは user_message 分類を外す(assistant / tool_result / hook_injected のみ扱う)。 + +### Event::History への segments 載せ方 + +Pod が吐く `Event::History.items: Vec` の user message オブジェクトに `segments` フィールドを embed する(既存の混合 JSON 表現を活かす)。TUI 側 `restore_history` は user 分岐で `segments` を読み出して `Block::UserMessage { segments }` を組む。 + ## 範囲外 - paste prune(segment メタデータを使って context から落とす話)。別チケットで扱う。 @@ -23,15 +36,17 @@ LLM 側の入力は flatten 済み文字列のままで良い(worker / llm-cli ## 完了条件 -- セッションログに segment 情報が persist される(新規セッションから書かれる行は segments を含む)。 +- セッションログに segment 情報が persist される(`LogEntry::UserInput` が `{ ts, segments }` 形)。 - TUI で paste を含むメッセージを送信 → セッションを開き直す → magenta チップが復元される。 -- segments を持たない旧ログ行も正常に restore でき、テキスト 1 segment として表示される。 -- 既存ビルド・テストを壊さない。新フォーマットに対する round-trip テスト 1 本は追加する。 +- 既存ビルド・テストが新スキーマで合格。 +- segments → flatten → Item の派生経路を round-trip テストで verify する。 ## 参照 -- `tickets/submit-segment-protocol.md`(前提) +- 前提: `tickets/session-log-decouple-item.md` - `crates/session-store/src/session_log.rs`(`LogEntry::UserInput`) -- `crates/session-store/src/session.rs`(`restore`, `RestoredState`) +- `crates/session-store/src/session.rs`(`save_delta`, `restore`, `RestoredState`) +- `crates/pod/src/pod.rs`(`run`, `flatten_segments`, `persist_turn`) +- `crates/pod/src/controller.rs`(`Event::UserMessage` broadcast 経路) - `crates/tui/src/app.rs`(`restore_history` — 現状 segment を捨てている地点) -- `crates/protocol/src/lib.rs`(`Segment`) +- `crates/protocol/src/lib.rs`(`Segment`, `Event::History`)