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
|
@@ -1,16 +1,18 @@
|
|
|
1
1
|
import { spawnSync, execFile } from 'child_process';
|
|
2
2
|
import { promisify } from 'util';
|
|
3
|
-
import { chmodSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from 'fs';
|
|
3
|
+
import { closeSync, chmodSync, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'fs';
|
|
4
4
|
import { homedir } from 'os';
|
|
5
|
-
import { dirname, isAbsolute, join, resolve } from 'path';
|
|
5
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'path';
|
|
6
6
|
import { CODEX_BYPASS_FLAG, CLAUDE_SKIP_PERMISSIONS_FLAG, MADMAX_FLAG, CONFIG_FLAG, LONG_CONFIG_FLAG, MODEL_FLAG, } from '../cli/constants.js';
|
|
7
7
|
import { getAgent } from '../agents/definitions.js';
|
|
8
8
|
import { buildCapturePaneArgv as sharedBuildCapturePaneArgv, buildVisibleCapturePaneArgv as sharedBuildVisibleCapturePaneArgv, normalizeTmuxCapture as sharedNormalizeTmuxCapture, paneHasActiveTask as sharedPaneHasActiveTask, paneIsBootstrapping as sharedPaneIsBootstrapping, paneShowsCodexViewport as sharedPaneShowsCodexViewport, paneLooksReady as sharedPaneLooksReady, } from '../scripts/tmux-hook-engine.js';
|
|
9
9
|
import { readActiveProviderEnvOverrides } from '../config/models.js';
|
|
10
|
-
import { extractModelProviderOverrideValue } from './model-contract.js';
|
|
10
|
+
import { classifyTeamWorkerLaunchPolicy, extractModelProviderOverrideValue, normalizeTeamWorkerLaunchArgs, parseTeamWorkerLaunchArgs, } from './model-contract.js';
|
|
11
11
|
import { sleep, sleepSync } from '../utils/sleep.js';
|
|
12
12
|
import { buildPlatformCommandSpec, classifySpawnError, resolveCommandPathForPlatform, spawnPlatformCommandSync, } from '../utils/platform-command.js';
|
|
13
13
|
import { resolveOmxCliEntryPath } from '../utils/paths.js';
|
|
14
|
+
import { readExactPaneProof, readExactPaneProofsSync, readExactPaneProofSync } from './exact-pane.js';
|
|
15
|
+
import { resolveCanonicalTeamStateRoot } from './state-root.js';
|
|
14
16
|
const execFileAsync = promisify(execFile);
|
|
15
17
|
import { HUD_RESIZE_RECONCILE_DELAY_SECONDS, HUD_TMUX_TEAM_HEIGHT_LINES } from '../hud/constants.js';
|
|
16
18
|
import { OMX_TMUX_HUD_OWNER_ENV } from '../hud/reconcile.js';
|
|
@@ -18,6 +20,41 @@ import { findHudWatchPaneIds, hudPaneMatchesOwner, OMX_TMUX_HUD_LEADER_PANE_ENV
|
|
|
18
20
|
const OMX_INSTANCE_OPTION = '@omx_instance_id';
|
|
19
21
|
const OMX_PANE_INSTANCE_OPTION = '@omx_pane_instance_id';
|
|
20
22
|
const OMX_TEAM_PANE_OWNER_OPTION = '@omx_team_pane_owner_id';
|
|
23
|
+
/** `kill(pid, 0)` establishes absence only on ESRCH. */
|
|
24
|
+
function probeProcessLiveness(pid) {
|
|
25
|
+
try {
|
|
26
|
+
process.kill(pid, 0);
|
|
27
|
+
return 'alive';
|
|
28
|
+
}
|
|
29
|
+
catch (error) {
|
|
30
|
+
return error.code === 'ESRCH' ? 'gone' : 'unknown';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
/** Carries live tmux resources when creation cannot safely roll them back. */
|
|
34
|
+
export class CreateTeamSessionPartialError extends Error {
|
|
35
|
+
partialSession;
|
|
36
|
+
proofUnavailable;
|
|
37
|
+
originalError;
|
|
38
|
+
cleanupErrors;
|
|
39
|
+
constructor(partialSession, proofUnavailable, originalError,
|
|
40
|
+
/** Cleanup commands that failed after resources were created and must be retried. */
|
|
41
|
+
cleanupErrors = []) {
|
|
42
|
+
super('create_team_session_cleanup_incomplete');
|
|
43
|
+
this.partialSession = partialSession;
|
|
44
|
+
this.proofUnavailable = proofUnavailable;
|
|
45
|
+
this.originalError = originalError;
|
|
46
|
+
this.cleanupErrors = cleanupErrors;
|
|
47
|
+
this.name = 'CreateTeamSessionPartialError';
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
class ExactPaneProofUnavailableError extends Error {
|
|
51
|
+
proof;
|
|
52
|
+
constructor(proof) {
|
|
53
|
+
super(`exact_pane_proof_unavailable:${proof.paneId}:${proof.reason}`);
|
|
54
|
+
this.proof = proof;
|
|
55
|
+
this.name = 'ExactPaneProofUnavailableError';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
21
58
|
const INJECTION_MARKER = '[OMX_TMUX_INJECT]';
|
|
22
59
|
const MODEL_INSTRUCTIONS_FILE_KEY = 'model_instructions_file';
|
|
23
60
|
const OMX_BYPASS_DEFAULT_SYSTEM_PROMPT_ENV = 'OMX_BYPASS_DEFAULT_SYSTEM_PROMPT';
|
|
@@ -74,6 +111,206 @@ function runTmux(args) {
|
|
|
74
111
|
}
|
|
75
112
|
return { ok: true, stdout: (result.stdout || '').trim() };
|
|
76
113
|
}
|
|
114
|
+
/** Preserve structured tmux fields, including an empty final field. */
|
|
115
|
+
/** Preserve structured tmux fields, including an empty final field. */
|
|
116
|
+
function runTmuxStructured(args) {
|
|
117
|
+
const { result } = spawnPlatformCommandSync('tmux', args, { encoding: 'utf-8' });
|
|
118
|
+
if (result.error)
|
|
119
|
+
return { ok: false, stderr: result.error.message };
|
|
120
|
+
if (result.status !== 0) {
|
|
121
|
+
return { ok: false, stderr: (result.stderr || '').trim() || `tmux exited ${result.status}` };
|
|
122
|
+
}
|
|
123
|
+
return { ok: true, stdout: (result.stdout || '').replace(/\r?\n$/, '') };
|
|
124
|
+
}
|
|
125
|
+
function restoredHudCleanupDebtPath(cwd, stateRoot) {
|
|
126
|
+
const canonicalStateRoot = resolveCanonicalTeamStateRoot(resolve(cwd));
|
|
127
|
+
if (typeof stateRoot !== 'string' || !stateRoot.trim()) {
|
|
128
|
+
return join(canonicalStateRoot, '.restored-hud-cleanup-debt.json');
|
|
129
|
+
}
|
|
130
|
+
const canonicalTeamsRoot = join(canonicalStateRoot, 'team');
|
|
131
|
+
const root = resolve(stateRoot);
|
|
132
|
+
const relativeRoot = relative(canonicalTeamsRoot, root);
|
|
133
|
+
if (!isAbsolute(stateRoot.trim())
|
|
134
|
+
|| !relativeRoot
|
|
135
|
+
|| relativeRoot === '..'
|
|
136
|
+
|| relativeRoot.startsWith(`..${sep}`)
|
|
137
|
+
|| isAbsolute(relativeRoot)
|
|
138
|
+
|| relativeRoot.split(sep).length !== 1) {
|
|
139
|
+
throw new Error('restored_hud_cleanup_debt_state_root_invalid');
|
|
140
|
+
}
|
|
141
|
+
for (let candidate = root;; candidate = dirname(candidate)) {
|
|
142
|
+
try {
|
|
143
|
+
const metadata = lstatSync(candidate);
|
|
144
|
+
if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
|
|
145
|
+
throw new Error('restored_hud_cleanup_debt_state_root_invalid');
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
catch (error) {
|
|
149
|
+
if (error.code !== 'ENOENT')
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
152
|
+
if (candidate === canonicalTeamsRoot)
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
return join(root, '.restored-hud-cleanup-debt.json');
|
|
156
|
+
}
|
|
157
|
+
function syncRestoredHudDebtParentSync(path) {
|
|
158
|
+
let descriptor;
|
|
159
|
+
try {
|
|
160
|
+
descriptor = openSync(dirname(path), 'r');
|
|
161
|
+
fsyncSync(descriptor);
|
|
162
|
+
}
|
|
163
|
+
catch (error) {
|
|
164
|
+
const code = error.code;
|
|
165
|
+
if (!(code === 'EPERM' && process.platform === 'win32'))
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
finally {
|
|
169
|
+
if (descriptor !== undefined)
|
|
170
|
+
closeSync(descriptor);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
function persistRestoredHudCleanupDebtSync(cwd, debt, stateRoot) {
|
|
174
|
+
const debtPath = restoredHudCleanupDebtPath(cwd, stateRoot);
|
|
175
|
+
mkdirSync(dirname(debtPath), { recursive: true });
|
|
176
|
+
// Recheck after mkdir: an untrusted Team root must still be canonical and
|
|
177
|
+
// non-symlinked immediately before the durable file mutation.
|
|
178
|
+
restoredHudCleanupDebtPath(cwd, stateRoot);
|
|
179
|
+
const temporaryPath = `${debtPath}.tmp.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`;
|
|
180
|
+
writeFileSync(temporaryPath, `${JSON.stringify(debt)}\n`, { mode: 0o600 });
|
|
181
|
+
const temporaryDescriptor = openSync(temporaryPath, 'r');
|
|
182
|
+
try {
|
|
183
|
+
fsyncSync(temporaryDescriptor);
|
|
184
|
+
}
|
|
185
|
+
finally {
|
|
186
|
+
closeSync(temporaryDescriptor);
|
|
187
|
+
}
|
|
188
|
+
renameSync(temporaryPath, debtPath);
|
|
189
|
+
syncRestoredHudDebtParentSync(debtPath);
|
|
190
|
+
}
|
|
191
|
+
function parseRestoredHudCleanupDebtSync(cwd, stateRoot) {
|
|
192
|
+
const debtPath = restoredHudCleanupDebtPath(cwd, stateRoot);
|
|
193
|
+
if (!existsSync(debtPath))
|
|
194
|
+
return null;
|
|
195
|
+
let debt;
|
|
196
|
+
try {
|
|
197
|
+
debt = JSON.parse(readFileSync(debtPath, 'utf8'));
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
throw new Error('restored_hud_cleanup_debt_malformed');
|
|
201
|
+
}
|
|
202
|
+
if (debt.schema_version !== 1 || debt.operation !== 'restored_hud_cleanup'
|
|
203
|
+
|| !hasExplicitWorkerPaneId(debt.pane_id)
|
|
204
|
+
|| !hasExplicitWorkerPaneId(debt.leader_pane_id)
|
|
205
|
+
|| !Number.isSafeInteger(debt.leader_pane_pid) || debt.leader_pane_pid <= 0
|
|
206
|
+
|| !(typeof debt.leader_pane_owner_id === 'string' || debt.leader_pane_owner_id === null)
|
|
207
|
+
|| debt.hud_owner_leader_pane_id !== debt.leader_pane_id
|
|
208
|
+
|| (debt.pane_pid !== null && (!Number.isSafeInteger(debt.pane_pid) || debt.pane_pid <= 0))) {
|
|
209
|
+
throw new Error('restored_hud_cleanup_debt_malformed');
|
|
210
|
+
}
|
|
211
|
+
return { path: debtPath, debt };
|
|
212
|
+
}
|
|
213
|
+
function removeRestoredHudCleanupDebtSync(path) {
|
|
214
|
+
unlinkSync(path);
|
|
215
|
+
syncRestoredHudDebtParentSync(path);
|
|
216
|
+
}
|
|
217
|
+
/**
|
|
218
|
+
* Replays a prior restored-HUD obligation. A live pane is killable only when
|
|
219
|
+
* its recorded PID, HUD command identity, and tagged leader identity all
|
|
220
|
+
* still match; PID-less and ambiguous records remain durable debt.
|
|
221
|
+
*/
|
|
222
|
+
export function reconcileRestoredHudCleanupDebtSync(cwd, stateRoot) {
|
|
223
|
+
const record = parseRestoredHudCleanupDebtSync(cwd, stateRoot);
|
|
224
|
+
if (!record)
|
|
225
|
+
return;
|
|
226
|
+
const { path, debt } = record;
|
|
227
|
+
const proof = readExactPaneProofSync(debt.pane_id);
|
|
228
|
+
if (proof.status === 'gone') {
|
|
229
|
+
removeRestoredHudCleanupDebtSync(path);
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
if (proof.status !== 'live' || debt.pane_pid === null || proof.pid !== debt.pane_pid || !debt.leader_pane_owner_id) {
|
|
233
|
+
throw new Error(`restored_hud_cleanup_debt_unresolved:${debt.pane_id}`);
|
|
234
|
+
}
|
|
235
|
+
const leader = requireLiveTeamOwnedPaneSync(debt.leader_pane_id, debt.leader_pane_pid, debt.leader_pane_owner_id);
|
|
236
|
+
const topology = listPanesResult(leader);
|
|
237
|
+
const hudMatches = !topology.error && topology.panes.filter((pane) => pane.paneId === proof.paneId
|
|
238
|
+
&& hudPaneMatchesOwner(pane, { leaderPaneId: debt.hud_owner_leader_pane_id })).length === 1;
|
|
239
|
+
if (!hudMatches)
|
|
240
|
+
throw new Error(`restored_hud_cleanup_debt_unresolved:${debt.pane_id}`);
|
|
241
|
+
killExactPaneSync(proof.paneId, debt.pane_pid, () => {
|
|
242
|
+
requireLiveTeamOwnedPaneSync(debt.leader_pane_id, debt.leader_pane_pid, debt.leader_pane_owner_id);
|
|
243
|
+
});
|
|
244
|
+
removeRestoredHudCleanupDebtSync(path);
|
|
245
|
+
}
|
|
246
|
+
/** Remove restored-HUD debt only after the canonical config transaction commits the same frozen identity. */
|
|
247
|
+
export function finalizeRestoredHudCleanupDebtSync(cwd, paneId, panePid, stateRoot, _requireFreshHudIdentity) {
|
|
248
|
+
const record = parseRestoredHudCleanupDebtSync(cwd, stateRoot);
|
|
249
|
+
if (!record)
|
|
250
|
+
return;
|
|
251
|
+
const { debt } = record;
|
|
252
|
+
if (debt.pane_id !== paneId || debt.pane_pid !== panePid || !debt.leader_pane_owner_id) {
|
|
253
|
+
throw new Error(`restored_hud_cleanup_debt_unresolved:${debt.pane_id}`);
|
|
254
|
+
}
|
|
255
|
+
const leader = requireLiveTeamOwnedPaneSync(debt.leader_pane_id, debt.leader_pane_pid, debt.leader_pane_owner_id);
|
|
256
|
+
const topology = listPanesResult(leader);
|
|
257
|
+
const hudMatches = !topology.error && topology.panes.filter((pane) => pane.paneId === debt.pane_id
|
|
258
|
+
&& hudPaneMatchesOwner(pane, { leaderPaneId: debt.hud_owner_leader_pane_id })).length === 1;
|
|
259
|
+
if (!hudMatches)
|
|
260
|
+
throw new Error(`restored_hud_cleanup_debt_unresolved:${debt.pane_id}`);
|
|
261
|
+
const finalProof = readExactPaneProofSync(debt.pane_id);
|
|
262
|
+
if (finalProof.status !== 'live' || finalProof.pid !== debt.pane_pid) {
|
|
263
|
+
throw new Error(`restored_hud_cleanup_debt_unresolved:${debt.pane_id}`);
|
|
264
|
+
}
|
|
265
|
+
removeRestoredHudCleanupDebtSync(record.path);
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Kill only a pane that a fresh global snapshot proves live. Callers treat
|
|
269
|
+
* gone/dead rows as already cleaned and unavailable snapshots as fail-closed.
|
|
270
|
+
*/
|
|
271
|
+
function requireLiveExactPaneSync(paneId, expectedPid) {
|
|
272
|
+
const proof = readExactPaneProofSync(paneId);
|
|
273
|
+
if (proof.status === 'unavailable')
|
|
274
|
+
throw new ExactPaneProofUnavailableError(proof);
|
|
275
|
+
if (proof.status === 'gone')
|
|
276
|
+
throw new Error(`tmux pane is not proven live: ${paneId}`);
|
|
277
|
+
if (expectedPid !== undefined && proof.pid !== expectedPid) {
|
|
278
|
+
throw new Error(`tmux pane identity changed: ${paneId}`);
|
|
279
|
+
}
|
|
280
|
+
return proof.paneId;
|
|
281
|
+
}
|
|
282
|
+
function killExactPaneSync(paneId, expectedPid, assertAuthorization) {
|
|
283
|
+
const proof = readExactPaneProofSync(paneId);
|
|
284
|
+
if (proof.status === 'unavailable')
|
|
285
|
+
throw new ExactPaneProofUnavailableError(proof);
|
|
286
|
+
if (proof.status === 'gone')
|
|
287
|
+
return;
|
|
288
|
+
if (expectedPid !== undefined && proof.pid !== expectedPid) {
|
|
289
|
+
throw new Error(`tmux pane identity changed: ${paneId}`);
|
|
290
|
+
}
|
|
291
|
+
assertAuthorization?.();
|
|
292
|
+
// Authorization can read tmux. Re-prove immediately after it so a recycled
|
|
293
|
+
// pane ID cannot be targeted by the subsequent kill.
|
|
294
|
+
const finalProof = readExactPaneProofSync(proof.paneId);
|
|
295
|
+
if (finalProof.status === 'unavailable')
|
|
296
|
+
throw new ExactPaneProofUnavailableError(finalProof);
|
|
297
|
+
if (finalProof.status === 'gone')
|
|
298
|
+
return;
|
|
299
|
+
if (finalProof.pid !== proof.pid || (expectedPid !== undefined && finalProof.pid !== expectedPid)) {
|
|
300
|
+
throw new Error(`tmux pane identity changed: ${paneId}`);
|
|
301
|
+
}
|
|
302
|
+
const result = runTmux(['kill-pane', '-t', finalProof.paneId]);
|
|
303
|
+
if (!result.ok)
|
|
304
|
+
throw new Error(`failed to kill tmux pane ${finalProof.paneId}: ${result.stderr}`);
|
|
305
|
+
const afterKill = readExactPaneProofSync(finalProof.paneId);
|
|
306
|
+
if (afterKill.status === 'unavailable')
|
|
307
|
+
throw new ExactPaneProofUnavailableError(afterKill);
|
|
308
|
+
if (afterKill.status !== 'gone') {
|
|
309
|
+
if (afterKill.pid !== finalProof.pid)
|
|
310
|
+
throw new Error(`tmux pane identity changed: ${paneId}`);
|
|
311
|
+
throw new Error(`tmux pane remains live after kill: ${paneId}`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
77
314
|
function appendNoUnderlineStyleFlags(style) {
|
|
78
315
|
const normalized = style
|
|
79
316
|
.split(/[,\s]+/)
|
|
@@ -86,7 +323,8 @@ function appendNoUnderlineStyleFlags(style) {
|
|
|
86
323
|
}
|
|
87
324
|
return combined.join(',');
|
|
88
325
|
}
|
|
89
|
-
function sanitizeTmuxStyleOption(sessionTarget, optionName) {
|
|
326
|
+
function sanitizeTmuxStyleOption(sessionTarget, optionName, beforeEffect) {
|
|
327
|
+
beforeEffect?.();
|
|
90
328
|
const shown = runTmux(['show-options', '-gv', '-t', sessionTarget, optionName]);
|
|
91
329
|
if (!shown.ok)
|
|
92
330
|
return false;
|
|
@@ -96,36 +334,39 @@ function sanitizeTmuxStyleOption(sessionTarget, optionName) {
|
|
|
96
334
|
const sanitized = appendNoUnderlineStyleFlags(current);
|
|
97
335
|
if (sanitized === current)
|
|
98
336
|
return true;
|
|
337
|
+
beforeEffect?.();
|
|
99
338
|
const result = runTmux(['set-option', '-t', sessionTarget, optionName, sanitized]);
|
|
100
339
|
return result.ok;
|
|
101
340
|
}
|
|
102
|
-
function tagPaneInstance(paneTarget, instanceId) {
|
|
341
|
+
function tagPaneInstance(paneTarget, instanceId, expectedPanePid) {
|
|
103
342
|
const target = paneTarget.trim();
|
|
104
343
|
const sanitized = instanceId.trim();
|
|
105
344
|
if (!target || !sanitized)
|
|
106
345
|
return;
|
|
107
|
-
const
|
|
346
|
+
const provenTarget = requireLiveExactPaneSync(target, expectedPanePid);
|
|
347
|
+
const result = runTmux(['set-option', '-p', '-t', provenTarget, OMX_PANE_INSTANCE_OPTION, sanitized]);
|
|
108
348
|
if (!result.ok) {
|
|
109
|
-
throw new Error(`failed to tag tmux pane ${
|
|
349
|
+
throw new Error(`failed to tag tmux pane ${provenTarget}: ${result.stderr}`);
|
|
110
350
|
}
|
|
111
351
|
}
|
|
112
|
-
export function tagPaneTeamOwner(paneTarget, teamOwnerId) {
|
|
352
|
+
export function tagPaneTeamOwner(paneTarget, teamOwnerId, expectedPanePid) {
|
|
113
353
|
const target = paneTarget.trim();
|
|
114
354
|
const sanitized = teamOwnerId.trim();
|
|
115
355
|
if (!target || !sanitized)
|
|
116
356
|
return;
|
|
117
|
-
const
|
|
357
|
+
const provenTarget = requireLiveExactPaneSync(target, expectedPanePid);
|
|
358
|
+
const result = runTmux(['set-option', '-p', '-t', provenTarget, OMX_TEAM_PANE_OWNER_OPTION, sanitized]);
|
|
118
359
|
if (!result.ok) {
|
|
119
|
-
throw new Error(`failed to tag tmux pane ${
|
|
360
|
+
throw new Error(`failed to tag tmux pane ${provenTarget}: ${result.stderr}`);
|
|
120
361
|
}
|
|
121
362
|
}
|
|
122
|
-
export function mitigateCopyModeUnderlineArtifacts(sessionTarget) {
|
|
363
|
+
export function mitigateCopyModeUnderlineArtifacts(sessionTarget, beforeEffect) {
|
|
123
364
|
const normalizedTarget = sessionTarget.trim();
|
|
124
365
|
if (normalizedTarget === '')
|
|
125
366
|
return false;
|
|
126
367
|
let applied = false;
|
|
127
368
|
for (const optionName of TMUX_COPY_MODE_STYLE_OPTIONS) {
|
|
128
|
-
if (sanitizeTmuxStyleOption(normalizedTarget, optionName)) {
|
|
369
|
+
if (sanitizeTmuxStyleOption(normalizedTarget, optionName, beforeEffect)) {
|
|
129
370
|
applied = true;
|
|
130
371
|
}
|
|
131
372
|
}
|
|
@@ -181,18 +422,97 @@ function baseSessionName(target) {
|
|
|
181
422
|
return target.split(':')[0] || target;
|
|
182
423
|
}
|
|
183
424
|
function listPanes(target) {
|
|
184
|
-
|
|
425
|
+
return listPanesResult(target).panes;
|
|
426
|
+
}
|
|
427
|
+
function listPanesResult(target) {
|
|
428
|
+
const result = runTmuxStructured(['list-panes', '-t', target, '-F', '#{pane_id}\t#{pane_current_command}\t#{pane_start_command}']);
|
|
185
429
|
if (!result.ok)
|
|
186
|
-
return [];
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
.
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
430
|
+
return { panes: [], error: result.stderr };
|
|
431
|
+
const panes = [];
|
|
432
|
+
for (const rawLine of result.stdout.split('\n')) {
|
|
433
|
+
const line = rawLine.endsWith('\r') ? rawLine.slice(0, -1) : rawLine;
|
|
434
|
+
if (line.length === 0)
|
|
435
|
+
continue;
|
|
436
|
+
const firstSeparator = line.indexOf('\t');
|
|
437
|
+
const secondSeparator = firstSeparator >= 0 ? line.indexOf('\t', firstSeparator + 1) : -1;
|
|
438
|
+
const paneId = firstSeparator >= 0 ? line.slice(0, firstSeparator) : line;
|
|
439
|
+
if (firstSeparator < 0 || secondSeparator < 0 || !/^%[0-9]+$/.test(paneId)) {
|
|
440
|
+
return { panes: [], error: 'malformed pane topology' };
|
|
441
|
+
}
|
|
442
|
+
if (panes.some((pane) => pane.paneId === paneId)) {
|
|
443
|
+
return { panes: [], error: 'malformed pane topology' };
|
|
444
|
+
}
|
|
445
|
+
panes.push({
|
|
446
|
+
paneId,
|
|
447
|
+
currentCommand: line.slice(firstSeparator + 1, secondSeparator),
|
|
448
|
+
startCommand: line.slice(secondSeparator + 1),
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
return { panes, error: null };
|
|
452
|
+
}
|
|
453
|
+
/**
|
|
454
|
+
* Window-wide layout effects may only touch the exact startup pane set. A
|
|
455
|
+
* target-scoped topology read rejects foreign/new panes, and targeted global
|
|
456
|
+
* snapshots pin every surviving pane identity. The final scoped read happens
|
|
457
|
+
* immediately before the mutation so panes added during owner reads cannot
|
|
458
|
+
* inherit that authority.
|
|
459
|
+
*/
|
|
460
|
+
/**
|
|
461
|
+
* Re-prove an explicitly Team-owned pane immediately before an effect. The
|
|
462
|
+
* double PID proof keeps ownership and process identity continuous across an
|
|
463
|
+
* untrusted tmux option read.
|
|
464
|
+
*/
|
|
465
|
+
function requireLiveTeamOwnedPaneSync(paneId, expectedPanePid, expectedTeamOwnerId) {
|
|
466
|
+
const target = requireLiveExactPaneSync(paneId, expectedPanePid);
|
|
467
|
+
const owner = readPaneTeamOwnerTagResult(target);
|
|
468
|
+
if (owner.status !== 'value' || owner.value !== expectedTeamOwnerId) {
|
|
469
|
+
const detail = owner.status === 'error' ? owner.error : 'missing';
|
|
470
|
+
throw new Error(`tmux pane team owner changed: ${target}: ${detail}`);
|
|
471
|
+
}
|
|
472
|
+
return requireLiveExactPaneSync(target, expectedPanePid);
|
|
473
|
+
}
|
|
474
|
+
function requireFrozenWindowTopologySync(teamTarget, expectedPanePids, expectedPaneOwners) {
|
|
475
|
+
const topology = listPanesResult(teamTarget);
|
|
476
|
+
if (topology.error)
|
|
477
|
+
throw new Error(`failed to read tmux pane topology: ${topology.error}`);
|
|
478
|
+
const actualPaneIds = new Set(topology.panes.map((pane) => pane.paneId));
|
|
479
|
+
if (actualPaneIds.size !== expectedPanePids.size
|
|
480
|
+
|| [...expectedPanePids.keys()].some((paneId) => !actualPaneIds.has(paneId))) {
|
|
481
|
+
throw new Error('tmux window topology changed before layout mutation');
|
|
482
|
+
}
|
|
483
|
+
for (const [paneId, expectedPanePid] of expectedPanePids) {
|
|
484
|
+
const expectedOwner = expectedPaneOwners?.get(paneId);
|
|
485
|
+
if (expectedOwner)
|
|
486
|
+
requireLiveTeamOwnedPaneSync(paneId, expectedPanePid, expectedOwner);
|
|
487
|
+
else {
|
|
488
|
+
const proof = readExactPaneProofSync(paneId);
|
|
489
|
+
if (proof.status === 'unavailable')
|
|
490
|
+
throw new ExactPaneProofUnavailableError(proof);
|
|
491
|
+
if (proof.status === 'gone')
|
|
492
|
+
throw new Error(`tmux pane is not proven live: ${proof.paneId}`);
|
|
493
|
+
if (proof.pid !== expectedPanePid)
|
|
494
|
+
throw new Error(`tmux pane identity changed: ${proof.paneId}`);
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
const finalProofs = readExactPaneProofsSync([...expectedPanePids.keys()]);
|
|
498
|
+
for (const [index, [paneId, expectedPanePid]] of [...expectedPanePids.entries()].entries()) {
|
|
499
|
+
const proof = finalProofs[index];
|
|
500
|
+
if (proof.status === 'unavailable')
|
|
501
|
+
throw new ExactPaneProofUnavailableError(proof);
|
|
502
|
+
if (proof.status === 'gone')
|
|
503
|
+
throw new Error(`tmux pane is not proven live: ${proof.paneId}`);
|
|
504
|
+
if (proof.paneId !== paneId || proof.pid !== expectedPanePid) {
|
|
505
|
+
throw new Error(`tmux pane identity changed: ${paneId}`);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
const finalTopology = listPanesResult(teamTarget);
|
|
509
|
+
if (finalTopology.error)
|
|
510
|
+
throw new Error(`failed to read tmux pane topology: ${finalTopology.error}`);
|
|
511
|
+
const finalPaneIds = new Set(finalTopology.panes.map((pane) => pane.paneId));
|
|
512
|
+
if (finalPaneIds.size !== expectedPanePids.size
|
|
513
|
+
|| [...expectedPanePids.keys()].some((paneId) => !finalPaneIds.has(paneId))) {
|
|
514
|
+
throw new Error('tmux window topology changed before layout mutation');
|
|
515
|
+
}
|
|
196
516
|
}
|
|
197
517
|
export function listPaneIds(target) {
|
|
198
518
|
return listPanes(target).map((pane) => pane.paneId);
|
|
@@ -237,12 +557,6 @@ export function chooseTeamLeaderPaneId(panes, preferredPaneId) {
|
|
|
237
557
|
return nonHud.paneId;
|
|
238
558
|
return preferredPaneId;
|
|
239
559
|
}
|
|
240
|
-
function findHudPaneIds(target, leaderPaneId) {
|
|
241
|
-
return findHudWatchPaneIds(listPanes(target), leaderPaneId, { leaderPaneId });
|
|
242
|
-
}
|
|
243
|
-
function findOwnedHudPaneIds(target, leaderPaneId) {
|
|
244
|
-
return findHudWatchPaneIds(listPanes(target), leaderPaneId, { leaderPaneId });
|
|
245
|
-
}
|
|
246
560
|
function readPaneCurrentPath(paneId) {
|
|
247
561
|
if (!paneId.startsWith('%'))
|
|
248
562
|
return null;
|
|
@@ -281,7 +595,8 @@ function shouldAttemptRestoreCwdCandidate(candidate) {
|
|
|
281
595
|
}
|
|
282
596
|
return pathIsUsableDirectory(candidate.rawPath);
|
|
283
597
|
}
|
|
284
|
-
function resolveStandaloneHudRestoreCwdCandidates(leaderPaneId, fallbackCwd, explicitCwd) {
|
|
598
|
+
function resolveStandaloneHudRestoreCwdCandidates(leaderPaneId, fallbackCwd, explicitCwd, beforeReadLiveLeaderCwd) {
|
|
599
|
+
beforeReadLiveLeaderCwd?.();
|
|
285
600
|
const liveLeaderCwd = readPaneCurrentPath(leaderPaneId);
|
|
286
601
|
return uniqueRestoreCwdCandidates([
|
|
287
602
|
{ source: 'explicit', rawPath: explicitCwd },
|
|
@@ -318,105 +633,161 @@ async function runTmuxAsync(args) {
|
|
|
318
633
|
return { ok: false, stderr: (err.stderr || err.message || '').trim() || 'tmux command failed' };
|
|
319
634
|
}
|
|
320
635
|
}
|
|
321
|
-
|
|
636
|
+
function hasExplicitWorkerPaneId(workerPaneId) {
|
|
637
|
+
return typeof workerPaneId === 'string' && workerPaneId.trim().length > 0;
|
|
638
|
+
}
|
|
639
|
+
function resolveWorkerPaneTargetSync(sessionName, workerIndex, workerPaneId) {
|
|
640
|
+
if (!hasExplicitWorkerPaneId(workerPaneId))
|
|
641
|
+
return paneTarget(sessionName, workerIndex);
|
|
642
|
+
const proof = readExactPaneProofSync(workerPaneId);
|
|
643
|
+
return proof.status === 'live' ? proof.paneId : null;
|
|
644
|
+
}
|
|
645
|
+
async function resolveWorkerPaneTargetAsync(sessionName, workerIndex, workerPaneId) {
|
|
646
|
+
if (!hasExplicitWorkerPaneId(workerPaneId))
|
|
647
|
+
return paneTarget(sessionName, workerIndex);
|
|
648
|
+
const proof = await readExactPaneProof(workerPaneId);
|
|
649
|
+
return proof.status === 'live' ? proof.paneId : null;
|
|
650
|
+
}
|
|
651
|
+
function createPinnedWorkerPaneTargetResolverSync(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId) {
|
|
652
|
+
if (!hasExplicitWorkerPaneId(workerPaneId)) {
|
|
653
|
+
return () => resolveWorkerPaneTargetSync(sessionName, workerIndex, workerPaneId);
|
|
654
|
+
}
|
|
655
|
+
const pinnedPid = expectedPanePid;
|
|
656
|
+
const expectedOwner = typeof expectedTeamOwnerId === 'string' ? expectedTeamOwnerId.trim() : '';
|
|
657
|
+
const canonicalHudPaneId = typeof hudPaneId === 'string' ? hudPaneId.trim() : '';
|
|
658
|
+
return () => {
|
|
659
|
+
if (typeof pinnedPid !== 'number' || !Number.isSafeInteger(pinnedPid) || pinnedPid <= 0)
|
|
660
|
+
return null;
|
|
661
|
+
if (!expectedOwner)
|
|
662
|
+
return null;
|
|
663
|
+
if (canonicalHudPaneId && workerPaneId === canonicalHudPaneId) {
|
|
664
|
+
throw new Error(`tmux worker pane is HUD target: ${workerPaneId}`);
|
|
665
|
+
}
|
|
666
|
+
const proof = readExactPaneProofSync(workerPaneId);
|
|
667
|
+
if (proof.status !== 'live')
|
|
668
|
+
return null;
|
|
669
|
+
if (proof.pid !== pinnedPid) {
|
|
670
|
+
throw new Error(`tmux pane identity changed: ${workerPaneId}`);
|
|
671
|
+
}
|
|
672
|
+
const owner = readPaneTeamOwnerTagResult(proof.paneId);
|
|
673
|
+
if (owner.status !== 'value' || owner.value !== expectedOwner)
|
|
674
|
+
return null;
|
|
675
|
+
const finalProof = readExactPaneProofSync(workerPaneId);
|
|
676
|
+
if (finalProof.status !== 'live')
|
|
677
|
+
return null;
|
|
678
|
+
if (finalProof.pid !== pinnedPid) {
|
|
679
|
+
throw new Error(`tmux pane identity changed: ${workerPaneId}`);
|
|
680
|
+
}
|
|
681
|
+
return finalProof.paneId;
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
function createPinnedWorkerPaneTargetResolver(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId) {
|
|
685
|
+
if (!hasExplicitWorkerPaneId(workerPaneId)) {
|
|
686
|
+
return () => resolveWorkerPaneTargetAsync(sessionName, workerIndex, workerPaneId);
|
|
687
|
+
}
|
|
688
|
+
const pinnedPid = expectedPanePid;
|
|
689
|
+
const expectedOwner = typeof expectedTeamOwnerId === 'string' ? expectedTeamOwnerId.trim() : '';
|
|
690
|
+
const canonicalHudPaneId = typeof hudPaneId === 'string' ? hudPaneId.trim() : '';
|
|
691
|
+
let lastResolvedPid;
|
|
692
|
+
const resolveTarget = async () => {
|
|
693
|
+
if (typeof pinnedPid !== 'number' || !Number.isSafeInteger(pinnedPid) || pinnedPid <= 0)
|
|
694
|
+
return null;
|
|
695
|
+
if (!expectedOwner)
|
|
696
|
+
return null;
|
|
697
|
+
if (canonicalHudPaneId && workerPaneId === canonicalHudPaneId) {
|
|
698
|
+
throw new Error(`tmux worker pane is HUD target: ${workerPaneId}`);
|
|
699
|
+
}
|
|
700
|
+
const proof = await readExactPaneProof(workerPaneId);
|
|
701
|
+
if (proof.status !== 'live')
|
|
702
|
+
return null;
|
|
703
|
+
if (proof.pid !== pinnedPid) {
|
|
704
|
+
throw new Error(`tmux pane identity changed: ${workerPaneId}`);
|
|
705
|
+
}
|
|
706
|
+
const owner = readPaneTeamOwnerTagResult(proof.paneId);
|
|
707
|
+
if (owner.status !== 'value' || owner.value !== expectedOwner) {
|
|
708
|
+
const detail = owner.status === 'error' ? owner.error : 'missing';
|
|
709
|
+
throw new Error(`tmux pane team owner changed: ${workerPaneId}: ${detail}`);
|
|
710
|
+
}
|
|
711
|
+
// The option read is untrusted and can race pane ID reuse. Its immediately
|
|
712
|
+
// adjacent PID proof authorizes the following capture/control/input effect.
|
|
713
|
+
const finalProof = await readExactPaneProof(workerPaneId);
|
|
714
|
+
if (finalProof.status !== 'live')
|
|
715
|
+
return null;
|
|
716
|
+
if (finalProof.pid !== pinnedPid) {
|
|
717
|
+
throw new Error(`tmux pane identity changed: ${workerPaneId}`);
|
|
718
|
+
}
|
|
719
|
+
lastResolvedPid = finalProof.pid;
|
|
720
|
+
return finalProof.paneId;
|
|
721
|
+
};
|
|
722
|
+
resolveTarget.lastResolvedPid = () => lastResolvedPid;
|
|
723
|
+
return resolveTarget;
|
|
724
|
+
}
|
|
725
|
+
async function requireAsyncPaneTarget(resolveTarget) {
|
|
726
|
+
const target = await resolveTarget();
|
|
727
|
+
if (!target)
|
|
728
|
+
throw new Error('tmux pane is not proven live');
|
|
729
|
+
return target;
|
|
730
|
+
}
|
|
731
|
+
async function sendKeyAsync(resolveTarget, key) {
|
|
732
|
+
const target = await requireAsyncPaneTarget(resolveTarget);
|
|
322
733
|
const result = await runTmuxAsync(['send-keys', '-t', target, key]);
|
|
323
734
|
if (!result.ok) {
|
|
324
735
|
throw new Error(`sendKeyAsync: failed to send ${key}: ${result.stderr}`);
|
|
325
736
|
}
|
|
326
737
|
}
|
|
327
|
-
async function capturePaneAsync(
|
|
738
|
+
async function capturePaneAsync(resolveTarget) {
|
|
739
|
+
const target = await resolveTarget();
|
|
740
|
+
if (!target)
|
|
741
|
+
return '';
|
|
328
742
|
const result = await runTmuxAsync(sharedBuildCapturePaneArgv(target, 80));
|
|
329
743
|
if (!result.ok)
|
|
330
744
|
return '';
|
|
331
745
|
return result.stdout;
|
|
332
746
|
}
|
|
333
|
-
|
|
747
|
+
/** Capture an explicit Team worker pane only after PID-owner-PID authorization. */
|
|
748
|
+
export async function captureWorkerPane(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId) {
|
|
749
|
+
const resolveTarget = createPinnedWorkerPaneTargetResolver(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId);
|
|
750
|
+
return capturePaneAsync(resolveTarget);
|
|
751
|
+
}
|
|
752
|
+
async function captureVisiblePaneAsync(resolveTarget) {
|
|
753
|
+
const target = await resolveTarget();
|
|
754
|
+
if (!target)
|
|
755
|
+
return '';
|
|
334
756
|
const result = await runTmuxAsync(sharedBuildVisibleCapturePaneArgv(target));
|
|
335
757
|
if (!result.ok)
|
|
336
758
|
return '';
|
|
337
759
|
return result.stdout;
|
|
338
760
|
}
|
|
339
|
-
async function isWorkerAliveAsync(sessionName, workerIndex, workerPaneId) {
|
|
340
|
-
if (workerPaneId
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
761
|
+
async function isWorkerAliveAsync(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId, resolveTarget) {
|
|
762
|
+
if (hasExplicitWorkerPaneId(workerPaneId)) {
|
|
763
|
+
const resolver = resolveTarget ?? createPinnedWorkerPaneTargetResolver(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId);
|
|
764
|
+
const target = await resolver();
|
|
765
|
+
const pid = resolver.lastResolvedPid?.() ?? expectedPanePid;
|
|
766
|
+
if (!target || typeof pid !== 'number' || !Number.isSafeInteger(pid) || pid <= 0)
|
|
767
|
+
return 'unknown';
|
|
768
|
+
return probeProcessLiveness(pid);
|
|
344
769
|
}
|
|
345
770
|
const result = await runTmuxAsync([
|
|
346
771
|
'list-panes',
|
|
347
|
-
'-t', paneTarget(sessionName, workerIndex
|
|
772
|
+
'-t', paneTarget(sessionName, workerIndex),
|
|
348
773
|
'-F',
|
|
349
774
|
'#{pane_dead} #{pane_pid}',
|
|
350
775
|
]);
|
|
351
776
|
if (!result.ok)
|
|
352
|
-
return
|
|
777
|
+
return 'unknown';
|
|
353
778
|
const line = result.stdout.split('\n')[0]?.trim();
|
|
354
779
|
if (!line)
|
|
355
|
-
return
|
|
780
|
+
return 'unknown';
|
|
356
781
|
const parts = line.split(/\s+/);
|
|
357
782
|
if (parts.length < 2)
|
|
358
|
-
return
|
|
783
|
+
return 'unknown';
|
|
359
784
|
const paneDead = parts[0];
|
|
360
785
|
const pid = Number.parseInt(parts[1], 10);
|
|
361
786
|
if (paneDead === '1')
|
|
362
|
-
return
|
|
363
|
-
if (!Number.isFinite(pid))
|
|
364
|
-
return false;
|
|
365
|
-
try {
|
|
366
|
-
process.kill(pid, 0);
|
|
367
|
-
return true;
|
|
368
|
-
}
|
|
369
|
-
catch {
|
|
370
|
-
return false;
|
|
371
|
-
}
|
|
372
|
-
}
|
|
373
|
-
function parsePaneLivenessLine(line, allowMissingPid) {
|
|
374
|
-
const parts = line.trim().split(/\s+/);
|
|
375
|
-
if (parts.length < 1 || parts[0] === '')
|
|
376
|
-
return null;
|
|
377
|
-
const paneDead = parts[0];
|
|
378
|
-
const pid = Number.parseInt(parts[1] ?? '', 10);
|
|
379
|
-
if (paneDead === '1')
|
|
380
|
-
return false;
|
|
787
|
+
return 'gone';
|
|
381
788
|
if (!Number.isFinite(pid))
|
|
382
|
-
return
|
|
383
|
-
|
|
384
|
-
process.kill(pid, 0);
|
|
385
|
-
return true;
|
|
386
|
-
}
|
|
387
|
-
catch {
|
|
388
|
-
return allowMissingPid;
|
|
389
|
-
}
|
|
390
|
-
}
|
|
391
|
-
function readPaneLivenessById(paneId) {
|
|
392
|
-
const result = runTmux(['list-panes', '-a', '-F', '#{pane_id} #{pane_dead} #{pane_pid}']);
|
|
393
|
-
if (!result.ok)
|
|
394
|
-
return null;
|
|
395
|
-
for (const line of result.stdout.split('\n')) {
|
|
396
|
-
const trimmed = line.trim();
|
|
397
|
-
if (!trimmed)
|
|
398
|
-
continue;
|
|
399
|
-
const [id = '', ...rest] = trimmed.split(/\s+/);
|
|
400
|
-
if (id !== paneId)
|
|
401
|
-
continue;
|
|
402
|
-
return parsePaneLivenessLine(rest.join(' '), true);
|
|
403
|
-
}
|
|
404
|
-
return null;
|
|
405
|
-
}
|
|
406
|
-
async function readPaneLivenessByIdAsync(paneId) {
|
|
407
|
-
const result = await runTmuxAsync(['list-panes', '-a', '-F', '#{pane_id} #{pane_dead} #{pane_pid}']);
|
|
408
|
-
if (!result.ok)
|
|
409
|
-
return null;
|
|
410
|
-
for (const line of result.stdout.split('\n')) {
|
|
411
|
-
const trimmed = line.trim();
|
|
412
|
-
if (!trimmed)
|
|
413
|
-
continue;
|
|
414
|
-
const [id = '', ...rest] = trimmed.split(/\s+/);
|
|
415
|
-
if (id !== paneId)
|
|
416
|
-
continue;
|
|
417
|
-
return parsePaneLivenessLine(rest.join(' '), true);
|
|
418
|
-
}
|
|
419
|
-
return null;
|
|
789
|
+
return 'unknown';
|
|
790
|
+
return probeProcessLiveness(pid);
|
|
420
791
|
}
|
|
421
792
|
function shellQuoteSingle(value) {
|
|
422
793
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
@@ -510,6 +881,20 @@ function buildNestedTmuxShellCommand(command) {
|
|
|
510
881
|
function buildBestEffortShellCommand(command) {
|
|
511
882
|
return `${command} >/dev/null 2>&1 || true`;
|
|
512
883
|
}
|
|
884
|
+
function buildAuthoritativeHudResizeShellCommand(hudPaneId, heightLines = HUD_TMUX_TEAM_HEIGHT_LINES, expectedPanePid, expectedPaneOwnerId, effectCommand = buildNestedTmuxShellCommand(buildHudResizeCommand(buildHudPaneTarget(hudPaneId), heightLines))) {
|
|
885
|
+
const target = buildHudPaneTarget(hudPaneId);
|
|
886
|
+
const snapshot = buildNestedTmuxShellCommand("list-panes -a -F '#{pane_id}\\t#{pane_dead}\\t#{pane_pid}'");
|
|
887
|
+
const expectedPid = typeof expectedPanePid === 'number' && Number.isSafeInteger(expectedPanePid) && expectedPanePid > 0
|
|
888
|
+
? ` && $3 == "${expectedPanePid}"`
|
|
889
|
+
: '';
|
|
890
|
+
const proof = `awk -F '\\t' -v pane='${target}' 'NF != 3 || $1 !~ /^%[0-9]+$/ || ($2 != "0" && $2 != "1") || (seen[$1]++) || ($1 == pane && ($3 !~ /^[1-9][0-9]*$/ || length($3) > 16 || (length($3) == 16 && ("x" $3) > "x9007199254740991"))) || ($1 != pane && $3 != "" && ($3 !~ /^[1-9][0-9]*$/ || length($3) > 16 || (length($3) == 16 && ("x" $3) > "x9007199254740991"))) { bad = 1 } $1 == pane && $2 == "0"${expectedPid} { live = 1 } END { exit bad || !live }'`;
|
|
891
|
+
const expectedOwner = expectedPaneOwnerId?.trim();
|
|
892
|
+
if (!expectedOwner) {
|
|
893
|
+
return `if snapshot=$(${snapshot}); then printf '%s\\n' "$snapshot" | ${proof} && ${effectCommand}; fi`;
|
|
894
|
+
}
|
|
895
|
+
const owner = buildNestedTmuxShellCommand(`show-option -qv -p -t ${target} ${OMX_TEAM_PANE_OWNER_OPTION}`);
|
|
896
|
+
return `if snapshot=$(${snapshot}); then printf '%s\\n' "$snapshot" | ${proof} && owner=$(${owner}) && [ "$owner" = ${shellQuoteSingle(expectedOwner)} ] && final_snapshot=$(${snapshot}) && printf '%s\\n' "$final_snapshot" | ${proof} && ${effectCommand}; fi`;
|
|
897
|
+
}
|
|
513
898
|
/** Upper bound for tmux hook indices (signed 32-bit max). */
|
|
514
899
|
const TMUX_HOOK_INDEX_MAX = 2147483647;
|
|
515
900
|
function buildResizeHookSlot(hookName) {
|
|
@@ -526,8 +911,8 @@ function buildClientAttachedHookSlot(hookName) {
|
|
|
526
911
|
}
|
|
527
912
|
return `client-attached[${Math.abs(hash) % TMUX_HOOK_INDEX_MAX}]`;
|
|
528
913
|
}
|
|
529
|
-
export function buildRegisterResizeHookArgs(hookTarget, hookName, hudPaneId, heightLines = HUD_TMUX_TEAM_HEIGHT_LINES) {
|
|
530
|
-
const resizeCommand = buildBestEffortShellCommand(
|
|
914
|
+
export function buildRegisterResizeHookArgs(hookTarget, hookName, hudPaneId, heightLines = HUD_TMUX_TEAM_HEIGHT_LINES, expectedPanePid, expectedPaneOwnerId) {
|
|
915
|
+
const resizeCommand = buildBestEffortShellCommand(buildAuthoritativeHudResizeShellCommand(hudPaneId, heightLines, expectedPanePid, expectedPaneOwnerId));
|
|
531
916
|
const hookCommand = shellQuoteSingle(`${resizeCommand}; sleep ${HUD_RESIZE_RECONCILE_DELAY_SECONDS}; ${resizeCommand}`);
|
|
532
917
|
return ['set-hook', '-t', hookTarget, buildResizeHookSlot(hookName), `run-shell -b ${hookCommand}`];
|
|
533
918
|
}
|
|
@@ -543,9 +928,11 @@ export function buildClientAttachedReconcileHookName(teamName, sessionName, wind
|
|
|
543
928
|
normalizeHudPaneToken(hudPaneId),
|
|
544
929
|
].join('_');
|
|
545
930
|
}
|
|
546
|
-
export function buildRegisterClientAttachedReconcileArgs(hookTarget, hookName, hudPaneId, heightLines = HUD_TMUX_TEAM_HEIGHT_LINES) {
|
|
931
|
+
export function buildRegisterClientAttachedReconcileArgs(hookTarget, hookName, hudPaneId, heightLines = HUD_TMUX_TEAM_HEIGHT_LINES, expectedPanePid, expectedPaneOwnerId) {
|
|
547
932
|
const hookSlot = buildClientAttachedHookSlot(hookName);
|
|
548
|
-
const
|
|
933
|
+
const resizeCommand = buildBestEffortShellCommand(buildAuthoritativeHudResizeShellCommand(hudPaneId, heightLines, expectedPanePid, expectedPaneOwnerId));
|
|
934
|
+
const unregisterCommand = buildBestEffortShellCommand(buildAuthoritativeHudResizeShellCommand(hudPaneId, heightLines, expectedPanePid, expectedPaneOwnerId, buildNestedTmuxShellCommand(`set-hook -u -t ${hookTarget} ${hookSlot}`)));
|
|
935
|
+
const oneShotCommand = shellQuoteSingle(`${resizeCommand}; ${unregisterCommand}`);
|
|
549
936
|
return ['set-hook', '-t', hookTarget, hookSlot, `run-shell -b ${oneShotCommand}`];
|
|
550
937
|
}
|
|
551
938
|
export function buildUnregisterClientAttachedReconcileArgs(hookTarget, hookName) {
|
|
@@ -555,18 +942,19 @@ export function unregisterResizeHook(hookTarget, hookName) {
|
|
|
555
942
|
const result = runTmux(buildUnregisterResizeHookArgs(hookTarget, hookName));
|
|
556
943
|
return result.ok;
|
|
557
944
|
}
|
|
558
|
-
export function buildScheduleDelayedHudResizeArgs(hudPaneId, delaySeconds = HUD_RESIZE_RECONCILE_DELAY_SECONDS, heightLines = HUD_TMUX_TEAM_HEIGHT_LINES) {
|
|
945
|
+
export function buildScheduleDelayedHudResizeArgs(hudPaneId, delaySeconds = HUD_RESIZE_RECONCILE_DELAY_SECONDS, heightLines = HUD_TMUX_TEAM_HEIGHT_LINES, expectedPanePid, expectedPaneOwnerId) {
|
|
559
946
|
const delay = Number.isFinite(delaySeconds) && delaySeconds > 0 ? delaySeconds : HUD_RESIZE_RECONCILE_DELAY_SECONDS;
|
|
560
|
-
return ['run-shell', '-b', `sleep ${delay}; ${buildBestEffortShellCommand(
|
|
947
|
+
return ['run-shell', '-b', `sleep ${delay}; ${buildBestEffortShellCommand(buildAuthoritativeHudResizeShellCommand(hudPaneId, heightLines, expectedPanePid, expectedPaneOwnerId))}`];
|
|
561
948
|
}
|
|
562
|
-
export function buildReconcileHudResizeArgs(hudPaneId, heightLines = HUD_TMUX_TEAM_HEIGHT_LINES) {
|
|
563
|
-
return ['run-shell', buildBestEffortShellCommand(
|
|
949
|
+
export function buildReconcileHudResizeArgs(hudPaneId, heightLines = HUD_TMUX_TEAM_HEIGHT_LINES, expectedPanePid, expectedPaneOwnerId) {
|
|
950
|
+
return ['run-shell', buildBestEffortShellCommand(buildAuthoritativeHudResizeShellCommand(hudPaneId, heightLines, expectedPanePid, expectedPaneOwnerId))];
|
|
564
951
|
}
|
|
565
|
-
function redrawLeaderPaneAfterTeamLayout(leaderPaneId) {
|
|
952
|
+
function redrawLeaderPaneAfterTeamLayout(leaderPaneId, expectedPanePid, expectedTeamOwnerId) {
|
|
566
953
|
const target = leaderPaneId.trim();
|
|
567
954
|
if (!target.startsWith('%'))
|
|
568
955
|
return;
|
|
569
|
-
|
|
956
|
+
const provenTarget = requireLiveTeamOwnedPaneSync(target, expectedPanePid, expectedTeamOwnerId);
|
|
957
|
+
runTmux(['send-keys', '-t', provenTarget, 'C-l']);
|
|
570
958
|
}
|
|
571
959
|
const ZSH_CANDIDATE_PATHS = ['/bin/zsh', '/usr/bin/zsh', '/usr/local/bin/zsh', '/opt/local/bin/zsh', '/opt/homebrew/bin/zsh'];
|
|
572
960
|
const BASH_CANDIDATE_PATHS = ['/bin/bash', '/usr/bin/bash'];
|
|
@@ -613,24 +1001,35 @@ function escapeTomlString(value) {
|
|
|
613
1001
|
function isModelInstructionsOverride(value) {
|
|
614
1002
|
return new RegExp(`^${MODEL_INSTRUCTIONS_FILE_KEY}\\s*=`).test(value.trim());
|
|
615
1003
|
}
|
|
616
|
-
function
|
|
617
|
-
for (let
|
|
618
|
-
const arg = args[
|
|
1004
|
+
function someConfigOverrideBeforeEndOfOptions(args, matches) {
|
|
1005
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
1006
|
+
const arg = args[index];
|
|
1007
|
+
if (arg === '--')
|
|
1008
|
+
break;
|
|
1009
|
+
let value;
|
|
619
1010
|
if (arg === CONFIG_FLAG || arg === LONG_CONFIG_FLAG) {
|
|
620
|
-
|
|
621
|
-
if (
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
continue;
|
|
1011
|
+
value = args[index + 1];
|
|
1012
|
+
if (value === '--')
|
|
1013
|
+
break;
|
|
1014
|
+
index += 1;
|
|
625
1015
|
}
|
|
626
|
-
if (arg.startsWith(`${
|
|
627
|
-
|
|
628
|
-
if (isModelInstructionsOverride(inlineValue))
|
|
629
|
-
return true;
|
|
1016
|
+
else if (arg.startsWith(`${CONFIG_FLAG}=`)) {
|
|
1017
|
+
value = arg.slice(`${CONFIG_FLAG}=`.length);
|
|
630
1018
|
}
|
|
1019
|
+
else if (arg.startsWith(`${LONG_CONFIG_FLAG}=`)) {
|
|
1020
|
+
value = arg.slice(`${LONG_CONFIG_FLAG}=`.length);
|
|
1021
|
+
}
|
|
1022
|
+
else {
|
|
1023
|
+
continue;
|
|
1024
|
+
}
|
|
1025
|
+
if (typeof value === 'string' && matches(value))
|
|
1026
|
+
return true;
|
|
631
1027
|
}
|
|
632
1028
|
return false;
|
|
633
1029
|
}
|
|
1030
|
+
function hasModelInstructionsOverride(args) {
|
|
1031
|
+
return someConfigOverrideBeforeEndOfOptions(args, isModelInstructionsOverride);
|
|
1032
|
+
}
|
|
634
1033
|
function normalizeTeamWorkerCliMode(raw, sourceEnv = OMX_TEAM_WORKER_CLI_ENV) {
|
|
635
1034
|
const normalized = String(raw ?? 'auto').trim().toLowerCase();
|
|
636
1035
|
if (normalized === '' || normalized === 'auto')
|
|
@@ -651,6 +1050,8 @@ function extractModelOverride(args) {
|
|
|
651
1050
|
let model = null;
|
|
652
1051
|
for (let i = 0; i < args.length; i++) {
|
|
653
1052
|
const arg = args[i];
|
|
1053
|
+
if (arg === '--')
|
|
1054
|
+
break;
|
|
654
1055
|
if (arg === MODEL_FLAG) {
|
|
655
1056
|
const maybeValue = args[i + 1];
|
|
656
1057
|
if (typeof maybeValue === 'string' && maybeValue.trim() !== '' && !maybeValue.startsWith('-')) {
|
|
@@ -764,6 +1165,18 @@ function shouldGrantExecutionBypassForRole(workerRole) {
|
|
|
764
1165
|
return true;
|
|
765
1166
|
return agent.tools === 'execution';
|
|
766
1167
|
}
|
|
1168
|
+
export function assertTeamWorkerCliPolicyCompatibility(workerCli, launchArgs) {
|
|
1169
|
+
const policy = classifyTeamWorkerLaunchPolicy(launchArgs);
|
|
1170
|
+
if ((workerCli === 'claude' || workerCli === 'gemini') && (policy === 'direct-policy' || policy === 'mixed-policy')) {
|
|
1171
|
+
throw new Error(`Selected team worker CLI "${workerCli}" is incompatible with an explicit approval or sandbox policy.`);
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
export function assertTeamWorkerLaunchPolicyInvariant(workerCli, launchArgs) {
|
|
1175
|
+
assertTeamWorkerCliPolicyCompatibility(workerCli, launchArgs);
|
|
1176
|
+
if (workerCli === 'codex' && classifyTeamWorkerLaunchPolicy(launchArgs) === 'mixed-policy') {
|
|
1177
|
+
throw new Error('internal_mixed_codex_worker_policy_argv');
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
767
1180
|
export function translateWorkerLaunchArgsForCli(workerCli, args, initialPrompt, workerRole) {
|
|
768
1181
|
if (workerCli === 'codex')
|
|
769
1182
|
return [...args];
|
|
@@ -858,23 +1271,10 @@ export function scrubTeamWorkerHudOwnershipEnv(env) {
|
|
|
858
1271
|
return scrubbed;
|
|
859
1272
|
}
|
|
860
1273
|
function hasConfigOverride(args, key) {
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
const value = args[index + 1];
|
|
866
|
-
if (typeof value === 'string' && value.trim().startsWith(prefix))
|
|
867
|
-
return true;
|
|
868
|
-
index += 1;
|
|
869
|
-
continue;
|
|
870
|
-
}
|
|
871
|
-
if (typeof arg === 'string' && arg.startsWith(`${LONG_CONFIG_FLAG}=`)) {
|
|
872
|
-
const value = arg.slice(`${LONG_CONFIG_FLAG}=`.length);
|
|
873
|
-
if (value.trim().startsWith(prefix))
|
|
874
|
-
return true;
|
|
875
|
-
}
|
|
876
|
-
}
|
|
877
|
-
return false;
|
|
1274
|
+
return someConfigOverrideBeforeEndOfOptions(args, (value) => {
|
|
1275
|
+
const trimmed = value.trim();
|
|
1276
|
+
return trimmed.startsWith(key) && /^\s*=/.test(trimmed.slice(key.length));
|
|
1277
|
+
});
|
|
878
1278
|
}
|
|
879
1279
|
function shouldDisableOmxMcpForTeamWorker(env) {
|
|
880
1280
|
const raw = env[OMX_TEAM_WORKER_MCP_COMPAT_ENV]?.trim().toLowerCase();
|
|
@@ -907,17 +1307,42 @@ function appendTeamWorkerMcpDisableOverrides(args, env) {
|
|
|
907
1307
|
const key = `mcp_servers.${server}.enabled`;
|
|
908
1308
|
if (hasConfigOverride(args, key))
|
|
909
1309
|
continue;
|
|
910
|
-
args.
|
|
1310
|
+
const endOfOptionsIndex = args.indexOf('--');
|
|
1311
|
+
args.splice(endOfOptionsIndex < 0 ? args.length : endOfOptionsIndex, 0, CONFIG_FLAG, `${key}=false`);
|
|
911
1312
|
}
|
|
912
1313
|
}
|
|
1314
|
+
function insertArgsBeforeEndOfOptions(args, insertedArgs) {
|
|
1315
|
+
const endOfOptionsIndex = args.indexOf('--');
|
|
1316
|
+
if (endOfOptionsIndex < 0)
|
|
1317
|
+
return [...args, ...insertedArgs];
|
|
1318
|
+
return [...args.slice(0, endOfOptionsIndex), ...insertedArgs, ...args.slice(endOfOptionsIndex)];
|
|
1319
|
+
}
|
|
1320
|
+
function insertCanonicalCodexBypassBeforeEndOfOptions(args) {
|
|
1321
|
+
const endOfOptionsIndex = args.indexOf('--');
|
|
1322
|
+
const preMarkerArgs = endOfOptionsIndex < 0 ? args : args.slice(0, endOfOptionsIndex);
|
|
1323
|
+
const postMarkerArgs = endOfOptionsIndex < 0 ? [] : args.slice(endOfOptionsIndex);
|
|
1324
|
+
return [
|
|
1325
|
+
...preMarkerArgs.filter((arg) => arg !== CODEX_BYPASS_FLAG && arg !== MADMAX_FLAG),
|
|
1326
|
+
CODEX_BYPASS_FLAG,
|
|
1327
|
+
...postMarkerArgs,
|
|
1328
|
+
];
|
|
1329
|
+
}
|
|
913
1330
|
function resolveWorkerLaunchArgs(extraArgs = [], cwd = process.cwd(), env = process.env) {
|
|
914
|
-
|
|
915
|
-
const
|
|
916
|
-
if (
|
|
917
|
-
merged
|
|
1331
|
+
let merged = [...extraArgs];
|
|
1332
|
+
const initialPolicy = classifyTeamWorkerLaunchPolicy(merged);
|
|
1333
|
+
if (initialPolicy === 'direct-policy') {
|
|
1334
|
+
merged = normalizeTeamWorkerLaunchArgs(merged);
|
|
1335
|
+
}
|
|
1336
|
+
const policy = classifyTeamWorkerLaunchPolicy(merged);
|
|
1337
|
+
const ambientWantsBypass = parseTeamWorkerLaunchArgs(process.argv, 'ambient process arguments', { directPolicyMode: 'ignore' }).wantsBypass;
|
|
1338
|
+
if (policy === 'none' || policy === 'bypass') {
|
|
1339
|
+
const wantsBypass = ambientWantsBypass || policy === 'bypass';
|
|
1340
|
+
if (wantsBypass) {
|
|
1341
|
+
merged = insertCanonicalCodexBypassBeforeEndOfOptions(merged);
|
|
1342
|
+
}
|
|
918
1343
|
}
|
|
919
1344
|
if (shouldBypassDefaultSystemPrompt(env) && !hasModelInstructionsOverride(merged)) {
|
|
920
|
-
merged
|
|
1345
|
+
merged = insertArgsBeforeEndOfOptions(merged, [CONFIG_FLAG, buildModelInstructionsOverride(cwd, env)]);
|
|
921
1346
|
}
|
|
922
1347
|
return merged;
|
|
923
1348
|
}
|
|
@@ -1040,11 +1465,15 @@ function buildWorkerProcessLaunchSpecForMode(mode, teamName, workerIndex, launch
|
|
|
1040
1465
|
const effectiveEnv = { ...process.env, ...extraEnv };
|
|
1041
1466
|
const fullLaunchArgs = resolveWorkerLaunchArgs(launchArgs, cwd, effectiveEnv);
|
|
1042
1467
|
const workerCli = workerCliOverride ?? resolveTeamWorkerCli(fullLaunchArgs, effectiveEnv);
|
|
1468
|
+
assertTeamWorkerLaunchPolicyInvariant(workerCli, fullLaunchArgs);
|
|
1043
1469
|
const cliLaunchArgs = translateWorkerLaunchArgsForCli(workerCli, fullLaunchArgs, initialPrompt, workerRole);
|
|
1470
|
+
const launchPolicy = workerCli === 'codex'
|
|
1471
|
+
? classifyTeamWorkerLaunchPolicy(cliLaunchArgs)
|
|
1472
|
+
: 'none';
|
|
1044
1473
|
const effectiveCliLaunchArgs = workerCli === 'codex'
|
|
1045
1474
|
&& shouldGrantExecutionBypassForRole(workerRole)
|
|
1046
|
-
&&
|
|
1047
|
-
?
|
|
1475
|
+
&& launchPolicy === 'none'
|
|
1476
|
+
? insertCanonicalCodexBypassBeforeEndOfOptions(cliLaunchArgs)
|
|
1048
1477
|
: cliLaunchArgs;
|
|
1049
1478
|
const workerCodexHomeOverride = typeof effectiveEnv.CODEX_HOME === 'string'
|
|
1050
1479
|
? effectiveEnv.CODEX_HOME.trim()
|
|
@@ -1160,16 +1589,35 @@ export function createTeamSession(teamName, workerCount, cwd, workerLaunchArgs =
|
|
|
1160
1589
|
}
|
|
1161
1590
|
const normalizedWorkerLaunchArgs = resolveWorkerLaunchArgs(workerLaunchArgs, cwd);
|
|
1162
1591
|
const defaultWorkerCliPlan = resolveTeamWorkerCliPlan(workerCount, normalizedWorkerLaunchArgs, process.env);
|
|
1163
|
-
const workerCliPlan =
|
|
1164
|
-
? workerStartups.map((startup, index) => startup.workerCli ?? defaultWorkerCliPlan[index])
|
|
1165
|
-
: defaultWorkerCliPlan;
|
|
1592
|
+
const workerCliPlan = Array.from({ length: workerCount }, (_, index) => workerStartups[index]?.workerCli ?? defaultWorkerCliPlan[index]);
|
|
1166
1593
|
for (const workerCli of new Set(workerCliPlan)) {
|
|
1167
1594
|
assertTeamWorkerCliBinaryAvailable(workerCli);
|
|
1168
1595
|
}
|
|
1596
|
+
const workerLaunchPolicyPlan = Array.from({ length: workerCount }, (_, index) => {
|
|
1597
|
+
const startup = workerStartups[index] ?? {};
|
|
1598
|
+
const workerCwd = startup.cwd || cwd;
|
|
1599
|
+
const workerEnv = startup.env || {};
|
|
1600
|
+
const launchArgs = startup.launchArgs || workerLaunchArgs;
|
|
1601
|
+
const effectiveLaunchArgs = resolveWorkerLaunchArgs(launchArgs, workerCwd, { ...process.env, ...workerEnv });
|
|
1602
|
+
assertTeamWorkerLaunchPolicyInvariant(workerCliPlan[index], effectiveLaunchArgs);
|
|
1603
|
+
return Object.freeze([...launchArgs]);
|
|
1604
|
+
});
|
|
1169
1605
|
const safeTeamName = sanitizeTeamName(teamName);
|
|
1170
1606
|
let registeredResizeHook = null;
|
|
1171
1607
|
let registeredClientAttachedHook = null;
|
|
1172
|
-
const
|
|
1608
|
+
const rollbackPanes = new Map();
|
|
1609
|
+
// An owner-tag command may have succeeded even if tmux reports an ambiguous
|
|
1610
|
+
// failure; rollback can only kill panes whose owner is re-authorized.
|
|
1611
|
+
const rollbackTaggedPaneOwnerIds = new Map();
|
|
1612
|
+
let partialTeamTarget = null;
|
|
1613
|
+
let partialLeaderPaneId = null;
|
|
1614
|
+
let partialLeaderPanePid;
|
|
1615
|
+
let partialTeamPaneOwnerId = '';
|
|
1616
|
+
const partialWorkerPaneIds = [];
|
|
1617
|
+
const partialWorkerPaneIdsByIndex = Array.from({ length: workerCount }, () => null);
|
|
1618
|
+
const partialWorkerPanePidsByIndex = Array.from({ length: workerCount }, () => null);
|
|
1619
|
+
let partialHudPaneId = null;
|
|
1620
|
+
let partialHudPanePid = null;
|
|
1173
1621
|
try {
|
|
1174
1622
|
const tmuxPaneTarget = process.env.TMUX_PANE;
|
|
1175
1623
|
const displayArgs = tmuxPaneTarget
|
|
@@ -1186,42 +1634,71 @@ export function createTeamSession(teamName, workerCount, cwd, workerLaunchArgs =
|
|
|
1186
1634
|
throw new Error(`failed to parse current tmux target: ${context.stdout}`);
|
|
1187
1635
|
}
|
|
1188
1636
|
const teamTarget = `${sessionName}:${windowIndex}`;
|
|
1637
|
+
partialTeamTarget = teamTarget;
|
|
1189
1638
|
const ownerSessionId = (options.ownerSessionId ?? process.env.OMX_SESSION_ID ?? '').trim();
|
|
1190
1639
|
const teamPaneOwnerId = (options.teamPaneOwnerId ?? `team:${safeTeamName}`).trim();
|
|
1640
|
+
partialTeamPaneOwnerId = teamPaneOwnerId;
|
|
1641
|
+
const paneListResult = listPanesResult(teamTarget);
|
|
1642
|
+
if (paneListResult.error)
|
|
1643
|
+
throw new Error(`failed to read tmux pane topology: ${paneListResult.error}`);
|
|
1644
|
+
const leaderPaneId = chooseTeamLeaderPaneId(paneListResult.panes, detectedLeaderPaneId);
|
|
1645
|
+
const leaderProof = readExactPaneProofSync(leaderPaneId);
|
|
1646
|
+
if (leaderProof.status === 'unavailable')
|
|
1647
|
+
throw new ExactPaneProofUnavailableError(leaderProof);
|
|
1648
|
+
if (leaderProof.status === 'gone')
|
|
1649
|
+
throw new Error(`tmux pane is not proven live: ${leaderPaneId}`);
|
|
1650
|
+
const leaderPanePid = leaderProof.pid;
|
|
1651
|
+
partialLeaderPaneId = leaderPaneId;
|
|
1652
|
+
partialLeaderPanePid = leaderPanePid;
|
|
1653
|
+
const initialHudPaneIds = findHudWatchPaneIds(paneListResult.panes, leaderPaneId, { leaderPaneId });
|
|
1654
|
+
const initialWindowPanePids = new Map([[leaderPaneId, leaderPanePid]]);
|
|
1655
|
+
const omxEntry = resolveOmxCliEntryPath();
|
|
1656
|
+
const canRecreateTeamHud = Boolean(omxEntry && omxEntry.trim() !== '');
|
|
1657
|
+
// Freeze every HUD owned by this leader even when startup cannot recreate
|
|
1658
|
+
// it; the untouched pane remains part of the authorized topology.
|
|
1659
|
+
for (const hudPaneId of initialHudPaneIds) {
|
|
1660
|
+
const proof = readExactPaneProofSync(hudPaneId);
|
|
1661
|
+
if (proof.status === 'unavailable')
|
|
1662
|
+
throw new ExactPaneProofUnavailableError(proof);
|
|
1663
|
+
if (proof.status === 'gone')
|
|
1664
|
+
throw new Error(`tmux pane is not proven live: ${hudPaneId}`);
|
|
1665
|
+
initialWindowPanePids.set(hudPaneId, proof.pid);
|
|
1666
|
+
}
|
|
1667
|
+
// Team mode prioritizes leader + worker visibility. Remove HUD panes only
|
|
1668
|
+
// when we can recreate the team HUD. Otherwise keep the existing HUD alive
|
|
1669
|
+
// instead of making it disappear on team startup failures or broken installs.
|
|
1670
|
+
requireFrozenWindowTopologySync(teamTarget, initialWindowPanePids);
|
|
1191
1671
|
if (ownerSessionId) {
|
|
1192
1672
|
const tagResult = runTmux(['set-option', '-t', sessionName, OMX_INSTANCE_OPTION, ownerSessionId]);
|
|
1193
1673
|
if (!tagResult.ok) {
|
|
1194
1674
|
throw new Error(`failed to tag tmux session ${sessionName}: ${tagResult.stderr}`);
|
|
1195
1675
|
}
|
|
1196
1676
|
}
|
|
1197
|
-
|
|
1198
|
-
|
|
1199
|
-
tagPaneInstance(leaderPaneId, ownerSessionId);
|
|
1200
|
-
tagPaneTeamOwner(leaderPaneId, teamPaneOwnerId);
|
|
1201
|
-
const initialHudPaneIds = findHudPaneIds(teamTarget, leaderPaneId);
|
|
1202
|
-
const omxEntry = resolveOmxCliEntryPath();
|
|
1203
|
-
const canRecreateTeamHud = Boolean(omxEntry && omxEntry.trim() !== '');
|
|
1204
|
-
// Team mode prioritizes leader + worker visibility. Remove HUD panes only
|
|
1205
|
-
// when we can recreate the team HUD. Otherwise keep the existing HUD alive
|
|
1206
|
-
// instead of making it disappear on team startup failures or broken installs.
|
|
1677
|
+
tagPaneInstance(leaderPaneId, ownerSessionId, leaderPanePid);
|
|
1678
|
+
tagPaneTeamOwner(leaderPaneId, teamPaneOwnerId, leaderPanePid);
|
|
1207
1679
|
if (canRecreateTeamHud) {
|
|
1208
|
-
for (const hudPaneId of
|
|
1209
|
-
|
|
1680
|
+
for (const [hudPaneId, hudPanePid] of initialWindowPanePids) {
|
|
1681
|
+
if (hudPaneId !== leaderPaneId)
|
|
1682
|
+
killExactPaneSync(hudPaneId, hudPanePid);
|
|
1210
1683
|
}
|
|
1211
1684
|
}
|
|
1212
1685
|
const workerPaneIds = [];
|
|
1686
|
+
const workerPanePidsByIndex = Array.from({ length: workerCount }, () => null);
|
|
1213
1687
|
let rightStackRootPaneId = null;
|
|
1688
|
+
let rightStackRootPanePid = null;
|
|
1689
|
+
const frozenWindowPanePids = new Map([[leaderPaneId, leaderPanePid]]);
|
|
1690
|
+
const frozenWindowPaneOwners = new Map([[leaderPaneId, teamPaneOwnerId]]);
|
|
1214
1691
|
for (let i = 1; i <= workerCount; i++) {
|
|
1215
1692
|
const startup = workerStartups[i - 1] || {};
|
|
1216
1693
|
const workerCwd = startup.cwd || cwd;
|
|
1217
1694
|
const tmuxWorkerCwd = translatePathForMsys(workerCwd);
|
|
1218
1695
|
const workerEnv = startup.env || {};
|
|
1219
|
-
const launchArgsForWorker =
|
|
1696
|
+
const launchArgsForWorker = [...(workerLaunchPolicyPlan[i - 1] ?? workerLaunchArgs)];
|
|
1220
1697
|
trustWorkerMiseConfigIfAvailable(workerCwd);
|
|
1221
1698
|
const cmd = writeWorkerStartupScriptCommand(safeTeamName, i, launchArgsForWorker, workerCwd, workerEnv, workerCliPlan[i - 1], startup.initialPrompt, startup.workerRole) ?? buildWorkerStartupCommand(safeTeamName, i, launchArgsForWorker, workerCwd, workerEnv, workerCliPlan[i - 1], startup.initialPrompt, startup.workerRole);
|
|
1222
1699
|
// First split creates the right side from leader. Remaining splits stack on the right.
|
|
1223
1700
|
const splitDirection = i === 1 ? '-h' : '-v';
|
|
1224
|
-
const splitTarget = i === 1 ? leaderPaneId : (rightStackRootPaneId ?? leaderPaneId);
|
|
1701
|
+
const splitTarget = requireLiveTeamOwnedPaneSync(i === 1 ? leaderPaneId : (rightStackRootPaneId ?? leaderPaneId), i === 1 ? leaderPanePid : (rightStackRootPanePid ?? leaderPanePid), teamPaneOwnerId);
|
|
1225
1702
|
const split = runTmux([
|
|
1226
1703
|
'split-window',
|
|
1227
1704
|
splitDirection,
|
|
@@ -1242,17 +1719,37 @@ export function createTeamSession(teamName, workerCount, cwd, workerLaunchArgs =
|
|
|
1242
1719
|
if (!paneId || !paneId.startsWith('%')) {
|
|
1243
1720
|
throw new Error(`failed to capture worker pane id for worker ${i}`);
|
|
1244
1721
|
}
|
|
1245
|
-
|
|
1722
|
+
// The pane exists once split-window returns its concrete ID. Persist it in
|
|
1723
|
+
// rollback/partial state before its first proof: a malformed or unavailable
|
|
1724
|
+
// topology must retain cleanup debt, but cannot authorize an effect.
|
|
1725
|
+
rollbackPanes.set(paneId, null);
|
|
1726
|
+
partialWorkerPaneIds.push(paneId);
|
|
1727
|
+
partialWorkerPaneIdsByIndex[i - 1] = paneId;
|
|
1728
|
+
const paneProof = readExactPaneProofSync(paneId);
|
|
1729
|
+
if (paneProof.status === 'unavailable')
|
|
1730
|
+
throw new ExactPaneProofUnavailableError(paneProof);
|
|
1731
|
+
if (paneProof.status === 'gone')
|
|
1732
|
+
throw new Error(`tmux pane is not proven live: ${paneId}`);
|
|
1733
|
+
const panePid = paneProof.pid;
|
|
1734
|
+
rollbackPanes.set(paneId, panePid);
|
|
1735
|
+
partialWorkerPanePidsByIndex[i - 1] = panePid;
|
|
1736
|
+
workerPanePidsByIndex[i - 1] = panePid;
|
|
1737
|
+
frozenWindowPanePids.set(paneId, panePid);
|
|
1246
1738
|
if (isNativeWindows() && !waitForPaneToRemainPresent(teamTarget, paneId)) {
|
|
1247
1739
|
throw new Error(`worker pane ${i} did not remain present after tmux split-window returned ${paneId}`);
|
|
1248
1740
|
}
|
|
1249
|
-
tagPaneInstance(paneId, ownerSessionId);
|
|
1250
|
-
|
|
1741
|
+
tagPaneInstance(paneId, ownerSessionId, panePid);
|
|
1742
|
+
rollbackTaggedPaneOwnerIds.set(paneId, teamPaneOwnerId);
|
|
1743
|
+
tagPaneTeamOwner(paneId, teamPaneOwnerId, panePid);
|
|
1744
|
+
frozenWindowPaneOwners.set(paneId, teamPaneOwnerId);
|
|
1251
1745
|
workerPaneIds.push(paneId);
|
|
1252
|
-
if (i === 1)
|
|
1746
|
+
if (i === 1) {
|
|
1253
1747
|
rightStackRootPaneId = paneId;
|
|
1748
|
+
rightStackRootPanePid = panePid;
|
|
1749
|
+
}
|
|
1254
1750
|
}
|
|
1255
1751
|
// Keep leader as full left/main pane; workers stay stacked on the right.
|
|
1752
|
+
requireFrozenWindowTopologySync(teamTarget, frozenWindowPanePids, frozenWindowPaneOwners);
|
|
1256
1753
|
runTmux(['select-layout', '-t', teamTarget, 'main-vertical']);
|
|
1257
1754
|
// Force leader pane to use half the window width.
|
|
1258
1755
|
const windowWidthResult = runTmux(['display-message', '-p', '-t', teamTarget, '#{window_width}']);
|
|
@@ -1260,7 +1757,9 @@ export function createTeamSession(teamName, workerCount, cwd, workerLaunchArgs =
|
|
|
1260
1757
|
const width = Number.parseInt(windowWidthResult.stdout.split('\n')[0]?.trim() || '', 10);
|
|
1261
1758
|
if (Number.isFinite(width) && width >= 40) {
|
|
1262
1759
|
const half = String(Math.floor(width / 2));
|
|
1760
|
+
requireFrozenWindowTopologySync(teamTarget, frozenWindowPanePids, frozenWindowPaneOwners);
|
|
1263
1761
|
runTmux(['set-window-option', '-t', teamTarget, 'main-pane-width', half]);
|
|
1762
|
+
requireFrozenWindowTopologySync(teamTarget, frozenWindowPanePids, frozenWindowPaneOwners);
|
|
1264
1763
|
runTmux(['select-layout', '-t', teamTarget, 'main-vertical']);
|
|
1265
1764
|
}
|
|
1266
1765
|
}
|
|
@@ -1274,23 +1773,39 @@ export function createTeamSession(teamName, workerCount, cwd, workerLaunchArgs =
|
|
|
1274
1773
|
if (canRecreateTeamHud && omxEntry) {
|
|
1275
1774
|
const hudCmd = `exec env ${formatHudEnvAssignments(process.env, { sessionId: ownerSessionId, leaderPaneId })} node ${shellQuoteSingle(translatePathForMsys(omxEntry))} hud --watch`;
|
|
1276
1775
|
const hudCwd = translatePathForMsys(cwd);
|
|
1776
|
+
requireFrozenWindowTopologySync(teamTarget, frozenWindowPanePids, frozenWindowPaneOwners);
|
|
1777
|
+
const hudSplitTarget = leaderPaneId;
|
|
1277
1778
|
const hudResult = runTmux([
|
|
1278
|
-
'split-window', '-v', '-f', '-l', String(HUD_TMUX_TEAM_HEIGHT_LINES), '-t',
|
|
1779
|
+
'split-window', '-v', '-f', '-l', String(HUD_TMUX_TEAM_HEIGHT_LINES), '-t', hudSplitTarget, '-d', '-P', '-F', '#{pane_id}', '-c', hudCwd, hudCmd,
|
|
1279
1780
|
]);
|
|
1280
1781
|
if (hudResult.ok) {
|
|
1281
1782
|
const id = hudResult.stdout.split('\n')[0]?.trim() ?? '';
|
|
1282
1783
|
if (id.startsWith('%')) {
|
|
1283
|
-
|
|
1784
|
+
// split-window has created a concrete pane even though no proof has
|
|
1785
|
+
// authorized an effect yet. Retain PID-less cleanup debt first.
|
|
1786
|
+
rollbackPanes.set(id, null);
|
|
1787
|
+
partialHudPaneId = id;
|
|
1788
|
+
partialHudPanePid = null;
|
|
1789
|
+
const hudProof = readExactPaneProofSync(id);
|
|
1790
|
+
if (hudProof.status === 'unavailable')
|
|
1791
|
+
throw new ExactPaneProofUnavailableError(hudProof);
|
|
1792
|
+
if (hudProof.status === 'gone')
|
|
1793
|
+
throw new Error(`tmux pane is not proven live: ${id}`);
|
|
1794
|
+
const hudPanePid = hudProof.pid;
|
|
1795
|
+
frozenWindowPanePids.set(id, hudPanePid);
|
|
1796
|
+
rollbackPanes.set(id, hudPanePid);
|
|
1797
|
+
partialHudPanePid = hudPanePid;
|
|
1284
1798
|
if (isNativeWindows() && !waitForPaneToRemainPresent(teamTarget, id)) {
|
|
1285
1799
|
throw new Error(`HUD pane did not remain present after tmux split-window returned ${id}`);
|
|
1286
1800
|
}
|
|
1287
|
-
tagPaneInstance(id, ownerSessionId);
|
|
1288
|
-
|
|
1801
|
+
tagPaneInstance(id, ownerSessionId, hudPanePid);
|
|
1802
|
+
rollbackTaggedPaneOwnerIds.set(id, teamPaneOwnerId);
|
|
1803
|
+
tagPaneTeamOwner(id, teamPaneOwnerId, hudPanePid);
|
|
1804
|
+
frozenWindowPaneOwners.set(id, teamPaneOwnerId);
|
|
1289
1805
|
hudPaneId = id;
|
|
1290
1806
|
if (isNativeWindows()) {
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
const reconcile = runTmux(buildHudResizeArgs(hudPaneId));
|
|
1807
|
+
const provenHudPaneId = requireLiveTeamOwnedPaneSync(hudPaneId, hudPanePid, teamPaneOwnerId);
|
|
1808
|
+
const reconcile = runTmux(buildHudResizeArgs(provenHudPaneId));
|
|
1294
1809
|
if (!reconcile.ok) {
|
|
1295
1810
|
throw new Error(`failed to reconcile HUD resize: ${reconcile.stderr}`);
|
|
1296
1811
|
}
|
|
@@ -1298,36 +1813,50 @@ export function createTeamSession(teamName, workerCount, cwd, workerLaunchArgs =
|
|
|
1298
1813
|
else {
|
|
1299
1814
|
const hookTarget = buildResizeHookTarget(sessionName, windowIndex);
|
|
1300
1815
|
const hookName = buildResizeHookName(safeTeamName, sessionName, windowIndex, hudPaneId);
|
|
1301
|
-
|
|
1816
|
+
requireLiveTeamOwnedPaneSync(hudPaneId, hudPanePid, teamPaneOwnerId);
|
|
1817
|
+
const registerHook = runTmux(buildRegisterResizeHookArgs(hookTarget, hookName, hudPaneId, HUD_TMUX_TEAM_HEIGHT_LINES, hudPanePid, teamPaneOwnerId));
|
|
1302
1818
|
const clientAttachedHookName = buildClientAttachedReconcileHookName(safeTeamName, sessionName, windowIndex, hudPaneId);
|
|
1303
1819
|
if (registerHook.ok) {
|
|
1304
1820
|
resizeHookTarget = hookTarget;
|
|
1305
1821
|
resizeHookName = hookName;
|
|
1306
|
-
registeredResizeHook = {
|
|
1822
|
+
registeredResizeHook = {
|
|
1823
|
+
name: resizeHookName,
|
|
1824
|
+
target: resizeHookTarget,
|
|
1825
|
+
leaderPaneId,
|
|
1826
|
+
leaderPanePid,
|
|
1827
|
+
hudPaneId,
|
|
1828
|
+
hudPanePid,
|
|
1829
|
+
teamPaneOwnerId,
|
|
1830
|
+
};
|
|
1307
1831
|
}
|
|
1308
1832
|
else {
|
|
1309
|
-
// tmux versions/builds that reject indexed client-resized hooks should not
|
|
1310
|
-
// abort madmax/team startup after panes were successfully created. Keep the
|
|
1311
|
-
// fallback narrow: skip only the long-lived resize hook metadata, then
|
|
1312
|
-
// still try the one-shot client-attached reconcile plus the explicit
|
|
1313
|
-
// delayed/direct resize checks below so real tmux/run-shell failures
|
|
1314
|
-
// still surface.
|
|
1315
1833
|
console.warn(`[omx] tmux resize hook unavailable for ${hookTarget} (${hookName}): ${registerHook.stderr}; `
|
|
1316
1834
|
+ 'continuing with best-effort HUD resize fallback.');
|
|
1317
1835
|
}
|
|
1318
|
-
|
|
1836
|
+
requireLiveTeamOwnedPaneSync(hudPaneId, hudPanePid, teamPaneOwnerId);
|
|
1837
|
+
const registerClientAttachedHook = runTmux(buildRegisterClientAttachedReconcileArgs(hookTarget, clientAttachedHookName, hudPaneId, HUD_TMUX_TEAM_HEIGHT_LINES, hudPanePid, teamPaneOwnerId));
|
|
1319
1838
|
if (registerClientAttachedHook.ok) {
|
|
1320
|
-
registeredClientAttachedHook = {
|
|
1839
|
+
registeredClientAttachedHook = {
|
|
1840
|
+
name: clientAttachedHookName,
|
|
1841
|
+
target: hookTarget,
|
|
1842
|
+
leaderPaneId,
|
|
1843
|
+
leaderPanePid,
|
|
1844
|
+
hudPaneId,
|
|
1845
|
+
hudPanePid,
|
|
1846
|
+
teamPaneOwnerId,
|
|
1847
|
+
};
|
|
1321
1848
|
}
|
|
1322
1849
|
else {
|
|
1323
1850
|
console.warn(`[omx] tmux client-attached resize fallback unavailable for ${hookTarget} `
|
|
1324
1851
|
+ `(${clientAttachedHookName}): ${registerClientAttachedHook.stderr}; continuing with delayed HUD resize fallback.`);
|
|
1325
1852
|
}
|
|
1326
|
-
|
|
1853
|
+
requireLiveTeamOwnedPaneSync(hudPaneId, hudPanePid, teamPaneOwnerId);
|
|
1854
|
+
const delayed = runTmux(buildScheduleDelayedHudResizeArgs(hudPaneId, HUD_RESIZE_RECONCILE_DELAY_SECONDS, HUD_TMUX_TEAM_HEIGHT_LINES, hudPanePid, teamPaneOwnerId));
|
|
1327
1855
|
if (!delayed.ok) {
|
|
1328
1856
|
console.warn(`[omx] tmux delayed HUD resize unavailable for ${hudPaneId}: ${delayed.stderr}; continuing.`);
|
|
1329
1857
|
}
|
|
1330
|
-
|
|
1858
|
+
requireLiveTeamOwnedPaneSync(hudPaneId, hudPanePid, teamPaneOwnerId);
|
|
1859
|
+
const reconcile = runTmux(buildReconcileHudResizeArgs(hudPaneId, HUD_TMUX_TEAM_HEIGHT_LINES, hudPanePid, teamPaneOwnerId));
|
|
1331
1860
|
if (!reconcile.ok) {
|
|
1332
1861
|
console.warn(`[omx] tmux HUD resize reconcile unavailable for ${hudPaneId}: ${reconcile.stderr}; continuing.`);
|
|
1333
1862
|
}
|
|
@@ -1335,21 +1864,31 @@ export function createTeamSession(teamName, workerCount, cwd, workerLaunchArgs =
|
|
|
1335
1864
|
}
|
|
1336
1865
|
}
|
|
1337
1866
|
}
|
|
1338
|
-
runTmux(['select-pane', '-t', leaderPaneId]);
|
|
1339
|
-
redrawLeaderPaneAfterTeamLayout(leaderPaneId);
|
|
1867
|
+
runTmux(['select-pane', '-t', requireLiveTeamOwnedPaneSync(leaderPaneId, leaderPanePid, teamPaneOwnerId)]);
|
|
1868
|
+
redrawLeaderPaneAfterTeamLayout(leaderPaneId, leaderPanePid, teamPaneOwnerId);
|
|
1340
1869
|
sleepSeconds(0.5);
|
|
1341
1870
|
// Enable mouse scrolling so agent output panes can be scrolled with the
|
|
1342
1871
|
// mouse wheel without conflicting with keyboard up/down arrow-key input
|
|
1343
1872
|
// history navigation in the Codex CLI input field. (issue #103)
|
|
1344
1873
|
// Opt-out: set OMX_TEAM_MOUSE=0 in the environment.
|
|
1345
1874
|
if (process.env.OMX_TEAM_MOUSE !== '0') {
|
|
1346
|
-
enableMouseScrolling(sessionName);
|
|
1875
|
+
enableMouseScrolling(sessionName, () => { requireLiveTeamOwnedPaneSync(leaderPaneId, leaderPanePid, teamPaneOwnerId); });
|
|
1876
|
+
}
|
|
1877
|
+
const sessionIncarnation = teamTarget.includes(':') ? null : queryDetachedTeamSession(sessionName);
|
|
1878
|
+
if (sessionIncarnation && sessionIncarnation.status !== 'exact') {
|
|
1879
|
+
throw new Error(`tmux_session_incarnation_unavailable:${sessionName}`);
|
|
1347
1880
|
}
|
|
1348
1881
|
return {
|
|
1349
1882
|
name: teamTarget,
|
|
1883
|
+
tmuxSessionId: sessionIncarnation?.incarnation.sessionId,
|
|
1884
|
+
tmuxSessionCreated: sessionIncarnation?.incarnation.sessionCreated,
|
|
1350
1885
|
workerCount,
|
|
1351
1886
|
cwd,
|
|
1352
1887
|
workerPaneIds,
|
|
1888
|
+
workerPaneIdsByIndex: [...workerPaneIds],
|
|
1889
|
+
workerPanePidsByIndex,
|
|
1890
|
+
leaderPanePid,
|
|
1891
|
+
hudPanePid: partialHudPanePid,
|
|
1353
1892
|
leaderPaneId,
|
|
1354
1893
|
hudPaneId,
|
|
1355
1894
|
resizeHookName,
|
|
@@ -1358,14 +1897,82 @@ export function createTeamSession(teamName, workerCount, cwd, workerLaunchArgs =
|
|
|
1358
1897
|
};
|
|
1359
1898
|
}
|
|
1360
1899
|
catch (error) {
|
|
1361
|
-
|
|
1362
|
-
|
|
1900
|
+
const cleanupErrors = [];
|
|
1901
|
+
const unregisterAuthorizedHook = (hook, unregister, label) => {
|
|
1902
|
+
try {
|
|
1903
|
+
requireLiveTeamOwnedPaneSync(hook.leaderPaneId, hook.leaderPanePid, hook.teamPaneOwnerId);
|
|
1904
|
+
requireLiveTeamOwnedPaneSync(hook.hudPaneId, hook.hudPanePid, hook.teamPaneOwnerId);
|
|
1905
|
+
const unregistered = runTmux(unregister(hook.target, hook.name));
|
|
1906
|
+
if (unregistered.ok)
|
|
1907
|
+
return true;
|
|
1908
|
+
cleanupErrors.push(`failed to unregister tmux ${label} hook ${hook.name}: ${unregistered.stderr}`);
|
|
1909
|
+
}
|
|
1910
|
+
catch (cleanupError) {
|
|
1911
|
+
cleanupErrors.push(`unable to authorize tmux ${label} hook cleanup ${hook.name}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
|
|
1912
|
+
}
|
|
1913
|
+
return false;
|
|
1914
|
+
};
|
|
1915
|
+
if (registeredClientAttachedHook && unregisterAuthorizedHook(registeredClientAttachedHook, buildUnregisterClientAttachedReconcileArgs, 'client-attached')) {
|
|
1916
|
+
registeredClientAttachedHook = null;
|
|
1363
1917
|
}
|
|
1364
|
-
if (registeredResizeHook) {
|
|
1365
|
-
|
|
1918
|
+
if (registeredResizeHook && unregisterAuthorizedHook(registeredResizeHook, buildUnregisterResizeHookArgs, 'resize')) {
|
|
1919
|
+
registeredResizeHook = null;
|
|
1366
1920
|
}
|
|
1367
|
-
|
|
1368
|
-
|
|
1921
|
+
const proofUnavailable = [];
|
|
1922
|
+
if (error instanceof ExactPaneProofUnavailableError)
|
|
1923
|
+
proofUnavailable.push(error.proof);
|
|
1924
|
+
const unresolvedPaneIds = new Set(rollbackPanes.keys());
|
|
1925
|
+
for (const [paneId, panePid] of rollbackPanes) {
|
|
1926
|
+
// split-window IDs without a positive exact PID proof are cleanup debt,
|
|
1927
|
+
// never authorization to affect a potentially recycled pane ID.
|
|
1928
|
+
if (panePid === null)
|
|
1929
|
+
continue;
|
|
1930
|
+
try {
|
|
1931
|
+
const expectedOwnerId = rollbackTaggedPaneOwnerIds.get(paneId);
|
|
1932
|
+
killExactPaneSync(paneId, panePid, expectedOwnerId
|
|
1933
|
+
? () => { requireLiveTeamOwnedPaneSync(paneId, panePid, expectedOwnerId); }
|
|
1934
|
+
: undefined);
|
|
1935
|
+
unresolvedPaneIds.delete(paneId);
|
|
1936
|
+
}
|
|
1937
|
+
catch (cleanupError) {
|
|
1938
|
+
if (cleanupError instanceof ExactPaneProofUnavailableError) {
|
|
1939
|
+
proofUnavailable.push(cleanupError.proof);
|
|
1940
|
+
break;
|
|
1941
|
+
}
|
|
1942
|
+
else {
|
|
1943
|
+
cleanupErrors.push(cleanupError instanceof Error ? cleanupError.message : String(cleanupError));
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
const unresolvedWorkerPaneIds = partialWorkerPaneIds.filter((paneId) => unresolvedPaneIds.has(paneId));
|
|
1948
|
+
const unresolvedWorkerPaneIdsByIndex = partialWorkerPaneIdsByIndex
|
|
1949
|
+
.map((paneId) => paneId && unresolvedPaneIds.has(paneId) ? paneId : null);
|
|
1950
|
+
const unresolvedWorkerPanePidsByIndex = partialWorkerPanePidsByIndex
|
|
1951
|
+
.map((panePid, index) => partialWorkerPaneIdsByIndex[index] && unresolvedPaneIds.has(partialWorkerPaneIdsByIndex[index]) ? panePid : null);
|
|
1952
|
+
const unresolvedHudPaneId = partialHudPaneId && (unresolvedPaneIds.has(partialHudPaneId)
|
|
1953
|
+
|| registeredResizeHook !== null
|
|
1954
|
+
|| registeredClientAttachedHook !== null)
|
|
1955
|
+
? partialHudPaneId
|
|
1956
|
+
: null;
|
|
1957
|
+
const hasRecoverablePartialArtifact = unresolvedPaneIds.size > 0
|
|
1958
|
+
|| registeredResizeHook !== null
|
|
1959
|
+
|| registeredClientAttachedHook !== null;
|
|
1960
|
+
if (hasRecoverablePartialArtifact && partialTeamTarget && partialLeaderPaneId) {
|
|
1961
|
+
throw new CreateTeamSessionPartialError({
|
|
1962
|
+
name: partialTeamTarget,
|
|
1963
|
+
workerCount,
|
|
1964
|
+
cwd,
|
|
1965
|
+
workerPaneIds: unresolvedWorkerPaneIds,
|
|
1966
|
+
workerPaneIdsByIndex: unresolvedWorkerPaneIdsByIndex,
|
|
1967
|
+
workerPanePidsByIndex: unresolvedWorkerPanePidsByIndex,
|
|
1968
|
+
leaderPanePid: partialLeaderPanePid,
|
|
1969
|
+
hudPanePid: unresolvedHudPaneId ? partialHudPanePid : null,
|
|
1970
|
+
leaderPaneId: partialLeaderPaneId,
|
|
1971
|
+
hudPaneId: unresolvedHudPaneId,
|
|
1972
|
+
resizeHookName: registeredResizeHook?.name ?? null,
|
|
1973
|
+
resizeHookTarget: registeredResizeHook?.target ?? null,
|
|
1974
|
+
teamPaneOwnerId: partialTeamPaneOwnerId,
|
|
1975
|
+
}, proofUnavailable, error, cleanupErrors);
|
|
1369
1976
|
}
|
|
1370
1977
|
throw error;
|
|
1371
1978
|
}
|
|
@@ -1374,34 +1981,73 @@ export function restoreStandaloneHudPane(leaderPaneId, cwd, options = {}) {
|
|
|
1374
1981
|
const normalizedLeaderPaneId = normalizePaneTarget(leaderPaneId);
|
|
1375
1982
|
if (!normalizedLeaderPaneId)
|
|
1376
1983
|
return null;
|
|
1984
|
+
// The split is irreversible without a durable cleanup obligation. Validate
|
|
1985
|
+
// its canonical Team-root location before authorizing any pane effect.
|
|
1986
|
+
restoredHudCleanupDebtPath(cwd, options.stateRoot);
|
|
1377
1987
|
const omxEntry = resolveOmxCliEntryPath();
|
|
1378
1988
|
if (!omxEntry || omxEntry.trim() === '')
|
|
1379
1989
|
return null;
|
|
1380
|
-
const
|
|
1990
|
+
const leaderPanePid = (() => {
|
|
1991
|
+
const proof = readExactPaneProofSync(normalizedLeaderPaneId);
|
|
1992
|
+
if (proof.status === 'unavailable')
|
|
1993
|
+
throw new ExactPaneProofUnavailableError(proof);
|
|
1994
|
+
if (proof.status === 'gone')
|
|
1995
|
+
throw new Error(`tmux pane is not proven live: ${normalizedLeaderPaneId}`);
|
|
1996
|
+
if (options.expectedLeaderPanePid !== undefined && proof.pid !== options.expectedLeaderPanePid) {
|
|
1997
|
+
throw new Error(`tmux pane identity changed: ${normalizedLeaderPaneId}`);
|
|
1998
|
+
}
|
|
1999
|
+
return proof.pid;
|
|
2000
|
+
})();
|
|
2001
|
+
const requireAuthorizedLeaderPane = () => {
|
|
2002
|
+
options.assertLeaderPaneAuthorization?.();
|
|
2003
|
+
return requireLiveExactPaneSync(normalizedLeaderPaneId, options.expectedLeaderPanePid ?? leaderPanePid);
|
|
2004
|
+
};
|
|
2005
|
+
requireAuthorizedLeaderPane();
|
|
2006
|
+
const paneListResult = listPanesResult(normalizedLeaderPaneId);
|
|
2007
|
+
if (paneListResult.error)
|
|
2008
|
+
throw new Error(`failed to read tmux pane topology: ${paneListResult.error}`);
|
|
2009
|
+
const [existingHudPaneId, ...duplicateHudPaneIds] = findHudWatchPaneIds(paneListResult.panes, normalizedLeaderPaneId, { leaderPaneId: normalizedLeaderPaneId });
|
|
2010
|
+
const ownedHudPanePids = new Map();
|
|
2011
|
+
for (const paneId of [existingHudPaneId, ...duplicateHudPaneIds]) {
|
|
2012
|
+
if (!paneId)
|
|
2013
|
+
continue;
|
|
2014
|
+
const proof = readExactPaneProofSync(paneId);
|
|
2015
|
+
if (proof.status === 'unavailable')
|
|
2016
|
+
throw new ExactPaneProofUnavailableError(proof);
|
|
2017
|
+
if (proof.status === 'gone')
|
|
2018
|
+
throw new Error(`tmux pane is not proven live: ${paneId}`);
|
|
2019
|
+
ownedHudPanePids.set(paneId, proof.pid);
|
|
2020
|
+
}
|
|
1381
2021
|
for (const paneId of duplicateHudPaneIds) {
|
|
1382
|
-
|
|
2022
|
+
killExactPaneSync(paneId, ownedHudPanePids.get(paneId), requireAuthorizedLeaderPane);
|
|
1383
2023
|
}
|
|
1384
2024
|
if (existingHudPaneId) {
|
|
1385
2025
|
if (isNativeWindows()) {
|
|
1386
|
-
|
|
2026
|
+
const exactHudPaneId = requireLiveExactPaneSync(existingHudPaneId, ownedHudPanePids.get(existingHudPaneId));
|
|
2027
|
+
requireAuthorizedLeaderPane();
|
|
2028
|
+
const reconcile = runTmux(buildHudResizeArgs(exactHudPaneId));
|
|
2029
|
+
if (!reconcile.ok)
|
|
2030
|
+
throw new Error(`failed to reconcile standalone HUD resize: ${reconcile.stderr}`);
|
|
1387
2031
|
}
|
|
1388
2032
|
else {
|
|
1389
|
-
|
|
1390
|
-
runTmux(
|
|
2033
|
+
requireAuthorizedLeaderPane();
|
|
2034
|
+
runTmux(buildScheduleDelayedHudResizeArgs(existingHudPaneId, HUD_RESIZE_RECONCILE_DELAY_SECONDS, HUD_TMUX_TEAM_HEIGHT_LINES, ownedHudPanePids.get(existingHudPaneId)));
|
|
2035
|
+
requireAuthorizedLeaderPane();
|
|
2036
|
+
runTmux(buildReconcileHudResizeArgs(existingHudPaneId, HUD_TMUX_TEAM_HEIGHT_LINES, ownedHudPanePids.get(existingHudPaneId)));
|
|
1391
2037
|
}
|
|
1392
|
-
runTmux(['select-pane', '-t',
|
|
2038
|
+
runTmux(['select-pane', '-t', requireAuthorizedLeaderPane()]);
|
|
1393
2039
|
return existingHudPaneId;
|
|
1394
2040
|
}
|
|
1395
2041
|
const hudCmd = `exec env ${formatHudEnvAssignments(process.env, { sessionId: options.sessionId, leaderPaneId: normalizedLeaderPaneId })} ${shellQuoteSingle(translatePathForMsys(resolveLeaderNodePath()))} ${shellQuoteSingle(translatePathForMsys(omxEntry))} hud --watch`;
|
|
1396
2042
|
let hudResult = null;
|
|
1397
|
-
for (const restoreCwd of resolveStandaloneHudRestoreCwdCandidates(normalizedLeaderPaneId, cwd, options.cwd)) {
|
|
2043
|
+
for (const restoreCwd of resolveStandaloneHudRestoreCwdCandidates(normalizedLeaderPaneId, cwd, options.cwd, requireAuthorizedLeaderPane)) {
|
|
1398
2044
|
const candidateResult = runTmux([
|
|
1399
2045
|
'split-window',
|
|
1400
2046
|
'-v',
|
|
1401
2047
|
'-l',
|
|
1402
2048
|
String(HUD_TMUX_TEAM_HEIGHT_LINES),
|
|
1403
2049
|
'-t',
|
|
1404
|
-
|
|
2050
|
+
requireAuthorizedLeaderPane(),
|
|
1405
2051
|
'-d',
|
|
1406
2052
|
'-P',
|
|
1407
2053
|
'-F',
|
|
@@ -1420,14 +2066,48 @@ export function restoreStandaloneHudPane(leaderPaneId, cwd, options = {}) {
|
|
|
1420
2066
|
const paneId = hudResult.stdout.split('\n')[0]?.trim() ?? '';
|
|
1421
2067
|
if (!paneId.startsWith('%'))
|
|
1422
2068
|
return null;
|
|
2069
|
+
persistRestoredHudCleanupDebtSync(cwd, {
|
|
2070
|
+
schema_version: 1,
|
|
2071
|
+
operation: 'restored_hud_cleanup',
|
|
2072
|
+
pane_id: paneId,
|
|
2073
|
+
pane_pid: null,
|
|
2074
|
+
leader_pane_id: normalizedLeaderPaneId,
|
|
2075
|
+
leader_pane_pid: options.expectedLeaderPanePid ?? leaderPanePid,
|
|
2076
|
+
leader_pane_owner_id: options.expectedLeaderPaneOwnerId?.trim() || null,
|
|
2077
|
+
hud_owner_leader_pane_id: normalizedLeaderPaneId,
|
|
2078
|
+
}, options.stateRoot);
|
|
2079
|
+
const hudPanePid = (() => {
|
|
2080
|
+
const proof = readExactPaneProofSync(paneId);
|
|
2081
|
+
if (proof.status === 'unavailable')
|
|
2082
|
+
throw new ExactPaneProofUnavailableError(proof);
|
|
2083
|
+
if (proof.status === 'gone')
|
|
2084
|
+
throw new Error(`tmux pane is not proven live: ${paneId}`);
|
|
2085
|
+
return proof.pid;
|
|
2086
|
+
})();
|
|
2087
|
+
persistRestoredHudCleanupDebtSync(cwd, {
|
|
2088
|
+
schema_version: 1,
|
|
2089
|
+
operation: 'restored_hud_cleanup',
|
|
2090
|
+
pane_id: paneId,
|
|
2091
|
+
pane_pid: hudPanePid,
|
|
2092
|
+
leader_pane_id: normalizedLeaderPaneId,
|
|
2093
|
+
leader_pane_pid: options.expectedLeaderPanePid ?? leaderPanePid,
|
|
2094
|
+
leader_pane_owner_id: options.expectedLeaderPaneOwnerId?.trim() || null,
|
|
2095
|
+
hud_owner_leader_pane_id: normalizedLeaderPaneId,
|
|
2096
|
+
}, options.stateRoot);
|
|
1423
2097
|
if (isNativeWindows()) {
|
|
1424
|
-
|
|
2098
|
+
const exactHudPaneId = requireLiveExactPaneSync(paneId, hudPanePid);
|
|
2099
|
+
requireAuthorizedLeaderPane();
|
|
2100
|
+
const reconcile = runTmux(buildHudResizeArgs(exactHudPaneId));
|
|
2101
|
+
if (!reconcile.ok)
|
|
2102
|
+
throw new Error(`failed to reconcile standalone HUD resize: ${reconcile.stderr}`);
|
|
1425
2103
|
}
|
|
1426
2104
|
else {
|
|
1427
|
-
|
|
1428
|
-
runTmux(
|
|
2105
|
+
requireAuthorizedLeaderPane();
|
|
2106
|
+
runTmux(buildScheduleDelayedHudResizeArgs(paneId, HUD_RESIZE_RECONCILE_DELAY_SECONDS, HUD_TMUX_TEAM_HEIGHT_LINES, hudPanePid));
|
|
2107
|
+
requireAuthorizedLeaderPane();
|
|
2108
|
+
runTmux(buildReconcileHudResizeArgs(paneId, HUD_TMUX_TEAM_HEIGHT_LINES, hudPanePid));
|
|
1429
2109
|
}
|
|
1430
|
-
runTmux(['select-pane', '-t',
|
|
2110
|
+
runTmux(['select-pane', '-t', requireAuthorizedLeaderPane()]);
|
|
1431
2111
|
return paneId;
|
|
1432
2112
|
}
|
|
1433
2113
|
/**
|
|
@@ -1439,21 +2119,21 @@ export function restoreStandaloneHudPane(leaderPaneId, cwd, options = {}) {
|
|
|
1439
2119
|
* oh-my-tmux, or other sessions. Returns true if the session mouse option
|
|
1440
2120
|
* was set successfully, false otherwise.
|
|
1441
2121
|
*/
|
|
1442
|
-
export function enableMouseScrolling(sessionTarget) {
|
|
2122
|
+
export function enableMouseScrolling(sessionTarget, beforeEffect) {
|
|
2123
|
+
beforeEffect?.();
|
|
1443
2124
|
const result = runTmux(['set-option', '-t', sessionTarget, 'mouse', 'on']);
|
|
1444
2125
|
if (!result.ok)
|
|
1445
2126
|
return false;
|
|
1446
2127
|
// Enable OSC 52 so copy-selection-and-cancel propagates selected text to
|
|
1447
2128
|
// the terminal's clipboard without requiring xclip or pbcopy. (closes #206)
|
|
2129
|
+
beforeEffect?.();
|
|
1448
2130
|
runTmux(['set-option', '-t', sessionTarget, 'set-clipboard', 'on']);
|
|
1449
2131
|
// Mouse selection enters tmux copy-mode. Keep the mitigation session-scoped
|
|
1450
2132
|
// so OMX does not mutate users' global tmux style defaults. (issue #1448)
|
|
1451
|
-
mitigateCopyModeUnderlineArtifacts(sessionTarget);
|
|
2133
|
+
mitigateCopyModeUnderlineArtifacts(sessionTarget, beforeEffect);
|
|
1452
2134
|
return true;
|
|
1453
2135
|
}
|
|
1454
|
-
function paneTarget(sessionName, workerIndex
|
|
1455
|
-
if (workerPaneId && workerPaneId.startsWith('%'))
|
|
1456
|
-
return workerPaneId;
|
|
2136
|
+
function paneTarget(sessionName, workerIndex) {
|
|
1457
2137
|
if (sessionName.includes(':')) {
|
|
1458
2138
|
return `${sessionName}.${workerIndex}`;
|
|
1459
2139
|
}
|
|
@@ -1499,24 +2179,50 @@ export function evaluateStartupDirectTriggerSafetyCapture(captured, workerCli) {
|
|
|
1499
2179
|
export async function evaluateStartupDirectTriggerSafety(sessionName, workerIndex, workerPaneId, workerCli) {
|
|
1500
2180
|
if (!isTmuxAvailable())
|
|
1501
2181
|
return { safe: false, reason: 'tmux_unavailable' };
|
|
1502
|
-
const target =
|
|
2182
|
+
const target = await resolveWorkerPaneTargetAsync(sessionName, workerIndex, workerPaneId);
|
|
2183
|
+
if (!target)
|
|
2184
|
+
return { safe: false, reason: 'capture_failed' };
|
|
1503
2185
|
const result = await runTmuxAsync(sharedBuildVisibleCapturePaneArgv(target));
|
|
1504
2186
|
if (!result.ok)
|
|
1505
2187
|
return { safe: false, reason: 'capture_failed' };
|
|
1506
2188
|
return evaluateStartupDirectTriggerSafetyCapture(result.stdout, workerCli);
|
|
1507
2189
|
}
|
|
1508
|
-
function acceptClaudeBypassPermissionsPrompt(
|
|
1509
|
-
|
|
2190
|
+
function acceptClaudeBypassPermissionsPrompt(resolveTarget) {
|
|
2191
|
+
const literalTarget = resolveTarget();
|
|
2192
|
+
if (!literalTarget)
|
|
2193
|
+
return false;
|
|
2194
|
+
runTmux(['send-keys', '-t', literalTarget, '-l', '--', '2']);
|
|
1510
2195
|
sleepFractionalSeconds(0.12);
|
|
1511
|
-
|
|
2196
|
+
const submitTarget = resolveTarget();
|
|
2197
|
+
if (!submitTarget)
|
|
2198
|
+
return false;
|
|
2199
|
+
runTmux(['send-keys', '-t', submitTarget, 'C-m']);
|
|
2200
|
+
return true;
|
|
1512
2201
|
}
|
|
1513
|
-
function dismissClaudeBypassPermissionsPromptIfPresent(
|
|
2202
|
+
function dismissClaudeBypassPermissionsPromptIfPresent(resolveTarget, captured) {
|
|
1514
2203
|
if (process.env.OMX_TEAM_AUTO_ACCEPT_BYPASS === '0')
|
|
1515
2204
|
return false;
|
|
1516
2205
|
if (!paneHasClaudeBypassPermissionsPrompt(captured))
|
|
1517
2206
|
return false;
|
|
1518
|
-
acceptClaudeBypassPermissionsPrompt(
|
|
1519
|
-
|
|
2207
|
+
return acceptClaudeBypassPermissionsPrompt(resolveTarget);
|
|
2208
|
+
}
|
|
2209
|
+
async function dismissClaudeBypassPermissionsPromptIfPresentAsync(resolveTarget, captured) {
|
|
2210
|
+
if (process.env.OMX_TEAM_AUTO_ACCEPT_BYPASS === '0')
|
|
2211
|
+
return false;
|
|
2212
|
+
if (!paneHasClaudeBypassPermissionsPrompt(captured))
|
|
2213
|
+
return false;
|
|
2214
|
+
const literalTarget = await resolveTarget();
|
|
2215
|
+
if (!literalTarget)
|
|
2216
|
+
return false;
|
|
2217
|
+
const literalSend = await runTmuxAsync(['send-keys', '-t', literalTarget, '-l', '--', '2']);
|
|
2218
|
+
if (!literalSend.ok)
|
|
2219
|
+
return false;
|
|
2220
|
+
await sleep(120);
|
|
2221
|
+
const submitTarget = await resolveTarget();
|
|
2222
|
+
if (!submitTarget)
|
|
2223
|
+
return false;
|
|
2224
|
+
const submitSend = await runTmuxAsync(['send-keys', '-t', submitTarget, 'C-m']);
|
|
2225
|
+
return submitSend.ok;
|
|
1520
2226
|
}
|
|
1521
2227
|
export const paneHasActiveTask = sharedPaneHasActiveTask;
|
|
1522
2228
|
export function classifyWorkerStartupInjectSafety(captured) {
|
|
@@ -1533,8 +2239,8 @@ export function classifyWorkerStartupInjectSafety(captured) {
|
|
|
1533
2239
|
return 'safe';
|
|
1534
2240
|
}
|
|
1535
2241
|
export async function checkWorkerStartupInjectSafety(sessionName, workerIndex, workerPaneId) {
|
|
1536
|
-
const
|
|
1537
|
-
const visibleCapture = await captureVisiblePaneAsync(
|
|
2242
|
+
const resolveTarget = () => resolveWorkerPaneTargetAsync(sessionName, workerIndex, workerPaneId);
|
|
2243
|
+
const visibleCapture = await captureVisiblePaneAsync(resolveTarget);
|
|
1538
2244
|
const visibleSafety = classifyWorkerStartupInjectSafety(visibleCapture);
|
|
1539
2245
|
if (visibleSafety === 'safe')
|
|
1540
2246
|
return { safe: true, reason: 'safe' };
|
|
@@ -1543,7 +2249,7 @@ export async function checkWorkerStartupInjectSafety(sessionName, workerIndex, w
|
|
|
1543
2249
|
if (!sharedPaneShowsCodexViewport(visibleCapture)) {
|
|
1544
2250
|
return { safe: false, reason: visibleSafety };
|
|
1545
2251
|
}
|
|
1546
|
-
const scrollbackCapture = await capturePaneAsync(
|
|
2252
|
+
const scrollbackCapture = await capturePaneAsync(resolveTarget);
|
|
1547
2253
|
const scrollbackSafety = classifyWorkerStartupInjectSafety(scrollbackCapture);
|
|
1548
2254
|
return scrollbackSafety === 'safe'
|
|
1549
2255
|
? { safe: true, reason: 'safe' }
|
|
@@ -1621,8 +2327,9 @@ export function shouldAttemptAdaptiveRetry(strategy, paneBusyAtStart, allowAdapt
|
|
|
1621
2327
|
return false;
|
|
1622
2328
|
return true;
|
|
1623
2329
|
}
|
|
1624
|
-
function sendLiteralTextOrThrow(
|
|
1625
|
-
const
|
|
2330
|
+
async function sendLiteralTextOrThrow(resolveTarget, text) {
|
|
2331
|
+
const target = await requireAsyncPaneTarget(resolveTarget);
|
|
2332
|
+
const send = await runTmuxAsync(['send-keys', '-t', target, '-l', '--', text]);
|
|
1626
2333
|
if (!send.ok) {
|
|
1627
2334
|
throw new Error(`sendToWorker: failed to send text: ${send.stderr}`);
|
|
1628
2335
|
}
|
|
@@ -1634,18 +2341,18 @@ function paneHasQueuedCodexSubmission(captured) {
|
|
|
1634
2341
|
return /messages to be submitted after next tool call/i.test(normalized)
|
|
1635
2342
|
|| /press esc to interrupt and send immediately/i.test(normalized);
|
|
1636
2343
|
}
|
|
1637
|
-
async function attemptSubmitRounds(
|
|
2344
|
+
async function attemptSubmitRounds(resolveTarget, text, rounds, queueFirstRound, submitKeyPressesPerRound) {
|
|
1638
2345
|
const presses = Math.max(1, Math.floor(submitKeyPressesPerRound));
|
|
1639
2346
|
for (let round = 0; round < rounds; round++) {
|
|
1640
2347
|
await sleep(100);
|
|
1641
2348
|
if (round === 0 && queueFirstRound) {
|
|
1642
|
-
await sendKeyAsync(
|
|
2349
|
+
await sendKeyAsync(resolveTarget, 'Tab');
|
|
1643
2350
|
await sleep(80);
|
|
1644
|
-
await sendKeyAsync(
|
|
2351
|
+
await sendKeyAsync(resolveTarget, 'C-m');
|
|
1645
2352
|
}
|
|
1646
2353
|
else {
|
|
1647
2354
|
for (let press = 0; press < presses; press++) {
|
|
1648
|
-
await sendKeyAsync(
|
|
2355
|
+
await sendKeyAsync(resolveTarget, 'C-m');
|
|
1649
2356
|
if (press < presses - 1) {
|
|
1650
2357
|
await sleep(200);
|
|
1651
2358
|
}
|
|
@@ -1653,8 +2360,8 @@ async function attemptSubmitRounds(target, text, rounds, queueFirstRound, submit
|
|
|
1653
2360
|
}
|
|
1654
2361
|
await sleep(140);
|
|
1655
2362
|
const [captured, visibleCapture] = await Promise.all([
|
|
1656
|
-
capturePaneAsync(
|
|
1657
|
-
captureVisiblePaneAsync(
|
|
2363
|
+
capturePaneAsync(resolveTarget),
|
|
2364
|
+
captureVisiblePaneAsync(resolveTarget),
|
|
1658
2365
|
]);
|
|
1659
2366
|
const normalizedCapture = normalizeWorkerTriggerForDraftMatch(captured);
|
|
1660
2367
|
if (!normalizedCapture.includes(normalizeWorkerTriggerForDraftMatch(text))
|
|
@@ -1665,26 +2372,34 @@ async function attemptSubmitRounds(target, text, rounds, queueFirstRound, submit
|
|
|
1665
2372
|
}
|
|
1666
2373
|
return false;
|
|
1667
2374
|
}
|
|
1668
|
-
export function waitForWorkerReady(sessionName, workerIndex, timeoutMs = 30_000, workerPaneId) {
|
|
2375
|
+
export function waitForWorkerReady(sessionName, workerIndex, timeoutMs = 30_000, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId) {
|
|
1669
2376
|
const initialBackoffMs = 150;
|
|
1670
2377
|
const maxBackoffMs = 8000;
|
|
1671
2378
|
const startedAt = Date.now();
|
|
1672
2379
|
let blockedByTrustPrompt = false;
|
|
1673
2380
|
let promptDismissed = false;
|
|
2381
|
+
const resolveTarget = createPinnedWorkerPaneTargetResolverSync(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId);
|
|
1674
2382
|
const sendRobustEnter = () => {
|
|
1675
|
-
const target = paneTarget(sessionName, workerIndex, workerPaneId);
|
|
1676
2383
|
// Trust + follow-up splash can require two submits in Codex TUI.
|
|
1677
2384
|
// Use C-m (carriage return) for raw-mode compatibility.
|
|
1678
|
-
|
|
2385
|
+
const firstTarget = resolveTarget();
|
|
2386
|
+
if (!firstTarget)
|
|
2387
|
+
return;
|
|
2388
|
+
runTmux(['send-keys', '-t', firstTarget, 'C-m']);
|
|
1679
2389
|
sleepFractionalSeconds(0.12);
|
|
1680
|
-
|
|
2390
|
+
const secondTarget = resolveTarget();
|
|
2391
|
+
if (!secondTarget)
|
|
2392
|
+
return;
|
|
2393
|
+
runTmux(['send-keys', '-t', secondTarget, 'C-m']);
|
|
1681
2394
|
};
|
|
1682
2395
|
const check = () => {
|
|
1683
|
-
const target =
|
|
2396
|
+
const target = resolveTarget();
|
|
2397
|
+
if (!target)
|
|
2398
|
+
return false;
|
|
1684
2399
|
const result = runTmux(sharedBuildVisibleCapturePaneArgv(target));
|
|
1685
2400
|
if (!result.ok)
|
|
1686
2401
|
return false;
|
|
1687
|
-
if (dismissClaudeBypassPermissionsPromptIfPresent(
|
|
2402
|
+
if (dismissClaudeBypassPermissionsPromptIfPresent(resolveTarget, result.stdout)) {
|
|
1688
2403
|
promptDismissed = true;
|
|
1689
2404
|
return false;
|
|
1690
2405
|
}
|
|
@@ -1709,7 +2424,10 @@ export function waitForWorkerReady(sessionName, workerIndex, timeoutMs = 30_000,
|
|
|
1709
2424
|
// scrollback for the prompt/helper text that may have slipped below the fold.
|
|
1710
2425
|
if (!sharedPaneShowsCodexViewport(result.stdout))
|
|
1711
2426
|
return false;
|
|
1712
|
-
const
|
|
2427
|
+
const scrollbackTarget = resolveTarget();
|
|
2428
|
+
if (!scrollbackTarget)
|
|
2429
|
+
return false;
|
|
2430
|
+
const scrollbackResult = runTmux(sharedBuildCapturePaneArgv(scrollbackTarget, 80));
|
|
1713
2431
|
if (!scrollbackResult.ok)
|
|
1714
2432
|
return false;
|
|
1715
2433
|
return paneLooksReady(scrollbackResult.stdout);
|
|
@@ -1737,26 +2455,34 @@ export function waitForWorkerReady(sessionName, workerIndex, timeoutMs = 30_000,
|
|
|
1737
2455
|
// Async twin of waitForWorkerReady for team startup fan-out. Keep the readiness
|
|
1738
2456
|
// semantics mirrored with the synchronous helper above, but yield between polls
|
|
1739
2457
|
// so one slow worker pane cannot block later workers' startup attempts.
|
|
1740
|
-
export async function waitForWorkerReadyAsync(sessionName, workerIndex, timeoutMs = 30_000, workerPaneId) {
|
|
2458
|
+
export async function waitForWorkerReadyAsync(sessionName, workerIndex, timeoutMs = 30_000, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId) {
|
|
1741
2459
|
const initialBackoffMs = 150;
|
|
1742
2460
|
const maxBackoffMs = 8000;
|
|
1743
2461
|
const startedAt = Date.now();
|
|
1744
2462
|
let blockedByTrustPrompt = false;
|
|
1745
2463
|
let promptDismissed = false;
|
|
2464
|
+
const resolveTarget = createPinnedWorkerPaneTargetResolver(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId);
|
|
1746
2465
|
const sendRobustEnter = async () => {
|
|
1747
|
-
const target = paneTarget(sessionName, workerIndex, workerPaneId);
|
|
1748
2466
|
// Trust + follow-up splash can require two submits in Codex TUI.
|
|
1749
2467
|
// Use C-m (carriage return) for raw-mode compatibility.
|
|
1750
|
-
|
|
2468
|
+
const firstTarget = await resolveTarget();
|
|
2469
|
+
if (!firstTarget)
|
|
2470
|
+
return;
|
|
2471
|
+
await runTmuxAsync(['send-keys', '-t', firstTarget, 'C-m']);
|
|
1751
2472
|
await sleep(120);
|
|
1752
|
-
|
|
2473
|
+
const secondTarget = await resolveTarget();
|
|
2474
|
+
if (!secondTarget)
|
|
2475
|
+
return;
|
|
2476
|
+
await runTmuxAsync(['send-keys', '-t', secondTarget, 'C-m']);
|
|
1753
2477
|
};
|
|
1754
2478
|
const check = async () => {
|
|
1755
|
-
const target =
|
|
2479
|
+
const target = await resolveTarget();
|
|
2480
|
+
if (!target)
|
|
2481
|
+
return false;
|
|
1756
2482
|
const result = await runTmuxAsync(sharedBuildVisibleCapturePaneArgv(target));
|
|
1757
2483
|
if (!result.ok)
|
|
1758
2484
|
return false;
|
|
1759
|
-
if (
|
|
2485
|
+
if (await dismissClaudeBypassPermissionsPromptIfPresentAsync(resolveTarget, result.stdout)) {
|
|
1760
2486
|
promptDismissed = true;
|
|
1761
2487
|
return false;
|
|
1762
2488
|
}
|
|
@@ -1781,7 +2507,10 @@ export async function waitForWorkerReadyAsync(sessionName, workerIndex, timeoutM
|
|
|
1781
2507
|
// scrollback for the prompt/helper text that may have slipped below the fold.
|
|
1782
2508
|
if (!sharedPaneShowsCodexViewport(result.stdout))
|
|
1783
2509
|
return false;
|
|
1784
|
-
const
|
|
2510
|
+
const scrollbackTarget = await resolveTarget();
|
|
2511
|
+
if (!scrollbackTarget)
|
|
2512
|
+
return false;
|
|
2513
|
+
const scrollbackResult = await runTmuxAsync(sharedBuildCapturePaneArgv(scrollbackTarget, 80));
|
|
1785
2514
|
if (!scrollbackResult.ok)
|
|
1786
2515
|
return false;
|
|
1787
2516
|
return paneLooksReady(scrollbackResult.stdout);
|
|
@@ -1811,21 +2540,30 @@ export async function waitForWorkerReadyAsync(sessionName, workerIndex, timeoutM
|
|
|
1811
2540
|
* Returns true if a trust prompt was found and dismissed, false otherwise.
|
|
1812
2541
|
* Opt-out: set OMX_TEAM_AUTO_TRUST=0 to disable auto-dismissal.
|
|
1813
2542
|
*/
|
|
1814
|
-
export function dismissTrustPromptIfPresent(sessionName, workerIndex, workerPaneId) {
|
|
2543
|
+
export function dismissTrustPromptIfPresent(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId) {
|
|
1815
2544
|
if (process.env.OMX_TEAM_AUTO_TRUST === '0')
|
|
1816
2545
|
return false;
|
|
1817
2546
|
if (!isTmuxAvailable())
|
|
1818
2547
|
return false;
|
|
1819
|
-
const
|
|
1820
|
-
const
|
|
2548
|
+
const resolveTarget = createPinnedWorkerPaneTargetResolverSync(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId);
|
|
2549
|
+
const captureTarget = resolveTarget();
|
|
2550
|
+
if (!captureTarget)
|
|
2551
|
+
return false;
|
|
2552
|
+
const result = runTmux(sharedBuildVisibleCapturePaneArgv(captureTarget));
|
|
1821
2553
|
if (!result.ok)
|
|
1822
2554
|
return false;
|
|
1823
2555
|
if (!paneHasTrustPrompt(result.stdout))
|
|
1824
2556
|
return false;
|
|
1825
2557
|
// Trust prompt detected; send C-m twice to dismiss (trust + follow-up splash)
|
|
1826
|
-
|
|
2558
|
+
const firstTarget = resolveTarget();
|
|
2559
|
+
if (!firstTarget)
|
|
2560
|
+
return false;
|
|
2561
|
+
runTmux(['send-keys', '-t', firstTarget, 'C-m']);
|
|
1827
2562
|
sleepFractionalSeconds(0.12);
|
|
1828
|
-
|
|
2563
|
+
const secondTarget = resolveTarget();
|
|
2564
|
+
if (!secondTarget)
|
|
2565
|
+
return false;
|
|
2566
|
+
runTmux(['send-keys', '-t', secondTarget, 'C-m']);
|
|
1829
2567
|
return true;
|
|
1830
2568
|
}
|
|
1831
2569
|
export const normalizeTmuxCapture = sharedNormalizeTmuxCapture;
|
|
@@ -1856,49 +2594,49 @@ export function sendToWorkerStdin(stdin, text) {
|
|
|
1856
2594
|
// Send SHORT text (<200 chars) to worker via tmux send-keys
|
|
1857
2595
|
// Validates: text < 200 chars, no injection marker
|
|
1858
2596
|
// Throws on violation
|
|
1859
|
-
export async function sendToWorker(sessionName, workerIndex, text, workerPaneId, workerCli) {
|
|
2597
|
+
export async function sendToWorker(sessionName, workerIndex, text, workerPaneId, workerCli, expectedPanePid, expectedTeamOwnerId, hudPaneId) {
|
|
1860
2598
|
assertWorkerTriggerText(text);
|
|
1861
|
-
const
|
|
2599
|
+
const resolveTarget = createPinnedWorkerPaneTargetResolver(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId);
|
|
1862
2600
|
const strategy = resolveSendStrategyFromEnv();
|
|
1863
2601
|
const resolvedWorkerCli = resolveWorkerCliForSend(workerIndex, workerCli);
|
|
1864
2602
|
// Guard: if the trust prompt is still present, advance it first so our trigger text
|
|
1865
2603
|
// doesn't get typed into the trust screen and ignored.
|
|
1866
|
-
const capturedStr = await capturePaneAsync(
|
|
2604
|
+
const capturedStr = await capturePaneAsync(resolveTarget);
|
|
1867
2605
|
const paneBusy = paneHasActiveTask(capturedStr);
|
|
1868
|
-
if (
|
|
2606
|
+
if (await dismissClaudeBypassPermissionsPromptIfPresentAsync(resolveTarget, capturedStr)) {
|
|
1869
2607
|
await sleep(200);
|
|
1870
2608
|
}
|
|
1871
2609
|
if (paneHasTrustPrompt(capturedStr)) {
|
|
1872
|
-
await sendKeyAsync(
|
|
2610
|
+
await sendKeyAsync(resolveTarget, 'C-m');
|
|
1873
2611
|
await sleep(120);
|
|
1874
|
-
await sendKeyAsync(
|
|
2612
|
+
await sendKeyAsync(resolveTarget, 'C-m');
|
|
1875
2613
|
await sleep(200);
|
|
1876
2614
|
}
|
|
1877
|
-
sendLiteralTextOrThrow(
|
|
2615
|
+
await sendLiteralTextOrThrow(resolveTarget, text);
|
|
1878
2616
|
// Allow the input buffer to settle before sending C-m
|
|
1879
2617
|
await sleep(150);
|
|
1880
2618
|
const allowAutoInterruptRetry = process.env[OMX_TEAM_AUTO_INTERRUPT_RETRY_ENV] !== '0';
|
|
1881
2619
|
const submitPlan = buildWorkerSubmitPlan(strategy, resolvedWorkerCli, paneBusy, allowAutoInterruptRetry);
|
|
1882
2620
|
if (submitPlan.shouldInterrupt) {
|
|
1883
2621
|
// Explicit interrupt mode: abort current turn first, then submit the new command.
|
|
1884
|
-
await sendKeyAsync(
|
|
2622
|
+
await sendKeyAsync(resolveTarget, 'C-c');
|
|
1885
2623
|
await sleep(100);
|
|
1886
2624
|
}
|
|
1887
2625
|
// Submit deterministically using CLI-specific plan:
|
|
1888
2626
|
// - Codex: queue-first Tab+C-m when configured/busy, then double C-m rounds.
|
|
1889
2627
|
// - Claude: direct C-m rounds only (never queue-first Tab).
|
|
1890
|
-
if (await attemptSubmitRounds(
|
|
2628
|
+
if (await attemptSubmitRounds(resolveTarget, text, submitPlan.rounds, submitPlan.queueFirstRound, submitPlan.submitKeyPressesPerRound))
|
|
1891
2629
|
return;
|
|
1892
2630
|
// Adaptive escalation for "likely unsent trigger text at ready prompt" cases:
|
|
1893
2631
|
// clear line, re-send trigger, then re-submit with deterministic C-m rounds.
|
|
1894
|
-
const latestCapture = await capturePaneAsync(
|
|
2632
|
+
const latestCapture = await capturePaneAsync(resolveTarget);
|
|
1895
2633
|
if (shouldAttemptAdaptiveRetry(strategy, paneBusy, submitPlan.allowAdaptiveRetry, latestCapture || null, text)) {
|
|
1896
2634
|
// Keep this branch non-interrupting to avoid canceling active turns on false positives.
|
|
1897
|
-
await sendKeyAsync(
|
|
2635
|
+
await sendKeyAsync(resolveTarget, 'C-u');
|
|
1898
2636
|
await sleep(80);
|
|
1899
|
-
sendLiteralTextOrThrow(
|
|
2637
|
+
await sendLiteralTextOrThrow(resolveTarget, text);
|
|
1900
2638
|
await sleep(120);
|
|
1901
|
-
if (await attemptSubmitRounds(
|
|
2639
|
+
if (await attemptSubmitRounds(resolveTarget, text, 4, false, submitPlan.submitKeyPressesPerRound))
|
|
1902
2640
|
return;
|
|
1903
2641
|
}
|
|
1904
2642
|
// Fail-open by default: Codex may keep the last submitted line visible even after executing it.
|
|
@@ -1908,15 +2646,15 @@ export async function sendToWorker(sessionName, workerIndex, text, workerPaneId,
|
|
|
1908
2646
|
throw new Error('sendToWorker: submit_failed (trigger text still visible after retries)');
|
|
1909
2647
|
}
|
|
1910
2648
|
// One last best-effort double C-m nudge, then verify.
|
|
1911
|
-
await sendKeyAsync(
|
|
2649
|
+
await sendKeyAsync(resolveTarget, 'C-m');
|
|
1912
2650
|
await sleep(120);
|
|
1913
|
-
await sendKeyAsync(
|
|
2651
|
+
await sendKeyAsync(resolveTarget, 'C-m');
|
|
1914
2652
|
// Post-submit verification: wait briefly and confirm the worker consumed the
|
|
1915
2653
|
// trigger (draft disappeared or active-task indicator appeared). Fixes #391.
|
|
1916
2654
|
await sleep(300);
|
|
1917
2655
|
const [verifyCapture, verifyVisibleCapture] = await Promise.all([
|
|
1918
|
-
capturePaneAsync(
|
|
1919
|
-
captureVisiblePaneAsync(
|
|
2656
|
+
capturePaneAsync(resolveTarget),
|
|
2657
|
+
captureVisiblePaneAsync(resolveTarget),
|
|
1920
2658
|
]);
|
|
1921
2659
|
if (verifyCapture) {
|
|
1922
2660
|
if (paneHasActiveTask(verifyCapture))
|
|
@@ -1926,14 +2664,14 @@ export async function sendToWorker(sessionName, workerIndex, text, workerPaneId,
|
|
|
1926
2664
|
return;
|
|
1927
2665
|
}
|
|
1928
2666
|
// Draft still visible and no active task — one more C-m attempt.
|
|
1929
|
-
await sendKeyAsync(
|
|
2667
|
+
await sendKeyAsync(resolveTarget, 'C-m');
|
|
1930
2668
|
await sleep(150);
|
|
1931
|
-
await sendKeyAsync(
|
|
1932
|
-
const finalVisibleCapture = await captureVisiblePaneAsync(
|
|
2669
|
+
await sendKeyAsync(resolveTarget, 'C-m');
|
|
2670
|
+
const finalVisibleCapture = await captureVisiblePaneAsync(resolveTarget);
|
|
1933
2671
|
if (paneHasQueuedCodexSubmission(finalVisibleCapture)) {
|
|
1934
2672
|
throw new Error('sendToWorker: submit_queued_after_tool_call');
|
|
1935
2673
|
}
|
|
1936
|
-
const finalCapture = await capturePaneAsync(
|
|
2674
|
+
const finalCapture = await capturePaneAsync(resolveTarget);
|
|
1937
2675
|
if (normalizeWorkerTriggerForDraftMatch(finalCapture).includes(normalizeWorkerTriggerForDraftMatch(text))
|
|
1938
2676
|
&& !paneHasActiveTask(finalCapture)
|
|
1939
2677
|
&& paneLooksReady(finalCapture)) {
|
|
@@ -1952,8 +2690,12 @@ export function notifyLeaderStatus(sessionName, message) {
|
|
|
1952
2690
|
return result.ok;
|
|
1953
2691
|
}
|
|
1954
2692
|
// Get PID of the shell process in a worker's tmux pane
|
|
1955
|
-
export function getWorkerPanePid(sessionName, workerIndex, workerPaneId) {
|
|
1956
|
-
|
|
2693
|
+
export function getWorkerPanePid(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId) {
|
|
2694
|
+
if (hasExplicitWorkerPaneId(workerPaneId)) {
|
|
2695
|
+
const target = createPinnedWorkerPaneTargetResolverSync(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId)();
|
|
2696
|
+
return target ? expectedPanePid ?? null : null;
|
|
2697
|
+
}
|
|
2698
|
+
const result = runTmux(['list-panes', '-t', paneTarget(sessionName, workerIndex), '-F', '#{pane_pid}']);
|
|
1957
2699
|
if (!result.ok)
|
|
1958
2700
|
return null;
|
|
1959
2701
|
const firstLine = result.stdout.split('\n')[0]?.trim();
|
|
@@ -1965,15 +2707,16 @@ export function getWorkerPanePid(sessionName, workerIndex, workerPaneId) {
|
|
|
1965
2707
|
return pid;
|
|
1966
2708
|
}
|
|
1967
2709
|
// Check if worker's tmux pane has a running process
|
|
1968
|
-
export function isWorkerAlive(sessionName, workerIndex, workerPaneId) {
|
|
1969
|
-
if (workerPaneId
|
|
1970
|
-
const
|
|
1971
|
-
if (
|
|
1972
|
-
return
|
|
2710
|
+
export function isWorkerAlive(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId) {
|
|
2711
|
+
if (hasExplicitWorkerPaneId(workerPaneId)) {
|
|
2712
|
+
const target = createPinnedWorkerPaneTargetResolverSync(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId)();
|
|
2713
|
+
if (!target || typeof expectedPanePid !== 'number')
|
|
2714
|
+
return false;
|
|
2715
|
+
return probeProcessLiveness(expectedPanePid) !== 'gone';
|
|
1973
2716
|
}
|
|
1974
2717
|
const result = runTmux([
|
|
1975
2718
|
'list-panes',
|
|
1976
|
-
'-t', paneTarget(sessionName, workerIndex
|
|
2719
|
+
'-t', paneTarget(sessionName, workerIndex),
|
|
1977
2720
|
'-F',
|
|
1978
2721
|
'#{pane_dead} #{pane_pid}',
|
|
1979
2722
|
]);
|
|
@@ -1991,23 +2734,17 @@ export function isWorkerAlive(sessionName, workerIndex, workerPaneId) {
|
|
|
1991
2734
|
return false;
|
|
1992
2735
|
if (!Number.isFinite(pid))
|
|
1993
2736
|
return false;
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
}
|
|
1998
|
-
catch {
|
|
1999
|
-
return false;
|
|
2000
|
-
}
|
|
2737
|
+
// Unknown is conservatively live so callers cannot classify a permission or
|
|
2738
|
+
// transient failure as stale cleanup authority.
|
|
2739
|
+
return probeProcessLiveness(pid) !== 'gone';
|
|
2001
2740
|
}
|
|
2002
|
-
export function isWorkerPaneOpen(sessionName, workerIndex, workerPaneId) {
|
|
2003
|
-
if (workerPaneId
|
|
2004
|
-
|
|
2005
|
-
if (paneStatus !== null)
|
|
2006
|
-
return paneStatus;
|
|
2741
|
+
export function isWorkerPaneOpen(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId) {
|
|
2742
|
+
if (hasExplicitWorkerPaneId(workerPaneId)) {
|
|
2743
|
+
return createPinnedWorkerPaneTargetResolverSync(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId)() !== null;
|
|
2007
2744
|
}
|
|
2008
2745
|
const result = runTmux([
|
|
2009
2746
|
'list-panes',
|
|
2010
|
-
'-t', paneTarget(sessionName, workerIndex
|
|
2747
|
+
'-t', paneTarget(sessionName, workerIndex),
|
|
2011
2748
|
'-F',
|
|
2012
2749
|
'#{pane_dead}',
|
|
2013
2750
|
]);
|
|
@@ -2020,28 +2757,69 @@ export function isWorkerPaneOpen(sessionName, workerIndex, workerPaneId) {
|
|
|
2020
2757
|
}
|
|
2021
2758
|
// Kill a specific worker: send C-c, then C-d, then kill-pane if still alive.
|
|
2022
2759
|
// leaderPaneId: when provided, the kill is skipped entirely if workerPaneId matches it.
|
|
2023
|
-
export async function killWorker(sessionName, workerIndex, workerPaneId, leaderPaneId) {
|
|
2760
|
+
export async function killWorker(sessionName, workerIndex, workerPaneId, leaderPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId) {
|
|
2024
2761
|
// Guard: never kill the leader's own pane.
|
|
2025
2762
|
if (leaderPaneId && workerPaneId === leaderPaneId)
|
|
2026
2763
|
return;
|
|
2027
|
-
|
|
2764
|
+
const resolveTarget = createPinnedWorkerPaneTargetResolver(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId);
|
|
2765
|
+
const initialTarget = await resolveTarget();
|
|
2766
|
+
if (!initialTarget)
|
|
2767
|
+
return;
|
|
2768
|
+
if (await isWorkerAliveAsync(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId, resolveTarget) !== 'alive')
|
|
2769
|
+
return;
|
|
2770
|
+
await runTmuxAsync(['send-keys', '-t', initialTarget, 'C-c']);
|
|
2028
2771
|
await sleep(1000);
|
|
2029
|
-
if (await isWorkerAliveAsync(sessionName, workerIndex, workerPaneId)) {
|
|
2030
|
-
|
|
2031
|
-
|
|
2032
|
-
|
|
2033
|
-
|
|
2034
|
-
|
|
2772
|
+
if (await isWorkerAliveAsync(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId, resolveTarget) === 'alive') {
|
|
2773
|
+
const exitTarget = await resolveTarget();
|
|
2774
|
+
if (exitTarget) {
|
|
2775
|
+
await runTmuxAsync(['send-keys', '-t', exitTarget, 'C-d']);
|
|
2776
|
+
await sleep(1000);
|
|
2777
|
+
}
|
|
2035
2778
|
}
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2779
|
+
if (await isWorkerAliveAsync(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId, resolveTarget) === 'alive') {
|
|
2780
|
+
const killTarget = await resolveTarget();
|
|
2781
|
+
if (!killTarget)
|
|
2782
|
+
return;
|
|
2783
|
+
const killed = await runTmuxAsync(['kill-pane', '-t', killTarget]);
|
|
2784
|
+
if (!killed.ok)
|
|
2785
|
+
throw new Error(`failed to kill tmux pane ${killTarget}: ${killed.stderr}`);
|
|
2786
|
+
const absence = await readExactPaneProof(killTarget);
|
|
2787
|
+
if (absence.status === 'unavailable')
|
|
2788
|
+
throw new ExactPaneProofUnavailableError(absence);
|
|
2789
|
+
if (absence.status !== 'gone')
|
|
2790
|
+
throw new Error(`tmux pane remains live after kill: ${killTarget}`);
|
|
2791
|
+
}
|
|
2792
|
+
}
|
|
2793
|
+
// Explicit pane targets require frozen PID, canonical Team owner, and HUD
|
|
2794
|
+
// exclusion. A successful kill is only accepted after a fresh absence proof.
|
|
2795
|
+
export function killWorkerByPaneId(workerPaneId, expectedPanePid, leaderPaneId, expectedTeamOwnerId, hudPaneId) {
|
|
2796
|
+
const expectedOwner = typeof expectedTeamOwnerId === 'string' ? expectedTeamOwnerId.trim() : '';
|
|
2797
|
+
if (!hasExplicitWorkerPaneId(workerPaneId)
|
|
2798
|
+
|| typeof expectedPanePid !== 'number'
|
|
2799
|
+
|| !Number.isSafeInteger(expectedPanePid)
|
|
2800
|
+
|| expectedPanePid <= 0
|
|
2801
|
+
|| !expectedOwner
|
|
2802
|
+
|| workerPaneId === hudPaneId)
|
|
2040
2803
|
return;
|
|
2041
|
-
// Guard: never kill the leader's own pane.
|
|
2042
2804
|
if (leaderPaneId && workerPaneId === leaderPaneId)
|
|
2043
2805
|
return;
|
|
2044
|
-
|
|
2806
|
+
const proof = readExactPaneProofSync(workerPaneId);
|
|
2807
|
+
if (proof.status !== 'live' || proof.pid !== expectedPanePid)
|
|
2808
|
+
return;
|
|
2809
|
+
const owner = readPaneTeamOwnerTagResult(proof.paneId);
|
|
2810
|
+
if (owner.status !== 'value' || owner.value !== expectedOwner)
|
|
2811
|
+
return;
|
|
2812
|
+
const finalProof = readExactPaneProofSync(proof.paneId);
|
|
2813
|
+
if (finalProof.status !== 'live' || finalProof.pid !== expectedPanePid)
|
|
2814
|
+
return;
|
|
2815
|
+
const result = runTmux(['kill-pane', '-t', finalProof.paneId]);
|
|
2816
|
+
if (!result.ok)
|
|
2817
|
+
throw new Error(`failed to kill tmux pane ${finalProof.paneId}: ${result.stderr}`);
|
|
2818
|
+
const absence = readExactPaneProofSync(finalProof.paneId);
|
|
2819
|
+
if (absence.status === 'unavailable')
|
|
2820
|
+
throw new ExactPaneProofUnavailableError(absence);
|
|
2821
|
+
if (absence.status !== 'gone')
|
|
2822
|
+
throw new Error(`tmux pane remains live after kill: ${finalProof.paneId}`);
|
|
2045
2823
|
}
|
|
2046
2824
|
export function paneHasOmxInstanceTag(paneId, instanceId) {
|
|
2047
2825
|
const normalizedPaneId = normalizePaneTarget(paneId);
|
|
@@ -2092,13 +2870,34 @@ export function readPaneTeamOwnerTagResult(paneId) {
|
|
|
2092
2870
|
return { status: 'missing' };
|
|
2093
2871
|
return { status: 'error', error: stderr || `tmux show-option exited ${result.status ?? 'unknown'}` };
|
|
2094
2872
|
}
|
|
2095
|
-
export async function killWorkerByPaneIdAsync(workerPaneId, leaderPaneId) {
|
|
2096
|
-
|
|
2873
|
+
export async function killWorkerByPaneIdAsync(workerPaneId, expectedPanePid, leaderPaneId, expectedTeamOwnerId, hudPaneId) {
|
|
2874
|
+
const expectedOwner = typeof expectedTeamOwnerId === 'string' ? expectedTeamOwnerId.trim() : '';
|
|
2875
|
+
if (!hasExplicitWorkerPaneId(workerPaneId)
|
|
2876
|
+
|| typeof expectedPanePid !== 'number'
|
|
2877
|
+
|| !Number.isSafeInteger(expectedPanePid)
|
|
2878
|
+
|| expectedPanePid <= 0
|
|
2879
|
+
|| !expectedOwner
|
|
2880
|
+
|| workerPaneId === hudPaneId)
|
|
2097
2881
|
return;
|
|
2098
|
-
// Guard: never kill the leader's own pane.
|
|
2099
2882
|
if (leaderPaneId && workerPaneId === leaderPaneId)
|
|
2100
2883
|
return;
|
|
2101
|
-
await
|
|
2884
|
+
const proof = await readExactPaneProof(workerPaneId);
|
|
2885
|
+
if (proof.status !== 'live' || proof.pid !== expectedPanePid)
|
|
2886
|
+
return;
|
|
2887
|
+
const owner = readPaneTeamOwnerTagResult(proof.paneId);
|
|
2888
|
+
if (owner.status !== 'value' || owner.value !== expectedOwner)
|
|
2889
|
+
return;
|
|
2890
|
+
const finalProof = await readExactPaneProof(proof.paneId);
|
|
2891
|
+
if (finalProof.status !== 'live' || finalProof.pid !== expectedPanePid)
|
|
2892
|
+
return;
|
|
2893
|
+
const result = await runTmuxAsync(['kill-pane', '-t', finalProof.paneId]);
|
|
2894
|
+
if (!result.ok)
|
|
2895
|
+
throw new Error(`failed to kill tmux pane ${finalProof.paneId}: ${result.stderr}`);
|
|
2896
|
+
const absence = await readExactPaneProof(finalProof.paneId);
|
|
2897
|
+
if (absence.status === 'unavailable')
|
|
2898
|
+
throw new ExactPaneProofUnavailableError(absence);
|
|
2899
|
+
if (absence.status !== 'gone')
|
|
2900
|
+
throw new Error(`tmux pane remains live after kill: ${finalProof.paneId}`);
|
|
2102
2901
|
}
|
|
2103
2902
|
function normalizePaneTarget(value) {
|
|
2104
2903
|
if (typeof value !== 'string')
|
|
@@ -2136,7 +2935,10 @@ function normalizePaneTargets(paneIds, options = {}) {
|
|
|
2136
2935
|
return { killablePaneIds, excluded };
|
|
2137
2936
|
}
|
|
2138
2937
|
export function resolveSharedSessionShutdownTopology(sessionName, preferredLeaderPaneId, teamName) {
|
|
2139
|
-
const
|
|
2938
|
+
const paneList = listPanesResult(sessionName);
|
|
2939
|
+
if (paneList.error !== null)
|
|
2940
|
+
return { status: 'unavailable', detail: paneList.error };
|
|
2941
|
+
const panes = paneList.panes;
|
|
2140
2942
|
const livePaneIds = panes
|
|
2141
2943
|
.map((pane) => normalizePaneTarget(pane.paneId))
|
|
2142
2944
|
.filter((paneId) => Boolean(paneId));
|
|
@@ -2144,6 +2946,7 @@ export function resolveSharedSessionShutdownTopology(sessionName, preferredLeade
|
|
|
2144
2946
|
if (panes.length === 0) {
|
|
2145
2947
|
return {
|
|
2146
2948
|
livePaneIds,
|
|
2949
|
+
status: 'available',
|
|
2147
2950
|
teamWorkerPaneIds: [],
|
|
2148
2951
|
leaderPaneId: fallbackLeaderPaneId,
|
|
2149
2952
|
hudPaneIds: [],
|
|
@@ -2174,6 +2977,7 @@ export function resolveSharedSessionShutdownTopology(sessionName, preferredLeade
|
|
|
2174
2977
|
: [];
|
|
2175
2978
|
return {
|
|
2176
2979
|
livePaneIds,
|
|
2980
|
+
status: 'available',
|
|
2177
2981
|
teamWorkerPaneIds: normalizedTeamWorkerPaneIds,
|
|
2178
2982
|
leaderPaneId: resolvedLeaderPaneId,
|
|
2179
2983
|
hudPaneIds,
|
|
@@ -2288,7 +3092,8 @@ function escapeRegExp(value) {
|
|
|
2288
3092
|
}
|
|
2289
3093
|
/**
|
|
2290
3094
|
* Shared pane-id-direct teardown primitive for worker pane cleanup.
|
|
2291
|
-
*
|
|
3095
|
+
* A fresh exact global proof is required before each kill; proven-gone panes
|
|
3096
|
+
* are teardown-compatible while unavailable proofs remain fail-closed.
|
|
2292
3097
|
*/
|
|
2293
3098
|
export async function teardownWorkerPanes(paneIds, options = {}) {
|
|
2294
3099
|
const { killablePaneIds, excluded } = normalizePaneTargets(paneIds, options);
|
|
@@ -2299,45 +3104,247 @@ export async function teardownWorkerPanes(paneIds, options = {}) {
|
|
|
2299
3104
|
const summary = {
|
|
2300
3105
|
attemptedPaneIds: killablePaneIds,
|
|
2301
3106
|
excluded,
|
|
3107
|
+
provenGonePaneIds: [],
|
|
3108
|
+
killedPaneIds: [],
|
|
3109
|
+
proofUnavailable: [],
|
|
2302
3110
|
kill: {
|
|
2303
|
-
attempted:
|
|
3111
|
+
attempted: 0,
|
|
2304
3112
|
succeeded: 0,
|
|
2305
3113
|
failed: 0,
|
|
3114
|
+
failedPaneIds: [],
|
|
2306
3115
|
},
|
|
2307
3116
|
};
|
|
2308
3117
|
for (const paneId of killablePaneIds) {
|
|
2309
|
-
const
|
|
2310
|
-
|
|
3118
|
+
const proof = await readExactPaneProof(paneId);
|
|
3119
|
+
const expectedPid = options.expectedPanePids?.[paneId];
|
|
3120
|
+
if (proof.status === 'gone') {
|
|
3121
|
+
summary.provenGonePaneIds.push(proof.paneId);
|
|
3122
|
+
continue;
|
|
3123
|
+
}
|
|
3124
|
+
if (proof.status === 'unavailable') {
|
|
3125
|
+
summary.proofUnavailable.push(proof);
|
|
3126
|
+
break;
|
|
3127
|
+
}
|
|
3128
|
+
if (typeof expectedPid !== 'number' || !Number.isSafeInteger(expectedPid) || expectedPid <= 0) {
|
|
3129
|
+
summary.proofUnavailable.push({
|
|
3130
|
+
status: 'unavailable',
|
|
3131
|
+
paneId,
|
|
3132
|
+
reason: 'pane_pid_changed',
|
|
3133
|
+
detail: 'expected positive pane PID is required before teardown kill',
|
|
3134
|
+
});
|
|
3135
|
+
break;
|
|
3136
|
+
}
|
|
3137
|
+
if (proof.pid !== expectedPid) {
|
|
3138
|
+
summary.proofUnavailable.push({
|
|
3139
|
+
status: 'unavailable',
|
|
3140
|
+
paneId,
|
|
3141
|
+
reason: 'pane_pid_changed',
|
|
3142
|
+
detail: `expected ${expectedPid}, got ${proof.pid}`,
|
|
3143
|
+
});
|
|
3144
|
+
break;
|
|
3145
|
+
}
|
|
3146
|
+
if (options.authorizePaneKill && !options.authorizePaneKill(proof.paneId, proof)) {
|
|
3147
|
+
summary.proofUnavailable.push({
|
|
3148
|
+
status: 'unavailable',
|
|
3149
|
+
paneId: proof.paneId,
|
|
3150
|
+
reason: 'pane_pid_changed',
|
|
3151
|
+
detail: 'pane owner authorization changed',
|
|
3152
|
+
});
|
|
3153
|
+
break;
|
|
3154
|
+
}
|
|
3155
|
+
// Owner authorization can read tmux. Re-prove the persisted identity after
|
|
3156
|
+
// it and immediately before kill-pane.
|
|
3157
|
+
const finalProof = await readExactPaneProof(proof.paneId);
|
|
3158
|
+
if (finalProof.status === 'gone') {
|
|
3159
|
+
summary.provenGonePaneIds.push(finalProof.paneId);
|
|
3160
|
+
continue;
|
|
3161
|
+
}
|
|
3162
|
+
if (finalProof.status === 'unavailable') {
|
|
3163
|
+
summary.proofUnavailable.push(finalProof);
|
|
3164
|
+
break;
|
|
3165
|
+
}
|
|
3166
|
+
if (finalProof.pid !== expectedPid) {
|
|
3167
|
+
summary.proofUnavailable.push({
|
|
3168
|
+
status: 'unavailable',
|
|
3169
|
+
paneId: finalProof.paneId,
|
|
3170
|
+
reason: 'pane_pid_changed',
|
|
3171
|
+
detail: `expected ${expectedPid}, got ${finalProof.pid}`,
|
|
3172
|
+
});
|
|
3173
|
+
break;
|
|
3174
|
+
}
|
|
3175
|
+
summary.kill.attempted += 1;
|
|
3176
|
+
const result = await runTmuxAsync(['kill-pane', '-t', finalProof.paneId]);
|
|
3177
|
+
if (!result.ok) {
|
|
3178
|
+
summary.kill.failed += 1;
|
|
3179
|
+
summary.kill.failedPaneIds.push(proof.paneId);
|
|
3180
|
+
await sleep(perPaneGrace);
|
|
3181
|
+
break;
|
|
3182
|
+
}
|
|
3183
|
+
const afterKill = await readExactPaneProof(finalProof.paneId);
|
|
3184
|
+
if (afterKill.status === 'gone') {
|
|
2311
3185
|
summary.kill.succeeded += 1;
|
|
2312
|
-
|
|
3186
|
+
summary.killedPaneIds.push(proof.paneId);
|
|
3187
|
+
}
|
|
3188
|
+
else if (afterKill.status === 'unavailable') {
|
|
3189
|
+
summary.proofUnavailable.push(afterKill);
|
|
3190
|
+
break;
|
|
3191
|
+
}
|
|
3192
|
+
else {
|
|
2313
3193
|
summary.kill.failed += 1;
|
|
3194
|
+
summary.kill.failedPaneIds.push(proof.paneId);
|
|
3195
|
+
await sleep(perPaneGrace);
|
|
3196
|
+
break;
|
|
3197
|
+
}
|
|
2314
3198
|
await sleep(perPaneGrace);
|
|
2315
3199
|
}
|
|
2316
3200
|
return summary;
|
|
2317
3201
|
}
|
|
2318
|
-
export async function killWorkerPanes(paneIds, leaderPaneId, graceMs = 2000, hudPaneId) {
|
|
2319
|
-
return teardownWorkerPanes(paneIds, {
|
|
2320
|
-
|
|
2321
|
-
|
|
2322
|
-
|
|
2323
|
-
|
|
2324
|
-
|
|
2325
|
-
}
|
|
2326
|
-
catch {
|
|
2327
|
-
// tolerate
|
|
2328
|
-
}
|
|
3202
|
+
export async function killWorkerPanes(paneIds, leaderPaneId, graceMs = 2000, hudPaneId, expectedPanePids) {
|
|
3203
|
+
return teardownWorkerPanes(paneIds, {
|
|
3204
|
+
leaderPaneId,
|
|
3205
|
+
hudPaneId: hudPaneId ?? null,
|
|
3206
|
+
graceMs,
|
|
3207
|
+
expectedPanePids,
|
|
3208
|
+
});
|
|
2329
3209
|
}
|
|
2330
|
-
//
|
|
2331
|
-
|
|
2332
|
-
|
|
2333
|
-
|
|
2334
|
-
|
|
2335
|
-
return result.stdout
|
|
3210
|
+
// Kill an entire detached tmux session only when the caller has already
|
|
3211
|
+
// established ownership. Success requires tmux to accept the kill and a fresh
|
|
3212
|
+
// session-list proof that the exact base session is absent.
|
|
3213
|
+
function parseTeamSessionNames(stdout) {
|
|
3214
|
+
return stdout
|
|
2336
3215
|
.split('\n')
|
|
2337
3216
|
.map(line => line.trim())
|
|
2338
3217
|
.filter(Boolean)
|
|
2339
3218
|
.map(baseSessionName);
|
|
2340
3219
|
}
|
|
3220
|
+
function isFinalTmuxServerGone(result) {
|
|
3221
|
+
return !result.ok && /^no server running on .+$/i.test(result.stderr);
|
|
3222
|
+
}
|
|
3223
|
+
/**
|
|
3224
|
+
* Reads pane and session incarnation in one tmux snapshot. This prevents a
|
|
3225
|
+
* same-name replacement from combining a stale session proof with a live pane.
|
|
3226
|
+
*/
|
|
3227
|
+
export function queryDetachedSessionLeaderBinding(paneId, pid, sessionName, expected) {
|
|
3228
|
+
const result = runTmuxStructured(['list-panes', '-a', '-F', '#{pane_id}\t#{pane_dead}\t#{pane_pid}\t#{session_name}\t#{session_id}\t#{session_created}']);
|
|
3229
|
+
if (!result.ok)
|
|
3230
|
+
return null;
|
|
3231
|
+
const seenPaneIds = new Set();
|
|
3232
|
+
let binding = null;
|
|
3233
|
+
for (const line of result.stdout.split('\n').filter(Boolean)) {
|
|
3234
|
+
const fields = line.split('\t');
|
|
3235
|
+
if (fields.length !== 6)
|
|
3236
|
+
return null;
|
|
3237
|
+
const [currentPaneId, dead, panePid, currentSessionName, sessionId, sessionCreated] = fields;
|
|
3238
|
+
if (!/^%[0-9]+$/.test(currentPaneId) || seenPaneIds.has(currentPaneId) || (dead !== '0' && dead !== '1')
|
|
3239
|
+
|| !/^[0-9]+$/.test(panePid) || !Number.isSafeInteger(Number(panePid)) || Number(panePid) <= 0
|
|
3240
|
+
|| !currentSessionName || !/^\$[0-9]+$/.test(sessionId) || !/^[0-9]+$/.test(sessionCreated))
|
|
3241
|
+
return null;
|
|
3242
|
+
seenPaneIds.add(currentPaneId);
|
|
3243
|
+
if (currentPaneId !== paneId)
|
|
3244
|
+
continue;
|
|
3245
|
+
if (dead !== '0' || Number(panePid) !== pid || currentSessionName !== sessionName
|
|
3246
|
+
|| sessionId !== expected.sessionId || sessionCreated !== expected.sessionCreated)
|
|
3247
|
+
return null;
|
|
3248
|
+
binding = { paneId: currentPaneId, pid: Number(panePid), sessionName: currentSessionName, incarnation: { sessionId, sessionCreated } };
|
|
3249
|
+
}
|
|
3250
|
+
return binding;
|
|
3251
|
+
}
|
|
3252
|
+
function parseDetachedSessionIncarnation(stdout, sessionName) {
|
|
3253
|
+
const baseName = baseSessionName(sessionName);
|
|
3254
|
+
const matches = [];
|
|
3255
|
+
for (const line of stdout.split('\n').filter(Boolean)) {
|
|
3256
|
+
const fields = line.split('\t');
|
|
3257
|
+
if (fields.length !== 3)
|
|
3258
|
+
return 'malformed';
|
|
3259
|
+
const [name, sessionId, sessionCreated] = fields;
|
|
3260
|
+
if (!name || !sessionId || !sessionCreated || !/^\$[0-9]+$/.test(sessionId) || !/^[0-9]+$/.test(sessionCreated))
|
|
3261
|
+
return 'malformed';
|
|
3262
|
+
if (baseSessionName(name) === baseName)
|
|
3263
|
+
matches.push({ sessionId, sessionCreated });
|
|
3264
|
+
}
|
|
3265
|
+
return matches.length === 0 ? null : matches.length === 1 ? matches[0] : 'malformed';
|
|
3266
|
+
}
|
|
3267
|
+
/**
|
|
3268
|
+
* Reads stable tmux incarnation evidence. Name-only presence is never effect
|
|
3269
|
+
* authority: malformed or unavailable evidence fails closed, and an expected
|
|
3270
|
+
* incarnation distinguishes an exact survivor from a recycled same-name session.
|
|
3271
|
+
*/
|
|
3272
|
+
export function queryDetachedTeamSession(sessionName, expected) {
|
|
3273
|
+
const sessions = runTmuxStructured(['list-sessions', '-F', '#{session_name}\t#{session_id}\t#{session_created}']);
|
|
3274
|
+
if (!sessions.ok) {
|
|
3275
|
+
return isFinalTmuxServerGone(sessions)
|
|
3276
|
+
? { status: 'absent' }
|
|
3277
|
+
: { status: 'unavailable', detail: sessions.stderr };
|
|
3278
|
+
}
|
|
3279
|
+
const incarnation = parseDetachedSessionIncarnation(sessions.stdout, sessionName);
|
|
3280
|
+
if (incarnation === 'malformed')
|
|
3281
|
+
return { status: 'unavailable', detail: 'malformed_session_incarnation' };
|
|
3282
|
+
if (incarnation === null)
|
|
3283
|
+
return { status: 'absent' };
|
|
3284
|
+
if (expected && (incarnation.sessionId !== expected.sessionId || incarnation.sessionCreated !== expected.sessionCreated)) {
|
|
3285
|
+
return { status: 'replacement', incarnation };
|
|
3286
|
+
}
|
|
3287
|
+
return { status: 'exact', incarnation };
|
|
3288
|
+
}
|
|
3289
|
+
/**
|
|
3290
|
+
* Re-proves every stable authorization component at the destructive sink. The
|
|
3291
|
+
* final tmux command queues an in-server predicate with the kill, so no client
|
|
3292
|
+
* can replace the session or pane between authorization and the effect.
|
|
3293
|
+
*/
|
|
3294
|
+
function buildDetachedSessionDestroyArgs(expected) {
|
|
3295
|
+
const leaderPaneId = expected.leaderPaneId.trim();
|
|
3296
|
+
const ownerId = expected.ownerId.trim();
|
|
3297
|
+
if (!/^%[0-9]+$/.test(leaderPaneId)
|
|
3298
|
+
|| !Number.isSafeInteger(expected.leaderPanePid)
|
|
3299
|
+
|| expected.leaderPanePid <= 0
|
|
3300
|
+
|| !/^\$[0-9]+$/.test(expected.sessionId)
|
|
3301
|
+
|| !/^[0-9]+$/.test(expected.sessionCreated)
|
|
3302
|
+
|| !/^[A-Za-z0-9._:@/-]+$/.test(ownerId))
|
|
3303
|
+
return null;
|
|
3304
|
+
const predicate = [
|
|
3305
|
+
'#{==:#{pane_dead},0}',
|
|
3306
|
+
`#{==:#{pane_id},${leaderPaneId}}`,
|
|
3307
|
+
`#{==:#{pane_pid},${expected.leaderPanePid}}`,
|
|
3308
|
+
`#{==:#{${OMX_TEAM_PANE_OWNER_OPTION}},${ownerId}}`,
|
|
3309
|
+
`#{==:#{session_id},${expected.sessionId}}`,
|
|
3310
|
+
`#{==:#{session_created},${expected.sessionCreated}}`,
|
|
3311
|
+
].reduce((combined, condition) => `#{&&:${combined},${condition}}`);
|
|
3312
|
+
return ['if-shell', '-F', '-t', leaderPaneId, predicate, `kill-session -t ${expected.sessionId}`, 'run-shell "exit 1"'];
|
|
3313
|
+
}
|
|
3314
|
+
export function requestDetachedTeamSessionDestroy(sessionName, expected) {
|
|
3315
|
+
const incarnation = { sessionId: expected.sessionId, sessionCreated: expected.sessionCreated };
|
|
3316
|
+
if (queryDetachedTeamSession(sessionName, incarnation).status !== 'exact')
|
|
3317
|
+
return false;
|
|
3318
|
+
if (!queryDetachedSessionLeaderBinding(expected.leaderPaneId, expected.leaderPanePid, sessionName, incarnation))
|
|
3319
|
+
return false;
|
|
3320
|
+
const pane = readExactPaneProofSync(expected.leaderPaneId);
|
|
3321
|
+
if (pane.status !== 'live' || pane.pid !== expected.leaderPanePid)
|
|
3322
|
+
return false;
|
|
3323
|
+
const owner = readPaneTeamOwnerTagResult(expected.leaderPaneId);
|
|
3324
|
+
if (owner.status !== 'value' || owner.value !== expected.ownerId)
|
|
3325
|
+
return false;
|
|
3326
|
+
const args = buildDetachedSessionDestroyArgs(expected);
|
|
3327
|
+
return args !== null && runTmux(args).ok;
|
|
3328
|
+
}
|
|
3329
|
+
export function destroyTeamSession(sessionName) {
|
|
3330
|
+
const result = runTmux(['kill-session', '-t', sessionName]);
|
|
3331
|
+
if (!result.ok)
|
|
3332
|
+
return false;
|
|
3333
|
+
const sessions = runTmux(['list-sessions', '-F', '#{session_name}']);
|
|
3334
|
+
// Killing tmux's final session also terminates its server. Its documented
|
|
3335
|
+
// no-server response is the only post-kill query failure that proves absence.
|
|
3336
|
+
if (!sessions.ok)
|
|
3337
|
+
return isFinalTmuxServerGone(sessions);
|
|
3338
|
+
return !parseTeamSessionNames(sessions.stdout).includes(baseSessionName(sessionName));
|
|
3339
|
+
}
|
|
3340
|
+
// A failed query is not a successful empty session list. Destructive callers
|
|
3341
|
+
// must require the latter before treating a detached session as absent.
|
|
3342
|
+
export function listTeamSessions() {
|
|
3343
|
+
const result = runTmux(['list-sessions', '-F', '#{session_name}']);
|
|
3344
|
+
if (!result.ok)
|
|
3345
|
+
return null;
|
|
3346
|
+
return parseTeamSessionNames(result.stdout);
|
|
3347
|
+
}
|
|
2341
3348
|
/**
|
|
2342
3349
|
* Notify the leader through durable mailbox state only.
|
|
2343
3350
|
*
|