oh-my-codex 0.20.2 → 0.20.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +7 -6
- package/Cargo.toml +1 -1
- package/README.md +26 -0
- package/crates/omx-runtime/Cargo.toml +1 -0
- package/crates/omx-runtime/src/main.rs +25 -2
- package/crates/omx-runtime/tests/execution.rs +219 -0
- package/crates/omx-runtime-core/src/dispatch.rs +96 -10
- package/crates/omx-runtime-core/src/engine.rs +839 -29
- package/crates/omx-runtime-core/src/lib.rs +8 -0
- package/dist/agents/__tests__/definitions.test.js +81 -0
- package/dist/agents/__tests__/definitions.test.js.map +1 -1
- package/dist/agents/__tests__/native-config.test.js +25 -0
- package/dist/agents/__tests__/native-config.test.js.map +1 -1
- package/dist/agents/native-config.d.ts +2 -1
- package/dist/agents/native-config.d.ts.map +1 -1
- package/dist/agents/native-config.js.map +1 -1
- package/dist/auth/hotswap.d.ts +6 -0
- package/dist/auth/hotswap.d.ts.map +1 -1
- package/dist/auth/hotswap.js +85 -15
- package/dist/auth/hotswap.js.map +1 -1
- package/dist/autopilot/__tests__/ralplan-gate.test.js +1 -1
- package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
- package/dist/cli/__tests__/auth.test.js +60 -6
- package/dist/cli/__tests__/auth.test.js.map +1 -1
- package/dist/cli/__tests__/codex-plugin-layout.test.js +80 -0
- package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-warning-copy.test.js +103 -1
- package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
- package/dist/cli/__tests__/exec.test.js +136 -0
- package/dist/cli/__tests__/exec.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +258 -4
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/launch-fallback.test.js +287 -1
- package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
- package/dist/cli/__tests__/mcp-parity.test.js +4 -1
- package/dist/cli/__tests__/mcp-parity.test.js.map +1 -1
- package/dist/cli/__tests__/native-hook-claim-journal.test.js +84 -6
- package/dist/cli/__tests__/native-hook-claim-journal.test.js.map +1 -1
- package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +3 -3
- package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -1
- package/dist/cli/__tests__/ralph.test.js +3 -3
- package/dist/cli/__tests__/ralph.test.js.map +1 -1
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js +56 -171
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js.map +1 -1
- package/dist/cli/__tests__/ralplan.test.js +37 -111
- package/dist/cli/__tests__/ralplan.test.js.map +1 -1
- package/dist/cli/__tests__/resume.test.js +11 -10
- package/dist/cli/__tests__/resume.test.js.map +1 -1
- package/dist/cli/__tests__/session-search-help.test.js +25 -1
- package/dist/cli/__tests__/session-search-help.test.js.map +1 -1
- package/dist/cli/__tests__/setup-install-mode.test.js +42 -4
- package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
- package/dist/cli/__tests__/team.test.js +105 -7
- package/dist/cli/__tests__/team.test.js.map +1 -1
- package/dist/cli/__tests__/uninstall.test.js +46 -0
- package/dist/cli/__tests__/uninstall.test.js.map +1 -1
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js +2 -1
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
- package/dist/cli/doctor.d.ts +3 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +15 -2
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.d.ts +4 -1
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +138 -38
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp-parity.js +2 -2
- package/dist/cli/mcp-parity.js.map +1 -1
- package/dist/cli/native-hook-claim-journal.d.ts +16 -5
- package/dist/cli/native-hook-claim-journal.d.ts.map +1 -1
- package/dist/cli/native-hook-claim-journal.js +42 -29
- package/dist/cli/native-hook-claim-journal.js.map +1 -1
- package/dist/cli/ralph.js +2 -2
- package/dist/cli/ralph.js.map +1 -1
- package/dist/cli/ralplan.d.ts +4 -8
- package/dist/cli/ralplan.d.ts.map +1 -1
- package/dist/cli/ralplan.js +19 -112
- package/dist/cli/ralplan.js.map +1 -1
- package/dist/cli/session-search.d.ts +5 -0
- package/dist/cli/session-search.d.ts.map +1 -1
- package/dist/cli/session-search.js +87 -0
- package/dist/cli/session-search.js.map +1 -1
- package/dist/cli/setup.d.ts +5 -0
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +80 -31
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/uninstall.d.ts +9 -0
- package/dist/cli/uninstall.d.ts.map +1 -1
- package/dist/cli/uninstall.js +61 -19
- package/dist/cli/uninstall.js.map +1 -1
- package/dist/compat/__tests__/rust-runtime-compat.test.js +1 -1
- package/dist/config/__tests__/generator-idempotent.test.js +330 -1
- package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
- package/dist/config/__tests__/generator-notify.test.js +2 -2
- package/dist/config/__tests__/generator-notify.test.js.map +1 -1
- package/dist/config/__tests__/models.test.js +67 -3
- package/dist/config/__tests__/models.test.js.map +1 -1
- package/dist/config/generator.d.ts +4 -20
- package/dist/config/generator.d.ts.map +1 -1
- package/dist/config/generator.js +499 -119
- package/dist/config/generator.js.map +1 -1
- package/dist/config/models.d.ts +12 -2
- package/dist/config/models.d.ts.map +1 -1
- package/dist/config/models.js +15 -1
- package/dist/config/models.js.map +1 -1
- package/dist/hooks/__tests__/agents-overlay.test.js +2 -2
- package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
- package/dist/hooks/__tests__/keyword-detector.test.js +4 -4
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js +238 -543
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +38 -4
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +96 -9
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +531 -12
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +155 -23
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js +482 -32
- package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +68 -1
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js +7 -1
- package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +43 -4
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/session.test.js +348 -2
- package/dist/hooks/__tests__/session.test.js.map +1 -1
- package/dist/hooks/agents-overlay.js +1 -1
- package/dist/hooks/session.d.ts +26 -2
- package/dist/hooks/session.d.ts.map +1 -1
- package/dist/hooks/session.js +169 -13
- package/dist/hooks/session.js.map +1 -1
- package/dist/leader/__tests__/contract.test.js +57 -7
- package/dist/leader/__tests__/contract.test.js.map +1 -1
- package/dist/leader/contract.d.ts +16 -0
- package/dist/leader/contract.d.ts.map +1 -1
- package/dist/leader/contract.js +79 -4
- package/dist/leader/contract.js.map +1 -1
- package/dist/mcp/__tests__/code-intel-server.test.js +31 -3
- package/dist/mcp/__tests__/code-intel-server.test.js.map +1 -1
- package/dist/mcp/__tests__/wiki-server.test.js +18 -0
- package/dist/mcp/__tests__/wiki-server.test.js.map +1 -1
- package/dist/mcp/code-intel-server.d.ts.map +1 -1
- package/dist/mcp/code-intel-server.js +13 -3
- package/dist/mcp/code-intel-server.js.map +1 -1
- package/dist/mcp/wiki-server.d.ts +11 -0
- package/dist/mcp/wiki-server.d.ts.map +1 -1
- package/dist/mcp/wiki-server.js +9 -2
- package/dist/mcp/wiki-server.js.map +1 -1
- package/dist/modes/__tests__/base-tmux-pane.test.js +4 -4
- package/dist/modes/__tests__/base-tmux-pane.test.js.map +1 -1
- package/dist/notifications/temp-contract.d.ts.map +1 -1
- package/dist/notifications/temp-contract.js +4 -0
- package/dist/notifications/temp-contract.js.map +1 -1
- package/dist/pipeline/__tests__/orchestrator.test.js +4 -4
- package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
- package/dist/pipeline/__tests__/stages.test.js +8 -8
- package/dist/pipeline/__tests__/stages.test.js.map +1 -1
- package/dist/ralplan/__tests__/consensus-gate.test.js +134 -10
- package/dist/ralplan/__tests__/consensus-gate.test.js.map +1 -1
- package/dist/ralplan/__tests__/documented-leader-preflight.test.d.ts +2 -0
- package/dist/ralplan/__tests__/documented-leader-preflight.test.d.ts.map +1 -0
- package/dist/ralplan/__tests__/documented-leader-preflight.test.js +46 -0
- package/dist/ralplan/__tests__/documented-leader-preflight.test.js.map +1 -0
- package/dist/ralplan/__tests__/runtime.test.js +8 -4
- package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
- package/dist/ralplan/consensus-gate.d.ts.map +1 -1
- package/dist/ralplan/consensus-gate.js +58 -50
- package/dist/ralplan/consensus-gate.js.map +1 -1
- package/dist/ralplan/documented-leader-preflight.d.ts +34 -0
- package/dist/ralplan/documented-leader-preflight.d.ts.map +1 -0
- package/dist/ralplan/documented-leader-preflight.js +58 -0
- package/dist/ralplan/documented-leader-preflight.js.map +1 -0
- package/dist/ralplan/runtime.d.ts +1 -0
- package/dist/ralplan/runtime.d.ts.map +1 -1
- package/dist/ralplan/runtime.js +11 -7
- package/dist/ralplan/runtime.js.map +1 -1
- package/dist/runtime/__tests__/bridge.test.js +42 -1
- package/dist/runtime/__tests__/bridge.test.js.map +1 -1
- package/dist/runtime/bridge.d.ts +12 -0
- package/dist/runtime/bridge.d.ts.map +1 -1
- package/dist/runtime/bridge.js +52 -1
- package/dist/runtime/bridge.js.map +1 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js +2620 -487
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/__tests__/reasoning-artifact-contract.test.d.ts +2 -0
- package/dist/scripts/__tests__/reasoning-artifact-contract.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/reasoning-artifact-contract.test.js +108 -0
- package/dist/scripts/__tests__/reasoning-artifact-contract.test.js.map +1 -0
- package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js +46 -173
- package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js.map +1 -1
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js +39 -159
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js.map +1 -1
- package/dist/scripts/__tests__/smoke-packed-install.test.js +254 -2
- package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
- package/dist/scripts/codex-native-hook.d.ts +4 -1
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +11013 -1153
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/scripts/notify-fallback-watcher.js +82 -108
- package/dist/scripts/notify-fallback-watcher.js.map +1 -1
- package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
- package/dist/scripts/notify-hook/auto-nudge.js +74 -3
- package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
- package/dist/scripts/notify-hook/team-dispatch.d.ts +9 -0
- package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-dispatch.js +234 -79
- package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
- package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-leader-nudge.js +39 -13
- package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.d.ts +12 -4
- package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.js +365 -44
- package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
- package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-worker-stop.js +19 -18
- package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
- package/dist/scripts/notify-hook/team-worker.d.ts +3 -0
- package/dist/scripts/notify-hook/team-worker.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-worker.js +37 -28
- package/dist/scripts/notify-hook/team-worker.js.map +1 -1
- package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
- package/dist/scripts/notify-hook/tmux-injection.js +28 -2
- package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
- package/dist/scripts/notify-hook.js +1 -0
- package/dist/scripts/notify-hook.js.map +1 -1
- package/dist/scripts/smoke-packed-install.d.ts +48 -3
- package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
- package/dist/scripts/smoke-packed-install.js +2796 -13
- package/dist/scripts/smoke-packed-install.js.map +1 -1
- package/dist/state/__tests__/mode-state-context.test.js +54 -0
- package/dist/state/__tests__/mode-state-context.test.js.map +1 -1
- package/dist/state/__tests__/operations.test.js +2 -0
- package/dist/state/__tests__/operations.test.js.map +1 -1
- package/dist/state/mode-state-context.d.ts +5 -0
- package/dist/state/mode-state-context.d.ts.map +1 -1
- package/dist/state/mode-state-context.js +59 -0
- package/dist/state/mode-state-context.js.map +1 -1
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.js +24 -245
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.js.map +1 -1
- package/dist/subagents/tracker.d.ts +1 -82
- package/dist/subagents/tracker.d.ts.map +1 -1
- package/dist/subagents/tracker.js +3 -275
- package/dist/subagents/tracker.js.map +1 -1
- package/dist/team/__tests__/api-interop.test.js +91 -1
- package/dist/team/__tests__/api-interop.test.js.map +1 -1
- package/dist/team/__tests__/delivery-e2e-smoke.test.js +52 -10
- package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
- package/dist/team/__tests__/idle-nudge.test.js +45 -15
- package/dist/team/__tests__/idle-nudge.test.js.map +1 -1
- package/dist/team/__tests__/mcp-comm.test.js +93 -27
- package/dist/team/__tests__/mcp-comm.test.js.map +1 -1
- package/dist/team/__tests__/model-contract.test.js +110 -3
- package/dist/team/__tests__/model-contract.test.js.map +1 -1
- package/dist/team/__tests__/runtime-cli.test.js +9 -0
- package/dist/team/__tests__/runtime-cli.test.js.map +1 -1
- package/dist/team/__tests__/runtime.test.js +3204 -335
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/scaling.test.js +2359 -142
- package/dist/team/__tests__/scaling.test.js.map +1 -1
- package/dist/team/__tests__/state.test.js +804 -36
- package/dist/team/__tests__/state.test.js.map +1 -1
- package/dist/team/__tests__/team-ops-contract.test.js +6 -0
- package/dist/team/__tests__/team-ops-contract.test.js.map +1 -1
- package/dist/team/__tests__/tmux-session.test.js +3049 -784
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/__tests__/worker-runtime-identity.test.js +38 -2
- package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
- package/dist/team/__tests__/worktree.test.js +6 -0
- package/dist/team/__tests__/worktree.test.js.map +1 -1
- package/dist/team/api-interop.d.ts.map +1 -1
- package/dist/team/api-interop.js +15 -9
- package/dist/team/api-interop.js.map +1 -1
- package/dist/team/exact-pane.d.ts +23 -0
- package/dist/team/exact-pane.d.ts.map +1 -0
- package/dist/team/exact-pane.js +133 -0
- package/dist/team/exact-pane.js.map +1 -0
- package/dist/team/idle-nudge.d.ts +12 -1
- package/dist/team/idle-nudge.d.ts.map +1 -1
- package/dist/team/idle-nudge.js +41 -14
- package/dist/team/idle-nudge.js.map +1 -1
- package/dist/team/model-contract.d.ts +2 -5
- package/dist/team/model-contract.d.ts.map +1 -1
- package/dist/team/model-contract.js +59 -17
- package/dist/team/model-contract.js.map +1 -1
- package/dist/team/runtime.d.ts +1 -0
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +1244 -180
- package/dist/team/runtime.js.map +1 -1
- package/dist/team/scaling.d.ts +13 -1
- package/dist/team/scaling.d.ts.map +1 -1
- package/dist/team/scaling.js +1430 -384
- package/dist/team/scaling.js.map +1 -1
- package/dist/team/state/dispatch.d.ts.map +1 -1
- package/dist/team/state/dispatch.js +26 -8
- package/dist/team/state/dispatch.js.map +1 -1
- package/dist/team/state/locks.d.ts.map +1 -1
- package/dist/team/state/locks.js +61 -13
- package/dist/team/state/locks.js.map +1 -1
- package/dist/team/state/tasks.d.ts.map +1 -1
- package/dist/team/state/tasks.js +4 -3
- package/dist/team/state/tasks.js.map +1 -1
- package/dist/team/state.d.ts +67 -1
- package/dist/team/state.d.ts.map +1 -1
- package/dist/team/state.js +636 -129
- package/dist/team/state.js.map +1 -1
- package/dist/team/team-ops.d.ts +5 -0
- package/dist/team/team-ops.d.ts.map +1 -1
- package/dist/team/team-ops.js +4 -0
- package/dist/team/team-ops.js.map +1 -1
- package/dist/team/tmux-session.d.ts +123 -23
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +1250 -291
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/team/worktree.d.ts.map +1 -1
- package/dist/team/worktree.js +4 -0
- package/dist/team/worktree.js.map +1 -1
- package/dist/utils/__tests__/file-durability.test.d.ts +2 -0
- package/dist/utils/__tests__/file-durability.test.d.ts.map +1 -0
- package/dist/utils/__tests__/file-durability.test.js +57 -0
- package/dist/utils/__tests__/file-durability.test.js.map +1 -0
- package/dist/utils/file-durability.d.ts +15 -0
- package/dist/utils/file-durability.d.ts.map +1 -0
- package/dist/utils/file-durability.js +37 -0
- package/dist/utils/file-durability.js.map +1 -0
- package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js +3 -0
- package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js.map +1 -1
- package/dist/wiki/lint.d.ts +4 -1
- package/dist/wiki/lint.d.ts.map +1 -1
- package/dist/wiki/lint.js +3 -2
- package/dist/wiki/lint.js.map +1 -1
- package/package.json +1 -1
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +24 -0
- package/plugins/oh-my-codex/skills/ralph/SKILL.md +6 -4
- package/plugins/oh-my-codex/skills/ralplan/SKILL.md +3 -2
- package/plugins/oh-my-codex/skills/team/SKILL.md +13 -9
- package/skills/ralph/SKILL.md +6 -4
- package/skills/ralplan/SKILL.md +3 -2
- package/skills/team/SKILL.md +13 -9
- package/src/scripts/__tests__/codex-native-hook.test.ts +2826 -497
- package/src/scripts/__tests__/reasoning-artifact-contract.test.ts +129 -0
- package/src/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.ts +48 -185
- package/src/scripts/__tests__/role-intent-durable-recovery-3181.test.ts +44 -177
- package/src/scripts/__tests__/smoke-packed-install.test.ts +320 -2
- package/src/scripts/codex-native-hook.ts +10715 -1069
- package/src/scripts/notify-fallback-watcher.ts +84 -115
- package/src/scripts/notify-hook/auto-nudge.ts +71 -3
- package/src/scripts/notify-hook/team-dispatch.ts +242 -75
- package/src/scripts/notify-hook/team-leader-nudge.ts +40 -18
- package/src/scripts/notify-hook/team-tmux-guard.ts +366 -39
- package/src/scripts/notify-hook/team-worker-stop.ts +20 -16
- package/src/scripts/notify-hook/team-worker.ts +39 -26
- package/src/scripts/notify-hook/tmux-injection.ts +28 -2
- package/src/scripts/notify-hook.ts +1 -0
- package/src/scripts/smoke-packed-install.ts +3196 -14
- package/dist/subagents/__tests__/adapted-role-binding.test.d.ts +0 -2
- package/dist/subagents/__tests__/adapted-role-binding.test.d.ts.map +0 -1
- package/dist/subagents/__tests__/adapted-role-binding.test.js +0 -988
- package/dist/subagents/__tests__/adapted-role-binding.test.js.map +0 -1
- package/dist/subagents/adapted-role-binding.d.ts +0 -17
- package/dist/subagents/adapted-role-binding.d.ts.map +0 -1
- package/dist/subagents/adapted-role-binding.js +0 -75
- package/dist/subagents/adapted-role-binding.js.map +0 -1
package/dist/team/runtime.js
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
|
+
import { createHash } from 'crypto';
|
|
1
2
|
import { join, resolve, dirname } from 'path';
|
|
2
|
-
import { existsSync, appendFileSync, mkdirSync } from 'fs';
|
|
3
|
+
import { existsSync, appendFileSync, mkdirSync, readFileSync } from 'fs';
|
|
3
4
|
import { mkdir, readdir, readFile, writeFile } from 'fs/promises';
|
|
4
5
|
import { performance } from 'perf_hooks';
|
|
5
6
|
import { spawn, spawnSync } from 'child_process';
|
|
6
|
-
import { sanitizeTeamName, isTmuxAvailable, hasCurrentTmuxClientContext, createTeamSession, buildWorkerProcessLaunchSpec, scrubTeamWorkerHudOwnershipEnv, resolveTeamWorkerCli, resolveTeamWorkerCliForResolvedLaunchArgs, assertTeamWorkerCliPolicyCompatibility, resolveTeamWorkerLaunchMode, waitForWorkerReady, waitForWorkerReadyAsync, dismissTrustPromptIfPresent, evaluateStartupDirectTriggerSafety, sendToWorker, sendToWorkerStdin, isWorkerAlive, isWorkerPaneOpen,
|
|
7
|
-
import {
|
|
7
|
+
import { sanitizeTeamName, isTmuxAvailable, hasCurrentTmuxClientContext, createTeamSession, CreateTeamSessionPartialError, buildWorkerProcessLaunchSpec, scrubTeamWorkerHudOwnershipEnv, resolveTeamWorkerCli, resolveTeamWorkerCliForResolvedLaunchArgs, assertTeamWorkerCliPolicyCompatibility, resolveTeamWorkerLaunchMode, waitForWorkerReady, waitForWorkerReadyAsync, dismissTrustPromptIfPresent, evaluateStartupDirectTriggerSafety, sendToWorker, sendToWorkerStdin, isWorkerAlive, isWorkerPaneOpen, paneHasOmxInstanceTag, readPaneTeamOwnerTagResult, restoreStandaloneHudPane, finalizeRestoredHudCleanupDebtSync, reconcileRestoredHudCleanupDebtSync, teardownWorkerPanes, unregisterResizeHook, queryDetachedTeamSession, requestDetachedTeamSessionDestroy, queryDetachedSessionLeaderBinding, listPaneIds, listTeamSessions, resolveSharedSessionShutdownTopology, } from './tmux-session.js';
|
|
8
|
+
import { readExactPaneProofSync, readExactPaneProofsSync } from './exact-pane.js';
|
|
9
|
+
import { reconcileScaleDownCleanupDebt } from './scaling.js';
|
|
10
|
+
import { teamInit as initTeamState, DEFAULT_MAX_WORKERS, teamReadConfig as readTeamConfig, teamWriteWorkerIdentity as writeWorkerIdentity, teamReadWorkerHeartbeat as readWorkerHeartbeat, teamReadWorkerStatus as readWorkerStatus, teamWriteWorkerInbox as writeWorkerInbox, teamCreateTask as createStateTask, teamReadTask as readTask, teamListTasks as listTasks, teamUpdateTask as updateTask, teamReadManifest as readTeamManifestV2, teamNormalizeGovernance as normalizeTeamGovernance, teamNormalizePolicy as normalizeTeamPolicy, teamClaimTask as claimTask, teamReleaseTaskClaim as releaseTaskClaim, teamReclaimExpiredTaskClaim as reclaimExpiredTaskClaim, teamAppendEvent as appendTeamEvent, teamReadTaskApproval as readTaskApproval, teamListMailbox as listMailboxMessages, teamMarkMessageDelivered as markMessageDelivered, teamMarkMessageNotified as markMessageNotified, teamEnqueueDispatchRequest as enqueueDispatchRequest, teamMarkDispatchRequestNotified as markDispatchRequestNotified, teamTransitionDispatchRequest as transitionDispatchRequest, teamReadDispatchRequest as readDispatchRequest, teamCleanup as cleanupTeamState, teamSaveConfig as saveTeamConfig, teamWriteShutdownRequest as writeShutdownRequest, teamReadShutdownAck as readShutdownAck, teamReadMonitorSnapshot as readMonitorSnapshot, teamWriteMonitorSnapshot as writeMonitorSnapshot, teamReadPhase as readTeamPhaseState, teamWritePhase as writeTeamPhaseState, teamWriteWorkerStatus as writeWorkerStatus, writeAtomic, teamWithTaskMembershipBarrier as withTeamTaskMembershipBarrier, teamRemoveDurableFile as removeDurableFile, } from './team-ops.js';
|
|
11
|
+
import { commitTeamMembershipTaskTransaction, writeTeamManifestV2 as writeTeamManifestV2State } from './state.js';
|
|
8
12
|
import { queueInboxInstruction, queueDirectMailboxMessage, queueBroadcastMailboxMessage, waitForDispatchReceipt, } from './mcp-comm.js';
|
|
9
13
|
import { appendTeamDeliveryLogForCwd } from './delivery-log.js';
|
|
10
14
|
import { remapRepoAwareDecompositionMetadataToCreatedTasks, } from './repo-aware-decomposition.js';
|
|
@@ -109,6 +113,40 @@ async function syncTeamModeStateOnShutdown(teamName, cwd, leaderSessionId) {
|
|
|
109
113
|
await syncExactTeamModeStateOnShutdown(teamName, cwd, normalizedLeaderSessionId);
|
|
110
114
|
}
|
|
111
115
|
}
|
|
116
|
+
async function mutateShutdownConfig(config, cwd, mutate) {
|
|
117
|
+
return await withTeamTaskMembershipBarrier(config.name, cwd, async () => {
|
|
118
|
+
const current = await readTeamConfig(config.name, cwd);
|
|
119
|
+
if (!current)
|
|
120
|
+
throw new Error(`shutdown_config_missing:${config.name}`);
|
|
121
|
+
const baseGeneration = current.config_generation ?? 0;
|
|
122
|
+
const teamRoot = join(current.team_state_root ?? resolveCanonicalTeamStateRoot(cwd), 'team', current.name);
|
|
123
|
+
const [currentConfigBytes, currentManifestBytes] = await Promise.all([
|
|
124
|
+
readFile(join(teamRoot, 'config.json'), 'utf8'),
|
|
125
|
+
existsSync(join(teamRoot, 'manifest.v2.json')) ? readFile(join(teamRoot, 'manifest.v2.json'), 'utf8') : null,
|
|
126
|
+
]);
|
|
127
|
+
mutate(current);
|
|
128
|
+
const currentManifest = currentManifestBytes === null ? null : JSON.parse(currentManifestBytes);
|
|
129
|
+
await commitTeamMembershipTaskTransaction(current.name, cwd, {
|
|
130
|
+
baseGeneration,
|
|
131
|
+
tasks: [],
|
|
132
|
+
config: { oldBytes: currentConfigBytes, newBytes: JSON.stringify(current, null, 2) },
|
|
133
|
+
manifest: {
|
|
134
|
+
oldBytes: currentManifestBytes,
|
|
135
|
+
newBytes: currentManifest === null ? null : JSON.stringify({
|
|
136
|
+
...currentManifest,
|
|
137
|
+
hud_pane_id: current.hud_pane_id,
|
|
138
|
+
hud_pane_pid: current.hud_pane_pid,
|
|
139
|
+
resize_hook_name: current.resize_hook_name,
|
|
140
|
+
resize_hook_target: current.resize_hook_target,
|
|
141
|
+
}, null, 2),
|
|
142
|
+
},
|
|
143
|
+
});
|
|
144
|
+
const committed = await readTeamConfig(current.name, cwd);
|
|
145
|
+
if (!committed)
|
|
146
|
+
throw new Error(`shutdown_config_missing_after_commit:${current.name}`);
|
|
147
|
+
return committed;
|
|
148
|
+
});
|
|
149
|
+
}
|
|
112
150
|
async function assertTeamStartupIsNonDestructive(teamName, cwd, leaderSessionId) {
|
|
113
151
|
const activeTeams = await findActiveTeams(cwd, leaderSessionId);
|
|
114
152
|
if (activeTeams.length > 0) {
|
|
@@ -131,16 +169,44 @@ async function assertTeamStartupIsNonDestructive(teamName, cwd, leaderSessionId)
|
|
|
131
169
|
}
|
|
132
170
|
export function applyCreatedInteractiveSessionToConfig(config, createdSession, workerPaneIds) {
|
|
133
171
|
config.tmux_session = createdSession.name;
|
|
172
|
+
config.tmux_session_id = createdSession.tmuxSessionId;
|
|
173
|
+
config.tmux_session_created = createdSession.tmuxSessionCreated;
|
|
134
174
|
config.leader_pane_id = createdSession.leaderPaneId;
|
|
135
175
|
config.hud_pane_id = createdSession.hudPaneId;
|
|
176
|
+
config.leader_pane_pid = typeof createdSession.leaderPanePid === 'number' && Number.isSafeInteger(createdSession.leaderPanePid) && createdSession.leaderPanePid > 0
|
|
177
|
+
? createdSession.leaderPanePid
|
|
178
|
+
: null;
|
|
179
|
+
config.hud_pane_pid = typeof createdSession.hudPanePid === 'number' && Number.isSafeInteger(createdSession.hudPanePid) && createdSession.hudPanePid > 0
|
|
180
|
+
? createdSession.hudPanePid
|
|
181
|
+
: null;
|
|
136
182
|
config.tmux_pane_owner_id = createdSession.teamPaneOwnerId;
|
|
137
183
|
config.resize_hook_name = createdSession.resizeHookName;
|
|
138
184
|
config.resize_hook_target = createdSession.resizeHookTarget;
|
|
185
|
+
const paneIdsByIndex = createdSession.workerPaneIdsByIndex;
|
|
186
|
+
const panePidsByIndex = createdSession.workerPanePidsByIndex;
|
|
187
|
+
if (paneIdsByIndex) {
|
|
188
|
+
for (let i = 0; i < paneIdsByIndex.length; i++) {
|
|
189
|
+
const paneId = paneIdsByIndex[i];
|
|
190
|
+
if (!paneId)
|
|
191
|
+
continue;
|
|
192
|
+
workerPaneIds[i] = paneId;
|
|
193
|
+
if (config.workers[i]) {
|
|
194
|
+
config.workers[i].pane_id = paneId;
|
|
195
|
+
const panePid = panePidsByIndex?.[i];
|
|
196
|
+
if (typeof panePid === 'number' && Number.isSafeInteger(panePid) && panePid > 0)
|
|
197
|
+
config.workers[i].pid = panePid;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
139
202
|
for (let i = 0; i < createdSession.workerPaneIds.length; i++) {
|
|
140
203
|
const paneId = createdSession.workerPaneIds[i];
|
|
141
204
|
workerPaneIds[i] = paneId;
|
|
142
205
|
if (config.workers[i]) {
|
|
143
206
|
config.workers[i].pane_id = paneId;
|
|
207
|
+
const panePid = createdSession.workerPanePidsByIndex?.[i];
|
|
208
|
+
if (typeof panePid === 'number' && Number.isSafeInteger(panePid) && panePid > 0)
|
|
209
|
+
config.workers[i].pid = panePid;
|
|
144
210
|
}
|
|
145
211
|
}
|
|
146
212
|
}
|
|
@@ -167,25 +233,41 @@ function collectShutdownPaneIds(params) {
|
|
|
167
233
|
}
|
|
168
234
|
return [...paneIds];
|
|
169
235
|
}
|
|
170
|
-
function
|
|
236
|
+
function collectAuthorizedSharedSessionWorkerPaneIds(paneIds, teamPaneOwnerId, canonicalWorkerPaneIds, initiallyTaggedWorkerPaneIds, authorizedTaggedWorkerPaneIds) {
|
|
171
237
|
const expectedOwnerId = teamPaneOwnerId.trim();
|
|
172
|
-
if (!expectedOwnerId)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
//
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
238
|
+
if (!expectedOwnerId && paneIds.length > 0) {
|
|
239
|
+
throw new Error('shutdown_shared_session_worker_owner_unavailable:missing_team_owner_id');
|
|
240
|
+
}
|
|
241
|
+
const authorized = new Set();
|
|
242
|
+
for (const paneId of paneIds) {
|
|
243
|
+
const owner = readPaneTeamOwnerTagResult(paneId);
|
|
244
|
+
if (initiallyTaggedWorkerPaneIds?.has(paneId)
|
|
245
|
+
&& (owner.status !== 'value' || owner.value !== expectedOwnerId)) {
|
|
246
|
+
throw new Error(`shutdown_shared_session_worker_owner_changed:${paneId}`);
|
|
247
|
+
}
|
|
248
|
+
if (owner.status === 'error') {
|
|
249
|
+
// A read failure is authority ambiguity only for an explicit persisted
|
|
250
|
+
// worker that topology independently identifies as a worker candidate.
|
|
251
|
+
if (canonicalWorkerPaneIds.has(paneId)) {
|
|
252
|
+
throw new Error(`shutdown_shared_session_worker_owner_unavailable:${paneId}:${owner.error}`);
|
|
253
|
+
}
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
if (owner.status === 'value') {
|
|
257
|
+
if (owner.value === expectedOwnerId) {
|
|
258
|
+
authorized.add(paneId);
|
|
259
|
+
authorizedTaggedWorkerPaneIds?.add(paneId);
|
|
260
|
+
}
|
|
261
|
+
else if (canonicalWorkerPaneIds.has(paneId)) {
|
|
262
|
+
throw new Error(`shutdown_shared_session_worker_owner_changed:${paneId}`);
|
|
263
|
+
}
|
|
264
|
+
continue;
|
|
265
|
+
}
|
|
266
|
+
if (canonicalWorkerPaneIds.has(paneId)) {
|
|
267
|
+
throw new Error(`shutdown_shared_session_worker_owner_changed:${paneId}`);
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
return [...authorized];
|
|
189
271
|
}
|
|
190
272
|
function isSharedSessionHudPaneReclaimable(params) {
|
|
191
273
|
const { paneId, persistedHudPaneId, leaderOwnedHudPaneIds, teamPaneOwnerId, onOwnerReadError } = params;
|
|
@@ -227,6 +309,209 @@ export function shouldPrekillInteractiveShutdownProcessTrees(sessionName) {
|
|
|
227
309
|
// teardown alone is insufficient.
|
|
228
310
|
return true;
|
|
229
311
|
}
|
|
312
|
+
function canonicalDetachedStateRoot(config, cwd) {
|
|
313
|
+
return resolve(config?.team_state_root ?? resolveCanonicalTeamStateRoot(cwd));
|
|
314
|
+
}
|
|
315
|
+
function detachedSessionDestroyReceiptPath(teamName, cwd, config) {
|
|
316
|
+
return join(canonicalDetachedStateRoot(config ?? null, cwd), 'team', teamName, '.detached-session-destroy-receipt.json');
|
|
317
|
+
}
|
|
318
|
+
function detachedConfigIdentity(teamName, cwd, config) {
|
|
319
|
+
const ownerId = typeof config.tmux_pane_owner_id === 'string' ? config.tmux_pane_owner_id.trim() : '';
|
|
320
|
+
const paneId = typeof config.leader_pane_id === 'string' ? config.leader_pane_id.trim() : '';
|
|
321
|
+
const panePid = config.leader_pane_pid;
|
|
322
|
+
if (config.name !== teamName || !config.created_at || !config.tmux_session || !ownerId || !/^%[0-9]+$/.test(paneId)
|
|
323
|
+
|| typeof panePid !== 'number' || !Number.isSafeInteger(panePid) || panePid <= 0)
|
|
324
|
+
return null;
|
|
325
|
+
// Bind the durable bytes of both canonical files, not only authorization
|
|
326
|
+
// fields. A worker/membership mutation invalidates a published receipt.
|
|
327
|
+
try {
|
|
328
|
+
const teamRoot = join(canonicalDetachedStateRoot(config, cwd), 'team', teamName);
|
|
329
|
+
const configBytes = readFileSync(join(teamRoot, 'config.json'), 'utf8');
|
|
330
|
+
const manifestBytes = readFileSync(join(teamRoot, 'manifest.v2.json'), 'utf8');
|
|
331
|
+
const parsedConfig = JSON.parse(configBytes);
|
|
332
|
+
const parsedManifest = JSON.parse(manifestBytes);
|
|
333
|
+
if (!Number.isSafeInteger(parsedConfig.config_generation)
|
|
334
|
+
|| !Number.isSafeInteger(parsedManifest.config_generation)
|
|
335
|
+
|| parsedConfig.config_generation !== parsedManifest.config_generation)
|
|
336
|
+
return null;
|
|
337
|
+
return createHash('sha256').update(JSON.stringify({
|
|
338
|
+
version: 2,
|
|
339
|
+
team_name: teamName,
|
|
340
|
+
state_root: canonicalDetachedStateRoot(config, cwd),
|
|
341
|
+
config_bytes: configBytes,
|
|
342
|
+
manifest_bytes: manifestBytes,
|
|
343
|
+
})).digest('hex');
|
|
344
|
+
}
|
|
345
|
+
catch {
|
|
346
|
+
return null;
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
/** Deterministic test seam for mutations after durable intent publication. */
|
|
350
|
+
let detachedSessionDestroyAfterJournalHook = null;
|
|
351
|
+
export function setDetachedSessionDestroyAfterJournalHookForTest(hook) {
|
|
352
|
+
detachedSessionDestroyAfterJournalHook = hook;
|
|
353
|
+
}
|
|
354
|
+
function isDetachedSessionDestroyReceipt(value) {
|
|
355
|
+
if (!value || typeof value !== 'object')
|
|
356
|
+
return false;
|
|
357
|
+
const receipt = value;
|
|
358
|
+
return receipt.schema_version === 2 && receipt.schema === 'omx.detached_session_destroy.v2'
|
|
359
|
+
&& receipt.operation === 'detached_session_destroy' && (receipt.status === 'intent' || receipt.status === 'accepted')
|
|
360
|
+
&& typeof receipt.team_name === 'string' && receipt.team_name.length > 0
|
|
361
|
+
&& typeof receipt.state_root === 'string' && receipt.state_root.length > 0 && resolve(receipt.state_root) === receipt.state_root
|
|
362
|
+
&& receipt.config_identity_version === 2 && typeof receipt.config_identity_digest === 'string' && /^[a-f0-9]{64}$/.test(receipt.config_identity_digest)
|
|
363
|
+
&& typeof receipt.session_name === 'string' && receipt.session_name.length > 0 && !receipt.session_name.includes(':')
|
|
364
|
+
&& typeof receipt.session_id === 'string' && receipt.session_id.length > 0 && !/\s/.test(receipt.session_id)
|
|
365
|
+
&& typeof receipt.session_created === 'string' && /^[0-9]+$/.test(receipt.session_created)
|
|
366
|
+
&& typeof receipt.leader_pane_id === 'string' && /^%[0-9]+$/.test(receipt.leader_pane_id)
|
|
367
|
+
&& typeof receipt.leader_pane_pid === 'number' && Number.isSafeInteger(receipt.leader_pane_pid) && receipt.leader_pane_pid > 0
|
|
368
|
+
&& typeof receipt.owner_id === 'string' && receipt.owner_id.length > 0;
|
|
369
|
+
}
|
|
370
|
+
async function readDetachedSessionDestroyReceipt(teamName, cwd, config) {
|
|
371
|
+
const path = detachedSessionDestroyReceiptPath(teamName, cwd, config);
|
|
372
|
+
if (!existsSync(path))
|
|
373
|
+
return null;
|
|
374
|
+
try {
|
|
375
|
+
const parsed = JSON.parse(await readFile(path, 'utf-8'));
|
|
376
|
+
if (!isDetachedSessionDestroyReceipt(parsed) || parsed.team_name !== teamName || parsed.state_root !== canonicalDetachedStateRoot(config, cwd))
|
|
377
|
+
throw new Error('invalid receipt');
|
|
378
|
+
return parsed;
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
throw new Error(`detached_session_destroy_receipt_malformed:${config?.tmux_session || 'missing_session'}`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
async function writeDetachedSessionDestroyReceipt(teamName, cwd, config, receipt) {
|
|
385
|
+
await writeAtomic(detachedSessionDestroyReceiptPath(teamName, cwd, config), `${JSON.stringify(receipt, null, 2)}\n`);
|
|
386
|
+
}
|
|
387
|
+
function validateDetachedReceiptConfig(teamName, cwd, config, receipt) {
|
|
388
|
+
if (receipt.team_name !== teamName || receipt.state_root !== canonicalDetachedStateRoot(config, cwd)
|
|
389
|
+
|| receipt.config_identity_version !== 2 || detachedConfigIdentity(teamName, cwd, config) !== receipt.config_identity_digest
|
|
390
|
+
|| config.tmux_session !== receipt.session_name || config.tmux_session_id !== receipt.session_id
|
|
391
|
+
|| config.tmux_session_created !== receipt.session_created || config.leader_pane_id !== receipt.leader_pane_id
|
|
392
|
+
|| config.leader_pane_pid !== receipt.leader_pane_pid || config.tmux_pane_owner_id !== receipt.owner_id) {
|
|
393
|
+
throw new Error(`detached_session_destroy_receipt_config_mismatch:${receipt.session_name}`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
function assertDetachedSessionDestroyAuthority(receipt) {
|
|
397
|
+
const expected = { sessionId: receipt.session_id, sessionCreated: receipt.session_created };
|
|
398
|
+
const query = queryDetachedTeamSession(receipt.session_name, expected);
|
|
399
|
+
if (query.status !== 'exact'
|
|
400
|
+
|| !queryDetachedSessionLeaderBinding(receipt.leader_pane_id, receipt.leader_pane_pid, receipt.session_name, expected)) {
|
|
401
|
+
throw new Error(`detached_session_destroy_authorization_unavailable:${receipt.session_name}`);
|
|
402
|
+
}
|
|
403
|
+
const proof = readExactPaneProofSync(receipt.leader_pane_id);
|
|
404
|
+
if (proof.status !== 'live' || proof.pid !== receipt.leader_pane_pid)
|
|
405
|
+
throw new Error(`detached_session_destroy_authorization_unavailable:${receipt.session_name}`);
|
|
406
|
+
const owner = readPaneTeamOwnerTagResult(proof.paneId);
|
|
407
|
+
if (owner.status !== 'value' || owner.value !== receipt.owner_id)
|
|
408
|
+
throw new Error(`detached_session_destroy_authorization_unavailable:${receipt.session_name}`);
|
|
409
|
+
}
|
|
410
|
+
function requestDetachedSessionDestroyAtFinalSink(receipt) {
|
|
411
|
+
return requestDetachedTeamSessionDestroy(receipt.session_name, {
|
|
412
|
+
sessionId: receipt.session_id,
|
|
413
|
+
sessionCreated: receipt.session_created,
|
|
414
|
+
leaderPaneId: receipt.leader_pane_id,
|
|
415
|
+
leaderPanePid: receipt.leader_pane_pid,
|
|
416
|
+
ownerId: receipt.owner_id,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
async function withDetachedSessionDestroyLock(teamName, cwd, fn) {
|
|
420
|
+
return await withTeamTaskMembershipBarrier(teamName, cwd, fn);
|
|
421
|
+
}
|
|
422
|
+
async function convergeDetachedSessionDestroyReceiptUnlocked(teamName, cwd, config, receipt) {
|
|
423
|
+
const query = queryDetachedTeamSession(receipt.session_name, { sessionId: receipt.session_id, sessionCreated: receipt.session_created });
|
|
424
|
+
if (query.status === 'unavailable' || query.status === 'replacement')
|
|
425
|
+
throw new Error(`detached_session_destroy_unresolved:${receipt.session_name}`);
|
|
426
|
+
if (query.status === 'absent') {
|
|
427
|
+
if (config) {
|
|
428
|
+
validateDetachedReceiptConfig(teamName, cwd, config, receipt);
|
|
429
|
+
config.tmux_session = '';
|
|
430
|
+
await saveTeamConfig(config, cwd);
|
|
431
|
+
}
|
|
432
|
+
else if (receipt.status !== 'accepted') {
|
|
433
|
+
throw new Error(`detached_session_destroy_authorization_unavailable:${receipt.session_name}`);
|
|
434
|
+
}
|
|
435
|
+
return true;
|
|
436
|
+
}
|
|
437
|
+
if (!config)
|
|
438
|
+
throw new Error(`detached_session_destroy_authorization_unavailable:${receipt.session_name}`);
|
|
439
|
+
validateDetachedReceiptConfig(teamName, cwd, config, receipt);
|
|
440
|
+
assertDetachedSessionDestroyAuthority(receipt);
|
|
441
|
+
if (!requestDetachedSessionDestroyAtFinalSink(receipt)) {
|
|
442
|
+
throw new Error(`detached_session_destroy_unresolved:${receipt.session_name}`);
|
|
443
|
+
}
|
|
444
|
+
await writeDetachedSessionDestroyReceipt(teamName, cwd, config, { ...receipt, status: 'accepted' });
|
|
445
|
+
return await convergeDetachedSessionDestroyReceiptUnlocked(teamName, cwd, config, { ...receipt, status: 'accepted' });
|
|
446
|
+
}
|
|
447
|
+
async function reconcileDetachedSessionDestroyReceipt(teamName, cwd, config) {
|
|
448
|
+
return await withDetachedSessionDestroyLock(teamName, cwd, async () => {
|
|
449
|
+
const canonical = await readTeamConfig(teamName, cwd);
|
|
450
|
+
const receipt = await readDetachedSessionDestroyReceipt(teamName, cwd, canonical ?? config);
|
|
451
|
+
if (!receipt)
|
|
452
|
+
return false;
|
|
453
|
+
if (!canonical) {
|
|
454
|
+
// Name/file presence is not authority. Accepted absence requires a
|
|
455
|
+
// successfully parsed current canonical generation.
|
|
456
|
+
throw new Error(`detached_session_destroy_authorization_unavailable:${receipt.session_name}`);
|
|
457
|
+
}
|
|
458
|
+
return await convergeDetachedSessionDestroyReceiptUnlocked(teamName, cwd, canonical, receipt);
|
|
459
|
+
});
|
|
460
|
+
}
|
|
461
|
+
async function destroyConfiguredDetachedTeamSession(teamName, cwd, config) {
|
|
462
|
+
if (!config.tmux_session)
|
|
463
|
+
return;
|
|
464
|
+
await withDetachedSessionDestroyLock(teamName, cwd, async () => {
|
|
465
|
+
const canonical = await readTeamConfig(teamName, cwd);
|
|
466
|
+
if (!canonical)
|
|
467
|
+
throw new Error(`detached_session_destroy_authorization_unavailable:${config.tmux_session}`);
|
|
468
|
+
const existing = await readDetachedSessionDestroyReceipt(teamName, cwd, canonical);
|
|
469
|
+
if (existing) {
|
|
470
|
+
await convergeDetachedSessionDestroyReceiptUnlocked(teamName, cwd, canonical, existing);
|
|
471
|
+
return;
|
|
472
|
+
}
|
|
473
|
+
const sessionName = canonical.tmux_session;
|
|
474
|
+
const sessionId = typeof canonical.tmux_session_id === 'string' ? canonical.tmux_session_id : '';
|
|
475
|
+
const sessionCreated = typeof canonical.tmux_session_created === 'string' ? canonical.tmux_session_created : '';
|
|
476
|
+
const digest = detachedConfigIdentity(teamName, cwd, canonical);
|
|
477
|
+
const ownerId = typeof canonical.tmux_pane_owner_id === 'string' ? canonical.tmux_pane_owner_id.trim() : '';
|
|
478
|
+
const leaderPaneId = typeof canonical.leader_pane_id === 'string' ? canonical.leader_pane_id.trim() : '';
|
|
479
|
+
const leaderPanePid = canonical.leader_pane_pid;
|
|
480
|
+
if (!digest || !sessionName || !/^\$[0-9]+$/.test(sessionId) || !/^[0-9]+$/.test(sessionCreated) || !ownerId || !/^%[0-9]+$/.test(leaderPaneId)
|
|
481
|
+
|| typeof leaderPanePid !== 'number' || !Number.isSafeInteger(leaderPanePid) || leaderPanePid <= 0) {
|
|
482
|
+
throw new Error(`detached_session_destroy_authorization_unavailable:${sessionName || 'missing_session'}`);
|
|
483
|
+
}
|
|
484
|
+
const receipt = {
|
|
485
|
+
schema_version: 2, schema: 'omx.detached_session_destroy.v2', operation: 'detached_session_destroy', status: 'intent',
|
|
486
|
+
team_name: teamName, state_root: canonicalDetachedStateRoot(canonical, cwd), config_identity_version: 2, config_identity_digest: digest,
|
|
487
|
+
session_name: sessionName, session_id: sessionId, session_created: sessionCreated,
|
|
488
|
+
leader_pane_id: leaderPaneId, leader_pane_pid: leaderPanePid, owner_id: ownerId,
|
|
489
|
+
};
|
|
490
|
+
assertDetachedSessionDestroyAuthority(receipt);
|
|
491
|
+
await writeDetachedSessionDestroyReceipt(teamName, cwd, canonical, receipt);
|
|
492
|
+
const afterJournalHook = detachedSessionDestroyAfterJournalHook;
|
|
493
|
+
detachedSessionDestroyAfterJournalHook = null;
|
|
494
|
+
await afterJournalHook?.();
|
|
495
|
+
const current = await readTeamConfig(teamName, cwd);
|
|
496
|
+
if (!current)
|
|
497
|
+
throw new Error(`detached_session_destroy_authorization_unavailable:${sessionName}`);
|
|
498
|
+
validateDetachedReceiptConfig(teamName, cwd, current, receipt);
|
|
499
|
+
assertDetachedSessionDestroyAuthority(receipt);
|
|
500
|
+
if (!requestDetachedSessionDestroyAtFinalSink(receipt)) {
|
|
501
|
+
throw new Error(`detached_session_destroy_unresolved:${sessionName}`);
|
|
502
|
+
}
|
|
503
|
+
await writeDetachedSessionDestroyReceipt(teamName, cwd, current, { ...receipt, status: 'accepted' });
|
|
504
|
+
await convergeDetachedSessionDestroyReceiptUnlocked(teamName, cwd, current, { ...receipt, status: 'accepted' });
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
function assertPaneTeardownProofsAvailable(operation, unavailableProofs) {
|
|
508
|
+
if (unavailableProofs.length === 0)
|
|
509
|
+
return;
|
|
510
|
+
const detail = unavailableProofs
|
|
511
|
+
.map((proof) => `${proof.paneId}:${proof.reason}${proof.detail ? `:${proof.detail}` : ''}`)
|
|
512
|
+
.join(',');
|
|
513
|
+
throw new Error(`${operation}_pane_proof_unavailable:${detail}`);
|
|
514
|
+
}
|
|
230
515
|
export async function cleanupTeamWorkerLaunchOrphanedMcpProcesses(dependencies = {}) {
|
|
231
516
|
const cleanup = dependencies.cleanup ?? (() => cleanupOmxMcpProcesses([], {
|
|
232
517
|
selectCandidates: findLaunchSafeCleanupCandidates,
|
|
@@ -1384,7 +1669,7 @@ function registerPromptWorkerHandle(teamName, workerName, child) {
|
|
|
1384
1669
|
if (!teamHandles)
|
|
1385
1670
|
return;
|
|
1386
1671
|
const handle = teamHandles.get(workerName);
|
|
1387
|
-
if (handle?.processGroupId &&
|
|
1672
|
+
if (handle?.processGroupId && probeProcessGroupLiveness(handle.processGroupId) !== 'gone') {
|
|
1388
1673
|
return;
|
|
1389
1674
|
}
|
|
1390
1675
|
teamHandles.delete(workerName);
|
|
@@ -1403,34 +1688,70 @@ function removePromptWorkerHandle(teamName, workerName) {
|
|
|
1403
1688
|
if (teamHandles.size === 0)
|
|
1404
1689
|
promptWorkerRegistry.delete(teamName);
|
|
1405
1690
|
}
|
|
1406
|
-
function
|
|
1691
|
+
function probePidLiveness(pid) {
|
|
1407
1692
|
if (!Number.isFinite(pid) || pid <= 0)
|
|
1408
|
-
return
|
|
1693
|
+
return 'gone';
|
|
1409
1694
|
try {
|
|
1410
1695
|
process.kill(pid, 0);
|
|
1411
|
-
return
|
|
1696
|
+
return 'alive';
|
|
1412
1697
|
}
|
|
1413
1698
|
catch (err) {
|
|
1414
1699
|
if (err.code === 'ESRCH')
|
|
1415
|
-
return
|
|
1700
|
+
return 'gone';
|
|
1416
1701
|
process.stderr.write(`[team/runtime] operation failed: ${err}\n`);
|
|
1417
|
-
return
|
|
1702
|
+
return 'unknown';
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
function isPidAlive(pid) {
|
|
1706
|
+
return probePidLiveness(pid) === 'alive';
|
|
1707
|
+
}
|
|
1708
|
+
function isPidGone(pid) {
|
|
1709
|
+
return probePidLiveness(pid) === 'gone';
|
|
1710
|
+
}
|
|
1711
|
+
async function captureProcessIdentity(pid) {
|
|
1712
|
+
// There is no portable process birth identifier exposed by Node. Refuse to
|
|
1713
|
+
// persist replayable PID debt on platforms without Linux's stable proc stat
|
|
1714
|
+
// start-time field rather than later treating a reused PID as authoritative.
|
|
1715
|
+
if (process.platform !== 'linux' || !Number.isSafeInteger(pid) || pid <= 0)
|
|
1716
|
+
return null;
|
|
1717
|
+
try {
|
|
1718
|
+
const stat = await readFile(`/proc/${pid}/stat`, 'utf8');
|
|
1719
|
+
const close = stat.lastIndexOf(')');
|
|
1720
|
+
const fields = close >= 0 ? stat.slice(close + 2).trim().split(/\s+/) : [];
|
|
1721
|
+
// field 22 is starttime; fields begin at field 3 after the comm value.
|
|
1722
|
+
const startTime = fields[19];
|
|
1723
|
+
return typeof startTime === 'string' && /^[0-9]+$/.test(startTime)
|
|
1724
|
+
? { pid, start_time: startTime }
|
|
1725
|
+
: null;
|
|
1418
1726
|
}
|
|
1727
|
+
catch {
|
|
1728
|
+
return null;
|
|
1729
|
+
}
|
|
1730
|
+
}
|
|
1731
|
+
async function captureProcessIdentities(pids) {
|
|
1732
|
+
const identities = await Promise.all([...new Set(pids)].map((pid) => captureProcessIdentity(pid)));
|
|
1733
|
+
return identities.every((identity) => identity !== null) ? identities : null;
|
|
1419
1734
|
}
|
|
1420
|
-
function
|
|
1735
|
+
async function probeProcessIdentity(identity) {
|
|
1736
|
+
const current = await captureProcessIdentity(identity.pid);
|
|
1737
|
+
if (current === null)
|
|
1738
|
+
return probePidLiveness(identity.pid) === 'gone' ? 'gone' : 'reused_or_unknown';
|
|
1739
|
+
return current.start_time === identity.start_time ? 'same' : 'reused_or_unknown';
|
|
1740
|
+
}
|
|
1741
|
+
function probeProcessGroupLiveness(processGroupId) {
|
|
1421
1742
|
if (process.platform === 'win32')
|
|
1422
|
-
return
|
|
1743
|
+
return 'gone';
|
|
1423
1744
|
if (!Number.isFinite(processGroupId) || processGroupId <= 0)
|
|
1424
|
-
return
|
|
1745
|
+
return 'gone';
|
|
1425
1746
|
try {
|
|
1426
1747
|
process.kill(-processGroupId, 0);
|
|
1427
|
-
return
|
|
1748
|
+
return 'alive';
|
|
1428
1749
|
}
|
|
1429
1750
|
catch (err) {
|
|
1430
1751
|
if (err.code === 'ESRCH')
|
|
1431
|
-
return
|
|
1752
|
+
return 'gone';
|
|
1432
1753
|
process.stderr.write(`[team/runtime] operation failed: ${err}\n`);
|
|
1433
|
-
return
|
|
1754
|
+
return 'unknown';
|
|
1434
1755
|
}
|
|
1435
1756
|
}
|
|
1436
1757
|
function listProcessTreeEntries() {
|
|
@@ -1491,11 +1812,11 @@ async function waitForTrackedPidsExit(pids, timeoutMs) {
|
|
|
1491
1812
|
return true;
|
|
1492
1813
|
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
1493
1814
|
while (Date.now() < deadline) {
|
|
1494
|
-
if (tracked.every(
|
|
1815
|
+
if (tracked.every(isPidGone))
|
|
1495
1816
|
return true;
|
|
1496
1817
|
await new Promise((resolve) => setTimeout(resolve, PROMPT_WORKER_EXIT_POLL_MS));
|
|
1497
1818
|
}
|
|
1498
|
-
return tracked.every(
|
|
1819
|
+
return tracked.every(isPidGone);
|
|
1499
1820
|
}
|
|
1500
1821
|
async function terminateTrackedProcessTree(rootPid, processGroupId = null, graceMs = PROMPT_WORKER_SIGTERM_WAIT_MS, killWaitMs = PROMPT_WORKER_SIGKILL_WAIT_MS) {
|
|
1501
1822
|
if (processGroupId && process.platform !== 'win32') {
|
|
@@ -1522,9 +1843,9 @@ async function terminateTrackedProcessTree(rootPid, processGroupId = null, grace
|
|
|
1522
1843
|
}
|
|
1523
1844
|
const groupDeadline = Date.now() + Math.max(0, graceMs);
|
|
1524
1845
|
while (Date.now() < groupDeadline) {
|
|
1525
|
-
const
|
|
1526
|
-
const
|
|
1527
|
-
if (
|
|
1846
|
+
const groupGone = probeProcessGroupLiveness(processGroupId) === 'gone';
|
|
1847
|
+
const descendantsGone = trackedPids.every(isPidGone);
|
|
1848
|
+
if (groupGone && descendantsGone) {
|
|
1528
1849
|
return { terminated: true, forcedKill: false, trackedPids };
|
|
1529
1850
|
}
|
|
1530
1851
|
await new Promise((resolve) => setTimeout(resolve, PROMPT_WORKER_EXIT_POLL_MS));
|
|
@@ -1551,15 +1872,15 @@ async function terminateTrackedProcessTree(rootPid, processGroupId = null, grace
|
|
|
1551
1872
|
}
|
|
1552
1873
|
const killDeadline = Date.now() + Math.max(0, killWaitMs);
|
|
1553
1874
|
while (Date.now() < killDeadline) {
|
|
1554
|
-
const
|
|
1555
|
-
const
|
|
1556
|
-
if (
|
|
1875
|
+
const groupGone = probeProcessGroupLiveness(processGroupId) === 'gone';
|
|
1876
|
+
const descendantsGone = trackedPids.every(isPidGone);
|
|
1877
|
+
if (groupGone && descendantsGone) {
|
|
1557
1878
|
return { terminated: true, forcedKill: true, trackedPids };
|
|
1558
1879
|
}
|
|
1559
1880
|
await new Promise((resolve) => setTimeout(resolve, PROMPT_WORKER_EXIT_POLL_MS));
|
|
1560
1881
|
}
|
|
1561
1882
|
return {
|
|
1562
|
-
terminated:
|
|
1883
|
+
terminated: probeProcessGroupLiveness(processGroupId) === 'gone' && trackedPids.every(isPidGone),
|
|
1563
1884
|
forcedKill: true,
|
|
1564
1885
|
trackedPids,
|
|
1565
1886
|
};
|
|
@@ -1567,7 +1888,7 @@ async function terminateTrackedProcessTree(rootPid, processGroupId = null, grace
|
|
|
1567
1888
|
const trackedPids = collectProcessTreePids(rootPid);
|
|
1568
1889
|
if (trackedPids.length === 0) {
|
|
1569
1890
|
return {
|
|
1570
|
-
terminated:
|
|
1891
|
+
terminated: isPidGone(rootPid),
|
|
1571
1892
|
forcedKill: false,
|
|
1572
1893
|
trackedPids: [],
|
|
1573
1894
|
};
|
|
@@ -1603,6 +1924,327 @@ async function terminateTrackedProcessTree(rootPid, processGroupId = null, grace
|
|
|
1603
1924
|
trackedPids,
|
|
1604
1925
|
};
|
|
1605
1926
|
}
|
|
1927
|
+
/**
|
|
1928
|
+
* Every operating-system effect attributed to an explicit pane must re-read
|
|
1929
|
+
* the pane's global row immediately before that effect. A changed/dead row is
|
|
1930
|
+
* not a fallback opportunity: it stops this pane's process-tree teardown.
|
|
1931
|
+
*/
|
|
1932
|
+
function reproveExactPaneAfterProcessEsrcH(paneId) {
|
|
1933
|
+
const proof = readExactPaneProofSync(paneId);
|
|
1934
|
+
if (proof.status === 'unavailable')
|
|
1935
|
+
return { status: 'unavailable', proof };
|
|
1936
|
+
if (proof.status === 'gone')
|
|
1937
|
+
return { status: 'gone' };
|
|
1938
|
+
// ESRCH establishes only that the process was absent. A still-live pane row
|
|
1939
|
+
// is not authority to advance the pane lifecycle without an absent reproof.
|
|
1940
|
+
return { status: 'stopped' };
|
|
1941
|
+
}
|
|
1942
|
+
function signalExactPaneProcess(paneId, authorizedPanePid, pid, signal) {
|
|
1943
|
+
const proof = readExactPaneProofSync(paneId);
|
|
1944
|
+
if (proof.status === 'unavailable')
|
|
1945
|
+
return { status: 'unavailable', proof };
|
|
1946
|
+
if (proof.status === 'live' && proof.pid !== authorizedPanePid)
|
|
1947
|
+
return { status: 'stopped' };
|
|
1948
|
+
if (proof.status === 'gone')
|
|
1949
|
+
return { status: 'stopped' };
|
|
1950
|
+
try {
|
|
1951
|
+
process.kill(pid, signal);
|
|
1952
|
+
return { status: 'alive' };
|
|
1953
|
+
}
|
|
1954
|
+
catch (error) {
|
|
1955
|
+
return error.code === 'ESRCH'
|
|
1956
|
+
? reproveExactPaneAfterProcessEsrcH(paneId)
|
|
1957
|
+
: { status: 'unknown' };
|
|
1958
|
+
}
|
|
1959
|
+
}
|
|
1960
|
+
function probeExactPaneProcess(paneId, authorizedPanePid, pid) {
|
|
1961
|
+
const proof = readExactPaneProofSync(paneId);
|
|
1962
|
+
if (proof.status === 'unavailable')
|
|
1963
|
+
return { status: 'unavailable', proof };
|
|
1964
|
+
if (proof.status === 'live' && proof.pid !== authorizedPanePid)
|
|
1965
|
+
return { status: 'stopped' };
|
|
1966
|
+
try {
|
|
1967
|
+
process.kill(pid, 0);
|
|
1968
|
+
return proof.status === 'gone' ? { status: 'stopped' } : { status: 'alive' };
|
|
1969
|
+
}
|
|
1970
|
+
catch (error) {
|
|
1971
|
+
return error.code === 'ESRCH'
|
|
1972
|
+
? reproveExactPaneAfterProcessEsrcH(paneId)
|
|
1973
|
+
: { status: 'unknown' };
|
|
1974
|
+
}
|
|
1975
|
+
}
|
|
1976
|
+
async function waitForExactPaneTrackedPidsExit(paneId, authorizedPanePid, pids, timeoutMs) {
|
|
1977
|
+
const trackedPids = [...pids];
|
|
1978
|
+
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
1979
|
+
do {
|
|
1980
|
+
let anyAlive = false;
|
|
1981
|
+
for (const pid of trackedPids) {
|
|
1982
|
+
const probe = probeExactPaneProcess(paneId, authorizedPanePid, pid);
|
|
1983
|
+
if (probe.status === 'unavailable')
|
|
1984
|
+
return { terminated: false, stopped: true, trackedPids, authorizedPanePid, proofUnavailable: probe.proof };
|
|
1985
|
+
if (probe.status === 'stopped')
|
|
1986
|
+
return { terminated: false, stopped: true, trackedPids, authorizedPanePid };
|
|
1987
|
+
if (probe.status === 'unknown')
|
|
1988
|
+
return { terminated: false, stopped: true, trackedPids, authorizedPanePid };
|
|
1989
|
+
if (probe.status === 'alive')
|
|
1990
|
+
anyAlive = true;
|
|
1991
|
+
}
|
|
1992
|
+
if (!anyAlive)
|
|
1993
|
+
return { terminated: true, stopped: false, trackedPids, authorizedPanePid };
|
|
1994
|
+
if (Date.now() >= deadline)
|
|
1995
|
+
break;
|
|
1996
|
+
await new Promise((resolve) => setTimeout(resolve, PROMPT_WORKER_EXIT_POLL_MS));
|
|
1997
|
+
} while (Date.now() < deadline);
|
|
1998
|
+
for (const pid of trackedPids) {
|
|
1999
|
+
const probe = probeExactPaneProcess(paneId, authorizedPanePid, pid);
|
|
2000
|
+
if (probe.status === 'unavailable')
|
|
2001
|
+
return { terminated: false, stopped: true, trackedPids, authorizedPanePid, proofUnavailable: probe.proof };
|
|
2002
|
+
if (probe.status === 'stopped' || probe.status === 'unknown')
|
|
2003
|
+
return { terminated: false, stopped: true, trackedPids, authorizedPanePid };
|
|
2004
|
+
if (probe.status === 'alive')
|
|
2005
|
+
return { terminated: false, stopped: false, trackedPids, authorizedPanePid };
|
|
2006
|
+
}
|
|
2007
|
+
return { terminated: true, stopped: false, trackedPids, authorizedPanePid };
|
|
2008
|
+
}
|
|
2009
|
+
async function terminateExactPaneProcessTree(paneId, expectedPanePid, graceMs = PROMPT_WORKER_SIGTERM_WAIT_MS, killWaitMs = PROMPT_WORKER_SIGKILL_WAIT_MS, authorizePaneEffect) {
|
|
2010
|
+
const authorization = readExactPaneProofSync(paneId);
|
|
2011
|
+
if (authorization.status === 'unavailable')
|
|
2012
|
+
return { terminated: false, stopped: true, trackedPids: [], proofUnavailable: authorization };
|
|
2013
|
+
if (authorization.status === 'gone')
|
|
2014
|
+
return { terminated: true, stopped: true, trackedPids: [] };
|
|
2015
|
+
if (typeof expectedPanePid === 'number' && authorization.pid !== expectedPanePid) {
|
|
2016
|
+
return { terminated: false, stopped: true, trackedPids: [], authorizedPanePid: authorization.pid, proofUnavailable: {
|
|
2017
|
+
status: 'unavailable', paneId, reason: 'pane_pid_changed', detail: `expected ${expectedPanePid}, got ${authorization.pid}`,
|
|
2018
|
+
} };
|
|
2019
|
+
}
|
|
2020
|
+
if (authorizePaneEffect && !authorizePaneEffect(paneId, authorization.pid)) {
|
|
2021
|
+
return {
|
|
2022
|
+
terminated: false,
|
|
2023
|
+
stopped: true,
|
|
2024
|
+
trackedPids: [],
|
|
2025
|
+
authorizedPanePid: authorization.pid,
|
|
2026
|
+
proofUnavailable: {
|
|
2027
|
+
status: 'unavailable',
|
|
2028
|
+
paneId,
|
|
2029
|
+
reason: 'pane_pid_changed',
|
|
2030
|
+
detail: 'pane owner authorization changed',
|
|
2031
|
+
},
|
|
2032
|
+
};
|
|
2033
|
+
}
|
|
2034
|
+
const trackedPids = collectProcessTreePids(authorization.pid);
|
|
2035
|
+
if (trackedPids.length === 0)
|
|
2036
|
+
return { terminated: true, stopped: false, trackedPids, authorizedPanePid: authorization.pid };
|
|
2037
|
+
// Birth evidence is mandatory for descendant signals. A pane's pinned PID
|
|
2038
|
+
// authorizes its root process, but a numeric descendant PID is never enough:
|
|
2039
|
+
// its /proc start time must still match immediately before each signal.
|
|
2040
|
+
const trackedProcessIdentities = await captureProcessIdentities(trackedPids) ?? undefined;
|
|
2041
|
+
const trackedProcessIdentityByPid = new Map((trackedProcessIdentities ?? []).map((identity) => [identity.pid, identity]));
|
|
2042
|
+
const revalidateDescendantIdentity = async (pid) => {
|
|
2043
|
+
if (pid === authorization.pid)
|
|
2044
|
+
return true;
|
|
2045
|
+
const identity = trackedProcessIdentityByPid.get(pid);
|
|
2046
|
+
return identity !== undefined && (await probeProcessIdentity(identity)) === 'same';
|
|
2047
|
+
};
|
|
2048
|
+
for (const pid of trackedPids) {
|
|
2049
|
+
if (authorizePaneEffect && !authorizePaneEffect(paneId, authorization.pid)) {
|
|
2050
|
+
return {
|
|
2051
|
+
terminated: false,
|
|
2052
|
+
stopped: true,
|
|
2053
|
+
trackedPids,
|
|
2054
|
+
trackedProcessIdentities,
|
|
2055
|
+
authorizedPanePid: authorization.pid,
|
|
2056
|
+
proofUnavailable: {
|
|
2057
|
+
status: 'unavailable',
|
|
2058
|
+
paneId,
|
|
2059
|
+
reason: 'pane_pid_changed',
|
|
2060
|
+
detail: 'pane owner authorization changed',
|
|
2061
|
+
},
|
|
2062
|
+
};
|
|
2063
|
+
}
|
|
2064
|
+
if (!await revalidateDescendantIdentity(pid)) {
|
|
2065
|
+
return { terminated: false, stopped: true, trackedPids, trackedProcessIdentities, authorizedPanePid: authorization.pid };
|
|
2066
|
+
}
|
|
2067
|
+
const signal = signalExactPaneProcess(paneId, authorization.pid, pid, 'SIGTERM');
|
|
2068
|
+
if (signal.status === 'unavailable')
|
|
2069
|
+
return { terminated: false, stopped: true, trackedPids, trackedProcessIdentities, authorizedPanePid: authorization.pid, proofUnavailable: signal.proof };
|
|
2070
|
+
if (signal.status === 'stopped' || signal.status === 'unknown')
|
|
2071
|
+
return { terminated: false, stopped: true, trackedPids, trackedProcessIdentities, authorizedPanePid: authorization.pid };
|
|
2072
|
+
}
|
|
2073
|
+
const graceful = await waitForExactPaneTrackedPidsExit(paneId, authorization.pid, trackedPids, graceMs);
|
|
2074
|
+
if (graceful.terminated || graceful.stopped)
|
|
2075
|
+
return { ...graceful, trackedProcessIdentities };
|
|
2076
|
+
for (const pid of trackedPids) {
|
|
2077
|
+
const probe = probeExactPaneProcess(paneId, authorization.pid, pid);
|
|
2078
|
+
if (probe.status === 'unavailable')
|
|
2079
|
+
return { terminated: false, stopped: true, trackedPids, trackedProcessIdentities, authorizedPanePid: authorization.pid, proofUnavailable: probe.proof };
|
|
2080
|
+
if (probe.status === 'stopped')
|
|
2081
|
+
return { terminated: false, stopped: true, trackedPids, trackedProcessIdentities, authorizedPanePid: authorization.pid };
|
|
2082
|
+
if (probe.status === 'gone')
|
|
2083
|
+
continue;
|
|
2084
|
+
if (authorizePaneEffect && !authorizePaneEffect(paneId, authorization.pid)) {
|
|
2085
|
+
return {
|
|
2086
|
+
terminated: false,
|
|
2087
|
+
stopped: true,
|
|
2088
|
+
trackedPids,
|
|
2089
|
+
trackedProcessIdentities,
|
|
2090
|
+
authorizedPanePid: authorization.pid,
|
|
2091
|
+
proofUnavailable: {
|
|
2092
|
+
status: 'unavailable',
|
|
2093
|
+
paneId,
|
|
2094
|
+
reason: 'pane_pid_changed',
|
|
2095
|
+
detail: 'pane owner authorization changed',
|
|
2096
|
+
},
|
|
2097
|
+
};
|
|
2098
|
+
}
|
|
2099
|
+
if (!await revalidateDescendantIdentity(pid)) {
|
|
2100
|
+
return { terminated: false, stopped: true, trackedPids, trackedProcessIdentities, authorizedPanePid: authorization.pid };
|
|
2101
|
+
}
|
|
2102
|
+
const signal = signalExactPaneProcess(paneId, authorization.pid, pid, 'SIGKILL');
|
|
2103
|
+
if (signal.status === 'unavailable')
|
|
2104
|
+
return { terminated: false, stopped: true, trackedPids, trackedProcessIdentities, authorizedPanePid: authorization.pid, proofUnavailable: signal.proof };
|
|
2105
|
+
if (signal.status === 'stopped' || signal.status === 'unknown')
|
|
2106
|
+
return { terminated: false, stopped: true, trackedPids, trackedProcessIdentities, authorizedPanePid: authorization.pid };
|
|
2107
|
+
}
|
|
2108
|
+
return { ...(await waitForExactPaneTrackedPidsExit(paneId, authorization.pid, trackedPids, killWaitMs)), trackedProcessIdentities };
|
|
2109
|
+
}
|
|
2110
|
+
function gonePaneDescendantCleanupDebtPath(teamName, cwd, config) {
|
|
2111
|
+
const stateRoot = config?.team_state_root ?? resolveCanonicalTeamStateRoot(resolve(cwd));
|
|
2112
|
+
return join(stateRoot, 'team', teamName, '.gone-pane-descendant-cleanup-debt.json');
|
|
2113
|
+
}
|
|
2114
|
+
function trackedPidsFromGonePaneDebtEntry(entry) {
|
|
2115
|
+
return 'tracked_pids' in entry
|
|
2116
|
+
? entry.tracked_pids
|
|
2117
|
+
: entry.tracked_processes.map((identity) => identity.pid);
|
|
2118
|
+
}
|
|
2119
|
+
async function persistGonePaneDescendantCleanupDebt(params) {
|
|
2120
|
+
const { teamName, cwd, config, paneId, teardown } = params;
|
|
2121
|
+
const unresolvedTrackedPids = teardown.trackedPids.filter((pid) => probePidLiveness(pid) !== 'gone');
|
|
2122
|
+
if (unresolvedTrackedPids.length === 0 || typeof teardown.authorizedPanePid !== 'number')
|
|
2123
|
+
return;
|
|
2124
|
+
const liveTrackedProcesses = (teardown.trackedProcessIdentities ?? []).filter((identity) => (unresolvedTrackedPids.includes(identity.pid)));
|
|
2125
|
+
const debtPath = gonePaneDescendantCleanupDebtPath(teamName, cwd, config);
|
|
2126
|
+
let prior = { schema_version: 1, operation: 'gone_pane_descendant_cleanup', entries: [] };
|
|
2127
|
+
try {
|
|
2128
|
+
prior = JSON.parse(await readFile(debtPath, 'utf8'));
|
|
2129
|
+
}
|
|
2130
|
+
catch (error) {
|
|
2131
|
+
if (error.code !== 'ENOENT')
|
|
2132
|
+
throw new Error('gone_pane_descendant_cleanup_debt_unreadable');
|
|
2133
|
+
}
|
|
2134
|
+
if (!isValidGonePaneDescendantCleanupDebt(prior))
|
|
2135
|
+
throw new Error('gone_pane_descendant_cleanup_debt_malformed');
|
|
2136
|
+
const priorEntry = prior.entries.find((entry) => entry.pane_id === paneId);
|
|
2137
|
+
const entries = prior.entries.filter((entry) => entry.pane_id !== paneId);
|
|
2138
|
+
if (liveTrackedProcesses.length !== unresolvedTrackedPids.length) {
|
|
2139
|
+
const priorIdentities = priorEntry?.tracked_processes ?? [];
|
|
2140
|
+
const identitiesByPid = new Map(priorIdentities.map((identity) => [identity.pid, identity]));
|
|
2141
|
+
for (const identity of liveTrackedProcesses) {
|
|
2142
|
+
if (!identitiesByPid.has(identity.pid))
|
|
2143
|
+
identitiesByPid.set(identity.pid, identity);
|
|
2144
|
+
}
|
|
2145
|
+
entries.push({
|
|
2146
|
+
pane_id: paneId,
|
|
2147
|
+
authorized_pane_pid: teardown.authorizedPanePid,
|
|
2148
|
+
tracked_pids: [...new Set([...(priorEntry ? trackedPidsFromGonePaneDebtEntry(priorEntry) : []), ...unresolvedTrackedPids])],
|
|
2149
|
+
...(identitiesByPid.size > 0 ? { tracked_processes: [...identitiesByPid.values()] } : {}),
|
|
2150
|
+
evidence: 'process_identity_unavailable',
|
|
2151
|
+
});
|
|
2152
|
+
}
|
|
2153
|
+
else {
|
|
2154
|
+
entries.push({
|
|
2155
|
+
pane_id: paneId,
|
|
2156
|
+
authorized_pane_pid: teardown.authorizedPanePid,
|
|
2157
|
+
tracked_processes: liveTrackedProcesses,
|
|
2158
|
+
evidence: teardown.proofUnavailable
|
|
2159
|
+
? `${teardown.proofUnavailable.reason}:${teardown.proofUnavailable.detail ?? ''}`
|
|
2160
|
+
: 'pane_authority_lost_during_descendant_teardown',
|
|
2161
|
+
});
|
|
2162
|
+
}
|
|
2163
|
+
await writeAtomic(debtPath, JSON.stringify({ schema_version: 1, operation: 'gone_pane_descendant_cleanup', entries }, null, 2));
|
|
2164
|
+
}
|
|
2165
|
+
function isValidProcessIdentity(value) {
|
|
2166
|
+
return typeof value === 'object' && value !== null
|
|
2167
|
+
&& Number.isSafeInteger(value.pid)
|
|
2168
|
+
&& value.pid > 0
|
|
2169
|
+
&& typeof value.start_time === 'string'
|
|
2170
|
+
&& /^[0-9]+$/.test(value.start_time);
|
|
2171
|
+
}
|
|
2172
|
+
function isValidGonePaneDescendantCleanupDebt(value) {
|
|
2173
|
+
if (typeof value !== 'object' || value === null)
|
|
2174
|
+
return false;
|
|
2175
|
+
const debt = value;
|
|
2176
|
+
if (debt.schema_version !== 1 || debt.operation !== 'gone_pane_descendant_cleanup' || !Array.isArray(debt.entries))
|
|
2177
|
+
return false;
|
|
2178
|
+
if (new Set(debt.entries.map((entry) => entry?.pane_id)).size !== debt.entries.length)
|
|
2179
|
+
return false;
|
|
2180
|
+
return debt.entries.every((entry) => {
|
|
2181
|
+
if (typeof entry !== 'object' || entry === null)
|
|
2182
|
+
return false;
|
|
2183
|
+
const candidate = entry;
|
|
2184
|
+
if (typeof candidate.pane_id !== 'string' || !/^%[0-9]+$/.test(candidate.pane_id)
|
|
2185
|
+
|| !Number.isSafeInteger(candidate.authorized_pane_pid)
|
|
2186
|
+
|| candidate.authorized_pane_pid <= 0)
|
|
2187
|
+
return false;
|
|
2188
|
+
if (candidate.evidence === 'process_identity_unavailable') {
|
|
2189
|
+
const trackedPids = candidate.tracked_pids;
|
|
2190
|
+
if (!Array.isArray(candidate.tracked_pids) || candidate.tracked_pids.length === 0
|
|
2191
|
+
|| trackedPids.some((pid) => !Number.isSafeInteger(pid) || pid <= 0)
|
|
2192
|
+
|| new Set(trackedPids).size !== trackedPids.length)
|
|
2193
|
+
return false;
|
|
2194
|
+
if (candidate.tracked_processes === undefined)
|
|
2195
|
+
return true;
|
|
2196
|
+
if (!Array.isArray(candidate.tracked_processes) || !candidate.tracked_processes.every(isValidProcessIdentity))
|
|
2197
|
+
return false;
|
|
2198
|
+
const trackedProcesses = candidate.tracked_processes;
|
|
2199
|
+
return trackedProcesses.every((identity) => trackedPids.includes(identity.pid))
|
|
2200
|
+
&& new Set(trackedProcesses.map((identity) => identity.pid)).size === trackedProcesses.length;
|
|
2201
|
+
}
|
|
2202
|
+
if (typeof candidate.evidence !== 'string' || candidate.evidence.length === 0
|
|
2203
|
+
|| candidate.tracked_pids !== undefined
|
|
2204
|
+
|| !Array.isArray(candidate.tracked_processes) || candidate.tracked_processes.length === 0
|
|
2205
|
+
|| !candidate.tracked_processes.every(isValidProcessIdentity))
|
|
2206
|
+
return false;
|
|
2207
|
+
const trackedProcesses = candidate.tracked_processes;
|
|
2208
|
+
return new Set(trackedProcesses.map((identity) => identity.pid)).size === trackedProcesses.length;
|
|
2209
|
+
});
|
|
2210
|
+
}
|
|
2211
|
+
async function reconcileGonePaneDescendantCleanupDebt(teamName, cwd, config) {
|
|
2212
|
+
const debtPath = gonePaneDescendantCleanupDebtPath(teamName, cwd, config);
|
|
2213
|
+
if (!existsSync(debtPath))
|
|
2214
|
+
return;
|
|
2215
|
+
let debt;
|
|
2216
|
+
try {
|
|
2217
|
+
debt = JSON.parse(await readFile(debtPath, 'utf8'));
|
|
2218
|
+
}
|
|
2219
|
+
catch {
|
|
2220
|
+
throw new Error('gone_pane_descendant_cleanup_debt_unreadable');
|
|
2221
|
+
}
|
|
2222
|
+
if (!isValidGonePaneDescendantCleanupDebt(debt))
|
|
2223
|
+
throw new Error('gone_pane_descendant_cleanup_debt_malformed');
|
|
2224
|
+
const unresolved = [];
|
|
2225
|
+
for (const entry of debt.entries) {
|
|
2226
|
+
if ('tracked_pids' in entry) {
|
|
2227
|
+
const trackedProcesses = entry.tracked_processes ?? [];
|
|
2228
|
+
const knownIdentities = new Map(trackedProcesses.map((identity) => [identity.pid, identity]));
|
|
2229
|
+
const states = await Promise.all(entry.tracked_pids.map((pid) => {
|
|
2230
|
+
const identity = knownIdentities.get(pid);
|
|
2231
|
+
return identity ? probeProcessIdentity(identity) : Promise.resolve(probePidLiveness(pid) === 'gone' ? 'gone' : 'reused_or_unknown');
|
|
2232
|
+
}));
|
|
2233
|
+
if (states.some((state) => state !== 'gone'))
|
|
2234
|
+
unresolved.push(entry);
|
|
2235
|
+
continue;
|
|
2236
|
+
}
|
|
2237
|
+
const states = await Promise.all(entry.tracked_processes.map(probeProcessIdentity));
|
|
2238
|
+
if (states.some((state) => state !== 'gone'))
|
|
2239
|
+
unresolved.push(entry);
|
|
2240
|
+
}
|
|
2241
|
+
if (unresolved.length === 0) {
|
|
2242
|
+
await removeDurableFile(debtPath);
|
|
2243
|
+
return;
|
|
2244
|
+
}
|
|
2245
|
+
await writeAtomic(debtPath, JSON.stringify({ ...debt, entries: unresolved }, null, 2));
|
|
2246
|
+
throw new Error(`gone_pane_descendant_cleanup_debt_unresolved:${unresolved.map((entry) => entry.pane_id).join(',')}`);
|
|
2247
|
+
}
|
|
1606
2248
|
async function teardownPromptWorker(teamName, workerName, fallbackPid, cwd, context) {
|
|
1607
2249
|
const handle = getPromptWorkerHandle(teamName, workerName);
|
|
1608
2250
|
const handlePid = handle?.pid;
|
|
@@ -1615,7 +2257,9 @@ async function teardownPromptWorker(teamName, workerName, fallbackPid, cwd, cont
|
|
|
1615
2257
|
return { terminated: true, forcedKill: false, pid: null };
|
|
1616
2258
|
}
|
|
1617
2259
|
const teardown = await terminateTrackedProcessTree(pid ?? 0, processGroupId);
|
|
1618
|
-
const processGone = processGroupId
|
|
2260
|
+
const processGone = processGroupId
|
|
2261
|
+
? probeProcessGroupLiveness(processGroupId) === 'gone'
|
|
2262
|
+
: isPidGone(pid);
|
|
1619
2263
|
if (teardown.terminated && processGone) {
|
|
1620
2264
|
removePromptWorkerHandle(teamName, workerName);
|
|
1621
2265
|
return { terminated: true, forcedKill: teardown.forcedKill, pid };
|
|
@@ -1625,31 +2269,27 @@ async function teardownPromptWorker(teamName, workerName, fallbackPid, cwd, cont
|
|
|
1625
2269
|
worker: workerName,
|
|
1626
2270
|
reason: `prompt_force_kill:${context}:pid=${pid}`,
|
|
1627
2271
|
}, cwd).catch(() => { });
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
};
|
|
1640
|
-
}
|
|
1641
|
-
removePromptWorkerHandle(teamName, workerName);
|
|
1642
|
-
return { terminated: true, forcedKill: teardown.forcedKill, pid };
|
|
2272
|
+
await appendTeamEvent(teamName, {
|
|
2273
|
+
type: 'worker_stopped',
|
|
2274
|
+
worker: workerName,
|
|
2275
|
+
reason: `prompt_teardown_failed:${context}:pid=${pid}`,
|
|
2276
|
+
}, cwd).catch(() => { });
|
|
2277
|
+
return {
|
|
2278
|
+
terminated: false,
|
|
2279
|
+
forcedKill: teardown.forcedKill,
|
|
2280
|
+
pid,
|
|
2281
|
+
error: 'still_alive_after_sigkill',
|
|
2282
|
+
};
|
|
1643
2283
|
}
|
|
1644
2284
|
function isPromptWorkerAlive(config, worker) {
|
|
1645
2285
|
const handle = getPromptWorkerHandle(config.name, worker.name);
|
|
1646
2286
|
if (handle?.child.exitCode === null && !handle.child.killed)
|
|
1647
2287
|
return true;
|
|
1648
|
-
if (handle?.processGroupId &&
|
|
2288
|
+
if (handle?.processGroupId && probeProcessGroupLiveness(handle.processGroupId) !== 'gone')
|
|
1649
2289
|
return true;
|
|
1650
|
-
if (process.platform !== 'win32' &&
|
|
2290
|
+
if (process.platform !== 'win32' && probeProcessGroupLiveness(worker.pid) !== 'gone')
|
|
1651
2291
|
return true;
|
|
1652
|
-
return
|
|
2292
|
+
return probePidLiveness(worker.pid) !== 'gone';
|
|
1653
2293
|
}
|
|
1654
2294
|
export { TEAM_LOW_COMPLEXITY_DEFAULT_MODEL };
|
|
1655
2295
|
export { resolveCanonicalTeamStateRoot };
|
|
@@ -1774,7 +2414,7 @@ async function writeDecompositionArtifacts(teamName, cwd, metadata) {
|
|
|
1774
2414
|
await writeFile(join(root, 'decomposition-report.json'), JSON.stringify(metadata, null, 2), 'utf8');
|
|
1775
2415
|
const manifest = await readTeamManifestV2(teamName, cwd);
|
|
1776
2416
|
if (manifest) {
|
|
1777
|
-
await
|
|
2417
|
+
await writeTeamManifestV2State({ ...manifest, team_decomposition: { ...metadata } }, cwd);
|
|
1778
2418
|
}
|
|
1779
2419
|
}
|
|
1780
2420
|
export async function settleStartupAttemptResults(attempts) {
|
|
@@ -1820,6 +2460,8 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
|
|
|
1820
2460
|
: rawIdentityScope;
|
|
1821
2461
|
const sanitized = buildInternalTeamName(displayName, identityScope);
|
|
1822
2462
|
const leaderSessionId = identityScope.sessionId || identityScope.paneId || identityScope.tmuxTarget || identityScope.runId;
|
|
2463
|
+
const existingStartupConfig = await readTeamConfig(sanitized, leaderCwd);
|
|
2464
|
+
await reconcileGonePaneDescendantCleanupDebt(sanitized, leaderCwd, existingStartupConfig ?? undefined);
|
|
1823
2465
|
await assertTeamStartupIsNonDestructive(sanitized, leaderCwd, leaderSessionId);
|
|
1824
2466
|
if (displayName !== sanitized) {
|
|
1825
2467
|
await assertTeamStartupIsNonDestructive(displayName, leaderCwd, leaderSessionId);
|
|
@@ -1913,7 +2555,18 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
|
|
|
1913
2555
|
let sessionCreated = false;
|
|
1914
2556
|
const createdWorkerPaneIds = [];
|
|
1915
2557
|
let createdLeaderPaneId;
|
|
2558
|
+
// A returned interactive session has successfully tagged these panes. Rollback
|
|
2559
|
+
// must retain that ownership authorization through its final kill proof.
|
|
2560
|
+
const startupTaggedPaneOwnerIds = new Map();
|
|
2561
|
+
const authorizeStartupRollbackPaneEffect = (paneId) => {
|
|
2562
|
+
const expectedOwnerId = startupTaggedPaneOwnerIds.get(paneId);
|
|
2563
|
+
if (!expectedOwnerId)
|
|
2564
|
+
return true;
|
|
2565
|
+
const currentOwner = readPaneTeamOwnerTagResult(paneId);
|
|
2566
|
+
return currentOwner.status === 'value' && currentOwner.value === expectedOwnerId;
|
|
2567
|
+
};
|
|
1916
2568
|
let config = null;
|
|
2569
|
+
const workerPaneIds = Array.from({ length: workerCount }, () => undefined);
|
|
1917
2570
|
const workerReadyTimeoutMs = resolveWorkerReadyTimeoutMs(launchEnv);
|
|
1918
2571
|
const workerStartupEvidenceTimeoutMs = resolveWorkerStartupEvidenceTimeoutMs(launchEnv, workerReadyTimeoutMs);
|
|
1919
2572
|
const startupDispatchRetries = resolveStartupDispatchRetries(launchEnv);
|
|
@@ -2111,7 +2764,6 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
|
|
|
2111
2764
|
workerRole: plan.workerRole,
|
|
2112
2765
|
};
|
|
2113
2766
|
});
|
|
2114
|
-
const workerPaneIds = Array.from({ length: workerCount }, () => undefined);
|
|
2115
2767
|
const materializeWorkerStartupState = async (bootstrapPlan, workerIndex, paneId) => {
|
|
2116
2768
|
const workerWorkspace = bootstrapPlan.workerWorkspace;
|
|
2117
2769
|
const identity = {
|
|
@@ -2128,13 +2780,19 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
|
|
|
2128
2780
|
worktree_created: workerWorkspace.worktreeCreated,
|
|
2129
2781
|
team_state_root: teamStateRoot,
|
|
2130
2782
|
};
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
if (
|
|
2134
|
-
|
|
2783
|
+
const persistedPanePid = config?.workers[workerIndex - 1]?.pid;
|
|
2784
|
+
if (workerLaunchMode === 'interactive' && paneId) {
|
|
2785
|
+
if (typeof persistedPanePid !== 'number' || !Number.isSafeInteger(persistedPanePid) || persistedPanePid <= 0) {
|
|
2786
|
+
throw new Error(`startup_worker_pane_pid_unavailable:${paneId}`);
|
|
2787
|
+
}
|
|
2788
|
+
const paneProof = readExactPaneProofSync(paneId);
|
|
2789
|
+
if (paneProof.status !== 'live' || paneProof.pid !== persistedPanePid) {
|
|
2790
|
+
throw new Error(`startup_worker_pane_identity_changed:${paneId}`);
|
|
2791
|
+
}
|
|
2792
|
+
identity.pid = persistedPanePid;
|
|
2135
2793
|
}
|
|
2136
|
-
else if (
|
|
2137
|
-
identity.pid =
|
|
2794
|
+
else if (typeof persistedPanePid === 'number') {
|
|
2795
|
+
identity.pid = persistedPanePid;
|
|
2138
2796
|
}
|
|
2139
2797
|
if (paneId)
|
|
2140
2798
|
identity.pane_id = paneId;
|
|
@@ -2179,6 +2837,20 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
|
|
|
2179
2837
|
createdWorkerPaneIds.push(...createdSession.workerPaneIds);
|
|
2180
2838
|
createdLeaderPaneId = createdSession.leaderPaneId;
|
|
2181
2839
|
applyCreatedInteractiveSessionToConfig(config, createdSession, workerPaneIds);
|
|
2840
|
+
const createdOwnerId = typeof createdSession.teamPaneOwnerId === 'string'
|
|
2841
|
+
? createdSession.teamPaneOwnerId.trim()
|
|
2842
|
+
: '';
|
|
2843
|
+
if (createdOwnerId) {
|
|
2844
|
+
for (const paneId of [
|
|
2845
|
+
createdSession.leaderPaneId,
|
|
2846
|
+
...createdSession.workerPaneIds,
|
|
2847
|
+
createdSession.hudPaneId,
|
|
2848
|
+
]) {
|
|
2849
|
+
if (typeof paneId === 'string' && paneId.trim().startsWith('%')) {
|
|
2850
|
+
startupTaggedPaneOwnerIds.set(paneId, createdOwnerId);
|
|
2851
|
+
}
|
|
2852
|
+
}
|
|
2853
|
+
}
|
|
2182
2854
|
for (const [index, paneId] of createdSession.workerPaneIds.entries()) {
|
|
2183
2855
|
startupTiming.mark('split_returned', { worker: `worker-${index + 1}`, pane_id: paneId });
|
|
2184
2856
|
}
|
|
@@ -2187,6 +2859,8 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
|
|
|
2187
2859
|
config.tmux_session = `prompt-${sanitized}`;
|
|
2188
2860
|
config.leader_pane_id = null;
|
|
2189
2861
|
config.hud_pane_id = null;
|
|
2862
|
+
config.leader_pane_pid = null;
|
|
2863
|
+
config.hud_pane_pid = null;
|
|
2190
2864
|
config.resize_hook_name = null;
|
|
2191
2865
|
config.resize_hook_target = null;
|
|
2192
2866
|
for (let i = 1; i <= workerCount; i++) {
|
|
@@ -2259,10 +2933,10 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
|
|
|
2259
2933
|
let startupReadyPromptObserved = false;
|
|
2260
2934
|
if (workerLaunchMode === 'interactive' && !skipWorkerReadyWait && !initialPrompt && !startupDirectOutcome?.ok) {
|
|
2261
2935
|
startupTiming.mark('ready_wait_start', { worker: workerName, pane_id: paneId });
|
|
2262
|
-
const ready = await waitForWorkerReadyAsync(sessionName, workerIndex, workerReadyTimeoutMs, paneId);
|
|
2936
|
+
const ready = await waitForWorkerReadyAsync(sessionName, workerIndex, workerReadyTimeoutMs, paneId, config.workers[workerIndex - 1]?.pid, config.tmux_pane_owner_id ?? undefined, config.hud_pane_id ?? undefined);
|
|
2263
2937
|
startupTiming.mark('ready_wait_end', { worker: workerName, pane_id: paneId, ok: ready });
|
|
2264
2938
|
if (!ready) {
|
|
2265
|
-
const workerAlive = isWorkerPaneOpen(sessionName, workerIndex, paneId);
|
|
2939
|
+
const workerAlive = isWorkerPaneOpen(sessionName, workerIndex, paneId, config.workers[workerIndex - 1]?.pid, config.tmux_pane_owner_id ?? undefined, config.hud_pane_id ?? undefined);
|
|
2266
2940
|
if (workerAlive) {
|
|
2267
2941
|
await recordRecoverableStartupIssue({
|
|
2268
2942
|
teamName: sanitized,
|
|
@@ -2323,8 +2997,8 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
|
|
|
2323
2997
|
break;
|
|
2324
2998
|
if (attempt < startupDispatchRetries) {
|
|
2325
2999
|
if (workerLaunchMode === 'interactive') {
|
|
2326
|
-
if (dismissTrustPromptIfPresent(sessionName, workerIndex, paneId)) {
|
|
2327
|
-
await waitForWorkerReadyAsync(sessionName, workerIndex, workerReadyTimeoutMs, paneId);
|
|
3000
|
+
if (dismissTrustPromptIfPresent(sessionName, workerIndex, paneId, config.workers[workerIndex - 1]?.pid, config.tmux_pane_owner_id ?? undefined, config.hud_pane_id ?? undefined)) {
|
|
3001
|
+
await waitForWorkerReadyAsync(sessionName, workerIndex, workerReadyTimeoutMs, paneId, config.workers[workerIndex - 1]?.pid, config.tmux_pane_owner_id ?? undefined, config.hud_pane_id ?? undefined);
|
|
2328
3002
|
}
|
|
2329
3003
|
else {
|
|
2330
3004
|
await new Promise((resolve) => setTimeout(resolve, Math.max(0, startupRetryDelayS * 1000)));
|
|
@@ -2339,7 +3013,7 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
|
|
|
2339
3013
|
if (!dispatchOutcome.ok) {
|
|
2340
3014
|
const workerAlive = workerLaunchMode === 'prompt'
|
|
2341
3015
|
? isPromptWorkerAlive(config, config.workers[workerIndex - 1])
|
|
2342
|
-
: isWorkerPaneOpen(sessionName, workerIndex, paneId);
|
|
3016
|
+
: isWorkerPaneOpen(sessionName, workerIndex, paneId, config.workers[workerIndex - 1]?.pid, config.tmux_pane_owner_id ?? undefined, config.hud_pane_id ?? undefined);
|
|
2343
3017
|
if (workerLaunchMode === 'prompt' && !workerAlive) {
|
|
2344
3018
|
await recordPromptStartupWorkerStopped({
|
|
2345
3019
|
teamName: sanitized,
|
|
@@ -2387,6 +3061,52 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
|
|
|
2387
3061
|
}
|
|
2388
3062
|
catch (error) {
|
|
2389
3063
|
const rollbackErrors = [];
|
|
3064
|
+
let preserveRollbackState = false;
|
|
3065
|
+
if (error instanceof CreateTeamSessionPartialError && config) {
|
|
3066
|
+
const partialSession = error.partialSession;
|
|
3067
|
+
sessionName = partialSession.name;
|
|
3068
|
+
sessionCreated = true;
|
|
3069
|
+
createdWorkerPaneIds.push(...partialSession.workerPaneIds);
|
|
3070
|
+
createdLeaderPaneId = partialSession.leaderPaneId;
|
|
3071
|
+
applyCreatedInteractiveSessionToConfig(config, partialSession, workerPaneIds);
|
|
3072
|
+
await saveTeamConfig(config, leaderCwd);
|
|
3073
|
+
const cleanupDebt = [
|
|
3074
|
+
...error.cleanupErrors,
|
|
3075
|
+
...error.proofUnavailable.map((proof) => `pane_proof_unavailable:${proof.paneId}:${proof.reason}`),
|
|
3076
|
+
];
|
|
3077
|
+
if (cleanupDebt.length > 0) {
|
|
3078
|
+
await appendTeamEvent(sanitized, {
|
|
3079
|
+
type: 'team_leader_nudge',
|
|
3080
|
+
worker: 'leader-fixed',
|
|
3081
|
+
reason: `startup_create_session_cleanup_debt:${cleanupDebt.join(';')}; retry preserved session resources`,
|
|
3082
|
+
}, leaderCwd).catch(() => { });
|
|
3083
|
+
}
|
|
3084
|
+
// CreateTeamSessionPartialError means create-time teardown did not reach a
|
|
3085
|
+
// proven-gone state. Preserve its config, hook registration, state and
|
|
3086
|
+
// worktrees exactly as saved above for a later retry; generic rollback
|
|
3087
|
+
// would otherwise destroy the retry evidence.
|
|
3088
|
+
assertPaneTeardownProofsAvailable('startup_rollback', error.proofUnavailable);
|
|
3089
|
+
throw error;
|
|
3090
|
+
}
|
|
3091
|
+
if (config && error instanceof Error && error.message.startsWith('startup_worker_pane_identity_changed:')) {
|
|
3092
|
+
const startupCleanupDebt = config.workers.flatMap((worker) => {
|
|
3093
|
+
if (!worker.pane_id || typeof worker.pid !== 'number' || !Number.isSafeInteger(worker.pid) || worker.pid <= 0)
|
|
3094
|
+
return [];
|
|
3095
|
+
const proof = readExactPaneProofSync(worker.pane_id);
|
|
3096
|
+
return proof.status === 'live' && proof.pid !== worker.pid
|
|
3097
|
+
? [{ status: 'unavailable', paneId: worker.pane_id, reason: 'pane_pid_changed', detail: `expected ${worker.pid}, got ${proof.pid}` }]
|
|
3098
|
+
: [];
|
|
3099
|
+
});
|
|
3100
|
+
if (startupCleanupDebt.length > 0) {
|
|
3101
|
+
await saveTeamConfig(config, leaderCwd);
|
|
3102
|
+
await appendTeamEvent(sanitized, {
|
|
3103
|
+
type: 'team_leader_nudge',
|
|
3104
|
+
worker: 'leader-fixed',
|
|
3105
|
+
reason: `startup_worker_pane_cleanup_debt:${startupCleanupDebt.map((proof) => `${proof.paneId}:${proof.reason}`).join(',')}; retry preserved session resources`,
|
|
3106
|
+
}, leaderCwd).catch(() => { });
|
|
3107
|
+
assertPaneTeardownProofsAvailable('startup_rollback', startupCleanupDebt);
|
|
3108
|
+
}
|
|
3109
|
+
}
|
|
2390
3110
|
if (sessionCreated) {
|
|
2391
3111
|
if (config?.resize_hook_name && config.resize_hook_target) {
|
|
2392
3112
|
try {
|
|
@@ -2399,44 +3119,87 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
|
|
|
2399
3119
|
rollbackErrors.push(`unregisterResizeHook: ${String(cleanupError)}`);
|
|
2400
3120
|
}
|
|
2401
3121
|
}
|
|
2402
|
-
if (config) {
|
|
2403
|
-
config.resize_hook_name = null;
|
|
2404
|
-
config.resize_hook_target = null;
|
|
2405
|
-
try {
|
|
2406
|
-
await saveTeamConfig(config, leaderCwd);
|
|
2407
|
-
}
|
|
2408
|
-
catch (cleanupError) {
|
|
2409
|
-
rollbackErrors.push(`saveTeamConfig(clear resize hook): ${String(cleanupError)}`);
|
|
2410
|
-
}
|
|
2411
|
-
}
|
|
2412
3122
|
// In split-pane topology, we must not kill the entire tmux session; kill only created panes.
|
|
2413
3123
|
if (sessionName.includes(':')) {
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
3124
|
+
const paneProcessProofUnavailable = [];
|
|
3125
|
+
for (const paneId of createdWorkerPaneIds) {
|
|
3126
|
+
const teardown = await terminateExactPaneProcessTree(paneId, config?.workers.find((worker) => worker.pane_id === paneId)?.pid, undefined, undefined, authorizeStartupRollbackPaneEffect);
|
|
3127
|
+
if (teardown.proofUnavailable) {
|
|
3128
|
+
if (config)
|
|
3129
|
+
await persistGonePaneDescendantCleanupDebt({ teamName: sanitized, cwd: leaderCwd, config, paneId, teardown });
|
|
3130
|
+
paneProcessProofUnavailable.push(teardown.proofUnavailable);
|
|
3131
|
+
break;
|
|
2421
3132
|
}
|
|
2422
|
-
|
|
2423
|
-
|
|
3133
|
+
if (!teardown.terminated && teardown.stopped) {
|
|
3134
|
+
if (config)
|
|
3135
|
+
await persistGonePaneDescendantCleanupDebt({ teamName: sanitized, cwd: leaderCwd, config, paneId, teardown });
|
|
3136
|
+
paneProcessProofUnavailable.push({
|
|
3137
|
+
status: 'unavailable',
|
|
3138
|
+
paneId,
|
|
3139
|
+
reason: 'pane_proof_lost_during_process_teardown',
|
|
3140
|
+
detail: 'exact pane authority was lost before the tracked process tree was resolved',
|
|
3141
|
+
});
|
|
3142
|
+
break;
|
|
2424
3143
|
}
|
|
2425
3144
|
}
|
|
2426
|
-
if (
|
|
2427
|
-
|
|
2428
|
-
await
|
|
3145
|
+
if (paneProcessProofUnavailable.length > 0) {
|
|
3146
|
+
if (config)
|
|
3147
|
+
await saveTeamConfig(config, leaderCwd);
|
|
3148
|
+
assertPaneTeardownProofsAvailable('startup_rollback', paneProcessProofUnavailable);
|
|
3149
|
+
}
|
|
3150
|
+
const rollbackPanePids = Object.fromEntries((config?.workers ?? [])
|
|
3151
|
+
.filter((worker) => typeof worker.pane_id === 'string' && typeof worker.pid === 'number')
|
|
3152
|
+
.map((worker) => [worker.pane_id, worker.pid]));
|
|
3153
|
+
if (config?.hud_pane_id && typeof config.hud_pane_pid === 'number') {
|
|
3154
|
+
rollbackPanePids[config.hud_pane_id] = config.hud_pane_pid;
|
|
3155
|
+
}
|
|
3156
|
+
const rollbackPaneIds = [
|
|
3157
|
+
...createdWorkerPaneIds,
|
|
3158
|
+
...(config?.hud_pane_id && typeof config.hud_pane_pid === 'number' ? [config.hud_pane_id] : []),
|
|
3159
|
+
].filter((paneId) => typeof paneId === 'string' && paneId.trim().startsWith('%'));
|
|
3160
|
+
const rollbackPaneTeardown = await teardownWorkerPanes(rollbackPaneIds, {
|
|
3161
|
+
leaderPaneId: createdLeaderPaneId,
|
|
3162
|
+
expectedPanePids: rollbackPanePids,
|
|
3163
|
+
authorizePaneKill: (paneId) => authorizeStartupRollbackPaneEffect(paneId),
|
|
3164
|
+
});
|
|
3165
|
+
const resolvedRollbackPaneIds = new Set([
|
|
3166
|
+
...rollbackPaneTeardown.provenGonePaneIds,
|
|
3167
|
+
...rollbackPaneTeardown.killedPaneIds,
|
|
3168
|
+
]);
|
|
3169
|
+
if (config && (rollbackPaneTeardown.proofUnavailable.length > 0 || rollbackPaneTeardown.kill.failed > 0)) {
|
|
3170
|
+
config.workers = config.workers.filter((worker) => !worker.pane_id || !resolvedRollbackPaneIds.has(worker.pane_id));
|
|
3171
|
+
config.worker_count = config.workers.length;
|
|
3172
|
+
if (config.hud_pane_id && resolvedRollbackPaneIds.has(config.hud_pane_id)) {
|
|
3173
|
+
config.hud_pane_id = null;
|
|
3174
|
+
config.hud_pane_pid = null;
|
|
2429
3175
|
}
|
|
2430
|
-
|
|
2431
|
-
|
|
3176
|
+
}
|
|
3177
|
+
if (rollbackPaneTeardown.proofUnavailable.length > 0) {
|
|
3178
|
+
if (config) {
|
|
3179
|
+
try {
|
|
3180
|
+
await saveTeamConfig(config, leaderCwd);
|
|
3181
|
+
}
|
|
3182
|
+
catch (cleanupError) {
|
|
3183
|
+
rollbackErrors.push(`saveTeamConfig(preserve rollback state): ${String(cleanupError)}`);
|
|
3184
|
+
}
|
|
2432
3185
|
}
|
|
3186
|
+
assertPaneTeardownProofsAvailable('startup_rollback', rollbackPaneTeardown.proofUnavailable);
|
|
3187
|
+
}
|
|
3188
|
+
if (rollbackPaneTeardown.kill.failed > 0) {
|
|
3189
|
+
if (config)
|
|
3190
|
+
await saveTeamConfig(config, leaderCwd);
|
|
3191
|
+
const originalMessage = error instanceof Error ? error.message : String(error);
|
|
3192
|
+
throw new Error(`${originalMessage}; rollback encountered errors: paneTeardown:${rollbackPaneTeardown.kill.failedPaneIds.join(',')}`);
|
|
2433
3193
|
}
|
|
2434
3194
|
}
|
|
2435
3195
|
else {
|
|
2436
3196
|
try {
|
|
2437
|
-
|
|
3197
|
+
if (!config)
|
|
3198
|
+
throw new Error('detached_session_destroy_authorization_unavailable:missing_config');
|
|
3199
|
+
await destroyConfiguredDetachedTeamSession(sanitized, leaderCwd, config);
|
|
2438
3200
|
}
|
|
2439
3201
|
catch (cleanupError) {
|
|
3202
|
+
preserveRollbackState = true;
|
|
2440
3203
|
rollbackErrors.push(`destroyTeamSession: ${String(cleanupError)}`);
|
|
2441
3204
|
}
|
|
2442
3205
|
}
|
|
@@ -2474,11 +3237,21 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
|
|
|
2474
3237
|
}
|
|
2475
3238
|
}
|
|
2476
3239
|
restoreTeamModelInstructionsFile(sanitized);
|
|
3240
|
+
let descendantCleanupDebtUnresolved = false;
|
|
2477
3241
|
try {
|
|
2478
|
-
await
|
|
3242
|
+
await reconcileGonePaneDescendantCleanupDebt(sanitized, leaderCwd, config ?? undefined);
|
|
2479
3243
|
}
|
|
2480
3244
|
catch (cleanupError) {
|
|
2481
|
-
|
|
3245
|
+
descendantCleanupDebtUnresolved = true;
|
|
3246
|
+
rollbackErrors.push(`reconcileGonePaneDescendantCleanupDebt: ${String(cleanupError)}`);
|
|
3247
|
+
}
|
|
3248
|
+
if (!descendantCleanupDebtUnresolved && !preserveRollbackState) {
|
|
3249
|
+
try {
|
|
3250
|
+
await cleanupTeamState(sanitized, leaderCwd);
|
|
3251
|
+
}
|
|
3252
|
+
catch (cleanupError) {
|
|
3253
|
+
rollbackErrors.push(`cleanupTeamState: ${String(cleanupError)}`);
|
|
3254
|
+
}
|
|
2482
3255
|
}
|
|
2483
3256
|
if (provisionedWorktrees.length > 0) {
|
|
2484
3257
|
try {
|
|
@@ -2541,7 +3314,7 @@ export async function monitorTeam(teamName, cwd) {
|
|
|
2541
3314
|
const workerSignals = await Promise.all(config.workers.map(async (worker) => {
|
|
2542
3315
|
const alive = config.worker_launch_mode === 'prompt'
|
|
2543
3316
|
? isPromptWorkerAlive(config, worker)
|
|
2544
|
-
: isWorkerAlive(sessionName, worker.index, worker.pane_id);
|
|
3317
|
+
: isWorkerAlive(sessionName, worker.index, worker.pane_id, worker.pid, config.tmux_pane_owner_id ?? undefined, config.hud_pane_id ?? undefined);
|
|
2545
3318
|
const [status, heartbeat] = await Promise.all([
|
|
2546
3319
|
readWorkerStatus(sanitized, worker.name, cwd),
|
|
2547
3320
|
readWorkerHeartbeat(sanitized, worker.name, cwd),
|
|
@@ -2791,8 +3564,8 @@ export async function assignTask(teamName, workerName, taskId, cwd) {
|
|
|
2791
3564
|
if (outcome.ok)
|
|
2792
3565
|
break;
|
|
2793
3566
|
if (attempt < maxAssignRetries && config.worker_launch_mode === 'interactive' && config.tmux_session) {
|
|
2794
|
-
if (dismissTrustPromptIfPresent(config.tmux_session, workerInfo.index, workerInfo.pane_id)) {
|
|
2795
|
-
waitForWorkerReady(config.tmux_session, workerInfo.index, resolveWorkerReadyTimeoutMs(process.env), workerInfo.pane_id);
|
|
3567
|
+
if (dismissTrustPromptIfPresent(config.tmux_session, workerInfo.index, workerInfo.pane_id, workerInfo.pid, config.tmux_pane_owner_id ?? undefined, config.hud_pane_id ?? undefined)) {
|
|
3568
|
+
waitForWorkerReady(config.tmux_session, workerInfo.index, resolveWorkerReadyTimeoutMs(process.env), workerInfo.pane_id, workerInfo.pid, config.tmux_pane_owner_id ?? undefined, config.hud_pane_id ?? undefined);
|
|
2796
3569
|
}
|
|
2797
3570
|
else {
|
|
2798
3571
|
await new Promise(r => setTimeout(r, assignRetryDelayS * 1000));
|
|
@@ -2857,20 +3630,62 @@ export async function shutdownTeam(teamName, cwd, options = {}) {
|
|
|
2857
3630
|
const confirmIssues = options.confirmIssues === true;
|
|
2858
3631
|
let skipWorkerAcks = false;
|
|
2859
3632
|
const sanitized = resolveTeamNameForCurrentContext(teamName, cwd);
|
|
2860
|
-
|
|
3633
|
+
let config = await readTeamConfig(sanitized, cwd);
|
|
2861
3634
|
if (!config) {
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
catch (err) {
|
|
2867
|
-
process.stderr.write(`[team/runtime] operation failed: ${err}\n`);
|
|
2868
|
-
}
|
|
3635
|
+
await reconcileDetachedSessionDestroyReceipt(sanitized, cwd, null);
|
|
3636
|
+
await reconcileGonePaneDescendantCleanupDebt(sanitized, cwd);
|
|
3637
|
+
// A missing config is not authority over a conventionally named tmux
|
|
3638
|
+
// session. It may be another team's or a recycled user session.
|
|
2869
3639
|
await cleanupTeamState(sanitized, cwd);
|
|
2870
3640
|
await syncTeamModeStateOnShutdown(sanitized, cwd);
|
|
2871
3641
|
restoreTeamModelInstructionsFile(sanitized);
|
|
2872
3642
|
return { commitHygieneArtifacts: null };
|
|
2873
3643
|
}
|
|
3644
|
+
// Reconcile accepted detached-session destruction before any other shutdown
|
|
3645
|
+
// effect. A receipt is the only durable authority across the kill/query crash
|
|
3646
|
+
// window and is deliberately fail-closed on malformed or unavailable input.
|
|
3647
|
+
await reconcileDetachedSessionDestroyReceipt(sanitized, cwd, config);
|
|
3648
|
+
config = await readTeamConfig(sanitized, cwd);
|
|
3649
|
+
if (!config)
|
|
3650
|
+
throw new Error(`shutdown_config_missing_after_detached_reconciliation:${sanitized}`);
|
|
3651
|
+
const restoredHudDebtRoot = join(config.team_state_root ?? resolveCanonicalTeamStateRoot(cwd), 'team', sanitized);
|
|
3652
|
+
const configuredRestoredHudPaneId = typeof config.hud_pane_id === 'string' && /^%[0-9]+$/.test(config.hud_pane_id)
|
|
3653
|
+
? config.hud_pane_id
|
|
3654
|
+
: null;
|
|
3655
|
+
const configuredRestoredHudPanePid = typeof config.hud_pane_pid === 'number'
|
|
3656
|
+
&& Number.isSafeInteger(config.hud_pane_pid)
|
|
3657
|
+
&& config.hud_pane_pid > 0
|
|
3658
|
+
? config.hud_pane_pid
|
|
3659
|
+
: null;
|
|
3660
|
+
if (configuredRestoredHudPaneId && configuredRestoredHudPanePid) {
|
|
3661
|
+
try {
|
|
3662
|
+
// A restored-HUD debt is finalized only when this canonical config
|
|
3663
|
+
// transaction records its exact frozen pane identity. A stale config may
|
|
3664
|
+
// point to a different HUD after a crash, so replay the pinned debt
|
|
3665
|
+
// rather than treating that mismatch as successful finalization.
|
|
3666
|
+
finalizeRestoredHudCleanupDebtSync(cwd, configuredRestoredHudPaneId, configuredRestoredHudPanePid, restoredHudDebtRoot, true);
|
|
3667
|
+
}
|
|
3668
|
+
catch (error) {
|
|
3669
|
+
if (!(error instanceof Error) || !error.message.startsWith('restored_hud_cleanup_debt_unresolved:')) {
|
|
3670
|
+
throw error;
|
|
3671
|
+
}
|
|
3672
|
+
// The config identity is not the debt identity. Remove it durably before
|
|
3673
|
+
// replaying the pinned obligation: a later detached-session teardown must
|
|
3674
|
+
// not treat a live, same-PID/owner non-HUD pane as the restored HUD.
|
|
3675
|
+
config = await mutateShutdownConfig(config, cwd, (current) => {
|
|
3676
|
+
current.hud_pane_id = null;
|
|
3677
|
+
current.hud_pane_pid = undefined;
|
|
3678
|
+
});
|
|
3679
|
+
reconcileRestoredHudCleanupDebtSync(cwd, restoredHudDebtRoot);
|
|
3680
|
+
}
|
|
3681
|
+
}
|
|
3682
|
+
else {
|
|
3683
|
+
reconcileRestoredHudCleanupDebtSync(cwd, restoredHudDebtRoot);
|
|
3684
|
+
}
|
|
3685
|
+
const priorScaleDownCleanup = await reconcileScaleDownCleanupDebt(sanitized, cwd, config);
|
|
3686
|
+
if (!priorScaleDownCleanup.ok)
|
|
3687
|
+
throw new Error(priorScaleDownCleanup.error);
|
|
3688
|
+
await reconcileGonePaneDescendantCleanupDebt(sanitized, cwd, config);
|
|
2874
3689
|
const manifest = await readTeamManifestV2(sanitized, cwd);
|
|
2875
3690
|
const leaderSessionId = typeof manifest?.leader?.session_id === 'string'
|
|
2876
3691
|
? manifest.leader.session_id.trim()
|
|
@@ -2909,6 +3724,19 @@ export async function shutdownTeam(teamName, cwd, options = {}) {
|
|
|
2909
3724
|
skipWorkerAcks = true;
|
|
2910
3725
|
}
|
|
2911
3726
|
const sessionName = config.tmux_session;
|
|
3727
|
+
const sharedSessionTopology = config.worker_launch_mode === 'interactive' && sessionName.includes(':')
|
|
3728
|
+
? resolveSharedSessionShutdownTopology(sessionName, config.leader_pane_id, sanitized)
|
|
3729
|
+
: null;
|
|
3730
|
+
if (sharedSessionTopology?.status === 'unavailable') {
|
|
3731
|
+
throw new Error(`shutdown_shared_session_topology_unavailable:${sharedSessionTopology.detail}`);
|
|
3732
|
+
}
|
|
3733
|
+
if (typeof config.hud_pane_id === 'string' && /^%[0-9]+$/.test(config.hud_pane_id)
|
|
3734
|
+
&& typeof config.hud_pane_pid === 'number' && Number.isSafeInteger(config.hud_pane_pid) && config.hud_pane_pid > 0) {
|
|
3735
|
+
const persistedHudProof = readExactPaneProofSync(config.hud_pane_id);
|
|
3736
|
+
if (persistedHudProof.status === 'live' && persistedHudProof.pid !== config.hud_pane_pid) {
|
|
3737
|
+
throw new Error(`shutdown_shared_session_HUD_pane_identity_changed:${config.hud_pane_id}`);
|
|
3738
|
+
}
|
|
3739
|
+
}
|
|
2912
3740
|
const dispatchPolicy = resolveDispatchPolicy(manifest?.policy, config.worker_launch_mode);
|
|
2913
3741
|
const shutdownRequestTimes = new Map();
|
|
2914
3742
|
if (!skipWorkerAcks) {
|
|
@@ -2964,7 +3792,7 @@ export async function shutdownTeam(teamName, cwd, options = {}) {
|
|
|
2964
3792
|
}
|
|
2965
3793
|
const anyAlive = config.workers.some((w) => (config.worker_launch_mode === 'prompt'
|
|
2966
3794
|
? isPromptWorkerAlive(config, w)
|
|
2967
|
-
: isWorkerAlive(sessionName, w.index, w.pane_id)));
|
|
3795
|
+
: isWorkerAlive(sessionName, w.index, w.pane_id, w.pid, config.tmux_pane_owner_id ?? undefined, config.hud_pane_id ?? undefined)));
|
|
2968
3796
|
if (!anyAlive)
|
|
2969
3797
|
break;
|
|
2970
3798
|
// Sleep 2s
|
|
@@ -2972,7 +3800,7 @@ export async function shutdownTeam(teamName, cwd, options = {}) {
|
|
|
2972
3800
|
}
|
|
2973
3801
|
const anyAliveAfterWait = config.workers.some((w) => (config.worker_launch_mode === 'prompt'
|
|
2974
3802
|
? isPromptWorkerAlive(config, w)
|
|
2975
|
-
: isWorkerAlive(sessionName, w.index, w.pane_id)));
|
|
3803
|
+
: isWorkerAlive(sessionName, w.index, w.pane_id, w.pid, config.tmux_pane_owner_id ?? undefined, config.hud_pane_id ?? undefined)));
|
|
2976
3804
|
if (anyAliveAfterWait && !force) {
|
|
2977
3805
|
// Workers may have accepted shutdown but not exited (Codex TUI requires explicit exit).
|
|
2978
3806
|
// In this case, proceed to force kill panes (next step) rather than failing and leaving state around.
|
|
@@ -2981,15 +3809,13 @@ export async function shutdownTeam(teamName, cwd, options = {}) {
|
|
|
2981
3809
|
// 3. Force kill remaining workers
|
|
2982
3810
|
const leaderPaneId = config.leader_pane_id;
|
|
2983
3811
|
const hudPaneId = config.hud_pane_id;
|
|
3812
|
+
const leaderPanePid = config.leader_pane_pid;
|
|
3813
|
+
const hudPanePid = config.hud_pane_pid;
|
|
2984
3814
|
if (config.worker_launch_mode === 'interactive') {
|
|
2985
|
-
const
|
|
2986
|
-
?
|
|
2987
|
-
:
|
|
2988
|
-
const effectiveLeaderPaneId = sharedSessionTopology ? sharedSessionTopology.leaderPaneId : leaderPaneId;
|
|
3815
|
+
const effectiveLeaderPaneId = sharedSessionTopology?.status === 'available'
|
|
3816
|
+
? sharedSessionTopology.leaderPaneId
|
|
3817
|
+
: leaderPaneId;
|
|
2989
3818
|
const tmuxPaneOwnerId = typeof config.tmux_pane_owner_id === 'string' ? config.tmux_pane_owner_id.trim() : '';
|
|
2990
|
-
const legacyPersistedWorkerPaneIds = new Set(config.workers
|
|
2991
|
-
.map((worker) => (typeof worker.pane_id === 'string' ? worker.pane_id.trim() : ''))
|
|
2992
|
-
.filter((paneId) => paneId.startsWith('%')));
|
|
2993
3819
|
const ownerReadWarnings = new Set();
|
|
2994
3820
|
const warnOwnerReadError = (kind, paneId, error) => {
|
|
2995
3821
|
const key = `${kind}:${paneId}:${error}`;
|
|
@@ -3005,57 +3831,253 @@ export async function shutdownTeam(teamName, cwd, options = {}) {
|
|
|
3005
3831
|
legacyInstanceId: leaderSessionId,
|
|
3006
3832
|
}) ? effectiveLeaderPaneId : null)
|
|
3007
3833
|
: effectiveLeaderPaneId;
|
|
3008
|
-
const
|
|
3009
|
-
?
|
|
3834
|
+
const reclaimableHudPaneIds = sharedSessionTopology
|
|
3835
|
+
? sharedSessionTopology.hudPaneIds.filter((paneId) => isSharedSessionHudPaneReclaimable({
|
|
3010
3836
|
paneId,
|
|
3011
3837
|
persistedHudPaneId: hudPaneId,
|
|
3012
3838
|
leaderOwnedHudPaneIds: sharedSessionTopology.leaderOwnedHudPaneIds,
|
|
3013
3839
|
teamPaneOwnerId: tmuxPaneOwnerId,
|
|
3014
|
-
onOwnerReadError: (
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
3840
|
+
onOwnerReadError: (candidateHudPaneId, error) => {
|
|
3841
|
+
warnOwnerReadError('HUD pane', candidateHudPaneId, error);
|
|
3842
|
+
},
|
|
3843
|
+
}))
|
|
3844
|
+
: (hudPaneId ? [hudPaneId] : []);
|
|
3845
|
+
const effectiveHudPaneId = reclaimableHudPaneIds[0] ?? null;
|
|
3846
|
+
const explicitPersistedWorkerPaneIds = new Set(config.workers
|
|
3847
|
+
.map((worker) => (typeof worker.pane_id === 'string' ? worker.pane_id.trim() : ''))
|
|
3848
|
+
.filter((paneId) => /^%[0-9]+$/.test(paneId))
|
|
3849
|
+
.filter((paneId) => paneId !== leaderPaneId && paneId !== hudPaneId));
|
|
3850
|
+
// Every persisted worker pane is a canonical shutdown member. Command markers
|
|
3851
|
+
// only discover additional candidates; they cannot erase canonical membership.
|
|
3852
|
+
const canonicalExplicitWorkerPaneIds = new Set(explicitPersistedWorkerPaneIds);
|
|
3853
|
+
const initiallyTaggedWorkerPaneIds = new Set();
|
|
3854
|
+
const authorizedDiscoveredWorkerPaneIds = sharedSessionTopology
|
|
3855
|
+
? collectAuthorizedSharedSessionWorkerPaneIds([...new Set([
|
|
3856
|
+
...canonicalExplicitWorkerPaneIds,
|
|
3857
|
+
...sharedSessionTopology.teamWorkerPaneIds,
|
|
3858
|
+
])], tmuxPaneOwnerId, canonicalExplicitWorkerPaneIds, undefined, initiallyTaggedWorkerPaneIds)
|
|
3859
|
+
: (sessionName ? listPaneIds(sessionName) : []);
|
|
3860
|
+
const excludedSharedWorkerPaneIds = new Set([effectiveLeaderPaneId, effectiveHudPaneId]
|
|
3861
|
+
.filter((paneId) => typeof paneId === 'string' && /^%[0-9]+$/.test(paneId)));
|
|
3862
|
+
const sharedWorkerPaneIds = [...new Set(authorizedDiscoveredWorkerPaneIds)];
|
|
3863
|
+
if (sharedSessionTopology && sharedWorkerPaneIds.some((paneId) => excludedSharedWorkerPaneIds.has(paneId))) {
|
|
3864
|
+
throw new Error(`shutdown_shared_session_worker_target_invalid:${sharedWorkerPaneIds.filter((paneId) => excludedSharedWorkerPaneIds.has(paneId)).join(',')}`);
|
|
3865
|
+
}
|
|
3866
|
+
// Freeze this union before any shared-session topology effect. HUD teardown
|
|
3867
|
+
// and restoration must never cause later worker target rediscovery.
|
|
3868
|
+
const shutdownPaneIds = sharedSessionTopology
|
|
3869
|
+
? sharedWorkerPaneIds
|
|
3870
|
+
: collectShutdownPaneIds({
|
|
3871
|
+
config,
|
|
3872
|
+
candidatePaneIds: authorizedDiscoveredWorkerPaneIds,
|
|
3873
|
+
leaderPaneId: effectiveLeaderPaneId,
|
|
3874
|
+
hudPaneId: effectiveHudPaneId,
|
|
3875
|
+
});
|
|
3876
|
+
const canonicalWorkerPaneIds = [...shutdownPaneIds];
|
|
3877
|
+
const expectedSharedWorkerPanePids = new Map();
|
|
3878
|
+
const prekillResolvedWorkerPaneIds = new Set();
|
|
3879
|
+
const freezeSharedPaneAuthorization = (paneId, kind, expectedPid) => {
|
|
3880
|
+
const proof = readExactPaneProofSync(paneId);
|
|
3881
|
+
if (proof.status !== 'live') {
|
|
3882
|
+
throw new Error(`shutdown_shared_session_${kind.replaceAll(' ', '_')}_proof_unavailable:${paneId}`);
|
|
3883
|
+
}
|
|
3884
|
+
if (typeof expectedPid === 'number') {
|
|
3885
|
+
if (!Number.isSafeInteger(expectedPid) || expectedPid <= 0) {
|
|
3886
|
+
throw new Error(`shutdown_shared_session_${kind.replaceAll(' ', '_')}_pid_missing:${paneId}`);
|
|
3887
|
+
}
|
|
3888
|
+
if (proof.pid !== expectedPid) {
|
|
3889
|
+
throw new Error(`shutdown_shared_session_${kind.replaceAll(' ', '_')}_identity_changed:${paneId}`);
|
|
3890
|
+
}
|
|
3891
|
+
}
|
|
3892
|
+
const owner = readPaneTeamOwnerTagResult(paneId);
|
|
3893
|
+
if (owner.status === 'error') {
|
|
3894
|
+
throw new Error(`shutdown_shared_session_${kind.replaceAll(' ', '_')}_owner_unavailable:${paneId}:${owner.error}`);
|
|
3895
|
+
}
|
|
3896
|
+
if (owner.status !== 'value' || !tmuxPaneOwnerId || owner.value !== tmuxPaneOwnerId) {
|
|
3897
|
+
throw new Error(`shutdown_shared_session_${kind.replaceAll(' ', '_')}_owner_changed:${paneId}`);
|
|
3898
|
+
}
|
|
3899
|
+
return { paneId, pid: proof.pid, owner: owner.value };
|
|
3900
|
+
};
|
|
3901
|
+
const assertSharedPaneAuthorizationContinuous = (authorization, kind) => {
|
|
3902
|
+
const scope = sharedSessionTopology ? 'shared_session' : 'detached_session';
|
|
3903
|
+
const proof = readExactPaneProofSync(authorization.paneId);
|
|
3904
|
+
if (proof.status !== 'live' || proof.pid !== authorization.pid) {
|
|
3905
|
+
throw new Error(`shutdown_${scope}_${kind.replaceAll(' ', '_')}_identity_changed:${authorization.paneId}`);
|
|
3906
|
+
}
|
|
3907
|
+
const owner = readPaneTeamOwnerTagResult(authorization.paneId);
|
|
3908
|
+
if (owner.status === 'error') {
|
|
3909
|
+
throw new Error(`shutdown_${scope}_${kind.replaceAll(' ', '_')}_owner_unavailable:${authorization.paneId}:${owner.error}`);
|
|
3910
|
+
}
|
|
3911
|
+
const currentOwner = owner.status === 'value' ? owner.value : null;
|
|
3912
|
+
if (currentOwner !== authorization.owner) {
|
|
3913
|
+
throw new Error(`shutdown_${scope}_${kind.replaceAll(' ', '_')}_owner_changed:${authorization.paneId}`);
|
|
3033
3914
|
}
|
|
3915
|
+
};
|
|
3916
|
+
const persistedHudPaneId = typeof hudPaneId === 'string' && /^%[0-9]+$/.test(hudPaneId)
|
|
3917
|
+
? hudPaneId
|
|
3918
|
+
: null;
|
|
3919
|
+
const persistedLeaderPaneId = typeof leaderPaneId === 'string' && /^%[0-9]+$/.test(leaderPaneId)
|
|
3920
|
+
? leaderPaneId
|
|
3921
|
+
: null;
|
|
3922
|
+
if (persistedHudPaneId && effectiveHudPaneId === persistedHudPaneId && (typeof hudPanePid !== 'number' || !Number.isSafeInteger(hudPanePid) || hudPanePid <= 0)) {
|
|
3923
|
+
const scope = sharedSessionTopology ? 'shared_session' : 'detached_session';
|
|
3924
|
+
throw new Error(`shutdown_${scope}_HUD_pane_pid_missing:${effectiveHudPaneId}`);
|
|
3034
3925
|
}
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
3926
|
+
const frozenHudAuthorization = effectiveHudPaneId
|
|
3927
|
+
? (sharedSessionTopology
|
|
3928
|
+
? freezeSharedPaneAuthorization(effectiveHudPaneId, 'HUD pane', effectiveHudPaneId === persistedHudPaneId ? hudPanePid : undefined)
|
|
3929
|
+
: (() => {
|
|
3930
|
+
if (!persistedHudPaneId || effectiveHudPaneId !== persistedHudPaneId || !tmuxPaneOwnerId
|
|
3931
|
+
|| typeof hudPanePid !== 'number' || !Number.isSafeInteger(hudPanePid) || hudPanePid <= 0) {
|
|
3932
|
+
throw new Error(`shutdown_detached_session_HUD_pane_authorization_unavailable:${effectiveHudPaneId}`);
|
|
3933
|
+
}
|
|
3934
|
+
const proof = readExactPaneProofSync(effectiveHudPaneId);
|
|
3935
|
+
if (proof.status !== 'live' || proof.pid !== hudPanePid) {
|
|
3936
|
+
throw new Error(`shutdown_detached_session_HUD_pane_identity_changed:${effectiveHudPaneId}`);
|
|
3937
|
+
}
|
|
3938
|
+
const owner = readPaneTeamOwnerTagResult(proof.paneId);
|
|
3939
|
+
if (owner.status !== 'value' || owner.value !== tmuxPaneOwnerId) {
|
|
3940
|
+
throw new Error(`shutdown_detached_session_HUD_pane_owner_changed:${effectiveHudPaneId}`);
|
|
3941
|
+
}
|
|
3942
|
+
const finalProof = readExactPaneProofSync(proof.paneId);
|
|
3943
|
+
if (finalProof.status !== 'live' || finalProof.pid !== hudPanePid) {
|
|
3944
|
+
throw new Error(`shutdown_detached_session_HUD_pane_identity_changed:${effectiveHudPaneId}`);
|
|
3945
|
+
}
|
|
3946
|
+
return { paneId: finalProof.paneId, pid: hudPanePid, owner: tmuxPaneOwnerId };
|
|
3947
|
+
})())
|
|
3948
|
+
: null;
|
|
3949
|
+
if (persistedLeaderPaneId && trustedHudRestoreLeaderPaneId === persistedLeaderPaneId && (typeof leaderPanePid !== 'number' || !Number.isSafeInteger(leaderPanePid) || leaderPanePid <= 0)) {
|
|
3950
|
+
throw new Error(`shutdown_shared_session_restore_leader_pane_pid_missing:${trustedHudRestoreLeaderPaneId}`);
|
|
3951
|
+
}
|
|
3952
|
+
const frozenRestoreLeaderAuthorization = sharedSessionTopology && trustedHudRestoreLeaderPaneId
|
|
3953
|
+
? freezeSharedPaneAuthorization(trustedHudRestoreLeaderPaneId, 'restore leader pane', trustedHudRestoreLeaderPaneId === persistedLeaderPaneId ? leaderPanePid : undefined)
|
|
3954
|
+
: null;
|
|
3955
|
+
const assertCompleteSharedWorkerPaneProofs = (allowResolvedGone = false) => {
|
|
3956
|
+
const unavailable = readExactPaneProofsSync(canonicalWorkerPaneIds).flatMap((proof) => {
|
|
3957
|
+
if (proof.status !== 'live') {
|
|
3958
|
+
if (allowResolvedGone && proof.status === 'gone' && prekillResolvedWorkerPaneIds.has(proof.paneId))
|
|
3959
|
+
return [];
|
|
3960
|
+
return proof.status === 'unavailable'
|
|
3961
|
+
? [proof]
|
|
3962
|
+
: [{ status: 'unavailable', paneId: proof.paneId, reason: 'pane_proof_lost_during_process_teardown' }];
|
|
3963
|
+
}
|
|
3964
|
+
const persistedPid = config.workers.find((worker) => worker.pane_id?.trim() === proof.paneId)?.pid;
|
|
3965
|
+
if (canonicalExplicitWorkerPaneIds.has(proof.paneId)
|
|
3966
|
+
&& (typeof persistedPid !== 'number' || !Number.isSafeInteger(persistedPid) || persistedPid <= 0)) {
|
|
3967
|
+
return [{ status: 'unavailable', paneId: proof.paneId, reason: 'pane_pid_changed', detail: 'canonical persisted pane PID is missing' }];
|
|
3968
|
+
}
|
|
3969
|
+
const expectedPid = typeof persistedPid === 'number' ? persistedPid : expectedSharedWorkerPanePids.get(proof.paneId);
|
|
3970
|
+
if (typeof expectedPid === 'number' && proof.pid !== expectedPid) {
|
|
3971
|
+
return [{ status: 'unavailable', paneId: proof.paneId, reason: 'pane_pid_changed', detail: `expected ${expectedPid}, got ${proof.pid}` }];
|
|
3972
|
+
}
|
|
3973
|
+
expectedSharedWorkerPanePids.set(proof.paneId, proof.pid);
|
|
3974
|
+
return [];
|
|
3975
|
+
});
|
|
3976
|
+
assertPaneTeardownProofsAvailable('shutdown', unavailable);
|
|
3977
|
+
};
|
|
3978
|
+
const frozenSharedWorkerOwnerIds = new Map();
|
|
3979
|
+
if (sharedSessionTopology) {
|
|
3980
|
+
assertCompleteSharedWorkerPaneProofs();
|
|
3981
|
+
for (const paneId of canonicalWorkerPaneIds) {
|
|
3982
|
+
const owner = readPaneTeamOwnerTagResult(paneId);
|
|
3983
|
+
if (owner.status === 'error') {
|
|
3984
|
+
throw new Error(`shutdown_shared_session_worker_owner_unavailable:${paneId}:${owner.error}`);
|
|
3044
3985
|
}
|
|
3986
|
+
if (owner.status !== 'value' || !tmuxPaneOwnerId || owner.value !== tmuxPaneOwnerId) {
|
|
3987
|
+
throw new Error(`shutdown_shared_session_worker_owner_changed:${paneId}`);
|
|
3988
|
+
}
|
|
3989
|
+
frozenSharedWorkerOwnerIds.set(paneId, owner.value);
|
|
3045
3990
|
}
|
|
3046
3991
|
}
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
3992
|
+
const authorizeFrozenSharedWorkerProcessSignal = (paneId, panePid) => {
|
|
3993
|
+
const expectedPid = expectedSharedWorkerPanePids.get(paneId)
|
|
3994
|
+
?? config.workers.find((worker) => worker.pane_id === paneId)?.pid;
|
|
3995
|
+
if (expectedPid !== panePid)
|
|
3996
|
+
return false;
|
|
3997
|
+
if (sharedSessionTopology && frozenSharedWorkerOwnerIds.has(paneId)) {
|
|
3998
|
+
const expectedOwner = frozenSharedWorkerOwnerIds.get(paneId);
|
|
3999
|
+
const owner = readPaneTeamOwnerTagResult(paneId);
|
|
4000
|
+
return owner.status === 'value' && owner.value === expectedOwner && expectedOwner === tmuxPaneOwnerId;
|
|
4001
|
+
}
|
|
4002
|
+
const owner = readPaneTeamOwnerTagResult(paneId);
|
|
4003
|
+
return owner.status === 'value' && owner.value === tmuxPaneOwnerId;
|
|
4004
|
+
};
|
|
4005
|
+
if (shouldPrekillInteractiveShutdownProcessTrees(sessionName)) {
|
|
4006
|
+
const paneProcessProofUnavailable = [];
|
|
4007
|
+
for (const paneId of shutdownPaneIds) {
|
|
4008
|
+
const teardown = await terminateExactPaneProcessTree(paneId, config.workers.find((worker) => worker.pane_id === paneId)?.pid ?? expectedSharedWorkerPanePids.get(paneId), undefined, undefined, authorizeFrozenSharedWorkerProcessSignal);
|
|
4009
|
+
if (teardown.proofUnavailable) {
|
|
4010
|
+
await persistGonePaneDescendantCleanupDebt({ teamName: sanitized, cwd, config, paneId, teardown });
|
|
4011
|
+
paneProcessProofUnavailable.push(teardown.proofUnavailable);
|
|
4012
|
+
break;
|
|
4013
|
+
}
|
|
4014
|
+
if (!teardown.terminated && teardown.stopped) {
|
|
4015
|
+
await persistGonePaneDescendantCleanupDebt({ teamName: sanitized, cwd, config, paneId, teardown });
|
|
4016
|
+
paneProcessProofUnavailable.push({
|
|
4017
|
+
status: 'unavailable',
|
|
4018
|
+
paneId,
|
|
4019
|
+
reason: 'pane_proof_lost_during_process_teardown',
|
|
4020
|
+
detail: 'exact pane authority was lost before the tracked process tree was resolved',
|
|
4021
|
+
});
|
|
4022
|
+
break;
|
|
4023
|
+
}
|
|
4024
|
+
if (teardown.terminated && readExactPaneProofSync(paneId).status === 'gone') {
|
|
4025
|
+
prekillResolvedWorkerPaneIds.add(paneId);
|
|
4026
|
+
}
|
|
4027
|
+
}
|
|
4028
|
+
if (paneProcessProofUnavailable.length > 0) {
|
|
4029
|
+
assertPaneTeardownProofsAvailable('shutdown', paneProcessProofUnavailable);
|
|
4030
|
+
}
|
|
4031
|
+
}
|
|
4032
|
+
// Revalidate ownership of the frozen discovered members, then re-prove the
|
|
4033
|
+
// frozen complete worker set in one snapshot immediately before the first
|
|
4034
|
+
// HUD topology effect. This never admits a newly discovered pane.
|
|
4035
|
+
if (sharedSessionTopology) {
|
|
4036
|
+
const reauthorizedDiscoveredWorkerPaneIds = collectAuthorizedSharedSessionWorkerPaneIds(authorizedDiscoveredWorkerPaneIds, tmuxPaneOwnerId, canonicalExplicitWorkerPaneIds, initiallyTaggedWorkerPaneIds);
|
|
4037
|
+
if (reauthorizedDiscoveredWorkerPaneIds.length !== authorizedDiscoveredWorkerPaneIds.length
|
|
4038
|
+
|| reauthorizedDiscoveredWorkerPaneIds.some((paneId) => !authorizedDiscoveredWorkerPaneIds.includes(paneId))) {
|
|
4039
|
+
throw new Error('shutdown_shared_session_worker_owner_changed');
|
|
4040
|
+
}
|
|
4041
|
+
assertCompleteSharedWorkerPaneProofs(true);
|
|
3052
4042
|
}
|
|
3053
4043
|
let restoredHudPaneId = null;
|
|
3054
4044
|
if (effectiveHudPaneId) {
|
|
3055
|
-
|
|
4045
|
+
// Confirm restoration authority before removing the shared HUD, then
|
|
4046
|
+
// check it again immediately before the split below.
|
|
4047
|
+
if (sessionName.includes(':') && frozenRestoreLeaderAuthorization) {
|
|
4048
|
+
assertSharedPaneAuthorizationContinuous(frozenRestoreLeaderAuthorization, 'restore leader pane');
|
|
4049
|
+
}
|
|
4050
|
+
if (frozenHudAuthorization) {
|
|
4051
|
+
assertSharedPaneAuthorizationContinuous(frozenHudAuthorization, 'HUD pane');
|
|
4052
|
+
}
|
|
4053
|
+
const hudTeardown = await teardownWorkerPanes([effectiveHudPaneId], {
|
|
4054
|
+
leaderPaneId: effectiveLeaderPaneId,
|
|
4055
|
+
expectedPanePids: frozenHudAuthorization
|
|
4056
|
+
? { [frozenHudAuthorization.paneId]: frozenHudAuthorization.pid }
|
|
4057
|
+
: undefined,
|
|
4058
|
+
authorizePaneKill: frozenHudAuthorization
|
|
4059
|
+
? () => {
|
|
4060
|
+
assertSharedPaneAuthorizationContinuous(frozenHudAuthorization, 'HUD pane');
|
|
4061
|
+
return true;
|
|
4062
|
+
}
|
|
4063
|
+
: undefined,
|
|
4064
|
+
});
|
|
4065
|
+
assertPaneTeardownProofsAvailable('shutdown', hudTeardown.proofUnavailable);
|
|
4066
|
+
if (hudTeardown.kill.failed > 0) {
|
|
4067
|
+
throw new Error(`shutdown_pane_teardown_failed:${hudTeardown.kill.failedPaneIds.join(',')}`);
|
|
4068
|
+
}
|
|
3056
4069
|
if (sessionName.includes(':')) {
|
|
4070
|
+
if (frozenRestoreLeaderAuthorization) {
|
|
4071
|
+
assertSharedPaneAuthorizationContinuous(frozenRestoreLeaderAuthorization, 'restore leader pane');
|
|
4072
|
+
}
|
|
3057
4073
|
restoredHudPaneId = restoreStandaloneHudPane(trustedHudRestoreLeaderPaneId, cwd, {
|
|
3058
4074
|
sessionId: leaderSessionId,
|
|
4075
|
+
expectedLeaderPanePid: frozenRestoreLeaderAuthorization?.pid,
|
|
4076
|
+
expectedLeaderPaneOwnerId: tmuxPaneOwnerId,
|
|
4077
|
+
stateRoot: join(config.team_state_root ?? resolveCanonicalTeamStateRoot(cwd), 'team', sanitized),
|
|
4078
|
+
assertLeaderPaneAuthorization: frozenRestoreLeaderAuthorization
|
|
4079
|
+
? () => assertSharedPaneAuthorizationContinuous(frozenRestoreLeaderAuthorization, 'restore leader pane')
|
|
4080
|
+
: undefined,
|
|
3059
4081
|
});
|
|
3060
4082
|
if (!restoredHudPaneId) {
|
|
3061
4083
|
const reason = trustedHudRestoreLeaderPaneId
|
|
@@ -3064,29 +4086,65 @@ export async function shutdownTeam(teamName, cwd, options = {}) {
|
|
|
3064
4086
|
console.warn(`[team shutdown] ${sanitized}: ${reason}`);
|
|
3065
4087
|
}
|
|
3066
4088
|
}
|
|
4089
|
+
if (restoredHudPaneId) {
|
|
4090
|
+
const restoredHudProof = readExactPaneProofSync(restoredHudPaneId);
|
|
4091
|
+
if (restoredHudProof.status !== 'live') {
|
|
4092
|
+
throw new Error(`shutdown_restored_hud_pane_proof_unavailable:${restoredHudPaneId}`);
|
|
4093
|
+
}
|
|
4094
|
+
config = await mutateShutdownConfig(config, cwd, (current) => {
|
|
4095
|
+
current.hud_pane_id = restoredHudPaneId;
|
|
4096
|
+
current.hud_pane_pid = restoredHudProof.pid;
|
|
4097
|
+
});
|
|
4098
|
+
finalizeRestoredHudCleanupDebtSync(cwd, restoredHudPaneId, restoredHudProof.pid, join(config.team_state_root ?? resolveCanonicalTeamStateRoot(cwd), 'team', sanitized), true);
|
|
4099
|
+
}
|
|
3067
4100
|
}
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
4101
|
+
let resizeHookWarning = null;
|
|
4102
|
+
if (config.resize_hook_name && config.resize_hook_target) {
|
|
4103
|
+
const resizeHookName = config.resize_hook_name;
|
|
4104
|
+
const unregistered = unregisterResizeHook(config.resize_hook_target, resizeHookName);
|
|
4105
|
+
if (!unregistered && isTmuxAvailable()) {
|
|
4106
|
+
const baseSession = sessionName.split(':')[0];
|
|
4107
|
+
const teamSessions = listTeamSessions();
|
|
4108
|
+
if (teamSessions === null || teamSessions.includes(baseSession)) {
|
|
4109
|
+
resizeHookWarning = `failed to unregister resize hook ${resizeHookName}`;
|
|
4110
|
+
}
|
|
4111
|
+
}
|
|
4112
|
+
}
|
|
4113
|
+
if (resizeHookWarning) {
|
|
4114
|
+
console.warn(`[team shutdown] ${sanitized}: ${resizeHookWarning}; continuing teardown`);
|
|
4115
|
+
}
|
|
4116
|
+
const workerTeardown = await teardownWorkerPanes(shutdownPaneIds.filter((paneId) => !prekillResolvedWorkerPaneIds.has(paneId)), {
|
|
3079
4117
|
leaderPaneId: effectiveLeaderPaneId,
|
|
3080
4118
|
hudPaneId: restoredHudPaneId ?? effectiveHudPaneId,
|
|
4119
|
+
expectedPanePids: {
|
|
4120
|
+
...Object.fromEntries(expectedSharedWorkerPanePids),
|
|
4121
|
+
...Object.fromEntries(config.workers
|
|
4122
|
+
.filter((worker) => typeof worker.pane_id === 'string' && typeof worker.pid === 'number')
|
|
4123
|
+
.map((worker) => [worker.pane_id, worker.pid])),
|
|
4124
|
+
},
|
|
4125
|
+
authorizePaneKill: sharedSessionTopology
|
|
4126
|
+
? (paneId) => {
|
|
4127
|
+
const expectedOwner = frozenSharedWorkerOwnerIds.get(paneId);
|
|
4128
|
+
if (expectedOwner === undefined)
|
|
4129
|
+
return false;
|
|
4130
|
+
const currentOwner = readPaneTeamOwnerTagResult(paneId);
|
|
4131
|
+
return currentOwner.status === 'value'
|
|
4132
|
+
&& currentOwner.value === expectedOwner
|
|
4133
|
+
&& expectedOwner === tmuxPaneOwnerId;
|
|
4134
|
+
}
|
|
4135
|
+
: undefined,
|
|
3081
4136
|
});
|
|
3082
|
-
|
|
4137
|
+
assertPaneTeardownProofsAvailable('shutdown', workerTeardown.proofUnavailable);
|
|
4138
|
+
if (workerTeardown.kill.failed > 0) {
|
|
4139
|
+
throw new Error(`shutdown_pane_teardown_failed:${workerTeardown.kill.failedPaneIds.join(',')}`);
|
|
4140
|
+
}
|
|
4141
|
+
config = await mutateShutdownConfig(config, cwd, (current) => {
|
|
4142
|
+
current.resize_hook_name = null;
|
|
4143
|
+
current.resize_hook_target = null;
|
|
4144
|
+
});
|
|
4145
|
+
// 4. Destroy a detached session only after an authoritative leader proof.
|
|
3083
4146
|
if (!sessionName.includes(':')) {
|
|
3084
|
-
|
|
3085
|
-
destroyTeamSession(sessionName);
|
|
3086
|
-
}
|
|
3087
|
-
catch (err) {
|
|
3088
|
-
process.stderr.write(`[team/runtime] operation failed: ${err}\n`);
|
|
3089
|
-
}
|
|
4147
|
+
await destroyConfiguredDetachedTeamSession(sanitized, cwd, config);
|
|
3090
4148
|
}
|
|
3091
4149
|
}
|
|
3092
4150
|
else {
|
|
@@ -3229,7 +4287,7 @@ export async function resumeTeam(teamName, cwd) {
|
|
|
3229
4287
|
.filter((worker) => {
|
|
3230
4288
|
if (!Number.isFinite(worker.pid) || (worker.pid ?? 0) <= 0)
|
|
3231
4289
|
return false;
|
|
3232
|
-
return
|
|
4290
|
+
return probePidLiveness(worker.pid) !== 'gone';
|
|
3233
4291
|
})
|
|
3234
4292
|
.filter((worker) => !getPromptWorkerHandle(sanitized, worker.name));
|
|
3235
4293
|
if (missingHandles.length > 0) {
|
|
@@ -3261,7 +4319,10 @@ async function findActiveTeams(cwd, leaderSessionId) {
|
|
|
3261
4319
|
const root = teamRuntimeTeamsRoot(cwd);
|
|
3262
4320
|
if (!existsSync(root))
|
|
3263
4321
|
return [];
|
|
3264
|
-
const
|
|
4322
|
+
const listedSessions = listTeamSessions();
|
|
4323
|
+
if (listedSessions === null)
|
|
4324
|
+
throw new Error('tmux_session_query_unavailable');
|
|
4325
|
+
const sessions = new Set(listedSessions);
|
|
3265
4326
|
const entries = await readdir(root, { withFileTypes: true });
|
|
3266
4327
|
const active = [];
|
|
3267
4328
|
for (const e of entries) {
|
|
@@ -3297,7 +4358,10 @@ async function detectAndCleanStaleTeam(teamName, leaderCwd, workerCount, confirm
|
|
|
3297
4358
|
const stateDir = teamRuntimeTeamRoot(teamName, leaderCwd);
|
|
3298
4359
|
if (!existsSync(stateDir))
|
|
3299
4360
|
return;
|
|
3300
|
-
const
|
|
4361
|
+
const listedSessions = listTeamSessions();
|
|
4362
|
+
if (listedSessions === null)
|
|
4363
|
+
return;
|
|
4364
|
+
const sessions = new Set(listedSessions);
|
|
3301
4365
|
if (sessions.has(`omx-team-${teamName}`))
|
|
3302
4366
|
return;
|
|
3303
4367
|
const repoRootResult = spawnSync('git', ['rev-parse', '--show-toplevel'], {
|
|
@@ -3455,7 +4519,7 @@ async function notifyWorkerOutcome(config, workerIndex, message, workerPaneId) {
|
|
|
3455
4519
|
return { ok: false, transport: 'tmux_send_keys', reason: 'tmux_unavailable' };
|
|
3456
4520
|
}
|
|
3457
4521
|
try {
|
|
3458
|
-
await sendToWorker(config.tmux_session, workerIndex, message, workerPaneId, worker.worker_cli);
|
|
4522
|
+
await sendToWorker(config.tmux_session, workerIndex, message, workerPaneId, worker.worker_cli, worker.pid ?? undefined, config.tmux_pane_owner_id ?? undefined, config.hud_pane_id ?? undefined);
|
|
3459
4523
|
return { ok: true, transport: 'tmux_send_keys', reason: 'tmux_send_keys_sent' };
|
|
3460
4524
|
}
|
|
3461
4525
|
catch (error) {
|