6.9 KiB
6.9 KiB
Pod: イベントハンドラからターン起動を分離する
背景
crates/pod/src/controller.rs の controller タスクは、 outer loop の Method ハンドラ内に イベント処理とターン起動を同居 させている。
該当する箇所:
Method::Run→ outer arm 内でrun_with_cancel_support(pod.run(...)).awaitとfinish_controller_run.awaitを呼ぶMethod::Notify(Idle) → outer arm 内でset_controller_status(Running).awaitのあとrun_with_cancel_support(pod.run_for_notification()).await/finish_controller_run.awaitMethod::PodEvent(Idle) → 同上 (副作用処理apply_event_side_effects.awaitも同じ arm 内)Method::Resume→ 同上
ハンドラ自身が「ターンを丸ごと await する長時間処理」 を抱えているため、 以下が起きる:
- 同じターン起動コード (
set_controller_status→run_with_cancel_support→finish_controller_run) が 4 箇所に重複する - ターン起動の発火元 (Run / Notify / PodEvent / Resume) と起動本体が分離されていないため、 起動条件の変更が複数 arm に波及する
- ターン起動に必要な前処理 (history への user message append / NotifyBuffer への push / status 遷移) も各 arm に分散している
ターン起動を outer loop の周回トップに集約し、 各イベントハンドラは「次のターンに渡す入力をその場で確定 (history append / NotifyBuffer push) + needs_run フラグセット」 までに留めることで、 起動コードの重複と入力経路の分散をなくす。
加えて、 PodController::spawn 自体が約 615 行に膨らんでおり、「組み立て (channel/runtime_dir/Worker hook 配線/ツール登録/PodHandle 構築)」と「実行ループ (controller タスク)」が単一関数に同居している。 outer loop を上記の形に書き直すタイミングで、 spawn の構造分解も同時に行う方が、 重複コードの統合先が見やすくなる。
なお、 別途観測されている「auto-kick されたターンの内部で controller がデッドロックする現象」 はこの整理では解決しない (inner select! のアーム内 await が pending する経路は構造上残る)。 根本原因の特定は別チケットの対象。
要件
- イベントハンドラは 「状態更新と
needs_runフラグ立て」 まで にとどめる。ハンドラ内でrun_with_cancel_support.await/finish_controller_run.awaitを呼ばない - outer loop の各周回はまず
needs_runを評価し、 立っていればターン起動 (run_with_cancel_support→finish_controller_run) を実行してフラグを降ろす。 立っていなければmethod_rx.recv().awaitで次の Method を待つ - 既存の inner select! によるターン中の Method 並行受信は維持する。 ターン本体の借用構造 (
&mut Podを Worker が抱える) も変更しない needs_runを立てる契機は最低限以下を含める (= 現状 auto-kick している経路):Method::RunMethod::ResumeMethod::Notify(Idle 時のみ)Method::PodEvent(Idle 時のみ)
- 「次のターンの起動意図と入力」 は
needs_runを起動意図を持つ enum として表現する:Run { input }/InterruptAndRun { input }/RunForNotification/ResumeMethod::Notify/Method::PodEventの Idle 経路は NotifyBuffer に push したうえでRunForNotificationを立てる (現状のpod.run_for_notification()が NotifyBuffer から自動取得する挙動に乗るため、 enum に入力を載せる必要はない)
Worker::run/Pod::runの入力受け取り API には触らない (interceptor のon_prompt_submitcancel / 書き換え / extras ordering の invariant に踏み込まない)- Pause / Shutdown / Cancel はハンドラ内で完結する (フラグ化しない、 既存通り即時処理)
- Running 中に来た
Method::Notify/Method::PodEventの挙動 (NotifyBuffer に積むだけ、 副作用は実行) は変えない - 上記改修に合わせて
PodController::spawnを以下の責務単位に分解する:- 初期化 (channel 群 /
RuntimeDir/ pod-immutable snapshot /SpawnedPodRegistry/alerter装着 / bash-output scope) - Worker への event bridge コールバック配線 (
on_turn_*/on_*_block/on_tool_result/on_usage/on_warning/on_history_append等) - ツール登録 (builtin / memory / spawn orchestration)
- 初期ファイル書き出し +
PodSharedState/PodHandle構築 +SocketServer起動 controller_loop— Idle/Paused 状態の event loop + 後処理 (現tokio::spawn(async move { loop { ... } })の本体)drive_turn— Running 状態の event loop。 現run_with_cancel_supportを改名し、controller_loopと同格の関数として並べる (「cancel support」 という実装詳細名から役割ベースの名前に改める)
- 初期化 (channel 群 /
- 分解後の
spawnはこの順序で各責務を呼び出す薄いフローになる。 Idle/Paused 側 (controller_loop) と Running 側 (drive_turn) の event loop が同格の関数として並ぶ形に揃える
完了条件
Method::Run/Method::Resume/Method::Notify(Idle)/Method::PodEvent(Idle)のいずれも、 ターン起動が outer loop の周回トップ 1 箇所に集約されており、 ハンドラ側にはターン起動コードが残っていない- 各イベントハンドラの async body 内に「ターン丸ごと」 や
finish_controller_run等の長時間 await が無い - 既存挙動が変わらない (どの Method で auto-kick されるか、 ターン中の Cancel / Pause / Shutdown が効くか、 NotifyBuffer に積まれた内容が次ターンで反映されるか、 ターン起動順序)
PodController::spawnが責務単位の関数列を順に呼ぶ薄いフローになっており、 単一関数の中に組み立てと実行ループが同居していない- Idle/Paused 状態の event loop (
controller_loop) と Running 状態の event loop (drive_turn) が同格の関数として並んでおり、 名前が役割を表している (run_with_cancel_supportの名前は残らない) crates/pod/tests/controller_test.rsの既存テストが通る
範囲外
- 観測されたデッドロックの根本原因特定 (別チケット予定)
Pod構造体の&mut selfborrow を分解して outer loop を完全 dispatcher 化する大改修 (将来検討)- Cancel / Shutdown の専用チャネル化 (controller が固まっている時には別経路でも効かないため、 メリット薄と判断して見送り)
Worker::run/Pod::runの入力なし API 化 (controller の dispatch を bool に圧縮するメリットに対し、 worker interceptor invariant を壊すコストが見合わない)