8.5 KiB
Log writer を sync 化 + LogEntry を単数化
背景
tickets/system-item-unify.md で Event::SystemItem / LogEntry::SystemItems を導入し、 Notify / PodEvent / 各 ref 解決を 1 経路に統合した。 ただし実装の過程で 2 つの設計上の歪みが残った:
1. LogEntry::SystemItems { items: Vec<SystemItem> } の複数形は実需要が無い
- 既存
LogEntry::AssistantItems { items: Vec<LoggedItem> }/ToolResults { items: Vec<LoggedItem> }の形に揃えて plural にしたが、 これは旧save_deltaの turn 末尾バッチ commit 時代の名残 - 現在は worker の
on_history_appendcallback が per-item で発火し、 drain task が 1 件ずつ classify+commit する形に移行済み。items: Vec<_>の中身は常に 1 件 SystemItemsも同様で、 interceptor が drain した N 件をまとめているだけで「グルーピング」 の意味はない (同 drain 機会に来ただけ)- wire 側では IPC server が
items.into_iter().map(...)で per-item のEvent::SystemItemに fan-out しており、 LogEntry 上のバッチと wire 上の単発の非対称が発生している
2. LogCommand::SystemItems 追加が二重設計
LogCommandは元来「worker の sync callback (on_history_append) を async commit に橋渡しする」 ための小さな ferry。Item + Flushの 2 variant だけだった- system items に typed kind 情報を運ぶために
LogCommand::SystemItems(Vec<SystemItem>)を増やしたが、 interceptor は async コンテキストなので sync→async の橋渡しは要らない。 単に「SystemItem を直接 commit する経路」 として LogCommand を流用しているだけで、LogCommandの元来の責務 (sync→async ferry) を曖昧にしている
3. そもそも log 周りで async が必要だった理由はほぼ無い
log 周りで async になっている箇所を棚卸しすると:
| 箇所 | 現在 async | 本当に async でないとダメか |
|---|---|---|
Store::append (FsStore) |
tokio::fs | ❌ std::fs::OpenOptions + writeln! で良い (1 行 append、 < 1KB、 < 1ms) |
Store::read_all (restore 時) |
tokio::fs::read_to_string | ❌ Pod 起動時 1 回、 hot path じゃない |
Store::read_head_hash |
tokio::fs | ❌ 同上 |
session_head mutex |
tokio::sync::AsyncMutex |
❌ async は不要、 sync mutex で十分 |
sink.publish / subscribe_with_snapshot |
sync (元から) | — |
broadcast::send |
sync (元から) | — |
つまり log subsystem を async にしていた本当の理由は tokio::fs を default 選択した点だけ。 hash chain 維持のためでも性能のためでもない。 そして sync callback が async commit を呼べないという mismatch を解消するために LogCommand ferry を作る必要があった。
log writer を sync にすればこの mismatch そのものが消え、 LogCommand / drain task / Flush バリアは丸ごと不要になる。
方針
A. log writer を sync に切り替える
Store::append/read_all/read_head_hashをstd::fs/std::ioベースの sync API に変更 (or sync 版を併設して段階移行)session_headをtokio::sync::AsyncMutexからparking_lot::Mutex(orstd::sync::Mutex) に変更SessionLogWriter::append_entry()を sync 関数に。 disk write の間 caller の thread が ms 未満ブロックする (local fs append、 < 1KB、 実害なし)- Pod の
commit_entry().awaitはcommit_entry()(await 無し) に - worker の
on_history_appendcallback が直接writer.append_entry(classified_entry)を呼ぶ - interceptor も
Arc<SessionLogWriter>を持って直接呼ぶ
B. LogCommand / drain task / Flush バリアの撤廃
LogCommandenum を削除run_log_drain関数を削除- mpsc channel (
log_cmd_tx/log_cmd_rx) を削除 persist_turnの Flush バリアを削除 (sync 書き込みなので順序は call 順で自然に決まる)- worker callback で直接 sync 書き込みするので、
Worker::on_history_append経由のフローはそのまま (sync closure)
C. LogEntry のアシスタント / ツール / システム系を単数バリアントに揃える
- 新名 (write side):
LogEntry::AssistantItem { ts, item: LoggedItem }LogEntry::ToolResult { ts, item: LoggedItem }LogEntry::SystemItem { ts, item: SystemItem }
- 旧 plural variant (
AssistantItems/ToolResults/HookInjectedItems/SystemItems) は 読み出し専用 (deserialize-only) として残置。 既存セッションログを開けることは保証する collect_stateは新旧両方をフラットにstate.historyに展開- 1 entry = 1 item になることで commit 経路が
LogEntry::<Singular> { item }に直接 1:1 写像する - wire 側の
Event::SystemItem(per-item) と log 上のLogEntry::SystemItem(per-item) が完全対称になり、 IPC server の fan-out ロジックが消える
ハッシュチェーン長は entry 数に比例して長くなるが、 hash chain 自体は別チケットで廃止予定なので長さの悪化は受け入れる。
要件
Storetrait の主要メソッド (append/read_all/read_head_hash/create_session等) を sync API に統一する。 async 版を残す必要があるかは内部判断 (例: 起動時に大きい log を読むread_allだけ async を残す案もあり、 必要に応じて選択)SessionLogWriter(Pod 側ラッパ) を sync API に統一し、 hash chain 計算 + session_head 更新 + sink publish を 1 つの sync 関数内で行うsession_headを sync mutex に変更LogCommand/LogDrainHandle::run_log_drain/log_cmd_tx/log_cmd_rx/ Flush バリアを削除PodInterceptor::on_prompt_submit/pending_history_appendsはArc<SessionLogWriter>を直接呼んで per-SystemItemに commit。 完了後にItem::system_messageを worker に返す- worker の
on_history_appendcallback が直接writer.append_entry(...)を呼ぶ (sync) LogEntryにAssistantItem/ToolResult/SystemItemの単数 variant を追加し、 新規書き込みはこれを使う- 旧 plural variant は read 専用として残し、 deserialize alias または独立 variant のどちらでも実装判断で良い。
collect_stateで同じ形に reduce されること - IPC server の
LogEntry::SystemItem受信は 1 件のEvent::SystemItemをそのまま送る単純な対応に cargo test --workspaceが pass する
完了条件
LogCommand/ drain task / Flush バリアがコードから消えている- worker の
on_history_appendcallback が直接 sync writer を呼んでいる PodInterceptorが writer を直接呼んで SystemItem を commit している (mpsc 経由ではない)StoreおよびSessionLogWriterの主要 commit API が sync 関数session_headの mutex 型がparking_lot::Mutexまたはstd::sync::Mutex- 新規セッションログに
assistant_item/tool_result/system_item(snake_case wire tag) が単数 entry として書かれている - 旧
assistant_items/tool_results/hook_injected_items/system_itemsを含むセッションログが読めて view 復元できる - IPC server で
LogEntry::SystemItemを受けたら 1 件のEvent::SystemItemを fan-out 無しで送るシンプルなマッピングに戻っている
範囲外
- ハッシュチェーン自体の廃止 (entry hash,
prev_hash,HashedEntry,session_headmutex の最終撤去) はtickets/persistence-semantics.mdの責務領域。 本チケットはその直前段階として、 mutex を sync 化するところまで - Session / Segment 階層への rename (
tickets/persistence-semantics.md) - 旧 plural entry の disk migration (ファイル書き換え)。 deserialize alias で読めるところまで
Event::SystemItempayload の変更 (引き続きserde_json::ValueでSystemItemの JSON 形)
関連
- 前提
tickets/system-item-unify.md(本チケットで完成した SystemItem 経路を、 本チケットで単数化 + direct writer 化する後続) - 関連
tickets/pod-state-from-session-log.md(per-item commit の drain task 経路を確立した前段。 本チケットでその drain task 自体を撤去する) - 後続
tickets/persistence-semantics.mdの「Entry hash の廃止」 セクション。 本チケットで sync 化したことが entry hash 廃止の足場になる