oh-my-codex 0.20.3 → 0.20.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +35 -18
- package/Cargo.toml +1 -1
- package/README.md +2 -2
- package/crates/omx-runtime/Cargo.toml +3 -0
- package/crates/omx-runtime/src/main.rs +390 -0
- package/crates/omx-runtime/tests/execution.rs +264 -0
- package/dist/adapt/__tests__/herdr-target.test.d.ts +2 -0
- package/dist/adapt/__tests__/herdr-target.test.d.ts.map +1 -0
- package/dist/adapt/__tests__/herdr-target.test.js +72 -0
- package/dist/adapt/__tests__/herdr-target.test.js.map +1 -0
- package/dist/adapt/contracts.d.ts +1 -1
- package/dist/adapt/contracts.d.ts.map +1 -1
- package/dist/adapt/contracts.js +1 -1
- package/dist/adapt/contracts.js.map +1 -1
- package/dist/adapt/herdr/__tests__/authority.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/authority.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/authority.test.js +49 -0
- package/dist/adapt/herdr/__tests__/authority.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/bridge.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/bridge.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/bridge.test.js +176 -0
- package/dist/adapt/herdr/__tests__/bridge.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/sanitize.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/sanitize.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/sanitize.test.js +45 -0
- package/dist/adapt/herdr/__tests__/sanitize.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/semantic.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/semantic.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/semantic.test.js +130 -0
- package/dist/adapt/herdr/__tests__/semantic.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/seq-store.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/seq-store.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/seq-store.test.js +54 -0
- package/dist/adapt/herdr/__tests__/seq-store.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/socket-live.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/socket-live.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/socket-live.test.js +104 -0
- package/dist/adapt/herdr/__tests__/socket-live.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/team-map.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/team-map.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/team-map.test.js +46 -0
- package/dist/adapt/herdr/__tests__/team-map.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/transport.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/transport.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/transport.test.js +160 -0
- package/dist/adapt/herdr/__tests__/transport.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/wiring.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/wiring.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/wiring.test.js +118 -0
- package/dist/adapt/herdr/__tests__/wiring.test.js.map +1 -0
- package/dist/adapt/herdr/authority.d.ts +34 -0
- package/dist/adapt/herdr/authority.d.ts.map +1 -0
- package/dist/adapt/herdr/authority.js +71 -0
- package/dist/adapt/herdr/authority.js.map +1 -0
- package/dist/adapt/herdr/bridge.d.ts +84 -0
- package/dist/adapt/herdr/bridge.d.ts.map +1 -0
- package/dist/adapt/herdr/bridge.js +222 -0
- package/dist/adapt/herdr/bridge.js.map +1 -0
- package/dist/adapt/herdr/sanitize.d.ts +20 -0
- package/dist/adapt/herdr/sanitize.d.ts.map +1 -0
- package/dist/adapt/herdr/sanitize.js +70 -0
- package/dist/adapt/herdr/sanitize.js.map +1 -0
- package/dist/adapt/herdr/semantic.d.ts +67 -0
- package/dist/adapt/herdr/semantic.d.ts.map +1 -0
- package/dist/adapt/herdr/semantic.js +170 -0
- package/dist/adapt/herdr/semantic.js.map +1 -0
- package/dist/adapt/herdr/seq-store.d.ts +32 -0
- package/dist/adapt/herdr/seq-store.d.ts.map +1 -0
- package/dist/adapt/herdr/seq-store.js +126 -0
- package/dist/adapt/herdr/seq-store.js.map +1 -0
- package/dist/adapt/herdr/team-map.d.ts +30 -0
- package/dist/adapt/herdr/team-map.d.ts.map +1 -0
- package/dist/adapt/herdr/team-map.js +37 -0
- package/dist/adapt/herdr/team-map.js.map +1 -0
- package/dist/adapt/herdr/transport.d.ts +115 -0
- package/dist/adapt/herdr/transport.d.ts.map +1 -0
- package/dist/adapt/herdr/transport.js +260 -0
- package/dist/adapt/herdr/transport.js.map +1 -0
- package/dist/adapt/herdr/wiring.d.ts +24 -0
- package/dist/adapt/herdr/wiring.d.ts.map +1 -0
- package/dist/adapt/herdr/wiring.js +62 -0
- package/dist/adapt/herdr/wiring.js.map +1 -0
- package/dist/adapt/herdr.d.ts +19 -0
- package/dist/adapt/herdr.d.ts.map +1 -0
- package/dist/adapt/herdr.js +115 -0
- package/dist/adapt/herdr.js.map +1 -0
- package/dist/adapt/index.d.ts +4 -0
- package/dist/adapt/index.d.ts.map +1 -1
- package/dist/adapt/index.js +48 -0
- package/dist/adapt/index.js.map +1 -1
- package/dist/adapt/registry.d.ts.map +1 -1
- package/dist/adapt/registry.js +11 -0
- package/dist/adapt/registry.js.map +1 -1
- package/dist/auth/__tests__/hotswap.test.js +60 -1
- package/dist/auth/__tests__/hotswap.test.js.map +1 -1
- package/dist/auth/__tests__/storage.test.js +33 -1
- package/dist/auth/__tests__/storage.test.js.map +1 -1
- package/dist/auth/hotswap.d.ts +16 -2
- package/dist/auth/hotswap.d.ts.map +1 -1
- package/dist/auth/hotswap.js +22 -7
- package/dist/auth/hotswap.js.map +1 -1
- package/dist/auth/storage.js +1 -1
- package/dist/auth/storage.js.map +1 -1
- package/dist/autopilot/__tests__/ralplan-gate.test.js +86 -1333
- package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
- package/dist/autopilot/ralplan-gate.d.ts.map +1 -1
- package/dist/autopilot/ralplan-gate.js +7 -6
- package/dist/autopilot/ralplan-gate.js.map +1 -1
- package/dist/cli/__tests__/agents-init.test.js +4 -23
- package/dist/cli/__tests__/agents-init.test.js.map +1 -1
- package/dist/cli/__tests__/api.test.js +61 -24
- package/dist/cli/__tests__/api.test.js.map +1 -1
- package/dist/cli/__tests__/auth.test.js +3 -1
- package/dist/cli/__tests__/auth.test.js.map +1 -1
- package/dist/cli/__tests__/codex-feature-probe.test.js +66 -1
- package/dist/cli/__tests__/codex-feature-probe.test.js.map +1 -1
- package/dist/cli/__tests__/codex-plugin-layout.test.js +70 -2
- package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
- package/dist/cli/__tests__/detached-leader-teardown.test.d.ts +2 -0
- package/dist/cli/__tests__/detached-leader-teardown.test.d.ts.map +1 -0
- package/dist/cli/__tests__/detached-leader-teardown.test.js +473 -0
- package/dist/cli/__tests__/detached-leader-teardown.test.js.map +1 -0
- package/dist/cli/__tests__/doctor-process-identity-readiness.test.d.ts +2 -0
- package/dist/cli/__tests__/doctor-process-identity-readiness.test.d.ts.map +1 -0
- package/dist/cli/__tests__/doctor-process-identity-readiness.test.js +103 -0
- package/dist/cli/__tests__/doctor-process-identity-readiness.test.js.map +1 -0
- package/dist/cli/__tests__/doctor-state-root-binding.test.d.ts +2 -0
- package/dist/cli/__tests__/doctor-state-root-binding.test.d.ts.map +1 -0
- package/dist/cli/__tests__/doctor-state-root-binding.test.js +139 -0
- package/dist/cli/__tests__/doctor-state-root-binding.test.js.map +1 -0
- package/dist/cli/__tests__/doctor-warning-copy.test.js +35 -3
- package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
- package/dist/cli/__tests__/exec.test.js +75 -0
- package/dist/cli/__tests__/exec.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +621 -460
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/launch-fallback.test.js +1020 -143
- package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
- package/dist/cli/__tests__/mcp-parity.test.js +4 -3
- package/dist/cli/__tests__/mcp-parity.test.js.map +1 -1
- package/dist/cli/__tests__/native-assets.test.js +398 -16
- package/dist/cli/__tests__/native-assets.test.js.map +1 -1
- package/dist/cli/__tests__/native-hook-claim-journal.test.js +53 -5
- package/dist/cli/__tests__/native-hook-claim-journal.test.js.map +1 -1
- package/dist/cli/__tests__/ralph.test.js +2 -2
- package/dist/cli/__tests__/ralph.test.js.map +1 -1
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js +57 -57
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js.map +1 -1
- package/dist/cli/__tests__/ralplan-child-binding-3212.test.d.ts +2 -0
- package/dist/cli/__tests__/ralplan-child-binding-3212.test.d.ts.map +1 -0
- package/dist/cli/__tests__/ralplan-child-binding-3212.test.js +56 -0
- package/dist/cli/__tests__/ralplan-child-binding-3212.test.js.map +1 -0
- package/dist/cli/__tests__/ralplan.test.js +84 -12
- package/dist/cli/__tests__/ralplan.test.js.map +1 -1
- package/dist/cli/__tests__/session-scoped-runtime.test.js +1118 -43
- package/dist/cli/__tests__/session-scoped-runtime.test.js.map +1 -1
- package/dist/cli/__tests__/setup-agents-overwrite.test.js +8 -55
- package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
- package/dist/cli/__tests__/setup-install-mode.test.js +183 -4
- package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
- package/dist/cli/__tests__/sparkshell-cli.test.js +93 -41
- package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
- package/dist/cli/__tests__/team.test.js +45 -1
- package/dist/cli/__tests__/team.test.js.map +1 -1
- package/dist/cli/__tests__/ultragoal.test.js +117 -2
- package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
- package/dist/cli/__tests__/uninstall.test.js +50 -5
- package/dist/cli/__tests__/uninstall.test.js.map +1 -1
- package/dist/cli/__tests__/update.test.js +669 -177
- package/dist/cli/__tests__/update.test.js.map +1 -1
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js +32 -2
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
- package/dist/cli/api.d.ts.map +1 -1
- package/dist/cli/api.js +9 -10
- package/dist/cli/api.js.map +1 -1
- package/dist/cli/codex-feature-probe.d.ts +16 -0
- package/dist/cli/codex-feature-probe.d.ts.map +1 -1
- package/dist/cli/codex-feature-probe.js +44 -0
- package/dist/cli/codex-feature-probe.js.map +1 -1
- package/dist/cli/codex-home.d.ts.map +1 -1
- package/dist/cli/codex-home.js +7 -4
- package/dist/cli/codex-home.js.map +1 -1
- package/dist/cli/doctor.d.ts +17 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +277 -3
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.d.ts +231 -12
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2298 -735
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/native-assets.d.ts +25 -0
- package/dist/cli/native-assets.d.ts.map +1 -1
- package/dist/cli/native-assets.js +448 -51
- package/dist/cli/native-assets.js.map +1 -1
- package/dist/cli/native-hook-claim-journal.d.ts +8 -4
- package/dist/cli/native-hook-claim-journal.d.ts.map +1 -1
- package/dist/cli/native-hook-claim-journal.js +30 -8
- package/dist/cli/native-hook-claim-journal.js.map +1 -1
- package/dist/cli/package-manager-ownership.d.ts +63 -0
- package/dist/cli/package-manager-ownership.d.ts.map +1 -0
- package/dist/cli/package-manager-ownership.js +323 -0
- package/dist/cli/package-manager-ownership.js.map +1 -0
- package/dist/cli/ralph.js +2 -2
- package/dist/cli/ralph.js.map +1 -1
- package/dist/cli/ralplan.d.ts +4 -2
- package/dist/cli/ralplan.d.ts.map +1 -1
- package/dist/cli/ralplan.js +76 -34
- package/dist/cli/ralplan.js.map +1 -1
- package/dist/cli/setup-preferences.d.ts +15 -0
- package/dist/cli/setup-preferences.d.ts.map +1 -1
- package/dist/cli/setup-preferences.js +27 -1
- package/dist/cli/setup-preferences.js.map +1 -1
- package/dist/cli/setup.d.ts +3 -0
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +55 -32
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/sparkshell.d.ts.map +1 -1
- package/dist/cli/sparkshell.js +52 -9
- package/dist/cli/sparkshell.js.map +1 -1
- package/dist/cli/ultragoal.d.ts.map +1 -1
- package/dist/cli/ultragoal.js +2 -2
- package/dist/cli/ultragoal.js.map +1 -1
- package/dist/cli/uninstall.d.ts.map +1 -1
- package/dist/cli/uninstall.js +27 -28
- package/dist/cli/uninstall.js.map +1 -1
- package/dist/cli/update-worker.d.ts +2 -0
- package/dist/cli/update-worker.d.ts.map +1 -0
- package/dist/cli/update-worker.js +142 -0
- package/dist/cli/update-worker.js.map +1 -0
- package/dist/cli/update.d.ts +14 -22
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +192 -241
- package/dist/cli/update.js.map +1 -1
- package/dist/config/__tests__/generator-idempotent.test.js +173 -29
- package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
- package/dist/config/__tests__/generator-notify.test.js +3 -1
- package/dist/config/__tests__/generator-notify.test.js.map +1 -1
- package/dist/config/generator.d.ts +25 -3
- package/dist/config/generator.d.ts.map +1 -1
- package/dist/config/generator.js +113 -35
- package/dist/config/generator.js.map +1 -1
- package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js +25 -0
- package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js.map +1 -1
- package/dist/goal-workflows/codex-goal-snapshot.d.ts +1 -1
- package/dist/goal-workflows/codex-goal-snapshot.d.ts.map +1 -1
- package/dist/goal-workflows/codex-goal-snapshot.js +2 -0
- package/dist/goal-workflows/codex-goal-snapshot.js.map +1 -1
- package/dist/hooks/__tests__/agents-overlay.test.js +3 -1
- package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js +27 -4
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.d.ts +2 -0
- package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.js +172 -0
- package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.js.map +1 -0
- package/dist/hooks/__tests__/keyword-detector.test.js +172 -33
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js +173 -11
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +1 -0
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +65 -17
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js +27 -0
- package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js +10 -118
- package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-regression-205.test.js +7 -0
- package/dist/hooks/__tests__/notify-hook-regression-205.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +2 -2
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +4 -1
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.js +2 -1
- package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +1 -0
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/prompt-guidance-contract.test.js +9 -0
- package/dist/hooks/__tests__/prompt-guidance-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/session.test.js +2976 -557
- package/dist/hooks/__tests__/session.test.js.map +1 -1
- package/dist/hooks/agents-overlay.js +1 -1
- package/dist/hooks/agents-overlay.js.map +1 -1
- package/dist/hooks/extensibility/__tests__/dispatcher.test.js +108 -2
- package/dist/hooks/extensibility/__tests__/dispatcher.test.js.map +1 -1
- package/dist/hooks/extensibility/__tests__/sdk.test.js +151 -1
- package/dist/hooks/extensibility/__tests__/sdk.test.js.map +1 -1
- package/dist/hooks/extensibility/dispatcher.d.ts.map +1 -1
- package/dist/hooks/extensibility/dispatcher.js +20 -3
- package/dist/hooks/extensibility/dispatcher.js.map +1 -1
- package/dist/hooks/extensibility/plugin-runner.js +1 -0
- package/dist/hooks/extensibility/plugin-runner.js.map +1 -1
- package/dist/hooks/extensibility/runtime.d.ts.map +1 -1
- package/dist/hooks/extensibility/runtime.js.map +1 -1
- package/dist/hooks/extensibility/sdk/paths.d.ts +1 -1
- package/dist/hooks/extensibility/sdk/paths.d.ts.map +1 -1
- package/dist/hooks/extensibility/sdk/paths.js +2 -2
- package/dist/hooks/extensibility/sdk/paths.js.map +1 -1
- package/dist/hooks/extensibility/sdk/runtime-state.d.ts +1 -1
- package/dist/hooks/extensibility/sdk/runtime-state.d.ts.map +1 -1
- package/dist/hooks/extensibility/sdk/runtime-state.js +55 -15
- package/dist/hooks/extensibility/sdk/runtime-state.js.map +1 -1
- package/dist/hooks/extensibility/sdk.d.ts +1 -0
- package/dist/hooks/extensibility/sdk.d.ts.map +1 -1
- package/dist/hooks/extensibility/sdk.js +1 -1
- package/dist/hooks/extensibility/sdk.js.map +1 -1
- package/dist/hooks/extensibility/types.d.ts +7 -0
- package/dist/hooks/extensibility/types.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.d.ts +10 -3
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +116 -20
- package/dist/hooks/keyword-detector.js.map +1 -1
- package/dist/hooks/session.d.ts +180 -14
- package/dist/hooks/session.d.ts.map +1 -1
- package/dist/hooks/session.js +1402 -281
- package/dist/hooks/session.js.map +1 -1
- package/dist/hud/__tests__/hud-tmux-injection.test.js +6 -6
- package/dist/hud/__tests__/hud-tmux-injection.test.js.map +1 -1
- package/dist/hud/__tests__/index.test.js +26 -16
- package/dist/hud/__tests__/index.test.js.map +1 -1
- package/dist/hud/__tests__/reconcile.test.js +83 -78
- package/dist/hud/__tests__/reconcile.test.js.map +1 -1
- package/dist/hud/__tests__/state.test.js +36 -31
- package/dist/hud/__tests__/state.test.js.map +1 -1
- package/dist/hud/__tests__/tmux.test.js +155 -198
- package/dist/hud/__tests__/tmux.test.js.map +1 -1
- package/dist/hud/tmux.d.ts +42 -4
- package/dist/hud/tmux.d.ts.map +1 -1
- package/dist/hud/tmux.js +1017 -139
- package/dist/hud/tmux.js.map +1 -1
- package/dist/leader/__tests__/contract.test.js +71 -34
- package/dist/leader/__tests__/contract.test.js.map +1 -1
- package/dist/leader/contract.d.ts +1 -6
- package/dist/leader/contract.d.ts.map +1 -1
- package/dist/leader/contract.js +25 -24
- package/dist/leader/contract.js.map +1 -1
- package/dist/mcp/__tests__/hermes-bridge.test.js +17 -0
- package/dist/mcp/__tests__/hermes-bridge.test.js.map +1 -1
- package/dist/mcp/__tests__/server-lifecycle.test.js +4 -4
- package/dist/mcp/__tests__/state-paths.test.js +735 -4
- package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
- package/dist/mcp/__tests__/state-server.test.js +1 -1
- package/dist/mcp/__tests__/state-server.test.js.map +1 -1
- package/dist/mcp/__tests__/trace-server.test.js +2 -2
- package/dist/mcp/__tests__/trace-server.test.js.map +1 -1
- package/dist/mcp/hermes-bridge.d.ts.map +1 -1
- package/dist/mcp/hermes-bridge.js +2 -2
- package/dist/mcp/hermes-bridge.js.map +1 -1
- package/dist/mcp/state-paths.d.ts +78 -4
- package/dist/mcp/state-paths.d.ts.map +1 -1
- package/dist/mcp/state-paths.js +543 -40
- package/dist/mcp/state-paths.js.map +1 -1
- package/dist/mcp/state-server.d.ts +4 -4
- package/dist/mcp/state-server.d.ts.map +1 -1
- package/dist/modes/__tests__/base-autopilot-gates.test.js +36 -7
- package/dist/modes/__tests__/base-autopilot-gates.test.js.map +1 -1
- package/dist/modes/__tests__/base-multi-state-compat.test.js +1 -1
- package/dist/modes/__tests__/base-multi-state-compat.test.js.map +1 -1
- package/dist/modes/__tests__/base-session-scope.test.js +17 -8
- package/dist/modes/__tests__/base-session-scope.test.js.map +1 -1
- package/dist/modes/base.d.ts +5 -5
- package/dist/modes/base.d.ts.map +1 -1
- package/dist/modes/base.js +76 -21
- package/dist/modes/base.js.map +1 -1
- package/dist/native-assets/archive.d.ts +17 -0
- package/dist/native-assets/archive.d.ts.map +1 -0
- package/dist/native-assets/archive.js +238 -0
- package/dist/native-assets/archive.js.map +1 -0
- package/dist/native-assets/policy.d.ts +42 -0
- package/dist/native-assets/policy.d.ts.map +1 -0
- package/dist/native-assets/policy.js +155 -0
- package/dist/native-assets/policy.js.map +1 -0
- package/dist/pipeline/__tests__/orchestrator.test.js +162 -40
- package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
- package/dist/pipeline/__tests__/stages.test.js +66 -149
- package/dist/pipeline/__tests__/stages.test.js.map +1 -1
- package/dist/pipeline/orchestrator.d.ts.map +1 -1
- package/dist/pipeline/orchestrator.js +124 -18
- package/dist/pipeline/orchestrator.js.map +1 -1
- package/dist/pipeline/stages/ralplan.d.ts.map +1 -1
- package/dist/pipeline/stages/ralplan.js +36 -8
- package/dist/pipeline/stages/ralplan.js.map +1 -1
- package/dist/ralplan/__tests__/consensus-gate.test.js +70 -1437
- package/dist/ralplan/__tests__/consensus-gate.test.js.map +1 -1
- package/dist/ralplan/__tests__/documented-leader-preflight.test.js +328 -39
- package/dist/ralplan/__tests__/documented-leader-preflight.test.js.map +1 -1
- package/dist/ralplan/__tests__/runtime.test.js +82 -102
- package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
- package/dist/ralplan/consensus-gate.d.ts +9 -1
- package/dist/ralplan/consensus-gate.d.ts.map +1 -1
- package/dist/ralplan/consensus-gate.js +40 -304
- package/dist/ralplan/consensus-gate.js.map +1 -1
- package/dist/ralplan/documented-leader-preflight.d.ts +15 -8
- package/dist/ralplan/documented-leader-preflight.d.ts.map +1 -1
- package/dist/ralplan/documented-leader-preflight.js +356 -54
- package/dist/ralplan/documented-leader-preflight.js.map +1 -1
- package/dist/ralplan/runtime.d.ts +1 -1
- package/dist/ralplan/runtime.d.ts.map +1 -1
- package/dist/ralplan/runtime.js +68 -173
- package/dist/ralplan/runtime.js.map +1 -1
- package/dist/runtime/run-state.d.ts +5 -1
- package/dist/runtime/run-state.d.ts.map +1 -1
- package/dist/runtime/run-state.js +15 -8
- package/dist/runtime/run-state.js.map +1 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js +14071 -8859
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/__tests__/issue-3293-callsite-parity.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3293-callsite-parity.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3293-callsite-parity.test.js +225 -0
- package/dist/scripts/__tests__/issue-3293-callsite-parity.test.js.map +1 -0
- package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.js +188 -0
- package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.js.map +1 -0
- package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.js +259 -0
- package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.js.map +1 -0
- package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.js +491 -0
- package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.js.map +1 -0
- package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.js +708 -0
- package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.js.map +1 -0
- package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.js +208 -0
- package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.js.map +1 -0
- package/dist/scripts/__tests__/notify-state-io.test.js +26 -0
- package/dist/scripts/__tests__/notify-state-io.test.js.map +1 -1
- package/dist/scripts/__tests__/notify-tmux-injection.test.js +23 -1
- package/dist/scripts/__tests__/notify-tmux-injection.test.js.map +1 -1
- package/dist/scripts/__tests__/postinstall.test.js +19 -1
- package/dist/scripts/__tests__/postinstall.test.js.map +1 -1
- package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js +25 -8
- package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js.map +1 -1
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js +60 -25
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js.map +1 -1
- package/dist/scripts/__tests__/smoke-packed-install.test.js +155 -16
- package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
- package/dist/scripts/__tests__/verify-native-release-assets.test.d.ts +2 -0
- package/dist/scripts/__tests__/verify-native-release-assets.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/verify-native-release-assets.test.js +235 -0
- package/dist/scripts/__tests__/verify-native-release-assets.test.js.map +1 -0
- package/dist/scripts/codex-native-hook.d.ts +50 -1
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +3559 -451
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/scripts/codex-native-pre-post.d.ts +9 -0
- package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
- package/dist/scripts/codex-native-pre-post.js +35 -12
- package/dist/scripts/codex-native-pre-post.js.map +1 -1
- package/dist/scripts/generate-native-release-manifest.js +80 -51
- package/dist/scripts/generate-native-release-manifest.js.map +1 -1
- package/dist/scripts/notify-fallback-watcher.js +28 -39
- package/dist/scripts/notify-fallback-watcher.js.map +1 -1
- package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
- package/dist/scripts/notify-hook/state-io.js +5 -0
- package/dist/scripts/notify-hook/state-io.js.map +1 -1
- package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-leader-nudge.js +32 -3
- package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
- package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-worker-stop.js +23 -3
- package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
- package/dist/scripts/notify-hook/team-worker.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-worker.js +45 -5
- package/dist/scripts/notify-hook/team-worker.js.map +1 -1
- package/dist/scripts/notify-hook/tmux-injection.d.ts +1 -0
- package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
- package/dist/scripts/notify-hook/tmux-injection.js +4 -1
- package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
- package/dist/scripts/notify-hook.js +78 -82
- package/dist/scripts/notify-hook.js.map +1 -1
- package/dist/scripts/postinstall-advisory.d.ts +14 -0
- package/dist/scripts/postinstall-advisory.d.ts.map +1 -0
- package/dist/scripts/postinstall-advisory.js +49 -0
- package/dist/scripts/postinstall-advisory.js.map +1 -0
- package/dist/scripts/postinstall.d.ts +1 -1
- package/dist/scripts/postinstall.d.ts.map +1 -1
- package/dist/scripts/postinstall.js +1 -1
- package/dist/scripts/postinstall.js.map +1 -1
- package/dist/scripts/smoke-packed-install.d.ts +32 -1
- package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
- package/dist/scripts/smoke-packed-install.js +413 -60
- package/dist/scripts/smoke-packed-install.js.map +1 -1
- package/dist/scripts/sync-plugin-mirror.d.ts.map +1 -1
- package/dist/scripts/sync-plugin-mirror.js +16 -1
- package/dist/scripts/sync-plugin-mirror.js.map +1 -1
- package/dist/scripts/verify-native-release-assets.js +37 -23
- package/dist/scripts/verify-native-release-assets.js.map +1 -1
- package/dist/state/__tests__/mode-state-context.test.js +12 -5
- package/dist/state/__tests__/mode-state-context.test.js.map +1 -1
- package/dist/state/__tests__/operations.test.js +460 -850
- package/dist/state/__tests__/operations.test.js.map +1 -1
- package/dist/state/__tests__/skill-active.test.js +400 -2
- package/dist/state/__tests__/skill-active.test.js.map +1 -1
- package/dist/state/mode-state-context.d.ts +3 -0
- package/dist/state/mode-state-context.d.ts.map +1 -1
- package/dist/state/mode-state-context.js +27 -13
- package/dist/state/mode-state-context.js.map +1 -1
- package/dist/state/operations.d.ts +3 -0
- package/dist/state/operations.d.ts.map +1 -1
- package/dist/state/operations.js +144 -40
- package/dist/state/operations.js.map +1 -1
- package/dist/state/skill-active.d.ts +18 -1
- package/dist/state/skill-active.d.ts.map +1 -1
- package/dist/state/skill-active.js +420 -23
- package/dist/state/skill-active.js.map +1 -1
- package/dist/state/workflow-transition-reconcile.d.ts +3 -0
- package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
- package/dist/state/workflow-transition-reconcile.js +10 -4
- package/dist/state/workflow-transition-reconcile.js.map +1 -1
- package/dist/state/workflow-transition.d.ts.map +1 -1
- package/dist/state/workflow-transition.js +3 -1
- package/dist/state/workflow-transition.js.map +1 -1
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.js +27 -0
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.js.map +1 -1
- package/dist/subagents/__tests__/tracker.test.js +837 -1048
- package/dist/subagents/__tests__/tracker.test.js.map +1 -1
- package/dist/subagents/tracker.d.ts +64 -60
- package/dist/subagents/tracker.d.ts.map +1 -1
- package/dist/subagents/tracker.js +809 -358
- package/dist/subagents/tracker.js.map +1 -1
- package/dist/team/__tests__/delivery-e2e-smoke.test.js +6 -7
- package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
- package/dist/team/__tests__/mcp-comm.test.js +104 -3
- package/dist/team/__tests__/mcp-comm.test.js.map +1 -1
- package/dist/team/__tests__/notice-ledger.test.d.ts +2 -0
- package/dist/team/__tests__/notice-ledger.test.d.ts.map +1 -0
- package/dist/team/__tests__/notice-ledger.test.js +131 -0
- package/dist/team/__tests__/notice-ledger.test.js.map +1 -0
- package/dist/team/__tests__/pane-teardown-residue-e2e.test.d.ts +2 -0
- package/dist/team/__tests__/pane-teardown-residue-e2e.test.d.ts.map +1 -0
- package/dist/team/__tests__/pane-teardown-residue-e2e.test.js +410 -0
- package/dist/team/__tests__/pane-teardown-residue-e2e.test.js.map +1 -0
- package/dist/team/__tests__/phase-controller.test.js +20 -0
- package/dist/team/__tests__/phase-controller.test.js.map +1 -1
- package/dist/team/__tests__/runtime.test.js +725 -59
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/scaling.test.js +222 -237
- package/dist/team/__tests__/scaling.test.js.map +1 -1
- package/dist/team/__tests__/state-root.test.js +117 -2
- package/dist/team/__tests__/state-root.test.js.map +1 -1
- package/dist/team/__tests__/tmux-session.test.js +887 -92
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/__tests__/tmux-source-authority-realtmux.test.d.ts +2 -0
- package/dist/team/__tests__/tmux-source-authority-realtmux.test.d.ts.map +1 -0
- package/dist/team/__tests__/tmux-source-authority-realtmux.test.js +149 -0
- package/dist/team/__tests__/tmux-source-authority-realtmux.test.js.map +1 -0
- package/dist/team/__tests__/tmux-test-fixture.d.ts +69 -0
- package/dist/team/__tests__/tmux-test-fixture.d.ts.map +1 -1
- package/dist/team/__tests__/tmux-test-fixture.js +242 -15
- package/dist/team/__tests__/tmux-test-fixture.js.map +1 -1
- package/dist/team/__tests__/tmux-test-fixture.test.js +177 -1
- package/dist/team/__tests__/tmux-test-fixture.test.js.map +1 -1
- package/dist/team/__tests__/worker-runtime-identity.test.js +45 -72
- package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
- package/dist/team/notice-ledger.d.ts +54 -0
- package/dist/team/notice-ledger.d.ts.map +1 -0
- package/dist/team/notice-ledger.js +259 -0
- package/dist/team/notice-ledger.js.map +1 -0
- package/dist/team/phase-controller.d.ts.map +1 -1
- package/dist/team/phase-controller.js +2 -0
- package/dist/team/phase-controller.js.map +1 -1
- package/dist/team/runtime.d.ts +3 -1
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +314 -142
- package/dist/team/runtime.js.map +1 -1
- package/dist/team/scaling.d.ts.map +1 -1
- package/dist/team/scaling.js +161 -91
- package/dist/team/scaling.js.map +1 -1
- package/dist/team/state/monitor.d.ts +10 -0
- package/dist/team/state/monitor.d.ts.map +1 -1
- package/dist/team/state/monitor.js +13 -0
- package/dist/team/state/monitor.js.map +1 -1
- package/dist/team/state/types.d.ts +15 -0
- package/dist/team/state/types.d.ts.map +1 -1
- package/dist/team/state/types.js.map +1 -1
- package/dist/team/state-root.d.ts +12 -0
- package/dist/team/state-root.d.ts.map +1 -1
- package/dist/team/state-root.js +177 -0
- package/dist/team/state-root.js.map +1 -1
- package/dist/team/state.d.ts +18 -0
- package/dist/team/state.d.ts.map +1 -1
- package/dist/team/state.js +36 -0
- package/dist/team/state.js.map +1 -1
- package/dist/team/tmux-session.d.ts +40 -4
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +513 -166
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/ultragoal/__tests__/artifacts.test.js +950 -2
- package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
- package/dist/ultragoal/__tests__/docs-contract.test.js +1 -0
- package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
- package/dist/ultragoal/artifacts.d.ts +35 -1
- package/dist/ultragoal/artifacts.d.ts.map +1 -1
- package/dist/ultragoal/artifacts.js +232 -46
- package/dist/ultragoal/artifacts.js.map +1 -1
- package/dist/utils/__tests__/file-durability.test.js +58 -1
- package/dist/utils/__tests__/file-durability.test.js.map +1 -1
- package/dist/utils/__tests__/paths.test.js +21 -1
- package/dist/utils/__tests__/paths.test.js.map +1 -1
- package/dist/utils/file-durability.d.ts +10 -0
- package/dist/utils/file-durability.d.ts.map +1 -1
- package/dist/utils/file-durability.js +34 -4
- package/dist/utils/file-durability.js.map +1 -1
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +14 -6
- package/dist/utils/paths.js.map +1 -1
- package/dist/verification/__tests__/ci-rust-gates.test.js +49 -4
- package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
- package/dist/verification/__tests__/explore-harness-release-workflow.test.js +60 -0
- package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +1 -1
- package/dist/verification/__tests__/native-release-manifest.test.js +157 -4
- package/dist/verification/__tests__/native-release-manifest.test.js.map +1 -1
- package/package.json +13 -5
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +81 -17
- package/plugins/oh-my-codex/skills/autopilot/SKILL.md +13 -5
- package/plugins/oh-my-codex/skills/cancel/SKILL.md +30 -85
- package/plugins/oh-my-codex/skills/ralph/SKILL.md +5 -5
- package/plugins/oh-my-codex/skills/ralplan/SKILL.md +15 -17
- package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +3 -2
- package/skills/autopilot/SKILL.md +13 -5
- package/skills/cancel/SKILL.md +30 -85
- package/skills/ralph/SKILL.md +5 -5
- package/skills/ralplan/SKILL.md +15 -17
- package/skills/ultragoal/SKILL.md +3 -2
- package/src/scripts/__tests__/codex-native-hook.test.ts +6091 -540
- package/src/scripts/__tests__/issue-3293-callsite-parity.test.ts +187 -0
- package/src/scripts/__tests__/issue-3293-cancel-transaction.test.ts +189 -0
- package/src/scripts/__tests__/issue-3293-hook-owned-cancel.test.ts +187 -0
- package/src/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.ts +591 -0
- package/src/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.ts +701 -0
- package/src/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.ts +211 -0
- package/src/scripts/__tests__/notify-state-io.test.ts +29 -0
- package/src/scripts/__tests__/notify-tmux-injection.test.ts +26 -1
- package/src/scripts/__tests__/postinstall.test.ts +23 -1
- package/src/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.ts +27 -8
- package/src/scripts/__tests__/role-intent-durable-recovery-3181.test.ts +61 -27
- package/src/scripts/__tests__/smoke-packed-install.test.ts +209 -18
- package/src/scripts/__tests__/verify-native-release-assets.test.ts +240 -0
- package/src/scripts/codex-native-hook.ts +3638 -475
- package/src/scripts/codex-native-pre-post.ts +43 -12
- package/src/scripts/generate-native-release-manifest.ts +83 -79
- package/src/scripts/notify-fallback-watcher.ts +38 -38
- package/src/scripts/notify-hook/state-io.ts +11 -0
- package/src/scripts/notify-hook/team-leader-nudge.ts +32 -4
- package/src/scripts/notify-hook/team-worker-stop.ts +23 -4
- package/src/scripts/notify-hook/team-worker.ts +43 -5
- package/src/scripts/notify-hook/tmux-injection.ts +5 -1
- package/src/scripts/notify-hook.ts +83 -82
- package/src/scripts/postinstall-advisory.ts +67 -0
- package/src/scripts/postinstall.ts +1 -1
- package/src/scripts/smoke-packed-install.ts +446 -59
- package/src/scripts/sync-plugin-mirror.ts +17 -1
- package/src/scripts/verify-native-release-assets.ts +39 -31
- package/templates/AGENTS.md +1 -0
package/dist/hooks/session.js
CHANGED
|
@@ -5,14 +5,26 @@
|
|
|
5
5
|
* serialized by an adjacent lock directory and become visible only after an
|
|
6
6
|
* atomic rename of a transaction-owned temporary file.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { execFileSync as nodeExecFileSync } from 'node:child_process';
|
|
9
|
+
import { appendFile, mkdtemp as nodeMkdtemp, link as nodeLink, lstat as nodeLstat, mkdir as nodeMkdir, open, readFile as nodeReadFile, readdir as nodeReaddir, realpath, rename as nodeRename, rmdir as nodeRmdir, rm, unlink as nodeUnlink, writeFile as nodeWriteFile, } from 'fs/promises';
|
|
10
|
+
import { appendFileSync, closeSync, constants as fsConstants, fstatSync, openSync, readFileSync } from 'fs';
|
|
11
|
+
import { tmpdir } from 'node:os';
|
|
10
12
|
import { createHash, randomUUID } from 'crypto';
|
|
11
|
-
import { dirname, join } from 'path';
|
|
13
|
+
import { basename, dirname, isAbsolute, join } from 'path';
|
|
12
14
|
import { omxRoot, omxLogsDir, sameFilePath } from '../utils/paths.js';
|
|
15
|
+
import { resolveRuntimeBinaryPath } from '../runtime/bridge.js';
|
|
13
16
|
import { getBaseStateDirWithSource, resolveWorkingDirectoryForState, } from '../mcp/state-paths.js';
|
|
14
17
|
import { emitDegradedDurabilityWarning, recordRegularFileSyncOutcome, syncRegularFile, } from '../utils/file-durability.js';
|
|
18
|
+
export class CommittedLaunchBlockedError extends Error {
|
|
19
|
+
secondaryFailures;
|
|
20
|
+
name = 'CommittedLaunchBlockedError';
|
|
21
|
+
constructor(secondaryFailures) {
|
|
22
|
+
super('Session pointer committed, but lock release left recovery evidence.');
|
|
23
|
+
this.secondaryFailures = secondaryFailures;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
15
26
|
const SESSION_FILE = 'session.json';
|
|
27
|
+
const SESSION_OWNER_FILE = 'session-owner.json';
|
|
16
28
|
const HISTORY_FILE = 'session-history.jsonl';
|
|
17
29
|
const SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
|
|
18
30
|
const SESSION_POINTER_TOKEN_PATTERN = /^[A-Za-z0-9_-]{16,128}$/;
|
|
@@ -20,6 +32,14 @@ const SHA256_PATTERN = /^[a-f0-9]{64}$/;
|
|
|
20
32
|
const LOCK_RETRY_DELAYS_MS = [25, 50, 100];
|
|
21
33
|
const DEFAULT_POINTER_TIMEOUT_MS = 5_000;
|
|
22
34
|
const NATIVE_POINTER_TIMEOUT_MS = 2_000;
|
|
35
|
+
function traceSessionFinalizationOperation(operation) {
|
|
36
|
+
if (process.env.OMX_TEST_DETACHED_TRACE !== '1')
|
|
37
|
+
return;
|
|
38
|
+
const tracePath = process.env.OMX_TEST_DETACHED_TRACE_PATH;
|
|
39
|
+
if (!tracePath)
|
|
40
|
+
return;
|
|
41
|
+
appendFileSync(tracePath, `[omx-test-session-finalization] ${operation}\n`);
|
|
42
|
+
}
|
|
23
43
|
/**
|
|
24
44
|
* Convert arbitrary input into a valid session ID without exposing validator
|
|
25
45
|
* exceptions to lifecycle or hook inputs.
|
|
@@ -31,9 +51,9 @@ export function normalizeSessionId(value) {
|
|
|
31
51
|
return SESSION_ID_PATTERN.test(trimmed) ? trimmed : undefined;
|
|
32
52
|
}
|
|
33
53
|
/** Resolve the one exact pointer path used by an operation. */
|
|
34
|
-
export function resolveSessionPointerContext(cwd) {
|
|
35
|
-
const normalizedCwd = resolveWorkingDirectoryForState(cwd);
|
|
36
|
-
const { baseStateDir, rootSource } = getBaseStateDirWithSource(normalizedCwd);
|
|
54
|
+
export function resolveSessionPointerContext(cwd, env = process.env) {
|
|
55
|
+
const normalizedCwd = resolveWorkingDirectoryForState(cwd, env);
|
|
56
|
+
const { baseStateDir, rootSource } = getBaseStateDirWithSource(normalizedCwd, env);
|
|
37
57
|
const pointerPath = join(baseStateDir, SESSION_FILE);
|
|
38
58
|
return {
|
|
39
59
|
cwd: normalizedCwd,
|
|
@@ -43,6 +63,26 @@ export function resolveSessionPointerContext(cwd) {
|
|
|
43
63
|
lockPath: `${pointerPath}.lock`,
|
|
44
64
|
};
|
|
45
65
|
}
|
|
66
|
+
function resolveNativeSessionOwnerContext(cwd, nativeSessionId) {
|
|
67
|
+
const root = resolveSessionPointerContext(cwd);
|
|
68
|
+
const normalized = normalizeSessionId(nativeSessionId);
|
|
69
|
+
if (!normalized) {
|
|
70
|
+
throw resolvedAbort(root, {
|
|
71
|
+
code: 'session_pointer_io_failure',
|
|
72
|
+
operation: 'pointer-classify',
|
|
73
|
+
lockPath: root.lockPath,
|
|
74
|
+
reason: 'A valid native session ID is required for owner evidence.',
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
const baseStateDir = join(root.baseStateDir, 'sessions', normalized);
|
|
78
|
+
const sessionPath = join(baseStateDir, SESSION_OWNER_FILE);
|
|
79
|
+
return {
|
|
80
|
+
...root,
|
|
81
|
+
baseStateDir,
|
|
82
|
+
sessionPath,
|
|
83
|
+
lockPath: `${sessionPath}.lock`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
46
86
|
function attemptedStateRootSource() {
|
|
47
87
|
try {
|
|
48
88
|
if (process.env.OMX_TEAM_STATE_ROOT?.trim())
|
|
@@ -139,6 +179,9 @@ const defaultFsDependencies = {
|
|
|
139
179
|
rename: async (from, to) => {
|
|
140
180
|
await nodeRename(from, to);
|
|
141
181
|
},
|
|
182
|
+
link: async (path, dest) => {
|
|
183
|
+
await nodeLink(path, dest);
|
|
184
|
+
},
|
|
142
185
|
unlink: async (path) => {
|
|
143
186
|
await nodeUnlink(path);
|
|
144
187
|
},
|
|
@@ -146,6 +189,28 @@ const defaultFsDependencies = {
|
|
|
146
189
|
await nodeRmdir(path);
|
|
147
190
|
},
|
|
148
191
|
};
|
|
192
|
+
async function defaultRecoveryRenameNoReplace(from, to) {
|
|
193
|
+
if (!from.trim() || !to.trim() || !isAbsolute(from) || !isAbsolute(to))
|
|
194
|
+
return 'unsupported';
|
|
195
|
+
try {
|
|
196
|
+
const stdout = nodeExecFileSync(resolveRuntimeBinaryPath(), ['fs-rename-no-replace', from, to], {
|
|
197
|
+
encoding: 'utf8',
|
|
198
|
+
maxBuffer: 64 * 1024,
|
|
199
|
+
shell: false,
|
|
200
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
201
|
+
timeout: 10_000,
|
|
202
|
+
});
|
|
203
|
+
const parsed = JSON.parse(String(stdout));
|
|
204
|
+
if (parsed.outcome === 'moved' || parsed.outcome === 'not-moved' || parsed.outcome === 'unsupported') {
|
|
205
|
+
return parsed.outcome;
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
// Missing binaries, spawn failures, hard runtime errors, and malformed output
|
|
210
|
+
// all fail closed; recovery must never fall back to an overwriting rename.
|
|
211
|
+
}
|
|
212
|
+
return 'unsupported';
|
|
213
|
+
}
|
|
149
214
|
/** @internal Exposed only so source-module tests can verify default ESRCH handling. */
|
|
150
215
|
export function __createDefaultPidProbeForTests(killZero) {
|
|
151
216
|
return (pid) => {
|
|
@@ -163,26 +228,40 @@ function defaultProbePid(pid) {
|
|
|
163
228
|
process.kill(targetPid, 0);
|
|
164
229
|
})(pid);
|
|
165
230
|
}
|
|
166
|
-
function
|
|
167
|
-
if (platform
|
|
168
|
-
return
|
|
169
|
-
|
|
170
|
-
if (
|
|
171
|
-
return {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
231
|
+
function defaultObserveProcess(pid, platform) {
|
|
232
|
+
if (platform === 'linux') {
|
|
233
|
+
return observeLinuxProcess(pid);
|
|
234
|
+
}
|
|
235
|
+
if (platform !== 'darwin' && platform !== 'win32') {
|
|
236
|
+
return { kind: 'unsupported' };
|
|
237
|
+
}
|
|
238
|
+
try {
|
|
239
|
+
const stdout = nodeExecFileSync(resolveRuntimeBinaryPath(), ['process-identity', String(pid)], {
|
|
240
|
+
encoding: 'utf8',
|
|
241
|
+
timeout: 3000,
|
|
242
|
+
maxBuffer: 64 * 1024,
|
|
243
|
+
shell: false,
|
|
244
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
245
|
+
});
|
|
246
|
+
return parseProcessIdentityOutput(stdout);
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
return { kind: 'unsupported' };
|
|
250
|
+
}
|
|
178
251
|
}
|
|
179
252
|
const defaultTransactionDependencies = {
|
|
180
253
|
fs: defaultFsDependencies,
|
|
181
254
|
nowMs: () => Date.now(),
|
|
182
255
|
sleep: async (ms) => await new Promise((resolve) => setTimeout(resolve, ms)),
|
|
183
256
|
token: () => randomUUID(),
|
|
257
|
+
runtimePlatform: process.platform,
|
|
184
258
|
probePid: defaultProbePid,
|
|
185
|
-
|
|
259
|
+
observeProcess: defaultObserveProcess,
|
|
260
|
+
atomicRenameNoReplace: defaultRecoveryRenameNoReplace,
|
|
261
|
+
};
|
|
262
|
+
export const defaultProcessInspectionProvider = {
|
|
263
|
+
probePid: defaultProbePid,
|
|
264
|
+
observeProcess: defaultObserveProcess,
|
|
186
265
|
};
|
|
187
266
|
let transactionDependencies = defaultTransactionDependencies;
|
|
188
267
|
/** @internal Source-module test harness. Always reset after a test. */
|
|
@@ -234,13 +313,118 @@ function readLinuxProcessIdentity(pid) {
|
|
|
234
313
|
return null;
|
|
235
314
|
}
|
|
236
315
|
}
|
|
237
|
-
|
|
316
|
+
const NODE_PLATFORMS = new Set([
|
|
317
|
+
'aix', 'android', 'darwin', 'freebsd', 'haiku', 'linux', 'openbsd', 'sunos', 'win32', 'cygwin', 'netbsd',
|
|
318
|
+
]);
|
|
319
|
+
const PROCESS_BIRTH_PATTERN = /^\d+(?:\.\d+)?$/;
|
|
320
|
+
function isNodePlatform(value) {
|
|
321
|
+
return typeof value === 'string' && NODE_PLATFORMS.has(value);
|
|
322
|
+
}
|
|
323
|
+
function isValidBirth(value) {
|
|
324
|
+
return typeof value === 'string' && PROCESS_BIRTH_PATTERN.test(value);
|
|
325
|
+
}
|
|
326
|
+
export function isValidProcessIdentity(value) {
|
|
327
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
328
|
+
return false;
|
|
329
|
+
const identity = value;
|
|
330
|
+
if (Object.keys(identity).some((key) => key !== 'platform' && key !== 'birth' && key !== 'cmdline_hash'))
|
|
331
|
+
return false;
|
|
332
|
+
return isNodePlatform(identity.platform)
|
|
333
|
+
&& isValidBirth(identity.birth)
|
|
334
|
+
&& (identity.cmdline_hash === undefined
|
|
335
|
+
|| typeof identity.cmdline_hash === 'string' && SHA256_PATTERN.test(identity.cmdline_hash));
|
|
336
|
+
}
|
|
337
|
+
function isComparableProcessIdentity(value) {
|
|
338
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
339
|
+
return false;
|
|
340
|
+
const identity = value;
|
|
341
|
+
return isNodePlatform(identity.platform)
|
|
342
|
+
&& typeof identity.birth === 'string'
|
|
343
|
+
&& identity.birth.length > 0
|
|
344
|
+
&& (identity.cmdline_hash === undefined
|
|
345
|
+
|| typeof identity.cmdline_hash === 'string' && SHA256_PATTERN.test(identity.cmdline_hash));
|
|
346
|
+
}
|
|
347
|
+
function observeLinuxProcess(pid) {
|
|
348
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
349
|
+
return { kind: 'error' };
|
|
350
|
+
const identity = readLinuxProcessIdentity(pid);
|
|
351
|
+
if (identity) {
|
|
352
|
+
const cmdlineHash = hashCmdline(identity.cmdline);
|
|
353
|
+
return {
|
|
354
|
+
kind: 'identity',
|
|
355
|
+
identity: {
|
|
356
|
+
platform: 'linux',
|
|
357
|
+
birth: String(identity.startTicks),
|
|
358
|
+
...(cmdlineHash ? { cmdline_hash: cmdlineHash } : {}),
|
|
359
|
+
},
|
|
360
|
+
};
|
|
361
|
+
}
|
|
362
|
+
let statContent;
|
|
238
363
|
try {
|
|
239
|
-
|
|
240
|
-
|
|
364
|
+
statContent = readFileSync(`/proc/${pid}/stat`, 'utf8');
|
|
365
|
+
}
|
|
366
|
+
catch (error) {
|
|
367
|
+
const code = errorCode(error);
|
|
368
|
+
if (code === 'ENOENT' || code === 'ESRCH')
|
|
369
|
+
return { kind: 'gone' };
|
|
370
|
+
if (code === 'EACCES' || code === 'EPERM')
|
|
371
|
+
return { kind: 'denied' };
|
|
372
|
+
return { kind: 'error' };
|
|
373
|
+
}
|
|
374
|
+
if (!statContent)
|
|
375
|
+
return { kind: 'error' };
|
|
376
|
+
const probe = defaultProbePid(pid);
|
|
377
|
+
if (probe === 'dead')
|
|
378
|
+
return { kind: 'gone' };
|
|
379
|
+
if (probe === 'indeterminate')
|
|
380
|
+
return { kind: 'denied' };
|
|
381
|
+
return { kind: 'error' };
|
|
382
|
+
}
|
|
383
|
+
function parseProcessIdentityOutput(stdout) {
|
|
384
|
+
try {
|
|
385
|
+
const parsed = JSON.parse(stdout);
|
|
386
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
387
|
+
return { kind: 'error' };
|
|
388
|
+
const value = parsed;
|
|
389
|
+
const keys = Object.keys(value);
|
|
390
|
+
const outcome = value.outcome;
|
|
391
|
+
if (outcome !== undefined) {
|
|
392
|
+
if (typeof outcome !== 'string')
|
|
393
|
+
return { kind: 'error' };
|
|
394
|
+
if (outcome === 'gone' || outcome === 'denied' || outcome === 'unsupported') {
|
|
395
|
+
if (keys.length !== 1)
|
|
396
|
+
return { kind: 'error' };
|
|
397
|
+
if (outcome === 'gone')
|
|
398
|
+
return { kind: 'gone' };
|
|
399
|
+
if (outcome === 'denied')
|
|
400
|
+
return { kind: 'denied' };
|
|
401
|
+
return { kind: 'unsupported' };
|
|
402
|
+
}
|
|
403
|
+
if (outcome === 'error')
|
|
404
|
+
return { kind: 'error' };
|
|
405
|
+
return { kind: 'error' };
|
|
406
|
+
}
|
|
407
|
+
if (keys.some((key) => key !== 'platform' && key !== 'birth' && key !== 'cmdline')) {
|
|
408
|
+
return { kind: 'error' };
|
|
409
|
+
}
|
|
410
|
+
const platform = value.platform;
|
|
411
|
+
const birth = value.birth;
|
|
412
|
+
if (!isNodePlatform(platform) || !isValidBirth(birth))
|
|
413
|
+
return { kind: 'error' };
|
|
414
|
+
if (value.cmdline !== undefined && typeof value.cmdline !== 'string')
|
|
415
|
+
return { kind: 'error' };
|
|
416
|
+
const cmdlineHash = hashCmdline(value.cmdline);
|
|
417
|
+
return {
|
|
418
|
+
kind: 'identity',
|
|
419
|
+
identity: {
|
|
420
|
+
platform,
|
|
421
|
+
birth,
|
|
422
|
+
...(cmdlineHash ? { cmdline_hash: cmdlineHash } : {}),
|
|
423
|
+
},
|
|
424
|
+
};
|
|
241
425
|
}
|
|
242
426
|
catch {
|
|
243
|
-
return
|
|
427
|
+
return { kind: 'error' };
|
|
244
428
|
}
|
|
245
429
|
}
|
|
246
430
|
/**
|
|
@@ -250,22 +434,122 @@ function defaultIsPidAlive(pid) {
|
|
|
250
434
|
export function isSessionStale(state, options = {}) {
|
|
251
435
|
if (!Number.isInteger(state.pid) || state.pid <= 0)
|
|
252
436
|
return true;
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
437
|
+
const runtimePlatform = options.platform ?? transactionDependencies.runtimePlatform ?? process.platform;
|
|
438
|
+
const probePid = options.probePid ?? (options.isPidAlive
|
|
439
|
+
? (pid) => options.isPidAlive(pid) ? 'alive' : 'dead'
|
|
440
|
+
: transactionDependencies.probePid);
|
|
441
|
+
const observeProcess = options.observeProcess ?? (options.readLinuxIdentity
|
|
442
|
+
? (pid, platform) => {
|
|
443
|
+
if (platform !== 'linux')
|
|
444
|
+
return { kind: 'unsupported' };
|
|
445
|
+
const identity = options.readLinuxIdentity(pid);
|
|
446
|
+
if (!identity)
|
|
447
|
+
return { kind: 'error' };
|
|
448
|
+
const cmdlineHash = hashCmdline(identity.cmdline);
|
|
449
|
+
return {
|
|
450
|
+
kind: 'identity',
|
|
451
|
+
identity: {
|
|
452
|
+
platform: 'linux',
|
|
453
|
+
birth: String(identity.startTicks),
|
|
454
|
+
...(cmdlineHash ? { cmdline_hash: cmdlineHash } : {}),
|
|
455
|
+
},
|
|
456
|
+
};
|
|
457
|
+
}
|
|
458
|
+
: transactionDependencies.observeProcess);
|
|
459
|
+
const dependencies = {
|
|
460
|
+
...transactionDependencies,
|
|
461
|
+
runtimePlatform,
|
|
462
|
+
probePid,
|
|
463
|
+
observeProcess,
|
|
464
|
+
};
|
|
465
|
+
return classifySessionProcess(state, dependencies) !== 'usable';
|
|
466
|
+
}
|
|
467
|
+
function safeObserve(provider, pid, runtimePlatform) {
|
|
468
|
+
try {
|
|
469
|
+
return provider.observeProcess(pid, runtimePlatform);
|
|
470
|
+
}
|
|
471
|
+
catch {
|
|
472
|
+
return { kind: 'error' };
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
function compareObservation(recorded, observation, runtimePlatform) {
|
|
476
|
+
if (!observation || typeof observation !== 'object' || observation.kind !== 'identity') {
|
|
477
|
+
return 'identity-unavailable';
|
|
478
|
+
}
|
|
479
|
+
const identity = observation.identity;
|
|
480
|
+
if (!isComparableProcessIdentity(identity) || identity.platform !== runtimePlatform) {
|
|
481
|
+
return 'identity-unavailable';
|
|
482
|
+
}
|
|
483
|
+
if (identity.birth !== recorded.birth)
|
|
484
|
+
return 'birth-mismatch-retryable';
|
|
485
|
+
if (recorded.cmdline_hash !== undefined && identity.cmdline_hash !== recorded.cmdline_hash) {
|
|
486
|
+
return 'identity-unavailable';
|
|
487
|
+
}
|
|
488
|
+
return 'match';
|
|
489
|
+
}
|
|
490
|
+
/** Compare recorded process birth evidence with a fresh provider observation. */
|
|
491
|
+
export function classifyRecordedIdentity(recorded, runtimePlatform, provider, pid) {
|
|
492
|
+
if (!isComparableProcessIdentity(recorded) || recorded.platform !== runtimePlatform || !Number.isInteger(pid) || pid <= 0) {
|
|
493
|
+
return { status: 'identity-unavailable' };
|
|
494
|
+
}
|
|
495
|
+
let probe;
|
|
496
|
+
try {
|
|
497
|
+
probe = provider.probePid(pid);
|
|
498
|
+
}
|
|
499
|
+
catch {
|
|
500
|
+
return { status: 'identity-unavailable' };
|
|
501
|
+
}
|
|
502
|
+
if (probe === 'dead')
|
|
503
|
+
return { status: 'gone' };
|
|
504
|
+
if (probe !== 'alive')
|
|
505
|
+
return { status: 'identity-unavailable' };
|
|
506
|
+
const first = compareObservation(recorded, safeObserve(provider, pid, runtimePlatform), runtimePlatform);
|
|
507
|
+
if (first === 'birth-mismatch-retryable') {
|
|
508
|
+
const second = compareObservation(recorded, safeObserve(provider, pid, runtimePlatform), runtimePlatform);
|
|
509
|
+
if (second === 'match')
|
|
510
|
+
return { status: 'match' };
|
|
511
|
+
if (second === 'birth-mismatch-retryable' || second === 'birth-mismatch')
|
|
512
|
+
return { status: 'birth-mismatch' };
|
|
513
|
+
return { status: 'identity-unavailable' };
|
|
514
|
+
}
|
|
515
|
+
if (first === 'match')
|
|
516
|
+
return { status: 'match' };
|
|
517
|
+
if (first === 'birth-mismatch')
|
|
518
|
+
return { status: 'birth-mismatch' };
|
|
519
|
+
return { status: 'identity-unavailable' };
|
|
520
|
+
}
|
|
521
|
+
function recordedIdentityForState(state, runtimePlatform) {
|
|
522
|
+
if (state.identity_schema_version !== undefined && state.identity_schema_version !== 2)
|
|
523
|
+
return 'invalid';
|
|
524
|
+
if (state.process_identity !== undefined) {
|
|
525
|
+
if (state.identity_schema_version !== 2 || !isValidProcessIdentity(state.process_identity))
|
|
526
|
+
return 'invalid';
|
|
527
|
+
if (state.platform !== undefined && state.platform !== state.process_identity.platform)
|
|
528
|
+
return 'invalid';
|
|
529
|
+
return state.process_identity;
|
|
530
|
+
}
|
|
531
|
+
if (state.identity_schema_version === 2)
|
|
532
|
+
return 'invalid';
|
|
533
|
+
const recordedPlatform = state.platform ?? runtimePlatform;
|
|
534
|
+
if (recordedPlatform === 'linux' && isValidStartTicks(state.pid_start_ticks)) {
|
|
535
|
+
const cmdlineHash = hashCmdline(state.pid_cmdline);
|
|
536
|
+
return {
|
|
537
|
+
platform: 'linux',
|
|
538
|
+
birth: String(state.pid_start_ticks),
|
|
539
|
+
...(cmdlineHash ? { cmdline_hash: cmdlineHash } : {}),
|
|
540
|
+
};
|
|
541
|
+
}
|
|
542
|
+
return null;
|
|
543
|
+
}
|
|
544
|
+
function probeIdentitylessProcess(pid, dependencies) {
|
|
545
|
+
let probe;
|
|
546
|
+
try {
|
|
547
|
+
probe = dependencies.probePid(pid);
|
|
548
|
+
}
|
|
549
|
+
catch {
|
|
550
|
+
return 'identity-indeterminate';
|
|
551
|
+
}
|
|
552
|
+
return probe === 'dead' ? 'stale-dead' : 'identity-indeterminate';
|
|
269
553
|
}
|
|
270
554
|
export function isSessionStateAuthoritativeForCwd(state, cwd) {
|
|
271
555
|
if (!normalizeSessionId(state.session_id))
|
|
@@ -285,54 +569,47 @@ export function isSessionStateUsable(state, cwd, options = {}) {
|
|
|
285
569
|
if (typeof state.cwd === 'string' && state.cwd.trim() && !isSessionStateAuthoritativeForCwd(state, cwd))
|
|
286
570
|
return false;
|
|
287
571
|
const hasPidMetadata = Number.isInteger(state.pid) && state.pid > 0;
|
|
288
|
-
const
|
|
289
|
-
|| typeof state.pid_cmdline === 'string'
|
|
290
|
-
|
|
572
|
+
const hasIdentityMetadata = typeof state.pid_start_ticks === 'number'
|
|
573
|
+
|| typeof state.pid_cmdline === 'string'
|
|
574
|
+
|| state.identity_schema_version !== undefined
|
|
575
|
+
|| state.process_identity !== undefined;
|
|
576
|
+
return !hasPidMetadata && !hasIdentityMetadata || !isSessionStale(state, options);
|
|
291
577
|
}
|
|
292
578
|
function isValidStartTicks(value) {
|
|
293
579
|
return typeof value === 'number' && Number.isInteger(value) && value >= 0;
|
|
294
580
|
}
|
|
295
581
|
function classifySessionProcess(state, dependencies) {
|
|
296
582
|
const hasPidMetadata = Number.isInteger(state.pid) && state.pid > 0;
|
|
297
|
-
const hasIdentityMetadata = typeof state.pid_start_ticks === 'number'
|
|
583
|
+
const hasIdentityMetadata = typeof state.pid_start_ticks === 'number'
|
|
584
|
+
|| typeof state.pid_cmdline === 'string'
|
|
585
|
+
|| state.identity_schema_version !== undefined
|
|
586
|
+
|| state.process_identity !== undefined;
|
|
298
587
|
if (!hasPidMetadata && !hasIdentityMetadata)
|
|
299
588
|
return 'usable';
|
|
300
589
|
if (!hasPidMetadata)
|
|
301
590
|
return 'identity-indeterminate';
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
}
|
|
306
|
-
catch {
|
|
307
|
-
return 'identity-indeterminate';
|
|
308
|
-
}
|
|
309
|
-
if (pidStatus === 'dead')
|
|
310
|
-
return 'stale-dead';
|
|
311
|
-
if (pidStatus !== 'alive')
|
|
312
|
-
return 'identity-indeterminate';
|
|
313
|
-
const platform = state.platform ?? process.platform;
|
|
314
|
-
if (platform !== 'linux')
|
|
315
|
-
return 'usable';
|
|
316
|
-
if (!isValidStartTicks(state.pid_start_ticks))
|
|
317
|
-
return 'identity-indeterminate';
|
|
318
|
-
let liveIdentity;
|
|
319
|
-
try {
|
|
320
|
-
liveIdentity = dependencies.readProcessIdentity(state.pid, platform);
|
|
321
|
-
}
|
|
322
|
-
catch {
|
|
323
|
-
return 'identity-indeterminate';
|
|
324
|
-
}
|
|
325
|
-
if (!liveIdentity || !isValidStartTicks(liveIdentity.startTicks))
|
|
326
|
-
return 'identity-indeterminate';
|
|
327
|
-
if (liveIdentity.startTicks !== state.pid_start_ticks)
|
|
328
|
-
return 'stale-dead';
|
|
329
|
-
if (liveIdentity.status !== 'matching')
|
|
330
|
-
return 'identity-indeterminate';
|
|
331
|
-
const expectedCmdlineHash = hashCmdline(state.pid_cmdline);
|
|
332
|
-
if (expectedCmdlineHash && liveIdentity.cmdlineHash !== expectedCmdlineHash) {
|
|
591
|
+
const runtimePlatform = dependencies.runtimePlatform;
|
|
592
|
+
const recorded = recordedIdentityForState(state, runtimePlatform);
|
|
593
|
+
if (recorded === 'invalid')
|
|
333
594
|
return 'identity-indeterminate';
|
|
595
|
+
if (!recorded)
|
|
596
|
+
return probeIdentitylessProcess(state.pid, dependencies);
|
|
597
|
+
const classification = classifyRecordedIdentity(recorded, runtimePlatform, dependencies, state.pid);
|
|
598
|
+
switch (classification.status) {
|
|
599
|
+
case 'match': return 'usable';
|
|
600
|
+
case 'gone':
|
|
601
|
+
case 'birth-mismatch': return 'stale-dead';
|
|
602
|
+
case 'identity-unavailable': return 'identity-indeterminate';
|
|
334
603
|
}
|
|
335
|
-
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Classify process liveness for an already-parsed selected session pointer.
|
|
607
|
+
* Callers that need cwd/state_root authority must validate those against the
|
|
608
|
+
* SAME snapshot themselves; this evaluates only pid/start-tick evidence so a
|
|
609
|
+
* single immutable snapshot can drive both decisions.
|
|
610
|
+
*/
|
|
611
|
+
export function classifySessionStateLiveness(state) {
|
|
612
|
+
return classifySessionProcess(state, transactionDependencies);
|
|
336
613
|
}
|
|
337
614
|
function classifyParsedSessionPointer(context, value, raw) {
|
|
338
615
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
@@ -344,6 +621,16 @@ function classifyParsedSessionPointer(context, value, raw) {
|
|
|
344
621
|
if (typeof state.cwd === 'string' && state.cwd.trim() && !isSessionStateAuthoritativeForCwd(state, context.cwd)) {
|
|
345
622
|
return { status: 'foreign-cwd', raw, state };
|
|
346
623
|
}
|
|
624
|
+
if (state.identity_schema_version !== undefined && state.identity_schema_version !== 2)
|
|
625
|
+
return { status: 'identity-indeterminate', raw, state };
|
|
626
|
+
if (state.process_identity !== undefined
|
|
627
|
+
&& (state.identity_schema_version !== 2
|
|
628
|
+
|| !isValidProcessIdentity(state.process_identity)
|
|
629
|
+
|| state.platform !== undefined && state.platform !== state.process_identity.platform)) {
|
|
630
|
+
return { status: 'malformed', raw, state };
|
|
631
|
+
}
|
|
632
|
+
if (state.identity_schema_version === 2 && state.process_identity === undefined)
|
|
633
|
+
return { status: 'malformed', raw, state };
|
|
347
634
|
const processStatus = classifySessionProcess(state, transactionDependencies);
|
|
348
635
|
return { status: processStatus, state, raw };
|
|
349
636
|
}
|
|
@@ -382,9 +669,9 @@ export async function readUsableSessionStateFromContext(context) {
|
|
|
382
669
|
return state && isSessionStateUsable(state, context.cwd) ? state : null;
|
|
383
670
|
}
|
|
384
671
|
/** Read current session state from the exact selected root. */
|
|
385
|
-
export async function readSessionState(cwd) {
|
|
672
|
+
export async function readSessionState(cwd, env = process.env) {
|
|
386
673
|
try {
|
|
387
|
-
return await readSessionStateFromContext(resolveSessionPointerContext(cwd));
|
|
674
|
+
return await readSessionStateFromContext(resolveSessionPointerContext(cwd, env));
|
|
388
675
|
}
|
|
389
676
|
catch {
|
|
390
677
|
return null;
|
|
@@ -400,7 +687,7 @@ export async function readUsableSessionState(cwd, options = {}) {
|
|
|
400
687
|
return null;
|
|
401
688
|
}
|
|
402
689
|
}
|
|
403
|
-
function createSessionState(cwd, sessionId, pid, platform,
|
|
690
|
+
function createSessionState(cwd, stateRoot, sessionId, pid, platform, identity, options = {}) {
|
|
404
691
|
const nowIso = options.nowIso ?? new Date().toISOString();
|
|
405
692
|
const nativeSessionId = normalizeNonempty(options.nativeSessionId);
|
|
406
693
|
const previousNativeSessionId = normalizeNonempty(options.previousNativeSessionId);
|
|
@@ -408,6 +695,27 @@ function createSessionState(cwd, sessionId, pid, platform, linuxIdentity, option
|
|
|
408
695
|
const ownerOmxSessionId = normalizeSessionId(options.ownerOmxSessionId);
|
|
409
696
|
const tmuxSessionName = normalizeNonempty(options.tmuxSessionName);
|
|
410
697
|
const tmuxPaneId = normalizeNonempty(options.tmuxPaneId);
|
|
698
|
+
const processIdentity = identity && 'birth' in identity
|
|
699
|
+
? identity
|
|
700
|
+
: identity
|
|
701
|
+
? {
|
|
702
|
+
platform: 'linux',
|
|
703
|
+
birth: String(identity.startTicks),
|
|
704
|
+
...(hashCmdline(identity.cmdline) ? { cmdline_hash: hashCmdline(identity.cmdline) } : {}),
|
|
705
|
+
}
|
|
706
|
+
: undefined;
|
|
707
|
+
const publishIdentity = processIdentity && isValidProcessIdentity(processIdentity) && processIdentity.platform === platform
|
|
708
|
+
? processIdentity
|
|
709
|
+
: undefined;
|
|
710
|
+
const legacyStartTicks = identity && 'startTicks' in identity
|
|
711
|
+
? identity.startTicks
|
|
712
|
+
: publishIdentity?.platform === 'linux' && Number.isSafeInteger(Number(publishIdentity.birth))
|
|
713
|
+
? Number(publishIdentity.birth)
|
|
714
|
+
: undefined;
|
|
715
|
+
const legacyLinuxIdentity = publishIdentity?.platform === 'linux' && publishIdentity.birth === String(legacyStartTicks)
|
|
716
|
+
? readLinuxProcessIdentity(pid)
|
|
717
|
+
: null;
|
|
718
|
+
const legacyCmdline = identity && 'cmdline' in identity ? identity.cmdline : legacyLinuxIdentity?.cmdline;
|
|
411
719
|
return {
|
|
412
720
|
session_id: sessionId,
|
|
413
721
|
...(nativeSessionId ? { native_session_id: nativeSessionId } : {}),
|
|
@@ -415,15 +723,23 @@ function createSessionState(cwd, sessionId, pid, platform, linuxIdentity, option
|
|
|
415
723
|
...(nativeSessionSwitchedAt ? { native_session_switched_at: nativeSessionSwitchedAt } : {}),
|
|
416
724
|
...(ownerOmxSessionId ? { owner_omx_session_id: ownerOmxSessionId } : {}),
|
|
417
725
|
started_at: options.startedAt ?? nowIso,
|
|
726
|
+
...(isValidToken(options.launchLineageToken) ? { launch_lineage_token: options.launchLineageToken } : {}),
|
|
418
727
|
cwd,
|
|
728
|
+
state_root: stateRoot,
|
|
419
729
|
pid,
|
|
420
730
|
platform,
|
|
421
|
-
...(
|
|
422
|
-
...(
|
|
731
|
+
...(publishIdentity ? { identity_schema_version: 2, process_identity: publishIdentity } : {}),
|
|
732
|
+
...(legacyStartTicks !== undefined ? { pid_start_ticks: legacyStartTicks } : {}),
|
|
733
|
+
...(legacyCmdline ? { pid_cmdline: legacyCmdline } : {}),
|
|
423
734
|
...(tmuxSessionName ? { tmux_session_name: tmuxSessionName } : {}),
|
|
424
735
|
...(tmuxPaneId ? { tmux_pane_id: tmuxPaneId } : {}),
|
|
425
736
|
};
|
|
426
737
|
}
|
|
738
|
+
function preserveExistingLaunchLineageToken(existing, state) {
|
|
739
|
+
return existing && Object.hasOwn(existing, 'launch_lineage_token')
|
|
740
|
+
? { ...state, launch_lineage_token: existing.launch_lineage_token }
|
|
741
|
+
: state;
|
|
742
|
+
}
|
|
427
743
|
function normalizeNonempty(value) {
|
|
428
744
|
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
429
745
|
}
|
|
@@ -431,7 +747,15 @@ function resolvePid(options) {
|
|
|
431
747
|
return Number.isInteger(options.pid) && options.pid && options.pid > 0 ? options.pid : process.pid;
|
|
432
748
|
}
|
|
433
749
|
function sessionIdentityFor(pid, platform) {
|
|
434
|
-
|
|
750
|
+
try {
|
|
751
|
+
const observation = transactionDependencies.observeProcess(pid, platform);
|
|
752
|
+
return observation.kind === 'identity' && isValidProcessIdentity(observation.identity) && observation.identity.platform === platform
|
|
753
|
+
? observation.identity
|
|
754
|
+
: null;
|
|
755
|
+
}
|
|
756
|
+
catch {
|
|
757
|
+
return null;
|
|
758
|
+
}
|
|
435
759
|
}
|
|
436
760
|
function currentOwnerAlias(state) {
|
|
437
761
|
return normalizeSessionId(state.owner_omx_session_id);
|
|
@@ -458,29 +782,38 @@ function isStartCompatible(existing, requestedSessionId) {
|
|
|
458
782
|
|| existing.native_session_id === requestedSessionId
|
|
459
783
|
|| currentOwnerAlias(existing) === requestedSessionId;
|
|
460
784
|
}
|
|
461
|
-
function getOmxLaunchSessionId(state) {
|
|
462
|
-
if (state.session_id.startsWith('omx-'))
|
|
463
|
-
return state.session_id;
|
|
464
|
-
const owner = currentOwnerAlias(state);
|
|
465
|
-
return owner?.startsWith('omx-') ? owner : undefined;
|
|
466
|
-
}
|
|
467
785
|
function parseLockOwner(raw) {
|
|
468
786
|
try {
|
|
469
|
-
const
|
|
470
|
-
if (!
|
|
787
|
+
const parsed = JSON.parse(raw);
|
|
788
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
471
789
|
return null;
|
|
472
|
-
|
|
790
|
+
const value = parsed;
|
|
791
|
+
if (!isValidToken(value.token) || typeof value.pid !== 'number' || !Number.isInteger(value.pid) || value.pid <= 0)
|
|
473
792
|
return null;
|
|
474
|
-
if (
|
|
793
|
+
if (!isNodePlatform(value.platform) || typeof value.created_at !== 'string' || !value.created_at)
|
|
475
794
|
return null;
|
|
795
|
+
if (value.version === 1) {
|
|
796
|
+
if (value.pid_start_ticks !== undefined && !isValidStartTicks(value.pid_start_ticks))
|
|
797
|
+
return null;
|
|
798
|
+
if (value.pid_cmdline_hash !== undefined
|
|
799
|
+
&& (typeof value.pid_cmdline_hash !== 'string' || !SHA256_PATTERN.test(value.pid_cmdline_hash))) {
|
|
800
|
+
return null;
|
|
801
|
+
}
|
|
802
|
+
return value;
|
|
476
803
|
}
|
|
477
|
-
if (value.
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
804
|
+
if (value.version === 2) {
|
|
805
|
+
const processIdentity = value.process_identity;
|
|
806
|
+
if (!isValidProcessIdentity(processIdentity) || processIdentity.platform !== value.platform)
|
|
807
|
+
return null;
|
|
808
|
+
if (value.pid_start_ticks !== undefined && !isValidStartTicks(value.pid_start_ticks))
|
|
809
|
+
return null;
|
|
810
|
+
if (value.pid_cmdline_hash !== undefined
|
|
811
|
+
&& (typeof value.pid_cmdline_hash !== 'string' || !SHA256_PATTERN.test(value.pid_cmdline_hash))) {
|
|
812
|
+
return null;
|
|
813
|
+
}
|
|
814
|
+
return value;
|
|
482
815
|
}
|
|
483
|
-
return
|
|
816
|
+
return null;
|
|
484
817
|
}
|
|
485
818
|
catch {
|
|
486
819
|
return null;
|
|
@@ -489,6 +822,17 @@ function parseLockOwner(raw) {
|
|
|
489
822
|
function isValidToken(value) {
|
|
490
823
|
return typeof value === 'string' && SESSION_POINTER_TOKEN_PATTERN.test(value);
|
|
491
824
|
}
|
|
825
|
+
function recordedIdentityForLockOwner(owner) {
|
|
826
|
+
if (owner.version === 2)
|
|
827
|
+
return owner.process_identity;
|
|
828
|
+
if (owner.platform !== 'linux' || !isValidStartTicks(owner.pid_start_ticks))
|
|
829
|
+
return null;
|
|
830
|
+
return {
|
|
831
|
+
platform: 'linux',
|
|
832
|
+
birth: String(owner.pid_start_ticks),
|
|
833
|
+
...(owner.pid_cmdline_hash ? { cmdline_hash: owner.pid_cmdline_hash } : {}),
|
|
834
|
+
};
|
|
835
|
+
}
|
|
492
836
|
async function inspectLockOwnerFile(ownerPath, missingStatus = 'missing') {
|
|
493
837
|
let raw;
|
|
494
838
|
try {
|
|
@@ -500,38 +844,26 @@ async function inspectLockOwnerFile(ownerPath, missingStatus = 'missing') {
|
|
|
500
844
|
const owner = parseLockOwner(raw);
|
|
501
845
|
if (!owner)
|
|
502
846
|
return { status: 'malformed' };
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
return { status: 'identity-indeterminate', owner };
|
|
516
|
-
}
|
|
517
|
-
let liveIdentity;
|
|
518
|
-
try {
|
|
519
|
-
liveIdentity = transactionDependencies.readProcessIdentity(owner.pid, owner.platform);
|
|
520
|
-
}
|
|
521
|
-
catch {
|
|
522
|
-
return { status: 'identity-indeterminate', owner };
|
|
523
|
-
}
|
|
524
|
-
if (!liveIdentity || !isValidStartTicks(liveIdentity.startTicks)) {
|
|
525
|
-
return { status: 'identity-indeterminate', owner };
|
|
847
|
+
const recorded = recordedIdentityForLockOwner(owner);
|
|
848
|
+
if (!recorded) {
|
|
849
|
+
let probe;
|
|
850
|
+
try {
|
|
851
|
+
probe = transactionDependencies.probePid(owner.pid);
|
|
852
|
+
}
|
|
853
|
+
catch {
|
|
854
|
+
return { status: 'identity-indeterminate', owner };
|
|
855
|
+
}
|
|
856
|
+
return probe === 'dead'
|
|
857
|
+
? { status: 'dead', owner }
|
|
858
|
+
: { status: 'identity-indeterminate', owner };
|
|
526
859
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
return { status: '
|
|
531
|
-
|
|
532
|
-
return { status: 'identity-indeterminate', owner };
|
|
860
|
+
const classification = classifyRecordedIdentity(recorded, transactionDependencies.runtimePlatform, transactionDependencies, owner.pid);
|
|
861
|
+
switch (classification.status) {
|
|
862
|
+
case 'match': return { status: 'live', owner };
|
|
863
|
+
case 'gone': return { status: 'dead', owner };
|
|
864
|
+
case 'birth-mismatch': return { status: 'reused', owner };
|
|
865
|
+
case 'identity-unavailable': return { status: 'identity-indeterminate', owner };
|
|
533
866
|
}
|
|
534
|
-
return { status: 'live', owner };
|
|
535
867
|
}
|
|
536
868
|
async function inspectLockOwner(lockPath) {
|
|
537
869
|
return await inspectLockOwnerFile(join(lockPath, 'owner.json'));
|
|
@@ -586,124 +918,292 @@ async function inspectSessionPointerLockAtContext(context) {
|
|
|
586
918
|
status,
|
|
587
919
|
lockPath: context.lockPath,
|
|
588
920
|
evidenceSource,
|
|
589
|
-
|
|
921
|
+
// Recoverable only on positively dead evidence (ESRCH), for canonical
|
|
922
|
+
// owner.json and pre-rename temp evidence alike; the atomic
|
|
923
|
+
// claim/quarantine protocol revalidates the exact entry, still-dead
|
|
924
|
+
// status, and owner token before quarantining. Live, reused,
|
|
925
|
+
// identity-indeterminate, ambiguous, symlink, malformed, unexpected,
|
|
926
|
+
// and io-error states stay fail-closed.
|
|
927
|
+
safeToRecover: status === 'dead',
|
|
590
928
|
};
|
|
591
929
|
}
|
|
592
930
|
/** Inspect only exact, regular owner evidence in the selected pointer lock. */
|
|
593
931
|
export async function inspectSessionPointerLock(cwd) {
|
|
594
932
|
return await inspectSessionPointerLockAtContext(resolveSessionPointerContext(cwd));
|
|
595
933
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
if (!inspection.safeToRecover) {
|
|
601
|
-
return { ...inspection, action: 'none', recovered: false, reason: inspection.status === 'absent' ? 'No session pointer lock exists.' : `Session pointer lock is not safe to recover (${inspection.status}).` };
|
|
602
|
-
}
|
|
603
|
-
let entries;
|
|
934
|
+
function sameRecoveryIdentity(stat, identity, kind) {
|
|
935
|
+
return !stat.isSymbolicLink() && (kind === 'file' ? stat.isFile() : stat.isDirectory()) && stat.dev === identity.dev && stat.ino === identity.ino;
|
|
936
|
+
}
|
|
937
|
+
async function lstatRecoveryPath(path) {
|
|
604
938
|
try {
|
|
605
|
-
|
|
606
|
-
}
|
|
607
|
-
catch {
|
|
608
|
-
return { ...inspection, action: 'none', recovered: false, reason: 'Unable to re-read session pointer lock evidence.' };
|
|
609
|
-
}
|
|
610
|
-
const evidenceName = inspection.evidenceSource === 'owner.json' ? 'owner.json' : entries.find((entry) => /^owner\.([A-Za-z0-9_-]{16,128})\.tmp$/.test(entry));
|
|
611
|
-
if (!evidenceName || entries.length !== 1) {
|
|
612
|
-
return { ...inspection, action: 'none', recovered: false, reason: 'Session pointer lock evidence changed before recovery claim.' };
|
|
939
|
+
return await transactionDependencies.fs.lstat(path);
|
|
613
940
|
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
941
|
+
catch (error) {
|
|
942
|
+
if (isNotFound(error))
|
|
943
|
+
return undefined;
|
|
944
|
+
throw error;
|
|
617
945
|
}
|
|
618
|
-
|
|
946
|
+
}
|
|
947
|
+
async function probeRecoveryRenameCapability() {
|
|
948
|
+
let probeDir;
|
|
619
949
|
try {
|
|
620
|
-
|
|
950
|
+
probeDir = await nodeMkdtemp(join(tmpdir(), 'omx-session-recovery-probe-'));
|
|
951
|
+
const from = join(probeDir, 'from');
|
|
952
|
+
const to = join(probeDir, 'to');
|
|
953
|
+
await nodeWriteFile(from, 'omx-recovery-capability-probe', { flag: 'wx' });
|
|
954
|
+
const outcome = await transactionDependencies.atomicRenameNoReplace(from, to);
|
|
955
|
+
return outcome === 'moved' || outcome === 'not-moved' || outcome === 'unsupported'
|
|
956
|
+
? outcome
|
|
957
|
+
: 'unsupported';
|
|
621
958
|
}
|
|
622
959
|
catch {
|
|
623
|
-
return
|
|
960
|
+
return 'unsupported';
|
|
624
961
|
}
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
962
|
+
finally {
|
|
963
|
+
if (probeDir) {
|
|
964
|
+
try {
|
|
965
|
+
await rm(probeDir, { recursive: true, force: true });
|
|
966
|
+
}
|
|
967
|
+
catch { /* best effort */ }
|
|
968
|
+
}
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
async function moveRecoveryPathNoReplace(from, to, identity, kind) {
|
|
629
972
|
try {
|
|
630
|
-
await transactionDependencies.
|
|
631
|
-
|
|
973
|
+
const outcome = await transactionDependencies.atomicRenameNoReplace(from, to);
|
|
974
|
+
if (outcome === 'unsupported')
|
|
975
|
+
return { moved: false, unsupported: true, reason: 'Atomic no-replace recovery rename is unsupported on this platform.' };
|
|
976
|
+
if (outcome === 'not-moved')
|
|
977
|
+
return { moved: false, reason: 'Atomic recovery move left its source pathname in place.' };
|
|
632
978
|
}
|
|
633
979
|
catch (error) {
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
980
|
+
return { moved: false, reason: `Atomic no-replace recovery rename failed (${errorCode(error) ?? 'unknown'}).` };
|
|
981
|
+
}
|
|
982
|
+
const destination = await lstatRecoveryPath(to);
|
|
983
|
+
const source = await lstatRecoveryPath(from);
|
|
984
|
+
if (destination && sameRecoveryIdentity(destination, identity, kind) && !source)
|
|
985
|
+
return { moved: true };
|
|
986
|
+
// A successor may have appeared at the canonical pathname after our move.
|
|
987
|
+
// Attempt a no-replace rollback only for the captured object; an occupied or
|
|
988
|
+
// foreign destination makes the rollback fail closed and leaves both residues.
|
|
989
|
+
if (destination && sameRecoveryIdentity(destination, identity, kind)) {
|
|
990
|
+
try {
|
|
991
|
+
await transactionDependencies.atomicRenameNoReplace(to, from);
|
|
992
|
+
}
|
|
993
|
+
catch { /* durable residue is safer than cleanup */ }
|
|
639
994
|
}
|
|
640
|
-
|
|
641
|
-
|
|
995
|
+
return { moved: false, reason: 'Atomic recovery move did not leave the captured object at its token-bound destination.' };
|
|
996
|
+
}
|
|
997
|
+
async function completeRecoveryCheckpoint(checkpointPath, checkpointBytes, checkpointIdentity) {
|
|
998
|
+
const completedPath = checkpointPath.replace(/\.json$/, '.completed');
|
|
999
|
+
const current = await lstatRecoveryPath(checkpointPath);
|
|
1000
|
+
if (!current || !sameRecoveryIdentity(current, checkpointIdentity, 'file') || await transactionDependencies.fs.readFile(checkpointPath, 'utf8') !== checkpointBytes) {
|
|
1001
|
+
return { completed: false, reason: 'Recovery checkpoint was replaced before completion; it was preserved as residue.' };
|
|
642
1002
|
}
|
|
643
|
-
const
|
|
644
|
-
|
|
1003
|
+
const moved = await moveRecoveryPathNoReplace(checkpointPath, completedPath, checkpointIdentity, 'file');
|
|
1004
|
+
if (!moved.moved)
|
|
1005
|
+
return { completed: false, reason: moved.reason };
|
|
1006
|
+
const completedBytes = await transactionDependencies.fs.readFile(completedPath, 'utf8');
|
|
1007
|
+
return completedBytes === checkpointBytes ? { completed: true } : { completed: false, reason: 'Completed recovery receipt bytes changed and were preserved as residue.' };
|
|
1008
|
+
}
|
|
1009
|
+
async function resumeRecoveryCheckpoint(context, checkpointPath, checkpointName) {
|
|
1010
|
+
const match = new RegExp(`^${basename(context.lockPath).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\.recovery\\.([A-Za-z0-9_-]{16,128})\\.([A-Za-z0-9_-]{16,128})\\.json$`).exec(checkpointName);
|
|
645
1011
|
try {
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
1012
|
+
const checkpointBytes = await transactionDependencies.fs.readFile(checkpointPath, 'utf8');
|
|
1013
|
+
const checkpointStat = await transactionDependencies.fs.lstat(checkpointPath);
|
|
1014
|
+
const checkpoint = JSON.parse(checkpointBytes);
|
|
1015
|
+
const ownerToken = match?.[1];
|
|
1016
|
+
const recoveryToken = match?.[2];
|
|
1017
|
+
const expectedSource = ownerToken && (checkpoint.sourcePath === join(context.lockPath, 'owner.json') || checkpoint.sourcePath === join(context.lockPath, `owner.${ownerToken}.tmp`));
|
|
1018
|
+
const expectedPark = `${context.lockPath}.parked-lock-${recoveryToken}`;
|
|
1019
|
+
const isLegacyV1 = checkpoint.version === 1 && checkpoint.lockIdentity === undefined && checkpoint.lockParkPath === undefined;
|
|
1020
|
+
if (!match || !expectedSource || !checkpoint.identity || !['evidence-pending', 'evidence-quarantined', 'directory-pending'].includes(checkpoint.phase) || ![1, 2, 3].includes(checkpoint.version) || !isLegacyV1 && checkpoint.lockParkPath !== expectedPark || !sameRecoveryIdentity(checkpointStat, { dev: checkpointStat.dev, ino: checkpointStat.ino }, 'file'))
|
|
1021
|
+
throw new Error('invalid checkpoint');
|
|
1022
|
+
const lockParkPath = checkpoint.lockParkPath ?? expectedPark;
|
|
1023
|
+
const evidenceIdentity = checkpoint.evidenceIdentity ?? checkpoint.identity;
|
|
1024
|
+
const claimPath = join(context.lockPath, `owner.${ownerToken}.${recoveryToken}.recovery`);
|
|
1025
|
+
const lock = await lstatRecoveryPath(context.lockPath);
|
|
1026
|
+
const parkedLock = await lstatRecoveryPath(lockParkPath);
|
|
1027
|
+
if (lock && parkedLock)
|
|
1028
|
+
throw new Error('checkpoint lock paths are both present');
|
|
1029
|
+
if (parkedLock) {
|
|
1030
|
+
if (!checkpoint.lockIdentity || !sameRecoveryIdentity(parkedLock, checkpoint.lockIdentity, 'directory'))
|
|
1031
|
+
throw new Error('checkpoint parked lock identity mismatch');
|
|
1032
|
+
const quarantinePath = `${context.lockPath}.quarantine.${ownerToken}.${recoveryToken}`;
|
|
1033
|
+
const completed = await completeRecoveryCheckpoint(checkpointPath, checkpointBytes, { dev: checkpointStat.dev, ino: checkpointStat.ino });
|
|
1034
|
+
if (!completed.completed)
|
|
1035
|
+
throw new Error(completed.reason);
|
|
1036
|
+
return { status: 'dead', lockPath: context.lockPath, evidenceSource: 'owner.json', safeToRecover: true, action: 'quarantined', recovered: true, reason: 'Dead session pointer lock recovery checkpoint resumed.', quarantinePath };
|
|
1037
|
+
}
|
|
1038
|
+
if (!lock || lock.isSymbolicLink() || !lock.isDirectory())
|
|
1039
|
+
throw new Error('checkpoint lock is missing or foreign');
|
|
1040
|
+
const source = await lstatRecoveryPath(checkpoint.sourcePath);
|
|
1041
|
+
const claim = await lstatRecoveryPath(claimPath);
|
|
1042
|
+
const evidencePath = source && sameRecoveryIdentity(source, evidenceIdentity, 'file') ? checkpoint.sourcePath
|
|
1043
|
+
: claim && sameRecoveryIdentity(claim, evidenceIdentity, 'file') ? claimPath : undefined;
|
|
1044
|
+
if (!evidencePath)
|
|
1045
|
+
throw new Error('checkpoint evidence is missing or foreign');
|
|
1046
|
+
if (claim && !sameRecoveryIdentity(claim, evidenceIdentity, 'file'))
|
|
1047
|
+
throw new Error('checkpoint claim is foreign');
|
|
1048
|
+
// v1/v2 did not persist the directory identity. It is safe to derive only
|
|
1049
|
+
// while the original directory still contains the exact recorded evidence.
|
|
1050
|
+
const lockIdentity = checkpoint.lockIdentity ?? { dev: lock.dev, ino: lock.ino };
|
|
1051
|
+
if (!sameRecoveryIdentity(lock, lockIdentity, 'directory'))
|
|
1052
|
+
throw new Error('checkpoint lock identity mismatch');
|
|
1053
|
+
const ownerBytes = await transactionDependencies.fs.readFile(evidencePath, 'utf8');
|
|
1054
|
+
const owner = await inspectLockOwnerFile(evidencePath);
|
|
1055
|
+
if (owner.status !== 'dead' || owner.owner?.token !== ownerToken || checkpoint.evidenceBytes !== undefined && checkpoint.evidenceBytes !== ownerBytes)
|
|
1056
|
+
throw new Error('checkpoint owner evidence changed');
|
|
1057
|
+
const quarantinePath = `${context.lockPath}.quarantine.${ownerToken}.${recoveryToken}`;
|
|
1058
|
+
const quarantine = await lstatRecoveryPath(quarantinePath);
|
|
1059
|
+
if (quarantine) {
|
|
1060
|
+
if (!sameRecoveryIdentity(quarantine, evidenceIdentity, 'file'))
|
|
1061
|
+
throw new Error('checkpoint quarantine is foreign');
|
|
1062
|
+
}
|
|
1063
|
+
else {
|
|
1064
|
+
await transactionDependencies.fs.link(evidencePath, quarantinePath);
|
|
1065
|
+
const linked = await lstatRecoveryPath(quarantinePath);
|
|
1066
|
+
if (!linked || !sameRecoveryIdentity(linked, evidenceIdentity, 'file'))
|
|
1067
|
+
throw new Error('checkpoint quarantine link mismatch');
|
|
1068
|
+
}
|
|
1069
|
+
// Revalidate the bytes, dead owner and identities immediately before moving
|
|
1070
|
+
// the entire directory incarnation out of the canonical lock pathname.
|
|
1071
|
+
const currentLock = await lstatRecoveryPath(context.lockPath);
|
|
1072
|
+
const currentEvidence = await lstatRecoveryPath(evidencePath);
|
|
1073
|
+
const currentBytes = currentEvidence && sameRecoveryIdentity(currentEvidence, evidenceIdentity, 'file') ? await transactionDependencies.fs.readFile(evidencePath, 'utf8') : undefined;
|
|
1074
|
+
const currentOwner = currentBytes === undefined ? undefined : await inspectLockOwnerFile(evidencePath);
|
|
1075
|
+
if (!currentLock || !sameRecoveryIdentity(currentLock, lockIdentity, 'directory') || currentBytes !== ownerBytes || currentOwner?.status !== 'dead' || currentOwner.owner?.token !== ownerToken)
|
|
1076
|
+
throw new Error('checkpoint owner evidence changed before directory quarantine');
|
|
1077
|
+
const lockEntries = await transactionDependencies.fs.readdir(context.lockPath);
|
|
1078
|
+
const allowedEntries = new Set([basename(checkpoint.sourcePath), basename(claimPath)]);
|
|
1079
|
+
if (lockEntries.some((entry) => !allowedEntries.has(entry)))
|
|
1080
|
+
throw new Error('checkpoint lock contains foreign recovery evidence');
|
|
1081
|
+
const moved = await moveRecoveryPathNoReplace(context.lockPath, lockParkPath, lockIdentity, 'directory');
|
|
1082
|
+
if (!moved.moved)
|
|
1083
|
+
throw new Error(moved.reason);
|
|
1084
|
+
const completed = await completeRecoveryCheckpoint(checkpointPath, checkpointBytes, { dev: checkpointStat.dev, ino: checkpointStat.ino });
|
|
1085
|
+
if (!completed.completed)
|
|
1086
|
+
throw new Error(completed.reason);
|
|
1087
|
+
return { status: 'dead', lockPath: context.lockPath, evidenceSource: 'owner.json', safeToRecover: true, action: 'quarantined', recovered: true, reason: 'Dead session pointer lock recovered into durable quarantine residues.', quarantinePath };
|
|
650
1088
|
}
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
return {
|
|
1089
|
+
catch (error) {
|
|
1090
|
+
const detail = error instanceof Error ? error.message : 'unknown';
|
|
1091
|
+
return { status: 'unexpected', lockPath: context.lockPath, evidenceSource: 'none', safeToRecover: false, action: 'none', recovered: false, reason: `Recovery checkpoint is malformed or no longer identifies its recorded recovery state (${detail}).` };
|
|
654
1092
|
}
|
|
655
|
-
|
|
1093
|
+
}
|
|
1094
|
+
/** Explicitly quarantine only a dead lock by moving its entire directory incarnation. */
|
|
1095
|
+
export async function recoverSessionPointerLock(cwd) {
|
|
1096
|
+
const context = resolveSessionPointerContext(cwd);
|
|
1097
|
+
const checkpointPrefix = `${basename(context.lockPath)}.recovery.`;
|
|
1098
|
+
let checkpointNames;
|
|
656
1099
|
try {
|
|
657
|
-
await transactionDependencies.fs.
|
|
658
|
-
return { ...claimed, action: 'none', recovered: false, reason: 'Recovery quarantine path already exists.' };
|
|
1100
|
+
checkpointNames = (await transactionDependencies.fs.readdir(dirname(context.lockPath))).filter((name) => name.startsWith(checkpointPrefix) && name.endsWith('.json'));
|
|
659
1101
|
}
|
|
660
1102
|
catch (error) {
|
|
661
1103
|
if (!isNotFound(error))
|
|
662
|
-
return {
|
|
663
|
-
|
|
664
|
-
try {
|
|
665
|
-
await transactionDependencies.fs.rename(claimPath, quarantinePath);
|
|
666
|
-
}
|
|
667
|
-
catch {
|
|
668
|
-
return { ...claimed, action: 'none', recovered: false, reason: 'Unable to quarantine the exact session pointer recovery claim.' };
|
|
1104
|
+
return { status: 'io-error', lockPath: context.lockPath, evidenceSource: 'none', safeToRecover: false, action: 'none', recovered: false, reason: 'Unable to enumerate session pointer recovery checkpoints.' };
|
|
1105
|
+
checkpointNames = [];
|
|
669
1106
|
}
|
|
1107
|
+
if (checkpointNames.length > 1)
|
|
1108
|
+
return { status: 'unexpected', lockPath: context.lockPath, evidenceSource: 'none', safeToRecover: false, action: 'none', recovered: false, reason: 'Multiple recovery checkpoints exist.' };
|
|
1109
|
+
if (checkpointNames.length === 1)
|
|
1110
|
+
return await resumeRecoveryCheckpoint(context, join(dirname(context.lockPath), checkpointNames[0]), checkpointNames[0]);
|
|
1111
|
+
const inspection = await inspectSessionPointerLockAtContext(context);
|
|
1112
|
+
if (!inspection.safeToRecover)
|
|
1113
|
+
return { ...inspection, action: 'none', recovered: false, reason: inspection.status === 'absent' ? 'No session pointer lock exists.' : `Session pointer lock is not safe to recover (${inspection.status}).` };
|
|
670
1114
|
try {
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
if (!
|
|
1115
|
+
const evidenceName = inspection.evidenceSource === 'owner.json'
|
|
1116
|
+
? 'owner.json'
|
|
1117
|
+
: (await transactionDependencies.fs.readdir(context.lockPath)).find((entry) => /^owner\.([A-Za-z0-9_-]{16,128})\.tmp$/.test(entry));
|
|
1118
|
+
if (!evidenceName)
|
|
1119
|
+
return { ...inspection, action: 'none', recovered: false, reason: 'Session pointer lock evidence changed before recovery claim.' };
|
|
1120
|
+
const evidencePath = join(context.lockPath, evidenceName);
|
|
1121
|
+
const evidence = await lstatRecoveryPath(evidencePath);
|
|
1122
|
+
const lock = await lstatRecoveryPath(context.lockPath);
|
|
1123
|
+
const ownerBytes = evidence && sameRecoveryIdentity(evidence, { dev: evidence.dev, ino: evidence.ino }, 'file')
|
|
1124
|
+
? await transactionDependencies.fs.readFile(evidencePath, 'utf8')
|
|
1125
|
+
: undefined;
|
|
1126
|
+
const owner = ownerBytes === undefined ? undefined : await inspectLockOwnerFile(evidencePath);
|
|
1127
|
+
if (!lock || lock.isSymbolicLink() || !lock.isDirectory() || !evidence
|
|
1128
|
+
|| !sameRecoveryIdentity(evidence, { dev: evidence.dev, ino: evidence.ino }, 'file')
|
|
1129
|
+
|| owner?.status !== 'dead' || !owner.owner) {
|
|
1130
|
+
return { ...inspection, action: 'none', recovered: false, reason: 'Session pointer lock evidence changed before recovery claim.' };
|
|
1131
|
+
}
|
|
1132
|
+
// Fresh recovery probes before checkpoint creation; an unsupported host leaves the
|
|
1133
|
+
// canonical lock and owner evidence untouched. Existing checkpoints are resumed
|
|
1134
|
+
// independently and may remain as durable recovery residue.
|
|
1135
|
+
const capability = await probeRecoveryRenameCapability();
|
|
1136
|
+
if (capability === 'unsupported') {
|
|
675
1137
|
return {
|
|
676
|
-
...
|
|
1138
|
+
...inspection,
|
|
677
1139
|
action: 'none',
|
|
678
1140
|
recovered: false,
|
|
679
|
-
reason: '
|
|
680
|
-
quarantinePath,
|
|
1141
|
+
reason: 'Atomic no-replace recovery rename is unsupported on this platform.',
|
|
681
1142
|
};
|
|
682
1143
|
}
|
|
1144
|
+
const recoveryToken = transactionDependencies.token();
|
|
1145
|
+
if (!isValidToken(recoveryToken))
|
|
1146
|
+
return { ...inspection, action: 'none', recovered: false, reason: 'Recovery claim token is invalid.' };
|
|
1147
|
+
const checkpointPath = `${context.lockPath}.recovery.${owner.owner.token}.${recoveryToken}.json`;
|
|
1148
|
+
const checkpoint = {
|
|
1149
|
+
version: 3,
|
|
1150
|
+
sourcePath: evidencePath,
|
|
1151
|
+
identity: { dev: evidence.dev, ino: evidence.ino },
|
|
1152
|
+
evidenceIdentity: { dev: evidence.dev, ino: evidence.ino },
|
|
1153
|
+
evidenceBytes: ownerBytes,
|
|
1154
|
+
lockIdentity: { dev: lock.dev, ino: lock.ino },
|
|
1155
|
+
lockParkPath: `${context.lockPath}.parked-lock-${recoveryToken}`,
|
|
1156
|
+
phase: 'evidence-pending',
|
|
1157
|
+
};
|
|
1158
|
+
try {
|
|
1159
|
+
await transactionDependencies.fs.writeFile(checkpointPath, JSON.stringify(checkpoint), { flag: 'wx' });
|
|
1160
|
+
}
|
|
1161
|
+
catch (error) {
|
|
1162
|
+
return { ...inspection, action: 'none', recovered: false, reason: `Unable to create recovery checkpoint (${errorCode(error) ?? 'unknown'}).` };
|
|
1163
|
+
}
|
|
1164
|
+
return await resumeRecoveryCheckpoint(context, checkpointPath, basename(checkpointPath));
|
|
1165
|
+
}
|
|
1166
|
+
catch (error) {
|
|
1167
|
+
return {
|
|
1168
|
+
...inspection,
|
|
1169
|
+
status: 'io-error',
|
|
1170
|
+
safeToRecover: false,
|
|
1171
|
+
action: 'none',
|
|
1172
|
+
recovered: false,
|
|
1173
|
+
reason: `Unable to inspect session pointer lock recovery state (${errorCode(error) ?? errorMessage(error)}).`,
|
|
1174
|
+
};
|
|
683
1175
|
}
|
|
684
|
-
return { ...inspection, action: 'quarantined', recovered: true, reason: 'Dead session pointer lock was atomically claimed and quarantined.', quarantinePath };
|
|
685
1176
|
}
|
|
686
1177
|
function buildLockOwner(token) {
|
|
687
|
-
|
|
1178
|
+
const runtimePlatform = transactionDependencies.runtimePlatform;
|
|
1179
|
+
let observation;
|
|
688
1180
|
try {
|
|
689
|
-
|
|
1181
|
+
observation = transactionDependencies.observeProcess(process.pid, runtimePlatform);
|
|
690
1182
|
}
|
|
691
1183
|
catch {
|
|
692
1184
|
// Publication remains valid with optional identity metadata omitted.
|
|
693
1185
|
}
|
|
694
|
-
|
|
695
|
-
|
|
1186
|
+
const identity = observation?.kind === 'identity' && isValidProcessIdentity(observation.identity)
|
|
1187
|
+
&& observation.identity.platform === runtimePlatform
|
|
1188
|
+
? observation.identity
|
|
1189
|
+
: undefined;
|
|
1190
|
+
const legacyStartTicks = identity?.platform === 'linux' && Number.isSafeInteger(Number(identity.birth))
|
|
1191
|
+
? Number(identity.birth)
|
|
1192
|
+
: undefined;
|
|
1193
|
+
const legacyHash = identity?.cmdline_hash && SHA256_PATTERN.test(identity.cmdline_hash)
|
|
1194
|
+
? identity.cmdline_hash
|
|
1195
|
+
: undefined;
|
|
1196
|
+
const common = {
|
|
696
1197
|
token,
|
|
697
1198
|
pid: process.pid,
|
|
698
|
-
platform:
|
|
699
|
-
...(
|
|
700
|
-
|
|
701
|
-
: {}),
|
|
702
|
-
...(identity?.status === 'matching' && identity.cmdlineHash && SHA256_PATTERN.test(identity.cmdlineHash)
|
|
703
|
-
? { pid_cmdline_hash: identity.cmdlineHash }
|
|
704
|
-
: {}),
|
|
1199
|
+
platform: runtimePlatform,
|
|
1200
|
+
...(legacyStartTicks !== undefined ? { pid_start_ticks: legacyStartTicks } : {}),
|
|
1201
|
+
...(legacyHash ? { pid_cmdline_hash: legacyHash } : {}),
|
|
705
1202
|
created_at: new Date(transactionDependencies.nowMs()).toISOString(),
|
|
706
1203
|
};
|
|
1204
|
+
return identity
|
|
1205
|
+
? { version: 2, ...common, process_identity: identity }
|
|
1206
|
+
: { version: 1, ...common };
|
|
707
1207
|
}
|
|
708
1208
|
async function removeOwnedPath(path) {
|
|
709
1209
|
try {
|
|
@@ -942,7 +1442,7 @@ async function releasePointerLock(lock) {
|
|
|
942
1442
|
if (!isNotFound(error)) {
|
|
943
1443
|
return [{
|
|
944
1444
|
operation: 'lock-release',
|
|
945
|
-
phase: 'remove-release-
|
|
1445
|
+
phase: 'remove-release-owner',
|
|
946
1446
|
ownership: 'released',
|
|
947
1447
|
message: `Unable to remove released session pointer lock owner: ${errorMessage(error)}`,
|
|
948
1448
|
cause: error,
|
|
@@ -965,6 +1465,10 @@ async function releasePointerLock(lock) {
|
|
|
965
1465
|
}];
|
|
966
1466
|
}
|
|
967
1467
|
}
|
|
1468
|
+
/** @internal Test-only release of a held pointer lock; do not use outside session tests. */
|
|
1469
|
+
export async function __releasePointerLockForTests(cwd, token) {
|
|
1470
|
+
return await releasePointerLock({ context: resolveSessionPointerContext(cwd), token });
|
|
1471
|
+
}
|
|
968
1472
|
function recoveryAbort(context, primary, failures, operation) {
|
|
969
1473
|
return resolvedAbort(context, {
|
|
970
1474
|
code: 'session_pointer_lock_recovery_required',
|
|
@@ -979,9 +1483,12 @@ function recoveryAbort(context, primary, failures, operation) {
|
|
|
979
1483
|
secondaryFailures: failures,
|
|
980
1484
|
});
|
|
981
1485
|
}
|
|
982
|
-
function releaseFailureError(failures) {
|
|
983
|
-
const error = new
|
|
984
|
-
|
|
1486
|
+
function releaseFailureError(failures, context, value) {
|
|
1487
|
+
const error = new CommittedLaunchBlockedError(failures);
|
|
1488
|
+
if (context)
|
|
1489
|
+
error.context = context;
|
|
1490
|
+
if (value !== undefined)
|
|
1491
|
+
error.value = value;
|
|
985
1492
|
return error;
|
|
986
1493
|
}
|
|
987
1494
|
async function finalizePrecommitAbort(lock, primary, pointerTempPath) {
|
|
@@ -1023,7 +1530,7 @@ function ownerConflictAbort(context, candidateSessionId, state, cause) {
|
|
|
1023
1530
|
...(cause ? { cause } : {}),
|
|
1024
1531
|
});
|
|
1025
1532
|
}
|
|
1026
|
-
async function writePointerTransaction(cwd, candidateSessionId, options, timeoutMs, transition, pointerState) {
|
|
1533
|
+
async function writePointerTransaction(cwd, candidateSessionId, options, timeoutMs, transition, pointerState, beforeCommit, afterCommit) {
|
|
1027
1534
|
let context;
|
|
1028
1535
|
try {
|
|
1029
1536
|
context = options.context ?? resolveSessionPointerContext(cwd);
|
|
@@ -1059,6 +1566,7 @@ async function writePointerTransaction(cwd, candidateSessionId, options, timeout
|
|
|
1059
1566
|
const lock = await acquirePointerLock(context, candidateSessionId, timeoutMs, platform, tracker, options.regularFileSync);
|
|
1060
1567
|
const pointerTempPath = `${context.sessionPath}.tmp-${lock.token}`;
|
|
1061
1568
|
let pointerCommitted = false;
|
|
1569
|
+
let pointerTempWritten = false;
|
|
1062
1570
|
try {
|
|
1063
1571
|
let pointer;
|
|
1064
1572
|
try {
|
|
@@ -1076,7 +1584,7 @@ async function writePointerTransaction(cwd, candidateSessionId, options, timeout
|
|
|
1076
1584
|
}
|
|
1077
1585
|
let next;
|
|
1078
1586
|
try {
|
|
1079
|
-
next = transition(pointer, context);
|
|
1587
|
+
next = await transition(pointer, context);
|
|
1080
1588
|
}
|
|
1081
1589
|
catch (error) {
|
|
1082
1590
|
if (isSessionPointerLaunchAbort(error))
|
|
@@ -1091,6 +1599,7 @@ async function writePointerTransaction(cwd, candidateSessionId, options, timeout
|
|
|
1091
1599
|
}
|
|
1092
1600
|
const serialized = JSON.stringify(pointerState(next), null, 2);
|
|
1093
1601
|
try {
|
|
1602
|
+
pointerTempWritten = true;
|
|
1094
1603
|
await transactionDependencies.fs.writeFile(pointerTempPath, serialized, { mode: 0o600, flag: 'wx' });
|
|
1095
1604
|
}
|
|
1096
1605
|
catch (error) {
|
|
@@ -1116,11 +1625,31 @@ async function writePointerTransaction(cwd, candidateSessionId, options, timeout
|
|
|
1116
1625
|
cause: error,
|
|
1117
1626
|
});
|
|
1118
1627
|
}
|
|
1628
|
+
if (beforeCommit)
|
|
1629
|
+
await beforeCommit(context);
|
|
1119
1630
|
try {
|
|
1120
1631
|
await transactionDependencies.fs.rename(pointerTempPath, context.sessionPath);
|
|
1121
1632
|
pointerCommitted = true;
|
|
1633
|
+
if (afterCommit) {
|
|
1634
|
+
try {
|
|
1635
|
+
await afterCommit(context);
|
|
1636
|
+
}
|
|
1637
|
+
catch (error) {
|
|
1638
|
+
const releaseFailures = await releasePointerLock(lock);
|
|
1639
|
+
throw releaseFailureError([
|
|
1640
|
+
{
|
|
1641
|
+
operation: 'lock-release', phase: 'rename', ownership: 'uncertain',
|
|
1642
|
+
message: `Selected state root identity changed after pointer publication: ${errorMessage(error)}`,
|
|
1643
|
+
cause: error, evidencePath: context.sessionPath,
|
|
1644
|
+
},
|
|
1645
|
+
...releaseFailures,
|
|
1646
|
+
], context, next);
|
|
1647
|
+
}
|
|
1648
|
+
}
|
|
1122
1649
|
}
|
|
1123
1650
|
catch (error) {
|
|
1651
|
+
if (error instanceof CommittedLaunchBlockedError)
|
|
1652
|
+
throw error;
|
|
1124
1653
|
throw resolvedAbort(context, {
|
|
1125
1654
|
code: 'session_pointer_io_failure',
|
|
1126
1655
|
operation: 'pointer-rename',
|
|
@@ -1132,7 +1661,7 @@ async function writePointerTransaction(cwd, candidateSessionId, options, timeout
|
|
|
1132
1661
|
}
|
|
1133
1662
|
const releaseFailures = await releasePointerLock(lock);
|
|
1134
1663
|
if (releaseFailures.length > 0)
|
|
1135
|
-
throw releaseFailureError(releaseFailures);
|
|
1664
|
+
throw releaseFailureError(releaseFailures, context, next);
|
|
1136
1665
|
emitDegradedDurabilityWarning('session pointer start/reconcile', tracker);
|
|
1137
1666
|
return { context, value: next };
|
|
1138
1667
|
}
|
|
@@ -1149,14 +1678,18 @@ async function writePointerTransaction(cwd, candidateSessionId, options, timeout
|
|
|
1149
1678
|
reason: `Session pointer transition failed before commit: ${errorMessage(error)}`,
|
|
1150
1679
|
cause: error,
|
|
1151
1680
|
});
|
|
1152
|
-
const ownsPointerTemp =
|
|
1153
|
-
|| primary.operation === 'pointer-fsync'
|
|
1154
|
-
|| primary.operation === 'pointer-rename';
|
|
1681
|
+
const ownsPointerTemp = pointerTempWritten;
|
|
1155
1682
|
throw await finalizePrecommitAbort(lock, primary, ownsPointerTemp ? pointerTempPath : undefined);
|
|
1156
1683
|
}
|
|
1157
1684
|
}
|
|
1158
|
-
function startPointerTransition(requestedSessionId, options) {
|
|
1685
|
+
function startPointerTransition(requestedSessionId, options, requireLaunchLineageToken = false, requireAbsent = false) {
|
|
1159
1686
|
return (pointer, context) => {
|
|
1687
|
+
if (requireAbsent && pointer.status !== 'absent') {
|
|
1688
|
+
if (pointer.status === 'usable' && pointer.state) {
|
|
1689
|
+
throw ownerConflictAbort(context, requestedSessionId, pointer.state);
|
|
1690
|
+
}
|
|
1691
|
+
throw unusablePointerAbort(context, requestedSessionId, pointer);
|
|
1692
|
+
}
|
|
1160
1693
|
if (pointer.status !== 'absent' && pointer.status !== 'stale-dead' && pointer.status !== 'usable') {
|
|
1161
1694
|
throw unusablePointerAbort(context, requestedSessionId, pointer);
|
|
1162
1695
|
}
|
|
@@ -1164,6 +1697,9 @@ function startPointerTransition(requestedSessionId, options) {
|
|
|
1164
1697
|
if (existing && !isStartCompatible(existing, requestedSessionId)) {
|
|
1165
1698
|
throw ownerConflictAbort(context, requestedSessionId, existing);
|
|
1166
1699
|
}
|
|
1700
|
+
if (requireLaunchLineageToken && existing && !isValidToken(existing.launch_lineage_token)) {
|
|
1701
|
+
throw ownerConflictAbort(context, requestedSessionId, existing);
|
|
1702
|
+
}
|
|
1167
1703
|
const canonicalSessionId = existing?.session_id ?? requestedSessionId;
|
|
1168
1704
|
const pid = resolvePid(options);
|
|
1169
1705
|
const platform = options.platform ?? process.platform;
|
|
@@ -1180,20 +1716,51 @@ function startPointerTransition(requestedSessionId, options) {
|
|
|
1180
1716
|
pid,
|
|
1181
1717
|
}, error);
|
|
1182
1718
|
}
|
|
1183
|
-
|
|
1719
|
+
const state = createSessionState(context.cwd, context.baseStateDir, canonicalSessionId, pid, platform, sessionIdentityFor(pid, platform), {
|
|
1184
1720
|
nativeSessionId: options.nativeSessionId ?? existing?.native_session_id,
|
|
1185
1721
|
previousNativeSessionId: options.previousNativeSessionId ?? existing?.previous_native_session_id,
|
|
1186
1722
|
nativeSessionSwitchedAt: options.nativeSessionSwitchedAt ?? existing?.native_session_switched_at,
|
|
1187
1723
|
...(ownerOmxSessionId ? { ownerOmxSessionId } : {}),
|
|
1724
|
+
launchLineageToken: existing
|
|
1725
|
+
? (isValidToken(existing.launch_lineage_token) ? existing.launch_lineage_token : undefined)
|
|
1726
|
+
: transactionDependencies.token(),
|
|
1188
1727
|
tmuxSessionName: options.tmuxSessionName ?? existing?.tmux_session_name,
|
|
1189
1728
|
tmuxPaneId: options.tmuxPaneId ?? existing?.tmux_pane_id,
|
|
1190
1729
|
});
|
|
1730
|
+
return preserveExistingLaunchLineageToken(existing, state);
|
|
1191
1731
|
};
|
|
1192
1732
|
}
|
|
1193
|
-
/**
|
|
1733
|
+
/** Create a wrapper-owned canonical pointer only when the selected pointer is absent. */
|
|
1194
1734
|
export async function writeSessionStart(cwd, sessionId, options = {}) {
|
|
1735
|
+
const candidateSessionId = normalizeSessionId(sessionId);
|
|
1736
|
+
let context;
|
|
1737
|
+
try {
|
|
1738
|
+
context = options.context ?? resolveSessionPointerContext(cwd);
|
|
1739
|
+
}
|
|
1740
|
+
catch (error) {
|
|
1741
|
+
throw contextAbort(cwd, candidateSessionId, error);
|
|
1742
|
+
}
|
|
1743
|
+
let pointer;
|
|
1744
|
+
try {
|
|
1745
|
+
pointer = await readSessionPointer(context);
|
|
1746
|
+
}
|
|
1747
|
+
catch (error) {
|
|
1748
|
+
throw resolvedAbort(context, {
|
|
1749
|
+
code: 'session_pointer_io_failure', operation: 'pointer-read', candidateSessionId,
|
|
1750
|
+
lockPath: context.lockPath, reason: `Unable to read selected session pointer: ${errorMessage(error)}`, cause: error,
|
|
1751
|
+
});
|
|
1752
|
+
}
|
|
1753
|
+
if (pointer.status !== 'absent') {
|
|
1754
|
+
if (pointer.status === 'usable' && pointer.state) {
|
|
1755
|
+
throw ownerConflictAbort(context, candidateSessionId ?? sessionId, pointer.state);
|
|
1756
|
+
}
|
|
1757
|
+
throw unusablePointerAbort(context, candidateSessionId ?? sessionId, pointer);
|
|
1758
|
+
}
|
|
1759
|
+
return await writeSessionStartTransition(cwd, sessionId, { ...options, context });
|
|
1760
|
+
}
|
|
1761
|
+
async function writeSessionStartTransition(cwd, sessionId, options, requireLaunchLineageToken = false, requireAbsent = false, beforeCommit, afterCommit) {
|
|
1195
1762
|
const requestedSessionId = normalizeSessionId(sessionId);
|
|
1196
|
-
const result = await writePointerTransaction(cwd, requestedSessionId, options, DEFAULT_POINTER_TIMEOUT_MS, startPointerTransition(requestedSessionId ?? sessionId, options), (state) => state);
|
|
1763
|
+
const result = await writePointerTransaction(cwd, requestedSessionId, options, DEFAULT_POINTER_TIMEOUT_MS, startPointerTransition(requestedSessionId ?? sessionId, options, requireLaunchLineageToken, requireAbsent), (state) => state, beforeCommit, afterCommit);
|
|
1197
1764
|
await appendToLogAtContext(result.context, {
|
|
1198
1765
|
event: 'session_start',
|
|
1199
1766
|
session_id: result.value.session_id,
|
|
@@ -1203,6 +1770,551 @@ export async function writeSessionStart(cwd, sessionId, options = {}) {
|
|
|
1203
1770
|
}).catch(() => { });
|
|
1204
1771
|
return result.value;
|
|
1205
1772
|
}
|
|
1773
|
+
const DIRECTORY_CAPABILITY_PLATFORMS = new Set([
|
|
1774
|
+
'linux', 'darwin', 'freebsd', 'openbsd', 'netbsd', 'sunos',
|
|
1775
|
+
]);
|
|
1776
|
+
export class LaunchContextResolutionError extends Error {
|
|
1777
|
+
cleanup;
|
|
1778
|
+
name = 'LaunchContextResolutionError';
|
|
1779
|
+
constructor(cleanup, cause) {
|
|
1780
|
+
super(`Unable to establish launch directory capability: ${errorMessage(cause)}`, { cause });
|
|
1781
|
+
this.cleanup = cleanup;
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
const bindingLeases = new WeakMap();
|
|
1785
|
+
const bindingFinalizations = new WeakMap();
|
|
1786
|
+
function safeError(error) {
|
|
1787
|
+
return {
|
|
1788
|
+
name: error instanceof Error ? error.name : 'Error',
|
|
1789
|
+
message: errorMessage(error),
|
|
1790
|
+
...(errorCode(error) ? { code: errorCode(error) } : {}),
|
|
1791
|
+
};
|
|
1792
|
+
}
|
|
1793
|
+
function lockDisposition(failures) {
|
|
1794
|
+
if (failures.some((failure) => failure.ownership === 'uncertain'))
|
|
1795
|
+
return 'uncertain';
|
|
1796
|
+
return failures.some((failure) => failure.ownership === 'held') ? 'held' : 'released-with-residue';
|
|
1797
|
+
}
|
|
1798
|
+
async function acquireDirectoryCapability(cwd, platform) {
|
|
1799
|
+
const cleanup = [];
|
|
1800
|
+
let canonicalRealpath;
|
|
1801
|
+
try {
|
|
1802
|
+
canonicalRealpath = await realpath(cwd);
|
|
1803
|
+
}
|
|
1804
|
+
catch (error) {
|
|
1805
|
+
throw new LaunchContextResolutionError({ capability: cleanup }, error);
|
|
1806
|
+
}
|
|
1807
|
+
if (!DIRECTORY_CAPABILITY_PLATFORMS.has(platform)) {
|
|
1808
|
+
return { canonicalRealpath, identity: { kind: 'unsupported', reason: 'platform' }, lease: { closed: false }, cleanup };
|
|
1809
|
+
}
|
|
1810
|
+
let handle;
|
|
1811
|
+
try {
|
|
1812
|
+
handle = await open(canonicalRealpath, 'r');
|
|
1813
|
+
}
|
|
1814
|
+
catch (error) {
|
|
1815
|
+
throw new LaunchContextResolutionError({ capability: cleanup }, error);
|
|
1816
|
+
}
|
|
1817
|
+
let statFailure;
|
|
1818
|
+
try {
|
|
1819
|
+
const stats = await handle.stat({ bigint: true });
|
|
1820
|
+
if (!stats.isDirectory() || typeof stats.dev !== 'bigint' || typeof stats.ino !== 'bigint' || stats.dev <= 0n || stats.ino <= 0n) {
|
|
1821
|
+
try {
|
|
1822
|
+
await handle.close();
|
|
1823
|
+
cleanup.push({ role: 'acquisition', phase: 'before-authorization', status: 'closed' });
|
|
1824
|
+
}
|
|
1825
|
+
catch (closeError) {
|
|
1826
|
+
cleanup.push({ role: 'acquisition', phase: 'before-authorization', status: 'failed', error: safeError(closeError) });
|
|
1827
|
+
throw new LaunchContextResolutionError({ capability: cleanup }, closeError);
|
|
1828
|
+
}
|
|
1829
|
+
return { canonicalRealpath, identity: { kind: 'unsupported', reason: 'inadequate-identity' }, lease: { closed: false }, cleanup };
|
|
1830
|
+
}
|
|
1831
|
+
return { canonicalRealpath, identity: { kind: 'supported', dev: stats.dev, ino: stats.ino }, lease: { handle, closed: false }, cleanup };
|
|
1832
|
+
}
|
|
1833
|
+
catch (error) {
|
|
1834
|
+
if (error instanceof LaunchContextResolutionError)
|
|
1835
|
+
throw error;
|
|
1836
|
+
statFailure = error;
|
|
1837
|
+
}
|
|
1838
|
+
try {
|
|
1839
|
+
await handle.close();
|
|
1840
|
+
cleanup.push({ role: 'acquisition', phase: 'before-authorization', status: 'closed' });
|
|
1841
|
+
}
|
|
1842
|
+
catch (closeError) {
|
|
1843
|
+
cleanup.push({ role: 'acquisition', phase: 'before-authorization', status: 'failed', error: safeError(closeError) });
|
|
1844
|
+
throw new LaunchContextResolutionError({ capability: cleanup }, closeError);
|
|
1845
|
+
}
|
|
1846
|
+
const code = errorCode(statFailure);
|
|
1847
|
+
if (code === 'ENOSYS' || code === 'ENOTSUP' || code === 'EOPNOTSUPP' || code === 'EINVAL') {
|
|
1848
|
+
return { canonicalRealpath, identity: { kind: 'unsupported', reason: 'stat-feature' }, lease: { closed: false }, cleanup };
|
|
1849
|
+
}
|
|
1850
|
+
throw new LaunchContextResolutionError({ capability: cleanup }, statFailure);
|
|
1851
|
+
}
|
|
1852
|
+
export async function closeLaunchSessionBindingOnce(binding, phase = 'post-finalization') {
|
|
1853
|
+
const lease = bindingLeases.get(binding);
|
|
1854
|
+
if (!lease || lease.closed)
|
|
1855
|
+
return lease?.closeEvidence ?? { role: 'original-retained', phase, status: 'not-needed' };
|
|
1856
|
+
lease.closed = true;
|
|
1857
|
+
try {
|
|
1858
|
+
await lease.handle?.close();
|
|
1859
|
+
return lease.closeEvidence = { role: 'original-retained', phase, status: 'closed' };
|
|
1860
|
+
}
|
|
1861
|
+
catch (error) {
|
|
1862
|
+
return lease.closeEvidence = { role: 'original-retained', phase, status: 'failed', error: safeError(error) };
|
|
1863
|
+
}
|
|
1864
|
+
}
|
|
1865
|
+
export async function establishLaunchSessionBinding(cwd, requestedSessionId, options = {}) {
|
|
1866
|
+
const platform = options.platform ?? process.platform;
|
|
1867
|
+
let context;
|
|
1868
|
+
try {
|
|
1869
|
+
context = options.context ?? resolveSessionPointerContext(cwd);
|
|
1870
|
+
await transactionDependencies.fs.mkdir(context.baseStateDir, { recursive: true });
|
|
1871
|
+
}
|
|
1872
|
+
catch (error) {
|
|
1873
|
+
throw new LaunchContextResolutionError({ capability: [] }, error);
|
|
1874
|
+
}
|
|
1875
|
+
const acquired = await acquireDirectoryCapability(context.baseStateDir, platform);
|
|
1876
|
+
const cleanup = () => ({ capability: acquired.cleanup });
|
|
1877
|
+
const revalidateSelectedRoot = async (lockedContext) => {
|
|
1878
|
+
if (acquired.identity.kind !== 'supported')
|
|
1879
|
+
return;
|
|
1880
|
+
const retained = await acquired.lease.handle?.stat({ bigint: true });
|
|
1881
|
+
if (!retained || !retained.isDirectory() || retained.dev !== acquired.identity.dev || retained.ino !== acquired.identity.ino) {
|
|
1882
|
+
throw resolvedAbort(lockedContext, {
|
|
1883
|
+
code: 'session_pointer_io_failure', operation: 'pointer-classify', candidateSessionId: normalizeSessionId(requestedSessionId),
|
|
1884
|
+
lockPath: lockedContext.lockPath, reason: 'Selected state root identity changed during pointer publication.',
|
|
1885
|
+
});
|
|
1886
|
+
}
|
|
1887
|
+
const canonical = await realpath(lockedContext.baseStateDir);
|
|
1888
|
+
const fresh = await open(canonical, 'r');
|
|
1889
|
+
try {
|
|
1890
|
+
const stats = await fresh.stat({ bigint: true });
|
|
1891
|
+
if (canonical !== acquired.canonicalRealpath || !stats.isDirectory() || stats.dev !== acquired.identity.dev || stats.ino !== acquired.identity.ino) {
|
|
1892
|
+
throw resolvedAbort(lockedContext, {
|
|
1893
|
+
code: 'session_pointer_io_failure', operation: 'pointer-classify', candidateSessionId: normalizeSessionId(requestedSessionId),
|
|
1894
|
+
lockPath: lockedContext.lockPath, reason: 'Selected state root was replaced during pointer publication.',
|
|
1895
|
+
});
|
|
1896
|
+
}
|
|
1897
|
+
}
|
|
1898
|
+
finally {
|
|
1899
|
+
await fresh.close();
|
|
1900
|
+
}
|
|
1901
|
+
};
|
|
1902
|
+
try {
|
|
1903
|
+
const state = await writeSessionStartTransition(context.cwd, requestedSessionId, { ...options, context }, true, true, revalidateSelectedRoot, revalidateSelectedRoot);
|
|
1904
|
+
const token = state.launch_lineage_token;
|
|
1905
|
+
if (!isValidToken(token)) {
|
|
1906
|
+
const close = await closeLease(acquired.lease, 'before-authorization');
|
|
1907
|
+
if (close)
|
|
1908
|
+
acquired.cleanup.push(close);
|
|
1909
|
+
throw new LaunchContextResolutionError(cleanup(), new Error('Committed session pointer lacks a valid lineage token.'));
|
|
1910
|
+
}
|
|
1911
|
+
const binding = Object.freeze({
|
|
1912
|
+
context: Object.freeze({ ...context }), canonicalRealpath: acquired.canonicalRealpath,
|
|
1913
|
+
directoryIdentity: acquired.identity, canonicalSessionId: state.session_id,
|
|
1914
|
+
...(state.owner_omx_session_id ? { ownerOmxSessionId: state.owner_omx_session_id } : {}),
|
|
1915
|
+
...(state.native_session_id ? { nativeSessionId: state.native_session_id } : {}),
|
|
1916
|
+
startedAt: state.started_at, launchLineageToken: token,
|
|
1917
|
+
});
|
|
1918
|
+
bindingLeases.set(binding, acquired.lease);
|
|
1919
|
+
return { kind: 'committed-released', binding, cleanup: cleanup() };
|
|
1920
|
+
}
|
|
1921
|
+
catch (error) {
|
|
1922
|
+
if (error instanceof CommittedLaunchBlockedError) {
|
|
1923
|
+
const state = error.value;
|
|
1924
|
+
const errorContext = error.context ?? context;
|
|
1925
|
+
const close = await closeLease(acquired.lease, 'before-authorization');
|
|
1926
|
+
if (close)
|
|
1927
|
+
acquired.cleanup.push(close);
|
|
1928
|
+
return {
|
|
1929
|
+
kind: 'committed-release-failed', evidence: { context: errorContext, canonicalSessionId: state?.session_id ?? requestedSessionId },
|
|
1930
|
+
error, lockDisposition: lockDisposition(error.secondaryFailures), secondaryFailures: error.secondaryFailures, cleanup: cleanup(),
|
|
1931
|
+
};
|
|
1932
|
+
}
|
|
1933
|
+
const close = await closeLease(acquired.lease, 'before-authorization');
|
|
1934
|
+
if (close)
|
|
1935
|
+
acquired.cleanup.push(close);
|
|
1936
|
+
if (isSessionPointerLaunchAbort(error))
|
|
1937
|
+
return { kind: 'precommit-aborted', abort: error, cleanup: cleanup() };
|
|
1938
|
+
throw error;
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
async function closeLease(lease, phase) {
|
|
1942
|
+
if (lease.closed)
|
|
1943
|
+
return undefined;
|
|
1944
|
+
lease.closed = true;
|
|
1945
|
+
try {
|
|
1946
|
+
await lease.handle?.close();
|
|
1947
|
+
return { role: 'acquisition', phase, status: 'closed' };
|
|
1948
|
+
}
|
|
1949
|
+
catch (error) {
|
|
1950
|
+
return { role: 'acquisition', phase, status: 'failed', error: safeError(error) };
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
export async function updateDetachedSessionMetadata(binding, patch) {
|
|
1954
|
+
const capability = [];
|
|
1955
|
+
const cleanup = { capability };
|
|
1956
|
+
try {
|
|
1957
|
+
let pointerBeforeTransaction;
|
|
1958
|
+
try {
|
|
1959
|
+
pointerBeforeTransaction = await readSessionPointer(binding.context);
|
|
1960
|
+
}
|
|
1961
|
+
catch (error) {
|
|
1962
|
+
throw resolvedAbort(binding.context, {
|
|
1963
|
+
code: 'session_pointer_io_failure', operation: 'pointer-read', candidateSessionId: binding.canonicalSessionId,
|
|
1964
|
+
lockPath: binding.context.lockPath, reason: `Unable to read selected session pointer: ${errorMessage(error)}`, cause: error,
|
|
1965
|
+
});
|
|
1966
|
+
}
|
|
1967
|
+
if (pointerBeforeTransaction.status !== 'usable'
|
|
1968
|
+
|| !isAuthorizedBoundPointer(binding, binding.context, binding.canonicalSessionId, pointerBeforeTransaction.state)) {
|
|
1969
|
+
throw unusablePointerAbort(binding.context, binding.canonicalSessionId, pointerBeforeTransaction);
|
|
1970
|
+
}
|
|
1971
|
+
const authorization = await authorizeBoundDirectoryBeforeTransaction(binding, pointerBeforeTransaction, binding.context.cwd);
|
|
1972
|
+
capability.push(...authorization.capability);
|
|
1973
|
+
const result = await writePointerTransaction(binding.context.cwd, binding.canonicalSessionId, { context: binding.context }, DEFAULT_POINTER_TIMEOUT_MS, async (pointer, context) => {
|
|
1974
|
+
const state = pointer.status === 'usable' ? pointer.state : undefined;
|
|
1975
|
+
if (!state || !isAuthorizedBoundPointer(binding, context, binding.canonicalSessionId, state)) {
|
|
1976
|
+
throw unusablePointerAbort(context, binding.canonicalSessionId, pointer);
|
|
1977
|
+
}
|
|
1978
|
+
await consumeBoundDirectoryAuthorizationUnderLock(binding, authorization, pointer);
|
|
1979
|
+
return {
|
|
1980
|
+
...state,
|
|
1981
|
+
...(patch.tmuxSessionName !== undefined ? { tmux_session_name: patch.tmuxSessionName } : {}),
|
|
1982
|
+
...(patch.tmuxPaneId !== undefined ? { tmux_pane_id: patch.tmuxPaneId } : {}),
|
|
1983
|
+
};
|
|
1984
|
+
}, (state) => state);
|
|
1985
|
+
return { kind: 'committed-released', evidence: { context: result.context, canonicalSessionId: binding.canonicalSessionId }, cleanup };
|
|
1986
|
+
}
|
|
1987
|
+
catch (error) {
|
|
1988
|
+
if (error instanceof CommittedLaunchBlockedError) {
|
|
1989
|
+
const context = error.context ?? binding.context;
|
|
1990
|
+
return { kind: 'committed-release-failed', evidence: { context, canonicalSessionId: binding.canonicalSessionId }, error,
|
|
1991
|
+
lockDisposition: lockDisposition(error.secondaryFailures), secondaryFailures: error.secondaryFailures, cleanup };
|
|
1992
|
+
}
|
|
1993
|
+
if (isSessionPointerLaunchAbort(error))
|
|
1994
|
+
return { kind: 'precommit-aborted', abort: error, cleanup };
|
|
1995
|
+
throw error;
|
|
1996
|
+
}
|
|
1997
|
+
}
|
|
1998
|
+
function isAuthorizedBoundPointer(binding, context, candidateSessionId, state) {
|
|
1999
|
+
traceSessionFinalizationOperation('auth-function-enter');
|
|
2000
|
+
traceSessionFinalizationOperation('lease-lookup-start');
|
|
2001
|
+
const lease = bindingLeases.get(binding);
|
|
2002
|
+
traceSessionFinalizationOperation('lease-lookup-done');
|
|
2003
|
+
traceSessionFinalizationOperation(`state-present:${state ? 'true' : 'false'}`);
|
|
2004
|
+
if (!lease || (binding.directoryIdentity.kind === 'supported' && lease.closed) || !state) {
|
|
2005
|
+
traceSessionFinalizationOperation('bound-auth-fail:lease-or-state');
|
|
2006
|
+
return false;
|
|
2007
|
+
}
|
|
2008
|
+
if (!isValidToken(binding.launchLineageToken) || !isValidToken(state.launch_lineage_token)) {
|
|
2009
|
+
traceSessionFinalizationOperation('bound-auth-fail:token-shape');
|
|
2010
|
+
return false;
|
|
2011
|
+
}
|
|
2012
|
+
if (candidateSessionId !== binding.canonicalSessionId) {
|
|
2013
|
+
traceSessionFinalizationOperation('bound-auth-fail:candidate-session');
|
|
2014
|
+
return false;
|
|
2015
|
+
}
|
|
2016
|
+
if (state.launch_lineage_token !== binding.launchLineageToken) {
|
|
2017
|
+
traceSessionFinalizationOperation('bound-auth-fail:lineage-token');
|
|
2018
|
+
return false;
|
|
2019
|
+
}
|
|
2020
|
+
if (state.started_at !== binding.startedAt) {
|
|
2021
|
+
traceSessionFinalizationOperation('bound-auth-fail:started-at');
|
|
2022
|
+
return false;
|
|
2023
|
+
}
|
|
2024
|
+
const directCanonicalIdentity = state.session_id === binding.canonicalSessionId
|
|
2025
|
+
&& ((state.owner_omx_session_id ?? undefined) === binding.ownerOmxSessionId
|
|
2026
|
+
|| (binding.ownerOmxSessionId === undefined && state.owner_omx_session_id === binding.canonicalSessionId))
|
|
2027
|
+
&& (binding.nativeSessionId === undefined || state.native_session_id === binding.nativeSessionId);
|
|
2028
|
+
const replacedNativeIdentity = state.owner_omx_session_id === binding.canonicalSessionId
|
|
2029
|
+
&& normalizeSessionId(state.session_id) !== undefined
|
|
2030
|
+
&& state.native_session_id === state.session_id;
|
|
2031
|
+
if (!directCanonicalIdentity && !replacedNativeIdentity) {
|
|
2032
|
+
traceSessionFinalizationOperation('bound-auth-fail:session-identity');
|
|
2033
|
+
return false;
|
|
2034
|
+
}
|
|
2035
|
+
if (context.rootSource !== binding.context.rootSource)
|
|
2036
|
+
traceSessionFinalizationOperation('bound-auth-fail:root-source');
|
|
2037
|
+
else if (context.baseStateDir !== binding.context.baseStateDir)
|
|
2038
|
+
traceSessionFinalizationOperation('bound-auth-fail:base-state-dir');
|
|
2039
|
+
else if (context.sessionPath !== binding.context.sessionPath)
|
|
2040
|
+
traceSessionFinalizationOperation('bound-auth-fail:session-path');
|
|
2041
|
+
else if (context.cwd !== binding.context.cwd)
|
|
2042
|
+
traceSessionFinalizationOperation('bound-auth-fail:context-cwd');
|
|
2043
|
+
else if (state.cwd !== binding.context.cwd)
|
|
2044
|
+
traceSessionFinalizationOperation('bound-auth-fail:state-cwd');
|
|
2045
|
+
else {
|
|
2046
|
+
traceSessionFinalizationOperation('bound-auth-success');
|
|
2047
|
+
return true;
|
|
2048
|
+
}
|
|
2049
|
+
return false;
|
|
2050
|
+
}
|
|
2051
|
+
function isAuthorizedBoundStaleDeadPointer(binding, context, candidateSessionId, state) {
|
|
2052
|
+
return isAuthorizedBoundPointer(binding, context, candidateSessionId, state)
|
|
2053
|
+
&& Boolean(normalizeSessionId(state?.native_session_id));
|
|
2054
|
+
}
|
|
2055
|
+
class BoundDirectoryComparisonDenied extends Error {
|
|
2056
|
+
comparison;
|
|
2057
|
+
capability;
|
|
2058
|
+
constructor(comparison, capability) {
|
|
2059
|
+
super(comparison.reason);
|
|
2060
|
+
this.comparison = comparison;
|
|
2061
|
+
this.capability = capability;
|
|
2062
|
+
}
|
|
2063
|
+
}
|
|
2064
|
+
function isAuthorizedBoundPointerForFinalization(binding, context, candidateSessionId, pointer, phase) {
|
|
2065
|
+
traceSessionFinalizationOperation(`${phase}-status:${pointer.status}`);
|
|
2066
|
+
if (pointer.status === 'usable') {
|
|
2067
|
+
traceSessionFinalizationOperation(`${phase}-usable-branch-enter`);
|
|
2068
|
+
return isAuthorizedBoundPointer(binding, context, candidateSessionId, pointer.state);
|
|
2069
|
+
}
|
|
2070
|
+
if (pointer.status === 'stale-dead') {
|
|
2071
|
+
traceSessionFinalizationOperation(`${phase}-stale-dead-branch-enter`);
|
|
2072
|
+
return isAuthorizedBoundStaleDeadPointer(binding, context, candidateSessionId, pointer.state);
|
|
2073
|
+
}
|
|
2074
|
+
if (pointer.status === 'identity-indeterminate') {
|
|
2075
|
+
traceSessionFinalizationOperation(`${phase}-identity-indeterminate-branch-enter`);
|
|
2076
|
+
const sameProcess = pointer.state?.pid === process.pid;
|
|
2077
|
+
traceSessionFinalizationOperation(`${phase}-identity-indeterminate-current-pid:${sameProcess ? 'true' : 'false'}`);
|
|
2078
|
+
return sameProcess && isAuthorizedBoundPointer(binding, context, candidateSessionId, pointer.state);
|
|
2079
|
+
}
|
|
2080
|
+
traceSessionFinalizationOperation(`${phase}-unsupported-status`);
|
|
2081
|
+
return false;
|
|
2082
|
+
}
|
|
2083
|
+
async function authorizeBoundDirectoryBeforeTransaction(binding, pointer, postLaunchCwd) {
|
|
2084
|
+
const capability = [];
|
|
2085
|
+
const lease = bindingLeases.get(binding);
|
|
2086
|
+
if (!lease || lease.closed) {
|
|
2087
|
+
throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: 'binding-lease-closed' }, capability);
|
|
2088
|
+
}
|
|
2089
|
+
try {
|
|
2090
|
+
if (!sameFilePath(binding.context.cwd, postLaunchCwd)
|
|
2091
|
+
|| !pointer.state?.cwd
|
|
2092
|
+
|| !sameFilePath(binding.context.cwd, pointer.state.cwd)) {
|
|
2093
|
+
throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: 'cwd-identity-mismatch' }, capability);
|
|
2094
|
+
}
|
|
2095
|
+
if (binding.directoryIdentity.kind !== 'supported') {
|
|
2096
|
+
return {
|
|
2097
|
+
comparison: { status: 'matched', reason: `unsupported-directory-capability:${binding.directoryIdentity.reason}` },
|
|
2098
|
+
capability,
|
|
2099
|
+
pointerStatus: pointer.status,
|
|
2100
|
+
pointerRaw: pointer.raw,
|
|
2101
|
+
};
|
|
2102
|
+
}
|
|
2103
|
+
const retained = fstatSync(lease.handle.fd, { bigint: true });
|
|
2104
|
+
if (!retained || !retained.isDirectory() || retained.dev !== binding.directoryIdentity.dev || retained.ino !== binding.directoryIdentity.ino) {
|
|
2105
|
+
throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: 'retained-directory-identity-mismatch' }, capability);
|
|
2106
|
+
}
|
|
2107
|
+
const canonical = await realpath(binding.context.baseStateDir);
|
|
2108
|
+
const fd = openSync(canonical, 'r');
|
|
2109
|
+
let matched = false;
|
|
2110
|
+
try {
|
|
2111
|
+
const stats = fstatSync(fd, { bigint: true });
|
|
2112
|
+
matched = canonical === binding.canonicalRealpath
|
|
2113
|
+
&& stats.isDirectory()
|
|
2114
|
+
&& stats.dev === binding.directoryIdentity.dev
|
|
2115
|
+
&& stats.ino === binding.directoryIdentity.ino;
|
|
2116
|
+
}
|
|
2117
|
+
finally {
|
|
2118
|
+
try {
|
|
2119
|
+
closeSync(fd);
|
|
2120
|
+
capability.push({ role: 'fresh-comparison', phase: 'before-authorization', status: 'closed' });
|
|
2121
|
+
}
|
|
2122
|
+
catch (error) {
|
|
2123
|
+
capability.push({ role: 'fresh-comparison', phase: 'before-authorization', status: 'failed', error: safeError(error) });
|
|
2124
|
+
throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: 'fresh-comparison-close-failed' }, capability);
|
|
2125
|
+
}
|
|
2126
|
+
}
|
|
2127
|
+
if (!matched)
|
|
2128
|
+
throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: 'directory-identity-mismatch' }, capability);
|
|
2129
|
+
return { comparison: { status: 'matched' }, capability, pointerStatus: pointer.status, pointerRaw: pointer.raw };
|
|
2130
|
+
}
|
|
2131
|
+
catch (error) {
|
|
2132
|
+
if (error instanceof BoundDirectoryComparisonDenied)
|
|
2133
|
+
throw error;
|
|
2134
|
+
throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: errorMessage(error) }, capability);
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
async function consumeBoundDirectoryAuthorizationUnderLock(binding, authorization, pointer) {
|
|
2138
|
+
if (authorization.comparison?.status !== 'matched' || binding.directoryIdentity.kind !== 'supported')
|
|
2139
|
+
return authorization;
|
|
2140
|
+
const lease = bindingLeases.get(binding);
|
|
2141
|
+
try {
|
|
2142
|
+
const retained = lease?.handle ? fstatSync(lease.handle.fd, { bigint: true }) : undefined;
|
|
2143
|
+
if (!retained || !retained.isDirectory() || retained.dev !== binding.directoryIdentity.dev || retained.ino !== binding.directoryIdentity.ino) {
|
|
2144
|
+
throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: 'retained-directory-identity-mismatch' }, authorization.capability);
|
|
2145
|
+
}
|
|
2146
|
+
if (!pointer.state?.cwd || !sameFilePath(binding.context.cwd, pointer.state.cwd)) {
|
|
2147
|
+
throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: 'cwd-identity-mismatch-under-lock' }, authorization.capability);
|
|
2148
|
+
}
|
|
2149
|
+
const canonical = await realpath(binding.context.baseStateDir);
|
|
2150
|
+
const fd = openSync(canonical, 'r');
|
|
2151
|
+
let matched = false;
|
|
2152
|
+
try {
|
|
2153
|
+
const stats = fstatSync(fd, { bigint: true });
|
|
2154
|
+
matched = canonical === binding.canonicalRealpath
|
|
2155
|
+
&& stats.isDirectory()
|
|
2156
|
+
&& stats.dev === binding.directoryIdentity.dev
|
|
2157
|
+
&& stats.ino === binding.directoryIdentity.ino;
|
|
2158
|
+
}
|
|
2159
|
+
finally {
|
|
2160
|
+
try {
|
|
2161
|
+
closeSync(fd);
|
|
2162
|
+
authorization.capability.push({ role: 'fresh-comparison', phase: 'before-authorization', status: 'closed' });
|
|
2163
|
+
}
|
|
2164
|
+
catch (error) {
|
|
2165
|
+
authorization.capability.push({ role: 'fresh-comparison', phase: 'before-authorization', status: 'failed', error: safeError(error) });
|
|
2166
|
+
throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: 'fresh-comparison-close-failed-under-lock' }, authorization.capability);
|
|
2167
|
+
}
|
|
2168
|
+
}
|
|
2169
|
+
if (!matched)
|
|
2170
|
+
throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: 'directory-identity-mismatch-under-lock' }, authorization.capability);
|
|
2171
|
+
if (pointer.status !== authorization.pointerStatus || pointer.raw !== authorization.pointerRaw) {
|
|
2172
|
+
throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: 'pointer-changed-before-consume' }, authorization.capability);
|
|
2173
|
+
}
|
|
2174
|
+
return authorization;
|
|
2175
|
+
}
|
|
2176
|
+
catch (error) {
|
|
2177
|
+
if (error instanceof BoundDirectoryComparisonDenied)
|
|
2178
|
+
throw error;
|
|
2179
|
+
throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: errorMessage(error) }, authorization.capability);
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
export function finalizeBoundOnce(binding, _reason, postLaunchCwd = binding.context.cwd) {
|
|
2183
|
+
const existing = bindingFinalizations.get(binding);
|
|
2184
|
+
if (existing)
|
|
2185
|
+
return existing;
|
|
2186
|
+
const finalization = (async () => {
|
|
2187
|
+
try {
|
|
2188
|
+
const revalidation = await writeSessionEnd(binding.context.cwd, binding.canonicalSessionId, {
|
|
2189
|
+
context: binding.context, binding, postLaunchCwd,
|
|
2190
|
+
});
|
|
2191
|
+
return { finalized: true, cleanup: { capability: revalidation.capability, comparison: revalidation.comparison } };
|
|
2192
|
+
}
|
|
2193
|
+
catch (error) {
|
|
2194
|
+
if (error instanceof BoundDirectoryComparisonDenied) {
|
|
2195
|
+
return { finalized: false, cleanup: { capability: error.capability, comparison: error.comparison } };
|
|
2196
|
+
}
|
|
2197
|
+
if (isSessionPointerLaunchAbort(error)) {
|
|
2198
|
+
return { finalized: false, cleanup: { capability: [], comparison: { status: 'denied', reason: error.code } } };
|
|
2199
|
+
}
|
|
2200
|
+
throw error;
|
|
2201
|
+
}
|
|
2202
|
+
})();
|
|
2203
|
+
bindingFinalizations.set(binding, finalization);
|
|
2204
|
+
return finalization;
|
|
2205
|
+
}
|
|
2206
|
+
function nativeSessionOwnerTransition(nativeSessionId, options) {
|
|
2207
|
+
return (pointer, context) => {
|
|
2208
|
+
if (pointer.status !== 'absent' && pointer.status !== 'stale-dead' && pointer.status !== 'usable') {
|
|
2209
|
+
throw unusablePointerAbort(context, nativeSessionId, pointer);
|
|
2210
|
+
}
|
|
2211
|
+
const pid = resolvePid(options);
|
|
2212
|
+
const platform = options.platform ?? process.platform;
|
|
2213
|
+
const linuxIdentity = sessionIdentityFor(pid, platform);
|
|
2214
|
+
const existing = pointer.status === 'usable' ? pointer.state : undefined;
|
|
2215
|
+
if (existing && (normalizeSessionId(existing.session_id) !== nativeSessionId
|
|
2216
|
+
|| normalizeSessionId(existing.native_session_id) !== nativeSessionId
|
|
2217
|
+
|| existing.platform !== platform
|
|
2218
|
+
|| !isSessionStateAuthoritativeForCwd(existing, context.cwd)
|
|
2219
|
+
|| existing.pid !== pid)) {
|
|
2220
|
+
throw ownerConflictAbort(context, nativeSessionId, existing);
|
|
2221
|
+
}
|
|
2222
|
+
return createSessionState(context.cwd, context.baseStateDir, nativeSessionId, pid, platform, linuxIdentity, {
|
|
2223
|
+
nativeSessionId,
|
|
2224
|
+
startedAt: existing?.started_at,
|
|
2225
|
+
tmuxSessionName: options.tmuxSessionName ?? existing?.tmux_session_name,
|
|
2226
|
+
tmuxPaneId: options.tmuxPaneId ?? existing?.tmux_pane_id,
|
|
2227
|
+
});
|
|
2228
|
+
};
|
|
2229
|
+
}
|
|
2230
|
+
export async function writeNativeSessionOwner(cwd, nativeSessionId, options = {}) {
|
|
2231
|
+
const normalized = normalizeSessionId(nativeSessionId);
|
|
2232
|
+
const context = resolveNativeSessionOwnerContext(cwd, nativeSessionId);
|
|
2233
|
+
const result = await writePointerTransaction(cwd, normalized, { context }, NATIVE_POINTER_TIMEOUT_MS, nativeSessionOwnerTransition(normalized ?? nativeSessionId, options), (state) => state);
|
|
2234
|
+
return result.value;
|
|
2235
|
+
}
|
|
2236
|
+
/** Read and classify the exact native session-owner sidecar without consulting alternate roots. */
|
|
2237
|
+
export async function readNativeSessionOwnerEvidence(cwd, nativeSessionId) {
|
|
2238
|
+
const normalized = normalizeSessionId(nativeSessionId);
|
|
2239
|
+
if (!normalized)
|
|
2240
|
+
return { status: 'malformed' };
|
|
2241
|
+
const context = resolveNativeSessionOwnerContext(cwd, normalized);
|
|
2242
|
+
const nativeOwnerDir = dirname(context.sessionPath);
|
|
2243
|
+
const sessionsDir = dirname(nativeOwnerDir);
|
|
2244
|
+
const stateRootDir = dirname(sessionsDir);
|
|
2245
|
+
const directoryPaths = [stateRootDir, sessionsDir, nativeOwnerDir];
|
|
2246
|
+
const directoryIdentities = [];
|
|
2247
|
+
try {
|
|
2248
|
+
for (const directoryPath of directoryPaths) {
|
|
2249
|
+
const stat = await transactionDependencies.fs.lstat(directoryPath);
|
|
2250
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
2251
|
+
return { status: 'malformed' };
|
|
2252
|
+
directoryIdentities.push({ path: directoryPath, dev: stat.dev, ino: stat.ino });
|
|
2253
|
+
}
|
|
2254
|
+
const pathStat = await transactionDependencies.fs.lstat(context.sessionPath);
|
|
2255
|
+
if (pathStat.isSymbolicLink() || !pathStat.isFile())
|
|
2256
|
+
return { status: 'malformed' };
|
|
2257
|
+
}
|
|
2258
|
+
catch (error) {
|
|
2259
|
+
if (isNotFound(error))
|
|
2260
|
+
return { status: 'absent' };
|
|
2261
|
+
throw error;
|
|
2262
|
+
}
|
|
2263
|
+
const handle = await open(context.sessionPath, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
|
|
2264
|
+
let raw;
|
|
2265
|
+
try {
|
|
2266
|
+
const openedStat = await handle.stat();
|
|
2267
|
+
if (!openedStat.isFile())
|
|
2268
|
+
return { status: 'malformed' };
|
|
2269
|
+
raw = await handle.readFile({ encoding: 'utf8' });
|
|
2270
|
+
const finalPathStat = await transactionDependencies.fs.lstat(context.sessionPath);
|
|
2271
|
+
if (finalPathStat.isSymbolicLink()
|
|
2272
|
+
|| finalPathStat.dev !== openedStat.dev
|
|
2273
|
+
|| finalPathStat.ino !== openedStat.ino) {
|
|
2274
|
+
return { status: 'malformed', raw };
|
|
2275
|
+
}
|
|
2276
|
+
for (const identity of directoryIdentities) {
|
|
2277
|
+
const finalDirectoryStat = await transactionDependencies.fs.lstat(identity.path);
|
|
2278
|
+
if (finalDirectoryStat.isSymbolicLink()
|
|
2279
|
+
|| !finalDirectoryStat.isDirectory()
|
|
2280
|
+
|| finalDirectoryStat.dev !== identity.dev
|
|
2281
|
+
|| finalDirectoryStat.ino !== identity.ino) {
|
|
2282
|
+
return { status: 'malformed', raw };
|
|
2283
|
+
}
|
|
2284
|
+
}
|
|
2285
|
+
}
|
|
2286
|
+
finally {
|
|
2287
|
+
await handle.close();
|
|
2288
|
+
}
|
|
2289
|
+
let pointer;
|
|
2290
|
+
try {
|
|
2291
|
+
pointer = classifyParsedSessionPointer(context, JSON.parse(raw), raw);
|
|
2292
|
+
}
|
|
2293
|
+
catch (error) {
|
|
2294
|
+
if (error instanceof SyntaxError)
|
|
2295
|
+
return { status: 'malformed', raw };
|
|
2296
|
+
throw error;
|
|
2297
|
+
}
|
|
2298
|
+
if (pointer.status === 'absent' || pointer.status === 'foreign-cwd' || pointer.status === 'malformed' || pointer.status === 'identity-indeterminate') {
|
|
2299
|
+
return pointer;
|
|
2300
|
+
}
|
|
2301
|
+
const state = pointer.state;
|
|
2302
|
+
return state?.session_id === normalized
|
|
2303
|
+
&& state.native_session_id === normalized
|
|
2304
|
+
&& state.platform === transactionDependencies.runtimePlatform
|
|
2305
|
+
&& isSessionStateAuthoritativeForCwd(state, context.cwd)
|
|
2306
|
+
? pointer
|
|
2307
|
+
: { status: 'malformed', state, raw: pointer.raw };
|
|
2308
|
+
}
|
|
2309
|
+
export async function readNativeSessionOwner(cwd, nativeSessionId) {
|
|
2310
|
+
try {
|
|
2311
|
+
const evidence = await readNativeSessionOwnerEvidence(cwd, nativeSessionId);
|
|
2312
|
+
return evidence.status === 'usable' ? evidence.state ?? null : null;
|
|
2313
|
+
}
|
|
2314
|
+
catch {
|
|
2315
|
+
return null;
|
|
2316
|
+
}
|
|
2317
|
+
}
|
|
1206
2318
|
function reconcileNativeTransition(nativeSessionId, options) {
|
|
1207
2319
|
return (pointer, context) => {
|
|
1208
2320
|
if (pointer.status !== 'absent' && pointer.status !== 'stale-dead' && pointer.status !== 'usable') {
|
|
@@ -1212,42 +2324,19 @@ function reconcileNativeTransition(nativeSessionId, options) {
|
|
|
1212
2324
|
const platform = options.platform ?? process.platform;
|
|
1213
2325
|
const linuxIdentity = sessionIdentityFor(pid, platform);
|
|
1214
2326
|
const existing = pointer.status === 'usable' ? pointer.state : undefined;
|
|
1215
|
-
const nowIso = new Date().toISOString();
|
|
1216
2327
|
if (!existing) {
|
|
1217
2328
|
const ownerCandidate = verifiedOwnerCandidate(context, options);
|
|
1218
2329
|
const ownerOmxSessionId = ownerCandidate;
|
|
1219
|
-
return {
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
}),
|
|
1224
|
-
};
|
|
2330
|
+
return createSessionState(context.cwd, context.baseStateDir, nativeSessionId, pid, platform, linuxIdentity, {
|
|
2331
|
+
nativeSessionId,
|
|
2332
|
+
...(ownerOmxSessionId ? { ownerOmxSessionId } : {}),
|
|
2333
|
+
});
|
|
1225
2334
|
}
|
|
1226
2335
|
const existingNativeSessionId = normalizeSessionId(existing.native_session_id);
|
|
1227
2336
|
if (existingNativeSessionId && existingNativeSessionId !== nativeSessionId) {
|
|
1228
|
-
|
|
1229
|
-
return {
|
|
1230
|
-
state: createSessionState(context.cwd, nativeSessionId, pid, platform, linuxIdentity, {
|
|
1231
|
-
nativeSessionId,
|
|
1232
|
-
...(ownerOmxSessionId ? {
|
|
1233
|
-
previousNativeSessionId: existingNativeSessionId,
|
|
1234
|
-
nativeSessionSwitchedAt: nowIso,
|
|
1235
|
-
ownerOmxSessionId,
|
|
1236
|
-
} : {}),
|
|
1237
|
-
}),
|
|
1238
|
-
...(ownerOmxSessionId ? {
|
|
1239
|
-
replacementLog: {
|
|
1240
|
-
event: 'native_session_replaced',
|
|
1241
|
-
session_id: ownerOmxSessionId,
|
|
1242
|
-
...(existing.session_id !== ownerOmxSessionId ? { active_session_id: existing.session_id } : {}),
|
|
1243
|
-
previous_native_session_id: existingNativeSessionId,
|
|
1244
|
-
replaced_by_native_session_id: nativeSessionId,
|
|
1245
|
-
pid,
|
|
1246
|
-
timestamp: nowIso,
|
|
1247
|
-
},
|
|
1248
|
-
} : {}),
|
|
1249
|
-
};
|
|
2337
|
+
throw ownerConflictAbort(context, nativeSessionId, existing);
|
|
1250
2338
|
}
|
|
2339
|
+
const nowIso = new Date().toISOString();
|
|
1251
2340
|
const ownerCandidate = verifiedOwnerCandidate(context, options);
|
|
1252
2341
|
let ownerOmxSessionId;
|
|
1253
2342
|
try {
|
|
@@ -1256,39 +2345,37 @@ function reconcileNativeTransition(nativeSessionId, options) {
|
|
|
1256
2345
|
catch (error) {
|
|
1257
2346
|
throw ownerConflictAbort(context, nativeSessionId, existing, error);
|
|
1258
2347
|
}
|
|
1259
|
-
return {
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
2348
|
+
return preserveExistingLaunchLineageToken(existing, createSessionState(context.cwd, context.baseStateDir, existing.session_id, pid, platform, linuxIdentity, {
|
|
2349
|
+
nowIso,
|
|
2350
|
+
nativeSessionId,
|
|
2351
|
+
previousNativeSessionId: existing.previous_native_session_id,
|
|
2352
|
+
nativeSessionSwitchedAt: existing.native_session_switched_at,
|
|
2353
|
+
...(ownerOmxSessionId ? { ownerOmxSessionId } : {}),
|
|
2354
|
+
startedAt: existing.started_at,
|
|
2355
|
+
tmuxSessionName: existing.tmux_session_name,
|
|
2356
|
+
tmuxPaneId: existing.tmux_pane_id,
|
|
2357
|
+
launchLineageToken: isValidToken(existing.launch_lineage_token)
|
|
2358
|
+
? existing.launch_lineage_token
|
|
2359
|
+
: undefined,
|
|
2360
|
+
}));
|
|
1271
2361
|
};
|
|
1272
2362
|
}
|
|
1273
2363
|
/**
|
|
1274
|
-
* Reconcile
|
|
1275
|
-
*
|
|
1276
|
-
*
|
|
2364
|
+
* Reconcile native SessionStart only when the selected pointer is absent, stale,
|
|
2365
|
+
* or already belongs to that native session. A different live native ID is
|
|
2366
|
+
* authoritative owner evidence and must never be replaced.
|
|
1277
2367
|
*/
|
|
1278
2368
|
export async function reconcileNativeSessionStart(cwd, nativeSessionId, options = {}) {
|
|
1279
2369
|
const normalizedNativeSessionId = normalizeSessionId(nativeSessionId);
|
|
1280
|
-
const result = await writePointerTransaction(cwd, normalizedNativeSessionId, options, NATIVE_POINTER_TIMEOUT_MS, reconcileNativeTransition(normalizedNativeSessionId ?? nativeSessionId, options), (
|
|
1281
|
-
if (result.value.replacementLog) {
|
|
1282
|
-
await appendToLogAtContext(result.context, result.value.replacementLog).catch(() => { });
|
|
1283
|
-
}
|
|
2370
|
+
const result = await writePointerTransaction(cwd, normalizedNativeSessionId, options, NATIVE_POINTER_TIMEOUT_MS, reconcileNativeTransition(normalizedNativeSessionId ?? nativeSessionId, options), (state) => state);
|
|
1284
2371
|
await appendToLogAtContext(result.context, {
|
|
1285
|
-
event:
|
|
1286
|
-
session_id: result.value.
|
|
2372
|
+
event: 'session_start_reconciled',
|
|
2373
|
+
session_id: result.value.session_id,
|
|
1287
2374
|
native_session_id: normalizedNativeSessionId ?? nativeSessionId,
|
|
1288
|
-
pid: result.value.
|
|
1289
|
-
timestamp:
|
|
2375
|
+
pid: result.value.pid,
|
|
2376
|
+
timestamp: new Date().toISOString(),
|
|
1290
2377
|
}).catch(() => { });
|
|
1291
|
-
return result.value
|
|
2378
|
+
return result.value;
|
|
1292
2379
|
}
|
|
1293
2380
|
function historyDirectory(context) {
|
|
1294
2381
|
return join(dirname(context.baseStateDir), 'logs');
|
|
@@ -1334,27 +2421,45 @@ export async function writeSessionEnd(cwd, sessionId, options = {}) {
|
|
|
1334
2421
|
cause,
|
|
1335
2422
|
});
|
|
1336
2423
|
}
|
|
2424
|
+
if (!options.binding) {
|
|
2425
|
+
throw resolvedAbort(context, {
|
|
2426
|
+
code: 'session_pointer_io_failure', operation: 'pointer-classify', candidateSessionId,
|
|
2427
|
+
lockPath: context.lockPath, reason: 'A live launch binding is required to end a session pointer.',
|
|
2428
|
+
});
|
|
2429
|
+
}
|
|
2430
|
+
let preLockPointer;
|
|
2431
|
+
traceSessionFinalizationOperation('preLock-read-start');
|
|
1337
2432
|
try {
|
|
1338
|
-
await
|
|
2433
|
+
preLockPointer = await readSessionPointer(context);
|
|
2434
|
+
traceSessionFinalizationOperation(`preLock-read-done:${preLockPointer.status}`);
|
|
1339
2435
|
}
|
|
1340
2436
|
catch (error) {
|
|
1341
2437
|
throw resolvedAbort(context, {
|
|
1342
|
-
code: 'session_pointer_io_failure',
|
|
1343
|
-
|
|
1344
|
-
candidateSessionId,
|
|
1345
|
-
lockPath: context.lockPath,
|
|
1346
|
-
reason: `Unable to create selected session state directory: ${errorMessage(error)}`,
|
|
1347
|
-
cause: error,
|
|
2438
|
+
code: 'session_pointer_io_failure', operation: 'pointer-read', candidateSessionId,
|
|
2439
|
+
lockPath: context.lockPath, reason: `Unable to read selected session pointer: ${errorMessage(error)}`, cause: error,
|
|
1348
2440
|
});
|
|
1349
2441
|
}
|
|
2442
|
+
const preLockAuthorized = isAuthorizedBoundPointerForFinalization(options.binding, context, candidateSessionId, preLockPointer, 'preLock');
|
|
2443
|
+
if (!preLockAuthorized)
|
|
2444
|
+
throw unusablePointerAbort(context, candidateSessionId, preLockPointer);
|
|
2445
|
+
traceSessionFinalizationOperation('pre-transaction-auth-start');
|
|
2446
|
+
await authorizeBoundDirectoryBeforeTransaction(options.binding, preLockPointer, options.postLaunchCwd ?? options.binding.context.cwd);
|
|
2447
|
+
traceSessionFinalizationOperation('pre-transaction-auth-done');
|
|
1350
2448
|
const platform = options.platform ?? process.platform;
|
|
1351
2449
|
const tracker = { degraded: false };
|
|
2450
|
+
traceSessionFinalizationOperation('acquirePointerLock-start');
|
|
1352
2451
|
const lock = await acquirePointerLock(context, candidateSessionId, DEFAULT_POINTER_TIMEOUT_MS, platform, tracker, options.regularFileSync);
|
|
2452
|
+
traceSessionFinalizationOperation('acquirePointerLock-done');
|
|
1353
2453
|
let primary;
|
|
2454
|
+
let revalidation = {
|
|
2455
|
+
comparison: { status: 'not-run' }, capability: [],
|
|
2456
|
+
};
|
|
1354
2457
|
try {
|
|
1355
2458
|
let pointer;
|
|
2459
|
+
traceSessionFinalizationOperation('under-lock-read-start');
|
|
1356
2460
|
try {
|
|
1357
2461
|
pointer = await readSessionPointer(context);
|
|
2462
|
+
traceSessionFinalizationOperation(`under-lock-read-done:${pointer.status}`);
|
|
1358
2463
|
}
|
|
1359
2464
|
catch (error) {
|
|
1360
2465
|
throw resolvedAbort(context, {
|
|
@@ -1366,14 +2471,17 @@ export async function writeSessionEnd(cwd, sessionId, options = {}) {
|
|
|
1366
2471
|
cause: error,
|
|
1367
2472
|
});
|
|
1368
2473
|
}
|
|
1369
|
-
|
|
2474
|
+
const authorized = isAuthorizedBoundPointerForFinalization(options.binding, context, candidateSessionId, pointer, 'under-lock');
|
|
2475
|
+
if (!authorized)
|
|
1370
2476
|
throw unusablePointerAbort(context, candidateSessionId, pointer);
|
|
1371
|
-
|
|
2477
|
+
traceSessionFinalizationOperation('under-lock-auth-start');
|
|
2478
|
+
const authorization = await authorizeBoundDirectoryBeforeTransaction(options.binding, pointer, options.postLaunchCwd ?? options.binding.context.cwd);
|
|
2479
|
+
traceSessionFinalizationOperation('under-lock-auth-done');
|
|
2480
|
+
traceSessionFinalizationOperation('under-lock-consume-start');
|
|
2481
|
+
revalidation = await consumeBoundDirectoryAuthorizationUnderLock(options.binding, authorization, pointer);
|
|
2482
|
+
traceSessionFinalizationOperation('under-lock-consume-done');
|
|
1372
2483
|
const state = pointer.state;
|
|
1373
|
-
const ownsCurrentSessionFile =
|
|
1374
|
-
|| state.session_id === candidateSessionId
|
|
1375
|
-
|| state.native_session_id === candidateSessionId
|
|
1376
|
-
|| state.owner_omx_session_id === candidateSessionId;
|
|
2484
|
+
const ownsCurrentSessionFile = true;
|
|
1377
2485
|
const endTime = new Date().toISOString();
|
|
1378
2486
|
const historyEntry = {
|
|
1379
2487
|
session_id: ownsCurrentSessionFile
|
|
@@ -1389,12 +2497,19 @@ export async function writeSessionEnd(cwd, sessionId, options = {}) {
|
|
|
1389
2497
|
: {}),
|
|
1390
2498
|
...(!ownsCurrentSessionFile && state?.session_id ? { preserved_active_session_id: state.session_id } : {}),
|
|
1391
2499
|
};
|
|
2500
|
+
traceSessionFinalizationOperation('history-mkdir-start');
|
|
1392
2501
|
await nodeMkdir(historyDirectory(context), { recursive: true });
|
|
2502
|
+
traceSessionFinalizationOperation('history-mkdir-done');
|
|
2503
|
+
traceSessionFinalizationOperation('history-append-start');
|
|
1393
2504
|
await appendFile(historyPath(context), `${JSON.stringify(historyEntry)}\n`);
|
|
2505
|
+
traceSessionFinalizationOperation('history-append-done');
|
|
2506
|
+
traceSessionFinalizationOperation('hud-cleanup-start');
|
|
1394
2507
|
await removeDeadSessionHudState(context, [
|
|
1395
2508
|
...(ownsCurrentSessionFile ? [state?.session_id, state?.native_session_id] : []),
|
|
1396
2509
|
candidateSessionId,
|
|
1397
2510
|
]);
|
|
2511
|
+
traceSessionFinalizationOperation('hud-cleanup-done');
|
|
2512
|
+
traceSessionFinalizationOperation('session-unlink-start');
|
|
1398
2513
|
if (ownsCurrentSessionFile) {
|
|
1399
2514
|
try {
|
|
1400
2515
|
await transactionDependencies.fs.unlink(context.sessionPath);
|
|
@@ -1404,6 +2519,8 @@ export async function writeSessionEnd(cwd, sessionId, options = {}) {
|
|
|
1404
2519
|
throw error;
|
|
1405
2520
|
}
|
|
1406
2521
|
}
|
|
2522
|
+
traceSessionFinalizationOperation('session-unlink-done');
|
|
2523
|
+
traceSessionFinalizationOperation('session-end-log-start');
|
|
1407
2524
|
await appendToLogAtContext(context, {
|
|
1408
2525
|
event: 'session_end',
|
|
1409
2526
|
session_id: historyEntry.session_id,
|
|
@@ -1412,11 +2529,14 @@ export async function writeSessionEnd(cwd, sessionId, options = {}) {
|
|
|
1412
2529
|
...(historyEntry.preserved_active_session_id ? { preserved_active_session_id: historyEntry.preserved_active_session_id } : {}),
|
|
1413
2530
|
timestamp: endTime,
|
|
1414
2531
|
}).catch(() => { });
|
|
2532
|
+
traceSessionFinalizationOperation('session-end-log-done');
|
|
1415
2533
|
}
|
|
1416
2534
|
catch (error) {
|
|
1417
2535
|
primary = error;
|
|
1418
2536
|
}
|
|
2537
|
+
traceSessionFinalizationOperation('lock-release-start');
|
|
1419
2538
|
const releaseFailures = await releasePointerLock(lock);
|
|
2539
|
+
traceSessionFinalizationOperation('lock-release-done');
|
|
1420
2540
|
if (primary) {
|
|
1421
2541
|
if (isSessionPointerLaunchAbort(primary) && releaseFailures.length > 0) {
|
|
1422
2542
|
throw recoveryAbort(context, primary, releaseFailures, 'lock-release');
|
|
@@ -1426,6 +2546,7 @@ export async function writeSessionEnd(cwd, sessionId, options = {}) {
|
|
|
1426
2546
|
if (releaseFailures.length > 0)
|
|
1427
2547
|
throw releaseFailureError(releaseFailures);
|
|
1428
2548
|
emitDegradedDurabilityWarning('session pointer end', tracker);
|
|
2549
|
+
return revalidation;
|
|
1429
2550
|
}
|
|
1430
2551
|
/** Reset session-scoped HUD/metrics files at launch. */
|
|
1431
2552
|
export async function resetSessionMetrics(cwd, sessionId) {
|