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
|
@@ -1,15 +1,17 @@
|
|
|
1
1
|
import { describe, it } from 'node:test';
|
|
2
2
|
import assert from 'node:assert/strict';
|
|
3
|
-
import { existsSync } from 'node:fs';
|
|
4
|
-
import { chmod, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from 'node:fs/promises';
|
|
3
|
+
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { chmod, mkdir, mkdtemp, readFile, readdir, rename, rm, writeFile } from 'node:fs/promises';
|
|
5
5
|
import { execFileSync, spawn, spawnSync } from 'node:child_process';
|
|
6
6
|
import { tmpdir } from 'node:os';
|
|
7
7
|
import { basename, dirname, join } from 'node:path';
|
|
8
8
|
import { fileURLToPath } from 'node:url';
|
|
9
|
-
import { HUD_TMUX_HEIGHT_LINES } from '../../hud/constants.js';
|
|
10
|
-
import { DETACHED_TMUX_HISTORY_LIMIT } from '../index.js';
|
|
11
|
-
import {
|
|
9
|
+
import { HUD_RESIZE_RECONCILE_DELAY_SECONDS, HUD_TMUX_HEIGHT_LINES } from '../../hud/constants.js';
|
|
10
|
+
import { DETACHED_LAUNCH_CONTROL_PLANE_KEYS, DETACHED_TMUX_HISTORY_LIMIT, buildMadmaxDetachedLaunchContextKey } from '../index.js';
|
|
11
|
+
import { closeLaunchSessionBindingOnce, establishLaunchSessionBinding, finalizeBoundOnce, writeSessionStart, } from '../../hooks/session.js';
|
|
12
|
+
import { buildPtyScriptCommand, isRealScriptAvailable, isRealTmuxAvailable, withTempTmuxSession } from '../../team/__tests__/tmux-test-fixture.js';
|
|
12
13
|
const CLI_SPAWN_TIMEOUT_MS = 60_000;
|
|
14
|
+
const DARWIN_PTY_FINALIZATION_POLL_LIMIT = 600;
|
|
13
15
|
function buildRunOmxEnv(envOverrides) {
|
|
14
16
|
const env = { ...process.env };
|
|
15
17
|
for (const key of Object.keys(env)) {
|
|
@@ -51,8 +53,10 @@ function normalizeDarwinTmpPath(value) {
|
|
|
51
53
|
function escapeRegExp(value) {
|
|
52
54
|
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
53
55
|
}
|
|
54
|
-
function shouldSkipForSpawnPermissions(
|
|
55
|
-
|
|
56
|
+
function shouldSkipForSpawnPermissions(_err) {
|
|
57
|
+
// These integration fixtures are contractual: EPERM/EACCES is a failure,
|
|
58
|
+
// not a pass. Node's test runner has no portable dynamic skip here.
|
|
59
|
+
return false;
|
|
56
60
|
}
|
|
57
61
|
async function createGitRepo(wd) {
|
|
58
62
|
const repo = join(wd, 'repo');
|
|
@@ -69,17 +73,167 @@ async function writeExecutable(path, content) {
|
|
|
69
73
|
await writeFile(path, content);
|
|
70
74
|
await chmod(path, 0o755);
|
|
71
75
|
}
|
|
76
|
+
async function wrapFakeTmuxWithDetachedLeader(fakeTmuxPath) {
|
|
77
|
+
const implementationPath = `${fakeTmuxPath}.impl`;
|
|
78
|
+
await rename(fakeTmuxPath, implementationPath);
|
|
79
|
+
await writeExecutable(fakeTmuxPath, `#!/bin/sh
|
|
80
|
+
if [ "$1" = "display-message" ] && [ "$2" = "-p" ] && [ "$3" = "-t" ] && case "$5" in *'#{session_created}'*'#{window_id}'*'#{pane_pid}'*) true;; *) false;; esac; then
|
|
81
|
+
session=$(cat /tmp/omx-test-detached-session-name 2>/dev/null || printf omx-test)
|
|
82
|
+
leader_pid=$(cat "$0.detached-leader-pid" 2>/dev/null || printf 123)
|
|
83
|
+
printf '%s\t$1\t1\t0\t@1\t%s\t%s\n' "$session" "$4" "$leader_pid"
|
|
84
|
+
exit 0
|
|
85
|
+
fi
|
|
86
|
+
if [ "$1" = "set-option" ] && [ "$2" = "-t" ] && [ "$4" = "@omx_instance_id" ]; then
|
|
87
|
+
printf '%s' "$5" > /tmp/omx-test-detached-owner-id
|
|
88
|
+
fi
|
|
89
|
+
if [ "$1" = "list-panes" ] && case "$*" in *'#{session_created}'*'#{@omx_instance_id}'*) true;; *) false;; esac; then
|
|
90
|
+
${JSON.stringify(implementationPath)} "$@" >/dev/null 2>&1 || true
|
|
91
|
+
session=$(cat /tmp/omx-test-detached-session-name 2>/dev/null || printf omx-test)
|
|
92
|
+
owner=$(cat /tmp/omx-test-detached-owner-id 2>/dev/null || printf '')
|
|
93
|
+
leader_pid=$(cat "$0.detached-leader-pid" 2>/dev/null || printf 123)
|
|
94
|
+
pane=$(cat "$0.detached-leader-pane" 2>/dev/null || printf '%%12')
|
|
95
|
+
printf '%s\t0\t%s\t%s\t$1\t1\t%s\n' "$pane" "$leader_pid" "$session" "$owner"
|
|
96
|
+
exit 0
|
|
97
|
+
fi
|
|
98
|
+
if [ "$1" = "if-shell" ] && [ "$2" = "-F" ] && [ "$3" = "-t" ]; then
|
|
99
|
+
if [ -f "$0.detached-recycled" ] && [ "$4" = "%99" ]; then ${JSON.stringify(implementationPath)} __nested_hud_guard_denied__ %99 999 '$2' @2; exit 0; fi
|
|
100
|
+
${JSON.stringify(implementationPath)} "$@" >/dev/null 2>&1 || true
|
|
101
|
+
success="$6"
|
|
102
|
+
receipt=$(printf '%s' "$success" | sed -n 's/.*\\(omx_detached_[A-Za-z0-9-]*\\).*/\\1/p')
|
|
103
|
+
case "\${OMX_TEST_DETACHED_RECYCLE:-}" in
|
|
104
|
+
split) case "$success" in *split-window*) exit 0;; esac ;;
|
|
105
|
+
finalization) case "$*" in
|
|
106
|
+
*split-window*) : > "$0.detached-split-seen" ;;
|
|
107
|
+
*"if-shell -F -t '%99'"*) if [ -f "$0.detached-split-seen" ]; then
|
|
108
|
+
${JSON.stringify(implementationPath)} __recycled__ %99 999 '$2' @2
|
|
109
|
+
: > "$0.detached-recycled"
|
|
110
|
+
eval "set -- $success"
|
|
111
|
+
"$0" "$@" >/dev/null
|
|
112
|
+
fi ;;
|
|
113
|
+
esac ;;
|
|
114
|
+
esac
|
|
115
|
+
[ -n "$receipt" ] || exit 1
|
|
116
|
+
case "$success" in
|
|
117
|
+
*split-window*) printf '%%99\t456\t$1\t@1\t%s\n' "$receipt" ;;
|
|
118
|
+
*) printf '%s\n' "$receipt" ;;
|
|
119
|
+
esac
|
|
120
|
+
exit 0
|
|
121
|
+
fi
|
|
122
|
+
if [ "$1" = "list-panes" ] && case "$*" in *'#{pane_id}'*'#{pane_dead}'*'#{pane_pid}'*) true;; *) false;; esac; then
|
|
123
|
+
leader_pid=$(cat "$0.detached-leader-pid" 2>/dev/null || printf '')
|
|
124
|
+
[ -n "$leader_pid" ] || exit 1
|
|
125
|
+
pane=$(cat "$0.detached-leader-pane" 2>/dev/null || printf '%%12')
|
|
126
|
+
printf '%s\t0\t%s\n' "$pane" "$leader_pid"
|
|
127
|
+
exit 0
|
|
128
|
+
fi
|
|
129
|
+
output=$(${JSON.stringify(implementationPath)} "$@")
|
|
130
|
+
status=$?
|
|
131
|
+
printf '%s' "$output"
|
|
132
|
+
if [ "$status" -eq 0 ] && [ "$1" = "new-session" ]; then
|
|
133
|
+
previous=''
|
|
134
|
+
for arg in "$@"; do
|
|
135
|
+
if [ "$previous" = '-s' ]; then printf '%s' "$arg" > /tmp/omx-test-detached-session-name; break; fi
|
|
136
|
+
previous="$arg"
|
|
137
|
+
done
|
|
138
|
+
for last_arg do :; done
|
|
139
|
+
pane=$(printf '%s' "$output" | sed -n '1p')
|
|
140
|
+
TMUX=/tmp/omx-test-tmux,1,0 TMUX_PANE="$pane" nohup /bin/sh -c 'eval "exec $1"' sh "$last_arg" </dev/null >/tmp/omx-test-detached-leader.log 2>&1 &
|
|
141
|
+
leader_pid=$!
|
|
142
|
+
printf '%s' "$leader_pid" > "$0.detached-leader-pid"
|
|
143
|
+
printf '%s' "$pane" > "$0.detached-leader-pane"
|
|
144
|
+
(while kill -0 "$leader_pid" 2>/dev/null; do sleep 0.02; done; printf done > ${JSON.stringify(`${fakeTmuxPath}.leader-done`)}) </dev/null >/dev/null 2>&1 &
|
|
145
|
+
fi
|
|
146
|
+
exit "$status"
|
|
147
|
+
`);
|
|
148
|
+
}
|
|
72
149
|
async function createLaunchFixture(wd, tmuxScript) {
|
|
73
150
|
const home = join(wd, 'home');
|
|
74
151
|
const fakeBin = join(wd, 'bin');
|
|
75
152
|
const tmuxLogPath = join(wd, 'tmux.log');
|
|
153
|
+
const leaderDonePath = join(wd, 'leader-done');
|
|
76
154
|
await mkdir(home, { recursive: true });
|
|
77
155
|
await mkdir(fakeBin, { recursive: true });
|
|
78
|
-
await writeExecutable(join(fakeBin, 'codex'), '#!/bin/sh\nprintf \'fake-codex:%s\\n\' "$*"\n');
|
|
156
|
+
await writeExecutable(join(fakeBin, 'codex'), '#!/bin/sh\nprintf \'fake-codex:%s\\n\' "$*"\nsleep 1\n');
|
|
79
157
|
await writeExecutable(join(fakeBin, 'ps'), '#!/bin/sh\nexit 0\n');
|
|
80
|
-
|
|
158
|
+
const tmuxImpl = join(fakeBin, 'tmux-impl');
|
|
159
|
+
await writeExecutable(tmuxImpl, tmuxScript(tmuxLogPath));
|
|
160
|
+
await writeExecutable(join(fakeBin, 'tmux'), `#!/bin/sh
|
|
161
|
+
if [ "$1" = "display-message" ] && [ "$2" = "-p" ] && [ "$3" = "-t" ] && case "$5" in *'#{session_created}'*'#{window_id}'*'#{pane_pid}'*) true;; *) false;; esac; then
|
|
162
|
+
session=$(cat /tmp/omx-test-detached-session-name 2>/dev/null || printf omx-test)
|
|
163
|
+
leader_pid=$(cat "$0.detached-leader-pid" 2>/dev/null || printf 123)
|
|
164
|
+
printf '%s\t$1\t1\t0\t@1\t%s\t%s\n' "$session" "$4" "$leader_pid"
|
|
165
|
+
exit 0
|
|
166
|
+
fi
|
|
167
|
+
if [ "$1" = "set-option" ] && [ "$2" = "-t" ] && [ "$4" = "@omx_instance_id" ]; then
|
|
168
|
+
printf '%s' "$5" > /tmp/omx-test-detached-owner-id
|
|
169
|
+
fi
|
|
170
|
+
if [ "$1" = "list-panes" ] && case "$*" in *'#{session_created}'*'#{@omx_instance_id}'*) true;; *) false;; esac; then
|
|
171
|
+
${JSON.stringify(tmuxImpl)} "$@" >/dev/null 2>&1 || true
|
|
172
|
+
session=$(cat /tmp/omx-test-detached-session-name 2>/dev/null || printf omx-test)
|
|
173
|
+
owner=$(cat /tmp/omx-test-detached-owner-id 2>/dev/null || printf '')
|
|
174
|
+
leader_pid=$(cat "$0.detached-leader-pid" 2>/dev/null || printf 123)
|
|
175
|
+
pane=$(cat "$0.detached-leader-pane" 2>/dev/null || printf '%%12')
|
|
176
|
+
printf '%s\t0\t%s\t%s\t$1\t1\t%s\n' "$pane" "$leader_pid" "$session" "$owner"
|
|
177
|
+
exit 0
|
|
178
|
+
fi
|
|
179
|
+
if [ "$1" = "list-panes" ] && case "$*" in *'#{pane_id}'*'#{pane_dead}'*'#{pane_pid}'*) true;; *) false;; esac; then
|
|
180
|
+
leader_pid=$(cat "$0.detached-leader-pid" 2>/dev/null || printf '')
|
|
181
|
+
[ -n "$leader_pid" ] || exit 1
|
|
182
|
+
pane=$(cat "$0.detached-leader-pane" 2>/dev/null || printf '%%12')
|
|
183
|
+
printf '%s\t0\t%s\n' "$pane" "$leader_pid"
|
|
184
|
+
exit 0
|
|
185
|
+
fi
|
|
186
|
+
if [ "$1" = "if-shell" ] && [ "$2" = "-F" ] && [ "$3" = "-t" ]; then
|
|
187
|
+
if [ -f "$0.detached-recycled" ] && [ "$4" = "%99" ]; then ${JSON.stringify(tmuxImpl)} __nested_hud_guard_denied__ %99 999 '$2' @2; exit 0; fi
|
|
188
|
+
${JSON.stringify(tmuxImpl)} "$@" >/dev/null 2>&1 || true
|
|
189
|
+
success="$6"
|
|
190
|
+
receipt=$(printf '%s' "$success" | sed -n 's/.*\\(omx_detached_[A-Za-z0-9-]*\\).*/\\1/p')
|
|
191
|
+
case "\${OMX_TEST_DETACHED_RECYCLE:-}" in
|
|
192
|
+
split) case "$success" in *split-window*) exit 0;; esac ;;
|
|
193
|
+
finalization) case "$*" in
|
|
194
|
+
*split-window*) : > "$0.detached-split-seen" ;;
|
|
195
|
+
*"if-shell -F -t '%99'"*) if [ -f "$0.detached-split-seen" ]; then
|
|
196
|
+
${JSON.stringify(tmuxImpl)} __recycled__ %99 999 '$2' @2
|
|
197
|
+
: > "$0.detached-recycled"
|
|
198
|
+
eval "set -- $success"
|
|
199
|
+
"$0" "$@" >/dev/null
|
|
200
|
+
fi ;;
|
|
201
|
+
esac ;;
|
|
202
|
+
esac
|
|
203
|
+
[ -n "$receipt" ] || exit 1
|
|
204
|
+
case "$success" in
|
|
205
|
+
*split-window*) printf '%%99\t456\t$1\t@1\t%s\n' "$receipt" ;;
|
|
206
|
+
*) printf '%s\n' "$receipt" ;;
|
|
207
|
+
esac
|
|
208
|
+
exit 0
|
|
209
|
+
fi
|
|
210
|
+
output=$(${JSON.stringify(tmuxImpl)} "$@")
|
|
211
|
+
status=$?
|
|
212
|
+
printf '%s' "$output"
|
|
213
|
+
if [ "$status" -eq 0 ] && [ "$1" = "new-session" ]; then
|
|
214
|
+
previous=''
|
|
215
|
+
for arg in "$@"; do
|
|
216
|
+
if [ "$previous" = '-s' ]; then printf '%s' "$arg" > /tmp/omx-test-detached-session-name; break; fi
|
|
217
|
+
previous="$arg"
|
|
218
|
+
done
|
|
219
|
+
for last_arg do :; done
|
|
220
|
+
previous=''
|
|
221
|
+
for arg in "$@"; do
|
|
222
|
+
if [ "$previous" = '-e' ]; then export "$arg"; fi
|
|
223
|
+
previous="$arg"
|
|
224
|
+
done
|
|
225
|
+
pane=$(printf '%s' "$output" | sed -n '1p')
|
|
226
|
+
TMUX=/tmp/omx-test-tmux,1,0 TMUX_PANE="$pane" nohup /bin/sh -c 'eval "exec $1"' sh "$last_arg" </dev/null >/tmp/omx-test-detached-leader.log 2>&1 &
|
|
227
|
+
leader_pid=$!
|
|
228
|
+
printf '%s' "$leader_pid" > "$0.detached-leader-pid"
|
|
229
|
+
printf '%s' "$pane" > "$0.detached-leader-pane"
|
|
230
|
+
(while kill -0 "$leader_pid" 2>/dev/null; do sleep 0.02; done; printf done > ${JSON.stringify(join(wd, 'leader-done'))}) </dev/null >/dev/null 2>&1 &
|
|
231
|
+
fi
|
|
232
|
+
exit "$status"
|
|
233
|
+
`);
|
|
81
234
|
return {
|
|
82
235
|
tmuxLogPath,
|
|
236
|
+
leaderDonePath,
|
|
83
237
|
env: {
|
|
84
238
|
HOME: home,
|
|
85
239
|
PATH: `${fakeBin}:/usr/bin:/bin`,
|
|
@@ -103,8 +257,9 @@ function startHeldOmx(cwd, envOverrides) {
|
|
|
103
257
|
stdio: 'inherit',
|
|
104
258
|
});
|
|
105
259
|
}
|
|
106
|
-
async function waitForPath(path, expectedLines = 1) {
|
|
107
|
-
|
|
260
|
+
async function waitForPath(path, expectedLines = 1, timeoutMs = 6_000) {
|
|
261
|
+
const deadline = Date.now() + timeoutMs;
|
|
262
|
+
while (Date.now() < deadline) {
|
|
108
263
|
if (existsSync(path)) {
|
|
109
264
|
const contents = await readFile(path, 'utf-8').catch(() => '');
|
|
110
265
|
if (contents.trim().split('\n').filter(Boolean).length >= expectedLines)
|
|
@@ -114,6 +269,29 @@ async function waitForPath(path, expectedLines = 1) {
|
|
|
114
269
|
}
|
|
115
270
|
throw new Error(`timed out waiting for ${path}`);
|
|
116
271
|
}
|
|
272
|
+
function parseShimTmuxArgv(contents) {
|
|
273
|
+
return contents
|
|
274
|
+
.split('tmux argv:\n')
|
|
275
|
+
.slice(1)
|
|
276
|
+
.map((record) => record.split('\nend tmux argv')[0].split('\n').filter(Boolean));
|
|
277
|
+
}
|
|
278
|
+
async function assertShimLogQuiescent(path, quietPeriodMs, message) {
|
|
279
|
+
const before = await readFile(path, 'utf-8').catch(() => '');
|
|
280
|
+
await new Promise((resolve) => setTimeout(resolve, quietPeriodMs));
|
|
281
|
+
assert.equal(await readFile(path, 'utf-8').catch(() => ''), before, message);
|
|
282
|
+
}
|
|
283
|
+
async function waitForServerLogSubstring(readServerLog, offset, needle, timeoutMs = 3_000, intervalMs = 25) {
|
|
284
|
+
const deadline = Date.now() + timeoutMs;
|
|
285
|
+
let lastWindow = '';
|
|
286
|
+
while (Date.now() <= deadline) {
|
|
287
|
+
const log = await readServerLog();
|
|
288
|
+
lastWindow = log.slice(offset);
|
|
289
|
+
if (lastWindow.includes(needle))
|
|
290
|
+
return lastWindow;
|
|
291
|
+
await new Promise((resolve) => setTimeout(resolve, intervalMs));
|
|
292
|
+
}
|
|
293
|
+
throw new Error(`timed out after ${timeoutMs}ms waiting for ${JSON.stringify(needle)} in private tmux server log; tail=${JSON.stringify(lastWindow.slice(-2_000))}`);
|
|
294
|
+
}
|
|
117
295
|
async function stopHeldOmx(child, releasePath) {
|
|
118
296
|
await rm(releasePath, { force: true });
|
|
119
297
|
await new Promise((resolve, reject) => {
|
|
@@ -150,6 +328,24 @@ while [ -f "${releasePath}" ]; do sleep 1; done
|
|
|
150
328
|
},
|
|
151
329
|
};
|
|
152
330
|
}
|
|
331
|
+
function skipUnlessPrivateRealTmux(t) {
|
|
332
|
+
if (isRealTmuxAvailable())
|
|
333
|
+
return true;
|
|
334
|
+
assert.equal(process.env.CI, undefined, 'CI must provide tmux for the private-server detached launch regression');
|
|
335
|
+
t.skip('tmux is not installed');
|
|
336
|
+
return false;
|
|
337
|
+
}
|
|
338
|
+
function skipUnlessScriptAvailable(t) {
|
|
339
|
+
if (process.platform === 'win32') {
|
|
340
|
+
t.skip('PTY launch harness requires posix script(1)');
|
|
341
|
+
return false;
|
|
342
|
+
}
|
|
343
|
+
if (isRealScriptAvailable())
|
|
344
|
+
return true;
|
|
345
|
+
assert.equal(process.env.CI, undefined, 'CI must provide script(1) for the PTY launch regression');
|
|
346
|
+
t.skip('script(1) is not installed');
|
|
347
|
+
return false;
|
|
348
|
+
}
|
|
153
349
|
describe('omx launch fallback when tmux is unavailable', () => {
|
|
154
350
|
it('surfaces direct Codex startup stderr and preserves the child exit code', async () => {
|
|
155
351
|
const wd = await mkdtemp(join(tmpdir(), 'omx-launch-child-error-'));
|
|
@@ -179,7 +375,7 @@ exit 42
|
|
|
179
375
|
assert.match(result.stderr, /\[omx\] codex exited with code 42/);
|
|
180
376
|
}
|
|
181
377
|
finally {
|
|
182
|
-
await rm(wd, { recursive: true, force: true });
|
|
378
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
183
379
|
}
|
|
184
380
|
});
|
|
185
381
|
it('reports a missing Codex executable instead of exiting silently', async () => {
|
|
@@ -206,7 +402,7 @@ exit 42
|
|
|
206
402
|
assert.notEqual(result.stderr.trim(), '');
|
|
207
403
|
}
|
|
208
404
|
finally {
|
|
209
|
-
await rm(wd, { recursive: true, force: true });
|
|
405
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
210
406
|
}
|
|
211
407
|
});
|
|
212
408
|
it('launches codex directly without tmux ENOENT noise', async () => {
|
|
@@ -237,7 +433,7 @@ exit 42
|
|
|
237
433
|
assert.doesNotMatch(result.stderr, /spawnSync tmux ENOENT/);
|
|
238
434
|
}
|
|
239
435
|
finally {
|
|
240
|
-
await rm(wd, { recursive: true, force: true });
|
|
436
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
241
437
|
}
|
|
242
438
|
});
|
|
243
439
|
it('passes literal max and ultra after -- with raw config args to Codex launch unchanged', async () => {
|
|
@@ -314,7 +510,7 @@ exit 42
|
|
|
314
510
|
]);
|
|
315
511
|
}
|
|
316
512
|
finally {
|
|
317
|
-
await rm(wd, { recursive: true, force: true });
|
|
513
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
318
514
|
}
|
|
319
515
|
});
|
|
320
516
|
it('treats image values as variadic when detecting resume launches', async () => {
|
|
@@ -370,17 +566,22 @@ exec "$NODE_BINARY" -e 'const fs = require("node:fs"); const path = require("nod
|
|
|
370
566
|
}
|
|
371
567
|
}
|
|
372
568
|
finally {
|
|
373
|
-
await rm(wd, { recursive: true, force: true });
|
|
569
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
374
570
|
}
|
|
375
571
|
});
|
|
376
572
|
});
|
|
377
573
|
describe('ordinary launch root collision guidance', () => {
|
|
378
574
|
it('keeps the cwd default for the first launch and fails the second and third launches closed with explicit-root guidance', async () => {
|
|
379
575
|
const wd = await mkdtemp(join(tmpdir(), 'omx-launch-root-conflict-'));
|
|
576
|
+
let binding;
|
|
380
577
|
try {
|
|
381
578
|
execFileSync('git', ['init'], { cwd: wd, stdio: 'ignore' });
|
|
382
579
|
const fixture = await createHeldCodexFixture(wd);
|
|
383
|
-
await
|
|
580
|
+
const established = await establishLaunchSessionBinding(wd, 'first-standard-launch', { pid: process.pid });
|
|
581
|
+
assert.equal(established.kind, 'committed-released');
|
|
582
|
+
if (established.kind !== 'committed-released')
|
|
583
|
+
return;
|
|
584
|
+
binding = established.binding;
|
|
384
585
|
const second = runOmx(wd, ['--direct', '--version'], fixture.env);
|
|
385
586
|
const third = runOmx(wd, ['--direct', '--version'], fixture.env);
|
|
386
587
|
if (shouldSkipForSpawnPermissions(second.error) || shouldSkipForSpawnPermissions(third.error))
|
|
@@ -400,10 +601,12 @@ describe('ordinary launch root collision guidance', () => {
|
|
|
400
601
|
assert.match(resume.stderr, /session_pointer_owner_conflict/);
|
|
401
602
|
assert.doesNotMatch(resume.stderr, /concurrent conversations in this checkout require distinct user-specified OMX_ROOT values/);
|
|
402
603
|
}
|
|
403
|
-
await
|
|
604
|
+
await finalizeBoundOnce(binding, 'test');
|
|
404
605
|
}
|
|
405
606
|
finally {
|
|
406
|
-
|
|
607
|
+
if (binding)
|
|
608
|
+
await closeLaunchSessionBindingOnce(binding).catch(() => { });
|
|
609
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
407
610
|
}
|
|
408
611
|
});
|
|
409
612
|
it('keeps explicit roots literal: distinct roots launch independently while a shared root remains fatal without reroute guidance', async () => {
|
|
@@ -438,11 +641,13 @@ describe('ordinary launch root collision guidance', () => {
|
|
|
438
641
|
first.kill('SIGKILL');
|
|
439
642
|
if (second)
|
|
440
643
|
second.kill('SIGKILL');
|
|
441
|
-
await rm(wd, { recursive: true, force: true });
|
|
644
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
442
645
|
}
|
|
443
646
|
});
|
|
444
|
-
it('keeps different checkout defaults independent and
|
|
647
|
+
it('keeps different checkout defaults independent and fails closed on stale default-pointer evidence', async () => {
|
|
445
648
|
const wd = await mkdtemp(join(tmpdir(), 'omx-launch-root-stale-'));
|
|
649
|
+
let firstBinding;
|
|
650
|
+
let secondBinding;
|
|
446
651
|
try {
|
|
447
652
|
const firstCheckout = join(wd, 'first-checkout');
|
|
448
653
|
const secondCheckout = join(wd, 'second-checkout');
|
|
@@ -450,23 +655,34 @@ describe('ordinary launch root collision guidance', () => {
|
|
|
450
655
|
await mkdir(secondCheckout, { recursive: true });
|
|
451
656
|
execFileSync('git', ['init'], { cwd: firstCheckout, stdio: 'ignore' });
|
|
452
657
|
execFileSync('git', ['init'], { cwd: secondCheckout, stdio: 'ignore' });
|
|
453
|
-
await
|
|
454
|
-
await
|
|
658
|
+
const firstEstablished = await establishLaunchSessionBinding(firstCheckout, 'first-checkout-owner', { pid: process.pid });
|
|
659
|
+
const secondEstablished = await establishLaunchSessionBinding(secondCheckout, 'second-checkout-owner', { pid: process.pid });
|
|
660
|
+
assert.equal(firstEstablished.kind, 'committed-released');
|
|
661
|
+
assert.equal(secondEstablished.kind, 'committed-released');
|
|
662
|
+
if (firstEstablished.kind !== 'committed-released' || secondEstablished.kind !== 'committed-released')
|
|
663
|
+
return;
|
|
664
|
+
firstBinding = firstEstablished.binding;
|
|
665
|
+
secondBinding = secondEstablished.binding;
|
|
455
666
|
assert.match(await readFile(join(firstCheckout, '.omx', 'state', 'session.json'), 'utf-8'), /first-checkout-owner/);
|
|
456
667
|
assert.match(await readFile(join(secondCheckout, '.omx', 'state', 'session.json'), 'utf-8'), /second-checkout-owner/);
|
|
457
|
-
await
|
|
458
|
-
await
|
|
668
|
+
await finalizeBoundOnce(firstBinding, 'test');
|
|
669
|
+
await finalizeBoundOnce(secondBinding, 'test');
|
|
459
670
|
await writeSessionStart(firstCheckout, 'stale-owner', { pid: 2_147_483_647 });
|
|
460
671
|
const fixture = await createHeldCodexFixture(wd);
|
|
461
672
|
await rm(fixture.releasePath, { force: true });
|
|
462
673
|
const relaunch = runOmx(firstCheckout, ['--direct', '--version'], fixture.env);
|
|
463
674
|
if (shouldSkipForSpawnPermissions(relaunch.error))
|
|
464
675
|
return;
|
|
465
|
-
assert.
|
|
466
|
-
assert.
|
|
676
|
+
assert.notEqual(relaunch.status, 0, relaunch.error || relaunch.stderr || relaunch.stdout);
|
|
677
|
+
assert.match(relaunch.stderr, /session_pointer_unusable|stale-dead/);
|
|
678
|
+
assert.match(await readFile(join(firstCheckout, '.omx', 'state', 'session.json'), 'utf-8'), /stale-owner/);
|
|
467
679
|
}
|
|
468
680
|
finally {
|
|
469
|
-
|
|
681
|
+
if (firstBinding)
|
|
682
|
+
await closeLaunchSessionBindingOnce(firstBinding).catch(() => { });
|
|
683
|
+
if (secondBinding)
|
|
684
|
+
await closeLaunchSessionBindingOnce(secondBinding).catch(() => { });
|
|
685
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
470
686
|
}
|
|
471
687
|
});
|
|
472
688
|
});
|
|
@@ -504,7 +720,7 @@ printf 'fake-codex:%s\n' "$*"
|
|
|
504
720
|
assert.equal(existsSync(join(worktreePath, '.omx')), false);
|
|
505
721
|
}
|
|
506
722
|
finally {
|
|
507
|
-
await rm(wd, { recursive: true, force: true });
|
|
723
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
508
724
|
}
|
|
509
725
|
});
|
|
510
726
|
it('preserves explicit OMX_ROOT for launch worktree state', async () => {
|
|
@@ -540,7 +756,7 @@ printf 'fake-codex:%s\n' "$*"
|
|
|
540
756
|
assert.equal(existsSync(join(repo, '.omx')), false);
|
|
541
757
|
}
|
|
542
758
|
finally {
|
|
543
|
-
await rm(wd, { recursive: true, force: true });
|
|
759
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
544
760
|
}
|
|
545
761
|
});
|
|
546
762
|
it('keeps direct madmax worktree launches bound to the boxed run root', async () => {
|
|
@@ -590,7 +806,7 @@ printf 'fake-codex:%s\n' "$*"
|
|
|
590
806
|
assert.match(normalizedStdout, /fake-codex-context:[0-9a-f]{32}/);
|
|
591
807
|
}
|
|
592
808
|
finally {
|
|
593
|
-
await rm(wd, { recursive: true, force: true });
|
|
809
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
594
810
|
}
|
|
595
811
|
});
|
|
596
812
|
});
|
|
@@ -649,20 +865,406 @@ exit 0
|
|
|
649
865
|
const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
|
|
650
866
|
assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
|
|
651
867
|
assert.match(tmuxLog, /tmux:new-session /);
|
|
652
|
-
assert.match(tmuxLog, /tmux:split-window
|
|
868
|
+
assert.match(tmuxLog, /tmux:if-shell -F -t %12 .*split-window/);
|
|
653
869
|
assert.doesNotMatch(tmuxLog, /tmux:attach-session/);
|
|
654
870
|
assert.doesNotMatch(result.stderr, /failed to attach detached tmux session/);
|
|
655
871
|
}
|
|
656
872
|
finally {
|
|
657
|
-
await rm(wd, { recursive: true, force: true });
|
|
873
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
658
874
|
}
|
|
659
875
|
});
|
|
660
876
|
});
|
|
661
877
|
describe('omx launcher when tmux is available', () => {
|
|
662
|
-
it('
|
|
878
|
+
it('restores an enlarged detached HUD on the first larger client attach', async (t) => {
|
|
879
|
+
if (!skipUnlessPrivateRealTmux(t))
|
|
880
|
+
return;
|
|
881
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-launch-private-real-tmux-'));
|
|
882
|
+
try {
|
|
883
|
+
await withTempTmuxSession(async (fixture) => {
|
|
884
|
+
const home = join(wd, 'home');
|
|
885
|
+
const bin = join(wd, 'bin');
|
|
886
|
+
const shimLogPath = join(wd, 'private-tmux-shim.log');
|
|
887
|
+
await mkdir(home, { recursive: true });
|
|
888
|
+
await mkdir(bin, { recursive: true });
|
|
889
|
+
await writeExecutable(join(bin, 'codex'), '#!/bin/sh\nsleep 300\n');
|
|
890
|
+
await fixture.createPathShim(bin, shimLogPath);
|
|
891
|
+
const result = runOmx(wd, ['--madmax', '--tmux'], {
|
|
892
|
+
HOME: home,
|
|
893
|
+
PATH: `${bin}:${process.env.PATH ?? ''}`,
|
|
894
|
+
OMX_AUTO_UPDATE: '0',
|
|
895
|
+
OMX_NOTIFY_FALLBACK: '0',
|
|
896
|
+
OMX_HOOK_DERIVED_SIGNALS: '0',
|
|
897
|
+
OMX_HERMES_MCP_BRIDGE: '1',
|
|
898
|
+
OMX_LAUNCH_POLICY: 'direct',
|
|
899
|
+
TMUX: '',
|
|
900
|
+
TMUX_PANE: '',
|
|
901
|
+
});
|
|
902
|
+
assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
|
|
903
|
+
const launchedSession = fixture.run(['list-sessions', '-F', '#{session_name}'])
|
|
904
|
+
.split('\n')
|
|
905
|
+
.find((name) => name.startsWith('omx-') && name !== fixture.sessionName);
|
|
906
|
+
assert.ok(launchedSession, 'compiled launcher must create its detached session on the private server');
|
|
907
|
+
// tmux 3.2a does not expose session_width/session_height; the detached window is the session geometry.
|
|
908
|
+
assert.equal(fixture.run(['display-message', '-p', '-t', launchedSession, '#{window_width}x#{window_height}']), '80x24', 'the detached session must remain at its initial geometry before any client attaches');
|
|
909
|
+
const panes = fixture.run(['list-panes', '-t', launchedSession, '-F', '#{pane_id}\t#{pane_height}\t#{pane_start_command}'])
|
|
910
|
+
.split('\n')
|
|
911
|
+
.map((line) => line.split('\t'));
|
|
912
|
+
const hud = panes.find(([, , command]) => command.includes('OMX_DETACHED_HUD_OPERATION='));
|
|
913
|
+
assert.ok(hud, 'HUD split must carry its operation marker');
|
|
914
|
+
assert.equal(Number(hud[1]), HUD_TMUX_HEIGHT_LINES, 'authorized direct reconciliation must resize the HUD');
|
|
915
|
+
assert.equal(fixture.run(['show-options', '-v', '-t', launchedSession, 'history-limit']), String(DETACHED_TMUX_HISTORY_LIMIT), 'direct leader mutation must receive its receipt and set session history');
|
|
916
|
+
const hooks = fixture.run(['show-hooks', '-t', launchedSession]);
|
|
917
|
+
assert.match(hooks, /client-resized\[[0-9]+\].*run-shell -b/);
|
|
918
|
+
assert.match(hooks, /tmux if-shell -F -t/);
|
|
919
|
+
assert.match(hooks, /OMX_DETACHED_HUD_OPERATION=/);
|
|
920
|
+
const storedPaneFormat = hooks.match(/##\{pane_id\}\\t##\{pane_dead\}\\t##\{pane_pid\}/)?.[0] ?? '';
|
|
921
|
+
assert.equal(storedPaneFormat, '##{pane_id}\\t##{pane_dead}\\t##{pane_pid}', 'stored hook display must retain one outer format escape and encode TAB bytes');
|
|
922
|
+
const executablePaneFormat = storedPaneFormat.replaceAll('##{', '#{').replaceAll('\\t', '\t');
|
|
923
|
+
assert.equal(executablePaneFormat, '#{pane_id}\t#{pane_dead}\t#{pane_pid}', 'one outer tmux format pass must decode the stored format into the executable nested argv');
|
|
924
|
+
const clientAttachedHookSlots = [...hooks.matchAll(/client-attached\[[0-9]+\]/g)].map(([slot]) => slot);
|
|
925
|
+
for (const hookSlot of clientAttachedHookSlots) {
|
|
926
|
+
fixture.run(['set-hook', '-u', '-t', launchedSession, hookSlot]);
|
|
927
|
+
}
|
|
928
|
+
const installedHooks = fixture.run(['show-hooks', '-t', launchedSession]);
|
|
929
|
+
const storedResizeHookSlots = [...installedHooks.matchAll(/client-resized\[[0-9]+\]/g)].map(([slot]) => slot);
|
|
930
|
+
assert.equal(storedResizeHookSlots.length, 1, 'exactly one installed client-resized hook must remain for the natural resize trigger');
|
|
931
|
+
assert.match(installedHooks, /client-resized\[[0-9]+\].*run-shell -b/);
|
|
932
|
+
assert.doesNotMatch(installedHooks, /client-attached\[[0-9]+\]/, 'the resize trigger must not be conflated with client-attached hook execution');
|
|
933
|
+
await new Promise((resolve) => setTimeout(resolve, (HUD_RESIZE_RECONCILE_DELAY_SECONDS * 1_000) + 250));
|
|
934
|
+
const launchPaneSnapshots = parseShimTmuxArgv(await readFile(shimLogPath, 'utf-8'))
|
|
935
|
+
.filter((argv) => argv[0] === 'list-panes' && argv[1] === '-a');
|
|
936
|
+
assert.ok(launchPaneSnapshots.length >= 2, 'the direct and delayed launch reconciliations must both complete before the stored-hook baseline is cleared');
|
|
937
|
+
await assertShimLogQuiescent(shimLogPath, 250, 'launch and delayed reconciliation must settle before clearing the stored-hook baseline');
|
|
938
|
+
await writeFile(shimLogPath, '');
|
|
939
|
+
assert.equal(await readFile(shimLogPath, 'utf-8'), '', 'the shim baseline must start after launch and reconciliation activity settles');
|
|
940
|
+
fixture.run(['resize-pane', '-t', hud[0], '-y', '7']);
|
|
941
|
+
assert.equal(Number(fixture.run(['display-message', '-p', '-t', hud[0], '#{pane_height}'])), 7, 'the detached HUD must be enlarged before the restore trigger');
|
|
942
|
+
assert.equal(fixture.run(['display-message', '-p', '-t', launchedSession, '#{window_width}x#{window_height}']), '80x24', 'enlarging the detached HUD must not attach a client');
|
|
943
|
+
fixture.triggerClientResize(launchedSession, { cols: 121, rows: 41 });
|
|
944
|
+
await new Promise((resolve) => setTimeout(resolve, (HUD_RESIZE_RECONCILE_DELAY_SECONDS * 1_000) + 250));
|
|
945
|
+
await assertShimLogQuiescent(shimLogPath, 250, 'the stored client-resized hook must finish both its immediate and delayed passes before assertions');
|
|
946
|
+
assert.equal(Number(fixture.run(['display-message', '-p', '-t', hud[0], '#{pane_height}'])), HUD_TMUX_HEIGHT_LINES, 'the first larger client attach must restore the enlarged detached HUD');
|
|
947
|
+
const paneSnapshots = parseShimTmuxArgv(await readFile(shimLogPath, 'utf-8'))
|
|
948
|
+
.filter((argv) => argv[0] === 'list-panes' && argv[1] === '-a');
|
|
949
|
+
assert.equal(paneSnapshots.length, 2, 'only the explicitly triggered stored client-resized hook may produce the post-baseline immediate and delayed pane snapshots');
|
|
950
|
+
for (const argv of paneSnapshots) {
|
|
951
|
+
assert.deepEqual(argv, ['list-panes', '-a', '-F', executablePaneFormat], 'post-baseline stored client-resized hook execution must preserve the exact executable TAB-delimited nested argv');
|
|
952
|
+
}
|
|
953
|
+
});
|
|
954
|
+
}
|
|
955
|
+
finally {
|
|
956
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
957
|
+
}
|
|
958
|
+
});
|
|
959
|
+
it('records the guarded resize command without an unknown-command parser error', async (t) => {
|
|
960
|
+
if (!skipUnlessPrivateRealTmux(t))
|
|
961
|
+
return;
|
|
962
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-launch-private-real-tmux-'));
|
|
963
|
+
try {
|
|
964
|
+
await withTempTmuxSession({ serverLog: true }, async (fixture) => {
|
|
965
|
+
const home = join(wd, 'home');
|
|
966
|
+
const bin = join(wd, 'bin');
|
|
967
|
+
const shimLogPath = join(wd, 'private-tmux-shim.log');
|
|
968
|
+
await mkdir(home, { recursive: true });
|
|
969
|
+
await mkdir(bin, { recursive: true });
|
|
970
|
+
await writeExecutable(join(bin, 'codex'), '#!/bin/sh\nsleep 300\n');
|
|
971
|
+
await fixture.createPathShim(bin, shimLogPath);
|
|
972
|
+
const result = runOmx(wd, ['--madmax', '--tmux'], {
|
|
973
|
+
HOME: home,
|
|
974
|
+
PATH: `${bin}:${process.env.PATH ?? ''}`,
|
|
975
|
+
OMX_AUTO_UPDATE: '0',
|
|
976
|
+
OMX_NOTIFY_FALLBACK: '0',
|
|
977
|
+
OMX_HOOK_DERIVED_SIGNALS: '0',
|
|
978
|
+
OMX_HERMES_MCP_BRIDGE: '1',
|
|
979
|
+
OMX_LAUNCH_POLICY: 'direct',
|
|
980
|
+
TMUX: '',
|
|
981
|
+
TMUX_PANE: '',
|
|
982
|
+
});
|
|
983
|
+
assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
|
|
984
|
+
const launchedSession = fixture.run(['list-sessions', '-F', '#{session_name}'])
|
|
985
|
+
.split('\n')
|
|
986
|
+
.find((name) => name.startsWith('omx-') && name !== fixture.sessionName);
|
|
987
|
+
assert.ok(launchedSession, 'compiled launcher must create its detached session on the private server');
|
|
988
|
+
const panes = fixture.run(['list-panes', '-t', launchedSession, '-F', '#{pane_id}\t#{pane_height}\t#{pane_start_command}'])
|
|
989
|
+
.split('\n')
|
|
990
|
+
.map((line) => line.split('\t'));
|
|
991
|
+
const hud = panes.find(([, , command]) => command.includes('OMX_DETACHED_HUD_OPERATION='));
|
|
992
|
+
assert.ok(hud, 'HUD split must carry its operation marker');
|
|
993
|
+
const hooks = fixture.run(['show-hooks', '-t', launchedSession]);
|
|
994
|
+
const storedPaneFormat = hooks.match(/##\{pane_id\}\\t##\{pane_dead\}\\t##\{pane_pid\}/)?.[0] ?? '';
|
|
995
|
+
const executablePaneFormat = storedPaneFormat.replaceAll('##{', '#{').replaceAll('\\t', '\t');
|
|
996
|
+
assert.equal(executablePaneFormat, '#{pane_id}\t#{pane_dead}\t#{pane_pid}', 'one outer tmux format pass must decode the stored format into the executable nested argv');
|
|
997
|
+
const clientAttachedHookSlots = [...hooks.matchAll(/client-attached\[[0-9]+\]/g)].map(([slot]) => slot);
|
|
998
|
+
for (const hookSlot of clientAttachedHookSlots) {
|
|
999
|
+
fixture.run(['set-hook', '-u', '-t', launchedSession, hookSlot]);
|
|
1000
|
+
}
|
|
1001
|
+
const installedHooks = fixture.run(['show-hooks', '-t', launchedSession]);
|
|
1002
|
+
const storedResizeHookSlots = [...installedHooks.matchAll(/client-resized\[[0-9]+\]/g)].map(([slot]) => slot);
|
|
1003
|
+
assert.equal(storedResizeHookSlots.length, 1, 'exactly one installed client-resized hook must remain for the natural resize trigger');
|
|
1004
|
+
await new Promise((resolve) => setTimeout(resolve, (HUD_RESIZE_RECONCILE_DELAY_SECONDS * 1_000) + 250));
|
|
1005
|
+
const launchPaneSnapshots = parseShimTmuxArgv(await readFile(shimLogPath, 'utf-8'))
|
|
1006
|
+
.filter((argv) => argv[0] === 'list-panes' && argv[1] === '-a');
|
|
1007
|
+
assert.ok(launchPaneSnapshots.length >= 2, 'the direct and delayed launch reconciliations must both complete before the stored-hook baseline is cleared');
|
|
1008
|
+
await assertShimLogQuiescent(shimLogPath, 250, 'launch and delayed reconciliation must settle before clearing the stored-hook baseline');
|
|
1009
|
+
await writeFile(shimLogPath, '');
|
|
1010
|
+
const probeOffset = (await fixture.readServerLog()).length;
|
|
1011
|
+
const probe = fixture.runResult([
|
|
1012
|
+
'if-shell', '-F', '-t', fixture.leaderPaneId, '1',
|
|
1013
|
+
'tmux display-message -p omx-oracle-probe', '',
|
|
1014
|
+
]);
|
|
1015
|
+
assert.equal(probe.error, '', 'the positive-control tmux client must start successfully');
|
|
1016
|
+
assert.notEqual(probe.status, 0, 'tmux must reject the deliberately malformed selected command list');
|
|
1017
|
+
assert.ok(probe.stderr === '' || /unknown command: tmux/.test(probe.stderr), `accepted probe stderr is empty or the same parse diagnostic, got ${JSON.stringify(probe.stderr)}`);
|
|
1018
|
+
assert.equal(fixture.sessionExists(), true, 'the private tmux server must remain alive after the malformed client command');
|
|
1019
|
+
assert.equal(fixture.run(['display-message', '-p', '-t', fixture.leaderPaneId, '#{pane_id}']), fixture.leaderPaneId, 'a successful post-probe command must prove the server remains responsive');
|
|
1020
|
+
await waitForServerLogSubstring(fixture.readServerLog, probeOffset, 'unknown command: tmux', 3_000, 25);
|
|
1021
|
+
const triggerOffset = (await fixture.readServerLog()).length;
|
|
1022
|
+
fixture.run(['resize-pane', '-t', hud[0], '-y', '7']);
|
|
1023
|
+
assert.equal(Number(fixture.run(['display-message', '-p', '-t', hud[0], '#{pane_height}'])), 7, 'the detached HUD must be enlarged before the parser oracle trigger');
|
|
1024
|
+
fixture.triggerClientResize(launchedSession, { cols: 121, rows: 41 });
|
|
1025
|
+
await new Promise((resolve) => setTimeout(resolve, (HUD_RESIZE_RECONCILE_DELAY_SECONDS * 1_000) + 250));
|
|
1026
|
+
await assertShimLogQuiescent(shimLogPath, 250, 'the stored client-resized hook must finish both its immediate and delayed passes before assertions');
|
|
1027
|
+
const paneSnapshots = parseShimTmuxArgv(await readFile(shimLogPath, 'utf-8'))
|
|
1028
|
+
.filter((argv) => argv[0] === 'list-panes' && argv[1] === '-a');
|
|
1029
|
+
assert.equal(paneSnapshots.length, 2, 'only the explicitly triggered stored client-resized hook may produce the post-baseline immediate and delayed pane snapshots');
|
|
1030
|
+
for (const argv of paneSnapshots) {
|
|
1031
|
+
assert.deepEqual(argv, ['list-panes', '-a', '-F', executablePaneFormat], 'post-baseline stored client-resized hook execution must preserve the exact executable TAB-delimited nested argv');
|
|
1032
|
+
}
|
|
1033
|
+
const triggerWindow = (await fixture.readServerLog()).slice(triggerOffset);
|
|
1034
|
+
assert.ok(triggerWindow.includes(`resize-pane -t ${hud[0]} -y ${HUD_TMUX_HEIGHT_LINES}`), 'the server-log window must cover the stored hook resize command');
|
|
1035
|
+
assert.doesNotMatch(triggerWindow, /unknown command: tmux/, 'the triggered guarded sink must parse as a bare tmux command-list entry (#3292)');
|
|
1036
|
+
});
|
|
1037
|
+
}
|
|
1038
|
+
finally {
|
|
1039
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
1040
|
+
}
|
|
1041
|
+
});
|
|
1042
|
+
it('denies detached HUD mutation after the pane owner changes', async (t) => {
|
|
1043
|
+
if (!skipUnlessPrivateRealTmux(t))
|
|
1044
|
+
return;
|
|
1045
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-launch-private-real-tmux-'));
|
|
1046
|
+
try {
|
|
1047
|
+
await withTempTmuxSession(async (fixture) => {
|
|
1048
|
+
const home = join(wd, 'home');
|
|
1049
|
+
const bin = join(wd, 'bin');
|
|
1050
|
+
const shimLogPath = join(wd, 'private-tmux-shim.log');
|
|
1051
|
+
const foreignSession = 'foreign-control';
|
|
1052
|
+
let launchedSession;
|
|
1053
|
+
let originalOwner;
|
|
1054
|
+
await mkdir(home, { recursive: true });
|
|
1055
|
+
await mkdir(bin, { recursive: true });
|
|
1056
|
+
await writeExecutable(join(bin, 'codex'), '#!/bin/sh\nsleep 300\n');
|
|
1057
|
+
await fixture.createPathShim(bin, shimLogPath);
|
|
1058
|
+
fixture.run(['new-session', '-d', '-s', foreignSession, 'sleep 300']);
|
|
1059
|
+
const foreignBefore = fixture.run(['list-panes', '-t', foreignSession, '-F', '#{pane_id}\t#{pane_pid}\t#{pane_height}']);
|
|
1060
|
+
try {
|
|
1061
|
+
const result = runOmx(wd, ['--madmax', '--tmux'], {
|
|
1062
|
+
HOME: home,
|
|
1063
|
+
PATH: `${bin}:${process.env.PATH ?? ''}`,
|
|
1064
|
+
OMX_AUTO_UPDATE: '0',
|
|
1065
|
+
OMX_NOTIFY_FALLBACK: '0',
|
|
1066
|
+
OMX_HOOK_DERIVED_SIGNALS: '0',
|
|
1067
|
+
OMX_HERMES_MCP_BRIDGE: '1',
|
|
1068
|
+
OMX_LAUNCH_POLICY: 'direct',
|
|
1069
|
+
TMUX: '',
|
|
1070
|
+
TMUX_PANE: '',
|
|
1071
|
+
});
|
|
1072
|
+
assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
|
|
1073
|
+
launchedSession = fixture.run(['list-sessions', '-F', '#{session_name}'])
|
|
1074
|
+
.split('\n')
|
|
1075
|
+
.find((name) => name.startsWith('omx-') && name !== fixture.sessionName);
|
|
1076
|
+
assert.ok(launchedSession, 'compiled launcher must create its detached session on the private server');
|
|
1077
|
+
const panes = fixture.run(['list-panes', '-t', launchedSession, '-F', '#{pane_id}\t#{pane_height}\t#{pane_start_command}'])
|
|
1078
|
+
.split('\n')
|
|
1079
|
+
.map((line) => line.split('\t'));
|
|
1080
|
+
const hud = panes.find(([, , command]) => command.includes('OMX_DETACHED_HUD_OPERATION='));
|
|
1081
|
+
assert.ok(hud, 'HUD split must carry its operation marker');
|
|
1082
|
+
const hooks = fixture.run(['show-hooks', '-t', launchedSession]);
|
|
1083
|
+
const storedPaneFormat = hooks.match(/##\{pane_id\}\\t##\{pane_dead\}\\t##\{pane_pid\}/)?.[0] ?? '';
|
|
1084
|
+
const executablePaneFormat = storedPaneFormat.replaceAll('##{', '#{').replaceAll('\\t', '\t');
|
|
1085
|
+
assert.equal(executablePaneFormat, '#{pane_id}\t#{pane_dead}\t#{pane_pid}', 'one outer tmux format pass must decode the stored format into the executable nested argv');
|
|
1086
|
+
const clientAttachedHookSlots = [...hooks.matchAll(/client-attached\[[0-9]+\]/g)].map(([slot]) => slot);
|
|
1087
|
+
for (const hookSlot of clientAttachedHookSlots) {
|
|
1088
|
+
fixture.run(['set-hook', '-u', '-t', launchedSession, hookSlot]);
|
|
1089
|
+
}
|
|
1090
|
+
const installedHooks = fixture.run(['show-hooks', '-t', launchedSession]);
|
|
1091
|
+
const storedResizeHookSlots = [...installedHooks.matchAll(/client-resized\[[0-9]+\]/g)].map(([slot]) => slot);
|
|
1092
|
+
assert.equal(storedResizeHookSlots.length, 1, 'exactly one installed client-resized hook must remain for the natural resize trigger');
|
|
1093
|
+
await new Promise((resolve) => setTimeout(resolve, (HUD_RESIZE_RECONCILE_DELAY_SECONDS * 1_000) + 250));
|
|
1094
|
+
const launchPaneSnapshots = parseShimTmuxArgv(await readFile(shimLogPath, 'utf-8'))
|
|
1095
|
+
.filter((argv) => argv[0] === 'list-panes' && argv[1] === '-a');
|
|
1096
|
+
assert.ok(launchPaneSnapshots.length >= 2, 'the direct and delayed launch reconciliations must both complete before the stored-hook baseline is cleared');
|
|
1097
|
+
await assertShimLogQuiescent(shimLogPath, 250, 'launch and delayed reconciliation must settle before clearing the stored-hook baseline');
|
|
1098
|
+
await writeFile(shimLogPath, '');
|
|
1099
|
+
originalOwner = fixture.run(['show-options', '-v', '-t', launchedSession, '@omx_instance_id']);
|
|
1100
|
+
assert.ok(originalOwner, 'the detached session must retain its original pane owner before the denial trigger');
|
|
1101
|
+
fixture.run(['resize-pane', '-t', hud[0], '-y', '7']);
|
|
1102
|
+
assert.equal(Number(fixture.run(['display-message', '-p', '-t', hud[0], '#{pane_height}'])), 7, 'the detached HUD must be enlarged before the authority denial trigger');
|
|
1103
|
+
fixture.run(['set-option', '-t', launchedSession, '@omx_instance_id', 'foreign-owner']);
|
|
1104
|
+
await writeFile(shimLogPath, '');
|
|
1105
|
+
fixture.triggerClientResize(launchedSession, { cols: 122, rows: 42 });
|
|
1106
|
+
await new Promise((resolve) => setTimeout(resolve, (HUD_RESIZE_RECONCILE_DELAY_SECONDS * 1_000) + 250));
|
|
1107
|
+
await assertShimLogQuiescent(shimLogPath, 250, 'the stored client-resized hook must finish both its immediate and delayed passes before assertions');
|
|
1108
|
+
const paneSnapshots = parseShimTmuxArgv(await readFile(shimLogPath, 'utf-8'))
|
|
1109
|
+
.filter((argv) => argv[0] === 'list-panes' && argv[1] === '-a');
|
|
1110
|
+
assert.equal(paneSnapshots.length, 2, 'only the explicitly triggered stored client-resized hook may produce the post-baseline immediate and delayed pane snapshots');
|
|
1111
|
+
for (const argv of paneSnapshots) {
|
|
1112
|
+
assert.deepEqual(argv, ['list-panes', '-a', '-F', executablePaneFormat], 'post-baseline stored client-resized hook execution must preserve the exact executable TAB-delimited nested argv');
|
|
1113
|
+
}
|
|
1114
|
+
assert.equal(Number(fixture.run(['display-message', '-p', '-t', hud[0], '#{pane_height}'])), 7, 'the foreign pane owner must deny the deferred HUD mutation');
|
|
1115
|
+
assert.equal(fixture.run(['list-panes', '-t', foreignSession, '-F', '#{pane_id}\t#{pane_pid}\t#{pane_height}']), foreignBefore, 'the denial trigger must not affect a foreign session');
|
|
1116
|
+
}
|
|
1117
|
+
finally {
|
|
1118
|
+
if (launchedSession && originalOwner && fixture.sessionExists(launchedSession)) {
|
|
1119
|
+
fixture.run(['set-option', '-t', launchedSession, '@omx_instance_id', originalOwner]);
|
|
1120
|
+
assert.equal(fixture.run(['show-options', '-v', '-t', launchedSession, '@omx_instance_id']), originalOwner, 'the original detached pane owner must be restored before fixture teardown');
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
finally {
|
|
1126
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
1127
|
+
}
|
|
1128
|
+
});
|
|
1129
|
+
it('cleans a poisoned private tmux server before replaying a held child and Stop', async (t) => {
|
|
1130
|
+
if (!skipUnlessPrivateRealTmux(t))
|
|
1131
|
+
return;
|
|
1132
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-launch-private-poisoned-server-'));
|
|
1133
|
+
try {
|
|
1134
|
+
await withTempTmuxSession(async (fixture) => {
|
|
1135
|
+
const home = join(wd, 'home');
|
|
1136
|
+
const bin = join(wd, 'bin');
|
|
1137
|
+
const releasePath = join(wd, 'child-release');
|
|
1138
|
+
const capturePath = join(wd, 'child-b-env.log');
|
|
1139
|
+
const poisonRuns = join(wd, 'poison-runs');
|
|
1140
|
+
const shimLogPath = join(wd, 'private-tmux-shim.log');
|
|
1141
|
+
const poisonEnv = Object.fromEntries(DETACHED_LAUNCH_CONTROL_PLANE_KEYS.map((key) => [
|
|
1142
|
+
key,
|
|
1143
|
+
key === 'OMX_RUNS_DIR' ? poisonRuns : `poison-${key.toLowerCase()}`,
|
|
1144
|
+
]));
|
|
1145
|
+
await mkdir(home, { recursive: true });
|
|
1146
|
+
await mkdir(bin, { recursive: true });
|
|
1147
|
+
await writeFile(releasePath, 'hold\n');
|
|
1148
|
+
const captureLines = DETACHED_LAUNCH_CONTROL_PLANE_KEYS
|
|
1149
|
+
.map((key) => `printf '%s=%s\\n' '${key}' "$${key}"`)
|
|
1150
|
+
.join('\n');
|
|
1151
|
+
await writeExecutable(join(bin, 'codex'), `#!/bin/sh
|
|
1152
|
+
{
|
|
1153
|
+
${captureLines}
|
|
1154
|
+
} > ${JSON.stringify(capturePath)}
|
|
1155
|
+
while [ -f ${JSON.stringify(releasePath)} ]; do sleep 1; done
|
|
1156
|
+
`);
|
|
1157
|
+
await fixture.createPathShim(bin, shimLogPath);
|
|
1158
|
+
for (const key of DETACHED_LAUNCH_CONTROL_PLANE_KEYS) {
|
|
1159
|
+
fixture.run(['set-environment', '-g', key, poisonEnv[key]]);
|
|
1160
|
+
}
|
|
1161
|
+
const result = runOmx(wd, ['--madmax', '--tmux'], {
|
|
1162
|
+
...poisonEnv,
|
|
1163
|
+
HOME: home,
|
|
1164
|
+
PATH: `${bin}:${process.env.PATH ?? ''}`,
|
|
1165
|
+
OMX_AUTO_UPDATE: '0',
|
|
1166
|
+
OMX_NOTIFY_FALLBACK: '0',
|
|
1167
|
+
OMX_HOOK_DERIVED_SIGNALS: '0',
|
|
1168
|
+
OMX_HERMES_MCP_BRIDGE: '1',
|
|
1169
|
+
OMX_LAUNCH_POLICY: 'direct',
|
|
1170
|
+
TMUX: '',
|
|
1171
|
+
TMUX_PANE: '',
|
|
1172
|
+
});
|
|
1173
|
+
assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
|
|
1174
|
+
assert.doesNotMatch(`${result.stderr}\n${result.stdout}`, /scope|pointer|state root.*error/i);
|
|
1175
|
+
await waitForPath(capturePath, 1, 15_000);
|
|
1176
|
+
const captured = Object.fromEntries((await readFile(capturePath, 'utf-8')).trim().split('\n').map((line) => {
|
|
1177
|
+
const separator = line.indexOf('=');
|
|
1178
|
+
return [line.slice(0, separator), line.slice(separator + 1)];
|
|
1179
|
+
}));
|
|
1180
|
+
assert.deepEqual(Object.keys(captured), [...DETACHED_LAUNCH_CONTROL_PLANE_KEYS]);
|
|
1181
|
+
assert.match(captured.OMX_SESSION_ID ?? '', /^omx-/);
|
|
1182
|
+
assert.equal(captured.CODEX_SESSION_ID, '');
|
|
1183
|
+
assert.equal(captured.SESSION_ID, '');
|
|
1184
|
+
assert.equal(captured.OMX_TMUX_HUD_OWNER, '1');
|
|
1185
|
+
assert.equal(captured.OMX_TMUX_HUD_LEADER_PANE, '');
|
|
1186
|
+
assert.match(captured.OMX_ROOT ?? '', new RegExp(`^${escapeRegExp(poisonRuns)}/run-`));
|
|
1187
|
+
assert.equal(captured.OMX_STATE_ROOT, '');
|
|
1188
|
+
assert.equal(captured.OMX_TEAM_STATE_ROOT, '');
|
|
1189
|
+
assert.equal(captured.OMXBOX_ACTIVE, '1');
|
|
1190
|
+
assert.equal(captured.OMX_SOURCE_CWD, wd);
|
|
1191
|
+
assert.match(captured.OMX_MADMAX_DETACHED_CONTEXT ?? '', /^[0-9a-f]{32}$/);
|
|
1192
|
+
assert.equal(captured.OMX_RUNS_DIR, poisonRuns);
|
|
1193
|
+
assert.equal(captured.OMX_TEAM_LEADER_CWD, '');
|
|
1194
|
+
assert.equal(captured.OMX_TEAM_WORKER, '');
|
|
1195
|
+
assert.equal(captured.OMX_TEAM_INTERNAL_WORKER, '');
|
|
1196
|
+
const launchedSession = fixture.run(['list-sessions', '-F', '#{session_name}'])
|
|
1197
|
+
.split('\n')
|
|
1198
|
+
.find((name) => name.startsWith('omx-') && name !== fixture.sessionName);
|
|
1199
|
+
assert.ok(launchedSession, 'compiled launcher must create a detached session on the private server');
|
|
1200
|
+
assert.equal(fixture.run(['show-options', '-v', '-t', launchedSession, 'history-limit']), String(DETACHED_TMUX_HISTORY_LIMIT));
|
|
1201
|
+
const sessionJsonPath = join(captured.OMX_ROOT, '.omx', 'state', 'session.json');
|
|
1202
|
+
await waitForPath(sessionJsonPath);
|
|
1203
|
+
const sessionJsonBeforeStop = await readFile(sessionJsonPath, 'utf-8');
|
|
1204
|
+
const sessionStateBeforeStop = JSON.parse(sessionJsonBeforeStop);
|
|
1205
|
+
assert.equal(sessionStateBeforeStop.session_id, captured.OMX_SESSION_ID);
|
|
1206
|
+
assert.equal(sessionStateBeforeStop.tmux_session_name, launchedSession);
|
|
1207
|
+
assert.match(sessionStateBeforeStop.tmux_pane_id ?? '', /^%[0-9]+$/);
|
|
1208
|
+
const stop = spawnSync(process.execPath, [join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'scripts', 'codex-native-hook.js')], {
|
|
1209
|
+
cwd: wd,
|
|
1210
|
+
input: JSON.stringify({
|
|
1211
|
+
hook_event_name: 'Stop',
|
|
1212
|
+
source: 'codex-app',
|
|
1213
|
+
cwd: wd,
|
|
1214
|
+
session_id: captured.OMX_SESSION_ID,
|
|
1215
|
+
thread_id: 'poisoned-server-child-b',
|
|
1216
|
+
turn_id: 'stop-poisoned-server-child-b',
|
|
1217
|
+
}),
|
|
1218
|
+
encoding: 'utf-8',
|
|
1219
|
+
env: buildRunOmxEnv({
|
|
1220
|
+
...captured,
|
|
1221
|
+
HOME: home,
|
|
1222
|
+
PATH: `${bin}:${process.env.PATH ?? ''}`,
|
|
1223
|
+
OMX_AUTO_UPDATE: '0',
|
|
1224
|
+
OMX_NOTIFY_FALLBACK: '0',
|
|
1225
|
+
OMX_HOOK_DERIVED_SIGNALS: '0',
|
|
1226
|
+
TMUX: '',
|
|
1227
|
+
TMUX_PANE: '',
|
|
1228
|
+
}),
|
|
1229
|
+
});
|
|
1230
|
+
assert.equal(stop.status, 0, stop.stderr || stop.stdout);
|
|
1231
|
+
assert.doesNotMatch(`${stop.stderr}\n${stop.stdout}`, /scope|pointer|state root.*error/i);
|
|
1232
|
+
const stopOutput = JSON.parse(stop.stdout.trim());
|
|
1233
|
+
assert.deepEqual(stopOutput, {}, 'a clean held-child Stop must be a JSON no-op');
|
|
1234
|
+
assert.notEqual(stopOutput.decision, 'block');
|
|
1235
|
+
const sessionJsonAfterStop = await readFile(sessionJsonPath, 'utf-8');
|
|
1236
|
+
const sessionStateAfterStop = JSON.parse(sessionJsonAfterStop);
|
|
1237
|
+
assert.equal(sessionStateAfterStop.session_id, captured.OMX_SESSION_ID);
|
|
1238
|
+
assert.equal(sessionStateAfterStop.tmux_session_name, launchedSession);
|
|
1239
|
+
assert.equal(sessionJsonAfterStop, sessionJsonBeforeStop, 'a no-op Stop must preserve the live B session pointer');
|
|
1240
|
+
const activeRecordPath = join(captured.OMX_RUNS_DIR, 'active-detached', `${captured.OMX_MADMAX_DETACHED_CONTEXT}.json`);
|
|
1241
|
+
assert.equal(existsSync(activeRecordPath), true, 'leader must retain its active record while child B is live');
|
|
1242
|
+
await rm(releasePath, { force: true });
|
|
1243
|
+
for (let attempt = 0; attempt < 300 && fixture.sessionExists(launchedSession); attempt += 1) {
|
|
1244
|
+
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
1245
|
+
}
|
|
1246
|
+
assert.equal(fixture.sessionExists(launchedSession), false, 'released child must clean up its detached tmux session');
|
|
1247
|
+
await waitForPath(join(captured.OMX_ROOT, '.omx', 'logs', 'session-history.jsonl'));
|
|
1248
|
+
assert.equal(existsSync(activeRecordPath), false, 'released child must remove its active record');
|
|
1249
|
+
assert.match(await readFile(join(captured.OMX_ROOT, '.omx', 'logs', 'session-history.jsonl'), 'utf-8'), new RegExp(escapeRegExp(captured.OMX_SESSION_ID)));
|
|
1250
|
+
});
|
|
1251
|
+
}
|
|
1252
|
+
finally {
|
|
1253
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
1254
|
+
}
|
|
1255
|
+
});
|
|
1256
|
+
it('does not let the outer launcher fabricate leader-owned active records', async () => {
|
|
663
1257
|
const wd = await mkdtemp(join(tmpdir(), 'omx-launch-madmax-reuse-'));
|
|
664
1258
|
try {
|
|
665
1259
|
const runs = join(wd, 'runs');
|
|
1260
|
+
const madmaxRoot = join(wd, 'madmax-root');
|
|
1261
|
+
const madmaxContext = buildMadmaxDetachedLaunchContextKey(wd, ['--madmax', '--tmux'], madmaxRoot);
|
|
1262
|
+
await mkdir(madmaxRoot, { recursive: true });
|
|
1263
|
+
await writeFile(join(madmaxRoot, '.omxbox-run.json'), JSON.stringify({
|
|
1264
|
+
cwd: madmaxRoot,
|
|
1265
|
+
source_cwd: wd,
|
|
1266
|
+
detached_launch_context: madmaxContext,
|
|
1267
|
+
}));
|
|
666
1268
|
const activeMarker = join(wd, 'active-session');
|
|
667
1269
|
const instanceMarker = join(wd, 'active-instance');
|
|
668
1270
|
const { env, tmuxLogPath } = await createLaunchFixture(wd, (logPath) => `#!/bin/sh
|
|
@@ -689,6 +1291,12 @@ case "$1" in
|
|
|
689
1291
|
printf 'hud-pane\n'
|
|
690
1292
|
exit 0
|
|
691
1293
|
;;
|
|
1294
|
+
list-panes)
|
|
1295
|
+
session=$(cat "${activeMarker}")
|
|
1296
|
+
instance=$(cat "${instanceMarker}")
|
|
1297
|
+
printf '%%12\t0\t4242\t%s\t$1\t100\t%s\n' "$session" "$instance"
|
|
1298
|
+
exit 0
|
|
1299
|
+
;;
|
|
692
1300
|
display-message)
|
|
693
1301
|
if [ "$2" = '-p' ] && [ "$3" = '#{socket_path}' ]; then
|
|
694
1302
|
printf '/tmp/tmux-test.sock\n'
|
|
@@ -724,8 +1332,10 @@ exit 0
|
|
|
724
1332
|
const baseEnv = {
|
|
725
1333
|
...env,
|
|
726
1334
|
OMX_RUNS_DIR: runs,
|
|
1335
|
+
OMX_ROOT: madmaxRoot,
|
|
727
1336
|
OMXBOX_ACTIVE: '1',
|
|
728
|
-
|
|
1337
|
+
OMX_SOURCE_CWD: wd,
|
|
1338
|
+
OMX_MADMAX_DETACHED_CONTEXT: madmaxContext,
|
|
729
1339
|
OMX_LAUNCH_POLICY: 'direct',
|
|
730
1340
|
TMUX: '',
|
|
731
1341
|
TMUX_PANE: '',
|
|
@@ -738,27 +1348,34 @@ exit 0
|
|
|
738
1348
|
if (shouldSkipForSpawnPermissions(second.error))
|
|
739
1349
|
return;
|
|
740
1350
|
assert.equal(second.status, 0, second.error || second.stderr || second.stdout);
|
|
741
|
-
assert.match(second.stderr, /madmax detached launch already active for this context
|
|
1351
|
+
assert.match(second.stderr, /madmax detached launch already active for this context/);
|
|
742
1352
|
const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
|
|
1353
|
+
assert.match(tmuxLog, new RegExp(`-e OMX_MADMAX_DETACHED_CONTEXT=${escapeRegExp(madmaxContext)}`));
|
|
1354
|
+
assert.match(tmuxLog, new RegExp(`-e OMX_RUNS_DIR=${escapeRegExp(runs)}`));
|
|
743
1355
|
assert.equal((tmuxLog.match(/tmux:new-session/g) || []).length, 1);
|
|
744
|
-
assert.equal((tmuxLog.match(/tmux:has-session/g) || []).length,
|
|
1356
|
+
assert.equal((tmuxLog.match(/tmux:has-session/g) || []).length, 0);
|
|
1357
|
+
assert.ok((tmuxLog.match(/tmux:list-panes/g) || []).length >= 2);
|
|
745
1358
|
assert.equal((tmuxLog.match(/tmux:attach-session/g) || []).length, 2);
|
|
746
|
-
const
|
|
747
|
-
assert.match(
|
|
748
|
-
assert.match(
|
|
749
|
-
assert.match(
|
|
1359
|
+
const activeRecord = await readFile(join(runs, 'active-detached', `${madmaxContext}.json`), 'utf-8');
|
|
1360
|
+
assert.match(activeRecord, /"tmux_session_name"/);
|
|
1361
|
+
assert.match(activeRecord, /"session_id"/);
|
|
1362
|
+
assert.match(activeRecord, /"tmux_pane_id": "%12"/);
|
|
1363
|
+
assert.match(activeRecord, /"leader_pid"/);
|
|
1364
|
+
assert.match(activeRecord, /"base_state_root"/);
|
|
1365
|
+
assert.match(activeRecord, /"lifecycle_phase": "ready"/);
|
|
750
1366
|
}
|
|
751
1367
|
finally {
|
|
752
|
-
await rm(wd, { recursive: true, force: true });
|
|
1368
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
753
1369
|
}
|
|
754
1370
|
});
|
|
755
|
-
it('records boxed runtime identity
|
|
1371
|
+
it('records boxed runtime identity in the leader-owned active record', async () => {
|
|
756
1372
|
const wd = await mkdtemp(join(tmpdir(), 'omx-launch-madmax-worktree-detached-'));
|
|
757
1373
|
try {
|
|
758
1374
|
const repo = await createGitRepo(wd);
|
|
759
1375
|
const runs = join(wd, 'runs');
|
|
760
1376
|
const instanceMarker = join(wd, 'active-instance');
|
|
761
|
-
const
|
|
1377
|
+
const sessionMarker = join(wd, 'active-session-name');
|
|
1378
|
+
const { env, tmuxLogPath, leaderDonePath } = await createLaunchFixture(wd, (logPath) => `#!/bin/sh
|
|
762
1379
|
printf 'tmux:%s\n' "$*" >> "${logPath}"
|
|
763
1380
|
case "$1" in
|
|
764
1381
|
-V)
|
|
@@ -769,13 +1386,25 @@ case "$1" in
|
|
|
769
1386
|
exit 1
|
|
770
1387
|
;;
|
|
771
1388
|
new-session)
|
|
1389
|
+
prev=''
|
|
1390
|
+
for arg in "$@"; do
|
|
1391
|
+
if [ "$prev" = '-s' ]; then printf '%s' "$arg" > "${sessionMarker}"; fi
|
|
1392
|
+
prev="$arg"
|
|
1393
|
+
done
|
|
772
1394
|
printf '%%77\n'
|
|
773
1395
|
exit 0
|
|
774
1396
|
;;
|
|
1397
|
+
|
|
775
1398
|
split-window)
|
|
776
1399
|
printf '%%78\n'
|
|
777
1400
|
exit 0
|
|
778
1401
|
;;
|
|
1402
|
+
list-panes)
|
|
1403
|
+
instance=$(cat "${instanceMarker}")
|
|
1404
|
+
printf '%%77\t0\t4242\t%s\t$1\t100\t%s\n' "$(cat "${sessionMarker}")" "$instance"
|
|
1405
|
+
exit 0
|
|
1406
|
+
;;
|
|
1407
|
+
|
|
779
1408
|
display-message)
|
|
780
1409
|
if [ "$2" = '-p' ] && [ "$3" = '#{socket_path}' ]; then
|
|
781
1410
|
printf '/tmp/tmux-test.sock\n'
|
|
@@ -821,19 +1450,17 @@ exit 0
|
|
|
821
1450
|
assert.equal(activeFiles.length, 1);
|
|
822
1451
|
const activeRecord = JSON.parse(await readFile(join(runs, 'active-detached', activeFiles[0]), 'utf-8'));
|
|
823
1452
|
const worktreePath = join(dirname(repo), `${basename(repo)}.omx-worktrees`, 'launch-detached');
|
|
824
|
-
assert.
|
|
825
|
-
assert.equal(
|
|
826
|
-
assert.equal(
|
|
827
|
-
assert.equal(activeRecord.session_id.startsWith('omx-'), true);
|
|
828
|
-
assert.equal(activeRecord.tmux_pane_id, '%77');
|
|
1453
|
+
assert.equal(normalizeDarwinTmpPath(activeRecord.source_cwd), normalizeDarwinTmpPath(worktreePath));
|
|
1454
|
+
assert.equal(typeof activeRecord.leader_pid, 'number');
|
|
1455
|
+
assert.equal(activeRecord.lifecycle_phase, 'ready');
|
|
829
1456
|
const tmuxLog = normalizeDarwinTmpPath(await readFile(tmuxLogPath, 'utf-8'));
|
|
830
|
-
assert.match(tmuxLog,
|
|
1457
|
+
assert.match(tmuxLog, /__detached-session-leader/);
|
|
831
1458
|
assert.match(tmuxLog, /-e OMXBOX_ACTIVE=1/);
|
|
832
1459
|
assert.match(tmuxLog, new RegExp(`-e OMX_SOURCE_CWD=${escapeRegExp(normalizeDarwinTmpPath(repo))}`));
|
|
833
|
-
|
|
1460
|
+
await waitForPath(leaderDonePath);
|
|
834
1461
|
}
|
|
835
1462
|
finally {
|
|
836
|
-
await rm(wd, { recursive: true, force: true });
|
|
1463
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
837
1464
|
}
|
|
838
1465
|
});
|
|
839
1466
|
it('does not mutate a stale active-detached tmux session without OMX ownership proof', async () => {
|
|
@@ -853,7 +1480,7 @@ exit 0
|
|
|
853
1480
|
session_id: 'expected-omx-session-id',
|
|
854
1481
|
tmux_pane_id: '%99',
|
|
855
1482
|
})}\n`);
|
|
856
|
-
const { env, tmuxLogPath } = await createLaunchFixture(wd, (logPath) => `#!/bin/sh
|
|
1483
|
+
const { env, tmuxLogPath, leaderDonePath } = await createLaunchFixture(wd, (logPath) => `#!/bin/sh
|
|
857
1484
|
printf 'tmux:%s\n' "$*" >> "${logPath}"
|
|
858
1485
|
case "$1" in
|
|
859
1486
|
-V)
|
|
@@ -907,29 +1534,35 @@ exit 0
|
|
|
907
1534
|
if (shouldSkipForSpawnPermissions(result.error))
|
|
908
1535
|
return;
|
|
909
1536
|
assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
|
|
1537
|
+
await waitForPath(leaderDonePath);
|
|
910
1538
|
const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
|
|
911
1539
|
assert.doesNotMatch(tmuxLog, /tmux:set-option .* -t user-owned-session .*history-limit/);
|
|
912
1540
|
assert.doesNotMatch(tmuxLog, /tmux:clear-history .*user-owned-session|tmux:clear-history .*%99/);
|
|
913
1541
|
assert.match(tmuxLog, /tmux:new-session /);
|
|
914
1542
|
}
|
|
915
1543
|
finally {
|
|
916
|
-
await rm(wd, { recursive: true, force: true });
|
|
1544
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
917
1545
|
}
|
|
918
1546
|
});
|
|
919
1547
|
it('does not reuse the same active-detached lock for independent --madmax --high launches', async () => {
|
|
920
1548
|
const wd = await mkdtemp(join(tmpdir(), 'omx-launch-madmax-independent-high-'));
|
|
921
1549
|
try {
|
|
922
1550
|
const runs = join(wd, 'runs');
|
|
1551
|
+
const sessionMarker = join(wd, 'independent-session');
|
|
1552
|
+
const instanceMarker = join(wd, 'independent-instance');
|
|
923
1553
|
const { env, tmuxLogPath } = await createLaunchFixture(wd, (logPath) => `#!/bin/sh
|
|
924
1554
|
printf 'tmux:%s\n' "$*" >> "${logPath}"
|
|
925
1555
|
case "$1" in
|
|
926
1556
|
-V) printf 'tmux 3.4\n'; exit 0 ;;
|
|
927
1557
|
has-session) exit 1 ;;
|
|
928
|
-
new-session) printf '%%12\n'; exit 0 ;;
|
|
1558
|
+
new-session) prev=''; for arg in "$@"; do if [ "$prev" = '-s' ]; then printf '%s' "$arg" > "${sessionMarker}"; fi; prev="$arg"; done; printf '%%12\n'; exit 0 ;;
|
|
1559
|
+
|
|
929
1560
|
split-window) printf 'hud-pane\n'; exit 0 ;;
|
|
930
1561
|
display-message) if [ "$2" = '-p' ] && [ "$3" = '#{socket_path}' ]; then printf '/tmp/tmux-test.sock\n'; else printf '0\n'; fi; exit 0 ;;
|
|
1562
|
+
list-panes) printf '%%12\t0\t4242\t%s\t$1\t100\t%s\n' "$(cat "${sessionMarker}")" "$(cat "${instanceMarker}")"; exit 0 ;;
|
|
931
1563
|
show-options) printf 'off\n'; exit 0 ;;
|
|
932
|
-
set-option
|
|
1564
|
+
set-option) if [ "$4" = '@omx_instance_id' ]; then printf '%s' "$5" > "${instanceMarker}"; fi; exit 0 ;;
|
|
1565
|
+
set-hook|attach-session|kill-session|run-shell|resize-pane) exit 0 ;;
|
|
933
1566
|
esac
|
|
934
1567
|
exit 0
|
|
935
1568
|
`);
|
|
@@ -954,13 +1587,14 @@ exit 0
|
|
|
954
1587
|
.map((line) => JSON.parse(line));
|
|
955
1588
|
assert.equal(registryEntries.length, 2);
|
|
956
1589
|
assert.notEqual(registryEntries[0].detached_launch_context, registryEntries[1].detached_launch_context, 'independent launches must get distinct active-detached lock identities');
|
|
957
|
-
|
|
958
|
-
assert.equal(
|
|
1590
|
+
const activeDetachedEntries = await readdir(join(runs, 'active-detached'));
|
|
1591
|
+
assert.equal(activeDetachedEntries.filter((entry) => entry.endsWith('.json')).length, 2);
|
|
1592
|
+
assert.equal(activeDetachedEntries.filter((entry) => entry.endsWith('.lock')).length, 0);
|
|
959
1593
|
const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
|
|
960
1594
|
assert.equal((tmuxLog.match(/tmux:new-session/g) || []).length, 2);
|
|
961
1595
|
}
|
|
962
1596
|
finally {
|
|
963
|
-
await rm(wd, { recursive: true, force: true });
|
|
1597
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
964
1598
|
}
|
|
965
1599
|
});
|
|
966
1600
|
it('allows distinct madmax detached launch contexts to create separate sessions', async () => {
|
|
@@ -997,7 +1631,44 @@ exit 0
|
|
|
997
1631
|
assert.equal((tmuxLog.match(/tmux:new-session/g) || []).length, 2);
|
|
998
1632
|
}
|
|
999
1633
|
finally {
|
|
1000
|
-
await rm(wd, { recursive: true, force: true });
|
|
1634
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
1635
|
+
}
|
|
1636
|
+
});
|
|
1637
|
+
it('denies detached HUD finalization when the receipted pane id is recycled', async () => {
|
|
1638
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-launch-detached-hud-recycle-'));
|
|
1639
|
+
try {
|
|
1640
|
+
const { env, tmuxLogPath } = await createLaunchFixture(wd, (logPath) => `#!/bin/sh
|
|
1641
|
+
printf 'tmux:%s\n' "$*" >> "${logPath}"
|
|
1642
|
+
case "$1" in
|
|
1643
|
+
-V) printf 'tmux 3.4\n'; exit 0 ;;
|
|
1644
|
+
has-session) exit 1 ;;
|
|
1645
|
+
new-session) printf '%%12\n'; exit 0 ;;
|
|
1646
|
+
split-window) printf '%%99\n'; exit 0 ;;
|
|
1647
|
+
display-message) printf '0\n'; exit 0 ;;
|
|
1648
|
+
show-options) printf 'off\n'; exit 0 ;;
|
|
1649
|
+
set-option|set-hook|attach-session|kill-session|run-shell|resize-pane) exit 0 ;;
|
|
1650
|
+
esac
|
|
1651
|
+
exit 0
|
|
1652
|
+
`);
|
|
1653
|
+
const result = runOmx(wd, ['--madmax', '--tmux'], {
|
|
1654
|
+
...env,
|
|
1655
|
+
OMX_LAUNCH_POLICY: 'direct',
|
|
1656
|
+
OMX_TEST_DETACHED_RECYCLE: 'finalization',
|
|
1657
|
+
TMUX: '',
|
|
1658
|
+
TMUX_PANE: '',
|
|
1659
|
+
});
|
|
1660
|
+
if (shouldSkipForSpawnPermissions(result.error))
|
|
1661
|
+
return;
|
|
1662
|
+
assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
|
|
1663
|
+
const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
|
|
1664
|
+
assert.match(tmuxLog, /tmux:if-shell -F -t %12 .*OMX_DETACHED_HUD_OPERATION=/);
|
|
1665
|
+
assert.match(tmuxLog, /if-shell -F -t '%99' .*456.*\$1.*@1.*OMX_DETACHED_HUD_OPERATION=/);
|
|
1666
|
+
assert.match(tmuxLog, /^tmux:__nested_hud_guard_denied__ %99 999 \$2 @2$/m);
|
|
1667
|
+
assert.doesNotMatch(tmuxLog, /run-shell -b ['"]run-shell -b /);
|
|
1668
|
+
assert.doesNotMatch(tmuxLog, /^tmux:resize-pane .*%99/m);
|
|
1669
|
+
}
|
|
1670
|
+
finally {
|
|
1671
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
1001
1672
|
}
|
|
1002
1673
|
});
|
|
1003
1674
|
it('launches --madmax through explicitly requested detached tmux so HUD bootstrap can run', async () => {
|
|
@@ -1049,6 +1720,7 @@ esac
|
|
|
1049
1720
|
exit 0
|
|
1050
1721
|
`);
|
|
1051
1722
|
await chmod(fakeTmuxPath, 0o755);
|
|
1723
|
+
await wrapFakeTmuxWithDetachedLeader(fakeTmuxPath);
|
|
1052
1724
|
const result = runOmx(wd, ['--madmax', '--tmux'], {
|
|
1053
1725
|
HOME: home,
|
|
1054
1726
|
PATH: `${fakeBin}:/usr/bin:/bin`,
|
|
@@ -1065,14 +1737,15 @@ exit 0
|
|
|
1065
1737
|
assert.doesNotMatch(tmuxLog, /tmux:show-options -gv history-limit/);
|
|
1066
1738
|
assert.doesNotMatch(tmuxLog, /tmux:set-option -g[q ]+history-limit/);
|
|
1067
1739
|
assert.match(tmuxLog, /tmux:new-session .* -s /);
|
|
1068
|
-
assert.match(tmuxLog, new RegExp(`tmux:
|
|
1069
|
-
assert.match(tmuxLog, new RegExp(`tmux:
|
|
1070
|
-
assert.match(tmuxLog, /tmux:
|
|
1071
|
-
assert.match(tmuxLog, new RegExp(`tmux:
|
|
1740
|
+
assert.match(tmuxLog, new RegExp(`tmux:if-shell -F -t %12 .*set-option.*history-limit.*${DETACHED_TMUX_HISTORY_LIMIT}`));
|
|
1741
|
+
assert.match(tmuxLog, new RegExp(`tmux:if-shell -F -t %12 .*set-option.*-pq.*%12.*history-limit.*${DETACHED_TMUX_HISTORY_LIMIT}`));
|
|
1742
|
+
assert.match(tmuxLog, /tmux:if-shell -F -t %12 .*set-hook.*client-detached\[[0-9]+\].*clear-history -t %12/);
|
|
1743
|
+
assert.match(tmuxLog, new RegExp(`tmux:if-shell -F -t %12 .*split-window.*-v.*-l.*${HUD_TMUX_HEIGHT_LINES}.*-t.*%12`));
|
|
1072
1744
|
assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
|
|
1745
|
+
await waitForPath(`${fakeTmuxPath}.leader-done`);
|
|
1073
1746
|
}
|
|
1074
1747
|
finally {
|
|
1075
|
-
await rm(wd, { recursive: true, force: true });
|
|
1748
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
1076
1749
|
}
|
|
1077
1750
|
});
|
|
1078
1751
|
it('preserves parent provider env without replaying terminal state over an OMX-created tmux pane', async () => {
|
|
@@ -1124,8 +1797,8 @@ case "$1" in
|
|
|
1124
1797
|
TERMINFO=/tmp/server-terminfo \
|
|
1125
1798
|
TERMINFO_DIRS=/tmp/server-terminfo-dirs \
|
|
1126
1799
|
TERMCAP=server-termcap \
|
|
1127
|
-
sh -c "$last"
|
|
1128
|
-
printf '
|
|
1800
|
+
nohup /bin/sh -c "$last" </dev/null >/tmp/omx-test-provider-leader.log 2>&1 &
|
|
1801
|
+
printf '%%12\n'
|
|
1129
1802
|
exit 0
|
|
1130
1803
|
;;
|
|
1131
1804
|
split-window)
|
|
@@ -1150,6 +1823,7 @@ case "$1" in
|
|
|
1150
1823
|
esac
|
|
1151
1824
|
exit 0
|
|
1152
1825
|
`);
|
|
1826
|
+
await wrapFakeTmuxWithDetachedLeader(join(fakeBin, 'tmux'));
|
|
1153
1827
|
const result = runOmx(wd, ['--tmux', '--madmax'], {
|
|
1154
1828
|
HOME: home,
|
|
1155
1829
|
PATH: `${fakeBin}:/usr/bin:/bin`,
|
|
@@ -1173,28 +1847,12 @@ exit 0
|
|
|
1173
1847
|
if (shouldSkipForSpawnPermissions(result.error))
|
|
1174
1848
|
return;
|
|
1175
1849
|
assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
|
|
1176
|
-
assert.equal(await readFile(envLogPath, 'utf-8'), [
|
|
1177
|
-
'custom=fake-provider-key',
|
|
1178
|
-
'marker=1',
|
|
1179
|
-
'term=tmux-256color',
|
|
1180
|
-
'term_program=tmux',
|
|
1181
|
-
'term_program_version=3.4',
|
|
1182
|
-
'colorterm=truecolor',
|
|
1183
|
-
'tmux=/tmp/tmux-test.sock,123,0',
|
|
1184
|
-
'tmux_pane=%12',
|
|
1185
|
-
'columns=211',
|
|
1186
|
-
'lines=77',
|
|
1187
|
-
'terminfo=/tmp/outer-terminfo',
|
|
1188
|
-
'terminfo_dirs=/tmp/outer-terminfo-dirs',
|
|
1189
|
-
'termcap=outer-termcap',
|
|
1190
|
-
'',
|
|
1191
|
-
].join('\n'));
|
|
1192
1850
|
const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
|
|
1193
|
-
assert.
|
|
1194
|
-
assert.doesNotMatch(tmuxLog, /CUSTOM_LLM_API_KEY=/);
|
|
1851
|
+
assert.match(tmuxLog, /__detached-session-leader/);
|
|
1852
|
+
assert.doesNotMatch(tmuxLog, /fake-provider-key|CUSTOM_LLM_API_KEY=/);
|
|
1195
1853
|
}
|
|
1196
1854
|
finally {
|
|
1197
|
-
await rm(wd, { recursive: true, force: true });
|
|
1855
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
1198
1856
|
}
|
|
1199
1857
|
});
|
|
1200
1858
|
it('launches directly with --direct and skips detached tmux bootstrap', async () => {
|
|
@@ -1222,7 +1880,7 @@ exit 0
|
|
|
1222
1880
|
assert.doesNotMatch(tmuxLog, /new-session|split-window|attach-session/);
|
|
1223
1881
|
}
|
|
1224
1882
|
finally {
|
|
1225
|
-
await rm(wd, { recursive: true, force: true });
|
|
1883
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
1226
1884
|
}
|
|
1227
1885
|
});
|
|
1228
1886
|
it('launches directly from OMX_LAUNCH_POLICY=direct and skips detached tmux bootstrap', async () => {
|
|
@@ -1246,7 +1904,7 @@ exit 0
|
|
|
1246
1904
|
assert.doesNotMatch(tmuxLog, /new-session|split-window|attach-session/);
|
|
1247
1905
|
}
|
|
1248
1906
|
finally {
|
|
1249
|
-
await rm(wd, { recursive: true, force: true });
|
|
1907
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
1250
1908
|
}
|
|
1251
1909
|
});
|
|
1252
1910
|
it('launches directly inside tmux with --direct and skips HUD/mouse/extended-key tmux calls', async () => {
|
|
@@ -1269,35 +1927,68 @@ exit 0
|
|
|
1269
1927
|
assert.doesNotMatch(tmuxLog, /split-window|show-options|extended-keys|mouse on/);
|
|
1270
1928
|
}
|
|
1271
1929
|
finally {
|
|
1272
|
-
await rm(wd, { recursive: true, force: true });
|
|
1930
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
1273
1931
|
}
|
|
1274
1932
|
});
|
|
1275
|
-
it('
|
|
1933
|
+
it('fails closed on an incomplete synthetic HUD split source inside tmux', async () => {
|
|
1276
1934
|
const wd = await mkdtemp(join(tmpdir(), 'omx-launch-inside-tmux-managed-'));
|
|
1277
1935
|
try {
|
|
1278
1936
|
const { env, tmuxLogPath } = await createLaunchFixture(wd, (tmuxLogPath) => `#!/bin/sh
|
|
1937
|
+
state="${tmuxLogPath}.hud-created"
|
|
1938
|
+
proof="${tmuxLogPath}.hud-proof"
|
|
1939
|
+
marker="${tmuxLogPath}.hud-marker"
|
|
1279
1940
|
printf 'tmux:%s\n' "$*" >> "${tmuxLogPath}"
|
|
1280
1941
|
case "$1" in
|
|
1281
1942
|
list-panes)
|
|
1943
|
+
case "$*" in
|
|
1944
|
+
*'#{pane_id}'*'#{pane_start_command}'*)
|
|
1945
|
+
if [ -f "$state" ]; then printf '%%1\tcodex\n%%2\tOMX_TMUX_SPLIT_OPERATION_MARKER='\''%s'\''; export OMX_TMUX_SPLIT_OPERATION_MARKER; exec env OMX_TMUX_HUD_OWNER=1 OMX_TMUX_HUD_LEADER_PANE='\''%%1'\'' node hud\n' "$(cat "$marker")"; else printf '%%1\tcodex\n'; fi
|
|
1946
|
+
;;
|
|
1947
|
+
*'#{pane_id}\t#{pane_dead}\t#{pane_pid}'*)
|
|
1948
|
+
printf '%%1\t0\t101\n%%2\t0\t202\n'
|
|
1949
|
+
;;
|
|
1950
|
+
*'#{pane_id}'*)
|
|
1951
|
+
if [ -f "$state" ]; then printf '%%1\n%%2\n'; else printf '%%1\n'; fi
|
|
1952
|
+
;;
|
|
1953
|
+
esac
|
|
1282
1954
|
exit 0
|
|
1283
1955
|
;;
|
|
1284
1956
|
split-window)
|
|
1285
|
-
printf '
|
|
1957
|
+
printf '%%2\n'
|
|
1958
|
+
exit 0
|
|
1959
|
+
;;
|
|
1960
|
+
if-shell)
|
|
1961
|
+
[ "$2" = '-F' ] && [ "$3" = '-t' ] && [ "$4" = '%1' ] || exit 1
|
|
1962
|
+
case "$5" in *'#{pane_pid},101'*'#{session_id},$7'*'#{window_id},@1'*) ;; *) exit 1 ;; esac
|
|
1963
|
+
success="$6"
|
|
1964
|
+
receipt=\${success##*display-message -p }
|
|
1965
|
+
receipt=\${receipt%% *}
|
|
1966
|
+
split_marker=$(printf '%s' "$success" | sed -n "s/.*OMX_TMUX_SPLIT_OPERATION_MARKER='\\([^']*\\)'.*/\\1/p")
|
|
1967
|
+
[ -n "$split_marker" ] || exit 1
|
|
1968
|
+
printf '%s' "$split_marker" > "$marker"
|
|
1969
|
+
: > "$state"
|
|
1970
|
+
printf '%s\n' "$receipt"
|
|
1286
1971
|
exit 0
|
|
1287
1972
|
;;
|
|
1288
1973
|
display-message)
|
|
1289
1974
|
case "$*" in
|
|
1290
1975
|
*'#{socket_path}'*) printf '/tmp/tmux-test.sock\n' ;;
|
|
1976
|
+
*'#{pane_id}'*'#{pane_dead}'*'#{pane_pid}'*'#{session_id}'*'#{window_id}'*) printf '%%1\t0\t101\t$7\t@1\n' ;;
|
|
1977
|
+
*'#{session_id}'*'#{window_id}'*) printf '$7\t@1\n' ;;
|
|
1291
1978
|
*'#S'*) printf 'managed-session\n' ;;
|
|
1292
1979
|
*) printf '0\n' ;;
|
|
1293
1980
|
esac
|
|
1294
1981
|
exit 0
|
|
1295
1982
|
;;
|
|
1296
1983
|
show-options)
|
|
1297
|
-
printf 'off\n'
|
|
1984
|
+
if [ "$2" = '-g' ] && [ "$3" = '-v' ]; then cat "$proof" 2>/dev/null || true; else printf 'off\n'; fi
|
|
1298
1985
|
exit 0
|
|
1299
1986
|
;;
|
|
1300
|
-
set-option
|
|
1987
|
+
set-option)
|
|
1988
|
+
if [ "$2" = '-g' ]; then printf '%s\n' "$4" > "$proof"; fi
|
|
1989
|
+
exit 0
|
|
1990
|
+
;;
|
|
1991
|
+
kill-pane)
|
|
1301
1992
|
exit 0
|
|
1302
1993
|
;;
|
|
1303
1994
|
esac
|
|
@@ -1313,15 +2004,15 @@ exit 0
|
|
|
1313
2004
|
const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
|
|
1314
2005
|
assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
|
|
1315
2006
|
assert.match(result.stdout, /fake-codex:.*--dangerously-bypass-approvals-and-sandbox/);
|
|
1316
|
-
assert.
|
|
2007
|
+
assert.doesNotMatch(tmuxLog, /tmux:if-shell -F -t %1 .*split-window/);
|
|
1317
2008
|
assert.match(tmuxLog, /tmux:set-option -t managed-session mouse on/);
|
|
1318
2009
|
assert.match(tmuxLog, /tmux:set-option -sq extended-keys always/);
|
|
1319
2010
|
}
|
|
1320
2011
|
finally {
|
|
1321
|
-
await rm(wd, { recursive: true, force: true });
|
|
2012
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
1322
2013
|
}
|
|
1323
2014
|
});
|
|
1324
|
-
it('
|
|
2015
|
+
it('does not probe an existing tmux server before detached tmux startup', async () => {
|
|
1325
2016
|
const wd = await mkdtemp(join(tmpdir(), 'omx-launch-tmux-missing-socket-'));
|
|
1326
2017
|
try {
|
|
1327
2018
|
const home = join(wd, 'home');
|
|
@@ -1343,12 +2034,9 @@ case "$1" in
|
|
|
1343
2034
|
printf 'tmux 3.6a\n'
|
|
1344
2035
|
exit 0
|
|
1345
2036
|
;;
|
|
1346
|
-
|
|
1347
|
-
printf 'error connecting to /private/tmp/tmux-501/default (No such file or directory)\n' >&2
|
|
1348
|
-
exit 1
|
|
1349
|
-
;;
|
|
2037
|
+
|
|
1350
2038
|
new-session)
|
|
1351
|
-
printf '
|
|
2039
|
+
printf '%%12\n'
|
|
1352
2040
|
exit 0
|
|
1353
2041
|
;;
|
|
1354
2042
|
split-window)
|
|
@@ -1374,6 +2062,7 @@ esac
|
|
|
1374
2062
|
exit 0
|
|
1375
2063
|
`);
|
|
1376
2064
|
await chmod(fakeTmuxPath, 0o755);
|
|
2065
|
+
await wrapFakeTmuxWithDetachedLeader(fakeTmuxPath);
|
|
1377
2066
|
const result = runOmx(wd, ['--madmax', '--tmux'], {
|
|
1378
2067
|
HOME: home,
|
|
1379
2068
|
PATH: `${fakeBin}:/usr/bin:/bin`,
|
|
@@ -1387,15 +2076,15 @@ exit 0
|
|
|
1387
2076
|
return;
|
|
1388
2077
|
const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
|
|
1389
2078
|
assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
|
|
1390
|
-
assert.
|
|
2079
|
+
assert.doesNotMatch(tmuxLog, /tmux:list-sessions/);
|
|
1391
2080
|
assert.match(tmuxLog, /tmux:new-session .* -s /);
|
|
1392
2081
|
assert.doesNotMatch(result.stderr, /server\/socket is unusable/);
|
|
1393
2082
|
}
|
|
1394
2083
|
finally {
|
|
1395
|
-
await rm(wd, { recursive: true, force: true });
|
|
2084
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
1396
2085
|
}
|
|
1397
2086
|
});
|
|
1398
|
-
it('
|
|
2087
|
+
it('does not fall back directly when a later detached tmux operation fails', async () => {
|
|
1399
2088
|
const wd = await mkdtemp(join(tmpdir(), 'omx-launch-tmux-stale-socket-'));
|
|
1400
2089
|
try {
|
|
1401
2090
|
const home = join(wd, 'home');
|
|
@@ -1404,6 +2093,7 @@ exit 0
|
|
|
1404
2093
|
const fakePsPath = join(fakeBin, 'ps');
|
|
1405
2094
|
const fakeTmuxPath = join(fakeBin, 'tmux');
|
|
1406
2095
|
const tmuxLogPath = join(wd, 'tmux.log');
|
|
2096
|
+
const runsPath = join(wd, 'runs');
|
|
1407
2097
|
await mkdir(home, { recursive: true });
|
|
1408
2098
|
await mkdir(fakeBin, { recursive: true });
|
|
1409
2099
|
await writeFile(fakeCodexPath, '#!/bin/sh\nprintf \'fake-codex:%s\\n\' "$*"\n');
|
|
@@ -1414,40 +2104,49 @@ exit 0
|
|
|
1414
2104
|
printf 'tmux:%s\n' "$*" >> "${tmuxLogPath}"
|
|
1415
2105
|
case "$1" in
|
|
1416
2106
|
-V)
|
|
2107
|
+
printf 'tmux:d0-runs:%s\n' "$(test -e "${runsPath}" && printf present || printf missing)" >> "${tmuxLogPath}"
|
|
2108
|
+
|
|
1417
2109
|
printf 'tmux 3.6a\n'
|
|
1418
2110
|
exit 0
|
|
1419
2111
|
;;
|
|
1420
|
-
|
|
1421
|
-
printf '
|
|
2112
|
+
new-session)
|
|
2113
|
+
printf 'unsafe-tmux-handle-secret\n' >&2
|
|
1422
2114
|
exit 1
|
|
1423
2115
|
;;
|
|
2116
|
+
|
|
1424
2117
|
esac
|
|
1425
2118
|
printf 'unexpected tmux command: %s\n' "$*" >&2
|
|
1426
2119
|
exit 1
|
|
1427
2120
|
`);
|
|
1428
2121
|
await chmod(fakeTmuxPath, 0o755);
|
|
2122
|
+
await wrapFakeTmuxWithDetachedLeader(fakeTmuxPath);
|
|
1429
2123
|
const result = runOmx(wd, ['--madmax', '--tmux'], {
|
|
1430
2124
|
HOME: home,
|
|
1431
2125
|
PATH: `${fakeBin}:/usr/bin:/bin`,
|
|
1432
2126
|
OMX_AUTO_UPDATE: '0',
|
|
1433
2127
|
OMX_NOTIFY_FALLBACK: '0',
|
|
1434
2128
|
OMX_HOOK_DERIVED_SIGNALS: '0',
|
|
2129
|
+
OMX_RUNS_DIR: runsPath,
|
|
1435
2130
|
TMUX: '',
|
|
1436
2131
|
TMUX_PANE: '',
|
|
1437
2132
|
});
|
|
1438
2133
|
if (shouldSkipForSpawnPermissions(result.error))
|
|
1439
2134
|
return;
|
|
1440
2135
|
const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
|
|
1441
|
-
assert.equal(result.status,
|
|
1442
|
-
assert.match(result.
|
|
1443
|
-
assert.
|
|
1444
|
-
assert.doesNotMatch(
|
|
2136
|
+
assert.equal(result.status, 1, result.error || result.stderr || result.stdout);
|
|
2137
|
+
assert.match(result.stderr, /detached launch safety failure during inert-session/);
|
|
2138
|
+
assert.doesNotMatch(result.stderr, /unsafe-tmux-handle-secret/);
|
|
2139
|
+
assert.doesNotMatch(result.stdout, /fake-codex/);
|
|
2140
|
+
assert.match(tmuxLog, /tmux:-V/);
|
|
2141
|
+
assert.match(tmuxLog, /tmux:d0-runs:missing/);
|
|
2142
|
+
assert.match(tmuxLog, /tmux:new-session/);
|
|
2143
|
+
assert.doesNotMatch(tmuxLog, /tmux:list-sessions|attach-session/);
|
|
1445
2144
|
}
|
|
1446
2145
|
finally {
|
|
1447
|
-
await rm(wd, { recursive: true, force: true });
|
|
2146
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
1448
2147
|
}
|
|
1449
2148
|
});
|
|
1450
|
-
it('
|
|
2149
|
+
it('preserves detached ownership when attaching the released tmux session fails', async () => {
|
|
1451
2150
|
const wd = await mkdtemp(join(tmpdir(), 'omx-launch-tmux-attach-fail-'));
|
|
1452
2151
|
try {
|
|
1453
2152
|
const home = join(wd, 'home');
|
|
@@ -1469,7 +2168,7 @@ case "$1" in
|
|
|
1469
2168
|
exit 0
|
|
1470
2169
|
;;
|
|
1471
2170
|
new-session)
|
|
1472
|
-
printf '
|
|
2171
|
+
printf '%%12\n'
|
|
1473
2172
|
exit 0
|
|
1474
2173
|
;;
|
|
1475
2174
|
split-window)
|
|
@@ -1499,6 +2198,7 @@ esac
|
|
|
1499
2198
|
exit 0
|
|
1500
2199
|
`);
|
|
1501
2200
|
await chmod(fakeTmuxPath, 0o755);
|
|
2201
|
+
await wrapFakeTmuxWithDetachedLeader(fakeTmuxPath);
|
|
1502
2202
|
const result = runOmx(wd, ['--madmax', '--tmux'], {
|
|
1503
2203
|
HOME: home,
|
|
1504
2204
|
PATH: `${fakeBin}:/usr/bin:/bin`,
|
|
@@ -1510,17 +2210,18 @@ exit 0
|
|
|
1510
2210
|
});
|
|
1511
2211
|
if (shouldSkipForSpawnPermissions(result.error))
|
|
1512
2212
|
return;
|
|
2213
|
+
await waitForPath(`${fakeTmuxPath}.leader-done`);
|
|
1513
2214
|
const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
|
|
1514
|
-
assert.equal(result.status,
|
|
1515
|
-
assert.match(result.
|
|
2215
|
+
assert.equal(result.status, 1, result.error || result.stderr || result.stdout);
|
|
2216
|
+
assert.match(result.stderr, /detached launch safety failure during post-release-attach/);
|
|
1516
2217
|
assert.match(tmuxLog, /tmux:attach-session -t /);
|
|
1517
|
-
assert.
|
|
2218
|
+
assert.doesNotMatch(tmuxLog, /tmux:kill-session -t /);
|
|
1518
2219
|
}
|
|
1519
2220
|
finally {
|
|
1520
|
-
await rm(wd, { recursive: true, force: true });
|
|
2221
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
1521
2222
|
}
|
|
1522
2223
|
});
|
|
1523
|
-
it('
|
|
2224
|
+
it('does not probe or fall back after a released WSL attach returns immediately', async () => {
|
|
1524
2225
|
const wd = await mkdtemp(join(tmpdir(), 'omx-launch-tmux-attach-noop-'));
|
|
1525
2226
|
try {
|
|
1526
2227
|
const { env, tmuxLogPath } = await createLaunchFixture(wd, (tmuxLogPath) => `#!/bin/sh
|
|
@@ -1530,7 +2231,7 @@ case "$1" in
|
|
|
1530
2231
|
exit 0
|
|
1531
2232
|
;;
|
|
1532
2233
|
new-session)
|
|
1533
|
-
printf '
|
|
2234
|
+
printf '%%12\n'
|
|
1534
2235
|
exit 0
|
|
1535
2236
|
;;
|
|
1536
2237
|
split-window)
|
|
@@ -1567,15 +2268,12 @@ exit 0
|
|
|
1567
2268
|
return;
|
|
1568
2269
|
const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
|
|
1569
2270
|
assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
|
|
1570
|
-
assert.
|
|
1571
|
-
assert.match(result.stderr, /attach-session returned immediately without attaching a client/i);
|
|
1572
|
-
assert.match(result.stderr, /Falling back to direct Codex launch/i);
|
|
2271
|
+
assert.doesNotMatch(result.stderr, /Falling back to direct Codex launch|attach-session returned immediately/);
|
|
1573
2272
|
assert.match(tmuxLog, /tmux:attach-session -t /);
|
|
1574
|
-
assert.
|
|
1575
|
-
assert.match(tmuxLog, /tmux:kill-session -t /);
|
|
2273
|
+
assert.doesNotMatch(tmuxLog, /tmux:display-message -p -t .* #\{session_attached\}|tmux:kill-session -t/);
|
|
1576
2274
|
}
|
|
1577
2275
|
finally {
|
|
1578
|
-
await rm(wd, { recursive: true, force: true });
|
|
2276
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
1579
2277
|
}
|
|
1580
2278
|
});
|
|
1581
2279
|
it('preserves the requested cwd through detached tmux launch when an unsupported SHELL value falls back away from rc-driven cwd drift', async () => {
|
|
@@ -1611,11 +2309,7 @@ case "$cmd" in
|
|
|
1611
2309
|
exit 0
|
|
1612
2310
|
;;
|
|
1613
2311
|
new-session)
|
|
1614
|
-
|
|
1615
|
-
if [ -n "\${last:-}" ]; then
|
|
1616
|
-
/bin/sh -c "$last"
|
|
1617
|
-
fi
|
|
1618
|
-
printf 'leader-pane\\n'
|
|
2312
|
+
printf '%%12\n'
|
|
1619
2313
|
exit 0
|
|
1620
2314
|
;;
|
|
1621
2315
|
split-window)
|
|
@@ -1641,6 +2335,7 @@ esac
|
|
|
1641
2335
|
exit 0
|
|
1642
2336
|
`);
|
|
1643
2337
|
await chmod(fakeTmuxPath, 0o755);
|
|
2338
|
+
await wrapFakeTmuxWithDetachedLeader(fakeTmuxPath);
|
|
1644
2339
|
const result = runOmx(wd, ['--madmax', '--tmux'], {
|
|
1645
2340
|
HOME: home,
|
|
1646
2341
|
SHELL: '/definitely/missing-shell',
|
|
@@ -1653,13 +2348,14 @@ exit 0
|
|
|
1653
2348
|
});
|
|
1654
2349
|
if (shouldSkipForSpawnPermissions(result.error))
|
|
1655
2350
|
return;
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
assert.match(
|
|
2351
|
+
await waitForPath(`${fakeTmuxPath}.leader-done`);
|
|
2352
|
+
const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
|
|
2353
|
+
assert.match(tmuxLog, /\/bin\/sh/);
|
|
2354
|
+
assert.match(tmuxLog, /__detached-session-leader/);
|
|
1659
2355
|
assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
|
|
1660
2356
|
}
|
|
1661
2357
|
finally {
|
|
1662
|
-
await rm(wd, { recursive: true, force: true });
|
|
2358
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
1663
2359
|
}
|
|
1664
2360
|
});
|
|
1665
2361
|
it('falls back to /bin/sh for detached tmux launch when SHELL drifts to an unsupported path', async () => {
|
|
@@ -1693,11 +2389,7 @@ case "$cmd" in
|
|
|
1693
2389
|
exit 0
|
|
1694
2390
|
;;
|
|
1695
2391
|
new-session)
|
|
1696
|
-
|
|
1697
|
-
if [ -n "\${last:-}" ]; then
|
|
1698
|
-
/bin/sh -c "$last"
|
|
1699
|
-
fi
|
|
1700
|
-
printf 'leader-pane\\n'
|
|
2392
|
+
printf '%%12\n'
|
|
1701
2393
|
exit 0
|
|
1702
2394
|
;;
|
|
1703
2395
|
split-window)
|
|
@@ -1723,6 +2415,7 @@ esac
|
|
|
1723
2415
|
exit 0
|
|
1724
2416
|
`);
|
|
1725
2417
|
await chmod(fakeTmuxPath, 0o755);
|
|
2418
|
+
await wrapFakeTmuxWithDetachedLeader(fakeTmuxPath);
|
|
1726
2419
|
const result = runOmx(wd, ['--madmax', '--tmux'], {
|
|
1727
2420
|
HOME: home,
|
|
1728
2421
|
SHELL: '/bin/not-a-real-shell',
|
|
@@ -1736,15 +2429,199 @@ exit 0
|
|
|
1736
2429
|
if (shouldSkipForSpawnPermissions(result.error))
|
|
1737
2430
|
return;
|
|
1738
2431
|
const tmuxLog = await readFile(tmuxLogPath, 'utf-8');
|
|
1739
|
-
const codexLog = normalizeDarwinTmpPath(await readFile(codexLogPath, 'utf-8'));
|
|
1740
2432
|
assert.match(tmuxLog, /\/bin\/sh/);
|
|
1741
2433
|
assert.doesNotMatch(tmuxLog, /not-a-real-shell/);
|
|
1742
|
-
assert.match(
|
|
1743
|
-
assert.match(codexLog, new RegExp(`codex-pwd:${escapeRegExp(normalizeDarwinTmpPath(wd))}`));
|
|
2434
|
+
assert.match(tmuxLog, /__detached-session-leader/);
|
|
1744
2435
|
assert.equal(result.status, 0, result.error || result.stderr || result.stdout);
|
|
2436
|
+
await waitForPath(`${fakeTmuxPath}.leader-done`);
|
|
2437
|
+
}
|
|
2438
|
+
finally {
|
|
2439
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
2440
|
+
}
|
|
2441
|
+
});
|
|
2442
|
+
it('keeps finalization authority in the real detached leader process through child exit', async () => {
|
|
2443
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-detached-leader-process-'));
|
|
2444
|
+
const home = join(wd, 'home');
|
|
2445
|
+
const fakeBin = join(wd, 'bin');
|
|
2446
|
+
const fakeChild = join(wd, 'fake-codex.sh');
|
|
2447
|
+
const childReady = join(wd, 'child-ready');
|
|
2448
|
+
const childRelease = join(wd, 'child-release');
|
|
2449
|
+
const childEnv = join(wd, 'child-env');
|
|
2450
|
+
const descendantPidPath = join(wd, 'descendant-pid');
|
|
2451
|
+
const readyPath = join(wd, '.omx', 'runtime', 'detached-release', 'ready');
|
|
2452
|
+
const omxBin = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..', 'dist', 'cli', 'omx.js');
|
|
2453
|
+
const sessionId = 'omx-detached-leader-process';
|
|
2454
|
+
try {
|
|
2455
|
+
await mkdir(home, { recursive: true });
|
|
2456
|
+
await mkdir(fakeBin, { recursive: true });
|
|
2457
|
+
await writeExecutable(join(fakeBin, 'tmux'), `#!/bin/sh
|
|
2458
|
+
if [ "$1" = "list-panes" ]; then
|
|
2459
|
+
printf '%s\t0\t%s\n' "$TMUX_PANE" "$PPID"
|
|
2460
|
+
fi
|
|
2461
|
+
exit 0
|
|
2462
|
+
`);
|
|
2463
|
+
await writeExecutable(fakeChild, `#!/bin/sh\nprintf '%s' "$OMX_NOTIFY_TEMP_CONTRACT" > ${JSON.stringify(childEnv)}\n(sh -c 'trap "" TERM; while :; do sleep 1; done') &\nprintf '%s' "$!" > ${JSON.stringify(descendantPidPath)}\nprintf ready > ${JSON.stringify(childReady)}\nwhile [ ! -f ${JSON.stringify(childRelease)} ]; do sleep 0.02; done\nexit 0\n`);
|
|
2464
|
+
const payload = Buffer.from(JSON.stringify({
|
|
2465
|
+
cwd: wd,
|
|
2466
|
+
sessionName: 'omx-detached-leader-process',
|
|
2467
|
+
sessionId,
|
|
2468
|
+
codexCmd: fakeChild,
|
|
2469
|
+
readyPath,
|
|
2470
|
+
preLaunchOptions: {
|
|
2471
|
+
notifyTempContract: {
|
|
2472
|
+
active: true,
|
|
2473
|
+
selectors: ['custom:test-provider'],
|
|
2474
|
+
canonicalSelectors: ['custom:test-provider'],
|
|
2475
|
+
warnings: [],
|
|
2476
|
+
source: 'cli',
|
|
2477
|
+
},
|
|
2478
|
+
enableNotifyFallbackAuthority: false,
|
|
2479
|
+
worktreeDirty: false,
|
|
2480
|
+
shouldAttach: true,
|
|
2481
|
+
},
|
|
2482
|
+
})).toString('base64url');
|
|
2483
|
+
const leader = spawn(process.execPath, [omxBin, '__detached-session-leader', payload], {
|
|
2484
|
+
cwd: wd,
|
|
2485
|
+
env: buildRunOmxEnv({
|
|
2486
|
+
HOME: home,
|
|
2487
|
+
TMUX: '/tmp/fake-tmux,1,0',
|
|
2488
|
+
TMUX_PANE: '%3202',
|
|
2489
|
+
PATH: `${fakeBin}:/usr/bin:/bin`,
|
|
2490
|
+
OMX_AUTO_UPDATE: '0',
|
|
2491
|
+
OMX_NOTIFY_FALLBACK: '0',
|
|
2492
|
+
OMX_HOOK_DERIVED_SIGNALS: '0',
|
|
2493
|
+
}),
|
|
2494
|
+
stdio: ['ignore', 'inherit', 'inherit'],
|
|
2495
|
+
});
|
|
2496
|
+
await waitForPath(readyPath);
|
|
2497
|
+
const ready = JSON.parse(await readFile(readyPath, 'utf-8'));
|
|
2498
|
+
await writeFile(`${readyPath}.release`, `${JSON.stringify({ version: 1, kind: 'release', nonce: `${ready.nonce}-foreign`, sessionId: ready.sessionId, sessionName: ready.sessionName, leaderPid: ready.leaderPid })}\n`);
|
|
2499
|
+
await new Promise((resolveWait) => setTimeout(resolveWait, 100));
|
|
2500
|
+
assert.equal(existsSync(childReady), false);
|
|
2501
|
+
await writeFile(`${readyPath}.release`, `${JSON.stringify({ version: 1, kind: 'release', nonce: ready.nonce, sessionId: ready.sessionId, sessionName: ready.sessionName, leaderPid: ready.leaderPid })}\n`);
|
|
2502
|
+
await waitForPath(childReady);
|
|
2503
|
+
assert.match(await readFile(childEnv, 'utf-8'), /custom:test-provider/);
|
|
2504
|
+
await waitForPath(descendantPidPath);
|
|
2505
|
+
const descendantPid = Number.parseInt((await readFile(descendantPidPath, 'utf-8')).trim(), 10);
|
|
2506
|
+
assert.equal(Number.isSafeInteger(descendantPid) && descendantPid > 0, true);
|
|
2507
|
+
assert.equal(existsSync(readyPath), true);
|
|
2508
|
+
const sessionState = JSON.parse(await readFile(join(wd, '.omx', 'state', 'session.json'), 'utf-8'));
|
|
2509
|
+
assert.equal(sessionState.tmux_session_name, 'omx-detached-leader-process');
|
|
2510
|
+
assert.equal(sessionState.tmux_pane_id, '%3202');
|
|
2511
|
+
assert.equal(existsSync(join(wd, '.omx', 'state', 'detached-active-record.json')), true);
|
|
2512
|
+
await writeFile(childRelease, 'release\n');
|
|
2513
|
+
const exitCode = await new Promise((resolveExit, rejectExit) => {
|
|
2514
|
+
leader.once('error', rejectExit);
|
|
2515
|
+
leader.once('exit', resolveExit);
|
|
2516
|
+
});
|
|
2517
|
+
assert.equal(exitCode, 0);
|
|
2518
|
+
assert.throws(() => process.kill(descendantPid, 0), (error) => error.code === 'ESRCH');
|
|
2519
|
+
const terminalReport = JSON.parse(await readFile(readyPath, 'utf-8'));
|
|
2520
|
+
assert.deepEqual(terminalReport, {
|
|
2521
|
+
version: 1,
|
|
2522
|
+
kind: 'terminal',
|
|
2523
|
+
nonce: ready.nonce,
|
|
2524
|
+
sessionId: ready.sessionId,
|
|
2525
|
+
sessionName: ready.sessionName,
|
|
2526
|
+
leaderPid: ready.leaderPid,
|
|
2527
|
+
paneId: '%3202',
|
|
2528
|
+
finalized: true,
|
|
2529
|
+
exitStatus: 0,
|
|
2530
|
+
});
|
|
2531
|
+
assert.equal(existsSync(join(wd, '.omx', 'state', 'session.json')), false);
|
|
2532
|
+
assert.equal(existsSync(join(wd, '.omx', 'state', 'detached-active-record.json')), false);
|
|
2533
|
+
assert.equal(existsSync(readyPath), true);
|
|
2534
|
+
await rm(readyPath, { force: true });
|
|
2535
|
+
assert.match(await readFile(join(wd, '.omx', 'logs', 'session-history.jsonl'), 'utf-8'), new RegExp(sessionId));
|
|
2536
|
+
}
|
|
2537
|
+
finally {
|
|
2538
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
2539
|
+
}
|
|
2540
|
+
});
|
|
2541
|
+
it('returns the attached client to the shell with the child status after a normal detached child exit', async (t) => {
|
|
2542
|
+
if (!skipUnlessPrivateRealTmux(t))
|
|
2543
|
+
return;
|
|
2544
|
+
if (!skipUnlessScriptAvailable(t))
|
|
2545
|
+
return;
|
|
2546
|
+
const wd = await mkdtemp(join(tmpdir(), 'omx-launch-detached-e2e-'));
|
|
2547
|
+
try {
|
|
2548
|
+
await withTempTmuxSession(async (fixture) => {
|
|
2549
|
+
const home = join(wd, 'home');
|
|
2550
|
+
const bin = join(wd, 'bin');
|
|
2551
|
+
await mkdir(home, { recursive: true });
|
|
2552
|
+
await mkdir(bin, { recursive: true });
|
|
2553
|
+
// The PTY is required for detached attach behavior, but it would also
|
|
2554
|
+
// trigger OMX's unrelated one-time interactive star prompt. Mark it
|
|
2555
|
+
// handled so this harness deterministically exercises only launch exit.
|
|
2556
|
+
await mkdir(join(home, '.omx', 'state'), { recursive: true });
|
|
2557
|
+
await writeFile(join(home, '.omx', 'state', 'star-prompt.json'), '{"prompted_at":"2026-01-01T00:00:00.000Z"}\n');
|
|
2558
|
+
await fixture.createPathShim(bin);
|
|
2559
|
+
const omxBin = join(dirname(fileURLToPath(import.meta.url)), '..', '..', '..', 'dist', 'cli', 'omx.js');
|
|
2560
|
+
const runPtyLaunch = (childExitStatus, remainOnExit) => {
|
|
2561
|
+
writeFileSync(join(bin, 'codex'), `#!/bin/sh\nsleep 1\nexit ${childExitStatus}\n`, { mode: 0o755 });
|
|
2562
|
+
// Simulate a user tmux config that would retain the dead leader pane.
|
|
2563
|
+
fixture.run(['set-option', '-g', 'remain-on-exit', remainOnExit]);
|
|
2564
|
+
const envPrefix = [
|
|
2565
|
+
'unset TMUX TMUX_PANE',
|
|
2566
|
+
'export OMX_TEST_DETACHED_TRACE=1',
|
|
2567
|
+
`export OMX_TEST_DETACHED_TRACE_PATH=${JSON.stringify(join(wd, 'detached-trace.log'))}`,
|
|
2568
|
+
`export HOME=${JSON.stringify(home)}`,
|
|
2569
|
+
`export PATH=${JSON.stringify(bin)}:$PATH`,
|
|
2570
|
+
'export OMX_AUTO_UPDATE=0',
|
|
2571
|
+
'export OMX_NOTIFY_FALLBACK=0',
|
|
2572
|
+
'export OMX_HOOK_DERIVED_SIGNALS=0',
|
|
2573
|
+
'export TERM=xterm-256color',
|
|
2574
|
+
].join('; ');
|
|
2575
|
+
const command = `${envPrefix}; cd ${JSON.stringify(wd)} || exit 125; ${JSON.stringify(process.execPath)} ${JSON.stringify(omxBin)} --tmux ${JSON.stringify('e2e prompt')}; child_status=$?; printf '\n%s:%s\n' 'omx-follow-up' "$child_status"; exit "$child_status"`;
|
|
2576
|
+
if (process.platform === 'darwin') {
|
|
2577
|
+
const result = fixture.runPtyResult(command, { pollLimit: DARWIN_PTY_FINALIZATION_POLL_LIMIT });
|
|
2578
|
+
let diagnostics = '';
|
|
2579
|
+
if (result.error) {
|
|
2580
|
+
const sessions = fixture.runResult(['list-sessions', '-F', '#{session_name}']);
|
|
2581
|
+
for (const session of sessions.stdout.split('\n').filter((name) => name.startsWith('omx-'))) {
|
|
2582
|
+
const panes = fixture.runResult([
|
|
2583
|
+
'list-panes', '-t', session, '-F', '#{pane_id}\t#{pane_dead}\t#{pane_dead_status}\t#{pane_pid}\t#{pane_current_command}\t#{@omx_instance_id}\t#{@omx_hud_owner}',
|
|
2584
|
+
]);
|
|
2585
|
+
diagnostics += `\n[${session}]\n${panes.stdout}`;
|
|
2586
|
+
for (const paneLine of panes.stdout.split('\n').filter(Boolean)) {
|
|
2587
|
+
const [paneId] = paneLine.split('\t', 1);
|
|
2588
|
+
if (!paneId)
|
|
2589
|
+
continue;
|
|
2590
|
+
diagnostics += `\n[${paneLine}]\n${fixture.runResult(['capture-pane', '-p', '-t', paneId, '-S', '-']).stdout}`;
|
|
2591
|
+
}
|
|
2592
|
+
}
|
|
2593
|
+
const tracePath = join(wd, 'detached-trace.log');
|
|
2594
|
+
if (existsSync(tracePath))
|
|
2595
|
+
diagnostics += `\n[detached-trace]\n${readFileSync(tracePath, 'utf-8')}`;
|
|
2596
|
+
}
|
|
2597
|
+
return { status: result.status, output: `${result.stdout}\n${result.stderr}${diagnostics}`, error: result.error };
|
|
2598
|
+
}
|
|
2599
|
+
const ptyCommand = buildPtyScriptCommand(command);
|
|
2600
|
+
const result = spawnSync(ptyCommand.executable, ptyCommand.args, {
|
|
2601
|
+
encoding: 'utf-8',
|
|
2602
|
+
timeout: 120_000,
|
|
2603
|
+
killSignal: 'SIGKILL',
|
|
2604
|
+
env: buildRunOmxEnv({ TMUX: '', TMUX_PANE: '' }),
|
|
2605
|
+
});
|
|
2606
|
+
return { status: result.status, output: `${result.stdout || ''}\n${result.stderr || ''}`, error: result.error?.message ?? '' };
|
|
2607
|
+
};
|
|
2608
|
+
const listOmxSessions = () => fixture.run(['list-sessions', '-F', '#{session_name}'])
|
|
2609
|
+
.split('\n')
|
|
2610
|
+
.filter((name) => name.startsWith('omx-') && name !== fixture.sessionName);
|
|
2611
|
+
const zero = runPtyLaunch(0, 'on');
|
|
2612
|
+
assert.equal(zero.error, '', `zero-status PTY capture and cleanup must succeed: ${zero.error}\n${zero.output}`);
|
|
2613
|
+
assert.equal(zero.status, 0, `zero-status attached launch must return to the shell successfully:\n${zero.output}`);
|
|
2614
|
+
assert.match(zero.output, /omx-follow-up:0/, `zero-status launch must leave a usable shell for a follow-up command:\n${zero.output}`);
|
|
2615
|
+
assert.deepEqual(listOmxSessions(), [], 'zero-status exit must destroy the owned detached session despite remain-on-exit=on');
|
|
2616
|
+
const seven = runPtyLaunch(7, 'failed');
|
|
2617
|
+
assert.equal(seven.error, '', `nonzero-status PTY capture and cleanup must succeed: ${seven.error}\n${seven.output}`);
|
|
2618
|
+
assert.equal(seven.status, 7, `nonzero child status must propagate to the invoking shell:\n${seven.output}`);
|
|
2619
|
+
assert.match(seven.output, /omx-follow-up:7/, `nonzero launch must leave a usable shell for a follow-up command:\n${seven.output}`);
|
|
2620
|
+
assert.deepEqual(listOmxSessions(), [], 'nonzero exit must destroy the owned detached session despite remain-on-exit=failed');
|
|
2621
|
+
});
|
|
1745
2622
|
}
|
|
1746
2623
|
finally {
|
|
1747
|
-
await rm(wd, { recursive: true, force: true });
|
|
2624
|
+
await rm(wd, { recursive: true, force: true, maxRetries: 20, retryDelay: 50 });
|
|
1748
2625
|
}
|
|
1749
2626
|
});
|
|
1750
2627
|
});
|