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