oh-my-codex 0.20.2 → 0.20.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +36 -18
- package/Cargo.toml +1 -1
- package/README.md +28 -2
- package/crates/omx-runtime/Cargo.toml +4 -0
- package/crates/omx-runtime/src/main.rs +415 -2
- package/crates/omx-runtime/tests/execution.rs +483 -0
- package/crates/omx-runtime-core/src/dispatch.rs +96 -10
- package/crates/omx-runtime-core/src/engine.rs +839 -29
- package/crates/omx-runtime-core/src/lib.rs +8 -0
- package/dist/adapt/__tests__/herdr-target.test.d.ts +2 -0
- package/dist/adapt/__tests__/herdr-target.test.d.ts.map +1 -0
- package/dist/adapt/__tests__/herdr-target.test.js +72 -0
- package/dist/adapt/__tests__/herdr-target.test.js.map +1 -0
- package/dist/adapt/contracts.d.ts +1 -1
- package/dist/adapt/contracts.d.ts.map +1 -1
- package/dist/adapt/contracts.js +1 -1
- package/dist/adapt/contracts.js.map +1 -1
- package/dist/adapt/herdr/__tests__/authority.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/authority.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/authority.test.js +49 -0
- package/dist/adapt/herdr/__tests__/authority.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/bridge.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/bridge.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/bridge.test.js +176 -0
- package/dist/adapt/herdr/__tests__/bridge.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/sanitize.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/sanitize.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/sanitize.test.js +45 -0
- package/dist/adapt/herdr/__tests__/sanitize.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/semantic.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/semantic.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/semantic.test.js +130 -0
- package/dist/adapt/herdr/__tests__/semantic.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/seq-store.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/seq-store.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/seq-store.test.js +54 -0
- package/dist/adapt/herdr/__tests__/seq-store.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/socket-live.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/socket-live.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/socket-live.test.js +104 -0
- package/dist/adapt/herdr/__tests__/socket-live.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/team-map.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/team-map.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/team-map.test.js +46 -0
- package/dist/adapt/herdr/__tests__/team-map.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/transport.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/transport.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/transport.test.js +160 -0
- package/dist/adapt/herdr/__tests__/transport.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/wiring.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/wiring.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/wiring.test.js +118 -0
- package/dist/adapt/herdr/__tests__/wiring.test.js.map +1 -0
- package/dist/adapt/herdr/authority.d.ts +34 -0
- package/dist/adapt/herdr/authority.d.ts.map +1 -0
- package/dist/adapt/herdr/authority.js +71 -0
- package/dist/adapt/herdr/authority.js.map +1 -0
- package/dist/adapt/herdr/bridge.d.ts +84 -0
- package/dist/adapt/herdr/bridge.d.ts.map +1 -0
- package/dist/adapt/herdr/bridge.js +222 -0
- package/dist/adapt/herdr/bridge.js.map +1 -0
- package/dist/adapt/herdr/sanitize.d.ts +20 -0
- package/dist/adapt/herdr/sanitize.d.ts.map +1 -0
- package/dist/adapt/herdr/sanitize.js +70 -0
- package/dist/adapt/herdr/sanitize.js.map +1 -0
- package/dist/adapt/herdr/semantic.d.ts +67 -0
- package/dist/adapt/herdr/semantic.d.ts.map +1 -0
- package/dist/adapt/herdr/semantic.js +170 -0
- package/dist/adapt/herdr/semantic.js.map +1 -0
- package/dist/adapt/herdr/seq-store.d.ts +32 -0
- package/dist/adapt/herdr/seq-store.d.ts.map +1 -0
- package/dist/adapt/herdr/seq-store.js +126 -0
- package/dist/adapt/herdr/seq-store.js.map +1 -0
- package/dist/adapt/herdr/team-map.d.ts +30 -0
- package/dist/adapt/herdr/team-map.d.ts.map +1 -0
- package/dist/adapt/herdr/team-map.js +37 -0
- package/dist/adapt/herdr/team-map.js.map +1 -0
- package/dist/adapt/herdr/transport.d.ts +115 -0
- package/dist/adapt/herdr/transport.d.ts.map +1 -0
- package/dist/adapt/herdr/transport.js +260 -0
- package/dist/adapt/herdr/transport.js.map +1 -0
- package/dist/adapt/herdr/wiring.d.ts +24 -0
- package/dist/adapt/herdr/wiring.d.ts.map +1 -0
- package/dist/adapt/herdr/wiring.js +62 -0
- package/dist/adapt/herdr/wiring.js.map +1 -0
- package/dist/adapt/herdr.d.ts +19 -0
- package/dist/adapt/herdr.d.ts.map +1 -0
- package/dist/adapt/herdr.js +115 -0
- package/dist/adapt/herdr.js.map +1 -0
- package/dist/adapt/index.d.ts +4 -0
- package/dist/adapt/index.d.ts.map +1 -1
- package/dist/adapt/index.js +48 -0
- package/dist/adapt/index.js.map +1 -1
- package/dist/adapt/registry.d.ts.map +1 -1
- package/dist/adapt/registry.js +11 -0
- package/dist/adapt/registry.js.map +1 -1
- package/dist/agents/__tests__/definitions.test.js +81 -0
- package/dist/agents/__tests__/definitions.test.js.map +1 -1
- package/dist/agents/__tests__/native-config.test.js +25 -0
- package/dist/agents/__tests__/native-config.test.js.map +1 -1
- package/dist/agents/native-config.d.ts +2 -1
- package/dist/agents/native-config.d.ts.map +1 -1
- package/dist/agents/native-config.js.map +1 -1
- package/dist/auth/__tests__/hotswap.test.js +60 -1
- package/dist/auth/__tests__/hotswap.test.js.map +1 -1
- package/dist/auth/__tests__/storage.test.js +33 -1
- package/dist/auth/__tests__/storage.test.js.map +1 -1
- package/dist/auth/hotswap.d.ts +22 -2
- package/dist/auth/hotswap.d.ts.map +1 -1
- package/dist/auth/hotswap.js +107 -22
- package/dist/auth/hotswap.js.map +1 -1
- package/dist/auth/storage.js +1 -1
- package/dist/auth/storage.js.map +1 -1
- package/dist/autopilot/__tests__/ralplan-gate.test.js +86 -1333
- package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
- package/dist/autopilot/ralplan-gate.d.ts.map +1 -1
- package/dist/autopilot/ralplan-gate.js +7 -6
- package/dist/autopilot/ralplan-gate.js.map +1 -1
- package/dist/cli/__tests__/agents-init.test.js +4 -23
- package/dist/cli/__tests__/agents-init.test.js.map +1 -1
- package/dist/cli/__tests__/api.test.js +61 -24
- package/dist/cli/__tests__/api.test.js.map +1 -1
- package/dist/cli/__tests__/auth.test.js +63 -7
- package/dist/cli/__tests__/auth.test.js.map +1 -1
- package/dist/cli/__tests__/codex-feature-probe.test.js +66 -1
- package/dist/cli/__tests__/codex-feature-probe.test.js.map +1 -1
- package/dist/cli/__tests__/codex-plugin-layout.test.js +150 -2
- package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
- package/dist/cli/__tests__/detached-leader-teardown.test.d.ts +2 -0
- package/dist/cli/__tests__/detached-leader-teardown.test.d.ts.map +1 -0
- package/dist/cli/__tests__/detached-leader-teardown.test.js +473 -0
- package/dist/cli/__tests__/detached-leader-teardown.test.js.map +1 -0
- package/dist/cli/__tests__/doctor-process-identity-readiness.test.d.ts +2 -0
- package/dist/cli/__tests__/doctor-process-identity-readiness.test.d.ts.map +1 -0
- package/dist/cli/__tests__/doctor-process-identity-readiness.test.js +103 -0
- package/dist/cli/__tests__/doctor-process-identity-readiness.test.js.map +1 -0
- package/dist/cli/__tests__/doctor-state-root-binding.test.d.ts +2 -0
- package/dist/cli/__tests__/doctor-state-root-binding.test.d.ts.map +1 -0
- package/dist/cli/__tests__/doctor-state-root-binding.test.js +139 -0
- package/dist/cli/__tests__/doctor-state-root-binding.test.js.map +1 -0
- package/dist/cli/__tests__/doctor-warning-copy.test.js +135 -1
- package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
- package/dist/cli/__tests__/exec.test.js +211 -0
- package/dist/cli/__tests__/exec.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +878 -463
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/launch-fallback.test.js +1290 -127
- package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
- package/dist/cli/__tests__/mcp-parity.test.js +8 -4
- package/dist/cli/__tests__/mcp-parity.test.js.map +1 -1
- package/dist/cli/__tests__/native-assets.test.js +398 -16
- package/dist/cli/__tests__/native-assets.test.js.map +1 -1
- package/dist/cli/__tests__/native-hook-claim-journal.test.js +132 -6
- package/dist/cli/__tests__/native-hook-claim-journal.test.js.map +1 -1
- package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +3 -3
- package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -1
- package/dist/cli/__tests__/ralph.test.js +3 -3
- package/dist/cli/__tests__/ralph.test.js.map +1 -1
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js +49 -164
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js.map +1 -1
- package/dist/cli/__tests__/ralplan-child-binding-3212.test.d.ts +2 -0
- package/dist/cli/__tests__/ralplan-child-binding-3212.test.d.ts.map +1 -0
- package/dist/cli/__tests__/ralplan-child-binding-3212.test.js +56 -0
- package/dist/cli/__tests__/ralplan-child-binding-3212.test.js.map +1 -0
- package/dist/cli/__tests__/ralplan.test.js +107 -109
- package/dist/cli/__tests__/ralplan.test.js.map +1 -1
- package/dist/cli/__tests__/resume.test.js +11 -10
- package/dist/cli/__tests__/resume.test.js.map +1 -1
- package/dist/cli/__tests__/session-scoped-runtime.test.js +1118 -43
- package/dist/cli/__tests__/session-scoped-runtime.test.js.map +1 -1
- package/dist/cli/__tests__/session-search-help.test.js +25 -1
- package/dist/cli/__tests__/session-search-help.test.js.map +1 -1
- package/dist/cli/__tests__/setup-agents-overwrite.test.js +8 -55
- package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
- package/dist/cli/__tests__/setup-install-mode.test.js +224 -7
- package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
- package/dist/cli/__tests__/sparkshell-cli.test.js +93 -41
- package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
- package/dist/cli/__tests__/team.test.js +150 -8
- package/dist/cli/__tests__/team.test.js.map +1 -1
- package/dist/cli/__tests__/ultragoal.test.js +117 -2
- package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
- package/dist/cli/__tests__/uninstall.test.js +95 -4
- package/dist/cli/__tests__/uninstall.test.js.map +1 -1
- package/dist/cli/__tests__/update.test.js +669 -177
- package/dist/cli/__tests__/update.test.js.map +1 -1
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js +33 -2
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
- package/dist/cli/api.d.ts.map +1 -1
- package/dist/cli/api.js +9 -10
- package/dist/cli/api.js.map +1 -1
- package/dist/cli/codex-feature-probe.d.ts +16 -0
- package/dist/cli/codex-feature-probe.d.ts.map +1 -1
- package/dist/cli/codex-feature-probe.js +44 -0
- package/dist/cli/codex-feature-probe.js.map +1 -1
- package/dist/cli/codex-home.d.ts.map +1 -1
- package/dist/cli/codex-home.js +7 -4
- package/dist/cli/codex-home.js.map +1 -1
- package/dist/cli/doctor.d.ts +20 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +290 -3
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.d.ts +234 -12
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2409 -746
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp-parity.js +2 -2
- package/dist/cli/mcp-parity.js.map +1 -1
- package/dist/cli/native-assets.d.ts +25 -0
- package/dist/cli/native-assets.d.ts.map +1 -1
- package/dist/cli/native-assets.js +448 -51
- package/dist/cli/native-assets.js.map +1 -1
- package/dist/cli/native-hook-claim-journal.d.ts +20 -5
- package/dist/cli/native-hook-claim-journal.d.ts.map +1 -1
- package/dist/cli/native-hook-claim-journal.js +67 -32
- package/dist/cli/native-hook-claim-journal.js.map +1 -1
- package/dist/cli/package-manager-ownership.d.ts +63 -0
- package/dist/cli/package-manager-ownership.d.ts.map +1 -0
- package/dist/cli/package-manager-ownership.js +323 -0
- package/dist/cli/package-manager-ownership.js.map +1 -0
- package/dist/cli/ralph.js +2 -2
- package/dist/cli/ralph.js.map +1 -1
- package/dist/cli/ralplan.d.ts +5 -7
- package/dist/cli/ralplan.d.ts.map +1 -1
- package/dist/cli/ralplan.js +83 -134
- package/dist/cli/ralplan.js.map +1 -1
- package/dist/cli/session-search.d.ts +5 -0
- package/dist/cli/session-search.d.ts.map +1 -1
- package/dist/cli/session-search.js +87 -0
- package/dist/cli/session-search.js.map +1 -1
- package/dist/cli/setup-preferences.d.ts +15 -0
- package/dist/cli/setup-preferences.d.ts.map +1 -1
- package/dist/cli/setup-preferences.js +27 -1
- package/dist/cli/setup-preferences.js.map +1 -1
- package/dist/cli/setup.d.ts +8 -0
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +119 -47
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/sparkshell.d.ts.map +1 -1
- package/dist/cli/sparkshell.js +52 -9
- package/dist/cli/sparkshell.js.map +1 -1
- package/dist/cli/ultragoal.d.ts.map +1 -1
- package/dist/cli/ultragoal.js +2 -2
- package/dist/cli/ultragoal.js.map +1 -1
- package/dist/cli/uninstall.d.ts +9 -0
- package/dist/cli/uninstall.d.ts.map +1 -1
- package/dist/cli/uninstall.js +71 -30
- package/dist/cli/uninstall.js.map +1 -1
- package/dist/cli/update-worker.d.ts +2 -0
- package/dist/cli/update-worker.d.ts.map +1 -0
- package/dist/cli/update-worker.js +142 -0
- package/dist/cli/update-worker.js.map +1 -0
- package/dist/cli/update.d.ts +14 -22
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +192 -241
- package/dist/cli/update.js.map +1 -1
- package/dist/compat/__tests__/rust-runtime-compat.test.js +1 -1
- package/dist/config/__tests__/generator-idempotent.test.js +503 -30
- package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
- package/dist/config/__tests__/generator-notify.test.js +5 -3
- package/dist/config/__tests__/generator-notify.test.js.map +1 -1
- package/dist/config/__tests__/models.test.js +67 -3
- package/dist/config/__tests__/models.test.js.map +1 -1
- package/dist/config/generator.d.ts +27 -21
- package/dist/config/generator.d.ts.map +1 -1
- package/dist/config/generator.js +610 -152
- package/dist/config/generator.js.map +1 -1
- package/dist/config/models.d.ts +12 -2
- package/dist/config/models.d.ts.map +1 -1
- package/dist/config/models.js +15 -1
- package/dist/config/models.js.map +1 -1
- package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js +25 -0
- package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js.map +1 -1
- package/dist/goal-workflows/codex-goal-snapshot.d.ts +1 -1
- package/dist/goal-workflows/codex-goal-snapshot.d.ts.map +1 -1
- package/dist/goal-workflows/codex-goal-snapshot.js +2 -0
- package/dist/goal-workflows/codex-goal-snapshot.js.map +1 -1
- package/dist/hooks/__tests__/agents-overlay.test.js +5 -3
- package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js +27 -4
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.d.ts +2 -0
- package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.js +172 -0
- package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.js.map +1 -0
- package/dist/hooks/__tests__/keyword-detector.test.js +176 -37
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js +402 -545
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +39 -4
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +161 -26
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js +27 -0
- package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js +10 -118
- package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-regression-205.test.js +7 -0
- package/dist/hooks/__tests__/notify-hook-regression-205.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +533 -14
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +159 -24
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js +482 -32
- package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.js +2 -1
- package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +68 -1
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js +7 -1
- package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +44 -4
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/prompt-guidance-contract.test.js +9 -0
- package/dist/hooks/__tests__/prompt-guidance-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/session.test.js +3130 -365
- package/dist/hooks/__tests__/session.test.js.map +1 -1
- package/dist/hooks/agents-overlay.js +1 -1
- package/dist/hooks/agents-overlay.js.map +1 -1
- package/dist/hooks/extensibility/__tests__/dispatcher.test.js +108 -2
- package/dist/hooks/extensibility/__tests__/dispatcher.test.js.map +1 -1
- package/dist/hooks/extensibility/__tests__/sdk.test.js +151 -1
- package/dist/hooks/extensibility/__tests__/sdk.test.js.map +1 -1
- package/dist/hooks/extensibility/dispatcher.d.ts.map +1 -1
- package/dist/hooks/extensibility/dispatcher.js +20 -3
- package/dist/hooks/extensibility/dispatcher.js.map +1 -1
- package/dist/hooks/extensibility/plugin-runner.js +1 -0
- package/dist/hooks/extensibility/plugin-runner.js.map +1 -1
- package/dist/hooks/extensibility/runtime.d.ts.map +1 -1
- package/dist/hooks/extensibility/runtime.js.map +1 -1
- package/dist/hooks/extensibility/sdk/paths.d.ts +1 -1
- package/dist/hooks/extensibility/sdk/paths.d.ts.map +1 -1
- package/dist/hooks/extensibility/sdk/paths.js +2 -2
- package/dist/hooks/extensibility/sdk/paths.js.map +1 -1
- package/dist/hooks/extensibility/sdk/runtime-state.d.ts +1 -1
- package/dist/hooks/extensibility/sdk/runtime-state.d.ts.map +1 -1
- package/dist/hooks/extensibility/sdk/runtime-state.js +55 -15
- package/dist/hooks/extensibility/sdk/runtime-state.js.map +1 -1
- package/dist/hooks/extensibility/sdk.d.ts +1 -0
- package/dist/hooks/extensibility/sdk.d.ts.map +1 -1
- package/dist/hooks/extensibility/sdk.js +1 -1
- package/dist/hooks/extensibility/sdk.js.map +1 -1
- package/dist/hooks/extensibility/types.d.ts +7 -0
- package/dist/hooks/extensibility/types.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.d.ts +10 -3
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +116 -20
- package/dist/hooks/keyword-detector.js.map +1 -1
- package/dist/hooks/session.d.ts +204 -14
- package/dist/hooks/session.d.ts.map +1 -1
- package/dist/hooks/session.js +1500 -223
- package/dist/hooks/session.js.map +1 -1
- package/dist/hud/__tests__/hud-tmux-injection.test.js +6 -6
- package/dist/hud/__tests__/hud-tmux-injection.test.js.map +1 -1
- package/dist/hud/__tests__/index.test.js +26 -16
- package/dist/hud/__tests__/index.test.js.map +1 -1
- package/dist/hud/__tests__/reconcile.test.js +83 -78
- package/dist/hud/__tests__/reconcile.test.js.map +1 -1
- package/dist/hud/__tests__/state.test.js +36 -31
- package/dist/hud/__tests__/state.test.js.map +1 -1
- package/dist/hud/__tests__/tmux.test.js +155 -198
- package/dist/hud/__tests__/tmux.test.js.map +1 -1
- package/dist/hud/tmux.d.ts +42 -4
- package/dist/hud/tmux.d.ts.map +1 -1
- package/dist/hud/tmux.js +1017 -139
- package/dist/hud/tmux.js.map +1 -1
- package/dist/leader/__tests__/contract.test.js +124 -37
- package/dist/leader/__tests__/contract.test.js.map +1 -1
- package/dist/leader/contract.d.ts +17 -6
- package/dist/leader/contract.d.ts.map +1 -1
- package/dist/leader/contract.js +102 -26
- package/dist/leader/contract.js.map +1 -1
- package/dist/mcp/__tests__/code-intel-server.test.js +31 -3
- package/dist/mcp/__tests__/code-intel-server.test.js.map +1 -1
- package/dist/mcp/__tests__/hermes-bridge.test.js +17 -0
- package/dist/mcp/__tests__/hermes-bridge.test.js.map +1 -1
- package/dist/mcp/__tests__/server-lifecycle.test.js +4 -4
- package/dist/mcp/__tests__/state-paths.test.js +735 -4
- package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
- package/dist/mcp/__tests__/state-server.test.js +1 -1
- package/dist/mcp/__tests__/state-server.test.js.map +1 -1
- package/dist/mcp/__tests__/trace-server.test.js +2 -2
- package/dist/mcp/__tests__/trace-server.test.js.map +1 -1
- package/dist/mcp/__tests__/wiki-server.test.js +18 -0
- package/dist/mcp/__tests__/wiki-server.test.js.map +1 -1
- package/dist/mcp/code-intel-server.d.ts.map +1 -1
- package/dist/mcp/code-intel-server.js +13 -3
- package/dist/mcp/code-intel-server.js.map +1 -1
- package/dist/mcp/hermes-bridge.d.ts.map +1 -1
- package/dist/mcp/hermes-bridge.js +2 -2
- package/dist/mcp/hermes-bridge.js.map +1 -1
- package/dist/mcp/state-paths.d.ts +78 -4
- package/dist/mcp/state-paths.d.ts.map +1 -1
- package/dist/mcp/state-paths.js +543 -40
- package/dist/mcp/state-paths.js.map +1 -1
- package/dist/mcp/state-server.d.ts +4 -4
- package/dist/mcp/state-server.d.ts.map +1 -1
- package/dist/mcp/wiki-server.d.ts +11 -0
- package/dist/mcp/wiki-server.d.ts.map +1 -1
- package/dist/mcp/wiki-server.js +9 -2
- package/dist/mcp/wiki-server.js.map +1 -1
- package/dist/modes/__tests__/base-autopilot-gates.test.js +36 -7
- package/dist/modes/__tests__/base-autopilot-gates.test.js.map +1 -1
- package/dist/modes/__tests__/base-multi-state-compat.test.js +1 -1
- package/dist/modes/__tests__/base-multi-state-compat.test.js.map +1 -1
- package/dist/modes/__tests__/base-session-scope.test.js +17 -8
- package/dist/modes/__tests__/base-session-scope.test.js.map +1 -1
- package/dist/modes/__tests__/base-tmux-pane.test.js +4 -4
- package/dist/modes/__tests__/base-tmux-pane.test.js.map +1 -1
- package/dist/modes/base.d.ts +5 -5
- package/dist/modes/base.d.ts.map +1 -1
- package/dist/modes/base.js +76 -21
- package/dist/modes/base.js.map +1 -1
- package/dist/native-assets/archive.d.ts +17 -0
- package/dist/native-assets/archive.d.ts.map +1 -0
- package/dist/native-assets/archive.js +238 -0
- package/dist/native-assets/archive.js.map +1 -0
- package/dist/native-assets/policy.d.ts +42 -0
- package/dist/native-assets/policy.d.ts.map +1 -0
- package/dist/native-assets/policy.js +155 -0
- package/dist/native-assets/policy.js.map +1 -0
- package/dist/notifications/temp-contract.d.ts.map +1 -1
- package/dist/notifications/temp-contract.js +4 -0
- package/dist/notifications/temp-contract.js.map +1 -1
- package/dist/pipeline/__tests__/orchestrator.test.js +164 -42
- package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
- package/dist/pipeline/__tests__/stages.test.js +72 -155
- package/dist/pipeline/__tests__/stages.test.js.map +1 -1
- package/dist/pipeline/orchestrator.d.ts.map +1 -1
- package/dist/pipeline/orchestrator.js +124 -18
- package/dist/pipeline/orchestrator.js.map +1 -1
- package/dist/pipeline/stages/ralplan.d.ts.map +1 -1
- package/dist/pipeline/stages/ralplan.js +36 -8
- package/dist/pipeline/stages/ralplan.js.map +1 -1
- package/dist/ralplan/__tests__/consensus-gate.test.js +73 -1316
- package/dist/ralplan/__tests__/consensus-gate.test.js.map +1 -1
- package/dist/ralplan/__tests__/documented-leader-preflight.test.d.ts +2 -0
- package/dist/ralplan/__tests__/documented-leader-preflight.test.d.ts.map +1 -0
- package/dist/ralplan/__tests__/documented-leader-preflight.test.js +335 -0
- package/dist/ralplan/__tests__/documented-leader-preflight.test.js.map +1 -0
- package/dist/ralplan/__tests__/runtime.test.js +85 -101
- package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
- package/dist/ralplan/consensus-gate.d.ts +9 -1
- package/dist/ralplan/consensus-gate.d.ts.map +1 -1
- package/dist/ralplan/consensus-gate.js +82 -338
- package/dist/ralplan/consensus-gate.js.map +1 -1
- package/dist/ralplan/documented-leader-preflight.d.ts +41 -0
- package/dist/ralplan/documented-leader-preflight.d.ts.map +1 -0
- package/dist/ralplan/documented-leader-preflight.js +360 -0
- package/dist/ralplan/documented-leader-preflight.js.map +1 -0
- package/dist/ralplan/runtime.d.ts +2 -1
- package/dist/ralplan/runtime.d.ts.map +1 -1
- package/dist/ralplan/runtime.js +67 -168
- package/dist/ralplan/runtime.js.map +1 -1
- package/dist/runtime/__tests__/bridge.test.js +42 -1
- package/dist/runtime/__tests__/bridge.test.js.map +1 -1
- package/dist/runtime/bridge.d.ts +12 -0
- package/dist/runtime/bridge.d.ts.map +1 -1
- package/dist/runtime/bridge.js +52 -1
- package/dist/runtime/bridge.js.map +1 -1
- package/dist/runtime/run-state.d.ts +5 -1
- package/dist/runtime/run-state.d.ts.map +1 -1
- package/dist/runtime/run-state.js +15 -8
- package/dist/runtime/run-state.js.map +1 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js +16323 -8978
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/__tests__/issue-3293-callsite-parity.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3293-callsite-parity.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3293-callsite-parity.test.js +225 -0
- package/dist/scripts/__tests__/issue-3293-callsite-parity.test.js.map +1 -0
- package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.js +188 -0
- package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.js.map +1 -0
- package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.js +259 -0
- package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.js.map +1 -0
- package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.js +491 -0
- package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.js.map +1 -0
- package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.js +708 -0
- package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.js.map +1 -0
- package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.js +208 -0
- package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.js.map +1 -0
- package/dist/scripts/__tests__/notify-state-io.test.js +26 -0
- package/dist/scripts/__tests__/notify-state-io.test.js.map +1 -1
- package/dist/scripts/__tests__/notify-tmux-injection.test.js +23 -1
- package/dist/scripts/__tests__/notify-tmux-injection.test.js.map +1 -1
- package/dist/scripts/__tests__/postinstall.test.js +19 -1
- package/dist/scripts/__tests__/postinstall.test.js.map +1 -1
- package/dist/scripts/__tests__/reasoning-artifact-contract.test.d.ts +2 -0
- package/dist/scripts/__tests__/reasoning-artifact-contract.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/reasoning-artifact-contract.test.js +108 -0
- package/dist/scripts/__tests__/reasoning-artifact-contract.test.js.map +1 -0
- package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js +63 -173
- package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js.map +1 -1
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js +74 -159
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js.map +1 -1
- package/dist/scripts/__tests__/smoke-packed-install.test.js +404 -13
- package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
- package/dist/scripts/__tests__/verify-native-release-assets.test.d.ts +2 -0
- package/dist/scripts/__tests__/verify-native-release-assets.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/verify-native-release-assets.test.js +235 -0
- package/dist/scripts/__tests__/verify-native-release-assets.test.js.map +1 -0
- package/dist/scripts/codex-native-hook.d.ts +53 -1
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +14448 -1480
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/scripts/codex-native-pre-post.d.ts +9 -0
- package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
- package/dist/scripts/codex-native-pre-post.js +35 -12
- package/dist/scripts/codex-native-pre-post.js.map +1 -1
- package/dist/scripts/generate-native-release-manifest.js +80 -51
- package/dist/scripts/generate-native-release-manifest.js.map +1 -1
- package/dist/scripts/notify-fallback-watcher.js +78 -115
- package/dist/scripts/notify-fallback-watcher.js.map +1 -1
- package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
- package/dist/scripts/notify-hook/auto-nudge.js +74 -3
- package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
- package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
- package/dist/scripts/notify-hook/state-io.js +5 -0
- package/dist/scripts/notify-hook/state-io.js.map +1 -1
- package/dist/scripts/notify-hook/team-dispatch.d.ts +9 -0
- package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-dispatch.js +234 -79
- package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
- package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-leader-nudge.js +71 -16
- package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.d.ts +12 -4
- package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.js +365 -44
- package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
- package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-worker-stop.js +42 -21
- package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
- package/dist/scripts/notify-hook/team-worker.d.ts +3 -0
- package/dist/scripts/notify-hook/team-worker.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-worker.js +82 -33
- package/dist/scripts/notify-hook/team-worker.js.map +1 -1
- package/dist/scripts/notify-hook/tmux-injection.d.ts +1 -0
- package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
- package/dist/scripts/notify-hook/tmux-injection.js +32 -3
- package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
- package/dist/scripts/notify-hook.js +79 -82
- package/dist/scripts/notify-hook.js.map +1 -1
- package/dist/scripts/postinstall-advisory.d.ts +14 -0
- package/dist/scripts/postinstall-advisory.d.ts.map +1 -0
- package/dist/scripts/postinstall-advisory.js +49 -0
- package/dist/scripts/postinstall-advisory.js.map +1 -0
- package/dist/scripts/postinstall.d.ts +1 -1
- package/dist/scripts/postinstall.d.ts.map +1 -1
- package/dist/scripts/postinstall.js +1 -1
- package/dist/scripts/postinstall.js.map +1 -1
- package/dist/scripts/smoke-packed-install.d.ts +80 -4
- package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
- package/dist/scripts/smoke-packed-install.js +3162 -26
- package/dist/scripts/smoke-packed-install.js.map +1 -1
- package/dist/scripts/sync-plugin-mirror.d.ts.map +1 -1
- package/dist/scripts/sync-plugin-mirror.js +16 -1
- package/dist/scripts/sync-plugin-mirror.js.map +1 -1
- package/dist/scripts/verify-native-release-assets.js +37 -23
- package/dist/scripts/verify-native-release-assets.js.map +1 -1
- package/dist/state/__tests__/mode-state-context.test.js +61 -0
- package/dist/state/__tests__/mode-state-context.test.js.map +1 -1
- package/dist/state/__tests__/operations.test.js +462 -850
- package/dist/state/__tests__/operations.test.js.map +1 -1
- package/dist/state/__tests__/skill-active.test.js +400 -2
- package/dist/state/__tests__/skill-active.test.js.map +1 -1
- package/dist/state/mode-state-context.d.ts +8 -0
- package/dist/state/mode-state-context.d.ts.map +1 -1
- package/dist/state/mode-state-context.js +73 -0
- package/dist/state/mode-state-context.js.map +1 -1
- package/dist/state/operations.d.ts +3 -0
- package/dist/state/operations.d.ts.map +1 -1
- package/dist/state/operations.js +144 -40
- package/dist/state/operations.js.map +1 -1
- package/dist/state/skill-active.d.ts +18 -1
- package/dist/state/skill-active.d.ts.map +1 -1
- package/dist/state/skill-active.js +420 -23
- package/dist/state/skill-active.js.map +1 -1
- package/dist/state/workflow-transition-reconcile.d.ts +3 -0
- package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
- package/dist/state/workflow-transition-reconcile.js +10 -4
- package/dist/state/workflow-transition-reconcile.js.map +1 -1
- package/dist/state/workflow-transition.d.ts.map +1 -1
- package/dist/state/workflow-transition.js +3 -1
- package/dist/state/workflow-transition.js.map +1 -1
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.js +53 -247
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.js.map +1 -1
- package/dist/subagents/__tests__/tracker.test.js +837 -1048
- package/dist/subagents/__tests__/tracker.test.js.map +1 -1
- package/dist/subagents/tracker.d.ts +58 -135
- package/dist/subagents/tracker.d.ts.map +1 -1
- package/dist/subagents/tracker.js +811 -632
- package/dist/subagents/tracker.js.map +1 -1
- package/dist/team/__tests__/api-interop.test.js +91 -1
- package/dist/team/__tests__/api-interop.test.js.map +1 -1
- package/dist/team/__tests__/delivery-e2e-smoke.test.js +58 -17
- package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
- package/dist/team/__tests__/idle-nudge.test.js +45 -15
- package/dist/team/__tests__/idle-nudge.test.js.map +1 -1
- package/dist/team/__tests__/mcp-comm.test.js +197 -30
- package/dist/team/__tests__/mcp-comm.test.js.map +1 -1
- package/dist/team/__tests__/model-contract.test.js +110 -3
- package/dist/team/__tests__/model-contract.test.js.map +1 -1
- package/dist/team/__tests__/notice-ledger.test.d.ts +2 -0
- package/dist/team/__tests__/notice-ledger.test.d.ts.map +1 -0
- package/dist/team/__tests__/notice-ledger.test.js +131 -0
- package/dist/team/__tests__/notice-ledger.test.js.map +1 -0
- package/dist/team/__tests__/pane-teardown-residue-e2e.test.d.ts +2 -0
- package/dist/team/__tests__/pane-teardown-residue-e2e.test.d.ts.map +1 -0
- package/dist/team/__tests__/pane-teardown-residue-e2e.test.js +410 -0
- package/dist/team/__tests__/pane-teardown-residue-e2e.test.js.map +1 -0
- package/dist/team/__tests__/phase-controller.test.js +20 -0
- package/dist/team/__tests__/phase-controller.test.js.map +1 -1
- package/dist/team/__tests__/runtime-cli.test.js +9 -0
- package/dist/team/__tests__/runtime-cli.test.js.map +1 -1
- package/dist/team/__tests__/runtime.test.js +3963 -428
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/scaling.test.js +2405 -203
- package/dist/team/__tests__/scaling.test.js.map +1 -1
- package/dist/team/__tests__/state-root.test.js +117 -2
- package/dist/team/__tests__/state-root.test.js.map +1 -1
- package/dist/team/__tests__/state.test.js +804 -36
- package/dist/team/__tests__/state.test.js.map +1 -1
- package/dist/team/__tests__/team-ops-contract.test.js +6 -0
- package/dist/team/__tests__/team-ops-contract.test.js.map +1 -1
- package/dist/team/__tests__/tmux-session.test.js +4099 -1039
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/__tests__/tmux-source-authority-realtmux.test.d.ts +2 -0
- package/dist/team/__tests__/tmux-source-authority-realtmux.test.d.ts.map +1 -0
- package/dist/team/__tests__/tmux-source-authority-realtmux.test.js +149 -0
- package/dist/team/__tests__/tmux-source-authority-realtmux.test.js.map +1 -0
- package/dist/team/__tests__/tmux-test-fixture.d.ts +69 -0
- package/dist/team/__tests__/tmux-test-fixture.d.ts.map +1 -1
- package/dist/team/__tests__/tmux-test-fixture.js +242 -15
- package/dist/team/__tests__/tmux-test-fixture.js.map +1 -1
- package/dist/team/__tests__/tmux-test-fixture.test.js +177 -1
- package/dist/team/__tests__/tmux-test-fixture.test.js.map +1 -1
- package/dist/team/__tests__/worker-runtime-identity.test.js +53 -44
- package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
- package/dist/team/__tests__/worktree.test.js +6 -0
- package/dist/team/__tests__/worktree.test.js.map +1 -1
- package/dist/team/api-interop.d.ts.map +1 -1
- package/dist/team/api-interop.js +15 -9
- package/dist/team/api-interop.js.map +1 -1
- package/dist/team/exact-pane.d.ts +23 -0
- package/dist/team/exact-pane.d.ts.map +1 -0
- package/dist/team/exact-pane.js +133 -0
- package/dist/team/exact-pane.js.map +1 -0
- package/dist/team/idle-nudge.d.ts +12 -1
- package/dist/team/idle-nudge.d.ts.map +1 -1
- package/dist/team/idle-nudge.js +41 -14
- package/dist/team/idle-nudge.js.map +1 -1
- package/dist/team/model-contract.d.ts +2 -5
- package/dist/team/model-contract.d.ts.map +1 -1
- package/dist/team/model-contract.js +59 -17
- package/dist/team/model-contract.js.map +1 -1
- package/dist/team/notice-ledger.d.ts +54 -0
- package/dist/team/notice-ledger.d.ts.map +1 -0
- package/dist/team/notice-ledger.js +259 -0
- package/dist/team/notice-ledger.js.map +1 -0
- package/dist/team/phase-controller.d.ts.map +1 -1
- package/dist/team/phase-controller.js +2 -0
- package/dist/team/phase-controller.js.map +1 -1
- package/dist/team/runtime.d.ts +4 -1
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +1527 -291
- package/dist/team/runtime.js.map +1 -1
- package/dist/team/scaling.d.ts +13 -1
- package/dist/team/scaling.d.ts.map +1 -1
- package/dist/team/scaling.js +1503 -387
- package/dist/team/scaling.js.map +1 -1
- package/dist/team/state/dispatch.d.ts.map +1 -1
- package/dist/team/state/dispatch.js +26 -8
- package/dist/team/state/dispatch.js.map +1 -1
- package/dist/team/state/locks.d.ts.map +1 -1
- package/dist/team/state/locks.js +61 -13
- package/dist/team/state/locks.js.map +1 -1
- package/dist/team/state/monitor.d.ts +10 -0
- package/dist/team/state/monitor.d.ts.map +1 -1
- package/dist/team/state/monitor.js +13 -0
- package/dist/team/state/monitor.js.map +1 -1
- package/dist/team/state/tasks.d.ts.map +1 -1
- package/dist/team/state/tasks.js +4 -3
- package/dist/team/state/tasks.js.map +1 -1
- package/dist/team/state/types.d.ts +15 -0
- package/dist/team/state/types.d.ts.map +1 -1
- package/dist/team/state/types.js.map +1 -1
- package/dist/team/state-root.d.ts +12 -0
- package/dist/team/state-root.d.ts.map +1 -1
- package/dist/team/state-root.js +177 -0
- package/dist/team/state-root.js.map +1 -1
- package/dist/team/state.d.ts +85 -1
- package/dist/team/state.d.ts.map +1 -1
- package/dist/team/state.js +672 -129
- package/dist/team/state.js.map +1 -1
- package/dist/team/team-ops.d.ts +5 -0
- package/dist/team/team-ops.d.ts.map +1 -1
- package/dist/team/team-ops.js +4 -0
- package/dist/team/team-ops.js.map +1 -1
- package/dist/team/tmux-session.d.ts +163 -27
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +1682 -376
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/team/worktree.d.ts.map +1 -1
- package/dist/team/worktree.js +4 -0
- package/dist/team/worktree.js.map +1 -1
- package/dist/ultragoal/__tests__/artifacts.test.js +950 -2
- package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
- package/dist/ultragoal/__tests__/docs-contract.test.js +1 -0
- package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
- package/dist/ultragoal/artifacts.d.ts +35 -1
- package/dist/ultragoal/artifacts.d.ts.map +1 -1
- package/dist/ultragoal/artifacts.js +232 -46
- package/dist/ultragoal/artifacts.js.map +1 -1
- package/dist/utils/__tests__/file-durability.test.d.ts +2 -0
- package/dist/utils/__tests__/file-durability.test.d.ts.map +1 -0
- package/dist/utils/__tests__/file-durability.test.js +114 -0
- package/dist/utils/__tests__/file-durability.test.js.map +1 -0
- package/dist/utils/__tests__/paths.test.js +21 -1
- package/dist/utils/__tests__/paths.test.js.map +1 -1
- package/dist/utils/file-durability.d.ts +25 -0
- package/dist/utils/file-durability.d.ts.map +1 -0
- package/dist/utils/file-durability.js +67 -0
- package/dist/utils/file-durability.js.map +1 -0
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +14 -6
- package/dist/utils/paths.js.map +1 -1
- package/dist/verification/__tests__/ci-rust-gates.test.js +49 -4
- package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
- package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js +3 -0
- package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js.map +1 -1
- package/dist/verification/__tests__/explore-harness-release-workflow.test.js +60 -0
- package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +1 -1
- package/dist/verification/__tests__/native-release-manifest.test.js +157 -4
- package/dist/verification/__tests__/native-release-manifest.test.js.map +1 -1
- package/dist/wiki/lint.d.ts +4 -1
- package/dist/wiki/lint.d.ts.map +1 -1
- package/dist/wiki/lint.js +3 -2
- package/dist/wiki/lint.js.map +1 -1
- package/package.json +13 -5
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +105 -17
- package/plugins/oh-my-codex/skills/autopilot/SKILL.md +13 -5
- package/plugins/oh-my-codex/skills/cancel/SKILL.md +30 -85
- package/plugins/oh-my-codex/skills/ralph/SKILL.md +6 -4
- package/plugins/oh-my-codex/skills/ralplan/SKILL.md +16 -17
- package/plugins/oh-my-codex/skills/team/SKILL.md +13 -9
- package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +3 -2
- package/skills/autopilot/SKILL.md +13 -5
- package/skills/cancel/SKILL.md +30 -85
- package/skills/ralph/SKILL.md +6 -4
- package/skills/ralplan/SKILL.md +16 -17
- package/skills/team/SKILL.md +13 -9
- package/skills/ultragoal/SKILL.md +3 -2
- package/src/scripts/__tests__/codex-native-hook.test.ts +8831 -951
- package/src/scripts/__tests__/issue-3293-callsite-parity.test.ts +187 -0
- package/src/scripts/__tests__/issue-3293-cancel-transaction.test.ts +189 -0
- package/src/scripts/__tests__/issue-3293-hook-owned-cancel.test.ts +187 -0
- package/src/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.ts +591 -0
- package/src/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.ts +701 -0
- package/src/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.ts +211 -0
- package/src/scripts/__tests__/notify-state-io.test.ts +29 -0
- package/src/scripts/__tests__/notify-tmux-injection.test.ts +26 -1
- package/src/scripts/__tests__/postinstall.test.ts +23 -1
- package/src/scripts/__tests__/reasoning-artifact-contract.test.ts +129 -0
- package/src/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.ts +67 -185
- package/src/scripts/__tests__/role-intent-durable-recovery-3181.test.ts +79 -178
- package/src/scripts/__tests__/smoke-packed-install.test.ts +525 -16
- package/src/scripts/__tests__/verify-native-release-assets.test.ts +240 -0
- package/src/scripts/codex-native-hook.ts +14238 -1429
- package/src/scripts/codex-native-pre-post.ts +43 -12
- package/src/scripts/generate-native-release-manifest.ts +83 -79
- package/src/scripts/notify-fallback-watcher.ts +91 -122
- package/src/scripts/notify-hook/auto-nudge.ts +71 -3
- package/src/scripts/notify-hook/state-io.ts +11 -0
- package/src/scripts/notify-hook/team-dispatch.ts +242 -75
- package/src/scripts/notify-hook/team-leader-nudge.ts +72 -22
- package/src/scripts/notify-hook/team-tmux-guard.ts +366 -39
- package/src/scripts/notify-hook/team-worker-stop.ts +43 -20
- package/src/scripts/notify-hook/team-worker.ts +82 -31
- package/src/scripts/notify-hook/tmux-injection.ts +33 -3
- package/src/scripts/notify-hook.ts +84 -82
- package/src/scripts/postinstall-advisory.ts +67 -0
- package/src/scripts/postinstall.ts +1 -1
- package/src/scripts/smoke-packed-install.ts +3608 -39
- package/src/scripts/sync-plugin-mirror.ts +17 -1
- package/src/scripts/verify-native-release-assets.ts +39 -31
- package/templates/AGENTS.md +1 -0
- package/dist/subagents/__tests__/adapted-role-binding.test.d.ts +0 -2
- package/dist/subagents/__tests__/adapted-role-binding.test.d.ts.map +0 -1
- package/dist/subagents/__tests__/adapted-role-binding.test.js +0 -988
- package/dist/subagents/__tests__/adapted-role-binding.test.js.map +0 -1
- package/dist/subagents/adapted-role-binding.d.ts +0 -17
- package/dist/subagents/adapted-role-binding.d.ts.map +0 -1
- package/dist/subagents/adapted-role-binding.js +0 -75
- package/dist/subagents/adapted-role-binding.js.map +0 -1
package/dist/hooks/session.js
CHANGED
|
@@ -5,13 +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';
|
|
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
|
+
}
|
|
14
26
|
const SESSION_FILE = 'session.json';
|
|
27
|
+
const SESSION_OWNER_FILE = 'session-owner.json';
|
|
15
28
|
const HISTORY_FILE = 'session-history.jsonl';
|
|
16
29
|
const SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
|
|
17
30
|
const SESSION_POINTER_TOKEN_PATTERN = /^[A-Za-z0-9_-]{16,128}$/;
|
|
@@ -19,6 +32,14 @@ const SHA256_PATTERN = /^[a-f0-9]{64}$/;
|
|
|
19
32
|
const LOCK_RETRY_DELAYS_MS = [25, 50, 100];
|
|
20
33
|
const DEFAULT_POINTER_TIMEOUT_MS = 5_000;
|
|
21
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
|
+
}
|
|
22
43
|
/**
|
|
23
44
|
* Convert arbitrary input into a valid session ID without exposing validator
|
|
24
45
|
* exceptions to lifecycle or hook inputs.
|
|
@@ -30,9 +51,9 @@ export function normalizeSessionId(value) {
|
|
|
30
51
|
return SESSION_ID_PATTERN.test(trimmed) ? trimmed : undefined;
|
|
31
52
|
}
|
|
32
53
|
/** Resolve the one exact pointer path used by an operation. */
|
|
33
|
-
export function resolveSessionPointerContext(cwd) {
|
|
34
|
-
const normalizedCwd = resolveWorkingDirectoryForState(cwd);
|
|
35
|
-
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);
|
|
36
57
|
const pointerPath = join(baseStateDir, SESSION_FILE);
|
|
37
58
|
return {
|
|
38
59
|
cwd: normalizedCwd,
|
|
@@ -42,6 +63,26 @@ export function resolveSessionPointerContext(cwd) {
|
|
|
42
63
|
lockPath: `${pointerPath}.lock`,
|
|
43
64
|
};
|
|
44
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
|
+
}
|
|
45
86
|
function attemptedStateRootSource() {
|
|
46
87
|
try {
|
|
47
88
|
if (process.env.OMX_TEAM_STATE_ROOT?.trim())
|
|
@@ -122,13 +163,14 @@ const defaultFsDependencies = {
|
|
|
122
163
|
},
|
|
123
164
|
readdir: async (path) => await nodeReaddir(path),
|
|
124
165
|
readFile: async (path, encoding) => await nodeReadFile(path, encoding),
|
|
166
|
+
lstat: async (path) => await nodeLstat(path),
|
|
125
167
|
writeFile: async (path, data, options) => {
|
|
126
168
|
await nodeWriteFile(path, data, options);
|
|
127
169
|
},
|
|
128
|
-
openAndSync: async (path) => {
|
|
170
|
+
openAndSync: async (path, platform, regularFileSync) => {
|
|
129
171
|
const handle = await open(path, 'r');
|
|
130
172
|
try {
|
|
131
|
-
await
|
|
173
|
+
return await syncRegularFile(regularFileSync ? { sync: () => regularFileSync(platform) } : handle, platform);
|
|
132
174
|
}
|
|
133
175
|
finally {
|
|
134
176
|
await handle.close();
|
|
@@ -137,6 +179,9 @@ const defaultFsDependencies = {
|
|
|
137
179
|
rename: async (from, to) => {
|
|
138
180
|
await nodeRename(from, to);
|
|
139
181
|
},
|
|
182
|
+
link: async (path, dest) => {
|
|
183
|
+
await nodeLink(path, dest);
|
|
184
|
+
},
|
|
140
185
|
unlink: async (path) => {
|
|
141
186
|
await nodeUnlink(path);
|
|
142
187
|
},
|
|
@@ -144,6 +189,28 @@ const defaultFsDependencies = {
|
|
|
144
189
|
await nodeRmdir(path);
|
|
145
190
|
},
|
|
146
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
|
+
}
|
|
147
214
|
/** @internal Exposed only so source-module tests can verify default ESRCH handling. */
|
|
148
215
|
export function __createDefaultPidProbeForTests(killZero) {
|
|
149
216
|
return (pid) => {
|
|
@@ -161,26 +228,40 @@ function defaultProbePid(pid) {
|
|
|
161
228
|
process.kill(targetPid, 0);
|
|
162
229
|
})(pid);
|
|
163
230
|
}
|
|
164
|
-
function
|
|
165
|
-
if (platform
|
|
166
|
-
return
|
|
167
|
-
|
|
168
|
-
if (
|
|
169
|
-
return {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
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
|
+
}
|
|
176
251
|
}
|
|
177
252
|
const defaultTransactionDependencies = {
|
|
178
253
|
fs: defaultFsDependencies,
|
|
179
254
|
nowMs: () => Date.now(),
|
|
180
255
|
sleep: async (ms) => await new Promise((resolve) => setTimeout(resolve, ms)),
|
|
181
256
|
token: () => randomUUID(),
|
|
257
|
+
runtimePlatform: process.platform,
|
|
182
258
|
probePid: defaultProbePid,
|
|
183
|
-
|
|
259
|
+
observeProcess: defaultObserveProcess,
|
|
260
|
+
atomicRenameNoReplace: defaultRecoveryRenameNoReplace,
|
|
261
|
+
};
|
|
262
|
+
export const defaultProcessInspectionProvider = {
|
|
263
|
+
probePid: defaultProbePid,
|
|
264
|
+
observeProcess: defaultObserveProcess,
|
|
184
265
|
};
|
|
185
266
|
let transactionDependencies = defaultTransactionDependencies;
|
|
186
267
|
/** @internal Source-module test harness. Always reset after a test. */
|
|
@@ -232,13 +313,118 @@ function readLinuxProcessIdentity(pid) {
|
|
|
232
313
|
return null;
|
|
233
314
|
}
|
|
234
315
|
}
|
|
235
|
-
|
|
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;
|
|
236
363
|
try {
|
|
237
|
-
|
|
238
|
-
|
|
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
|
+
};
|
|
239
425
|
}
|
|
240
426
|
catch {
|
|
241
|
-
return
|
|
427
|
+
return { kind: 'error' };
|
|
242
428
|
}
|
|
243
429
|
}
|
|
244
430
|
/**
|
|
@@ -248,22 +434,122 @@ function defaultIsPidAlive(pid) {
|
|
|
248
434
|
export function isSessionStale(state, options = {}) {
|
|
249
435
|
if (!Number.isInteger(state.pid) || state.pid <= 0)
|
|
250
436
|
return true;
|
|
251
|
-
const
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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';
|
|
267
553
|
}
|
|
268
554
|
export function isSessionStateAuthoritativeForCwd(state, cwd) {
|
|
269
555
|
if (!normalizeSessionId(state.session_id))
|
|
@@ -283,54 +569,47 @@ export function isSessionStateUsable(state, cwd, options = {}) {
|
|
|
283
569
|
if (typeof state.cwd === 'string' && state.cwd.trim() && !isSessionStateAuthoritativeForCwd(state, cwd))
|
|
284
570
|
return false;
|
|
285
571
|
const hasPidMetadata = Number.isInteger(state.pid) && state.pid > 0;
|
|
286
|
-
const
|
|
287
|
-
|| typeof state.pid_cmdline === 'string'
|
|
288
|
-
|
|
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);
|
|
289
577
|
}
|
|
290
578
|
function isValidStartTicks(value) {
|
|
291
579
|
return typeof value === 'number' && Number.isInteger(value) && value >= 0;
|
|
292
580
|
}
|
|
293
581
|
function classifySessionProcess(state, dependencies) {
|
|
294
582
|
const hasPidMetadata = Number.isInteger(state.pid) && state.pid > 0;
|
|
295
|
-
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;
|
|
296
587
|
if (!hasPidMetadata && !hasIdentityMetadata)
|
|
297
588
|
return 'usable';
|
|
298
589
|
if (!hasPidMetadata)
|
|
299
590
|
return 'identity-indeterminate';
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
}
|
|
304
|
-
catch {
|
|
305
|
-
return 'identity-indeterminate';
|
|
306
|
-
}
|
|
307
|
-
if (pidStatus === 'dead')
|
|
308
|
-
return 'stale-dead';
|
|
309
|
-
if (pidStatus !== 'alive')
|
|
310
|
-
return 'identity-indeterminate';
|
|
311
|
-
const platform = state.platform ?? process.platform;
|
|
312
|
-
if (platform !== 'linux')
|
|
313
|
-
return 'usable';
|
|
314
|
-
if (!isValidStartTicks(state.pid_start_ticks))
|
|
315
|
-
return 'identity-indeterminate';
|
|
316
|
-
let liveIdentity;
|
|
317
|
-
try {
|
|
318
|
-
liveIdentity = dependencies.readProcessIdentity(state.pid, platform);
|
|
319
|
-
}
|
|
320
|
-
catch {
|
|
591
|
+
const runtimePlatform = dependencies.runtimePlatform;
|
|
592
|
+
const recorded = recordedIdentityForState(state, runtimePlatform);
|
|
593
|
+
if (recorded === 'invalid')
|
|
321
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';
|
|
322
603
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
}
|
|
333
|
-
return 'usable';
|
|
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);
|
|
334
613
|
}
|
|
335
614
|
function classifyParsedSessionPointer(context, value, raw) {
|
|
336
615
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
@@ -342,6 +621,16 @@ function classifyParsedSessionPointer(context, value, raw) {
|
|
|
342
621
|
if (typeof state.cwd === 'string' && state.cwd.trim() && !isSessionStateAuthoritativeForCwd(state, context.cwd)) {
|
|
343
622
|
return { status: 'foreign-cwd', raw, state };
|
|
344
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 };
|
|
345
634
|
const processStatus = classifySessionProcess(state, transactionDependencies);
|
|
346
635
|
return { status: processStatus, state, raw };
|
|
347
636
|
}
|
|
@@ -380,9 +669,9 @@ export async function readUsableSessionStateFromContext(context) {
|
|
|
380
669
|
return state && isSessionStateUsable(state, context.cwd) ? state : null;
|
|
381
670
|
}
|
|
382
671
|
/** Read current session state from the exact selected root. */
|
|
383
|
-
export async function readSessionState(cwd) {
|
|
672
|
+
export async function readSessionState(cwd, env = process.env) {
|
|
384
673
|
try {
|
|
385
|
-
return await readSessionStateFromContext(resolveSessionPointerContext(cwd));
|
|
674
|
+
return await readSessionStateFromContext(resolveSessionPointerContext(cwd, env));
|
|
386
675
|
}
|
|
387
676
|
catch {
|
|
388
677
|
return null;
|
|
@@ -398,7 +687,7 @@ export async function readUsableSessionState(cwd, options = {}) {
|
|
|
398
687
|
return null;
|
|
399
688
|
}
|
|
400
689
|
}
|
|
401
|
-
function createSessionState(cwd, sessionId, pid, platform,
|
|
690
|
+
function createSessionState(cwd, stateRoot, sessionId, pid, platform, identity, options = {}) {
|
|
402
691
|
const nowIso = options.nowIso ?? new Date().toISOString();
|
|
403
692
|
const nativeSessionId = normalizeNonempty(options.nativeSessionId);
|
|
404
693
|
const previousNativeSessionId = normalizeNonempty(options.previousNativeSessionId);
|
|
@@ -406,6 +695,27 @@ function createSessionState(cwd, sessionId, pid, platform, linuxIdentity, option
|
|
|
406
695
|
const ownerOmxSessionId = normalizeSessionId(options.ownerOmxSessionId);
|
|
407
696
|
const tmuxSessionName = normalizeNonempty(options.tmuxSessionName);
|
|
408
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;
|
|
409
719
|
return {
|
|
410
720
|
session_id: sessionId,
|
|
411
721
|
...(nativeSessionId ? { native_session_id: nativeSessionId } : {}),
|
|
@@ -413,15 +723,23 @@ function createSessionState(cwd, sessionId, pid, platform, linuxIdentity, option
|
|
|
413
723
|
...(nativeSessionSwitchedAt ? { native_session_switched_at: nativeSessionSwitchedAt } : {}),
|
|
414
724
|
...(ownerOmxSessionId ? { owner_omx_session_id: ownerOmxSessionId } : {}),
|
|
415
725
|
started_at: options.startedAt ?? nowIso,
|
|
726
|
+
...(isValidToken(options.launchLineageToken) ? { launch_lineage_token: options.launchLineageToken } : {}),
|
|
416
727
|
cwd,
|
|
728
|
+
state_root: stateRoot,
|
|
417
729
|
pid,
|
|
418
730
|
platform,
|
|
419
|
-
...(
|
|
420
|
-
...(
|
|
731
|
+
...(publishIdentity ? { identity_schema_version: 2, process_identity: publishIdentity } : {}),
|
|
732
|
+
...(legacyStartTicks !== undefined ? { pid_start_ticks: legacyStartTicks } : {}),
|
|
733
|
+
...(legacyCmdline ? { pid_cmdline: legacyCmdline } : {}),
|
|
421
734
|
...(tmuxSessionName ? { tmux_session_name: tmuxSessionName } : {}),
|
|
422
735
|
...(tmuxPaneId ? { tmux_pane_id: tmuxPaneId } : {}),
|
|
423
736
|
};
|
|
424
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
|
+
}
|
|
425
743
|
function normalizeNonempty(value) {
|
|
426
744
|
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
427
745
|
}
|
|
@@ -429,7 +747,15 @@ function resolvePid(options) {
|
|
|
429
747
|
return Number.isInteger(options.pid) && options.pid && options.pid > 0 ? options.pid : process.pid;
|
|
430
748
|
}
|
|
431
749
|
function sessionIdentityFor(pid, platform) {
|
|
432
|
-
|
|
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
|
+
}
|
|
433
759
|
}
|
|
434
760
|
function currentOwnerAlias(state) {
|
|
435
761
|
return normalizeSessionId(state.owner_omx_session_id);
|
|
@@ -456,29 +782,38 @@ function isStartCompatible(existing, requestedSessionId) {
|
|
|
456
782
|
|| existing.native_session_id === requestedSessionId
|
|
457
783
|
|| currentOwnerAlias(existing) === requestedSessionId;
|
|
458
784
|
}
|
|
459
|
-
function getOmxLaunchSessionId(state) {
|
|
460
|
-
if (state.session_id.startsWith('omx-'))
|
|
461
|
-
return state.session_id;
|
|
462
|
-
const owner = currentOwnerAlias(state);
|
|
463
|
-
return owner?.startsWith('omx-') ? owner : undefined;
|
|
464
|
-
}
|
|
465
785
|
function parseLockOwner(raw) {
|
|
466
786
|
try {
|
|
467
|
-
const
|
|
468
|
-
if (!
|
|
787
|
+
const parsed = JSON.parse(raw);
|
|
788
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
469
789
|
return null;
|
|
470
|
-
|
|
790
|
+
const value = parsed;
|
|
791
|
+
if (!isValidToken(value.token) || typeof value.pid !== 'number' || !Number.isInteger(value.pid) || value.pid <= 0)
|
|
471
792
|
return null;
|
|
472
|
-
if (
|
|
793
|
+
if (!isNodePlatform(value.platform) || typeof value.created_at !== 'string' || !value.created_at)
|
|
473
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;
|
|
474
803
|
}
|
|
475
|
-
if (value.
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
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;
|
|
480
815
|
}
|
|
481
|
-
return
|
|
816
|
+
return null;
|
|
482
817
|
}
|
|
483
818
|
catch {
|
|
484
819
|
return null;
|
|
@@ -487,73 +822,388 @@ function parseLockOwner(raw) {
|
|
|
487
822
|
function isValidToken(value) {
|
|
488
823
|
return typeof value === 'string' && SESSION_POINTER_TOKEN_PATTERN.test(value);
|
|
489
824
|
}
|
|
490
|
-
|
|
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
|
+
}
|
|
836
|
+
async function inspectLockOwnerFile(ownerPath, missingStatus = 'missing') {
|
|
491
837
|
let raw;
|
|
492
838
|
try {
|
|
493
|
-
raw = await transactionDependencies.fs.readFile(
|
|
839
|
+
raw = await transactionDependencies.fs.readFile(ownerPath, 'utf8');
|
|
494
840
|
}
|
|
495
841
|
catch (error) {
|
|
496
|
-
return isNotFound(error) ? { status:
|
|
842
|
+
return isNotFound(error) ? { status: missingStatus } : { status: 'identity-indeterminate' };
|
|
497
843
|
}
|
|
498
844
|
const owner = parseLockOwner(raw);
|
|
499
845
|
if (!owner)
|
|
500
846
|
return { status: 'malformed' };
|
|
501
|
-
|
|
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 };
|
|
859
|
+
}
|
|
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 };
|
|
866
|
+
}
|
|
867
|
+
}
|
|
868
|
+
async function inspectLockOwner(lockPath) {
|
|
869
|
+
return await inspectLockOwnerFile(join(lockPath, 'owner.json'));
|
|
870
|
+
}
|
|
871
|
+
async function inspectSessionPointerLockAtContext(context) {
|
|
872
|
+
const absent = () => ({ status: 'absent', lockPath: context.lockPath, evidenceSource: 'none', safeToRecover: false });
|
|
873
|
+
let lockStat;
|
|
874
|
+
try {
|
|
875
|
+
lockStat = await transactionDependencies.fs.lstat(context.lockPath);
|
|
876
|
+
}
|
|
877
|
+
catch (error) {
|
|
878
|
+
return isNotFound(error) ? absent() : { status: 'io-error', lockPath: context.lockPath, evidenceSource: 'none', safeToRecover: false };
|
|
879
|
+
}
|
|
880
|
+
if (lockStat.isSymbolicLink())
|
|
881
|
+
return { status: 'symlink', lockPath: context.lockPath, evidenceSource: 'none', safeToRecover: false };
|
|
882
|
+
if (!lockStat.isDirectory())
|
|
883
|
+
return { status: 'unexpected', lockPath: context.lockPath, evidenceSource: 'none', safeToRecover: false };
|
|
884
|
+
let entries;
|
|
502
885
|
try {
|
|
503
|
-
|
|
886
|
+
entries = await transactionDependencies.fs.readdir(context.lockPath);
|
|
504
887
|
}
|
|
505
888
|
catch {
|
|
506
|
-
return { status: '
|
|
507
|
-
}
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
if (
|
|
511
|
-
return { status: '
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
889
|
+
return { status: 'io-error', lockPath: context.lockPath, evidenceSource: 'none', safeToRecover: false };
|
|
890
|
+
}
|
|
891
|
+
const tempEntries = entries.filter((entry) => /^owner\.([A-Za-z0-9_-]{16,128})\.tmp$/.test(entry));
|
|
892
|
+
const hasCanonical = entries.includes('owner.json');
|
|
893
|
+
if (entries.length === 0)
|
|
894
|
+
return { status: 'missing-owner', lockPath: context.lockPath, evidenceSource: 'none', safeToRecover: false };
|
|
895
|
+
if ((hasCanonical && entries.length !== 1) || (!hasCanonical && tempEntries.length !== 1) || entries.length !== 1) {
|
|
896
|
+
return { status: entries.length > 1 && (hasCanonical || tempEntries.length > 0) ? 'ambiguous' : 'unexpected', lockPath: context.lockPath, evidenceSource: 'none', safeToRecover: false };
|
|
897
|
+
}
|
|
898
|
+
const evidenceName = hasCanonical ? 'owner.json' : tempEntries[0];
|
|
899
|
+
const evidenceSource = hasCanonical ? 'owner.json' : 'owner-temp';
|
|
900
|
+
const evidencePath = join(context.lockPath, evidenceName);
|
|
901
|
+
let evidenceStat;
|
|
518
902
|
try {
|
|
519
|
-
|
|
903
|
+
evidenceStat = await transactionDependencies.fs.lstat(evidencePath);
|
|
520
904
|
}
|
|
521
905
|
catch {
|
|
522
|
-
return { status: '
|
|
906
|
+
return { status: 'io-error', lockPath: context.lockPath, evidenceSource, safeToRecover: false };
|
|
907
|
+
}
|
|
908
|
+
if (evidenceStat.isSymbolicLink())
|
|
909
|
+
return { status: 'symlink', lockPath: context.lockPath, evidenceSource, safeToRecover: false };
|
|
910
|
+
if (!evidenceStat.isFile())
|
|
911
|
+
return { status: 'unexpected', lockPath: context.lockPath, evidenceSource, safeToRecover: false };
|
|
912
|
+
const owner = await inspectLockOwnerFile(evidencePath, 'missing');
|
|
913
|
+
if (evidenceSource === 'owner-temp' && owner.owner?.token !== /^owner\.([A-Za-z0-9_-]{16,128})\.tmp$/.exec(evidenceName)?.[1]) {
|
|
914
|
+
return { status: 'malformed', lockPath: context.lockPath, evidenceSource, safeToRecover: false };
|
|
915
|
+
}
|
|
916
|
+
const status = owner.status === 'missing' ? 'io-error' : owner.status;
|
|
917
|
+
return {
|
|
918
|
+
status,
|
|
919
|
+
lockPath: context.lockPath,
|
|
920
|
+
evidenceSource,
|
|
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',
|
|
928
|
+
};
|
|
929
|
+
}
|
|
930
|
+
/** Inspect only exact, regular owner evidence in the selected pointer lock. */
|
|
931
|
+
export async function inspectSessionPointerLock(cwd) {
|
|
932
|
+
return await inspectSessionPointerLockAtContext(resolveSessionPointerContext(cwd));
|
|
933
|
+
}
|
|
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) {
|
|
938
|
+
try {
|
|
939
|
+
return await transactionDependencies.fs.lstat(path);
|
|
940
|
+
}
|
|
941
|
+
catch (error) {
|
|
942
|
+
if (isNotFound(error))
|
|
943
|
+
return undefined;
|
|
944
|
+
throw error;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
async function probeRecoveryRenameCapability() {
|
|
948
|
+
let probeDir;
|
|
949
|
+
try {
|
|
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';
|
|
958
|
+
}
|
|
959
|
+
catch {
|
|
960
|
+
return 'unsupported';
|
|
961
|
+
}
|
|
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) {
|
|
972
|
+
try {
|
|
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.' };
|
|
978
|
+
}
|
|
979
|
+
catch (error) {
|
|
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 */ }
|
|
994
|
+
}
|
|
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.' };
|
|
1002
|
+
}
|
|
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);
|
|
1011
|
+
try {
|
|
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 };
|
|
523
1088
|
}
|
|
524
|
-
|
|
525
|
-
|
|
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}).` };
|
|
526
1092
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
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;
|
|
1099
|
+
try {
|
|
1100
|
+
checkpointNames = (await transactionDependencies.fs.readdir(dirname(context.lockPath))).filter((name) => name.startsWith(checkpointPrefix) && name.endsWith('.json'));
|
|
1101
|
+
}
|
|
1102
|
+
catch (error) {
|
|
1103
|
+
if (!isNotFound(error))
|
|
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 = [];
|
|
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}).` };
|
|
1114
|
+
try {
|
|
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') {
|
|
1137
|
+
return {
|
|
1138
|
+
...inspection,
|
|
1139
|
+
action: 'none',
|
|
1140
|
+
recovered: false,
|
|
1141
|
+
reason: 'Atomic no-replace recovery rename is unsupported on this platform.',
|
|
1142
|
+
};
|
|
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
|
+
};
|
|
533
1175
|
}
|
|
534
|
-
return { status: 'live', owner };
|
|
535
1176
|
}
|
|
536
1177
|
function buildLockOwner(token) {
|
|
537
|
-
|
|
1178
|
+
const runtimePlatform = transactionDependencies.runtimePlatform;
|
|
1179
|
+
let observation;
|
|
538
1180
|
try {
|
|
539
|
-
|
|
1181
|
+
observation = transactionDependencies.observeProcess(process.pid, runtimePlatform);
|
|
540
1182
|
}
|
|
541
1183
|
catch {
|
|
542
1184
|
// Publication remains valid with optional identity metadata omitted.
|
|
543
1185
|
}
|
|
544
|
-
|
|
545
|
-
|
|
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 = {
|
|
546
1197
|
token,
|
|
547
1198
|
pid: process.pid,
|
|
548
|
-
platform:
|
|
549
|
-
...(
|
|
550
|
-
|
|
551
|
-
: {}),
|
|
552
|
-
...(identity?.status === 'matching' && identity.cmdlineHash && SHA256_PATTERN.test(identity.cmdlineHash)
|
|
553
|
-
? { pid_cmdline_hash: identity.cmdlineHash }
|
|
554
|
-
: {}),
|
|
1199
|
+
platform: runtimePlatform,
|
|
1200
|
+
...(legacyStartTicks !== undefined ? { pid_start_ticks: legacyStartTicks } : {}),
|
|
1201
|
+
...(legacyHash ? { pid_cmdline_hash: legacyHash } : {}),
|
|
555
1202
|
created_at: new Date(transactionDependencies.nowMs()).toISOString(),
|
|
556
1203
|
};
|
|
1204
|
+
return identity
|
|
1205
|
+
? { version: 2, ...common, process_identity: identity }
|
|
1206
|
+
: { version: 1, ...common };
|
|
557
1207
|
}
|
|
558
1208
|
async function removeOwnedPath(path) {
|
|
559
1209
|
try {
|
|
@@ -639,7 +1289,7 @@ async function rollbackUnpublishedLock(context, ownerTempPath) {
|
|
|
639
1289
|
}
|
|
640
1290
|
return failures;
|
|
641
1291
|
}
|
|
642
|
-
async function acquirePointerLock(context, candidateSessionId, timeoutMs) {
|
|
1292
|
+
async function acquirePointerLock(context, candidateSessionId, timeoutMs, platform, tracker, regularFileSync) {
|
|
643
1293
|
const deadline = transactionDependencies.nowMs() + timeoutMs;
|
|
644
1294
|
let attempt = 0;
|
|
645
1295
|
while (true) {
|
|
@@ -734,7 +1384,7 @@ async function acquirePointerLock(context, candidateSessionId, timeoutMs) {
|
|
|
734
1384
|
const ownerPath = join(context.lockPath, 'owner.json');
|
|
735
1385
|
try {
|
|
736
1386
|
await transactionDependencies.fs.writeFile(ownerTempPath, JSON.stringify(buildLockOwner(token)), { mode: 0o600 });
|
|
737
|
-
await transactionDependencies.fs.openAndSync(ownerTempPath);
|
|
1387
|
+
recordRegularFileSyncOutcome(tracker, await transactionDependencies.fs.openAndSync(ownerTempPath, platform, regularFileSync));
|
|
738
1388
|
await transactionDependencies.fs.rename(ownerTempPath, ownerPath);
|
|
739
1389
|
}
|
|
740
1390
|
catch (error) {
|
|
@@ -792,7 +1442,7 @@ async function releasePointerLock(lock) {
|
|
|
792
1442
|
if (!isNotFound(error)) {
|
|
793
1443
|
return [{
|
|
794
1444
|
operation: 'lock-release',
|
|
795
|
-
phase: 'remove-release-
|
|
1445
|
+
phase: 'remove-release-owner',
|
|
796
1446
|
ownership: 'released',
|
|
797
1447
|
message: `Unable to remove released session pointer lock owner: ${errorMessage(error)}`,
|
|
798
1448
|
cause: error,
|
|
@@ -815,6 +1465,10 @@ async function releasePointerLock(lock) {
|
|
|
815
1465
|
}];
|
|
816
1466
|
}
|
|
817
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
|
+
}
|
|
818
1472
|
function recoveryAbort(context, primary, failures, operation) {
|
|
819
1473
|
return resolvedAbort(context, {
|
|
820
1474
|
code: 'session_pointer_lock_recovery_required',
|
|
@@ -829,9 +1483,12 @@ function recoveryAbort(context, primary, failures, operation) {
|
|
|
829
1483
|
secondaryFailures: failures,
|
|
830
1484
|
});
|
|
831
1485
|
}
|
|
832
|
-
function releaseFailureError(failures) {
|
|
833
|
-
const error = new
|
|
834
|
-
|
|
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;
|
|
835
1492
|
return error;
|
|
836
1493
|
}
|
|
837
1494
|
async function finalizePrecommitAbort(lock, primary, pointerTempPath) {
|
|
@@ -873,7 +1530,7 @@ function ownerConflictAbort(context, candidateSessionId, state, cause) {
|
|
|
873
1530
|
...(cause ? { cause } : {}),
|
|
874
1531
|
});
|
|
875
1532
|
}
|
|
876
|
-
async function writePointerTransaction(cwd, candidateSessionId, options, timeoutMs, transition, pointerState) {
|
|
1533
|
+
async function writePointerTransaction(cwd, candidateSessionId, options, timeoutMs, transition, pointerState, beforeCommit, afterCommit) {
|
|
877
1534
|
let context;
|
|
878
1535
|
try {
|
|
879
1536
|
context = options.context ?? resolveSessionPointerContext(cwd);
|
|
@@ -904,9 +1561,12 @@ async function writePointerTransaction(cwd, candidateSessionId, options, timeout
|
|
|
904
1561
|
cause: error,
|
|
905
1562
|
});
|
|
906
1563
|
}
|
|
907
|
-
const
|
|
1564
|
+
const platform = options.platform ?? process.platform;
|
|
1565
|
+
const tracker = { degraded: false };
|
|
1566
|
+
const lock = await acquirePointerLock(context, candidateSessionId, timeoutMs, platform, tracker, options.regularFileSync);
|
|
908
1567
|
const pointerTempPath = `${context.sessionPath}.tmp-${lock.token}`;
|
|
909
1568
|
let pointerCommitted = false;
|
|
1569
|
+
let pointerTempWritten = false;
|
|
910
1570
|
try {
|
|
911
1571
|
let pointer;
|
|
912
1572
|
try {
|
|
@@ -924,7 +1584,7 @@ async function writePointerTransaction(cwd, candidateSessionId, options, timeout
|
|
|
924
1584
|
}
|
|
925
1585
|
let next;
|
|
926
1586
|
try {
|
|
927
|
-
next = transition(pointer, context);
|
|
1587
|
+
next = await transition(pointer, context);
|
|
928
1588
|
}
|
|
929
1589
|
catch (error) {
|
|
930
1590
|
if (isSessionPointerLaunchAbort(error))
|
|
@@ -939,6 +1599,7 @@ async function writePointerTransaction(cwd, candidateSessionId, options, timeout
|
|
|
939
1599
|
}
|
|
940
1600
|
const serialized = JSON.stringify(pointerState(next), null, 2);
|
|
941
1601
|
try {
|
|
1602
|
+
pointerTempWritten = true;
|
|
942
1603
|
await transactionDependencies.fs.writeFile(pointerTempPath, serialized, { mode: 0o600, flag: 'wx' });
|
|
943
1604
|
}
|
|
944
1605
|
catch (error) {
|
|
@@ -952,7 +1613,7 @@ async function writePointerTransaction(cwd, candidateSessionId, options, timeout
|
|
|
952
1613
|
});
|
|
953
1614
|
}
|
|
954
1615
|
try {
|
|
955
|
-
await transactionDependencies.fs.openAndSync(pointerTempPath);
|
|
1616
|
+
recordRegularFileSyncOutcome(tracker, await transactionDependencies.fs.openAndSync(pointerTempPath, platform, options.regularFileSync));
|
|
956
1617
|
}
|
|
957
1618
|
catch (error) {
|
|
958
1619
|
throw resolvedAbort(context, {
|
|
@@ -964,11 +1625,31 @@ async function writePointerTransaction(cwd, candidateSessionId, options, timeout
|
|
|
964
1625
|
cause: error,
|
|
965
1626
|
});
|
|
966
1627
|
}
|
|
1628
|
+
if (beforeCommit)
|
|
1629
|
+
await beforeCommit(context);
|
|
967
1630
|
try {
|
|
968
1631
|
await transactionDependencies.fs.rename(pointerTempPath, context.sessionPath);
|
|
969
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
|
+
}
|
|
970
1649
|
}
|
|
971
1650
|
catch (error) {
|
|
1651
|
+
if (error instanceof CommittedLaunchBlockedError)
|
|
1652
|
+
throw error;
|
|
972
1653
|
throw resolvedAbort(context, {
|
|
973
1654
|
code: 'session_pointer_io_failure',
|
|
974
1655
|
operation: 'pointer-rename',
|
|
@@ -980,7 +1661,8 @@ async function writePointerTransaction(cwd, candidateSessionId, options, timeout
|
|
|
980
1661
|
}
|
|
981
1662
|
const releaseFailures = await releasePointerLock(lock);
|
|
982
1663
|
if (releaseFailures.length > 0)
|
|
983
|
-
throw releaseFailureError(releaseFailures);
|
|
1664
|
+
throw releaseFailureError(releaseFailures, context, next);
|
|
1665
|
+
emitDegradedDurabilityWarning('session pointer start/reconcile', tracker);
|
|
984
1666
|
return { context, value: next };
|
|
985
1667
|
}
|
|
986
1668
|
catch (error) {
|
|
@@ -996,14 +1678,18 @@ async function writePointerTransaction(cwd, candidateSessionId, options, timeout
|
|
|
996
1678
|
reason: `Session pointer transition failed before commit: ${errorMessage(error)}`,
|
|
997
1679
|
cause: error,
|
|
998
1680
|
});
|
|
999
|
-
const ownsPointerTemp =
|
|
1000
|
-
|| primary.operation === 'pointer-fsync'
|
|
1001
|
-
|| primary.operation === 'pointer-rename';
|
|
1681
|
+
const ownsPointerTemp = pointerTempWritten;
|
|
1002
1682
|
throw await finalizePrecommitAbort(lock, primary, ownsPointerTemp ? pointerTempPath : undefined);
|
|
1003
1683
|
}
|
|
1004
1684
|
}
|
|
1005
|
-
function startPointerTransition(requestedSessionId, options) {
|
|
1685
|
+
function startPointerTransition(requestedSessionId, options, requireLaunchLineageToken = false, requireAbsent = false) {
|
|
1006
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
|
+
}
|
|
1007
1693
|
if (pointer.status !== 'absent' && pointer.status !== 'stale-dead' && pointer.status !== 'usable') {
|
|
1008
1694
|
throw unusablePointerAbort(context, requestedSessionId, pointer);
|
|
1009
1695
|
}
|
|
@@ -1011,6 +1697,9 @@ function startPointerTransition(requestedSessionId, options) {
|
|
|
1011
1697
|
if (existing && !isStartCompatible(existing, requestedSessionId)) {
|
|
1012
1698
|
throw ownerConflictAbort(context, requestedSessionId, existing);
|
|
1013
1699
|
}
|
|
1700
|
+
if (requireLaunchLineageToken && existing && !isValidToken(existing.launch_lineage_token)) {
|
|
1701
|
+
throw ownerConflictAbort(context, requestedSessionId, existing);
|
|
1702
|
+
}
|
|
1014
1703
|
const canonicalSessionId = existing?.session_id ?? requestedSessionId;
|
|
1015
1704
|
const pid = resolvePid(options);
|
|
1016
1705
|
const platform = options.platform ?? process.platform;
|
|
@@ -1027,20 +1716,51 @@ function startPointerTransition(requestedSessionId, options) {
|
|
|
1027
1716
|
pid,
|
|
1028
1717
|
}, error);
|
|
1029
1718
|
}
|
|
1030
|
-
|
|
1719
|
+
const state = createSessionState(context.cwd, context.baseStateDir, canonicalSessionId, pid, platform, sessionIdentityFor(pid, platform), {
|
|
1031
1720
|
nativeSessionId: options.nativeSessionId ?? existing?.native_session_id,
|
|
1032
1721
|
previousNativeSessionId: options.previousNativeSessionId ?? existing?.previous_native_session_id,
|
|
1033
1722
|
nativeSessionSwitchedAt: options.nativeSessionSwitchedAt ?? existing?.native_session_switched_at,
|
|
1034
1723
|
...(ownerOmxSessionId ? { ownerOmxSessionId } : {}),
|
|
1724
|
+
launchLineageToken: existing
|
|
1725
|
+
? (isValidToken(existing.launch_lineage_token) ? existing.launch_lineage_token : undefined)
|
|
1726
|
+
: transactionDependencies.token(),
|
|
1035
1727
|
tmuxSessionName: options.tmuxSessionName ?? existing?.tmux_session_name,
|
|
1036
1728
|
tmuxPaneId: options.tmuxPaneId ?? existing?.tmux_pane_id,
|
|
1037
1729
|
});
|
|
1730
|
+
return preserveExistingLaunchLineageToken(existing, state);
|
|
1038
1731
|
};
|
|
1039
1732
|
}
|
|
1040
|
-
/**
|
|
1733
|
+
/** Create a wrapper-owned canonical pointer only when the selected pointer is absent. */
|
|
1041
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) {
|
|
1042
1762
|
const requestedSessionId = normalizeSessionId(sessionId);
|
|
1043
|
-
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);
|
|
1044
1764
|
await appendToLogAtContext(result.context, {
|
|
1045
1765
|
event: 'session_start',
|
|
1046
1766
|
session_id: result.value.session_id,
|
|
@@ -1050,6 +1770,551 @@ export async function writeSessionStart(cwd, sessionId, options = {}) {
|
|
|
1050
1770
|
}).catch(() => { });
|
|
1051
1771
|
return result.value;
|
|
1052
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
|
+
}
|
|
1053
2318
|
function reconcileNativeTransition(nativeSessionId, options) {
|
|
1054
2319
|
return (pointer, context) => {
|
|
1055
2320
|
if (pointer.status !== 'absent' && pointer.status !== 'stale-dead' && pointer.status !== 'usable') {
|
|
@@ -1059,42 +2324,19 @@ function reconcileNativeTransition(nativeSessionId, options) {
|
|
|
1059
2324
|
const platform = options.platform ?? process.platform;
|
|
1060
2325
|
const linuxIdentity = sessionIdentityFor(pid, platform);
|
|
1061
2326
|
const existing = pointer.status === 'usable' ? pointer.state : undefined;
|
|
1062
|
-
const nowIso = new Date().toISOString();
|
|
1063
2327
|
if (!existing) {
|
|
1064
2328
|
const ownerCandidate = verifiedOwnerCandidate(context, options);
|
|
1065
2329
|
const ownerOmxSessionId = ownerCandidate;
|
|
1066
|
-
return {
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
}),
|
|
1071
|
-
};
|
|
2330
|
+
return createSessionState(context.cwd, context.baseStateDir, nativeSessionId, pid, platform, linuxIdentity, {
|
|
2331
|
+
nativeSessionId,
|
|
2332
|
+
...(ownerOmxSessionId ? { ownerOmxSessionId } : {}),
|
|
2333
|
+
});
|
|
1072
2334
|
}
|
|
1073
2335
|
const existingNativeSessionId = normalizeSessionId(existing.native_session_id);
|
|
1074
2336
|
if (existingNativeSessionId && existingNativeSessionId !== nativeSessionId) {
|
|
1075
|
-
|
|
1076
|
-
return {
|
|
1077
|
-
state: createSessionState(context.cwd, nativeSessionId, pid, platform, linuxIdentity, {
|
|
1078
|
-
nativeSessionId,
|
|
1079
|
-
...(ownerOmxSessionId ? {
|
|
1080
|
-
previousNativeSessionId: existingNativeSessionId,
|
|
1081
|
-
nativeSessionSwitchedAt: nowIso,
|
|
1082
|
-
ownerOmxSessionId,
|
|
1083
|
-
} : {}),
|
|
1084
|
-
}),
|
|
1085
|
-
...(ownerOmxSessionId ? {
|
|
1086
|
-
replacementLog: {
|
|
1087
|
-
event: 'native_session_replaced',
|
|
1088
|
-
session_id: ownerOmxSessionId,
|
|
1089
|
-
...(existing.session_id !== ownerOmxSessionId ? { active_session_id: existing.session_id } : {}),
|
|
1090
|
-
previous_native_session_id: existingNativeSessionId,
|
|
1091
|
-
replaced_by_native_session_id: nativeSessionId,
|
|
1092
|
-
pid,
|
|
1093
|
-
timestamp: nowIso,
|
|
1094
|
-
},
|
|
1095
|
-
} : {}),
|
|
1096
|
-
};
|
|
2337
|
+
throw ownerConflictAbort(context, nativeSessionId, existing);
|
|
1097
2338
|
}
|
|
2339
|
+
const nowIso = new Date().toISOString();
|
|
1098
2340
|
const ownerCandidate = verifiedOwnerCandidate(context, options);
|
|
1099
2341
|
let ownerOmxSessionId;
|
|
1100
2342
|
try {
|
|
@@ -1103,39 +2345,37 @@ function reconcileNativeTransition(nativeSessionId, options) {
|
|
|
1103
2345
|
catch (error) {
|
|
1104
2346
|
throw ownerConflictAbort(context, nativeSessionId, existing, error);
|
|
1105
2347
|
}
|
|
1106
|
-
return {
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
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
|
+
}));
|
|
1118
2361
|
};
|
|
1119
2362
|
}
|
|
1120
2363
|
/**
|
|
1121
|
-
* Reconcile
|
|
1122
|
-
*
|
|
1123
|
-
*
|
|
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.
|
|
1124
2367
|
*/
|
|
1125
2368
|
export async function reconcileNativeSessionStart(cwd, nativeSessionId, options = {}) {
|
|
1126
2369
|
const normalizedNativeSessionId = normalizeSessionId(nativeSessionId);
|
|
1127
|
-
const result = await writePointerTransaction(cwd, normalizedNativeSessionId, options, NATIVE_POINTER_TIMEOUT_MS, reconcileNativeTransition(normalizedNativeSessionId ?? nativeSessionId, options), (
|
|
1128
|
-
if (result.value.replacementLog) {
|
|
1129
|
-
await appendToLogAtContext(result.context, result.value.replacementLog).catch(() => { });
|
|
1130
|
-
}
|
|
2370
|
+
const result = await writePointerTransaction(cwd, normalizedNativeSessionId, options, NATIVE_POINTER_TIMEOUT_MS, reconcileNativeTransition(normalizedNativeSessionId ?? nativeSessionId, options), (state) => state);
|
|
1131
2371
|
await appendToLogAtContext(result.context, {
|
|
1132
|
-
event:
|
|
1133
|
-
session_id: result.value.
|
|
2372
|
+
event: 'session_start_reconciled',
|
|
2373
|
+
session_id: result.value.session_id,
|
|
1134
2374
|
native_session_id: normalizedNativeSessionId ?? nativeSessionId,
|
|
1135
|
-
pid: result.value.
|
|
1136
|
-
timestamp:
|
|
2375
|
+
pid: result.value.pid,
|
|
2376
|
+
timestamp: new Date().toISOString(),
|
|
1137
2377
|
}).catch(() => { });
|
|
1138
|
-
return result.value
|
|
2378
|
+
return result.value;
|
|
1139
2379
|
}
|
|
1140
2380
|
function historyDirectory(context) {
|
|
1141
2381
|
return join(dirname(context.baseStateDir), 'logs');
|
|
@@ -1181,25 +2421,45 @@ export async function writeSessionEnd(cwd, sessionId, options = {}) {
|
|
|
1181
2421
|
cause,
|
|
1182
2422
|
});
|
|
1183
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');
|
|
1184
2432
|
try {
|
|
1185
|
-
await
|
|
2433
|
+
preLockPointer = await readSessionPointer(context);
|
|
2434
|
+
traceSessionFinalizationOperation(`preLock-read-done:${preLockPointer.status}`);
|
|
1186
2435
|
}
|
|
1187
2436
|
catch (error) {
|
|
1188
2437
|
throw resolvedAbort(context, {
|
|
1189
|
-
code: 'session_pointer_io_failure',
|
|
1190
|
-
|
|
1191
|
-
candidateSessionId,
|
|
1192
|
-
lockPath: context.lockPath,
|
|
1193
|
-
reason: `Unable to create selected session state directory: ${errorMessage(error)}`,
|
|
1194
|
-
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,
|
|
1195
2440
|
});
|
|
1196
2441
|
}
|
|
1197
|
-
const
|
|
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');
|
|
2448
|
+
const platform = options.platform ?? process.platform;
|
|
2449
|
+
const tracker = { degraded: false };
|
|
2450
|
+
traceSessionFinalizationOperation('acquirePointerLock-start');
|
|
2451
|
+
const lock = await acquirePointerLock(context, candidateSessionId, DEFAULT_POINTER_TIMEOUT_MS, platform, tracker, options.regularFileSync);
|
|
2452
|
+
traceSessionFinalizationOperation('acquirePointerLock-done');
|
|
1198
2453
|
let primary;
|
|
2454
|
+
let revalidation = {
|
|
2455
|
+
comparison: { status: 'not-run' }, capability: [],
|
|
2456
|
+
};
|
|
1199
2457
|
try {
|
|
1200
2458
|
let pointer;
|
|
2459
|
+
traceSessionFinalizationOperation('under-lock-read-start');
|
|
1201
2460
|
try {
|
|
1202
2461
|
pointer = await readSessionPointer(context);
|
|
2462
|
+
traceSessionFinalizationOperation(`under-lock-read-done:${pointer.status}`);
|
|
1203
2463
|
}
|
|
1204
2464
|
catch (error) {
|
|
1205
2465
|
throw resolvedAbort(context, {
|
|
@@ -1211,14 +2471,17 @@ export async function writeSessionEnd(cwd, sessionId, options = {}) {
|
|
|
1211
2471
|
cause: error,
|
|
1212
2472
|
});
|
|
1213
2473
|
}
|
|
1214
|
-
|
|
2474
|
+
const authorized = isAuthorizedBoundPointerForFinalization(options.binding, context, candidateSessionId, pointer, 'under-lock');
|
|
2475
|
+
if (!authorized)
|
|
1215
2476
|
throw unusablePointerAbort(context, candidateSessionId, pointer);
|
|
1216
|
-
|
|
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');
|
|
1217
2483
|
const state = pointer.state;
|
|
1218
|
-
const ownsCurrentSessionFile =
|
|
1219
|
-
|| state.session_id === candidateSessionId
|
|
1220
|
-
|| state.native_session_id === candidateSessionId
|
|
1221
|
-
|| state.owner_omx_session_id === candidateSessionId;
|
|
2484
|
+
const ownsCurrentSessionFile = true;
|
|
1222
2485
|
const endTime = new Date().toISOString();
|
|
1223
2486
|
const historyEntry = {
|
|
1224
2487
|
session_id: ownsCurrentSessionFile
|
|
@@ -1234,12 +2497,19 @@ export async function writeSessionEnd(cwd, sessionId, options = {}) {
|
|
|
1234
2497
|
: {}),
|
|
1235
2498
|
...(!ownsCurrentSessionFile && state?.session_id ? { preserved_active_session_id: state.session_id } : {}),
|
|
1236
2499
|
};
|
|
2500
|
+
traceSessionFinalizationOperation('history-mkdir-start');
|
|
1237
2501
|
await nodeMkdir(historyDirectory(context), { recursive: true });
|
|
2502
|
+
traceSessionFinalizationOperation('history-mkdir-done');
|
|
2503
|
+
traceSessionFinalizationOperation('history-append-start');
|
|
1238
2504
|
await appendFile(historyPath(context), `${JSON.stringify(historyEntry)}\n`);
|
|
2505
|
+
traceSessionFinalizationOperation('history-append-done');
|
|
2506
|
+
traceSessionFinalizationOperation('hud-cleanup-start');
|
|
1239
2507
|
await removeDeadSessionHudState(context, [
|
|
1240
2508
|
...(ownsCurrentSessionFile ? [state?.session_id, state?.native_session_id] : []),
|
|
1241
2509
|
candidateSessionId,
|
|
1242
2510
|
]);
|
|
2511
|
+
traceSessionFinalizationOperation('hud-cleanup-done');
|
|
2512
|
+
traceSessionFinalizationOperation('session-unlink-start');
|
|
1243
2513
|
if (ownsCurrentSessionFile) {
|
|
1244
2514
|
try {
|
|
1245
2515
|
await transactionDependencies.fs.unlink(context.sessionPath);
|
|
@@ -1249,6 +2519,8 @@ export async function writeSessionEnd(cwd, sessionId, options = {}) {
|
|
|
1249
2519
|
throw error;
|
|
1250
2520
|
}
|
|
1251
2521
|
}
|
|
2522
|
+
traceSessionFinalizationOperation('session-unlink-done');
|
|
2523
|
+
traceSessionFinalizationOperation('session-end-log-start');
|
|
1252
2524
|
await appendToLogAtContext(context, {
|
|
1253
2525
|
event: 'session_end',
|
|
1254
2526
|
session_id: historyEntry.session_id,
|
|
@@ -1257,11 +2529,14 @@ export async function writeSessionEnd(cwd, sessionId, options = {}) {
|
|
|
1257
2529
|
...(historyEntry.preserved_active_session_id ? { preserved_active_session_id: historyEntry.preserved_active_session_id } : {}),
|
|
1258
2530
|
timestamp: endTime,
|
|
1259
2531
|
}).catch(() => { });
|
|
2532
|
+
traceSessionFinalizationOperation('session-end-log-done');
|
|
1260
2533
|
}
|
|
1261
2534
|
catch (error) {
|
|
1262
2535
|
primary = error;
|
|
1263
2536
|
}
|
|
2537
|
+
traceSessionFinalizationOperation('lock-release-start');
|
|
1264
2538
|
const releaseFailures = await releasePointerLock(lock);
|
|
2539
|
+
traceSessionFinalizationOperation('lock-release-done');
|
|
1265
2540
|
if (primary) {
|
|
1266
2541
|
if (isSessionPointerLaunchAbort(primary) && releaseFailures.length > 0) {
|
|
1267
2542
|
throw recoveryAbort(context, primary, releaseFailures, 'lock-release');
|
|
@@ -1270,6 +2545,8 @@ export async function writeSessionEnd(cwd, sessionId, options = {}) {
|
|
|
1270
2545
|
}
|
|
1271
2546
|
if (releaseFailures.length > 0)
|
|
1272
2547
|
throw releaseFailureError(releaseFailures);
|
|
2548
|
+
emitDegradedDurabilityWarning('session pointer end', tracker);
|
|
2549
|
+
return revalidation;
|
|
1273
2550
|
}
|
|
1274
2551
|
/** Reset session-scoped HUD/metrics files at launch. */
|
|
1275
2552
|
export async function resetSessionMetrics(cwd, sessionId) {
|