oh-my-codex 0.20.2 → 0.20.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +36 -18
- package/Cargo.toml +1 -1
- package/README.md +28 -2
- package/crates/omx-runtime/Cargo.toml +4 -0
- package/crates/omx-runtime/src/main.rs +415 -2
- package/crates/omx-runtime/tests/execution.rs +483 -0
- package/crates/omx-runtime-core/src/dispatch.rs +96 -10
- package/crates/omx-runtime-core/src/engine.rs +839 -29
- package/crates/omx-runtime-core/src/lib.rs +8 -0
- package/dist/adapt/__tests__/herdr-target.test.d.ts +2 -0
- package/dist/adapt/__tests__/herdr-target.test.d.ts.map +1 -0
- package/dist/adapt/__tests__/herdr-target.test.js +72 -0
- package/dist/adapt/__tests__/herdr-target.test.js.map +1 -0
- package/dist/adapt/contracts.d.ts +1 -1
- package/dist/adapt/contracts.d.ts.map +1 -1
- package/dist/adapt/contracts.js +1 -1
- package/dist/adapt/contracts.js.map +1 -1
- package/dist/adapt/herdr/__tests__/authority.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/authority.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/authority.test.js +49 -0
- package/dist/adapt/herdr/__tests__/authority.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/bridge.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/bridge.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/bridge.test.js +176 -0
- package/dist/adapt/herdr/__tests__/bridge.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/sanitize.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/sanitize.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/sanitize.test.js +45 -0
- package/dist/adapt/herdr/__tests__/sanitize.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/semantic.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/semantic.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/semantic.test.js +130 -0
- package/dist/adapt/herdr/__tests__/semantic.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/seq-store.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/seq-store.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/seq-store.test.js +54 -0
- package/dist/adapt/herdr/__tests__/seq-store.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/socket-live.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/socket-live.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/socket-live.test.js +104 -0
- package/dist/adapt/herdr/__tests__/socket-live.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/team-map.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/team-map.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/team-map.test.js +46 -0
- package/dist/adapt/herdr/__tests__/team-map.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/transport.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/transport.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/transport.test.js +160 -0
- package/dist/adapt/herdr/__tests__/transport.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/wiring.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/wiring.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/wiring.test.js +118 -0
- package/dist/adapt/herdr/__tests__/wiring.test.js.map +1 -0
- package/dist/adapt/herdr/authority.d.ts +34 -0
- package/dist/adapt/herdr/authority.d.ts.map +1 -0
- package/dist/adapt/herdr/authority.js +71 -0
- package/dist/adapt/herdr/authority.js.map +1 -0
- package/dist/adapt/herdr/bridge.d.ts +84 -0
- package/dist/adapt/herdr/bridge.d.ts.map +1 -0
- package/dist/adapt/herdr/bridge.js +222 -0
- package/dist/adapt/herdr/bridge.js.map +1 -0
- package/dist/adapt/herdr/sanitize.d.ts +20 -0
- package/dist/adapt/herdr/sanitize.d.ts.map +1 -0
- package/dist/adapt/herdr/sanitize.js +70 -0
- package/dist/adapt/herdr/sanitize.js.map +1 -0
- package/dist/adapt/herdr/semantic.d.ts +67 -0
- package/dist/adapt/herdr/semantic.d.ts.map +1 -0
- package/dist/adapt/herdr/semantic.js +170 -0
- package/dist/adapt/herdr/semantic.js.map +1 -0
- package/dist/adapt/herdr/seq-store.d.ts +32 -0
- package/dist/adapt/herdr/seq-store.d.ts.map +1 -0
- package/dist/adapt/herdr/seq-store.js +126 -0
- package/dist/adapt/herdr/seq-store.js.map +1 -0
- package/dist/adapt/herdr/team-map.d.ts +30 -0
- package/dist/adapt/herdr/team-map.d.ts.map +1 -0
- package/dist/adapt/herdr/team-map.js +37 -0
- package/dist/adapt/herdr/team-map.js.map +1 -0
- package/dist/adapt/herdr/transport.d.ts +115 -0
- package/dist/adapt/herdr/transport.d.ts.map +1 -0
- package/dist/adapt/herdr/transport.js +260 -0
- package/dist/adapt/herdr/transport.js.map +1 -0
- package/dist/adapt/herdr/wiring.d.ts +24 -0
- package/dist/adapt/herdr/wiring.d.ts.map +1 -0
- package/dist/adapt/herdr/wiring.js +62 -0
- package/dist/adapt/herdr/wiring.js.map +1 -0
- package/dist/adapt/herdr.d.ts +19 -0
- package/dist/adapt/herdr.d.ts.map +1 -0
- package/dist/adapt/herdr.js +115 -0
- package/dist/adapt/herdr.js.map +1 -0
- package/dist/adapt/index.d.ts +4 -0
- package/dist/adapt/index.d.ts.map +1 -1
- package/dist/adapt/index.js +48 -0
- package/dist/adapt/index.js.map +1 -1
- package/dist/adapt/registry.d.ts.map +1 -1
- package/dist/adapt/registry.js +11 -0
- package/dist/adapt/registry.js.map +1 -1
- package/dist/agents/__tests__/definitions.test.js +81 -0
- package/dist/agents/__tests__/definitions.test.js.map +1 -1
- package/dist/agents/__tests__/native-config.test.js +25 -0
- package/dist/agents/__tests__/native-config.test.js.map +1 -1
- package/dist/agents/native-config.d.ts +2 -1
- package/dist/agents/native-config.d.ts.map +1 -1
- package/dist/agents/native-config.js.map +1 -1
- package/dist/auth/__tests__/hotswap.test.js +60 -1
- package/dist/auth/__tests__/hotswap.test.js.map +1 -1
- package/dist/auth/__tests__/storage.test.js +33 -1
- package/dist/auth/__tests__/storage.test.js.map +1 -1
- package/dist/auth/hotswap.d.ts +22 -2
- package/dist/auth/hotswap.d.ts.map +1 -1
- package/dist/auth/hotswap.js +107 -22
- package/dist/auth/hotswap.js.map +1 -1
- package/dist/auth/storage.js +1 -1
- package/dist/auth/storage.js.map +1 -1
- package/dist/autopilot/__tests__/ralplan-gate.test.js +86 -1333
- package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
- package/dist/autopilot/ralplan-gate.d.ts.map +1 -1
- package/dist/autopilot/ralplan-gate.js +7 -6
- package/dist/autopilot/ralplan-gate.js.map +1 -1
- package/dist/cli/__tests__/agents-init.test.js +4 -23
- package/dist/cli/__tests__/agents-init.test.js.map +1 -1
- package/dist/cli/__tests__/api.test.js +61 -24
- package/dist/cli/__tests__/api.test.js.map +1 -1
- package/dist/cli/__tests__/auth.test.js +63 -7
- package/dist/cli/__tests__/auth.test.js.map +1 -1
- package/dist/cli/__tests__/codex-feature-probe.test.js +66 -1
- package/dist/cli/__tests__/codex-feature-probe.test.js.map +1 -1
- package/dist/cli/__tests__/codex-plugin-layout.test.js +150 -2
- package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
- package/dist/cli/__tests__/detached-leader-teardown.test.d.ts +2 -0
- package/dist/cli/__tests__/detached-leader-teardown.test.d.ts.map +1 -0
- package/dist/cli/__tests__/detached-leader-teardown.test.js +473 -0
- package/dist/cli/__tests__/detached-leader-teardown.test.js.map +1 -0
- package/dist/cli/__tests__/doctor-process-identity-readiness.test.d.ts +2 -0
- package/dist/cli/__tests__/doctor-process-identity-readiness.test.d.ts.map +1 -0
- package/dist/cli/__tests__/doctor-process-identity-readiness.test.js +103 -0
- package/dist/cli/__tests__/doctor-process-identity-readiness.test.js.map +1 -0
- package/dist/cli/__tests__/doctor-state-root-binding.test.d.ts +2 -0
- package/dist/cli/__tests__/doctor-state-root-binding.test.d.ts.map +1 -0
- package/dist/cli/__tests__/doctor-state-root-binding.test.js +139 -0
- package/dist/cli/__tests__/doctor-state-root-binding.test.js.map +1 -0
- package/dist/cli/__tests__/doctor-warning-copy.test.js +135 -1
- package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
- package/dist/cli/__tests__/exec.test.js +211 -0
- package/dist/cli/__tests__/exec.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +878 -463
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/launch-fallback.test.js +1290 -127
- package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
- package/dist/cli/__tests__/mcp-parity.test.js +8 -4
- package/dist/cli/__tests__/mcp-parity.test.js.map +1 -1
- package/dist/cli/__tests__/native-assets.test.js +398 -16
- package/dist/cli/__tests__/native-assets.test.js.map +1 -1
- package/dist/cli/__tests__/native-hook-claim-journal.test.js +132 -6
- package/dist/cli/__tests__/native-hook-claim-journal.test.js.map +1 -1
- package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +3 -3
- package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -1
- package/dist/cli/__tests__/ralph.test.js +3 -3
- package/dist/cli/__tests__/ralph.test.js.map +1 -1
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js +49 -164
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js.map +1 -1
- package/dist/cli/__tests__/ralplan-child-binding-3212.test.d.ts +2 -0
- package/dist/cli/__tests__/ralplan-child-binding-3212.test.d.ts.map +1 -0
- package/dist/cli/__tests__/ralplan-child-binding-3212.test.js +56 -0
- package/dist/cli/__tests__/ralplan-child-binding-3212.test.js.map +1 -0
- package/dist/cli/__tests__/ralplan.test.js +107 -109
- package/dist/cli/__tests__/ralplan.test.js.map +1 -1
- package/dist/cli/__tests__/resume.test.js +11 -10
- package/dist/cli/__tests__/resume.test.js.map +1 -1
- package/dist/cli/__tests__/session-scoped-runtime.test.js +1118 -43
- package/dist/cli/__tests__/session-scoped-runtime.test.js.map +1 -1
- package/dist/cli/__tests__/session-search-help.test.js +25 -1
- package/dist/cli/__tests__/session-search-help.test.js.map +1 -1
- package/dist/cli/__tests__/setup-agents-overwrite.test.js +8 -55
- package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
- package/dist/cli/__tests__/setup-install-mode.test.js +224 -7
- package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
- package/dist/cli/__tests__/sparkshell-cli.test.js +93 -41
- package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
- package/dist/cli/__tests__/team.test.js +150 -8
- package/dist/cli/__tests__/team.test.js.map +1 -1
- package/dist/cli/__tests__/ultragoal.test.js +117 -2
- package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
- package/dist/cli/__tests__/uninstall.test.js +95 -4
- package/dist/cli/__tests__/uninstall.test.js.map +1 -1
- package/dist/cli/__tests__/update.test.js +669 -177
- package/dist/cli/__tests__/update.test.js.map +1 -1
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js +33 -2
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
- package/dist/cli/api.d.ts.map +1 -1
- package/dist/cli/api.js +9 -10
- package/dist/cli/api.js.map +1 -1
- package/dist/cli/codex-feature-probe.d.ts +16 -0
- package/dist/cli/codex-feature-probe.d.ts.map +1 -1
- package/dist/cli/codex-feature-probe.js +44 -0
- package/dist/cli/codex-feature-probe.js.map +1 -1
- package/dist/cli/codex-home.d.ts.map +1 -1
- package/dist/cli/codex-home.js +7 -4
- package/dist/cli/codex-home.js.map +1 -1
- package/dist/cli/doctor.d.ts +20 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +290 -3
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.d.ts +234 -12
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2409 -746
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp-parity.js +2 -2
- package/dist/cli/mcp-parity.js.map +1 -1
- package/dist/cli/native-assets.d.ts +25 -0
- package/dist/cli/native-assets.d.ts.map +1 -1
- package/dist/cli/native-assets.js +448 -51
- package/dist/cli/native-assets.js.map +1 -1
- package/dist/cli/native-hook-claim-journal.d.ts +20 -5
- package/dist/cli/native-hook-claim-journal.d.ts.map +1 -1
- package/dist/cli/native-hook-claim-journal.js +67 -32
- package/dist/cli/native-hook-claim-journal.js.map +1 -1
- package/dist/cli/package-manager-ownership.d.ts +63 -0
- package/dist/cli/package-manager-ownership.d.ts.map +1 -0
- package/dist/cli/package-manager-ownership.js +323 -0
- package/dist/cli/package-manager-ownership.js.map +1 -0
- package/dist/cli/ralph.js +2 -2
- package/dist/cli/ralph.js.map +1 -1
- package/dist/cli/ralplan.d.ts +5 -7
- package/dist/cli/ralplan.d.ts.map +1 -1
- package/dist/cli/ralplan.js +83 -134
- package/dist/cli/ralplan.js.map +1 -1
- package/dist/cli/session-search.d.ts +5 -0
- package/dist/cli/session-search.d.ts.map +1 -1
- package/dist/cli/session-search.js +87 -0
- package/dist/cli/session-search.js.map +1 -1
- package/dist/cli/setup-preferences.d.ts +15 -0
- package/dist/cli/setup-preferences.d.ts.map +1 -1
- package/dist/cli/setup-preferences.js +27 -1
- package/dist/cli/setup-preferences.js.map +1 -1
- package/dist/cli/setup.d.ts +8 -0
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +119 -47
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/sparkshell.d.ts.map +1 -1
- package/dist/cli/sparkshell.js +52 -9
- package/dist/cli/sparkshell.js.map +1 -1
- package/dist/cli/ultragoal.d.ts.map +1 -1
- package/dist/cli/ultragoal.js +2 -2
- package/dist/cli/ultragoal.js.map +1 -1
- package/dist/cli/uninstall.d.ts +9 -0
- package/dist/cli/uninstall.d.ts.map +1 -1
- package/dist/cli/uninstall.js +71 -30
- package/dist/cli/uninstall.js.map +1 -1
- package/dist/cli/update-worker.d.ts +2 -0
- package/dist/cli/update-worker.d.ts.map +1 -0
- package/dist/cli/update-worker.js +142 -0
- package/dist/cli/update-worker.js.map +1 -0
- package/dist/cli/update.d.ts +14 -22
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +192 -241
- package/dist/cli/update.js.map +1 -1
- package/dist/compat/__tests__/rust-runtime-compat.test.js +1 -1
- package/dist/config/__tests__/generator-idempotent.test.js +503 -30
- package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
- package/dist/config/__tests__/generator-notify.test.js +5 -3
- package/dist/config/__tests__/generator-notify.test.js.map +1 -1
- package/dist/config/__tests__/models.test.js +67 -3
- package/dist/config/__tests__/models.test.js.map +1 -1
- package/dist/config/generator.d.ts +27 -21
- package/dist/config/generator.d.ts.map +1 -1
- package/dist/config/generator.js +610 -152
- package/dist/config/generator.js.map +1 -1
- package/dist/config/models.d.ts +12 -2
- package/dist/config/models.d.ts.map +1 -1
- package/dist/config/models.js +15 -1
- package/dist/config/models.js.map +1 -1
- package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js +25 -0
- package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js.map +1 -1
- package/dist/goal-workflows/codex-goal-snapshot.d.ts +1 -1
- package/dist/goal-workflows/codex-goal-snapshot.d.ts.map +1 -1
- package/dist/goal-workflows/codex-goal-snapshot.js +2 -0
- package/dist/goal-workflows/codex-goal-snapshot.js.map +1 -1
- package/dist/hooks/__tests__/agents-overlay.test.js +5 -3
- package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js +27 -4
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.d.ts +2 -0
- package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.js +172 -0
- package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.js.map +1 -0
- package/dist/hooks/__tests__/keyword-detector.test.js +176 -37
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js +402 -545
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +39 -4
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +161 -26
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js +27 -0
- package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js +10 -118
- package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-regression-205.test.js +7 -0
- package/dist/hooks/__tests__/notify-hook-regression-205.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +533 -14
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +159 -24
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js +482 -32
- package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.js +2 -1
- package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +68 -1
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js +7 -1
- package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +44 -4
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/prompt-guidance-contract.test.js +9 -0
- package/dist/hooks/__tests__/prompt-guidance-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/session.test.js +3130 -365
- package/dist/hooks/__tests__/session.test.js.map +1 -1
- package/dist/hooks/agents-overlay.js +1 -1
- package/dist/hooks/agents-overlay.js.map +1 -1
- package/dist/hooks/extensibility/__tests__/dispatcher.test.js +108 -2
- package/dist/hooks/extensibility/__tests__/dispatcher.test.js.map +1 -1
- package/dist/hooks/extensibility/__tests__/sdk.test.js +151 -1
- package/dist/hooks/extensibility/__tests__/sdk.test.js.map +1 -1
- package/dist/hooks/extensibility/dispatcher.d.ts.map +1 -1
- package/dist/hooks/extensibility/dispatcher.js +20 -3
- package/dist/hooks/extensibility/dispatcher.js.map +1 -1
- package/dist/hooks/extensibility/plugin-runner.js +1 -0
- package/dist/hooks/extensibility/plugin-runner.js.map +1 -1
- package/dist/hooks/extensibility/runtime.d.ts.map +1 -1
- package/dist/hooks/extensibility/runtime.js.map +1 -1
- package/dist/hooks/extensibility/sdk/paths.d.ts +1 -1
- package/dist/hooks/extensibility/sdk/paths.d.ts.map +1 -1
- package/dist/hooks/extensibility/sdk/paths.js +2 -2
- package/dist/hooks/extensibility/sdk/paths.js.map +1 -1
- package/dist/hooks/extensibility/sdk/runtime-state.d.ts +1 -1
- package/dist/hooks/extensibility/sdk/runtime-state.d.ts.map +1 -1
- package/dist/hooks/extensibility/sdk/runtime-state.js +55 -15
- package/dist/hooks/extensibility/sdk/runtime-state.js.map +1 -1
- package/dist/hooks/extensibility/sdk.d.ts +1 -0
- package/dist/hooks/extensibility/sdk.d.ts.map +1 -1
- package/dist/hooks/extensibility/sdk.js +1 -1
- package/dist/hooks/extensibility/sdk.js.map +1 -1
- package/dist/hooks/extensibility/types.d.ts +7 -0
- package/dist/hooks/extensibility/types.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.d.ts +10 -3
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +116 -20
- package/dist/hooks/keyword-detector.js.map +1 -1
- package/dist/hooks/session.d.ts +204 -14
- package/dist/hooks/session.d.ts.map +1 -1
- package/dist/hooks/session.js +1500 -223
- package/dist/hooks/session.js.map +1 -1
- package/dist/hud/__tests__/hud-tmux-injection.test.js +6 -6
- package/dist/hud/__tests__/hud-tmux-injection.test.js.map +1 -1
- package/dist/hud/__tests__/index.test.js +26 -16
- package/dist/hud/__tests__/index.test.js.map +1 -1
- package/dist/hud/__tests__/reconcile.test.js +83 -78
- package/dist/hud/__tests__/reconcile.test.js.map +1 -1
- package/dist/hud/__tests__/state.test.js +36 -31
- package/dist/hud/__tests__/state.test.js.map +1 -1
- package/dist/hud/__tests__/tmux.test.js +155 -198
- package/dist/hud/__tests__/tmux.test.js.map +1 -1
- package/dist/hud/tmux.d.ts +42 -4
- package/dist/hud/tmux.d.ts.map +1 -1
- package/dist/hud/tmux.js +1017 -139
- package/dist/hud/tmux.js.map +1 -1
- package/dist/leader/__tests__/contract.test.js +124 -37
- package/dist/leader/__tests__/contract.test.js.map +1 -1
- package/dist/leader/contract.d.ts +17 -6
- package/dist/leader/contract.d.ts.map +1 -1
- package/dist/leader/contract.js +102 -26
- package/dist/leader/contract.js.map +1 -1
- package/dist/mcp/__tests__/code-intel-server.test.js +31 -3
- package/dist/mcp/__tests__/code-intel-server.test.js.map +1 -1
- package/dist/mcp/__tests__/hermes-bridge.test.js +17 -0
- package/dist/mcp/__tests__/hermes-bridge.test.js.map +1 -1
- package/dist/mcp/__tests__/server-lifecycle.test.js +4 -4
- package/dist/mcp/__tests__/state-paths.test.js +735 -4
- package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
- package/dist/mcp/__tests__/state-server.test.js +1 -1
- package/dist/mcp/__tests__/state-server.test.js.map +1 -1
- package/dist/mcp/__tests__/trace-server.test.js +2 -2
- package/dist/mcp/__tests__/trace-server.test.js.map +1 -1
- package/dist/mcp/__tests__/wiki-server.test.js +18 -0
- package/dist/mcp/__tests__/wiki-server.test.js.map +1 -1
- package/dist/mcp/code-intel-server.d.ts.map +1 -1
- package/dist/mcp/code-intel-server.js +13 -3
- package/dist/mcp/code-intel-server.js.map +1 -1
- package/dist/mcp/hermes-bridge.d.ts.map +1 -1
- package/dist/mcp/hermes-bridge.js +2 -2
- package/dist/mcp/hermes-bridge.js.map +1 -1
- package/dist/mcp/state-paths.d.ts +78 -4
- package/dist/mcp/state-paths.d.ts.map +1 -1
- package/dist/mcp/state-paths.js +543 -40
- package/dist/mcp/state-paths.js.map +1 -1
- package/dist/mcp/state-server.d.ts +4 -4
- package/dist/mcp/state-server.d.ts.map +1 -1
- package/dist/mcp/wiki-server.d.ts +11 -0
- package/dist/mcp/wiki-server.d.ts.map +1 -1
- package/dist/mcp/wiki-server.js +9 -2
- package/dist/mcp/wiki-server.js.map +1 -1
- package/dist/modes/__tests__/base-autopilot-gates.test.js +36 -7
- package/dist/modes/__tests__/base-autopilot-gates.test.js.map +1 -1
- package/dist/modes/__tests__/base-multi-state-compat.test.js +1 -1
- package/dist/modes/__tests__/base-multi-state-compat.test.js.map +1 -1
- package/dist/modes/__tests__/base-session-scope.test.js +17 -8
- package/dist/modes/__tests__/base-session-scope.test.js.map +1 -1
- package/dist/modes/__tests__/base-tmux-pane.test.js +4 -4
- package/dist/modes/__tests__/base-tmux-pane.test.js.map +1 -1
- package/dist/modes/base.d.ts +5 -5
- package/dist/modes/base.d.ts.map +1 -1
- package/dist/modes/base.js +76 -21
- package/dist/modes/base.js.map +1 -1
- package/dist/native-assets/archive.d.ts +17 -0
- package/dist/native-assets/archive.d.ts.map +1 -0
- package/dist/native-assets/archive.js +238 -0
- package/dist/native-assets/archive.js.map +1 -0
- package/dist/native-assets/policy.d.ts +42 -0
- package/dist/native-assets/policy.d.ts.map +1 -0
- package/dist/native-assets/policy.js +155 -0
- package/dist/native-assets/policy.js.map +1 -0
- package/dist/notifications/temp-contract.d.ts.map +1 -1
- package/dist/notifications/temp-contract.js +4 -0
- package/dist/notifications/temp-contract.js.map +1 -1
- package/dist/pipeline/__tests__/orchestrator.test.js +164 -42
- package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
- package/dist/pipeline/__tests__/stages.test.js +72 -155
- package/dist/pipeline/__tests__/stages.test.js.map +1 -1
- package/dist/pipeline/orchestrator.d.ts.map +1 -1
- package/dist/pipeline/orchestrator.js +124 -18
- package/dist/pipeline/orchestrator.js.map +1 -1
- package/dist/pipeline/stages/ralplan.d.ts.map +1 -1
- package/dist/pipeline/stages/ralplan.js +36 -8
- package/dist/pipeline/stages/ralplan.js.map +1 -1
- package/dist/ralplan/__tests__/consensus-gate.test.js +73 -1316
- package/dist/ralplan/__tests__/consensus-gate.test.js.map +1 -1
- package/dist/ralplan/__tests__/documented-leader-preflight.test.d.ts +2 -0
- package/dist/ralplan/__tests__/documented-leader-preflight.test.d.ts.map +1 -0
- package/dist/ralplan/__tests__/documented-leader-preflight.test.js +335 -0
- package/dist/ralplan/__tests__/documented-leader-preflight.test.js.map +1 -0
- package/dist/ralplan/__tests__/runtime.test.js +85 -101
- package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
- package/dist/ralplan/consensus-gate.d.ts +9 -1
- package/dist/ralplan/consensus-gate.d.ts.map +1 -1
- package/dist/ralplan/consensus-gate.js +82 -338
- package/dist/ralplan/consensus-gate.js.map +1 -1
- package/dist/ralplan/documented-leader-preflight.d.ts +41 -0
- package/dist/ralplan/documented-leader-preflight.d.ts.map +1 -0
- package/dist/ralplan/documented-leader-preflight.js +360 -0
- package/dist/ralplan/documented-leader-preflight.js.map +1 -0
- package/dist/ralplan/runtime.d.ts +2 -1
- package/dist/ralplan/runtime.d.ts.map +1 -1
- package/dist/ralplan/runtime.js +67 -168
- package/dist/ralplan/runtime.js.map +1 -1
- package/dist/runtime/__tests__/bridge.test.js +42 -1
- package/dist/runtime/__tests__/bridge.test.js.map +1 -1
- package/dist/runtime/bridge.d.ts +12 -0
- package/dist/runtime/bridge.d.ts.map +1 -1
- package/dist/runtime/bridge.js +52 -1
- package/dist/runtime/bridge.js.map +1 -1
- package/dist/runtime/run-state.d.ts +5 -1
- package/dist/runtime/run-state.d.ts.map +1 -1
- package/dist/runtime/run-state.js +15 -8
- package/dist/runtime/run-state.js.map +1 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js +16323 -8978
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/__tests__/issue-3293-callsite-parity.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3293-callsite-parity.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3293-callsite-parity.test.js +225 -0
- package/dist/scripts/__tests__/issue-3293-callsite-parity.test.js.map +1 -0
- package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.js +188 -0
- package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.js.map +1 -0
- package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.js +259 -0
- package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.js.map +1 -0
- package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.js +491 -0
- package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.js.map +1 -0
- package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.js +708 -0
- package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.js.map +1 -0
- package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.js +208 -0
- package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.js.map +1 -0
- package/dist/scripts/__tests__/notify-state-io.test.js +26 -0
- package/dist/scripts/__tests__/notify-state-io.test.js.map +1 -1
- package/dist/scripts/__tests__/notify-tmux-injection.test.js +23 -1
- package/dist/scripts/__tests__/notify-tmux-injection.test.js.map +1 -1
- package/dist/scripts/__tests__/postinstall.test.js +19 -1
- package/dist/scripts/__tests__/postinstall.test.js.map +1 -1
- package/dist/scripts/__tests__/reasoning-artifact-contract.test.d.ts +2 -0
- package/dist/scripts/__tests__/reasoning-artifact-contract.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/reasoning-artifact-contract.test.js +108 -0
- package/dist/scripts/__tests__/reasoning-artifact-contract.test.js.map +1 -0
- package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js +63 -173
- package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js.map +1 -1
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js +74 -159
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js.map +1 -1
- package/dist/scripts/__tests__/smoke-packed-install.test.js +404 -13
- package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
- package/dist/scripts/__tests__/verify-native-release-assets.test.d.ts +2 -0
- package/dist/scripts/__tests__/verify-native-release-assets.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/verify-native-release-assets.test.js +235 -0
- package/dist/scripts/__tests__/verify-native-release-assets.test.js.map +1 -0
- package/dist/scripts/codex-native-hook.d.ts +53 -1
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +14448 -1480
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/scripts/codex-native-pre-post.d.ts +9 -0
- package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
- package/dist/scripts/codex-native-pre-post.js +35 -12
- package/dist/scripts/codex-native-pre-post.js.map +1 -1
- package/dist/scripts/generate-native-release-manifest.js +80 -51
- package/dist/scripts/generate-native-release-manifest.js.map +1 -1
- package/dist/scripts/notify-fallback-watcher.js +78 -115
- package/dist/scripts/notify-fallback-watcher.js.map +1 -1
- package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
- package/dist/scripts/notify-hook/auto-nudge.js +74 -3
- package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
- package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
- package/dist/scripts/notify-hook/state-io.js +5 -0
- package/dist/scripts/notify-hook/state-io.js.map +1 -1
- package/dist/scripts/notify-hook/team-dispatch.d.ts +9 -0
- package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-dispatch.js +234 -79
- package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
- package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-leader-nudge.js +71 -16
- package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.d.ts +12 -4
- package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.js +365 -44
- package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
- package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-worker-stop.js +42 -21
- package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
- package/dist/scripts/notify-hook/team-worker.d.ts +3 -0
- package/dist/scripts/notify-hook/team-worker.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-worker.js +82 -33
- package/dist/scripts/notify-hook/team-worker.js.map +1 -1
- package/dist/scripts/notify-hook/tmux-injection.d.ts +1 -0
- package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
- package/dist/scripts/notify-hook/tmux-injection.js +32 -3
- package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
- package/dist/scripts/notify-hook.js +79 -82
- package/dist/scripts/notify-hook.js.map +1 -1
- package/dist/scripts/postinstall-advisory.d.ts +14 -0
- package/dist/scripts/postinstall-advisory.d.ts.map +1 -0
- package/dist/scripts/postinstall-advisory.js +49 -0
- package/dist/scripts/postinstall-advisory.js.map +1 -0
- package/dist/scripts/postinstall.d.ts +1 -1
- package/dist/scripts/postinstall.d.ts.map +1 -1
- package/dist/scripts/postinstall.js +1 -1
- package/dist/scripts/postinstall.js.map +1 -1
- package/dist/scripts/smoke-packed-install.d.ts +80 -4
- package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
- package/dist/scripts/smoke-packed-install.js +3162 -26
- package/dist/scripts/smoke-packed-install.js.map +1 -1
- package/dist/scripts/sync-plugin-mirror.d.ts.map +1 -1
- package/dist/scripts/sync-plugin-mirror.js +16 -1
- package/dist/scripts/sync-plugin-mirror.js.map +1 -1
- package/dist/scripts/verify-native-release-assets.js +37 -23
- package/dist/scripts/verify-native-release-assets.js.map +1 -1
- package/dist/state/__tests__/mode-state-context.test.js +61 -0
- package/dist/state/__tests__/mode-state-context.test.js.map +1 -1
- package/dist/state/__tests__/operations.test.js +462 -850
- package/dist/state/__tests__/operations.test.js.map +1 -1
- package/dist/state/__tests__/skill-active.test.js +400 -2
- package/dist/state/__tests__/skill-active.test.js.map +1 -1
- package/dist/state/mode-state-context.d.ts +8 -0
- package/dist/state/mode-state-context.d.ts.map +1 -1
- package/dist/state/mode-state-context.js +73 -0
- package/dist/state/mode-state-context.js.map +1 -1
- package/dist/state/operations.d.ts +3 -0
- package/dist/state/operations.d.ts.map +1 -1
- package/dist/state/operations.js +144 -40
- package/dist/state/operations.js.map +1 -1
- package/dist/state/skill-active.d.ts +18 -1
- package/dist/state/skill-active.d.ts.map +1 -1
- package/dist/state/skill-active.js +420 -23
- package/dist/state/skill-active.js.map +1 -1
- package/dist/state/workflow-transition-reconcile.d.ts +3 -0
- package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
- package/dist/state/workflow-transition-reconcile.js +10 -4
- package/dist/state/workflow-transition-reconcile.js.map +1 -1
- package/dist/state/workflow-transition.d.ts.map +1 -1
- package/dist/state/workflow-transition.js +3 -1
- package/dist/state/workflow-transition.js.map +1 -1
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.js +53 -247
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.js.map +1 -1
- package/dist/subagents/__tests__/tracker.test.js +837 -1048
- package/dist/subagents/__tests__/tracker.test.js.map +1 -1
- package/dist/subagents/tracker.d.ts +58 -135
- package/dist/subagents/tracker.d.ts.map +1 -1
- package/dist/subagents/tracker.js +811 -632
- package/dist/subagents/tracker.js.map +1 -1
- package/dist/team/__tests__/api-interop.test.js +91 -1
- package/dist/team/__tests__/api-interop.test.js.map +1 -1
- package/dist/team/__tests__/delivery-e2e-smoke.test.js +58 -17
- package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
- package/dist/team/__tests__/idle-nudge.test.js +45 -15
- package/dist/team/__tests__/idle-nudge.test.js.map +1 -1
- package/dist/team/__tests__/mcp-comm.test.js +197 -30
- package/dist/team/__tests__/mcp-comm.test.js.map +1 -1
- package/dist/team/__tests__/model-contract.test.js +110 -3
- package/dist/team/__tests__/model-contract.test.js.map +1 -1
- package/dist/team/__tests__/notice-ledger.test.d.ts +2 -0
- package/dist/team/__tests__/notice-ledger.test.d.ts.map +1 -0
- package/dist/team/__tests__/notice-ledger.test.js +131 -0
- package/dist/team/__tests__/notice-ledger.test.js.map +1 -0
- package/dist/team/__tests__/pane-teardown-residue-e2e.test.d.ts +2 -0
- package/dist/team/__tests__/pane-teardown-residue-e2e.test.d.ts.map +1 -0
- package/dist/team/__tests__/pane-teardown-residue-e2e.test.js +410 -0
- package/dist/team/__tests__/pane-teardown-residue-e2e.test.js.map +1 -0
- package/dist/team/__tests__/phase-controller.test.js +20 -0
- package/dist/team/__tests__/phase-controller.test.js.map +1 -1
- package/dist/team/__tests__/runtime-cli.test.js +9 -0
- package/dist/team/__tests__/runtime-cli.test.js.map +1 -1
- package/dist/team/__tests__/runtime.test.js +3963 -428
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/scaling.test.js +2405 -203
- package/dist/team/__tests__/scaling.test.js.map +1 -1
- package/dist/team/__tests__/state-root.test.js +117 -2
- package/dist/team/__tests__/state-root.test.js.map +1 -1
- package/dist/team/__tests__/state.test.js +804 -36
- package/dist/team/__tests__/state.test.js.map +1 -1
- package/dist/team/__tests__/team-ops-contract.test.js +6 -0
- package/dist/team/__tests__/team-ops-contract.test.js.map +1 -1
- package/dist/team/__tests__/tmux-session.test.js +4099 -1039
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/__tests__/tmux-source-authority-realtmux.test.d.ts +2 -0
- package/dist/team/__tests__/tmux-source-authority-realtmux.test.d.ts.map +1 -0
- package/dist/team/__tests__/tmux-source-authority-realtmux.test.js +149 -0
- package/dist/team/__tests__/tmux-source-authority-realtmux.test.js.map +1 -0
- package/dist/team/__tests__/tmux-test-fixture.d.ts +69 -0
- package/dist/team/__tests__/tmux-test-fixture.d.ts.map +1 -1
- package/dist/team/__tests__/tmux-test-fixture.js +242 -15
- package/dist/team/__tests__/tmux-test-fixture.js.map +1 -1
- package/dist/team/__tests__/tmux-test-fixture.test.js +177 -1
- package/dist/team/__tests__/tmux-test-fixture.test.js.map +1 -1
- package/dist/team/__tests__/worker-runtime-identity.test.js +53 -44
- package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
- package/dist/team/__tests__/worktree.test.js +6 -0
- package/dist/team/__tests__/worktree.test.js.map +1 -1
- package/dist/team/api-interop.d.ts.map +1 -1
- package/dist/team/api-interop.js +15 -9
- package/dist/team/api-interop.js.map +1 -1
- package/dist/team/exact-pane.d.ts +23 -0
- package/dist/team/exact-pane.d.ts.map +1 -0
- package/dist/team/exact-pane.js +133 -0
- package/dist/team/exact-pane.js.map +1 -0
- package/dist/team/idle-nudge.d.ts +12 -1
- package/dist/team/idle-nudge.d.ts.map +1 -1
- package/dist/team/idle-nudge.js +41 -14
- package/dist/team/idle-nudge.js.map +1 -1
- package/dist/team/model-contract.d.ts +2 -5
- package/dist/team/model-contract.d.ts.map +1 -1
- package/dist/team/model-contract.js +59 -17
- package/dist/team/model-contract.js.map +1 -1
- package/dist/team/notice-ledger.d.ts +54 -0
- package/dist/team/notice-ledger.d.ts.map +1 -0
- package/dist/team/notice-ledger.js +259 -0
- package/dist/team/notice-ledger.js.map +1 -0
- package/dist/team/phase-controller.d.ts.map +1 -1
- package/dist/team/phase-controller.js +2 -0
- package/dist/team/phase-controller.js.map +1 -1
- package/dist/team/runtime.d.ts +4 -1
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +1527 -291
- package/dist/team/runtime.js.map +1 -1
- package/dist/team/scaling.d.ts +13 -1
- package/dist/team/scaling.d.ts.map +1 -1
- package/dist/team/scaling.js +1503 -387
- package/dist/team/scaling.js.map +1 -1
- package/dist/team/state/dispatch.d.ts.map +1 -1
- package/dist/team/state/dispatch.js +26 -8
- package/dist/team/state/dispatch.js.map +1 -1
- package/dist/team/state/locks.d.ts.map +1 -1
- package/dist/team/state/locks.js +61 -13
- package/dist/team/state/locks.js.map +1 -1
- package/dist/team/state/monitor.d.ts +10 -0
- package/dist/team/state/monitor.d.ts.map +1 -1
- package/dist/team/state/monitor.js +13 -0
- package/dist/team/state/monitor.js.map +1 -1
- package/dist/team/state/tasks.d.ts.map +1 -1
- package/dist/team/state/tasks.js +4 -3
- package/dist/team/state/tasks.js.map +1 -1
- package/dist/team/state/types.d.ts +15 -0
- package/dist/team/state/types.d.ts.map +1 -1
- package/dist/team/state/types.js.map +1 -1
- package/dist/team/state-root.d.ts +12 -0
- package/dist/team/state-root.d.ts.map +1 -1
- package/dist/team/state-root.js +177 -0
- package/dist/team/state-root.js.map +1 -1
- package/dist/team/state.d.ts +85 -1
- package/dist/team/state.d.ts.map +1 -1
- package/dist/team/state.js +672 -129
- package/dist/team/state.js.map +1 -1
- package/dist/team/team-ops.d.ts +5 -0
- package/dist/team/team-ops.d.ts.map +1 -1
- package/dist/team/team-ops.js +4 -0
- package/dist/team/team-ops.js.map +1 -1
- package/dist/team/tmux-session.d.ts +163 -27
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +1682 -376
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/team/worktree.d.ts.map +1 -1
- package/dist/team/worktree.js +4 -0
- package/dist/team/worktree.js.map +1 -1
- package/dist/ultragoal/__tests__/artifacts.test.js +950 -2
- package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
- package/dist/ultragoal/__tests__/docs-contract.test.js +1 -0
- package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
- package/dist/ultragoal/artifacts.d.ts +35 -1
- package/dist/ultragoal/artifacts.d.ts.map +1 -1
- package/dist/ultragoal/artifacts.js +232 -46
- package/dist/ultragoal/artifacts.js.map +1 -1
- package/dist/utils/__tests__/file-durability.test.d.ts +2 -0
- package/dist/utils/__tests__/file-durability.test.d.ts.map +1 -0
- package/dist/utils/__tests__/file-durability.test.js +114 -0
- package/dist/utils/__tests__/file-durability.test.js.map +1 -0
- package/dist/utils/__tests__/paths.test.js +21 -1
- package/dist/utils/__tests__/paths.test.js.map +1 -1
- package/dist/utils/file-durability.d.ts +25 -0
- package/dist/utils/file-durability.d.ts.map +1 -0
- package/dist/utils/file-durability.js +67 -0
- package/dist/utils/file-durability.js.map +1 -0
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +14 -6
- package/dist/utils/paths.js.map +1 -1
- package/dist/verification/__tests__/ci-rust-gates.test.js +49 -4
- package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
- package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js +3 -0
- package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js.map +1 -1
- package/dist/verification/__tests__/explore-harness-release-workflow.test.js +60 -0
- package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +1 -1
- package/dist/verification/__tests__/native-release-manifest.test.js +157 -4
- package/dist/verification/__tests__/native-release-manifest.test.js.map +1 -1
- package/dist/wiki/lint.d.ts +4 -1
- package/dist/wiki/lint.d.ts.map +1 -1
- package/dist/wiki/lint.js +3 -2
- package/dist/wiki/lint.js.map +1 -1
- package/package.json +13 -5
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +105 -17
- package/plugins/oh-my-codex/skills/autopilot/SKILL.md +13 -5
- package/plugins/oh-my-codex/skills/cancel/SKILL.md +30 -85
- package/plugins/oh-my-codex/skills/ralph/SKILL.md +6 -4
- package/plugins/oh-my-codex/skills/ralplan/SKILL.md +16 -17
- package/plugins/oh-my-codex/skills/team/SKILL.md +13 -9
- package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +3 -2
- package/skills/autopilot/SKILL.md +13 -5
- package/skills/cancel/SKILL.md +30 -85
- package/skills/ralph/SKILL.md +6 -4
- package/skills/ralplan/SKILL.md +16 -17
- package/skills/team/SKILL.md +13 -9
- package/skills/ultragoal/SKILL.md +3 -2
- package/src/scripts/__tests__/codex-native-hook.test.ts +8831 -951
- package/src/scripts/__tests__/issue-3293-callsite-parity.test.ts +187 -0
- package/src/scripts/__tests__/issue-3293-cancel-transaction.test.ts +189 -0
- package/src/scripts/__tests__/issue-3293-hook-owned-cancel.test.ts +187 -0
- package/src/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.ts +591 -0
- package/src/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.ts +701 -0
- package/src/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.ts +211 -0
- package/src/scripts/__tests__/notify-state-io.test.ts +29 -0
- package/src/scripts/__tests__/notify-tmux-injection.test.ts +26 -1
- package/src/scripts/__tests__/postinstall.test.ts +23 -1
- package/src/scripts/__tests__/reasoning-artifact-contract.test.ts +129 -0
- package/src/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.ts +67 -185
- package/src/scripts/__tests__/role-intent-durable-recovery-3181.test.ts +79 -178
- package/src/scripts/__tests__/smoke-packed-install.test.ts +525 -16
- package/src/scripts/__tests__/verify-native-release-assets.test.ts +240 -0
- package/src/scripts/codex-native-hook.ts +14238 -1429
- package/src/scripts/codex-native-pre-post.ts +43 -12
- package/src/scripts/generate-native-release-manifest.ts +83 -79
- package/src/scripts/notify-fallback-watcher.ts +91 -122
- package/src/scripts/notify-hook/auto-nudge.ts +71 -3
- package/src/scripts/notify-hook/state-io.ts +11 -0
- package/src/scripts/notify-hook/team-dispatch.ts +242 -75
- package/src/scripts/notify-hook/team-leader-nudge.ts +72 -22
- package/src/scripts/notify-hook/team-tmux-guard.ts +366 -39
- package/src/scripts/notify-hook/team-worker-stop.ts +43 -20
- package/src/scripts/notify-hook/team-worker.ts +82 -31
- package/src/scripts/notify-hook/tmux-injection.ts +33 -3
- package/src/scripts/notify-hook.ts +84 -82
- package/src/scripts/postinstall-advisory.ts +67 -0
- package/src/scripts/postinstall.ts +1 -1
- package/src/scripts/smoke-packed-install.ts +3608 -39
- package/src/scripts/sync-plugin-mirror.ts +17 -1
- package/src/scripts/verify-native-release-assets.ts +39 -31
- package/templates/AGENTS.md +1 -0
- package/dist/subagents/__tests__/adapted-role-binding.test.d.ts +0 -2
- package/dist/subagents/__tests__/adapted-role-binding.test.d.ts.map +0 -1
- package/dist/subagents/__tests__/adapted-role-binding.test.js +0 -988
- package/dist/subagents/__tests__/adapted-role-binding.test.js.map +0 -1
- package/dist/subagents/adapted-role-binding.d.ts +0 -17
- package/dist/subagents/adapted-role-binding.d.ts.map +0 -1
- package/dist/subagents/adapted-role-binding.js +0 -75
- package/dist/subagents/adapted-role-binding.js.map +0 -1
package/dist/cli/index.js
CHANGED
|
@@ -3,9 +3,9 @@
|
|
|
3
3
|
* Multi-agent orchestration for OpenAI Codex CLI
|
|
4
4
|
*/
|
|
5
5
|
import { execFileSync, spawn } from "child_process";
|
|
6
|
-
import { basename, dirname, join, posix, resolve, win32 } from "path";
|
|
7
|
-
import { chmodSync, existsSync, lstatSync, mkdirSync, readFileSync, realpathSync, rmSync, statSync, writeFileSync } from "fs";
|
|
8
|
-
import { copyFile, cp, lstat, mkdir, readFile, readdir, rm, stat, symlink, utimes, writeFile } from "fs/promises";
|
|
6
|
+
import { basename, dirname, isAbsolute, join, posix, relative, resolve, win32 } from "path";
|
|
7
|
+
import { appendFileSync, chmodSync, closeSync, constants as fsConstants, existsSync, fsyncSync, lstatSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, rmSync, statSync, unlinkSync, writeFileSync } from "fs";
|
|
8
|
+
import { copyFile, cp, lstat, mkdir, open, readFile, readdir, rm, stat, symlink, utimes, writeFile } from "fs/promises";
|
|
9
9
|
import { constants as osConstants, homedir } from "os";
|
|
10
10
|
import { createHash, randomUUID } from "crypto";
|
|
11
11
|
import { setup, SETUP_MCP_MODES, SETUP_SCOPES, SETUP_TEAM_MODES, } from "./setup.js";
|
|
@@ -39,25 +39,25 @@ import { adaptCommand } from "./adapt.js";
|
|
|
39
39
|
import { listCommand } from "./list.js";
|
|
40
40
|
import { authCommand } from "./auth.js";
|
|
41
41
|
import { missionCommand } from "./mission.js";
|
|
42
|
-
import { runAuthHotswap } from "../auth/hotswap.js";
|
|
42
|
+
import { resolveCodexGlobalOptionValue, runAuthHotswap, } from "../auth/hotswap.js";
|
|
43
43
|
import { MADMAX_FLAG, CODEX_BYPASS_FLAG, HIGH_REASONING_FLAG, XHIGH_REASONING_FLAG, SPARK_FLAG, MADMAX_SPARK_FLAG, CONFIG_FLAG, LONG_CONFIG_FLAG, } from "./constants.js";
|
|
44
|
-
import { getBaseStateDir, getStateDir, listModeStateFilesWithScopePreference, resolveWritableStateScope, } from "../mcp/state-paths.js";
|
|
44
|
+
import { getBaseStateDir, getBaseStateDirWithSource, getStateDir, isModeStateFilename, listModeStateFilesWithScopePreference, resolveWritableStateScope, } from "../mcp/state-paths.js";
|
|
45
45
|
import { evaluateRalphCompletionAuditEvidence, isRalphCompletePhase } from "../ralph/completion-audit.js";
|
|
46
46
|
import { normalizeTerminalWorkflowState } from "../state/terminal-normalization.js";
|
|
47
47
|
import { readPersistedSetupPreferences, resolveCodexConfigPathForLaunch, resolveCodexHomeForLaunch, resolveProjectLocalCodexHomeForLaunch, } from "./codex-home.js";
|
|
48
48
|
import { discoverProjectRuntimeCodexHomes } from "./project-runtime-codex-homes.js";
|
|
49
49
|
import { discoverOmxPluginCacheDirs, hasLocalOmxPluginEnablement, materializePackagedOmxPluginCache, packagedOmxPluginVersion, resolvePackagedOmxMarketplace, upsertLocalOmxMarketplaceRegistration, upsertLocalOmxPluginEnablement, } from "./plugin-marketplace.js";
|
|
50
50
|
import { escapeTomlString, readTopLevelTomlString, upsertTopLevelTomlString } from "../utils/toml.js";
|
|
51
|
-
import {
|
|
51
|
+
import { ROOT_REASONING_EFFORTS, isUnsupportedRootReasoningEffort, normalizeUnsupportedRootReasoningEffort, } from "../config/models.js";
|
|
52
52
|
export { readPersistedSetupPreferences, readPersistedSetupScope, resolveCodexConfigPathForLaunch, resolveCodexHomeForLaunch, resolveProjectLocalCodexHomeForLaunch, } from "./codex-home.js";
|
|
53
|
-
import { SKILL_ACTIVE_STATE_MODE, extractSessionIdFromInitializedStatePath,
|
|
53
|
+
import { SKILL_ACTIVE_STATE_MODE, extractSessionIdFromInitializedStatePath, listActiveSkills, syncCanonicalSkillStateForMode, updateRootSkillActiveStateForStateDir, } from "../state/skill-active.js";
|
|
54
54
|
import { isTrackedWorkflowMode } from "../state/workflow-transition.js";
|
|
55
55
|
import { maybeCheckAndPromptUpdate, runImmediateUpdate } from "./update.js";
|
|
56
56
|
import { maybePromptGithubStar } from "./star-prompt.js";
|
|
57
57
|
import { generateOverlay, removeSessionModelInstructionsFile, resolveSessionOrchestrationMode, sessionModelInstructionsPath, writeSessionModelInstructionsFile, } from "../hooks/agents-overlay.js";
|
|
58
|
-
import {
|
|
58
|
+
import { closeLaunchSessionBindingOnce, establishLaunchSessionBinding, readNativeSessionOwnerEvidence, readSessionPointer, readSessionState, resolveSessionPointerContext, finalizeBoundOnce, isSessionPointerLaunchAbort, normalizeSessionId, resetSessionMetrics, } from "../hooks/session.js";
|
|
59
59
|
import { probeActualTmuxInstanceEvidence, tmuxEvidenceBindsCandidate } from "../scripts/notify-hook/managed-tmux.js";
|
|
60
|
-
import { buildClientAttachedReconcileHookName, buildReconcileHudResizeArgs, buildRegisterClientAttachedReconcileArgs, buildRegisterResizeHookArgs, buildResizeHookName, buildResizeHookTarget, buildScheduleDelayedHudResizeArgs, buildUnregisterClientAttachedReconcileArgs, buildUnregisterResizeHookArgs, enableMouseScrolling, isMsysOrGitBash, isNativeWindows, isTmuxAvailable,
|
|
60
|
+
import { buildClientAttachedReconcileHookName, buildReconcileHudResizeArgs, buildRegisterClientAttachedReconcileArgs, buildRegisterResizeHookArgs, buildResizeHookName, buildResizeHookTarget, buildScheduleDelayedHudResizeArgs, buildUnregisterClientAttachedReconcileArgs, buildUnregisterResizeHookArgs, enableMouseScrolling, isMsysOrGitBash, isNativeWindows, isTmuxAvailable, } from "../team/tmux-session.js";
|
|
61
61
|
import { getPackageRoot } from "../utils/package.js";
|
|
62
62
|
import { codexConfigPath, omxRoot, rememberOmxLaunchContext, resolveOmxCliEntryPath } from "../utils/paths.js";
|
|
63
63
|
import { cleanCodexModelAvailabilityNuxIfNeeded, extractSharedMcpRegistryServersFromConfig, repairConfigIfNeeded, repairProjectScopeTrustStateForLaunch, syncProjectScopeTrustStateFromRuntime } from "../config/generator.js";
|
|
@@ -73,6 +73,7 @@ import { buildHookEvent } from "../hooks/extensibility/events.js";
|
|
|
73
73
|
import { dispatchHookEvent } from "../hooks/extensibility/dispatcher.js";
|
|
74
74
|
import { collectInheritableTeamWorkerArgs as collectInheritableTeamWorkerArgsShared, parseTeamWorkerLaunchArgs, resolveTeamWorkerLaunchArgs, resolveTeamLowComplexityDefaultModel, serializeTeamWorkerLaunchArgs, TEAM_WORKER_INHERITED_MODEL_ENV, } from "../team/model-contract.js";
|
|
75
75
|
import { parseWorktreeMode, planWorktreeTarget, ensureWorktree, } from "../team/worktree.js";
|
|
76
|
+
import { resolveVerifiedDetachedTeamContext } from "../team/state-root.js";
|
|
76
77
|
import { ensureReusableNodeModules } from "../utils/repo-deps.js";
|
|
77
78
|
import { resolveWorktreeToolContext, worktreeToolContextEnv } from "../utils/worktree-tool-context.js";
|
|
78
79
|
import { OMX_NOTIFY_TEMP_CONTRACT_ENV, parseNotifyTempContractFromArgs, serializeNotifyTempContract, } from "../notifications/temp-contract.js";
|
|
@@ -123,6 +124,7 @@ Usage:
|
|
|
123
124
|
omx explore DEPRECATED compatibility command; use normal repo inspection or omx sparkshell
|
|
124
125
|
omx api Run native omx-api localhost gateway commands (serve|status|stop|generate)
|
|
125
126
|
omx session Search and summarize local session history (--codex-home <path> escape hatch)
|
|
127
|
+
Includes session lock inspect/recover diagnostics
|
|
126
128
|
omx url Passive URL reader (read <url> --json)
|
|
127
129
|
omx capabilities
|
|
128
130
|
Lock/check deterministic configured tool, skill, agent, and observation surfaces
|
|
@@ -133,7 +135,8 @@ Usage:
|
|
|
133
135
|
Alias for agents-init (lightweight AGENTS bootstrap only)
|
|
134
136
|
omx team Spawn parallel worker panes in tmux and bootstrap inbox/task state
|
|
135
137
|
omx ralph Launch Codex with ralph persistence mode active
|
|
136
|
-
omx ralplan
|
|
138
|
+
omx ralplan Adapted Ralplan authority support; preflight applies only when native role routing is unavailable
|
|
139
|
+
and adapted Ralplan authority is requested; ordinary work remains under its own workflow gates
|
|
137
140
|
omx ultragoal Create, resume, and checkpoint durable multi-goal plans over Codex goal mode
|
|
138
141
|
omx performance-goal
|
|
139
142
|
Create, hand off, and gate evaluator-backed performance goals
|
|
@@ -235,10 +238,15 @@ const OMX_MODEL_INSTRUCTIONS_FILE_ENV = "OMX_MODEL_INSTRUCTIONS_FILE";
|
|
|
235
238
|
const OMX_INSTANCE_OPTION = "@omx_instance_id";
|
|
236
239
|
const OMX_RALPH_APPEND_INSTRUCTIONS_FILE_ENV = "OMX_RALPH_APPEND_INSTRUCTIONS_FILE";
|
|
237
240
|
const OMX_AUTORESEARCH_APPEND_INSTRUCTIONS_FILE_ENV = "OMX_AUTORESEARCH_APPEND_INSTRUCTIONS_FILE";
|
|
238
|
-
const REASONING_MODES =
|
|
241
|
+
const REASONING_MODES = ROOT_REASONING_EFFORTS;
|
|
239
242
|
const REASONING_MODE_SET = new Set(REASONING_MODES);
|
|
240
243
|
const REASONING_USAGE = "Usage: omx reasoning <low|medium|high|xhigh>";
|
|
241
|
-
|
|
244
|
+
function unsupportedLaunchShorthandError(flag) {
|
|
245
|
+
const guidance = flag === "--max"
|
|
246
|
+
? 'No --max shorthand exists; use agentReasoning for per-agent "max" or pass -c model_reasoning_effort=... directly to Codex.'
|
|
247
|
+
: '"ultra" is not an OMX root or per-agent reasoning value and is not an alias for "max".';
|
|
248
|
+
return new Error(`Unsupported OMX launch shorthand "${flag}".\n${guidance}\nRun "omx help" for usage.`);
|
|
249
|
+
}
|
|
242
250
|
const ALLOWED_SHELLS = new Set([
|
|
243
251
|
"/bin/sh",
|
|
244
252
|
"/bin/bash",
|
|
@@ -256,7 +264,6 @@ const ALLOWED_SHELLS = new Set([
|
|
|
256
264
|
"/opt/local/bin/zsh",
|
|
257
265
|
"/opt/homebrew/bin/zsh",
|
|
258
266
|
]);
|
|
259
|
-
const WINDOWS_DETACHED_BOOTSTRAP_DELAY_MS = 2500;
|
|
260
267
|
const CODEX_VERSION_FLAGS = new Set(["--version", "-V"]);
|
|
261
268
|
const TMUX_EXTENDED_KEYS_MODE = "always";
|
|
262
269
|
const TMUX_EXTENDED_KEYS_FALLBACK_MODE = "off";
|
|
@@ -264,6 +271,10 @@ const TMUX_EXTENDED_KEYS_LEASE_DIR = "tmux-extended-keys";
|
|
|
264
271
|
const TMUX_EXTENDED_KEYS_LOCK_RETRY_MS = 20;
|
|
265
272
|
const TMUX_EXTENDED_KEYS_LOCK_MAX_ATTEMPTS = 100;
|
|
266
273
|
const TMUX_EXTENDED_KEYS_LOCK_STALE_MS = 30_000;
|
|
274
|
+
export const DETACHED_LEADER_READY_TIMEOUT_MS = 120_000;
|
|
275
|
+
const RESUME_HELP = `Usage: omx resume [--project] [--codex-home <path>] [codex resume options]
|
|
276
|
+
|
|
277
|
+
Read-only help does not prepare or launch a Codex session.`;
|
|
267
278
|
const NESTED_HELP_COMMANDS = new Set([
|
|
268
279
|
"ask",
|
|
269
280
|
"question",
|
|
@@ -482,6 +493,15 @@ export function resolveSetupTeamModeArg(args) {
|
|
|
482
493
|
}
|
|
483
494
|
return value;
|
|
484
495
|
}
|
|
496
|
+
function splitOmxArgsAtEndOfOptions(args) {
|
|
497
|
+
const endOfOptionsIndex = args.indexOf("--");
|
|
498
|
+
if (endOfOptionsIndex === -1)
|
|
499
|
+
return { omxArgs: args, suffix: [] };
|
|
500
|
+
return {
|
|
501
|
+
omxArgs: args.slice(0, endOfOptionsIndex),
|
|
502
|
+
suffix: args.slice(endOfOptionsIndex),
|
|
503
|
+
};
|
|
504
|
+
}
|
|
485
505
|
export function resolveCliInvocation(args) {
|
|
486
506
|
const firstArg = args[0];
|
|
487
507
|
if (firstArg === "--help" || firstArg === "-h") {
|
|
@@ -502,6 +522,9 @@ export function resolveCliInvocation(args) {
|
|
|
502
522
|
if (firstArg === "resume") {
|
|
503
523
|
return { command: "resume", launchArgs: args.slice(1) };
|
|
504
524
|
}
|
|
525
|
+
if (firstArg === "__detached-session-leader") {
|
|
526
|
+
return { command: firstArg, launchArgs: args.slice(1) };
|
|
527
|
+
}
|
|
505
528
|
return { command: firstArg, launchArgs: [] };
|
|
506
529
|
}
|
|
507
530
|
export function resolveUpdateChannelArg(args) {
|
|
@@ -845,13 +868,14 @@ export async function prepareCodexHomeForLaunch(cwd, sessionId, env = process.en
|
|
|
845
868
|
};
|
|
846
869
|
}
|
|
847
870
|
export function parseResumeCodexHomeSelection(args) {
|
|
871
|
+
const { omxArgs, suffix } = splitOmxArgsAtEndOfOptions(args);
|
|
848
872
|
const nextArgs = [];
|
|
849
873
|
let explicitCodexHome;
|
|
850
874
|
let projectOnly = false;
|
|
851
|
-
for (let index = 0; index <
|
|
852
|
-
const arg =
|
|
875
|
+
for (let index = 0; index < omxArgs.length; index += 1) {
|
|
876
|
+
const arg = omxArgs[index];
|
|
853
877
|
if (arg === "--codex-home") {
|
|
854
|
-
const value =
|
|
878
|
+
const value = omxArgs[index + 1];
|
|
855
879
|
if (!value || value.startsWith("-")) {
|
|
856
880
|
throw new Error("Missing value after --codex-home.");
|
|
857
881
|
}
|
|
@@ -873,7 +897,7 @@ export function parseResumeCodexHomeSelection(args) {
|
|
|
873
897
|
nextArgs.push(arg);
|
|
874
898
|
}
|
|
875
899
|
return {
|
|
876
|
-
args: nextArgs,
|
|
900
|
+
args: [...nextArgs, ...suffix],
|
|
877
901
|
explicitCodexHome,
|
|
878
902
|
projectOnly,
|
|
879
903
|
};
|
|
@@ -918,8 +942,32 @@ export async function preflightResumeOmxPluginState(codexHomeDir, pkgRoot = getP
|
|
|
918
942
|
configUpdated,
|
|
919
943
|
};
|
|
920
944
|
}
|
|
921
|
-
|
|
922
|
-
|
|
945
|
+
export { CODEX_GLOBAL_OPTIONS_WITH_SPLIT_VALUE } from "../auth/hotswap.js";
|
|
946
|
+
export function isResumeCodexLaunch(args) {
|
|
947
|
+
const { omxArgs } = splitOmxArgsAtEndOfOptions(args);
|
|
948
|
+
for (let index = 0; index < omxArgs.length; index += 1) {
|
|
949
|
+
const arg = omxArgs[index];
|
|
950
|
+
const optionValue = resolveCodexGlobalOptionValue(arg);
|
|
951
|
+
if (optionValue?.valueArity === "single") {
|
|
952
|
+
if (!optionValue.attached)
|
|
953
|
+
index += 1;
|
|
954
|
+
continue;
|
|
955
|
+
}
|
|
956
|
+
if (optionValue?.valueArity === "variadic") {
|
|
957
|
+
if (optionValue.attached)
|
|
958
|
+
continue;
|
|
959
|
+
let nextIndex = index + 1;
|
|
960
|
+
while (nextIndex < omxArgs.length && !omxArgs[nextIndex].startsWith("-")) {
|
|
961
|
+
nextIndex += 1;
|
|
962
|
+
}
|
|
963
|
+
index = nextIndex - 1;
|
|
964
|
+
continue;
|
|
965
|
+
}
|
|
966
|
+
if (arg.startsWith("-"))
|
|
967
|
+
continue;
|
|
968
|
+
return arg === "resume";
|
|
969
|
+
}
|
|
970
|
+
return false;
|
|
923
971
|
}
|
|
924
972
|
async function prepareResumeCodexHomeForLaunch(cwd, sessionId, args, env = process.env) {
|
|
925
973
|
const selection = parseResumeCodexHomeSelection(args);
|
|
@@ -999,6 +1047,228 @@ function execTmuxFileSync(args, options) {
|
|
|
999
1047
|
...(process.platform === "win32" ? { windowsHide: true } : {}),
|
|
1000
1048
|
});
|
|
1001
1049
|
}
|
|
1050
|
+
function captureDetachedLeaderAuthority(leaderPaneId, expectedSessionName, ownerId) {
|
|
1051
|
+
if (!/^%[0-9]+$/.test(leaderPaneId) || !ownerId)
|
|
1052
|
+
throw new Error("invalid detached leader authority target");
|
|
1053
|
+
const output = execTmuxFileSync([
|
|
1054
|
+
"display-message", "-p", "-t", leaderPaneId,
|
|
1055
|
+
"#{session_name}\t#{session_id}\t#{session_created}\t#{window_index}\t#{window_id}\t#{pane_id}\t#{pane_pid}",
|
|
1056
|
+
], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
1057
|
+
const fields = output.split("\t");
|
|
1058
|
+
if (fields.length !== 7)
|
|
1059
|
+
throw new Error("malformed detached leader authority");
|
|
1060
|
+
const [sessionName, sessionId, sessionCreated, windowIndex, windowId, paneId, panePid] = fields;
|
|
1061
|
+
if (sessionName !== expectedSessionName || paneId !== leaderPaneId || !/^\$[0-9]+$/.test(sessionId)
|
|
1062
|
+
|| !/^[0-9]+$/.test(sessionCreated) || !/^[0-9]+$/.test(windowIndex)
|
|
1063
|
+
|| !/^@[0-9]+$/.test(windowId) || !/^[1-9][0-9]*$/.test(panePid)) {
|
|
1064
|
+
throw new Error("malformed detached leader authority");
|
|
1065
|
+
}
|
|
1066
|
+
const parsedPid = Number(panePid);
|
|
1067
|
+
if (!Number.isSafeInteger(parsedPid) || parsedPid <= 0)
|
|
1068
|
+
throw new Error("malformed detached leader authority");
|
|
1069
|
+
return { paneId, panePid: parsedPid, sessionName, sessionId, sessionCreated, windowId, windowIndex, ownerId };
|
|
1070
|
+
}
|
|
1071
|
+
/** Internal detached-launch seam. Exported solely for deterministic CLI tests. */
|
|
1072
|
+
export function parseDetachedLeaderPaneIdByPid(snapshot, leaderPid) {
|
|
1073
|
+
if (!Number.isSafeInteger(leaderPid) || leaderPid <= 0)
|
|
1074
|
+
throw new Error("invalid detached leader pid");
|
|
1075
|
+
const matches = snapshot
|
|
1076
|
+
.split("\n")
|
|
1077
|
+
.map((line) => line.trim())
|
|
1078
|
+
.filter(Boolean)
|
|
1079
|
+
.map((line) => line.split("\t"))
|
|
1080
|
+
.filter(([paneId, paneDead, panePid]) => /^%[0-9]+$/.test(paneId ?? "") && paneDead === "0" && Number(panePid) === leaderPid)
|
|
1081
|
+
.map(([paneId]) => paneId);
|
|
1082
|
+
if (matches.length !== 1)
|
|
1083
|
+
throw new Error("detached leader pane identity is unavailable");
|
|
1084
|
+
return matches[0];
|
|
1085
|
+
}
|
|
1086
|
+
function detachedLeaderAuthorityCondition(authority, requireOwner = true) {
|
|
1087
|
+
const conditions = [
|
|
1088
|
+
"#{==:#{pane_dead},0}",
|
|
1089
|
+
`#{==:#{pane_id},${authority.paneId}}`,
|
|
1090
|
+
`#{==:#{pane_pid},${authority.panePid}}`,
|
|
1091
|
+
`#{==:#{session_id},${authority.sessionId}}`,
|
|
1092
|
+
`#{==:#{session_created},${authority.sessionCreated}}`,
|
|
1093
|
+
`#{==:#{window_id},${authority.windowId}}`,
|
|
1094
|
+
...(requireOwner ? [`#{==:#{@omx_instance_id},${authority.ownerId}}`] : []),
|
|
1095
|
+
];
|
|
1096
|
+
return conditions.reduce((combined, condition) => `#{&&:${combined},${condition}}`);
|
|
1097
|
+
}
|
|
1098
|
+
function detachedHudAuthorityCondition(authority, requireMarker = true) {
|
|
1099
|
+
const conditions = [
|
|
1100
|
+
"#{==:#{pane_dead},0}",
|
|
1101
|
+
`#{==:#{pane_id},${authority.paneId}}`,
|
|
1102
|
+
`#{==:#{pane_pid},${authority.panePid}}`,
|
|
1103
|
+
`#{==:#{session_id},${authority.sessionId}}`,
|
|
1104
|
+
`#{==:#{window_id},${authority.windowId}}`,
|
|
1105
|
+
...(requireMarker ? [`#{m:*OMX_DETACHED_HUD_OPERATION=${authority.operationMarker}*,#{pane_start_command}}`] : []),
|
|
1106
|
+
];
|
|
1107
|
+
return conditions.reduce((combined, condition) => `#{&&:${combined},${condition}}`);
|
|
1108
|
+
}
|
|
1109
|
+
function detachedAuthorityReceipt() {
|
|
1110
|
+
return `omx_detached_${randomUUID()}`;
|
|
1111
|
+
}
|
|
1112
|
+
function runDetachedLeaderMutation(authority, args, requireOwner = true) {
|
|
1113
|
+
const receipt = detachedAuthorityReceipt();
|
|
1114
|
+
const success = `${args.map(quoteShellArg).join(" ")} ; display-message -p ${quoteShellArg(receipt)}`;
|
|
1115
|
+
const output = execTmuxFileSync([
|
|
1116
|
+
"if-shell", "-F", "-t", authority.paneId, detachedLeaderAuthorityCondition(authority, requireOwner),
|
|
1117
|
+
success, "display-message -p ''",
|
|
1118
|
+
], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
1119
|
+
if (output !== receipt)
|
|
1120
|
+
throw new Error("detached leader authority changed before tmux mutation");
|
|
1121
|
+
}
|
|
1122
|
+
function detachedPreReportCleanupCondition(authority) {
|
|
1123
|
+
const conditions = [
|
|
1124
|
+
"#{==:#{pane_dead},1}",
|
|
1125
|
+
`#{==:#{pane_id},${authority.paneId}}`,
|
|
1126
|
+
`#{==:#{session_name},${authority.sessionName}}`,
|
|
1127
|
+
`#{==:#{session_id},${authority.sessionId}}`,
|
|
1128
|
+
`#{==:#{session_created},${authority.sessionCreated}}`,
|
|
1129
|
+
`#{==:#{window_id},${authority.windowId}}`,
|
|
1130
|
+
];
|
|
1131
|
+
return conditions.reduce((combined, condition) => `#{&&:${combined},${condition}}`);
|
|
1132
|
+
}
|
|
1133
|
+
export function cleanupDetachedPreReportSession(authority) {
|
|
1134
|
+
const receipt = detachedAuthorityReceipt();
|
|
1135
|
+
const success = `kill-session -t ${quoteShellArg(authority.sessionName)} ; display-message -p ${quoteShellArg(receipt)}`;
|
|
1136
|
+
const output = execTmuxFileSync([
|
|
1137
|
+
"if-shell", "-F", "-t", authority.paneId, detachedPreReportCleanupCondition(authority),
|
|
1138
|
+
success, "display-message -p ''",
|
|
1139
|
+
], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
1140
|
+
if (output !== receipt)
|
|
1141
|
+
throw new Error("detached pre-report topology changed before cleanup");
|
|
1142
|
+
}
|
|
1143
|
+
function runDetachedHudMutation(leaderAuthority, hudAuthority, args, requireMarker = true) {
|
|
1144
|
+
const receipt = detachedAuthorityReceipt();
|
|
1145
|
+
const success = `${args.map(quoteShellArg).join(" ")} ; display-message -p ${quoteShellArg(receipt)}`;
|
|
1146
|
+
const hudGuard = `if-shell -F -t ${quoteShellArg(hudAuthority.paneId)} ${quoteShellArg(detachedHudAuthorityCondition(hudAuthority, requireMarker))} ${quoteShellArg(success)} ${quoteShellArg("display-message -p ''")}`;
|
|
1147
|
+
const output = execTmuxFileSync([
|
|
1148
|
+
"if-shell", "-F", "-t", leaderAuthority.paneId, detachedLeaderAuthorityCondition(leaderAuthority),
|
|
1149
|
+
hudGuard, "display-message -p ''",
|
|
1150
|
+
], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
1151
|
+
if (output !== receipt)
|
|
1152
|
+
throw new Error("detached leader or HUD authority changed before tmux mutation");
|
|
1153
|
+
}
|
|
1154
|
+
export function cleanupDetachedHudPane(authority, ownerId) {
|
|
1155
|
+
if (ownerId) {
|
|
1156
|
+
execTmuxFileSync(["kill-pane", "-t", authority.paneId], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
1157
|
+
}
|
|
1158
|
+
else {
|
|
1159
|
+
execTmuxFileSync([
|
|
1160
|
+
"if-shell", "-F", "-t", authority.paneId, detachedHudAuthorityCondition(authority, false),
|
|
1161
|
+
`kill-pane -t ${quoteShellArg(authority.paneId)}`, "",
|
|
1162
|
+
], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
1163
|
+
}
|
|
1164
|
+
for (let attempt = 0; attempt < 50; attempt += 1) {
|
|
1165
|
+
const panes = execTmuxFileSync(["list-panes", "-a", "-F", "#{pane_id}"], {
|
|
1166
|
+
encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"],
|
|
1167
|
+
}).split("\n").map((value) => value.trim()).filter(Boolean);
|
|
1168
|
+
if (!panes.includes(authority.paneId))
|
|
1169
|
+
return;
|
|
1170
|
+
blockMs(20);
|
|
1171
|
+
}
|
|
1172
|
+
throw new Error("detached HUD topology changed before teardown");
|
|
1173
|
+
}
|
|
1174
|
+
function tagDetachedHudPane(leaderAuthority, authority, ownerId) {
|
|
1175
|
+
runDetachedHudMutation(leaderAuthority, authority, [
|
|
1176
|
+
"set-option", "-pq", "-t", authority.paneId, "@omx_hud_owner", ownerId,
|
|
1177
|
+
], false);
|
|
1178
|
+
}
|
|
1179
|
+
export function discoverDetachedHudAuthority(sessionName, ownerId) {
|
|
1180
|
+
const rows = execTmuxFileSync([
|
|
1181
|
+
"list-panes", "-a", "-F",
|
|
1182
|
+
"#{pane_id}\t#{pane_dead}\t#{pane_pid}\t#{session_name}\t#{session_id}\t#{session_created}\t#{window_id}\t#{@omx_hud_owner}",
|
|
1183
|
+
], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
1184
|
+
const matches = rows.split("\n").map((line) => line.trim()).filter(Boolean).flatMap((line) => {
|
|
1185
|
+
const [paneId, paneDead, panePidRaw, discoveredSessionName, sessionId, sessionCreated, windowId, owner] = line.split("\t");
|
|
1186
|
+
const panePid = Number(panePidRaw);
|
|
1187
|
+
if (paneDead !== "0" || discoveredSessionName !== sessionName || owner !== ownerId || !/^%\d+$/.test(paneId ?? "") || !/^\$\d+$/.test(sessionId ?? "") || !/^\d+$/.test(sessionCreated ?? "") || !/^@\d+$/.test(windowId ?? "") || !Number.isSafeInteger(panePid) || panePid <= 0)
|
|
1188
|
+
return [];
|
|
1189
|
+
return [{ paneId: paneId, panePid, sessionId: sessionId, sessionCreated: sessionCreated, windowId: windowId, operationMarker: randomUUID() }];
|
|
1190
|
+
});
|
|
1191
|
+
if (matches.length > 1)
|
|
1192
|
+
throw new Error("multiple detached HUD ownership tags found");
|
|
1193
|
+
return matches[0];
|
|
1194
|
+
}
|
|
1195
|
+
function buildDeferredDetachedHudGuard(leaderAuthority, hudAuthority, command) {
|
|
1196
|
+
const deferredReceipt = detachedAuthorityReceipt();
|
|
1197
|
+
let guardedSinkCount = 0;
|
|
1198
|
+
const guardedResize = (_match, sink) => {
|
|
1199
|
+
guardedSinkCount += 1;
|
|
1200
|
+
// The stored hook payload is consumed by one outer tmux format pass when
|
|
1201
|
+
// run-shell executes it. The injected authority conditions must survive that
|
|
1202
|
+
// pass so they are evaluated at execution time against the guarded panes;
|
|
1203
|
+
// unescaped, tmux pre-expands them against the hook's own pane and collapses
|
|
1204
|
+
// them to constants, denying the resize before the sink is parsed (#3292).
|
|
1205
|
+
const outerFormatEscaped = (condition) => condition.replaceAll("#{", "##{");
|
|
1206
|
+
const success = `${sink} ; display-message -p ${quoteShellArg(deferredReceipt)}`;
|
|
1207
|
+
const hudGuard = `if-shell -F -t ${quoteShellArg(hudAuthority.paneId)} ${quoteShellArg(outerFormatEscaped(detachedHudAuthorityCondition(hudAuthority)))} ${quoteShellArg(success)} ${quoteShellArg("")}`;
|
|
1208
|
+
return `tmux if-shell -F -t ${quoteShellArg(leaderAuthority.paneId)} ${quoteShellArg(outerFormatEscaped(detachedLeaderAuthorityCondition(leaderAuthority)))} ${quoteShellArg(hudGuard)} ${quoteShellArg("")}`;
|
|
1209
|
+
};
|
|
1210
|
+
const outerFormatEscapedCommand = command
|
|
1211
|
+
.replaceAll('#{pane_id}', '##{pane_id}')
|
|
1212
|
+
.replaceAll('#{pane_dead}', '##{pane_dead}')
|
|
1213
|
+
.replaceAll('#{pane_pid}', '##{pane_pid}');
|
|
1214
|
+
const guardedCommand = outerFormatEscapedCommand.replace(/tmux (resize-pane -t %[0-9]+ -y [1-9][0-9]*)/g, guardedResize);
|
|
1215
|
+
if (guardedSinkCount === 0) {
|
|
1216
|
+
throw new Error("detached deferred HUD mutation lacks a recognized resize sink");
|
|
1217
|
+
}
|
|
1218
|
+
return guardedCommand;
|
|
1219
|
+
}
|
|
1220
|
+
export function guardDetachedHudDeferredMutation(leaderAuthority, hudAuthority, args) {
|
|
1221
|
+
if (args[0] === "set-hook" && typeof args.at(-1) === "string") {
|
|
1222
|
+
const hookCommand = args.at(-1);
|
|
1223
|
+
const runShellPrefix = "run-shell -b ";
|
|
1224
|
+
if (!hookCommand.startsWith(runShellPrefix)) {
|
|
1225
|
+
throw new Error("detached deferred HUD hook lacks a run-shell command");
|
|
1226
|
+
}
|
|
1227
|
+
const quotedPayload = hookCommand.slice(runShellPrefix.length);
|
|
1228
|
+
if (!quotedPayload.startsWith("'") || !quotedPayload.endsWith("'")) {
|
|
1229
|
+
throw new Error("detached deferred HUD hook has an invalid run-shell payload");
|
|
1230
|
+
}
|
|
1231
|
+
const payload = quotedPayload
|
|
1232
|
+
.slice(1, -1)
|
|
1233
|
+
.replaceAll(`'"'"'`, "'")
|
|
1234
|
+
.replaceAll("'\\''", "'");
|
|
1235
|
+
const guardedPayload = buildDeferredDetachedHudGuard(leaderAuthority, hudAuthority, payload);
|
|
1236
|
+
return [...args.slice(0, -1), `${runShellPrefix}${quoteShellArg(guardedPayload)}`];
|
|
1237
|
+
}
|
|
1238
|
+
if (args[0] === "run-shell") {
|
|
1239
|
+
const commandIndex = args.length - 1;
|
|
1240
|
+
return [...args.slice(0, commandIndex), buildDeferredDetachedHudGuard(leaderAuthority, hudAuthority, args[commandIndex])];
|
|
1241
|
+
}
|
|
1242
|
+
return args;
|
|
1243
|
+
}
|
|
1244
|
+
function runDetachedLeaderSplit(authority, args) {
|
|
1245
|
+
const receipt = detachedAuthorityReceipt();
|
|
1246
|
+
const operationMarker = randomUUID();
|
|
1247
|
+
const splitArgs = [...args];
|
|
1248
|
+
const targetIndex = splitArgs.indexOf("-t");
|
|
1249
|
+
if (targetIndex < 0 || splitArgs[targetIndex + 1] !== authority.sessionName)
|
|
1250
|
+
throw new Error("invalid detached HUD split target");
|
|
1251
|
+
splitArgs[targetIndex + 1] = authority.paneId;
|
|
1252
|
+
const formatIndex = splitArgs.indexOf("-F");
|
|
1253
|
+
if (formatIndex < 0 || splitArgs[formatIndex + 1] !== "#{pane_id}")
|
|
1254
|
+
throw new Error("invalid detached HUD split receipt format");
|
|
1255
|
+
const commandIndex = splitArgs.length - 1;
|
|
1256
|
+
splitArgs[commandIndex] = `env OMX_DETACHED_HUD_OPERATION=${operationMarker} ${splitArgs[commandIndex]}`;
|
|
1257
|
+
splitArgs[formatIndex + 1] = `#{pane_id}\t#{pane_pid}\t#{session_id}\t#{window_id}\t${receipt}`;
|
|
1258
|
+
const output = execTmuxFileSync([
|
|
1259
|
+
"if-shell", "-F", "-t", authority.paneId, detachedLeaderAuthorityCondition(authority),
|
|
1260
|
+
splitArgs.map(quoteShellArg).join(" "), "display-message -p ''",
|
|
1261
|
+
], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
1262
|
+
const [paneId, panePid, sessionId, windowId, observedReceipt, ...extra] = output.split("\t");
|
|
1263
|
+
if (!/^%[0-9]+$/.test(paneId) || !/^[1-9][0-9]*$/.test(panePid) || !/^\$[0-9]+$/.test(sessionId)
|
|
1264
|
+
|| !/^@[0-9]+$/.test(windowId) || observedReceipt !== receipt || extra.length !== 0) {
|
|
1265
|
+
throw new Error("detached leader authority changed before HUD split");
|
|
1266
|
+
}
|
|
1267
|
+
const parsedPid = Number(panePid);
|
|
1268
|
+
if (!Number.isSafeInteger(parsedPid) || parsedPid <= 0)
|
|
1269
|
+
throw new Error("malformed detached HUD authority");
|
|
1270
|
+
return { paneId, panePid: parsedPid, sessionId, windowId, operationMarker };
|
|
1271
|
+
}
|
|
1002
1272
|
function readTmuxEnvValueForTarget(targetPaneId) {
|
|
1003
1273
|
if (!targetPaneId.startsWith("%"))
|
|
1004
1274
|
return undefined;
|
|
@@ -1086,29 +1356,6 @@ function clearDetachedTmuxSessionHistoryIfUnattached(sessionName, leaderPaneId)
|
|
|
1086
1356
|
logCliOperationFailure(err);
|
|
1087
1357
|
}
|
|
1088
1358
|
}
|
|
1089
|
-
function readTmuxSessionInstanceId(sessionName) {
|
|
1090
|
-
try {
|
|
1091
|
-
return execTmuxFileSync(["show-options", "-qv", "-t", sessionName, OMX_INSTANCE_OPTION], {
|
|
1092
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
1093
|
-
encoding: "utf-8",
|
|
1094
|
-
}).trim();
|
|
1095
|
-
}
|
|
1096
|
-
catch {
|
|
1097
|
-
return null;
|
|
1098
|
-
}
|
|
1099
|
-
}
|
|
1100
|
-
function tmuxPaneBelongsToSession(paneId, sessionName) {
|
|
1101
|
-
try {
|
|
1102
|
-
const paneSessionName = execTmuxFileSync(["display-message", "-p", "-t", paneId, "#{session_name}"], {
|
|
1103
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
1104
|
-
encoding: "utf-8",
|
|
1105
|
-
}).trim();
|
|
1106
|
-
return paneSessionName === sessionName;
|
|
1107
|
-
}
|
|
1108
|
-
catch {
|
|
1109
|
-
return false;
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
1359
|
function buildDetachedHistoryPruneHookCommand(leaderPaneId) {
|
|
1113
1360
|
// The leader pane can be gone by the time the hook fires (e.g. crashed
|
|
1114
1361
|
// leader with a lingering session); suppress errors so tmux does not queue
|
|
@@ -1132,12 +1379,30 @@ function hasErrnoCode(error, code) {
|
|
|
1132
1379
|
function isMissingTmuxLaunchNoise(error) {
|
|
1133
1380
|
return error instanceof Error && /spawnSync tmux ENOENT/i.test(error.message);
|
|
1134
1381
|
}
|
|
1382
|
+
function reportOrdinaryLaunchRootConflict(error, cwd, ordinaryLaunch) {
|
|
1383
|
+
if (!ordinaryLaunch || !isSessionPointerLaunchAbort(error)
|
|
1384
|
+
|| error.code !== "session_pointer_owner_conflict"
|
|
1385
|
+
|| getBaseStateDirWithSource(cwd).rootSource !== "cwd-default")
|
|
1386
|
+
return;
|
|
1387
|
+
console.error("[omx] concurrent conversations in this checkout require distinct user-specified OMX_ROOT values.\n" +
|
|
1388
|
+
"[omx] Choose a distinct directory and set OMX_ROOT before launching the additional conversation.\n" +
|
|
1389
|
+
"[omx] POSIX: OMX_ROOT=\"$HOME/.omx/instances/second-conversation\" omx\n" +
|
|
1390
|
+
"[omx] PowerShell: $env:OMX_ROOT = \"$HOME/.omx/instances/second-conversation\"; omx\n" +
|
|
1391
|
+
"[omx] cmd.exe: set \"OMX_ROOT=%USERPROFILE%\\.omx\\instances\\second-conversation\" && omx\n" +
|
|
1392
|
+
"[omx] The selected OMX_ROOT is used literally; OMX does not reroute or allocate one automatically.");
|
|
1393
|
+
}
|
|
1135
1394
|
function logCliOperationFailure(error) {
|
|
1136
1395
|
if (isMissingTmuxLaunchNoise(error))
|
|
1137
1396
|
return;
|
|
1138
1397
|
process.stderr.write(`[cli/index] operation failed: ${error}
|
|
1139
1398
|
`);
|
|
1140
1399
|
}
|
|
1400
|
+
function logEstablishmentCleanupEvidence(error) {
|
|
1401
|
+
const cleanup = error?.establishmentCleanup;
|
|
1402
|
+
if (!cleanup)
|
|
1403
|
+
return;
|
|
1404
|
+
process.stderr.write(`[omx] establishment cleanup evidence: ${JSON.stringify(cleanup)}\n`);
|
|
1405
|
+
}
|
|
1141
1406
|
function tmuxFailureMessage(error) {
|
|
1142
1407
|
if (!error || typeof error !== "object")
|
|
1143
1408
|
return String(error);
|
|
@@ -1152,77 +1417,18 @@ function isUnsupportedTmuxExtendedKeysFailure(error) {
|
|
|
1152
1417
|
return (message.includes("extended-keys") &&
|
|
1153
1418
|
/(?:invalid|unknown|unsupported) (?:option|flag|argument)|no such option|unknown option/.test(message));
|
|
1154
1419
|
}
|
|
1155
|
-
function
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
|
|
1162
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1163
|
-
encoding: "utf-8",
|
|
1164
|
-
});
|
|
1165
|
-
return { usable: true };
|
|
1166
|
-
}
|
|
1167
|
-
catch (err) {
|
|
1168
|
-
const reason = tmuxFailureMessage(err);
|
|
1169
|
-
if (isBenignMissingTmuxServerMessage(reason)) {
|
|
1170
|
-
return { usable: true };
|
|
1171
|
-
}
|
|
1172
|
-
return { usable: false, reason };
|
|
1173
|
-
}
|
|
1174
|
-
}
|
|
1175
|
-
function warnDetachedTmuxFallback(reason) {
|
|
1176
|
-
const suffix = reason ? ` (${reason})` : "";
|
|
1177
|
-
console.warn(`[omx] warning: tmux is installed but its server/socket is unusable${suffix}. Falling back to direct Codex launch.`);
|
|
1178
|
-
}
|
|
1179
|
-
const QUICK_ATTACH_NOOP_THRESHOLD_MS = 2_000;
|
|
1180
|
-
function isWslWindowsTerminalEnvironment(env) {
|
|
1181
|
-
return Boolean(env.WT_SESSION?.trim() &&
|
|
1182
|
-
(env.WSL_INTEROP?.trim() ||
|
|
1183
|
-
env.WSL_DISTRO_NAME?.trim() ||
|
|
1184
|
-
env.WSLENV?.trim()));
|
|
1185
|
-
}
|
|
1186
|
-
function readDetachedSessionAttachedClientCount(sessionName) {
|
|
1187
|
-
try {
|
|
1188
|
-
const output = execTmuxFileSync(["display-message", "-p", "-t", sessionName, "#{session_attached}"], {
|
|
1189
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1190
|
-
encoding: "utf-8",
|
|
1191
|
-
}).trim();
|
|
1192
|
-
const parsed = Number.parseInt(output, 10);
|
|
1193
|
-
return Number.isFinite(parsed) ? parsed : null;
|
|
1194
|
-
}
|
|
1195
|
-
catch (err) {
|
|
1196
|
-
logCliOperationFailure(err);
|
|
1197
|
-
return null;
|
|
1420
|
+
function probeDetachedTmuxTransport() {
|
|
1421
|
+
const { result } = spawnPlatformCommandSync("tmux", ["-V"], {
|
|
1422
|
+
encoding: "utf-8",
|
|
1423
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
1424
|
+
});
|
|
1425
|
+
if (result.error || result.status !== 0) {
|
|
1426
|
+
throw new DetachedTransportUnavailable();
|
|
1198
1427
|
}
|
|
1199
1428
|
}
|
|
1200
|
-
function assertDetachedAttachDidNotNoop(sessionName, elapsedMs, env) {
|
|
1201
|
-
if (!isWslWindowsTerminalEnvironment(env))
|
|
1202
|
-
return;
|
|
1203
|
-
if (elapsedMs >= QUICK_ATTACH_NOOP_THRESHOLD_MS)
|
|
1204
|
-
return;
|
|
1205
|
-
const attachedClients = readDetachedSessionAttachedClientCount(sessionName);
|
|
1206
|
-
if (attachedClients === null || attachedClients > 0)
|
|
1207
|
-
return;
|
|
1208
|
-
throw new Error([
|
|
1209
|
-
"tmux attach-session returned immediately without attaching a client",
|
|
1210
|
-
`(session=${sessionName}).`,
|
|
1211
|
-
"This can happen on WSL2 under Windows Terminal.",
|
|
1212
|
-
"Falling back to direct Codex launch.",
|
|
1213
|
-
].join(" "));
|
|
1214
|
-
}
|
|
1215
1429
|
function resolveTmuxAwareLaunchPolicy(explicitLaunchPolicy, nativeWindows) {
|
|
1216
|
-
const launchPolicy = resolveCodexLaunchPolicy(process.env, process.platform,
|
|
1217
|
-
|
|
1218
|
-
return { launchPolicy, effectiveExplicitLaunchPolicy: explicitLaunchPolicy };
|
|
1219
|
-
}
|
|
1220
|
-
const tmuxHealth = checkDetachedTmuxLaunchHealth();
|
|
1221
|
-
if (tmuxHealth.usable) {
|
|
1222
|
-
return { launchPolicy, effectiveExplicitLaunchPolicy: explicitLaunchPolicy };
|
|
1223
|
-
}
|
|
1224
|
-
warnDetachedTmuxFallback(tmuxHealth.reason);
|
|
1225
|
-
return { launchPolicy: "direct", effectiveExplicitLaunchPolicy: "direct" };
|
|
1430
|
+
const launchPolicy = resolveCodexLaunchPolicy(process.env, process.platform, Boolean(resolveTmuxBinaryForPlatform()), nativeWindows, undefined, undefined, explicitLaunchPolicy);
|
|
1431
|
+
return { launchPolicy, effectiveExplicitLaunchPolicy: explicitLaunchPolicy };
|
|
1226
1432
|
}
|
|
1227
1433
|
export function resolveSignalExitCode(signal) {
|
|
1228
1434
|
if (!signal)
|
|
@@ -1452,15 +1658,6 @@ export function resolveOmxRootForLaunch(cwd, env = process.env) {
|
|
|
1452
1658
|
function resolveLaunchPath(cwd, raw) {
|
|
1453
1659
|
return isCrossPlatformAbsolutePath(raw) ? raw : join(cwd, raw);
|
|
1454
1660
|
}
|
|
1455
|
-
function resolveHudRuntimeRootSource(omxRootOverride, env = process.env) {
|
|
1456
|
-
if (env.OMX_TEAM_STATE_ROOT?.trim())
|
|
1457
|
-
return 'team-env';
|
|
1458
|
-
if (env.OMX_ROOT?.trim() || omxRootOverride)
|
|
1459
|
-
return 'omx-root-env';
|
|
1460
|
-
if (env.OMX_STATE_ROOT?.trim())
|
|
1461
|
-
return 'omx-state-root-env';
|
|
1462
|
-
return 'cwd-default';
|
|
1463
|
-
}
|
|
1464
1661
|
export function resolveHudRuntimeRootForLaunch(cwd, env = process.env) {
|
|
1465
1662
|
const omxTeamStateRoot = env.OMX_TEAM_STATE_ROOT?.trim();
|
|
1466
1663
|
if (omxTeamStateRoot) {
|
|
@@ -1519,6 +1716,8 @@ export function captureMadmaxWorktreeRuntimeContext(options) {
|
|
|
1519
1716
|
const inheritedRoot = resolveInheritedMadmaxRoot(env);
|
|
1520
1717
|
if (!inheritedRoot)
|
|
1521
1718
|
return undefined;
|
|
1719
|
+
if (!madmaxInheritedContextMatchesLaunch(options.sourceCwd, options.originalLaunchArgs, env))
|
|
1720
|
+
return undefined;
|
|
1522
1721
|
const sourceCwd = env.OMX_SOURCE_CWD?.trim() || options.sourceCwd;
|
|
1523
1722
|
const worktreeCwd = options.worktreeCwd?.trim();
|
|
1524
1723
|
const omxStateRoot = env.OMX_STATE_ROOT?.trim();
|
|
@@ -1557,10 +1756,31 @@ function launchArgRequestsDisposableWorktree(arg) {
|
|
|
1557
1756
|
(arg.startsWith("-w") && arg.length > 2);
|
|
1558
1757
|
}
|
|
1559
1758
|
function launchArgsRequestMadmaxIsolation(launchArgs) {
|
|
1560
|
-
|
|
1759
|
+
for (const arg of launchArgs) {
|
|
1760
|
+
if (arg === "--")
|
|
1761
|
+
break;
|
|
1762
|
+
if (arg === MADMAX_FLAG || arg === MADMAX_SPARK_FLAG)
|
|
1763
|
+
return true;
|
|
1764
|
+
}
|
|
1765
|
+
return false;
|
|
1766
|
+
}
|
|
1767
|
+
function launchArgsRequestAuthHotswap(launchArgs) {
|
|
1768
|
+
for (const arg of launchArgs) {
|
|
1769
|
+
if (arg === "--")
|
|
1770
|
+
break;
|
|
1771
|
+
if (arg === "--hotswap")
|
|
1772
|
+
return true;
|
|
1773
|
+
}
|
|
1774
|
+
return false;
|
|
1561
1775
|
}
|
|
1562
1776
|
function launchArgsRequestDisposableWorktree(launchArgs) {
|
|
1563
|
-
|
|
1777
|
+
for (const arg of launchArgs) {
|
|
1778
|
+
if (arg === "--")
|
|
1779
|
+
break;
|
|
1780
|
+
if (launchArgRequestsDisposableWorktree(arg))
|
|
1781
|
+
return true;
|
|
1782
|
+
}
|
|
1783
|
+
return false;
|
|
1564
1784
|
}
|
|
1565
1785
|
function clearInheritedMadmaxRootForDisposableWorktreeLaunch(launchArgs, env = process.env) {
|
|
1566
1786
|
if (!launchArgsRequestDisposableWorktree(launchArgs))
|
|
@@ -1685,31 +1905,77 @@ function readMadmaxDetachedActiveRecord(recordPath) {
|
|
|
1685
1905
|
tmux_session_name: parsed.tmux_session_name,
|
|
1686
1906
|
...(typeof parsed.session_id === "string" ? { session_id: parsed.session_id } : {}),
|
|
1687
1907
|
...(typeof parsed.tmux_pane_id === "string" ? { tmux_pane_id: parsed.tmux_pane_id } : {}),
|
|
1908
|
+
...(typeof parsed.tmux_internal_session_id === "string" ? { tmux_internal_session_id: parsed.tmux_internal_session_id } : {}),
|
|
1909
|
+
...(typeof parsed.tmux_session_created === "string" ? { tmux_session_created: parsed.tmux_session_created } : {}),
|
|
1910
|
+
...(typeof parsed.tmux_pane_pid === "number" ? { tmux_pane_pid: parsed.tmux_pane_pid } : {}),
|
|
1688
1911
|
...(typeof parsed.worktree_cwd === "string" ? { worktree_cwd: parsed.worktree_cwd } : {}),
|
|
1912
|
+
...(typeof parsed.launch_nonce === "string" ? { launch_nonce: parsed.launch_nonce } : {}),
|
|
1913
|
+
...(typeof parsed.leader_pid === "number" && Number.isSafeInteger(parsed.leader_pid) && parsed.leader_pid > 0 ? { leader_pid: parsed.leader_pid } : {}),
|
|
1914
|
+
...(typeof parsed.base_state_root === "string" ? { base_state_root: parsed.base_state_root } : {}),
|
|
1915
|
+
...(parsed.lifecycle_phase === "ready" || parsed.lifecycle_phase === "terminal" ? { lifecycle_phase: parsed.lifecycle_phase } : {}),
|
|
1689
1916
|
};
|
|
1690
1917
|
}
|
|
1691
1918
|
catch {
|
|
1692
1919
|
return null;
|
|
1693
1920
|
}
|
|
1694
1921
|
}
|
|
1922
|
+
function queryMadmaxDetachedRuntimeBinding(record) {
|
|
1923
|
+
if (!record.tmux_pane_id || !record.session_id)
|
|
1924
|
+
return null;
|
|
1925
|
+
try {
|
|
1926
|
+
const output = execTmuxFileSync([
|
|
1927
|
+
"list-panes", "-a", "-F",
|
|
1928
|
+
"#{pane_id}\t#{pane_dead}\t#{pane_pid}\t#{session_name}\t#{session_id}\t#{session_created}\t#{@omx_instance_id}",
|
|
1929
|
+
], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
1930
|
+
let binding = null;
|
|
1931
|
+
const seen = new Set();
|
|
1932
|
+
for (const line of output.split("\n").filter(Boolean)) {
|
|
1933
|
+
const fields = line.split("\t");
|
|
1934
|
+
if (fields.length !== 7)
|
|
1935
|
+
return null;
|
|
1936
|
+
const [paneId, dead, panePidRaw, sessionName, internalSessionId, sessionCreated, omxInstanceId] = fields;
|
|
1937
|
+
const panePid = Number(panePidRaw);
|
|
1938
|
+
if (!/^%[0-9]+$/.test(paneId) || seen.has(paneId) || (dead !== "0" && dead !== "1")
|
|
1939
|
+
|| !Number.isSafeInteger(panePid) || panePid <= 0 || !sessionName
|
|
1940
|
+
|| !/^\$[0-9]+$/.test(internalSessionId) || !/^[0-9]+$/.test(sessionCreated))
|
|
1941
|
+
return null;
|
|
1942
|
+
seen.add(paneId);
|
|
1943
|
+
if (paneId !== record.tmux_pane_id)
|
|
1944
|
+
continue;
|
|
1945
|
+
if (dead !== "0" || sessionName !== record.tmux_session_name || omxInstanceId !== record.session_id)
|
|
1946
|
+
return null;
|
|
1947
|
+
binding = { paneId, panePid, sessionName, internalSessionId, sessionCreated, omxInstanceId };
|
|
1948
|
+
}
|
|
1949
|
+
return binding;
|
|
1950
|
+
}
|
|
1951
|
+
catch {
|
|
1952
|
+
return null;
|
|
1953
|
+
}
|
|
1954
|
+
}
|
|
1955
|
+
function hasMatchingMadmaxDetachedRuntimeBinding(record) {
|
|
1956
|
+
const binding = queryMadmaxDetachedRuntimeBinding(record);
|
|
1957
|
+
return Boolean(binding
|
|
1958
|
+
&& binding.panePid === record.tmux_pane_pid
|
|
1959
|
+
&& binding.internalSessionId === record.tmux_internal_session_id
|
|
1960
|
+
&& binding.sessionCreated === record.tmux_session_created);
|
|
1961
|
+
}
|
|
1695
1962
|
function isReusableMadmaxDetachedActiveRecord(record) {
|
|
1696
|
-
if (!
|
|
1963
|
+
if (record.lifecycle_phase !== "ready" || !record.leader_pid || !record.base_state_root || !record.launch_nonce)
|
|
1697
1964
|
return false;
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1965
|
+
try {
|
|
1966
|
+
process.kill(record.leader_pid, 0);
|
|
1967
|
+
}
|
|
1968
|
+
catch {
|
|
1701
1969
|
return false;
|
|
1702
1970
|
}
|
|
1703
|
-
return tmuxPaneBelongsToSession(record.tmux_pane_id, record.tmux_session_name);
|
|
1704
|
-
}
|
|
1705
|
-
function detachedTmuxSessionExists(sessionName) {
|
|
1706
1971
|
try {
|
|
1707
|
-
|
|
1708
|
-
|
|
1972
|
+
if (realpathSync(record.base_state_root) !== record.base_state_root)
|
|
1973
|
+
return false;
|
|
1709
1974
|
}
|
|
1710
1975
|
catch {
|
|
1711
1976
|
return false;
|
|
1712
1977
|
}
|
|
1978
|
+
return hasMatchingMadmaxDetachedRuntimeBinding(record);
|
|
1713
1979
|
}
|
|
1714
1980
|
function readMadmaxDetachedLockOwner(lockPath) {
|
|
1715
1981
|
try {
|
|
@@ -1778,7 +2044,7 @@ function inspectMadmaxDetachedContextLock(lockPath) {
|
|
|
1778
2044
|
diagnostic: `lock has no readable owner pid yet; lock age ${Math.round(ageMs)}ms`,
|
|
1779
2045
|
};
|
|
1780
2046
|
}
|
|
1781
|
-
export function withMadmaxDetachedContextLock(runsRoot, contextKey, run, options = {}) {
|
|
2047
|
+
export async function withMadmaxDetachedContextLock(runsRoot, contextKey, run, options = {}) {
|
|
1782
2048
|
const lockPath = join(runsRoot, MADMAX_DETACHED_ACTIVE_DIR, `${contextKey}.lock`);
|
|
1783
2049
|
const maxAttempts = options.maxAttempts ?? MADMAX_DETACHED_LOCK_MAX_ATTEMPTS;
|
|
1784
2050
|
const retryMs = options.retryMs ?? MADMAX_DETACHED_LOCK_RETRY_MS;
|
|
@@ -1796,7 +2062,7 @@ export function withMadmaxDetachedContextLock(runsRoot, contextKey, run, options
|
|
|
1796
2062
|
};
|
|
1797
2063
|
writeFileSync(join(lockPath, "owner.json"), `${JSON.stringify(owner, null, 2)}\n`, { mode: 0o600 });
|
|
1798
2064
|
writeFileSync(join(lockPath, "pid"), String(process.pid));
|
|
1799
|
-
return run();
|
|
2065
|
+
return await run();
|
|
1800
2066
|
}
|
|
1801
2067
|
finally {
|
|
1802
2068
|
rmSync(lockPath, { recursive: true, force: true });
|
|
@@ -1824,6 +2090,7 @@ function readMadmaxRunMetadata(runRoot) {
|
|
|
1824
2090
|
const parsed = JSON.parse(readFileSync(join(runRoot, ".omxbox-run.json"), "utf-8"));
|
|
1825
2091
|
return {
|
|
1826
2092
|
...(typeof parsed.cwd === "string" ? { cwd: parsed.cwd } : {}),
|
|
2093
|
+
...(typeof parsed.source_cwd === "string" ? { source_cwd: parsed.source_cwd } : {}),
|
|
1827
2094
|
...(typeof parsed.detached_launch_context === "string"
|
|
1828
2095
|
? { detached_launch_context: parsed.detached_launch_context }
|
|
1829
2096
|
: {}),
|
|
@@ -1851,13 +2118,27 @@ function madmaxInheritedContextMatchesLaunch(cwd, launchArgs, env) {
|
|
|
1851
2118
|
return false;
|
|
1852
2119
|
if (metadata.cwd && metadata.cwd !== inheritedRoot)
|
|
1853
2120
|
return false;
|
|
2121
|
+
const sourceCwd = env.OMX_SOURCE_CWD?.trim();
|
|
2122
|
+
if (!sourceCwd || !metadata.source_cwd || metadata.source_cwd !== sourceCwd)
|
|
2123
|
+
return false;
|
|
1854
2124
|
if (metadata.detached_launch_context !== context)
|
|
1855
2125
|
return false;
|
|
1856
2126
|
const expectedContext = buildMadmaxDetachedLaunchContextKey(cwd, [...launchArgs], inheritedRoot);
|
|
1857
2127
|
return expectedContext === context;
|
|
1858
2128
|
}
|
|
1859
|
-
function
|
|
1860
|
-
|
|
2129
|
+
function resolveVerifiedMadmaxDetachedContext(cwd, launchArgs, env) {
|
|
2130
|
+
if (!madmaxInheritedContextMatchesLaunch(cwd, launchArgs, env))
|
|
2131
|
+
return undefined;
|
|
2132
|
+
const inheritedRoot = resolveInheritedMadmaxRoot(env);
|
|
2133
|
+
const context = env[OMX_MADMAX_DETACHED_CONTEXT_ENV]?.trim();
|
|
2134
|
+
if (!inheritedRoot || !context)
|
|
2135
|
+
return undefined;
|
|
2136
|
+
return {
|
|
2137
|
+
root: resolveLaunchPath(cwd, inheritedRoot),
|
|
2138
|
+
sourceCwd: env.OMX_SOURCE_CWD?.trim() || cwd,
|
|
2139
|
+
context,
|
|
2140
|
+
runsRoot: resolveMadmaxRunsRoot(env),
|
|
2141
|
+
};
|
|
1861
2142
|
}
|
|
1862
2143
|
function cleanupCurrentMadmaxReuseRunRoot(env, runsRoot) {
|
|
1863
2144
|
const runRoot = env.OMX_ROOT;
|
|
@@ -1869,15 +2150,236 @@ function cleanupCurrentMadmaxReuseRunRoot(env, runsRoot) {
|
|
|
1869
2150
|
rmSync(runRoot, { recursive: true, force: true });
|
|
1870
2151
|
}
|
|
1871
2152
|
function writeMadmaxDetachedActiveRecord(recordPath, record) {
|
|
1872
|
-
|
|
1873
|
-
|
|
2153
|
+
const bytes = `${JSON.stringify(record, null, 2)}\n`;
|
|
2154
|
+
const tempPath = `${recordPath}.${process.pid}.${randomUUID()}.tmp`;
|
|
2155
|
+
let fd;
|
|
2156
|
+
mkdirSync(dirname(recordPath), { recursive: true, mode: 0o700 });
|
|
2157
|
+
try {
|
|
2158
|
+
fd = openSync(tempPath, "wx", 0o600);
|
|
2159
|
+
writeFileSync(fd, bytes, "utf-8");
|
|
2160
|
+
try {
|
|
2161
|
+
fsyncSync(fd);
|
|
2162
|
+
}
|
|
2163
|
+
catch (error) {
|
|
2164
|
+
if (!(process.platform === "win32" && error.code === "EPERM"))
|
|
2165
|
+
throw error;
|
|
2166
|
+
}
|
|
2167
|
+
closeSync(fd);
|
|
2168
|
+
fd = undefined;
|
|
2169
|
+
renameSync(tempPath, recordPath);
|
|
2170
|
+
const directoryFd = openSync(dirname(recordPath), "r");
|
|
2171
|
+
try {
|
|
2172
|
+
try {
|
|
2173
|
+
fsyncSync(directoryFd);
|
|
2174
|
+
}
|
|
2175
|
+
catch (error) {
|
|
2176
|
+
if (!(process.platform === "win32" && error.code === "EPERM"))
|
|
2177
|
+
throw error;
|
|
2178
|
+
}
|
|
2179
|
+
}
|
|
2180
|
+
finally {
|
|
2181
|
+
closeSync(directoryFd);
|
|
2182
|
+
}
|
|
2183
|
+
return {
|
|
2184
|
+
bytes,
|
|
2185
|
+
digest: createHash("sha256").update(bytes).digest("hex"),
|
|
2186
|
+
nonce: record.launch_nonce ?? "",
|
|
2187
|
+
};
|
|
2188
|
+
}
|
|
2189
|
+
finally {
|
|
2190
|
+
if (fd !== undefined)
|
|
2191
|
+
closeSync(fd);
|
|
2192
|
+
try {
|
|
2193
|
+
unlinkSync(tempPath);
|
|
2194
|
+
}
|
|
2195
|
+
catch { }
|
|
2196
|
+
}
|
|
1874
2197
|
}
|
|
1875
|
-
|
|
1876
|
-
|
|
2198
|
+
function detachedFailureCode(error) {
|
|
2199
|
+
const code = error && typeof error === "object" && "code" in error
|
|
2200
|
+
? error.code
|
|
2201
|
+
: undefined;
|
|
2202
|
+
return typeof code === "string" && /^(?:EACCES|EAGAIN|EBUSY|ECANCELED|ECONNREFUSED|EEXIST|EINTR|EINVAL|EIO|EISDIR|EMFILE|ENFILE|ENOENT|ENOSPC|ENOTDIR|EPERM|EPIPE|EROFS|ESRCH|ETIMEDOUT)$/.test(code) ? code : "unknown";
|
|
2203
|
+
}
|
|
2204
|
+
export function describeDetachedLeaderFailure(error) {
|
|
2205
|
+
const describe = (value, depth) => {
|
|
2206
|
+
if (depth > 4)
|
|
2207
|
+
return "nested failure";
|
|
2208
|
+
if (value instanceof AggregateError) {
|
|
2209
|
+
return [value.message, ...[...value.errors].map((child) => describe(child, depth + 1))]
|
|
2210
|
+
.filter(Boolean)
|
|
2211
|
+
.join(": ");
|
|
2212
|
+
}
|
|
2213
|
+
if (value instanceof Error)
|
|
2214
|
+
return value.message;
|
|
2215
|
+
return String(value);
|
|
2216
|
+
};
|
|
2217
|
+
return describe(error, 0).replace(/[\r\n\t]+/g, " ").slice(0, 1_024);
|
|
2218
|
+
}
|
|
2219
|
+
export class DetachedLaunchSafetyError extends Error {
|
|
2220
|
+
phase;
|
|
2221
|
+
cause;
|
|
2222
|
+
report;
|
|
2223
|
+
name = "DetachedLaunchSafetyError";
|
|
2224
|
+
constructor(phase, cause, report) {
|
|
2225
|
+
super(`detached launch safety failure during ${phase}${phase === "completion" && cause instanceof Error ? `: ${cause.message}` : ""}`);
|
|
2226
|
+
this.phase = phase;
|
|
2227
|
+
this.cause = cause;
|
|
2228
|
+
this.report = report;
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
export class DetachedTransportUnavailable extends Error {
|
|
2232
|
+
reason;
|
|
2233
|
+
name = "DetachedTransportUnavailable";
|
|
2234
|
+
noSideEffects = true;
|
|
2235
|
+
constructor(reason) {
|
|
2236
|
+
super("detached transport is unavailable before launch side effects");
|
|
2237
|
+
this.reason = reason;
|
|
2238
|
+
}
|
|
2239
|
+
}
|
|
2240
|
+
/** Internal detached-launch seam. Exported solely for deterministic CLI tests. */
|
|
2241
|
+
export async function executeDetachedLaunchStateMachine(input, deps) {
|
|
2242
|
+
const report = input.preflight.report;
|
|
2243
|
+
const transition = (name) => { report.transitions.push(name); };
|
|
2244
|
+
let binding;
|
|
2245
|
+
let ownedRecord;
|
|
2246
|
+
let released = false;
|
|
2247
|
+
let retainedAuthority = false;
|
|
2248
|
+
let rollbackAuthorized = false;
|
|
2249
|
+
try {
|
|
2250
|
+
transition("D1");
|
|
2251
|
+
binding = await deps.establish();
|
|
2252
|
+
retainedAuthority = true;
|
|
2253
|
+
transition("D2");
|
|
2254
|
+
const completion = await deps.complete(binding);
|
|
2255
|
+
if (completion.kind === "failure") {
|
|
2256
|
+
const failures = [completion.error];
|
|
2257
|
+
try {
|
|
2258
|
+
report.rollback.attempted.push("setup-finalization");
|
|
2259
|
+
await deps.finalizeSetupFailure(binding);
|
|
2260
|
+
}
|
|
2261
|
+
catch (finalizationError) {
|
|
2262
|
+
report.rollback.failures.push({ step: "setup-finalization", status: "failed", code: detachedFailureCode(finalizationError) });
|
|
2263
|
+
failures.push(finalizationError);
|
|
2264
|
+
}
|
|
2265
|
+
throw new DetachedLaunchSafetyError("completion", new AggregateError(failures, `preLaunch ${completion.operation} failed: ${describeDetachedLeaderFailure(completion.error)}`), report);
|
|
2266
|
+
}
|
|
2267
|
+
transition("D3");
|
|
2268
|
+
const inertSession = await deps.createInertSession();
|
|
2269
|
+
transition("D4");
|
|
2270
|
+
const pane = await deps.capturePane(inertSession);
|
|
2271
|
+
transition("D5");
|
|
2272
|
+
if (await deps.updateNameMetadata(binding, inertSession) !== "committed-released") {
|
|
2273
|
+
throw new DetachedLaunchSafetyError("name-metadata", new Error("detached name metadata did not release"), report);
|
|
2274
|
+
}
|
|
2275
|
+
transition("D6");
|
|
2276
|
+
if (await deps.updatePaneMetadata(binding, pane) !== "committed-released") {
|
|
2277
|
+
throw new DetachedLaunchSafetyError("pane-metadata", new Error("detached pane metadata did not release"), report);
|
|
2278
|
+
}
|
|
2279
|
+
transition("D7");
|
|
2280
|
+
ownedRecord = await deps.publishActiveRecord(inertSession, pane);
|
|
2281
|
+
transition("D9");
|
|
2282
|
+
try {
|
|
2283
|
+
await deps.releaseBarrier();
|
|
2284
|
+
released = true;
|
|
2285
|
+
}
|
|
2286
|
+
catch (releaseError) {
|
|
2287
|
+
// A failed publication must not let the outer launcher destroy the
|
|
2288
|
+
// leader's finalization capability. Request a nonce-bound abort and
|
|
2289
|
+
// preserve all artifacts on timeout, malformed, or foreign reports.
|
|
2290
|
+
let resolution;
|
|
2291
|
+
try {
|
|
2292
|
+
resolution = await deps.abortAndAwaitFinalization?.(releaseError);
|
|
2293
|
+
}
|
|
2294
|
+
catch (abortError) {
|
|
2295
|
+
released = true;
|
|
2296
|
+
throw new AggregateError([releaseError, abortError], "detached release and abort publication both failed");
|
|
2297
|
+
}
|
|
2298
|
+
rollbackAuthorized = resolution?.acknowledged === true && Boolean(resolution.nonce && resolution.sessionId && resolution.sessionName &&
|
|
2299
|
+
Number.isSafeInteger(resolution.leaderPid) && resolution.kind);
|
|
2300
|
+
if (!rollbackAuthorized)
|
|
2301
|
+
released = true;
|
|
2302
|
+
throw releaseError;
|
|
2303
|
+
}
|
|
2304
|
+
transition("D10");
|
|
2305
|
+
await deps.attachOrReturn(inertSession, input.preflight.shouldAttach);
|
|
2306
|
+
return { kind: input.preflight.shouldAttach ? "attached" : "returned", released: true, report };
|
|
2307
|
+
}
|
|
2308
|
+
catch (error) {
|
|
2309
|
+
deps.diagnostic?.(error);
|
|
2310
|
+
if (retainedAuthority && !rollbackAuthorized && !released) {
|
|
2311
|
+
try {
|
|
2312
|
+
const resolution = await deps.abortAndAwaitFinalization?.(error);
|
|
2313
|
+
rollbackAuthorized = resolution?.rollbackAuthorized === true || (resolution?.acknowledged === true && Boolean(resolution.nonce && resolution.sessionId && resolution.sessionName &&
|
|
2314
|
+
Number.isSafeInteger(resolution.leaderPid) && resolution.kind));
|
|
2315
|
+
if (!rollbackAuthorized)
|
|
2316
|
+
released = true;
|
|
2317
|
+
}
|
|
2318
|
+
catch (abortError) {
|
|
2319
|
+
released = true;
|
|
2320
|
+
deps.diagnostic?.(abortError);
|
|
2321
|
+
}
|
|
2322
|
+
}
|
|
2323
|
+
if (!released) {
|
|
2324
|
+
try {
|
|
2325
|
+
report.rollback.attempted.push("rollback");
|
|
2326
|
+
await deps.rollback(ownedRecord, report);
|
|
2327
|
+
}
|
|
2328
|
+
catch (rollbackError) {
|
|
2329
|
+
report.rollback.failures.push({ step: "rollback", status: "failed", code: detachedFailureCode(rollbackError) });
|
|
2330
|
+
deps.diagnostic?.(rollbackError);
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
if (error instanceof DetachedLaunchSafetyError) {
|
|
2334
|
+
throw new DetachedLaunchSafetyError(error.phase, error.cause, report);
|
|
2335
|
+
}
|
|
2336
|
+
const phase = report.transitions.at(-1);
|
|
2337
|
+
const phaseName = phase === "D1" ? "establishment"
|
|
2338
|
+
: phase === "D2" ? "completion"
|
|
2339
|
+
: phase === "D3" ? "inert-session"
|
|
2340
|
+
: phase === "D4" ? "pane-id"
|
|
2341
|
+
: phase === "D5" ? "name-metadata"
|
|
2342
|
+
: phase === "D6" ? "pane-metadata"
|
|
2343
|
+
: phase === "D7" ? "active-record"
|
|
2344
|
+
: phase === "D9" ? "barrier-release"
|
|
2345
|
+
: "post-release-attach";
|
|
2346
|
+
throw new DetachedLaunchSafetyError(phaseName, error, report);
|
|
2347
|
+
}
|
|
1877
2348
|
}
|
|
1878
2349
|
class MadmaxDetachedGuardError extends Error {
|
|
1879
2350
|
failClosed = true;
|
|
1880
2351
|
}
|
|
2352
|
+
function preflightDetachedLaunch() {
|
|
2353
|
+
// D0 must remain transport-only: `tmux -V` neither connects to nor creates a server.
|
|
2354
|
+
probeDetachedTmuxTransport();
|
|
2355
|
+
const report = { transitions: ["D0"], rollback: { attempted: [], failures: [] } };
|
|
2356
|
+
return Object.freeze({
|
|
2357
|
+
kind: "available",
|
|
2358
|
+
shouldAttach: shouldAttachDetachedTmuxSession(process.env),
|
|
2359
|
+
report,
|
|
2360
|
+
});
|
|
2361
|
+
}
|
|
2362
|
+
function handleMadmaxDetachedReuse(runtimeContext, preflight) {
|
|
2363
|
+
const contextKey = runtimeContext?.madmaxDetachedContext ?? process.env[OMX_MADMAX_DETACHED_CONTEXT_ENV]?.trim();
|
|
2364
|
+
const activeRecordPath = contextKey ? madmaxDetachedActiveRecordPath(resolveMadmaxRunsRoot(process.env), contextKey) : null;
|
|
2365
|
+
const activeRecord = activeRecordPath ? readMadmaxDetachedActiveRecord(activeRecordPath) : null;
|
|
2366
|
+
if (!activeRecord || activeRecord.context_key !== contextKey || !isReusableMadmaxDetachedActiveRecord(activeRecord))
|
|
2367
|
+
return false;
|
|
2368
|
+
cleanupCurrentMadmaxReuseRunRoot(process.env, resolveMadmaxRunsRoot(process.env));
|
|
2369
|
+
setDetachedTmuxSessionHistoryLimit(activeRecord.tmux_session_name, activeRecord.tmux_pane_id);
|
|
2370
|
+
if (!preflight.shouldAttach) {
|
|
2371
|
+
clearDetachedTmuxSessionHistoryIfUnattached(activeRecord.tmux_session_name, activeRecord.tmux_pane_id);
|
|
2372
|
+
return true;
|
|
2373
|
+
}
|
|
2374
|
+
process.stderr.write(`[omx] madmax detached launch already active for this context; attaching ${activeRecord.tmux_session_name} instead of starting a duplicate.\n`);
|
|
2375
|
+
try {
|
|
2376
|
+
execTmuxFileSync(["attach-session", "-t", activeRecord.tmux_session_name], { stdio: "inherit" });
|
|
2377
|
+
}
|
|
2378
|
+
catch (error) {
|
|
2379
|
+
throw new DetachedLaunchSafetyError("post-release-attach", error, preflight.report);
|
|
2380
|
+
}
|
|
2381
|
+
return true;
|
|
2382
|
+
}
|
|
1881
2383
|
export function createMadmaxIsolatedRoot(sourceCwd, argv, env = process.env) {
|
|
1882
2384
|
const runsRoot = resolveMadmaxRunsRoot(env);
|
|
1883
2385
|
mkdirSync(runsRoot, { recursive: true });
|
|
@@ -1908,6 +2410,23 @@ function activateMadmaxIsolationIfNeeded(command, launchArgs, cwd, env = process
|
|
|
1908
2410
|
env.OMX_SOURCE_CWD = cwd;
|
|
1909
2411
|
process.stderr.write(`[omx] madmax isolated state: ${runDir} (source: ${cwd})\n`);
|
|
1910
2412
|
}
|
|
2413
|
+
function renderDetachedLaunchSafetyReport(error) {
|
|
2414
|
+
const establishmentCleanup = error.report.establishmentCleanup?.capability.map((entry) => ({
|
|
2415
|
+
role: entry.role,
|
|
2416
|
+
phase: entry.phase,
|
|
2417
|
+
status: entry.status,
|
|
2418
|
+
...(entry.error ? { failureCode: detachedFailureCode(entry.error) } : {}),
|
|
2419
|
+
}));
|
|
2420
|
+
return JSON.stringify({
|
|
2421
|
+
phase: error.phase,
|
|
2422
|
+
transitions: error.report.transitions,
|
|
2423
|
+
...(establishmentCleanup ? { establishmentCleanup } : {}),
|
|
2424
|
+
rollback: {
|
|
2425
|
+
attempted: error.report.rollback.attempted,
|
|
2426
|
+
failures: error.report.rollback.failures.map(({ step, status, code }) => ({ step, status, code })),
|
|
2427
|
+
},
|
|
2428
|
+
});
|
|
2429
|
+
}
|
|
1911
2430
|
export async function main(args) {
|
|
1912
2431
|
const knownCommands = new Set([
|
|
1913
2432
|
"launch",
|
|
@@ -1916,6 +2435,7 @@ export async function main(args) {
|
|
|
1916
2435
|
"imagegen",
|
|
1917
2436
|
"capabilities",
|
|
1918
2437
|
"setup",
|
|
2438
|
+
"__detached-session-leader",
|
|
1919
2439
|
"update",
|
|
1920
2440
|
"list",
|
|
1921
2441
|
"agents",
|
|
@@ -1954,7 +2474,8 @@ export async function main(args) {
|
|
|
1954
2474
|
]);
|
|
1955
2475
|
const firstArg = args[0];
|
|
1956
2476
|
const { command, launchArgs } = resolveCliInvocation(args);
|
|
1957
|
-
const
|
|
2477
|
+
const { omxArgs } = splitOmxArgsAtEndOfOptions(args);
|
|
2478
|
+
const flags = new Set(omxArgs.filter((arg) => arg.startsWith("--")));
|
|
1958
2479
|
const options = {
|
|
1959
2480
|
force: flags.has("--force"),
|
|
1960
2481
|
mergeAgents: undefined,
|
|
@@ -1966,11 +2487,27 @@ export async function main(args) {
|
|
|
1966
2487
|
console.log(HELP);
|
|
1967
2488
|
return;
|
|
1968
2489
|
}
|
|
1969
|
-
|
|
2490
|
+
if (command !== "launch" && command !== "resume") {
|
|
2491
|
+
activateMadmaxIsolationIfNeeded(command, launchArgs, process.cwd(), process.env);
|
|
2492
|
+
}
|
|
1970
2493
|
try {
|
|
2494
|
+
if (command === "session") {
|
|
2495
|
+
await sessionCommand(args.slice(1));
|
|
2496
|
+
return;
|
|
2497
|
+
}
|
|
2498
|
+
if (command === "resume" && launchArgs.some((arg) => arg === "--help" || arg === "-h")) {
|
|
2499
|
+
console.log(RESUME_HELP);
|
|
2500
|
+
return;
|
|
2501
|
+
}
|
|
1971
2502
|
switch (command) {
|
|
2503
|
+
case "__detached-session-leader": {
|
|
2504
|
+
const payload = decodeDetachedLeaderPayload(launchArgs[0]);
|
|
2505
|
+
await runDetachedSessionLeader(payload);
|
|
2506
|
+
process.exit(typeof process.exitCode === "number" ? process.exitCode : 0);
|
|
2507
|
+
break;
|
|
2508
|
+
}
|
|
1972
2509
|
case "launch":
|
|
1973
|
-
if (launchArgs
|
|
2510
|
+
if (launchArgsRequestAuthHotswap(launchArgs)) {
|
|
1974
2511
|
await launchWithAuthHotswap(launchArgs);
|
|
1975
2512
|
}
|
|
1976
2513
|
else {
|
|
@@ -2081,9 +2618,6 @@ export async function main(args) {
|
|
|
2081
2618
|
case "team":
|
|
2082
2619
|
await teamCommand(args.slice(1), options);
|
|
2083
2620
|
break;
|
|
2084
|
-
case "session":
|
|
2085
|
-
await sessionCommand(args.slice(1));
|
|
2086
|
-
break;
|
|
2087
2621
|
case "url":
|
|
2088
2622
|
await urlCommand(args.slice(1));
|
|
2089
2623
|
break;
|
|
@@ -2167,7 +2701,13 @@ export async function main(args) {
|
|
|
2167
2701
|
}
|
|
2168
2702
|
}
|
|
2169
2703
|
catch (err) {
|
|
2170
|
-
|
|
2704
|
+
if (err instanceof DetachedLaunchSafetyError) {
|
|
2705
|
+
console.error(`Error: ${err.message}`);
|
|
2706
|
+
console.error(`[omx] detached launch safety report: ${renderDetachedLaunchSafetyReport(err)}`);
|
|
2707
|
+
}
|
|
2708
|
+
else {
|
|
2709
|
+
console.error(`Error: ${err instanceof Error ? err.message : err}`);
|
|
2710
|
+
}
|
|
2171
2711
|
process.exit(1);
|
|
2172
2712
|
}
|
|
2173
2713
|
}
|
|
@@ -2305,9 +2845,14 @@ async function reasoningCommand(args) {
|
|
|
2305
2845
|
return;
|
|
2306
2846
|
}
|
|
2307
2847
|
if (!REASONING_MODE_SET.has(mode)) {
|
|
2308
|
-
const
|
|
2309
|
-
?
|
|
2310
|
-
:
|
|
2848
|
+
const unsupportedMode = isUnsupportedRootReasoningEffort(mode)
|
|
2849
|
+
? normalizeUnsupportedRootReasoningEffort(mode)
|
|
2850
|
+
: undefined;
|
|
2851
|
+
const guidance = unsupportedMode === "max"
|
|
2852
|
+
? `Reasoning mode "${mode}" is not supported by "omx reasoning".\nPer-agent "max" is configured with agentReasoning; direct -c model_reasoning_effort=... is passed to Codex and remains capability-dependent.\n`
|
|
2853
|
+
: unsupportedMode === "ultra"
|
|
2854
|
+
? `Reasoning mode "${mode}" is not supported by OMX root or per-agent reasoning and is not an alias for "max".\nDirect -c model_reasoning_effort=... remains opaque Codex passthrough.\n`
|
|
2855
|
+
: "";
|
|
2311
2856
|
throw new Error(`${guidance}Invalid reasoning mode "${mode}". Expected one of: ${REASONING_MODES.join(", ")}.\n${REASONING_USAGE}`);
|
|
2312
2857
|
}
|
|
2313
2858
|
const { mkdir, readFile, writeFile } = await import("fs/promises");
|
|
@@ -2321,7 +2866,9 @@ async function reasoningCommand(args) {
|
|
|
2321
2866
|
}
|
|
2322
2867
|
export async function launchWithAuthHotswap(args) {
|
|
2323
2868
|
const launchCwd = process.cwd();
|
|
2324
|
-
const
|
|
2869
|
+
const { omxArgs, suffix } = splitOmxArgsAtEndOfOptions(args);
|
|
2870
|
+
const parsedWorktree = parseWorktreeMode(omxArgs);
|
|
2871
|
+
const hotswapArgs = [...parsedWorktree.remainingArgs, ...suffix];
|
|
2325
2872
|
let cwd = launchCwd;
|
|
2326
2873
|
let worktreeDirty = false;
|
|
2327
2874
|
let ensuredLaunchWorktree;
|
|
@@ -2375,7 +2922,7 @@ export async function launchWithAuthHotswap(args) {
|
|
|
2375
2922
|
}
|
|
2376
2923
|
const status = await runAuthHotswap({
|
|
2377
2924
|
cwd,
|
|
2378
|
-
argv:
|
|
2925
|
+
argv: hotswapArgs,
|
|
2379
2926
|
lifecycle: {
|
|
2380
2927
|
prepareCodexHomeForLaunch,
|
|
2381
2928
|
preLaunch: (launchPath, sessionId, notifyTempContract, codexHomeOverride, enableAuthority) => preLaunch(launchPath, sessionId, notifyTempContract, codexHomeOverride, enableAuthority, worktreeDirty),
|
|
@@ -2391,38 +2938,31 @@ export async function launchWithAuthHotswap(args) {
|
|
|
2391
2938
|
process.exitCode = status;
|
|
2392
2939
|
}
|
|
2393
2940
|
export async function launchWithHud(args) {
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
|
|
2404
|
-
|
|
2405
|
-
"[omx] winget install psmux\n" +
|
|
2406
|
-
"[omx] See: https://github.com/marlocarlo/psmux");
|
|
2407
|
-
}
|
|
2408
|
-
else {
|
|
2409
|
-
console.warn(`[omx] warning: tmux probe failed on native Windows (${errno.code || errno.message}). Continuing without tmux/HUD.`);
|
|
2410
|
-
}
|
|
2941
|
+
const launchCwd = process.cwd();
|
|
2942
|
+
const { omxArgs, suffix } = splitOmxArgsAtEndOfOptions(args);
|
|
2943
|
+
const parsedWorktree = parseWorktreeMode(omxArgs);
|
|
2944
|
+
const notifyTempResult = resolveNotifyTempContract(parsedWorktree.remainingArgs, process.env);
|
|
2945
|
+
const passthroughArgs = [...notifyTempResult.passthroughArgs, ...suffix];
|
|
2946
|
+
const explicitLaunchPolicy = resolveEffectiveLeaderLaunchPolicyOverride(passthroughArgs, process.env);
|
|
2947
|
+
let { launchPolicy } = resolveTmuxAwareLaunchPolicy(explicitLaunchPolicy, isNativeWindows());
|
|
2948
|
+
let detachedPreflight;
|
|
2949
|
+
if (launchPolicy === "detached-tmux") {
|
|
2950
|
+
try {
|
|
2951
|
+
detachedPreflight = preflightDetachedLaunch();
|
|
2411
2952
|
}
|
|
2412
|
-
|
|
2413
|
-
|
|
2414
|
-
|
|
2953
|
+
catch (error) {
|
|
2954
|
+
if (!(error instanceof DetachedTransportUnavailable))
|
|
2955
|
+
throw error;
|
|
2956
|
+
console.warn("[omx] warning: detached tmux transport is unavailable. Falling back to direct Codex launch.");
|
|
2957
|
+
launchPolicy = "direct";
|
|
2415
2958
|
}
|
|
2416
2959
|
}
|
|
2417
|
-
const launchCwd = process.cwd();
|
|
2418
|
-
const parsedWorktree = parseWorktreeMode(args);
|
|
2419
|
-
const notifyTempResult = resolveNotifyTempContract(parsedWorktree.remainingArgs, process.env);
|
|
2420
|
-
const explicitLaunchPolicy = resolveEffectiveLeaderLaunchPolicyOverride(notifyTempResult.passthroughArgs, process.env);
|
|
2421
|
-
const persistentCodexHomeForLaunch = resolveCodexHomeForLaunch(launchCwd, process.env);
|
|
2422
|
-
const { launchPolicy, effectiveExplicitLaunchPolicy } = resolveTmuxAwareLaunchPolicy(explicitLaunchPolicy, isNativeWindows());
|
|
2423
2960
|
const enableNotifyFallbackAuthority = launchPolicy === "direct";
|
|
2424
|
-
|
|
2425
|
-
|
|
2961
|
+
activateMadmaxIsolationIfNeeded("launch", args, launchCwd, process.env);
|
|
2962
|
+
const verifiedMadmaxLaunchContext = resolveVerifiedMadmaxDetachedContext(launchCwd, args, process.env);
|
|
2963
|
+
const persistentCodexHomeForLaunch = resolveCodexHomeForLaunch(launchCwd, process.env);
|
|
2964
|
+
const workerSparkModel = resolveWorkerSparkModel(passthroughArgs, persistentCodexHomeForLaunch);
|
|
2965
|
+
let normalizedArgs = normalizeCodexLaunchArgs(passthroughArgs);
|
|
2426
2966
|
let cwd = launchCwd;
|
|
2427
2967
|
let worktreeDirty = false;
|
|
2428
2968
|
let ensuredLaunchWorktree;
|
|
@@ -2451,7 +2991,7 @@ export async function launchWithHud(args) {
|
|
|
2451
2991
|
}
|
|
2452
2992
|
}
|
|
2453
2993
|
const madmaxWorktreeRuntimeContext = captureMadmaxWorktreeRuntimeContext({
|
|
2454
|
-
originalLaunchArgs:
|
|
2994
|
+
originalLaunchArgs: omxArgs,
|
|
2455
2995
|
worktreeEnabled: Boolean(parsedWorktree.mode.enabled && ensuredLaunchWorktree?.enabled),
|
|
2456
2996
|
sourceCwd: launchCwd,
|
|
2457
2997
|
worktreeCwd: ensuredLaunchWorktree?.enabled ? ensuredLaunchWorktree.worktreePath : undefined,
|
|
@@ -2460,6 +3000,8 @@ export async function launchWithHud(args) {
|
|
|
2460
3000
|
clearInheritedMadmaxRootForDisposableWorktreeLaunch(parsedWorktree.remainingArgs);
|
|
2461
3001
|
applyDisposableWorktreeOmxRootForLaunch(ensuredLaunchWorktree);
|
|
2462
3002
|
applyWorktreeToolContextForLaunch(cwd, ensuredLaunchWorktree);
|
|
3003
|
+
if (launchPolicy === "detached-tmux" && detachedPreflight && handleMadmaxDetachedReuse(madmaxWorktreeRuntimeContext, detachedPreflight))
|
|
3004
|
+
return;
|
|
2463
3005
|
const sessionId = `omx-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
2464
3006
|
try {
|
|
2465
3007
|
await maybeCheckAndPromptUpdate(cwd);
|
|
@@ -2501,43 +3043,86 @@ export async function launchWithHud(args) {
|
|
|
2501
3043
|
const codexHomeOverride = preparedCodexHome.codexHomeOverride;
|
|
2502
3044
|
const sqliteHomeOverride = preparedCodexHome.sqliteHomeOverride;
|
|
2503
3045
|
const projectLocalCodexHomeForCleanup = preparedCodexHome.projectLocalCodexHomeForCleanup;
|
|
2504
|
-
|
|
2505
|
-
|
|
2506
|
-
|
|
3046
|
+
let insideTmuxControlPlane;
|
|
3047
|
+
if (launchPolicy === "inside-tmux") {
|
|
3048
|
+
let verifiedTeamContext;
|
|
3049
|
+
try {
|
|
3050
|
+
verifiedTeamContext = await resolveVerifiedDetachedTeamContext(cwd, process.env);
|
|
3051
|
+
}
|
|
3052
|
+
catch {
|
|
3053
|
+
verifiedTeamContext = undefined;
|
|
3054
|
+
}
|
|
3055
|
+
insideTmuxControlPlane = buildDetachedLaunchControlPlane({
|
|
3056
|
+
cwd,
|
|
3057
|
+
sessionId,
|
|
3058
|
+
env: process.env,
|
|
3059
|
+
omxRootOverride: resolveOmxRootForLaunch(cwd, process.env),
|
|
3060
|
+
runtimeContext: madmaxWorktreeRuntimeContext,
|
|
3061
|
+
verifiedMadmaxContext: verifiedMadmaxLaunchContext,
|
|
3062
|
+
leaderPaneId: process.env.TMUX_PANE,
|
|
3063
|
+
verifiedTeamContext,
|
|
3064
|
+
});
|
|
2507
3065
|
}
|
|
2508
|
-
|
|
2509
|
-
|
|
2510
|
-
|
|
3066
|
+
const restoreInsideTmuxControlPlane = insideTmuxControlPlane
|
|
3067
|
+
? applyLaunchOwnedControlPlane(insideTmuxControlPlane)
|
|
3068
|
+
: undefined;
|
|
3069
|
+
let launch;
|
|
3070
|
+
if (launchPolicy !== "detached-tmux") {
|
|
3071
|
+
try {
|
|
3072
|
+
launch = await preLaunch(cwd, sessionId, notifyTempResult.contract, codexHomeOverride, enableNotifyFallbackAuthority, worktreeDirty);
|
|
3073
|
+
}
|
|
3074
|
+
catch (err) {
|
|
3075
|
+
logEstablishmentCleanupEvidence(err);
|
|
3076
|
+
if (isSessionPointerLaunchAbort(err)) {
|
|
3077
|
+
console.error(`[omx] session pointer launch aborted: ${err.code}`);
|
|
3078
|
+
reportOrdinaryLaunchRootConflict(err, cwd, cwd === launchCwd && !parsedWorktree.mode.enabled && !isResumeCodexLaunch(normalizedArgs));
|
|
3079
|
+
}
|
|
3080
|
+
restoreInsideTmuxControlPlane?.();
|
|
2511
3081
|
await cleanupRuntimeCodexHome(preparedCodexHome.runtimeCodexHomeForCleanup, projectLocalCodexHomeForCleanup).catch((cleanupErr) => {
|
|
2512
3082
|
console.error(`[omx] preLaunch abort cleanup warning: ${cleanupErr instanceof Error ? cleanupErr.message : cleanupErr}`);
|
|
2513
3083
|
});
|
|
2514
3084
|
throw err;
|
|
2515
3085
|
}
|
|
2516
|
-
// preLaunch errors after pointer commit must not prevent Codex from starting.
|
|
2517
|
-
console.error(`[omx] preLaunch warning: ${err instanceof Error ? err.message : err}`);
|
|
2518
3086
|
}
|
|
2519
|
-
// ── Phase 2: run ────────────────────────────────────────────────────────
|
|
2520
3087
|
let postLaunchHandledExternally = false;
|
|
2521
3088
|
try {
|
|
2522
3089
|
const notifyTempContractRaw = notifyTempResult.contract.active
|
|
2523
3090
|
? serializeNotifyTempContract(notifyTempResult.contract)
|
|
2524
3091
|
: null;
|
|
2525
|
-
const launchResult = runCodex(cwd, normalizedArgs, sessionId, workerSparkModel, codexHomeOverride, sqliteHomeOverride, notifyTempContractRaw,
|
|
3092
|
+
const launchResult = await runCodex(cwd, normalizedArgs, sessionId, workerSparkModel, codexHomeOverride, sqliteHomeOverride, notifyTempContractRaw, {
|
|
3093
|
+
notifyTempContract: notifyTempResult.contract,
|
|
3094
|
+
enableNotifyFallbackAuthority,
|
|
3095
|
+
worktreeDirty,
|
|
3096
|
+
}, launchPolicy, detachedPreflight, projectLocalCodexHomeForCleanup, preparedCodexHome.runtimeCodexHomeForCleanup, madmaxWorktreeRuntimeContext, verifiedMadmaxLaunchContext, insideTmuxControlPlane);
|
|
2526
3097
|
postLaunchHandledExternally = launchResult.postLaunchHandledExternally;
|
|
2527
3098
|
}
|
|
3099
|
+
catch (error) {
|
|
3100
|
+
if (error instanceof DetachedLaunchSafetyError)
|
|
3101
|
+
postLaunchHandledExternally = true;
|
|
3102
|
+
throw error;
|
|
3103
|
+
}
|
|
2528
3104
|
finally {
|
|
2529
|
-
|
|
2530
|
-
|
|
2531
|
-
|
|
3105
|
+
if (!postLaunchHandledExternally && launch) {
|
|
3106
|
+
try {
|
|
3107
|
+
await postLaunch(cwd, sessionId, launch.binding, codexHomeOverride, enableNotifyFallbackAuthority, projectLocalCodexHomeForCleanup);
|
|
3108
|
+
}
|
|
3109
|
+
finally {
|
|
3110
|
+
restoreInsideTmuxControlPlane?.();
|
|
3111
|
+
}
|
|
2532
3112
|
await cleanupRuntimeCodexHome(preparedCodexHome.runtimeCodexHomeForCleanup, projectLocalCodexHomeForCleanup).catch(logCliOperationFailure);
|
|
2533
3113
|
}
|
|
3114
|
+
else {
|
|
3115
|
+
restoreInsideTmuxControlPlane?.();
|
|
3116
|
+
}
|
|
2534
3117
|
}
|
|
2535
3118
|
}
|
|
2536
3119
|
export async function execWithOverlay(args) {
|
|
2537
3120
|
const launchCwd = process.cwd();
|
|
2538
|
-
const
|
|
3121
|
+
const { omxArgs, suffix } = splitOmxArgsAtEndOfOptions(args);
|
|
3122
|
+
const parsedWorktree = parseWorktreeMode(omxArgs);
|
|
2539
3123
|
const notifyTempResult = resolveNotifyTempContract(parsedWorktree.remainingArgs, process.env);
|
|
2540
|
-
const
|
|
3124
|
+
const passthroughArgs = [...notifyTempResult.passthroughArgs, ...suffix];
|
|
3125
|
+
const normalizedArgs = normalizeCodexLaunchArgs(passthroughArgs);
|
|
2541
3126
|
let cwd = launchCwd;
|
|
2542
3127
|
let worktreeDirty = false;
|
|
2543
3128
|
let ensuredLaunchWorktree;
|
|
@@ -2595,18 +3180,19 @@ export async function execWithOverlay(args) {
|
|
|
2595
3180
|
const codexHomeOverride = preparedCodexHome.codexHomeOverride;
|
|
2596
3181
|
const sqliteHomeOverride = preparedCodexHome.sqliteHomeOverride;
|
|
2597
3182
|
const projectLocalCodexHomeForCleanup = preparedCodexHome.projectLocalCodexHomeForCleanup;
|
|
3183
|
+
let launch;
|
|
2598
3184
|
try {
|
|
2599
|
-
await preLaunch(cwd, sessionId, notifyTempResult.contract, codexHomeOverride, true, worktreeDirty);
|
|
3185
|
+
launch = await preLaunch(cwd, sessionId, notifyTempResult.contract, codexHomeOverride, true, worktreeDirty);
|
|
2600
3186
|
}
|
|
2601
3187
|
catch (err) {
|
|
3188
|
+
logEstablishmentCleanupEvidence(err);
|
|
2602
3189
|
if (isSessionPointerLaunchAbort(err)) {
|
|
2603
3190
|
console.error(`[omx] session pointer launch aborted: ${err.code}`);
|
|
2604
|
-
await cleanupRuntimeCodexHome(preparedCodexHome.runtimeCodexHomeForCleanup, projectLocalCodexHomeForCleanup).catch((cleanupErr) => {
|
|
2605
|
-
console.error(`[omx] preLaunch abort cleanup warning: ${cleanupErr instanceof Error ? cleanupErr.message : cleanupErr}`);
|
|
2606
|
-
});
|
|
2607
|
-
throw err;
|
|
2608
3191
|
}
|
|
2609
|
-
|
|
3192
|
+
await cleanupRuntimeCodexHome(preparedCodexHome.runtimeCodexHomeForCleanup, projectLocalCodexHomeForCleanup).catch((cleanupErr) => {
|
|
3193
|
+
console.error(`[omx] preLaunch abort cleanup warning: ${cleanupErr instanceof Error ? cleanupErr.message : cleanupErr}`);
|
|
3194
|
+
});
|
|
3195
|
+
throw err;
|
|
2610
3196
|
}
|
|
2611
3197
|
try {
|
|
2612
3198
|
const notifyTempContractRaw = notifyTempResult.contract.active
|
|
@@ -2614,27 +3200,34 @@ export async function execWithOverlay(args) {
|
|
|
2614
3200
|
: null;
|
|
2615
3201
|
const codexArgs = injectModelInstructionsBypassArgs(cwd, ["exec", ...normalizedArgs], process.env, sessionModelInstructionsPath(cwd, sessionId));
|
|
2616
3202
|
const omxRootOverride = resolveOmxRootForLaunch(cwd, process.env);
|
|
2617
|
-
const
|
|
2618
|
-
|
|
3203
|
+
const omxBin = resolveOmxCliEntryPath({ argv1: process.argv[1], cwd, env: process.env });
|
|
3204
|
+
if (!omxBin)
|
|
3205
|
+
throw new Error("Unable to resolve OMX launcher path for exec");
|
|
3206
|
+
const hudRuntimeRoot = resolveHudRuntimeRootForLaunch(cwd, process.env);
|
|
3207
|
+
const codexEnvBase = prependOmxRuntimeCommandShimToEnv(cwd, {
|
|
3208
|
+
...stripHermesMcpBridgeEnv(process.env),
|
|
2619
3209
|
...(codexHomeOverride ? { CODEX_HOME: codexHomeOverride } : {}),
|
|
2620
3210
|
...(sqliteHomeOverride ? { [CODEX_SQLITE_HOME_ENV]: sqliteHomeOverride } : {}),
|
|
2621
3211
|
...(omxRootOverride ? { OMX_ROOT: omxRootOverride } : {}),
|
|
3212
|
+
}, omxBin);
|
|
3213
|
+
// Correlates plugin hook routing only; it is not an authority token.
|
|
3214
|
+
const pluginHookRoutingLaunchId = randomUUID();
|
|
3215
|
+
const codexEnv = {
|
|
3216
|
+
...codexEnvBase,
|
|
3217
|
+
OMX_CODEX_LAUNCH_ID: pluginHookRoutingLaunchId,
|
|
3218
|
+
...buildHudRuntimeEnv({ sessionId, ...hudRuntimeRoot }).env,
|
|
3219
|
+
...(notifyTempContractRaw ? { [OMX_NOTIFY_TEMP_CONTRACT_ENV]: notifyTempContractRaw } : {}),
|
|
2622
3220
|
};
|
|
2623
|
-
const codexEnv = notifyTempContractRaw
|
|
2624
|
-
? {
|
|
2625
|
-
...codexEnvBase,
|
|
2626
|
-
[OMX_NOTIFY_TEMP_CONTRACT_ENV]: notifyTempContractRaw,
|
|
2627
|
-
}
|
|
2628
|
-
: codexEnvBase;
|
|
2629
3221
|
runCodexBlocking(cwd, codexArgs, codexEnv);
|
|
2630
3222
|
}
|
|
2631
3223
|
finally {
|
|
2632
|
-
await postLaunch(cwd, sessionId, codexHomeOverride, true, projectLocalCodexHomeForCleanup);
|
|
3224
|
+
await postLaunch(cwd, sessionId, launch.binding, codexHomeOverride, true, projectLocalCodexHomeForCleanup);
|
|
2633
3225
|
await cleanupRuntimeCodexHome(preparedCodexHome.runtimeCodexHomeForCleanup, projectLocalCodexHomeForCleanup).catch(logCliOperationFailure);
|
|
2634
3226
|
}
|
|
2635
3227
|
}
|
|
2636
3228
|
export function normalizeCodexLaunchArgs(args) {
|
|
2637
|
-
const
|
|
3229
|
+
const { omxArgs, suffix } = splitOmxArgsAtEndOfOptions(args);
|
|
3230
|
+
const parsed = parseWorktreeMode(omxArgs);
|
|
2638
3231
|
const launchPolicyParsed = splitLeaderLaunchPolicyArgs(parsed.remainingArgs);
|
|
2639
3232
|
const normalized = [];
|
|
2640
3233
|
let wantsBypass = false;
|
|
@@ -2662,7 +3255,7 @@ export function normalizeCodexLaunchArgs(args) {
|
|
|
2662
3255
|
continue;
|
|
2663
3256
|
}
|
|
2664
3257
|
if (arg === "--max" || arg === "--ultra") {
|
|
2665
|
-
throw
|
|
3258
|
+
throw unsupportedLaunchShorthandError(arg);
|
|
2666
3259
|
}
|
|
2667
3260
|
if (arg === SPARK_FLAG) {
|
|
2668
3261
|
// Spark model is injected into worker env only (not the leader). Consume flag.
|
|
@@ -2681,7 +3274,7 @@ export function normalizeCodexLaunchArgs(args) {
|
|
|
2681
3274
|
if (reasoningMode) {
|
|
2682
3275
|
normalized.push(CONFIG_FLAG, `${REASONING_KEY}="${reasoningMode}"`);
|
|
2683
3276
|
}
|
|
2684
|
-
return normalized;
|
|
3277
|
+
return [...normalized, ...suffix];
|
|
2685
3278
|
}
|
|
2686
3279
|
/**
|
|
2687
3280
|
* Returns the spark model string if --spark or --madmax-spark appears in the
|
|
@@ -2690,6 +3283,8 @@ export function normalizeCodexLaunchArgs(args) {
|
|
|
2690
3283
|
*/
|
|
2691
3284
|
export function resolveWorkerSparkModel(args, codexHomeOverride) {
|
|
2692
3285
|
for (const arg of args) {
|
|
3286
|
+
if (arg === "--")
|
|
3287
|
+
break;
|
|
2693
3288
|
if (arg === SPARK_FLAG || arg === MADMAX_SPARK_FLAG) {
|
|
2694
3289
|
return resolveTeamLowComplexityDefaultModel(codexHomeOverride);
|
|
2695
3290
|
}
|
|
@@ -2830,12 +3425,14 @@ function buildNativeHookBaseContext(cwd, sessionId, normalizedEvent, extra = {})
|
|
|
2830
3425
|
export function injectModelInstructionsBypassArgs(cwd, args, env = process.env, defaultFilePath) {
|
|
2831
3426
|
if (!shouldBypassDefaultSystemPrompt(env))
|
|
2832
3427
|
return [...args];
|
|
2833
|
-
|
|
3428
|
+
const { omxArgs, suffix } = splitOmxArgsAtEndOfOptions(args);
|
|
3429
|
+
if (hasModelInstructionsOverride(omxArgs))
|
|
2834
3430
|
return [...args];
|
|
2835
3431
|
return [
|
|
2836
|
-
...
|
|
3432
|
+
...omxArgs,
|
|
2837
3433
|
CONFIG_FLAG,
|
|
2838
3434
|
buildModelInstructionsOverride(cwd, env, defaultFilePath),
|
|
3435
|
+
...suffix,
|
|
2839
3436
|
];
|
|
2840
3437
|
}
|
|
2841
3438
|
export function collectInheritableTeamWorkerArgs(codexArgs) {
|
|
@@ -2906,9 +3503,6 @@ export function detectDetachedSessionWindowIndex(sessionName) {
|
|
|
2906
3503
|
return null;
|
|
2907
3504
|
}
|
|
2908
3505
|
}
|
|
2909
|
-
function escapeShellDoubleQuotedValue(value) {
|
|
2910
|
-
return value.replace(/["\\$`]/g, "\\$&");
|
|
2911
|
-
}
|
|
2912
3506
|
function sanitizeTmuxLeaseKey(value) {
|
|
2913
3507
|
const cleaned = value
|
|
2914
3508
|
.toLowerCase()
|
|
@@ -3094,96 +3688,163 @@ function withTmuxExtendedKeysLeaseLock(cwd, socketPath, run) {
|
|
|
3094
3688
|
}
|
|
3095
3689
|
throw new Error(`timed out waiting for tmux extended-keys lease lock: ${lockPath}`);
|
|
3096
3690
|
}
|
|
3097
|
-
function buildDetachedSessionLeaderCommand(cwd, sessionName, codexCmd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, parentEnvFilePath) {
|
|
3098
|
-
const
|
|
3099
|
-
|
|
3100
|
-
|
|
3101
|
-
|
|
3102
|
-
|
|
3103
|
-
|
|
3104
|
-
|
|
3105
|
-
|
|
3106
|
-
|
|
3107
|
-
|
|
3108
|
-
|
|
3109
|
-
|
|
3110
|
-
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
3114
|
-
|
|
3115
|
-
|
|
3116
|
-
|
|
3117
|
-
|
|
3118
|
-
|
|
3119
|
-
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
"fi;",
|
|
3125
|
-
"exit $status;",
|
|
3126
|
-
"};",
|
|
3127
|
-
"trap omx_detached_session_cleanup 0 INT TERM HUP;",
|
|
3128
|
-
parentEnvSource,
|
|
3129
|
-
"unset OMX_HERMES_MCP_BRIDGE;",
|
|
3130
|
-
"omx_codex_started_at=$(date +%s 2>/dev/null || printf 0);",
|
|
3131
|
-
`${codexCmd} <&3 &`,
|
|
3132
|
-
"omx_codex_pid=$!;",
|
|
3133
|
-
'wait "$omx_codex_pid";',
|
|
3134
|
-
"omx_codex_status=$?;",
|
|
3135
|
-
"omx_codex_finished_at=$(date +%s 2>/dev/null || printf 0);",
|
|
3136
|
-
'omx_codex_elapsed=$((omx_codex_finished_at - omx_codex_started_at));',
|
|
3137
|
-
'if [ "$omx_codex_status" -eq 0 ] && [ "$omx_codex_elapsed" -le 2 ]; then',
|
|
3138
|
-
'printf "\\n[omx] codex exited immediately with code 0 during startup. The detached tmux session is being kept open so any output above remains visible. Press Enter to close this OMX session.\\n" >&2;',
|
|
3139
|
-
'IFS= read -r _omx_close || true;',
|
|
3140
|
-
'elif [ "$omx_codex_status" -gt 0 ] && [ "$omx_codex_status" -lt 128 ] && [ "$omx_codex_elapsed" -le 2 ]; then',
|
|
3141
|
-
'printf "\\n[omx] codex exited with code %s during startup. The detached tmux session is being kept open so the error above remains visible. Press Enter to close this OMX session.\\n" "$omx_codex_status" >&2;',
|
|
3142
|
-
'IFS= read -r _omx_close || true;',
|
|
3143
|
-
'elif [ "$omx_codex_status" -gt 0 ] && [ "$omx_codex_status" -lt 128 ]; then',
|
|
3144
|
-
'printf "\\n[omx] codex exited with code %s. The detached tmux session is being kept open so the error above remains visible. Press Enter to close this OMX session.\\n" "$omx_codex_status" >&2;',
|
|
3145
|
-
'IFS= read -r _omx_close || true;',
|
|
3146
|
-
"fi;",
|
|
3147
|
-
'exit "$omx_codex_status";',
|
|
3148
|
-
].join(" ");
|
|
3149
|
-
return `/bin/sh -c ${quoteShellArg(wrapped)}`;
|
|
3150
|
-
}
|
|
3151
|
-
function buildDetachedSessionPostLaunchHelperCommand(cwd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup) {
|
|
3152
|
-
const cwdLiteral = JSON.stringify(cwd);
|
|
3153
|
-
const sessionIdLiteral = JSON.stringify(sessionId);
|
|
3154
|
-
const codexHomeLiteral = typeof codexHomeOverride === "string" && codexHomeOverride.length > 0
|
|
3155
|
-
? JSON.stringify(codexHomeOverride)
|
|
3156
|
-
: "undefined";
|
|
3157
|
-
const projectLocalCleanupLiteral = typeof projectLocalCodexHomeForCleanup === "string" &&
|
|
3158
|
-
projectLocalCodexHomeForCleanup.length > 0
|
|
3159
|
-
? JSON.stringify(projectLocalCodexHomeForCleanup)
|
|
3160
|
-
: "undefined";
|
|
3161
|
-
const runtimeCodexHomeCleanupLiteral = typeof runtimeCodexHomeForCleanup === "string" &&
|
|
3162
|
-
runtimeCodexHomeForCleanup.length > 0
|
|
3163
|
-
? JSON.stringify(runtimeCodexHomeForCleanup)
|
|
3164
|
-
: "undefined";
|
|
3165
|
-
const moduleUrlLiteral = JSON.stringify(import.meta.url);
|
|
3166
|
-
const script = [
|
|
3167
|
-
`const mod = await import(${moduleUrlLiteral});`,
|
|
3168
|
-
`await mod.runDetachedSessionPostLaunch(${cwdLiteral}, ${sessionIdLiteral}, ${codexHomeLiteral}, ${projectLocalCleanupLiteral}, ${runtimeCodexHomeCleanupLiteral});`,
|
|
3169
|
-
].join(" ");
|
|
3170
|
-
return `${quoteShellArg(process.execPath)} --input-type=module -e ${quoteShellArg(script)}`;
|
|
3171
|
-
}
|
|
3172
|
-
function execTmuxSync(args, execFileSyncImpl = (file, tmuxArgs) => execFileSync(file, tmuxArgs, {
|
|
3173
|
-
encoding: "utf-8",
|
|
3174
|
-
...(process.platform === "win32" ? { windowsHide: true } : {}),
|
|
3175
|
-
})) {
|
|
3176
|
-
return execFileSyncImpl(resolveTmuxExecutableForLaunch(), [...args]).trim();
|
|
3691
|
+
function buildDetachedSessionLeaderCommand(cwd, sessionName, codexCmd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, parentEnvFilePath, readyPath, preLaunchOptions, omxBin = process.argv[1]) {
|
|
3692
|
+
const payload = Buffer.from(JSON.stringify({
|
|
3693
|
+
cwd, sessionName, sessionId, codexCmd, codexHomeOverride,
|
|
3694
|
+
projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, parentEnvFilePath,
|
|
3695
|
+
readyPath, preLaunchOptions,
|
|
3696
|
+
})).toString("base64url");
|
|
3697
|
+
return `/bin/sh -c ${quoteShellArg(`${parentEnvFilePath ? `if [ -r ${quoteShellArg(parentEnvFilePath)} ]; then . ${quoteShellArg(parentEnvFilePath)}; rm -f ${quoteShellArg(parentEnvFilePath)}; fi; ` : ""}exec ${quoteShellArg(process.execPath)} ${quoteShellArg(omxBin)} __detached-session-leader ${quoteShellArg(payload)}`)}`;
|
|
3698
|
+
}
|
|
3699
|
+
function buildDetachedWindowsLeaderCommand(cwd, sessionName, codexCmd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, parentEnv, readyPath, preLaunchOptions, omxBin = process.argv[1]) {
|
|
3700
|
+
const payload = Buffer.from(JSON.stringify({
|
|
3701
|
+
cwd, sessionName, sessionId, codexCmd, codexHomeOverride,
|
|
3702
|
+
projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup,
|
|
3703
|
+
...(parentEnv ? { parentEnv: detachedSessionParentEnvironment(parentEnv, true) } : {}),
|
|
3704
|
+
readyPath, preLaunchOptions,
|
|
3705
|
+
})).toString("base64url");
|
|
3706
|
+
const invocation = `& ${quotePowerShellArg(process.execPath)} ${quotePowerShellArg(omxBin)} __detached-session-leader ${quotePowerShellArg(payload)}`;
|
|
3707
|
+
return `powershell.exe -NoLogo -NoProfile -NonInteractive -Command ${quotePowerShellArg(invocation)}`;
|
|
3708
|
+
}
|
|
3709
|
+
export function isDetachedReadyReportAuthorized(report, expected) {
|
|
3710
|
+
if (report?.kind !== "ready" || report.nonce !== expected.nonce
|
|
3711
|
+
|| report.sessionId !== expected.sessionId || report.sessionName !== expected.sessionName
|
|
3712
|
+
|| typeof report.leaderPid !== "number" || report.leaderPid <= 0)
|
|
3713
|
+
return false;
|
|
3714
|
+
if (expected.shouldAttach && expected.leaderPanePid !== null) {
|
|
3715
|
+
return report.leaderPid === expected.leaderPanePid;
|
|
3716
|
+
}
|
|
3717
|
+
return report.paneId === expected.leaderPaneId;
|
|
3177
3718
|
}
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3719
|
+
function writeDetachedLeaderReport(path, report) {
|
|
3720
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
3721
|
+
const tempPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
3722
|
+
let fd;
|
|
3182
3723
|
try {
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3724
|
+
fd = openSync(tempPath, "wx", 0o600);
|
|
3725
|
+
writeFileSync(fd, `${JSON.stringify(report)}\n`, "utf-8");
|
|
3726
|
+
try {
|
|
3727
|
+
fsyncSync(fd);
|
|
3728
|
+
}
|
|
3729
|
+
catch (error) {
|
|
3730
|
+
if (!(process.platform === "win32" && error.code === "EPERM"))
|
|
3731
|
+
throw error;
|
|
3732
|
+
}
|
|
3733
|
+
closeSync(fd);
|
|
3734
|
+
fd = undefined;
|
|
3735
|
+
renameSync(tempPath, path);
|
|
3736
|
+
const directoryFd = openSync(dirname(path), "r");
|
|
3737
|
+
try {
|
|
3738
|
+
try {
|
|
3739
|
+
fsyncSync(directoryFd);
|
|
3740
|
+
}
|
|
3741
|
+
catch (error) {
|
|
3742
|
+
if (!(process.platform === "win32" && error.code === "EPERM"))
|
|
3743
|
+
throw error;
|
|
3744
|
+
}
|
|
3745
|
+
}
|
|
3746
|
+
finally {
|
|
3747
|
+
closeSync(directoryFd);
|
|
3748
|
+
}
|
|
3749
|
+
}
|
|
3750
|
+
finally {
|
|
3751
|
+
if (fd !== undefined)
|
|
3752
|
+
closeSync(fd);
|
|
3753
|
+
try {
|
|
3754
|
+
unlinkSync(tempPath);
|
|
3755
|
+
}
|
|
3756
|
+
catch { }
|
|
3757
|
+
}
|
|
3758
|
+
}
|
|
3759
|
+
function readDetachedLeaderReport(path) {
|
|
3760
|
+
try {
|
|
3761
|
+
const value = JSON.parse(readFileSync(path, "utf8"));
|
|
3762
|
+
if (!value || typeof value !== "object")
|
|
3763
|
+
return undefined;
|
|
3764
|
+
const report = value;
|
|
3765
|
+
if (report.version !== 1 || !["ready", "failed", "terminal", "release", "abort"].includes(String(report.kind)) ||
|
|
3766
|
+
typeof report.nonce !== "string" || typeof report.sessionId !== "string" || typeof report.sessionName !== "string")
|
|
3767
|
+
return undefined;
|
|
3768
|
+
if (report.paneId !== undefined && typeof report.paneId !== "string")
|
|
3769
|
+
return undefined;
|
|
3770
|
+
if (report.leaderPid !== undefined && (!Number.isSafeInteger(report.leaderPid) || Number(report.leaderPid) <= 0))
|
|
3771
|
+
return undefined;
|
|
3772
|
+
if (report.exitStatus !== undefined && (!Number.isSafeInteger(report.exitStatus) || Number(report.exitStatus) < 0 || Number(report.exitStatus) > 255))
|
|
3773
|
+
return undefined;
|
|
3774
|
+
return report;
|
|
3775
|
+
}
|
|
3776
|
+
catch {
|
|
3777
|
+
return undefined;
|
|
3778
|
+
}
|
|
3779
|
+
}
|
|
3780
|
+
/** Internal detached-launch seam. Exported solely for deterministic CLI tests. */
|
|
3781
|
+
export function publishDetachedReleaseMarker(releaseMarkerPath, nonce, sessionId, sessionName, leaderPid, hud) {
|
|
3782
|
+
writeDetachedLeaderReport(`${releaseMarkerPath}.release`, {
|
|
3783
|
+
version: 1, kind: "release", nonce, sessionId, sessionName, leaderPid, ...(hud ? { hud } : {}),
|
|
3784
|
+
});
|
|
3785
|
+
}
|
|
3786
|
+
/** Internal detached-launch seam. Exported solely for deterministic CLI tests. */
|
|
3787
|
+
export function applyDetachedTerminalExitStatus(releaseMarkerPath, expected) {
|
|
3788
|
+
const report = readDetachedLeaderReport(releaseMarkerPath);
|
|
3789
|
+
if (!report || report.kind !== "terminal" || report.finalized !== true)
|
|
3790
|
+
return false;
|
|
3791
|
+
if (report.nonce !== expected.nonce || report.sessionId !== expected.sessionId ||
|
|
3792
|
+
report.sessionName !== expected.sessionName || report.leaderPid !== expected.leaderPid)
|
|
3793
|
+
return false;
|
|
3794
|
+
if (typeof report.exitStatus !== "number")
|
|
3795
|
+
return false;
|
|
3796
|
+
process.exitCode = report.exitStatus;
|
|
3797
|
+
return true;
|
|
3798
|
+
}
|
|
3799
|
+
/** Internal detached-launch seam. Exported solely for deterministic CLI tests. */
|
|
3800
|
+
export function probeExactDetachedSessionExists(authority) {
|
|
3801
|
+
if (!authority)
|
|
3802
|
+
return undefined;
|
|
3803
|
+
try {
|
|
3804
|
+
// A retained dead pane (remain-on-exit) keeps its pid/topology, so prove
|
|
3805
|
+
// liveness first: a dead leader with no valid terminal report is a failure,
|
|
3806
|
+
// never a manual detach.
|
|
3807
|
+
const paneDead = execTmuxFileSync(["display-message", "-p", "-t", authority.paneId, "#{pane_dead}"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] }).trim();
|
|
3808
|
+
if (paneDead === "1")
|
|
3809
|
+
return false;
|
|
3810
|
+
if (paneDead !== "0")
|
|
3811
|
+
return undefined;
|
|
3812
|
+
// Re-capture the exact leader-pane topology the bootstrap authority recorded.
|
|
3813
|
+
// A matching recapture proves the owned session and its leader survive (manual
|
|
3814
|
+
// detach); a destroyed session, recycled pane id, or tmux error fails closed.
|
|
3815
|
+
const current = captureDetachedLeaderAuthority(authority.paneId, authority.sessionName, authority.ownerId);
|
|
3816
|
+
return current.panePid === authority.panePid && current.sessionId === authority.sessionId
|
|
3817
|
+
&& current.sessionCreated === authority.sessionCreated && current.windowId === authority.windowId
|
|
3818
|
+
&& current.windowIndex === authority.windowIndex;
|
|
3819
|
+
}
|
|
3820
|
+
catch {
|
|
3821
|
+
return undefined;
|
|
3822
|
+
}
|
|
3823
|
+
}
|
|
3824
|
+
/** Internal detached-launch seam. Exported solely for deterministic CLI tests. */
|
|
3825
|
+
export function resolveDetachedAttachExitStatus(releaseMarkerPath, expected, probe) {
|
|
3826
|
+
if (applyDetachedTerminalExitStatus(releaseMarkerPath, expected))
|
|
3827
|
+
return "applied";
|
|
3828
|
+
return probe.exactSessionExists() === true ? "manual-detach" : "protocol-failure";
|
|
3829
|
+
}
|
|
3830
|
+
function publishDetachedAbortMarker(releaseMarkerPath, nonce, sessionId, sessionName, leaderPid) {
|
|
3831
|
+
writeDetachedLeaderReport(`${releaseMarkerPath}.abort`, { version: 1, kind: "abort", nonce, sessionId, sessionName, leaderPid });
|
|
3832
|
+
}
|
|
3833
|
+
function execTmuxSync(args, execFileSyncImpl = (file, tmuxArgs) => execFileSync(file, tmuxArgs, {
|
|
3834
|
+
encoding: "utf-8",
|
|
3835
|
+
...(process.platform === "win32" ? { windowsHide: true } : {}),
|
|
3836
|
+
})) {
|
|
3837
|
+
return execFileSyncImpl(resolveTmuxExecutableForLaunch(), [...args]).trim();
|
|
3838
|
+
}
|
|
3839
|
+
export function acquireTmuxExtendedKeysLease(cwd, execFileSyncImpl = (file, tmuxArgs) => execFileSync(file, tmuxArgs, {
|
|
3840
|
+
encoding: "utf-8",
|
|
3841
|
+
...(process.platform === "win32" ? { windowsHide: true } : {}),
|
|
3842
|
+
}), ownerPid = process.pid) {
|
|
3843
|
+
try {
|
|
3844
|
+
const socketPath = resolveTmuxSocketPath(execFileSyncImpl);
|
|
3845
|
+
const leasePath = tmuxExtendedKeysLeasePath(cwd, socketPath);
|
|
3846
|
+
const holderPid = Number.isSafeInteger(ownerPid) && ownerPid > 0 ? ownerPid : process.pid;
|
|
3847
|
+
const leaseId = `${holderPid}-${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
3187
3848
|
withTmuxExtendedKeysLeaseLock(cwd, socketPath, () => {
|
|
3188
3849
|
const stateRaw = readTmuxExtendedKeysLeaseState(leasePath);
|
|
3189
3850
|
const state = stateRaw ? reapDeadTmuxExtendedKeysLeaseHolders(stateRaw) : null;
|
|
@@ -3253,19 +3914,123 @@ export function releaseTmuxExtendedKeysLease(cwd, leaseHandle, execFileSyncImpl
|
|
|
3253
3914
|
}
|
|
3254
3915
|
}
|
|
3255
3916
|
}
|
|
3256
|
-
|
|
3257
|
-
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3263
|
-
|
|
3264
|
-
|
|
3265
|
-
|
|
3917
|
+
export const DETACHED_LAUNCH_CONTROL_PLANE_KEYS = [
|
|
3918
|
+
"OMX_SESSION_ID",
|
|
3919
|
+
"CODEX_SESSION_ID",
|
|
3920
|
+
"SESSION_ID",
|
|
3921
|
+
"OMX_TMUX_HUD_OWNER",
|
|
3922
|
+
"OMX_TMUX_HUD_LEADER_PANE",
|
|
3923
|
+
"OMX_ROOT",
|
|
3924
|
+
"OMX_STATE_ROOT",
|
|
3925
|
+
"OMX_TEAM_STATE_ROOT",
|
|
3926
|
+
"OMXBOX_ACTIVE",
|
|
3927
|
+
"OMX_SOURCE_CWD",
|
|
3928
|
+
"OMX_MADMAX_DETACHED_CONTEXT",
|
|
3929
|
+
"OMX_RUNS_DIR",
|
|
3930
|
+
"OMX_TEAM_LEADER_CWD",
|
|
3931
|
+
"OMX_TEAM_WORKER",
|
|
3932
|
+
"OMX_TEAM_INTERNAL_WORKER",
|
|
3933
|
+
];
|
|
3934
|
+
const DETACHED_LAUNCH_CONTROL_PLANE_KEY_SET = new Set(DETACHED_LAUNCH_CONTROL_PLANE_KEYS);
|
|
3935
|
+
function isDetachedLaunchControlPlaneKey(key, caseInsensitive = false) {
|
|
3936
|
+
return caseInsensitive
|
|
3937
|
+
? DETACHED_LAUNCH_CONTROL_PLANE_KEY_SET.has(key.toUpperCase())
|
|
3938
|
+
: DETACHED_LAUNCH_CONTROL_PLANE_KEY_SET.has(key);
|
|
3939
|
+
}
|
|
3940
|
+
function emptyDetachedLaunchControlPlaneValues() {
|
|
3941
|
+
return Object.fromEntries(DETACHED_LAUNCH_CONTROL_PLANE_KEYS.map((key) => [key, ""]));
|
|
3942
|
+
}
|
|
3943
|
+
function resolveDetachedRootValue(cwd, raw) {
|
|
3944
|
+
return isCrossPlatformAbsolutePath(raw) ? raw : resolve(cwd, raw);
|
|
3945
|
+
}
|
|
3946
|
+
export function buildDetachedLaunchControlPlane(options) {
|
|
3947
|
+
const env = options.env ?? process.env;
|
|
3948
|
+
const values = emptyDetachedLaunchControlPlaneValues();
|
|
3949
|
+
values.OMX_SESSION_ID = options.sessionId;
|
|
3950
|
+
values.OMX_TMUX_HUD_OWNER = "1";
|
|
3951
|
+
values.OMX_TMUX_HUD_LEADER_PANE = options.leaderPaneId ?? "";
|
|
3952
|
+
const runtimeContext = options.runtimeContext;
|
|
3953
|
+
const verifiedMadmaxContext = options.verifiedMadmaxContext;
|
|
3954
|
+
if (runtimeContext && verifiedMadmaxContext) {
|
|
3955
|
+
values.OMX_ROOT = verifiedMadmaxContext.root;
|
|
3956
|
+
values.OMXBOX_ACTIVE = "1";
|
|
3957
|
+
values.OMX_SOURCE_CWD = verifiedMadmaxContext.sourceCwd;
|
|
3958
|
+
values.OMX_MADMAX_DETACHED_CONTEXT = verifiedMadmaxContext.context;
|
|
3959
|
+
values.OMX_RUNS_DIR = verifiedMadmaxContext.runsRoot;
|
|
3960
|
+
}
|
|
3961
|
+
else {
|
|
3962
|
+
const madmaxGuardPresent = env.OMXBOX_ACTIVE === "1";
|
|
3963
|
+
const teamRoot = env.OMX_TEAM_STATE_ROOT?.trim();
|
|
3964
|
+
const root = env.OMX_ROOT?.trim();
|
|
3965
|
+
const stateRoot = env.OMX_STATE_ROOT?.trim();
|
|
3966
|
+
const resolvedStateRoot = stateRoot ? resolveDetachedRootValue(options.cwd, stateRoot) : undefined;
|
|
3967
|
+
const explicitOverrideWins = Boolean(options.omxRootOverride?.trim()
|
|
3968
|
+
&& !teamRoot
|
|
3969
|
+
&& !root
|
|
3970
|
+
&& (!resolvedStateRoot || resolveDetachedRootValue(options.cwd, options.omxRootOverride.trim()) !== resolvedStateRoot));
|
|
3971
|
+
if (teamRoot)
|
|
3972
|
+
values.OMX_TEAM_STATE_ROOT = resolveDetachedRootValue(options.cwd, teamRoot);
|
|
3973
|
+
if (!madmaxGuardPresent) {
|
|
3974
|
+
if (explicitOverrideWins)
|
|
3975
|
+
values.OMX_ROOT = resolveDetachedRootValue(options.cwd, options.omxRootOverride.trim());
|
|
3976
|
+
else if (!teamRoot && root)
|
|
3977
|
+
values.OMX_ROOT = resolveDetachedRootValue(options.cwd, root);
|
|
3978
|
+
else if (!teamRoot && stateRoot)
|
|
3979
|
+
values.OMX_STATE_ROOT = resolvedStateRoot;
|
|
3980
|
+
else if (!teamRoot && options.omxRootOverride?.trim())
|
|
3981
|
+
values.OMX_ROOT = resolveDetachedRootValue(options.cwd, options.omxRootOverride.trim());
|
|
3982
|
+
}
|
|
3983
|
+
if (verifiedMadmaxContext) {
|
|
3984
|
+
values.OMX_ROOT = verifiedMadmaxContext.root;
|
|
3985
|
+
values.OMX_STATE_ROOT = "";
|
|
3986
|
+
values.OMX_TEAM_STATE_ROOT = "";
|
|
3987
|
+
values.OMXBOX_ACTIVE = "1";
|
|
3988
|
+
values.OMX_SOURCE_CWD = verifiedMadmaxContext.sourceCwd;
|
|
3989
|
+
values.OMX_MADMAX_DETACHED_CONTEXT = verifiedMadmaxContext.context;
|
|
3990
|
+
values.OMX_RUNS_DIR = verifiedMadmaxContext.runsRoot;
|
|
3991
|
+
}
|
|
3992
|
+
}
|
|
3993
|
+
const verifiedTeam = options.verifiedTeamContext;
|
|
3994
|
+
if (verifiedTeam && !verifiedMadmaxContext) {
|
|
3995
|
+
values.OMX_ROOT = "";
|
|
3996
|
+
values.OMX_STATE_ROOT = "";
|
|
3997
|
+
values.OMXBOX_ACTIVE = "";
|
|
3998
|
+
values.OMX_SOURCE_CWD = "";
|
|
3999
|
+
values.OMX_MADMAX_DETACHED_CONTEXT = "";
|
|
4000
|
+
values.OMX_RUNS_DIR = "";
|
|
4001
|
+
values.OMX_TEAM_STATE_ROOT = verifiedTeam.stateRoot;
|
|
4002
|
+
values.OMX_TEAM_LEADER_CWD = verifiedTeam.leaderCwd;
|
|
4003
|
+
values.OMX_TEAM_WORKER = verifiedTeam.worker;
|
|
4004
|
+
values.OMX_TEAM_INTERNAL_WORKER = verifiedTeam.internalWorker;
|
|
4005
|
+
}
|
|
4006
|
+
return Object.freeze({
|
|
4007
|
+
values: Object.freeze(values),
|
|
4008
|
+
keys: DETACHED_LAUNCH_CONTROL_PLANE_KEYS,
|
|
4009
|
+
});
|
|
3266
4010
|
}
|
|
3267
|
-
|
|
3268
|
-
|
|
4011
|
+
export const resolveDetachedLaunchControlPlane = buildDetachedLaunchControlPlane;
|
|
4012
|
+
function detachedLaunchControlPlaneEnv(controlPlane) {
|
|
4013
|
+
return Object.fromEntries(DETACHED_LAUNCH_CONTROL_PLANE_KEYS.map((key) => [key, controlPlane.values[key]]));
|
|
4014
|
+
}
|
|
4015
|
+
function applyLaunchOwnedControlPlane(controlPlane) {
|
|
4016
|
+
const previous = new Map();
|
|
4017
|
+
for (const key of DETACHED_LAUNCH_CONTROL_PLANE_KEYS) {
|
|
4018
|
+
previous.set(key, process.env[key]);
|
|
4019
|
+
const value = controlPlane.values[key];
|
|
4020
|
+
if (value === "")
|
|
4021
|
+
delete process.env[key];
|
|
4022
|
+
else
|
|
4023
|
+
process.env[key] = value;
|
|
4024
|
+
}
|
|
4025
|
+
return () => {
|
|
4026
|
+
for (const key of DETACHED_LAUNCH_CONTROL_PLANE_KEYS) {
|
|
4027
|
+
const value = previous.get(key);
|
|
4028
|
+
if (typeof value === "string")
|
|
4029
|
+
process.env[key] = value;
|
|
4030
|
+
else
|
|
4031
|
+
delete process.env[key];
|
|
4032
|
+
}
|
|
4033
|
+
};
|
|
3269
4034
|
}
|
|
3270
4035
|
const SHELL_ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
3271
4036
|
const DETACHED_SESSION_PANE_ENV_KEYS = new Set([
|
|
@@ -3277,21 +4042,23 @@ const DETACHED_SESSION_PANE_ENV_KEYS = new Set([
|
|
|
3277
4042
|
"COLUMNS",
|
|
3278
4043
|
"LINES",
|
|
3279
4044
|
]);
|
|
3280
|
-
|
|
3281
|
-
const
|
|
4045
|
+
function detachedSessionParentEnvironment(env, caseInsensitive = false) {
|
|
4046
|
+
const values = {};
|
|
3282
4047
|
for (const key of Object.keys(env).sort()) {
|
|
3283
|
-
if (!SHELL_ENV_NAME_PATTERN.test(key)
|
|
3284
|
-
|
|
3285
|
-
|
|
4048
|
+
if (!SHELL_ENV_NAME_PATTERN.test(key)
|
|
4049
|
+
|| DETACHED_SESSION_PANE_ENV_KEYS.has(key)
|
|
4050
|
+
|| isDetachedLaunchControlPlaneKey(key, caseInsensitive))
|
|
3286
4051
|
continue;
|
|
3287
4052
|
const value = env[key];
|
|
3288
|
-
if (typeof value
|
|
3289
|
-
|
|
3290
|
-
if (value.includes("\0"))
|
|
3291
|
-
continue;
|
|
3292
|
-
lines.push(`export ${key}=${quoteShellArg(value)}`);
|
|
4053
|
+
if (typeof value === "string" && !value.includes("\0"))
|
|
4054
|
+
values[key] = value;
|
|
3293
4055
|
}
|
|
3294
|
-
return
|
|
4056
|
+
return values;
|
|
4057
|
+
}
|
|
4058
|
+
export function serializeDetachedSessionParentEnv(env) {
|
|
4059
|
+
return `${Object.entries(detachedSessionParentEnvironment(env))
|
|
4060
|
+
.map(([key, value]) => `export ${key}=${quoteShellArg(value)}`)
|
|
4061
|
+
.join("\n")}\n`;
|
|
3295
4062
|
}
|
|
3296
4063
|
export function detachedSessionParentEnvFilePath(cwd, sessionId) {
|
|
3297
4064
|
const safeSessionId = sessionId.replace(/[^A-Za-z0-9_.-]/g, "_");
|
|
@@ -3319,27 +4086,21 @@ export function withTmuxExtendedKeys(cwd, run, execFileSyncImpl = (file, tmuxArg
|
|
|
3319
4086
|
releaseTmuxExtendedKeysLease(cwd, leaseHandle, execFileSyncImpl);
|
|
3320
4087
|
}
|
|
3321
4088
|
}
|
|
3322
|
-
export function buildDetachedSessionBootstrapSteps(sessionName, cwd, codexCmd, hudCmd, workerLaunchArgs, codexHomeOverride, notifyTempContractRaw, nativeWindows = false, sessionId, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, omxRootOverride, env = process.env, sqliteHomeOverride, parentEnvFilePath, inheritedWorkerModel
|
|
4089
|
+
export function buildDetachedSessionBootstrapSteps(sessionName, cwd, codexCmd, hudCmd, workerLaunchArgs, codexHomeOverride, notifyTempContractRaw, nativeWindows = false, sessionId, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, omxRootOverride, env = process.env, sqliteHomeOverride, parentEnvFilePath, inheritedWorkerModel, releaseMarkerPath, omxBin = process.argv[1], preLaunchOptions = {
|
|
4090
|
+
enableNotifyFallbackAuthority: false,
|
|
4091
|
+
worktreeDirty: false,
|
|
4092
|
+
shouldAttach: true,
|
|
4093
|
+
}, controlPlane) {
|
|
3323
4094
|
const detachedLeaderCmd = nativeWindows
|
|
3324
|
-
?
|
|
3325
|
-
: buildDetachedSessionLeaderCommand(cwd, sessionName, codexCmd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, parentEnvFilePath);
|
|
3326
|
-
const
|
|
3327
|
-
|
|
3328
|
-
:
|
|
3329
|
-
|
|
3330
|
-
|
|
3331
|
-
|
|
3332
|
-
|
|
3333
|
-
: hasExplicitRootOverride
|
|
3334
|
-
? {
|
|
3335
|
-
omxRoot: omxRootOverride,
|
|
3336
|
-
rootSource: resolveHudRuntimeRootSource(omxRootOverride, env),
|
|
3337
|
-
}
|
|
3338
|
-
: resolveHudRuntimeRootForLaunch(cwd, env);
|
|
3339
|
-
const hudRuntimeEnv = buildHudRuntimeEnv({
|
|
3340
|
-
sessionId,
|
|
3341
|
-
...hudRuntimeRoot,
|
|
3342
|
-
}).env;
|
|
4095
|
+
? buildDetachedWindowsLeaderCommand(cwd, sessionName, codexCmd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, env, releaseMarkerPath, preLaunchOptions, omxBin)
|
|
4096
|
+
: buildDetachedSessionLeaderCommand(cwd, sessionName, codexCmd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, parentEnvFilePath, releaseMarkerPath, preLaunchOptions, omxBin);
|
|
4097
|
+
const launchControlPlane = controlPlane ?? buildDetachedLaunchControlPlane({
|
|
4098
|
+
cwd,
|
|
4099
|
+
sessionId: sessionId ?? "",
|
|
4100
|
+
env,
|
|
4101
|
+
omxRootOverride,
|
|
4102
|
+
});
|
|
4103
|
+
const controlPlaneEnv = detachedLaunchControlPlaneEnv(launchControlPlane);
|
|
3343
4104
|
const newSessionArgs = [
|
|
3344
4105
|
"new-session",
|
|
3345
4106
|
"-d",
|
|
@@ -3350,15 +4111,10 @@ export function buildDetachedSessionBootstrapSteps(sessionName, cwd, codexCmd, h
|
|
|
3350
4111
|
sessionName,
|
|
3351
4112
|
"-c",
|
|
3352
4113
|
cwd,
|
|
4114
|
+
...DETACHED_LAUNCH_CONTROL_PLANE_KEYS.flatMap((key) => ["-e", `${key}=${controlPlaneEnv[key]}`]),
|
|
3353
4115
|
...(workerLaunchArgs ? ["-e", `${TEAM_WORKER_LAUNCH_ARGS_ENV}=${workerLaunchArgs}`] : []),
|
|
3354
|
-
...Object.entries(hudRuntimeEnv).map(([key, value]) => ["-e", `${key}=${value}`]).flat(),
|
|
3355
4116
|
...(codexHomeOverride ? ["-e", `CODEX_HOME=${codexHomeOverride}`] : []),
|
|
3356
4117
|
...(sqliteHomeOverride ? ["-e", `${CODEX_SQLITE_HOME_ENV}=${sqliteHomeOverride}`] : []),
|
|
3357
|
-
...(env.OMXBOX_ACTIVE ? ["-e", `OMXBOX_ACTIVE=${env.OMXBOX_ACTIVE}`] : []),
|
|
3358
|
-
...(env.OMX_SOURCE_CWD ? ["-e", `OMX_SOURCE_CWD=${env.OMX_SOURCE_CWD}`] : []),
|
|
3359
|
-
...(env[OMX_MADMAX_DETACHED_CONTEXT_ENV]
|
|
3360
|
-
? ["-e", `${OMX_MADMAX_DETACHED_CONTEXT_ENV}=${env[OMX_MADMAX_DETACHED_CONTEXT_ENV]}`]
|
|
3361
|
-
: []),
|
|
3362
4118
|
...(notifyTempContractRaw
|
|
3363
4119
|
? ["-e", `${OMX_NOTIFY_TEMP_CONTRACT_ENV}=${notifyTempContractRaw}`]
|
|
3364
4120
|
: []),
|
|
@@ -3618,41 +4374,40 @@ function isAutopilotReviewPendingPostLaunchState(state) {
|
|
|
3618
4374
|
function postLaunchUniqueStrings(values) {
|
|
3619
4375
|
return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
|
|
3620
4376
|
}
|
|
3621
|
-
async function scrubPostLaunchRootSkillActiveForSession(stateDir, sessionId, nowIso,
|
|
4377
|
+
async function scrubPostLaunchRootSkillActiveForSession(stateDir, sessionId, nowIso, writeRootStateFn) {
|
|
3622
4378
|
const normalizedSessionId = cleanPostLaunchString(sessionId);
|
|
3623
4379
|
if (!normalizedSessionId)
|
|
3624
4380
|
return;
|
|
3625
|
-
|
|
3626
|
-
|
|
3627
|
-
|
|
3628
|
-
|
|
3629
|
-
|
|
3630
|
-
|
|
3631
|
-
|
|
3632
|
-
|
|
3633
|
-
|
|
3634
|
-
|
|
3635
|
-
|
|
3636
|
-
|
|
3637
|
-
|
|
3638
|
-
return
|
|
3639
|
-
|
|
4381
|
+
await writeRootStateFn(stateDir, (rootState) => {
|
|
4382
|
+
if (!rootState)
|
|
4383
|
+
return null;
|
|
4384
|
+
const rootSessionIds = postLaunchUniqueStrings([
|
|
4385
|
+
cleanPostLaunchString(rootState.session_id),
|
|
4386
|
+
cleanPostLaunchString(extractSessionIdFromInitializedStatePath(rootState.initialized_state_path)),
|
|
4387
|
+
]);
|
|
4388
|
+
const rootBelongsToSession = rootSessionIds.includes(normalizedSessionId);
|
|
4389
|
+
const entries = listActiveSkills(rootState);
|
|
4390
|
+
const keptEntries = entries.filter((entry) => {
|
|
4391
|
+
const entrySessionId = cleanPostLaunchString(entry.session_id);
|
|
4392
|
+
if (entrySessionId)
|
|
4393
|
+
return entrySessionId !== normalizedSessionId;
|
|
4394
|
+
return !rootBelongsToSession;
|
|
4395
|
+
});
|
|
4396
|
+
if (keptEntries.length === entries.length && rootState.active !== true)
|
|
4397
|
+
return null;
|
|
4398
|
+
if (keptEntries.length === entries.length && !rootBelongsToSession)
|
|
4399
|
+
return null;
|
|
4400
|
+
return {
|
|
4401
|
+
...rootState,
|
|
4402
|
+
active: keptEntries.length > 0,
|
|
4403
|
+
skill: keptEntries[0]?.skill ?? (keptEntries.length > 0 ? cleanPostLaunchString(rootState.skill) : ""),
|
|
4404
|
+
phase: keptEntries[0]?.phase ?? (keptEntries.length > 0 ? cleanPostLaunchString(rootState.phase) : "complete"),
|
|
4405
|
+
updated_at: nowIso,
|
|
4406
|
+
active_skills: keptEntries,
|
|
4407
|
+
post_launch_reconciled_at: nowIso,
|
|
4408
|
+
post_launch_reconciliation_reason: "terminal_session_cleanup",
|
|
4409
|
+
};
|
|
3640
4410
|
});
|
|
3641
|
-
if (keptEntries.length === entries.length && rootState.active !== true)
|
|
3642
|
-
return;
|
|
3643
|
-
if (keptEntries.length === entries.length && !rootBelongsToSession)
|
|
3644
|
-
return;
|
|
3645
|
-
const nextRoot = {
|
|
3646
|
-
...rootState,
|
|
3647
|
-
active: keptEntries.length > 0,
|
|
3648
|
-
skill: keptEntries[0]?.skill ?? (keptEntries.length > 0 ? cleanPostLaunchString(rootState.skill) : ""),
|
|
3649
|
-
phase: keptEntries[0]?.phase ?? (keptEntries.length > 0 ? cleanPostLaunchString(rootState.phase) : "complete"),
|
|
3650
|
-
updated_at: nowIso,
|
|
3651
|
-
active_skills: keptEntries,
|
|
3652
|
-
post_launch_reconciled_at: nowIso,
|
|
3653
|
-
post_launch_reconciliation_reason: "terminal_session_cleanup",
|
|
3654
|
-
};
|
|
3655
|
-
await writeFileFn(rootPath, JSON.stringify(nextRoot, null, 2));
|
|
3656
4411
|
}
|
|
3657
4412
|
function buildRecoveredPostLaunchModeState(mode, completedAt) {
|
|
3658
4413
|
return {
|
|
@@ -3699,9 +4454,14 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
|
|
|
3699
4454
|
? [getStateDir(cwd, sessionId)]
|
|
3700
4455
|
: [getBaseStateDir(cwd)];
|
|
3701
4456
|
const rootStateDir = getBaseStateDir(cwd);
|
|
3702
|
-
const
|
|
3703
|
-
|
|
3704
|
-
|
|
4457
|
+
const writeRootState = dependencies.writeRootState ?? ((stateDir, update) => (updateRootSkillActiveStateForStateDir(stateDir, update)));
|
|
4458
|
+
const writeModeState = async (stateDir, mode, path, state) => {
|
|
4459
|
+
if (stateDir === rootStateDir && mode === SKILL_ACTIVE_STATE_MODE) {
|
|
4460
|
+
await writeRootState(stateDir, () => state);
|
|
4461
|
+
return;
|
|
4462
|
+
}
|
|
4463
|
+
await writeFile(path, JSON.stringify(state, null, 2));
|
|
4464
|
+
};
|
|
3705
4465
|
let preserveSkillActiveForReviewPendingAutopilot = false;
|
|
3706
4466
|
for (const stateDir of scopedDirs) {
|
|
3707
4467
|
const files = await readdir(stateDir).catch(() => []);
|
|
@@ -3713,7 +4473,7 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
|
|
|
3713
4473
|
&& isAutopilotReviewPendingPostLaunchState(autopilotPrecheck.state);
|
|
3714
4474
|
preserveSkillActiveForReviewPendingAutopilot ||= preserveReviewPendingAutopilot;
|
|
3715
4475
|
for (const file of files) {
|
|
3716
|
-
if (!
|
|
4476
|
+
if (!isModeStateFilename(file))
|
|
3717
4477
|
continue;
|
|
3718
4478
|
const path = join(stateDir, file);
|
|
3719
4479
|
const mode = file.slice(0, -"-state.json".length);
|
|
@@ -3722,9 +4482,10 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
|
|
|
3722
4482
|
if (result.kind === "recoverable") {
|
|
3723
4483
|
try {
|
|
3724
4484
|
const completedAt = now().toISOString();
|
|
3725
|
-
|
|
4485
|
+
const recoveredState = mode === SKILL_ACTIVE_STATE_MODE
|
|
3726
4486
|
? buildRecoveredPostLaunchSkillActiveState(completedAt)
|
|
3727
|
-
: buildRecoveredPostLaunchModeState(mode, completedAt)
|
|
4487
|
+
: buildRecoveredPostLaunchModeState(mode, completedAt);
|
|
4488
|
+
await writeModeState(stateDir, mode, path, recoveredState);
|
|
3728
4489
|
if (isTrackedWorkflowMode(mode)) {
|
|
3729
4490
|
await syncCanonicalSkillStateForMode({
|
|
3730
4491
|
cwd,
|
|
@@ -3757,12 +4518,12 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
|
|
|
3757
4518
|
: normalizeTerminalWorkflowState(result.state, { mode, nowIso: completedAt });
|
|
3758
4519
|
if (normalized.changed) {
|
|
3759
4520
|
result.state = normalized.state;
|
|
3760
|
-
await
|
|
4521
|
+
await writeModeState(stateDir, mode, path, result.state);
|
|
3761
4522
|
}
|
|
3762
4523
|
if (mode === "ralph") {
|
|
3763
4524
|
const completedAt = now().toISOString();
|
|
3764
4525
|
if (markRalphCompletionAuditBlockedForPostLaunch(result.state, cwd, completedAt)) {
|
|
3765
|
-
await
|
|
4526
|
+
await writeModeState(stateDir, mode, path, result.state);
|
|
3766
4527
|
await syncCanonicalSkillStateForMode({
|
|
3767
4528
|
cwd,
|
|
3768
4529
|
baseStateDir: rootStateDir,
|
|
@@ -3788,7 +4549,7 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
|
|
|
3788
4549
|
result.state.phase = "complete";
|
|
3789
4550
|
result.state.updated_at = completedAt;
|
|
3790
4551
|
result.state.active_skills = [];
|
|
3791
|
-
await
|
|
4552
|
+
await writeModeState(stateDir, mode, path, result.state);
|
|
3792
4553
|
continue;
|
|
3793
4554
|
}
|
|
3794
4555
|
result.state.active = false;
|
|
@@ -3799,7 +4560,7 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
|
|
|
3799
4560
|
result.state.interrupted_at = completedAt;
|
|
3800
4561
|
result.state.stop_reason = cleanPostLaunchString(result.state.stop_reason) || "session_exit";
|
|
3801
4562
|
}
|
|
3802
|
-
await
|
|
4563
|
+
await writeModeState(stateDir, mode, path, result.state);
|
|
3803
4564
|
if (isTrackedWorkflowMode(mode)) {
|
|
3804
4565
|
await syncCanonicalSkillStateForMode({
|
|
3805
4566
|
cwd,
|
|
@@ -3821,7 +4582,7 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
|
|
|
3821
4582
|
if (sessionId) {
|
|
3822
4583
|
try {
|
|
3823
4584
|
if (!preserveSkillActiveForReviewPendingAutopilot) {
|
|
3824
|
-
await scrubPostLaunchRootSkillActiveForSession(rootStateDir, sessionId, now().toISOString(),
|
|
4585
|
+
await scrubPostLaunchRootSkillActiveForSession(rootStateDir, sessionId, now().toISOString(), writeRootState);
|
|
3825
4586
|
}
|
|
3826
4587
|
}
|
|
3827
4588
|
catch (err) {
|
|
@@ -3847,115 +4608,140 @@ export async function reapPostLaunchOrphanedMcpProcesses(dependencies = {}) {
|
|
|
3847
4608
|
writeError(`[cli/index] postLaunch MCP cleanup failed: ${err}\n`);
|
|
3848
4609
|
}
|
|
3849
4610
|
}
|
|
3850
|
-
|
|
3851
|
-
|
|
3852
|
-
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
3856
|
-
|
|
3857
|
-
|
|
3858
|
-
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
4611
|
+
async function establishPreLaunchBinding(cwd, sessionId, metadata = {}) {
|
|
4612
|
+
const pointerOptions = await resolvePreLaunchSessionPointerOptions();
|
|
4613
|
+
const established = await establishLaunchSessionBinding(cwd, sessionId, { ...pointerOptions, ...metadata });
|
|
4614
|
+
if (established.kind === "committed-released")
|
|
4615
|
+
return { binding: established.binding, cleanup: established.cleanup };
|
|
4616
|
+
const error = established.kind === "precommit-aborted" ? established.abort : established.error;
|
|
4617
|
+
Object.assign(error, { establishmentCleanup: established.cleanup });
|
|
4618
|
+
throw error;
|
|
4619
|
+
}
|
|
4620
|
+
async function failPreLaunchSetup(binding) {
|
|
4621
|
+
const failures = [];
|
|
4622
|
+
try {
|
|
4623
|
+
await finalizeBoundOnce(binding, "setup-failure");
|
|
4624
|
+
}
|
|
4625
|
+
catch (error) {
|
|
4626
|
+
failures.push(error);
|
|
4627
|
+
}
|
|
4628
|
+
finally {
|
|
4629
|
+
try {
|
|
4630
|
+
const close = await closeLaunchSessionBindingOnce(binding);
|
|
4631
|
+
if (close.status === "failed")
|
|
4632
|
+
failures.push(close.error ?? new Error("bound session close failed"));
|
|
4633
|
+
}
|
|
4634
|
+
catch (error) {
|
|
4635
|
+
failures.push(error);
|
|
4636
|
+
}
|
|
4637
|
+
}
|
|
4638
|
+
if (failures.length > 0)
|
|
4639
|
+
throw new AggregateError(failures, "preLaunch setup finalization failed");
|
|
4640
|
+
}
|
|
4641
|
+
async function completePreLaunchSetup(cwd, sessionId, notifyTempContract, codexHomeOverride, enableNotifyFallbackAuthority, worktreeDirty) {
|
|
4642
|
+
const degraded = [];
|
|
3862
4643
|
try {
|
|
3863
4644
|
const cleanup = await cleanupLaunchOrphanedMcpProcesses();
|
|
3864
|
-
if (cleanup.terminatedCount > 0)
|
|
4645
|
+
if (cleanup.terminatedCount > 0)
|
|
3865
4646
|
console.log(`[omx] Reaped ${cleanup.terminatedCount} orphaned OMX MCP process(es) before launch.`);
|
|
3866
|
-
|
|
3867
|
-
if (cleanup.failedPids.length > 0) {
|
|
4647
|
+
if (cleanup.failedPids.length > 0)
|
|
3868
4648
|
console.warn(`[omx] Failed to reap ${cleanup.failedPids.length} orphaned OMX MCP process(es); continuing launch.`);
|
|
3869
|
-
}
|
|
3870
4649
|
}
|
|
3871
|
-
catch (
|
|
3872
|
-
|
|
3873
|
-
|
|
4650
|
+
catch (error) {
|
|
4651
|
+
degraded.push("orphan-reaping");
|
|
4652
|
+
logCliOperationFailure(error);
|
|
4653
|
+
}
|
|
4654
|
+
let instructions;
|
|
4655
|
+
try {
|
|
4656
|
+
const orchestrationMode = await resolveSessionOrchestrationMode(cwd, sessionId);
|
|
4657
|
+
const overlay = await generateOverlay(cwd, sessionId, { orchestrationMode });
|
|
4658
|
+
const launchAppendix = await readLaunchAppendInstructions();
|
|
4659
|
+
const dirtyWorktreeGuidance = worktreeDirty
|
|
4660
|
+
? `\n\n## Session start: dirty worktree detected\n\nThis worktree has uncommitted changes that were present when the session launched.\nBefore executing the requested task, resolve the dirty state first:\n1. Review uncommitted changes with \`git status\` and \`git diff\`.\n2. Commit, stash, or discard changes as appropriate.\n3. Then proceed with the original task.`
|
|
4661
|
+
: "";
|
|
4662
|
+
instructions = launchAppendix.trim().length > 0 ? `${overlay}\n\n${launchAppendix}${dirtyWorktreeGuidance}` : `${overlay}${dirtyWorktreeGuidance}`;
|
|
4663
|
+
}
|
|
4664
|
+
catch (error) {
|
|
4665
|
+
return { kind: "failure", operation: "overlay", error };
|
|
4666
|
+
}
|
|
4667
|
+
try {
|
|
4668
|
+
await writeSessionModelInstructionsFile(cwd, sessionId, instructions);
|
|
4669
|
+
}
|
|
4670
|
+
catch (error) {
|
|
4671
|
+
return { kind: "failure", operation: "session-instructions", error };
|
|
4672
|
+
}
|
|
4673
|
+
try {
|
|
4674
|
+
await resetSessionMetrics(cwd, sessionId);
|
|
4675
|
+
tagCurrentTmuxSessionWithInstance(sessionId);
|
|
4676
|
+
}
|
|
4677
|
+
catch (error) {
|
|
4678
|
+
return { kind: "failure", operation: "session-metrics", error };
|
|
3874
4679
|
}
|
|
3875
|
-
// 2. Establish the canonical pointer before any session-scoped launch artifact.
|
|
3876
|
-
await writeSessionStart(cwd, sessionId, await resolvePreLaunchSessionPointerOptions());
|
|
3877
|
-
// 3. Generate runtime overlay + write session-scoped model instructions file
|
|
3878
|
-
const orchestrationMode = await resolveSessionOrchestrationMode(cwd, sessionId);
|
|
3879
|
-
const overlay = await generateOverlay(cwd, sessionId, { orchestrationMode });
|
|
3880
|
-
const launchAppendix = await readLaunchAppendInstructions();
|
|
3881
|
-
const dirtyWorktreeGuidance = worktreeDirty
|
|
3882
|
-
? `\n\n## Session start: dirty worktree detected\n\nThis worktree has uncommitted changes that were present when the session launched.\nBefore executing the requested task, resolve the dirty state first:\n1. Review uncommitted changes with \`git status\` and \`git diff\`.\n2. Commit, stash, or discard changes as appropriate.\n3. Then proceed with the original task.`
|
|
3883
|
-
: "";
|
|
3884
|
-
const sessionInstructions = launchAppendix.trim().length > 0
|
|
3885
|
-
? `${overlay}
|
|
3886
|
-
|
|
3887
|
-
${launchAppendix}${dirtyWorktreeGuidance}`
|
|
3888
|
-
: `${overlay}${dirtyWorktreeGuidance}`;
|
|
3889
|
-
await writeSessionModelInstructionsFile(cwd, sessionId, sessionInstructions);
|
|
3890
|
-
// 4. Reset session metrics and tag the established session.
|
|
3891
|
-
await resetSessionMetrics(cwd, sessionId);
|
|
3892
|
-
tagCurrentTmuxSessionWithInstance(sessionId);
|
|
3893
|
-
// 5. Start notify fallback watcher (best effort)
|
|
3894
4680
|
try {
|
|
3895
4681
|
await startNotifyFallbackWatcher(cwd, { codexHomeOverride, enableAuthority: enableNotifyFallbackAuthority, sessionId });
|
|
3896
4682
|
}
|
|
3897
|
-
catch (
|
|
3898
|
-
|
|
3899
|
-
|
|
4683
|
+
catch (error) {
|
|
4684
|
+
degraded.push("notify-watcher");
|
|
4685
|
+
logCliOperationFailure(error);
|
|
3900
4686
|
}
|
|
3901
|
-
// 6. Start derived watcher (best effort, opt-in)
|
|
3902
4687
|
try {
|
|
3903
4688
|
await startHookDerivedWatcher(cwd);
|
|
3904
4689
|
}
|
|
3905
|
-
catch (
|
|
3906
|
-
|
|
3907
|
-
|
|
4690
|
+
catch (error) {
|
|
4691
|
+
degraded.push("derived-watcher");
|
|
4692
|
+
logCliOperationFailure(error);
|
|
3908
4693
|
}
|
|
3909
|
-
// 7. Emit temp notification startup summary + warnings, then send session-start lifecycle notification (best effort)
|
|
3910
4694
|
try {
|
|
3911
4695
|
if (notifyTempContract?.active) {
|
|
3912
|
-
process.env[OMX_NOTIFY_TEMP_CONTRACT_ENV] =
|
|
3913
|
-
serializeNotifyTempContract(notifyTempContract);
|
|
4696
|
+
process.env[OMX_NOTIFY_TEMP_CONTRACT_ENV] = serializeNotifyTempContract(notifyTempContract);
|
|
3914
4697
|
const { getNotificationConfig } = await import("../notifications/config.js");
|
|
3915
|
-
const
|
|
3916
|
-
const
|
|
3917
|
-
for (const info of startup.infoLines) {
|
|
4698
|
+
const startup = buildNotifyTempStartupMessages(notifyTempContract, Boolean(getNotificationConfig()?.enabled));
|
|
4699
|
+
for (const info of startup.infoLines)
|
|
3918
4700
|
console.log(`[omx] ${info}`);
|
|
3919
|
-
|
|
3920
|
-
for (const warning of startup.warningLines) {
|
|
4701
|
+
for (const warning of startup.warningLines)
|
|
3921
4702
|
console.warn(`[omx] ${warning}`);
|
|
3922
|
-
}
|
|
3923
4703
|
}
|
|
3924
|
-
else
|
|
4704
|
+
else
|
|
3925
4705
|
delete process.env[OMX_NOTIFY_TEMP_CONTRACT_ENV];
|
|
3926
|
-
}
|
|
3927
4706
|
const { notifyLifecycle } = await import("../notifications/index.js");
|
|
3928
|
-
await notifyLifecycle("session-start", {
|
|
3929
|
-
sessionId,
|
|
3930
|
-
projectPath: cwd,
|
|
3931
|
-
projectName: basename(cwd),
|
|
3932
|
-
});
|
|
4707
|
+
await notifyLifecycle("session-start", { sessionId, projectPath: cwd, projectName: basename(cwd) });
|
|
3933
4708
|
}
|
|
3934
|
-
catch (
|
|
3935
|
-
|
|
3936
|
-
|
|
4709
|
+
catch (error) {
|
|
4710
|
+
degraded.push("notification");
|
|
4711
|
+
logCliOperationFailure(error);
|
|
3937
4712
|
}
|
|
3938
|
-
// 8. Dispatch native hook event (best effort)
|
|
3939
4713
|
try {
|
|
3940
|
-
await emitNativeHookEvent(cwd, "session-start", {
|
|
3941
|
-
session_id: sessionId,
|
|
3942
|
-
context: buildNativeHookBaseContext(cwd, sessionId, "started", {
|
|
3943
|
-
project_path: cwd,
|
|
3944
|
-
project_name: basename(cwd),
|
|
3945
|
-
status: "started",
|
|
3946
|
-
}),
|
|
3947
|
-
});
|
|
4714
|
+
await emitNativeHookEvent(cwd, "session-start", { session_id: sessionId, context: buildNativeHookBaseContext(cwd, sessionId, "started", { project_path: cwd, project_name: basename(cwd), status: "started" }) });
|
|
3948
4715
|
}
|
|
3949
|
-
catch (
|
|
3950
|
-
|
|
3951
|
-
|
|
4716
|
+
catch (error) {
|
|
4717
|
+
degraded.push("native-lifecycle-event");
|
|
4718
|
+
logCliOperationFailure(error);
|
|
4719
|
+
}
|
|
4720
|
+
return degraded.length === 0 ? { kind: "success" } : { kind: "degraded-success", operations: degraded };
|
|
4721
|
+
}
|
|
4722
|
+
export async function preLaunch(cwd, sessionId, notifyTempContract, codexHomeOverride, enableNotifyFallbackAuthority = false, worktreeDirty = false) {
|
|
4723
|
+
const established = await establishPreLaunchBinding(cwd, sessionId);
|
|
4724
|
+
const completion = {
|
|
4725
|
+
...await completePreLaunchSetup(cwd, sessionId, notifyTempContract, codexHomeOverride, enableNotifyFallbackAuthority, worktreeDirty),
|
|
4726
|
+
establishmentCleanup: established.cleanup,
|
|
4727
|
+
};
|
|
4728
|
+
if (completion.kind === "failure") {
|
|
4729
|
+
try {
|
|
4730
|
+
await failPreLaunchSetup(established.binding);
|
|
4731
|
+
}
|
|
4732
|
+
catch (cleanupError) {
|
|
4733
|
+
throw new AggregateError([completion.error, cleanupError], `preLaunch ${completion.operation} failed`);
|
|
4734
|
+
}
|
|
4735
|
+
throw completion.error;
|
|
3952
4736
|
}
|
|
4737
|
+
return { binding: established.binding, completion };
|
|
3953
4738
|
}
|
|
3954
4739
|
/**
|
|
3955
4740
|
* runCodex: Launch Codex CLI (blocks until exit).
|
|
3956
4741
|
* All 3 paths (new tmux, existing tmux, no tmux) block via execSync/execFileSync.
|
|
3957
4742
|
*/
|
|
3958
|
-
function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, sqliteHomeOverride, notifyTempContractRaw,
|
|
4743
|
+
async function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, sqliteHomeOverride, notifyTempContractRaw, preLaunchOptions, launchPolicy = "direct", detachedPreflight, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, runtimeContext, verifiedMadmaxContext, launchControlPlane) {
|
|
4744
|
+
void preLaunchOptions;
|
|
3959
4745
|
const launchArgs = injectModelInstructionsBypassArgs(cwd, args, process.env, sessionModelInstructionsPath(cwd, sessionId));
|
|
3960
4746
|
const nativeWindows = isNativeWindows();
|
|
3961
4747
|
const omxBin = resolveOmxCliEntryPath({ argv1: process.argv[1], cwd, env: process.env });
|
|
@@ -3963,19 +4749,59 @@ function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, s
|
|
|
3963
4749
|
throw new Error("Unable to resolve OMX launcher path for tmux HUD bootstrap");
|
|
3964
4750
|
}
|
|
3965
4751
|
const runtimeEnvOverlay = buildMadmaxWorktreeRuntimeEnvOverlay(runtimeContext);
|
|
4752
|
+
let verifiedTeamContext;
|
|
4753
|
+
if (launchPolicy === "detached-tmux") {
|
|
4754
|
+
try {
|
|
4755
|
+
verifiedTeamContext = await resolveVerifiedDetachedTeamContext(cwd, process.env);
|
|
4756
|
+
}
|
|
4757
|
+
catch {
|
|
4758
|
+
verifiedTeamContext = undefined;
|
|
4759
|
+
}
|
|
4760
|
+
}
|
|
4761
|
+
const verifiedMadmaxContextForLaunch = verifiedMadmaxContext ?? (runtimeContext?.madmaxDetachedContext
|
|
4762
|
+
? {
|
|
4763
|
+
root: runtimeContext.omxRoot,
|
|
4764
|
+
sourceCwd: runtimeContext.sourceCwd,
|
|
4765
|
+
context: runtimeContext.madmaxDetachedContext,
|
|
4766
|
+
runsRoot: resolveMadmaxRunsRoot(process.env),
|
|
4767
|
+
}
|
|
4768
|
+
: resolveVerifiedMadmaxDetachedContext(cwd, launchArgs, process.env));
|
|
4769
|
+
const detachedControlPlane = launchPolicy === "detached-tmux"
|
|
4770
|
+
? buildDetachedLaunchControlPlane({
|
|
4771
|
+
cwd,
|
|
4772
|
+
sessionId,
|
|
4773
|
+
env: process.env,
|
|
4774
|
+
omxRootOverride: runtimeContext?.omxRoot ?? resolveOmxRootForLaunch(cwd, process.env),
|
|
4775
|
+
runtimeContext,
|
|
4776
|
+
verifiedMadmaxContext: verifiedMadmaxContextForLaunch,
|
|
4777
|
+
verifiedTeamContext,
|
|
4778
|
+
})
|
|
4779
|
+
: undefined;
|
|
4780
|
+
const launchOwnedControlPlane = detachedControlPlane ?? launchControlPlane;
|
|
4781
|
+
const launchOwnedControlPlaneEnv = launchOwnedControlPlane
|
|
4782
|
+
? detachedLaunchControlPlaneEnv(launchOwnedControlPlane)
|
|
4783
|
+
: {};
|
|
4784
|
+
const detachedSelectedStateEnv = detachedControlPlane
|
|
4785
|
+
? { ...process.env, ...detachedLaunchControlPlaneEnv(detachedControlPlane) }
|
|
4786
|
+
: undefined;
|
|
3966
4787
|
const omxRootOverride = runtimeContext?.omxRoot ?? resolveOmxRootForLaunch(cwd, process.env);
|
|
4788
|
+
const selectedOmxRootOverride = launchOwnedControlPlane
|
|
4789
|
+
? launchOwnedControlPlane.values.OMX_ROOT || undefined
|
|
4790
|
+
: omxRootOverride;
|
|
3967
4791
|
const currentPaneId = process.env.TMUX_PANE;
|
|
3968
4792
|
const hudRuntimeRoot = runtimeContext
|
|
3969
4793
|
? { omxRoot: runtimeContext.omxRoot, rootSource: 'omx-root-env' }
|
|
3970
4794
|
: resolveHudRuntimeRootForLaunch(cwd, process.env);
|
|
3971
|
-
const hudRuntimeEnv =
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
4795
|
+
const hudRuntimeEnv = launchOwnedControlPlane
|
|
4796
|
+
? Object.fromEntries(Object.entries(launchOwnedControlPlaneEnv))
|
|
4797
|
+
: {
|
|
4798
|
+
...buildHudRuntimeEnv({
|
|
4799
|
+
sessionId,
|
|
4800
|
+
leaderPaneId: currentPaneId,
|
|
4801
|
+
...hudRuntimeRoot,
|
|
4802
|
+
}).env,
|
|
4803
|
+
...runtimeEnvOverlay,
|
|
4804
|
+
};
|
|
3979
4805
|
const hudEnvArgs = Object.entries(hudRuntimeEnv).map(([key, value]) => `${key}=${value}`);
|
|
3980
4806
|
const hudCmd = nativeWindows
|
|
3981
4807
|
? buildWindowsPromptCommand("node", [omxBin, "hud", "--watch"])
|
|
@@ -3990,13 +4816,17 @@ function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, s
|
|
|
3990
4816
|
...stripHermesMcpBridgeEnv(process.env),
|
|
3991
4817
|
...(codexHomeOverride ? { CODEX_HOME: codexHomeOverride } : {}),
|
|
3992
4818
|
...(sqliteHomeOverride ? { [CODEX_SQLITE_HOME_ENV]: sqliteHomeOverride } : {}),
|
|
3993
|
-
...(omxRootOverride ? { OMX_ROOT: omxRootOverride } : {}),
|
|
4819
|
+
...(!launchOwnedControlPlane && omxRootOverride ? { OMX_ROOT: omxRootOverride } : {}),
|
|
3994
4820
|
...runtimeEnvOverlay,
|
|
4821
|
+
...launchOwnedControlPlaneEnv,
|
|
3995
4822
|
}, omxBin);
|
|
4823
|
+
// Correlates plugin hook routing only; it is not an authority token.
|
|
4824
|
+
const pluginHookRoutingLaunchId = randomUUID();
|
|
3996
4825
|
const codexEnvWithSession = {
|
|
3997
4826
|
...codexBaseEnv,
|
|
3998
|
-
OMX_CODEX_LAUNCH_ID:
|
|
4827
|
+
OMX_CODEX_LAUNCH_ID: pluginHookRoutingLaunchId,
|
|
3999
4828
|
...buildHudRuntimeEnv({ sessionId }).env,
|
|
4829
|
+
...launchOwnedControlPlaneEnv,
|
|
4000
4830
|
};
|
|
4001
4831
|
const codexEnv = workerLaunchArgs
|
|
4002
4832
|
? {
|
|
@@ -4008,8 +4838,9 @@ function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, s
|
|
|
4008
4838
|
const codexEnvWithNotify = notifyTempContractRaw
|
|
4009
4839
|
? { ...codexEnv, [OMX_NOTIFY_TEMP_CONTRACT_ENV]: notifyTempContractRaw }
|
|
4010
4840
|
: codexEnv;
|
|
4011
|
-
const runtimeHookEnv =
|
|
4012
|
-
|
|
4841
|
+
const runtimeHookEnv = launchOwnedControlPlane
|
|
4842
|
+
? { ...process.env, ...runtimeEnvOverlay, ...launchOwnedControlPlaneEnv }
|
|
4843
|
+
: { ...process.env, ...runtimeEnvOverlay };
|
|
4013
4844
|
if (isCodexVersionRequest(launchArgs)) {
|
|
4014
4845
|
runCodexBlocking(cwd, launchArgs, codexEnvWithNotify);
|
|
4015
4846
|
return { postLaunchHandledExternally: false };
|
|
@@ -4045,7 +4876,7 @@ function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, s
|
|
|
4045
4876
|
currentPaneId,
|
|
4046
4877
|
cwd,
|
|
4047
4878
|
sessionId,
|
|
4048
|
-
omxRootOverride,
|
|
4879
|
+
omxRootOverride: selectedOmxRootOverride,
|
|
4049
4880
|
baseEnv: runtimeHookEnv,
|
|
4050
4881
|
});
|
|
4051
4882
|
}
|
|
@@ -4072,7 +4903,7 @@ function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, s
|
|
|
4072
4903
|
currentPaneId,
|
|
4073
4904
|
cwd,
|
|
4074
4905
|
sessionId,
|
|
4075
|
-
omxRootOverride,
|
|
4906
|
+
omxRootOverride: selectedOmxRootOverride,
|
|
4076
4907
|
baseEnv: runtimeHookEnv,
|
|
4077
4908
|
});
|
|
4078
4909
|
}
|
|
@@ -4133,214 +4964,247 @@ function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, s
|
|
|
4133
4964
|
return { postLaunchHandledExternally: false };
|
|
4134
4965
|
}
|
|
4135
4966
|
else {
|
|
4136
|
-
|
|
4967
|
+
if (!detachedPreflight) {
|
|
4968
|
+
throw new DetachedLaunchSafetyError("establishment", new Error("detached launch requires a frozen available preflight"), { transitions: ["D0"], rollback: { attempted: [], failures: [] } });
|
|
4969
|
+
}
|
|
4137
4970
|
const codexCmd = buildTmuxPaneCommand("codex", launchArgs);
|
|
4138
4971
|
const detachedWindowsCodexCmd = nativeWindows
|
|
4139
|
-
?
|
|
4972
|
+
? buildWindowsDetachedChildCommand("codex", launchArgs)
|
|
4140
4973
|
: null;
|
|
4141
4974
|
const sessionName = buildDetachedTmuxSessionName(cwd, sessionId);
|
|
4142
|
-
const
|
|
4143
|
-
|
|
4144
|
-
|
|
4145
|
-
|
|
4146
|
-
|
|
4147
|
-
|
|
4148
|
-
|
|
4149
|
-
|
|
4150
|
-
|
|
4151
|
-
|
|
4152
|
-
|
|
4153
|
-
|
|
4154
|
-
|
|
4155
|
-
|
|
4156
|
-
if (!
|
|
4157
|
-
clearDetachedTmuxSessionHistoryIfUnattached(activeRecord.tmux_session_name, activeRecord.tmux_pane_id);
|
|
4158
|
-
process.stderr.write(`[omx] madmax detached launch already active for this context; reusing ${activeRecord.tmux_session_name} without attaching because this launch is a Hermes MCP bridge.\n`);
|
|
4159
|
-
return { postLaunchHandledExternally: true };
|
|
4160
|
-
}
|
|
4161
|
-
process.stderr.write(`[omx] madmax detached launch already active for this context; attaching ${activeRecord.tmux_session_name} instead of starting a duplicate.\n`);
|
|
4162
|
-
try {
|
|
4163
|
-
execTmuxFileSync(["attach-session", "-t", activeRecord.tmux_session_name], {
|
|
4164
|
-
stdio: "inherit",
|
|
4165
|
-
});
|
|
4166
|
-
}
|
|
4167
|
-
catch (err) {
|
|
4168
|
-
logCliOperationFailure(err);
|
|
4169
|
-
throw new MadmaxDetachedReuseError(`refusing duplicate madmax detached launch: existing session ${activeRecord.tmux_session_name} is active but attach failed`);
|
|
4170
|
-
}
|
|
4171
|
-
return { postLaunchHandledExternally: true };
|
|
4172
|
-
}
|
|
4173
|
-
if (activeRecordPath && activeRecord) {
|
|
4174
|
-
rmSync(activeRecordPath, { force: true });
|
|
4175
|
-
}
|
|
4176
|
-
let detachedSessionBindingWrite = Promise.resolve();
|
|
4177
|
-
const writeDetachedSessionBinding = (tmuxPaneId) => {
|
|
4178
|
-
detachedSessionBindingWrite = detachedSessionBindingWrite
|
|
4179
|
-
.catch((err) => {
|
|
4180
|
-
logCliOperationFailure(err);
|
|
4181
|
-
})
|
|
4182
|
-
.then(() => writeSessionStart(cwd, sessionId, {
|
|
4183
|
-
tmuxSessionName: sessionName,
|
|
4184
|
-
...(tmuxPaneId ? { tmuxPaneId } : {}),
|
|
4185
|
-
}));
|
|
4186
|
-
void detachedSessionBindingWrite.catch((err) => {
|
|
4187
|
-
logCliOperationFailure(err);
|
|
4188
|
-
// Non-fatal: managed tmux recovery can still use compatibility fallback.
|
|
4189
|
-
});
|
|
4190
|
-
};
|
|
4191
|
-
writeDetachedSessionBinding();
|
|
4192
|
-
let createdDetachedSession = false;
|
|
4193
|
-
let registeredHookTarget = null;
|
|
4194
|
-
let registeredHookName = null;
|
|
4195
|
-
let registeredClientAttachedHookName = null;
|
|
4196
|
-
let detachedParentEnvFilePath;
|
|
4197
|
-
let detachedLeaderPaneId = null;
|
|
4198
|
-
try {
|
|
4199
|
-
// This path is the user-shell interactive launch: OMX creates a tmux
|
|
4200
|
-
// session and immediately attaches the user's terminal to it. If a tmux
|
|
4201
|
-
// server already exists, `new-session -e` only forwards explicit values,
|
|
4202
|
-
// so provider-specific parent-shell keys would disappear. Source a
|
|
4203
|
-
// private env file inside the leader shell instead of putting every
|
|
4204
|
-
// parent env value on the tmux command line or in logs.
|
|
4205
|
-
if (!nativeWindows) {
|
|
4975
|
+
const contextKey = detachedControlPlane?.values.OMX_MADMAX_DETACHED_CONTEXT.trim() || undefined;
|
|
4976
|
+
const runsRoot = detachedControlPlane?.values.OMX_RUNS_DIR || resolveMadmaxRunsRoot(process.env);
|
|
4977
|
+
const detachedLaunchNonce = randomUUID();
|
|
4978
|
+
const releaseMarkerPath = join(omxRoot(cwd), "runtime", "detached-release", `${sessionId}.${detachedLaunchNonce}.release`);
|
|
4979
|
+
let detachedParentEnvFilePath;
|
|
4980
|
+
let detachedLeaderPaneId = null;
|
|
4981
|
+
let detachedLeaderAuthority = null;
|
|
4982
|
+
let detachedHudAuthority = null;
|
|
4983
|
+
let detachedLeaderPid = null;
|
|
4984
|
+
let rollbackFromPreReportAuthority = false;
|
|
4985
|
+
let attachStep = null;
|
|
4986
|
+
const launchDetachedSession = async () => {
|
|
4987
|
+
let createdSession = false;
|
|
4988
|
+
const bootstrapLeader = async () => {
|
|
4989
|
+
if (!nativeWindows)
|
|
4206
4990
|
detachedParentEnvFilePath = writeDetachedSessionParentEnvFile(cwd, sessionId, codexEnvWithNotify);
|
|
4207
|
-
|
|
4208
|
-
|
|
4991
|
+
const bootstrapSteps = buildDetachedSessionBootstrapSteps(sessionName, cwd, nativeWindows && detachedWindowsCodexCmd ? detachedWindowsCodexCmd : codexCmd, hudCmd, workerLaunchArgs, codexHomeOverride, notifyTempContractRaw, nativeWindows, sessionId, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, omxRootOverride, codexEnvWithNotify, sqliteHomeOverride, detachedParentEnvFilePath, inheritedWorkerModel, releaseMarkerPath, omxBin, {
|
|
4992
|
+
...(preLaunchOptions.notifyTempContract.active ? { notifyTempContract: preLaunchOptions.notifyTempContract } : {}),
|
|
4993
|
+
enableNotifyFallbackAuthority: preLaunchOptions.enableNotifyFallbackAuthority,
|
|
4994
|
+
worktreeDirty: preLaunchOptions.worktreeDirty,
|
|
4995
|
+
shouldAttach: detachedPreflight.shouldAttach,
|
|
4996
|
+
}, detachedControlPlane);
|
|
4209
4997
|
for (const step of bootstrapSteps) {
|
|
4210
|
-
|
|
4211
|
-
|
|
4212
|
-
|
|
4213
|
-
|
|
4214
|
-
|
|
4215
|
-
|
|
4216
|
-
|
|
4217
|
-
if (leaderPaneId) {
|
|
4998
|
+
try {
|
|
4999
|
+
if (step.name === "new-session") {
|
|
5000
|
+
const output = execTmuxFileSync(step.args, { stdio: "pipe", encoding: "utf-8" });
|
|
5001
|
+
createdSession = true;
|
|
5002
|
+
const leaderPaneId = parsePaneIdFromTmuxOutput(output || "");
|
|
5003
|
+
if (!leaderPaneId)
|
|
5004
|
+
throw new Error("detached session did not report a leader pane id");
|
|
4218
5005
|
detachedLeaderPaneId = leaderPaneId;
|
|
4219
|
-
|
|
4220
|
-
|
|
4221
|
-
writeMadmaxDetachedActiveRecord(activeRecordPath, {
|
|
4222
|
-
version: 1,
|
|
4223
|
-
context_key: contextKey,
|
|
4224
|
-
created_at: new Date().toISOString(),
|
|
4225
|
-
source_cwd: runtimeContext?.sourceCwd ?? process.env.OMX_SOURCE_CWD ?? cwd,
|
|
4226
|
-
...(runtimeContext?.worktreeCwd ? { worktree_cwd: runtimeContext.worktreeCwd } : {}),
|
|
4227
|
-
argv: args,
|
|
4228
|
-
run_dir: runtimeContext?.omxRoot ?? process.env.OMX_ROOT ?? cwd,
|
|
4229
|
-
tmux_session_name: sessionName,
|
|
4230
|
-
session_id: sessionId,
|
|
4231
|
-
tmux_pane_id: leaderPaneId,
|
|
4232
|
-
});
|
|
4233
|
-
}
|
|
4234
|
-
writeDetachedSessionBinding(leaderPaneId);
|
|
5006
|
+
detachedLeaderAuthority = captureDetachedLeaderAuthority(leaderPaneId, sessionName, sessionId);
|
|
5007
|
+
continue;
|
|
4235
5008
|
}
|
|
4236
|
-
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
|
|
4241
|
-
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
const clientAttachedHookName = hudPaneId && hookWindowIndex
|
|
4249
|
-
? buildClientAttachedReconcileHookName("launch", sessionName, hookWindowIndex, hudPaneId)
|
|
4250
|
-
: null;
|
|
4251
|
-
const finalizeSteps = buildDetachedSessionFinalizeSteps(sessionName, hudPaneId, hookWindowIndex, process.env.OMX_MOUSE !== "0", nativeWindows, shouldAttachDetachedTmuxSession(process.env), detachedLeaderPaneId);
|
|
4252
|
-
if (nativeWindows && detachedWindowsCodexCmd) {
|
|
4253
|
-
scheduleDetachedWindowsCodexLaunch(sessionName, detachedWindowsCodexCmd);
|
|
5009
|
+
const authority = detachedLeaderAuthority;
|
|
5010
|
+
if (!authority)
|
|
5011
|
+
throw new Error("detached leader authority missing before tmux mutation");
|
|
5012
|
+
if (step.name === "tag-session") {
|
|
5013
|
+
runDetachedLeaderMutation(authority, step.args, false);
|
|
5014
|
+
runDetachedLeaderMutation(authority, ["set-option", "-q", "-t", authority.sessionName, "history-limit", String(DETACHED_TMUX_HISTORY_LIMIT)]);
|
|
5015
|
+
runDetachedLeaderMutation(authority, ["set-option", "-pq", "-t", authority.paneId, "history-limit", String(DETACHED_TMUX_HISTORY_LIMIT)]);
|
|
5016
|
+
// #3266: the owned leader pane must close with its process so a normal
|
|
5017
|
+
// child exit destroys the session naturally even when the user's tmux
|
|
5018
|
+
// config inherits remain-on-exit=on/failed.
|
|
5019
|
+
runDetachedLeaderMutation(authority, ["set-option", "-pq", "-t", authority.paneId, "remain-on-exit", "off"]);
|
|
5020
|
+
continue;
|
|
4254
5021
|
}
|
|
4255
|
-
|
|
4256
|
-
|
|
4257
|
-
|
|
4258
|
-
|
|
4259
|
-
|
|
4260
|
-
|
|
4261
|
-
|
|
4262
|
-
|
|
5022
|
+
if (step.name !== "split-and-capture-hud-pane") {
|
|
5023
|
+
throw new Error(`unexpected detached bootstrap mutation: ${step.name}`);
|
|
5024
|
+
}
|
|
5025
|
+
detachedHudAuthority = runDetachedLeaderSplit(authority, step.args);
|
|
5026
|
+
tagDetachedHudPane(authority, detachedHudAuthority, sessionId);
|
|
5027
|
+
for (const finalizeStep of buildDetachedSessionFinalizeSteps(authority.sessionName, detachedHudAuthority.paneId, authority.windowIndex, process.env.OMX_MOUSE !== "0", nativeWindows, detachedPreflight.shouldAttach, authority.paneId)) {
|
|
5028
|
+
if (finalizeStep.name === "attach-session") {
|
|
5029
|
+
attachStep = finalizeStep;
|
|
4263
5030
|
continue;
|
|
4264
5031
|
}
|
|
4265
|
-
|
|
4266
|
-
try {
|
|
4267
|
-
const startedAtMs = Date.now();
|
|
4268
|
-
execTmuxFileSync(finalizeStep.args, { stdio });
|
|
4269
|
-
if (finalizeStep.name === "attach-session") {
|
|
4270
|
-
assertDetachedAttachDidNotNoop(sessionName, Date.now() - startedAtMs, process.env);
|
|
4271
|
-
}
|
|
4272
|
-
}
|
|
4273
|
-
catch (err) {
|
|
4274
|
-
logCliOperationFailure(err);
|
|
4275
|
-
if (finalizeStep.name === "attach-session")
|
|
4276
|
-
throw new Error("failed to attach detached tmux session");
|
|
5032
|
+
if (finalizeStep.name === "sanitize-copy-mode-style")
|
|
4277
5033
|
continue;
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4284
|
-
|
|
4285
|
-
|
|
4286
|
-
|
|
4287
|
-
|
|
4288
|
-
}
|
|
4289
|
-
if (finalizeStep.name === "reconcile-hud-resize") {
|
|
4290
|
-
registerDetachedHudLayoutReconcileHook({
|
|
4291
|
-
hudPaneId,
|
|
4292
|
-
detachedLeaderPaneId,
|
|
4293
|
-
cwd,
|
|
4294
|
-
sessionId,
|
|
4295
|
-
omxBin,
|
|
4296
|
-
omxRootOverride,
|
|
4297
|
-
baseEnv: runtimeHookEnv,
|
|
4298
|
-
});
|
|
4299
|
-
}
|
|
5034
|
+
const targetsHudPane = new Set([
|
|
5035
|
+
"register-resize-hook",
|
|
5036
|
+
"register-client-attached-reconcile",
|
|
5037
|
+
"schedule-delayed-resize",
|
|
5038
|
+
"reconcile-hud-resize",
|
|
5039
|
+
]).has(finalizeStep.name);
|
|
5040
|
+
if (targetsHudPane)
|
|
5041
|
+
runDetachedHudMutation(authority, detachedHudAuthority, guardDetachedHudDeferredMutation(authority, detachedHudAuthority, finalizeStep.args));
|
|
5042
|
+
else
|
|
5043
|
+
runDetachedLeaderMutation(authority, finalizeStep.args);
|
|
4300
5044
|
}
|
|
4301
5045
|
}
|
|
5046
|
+
catch (error) {
|
|
5047
|
+
throw new DetachedLaunchSafetyError(step.name === "new-session" ? "inert-session" : "pane-id", error, detachedPreflight.report);
|
|
5048
|
+
}
|
|
4302
5049
|
}
|
|
4303
|
-
return
|
|
4304
|
-
}
|
|
4305
|
-
|
|
4306
|
-
|
|
4307
|
-
|
|
4308
|
-
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
5050
|
+
return undefined;
|
|
5051
|
+
};
|
|
5052
|
+
const launchTerminal = await executeDetachedLaunchStateMachine({ preflight: detachedPreflight }, {
|
|
5053
|
+
establish: bootstrapLeader,
|
|
5054
|
+
complete: async () => {
|
|
5055
|
+
const readyDeadline = Date.now() + DETACHED_LEADER_READY_TIMEOUT_MS;
|
|
5056
|
+
while (true) {
|
|
5057
|
+
const report = readDetachedLeaderReport(releaseMarkerPath);
|
|
5058
|
+
if (isDetachedReadyReportAuthorized(report, {
|
|
5059
|
+
nonce: detachedLaunchNonce,
|
|
5060
|
+
sessionId,
|
|
5061
|
+
sessionName,
|
|
5062
|
+
shouldAttach: detachedPreflight.shouldAttach,
|
|
5063
|
+
leaderPaneId: detachedLeaderPaneId,
|
|
5064
|
+
leaderPanePid: detachedLeaderAuthority?.panePid ?? null,
|
|
5065
|
+
})) {
|
|
5066
|
+
detachedLeaderPid = report.leaderPid;
|
|
5067
|
+
return { kind: "success" };
|
|
5068
|
+
}
|
|
5069
|
+
if (report?.nonce === detachedLaunchNonce && report.sessionId === sessionId && report.sessionName === sessionName && report.leaderPid && report.kind === "failed") {
|
|
5070
|
+
detachedLeaderPid = report.leaderPid;
|
|
5071
|
+
return { kind: "failure", operation: "session-instructions", error: new Error(report.error || "detached leader setup failed") };
|
|
5072
|
+
}
|
|
5073
|
+
if (Date.now() >= readyDeadline)
|
|
5074
|
+
return { kind: "failure", operation: "session-instructions", error: new Error("detached leader readiness timed out") };
|
|
5075
|
+
blockMs(20);
|
|
5076
|
+
}
|
|
5077
|
+
},
|
|
5078
|
+
createInertSession: async () => {
|
|
5079
|
+
const report = readDetachedLeaderReport(releaseMarkerPath);
|
|
5080
|
+
if (report?.kind !== "ready" || report.nonce !== detachedLaunchNonce || report.sessionId !== sessionId || report.sessionName !== sessionName || report.leaderPid !== detachedLeaderPid) {
|
|
5081
|
+
throw new Error("detached ready report does not bind the inert session");
|
|
5082
|
+
}
|
|
5083
|
+
return sessionName;
|
|
5084
|
+
},
|
|
5085
|
+
capturePane: async () => {
|
|
5086
|
+
const report = readDetachedLeaderReport(releaseMarkerPath);
|
|
5087
|
+
if (!detachedLeaderPaneId || report?.kind !== "ready" || report.paneId !== detachedLeaderPaneId || report.leaderPid !== detachedLeaderPid) {
|
|
5088
|
+
throw new Error("detached ready report does not bind the leader pane");
|
|
5089
|
+
}
|
|
5090
|
+
return detachedLeaderPaneId;
|
|
5091
|
+
},
|
|
5092
|
+
updateNameMetadata: async () => {
|
|
5093
|
+
const state = await readSessionState(cwd, detachedSelectedStateEnv);
|
|
5094
|
+
if (state?.session_id !== sessionId || state.tmux_session_name !== sessionName) {
|
|
5095
|
+
throw new Error("detached session-name metadata was not committed by the leader");
|
|
5096
|
+
}
|
|
5097
|
+
return "committed-released";
|
|
5098
|
+
},
|
|
5099
|
+
updatePaneMetadata: async (_binding, pane) => {
|
|
5100
|
+
const state = await readSessionState(cwd, detachedSelectedStateEnv);
|
|
5101
|
+
if (state?.session_id !== sessionId || state.tmux_pane_id !== pane) {
|
|
5102
|
+
throw new Error("detached pane metadata was not committed by the leader");
|
|
5103
|
+
}
|
|
5104
|
+
return "committed-released";
|
|
5105
|
+
},
|
|
5106
|
+
publishActiveRecord: async () => {
|
|
5107
|
+
const activeRecordPath = contextKey ? madmaxDetachedActiveRecordPath(runsRoot, contextKey) : join(omxRoot(cwd), "state", "detached-active-record.json");
|
|
5108
|
+
const record = readMadmaxDetachedActiveRecord(activeRecordPath);
|
|
5109
|
+
if (!record || record.launch_nonce !== detachedLaunchNonce || record.leader_pid !== detachedLeaderPid || record.session_id !== sessionId || record.tmux_session_name !== sessionName || record.tmux_pane_id !== detachedLeaderPaneId)
|
|
5110
|
+
throw new Error("detached active record does not bind the ready leader");
|
|
5111
|
+
const bytes = readFileSync(activeRecordPath, "utf-8");
|
|
5112
|
+
return { bytes, digest: createHash("sha256").update(bytes).digest("hex"), nonce: detachedLaunchNonce };
|
|
5113
|
+
},
|
|
5114
|
+
finalizeSetupFailure: async () => { },
|
|
5115
|
+
releaseBarrier: async () => {
|
|
5116
|
+
if (!detachedLeaderPid)
|
|
5117
|
+
throw new Error("detached leader PID missing before release");
|
|
5118
|
+
publishDetachedReleaseMarker(releaseMarkerPath, detachedLaunchNonce, sessionId, sessionName, detachedLeaderPid, detachedHudAuthority ?? undefined);
|
|
5119
|
+
},
|
|
5120
|
+
abortAndAwaitFinalization: async () => {
|
|
5121
|
+
// The leader has the retained binding. Publishing abort is the only
|
|
5122
|
+
// outer action permitted after a D9 write failure; a mismatched or
|
|
5123
|
+
// missing report leaves every artifact and the tmux session intact.
|
|
5124
|
+
if (!detachedLeaderPid) {
|
|
5125
|
+
rollbackFromPreReportAuthority = detachedLeaderAuthority !== null;
|
|
5126
|
+
return { acknowledged: false, rollbackAuthorized: rollbackFromPreReportAuthority };
|
|
5127
|
+
}
|
|
5128
|
+
const current = readDetachedLeaderReport(releaseMarkerPath);
|
|
5129
|
+
if (current?.nonce === detachedLaunchNonce && current.sessionId === sessionId &&
|
|
5130
|
+
current.sessionName === sessionName && current.leaderPid === detachedLeaderPid &&
|
|
5131
|
+
current.finalized === true && (current.kind === "failed" || current.kind === "terminal"))
|
|
5132
|
+
return {
|
|
5133
|
+
acknowledged: true,
|
|
5134
|
+
nonce: current.nonce,
|
|
5135
|
+
sessionId: current.sessionId,
|
|
5136
|
+
sessionName: current.sessionName,
|
|
5137
|
+
leaderPid: current.leaderPid,
|
|
5138
|
+
kind: current.kind,
|
|
5139
|
+
};
|
|
5140
|
+
publishDetachedAbortMarker(releaseMarkerPath, detachedLaunchNonce, sessionId, sessionName, detachedLeaderPid);
|
|
5141
|
+
const deadline = Date.now() + 30_000;
|
|
5142
|
+
while (Date.now() < deadline) {
|
|
5143
|
+
const report = readDetachedLeaderReport(releaseMarkerPath);
|
|
5144
|
+
if (report?.nonce === detachedLaunchNonce && report.sessionId === sessionId &&
|
|
5145
|
+
report.sessionName === sessionName && report.leaderPid === detachedLeaderPid &&
|
|
5146
|
+
report.finalized === true && (report.kind === "failed" || report.kind === "terminal"))
|
|
5147
|
+
return {
|
|
5148
|
+
acknowledged: true,
|
|
5149
|
+
nonce: report.nonce,
|
|
5150
|
+
sessionId: report.sessionId,
|
|
5151
|
+
sessionName: report.sessionName,
|
|
5152
|
+
leaderPid: report.leaderPid,
|
|
5153
|
+
kind: report.kind,
|
|
5154
|
+
};
|
|
5155
|
+
blockMs(20);
|
|
5156
|
+
}
|
|
5157
|
+
return { acknowledged: false };
|
|
5158
|
+
},
|
|
5159
|
+
attachOrReturn: async () => { if (attachStep)
|
|
5160
|
+
execTmuxFileSync(attachStep.args, { stdio: "inherit" }); },
|
|
5161
|
+
rollback: async (_ownedRecord, report) => {
|
|
5162
|
+
const attempt = async (step, operation) => {
|
|
5163
|
+
report.rollback.attempted.push(step);
|
|
4315
5164
|
try {
|
|
4316
|
-
|
|
5165
|
+
await operation();
|
|
4317
5166
|
}
|
|
4318
|
-
catch (
|
|
4319
|
-
|
|
4320
|
-
|
|
5167
|
+
catch (error) {
|
|
5168
|
+
report.rollback.failures.push({ step, status: "failed", code: detachedFailureCode(error) });
|
|
5169
|
+
}
|
|
5170
|
+
};
|
|
5171
|
+
await attempt("parent-env", async () => { if (detachedParentEnvFilePath)
|
|
5172
|
+
await rm(detachedParentEnvFilePath, { force: true }); });
|
|
5173
|
+
await attempt("runtime-codex-home", () => cleanupRuntimeCodexHome(runtimeCodexHomeForCleanup, projectLocalCodexHomeForCleanup));
|
|
5174
|
+
await attempt("rollback", () => { rmSync(releaseMarkerPath, { force: true }); rmSync(`${releaseMarkerPath}.release`, { force: true }); rmSync(`${releaseMarkerPath}.abort`, { force: true }); });
|
|
5175
|
+
if (createdSession) {
|
|
5176
|
+
for (const step of buildDetachedSessionRollbackSteps(sessionName, null, null, null)) {
|
|
5177
|
+
await attempt(`session:${step.name}`, () => {
|
|
5178
|
+
if (!detachedLeaderAuthority)
|
|
5179
|
+
throw new Error("detached leader authority missing before rollback");
|
|
5180
|
+
if (rollbackFromPreReportAuthority && step.name === "kill-session") {
|
|
5181
|
+
cleanupDetachedPreReportSession(detachedLeaderAuthority);
|
|
5182
|
+
return;
|
|
5183
|
+
}
|
|
5184
|
+
runDetachedLeaderMutation(detachedLeaderAuthority, step.args);
|
|
5185
|
+
});
|
|
4321
5186
|
}
|
|
4322
5187
|
}
|
|
5188
|
+
},
|
|
5189
|
+
});
|
|
5190
|
+
if (launchTerminal.kind === "attached" && detachedLeaderPid) {
|
|
5191
|
+
// #3266: propagate the finalized detached child's exit status to the invoking shell.
|
|
5192
|
+
// A rejected status protocol is only benign when the exact owned session
|
|
5193
|
+
// provably survives (manual detach); a destroyed or ambiguous session with
|
|
5194
|
+
// no authenticated finalized status is a protocol failure, never silent success.
|
|
5195
|
+
const attachResolution = resolveDetachedAttachExitStatus(releaseMarkerPath, { nonce: detachedLaunchNonce, sessionId, sessionName, leaderPid: detachedLeaderPid }, { exactSessionExists: () => probeExactDetachedSessionExists(detachedLeaderAuthority) });
|
|
5196
|
+
if (attachResolution === "protocol-failure") {
|
|
5197
|
+
process.stderr.write("[omx] detached session ended without an authenticated finalized terminal status; treating the launch as failed.\n");
|
|
5198
|
+
if (!process.exitCode)
|
|
5199
|
+
process.exitCode = 1;
|
|
4323
5200
|
}
|
|
4324
|
-
throw err;
|
|
4325
5201
|
}
|
|
5202
|
+
return { postLaunchHandledExternally: true };
|
|
4326
5203
|
};
|
|
4327
|
-
|
|
4328
|
-
|
|
4329
|
-
try {
|
|
4330
|
-
if (isMadmaxDetachedGuardEnabled(process.env) && contextKey) {
|
|
4331
|
-
return withMadmaxDetachedContextLock(runsRoot, contextKey, launchDetachedSession);
|
|
4332
|
-
}
|
|
4333
|
-
return launchDetachedSession();
|
|
4334
|
-
}
|
|
4335
|
-
catch (err) {
|
|
4336
|
-
if (err instanceof MadmaxDetachedReuseError || err instanceof MadmaxDetachedGuardError) {
|
|
4337
|
-
throw err;
|
|
4338
|
-
}
|
|
4339
|
-
logCliOperationFailure(err);
|
|
4340
|
-
// tmux not available or failed, just run codex directly
|
|
4341
|
-
runCodexBlocking(cwd, launchArgs, codexEnvWithNotify);
|
|
4342
|
-
return { postLaunchHandledExternally: false };
|
|
5204
|
+
if (contextKey) {
|
|
5205
|
+
return await withMadmaxDetachedContextLock(runsRoot, contextKey, launchDetachedSession);
|
|
4343
5206
|
}
|
|
5207
|
+
return await launchDetachedSession();
|
|
4344
5208
|
}
|
|
4345
5209
|
}
|
|
4346
5210
|
function listHudWatchPaneIdsInCurrentWindow(currentPaneId, owner = {}) {
|
|
@@ -4383,8 +5247,9 @@ export function buildTmuxShellCommand(command, args) {
|
|
|
4383
5247
|
function encodePowerShellCommand(commandText) {
|
|
4384
5248
|
return Buffer.from(commandText, "utf16le").toString("base64");
|
|
4385
5249
|
}
|
|
4386
|
-
function isCodexVersionRequest(args) {
|
|
4387
|
-
|
|
5250
|
+
export function isCodexVersionRequest(args) {
|
|
5251
|
+
const { omxArgs } = splitOmxArgsAtEndOfOptions(args);
|
|
5252
|
+
return omxArgs.some((arg) => CODEX_VERSION_FLAGS.has(arg));
|
|
4388
5253
|
}
|
|
4389
5254
|
export function buildWindowsPromptCommand(command, args) {
|
|
4390
5255
|
const invocation = [
|
|
@@ -4399,9 +5264,27 @@ export function buildWindowsPromptCommand(command, args) {
|
|
|
4399
5264
|
return `powershell.exe -NoLogo -NoExit -EncodedCommand ${encodePowerShellCommand(wrappedCommand)}`;
|
|
4400
5265
|
}
|
|
4401
5266
|
/**
|
|
4402
|
-
*
|
|
4403
|
-
*
|
|
4404
|
-
*
|
|
5267
|
+
* Build a Windows child command for detached ownership. Unlike the interactive
|
|
5268
|
+
* prompt wrapper, this process exits as soon as Codex does so the outer barrier
|
|
5269
|
+
* can run its post-launch cleanup.
|
|
5270
|
+
*/
|
|
5271
|
+
export function buildWindowsDetachedChildCommand(command, args) {
|
|
5272
|
+
const invocation = [
|
|
5273
|
+
"&",
|
|
5274
|
+
quotePowerShellArg(command),
|
|
5275
|
+
...args.map(quotePowerShellArg),
|
|
5276
|
+
].join(" ");
|
|
5277
|
+
const wrappedCommand = [
|
|
5278
|
+
"$ErrorActionPreference = 'Stop'",
|
|
5279
|
+
invocation,
|
|
5280
|
+
"exit $LASTEXITCODE",
|
|
5281
|
+
].join("; ");
|
|
5282
|
+
return `powershell.exe -NoLogo -NoProfile -NonInteractive -EncodedCommand ${encodePowerShellCommand(wrappedCommand)}`;
|
|
5283
|
+
}
|
|
5284
|
+
/**
|
|
5285
|
+
* Wrap a command for tmux pane execution while preserving the tmux pane cwd.
|
|
5286
|
+
* tmux already starts the pane at `-c <cwd>`; using a login shell here can
|
|
5287
|
+
* reset that cwd back to the shell's startup directory on some setups.
|
|
4405
5288
|
*
|
|
4406
5289
|
* Do not source user shell rc files by default. In issue #2282 the surviving
|
|
4407
5290
|
* OOM signature was thousands of bash processes, not MCP node children;
|
|
@@ -4434,45 +5317,323 @@ function quoteShellArg(value) {
|
|
|
4434
5317
|
function quotePowerShellArg(value) {
|
|
4435
5318
|
return `'${value.replace(/'/g, "''")}'`;
|
|
4436
5319
|
}
|
|
4437
|
-
export function
|
|
4438
|
-
|
|
4439
|
-
|
|
4440
|
-
|
|
4441
|
-
|
|
4442
|
-
|
|
4443
|
-
|
|
4444
|
-
|
|
4445
|
-
|
|
4446
|
-
|
|
4447
|
-
|
|
4448
|
-
|
|
4449
|
-
|
|
4450
|
-
|
|
4451
|
-
|
|
5320
|
+
export function decodeDetachedLeaderPayload(encoded) {
|
|
5321
|
+
if (!encoded || !/^[A-Za-z0-9_-]+$/.test(encoded))
|
|
5322
|
+
throw new Error("invalid detached leader payload");
|
|
5323
|
+
let value;
|
|
5324
|
+
try {
|
|
5325
|
+
value = JSON.parse(Buffer.from(encoded, "base64url").toString("utf8"));
|
|
5326
|
+
}
|
|
5327
|
+
catch {
|
|
5328
|
+
throw new Error("invalid detached leader payload");
|
|
5329
|
+
}
|
|
5330
|
+
if (!value || typeof value !== "object")
|
|
5331
|
+
throw new Error("invalid detached leader payload");
|
|
5332
|
+
const payload = value;
|
|
5333
|
+
const options = payload.preLaunchOptions;
|
|
5334
|
+
if (typeof payload.cwd !== "string" || typeof payload.sessionName !== "string" || typeof payload.sessionId !== "string" ||
|
|
5335
|
+
typeof payload.codexCmd !== "string" || !options || typeof options !== "object" ||
|
|
5336
|
+
typeof options.enableNotifyFallbackAuthority !== "boolean" ||
|
|
5337
|
+
typeof options.worktreeDirty !== "boolean" ||
|
|
5338
|
+
typeof options.shouldAttach !== "boolean")
|
|
5339
|
+
throw new Error("invalid detached leader payload");
|
|
5340
|
+
const notifyTempContract = options.notifyTempContract;
|
|
5341
|
+
if (notifyTempContract !== undefined && (!notifyTempContract || typeof notifyTempContract !== "object"))
|
|
5342
|
+
throw new Error("invalid detached leader payload");
|
|
5343
|
+
const parentEnv = payload.parentEnv;
|
|
5344
|
+
if (parentEnv !== undefined && (!parentEnv || typeof parentEnv !== "object" ||
|
|
5345
|
+
Object.entries(parentEnv).some(([key, value]) => !SHELL_ENV_NAME_PATTERN.test(key) || DETACHED_SESSION_PANE_ENV_KEYS.has(key) || typeof value !== "string" || value.includes("\0")))) {
|
|
5346
|
+
throw new Error("invalid detached leader parent environment");
|
|
5347
|
+
}
|
|
5348
|
+
return {
|
|
5349
|
+
cwd: payload.cwd, sessionName: payload.sessionName, sessionId: payload.sessionId, codexCmd: payload.codexCmd,
|
|
5350
|
+
...(typeof payload.codexHomeOverride === "string" ? { codexHomeOverride: payload.codexHomeOverride } : {}),
|
|
5351
|
+
...(typeof payload.projectLocalCodexHomeForCleanup === "string" ? { projectLocalCodexHomeForCleanup: payload.projectLocalCodexHomeForCleanup } : {}),
|
|
5352
|
+
...(typeof payload.runtimeCodexHomeForCleanup === "string" ? { runtimeCodexHomeForCleanup: payload.runtimeCodexHomeForCleanup } : {}),
|
|
5353
|
+
...(parentEnv ? { parentEnv: parentEnv } : {}),
|
|
5354
|
+
...(typeof payload.readyPath === "string" ? { readyPath: payload.readyPath } : {}),
|
|
5355
|
+
preLaunchOptions: {
|
|
5356
|
+
...(notifyTempContract ? { notifyTempContract: notifyTempContract } : {}),
|
|
5357
|
+
enableNotifyFallbackAuthority: options.enableNotifyFallbackAuthority,
|
|
5358
|
+
worktreeDirty: options.worktreeDirty,
|
|
5359
|
+
shouldAttach: options.shouldAttach,
|
|
5360
|
+
},
|
|
5361
|
+
};
|
|
4452
5362
|
}
|
|
4453
|
-
function
|
|
4454
|
-
|
|
4455
|
-
|
|
4456
|
-
|
|
4457
|
-
|
|
5363
|
+
function parseDetachedHudAuthorityProof(value) {
|
|
5364
|
+
if (!value || typeof value !== "object")
|
|
5365
|
+
return undefined;
|
|
5366
|
+
const proof = value;
|
|
5367
|
+
if (typeof proof.paneId !== "string" || !/^%[0-9]+$/.test(proof.paneId))
|
|
5368
|
+
return undefined;
|
|
5369
|
+
if (typeof proof.panePid !== "number" || !Number.isSafeInteger(proof.panePid) || proof.panePid <= 0)
|
|
5370
|
+
return undefined;
|
|
5371
|
+
if (typeof proof.sessionId !== "string" || !/^\$[0-9]+$/.test(proof.sessionId))
|
|
5372
|
+
return undefined;
|
|
5373
|
+
if (typeof proof.windowId !== "string" || !/^@[0-9]+$/.test(proof.windowId))
|
|
5374
|
+
return undefined;
|
|
5375
|
+
if (typeof proof.operationMarker !== "string" || !/^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(proof.operationMarker))
|
|
5376
|
+
return undefined;
|
|
5377
|
+
return { paneId: proof.paneId, panePid: proof.panePid, sessionId: proof.sessionId, windowId: proof.windowId, operationMarker: proof.operationMarker };
|
|
5378
|
+
}
|
|
5379
|
+
function teardownDetachedOwnedHudPane(leaderPaneId, payload, hudProof) {
|
|
5380
|
+
// #3266: on a normal child exit, kill exactly the bootstrap-proven OMX HUD pane so
|
|
5381
|
+
// the leader pane becomes the last live pane and the owned session closes naturally,
|
|
5382
|
+
// returning the attached client to its shell. Fail-closed: any doubt means zero mutations.
|
|
5383
|
+
// Contract: only the proven HUD is removed. A pane the user added to the detached
|
|
5384
|
+
// session is deliberately preserved, which intentionally keeps that session (and any
|
|
5385
|
+
// attached client) alive; natural closure and shell return happen only when no other
|
|
5386
|
+
// panes remain.
|
|
5387
|
+
const discovered = discoverDetachedHudAuthority(payload.sessionName, payload.sessionId);
|
|
5388
|
+
const proof = discovered ?? hudProof;
|
|
5389
|
+
if (!proof)
|
|
5390
|
+
return;
|
|
5391
|
+
try {
|
|
5392
|
+
cleanupDetachedHudPane(proof, discovered ? payload.sessionId : undefined);
|
|
5393
|
+
execTmuxFileSync(["kill-pane", "-t", leaderPaneId], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
5394
|
+
}
|
|
5395
|
+
catch (error) {
|
|
5396
|
+
traceDetachedLeaderPhase(`hud-teardown-error:${describeDetachedLeaderFailure(error)}`);
|
|
5397
|
+
logCliOperationFailure(error);
|
|
5398
|
+
}
|
|
5399
|
+
}
|
|
5400
|
+
function traceDetachedLeaderPhase(phase) {
|
|
5401
|
+
if (process.env.OMX_TEST_DETACHED_TRACE !== "1")
|
|
5402
|
+
return;
|
|
5403
|
+
const line = `[omx-test-detached] ${phase}\n`;
|
|
5404
|
+
process.stderr.write(line);
|
|
5405
|
+
const tracePath = process.env.OMX_TEST_DETACHED_TRACE_PATH;
|
|
5406
|
+
if (tracePath)
|
|
5407
|
+
appendFileSync(tracePath, line);
|
|
5408
|
+
}
|
|
5409
|
+
async function runDetachedSessionLeader(payload) {
|
|
5410
|
+
for (const [key, value] of Object.entries(payload.parentEnv ?? {})) {
|
|
5411
|
+
if (isDetachedLaunchControlPlaneKey(key, process.platform === "win32"))
|
|
5412
|
+
continue;
|
|
5413
|
+
process.env[key] = value;
|
|
5414
|
+
}
|
|
5415
|
+
let pane;
|
|
5416
|
+
const inheritedPane = process.env.TMUX_PANE?.trim();
|
|
5417
|
+
if (payload.preLaunchOptions.shouldAttach === false) {
|
|
5418
|
+
if (!inheritedPane || !/^%[0-9]+$/.test(inheritedPane)) {
|
|
5419
|
+
throw new Error("detached Hermes leader has no tmux pane identity");
|
|
5420
|
+
}
|
|
5421
|
+
pane = inheritedPane;
|
|
5422
|
+
}
|
|
5423
|
+
else {
|
|
5424
|
+
const paneSnapshot = execTmuxFileSync(["list-panes", "-t", payload.sessionName, "-F", "#{pane_id}\t#{pane_dead}\t#{pane_pid}"], { encoding: "utf-8", stdio: ["ignore", "pipe", "ignore"] });
|
|
5425
|
+
pane = parseDetachedLeaderPaneIdByPid(paneSnapshot, process.pid);
|
|
5426
|
+
}
|
|
5427
|
+
process.env.TMUX_PANE = pane;
|
|
5428
|
+
const established = await establishPreLaunchBinding(payload.cwd, payload.sessionId, {
|
|
5429
|
+
tmuxSessionName: payload.sessionName,
|
|
5430
|
+
tmuxPaneId: pane,
|
|
4458
5431
|
});
|
|
4459
|
-
|
|
5432
|
+
const binding = established.binding;
|
|
5433
|
+
let ownedRecord;
|
|
5434
|
+
let detachedHudProof;
|
|
5435
|
+
const nonce = payload.readyPath?.split(".").at(-2) || payload.sessionId;
|
|
5436
|
+
const contextKey = process.env[OMX_MADMAX_DETACHED_CONTEXT_ENV]?.trim();
|
|
5437
|
+
const activeRecordPath = contextKey
|
|
5438
|
+
? madmaxDetachedActiveRecordPath(resolveMadmaxRunsRoot(process.env), contextKey)
|
|
5439
|
+
: join(omxRoot(payload.cwd), "state", "detached-active-record.json");
|
|
5440
|
+
let externalInterrupt;
|
|
5441
|
+
try {
|
|
5442
|
+
const completion = await completePreLaunchSetup(payload.cwd, payload.sessionId, payload.preLaunchOptions.notifyTempContract, payload.codexHomeOverride, payload.preLaunchOptions.enableNotifyFallbackAuthority, payload.preLaunchOptions.worktreeDirty);
|
|
5443
|
+
if (completion.kind === "failure") {
|
|
5444
|
+
const finalization = await finalizeBoundOnce(binding, "setup-failure", payload.cwd);
|
|
5445
|
+
if (!finalization.finalized)
|
|
5446
|
+
throw new Error(`bound setup finalization denied: ${finalization.cleanup.comparison?.reason ?? "unknown reason"}`);
|
|
5447
|
+
throw completion.error;
|
|
5448
|
+
}
|
|
5449
|
+
const record = {
|
|
5450
|
+
version: 1, context_key: contextKey ?? payload.sessionId, created_at: new Date().toISOString(),
|
|
5451
|
+
source_cwd: payload.cwd, argv: [payload.codexCmd], run_dir: process.env.OMX_ROOT ?? payload.cwd,
|
|
5452
|
+
tmux_session_name: payload.sessionName, session_id: payload.sessionId, tmux_pane_id: pane,
|
|
5453
|
+
launch_nonce: nonce, leader_pid: process.pid, base_state_root: binding.context.baseStateDir,
|
|
5454
|
+
lifecycle_phase: "ready",
|
|
5455
|
+
};
|
|
5456
|
+
const runtimeBinding = queryMadmaxDetachedRuntimeBinding(record);
|
|
5457
|
+
ownedRecord = writeMadmaxDetachedActiveRecord(activeRecordPath, {
|
|
5458
|
+
...record,
|
|
5459
|
+
...(runtimeBinding ? {
|
|
5460
|
+
tmux_internal_session_id: runtimeBinding.internalSessionId,
|
|
5461
|
+
tmux_session_created: runtimeBinding.sessionCreated,
|
|
5462
|
+
tmux_pane_pid: runtimeBinding.panePid,
|
|
5463
|
+
} : {}),
|
|
5464
|
+
});
|
|
5465
|
+
if (payload.readyPath) {
|
|
5466
|
+
writeDetachedLeaderReport(payload.readyPath, { version: 1, kind: "ready", nonce, sessionId: payload.sessionId, sessionName: payload.sessionName, paneId: pane, leaderPid: process.pid });
|
|
5467
|
+
const releasePath = `${payload.readyPath}.release`;
|
|
5468
|
+
const abortPath = `${payload.readyPath}.abort`;
|
|
5469
|
+
const releaseDeadline = Date.now() + 30_000;
|
|
5470
|
+
let interrupted;
|
|
5471
|
+
const interrupt = (signal) => { interrupted = signal; };
|
|
5472
|
+
process.once("SIGINT", () => interrupt("SIGINT"));
|
|
5473
|
+
process.once("SIGTERM", () => interrupt("SIGTERM"));
|
|
5474
|
+
process.once("SIGHUP", () => interrupt("SIGHUP"));
|
|
5475
|
+
while (true) {
|
|
5476
|
+
if (interrupted)
|
|
5477
|
+
throw new Error(`detached leader interrupted before release: ${interrupted}`);
|
|
5478
|
+
const release = readDetachedLeaderReport(releasePath);
|
|
5479
|
+
if (release?.kind === "release" && release.nonce === nonce && release.sessionId === payload.sessionId && release.sessionName === payload.sessionName && release.leaderPid === process.pid) {
|
|
5480
|
+
detachedHudProof = parseDetachedHudAuthorityProof(release.hud);
|
|
5481
|
+
break;
|
|
5482
|
+
}
|
|
5483
|
+
const abort = readDetachedLeaderReport(abortPath);
|
|
5484
|
+
if (abort?.kind === "abort" && abort.nonce === nonce && abort.sessionId === payload.sessionId && abort.sessionName === payload.sessionName && abort.leaderPid === process.pid) {
|
|
5485
|
+
throw new Error("detached launch aborted before release");
|
|
5486
|
+
}
|
|
5487
|
+
if (Date.now() >= releaseDeadline)
|
|
5488
|
+
throw new Error("detached leader release timed out");
|
|
5489
|
+
blockMs(20);
|
|
5490
|
+
}
|
|
5491
|
+
rmSync(releasePath, { force: true });
|
|
5492
|
+
rmSync(abortPath, { force: true });
|
|
5493
|
+
}
|
|
5494
|
+
const child = spawn(process.platform === "win32" ? "powershell.exe" : "/bin/sh", process.platform === "win32" ? ["-NoProfile", "-Command", payload.codexCmd] : ["-c", payload.codexCmd], {
|
|
5495
|
+
cwd: payload.cwd,
|
|
5496
|
+
stdio: "inherit",
|
|
5497
|
+
...(process.platform === "win32" ? {} : { detached: true }),
|
|
5498
|
+
});
|
|
5499
|
+
let escalation;
|
|
5500
|
+
const signalChildTree = (signal, force = false) => {
|
|
5501
|
+
if (!child.pid)
|
|
5502
|
+
return;
|
|
5503
|
+
try {
|
|
5504
|
+
if (process.platform === "win32")
|
|
5505
|
+
execFileSync("taskkill", ["/PID", String(child.pid), "/T", ...(force ? ["/F"] : [])], { stdio: "ignore" });
|
|
5506
|
+
else
|
|
5507
|
+
process.kill(-child.pid, signal);
|
|
5508
|
+
}
|
|
5509
|
+
catch { }
|
|
5510
|
+
};
|
|
5511
|
+
const forward = (signal) => {
|
|
5512
|
+
externalInterrupt ??= signal;
|
|
5513
|
+
signalChildTree(signal);
|
|
5514
|
+
if (!escalation) {
|
|
5515
|
+
escalation = setTimeout(() => signalChildTree("SIGKILL", true), 5_000);
|
|
5516
|
+
escalation.unref();
|
|
5517
|
+
}
|
|
5518
|
+
};
|
|
5519
|
+
process.once("SIGINT", () => forward("SIGINT"));
|
|
5520
|
+
process.once("SIGTERM", () => forward("SIGTERM"));
|
|
5521
|
+
process.once("SIGHUP", () => forward("SIGHUP"));
|
|
5522
|
+
traceDetachedLeaderPhase("child-wait-start");
|
|
5523
|
+
const outcome = await new Promise((resolveChild, rejectChild) => {
|
|
5524
|
+
child.once("error", rejectChild);
|
|
5525
|
+
child.once("exit", (code, signal) => resolveChild({ code, signal }));
|
|
5526
|
+
});
|
|
5527
|
+
traceDetachedLeaderPhase(`child-exit:${String(outcome.code)}:${String(outcome.signal)}`);
|
|
5528
|
+
if (escalation)
|
|
5529
|
+
clearTimeout(escalation);
|
|
5530
|
+
if (process.platform !== "win32" && child.pid) {
|
|
5531
|
+
const waitForGroupExit = async (deadline) => {
|
|
5532
|
+
while (Date.now() < deadline) {
|
|
5533
|
+
try {
|
|
5534
|
+
process.kill(-child.pid, 0);
|
|
5535
|
+
await new Promise((resolveWait) => setTimeout(resolveWait, 20));
|
|
5536
|
+
}
|
|
5537
|
+
catch {
|
|
5538
|
+
return true;
|
|
5539
|
+
}
|
|
5540
|
+
}
|
|
5541
|
+
return false;
|
|
5542
|
+
};
|
|
5543
|
+
let groupGone = await waitForGroupExit(Date.now() + 5_000);
|
|
5544
|
+
if (!groupGone) {
|
|
5545
|
+
signalChildTree("SIGKILL", true);
|
|
5546
|
+
groupGone = await waitForGroupExit(Date.now() + 5_000);
|
|
5547
|
+
}
|
|
5548
|
+
if (!groupGone)
|
|
5549
|
+
throw new Error("detached child process group did not disappear after forced termination");
|
|
5550
|
+
}
|
|
5551
|
+
const signalExitCodes = { SIGHUP: 129, SIGINT: 130, SIGTERM: 143, SIGKILL: 137 };
|
|
5552
|
+
process.exitCode = externalInterrupt
|
|
5553
|
+
? signalExitCodes[externalInterrupt] ?? 1
|
|
5554
|
+
: outcome.code ?? (outcome.signal ? signalExitCodes[outcome.signal] ?? 1 : 1);
|
|
5555
|
+
traceDetachedLeaderPhase("post-launch-start");
|
|
5556
|
+
await postLaunch(payload.cwd, payload.sessionId, binding, payload.codexHomeOverride, payload.preLaunchOptions.enableNotifyFallbackAuthority, payload.projectLocalCodexHomeForCleanup, async () => {
|
|
5557
|
+
if (payload.readyPath)
|
|
5558
|
+
writeDetachedLeaderReport(payload.readyPath, {
|
|
5559
|
+
version: 1, kind: "terminal", nonce, sessionId: payload.sessionId, sessionName: payload.sessionName,
|
|
5560
|
+
paneId: pane, leaderPid: process.pid, finalized: true,
|
|
5561
|
+
...(typeof process.exitCode === "number" ? { exitStatus: process.exitCode } : {}),
|
|
5562
|
+
});
|
|
5563
|
+
traceDetachedLeaderPhase("terminal-report-written");
|
|
5564
|
+
if (ownedRecord) {
|
|
5565
|
+
const bytes = readFileSync(activeRecordPath, "utf-8");
|
|
5566
|
+
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
5567
|
+
if (bytes === ownedRecord.bytes && digest === ownedRecord.digest)
|
|
5568
|
+
rmSync(activeRecordPath, { force: true });
|
|
5569
|
+
}
|
|
5570
|
+
if (!externalInterrupt && outcome.code !== null) {
|
|
5571
|
+
traceDetachedLeaderPhase("hud-teardown-start");
|
|
5572
|
+
teardownDetachedOwnedHudPane(pane, payload, detachedHudProof);
|
|
5573
|
+
traceDetachedLeaderPhase("hud-teardown-done");
|
|
5574
|
+
}
|
|
5575
|
+
});
|
|
5576
|
+
traceDetachedLeaderPhase("post-launch-done");
|
|
5577
|
+
await cleanupRuntimeCodexHome(payload.runtimeCodexHomeForCleanup, payload.projectLocalCodexHomeForCleanup);
|
|
5578
|
+
traceDetachedLeaderPhase("leader-return");
|
|
5579
|
+
}
|
|
5580
|
+
catch (error) {
|
|
5581
|
+
let failure = error;
|
|
5582
|
+
let finalized = false;
|
|
5583
|
+
try {
|
|
5584
|
+
await postLaunch(payload.cwd, payload.sessionId, binding, payload.codexHomeOverride, payload.preLaunchOptions.enableNotifyFallbackAuthority, payload.projectLocalCodexHomeForCleanup);
|
|
5585
|
+
finalized = true;
|
|
5586
|
+
await cleanupRuntimeCodexHome(payload.runtimeCodexHomeForCleanup, payload.projectLocalCodexHomeForCleanup);
|
|
5587
|
+
}
|
|
5588
|
+
catch (lifecycleError) {
|
|
5589
|
+
failure = new AggregateError([error, lifecycleError], `detached leader failure finalization did not complete: launch=${describeDetachedLeaderFailure(error)}; lifecycle=${describeDetachedLeaderFailure(lifecycleError)}`);
|
|
5590
|
+
}
|
|
5591
|
+
if (finalized && ownedRecord) {
|
|
5592
|
+
try {
|
|
5593
|
+
const bytes = readFileSync(activeRecordPath, "utf-8");
|
|
5594
|
+
const digest = createHash("sha256").update(bytes).digest("hex");
|
|
5595
|
+
if (bytes === ownedRecord.bytes && digest === ownedRecord.digest)
|
|
5596
|
+
rmSync(activeRecordPath, { force: true });
|
|
5597
|
+
}
|
|
5598
|
+
catch { }
|
|
5599
|
+
}
|
|
5600
|
+
if (payload.readyPath)
|
|
5601
|
+
writeDetachedLeaderReport(payload.readyPath, {
|
|
5602
|
+
version: 1, kind: "failed", nonce, sessionId: payload.sessionId, sessionName: payload.sessionName,
|
|
5603
|
+
leaderPid: process.pid, finalized, error: failure instanceof Error ? failure.message : String(failure),
|
|
5604
|
+
});
|
|
5605
|
+
throw failure;
|
|
5606
|
+
}
|
|
5607
|
+
finally {
|
|
5608
|
+
await closeLaunchSessionBindingOnce(binding);
|
|
5609
|
+
}
|
|
4460
5610
|
}
|
|
4461
5611
|
/**
|
|
4462
5612
|
* postLaunch: Clean up after Codex exits.
|
|
4463
5613
|
* Each step is independently fault-tolerant (try/catch per step).
|
|
4464
5614
|
*/
|
|
4465
|
-
export async function postLaunch(cwd, sessionId, codexHomeOverride, enableNotifyFallbackAuthority = false, projectLocalCodexHomeForCleanup) {
|
|
4466
|
-
|
|
4467
|
-
|
|
5615
|
+
export async function postLaunch(cwd, sessionId, binding, codexHomeOverride, enableNotifyFallbackAuthority = false, projectLocalCodexHomeForCleanup, onFinalized) {
|
|
5616
|
+
const sessionStartedAt = binding.startedAt;
|
|
5617
|
+
// Pointer authority is consumed before any terminal cleanup. A denied or
|
|
5618
|
+
// unproven binding must leave every cleanup target untouched.
|
|
5619
|
+
let terminalError;
|
|
4468
5620
|
try {
|
|
4469
|
-
const
|
|
4470
|
-
|
|
5621
|
+
const finalization = await finalizeBoundOnce(binding, "post-launch", cwd);
|
|
5622
|
+
if (!finalization.finalized)
|
|
5623
|
+
terminalError = new Error(`bound session finalization denied: ${finalization.cleanup.comparison?.reason ?? "unknown reason"}`);
|
|
5624
|
+
const closeFailure = finalization.cleanup.capability.find((evidence) => evidence.status === "failed");
|
|
5625
|
+
if (closeFailure)
|
|
5626
|
+
terminalError = new Error(`bound session retained close failed: ${closeFailure.error?.message ?? "unknown error"}`);
|
|
4471
5627
|
}
|
|
4472
|
-
catch (
|
|
4473
|
-
|
|
4474
|
-
// Non-fatal
|
|
5628
|
+
catch (error) {
|
|
5629
|
+
terminalError = error;
|
|
4475
5630
|
}
|
|
5631
|
+
if (terminalError) {
|
|
5632
|
+
// Denial diagnostics are complete; do not mutate ordinary lifecycle state.
|
|
5633
|
+
await closeLaunchSessionBindingOnce(binding);
|
|
5634
|
+
throw terminalError;
|
|
5635
|
+
}
|
|
5636
|
+
await onFinalized?.();
|
|
4476
5637
|
// 0. Reap MCP orphans left behind by the session that just exited.
|
|
4477
5638
|
await reapPostLaunchOrphanedMcpProcesses();
|
|
4478
5639
|
// 0. Flush fallback watcher once to reduce race with fast codex exit.
|
|
@@ -4523,13 +5684,6 @@ export async function postLaunch(cwd, sessionId, codexHomeOverride, enableNotify
|
|
|
4523
5684
|
catch (err) {
|
|
4524
5685
|
console.error(`[omx] postLaunch: model instructions cleanup failed: ${err instanceof Error ? err.message : err}`);
|
|
4525
5686
|
}
|
|
4526
|
-
// 2. Archive session (write history, delete session.json)
|
|
4527
|
-
try {
|
|
4528
|
-
await writeSessionEnd(cwd, sessionId);
|
|
4529
|
-
}
|
|
4530
|
-
catch (err) {
|
|
4531
|
-
console.error(`[omx] postLaunch: session archive failed: ${err instanceof Error ? err.message : err}`);
|
|
4532
|
-
}
|
|
4533
5687
|
// 2.5. Best-effort wiki session capture
|
|
4534
5688
|
try {
|
|
4535
5689
|
const { onSessionEnd } = await import("../wiki/lifecycle.js");
|
|
@@ -4601,10 +5755,59 @@ export async function postLaunch(cwd, sessionId, codexHomeOverride, enableNotify
|
|
|
4601
5755
|
logCliOperationFailure(err);
|
|
4602
5756
|
// Non-fatal
|
|
4603
5757
|
}
|
|
5758
|
+
const closeEvidence = await closeLaunchSessionBindingOnce(binding);
|
|
5759
|
+
if (closeEvidence.status === "failed") {
|
|
5760
|
+
throw new Error(`bound session retained close failed: ${closeEvidence.error?.message ?? "unknown error"}`);
|
|
5761
|
+
}
|
|
4604
5762
|
}
|
|
4605
|
-
export async function runDetachedSessionPostLaunch(cwd, sessionId,
|
|
4606
|
-
|
|
4607
|
-
|
|
5763
|
+
export async function runDetachedSessionPostLaunch(cwd, sessionId, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, releaseMarkerPath) {
|
|
5764
|
+
// The detached child is intentionally unbound: it must never perform parent
|
|
5765
|
+
// pointer finalization or cleanup after the release barrier. It still owns
|
|
5766
|
+
// every non-pointer terminal lifecycle responsibility.
|
|
5767
|
+
const failures = [];
|
|
5768
|
+
const strict = async (operation) => {
|
|
5769
|
+
try {
|
|
5770
|
+
await operation();
|
|
5771
|
+
}
|
|
5772
|
+
catch (error) {
|
|
5773
|
+
failures.push(error);
|
|
5774
|
+
}
|
|
5775
|
+
};
|
|
5776
|
+
await strict(() => reapPostLaunchOrphanedMcpProcesses());
|
|
5777
|
+
await strict(() => flushNotifyFallbackOnce(cwd, { sessionId }));
|
|
5778
|
+
await strict(() => stopNotifyFallbackWatcher(cwd));
|
|
5779
|
+
await strict(() => flushHookDerivedWatcherOnce(cwd));
|
|
5780
|
+
await strict(() => stopHookDerivedWatcher(cwd));
|
|
5781
|
+
if (projectLocalCodexHomeForCleanup) {
|
|
5782
|
+
await strict(() => cleanCodexModelAvailabilityNuxIfNeeded(join(projectLocalCodexHomeForCleanup, "config.toml")));
|
|
5783
|
+
}
|
|
5784
|
+
await strict(() => removeSessionModelInstructionsFile(cwd, sessionId));
|
|
5785
|
+
await strict(async () => {
|
|
5786
|
+
const { onSessionEnd } = await import("../wiki/lifecycle.js");
|
|
5787
|
+
onSessionEnd({ cwd, session_id: sessionId });
|
|
5788
|
+
});
|
|
5789
|
+
await strict(() => cleanupPostLaunchModeStateFiles(cwd, sessionId));
|
|
5790
|
+
await strict(async () => {
|
|
5791
|
+
const { notifyLifecycle } = await import("../notifications/index.js");
|
|
5792
|
+
await notifyLifecycle("session-end", {
|
|
5793
|
+
sessionId, projectPath: cwd, projectName: basename(cwd), reason: "session_exit",
|
|
5794
|
+
});
|
|
5795
|
+
});
|
|
5796
|
+
await strict(async () => {
|
|
5797
|
+
const { markOwnedTeamsLeaderSessionStopped } = await import("../team/state.js");
|
|
5798
|
+
await markOwnedTeamsLeaderSessionStopped(cwd, sessionId);
|
|
5799
|
+
});
|
|
5800
|
+
await strict(() => emitNativeHookEvent(cwd, "session-end", {
|
|
5801
|
+
session_id: sessionId,
|
|
5802
|
+
context: buildNativeHookBaseContext(cwd, sessionId, "finished", {
|
|
5803
|
+
project_path: cwd, project_name: basename(cwd), reason: "session_exit", status: "finished",
|
|
5804
|
+
}),
|
|
5805
|
+
}));
|
|
5806
|
+
await strict(() => cleanupRuntimeCodexHome(runtimeCodexHomeForCleanup, projectLocalCodexHomeForCleanup));
|
|
5807
|
+
if (releaseMarkerPath)
|
|
5808
|
+
await strict(() => rm(releaseMarkerPath, { force: true }));
|
|
5809
|
+
if (failures.length > 0)
|
|
5810
|
+
throw new AggregateError(failures, "detached post-launch lifecycle cleanup failed");
|
|
4608
5811
|
}
|
|
4609
5812
|
async function emitNativeHookEvent(cwd, event, opts = {}) {
|
|
4610
5813
|
const payload = buildHookEvent(event, {
|
|
@@ -5043,14 +6246,13 @@ async function listHookVisibleRunDirStateRefs(cwd) {
|
|
|
5043
6246
|
if (!sourceCwd || !runDir)
|
|
5044
6247
|
return;
|
|
5045
6248
|
try {
|
|
5046
|
-
if (canonicalizePathForRunDirMatch(sourceCwd) !== canonicalCwd
|
|
5047
|
-
(!worktreeCwd || canonicalizePathForRunDirMatch(worktreeCwd) !== canonicalCwd))
|
|
6249
|
+
if (canonicalizePathForRunDirMatch(sourceCwd) !== canonicalCwd
|
|
6250
|
+
&& (!worktreeCwd || canonicalizePathForRunDirMatch(worktreeCwd) !== canonicalCwd))
|
|
5048
6251
|
return;
|
|
5049
6252
|
const resolvedRunDir = resolve(runDir);
|
|
5050
6253
|
if (resolvedRunDir !== canonicalRunsRoot
|
|
5051
|
-
&& !resolvedRunDir.startsWith(`${canonicalRunsRoot}/`))
|
|
6254
|
+
&& !resolvedRunDir.startsWith(`${canonicalRunsRoot}/`))
|
|
5052
6255
|
return;
|
|
5053
|
-
}
|
|
5054
6256
|
runDirs.add(resolvedRunDir);
|
|
5055
6257
|
}
|
|
5056
6258
|
catch {
|
|
@@ -5103,7 +6305,7 @@ async function listHookVisibleRunDirStateRefs(cwd) {
|
|
|
5103
6305
|
for (const dir of candidateDirs) {
|
|
5104
6306
|
const files = await readdir(dir).catch(() => []);
|
|
5105
6307
|
for (const file of files) {
|
|
5106
|
-
if (!
|
|
6308
|
+
if (!isModeStateFilename(file))
|
|
5107
6309
|
continue;
|
|
5108
6310
|
const path = join(dir, file);
|
|
5109
6311
|
if (seenPaths.has(path))
|
|
@@ -5119,41 +6321,362 @@ async function listHookVisibleRunDirStateRefs(cwd) {
|
|
|
5119
6321
|
}
|
|
5120
6322
|
return refs.sort((a, b) => a.mode.localeCompare(b.mode));
|
|
5121
6323
|
}
|
|
5122
|
-
|
|
5123
|
-
const
|
|
5124
|
-
|
|
6324
|
+
function isCanonicalPathWithin(parent, child, allowEqual = false) {
|
|
6325
|
+
const rel = relative(parent, child);
|
|
6326
|
+
if (rel === "")
|
|
6327
|
+
return allowEqual;
|
|
6328
|
+
return rel !== ".." && !rel.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`) && !isAbsolute(rel);
|
|
6329
|
+
}
|
|
6330
|
+
async function selectAuthorizedHookVisibleRunDirState(cwd) {
|
|
6331
|
+
const runsRoot = resolveMadmaxRunsRoot(process.env);
|
|
6332
|
+
const activeDir = join(runsRoot, MADMAX_DETACHED_ACTIVE_DIR);
|
|
6333
|
+
const canonicalCwd = canonicalizePathForRunDirMatch(cwd);
|
|
6334
|
+
let canonicalRunsRoot;
|
|
6335
|
+
try {
|
|
6336
|
+
canonicalRunsRoot = realpathSync(resolve(runsRoot));
|
|
6337
|
+
}
|
|
6338
|
+
catch {
|
|
6339
|
+
return null;
|
|
6340
|
+
}
|
|
6341
|
+
const candidates = [];
|
|
6342
|
+
const files = await readdir(activeDir).catch(() => []);
|
|
6343
|
+
for (const file of files) {
|
|
6344
|
+
if (!file.endsWith(".json"))
|
|
6345
|
+
continue;
|
|
6346
|
+
const record = readMadmaxDetachedActiveRecord(join(activeDir, file));
|
|
6347
|
+
if (!record || file !== `${record.context_key}.json`)
|
|
6348
|
+
continue;
|
|
6349
|
+
if (!record.session_id || normalizeSessionId(record.session_id) !== record.session_id)
|
|
6350
|
+
continue;
|
|
6351
|
+
if (!hasMatchingMadmaxDetachedRuntimeBinding(record))
|
|
6352
|
+
continue;
|
|
6353
|
+
if (canonicalizePathForRunDirMatch(record.source_cwd) !== canonicalCwd
|
|
6354
|
+
&& (!record.worktree_cwd || canonicalizePathForRunDirMatch(record.worktree_cwd) !== canonicalCwd))
|
|
6355
|
+
continue;
|
|
6356
|
+
try {
|
|
6357
|
+
const canonicalRunDir = realpathSync(resolve(record.run_dir));
|
|
6358
|
+
if (!isCanonicalPathWithin(canonicalRunsRoot, canonicalRunDir)) {
|
|
6359
|
+
throw new Error("run directory escapes the authorized runs root");
|
|
6360
|
+
}
|
|
6361
|
+
const stateDir = realpathSync(join(canonicalRunDir, ".omx", "state"));
|
|
6362
|
+
if (!isCanonicalPathWithin(canonicalRunDir, stateDir)) {
|
|
6363
|
+
throw new Error("state directory escapes the authorized run directory");
|
|
6364
|
+
}
|
|
6365
|
+
const session = JSON.parse(await readFile(join(stateDir, "session.json"), "utf-8"));
|
|
6366
|
+
if (session.session_id !== record.session_id)
|
|
6367
|
+
throw new Error("run session pointer changed");
|
|
6368
|
+
const sessionDir = realpathSync(join(stateDir, "sessions", record.session_id));
|
|
6369
|
+
if (!isCanonicalPathWithin(stateDir, sessionDir)) {
|
|
6370
|
+
throw new Error("session directory escapes the authorized state directory");
|
|
6371
|
+
}
|
|
6372
|
+
candidates.push({ sessionDir, sessionId: record.session_id, record });
|
|
6373
|
+
}
|
|
6374
|
+
catch (err) {
|
|
6375
|
+
throw new Error(`Refusing cancellation because detached run authority is invalid: ${record.run_dir}.`, { cause: err });
|
|
6376
|
+
}
|
|
6377
|
+
}
|
|
6378
|
+
if (candidates.length > 1)
|
|
6379
|
+
throw new Error("Refusing cancellation because multiple detached run authorities match.");
|
|
6380
|
+
if (candidates.length === 0)
|
|
6381
|
+
return null;
|
|
6382
|
+
const [{ sessionDir, sessionId, record }] = candidates;
|
|
6383
|
+
const refs = [];
|
|
6384
|
+
const stateFiles = await readdir(sessionDir).catch(() => []);
|
|
6385
|
+
for (const file of stateFiles) {
|
|
6386
|
+
if (!isModeStateFilename(file))
|
|
6387
|
+
continue;
|
|
6388
|
+
const path = join(sessionDir, file);
|
|
6389
|
+
try {
|
|
6390
|
+
const fileStat = lstatSync(path);
|
|
6391
|
+
if (!fileStat.isFile() || fileStat.isSymbolicLink()) {
|
|
6392
|
+
throw new Error(`Refusing cancellation through non-regular run state target: ${path}.`);
|
|
6393
|
+
}
|
|
6394
|
+
const canonicalFile = realpathSync(path);
|
|
6395
|
+
if (!isCanonicalPathWithin(sessionDir, canonicalFile)) {
|
|
6396
|
+
throw new Error(`Refusing cancellation outside authorized run session: ${path}.`);
|
|
6397
|
+
}
|
|
6398
|
+
refs.push({
|
|
6399
|
+
mode: file.slice(0, -"-state.json".length),
|
|
6400
|
+
path: canonicalFile,
|
|
6401
|
+
scope: "session",
|
|
6402
|
+
});
|
|
6403
|
+
}
|
|
6404
|
+
catch (err) {
|
|
6405
|
+
throw new Error(`Refusing cancellation because detached run state authority is invalid: ${path}.`, { cause: err });
|
|
6406
|
+
}
|
|
6407
|
+
}
|
|
6408
|
+
return { refs: refs.sort((a, b) => a.mode.localeCompare(b.mode)), sessionId, sessionDir, record };
|
|
6409
|
+
}
|
|
6410
|
+
function assertCancellationAuthorityPath(baseStateDir, authorityRoot) {
|
|
6411
|
+
const resolvedBase = resolve(baseStateDir);
|
|
6412
|
+
const resolvedAuthority = resolve(authorityRoot);
|
|
6413
|
+
const baseStat = lstatSync(resolvedBase);
|
|
6414
|
+
if (!baseStat.isDirectory() || baseStat.isSymbolicLink() || realpathSync(resolvedBase) !== resolvedBase) {
|
|
6415
|
+
throw new Error(`Refusing cancellation through symlinked state authority root: ${baseStateDir}.`);
|
|
6416
|
+
}
|
|
6417
|
+
if (!isCanonicalPathWithin(resolvedBase, resolvedAuthority, true)) {
|
|
6418
|
+
throw new Error(`Refusing cancellation outside base state authority: ${authorityRoot}.`);
|
|
6419
|
+
}
|
|
6420
|
+
let current = resolvedBase;
|
|
6421
|
+
const rel = relative(resolvedBase, resolvedAuthority);
|
|
6422
|
+
for (const component of rel ? rel.split(/[\\/]+/) : []) {
|
|
6423
|
+
current = join(current, component);
|
|
6424
|
+
const componentStat = lstatSync(current);
|
|
6425
|
+
if (!componentStat.isDirectory() || componentStat.isSymbolicLink() || realpathSync(current) !== current) {
|
|
6426
|
+
throw new Error(`Refusing cancellation through symlinked authority component: ${current}.`);
|
|
6427
|
+
}
|
|
6428
|
+
}
|
|
6429
|
+
return resolvedAuthority;
|
|
6430
|
+
}
|
|
6431
|
+
function collectCancellationOwnerIds(baseStateDir, sessionId) {
|
|
6432
|
+
const ownerIds = new Set();
|
|
6433
|
+
if (sessionId)
|
|
6434
|
+
ownerIds.add(sessionId);
|
|
6435
|
+
try {
|
|
6436
|
+
const pointer = JSON.parse(readFileSync(join(baseStateDir, "session.json"), "utf-8"));
|
|
6437
|
+
for (const field of [
|
|
6438
|
+
"session_id",
|
|
6439
|
+
"native_session_id",
|
|
6440
|
+
"codex_session_id",
|
|
6441
|
+
"owner_omx_session_id",
|
|
6442
|
+
"owner_codex_session_id",
|
|
6443
|
+
]) {
|
|
6444
|
+
const normalized = normalizeSessionId(pointer[field]);
|
|
6445
|
+
if (normalized)
|
|
6446
|
+
ownerIds.add(normalized);
|
|
6447
|
+
}
|
|
6448
|
+
}
|
|
6449
|
+
catch { }
|
|
6450
|
+
return ownerIds;
|
|
6451
|
+
}
|
|
6452
|
+
function assertCancellationOwnerMetadata(value, ownerIds, path) {
|
|
6453
|
+
if (Object.prototype.hasOwnProperty.call(value, "session_id")) {
|
|
6454
|
+
const sessionId = normalizeSessionId(value.session_id);
|
|
6455
|
+
if (!sessionId || !ownerIds.has(sessionId)) {
|
|
6456
|
+
throw new Error(`Refusing contradictory session_id in ${path}.`);
|
|
6457
|
+
}
|
|
6458
|
+
}
|
|
6459
|
+
for (const ownerField of ["owner_omx_session_id", "owner_codex_session_id"]) {
|
|
6460
|
+
if (!Object.prototype.hasOwnProperty.call(value, ownerField))
|
|
6461
|
+
continue;
|
|
6462
|
+
const rawOwner = value[ownerField];
|
|
6463
|
+
const blankOwner = typeof rawOwner === "string" && rawOwner.trim() === "";
|
|
6464
|
+
const owner = normalizeSessionId(rawOwner);
|
|
6465
|
+
if (!blankOwner && (!owner || !ownerIds.has(owner))) {
|
|
6466
|
+
throw new Error(`Refusing contradictory ${ownerField} in ${path}.`);
|
|
6467
|
+
}
|
|
6468
|
+
}
|
|
6469
|
+
}
|
|
6470
|
+
async function resolveExactSessionCancellationAuthority(cwd, baseStateDir, sessionId) {
|
|
6471
|
+
if (!sessionId)
|
|
6472
|
+
return undefined;
|
|
6473
|
+
let pointer;
|
|
6474
|
+
let authorityCwd = cwd;
|
|
6475
|
+
try {
|
|
6476
|
+
const rawPointer = JSON.parse(await readFile(join(baseStateDir, "session.json"), "utf-8"));
|
|
6477
|
+
if (typeof rawPointer.cwd === "string" && rawPointer.cwd.trim())
|
|
6478
|
+
authorityCwd = rawPointer.cwd.trim();
|
|
6479
|
+
pointer = await readSessionPointer(resolveSessionPointerContext(authorityCwd));
|
|
6480
|
+
}
|
|
6481
|
+
catch {
|
|
6482
|
+
return undefined;
|
|
6483
|
+
}
|
|
6484
|
+
const nativeSessionId = normalizeSessionId(pointer.state?.native_session_id);
|
|
6485
|
+
if (pointer.status !== "usable" || pointer.state?.session_id !== sessionId || !nativeSessionId)
|
|
6486
|
+
return undefined;
|
|
6487
|
+
try {
|
|
6488
|
+
const ownerEvidence = await readNativeSessionOwnerEvidence(authorityCwd, nativeSessionId);
|
|
6489
|
+
if (ownerEvidence.status !== "usable")
|
|
6490
|
+
return undefined;
|
|
6491
|
+
}
|
|
6492
|
+
catch {
|
|
6493
|
+
return undefined;
|
|
6494
|
+
}
|
|
6495
|
+
return { sessionId, nativeSessionId, authorityCwd };
|
|
6496
|
+
}
|
|
6497
|
+
function assertExactSkillOwner(value, authority, path, allowStaleTopCodexOwner = false) {
|
|
6498
|
+
if (Object.prototype.hasOwnProperty.call(value, "session_id")
|
|
6499
|
+
&& normalizeSessionId(value.session_id) !== authority.sessionId) {
|
|
6500
|
+
throw new Error(`Refusing contradictory session_id in ${path}.`);
|
|
6501
|
+
}
|
|
6502
|
+
if (Object.prototype.hasOwnProperty.call(value, "owner_omx_session_id")) {
|
|
6503
|
+
const rawOwner = value.owner_omx_session_id;
|
|
6504
|
+
const blankOwner = typeof rawOwner === "string" && rawOwner.trim() === "";
|
|
6505
|
+
const owner = normalizeSessionId(rawOwner);
|
|
6506
|
+
if (!blankOwner && (!owner || owner !== authority.sessionId)) {
|
|
6507
|
+
throw new Error(`Refusing contradictory owner_omx_session_id in ${path}.`);
|
|
6508
|
+
}
|
|
6509
|
+
}
|
|
6510
|
+
if (Object.prototype.hasOwnProperty.call(value, "owner_codex_session_id")) {
|
|
6511
|
+
const rawOwner = value.owner_codex_session_id;
|
|
6512
|
+
const blankOwner = typeof rawOwner === "string" && rawOwner.trim() === "";
|
|
6513
|
+
const owner = normalizeSessionId(rawOwner);
|
|
6514
|
+
if (!blankOwner && (!owner || (!allowStaleTopCodexOwner && owner !== authority.nativeSessionId))) {
|
|
6515
|
+
throw new Error(`Refusing contradictory owner_codex_session_id in ${path}.`);
|
|
6516
|
+
}
|
|
6517
|
+
}
|
|
6518
|
+
}
|
|
6519
|
+
function assertCompatibleNestedSkillOwners(value, authority, path) {
|
|
6520
|
+
if (!Object.prototype.hasOwnProperty.call(value, "active_skills"))
|
|
6521
|
+
return;
|
|
6522
|
+
if (!Array.isArray(value.active_skills)) {
|
|
6523
|
+
throw new Error(`Refusing malformed active_skills in ${path}.`);
|
|
6524
|
+
}
|
|
6525
|
+
for (const [index, skill] of value.active_skills.entries()) {
|
|
6526
|
+
const entryPath = `${path} active_skills[${index}]`;
|
|
6527
|
+
if (!skill || typeof skill !== "object" || Array.isArray(skill)) {
|
|
6528
|
+
throw new Error(`Refusing malformed active_skills entry ${index} in ${path}.`);
|
|
6529
|
+
}
|
|
6530
|
+
const entry = skill;
|
|
6531
|
+
if (typeof entry.skill !== "string" || entry.skill.trim() === "") {
|
|
6532
|
+
throw new Error(`Refusing malformed skill in ${entryPath}.`);
|
|
6533
|
+
}
|
|
6534
|
+
for (const field of ["active"]) {
|
|
6535
|
+
if (Object.prototype.hasOwnProperty.call(entry, field) && typeof entry[field] !== "boolean") {
|
|
6536
|
+
throw new Error(`Refusing malformed ${field} in ${entryPath}.`);
|
|
6537
|
+
}
|
|
6538
|
+
}
|
|
6539
|
+
if (Object.prototype.hasOwnProperty.call(entry, "phase") && typeof entry.phase !== "string") {
|
|
6540
|
+
throw new Error(`Refusing malformed phase in ${entryPath}.`);
|
|
6541
|
+
}
|
|
6542
|
+
for (const [field, fieldValue] of Object.entries(entry)) {
|
|
6543
|
+
if (field.endsWith("_at") && typeof fieldValue !== "string") {
|
|
6544
|
+
throw new Error(`Refusing malformed ${field} in ${entryPath}.`);
|
|
6545
|
+
}
|
|
6546
|
+
}
|
|
6547
|
+
assertExactSkillOwner(entry, authority, entryPath);
|
|
6548
|
+
}
|
|
6549
|
+
}
|
|
6550
|
+
async function cancelModes(args = [], options = {}) {
|
|
6551
|
+
const cwd = options.cwd ?? process.cwd();
|
|
5125
6552
|
const nowIso = new Date().toISOString();
|
|
5126
|
-
|
|
6553
|
+
if (args.length > 1 || args.some((arg) => arg !== "--force")) {
|
|
6554
|
+
const unsupported = args.find((arg) => arg !== "--force") ?? args[1] ?? "";
|
|
6555
|
+
throw new Error(unsupported === "--all"
|
|
6556
|
+
? "omx cancel --all is not supported; broad workspace cancellation requires a separate command."
|
|
6557
|
+
: `Unknown cancel argument: ${unsupported}`);
|
|
6558
|
+
}
|
|
6559
|
+
const force = args.length === 1;
|
|
5127
6560
|
try {
|
|
5128
6561
|
const writableScope = await resolveWritableStateScope(cwd);
|
|
5129
|
-
const
|
|
6562
|
+
const baseStateDir = getBaseStateDir(cwd);
|
|
6563
|
+
const exactAuthority = writableScope.source === "session"
|
|
6564
|
+
? await resolveExactSessionCancellationAuthority(cwd, baseStateDir, writableScope.sessionId)
|
|
6565
|
+
: undefined;
|
|
6566
|
+
const expectedOwnerIds = collectCancellationOwnerIds(baseStateDir, writableScope.sessionId);
|
|
6567
|
+
const loadStates = async (refs, authorityRoot, ownerIds = expectedOwnerIds) => {
|
|
5130
6568
|
const loaded = new Map();
|
|
6569
|
+
if (refs.length === 0)
|
|
6570
|
+
return loaded;
|
|
6571
|
+
const canonicalAuthorityRoot = assertCancellationAuthorityPath(authorityRoot === writableScope.stateDir ? baseStateDir : authorityRoot, authorityRoot);
|
|
5131
6572
|
for (const ref of refs) {
|
|
5132
|
-
const
|
|
6573
|
+
const fileStat = lstatSync(ref.path);
|
|
6574
|
+
if (!fileStat.isFile() || fileStat.isSymbolicLink()) {
|
|
6575
|
+
throw new Error(`Refusing cancellation through non-regular state target ${ref.path}.`);
|
|
6576
|
+
}
|
|
6577
|
+
const canonicalPath = realpathSync(ref.path);
|
|
6578
|
+
const canonicalParent = realpathSync(dirname(ref.path));
|
|
6579
|
+
if (!isCanonicalPathWithin(canonicalAuthorityRoot, canonicalParent, true)
|
|
6580
|
+
|| !isCanonicalPathWithin(canonicalAuthorityRoot, canonicalPath)) {
|
|
6581
|
+
throw new Error(`Refusing cancellation outside authorized state root: ${ref.path}.`);
|
|
6582
|
+
}
|
|
6583
|
+
const content = await readFile(canonicalPath, "utf-8");
|
|
5133
6584
|
let parsedState;
|
|
5134
6585
|
try {
|
|
5135
|
-
|
|
6586
|
+
const parsed = JSON.parse(content);
|
|
6587
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
6588
|
+
throw new Error("state must be a JSON object");
|
|
6589
|
+
}
|
|
6590
|
+
parsedState = parsed;
|
|
5136
6591
|
}
|
|
5137
6592
|
catch (err) {
|
|
5138
6593
|
logCliOperationFailure(err);
|
|
5139
|
-
|
|
6594
|
+
throw new Error(`Refusing partial cancellation because ${ref.path} is malformed.`, { cause: err });
|
|
6595
|
+
}
|
|
6596
|
+
if (typeof parsedState.mode === "string" && parsedState.mode !== ref.mode) {
|
|
6597
|
+
throw new Error(`Refusing contradictory mode state in ${ref.path}.`);
|
|
6598
|
+
}
|
|
6599
|
+
if (ref.mode === SKILL_ACTIVE_STATE_MODE && exactAuthority) {
|
|
6600
|
+
const hasTopCodexOwner = Object.prototype.hasOwnProperty.call(parsedState, "owner_codex_session_id");
|
|
6601
|
+
const displacedOwnerId = hasTopCodexOwner
|
|
6602
|
+
? normalizeSessionId(parsedState.owner_codex_session_id)
|
|
6603
|
+
: undefined;
|
|
6604
|
+
const blankTopCodexOwner = typeof parsedState.owner_codex_session_id === "string"
|
|
6605
|
+
&& parsedState.owner_codex_session_id.trim() === "";
|
|
6606
|
+
if (hasTopCodexOwner && !blankTopCodexOwner && !displacedOwnerId) {
|
|
6607
|
+
throw new Error(`Refusing contradictory owner_codex_session_id in ${ref.path}.`);
|
|
6608
|
+
}
|
|
6609
|
+
assertExactSkillOwner(parsedState, exactAuthority, ref.path, hasTopCodexOwner);
|
|
6610
|
+
assertCompatibleNestedSkillOwners(parsedState, exactAuthority, ref.path);
|
|
6611
|
+
if (displacedOwnerId && displacedOwnerId !== exactAuthority.nativeSessionId) {
|
|
6612
|
+
let displacedEvidence;
|
|
6613
|
+
try {
|
|
6614
|
+
displacedEvidence = await readNativeSessionOwnerEvidence(exactAuthority.authorityCwd, displacedOwnerId);
|
|
6615
|
+
}
|
|
6616
|
+
catch (error) {
|
|
6617
|
+
throw new Error(`Refusing ambiguous displaced owner evidence for ${ref.path}.`, { cause: error });
|
|
6618
|
+
}
|
|
6619
|
+
if (displacedEvidence.status !== "absent" && displacedEvidence.status !== "stale-dead") {
|
|
6620
|
+
throw new Error(`Refusing non-stale displaced owner evidence for ${ref.path}.`);
|
|
6621
|
+
}
|
|
6622
|
+
parsedState.owner_codex_session_id = exactAuthority.nativeSessionId;
|
|
6623
|
+
}
|
|
6624
|
+
}
|
|
6625
|
+
else {
|
|
6626
|
+
assertCancellationOwnerMetadata(parsedState, ownerIds, ref.path);
|
|
6627
|
+
if (Object.prototype.hasOwnProperty.call(parsedState, "active_skills")) {
|
|
6628
|
+
if (!Array.isArray(parsedState.active_skills)) {
|
|
6629
|
+
throw new Error(`Refusing malformed active_skills in ${ref.path}.`);
|
|
6630
|
+
}
|
|
6631
|
+
for (const [index, skill] of parsedState.active_skills.entries()) {
|
|
6632
|
+
if (!skill || typeof skill !== "object" || Array.isArray(skill)) {
|
|
6633
|
+
throw new Error(`Refusing malformed active_skills entry ${index} in ${ref.path}.`);
|
|
6634
|
+
}
|
|
6635
|
+
assertCancellationOwnerMetadata(skill, ownerIds, `${ref.path} active_skills[${index}]`);
|
|
6636
|
+
}
|
|
6637
|
+
}
|
|
5140
6638
|
}
|
|
5141
6639
|
loaded.set(ref.mode, {
|
|
5142
|
-
path:
|
|
6640
|
+
path: canonicalPath,
|
|
5143
6641
|
scope: ref.scope,
|
|
5144
6642
|
state: parsedState,
|
|
6643
|
+
originalContent: content,
|
|
6644
|
+
dev: fileStat.dev,
|
|
6645
|
+
ino: fileStat.ino,
|
|
5145
6646
|
});
|
|
5146
6647
|
}
|
|
5147
6648
|
return loaded;
|
|
5148
6649
|
};
|
|
5149
|
-
|
|
6650
|
+
const preferredRefs = writableScope.source === "root"
|
|
6651
|
+
? (await readdir(writableScope.stateDir).catch(() => []))
|
|
6652
|
+
.filter((file) => isModeStateFilename(file))
|
|
6653
|
+
.map((file) => ({
|
|
6654
|
+
mode: file.slice(0, -"-state.json".length),
|
|
6655
|
+
path: join(writableScope.stateDir, file),
|
|
6656
|
+
scope: "root",
|
|
6657
|
+
}))
|
|
6658
|
+
: await listModeStateFilesWithScopePreference(cwd, writableScope.sessionId);
|
|
6659
|
+
const hasPreferredSessionStateFiles = preferredRefs.some((ref) => ref.scope === "session");
|
|
6660
|
+
const targetRefs = writableScope.source === "session"
|
|
6661
|
+
? preferredRefs.filter((ref) => ref.scope === "session")
|
|
6662
|
+
: preferredRefs;
|
|
6663
|
+
let runSelection = null;
|
|
6664
|
+
let states = await loadStates(targetRefs, writableScope.stateDir, expectedOwnerIds);
|
|
5150
6665
|
const hasActiveWorkflowMode = (entries) => [...entries.entries()].some(([mode, entry]) => mode !== SKILL_ACTIVE_STATE_MODE && entry.state.active === true);
|
|
5151
|
-
if (
|
|
5152
|
-
|
|
5153
|
-
|
|
5154
|
-
|
|
6666
|
+
if (writableScope.source === "root"
|
|
6667
|
+
&& !hasActiveWorkflowMode(states)
|
|
6668
|
+
&& !hasPreferredSessionStateFiles) {
|
|
6669
|
+
runSelection = await selectAuthorizedHookVisibleRunDirState(cwd);
|
|
6670
|
+
if (runSelection) {
|
|
6671
|
+
const runOwnerIds = new Set([runSelection.sessionId]);
|
|
6672
|
+
const runDirStates = await loadStates(runSelection.refs, runSelection.sessionDir, runOwnerIds);
|
|
6673
|
+
if (hasActiveWorkflowMode(runDirStates))
|
|
6674
|
+
states = runDirStates;
|
|
6675
|
+
else
|
|
6676
|
+
runSelection = null;
|
|
6677
|
+
}
|
|
5155
6678
|
}
|
|
5156
|
-
const currentSessionId = writableScope.sessionId ?? "";
|
|
6679
|
+
const currentSessionId = writableScope.sessionId ?? runSelection?.sessionId ?? "";
|
|
5157
6680
|
const changed = new Set();
|
|
5158
6681
|
const reported = new Set();
|
|
5159
6682
|
const cancelMode = (mode, phase = "cancelled", reportIfWasActive = true) => {
|
|
@@ -5184,13 +6707,74 @@ async function cancelModes(args = []) {
|
|
|
5184
6707
|
if (reportIfWasActive && wasActive && mode !== SKILL_ACTIVE_STATE_MODE)
|
|
5185
6708
|
reported.add(mode);
|
|
5186
6709
|
};
|
|
5187
|
-
const
|
|
6710
|
+
const linkedRalphMode = (state) => {
|
|
6711
|
+
if (state.linked_ultrawork === true || state.linked_mode === "ultrawork")
|
|
6712
|
+
return "ultrawork";
|
|
6713
|
+
if (state.linked_ecomode === true || state.linked_mode === "ecomode")
|
|
6714
|
+
return "ecomode";
|
|
6715
|
+
return undefined;
|
|
6716
|
+
};
|
|
6717
|
+
const cancelRalphSkillEntries = (linkedMode) => {
|
|
6718
|
+
const entry = states.get(SKILL_ACTIVE_STATE_MODE);
|
|
6719
|
+
if (!entry)
|
|
6720
|
+
return;
|
|
6721
|
+
const matchingSkills = new Set(["ralph", ...(linkedMode ? [linkedMode] : [])]);
|
|
6722
|
+
const activeSkills = Array.isArray(entry.state.active_skills)
|
|
6723
|
+
? entry.state.active_skills
|
|
6724
|
+
: [];
|
|
6725
|
+
let changedSkillEntries = false;
|
|
6726
|
+
const nextSkills = activeSkills.map((skill) => {
|
|
6727
|
+
if (!skill || typeof skill !== "object" || Array.isArray(skill))
|
|
6728
|
+
return skill;
|
|
6729
|
+
const skillEntry = skill;
|
|
6730
|
+
if (skillEntry.active === false || typeof skillEntry.skill !== "string" || !matchingSkills.has(skillEntry.skill)) {
|
|
6731
|
+
return skill;
|
|
6732
|
+
}
|
|
6733
|
+
changedSkillEntries = true;
|
|
6734
|
+
return { ...skillEntry, active: false, phase: "cancelled" };
|
|
6735
|
+
});
|
|
6736
|
+
const remainingActiveSkills = nextSkills.filter((skill) => (!!skill && typeof skill === "object" && !Array.isArray(skill)
|
|
6737
|
+
&& skill.active !== false));
|
|
6738
|
+
if (!changedSkillEntries) {
|
|
6739
|
+
if (entry.state.active !== true)
|
|
6740
|
+
return;
|
|
6741
|
+
if (typeof entry.state.skill !== "string" || !matchingSkills.has(entry.state.skill))
|
|
6742
|
+
return;
|
|
6743
|
+
}
|
|
6744
|
+
entry.state.active_skills = nextSkills;
|
|
6745
|
+
entry.state.updated_at = nowIso;
|
|
6746
|
+
entry.state.last_turn_at = nowIso;
|
|
6747
|
+
if (remainingActiveSkills.length > 0) {
|
|
6748
|
+
const primary = remainingActiveSkills[0];
|
|
6749
|
+
entry.state.active = true;
|
|
6750
|
+
entry.state.skill = primary.skill;
|
|
6751
|
+
if (typeof primary.phase === "string") {
|
|
6752
|
+
entry.state.phase = primary.phase;
|
|
6753
|
+
entry.state.current_phase = primary.phase;
|
|
6754
|
+
}
|
|
6755
|
+
else {
|
|
6756
|
+
delete entry.state.phase;
|
|
6757
|
+
delete entry.state.current_phase;
|
|
6758
|
+
}
|
|
6759
|
+
delete entry.state.completed_at;
|
|
6760
|
+
}
|
|
6761
|
+
else {
|
|
6762
|
+
entry.state.active = false;
|
|
6763
|
+
entry.state.skill = "";
|
|
6764
|
+
entry.state.phase = "cancelled";
|
|
6765
|
+
entry.state.current_phase = "cancelled";
|
|
6766
|
+
entry.state.completed_at = nowIso;
|
|
6767
|
+
}
|
|
6768
|
+
changed.add(SKILL_ACTIVE_STATE_MODE);
|
|
6769
|
+
};
|
|
5188
6770
|
const ralph = states.get("ralph");
|
|
5189
6771
|
const hadActiveRalph = !!(ralph && ralph.state.active === true);
|
|
5190
6772
|
if (ralph && ralph.state.active === true) {
|
|
6773
|
+
const linkedMode = linkedRalphMode(ralph.state);
|
|
6774
|
+
if (linkedMode)
|
|
6775
|
+
cancelMode(linkedMode, "cancelled", true);
|
|
5191
6776
|
cancelMode("ralph", "cancelled", true);
|
|
5192
|
-
|
|
5193
|
-
cancelMode("ultrawork", "cancelled", true);
|
|
6777
|
+
cancelRalphSkillEntries(linkedMode);
|
|
5194
6778
|
}
|
|
5195
6779
|
if (!hadActiveRalph) {
|
|
5196
6780
|
for (const [mode, entry] of states.entries()) {
|
|
@@ -5198,11 +6782,11 @@ async function cancelModes(args = []) {
|
|
|
5198
6782
|
cancelMode(mode, "cancelled", true);
|
|
5199
6783
|
}
|
|
5200
6784
|
}
|
|
5201
|
-
|
|
5202
|
-
if (!
|
|
5203
|
-
|
|
5204
|
-
|
|
5205
|
-
}
|
|
6785
|
+
const assertRunAuthority = () => {
|
|
6786
|
+
if (runSelection && !hasMatchingMadmaxDetachedRuntimeBinding(runSelection.record)) {
|
|
6787
|
+
throw new Error("Refusing cancellation because detached run authority changed.");
|
|
6788
|
+
}
|
|
6789
|
+
};
|
|
5206
6790
|
if (force && currentSessionId) {
|
|
5207
6791
|
const stopStateEntries = [...states.entries()].filter(([mode]) => mode === "native-stop");
|
|
5208
6792
|
for (const [, entry] of stopStateEntries) {
|
|
@@ -5213,10 +6797,85 @@ async function cancelModes(args = []) {
|
|
|
5213
6797
|
continue;
|
|
5214
6798
|
delete sessions[currentSessionId];
|
|
5215
6799
|
entry.state.sessions = sessions;
|
|
5216
|
-
await writeFile(entry.path, JSON.stringify(entry.state, null, 2));
|
|
5217
6800
|
changed.add("native-stop");
|
|
5218
6801
|
}
|
|
5219
6802
|
}
|
|
6803
|
+
const orderedChanges = [...changed]
|
|
6804
|
+
.sort((left, right) => {
|
|
6805
|
+
if (left === "ralph")
|
|
6806
|
+
return 1;
|
|
6807
|
+
if (right === "ralph")
|
|
6808
|
+
return -1;
|
|
6809
|
+
return left.localeCompare(right);
|
|
6810
|
+
})
|
|
6811
|
+
.map((mode) => {
|
|
6812
|
+
const entry = states.get(mode);
|
|
6813
|
+
if (!entry)
|
|
6814
|
+
throw new Error(`Missing frozen cancellation entry for ${mode}.`);
|
|
6815
|
+
return { mode, entry, nextContent: JSON.stringify(entry.state, null, 2) };
|
|
6816
|
+
});
|
|
6817
|
+
const opened = [];
|
|
6818
|
+
try {
|
|
6819
|
+
for (const change of orderedChanges) {
|
|
6820
|
+
assertRunAuthority();
|
|
6821
|
+
const handle = await open(change.entry.path, fsConstants.O_RDWR | fsConstants.O_NOFOLLOW);
|
|
6822
|
+
const currentStat = await handle.stat();
|
|
6823
|
+
const currentContent = await handle.readFile({ encoding: "utf-8" });
|
|
6824
|
+
if (!currentStat.isFile() || currentStat.dev !== change.entry.dev || currentStat.ino !== change.entry.ino) {
|
|
6825
|
+
await handle.close();
|
|
6826
|
+
throw new Error(`Refusing cancellation because state identity changed: ${change.entry.path}.`);
|
|
6827
|
+
}
|
|
6828
|
+
if (currentContent !== change.entry.originalContent) {
|
|
6829
|
+
await handle.close();
|
|
6830
|
+
throw new Error(`Refusing cancellation because state content changed: ${change.entry.path}.`);
|
|
6831
|
+
}
|
|
6832
|
+
opened.push({ ...change, handle });
|
|
6833
|
+
}
|
|
6834
|
+
const committed = [];
|
|
6835
|
+
const cancellationTestWriteFailureMode = options.testFaults?.writeFailureMode;
|
|
6836
|
+
const cancellationTestRollbackFailureMode = options.testFaults?.rollbackFailureMode;
|
|
6837
|
+
try {
|
|
6838
|
+
for (const openedEntry of opened) {
|
|
6839
|
+
assertRunAuthority();
|
|
6840
|
+
committed.push(openedEntry);
|
|
6841
|
+
if (cancellationTestWriteFailureMode === openedEntry.mode) {
|
|
6842
|
+
throw new Error(`Injected cancellation write failure for ${openedEntry.mode}.`);
|
|
6843
|
+
}
|
|
6844
|
+
await openedEntry.handle.truncate(0);
|
|
6845
|
+
await openedEntry.handle.write(openedEntry.nextContent, 0, "utf-8");
|
|
6846
|
+
await openedEntry.handle.sync();
|
|
6847
|
+
}
|
|
6848
|
+
}
|
|
6849
|
+
catch (writeError) {
|
|
6850
|
+
let rollbackFailureCount = 0;
|
|
6851
|
+
const rollbackFailureSample = [];
|
|
6852
|
+
for (const committedEntry of committed.reverse()) {
|
|
6853
|
+
try {
|
|
6854
|
+
if (cancellationTestRollbackFailureMode === committedEntry.mode) {
|
|
6855
|
+
throw new Error(`Injected cancellation rollback failure for ${committedEntry.mode}.`);
|
|
6856
|
+
}
|
|
6857
|
+
await committedEntry.handle.truncate(0);
|
|
6858
|
+
await committedEntry.handle.write(committedEntry.entry.originalContent, 0, "utf-8");
|
|
6859
|
+
await committedEntry.handle.sync();
|
|
6860
|
+
}
|
|
6861
|
+
catch {
|
|
6862
|
+
rollbackFailureCount += 1;
|
|
6863
|
+
if (rollbackFailureSample.length < 3) {
|
|
6864
|
+
rollbackFailureSample.push(committedEntry.mode.replace(/[^A-Za-z0-9_-]/g, "_"));
|
|
6865
|
+
}
|
|
6866
|
+
}
|
|
6867
|
+
}
|
|
6868
|
+
if (rollbackFailureCount > 0) {
|
|
6869
|
+
const primaryMessage = writeError instanceof Error ? writeError.message : String(writeError);
|
|
6870
|
+
const sample = rollbackFailureSample.length > 0 ? `:${rollbackFailureSample.join(",")}` : "";
|
|
6871
|
+
throw new Error(`${primaryMessage} (cancellation_rollback_failed:${rollbackFailureCount}${sample}).`, { cause: writeError });
|
|
6872
|
+
}
|
|
6873
|
+
throw writeError;
|
|
6874
|
+
}
|
|
6875
|
+
}
|
|
6876
|
+
finally {
|
|
6877
|
+
await Promise.all(opened.map(({ handle }) => handle.close().catch(() => undefined)));
|
|
6878
|
+
}
|
|
5220
6879
|
for (const mode of reported) {
|
|
5221
6880
|
console.log(`Cancelled: ${mode}`);
|
|
5222
6881
|
}
|
|
@@ -5226,7 +6885,11 @@ async function cancelModes(args = []) {
|
|
|
5226
6885
|
}
|
|
5227
6886
|
catch (err) {
|
|
5228
6887
|
logCliOperationFailure(err);
|
|
5229
|
-
|
|
6888
|
+
process.exitCode = 1;
|
|
5230
6889
|
}
|
|
5231
6890
|
}
|
|
6891
|
+
/** Test-only entrypoint for deterministic transaction fault injection without runtime environment controls. */
|
|
6892
|
+
export async function cancelModesForTest(cwd, args, testFaults) {
|
|
6893
|
+
await cancelModes(args, { cwd, testFaults });
|
|
6894
|
+
}
|
|
5232
6895
|
//# sourceMappingURL=index.js.map
|