shutdown実装完了
This commit is contained in:
parent
aa138e6583
commit
79f342ca60
1
TODO.md
1
TODO.md
|
|
@ -8,6 +8,5 @@
|
|||
- [ ] Pod オーケストレーション: LLM によるマルチエージェント分業 → [tickets/pod-orchestration.md](tickets/pod-orchestration.md)
|
||||
- [ ] ネイティブ GUI クライアント MVP → [tickets/native-gui-mvp.md](tickets/native-gui-mvp.md)
|
||||
- [ ] TUI 拡充
|
||||
- [ ] Pod の明示的 shutdown → [tickets/tui-pod-shutdown.md](tickets/tui-pod-shutdown.md)
|
||||
- [ ] 新しい Pod を spawn する UI の設計 → [tickets/tui-pod-spawn-ui.md](tickets/tui-pod-spawn-ui.md)
|
||||
- [ ] ツール呼び出しのフレーム更新型表示 → [tickets/tui-tool-call-ui.md](tickets/tui-tool-call-ui.md)
|
||||
|
|
|
|||
255
tickets/pod-orchestration.md
Normal file
255
tickets/pod-orchestration.md
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
# Pod オーケストレーション: LLM によるマルチエージェント分業
|
||||
|
||||
## 背景
|
||||
|
||||
Insomnia の差別化の中核は「複数の Pod が独立プロセスとして並行動作し、LLM が自律的に分業・統括できる」点にある。シングルエージェントの LLM リグが1つのコンテキストで全タスクを処理するのに対し、Insomnia は**タスクを別 Pod に委譲し、結果を集約してフィードバックする**オーケストレーションを LLM 自身に行わせることで、コンテキスト長・専門性・並列性の壁を突破する。
|
||||
|
||||
現状の Pod は単独で動くことしかできず、Pod 間の連携は外部の人間が仲介するしかない。LLM が「この調査は別の Pod に任せて、結果が返ってきたら本筋に統合する」という判断を**ツールとして**実行できる仕組みが無い。
|
||||
|
||||
## ゴール
|
||||
|
||||
Pod の LLM が、ツール呼び出しを通じて別 Pod のライフサイクル全体(生成・指示・結果読み取り・終了)を制御でき、spawned Pod の進捗を非同期に受け取れる基盤を設計・実装する。あわせて、人間がこの Pod ネットワークを監視・介入できる観測経路を用意する。
|
||||
|
||||
## コンセプト: 独立したピアとしての Pod
|
||||
|
||||
```
|
||||
Human (GUI / TUI) ─── Pod A (orchestrator)
|
||||
├── socket ──→ Pod B (researcher)
|
||||
├── socket ──→ Pod C (coder)
|
||||
│ └── socket ──→ Pod D (reviewer)
|
||||
└── socket ──→ Pod E (tester)
|
||||
```
|
||||
|
||||
### プロセス独立
|
||||
|
||||
- spawn された Pod は **完全に独立したプロセス** として動作する。OS レベルの親子関係(subprocess)は持たない
|
||||
- spawner が落ちても、spawned Pod は**続行**する。再接続も可能
|
||||
- spawner と spawned Pod の関係は「socket client として接続している」だけ。関係性はプロセスではなく**ソケット接続**で表現される
|
||||
- すべての Pod は同格。「誰が spawn したか」は Pod の runtime 属性であり、プロセスの従属関係ではない
|
||||
- Pod の発見には**レジストリ**(runtime_dir 内の socket パスを列挙、または明示的な登録)が必要
|
||||
|
||||
### Scope の分譲
|
||||
|
||||
- Pod A が Pod B を spawn するとき、A は自身の scope の一部を B に**譲渡**する
|
||||
- 譲渡した scope 領域は A の effective scope から **deny** される(A は自分が譲った部分にアクセスできなくなる)
|
||||
- これにより**構造的に排他制御が保証**される:同一パスに対して write 権限を持つ Pod は常に高々1つ
|
||||
- Pod B が終了すると、譲渡された scope は A に**返却**される(A の deny が解除される)
|
||||
- Pod B が scope の一部をさらに Pod D に分譲した場合、B の終了時に D が保持している分は D にそのまま残る(D は独立して生きているため)。D が終了するまで、その scope 領域は誰にも返却されない
|
||||
|
||||
### `tickets/scope-exclusion.md` との関係
|
||||
|
||||
scope 分譲により Pod 間の write 排他制御は spawn 時に構造的に保証される。`tickets/scope-exclusion.md` が扱おうとしていた「複数 Pod が同一パスに同時 write する問題」は、**本チケットの scope 分譲モデルで吸収される**。
|
||||
|
||||
ただし、分譲ではなく**共有 read** (複数 Pod が同じパスを同時に read する) は引き続き許可される。read 同士は衝突しない。
|
||||
|
||||
## LLM に公開するツール群
|
||||
|
||||
Pod の LLM が使えるツールとして以下を設計する。いずれも通常の Tool trait 実装で、Worker に登録される。
|
||||
|
||||
### `SpawnPod`
|
||||
|
||||
新しい Pod を起動し、scope の一部を譲渡する。
|
||||
|
||||
入力:
|
||||
- `name`: spawned Pod の識別名(spawner のスコープ内で一意)
|
||||
- `instruction`: spawned Pod の instruction ファイル参照(省略時は `$insomnia/default`)
|
||||
- `task`: spawned Pod への最初のメッセージ(spawn 後に即座に run される)
|
||||
- `scope`: 譲渡する scope 定義(allow / deny ルール)。**spawner の現在の effective scope のサブセットでなければならない**。バリデーションで超過分は拒否
|
||||
|
||||
出力:
|
||||
- spawned Pod の識別子(`pod_id`)と接続状態
|
||||
|
||||
内部動作:
|
||||
- spawner の effective scope から、譲渡する scope 領域を deny に追加(spawner 側の scope を縮小)
|
||||
- PodFactory のカスケード(user manifest + project manifest)に加え、spawner からの overlay(譲渡 scope + instruction + provider 等)を重ねて spawned Pod の PodManifest を構築
|
||||
- 独立プロセスとして Pod を起動、socket 確立
|
||||
- `task` を `Method::Run` で送信
|
||||
|
||||
### `SendToPod`
|
||||
|
||||
spawned Pod にメッセージを送る。
|
||||
|
||||
入力:
|
||||
- `pod_id`: 対象の Pod
|
||||
- `message`: 送信するテキスト
|
||||
|
||||
出力:
|
||||
- 送信確認
|
||||
|
||||
### `ReadPodOutput`
|
||||
|
||||
spawned Pod の最新の出力を読む。
|
||||
|
||||
入力:
|
||||
- `pod_id`: 対象の Pod
|
||||
- `since`: 前回読んだ時点からの差分のみ取得するオプション(省略時は全出力)
|
||||
|
||||
出力:
|
||||
- 対象 Pod の assistant 応答テキスト(最新ターン or 差分)
|
||||
- 現在の状態(`running` / `idle` / `stopped`)
|
||||
|
||||
### `StopPod`
|
||||
|
||||
spawned Pod を終了させ、譲渡した scope を回収する。
|
||||
|
||||
入力:
|
||||
- `pod_id`: 対象の Pod
|
||||
|
||||
出力:
|
||||
- 終了確認
|
||||
- 回収された scope の要約
|
||||
|
||||
内部動作:
|
||||
- 対象 Pod に graceful shutdown を要求
|
||||
- 対象 Pod が保持していた scope のうち、さらに下流に分譲されていない分を spawner に返却
|
||||
- spawner の deny リストから返却分を解除
|
||||
|
||||
### `ListPods`
|
||||
|
||||
spawner が spawn した Pod の一覧とそれぞれの状態を返す。
|
||||
|
||||
入力: なし
|
||||
|
||||
出力:
|
||||
- spawned Pod の `pod_id`、`name`、`status`(running / idle / stopped)、譲渡中の scope 要約、最終応答の要約
|
||||
|
||||
## 非同期通知
|
||||
|
||||
spawned Pod が出力を生成したとき、spawner の LLM にそれを伝える仕組みが必要。ただし LLM は「ターン」の単位で動くため、イベントストリームをリアルタイムに割り込ませるのは不自然。
|
||||
|
||||
### 方針: ターン間フックで集約通知
|
||||
|
||||
- spawner のターンが終了した後、次のターンの開始前に**フック**が走り、spawned Pod から届いているイベントを集約する
|
||||
- 集約された通知は次のターンの**先頭に system message として注入**される(例: `[pod "researcher"] completed: found 3 relevant files`)
|
||||
- 通知が無い場合は何もしない
|
||||
|
||||
### 通知の粒度
|
||||
|
||||
- **ターン完了通知**: spawned Pod の1ターンが終わったとき
|
||||
- **エラー通知**: spawned Pod でエラーが発生したとき
|
||||
- **終了通知**: spawned Pod が停止したとき(scope 返却が発生する)
|
||||
|
||||
ツール出力の内容自体は `ReadPodOutput` で明示的に取りに行くモデル。通知は「何か変化があった」というシグナルのみで、内容全体は含まない(spawner のコンテキストを圧迫しない)。
|
||||
|
||||
## Scope の分譲と回収の詳細
|
||||
|
||||
### 分譲時
|
||||
|
||||
1. spawner が `SpawnPod` で `scope` を指定
|
||||
2. システムが検証: 指定された scope が spawner の **現在の effective scope のサブセット**であること
|
||||
3. 検証通過後、spawner の effective scope から譲渡分を deny として差し引く
|
||||
4. spawned Pod は譲渡された scope を自身の allow として持って起動
|
||||
|
||||
### 回収時(StopPod または spawned Pod の自発的終了)
|
||||
|
||||
1. 終了する Pod が保持している effective scope を確認
|
||||
2. そのうち、さらに下流に分譲中の scope は除外(下流 Pod が生きている間は返却されない)
|
||||
3. 残りの scope を spawner の deny から解除し、spawner の effective scope に復帰
|
||||
|
||||
### 分譲チェーンのケース
|
||||
|
||||
Pod A が Pod B に `/src` を分譲 → Pod B が Pod D に `/src/core` を分譲:
|
||||
|
||||
- A の scope: `/src` は deny
|
||||
- B の scope: `/src` を持つが `/src/core` は deny(D に分譲済み)
|
||||
- D の scope: `/src/core`
|
||||
|
||||
B が終了すると:
|
||||
- `/src` のうち D に分譲中の `/src/core` は返却されない(D が生きている)
|
||||
- `/src` から `/src/core` を除いた部分が A に返却される
|
||||
- A の scope: `/src` の `/src/core` 以外が復帰。`/src/core` は引き続き deny(D が保持中)
|
||||
- D が終了すると `/src/core` が A に返却される
|
||||
|
||||
### runtime scope state
|
||||
|
||||
- Pod の scope は manifest の static 定義だけでは決まらない。分譲と回収による**動的な state** が加わる
|
||||
- この state を管理するレイヤーが必要(Pod 内のメモリ状態 + 永続化の選択肢)
|
||||
- spawner がクラッシュして復帰した場合、分譲中の scope を復元する必要がある → scope ledger の永続化が事実上必須
|
||||
|
||||
## Pod の設定
|
||||
|
||||
### instruction
|
||||
|
||||
- spawned Pod の `instruction` は `SpawnPod` の引数で明示指定
|
||||
- 省略時は `$insomnia/default`
|
||||
- spawner の instruction を引き継ぐケースはまれ(分業目的の spawn なので、spawned Pod には固有の役割がある)
|
||||
|
||||
### provider
|
||||
|
||||
- API キーと provider 設定は通常ユーザー manifest / プロジェクト manifest 由来なので、spawned Pod は PodFactory 経由で同じカスケードから取得する
|
||||
- spawned Pod に異なるモデルを使わせたい場合は overlay で上書きする(例: 調査用 Pod には小さいモデル、コード生成にはフルモデル)
|
||||
|
||||
## 人間向けの監視
|
||||
|
||||
### 各 Pod は独立して観測可能
|
||||
|
||||
- すべての Pod は通常の socket サーバーを持つ。人間は GUI / TUI で任意の Pod に接続して会話を閲覧・介入できる
|
||||
- Pod 間に主従関係が無いので、人間はどの Pod にも同格にアクセスできる
|
||||
|
||||
### Pod ネットワークの可視化
|
||||
|
||||
- GUI / TUI が Pod の一覧を表示(spawn 関係をグラフまたはリストで表現)
|
||||
- 各ノードに status(running / idle / stopped)、保持中の scope 要約、最終ターンの要約を表示
|
||||
- ノードをクリック/選択すると、その Pod の会話ビューに切り替わる
|
||||
|
||||
### 介入
|
||||
|
||||
- 人間は任意の Pod に直接メッセージを送れる(通常のクライアントとして接続)
|
||||
- 人間が Pod を直接 stop できる(scope は spawner に返却される)
|
||||
- spawner の LLM は spawned Pod に人間が介入したことを(次の通知で)知る
|
||||
|
||||
## Pod の発見と登録
|
||||
|
||||
プロセスが独立しているため、起動中の Pod を**発見**する仕組みが要る。
|
||||
|
||||
候補:
|
||||
- **A. runtime_dir convention**: 各 Pod が socket path を `runtime_dir/<pod_name>.sock` に作る。ディレクトリを列挙すれば一覧が取れる(現行の仕組みの延長)
|
||||
- **B. 明示的なレジストリ**: Pod 起動時に共有レジストリ(ファイルまたは IPC)に登録。終了時に削除
|
||||
- **C. daemon**: 軽量な daemon プロセスがレジストリ役を担う(`crates/daemon` は現在空)
|
||||
|
||||
現時点では A が最もシンプル。daemon が必要になるのは「リモート Pod」や「Pod の自動再起動」が必要になってから。
|
||||
|
||||
## 設計で決めること
|
||||
|
||||
- **Pod の起動方式**: 独立プロセスとして `pod` バイナリを起動する形で確定。起動方法の詳細(fork? 直接 exec? nix-shell 経由?)
|
||||
- **scope ledger の永続化方式**: ファイルベース / session-store / 別の永続ストア
|
||||
- **scope の分譲粒度**: パス単位で分譲するか、permission レベル(read / write)でも分譲できるか(例: write だけ譲って read は共有する等)
|
||||
- **通知の注入方式**: system message として注入するか、専用の `Event` としてターン開始前に Worker に流すか
|
||||
- **通知のバッファリング**: spawner の1ターンが数分かかる場合、その間のイベントをどこに溜めるか
|
||||
- **Pod の発見**: A (runtime_dir) / B (レジストリファイル) / C (daemon)
|
||||
- **リソース制限**: 最大 spawned Pod 数、ネスト深さの上限
|
||||
- **ツール名**: `SpawnPod` / `SendToPod` / `ReadPodOutput` / `StopPod` / `ListPods` の命名はそのままで良いか
|
||||
- **Pod ネットワークの可視化**: GUI / TUI どちらに先行実装するか
|
||||
- **spawner 復帰時の再接続**: scope ledger を読んで spawned Pod のソケットに再接続する手順
|
||||
- **分譲チェーンの depth limit**: 再帰的 spawn の深さ上限を設けるか
|
||||
|
||||
## 完了条件
|
||||
|
||||
- Pod の LLM が `SpawnPod` ツールで別 Pod を起動でき、spawned Pod が独立プロセスとして動作する
|
||||
- spawn 時に scope が分譲され、spawner の effective scope が縮小されることを検証できる
|
||||
- `SendToPod` で spawned Pod にメッセージを送り、`ReadPodOutput` で応答を読み取れる
|
||||
- `StopPod` で spawned Pod を graceful に停止でき、scope が spawner に返却される
|
||||
- `ListPods` で spawned Pod の状態と scope 要約を一覧できる
|
||||
- spawned Pod のターン完了・エラー・停止が spawner に非同期通知され、次のターン開始時に LLM に伝わる
|
||||
- spawner が停止しても spawned Pod が続行し、spawner 復帰時に再接続できる
|
||||
- 人間が GUI または TUI で Pod ネットワークを閲覧でき、任意の Pod に接続して会話を見られる
|
||||
- spawned Pod がさらに別の Pod を spawn する再帰的なネットワークが動作する
|
||||
- scope 分譲チェーンの返却(中間ノードの終了時に部分返却)が正しく動作する
|
||||
|
||||
## 他チケットとの関係
|
||||
|
||||
- **tickets/scope-exclusion.md**: scope 分譲モデルにより**本チケットに吸収される**。write 排他制御は分譲時に構造的に保証されるため、別の排他制御メカニズムは不要。scope-exclusion.md は本チケットの scope 設計がカバーする
|
||||
- **tickets/protocol-design.md**: Protocol に Pod 間通知のイベント(spawned Pod の状態変化通知など)を追加する必要があるかもしれない。ただし各 Pod は通常の socket サーバーなので、protocol 拡張なしで「複数 socket に繋ぐクライアント」として実装できる可能性もある
|
||||
- **tickets/native-gui-mvp.md**: Pod ネットワーク可視化は GUI 側の拡張。MVP scope には含まれていないが、本チケットの監視要件は GUI の次フェーズに自然に接続する
|
||||
- **tickets/tui-pod-spawn-ui.md**: TUI からの Pod spawn UI。本チケットは LLM からの spawn だが、spawn のインフラ(PodFactory + プロセス起動)は共通
|
||||
- **tickets/tui-pod-shutdown.md**: Pod ネットワークの shutdown 戦略にも影響。Pod を停止したとき scope がどこに返るか
|
||||
- **tickets/permission-extension-point.md**: spawned Pod のツール実行パーミッションを spawner が制御する可能性
|
||||
|
||||
## 範囲外
|
||||
|
||||
- **Pod 間の直接メッセージパッシング**: Pod 同士が spawner を介さずに直接通信する仕組み。すべてのやり取りは spawner(オーケストレーター)を介す
|
||||
- **共有メモリ / 共有状態**: Pod 間のデータ共有はツール経由のテキストメッセージのみ。ファイルシステムを介した暗黙の共有は scope が許す範囲で起きうるが、それは Pod の既存動作の延長であり、分譲により write 衝突は構造的に排除される
|
||||
- **自動スケーリング / 負荷分散**: Pod の spawn 先は常にローカルマシン。リモート実行やクラウド分散は扱わない
|
||||
- **課金・トークン予算管理**: spawned Pod が消費するトークンの予算制御や spawner への請求集約は扱わない
|
||||
- **人間がネットワークの構造を変更する操作**(Pod の spawn 元替え、ネットワークの組み替えなど)
|
||||
- **daemon の導入**: Pod の発見は runtime_dir convention または軽量レジストリで対応。daemon は必要が明確になってから
|
||||
|
|
@ -1,53 +0,0 @@
|
|||
# TUI: Pod を明示的に終了させる操作
|
||||
|
||||
## レビュー状態
|
||||
|
||||
初回レビュー実施済み。[tui-pod-shutdown.review.md](tui-pod-shutdown.review.md) を参照。
|
||||
要件達成、アーキテクチャは「通常後処理を経由してからの終了」を保証。指摘1件(shutdown 中の進行表示が無い — 実害なしで不問)。**受け入れ可**。
|
||||
|
||||
## 背景
|
||||
|
||||
現状、TUI から Pod を終了させる手段は Ctrl-C / プロセス終了に頼っている。これだと:
|
||||
|
||||
- 実行中のターンが中断された状態で終わり、session-store の永続化が中途半端になる可能性
|
||||
- 複数 Pod を並列で扱えるようになったとき(`tickets/tui-pod-spawn-ui.md`)、「今表示している Pod だけを畳む」操作と「TUI 全体を終わる」操作を区別する必要がある
|
||||
- ユーザーが「Pod の作業を完了としてクローズする」という意図を表現できない
|
||||
|
||||
TUI から明示的に Pod の shutdown を指示できる操作を追加する。
|
||||
|
||||
## 要件
|
||||
|
||||
### 操作
|
||||
|
||||
- TUI 内のキーバインドで Pod の終了を開始できる。
|
||||
- 破壊的な副作用(セッションは残るが実行中ターンは落ちる)を避けるため、**実行中のターンがあれば確認を挟む**。
|
||||
- 確認なしで終了する強制モードを用意するかは設計時に判断。
|
||||
|
||||
### Pod 側の shutdown 手順
|
||||
|
||||
- 実行中のターンがあれば、既存のキャンセル機構(`WorkerError::Cancelled`)で中断する。
|
||||
- キャンセル完了を待ってから session-store にフラッシュし、Pod 状態を `Stopped` 相当に遷移させる。
|
||||
- 中断時点までの turn は整合を保って永続化される(途中の partial turn は巻き戻される)。
|
||||
|
||||
### TUI 側の挙動
|
||||
|
||||
- shutdown 完了後、TUI はその Pod の表示を閉じる。
|
||||
- 他に Pod が無ければ TUI 自体も正常終了する(並列管理が無い現状ではこちらの挙動で良い)。
|
||||
- shutdown 中は進行状況が画面上で分かる(「shutting down...」等の表示)。
|
||||
|
||||
## 設計で決めること
|
||||
|
||||
- **キーバインド**: `q` / `Ctrl-D` / `:quit` のどれを当てるか、強制モードのキーを別立てにするか
|
||||
- **実行中ターンがあるときの確認 UI**: モーダル / フッター問い合わせ / キー再押下
|
||||
- **Pod 層 API**: 既存の cancel + persist を組合せた shutdown メソッドを Pod に生やすか、Controller 側で段取るか
|
||||
|
||||
## 完了条件
|
||||
|
||||
- TUI からキー操作で Pod を shutdown できる。
|
||||
- 実行中のターンがあった場合は確認を経由し、キャンセル → 永続化 → 画面クローズの順で進む。
|
||||
- shutdown 後に `test_pod.local.toml` の session を再開すると、中断されたターンの副作用が残っていない整合状態で開ける。
|
||||
|
||||
## 範囲外
|
||||
|
||||
- 複数 Pod 並列管理下での「この Pod だけ閉じる」と「全部閉じる」の使い分け。並列管理自体が無いため、本チケットでは単一 Pod 前提とする。仕様は `tickets/tui-pod-spawn-ui.md` で改めて整理する。
|
||||
- Pod の一時停止(resume 可能な pause)。本チケットは完全な shutdown のみ扱う。
|
||||
|
|
@ -1,99 +0,0 @@
|
|||
# レビュー: TUI: Pod を明示的に終了させる操作
|
||||
|
||||
対象差分: `crates/protocol/src/lib.rs`, `crates/pod/src/{controller,lib,main}.rs`, `crates/pod/tests/controller_test.rs`, `crates/pod/examples/pod_protocol.rs`, `crates/tui/src/{app,main}.rs`(staged、未コミット)
|
||||
|
||||
## 要件達成状況
|
||||
|
||||
| 要件 | 状態 |
|
||||
|---|---|
|
||||
| TUI 内のキーバインドで Pod の終了を開始できる | ✅ `Ctrl-D` で `Method::Shutdown` を送信 |
|
||||
| 実行中のターンがあれば確認を挟む | ✅ `handle_shutdown`: running 中は「Press Ctrl-D again」警告を表示、3秒以内の再押下で確定 |
|
||||
| 実行中のターンがなければ即座に終了 | ✅ `!app.running` なら即 `Method::Shutdown` |
|
||||
| 既存のキャンセル機構で中断する | ✅ `run_with_cancel_support` 内で `Method::Shutdown` 受信時に `cancel_tx.try_send(())` + `shutdown_requested = true` |
|
||||
| キャンセル完了後 session-store にフラッシュ | ✅ `run_with_cancel_support` が `(status, shutdown)` を返した後、controller loop 内で `write_status` / `write_history` が走ってから `break` |
|
||||
| shutdown 完了後 TUI が終了する | ✅ `Event::Shutdown` → `app.quit = true`、main loop が break |
|
||||
| Pod プロセス自体も正常終了する | ✅ controller が `shutdown_tx.send(())` → `main.rs` の `shutdown_rx` が発火 → `drop(handle)` → `ExitCode::SUCCESS` |
|
||||
| shutdown 中の進行状況が画面で分かる | 🟡 「Press Ctrl-D again to cancel and shut down.」メッセージは出るが、shutdown 後の「shutting down...」表示は無い(`Event::Shutdown` 受信時に `app.quit = true` だけで即終了)|
|
||||
|
||||
## アーキテクチャ
|
||||
|
||||
### Protocol 拡張
|
||||
|
||||
- `Method::Shutdown` と `Event::Shutdown` が protocol に追加。最小限の拡張で意味が明確
|
||||
- `Method::Shutdown` は Run/Resume/Cancel と同格の method バリアント。socket 層での特殊扱い不要
|
||||
|
||||
### Controller 内の shutdown フロー
|
||||
|
||||
```
|
||||
Idle 状態で Shutdown 受信:
|
||||
→ Event::Shutdown を broadcast → controller loop break → shutdown_tx.send(())
|
||||
|
||||
Running 状態で Shutdown 受信 (run_with_cancel_support 内):
|
||||
→ cancel_tx.try_send(()) で実行中ターンをキャンセル
|
||||
→ shutdown_requested = true をフラグ
|
||||
→ pod_future が Cancelled/Error で完了
|
||||
→ (PodStatus::Idle, shutdown=true) を返す
|
||||
→ controller loop が compaction → write_status → write_history の通常後処理
|
||||
→ shutdown フラグを見て Event::Shutdown → break → shutdown_tx.send(())
|
||||
```
|
||||
|
||||
**重要**: shutdown が来ても**通常の後処理(compaction, persist)を必ず経由**してから抜ける。session の整合性が壊れない。この設計は正しい。
|
||||
|
||||
### main.rs の二択 select
|
||||
|
||||
```rust
|
||||
tokio::select! {
|
||||
_ = tokio::signal::ctrl_c() => { ... }
|
||||
_ = shutdown_rx => { ... }
|
||||
}
|
||||
```
|
||||
|
||||
Ctrl-C(signal)と client 要求の Shutdown を同格に扱う。どちらが先に来ても `drop(handle)` → 正常終了。
|
||||
|
||||
### TUI の確認 UI
|
||||
|
||||
`shutdown_confirm: Option<Instant>` フィールドで「最後に Ctrl-D を押した時刻」を保持。3秒以内の再押下で確定。Timeout 後は再度リセット。シンプルで十分。
|
||||
|
||||
## 指摘事項
|
||||
|
||||
### 1. 🟡 shutdown 中の進行表示が無い
|
||||
|
||||
チケット要件:
|
||||
> shutdown 中は進行状況が画面上で分かる(「shutting down...」等の表示)。
|
||||
|
||||
実装:
|
||||
- `Event::Shutdown` を受けた瞬間に `app.quit = true` → main loop が即 break → terminal restore
|
||||
- ユーザーが「shutting down」を読む暇がない
|
||||
|
||||
実際には shutdown はほぼ瞬時(キャンセル完了 + persist + Event::Shutdown が数十 ms)なので視認できるタイミングがそもそも無い。要件の「shutting down...」は「長い shutdown を想定した表示」だが、実装上 shutdown は速いので**実用上問題にならない**。
|
||||
|
||||
**判断**: 実害なし。将来 shutdown が重くなった場合(大量ターンの persist 等)に表示を足すのは容易(`Event::Shutdown` 受信時に output_queue に push してから quit を遅延させればよい)。**不問**。
|
||||
|
||||
### 2. 🟢 `shutdown_confirm` が `Instant` ベース
|
||||
|
||||
wall clock ではなく `std::time::Instant` を使っている。NTP ジャンプに影響されない。正しい選択。
|
||||
|
||||
### 3. 🟢 Resume パスでも shutdown 対応
|
||||
|
||||
`Method::Resume` のハンドラも `run_with_cancel_support` を通っており、Resume 中に `Shutdown` が来ても同じフローで処理される。漏れなし。
|
||||
|
||||
### 4. 🟢 examples / tests の追随
|
||||
|
||||
- `pod_protocol.rs`: `PodController::spawn` の戻り値を `(handle, _shutdown_rx)` に分解
|
||||
- `controller_test.rs`: 同上
|
||||
- 既存テストが壊れていない
|
||||
|
||||
### 5. 🟢 `Ctrl-D` のキーバインド選択
|
||||
|
||||
`Ctrl-D` は shell の EOF 慣例に合っており、「この Pod との対話を終える」という意味が自然。`q` や `:quit` は通常入力との衝突リスクがあるので `Ctrl-D` は妥当。
|
||||
|
||||
## テスト
|
||||
|
||||
- `controller_test.rs`: 戻り値の型変更に追随。shutdown 固有のテスト(Shutdown method を送って controller が break するか、persist 後に shutdown_rx が発火するか等)は**未追加**
|
||||
- `handle_shutdown` のユニットテスト(running 時の confirm フロー、timeout 後のリセット等)は**未追加**
|
||||
|
||||
テストが薄い点はあるが、shutdown フローは controller loop 内の分岐であり、integration test で網羅すると MockClient の応答タイミング制御が必要になって重くなる。最小実装としては許容。
|
||||
|
||||
## 結論
|
||||
|
||||
**受け入れ可**。要件はほぼ達成、アーキテクチャは「通常の後処理を必ず経由してから終了」という最重要の不変条件を守っている。指摘1(shutdown 中の進行表示)は実害なしで不問。テストの薄さは将来必要になったら足す程度。
|
||||
Loading…
Reference in New Issue
Block a user