session-log関連完了
This commit is contained in:
parent
09d56272d8
commit
426d477584
2
TODO.md
2
TODO.md
|
|
@ -14,8 +14,6 @@
|
|||
- [ ] ユーザーマニフェストのモデル設定 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)
|
||||
- [ ] 使用頻度メトリクス + Knowledge 化候補レポート → [tickets/memory-usage-metrics.md](tickets/memory-usage-metrics.md)
|
||||
|
|
|
|||
|
|
@ -1,88 +0,0 @@
|
|||
# Session log の Item 依存を切り離す
|
||||
|
||||
## 背景
|
||||
|
||||
`crates/session-store` の `LogEntry` は、history 構成パートをすべて llm-worker の `Item` enum でそのままシリアライズしている:
|
||||
|
||||
- `UserInput.item: Item`
|
||||
- `AssistantItems.items: Vec<Item>`
|
||||
- `ToolResults.items: Vec<Item>`
|
||||
- `HookInjectedItems.items: Vec<Item>`
|
||||
- `SessionStart.history: Vec<Item>`
|
||||
|
||||
これは worker 内部型と永続フォーマットを結合しているため、`Item` / `ContentPart` / `Reasoning` のフィールド追加・名称変更が即ログ非互換になる。永続データは llm-worker の進化と独立に安定したスキーマを持つべき。
|
||||
|
||||
`tickets/session-log-segments.md`(user message を `Vec<Segment>` で残す)の前提として、まず 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<LoggedContentPart>, .. },
|
||||
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<LoggedItem>` に置換
|
||||
- `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` の呼び出し側)
|
||||
|
||||
## Review
|
||||
|
||||
- 状態: Approve
|
||||
- レビュー詳細: [./session-log-decouple-item.review.md](./session-log-decouple-item.review.md)
|
||||
- 日付: 2026-04-29
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
# Review: Session log の Item 依存を切り離す
|
||||
|
||||
## 前提・要件の確認
|
||||
|
||||
- **session-store crate が `LoggedItem` 系の独自型を export し、`Item` への依存が UserInput を除いて消える**
|
||||
満たされている。`crates/session-store/src/logged_item.rs` に `LoggedItem` / `LoggedRole` / `LoggedContentPart` を新設し、`crates/session-store/src/lib.rs:31-43` で公開。`LogEntry` の `AssistantItems` / `ToolResults` / `HookInjectedItems` / `SessionStart.history` は `Vec<LoggedItem>` に置換済み(`session_log.rs:106,123,126,129`)。`UserInput` のみ後続チケットの責務として `Item` 参照が残るが、本チケットでは触らない方針通り。
|
||||
- **`collect_state` で組まれた `RestoredState.history` が従来と同じ Item 列を返す**
|
||||
満たされている。`session_log.rs:252,262,267,272` で `Item::from(LoggedItem)` を介して再構築。既存の worker / pod テスト(`cargo test --workspace` 全合格)と置換テスト群で確認済み。
|
||||
- **`save_delta` の外側 API は変えず、内部で Item → LoggedItem 変換を通す**
|
||||
満たされている。`session.rs:178` のシグネチャ `&[Item]` は不変、`to_logged()` 経由で永続化(`session.rs:202,221,232`)。
|
||||
- **session-store の単体テストを新スキーマに合わせて書き換え**
|
||||
満たされている。`fs_store_test.rs` / `session_test.rs` を `.into()` 経由に更新。
|
||||
- **Round-trip テスト 1 本追加**
|
||||
超過達成。`logged_item.rs` 内に 5 本の round-trip テスト(user_message / tool_call / reasoning+ZDR / tool_result_with_content / kind タグ確認)を追加。
|
||||
- **既存ビルド・全テストが新スキーマで合格**
|
||||
満たされている。`cargo test --workspace` 全合格。
|
||||
|
||||
## アーキテクチャ・スコープ
|
||||
|
||||
- 永続スキーマと worker 内部型の分離を logged_item モジュールで完結させている。From/Into 変換は session-store の責務として閉じており、pod / worker から見れば save_delta の API は不変。レイヤ違反なし。
|
||||
- LLM ZDR の制約(Reasoning::encrypted_content の保持)が頭注(`logged_item.rs:11-13`)に明記され、テスト(`round_trip_reasoning_preserves_encrypted_content`)でも担保されている。「replay に必要な field のみ持つ」という方針判断の妥当性を担保する記述として適切。
|
||||
- `id: ItemId` / `status: ItemStatus` を意図的に落とし、replay 時に `None` で synthesize する方針はチケット記載と一致。output-side metadata と replay-side metadata の分離が綺麗に効いている。
|
||||
- `LogEntry::Extension` などの既存ログ拡張点に手を入れていない点も適切(スコープ厳守)。
|
||||
- session-store の `lib.rs` で `llm_worker::{Item, ContentPart, Role}` を再 export しているが、これは `RestoredState.history: Vec<Item>` を消費する側のために残している既存挙動の維持で、ticket の趣旨(`Item` 依存を「ログスキーマから」剥がす)には反しない。
|
||||
|
||||
## 指摘事項
|
||||
|
||||
### Non-blocking / Follow-up
|
||||
|
||||
- `lib.rs:45` の `pub use llm_worker::llm_client::types::{ContentPart, Item, Role};` は外部利用箇所が見当たらない(`grep` 結果)。dead 再エクスポートは将来のクリーンアップ余地としてメモ。本チケットの範疇では削除不要。
|
||||
|
||||
### Nits
|
||||
|
||||
- `from_logged` は `pub` で公開されているが、現状 `collect_state` 経由の `iter().cloned().map(Into::into)` パターンしか使われていない。slice 版が必要になるまで public API として残す価値があるかは要観察。
|
||||
|
||||
## 判断
|
||||
|
||||
**Approve** — チケットの完了条件はすべて満たされ、round-trip テストも要件以上に厚く配置されている。スキーマ分離の方針通り Item 依存は UserInput を除いて消えた。後続 `session-log-segments` の前提が綺麗に整っている。
|
||||
|
|
@ -1,58 +0,0 @@
|
|||
# セッションログの Segment 保持
|
||||
|
||||
## 背景
|
||||
|
||||
`submit-segment-protocol` で `Method::Run` / `Event::UserMessage` は `Vec<Segment>` を運ぶようになり、TUI では paste を `[Clipboard #N | X chars, Y lines]` のラベルチップとして再描画できる。一方でセッション復元経路は worker の history(`Item::user_message(text)`)からしか情報を拾えず、`crates/tui/src/app.rs` の `restore_history` は user message を `vec![Segment::text(text)]` 1 個に潰して復元している。
|
||||
|
||||
つまり「ライブで届いた `Event::UserMessage` では paste チップが見える / 同じセッションを開き直すと潰れたテキストになる」という非対称が残っている。今後 paste 部分を context から prune する話(後続チケット)にも segment 単位の永続化が前提になるため、ここで土台を入れたい。
|
||||
|
||||
LLM 側の入力は flatten 済み文字列のままで良い(worker / llm-client 層は変更しない)。永続化と client 復元経路にだけ segment を残す。
|
||||
|
||||
## 前提チケット
|
||||
|
||||
- `tickets/session-log-decouple-item.md` — session-store が llm-worker `Item` から独立した永続スキーマを持つようにする。本チケットはその上で `UserInput` を `Item` から `Vec<Segment>` に置き換える。
|
||||
|
||||
## 要件
|
||||
|
||||
- セッションログに user message を `Vec<Segment>` として残す。worker の `Item` 表現や LLM 送信 payload は変更しない(`flatten_segments` の結果を引き続き食わせる)。
|
||||
- `LogEntry::UserInput` から `item: Item` を取り除き、`segments: Vec<Segment>` のみ持つ形に置き換える。replay 時には `flatten_segments` で 1 文字列にして `Item::user_message(text)` を派生させ worker history に積む。
|
||||
- 復元経路で client が segments を取り戻せるようにする。最低限、TUI の `restore_history` が paste segment を典型の magenta `[Clipboard #N | ...]` チップとして再構築できること。
|
||||
- 後方互換は持たない。既存 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<serde_json::Value>` の user message オブジェクトに `segments` フィールドを embed する(既存の混合 JSON 表現を活かす)。TUI 側 `restore_history` は user 分岐で `segments` を読み出して `Block::UserMessage { segments }` を組む。
|
||||
|
||||
## 範囲外
|
||||
|
||||
- paste prune(segment メタデータを使って context から落とす話)。別チケットで扱う。
|
||||
- FileRef / KnowledgeRef / WorkflowInvoke の resolve 結果の永続化。これらは resolver 実装チケット側の責務。
|
||||
- worker / llm-client 層の API 変更。
|
||||
|
||||
## 完了条件
|
||||
|
||||
- セッションログに segment 情報が persist される(`LogEntry::UserInput` が `{ ts, segments }` 形)。
|
||||
- TUI で paste を含むメッセージを送信 → セッションを開き直す → magenta チップが復元される。
|
||||
- 既存ビルド・テストが新スキーマで合格。
|
||||
- segments → flatten → Item の派生経路を round-trip テストで verify する。
|
||||
|
||||
## 参照
|
||||
|
||||
- 前提: `tickets/session-log-decouple-item.md`
|
||||
- `crates/session-store/src/session_log.rs`(`LogEntry::UserInput`)
|
||||
- `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`, `Event::History`)
|
||||
|
||||
## Review
|
||||
|
||||
- 状態: Request changes
|
||||
- レビュー詳細: [./session-log-segments.review.md](./session-log-segments.review.md)
|
||||
- 日付: 2026-04-29
|
||||
|
|
@ -1,54 +0,0 @@
|
|||
# Review: セッションログの Segment 保持
|
||||
|
||||
## 前提・要件の確認
|
||||
|
||||
- **`LogEntry::UserInput` から `item: Item` を取り除き `segments: Vec<Segment>` のみ持つ**
|
||||
満たされている。`session_log.rs:120` で置換済み。`collect_state` は `Segment::flatten_to_text` 経由で `Item::user_message(text)` を派生(`session_log.rs:254-258`)。
|
||||
- **セッションログに Segment を persist し、`Pod` の入口で submit-time に直書き、`save_delta` は user_message を skip**
|
||||
満たされている。`session.rs:148-164` に `save_user_input` 関数追加。`session.rs:188-190` で `is_user_message()` を skip。`pod.rs:608-615` で `Pod::run` の入口(flatten 直前)に `save_user_input` を埋め込み、in-memory `user_segments` に push。
|
||||
- **復元経路で client が segments を取り戻す(TUI で paste チップが復元)**
|
||||
実装済み。`ipc/server.rs:91-127` で `Method::GetHistory` 応答時に worker history JSON の user_message に `segments` フィールドを embed、`tui/src/app.rs:461-473` の `restore_history` で `segments` を読み出して `Block::UserMessage` を組み立てる。手動 UI 確認は未消化(report 申告通り)。
|
||||
- **worker / llm-client 層は変更しない**
|
||||
守られている。`Segment::flatten_to_text` を `protocol` に追加した以外、worker / llm-client API に変更なし。
|
||||
- **後方互換は持たない**
|
||||
既存 jsonl の読み込みフォールバックは入れていない(適切)。
|
||||
- **Round-trip テスト**
|
||||
`replay_user_input_segments_round_trip` 追加(`session_log.rs:682-751`)。Text/Paste/FileRef を含む混合 segments の JSON 往復+`flatten_to_text` 派生+`user_segments` 保持を一気に検証。テスト粒度は要件十分。
|
||||
- **既存ビルド・テストが新スキーマで合格**
|
||||
`cargo test --workspace` 全合格を確認。
|
||||
|
||||
## アーキテクチャ・スコープ
|
||||
|
||||
- `Segment::flatten_to_text` を `protocol` 側に純粋関数として抜き、Pod 側の `flatten_segments` を「アラート発火 + flatten_to_text 呼び出し」へ整理した分離(`pod.rs:636-682`)は正しい。replay 経路がアラートを再発火させない設計が両立している。
|
||||
- session-store が `protocol::Segment` に依存する構造は妥当。`Cargo.toml` には `cargo add` で追加されており(コミットの差分通り)、依存方向(session-store → protocol)も既存の依存グラフ的に問題ない。
|
||||
- `RestoredState.user_segments` を別フィールドとして並走させ、replay 経路では Item と Segment を二重管理する設計は、worker history の責務(LLM への渡し物)と client 復元の責務(typed atom 描画)の分離として理にかなっている。
|
||||
- IPC server で `is_user_message()` 数と `user_segments` 数の差分から「seed history は最初に詰まれる」という性質を使った右寄せ整列(`ipc/server.rs:101-126`)は、ticket 記載の前提("seed history は segments を持たない、live submission のみ持つ")の素直な実装。
|
||||
|
||||
## 指摘事項
|
||||
|
||||
### Blocking
|
||||
|
||||
- **`Pod::compact()` 後に `self.user_segments` がクリアされない**(`pod.rs:1165-1224`)
|
||||
`compact()` は worker history を `[summary, ...retained_items]` に置換するが、`self.user_segments` は触られていない。プロセス継続中に compaction が走った場合、次の `Method::GetHistory` で `ipc/server.rs:103` の `total_user_msgs.saturating_sub(segments_per_user.len())` が想定外の値になり、segments と worker user_message が錯誤マッチする。
|
||||
例: 5 user_msg を投げた後 compaction で 1 だけ retained のとき、`total=1, segs=5 → skip=0` となり、retained の user_msg に 1 番目の古い segments が貼られる。
|
||||
対策案: `compact()` 内で `retained_items` 中の `is_user_message()` 件数 K を数え、`self.user_segments = self.user_segments.split_off(self.user_segments.len() - K)` 相当に切り詰める(K=0 なら clear)。controller 側 `shared_state` も同期するため、compact 後に `shared_state.set_user_segments(pod.user_segments().to_vec())` を呼び直す経路が要る。
|
||||
なお post-compaction 後にプロセスを再起動して restore する場合は、新セッションの jsonl に UserInput が無いので `state.user_segments` は空になり、自動的に整列が直る。問題は**プロセス継続中の compaction → 次の GetHistory** の窓に限定されるが、TUI の通常運用フローで踏みうる。
|
||||
|
||||
### Non-blocking / Follow-up
|
||||
|
||||
- **`save_user_input` 失敗時の shared_state ghost segment**(`controller.rs:331` と `pod.rs:608-615` の順序)
|
||||
Controller は `Method::Run` 受信直後に `shared_state.push_user_segments(input.clone())` してから run_future を await する。`save_user_input` が I/O エラーで失敗すると、`pod.user_segments` には push されないが `shared_state.user_segments` には残り、worker.history にも user_msg は積まれない(`save_user_input` の失敗で `pod.run` が早期 return するため)。次回 GetHistory で 1 段ずれる可能性。実害は store I/O 障害のレアケースに限定されるが、`run_future` 完了後に `shared_state.set_user_segments(pod.user_segments().to_vec())` で同期し直すのが安全。
|
||||
- **seed history の segments 喪失**(`pod.rs:294-295`, `session_log.rs:217-223` のコメント参照)
|
||||
ticket の範囲外として明示済み・コメントでも記述されている設計判断。compaction を境に paste チップの典型 magenta 復元が単純テキストにフォールバックする挙動はユーザー視点で受容可能だが、UX として把握しておく価値あり。後続で必要なら `SessionStart.history` 側にも segments を持たせる拡張余地あり。
|
||||
- **chain hash の確定性**
|
||||
`Segment` の serde 実装は HashMap 不使用・field 順序固定で、`session_log.rs:466-499` の `hash_chain_is_deterministic` / `different_content_produces_different_hash` で確認済み。スキーマ変更で hash 値は別物になるが、新スキーマ内での安定性は壊れていない(要件通り)。
|
||||
- 手動 UI 確認(paste chip の magenta 復元)の自動テスト化は未消化。session-store + flatten 経路は単体テストでカバーされているが、TUI の `restore_history` 経路は serde_json::Value からの復元アサーションが望ましい follow-up。
|
||||
|
||||
### Nits
|
||||
|
||||
- `pod.rs:615` の `self.user_segments.push(input.clone())` は直前で `save_user_input` に `input.clone()` を渡しているため `input` を 2 回 clone している。`Vec<Segment>` の clone はそこそこ重い(特に Paste の content)ので、`save_user_input(... segments)` の引数を所有 `Vec<Segment>` のまま受けて push 後に消費するか、`save_user_input` を `&[Segment]` に変えてから push 一回にする選択肢。マイクロ最適化なので必須ではない。
|
||||
- `ipc/server.rs:105-114` のコメント「seed user_messages always come first」は、`ensure_head_or_fork` の auto-fork 経路で発生しうる "seed + 一部 segments のあとさらに live segments" のケースまで明示してくれると将来の保守者に親切(実際には auto-fork 後も pod.user_segments は live 分のみ累積する形で右寄せが成立しているが、コメントを読むと一瞬不安になる)。
|
||||
|
||||
## 判断
|
||||
|
||||
**Request changes** — `Pod::compact()` 後に `self.user_segments` を切り詰めない件(Blocking)が、ticket の完了条件「現行の compaction / fork / restore のフローを壊さない」に直接抵触する。修正は数行+shared_state 再同期で済む規模。それ以外の要件は満たされており、修正後は Approve 想定。
|
||||
Loading…
Reference in New Issue
Block a user