SpawnPodツールが落ちる問題の発見

This commit is contained in:
Keisuke Hirata 2026-04-19 15:14:15 +09:00
parent ddd7327290
commit 25df7a79c1
3 changed files with 84 additions and 3 deletions

View File

@ -5,6 +5,7 @@
- [ ] パーミッション: パターンベースのツール実行制御 → [tickets/permission-extension-point.md](tickets/permission-extension-point.md)
- [ ] Pod オーケストレーション
- [ ] 動的 Scope 変更 → [tickets/dynamic-scope.md](tickets/dynamic-scope.md)
- [ ] SpawnPod が親の provider 設定を引き継ぐ → [tickets/spawn-inherit-provider.md](tickets/spawn-inherit-provider.md)
- [ ] ネイティブ GUI クライアント MVP → [tickets/native-gui-mvp.md](tickets/native-gui-mvp.md)
- [ ] TUI 拡充
- [ ] 新しい Pod を spawn する UI の設計 → [tickets/tui-pod-spawn-ui.md](tickets/tui-pod-spawn-ui.md)

View File

@ -194,7 +194,9 @@ impl Tool for SpawnPodTool {
}
};
let start_outcome = self.exec_child(&overlay_toml, &predicted_socket).await;
let start_outcome = self
.exec_child(&input.name, &overlay_toml, &predicted_socket)
.await;
if let Err(e) = start_outcome {
self.release_reservation(&lock_path, &input.name);
return Err(e);
@ -242,11 +244,31 @@ impl Tool for SpawnPodTool {
impl SpawnPodTool {
async fn exec_child(
&self,
pod_name: &str,
overlay_toml: &str,
predicted_socket: &Path,
) -> Result<(), ToolError> {
let pod_command = std::env::var("INSOMNIA_POD_COMMAND").unwrap_or_else(|_| "pod".into());
// Pre-create the child's runtime dir so we have a stable place to
// capture its stderr before it has had a chance to bind anything.
// The child's own `RuntimeDir::create` will `create_dir_all` the
// same path again — that's idempotent. On clean exit the child's
// RuntimeDir Drop tears the dir (and this log) down with it.
let pod_runtime_dir = self.runtime_base.join(pod_name);
tokio::fs::create_dir_all(&pod_runtime_dir)
.await
.map_err(|e| {
ToolError::ExecutionFailed(format!(
"create runtime dir {}: {e}",
pod_runtime_dir.display()
))
})?;
let stderr_path = pod_runtime_dir.join("stderr.log");
let stderr_file = std::fs::File::create(&stderr_path).map_err(|e| {
ToolError::ExecutionFailed(format!("open {}: {e}", stderr_path.display()))
})?;
let mut cmd = Command::new(&pod_command);
cmd.arg("--adopt")
.arg("--callback")
@ -256,7 +278,7 @@ impl SpawnPodTool {
.current_dir(&self.spawner_pwd)
.stdin(Stdio::null())
.stdout(Stdio::null())
.stderr(Stdio::null())
.stderr(Stdio::from(stderr_file))
.process_group(0);
let child = cmd
@ -269,7 +291,10 @@ impl SpawnPodTool {
// orphans. Lifecycle tracking lives in `spawned_pods.json`.
drop(child);
wait_for_socket(predicted_socket, SOCKET_WAIT_TIMEOUT).await
match wait_for_socket(predicted_socket, SOCKET_WAIT_TIMEOUT).await {
Ok(()) => Ok(()),
Err(e) => Err(annotate_with_stderr(e, &stderr_path).await),
}
}
fn release_reservation(&self, lock_path: &Path, pod_name: &str) {
@ -330,6 +355,33 @@ fn build_overlay_toml(
toml::to_string(&overlay)
}
/// Tail of the spawned child's `stderr.log` to splice into a startup
/// failure message. Capped so a chatty child can't blow up the LLM's
/// tool-result budget — debugging beyond this should read the file
/// directly.
const STDERR_TAIL_BYTES: usize = 4 * 1024;
async fn annotate_with_stderr(err: ToolError, stderr_path: &Path) -> ToolError {
let tail = match tokio::fs::read(stderr_path).await {
Ok(bytes) => {
let start = bytes.len().saturating_sub(STDERR_TAIL_BYTES);
String::from_utf8_lossy(&bytes[start..]).into_owned()
}
Err(_) => return err,
};
let trimmed = tail.trim();
if trimmed.is_empty() {
return err;
}
match err {
ToolError::ExecutionFailed(msg) => ToolError::ExecutionFailed(format!(
"{msg}\n--- child stderr ({}) ---\n{trimmed}",
stderr_path.display()
)),
other => other,
}
}
async fn wait_for_socket(path: &Path, timeout: Duration) -> Result<(), ToolError> {
let deadline = tokio::time::Instant::now() + timeout;
loop {

View File

@ -0,0 +1,28 @@
# SpawnPod が親の provider 設定を引き継ぐ
## 背景
`SpawnPod` ツールは子 Pod に渡す overlay TOML に `pod.name` / `worker.instruction` / `scope` のみを含め、`provider` を含めない(`crates/pod/src/spawn_pod.rs::build_overlay_toml`)。
子 Pod は起動時に manifest cascade を一からやり直すため、デフォルトのインストラクション (`$insomnia/default`) を使うと `provider.kind` が見つからず `failed to resolve manifest cascade: missing required field: provider.kind` で起動失敗する。結果として `SpawnPod` ツールがデフォルト引数では事実上使えない状態になっている。
## ゴール
`SpawnPod` で起動した子 Pod が、特に指定なしで親 Pod と同じ provider 設定kind, model, api_key 等)を使って起動できる。
## 必要な変更
- 親 Pod の resolved provider を `SpawnPodTool` から参照できるようにする(`SpawnPodTool::new` の引数追加 等)
- `build_overlay_toml` が overlay に provider を含める
- ツール入力 (`SpawnPodInput`) からの provider 指定は今回は不要(範囲外を参照)
## 完了条件
- `SpawnPod``provider` 指定なしで呼び出すと、子 Pod が親と同じ provider 設定で起動し、`Method::Run` を受けて応答できる
- 既存の spawn_pod 統合テスト (`crates/pod/tests/spawn_pod_test.rs`) が引き続きパス
- 親が provider を持たないケース(あり得るなら)の挙動が定まっている
## 範囲外
- ツール入力からの provider overrideモデルだけ差し替えたい等のマルチエージェント用途。将来別チケットで `SpawnPodInput` に optional provider override を追加する形で扱う
- インストラクションファイル側で provider を指定する仕組み