oh-my-codex 0.20.2 → 0.20.5
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 +36 -18
- package/Cargo.toml +1 -1
- package/README.md +28 -2
- package/crates/omx-runtime/Cargo.toml +4 -0
- package/crates/omx-runtime/src/main.rs +415 -2
- package/crates/omx-runtime/tests/execution.rs +483 -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/adapt/__tests__/herdr-target.test.d.ts +2 -0
- package/dist/adapt/__tests__/herdr-target.test.d.ts.map +1 -0
- package/dist/adapt/__tests__/herdr-target.test.js +72 -0
- package/dist/adapt/__tests__/herdr-target.test.js.map +1 -0
- package/dist/adapt/contracts.d.ts +1 -1
- package/dist/adapt/contracts.d.ts.map +1 -1
- package/dist/adapt/contracts.js +1 -1
- package/dist/adapt/contracts.js.map +1 -1
- package/dist/adapt/herdr/__tests__/authority.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/authority.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/authority.test.js +49 -0
- package/dist/adapt/herdr/__tests__/authority.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/bridge.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/bridge.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/bridge.test.js +176 -0
- package/dist/adapt/herdr/__tests__/bridge.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/sanitize.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/sanitize.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/sanitize.test.js +45 -0
- package/dist/adapt/herdr/__tests__/sanitize.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/semantic.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/semantic.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/semantic.test.js +130 -0
- package/dist/adapt/herdr/__tests__/semantic.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/seq-store.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/seq-store.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/seq-store.test.js +54 -0
- package/dist/adapt/herdr/__tests__/seq-store.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/socket-live.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/socket-live.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/socket-live.test.js +104 -0
- package/dist/adapt/herdr/__tests__/socket-live.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/team-map.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/team-map.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/team-map.test.js +46 -0
- package/dist/adapt/herdr/__tests__/team-map.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/transport.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/transport.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/transport.test.js +160 -0
- package/dist/adapt/herdr/__tests__/transport.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/wiring.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/wiring.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/wiring.test.js +118 -0
- package/dist/adapt/herdr/__tests__/wiring.test.js.map +1 -0
- package/dist/adapt/herdr/authority.d.ts +34 -0
- package/dist/adapt/herdr/authority.d.ts.map +1 -0
- package/dist/adapt/herdr/authority.js +71 -0
- package/dist/adapt/herdr/authority.js.map +1 -0
- package/dist/adapt/herdr/bridge.d.ts +84 -0
- package/dist/adapt/herdr/bridge.d.ts.map +1 -0
- package/dist/adapt/herdr/bridge.js +222 -0
- package/dist/adapt/herdr/bridge.js.map +1 -0
- package/dist/adapt/herdr/sanitize.d.ts +20 -0
- package/dist/adapt/herdr/sanitize.d.ts.map +1 -0
- package/dist/adapt/herdr/sanitize.js +70 -0
- package/dist/adapt/herdr/sanitize.js.map +1 -0
- package/dist/adapt/herdr/semantic.d.ts +67 -0
- package/dist/adapt/herdr/semantic.d.ts.map +1 -0
- package/dist/adapt/herdr/semantic.js +170 -0
- package/dist/adapt/herdr/semantic.js.map +1 -0
- package/dist/adapt/herdr/seq-store.d.ts +32 -0
- package/dist/adapt/herdr/seq-store.d.ts.map +1 -0
- package/dist/adapt/herdr/seq-store.js +126 -0
- package/dist/adapt/herdr/seq-store.js.map +1 -0
- package/dist/adapt/herdr/team-map.d.ts +30 -0
- package/dist/adapt/herdr/team-map.d.ts.map +1 -0
- package/dist/adapt/herdr/team-map.js +37 -0
- package/dist/adapt/herdr/team-map.js.map +1 -0
- package/dist/adapt/herdr/transport.d.ts +115 -0
- package/dist/adapt/herdr/transport.d.ts.map +1 -0
- package/dist/adapt/herdr/transport.js +260 -0
- package/dist/adapt/herdr/transport.js.map +1 -0
- package/dist/adapt/herdr/wiring.d.ts +24 -0
- package/dist/adapt/herdr/wiring.d.ts.map +1 -0
- package/dist/adapt/herdr/wiring.js +62 -0
- package/dist/adapt/herdr/wiring.js.map +1 -0
- package/dist/adapt/herdr.d.ts +19 -0
- package/dist/adapt/herdr.d.ts.map +1 -0
- package/dist/adapt/herdr.js +115 -0
- package/dist/adapt/herdr.js.map +1 -0
- package/dist/adapt/index.d.ts +4 -0
- package/dist/adapt/index.d.ts.map +1 -1
- package/dist/adapt/index.js +48 -0
- package/dist/adapt/index.js.map +1 -1
- package/dist/adapt/registry.d.ts.map +1 -1
- package/dist/adapt/registry.js +11 -0
- package/dist/adapt/registry.js.map +1 -1
- 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/__tests__/hotswap.test.js +60 -1
- package/dist/auth/__tests__/hotswap.test.js.map +1 -1
- package/dist/auth/__tests__/storage.test.js +33 -1
- package/dist/auth/__tests__/storage.test.js.map +1 -1
- package/dist/auth/hotswap.d.ts +22 -2
- package/dist/auth/hotswap.d.ts.map +1 -1
- package/dist/auth/hotswap.js +107 -22
- package/dist/auth/hotswap.js.map +1 -1
- package/dist/auth/storage.js +1 -1
- package/dist/auth/storage.js.map +1 -1
- package/dist/autopilot/__tests__/ralplan-gate.test.js +86 -1333
- package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
- package/dist/autopilot/ralplan-gate.d.ts.map +1 -1
- package/dist/autopilot/ralplan-gate.js +7 -6
- package/dist/autopilot/ralplan-gate.js.map +1 -1
- package/dist/cli/__tests__/agents-init.test.js +4 -23
- package/dist/cli/__tests__/agents-init.test.js.map +1 -1
- package/dist/cli/__tests__/api.test.js +61 -24
- package/dist/cli/__tests__/api.test.js.map +1 -1
- package/dist/cli/__tests__/auth.test.js +63 -7
- package/dist/cli/__tests__/auth.test.js.map +1 -1
- package/dist/cli/__tests__/codex-feature-probe.test.js +66 -1
- package/dist/cli/__tests__/codex-feature-probe.test.js.map +1 -1
- package/dist/cli/__tests__/codex-plugin-layout.test.js +150 -2
- package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
- package/dist/cli/__tests__/detached-leader-teardown.test.d.ts +2 -0
- package/dist/cli/__tests__/detached-leader-teardown.test.d.ts.map +1 -0
- package/dist/cli/__tests__/detached-leader-teardown.test.js +473 -0
- package/dist/cli/__tests__/detached-leader-teardown.test.js.map +1 -0
- package/dist/cli/__tests__/doctor-process-identity-readiness.test.d.ts +2 -0
- package/dist/cli/__tests__/doctor-process-identity-readiness.test.d.ts.map +1 -0
- package/dist/cli/__tests__/doctor-process-identity-readiness.test.js +103 -0
- package/dist/cli/__tests__/doctor-process-identity-readiness.test.js.map +1 -0
- package/dist/cli/__tests__/doctor-state-root-binding.test.d.ts +2 -0
- package/dist/cli/__tests__/doctor-state-root-binding.test.d.ts.map +1 -0
- package/dist/cli/__tests__/doctor-state-root-binding.test.js +139 -0
- package/dist/cli/__tests__/doctor-state-root-binding.test.js.map +1 -0
- package/dist/cli/__tests__/doctor-warning-copy.test.js +135 -1
- package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
- package/dist/cli/__tests__/exec.test.js +211 -0
- package/dist/cli/__tests__/exec.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +878 -463
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/launch-fallback.test.js +1290 -127
- package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
- package/dist/cli/__tests__/mcp-parity.test.js +8 -4
- package/dist/cli/__tests__/mcp-parity.test.js.map +1 -1
- package/dist/cli/__tests__/native-assets.test.js +398 -16
- package/dist/cli/__tests__/native-assets.test.js.map +1 -1
- package/dist/cli/__tests__/native-hook-claim-journal.test.js +132 -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 +49 -164
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js.map +1 -1
- package/dist/cli/__tests__/ralplan-child-binding-3212.test.d.ts +2 -0
- package/dist/cli/__tests__/ralplan-child-binding-3212.test.d.ts.map +1 -0
- package/dist/cli/__tests__/ralplan-child-binding-3212.test.js +56 -0
- package/dist/cli/__tests__/ralplan-child-binding-3212.test.js.map +1 -0
- package/dist/cli/__tests__/ralplan.test.js +107 -109
- 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-scoped-runtime.test.js +1118 -43
- package/dist/cli/__tests__/session-scoped-runtime.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-agents-overwrite.test.js +8 -55
- package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
- package/dist/cli/__tests__/setup-install-mode.test.js +224 -7
- package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
- package/dist/cli/__tests__/sparkshell-cli.test.js +93 -41
- package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
- package/dist/cli/__tests__/team.test.js +150 -8
- package/dist/cli/__tests__/team.test.js.map +1 -1
- package/dist/cli/__tests__/ultragoal.test.js +117 -2
- package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
- package/dist/cli/__tests__/uninstall.test.js +95 -4
- package/dist/cli/__tests__/uninstall.test.js.map +1 -1
- package/dist/cli/__tests__/update.test.js +669 -177
- package/dist/cli/__tests__/update.test.js.map +1 -1
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js +33 -2
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
- package/dist/cli/api.d.ts.map +1 -1
- package/dist/cli/api.js +9 -10
- package/dist/cli/api.js.map +1 -1
- package/dist/cli/codex-feature-probe.d.ts +16 -0
- package/dist/cli/codex-feature-probe.d.ts.map +1 -1
- package/dist/cli/codex-feature-probe.js +44 -0
- package/dist/cli/codex-feature-probe.js.map +1 -1
- package/dist/cli/codex-home.d.ts.map +1 -1
- package/dist/cli/codex-home.js +7 -4
- package/dist/cli/codex-home.js.map +1 -1
- package/dist/cli/doctor.d.ts +20 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +290 -3
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.d.ts +234 -12
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2409 -746
- 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-assets.d.ts +25 -0
- package/dist/cli/native-assets.d.ts.map +1 -1
- package/dist/cli/native-assets.js +448 -51
- package/dist/cli/native-assets.js.map +1 -1
- package/dist/cli/native-hook-claim-journal.d.ts +20 -5
- package/dist/cli/native-hook-claim-journal.d.ts.map +1 -1
- package/dist/cli/native-hook-claim-journal.js +67 -32
- package/dist/cli/native-hook-claim-journal.js.map +1 -1
- package/dist/cli/package-manager-ownership.d.ts +63 -0
- package/dist/cli/package-manager-ownership.d.ts.map +1 -0
- package/dist/cli/package-manager-ownership.js +323 -0
- package/dist/cli/package-manager-ownership.js.map +1 -0
- package/dist/cli/ralph.js +2 -2
- package/dist/cli/ralph.js.map +1 -1
- package/dist/cli/ralplan.d.ts +5 -7
- package/dist/cli/ralplan.d.ts.map +1 -1
- package/dist/cli/ralplan.js +83 -134
- 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-preferences.d.ts +15 -0
- package/dist/cli/setup-preferences.d.ts.map +1 -1
- package/dist/cli/setup-preferences.js +27 -1
- package/dist/cli/setup-preferences.js.map +1 -1
- package/dist/cli/setup.d.ts +8 -0
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +119 -47
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/sparkshell.d.ts.map +1 -1
- package/dist/cli/sparkshell.js +52 -9
- package/dist/cli/sparkshell.js.map +1 -1
- package/dist/cli/ultragoal.d.ts.map +1 -1
- package/dist/cli/ultragoal.js +2 -2
- package/dist/cli/ultragoal.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 +71 -30
- package/dist/cli/uninstall.js.map +1 -1
- package/dist/cli/update-worker.d.ts +2 -0
- package/dist/cli/update-worker.d.ts.map +1 -0
- package/dist/cli/update-worker.js +142 -0
- package/dist/cli/update-worker.js.map +1 -0
- package/dist/cli/update.d.ts +14 -22
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +192 -241
- package/dist/cli/update.js.map +1 -1
- package/dist/compat/__tests__/rust-runtime-compat.test.js +1 -1
- package/dist/config/__tests__/generator-idempotent.test.js +503 -30
- package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
- package/dist/config/__tests__/generator-notify.test.js +5 -3
- 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 +27 -21
- package/dist/config/generator.d.ts.map +1 -1
- package/dist/config/generator.js +610 -152
- 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/goal-workflows/__tests__/codex-goal-snapshot.test.js +25 -0
- package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js.map +1 -1
- package/dist/goal-workflows/codex-goal-snapshot.d.ts +1 -1
- package/dist/goal-workflows/codex-goal-snapshot.d.ts.map +1 -1
- package/dist/goal-workflows/codex-goal-snapshot.js +2 -0
- package/dist/goal-workflows/codex-goal-snapshot.js.map +1 -1
- package/dist/hooks/__tests__/agents-overlay.test.js +5 -3
- package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js +27 -4
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.d.ts +2 -0
- package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.js +172 -0
- package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.js.map +1 -0
- package/dist/hooks/__tests__/keyword-detector.test.js +176 -37
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js +402 -545
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +39 -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 +161 -26
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js +27 -0
- package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js +10 -118
- package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-regression-205.test.js +7 -0
- package/dist/hooks/__tests__/notify-hook-regression-205.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +533 -14
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +159 -24
- 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-team-worker-fail-closed.test.js +2 -1
- package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.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 +44 -4
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/prompt-guidance-contract.test.js +9 -0
- package/dist/hooks/__tests__/prompt-guidance-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/session.test.js +3130 -365
- package/dist/hooks/__tests__/session.test.js.map +1 -1
- package/dist/hooks/agents-overlay.js +1 -1
- package/dist/hooks/agents-overlay.js.map +1 -1
- package/dist/hooks/extensibility/__tests__/dispatcher.test.js +108 -2
- package/dist/hooks/extensibility/__tests__/dispatcher.test.js.map +1 -1
- package/dist/hooks/extensibility/__tests__/sdk.test.js +151 -1
- package/dist/hooks/extensibility/__tests__/sdk.test.js.map +1 -1
- package/dist/hooks/extensibility/dispatcher.d.ts.map +1 -1
- package/dist/hooks/extensibility/dispatcher.js +20 -3
- package/dist/hooks/extensibility/dispatcher.js.map +1 -1
- package/dist/hooks/extensibility/plugin-runner.js +1 -0
- package/dist/hooks/extensibility/plugin-runner.js.map +1 -1
- package/dist/hooks/extensibility/runtime.d.ts.map +1 -1
- package/dist/hooks/extensibility/runtime.js.map +1 -1
- package/dist/hooks/extensibility/sdk/paths.d.ts +1 -1
- package/dist/hooks/extensibility/sdk/paths.d.ts.map +1 -1
- package/dist/hooks/extensibility/sdk/paths.js +2 -2
- package/dist/hooks/extensibility/sdk/paths.js.map +1 -1
- package/dist/hooks/extensibility/sdk/runtime-state.d.ts +1 -1
- package/dist/hooks/extensibility/sdk/runtime-state.d.ts.map +1 -1
- package/dist/hooks/extensibility/sdk/runtime-state.js +55 -15
- package/dist/hooks/extensibility/sdk/runtime-state.js.map +1 -1
- package/dist/hooks/extensibility/sdk.d.ts +1 -0
- package/dist/hooks/extensibility/sdk.d.ts.map +1 -1
- package/dist/hooks/extensibility/sdk.js +1 -1
- package/dist/hooks/extensibility/sdk.js.map +1 -1
- package/dist/hooks/extensibility/types.d.ts +7 -0
- package/dist/hooks/extensibility/types.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.d.ts +10 -3
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +116 -20
- package/dist/hooks/keyword-detector.js.map +1 -1
- package/dist/hooks/session.d.ts +204 -14
- package/dist/hooks/session.d.ts.map +1 -1
- package/dist/hooks/session.js +1500 -223
- package/dist/hooks/session.js.map +1 -1
- package/dist/hud/__tests__/hud-tmux-injection.test.js +6 -6
- package/dist/hud/__tests__/hud-tmux-injection.test.js.map +1 -1
- package/dist/hud/__tests__/index.test.js +26 -16
- package/dist/hud/__tests__/index.test.js.map +1 -1
- package/dist/hud/__tests__/reconcile.test.js +83 -78
- package/dist/hud/__tests__/reconcile.test.js.map +1 -1
- package/dist/hud/__tests__/state.test.js +36 -31
- package/dist/hud/__tests__/state.test.js.map +1 -1
- package/dist/hud/__tests__/tmux.test.js +155 -198
- package/dist/hud/__tests__/tmux.test.js.map +1 -1
- package/dist/hud/tmux.d.ts +42 -4
- package/dist/hud/tmux.d.ts.map +1 -1
- package/dist/hud/tmux.js +1017 -139
- package/dist/hud/tmux.js.map +1 -1
- package/dist/leader/__tests__/contract.test.js +124 -37
- package/dist/leader/__tests__/contract.test.js.map +1 -1
- package/dist/leader/contract.d.ts +17 -6
- package/dist/leader/contract.d.ts.map +1 -1
- package/dist/leader/contract.js +102 -26
- 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__/hermes-bridge.test.js +17 -0
- package/dist/mcp/__tests__/hermes-bridge.test.js.map +1 -1
- package/dist/mcp/__tests__/server-lifecycle.test.js +4 -4
- package/dist/mcp/__tests__/state-paths.test.js +735 -4
- package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
- package/dist/mcp/__tests__/state-server.test.js +1 -1
- package/dist/mcp/__tests__/state-server.test.js.map +1 -1
- package/dist/mcp/__tests__/trace-server.test.js +2 -2
- package/dist/mcp/__tests__/trace-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/hermes-bridge.d.ts.map +1 -1
- package/dist/mcp/hermes-bridge.js +2 -2
- package/dist/mcp/hermes-bridge.js.map +1 -1
- package/dist/mcp/state-paths.d.ts +78 -4
- package/dist/mcp/state-paths.d.ts.map +1 -1
- package/dist/mcp/state-paths.js +543 -40
- package/dist/mcp/state-paths.js.map +1 -1
- package/dist/mcp/state-server.d.ts +4 -4
- package/dist/mcp/state-server.d.ts.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-autopilot-gates.test.js +36 -7
- package/dist/modes/__tests__/base-autopilot-gates.test.js.map +1 -1
- package/dist/modes/__tests__/base-multi-state-compat.test.js +1 -1
- package/dist/modes/__tests__/base-multi-state-compat.test.js.map +1 -1
- package/dist/modes/__tests__/base-session-scope.test.js +17 -8
- package/dist/modes/__tests__/base-session-scope.test.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/modes/base.d.ts +5 -5
- package/dist/modes/base.d.ts.map +1 -1
- package/dist/modes/base.js +76 -21
- package/dist/modes/base.js.map +1 -1
- package/dist/native-assets/archive.d.ts +17 -0
- package/dist/native-assets/archive.d.ts.map +1 -0
- package/dist/native-assets/archive.js +238 -0
- package/dist/native-assets/archive.js.map +1 -0
- package/dist/native-assets/policy.d.ts +42 -0
- package/dist/native-assets/policy.d.ts.map +1 -0
- package/dist/native-assets/policy.js +155 -0
- package/dist/native-assets/policy.js.map +1 -0
- 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 +164 -42
- package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
- package/dist/pipeline/__tests__/stages.test.js +72 -155
- package/dist/pipeline/__tests__/stages.test.js.map +1 -1
- package/dist/pipeline/orchestrator.d.ts.map +1 -1
- package/dist/pipeline/orchestrator.js +124 -18
- package/dist/pipeline/orchestrator.js.map +1 -1
- package/dist/pipeline/stages/ralplan.d.ts.map +1 -1
- package/dist/pipeline/stages/ralplan.js +36 -8
- package/dist/pipeline/stages/ralplan.js.map +1 -1
- package/dist/ralplan/__tests__/consensus-gate.test.js +73 -1316
- 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 +335 -0
- package/dist/ralplan/__tests__/documented-leader-preflight.test.js.map +1 -0
- package/dist/ralplan/__tests__/runtime.test.js +85 -101
- package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
- package/dist/ralplan/consensus-gate.d.ts +9 -1
- package/dist/ralplan/consensus-gate.d.ts.map +1 -1
- package/dist/ralplan/consensus-gate.js +82 -338
- package/dist/ralplan/consensus-gate.js.map +1 -1
- package/dist/ralplan/documented-leader-preflight.d.ts +41 -0
- package/dist/ralplan/documented-leader-preflight.d.ts.map +1 -0
- package/dist/ralplan/documented-leader-preflight.js +360 -0
- package/dist/ralplan/documented-leader-preflight.js.map +1 -0
- package/dist/ralplan/runtime.d.ts +2 -1
- package/dist/ralplan/runtime.d.ts.map +1 -1
- package/dist/ralplan/runtime.js +67 -168
- 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/runtime/run-state.d.ts +5 -1
- package/dist/runtime/run-state.d.ts.map +1 -1
- package/dist/runtime/run-state.js +15 -8
- package/dist/runtime/run-state.js.map +1 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js +16323 -8978
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/__tests__/issue-3293-callsite-parity.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3293-callsite-parity.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3293-callsite-parity.test.js +225 -0
- package/dist/scripts/__tests__/issue-3293-callsite-parity.test.js.map +1 -0
- package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.js +188 -0
- package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.js.map +1 -0
- package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.js +259 -0
- package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.js.map +1 -0
- package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.js +491 -0
- package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.js.map +1 -0
- package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.js +708 -0
- package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.js.map +1 -0
- package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.js +208 -0
- package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.js.map +1 -0
- package/dist/scripts/__tests__/notify-state-io.test.js +26 -0
- package/dist/scripts/__tests__/notify-state-io.test.js.map +1 -1
- package/dist/scripts/__tests__/notify-tmux-injection.test.js +23 -1
- package/dist/scripts/__tests__/notify-tmux-injection.test.js.map +1 -1
- package/dist/scripts/__tests__/postinstall.test.js +19 -1
- package/dist/scripts/__tests__/postinstall.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 +63 -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 +74 -159
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js.map +1 -1
- package/dist/scripts/__tests__/smoke-packed-install.test.js +404 -13
- package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
- package/dist/scripts/__tests__/verify-native-release-assets.test.d.ts +2 -0
- package/dist/scripts/__tests__/verify-native-release-assets.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/verify-native-release-assets.test.js +235 -0
- package/dist/scripts/__tests__/verify-native-release-assets.test.js.map +1 -0
- package/dist/scripts/codex-native-hook.d.ts +53 -1
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +14448 -1480
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/scripts/codex-native-pre-post.d.ts +9 -0
- package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
- package/dist/scripts/codex-native-pre-post.js +35 -12
- package/dist/scripts/codex-native-pre-post.js.map +1 -1
- package/dist/scripts/generate-native-release-manifest.js +80 -51
- package/dist/scripts/generate-native-release-manifest.js.map +1 -1
- package/dist/scripts/notify-fallback-watcher.js +78 -115
- 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/state-io.d.ts.map +1 -1
- package/dist/scripts/notify-hook/state-io.js +5 -0
- package/dist/scripts/notify-hook/state-io.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 +71 -16
- 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 +42 -21
- 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 +82 -33
- package/dist/scripts/notify-hook/team-worker.js.map +1 -1
- package/dist/scripts/notify-hook/tmux-injection.d.ts +1 -0
- package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
- package/dist/scripts/notify-hook/tmux-injection.js +32 -3
- package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
- package/dist/scripts/notify-hook.js +79 -82
- package/dist/scripts/notify-hook.js.map +1 -1
- package/dist/scripts/postinstall-advisory.d.ts +14 -0
- package/dist/scripts/postinstall-advisory.d.ts.map +1 -0
- package/dist/scripts/postinstall-advisory.js +49 -0
- package/dist/scripts/postinstall-advisory.js.map +1 -0
- package/dist/scripts/postinstall.d.ts +1 -1
- package/dist/scripts/postinstall.d.ts.map +1 -1
- package/dist/scripts/postinstall.js +1 -1
- package/dist/scripts/postinstall.js.map +1 -1
- package/dist/scripts/smoke-packed-install.d.ts +80 -4
- package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
- package/dist/scripts/smoke-packed-install.js +3162 -26
- package/dist/scripts/smoke-packed-install.js.map +1 -1
- package/dist/scripts/sync-plugin-mirror.d.ts.map +1 -1
- package/dist/scripts/sync-plugin-mirror.js +16 -1
- package/dist/scripts/sync-plugin-mirror.js.map +1 -1
- package/dist/scripts/verify-native-release-assets.js +37 -23
- package/dist/scripts/verify-native-release-assets.js.map +1 -1
- package/dist/state/__tests__/mode-state-context.test.js +61 -0
- package/dist/state/__tests__/mode-state-context.test.js.map +1 -1
- package/dist/state/__tests__/operations.test.js +462 -850
- package/dist/state/__tests__/operations.test.js.map +1 -1
- package/dist/state/__tests__/skill-active.test.js +400 -2
- package/dist/state/__tests__/skill-active.test.js.map +1 -1
- package/dist/state/mode-state-context.d.ts +8 -0
- package/dist/state/mode-state-context.d.ts.map +1 -1
- package/dist/state/mode-state-context.js +73 -0
- package/dist/state/mode-state-context.js.map +1 -1
- package/dist/state/operations.d.ts +3 -0
- package/dist/state/operations.d.ts.map +1 -1
- package/dist/state/operations.js +144 -40
- package/dist/state/operations.js.map +1 -1
- package/dist/state/skill-active.d.ts +18 -1
- package/dist/state/skill-active.d.ts.map +1 -1
- package/dist/state/skill-active.js +420 -23
- package/dist/state/skill-active.js.map +1 -1
- package/dist/state/workflow-transition-reconcile.d.ts +3 -0
- package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
- package/dist/state/workflow-transition-reconcile.js +10 -4
- package/dist/state/workflow-transition-reconcile.js.map +1 -1
- package/dist/state/workflow-transition.d.ts.map +1 -1
- package/dist/state/workflow-transition.js +3 -1
- package/dist/state/workflow-transition.js.map +1 -1
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.js +53 -247
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.js.map +1 -1
- package/dist/subagents/__tests__/tracker.test.js +837 -1048
- package/dist/subagents/__tests__/tracker.test.js.map +1 -1
- package/dist/subagents/tracker.d.ts +58 -135
- package/dist/subagents/tracker.d.ts.map +1 -1
- package/dist/subagents/tracker.js +811 -632
- 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 +58 -17
- 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 +197 -30
- 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__/notice-ledger.test.d.ts +2 -0
- package/dist/team/__tests__/notice-ledger.test.d.ts.map +1 -0
- package/dist/team/__tests__/notice-ledger.test.js +131 -0
- package/dist/team/__tests__/notice-ledger.test.js.map +1 -0
- package/dist/team/__tests__/pane-teardown-residue-e2e.test.d.ts +2 -0
- package/dist/team/__tests__/pane-teardown-residue-e2e.test.d.ts.map +1 -0
- package/dist/team/__tests__/pane-teardown-residue-e2e.test.js +410 -0
- package/dist/team/__tests__/pane-teardown-residue-e2e.test.js.map +1 -0
- package/dist/team/__tests__/phase-controller.test.js +20 -0
- package/dist/team/__tests__/phase-controller.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 +3963 -428
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/scaling.test.js +2405 -203
- package/dist/team/__tests__/scaling.test.js.map +1 -1
- package/dist/team/__tests__/state-root.test.js +117 -2
- package/dist/team/__tests__/state-root.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 +4099 -1039
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/__tests__/tmux-source-authority-realtmux.test.d.ts +2 -0
- package/dist/team/__tests__/tmux-source-authority-realtmux.test.d.ts.map +1 -0
- package/dist/team/__tests__/tmux-source-authority-realtmux.test.js +149 -0
- package/dist/team/__tests__/tmux-source-authority-realtmux.test.js.map +1 -0
- package/dist/team/__tests__/tmux-test-fixture.d.ts +69 -0
- package/dist/team/__tests__/tmux-test-fixture.d.ts.map +1 -1
- package/dist/team/__tests__/tmux-test-fixture.js +242 -15
- package/dist/team/__tests__/tmux-test-fixture.js.map +1 -1
- package/dist/team/__tests__/tmux-test-fixture.test.js +177 -1
- package/dist/team/__tests__/tmux-test-fixture.test.js.map +1 -1
- package/dist/team/__tests__/worker-runtime-identity.test.js +53 -44
- 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/notice-ledger.d.ts +54 -0
- package/dist/team/notice-ledger.d.ts.map +1 -0
- package/dist/team/notice-ledger.js +259 -0
- package/dist/team/notice-ledger.js.map +1 -0
- package/dist/team/phase-controller.d.ts.map +1 -1
- package/dist/team/phase-controller.js +2 -0
- package/dist/team/phase-controller.js.map +1 -1
- package/dist/team/runtime.d.ts +4 -1
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +1527 -291
- 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 +1503 -387
- 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/monitor.d.ts +10 -0
- package/dist/team/state/monitor.d.ts.map +1 -1
- package/dist/team/state/monitor.js +13 -0
- package/dist/team/state/monitor.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/types.d.ts +15 -0
- package/dist/team/state/types.d.ts.map +1 -1
- package/dist/team/state/types.js.map +1 -1
- package/dist/team/state-root.d.ts +12 -0
- package/dist/team/state-root.d.ts.map +1 -1
- package/dist/team/state-root.js +177 -0
- package/dist/team/state-root.js.map +1 -1
- package/dist/team/state.d.ts +85 -1
- package/dist/team/state.d.ts.map +1 -1
- package/dist/team/state.js +672 -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 +163 -27
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +1682 -376
- 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/ultragoal/__tests__/artifacts.test.js +950 -2
- package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
- package/dist/ultragoal/__tests__/docs-contract.test.js +1 -0
- package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
- package/dist/ultragoal/artifacts.d.ts +35 -1
- package/dist/ultragoal/artifacts.d.ts.map +1 -1
- package/dist/ultragoal/artifacts.js +232 -46
- package/dist/ultragoal/artifacts.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 +114 -0
- package/dist/utils/__tests__/file-durability.test.js.map +1 -0
- package/dist/utils/__tests__/paths.test.js +21 -1
- package/dist/utils/__tests__/paths.test.js.map +1 -1
- package/dist/utils/file-durability.d.ts +25 -0
- package/dist/utils/file-durability.d.ts.map +1 -0
- package/dist/utils/file-durability.js +67 -0
- package/dist/utils/file-durability.js.map +1 -0
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +14 -6
- package/dist/utils/paths.js.map +1 -1
- package/dist/verification/__tests__/ci-rust-gates.test.js +49 -4
- package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
- 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/verification/__tests__/explore-harness-release-workflow.test.js +60 -0
- package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +1 -1
- package/dist/verification/__tests__/native-release-manifest.test.js +157 -4
- package/dist/verification/__tests__/native-release-manifest.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 +13 -5
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +105 -17
- package/plugins/oh-my-codex/skills/autopilot/SKILL.md +13 -5
- package/plugins/oh-my-codex/skills/cancel/SKILL.md +30 -85
- package/plugins/oh-my-codex/skills/ralph/SKILL.md +6 -4
- package/plugins/oh-my-codex/skills/ralplan/SKILL.md +16 -17
- package/plugins/oh-my-codex/skills/team/SKILL.md +13 -9
- package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +3 -2
- package/skills/autopilot/SKILL.md +13 -5
- package/skills/cancel/SKILL.md +30 -85
- package/skills/ralph/SKILL.md +6 -4
- package/skills/ralplan/SKILL.md +16 -17
- package/skills/team/SKILL.md +13 -9
- package/skills/ultragoal/SKILL.md +3 -2
- package/src/scripts/__tests__/codex-native-hook.test.ts +8831 -951
- package/src/scripts/__tests__/issue-3293-callsite-parity.test.ts +187 -0
- package/src/scripts/__tests__/issue-3293-cancel-transaction.test.ts +189 -0
- package/src/scripts/__tests__/issue-3293-hook-owned-cancel.test.ts +187 -0
- package/src/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.ts +591 -0
- package/src/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.ts +701 -0
- package/src/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.ts +211 -0
- package/src/scripts/__tests__/notify-state-io.test.ts +29 -0
- package/src/scripts/__tests__/notify-tmux-injection.test.ts +26 -1
- package/src/scripts/__tests__/postinstall.test.ts +23 -1
- package/src/scripts/__tests__/reasoning-artifact-contract.test.ts +129 -0
- package/src/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.ts +67 -185
- package/src/scripts/__tests__/role-intent-durable-recovery-3181.test.ts +79 -178
- package/src/scripts/__tests__/smoke-packed-install.test.ts +525 -16
- package/src/scripts/__tests__/verify-native-release-assets.test.ts +240 -0
- package/src/scripts/codex-native-hook.ts +14238 -1429
- package/src/scripts/codex-native-pre-post.ts +43 -12
- package/src/scripts/generate-native-release-manifest.ts +83 -79
- package/src/scripts/notify-fallback-watcher.ts +91 -122
- package/src/scripts/notify-hook/auto-nudge.ts +71 -3
- package/src/scripts/notify-hook/state-io.ts +11 -0
- package/src/scripts/notify-hook/team-dispatch.ts +242 -75
- package/src/scripts/notify-hook/team-leader-nudge.ts +72 -22
- package/src/scripts/notify-hook/team-tmux-guard.ts +366 -39
- package/src/scripts/notify-hook/team-worker-stop.ts +43 -20
- package/src/scripts/notify-hook/team-worker.ts +82 -31
- package/src/scripts/notify-hook/tmux-injection.ts +33 -3
- package/src/scripts/notify-hook.ts +84 -82
- package/src/scripts/postinstall-advisory.ts +67 -0
- package/src/scripts/postinstall.ts +1 -1
- package/src/scripts/smoke-packed-install.ts +3608 -39
- package/src/scripts/sync-plugin-mirror.ts +17 -1
- package/src/scripts/verify-native-release-assets.ts +39 -31
- package/templates/AGENTS.md +1 -0
- 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, waitForWorkerReadyDetailedAsync, 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, teamContinuationRequiredDiagnostic, 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';
|
|
@@ -30,6 +34,7 @@ export { resolveTeamWorkerCliForResolvedLaunchArgs };
|
|
|
30
34
|
import { buildApprovedTeamHandoffSection, buildApprovedTeamExecutionBinding, normalizeApprovedTeamExecutionBinding, readApprovedTeamExecutionHintOutcomeFromBinding, resolvePersistedApprovedTeamExecutionContinuityState, writePersistedApprovedTeamExecutionBinding, } from './approved-execution.js';
|
|
31
35
|
import { readPersistedTeamUltragoalContext, renderLeaderOwnedUltragoalContextSection, resolveLeaderOwnedUltragoalContextOutcome, writePersistedTeamUltragoalContext, } from './ultragoal-context.js';
|
|
32
36
|
import { appendTeamCommitHygieneEntries, buildTeamCommitHygieneContext, resolveTeamCommitHygieneArtifactCwd, writeTeamCommitHygieneContext, } from './commit-hygiene.js';
|
|
37
|
+
import { classifyRecordedIdentity, defaultProcessInspectionProvider, } from '../hooks/session.js';
|
|
33
38
|
import { assertCleanLeaderWorkspaceForWorkerWorktrees, ensureWorktree, isGitRepository, isWorktreeDirty, planWorktreeTarget, removeWorktreeForce, rollbackProvisionedWorktrees, } from './worktree.js';
|
|
34
39
|
import { cleanupOmxMcpProcesses, findLaunchSafeCleanupCandidates, } from '../cli/cleanup.js';
|
|
35
40
|
function joinContextSections(...sections) {
|
|
@@ -109,6 +114,105 @@ async function syncTeamModeStateOnShutdown(teamName, cwd, leaderSessionId) {
|
|
|
109
114
|
await syncExactTeamModeStateOnShutdown(teamName, cwd, normalizedLeaderSessionId);
|
|
110
115
|
}
|
|
111
116
|
}
|
|
117
|
+
async function mutateShutdownConfig(config, cwd, mutate) {
|
|
118
|
+
return await withTeamTaskMembershipBarrier(config.name, cwd, async () => {
|
|
119
|
+
const current = await readTeamConfig(config.name, cwd);
|
|
120
|
+
if (!current)
|
|
121
|
+
throw new Error(`shutdown_config_missing:${config.name}`);
|
|
122
|
+
const baseGeneration = current.config_generation ?? 0;
|
|
123
|
+
const teamRoot = join(current.team_state_root ?? resolveCanonicalTeamStateRoot(cwd), 'team', current.name);
|
|
124
|
+
const [currentConfigBytes, currentManifestBytes] = await Promise.all([
|
|
125
|
+
readFile(join(teamRoot, 'config.json'), 'utf8'),
|
|
126
|
+
existsSync(join(teamRoot, 'manifest.v2.json')) ? readFile(join(teamRoot, 'manifest.v2.json'), 'utf8') : null,
|
|
127
|
+
]);
|
|
128
|
+
mutate(current);
|
|
129
|
+
const currentManifest = currentManifestBytes === null ? null : JSON.parse(currentManifestBytes);
|
|
130
|
+
await commitTeamMembershipTaskTransaction(current.name, cwd, {
|
|
131
|
+
baseGeneration,
|
|
132
|
+
tasks: [],
|
|
133
|
+
config: { oldBytes: currentConfigBytes, newBytes: JSON.stringify(current, null, 2) },
|
|
134
|
+
manifest: {
|
|
135
|
+
oldBytes: currentManifestBytes,
|
|
136
|
+
newBytes: currentManifest === null ? null : JSON.stringify({
|
|
137
|
+
...currentManifest,
|
|
138
|
+
hud_pane_id: current.hud_pane_id,
|
|
139
|
+
hud_pane_pid: current.hud_pane_pid,
|
|
140
|
+
resize_hook_name: current.resize_hook_name,
|
|
141
|
+
resize_hook_target: current.resize_hook_target,
|
|
142
|
+
startup_cleanup_panes: current.startup_cleanup_panes,
|
|
143
|
+
}, null, 2),
|
|
144
|
+
},
|
|
145
|
+
});
|
|
146
|
+
const committed = await readTeamConfig(current.name, cwd);
|
|
147
|
+
if (!committed)
|
|
148
|
+
throw new Error(`shutdown_config_missing_after_commit:${current.name}`);
|
|
149
|
+
return committed;
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
export async function reconcileStartupCleanupPanes(config, cwd) {
|
|
153
|
+
const cleanupPanes = config.startup_cleanup_panes ?? [];
|
|
154
|
+
if (cleanupPanes.length === 0)
|
|
155
|
+
return config;
|
|
156
|
+
const teamPaneOwnerId = config.tmux_pane_owner_id?.trim() ?? '';
|
|
157
|
+
if (!teamPaneOwnerId)
|
|
158
|
+
throw new Error('startup_cleanup_pane_owner_unavailable:missing_team_owner_id');
|
|
159
|
+
const resolvedPaneIds = new Set();
|
|
160
|
+
const expectedPanePids = {};
|
|
161
|
+
for (const cleanupPane of cleanupPanes) {
|
|
162
|
+
if (cleanupPane.pane_id === config.leader_pane_id || cleanupPane.pane_id === config.hud_pane_id) {
|
|
163
|
+
throw new Error(`startup_cleanup_pane_target_invalid:${cleanupPane.pane_id}`);
|
|
164
|
+
}
|
|
165
|
+
const proof = readExactPaneProofSync(cleanupPane.pane_id);
|
|
166
|
+
if (proof.status === 'gone') {
|
|
167
|
+
resolvedPaneIds.add(cleanupPane.pane_id);
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
if (proof.status === 'unavailable') {
|
|
171
|
+
assertPaneTeardownProofsAvailable('startup_cleanup', [proof]);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (cleanupPane.pid === null) {
|
|
175
|
+
throw new Error(`startup_cleanup_pane_pid_missing:${cleanupPane.pane_id}`);
|
|
176
|
+
}
|
|
177
|
+
if (proof.pid !== cleanupPane.pid) {
|
|
178
|
+
throw new Error(`startup_cleanup_pane_identity_changed:${cleanupPane.pane_id}`);
|
|
179
|
+
}
|
|
180
|
+
const owner = readPaneTeamOwnerTagResult(cleanupPane.pane_id);
|
|
181
|
+
if (owner.status === 'error') {
|
|
182
|
+
throw new Error(`startup_cleanup_pane_owner_unavailable:${cleanupPane.pane_id}:${owner.error}`);
|
|
183
|
+
}
|
|
184
|
+
if (owner.status !== 'value' || owner.value !== teamPaneOwnerId) {
|
|
185
|
+
throw new Error(`startup_cleanup_pane_owner_changed:${cleanupPane.pane_id}`);
|
|
186
|
+
}
|
|
187
|
+
expectedPanePids[cleanupPane.pane_id] = cleanupPane.pid;
|
|
188
|
+
}
|
|
189
|
+
const livePaneIds = Object.keys(expectedPanePids);
|
|
190
|
+
if (livePaneIds.length > 0) {
|
|
191
|
+
const teardown = await teardownWorkerPanes(livePaneIds, {
|
|
192
|
+
leaderPaneId: config.leader_pane_id,
|
|
193
|
+
hudPaneId: config.hud_pane_id,
|
|
194
|
+
expectedPanePids,
|
|
195
|
+
authorizePaneKill: (paneId, proof) => {
|
|
196
|
+
if (expectedPanePids[paneId] !== proof.pid)
|
|
197
|
+
return false;
|
|
198
|
+
const owner = readPaneTeamOwnerTagResult(paneId);
|
|
199
|
+
return owner.status === 'value' && owner.value === teamPaneOwnerId;
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
assertPaneTeardownProofsAvailable('startup_cleanup', teardown.proofUnavailable);
|
|
203
|
+
for (const paneId of teardown.killedPaneIds)
|
|
204
|
+
resolvedPaneIds.add(paneId);
|
|
205
|
+
}
|
|
206
|
+
const nextConfig = await mutateShutdownConfig(config, cwd, (current) => {
|
|
207
|
+
const unresolved = (current.startup_cleanup_panes ?? [])
|
|
208
|
+
.filter((cleanupPane) => !resolvedPaneIds.has(cleanupPane.pane_id));
|
|
209
|
+
current.startup_cleanup_panes = unresolved.length > 0 ? unresolved : undefined;
|
|
210
|
+
});
|
|
211
|
+
if ((nextConfig.startup_cleanup_panes?.length ?? 0) > 0) {
|
|
212
|
+
throw new Error(`startup_cleanup_pane_teardown_failed:${nextConfig.startup_cleanup_panes.map((pane) => pane.pane_id).join(',')}`);
|
|
213
|
+
}
|
|
214
|
+
return nextConfig;
|
|
215
|
+
}
|
|
112
216
|
async function assertTeamStartupIsNonDestructive(teamName, cwd, leaderSessionId) {
|
|
113
217
|
const activeTeams = await findActiveTeams(cwd, leaderSessionId);
|
|
114
218
|
if (activeTeams.length > 0) {
|
|
@@ -129,18 +233,53 @@ async function assertTeamStartupIsNonDestructive(teamName, cwd, leaderSessionId)
|
|
|
129
233
|
throw new Error(`team_name_conflict: active team state already exists for "${teamName}" (phase: ${renderedPhase}, tmux: ${tmuxSession}). `
|
|
130
234
|
+ `Use "omx team status ${teamName}", "omx team resume ${teamName}", or "omx team shutdown ${teamName}" instead of launching a duplicate team.`);
|
|
131
235
|
}
|
|
236
|
+
let terminalEpochStartedHookForTest = null;
|
|
237
|
+
export function setTerminalEpochStartedHookForTest(hook) {
|
|
238
|
+
terminalEpochStartedHookForTest = hook;
|
|
239
|
+
}
|
|
132
240
|
export function applyCreatedInteractiveSessionToConfig(config, createdSession, workerPaneIds) {
|
|
133
241
|
config.tmux_session = createdSession.name;
|
|
242
|
+
config.tmux_session_id = createdSession.tmuxSessionId;
|
|
243
|
+
config.tmux_session_created = createdSession.tmuxSessionCreated;
|
|
134
244
|
config.leader_pane_id = createdSession.leaderPaneId;
|
|
135
245
|
config.hud_pane_id = createdSession.hudPaneId;
|
|
246
|
+
config.leader_pane_pid = typeof createdSession.leaderPanePid === 'number' && Number.isSafeInteger(createdSession.leaderPanePid) && createdSession.leaderPanePid > 0
|
|
247
|
+
? createdSession.leaderPanePid
|
|
248
|
+
: null;
|
|
249
|
+
config.hud_pane_pid = typeof createdSession.hudPanePid === 'number' && Number.isSafeInteger(createdSession.hudPanePid) && createdSession.hudPanePid > 0
|
|
250
|
+
? createdSession.hudPanePid
|
|
251
|
+
: null;
|
|
136
252
|
config.tmux_pane_owner_id = createdSession.teamPaneOwnerId;
|
|
137
253
|
config.resize_hook_name = createdSession.resizeHookName;
|
|
138
254
|
config.resize_hook_target = createdSession.resizeHookTarget;
|
|
255
|
+
config.startup_cleanup_panes = createdSession.startupCleanupPanes
|
|
256
|
+
?.map(({ paneId, panePid }) => ({ pane_id: paneId, pid: panePid }))
|
|
257
|
+
.filter(({ pane_id }) => /^%[0-9]+$/.test(pane_id));
|
|
258
|
+
const paneIdsByIndex = createdSession.workerPaneIdsByIndex;
|
|
259
|
+
const panePidsByIndex = createdSession.workerPanePidsByIndex;
|
|
260
|
+
if (paneIdsByIndex) {
|
|
261
|
+
for (let i = 0; i < paneIdsByIndex.length; i++) {
|
|
262
|
+
const paneId = paneIdsByIndex[i];
|
|
263
|
+
if (!paneId)
|
|
264
|
+
continue;
|
|
265
|
+
workerPaneIds[i] = paneId;
|
|
266
|
+
if (config.workers[i]) {
|
|
267
|
+
config.workers[i].pane_id = paneId;
|
|
268
|
+
const panePid = panePidsByIndex?.[i];
|
|
269
|
+
if (typeof panePid === 'number' && Number.isSafeInteger(panePid) && panePid > 0)
|
|
270
|
+
config.workers[i].pid = panePid;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
139
275
|
for (let i = 0; i < createdSession.workerPaneIds.length; i++) {
|
|
140
276
|
const paneId = createdSession.workerPaneIds[i];
|
|
141
277
|
workerPaneIds[i] = paneId;
|
|
142
278
|
if (config.workers[i]) {
|
|
143
279
|
config.workers[i].pane_id = paneId;
|
|
280
|
+
const panePid = createdSession.workerPanePidsByIndex?.[i];
|
|
281
|
+
if (typeof panePid === 'number' && Number.isSafeInteger(panePid) && panePid > 0)
|
|
282
|
+
config.workers[i].pid = panePid;
|
|
144
283
|
}
|
|
145
284
|
}
|
|
146
285
|
}
|
|
@@ -167,25 +306,41 @@ function collectShutdownPaneIds(params) {
|
|
|
167
306
|
}
|
|
168
307
|
return [...paneIds];
|
|
169
308
|
}
|
|
170
|
-
function
|
|
309
|
+
function collectAuthorizedSharedSessionWorkerPaneIds(paneIds, teamPaneOwnerId, canonicalWorkerPaneIds, initiallyTaggedWorkerPaneIds, authorizedTaggedWorkerPaneIds) {
|
|
171
310
|
const expectedOwnerId = teamPaneOwnerId.trim();
|
|
172
|
-
if (!expectedOwnerId)
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
if (
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
//
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
311
|
+
if (!expectedOwnerId && paneIds.length > 0) {
|
|
312
|
+
throw new Error('shutdown_shared_session_worker_owner_unavailable:missing_team_owner_id');
|
|
313
|
+
}
|
|
314
|
+
const authorized = new Set();
|
|
315
|
+
for (const paneId of paneIds) {
|
|
316
|
+
const owner = readPaneTeamOwnerTagResult(paneId);
|
|
317
|
+
if (initiallyTaggedWorkerPaneIds?.has(paneId)
|
|
318
|
+
&& (owner.status !== 'value' || owner.value !== expectedOwnerId)) {
|
|
319
|
+
throw new Error(`shutdown_shared_session_worker_owner_changed:${paneId}`);
|
|
320
|
+
}
|
|
321
|
+
if (owner.status === 'error') {
|
|
322
|
+
// A read failure is authority ambiguity only for an explicit persisted
|
|
323
|
+
// worker that topology independently identifies as a worker candidate.
|
|
324
|
+
if (canonicalWorkerPaneIds.has(paneId)) {
|
|
325
|
+
throw new Error(`shutdown_shared_session_worker_owner_unavailable:${paneId}:${owner.error}`);
|
|
326
|
+
}
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
if (owner.status === 'value') {
|
|
330
|
+
if (owner.value === expectedOwnerId) {
|
|
331
|
+
authorized.add(paneId);
|
|
332
|
+
authorizedTaggedWorkerPaneIds?.add(paneId);
|
|
333
|
+
}
|
|
334
|
+
else if (canonicalWorkerPaneIds.has(paneId)) {
|
|
335
|
+
throw new Error(`shutdown_shared_session_worker_owner_changed:${paneId}`);
|
|
336
|
+
}
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
if (canonicalWorkerPaneIds.has(paneId)) {
|
|
340
|
+
throw new Error(`shutdown_shared_session_worker_owner_changed:${paneId}`);
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
return [...authorized];
|
|
189
344
|
}
|
|
190
345
|
function isSharedSessionHudPaneReclaimable(params) {
|
|
191
346
|
const { paneId, persistedHudPaneId, leaderOwnedHudPaneIds, teamPaneOwnerId, onOwnerReadError } = params;
|
|
@@ -227,6 +382,209 @@ export function shouldPrekillInteractiveShutdownProcessTrees(sessionName) {
|
|
|
227
382
|
// teardown alone is insufficient.
|
|
228
383
|
return true;
|
|
229
384
|
}
|
|
385
|
+
function canonicalDetachedStateRoot(config, cwd) {
|
|
386
|
+
return resolve(config?.team_state_root ?? resolveCanonicalTeamStateRoot(cwd));
|
|
387
|
+
}
|
|
388
|
+
function detachedSessionDestroyReceiptPath(teamName, cwd, config) {
|
|
389
|
+
return join(canonicalDetachedStateRoot(config ?? null, cwd), 'team', teamName, '.detached-session-destroy-receipt.json');
|
|
390
|
+
}
|
|
391
|
+
function detachedConfigIdentity(teamName, cwd, config) {
|
|
392
|
+
const ownerId = typeof config.tmux_pane_owner_id === 'string' ? config.tmux_pane_owner_id.trim() : '';
|
|
393
|
+
const paneId = typeof config.leader_pane_id === 'string' ? config.leader_pane_id.trim() : '';
|
|
394
|
+
const panePid = config.leader_pane_pid;
|
|
395
|
+
if (config.name !== teamName || !config.created_at || !config.tmux_session || !ownerId || !/^%[0-9]+$/.test(paneId)
|
|
396
|
+
|| typeof panePid !== 'number' || !Number.isSafeInteger(panePid) || panePid <= 0)
|
|
397
|
+
return null;
|
|
398
|
+
// Bind the durable bytes of both canonical files, not only authorization
|
|
399
|
+
// fields. A worker/membership mutation invalidates a published receipt.
|
|
400
|
+
try {
|
|
401
|
+
const teamRoot = join(canonicalDetachedStateRoot(config, cwd), 'team', teamName);
|
|
402
|
+
const configBytes = readFileSync(join(teamRoot, 'config.json'), 'utf8');
|
|
403
|
+
const manifestBytes = readFileSync(join(teamRoot, 'manifest.v2.json'), 'utf8');
|
|
404
|
+
const parsedConfig = JSON.parse(configBytes);
|
|
405
|
+
const parsedManifest = JSON.parse(manifestBytes);
|
|
406
|
+
if (!Number.isSafeInteger(parsedConfig.config_generation)
|
|
407
|
+
|| !Number.isSafeInteger(parsedManifest.config_generation)
|
|
408
|
+
|| parsedConfig.config_generation !== parsedManifest.config_generation)
|
|
409
|
+
return null;
|
|
410
|
+
return createHash('sha256').update(JSON.stringify({
|
|
411
|
+
version: 2,
|
|
412
|
+
team_name: teamName,
|
|
413
|
+
state_root: canonicalDetachedStateRoot(config, cwd),
|
|
414
|
+
config_bytes: configBytes,
|
|
415
|
+
manifest_bytes: manifestBytes,
|
|
416
|
+
})).digest('hex');
|
|
417
|
+
}
|
|
418
|
+
catch {
|
|
419
|
+
return null;
|
|
420
|
+
}
|
|
421
|
+
}
|
|
422
|
+
/** Deterministic test seam for mutations after durable intent publication. */
|
|
423
|
+
let detachedSessionDestroyAfterJournalHook = null;
|
|
424
|
+
export function setDetachedSessionDestroyAfterJournalHookForTest(hook) {
|
|
425
|
+
detachedSessionDestroyAfterJournalHook = hook;
|
|
426
|
+
}
|
|
427
|
+
function isDetachedSessionDestroyReceipt(value) {
|
|
428
|
+
if (!value || typeof value !== 'object')
|
|
429
|
+
return false;
|
|
430
|
+
const receipt = value;
|
|
431
|
+
return receipt.schema_version === 2 && receipt.schema === 'omx.detached_session_destroy.v2'
|
|
432
|
+
&& receipt.operation === 'detached_session_destroy' && (receipt.status === 'intent' || receipt.status === 'accepted')
|
|
433
|
+
&& typeof receipt.team_name === 'string' && receipt.team_name.length > 0
|
|
434
|
+
&& typeof receipt.state_root === 'string' && receipt.state_root.length > 0 && resolve(receipt.state_root) === receipt.state_root
|
|
435
|
+
&& receipt.config_identity_version === 2 && typeof receipt.config_identity_digest === 'string' && /^[a-f0-9]{64}$/.test(receipt.config_identity_digest)
|
|
436
|
+
&& typeof receipt.session_name === 'string' && receipt.session_name.length > 0 && !receipt.session_name.includes(':')
|
|
437
|
+
&& typeof receipt.session_id === 'string' && receipt.session_id.length > 0 && !/\s/.test(receipt.session_id)
|
|
438
|
+
&& typeof receipt.session_created === 'string' && /^[0-9]+$/.test(receipt.session_created)
|
|
439
|
+
&& typeof receipt.leader_pane_id === 'string' && /^%[0-9]+$/.test(receipt.leader_pane_id)
|
|
440
|
+
&& typeof receipt.leader_pane_pid === 'number' && Number.isSafeInteger(receipt.leader_pane_pid) && receipt.leader_pane_pid > 0
|
|
441
|
+
&& typeof receipt.owner_id === 'string' && receipt.owner_id.length > 0;
|
|
442
|
+
}
|
|
443
|
+
async function readDetachedSessionDestroyReceipt(teamName, cwd, config) {
|
|
444
|
+
const path = detachedSessionDestroyReceiptPath(teamName, cwd, config);
|
|
445
|
+
if (!existsSync(path))
|
|
446
|
+
return null;
|
|
447
|
+
try {
|
|
448
|
+
const parsed = JSON.parse(await readFile(path, 'utf-8'));
|
|
449
|
+
if (!isDetachedSessionDestroyReceipt(parsed) || parsed.team_name !== teamName || parsed.state_root !== canonicalDetachedStateRoot(config, cwd))
|
|
450
|
+
throw new Error('invalid receipt');
|
|
451
|
+
return parsed;
|
|
452
|
+
}
|
|
453
|
+
catch {
|
|
454
|
+
throw new Error(`detached_session_destroy_receipt_malformed:${config?.tmux_session || 'missing_session'}`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
async function writeDetachedSessionDestroyReceipt(teamName, cwd, config, receipt) {
|
|
458
|
+
await writeAtomic(detachedSessionDestroyReceiptPath(teamName, cwd, config), `${JSON.stringify(receipt, null, 2)}\n`);
|
|
459
|
+
}
|
|
460
|
+
function validateDetachedReceiptConfig(teamName, cwd, config, receipt) {
|
|
461
|
+
if (receipt.team_name !== teamName || receipt.state_root !== canonicalDetachedStateRoot(config, cwd)
|
|
462
|
+
|| receipt.config_identity_version !== 2 || detachedConfigIdentity(teamName, cwd, config) !== receipt.config_identity_digest
|
|
463
|
+
|| config.tmux_session !== receipt.session_name || config.tmux_session_id !== receipt.session_id
|
|
464
|
+
|| config.tmux_session_created !== receipt.session_created || config.leader_pane_id !== receipt.leader_pane_id
|
|
465
|
+
|| config.leader_pane_pid !== receipt.leader_pane_pid || config.tmux_pane_owner_id !== receipt.owner_id) {
|
|
466
|
+
throw new Error(`detached_session_destroy_receipt_config_mismatch:${receipt.session_name}`);
|
|
467
|
+
}
|
|
468
|
+
}
|
|
469
|
+
function assertDetachedSessionDestroyAuthority(receipt) {
|
|
470
|
+
const expected = { sessionId: receipt.session_id, sessionCreated: receipt.session_created };
|
|
471
|
+
const query = queryDetachedTeamSession(receipt.session_name, expected);
|
|
472
|
+
if (query.status !== 'exact'
|
|
473
|
+
|| !queryDetachedSessionLeaderBinding(receipt.leader_pane_id, receipt.leader_pane_pid, receipt.session_name, expected)) {
|
|
474
|
+
throw new Error(`detached_session_destroy_authorization_unavailable:${receipt.session_name}`);
|
|
475
|
+
}
|
|
476
|
+
const proof = readExactPaneProofSync(receipt.leader_pane_id);
|
|
477
|
+
if (proof.status !== 'live' || proof.pid !== receipt.leader_pane_pid)
|
|
478
|
+
throw new Error(`detached_session_destroy_authorization_unavailable:${receipt.session_name}`);
|
|
479
|
+
const owner = readPaneTeamOwnerTagResult(proof.paneId);
|
|
480
|
+
if (owner.status !== 'value' || owner.value !== receipt.owner_id)
|
|
481
|
+
throw new Error(`detached_session_destroy_authorization_unavailable:${receipt.session_name}`);
|
|
482
|
+
}
|
|
483
|
+
function requestDetachedSessionDestroyAtFinalSink(receipt) {
|
|
484
|
+
return requestDetachedTeamSessionDestroy(receipt.session_name, {
|
|
485
|
+
sessionId: receipt.session_id,
|
|
486
|
+
sessionCreated: receipt.session_created,
|
|
487
|
+
leaderPaneId: receipt.leader_pane_id,
|
|
488
|
+
leaderPanePid: receipt.leader_pane_pid,
|
|
489
|
+
ownerId: receipt.owner_id,
|
|
490
|
+
});
|
|
491
|
+
}
|
|
492
|
+
async function withDetachedSessionDestroyLock(teamName, cwd, fn) {
|
|
493
|
+
return await withTeamTaskMembershipBarrier(teamName, cwd, fn);
|
|
494
|
+
}
|
|
495
|
+
async function convergeDetachedSessionDestroyReceiptUnlocked(teamName, cwd, config, receipt) {
|
|
496
|
+
const query = queryDetachedTeamSession(receipt.session_name, { sessionId: receipt.session_id, sessionCreated: receipt.session_created });
|
|
497
|
+
if (query.status === 'unavailable' || query.status === 'replacement')
|
|
498
|
+
throw new Error(`detached_session_destroy_unresolved:${receipt.session_name}`);
|
|
499
|
+
if (query.status === 'absent') {
|
|
500
|
+
if (config) {
|
|
501
|
+
validateDetachedReceiptConfig(teamName, cwd, config, receipt);
|
|
502
|
+
config.tmux_session = '';
|
|
503
|
+
await saveTeamConfig(config, cwd);
|
|
504
|
+
}
|
|
505
|
+
else if (receipt.status !== 'accepted') {
|
|
506
|
+
throw new Error(`detached_session_destroy_authorization_unavailable:${receipt.session_name}`);
|
|
507
|
+
}
|
|
508
|
+
return true;
|
|
509
|
+
}
|
|
510
|
+
if (!config)
|
|
511
|
+
throw new Error(`detached_session_destroy_authorization_unavailable:${receipt.session_name}`);
|
|
512
|
+
validateDetachedReceiptConfig(teamName, cwd, config, receipt);
|
|
513
|
+
assertDetachedSessionDestroyAuthority(receipt);
|
|
514
|
+
if (!requestDetachedSessionDestroyAtFinalSink(receipt)) {
|
|
515
|
+
throw new Error(`detached_session_destroy_unresolved:${receipt.session_name}`);
|
|
516
|
+
}
|
|
517
|
+
await writeDetachedSessionDestroyReceipt(teamName, cwd, config, { ...receipt, status: 'accepted' });
|
|
518
|
+
return await convergeDetachedSessionDestroyReceiptUnlocked(teamName, cwd, config, { ...receipt, status: 'accepted' });
|
|
519
|
+
}
|
|
520
|
+
async function reconcileDetachedSessionDestroyReceipt(teamName, cwd, config) {
|
|
521
|
+
return await withDetachedSessionDestroyLock(teamName, cwd, async () => {
|
|
522
|
+
const canonical = await readTeamConfig(teamName, cwd);
|
|
523
|
+
const receipt = await readDetachedSessionDestroyReceipt(teamName, cwd, canonical ?? config);
|
|
524
|
+
if (!receipt)
|
|
525
|
+
return false;
|
|
526
|
+
if (!canonical) {
|
|
527
|
+
// Name/file presence is not authority. Accepted absence requires a
|
|
528
|
+
// successfully parsed current canonical generation.
|
|
529
|
+
throw new Error(`detached_session_destroy_authorization_unavailable:${receipt.session_name}`);
|
|
530
|
+
}
|
|
531
|
+
return await convergeDetachedSessionDestroyReceiptUnlocked(teamName, cwd, canonical, receipt);
|
|
532
|
+
});
|
|
533
|
+
}
|
|
534
|
+
async function destroyConfiguredDetachedTeamSession(teamName, cwd, config) {
|
|
535
|
+
if (!config.tmux_session)
|
|
536
|
+
return;
|
|
537
|
+
await withDetachedSessionDestroyLock(teamName, cwd, async () => {
|
|
538
|
+
const canonical = await readTeamConfig(teamName, cwd);
|
|
539
|
+
if (!canonical)
|
|
540
|
+
throw new Error(`detached_session_destroy_authorization_unavailable:${config.tmux_session}`);
|
|
541
|
+
const existing = await readDetachedSessionDestroyReceipt(teamName, cwd, canonical);
|
|
542
|
+
if (existing) {
|
|
543
|
+
await convergeDetachedSessionDestroyReceiptUnlocked(teamName, cwd, canonical, existing);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
const sessionName = canonical.tmux_session;
|
|
547
|
+
const sessionId = typeof canonical.tmux_session_id === 'string' ? canonical.tmux_session_id : '';
|
|
548
|
+
const sessionCreated = typeof canonical.tmux_session_created === 'string' ? canonical.tmux_session_created : '';
|
|
549
|
+
const digest = detachedConfigIdentity(teamName, cwd, canonical);
|
|
550
|
+
const ownerId = typeof canonical.tmux_pane_owner_id === 'string' ? canonical.tmux_pane_owner_id.trim() : '';
|
|
551
|
+
const leaderPaneId = typeof canonical.leader_pane_id === 'string' ? canonical.leader_pane_id.trim() : '';
|
|
552
|
+
const leaderPanePid = canonical.leader_pane_pid;
|
|
553
|
+
if (!digest || !sessionName || !/^\$[0-9]+$/.test(sessionId) || !/^[0-9]+$/.test(sessionCreated) || !ownerId || !/^%[0-9]+$/.test(leaderPaneId)
|
|
554
|
+
|| typeof leaderPanePid !== 'number' || !Number.isSafeInteger(leaderPanePid) || leaderPanePid <= 0) {
|
|
555
|
+
throw new Error(`detached_session_destroy_authorization_unavailable:${sessionName || 'missing_session'}`);
|
|
556
|
+
}
|
|
557
|
+
const receipt = {
|
|
558
|
+
schema_version: 2, schema: 'omx.detached_session_destroy.v2', operation: 'detached_session_destroy', status: 'intent',
|
|
559
|
+
team_name: teamName, state_root: canonicalDetachedStateRoot(canonical, cwd), config_identity_version: 2, config_identity_digest: digest,
|
|
560
|
+
session_name: sessionName, session_id: sessionId, session_created: sessionCreated,
|
|
561
|
+
leader_pane_id: leaderPaneId, leader_pane_pid: leaderPanePid, owner_id: ownerId,
|
|
562
|
+
};
|
|
563
|
+
assertDetachedSessionDestroyAuthority(receipt);
|
|
564
|
+
await writeDetachedSessionDestroyReceipt(teamName, cwd, canonical, receipt);
|
|
565
|
+
const afterJournalHook = detachedSessionDestroyAfterJournalHook;
|
|
566
|
+
detachedSessionDestroyAfterJournalHook = null;
|
|
567
|
+
await afterJournalHook?.();
|
|
568
|
+
const current = await readTeamConfig(teamName, cwd);
|
|
569
|
+
if (!current)
|
|
570
|
+
throw new Error(`detached_session_destroy_authorization_unavailable:${sessionName}`);
|
|
571
|
+
validateDetachedReceiptConfig(teamName, cwd, current, receipt);
|
|
572
|
+
assertDetachedSessionDestroyAuthority(receipt);
|
|
573
|
+
if (!requestDetachedSessionDestroyAtFinalSink(receipt)) {
|
|
574
|
+
throw new Error(`detached_session_destroy_unresolved:${sessionName}`);
|
|
575
|
+
}
|
|
576
|
+
await writeDetachedSessionDestroyReceipt(teamName, cwd, current, { ...receipt, status: 'accepted' });
|
|
577
|
+
await convergeDetachedSessionDestroyReceiptUnlocked(teamName, cwd, current, { ...receipt, status: 'accepted' });
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
function assertPaneTeardownProofsAvailable(operation, unavailableProofs) {
|
|
581
|
+
if (unavailableProofs.length === 0)
|
|
582
|
+
return;
|
|
583
|
+
const detail = unavailableProofs
|
|
584
|
+
.map((proof) => `${proof.paneId}:${proof.reason}${proof.detail ? `:${proof.detail}` : ''}`)
|
|
585
|
+
.join(',');
|
|
586
|
+
throw new Error(`${operation}_pane_proof_unavailable:${detail}`);
|
|
587
|
+
}
|
|
230
588
|
export async function cleanupTeamWorkerLaunchOrphanedMcpProcesses(dependencies = {}) {
|
|
231
589
|
const cleanup = dependencies.cleanup ?? (() => cleanupOmxMcpProcesses([], {
|
|
232
590
|
selectCandidates: findLaunchSafeCleanupCandidates,
|
|
@@ -1314,10 +1672,13 @@ function isStartupEvidenceMissingReason(reason) {
|
|
|
1314
1672
|
|| normalized.includes('no_evidence')
|
|
1315
1673
|
|| normalized.includes('fallback_attempted_but_unconfirmed');
|
|
1316
1674
|
}
|
|
1675
|
+
function isStartupIncompatibleReason(reason) {
|
|
1676
|
+
return reason.trim() === 'codex_bypass_mdm_incompatible';
|
|
1677
|
+
}
|
|
1317
1678
|
async function recordRecoverableStartupIssue(params) {
|
|
1318
1679
|
const { teamName, workerName, taskIds, reason, cwd } = params;
|
|
1319
1680
|
const updatedAt = new Date().toISOString();
|
|
1320
|
-
const currentTaskId = isStartupEvidenceMissingReason(reason) ? undefined : taskIds[0];
|
|
1681
|
+
const currentTaskId = isStartupEvidenceMissingReason(reason) || isStartupIncompatibleReason(reason) ? undefined : taskIds[0];
|
|
1321
1682
|
await writeWorkerStatus(teamName, workerName, {
|
|
1322
1683
|
state: 'unknown',
|
|
1323
1684
|
current_task_id: currentTaskId,
|
|
@@ -1384,7 +1745,7 @@ function registerPromptWorkerHandle(teamName, workerName, child) {
|
|
|
1384
1745
|
if (!teamHandles)
|
|
1385
1746
|
return;
|
|
1386
1747
|
const handle = teamHandles.get(workerName);
|
|
1387
|
-
if (handle?.processGroupId &&
|
|
1748
|
+
if (handle?.processGroupId && probeProcessGroupLiveness(handle.processGroupId) !== 'gone') {
|
|
1388
1749
|
return;
|
|
1389
1750
|
}
|
|
1390
1751
|
teamHandles.delete(workerName);
|
|
@@ -1403,34 +1764,70 @@ function removePromptWorkerHandle(teamName, workerName) {
|
|
|
1403
1764
|
if (teamHandles.size === 0)
|
|
1404
1765
|
promptWorkerRegistry.delete(teamName);
|
|
1405
1766
|
}
|
|
1406
|
-
function
|
|
1767
|
+
function probePidLiveness(pid) {
|
|
1407
1768
|
if (!Number.isFinite(pid) || pid <= 0)
|
|
1408
|
-
return
|
|
1769
|
+
return 'gone';
|
|
1409
1770
|
try {
|
|
1410
1771
|
process.kill(pid, 0);
|
|
1411
|
-
return
|
|
1772
|
+
return 'alive';
|
|
1412
1773
|
}
|
|
1413
1774
|
catch (err) {
|
|
1414
1775
|
if (err.code === 'ESRCH')
|
|
1415
|
-
return
|
|
1776
|
+
return 'gone';
|
|
1416
1777
|
process.stderr.write(`[team/runtime] operation failed: ${err}\n`);
|
|
1417
|
-
return
|
|
1778
|
+
return 'unknown';
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
function isPidAlive(pid) {
|
|
1782
|
+
return probePidLiveness(pid) === 'alive';
|
|
1783
|
+
}
|
|
1784
|
+
function isPidGone(pid) {
|
|
1785
|
+
return probePidLiveness(pid) === 'gone';
|
|
1786
|
+
}
|
|
1787
|
+
async function captureProcessIdentity(pid) {
|
|
1788
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
1789
|
+
return null;
|
|
1790
|
+
const observation = defaultProcessInspectionProvider.observeProcess(pid, process.platform);
|
|
1791
|
+
if (observation.kind !== 'identity')
|
|
1792
|
+
return null;
|
|
1793
|
+
return { pid, process_identity: observation.identity };
|
|
1794
|
+
}
|
|
1795
|
+
async function captureProcessIdentities(pids) {
|
|
1796
|
+
const identities = await Promise.all([...new Set(pids)].map((pid) => captureProcessIdentity(pid)));
|
|
1797
|
+
return identities.every((identity) => identity !== null) ? identities : null;
|
|
1798
|
+
}
|
|
1799
|
+
async function probeProcessIdentity(identity) {
|
|
1800
|
+
const recorded = 'process_identity' in identity
|
|
1801
|
+
? identity.process_identity
|
|
1802
|
+
: { platform: 'linux', birth: identity.start_time };
|
|
1803
|
+
const runtimePlatform = 'process_identity' in identity
|
|
1804
|
+
? identity.process_identity.platform
|
|
1805
|
+
: 'linux'; // V1 is always Linux.
|
|
1806
|
+
if (runtimePlatform !== process.platform && 'process_identity' in identity) {
|
|
1807
|
+
return 'reused_or_unknown';
|
|
1808
|
+
}
|
|
1809
|
+
const classification = classifyRecordedIdentity(recorded, process.platform, defaultProcessInspectionProvider, identity.pid);
|
|
1810
|
+
switch (classification.status) {
|
|
1811
|
+
case 'match': return 'same';
|
|
1812
|
+
case 'gone': return 'gone';
|
|
1813
|
+
case 'birth-mismatch': return 'reused_or_unknown';
|
|
1814
|
+
case 'identity-unavailable': return 'reused_or_unknown';
|
|
1418
1815
|
}
|
|
1419
1816
|
}
|
|
1420
|
-
function
|
|
1817
|
+
function probeProcessGroupLiveness(processGroupId) {
|
|
1421
1818
|
if (process.platform === 'win32')
|
|
1422
|
-
return
|
|
1819
|
+
return 'gone';
|
|
1423
1820
|
if (!Number.isFinite(processGroupId) || processGroupId <= 0)
|
|
1424
|
-
return
|
|
1821
|
+
return 'gone';
|
|
1425
1822
|
try {
|
|
1426
1823
|
process.kill(-processGroupId, 0);
|
|
1427
|
-
return
|
|
1824
|
+
return 'alive';
|
|
1428
1825
|
}
|
|
1429
1826
|
catch (err) {
|
|
1430
1827
|
if (err.code === 'ESRCH')
|
|
1431
|
-
return
|
|
1828
|
+
return 'gone';
|
|
1432
1829
|
process.stderr.write(`[team/runtime] operation failed: ${err}\n`);
|
|
1433
|
-
return
|
|
1830
|
+
return 'unknown';
|
|
1434
1831
|
}
|
|
1435
1832
|
}
|
|
1436
1833
|
function listProcessTreeEntries() {
|
|
@@ -1491,11 +1888,11 @@ async function waitForTrackedPidsExit(pids, timeoutMs) {
|
|
|
1491
1888
|
return true;
|
|
1492
1889
|
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
1493
1890
|
while (Date.now() < deadline) {
|
|
1494
|
-
if (tracked.every(
|
|
1891
|
+
if (tracked.every(isPidGone))
|
|
1495
1892
|
return true;
|
|
1496
1893
|
await new Promise((resolve) => setTimeout(resolve, PROMPT_WORKER_EXIT_POLL_MS));
|
|
1497
1894
|
}
|
|
1498
|
-
return tracked.every(
|
|
1895
|
+
return tracked.every(isPidGone);
|
|
1499
1896
|
}
|
|
1500
1897
|
async function terminateTrackedProcessTree(rootPid, processGroupId = null, graceMs = PROMPT_WORKER_SIGTERM_WAIT_MS, killWaitMs = PROMPT_WORKER_SIGKILL_WAIT_MS) {
|
|
1501
1898
|
if (processGroupId && process.platform !== 'win32') {
|
|
@@ -1522,9 +1919,9 @@ async function terminateTrackedProcessTree(rootPid, processGroupId = null, grace
|
|
|
1522
1919
|
}
|
|
1523
1920
|
const groupDeadline = Date.now() + Math.max(0, graceMs);
|
|
1524
1921
|
while (Date.now() < groupDeadline) {
|
|
1525
|
-
const
|
|
1526
|
-
const
|
|
1527
|
-
if (
|
|
1922
|
+
const groupGone = probeProcessGroupLiveness(processGroupId) === 'gone';
|
|
1923
|
+
const descendantsGone = trackedPids.every(isPidGone);
|
|
1924
|
+
if (groupGone && descendantsGone) {
|
|
1528
1925
|
return { terminated: true, forcedKill: false, trackedPids };
|
|
1529
1926
|
}
|
|
1530
1927
|
await new Promise((resolve) => setTimeout(resolve, PROMPT_WORKER_EXIT_POLL_MS));
|
|
@@ -1551,15 +1948,15 @@ async function terminateTrackedProcessTree(rootPid, processGroupId = null, grace
|
|
|
1551
1948
|
}
|
|
1552
1949
|
const killDeadline = Date.now() + Math.max(0, killWaitMs);
|
|
1553
1950
|
while (Date.now() < killDeadline) {
|
|
1554
|
-
const
|
|
1555
|
-
const
|
|
1556
|
-
if (
|
|
1951
|
+
const groupGone = probeProcessGroupLiveness(processGroupId) === 'gone';
|
|
1952
|
+
const descendantsGone = trackedPids.every(isPidGone);
|
|
1953
|
+
if (groupGone && descendantsGone) {
|
|
1557
1954
|
return { terminated: true, forcedKill: true, trackedPids };
|
|
1558
1955
|
}
|
|
1559
1956
|
await new Promise((resolve) => setTimeout(resolve, PROMPT_WORKER_EXIT_POLL_MS));
|
|
1560
1957
|
}
|
|
1561
1958
|
return {
|
|
1562
|
-
terminated:
|
|
1959
|
+
terminated: probeProcessGroupLiveness(processGroupId) === 'gone' && trackedPids.every(isPidGone),
|
|
1563
1960
|
forcedKill: true,
|
|
1564
1961
|
trackedPids,
|
|
1565
1962
|
};
|
|
@@ -1567,7 +1964,7 @@ async function terminateTrackedProcessTree(rootPid, processGroupId = null, grace
|
|
|
1567
1964
|
const trackedPids = collectProcessTreePids(rootPid);
|
|
1568
1965
|
if (trackedPids.length === 0) {
|
|
1569
1966
|
return {
|
|
1570
|
-
terminated:
|
|
1967
|
+
terminated: isPidGone(rootPid),
|
|
1571
1968
|
forcedKill: false,
|
|
1572
1969
|
trackedPids: [],
|
|
1573
1970
|
};
|
|
@@ -1603,6 +2000,348 @@ async function terminateTrackedProcessTree(rootPid, processGroupId = null, grace
|
|
|
1603
2000
|
trackedPids,
|
|
1604
2001
|
};
|
|
1605
2002
|
}
|
|
2003
|
+
/**
|
|
2004
|
+
* Every operating-system effect attributed to an explicit pane must re-read
|
|
2005
|
+
* the pane's global row immediately before that effect. A changed/dead row is
|
|
2006
|
+
* not a fallback opportunity: it stops this pane's process-tree teardown.
|
|
2007
|
+
*/
|
|
2008
|
+
function reproveExactPaneAfterProcessEsrcH(paneId) {
|
|
2009
|
+
const proof = readExactPaneProofSync(paneId);
|
|
2010
|
+
if (proof.status === 'unavailable')
|
|
2011
|
+
return { status: 'unavailable', proof };
|
|
2012
|
+
if (proof.status === 'gone')
|
|
2013
|
+
return { status: 'gone' };
|
|
2014
|
+
// ESRCH establishes only that the process was absent. A still-live pane row
|
|
2015
|
+
// is not authority to advance the pane lifecycle without an absent reproof.
|
|
2016
|
+
return { status: 'stopped' };
|
|
2017
|
+
}
|
|
2018
|
+
function signalExactPaneProcess(paneId, authorizedPanePid, pid, signal) {
|
|
2019
|
+
const proof = readExactPaneProofSync(paneId);
|
|
2020
|
+
if (proof.status === 'unavailable')
|
|
2021
|
+
return { status: 'unavailable', proof };
|
|
2022
|
+
if (proof.status === 'live' && proof.pid !== authorizedPanePid)
|
|
2023
|
+
return { status: 'stopped' };
|
|
2024
|
+
if (proof.status === 'gone')
|
|
2025
|
+
return { status: 'stopped' };
|
|
2026
|
+
try {
|
|
2027
|
+
process.kill(pid, signal);
|
|
2028
|
+
return { status: 'alive' };
|
|
2029
|
+
}
|
|
2030
|
+
catch (error) {
|
|
2031
|
+
return error.code === 'ESRCH'
|
|
2032
|
+
? reproveExactPaneAfterProcessEsrcH(paneId)
|
|
2033
|
+
: { status: 'unknown' };
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
function probeExactPaneProcess(paneId, authorizedPanePid, pid) {
|
|
2037
|
+
const proof = readExactPaneProofSync(paneId);
|
|
2038
|
+
if (proof.status === 'unavailable')
|
|
2039
|
+
return { status: 'unavailable', proof };
|
|
2040
|
+
if (proof.status === 'live' && proof.pid !== authorizedPanePid)
|
|
2041
|
+
return { status: 'stopped' };
|
|
2042
|
+
try {
|
|
2043
|
+
process.kill(pid, 0);
|
|
2044
|
+
return proof.status === 'gone' ? { status: 'stopped' } : { status: 'alive' };
|
|
2045
|
+
}
|
|
2046
|
+
catch (error) {
|
|
2047
|
+
return error.code === 'ESRCH'
|
|
2048
|
+
? reproveExactPaneAfterProcessEsrcH(paneId)
|
|
2049
|
+
: { status: 'unknown' };
|
|
2050
|
+
}
|
|
2051
|
+
}
|
|
2052
|
+
async function waitForExactPaneTrackedPidsExit(paneId, authorizedPanePid, pids, timeoutMs) {
|
|
2053
|
+
const trackedPids = [...pids];
|
|
2054
|
+
const deadline = Date.now() + Math.max(0, timeoutMs);
|
|
2055
|
+
do {
|
|
2056
|
+
let anyAlive = false;
|
|
2057
|
+
for (const pid of trackedPids) {
|
|
2058
|
+
const probe = probeExactPaneProcess(paneId, authorizedPanePid, pid);
|
|
2059
|
+
if (probe.status === 'unavailable')
|
|
2060
|
+
return { terminated: false, stopped: true, trackedPids, authorizedPanePid, proofUnavailable: probe.proof };
|
|
2061
|
+
if (probe.status === 'stopped')
|
|
2062
|
+
return { terminated: false, stopped: true, trackedPids, authorizedPanePid };
|
|
2063
|
+
if (probe.status === 'unknown')
|
|
2064
|
+
return { terminated: false, stopped: true, trackedPids, authorizedPanePid };
|
|
2065
|
+
if (probe.status === 'alive')
|
|
2066
|
+
anyAlive = true;
|
|
2067
|
+
}
|
|
2068
|
+
if (!anyAlive)
|
|
2069
|
+
return { terminated: true, stopped: false, trackedPids, authorizedPanePid };
|
|
2070
|
+
if (Date.now() >= deadline)
|
|
2071
|
+
break;
|
|
2072
|
+
await new Promise((resolve) => setTimeout(resolve, PROMPT_WORKER_EXIT_POLL_MS));
|
|
2073
|
+
} while (Date.now() < deadline);
|
|
2074
|
+
for (const pid of trackedPids) {
|
|
2075
|
+
const probe = probeExactPaneProcess(paneId, authorizedPanePid, pid);
|
|
2076
|
+
if (probe.status === 'unavailable')
|
|
2077
|
+
return { terminated: false, stopped: true, trackedPids, authorizedPanePid, proofUnavailable: probe.proof };
|
|
2078
|
+
if (probe.status === 'stopped' || probe.status === 'unknown')
|
|
2079
|
+
return { terminated: false, stopped: true, trackedPids, authorizedPanePid };
|
|
2080
|
+
if (probe.status === 'alive')
|
|
2081
|
+
return { terminated: false, stopped: false, trackedPids, authorizedPanePid };
|
|
2082
|
+
}
|
|
2083
|
+
return { terminated: true, stopped: false, trackedPids, authorizedPanePid };
|
|
2084
|
+
}
|
|
2085
|
+
async function terminateExactPaneProcessTree(paneId, expectedPanePid, graceMs = PROMPT_WORKER_SIGTERM_WAIT_MS, killWaitMs = PROMPT_WORKER_SIGKILL_WAIT_MS, authorizePaneEffect) {
|
|
2086
|
+
const authorization = readExactPaneProofSync(paneId);
|
|
2087
|
+
if (authorization.status === 'unavailable')
|
|
2088
|
+
return { terminated: false, stopped: true, trackedPids: [], proofUnavailable: authorization };
|
|
2089
|
+
if (authorization.status === 'gone')
|
|
2090
|
+
return { terminated: true, stopped: true, trackedPids: [] };
|
|
2091
|
+
if (typeof expectedPanePid === 'number' && authorization.pid !== expectedPanePid) {
|
|
2092
|
+
return { terminated: false, stopped: true, trackedPids: [], authorizedPanePid: authorization.pid, proofUnavailable: {
|
|
2093
|
+
status: 'unavailable', paneId, reason: 'pane_pid_changed', detail: `expected ${expectedPanePid}, got ${authorization.pid}`,
|
|
2094
|
+
} };
|
|
2095
|
+
}
|
|
2096
|
+
if (authorizePaneEffect && !authorizePaneEffect(paneId, authorization.pid)) {
|
|
2097
|
+
return {
|
|
2098
|
+
terminated: false,
|
|
2099
|
+
stopped: true,
|
|
2100
|
+
trackedPids: [],
|
|
2101
|
+
authorizedPanePid: authorization.pid,
|
|
2102
|
+
proofUnavailable: {
|
|
2103
|
+
status: 'unavailable',
|
|
2104
|
+
paneId,
|
|
2105
|
+
reason: 'pane_pid_changed',
|
|
2106
|
+
detail: 'pane owner authorization changed',
|
|
2107
|
+
},
|
|
2108
|
+
};
|
|
2109
|
+
}
|
|
2110
|
+
const trackedPids = collectProcessTreePids(authorization.pid);
|
|
2111
|
+
if (trackedPids.length === 0)
|
|
2112
|
+
return { terminated: true, stopped: false, trackedPids, authorizedPanePid: authorization.pid };
|
|
2113
|
+
// Birth evidence is mandatory for descendant signals. A pane's pinned PID
|
|
2114
|
+
// authorizes its root process, but a numeric descendant PID is never enough:
|
|
2115
|
+
// its /proc start time must still match immediately before each signal.
|
|
2116
|
+
const trackedProcessIdentities = await captureProcessIdentities(trackedPids) ?? undefined;
|
|
2117
|
+
const trackedProcessIdentityByPid = new Map((trackedProcessIdentities ?? []).map((identity) => [identity.pid, identity]));
|
|
2118
|
+
const revalidateDescendantIdentity = async (pid) => {
|
|
2119
|
+
if (pid === authorization.pid)
|
|
2120
|
+
return true;
|
|
2121
|
+
const identity = trackedProcessIdentityByPid.get(pid);
|
|
2122
|
+
return identity !== undefined && (await probeProcessIdentity(identity)) === 'same';
|
|
2123
|
+
};
|
|
2124
|
+
for (const pid of trackedPids) {
|
|
2125
|
+
if (authorizePaneEffect && !authorizePaneEffect(paneId, authorization.pid)) {
|
|
2126
|
+
return {
|
|
2127
|
+
terminated: false,
|
|
2128
|
+
stopped: true,
|
|
2129
|
+
trackedPids,
|
|
2130
|
+
trackedProcessIdentities,
|
|
2131
|
+
authorizedPanePid: authorization.pid,
|
|
2132
|
+
proofUnavailable: {
|
|
2133
|
+
status: 'unavailable',
|
|
2134
|
+
paneId,
|
|
2135
|
+
reason: 'pane_pid_changed',
|
|
2136
|
+
detail: 'pane owner authorization changed',
|
|
2137
|
+
},
|
|
2138
|
+
};
|
|
2139
|
+
}
|
|
2140
|
+
if (!await revalidateDescendantIdentity(pid)) {
|
|
2141
|
+
return { terminated: false, stopped: true, trackedPids, trackedProcessIdentities, authorizedPanePid: authorization.pid };
|
|
2142
|
+
}
|
|
2143
|
+
const signal = signalExactPaneProcess(paneId, authorization.pid, pid, 'SIGTERM');
|
|
2144
|
+
if (signal.status === 'unavailable')
|
|
2145
|
+
return { terminated: false, stopped: true, trackedPids, trackedProcessIdentities, authorizedPanePid: authorization.pid, proofUnavailable: signal.proof };
|
|
2146
|
+
if (signal.status === 'stopped' || signal.status === 'unknown')
|
|
2147
|
+
return { terminated: false, stopped: true, trackedPids, trackedProcessIdentities, authorizedPanePid: authorization.pid };
|
|
2148
|
+
}
|
|
2149
|
+
const graceful = await waitForExactPaneTrackedPidsExit(paneId, authorization.pid, trackedPids, graceMs);
|
|
2150
|
+
if (graceful.terminated || graceful.stopped)
|
|
2151
|
+
return { ...graceful, trackedProcessIdentities };
|
|
2152
|
+
for (const pid of trackedPids) {
|
|
2153
|
+
const probe = probeExactPaneProcess(paneId, authorization.pid, pid);
|
|
2154
|
+
if (probe.status === 'unavailable')
|
|
2155
|
+
return { terminated: false, stopped: true, trackedPids, trackedProcessIdentities, authorizedPanePid: authorization.pid, proofUnavailable: probe.proof };
|
|
2156
|
+
if (probe.status === 'stopped')
|
|
2157
|
+
return { terminated: false, stopped: true, trackedPids, trackedProcessIdentities, authorizedPanePid: authorization.pid };
|
|
2158
|
+
if (probe.status === 'gone')
|
|
2159
|
+
continue;
|
|
2160
|
+
if (authorizePaneEffect && !authorizePaneEffect(paneId, authorization.pid)) {
|
|
2161
|
+
return {
|
|
2162
|
+
terminated: false,
|
|
2163
|
+
stopped: true,
|
|
2164
|
+
trackedPids,
|
|
2165
|
+
trackedProcessIdentities,
|
|
2166
|
+
authorizedPanePid: authorization.pid,
|
|
2167
|
+
proofUnavailable: {
|
|
2168
|
+
status: 'unavailable',
|
|
2169
|
+
paneId,
|
|
2170
|
+
reason: 'pane_pid_changed',
|
|
2171
|
+
detail: 'pane owner authorization changed',
|
|
2172
|
+
},
|
|
2173
|
+
};
|
|
2174
|
+
}
|
|
2175
|
+
if (!await revalidateDescendantIdentity(pid)) {
|
|
2176
|
+
return { terminated: false, stopped: true, trackedPids, trackedProcessIdentities, authorizedPanePid: authorization.pid };
|
|
2177
|
+
}
|
|
2178
|
+
const signal = signalExactPaneProcess(paneId, authorization.pid, pid, 'SIGKILL');
|
|
2179
|
+
if (signal.status === 'unavailable')
|
|
2180
|
+
return { terminated: false, stopped: true, trackedPids, trackedProcessIdentities, authorizedPanePid: authorization.pid, proofUnavailable: signal.proof };
|
|
2181
|
+
if (signal.status === 'stopped' || signal.status === 'unknown')
|
|
2182
|
+
return { terminated: false, stopped: true, trackedPids, trackedProcessIdentities, authorizedPanePid: authorization.pid };
|
|
2183
|
+
}
|
|
2184
|
+
return { ...(await waitForExactPaneTrackedPidsExit(paneId, authorization.pid, trackedPids, killWaitMs)), trackedProcessIdentities };
|
|
2185
|
+
}
|
|
2186
|
+
function gonePaneDescendantCleanupDebtPath(teamName, cwd, config) {
|
|
2187
|
+
const stateRoot = config?.team_state_root ?? resolveCanonicalTeamStateRoot(resolve(cwd));
|
|
2188
|
+
return join(stateRoot, 'team', teamName, '.gone-pane-descendant-cleanup-debt.json');
|
|
2189
|
+
}
|
|
2190
|
+
function trackedPidsFromGonePaneDebtEntry(entry) {
|
|
2191
|
+
return 'tracked_pids' in entry
|
|
2192
|
+
? entry.tracked_pids
|
|
2193
|
+
: entry.tracked_processes.map((identity) => identity.pid);
|
|
2194
|
+
}
|
|
2195
|
+
async function persistGonePaneDescendantCleanupDebt(params) {
|
|
2196
|
+
const { teamName, cwd, config, paneId, teardown } = params;
|
|
2197
|
+
const unresolvedTrackedPids = teardown.trackedPids.filter((pid) => probePidLiveness(pid) !== 'gone');
|
|
2198
|
+
if (unresolvedTrackedPids.length === 0 || typeof teardown.authorizedPanePid !== 'number')
|
|
2199
|
+
return;
|
|
2200
|
+
const liveTrackedProcesses = (teardown.trackedProcessIdentities ?? []).filter((identity) => (unresolvedTrackedPids.includes(identity.pid)));
|
|
2201
|
+
const debtPath = gonePaneDescendantCleanupDebtPath(teamName, cwd, config);
|
|
2202
|
+
let prior = { schema_version: 1, operation: 'gone_pane_descendant_cleanup', entries: [] };
|
|
2203
|
+
try {
|
|
2204
|
+
prior = JSON.parse(await readFile(debtPath, 'utf8'));
|
|
2205
|
+
}
|
|
2206
|
+
catch (error) {
|
|
2207
|
+
if (error.code !== 'ENOENT')
|
|
2208
|
+
throw new Error('gone_pane_descendant_cleanup_debt_unreadable');
|
|
2209
|
+
}
|
|
2210
|
+
if (!isValidGonePaneDescendantCleanupDebt(prior))
|
|
2211
|
+
throw new Error('gone_pane_descendant_cleanup_debt_malformed');
|
|
2212
|
+
const priorEntry = prior.entries.find((entry) => entry.pane_id === paneId);
|
|
2213
|
+
const entries = prior.entries.filter((entry) => entry.pane_id !== paneId);
|
|
2214
|
+
if (liveTrackedProcesses.length !== unresolvedTrackedPids.length) {
|
|
2215
|
+
const priorIdentities = priorEntry?.tracked_processes ?? [];
|
|
2216
|
+
const identitiesByPid = new Map(priorIdentities.map((identity) => [identity.pid, identity]));
|
|
2217
|
+
for (const identity of liveTrackedProcesses) {
|
|
2218
|
+
if (!identitiesByPid.has(identity.pid))
|
|
2219
|
+
identitiesByPid.set(identity.pid, identity);
|
|
2220
|
+
}
|
|
2221
|
+
entries.push({
|
|
2222
|
+
pane_id: paneId,
|
|
2223
|
+
authorized_pane_pid: teardown.authorizedPanePid,
|
|
2224
|
+
tracked_pids: [...new Set([...(priorEntry ? trackedPidsFromGonePaneDebtEntry(priorEntry) : []), ...unresolvedTrackedPids])],
|
|
2225
|
+
...(identitiesByPid.size > 0 ? { tracked_processes: [...identitiesByPid.values()] } : {}),
|
|
2226
|
+
evidence: 'process_identity_unavailable',
|
|
2227
|
+
});
|
|
2228
|
+
}
|
|
2229
|
+
else {
|
|
2230
|
+
entries.push({
|
|
2231
|
+
pane_id: paneId,
|
|
2232
|
+
authorized_pane_pid: teardown.authorizedPanePid,
|
|
2233
|
+
tracked_processes: liveTrackedProcesses,
|
|
2234
|
+
evidence: teardown.proofUnavailable
|
|
2235
|
+
? `${teardown.proofUnavailable.reason}:${teardown.proofUnavailable.detail ?? ''}`
|
|
2236
|
+
: 'pane_authority_lost_during_descendant_teardown',
|
|
2237
|
+
});
|
|
2238
|
+
}
|
|
2239
|
+
await writeAtomic(debtPath, JSON.stringify({ schema_version: 1, operation: 'gone_pane_descendant_cleanup', entries }, null, 2));
|
|
2240
|
+
}
|
|
2241
|
+
const TRACKED_PROCESS_PLATFORMS = new Set([
|
|
2242
|
+
'aix', 'android', 'darwin', 'freebsd', 'haiku', 'linux', 'openbsd', 'sunos', 'win32', 'cygwin', 'netbsd',
|
|
2243
|
+
]);
|
|
2244
|
+
const TRACKED_PROCESS_BIRTH_PATTERN = /^\d+(?:\.\d+)?$/;
|
|
2245
|
+
const TRACKED_PROCESS_CMDLINE_HASH_PATTERN = /^[a-f0-9]{64}$/;
|
|
2246
|
+
function isValidSharedProcessIdentity(value) {
|
|
2247
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
2248
|
+
return false;
|
|
2249
|
+
const identity = value;
|
|
2250
|
+
if (Object.keys(identity).some((key) => key !== 'platform' && key !== 'birth' && key !== 'cmdline_hash'))
|
|
2251
|
+
return false;
|
|
2252
|
+
return typeof identity.platform === 'string'
|
|
2253
|
+
&& TRACKED_PROCESS_PLATFORMS.has(identity.platform)
|
|
2254
|
+
&& typeof identity.birth === 'string'
|
|
2255
|
+
&& TRACKED_PROCESS_BIRTH_PATTERN.test(identity.birth)
|
|
2256
|
+
&& (identity.cmdline_hash === undefined
|
|
2257
|
+
|| typeof identity.cmdline_hash === 'string' && TRACKED_PROCESS_CMDLINE_HASH_PATTERN.test(identity.cmdline_hash));
|
|
2258
|
+
}
|
|
2259
|
+
function isValidProcessIdentity(value) {
|
|
2260
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value))
|
|
2261
|
+
return false;
|
|
2262
|
+
const candidate = value;
|
|
2263
|
+
if (!Number.isSafeInteger(candidate.pid) || candidate.pid <= 0)
|
|
2264
|
+
return false;
|
|
2265
|
+
if ('process_identity' in candidate)
|
|
2266
|
+
return isValidSharedProcessIdentity(candidate.process_identity);
|
|
2267
|
+
return typeof candidate.start_time === 'string' && /^[0-9]+$/.test(candidate.start_time);
|
|
2268
|
+
}
|
|
2269
|
+
function isValidGonePaneDescendantCleanupDebt(value) {
|
|
2270
|
+
if (typeof value !== 'object' || value === null)
|
|
2271
|
+
return false;
|
|
2272
|
+
const debt = value;
|
|
2273
|
+
if (debt.schema_version !== 1 || debt.operation !== 'gone_pane_descendant_cleanup' || !Array.isArray(debt.entries))
|
|
2274
|
+
return false;
|
|
2275
|
+
if (new Set(debt.entries.map((entry) => entry?.pane_id)).size !== debt.entries.length)
|
|
2276
|
+
return false;
|
|
2277
|
+
return debt.entries.every((entry) => {
|
|
2278
|
+
if (typeof entry !== 'object' || entry === null)
|
|
2279
|
+
return false;
|
|
2280
|
+
const candidate = entry;
|
|
2281
|
+
if (typeof candidate.pane_id !== 'string' || !/^%[0-9]+$/.test(candidate.pane_id)
|
|
2282
|
+
|| !Number.isSafeInteger(candidate.authorized_pane_pid)
|
|
2283
|
+
|| candidate.authorized_pane_pid <= 0)
|
|
2284
|
+
return false;
|
|
2285
|
+
if (candidate.evidence === 'process_identity_unavailable') {
|
|
2286
|
+
const trackedPids = candidate.tracked_pids;
|
|
2287
|
+
if (!Array.isArray(candidate.tracked_pids) || candidate.tracked_pids.length === 0
|
|
2288
|
+
|| trackedPids.some((pid) => !Number.isSafeInteger(pid) || pid <= 0)
|
|
2289
|
+
|| new Set(trackedPids).size !== trackedPids.length)
|
|
2290
|
+
return false;
|
|
2291
|
+
if (candidate.tracked_processes === undefined)
|
|
2292
|
+
return true;
|
|
2293
|
+
if (!Array.isArray(candidate.tracked_processes) || !candidate.tracked_processes.every(isValidProcessIdentity))
|
|
2294
|
+
return false;
|
|
2295
|
+
const trackedProcesses = candidate.tracked_processes;
|
|
2296
|
+
return trackedProcesses.every((identity) => trackedPids.includes(identity.pid))
|
|
2297
|
+
&& new Set(trackedProcesses.map((identity) => identity.pid)).size === trackedProcesses.length;
|
|
2298
|
+
}
|
|
2299
|
+
if (typeof candidate.evidence !== 'string' || candidate.evidence.length === 0
|
|
2300
|
+
|| candidate.tracked_pids !== undefined
|
|
2301
|
+
|| !Array.isArray(candidate.tracked_processes) || candidate.tracked_processes.length === 0
|
|
2302
|
+
|| !candidate.tracked_processes.every(isValidProcessIdentity))
|
|
2303
|
+
return false;
|
|
2304
|
+
const trackedProcesses = candidate.tracked_processes;
|
|
2305
|
+
return new Set(trackedProcesses.map((identity) => identity.pid)).size === trackedProcesses.length;
|
|
2306
|
+
});
|
|
2307
|
+
}
|
|
2308
|
+
async function reconcileGonePaneDescendantCleanupDebt(teamName, cwd, config) {
|
|
2309
|
+
const debtPath = gonePaneDescendantCleanupDebtPath(teamName, cwd, config);
|
|
2310
|
+
if (!existsSync(debtPath))
|
|
2311
|
+
return;
|
|
2312
|
+
let debt;
|
|
2313
|
+
try {
|
|
2314
|
+
debt = JSON.parse(await readFile(debtPath, 'utf8'));
|
|
2315
|
+
}
|
|
2316
|
+
catch {
|
|
2317
|
+
throw new Error('gone_pane_descendant_cleanup_debt_unreadable');
|
|
2318
|
+
}
|
|
2319
|
+
if (!isValidGonePaneDescendantCleanupDebt(debt))
|
|
2320
|
+
throw new Error('gone_pane_descendant_cleanup_debt_malformed');
|
|
2321
|
+
const unresolved = [];
|
|
2322
|
+
for (const entry of debt.entries) {
|
|
2323
|
+
if ('tracked_pids' in entry) {
|
|
2324
|
+
const trackedProcesses = entry.tracked_processes ?? [];
|
|
2325
|
+
const knownIdentities = new Map(trackedProcesses.map((identity) => [identity.pid, identity]));
|
|
2326
|
+
const states = await Promise.all(entry.tracked_pids.map((pid) => {
|
|
2327
|
+
const identity = knownIdentities.get(pid);
|
|
2328
|
+
return identity ? probeProcessIdentity(identity) : Promise.resolve(probePidLiveness(pid) === 'gone' ? 'gone' : 'reused_or_unknown');
|
|
2329
|
+
}));
|
|
2330
|
+
if (states.some((state) => state !== 'gone'))
|
|
2331
|
+
unresolved.push(entry);
|
|
2332
|
+
continue;
|
|
2333
|
+
}
|
|
2334
|
+
const states = await Promise.all(entry.tracked_processes.map(probeProcessIdentity));
|
|
2335
|
+
if (states.some((state) => state !== 'gone'))
|
|
2336
|
+
unresolved.push(entry);
|
|
2337
|
+
}
|
|
2338
|
+
if (unresolved.length === 0) {
|
|
2339
|
+
await removeDurableFile(debtPath);
|
|
2340
|
+
return;
|
|
2341
|
+
}
|
|
2342
|
+
await writeAtomic(debtPath, JSON.stringify({ ...debt, entries: unresolved }, null, 2));
|
|
2343
|
+
throw new Error(`gone_pane_descendant_cleanup_debt_unresolved:${unresolved.map((entry) => entry.pane_id).join(',')}`);
|
|
2344
|
+
}
|
|
1606
2345
|
async function teardownPromptWorker(teamName, workerName, fallbackPid, cwd, context) {
|
|
1607
2346
|
const handle = getPromptWorkerHandle(teamName, workerName);
|
|
1608
2347
|
const handlePid = handle?.pid;
|
|
@@ -1615,7 +2354,9 @@ async function teardownPromptWorker(teamName, workerName, fallbackPid, cwd, cont
|
|
|
1615
2354
|
return { terminated: true, forcedKill: false, pid: null };
|
|
1616
2355
|
}
|
|
1617
2356
|
const teardown = await terminateTrackedProcessTree(pid ?? 0, processGroupId);
|
|
1618
|
-
const processGone = processGroupId
|
|
2357
|
+
const processGone = processGroupId
|
|
2358
|
+
? probeProcessGroupLiveness(processGroupId) === 'gone'
|
|
2359
|
+
: isPidGone(pid);
|
|
1619
2360
|
if (teardown.terminated && processGone) {
|
|
1620
2361
|
removePromptWorkerHandle(teamName, workerName);
|
|
1621
2362
|
return { terminated: true, forcedKill: teardown.forcedKill, pid };
|
|
@@ -1625,31 +2366,27 @@ async function teardownPromptWorker(teamName, workerName, fallbackPid, cwd, cont
|
|
|
1625
2366
|
worker: workerName,
|
|
1626
2367
|
reason: `prompt_force_kill:${context}:pid=${pid}`,
|
|
1627
2368
|
}, 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 };
|
|
2369
|
+
await appendTeamEvent(teamName, {
|
|
2370
|
+
type: 'worker_stopped',
|
|
2371
|
+
worker: workerName,
|
|
2372
|
+
reason: `prompt_teardown_failed:${context}:pid=${pid}`,
|
|
2373
|
+
}, cwd).catch(() => { });
|
|
2374
|
+
return {
|
|
2375
|
+
terminated: false,
|
|
2376
|
+
forcedKill: teardown.forcedKill,
|
|
2377
|
+
pid,
|
|
2378
|
+
error: 'still_alive_after_sigkill',
|
|
2379
|
+
};
|
|
1643
2380
|
}
|
|
1644
2381
|
function isPromptWorkerAlive(config, worker) {
|
|
1645
2382
|
const handle = getPromptWorkerHandle(config.name, worker.name);
|
|
1646
2383
|
if (handle?.child.exitCode === null && !handle.child.killed)
|
|
1647
2384
|
return true;
|
|
1648
|
-
if (handle?.processGroupId &&
|
|
2385
|
+
if (handle?.processGroupId && probeProcessGroupLiveness(handle.processGroupId) !== 'gone')
|
|
1649
2386
|
return true;
|
|
1650
|
-
if (process.platform !== 'win32' &&
|
|
2387
|
+
if (process.platform !== 'win32' && probeProcessGroupLiveness(worker.pid) !== 'gone')
|
|
1651
2388
|
return true;
|
|
1652
|
-
return
|
|
2389
|
+
return probePidLiveness(worker.pid) !== 'gone';
|
|
1653
2390
|
}
|
|
1654
2391
|
export { TEAM_LOW_COMPLEXITY_DEFAULT_MODEL };
|
|
1655
2392
|
export { resolveCanonicalTeamStateRoot };
|
|
@@ -1774,7 +2511,7 @@ async function writeDecompositionArtifacts(teamName, cwd, metadata) {
|
|
|
1774
2511
|
await writeFile(join(root, 'decomposition-report.json'), JSON.stringify(metadata, null, 2), 'utf8');
|
|
1775
2512
|
const manifest = await readTeamManifestV2(teamName, cwd);
|
|
1776
2513
|
if (manifest) {
|
|
1777
|
-
await
|
|
2514
|
+
await writeTeamManifestV2State({ ...manifest, team_decomposition: { ...metadata } }, cwd);
|
|
1778
2515
|
}
|
|
1779
2516
|
}
|
|
1780
2517
|
export async function settleStartupAttemptResults(attempts) {
|
|
@@ -1820,6 +2557,8 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
|
|
|
1820
2557
|
: rawIdentityScope;
|
|
1821
2558
|
const sanitized = buildInternalTeamName(displayName, identityScope);
|
|
1822
2559
|
const leaderSessionId = identityScope.sessionId || identityScope.paneId || identityScope.tmuxTarget || identityScope.runId;
|
|
2560
|
+
const existingStartupConfig = await readTeamConfig(sanitized, leaderCwd);
|
|
2561
|
+
await reconcileGonePaneDescendantCleanupDebt(sanitized, leaderCwd, existingStartupConfig ?? undefined);
|
|
1823
2562
|
await assertTeamStartupIsNonDestructive(sanitized, leaderCwd, leaderSessionId);
|
|
1824
2563
|
if (displayName !== sanitized) {
|
|
1825
2564
|
await assertTeamStartupIsNonDestructive(displayName, leaderCwd, leaderSessionId);
|
|
@@ -1913,7 +2652,18 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
|
|
|
1913
2652
|
let sessionCreated = false;
|
|
1914
2653
|
const createdWorkerPaneIds = [];
|
|
1915
2654
|
let createdLeaderPaneId;
|
|
2655
|
+
// A returned interactive session has successfully tagged these panes. Rollback
|
|
2656
|
+
// must retain that ownership authorization through its final kill proof.
|
|
2657
|
+
const startupTaggedPaneOwnerIds = new Map();
|
|
2658
|
+
const authorizeStartupRollbackPaneEffect = (paneId) => {
|
|
2659
|
+
const expectedOwnerId = startupTaggedPaneOwnerIds.get(paneId);
|
|
2660
|
+
if (!expectedOwnerId)
|
|
2661
|
+
return true;
|
|
2662
|
+
const currentOwner = readPaneTeamOwnerTagResult(paneId);
|
|
2663
|
+
return currentOwner.status === 'value' && currentOwner.value === expectedOwnerId;
|
|
2664
|
+
};
|
|
1916
2665
|
let config = null;
|
|
2666
|
+
const workerPaneIds = Array.from({ length: workerCount }, () => undefined);
|
|
1917
2667
|
const workerReadyTimeoutMs = resolveWorkerReadyTimeoutMs(launchEnv);
|
|
1918
2668
|
const workerStartupEvidenceTimeoutMs = resolveWorkerStartupEvidenceTimeoutMs(launchEnv, workerReadyTimeoutMs);
|
|
1919
2669
|
const startupDispatchRetries = resolveStartupDispatchRetries(launchEnv);
|
|
@@ -2111,7 +2861,6 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
|
|
|
2111
2861
|
workerRole: plan.workerRole,
|
|
2112
2862
|
};
|
|
2113
2863
|
});
|
|
2114
|
-
const workerPaneIds = Array.from({ length: workerCount }, () => undefined);
|
|
2115
2864
|
const materializeWorkerStartupState = async (bootstrapPlan, workerIndex, paneId) => {
|
|
2116
2865
|
const workerWorkspace = bootstrapPlan.workerWorkspace;
|
|
2117
2866
|
const identity = {
|
|
@@ -2128,13 +2877,19 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
|
|
|
2128
2877
|
worktree_created: workerWorkspace.worktreeCreated,
|
|
2129
2878
|
team_state_root: teamStateRoot,
|
|
2130
2879
|
};
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
if (
|
|
2134
|
-
|
|
2880
|
+
const persistedPanePid = config?.workers[workerIndex - 1]?.pid;
|
|
2881
|
+
if (workerLaunchMode === 'interactive' && paneId) {
|
|
2882
|
+
if (typeof persistedPanePid !== 'number' || !Number.isSafeInteger(persistedPanePid) || persistedPanePid <= 0) {
|
|
2883
|
+
throw new Error(`startup_worker_pane_pid_unavailable:${paneId}`);
|
|
2884
|
+
}
|
|
2885
|
+
const paneProof = readExactPaneProofSync(paneId);
|
|
2886
|
+
if (paneProof.status !== 'live' || paneProof.pid !== persistedPanePid) {
|
|
2887
|
+
throw new Error(`startup_worker_pane_identity_changed:${paneId}`);
|
|
2888
|
+
}
|
|
2889
|
+
identity.pid = persistedPanePid;
|
|
2135
2890
|
}
|
|
2136
|
-
else if (
|
|
2137
|
-
identity.pid =
|
|
2891
|
+
else if (typeof persistedPanePid === 'number') {
|
|
2892
|
+
identity.pid = persistedPanePid;
|
|
2138
2893
|
}
|
|
2139
2894
|
if (paneId)
|
|
2140
2895
|
identity.pane_id = paneId;
|
|
@@ -2172,6 +2927,7 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
|
|
|
2172
2927
|
writeWarning: options.writeCleanupWarning,
|
|
2173
2928
|
});
|
|
2174
2929
|
const startupTiming = createStartupTimingRecorder(sanitized, leaderCwd);
|
|
2930
|
+
let workerStartupArgs;
|
|
2175
2931
|
if (workerLaunchMode === 'interactive') {
|
|
2176
2932
|
const createdSession = createTeamSession(sanitized, workerCount, leaderCwd, Array.from(workerLaunchPolicyPlan[0]?.workerLaunchArgs ?? []), workerStartups, { ownerSessionId: leaderSessionId, teamPaneOwnerId: config.tmux_pane_owner_id });
|
|
2177
2933
|
sessionName = createdSession.name;
|
|
@@ -2179,6 +2935,21 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
|
|
|
2179
2935
|
createdWorkerPaneIds.push(...createdSession.workerPaneIds);
|
|
2180
2936
|
createdLeaderPaneId = createdSession.leaderPaneId;
|
|
2181
2937
|
applyCreatedInteractiveSessionToConfig(config, createdSession, workerPaneIds);
|
|
2938
|
+
workerStartupArgs = createdSession.workerStartupArgs;
|
|
2939
|
+
const createdOwnerId = typeof createdSession.teamPaneOwnerId === 'string'
|
|
2940
|
+
? createdSession.teamPaneOwnerId.trim()
|
|
2941
|
+
: '';
|
|
2942
|
+
if (createdOwnerId) {
|
|
2943
|
+
for (const paneId of [
|
|
2944
|
+
createdSession.leaderPaneId,
|
|
2945
|
+
...createdSession.workerPaneIds,
|
|
2946
|
+
createdSession.hudPaneId,
|
|
2947
|
+
]) {
|
|
2948
|
+
if (typeof paneId === 'string' && paneId.trim().startsWith('%')) {
|
|
2949
|
+
startupTaggedPaneOwnerIds.set(paneId, createdOwnerId);
|
|
2950
|
+
}
|
|
2951
|
+
}
|
|
2952
|
+
}
|
|
2182
2953
|
for (const [index, paneId] of createdSession.workerPaneIds.entries()) {
|
|
2183
2954
|
startupTiming.mark('split_returned', { worker: `worker-${index + 1}`, pane_id: paneId });
|
|
2184
2955
|
}
|
|
@@ -2187,6 +2958,8 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
|
|
|
2187
2958
|
config.tmux_session = `prompt-${sanitized}`;
|
|
2188
2959
|
config.leader_pane_id = null;
|
|
2189
2960
|
config.hud_pane_id = null;
|
|
2961
|
+
config.leader_pane_pid = null;
|
|
2962
|
+
config.hud_pane_pid = null;
|
|
2190
2963
|
config.resize_hook_name = null;
|
|
2191
2964
|
config.resize_hook_target = null;
|
|
2192
2965
|
for (let i = 1; i <= workerCount; i++) {
|
|
@@ -2248,6 +3021,7 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
|
|
|
2248
3021
|
workerIndex,
|
|
2249
3022
|
paneId,
|
|
2250
3023
|
workerCli: bootstrapPlan.workerCli,
|
|
3024
|
+
finalArgs: workerStartupArgs?.[workerIndex - 1],
|
|
2251
3025
|
inbox,
|
|
2252
3026
|
triggerMessage: trigger,
|
|
2253
3027
|
intent: triggerIntent,
|
|
@@ -2256,13 +3030,48 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
|
|
|
2256
3030
|
timing: startupTiming,
|
|
2257
3031
|
})
|
|
2258
3032
|
: null;
|
|
3033
|
+
if (startupDirectOutcome?.reason === 'codex_bypass_mdm_incompatible') {
|
|
3034
|
+
await recordRecoverableStartupIssue({
|
|
3035
|
+
teamName: sanitized,
|
|
3036
|
+
workerName,
|
|
3037
|
+
taskIds: workerTasks.map((task) => task.id),
|
|
3038
|
+
reason: startupDirectOutcome.reason,
|
|
3039
|
+
cwd: leaderCwd,
|
|
3040
|
+
});
|
|
3041
|
+
return {
|
|
3042
|
+
ok: false,
|
|
3043
|
+
workerIndex,
|
|
3044
|
+
workerName,
|
|
3045
|
+
error: new Error(`worker_startup_incompatible:${workerName}:${startupDirectOutcome.reason}`),
|
|
3046
|
+
};
|
|
3047
|
+
}
|
|
2259
3048
|
let startupReadyPromptObserved = false;
|
|
2260
3049
|
if (workerLaunchMode === 'interactive' && !skipWorkerReadyWait && !initialPrompt && !startupDirectOutcome?.ok) {
|
|
2261
3050
|
startupTiming.mark('ready_wait_start', { worker: workerName, pane_id: paneId });
|
|
2262
|
-
|
|
3051
|
+
let readinessIncompatibility = null;
|
|
3052
|
+
const ready = await waitForWorkerReadyDetailedAsync(sessionName, workerIndex, workerReadyTimeoutMs, paneId, config.workers[workerIndex - 1]?.pid, config.tmux_pane_owner_id ?? undefined, config.hud_pane_id ?? undefined, {
|
|
3053
|
+
workerCli: bootstrapPlan.workerCli,
|
|
3054
|
+
finalArgs: workerStartupArgs?.[workerIndex - 1],
|
|
3055
|
+
onIncompatible: (reason) => { readinessIncompatibility = reason; },
|
|
3056
|
+
});
|
|
2263
3057
|
startupTiming.mark('ready_wait_end', { worker: workerName, pane_id: paneId, ok: ready });
|
|
3058
|
+
if (readinessIncompatibility) {
|
|
3059
|
+
await recordRecoverableStartupIssue({
|
|
3060
|
+
teamName: sanitized,
|
|
3061
|
+
workerName,
|
|
3062
|
+
taskIds: workerTasks.map((task) => task.id),
|
|
3063
|
+
reason: readinessIncompatibility,
|
|
3064
|
+
cwd: leaderCwd,
|
|
3065
|
+
});
|
|
3066
|
+
return {
|
|
3067
|
+
ok: false,
|
|
3068
|
+
workerIndex,
|
|
3069
|
+
workerName,
|
|
3070
|
+
error: new Error(`worker_startup_incompatible:${workerName}:${readinessIncompatibility}`),
|
|
3071
|
+
};
|
|
3072
|
+
}
|
|
2264
3073
|
if (!ready) {
|
|
2265
|
-
const workerAlive = isWorkerPaneOpen(sessionName, workerIndex, paneId);
|
|
3074
|
+
const workerAlive = isWorkerPaneOpen(sessionName, workerIndex, paneId, config.workers[workerIndex - 1]?.pid, config.tmux_pane_owner_id ?? undefined, config.hud_pane_id ?? undefined);
|
|
2266
3075
|
if (workerAlive) {
|
|
2267
3076
|
await recordRecoverableStartupIssue({
|
|
2268
3077
|
teamName: sanitized,
|
|
@@ -2323,8 +3132,8 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
|
|
|
2323
3132
|
break;
|
|
2324
3133
|
if (attempt < startupDispatchRetries) {
|
|
2325
3134
|
if (workerLaunchMode === 'interactive') {
|
|
2326
|
-
if (dismissTrustPromptIfPresent(sessionName, workerIndex, paneId)) {
|
|
2327
|
-
await waitForWorkerReadyAsync(sessionName, workerIndex, workerReadyTimeoutMs, paneId);
|
|
3135
|
+
if (dismissTrustPromptIfPresent(sessionName, workerIndex, paneId, config.workers[workerIndex - 1]?.pid, config.tmux_pane_owner_id ?? undefined, config.hud_pane_id ?? undefined)) {
|
|
3136
|
+
await waitForWorkerReadyAsync(sessionName, workerIndex, workerReadyTimeoutMs, paneId, config.workers[workerIndex - 1]?.pid, config.tmux_pane_owner_id ?? undefined, config.hud_pane_id ?? undefined);
|
|
2328
3137
|
}
|
|
2329
3138
|
else {
|
|
2330
3139
|
await new Promise((resolve) => setTimeout(resolve, Math.max(0, startupRetryDelayS * 1000)));
|
|
@@ -2339,7 +3148,7 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
|
|
|
2339
3148
|
if (!dispatchOutcome.ok) {
|
|
2340
3149
|
const workerAlive = workerLaunchMode === 'prompt'
|
|
2341
3150
|
? isPromptWorkerAlive(config, config.workers[workerIndex - 1])
|
|
2342
|
-
: isWorkerPaneOpen(sessionName, workerIndex, paneId);
|
|
3151
|
+
: isWorkerPaneOpen(sessionName, workerIndex, paneId, config.workers[workerIndex - 1]?.pid, config.tmux_pane_owner_id ?? undefined, config.hud_pane_id ?? undefined);
|
|
2343
3152
|
if (workerLaunchMode === 'prompt' && !workerAlive) {
|
|
2344
3153
|
await recordPromptStartupWorkerStopped({
|
|
2345
3154
|
teamName: sanitized,
|
|
@@ -2387,6 +3196,52 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
|
|
|
2387
3196
|
}
|
|
2388
3197
|
catch (error) {
|
|
2389
3198
|
const rollbackErrors = [];
|
|
3199
|
+
let preserveRollbackState = false;
|
|
3200
|
+
if (error instanceof CreateTeamSessionPartialError && config) {
|
|
3201
|
+
const partialSession = error.partialSession;
|
|
3202
|
+
sessionName = partialSession.name;
|
|
3203
|
+
sessionCreated = true;
|
|
3204
|
+
createdWorkerPaneIds.push(...partialSession.workerPaneIds);
|
|
3205
|
+
createdLeaderPaneId = partialSession.leaderPaneId;
|
|
3206
|
+
applyCreatedInteractiveSessionToConfig(config, partialSession, workerPaneIds);
|
|
3207
|
+
await saveTeamConfig(config, leaderCwd);
|
|
3208
|
+
const cleanupDebt = [
|
|
3209
|
+
...error.cleanupErrors,
|
|
3210
|
+
...error.proofUnavailable.map((proof) => `pane_proof_unavailable:${proof.paneId}:${proof.reason}`),
|
|
3211
|
+
];
|
|
3212
|
+
if (cleanupDebt.length > 0) {
|
|
3213
|
+
await appendTeamEvent(sanitized, {
|
|
3214
|
+
type: 'team_leader_nudge',
|
|
3215
|
+
worker: 'leader-fixed',
|
|
3216
|
+
reason: `startup_create_session_cleanup_debt:${cleanupDebt.join(';')}; retry preserved session resources`,
|
|
3217
|
+
}, leaderCwd).catch(() => { });
|
|
3218
|
+
}
|
|
3219
|
+
// CreateTeamSessionPartialError means create-time teardown did not reach a
|
|
3220
|
+
// proven-gone state. Preserve its config, hook registration, state and
|
|
3221
|
+
// worktrees exactly as saved above for a later retry; generic rollback
|
|
3222
|
+
// would otherwise destroy the retry evidence.
|
|
3223
|
+
assertPaneTeardownProofsAvailable('startup_rollback', error.proofUnavailable);
|
|
3224
|
+
throw error;
|
|
3225
|
+
}
|
|
3226
|
+
if (config && error instanceof Error && error.message.startsWith('startup_worker_pane_identity_changed:')) {
|
|
3227
|
+
const startupCleanupDebt = config.workers.flatMap((worker) => {
|
|
3228
|
+
if (!worker.pane_id || typeof worker.pid !== 'number' || !Number.isSafeInteger(worker.pid) || worker.pid <= 0)
|
|
3229
|
+
return [];
|
|
3230
|
+
const proof = readExactPaneProofSync(worker.pane_id);
|
|
3231
|
+
return proof.status === 'live' && proof.pid !== worker.pid
|
|
3232
|
+
? [{ status: 'unavailable', paneId: worker.pane_id, reason: 'pane_pid_changed', detail: `expected ${worker.pid}, got ${proof.pid}` }]
|
|
3233
|
+
: [];
|
|
3234
|
+
});
|
|
3235
|
+
if (startupCleanupDebt.length > 0) {
|
|
3236
|
+
await saveTeamConfig(config, leaderCwd);
|
|
3237
|
+
await appendTeamEvent(sanitized, {
|
|
3238
|
+
type: 'team_leader_nudge',
|
|
3239
|
+
worker: 'leader-fixed',
|
|
3240
|
+
reason: `startup_worker_pane_cleanup_debt:${startupCleanupDebt.map((proof) => `${proof.paneId}:${proof.reason}`).join(',')}; retry preserved session resources`,
|
|
3241
|
+
}, leaderCwd).catch(() => { });
|
|
3242
|
+
assertPaneTeardownProofsAvailable('startup_rollback', startupCleanupDebt);
|
|
3243
|
+
}
|
|
3244
|
+
}
|
|
2390
3245
|
if (sessionCreated) {
|
|
2391
3246
|
if (config?.resize_hook_name && config.resize_hook_target) {
|
|
2392
3247
|
try {
|
|
@@ -2399,44 +3254,87 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
|
|
|
2399
3254
|
rollbackErrors.push(`unregisterResizeHook: ${String(cleanupError)}`);
|
|
2400
3255
|
}
|
|
2401
3256
|
}
|
|
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
3257
|
// In split-pane topology, we must not kill the entire tmux session; kill only created panes.
|
|
2413
3258
|
if (sessionName.includes(':')) {
|
|
2414
|
-
|
|
2415
|
-
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
3259
|
+
const paneProcessProofUnavailable = [];
|
|
3260
|
+
for (const paneId of createdWorkerPaneIds) {
|
|
3261
|
+
const teardown = await terminateExactPaneProcessTree(paneId, config?.workers.find((worker) => worker.pane_id === paneId)?.pid, undefined, undefined, authorizeStartupRollbackPaneEffect);
|
|
3262
|
+
if (teardown.proofUnavailable) {
|
|
3263
|
+
if (config)
|
|
3264
|
+
await persistGonePaneDescendantCleanupDebt({ teamName: sanitized, cwd: leaderCwd, config, paneId, teardown });
|
|
3265
|
+
paneProcessProofUnavailable.push(teardown.proofUnavailable);
|
|
3266
|
+
break;
|
|
2421
3267
|
}
|
|
2422
|
-
|
|
2423
|
-
|
|
3268
|
+
if (!teardown.terminated && teardown.stopped) {
|
|
3269
|
+
if (config)
|
|
3270
|
+
await persistGonePaneDescendantCleanupDebt({ teamName: sanitized, cwd: leaderCwd, config, paneId, teardown });
|
|
3271
|
+
paneProcessProofUnavailable.push({
|
|
3272
|
+
status: 'unavailable',
|
|
3273
|
+
paneId,
|
|
3274
|
+
reason: 'pane_proof_lost_during_process_teardown',
|
|
3275
|
+
detail: 'exact pane authority was lost before the tracked process tree was resolved',
|
|
3276
|
+
});
|
|
3277
|
+
break;
|
|
2424
3278
|
}
|
|
2425
3279
|
}
|
|
2426
|
-
if (
|
|
2427
|
-
|
|
2428
|
-
await
|
|
3280
|
+
if (paneProcessProofUnavailable.length > 0) {
|
|
3281
|
+
if (config)
|
|
3282
|
+
await saveTeamConfig(config, leaderCwd);
|
|
3283
|
+
assertPaneTeardownProofsAvailable('startup_rollback', paneProcessProofUnavailable);
|
|
3284
|
+
}
|
|
3285
|
+
const rollbackPanePids = Object.fromEntries((config?.workers ?? [])
|
|
3286
|
+
.filter((worker) => typeof worker.pane_id === 'string' && typeof worker.pid === 'number')
|
|
3287
|
+
.map((worker) => [worker.pane_id, worker.pid]));
|
|
3288
|
+
if (config?.hud_pane_id && typeof config.hud_pane_pid === 'number') {
|
|
3289
|
+
rollbackPanePids[config.hud_pane_id] = config.hud_pane_pid;
|
|
3290
|
+
}
|
|
3291
|
+
const rollbackPaneIds = [
|
|
3292
|
+
...createdWorkerPaneIds,
|
|
3293
|
+
...(config?.hud_pane_id && typeof config.hud_pane_pid === 'number' ? [config.hud_pane_id] : []),
|
|
3294
|
+
].filter((paneId) => typeof paneId === 'string' && paneId.trim().startsWith('%'));
|
|
3295
|
+
const rollbackPaneTeardown = await teardownWorkerPanes(rollbackPaneIds, {
|
|
3296
|
+
leaderPaneId: createdLeaderPaneId,
|
|
3297
|
+
expectedPanePids: rollbackPanePids,
|
|
3298
|
+
authorizePaneKill: (paneId) => authorizeStartupRollbackPaneEffect(paneId),
|
|
3299
|
+
});
|
|
3300
|
+
const resolvedRollbackPaneIds = new Set([
|
|
3301
|
+
...rollbackPaneTeardown.provenGonePaneIds,
|
|
3302
|
+
...rollbackPaneTeardown.killedPaneIds,
|
|
3303
|
+
]);
|
|
3304
|
+
if (config && (rollbackPaneTeardown.proofUnavailable.length > 0 || rollbackPaneTeardown.kill.failed > 0)) {
|
|
3305
|
+
config.workers = config.workers.filter((worker) => !worker.pane_id || !resolvedRollbackPaneIds.has(worker.pane_id));
|
|
3306
|
+
config.worker_count = config.workers.length;
|
|
3307
|
+
if (config.hud_pane_id && resolvedRollbackPaneIds.has(config.hud_pane_id)) {
|
|
3308
|
+
config.hud_pane_id = null;
|
|
3309
|
+
config.hud_pane_pid = null;
|
|
2429
3310
|
}
|
|
2430
|
-
|
|
2431
|
-
|
|
3311
|
+
}
|
|
3312
|
+
if (rollbackPaneTeardown.proofUnavailable.length > 0) {
|
|
3313
|
+
if (config) {
|
|
3314
|
+
try {
|
|
3315
|
+
await saveTeamConfig(config, leaderCwd);
|
|
3316
|
+
}
|
|
3317
|
+
catch (cleanupError) {
|
|
3318
|
+
rollbackErrors.push(`saveTeamConfig(preserve rollback state): ${String(cleanupError)}`);
|
|
3319
|
+
}
|
|
2432
3320
|
}
|
|
3321
|
+
assertPaneTeardownProofsAvailable('startup_rollback', rollbackPaneTeardown.proofUnavailable);
|
|
3322
|
+
}
|
|
3323
|
+
if (rollbackPaneTeardown.kill.failed > 0) {
|
|
3324
|
+
if (config)
|
|
3325
|
+
await saveTeamConfig(config, leaderCwd);
|
|
3326
|
+
const originalMessage = error instanceof Error ? error.message : String(error);
|
|
3327
|
+
throw new Error(`${originalMessage}; rollback encountered errors: paneTeardown:${rollbackPaneTeardown.kill.failedPaneIds.join(',')}`);
|
|
2433
3328
|
}
|
|
2434
3329
|
}
|
|
2435
3330
|
else {
|
|
2436
3331
|
try {
|
|
2437
|
-
|
|
3332
|
+
if (!config)
|
|
3333
|
+
throw new Error('detached_session_destroy_authorization_unavailable:missing_config');
|
|
3334
|
+
await destroyConfiguredDetachedTeamSession(sanitized, leaderCwd, config);
|
|
2438
3335
|
}
|
|
2439
3336
|
catch (cleanupError) {
|
|
3337
|
+
preserveRollbackState = true;
|
|
2440
3338
|
rollbackErrors.push(`destroyTeamSession: ${String(cleanupError)}`);
|
|
2441
3339
|
}
|
|
2442
3340
|
}
|
|
@@ -2474,11 +3372,21 @@ export async function startTeam(teamName, task, agentType, workerCount, tasks, c
|
|
|
2474
3372
|
}
|
|
2475
3373
|
}
|
|
2476
3374
|
restoreTeamModelInstructionsFile(sanitized);
|
|
3375
|
+
let descendantCleanupDebtUnresolved = false;
|
|
2477
3376
|
try {
|
|
2478
|
-
await
|
|
3377
|
+
await reconcileGonePaneDescendantCleanupDebt(sanitized, leaderCwd, config ?? undefined);
|
|
2479
3378
|
}
|
|
2480
3379
|
catch (cleanupError) {
|
|
2481
|
-
|
|
3380
|
+
descendantCleanupDebtUnresolved = true;
|
|
3381
|
+
rollbackErrors.push(`reconcileGonePaneDescendantCleanupDebt: ${String(cleanupError)}`);
|
|
3382
|
+
}
|
|
3383
|
+
if (!descendantCleanupDebtUnresolved && !preserveRollbackState) {
|
|
3384
|
+
try {
|
|
3385
|
+
await cleanupTeamState(sanitized, leaderCwd);
|
|
3386
|
+
}
|
|
3387
|
+
catch (cleanupError) {
|
|
3388
|
+
rollbackErrors.push(`cleanupTeamState: ${String(cleanupError)}`);
|
|
3389
|
+
}
|
|
2482
3390
|
}
|
|
2483
3391
|
if (provisionedWorktrees.length > 0) {
|
|
2484
3392
|
try {
|
|
@@ -2541,7 +3449,7 @@ export async function monitorTeam(teamName, cwd) {
|
|
|
2541
3449
|
const workerSignals = await Promise.all(config.workers.map(async (worker) => {
|
|
2542
3450
|
const alive = config.worker_launch_mode === 'prompt'
|
|
2543
3451
|
? isPromptWorkerAlive(config, worker)
|
|
2544
|
-
: isWorkerAlive(sessionName, worker.index, worker.pane_id);
|
|
3452
|
+
: isWorkerAlive(sessionName, worker.index, worker.pane_id, worker.pid, config.tmux_pane_owner_id ?? undefined, config.hud_pane_id ?? undefined);
|
|
2545
3453
|
const [status, heartbeat] = await Promise.all([
|
|
2546
3454
|
readWorkerStatus(sanitized, worker.name, cwd),
|
|
2547
3455
|
readWorkerHeartbeat(sanitized, worker.name, cwd),
|
|
@@ -2660,7 +3568,9 @@ export async function monitorTeam(teamName, cwd) {
|
|
|
2660
3568
|
cwd,
|
|
2661
3569
|
});
|
|
2662
3570
|
const mailboxDeliveryStartMs = performance.now();
|
|
2663
|
-
const mailboxNotifiedByMessageId =
|
|
3571
|
+
const mailboxNotifiedByMessageId = phaseState.terminal_epoch
|
|
3572
|
+
? {}
|
|
3573
|
+
: await deliverPendingMailboxMessages(sanitized, config, workers, previousSnapshot?.mailboxNotifiedByMessageId ?? {}, dispatchPolicy, cwd);
|
|
2664
3574
|
const mailboxDeliveryMs = performance.now() - mailboxDeliveryStartMs;
|
|
2665
3575
|
// Prune ephemeral status messages from leader mailbox (TTL: 60s)
|
|
2666
3576
|
try {
|
|
@@ -2723,106 +3633,112 @@ export async function monitorTeam(teamName, cwd) {
|
|
|
2723
3633
|
*/
|
|
2724
3634
|
export async function assignTask(teamName, workerName, taskId, cwd) {
|
|
2725
3635
|
const sanitized = sanitizeTeamName(teamName);
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
const governance = resolveGovernancePolicy(manifest?.governance);
|
|
2731
|
-
if (governance.delegation_only && workerName === 'leader-fixed') {
|
|
2732
|
-
throw new Error('delegation_only_violation');
|
|
2733
|
-
}
|
|
2734
|
-
if (governance.plan_approval_required && task.requires_code_change === true) {
|
|
2735
|
-
const approved = await isTaskApprovedForExecution(sanitized, taskId, cwd);
|
|
2736
|
-
if (!approved) {
|
|
2737
|
-
throw new Error('plan_approval_required');
|
|
3636
|
+
return await withTeamTaskMembershipBarrier(sanitized, cwd, async () => {
|
|
3637
|
+
const phaseState = await readTeamPhaseState(sanitized, cwd);
|
|
3638
|
+
if (phaseState?.terminal_epoch || (phaseState && isTerminalPhase(phaseState.current_phase))) {
|
|
3639
|
+
throw new Error(teamContinuationRequiredDiagnostic(phaseState));
|
|
2738
3640
|
}
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
const dispatchPolicy = resolveDispatchPolicy(manifest?.policy, config.worker_launch_mode);
|
|
2747
|
-
const claim = await claimTask(sanitized, taskId, workerName, task.version ?? 1, cwd);
|
|
2748
|
-
if (!claim.ok) {
|
|
2749
|
-
if (claim.error === 'blocked_dependency') {
|
|
2750
|
-
throw new Error(`blocked_dependency:${(claim.dependencies ?? []).join(',')}`);
|
|
3641
|
+
const task = await readTask(sanitized, taskId, cwd);
|
|
3642
|
+
if (!task)
|
|
3643
|
+
throw new Error(`Task ${taskId} not found`);
|
|
3644
|
+
const manifest = await readTeamManifestV2(sanitized, cwd);
|
|
3645
|
+
const governance = resolveGovernancePolicy(manifest?.governance);
|
|
3646
|
+
if (governance.delegation_only && workerName === 'leader-fixed') {
|
|
3647
|
+
throw new Error('delegation_only_violation');
|
|
2751
3648
|
}
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
const approvedExecutionState = await resolvePersistedApprovedTeamExecutionContinuityState(sanitized, config.leader_cwd ?? cwd, config.team_state_root ?? resolveCanonicalTeamStateRoot(config.leader_cwd ?? cwd));
|
|
2757
|
-
const persistedUltragoalContext = await readPersistedTeamUltragoalContext(sanitized, config.leader_cwd ?? cwd, config.team_state_root ?? resolveCanonicalTeamStateRoot(config.leader_cwd ?? cwd));
|
|
2758
|
-
const approvedContextSection = joinContextSections(approvedExecutionState.status === 'valid'
|
|
2759
|
-
? buildApprovedTeamHandoffSection(approvedExecutionState.approvedHint)
|
|
2760
|
-
: undefined, renderLeaderOwnedUltragoalContextSection(persistedUltragoalContext));
|
|
2761
|
-
const currentTasks = await listTasks(sanitized, cwd);
|
|
2762
|
-
const currentCoordinationPlan = currentTasks.length > 0
|
|
2763
|
-
? synthesizeCoordinationPlans(currentTasks).find((_, index) => currentTasks[index]?.id === task.id) ?? synthesizeCoordinationPlan(task)
|
|
2764
|
-
: synthesizeCoordinationPlan(task);
|
|
2765
|
-
const hasCurrentCoordination = task.coordination?.source === 'explicit' || coordinationPlansEqual(task.coordination, currentCoordinationPlan);
|
|
2766
|
-
const taskForInbox = task.delegation && hasCurrentCoordination
|
|
2767
|
-
? task
|
|
2768
|
-
: (await updateTask(sanitized, taskId, {
|
|
2769
|
-
delegation: task.delegation ?? synthesizeDelegationPlan(task),
|
|
2770
|
-
coordination: task.coordination?.source === 'explicit' ? task.coordination : currentCoordinationPlan,
|
|
2771
|
-
}, cwd)) ?? task;
|
|
2772
|
-
const inbox = generateTaskAssignmentInbox(workerName, sanitized, taskForInbox, { approvedContextSection });
|
|
2773
|
-
const maxAssignRetries = 2;
|
|
2774
|
-
const assignRetryDelayS = 2;
|
|
2775
|
-
let outcome = { ok: false, transport: 'none', reason: 'not_attempted' };
|
|
2776
|
-
const triggerDirective = buildTriggerDirective(workerName, sanitized, resolveInstructionStateRoot(workerInfo.worktree_path));
|
|
2777
|
-
for (let attempt = 1; attempt <= maxAssignRetries; attempt++) {
|
|
2778
|
-
outcome = await dispatchCriticalInboxInstruction({
|
|
2779
|
-
teamName: sanitized,
|
|
2780
|
-
config,
|
|
2781
|
-
workerName,
|
|
2782
|
-
workerIndex: workerInfo.index,
|
|
2783
|
-
paneId: workerInfo.pane_id,
|
|
2784
|
-
inbox,
|
|
2785
|
-
triggerMessage: triggerDirective.text,
|
|
2786
|
-
intent: triggerDirective.intent,
|
|
2787
|
-
cwd,
|
|
2788
|
-
dispatchPolicy,
|
|
2789
|
-
inboxCorrelationKey: `assign:${taskId}:${workerName}`,
|
|
2790
|
-
});
|
|
2791
|
-
if (outcome.ok)
|
|
2792
|
-
break;
|
|
2793
|
-
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);
|
|
2796
|
-
}
|
|
2797
|
-
else {
|
|
2798
|
-
await new Promise(r => setTimeout(r, assignRetryDelayS * 1000));
|
|
2799
|
-
}
|
|
3649
|
+
if (governance.plan_approval_required && task.requires_code_change === true) {
|
|
3650
|
+
const approved = await isTaskApprovedForExecution(sanitized, taskId, cwd);
|
|
3651
|
+
if (!approved) {
|
|
3652
|
+
throw new Error('plan_approval_required');
|
|
2800
3653
|
}
|
|
2801
3654
|
}
|
|
2802
|
-
|
|
2803
|
-
|
|
3655
|
+
const config = await readTeamConfig(sanitized, cwd);
|
|
3656
|
+
if (!config)
|
|
3657
|
+
throw new Error(`Team ${sanitized} not found`);
|
|
3658
|
+
const workerInfo = config.workers.find(w => w.name === workerName);
|
|
3659
|
+
if (!workerInfo)
|
|
3660
|
+
throw new Error(`Worker ${workerName} not found in team`);
|
|
3661
|
+
const dispatchPolicy = resolveDispatchPolicy(manifest?.policy, config.worker_launch_mode);
|
|
3662
|
+
const claim = await claimTask(sanitized, taskId, workerName, task.version ?? 1, cwd);
|
|
3663
|
+
if (!claim.ok) {
|
|
3664
|
+
if (claim.error === 'blocked_dependency') {
|
|
3665
|
+
throw new Error(`blocked_dependency:${(claim.dependencies ?? []).join(',')}`);
|
|
3666
|
+
}
|
|
3667
|
+
throw new Error(claim.error);
|
|
2804
3668
|
}
|
|
2805
|
-
}
|
|
2806
|
-
catch (error) {
|
|
2807
|
-
// Roll back claim to avoid stuck in_progress tasks on any post-claim dispatch failure.
|
|
2808
|
-
const released = await releaseTaskClaim(sanitized, taskId, claim.claimToken, workerName, cwd);
|
|
2809
|
-
const reason = error instanceof Error && error.message.trim() !== ''
|
|
2810
|
-
? error.message
|
|
2811
|
-
: 'worker_assignment_failed';
|
|
2812
3669
|
try {
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
3670
|
+
// Retry dispatch up to 2 times to handle trust prompts during assignment (fixes #393).
|
|
3671
|
+
const approvedExecutionState = await resolvePersistedApprovedTeamExecutionContinuityState(sanitized, config.leader_cwd ?? cwd, config.team_state_root ?? resolveCanonicalTeamStateRoot(config.leader_cwd ?? cwd));
|
|
3672
|
+
const persistedUltragoalContext = await readPersistedTeamUltragoalContext(sanitized, config.leader_cwd ?? cwd, config.team_state_root ?? resolveCanonicalTeamStateRoot(config.leader_cwd ?? cwd));
|
|
3673
|
+
const approvedContextSection = joinContextSections(approvedExecutionState.status === 'valid'
|
|
3674
|
+
? buildApprovedTeamHandoffSection(approvedExecutionState.approvedHint)
|
|
3675
|
+
: undefined, renderLeaderOwnedUltragoalContextSection(persistedUltragoalContext));
|
|
3676
|
+
const currentTasks = await listTasks(sanitized, cwd);
|
|
3677
|
+
const currentCoordinationPlan = currentTasks.length > 0
|
|
3678
|
+
? synthesizeCoordinationPlans(currentTasks).find((_, index) => currentTasks[index]?.id === task.id) ?? synthesizeCoordinationPlan(task)
|
|
3679
|
+
: synthesizeCoordinationPlan(task);
|
|
3680
|
+
const hasCurrentCoordination = task.coordination?.source === 'explicit' || coordinationPlansEqual(task.coordination, currentCoordinationPlan);
|
|
3681
|
+
const taskForInbox = task.delegation && hasCurrentCoordination
|
|
3682
|
+
? task
|
|
3683
|
+
: (await updateTask(sanitized, taskId, {
|
|
3684
|
+
delegation: task.delegation ?? synthesizeDelegationPlan(task),
|
|
3685
|
+
coordination: task.coordination?.source === 'explicit' ? task.coordination : currentCoordinationPlan,
|
|
3686
|
+
}, cwd)) ?? task;
|
|
3687
|
+
const inbox = generateTaskAssignmentInbox(workerName, sanitized, taskForInbox, { approvedContextSection });
|
|
3688
|
+
const maxAssignRetries = 2;
|
|
3689
|
+
const assignRetryDelayS = 2;
|
|
3690
|
+
let outcome = { ok: false, transport: 'none', reason: 'not_attempted' };
|
|
3691
|
+
const triggerDirective = buildTriggerDirective(workerName, sanitized, resolveInstructionStateRoot(workerInfo.worktree_path));
|
|
3692
|
+
for (let attempt = 1; attempt <= maxAssignRetries; attempt++) {
|
|
3693
|
+
outcome = await dispatchCriticalInboxInstruction({
|
|
3694
|
+
teamName: sanitized,
|
|
3695
|
+
config,
|
|
3696
|
+
workerName,
|
|
3697
|
+
workerIndex: workerInfo.index,
|
|
3698
|
+
paneId: workerInfo.pane_id,
|
|
3699
|
+
inbox,
|
|
3700
|
+
triggerMessage: triggerDirective.text,
|
|
3701
|
+
intent: triggerDirective.intent,
|
|
3702
|
+
cwd,
|
|
3703
|
+
dispatchPolicy,
|
|
3704
|
+
inboxCorrelationKey: `assign:${taskId}:${workerName}`,
|
|
3705
|
+
});
|
|
3706
|
+
if (outcome.ok)
|
|
3707
|
+
break;
|
|
3708
|
+
if (attempt < maxAssignRetries && config.worker_launch_mode === 'interactive' && config.tmux_session) {
|
|
3709
|
+
if (dismissTrustPromptIfPresent(config.tmux_session, workerInfo.index, workerInfo.pane_id, workerInfo.pid, config.tmux_pane_owner_id ?? undefined, config.hud_pane_id ?? undefined)) {
|
|
3710
|
+
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);
|
|
3711
|
+
}
|
|
3712
|
+
else {
|
|
3713
|
+
await new Promise(r => setTimeout(r, assignRetryDelayS * 1000));
|
|
3714
|
+
}
|
|
3715
|
+
}
|
|
3716
|
+
}
|
|
3717
|
+
if (!outcome.ok) {
|
|
3718
|
+
throw new Error('worker_notify_failed');
|
|
3719
|
+
}
|
|
2818
3720
|
}
|
|
2819
|
-
|
|
2820
|
-
|
|
3721
|
+
catch (error) {
|
|
3722
|
+
// Roll back claim to avoid stuck in_progress tasks on any post-claim dispatch failure.
|
|
3723
|
+
const released = await releaseTaskClaim(sanitized, taskId, claim.claimToken, workerName, cwd);
|
|
3724
|
+
const reason = error instanceof Error && error.message.trim() !== ''
|
|
3725
|
+
? error.message
|
|
3726
|
+
: 'worker_assignment_failed';
|
|
3727
|
+
try {
|
|
3728
|
+
await writeWorkerInbox(sanitized, workerName, `# Assignment Cancelled\n\nTask ${taskId} was not dispatched due to ${reason}.\nDo not execute this task from prior inbox content.`, cwd);
|
|
3729
|
+
}
|
|
3730
|
+
catch (err) {
|
|
3731
|
+
process.stderr.write(`[team/runtime] operation failed: ${err}\n`);
|
|
3732
|
+
// best effort
|
|
3733
|
+
}
|
|
3734
|
+
if (!released.ok) {
|
|
3735
|
+
throw new Error(`${reason}:${released.error}`);
|
|
3736
|
+
}
|
|
3737
|
+
if (reason === 'worker_notify_failed')
|
|
3738
|
+
throw new Error('worker_notify_failed');
|
|
3739
|
+
throw new Error(`worker_assignment_failed:${reason}`);
|
|
2821
3740
|
}
|
|
2822
|
-
|
|
2823
|
-
throw new Error('worker_notify_failed');
|
|
2824
|
-
throw new Error(`worker_assignment_failed:${reason}`);
|
|
2825
|
-
}
|
|
3741
|
+
});
|
|
2826
3742
|
}
|
|
2827
3743
|
/**
|
|
2828
3744
|
* Reassign a task from one worker to another.
|
|
@@ -2857,58 +3773,139 @@ export async function shutdownTeam(teamName, cwd, options = {}) {
|
|
|
2857
3773
|
const confirmIssues = options.confirmIssues === true;
|
|
2858
3774
|
let skipWorkerAcks = false;
|
|
2859
3775
|
const sanitized = resolveTeamNameForCurrentContext(teamName, cwd);
|
|
2860
|
-
|
|
3776
|
+
let config = await readTeamConfig(sanitized, cwd);
|
|
2861
3777
|
if (!config) {
|
|
2862
|
-
|
|
2863
|
-
|
|
2864
|
-
|
|
2865
|
-
|
|
2866
|
-
catch (err) {
|
|
2867
|
-
process.stderr.write(`[team/runtime] operation failed: ${err}\n`);
|
|
2868
|
-
}
|
|
3778
|
+
await reconcileDetachedSessionDestroyReceipt(sanitized, cwd, null);
|
|
3779
|
+
await reconcileGonePaneDescendantCleanupDebt(sanitized, cwd);
|
|
3780
|
+
// A missing config is not authority over a conventionally named tmux
|
|
3781
|
+
// session. It may be another team's or a recycled user session.
|
|
2869
3782
|
await cleanupTeamState(sanitized, cwd);
|
|
2870
3783
|
await syncTeamModeStateOnShutdown(sanitized, cwd);
|
|
2871
3784
|
restoreTeamModelInstructionsFile(sanitized);
|
|
2872
3785
|
return { commitHygieneArtifacts: null };
|
|
2873
3786
|
}
|
|
3787
|
+
// Reconcile accepted detached-session destruction before any other shutdown
|
|
3788
|
+
// effect. A receipt is the only durable authority across the kill/query crash
|
|
3789
|
+
// window and is deliberately fail-closed on malformed or unavailable input.
|
|
3790
|
+
await reconcileDetachedSessionDestroyReceipt(sanitized, cwd, config);
|
|
3791
|
+
config = await readTeamConfig(sanitized, cwd);
|
|
3792
|
+
if (!config)
|
|
3793
|
+
throw new Error(`shutdown_config_missing_after_detached_reconciliation:${sanitized}`);
|
|
3794
|
+
config = await reconcileStartupCleanupPanes(config, cwd);
|
|
3795
|
+
const restoredHudDebtRoot = join(config.team_state_root ?? resolveCanonicalTeamStateRoot(cwd), 'team', sanitized);
|
|
3796
|
+
const configuredRestoredHudPaneId = typeof config.hud_pane_id === 'string' && /^%[0-9]+$/.test(config.hud_pane_id)
|
|
3797
|
+
? config.hud_pane_id
|
|
3798
|
+
: null;
|
|
3799
|
+
const configuredRestoredHudPanePid = typeof config.hud_pane_pid === 'number'
|
|
3800
|
+
&& Number.isSafeInteger(config.hud_pane_pid)
|
|
3801
|
+
&& config.hud_pane_pid > 0
|
|
3802
|
+
? config.hud_pane_pid
|
|
3803
|
+
: null;
|
|
3804
|
+
if (configuredRestoredHudPaneId && configuredRestoredHudPanePid) {
|
|
3805
|
+
try {
|
|
3806
|
+
// A restored-HUD debt is finalized only when this canonical config
|
|
3807
|
+
// transaction records its exact frozen pane identity. A stale config may
|
|
3808
|
+
// point to a different HUD after a crash, so replay the pinned debt
|
|
3809
|
+
// rather than treating that mismatch as successful finalization.
|
|
3810
|
+
finalizeRestoredHudCleanupDebtSync(cwd, configuredRestoredHudPaneId, configuredRestoredHudPanePid, restoredHudDebtRoot, true);
|
|
3811
|
+
}
|
|
3812
|
+
catch (error) {
|
|
3813
|
+
if (!(error instanceof Error) || !error.message.startsWith('restored_hud_cleanup_debt_unresolved:')) {
|
|
3814
|
+
throw error;
|
|
3815
|
+
}
|
|
3816
|
+
// The config identity is not the debt identity. Remove it durably before
|
|
3817
|
+
// replaying the pinned obligation: a later detached-session teardown must
|
|
3818
|
+
// not treat a live, same-PID/owner non-HUD pane as the restored HUD.
|
|
3819
|
+
config = await mutateShutdownConfig(config, cwd, (current) => {
|
|
3820
|
+
current.hud_pane_id = null;
|
|
3821
|
+
current.hud_pane_pid = undefined;
|
|
3822
|
+
});
|
|
3823
|
+
reconcileRestoredHudCleanupDebtSync(cwd, restoredHudDebtRoot);
|
|
3824
|
+
}
|
|
3825
|
+
}
|
|
3826
|
+
else {
|
|
3827
|
+
reconcileRestoredHudCleanupDebtSync(cwd, restoredHudDebtRoot);
|
|
3828
|
+
}
|
|
3829
|
+
const priorScaleDownCleanup = await reconcileScaleDownCleanupDebt(sanitized, cwd, config);
|
|
3830
|
+
if (!priorScaleDownCleanup.ok)
|
|
3831
|
+
throw new Error(priorScaleDownCleanup.error);
|
|
3832
|
+
await reconcileGonePaneDescendantCleanupDebt(sanitized, cwd, config);
|
|
2874
3833
|
const manifest = await readTeamManifestV2(sanitized, cwd);
|
|
2875
3834
|
const leaderSessionId = typeof manifest?.leader?.session_id === 'string'
|
|
2876
3835
|
? manifest.leader.session_id.trim()
|
|
2877
3836
|
: '';
|
|
2878
3837
|
const governance = resolveGovernancePolicy(manifest?.governance, manifest?.policy);
|
|
2879
|
-
|
|
2880
|
-
const
|
|
3838
|
+
const classification = await withTeamTaskMembershipBarrier(sanitized, cwd, async () => {
|
|
3839
|
+
const current = await classifyShutdown({
|
|
2881
3840
|
teamName: sanitized,
|
|
2882
3841
|
cwd,
|
|
2883
|
-
config,
|
|
3842
|
+
config: config,
|
|
2884
3843
|
governance,
|
|
2885
3844
|
confirmIssues,
|
|
2886
3845
|
});
|
|
2887
|
-
const { gate,
|
|
2888
|
-
|
|
2889
|
-
type: 'shutdown_gate',
|
|
2890
|
-
worker: 'leader-fixed',
|
|
2891
|
-
reason: `allowed=${gate.allowed} total=${gate.total} pending=${gate.pending} blocked=${gate.blocked} in_progress=${gate.in_progress} completed=${gate.completed} failed=${gate.failed} cleanup_requires_all_workers_inactive=${governance.cleanup_requires_all_workers_inactive} dirty_workers=${dirtyWorkers.join('|') || 'none'} confirm_issues=${confirmIssues} clean_fast_path=${useCleanFastPath}`,
|
|
2892
|
-
}, cwd).catch(() => { });
|
|
2893
|
-
if (!gate.allowed) {
|
|
3846
|
+
const { gate, requiresIssueConfirmation } = current;
|
|
3847
|
+
if (!force && !gate.allowed) {
|
|
2894
3848
|
if (requiresIssueConfirmation) {
|
|
2895
3849
|
throw new Error(`shutdown_confirm_issues_required:failed=${gate.failed}:rerun=omx team shutdown ${sanitized} --confirm-issues`);
|
|
2896
3850
|
}
|
|
2897
3851
|
throw new Error(`shutdown_gate_blocked:pending=${gate.pending},blocked=${gate.blocked},in_progress=${gate.in_progress},failed=${gate.failed}`);
|
|
2898
3852
|
}
|
|
2899
|
-
|
|
2900
|
-
|
|
3853
|
+
const now = new Date().toISOString();
|
|
3854
|
+
const existingPhase = await readTeamPhaseState(sanitized, cwd);
|
|
3855
|
+
const terminalReason = force ? 'forced_shutdown' : 'shutdown_gate_passed';
|
|
3856
|
+
await writeTeamPhaseState(sanitized, {
|
|
3857
|
+
current_phase: existingPhase?.current_phase ?? 'team-exec',
|
|
3858
|
+
max_fix_attempts: existingPhase?.max_fix_attempts ?? 3,
|
|
3859
|
+
current_fix_attempt: existingPhase?.current_fix_attempt ?? 0,
|
|
3860
|
+
transitions: existingPhase?.transitions ?? [],
|
|
3861
|
+
updated_at: now,
|
|
3862
|
+
terminal_epoch: existingPhase?.terminal_epoch ?? now,
|
|
3863
|
+
terminal_reason: existingPhase?.terminal_reason ?? terminalReason,
|
|
3864
|
+
final_task_counts: {
|
|
3865
|
+
total: gate.total,
|
|
3866
|
+
pending: gate.pending,
|
|
3867
|
+
blocked: gate.blocked,
|
|
3868
|
+
in_progress: gate.in_progress,
|
|
3869
|
+
completed: gate.completed,
|
|
3870
|
+
failed: gate.failed,
|
|
3871
|
+
},
|
|
3872
|
+
}, cwd);
|
|
3873
|
+
return current;
|
|
3874
|
+
});
|
|
3875
|
+
const terminalPhase = await readTeamPhaseState(sanitized, cwd);
|
|
3876
|
+
if (terminalPhase && terminalEpochStartedHookForTest) {
|
|
3877
|
+
await terminalEpochStartedHookForTest(sanitized, cwd, terminalPhase);
|
|
3878
|
+
}
|
|
3879
|
+
const { gate, dirtyWorkers, useCleanFastPath } = classification;
|
|
3880
|
+
await appendTeamEvent(sanitized, {
|
|
3881
|
+
type: force ? 'shutdown_gate_forced' : 'shutdown_gate',
|
|
3882
|
+
worker: 'leader-fixed',
|
|
3883
|
+
reason: force
|
|
3884
|
+
? `force_bypass total=${gate.total} pending=${gate.pending} blocked=${gate.blocked} in_progress=${gate.in_progress} completed=${gate.completed} failed=${gate.failed}`
|
|
3885
|
+
: `allowed=${gate.allowed} total=${gate.total} pending=${gate.pending} blocked=${gate.blocked} in_progress=${gate.in_progress} completed=${gate.completed} failed=${gate.failed} cleanup_requires_all_workers_inactive=${governance.cleanup_requires_all_workers_inactive} dirty_workers=${dirtyWorkers.join('|') || 'none'} confirm_issues=${confirmIssues} clean_fast_path=${useCleanFastPath}`,
|
|
3886
|
+
}, cwd).catch(() => { });
|
|
2901
3887
|
if (force) {
|
|
2902
|
-
await appendTeamEvent(sanitized, {
|
|
2903
|
-
type: 'shutdown_gate_forced',
|
|
2904
|
-
worker: 'leader-fixed',
|
|
2905
|
-
reason: 'force_bypass',
|
|
2906
|
-
}, cwd).catch(() => { });
|
|
2907
3888
|
// Explicit force means teardown now. Do not spend the graceful ack window
|
|
2908
3889
|
// waiting for interactive panes or prompt workers that will be killed below.
|
|
2909
3890
|
skipWorkerAcks = true;
|
|
2910
3891
|
}
|
|
3892
|
+
else {
|
|
3893
|
+
skipWorkerAcks = useCleanFastPath;
|
|
3894
|
+
}
|
|
2911
3895
|
const sessionName = config.tmux_session;
|
|
3896
|
+
const sharedSessionTopology = config.worker_launch_mode === 'interactive' && sessionName.includes(':')
|
|
3897
|
+
? resolveSharedSessionShutdownTopology(sessionName, config.leader_pane_id, sanitized)
|
|
3898
|
+
: null;
|
|
3899
|
+
if (sharedSessionTopology?.status === 'unavailable') {
|
|
3900
|
+
throw new Error(`shutdown_shared_session_topology_unavailable:${sharedSessionTopology.detail}`);
|
|
3901
|
+
}
|
|
3902
|
+
if (typeof config.hud_pane_id === 'string' && /^%[0-9]+$/.test(config.hud_pane_id)
|
|
3903
|
+
&& typeof config.hud_pane_pid === 'number' && Number.isSafeInteger(config.hud_pane_pid) && config.hud_pane_pid > 0) {
|
|
3904
|
+
const persistedHudProof = readExactPaneProofSync(config.hud_pane_id);
|
|
3905
|
+
if (persistedHudProof.status === 'live' && persistedHudProof.pid !== config.hud_pane_pid) {
|
|
3906
|
+
throw new Error(`shutdown_shared_session_HUD_pane_identity_changed:${config.hud_pane_id}`);
|
|
3907
|
+
}
|
|
3908
|
+
}
|
|
2912
3909
|
const dispatchPolicy = resolveDispatchPolicy(manifest?.policy, config.worker_launch_mode);
|
|
2913
3910
|
const shutdownRequestTimes = new Map();
|
|
2914
3911
|
if (!skipWorkerAcks) {
|
|
@@ -2964,7 +3961,7 @@ export async function shutdownTeam(teamName, cwd, options = {}) {
|
|
|
2964
3961
|
}
|
|
2965
3962
|
const anyAlive = config.workers.some((w) => (config.worker_launch_mode === 'prompt'
|
|
2966
3963
|
? isPromptWorkerAlive(config, w)
|
|
2967
|
-
: isWorkerAlive(sessionName, w.index, w.pane_id)));
|
|
3964
|
+
: isWorkerAlive(sessionName, w.index, w.pane_id, w.pid, config.tmux_pane_owner_id ?? undefined, config.hud_pane_id ?? undefined)));
|
|
2968
3965
|
if (!anyAlive)
|
|
2969
3966
|
break;
|
|
2970
3967
|
// Sleep 2s
|
|
@@ -2972,7 +3969,7 @@ export async function shutdownTeam(teamName, cwd, options = {}) {
|
|
|
2972
3969
|
}
|
|
2973
3970
|
const anyAliveAfterWait = config.workers.some((w) => (config.worker_launch_mode === 'prompt'
|
|
2974
3971
|
? isPromptWorkerAlive(config, w)
|
|
2975
|
-
: isWorkerAlive(sessionName, w.index, w.pane_id)));
|
|
3972
|
+
: isWorkerAlive(sessionName, w.index, w.pane_id, w.pid, config.tmux_pane_owner_id ?? undefined, config.hud_pane_id ?? undefined)));
|
|
2976
3973
|
if (anyAliveAfterWait && !force) {
|
|
2977
3974
|
// Workers may have accepted shutdown but not exited (Codex TUI requires explicit exit).
|
|
2978
3975
|
// In this case, proceed to force kill panes (next step) rather than failing and leaving state around.
|
|
@@ -2981,15 +3978,13 @@ export async function shutdownTeam(teamName, cwd, options = {}) {
|
|
|
2981
3978
|
// 3. Force kill remaining workers
|
|
2982
3979
|
const leaderPaneId = config.leader_pane_id;
|
|
2983
3980
|
const hudPaneId = config.hud_pane_id;
|
|
3981
|
+
const leaderPanePid = config.leader_pane_pid;
|
|
3982
|
+
const hudPanePid = config.hud_pane_pid;
|
|
2984
3983
|
if (config.worker_launch_mode === 'interactive') {
|
|
2985
|
-
const
|
|
2986
|
-
?
|
|
2987
|
-
:
|
|
2988
|
-
const effectiveLeaderPaneId = sharedSessionTopology ? sharedSessionTopology.leaderPaneId : leaderPaneId;
|
|
3984
|
+
const effectiveLeaderPaneId = sharedSessionTopology?.status === 'available'
|
|
3985
|
+
? sharedSessionTopology.leaderPaneId
|
|
3986
|
+
: leaderPaneId;
|
|
2989
3987
|
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
3988
|
const ownerReadWarnings = new Set();
|
|
2994
3989
|
const warnOwnerReadError = (kind, paneId, error) => {
|
|
2995
3990
|
const key = `${kind}:${paneId}:${error}`;
|
|
@@ -3005,57 +4000,253 @@ export async function shutdownTeam(teamName, cwd, options = {}) {
|
|
|
3005
4000
|
legacyInstanceId: leaderSessionId,
|
|
3006
4001
|
}) ? effectiveLeaderPaneId : null)
|
|
3007
4002
|
: effectiveLeaderPaneId;
|
|
3008
|
-
const
|
|
3009
|
-
?
|
|
4003
|
+
const reclaimableHudPaneIds = sharedSessionTopology
|
|
4004
|
+
? sharedSessionTopology.hudPaneIds.filter((paneId) => isSharedSessionHudPaneReclaimable({
|
|
3010
4005
|
paneId,
|
|
3011
4006
|
persistedHudPaneId: hudPaneId,
|
|
3012
4007
|
leaderOwnedHudPaneIds: sharedSessionTopology.leaderOwnedHudPaneIds,
|
|
3013
4008
|
teamPaneOwnerId: tmuxPaneOwnerId,
|
|
3014
|
-
onOwnerReadError: (
|
|
3015
|
-
|
|
3016
|
-
|
|
3017
|
-
|
|
3018
|
-
|
|
3019
|
-
|
|
3020
|
-
|
|
3021
|
-
|
|
3022
|
-
|
|
3023
|
-
|
|
3024
|
-
|
|
3025
|
-
|
|
3026
|
-
|
|
3027
|
-
|
|
3028
|
-
|
|
3029
|
-
|
|
3030
|
-
|
|
3031
|
-
|
|
3032
|
-
|
|
4009
|
+
onOwnerReadError: (candidateHudPaneId, error) => {
|
|
4010
|
+
warnOwnerReadError('HUD pane', candidateHudPaneId, error);
|
|
4011
|
+
},
|
|
4012
|
+
}))
|
|
4013
|
+
: (hudPaneId ? [hudPaneId] : []);
|
|
4014
|
+
const effectiveHudPaneId = reclaimableHudPaneIds[0] ?? null;
|
|
4015
|
+
const explicitPersistedWorkerPaneIds = new Set(config.workers
|
|
4016
|
+
.map((worker) => (typeof worker.pane_id === 'string' ? worker.pane_id.trim() : ''))
|
|
4017
|
+
.filter((paneId) => /^%[0-9]+$/.test(paneId))
|
|
4018
|
+
.filter((paneId) => paneId !== leaderPaneId && paneId !== hudPaneId));
|
|
4019
|
+
// Every persisted worker pane is a canonical shutdown member. Command markers
|
|
4020
|
+
// only discover additional candidates; they cannot erase canonical membership.
|
|
4021
|
+
const canonicalExplicitWorkerPaneIds = new Set(explicitPersistedWorkerPaneIds);
|
|
4022
|
+
const initiallyTaggedWorkerPaneIds = new Set();
|
|
4023
|
+
const authorizedDiscoveredWorkerPaneIds = sharedSessionTopology
|
|
4024
|
+
? collectAuthorizedSharedSessionWorkerPaneIds([...new Set([
|
|
4025
|
+
...canonicalExplicitWorkerPaneIds,
|
|
4026
|
+
...sharedSessionTopology.teamWorkerPaneIds,
|
|
4027
|
+
])], tmuxPaneOwnerId, canonicalExplicitWorkerPaneIds, undefined, initiallyTaggedWorkerPaneIds)
|
|
4028
|
+
: (sessionName ? listPaneIds(sessionName) : []);
|
|
4029
|
+
const excludedSharedWorkerPaneIds = new Set([effectiveLeaderPaneId, effectiveHudPaneId]
|
|
4030
|
+
.filter((paneId) => typeof paneId === 'string' && /^%[0-9]+$/.test(paneId)));
|
|
4031
|
+
const sharedWorkerPaneIds = [...new Set(authorizedDiscoveredWorkerPaneIds)];
|
|
4032
|
+
if (sharedSessionTopology && sharedWorkerPaneIds.some((paneId) => excludedSharedWorkerPaneIds.has(paneId))) {
|
|
4033
|
+
throw new Error(`shutdown_shared_session_worker_target_invalid:${sharedWorkerPaneIds.filter((paneId) => excludedSharedWorkerPaneIds.has(paneId)).join(',')}`);
|
|
4034
|
+
}
|
|
4035
|
+
// Freeze this union before any shared-session topology effect. HUD teardown
|
|
4036
|
+
// and restoration must never cause later worker target rediscovery.
|
|
4037
|
+
const shutdownPaneIds = sharedSessionTopology
|
|
4038
|
+
? sharedWorkerPaneIds
|
|
4039
|
+
: collectShutdownPaneIds({
|
|
4040
|
+
config,
|
|
4041
|
+
candidatePaneIds: authorizedDiscoveredWorkerPaneIds,
|
|
4042
|
+
leaderPaneId: effectiveLeaderPaneId,
|
|
4043
|
+
hudPaneId: effectiveHudPaneId,
|
|
4044
|
+
});
|
|
4045
|
+
const canonicalWorkerPaneIds = [...shutdownPaneIds];
|
|
4046
|
+
const expectedSharedWorkerPanePids = new Map();
|
|
4047
|
+
const prekillResolvedWorkerPaneIds = new Set();
|
|
4048
|
+
const freezeSharedPaneAuthorization = (paneId, kind, expectedPid) => {
|
|
4049
|
+
const proof = readExactPaneProofSync(paneId);
|
|
4050
|
+
if (proof.status !== 'live') {
|
|
4051
|
+
throw new Error(`shutdown_shared_session_${kind.replaceAll(' ', '_')}_proof_unavailable:${paneId}`);
|
|
4052
|
+
}
|
|
4053
|
+
if (typeof expectedPid === 'number') {
|
|
4054
|
+
if (!Number.isSafeInteger(expectedPid) || expectedPid <= 0) {
|
|
4055
|
+
throw new Error(`shutdown_shared_session_${kind.replaceAll(' ', '_')}_pid_missing:${paneId}`);
|
|
4056
|
+
}
|
|
4057
|
+
if (proof.pid !== expectedPid) {
|
|
4058
|
+
throw new Error(`shutdown_shared_session_${kind.replaceAll(' ', '_')}_identity_changed:${paneId}`);
|
|
4059
|
+
}
|
|
4060
|
+
}
|
|
4061
|
+
const owner = readPaneTeamOwnerTagResult(paneId);
|
|
4062
|
+
if (owner.status === 'error') {
|
|
4063
|
+
throw new Error(`shutdown_shared_session_${kind.replaceAll(' ', '_')}_owner_unavailable:${paneId}:${owner.error}`);
|
|
4064
|
+
}
|
|
4065
|
+
if (owner.status !== 'value' || !tmuxPaneOwnerId || owner.value !== tmuxPaneOwnerId) {
|
|
4066
|
+
throw new Error(`shutdown_shared_session_${kind.replaceAll(' ', '_')}_owner_changed:${paneId}`);
|
|
4067
|
+
}
|
|
4068
|
+
return { paneId, pid: proof.pid, owner: owner.value };
|
|
4069
|
+
};
|
|
4070
|
+
const assertSharedPaneAuthorizationContinuous = (authorization, kind) => {
|
|
4071
|
+
const scope = sharedSessionTopology ? 'shared_session' : 'detached_session';
|
|
4072
|
+
const proof = readExactPaneProofSync(authorization.paneId);
|
|
4073
|
+
if (proof.status !== 'live' || proof.pid !== authorization.pid) {
|
|
4074
|
+
throw new Error(`shutdown_${scope}_${kind.replaceAll(' ', '_')}_identity_changed:${authorization.paneId}`);
|
|
4075
|
+
}
|
|
4076
|
+
const owner = readPaneTeamOwnerTagResult(authorization.paneId);
|
|
4077
|
+
if (owner.status === 'error') {
|
|
4078
|
+
throw new Error(`shutdown_${scope}_${kind.replaceAll(' ', '_')}_owner_unavailable:${authorization.paneId}:${owner.error}`);
|
|
4079
|
+
}
|
|
4080
|
+
const currentOwner = owner.status === 'value' ? owner.value : null;
|
|
4081
|
+
if (currentOwner !== authorization.owner) {
|
|
4082
|
+
throw new Error(`shutdown_${scope}_${kind.replaceAll(' ', '_')}_owner_changed:${authorization.paneId}`);
|
|
3033
4083
|
}
|
|
4084
|
+
};
|
|
4085
|
+
const persistedHudPaneId = typeof hudPaneId === 'string' && /^%[0-9]+$/.test(hudPaneId)
|
|
4086
|
+
? hudPaneId
|
|
4087
|
+
: null;
|
|
4088
|
+
const persistedLeaderPaneId = typeof leaderPaneId === 'string' && /^%[0-9]+$/.test(leaderPaneId)
|
|
4089
|
+
? leaderPaneId
|
|
4090
|
+
: null;
|
|
4091
|
+
if (persistedHudPaneId && effectiveHudPaneId === persistedHudPaneId && (typeof hudPanePid !== 'number' || !Number.isSafeInteger(hudPanePid) || hudPanePid <= 0)) {
|
|
4092
|
+
const scope = sharedSessionTopology ? 'shared_session' : 'detached_session';
|
|
4093
|
+
throw new Error(`shutdown_${scope}_HUD_pane_pid_missing:${effectiveHudPaneId}`);
|
|
3034
4094
|
}
|
|
3035
|
-
|
|
3036
|
-
|
|
3037
|
-
|
|
3038
|
-
|
|
3039
|
-
|
|
3040
|
-
|
|
3041
|
-
|
|
3042
|
-
|
|
3043
|
-
|
|
4095
|
+
const frozenHudAuthorization = effectiveHudPaneId
|
|
4096
|
+
? (sharedSessionTopology
|
|
4097
|
+
? freezeSharedPaneAuthorization(effectiveHudPaneId, 'HUD pane', effectiveHudPaneId === persistedHudPaneId ? hudPanePid : undefined)
|
|
4098
|
+
: (() => {
|
|
4099
|
+
if (!persistedHudPaneId || effectiveHudPaneId !== persistedHudPaneId || !tmuxPaneOwnerId
|
|
4100
|
+
|| typeof hudPanePid !== 'number' || !Number.isSafeInteger(hudPanePid) || hudPanePid <= 0) {
|
|
4101
|
+
throw new Error(`shutdown_detached_session_HUD_pane_authorization_unavailable:${effectiveHudPaneId}`);
|
|
4102
|
+
}
|
|
4103
|
+
const proof = readExactPaneProofSync(effectiveHudPaneId);
|
|
4104
|
+
if (proof.status !== 'live' || proof.pid !== hudPanePid) {
|
|
4105
|
+
throw new Error(`shutdown_detached_session_HUD_pane_identity_changed:${effectiveHudPaneId}`);
|
|
4106
|
+
}
|
|
4107
|
+
const owner = readPaneTeamOwnerTagResult(proof.paneId);
|
|
4108
|
+
if (owner.status !== 'value' || owner.value !== tmuxPaneOwnerId) {
|
|
4109
|
+
throw new Error(`shutdown_detached_session_HUD_pane_owner_changed:${effectiveHudPaneId}`);
|
|
4110
|
+
}
|
|
4111
|
+
const finalProof = readExactPaneProofSync(proof.paneId);
|
|
4112
|
+
if (finalProof.status !== 'live' || finalProof.pid !== hudPanePid) {
|
|
4113
|
+
throw new Error(`shutdown_detached_session_HUD_pane_identity_changed:${effectiveHudPaneId}`);
|
|
4114
|
+
}
|
|
4115
|
+
return { paneId: finalProof.paneId, pid: hudPanePid, owner: tmuxPaneOwnerId };
|
|
4116
|
+
})())
|
|
4117
|
+
: null;
|
|
4118
|
+
if (persistedLeaderPaneId && trustedHudRestoreLeaderPaneId === persistedLeaderPaneId && (typeof leaderPanePid !== 'number' || !Number.isSafeInteger(leaderPanePid) || leaderPanePid <= 0)) {
|
|
4119
|
+
throw new Error(`shutdown_shared_session_restore_leader_pane_pid_missing:${trustedHudRestoreLeaderPaneId}`);
|
|
4120
|
+
}
|
|
4121
|
+
const frozenRestoreLeaderAuthorization = sharedSessionTopology && trustedHudRestoreLeaderPaneId
|
|
4122
|
+
? freezeSharedPaneAuthorization(trustedHudRestoreLeaderPaneId, 'restore leader pane', trustedHudRestoreLeaderPaneId === persistedLeaderPaneId ? leaderPanePid : undefined)
|
|
4123
|
+
: null;
|
|
4124
|
+
const assertCompleteSharedWorkerPaneProofs = (allowResolvedGone = false) => {
|
|
4125
|
+
const unavailable = readExactPaneProofsSync(canonicalWorkerPaneIds).flatMap((proof) => {
|
|
4126
|
+
if (proof.status !== 'live') {
|
|
4127
|
+
if (allowResolvedGone && proof.status === 'gone' && prekillResolvedWorkerPaneIds.has(proof.paneId))
|
|
4128
|
+
return [];
|
|
4129
|
+
return proof.status === 'unavailable'
|
|
4130
|
+
? [proof]
|
|
4131
|
+
: [{ status: 'unavailable', paneId: proof.paneId, reason: 'pane_proof_lost_during_process_teardown' }];
|
|
4132
|
+
}
|
|
4133
|
+
const persistedPid = config.workers.find((worker) => worker.pane_id?.trim() === proof.paneId)?.pid;
|
|
4134
|
+
if (canonicalExplicitWorkerPaneIds.has(proof.paneId)
|
|
4135
|
+
&& (typeof persistedPid !== 'number' || !Number.isSafeInteger(persistedPid) || persistedPid <= 0)) {
|
|
4136
|
+
return [{ status: 'unavailable', paneId: proof.paneId, reason: 'pane_pid_changed', detail: 'canonical persisted pane PID is missing' }];
|
|
3044
4137
|
}
|
|
4138
|
+
const expectedPid = typeof persistedPid === 'number' ? persistedPid : expectedSharedWorkerPanePids.get(proof.paneId);
|
|
4139
|
+
if (typeof expectedPid === 'number' && proof.pid !== expectedPid) {
|
|
4140
|
+
return [{ status: 'unavailable', paneId: proof.paneId, reason: 'pane_pid_changed', detail: `expected ${expectedPid}, got ${proof.pid}` }];
|
|
4141
|
+
}
|
|
4142
|
+
expectedSharedWorkerPanePids.set(proof.paneId, proof.pid);
|
|
4143
|
+
return [];
|
|
4144
|
+
});
|
|
4145
|
+
assertPaneTeardownProofsAvailable('shutdown', unavailable);
|
|
4146
|
+
};
|
|
4147
|
+
const frozenSharedWorkerOwnerIds = new Map();
|
|
4148
|
+
if (sharedSessionTopology) {
|
|
4149
|
+
assertCompleteSharedWorkerPaneProofs();
|
|
4150
|
+
for (const paneId of canonicalWorkerPaneIds) {
|
|
4151
|
+
const owner = readPaneTeamOwnerTagResult(paneId);
|
|
4152
|
+
if (owner.status === 'error') {
|
|
4153
|
+
throw new Error(`shutdown_shared_session_worker_owner_unavailable:${paneId}:${owner.error}`);
|
|
4154
|
+
}
|
|
4155
|
+
if (owner.status !== 'value' || !tmuxPaneOwnerId || owner.value !== tmuxPaneOwnerId) {
|
|
4156
|
+
throw new Error(`shutdown_shared_session_worker_owner_changed:${paneId}`);
|
|
4157
|
+
}
|
|
4158
|
+
frozenSharedWorkerOwnerIds.set(paneId, owner.value);
|
|
3045
4159
|
}
|
|
3046
4160
|
}
|
|
3047
|
-
|
|
3048
|
-
|
|
3049
|
-
|
|
3050
|
-
|
|
3051
|
-
|
|
4161
|
+
const authorizeFrozenSharedWorkerProcessSignal = (paneId, panePid) => {
|
|
4162
|
+
const expectedPid = expectedSharedWorkerPanePids.get(paneId)
|
|
4163
|
+
?? config.workers.find((worker) => worker.pane_id === paneId)?.pid;
|
|
4164
|
+
if (expectedPid !== panePid)
|
|
4165
|
+
return false;
|
|
4166
|
+
if (sharedSessionTopology && frozenSharedWorkerOwnerIds.has(paneId)) {
|
|
4167
|
+
const expectedOwner = frozenSharedWorkerOwnerIds.get(paneId);
|
|
4168
|
+
const owner = readPaneTeamOwnerTagResult(paneId);
|
|
4169
|
+
return owner.status === 'value' && owner.value === expectedOwner && expectedOwner === tmuxPaneOwnerId;
|
|
4170
|
+
}
|
|
4171
|
+
const owner = readPaneTeamOwnerTagResult(paneId);
|
|
4172
|
+
return owner.status === 'value' && owner.value === tmuxPaneOwnerId;
|
|
4173
|
+
};
|
|
4174
|
+
if (shouldPrekillInteractiveShutdownProcessTrees(sessionName)) {
|
|
4175
|
+
const paneProcessProofUnavailable = [];
|
|
4176
|
+
for (const paneId of shutdownPaneIds) {
|
|
4177
|
+
const teardown = await terminateExactPaneProcessTree(paneId, config.workers.find((worker) => worker.pane_id === paneId)?.pid ?? expectedSharedWorkerPanePids.get(paneId), undefined, undefined, authorizeFrozenSharedWorkerProcessSignal);
|
|
4178
|
+
if (teardown.proofUnavailable) {
|
|
4179
|
+
await persistGonePaneDescendantCleanupDebt({ teamName: sanitized, cwd, config, paneId, teardown });
|
|
4180
|
+
paneProcessProofUnavailable.push(teardown.proofUnavailable);
|
|
4181
|
+
break;
|
|
4182
|
+
}
|
|
4183
|
+
if (!teardown.terminated && teardown.stopped) {
|
|
4184
|
+
await persistGonePaneDescendantCleanupDebt({ teamName: sanitized, cwd, config, paneId, teardown });
|
|
4185
|
+
paneProcessProofUnavailable.push({
|
|
4186
|
+
status: 'unavailable',
|
|
4187
|
+
paneId,
|
|
4188
|
+
reason: 'pane_proof_lost_during_process_teardown',
|
|
4189
|
+
detail: 'exact pane authority was lost before the tracked process tree was resolved',
|
|
4190
|
+
});
|
|
4191
|
+
break;
|
|
4192
|
+
}
|
|
4193
|
+
if (teardown.terminated && readExactPaneProofSync(paneId).status === 'gone') {
|
|
4194
|
+
prekillResolvedWorkerPaneIds.add(paneId);
|
|
4195
|
+
}
|
|
4196
|
+
}
|
|
4197
|
+
if (paneProcessProofUnavailable.length > 0) {
|
|
4198
|
+
assertPaneTeardownProofsAvailable('shutdown', paneProcessProofUnavailable);
|
|
4199
|
+
}
|
|
4200
|
+
}
|
|
4201
|
+
// Revalidate ownership of the frozen discovered members, then re-prove the
|
|
4202
|
+
// frozen complete worker set in one snapshot immediately before the first
|
|
4203
|
+
// HUD topology effect. This never admits a newly discovered pane.
|
|
4204
|
+
if (sharedSessionTopology) {
|
|
4205
|
+
const reauthorizedDiscoveredWorkerPaneIds = collectAuthorizedSharedSessionWorkerPaneIds(authorizedDiscoveredWorkerPaneIds, tmuxPaneOwnerId, canonicalExplicitWorkerPaneIds, initiallyTaggedWorkerPaneIds);
|
|
4206
|
+
if (reauthorizedDiscoveredWorkerPaneIds.length !== authorizedDiscoveredWorkerPaneIds.length
|
|
4207
|
+
|| reauthorizedDiscoveredWorkerPaneIds.some((paneId) => !authorizedDiscoveredWorkerPaneIds.includes(paneId))) {
|
|
4208
|
+
throw new Error('shutdown_shared_session_worker_owner_changed');
|
|
4209
|
+
}
|
|
4210
|
+
assertCompleteSharedWorkerPaneProofs(true);
|
|
3052
4211
|
}
|
|
3053
4212
|
let restoredHudPaneId = null;
|
|
3054
4213
|
if (effectiveHudPaneId) {
|
|
3055
|
-
|
|
4214
|
+
// Confirm restoration authority before removing the shared HUD, then
|
|
4215
|
+
// check it again immediately before the split below.
|
|
4216
|
+
if (sessionName.includes(':') && frozenRestoreLeaderAuthorization) {
|
|
4217
|
+
assertSharedPaneAuthorizationContinuous(frozenRestoreLeaderAuthorization, 'restore leader pane');
|
|
4218
|
+
}
|
|
4219
|
+
if (frozenHudAuthorization) {
|
|
4220
|
+
assertSharedPaneAuthorizationContinuous(frozenHudAuthorization, 'HUD pane');
|
|
4221
|
+
}
|
|
4222
|
+
const hudTeardown = await teardownWorkerPanes([effectiveHudPaneId], {
|
|
4223
|
+
leaderPaneId: effectiveLeaderPaneId,
|
|
4224
|
+
expectedPanePids: frozenHudAuthorization
|
|
4225
|
+
? { [frozenHudAuthorization.paneId]: frozenHudAuthorization.pid }
|
|
4226
|
+
: undefined,
|
|
4227
|
+
authorizePaneKill: frozenHudAuthorization
|
|
4228
|
+
? () => {
|
|
4229
|
+
assertSharedPaneAuthorizationContinuous(frozenHudAuthorization, 'HUD pane');
|
|
4230
|
+
return true;
|
|
4231
|
+
}
|
|
4232
|
+
: undefined,
|
|
4233
|
+
});
|
|
4234
|
+
assertPaneTeardownProofsAvailable('shutdown', hudTeardown.proofUnavailable);
|
|
4235
|
+
if (hudTeardown.kill.failed > 0) {
|
|
4236
|
+
throw new Error(`shutdown_pane_teardown_failed:${hudTeardown.kill.failedPaneIds.join(',')}`);
|
|
4237
|
+
}
|
|
3056
4238
|
if (sessionName.includes(':')) {
|
|
4239
|
+
if (frozenRestoreLeaderAuthorization) {
|
|
4240
|
+
assertSharedPaneAuthorizationContinuous(frozenRestoreLeaderAuthorization, 'restore leader pane');
|
|
4241
|
+
}
|
|
3057
4242
|
restoredHudPaneId = restoreStandaloneHudPane(trustedHudRestoreLeaderPaneId, cwd, {
|
|
3058
4243
|
sessionId: leaderSessionId,
|
|
4244
|
+
expectedLeaderPanePid: frozenRestoreLeaderAuthorization?.pid,
|
|
4245
|
+
expectedLeaderPaneOwnerId: tmuxPaneOwnerId,
|
|
4246
|
+
stateRoot: join(config.team_state_root ?? resolveCanonicalTeamStateRoot(cwd), 'team', sanitized),
|
|
4247
|
+
assertLeaderPaneAuthorization: frozenRestoreLeaderAuthorization
|
|
4248
|
+
? () => assertSharedPaneAuthorizationContinuous(frozenRestoreLeaderAuthorization, 'restore leader pane')
|
|
4249
|
+
: undefined,
|
|
3059
4250
|
});
|
|
3060
4251
|
if (!restoredHudPaneId) {
|
|
3061
4252
|
const reason = trustedHudRestoreLeaderPaneId
|
|
@@ -3064,29 +4255,65 @@ export async function shutdownTeam(teamName, cwd, options = {}) {
|
|
|
3064
4255
|
console.warn(`[team shutdown] ${sanitized}: ${reason}`);
|
|
3065
4256
|
}
|
|
3066
4257
|
}
|
|
4258
|
+
if (restoredHudPaneId) {
|
|
4259
|
+
const restoredHudProof = readExactPaneProofSync(restoredHudPaneId);
|
|
4260
|
+
if (restoredHudProof.status !== 'live') {
|
|
4261
|
+
throw new Error(`shutdown_restored_hud_pane_proof_unavailable:${restoredHudPaneId}`);
|
|
4262
|
+
}
|
|
4263
|
+
config = await mutateShutdownConfig(config, cwd, (current) => {
|
|
4264
|
+
current.hud_pane_id = restoredHudPaneId;
|
|
4265
|
+
current.hud_pane_pid = restoredHudProof.pid;
|
|
4266
|
+
});
|
|
4267
|
+
finalizeRestoredHudCleanupDebtSync(cwd, restoredHudPaneId, restoredHudProof.pid, join(config.team_state_root ?? resolveCanonicalTeamStateRoot(cwd), 'team', sanitized), true);
|
|
4268
|
+
}
|
|
3067
4269
|
}
|
|
3068
|
-
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3076
|
-
|
|
3077
|
-
|
|
3078
|
-
|
|
4270
|
+
let resizeHookWarning = null;
|
|
4271
|
+
if (config.resize_hook_name && config.resize_hook_target) {
|
|
4272
|
+
const resizeHookName = config.resize_hook_name;
|
|
4273
|
+
const unregistered = unregisterResizeHook(config.resize_hook_target, resizeHookName);
|
|
4274
|
+
if (!unregistered && isTmuxAvailable()) {
|
|
4275
|
+
const baseSession = sessionName.split(':')[0];
|
|
4276
|
+
const teamSessions = listTeamSessions();
|
|
4277
|
+
if (teamSessions === null || teamSessions.includes(baseSession)) {
|
|
4278
|
+
resizeHookWarning = `failed to unregister resize hook ${resizeHookName}`;
|
|
4279
|
+
}
|
|
4280
|
+
}
|
|
4281
|
+
}
|
|
4282
|
+
if (resizeHookWarning) {
|
|
4283
|
+
console.warn(`[team shutdown] ${sanitized}: ${resizeHookWarning}; continuing teardown`);
|
|
4284
|
+
}
|
|
4285
|
+
const workerTeardown = await teardownWorkerPanes(shutdownPaneIds.filter((paneId) => !prekillResolvedWorkerPaneIds.has(paneId)), {
|
|
3079
4286
|
leaderPaneId: effectiveLeaderPaneId,
|
|
3080
4287
|
hudPaneId: restoredHudPaneId ?? effectiveHudPaneId,
|
|
4288
|
+
expectedPanePids: {
|
|
4289
|
+
...Object.fromEntries(expectedSharedWorkerPanePids),
|
|
4290
|
+
...Object.fromEntries(config.workers
|
|
4291
|
+
.filter((worker) => typeof worker.pane_id === 'string' && typeof worker.pid === 'number')
|
|
4292
|
+
.map((worker) => [worker.pane_id, worker.pid])),
|
|
4293
|
+
},
|
|
4294
|
+
authorizePaneKill: sharedSessionTopology
|
|
4295
|
+
? (paneId) => {
|
|
4296
|
+
const expectedOwner = frozenSharedWorkerOwnerIds.get(paneId);
|
|
4297
|
+
if (expectedOwner === undefined)
|
|
4298
|
+
return false;
|
|
4299
|
+
const currentOwner = readPaneTeamOwnerTagResult(paneId);
|
|
4300
|
+
return currentOwner.status === 'value'
|
|
4301
|
+
&& currentOwner.value === expectedOwner
|
|
4302
|
+
&& expectedOwner === tmuxPaneOwnerId;
|
|
4303
|
+
}
|
|
4304
|
+
: undefined,
|
|
3081
4305
|
});
|
|
3082
|
-
|
|
4306
|
+
assertPaneTeardownProofsAvailable('shutdown', workerTeardown.proofUnavailable);
|
|
4307
|
+
if (workerTeardown.kill.failed > 0) {
|
|
4308
|
+
throw new Error(`shutdown_pane_teardown_failed:${workerTeardown.kill.failedPaneIds.join(',')}`);
|
|
4309
|
+
}
|
|
4310
|
+
config = await mutateShutdownConfig(config, cwd, (current) => {
|
|
4311
|
+
current.resize_hook_name = null;
|
|
4312
|
+
current.resize_hook_target = null;
|
|
4313
|
+
});
|
|
4314
|
+
// 4. Destroy a detached session only after an authoritative leader proof.
|
|
3083
4315
|
if (!sessionName.includes(':')) {
|
|
3084
|
-
|
|
3085
|
-
destroyTeamSession(sessionName);
|
|
3086
|
-
}
|
|
3087
|
-
catch (err) {
|
|
3088
|
-
process.stderr.write(`[team/runtime] operation failed: ${err}\n`);
|
|
3089
|
-
}
|
|
4316
|
+
await destroyConfiguredDetachedTeamSession(sanitized, cwd, config);
|
|
3090
4317
|
}
|
|
3091
4318
|
}
|
|
3092
4319
|
else {
|
|
@@ -3229,7 +4456,7 @@ export async function resumeTeam(teamName, cwd) {
|
|
|
3229
4456
|
.filter((worker) => {
|
|
3230
4457
|
if (!Number.isFinite(worker.pid) || (worker.pid ?? 0) <= 0)
|
|
3231
4458
|
return false;
|
|
3232
|
-
return
|
|
4459
|
+
return probePidLiveness(worker.pid) !== 'gone';
|
|
3233
4460
|
})
|
|
3234
4461
|
.filter((worker) => !getPromptWorkerHandle(sanitized, worker.name));
|
|
3235
4462
|
if (missingHandles.length > 0) {
|
|
@@ -3261,7 +4488,10 @@ async function findActiveTeams(cwd, leaderSessionId) {
|
|
|
3261
4488
|
const root = teamRuntimeTeamsRoot(cwd);
|
|
3262
4489
|
if (!existsSync(root))
|
|
3263
4490
|
return [];
|
|
3264
|
-
const
|
|
4491
|
+
const listedSessions = listTeamSessions();
|
|
4492
|
+
if (listedSessions === null)
|
|
4493
|
+
throw new Error('tmux_session_query_unavailable');
|
|
4494
|
+
const sessions = new Set(listedSessions);
|
|
3265
4495
|
const entries = await readdir(root, { withFileTypes: true });
|
|
3266
4496
|
const active = [];
|
|
3267
4497
|
for (const e of entries) {
|
|
@@ -3297,7 +4527,10 @@ async function detectAndCleanStaleTeam(teamName, leaderCwd, workerCount, confirm
|
|
|
3297
4527
|
const stateDir = teamRuntimeTeamRoot(teamName, leaderCwd);
|
|
3298
4528
|
if (!existsSync(stateDir))
|
|
3299
4529
|
return;
|
|
3300
|
-
const
|
|
4530
|
+
const listedSessions = listTeamSessions();
|
|
4531
|
+
if (listedSessions === null)
|
|
4532
|
+
return;
|
|
4533
|
+
const sessions = new Set(listedSessions);
|
|
3301
4534
|
if (sessions.has(`omx-team-${teamName}`))
|
|
3302
4535
|
return;
|
|
3303
4536
|
const repoRootResult = spawnSync('git', ['rev-parse', '--show-toplevel'], {
|
|
@@ -3455,7 +4688,7 @@ async function notifyWorkerOutcome(config, workerIndex, message, workerPaneId) {
|
|
|
3455
4688
|
return { ok: false, transport: 'tmux_send_keys', reason: 'tmux_unavailable' };
|
|
3456
4689
|
}
|
|
3457
4690
|
try {
|
|
3458
|
-
await sendToWorker(config.tmux_session, workerIndex, message, workerPaneId, worker.worker_cli);
|
|
4691
|
+
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
4692
|
return { ok: true, transport: 'tmux_send_keys', reason: 'tmux_send_keys_sent' };
|
|
3460
4693
|
}
|
|
3461
4694
|
catch (error) {
|
|
@@ -3492,8 +4725,8 @@ async function markDispatchRequestLeaderPaneMissingDeferred(params) {
|
|
|
3492
4725
|
}, cwd).catch(() => { });
|
|
3493
4726
|
}
|
|
3494
4727
|
async function attemptStartupDirectTrigger(params) {
|
|
3495
|
-
const { teamName, config, workerName, workerIndex, paneId, workerCli, inbox, triggerMessage, intent, taskIds, cwd, timing, } = params;
|
|
3496
|
-
const safety = await evaluateStartupDirectTriggerSafety(config.tmux_session, workerIndex, paneId, workerCli);
|
|
4728
|
+
const { teamName, config, workerName, workerIndex, paneId, workerCli, finalArgs, inbox, triggerMessage, intent, taskIds, cwd, timing, } = params;
|
|
4729
|
+
const safety = await evaluateStartupDirectTriggerSafety(config.tmux_session, workerIndex, paneId, workerCli, finalArgs);
|
|
3497
4730
|
if (!safety.safe) {
|
|
3498
4731
|
timing.mark('startup_direct_bypass', {
|
|
3499
4732
|
worker: workerName,
|
|
@@ -3501,6 +4734,9 @@ async function attemptStartupDirectTrigger(params) {
|
|
|
3501
4734
|
ok: false,
|
|
3502
4735
|
reason: `startup_direct_unsafe:${safety.reason}`,
|
|
3503
4736
|
});
|
|
4737
|
+
if (safety.reason === 'codex_bypass_mdm_incompatible') {
|
|
4738
|
+
return { ok: false, transport: 'none', reason: safety.reason };
|
|
4739
|
+
}
|
|
3504
4740
|
return null;
|
|
3505
4741
|
}
|
|
3506
4742
|
const queued = await queueInboxInstruction({
|