diff --git a/.gitignore b/.gitignore index 0de651ce..ef0d23e4 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ .direnv *.local* .env +.worktree diff --git a/TODO.md b/TODO.md index 4b1d93af..028ae843 100644 --- a/TODO.md +++ b/TODO.md @@ -2,6 +2,7 @@ - [ ] 内部 Worker / 内部 Pod の Workflow 化 → [tickets/internal-worker-workflow.md](tickets/internal-worker-workflow.md) - [ ] Agent Skills を Workflow として ingest → [tickets/agent-skills.md](tickets/agent-skills.md) - [ ] パーミッション: パターンベースのツール実行制御 → [tickets/permission-extension-point.md](tickets/permission-extension-point.md) +- [ ] Resume 時の Scope claim の改善 → [tickets/resume-scope-claim.md](tickets/resume-scope-claim.md) - [ ] Pod CLI: マニフェスト関連フラグの整理 → [tickets/pod-cli-manifest-flags.md](tickets/pod-cli-manifest-flags.md) - [ ] OpenAI Responses: sampling パラメータの取り扱い → [tickets/responses-sampling-params.md](tickets/responses-sampling-params.md) - [ ] llm-worker のエラー耐性 diff --git a/crates/tui/src/spawn.rs b/crates/tui/src/spawn.rs index 2cebe80b..b8829e48 100644 --- a/crates/tui/src/spawn.rs +++ b/crates/tui/src/spawn.rs @@ -268,12 +268,11 @@ async fn wait_for_ready( form: &mut Form, overlay_toml: &str, ) -> Result { - let (pod_bin, pod_args) = resolve_pod_command(); + let pod_bin = resolve_pod_command(); let cwd = std::env::current_dir().map_err(SpawnError::Io)?; let mut command = Command::new(&pod_bin); command - .args(&pod_args) .arg("--overlay") .arg(overlay_toml) .current_dir(&cwd) @@ -375,28 +374,21 @@ fn build_overlay_toml(form: &Form) -> String { toml::to_string(&toml::Value::Table(root)).expect("overlay serialisation cannot fail") } -/// Resolves the program (and any leading args) used to launch a child Pod. +/// Resolves the binary used to launch a child Pod. Must point at a +/// `pod`-compatible executable — the parent reads the child's stderr +/// directly looking for `INSOMNIA-READY`, so any wrapper that emits +/// extra lines on stderr will pollute that handshake. /// -/// `INSOMNIA_POD_COMMAND` is split on whitespace so devshells can point it -/// at e.g. `cargo run -p pod --quiet --`; the first token is the program -/// and the rest are prepended before `--overlay` and friends. -fn resolve_pod_command() -> (PathBuf, Vec) { +/// `INSOMNIA_POD_COMMAND` overrides the lookup (used by tests to inject +/// a mock binary). Otherwise we defer to `PATH` — missing binary +/// surfaces as the spawn `io::Error`. +fn resolve_pod_command() -> PathBuf { if let Ok(cmd) = std::env::var("INSOMNIA_POD_COMMAND") { - let mut tokens = cmd.split_whitespace(); - if let Some(program) = tokens.next() { - let args = tokens.map(str::to_owned).collect(); - return (PathBuf::from(program), args); + if !cmd.is_empty() { + return PathBuf::from(cmd); } } - if let Ok(exe) = std::env::current_exe() { - if let Some(dir) = exe.parent() { - let candidate = dir.join("pod"); - if candidate.is_file() { - return (candidate, Vec::new()); - } - } - } - (PathBuf::from("pod"), Vec::new()) + PathBuf::from("pod") } struct StderrTail { diff --git a/devshell.nix b/devshell.nix index 275bbe00..38a6912d 100644 --- a/devshell.nix +++ b/devshell.nix @@ -1,4 +1,23 @@ { pkgs }: +let + # Dev-only wrapper. tui の spawn 経路は `pod` バイナリを直に exec し、 + # stderr の `INSOMNIA-READY` 行で握手するので、cargo の進捗や rustc の + # warning が混ざると tail に余計な行が積もり本当のエラーが押し出される。 + # ここで一度ビルドを切り離し、成功時はビルド出力を一切捨てて素のバイナリ + # を exec、失敗時のみ build log を stderr に流して exit する。 + pod-dev = pkgs.writeShellScriptBin "pod" '' + set -u + buildlog=$(mktemp) + trap 'rm -f "$buildlog"' EXIT + if ! cargo build --quiet -p pod 2>"$buildlog"; then + cat "$buildlog" >&2 + exit 1 + fi + manifest=$(cargo locate-project --workspace --message-format plain 2>/dev/null) + target_dir=''${CARGO_TARGET_DIR:-$(dirname "$manifest")/target} + exec "$target_dir/debug/pod" "$@" + ''; +in pkgs.mkShell { packages = with pkgs; [ nixfmt @@ -6,12 +25,12 @@ pkgs.mkShell { git rustc cargo + pod-dev ]; buildInputs = with pkgs; [ pkg-config openssl ]; - INSOMNIA_POD_COMMAND = "cargo run -p pod --quiet --"; shellHook = '' echo "dev-shell-loaded" ''; diff --git a/tickets/resume-scope-claim.md b/tickets/resume-scope-claim.md new file mode 100644 index 00000000..48f0d896 --- /dev/null +++ b/tickets/resume-scope-claim.md @@ -0,0 +1,40 @@ +# Resume 時の Scope Claim の改善 + +## 背景 + +`tickets/dynamic-scope.md` で in-process Scope の縮小(SpawnPod による委譲時の Write revoke)と pod-registry 上の delegation 記録が揃った。これにより「セッション中に scope が縮む」状態を Pod / registry の双方が一貫して表現できる。 + +一方で `tui -r` 経由の resume は、`crates/tui/src/spawn.rs` の `build_overlay_toml` を通じて fresh spawn と同じロジックで overlay を合成する。manifest cascade に scope 宣言が無い場合、cwd 直下に `write` 再帰の rule を毎回付ける挙動。 + +このため次のような衝突が起きる: + +- セッション S が稼働中に SpawnPod で子 C を作り、cwd 配下のサブパスを委譲した +- 親が exit、子 C は registry 上にエントリが残存(あるいはまだ稼働中) +- ユーザーが S を resume しようとすると、新しい Pod が cwd 全体に `write` を claim → 委譲された部分と overlap して registry が拒否 + +resume の意図は「過去のセッションの続きを取る」であって「過去の effective scope より広い範囲を新たに掴み直す」ではない。現状は後者になっており、過去に手放した scope を resume が勝手に取り戻そうとする形になっている。 + +## ゴール + +セッション resume 時に claim する scope が、当該セッションが最後に持っていた effective scope に揃う。委譲済み・他 Pod が保持中の部分は claim 対象から外れ、resume された Pod は当時と同じ範囲だけで動作する。 + +## 要件 + +- resume 時の overlay 合成は cwd 盲信ではなく、当該セッションが過去に持っていた scope を反映する。情報源は session log / registry / その他のいずれでも良いが、何らかの永続情報から復元できること +- 過去の scope 情報が取得できないセッション(旧形式 / 破損)は、明示的なエラーで止めるか、ユーザーに確認させてから fresh claim にフォールバックする(黙って広げない) +- claim 試行が registry の既存 allocation と衝突した場合、エラーメッセージで衝突相手の Pod 名 と target rule の双方が伝わる(現状は Pod 名のみ) +- 委譲済みエントリ(`delegated_from` を持つ allocation)が同じセッションの委譲チェーンに属する場合、resume はその範囲を claim せずに進行する + +## 完了条件 + +- 「親 Pod がセッション中に SpawnPod を実行 → 子に委譲 → 親 exit → 親セッションを resume」のフローが、既存子 allocation を残したまま衝突なしで成功する +- 既存の無関係な Pod と衝突するケースは、衝突 rule と相手 Pod 名を含む明確なエラーで失敗する +- 単体テスト or 統合テストで上記 2 ケースが検証される +- 既存の fresh spawn (resume なし) の挙動には変化なし + +## 範囲外 + +- 過去スコープの永続化スキーマを新規導入するかの判断は実装時に決める(session log の既存フィールドで足りるなら追加しない) +- 自動的に既存 Pod を kill / reclaim して claim を通す挙動 +- protocol 経由の外部からの GrantScope / RevokeScope(`tickets/dynamic-scope.md` の範囲外宣言を継承) +- registry 側のエラー型の全面再設計(rule 情報を含めるための最小限の拡張のみで足りる想定)