oh-my-codex 0.20.3 → 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 +35 -18
- package/Cargo.toml +1 -1
- package/README.md +2 -2
- package/crates/omx-runtime/Cargo.toml +3 -0
- package/crates/omx-runtime/src/main.rs +390 -0
- package/crates/omx-runtime/tests/execution.rs +264 -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/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 +16 -2
- package/dist/auth/hotswap.d.ts.map +1 -1
- package/dist/auth/hotswap.js +22 -7
- 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 +3 -1
- 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 +70 -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 +35 -3
- package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
- package/dist/cli/__tests__/exec.test.js +75 -0
- package/dist/cli/__tests__/exec.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +621 -460
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/launch-fallback.test.js +1020 -143
- package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
- package/dist/cli/__tests__/mcp-parity.test.js +4 -3
- 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 +53 -5
- package/dist/cli/__tests__/native-hook-claim-journal.test.js.map +1 -1
- package/dist/cli/__tests__/ralph.test.js +2 -2
- package/dist/cli/__tests__/ralph.test.js.map +1 -1
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js +57 -57
- 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 +84 -12
- package/dist/cli/__tests__/ralplan.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__/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 +183 -4
- 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 +45 -1
- 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 +50 -5
- 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 +32 -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 +17 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +277 -3
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.d.ts +231 -12
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2298 -735
- package/dist/cli/index.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 +8 -4
- package/dist/cli/native-hook-claim-journal.d.ts.map +1 -1
- package/dist/cli/native-hook-claim-journal.js +30 -8
- 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 +4 -2
- package/dist/cli/ralplan.d.ts.map +1 -1
- package/dist/cli/ralplan.js +76 -34
- package/dist/cli/ralplan.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 +3 -0
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +55 -32
- 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.map +1 -1
- package/dist/cli/uninstall.js +27 -28
- 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/config/__tests__/generator-idempotent.test.js +173 -29
- package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
- package/dist/config/__tests__/generator-notify.test.js +3 -1
- package/dist/config/__tests__/generator-notify.test.js.map +1 -1
- package/dist/config/generator.d.ts +25 -3
- package/dist/config/generator.d.ts.map +1 -1
- package/dist/config/generator.js +113 -35
- package/dist/config/generator.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 +3 -1
- 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 +172 -33
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js +173 -11
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +1 -0
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +65 -17
- 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 +2 -2
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +4 -1
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.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-worker-idle.test.js +1 -0
- 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 +2976 -557
- 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 +180 -14
- package/dist/hooks/session.d.ts.map +1 -1
- package/dist/hooks/session.js +1402 -281
- 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 +71 -34
- package/dist/leader/__tests__/contract.test.js.map +1 -1
- package/dist/leader/contract.d.ts +1 -6
- package/dist/leader/contract.d.ts.map +1 -1
- package/dist/leader/contract.js +25 -24
- package/dist/leader/contract.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/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/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/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/pipeline/__tests__/orchestrator.test.js +162 -40
- package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
- package/dist/pipeline/__tests__/stages.test.js +66 -149
- 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 +70 -1437
- package/dist/ralplan/__tests__/consensus-gate.test.js.map +1 -1
- package/dist/ralplan/__tests__/documented-leader-preflight.test.js +328 -39
- package/dist/ralplan/__tests__/documented-leader-preflight.test.js.map +1 -1
- package/dist/ralplan/__tests__/runtime.test.js +82 -102
- 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 +40 -304
- package/dist/ralplan/consensus-gate.js.map +1 -1
- package/dist/ralplan/documented-leader-preflight.d.ts +15 -8
- package/dist/ralplan/documented-leader-preflight.d.ts.map +1 -1
- package/dist/ralplan/documented-leader-preflight.js +356 -54
- package/dist/ralplan/documented-leader-preflight.js.map +1 -1
- package/dist/ralplan/runtime.d.ts +1 -1
- package/dist/ralplan/runtime.d.ts.map +1 -1
- package/dist/ralplan/runtime.js +68 -173
- package/dist/ralplan/runtime.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 +14071 -8859
- 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__/role-intent-bootstrap-e2e-3181.test.js +25 -8
- 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 +60 -25
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js.map +1 -1
- package/dist/scripts/__tests__/smoke-packed-install.test.js +155 -16
- 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 +50 -1
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +3559 -451
- 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 +28 -39
- package/dist/scripts/notify-fallback-watcher.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-leader-nudge.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-leader-nudge.js +32 -3
- package/dist/scripts/notify-hook/team-leader-nudge.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 +23 -3
- package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
- package/dist/scripts/notify-hook/team-worker.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-worker.js +45 -5
- 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 +4 -1
- package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
- package/dist/scripts/notify-hook.js +78 -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 +32 -1
- package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
- package/dist/scripts/smoke-packed-install.js +413 -60
- 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 +12 -5
- package/dist/state/__tests__/mode-state-context.test.js.map +1 -1
- package/dist/state/__tests__/operations.test.js +460 -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 +3 -0
- package/dist/state/mode-state-context.d.ts.map +1 -1
- package/dist/state/mode-state-context.js +27 -13
- 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 +27 -0
- 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 +64 -60
- package/dist/subagents/tracker.d.ts.map +1 -1
- package/dist/subagents/tracker.js +809 -358
- package/dist/subagents/tracker.js.map +1 -1
- package/dist/team/__tests__/delivery-e2e-smoke.test.js +6 -7
- package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
- package/dist/team/__tests__/mcp-comm.test.js +104 -3
- package/dist/team/__tests__/mcp-comm.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.test.js +725 -59
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/scaling.test.js +222 -237
- 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__/tmux-session.test.js +887 -92
- 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 +45 -72
- package/dist/team/__tests__/worker-runtime-identity.test.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 +3 -1
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +314 -142
- package/dist/team/runtime.js.map +1 -1
- package/dist/team/scaling.d.ts.map +1 -1
- package/dist/team/scaling.js +161 -91
- package/dist/team/scaling.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/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 +18 -0
- package/dist/team/state.d.ts.map +1 -1
- package/dist/team/state.js +36 -0
- package/dist/team/state.js.map +1 -1
- package/dist/team/tmux-session.d.ts +40 -4
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +513 -166
- package/dist/team/tmux-session.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.js +58 -1
- package/dist/utils/__tests__/file-durability.test.js.map +1 -1
- 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 +10 -0
- package/dist/utils/file-durability.d.ts.map +1 -1
- package/dist/utils/file-durability.js +34 -4
- package/dist/utils/file-durability.js.map +1 -1
- 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__/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/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 +81 -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 +5 -5
- package/plugins/oh-my-codex/skills/ralplan/SKILL.md +15 -17
- 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 +5 -5
- package/skills/ralplan/SKILL.md +15 -17
- package/skills/ultragoal/SKILL.md +3 -2
- package/src/scripts/__tests__/codex-native-hook.test.ts +6091 -540
- 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__/role-intent-bootstrap-e2e-3181.test.ts +27 -8
- package/src/scripts/__tests__/role-intent-durable-recovery-3181.test.ts +61 -27
- package/src/scripts/__tests__/smoke-packed-install.test.ts +209 -18
- package/src/scripts/__tests__/verify-native-release-assets.test.ts +240 -0
- package/src/scripts/codex-native-hook.ts +3638 -475
- 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 +38 -38
- package/src/scripts/notify-hook/state-io.ts +11 -0
- package/src/scripts/notify-hook/team-leader-nudge.ts +32 -4
- package/src/scripts/notify-hook/team-worker-stop.ts +23 -4
- package/src/scripts/notify-hook/team-worker.ts +43 -5
- package/src/scripts/notify-hook/tmux-injection.ts +5 -1
- package/src/scripts/notify-hook.ts +83 -82
- package/src/scripts/postinstall-advisory.ts +67 -0
- package/src/scripts/postinstall.ts +1 -1
- package/src/scripts/smoke-packed-install.ts +446 -59
- 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/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";
|
|
@@ -41,7 +41,7 @@ import { authCommand } from "./auth.js";
|
|
|
41
41
|
import { missionCommand } from "./mission.js";
|
|
42
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, getBaseStateDirWithSource, 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";
|
|
@@ -50,14 +50,14 @@ import { discoverOmxPluginCacheDirs, hasLocalOmxPluginEnablement, materializePac
|
|
|
50
50
|
import { escapeTomlString, readTopLevelTomlString, upsertTopLevelTomlString } from "../utils/toml.js";
|
|
51
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";
|
|
@@ -134,7 +135,8 @@ Usage:
|
|
|
134
135
|
Alias for agents-init (lightweight AGENTS bootstrap only)
|
|
135
136
|
omx team Spawn parallel worker panes in tmux and bootstrap inbox/task state
|
|
136
137
|
omx ralph Launch Codex with ralph persistence mode active
|
|
137
|
-
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
|
|
138
140
|
omx ultragoal Create, resume, and checkpoint durable multi-goal plans over Codex goal mode
|
|
139
141
|
omx performance-goal
|
|
140
142
|
Create, hand off, and gate evaluator-backed performance goals
|
|
@@ -262,7 +264,6 @@ const ALLOWED_SHELLS = new Set([
|
|
|
262
264
|
"/opt/local/bin/zsh",
|
|
263
265
|
"/opt/homebrew/bin/zsh",
|
|
264
266
|
]);
|
|
265
|
-
const WINDOWS_DETACHED_BOOTSTRAP_DELAY_MS = 2500;
|
|
266
267
|
const CODEX_VERSION_FLAGS = new Set(["--version", "-V"]);
|
|
267
268
|
const TMUX_EXTENDED_KEYS_MODE = "always";
|
|
268
269
|
const TMUX_EXTENDED_KEYS_FALLBACK_MODE = "off";
|
|
@@ -270,6 +271,7 @@ const TMUX_EXTENDED_KEYS_LEASE_DIR = "tmux-extended-keys";
|
|
|
270
271
|
const TMUX_EXTENDED_KEYS_LOCK_RETRY_MS = 20;
|
|
271
272
|
const TMUX_EXTENDED_KEYS_LOCK_MAX_ATTEMPTS = 100;
|
|
272
273
|
const TMUX_EXTENDED_KEYS_LOCK_STALE_MS = 30_000;
|
|
274
|
+
export const DETACHED_LEADER_READY_TIMEOUT_MS = 120_000;
|
|
273
275
|
const RESUME_HELP = `Usage: omx resume [--project] [--codex-home <path>] [codex resume options]
|
|
274
276
|
|
|
275
277
|
Read-only help does not prepare or launch a Codex session.`;
|
|
@@ -520,6 +522,9 @@ export function resolveCliInvocation(args) {
|
|
|
520
522
|
if (firstArg === "resume") {
|
|
521
523
|
return { command: "resume", launchArgs: args.slice(1) };
|
|
522
524
|
}
|
|
525
|
+
if (firstArg === "__detached-session-leader") {
|
|
526
|
+
return { command: firstArg, launchArgs: args.slice(1) };
|
|
527
|
+
}
|
|
523
528
|
return { command: firstArg, launchArgs: [] };
|
|
524
529
|
}
|
|
525
530
|
export function resolveUpdateChannelArg(args) {
|
|
@@ -1042,6 +1047,228 @@ function execTmuxFileSync(args, options) {
|
|
|
1042
1047
|
...(process.platform === "win32" ? { windowsHide: true } : {}),
|
|
1043
1048
|
});
|
|
1044
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
|
+
}
|
|
1045
1272
|
function readTmuxEnvValueForTarget(targetPaneId) {
|
|
1046
1273
|
if (!targetPaneId.startsWith("%"))
|
|
1047
1274
|
return undefined;
|
|
@@ -1129,29 +1356,6 @@ function clearDetachedTmuxSessionHistoryIfUnattached(sessionName, leaderPaneId)
|
|
|
1129
1356
|
logCliOperationFailure(err);
|
|
1130
1357
|
}
|
|
1131
1358
|
}
|
|
1132
|
-
function readTmuxSessionInstanceId(sessionName) {
|
|
1133
|
-
try {
|
|
1134
|
-
return execTmuxFileSync(["show-options", "-qv", "-t", sessionName, OMX_INSTANCE_OPTION], {
|
|
1135
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
1136
|
-
encoding: "utf-8",
|
|
1137
|
-
}).trim();
|
|
1138
|
-
}
|
|
1139
|
-
catch {
|
|
1140
|
-
return null;
|
|
1141
|
-
}
|
|
1142
|
-
}
|
|
1143
|
-
function tmuxPaneBelongsToSession(paneId, sessionName) {
|
|
1144
|
-
try {
|
|
1145
|
-
const paneSessionName = execTmuxFileSync(["display-message", "-p", "-t", paneId, "#{session_name}"], {
|
|
1146
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
1147
|
-
encoding: "utf-8",
|
|
1148
|
-
}).trim();
|
|
1149
|
-
return paneSessionName === sessionName;
|
|
1150
|
-
}
|
|
1151
|
-
catch {
|
|
1152
|
-
return false;
|
|
1153
|
-
}
|
|
1154
|
-
}
|
|
1155
1359
|
function buildDetachedHistoryPruneHookCommand(leaderPaneId) {
|
|
1156
1360
|
// The leader pane can be gone by the time the hook fires (e.g. crashed
|
|
1157
1361
|
// leader with a lingering session); suppress errors so tmux does not queue
|
|
@@ -1193,6 +1397,12 @@ function logCliOperationFailure(error) {
|
|
|
1193
1397
|
process.stderr.write(`[cli/index] operation failed: ${error}
|
|
1194
1398
|
`);
|
|
1195
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
|
+
}
|
|
1196
1406
|
function tmuxFailureMessage(error) {
|
|
1197
1407
|
if (!error || typeof error !== "object")
|
|
1198
1408
|
return String(error);
|
|
@@ -1207,77 +1417,18 @@ function isUnsupportedTmuxExtendedKeysFailure(error) {
|
|
|
1207
1417
|
return (message.includes("extended-keys") &&
|
|
1208
1418
|
/(?:invalid|unknown|unsupported) (?:option|flag|argument)|no such option|unknown option/.test(message));
|
|
1209
1419
|
}
|
|
1210
|
-
function
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
|
|
1216
|
-
|
|
1217
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1218
|
-
encoding: "utf-8",
|
|
1219
|
-
});
|
|
1220
|
-
return { usable: true };
|
|
1221
|
-
}
|
|
1222
|
-
catch (err) {
|
|
1223
|
-
const reason = tmuxFailureMessage(err);
|
|
1224
|
-
if (isBenignMissingTmuxServerMessage(reason)) {
|
|
1225
|
-
return { usable: true };
|
|
1226
|
-
}
|
|
1227
|
-
return { usable: false, reason };
|
|
1228
|
-
}
|
|
1229
|
-
}
|
|
1230
|
-
function warnDetachedTmuxFallback(reason) {
|
|
1231
|
-
const suffix = reason ? ` (${reason})` : "";
|
|
1232
|
-
console.warn(`[omx] warning: tmux is installed but its server/socket is unusable${suffix}. Falling back to direct Codex launch.`);
|
|
1233
|
-
}
|
|
1234
|
-
const QUICK_ATTACH_NOOP_THRESHOLD_MS = 2_000;
|
|
1235
|
-
function isWslWindowsTerminalEnvironment(env) {
|
|
1236
|
-
return Boolean(env.WT_SESSION?.trim() &&
|
|
1237
|
-
(env.WSL_INTEROP?.trim() ||
|
|
1238
|
-
env.WSL_DISTRO_NAME?.trim() ||
|
|
1239
|
-
env.WSLENV?.trim()));
|
|
1240
|
-
}
|
|
1241
|
-
function readDetachedSessionAttachedClientCount(sessionName) {
|
|
1242
|
-
try {
|
|
1243
|
-
const output = execTmuxFileSync(["display-message", "-p", "-t", sessionName, "#{session_attached}"], {
|
|
1244
|
-
stdio: ["ignore", "pipe", "pipe"],
|
|
1245
|
-
encoding: "utf-8",
|
|
1246
|
-
}).trim();
|
|
1247
|
-
const parsed = Number.parseInt(output, 10);
|
|
1248
|
-
return Number.isFinite(parsed) ? parsed : null;
|
|
1249
|
-
}
|
|
1250
|
-
catch (err) {
|
|
1251
|
-
logCliOperationFailure(err);
|
|
1252
|
-
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();
|
|
1253
1427
|
}
|
|
1254
1428
|
}
|
|
1255
|
-
function assertDetachedAttachDidNotNoop(sessionName, elapsedMs, env) {
|
|
1256
|
-
if (!isWslWindowsTerminalEnvironment(env))
|
|
1257
|
-
return;
|
|
1258
|
-
if (elapsedMs >= QUICK_ATTACH_NOOP_THRESHOLD_MS)
|
|
1259
|
-
return;
|
|
1260
|
-
const attachedClients = readDetachedSessionAttachedClientCount(sessionName);
|
|
1261
|
-
if (attachedClients === null || attachedClients > 0)
|
|
1262
|
-
return;
|
|
1263
|
-
throw new Error([
|
|
1264
|
-
"tmux attach-session returned immediately without attaching a client",
|
|
1265
|
-
`(session=${sessionName}).`,
|
|
1266
|
-
"This can happen on WSL2 under Windows Terminal.",
|
|
1267
|
-
"Falling back to direct Codex launch.",
|
|
1268
|
-
].join(" "));
|
|
1269
|
-
}
|
|
1270
1429
|
function resolveTmuxAwareLaunchPolicy(explicitLaunchPolicy, nativeWindows) {
|
|
1271
|
-
const launchPolicy = resolveCodexLaunchPolicy(process.env, process.platform,
|
|
1272
|
-
|
|
1273
|
-
return { launchPolicy, effectiveExplicitLaunchPolicy: explicitLaunchPolicy };
|
|
1274
|
-
}
|
|
1275
|
-
const tmuxHealth = checkDetachedTmuxLaunchHealth();
|
|
1276
|
-
if (tmuxHealth.usable) {
|
|
1277
|
-
return { launchPolicy, effectiveExplicitLaunchPolicy: explicitLaunchPolicy };
|
|
1278
|
-
}
|
|
1279
|
-
warnDetachedTmuxFallback(tmuxHealth.reason);
|
|
1280
|
-
return { launchPolicy: "direct", effectiveExplicitLaunchPolicy: "direct" };
|
|
1430
|
+
const launchPolicy = resolveCodexLaunchPolicy(process.env, process.platform, Boolean(resolveTmuxBinaryForPlatform()), nativeWindows, undefined, undefined, explicitLaunchPolicy);
|
|
1431
|
+
return { launchPolicy, effectiveExplicitLaunchPolicy: explicitLaunchPolicy };
|
|
1281
1432
|
}
|
|
1282
1433
|
export function resolveSignalExitCode(signal) {
|
|
1283
1434
|
if (!signal)
|
|
@@ -1507,15 +1658,6 @@ export function resolveOmxRootForLaunch(cwd, env = process.env) {
|
|
|
1507
1658
|
function resolveLaunchPath(cwd, raw) {
|
|
1508
1659
|
return isCrossPlatformAbsolutePath(raw) ? raw : join(cwd, raw);
|
|
1509
1660
|
}
|
|
1510
|
-
function resolveHudRuntimeRootSource(omxRootOverride, env = process.env) {
|
|
1511
|
-
if (env.OMX_TEAM_STATE_ROOT?.trim())
|
|
1512
|
-
return 'team-env';
|
|
1513
|
-
if (env.OMX_ROOT?.trim() || omxRootOverride)
|
|
1514
|
-
return 'omx-root-env';
|
|
1515
|
-
if (env.OMX_STATE_ROOT?.trim())
|
|
1516
|
-
return 'omx-state-root-env';
|
|
1517
|
-
return 'cwd-default';
|
|
1518
|
-
}
|
|
1519
1661
|
export function resolveHudRuntimeRootForLaunch(cwd, env = process.env) {
|
|
1520
1662
|
const omxTeamStateRoot = env.OMX_TEAM_STATE_ROOT?.trim();
|
|
1521
1663
|
if (omxTeamStateRoot) {
|
|
@@ -1574,6 +1716,8 @@ export function captureMadmaxWorktreeRuntimeContext(options) {
|
|
|
1574
1716
|
const inheritedRoot = resolveInheritedMadmaxRoot(env);
|
|
1575
1717
|
if (!inheritedRoot)
|
|
1576
1718
|
return undefined;
|
|
1719
|
+
if (!madmaxInheritedContextMatchesLaunch(options.sourceCwd, options.originalLaunchArgs, env))
|
|
1720
|
+
return undefined;
|
|
1577
1721
|
const sourceCwd = env.OMX_SOURCE_CWD?.trim() || options.sourceCwd;
|
|
1578
1722
|
const worktreeCwd = options.worktreeCwd?.trim();
|
|
1579
1723
|
const omxStateRoot = env.OMX_STATE_ROOT?.trim();
|
|
@@ -1761,31 +1905,77 @@ function readMadmaxDetachedActiveRecord(recordPath) {
|
|
|
1761
1905
|
tmux_session_name: parsed.tmux_session_name,
|
|
1762
1906
|
...(typeof parsed.session_id === "string" ? { session_id: parsed.session_id } : {}),
|
|
1763
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 } : {}),
|
|
1764
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 } : {}),
|
|
1765
1916
|
};
|
|
1766
1917
|
}
|
|
1767
1918
|
catch {
|
|
1768
1919
|
return null;
|
|
1769
1920
|
}
|
|
1770
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
|
+
}
|
|
1771
1962
|
function isReusableMadmaxDetachedActiveRecord(record) {
|
|
1772
|
-
if (!
|
|
1773
|
-
return false;
|
|
1774
|
-
if (!record.session_id || !record.tmux_pane_id)
|
|
1963
|
+
if (record.lifecycle_phase !== "ready" || !record.leader_pid || !record.base_state_root || !record.launch_nonce)
|
|
1775
1964
|
return false;
|
|
1776
|
-
|
|
1965
|
+
try {
|
|
1966
|
+
process.kill(record.leader_pid, 0);
|
|
1967
|
+
}
|
|
1968
|
+
catch {
|
|
1777
1969
|
return false;
|
|
1778
1970
|
}
|
|
1779
|
-
return tmuxPaneBelongsToSession(record.tmux_pane_id, record.tmux_session_name);
|
|
1780
|
-
}
|
|
1781
|
-
function detachedTmuxSessionExists(sessionName) {
|
|
1782
1971
|
try {
|
|
1783
|
-
|
|
1784
|
-
|
|
1972
|
+
if (realpathSync(record.base_state_root) !== record.base_state_root)
|
|
1973
|
+
return false;
|
|
1785
1974
|
}
|
|
1786
1975
|
catch {
|
|
1787
1976
|
return false;
|
|
1788
1977
|
}
|
|
1978
|
+
return hasMatchingMadmaxDetachedRuntimeBinding(record);
|
|
1789
1979
|
}
|
|
1790
1980
|
function readMadmaxDetachedLockOwner(lockPath) {
|
|
1791
1981
|
try {
|
|
@@ -1854,7 +2044,7 @@ function inspectMadmaxDetachedContextLock(lockPath) {
|
|
|
1854
2044
|
diagnostic: `lock has no readable owner pid yet; lock age ${Math.round(ageMs)}ms`,
|
|
1855
2045
|
};
|
|
1856
2046
|
}
|
|
1857
|
-
export function withMadmaxDetachedContextLock(runsRoot, contextKey, run, options = {}) {
|
|
2047
|
+
export async function withMadmaxDetachedContextLock(runsRoot, contextKey, run, options = {}) {
|
|
1858
2048
|
const lockPath = join(runsRoot, MADMAX_DETACHED_ACTIVE_DIR, `${contextKey}.lock`);
|
|
1859
2049
|
const maxAttempts = options.maxAttempts ?? MADMAX_DETACHED_LOCK_MAX_ATTEMPTS;
|
|
1860
2050
|
const retryMs = options.retryMs ?? MADMAX_DETACHED_LOCK_RETRY_MS;
|
|
@@ -1872,7 +2062,7 @@ export function withMadmaxDetachedContextLock(runsRoot, contextKey, run, options
|
|
|
1872
2062
|
};
|
|
1873
2063
|
writeFileSync(join(lockPath, "owner.json"), `${JSON.stringify(owner, null, 2)}\n`, { mode: 0o600 });
|
|
1874
2064
|
writeFileSync(join(lockPath, "pid"), String(process.pid));
|
|
1875
|
-
return run();
|
|
2065
|
+
return await run();
|
|
1876
2066
|
}
|
|
1877
2067
|
finally {
|
|
1878
2068
|
rmSync(lockPath, { recursive: true, force: true });
|
|
@@ -1900,6 +2090,7 @@ function readMadmaxRunMetadata(runRoot) {
|
|
|
1900
2090
|
const parsed = JSON.parse(readFileSync(join(runRoot, ".omxbox-run.json"), "utf-8"));
|
|
1901
2091
|
return {
|
|
1902
2092
|
...(typeof parsed.cwd === "string" ? { cwd: parsed.cwd } : {}),
|
|
2093
|
+
...(typeof parsed.source_cwd === "string" ? { source_cwd: parsed.source_cwd } : {}),
|
|
1903
2094
|
...(typeof parsed.detached_launch_context === "string"
|
|
1904
2095
|
? { detached_launch_context: parsed.detached_launch_context }
|
|
1905
2096
|
: {}),
|
|
@@ -1927,13 +2118,27 @@ function madmaxInheritedContextMatchesLaunch(cwd, launchArgs, env) {
|
|
|
1927
2118
|
return false;
|
|
1928
2119
|
if (metadata.cwd && metadata.cwd !== inheritedRoot)
|
|
1929
2120
|
return false;
|
|
2121
|
+
const sourceCwd = env.OMX_SOURCE_CWD?.trim();
|
|
2122
|
+
if (!sourceCwd || !metadata.source_cwd || metadata.source_cwd !== sourceCwd)
|
|
2123
|
+
return false;
|
|
1930
2124
|
if (metadata.detached_launch_context !== context)
|
|
1931
2125
|
return false;
|
|
1932
2126
|
const expectedContext = buildMadmaxDetachedLaunchContextKey(cwd, [...launchArgs], inheritedRoot);
|
|
1933
2127
|
return expectedContext === context;
|
|
1934
2128
|
}
|
|
1935
|
-
function
|
|
1936
|
-
|
|
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
|
+
};
|
|
1937
2142
|
}
|
|
1938
2143
|
function cleanupCurrentMadmaxReuseRunRoot(env, runsRoot) {
|
|
1939
2144
|
const runRoot = env.OMX_ROOT;
|
|
@@ -1945,15 +2150,236 @@ function cleanupCurrentMadmaxReuseRunRoot(env, runsRoot) {
|
|
|
1945
2150
|
rmSync(runRoot, { recursive: true, force: true });
|
|
1946
2151
|
}
|
|
1947
2152
|
function writeMadmaxDetachedActiveRecord(recordPath, record) {
|
|
1948
|
-
|
|
1949
|
-
|
|
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
|
+
}
|
|
1950
2197
|
}
|
|
1951
|
-
|
|
1952
|
-
|
|
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
|
+
}
|
|
1953
2348
|
}
|
|
1954
2349
|
class MadmaxDetachedGuardError extends Error {
|
|
1955
2350
|
failClosed = true;
|
|
1956
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
|
+
}
|
|
1957
2383
|
export function createMadmaxIsolatedRoot(sourceCwd, argv, env = process.env) {
|
|
1958
2384
|
const runsRoot = resolveMadmaxRunsRoot(env);
|
|
1959
2385
|
mkdirSync(runsRoot, { recursive: true });
|
|
@@ -1984,6 +2410,23 @@ function activateMadmaxIsolationIfNeeded(command, launchArgs, cwd, env = process
|
|
|
1984
2410
|
env.OMX_SOURCE_CWD = cwd;
|
|
1985
2411
|
process.stderr.write(`[omx] madmax isolated state: ${runDir} (source: ${cwd})\n`);
|
|
1986
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
|
+
}
|
|
1987
2430
|
export async function main(args) {
|
|
1988
2431
|
const knownCommands = new Set([
|
|
1989
2432
|
"launch",
|
|
@@ -1992,6 +2435,7 @@ export async function main(args) {
|
|
|
1992
2435
|
"imagegen",
|
|
1993
2436
|
"capabilities",
|
|
1994
2437
|
"setup",
|
|
2438
|
+
"__detached-session-leader",
|
|
1995
2439
|
"update",
|
|
1996
2440
|
"list",
|
|
1997
2441
|
"agents",
|
|
@@ -2043,6 +2487,9 @@ export async function main(args) {
|
|
|
2043
2487
|
console.log(HELP);
|
|
2044
2488
|
return;
|
|
2045
2489
|
}
|
|
2490
|
+
if (command !== "launch" && command !== "resume") {
|
|
2491
|
+
activateMadmaxIsolationIfNeeded(command, launchArgs, process.cwd(), process.env);
|
|
2492
|
+
}
|
|
2046
2493
|
try {
|
|
2047
2494
|
if (command === "session") {
|
|
2048
2495
|
await sessionCommand(args.slice(1));
|
|
@@ -2052,8 +2499,13 @@ export async function main(args) {
|
|
|
2052
2499
|
console.log(RESUME_HELP);
|
|
2053
2500
|
return;
|
|
2054
2501
|
}
|
|
2055
|
-
activateMadmaxIsolationIfNeeded(command, launchArgs, process.cwd(), process.env);
|
|
2056
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
|
+
}
|
|
2057
2509
|
case "launch":
|
|
2058
2510
|
if (launchArgsRequestAuthHotswap(launchArgs)) {
|
|
2059
2511
|
await launchWithAuthHotswap(launchArgs);
|
|
@@ -2249,7 +2701,13 @@ export async function main(args) {
|
|
|
2249
2701
|
}
|
|
2250
2702
|
}
|
|
2251
2703
|
catch (err) {
|
|
2252
|
-
|
|
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
|
+
}
|
|
2253
2711
|
process.exit(1);
|
|
2254
2712
|
}
|
|
2255
2713
|
}
|
|
@@ -2480,38 +2938,29 @@ export async function launchWithAuthHotswap(args) {
|
|
|
2480
2938
|
process.exitCode = status;
|
|
2481
2939
|
}
|
|
2482
2940
|
export async function launchWithHud(args) {
|
|
2483
|
-
if (isNativeWindows()) {
|
|
2484
|
-
const { result } = spawnPlatformCommandSync("tmux", ["-V"], {
|
|
2485
|
-
encoding: "utf-8",
|
|
2486
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
2487
|
-
});
|
|
2488
|
-
if (result.error) {
|
|
2489
|
-
const errno = result.error;
|
|
2490
|
-
const kind = classifySpawnError(errno);
|
|
2491
|
-
if (kind === "missing") {
|
|
2492
|
-
console.warn("[omx] warning: tmux was not found on native Windows. Continuing without tmux/HUD.\n" +
|
|
2493
|
-
"[omx] To enable tmux-backed features, install psmux:\n" +
|
|
2494
|
-
"[omx] winget install psmux\n" +
|
|
2495
|
-
"[omx] See: https://github.com/marlocarlo/psmux");
|
|
2496
|
-
}
|
|
2497
|
-
else {
|
|
2498
|
-
console.warn(`[omx] warning: tmux probe failed on native Windows (${errno.code || errno.message}). Continuing without tmux/HUD.`);
|
|
2499
|
-
}
|
|
2500
|
-
}
|
|
2501
|
-
else if (result.status !== 0 && !isTmuxAvailable()) {
|
|
2502
|
-
const stderr = (result.stderr || "").trim();
|
|
2503
|
-
console.warn(`[omx] warning: tmux reported an error on native Windows${stderr ? ` (${stderr})` : ""}. Continuing without tmux/HUD.`);
|
|
2504
|
-
}
|
|
2505
|
-
}
|
|
2506
2941
|
const launchCwd = process.cwd();
|
|
2507
2942
|
const { omxArgs, suffix } = splitOmxArgsAtEndOfOptions(args);
|
|
2508
2943
|
const parsedWorktree = parseWorktreeMode(omxArgs);
|
|
2509
2944
|
const notifyTempResult = resolveNotifyTempContract(parsedWorktree.remainingArgs, process.env);
|
|
2510
2945
|
const passthroughArgs = [...notifyTempResult.passthroughArgs, ...suffix];
|
|
2511
2946
|
const explicitLaunchPolicy = resolveEffectiveLeaderLaunchPolicyOverride(passthroughArgs, process.env);
|
|
2512
|
-
|
|
2513
|
-
|
|
2947
|
+
let { launchPolicy } = resolveTmuxAwareLaunchPolicy(explicitLaunchPolicy, isNativeWindows());
|
|
2948
|
+
let detachedPreflight;
|
|
2949
|
+
if (launchPolicy === "detached-tmux") {
|
|
2950
|
+
try {
|
|
2951
|
+
detachedPreflight = preflightDetachedLaunch();
|
|
2952
|
+
}
|
|
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";
|
|
2958
|
+
}
|
|
2959
|
+
}
|
|
2514
2960
|
const enableNotifyFallbackAuthority = launchPolicy === "direct";
|
|
2961
|
+
activateMadmaxIsolationIfNeeded("launch", args, launchCwd, process.env);
|
|
2962
|
+
const verifiedMadmaxLaunchContext = resolveVerifiedMadmaxDetachedContext(launchCwd, args, process.env);
|
|
2963
|
+
const persistentCodexHomeForLaunch = resolveCodexHomeForLaunch(launchCwd, process.env);
|
|
2515
2964
|
const workerSparkModel = resolveWorkerSparkModel(passthroughArgs, persistentCodexHomeForLaunch);
|
|
2516
2965
|
let normalizedArgs = normalizeCodexLaunchArgs(passthroughArgs);
|
|
2517
2966
|
let cwd = launchCwd;
|
|
@@ -2551,6 +3000,8 @@ export async function launchWithHud(args) {
|
|
|
2551
3000
|
clearInheritedMadmaxRootForDisposableWorktreeLaunch(parsedWorktree.remainingArgs);
|
|
2552
3001
|
applyDisposableWorktreeOmxRootForLaunch(ensuredLaunchWorktree);
|
|
2553
3002
|
applyWorktreeToolContextForLaunch(cwd, ensuredLaunchWorktree);
|
|
3003
|
+
if (launchPolicy === "detached-tmux" && detachedPreflight && handleMadmaxDetachedReuse(madmaxWorktreeRuntimeContext, detachedPreflight))
|
|
3004
|
+
return;
|
|
2554
3005
|
const sessionId = `omx-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
2555
3006
|
try {
|
|
2556
3007
|
await maybeCheckAndPromptUpdate(cwd);
|
|
@@ -2592,37 +3043,77 @@ export async function launchWithHud(args) {
|
|
|
2592
3043
|
const codexHomeOverride = preparedCodexHome.codexHomeOverride;
|
|
2593
3044
|
const sqliteHomeOverride = preparedCodexHome.sqliteHomeOverride;
|
|
2594
3045
|
const projectLocalCodexHomeForCleanup = preparedCodexHome.projectLocalCodexHomeForCleanup;
|
|
2595
|
-
|
|
2596
|
-
|
|
2597
|
-
|
|
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
|
+
});
|
|
2598
3065
|
}
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
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?.();
|
|
2603
3081
|
await cleanupRuntimeCodexHome(preparedCodexHome.runtimeCodexHomeForCleanup, projectLocalCodexHomeForCleanup).catch((cleanupErr) => {
|
|
2604
3082
|
console.error(`[omx] preLaunch abort cleanup warning: ${cleanupErr instanceof Error ? cleanupErr.message : cleanupErr}`);
|
|
2605
3083
|
});
|
|
2606
3084
|
throw err;
|
|
2607
3085
|
}
|
|
2608
|
-
// preLaunch errors after pointer commit must not prevent Codex from starting.
|
|
2609
|
-
console.error(`[omx] preLaunch warning: ${err instanceof Error ? err.message : err}`);
|
|
2610
3086
|
}
|
|
2611
|
-
// ── Phase 2: run ────────────────────────────────────────────────────────
|
|
2612
3087
|
let postLaunchHandledExternally = false;
|
|
2613
3088
|
try {
|
|
2614
3089
|
const notifyTempContractRaw = notifyTempResult.contract.active
|
|
2615
3090
|
? serializeNotifyTempContract(notifyTempResult.contract)
|
|
2616
3091
|
: null;
|
|
2617
|
-
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);
|
|
2618
3097
|
postLaunchHandledExternally = launchResult.postLaunchHandledExternally;
|
|
2619
3098
|
}
|
|
3099
|
+
catch (error) {
|
|
3100
|
+
if (error instanceof DetachedLaunchSafetyError)
|
|
3101
|
+
postLaunchHandledExternally = true;
|
|
3102
|
+
throw error;
|
|
3103
|
+
}
|
|
2620
3104
|
finally {
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
3105
|
+
if (!postLaunchHandledExternally && launch) {
|
|
3106
|
+
try {
|
|
3107
|
+
await postLaunch(cwd, sessionId, launch.binding, codexHomeOverride, enableNotifyFallbackAuthority, projectLocalCodexHomeForCleanup);
|
|
3108
|
+
}
|
|
3109
|
+
finally {
|
|
3110
|
+
restoreInsideTmuxControlPlane?.();
|
|
3111
|
+
}
|
|
2624
3112
|
await cleanupRuntimeCodexHome(preparedCodexHome.runtimeCodexHomeForCleanup, projectLocalCodexHomeForCleanup).catch(logCliOperationFailure);
|
|
2625
3113
|
}
|
|
3114
|
+
else {
|
|
3115
|
+
restoreInsideTmuxControlPlane?.();
|
|
3116
|
+
}
|
|
2626
3117
|
}
|
|
2627
3118
|
}
|
|
2628
3119
|
export async function execWithOverlay(args) {
|
|
@@ -2689,18 +3180,19 @@ export async function execWithOverlay(args) {
|
|
|
2689
3180
|
const codexHomeOverride = preparedCodexHome.codexHomeOverride;
|
|
2690
3181
|
const sqliteHomeOverride = preparedCodexHome.sqliteHomeOverride;
|
|
2691
3182
|
const projectLocalCodexHomeForCleanup = preparedCodexHome.projectLocalCodexHomeForCleanup;
|
|
3183
|
+
let launch;
|
|
2692
3184
|
try {
|
|
2693
|
-
await preLaunch(cwd, sessionId, notifyTempResult.contract, codexHomeOverride, true, worktreeDirty);
|
|
3185
|
+
launch = await preLaunch(cwd, sessionId, notifyTempResult.contract, codexHomeOverride, true, worktreeDirty);
|
|
2694
3186
|
}
|
|
2695
3187
|
catch (err) {
|
|
3188
|
+
logEstablishmentCleanupEvidence(err);
|
|
2696
3189
|
if (isSessionPointerLaunchAbort(err)) {
|
|
2697
3190
|
console.error(`[omx] session pointer launch aborted: ${err.code}`);
|
|
2698
|
-
await cleanupRuntimeCodexHome(preparedCodexHome.runtimeCodexHomeForCleanup, projectLocalCodexHomeForCleanup).catch((cleanupErr) => {
|
|
2699
|
-
console.error(`[omx] preLaunch abort cleanup warning: ${cleanupErr instanceof Error ? cleanupErr.message : cleanupErr}`);
|
|
2700
|
-
});
|
|
2701
|
-
throw err;
|
|
2702
3191
|
}
|
|
2703
|
-
|
|
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;
|
|
2704
3196
|
}
|
|
2705
3197
|
try {
|
|
2706
3198
|
const notifyTempContractRaw = notifyTempResult.contract.active
|
|
@@ -2708,22 +3200,28 @@ export async function execWithOverlay(args) {
|
|
|
2708
3200
|
: null;
|
|
2709
3201
|
const codexArgs = injectModelInstructionsBypassArgs(cwd, ["exec", ...normalizedArgs], process.env, sessionModelInstructionsPath(cwd, sessionId));
|
|
2710
3202
|
const omxRootOverride = resolveOmxRootForLaunch(cwd, process.env);
|
|
2711
|
-
const
|
|
2712
|
-
|
|
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),
|
|
2713
3209
|
...(codexHomeOverride ? { CODEX_HOME: codexHomeOverride } : {}),
|
|
2714
3210
|
...(sqliteHomeOverride ? { [CODEX_SQLITE_HOME_ENV]: sqliteHomeOverride } : {}),
|
|
2715
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 } : {}),
|
|
2716
3220
|
};
|
|
2717
|
-
const codexEnv = notifyTempContractRaw
|
|
2718
|
-
? {
|
|
2719
|
-
...codexEnvBase,
|
|
2720
|
-
[OMX_NOTIFY_TEMP_CONTRACT_ENV]: notifyTempContractRaw,
|
|
2721
|
-
}
|
|
2722
|
-
: codexEnvBase;
|
|
2723
3221
|
runCodexBlocking(cwd, codexArgs, codexEnv);
|
|
2724
3222
|
}
|
|
2725
3223
|
finally {
|
|
2726
|
-
await postLaunch(cwd, sessionId, codexHomeOverride, true, projectLocalCodexHomeForCleanup);
|
|
3224
|
+
await postLaunch(cwd, sessionId, launch.binding, codexHomeOverride, true, projectLocalCodexHomeForCleanup);
|
|
2727
3225
|
await cleanupRuntimeCodexHome(preparedCodexHome.runtimeCodexHomeForCleanup, projectLocalCodexHomeForCleanup).catch(logCliOperationFailure);
|
|
2728
3226
|
}
|
|
2729
3227
|
}
|
|
@@ -3005,9 +3503,6 @@ export function detectDetachedSessionWindowIndex(sessionName) {
|
|
|
3005
3503
|
return null;
|
|
3006
3504
|
}
|
|
3007
3505
|
}
|
|
3008
|
-
function escapeShellDoubleQuotedValue(value) {
|
|
3009
|
-
return value.replace(/["\\$`]/g, "\\$&");
|
|
3010
|
-
}
|
|
3011
3506
|
function sanitizeTmuxLeaseKey(value) {
|
|
3012
3507
|
const cleaned = value
|
|
3013
3508
|
.toLowerCase()
|
|
@@ -3193,121 +3688,188 @@ function withTmuxExtendedKeysLeaseLock(cwd, socketPath, run) {
|
|
|
3193
3688
|
}
|
|
3194
3689
|
throw new Error(`timed out waiting for tmux extended-keys lease lock: ${lockPath}`);
|
|
3195
3690
|
}
|
|
3196
|
-
function buildDetachedSessionLeaderCommand(cwd, sessionName, codexCmd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, parentEnvFilePath) {
|
|
3197
|
-
const
|
|
3198
|
-
|
|
3199
|
-
|
|
3200
|
-
|
|
3201
|
-
|
|
3202
|
-
|
|
3203
|
-
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
|
|
3207
|
-
|
|
3208
|
-
|
|
3209
|
-
|
|
3210
|
-
|
|
3211
|
-
|
|
3212
|
-
|
|
3213
|
-
|
|
3214
|
-
|
|
3215
|
-
|
|
3216
|
-
|
|
3217
|
-
|
|
3218
|
-
|
|
3219
|
-
|
|
3220
|
-
|
|
3221
|
-
|
|
3222
|
-
|
|
3223
|
-
"fi;",
|
|
3224
|
-
"exit $status;",
|
|
3225
|
-
"};",
|
|
3226
|
-
"trap omx_detached_session_cleanup 0 INT TERM HUP;",
|
|
3227
|
-
parentEnvSource,
|
|
3228
|
-
"unset OMX_HERMES_MCP_BRIDGE;",
|
|
3229
|
-
"omx_codex_started_at=$(date +%s 2>/dev/null || printf 0);",
|
|
3230
|
-
`${codexCmd} <&3 &`,
|
|
3231
|
-
"omx_codex_pid=$!;",
|
|
3232
|
-
'wait "$omx_codex_pid";',
|
|
3233
|
-
"omx_codex_status=$?;",
|
|
3234
|
-
"omx_codex_finished_at=$(date +%s 2>/dev/null || printf 0);",
|
|
3235
|
-
'omx_codex_elapsed=$((omx_codex_finished_at - omx_codex_started_at));',
|
|
3236
|
-
'if [ "$omx_codex_status" -eq 0 ] && [ "$omx_codex_elapsed" -le 2 ]; then',
|
|
3237
|
-
'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;',
|
|
3238
|
-
'IFS= read -r _omx_close || true;',
|
|
3239
|
-
'elif [ "$omx_codex_status" -gt 0 ] && [ "$omx_codex_status" -lt 128 ] && [ "$omx_codex_elapsed" -le 2 ]; then',
|
|
3240
|
-
'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;',
|
|
3241
|
-
'IFS= read -r _omx_close || true;',
|
|
3242
|
-
'elif [ "$omx_codex_status" -gt 0 ] && [ "$omx_codex_status" -lt 128 ]; then',
|
|
3243
|
-
'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;',
|
|
3244
|
-
'IFS= read -r _omx_close || true;',
|
|
3245
|
-
"fi;",
|
|
3246
|
-
'exit "$omx_codex_status";',
|
|
3247
|
-
].join(" ");
|
|
3248
|
-
return `/bin/sh -c ${quoteShellArg(wrapped)}`;
|
|
3249
|
-
}
|
|
3250
|
-
function buildDetachedSessionPostLaunchHelperCommand(cwd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup) {
|
|
3251
|
-
const cwdLiteral = JSON.stringify(cwd);
|
|
3252
|
-
const sessionIdLiteral = JSON.stringify(sessionId);
|
|
3253
|
-
const codexHomeLiteral = typeof codexHomeOverride === "string" && codexHomeOverride.length > 0
|
|
3254
|
-
? JSON.stringify(codexHomeOverride)
|
|
3255
|
-
: "undefined";
|
|
3256
|
-
const projectLocalCleanupLiteral = typeof projectLocalCodexHomeForCleanup === "string" &&
|
|
3257
|
-
projectLocalCodexHomeForCleanup.length > 0
|
|
3258
|
-
? JSON.stringify(projectLocalCodexHomeForCleanup)
|
|
3259
|
-
: "undefined";
|
|
3260
|
-
const runtimeCodexHomeCleanupLiteral = typeof runtimeCodexHomeForCleanup === "string" &&
|
|
3261
|
-
runtimeCodexHomeForCleanup.length > 0
|
|
3262
|
-
? JSON.stringify(runtimeCodexHomeForCleanup)
|
|
3263
|
-
: "undefined";
|
|
3264
|
-
const moduleUrlLiteral = JSON.stringify(import.meta.url);
|
|
3265
|
-
const script = [
|
|
3266
|
-
`const mod = await import(${moduleUrlLiteral});`,
|
|
3267
|
-
`await mod.runDetachedSessionPostLaunch(${cwdLiteral}, ${sessionIdLiteral}, ${codexHomeLiteral}, ${projectLocalCleanupLiteral}, ${runtimeCodexHomeCleanupLiteral});`,
|
|
3268
|
-
].join(" ");
|
|
3269
|
-
return `${quoteShellArg(process.execPath)} --input-type=module -e ${quoteShellArg(script)}`;
|
|
3270
|
-
}
|
|
3271
|
-
function execTmuxSync(args, execFileSyncImpl = (file, tmuxArgs) => execFileSync(file, tmuxArgs, {
|
|
3272
|
-
encoding: "utf-8",
|
|
3273
|
-
...(process.platform === "win32" ? { windowsHide: true } : {}),
|
|
3274
|
-
})) {
|
|
3275
|
-
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;
|
|
3276
3718
|
}
|
|
3277
|
-
|
|
3278
|
-
|
|
3279
|
-
|
|
3280
|
-
|
|
3719
|
+
function writeDetachedLeaderReport(path, report) {
|
|
3720
|
+
mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
|
|
3721
|
+
const tempPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
3722
|
+
let fd;
|
|
3281
3723
|
try {
|
|
3282
|
-
|
|
3283
|
-
|
|
3284
|
-
|
|
3285
|
-
|
|
3286
|
-
|
|
3287
|
-
|
|
3288
|
-
|
|
3289
|
-
|
|
3290
|
-
|
|
3291
|
-
|
|
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);
|
|
3292
3740
|
}
|
|
3293
|
-
|
|
3294
|
-
|
|
3295
|
-
|
|
3296
|
-
execTmuxSync(["set-option", "-sq", "extended-keys", TMUX_EXTENDED_KEYS_MODE], execFileSyncImpl);
|
|
3297
|
-
writeTmuxExtendedKeysLeaseState(leasePath, {
|
|
3298
|
-
originalMode: previousMode,
|
|
3299
|
-
holders: [createTmuxExtendedKeysLeaseHolder(leaseId, holderPid)],
|
|
3300
|
-
});
|
|
3301
|
-
return;
|
|
3741
|
+
catch (error) {
|
|
3742
|
+
if (!(process.platform === "win32" && error.code === "EPERM"))
|
|
3743
|
+
throw error;
|
|
3302
3744
|
}
|
|
3303
|
-
|
|
3304
|
-
|
|
3305
|
-
|
|
3306
|
-
|
|
3307
|
-
}
|
|
3308
|
-
|
|
3309
|
-
if (
|
|
3310
|
-
|
|
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)}`;
|
|
3848
|
+
withTmuxExtendedKeysLeaseLock(cwd, socketPath, () => {
|
|
3849
|
+
const stateRaw = readTmuxExtendedKeysLeaseState(leasePath);
|
|
3850
|
+
const state = stateRaw ? reapDeadTmuxExtendedKeysLeaseHolders(stateRaw) : null;
|
|
3851
|
+
if (stateRaw && state?.holders.length === 0) {
|
|
3852
|
+
execTmuxSync(["set-option", "-sq", "extended-keys", state.originalMode], execFileSyncImpl);
|
|
3853
|
+
rmSync(leasePath, { force: true });
|
|
3854
|
+
}
|
|
3855
|
+
if (!state || state.holders.length === 0) {
|
|
3856
|
+
const previousMode = execTmuxSync(["show-options", "-sv", "extended-keys"], execFileSyncImpl) ||
|
|
3857
|
+
TMUX_EXTENDED_KEYS_FALLBACK_MODE;
|
|
3858
|
+
execTmuxSync(["set-option", "-sq", "extended-keys", TMUX_EXTENDED_KEYS_MODE], execFileSyncImpl);
|
|
3859
|
+
writeTmuxExtendedKeysLeaseState(leasePath, {
|
|
3860
|
+
originalMode: previousMode,
|
|
3861
|
+
holders: [createTmuxExtendedKeysLeaseHolder(leaseId, holderPid)],
|
|
3862
|
+
});
|
|
3863
|
+
return;
|
|
3864
|
+
}
|
|
3865
|
+
state.holders.push(createTmuxExtendedKeysLeaseHolder(leaseId, holderPid));
|
|
3866
|
+
writeTmuxExtendedKeysLeaseState(leasePath, state);
|
|
3867
|
+
});
|
|
3868
|
+
return `${socketPath}\t${leaseId}`;
|
|
3869
|
+
}
|
|
3870
|
+
catch (err) {
|
|
3871
|
+
if (!isUnsupportedTmuxExtendedKeysFailure(err)) {
|
|
3872
|
+
logCliOperationFailure(err);
|
|
3311
3873
|
}
|
|
3312
3874
|
return null;
|
|
3313
3875
|
}
|
|
@@ -3352,19 +3914,123 @@ export function releaseTmuxExtendedKeysLease(cwd, leaseHandle, execFileSyncImpl
|
|
|
3352
3914
|
}
|
|
3353
3915
|
}
|
|
3354
3916
|
}
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
|
|
3359
|
-
|
|
3360
|
-
|
|
3361
|
-
|
|
3362
|
-
|
|
3363
|
-
|
|
3364
|
-
|
|
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
|
+
});
|
|
3365
4010
|
}
|
|
3366
|
-
|
|
3367
|
-
|
|
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
|
+
};
|
|
3368
4034
|
}
|
|
3369
4035
|
const SHELL_ENV_NAME_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
3370
4036
|
const DETACHED_SESSION_PANE_ENV_KEYS = new Set([
|
|
@@ -3376,21 +4042,23 @@ const DETACHED_SESSION_PANE_ENV_KEYS = new Set([
|
|
|
3376
4042
|
"COLUMNS",
|
|
3377
4043
|
"LINES",
|
|
3378
4044
|
]);
|
|
3379
|
-
|
|
3380
|
-
const
|
|
4045
|
+
function detachedSessionParentEnvironment(env, caseInsensitive = false) {
|
|
4046
|
+
const values = {};
|
|
3381
4047
|
for (const key of Object.keys(env).sort()) {
|
|
3382
|
-
if (!SHELL_ENV_NAME_PATTERN.test(key)
|
|
3383
|
-
|
|
3384
|
-
|
|
4048
|
+
if (!SHELL_ENV_NAME_PATTERN.test(key)
|
|
4049
|
+
|| DETACHED_SESSION_PANE_ENV_KEYS.has(key)
|
|
4050
|
+
|| isDetachedLaunchControlPlaneKey(key, caseInsensitive))
|
|
3385
4051
|
continue;
|
|
3386
4052
|
const value = env[key];
|
|
3387
|
-
if (typeof value
|
|
3388
|
-
|
|
3389
|
-
if (value.includes("\0"))
|
|
3390
|
-
continue;
|
|
3391
|
-
lines.push(`export ${key}=${quoteShellArg(value)}`);
|
|
4053
|
+
if (typeof value === "string" && !value.includes("\0"))
|
|
4054
|
+
values[key] = value;
|
|
3392
4055
|
}
|
|
3393
|
-
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`;
|
|
3394
4062
|
}
|
|
3395
4063
|
export function detachedSessionParentEnvFilePath(cwd, sessionId) {
|
|
3396
4064
|
const safeSessionId = sessionId.replace(/[^A-Za-z0-9_.-]/g, "_");
|
|
@@ -3418,27 +4086,21 @@ export function withTmuxExtendedKeys(cwd, run, execFileSyncImpl = (file, tmuxArg
|
|
|
3418
4086
|
releaseTmuxExtendedKeysLease(cwd, leaseHandle, execFileSyncImpl);
|
|
3419
4087
|
}
|
|
3420
4088
|
}
|
|
3421
|
-
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) {
|
|
3422
4094
|
const detachedLeaderCmd = nativeWindows
|
|
3423
|
-
?
|
|
3424
|
-
: buildDetachedSessionLeaderCommand(cwd, sessionName, codexCmd, sessionId, codexHomeOverride, projectLocalCodexHomeForCleanup, runtimeCodexHomeForCleanup, parentEnvFilePath);
|
|
3425
|
-
const
|
|
3426
|
-
|
|
3427
|
-
:
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
: hasExplicitRootOverride
|
|
3433
|
-
? {
|
|
3434
|
-
omxRoot: omxRootOverride,
|
|
3435
|
-
rootSource: resolveHudRuntimeRootSource(omxRootOverride, env),
|
|
3436
|
-
}
|
|
3437
|
-
: resolveHudRuntimeRootForLaunch(cwd, env);
|
|
3438
|
-
const hudRuntimeEnv = buildHudRuntimeEnv({
|
|
3439
|
-
sessionId,
|
|
3440
|
-
...hudRuntimeRoot,
|
|
3441
|
-
}).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);
|
|
3442
4104
|
const newSessionArgs = [
|
|
3443
4105
|
"new-session",
|
|
3444
4106
|
"-d",
|
|
@@ -3449,15 +4111,10 @@ export function buildDetachedSessionBootstrapSteps(sessionName, cwd, codexCmd, h
|
|
|
3449
4111
|
sessionName,
|
|
3450
4112
|
"-c",
|
|
3451
4113
|
cwd,
|
|
4114
|
+
...DETACHED_LAUNCH_CONTROL_PLANE_KEYS.flatMap((key) => ["-e", `${key}=${controlPlaneEnv[key]}`]),
|
|
3452
4115
|
...(workerLaunchArgs ? ["-e", `${TEAM_WORKER_LAUNCH_ARGS_ENV}=${workerLaunchArgs}`] : []),
|
|
3453
|
-
...Object.entries(hudRuntimeEnv).map(([key, value]) => ["-e", `${key}=${value}`]).flat(),
|
|
3454
4116
|
...(codexHomeOverride ? ["-e", `CODEX_HOME=${codexHomeOverride}`] : []),
|
|
3455
4117
|
...(sqliteHomeOverride ? ["-e", `${CODEX_SQLITE_HOME_ENV}=${sqliteHomeOverride}`] : []),
|
|
3456
|
-
...(env.OMXBOX_ACTIVE ? ["-e", `OMXBOX_ACTIVE=${env.OMXBOX_ACTIVE}`] : []),
|
|
3457
|
-
...(env.OMX_SOURCE_CWD ? ["-e", `OMX_SOURCE_CWD=${env.OMX_SOURCE_CWD}`] : []),
|
|
3458
|
-
...(env[OMX_MADMAX_DETACHED_CONTEXT_ENV]
|
|
3459
|
-
? ["-e", `${OMX_MADMAX_DETACHED_CONTEXT_ENV}=${env[OMX_MADMAX_DETACHED_CONTEXT_ENV]}`]
|
|
3460
|
-
: []),
|
|
3461
4118
|
...(notifyTempContractRaw
|
|
3462
4119
|
? ["-e", `${OMX_NOTIFY_TEMP_CONTRACT_ENV}=${notifyTempContractRaw}`]
|
|
3463
4120
|
: []),
|
|
@@ -3717,41 +4374,40 @@ function isAutopilotReviewPendingPostLaunchState(state) {
|
|
|
3717
4374
|
function postLaunchUniqueStrings(values) {
|
|
3718
4375
|
return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
|
|
3719
4376
|
}
|
|
3720
|
-
async function scrubPostLaunchRootSkillActiveForSession(stateDir, sessionId, nowIso,
|
|
4377
|
+
async function scrubPostLaunchRootSkillActiveForSession(stateDir, sessionId, nowIso, writeRootStateFn) {
|
|
3721
4378
|
const normalizedSessionId = cleanPostLaunchString(sessionId);
|
|
3722
4379
|
if (!normalizedSessionId)
|
|
3723
4380
|
return;
|
|
3724
|
-
|
|
3725
|
-
|
|
3726
|
-
|
|
3727
|
-
|
|
3728
|
-
|
|
3729
|
-
|
|
3730
|
-
|
|
3731
|
-
|
|
3732
|
-
|
|
3733
|
-
|
|
3734
|
-
|
|
3735
|
-
|
|
3736
|
-
|
|
3737
|
-
return
|
|
3738
|
-
|
|
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
|
+
};
|
|
3739
4410
|
});
|
|
3740
|
-
if (keptEntries.length === entries.length && rootState.active !== true)
|
|
3741
|
-
return;
|
|
3742
|
-
if (keptEntries.length === entries.length && !rootBelongsToSession)
|
|
3743
|
-
return;
|
|
3744
|
-
const nextRoot = {
|
|
3745
|
-
...rootState,
|
|
3746
|
-
active: keptEntries.length > 0,
|
|
3747
|
-
skill: keptEntries[0]?.skill ?? (keptEntries.length > 0 ? cleanPostLaunchString(rootState.skill) : ""),
|
|
3748
|
-
phase: keptEntries[0]?.phase ?? (keptEntries.length > 0 ? cleanPostLaunchString(rootState.phase) : "complete"),
|
|
3749
|
-
updated_at: nowIso,
|
|
3750
|
-
active_skills: keptEntries,
|
|
3751
|
-
post_launch_reconciled_at: nowIso,
|
|
3752
|
-
post_launch_reconciliation_reason: "terminal_session_cleanup",
|
|
3753
|
-
};
|
|
3754
|
-
await writeFileFn(rootPath, JSON.stringify(nextRoot, null, 2));
|
|
3755
4411
|
}
|
|
3756
4412
|
function buildRecoveredPostLaunchModeState(mode, completedAt) {
|
|
3757
4413
|
return {
|
|
@@ -3798,9 +4454,14 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
|
|
|
3798
4454
|
? [getStateDir(cwd, sessionId)]
|
|
3799
4455
|
: [getBaseStateDir(cwd)];
|
|
3800
4456
|
const rootStateDir = getBaseStateDir(cwd);
|
|
3801
|
-
const
|
|
3802
|
-
|
|
3803
|
-
|
|
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
|
+
};
|
|
3804
4465
|
let preserveSkillActiveForReviewPendingAutopilot = false;
|
|
3805
4466
|
for (const stateDir of scopedDirs) {
|
|
3806
4467
|
const files = await readdir(stateDir).catch(() => []);
|
|
@@ -3812,7 +4473,7 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
|
|
|
3812
4473
|
&& isAutopilotReviewPendingPostLaunchState(autopilotPrecheck.state);
|
|
3813
4474
|
preserveSkillActiveForReviewPendingAutopilot ||= preserveReviewPendingAutopilot;
|
|
3814
4475
|
for (const file of files) {
|
|
3815
|
-
if (!
|
|
4476
|
+
if (!isModeStateFilename(file))
|
|
3816
4477
|
continue;
|
|
3817
4478
|
const path = join(stateDir, file);
|
|
3818
4479
|
const mode = file.slice(0, -"-state.json".length);
|
|
@@ -3821,9 +4482,10 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
|
|
|
3821
4482
|
if (result.kind === "recoverable") {
|
|
3822
4483
|
try {
|
|
3823
4484
|
const completedAt = now().toISOString();
|
|
3824
|
-
|
|
4485
|
+
const recoveredState = mode === SKILL_ACTIVE_STATE_MODE
|
|
3825
4486
|
? buildRecoveredPostLaunchSkillActiveState(completedAt)
|
|
3826
|
-
: buildRecoveredPostLaunchModeState(mode, completedAt)
|
|
4487
|
+
: buildRecoveredPostLaunchModeState(mode, completedAt);
|
|
4488
|
+
await writeModeState(stateDir, mode, path, recoveredState);
|
|
3827
4489
|
if (isTrackedWorkflowMode(mode)) {
|
|
3828
4490
|
await syncCanonicalSkillStateForMode({
|
|
3829
4491
|
cwd,
|
|
@@ -3856,12 +4518,12 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
|
|
|
3856
4518
|
: normalizeTerminalWorkflowState(result.state, { mode, nowIso: completedAt });
|
|
3857
4519
|
if (normalized.changed) {
|
|
3858
4520
|
result.state = normalized.state;
|
|
3859
|
-
await
|
|
4521
|
+
await writeModeState(stateDir, mode, path, result.state);
|
|
3860
4522
|
}
|
|
3861
4523
|
if (mode === "ralph") {
|
|
3862
4524
|
const completedAt = now().toISOString();
|
|
3863
4525
|
if (markRalphCompletionAuditBlockedForPostLaunch(result.state, cwd, completedAt)) {
|
|
3864
|
-
await
|
|
4526
|
+
await writeModeState(stateDir, mode, path, result.state);
|
|
3865
4527
|
await syncCanonicalSkillStateForMode({
|
|
3866
4528
|
cwd,
|
|
3867
4529
|
baseStateDir: rootStateDir,
|
|
@@ -3887,7 +4549,7 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
|
|
|
3887
4549
|
result.state.phase = "complete";
|
|
3888
4550
|
result.state.updated_at = completedAt;
|
|
3889
4551
|
result.state.active_skills = [];
|
|
3890
|
-
await
|
|
4552
|
+
await writeModeState(stateDir, mode, path, result.state);
|
|
3891
4553
|
continue;
|
|
3892
4554
|
}
|
|
3893
4555
|
result.state.active = false;
|
|
@@ -3898,7 +4560,7 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
|
|
|
3898
4560
|
result.state.interrupted_at = completedAt;
|
|
3899
4561
|
result.state.stop_reason = cleanPostLaunchString(result.state.stop_reason) || "session_exit";
|
|
3900
4562
|
}
|
|
3901
|
-
await
|
|
4563
|
+
await writeModeState(stateDir, mode, path, result.state);
|
|
3902
4564
|
if (isTrackedWorkflowMode(mode)) {
|
|
3903
4565
|
await syncCanonicalSkillStateForMode({
|
|
3904
4566
|
cwd,
|
|
@@ -3920,7 +4582,7 @@ export async function cleanupPostLaunchModeStateFiles(cwd, sessionId, dependenci
|
|
|
3920
4582
|
if (sessionId) {
|
|
3921
4583
|
try {
|
|
3922
4584
|
if (!preserveSkillActiveForReviewPendingAutopilot) {
|
|
3923
|
-
await scrubPostLaunchRootSkillActiveForSession(rootStateDir, sessionId, now().toISOString(),
|
|
4585
|
+
await scrubPostLaunchRootSkillActiveForSession(rootStateDir, sessionId, now().toISOString(), writeRootState);
|
|
3924
4586
|
}
|
|
3925
4587
|
}
|
|
3926
4588
|
catch (err) {
|
|
@@ -3946,115 +4608,140 @@ export async function reapPostLaunchOrphanedMcpProcesses(dependencies = {}) {
|
|
|
3946
4608
|
writeError(`[cli/index] postLaunch MCP cleanup failed: ${err}\n`);
|
|
3947
4609
|
}
|
|
3948
4610
|
}
|
|
3949
|
-
|
|
3950
|
-
|
|
3951
|
-
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
|
|
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 = [];
|
|
3961
4643
|
try {
|
|
3962
4644
|
const cleanup = await cleanupLaunchOrphanedMcpProcesses();
|
|
3963
|
-
if (cleanup.terminatedCount > 0)
|
|
4645
|
+
if (cleanup.terminatedCount > 0)
|
|
3964
4646
|
console.log(`[omx] Reaped ${cleanup.terminatedCount} orphaned OMX MCP process(es) before launch.`);
|
|
3965
|
-
|
|
3966
|
-
if (cleanup.failedPids.length > 0) {
|
|
4647
|
+
if (cleanup.failedPids.length > 0)
|
|
3967
4648
|
console.warn(`[omx] Failed to reap ${cleanup.failedPids.length} orphaned OMX MCP process(es); continuing launch.`);
|
|
3968
|
-
}
|
|
3969
4649
|
}
|
|
3970
|
-
catch (
|
|
3971
|
-
|
|
3972
|
-
|
|
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 };
|
|
3973
4679
|
}
|
|
3974
|
-
// 2. Establish the canonical pointer before any session-scoped launch artifact.
|
|
3975
|
-
await writeSessionStart(cwd, sessionId, await resolvePreLaunchSessionPointerOptions());
|
|
3976
|
-
// 3. Generate runtime overlay + write session-scoped model instructions file
|
|
3977
|
-
const orchestrationMode = await resolveSessionOrchestrationMode(cwd, sessionId);
|
|
3978
|
-
const overlay = await generateOverlay(cwd, sessionId, { orchestrationMode });
|
|
3979
|
-
const launchAppendix = await readLaunchAppendInstructions();
|
|
3980
|
-
const dirtyWorktreeGuidance = worktreeDirty
|
|
3981
|
-
? `\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.`
|
|
3982
|
-
: "";
|
|
3983
|
-
const sessionInstructions = launchAppendix.trim().length > 0
|
|
3984
|
-
? `${overlay}
|
|
3985
|
-
|
|
3986
|
-
${launchAppendix}${dirtyWorktreeGuidance}`
|
|
3987
|
-
: `${overlay}${dirtyWorktreeGuidance}`;
|
|
3988
|
-
await writeSessionModelInstructionsFile(cwd, sessionId, sessionInstructions);
|
|
3989
|
-
// 4. Reset session metrics and tag the established session.
|
|
3990
|
-
await resetSessionMetrics(cwd, sessionId);
|
|
3991
|
-
tagCurrentTmuxSessionWithInstance(sessionId);
|
|
3992
|
-
// 5. Start notify fallback watcher (best effort)
|
|
3993
4680
|
try {
|
|
3994
4681
|
await startNotifyFallbackWatcher(cwd, { codexHomeOverride, enableAuthority: enableNotifyFallbackAuthority, sessionId });
|
|
3995
4682
|
}
|
|
3996
|
-
catch (
|
|
3997
|
-
|
|
3998
|
-
|
|
4683
|
+
catch (error) {
|
|
4684
|
+
degraded.push("notify-watcher");
|
|
4685
|
+
logCliOperationFailure(error);
|
|
3999
4686
|
}
|
|
4000
|
-
// 6. Start derived watcher (best effort, opt-in)
|
|
4001
4687
|
try {
|
|
4002
4688
|
await startHookDerivedWatcher(cwd);
|
|
4003
4689
|
}
|
|
4004
|
-
catch (
|
|
4005
|
-
|
|
4006
|
-
|
|
4690
|
+
catch (error) {
|
|
4691
|
+
degraded.push("derived-watcher");
|
|
4692
|
+
logCliOperationFailure(error);
|
|
4007
4693
|
}
|
|
4008
|
-
// 7. Emit temp notification startup summary + warnings, then send session-start lifecycle notification (best effort)
|
|
4009
4694
|
try {
|
|
4010
4695
|
if (notifyTempContract?.active) {
|
|
4011
|
-
process.env[OMX_NOTIFY_TEMP_CONTRACT_ENV] =
|
|
4012
|
-
serializeNotifyTempContract(notifyTempContract);
|
|
4696
|
+
process.env[OMX_NOTIFY_TEMP_CONTRACT_ENV] = serializeNotifyTempContract(notifyTempContract);
|
|
4013
4697
|
const { getNotificationConfig } = await import("../notifications/config.js");
|
|
4014
|
-
const
|
|
4015
|
-
const
|
|
4016
|
-
for (const info of startup.infoLines) {
|
|
4698
|
+
const startup = buildNotifyTempStartupMessages(notifyTempContract, Boolean(getNotificationConfig()?.enabled));
|
|
4699
|
+
for (const info of startup.infoLines)
|
|
4017
4700
|
console.log(`[omx] ${info}`);
|
|
4018
|
-
|
|
4019
|
-
for (const warning of startup.warningLines) {
|
|
4701
|
+
for (const warning of startup.warningLines)
|
|
4020
4702
|
console.warn(`[omx] ${warning}`);
|
|
4021
|
-
}
|
|
4022
4703
|
}
|
|
4023
|
-
else
|
|
4704
|
+
else
|
|
4024
4705
|
delete process.env[OMX_NOTIFY_TEMP_CONTRACT_ENV];
|
|
4025
|
-
}
|
|
4026
4706
|
const { notifyLifecycle } = await import("../notifications/index.js");
|
|
4027
|
-
await notifyLifecycle("session-start", {
|
|
4028
|
-
sessionId,
|
|
4029
|
-
projectPath: cwd,
|
|
4030
|
-
projectName: basename(cwd),
|
|
4031
|
-
});
|
|
4707
|
+
await notifyLifecycle("session-start", { sessionId, projectPath: cwd, projectName: basename(cwd) });
|
|
4032
4708
|
}
|
|
4033
|
-
catch (
|
|
4034
|
-
|
|
4035
|
-
|
|
4709
|
+
catch (error) {
|
|
4710
|
+
degraded.push("notification");
|
|
4711
|
+
logCliOperationFailure(error);
|
|
4036
4712
|
}
|
|
4037
|
-
// 8. Dispatch native hook event (best effort)
|
|
4038
4713
|
try {
|
|
4039
|
-
await emitNativeHookEvent(cwd, "session-start", {
|
|
4040
|
-
session_id: sessionId,
|
|
4041
|
-
context: buildNativeHookBaseContext(cwd, sessionId, "started", {
|
|
4042
|
-
project_path: cwd,
|
|
4043
|
-
project_name: basename(cwd),
|
|
4044
|
-
status: "started",
|
|
4045
|
-
}),
|
|
4046
|
-
});
|
|
4714
|
+
await emitNativeHookEvent(cwd, "session-start", { session_id: sessionId, context: buildNativeHookBaseContext(cwd, sessionId, "started", { project_path: cwd, project_name: basename(cwd), status: "started" }) });
|
|
4047
4715
|
}
|
|
4048
|
-
catch (
|
|
4049
|
-
|
|
4050
|
-
|
|
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;
|
|
4051
4736
|
}
|
|
4737
|
+
return { binding: established.binding, completion };
|
|
4052
4738
|
}
|
|
4053
4739
|
/**
|
|
4054
4740
|
* runCodex: Launch Codex CLI (blocks until exit).
|
|
4055
4741
|
* All 3 paths (new tmux, existing tmux, no tmux) block via execSync/execFileSync.
|
|
4056
4742
|
*/
|
|
4057
|
-
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;
|
|
4058
4745
|
const launchArgs = injectModelInstructionsBypassArgs(cwd, args, process.env, sessionModelInstructionsPath(cwd, sessionId));
|
|
4059
4746
|
const nativeWindows = isNativeWindows();
|
|
4060
4747
|
const omxBin = resolveOmxCliEntryPath({ argv1: process.argv[1], cwd, env: process.env });
|
|
@@ -4062,19 +4749,59 @@ function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, s
|
|
|
4062
4749
|
throw new Error("Unable to resolve OMX launcher path for tmux HUD bootstrap");
|
|
4063
4750
|
}
|
|
4064
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;
|
|
4065
4787
|
const omxRootOverride = runtimeContext?.omxRoot ?? resolveOmxRootForLaunch(cwd, process.env);
|
|
4788
|
+
const selectedOmxRootOverride = launchOwnedControlPlane
|
|
4789
|
+
? launchOwnedControlPlane.values.OMX_ROOT || undefined
|
|
4790
|
+
: omxRootOverride;
|
|
4066
4791
|
const currentPaneId = process.env.TMUX_PANE;
|
|
4067
4792
|
const hudRuntimeRoot = runtimeContext
|
|
4068
4793
|
? { omxRoot: runtimeContext.omxRoot, rootSource: 'omx-root-env' }
|
|
4069
4794
|
: resolveHudRuntimeRootForLaunch(cwd, process.env);
|
|
4070
|
-
const hudRuntimeEnv =
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
|
|
4075
|
-
|
|
4076
|
-
|
|
4077
|
-
|
|
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
|
+
};
|
|
4078
4805
|
const hudEnvArgs = Object.entries(hudRuntimeEnv).map(([key, value]) => `${key}=${value}`);
|
|
4079
4806
|
const hudCmd = nativeWindows
|
|
4080
4807
|
? buildWindowsPromptCommand("node", [omxBin, "hud", "--watch"])
|
|
@@ -4089,13 +4816,17 @@ function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, s
|
|
|
4089
4816
|
...stripHermesMcpBridgeEnv(process.env),
|
|
4090
4817
|
...(codexHomeOverride ? { CODEX_HOME: codexHomeOverride } : {}),
|
|
4091
4818
|
...(sqliteHomeOverride ? { [CODEX_SQLITE_HOME_ENV]: sqliteHomeOverride } : {}),
|
|
4092
|
-
...(omxRootOverride ? { OMX_ROOT: omxRootOverride } : {}),
|
|
4819
|
+
...(!launchOwnedControlPlane && omxRootOverride ? { OMX_ROOT: omxRootOverride } : {}),
|
|
4093
4820
|
...runtimeEnvOverlay,
|
|
4821
|
+
...launchOwnedControlPlaneEnv,
|
|
4094
4822
|
}, omxBin);
|
|
4823
|
+
// Correlates plugin hook routing only; it is not an authority token.
|
|
4824
|
+
const pluginHookRoutingLaunchId = randomUUID();
|
|
4095
4825
|
const codexEnvWithSession = {
|
|
4096
4826
|
...codexBaseEnv,
|
|
4097
|
-
OMX_CODEX_LAUNCH_ID:
|
|
4827
|
+
OMX_CODEX_LAUNCH_ID: pluginHookRoutingLaunchId,
|
|
4098
4828
|
...buildHudRuntimeEnv({ sessionId }).env,
|
|
4829
|
+
...launchOwnedControlPlaneEnv,
|
|
4099
4830
|
};
|
|
4100
4831
|
const codexEnv = workerLaunchArgs
|
|
4101
4832
|
? {
|
|
@@ -4107,8 +4838,9 @@ function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, s
|
|
|
4107
4838
|
const codexEnvWithNotify = notifyTempContractRaw
|
|
4108
4839
|
? { ...codexEnv, [OMX_NOTIFY_TEMP_CONTRACT_ENV]: notifyTempContractRaw }
|
|
4109
4840
|
: codexEnv;
|
|
4110
|
-
const runtimeHookEnv =
|
|
4111
|
-
|
|
4841
|
+
const runtimeHookEnv = launchOwnedControlPlane
|
|
4842
|
+
? { ...process.env, ...runtimeEnvOverlay, ...launchOwnedControlPlaneEnv }
|
|
4843
|
+
: { ...process.env, ...runtimeEnvOverlay };
|
|
4112
4844
|
if (isCodexVersionRequest(launchArgs)) {
|
|
4113
4845
|
runCodexBlocking(cwd, launchArgs, codexEnvWithNotify);
|
|
4114
4846
|
return { postLaunchHandledExternally: false };
|
|
@@ -4144,7 +4876,7 @@ function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, s
|
|
|
4144
4876
|
currentPaneId,
|
|
4145
4877
|
cwd,
|
|
4146
4878
|
sessionId,
|
|
4147
|
-
omxRootOverride,
|
|
4879
|
+
omxRootOverride: selectedOmxRootOverride,
|
|
4148
4880
|
baseEnv: runtimeHookEnv,
|
|
4149
4881
|
});
|
|
4150
4882
|
}
|
|
@@ -4171,7 +4903,7 @@ function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, s
|
|
|
4171
4903
|
currentPaneId,
|
|
4172
4904
|
cwd,
|
|
4173
4905
|
sessionId,
|
|
4174
|
-
omxRootOverride,
|
|
4906
|
+
omxRootOverride: selectedOmxRootOverride,
|
|
4175
4907
|
baseEnv: runtimeHookEnv,
|
|
4176
4908
|
});
|
|
4177
4909
|
}
|
|
@@ -4232,214 +4964,247 @@ function runCodex(cwd, args, sessionId, workerDefaultModel, codexHomeOverride, s
|
|
|
4232
4964
|
return { postLaunchHandledExternally: false };
|
|
4233
4965
|
}
|
|
4234
4966
|
else {
|
|
4235
|
-
|
|
4967
|
+
if (!detachedPreflight) {
|
|
4968
|
+
throw new DetachedLaunchSafetyError("establishment", new Error("detached launch requires a frozen available preflight"), { transitions: ["D0"], rollback: { attempted: [], failures: [] } });
|
|
4969
|
+
}
|
|
4236
4970
|
const codexCmd = buildTmuxPaneCommand("codex", launchArgs);
|
|
4237
4971
|
const detachedWindowsCodexCmd = nativeWindows
|
|
4238
|
-
?
|
|
4972
|
+
? buildWindowsDetachedChildCommand("codex", launchArgs)
|
|
4239
4973
|
: null;
|
|
4240
4974
|
const sessionName = buildDetachedTmuxSessionName(cwd, sessionId);
|
|
4241
|
-
const
|
|
4242
|
-
|
|
4243
|
-
|
|
4244
|
-
|
|
4245
|
-
|
|
4246
|
-
|
|
4247
|
-
|
|
4248
|
-
|
|
4249
|
-
|
|
4250
|
-
|
|
4251
|
-
|
|
4252
|
-
|
|
4253
|
-
|
|
4254
|
-
|
|
4255
|
-
if (!
|
|
4256
|
-
clearDetachedTmuxSessionHistoryIfUnattached(activeRecord.tmux_session_name, activeRecord.tmux_pane_id);
|
|
4257
|
-
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`);
|
|
4258
|
-
return { postLaunchHandledExternally: true };
|
|
4259
|
-
}
|
|
4260
|
-
process.stderr.write(`[omx] madmax detached launch already active for this context; attaching ${activeRecord.tmux_session_name} instead of starting a duplicate.\n`);
|
|
4261
|
-
try {
|
|
4262
|
-
execTmuxFileSync(["attach-session", "-t", activeRecord.tmux_session_name], {
|
|
4263
|
-
stdio: "inherit",
|
|
4264
|
-
});
|
|
4265
|
-
}
|
|
4266
|
-
catch (err) {
|
|
4267
|
-
logCliOperationFailure(err);
|
|
4268
|
-
throw new MadmaxDetachedReuseError(`refusing duplicate madmax detached launch: existing session ${activeRecord.tmux_session_name} is active but attach failed`);
|
|
4269
|
-
}
|
|
4270
|
-
return { postLaunchHandledExternally: true };
|
|
4271
|
-
}
|
|
4272
|
-
if (activeRecordPath && activeRecord) {
|
|
4273
|
-
rmSync(activeRecordPath, { force: true });
|
|
4274
|
-
}
|
|
4275
|
-
let detachedSessionBindingWrite = Promise.resolve();
|
|
4276
|
-
const writeDetachedSessionBinding = (tmuxPaneId) => {
|
|
4277
|
-
detachedSessionBindingWrite = detachedSessionBindingWrite
|
|
4278
|
-
.catch((err) => {
|
|
4279
|
-
logCliOperationFailure(err);
|
|
4280
|
-
})
|
|
4281
|
-
.then(() => writeSessionStart(cwd, sessionId, {
|
|
4282
|
-
tmuxSessionName: sessionName,
|
|
4283
|
-
...(tmuxPaneId ? { tmuxPaneId } : {}),
|
|
4284
|
-
}));
|
|
4285
|
-
void detachedSessionBindingWrite.catch((err) => {
|
|
4286
|
-
logCliOperationFailure(err);
|
|
4287
|
-
// Non-fatal: managed tmux recovery can still use compatibility fallback.
|
|
4288
|
-
});
|
|
4289
|
-
};
|
|
4290
|
-
writeDetachedSessionBinding();
|
|
4291
|
-
let createdDetachedSession = false;
|
|
4292
|
-
let registeredHookTarget = null;
|
|
4293
|
-
let registeredHookName = null;
|
|
4294
|
-
let registeredClientAttachedHookName = null;
|
|
4295
|
-
let detachedParentEnvFilePath;
|
|
4296
|
-
let detachedLeaderPaneId = null;
|
|
4297
|
-
try {
|
|
4298
|
-
// This path is the user-shell interactive launch: OMX creates a tmux
|
|
4299
|
-
// session and immediately attaches the user's terminal to it. If a tmux
|
|
4300
|
-
// server already exists, `new-session -e` only forwards explicit values,
|
|
4301
|
-
// so provider-specific parent-shell keys would disappear. Source a
|
|
4302
|
-
// private env file inside the leader shell instead of putting every
|
|
4303
|
-
// parent env value on the tmux command line or in logs.
|
|
4304
|
-
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)
|
|
4305
4990
|
detachedParentEnvFilePath = writeDetachedSessionParentEnvFile(cwd, sessionId, codexEnvWithNotify);
|
|
4306
|
-
|
|
4307
|
-
|
|
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);
|
|
4308
4997
|
for (const step of bootstrapSteps) {
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
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");
|
|
4317
5005
|
detachedLeaderPaneId = leaderPaneId;
|
|
4318
|
-
|
|
4319
|
-
|
|
4320
|
-
writeMadmaxDetachedActiveRecord(activeRecordPath, {
|
|
4321
|
-
version: 1,
|
|
4322
|
-
context_key: contextKey,
|
|
4323
|
-
created_at: new Date().toISOString(),
|
|
4324
|
-
source_cwd: runtimeContext?.sourceCwd ?? process.env.OMX_SOURCE_CWD ?? cwd,
|
|
4325
|
-
...(runtimeContext?.worktreeCwd ? { worktree_cwd: runtimeContext.worktreeCwd } : {}),
|
|
4326
|
-
argv: args,
|
|
4327
|
-
run_dir: runtimeContext?.omxRoot ?? process.env.OMX_ROOT ?? cwd,
|
|
4328
|
-
tmux_session_name: sessionName,
|
|
4329
|
-
session_id: sessionId,
|
|
4330
|
-
tmux_pane_id: leaderPaneId,
|
|
4331
|
-
});
|
|
4332
|
-
}
|
|
4333
|
-
writeDetachedSessionBinding(leaderPaneId);
|
|
5006
|
+
detachedLeaderAuthority = captureDetachedLeaderAuthority(leaderPaneId, sessionName, sessionId);
|
|
5007
|
+
continue;
|
|
4334
5008
|
}
|
|
4335
|
-
|
|
4336
|
-
|
|
4337
|
-
|
|
4338
|
-
|
|
4339
|
-
|
|
4340
|
-
|
|
4341
|
-
|
|
4342
|
-
|
|
4343
|
-
|
|
4344
|
-
|
|
4345
|
-
|
|
4346
|
-
|
|
4347
|
-
const clientAttachedHookName = hudPaneId && hookWindowIndex
|
|
4348
|
-
? buildClientAttachedReconcileHookName("launch", sessionName, hookWindowIndex, hudPaneId)
|
|
4349
|
-
: null;
|
|
4350
|
-
const finalizeSteps = buildDetachedSessionFinalizeSteps(sessionName, hudPaneId, hookWindowIndex, process.env.OMX_MOUSE !== "0", nativeWindows, shouldAttachDetachedTmuxSession(process.env), detachedLeaderPaneId);
|
|
4351
|
-
if (nativeWindows && detachedWindowsCodexCmd) {
|
|
4352
|
-
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;
|
|
4353
5021
|
}
|
|
4354
|
-
|
|
4355
|
-
|
|
4356
|
-
|
|
4357
|
-
|
|
4358
|
-
|
|
4359
|
-
|
|
4360
|
-
|
|
4361
|
-
|
|
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;
|
|
4362
5030
|
continue;
|
|
4363
5031
|
}
|
|
4364
|
-
|
|
4365
|
-
try {
|
|
4366
|
-
const startedAtMs = Date.now();
|
|
4367
|
-
execTmuxFileSync(finalizeStep.args, { stdio });
|
|
4368
|
-
if (finalizeStep.name === "attach-session") {
|
|
4369
|
-
assertDetachedAttachDidNotNoop(sessionName, Date.now() - startedAtMs, process.env);
|
|
4370
|
-
}
|
|
4371
|
-
}
|
|
4372
|
-
catch (err) {
|
|
4373
|
-
logCliOperationFailure(err);
|
|
4374
|
-
if (finalizeStep.name === "attach-session")
|
|
4375
|
-
throw new Error("failed to attach detached tmux session");
|
|
5032
|
+
if (finalizeStep.name === "sanitize-copy-mode-style")
|
|
4376
5033
|
continue;
|
|
4377
|
-
|
|
4378
|
-
|
|
4379
|
-
|
|
4380
|
-
|
|
4381
|
-
|
|
4382
|
-
|
|
4383
|
-
|
|
4384
|
-
|
|
4385
|
-
|
|
4386
|
-
|
|
4387
|
-
}
|
|
4388
|
-
if (finalizeStep.name === "reconcile-hud-resize") {
|
|
4389
|
-
registerDetachedHudLayoutReconcileHook({
|
|
4390
|
-
hudPaneId,
|
|
4391
|
-
detachedLeaderPaneId,
|
|
4392
|
-
cwd,
|
|
4393
|
-
sessionId,
|
|
4394
|
-
omxBin,
|
|
4395
|
-
omxRootOverride,
|
|
4396
|
-
baseEnv: runtimeHookEnv,
|
|
4397
|
-
});
|
|
4398
|
-
}
|
|
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);
|
|
4399
5044
|
}
|
|
4400
5045
|
}
|
|
5046
|
+
catch (error) {
|
|
5047
|
+
throw new DetachedLaunchSafetyError(step.name === "new-session" ? "inert-session" : "pane-id", error, detachedPreflight.report);
|
|
5048
|
+
}
|
|
4401
5049
|
}
|
|
4402
|
-
return
|
|
4403
|
-
}
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
|
|
4407
|
-
|
|
4408
|
-
|
|
4409
|
-
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
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);
|
|
4414
5164
|
try {
|
|
4415
|
-
|
|
5165
|
+
await operation();
|
|
4416
5166
|
}
|
|
4417
|
-
catch (
|
|
4418
|
-
|
|
4419
|
-
|
|
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
|
+
});
|
|
4420
5186
|
}
|
|
4421
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;
|
|
4422
5200
|
}
|
|
4423
|
-
throw err;
|
|
4424
5201
|
}
|
|
5202
|
+
return { postLaunchHandledExternally: true };
|
|
4425
5203
|
};
|
|
4426
|
-
|
|
4427
|
-
|
|
4428
|
-
try {
|
|
4429
|
-
if (isMadmaxDetachedGuardEnabled(process.env) && contextKey) {
|
|
4430
|
-
return withMadmaxDetachedContextLock(runsRoot, contextKey, launchDetachedSession);
|
|
4431
|
-
}
|
|
4432
|
-
return launchDetachedSession();
|
|
4433
|
-
}
|
|
4434
|
-
catch (err) {
|
|
4435
|
-
if (err instanceof MadmaxDetachedReuseError || err instanceof MadmaxDetachedGuardError) {
|
|
4436
|
-
throw err;
|
|
4437
|
-
}
|
|
4438
|
-
logCliOperationFailure(err);
|
|
4439
|
-
// tmux not available or failed, just run codex directly
|
|
4440
|
-
runCodexBlocking(cwd, launchArgs, codexEnvWithNotify);
|
|
4441
|
-
return { postLaunchHandledExternally: false };
|
|
5204
|
+
if (contextKey) {
|
|
5205
|
+
return await withMadmaxDetachedContextLock(runsRoot, contextKey, launchDetachedSession);
|
|
4442
5206
|
}
|
|
5207
|
+
return await launchDetachedSession();
|
|
4443
5208
|
}
|
|
4444
5209
|
}
|
|
4445
5210
|
function listHudWatchPaneIdsInCurrentWindow(currentPaneId, owner = {}) {
|
|
@@ -4486,7 +5251,24 @@ export function isCodexVersionRequest(args) {
|
|
|
4486
5251
|
const { omxArgs } = splitOmxArgsAtEndOfOptions(args);
|
|
4487
5252
|
return omxArgs.some((arg) => CODEX_VERSION_FLAGS.has(arg));
|
|
4488
5253
|
}
|
|
4489
|
-
export function buildWindowsPromptCommand(command, args) {
|
|
5254
|
+
export function buildWindowsPromptCommand(command, args) {
|
|
5255
|
+
const invocation = [
|
|
5256
|
+
"&",
|
|
5257
|
+
quotePowerShellArg(command),
|
|
5258
|
+
...args.map(quotePowerShellArg),
|
|
5259
|
+
].join(" ");
|
|
5260
|
+
const wrappedCommand = [
|
|
5261
|
+
"$ErrorActionPreference = 'Stop'",
|
|
5262
|
+
`& { ${invocation} }`,
|
|
5263
|
+
].join("; ");
|
|
5264
|
+
return `powershell.exe -NoLogo -NoExit -EncodedCommand ${encodePowerShellCommand(wrappedCommand)}`;
|
|
5265
|
+
}
|
|
5266
|
+
/**
|
|
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) {
|
|
4490
5272
|
const invocation = [
|
|
4491
5273
|
"&",
|
|
4492
5274
|
quotePowerShellArg(command),
|
|
@@ -4494,9 +5276,10 @@ export function buildWindowsPromptCommand(command, args) {
|
|
|
4494
5276
|
].join(" ");
|
|
4495
5277
|
const wrappedCommand = [
|
|
4496
5278
|
"$ErrorActionPreference = 'Stop'",
|
|
4497
|
-
|
|
5279
|
+
invocation,
|
|
5280
|
+
"exit $LASTEXITCODE",
|
|
4498
5281
|
].join("; ");
|
|
4499
|
-
return `powershell.exe -NoLogo -
|
|
5282
|
+
return `powershell.exe -NoLogo -NoProfile -NonInteractive -EncodedCommand ${encodePowerShellCommand(wrappedCommand)}`;
|
|
4500
5283
|
}
|
|
4501
5284
|
/**
|
|
4502
5285
|
* Wrap a command for tmux pane execution while preserving the tmux pane cwd.
|
|
@@ -4534,45 +5317,323 @@ function quoteShellArg(value) {
|
|
|
4534
5317
|
function quotePowerShellArg(value) {
|
|
4535
5318
|
return `'${value.replace(/'/g, "''")}'`;
|
|
4536
5319
|
}
|
|
4537
|
-
export function
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
|
|
4541
|
-
|
|
4542
|
-
|
|
4543
|
-
|
|
4544
|
-
|
|
4545
|
-
|
|
4546
|
-
|
|
4547
|
-
|
|
4548
|
-
|
|
4549
|
-
|
|
4550
|
-
|
|
4551
|
-
|
|
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
|
+
};
|
|
4552
5362
|
}
|
|
4553
|
-
function
|
|
4554
|
-
|
|
4555
|
-
|
|
4556
|
-
|
|
4557
|
-
|
|
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,
|
|
4558
5431
|
});
|
|
4559
|
-
|
|
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
|
+
}
|
|
4560
5610
|
}
|
|
4561
5611
|
/**
|
|
4562
5612
|
* postLaunch: Clean up after Codex exits.
|
|
4563
5613
|
* Each step is independently fault-tolerant (try/catch per step).
|
|
4564
5614
|
*/
|
|
4565
|
-
export async function postLaunch(cwd, sessionId, codexHomeOverride, enableNotifyFallbackAuthority = false, projectLocalCodexHomeForCleanup) {
|
|
4566
|
-
|
|
4567
|
-
|
|
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;
|
|
4568
5620
|
try {
|
|
4569
|
-
const
|
|
4570
|
-
|
|
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"}`);
|
|
4571
5627
|
}
|
|
4572
|
-
catch (
|
|
4573
|
-
|
|
4574
|
-
// Non-fatal
|
|
5628
|
+
catch (error) {
|
|
5629
|
+
terminalError = error;
|
|
4575
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?.();
|
|
4576
5637
|
// 0. Reap MCP orphans left behind by the session that just exited.
|
|
4577
5638
|
await reapPostLaunchOrphanedMcpProcesses();
|
|
4578
5639
|
// 0. Flush fallback watcher once to reduce race with fast codex exit.
|
|
@@ -4623,13 +5684,6 @@ export async function postLaunch(cwd, sessionId, codexHomeOverride, enableNotify
|
|
|
4623
5684
|
catch (err) {
|
|
4624
5685
|
console.error(`[omx] postLaunch: model instructions cleanup failed: ${err instanceof Error ? err.message : err}`);
|
|
4625
5686
|
}
|
|
4626
|
-
// 2. Archive session (write history, delete session.json)
|
|
4627
|
-
try {
|
|
4628
|
-
await writeSessionEnd(cwd, sessionId);
|
|
4629
|
-
}
|
|
4630
|
-
catch (err) {
|
|
4631
|
-
console.error(`[omx] postLaunch: session archive failed: ${err instanceof Error ? err.message : err}`);
|
|
4632
|
-
}
|
|
4633
5687
|
// 2.5. Best-effort wiki session capture
|
|
4634
5688
|
try {
|
|
4635
5689
|
const { onSessionEnd } = await import("../wiki/lifecycle.js");
|
|
@@ -4701,10 +5755,59 @@ export async function postLaunch(cwd, sessionId, codexHomeOverride, enableNotify
|
|
|
4701
5755
|
logCliOperationFailure(err);
|
|
4702
5756
|
// Non-fatal
|
|
4703
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
|
+
}
|
|
4704
5762
|
}
|
|
4705
|
-
export async function runDetachedSessionPostLaunch(cwd, sessionId,
|
|
4706
|
-
|
|
4707
|
-
|
|
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");
|
|
4708
5811
|
}
|
|
4709
5812
|
async function emitNativeHookEvent(cwd, event, opts = {}) {
|
|
4710
5813
|
const payload = buildHookEvent(event, {
|
|
@@ -5143,14 +6246,13 @@ async function listHookVisibleRunDirStateRefs(cwd) {
|
|
|
5143
6246
|
if (!sourceCwd || !runDir)
|
|
5144
6247
|
return;
|
|
5145
6248
|
try {
|
|
5146
|
-
if (canonicalizePathForRunDirMatch(sourceCwd) !== canonicalCwd
|
|
5147
|
-
(!worktreeCwd || canonicalizePathForRunDirMatch(worktreeCwd) !== canonicalCwd))
|
|
6249
|
+
if (canonicalizePathForRunDirMatch(sourceCwd) !== canonicalCwd
|
|
6250
|
+
&& (!worktreeCwd || canonicalizePathForRunDirMatch(worktreeCwd) !== canonicalCwd))
|
|
5148
6251
|
return;
|
|
5149
6252
|
const resolvedRunDir = resolve(runDir);
|
|
5150
6253
|
if (resolvedRunDir !== canonicalRunsRoot
|
|
5151
|
-
&& !resolvedRunDir.startsWith(`${canonicalRunsRoot}/`))
|
|
6254
|
+
&& !resolvedRunDir.startsWith(`${canonicalRunsRoot}/`))
|
|
5152
6255
|
return;
|
|
5153
|
-
}
|
|
5154
6256
|
runDirs.add(resolvedRunDir);
|
|
5155
6257
|
}
|
|
5156
6258
|
catch {
|
|
@@ -5203,7 +6305,7 @@ async function listHookVisibleRunDirStateRefs(cwd) {
|
|
|
5203
6305
|
for (const dir of candidateDirs) {
|
|
5204
6306
|
const files = await readdir(dir).catch(() => []);
|
|
5205
6307
|
for (const file of files) {
|
|
5206
|
-
if (!
|
|
6308
|
+
if (!isModeStateFilename(file))
|
|
5207
6309
|
continue;
|
|
5208
6310
|
const path = join(dir, file);
|
|
5209
6311
|
if (seenPaths.has(path))
|
|
@@ -5219,41 +6321,362 @@ async function listHookVisibleRunDirStateRefs(cwd) {
|
|
|
5219
6321
|
}
|
|
5220
6322
|
return refs.sort((a, b) => a.mode.localeCompare(b.mode));
|
|
5221
6323
|
}
|
|
5222
|
-
|
|
5223
|
-
const
|
|
5224
|
-
|
|
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();
|
|
5225
6552
|
const nowIso = new Date().toISOString();
|
|
5226
|
-
|
|
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;
|
|
5227
6560
|
try {
|
|
5228
6561
|
const writableScope = await resolveWritableStateScope(cwd);
|
|
5229
|
-
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) => {
|
|
5230
6568
|
const loaded = new Map();
|
|
6569
|
+
if (refs.length === 0)
|
|
6570
|
+
return loaded;
|
|
6571
|
+
const canonicalAuthorityRoot = assertCancellationAuthorityPath(authorityRoot === writableScope.stateDir ? baseStateDir : authorityRoot, authorityRoot);
|
|
5231
6572
|
for (const ref of refs) {
|
|
5232
|
-
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");
|
|
5233
6584
|
let parsedState;
|
|
5234
6585
|
try {
|
|
5235
|
-
|
|
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;
|
|
5236
6591
|
}
|
|
5237
6592
|
catch (err) {
|
|
5238
6593
|
logCliOperationFailure(err);
|
|
5239
|
-
|
|
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
|
+
}
|
|
5240
6638
|
}
|
|
5241
6639
|
loaded.set(ref.mode, {
|
|
5242
|
-
path:
|
|
6640
|
+
path: canonicalPath,
|
|
5243
6641
|
scope: ref.scope,
|
|
5244
6642
|
state: parsedState,
|
|
6643
|
+
originalContent: content,
|
|
6644
|
+
dev: fileStat.dev,
|
|
6645
|
+
ino: fileStat.ino,
|
|
5245
6646
|
});
|
|
5246
6647
|
}
|
|
5247
6648
|
return loaded;
|
|
5248
6649
|
};
|
|
5249
|
-
|
|
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);
|
|
5250
6665
|
const hasActiveWorkflowMode = (entries) => [...entries.entries()].some(([mode, entry]) => mode !== SKILL_ACTIVE_STATE_MODE && entry.state.active === true);
|
|
5251
|
-
if (
|
|
5252
|
-
|
|
5253
|
-
|
|
5254
|
-
|
|
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
|
+
}
|
|
5255
6678
|
}
|
|
5256
|
-
const currentSessionId = writableScope.sessionId ?? "";
|
|
6679
|
+
const currentSessionId = writableScope.sessionId ?? runSelection?.sessionId ?? "";
|
|
5257
6680
|
const changed = new Set();
|
|
5258
6681
|
const reported = new Set();
|
|
5259
6682
|
const cancelMode = (mode, phase = "cancelled", reportIfWasActive = true) => {
|
|
@@ -5284,13 +6707,74 @@ async function cancelModes(args = []) {
|
|
|
5284
6707
|
if (reportIfWasActive && wasActive && mode !== SKILL_ACTIVE_STATE_MODE)
|
|
5285
6708
|
reported.add(mode);
|
|
5286
6709
|
};
|
|
5287
|
-
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
|
+
};
|
|
5288
6770
|
const ralph = states.get("ralph");
|
|
5289
6771
|
const hadActiveRalph = !!(ralph && ralph.state.active === true);
|
|
5290
6772
|
if (ralph && ralph.state.active === true) {
|
|
6773
|
+
const linkedMode = linkedRalphMode(ralph.state);
|
|
6774
|
+
if (linkedMode)
|
|
6775
|
+
cancelMode(linkedMode, "cancelled", true);
|
|
5291
6776
|
cancelMode("ralph", "cancelled", true);
|
|
5292
|
-
|
|
5293
|
-
cancelMode("ultrawork", "cancelled", true);
|
|
6777
|
+
cancelRalphSkillEntries(linkedMode);
|
|
5294
6778
|
}
|
|
5295
6779
|
if (!hadActiveRalph) {
|
|
5296
6780
|
for (const [mode, entry] of states.entries()) {
|
|
@@ -5298,11 +6782,11 @@ async function cancelModes(args = []) {
|
|
|
5298
6782
|
cancelMode(mode, "cancelled", true);
|
|
5299
6783
|
}
|
|
5300
6784
|
}
|
|
5301
|
-
|
|
5302
|
-
if (!
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
}
|
|
6785
|
+
const assertRunAuthority = () => {
|
|
6786
|
+
if (runSelection && !hasMatchingMadmaxDetachedRuntimeBinding(runSelection.record)) {
|
|
6787
|
+
throw new Error("Refusing cancellation because detached run authority changed.");
|
|
6788
|
+
}
|
|
6789
|
+
};
|
|
5306
6790
|
if (force && currentSessionId) {
|
|
5307
6791
|
const stopStateEntries = [...states.entries()].filter(([mode]) => mode === "native-stop");
|
|
5308
6792
|
for (const [, entry] of stopStateEntries) {
|
|
@@ -5313,10 +6797,85 @@ async function cancelModes(args = []) {
|
|
|
5313
6797
|
continue;
|
|
5314
6798
|
delete sessions[currentSessionId];
|
|
5315
6799
|
entry.state.sessions = sessions;
|
|
5316
|
-
await writeFile(entry.path, JSON.stringify(entry.state, null, 2));
|
|
5317
6800
|
changed.add("native-stop");
|
|
5318
6801
|
}
|
|
5319
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
|
+
}
|
|
5320
6879
|
for (const mode of reported) {
|
|
5321
6880
|
console.log(`Cancelled: ${mode}`);
|
|
5322
6881
|
}
|
|
@@ -5326,7 +6885,11 @@ async function cancelModes(args = []) {
|
|
|
5326
6885
|
}
|
|
5327
6886
|
catch (err) {
|
|
5328
6887
|
logCliOperationFailure(err);
|
|
5329
|
-
|
|
6888
|
+
process.exitCode = 1;
|
|
5330
6889
|
}
|
|
5331
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
|
+
}
|
|
5332
6895
|
//# sourceMappingURL=index.js.map
|