oh-my-codex 0.20.2 → 0.20.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +36 -18
- package/Cargo.toml +1 -1
- package/README.md +28 -2
- package/crates/omx-runtime/Cargo.toml +4 -0
- package/crates/omx-runtime/src/main.rs +415 -2
- package/crates/omx-runtime/tests/execution.rs +483 -0
- package/crates/omx-runtime-core/src/dispatch.rs +96 -10
- package/crates/omx-runtime-core/src/engine.rs +839 -29
- package/crates/omx-runtime-core/src/lib.rs +8 -0
- package/dist/adapt/__tests__/herdr-target.test.d.ts +2 -0
- package/dist/adapt/__tests__/herdr-target.test.d.ts.map +1 -0
- package/dist/adapt/__tests__/herdr-target.test.js +72 -0
- package/dist/adapt/__tests__/herdr-target.test.js.map +1 -0
- package/dist/adapt/contracts.d.ts +1 -1
- package/dist/adapt/contracts.d.ts.map +1 -1
- package/dist/adapt/contracts.js +1 -1
- package/dist/adapt/contracts.js.map +1 -1
- package/dist/adapt/herdr/__tests__/authority.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/authority.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/authority.test.js +49 -0
- package/dist/adapt/herdr/__tests__/authority.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/bridge.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/bridge.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/bridge.test.js +176 -0
- package/dist/adapt/herdr/__tests__/bridge.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/sanitize.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/sanitize.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/sanitize.test.js +45 -0
- package/dist/adapt/herdr/__tests__/sanitize.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/semantic.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/semantic.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/semantic.test.js +130 -0
- package/dist/adapt/herdr/__tests__/semantic.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/seq-store.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/seq-store.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/seq-store.test.js +54 -0
- package/dist/adapt/herdr/__tests__/seq-store.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/socket-live.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/socket-live.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/socket-live.test.js +104 -0
- package/dist/adapt/herdr/__tests__/socket-live.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/team-map.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/team-map.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/team-map.test.js +46 -0
- package/dist/adapt/herdr/__tests__/team-map.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/transport.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/transport.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/transport.test.js +160 -0
- package/dist/adapt/herdr/__tests__/transport.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/wiring.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/wiring.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/wiring.test.js +118 -0
- package/dist/adapt/herdr/__tests__/wiring.test.js.map +1 -0
- package/dist/adapt/herdr/authority.d.ts +34 -0
- package/dist/adapt/herdr/authority.d.ts.map +1 -0
- package/dist/adapt/herdr/authority.js +71 -0
- package/dist/adapt/herdr/authority.js.map +1 -0
- package/dist/adapt/herdr/bridge.d.ts +84 -0
- package/dist/adapt/herdr/bridge.d.ts.map +1 -0
- package/dist/adapt/herdr/bridge.js +222 -0
- package/dist/adapt/herdr/bridge.js.map +1 -0
- package/dist/adapt/herdr/sanitize.d.ts +20 -0
- package/dist/adapt/herdr/sanitize.d.ts.map +1 -0
- package/dist/adapt/herdr/sanitize.js +70 -0
- package/dist/adapt/herdr/sanitize.js.map +1 -0
- package/dist/adapt/herdr/semantic.d.ts +67 -0
- package/dist/adapt/herdr/semantic.d.ts.map +1 -0
- package/dist/adapt/herdr/semantic.js +170 -0
- package/dist/adapt/herdr/semantic.js.map +1 -0
- package/dist/adapt/herdr/seq-store.d.ts +32 -0
- package/dist/adapt/herdr/seq-store.d.ts.map +1 -0
- package/dist/adapt/herdr/seq-store.js +126 -0
- package/dist/adapt/herdr/seq-store.js.map +1 -0
- package/dist/adapt/herdr/team-map.d.ts +30 -0
- package/dist/adapt/herdr/team-map.d.ts.map +1 -0
- package/dist/adapt/herdr/team-map.js +37 -0
- package/dist/adapt/herdr/team-map.js.map +1 -0
- package/dist/adapt/herdr/transport.d.ts +115 -0
- package/dist/adapt/herdr/transport.d.ts.map +1 -0
- package/dist/adapt/herdr/transport.js +260 -0
- package/dist/adapt/herdr/transport.js.map +1 -0
- package/dist/adapt/herdr/wiring.d.ts +24 -0
- package/dist/adapt/herdr/wiring.d.ts.map +1 -0
- package/dist/adapt/herdr/wiring.js +62 -0
- package/dist/adapt/herdr/wiring.js.map +1 -0
- package/dist/adapt/herdr.d.ts +19 -0
- package/dist/adapt/herdr.d.ts.map +1 -0
- package/dist/adapt/herdr.js +115 -0
- package/dist/adapt/herdr.js.map +1 -0
- package/dist/adapt/index.d.ts +4 -0
- package/dist/adapt/index.d.ts.map +1 -1
- package/dist/adapt/index.js +48 -0
- package/dist/adapt/index.js.map +1 -1
- package/dist/adapt/registry.d.ts.map +1 -1
- package/dist/adapt/registry.js +11 -0
- package/dist/adapt/registry.js.map +1 -1
- package/dist/agents/__tests__/definitions.test.js +81 -0
- package/dist/agents/__tests__/definitions.test.js.map +1 -1
- package/dist/agents/__tests__/native-config.test.js +25 -0
- package/dist/agents/__tests__/native-config.test.js.map +1 -1
- package/dist/agents/native-config.d.ts +2 -1
- package/dist/agents/native-config.d.ts.map +1 -1
- package/dist/agents/native-config.js.map +1 -1
- package/dist/auth/__tests__/hotswap.test.js +60 -1
- package/dist/auth/__tests__/hotswap.test.js.map +1 -1
- package/dist/auth/__tests__/storage.test.js +33 -1
- package/dist/auth/__tests__/storage.test.js.map +1 -1
- package/dist/auth/hotswap.d.ts +22 -2
- package/dist/auth/hotswap.d.ts.map +1 -1
- package/dist/auth/hotswap.js +107 -22
- package/dist/auth/hotswap.js.map +1 -1
- package/dist/auth/storage.js +1 -1
- package/dist/auth/storage.js.map +1 -1
- package/dist/autopilot/__tests__/ralplan-gate.test.js +86 -1333
- package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
- package/dist/autopilot/ralplan-gate.d.ts.map +1 -1
- package/dist/autopilot/ralplan-gate.js +7 -6
- package/dist/autopilot/ralplan-gate.js.map +1 -1
- package/dist/cli/__tests__/agents-init.test.js +4 -23
- package/dist/cli/__tests__/agents-init.test.js.map +1 -1
- package/dist/cli/__tests__/api.test.js +61 -24
- package/dist/cli/__tests__/api.test.js.map +1 -1
- package/dist/cli/__tests__/auth.test.js +63 -7
- package/dist/cli/__tests__/auth.test.js.map +1 -1
- package/dist/cli/__tests__/codex-feature-probe.test.js +66 -1
- package/dist/cli/__tests__/codex-feature-probe.test.js.map +1 -1
- package/dist/cli/__tests__/codex-plugin-layout.test.js +150 -2
- package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
- package/dist/cli/__tests__/detached-leader-teardown.test.d.ts +2 -0
- package/dist/cli/__tests__/detached-leader-teardown.test.d.ts.map +1 -0
- package/dist/cli/__tests__/detached-leader-teardown.test.js +473 -0
- package/dist/cli/__tests__/detached-leader-teardown.test.js.map +1 -0
- package/dist/cli/__tests__/doctor-process-identity-readiness.test.d.ts +2 -0
- package/dist/cli/__tests__/doctor-process-identity-readiness.test.d.ts.map +1 -0
- package/dist/cli/__tests__/doctor-process-identity-readiness.test.js +103 -0
- package/dist/cli/__tests__/doctor-process-identity-readiness.test.js.map +1 -0
- package/dist/cli/__tests__/doctor-state-root-binding.test.d.ts +2 -0
- package/dist/cli/__tests__/doctor-state-root-binding.test.d.ts.map +1 -0
- package/dist/cli/__tests__/doctor-state-root-binding.test.js +139 -0
- package/dist/cli/__tests__/doctor-state-root-binding.test.js.map +1 -0
- package/dist/cli/__tests__/doctor-warning-copy.test.js +135 -1
- package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
- package/dist/cli/__tests__/exec.test.js +211 -0
- package/dist/cli/__tests__/exec.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +878 -463
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/launch-fallback.test.js +1290 -127
- package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
- package/dist/cli/__tests__/mcp-parity.test.js +8 -4
- package/dist/cli/__tests__/mcp-parity.test.js.map +1 -1
- package/dist/cli/__tests__/native-assets.test.js +398 -16
- package/dist/cli/__tests__/native-assets.test.js.map +1 -1
- package/dist/cli/__tests__/native-hook-claim-journal.test.js +132 -6
- package/dist/cli/__tests__/native-hook-claim-journal.test.js.map +1 -1
- package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +3 -3
- package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -1
- package/dist/cli/__tests__/ralph.test.js +3 -3
- package/dist/cli/__tests__/ralph.test.js.map +1 -1
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js +49 -164
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js.map +1 -1
- package/dist/cli/__tests__/ralplan-child-binding-3212.test.d.ts +2 -0
- package/dist/cli/__tests__/ralplan-child-binding-3212.test.d.ts.map +1 -0
- package/dist/cli/__tests__/ralplan-child-binding-3212.test.js +56 -0
- package/dist/cli/__tests__/ralplan-child-binding-3212.test.js.map +1 -0
- package/dist/cli/__tests__/ralplan.test.js +107 -109
- package/dist/cli/__tests__/ralplan.test.js.map +1 -1
- package/dist/cli/__tests__/resume.test.js +11 -10
- package/dist/cli/__tests__/resume.test.js.map +1 -1
- package/dist/cli/__tests__/session-scoped-runtime.test.js +1118 -43
- package/dist/cli/__tests__/session-scoped-runtime.test.js.map +1 -1
- package/dist/cli/__tests__/session-search-help.test.js +25 -1
- package/dist/cli/__tests__/session-search-help.test.js.map +1 -1
- package/dist/cli/__tests__/setup-agents-overwrite.test.js +8 -55
- package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
- package/dist/cli/__tests__/setup-install-mode.test.js +224 -7
- package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
- package/dist/cli/__tests__/sparkshell-cli.test.js +93 -41
- package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
- package/dist/cli/__tests__/team.test.js +150 -8
- package/dist/cli/__tests__/team.test.js.map +1 -1
- package/dist/cli/__tests__/ultragoal.test.js +117 -2
- package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
- package/dist/cli/__tests__/uninstall.test.js +95 -4
- package/dist/cli/__tests__/uninstall.test.js.map +1 -1
- package/dist/cli/__tests__/update.test.js +669 -177
- package/dist/cli/__tests__/update.test.js.map +1 -1
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js +33 -2
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
- package/dist/cli/api.d.ts.map +1 -1
- package/dist/cli/api.js +9 -10
- package/dist/cli/api.js.map +1 -1
- package/dist/cli/codex-feature-probe.d.ts +16 -0
- package/dist/cli/codex-feature-probe.d.ts.map +1 -1
- package/dist/cli/codex-feature-probe.js +44 -0
- package/dist/cli/codex-feature-probe.js.map +1 -1
- package/dist/cli/codex-home.d.ts.map +1 -1
- package/dist/cli/codex-home.js +7 -4
- package/dist/cli/codex-home.js.map +1 -1
- package/dist/cli/doctor.d.ts +20 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +290 -3
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.d.ts +234 -12
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2409 -746
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp-parity.js +2 -2
- package/dist/cli/mcp-parity.js.map +1 -1
- package/dist/cli/native-assets.d.ts +25 -0
- package/dist/cli/native-assets.d.ts.map +1 -1
- package/dist/cli/native-assets.js +448 -51
- package/dist/cli/native-assets.js.map +1 -1
- package/dist/cli/native-hook-claim-journal.d.ts +20 -5
- package/dist/cli/native-hook-claim-journal.d.ts.map +1 -1
- package/dist/cli/native-hook-claim-journal.js +67 -32
- package/dist/cli/native-hook-claim-journal.js.map +1 -1
- package/dist/cli/package-manager-ownership.d.ts +63 -0
- package/dist/cli/package-manager-ownership.d.ts.map +1 -0
- package/dist/cli/package-manager-ownership.js +323 -0
- package/dist/cli/package-manager-ownership.js.map +1 -0
- package/dist/cli/ralph.js +2 -2
- package/dist/cli/ralph.js.map +1 -1
- package/dist/cli/ralplan.d.ts +5 -7
- package/dist/cli/ralplan.d.ts.map +1 -1
- package/dist/cli/ralplan.js +83 -134
- package/dist/cli/ralplan.js.map +1 -1
- package/dist/cli/session-search.d.ts +5 -0
- package/dist/cli/session-search.d.ts.map +1 -1
- package/dist/cli/session-search.js +87 -0
- package/dist/cli/session-search.js.map +1 -1
- package/dist/cli/setup-preferences.d.ts +15 -0
- package/dist/cli/setup-preferences.d.ts.map +1 -1
- package/dist/cli/setup-preferences.js +27 -1
- package/dist/cli/setup-preferences.js.map +1 -1
- package/dist/cli/setup.d.ts +8 -0
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +119 -47
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/sparkshell.d.ts.map +1 -1
- package/dist/cli/sparkshell.js +52 -9
- package/dist/cli/sparkshell.js.map +1 -1
- package/dist/cli/ultragoal.d.ts.map +1 -1
- package/dist/cli/ultragoal.js +2 -2
- package/dist/cli/ultragoal.js.map +1 -1
- package/dist/cli/uninstall.d.ts +9 -0
- package/dist/cli/uninstall.d.ts.map +1 -1
- package/dist/cli/uninstall.js +71 -30
- package/dist/cli/uninstall.js.map +1 -1
- package/dist/cli/update-worker.d.ts +2 -0
- package/dist/cli/update-worker.d.ts.map +1 -0
- package/dist/cli/update-worker.js +142 -0
- package/dist/cli/update-worker.js.map +1 -0
- package/dist/cli/update.d.ts +14 -22
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +192 -241
- package/dist/cli/update.js.map +1 -1
- package/dist/compat/__tests__/rust-runtime-compat.test.js +1 -1
- package/dist/config/__tests__/generator-idempotent.test.js +503 -30
- package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
- package/dist/config/__tests__/generator-notify.test.js +5 -3
- package/dist/config/__tests__/generator-notify.test.js.map +1 -1
- package/dist/config/__tests__/models.test.js +67 -3
- package/dist/config/__tests__/models.test.js.map +1 -1
- package/dist/config/generator.d.ts +27 -21
- package/dist/config/generator.d.ts.map +1 -1
- package/dist/config/generator.js +610 -152
- package/dist/config/generator.js.map +1 -1
- package/dist/config/models.d.ts +12 -2
- package/dist/config/models.d.ts.map +1 -1
- package/dist/config/models.js +15 -1
- package/dist/config/models.js.map +1 -1
- package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js +25 -0
- package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js.map +1 -1
- package/dist/goal-workflows/codex-goal-snapshot.d.ts +1 -1
- package/dist/goal-workflows/codex-goal-snapshot.d.ts.map +1 -1
- package/dist/goal-workflows/codex-goal-snapshot.js +2 -0
- package/dist/goal-workflows/codex-goal-snapshot.js.map +1 -1
- package/dist/hooks/__tests__/agents-overlay.test.js +5 -3
- package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js +27 -4
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.d.ts +2 -0
- package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.js +172 -0
- package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.js.map +1 -0
- package/dist/hooks/__tests__/keyword-detector.test.js +176 -37
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js +402 -545
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +39 -4
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +161 -26
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js +27 -0
- package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js +10 -118
- package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-regression-205.test.js +7 -0
- package/dist/hooks/__tests__/notify-hook-regression-205.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +533 -14
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +159 -24
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js +482 -32
- package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.js +2 -1
- package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +68 -1
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js +7 -1
- package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +44 -4
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/prompt-guidance-contract.test.js +9 -0
- package/dist/hooks/__tests__/prompt-guidance-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/session.test.js +3130 -365
- package/dist/hooks/__tests__/session.test.js.map +1 -1
- package/dist/hooks/agents-overlay.js +1 -1
- package/dist/hooks/agents-overlay.js.map +1 -1
- package/dist/hooks/extensibility/__tests__/dispatcher.test.js +108 -2
- package/dist/hooks/extensibility/__tests__/dispatcher.test.js.map +1 -1
- package/dist/hooks/extensibility/__tests__/sdk.test.js +151 -1
- package/dist/hooks/extensibility/__tests__/sdk.test.js.map +1 -1
- package/dist/hooks/extensibility/dispatcher.d.ts.map +1 -1
- package/dist/hooks/extensibility/dispatcher.js +20 -3
- package/dist/hooks/extensibility/dispatcher.js.map +1 -1
- package/dist/hooks/extensibility/plugin-runner.js +1 -0
- package/dist/hooks/extensibility/plugin-runner.js.map +1 -1
- package/dist/hooks/extensibility/runtime.d.ts.map +1 -1
- package/dist/hooks/extensibility/runtime.js.map +1 -1
- package/dist/hooks/extensibility/sdk/paths.d.ts +1 -1
- package/dist/hooks/extensibility/sdk/paths.d.ts.map +1 -1
- package/dist/hooks/extensibility/sdk/paths.js +2 -2
- package/dist/hooks/extensibility/sdk/paths.js.map +1 -1
- package/dist/hooks/extensibility/sdk/runtime-state.d.ts +1 -1
- package/dist/hooks/extensibility/sdk/runtime-state.d.ts.map +1 -1
- package/dist/hooks/extensibility/sdk/runtime-state.js +55 -15
- package/dist/hooks/extensibility/sdk/runtime-state.js.map +1 -1
- package/dist/hooks/extensibility/sdk.d.ts +1 -0
- package/dist/hooks/extensibility/sdk.d.ts.map +1 -1
- package/dist/hooks/extensibility/sdk.js +1 -1
- package/dist/hooks/extensibility/sdk.js.map +1 -1
- package/dist/hooks/extensibility/types.d.ts +7 -0
- package/dist/hooks/extensibility/types.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.d.ts +10 -3
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +116 -20
- package/dist/hooks/keyword-detector.js.map +1 -1
- package/dist/hooks/session.d.ts +204 -14
- package/dist/hooks/session.d.ts.map +1 -1
- package/dist/hooks/session.js +1500 -223
- package/dist/hooks/session.js.map +1 -1
- package/dist/hud/__tests__/hud-tmux-injection.test.js +6 -6
- package/dist/hud/__tests__/hud-tmux-injection.test.js.map +1 -1
- package/dist/hud/__tests__/index.test.js +26 -16
- package/dist/hud/__tests__/index.test.js.map +1 -1
- package/dist/hud/__tests__/reconcile.test.js +83 -78
- package/dist/hud/__tests__/reconcile.test.js.map +1 -1
- package/dist/hud/__tests__/state.test.js +36 -31
- package/dist/hud/__tests__/state.test.js.map +1 -1
- package/dist/hud/__tests__/tmux.test.js +155 -198
- package/dist/hud/__tests__/tmux.test.js.map +1 -1
- package/dist/hud/tmux.d.ts +42 -4
- package/dist/hud/tmux.d.ts.map +1 -1
- package/dist/hud/tmux.js +1017 -139
- package/dist/hud/tmux.js.map +1 -1
- package/dist/leader/__tests__/contract.test.js +124 -37
- package/dist/leader/__tests__/contract.test.js.map +1 -1
- package/dist/leader/contract.d.ts +17 -6
- package/dist/leader/contract.d.ts.map +1 -1
- package/dist/leader/contract.js +102 -26
- package/dist/leader/contract.js.map +1 -1
- package/dist/mcp/__tests__/code-intel-server.test.js +31 -3
- package/dist/mcp/__tests__/code-intel-server.test.js.map +1 -1
- package/dist/mcp/__tests__/hermes-bridge.test.js +17 -0
- package/dist/mcp/__tests__/hermes-bridge.test.js.map +1 -1
- package/dist/mcp/__tests__/server-lifecycle.test.js +4 -4
- package/dist/mcp/__tests__/state-paths.test.js +735 -4
- package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
- package/dist/mcp/__tests__/state-server.test.js +1 -1
- package/dist/mcp/__tests__/state-server.test.js.map +1 -1
- package/dist/mcp/__tests__/trace-server.test.js +2 -2
- package/dist/mcp/__tests__/trace-server.test.js.map +1 -1
- package/dist/mcp/__tests__/wiki-server.test.js +18 -0
- package/dist/mcp/__tests__/wiki-server.test.js.map +1 -1
- package/dist/mcp/code-intel-server.d.ts.map +1 -1
- package/dist/mcp/code-intel-server.js +13 -3
- package/dist/mcp/code-intel-server.js.map +1 -1
- package/dist/mcp/hermes-bridge.d.ts.map +1 -1
- package/dist/mcp/hermes-bridge.js +2 -2
- package/dist/mcp/hermes-bridge.js.map +1 -1
- package/dist/mcp/state-paths.d.ts +78 -4
- package/dist/mcp/state-paths.d.ts.map +1 -1
- package/dist/mcp/state-paths.js +543 -40
- package/dist/mcp/state-paths.js.map +1 -1
- package/dist/mcp/state-server.d.ts +4 -4
- package/dist/mcp/state-server.d.ts.map +1 -1
- package/dist/mcp/wiki-server.d.ts +11 -0
- package/dist/mcp/wiki-server.d.ts.map +1 -1
- package/dist/mcp/wiki-server.js +9 -2
- package/dist/mcp/wiki-server.js.map +1 -1
- package/dist/modes/__tests__/base-autopilot-gates.test.js +36 -7
- package/dist/modes/__tests__/base-autopilot-gates.test.js.map +1 -1
- package/dist/modes/__tests__/base-multi-state-compat.test.js +1 -1
- package/dist/modes/__tests__/base-multi-state-compat.test.js.map +1 -1
- package/dist/modes/__tests__/base-session-scope.test.js +17 -8
- package/dist/modes/__tests__/base-session-scope.test.js.map +1 -1
- package/dist/modes/__tests__/base-tmux-pane.test.js +4 -4
- package/dist/modes/__tests__/base-tmux-pane.test.js.map +1 -1
- package/dist/modes/base.d.ts +5 -5
- package/dist/modes/base.d.ts.map +1 -1
- package/dist/modes/base.js +76 -21
- package/dist/modes/base.js.map +1 -1
- package/dist/native-assets/archive.d.ts +17 -0
- package/dist/native-assets/archive.d.ts.map +1 -0
- package/dist/native-assets/archive.js +238 -0
- package/dist/native-assets/archive.js.map +1 -0
- package/dist/native-assets/policy.d.ts +42 -0
- package/dist/native-assets/policy.d.ts.map +1 -0
- package/dist/native-assets/policy.js +155 -0
- package/dist/native-assets/policy.js.map +1 -0
- package/dist/notifications/temp-contract.d.ts.map +1 -1
- package/dist/notifications/temp-contract.js +4 -0
- package/dist/notifications/temp-contract.js.map +1 -1
- package/dist/pipeline/__tests__/orchestrator.test.js +164 -42
- package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
- package/dist/pipeline/__tests__/stages.test.js +72 -155
- package/dist/pipeline/__tests__/stages.test.js.map +1 -1
- package/dist/pipeline/orchestrator.d.ts.map +1 -1
- package/dist/pipeline/orchestrator.js +124 -18
- package/dist/pipeline/orchestrator.js.map +1 -1
- package/dist/pipeline/stages/ralplan.d.ts.map +1 -1
- package/dist/pipeline/stages/ralplan.js +36 -8
- package/dist/pipeline/stages/ralplan.js.map +1 -1
- package/dist/ralplan/__tests__/consensus-gate.test.js +73 -1316
- package/dist/ralplan/__tests__/consensus-gate.test.js.map +1 -1
- package/dist/ralplan/__tests__/documented-leader-preflight.test.d.ts +2 -0
- package/dist/ralplan/__tests__/documented-leader-preflight.test.d.ts.map +1 -0
- package/dist/ralplan/__tests__/documented-leader-preflight.test.js +335 -0
- package/dist/ralplan/__tests__/documented-leader-preflight.test.js.map +1 -0
- package/dist/ralplan/__tests__/runtime.test.js +85 -101
- package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
- package/dist/ralplan/consensus-gate.d.ts +9 -1
- package/dist/ralplan/consensus-gate.d.ts.map +1 -1
- package/dist/ralplan/consensus-gate.js +82 -338
- package/dist/ralplan/consensus-gate.js.map +1 -1
- package/dist/ralplan/documented-leader-preflight.d.ts +41 -0
- package/dist/ralplan/documented-leader-preflight.d.ts.map +1 -0
- package/dist/ralplan/documented-leader-preflight.js +360 -0
- package/dist/ralplan/documented-leader-preflight.js.map +1 -0
- package/dist/ralplan/runtime.d.ts +2 -1
- package/dist/ralplan/runtime.d.ts.map +1 -1
- package/dist/ralplan/runtime.js +67 -168
- package/dist/ralplan/runtime.js.map +1 -1
- package/dist/runtime/__tests__/bridge.test.js +42 -1
- package/dist/runtime/__tests__/bridge.test.js.map +1 -1
- package/dist/runtime/bridge.d.ts +12 -0
- package/dist/runtime/bridge.d.ts.map +1 -1
- package/dist/runtime/bridge.js +52 -1
- package/dist/runtime/bridge.js.map +1 -1
- package/dist/runtime/run-state.d.ts +5 -1
- package/dist/runtime/run-state.d.ts.map +1 -1
- package/dist/runtime/run-state.js +15 -8
- package/dist/runtime/run-state.js.map +1 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js +16323 -8978
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/__tests__/issue-3293-callsite-parity.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3293-callsite-parity.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3293-callsite-parity.test.js +225 -0
- package/dist/scripts/__tests__/issue-3293-callsite-parity.test.js.map +1 -0
- package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.js +188 -0
- package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.js.map +1 -0
- package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.js +259 -0
- package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.js.map +1 -0
- package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.js +491 -0
- package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.js.map +1 -0
- package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.js +708 -0
- package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.js.map +1 -0
- package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.js +208 -0
- package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.js.map +1 -0
- package/dist/scripts/__tests__/notify-state-io.test.js +26 -0
- package/dist/scripts/__tests__/notify-state-io.test.js.map +1 -1
- package/dist/scripts/__tests__/notify-tmux-injection.test.js +23 -1
- package/dist/scripts/__tests__/notify-tmux-injection.test.js.map +1 -1
- package/dist/scripts/__tests__/postinstall.test.js +19 -1
- package/dist/scripts/__tests__/postinstall.test.js.map +1 -1
- package/dist/scripts/__tests__/reasoning-artifact-contract.test.d.ts +2 -0
- package/dist/scripts/__tests__/reasoning-artifact-contract.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/reasoning-artifact-contract.test.js +108 -0
- package/dist/scripts/__tests__/reasoning-artifact-contract.test.js.map +1 -0
- package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js +63 -173
- package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js.map +1 -1
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js +74 -159
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js.map +1 -1
- package/dist/scripts/__tests__/smoke-packed-install.test.js +404 -13
- package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
- package/dist/scripts/__tests__/verify-native-release-assets.test.d.ts +2 -0
- package/dist/scripts/__tests__/verify-native-release-assets.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/verify-native-release-assets.test.js +235 -0
- package/dist/scripts/__tests__/verify-native-release-assets.test.js.map +1 -0
- package/dist/scripts/codex-native-hook.d.ts +53 -1
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +14448 -1480
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/scripts/codex-native-pre-post.d.ts +9 -0
- package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
- package/dist/scripts/codex-native-pre-post.js +35 -12
- package/dist/scripts/codex-native-pre-post.js.map +1 -1
- package/dist/scripts/generate-native-release-manifest.js +80 -51
- package/dist/scripts/generate-native-release-manifest.js.map +1 -1
- package/dist/scripts/notify-fallback-watcher.js +78 -115
- package/dist/scripts/notify-fallback-watcher.js.map +1 -1
- package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
- package/dist/scripts/notify-hook/auto-nudge.js +74 -3
- package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
- package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
- package/dist/scripts/notify-hook/state-io.js +5 -0
- package/dist/scripts/notify-hook/state-io.js.map +1 -1
- package/dist/scripts/notify-hook/team-dispatch.d.ts +9 -0
- package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-dispatch.js +234 -79
- package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
- package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-leader-nudge.js +71 -16
- package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.d.ts +12 -4
- package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.js +365 -44
- package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
- package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-worker-stop.js +42 -21
- package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
- package/dist/scripts/notify-hook/team-worker.d.ts +3 -0
- package/dist/scripts/notify-hook/team-worker.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-worker.js +82 -33
- package/dist/scripts/notify-hook/team-worker.js.map +1 -1
- package/dist/scripts/notify-hook/tmux-injection.d.ts +1 -0
- package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
- package/dist/scripts/notify-hook/tmux-injection.js +32 -3
- package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
- package/dist/scripts/notify-hook.js +79 -82
- package/dist/scripts/notify-hook.js.map +1 -1
- package/dist/scripts/postinstall-advisory.d.ts +14 -0
- package/dist/scripts/postinstall-advisory.d.ts.map +1 -0
- package/dist/scripts/postinstall-advisory.js +49 -0
- package/dist/scripts/postinstall-advisory.js.map +1 -0
- package/dist/scripts/postinstall.d.ts +1 -1
- package/dist/scripts/postinstall.d.ts.map +1 -1
- package/dist/scripts/postinstall.js +1 -1
- package/dist/scripts/postinstall.js.map +1 -1
- package/dist/scripts/smoke-packed-install.d.ts +80 -4
- package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
- package/dist/scripts/smoke-packed-install.js +3162 -26
- package/dist/scripts/smoke-packed-install.js.map +1 -1
- package/dist/scripts/sync-plugin-mirror.d.ts.map +1 -1
- package/dist/scripts/sync-plugin-mirror.js +16 -1
- package/dist/scripts/sync-plugin-mirror.js.map +1 -1
- package/dist/scripts/verify-native-release-assets.js +37 -23
- package/dist/scripts/verify-native-release-assets.js.map +1 -1
- package/dist/state/__tests__/mode-state-context.test.js +61 -0
- package/dist/state/__tests__/mode-state-context.test.js.map +1 -1
- package/dist/state/__tests__/operations.test.js +462 -850
- package/dist/state/__tests__/operations.test.js.map +1 -1
- package/dist/state/__tests__/skill-active.test.js +400 -2
- package/dist/state/__tests__/skill-active.test.js.map +1 -1
- package/dist/state/mode-state-context.d.ts +8 -0
- package/dist/state/mode-state-context.d.ts.map +1 -1
- package/dist/state/mode-state-context.js +73 -0
- package/dist/state/mode-state-context.js.map +1 -1
- package/dist/state/operations.d.ts +3 -0
- package/dist/state/operations.d.ts.map +1 -1
- package/dist/state/operations.js +144 -40
- package/dist/state/operations.js.map +1 -1
- package/dist/state/skill-active.d.ts +18 -1
- package/dist/state/skill-active.d.ts.map +1 -1
- package/dist/state/skill-active.js +420 -23
- package/dist/state/skill-active.js.map +1 -1
- package/dist/state/workflow-transition-reconcile.d.ts +3 -0
- package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
- package/dist/state/workflow-transition-reconcile.js +10 -4
- package/dist/state/workflow-transition-reconcile.js.map +1 -1
- package/dist/state/workflow-transition.d.ts.map +1 -1
- package/dist/state/workflow-transition.js +3 -1
- package/dist/state/workflow-transition.js.map +1 -1
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.js +53 -247
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.js.map +1 -1
- package/dist/subagents/__tests__/tracker.test.js +837 -1048
- package/dist/subagents/__tests__/tracker.test.js.map +1 -1
- package/dist/subagents/tracker.d.ts +58 -135
- package/dist/subagents/tracker.d.ts.map +1 -1
- package/dist/subagents/tracker.js +811 -632
- package/dist/subagents/tracker.js.map +1 -1
- package/dist/team/__tests__/api-interop.test.js +91 -1
- package/dist/team/__tests__/api-interop.test.js.map +1 -1
- package/dist/team/__tests__/delivery-e2e-smoke.test.js +58 -17
- package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
- package/dist/team/__tests__/idle-nudge.test.js +45 -15
- package/dist/team/__tests__/idle-nudge.test.js.map +1 -1
- package/dist/team/__tests__/mcp-comm.test.js +197 -30
- package/dist/team/__tests__/mcp-comm.test.js.map +1 -1
- package/dist/team/__tests__/model-contract.test.js +110 -3
- package/dist/team/__tests__/model-contract.test.js.map +1 -1
- package/dist/team/__tests__/notice-ledger.test.d.ts +2 -0
- package/dist/team/__tests__/notice-ledger.test.d.ts.map +1 -0
- package/dist/team/__tests__/notice-ledger.test.js +131 -0
- package/dist/team/__tests__/notice-ledger.test.js.map +1 -0
- package/dist/team/__tests__/pane-teardown-residue-e2e.test.d.ts +2 -0
- package/dist/team/__tests__/pane-teardown-residue-e2e.test.d.ts.map +1 -0
- package/dist/team/__tests__/pane-teardown-residue-e2e.test.js +410 -0
- package/dist/team/__tests__/pane-teardown-residue-e2e.test.js.map +1 -0
- package/dist/team/__tests__/phase-controller.test.js +20 -0
- package/dist/team/__tests__/phase-controller.test.js.map +1 -1
- package/dist/team/__tests__/runtime-cli.test.js +9 -0
- package/dist/team/__tests__/runtime-cli.test.js.map +1 -1
- package/dist/team/__tests__/runtime.test.js +3963 -428
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/scaling.test.js +2405 -203
- package/dist/team/__tests__/scaling.test.js.map +1 -1
- package/dist/team/__tests__/state-root.test.js +117 -2
- package/dist/team/__tests__/state-root.test.js.map +1 -1
- package/dist/team/__tests__/state.test.js +804 -36
- package/dist/team/__tests__/state.test.js.map +1 -1
- package/dist/team/__tests__/team-ops-contract.test.js +6 -0
- package/dist/team/__tests__/team-ops-contract.test.js.map +1 -1
- package/dist/team/__tests__/tmux-session.test.js +4099 -1039
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/__tests__/tmux-source-authority-realtmux.test.d.ts +2 -0
- package/dist/team/__tests__/tmux-source-authority-realtmux.test.d.ts.map +1 -0
- package/dist/team/__tests__/tmux-source-authority-realtmux.test.js +149 -0
- package/dist/team/__tests__/tmux-source-authority-realtmux.test.js.map +1 -0
- package/dist/team/__tests__/tmux-test-fixture.d.ts +69 -0
- package/dist/team/__tests__/tmux-test-fixture.d.ts.map +1 -1
- package/dist/team/__tests__/tmux-test-fixture.js +242 -15
- package/dist/team/__tests__/tmux-test-fixture.js.map +1 -1
- package/dist/team/__tests__/tmux-test-fixture.test.js +177 -1
- package/dist/team/__tests__/tmux-test-fixture.test.js.map +1 -1
- package/dist/team/__tests__/worker-runtime-identity.test.js +53 -44
- package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
- package/dist/team/__tests__/worktree.test.js +6 -0
- package/dist/team/__tests__/worktree.test.js.map +1 -1
- package/dist/team/api-interop.d.ts.map +1 -1
- package/dist/team/api-interop.js +15 -9
- package/dist/team/api-interop.js.map +1 -1
- package/dist/team/exact-pane.d.ts +23 -0
- package/dist/team/exact-pane.d.ts.map +1 -0
- package/dist/team/exact-pane.js +133 -0
- package/dist/team/exact-pane.js.map +1 -0
- package/dist/team/idle-nudge.d.ts +12 -1
- package/dist/team/idle-nudge.d.ts.map +1 -1
- package/dist/team/idle-nudge.js +41 -14
- package/dist/team/idle-nudge.js.map +1 -1
- package/dist/team/model-contract.d.ts +2 -5
- package/dist/team/model-contract.d.ts.map +1 -1
- package/dist/team/model-contract.js +59 -17
- package/dist/team/model-contract.js.map +1 -1
- package/dist/team/notice-ledger.d.ts +54 -0
- package/dist/team/notice-ledger.d.ts.map +1 -0
- package/dist/team/notice-ledger.js +259 -0
- package/dist/team/notice-ledger.js.map +1 -0
- package/dist/team/phase-controller.d.ts.map +1 -1
- package/dist/team/phase-controller.js +2 -0
- package/dist/team/phase-controller.js.map +1 -1
- package/dist/team/runtime.d.ts +4 -1
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +1527 -291
- package/dist/team/runtime.js.map +1 -1
- package/dist/team/scaling.d.ts +13 -1
- package/dist/team/scaling.d.ts.map +1 -1
- package/dist/team/scaling.js +1503 -387
- package/dist/team/scaling.js.map +1 -1
- package/dist/team/state/dispatch.d.ts.map +1 -1
- package/dist/team/state/dispatch.js +26 -8
- package/dist/team/state/dispatch.js.map +1 -1
- package/dist/team/state/locks.d.ts.map +1 -1
- package/dist/team/state/locks.js +61 -13
- package/dist/team/state/locks.js.map +1 -1
- package/dist/team/state/monitor.d.ts +10 -0
- package/dist/team/state/monitor.d.ts.map +1 -1
- package/dist/team/state/monitor.js +13 -0
- package/dist/team/state/monitor.js.map +1 -1
- package/dist/team/state/tasks.d.ts.map +1 -1
- package/dist/team/state/tasks.js +4 -3
- package/dist/team/state/tasks.js.map +1 -1
- package/dist/team/state/types.d.ts +15 -0
- package/dist/team/state/types.d.ts.map +1 -1
- package/dist/team/state/types.js.map +1 -1
- package/dist/team/state-root.d.ts +12 -0
- package/dist/team/state-root.d.ts.map +1 -1
- package/dist/team/state-root.js +177 -0
- package/dist/team/state-root.js.map +1 -1
- package/dist/team/state.d.ts +85 -1
- package/dist/team/state.d.ts.map +1 -1
- package/dist/team/state.js +672 -129
- package/dist/team/state.js.map +1 -1
- package/dist/team/team-ops.d.ts +5 -0
- package/dist/team/team-ops.d.ts.map +1 -1
- package/dist/team/team-ops.js +4 -0
- package/dist/team/team-ops.js.map +1 -1
- package/dist/team/tmux-session.d.ts +163 -27
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +1682 -376
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/team/worktree.d.ts.map +1 -1
- package/dist/team/worktree.js +4 -0
- package/dist/team/worktree.js.map +1 -1
- package/dist/ultragoal/__tests__/artifacts.test.js +950 -2
- package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
- package/dist/ultragoal/__tests__/docs-contract.test.js +1 -0
- package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
- package/dist/ultragoal/artifacts.d.ts +35 -1
- package/dist/ultragoal/artifacts.d.ts.map +1 -1
- package/dist/ultragoal/artifacts.js +232 -46
- package/dist/ultragoal/artifacts.js.map +1 -1
- package/dist/utils/__tests__/file-durability.test.d.ts +2 -0
- package/dist/utils/__tests__/file-durability.test.d.ts.map +1 -0
- package/dist/utils/__tests__/file-durability.test.js +114 -0
- package/dist/utils/__tests__/file-durability.test.js.map +1 -0
- package/dist/utils/__tests__/paths.test.js +21 -1
- package/dist/utils/__tests__/paths.test.js.map +1 -1
- package/dist/utils/file-durability.d.ts +25 -0
- package/dist/utils/file-durability.d.ts.map +1 -0
- package/dist/utils/file-durability.js +67 -0
- package/dist/utils/file-durability.js.map +1 -0
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +14 -6
- package/dist/utils/paths.js.map +1 -1
- package/dist/verification/__tests__/ci-rust-gates.test.js +49 -4
- package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
- package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js +3 -0
- package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js.map +1 -1
- package/dist/verification/__tests__/explore-harness-release-workflow.test.js +60 -0
- package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +1 -1
- package/dist/verification/__tests__/native-release-manifest.test.js +157 -4
- package/dist/verification/__tests__/native-release-manifest.test.js.map +1 -1
- package/dist/wiki/lint.d.ts +4 -1
- package/dist/wiki/lint.d.ts.map +1 -1
- package/dist/wiki/lint.js +3 -2
- package/dist/wiki/lint.js.map +1 -1
- package/package.json +13 -5
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +105 -17
- package/plugins/oh-my-codex/skills/autopilot/SKILL.md +13 -5
- package/plugins/oh-my-codex/skills/cancel/SKILL.md +30 -85
- package/plugins/oh-my-codex/skills/ralph/SKILL.md +6 -4
- package/plugins/oh-my-codex/skills/ralplan/SKILL.md +16 -17
- package/plugins/oh-my-codex/skills/team/SKILL.md +13 -9
- package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +3 -2
- package/skills/autopilot/SKILL.md +13 -5
- package/skills/cancel/SKILL.md +30 -85
- package/skills/ralph/SKILL.md +6 -4
- package/skills/ralplan/SKILL.md +16 -17
- package/skills/team/SKILL.md +13 -9
- package/skills/ultragoal/SKILL.md +3 -2
- package/src/scripts/__tests__/codex-native-hook.test.ts +8831 -951
- package/src/scripts/__tests__/issue-3293-callsite-parity.test.ts +187 -0
- package/src/scripts/__tests__/issue-3293-cancel-transaction.test.ts +189 -0
- package/src/scripts/__tests__/issue-3293-hook-owned-cancel.test.ts +187 -0
- package/src/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.ts +591 -0
- package/src/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.ts +701 -0
- package/src/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.ts +211 -0
- package/src/scripts/__tests__/notify-state-io.test.ts +29 -0
- package/src/scripts/__tests__/notify-tmux-injection.test.ts +26 -1
- package/src/scripts/__tests__/postinstall.test.ts +23 -1
- package/src/scripts/__tests__/reasoning-artifact-contract.test.ts +129 -0
- package/src/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.ts +67 -185
- package/src/scripts/__tests__/role-intent-durable-recovery-3181.test.ts +79 -178
- package/src/scripts/__tests__/smoke-packed-install.test.ts +525 -16
- package/src/scripts/__tests__/verify-native-release-assets.test.ts +240 -0
- package/src/scripts/codex-native-hook.ts +14238 -1429
- package/src/scripts/codex-native-pre-post.ts +43 -12
- package/src/scripts/generate-native-release-manifest.ts +83 -79
- package/src/scripts/notify-fallback-watcher.ts +91 -122
- package/src/scripts/notify-hook/auto-nudge.ts +71 -3
- package/src/scripts/notify-hook/state-io.ts +11 -0
- package/src/scripts/notify-hook/team-dispatch.ts +242 -75
- package/src/scripts/notify-hook/team-leader-nudge.ts +72 -22
- package/src/scripts/notify-hook/team-tmux-guard.ts +366 -39
- package/src/scripts/notify-hook/team-worker-stop.ts +43 -20
- package/src/scripts/notify-hook/team-worker.ts +82 -31
- package/src/scripts/notify-hook/tmux-injection.ts +33 -3
- package/src/scripts/notify-hook.ts +84 -82
- package/src/scripts/postinstall-advisory.ts +67 -0
- package/src/scripts/postinstall.ts +1 -1
- package/src/scripts/smoke-packed-install.ts +3608 -39
- package/src/scripts/sync-plugin-mirror.ts +17 -1
- package/src/scripts/verify-native-release-assets.ts +39 -31
- package/templates/AGENTS.md +1 -0
- package/dist/subagents/__tests__/adapted-role-binding.test.d.ts +0 -2
- package/dist/subagents/__tests__/adapted-role-binding.test.d.ts.map +0 -1
- package/dist/subagents/__tests__/adapted-role-binding.test.js +0 -988
- package/dist/subagents/__tests__/adapted-role-binding.test.js.map +0 -1
- package/dist/subagents/adapted-role-binding.d.ts +0 -17
- package/dist/subagents/adapted-role-binding.d.ts.map +0 -1
- package/dist/subagents/adapted-role-binding.js +0 -75
- package/dist/subagents/adapted-role-binding.js.map +0 -1
|
@@ -1,8 +1,9 @@
|
|
|
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 {
|
|
5
|
+
import { randomBytes } from 'crypto';
|
|
6
|
+
import { dirname, isAbsolute, join, relative, resolve, sep } from 'path';
|
|
6
7
|
import { CODEX_BYPASS_FLAG, CLAUDE_SKIP_PERMISSIONS_FLAG, MADMAX_FLAG, CONFIG_FLAG, LONG_CONFIG_FLAG, MODEL_FLAG, } from '../cli/constants.js';
|
|
7
8
|
import { getAgent } from '../agents/definitions.js';
|
|
8
9
|
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';
|
|
@@ -11,6 +12,8 @@ import { classifyTeamWorkerLaunchPolicy, extractModelProviderOverrideValue, norm
|
|
|
11
12
|
import { sleep, sleepSync } from '../utils/sleep.js';
|
|
12
13
|
import { buildPlatformCommandSpec, classifySpawnError, resolveCommandPathForPlatform, spawnPlatformCommandSync, } from '../utils/platform-command.js';
|
|
13
14
|
import { resolveOmxCliEntryPath } from '../utils/paths.js';
|
|
15
|
+
import { readExactPaneProof, readExactPaneProofsSync, readExactPaneProofSync } from './exact-pane.js';
|
|
16
|
+
import { resolveCanonicalTeamStateRoot } from './state-root.js';
|
|
14
17
|
const execFileAsync = promisify(execFile);
|
|
15
18
|
import { HUD_RESIZE_RECONCILE_DELAY_SECONDS, HUD_TMUX_TEAM_HEIGHT_LINES } from '../hud/constants.js';
|
|
16
19
|
import { OMX_TMUX_HUD_OWNER_ENV } from '../hud/reconcile.js';
|
|
@@ -18,6 +21,51 @@ import { findHudWatchPaneIds, hudPaneMatchesOwner, OMX_TMUX_HUD_LEADER_PANE_ENV
|
|
|
18
21
|
const OMX_INSTANCE_OPTION = '@omx_instance_id';
|
|
19
22
|
const OMX_PANE_INSTANCE_OPTION = '@omx_pane_instance_id';
|
|
20
23
|
const OMX_TEAM_PANE_OWNER_OPTION = '@omx_team_pane_owner_id';
|
|
24
|
+
/** `kill(pid, 0)` establishes absence only on ESRCH. */
|
|
25
|
+
function probeProcessLiveness(pid) {
|
|
26
|
+
try {
|
|
27
|
+
process.kill(pid, 0);
|
|
28
|
+
return 'alive';
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
return error.code === 'ESRCH' ? 'gone' : 'unknown';
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/** Carries live tmux resources when creation cannot safely roll them back. */
|
|
35
|
+
export class CreateTeamSessionPartialError extends Error {
|
|
36
|
+
partialSession;
|
|
37
|
+
proofUnavailable;
|
|
38
|
+
originalError;
|
|
39
|
+
cleanupErrors;
|
|
40
|
+
constructor(partialSession, proofUnavailable, originalError,
|
|
41
|
+
/** Cleanup commands that failed after resources were created and must be retried. */
|
|
42
|
+
cleanupErrors = []) {
|
|
43
|
+
super('create_team_session_cleanup_incomplete');
|
|
44
|
+
this.partialSession = partialSession;
|
|
45
|
+
this.proofUnavailable = proofUnavailable;
|
|
46
|
+
this.originalError = originalError;
|
|
47
|
+
this.cleanupErrors = cleanupErrors;
|
|
48
|
+
this.name = 'CreateTeamSessionPartialError';
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
class ExactPaneProofUnavailableError extends Error {
|
|
52
|
+
proof;
|
|
53
|
+
constructor(proof) {
|
|
54
|
+
super(`exact_pane_proof_unavailable:${proof.paneId}:${proof.reason}`);
|
|
55
|
+
this.proof = proof;
|
|
56
|
+
this.name = 'ExactPaneProofUnavailableError';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
class AmbiguousSplitWindowOutputError extends Error {
|
|
60
|
+
newlyObservedPaneIds;
|
|
61
|
+
reconciliationError;
|
|
62
|
+
constructor(newlyObservedPaneIds, reconciliationError = null) {
|
|
63
|
+
super('tmux_split_window_output_ambiguous');
|
|
64
|
+
this.newlyObservedPaneIds = newlyObservedPaneIds;
|
|
65
|
+
this.reconciliationError = reconciliationError;
|
|
66
|
+
this.name = 'AmbiguousSplitWindowOutputError';
|
|
67
|
+
}
|
|
68
|
+
}
|
|
21
69
|
const INJECTION_MARKER = '[OMX_TMUX_INJECT]';
|
|
22
70
|
const MODEL_INSTRUCTIONS_FILE_KEY = 'model_instructions_file';
|
|
23
71
|
const OMX_BYPASS_DEFAULT_SYSTEM_PROMPT_ENV = 'OMX_BYPASS_DEFAULT_SYSTEM_PROMPT';
|
|
@@ -64,7 +112,7 @@ const TMUX_PANE_STABILITY_POLL_MS = 60;
|
|
|
64
112
|
const TMUX_PANE_STABILITY_POLLS_REQUIRED = 2;
|
|
65
113
|
const TMUX_PANE_STABILITY_TIMEOUT_MS = 750;
|
|
66
114
|
const OMX_TEAM_STATE_ROOT_ENV = 'OMX_TEAM_STATE_ROOT';
|
|
67
|
-
function runTmux(args) {
|
|
115
|
+
function runTmux(args, preserveStdout = false) {
|
|
68
116
|
const { result } = spawnPlatformCommandSync('tmux', args, { encoding: 'utf-8' });
|
|
69
117
|
if (result.error) {
|
|
70
118
|
return { ok: false, stderr: result.error.message };
|
|
@@ -72,7 +120,325 @@ function runTmux(args) {
|
|
|
72
120
|
if (result.status !== 0) {
|
|
73
121
|
return { ok: false, stderr: (result.stderr || '').trim() || `tmux exited ${result.status}` };
|
|
74
122
|
}
|
|
75
|
-
|
|
123
|
+
const stdout = result.stdout || '';
|
|
124
|
+
return { ok: true, stdout: preserveStdout ? stdout : stdout.trim() };
|
|
125
|
+
}
|
|
126
|
+
export function parseSplitWindowPaneId(stdout, expectedReceipt) {
|
|
127
|
+
const match = expectedReceipt === undefined
|
|
128
|
+
? /^(%\d+)(?:\r?\n)?$/.exec(stdout)
|
|
129
|
+
: /^(%\d+)\t([^\r\n\t]+)(?:\r?\n)?$/.exec(stdout);
|
|
130
|
+
if (!match || (expectedReceipt !== undefined && match[2] !== expectedReceipt))
|
|
131
|
+
return null;
|
|
132
|
+
return match[1];
|
|
133
|
+
}
|
|
134
|
+
/** Preserve structured tmux fields, including an empty final field. */
|
|
135
|
+
/** Preserve structured tmux fields, including an empty final field. */
|
|
136
|
+
function runTmuxStructured(args) {
|
|
137
|
+
const { result } = spawnPlatformCommandSync('tmux', args, { encoding: 'utf-8' });
|
|
138
|
+
if (result.error)
|
|
139
|
+
return { ok: false, stderr: result.error.message };
|
|
140
|
+
if (result.status !== 0) {
|
|
141
|
+
return { ok: false, stderr: (result.stderr || '').trim() || `tmux exited ${result.status}` };
|
|
142
|
+
}
|
|
143
|
+
return { ok: true, stdout: (result.stdout || '').replace(/\r?\n$/, '') };
|
|
144
|
+
}
|
|
145
|
+
function captureSourcePaneAuthority(paneId, expectedTeamPaneOwnerId) {
|
|
146
|
+
const target = paneId.trim();
|
|
147
|
+
if (!/^%[0-9]+$/.test(target))
|
|
148
|
+
throw new Error(`invalid tmux source pane: ${paneId}`);
|
|
149
|
+
const result = runTmuxStructured([
|
|
150
|
+
'display-message', '-p', '-t', target,
|
|
151
|
+
'#{session_name}\t#{session_id}\t#{session_created}\t#{window_index}\t#{window_id}\t#{pane_id}\t#{pane_pid}',
|
|
152
|
+
]);
|
|
153
|
+
if (!result.ok)
|
|
154
|
+
throw new Error(`failed to capture tmux source authority: ${result.stderr}`);
|
|
155
|
+
const fields = result.stdout.split('\t');
|
|
156
|
+
if (fields.length !== 7)
|
|
157
|
+
throw new Error('malformed tmux source authority');
|
|
158
|
+
const [sessionName, sessionId, sessionCreated, windowIndex, windowId, capturedPaneId, panePid] = fields;
|
|
159
|
+
if (!sessionName || !/^\$[0-9]+$/.test(sessionId) || !/^[0-9]+$/.test(sessionCreated)
|
|
160
|
+
|| !/^[0-9]+$/.test(windowIndex) || !/^@[0-9]+$/.test(windowId)
|
|
161
|
+
|| capturedPaneId !== target || !/^[1-9][0-9]*$/.test(panePid)) {
|
|
162
|
+
throw new Error('malformed tmux source authority');
|
|
163
|
+
}
|
|
164
|
+
const parsedPid = Number(panePid);
|
|
165
|
+
if (!Number.isSafeInteger(parsedPid) || parsedPid <= 0)
|
|
166
|
+
throw new Error('malformed tmux source authority');
|
|
167
|
+
const proof = readExactPaneProofSync(target);
|
|
168
|
+
if (proof.status === 'unavailable')
|
|
169
|
+
throw new ExactPaneProofUnavailableError(proof);
|
|
170
|
+
if (proof.status !== 'live' || proof.pid !== parsedPid)
|
|
171
|
+
throw new Error(`tmux pane identity changed: ${target}`);
|
|
172
|
+
const ownerResult = runTmuxStructured(['show-option', '-qv', '-p', '-t', target, OMX_TEAM_PANE_OWNER_OPTION]);
|
|
173
|
+
if (!ownerResult.ok)
|
|
174
|
+
throw new Error(`failed to capture tmux pane owner: ${ownerResult.stderr}`);
|
|
175
|
+
const teamPaneOwnerId = ownerResult.stdout.trim() || null;
|
|
176
|
+
if (teamPaneOwnerId && !/^[A-Za-z0-9._:-]+$/.test(teamPaneOwnerId)) {
|
|
177
|
+
throw new Error('malformed tmux pane owner');
|
|
178
|
+
}
|
|
179
|
+
if (expectedTeamPaneOwnerId !== undefined && teamPaneOwnerId !== expectedTeamPaneOwnerId) {
|
|
180
|
+
throw new Error(`tmux pane team owner changed: ${target}`);
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
paneId: target,
|
|
184
|
+
panePid: parsedPid,
|
|
185
|
+
sessionName,
|
|
186
|
+
sessionId,
|
|
187
|
+
sessionCreated,
|
|
188
|
+
windowId,
|
|
189
|
+
windowIndex,
|
|
190
|
+
teamPaneOwnerId,
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
function sourceAuthorityPredicate(source) {
|
|
194
|
+
return [
|
|
195
|
+
'#{==:#{pane_dead},0}',
|
|
196
|
+
`#{==:#{pane_id},${source.paneId}}`,
|
|
197
|
+
`#{==:#{pane_pid},${source.panePid}}`,
|
|
198
|
+
`#{==:#{session_id},${source.sessionId}}`,
|
|
199
|
+
`#{==:#{session_created},${source.sessionCreated}}`,
|
|
200
|
+
`#{==:#{window_id},${source.windowId}}`,
|
|
201
|
+
...(source.teamPaneOwnerId ? [`#{==:#{${OMX_TEAM_PANE_OWNER_OPTION}},${source.teamPaneOwnerId}}`] : []),
|
|
202
|
+
].reduce((combined, condition) => `#{&&:${combined},${condition}}`);
|
|
203
|
+
}
|
|
204
|
+
function sourceTransactionReceipt() {
|
|
205
|
+
return `omx_source_${process.pid}_${Date.now()}_${randomBytes(16).toString('hex')}`;
|
|
206
|
+
}
|
|
207
|
+
function bindSplitReceiptToPaneCommand(command, receipt) {
|
|
208
|
+
return `${command} # ${receipt}`;
|
|
209
|
+
}
|
|
210
|
+
/** Queue an effect in the source pane's tmux server only when its exact pane/session/window incarnation still matches. */
|
|
211
|
+
export function runSourceAuthorizedTmux(source, effect, receipt = sourceTransactionReceipt()) {
|
|
212
|
+
const result = runTmux([
|
|
213
|
+
'if-shell', '-F', '-t', source.paneId, sourceAuthorityPredicate(source),
|
|
214
|
+
`${effect} ; display-message -p ${shellQuoteSingle(receipt)}`,
|
|
215
|
+
"display-message -p ''",
|
|
216
|
+
]);
|
|
217
|
+
if (!result.ok)
|
|
218
|
+
throw new Error(`tmux source authority transaction failed: ${result.stderr}`);
|
|
219
|
+
if (result.stdout !== receipt)
|
|
220
|
+
throw new Error('tmux source authority changed before effect');
|
|
221
|
+
return receipt;
|
|
222
|
+
}
|
|
223
|
+
function runSourceAuthorizedSplit(source, buildEffect) {
|
|
224
|
+
// Reconcile against the immutable window ID, not the mutable session:index
|
|
225
|
+
// target: a concurrent renumber between the guarded split and the after-read
|
|
226
|
+
// must never turn an unrelated window's pane into kill authority.
|
|
227
|
+
const windowTarget = source.windowId;
|
|
228
|
+
const beforeTopology = listPanesResult(windowTarget);
|
|
229
|
+
if (beforeTopology.error) {
|
|
230
|
+
throw new Error(`failed to read tmux pane topology before split: ${beforeTopology.error}`);
|
|
231
|
+
}
|
|
232
|
+
const beforePaneIds = new Set(beforeTopology.panes.map((pane) => pane.paneId));
|
|
233
|
+
const receipt = sourceTransactionReceipt();
|
|
234
|
+
const effect = buildEffect(receipt);
|
|
235
|
+
const result = runTmux([
|
|
236
|
+
'if-shell', '-F', '-t', source.paneId, sourceAuthorityPredicate(source),
|
|
237
|
+
effect.replace("-F '#{pane_id}'", `-F '#{pane_id}\\t${receipt}'`),
|
|
238
|
+
"display-message -p ''",
|
|
239
|
+
], true);
|
|
240
|
+
if (!result.ok)
|
|
241
|
+
throw new Error(`tmux source authority transaction failed: ${result.stderr}`);
|
|
242
|
+
const paneId = parseSplitWindowPaneId(result.stdout, receipt);
|
|
243
|
+
if (!paneId) {
|
|
244
|
+
const afterTopology = listPanesResult(windowTarget);
|
|
245
|
+
const newlyObservedPaneIds = afterTopology.error
|
|
246
|
+
? []
|
|
247
|
+
: afterTopology.panes
|
|
248
|
+
.filter((pane) => !beforePaneIds.has(pane.paneId) && pane.startCommand.includes(receipt))
|
|
249
|
+
.map((pane) => pane.paneId);
|
|
250
|
+
throw new AmbiguousSplitWindowOutputError(newlyObservedPaneIds, afterTopology.error);
|
|
251
|
+
}
|
|
252
|
+
return paneId;
|
|
253
|
+
}
|
|
254
|
+
function restoredHudCleanupDebtPath(cwd, stateRoot) {
|
|
255
|
+
const canonicalStateRoot = resolveCanonicalTeamStateRoot(resolve(cwd));
|
|
256
|
+
if (typeof stateRoot !== 'string' || !stateRoot.trim()) {
|
|
257
|
+
return join(canonicalStateRoot, '.restored-hud-cleanup-debt.json');
|
|
258
|
+
}
|
|
259
|
+
const canonicalTeamsRoot = join(canonicalStateRoot, 'team');
|
|
260
|
+
const root = resolve(stateRoot);
|
|
261
|
+
const relativeRoot = relative(canonicalTeamsRoot, root);
|
|
262
|
+
if (!isAbsolute(stateRoot.trim())
|
|
263
|
+
|| !relativeRoot
|
|
264
|
+
|| relativeRoot === '..'
|
|
265
|
+
|| relativeRoot.startsWith(`..${sep}`)
|
|
266
|
+
|| isAbsolute(relativeRoot)
|
|
267
|
+
|| relativeRoot.split(sep).length !== 1) {
|
|
268
|
+
throw new Error('restored_hud_cleanup_debt_state_root_invalid');
|
|
269
|
+
}
|
|
270
|
+
for (let candidate = root;; candidate = dirname(candidate)) {
|
|
271
|
+
try {
|
|
272
|
+
const metadata = lstatSync(candidate);
|
|
273
|
+
if (metadata.isSymbolicLink() || !metadata.isDirectory()) {
|
|
274
|
+
throw new Error('restored_hud_cleanup_debt_state_root_invalid');
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
if (error.code !== 'ENOENT')
|
|
279
|
+
throw error;
|
|
280
|
+
}
|
|
281
|
+
if (candidate === canonicalTeamsRoot)
|
|
282
|
+
break;
|
|
283
|
+
}
|
|
284
|
+
return join(root, '.restored-hud-cleanup-debt.json');
|
|
285
|
+
}
|
|
286
|
+
function syncRestoredHudDebtParentSync(path) {
|
|
287
|
+
let descriptor;
|
|
288
|
+
try {
|
|
289
|
+
descriptor = openSync(dirname(path), 'r');
|
|
290
|
+
fsyncSync(descriptor);
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
const code = error.code;
|
|
294
|
+
if (!(code === 'EPERM' && process.platform === 'win32'))
|
|
295
|
+
throw error;
|
|
296
|
+
}
|
|
297
|
+
finally {
|
|
298
|
+
if (descriptor !== undefined)
|
|
299
|
+
closeSync(descriptor);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
function persistRestoredHudCleanupDebtSync(cwd, debt, stateRoot) {
|
|
303
|
+
const debtPath = restoredHudCleanupDebtPath(cwd, stateRoot);
|
|
304
|
+
mkdirSync(dirname(debtPath), { recursive: true });
|
|
305
|
+
// Recheck after mkdir: an untrusted Team root must still be canonical and
|
|
306
|
+
// non-symlinked immediately before the durable file mutation.
|
|
307
|
+
restoredHudCleanupDebtPath(cwd, stateRoot);
|
|
308
|
+
const temporaryPath = `${debtPath}.tmp.${process.pid}.${Date.now()}.${Math.random().toString(16).slice(2)}`;
|
|
309
|
+
writeFileSync(temporaryPath, `${JSON.stringify(debt)}\n`, { mode: 0o600 });
|
|
310
|
+
const temporaryDescriptor = openSync(temporaryPath, 'r');
|
|
311
|
+
try {
|
|
312
|
+
fsyncSync(temporaryDescriptor);
|
|
313
|
+
}
|
|
314
|
+
finally {
|
|
315
|
+
closeSync(temporaryDescriptor);
|
|
316
|
+
}
|
|
317
|
+
renameSync(temporaryPath, debtPath);
|
|
318
|
+
syncRestoredHudDebtParentSync(debtPath);
|
|
319
|
+
}
|
|
320
|
+
function parseRestoredHudCleanupDebtSync(cwd, stateRoot) {
|
|
321
|
+
const debtPath = restoredHudCleanupDebtPath(cwd, stateRoot);
|
|
322
|
+
if (!existsSync(debtPath))
|
|
323
|
+
return null;
|
|
324
|
+
let debt;
|
|
325
|
+
try {
|
|
326
|
+
debt = JSON.parse(readFileSync(debtPath, 'utf8'));
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
throw new Error('restored_hud_cleanup_debt_malformed');
|
|
330
|
+
}
|
|
331
|
+
if (debt.schema_version !== 1 || debt.operation !== 'restored_hud_cleanup'
|
|
332
|
+
|| !hasExplicitWorkerPaneId(debt.pane_id)
|
|
333
|
+
|| !hasExplicitWorkerPaneId(debt.leader_pane_id)
|
|
334
|
+
|| !Number.isSafeInteger(debt.leader_pane_pid) || debt.leader_pane_pid <= 0
|
|
335
|
+
|| !(typeof debt.leader_pane_owner_id === 'string' || debt.leader_pane_owner_id === null)
|
|
336
|
+
|| debt.hud_owner_leader_pane_id !== debt.leader_pane_id
|
|
337
|
+
|| (debt.pane_pid !== null && (!Number.isSafeInteger(debt.pane_pid) || debt.pane_pid <= 0))) {
|
|
338
|
+
throw new Error('restored_hud_cleanup_debt_malformed');
|
|
339
|
+
}
|
|
340
|
+
return { path: debtPath, debt };
|
|
341
|
+
}
|
|
342
|
+
function removeRestoredHudCleanupDebtSync(path) {
|
|
343
|
+
unlinkSync(path);
|
|
344
|
+
syncRestoredHudDebtParentSync(path);
|
|
345
|
+
}
|
|
346
|
+
/**
|
|
347
|
+
* Replays a prior restored-HUD obligation. A live pane is killable only when
|
|
348
|
+
* its recorded PID, HUD command identity, and tagged leader identity all
|
|
349
|
+
* still match; PID-less and ambiguous records remain durable debt.
|
|
350
|
+
*/
|
|
351
|
+
export function reconcileRestoredHudCleanupDebtSync(cwd, stateRoot) {
|
|
352
|
+
const record = parseRestoredHudCleanupDebtSync(cwd, stateRoot);
|
|
353
|
+
if (!record)
|
|
354
|
+
return;
|
|
355
|
+
const { path, debt } = record;
|
|
356
|
+
const proof = readExactPaneProofSync(debt.pane_id);
|
|
357
|
+
if (proof.status === 'gone') {
|
|
358
|
+
removeRestoredHudCleanupDebtSync(path);
|
|
359
|
+
return;
|
|
360
|
+
}
|
|
361
|
+
if (proof.status !== 'live' || debt.pane_pid === null || proof.pid !== debt.pane_pid || !debt.leader_pane_owner_id) {
|
|
362
|
+
throw new Error(`restored_hud_cleanup_debt_unresolved:${debt.pane_id}`);
|
|
363
|
+
}
|
|
364
|
+
const leader = requireLiveTeamOwnedPaneSync(debt.leader_pane_id, debt.leader_pane_pid, debt.leader_pane_owner_id);
|
|
365
|
+
const topology = listPanesResult(leader);
|
|
366
|
+
const hudMatches = !topology.error && topology.panes.filter((pane) => pane.paneId === proof.paneId
|
|
367
|
+
&& hudPaneMatchesOwner(pane, { leaderPaneId: debt.hud_owner_leader_pane_id })).length === 1;
|
|
368
|
+
if (!hudMatches)
|
|
369
|
+
throw new Error(`restored_hud_cleanup_debt_unresolved:${debt.pane_id}`);
|
|
370
|
+
killExactPaneSync(proof.paneId, debt.pane_pid, () => {
|
|
371
|
+
requireLiveTeamOwnedPaneSync(debt.leader_pane_id, debt.leader_pane_pid, debt.leader_pane_owner_id);
|
|
372
|
+
});
|
|
373
|
+
removeRestoredHudCleanupDebtSync(path);
|
|
374
|
+
}
|
|
375
|
+
/** Remove restored-HUD debt only after the canonical config transaction commits the same frozen identity. */
|
|
376
|
+
export function finalizeRestoredHudCleanupDebtSync(cwd, paneId, panePid, stateRoot, _requireFreshHudIdentity) {
|
|
377
|
+
const record = parseRestoredHudCleanupDebtSync(cwd, stateRoot);
|
|
378
|
+
if (!record)
|
|
379
|
+
return;
|
|
380
|
+
const { debt } = record;
|
|
381
|
+
if (debt.pane_id !== paneId || debt.pane_pid !== panePid || !debt.leader_pane_owner_id) {
|
|
382
|
+
throw new Error(`restored_hud_cleanup_debt_unresolved:${debt.pane_id}`);
|
|
383
|
+
}
|
|
384
|
+
const leader = requireLiveTeamOwnedPaneSync(debt.leader_pane_id, debt.leader_pane_pid, debt.leader_pane_owner_id);
|
|
385
|
+
const topology = listPanesResult(leader);
|
|
386
|
+
const hudMatches = !topology.error && topology.panes.filter((pane) => pane.paneId === debt.pane_id
|
|
387
|
+
&& hudPaneMatchesOwner(pane, { leaderPaneId: debt.hud_owner_leader_pane_id })).length === 1;
|
|
388
|
+
if (!hudMatches)
|
|
389
|
+
throw new Error(`restored_hud_cleanup_debt_unresolved:${debt.pane_id}`);
|
|
390
|
+
const finalProof = readExactPaneProofSync(debt.pane_id);
|
|
391
|
+
if (finalProof.status !== 'live' || finalProof.pid !== debt.pane_pid) {
|
|
392
|
+
throw new Error(`restored_hud_cleanup_debt_unresolved:${debt.pane_id}`);
|
|
393
|
+
}
|
|
394
|
+
removeRestoredHudCleanupDebtSync(record.path);
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Kill only a pane that a fresh global snapshot proves live. Callers treat
|
|
398
|
+
* gone/dead rows as already cleaned and unavailable snapshots as fail-closed.
|
|
399
|
+
*/
|
|
400
|
+
function requireLiveExactPaneSync(paneId, expectedPid) {
|
|
401
|
+
const proof = readExactPaneProofSync(paneId);
|
|
402
|
+
if (proof.status === 'unavailable')
|
|
403
|
+
throw new ExactPaneProofUnavailableError(proof);
|
|
404
|
+
if (proof.status === 'gone')
|
|
405
|
+
throw new Error(`tmux pane is not proven live: ${paneId}`);
|
|
406
|
+
if (expectedPid !== undefined && proof.pid !== expectedPid) {
|
|
407
|
+
throw new Error(`tmux pane identity changed: ${paneId}`);
|
|
408
|
+
}
|
|
409
|
+
return proof.paneId;
|
|
410
|
+
}
|
|
411
|
+
function killExactPaneSync(paneId, expectedPid, assertAuthorization) {
|
|
412
|
+
const proof = readExactPaneProofSync(paneId);
|
|
413
|
+
if (proof.status === 'unavailable')
|
|
414
|
+
throw new ExactPaneProofUnavailableError(proof);
|
|
415
|
+
if (proof.status === 'gone')
|
|
416
|
+
return;
|
|
417
|
+
if (expectedPid !== undefined && proof.pid !== expectedPid) {
|
|
418
|
+
throw new Error(`tmux pane identity changed: ${paneId}`);
|
|
419
|
+
}
|
|
420
|
+
assertAuthorization?.();
|
|
421
|
+
// Authorization can read tmux. Re-prove immediately after it so a recycled
|
|
422
|
+
// pane ID cannot be targeted by the subsequent kill.
|
|
423
|
+
const finalProof = readExactPaneProofSync(proof.paneId);
|
|
424
|
+
if (finalProof.status === 'unavailable')
|
|
425
|
+
throw new ExactPaneProofUnavailableError(finalProof);
|
|
426
|
+
if (finalProof.status === 'gone')
|
|
427
|
+
return;
|
|
428
|
+
if (finalProof.pid !== proof.pid || (expectedPid !== undefined && finalProof.pid !== expectedPid)) {
|
|
429
|
+
throw new Error(`tmux pane identity changed: ${paneId}`);
|
|
430
|
+
}
|
|
431
|
+
const result = runTmux(['kill-pane', '-t', finalProof.paneId]);
|
|
432
|
+
if (!result.ok)
|
|
433
|
+
throw new Error(`failed to kill tmux pane ${finalProof.paneId}: ${result.stderr}`);
|
|
434
|
+
const afterKill = readExactPaneProofSync(finalProof.paneId);
|
|
435
|
+
if (afterKill.status === 'unavailable')
|
|
436
|
+
throw new ExactPaneProofUnavailableError(afterKill);
|
|
437
|
+
if (afterKill.status !== 'gone') {
|
|
438
|
+
if (afterKill.pid !== finalProof.pid)
|
|
439
|
+
throw new Error(`tmux pane identity changed: ${paneId}`);
|
|
440
|
+
throw new Error(`tmux pane remains live after kill: ${paneId}`);
|
|
441
|
+
}
|
|
76
442
|
}
|
|
77
443
|
function appendNoUnderlineStyleFlags(style) {
|
|
78
444
|
const normalized = style
|
|
@@ -86,7 +452,8 @@ function appendNoUnderlineStyleFlags(style) {
|
|
|
86
452
|
}
|
|
87
453
|
return combined.join(',');
|
|
88
454
|
}
|
|
89
|
-
function sanitizeTmuxStyleOption(sessionTarget, optionName) {
|
|
455
|
+
function sanitizeTmuxStyleOption(sessionTarget, optionName, beforeEffect) {
|
|
456
|
+
beforeEffect?.();
|
|
90
457
|
const shown = runTmux(['show-options', '-gv', '-t', sessionTarget, optionName]);
|
|
91
458
|
if (!shown.ok)
|
|
92
459
|
return false;
|
|
@@ -96,36 +463,28 @@ function sanitizeTmuxStyleOption(sessionTarget, optionName) {
|
|
|
96
463
|
const sanitized = appendNoUnderlineStyleFlags(current);
|
|
97
464
|
if (sanitized === current)
|
|
98
465
|
return true;
|
|
466
|
+
beforeEffect?.();
|
|
99
467
|
const result = runTmux(['set-option', '-t', sessionTarget, optionName, sanitized]);
|
|
100
468
|
return result.ok;
|
|
101
469
|
}
|
|
102
|
-
function
|
|
103
|
-
const target = paneTarget.trim();
|
|
104
|
-
const sanitized = instanceId.trim();
|
|
105
|
-
if (!target || !sanitized)
|
|
106
|
-
return;
|
|
107
|
-
const result = runTmux(['set-option', '-p', '-t', target, OMX_PANE_INSTANCE_OPTION, sanitized]);
|
|
108
|
-
if (!result.ok) {
|
|
109
|
-
throw new Error(`failed to tag tmux pane ${target}: ${result.stderr}`);
|
|
110
|
-
}
|
|
111
|
-
}
|
|
112
|
-
export function tagPaneTeamOwner(paneTarget, teamOwnerId) {
|
|
470
|
+
export function tagPaneTeamOwner(paneTarget, teamOwnerId, expectedPanePid) {
|
|
113
471
|
const target = paneTarget.trim();
|
|
114
472
|
const sanitized = teamOwnerId.trim();
|
|
115
473
|
if (!target || !sanitized)
|
|
116
474
|
return;
|
|
117
|
-
const
|
|
475
|
+
const provenTarget = requireLiveExactPaneSync(target, expectedPanePid);
|
|
476
|
+
const result = runTmux(['set-option', '-p', '-t', provenTarget, OMX_TEAM_PANE_OWNER_OPTION, sanitized]);
|
|
118
477
|
if (!result.ok) {
|
|
119
|
-
throw new Error(`failed to tag tmux pane ${
|
|
478
|
+
throw new Error(`failed to tag tmux pane ${provenTarget}: ${result.stderr}`);
|
|
120
479
|
}
|
|
121
480
|
}
|
|
122
|
-
export function mitigateCopyModeUnderlineArtifacts(sessionTarget) {
|
|
481
|
+
export function mitigateCopyModeUnderlineArtifacts(sessionTarget, beforeEffect) {
|
|
123
482
|
const normalizedTarget = sessionTarget.trim();
|
|
124
483
|
if (normalizedTarget === '')
|
|
125
484
|
return false;
|
|
126
485
|
let applied = false;
|
|
127
486
|
for (const optionName of TMUX_COPY_MODE_STYLE_OPTIONS) {
|
|
128
|
-
if (sanitizeTmuxStyleOption(normalizedTarget, optionName)) {
|
|
487
|
+
if (sanitizeTmuxStyleOption(normalizedTarget, optionName, beforeEffect)) {
|
|
129
488
|
applied = true;
|
|
130
489
|
}
|
|
131
490
|
}
|
|
@@ -181,18 +540,104 @@ function baseSessionName(target) {
|
|
|
181
540
|
return target.split(':')[0] || target;
|
|
182
541
|
}
|
|
183
542
|
function listPanes(target) {
|
|
184
|
-
|
|
543
|
+
return listPanesResult(target).panes;
|
|
544
|
+
}
|
|
545
|
+
function listPanesResult(target) {
|
|
546
|
+
const result = runTmuxStructured(['list-panes', '-t', target, '-F', '#{pane_id}\t#{pane_current_command}\t#{pane_start_command}']);
|
|
185
547
|
if (!result.ok)
|
|
186
|
-
return [];
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
.
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
const
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
548
|
+
return { panes: [], error: result.stderr };
|
|
549
|
+
const panes = [];
|
|
550
|
+
for (const rawLine of result.stdout.split('\n')) {
|
|
551
|
+
const line = rawLine.endsWith('\r') ? rawLine.slice(0, -1) : rawLine;
|
|
552
|
+
if (line.length === 0)
|
|
553
|
+
continue;
|
|
554
|
+
const firstSeparator = line.indexOf('\t');
|
|
555
|
+
const secondSeparator = firstSeparator >= 0 ? line.indexOf('\t', firstSeparator + 1) : -1;
|
|
556
|
+
const paneId = firstSeparator >= 0 ? line.slice(0, firstSeparator) : line;
|
|
557
|
+
if (firstSeparator < 0 || secondSeparator < 0 || !/^%[0-9]+$/.test(paneId)) {
|
|
558
|
+
return { panes: [], error: 'malformed pane topology' };
|
|
559
|
+
}
|
|
560
|
+
if (panes.some((pane) => pane.paneId === paneId)) {
|
|
561
|
+
return { panes: [], error: 'malformed pane topology' };
|
|
562
|
+
}
|
|
563
|
+
panes.push({
|
|
564
|
+
paneId,
|
|
565
|
+
currentCommand: line.slice(firstSeparator + 1, secondSeparator),
|
|
566
|
+
startCommand: line.slice(secondSeparator + 1),
|
|
567
|
+
});
|
|
568
|
+
}
|
|
569
|
+
return { panes, error: null };
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* Window-wide layout effects may only touch the exact startup pane set. A
|
|
573
|
+
* target-scoped topology read rejects foreign/new panes, and targeted global
|
|
574
|
+
* snapshots pin every surviving pane identity. The final scoped read happens
|
|
575
|
+
* immediately before the mutation so panes added during owner reads cannot
|
|
576
|
+
* inherit that authority.
|
|
577
|
+
*/
|
|
578
|
+
/**
|
|
579
|
+
* Re-prove an explicitly Team-owned pane immediately before an effect. The
|
|
580
|
+
* double PID proof keeps ownership and process identity continuous across an
|
|
581
|
+
* untrusted tmux option read.
|
|
582
|
+
*/
|
|
583
|
+
function requireLiveTeamOwnedPaneSync(paneId, expectedPanePid, expectedTeamOwnerId) {
|
|
584
|
+
const target = requireLiveExactPaneSync(paneId, expectedPanePid);
|
|
585
|
+
const owner = readPaneTeamOwnerTagResult(target);
|
|
586
|
+
if (owner.status !== 'value' || owner.value !== expectedTeamOwnerId) {
|
|
587
|
+
const detail = owner.status === 'error' ? owner.error : 'missing';
|
|
588
|
+
throw new Error(`tmux pane team owner changed: ${target}: ${detail}`);
|
|
589
|
+
}
|
|
590
|
+
return requireLiveExactPaneSync(target, expectedPanePid);
|
|
591
|
+
}
|
|
592
|
+
function frozenWindowTopologyError(teamTarget, expectedPaneIds, actualPaneIds) {
|
|
593
|
+
const expected = [...expectedPaneIds].sort();
|
|
594
|
+
const actual = [...actualPaneIds].sort();
|
|
595
|
+
const unexpected = actual.filter((paneId) => !expectedPaneIds.has(paneId));
|
|
596
|
+
const missing = expected.filter((paneId) => !actualPaneIds.has(paneId));
|
|
597
|
+
return new Error(`tmux window topology changed before layout mutation: target=${teamTarget}; expected=[${expected.join(',')}]; actual=[${actual.join(',')}]; unexpected=[${unexpected.join(',')}]; missing=[${missing.join(',')}]. Team requires an isolated tmux window with no host-owned foreign panes`);
|
|
598
|
+
}
|
|
599
|
+
function requireFrozenWindowTopologySync(teamTarget, expectedPanePids, expectedPaneOwners) {
|
|
600
|
+
const topology = listPanesResult(teamTarget);
|
|
601
|
+
if (topology.error)
|
|
602
|
+
throw new Error(`failed to read tmux pane topology: ${topology.error}`);
|
|
603
|
+
const actualPaneIds = new Set(topology.panes.map((pane) => pane.paneId));
|
|
604
|
+
if (actualPaneIds.size !== expectedPanePids.size
|
|
605
|
+
|| [...expectedPanePids.keys()].some((paneId) => !actualPaneIds.has(paneId))) {
|
|
606
|
+
throw frozenWindowTopologyError(teamTarget, new Set(expectedPanePids.keys()), actualPaneIds);
|
|
607
|
+
}
|
|
608
|
+
for (const [paneId, expectedPanePid] of expectedPanePids) {
|
|
609
|
+
const expectedOwner = expectedPaneOwners?.get(paneId);
|
|
610
|
+
if (expectedOwner)
|
|
611
|
+
requireLiveTeamOwnedPaneSync(paneId, expectedPanePid, expectedOwner);
|
|
612
|
+
else {
|
|
613
|
+
const proof = readExactPaneProofSync(paneId);
|
|
614
|
+
if (proof.status === 'unavailable')
|
|
615
|
+
throw new ExactPaneProofUnavailableError(proof);
|
|
616
|
+
if (proof.status === 'gone')
|
|
617
|
+
throw new Error(`tmux pane is not proven live: ${proof.paneId}`);
|
|
618
|
+
if (proof.pid !== expectedPanePid)
|
|
619
|
+
throw new Error(`tmux pane identity changed: ${proof.paneId}`);
|
|
620
|
+
}
|
|
621
|
+
}
|
|
622
|
+
const finalProofs = readExactPaneProofsSync([...expectedPanePids.keys()]);
|
|
623
|
+
for (const [index, [paneId, expectedPanePid]] of [...expectedPanePids.entries()].entries()) {
|
|
624
|
+
const proof = finalProofs[index];
|
|
625
|
+
if (proof.status === 'unavailable')
|
|
626
|
+
throw new ExactPaneProofUnavailableError(proof);
|
|
627
|
+
if (proof.status === 'gone')
|
|
628
|
+
throw new Error(`tmux pane is not proven live: ${proof.paneId}`);
|
|
629
|
+
if (proof.paneId !== paneId || proof.pid !== expectedPanePid) {
|
|
630
|
+
throw new Error(`tmux pane identity changed: ${paneId}`);
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
const finalTopology = listPanesResult(teamTarget);
|
|
634
|
+
if (finalTopology.error)
|
|
635
|
+
throw new Error(`failed to read tmux pane topology: ${finalTopology.error}`);
|
|
636
|
+
const finalPaneIds = new Set(finalTopology.panes.map((pane) => pane.paneId));
|
|
637
|
+
if (finalPaneIds.size !== expectedPanePids.size
|
|
638
|
+
|| [...expectedPanePids.keys()].some((paneId) => !finalPaneIds.has(paneId))) {
|
|
639
|
+
throw frozenWindowTopologyError(teamTarget, new Set(expectedPanePids.keys()), finalPaneIds);
|
|
640
|
+
}
|
|
196
641
|
}
|
|
197
642
|
export function listPaneIds(target) {
|
|
198
643
|
return listPanes(target).map((pane) => pane.paneId);
|
|
@@ -237,12 +682,6 @@ export function chooseTeamLeaderPaneId(panes, preferredPaneId) {
|
|
|
237
682
|
return nonHud.paneId;
|
|
238
683
|
return preferredPaneId;
|
|
239
684
|
}
|
|
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
685
|
function readPaneCurrentPath(paneId) {
|
|
247
686
|
if (!paneId.startsWith('%'))
|
|
248
687
|
return null;
|
|
@@ -281,7 +720,8 @@ function shouldAttemptRestoreCwdCandidate(candidate) {
|
|
|
281
720
|
}
|
|
282
721
|
return pathIsUsableDirectory(candidate.rawPath);
|
|
283
722
|
}
|
|
284
|
-
function resolveStandaloneHudRestoreCwdCandidates(leaderPaneId, fallbackCwd, explicitCwd) {
|
|
723
|
+
function resolveStandaloneHudRestoreCwdCandidates(leaderPaneId, fallbackCwd, explicitCwd, beforeReadLiveLeaderCwd) {
|
|
724
|
+
beforeReadLiveLeaderCwd?.();
|
|
285
725
|
const liveLeaderCwd = readPaneCurrentPath(leaderPaneId);
|
|
286
726
|
return uniqueRestoreCwdCandidates([
|
|
287
727
|
{ source: 'explicit', rawPath: explicitCwd },
|
|
@@ -318,105 +758,214 @@ async function runTmuxAsync(args) {
|
|
|
318
758
|
return { ok: false, stderr: (err.stderr || err.message || '').trim() || 'tmux command failed' };
|
|
319
759
|
}
|
|
320
760
|
}
|
|
321
|
-
|
|
761
|
+
function hasExplicitWorkerPaneId(workerPaneId) {
|
|
762
|
+
return typeof workerPaneId === 'string' && workerPaneId.trim().length > 0;
|
|
763
|
+
}
|
|
764
|
+
function readExactWorkerPaneLivenessProofSync(paneId) {
|
|
765
|
+
if (!/^%[0-9]+$/.test(paneId))
|
|
766
|
+
return { status: 'unavailable', paneId, reason: 'invalid_pane_id' };
|
|
767
|
+
const result = runTmuxStructured([
|
|
768
|
+
'display-message', '-p', '-t', paneId,
|
|
769
|
+
`#{pane_id}\t#{pane_dead}\t#{pane_pid}\t#{${OMX_TEAM_PANE_OWNER_OPTION}}`,
|
|
770
|
+
]);
|
|
771
|
+
if (!result.ok) {
|
|
772
|
+
if (/can't find pane|no such pane|unknown pane/i.test(result.stderr)) {
|
|
773
|
+
return { status: 'gone', paneId, reason: 'absent' };
|
|
774
|
+
}
|
|
775
|
+
return { status: 'unavailable', paneId, reason: `query_failed:${result.stderr || 'unknown'}` };
|
|
776
|
+
}
|
|
777
|
+
const lines = result.stdout.replace(/\r\n/g, '\n').split('\n').filter((line) => line.length > 0);
|
|
778
|
+
if (lines.length !== 1)
|
|
779
|
+
return { status: 'unavailable', paneId, reason: 'malformed_exact_pane_result' };
|
|
780
|
+
const fields = lines[0].split('\t');
|
|
781
|
+
if (fields.length !== 4)
|
|
782
|
+
return { status: 'unavailable', paneId, reason: 'malformed_exact_pane_result' };
|
|
783
|
+
const [actualPaneId, paneDead, rawPid, ownerId] = fields;
|
|
784
|
+
if (actualPaneId !== paneId) {
|
|
785
|
+
return { status: 'unavailable', paneId, reason: `pane_id_changed:expected=${paneId}:actual=${actualPaneId}` };
|
|
786
|
+
}
|
|
787
|
+
if (paneDead !== '0' && paneDead !== '1')
|
|
788
|
+
return { status: 'unavailable', paneId, reason: 'malformed_exact_pane_result' };
|
|
789
|
+
if (!/^[0-9]+$/.test(rawPid))
|
|
790
|
+
return { status: 'unavailable', paneId, reason: 'malformed_exact_pane_result' };
|
|
791
|
+
const pid = Number(rawPid);
|
|
792
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
793
|
+
return { status: 'unavailable', paneId, reason: 'malformed_exact_pane_result' };
|
|
794
|
+
if (paneDead === '1')
|
|
795
|
+
return { status: 'gone', paneId, reason: 'dead' };
|
|
796
|
+
return { status: 'live', paneId, pid, ownerId };
|
|
797
|
+
}
|
|
798
|
+
function requireExactWorkerPaneLivenessIdentity(paneId, expectedPanePid, expectedTeamOwnerId, hudPaneId) {
|
|
799
|
+
if (hudPaneId?.trim() && paneId === hudPaneId.trim()) {
|
|
800
|
+
throw new Error(`tmux worker pane is HUD target: ${paneId}`);
|
|
801
|
+
}
|
|
802
|
+
const proof = readExactWorkerPaneLivenessProofSync(paneId);
|
|
803
|
+
if (proof.status === 'unavailable' && proof.reason.startsWith('pane_id_changed:')) {
|
|
804
|
+
throw new Error(`tmux worker pane identity changed: ${paneId}: ${proof.reason}`);
|
|
805
|
+
}
|
|
806
|
+
if (proof.status !== 'live')
|
|
807
|
+
return proof;
|
|
808
|
+
if (typeof expectedPanePid === 'number' && proof.pid !== expectedPanePid) {
|
|
809
|
+
throw new Error(`tmux worker pane PID changed: ${paneId}: expected ${expectedPanePid}, got ${proof.pid}`);
|
|
810
|
+
}
|
|
811
|
+
const expectedOwner = typeof expectedTeamOwnerId === 'string' ? expectedTeamOwnerId.trim() : '';
|
|
812
|
+
if (expectedOwner && proof.ownerId !== expectedOwner) {
|
|
813
|
+
throw new Error(`tmux worker pane incarnation changed: ${paneId}: expected ${expectedOwner}, got ${proof.ownerId || 'missing'}`);
|
|
814
|
+
}
|
|
815
|
+
return proof;
|
|
816
|
+
}
|
|
817
|
+
function resolveWorkerPaneTargetSync(sessionName, workerIndex, workerPaneId) {
|
|
818
|
+
if (!hasExplicitWorkerPaneId(workerPaneId))
|
|
819
|
+
return paneTarget(sessionName, workerIndex);
|
|
820
|
+
const proof = readExactPaneProofSync(workerPaneId);
|
|
821
|
+
return proof.status === 'live' ? proof.paneId : null;
|
|
822
|
+
}
|
|
823
|
+
async function resolveWorkerPaneTargetAsync(sessionName, workerIndex, workerPaneId) {
|
|
824
|
+
if (!hasExplicitWorkerPaneId(workerPaneId))
|
|
825
|
+
return paneTarget(sessionName, workerIndex);
|
|
826
|
+
const proof = await readExactPaneProof(workerPaneId);
|
|
827
|
+
return proof.status === 'live' ? proof.paneId : null;
|
|
828
|
+
}
|
|
829
|
+
function createPinnedWorkerPaneTargetResolverSync(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId) {
|
|
830
|
+
if (!hasExplicitWorkerPaneId(workerPaneId)) {
|
|
831
|
+
return () => resolveWorkerPaneTargetSync(sessionName, workerIndex, workerPaneId);
|
|
832
|
+
}
|
|
833
|
+
const pinnedPid = expectedPanePid;
|
|
834
|
+
const expectedOwner = typeof expectedTeamOwnerId === 'string' ? expectedTeamOwnerId.trim() : '';
|
|
835
|
+
const canonicalHudPaneId = typeof hudPaneId === 'string' ? hudPaneId.trim() : '';
|
|
836
|
+
return () => {
|
|
837
|
+
if (typeof pinnedPid !== 'number' || !Number.isSafeInteger(pinnedPid) || pinnedPid <= 0)
|
|
838
|
+
return null;
|
|
839
|
+
if (!expectedOwner)
|
|
840
|
+
return null;
|
|
841
|
+
if (canonicalHudPaneId && workerPaneId === canonicalHudPaneId) {
|
|
842
|
+
throw new Error(`tmux worker pane is HUD target: ${workerPaneId}`);
|
|
843
|
+
}
|
|
844
|
+
const proof = readExactPaneProofSync(workerPaneId);
|
|
845
|
+
if (proof.status !== 'live')
|
|
846
|
+
return null;
|
|
847
|
+
if (proof.pid !== pinnedPid) {
|
|
848
|
+
throw new Error(`tmux pane identity changed: ${workerPaneId}`);
|
|
849
|
+
}
|
|
850
|
+
const owner = readPaneTeamOwnerTagResult(proof.paneId);
|
|
851
|
+
if (owner.status !== 'value' || owner.value !== expectedOwner)
|
|
852
|
+
return null;
|
|
853
|
+
const finalProof = readExactPaneProofSync(workerPaneId);
|
|
854
|
+
if (finalProof.status !== 'live')
|
|
855
|
+
return null;
|
|
856
|
+
if (finalProof.pid !== pinnedPid) {
|
|
857
|
+
throw new Error(`tmux pane identity changed: ${workerPaneId}`);
|
|
858
|
+
}
|
|
859
|
+
return finalProof.paneId;
|
|
860
|
+
};
|
|
861
|
+
}
|
|
862
|
+
function createPinnedWorkerPaneTargetResolver(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId) {
|
|
863
|
+
if (!hasExplicitWorkerPaneId(workerPaneId)) {
|
|
864
|
+
return () => resolveWorkerPaneTargetAsync(sessionName, workerIndex, workerPaneId);
|
|
865
|
+
}
|
|
866
|
+
const pinnedPid = expectedPanePid;
|
|
867
|
+
const expectedOwner = typeof expectedTeamOwnerId === 'string' ? expectedTeamOwnerId.trim() : '';
|
|
868
|
+
const canonicalHudPaneId = typeof hudPaneId === 'string' ? hudPaneId.trim() : '';
|
|
869
|
+
let lastResolvedPid;
|
|
870
|
+
const resolveTarget = async () => {
|
|
871
|
+
if (typeof pinnedPid !== 'number' || !Number.isSafeInteger(pinnedPid) || pinnedPid <= 0)
|
|
872
|
+
return null;
|
|
873
|
+
if (!expectedOwner)
|
|
874
|
+
return null;
|
|
875
|
+
if (canonicalHudPaneId && workerPaneId === canonicalHudPaneId) {
|
|
876
|
+
throw new Error(`tmux worker pane is HUD target: ${workerPaneId}`);
|
|
877
|
+
}
|
|
878
|
+
const proof = await readExactPaneProof(workerPaneId);
|
|
879
|
+
if (proof.status !== 'live')
|
|
880
|
+
return null;
|
|
881
|
+
if (proof.pid !== pinnedPid) {
|
|
882
|
+
throw new Error(`tmux pane identity changed: ${workerPaneId}`);
|
|
883
|
+
}
|
|
884
|
+
const owner = readPaneTeamOwnerTagResult(proof.paneId);
|
|
885
|
+
if (owner.status !== 'value' || owner.value !== expectedOwner) {
|
|
886
|
+
const detail = owner.status === 'error' ? owner.error : 'missing';
|
|
887
|
+
throw new Error(`tmux pane team owner changed: ${workerPaneId}: ${detail}`);
|
|
888
|
+
}
|
|
889
|
+
// The option read is untrusted and can race pane ID reuse. Its immediately
|
|
890
|
+
// adjacent PID proof authorizes the following capture/control/input effect.
|
|
891
|
+
const finalProof = await readExactPaneProof(workerPaneId);
|
|
892
|
+
if (finalProof.status !== 'live')
|
|
893
|
+
return null;
|
|
894
|
+
if (finalProof.pid !== pinnedPid) {
|
|
895
|
+
throw new Error(`tmux pane identity changed: ${workerPaneId}`);
|
|
896
|
+
}
|
|
897
|
+
lastResolvedPid = finalProof.pid;
|
|
898
|
+
return finalProof.paneId;
|
|
899
|
+
};
|
|
900
|
+
resolveTarget.lastResolvedPid = () => lastResolvedPid;
|
|
901
|
+
return resolveTarget;
|
|
902
|
+
}
|
|
903
|
+
async function requireAsyncPaneTarget(resolveTarget) {
|
|
904
|
+
const target = await resolveTarget();
|
|
905
|
+
if (!target)
|
|
906
|
+
throw new Error('tmux pane is not proven live');
|
|
907
|
+
return target;
|
|
908
|
+
}
|
|
909
|
+
async function sendKeyAsync(resolveTarget, key) {
|
|
910
|
+
const target = await requireAsyncPaneTarget(resolveTarget);
|
|
322
911
|
const result = await runTmuxAsync(['send-keys', '-t', target, key]);
|
|
323
912
|
if (!result.ok) {
|
|
324
913
|
throw new Error(`sendKeyAsync: failed to send ${key}: ${result.stderr}`);
|
|
325
914
|
}
|
|
326
915
|
}
|
|
327
|
-
async function capturePaneAsync(
|
|
916
|
+
async function capturePaneAsync(resolveTarget) {
|
|
917
|
+
const target = await resolveTarget();
|
|
918
|
+
if (!target)
|
|
919
|
+
return '';
|
|
328
920
|
const result = await runTmuxAsync(sharedBuildCapturePaneArgv(target, 80));
|
|
329
921
|
if (!result.ok)
|
|
330
922
|
return '';
|
|
331
923
|
return result.stdout;
|
|
332
924
|
}
|
|
333
|
-
|
|
925
|
+
/** Capture an explicit Team worker pane only after PID-owner-PID authorization. */
|
|
926
|
+
export async function captureWorkerPane(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId) {
|
|
927
|
+
const resolveTarget = createPinnedWorkerPaneTargetResolver(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId);
|
|
928
|
+
return capturePaneAsync(resolveTarget);
|
|
929
|
+
}
|
|
930
|
+
async function captureVisiblePaneAsync(resolveTarget) {
|
|
931
|
+
const target = await resolveTarget();
|
|
932
|
+
if (!target)
|
|
933
|
+
return '';
|
|
334
934
|
const result = await runTmuxAsync(sharedBuildVisibleCapturePaneArgv(target));
|
|
335
935
|
if (!result.ok)
|
|
336
936
|
return '';
|
|
337
937
|
return result.stdout;
|
|
338
938
|
}
|
|
339
|
-
async function isWorkerAliveAsync(sessionName, workerIndex, workerPaneId) {
|
|
340
|
-
if (workerPaneId
|
|
341
|
-
const
|
|
342
|
-
|
|
343
|
-
|
|
939
|
+
async function isWorkerAliveAsync(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId, resolveTarget) {
|
|
940
|
+
if (hasExplicitWorkerPaneId(workerPaneId)) {
|
|
941
|
+
const resolver = resolveTarget ?? createPinnedWorkerPaneTargetResolver(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId);
|
|
942
|
+
const target = await resolver();
|
|
943
|
+
const pid = resolver.lastResolvedPid?.() ?? expectedPanePid;
|
|
944
|
+
if (!target || typeof pid !== 'number' || !Number.isSafeInteger(pid) || pid <= 0)
|
|
945
|
+
return 'unknown';
|
|
946
|
+
return probeProcessLiveness(pid);
|
|
344
947
|
}
|
|
345
948
|
const result = await runTmuxAsync([
|
|
346
949
|
'list-panes',
|
|
347
|
-
'-t', paneTarget(sessionName, workerIndex
|
|
950
|
+
'-t', paneTarget(sessionName, workerIndex),
|
|
348
951
|
'-F',
|
|
349
952
|
'#{pane_dead} #{pane_pid}',
|
|
350
953
|
]);
|
|
351
954
|
if (!result.ok)
|
|
352
|
-
return
|
|
955
|
+
return 'unknown';
|
|
353
956
|
const line = result.stdout.split('\n')[0]?.trim();
|
|
354
957
|
if (!line)
|
|
355
|
-
return
|
|
958
|
+
return 'unknown';
|
|
356
959
|
const parts = line.split(/\s+/);
|
|
357
960
|
if (parts.length < 2)
|
|
358
|
-
return
|
|
961
|
+
return 'unknown';
|
|
359
962
|
const paneDead = parts[0];
|
|
360
963
|
const pid = Number.parseInt(parts[1], 10);
|
|
361
964
|
if (paneDead === '1')
|
|
362
|
-
return
|
|
965
|
+
return 'gone';
|
|
363
966
|
if (!Number.isFinite(pid))
|
|
364
|
-
return
|
|
365
|
-
|
|
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;
|
|
381
|
-
if (!Number.isFinite(pid))
|
|
382
|
-
return allowMissingPid ? true : false;
|
|
383
|
-
try {
|
|
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;
|
|
967
|
+
return 'unknown';
|
|
968
|
+
return probeProcessLiveness(pid);
|
|
420
969
|
}
|
|
421
970
|
function shellQuoteSingle(value) {
|
|
422
971
|
return `'${value.replace(/'/g, `'\\''`)}'`;
|
|
@@ -510,6 +1059,20 @@ function buildNestedTmuxShellCommand(command) {
|
|
|
510
1059
|
function buildBestEffortShellCommand(command) {
|
|
511
1060
|
return `${command} >/dev/null 2>&1 || true`;
|
|
512
1061
|
}
|
|
1062
|
+
function buildAuthoritativeHudResizeShellCommand(hudPaneId, heightLines = HUD_TMUX_TEAM_HEIGHT_LINES, expectedPanePid, expectedPaneOwnerId, effectCommand = buildNestedTmuxShellCommand(buildHudResizeCommand(buildHudPaneTarget(hudPaneId), heightLines))) {
|
|
1063
|
+
const target = buildHudPaneTarget(hudPaneId);
|
|
1064
|
+
const snapshot = buildNestedTmuxShellCommand("list-panes -a -F '#{pane_id}\t#{pane_dead}\t#{pane_pid}'");
|
|
1065
|
+
const expectedPid = typeof expectedPanePid === 'number' && Number.isSafeInteger(expectedPanePid) && expectedPanePid > 0
|
|
1066
|
+
? ` && $3 == "${expectedPanePid}"`
|
|
1067
|
+
: '';
|
|
1068
|
+
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 }'`;
|
|
1069
|
+
const expectedOwner = expectedPaneOwnerId?.trim();
|
|
1070
|
+
if (!expectedOwner) {
|
|
1071
|
+
return `if snapshot=$(${snapshot}); then printf '%s\\n' "$snapshot" | ${proof} && ${effectCommand}; fi`;
|
|
1072
|
+
}
|
|
1073
|
+
const owner = buildNestedTmuxShellCommand(`show-option -qv -p -t ${target} ${OMX_TEAM_PANE_OWNER_OPTION}`);
|
|
1074
|
+
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`;
|
|
1075
|
+
}
|
|
513
1076
|
/** Upper bound for tmux hook indices (signed 32-bit max). */
|
|
514
1077
|
const TMUX_HOOK_INDEX_MAX = 2147483647;
|
|
515
1078
|
function buildResizeHookSlot(hookName) {
|
|
@@ -526,8 +1089,8 @@ function buildClientAttachedHookSlot(hookName) {
|
|
|
526
1089
|
}
|
|
527
1090
|
return `client-attached[${Math.abs(hash) % TMUX_HOOK_INDEX_MAX}]`;
|
|
528
1091
|
}
|
|
529
|
-
export function buildRegisterResizeHookArgs(hookTarget, hookName, hudPaneId, heightLines = HUD_TMUX_TEAM_HEIGHT_LINES) {
|
|
530
|
-
const resizeCommand = buildBestEffortShellCommand(
|
|
1092
|
+
export function buildRegisterResizeHookArgs(hookTarget, hookName, hudPaneId, heightLines = HUD_TMUX_TEAM_HEIGHT_LINES, expectedPanePid, expectedPaneOwnerId) {
|
|
1093
|
+
const resizeCommand = buildBestEffortShellCommand(buildAuthoritativeHudResizeShellCommand(hudPaneId, heightLines, expectedPanePid, expectedPaneOwnerId));
|
|
531
1094
|
const hookCommand = shellQuoteSingle(`${resizeCommand}; sleep ${HUD_RESIZE_RECONCILE_DELAY_SECONDS}; ${resizeCommand}`);
|
|
532
1095
|
return ['set-hook', '-t', hookTarget, buildResizeHookSlot(hookName), `run-shell -b ${hookCommand}`];
|
|
533
1096
|
}
|
|
@@ -543,9 +1106,11 @@ export function buildClientAttachedReconcileHookName(teamName, sessionName, wind
|
|
|
543
1106
|
normalizeHudPaneToken(hudPaneId),
|
|
544
1107
|
].join('_');
|
|
545
1108
|
}
|
|
546
|
-
export function buildRegisterClientAttachedReconcileArgs(hookTarget, hookName, hudPaneId, heightLines = HUD_TMUX_TEAM_HEIGHT_LINES) {
|
|
1109
|
+
export function buildRegisterClientAttachedReconcileArgs(hookTarget, hookName, hudPaneId, heightLines = HUD_TMUX_TEAM_HEIGHT_LINES, expectedPanePid, expectedPaneOwnerId) {
|
|
547
1110
|
const hookSlot = buildClientAttachedHookSlot(hookName);
|
|
548
|
-
const
|
|
1111
|
+
const resizeCommand = buildBestEffortShellCommand(buildAuthoritativeHudResizeShellCommand(hudPaneId, heightLines, expectedPanePid, expectedPaneOwnerId));
|
|
1112
|
+
const unregisterCommand = buildBestEffortShellCommand(buildAuthoritativeHudResizeShellCommand(hudPaneId, heightLines, expectedPanePid, expectedPaneOwnerId, buildNestedTmuxShellCommand(`set-hook -u -t ${hookTarget} ${hookSlot}`)));
|
|
1113
|
+
const oneShotCommand = shellQuoteSingle(`${resizeCommand}; ${unregisterCommand}`);
|
|
549
1114
|
return ['set-hook', '-t', hookTarget, hookSlot, `run-shell -b ${oneShotCommand}`];
|
|
550
1115
|
}
|
|
551
1116
|
export function buildUnregisterClientAttachedReconcileArgs(hookTarget, hookName) {
|
|
@@ -555,18 +1120,18 @@ export function unregisterResizeHook(hookTarget, hookName) {
|
|
|
555
1120
|
const result = runTmux(buildUnregisterResizeHookArgs(hookTarget, hookName));
|
|
556
1121
|
return result.ok;
|
|
557
1122
|
}
|
|
558
|
-
export function buildScheduleDelayedHudResizeArgs(hudPaneId, delaySeconds = HUD_RESIZE_RECONCILE_DELAY_SECONDS, heightLines = HUD_TMUX_TEAM_HEIGHT_LINES) {
|
|
1123
|
+
export function buildScheduleDelayedHudResizeArgs(hudPaneId, delaySeconds = HUD_RESIZE_RECONCILE_DELAY_SECONDS, heightLines = HUD_TMUX_TEAM_HEIGHT_LINES, expectedPanePid, expectedPaneOwnerId) {
|
|
559
1124
|
const delay = Number.isFinite(delaySeconds) && delaySeconds > 0 ? delaySeconds : HUD_RESIZE_RECONCILE_DELAY_SECONDS;
|
|
560
|
-
return ['run-shell', '-b', `sleep ${delay}; ${buildBestEffortShellCommand(
|
|
1125
|
+
return ['run-shell', '-b', `sleep ${delay}; ${buildBestEffortShellCommand(buildAuthoritativeHudResizeShellCommand(hudPaneId, heightLines, expectedPanePid, expectedPaneOwnerId))}`];
|
|
561
1126
|
}
|
|
562
|
-
export function buildReconcileHudResizeArgs(hudPaneId, heightLines = HUD_TMUX_TEAM_HEIGHT_LINES) {
|
|
563
|
-
return ['run-shell', buildBestEffortShellCommand(
|
|
1127
|
+
export function buildReconcileHudResizeArgs(hudPaneId, heightLines = HUD_TMUX_TEAM_HEIGHT_LINES, expectedPanePid, expectedPaneOwnerId) {
|
|
1128
|
+
return ['run-shell', buildBestEffortShellCommand(buildAuthoritativeHudResizeShellCommand(hudPaneId, heightLines, expectedPanePid, expectedPaneOwnerId))];
|
|
564
1129
|
}
|
|
565
|
-
function redrawLeaderPaneAfterTeamLayout(
|
|
566
|
-
const
|
|
567
|
-
if (
|
|
568
|
-
|
|
569
|
-
|
|
1130
|
+
function redrawLeaderPaneAfterTeamLayout(source, expectedTeamOwnerId) {
|
|
1131
|
+
const provenTarget = requireLiveTeamOwnedPaneSync(source.paneId, source.panePid, expectedTeamOwnerId);
|
|
1132
|
+
if (provenTarget !== source.paneId)
|
|
1133
|
+
throw new Error(`tmux pane identity changed: ${source.paneId}`);
|
|
1134
|
+
runSourceAuthorizedTmux(source, `send-keys -t ${source.paneId} C-l`);
|
|
570
1135
|
}
|
|
571
1136
|
const ZSH_CANDIDATE_PATHS = ['/bin/zsh', '/usr/bin/zsh', '/usr/local/bin/zsh', '/opt/local/bin/zsh', '/opt/homebrew/bin/zsh'];
|
|
572
1137
|
const BASH_CANDIDATE_PATHS = ['/bin/bash', '/usr/bin/bash'];
|
|
@@ -923,6 +1488,23 @@ function appendTeamWorkerMcpDisableOverrides(args, env) {
|
|
|
923
1488
|
args.splice(endOfOptionsIndex < 0 ? args.length : endOfOptionsIndex, 0, CONFIG_FLAG, `${key}=false`);
|
|
924
1489
|
}
|
|
925
1490
|
}
|
|
1491
|
+
const CODEX_BYPASS_MDM_INCOMPATIBLE_REASON = 'codex_bypass_mdm_incompatible';
|
|
1492
|
+
const CODEX_BYPASS_MDM_REJECTED_MARKER = 'MDM_POLICY_REJECTED: approval_policy=never forbids --dangerously-bypass-approvals-and-sandbox';
|
|
1493
|
+
export function classifyCodexBypassMdmIncompatibility(captured, workerCli, finalArgs) {
|
|
1494
|
+
if (workerCli !== 'codex' || !finalArgs || !parseTeamWorkerLaunchArgs([...finalArgs], 'final tmux worker arguments', { directPolicyMode: 'ignore' }).wantsBypass)
|
|
1495
|
+
return null;
|
|
1496
|
+
const normalizedLines = captured.replace(/\r\n?/g, '\n').split('\n');
|
|
1497
|
+
return normalizedLines.includes(CODEX_BYPASS_MDM_REJECTED_MARKER)
|
|
1498
|
+
? CODEX_BYPASS_MDM_INCOMPATIBLE_REASON
|
|
1499
|
+
: null;
|
|
1500
|
+
}
|
|
1501
|
+
function buildAttachedTmuxWorkerStartupPlan(teamName, workerIndex, launchArgs, cwd, extraEnv, workerCliOverride, initialPrompt, workerRole) {
|
|
1502
|
+
const processSpec = buildWorkerStartupProcessLaunchSpec(teamName, workerIndex, launchArgs, cwd, extraEnv, workerCliOverride, initialPrompt, workerRole);
|
|
1503
|
+
const startupArgs = [...processSpec.args];
|
|
1504
|
+
if (processSpec.workerCli === 'codex')
|
|
1505
|
+
appendTeamWorkerMcpDisableOverrides(startupArgs, { ...process.env, ...extraEnv });
|
|
1506
|
+
return Object.freeze({ processSpec, startupArgs: Object.freeze(startupArgs) });
|
|
1507
|
+
}
|
|
926
1508
|
function insertArgsBeforeEndOfOptions(args, insertedArgs) {
|
|
927
1509
|
const endOfOptionsIndex = args.indexOf('--');
|
|
928
1510
|
if (endOfOptionsIndex < 0)
|
|
@@ -1046,24 +1628,17 @@ function buildWorkerStartupScriptContent(processSpec, startupEnv, startupArgs, c
|
|
|
1046
1628
|
'',
|
|
1047
1629
|
].filter((line) => line !== '').join('\n');
|
|
1048
1630
|
}
|
|
1049
|
-
export function writeWorkerStartupScriptCommand(teamName, workerIndex, launchArgs = [], cwd = process.cwd(), extraEnv = {}, workerCliOverride, initialPrompt, workerRole) {
|
|
1631
|
+
export function writeWorkerStartupScriptCommand(teamName, workerIndex, launchArgs = [], cwd = process.cwd(), extraEnv = {}, workerCliOverride, initialPrompt, workerRole, attachedPlan) {
|
|
1050
1632
|
if (process.platform === 'win32' && !isMsysOrGitBash())
|
|
1051
1633
|
return null;
|
|
1052
1634
|
const stateRoot = extraEnv[OMX_TEAM_STATE_ROOT_ENV]?.trim();
|
|
1053
1635
|
if (!stateRoot)
|
|
1054
1636
|
return null;
|
|
1055
|
-
const
|
|
1056
|
-
const startupEnv = {
|
|
1057
|
-
...readTmuxWorkerAmbientEnv(process.env),
|
|
1058
|
-
...processSpec.env,
|
|
1059
|
-
};
|
|
1060
|
-
const startupArgs = [...processSpec.args];
|
|
1061
|
-
if (processSpec.workerCli === 'codex') {
|
|
1062
|
-
appendTeamWorkerMcpDisableOverrides(startupArgs, { ...process.env, ...extraEnv });
|
|
1063
|
-
}
|
|
1637
|
+
const startupPlan = attachedPlan ?? buildAttachedTmuxWorkerStartupPlan(teamName, workerIndex, launchArgs, cwd, extraEnv, workerCliOverride, initialPrompt, workerRole);
|
|
1638
|
+
const startupEnv = { ...readTmuxWorkerAmbientEnv(process.env), ...startupPlan.processSpec.env };
|
|
1064
1639
|
const scriptPath = join(stateRoot, 'team', teamName, 'runtime', `worker-${workerIndex}-startup.sh`);
|
|
1065
1640
|
mkdirSync(dirname(scriptPath), { recursive: true });
|
|
1066
|
-
writeFileSync(scriptPath, buildWorkerStartupScriptContent(processSpec, startupEnv, startupArgs, cwd, extraEnv), 'utf-8');
|
|
1641
|
+
writeFileSync(scriptPath, buildWorkerStartupScriptContent(startupPlan.processSpec, startupEnv, [...startupPlan.startupArgs], cwd, extraEnv), 'utf-8');
|
|
1067
1642
|
chmodSync(scriptPath, 0o700);
|
|
1068
1643
|
return `exec /bin/sh ${shellQuoteSingle(translatePathForMsys(scriptPath))}`;
|
|
1069
1644
|
}
|
|
@@ -1217,7 +1792,20 @@ export function createTeamSession(teamName, workerCount, cwd, workerLaunchArgs =
|
|
|
1217
1792
|
const safeTeamName = sanitizeTeamName(teamName);
|
|
1218
1793
|
let registeredResizeHook = null;
|
|
1219
1794
|
let registeredClientAttachedHook = null;
|
|
1220
|
-
const
|
|
1795
|
+
const rollbackPanes = new Map();
|
|
1796
|
+
// An owner-tag command may have succeeded even if tmux reports an ambiguous
|
|
1797
|
+
// failure; rollback can only kill panes whose owner is re-authorized.
|
|
1798
|
+
const rollbackTaggedPaneOwnerIds = new Map();
|
|
1799
|
+
let partialTeamTarget = null;
|
|
1800
|
+
let partialLeaderPaneId = null;
|
|
1801
|
+
let partialLeaderPanePid;
|
|
1802
|
+
let partialTeamPaneOwnerId = '';
|
|
1803
|
+
const partialWorkerPaneIds = [];
|
|
1804
|
+
const partialWorkerPaneIdsByIndex = Array.from({ length: workerCount }, () => null);
|
|
1805
|
+
const partialWorkerPanePidsByIndex = Array.from({ length: workerCount }, () => null);
|
|
1806
|
+
const partialStartupCleanupPanePids = new Map();
|
|
1807
|
+
let partialHudPaneId = null;
|
|
1808
|
+
let partialHudPanePid = null;
|
|
1221
1809
|
try {
|
|
1222
1810
|
const tmuxPaneTarget = process.env.TMUX_PANE;
|
|
1223
1811
|
const displayArgs = tmuxPaneTarget
|
|
@@ -1234,82 +1822,185 @@ export function createTeamSession(teamName, workerCount, cwd, workerLaunchArgs =
|
|
|
1234
1822
|
throw new Error(`failed to parse current tmux target: ${context.stdout}`);
|
|
1235
1823
|
}
|
|
1236
1824
|
const teamTarget = `${sessionName}:${windowIndex}`;
|
|
1825
|
+
partialTeamTarget = teamTarget;
|
|
1237
1826
|
const ownerSessionId = (options.ownerSessionId ?? process.env.OMX_SESSION_ID ?? '').trim();
|
|
1238
1827
|
const teamPaneOwnerId = (options.teamPaneOwnerId ?? `team:${safeTeamName}`).trim();
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1828
|
+
partialTeamPaneOwnerId = teamPaneOwnerId;
|
|
1829
|
+
const paneListResult = listPanesResult(teamTarget);
|
|
1830
|
+
if (paneListResult.error)
|
|
1831
|
+
throw new Error(`failed to read tmux pane topology: ${paneListResult.error}`);
|
|
1832
|
+
const leaderPaneId = chooseTeamLeaderPaneId(paneListResult.panes, detectedLeaderPaneId);
|
|
1833
|
+
const leaderProof = readExactPaneProofSync(leaderPaneId);
|
|
1834
|
+
if (leaderProof.status === 'unavailable')
|
|
1835
|
+
throw new ExactPaneProofUnavailableError(leaderProof);
|
|
1836
|
+
if (leaderProof.status === 'gone')
|
|
1837
|
+
throw new Error(`tmux pane is not proven live: ${leaderPaneId}`);
|
|
1838
|
+
let leaderSource = captureSourcePaneAuthority(leaderPaneId);
|
|
1839
|
+
if (leaderSource.sessionName !== sessionName || leaderSource.windowIndex !== windowIndex || leaderSource.panePid !== leaderProof.pid) {
|
|
1840
|
+
throw new Error('tmux source authority changed during team startup');
|
|
1244
1841
|
}
|
|
1245
|
-
const
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
const
|
|
1842
|
+
const leaderPanePid = leaderSource.panePid;
|
|
1843
|
+
partialLeaderPaneId = leaderPaneId;
|
|
1844
|
+
partialLeaderPanePid = leaderPanePid;
|
|
1845
|
+
const initialHudPaneIds = findHudWatchPaneIds(paneListResult.panes, leaderPaneId, { leaderPaneId });
|
|
1846
|
+
const initialWindowPanePids = new Map([[leaderPaneId, leaderPanePid]]);
|
|
1250
1847
|
const omxEntry = resolveOmxCliEntryPath();
|
|
1251
1848
|
const canRecreateTeamHud = Boolean(omxEntry && omxEntry.trim() !== '');
|
|
1849
|
+
// Freeze every HUD owned by this leader even when startup cannot recreate
|
|
1850
|
+
// it; the untouched pane remains part of the authorized topology.
|
|
1851
|
+
for (const hudPaneId of initialHudPaneIds) {
|
|
1852
|
+
const proof = readExactPaneProofSync(hudPaneId);
|
|
1853
|
+
if (proof.status === 'unavailable')
|
|
1854
|
+
throw new ExactPaneProofUnavailableError(proof);
|
|
1855
|
+
if (proof.status === 'gone')
|
|
1856
|
+
throw new Error(`tmux pane is not proven live: ${hudPaneId}`);
|
|
1857
|
+
initialWindowPanePids.set(hudPaneId, proof.pid);
|
|
1858
|
+
}
|
|
1252
1859
|
// Team mode prioritizes leader + worker visibility. Remove HUD panes only
|
|
1253
1860
|
// when we can recreate the team HUD. Otherwise keep the existing HUD alive
|
|
1254
1861
|
// instead of making it disappear on team startup failures or broken installs.
|
|
1862
|
+
requireFrozenWindowTopologySync(teamTarget, initialWindowPanePids);
|
|
1863
|
+
if (ownerSessionId) {
|
|
1864
|
+
runSourceAuthorizedTmux(leaderSource, `set-option -t ${leaderSource.sessionId} ${OMX_INSTANCE_OPTION} ${shellQuoteSingle(ownerSessionId)}`);
|
|
1865
|
+
}
|
|
1866
|
+
if (ownerSessionId) {
|
|
1867
|
+
runSourceAuthorizedTmux(leaderSource, `set-option -p -t ${leaderPaneId} ${OMX_PANE_INSTANCE_OPTION} ${shellQuoteSingle(ownerSessionId)}`);
|
|
1868
|
+
}
|
|
1869
|
+
runSourceAuthorizedTmux(leaderSource, `set-option -p -t ${leaderPaneId} ${OMX_TEAM_PANE_OWNER_OPTION} ${shellQuoteSingle(teamPaneOwnerId)}`);
|
|
1870
|
+
// All subsequent Team mutations must bind the frozen leader owner in the
|
|
1871
|
+
// same tmux-server transaction as their effect. The initial tag itself is
|
|
1872
|
+
// deliberately the sole bootstrap mutation before an owner exists.
|
|
1873
|
+
leaderSource = captureSourcePaneAuthority(leaderPaneId, teamPaneOwnerId);
|
|
1874
|
+
const exactProveAndTagReconciledPane = (paneId) => {
|
|
1875
|
+
const proof = readExactPaneProofSync(paneId);
|
|
1876
|
+
if (proof.status === 'unavailable')
|
|
1877
|
+
throw new ExactPaneProofUnavailableError(proof);
|
|
1878
|
+
if (proof.status === 'gone')
|
|
1879
|
+
return null;
|
|
1880
|
+
const paneSource = captureSourcePaneAuthority(paneId);
|
|
1881
|
+
if (paneSource.panePid !== proof.pid)
|
|
1882
|
+
throw new Error(`tmux pane identity changed: ${paneId}`);
|
|
1883
|
+
if (paneSource.teamPaneOwnerId && paneSource.teamPaneOwnerId !== teamPaneOwnerId) {
|
|
1884
|
+
throw new Error(`tmux pane team owner changed: ${paneId}`);
|
|
1885
|
+
}
|
|
1886
|
+
if (ownerSessionId) {
|
|
1887
|
+
runSourceAuthorizedTmux(paneSource, `set-option -p -t ${paneId} ${OMX_PANE_INSTANCE_OPTION} ${shellQuoteSingle(ownerSessionId)}`);
|
|
1888
|
+
}
|
|
1889
|
+
rollbackTaggedPaneOwnerIds.set(paneId, teamPaneOwnerId);
|
|
1890
|
+
runSourceAuthorizedTmux(paneSource, `set-option -p -t ${paneId} ${OMX_TEAM_PANE_OWNER_OPTION} ${shellQuoteSingle(teamPaneOwnerId)}`);
|
|
1891
|
+
rollbackPanes.set(paneId, proof.pid);
|
|
1892
|
+
return proof.pid;
|
|
1893
|
+
};
|
|
1255
1894
|
if (canRecreateTeamHud) {
|
|
1256
|
-
for (const hudPaneId of
|
|
1257
|
-
|
|
1895
|
+
for (const [hudPaneId, hudPanePid] of initialWindowPanePids) {
|
|
1896
|
+
if (hudPaneId !== leaderPaneId)
|
|
1897
|
+
killExactPaneSync(hudPaneId, hudPanePid);
|
|
1258
1898
|
}
|
|
1259
1899
|
}
|
|
1260
1900
|
const workerPaneIds = [];
|
|
1901
|
+
const workerStartupArgs = Array.from({ length: workerCount }, () => Object.freeze([]));
|
|
1902
|
+
const workerPanePidsByIndex = Array.from({ length: workerCount }, () => null);
|
|
1261
1903
|
let rightStackRootPaneId = null;
|
|
1904
|
+
let rightStackRootPanePid = null;
|
|
1905
|
+
const frozenWindowPanePids = new Map([[leaderPaneId, leaderPanePid]]);
|
|
1906
|
+
const frozenWindowPaneOwners = new Map([[leaderPaneId, teamPaneOwnerId]]);
|
|
1262
1907
|
for (let i = 1; i <= workerCount; i++) {
|
|
1263
1908
|
const startup = workerStartups[i - 1] || {};
|
|
1264
1909
|
const workerCwd = startup.cwd || cwd;
|
|
1265
1910
|
const tmuxWorkerCwd = translatePathForMsys(workerCwd);
|
|
1266
1911
|
const workerEnv = startup.env || {};
|
|
1267
1912
|
const launchArgsForWorker = [...(workerLaunchPolicyPlan[i - 1] ?? workerLaunchArgs)];
|
|
1913
|
+
const attachedStartupPlan = buildAttachedTmuxWorkerStartupPlan(safeTeamName, i, launchArgsForWorker, workerCwd, workerEnv, workerCliPlan[i - 1], startup.initialPrompt, startup.workerRole);
|
|
1914
|
+
workerStartupArgs[i - 1] = attachedStartupPlan.startupArgs;
|
|
1268
1915
|
trustWorkerMiseConfigIfAvailable(workerCwd);
|
|
1269
|
-
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);
|
|
1916
|
+
const cmd = writeWorkerStartupScriptCommand(safeTeamName, i, launchArgsForWorker, workerCwd, workerEnv, workerCliPlan[i - 1], startup.initialPrompt, startup.workerRole, attachedStartupPlan) ?? buildWorkerStartupCommand(safeTeamName, i, launchArgsForWorker, workerCwd, workerEnv, workerCliPlan[i - 1], startup.initialPrompt, startup.workerRole);
|
|
1270
1917
|
// First split creates the right side from leader. Remaining splits stack on the right.
|
|
1271
1918
|
const splitDirection = i === 1 ? '-h' : '-v';
|
|
1272
|
-
const splitTarget = i === 1 ? leaderPaneId : (rightStackRootPaneId ?? leaderPaneId);
|
|
1273
|
-
const
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
splitTarget,
|
|
1278
|
-
'-d',
|
|
1279
|
-
'-P',
|
|
1280
|
-
'-F',
|
|
1281
|
-
'#{pane_id}',
|
|
1282
|
-
'-c',
|
|
1283
|
-
tmuxWorkerCwd,
|
|
1284
|
-
cmd,
|
|
1285
|
-
]);
|
|
1286
|
-
if (!split.ok) {
|
|
1287
|
-
throw new Error(`failed to create worker pane ${i}: ${split.stderr}`);
|
|
1919
|
+
const splitTarget = requireLiveTeamOwnedPaneSync(i === 1 ? leaderPaneId : (rightStackRootPaneId ?? leaderPaneId), i === 1 ? leaderPanePid : (rightStackRootPanePid ?? leaderPanePid), teamPaneOwnerId);
|
|
1920
|
+
const splitSource = captureSourcePaneAuthority(splitTarget, teamPaneOwnerId);
|
|
1921
|
+
let paneId;
|
|
1922
|
+
try {
|
|
1923
|
+
paneId = runSourceAuthorizedSplit(splitSource, (receipt) => `split-window ${splitDirection} -t ${splitTarget} -d -P -F '#{pane_id}' -c ${shellQuoteSingle(tmuxWorkerCwd)} ${shellQuoteSingle(bindSplitReceiptToPaneCommand(cmd, receipt))}`);
|
|
1288
1924
|
}
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1925
|
+
catch (error) {
|
|
1926
|
+
if (error instanceof AmbiguousSplitWindowOutputError) {
|
|
1927
|
+
const isUnassignedAmbiguity = error.newlyObservedPaneIds.length > 1;
|
|
1928
|
+
for (const candidatePaneId of error.newlyObservedPaneIds) {
|
|
1929
|
+
rollbackPanes.set(candidatePaneId, null);
|
|
1930
|
+
partialWorkerPaneIds.push(candidatePaneId);
|
|
1931
|
+
if (isUnassignedAmbiguity)
|
|
1932
|
+
partialStartupCleanupPanePids.set(candidatePaneId, null);
|
|
1933
|
+
}
|
|
1934
|
+
if (error.newlyObservedPaneIds.length === 1) {
|
|
1935
|
+
partialWorkerPaneIdsByIndex[i - 1] = error.newlyObservedPaneIds[0];
|
|
1936
|
+
}
|
|
1937
|
+
for (const candidatePaneId of error.newlyObservedPaneIds) {
|
|
1938
|
+
const reconciledPid = exactProveAndTagReconciledPane(candidatePaneId);
|
|
1939
|
+
if (reconciledPid === null) {
|
|
1940
|
+
rollbackPanes.delete(candidatePaneId);
|
|
1941
|
+
partialWorkerPaneIds.splice(partialWorkerPaneIds.lastIndexOf(candidatePaneId), 1);
|
|
1942
|
+
partialStartupCleanupPanePids.delete(candidatePaneId);
|
|
1943
|
+
if (partialWorkerPaneIdsByIndex[i - 1] === candidatePaneId) {
|
|
1944
|
+
partialWorkerPaneIdsByIndex[i - 1] = null;
|
|
1945
|
+
}
|
|
1946
|
+
}
|
|
1947
|
+
else if (isUnassignedAmbiguity) {
|
|
1948
|
+
partialStartupCleanupPanePids.set(candidatePaneId, reconciledPid);
|
|
1949
|
+
}
|
|
1950
|
+
else {
|
|
1951
|
+
partialWorkerPanePidsByIndex[i - 1] = reconciledPid;
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
throw error;
|
|
1292
1956
|
}
|
|
1293
|
-
|
|
1957
|
+
// The pane exists once split-window returns its concrete ID. Persist it in
|
|
1958
|
+
// rollback/partial state before its first proof: a malformed or unavailable
|
|
1959
|
+
// topology must retain cleanup debt, but cannot authorize an effect.
|
|
1960
|
+
rollbackPanes.set(paneId, null);
|
|
1961
|
+
partialWorkerPaneIds.push(paneId);
|
|
1962
|
+
partialWorkerPaneIdsByIndex[i - 1] = paneId;
|
|
1963
|
+
const paneProof = readExactPaneProofSync(paneId);
|
|
1964
|
+
if (paneProof.status === 'unavailable')
|
|
1965
|
+
throw new ExactPaneProofUnavailableError(paneProof);
|
|
1966
|
+
if (paneProof.status === 'gone')
|
|
1967
|
+
throw new Error(`tmux pane is not proven live: ${paneId}`);
|
|
1968
|
+
const panePid = paneProof.pid;
|
|
1969
|
+
rollbackPanes.set(paneId, panePid);
|
|
1970
|
+
partialWorkerPanePidsByIndex[i - 1] = panePid;
|
|
1971
|
+
workerPanePidsByIndex[i - 1] = panePid;
|
|
1972
|
+
frozenWindowPanePids.set(paneId, panePid);
|
|
1294
1973
|
if (isNativeWindows() && !waitForPaneToRemainPresent(teamTarget, paneId)) {
|
|
1295
1974
|
throw new Error(`worker pane ${i} did not remain present after tmux split-window returned ${paneId}`);
|
|
1296
1975
|
}
|
|
1297
|
-
|
|
1298
|
-
|
|
1976
|
+
const paneSource = captureSourcePaneAuthority(paneId);
|
|
1977
|
+
if (paneSource.panePid !== panePid)
|
|
1978
|
+
throw new Error(`tmux pane identity changed: ${paneId}`);
|
|
1979
|
+
if (ownerSessionId) {
|
|
1980
|
+
runSourceAuthorizedTmux(paneSource, `set-option -p -t ${paneId} ${OMX_PANE_INSTANCE_OPTION} ${shellQuoteSingle(ownerSessionId)}`);
|
|
1981
|
+
}
|
|
1982
|
+
rollbackTaggedPaneOwnerIds.set(paneId, teamPaneOwnerId);
|
|
1983
|
+
runSourceAuthorizedTmux(paneSource, `set-option -p -t ${paneId} ${OMX_TEAM_PANE_OWNER_OPTION} ${shellQuoteSingle(teamPaneOwnerId)}`);
|
|
1984
|
+
frozenWindowPaneOwners.set(paneId, teamPaneOwnerId);
|
|
1299
1985
|
workerPaneIds.push(paneId);
|
|
1300
|
-
if (i === 1)
|
|
1986
|
+
if (i === 1) {
|
|
1301
1987
|
rightStackRootPaneId = paneId;
|
|
1988
|
+
rightStackRootPanePid = panePid;
|
|
1989
|
+
}
|
|
1302
1990
|
}
|
|
1303
1991
|
// Keep leader as full left/main pane; workers stay stacked on the right.
|
|
1304
|
-
|
|
1992
|
+
requireFrozenWindowTopologySync(teamTarget, frozenWindowPanePids, frozenWindowPaneOwners);
|
|
1993
|
+
runSourceAuthorizedTmux(leaderSource, `select-layout -t ${leaderSource.windowId} main-vertical`);
|
|
1305
1994
|
// Force leader pane to use half the window width.
|
|
1306
1995
|
const windowWidthResult = runTmux(['display-message', '-p', '-t', teamTarget, '#{window_width}']);
|
|
1307
1996
|
if (windowWidthResult.ok) {
|
|
1308
1997
|
const width = Number.parseInt(windowWidthResult.stdout.split('\n')[0]?.trim() || '', 10);
|
|
1309
1998
|
if (Number.isFinite(width) && width >= 40) {
|
|
1310
1999
|
const half = String(Math.floor(width / 2));
|
|
1311
|
-
|
|
1312
|
-
|
|
2000
|
+
requireFrozenWindowTopologySync(teamTarget, frozenWindowPanePids, frozenWindowPaneOwners);
|
|
2001
|
+
runSourceAuthorizedTmux(leaderSource, `set-window-option -t ${leaderSource.windowId} main-pane-width ${half}`);
|
|
2002
|
+
requireFrozenWindowTopologySync(teamTarget, frozenWindowPanePids, frozenWindowPaneOwners);
|
|
2003
|
+
runSourceAuthorizedTmux(leaderSource, `select-layout -t ${leaderSource.windowId} main-vertical`);
|
|
1313
2004
|
}
|
|
1314
2005
|
}
|
|
1315
2006
|
// Re-create a single team HUD as a full-width bottom strip spanning both
|
|
@@ -1322,82 +2013,159 @@ export function createTeamSession(teamName, workerCount, cwd, workerLaunchArgs =
|
|
|
1322
2013
|
if (canRecreateTeamHud && omxEntry) {
|
|
1323
2014
|
const hudCmd = `exec env ${formatHudEnvAssignments(process.env, { sessionId: ownerSessionId, leaderPaneId })} node ${shellQuoteSingle(translatePathForMsys(omxEntry))} hud --watch`;
|
|
1324
2015
|
const hudCwd = translatePathForMsys(cwd);
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
2016
|
+
requireFrozenWindowTopologySync(teamTarget, frozenWindowPanePids, frozenWindowPaneOwners);
|
|
2017
|
+
const hudSplitTarget = leaderPaneId;
|
|
2018
|
+
const hudSource = captureSourcePaneAuthority(hudSplitTarget, teamPaneOwnerId);
|
|
2019
|
+
let id;
|
|
2020
|
+
try {
|
|
2021
|
+
id = runSourceAuthorizedSplit(hudSource, (receipt) => `split-window -v -f -l ${HUD_TMUX_TEAM_HEIGHT_LINES} -t ${hudSplitTarget} -d -P -F '#{pane_id}' -c ${shellQuoteSingle(hudCwd)} ${shellQuoteSingle(bindSplitReceiptToPaneCommand(hudCmd, receipt))}`);
|
|
2022
|
+
}
|
|
2023
|
+
catch (error) {
|
|
2024
|
+
if (error instanceof AmbiguousSplitWindowOutputError) {
|
|
2025
|
+
const isUnassignedAmbiguity = error.newlyObservedPaneIds.length > 1;
|
|
2026
|
+
for (const candidatePaneId of error.newlyObservedPaneIds) {
|
|
2027
|
+
rollbackPanes.set(candidatePaneId, null);
|
|
2028
|
+
if (isUnassignedAmbiguity)
|
|
2029
|
+
partialStartupCleanupPanePids.set(candidatePaneId, null);
|
|
1334
2030
|
}
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
if (isNativeWindows()) {
|
|
1339
|
-
// Native Windows tmux support may flow through psmux; issuing a
|
|
1340
|
-
// direct control-plane resize avoids nested run-shell PATH drift.
|
|
1341
|
-
const reconcile = runTmux(buildHudResizeArgs(hudPaneId));
|
|
1342
|
-
if (!reconcile.ok) {
|
|
1343
|
-
throw new Error(`failed to reconcile HUD resize: ${reconcile.stderr}`);
|
|
1344
|
-
}
|
|
2031
|
+
if (error.newlyObservedPaneIds.length === 1) {
|
|
2032
|
+
partialHudPaneId = error.newlyObservedPaneIds[0];
|
|
2033
|
+
partialHudPanePid = null;
|
|
1345
2034
|
}
|
|
1346
|
-
|
|
1347
|
-
const
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
resizeHookName = hookName;
|
|
1354
|
-
registeredResizeHook = { name: resizeHookName, target: resizeHookTarget };
|
|
2035
|
+
for (const candidatePaneId of error.newlyObservedPaneIds) {
|
|
2036
|
+
const reconciledPid = exactProveAndTagReconciledPane(candidatePaneId);
|
|
2037
|
+
if (reconciledPid === null) {
|
|
2038
|
+
rollbackPanes.delete(candidatePaneId);
|
|
2039
|
+
partialStartupCleanupPanePids.delete(candidatePaneId);
|
|
2040
|
+
if (partialHudPaneId === candidatePaneId)
|
|
2041
|
+
partialHudPaneId = null;
|
|
1355
2042
|
}
|
|
1356
|
-
else {
|
|
1357
|
-
|
|
1358
|
-
// abort madmax/team startup after panes were successfully created. Keep the
|
|
1359
|
-
// fallback narrow: skip only the long-lived resize hook metadata, then
|
|
1360
|
-
// still try the one-shot client-attached reconcile plus the explicit
|
|
1361
|
-
// delayed/direct resize checks below so real tmux/run-shell failures
|
|
1362
|
-
// still surface.
|
|
1363
|
-
console.warn(`[omx] tmux resize hook unavailable for ${hookTarget} (${hookName}): ${registerHook.stderr}; `
|
|
1364
|
-
+ 'continuing with best-effort HUD resize fallback.');
|
|
1365
|
-
}
|
|
1366
|
-
const registerClientAttachedHook = runTmux(buildRegisterClientAttachedReconcileArgs(hookTarget, clientAttachedHookName, hudPaneId));
|
|
1367
|
-
if (registerClientAttachedHook.ok) {
|
|
1368
|
-
registeredClientAttachedHook = { name: clientAttachedHookName, target: hookTarget };
|
|
2043
|
+
else if (isUnassignedAmbiguity) {
|
|
2044
|
+
partialStartupCleanupPanePids.set(candidatePaneId, reconciledPid);
|
|
1369
2045
|
}
|
|
1370
2046
|
else {
|
|
1371
|
-
|
|
1372
|
-
+ `(${clientAttachedHookName}): ${registerClientAttachedHook.stderr}; continuing with delayed HUD resize fallback.`);
|
|
1373
|
-
}
|
|
1374
|
-
const delayed = runTmux(buildScheduleDelayedHudResizeArgs(hudPaneId));
|
|
1375
|
-
if (!delayed.ok) {
|
|
1376
|
-
console.warn(`[omx] tmux delayed HUD resize unavailable for ${hudPaneId}: ${delayed.stderr}; continuing.`);
|
|
1377
|
-
}
|
|
1378
|
-
const reconcile = runTmux(buildReconcileHudResizeArgs(hudPaneId));
|
|
1379
|
-
if (!reconcile.ok) {
|
|
1380
|
-
console.warn(`[omx] tmux HUD resize reconcile unavailable for ${hudPaneId}: ${reconcile.stderr}; continuing.`);
|
|
2047
|
+
partialHudPanePid = reconciledPid;
|
|
1381
2048
|
}
|
|
1382
2049
|
}
|
|
1383
2050
|
}
|
|
2051
|
+
throw error;
|
|
2052
|
+
}
|
|
2053
|
+
{
|
|
2054
|
+
// split-window has created a concrete pane even though no proof has
|
|
2055
|
+
// authorized an effect yet. Retain PID-less cleanup debt first.
|
|
2056
|
+
rollbackPanes.set(id, null);
|
|
2057
|
+
partialHudPaneId = id;
|
|
2058
|
+
partialHudPanePid = null;
|
|
2059
|
+
const hudProof = readExactPaneProofSync(id);
|
|
2060
|
+
if (hudProof.status === 'unavailable')
|
|
2061
|
+
throw new ExactPaneProofUnavailableError(hudProof);
|
|
2062
|
+
if (hudProof.status === 'gone')
|
|
2063
|
+
throw new Error(`tmux pane is not proven live: ${id}`);
|
|
2064
|
+
const hudPanePid = hudProof.pid;
|
|
2065
|
+
frozenWindowPanePids.set(id, hudPanePid);
|
|
2066
|
+
rollbackPanes.set(id, hudPanePid);
|
|
2067
|
+
partialHudPanePid = hudPanePid;
|
|
2068
|
+
if (isNativeWindows() && !waitForPaneToRemainPresent(teamTarget, id)) {
|
|
2069
|
+
throw new Error(`HUD pane did not remain present after tmux split-window returned ${id}`);
|
|
2070
|
+
}
|
|
2071
|
+
const hudSource = captureSourcePaneAuthority(id);
|
|
2072
|
+
if (hudSource.panePid !== hudPanePid)
|
|
2073
|
+
throw new Error(`tmux pane identity changed: ${id}`);
|
|
2074
|
+
if (ownerSessionId) {
|
|
2075
|
+
runSourceAuthorizedTmux(hudSource, `set-option -p -t ${id} ${OMX_PANE_INSTANCE_OPTION} ${shellQuoteSingle(ownerSessionId)}`);
|
|
2076
|
+
}
|
|
2077
|
+
rollbackTaggedPaneOwnerIds.set(id, teamPaneOwnerId);
|
|
2078
|
+
runSourceAuthorizedTmux(hudSource, `set-option -p -t ${id} ${OMX_TEAM_PANE_OWNER_OPTION} ${shellQuoteSingle(teamPaneOwnerId)}`);
|
|
2079
|
+
frozenWindowPaneOwners.set(id, teamPaneOwnerId);
|
|
2080
|
+
hudPaneId = id;
|
|
2081
|
+
if (isNativeWindows()) {
|
|
2082
|
+
const provenHudPaneId = requireLiveTeamOwnedPaneSync(hudPaneId, hudPanePid, teamPaneOwnerId);
|
|
2083
|
+
const reconcile = runTmux(buildHudResizeArgs(provenHudPaneId));
|
|
2084
|
+
if (!reconcile.ok) {
|
|
2085
|
+
throw new Error(`failed to reconcile HUD resize: ${reconcile.stderr}`);
|
|
2086
|
+
}
|
|
2087
|
+
}
|
|
2088
|
+
else {
|
|
2089
|
+
const hookTarget = buildResizeHookTarget(sessionName, windowIndex);
|
|
2090
|
+
const hookName = buildResizeHookName(safeTeamName, sessionName, windowIndex, hudPaneId);
|
|
2091
|
+
requireLiveTeamOwnedPaneSync(hudPaneId, hudPanePid, teamPaneOwnerId);
|
|
2092
|
+
const registerHook = runTmux(buildRegisterResizeHookArgs(hookTarget, hookName, hudPaneId, HUD_TMUX_TEAM_HEIGHT_LINES, hudPanePid, teamPaneOwnerId));
|
|
2093
|
+
const clientAttachedHookName = buildClientAttachedReconcileHookName(safeTeamName, sessionName, windowIndex, hudPaneId);
|
|
2094
|
+
if (registerHook.ok) {
|
|
2095
|
+
resizeHookTarget = hookTarget;
|
|
2096
|
+
resizeHookName = hookName;
|
|
2097
|
+
registeredResizeHook = {
|
|
2098
|
+
name: resizeHookName,
|
|
2099
|
+
target: resizeHookTarget,
|
|
2100
|
+
leaderPaneId,
|
|
2101
|
+
leaderPanePid,
|
|
2102
|
+
hudPaneId,
|
|
2103
|
+
hudPanePid,
|
|
2104
|
+
teamPaneOwnerId,
|
|
2105
|
+
};
|
|
2106
|
+
}
|
|
2107
|
+
else {
|
|
2108
|
+
console.warn(`[omx] tmux resize hook unavailable for ${hookTarget} (${hookName}): ${registerHook.stderr}; `
|
|
2109
|
+
+ 'continuing with best-effort HUD resize fallback.');
|
|
2110
|
+
}
|
|
2111
|
+
requireLiveTeamOwnedPaneSync(hudPaneId, hudPanePid, teamPaneOwnerId);
|
|
2112
|
+
const registerClientAttachedHook = runTmux(buildRegisterClientAttachedReconcileArgs(hookTarget, clientAttachedHookName, hudPaneId, HUD_TMUX_TEAM_HEIGHT_LINES, hudPanePid, teamPaneOwnerId));
|
|
2113
|
+
if (registerClientAttachedHook.ok) {
|
|
2114
|
+
registeredClientAttachedHook = {
|
|
2115
|
+
name: clientAttachedHookName,
|
|
2116
|
+
target: hookTarget,
|
|
2117
|
+
leaderPaneId,
|
|
2118
|
+
leaderPanePid,
|
|
2119
|
+
hudPaneId,
|
|
2120
|
+
hudPanePid,
|
|
2121
|
+
teamPaneOwnerId,
|
|
2122
|
+
};
|
|
2123
|
+
}
|
|
2124
|
+
else {
|
|
2125
|
+
console.warn(`[omx] tmux client-attached resize fallback unavailable for ${hookTarget} `
|
|
2126
|
+
+ `(${clientAttachedHookName}): ${registerClientAttachedHook.stderr}; continuing with delayed HUD resize fallback.`);
|
|
2127
|
+
}
|
|
2128
|
+
requireLiveTeamOwnedPaneSync(hudPaneId, hudPanePid, teamPaneOwnerId);
|
|
2129
|
+
const delayed = runTmux(buildScheduleDelayedHudResizeArgs(hudPaneId, HUD_RESIZE_RECONCILE_DELAY_SECONDS, HUD_TMUX_TEAM_HEIGHT_LINES, hudPanePid, teamPaneOwnerId));
|
|
2130
|
+
if (!delayed.ok) {
|
|
2131
|
+
console.warn(`[omx] tmux delayed HUD resize unavailable for ${hudPaneId}: ${delayed.stderr}; continuing.`);
|
|
2132
|
+
}
|
|
2133
|
+
requireLiveTeamOwnedPaneSync(hudPaneId, hudPanePid, teamPaneOwnerId);
|
|
2134
|
+
const reconcile = runTmux(buildReconcileHudResizeArgs(hudPaneId, HUD_TMUX_TEAM_HEIGHT_LINES, hudPanePid, teamPaneOwnerId));
|
|
2135
|
+
if (!reconcile.ok) {
|
|
2136
|
+
console.warn(`[omx] tmux HUD resize reconcile unavailable for ${hudPaneId}: ${reconcile.stderr}; continuing.`);
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
1384
2139
|
}
|
|
1385
2140
|
}
|
|
1386
|
-
|
|
1387
|
-
|
|
2141
|
+
const focusedLeader = requireLiveTeamOwnedPaneSync(leaderPaneId, leaderPanePid, teamPaneOwnerId);
|
|
2142
|
+
if (focusedLeader !== leaderSource.paneId)
|
|
2143
|
+
throw new Error(`tmux pane identity changed: ${leaderPaneId}`);
|
|
2144
|
+
runSourceAuthorizedTmux(leaderSource, `select-pane -t ${leaderPaneId}`);
|
|
2145
|
+
redrawLeaderPaneAfterTeamLayout(leaderSource, teamPaneOwnerId);
|
|
1388
2146
|
sleepSeconds(0.5);
|
|
1389
2147
|
// Enable mouse scrolling so agent output panes can be scrolled with the
|
|
1390
2148
|
// mouse wheel without conflicting with keyboard up/down arrow-key input
|
|
1391
2149
|
// history navigation in the Codex CLI input field. (issue #103)
|
|
1392
2150
|
// Opt-out: set OMX_TEAM_MOUSE=0 in the environment.
|
|
1393
2151
|
if (process.env.OMX_TEAM_MOUSE !== '0') {
|
|
1394
|
-
enableMouseScrolling(sessionName);
|
|
2152
|
+
enableMouseScrolling(sessionName, () => { requireLiveTeamOwnedPaneSync(leaderPaneId, leaderPanePid, teamPaneOwnerId); });
|
|
1395
2153
|
}
|
|
1396
2154
|
return {
|
|
1397
2155
|
name: teamTarget,
|
|
2156
|
+
tmuxSessionId: leaderSource.sessionId,
|
|
2157
|
+
tmuxSessionCreated: leaderSource.sessionCreated,
|
|
1398
2158
|
workerCount,
|
|
1399
2159
|
cwd,
|
|
1400
2160
|
workerPaneIds,
|
|
2161
|
+
workerPaneIdsByIndex: [...workerPaneIds],
|
|
2162
|
+
workerPanePidsByIndex,
|
|
2163
|
+
leaderPanePid,
|
|
2164
|
+
workerStartupArgs: Object.freeze(workerStartupArgs),
|
|
2165
|
+
leaderSessionId: leaderSource.sessionId,
|
|
2166
|
+
leaderSessionCreated: leaderSource.sessionCreated,
|
|
2167
|
+
leaderWindowId: leaderSource.windowId,
|
|
2168
|
+
hudPanePid: partialHudPanePid,
|
|
1401
2169
|
leaderPaneId,
|
|
1402
2170
|
hudPaneId,
|
|
1403
2171
|
resizeHookName,
|
|
@@ -1406,14 +2174,90 @@ export function createTeamSession(teamName, workerCount, cwd, workerLaunchArgs =
|
|
|
1406
2174
|
};
|
|
1407
2175
|
}
|
|
1408
2176
|
catch (error) {
|
|
1409
|
-
|
|
1410
|
-
|
|
2177
|
+
const cleanupErrors = [];
|
|
2178
|
+
if (error instanceof AmbiguousSplitWindowOutputError && error.reconciliationError) {
|
|
2179
|
+
cleanupErrors.push('failed to reconcile tmux pane topology after ambiguous split output');
|
|
1411
2180
|
}
|
|
1412
|
-
|
|
1413
|
-
|
|
2181
|
+
const unregisterAuthorizedHook = (hook, unregister, label) => {
|
|
2182
|
+
try {
|
|
2183
|
+
requireLiveTeamOwnedPaneSync(hook.leaderPaneId, hook.leaderPanePid, hook.teamPaneOwnerId);
|
|
2184
|
+
requireLiveTeamOwnedPaneSync(hook.hudPaneId, hook.hudPanePid, hook.teamPaneOwnerId);
|
|
2185
|
+
const unregistered = runTmux(unregister(hook.target, hook.name));
|
|
2186
|
+
if (unregistered.ok)
|
|
2187
|
+
return true;
|
|
2188
|
+
cleanupErrors.push(`failed to unregister tmux ${label} hook ${hook.name}: ${unregistered.stderr}`);
|
|
2189
|
+
}
|
|
2190
|
+
catch (cleanupError) {
|
|
2191
|
+
cleanupErrors.push(`unable to authorize tmux ${label} hook cleanup ${hook.name}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
|
|
2192
|
+
}
|
|
2193
|
+
return false;
|
|
2194
|
+
};
|
|
2195
|
+
if (registeredClientAttachedHook && unregisterAuthorizedHook(registeredClientAttachedHook, buildUnregisterClientAttachedReconcileArgs, 'client-attached')) {
|
|
2196
|
+
registeredClientAttachedHook = null;
|
|
2197
|
+
}
|
|
2198
|
+
if (registeredResizeHook && unregisterAuthorizedHook(registeredResizeHook, buildUnregisterResizeHookArgs, 'resize')) {
|
|
2199
|
+
registeredResizeHook = null;
|
|
2200
|
+
}
|
|
2201
|
+
const proofUnavailable = [];
|
|
2202
|
+
if (error instanceof ExactPaneProofUnavailableError)
|
|
2203
|
+
proofUnavailable.push(error.proof);
|
|
2204
|
+
const unresolvedPaneIds = new Set(rollbackPanes.keys());
|
|
2205
|
+
for (const [paneId, panePid] of rollbackPanes) {
|
|
2206
|
+
// split-window IDs without a positive exact PID proof are cleanup debt,
|
|
2207
|
+
// never authorization to affect a potentially recycled pane ID.
|
|
2208
|
+
if (panePid === null)
|
|
2209
|
+
continue;
|
|
2210
|
+
try {
|
|
2211
|
+
const expectedOwnerId = rollbackTaggedPaneOwnerIds.get(paneId);
|
|
2212
|
+
killExactPaneSync(paneId, panePid, expectedOwnerId
|
|
2213
|
+
? () => { requireLiveTeamOwnedPaneSync(paneId, panePid, expectedOwnerId); }
|
|
2214
|
+
: undefined);
|
|
2215
|
+
unresolvedPaneIds.delete(paneId);
|
|
2216
|
+
}
|
|
2217
|
+
catch (cleanupError) {
|
|
2218
|
+
if (cleanupError instanceof ExactPaneProofUnavailableError) {
|
|
2219
|
+
proofUnavailable.push(cleanupError.proof);
|
|
2220
|
+
break;
|
|
2221
|
+
}
|
|
2222
|
+
else {
|
|
2223
|
+
cleanupErrors.push(cleanupError instanceof Error ? cleanupError.message : String(cleanupError));
|
|
2224
|
+
}
|
|
2225
|
+
}
|
|
1414
2226
|
}
|
|
1415
|
-
|
|
1416
|
-
|
|
2227
|
+
const unresolvedWorkerPaneIds = partialWorkerPaneIds.filter((paneId) => unresolvedPaneIds.has(paneId));
|
|
2228
|
+
const unresolvedWorkerPaneIdsByIndex = partialWorkerPaneIdsByIndex
|
|
2229
|
+
.map((paneId) => paneId && unresolvedPaneIds.has(paneId) ? paneId : null);
|
|
2230
|
+
const unresolvedWorkerPanePidsByIndex = partialWorkerPanePidsByIndex
|
|
2231
|
+
.map((panePid, index) => partialWorkerPaneIdsByIndex[index] && unresolvedPaneIds.has(partialWorkerPaneIdsByIndex[index]) ? panePid : null);
|
|
2232
|
+
const unresolvedStartupCleanupPanes = [...partialStartupCleanupPanePids.entries()]
|
|
2233
|
+
.filter(([paneId]) => unresolvedPaneIds.has(paneId))
|
|
2234
|
+
.map(([paneId, panePid]) => ({ paneId, panePid }));
|
|
2235
|
+
const unresolvedHudPaneId = partialHudPaneId && (unresolvedPaneIds.has(partialHudPaneId)
|
|
2236
|
+
|| registeredResizeHook !== null
|
|
2237
|
+
|| registeredClientAttachedHook !== null)
|
|
2238
|
+
? partialHudPaneId
|
|
2239
|
+
: null;
|
|
2240
|
+
const hasRecoverablePartialArtifact = unresolvedPaneIds.size > 0
|
|
2241
|
+
|| registeredResizeHook !== null
|
|
2242
|
+
|| registeredClientAttachedHook !== null
|
|
2243
|
+
|| (error instanceof AmbiguousSplitWindowOutputError && error.reconciliationError !== null);
|
|
2244
|
+
if (hasRecoverablePartialArtifact && partialTeamTarget && partialLeaderPaneId) {
|
|
2245
|
+
throw new CreateTeamSessionPartialError({
|
|
2246
|
+
name: partialTeamTarget,
|
|
2247
|
+
workerCount,
|
|
2248
|
+
cwd,
|
|
2249
|
+
workerPaneIds: unresolvedWorkerPaneIds,
|
|
2250
|
+
workerPaneIdsByIndex: unresolvedWorkerPaneIdsByIndex,
|
|
2251
|
+
workerPanePidsByIndex: unresolvedWorkerPanePidsByIndex,
|
|
2252
|
+
startupCleanupPanes: unresolvedStartupCleanupPanes,
|
|
2253
|
+
leaderPanePid: partialLeaderPanePid,
|
|
2254
|
+
hudPanePid: unresolvedHudPaneId ? partialHudPanePid : null,
|
|
2255
|
+
leaderPaneId: partialLeaderPaneId,
|
|
2256
|
+
hudPaneId: unresolvedHudPaneId,
|
|
2257
|
+
resizeHookName: registeredResizeHook?.name ?? null,
|
|
2258
|
+
resizeHookTarget: registeredResizeHook?.target ?? null,
|
|
2259
|
+
teamPaneOwnerId: partialTeamPaneOwnerId,
|
|
2260
|
+
}, proofUnavailable, error, cleanupErrors);
|
|
1417
2261
|
}
|
|
1418
2262
|
throw error;
|
|
1419
2263
|
}
|
|
@@ -1422,34 +2266,75 @@ export function restoreStandaloneHudPane(leaderPaneId, cwd, options = {}) {
|
|
|
1422
2266
|
const normalizedLeaderPaneId = normalizePaneTarget(leaderPaneId);
|
|
1423
2267
|
if (!normalizedLeaderPaneId)
|
|
1424
2268
|
return null;
|
|
2269
|
+
// The split is irreversible without a durable cleanup obligation. Validate
|
|
2270
|
+
// its canonical Team-root location before authorizing any pane effect.
|
|
2271
|
+
restoredHudCleanupDebtPath(cwd, options.stateRoot);
|
|
1425
2272
|
const omxEntry = resolveOmxCliEntryPath();
|
|
1426
2273
|
if (!omxEntry || omxEntry.trim() === '')
|
|
1427
2274
|
return null;
|
|
1428
|
-
const
|
|
2275
|
+
const leaderPanePid = (() => {
|
|
2276
|
+
const proof = readExactPaneProofSync(normalizedLeaderPaneId);
|
|
2277
|
+
if (proof.status === 'unavailable')
|
|
2278
|
+
throw new ExactPaneProofUnavailableError(proof);
|
|
2279
|
+
if (proof.status === 'gone')
|
|
2280
|
+
throw new Error(`tmux pane is not proven live: ${normalizedLeaderPaneId}`);
|
|
2281
|
+
if (options.expectedLeaderPanePid !== undefined && proof.pid !== options.expectedLeaderPanePid) {
|
|
2282
|
+
throw new Error(`tmux pane identity changed: ${normalizedLeaderPaneId}`);
|
|
2283
|
+
}
|
|
2284
|
+
return proof.pid;
|
|
2285
|
+
})();
|
|
2286
|
+
const requireAuthorizedLeaderPane = () => {
|
|
2287
|
+
options.assertLeaderPaneAuthorization?.();
|
|
2288
|
+
return requireLiveExactPaneSync(normalizedLeaderPaneId, options.expectedLeaderPanePid ?? leaderPanePid);
|
|
2289
|
+
};
|
|
2290
|
+
requireAuthorizedLeaderPane();
|
|
2291
|
+
const paneListResult = listPanesResult(normalizedLeaderPaneId);
|
|
2292
|
+
if (paneListResult.error)
|
|
2293
|
+
throw new Error(`failed to read tmux pane topology: ${paneListResult.error}`);
|
|
2294
|
+
const [existingHudPaneId, ...duplicateHudPaneIds] = findHudWatchPaneIds(paneListResult.panes, normalizedLeaderPaneId, { leaderPaneId: normalizedLeaderPaneId });
|
|
2295
|
+
const ownedHudPanePids = new Map();
|
|
2296
|
+
for (const paneId of [existingHudPaneId, ...duplicateHudPaneIds]) {
|
|
2297
|
+
if (!paneId)
|
|
2298
|
+
continue;
|
|
2299
|
+
const proof = readExactPaneProofSync(paneId);
|
|
2300
|
+
if (proof.status === 'unavailable')
|
|
2301
|
+
throw new ExactPaneProofUnavailableError(proof);
|
|
2302
|
+
if (proof.status === 'gone')
|
|
2303
|
+
throw new Error(`tmux pane is not proven live: ${paneId}`);
|
|
2304
|
+
ownedHudPanePids.set(paneId, proof.pid);
|
|
2305
|
+
}
|
|
1429
2306
|
for (const paneId of duplicateHudPaneIds) {
|
|
1430
|
-
|
|
2307
|
+
killExactPaneSync(paneId, ownedHudPanePids.get(paneId), requireAuthorizedLeaderPane);
|
|
1431
2308
|
}
|
|
1432
2309
|
if (existingHudPaneId) {
|
|
1433
2310
|
if (isNativeWindows()) {
|
|
1434
|
-
|
|
2311
|
+
const exactHudPaneId = requireLiveExactPaneSync(existingHudPaneId, ownedHudPanePids.get(existingHudPaneId));
|
|
2312
|
+
requireAuthorizedLeaderPane();
|
|
2313
|
+
const reconcile = runTmux(buildHudResizeArgs(exactHudPaneId));
|
|
2314
|
+
if (!reconcile.ok)
|
|
2315
|
+
throw new Error(`failed to reconcile standalone HUD resize: ${reconcile.stderr}`);
|
|
1435
2316
|
}
|
|
1436
2317
|
else {
|
|
1437
|
-
|
|
1438
|
-
runTmux(
|
|
2318
|
+
requireAuthorizedLeaderPane();
|
|
2319
|
+
runTmux(buildScheduleDelayedHudResizeArgs(existingHudPaneId, HUD_RESIZE_RECONCILE_DELAY_SECONDS, HUD_TMUX_TEAM_HEIGHT_LINES, ownedHudPanePids.get(existingHudPaneId)));
|
|
2320
|
+
requireAuthorizedLeaderPane();
|
|
2321
|
+
runTmux(buildReconcileHudResizeArgs(existingHudPaneId, HUD_TMUX_TEAM_HEIGHT_LINES, ownedHudPanePids.get(existingHudPaneId)));
|
|
1439
2322
|
}
|
|
1440
|
-
runTmux(['select-pane', '-t',
|
|
2323
|
+
runTmux(['select-pane', '-t', requireAuthorizedLeaderPane()]);
|
|
1441
2324
|
return existingHudPaneId;
|
|
1442
2325
|
}
|
|
1443
|
-
const
|
|
2326
|
+
const beforeSplitPaneIds = new Set(paneListResult.panes.map((pane) => pane.paneId));
|
|
2327
|
+
const splitReceipt = sourceTransactionReceipt();
|
|
2328
|
+
const hudCmd = bindSplitReceiptToPaneCommand(`exec env ${formatHudEnvAssignments(process.env, { sessionId: options.sessionId, leaderPaneId: normalizedLeaderPaneId })} ${shellQuoteSingle(translatePathForMsys(resolveLeaderNodePath()))} ${shellQuoteSingle(translatePathForMsys(omxEntry))} hud --watch`, splitReceipt);
|
|
1444
2329
|
let hudResult = null;
|
|
1445
|
-
for (const restoreCwd of resolveStandaloneHudRestoreCwdCandidates(normalizedLeaderPaneId, cwd, options.cwd)) {
|
|
2330
|
+
for (const restoreCwd of resolveStandaloneHudRestoreCwdCandidates(normalizedLeaderPaneId, cwd, options.cwd, requireAuthorizedLeaderPane)) {
|
|
1446
2331
|
const candidateResult = runTmux([
|
|
1447
2332
|
'split-window',
|
|
1448
2333
|
'-v',
|
|
1449
2334
|
'-l',
|
|
1450
2335
|
String(HUD_TMUX_TEAM_HEIGHT_LINES),
|
|
1451
2336
|
'-t',
|
|
1452
|
-
|
|
2337
|
+
requireAuthorizedLeaderPane(),
|
|
1453
2338
|
'-d',
|
|
1454
2339
|
'-P',
|
|
1455
2340
|
'-F',
|
|
@@ -1457,7 +2342,7 @@ export function restoreStandaloneHudPane(leaderPaneId, cwd, options = {}) {
|
|
|
1457
2342
|
'-c',
|
|
1458
2343
|
translatePathForMsys(restoreCwd.rawPath),
|
|
1459
2344
|
hudCmd,
|
|
1460
|
-
]);
|
|
2345
|
+
], true);
|
|
1461
2346
|
if (candidateResult.ok) {
|
|
1462
2347
|
hudResult = candidateResult;
|
|
1463
2348
|
break;
|
|
@@ -1465,17 +2350,87 @@ export function restoreStandaloneHudPane(leaderPaneId, cwd, options = {}) {
|
|
|
1465
2350
|
}
|
|
1466
2351
|
if (!hudResult?.ok)
|
|
1467
2352
|
return null;
|
|
1468
|
-
const paneId = hudResult.stdout
|
|
1469
|
-
if (!paneId
|
|
1470
|
-
|
|
2353
|
+
const paneId = parseSplitWindowPaneId(hudResult.stdout);
|
|
2354
|
+
if (!paneId) {
|
|
2355
|
+
const afterTopology = listPanesResult(normalizedLeaderPaneId);
|
|
2356
|
+
if (afterTopology.error) {
|
|
2357
|
+
throw new Error('restored_hud_split_topology_reconciliation_failed');
|
|
2358
|
+
}
|
|
2359
|
+
const newlyObservedPaneIds = afterTopology.panes
|
|
2360
|
+
.filter((pane) => !beforeSplitPaneIds.has(pane.paneId) && pane.startCommand.includes(splitReceipt))
|
|
2361
|
+
.map((pane) => pane.paneId);
|
|
2362
|
+
if (newlyObservedPaneIds.length === 1) {
|
|
2363
|
+
const reconciledPaneId = newlyObservedPaneIds[0];
|
|
2364
|
+
persistRestoredHudCleanupDebtSync(cwd, {
|
|
2365
|
+
schema_version: 1,
|
|
2366
|
+
operation: 'restored_hud_cleanup',
|
|
2367
|
+
pane_id: reconciledPaneId,
|
|
2368
|
+
pane_pid: null,
|
|
2369
|
+
leader_pane_id: normalizedLeaderPaneId,
|
|
2370
|
+
leader_pane_pid: options.expectedLeaderPanePid ?? leaderPanePid,
|
|
2371
|
+
leader_pane_owner_id: options.expectedLeaderPaneOwnerId?.trim() || null,
|
|
2372
|
+
hud_owner_leader_pane_id: normalizedLeaderPaneId,
|
|
2373
|
+
}, options.stateRoot);
|
|
2374
|
+
const reconciledProof = readExactPaneProofSync(reconciledPaneId);
|
|
2375
|
+
if (reconciledProof.status === 'unavailable')
|
|
2376
|
+
throw new ExactPaneProofUnavailableError(reconciledProof);
|
|
2377
|
+
if (reconciledProof.status === 'gone')
|
|
2378
|
+
throw new Error(`tmux pane is not proven live: ${reconciledPaneId}`);
|
|
2379
|
+
persistRestoredHudCleanupDebtSync(cwd, {
|
|
2380
|
+
schema_version: 1,
|
|
2381
|
+
operation: 'restored_hud_cleanup',
|
|
2382
|
+
pane_id: reconciledPaneId,
|
|
2383
|
+
pane_pid: reconciledProof.pid,
|
|
2384
|
+
leader_pane_id: normalizedLeaderPaneId,
|
|
2385
|
+
leader_pane_pid: options.expectedLeaderPanePid ?? leaderPanePid,
|
|
2386
|
+
leader_pane_owner_id: options.expectedLeaderPaneOwnerId?.trim() || null,
|
|
2387
|
+
hud_owner_leader_pane_id: normalizedLeaderPaneId,
|
|
2388
|
+
}, options.stateRoot);
|
|
2389
|
+
}
|
|
2390
|
+
throw new Error('restored_hud_split_output_ambiguous');
|
|
2391
|
+
}
|
|
2392
|
+
persistRestoredHudCleanupDebtSync(cwd, {
|
|
2393
|
+
schema_version: 1,
|
|
2394
|
+
operation: 'restored_hud_cleanup',
|
|
2395
|
+
pane_id: paneId,
|
|
2396
|
+
pane_pid: null,
|
|
2397
|
+
leader_pane_id: normalizedLeaderPaneId,
|
|
2398
|
+
leader_pane_pid: options.expectedLeaderPanePid ?? leaderPanePid,
|
|
2399
|
+
leader_pane_owner_id: options.expectedLeaderPaneOwnerId?.trim() || null,
|
|
2400
|
+
hud_owner_leader_pane_id: normalizedLeaderPaneId,
|
|
2401
|
+
}, options.stateRoot);
|
|
2402
|
+
const hudPanePid = (() => {
|
|
2403
|
+
const proof = readExactPaneProofSync(paneId);
|
|
2404
|
+
if (proof.status === 'unavailable')
|
|
2405
|
+
throw new ExactPaneProofUnavailableError(proof);
|
|
2406
|
+
if (proof.status === 'gone')
|
|
2407
|
+
throw new Error(`tmux pane is not proven live: ${paneId}`);
|
|
2408
|
+
return proof.pid;
|
|
2409
|
+
})();
|
|
2410
|
+
persistRestoredHudCleanupDebtSync(cwd, {
|
|
2411
|
+
schema_version: 1,
|
|
2412
|
+
operation: 'restored_hud_cleanup',
|
|
2413
|
+
pane_id: paneId,
|
|
2414
|
+
pane_pid: hudPanePid,
|
|
2415
|
+
leader_pane_id: normalizedLeaderPaneId,
|
|
2416
|
+
leader_pane_pid: options.expectedLeaderPanePid ?? leaderPanePid,
|
|
2417
|
+
leader_pane_owner_id: options.expectedLeaderPaneOwnerId?.trim() || null,
|
|
2418
|
+
hud_owner_leader_pane_id: normalizedLeaderPaneId,
|
|
2419
|
+
}, options.stateRoot);
|
|
1471
2420
|
if (isNativeWindows()) {
|
|
1472
|
-
|
|
2421
|
+
const exactHudPaneId = requireLiveExactPaneSync(paneId, hudPanePid);
|
|
2422
|
+
requireAuthorizedLeaderPane();
|
|
2423
|
+
const reconcile = runTmux(buildHudResizeArgs(exactHudPaneId));
|
|
2424
|
+
if (!reconcile.ok)
|
|
2425
|
+
throw new Error(`failed to reconcile standalone HUD resize: ${reconcile.stderr}`);
|
|
1473
2426
|
}
|
|
1474
2427
|
else {
|
|
1475
|
-
|
|
1476
|
-
runTmux(
|
|
2428
|
+
requireAuthorizedLeaderPane();
|
|
2429
|
+
runTmux(buildScheduleDelayedHudResizeArgs(paneId, HUD_RESIZE_RECONCILE_DELAY_SECONDS, HUD_TMUX_TEAM_HEIGHT_LINES, hudPanePid));
|
|
2430
|
+
requireAuthorizedLeaderPane();
|
|
2431
|
+
runTmux(buildReconcileHudResizeArgs(paneId, HUD_TMUX_TEAM_HEIGHT_LINES, hudPanePid));
|
|
1477
2432
|
}
|
|
1478
|
-
runTmux(['select-pane', '-t',
|
|
2433
|
+
runTmux(['select-pane', '-t', requireAuthorizedLeaderPane()]);
|
|
1479
2434
|
return paneId;
|
|
1480
2435
|
}
|
|
1481
2436
|
/**
|
|
@@ -1487,21 +2442,21 @@ export function restoreStandaloneHudPane(leaderPaneId, cwd, options = {}) {
|
|
|
1487
2442
|
* oh-my-tmux, or other sessions. Returns true if the session mouse option
|
|
1488
2443
|
* was set successfully, false otherwise.
|
|
1489
2444
|
*/
|
|
1490
|
-
export function enableMouseScrolling(sessionTarget) {
|
|
2445
|
+
export function enableMouseScrolling(sessionTarget, beforeEffect) {
|
|
2446
|
+
beforeEffect?.();
|
|
1491
2447
|
const result = runTmux(['set-option', '-t', sessionTarget, 'mouse', 'on']);
|
|
1492
2448
|
if (!result.ok)
|
|
1493
2449
|
return false;
|
|
1494
2450
|
// Enable OSC 52 so copy-selection-and-cancel propagates selected text to
|
|
1495
2451
|
// the terminal's clipboard without requiring xclip or pbcopy. (closes #206)
|
|
2452
|
+
beforeEffect?.();
|
|
1496
2453
|
runTmux(['set-option', '-t', sessionTarget, 'set-clipboard', 'on']);
|
|
1497
2454
|
// Mouse selection enters tmux copy-mode. Keep the mitigation session-scoped
|
|
1498
2455
|
// so OMX does not mutate users' global tmux style defaults. (issue #1448)
|
|
1499
|
-
mitigateCopyModeUnderlineArtifacts(sessionTarget);
|
|
2456
|
+
mitigateCopyModeUnderlineArtifacts(sessionTarget, beforeEffect);
|
|
1500
2457
|
return true;
|
|
1501
2458
|
}
|
|
1502
|
-
function paneTarget(sessionName, workerIndex
|
|
1503
|
-
if (workerPaneId && workerPaneId.startsWith('%'))
|
|
1504
|
-
return workerPaneId;
|
|
2459
|
+
function paneTarget(sessionName, workerIndex) {
|
|
1505
2460
|
if (sessionName.includes(':')) {
|
|
1506
2461
|
return `${sessionName}.${workerIndex}`;
|
|
1507
2462
|
}
|
|
@@ -1531,7 +2486,10 @@ function paneHasClaudeBypassPermissionsPrompt(captured) {
|
|
|
1531
2486
|
&& tail.some((line) => /Enter\s*to\s*confirm/i.test(line));
|
|
1532
2487
|
return hasWarning && hasChoices;
|
|
1533
2488
|
}
|
|
1534
|
-
export function evaluateStartupDirectTriggerSafetyCapture(captured, workerCli) {
|
|
2489
|
+
export function evaluateStartupDirectTriggerSafetyCapture(captured, workerCli, finalArgs) {
|
|
2490
|
+
const incompatibleReason = classifyCodexBypassMdmIncompatibility(captured, workerCli, finalArgs);
|
|
2491
|
+
if (incompatibleReason)
|
|
2492
|
+
return { safe: false, reason: incompatibleReason };
|
|
1535
2493
|
if (paneHasTrustPrompt(captured))
|
|
1536
2494
|
return { safe: false, reason: 'trust_prompt' };
|
|
1537
2495
|
if (paneHasClaudeBypassPermissionsPrompt(captured))
|
|
@@ -1544,27 +2502,53 @@ export function evaluateStartupDirectTriggerSafetyCapture(captured, workerCli) {
|
|
|
1544
2502
|
return { safe: true, reason: 'codex_viewport' };
|
|
1545
2503
|
return { safe: false, reason: 'not_agent_viewport' };
|
|
1546
2504
|
}
|
|
1547
|
-
export async function evaluateStartupDirectTriggerSafety(sessionName, workerIndex, workerPaneId, workerCli) {
|
|
2505
|
+
export async function evaluateStartupDirectTriggerSafety(sessionName, workerIndex, workerPaneId, workerCli, finalArgs) {
|
|
1548
2506
|
if (!isTmuxAvailable())
|
|
1549
2507
|
return { safe: false, reason: 'tmux_unavailable' };
|
|
1550
|
-
const target =
|
|
2508
|
+
const target = await resolveWorkerPaneTargetAsync(sessionName, workerIndex, workerPaneId);
|
|
2509
|
+
if (!target)
|
|
2510
|
+
return { safe: false, reason: 'capture_failed' };
|
|
1551
2511
|
const result = await runTmuxAsync(sharedBuildVisibleCapturePaneArgv(target));
|
|
1552
2512
|
if (!result.ok)
|
|
1553
2513
|
return { safe: false, reason: 'capture_failed' };
|
|
1554
|
-
return evaluateStartupDirectTriggerSafetyCapture(result.stdout, workerCli);
|
|
2514
|
+
return evaluateStartupDirectTriggerSafetyCapture(result.stdout, workerCli, finalArgs);
|
|
1555
2515
|
}
|
|
1556
|
-
function acceptClaudeBypassPermissionsPrompt(
|
|
1557
|
-
|
|
2516
|
+
function acceptClaudeBypassPermissionsPrompt(resolveTarget) {
|
|
2517
|
+
const literalTarget = resolveTarget();
|
|
2518
|
+
if (!literalTarget)
|
|
2519
|
+
return false;
|
|
2520
|
+
runTmux(['send-keys', '-t', literalTarget, '-l', '--', '2']);
|
|
1558
2521
|
sleepFractionalSeconds(0.12);
|
|
1559
|
-
|
|
2522
|
+
const submitTarget = resolveTarget();
|
|
2523
|
+
if (!submitTarget)
|
|
2524
|
+
return false;
|
|
2525
|
+
runTmux(['send-keys', '-t', submitTarget, 'C-m']);
|
|
2526
|
+
return true;
|
|
1560
2527
|
}
|
|
1561
|
-
function dismissClaudeBypassPermissionsPromptIfPresent(
|
|
2528
|
+
function dismissClaudeBypassPermissionsPromptIfPresent(resolveTarget, captured) {
|
|
1562
2529
|
if (process.env.OMX_TEAM_AUTO_ACCEPT_BYPASS === '0')
|
|
1563
2530
|
return false;
|
|
1564
2531
|
if (!paneHasClaudeBypassPermissionsPrompt(captured))
|
|
1565
2532
|
return false;
|
|
1566
|
-
acceptClaudeBypassPermissionsPrompt(
|
|
1567
|
-
|
|
2533
|
+
return acceptClaudeBypassPermissionsPrompt(resolveTarget);
|
|
2534
|
+
}
|
|
2535
|
+
async function dismissClaudeBypassPermissionsPromptIfPresentAsync(resolveTarget, captured) {
|
|
2536
|
+
if (process.env.OMX_TEAM_AUTO_ACCEPT_BYPASS === '0')
|
|
2537
|
+
return false;
|
|
2538
|
+
if (!paneHasClaudeBypassPermissionsPrompt(captured))
|
|
2539
|
+
return false;
|
|
2540
|
+
const literalTarget = await resolveTarget();
|
|
2541
|
+
if (!literalTarget)
|
|
2542
|
+
return false;
|
|
2543
|
+
const literalSend = await runTmuxAsync(['send-keys', '-t', literalTarget, '-l', '--', '2']);
|
|
2544
|
+
if (!literalSend.ok)
|
|
2545
|
+
return false;
|
|
2546
|
+
await sleep(120);
|
|
2547
|
+
const submitTarget = await resolveTarget();
|
|
2548
|
+
if (!submitTarget)
|
|
2549
|
+
return false;
|
|
2550
|
+
const submitSend = await runTmuxAsync(['send-keys', '-t', submitTarget, 'C-m']);
|
|
2551
|
+
return submitSend.ok;
|
|
1568
2552
|
}
|
|
1569
2553
|
export const paneHasActiveTask = sharedPaneHasActiveTask;
|
|
1570
2554
|
export function classifyWorkerStartupInjectSafety(captured) {
|
|
@@ -1581,8 +2565,8 @@ export function classifyWorkerStartupInjectSafety(captured) {
|
|
|
1581
2565
|
return 'safe';
|
|
1582
2566
|
}
|
|
1583
2567
|
export async function checkWorkerStartupInjectSafety(sessionName, workerIndex, workerPaneId) {
|
|
1584
|
-
const
|
|
1585
|
-
const visibleCapture = await captureVisiblePaneAsync(
|
|
2568
|
+
const resolveTarget = () => resolveWorkerPaneTargetAsync(sessionName, workerIndex, workerPaneId);
|
|
2569
|
+
const visibleCapture = await captureVisiblePaneAsync(resolveTarget);
|
|
1586
2570
|
const visibleSafety = classifyWorkerStartupInjectSafety(visibleCapture);
|
|
1587
2571
|
if (visibleSafety === 'safe')
|
|
1588
2572
|
return { safe: true, reason: 'safe' };
|
|
@@ -1591,7 +2575,7 @@ export async function checkWorkerStartupInjectSafety(sessionName, workerIndex, w
|
|
|
1591
2575
|
if (!sharedPaneShowsCodexViewport(visibleCapture)) {
|
|
1592
2576
|
return { safe: false, reason: visibleSafety };
|
|
1593
2577
|
}
|
|
1594
|
-
const scrollbackCapture = await capturePaneAsync(
|
|
2578
|
+
const scrollbackCapture = await capturePaneAsync(resolveTarget);
|
|
1595
2579
|
const scrollbackSafety = classifyWorkerStartupInjectSafety(scrollbackCapture);
|
|
1596
2580
|
return scrollbackSafety === 'safe'
|
|
1597
2581
|
? { safe: true, reason: 'safe' }
|
|
@@ -1669,8 +2653,9 @@ export function shouldAttemptAdaptiveRetry(strategy, paneBusyAtStart, allowAdapt
|
|
|
1669
2653
|
return false;
|
|
1670
2654
|
return true;
|
|
1671
2655
|
}
|
|
1672
|
-
function sendLiteralTextOrThrow(
|
|
1673
|
-
const
|
|
2656
|
+
async function sendLiteralTextOrThrow(resolveTarget, text) {
|
|
2657
|
+
const target = await requireAsyncPaneTarget(resolveTarget);
|
|
2658
|
+
const send = await runTmuxAsync(['send-keys', '-t', target, '-l', '--', text]);
|
|
1674
2659
|
if (!send.ok) {
|
|
1675
2660
|
throw new Error(`sendToWorker: failed to send text: ${send.stderr}`);
|
|
1676
2661
|
}
|
|
@@ -1682,18 +2667,18 @@ function paneHasQueuedCodexSubmission(captured) {
|
|
|
1682
2667
|
return /messages to be submitted after next tool call/i.test(normalized)
|
|
1683
2668
|
|| /press esc to interrupt and send immediately/i.test(normalized);
|
|
1684
2669
|
}
|
|
1685
|
-
async function attemptSubmitRounds(
|
|
2670
|
+
async function attemptSubmitRounds(resolveTarget, text, rounds, queueFirstRound, submitKeyPressesPerRound) {
|
|
1686
2671
|
const presses = Math.max(1, Math.floor(submitKeyPressesPerRound));
|
|
1687
2672
|
for (let round = 0; round < rounds; round++) {
|
|
1688
2673
|
await sleep(100);
|
|
1689
2674
|
if (round === 0 && queueFirstRound) {
|
|
1690
|
-
await sendKeyAsync(
|
|
2675
|
+
await sendKeyAsync(resolveTarget, 'Tab');
|
|
1691
2676
|
await sleep(80);
|
|
1692
|
-
await sendKeyAsync(
|
|
2677
|
+
await sendKeyAsync(resolveTarget, 'C-m');
|
|
1693
2678
|
}
|
|
1694
2679
|
else {
|
|
1695
2680
|
for (let press = 0; press < presses; press++) {
|
|
1696
|
-
await sendKeyAsync(
|
|
2681
|
+
await sendKeyAsync(resolveTarget, 'C-m');
|
|
1697
2682
|
if (press < presses - 1) {
|
|
1698
2683
|
await sleep(200);
|
|
1699
2684
|
}
|
|
@@ -1701,8 +2686,8 @@ async function attemptSubmitRounds(target, text, rounds, queueFirstRound, submit
|
|
|
1701
2686
|
}
|
|
1702
2687
|
await sleep(140);
|
|
1703
2688
|
const [captured, visibleCapture] = await Promise.all([
|
|
1704
|
-
capturePaneAsync(
|
|
1705
|
-
captureVisiblePaneAsync(
|
|
2689
|
+
capturePaneAsync(resolveTarget),
|
|
2690
|
+
captureVisiblePaneAsync(resolveTarget),
|
|
1706
2691
|
]);
|
|
1707
2692
|
const normalizedCapture = normalizeWorkerTriggerForDraftMatch(captured);
|
|
1708
2693
|
if (!normalizedCapture.includes(normalizeWorkerTriggerForDraftMatch(text))
|
|
@@ -1713,26 +2698,34 @@ async function attemptSubmitRounds(target, text, rounds, queueFirstRound, submit
|
|
|
1713
2698
|
}
|
|
1714
2699
|
return false;
|
|
1715
2700
|
}
|
|
1716
|
-
export function waitForWorkerReady(sessionName, workerIndex, timeoutMs = 30_000, workerPaneId) {
|
|
2701
|
+
export function waitForWorkerReady(sessionName, workerIndex, timeoutMs = 30_000, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId) {
|
|
1717
2702
|
const initialBackoffMs = 150;
|
|
1718
2703
|
const maxBackoffMs = 8000;
|
|
1719
2704
|
const startedAt = Date.now();
|
|
1720
2705
|
let blockedByTrustPrompt = false;
|
|
1721
2706
|
let promptDismissed = false;
|
|
2707
|
+
const resolveTarget = createPinnedWorkerPaneTargetResolverSync(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId);
|
|
1722
2708
|
const sendRobustEnter = () => {
|
|
1723
|
-
const target = paneTarget(sessionName, workerIndex, workerPaneId);
|
|
1724
2709
|
// Trust + follow-up splash can require two submits in Codex TUI.
|
|
1725
2710
|
// Use C-m (carriage return) for raw-mode compatibility.
|
|
1726
|
-
|
|
2711
|
+
const firstTarget = resolveTarget();
|
|
2712
|
+
if (!firstTarget)
|
|
2713
|
+
return;
|
|
2714
|
+
runTmux(['send-keys', '-t', firstTarget, 'C-m']);
|
|
1727
2715
|
sleepFractionalSeconds(0.12);
|
|
1728
|
-
|
|
2716
|
+
const secondTarget = resolveTarget();
|
|
2717
|
+
if (!secondTarget)
|
|
2718
|
+
return;
|
|
2719
|
+
runTmux(['send-keys', '-t', secondTarget, 'C-m']);
|
|
1729
2720
|
};
|
|
1730
2721
|
const check = () => {
|
|
1731
|
-
const target =
|
|
2722
|
+
const target = resolveTarget();
|
|
2723
|
+
if (!target)
|
|
2724
|
+
return false;
|
|
1732
2725
|
const result = runTmux(sharedBuildVisibleCapturePaneArgv(target));
|
|
1733
2726
|
if (!result.ok)
|
|
1734
2727
|
return false;
|
|
1735
|
-
if (dismissClaudeBypassPermissionsPromptIfPresent(
|
|
2728
|
+
if (dismissClaudeBypassPermissionsPromptIfPresent(resolveTarget, result.stdout)) {
|
|
1736
2729
|
promptDismissed = true;
|
|
1737
2730
|
return false;
|
|
1738
2731
|
}
|
|
@@ -1757,7 +2750,10 @@ export function waitForWorkerReady(sessionName, workerIndex, timeoutMs = 30_000,
|
|
|
1757
2750
|
// scrollback for the prompt/helper text that may have slipped below the fold.
|
|
1758
2751
|
if (!sharedPaneShowsCodexViewport(result.stdout))
|
|
1759
2752
|
return false;
|
|
1760
|
-
const
|
|
2753
|
+
const scrollbackTarget = resolveTarget();
|
|
2754
|
+
if (!scrollbackTarget)
|
|
2755
|
+
return false;
|
|
2756
|
+
const scrollbackResult = runTmux(sharedBuildCapturePaneArgv(scrollbackTarget, 80));
|
|
1761
2757
|
if (!scrollbackResult.ok)
|
|
1762
2758
|
return false;
|
|
1763
2759
|
return paneLooksReady(scrollbackResult.stdout);
|
|
@@ -1785,26 +2781,41 @@ export function waitForWorkerReady(sessionName, workerIndex, timeoutMs = 30_000,
|
|
|
1785
2781
|
// Async twin of waitForWorkerReady for team startup fan-out. Keep the readiness
|
|
1786
2782
|
// semantics mirrored with the synchronous helper above, but yield between polls
|
|
1787
2783
|
// so one slow worker pane cannot block later workers' startup attempts.
|
|
1788
|
-
export async function
|
|
2784
|
+
export async function waitForWorkerReadyDetailedAsync(sessionName, workerIndex, timeoutMs = 30_000, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId, incompatibility) {
|
|
1789
2785
|
const initialBackoffMs = 150;
|
|
1790
2786
|
const maxBackoffMs = 8000;
|
|
1791
2787
|
const startedAt = Date.now();
|
|
1792
2788
|
let blockedByTrustPrompt = false;
|
|
1793
2789
|
let promptDismissed = false;
|
|
2790
|
+
let startupIncompatible = false;
|
|
2791
|
+
const resolveTarget = createPinnedWorkerPaneTargetResolver(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId);
|
|
1794
2792
|
const sendRobustEnter = async () => {
|
|
1795
|
-
const target = paneTarget(sessionName, workerIndex, workerPaneId);
|
|
1796
2793
|
// Trust + follow-up splash can require two submits in Codex TUI.
|
|
1797
2794
|
// Use C-m (carriage return) for raw-mode compatibility.
|
|
1798
|
-
|
|
2795
|
+
const firstTarget = await resolveTarget();
|
|
2796
|
+
if (!firstTarget)
|
|
2797
|
+
return;
|
|
2798
|
+
await runTmuxAsync(['send-keys', '-t', firstTarget, 'C-m']);
|
|
1799
2799
|
await sleep(120);
|
|
1800
|
-
|
|
2800
|
+
const secondTarget = await resolveTarget();
|
|
2801
|
+
if (!secondTarget)
|
|
2802
|
+
return;
|
|
2803
|
+
await runTmuxAsync(['send-keys', '-t', secondTarget, 'C-m']);
|
|
1801
2804
|
};
|
|
1802
2805
|
const check = async () => {
|
|
1803
|
-
const target =
|
|
2806
|
+
const target = await resolveTarget();
|
|
2807
|
+
if (!target)
|
|
2808
|
+
return false;
|
|
1804
2809
|
const result = await runTmuxAsync(sharedBuildVisibleCapturePaneArgv(target));
|
|
1805
2810
|
if (!result.ok)
|
|
1806
2811
|
return false;
|
|
1807
|
-
|
|
2812
|
+
const incompatibleReason = classifyCodexBypassMdmIncompatibility(result.stdout, incompatibility?.workerCli, incompatibility?.finalArgs);
|
|
2813
|
+
if (incompatibleReason) {
|
|
2814
|
+
startupIncompatible = true;
|
|
2815
|
+
incompatibility?.onIncompatible(incompatibleReason);
|
|
2816
|
+
return false;
|
|
2817
|
+
}
|
|
2818
|
+
if (await dismissClaudeBypassPermissionsPromptIfPresentAsync(resolveTarget, result.stdout)) {
|
|
1808
2819
|
promptDismissed = true;
|
|
1809
2820
|
return false;
|
|
1810
2821
|
}
|
|
@@ -1829,7 +2840,10 @@ export async function waitForWorkerReadyAsync(sessionName, workerIndex, timeoutM
|
|
|
1829
2840
|
// scrollback for the prompt/helper text that may have slipped below the fold.
|
|
1830
2841
|
if (!sharedPaneShowsCodexViewport(result.stdout))
|
|
1831
2842
|
return false;
|
|
1832
|
-
const
|
|
2843
|
+
const scrollbackTarget = await resolveTarget();
|
|
2844
|
+
if (!scrollbackTarget)
|
|
2845
|
+
return false;
|
|
2846
|
+
const scrollbackResult = await runTmuxAsync(sharedBuildCapturePaneArgv(scrollbackTarget, 80));
|
|
1833
2847
|
if (!scrollbackResult.ok)
|
|
1834
2848
|
return false;
|
|
1835
2849
|
return paneLooksReady(scrollbackResult.stdout);
|
|
@@ -1838,6 +2852,8 @@ export async function waitForWorkerReadyAsync(sessionName, workerIndex, timeoutM
|
|
|
1838
2852
|
while (Date.now() - startedAt < timeoutMs) {
|
|
1839
2853
|
if (await check())
|
|
1840
2854
|
return true;
|
|
2855
|
+
if (startupIncompatible)
|
|
2856
|
+
return false;
|
|
1841
2857
|
if (blockedByTrustPrompt)
|
|
1842
2858
|
return false;
|
|
1843
2859
|
// After dismissing a trust prompt, reset backoff so we re-check quickly
|
|
@@ -1854,26 +2870,38 @@ export async function waitForWorkerReadyAsync(sessionName, workerIndex, timeoutM
|
|
|
1854
2870
|
}
|
|
1855
2871
|
return false;
|
|
1856
2872
|
}
|
|
2873
|
+
export async function waitForWorkerReadyAsync(sessionName, workerIndex, timeoutMs = 30_000, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId) {
|
|
2874
|
+
return waitForWorkerReadyDetailedAsync(sessionName, workerIndex, timeoutMs, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId);
|
|
2875
|
+
}
|
|
1857
2876
|
/**
|
|
1858
2877
|
* Detect and auto-dismiss a Codex "Trust this directory?" prompt in a worker pane.
|
|
1859
2878
|
* Returns true if a trust prompt was found and dismissed, false otherwise.
|
|
1860
2879
|
* Opt-out: set OMX_TEAM_AUTO_TRUST=0 to disable auto-dismissal.
|
|
1861
2880
|
*/
|
|
1862
|
-
export function dismissTrustPromptIfPresent(sessionName, workerIndex, workerPaneId) {
|
|
2881
|
+
export function dismissTrustPromptIfPresent(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId) {
|
|
1863
2882
|
if (process.env.OMX_TEAM_AUTO_TRUST === '0')
|
|
1864
2883
|
return false;
|
|
1865
2884
|
if (!isTmuxAvailable())
|
|
1866
2885
|
return false;
|
|
1867
|
-
const
|
|
1868
|
-
const
|
|
2886
|
+
const resolveTarget = createPinnedWorkerPaneTargetResolverSync(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId);
|
|
2887
|
+
const captureTarget = resolveTarget();
|
|
2888
|
+
if (!captureTarget)
|
|
2889
|
+
return false;
|
|
2890
|
+
const result = runTmux(sharedBuildVisibleCapturePaneArgv(captureTarget));
|
|
1869
2891
|
if (!result.ok)
|
|
1870
2892
|
return false;
|
|
1871
2893
|
if (!paneHasTrustPrompt(result.stdout))
|
|
1872
2894
|
return false;
|
|
1873
2895
|
// Trust prompt detected; send C-m twice to dismiss (trust + follow-up splash)
|
|
1874
|
-
|
|
2896
|
+
const firstTarget = resolveTarget();
|
|
2897
|
+
if (!firstTarget)
|
|
2898
|
+
return false;
|
|
2899
|
+
runTmux(['send-keys', '-t', firstTarget, 'C-m']);
|
|
1875
2900
|
sleepFractionalSeconds(0.12);
|
|
1876
|
-
|
|
2901
|
+
const secondTarget = resolveTarget();
|
|
2902
|
+
if (!secondTarget)
|
|
2903
|
+
return false;
|
|
2904
|
+
runTmux(['send-keys', '-t', secondTarget, 'C-m']);
|
|
1877
2905
|
return true;
|
|
1878
2906
|
}
|
|
1879
2907
|
export const normalizeTmuxCapture = sharedNormalizeTmuxCapture;
|
|
@@ -1904,49 +2932,49 @@ export function sendToWorkerStdin(stdin, text) {
|
|
|
1904
2932
|
// Send SHORT text (<200 chars) to worker via tmux send-keys
|
|
1905
2933
|
// Validates: text < 200 chars, no injection marker
|
|
1906
2934
|
// Throws on violation
|
|
1907
|
-
export async function sendToWorker(sessionName, workerIndex, text, workerPaneId, workerCli) {
|
|
2935
|
+
export async function sendToWorker(sessionName, workerIndex, text, workerPaneId, workerCli, expectedPanePid, expectedTeamOwnerId, hudPaneId) {
|
|
1908
2936
|
assertWorkerTriggerText(text);
|
|
1909
|
-
const
|
|
2937
|
+
const resolveTarget = createPinnedWorkerPaneTargetResolver(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId);
|
|
1910
2938
|
const strategy = resolveSendStrategyFromEnv();
|
|
1911
2939
|
const resolvedWorkerCli = resolveWorkerCliForSend(workerIndex, workerCli);
|
|
1912
2940
|
// Guard: if the trust prompt is still present, advance it first so our trigger text
|
|
1913
2941
|
// doesn't get typed into the trust screen and ignored.
|
|
1914
|
-
const capturedStr = await capturePaneAsync(
|
|
2942
|
+
const capturedStr = await capturePaneAsync(resolveTarget);
|
|
1915
2943
|
const paneBusy = paneHasActiveTask(capturedStr);
|
|
1916
|
-
if (
|
|
2944
|
+
if (await dismissClaudeBypassPermissionsPromptIfPresentAsync(resolveTarget, capturedStr)) {
|
|
1917
2945
|
await sleep(200);
|
|
1918
2946
|
}
|
|
1919
2947
|
if (paneHasTrustPrompt(capturedStr)) {
|
|
1920
|
-
await sendKeyAsync(
|
|
2948
|
+
await sendKeyAsync(resolveTarget, 'C-m');
|
|
1921
2949
|
await sleep(120);
|
|
1922
|
-
await sendKeyAsync(
|
|
2950
|
+
await sendKeyAsync(resolveTarget, 'C-m');
|
|
1923
2951
|
await sleep(200);
|
|
1924
2952
|
}
|
|
1925
|
-
sendLiteralTextOrThrow(
|
|
2953
|
+
await sendLiteralTextOrThrow(resolveTarget, text);
|
|
1926
2954
|
// Allow the input buffer to settle before sending C-m
|
|
1927
2955
|
await sleep(150);
|
|
1928
2956
|
const allowAutoInterruptRetry = process.env[OMX_TEAM_AUTO_INTERRUPT_RETRY_ENV] !== '0';
|
|
1929
2957
|
const submitPlan = buildWorkerSubmitPlan(strategy, resolvedWorkerCli, paneBusy, allowAutoInterruptRetry);
|
|
1930
2958
|
if (submitPlan.shouldInterrupt) {
|
|
1931
2959
|
// Explicit interrupt mode: abort current turn first, then submit the new command.
|
|
1932
|
-
await sendKeyAsync(
|
|
2960
|
+
await sendKeyAsync(resolveTarget, 'C-c');
|
|
1933
2961
|
await sleep(100);
|
|
1934
2962
|
}
|
|
1935
2963
|
// Submit deterministically using CLI-specific plan:
|
|
1936
2964
|
// - Codex: queue-first Tab+C-m when configured/busy, then double C-m rounds.
|
|
1937
2965
|
// - Claude: direct C-m rounds only (never queue-first Tab).
|
|
1938
|
-
if (await attemptSubmitRounds(
|
|
2966
|
+
if (await attemptSubmitRounds(resolveTarget, text, submitPlan.rounds, submitPlan.queueFirstRound, submitPlan.submitKeyPressesPerRound))
|
|
1939
2967
|
return;
|
|
1940
2968
|
// Adaptive escalation for "likely unsent trigger text at ready prompt" cases:
|
|
1941
2969
|
// clear line, re-send trigger, then re-submit with deterministic C-m rounds.
|
|
1942
|
-
const latestCapture = await capturePaneAsync(
|
|
2970
|
+
const latestCapture = await capturePaneAsync(resolveTarget);
|
|
1943
2971
|
if (shouldAttemptAdaptiveRetry(strategy, paneBusy, submitPlan.allowAdaptiveRetry, latestCapture || null, text)) {
|
|
1944
2972
|
// Keep this branch non-interrupting to avoid canceling active turns on false positives.
|
|
1945
|
-
await sendKeyAsync(
|
|
2973
|
+
await sendKeyAsync(resolveTarget, 'C-u');
|
|
1946
2974
|
await sleep(80);
|
|
1947
|
-
sendLiteralTextOrThrow(
|
|
2975
|
+
await sendLiteralTextOrThrow(resolveTarget, text);
|
|
1948
2976
|
await sleep(120);
|
|
1949
|
-
if (await attemptSubmitRounds(
|
|
2977
|
+
if (await attemptSubmitRounds(resolveTarget, text, 4, false, submitPlan.submitKeyPressesPerRound))
|
|
1950
2978
|
return;
|
|
1951
2979
|
}
|
|
1952
2980
|
// Fail-open by default: Codex may keep the last submitted line visible even after executing it.
|
|
@@ -1956,15 +2984,15 @@ export async function sendToWorker(sessionName, workerIndex, text, workerPaneId,
|
|
|
1956
2984
|
throw new Error('sendToWorker: submit_failed (trigger text still visible after retries)');
|
|
1957
2985
|
}
|
|
1958
2986
|
// One last best-effort double C-m nudge, then verify.
|
|
1959
|
-
await sendKeyAsync(
|
|
2987
|
+
await sendKeyAsync(resolveTarget, 'C-m');
|
|
1960
2988
|
await sleep(120);
|
|
1961
|
-
await sendKeyAsync(
|
|
2989
|
+
await sendKeyAsync(resolveTarget, 'C-m');
|
|
1962
2990
|
// Post-submit verification: wait briefly and confirm the worker consumed the
|
|
1963
2991
|
// trigger (draft disappeared or active-task indicator appeared). Fixes #391.
|
|
1964
2992
|
await sleep(300);
|
|
1965
2993
|
const [verifyCapture, verifyVisibleCapture] = await Promise.all([
|
|
1966
|
-
capturePaneAsync(
|
|
1967
|
-
captureVisiblePaneAsync(
|
|
2994
|
+
capturePaneAsync(resolveTarget),
|
|
2995
|
+
captureVisiblePaneAsync(resolveTarget),
|
|
1968
2996
|
]);
|
|
1969
2997
|
if (verifyCapture) {
|
|
1970
2998
|
if (paneHasActiveTask(verifyCapture))
|
|
@@ -1974,14 +3002,14 @@ export async function sendToWorker(sessionName, workerIndex, text, workerPaneId,
|
|
|
1974
3002
|
return;
|
|
1975
3003
|
}
|
|
1976
3004
|
// Draft still visible and no active task — one more C-m attempt.
|
|
1977
|
-
await sendKeyAsync(
|
|
3005
|
+
await sendKeyAsync(resolveTarget, 'C-m');
|
|
1978
3006
|
await sleep(150);
|
|
1979
|
-
await sendKeyAsync(
|
|
1980
|
-
const finalVisibleCapture = await captureVisiblePaneAsync(
|
|
3007
|
+
await sendKeyAsync(resolveTarget, 'C-m');
|
|
3008
|
+
const finalVisibleCapture = await captureVisiblePaneAsync(resolveTarget);
|
|
1981
3009
|
if (paneHasQueuedCodexSubmission(finalVisibleCapture)) {
|
|
1982
3010
|
throw new Error('sendToWorker: submit_queued_after_tool_call');
|
|
1983
3011
|
}
|
|
1984
|
-
const finalCapture = await capturePaneAsync(
|
|
3012
|
+
const finalCapture = await capturePaneAsync(resolveTarget);
|
|
1985
3013
|
if (normalizeWorkerTriggerForDraftMatch(finalCapture).includes(normalizeWorkerTriggerForDraftMatch(text))
|
|
1986
3014
|
&& !paneHasActiveTask(finalCapture)
|
|
1987
3015
|
&& paneLooksReady(finalCapture)) {
|
|
@@ -2000,8 +3028,12 @@ export function notifyLeaderStatus(sessionName, message) {
|
|
|
2000
3028
|
return result.ok;
|
|
2001
3029
|
}
|
|
2002
3030
|
// Get PID of the shell process in a worker's tmux pane
|
|
2003
|
-
export function getWorkerPanePid(sessionName, workerIndex, workerPaneId) {
|
|
2004
|
-
|
|
3031
|
+
export function getWorkerPanePid(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId) {
|
|
3032
|
+
if (hasExplicitWorkerPaneId(workerPaneId)) {
|
|
3033
|
+
const proof = requireExactWorkerPaneLivenessIdentity(workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId);
|
|
3034
|
+
return proof.status === 'live' ? proof.pid : null;
|
|
3035
|
+
}
|
|
3036
|
+
const result = runTmux(['list-panes', '-t', paneTarget(sessionName, workerIndex), '-F', '#{pane_pid}']);
|
|
2005
3037
|
if (!result.ok)
|
|
2006
3038
|
return null;
|
|
2007
3039
|
const firstLine = result.stdout.split('\n')[0]?.trim();
|
|
@@ -2013,15 +3045,25 @@ export function getWorkerPanePid(sessionName, workerIndex, workerPaneId) {
|
|
|
2013
3045
|
return pid;
|
|
2014
3046
|
}
|
|
2015
3047
|
// Check if worker's tmux pane has a running process
|
|
2016
|
-
export function isWorkerAlive(sessionName, workerIndex, workerPaneId) {
|
|
2017
|
-
if (workerPaneId
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
|
|
3048
|
+
export function isWorkerAlive(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId) {
|
|
3049
|
+
if (hasExplicitWorkerPaneId(workerPaneId)) {
|
|
3050
|
+
if (hudPaneId?.trim() && workerPaneId === hudPaneId.trim()) {
|
|
3051
|
+
throw new Error(`tmux worker pane is HUD target: ${workerPaneId}`);
|
|
3052
|
+
}
|
|
3053
|
+
const proof = requireExactWorkerPaneLivenessIdentity(workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId);
|
|
3054
|
+
if (proof.status === 'unavailable')
|
|
3055
|
+
return true;
|
|
3056
|
+
if (proof.status === 'gone')
|
|
3057
|
+
return false;
|
|
3058
|
+
if (typeof expectedPanePid !== 'number' || !Number.isSafeInteger(expectedPanePid) || expectedPanePid <= 0)
|
|
3059
|
+
return true;
|
|
3060
|
+
if (typeof expectedTeamOwnerId !== 'string' || expectedTeamOwnerId.trim() === '')
|
|
3061
|
+
return true;
|
|
3062
|
+
return probeProcessLiveness(proof.pid) !== 'gone';
|
|
2021
3063
|
}
|
|
2022
3064
|
const result = runTmux([
|
|
2023
3065
|
'list-panes',
|
|
2024
|
-
'-t', paneTarget(sessionName, workerIndex
|
|
3066
|
+
'-t', paneTarget(sessionName, workerIndex),
|
|
2025
3067
|
'-F',
|
|
2026
3068
|
'#{pane_dead} #{pane_pid}',
|
|
2027
3069
|
]);
|
|
@@ -2039,23 +3081,17 @@ export function isWorkerAlive(sessionName, workerIndex, workerPaneId) {
|
|
|
2039
3081
|
return false;
|
|
2040
3082
|
if (!Number.isFinite(pid))
|
|
2041
3083
|
return false;
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
}
|
|
2046
|
-
catch {
|
|
2047
|
-
return false;
|
|
2048
|
-
}
|
|
3084
|
+
// Unknown is conservatively live so callers cannot classify a permission or
|
|
3085
|
+
// transient failure as stale cleanup authority.
|
|
3086
|
+
return probeProcessLiveness(pid) !== 'gone';
|
|
2049
3087
|
}
|
|
2050
|
-
export function isWorkerPaneOpen(sessionName, workerIndex, workerPaneId) {
|
|
2051
|
-
if (workerPaneId
|
|
2052
|
-
|
|
2053
|
-
if (paneStatus !== null)
|
|
2054
|
-
return paneStatus;
|
|
3088
|
+
export function isWorkerPaneOpen(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId) {
|
|
3089
|
+
if (hasExplicitWorkerPaneId(workerPaneId)) {
|
|
3090
|
+
return createPinnedWorkerPaneTargetResolverSync(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId)() !== null;
|
|
2055
3091
|
}
|
|
2056
3092
|
const result = runTmux([
|
|
2057
3093
|
'list-panes',
|
|
2058
|
-
'-t', paneTarget(sessionName, workerIndex
|
|
3094
|
+
'-t', paneTarget(sessionName, workerIndex),
|
|
2059
3095
|
'-F',
|
|
2060
3096
|
'#{pane_dead}',
|
|
2061
3097
|
]);
|
|
@@ -2068,28 +3104,69 @@ export function isWorkerPaneOpen(sessionName, workerIndex, workerPaneId) {
|
|
|
2068
3104
|
}
|
|
2069
3105
|
// Kill a specific worker: send C-c, then C-d, then kill-pane if still alive.
|
|
2070
3106
|
// leaderPaneId: when provided, the kill is skipped entirely if workerPaneId matches it.
|
|
2071
|
-
export async function killWorker(sessionName, workerIndex, workerPaneId, leaderPaneId) {
|
|
3107
|
+
export async function killWorker(sessionName, workerIndex, workerPaneId, leaderPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId) {
|
|
2072
3108
|
// Guard: never kill the leader's own pane.
|
|
2073
3109
|
if (leaderPaneId && workerPaneId === leaderPaneId)
|
|
2074
3110
|
return;
|
|
2075
|
-
|
|
3111
|
+
const resolveTarget = createPinnedWorkerPaneTargetResolver(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId);
|
|
3112
|
+
const initialTarget = await resolveTarget();
|
|
3113
|
+
if (!initialTarget)
|
|
3114
|
+
return;
|
|
3115
|
+
if (await isWorkerAliveAsync(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId, resolveTarget) !== 'alive')
|
|
3116
|
+
return;
|
|
3117
|
+
await runTmuxAsync(['send-keys', '-t', initialTarget, 'C-c']);
|
|
2076
3118
|
await sleep(1000);
|
|
2077
|
-
if (await isWorkerAliveAsync(sessionName, workerIndex, workerPaneId)) {
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
3119
|
+
if (await isWorkerAliveAsync(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId, resolveTarget) === 'alive') {
|
|
3120
|
+
const exitTarget = await resolveTarget();
|
|
3121
|
+
if (exitTarget) {
|
|
3122
|
+
await runTmuxAsync(['send-keys', '-t', exitTarget, 'C-d']);
|
|
3123
|
+
await sleep(1000);
|
|
3124
|
+
}
|
|
2083
3125
|
}
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
3126
|
+
if (await isWorkerAliveAsync(sessionName, workerIndex, workerPaneId, expectedPanePid, expectedTeamOwnerId, hudPaneId, resolveTarget) === 'alive') {
|
|
3127
|
+
const killTarget = await resolveTarget();
|
|
3128
|
+
if (!killTarget)
|
|
3129
|
+
return;
|
|
3130
|
+
const killed = await runTmuxAsync(['kill-pane', '-t', killTarget]);
|
|
3131
|
+
if (!killed.ok)
|
|
3132
|
+
throw new Error(`failed to kill tmux pane ${killTarget}: ${killed.stderr}`);
|
|
3133
|
+
const absence = await readExactPaneProof(killTarget);
|
|
3134
|
+
if (absence.status === 'unavailable')
|
|
3135
|
+
throw new ExactPaneProofUnavailableError(absence);
|
|
3136
|
+
if (absence.status !== 'gone')
|
|
3137
|
+
throw new Error(`tmux pane remains live after kill: ${killTarget}`);
|
|
3138
|
+
}
|
|
3139
|
+
}
|
|
3140
|
+
// Explicit pane targets require frozen PID, canonical Team owner, and HUD
|
|
3141
|
+
// exclusion. A successful kill is only accepted after a fresh absence proof.
|
|
3142
|
+
export function killWorkerByPaneId(workerPaneId, expectedPanePid, leaderPaneId, expectedTeamOwnerId, hudPaneId) {
|
|
3143
|
+
const expectedOwner = typeof expectedTeamOwnerId === 'string' ? expectedTeamOwnerId.trim() : '';
|
|
3144
|
+
if (!hasExplicitWorkerPaneId(workerPaneId)
|
|
3145
|
+
|| typeof expectedPanePid !== 'number'
|
|
3146
|
+
|| !Number.isSafeInteger(expectedPanePid)
|
|
3147
|
+
|| expectedPanePid <= 0
|
|
3148
|
+
|| !expectedOwner
|
|
3149
|
+
|| workerPaneId === hudPaneId)
|
|
2088
3150
|
return;
|
|
2089
|
-
// Guard: never kill the leader's own pane.
|
|
2090
3151
|
if (leaderPaneId && workerPaneId === leaderPaneId)
|
|
2091
3152
|
return;
|
|
2092
|
-
|
|
3153
|
+
const proof = readExactPaneProofSync(workerPaneId);
|
|
3154
|
+
if (proof.status !== 'live' || proof.pid !== expectedPanePid)
|
|
3155
|
+
return;
|
|
3156
|
+
const owner = readPaneTeamOwnerTagResult(proof.paneId);
|
|
3157
|
+
if (owner.status !== 'value' || owner.value !== expectedOwner)
|
|
3158
|
+
return;
|
|
3159
|
+
const finalProof = readExactPaneProofSync(proof.paneId);
|
|
3160
|
+
if (finalProof.status !== 'live' || finalProof.pid !== expectedPanePid)
|
|
3161
|
+
return;
|
|
3162
|
+
const result = runTmux(['kill-pane', '-t', finalProof.paneId]);
|
|
3163
|
+
if (!result.ok)
|
|
3164
|
+
throw new Error(`failed to kill tmux pane ${finalProof.paneId}: ${result.stderr}`);
|
|
3165
|
+
const absence = readExactPaneProofSync(finalProof.paneId);
|
|
3166
|
+
if (absence.status === 'unavailable')
|
|
3167
|
+
throw new ExactPaneProofUnavailableError(absence);
|
|
3168
|
+
if (absence.status !== 'gone')
|
|
3169
|
+
throw new Error(`tmux pane remains live after kill: ${finalProof.paneId}`);
|
|
2093
3170
|
}
|
|
2094
3171
|
export function paneHasOmxInstanceTag(paneId, instanceId) {
|
|
2095
3172
|
const normalizedPaneId = normalizePaneTarget(paneId);
|
|
@@ -2140,13 +3217,34 @@ export function readPaneTeamOwnerTagResult(paneId) {
|
|
|
2140
3217
|
return { status: 'missing' };
|
|
2141
3218
|
return { status: 'error', error: stderr || `tmux show-option exited ${result.status ?? 'unknown'}` };
|
|
2142
3219
|
}
|
|
2143
|
-
export async function killWorkerByPaneIdAsync(workerPaneId, leaderPaneId) {
|
|
2144
|
-
|
|
3220
|
+
export async function killWorkerByPaneIdAsync(workerPaneId, expectedPanePid, leaderPaneId, expectedTeamOwnerId, hudPaneId) {
|
|
3221
|
+
const expectedOwner = typeof expectedTeamOwnerId === 'string' ? expectedTeamOwnerId.trim() : '';
|
|
3222
|
+
if (!hasExplicitWorkerPaneId(workerPaneId)
|
|
3223
|
+
|| typeof expectedPanePid !== 'number'
|
|
3224
|
+
|| !Number.isSafeInteger(expectedPanePid)
|
|
3225
|
+
|| expectedPanePid <= 0
|
|
3226
|
+
|| !expectedOwner
|
|
3227
|
+
|| workerPaneId === hudPaneId)
|
|
2145
3228
|
return;
|
|
2146
|
-
// Guard: never kill the leader's own pane.
|
|
2147
3229
|
if (leaderPaneId && workerPaneId === leaderPaneId)
|
|
2148
3230
|
return;
|
|
2149
|
-
await
|
|
3231
|
+
const proof = await readExactPaneProof(workerPaneId);
|
|
3232
|
+
if (proof.status !== 'live' || proof.pid !== expectedPanePid)
|
|
3233
|
+
return;
|
|
3234
|
+
const owner = readPaneTeamOwnerTagResult(proof.paneId);
|
|
3235
|
+
if (owner.status !== 'value' || owner.value !== expectedOwner)
|
|
3236
|
+
return;
|
|
3237
|
+
const finalProof = await readExactPaneProof(proof.paneId);
|
|
3238
|
+
if (finalProof.status !== 'live' || finalProof.pid !== expectedPanePid)
|
|
3239
|
+
return;
|
|
3240
|
+
const result = await runTmuxAsync(['kill-pane', '-t', finalProof.paneId]);
|
|
3241
|
+
if (!result.ok)
|
|
3242
|
+
throw new Error(`failed to kill tmux pane ${finalProof.paneId}: ${result.stderr}`);
|
|
3243
|
+
const absence = await readExactPaneProof(finalProof.paneId);
|
|
3244
|
+
if (absence.status === 'unavailable')
|
|
3245
|
+
throw new ExactPaneProofUnavailableError(absence);
|
|
3246
|
+
if (absence.status !== 'gone')
|
|
3247
|
+
throw new Error(`tmux pane remains live after kill: ${finalProof.paneId}`);
|
|
2150
3248
|
}
|
|
2151
3249
|
function normalizePaneTarget(value) {
|
|
2152
3250
|
if (typeof value !== 'string')
|
|
@@ -2184,7 +3282,10 @@ function normalizePaneTargets(paneIds, options = {}) {
|
|
|
2184
3282
|
return { killablePaneIds, excluded };
|
|
2185
3283
|
}
|
|
2186
3284
|
export function resolveSharedSessionShutdownTopology(sessionName, preferredLeaderPaneId, teamName) {
|
|
2187
|
-
const
|
|
3285
|
+
const paneList = listPanesResult(sessionName);
|
|
3286
|
+
if (paneList.error !== null)
|
|
3287
|
+
return { status: 'unavailable', detail: paneList.error };
|
|
3288
|
+
const panes = paneList.panes;
|
|
2188
3289
|
const livePaneIds = panes
|
|
2189
3290
|
.map((pane) => normalizePaneTarget(pane.paneId))
|
|
2190
3291
|
.filter((paneId) => Boolean(paneId));
|
|
@@ -2192,6 +3293,7 @@ export function resolveSharedSessionShutdownTopology(sessionName, preferredLeade
|
|
|
2192
3293
|
if (panes.length === 0) {
|
|
2193
3294
|
return {
|
|
2194
3295
|
livePaneIds,
|
|
3296
|
+
status: 'available',
|
|
2195
3297
|
teamWorkerPaneIds: [],
|
|
2196
3298
|
leaderPaneId: fallbackLeaderPaneId,
|
|
2197
3299
|
hudPaneIds: [],
|
|
@@ -2222,6 +3324,7 @@ export function resolveSharedSessionShutdownTopology(sessionName, preferredLeade
|
|
|
2222
3324
|
: [];
|
|
2223
3325
|
return {
|
|
2224
3326
|
livePaneIds,
|
|
3327
|
+
status: 'available',
|
|
2225
3328
|
teamWorkerPaneIds: normalizedTeamWorkerPaneIds,
|
|
2226
3329
|
leaderPaneId: resolvedLeaderPaneId,
|
|
2227
3330
|
hudPaneIds,
|
|
@@ -2336,7 +3439,8 @@ function escapeRegExp(value) {
|
|
|
2336
3439
|
}
|
|
2337
3440
|
/**
|
|
2338
3441
|
* Shared pane-id-direct teardown primitive for worker pane cleanup.
|
|
2339
|
-
*
|
|
3442
|
+
* A fresh exact global proof is required before each kill; proven-gone panes
|
|
3443
|
+
* are teardown-compatible while unavailable proofs remain fail-closed.
|
|
2340
3444
|
*/
|
|
2341
3445
|
export async function teardownWorkerPanes(paneIds, options = {}) {
|
|
2342
3446
|
const { killablePaneIds, excluded } = normalizePaneTargets(paneIds, options);
|
|
@@ -2347,45 +3451,247 @@ export async function teardownWorkerPanes(paneIds, options = {}) {
|
|
|
2347
3451
|
const summary = {
|
|
2348
3452
|
attemptedPaneIds: killablePaneIds,
|
|
2349
3453
|
excluded,
|
|
3454
|
+
provenGonePaneIds: [],
|
|
3455
|
+
killedPaneIds: [],
|
|
3456
|
+
proofUnavailable: [],
|
|
2350
3457
|
kill: {
|
|
2351
|
-
attempted:
|
|
3458
|
+
attempted: 0,
|
|
2352
3459
|
succeeded: 0,
|
|
2353
3460
|
failed: 0,
|
|
3461
|
+
failedPaneIds: [],
|
|
2354
3462
|
},
|
|
2355
3463
|
};
|
|
2356
3464
|
for (const paneId of killablePaneIds) {
|
|
2357
|
-
const
|
|
2358
|
-
|
|
3465
|
+
const proof = await readExactPaneProof(paneId);
|
|
3466
|
+
const expectedPid = options.expectedPanePids?.[paneId];
|
|
3467
|
+
if (proof.status === 'gone') {
|
|
3468
|
+
summary.provenGonePaneIds.push(proof.paneId);
|
|
3469
|
+
continue;
|
|
3470
|
+
}
|
|
3471
|
+
if (proof.status === 'unavailable') {
|
|
3472
|
+
summary.proofUnavailable.push(proof);
|
|
3473
|
+
break;
|
|
3474
|
+
}
|
|
3475
|
+
if (typeof expectedPid !== 'number' || !Number.isSafeInteger(expectedPid) || expectedPid <= 0) {
|
|
3476
|
+
summary.proofUnavailable.push({
|
|
3477
|
+
status: 'unavailable',
|
|
3478
|
+
paneId,
|
|
3479
|
+
reason: 'pane_pid_changed',
|
|
3480
|
+
detail: 'expected positive pane PID is required before teardown kill',
|
|
3481
|
+
});
|
|
3482
|
+
break;
|
|
3483
|
+
}
|
|
3484
|
+
if (proof.pid !== expectedPid) {
|
|
3485
|
+
summary.proofUnavailable.push({
|
|
3486
|
+
status: 'unavailable',
|
|
3487
|
+
paneId,
|
|
3488
|
+
reason: 'pane_pid_changed',
|
|
3489
|
+
detail: `expected ${expectedPid}, got ${proof.pid}`,
|
|
3490
|
+
});
|
|
3491
|
+
break;
|
|
3492
|
+
}
|
|
3493
|
+
if (options.authorizePaneKill && !options.authorizePaneKill(proof.paneId, proof)) {
|
|
3494
|
+
summary.proofUnavailable.push({
|
|
3495
|
+
status: 'unavailable',
|
|
3496
|
+
paneId: proof.paneId,
|
|
3497
|
+
reason: 'pane_pid_changed',
|
|
3498
|
+
detail: 'pane owner authorization changed',
|
|
3499
|
+
});
|
|
3500
|
+
break;
|
|
3501
|
+
}
|
|
3502
|
+
// Owner authorization can read tmux. Re-prove the persisted identity after
|
|
3503
|
+
// it and immediately before kill-pane.
|
|
3504
|
+
const finalProof = await readExactPaneProof(proof.paneId);
|
|
3505
|
+
if (finalProof.status === 'gone') {
|
|
3506
|
+
summary.provenGonePaneIds.push(finalProof.paneId);
|
|
3507
|
+
continue;
|
|
3508
|
+
}
|
|
3509
|
+
if (finalProof.status === 'unavailable') {
|
|
3510
|
+
summary.proofUnavailable.push(finalProof);
|
|
3511
|
+
break;
|
|
3512
|
+
}
|
|
3513
|
+
if (finalProof.pid !== expectedPid) {
|
|
3514
|
+
summary.proofUnavailable.push({
|
|
3515
|
+
status: 'unavailable',
|
|
3516
|
+
paneId: finalProof.paneId,
|
|
3517
|
+
reason: 'pane_pid_changed',
|
|
3518
|
+
detail: `expected ${expectedPid}, got ${finalProof.pid}`,
|
|
3519
|
+
});
|
|
3520
|
+
break;
|
|
3521
|
+
}
|
|
3522
|
+
summary.kill.attempted += 1;
|
|
3523
|
+
const result = await runTmuxAsync(['kill-pane', '-t', finalProof.paneId]);
|
|
3524
|
+
if (!result.ok) {
|
|
3525
|
+
summary.kill.failed += 1;
|
|
3526
|
+
summary.kill.failedPaneIds.push(proof.paneId);
|
|
3527
|
+
await sleep(perPaneGrace);
|
|
3528
|
+
break;
|
|
3529
|
+
}
|
|
3530
|
+
const afterKill = await readExactPaneProof(finalProof.paneId);
|
|
3531
|
+
if (afterKill.status === 'gone') {
|
|
2359
3532
|
summary.kill.succeeded += 1;
|
|
2360
|
-
|
|
3533
|
+
summary.killedPaneIds.push(proof.paneId);
|
|
3534
|
+
}
|
|
3535
|
+
else if (afterKill.status === 'unavailable') {
|
|
3536
|
+
summary.proofUnavailable.push(afterKill);
|
|
3537
|
+
break;
|
|
3538
|
+
}
|
|
3539
|
+
else {
|
|
2361
3540
|
summary.kill.failed += 1;
|
|
3541
|
+
summary.kill.failedPaneIds.push(proof.paneId);
|
|
3542
|
+
await sleep(perPaneGrace);
|
|
3543
|
+
break;
|
|
3544
|
+
}
|
|
2362
3545
|
await sleep(perPaneGrace);
|
|
2363
3546
|
}
|
|
2364
3547
|
return summary;
|
|
2365
3548
|
}
|
|
2366
|
-
export async function killWorkerPanes(paneIds, leaderPaneId, graceMs = 2000, hudPaneId) {
|
|
2367
|
-
return teardownWorkerPanes(paneIds, {
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
}
|
|
2374
|
-
catch {
|
|
2375
|
-
// tolerate
|
|
2376
|
-
}
|
|
3549
|
+
export async function killWorkerPanes(paneIds, leaderPaneId, graceMs = 2000, hudPaneId, expectedPanePids) {
|
|
3550
|
+
return teardownWorkerPanes(paneIds, {
|
|
3551
|
+
leaderPaneId,
|
|
3552
|
+
hudPaneId: hudPaneId ?? null,
|
|
3553
|
+
graceMs,
|
|
3554
|
+
expectedPanePids,
|
|
3555
|
+
});
|
|
2377
3556
|
}
|
|
2378
|
-
//
|
|
2379
|
-
|
|
2380
|
-
|
|
2381
|
-
|
|
2382
|
-
|
|
2383
|
-
return result.stdout
|
|
3557
|
+
// Kill an entire detached tmux session only when the caller has already
|
|
3558
|
+
// established ownership. Success requires tmux to accept the kill and a fresh
|
|
3559
|
+
// session-list proof that the exact base session is absent.
|
|
3560
|
+
function parseTeamSessionNames(stdout) {
|
|
3561
|
+
return stdout
|
|
2384
3562
|
.split('\n')
|
|
2385
3563
|
.map(line => line.trim())
|
|
2386
3564
|
.filter(Boolean)
|
|
2387
3565
|
.map(baseSessionName);
|
|
2388
3566
|
}
|
|
3567
|
+
function isFinalTmuxServerGone(result) {
|
|
3568
|
+
return !result.ok && /^no server running on .+$/i.test(result.stderr);
|
|
3569
|
+
}
|
|
3570
|
+
/**
|
|
3571
|
+
* Reads pane and session incarnation in one tmux snapshot. This prevents a
|
|
3572
|
+
* same-name replacement from combining a stale session proof with a live pane.
|
|
3573
|
+
*/
|
|
3574
|
+
export function queryDetachedSessionLeaderBinding(paneId, pid, sessionName, expected) {
|
|
3575
|
+
const result = runTmuxStructured(['list-panes', '-a', '-F', '#{pane_id}\t#{pane_dead}\t#{pane_pid}\t#{session_name}\t#{session_id}\t#{session_created}']);
|
|
3576
|
+
if (!result.ok)
|
|
3577
|
+
return null;
|
|
3578
|
+
const seenPaneIds = new Set();
|
|
3579
|
+
let binding = null;
|
|
3580
|
+
for (const line of result.stdout.split('\n').filter(Boolean)) {
|
|
3581
|
+
const fields = line.split('\t');
|
|
3582
|
+
if (fields.length !== 6)
|
|
3583
|
+
return null;
|
|
3584
|
+
const [currentPaneId, dead, panePid, currentSessionName, sessionId, sessionCreated] = fields;
|
|
3585
|
+
if (!/^%[0-9]+$/.test(currentPaneId) || seenPaneIds.has(currentPaneId) || (dead !== '0' && dead !== '1')
|
|
3586
|
+
|| !/^[0-9]+$/.test(panePid) || !Number.isSafeInteger(Number(panePid)) || Number(panePid) <= 0
|
|
3587
|
+
|| !currentSessionName || !/^\$[0-9]+$/.test(sessionId) || !/^[0-9]+$/.test(sessionCreated))
|
|
3588
|
+
return null;
|
|
3589
|
+
seenPaneIds.add(currentPaneId);
|
|
3590
|
+
if (currentPaneId !== paneId)
|
|
3591
|
+
continue;
|
|
3592
|
+
if (dead !== '0' || Number(panePid) !== pid || currentSessionName !== sessionName
|
|
3593
|
+
|| sessionId !== expected.sessionId || sessionCreated !== expected.sessionCreated)
|
|
3594
|
+
return null;
|
|
3595
|
+
binding = { paneId: currentPaneId, pid: Number(panePid), sessionName: currentSessionName, incarnation: { sessionId, sessionCreated } };
|
|
3596
|
+
}
|
|
3597
|
+
return binding;
|
|
3598
|
+
}
|
|
3599
|
+
function parseDetachedSessionIncarnation(stdout, sessionName) {
|
|
3600
|
+
const baseName = baseSessionName(sessionName);
|
|
3601
|
+
const matches = [];
|
|
3602
|
+
for (const line of stdout.split('\n').filter(Boolean)) {
|
|
3603
|
+
const fields = line.split('\t');
|
|
3604
|
+
if (fields.length !== 3)
|
|
3605
|
+
return 'malformed';
|
|
3606
|
+
const [name, sessionId, sessionCreated] = fields;
|
|
3607
|
+
if (!name || !sessionId || !sessionCreated || !/^\$[0-9]+$/.test(sessionId) || !/^[0-9]+$/.test(sessionCreated))
|
|
3608
|
+
return 'malformed';
|
|
3609
|
+
if (baseSessionName(name) === baseName)
|
|
3610
|
+
matches.push({ sessionId, sessionCreated });
|
|
3611
|
+
}
|
|
3612
|
+
return matches.length === 0 ? null : matches.length === 1 ? matches[0] : 'malformed';
|
|
3613
|
+
}
|
|
3614
|
+
/**
|
|
3615
|
+
* Reads stable tmux incarnation evidence. Name-only presence is never effect
|
|
3616
|
+
* authority: malformed or unavailable evidence fails closed, and an expected
|
|
3617
|
+
* incarnation distinguishes an exact survivor from a recycled same-name session.
|
|
3618
|
+
*/
|
|
3619
|
+
export function queryDetachedTeamSession(sessionName, expected) {
|
|
3620
|
+
const sessions = runTmuxStructured(['list-sessions', '-F', '#{session_name}\t#{session_id}\t#{session_created}']);
|
|
3621
|
+
if (!sessions.ok) {
|
|
3622
|
+
return isFinalTmuxServerGone(sessions)
|
|
3623
|
+
? { status: 'absent' }
|
|
3624
|
+
: { status: 'unavailable', detail: sessions.stderr };
|
|
3625
|
+
}
|
|
3626
|
+
const incarnation = parseDetachedSessionIncarnation(sessions.stdout, sessionName);
|
|
3627
|
+
if (incarnation === 'malformed')
|
|
3628
|
+
return { status: 'unavailable', detail: 'malformed_session_incarnation' };
|
|
3629
|
+
if (incarnation === null)
|
|
3630
|
+
return { status: 'absent' };
|
|
3631
|
+
if (expected && (incarnation.sessionId !== expected.sessionId || incarnation.sessionCreated !== expected.sessionCreated)) {
|
|
3632
|
+
return { status: 'replacement', incarnation };
|
|
3633
|
+
}
|
|
3634
|
+
return { status: 'exact', incarnation };
|
|
3635
|
+
}
|
|
3636
|
+
/**
|
|
3637
|
+
* Re-proves every stable authorization component at the destructive sink. The
|
|
3638
|
+
* final tmux command queues an in-server predicate with the kill, so no client
|
|
3639
|
+
* can replace the session or pane between authorization and the effect.
|
|
3640
|
+
*/
|
|
3641
|
+
function buildDetachedSessionDestroyArgs(expected) {
|
|
3642
|
+
const leaderPaneId = expected.leaderPaneId.trim();
|
|
3643
|
+
const ownerId = expected.ownerId.trim();
|
|
3644
|
+
if (!/^%[0-9]+$/.test(leaderPaneId)
|
|
3645
|
+
|| !Number.isSafeInteger(expected.leaderPanePid)
|
|
3646
|
+
|| expected.leaderPanePid <= 0
|
|
3647
|
+
|| !/^\$[0-9]+$/.test(expected.sessionId)
|
|
3648
|
+
|| !/^[0-9]+$/.test(expected.sessionCreated)
|
|
3649
|
+
|| !/^[A-Za-z0-9._:@/-]+$/.test(ownerId))
|
|
3650
|
+
return null;
|
|
3651
|
+
const predicate = [
|
|
3652
|
+
'#{==:#{pane_dead},0}',
|
|
3653
|
+
`#{==:#{pane_id},${leaderPaneId}}`,
|
|
3654
|
+
`#{==:#{pane_pid},${expected.leaderPanePid}}`,
|
|
3655
|
+
`#{==:#{${OMX_TEAM_PANE_OWNER_OPTION}},${ownerId}}`,
|
|
3656
|
+
`#{==:#{session_id},${expected.sessionId}}`,
|
|
3657
|
+
`#{==:#{session_created},${expected.sessionCreated}}`,
|
|
3658
|
+
].reduce((combined, condition) => `#{&&:${combined},${condition}}`);
|
|
3659
|
+
return ['if-shell', '-F', '-t', leaderPaneId, predicate, `kill-session -t ${expected.sessionId}`, 'run-shell "exit 1"'];
|
|
3660
|
+
}
|
|
3661
|
+
export function requestDetachedTeamSessionDestroy(sessionName, expected) {
|
|
3662
|
+
const incarnation = { sessionId: expected.sessionId, sessionCreated: expected.sessionCreated };
|
|
3663
|
+
if (queryDetachedTeamSession(sessionName, incarnation).status !== 'exact')
|
|
3664
|
+
return false;
|
|
3665
|
+
if (!queryDetachedSessionLeaderBinding(expected.leaderPaneId, expected.leaderPanePid, sessionName, incarnation))
|
|
3666
|
+
return false;
|
|
3667
|
+
const pane = readExactPaneProofSync(expected.leaderPaneId);
|
|
3668
|
+
if (pane.status !== 'live' || pane.pid !== expected.leaderPanePid)
|
|
3669
|
+
return false;
|
|
3670
|
+
const owner = readPaneTeamOwnerTagResult(expected.leaderPaneId);
|
|
3671
|
+
if (owner.status !== 'value' || owner.value !== expected.ownerId)
|
|
3672
|
+
return false;
|
|
3673
|
+
const args = buildDetachedSessionDestroyArgs(expected);
|
|
3674
|
+
return args !== null && runTmux(args).ok;
|
|
3675
|
+
}
|
|
3676
|
+
export function destroyTeamSession(sessionName) {
|
|
3677
|
+
const result = runTmux(['kill-session', '-t', sessionName]);
|
|
3678
|
+
if (!result.ok)
|
|
3679
|
+
return false;
|
|
3680
|
+
const sessions = runTmux(['list-sessions', '-F', '#{session_name}']);
|
|
3681
|
+
// Killing tmux's final session also terminates its server. Its documented
|
|
3682
|
+
// no-server response is the only post-kill query failure that proves absence.
|
|
3683
|
+
if (!sessions.ok)
|
|
3684
|
+
return isFinalTmuxServerGone(sessions);
|
|
3685
|
+
return !parseTeamSessionNames(sessions.stdout).includes(baseSessionName(sessionName));
|
|
3686
|
+
}
|
|
3687
|
+
// A failed query is not a successful empty session list. Destructive callers
|
|
3688
|
+
// must require the latter before treating a detached session as absent.
|
|
3689
|
+
export function listTeamSessions() {
|
|
3690
|
+
const result = runTmux(['list-sessions', '-F', '#{session_name}']);
|
|
3691
|
+
if (!result.ok)
|
|
3692
|
+
return null;
|
|
3693
|
+
return parseTeamSessionNames(result.stdout);
|
|
3694
|
+
}
|
|
2389
3695
|
/**
|
|
2390
3696
|
* Notify the leader through durable mailbox state only.
|
|
2391
3697
|
*
|