oh-my-codex 0.20.3 → 0.21.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/Cargo.lock +35 -18
- package/Cargo.toml +1 -1
- package/README.md +22 -19
- package/crates/omx-runtime/Cargo.toml +3 -0
- package/crates/omx-runtime/src/main.rs +390 -0
- package/crates/omx-runtime/tests/execution.rs +264 -0
- package/dist/adapt/__tests__/herdr-target.test.d.ts +2 -0
- package/dist/adapt/__tests__/herdr-target.test.d.ts.map +1 -0
- package/dist/adapt/__tests__/herdr-target.test.js +72 -0
- package/dist/adapt/__tests__/herdr-target.test.js.map +1 -0
- package/dist/adapt/contracts.d.ts +1 -1
- package/dist/adapt/contracts.d.ts.map +1 -1
- package/dist/adapt/contracts.js +1 -1
- package/dist/adapt/contracts.js.map +1 -1
- package/dist/adapt/herdr/__tests__/authority.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/authority.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/authority.test.js +49 -0
- package/dist/adapt/herdr/__tests__/authority.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/bridge.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/bridge.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/bridge.test.js +176 -0
- package/dist/adapt/herdr/__tests__/bridge.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/sanitize.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/sanitize.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/sanitize.test.js +45 -0
- package/dist/adapt/herdr/__tests__/sanitize.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/semantic.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/semantic.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/semantic.test.js +130 -0
- package/dist/adapt/herdr/__tests__/semantic.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/seq-store.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/seq-store.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/seq-store.test.js +54 -0
- package/dist/adapt/herdr/__tests__/seq-store.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/socket-live.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/socket-live.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/socket-live.test.js +104 -0
- package/dist/adapt/herdr/__tests__/socket-live.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/team-map.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/team-map.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/team-map.test.js +46 -0
- package/dist/adapt/herdr/__tests__/team-map.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/transport.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/transport.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/transport.test.js +160 -0
- package/dist/adapt/herdr/__tests__/transport.test.js.map +1 -0
- package/dist/adapt/herdr/__tests__/wiring.test.d.ts +2 -0
- package/dist/adapt/herdr/__tests__/wiring.test.d.ts.map +1 -0
- package/dist/adapt/herdr/__tests__/wiring.test.js +118 -0
- package/dist/adapt/herdr/__tests__/wiring.test.js.map +1 -0
- package/dist/adapt/herdr/authority.d.ts +34 -0
- package/dist/adapt/herdr/authority.d.ts.map +1 -0
- package/dist/adapt/herdr/authority.js +71 -0
- package/dist/adapt/herdr/authority.js.map +1 -0
- package/dist/adapt/herdr/bridge.d.ts +84 -0
- package/dist/adapt/herdr/bridge.d.ts.map +1 -0
- package/dist/adapt/herdr/bridge.js +222 -0
- package/dist/adapt/herdr/bridge.js.map +1 -0
- package/dist/adapt/herdr/sanitize.d.ts +20 -0
- package/dist/adapt/herdr/sanitize.d.ts.map +1 -0
- package/dist/adapt/herdr/sanitize.js +70 -0
- package/dist/adapt/herdr/sanitize.js.map +1 -0
- package/dist/adapt/herdr/semantic.d.ts +67 -0
- package/dist/adapt/herdr/semantic.d.ts.map +1 -0
- package/dist/adapt/herdr/semantic.js +170 -0
- package/dist/adapt/herdr/semantic.js.map +1 -0
- package/dist/adapt/herdr/seq-store.d.ts +32 -0
- package/dist/adapt/herdr/seq-store.d.ts.map +1 -0
- package/dist/adapt/herdr/seq-store.js +126 -0
- package/dist/adapt/herdr/seq-store.js.map +1 -0
- package/dist/adapt/herdr/team-map.d.ts +30 -0
- package/dist/adapt/herdr/team-map.d.ts.map +1 -0
- package/dist/adapt/herdr/team-map.js +37 -0
- package/dist/adapt/herdr/team-map.js.map +1 -0
- package/dist/adapt/herdr/transport.d.ts +115 -0
- package/dist/adapt/herdr/transport.d.ts.map +1 -0
- package/dist/adapt/herdr/transport.js +260 -0
- package/dist/adapt/herdr/transport.js.map +1 -0
- package/dist/adapt/herdr/wiring.d.ts +24 -0
- package/dist/adapt/herdr/wiring.d.ts.map +1 -0
- package/dist/adapt/herdr/wiring.js +62 -0
- package/dist/adapt/herdr/wiring.js.map +1 -0
- package/dist/adapt/herdr.d.ts +19 -0
- package/dist/adapt/herdr.d.ts.map +1 -0
- package/dist/adapt/herdr.js +115 -0
- package/dist/adapt/herdr.js.map +1 -0
- package/dist/adapt/index.d.ts +4 -0
- package/dist/adapt/index.d.ts.map +1 -1
- package/dist/adapt/index.js +48 -0
- package/dist/adapt/index.js.map +1 -1
- package/dist/adapt/registry.d.ts.map +1 -1
- package/dist/adapt/registry.js +11 -0
- package/dist/adapt/registry.js.map +1 -1
- package/dist/agents/__tests__/definitions.test.js +0 -27
- package/dist/agents/__tests__/definitions.test.js.map +1 -1
- package/dist/agents/__tests__/native-config.test.js +3 -8
- package/dist/agents/__tests__/native-config.test.js.map +1 -1
- package/dist/agents/definitions.d.ts.map +1 -1
- package/dist/agents/definitions.js +0 -41
- package/dist/agents/definitions.js.map +1 -1
- package/dist/agents/policy.d.ts.map +1 -1
- package/dist/agents/policy.js +0 -1
- package/dist/agents/policy.js.map +1 -1
- package/dist/auth/__tests__/hotswap.test.js +60 -1
- package/dist/auth/__tests__/hotswap.test.js.map +1 -1
- package/dist/auth/__tests__/storage.test.js +33 -1
- package/dist/auth/__tests__/storage.test.js.map +1 -1
- package/dist/auth/hotswap.d.ts +16 -2
- package/dist/auth/hotswap.d.ts.map +1 -1
- package/dist/auth/hotswap.js +22 -7
- package/dist/auth/hotswap.js.map +1 -1
- package/dist/auth/storage.js +1 -1
- package/dist/auth/storage.js.map +1 -1
- package/dist/autopilot/__tests__/completion-gate-advisory.test.d.ts +2 -0
- package/dist/autopilot/__tests__/completion-gate-advisory.test.d.ts.map +1 -0
- package/dist/autopilot/__tests__/completion-gate-advisory.test.js +383 -0
- package/dist/autopilot/__tests__/completion-gate-advisory.test.js.map +1 -0
- package/dist/autopilot/completion-gate.d.ts +6 -1
- package/dist/autopilot/completion-gate.d.ts.map +1 -1
- package/dist/autopilot/completion-gate.js +247 -7
- package/dist/autopilot/completion-gate.js.map +1 -1
- package/dist/catalog/__tests__/generator.test.js +15 -10
- package/dist/catalog/__tests__/generator.test.js.map +1 -1
- package/dist/catalog/__tests__/plugin-bundle-ssot.test.js +13 -7
- package/dist/catalog/__tests__/plugin-bundle-ssot.test.js.map +1 -1
- package/dist/catalog/__tests__/schema.test.js +3 -5
- package/dist/catalog/__tests__/schema.test.js.map +1 -1
- package/dist/catalog/schema.js +1 -1
- package/dist/catalog/schema.js.map +1 -1
- package/dist/cli/__tests__/agents-init.test.js +4 -23
- package/dist/cli/__tests__/agents-init.test.js.map +1 -1
- package/dist/cli/__tests__/api.test.js +61 -24
- package/dist/cli/__tests__/api.test.js.map +1 -1
- package/dist/cli/__tests__/auth.test.js +3 -1
- package/dist/cli/__tests__/auth.test.js.map +1 -1
- package/dist/cli/__tests__/autopilot.test.d.ts +2 -0
- package/dist/cli/__tests__/autopilot.test.d.ts.map +1 -0
- package/dist/cli/__tests__/autopilot.test.js +143 -0
- package/dist/cli/__tests__/autopilot.test.js.map +1 -0
- 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 +76 -4
- 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 +206 -0
- package/dist/cli/__tests__/doctor-state-root-binding.test.js.map +1 -0
- package/dist/cli/__tests__/doctor-team.test.js +85 -0
- package/dist/cli/__tests__/doctor-team.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-warning-copy.test.js +38 -6
- package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
- package/dist/cli/__tests__/exec.test.js +75 -0
- package/dist/cli/__tests__/exec.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +621 -460
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/launch-fallback.test.js +1125 -143
- package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
- package/dist/cli/__tests__/mcp-parity.test.js +4 -3
- package/dist/cli/__tests__/mcp-parity.test.js.map +1 -1
- package/dist/cli/__tests__/native-assets.test.js +398 -16
- package/dist/cli/__tests__/native-assets.test.js.map +1 -1
- package/dist/cli/__tests__/native-hook-claim-journal.test.js +53 -5
- package/dist/cli/__tests__/native-hook-claim-journal.test.js.map +1 -1
- package/dist/cli/__tests__/package-bin-contract.test.js +5 -8
- package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
- package/dist/cli/__tests__/ralph-deslop-contract.test.js +4 -18
- package/dist/cli/__tests__/ralph-deslop-contract.test.js.map +1 -1
- package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +4 -30
- package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -1
- package/dist/cli/__tests__/ralph-prd-deep-interview.test.js +4 -15
- package/dist/cli/__tests__/ralph-prd-deep-interview.test.js.map +1 -1
- package/dist/cli/__tests__/ralph.test.js +2 -2
- package/dist/cli/__tests__/ralph.test.js.map +1 -1
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js +57 -57
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js.map +1 -1
- package/dist/cli/__tests__/ralplan-child-binding-3212.test.d.ts +2 -0
- package/dist/cli/__tests__/ralplan-child-binding-3212.test.d.ts.map +1 -0
- package/dist/cli/__tests__/ralplan-child-binding-3212.test.js +56 -0
- package/dist/cli/__tests__/ralplan-child-binding-3212.test.js.map +1 -0
- package/dist/cli/__tests__/ralplan.test.js +131 -14
- package/dist/cli/__tests__/ralplan.test.js.map +1 -1
- package/dist/cli/__tests__/session-scoped-runtime.test.js +1153 -43
- package/dist/cli/__tests__/session-scoped-runtime.test.js.map +1 -1
- package/dist/cli/__tests__/session-search-help.test.js +65 -3
- package/dist/cli/__tests__/session-search-help.test.js.map +1 -1
- package/dist/cli/__tests__/setup-agents-overwrite.test.js +57 -55
- package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
- package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js +34 -0
- package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js.map +1 -1
- package/dist/cli/__tests__/setup-hooks-trust-e2e.test.js +1 -1
- package/dist/cli/__tests__/setup-hooks-trust-e2e.test.js.map +1 -1
- package/dist/cli/__tests__/setup-install-mode.test.js +258 -26
- package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
- package/dist/cli/__tests__/setup-prompts-overwrite.test.js +1 -1
- package/dist/cli/__tests__/setup-prompts-overwrite.test.js.map +1 -1
- package/dist/cli/__tests__/setup-refresh.test.js +3 -6
- package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
- package/dist/cli/__tests__/setup-skills-overwrite.test.js +180 -21
- package/dist/cli/__tests__/setup-skills-overwrite.test.js.map +1 -1
- package/dist/cli/__tests__/sparkshell-cli.test.js +93 -41
- package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
- package/dist/cli/__tests__/team.test.js +45 -1
- package/dist/cli/__tests__/team.test.js.map +1 -1
- package/dist/cli/__tests__/ultragoal.test.js +161 -2
- package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
- package/dist/cli/__tests__/uninstall.test.js +73 -28
- package/dist/cli/__tests__/uninstall.test.js.map +1 -1
- package/dist/cli/__tests__/update.test.js +749 -177
- package/dist/cli/__tests__/update.test.js.map +1 -1
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js +32 -2
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
- package/dist/cli/api.d.ts.map +1 -1
- package/dist/cli/api.js +9 -10
- package/dist/cli/api.js.map +1 -1
- package/dist/cli/autopilot.d.ts +8 -0
- package/dist/cli/autopilot.d.ts.map +1 -0
- package/dist/cli/autopilot.js +160 -0
- package/dist/cli/autopilot.js.map +1 -0
- 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 +29 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +433 -19
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/index.d.ts +231 -12
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +2354 -724
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp-parity.d.ts.map +1 -1
- package/dist/cli/mcp-parity.js +7 -3
- package/dist/cli/mcp-parity.js.map +1 -1
- package/dist/cli/native-assets.d.ts +26 -1
- package/dist/cli/native-assets.d.ts.map +1 -1
- package/dist/cli/native-assets.js +448 -51
- package/dist/cli/native-assets.js.map +1 -1
- package/dist/cli/native-hook-claim-journal.d.ts +8 -4
- package/dist/cli/native-hook-claim-journal.d.ts.map +1 -1
- package/dist/cli/native-hook-claim-journal.js +30 -8
- package/dist/cli/native-hook-claim-journal.js.map +1 -1
- package/dist/cli/package-manager-ownership.d.ts +69 -0
- package/dist/cli/package-manager-ownership.d.ts.map +1 -0
- package/dist/cli/package-manager-ownership.js +338 -0
- package/dist/cli/package-manager-ownership.js.map +1 -0
- package/dist/cli/plugin-marketplace.d.ts +1 -0
- package/dist/cli/plugin-marketplace.d.ts.map +1 -1
- package/dist/cli/plugin-marketplace.js +123 -55
- package/dist/cli/plugin-marketplace.js.map +1 -1
- package/dist/cli/ralph.js +2 -2
- package/dist/cli/ralph.js.map +1 -1
- package/dist/cli/ralplan.d.ts +6 -2
- package/dist/cli/ralplan.d.ts.map +1 -1
- package/dist/cli/ralplan.js +134 -34
- package/dist/cli/ralplan.js.map +1 -1
- package/dist/cli/session-search.d.ts.map +1 -1
- package/dist/cli/session-search.js +38 -1
- 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 +6 -0
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +425 -215
- 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 +1 -1
- package/dist/cli/ultragoal.d.ts.map +1 -1
- package/dist/cli/ultragoal.js +10 -7
- package/dist/cli/ultragoal.js.map +1 -1
- package/dist/cli/uninstall.d.ts.map +1 -1
- package/dist/cli/uninstall.js +27 -28
- package/dist/cli/uninstall.js.map +1 -1
- package/dist/cli/update-worker.d.ts +2 -0
- package/dist/cli/update-worker.d.ts.map +1 -0
- package/dist/cli/update-worker.js +142 -0
- package/dist/cli/update-worker.js.map +1 -0
- package/dist/cli/update.d.ts +16 -23
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +212 -241
- package/dist/cli/update.js.map +1 -1
- package/dist/compat/__tests__/upgrade-from-0-20.test.d.ts +2 -0
- package/dist/compat/__tests__/upgrade-from-0-20.test.d.ts.map +1 -0
- package/dist/compat/__tests__/upgrade-from-0-20.test.js +109 -0
- package/dist/compat/__tests__/upgrade-from-0-20.test.js.map +1 -0
- package/dist/compat/upgrade-from-0-20.d.ts +64 -0
- package/dist/compat/upgrade-from-0-20.d.ts.map +1 -0
- package/dist/compat/upgrade-from-0-20.js +276 -0
- package/dist/compat/upgrade-from-0-20.js.map +1 -0
- package/dist/config/__tests__/generator-idempotent.test.js +173 -29
- package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
- package/dist/config/__tests__/generator-notify.test.js +3 -1
- package/dist/config/__tests__/generator-notify.test.js.map +1 -1
- package/dist/config/generator.d.ts +25 -3
- package/dist/config/generator.d.ts.map +1 -1
- package/dist/config/generator.js +113 -35
- package/dist/config/generator.js.map +1 -1
- package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js +25 -0
- package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js.map +1 -1
- package/dist/goal-workflows/codex-goal-snapshot.d.ts +1 -1
- package/dist/goal-workflows/codex-goal-snapshot.d.ts.map +1 -1
- package/dist/goal-workflows/codex-goal-snapshot.js +2 -0
- package/dist/goal-workflows/codex-goal-snapshot.js.map +1 -1
- package/dist/hooks/__tests__/agents-overlay.test.js +3 -1
- package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
- package/dist/hooks/__tests__/analyze-skill-contract.test.js +24 -31
- package/dist/hooks/__tests__/analyze-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/anti-slop-workflow.test.js +66 -135
- package/dist/hooks/__tests__/anti-slop-workflow.test.js.map +1 -1
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js +47 -106
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/code-review-skill-contract.test.js +16 -18
- package/dist/hooks/__tests__/code-review-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/consensus-execution-handoff.test.d.ts +2 -14
- package/dist/hooks/__tests__/consensus-execution-handoff.test.d.ts.map +1 -1
- package/dist/hooks/__tests__/consensus-execution-handoff.test.js +56 -234
- package/dist/hooks/__tests__/consensus-execution-handoff.test.js.map +1 -1
- package/dist/hooks/__tests__/deep-interview-contract.test.js +1 -3
- package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/design-skill.test.js +18 -16
- package/dist/hooks/__tests__/design-skill.test.js.map +1 -1
- package/dist/hooks/__tests__/explore-sparkshell-guidance-contract.test.js +24 -20
- package/dist/hooks/__tests__/explore-sparkshell-guidance-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 +164 -0
- package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.js.map +1 -0
- package/dist/hooks/__tests__/issue-3497-hook-simplification.test.d.ts +2 -0
- package/dist/hooks/__tests__/issue-3497-hook-simplification.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/issue-3497-hook-simplification.test.js +169 -0
- package/dist/hooks/__tests__/issue-3497-hook-simplification.test.js.map +1 -0
- package/dist/hooks/__tests__/keyword-detector.test.js +226 -858
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js +176 -14
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +1 -0
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +33 -21
- 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-ralph-resume.test.js +14 -904
- package/dist/hooks/__tests__/notify-hook-ralph-resume.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-session-scope.test.js +5 -5
- package/dist/hooks/__tests__/notify-hook-session-scope.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +7 -7
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +11 -8
- 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 +7 -6
- 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-worker-idle.test.js +1 -0
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/pre-context-gate-skills.test.js +19 -21
- package/dist/hooks/__tests__/pre-context-gate-skills.test.js.map +1 -1
- package/dist/hooks/__tests__/prompt-guidance-contract.test.js +23 -3
- package/dist/hooks/__tests__/prompt-guidance-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js +23 -28
- package/dist/hooks/__tests__/prompt-guidance-wave-two.test.js.map +1 -1
- package/dist/hooks/__tests__/research-workflow-boundaries.test.js +9 -8
- package/dist/hooks/__tests__/research-workflow-boundaries.test.js.map +1 -1
- package/dist/hooks/__tests__/session.test.js +3656 -656
- package/dist/hooks/__tests__/session.test.js.map +1 -1
- package/dist/hooks/__tests__/skill-catalog-hygiene.test.js +13 -22
- package/dist/hooks/__tests__/skill-catalog-hygiene.test.js.map +1 -1
- package/dist/hooks/__tests__/skill-guidance-contract.test.js +3 -13
- package/dist/hooks/__tests__/skill-guidance-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/sunset-routing-contract.test.d.ts +2 -0
- package/dist/hooks/__tests__/sunset-routing-contract.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/sunset-routing-contract.test.js +172 -0
- package/dist/hooks/__tests__/sunset-routing-contract.test.js.map +1 -0
- package/dist/hooks/__tests__/team-runtime-gating-docs-contract.test.js +2 -2
- package/dist/hooks/__tests__/team-runtime-gating-docs-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/tmux-hook-engine.test.js +18 -3
- package/dist/hooks/__tests__/tmux-hook-engine.test.js.map +1 -1
- package/dist/hooks/__tests__/visual-ralph-skill.test.js +20 -17
- package/dist/hooks/__tests__/visual-ralph-skill.test.js.map +1 -1
- package/dist/hooks/__tests__/visual-verdict-loop.test.js +17 -18
- package/dist/hooks/__tests__/visual-verdict-loop.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 +6 -47
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +156 -268
- package/dist/hooks/keyword-detector.js.map +1 -1
- package/dist/hooks/keyword-registry.d.ts.map +1 -1
- package/dist/hooks/keyword-registry.js +14 -26
- package/dist/hooks/keyword-registry.js.map +1 -1
- package/dist/hooks/native/capability-warnings.d.ts +16 -0
- package/dist/hooks/native/capability-warnings.d.ts.map +1 -0
- package/dist/hooks/native/capability-warnings.js +40 -0
- package/dist/hooks/native/capability-warnings.js.map +1 -0
- package/dist/hooks/native/index.d.ts +17 -0
- package/dist/hooks/native/index.d.ts.map +1 -0
- package/dist/hooks/native/index.js +17 -0
- package/dist/hooks/native/index.js.map +1 -0
- package/dist/hooks/native/pre-tool-use-advisory.d.ts +17 -0
- package/dist/hooks/native/pre-tool-use-advisory.d.ts.map +1 -0
- package/dist/hooks/native/pre-tool-use-advisory.js +112 -0
- package/dist/hooks/native/pre-tool-use-advisory.js.map +1 -0
- package/dist/hooks/native/types.d.ts +30 -0
- package/dist/hooks/native/types.d.ts.map +1 -0
- package/dist/hooks/native/types.js +22 -0
- package/dist/hooks/native/types.js.map +1 -0
- package/dist/hooks/prompt-guidance-contract.d.ts.map +1 -1
- package/dist/hooks/prompt-guidance-contract.js +46 -93
- package/dist/hooks/prompt-guidance-contract.js.map +1 -1
- package/dist/hooks/session.d.ts +197 -18
- package/dist/hooks/session.d.ts.map +1 -1
- package/dist/hooks/session.js +1939 -271
- package/dist/hooks/session.js.map +1 -1
- package/dist/hooks/sunset-stub.d.ts +17 -0
- package/dist/hooks/sunset-stub.d.ts.map +1 -0
- package/dist/hooks/sunset-stub.js +125 -0
- package/dist/hooks/sunset-stub.js.map +1 -0
- 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-split-realtmux.test.d.ts +2 -0
- package/dist/hud/__tests__/tmux-split-realtmux.test.d.ts.map +1 -0
- package/dist/hud/__tests__/tmux-split-realtmux.test.js +112 -0
- package/dist/hud/__tests__/tmux-split-realtmux.test.js.map +1 -0
- package/dist/hud/__tests__/tmux.test.js +217 -198
- package/dist/hud/__tests__/tmux.test.js.map +1 -1
- package/dist/hud/tmux.d.ts +43 -4
- package/dist/hud/tmux.d.ts.map +1 -1
- package/dist/hud/tmux.js +1025 -139
- package/dist/hud/tmux.js.map +1 -1
- package/dist/leader/__tests__/contract.test.js +71 -34
- package/dist/leader/__tests__/contract.test.js.map +1 -1
- package/dist/leader/contract.d.ts +1 -6
- package/dist/leader/contract.d.ts.map +1 -1
- package/dist/leader/contract.js +25 -24
- package/dist/leader/contract.js.map +1 -1
- package/dist/mcp/__tests__/hermes-bridge.test.js +17 -0
- package/dist/mcp/__tests__/hermes-bridge.test.js.map +1 -1
- package/dist/mcp/__tests__/server-lifecycle.test.js +4 -4
- package/dist/mcp/__tests__/state-paths.test.js +735 -4
- package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
- package/dist/mcp/__tests__/state-server-ralph-phase.test.js +29 -4
- package/dist/mcp/__tests__/state-server-ralph-phase.test.js.map +1 -1
- package/dist/mcp/__tests__/state-server-schema.test.js +13 -3
- package/dist/mcp/__tests__/state-server-schema.test.js.map +1 -1
- package/dist/mcp/__tests__/state-server.test.js +11 -1033
- package/dist/mcp/__tests__/state-server.test.js.map +1 -1
- package/dist/mcp/__tests__/trace-server.test.js +2 -2
- package/dist/mcp/__tests__/trace-server.test.js.map +1 -1
- package/dist/mcp/hermes-bridge.d.ts.map +1 -1
- package/dist/mcp/hermes-bridge.js +2 -2
- package/dist/mcp/hermes-bridge.js.map +1 -1
- package/dist/mcp/state-paths.d.ts +78 -4
- package/dist/mcp/state-paths.d.ts.map +1 -1
- package/dist/mcp/state-paths.js +543 -40
- package/dist/mcp/state-paths.js.map +1 -1
- package/dist/mcp/state-server.d.ts +68 -97
- package/dist/mcp/state-server.d.ts.map +1 -1
- package/dist/mcp/state-server.js +116 -67
- package/dist/mcp/state-server.js.map +1 -1
- package/dist/modes/__tests__/base-autopilot-gates.test.js +1 -168
- package/dist/modes/__tests__/base-autopilot-gates.test.js.map +1 -1
- package/dist/modes/__tests__/base-autoresearch-contract.test.js +1 -85
- package/dist/modes/__tests__/base-autoresearch-contract.test.js.map +1 -1
- package/dist/modes/__tests__/base-multi-state-compat.test.js +1 -35
- package/dist/modes/__tests__/base-multi-state-compat.test.js.map +1 -1
- package/dist/modes/__tests__/base-session-scope.test.js +10 -158
- package/dist/modes/__tests__/base-session-scope.test.js.map +1 -1
- package/dist/modes/base.d.ts +6 -6
- package/dist/modes/base.d.ts.map +1 -1
- package/dist/modes/base.js +116 -90
- 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/__tests__/formatter.test.js +8 -0
- package/dist/notifications/__tests__/formatter.test.js.map +1 -1
- package/dist/notifications/__tests__/template-engine.test.js +8 -0
- package/dist/notifications/__tests__/template-engine.test.js.map +1 -1
- package/dist/notifications/formatter.js +1 -1
- package/dist/notifications/formatter.js.map +1 -1
- package/dist/notifications/template-engine.js +1 -1
- package/dist/notifications/template-engine.js.map +1 -1
- package/dist/planning/__tests__/approved-execution-lifecycle-matrix.test.js +0 -21
- package/dist/planning/__tests__/approved-execution-lifecycle-matrix.test.js.map +1 -1
- package/dist/question/autopilot-wait.d.ts.map +1 -1
- package/dist/question/autopilot-wait.js +2 -2
- package/dist/question/autopilot-wait.js.map +1 -1
- package/dist/question/deep-interview.d.ts +1 -2
- package/dist/question/deep-interview.d.ts.map +1 -1
- package/dist/question/deep-interview.js +4 -4
- package/dist/question/deep-interview.js.map +1 -1
- package/dist/ralplan/__tests__/documented-leader-preflight.test.js +328 -39
- package/dist/ralplan/__tests__/documented-leader-preflight.test.js.map +1 -1
- package/dist/ralplan/__tests__/runtime.test.js +56 -821
- package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
- package/dist/ralplan/documented-leader-preflight.d.ts +15 -8
- package/dist/ralplan/documented-leader-preflight.d.ts.map +1 -1
- package/dist/ralplan/documented-leader-preflight.js +356 -54
- package/dist/ralplan/documented-leader-preflight.js.map +1 -1
- package/dist/ralplan/runtime.d.ts +2 -2
- package/dist/ralplan/runtime.d.ts.map +1 -1
- package/dist/ralplan/runtime.js +145 -163
- package/dist/ralplan/runtime.js.map +1 -1
- package/dist/runtime/bridge.d.ts.map +1 -1
- package/dist/runtime/bridge.js +45 -0
- 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 +12490 -19414
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/__tests__/docs-site-contract.test.js +2 -2
- package/dist/scripts/__tests__/docs-site-contract.test.js.map +1 -1
- package/dist/scripts/__tests__/generated-artifact-drift.test.d.ts +2 -0
- package/dist/scripts/__tests__/generated-artifact-drift.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/generated-artifact-drift.test.js +84 -0
- package/dist/scripts/__tests__/generated-artifact-drift.test.js.map +1 -0
- 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 +235 -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 +207 -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 +156 -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 +191 -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 +40 -0
- package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.js.map +1 -0
- package/dist/scripts/__tests__/issue-3481-conductor-deadlock.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3481-conductor-deadlock.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3481-conductor-deadlock.test.js +63 -0
- package/dist/scripts/__tests__/issue-3481-conductor-deadlock.test.js.map +1 -0
- package/dist/scripts/__tests__/issue-3486-pretooluse-lock-regression.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3486-pretooluse-lock-regression.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3486-pretooluse-lock-regression.test.js +65 -0
- package/dist/scripts/__tests__/issue-3486-pretooluse-lock-regression.test.js.map +1 -0
- package/dist/scripts/__tests__/issue-3536-external-team-state-root.test.d.ts +2 -0
- package/dist/scripts/__tests__/issue-3536-external-team-state-root.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/issue-3536-external-team-state-root.test.js +189 -0
- package/dist/scripts/__tests__/issue-3536-external-team-state-root.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__/prompt-inventory.test.js +54 -1
- package/dist/scripts/__tests__/prompt-inventory.test.js.map +1 -1
- package/dist/scripts/__tests__/reasoning-artifact-contract.test.js +2 -2
- package/dist/scripts/__tests__/reasoning-artifact-contract.test.js.map +1 -1
- package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js +46 -60
- package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js.map +1 -1
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js +60 -25
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js.map +1 -1
- package/dist/scripts/__tests__/smoke-packed-install.test.js +168 -20
- 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 +51 -2
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +3648 -1332
- 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 +33 -22
- package/dist/scripts/codex-native-pre-post.js.map +1 -1
- package/dist/scripts/generate-native-release-manifest.js +80 -51
- package/dist/scripts/generate-native-release-manifest.js.map +1 -1
- package/dist/scripts/notify-fallback-watcher.js +28 -39
- package/dist/scripts/notify-fallback-watcher.js.map +1 -1
- package/dist/scripts/notify-hook/auto-nudge.d.ts +2 -48
- package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
- package/dist/scripts/notify-hook/auto-nudge.js +1 -0
- 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.map +1 -1
- package/dist/scripts/notify-hook/team-dispatch.js +8 -5
- 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 +32 -3
- package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.d.ts +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.js +3 -2
- 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 +23 -3
- package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
- package/dist/scripts/notify-hook/team-worker.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-worker.js +45 -5
- package/dist/scripts/notify-hook/team-worker.js.map +1 -1
- package/dist/scripts/notify-hook/tmux-injection.d.ts +1 -0
- package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
- package/dist/scripts/notify-hook/tmux-injection.js +4 -1
- package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
- package/dist/scripts/notify-hook.d.ts.map +1 -1
- package/dist/scripts/notify-hook.js +71 -124
- 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/prompt-inventory.d.ts +39 -0
- package/dist/scripts/prompt-inventory.d.ts.map +1 -1
- package/dist/scripts/prompt-inventory.js +108 -3
- package/dist/scripts/prompt-inventory.js.map +1 -1
- package/dist/scripts/run-compiled-ci.js +2 -0
- package/dist/scripts/run-compiled-ci.js.map +1 -1
- package/dist/scripts/run-test-files.js +1 -0
- package/dist/scripts/run-test-files.js.map +1 -1
- package/dist/scripts/smoke-packed-install.d.ts +32 -1
- package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
- package/dist/scripts/smoke-packed-install.js +407 -60
- package/dist/scripts/smoke-packed-install.js.map +1 -1
- package/dist/scripts/sync-plugin-mirror.d.ts.map +1 -1
- package/dist/scripts/sync-plugin-mirror.js +16 -1
- package/dist/scripts/sync-plugin-mirror.js.map +1 -1
- package/dist/scripts/sync-prompt-guidance-fragments.js +62 -20
- package/dist/scripts/sync-prompt-guidance-fragments.js.map +1 -1
- package/dist/scripts/tmux-hook-engine.d.ts +1 -1
- package/dist/scripts/tmux-hook-engine.d.ts.map +1 -1
- package/dist/scripts/tmux-hook-engine.js +3 -3
- package/dist/scripts/tmux-hook-engine.js.map +1 -1
- package/dist/scripts/verify-capabilities-lock.d.ts +3 -0
- package/dist/scripts/verify-capabilities-lock.d.ts.map +1 -0
- package/dist/scripts/verify-capabilities-lock.js +22 -0
- package/dist/scripts/verify-capabilities-lock.js.map +1 -0
- 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__/authority-decreasing.test.d.ts +2 -0
- package/dist/state/__tests__/authority-decreasing.test.d.ts.map +1 -0
- package/dist/state/__tests__/authority-decreasing.test.js +78 -0
- package/dist/state/__tests__/authority-decreasing.test.js.map +1 -0
- package/dist/state/__tests__/handoff-carrier.test.d.ts +2 -0
- package/dist/state/__tests__/handoff-carrier.test.d.ts.map +1 -0
- package/dist/state/__tests__/handoff-carrier.test.js +96 -0
- package/dist/state/__tests__/handoff-carrier.test.js.map +1 -0
- package/dist/state/__tests__/mode-state-context.test.js +12 -5
- package/dist/state/__tests__/mode-state-context.test.js.map +1 -1
- package/dist/state/__tests__/operations.test.js +870 -2517
- package/dist/state/__tests__/operations.test.js.map +1 -1
- package/dist/state/__tests__/single-writer-invariant.test.d.ts +2 -0
- package/dist/state/__tests__/single-writer-invariant.test.d.ts.map +1 -0
- package/dist/state/__tests__/single-writer-invariant.test.js +342 -0
- package/dist/state/__tests__/single-writer-invariant.test.js.map +1 -0
- package/dist/state/__tests__/skill-active.test.js +400 -2
- package/dist/state/__tests__/skill-active.test.js.map +1 -1
- package/dist/state/__tests__/stale-state-neutralization.test.d.ts +2 -0
- package/dist/state/__tests__/stale-state-neutralization.test.d.ts.map +1 -0
- package/dist/state/__tests__/stale-state-neutralization.test.js +84 -0
- package/dist/state/__tests__/stale-state-neutralization.test.js.map +1 -0
- package/dist/state/__tests__/workflow-transition.test.js +20 -33
- package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
- package/dist/state/handoff-carrier.d.ts +46 -0
- package/dist/state/handoff-carrier.d.ts.map +1 -0
- package/dist/state/handoff-carrier.js +85 -0
- package/dist/state/handoff-carrier.js.map +1 -0
- package/dist/state/mode-state-context.d.ts +3 -0
- package/dist/state/mode-state-context.d.ts.map +1 -1
- package/dist/state/mode-state-context.js +27 -13
- package/dist/state/mode-state-context.js.map +1 -1
- package/dist/state/namespace-owners.d.ts +34 -0
- package/dist/state/namespace-owners.d.ts.map +1 -0
- package/dist/state/namespace-owners.js +127 -0
- package/dist/state/namespace-owners.js.map +1 -0
- package/dist/state/operations.d.ts +9 -4
- package/dist/state/operations.d.ts.map +1 -1
- package/dist/state/operations.js +194 -235
- 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 +421 -31
- 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 +11 -31
- package/dist/state/workflow-transition-reconcile.js.map +1 -1
- package/dist/state/workflow-transition.d.ts +1 -27
- package/dist/state/workflow-transition.d.ts.map +1 -1
- package/dist/state/workflow-transition.js +9 -790
- package/dist/state/workflow-transition.js.map +1 -1
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.js +27 -0
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.js.map +1 -1
- package/dist/subagents/__tests__/tracker.test.js +837 -1048
- package/dist/subagents/__tests__/tracker.test.js.map +1 -1
- package/dist/subagents/tracker.d.ts +64 -60
- package/dist/subagents/tracker.d.ts.map +1 -1
- package/dist/subagents/tracker.js +809 -358
- package/dist/subagents/tracker.js.map +1 -1
- package/dist/team/__tests__/api-interop.test.js +1 -0
- package/dist/team/__tests__/api-interop.test.js.map +1 -1
- package/dist/team/__tests__/delivery-e2e-smoke.test.js +6 -7
- package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
- package/dist/team/__tests__/mcp-comm.test.js +104 -3
- package/dist/team/__tests__/mcp-comm.test.js.map +1 -1
- package/dist/team/__tests__/notice-ledger.test.d.ts +2 -0
- package/dist/team/__tests__/notice-ledger.test.d.ts.map +1 -0
- package/dist/team/__tests__/notice-ledger.test.js +131 -0
- package/dist/team/__tests__/notice-ledger.test.js.map +1 -0
- package/dist/team/__tests__/pane-teardown-residue-e2e.test.d.ts +2 -0
- package/dist/team/__tests__/pane-teardown-residue-e2e.test.d.ts.map +1 -0
- package/dist/team/__tests__/pane-teardown-residue-e2e.test.js +410 -0
- package/dist/team/__tests__/pane-teardown-residue-e2e.test.js.map +1 -0
- package/dist/team/__tests__/phase-controller.test.js +20 -0
- package/dist/team/__tests__/phase-controller.test.js.map +1 -1
- package/dist/team/__tests__/runtime.test.js +725 -59
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/scaling.test.js +269 -245
- 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__/team-ops-contract.test.js +1 -0
- package/dist/team/__tests__/team-ops-contract.test.js.map +1 -1
- package/dist/team/__tests__/tmux-session.test.js +1093 -101
- 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 +159 -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-bootstrap.test.js +11 -13
- package/dist/team/__tests__/worker-bootstrap.test.js.map +1 -1
- package/dist/team/__tests__/worker-provenance.test.d.ts +2 -0
- package/dist/team/__tests__/worker-provenance.test.d.ts.map +1 -0
- package/dist/team/__tests__/worker-provenance.test.js +251 -0
- package/dist/team/__tests__/worker-provenance.test.js.map +1 -0
- package/dist/team/__tests__/worker-runtime-identity.test.js +45 -72
- package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
- package/dist/team/api-interop.d.ts.map +1 -1
- package/dist/team/api-interop.js +14 -8
- package/dist/team/api-interop.js.map +1 -1
- package/dist/team/mcp-comm.d.ts.map +1 -1
- package/dist/team/mcp-comm.js +3 -8
- package/dist/team/mcp-comm.js.map +1 -1
- package/dist/team/notice-ledger.d.ts +54 -0
- package/dist/team/notice-ledger.d.ts.map +1 -0
- package/dist/team/notice-ledger.js +259 -0
- package/dist/team/notice-ledger.js.map +1 -0
- package/dist/team/phase-controller.d.ts.map +1 -1
- package/dist/team/phase-controller.js +2 -0
- package/dist/team/phase-controller.js.map +1 -1
- package/dist/team/runtime.d.ts +3 -1
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +314 -142
- package/dist/team/runtime.js.map +1 -1
- package/dist/team/scaling.d.ts.map +1 -1
- package/dist/team/scaling.js +161 -91
- package/dist/team/scaling.js.map +1 -1
- package/dist/team/state/monitor.d.ts +10 -0
- package/dist/team/state/monitor.d.ts.map +1 -1
- package/dist/team/state/monitor.js +13 -0
- package/dist/team/state/monitor.js.map +1 -1
- package/dist/team/state/types.d.ts +15 -0
- package/dist/team/state/types.d.ts.map +1 -1
- package/dist/team/state/types.js.map +1 -1
- package/dist/team/state-root.d.ts +12 -0
- package/dist/team/state-root.d.ts.map +1 -1
- package/dist/team/state-root.js +177 -0
- package/dist/team/state-root.js.map +1 -1
- package/dist/team/state.d.ts +19 -0
- package/dist/team/state.d.ts.map +1 -1
- package/dist/team/state.js +47 -1
- package/dist/team/state.js.map +1 -1
- package/dist/team/team-ops.d.ts +1 -0
- package/dist/team/team-ops.d.ts.map +1 -1
- package/dist/team/team-ops.js +1 -0
- package/dist/team/team-ops.js.map +1 -1
- package/dist/team/tmux-session.d.ts +42 -4
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +572 -196
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/team/ultragoal-context.d.ts +1 -0
- package/dist/team/ultragoal-context.d.ts.map +1 -1
- package/dist/team/ultragoal-context.js +2 -0
- package/dist/team/ultragoal-context.js.map +1 -1
- package/dist/team/worker-provenance.d.ts +35 -0
- package/dist/team/worker-provenance.d.ts.map +1 -0
- package/dist/team/worker-provenance.js +213 -0
- package/dist/team/worker-provenance.js.map +1 -0
- package/dist/ultragoal/__tests__/artifacts.test.js +1053 -2
- package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
- package/dist/ultragoal/__tests__/docs-contract.test.js +89 -59
- package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
- package/dist/ultragoal/artifacts.d.ts +36 -1
- package/dist/ultragoal/artifacts.d.ts.map +1 -1
- package/dist/ultragoal/artifacts.js +267 -51
- package/dist/ultragoal/artifacts.js.map +1 -1
- package/dist/url-reader/__tests__/url-reader.test.js +14 -0
- package/dist/url-reader/__tests__/url-reader.test.js.map +1 -1
- package/dist/url-reader/index.js +5 -3
- package/dist/url-reader/index.js.map +1 -1
- package/dist/utils/__tests__/file-durability.test.js +58 -1
- package/dist/utils/__tests__/file-durability.test.js.map +1 -1
- package/dist/utils/__tests__/paths.test.js +21 -1
- package/dist/utils/__tests__/paths.test.js.map +1 -1
- package/dist/utils/__tests__/version.test.js +17 -0
- package/dist/utils/__tests__/version.test.js.map +1 -1
- package/dist/utils/file-durability.d.ts +10 -0
- package/dist/utils/file-durability.d.ts.map +1 -1
- package/dist/utils/file-durability.js +34 -4
- package/dist/utils/file-durability.js.map +1 -1
- package/dist/utils/paths.d.ts.map +1 -1
- package/dist/utils/paths.js +14 -6
- package/dist/utils/paths.js.map +1 -1
- package/dist/utils/version.d.ts.map +1 -1
- package/dist/utils/version.js +2 -1
- package/dist/utils/version.js.map +1 -1
- package/dist/verification/__tests__/ci-rust-gates.test.js +49 -4
- package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
- package/dist/verification/__tests__/explore-harness-release-workflow.test.js +60 -0
- package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +1 -1
- package/dist/verification/__tests__/native-release-manifest.test.js +157 -4
- package/dist/verification/__tests__/native-release-manifest.test.js.map +1 -1
- package/package.json +17 -6
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +81 -17
- package/plugins/oh-my-codex/skills/ai-slop-cleaner/SKILL.md +60 -135
- package/plugins/oh-my-codex/skills/analyze/SKILL.md +35 -111
- package/plugins/oh-my-codex/skills/autopilot/SKILL.md +72 -163
- package/plugins/oh-my-codex/skills/cancel/SKILL.md +33 -392
- package/plugins/oh-my-codex/skills/code-review/SKILL.md +66 -256
- package/plugins/oh-my-codex/skills/configure-notifications/SKILL.md +30 -216
- package/plugins/oh-my-codex/skills/deep-interview/SKILL.md +11 -10
- package/plugins/oh-my-codex/skills/design/SKILL.md +32 -132
- package/plugins/oh-my-codex/skills/doctor/SKILL.md +56 -177
- package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +35 -116
- package/plugins/oh-my-codex/skills/plan/SKILL.md +45 -228
- package/plugins/oh-my-codex/skills/ralplan/SKILL.md +20 -22
- package/plugins/oh-my-codex/skills/skill/SKILL.md +82 -807
- package/plugins/oh-my-codex/skills/team/SKILL.md +32 -520
- package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +47 -150
- package/plugins/oh-my-codex/skills/ultraqa/SKILL.md +75 -252
- package/plugins/oh-my-codex/skills/visual-ralph/SKILL.md +33 -111
- package/plugins/oh-my-codex/skills/worker/SKILL.md +40 -113
- package/prompts/analyst.md +27 -107
- package/prompts/api-reviewer.md +17 -84
- package/prompts/architect.md +31 -70
- package/prompts/code-reviewer.md +18 -107
- package/prompts/code-simplifier.md +32 -110
- package/prompts/critic.md +32 -54
- package/prompts/dependency-expert.md +37 -107
- package/prompts/designer.md +40 -107
- package/prompts/executor.md +26 -75
- package/prompts/information-architect.md +51 -200
- package/prompts/performance-reviewer.md +17 -81
- package/prompts/planner.md +33 -76
- package/prompts/product-analyst.md +65 -280
- package/prompts/product-manager.md +48 -216
- package/prompts/qa-tester.md +33 -104
- package/prompts/quality-reviewer.md +17 -93
- package/prompts/quality-strategist.md +31 -255
- package/prompts/researcher.md +32 -89
- package/prompts/security-reviewer.md +12 -98
- package/prompts/test-engineer.md +29 -110
- package/prompts/ux-researcher.md +46 -300
- package/prompts/verifier.md +23 -51
- package/skills/ai-slop-cleaner/SKILL.md +60 -135
- package/skills/analyze/SKILL.md +35 -111
- package/skills/autopilot/SKILL.md +72 -163
- package/skills/autoresearch-goal/SKILL.md +6 -31
- package/skills/cancel/SKILL.md +33 -392
- package/skills/code-review/SKILL.md +66 -256
- package/skills/configure-notifications/SKILL.md +30 -216
- package/skills/deep-interview/SKILL.md +11 -10
- package/skills/design/SKILL.md +32 -132
- package/skills/doctor/SKILL.md +56 -177
- package/skills/omx-setup/SKILL.md +35 -116
- package/skills/pipeline/SKILL.md +6 -89
- package/skills/plan/SKILL.md +45 -228
- package/skills/ralph/SKILL.md +6 -290
- package/skills/ralplan/SKILL.md +20 -22
- package/skills/skill/SKILL.md +82 -807
- package/skills/team/SKILL.md +32 -520
- package/skills/ultragoal/SKILL.md +47 -150
- package/skills/ultraqa/SKILL.md +75 -252
- package/skills/ultrawork/SKILL.md +6 -182
- package/skills/visual-ralph/SKILL.md +33 -111
- package/skills/worker/SKILL.md +40 -113
- package/src/scripts/__tests__/codex-native-hook.test.ts +18888 -28799
- package/src/scripts/__tests__/docs-site-contract.test.ts +2 -2
- package/src/scripts/__tests__/generated-artifact-drift.test.ts +97 -0
- package/src/scripts/__tests__/issue-3293-callsite-parity.test.ts +197 -0
- package/src/scripts/__tests__/issue-3293-cancel-transaction.test.ts +189 -0
- package/src/scripts/__tests__/issue-3293-hook-owned-cancel.test.ts +186 -0
- package/src/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.ts +177 -0
- package/src/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.ts +201 -0
- package/src/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.ts +43 -0
- package/src/scripts/__tests__/issue-3481-conductor-deadlock.test.ts +77 -0
- package/src/scripts/__tests__/issue-3486-pretooluse-lock-regression.test.ts +95 -0
- package/src/scripts/__tests__/issue-3536-external-team-state-root.test.ts +216 -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__/prompt-inventory.test.ts +62 -1
- package/src/scripts/__tests__/reasoning-artifact-contract.test.ts +1 -2
- package/src/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.ts +49 -59
- package/src/scripts/__tests__/role-intent-durable-recovery-3181.test.ts +61 -27
- package/src/scripts/__tests__/smoke-packed-install.test.ts +223 -23
- package/src/scripts/__tests__/verify-native-release-assets.test.ts +240 -0
- package/src/scripts/codex-native-hook.ts +3618 -1383
- package/src/scripts/codex-native-pre-post.ts +50 -24
- package/src/scripts/generate-native-release-manifest.ts +83 -79
- package/src/scripts/notify-fallback-watcher.ts +38 -38
- package/src/scripts/notify-hook/auto-nudge.ts +1 -0
- package/src/scripts/notify-hook/state-io.ts +11 -0
- package/src/scripts/notify-hook/team-dispatch.ts +8 -5
- package/src/scripts/notify-hook/team-leader-nudge.ts +32 -4
- package/src/scripts/notify-hook/team-tmux-guard.ts +3 -1
- package/src/scripts/notify-hook/team-worker-stop.ts +23 -4
- package/src/scripts/notify-hook/team-worker.ts +43 -5
- package/src/scripts/notify-hook/tmux-injection.ts +5 -1
- package/src/scripts/notify-hook.ts +76 -124
- package/src/scripts/postinstall-advisory.ts +67 -0
- package/src/scripts/postinstall.ts +1 -1
- package/src/scripts/prompt-inventory.ts +142 -3
- package/src/scripts/run-compiled-ci.ts +2 -0
- package/src/scripts/run-test-files.ts +1 -0
- package/src/scripts/smoke-packed-install.ts +440 -59
- package/src/scripts/sync-plugin-mirror.ts +17 -1
- package/src/scripts/sync-prompt-guidance-fragments.ts +59 -22
- package/src/scripts/tmux-hook-engine.ts +3 -3
- package/src/scripts/verify-capabilities-lock.ts +23 -0
- package/src/scripts/verify-native-release-assets.ts +39 -31
- package/templates/AGENTS.md +45 -9
- package/templates/catalog-manifest.json +6 -146
- package/dist/autopilot/__tests__/deep-interview-gate.test.d.ts +0 -2
- package/dist/autopilot/__tests__/deep-interview-gate.test.d.ts.map +0 -1
- package/dist/autopilot/__tests__/deep-interview-gate.test.js +0 -215
- package/dist/autopilot/__tests__/deep-interview-gate.test.js.map +0 -1
- package/dist/autopilot/__tests__/ralplan-gate.test.d.ts +0 -2
- package/dist/autopilot/__tests__/ralplan-gate.test.d.ts.map +0 -1
- package/dist/autopilot/__tests__/ralplan-gate.test.js +0 -1361
- package/dist/autopilot/__tests__/ralplan-gate.test.js.map +0 -1
- package/dist/autopilot/deep-interview-gate.d.ts +0 -18
- package/dist/autopilot/deep-interview-gate.d.ts.map +0 -1
- package/dist/autopilot/deep-interview-gate.js +0 -396
- package/dist/autopilot/deep-interview-gate.js.map +0 -1
- package/dist/autopilot/ralplan-gate.d.ts +0 -18
- package/dist/autopilot/ralplan-gate.d.ts.map +0 -1
- package/dist/autopilot/ralplan-gate.js +0 -148
- package/dist/autopilot/ralplan-gate.js.map +0 -1
- package/dist/hooks/__tests__/prometheus-strict-contract.test.d.ts +0 -2
- package/dist/hooks/__tests__/prometheus-strict-contract.test.d.ts.map +0 -1
- package/dist/hooks/__tests__/prometheus-strict-contract.test.js +0 -320
- package/dist/hooks/__tests__/prometheus-strict-contract.test.js.map +0 -1
- package/dist/pipeline/__tests__/orchestrator.test.d.ts +0 -2
- package/dist/pipeline/__tests__/orchestrator.test.d.ts.map +0 -1
- package/dist/pipeline/__tests__/orchestrator.test.js +0 -770
- package/dist/pipeline/__tests__/orchestrator.test.js.map +0 -1
- package/dist/pipeline/__tests__/stages.test.d.ts +0 -2
- package/dist/pipeline/__tests__/stages.test.d.ts.map +0 -1
- package/dist/pipeline/__tests__/stages.test.js +0 -1778
- package/dist/pipeline/__tests__/stages.test.js.map +0 -1
- package/dist/pipeline/index.d.ts +0 -25
- package/dist/pipeline/index.d.ts.map +0 -1
- package/dist/pipeline/index.js +0 -17
- package/dist/pipeline/index.js.map +0 -1
- package/dist/pipeline/orchestrator.d.ts +0 -52
- package/dist/pipeline/orchestrator.d.ts.map +0 -1
- package/dist/pipeline/orchestrator.js +0 -399
- package/dist/pipeline/orchestrator.js.map +0 -1
- package/dist/pipeline/review-verdict.d.ts +0 -3
- package/dist/pipeline/review-verdict.d.ts.map +0 -1
- package/dist/pipeline/review-verdict.js +0 -14
- package/dist/pipeline/review-verdict.js.map +0 -1
- package/dist/pipeline/stages/code-review.d.ts +0 -35
- package/dist/pipeline/stages/code-review.d.ts.map +0 -1
- package/dist/pipeline/stages/code-review.js +0 -55
- package/dist/pipeline/stages/code-review.js.map +0 -1
- package/dist/pipeline/stages/deep-interview.d.ts +0 -15
- package/dist/pipeline/stages/deep-interview.d.ts.map +0 -1
- package/dist/pipeline/stages/deep-interview.js +0 -32
- package/dist/pipeline/stages/deep-interview.js.map +0 -1
- package/dist/pipeline/stages/ralph-verify.d.ts +0 -53
- package/dist/pipeline/stages/ralph-verify.d.ts.map +0 -1
- package/dist/pipeline/stages/ralph-verify.js +0 -89
- package/dist/pipeline/stages/ralph-verify.js.map +0 -1
- package/dist/pipeline/stages/ralplan.d.ts +0 -27
- package/dist/pipeline/stages/ralplan.d.ts.map +0 -1
- package/dist/pipeline/stages/ralplan.js +0 -152
- package/dist/pipeline/stages/ralplan.js.map +0 -1
- package/dist/pipeline/stages/team-exec.d.ts +0 -52
- package/dist/pipeline/stages/team-exec.d.ts.map +0 -1
- package/dist/pipeline/stages/team-exec.js +0 -266
- package/dist/pipeline/stages/team-exec.js.map +0 -1
- package/dist/pipeline/stages/ultragoal.d.ts +0 -23
- package/dist/pipeline/stages/ultragoal.d.ts.map +0 -1
- package/dist/pipeline/stages/ultragoal.js +0 -53
- package/dist/pipeline/stages/ultragoal.js.map +0 -1
- package/dist/pipeline/stages/ultraqa.d.ts +0 -33
- package/dist/pipeline/stages/ultraqa.d.ts.map +0 -1
- package/dist/pipeline/stages/ultraqa.js +0 -49
- package/dist/pipeline/stages/ultraqa.js.map +0 -1
- package/dist/pipeline/types.d.ts +0 -124
- package/dist/pipeline/types.d.ts.map +0 -1
- package/dist/pipeline/types.js +0 -8
- package/dist/pipeline/types.js.map +0 -1
- package/dist/ralplan/__tests__/consensus-gate.test.d.ts +0 -2
- package/dist/ralplan/__tests__/consensus-gate.test.d.ts.map +0 -1
- package/dist/ralplan/__tests__/consensus-gate.test.js +0 -1455
- package/dist/ralplan/__tests__/consensus-gate.test.js.map +0 -1
- package/dist/ralplan/consensus-gate.d.ts +0 -62
- package/dist/ralplan/consensus-gate.d.ts.map +0 -1
- package/dist/ralplan/consensus-gate.js +0 -760
- package/dist/ralplan/consensus-gate.js.map +0 -1
- package/dist/scripts/notify-hook/ralph-session-resume.d.ts +0 -25
- package/dist/scripts/notify-hook/ralph-session-resume.d.ts.map +0 -1
- package/dist/scripts/notify-hook/ralph-session-resume.js +0 -377
- package/dist/scripts/notify-hook/ralph-session-resume.js.map +0 -1
- package/dist/state/__tests__/planning-gate.test.d.ts +0 -2
- package/dist/state/__tests__/planning-gate.test.d.ts.map +0 -1
- package/dist/state/__tests__/planning-gate.test.js +0 -629
- package/dist/state/__tests__/planning-gate.test.js.map +0 -1
- package/plugins/oh-my-codex/skills/autoresearch-goal/SKILL.md +0 -39
- package/plugins/oh-my-codex/skills/pipeline/SKILL.md +0 -97
- package/plugins/oh-my-codex/skills/prometheus-strict/README.md +0 -35
- package/plugins/oh-my-codex/skills/prometheus-strict/SKILL.md +0 -219
- package/plugins/oh-my-codex/skills/ralph/SKILL.md +0 -298
- package/plugins/oh-my-codex/skills/ultrawork/SKILL.md +0 -190
- package/plugins/oh-my-codex/skills/ultrawork/references/agent-tiers.md +0 -62
- package/prompts/prometheus-strict-metis.md +0 -274
- package/prompts/prometheus-strict-momus.md +0 -82
- package/prompts/prometheus-strict-oracle.md +0 -107
- package/prompts/scholastic.md +0 -11
- package/prompts/sisyphus-lite.md +0 -111
- package/skills/ask-claude/SKILL.md +0 -12
- package/skills/ask-gemini/SKILL.md +0 -12
- package/skills/build-fix/SKILL.md +0 -10
- package/skills/deepsearch/SKILL.md +0 -10
- package/skills/ecomode/SKILL.md +0 -114
- package/skills/ecomode/references/agent-tiers.md +0 -61
- package/skills/frontend-ui-ux/SKILL.md +0 -16
- package/skills/help/SKILL.md +0 -10
- package/skills/note/SKILL.md +0 -10
- package/skills/prometheus-strict/README.md +0 -35
- package/skills/prometheus-strict/SKILL.md +0 -219
- package/skills/ralph-init/SKILL.md +0 -10
- package/skills/review/SKILL.md +0 -10
- package/skills/security-review/SKILL.md +0 -10
- package/skills/swarm/SKILL.md +0 -12
- package/skills/tdd/SKILL.md +0 -104
- package/skills/trace/SKILL.md +0 -10
- package/skills/visual-verdict/SKILL.md +0 -10
- package/skills/web-clone/SKILL.md +0 -357
- package/src/scripts/notify-hook/ralph-session-resume.ts +0 -466
package/dist/hooks/session.js
CHANGED
|
@@ -5,14 +5,26 @@
|
|
|
5
5
|
* serialized by an adjacent lock directory and become visible only after an
|
|
6
6
|
* atomic rename of a transaction-owned temporary file.
|
|
7
7
|
*/
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
8
|
+
import { execFileSync as nodeExecFileSync } from 'node:child_process';
|
|
9
|
+
import { appendFile, mkdtemp as nodeMkdtemp, link as nodeLink, lstat as nodeLstat, mkdir as nodeMkdir, open, readFile as nodeReadFile, readdir as nodeReaddir, realpath, rename as nodeRename, rmdir as nodeRmdir, rm, unlink as nodeUnlink, writeFile as nodeWriteFile, } from 'fs/promises';
|
|
10
|
+
import { appendFileSync, closeSync, constants as fsConstants, fstatSync, openSync, readFileSync, realpathSync } 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, relative, resolve, sep } from 'path';
|
|
12
14
|
import { omxRoot, omxLogsDir, sameFilePath } from '../utils/paths.js';
|
|
15
|
+
import { resolveRuntimeBinaryPath } from '../runtime/bridge.js';
|
|
13
16
|
import { getBaseStateDirWithSource, resolveWorkingDirectoryForState, } from '../mcp/state-paths.js';
|
|
14
17
|
import { emitDegradedDurabilityWarning, recordRegularFileSyncOutcome, syncRegularFile, } from '../utils/file-durability.js';
|
|
18
|
+
export class CommittedLaunchBlockedError extends Error {
|
|
19
|
+
secondaryFailures;
|
|
20
|
+
name = 'CommittedLaunchBlockedError';
|
|
21
|
+
constructor(secondaryFailures) {
|
|
22
|
+
super('Session pointer committed, but lock release left recovery evidence.');
|
|
23
|
+
this.secondaryFailures = secondaryFailures;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
15
26
|
const SESSION_FILE = 'session.json';
|
|
27
|
+
const SESSION_OWNER_FILE = 'session-owner.json';
|
|
16
28
|
const HISTORY_FILE = 'session-history.jsonl';
|
|
17
29
|
const SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
|
|
18
30
|
const SESSION_POINTER_TOKEN_PATTERN = /^[A-Za-z0-9_-]{16,128}$/;
|
|
@@ -20,6 +32,14 @@ const SHA256_PATTERN = /^[a-f0-9]{64}$/;
|
|
|
20
32
|
const LOCK_RETRY_DELAYS_MS = [25, 50, 100];
|
|
21
33
|
const DEFAULT_POINTER_TIMEOUT_MS = 5_000;
|
|
22
34
|
const NATIVE_POINTER_TIMEOUT_MS = 2_000;
|
|
35
|
+
function traceSessionFinalizationOperation(operation) {
|
|
36
|
+
if (process.env.OMX_TEST_DETACHED_TRACE !== '1')
|
|
37
|
+
return;
|
|
38
|
+
const tracePath = process.env.OMX_TEST_DETACHED_TRACE_PATH;
|
|
39
|
+
if (!tracePath)
|
|
40
|
+
return;
|
|
41
|
+
appendFileSync(tracePath, `[omx-test-session-finalization] ${operation}\n`);
|
|
42
|
+
}
|
|
23
43
|
/**
|
|
24
44
|
* Convert arbitrary input into a valid session ID without exposing validator
|
|
25
45
|
* exceptions to lifecycle or hook inputs.
|
|
@@ -31,9 +51,9 @@ export function normalizeSessionId(value) {
|
|
|
31
51
|
return SESSION_ID_PATTERN.test(trimmed) ? trimmed : undefined;
|
|
32
52
|
}
|
|
33
53
|
/** Resolve the one exact pointer path used by an operation. */
|
|
34
|
-
export function resolveSessionPointerContext(cwd) {
|
|
35
|
-
const normalizedCwd = resolveWorkingDirectoryForState(cwd);
|
|
36
|
-
const { baseStateDir, rootSource } = getBaseStateDirWithSource(normalizedCwd);
|
|
54
|
+
export function resolveSessionPointerContext(cwd, env = process.env) {
|
|
55
|
+
const normalizedCwd = resolveWorkingDirectoryForState(cwd, env);
|
|
56
|
+
const { baseStateDir, rootSource } = getBaseStateDirWithSource(normalizedCwd, env);
|
|
37
57
|
const pointerPath = join(baseStateDir, SESSION_FILE);
|
|
38
58
|
return {
|
|
39
59
|
cwd: normalizedCwd,
|
|
@@ -43,6 +63,26 @@ export function resolveSessionPointerContext(cwd) {
|
|
|
43
63
|
lockPath: `${pointerPath}.lock`,
|
|
44
64
|
};
|
|
45
65
|
}
|
|
66
|
+
function resolveNativeSessionOwnerContext(cwd, nativeSessionId) {
|
|
67
|
+
const root = resolveSessionPointerContext(cwd);
|
|
68
|
+
const normalized = normalizeSessionId(nativeSessionId);
|
|
69
|
+
if (!normalized) {
|
|
70
|
+
throw resolvedAbort(root, {
|
|
71
|
+
code: 'session_pointer_io_failure',
|
|
72
|
+
operation: 'pointer-classify',
|
|
73
|
+
lockPath: root.lockPath,
|
|
74
|
+
reason: 'A valid native session ID is required for owner evidence.',
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
const baseStateDir = join(root.baseStateDir, 'sessions', normalized);
|
|
78
|
+
const sessionPath = join(baseStateDir, SESSION_OWNER_FILE);
|
|
79
|
+
return {
|
|
80
|
+
...root,
|
|
81
|
+
baseStateDir,
|
|
82
|
+
sessionPath,
|
|
83
|
+
lockPath: `${sessionPath}.lock`,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
46
86
|
function attemptedStateRootSource() {
|
|
47
87
|
try {
|
|
48
88
|
if (process.env.OMX_TEAM_STATE_ROOT?.trim())
|
|
@@ -123,6 +163,7 @@ const defaultFsDependencies = {
|
|
|
123
163
|
},
|
|
124
164
|
readdir: async (path) => await nodeReaddir(path),
|
|
125
165
|
readFile: async (path, encoding) => await nodeReadFile(path, encoding),
|
|
166
|
+
readBytes: async (path) => await nodeReadFile(path),
|
|
126
167
|
lstat: async (path) => await nodeLstat(path),
|
|
127
168
|
writeFile: async (path, data, options) => {
|
|
128
169
|
await nodeWriteFile(path, data, options);
|
|
@@ -139,6 +180,9 @@ const defaultFsDependencies = {
|
|
|
139
180
|
rename: async (from, to) => {
|
|
140
181
|
await nodeRename(from, to);
|
|
141
182
|
},
|
|
183
|
+
link: async (path, dest) => {
|
|
184
|
+
await nodeLink(path, dest);
|
|
185
|
+
},
|
|
142
186
|
unlink: async (path) => {
|
|
143
187
|
await nodeUnlink(path);
|
|
144
188
|
},
|
|
@@ -146,6 +190,28 @@ const defaultFsDependencies = {
|
|
|
146
190
|
await nodeRmdir(path);
|
|
147
191
|
},
|
|
148
192
|
};
|
|
193
|
+
async function defaultRecoveryRenameNoReplace(from, to) {
|
|
194
|
+
if (!from.trim() || !to.trim() || !isAbsolute(from) || !isAbsolute(to))
|
|
195
|
+
return 'unsupported';
|
|
196
|
+
try {
|
|
197
|
+
const stdout = nodeExecFileSync(resolveRuntimeBinaryPath(), ['fs-rename-no-replace', from, to], {
|
|
198
|
+
encoding: 'utf8',
|
|
199
|
+
maxBuffer: 64 * 1024,
|
|
200
|
+
shell: false,
|
|
201
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
202
|
+
timeout: 10_000,
|
|
203
|
+
});
|
|
204
|
+
const parsed = JSON.parse(String(stdout));
|
|
205
|
+
if (parsed.outcome === 'moved' || parsed.outcome === 'not-moved' || parsed.outcome === 'unsupported') {
|
|
206
|
+
return parsed.outcome;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
catch {
|
|
210
|
+
// Missing binaries, spawn failures, hard runtime errors, and malformed output
|
|
211
|
+
// all fail closed; recovery must never fall back to an overwriting rename.
|
|
212
|
+
}
|
|
213
|
+
return 'unsupported';
|
|
214
|
+
}
|
|
149
215
|
/** @internal Exposed only so source-module tests can verify default ESRCH handling. */
|
|
150
216
|
export function __createDefaultPidProbeForTests(killZero) {
|
|
151
217
|
return (pid) => {
|
|
@@ -163,26 +229,40 @@ function defaultProbePid(pid) {
|
|
|
163
229
|
process.kill(targetPid, 0);
|
|
164
230
|
})(pid);
|
|
165
231
|
}
|
|
166
|
-
function
|
|
167
|
-
if (platform
|
|
168
|
-
return
|
|
169
|
-
|
|
170
|
-
if (
|
|
171
|
-
return {
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
232
|
+
function defaultObserveProcess(pid, platform) {
|
|
233
|
+
if (platform === 'linux') {
|
|
234
|
+
return observeLinuxProcess(pid);
|
|
235
|
+
}
|
|
236
|
+
if (platform !== 'darwin' && platform !== 'win32') {
|
|
237
|
+
return { kind: 'unsupported' };
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
const stdout = nodeExecFileSync(resolveRuntimeBinaryPath(), ['process-identity', String(pid)], {
|
|
241
|
+
encoding: 'utf8',
|
|
242
|
+
timeout: 3000,
|
|
243
|
+
maxBuffer: 64 * 1024,
|
|
244
|
+
shell: false,
|
|
245
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
246
|
+
});
|
|
247
|
+
return parseProcessIdentityOutput(stdout);
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
return { kind: 'unsupported' };
|
|
251
|
+
}
|
|
178
252
|
}
|
|
179
253
|
const defaultTransactionDependencies = {
|
|
180
254
|
fs: defaultFsDependencies,
|
|
181
255
|
nowMs: () => Date.now(),
|
|
182
256
|
sleep: async (ms) => await new Promise((resolve) => setTimeout(resolve, ms)),
|
|
183
257
|
token: () => randomUUID(),
|
|
258
|
+
runtimePlatform: process.platform,
|
|
184
259
|
probePid: defaultProbePid,
|
|
185
|
-
|
|
260
|
+
observeProcess: defaultObserveProcess,
|
|
261
|
+
atomicRenameNoReplace: defaultRecoveryRenameNoReplace,
|
|
262
|
+
};
|
|
263
|
+
export const defaultProcessInspectionProvider = {
|
|
264
|
+
probePid: defaultProbePid,
|
|
265
|
+
observeProcess: defaultObserveProcess,
|
|
186
266
|
};
|
|
187
267
|
let transactionDependencies = defaultTransactionDependencies;
|
|
188
268
|
/** @internal Source-module test harness. Always reset after a test. */
|
|
@@ -234,38 +314,344 @@ function readLinuxProcessIdentity(pid) {
|
|
|
234
314
|
return null;
|
|
235
315
|
}
|
|
236
316
|
}
|
|
237
|
-
|
|
317
|
+
const NODE_PLATFORMS = new Set([
|
|
318
|
+
'aix', 'android', 'darwin', 'freebsd', 'haiku', 'linux', 'openbsd', 'sunos', 'win32', 'cygwin', 'netbsd',
|
|
319
|
+
]);
|
|
320
|
+
const PROCESS_BIRTH_PATTERN = /^\d+(?:\.\d+)?$/;
|
|
321
|
+
function isNodePlatform(value) {
|
|
322
|
+
return typeof value === 'string' && NODE_PLATFORMS.has(value);
|
|
323
|
+
}
|
|
324
|
+
function isValidBirth(value) {
|
|
325
|
+
return typeof value === 'string' && PROCESS_BIRTH_PATTERN.test(value);
|
|
326
|
+
}
|
|
327
|
+
export function isValidProcessIdentity(value) {
|
|
328
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
329
|
+
return false;
|
|
330
|
+
const identity = value;
|
|
331
|
+
if (Object.keys(identity).some((key) => key !== 'platform' && key !== 'birth' && key !== 'cmdline_hash'))
|
|
332
|
+
return false;
|
|
333
|
+
return isNodePlatform(identity.platform)
|
|
334
|
+
&& isValidBirth(identity.birth)
|
|
335
|
+
&& (identity.cmdline_hash === undefined
|
|
336
|
+
|| typeof identity.cmdline_hash === 'string' && SHA256_PATTERN.test(identity.cmdline_hash));
|
|
337
|
+
}
|
|
338
|
+
function isComparableProcessIdentity(value) {
|
|
339
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
340
|
+
return false;
|
|
341
|
+
const identity = value;
|
|
342
|
+
return isNodePlatform(identity.platform)
|
|
343
|
+
&& typeof identity.birth === 'string'
|
|
344
|
+
&& identity.birth.length > 0
|
|
345
|
+
&& (identity.cmdline_hash === undefined
|
|
346
|
+
|| typeof identity.cmdline_hash === 'string' && SHA256_PATTERN.test(identity.cmdline_hash));
|
|
347
|
+
}
|
|
348
|
+
function observeLinuxProcess(pid) {
|
|
349
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
350
|
+
return { kind: 'error' };
|
|
351
|
+
const identity = readLinuxProcessIdentity(pid);
|
|
352
|
+
if (identity) {
|
|
353
|
+
const cmdlineHash = hashCmdline(identity.cmdline);
|
|
354
|
+
return {
|
|
355
|
+
kind: 'identity',
|
|
356
|
+
identity: {
|
|
357
|
+
platform: 'linux',
|
|
358
|
+
birth: String(identity.startTicks),
|
|
359
|
+
...(cmdlineHash ? { cmdline_hash: cmdlineHash } : {}),
|
|
360
|
+
},
|
|
361
|
+
};
|
|
362
|
+
}
|
|
363
|
+
let statContent;
|
|
238
364
|
try {
|
|
239
|
-
|
|
240
|
-
|
|
365
|
+
statContent = readFileSync(`/proc/${pid}/stat`, 'utf8');
|
|
366
|
+
}
|
|
367
|
+
catch (error) {
|
|
368
|
+
const code = errorCode(error);
|
|
369
|
+
if (code === 'ENOENT' || code === 'ESRCH')
|
|
370
|
+
return { kind: 'gone' };
|
|
371
|
+
if (code === 'EACCES' || code === 'EPERM')
|
|
372
|
+
return { kind: 'denied' };
|
|
373
|
+
return { kind: 'error' };
|
|
374
|
+
}
|
|
375
|
+
if (!statContent)
|
|
376
|
+
return { kind: 'error' };
|
|
377
|
+
const probe = defaultProbePid(pid);
|
|
378
|
+
if (probe === 'dead')
|
|
379
|
+
return { kind: 'gone' };
|
|
380
|
+
if (probe === 'indeterminate')
|
|
381
|
+
return { kind: 'denied' };
|
|
382
|
+
return { kind: 'error' };
|
|
383
|
+
}
|
|
384
|
+
function parseProcessIdentityOutput(stdout) {
|
|
385
|
+
try {
|
|
386
|
+
const parsed = JSON.parse(stdout);
|
|
387
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
388
|
+
return { kind: 'error' };
|
|
389
|
+
const value = parsed;
|
|
390
|
+
const keys = Object.keys(value);
|
|
391
|
+
const outcome = value.outcome;
|
|
392
|
+
if (outcome !== undefined) {
|
|
393
|
+
if (typeof outcome !== 'string')
|
|
394
|
+
return { kind: 'error' };
|
|
395
|
+
if (outcome === 'gone' || outcome === 'denied' || outcome === 'unsupported') {
|
|
396
|
+
if (keys.length !== 1)
|
|
397
|
+
return { kind: 'error' };
|
|
398
|
+
if (outcome === 'gone')
|
|
399
|
+
return { kind: 'gone' };
|
|
400
|
+
if (outcome === 'denied')
|
|
401
|
+
return { kind: 'denied' };
|
|
402
|
+
return { kind: 'unsupported' };
|
|
403
|
+
}
|
|
404
|
+
if (outcome === 'error')
|
|
405
|
+
return { kind: 'error' };
|
|
406
|
+
return { kind: 'error' };
|
|
407
|
+
}
|
|
408
|
+
if (keys.some((key) => key !== 'platform' && key !== 'birth' && key !== 'cmdline')) {
|
|
409
|
+
return { kind: 'error' };
|
|
410
|
+
}
|
|
411
|
+
const platform = value.platform;
|
|
412
|
+
const birth = value.birth;
|
|
413
|
+
if (!isNodePlatform(platform) || !isValidBirth(birth))
|
|
414
|
+
return { kind: 'error' };
|
|
415
|
+
if (value.cmdline !== undefined && typeof value.cmdline !== 'string')
|
|
416
|
+
return { kind: 'error' };
|
|
417
|
+
const cmdlineHash = hashCmdline(value.cmdline);
|
|
418
|
+
return {
|
|
419
|
+
kind: 'identity',
|
|
420
|
+
identity: {
|
|
421
|
+
platform,
|
|
422
|
+
birth,
|
|
423
|
+
...(cmdlineHash ? { cmdline_hash: cmdlineHash } : {}),
|
|
424
|
+
},
|
|
425
|
+
};
|
|
241
426
|
}
|
|
242
427
|
catch {
|
|
243
|
-
return
|
|
428
|
+
return { kind: 'error' };
|
|
244
429
|
}
|
|
245
430
|
}
|
|
246
431
|
/**
|
|
247
432
|
* Legacy boolean stale check retained for read compatibility. The pointer
|
|
248
433
|
* classifier below keeps indeterminate liveness distinct from definitely dead.
|
|
434
|
+
*
|
|
435
|
+
* KNOWN GAP (darwin): `identity-indeterminate` is treated as stale here, which is fail-closed and
|
|
436
|
+
* correct on platforms that DO record process-birth evidence but cannot read it. On darwin no
|
|
437
|
+
* identity source exists at all, so a live session also classifies indeterminate and reads as
|
|
438
|
+
* stale. Two suite cases pin that gap (`treats symlinked cwd aliases as authoritative`,
|
|
439
|
+
* `writes session start/end lifecycle artifacts`), while `returns true on Linux when identity
|
|
440
|
+
* metadata is missing` / `... cannot be read` pin the fail-closed behavior, so the two cannot be
|
|
441
|
+
* reconciled by changing this mapping. Fixing it requires distinguishing "no identity source on
|
|
442
|
+
* this platform" from "identity expected but unavailable" in `classifySessionProcess` itself.
|
|
443
|
+
* Callers needing a safe live-session answer today use `classifySessionStateLiveness(...) !==
|
|
444
|
+
* 'stale-dead'` (see src/cli/setup.ts overwrite guard).
|
|
249
445
|
*/
|
|
446
|
+
function sessionClassificationDependencies(options) {
|
|
447
|
+
const runtimePlatform = options.platform ?? transactionDependencies.runtimePlatform ?? process.platform;
|
|
448
|
+
const probePid = options.probePid ?? (options.isPidAlive
|
|
449
|
+
? (pid) => options.isPidAlive(pid) ? 'alive' : 'dead'
|
|
450
|
+
: transactionDependencies.probePid);
|
|
451
|
+
const observeProcess = options.observeProcess ?? (options.readLinuxIdentity
|
|
452
|
+
? (pid, platform) => {
|
|
453
|
+
if (platform !== 'linux')
|
|
454
|
+
return { kind: 'unsupported' };
|
|
455
|
+
const identity = options.readLinuxIdentity(pid);
|
|
456
|
+
if (!identity)
|
|
457
|
+
return { kind: 'error' };
|
|
458
|
+
const cmdlineHash = hashCmdline(identity.cmdline);
|
|
459
|
+
return {
|
|
460
|
+
kind: 'identity',
|
|
461
|
+
identity: {
|
|
462
|
+
platform: 'linux',
|
|
463
|
+
birth: String(identity.startTicks),
|
|
464
|
+
...(cmdlineHash ? { cmdline_hash: cmdlineHash } : {}),
|
|
465
|
+
},
|
|
466
|
+
};
|
|
467
|
+
}
|
|
468
|
+
: transactionDependencies.observeProcess);
|
|
469
|
+
return { runtimePlatform, probePid, observeProcess };
|
|
470
|
+
}
|
|
250
471
|
export function isSessionStale(state, options = {}) {
|
|
251
472
|
if (!Number.isInteger(state.pid) || state.pid <= 0)
|
|
252
473
|
return true;
|
|
253
|
-
const
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
474
|
+
const { runtimePlatform, probePid, observeProcess } = sessionClassificationDependencies(options);
|
|
475
|
+
const dependencies = {
|
|
476
|
+
...transactionDependencies,
|
|
477
|
+
runtimePlatform,
|
|
478
|
+
probePid,
|
|
479
|
+
observeProcess,
|
|
480
|
+
};
|
|
481
|
+
const classification = classifySessionProcess(state, dependencies);
|
|
482
|
+
if (classification === 'usable')
|
|
258
483
|
return false;
|
|
259
|
-
|
|
260
|
-
if (!liveIdentity || typeof state.pid_start_ticks !== 'number')
|
|
261
|
-
return true;
|
|
262
|
-
if (state.pid_start_ticks !== liveIdentity.startTicks)
|
|
484
|
+
if (classification === 'stale-dead')
|
|
263
485
|
return true;
|
|
264
|
-
|
|
265
|
-
|
|
486
|
+
// `identity-indeterminate` means two different things and only one of them is stale.
|
|
487
|
+
//
|
|
488
|
+
// Where the platform CAN record process-birth evidence (linux) but it is missing or unreadable,
|
|
489
|
+
// fail closed: that is the case `returns true on Linux when identity metadata is missing` and
|
|
490
|
+
// `... when live identity cannot be read` pin deliberately.
|
|
491
|
+
//
|
|
492
|
+
// Where no identity source exists at all, every live session classifies indeterminate, so failing
|
|
493
|
+
// closed reported a running session as dead. There a positively alive pid is the strongest
|
|
494
|
+
// evidence available, and this is a READ predicate, not an authority grant: the classifier still
|
|
495
|
+
// returns identity-indeterminate (pinned by `classifies identity-less live non-Linux pointers as
|
|
496
|
+
// identity-indeterminate`), and authority-granting paths keep requiring `usable`.
|
|
497
|
+
return !isUnobservableLivePointerState(state, runtimePlatform, probePid);
|
|
498
|
+
}
|
|
499
|
+
function safeObserve(provider, pid, runtimePlatform) {
|
|
500
|
+
try {
|
|
501
|
+
return provider.observeProcess(pid, runtimePlatform);
|
|
502
|
+
}
|
|
503
|
+
catch {
|
|
504
|
+
return { kind: 'error' };
|
|
505
|
+
}
|
|
506
|
+
}
|
|
507
|
+
function compareObservation(recorded, observation, runtimePlatform) {
|
|
508
|
+
if (!observation || typeof observation !== 'object' || observation.kind !== 'identity') {
|
|
509
|
+
return 'identity-unavailable';
|
|
510
|
+
}
|
|
511
|
+
const identity = observation.identity;
|
|
512
|
+
if (!isComparableProcessIdentity(identity) || identity.platform !== runtimePlatform) {
|
|
513
|
+
return 'identity-unavailable';
|
|
514
|
+
}
|
|
515
|
+
if (identity.birth !== recorded.birth)
|
|
516
|
+
return 'birth-mismatch-retryable';
|
|
517
|
+
if (recorded.cmdline_hash !== undefined && identity.cmdline_hash !== recorded.cmdline_hash) {
|
|
518
|
+
return 'identity-unavailable';
|
|
519
|
+
}
|
|
520
|
+
return 'match';
|
|
521
|
+
}
|
|
522
|
+
/** Compare recorded process birth evidence with a fresh provider observation. */
|
|
523
|
+
export function classifyRecordedIdentity(recorded, runtimePlatform, provider, pid) {
|
|
524
|
+
if (!isComparableProcessIdentity(recorded) || recorded.platform !== runtimePlatform || !Number.isInteger(pid) || pid <= 0) {
|
|
525
|
+
return { status: 'identity-unavailable' };
|
|
526
|
+
}
|
|
527
|
+
let probe;
|
|
528
|
+
try {
|
|
529
|
+
probe = provider.probePid(pid);
|
|
530
|
+
}
|
|
531
|
+
catch {
|
|
532
|
+
return { status: 'identity-unavailable' };
|
|
533
|
+
}
|
|
534
|
+
if (probe === 'dead')
|
|
535
|
+
return { status: 'gone' };
|
|
536
|
+
if (probe !== 'alive')
|
|
537
|
+
return { status: 'identity-unavailable' };
|
|
538
|
+
const first = compareObservation(recorded, safeObserve(provider, pid, runtimePlatform), runtimePlatform);
|
|
539
|
+
if (first === 'birth-mismatch-retryable') {
|
|
540
|
+
const second = compareObservation(recorded, safeObserve(provider, pid, runtimePlatform), runtimePlatform);
|
|
541
|
+
if (second === 'match')
|
|
542
|
+
return { status: 'match' };
|
|
543
|
+
if (second === 'birth-mismatch-retryable' || second === 'birth-mismatch')
|
|
544
|
+
return { status: 'birth-mismatch' };
|
|
545
|
+
return { status: 'identity-unavailable' };
|
|
546
|
+
}
|
|
547
|
+
if (first === 'match')
|
|
548
|
+
return { status: 'match' };
|
|
549
|
+
if (first === 'birth-mismatch')
|
|
550
|
+
return { status: 'birth-mismatch' };
|
|
551
|
+
return { status: 'identity-unavailable' };
|
|
552
|
+
}
|
|
553
|
+
function recordedIdentityForState(state, runtimePlatform) {
|
|
554
|
+
if (state.identity_schema_version !== undefined && state.identity_schema_version !== 2)
|
|
555
|
+
return 'invalid';
|
|
556
|
+
if (state.process_identity !== undefined) {
|
|
557
|
+
if (state.identity_schema_version !== 2 || !isValidProcessIdentity(state.process_identity))
|
|
558
|
+
return 'invalid';
|
|
559
|
+
if (state.platform !== undefined && state.platform !== state.process_identity.platform)
|
|
560
|
+
return 'invalid';
|
|
561
|
+
return state.process_identity;
|
|
562
|
+
}
|
|
563
|
+
if (state.identity_schema_version === 2)
|
|
564
|
+
return 'invalid';
|
|
565
|
+
const recordedPlatform = state.platform ?? runtimePlatform;
|
|
566
|
+
if (recordedPlatform === 'linux' && isValidStartTicks(state.pid_start_ticks)) {
|
|
567
|
+
const cmdlineHash = hashCmdline(state.pid_cmdline);
|
|
568
|
+
return {
|
|
569
|
+
platform: 'linux',
|
|
570
|
+
birth: String(state.pid_start_ticks),
|
|
571
|
+
...(cmdlineHash ? { cmdline_hash: cmdlineHash } : {}),
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
return null;
|
|
575
|
+
}
|
|
576
|
+
/**
|
|
577
|
+
* A state with no recorded identity is indeterminate unless the pid is positively dead.
|
|
578
|
+
*
|
|
579
|
+
* Do NOT relax this to "alive + non-linux => usable": `classifies identity-less live non-Linux
|
|
580
|
+
* pointers as identity-indeterminate` pins it deliberately, because without birth evidence an
|
|
581
|
+
* alive pid cannot be distinguished from a REUSED pid, and granting `usable` there would let a
|
|
582
|
+
* foreign process inherit session authority. Read paths that only need "is anyone alive here"
|
|
583
|
+
* must ask `classifySessionStateLiveness(...) !== 'stale-dead'` instead (see the overwrite guard
|
|
584
|
+
* in src/cli/setup.ts); authority-granting paths keep requiring `usable`.
|
|
585
|
+
*/
|
|
586
|
+
function probeIdentitylessProcess(pid, dependencies) {
|
|
587
|
+
let probe;
|
|
588
|
+
try {
|
|
589
|
+
probe = dependencies.probePid(pid);
|
|
590
|
+
}
|
|
591
|
+
catch {
|
|
592
|
+
return 'identity-indeterminate';
|
|
593
|
+
}
|
|
594
|
+
return probe === 'dead' ? 'stale-dead' : 'identity-indeterminate';
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Is an `identity-indeterminate` pointer merely UNOBSERVABLE on this platform, rather than
|
|
598
|
+
* untrustworthy?
|
|
599
|
+
*
|
|
600
|
+
* Three distinct situations collapse into `identity-indeterminate`, and only the first one describes
|
|
601
|
+
* a pointer that may legitimately be live:
|
|
602
|
+
* - the platform records no process-birth evidence at all (every live pointer lands here);
|
|
603
|
+
* - linux, which does record it, is missing or cannot read it (fail closed: the metadata should
|
|
604
|
+
* exist);
|
|
605
|
+
* - the evidence claims a DIFFERENT platform, which is forged or foreign (fail closed).
|
|
606
|
+
*
|
|
607
|
+
* Read predicates and preservation/refusal decisions may treat the first case as occupied. Authority
|
|
608
|
+
* to own writes still requires `usable`, and the classifier itself is unchanged.
|
|
609
|
+
*/
|
|
610
|
+
function isUnobservableLivePointerState(state,
|
|
611
|
+
// Explicit rather than read from transactionDependencies: isSessionStale accepts injected platform
|
|
612
|
+
// and probe overrides, and silently using the host values there evaluated the linux fail-closed
|
|
613
|
+
// cases against the wrong runtime.
|
|
614
|
+
runtimePlatform = transactionDependencies.runtimePlatform, probe = transactionDependencies.probePid) {
|
|
615
|
+
if (!state)
|
|
616
|
+
return false;
|
|
617
|
+
const recordedPlatform = state.process_identity?.platform ?? state.platform ?? runtimePlatform;
|
|
618
|
+
if (recordedPlatform !== runtimePlatform)
|
|
619
|
+
return false;
|
|
620
|
+
if (recordedPlatform === 'linux')
|
|
621
|
+
return false;
|
|
622
|
+
if (typeof state.pid !== 'number' || !Number.isInteger(state.pid) || state.pid <= 0)
|
|
623
|
+
return false;
|
|
624
|
+
// POSITIVE liveness is required, not merely a plausible shape. A pid that cannot be probed at all
|
|
625
|
+
// (for example an out-of-range one) is not evidence of a live session, and admitting it would let
|
|
626
|
+
// a stale pointer survive a relaunch instead of failing closed.
|
|
627
|
+
try {
|
|
628
|
+
return probe(state.pid) === 'alive';
|
|
629
|
+
}
|
|
630
|
+
catch {
|
|
266
631
|
return false;
|
|
267
|
-
|
|
268
|
-
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
/**
|
|
635
|
+
* May an existing pointer whose liveness could NOT be proven be reconciled in place?
|
|
636
|
+
*
|
|
637
|
+
* `isUnobservableLivePointerState` answers "someone may be here", which is enough to REFUSE a foreign
|
|
638
|
+
* claim or to preserve lineage, but it is not proof of WHO is here, so reconciling it is an adoption
|
|
639
|
+
* and needs its own justification.
|
|
640
|
+
*
|
|
641
|
+
* Neither pid NOR platform equality can be required here, and both were measured rather than assumed:
|
|
642
|
+
* native SessionStart reconciliation exists so a native Codex process can adopt a pointer an OMX
|
|
643
|
+
* launch wrote, so the pid legitimately differs, and `preserves canonical session id while
|
|
644
|
+
* reconciling native SessionStart metadata` also reconciles with a different announced platform.
|
|
645
|
+
* Requiring either refused legitimate reconciliation (that test fails), which is the defect this
|
|
646
|
+
* change set fixes.
|
|
647
|
+
*
|
|
648
|
+
* What bounds the adoption is therefore: this cwd-authority check, asserted locally so the invariant
|
|
649
|
+
* is visible at the mutation site instead of only implied by the classifier's foreign-cwd rejection;
|
|
650
|
+
* plus native-id compatibility and owner-alias merge in the caller. A pointer for another working
|
|
651
|
+
* directory or another session can never be adopted.
|
|
652
|
+
*/
|
|
653
|
+
function mayAdoptUnprovenPointer(existing, cwd) {
|
|
654
|
+
return isSessionStateAuthoritativeForCwd(existing, cwd);
|
|
269
655
|
}
|
|
270
656
|
export function isSessionStateAuthoritativeForCwd(state, cwd) {
|
|
271
657
|
if (!normalizeSessionId(state.session_id))
|
|
@@ -284,55 +670,49 @@ export function isSessionStateUsable(state, cwd, options = {}) {
|
|
|
284
670
|
return false;
|
|
285
671
|
if (typeof state.cwd === 'string' && state.cwd.trim() && !isSessionStateAuthoritativeForCwd(state, cwd))
|
|
286
672
|
return false;
|
|
287
|
-
const
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
673
|
+
const { runtimePlatform, probePid, observeProcess } = sessionClassificationDependencies(options);
|
|
674
|
+
return classifySessionProcess(state, {
|
|
675
|
+
...transactionDependencies,
|
|
676
|
+
runtimePlatform,
|
|
677
|
+
probePid,
|
|
678
|
+
observeProcess,
|
|
679
|
+
}) === 'usable';
|
|
291
680
|
}
|
|
292
681
|
function isValidStartTicks(value) {
|
|
293
682
|
return typeof value === 'number' && Number.isInteger(value) && value >= 0;
|
|
294
683
|
}
|
|
295
684
|
function classifySessionProcess(state, dependencies) {
|
|
296
685
|
const hasPidMetadata = Number.isInteger(state.pid) && state.pid > 0;
|
|
297
|
-
const hasIdentityMetadata = typeof state.pid_start_ticks === 'number'
|
|
686
|
+
const hasIdentityMetadata = typeof state.pid_start_ticks === 'number'
|
|
687
|
+
|| typeof state.pid_cmdline === 'string'
|
|
688
|
+
|| state.identity_schema_version !== undefined
|
|
689
|
+
|| state.process_identity !== undefined;
|
|
298
690
|
if (!hasPidMetadata && !hasIdentityMetadata)
|
|
299
691
|
return 'usable';
|
|
300
692
|
if (!hasPidMetadata)
|
|
301
693
|
return 'identity-indeterminate';
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
}
|
|
306
|
-
catch {
|
|
307
|
-
return 'identity-indeterminate';
|
|
308
|
-
}
|
|
309
|
-
if (pidStatus === 'dead')
|
|
310
|
-
return 'stale-dead';
|
|
311
|
-
if (pidStatus !== 'alive')
|
|
312
|
-
return 'identity-indeterminate';
|
|
313
|
-
const platform = state.platform ?? process.platform;
|
|
314
|
-
if (platform !== 'linux')
|
|
315
|
-
return 'usable';
|
|
316
|
-
if (!isValidStartTicks(state.pid_start_ticks))
|
|
317
|
-
return 'identity-indeterminate';
|
|
318
|
-
let liveIdentity;
|
|
319
|
-
try {
|
|
320
|
-
liveIdentity = dependencies.readProcessIdentity(state.pid, platform);
|
|
321
|
-
}
|
|
322
|
-
catch {
|
|
694
|
+
const runtimePlatform = dependencies.runtimePlatform;
|
|
695
|
+
const recorded = recordedIdentityForState(state, runtimePlatform);
|
|
696
|
+
if (recorded === 'invalid')
|
|
323
697
|
return 'identity-indeterminate';
|
|
698
|
+
if (!recorded)
|
|
699
|
+
return probeIdentitylessProcess(state.pid, dependencies);
|
|
700
|
+
const classification = classifyRecordedIdentity(recorded, runtimePlatform, dependencies, state.pid);
|
|
701
|
+
switch (classification.status) {
|
|
702
|
+
case 'match': return 'usable';
|
|
703
|
+
case 'gone':
|
|
704
|
+
case 'birth-mismatch': return 'stale-dead';
|
|
705
|
+
case 'identity-unavailable': return 'identity-indeterminate';
|
|
324
706
|
}
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
}
|
|
335
|
-
return 'usable';
|
|
707
|
+
}
|
|
708
|
+
/**
|
|
709
|
+
* Classify process liveness for an already-parsed selected session pointer.
|
|
710
|
+
* Callers that need cwd/state_root authority must validate those against the
|
|
711
|
+
* SAME snapshot themselves; this evaluates only pid/start-tick evidence so a
|
|
712
|
+
* single immutable snapshot can drive both decisions.
|
|
713
|
+
*/
|
|
714
|
+
export function classifySessionStateLiveness(state) {
|
|
715
|
+
return classifySessionProcess(state, transactionDependencies);
|
|
336
716
|
}
|
|
337
717
|
function classifyParsedSessionPointer(context, value, raw) {
|
|
338
718
|
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
@@ -344,6 +724,16 @@ function classifyParsedSessionPointer(context, value, raw) {
|
|
|
344
724
|
if (typeof state.cwd === 'string' && state.cwd.trim() && !isSessionStateAuthoritativeForCwd(state, context.cwd)) {
|
|
345
725
|
return { status: 'foreign-cwd', raw, state };
|
|
346
726
|
}
|
|
727
|
+
if (state.identity_schema_version !== undefined && state.identity_schema_version !== 2)
|
|
728
|
+
return { status: 'identity-indeterminate', raw, state };
|
|
729
|
+
if (state.process_identity !== undefined
|
|
730
|
+
&& (state.identity_schema_version !== 2
|
|
731
|
+
|| !isValidProcessIdentity(state.process_identity)
|
|
732
|
+
|| state.platform !== undefined && state.platform !== state.process_identity.platform)) {
|
|
733
|
+
return { status: 'malformed', raw, state };
|
|
734
|
+
}
|
|
735
|
+
if (state.identity_schema_version === 2 && state.process_identity === undefined)
|
|
736
|
+
return { status: 'malformed', raw, state };
|
|
347
737
|
const processStatus = classifySessionProcess(state, transactionDependencies);
|
|
348
738
|
return { status: processStatus, state, raw };
|
|
349
739
|
}
|
|
@@ -382,9 +772,9 @@ export async function readUsableSessionStateFromContext(context) {
|
|
|
382
772
|
return state && isSessionStateUsable(state, context.cwd) ? state : null;
|
|
383
773
|
}
|
|
384
774
|
/** Read current session state from the exact selected root. */
|
|
385
|
-
export async function readSessionState(cwd) {
|
|
775
|
+
export async function readSessionState(cwd, env = process.env) {
|
|
386
776
|
try {
|
|
387
|
-
return await readSessionStateFromContext(resolveSessionPointerContext(cwd));
|
|
777
|
+
return await readSessionStateFromContext(resolveSessionPointerContext(cwd, env));
|
|
388
778
|
}
|
|
389
779
|
catch {
|
|
390
780
|
return null;
|
|
@@ -400,7 +790,7 @@ export async function readUsableSessionState(cwd, options = {}) {
|
|
|
400
790
|
return null;
|
|
401
791
|
}
|
|
402
792
|
}
|
|
403
|
-
function createSessionState(cwd, sessionId, pid, platform,
|
|
793
|
+
function createSessionState(cwd, stateRoot, sessionId, pid, platform, identity, options = {}) {
|
|
404
794
|
const nowIso = options.nowIso ?? new Date().toISOString();
|
|
405
795
|
const nativeSessionId = normalizeNonempty(options.nativeSessionId);
|
|
406
796
|
const previousNativeSessionId = normalizeNonempty(options.previousNativeSessionId);
|
|
@@ -408,6 +798,27 @@ function createSessionState(cwd, sessionId, pid, platform, linuxIdentity, option
|
|
|
408
798
|
const ownerOmxSessionId = normalizeSessionId(options.ownerOmxSessionId);
|
|
409
799
|
const tmuxSessionName = normalizeNonempty(options.tmuxSessionName);
|
|
410
800
|
const tmuxPaneId = normalizeNonempty(options.tmuxPaneId);
|
|
801
|
+
const processIdentity = identity && 'birth' in identity
|
|
802
|
+
? identity
|
|
803
|
+
: identity
|
|
804
|
+
? {
|
|
805
|
+
platform: 'linux',
|
|
806
|
+
birth: String(identity.startTicks),
|
|
807
|
+
...(hashCmdline(identity.cmdline) ? { cmdline_hash: hashCmdline(identity.cmdline) } : {}),
|
|
808
|
+
}
|
|
809
|
+
: undefined;
|
|
810
|
+
const publishIdentity = processIdentity && isValidProcessIdentity(processIdentity) && processIdentity.platform === platform
|
|
811
|
+
? processIdentity
|
|
812
|
+
: undefined;
|
|
813
|
+
const legacyStartTicks = identity && 'startTicks' in identity
|
|
814
|
+
? identity.startTicks
|
|
815
|
+
: publishIdentity?.platform === 'linux' && Number.isSafeInteger(Number(publishIdentity.birth))
|
|
816
|
+
? Number(publishIdentity.birth)
|
|
817
|
+
: undefined;
|
|
818
|
+
const legacyLinuxIdentity = publishIdentity?.platform === 'linux' && publishIdentity.birth === String(legacyStartTicks)
|
|
819
|
+
? readLinuxProcessIdentity(pid)
|
|
820
|
+
: null;
|
|
821
|
+
const legacyCmdline = identity && 'cmdline' in identity ? identity.cmdline : legacyLinuxIdentity?.cmdline;
|
|
411
822
|
return {
|
|
412
823
|
session_id: sessionId,
|
|
413
824
|
...(nativeSessionId ? { native_session_id: nativeSessionId } : {}),
|
|
@@ -415,15 +826,23 @@ function createSessionState(cwd, sessionId, pid, platform, linuxIdentity, option
|
|
|
415
826
|
...(nativeSessionSwitchedAt ? { native_session_switched_at: nativeSessionSwitchedAt } : {}),
|
|
416
827
|
...(ownerOmxSessionId ? { owner_omx_session_id: ownerOmxSessionId } : {}),
|
|
417
828
|
started_at: options.startedAt ?? nowIso,
|
|
829
|
+
...(isValidToken(options.launchLineageToken) ? { launch_lineage_token: options.launchLineageToken } : {}),
|
|
418
830
|
cwd,
|
|
831
|
+
state_root: stateRoot,
|
|
419
832
|
pid,
|
|
420
833
|
platform,
|
|
421
|
-
...(
|
|
422
|
-
...(
|
|
834
|
+
...(publishIdentity ? { identity_schema_version: 2, process_identity: publishIdentity } : {}),
|
|
835
|
+
...(legacyStartTicks !== undefined ? { pid_start_ticks: legacyStartTicks } : {}),
|
|
836
|
+
...(legacyCmdline ? { pid_cmdline: legacyCmdline } : {}),
|
|
423
837
|
...(tmuxSessionName ? { tmux_session_name: tmuxSessionName } : {}),
|
|
424
838
|
...(tmuxPaneId ? { tmux_pane_id: tmuxPaneId } : {}),
|
|
425
839
|
};
|
|
426
840
|
}
|
|
841
|
+
function preserveExistingLaunchLineageToken(existing, state) {
|
|
842
|
+
return existing && Object.hasOwn(existing, 'launch_lineage_token')
|
|
843
|
+
? { ...state, launch_lineage_token: existing.launch_lineage_token }
|
|
844
|
+
: state;
|
|
845
|
+
}
|
|
427
846
|
function normalizeNonempty(value) {
|
|
428
847
|
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
429
848
|
}
|
|
@@ -431,7 +850,15 @@ function resolvePid(options) {
|
|
|
431
850
|
return Number.isInteger(options.pid) && options.pid && options.pid > 0 ? options.pid : process.pid;
|
|
432
851
|
}
|
|
433
852
|
function sessionIdentityFor(pid, platform) {
|
|
434
|
-
|
|
853
|
+
try {
|
|
854
|
+
const observation = transactionDependencies.observeProcess(pid, platform);
|
|
855
|
+
return observation.kind === 'identity' && isValidProcessIdentity(observation.identity) && observation.identity.platform === platform
|
|
856
|
+
? observation.identity
|
|
857
|
+
: null;
|
|
858
|
+
}
|
|
859
|
+
catch {
|
|
860
|
+
return null;
|
|
861
|
+
}
|
|
435
862
|
}
|
|
436
863
|
function currentOwnerAlias(state) {
|
|
437
864
|
return normalizeSessionId(state.owner_omx_session_id);
|
|
@@ -458,29 +885,38 @@ function isStartCompatible(existing, requestedSessionId) {
|
|
|
458
885
|
|| existing.native_session_id === requestedSessionId
|
|
459
886
|
|| currentOwnerAlias(existing) === requestedSessionId;
|
|
460
887
|
}
|
|
461
|
-
function getOmxLaunchSessionId(state) {
|
|
462
|
-
if (state.session_id.startsWith('omx-'))
|
|
463
|
-
return state.session_id;
|
|
464
|
-
const owner = currentOwnerAlias(state);
|
|
465
|
-
return owner?.startsWith('omx-') ? owner : undefined;
|
|
466
|
-
}
|
|
467
888
|
function parseLockOwner(raw) {
|
|
468
889
|
try {
|
|
469
|
-
const
|
|
470
|
-
if (!
|
|
890
|
+
const parsed = JSON.parse(raw);
|
|
891
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
|
|
471
892
|
return null;
|
|
472
|
-
|
|
893
|
+
const value = parsed;
|
|
894
|
+
if (!isValidToken(value.token) || typeof value.pid !== 'number' || !Number.isInteger(value.pid) || value.pid <= 0)
|
|
473
895
|
return null;
|
|
474
|
-
if (
|
|
896
|
+
if (!isNodePlatform(value.platform) || typeof value.created_at !== 'string' || !value.created_at)
|
|
475
897
|
return null;
|
|
898
|
+
if (value.version === 1) {
|
|
899
|
+
if (value.pid_start_ticks !== undefined && !isValidStartTicks(value.pid_start_ticks))
|
|
900
|
+
return null;
|
|
901
|
+
if (value.pid_cmdline_hash !== undefined
|
|
902
|
+
&& (typeof value.pid_cmdline_hash !== 'string' || !SHA256_PATTERN.test(value.pid_cmdline_hash))) {
|
|
903
|
+
return null;
|
|
904
|
+
}
|
|
905
|
+
return value;
|
|
476
906
|
}
|
|
477
|
-
if (value.
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
907
|
+
if (value.version === 2) {
|
|
908
|
+
const processIdentity = value.process_identity;
|
|
909
|
+
if (!isValidProcessIdentity(processIdentity) || processIdentity.platform !== value.platform)
|
|
910
|
+
return null;
|
|
911
|
+
if (value.pid_start_ticks !== undefined && !isValidStartTicks(value.pid_start_ticks))
|
|
912
|
+
return null;
|
|
913
|
+
if (value.pid_cmdline_hash !== undefined
|
|
914
|
+
&& (typeof value.pid_cmdline_hash !== 'string' || !SHA256_PATTERN.test(value.pid_cmdline_hash))) {
|
|
915
|
+
return null;
|
|
916
|
+
}
|
|
917
|
+
return value;
|
|
482
918
|
}
|
|
483
|
-
return
|
|
919
|
+
return null;
|
|
484
920
|
}
|
|
485
921
|
catch {
|
|
486
922
|
return null;
|
|
@@ -489,6 +925,17 @@ function parseLockOwner(raw) {
|
|
|
489
925
|
function isValidToken(value) {
|
|
490
926
|
return typeof value === 'string' && SESSION_POINTER_TOKEN_PATTERN.test(value);
|
|
491
927
|
}
|
|
928
|
+
function recordedIdentityForLockOwner(owner) {
|
|
929
|
+
if (owner.version === 2)
|
|
930
|
+
return owner.process_identity;
|
|
931
|
+
if (owner.platform !== 'linux' || !isValidStartTicks(owner.pid_start_ticks))
|
|
932
|
+
return null;
|
|
933
|
+
return {
|
|
934
|
+
platform: 'linux',
|
|
935
|
+
birth: String(owner.pid_start_ticks),
|
|
936
|
+
...(owner.pid_cmdline_hash ? { cmdline_hash: owner.pid_cmdline_hash } : {}),
|
|
937
|
+
};
|
|
938
|
+
}
|
|
492
939
|
async function inspectLockOwnerFile(ownerPath, missingStatus = 'missing') {
|
|
493
940
|
let raw;
|
|
494
941
|
try {
|
|
@@ -500,38 +947,32 @@ async function inspectLockOwnerFile(ownerPath, missingStatus = 'missing') {
|
|
|
500
947
|
const owner = parseLockOwner(raw);
|
|
501
948
|
if (!owner)
|
|
502
949
|
return { status: 'malformed' };
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
}
|
|
521
|
-
catch {
|
|
522
|
-
return { status: 'identity-indeterminate', owner };
|
|
523
|
-
}
|
|
524
|
-
if (!liveIdentity || !isValidStartTicks(liveIdentity.startTicks)) {
|
|
950
|
+
const recorded = recordedIdentityForLockOwner(owner);
|
|
951
|
+
if (!recorded) {
|
|
952
|
+
let probe;
|
|
953
|
+
try {
|
|
954
|
+
probe = transactionDependencies.probePid(owner.pid);
|
|
955
|
+
}
|
|
956
|
+
catch {
|
|
957
|
+
return { status: 'identity-indeterminate', owner };
|
|
958
|
+
}
|
|
959
|
+
if (probe === 'dead')
|
|
960
|
+
return { status: 'dead', owner };
|
|
961
|
+
// Same read-vs-authority split as the pointer path: where the platform records no process-birth
|
|
962
|
+
// evidence, an alive pid is the strongest signal available and the lock is occupied. Reaping
|
|
963
|
+
// still requires a positively 'dead' owner (safeToRecover === 'dead'), so nothing becomes
|
|
964
|
+
// reclaimable here; only "is this lock held" answers correctly.
|
|
965
|
+
if (probe === 'alive' && isUnobservableLivePointerState(owner))
|
|
966
|
+
return { status: 'live', owner };
|
|
525
967
|
return { status: 'identity-indeterminate', owner };
|
|
526
968
|
}
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
return { status: '
|
|
531
|
-
|
|
532
|
-
return { status: 'identity-indeterminate', owner };
|
|
969
|
+
const classification = classifyRecordedIdentity(recorded, transactionDependencies.runtimePlatform, transactionDependencies, owner.pid);
|
|
970
|
+
switch (classification.status) {
|
|
971
|
+
case 'match': return { status: 'live', owner };
|
|
972
|
+
case 'gone': return { status: 'dead', owner };
|
|
973
|
+
case 'birth-mismatch': return { status: 'reused', owner };
|
|
974
|
+
case 'identity-unavailable': return { status: 'identity-indeterminate', owner };
|
|
533
975
|
}
|
|
534
|
-
return { status: 'live', owner };
|
|
535
976
|
}
|
|
536
977
|
async function inspectLockOwner(lockPath) {
|
|
537
978
|
return await inspectLockOwnerFile(join(lockPath, 'owner.json'));
|
|
@@ -586,124 +1027,674 @@ async function inspectSessionPointerLockAtContext(context) {
|
|
|
586
1027
|
status,
|
|
587
1028
|
lockPath: context.lockPath,
|
|
588
1029
|
evidenceSource,
|
|
589
|
-
|
|
1030
|
+
// Recoverable only on positively dead evidence (ESRCH), for canonical
|
|
1031
|
+
// owner.json and pre-rename temp evidence alike; the atomic
|
|
1032
|
+
// claim/quarantine protocol revalidates the exact entry, still-dead
|
|
1033
|
+
// status, and owner token before quarantining. Live, reused,
|
|
1034
|
+
// identity-indeterminate, ambiguous, symlink, malformed, unexpected,
|
|
1035
|
+
// and io-error states stay fail-closed.
|
|
1036
|
+
safeToRecover: status === 'dead',
|
|
590
1037
|
};
|
|
591
1038
|
}
|
|
592
1039
|
/** Inspect only exact, regular owner evidence in the selected pointer lock. */
|
|
593
1040
|
export async function inspectSessionPointerLock(cwd) {
|
|
594
1041
|
return await inspectSessionPointerLockAtContext(resolveSessionPointerContext(cwd));
|
|
595
1042
|
}
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
return
|
|
1043
|
+
function sameRecoveryIdentity(stat, identity, kind) {
|
|
1044
|
+
return !stat.isSymbolicLink() && (kind === 'file' ? stat.isFile() : stat.isDirectory()) && stat.dev === identity.dev && stat.ino === identity.ino;
|
|
1045
|
+
}
|
|
1046
|
+
async function lstatRecoveryPath(path) {
|
|
1047
|
+
try {
|
|
1048
|
+
return await transactionDependencies.fs.lstat(path);
|
|
602
1049
|
}
|
|
603
|
-
|
|
1050
|
+
catch (error) {
|
|
1051
|
+
if (isNotFound(error))
|
|
1052
|
+
return undefined;
|
|
1053
|
+
throw error;
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
async function probeRecoveryRenameCapability() {
|
|
1057
|
+
let probeDir;
|
|
604
1058
|
try {
|
|
605
|
-
|
|
1059
|
+
probeDir = await nodeMkdtemp(join(tmpdir(), 'omx-session-recovery-probe-'));
|
|
1060
|
+
const from = join(probeDir, 'from');
|
|
1061
|
+
const to = join(probeDir, 'to');
|
|
1062
|
+
await nodeWriteFile(from, 'omx-recovery-capability-probe', { flag: 'wx' });
|
|
1063
|
+
const outcome = await transactionDependencies.atomicRenameNoReplace(from, to);
|
|
1064
|
+
return outcome === 'moved' || outcome === 'not-moved' || outcome === 'unsupported'
|
|
1065
|
+
? outcome
|
|
1066
|
+
: 'unsupported';
|
|
606
1067
|
}
|
|
607
1068
|
catch {
|
|
608
|
-
return
|
|
1069
|
+
return 'unsupported';
|
|
609
1070
|
}
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
1071
|
+
finally {
|
|
1072
|
+
if (probeDir) {
|
|
1073
|
+
try {
|
|
1074
|
+
await rm(probeDir, { recursive: true, force: true });
|
|
1075
|
+
}
|
|
1076
|
+
catch { /* best effort */ }
|
|
1077
|
+
}
|
|
613
1078
|
}
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
1079
|
+
}
|
|
1080
|
+
async function moveRecoveryPathNoReplace(from, to, identity, kind, onAtomicMove) {
|
|
1081
|
+
try {
|
|
1082
|
+
const outcome = await transactionDependencies.atomicRenameNoReplace(from, to);
|
|
1083
|
+
if (outcome === 'unsupported')
|
|
1084
|
+
return { moved: false, unsupported: true, reason: 'Atomic no-replace recovery rename is unsupported on this platform.' };
|
|
1085
|
+
if (outcome === 'not-moved')
|
|
1086
|
+
return { moved: false, reason: 'Atomic recovery move left its source pathname in place.' };
|
|
1087
|
+
onAtomicMove?.();
|
|
617
1088
|
}
|
|
618
|
-
|
|
1089
|
+
catch (error) {
|
|
1090
|
+
return { moved: false, reason: `Atomic no-replace recovery rename failed (${errorCode(error) ?? 'unknown'}).` };
|
|
1091
|
+
}
|
|
1092
|
+
const destination = await lstatRecoveryPath(to);
|
|
1093
|
+
const source = await lstatRecoveryPath(from);
|
|
1094
|
+
if (destination && sameRecoveryIdentity(destination, identity, kind) && !source)
|
|
1095
|
+
return { moved: true };
|
|
1096
|
+
// A successor may have appeared at the canonical pathname after our move.
|
|
1097
|
+
// Attempt a no-replace rollback only for the captured object; an occupied or
|
|
1098
|
+
// foreign destination makes the rollback fail closed and leaves both residues.
|
|
1099
|
+
if (destination && sameRecoveryIdentity(destination, identity, kind)) {
|
|
1100
|
+
try {
|
|
1101
|
+
await transactionDependencies.atomicRenameNoReplace(to, from);
|
|
1102
|
+
}
|
|
1103
|
+
catch { /* durable residue is safer than cleanup */ }
|
|
1104
|
+
}
|
|
1105
|
+
return { moved: false, reason: 'Atomic recovery move did not leave the captured object at its token-bound destination.' };
|
|
1106
|
+
}
|
|
1107
|
+
async function completeRecoveryCheckpoint(checkpointPath, checkpointBytes, checkpointIdentity) {
|
|
1108
|
+
const completedPath = checkpointPath.replace(/\.json$/, '.completed');
|
|
1109
|
+
const current = await lstatRecoveryPath(checkpointPath);
|
|
1110
|
+
if (!current || !sameRecoveryIdentity(current, checkpointIdentity, 'file') || await transactionDependencies.fs.readFile(checkpointPath, 'utf8') !== checkpointBytes) {
|
|
1111
|
+
return { completed: false, reason: 'Recovery checkpoint was replaced before completion; it was preserved as residue.' };
|
|
1112
|
+
}
|
|
1113
|
+
const moved = await moveRecoveryPathNoReplace(checkpointPath, completedPath, checkpointIdentity, 'file');
|
|
1114
|
+
if (!moved.moved)
|
|
1115
|
+
return { completed: false, reason: moved.reason };
|
|
1116
|
+
const completedBytes = await transactionDependencies.fs.readFile(completedPath, 'utf8');
|
|
1117
|
+
return completedBytes === checkpointBytes ? { completed: true } : { completed: false, reason: 'Completed recovery receipt bytes changed and were preserved as residue.' };
|
|
1118
|
+
}
|
|
1119
|
+
async function resumeRecoveryCheckpoint(context, checkpointPath, checkpointName) {
|
|
1120
|
+
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);
|
|
619
1121
|
try {
|
|
620
|
-
|
|
1122
|
+
const checkpointBytes = await transactionDependencies.fs.readFile(checkpointPath, 'utf8');
|
|
1123
|
+
const checkpointStat = await transactionDependencies.fs.lstat(checkpointPath);
|
|
1124
|
+
const checkpoint = JSON.parse(checkpointBytes);
|
|
1125
|
+
const ownerToken = match?.[1];
|
|
1126
|
+
const recoveryToken = match?.[2];
|
|
1127
|
+
const expectedSource = ownerToken && (checkpoint.sourcePath === join(context.lockPath, 'owner.json') || checkpoint.sourcePath === join(context.lockPath, `owner.${ownerToken}.tmp`));
|
|
1128
|
+
const expectedPark = `${context.lockPath}.parked-lock-${recoveryToken}`;
|
|
1129
|
+
const isLegacyV1 = checkpoint.version === 1 && checkpoint.lockIdentity === undefined && checkpoint.lockParkPath === undefined;
|
|
1130
|
+
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'))
|
|
1131
|
+
throw new Error('invalid checkpoint');
|
|
1132
|
+
const lockParkPath = checkpoint.lockParkPath ?? expectedPark;
|
|
1133
|
+
const evidenceIdentity = checkpoint.evidenceIdentity ?? checkpoint.identity;
|
|
1134
|
+
const claimPath = join(context.lockPath, `owner.${ownerToken}.${recoveryToken}.recovery`);
|
|
1135
|
+
const lock = await lstatRecoveryPath(context.lockPath);
|
|
1136
|
+
const parkedLock = await lstatRecoveryPath(lockParkPath);
|
|
1137
|
+
if (lock && parkedLock)
|
|
1138
|
+
throw new Error('checkpoint lock paths are both present');
|
|
1139
|
+
if (parkedLock) {
|
|
1140
|
+
if (!checkpoint.lockIdentity || !sameRecoveryIdentity(parkedLock, checkpoint.lockIdentity, 'directory'))
|
|
1141
|
+
throw new Error('checkpoint parked lock identity mismatch');
|
|
1142
|
+
const quarantinePath = `${context.lockPath}.quarantine.${ownerToken}.${recoveryToken}`;
|
|
1143
|
+
const completed = await completeRecoveryCheckpoint(checkpointPath, checkpointBytes, { dev: checkpointStat.dev, ino: checkpointStat.ino });
|
|
1144
|
+
if (!completed.completed)
|
|
1145
|
+
throw new Error(completed.reason);
|
|
1146
|
+
return { status: 'dead', lockPath: context.lockPath, evidenceSource: 'owner.json', safeToRecover: true, action: 'quarantined', recovered: true, reason: 'Dead session pointer lock recovery checkpoint resumed.', quarantinePath };
|
|
1147
|
+
}
|
|
1148
|
+
if (!lock || lock.isSymbolicLink() || !lock.isDirectory())
|
|
1149
|
+
throw new Error('checkpoint lock is missing or foreign');
|
|
1150
|
+
const source = await lstatRecoveryPath(checkpoint.sourcePath);
|
|
1151
|
+
const claim = await lstatRecoveryPath(claimPath);
|
|
1152
|
+
const evidencePath = source && sameRecoveryIdentity(source, evidenceIdentity, 'file') ? checkpoint.sourcePath
|
|
1153
|
+
: claim && sameRecoveryIdentity(claim, evidenceIdentity, 'file') ? claimPath : undefined;
|
|
1154
|
+
if (!evidencePath)
|
|
1155
|
+
throw new Error('checkpoint evidence is missing or foreign');
|
|
1156
|
+
if (claim && !sameRecoveryIdentity(claim, evidenceIdentity, 'file'))
|
|
1157
|
+
throw new Error('checkpoint claim is foreign');
|
|
1158
|
+
// v1/v2 did not persist the directory identity. It is safe to derive only
|
|
1159
|
+
// while the original directory still contains the exact recorded evidence.
|
|
1160
|
+
const lockIdentity = checkpoint.lockIdentity ?? { dev: lock.dev, ino: lock.ino };
|
|
1161
|
+
if (!sameRecoveryIdentity(lock, lockIdentity, 'directory'))
|
|
1162
|
+
throw new Error('checkpoint lock identity mismatch');
|
|
1163
|
+
const ownerBytes = await transactionDependencies.fs.readFile(evidencePath, 'utf8');
|
|
1164
|
+
const owner = await inspectLockOwnerFile(evidencePath);
|
|
1165
|
+
if (owner.status !== 'dead' || owner.owner?.token !== ownerToken || checkpoint.evidenceBytes !== undefined && checkpoint.evidenceBytes !== ownerBytes)
|
|
1166
|
+
throw new Error('checkpoint owner evidence changed');
|
|
1167
|
+
const quarantinePath = `${context.lockPath}.quarantine.${ownerToken}.${recoveryToken}`;
|
|
1168
|
+
const quarantine = await lstatRecoveryPath(quarantinePath);
|
|
1169
|
+
if (quarantine) {
|
|
1170
|
+
if (!sameRecoveryIdentity(quarantine, evidenceIdentity, 'file'))
|
|
1171
|
+
throw new Error('checkpoint quarantine is foreign');
|
|
1172
|
+
}
|
|
1173
|
+
else {
|
|
1174
|
+
await transactionDependencies.fs.link(evidencePath, quarantinePath);
|
|
1175
|
+
const linked = await lstatRecoveryPath(quarantinePath);
|
|
1176
|
+
if (!linked || !sameRecoveryIdentity(linked, evidenceIdentity, 'file'))
|
|
1177
|
+
throw new Error('checkpoint quarantine link mismatch');
|
|
1178
|
+
}
|
|
1179
|
+
// Revalidate the bytes, dead owner and identities immediately before moving
|
|
1180
|
+
// the entire directory incarnation out of the canonical lock pathname.
|
|
1181
|
+
const currentLock = await lstatRecoveryPath(context.lockPath);
|
|
1182
|
+
const currentEvidence = await lstatRecoveryPath(evidencePath);
|
|
1183
|
+
const currentBytes = currentEvidence && sameRecoveryIdentity(currentEvidence, evidenceIdentity, 'file') ? await transactionDependencies.fs.readFile(evidencePath, 'utf8') : undefined;
|
|
1184
|
+
const currentOwner = currentBytes === undefined ? undefined : await inspectLockOwnerFile(evidencePath);
|
|
1185
|
+
if (!currentLock || !sameRecoveryIdentity(currentLock, lockIdentity, 'directory') || currentBytes !== ownerBytes || currentOwner?.status !== 'dead' || currentOwner.owner?.token !== ownerToken)
|
|
1186
|
+
throw new Error('checkpoint owner evidence changed before directory quarantine');
|
|
1187
|
+
const lockEntries = await transactionDependencies.fs.readdir(context.lockPath);
|
|
1188
|
+
const allowedEntries = new Set([basename(checkpoint.sourcePath), basename(claimPath)]);
|
|
1189
|
+
if (lockEntries.some((entry) => !allowedEntries.has(entry)))
|
|
1190
|
+
throw new Error('checkpoint lock contains foreign recovery evidence');
|
|
1191
|
+
const moved = await moveRecoveryPathNoReplace(context.lockPath, lockParkPath, lockIdentity, 'directory');
|
|
1192
|
+
if (!moved.moved)
|
|
1193
|
+
throw new Error(moved.reason);
|
|
1194
|
+
const completed = await completeRecoveryCheckpoint(checkpointPath, checkpointBytes, { dev: checkpointStat.dev, ino: checkpointStat.ino });
|
|
1195
|
+
if (!completed.completed)
|
|
1196
|
+
throw new Error(completed.reason);
|
|
1197
|
+
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 };
|
|
621
1198
|
}
|
|
622
|
-
catch {
|
|
623
|
-
|
|
1199
|
+
catch (error) {
|
|
1200
|
+
const detail = error instanceof Error ? error.message : 'unknown';
|
|
1201
|
+
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}).` };
|
|
624
1202
|
}
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
const
|
|
1203
|
+
}
|
|
1204
|
+
/** Explicitly quarantine only a dead lock by moving its entire directory incarnation. */
|
|
1205
|
+
export async function recoverSessionPointerLock(cwd) {
|
|
1206
|
+
const context = resolveSessionPointerContext(cwd);
|
|
1207
|
+
const checkpointPrefix = `${basename(context.lockPath)}.recovery.`;
|
|
1208
|
+
let checkpointNames;
|
|
629
1209
|
try {
|
|
630
|
-
await transactionDependencies.fs.
|
|
631
|
-
return { ...inspection, action: 'none', recovered: false, reason: 'Recovery claim path already exists.' };
|
|
1210
|
+
checkpointNames = (await transactionDependencies.fs.readdir(dirname(context.lockPath))).filter((name) => name.startsWith(checkpointPrefix) && name.endsWith('.json'));
|
|
632
1211
|
}
|
|
633
1212
|
catch (error) {
|
|
634
1213
|
if (!isNotFound(error))
|
|
635
|
-
return {
|
|
1214
|
+
return { status: 'io-error', lockPath: context.lockPath, evidenceSource: 'none', safeToRecover: false, action: 'none', recovered: false, reason: 'Unable to enumerate session pointer recovery checkpoints.' };
|
|
1215
|
+
checkpointNames = [];
|
|
1216
|
+
}
|
|
1217
|
+
if (checkpointNames.length > 1)
|
|
1218
|
+
return { status: 'unexpected', lockPath: context.lockPath, evidenceSource: 'none', safeToRecover: false, action: 'none', recovered: false, reason: 'Multiple recovery checkpoints exist.' };
|
|
1219
|
+
if (checkpointNames.length === 1)
|
|
1220
|
+
return await resumeRecoveryCheckpoint(context, join(dirname(context.lockPath), checkpointNames[0]), checkpointNames[0]);
|
|
1221
|
+
const inspection = await inspectSessionPointerLockAtContext(context);
|
|
1222
|
+
if (!inspection.safeToRecover)
|
|
1223
|
+
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}).` };
|
|
1224
|
+
try {
|
|
1225
|
+
const evidenceName = inspection.evidenceSource === 'owner.json'
|
|
1226
|
+
? 'owner.json'
|
|
1227
|
+
: (await transactionDependencies.fs.readdir(context.lockPath)).find((entry) => /^owner\.([A-Za-z0-9_-]{16,128})\.tmp$/.test(entry));
|
|
1228
|
+
if (!evidenceName)
|
|
1229
|
+
return { ...inspection, action: 'none', recovered: false, reason: 'Session pointer lock evidence changed before recovery claim.' };
|
|
1230
|
+
const evidencePath = join(context.lockPath, evidenceName);
|
|
1231
|
+
const evidence = await lstatRecoveryPath(evidencePath);
|
|
1232
|
+
const lock = await lstatRecoveryPath(context.lockPath);
|
|
1233
|
+
const ownerBytes = evidence && sameRecoveryIdentity(evidence, { dev: evidence.dev, ino: evidence.ino }, 'file')
|
|
1234
|
+
? await transactionDependencies.fs.readFile(evidencePath, 'utf8')
|
|
1235
|
+
: undefined;
|
|
1236
|
+
const owner = ownerBytes === undefined ? undefined : await inspectLockOwnerFile(evidencePath);
|
|
1237
|
+
if (!lock || lock.isSymbolicLink() || !lock.isDirectory() || !evidence
|
|
1238
|
+
|| !sameRecoveryIdentity(evidence, { dev: evidence.dev, ino: evidence.ino }, 'file')
|
|
1239
|
+
|| owner?.status !== 'dead' || !owner.owner) {
|
|
1240
|
+
return { ...inspection, action: 'none', recovered: false, reason: 'Session pointer lock evidence changed before recovery claim.' };
|
|
1241
|
+
}
|
|
1242
|
+
// Fresh recovery probes before checkpoint creation; an unsupported host leaves the
|
|
1243
|
+
// canonical lock and owner evidence untouched. Existing checkpoints are resumed
|
|
1244
|
+
// independently and may remain as durable recovery residue.
|
|
1245
|
+
const capability = await probeRecoveryRenameCapability();
|
|
1246
|
+
if (capability === 'unsupported') {
|
|
1247
|
+
return {
|
|
1248
|
+
...inspection,
|
|
1249
|
+
action: 'none',
|
|
1250
|
+
recovered: false,
|
|
1251
|
+
reason: 'Atomic no-replace recovery rename is unsupported on this platform.',
|
|
1252
|
+
};
|
|
1253
|
+
}
|
|
1254
|
+
const recoveryToken = transactionDependencies.token();
|
|
1255
|
+
if (!isValidToken(recoveryToken))
|
|
1256
|
+
return { ...inspection, action: 'none', recovered: false, reason: 'Recovery claim token is invalid.' };
|
|
1257
|
+
const checkpointPath = `${context.lockPath}.recovery.${owner.owner.token}.${recoveryToken}.json`;
|
|
1258
|
+
const checkpoint = {
|
|
1259
|
+
version: 3,
|
|
1260
|
+
sourcePath: evidencePath,
|
|
1261
|
+
identity: { dev: evidence.dev, ino: evidence.ino },
|
|
1262
|
+
evidenceIdentity: { dev: evidence.dev, ino: evidence.ino },
|
|
1263
|
+
evidenceBytes: ownerBytes,
|
|
1264
|
+
lockIdentity: { dev: lock.dev, ino: lock.ino },
|
|
1265
|
+
lockParkPath: `${context.lockPath}.parked-lock-${recoveryToken}`,
|
|
1266
|
+
phase: 'evidence-pending',
|
|
1267
|
+
};
|
|
1268
|
+
try {
|
|
1269
|
+
await transactionDependencies.fs.writeFile(checkpointPath, JSON.stringify(checkpoint), { flag: 'wx' });
|
|
1270
|
+
}
|
|
1271
|
+
catch (error) {
|
|
1272
|
+
return { ...inspection, action: 'none', recovered: false, reason: `Unable to create recovery checkpoint (${errorCode(error) ?? 'unknown'}).` };
|
|
1273
|
+
}
|
|
1274
|
+
return await resumeRecoveryCheckpoint(context, checkpointPath, basename(checkpointPath));
|
|
1275
|
+
}
|
|
1276
|
+
catch (error) {
|
|
1277
|
+
return {
|
|
1278
|
+
...inspection,
|
|
1279
|
+
status: 'io-error',
|
|
1280
|
+
safeToRecover: false,
|
|
1281
|
+
action: 'none',
|
|
1282
|
+
recovered: false,
|
|
1283
|
+
reason: `Unable to inspect session pointer lock recovery state (${errorCode(error) ?? errorMessage(error)}).`,
|
|
1284
|
+
};
|
|
636
1285
|
}
|
|
1286
|
+
}
|
|
1287
|
+
function selectedRootAuthorityFailure(context, state) {
|
|
1288
|
+
let recordedCwd;
|
|
1289
|
+
let observedCwd;
|
|
637
1290
|
try {
|
|
638
|
-
|
|
1291
|
+
if (typeof state.cwd !== 'string' || !state.cwd.trim())
|
|
1292
|
+
return 'foreign-cwd';
|
|
1293
|
+
recordedCwd = realpathSync(resolve(state.cwd));
|
|
1294
|
+
observedCwd = realpathSync(resolve(context.cwd));
|
|
1295
|
+
const cwdRelative = relative(recordedCwd, observedCwd);
|
|
1296
|
+
if (cwdRelative === '..' || cwdRelative.startsWith(`..${sep}`) || isAbsolute(cwdRelative))
|
|
1297
|
+
return 'foreign-cwd';
|
|
639
1298
|
}
|
|
640
1299
|
catch {
|
|
641
|
-
return
|
|
1300
|
+
return 'foreign-cwd';
|
|
642
1301
|
}
|
|
643
|
-
const claimed = await inspectSessionPointerLockAtContext(context);
|
|
644
|
-
let claimedEntries;
|
|
645
1302
|
try {
|
|
646
|
-
|
|
1303
|
+
const recordedRoot = typeof state.state_root === 'string' && state.state_root.trim()
|
|
1304
|
+
? state.state_root
|
|
1305
|
+
: join(recordedCwd, '.omx', 'state');
|
|
1306
|
+
return sameFilePath(recordedRoot, context.baseStateDir) ? undefined : 'foreign-root';
|
|
647
1307
|
}
|
|
648
1308
|
catch {
|
|
649
|
-
return
|
|
1309
|
+
return 'foreign-root';
|
|
1310
|
+
}
|
|
1311
|
+
}
|
|
1312
|
+
function classifyParsedSessionPointerForRecovery(value, raw) {
|
|
1313
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
1314
|
+
return { status: 'malformed', raw };
|
|
1315
|
+
const state = value;
|
|
1316
|
+
if (!normalizeSessionId(state.session_id))
|
|
1317
|
+
return { status: 'malformed', raw };
|
|
1318
|
+
if (state.identity_schema_version !== undefined && state.identity_schema_version !== 2) {
|
|
1319
|
+
return { status: 'identity-indeterminate', raw, state };
|
|
650
1320
|
}
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
1321
|
+
if (state.process_identity !== undefined
|
|
1322
|
+
&& (state.identity_schema_version !== 2
|
|
1323
|
+
|| !isValidProcessIdentity(state.process_identity)
|
|
1324
|
+
|| state.platform !== undefined && state.platform !== state.process_identity.platform)) {
|
|
1325
|
+
return { status: 'malformed', raw, state };
|
|
654
1326
|
}
|
|
655
|
-
|
|
1327
|
+
if (state.identity_schema_version === 2 && state.process_identity === undefined)
|
|
1328
|
+
return { status: 'malformed', raw, state };
|
|
1329
|
+
return { status: classifySessionProcess(state, transactionDependencies), raw, state };
|
|
1330
|
+
}
|
|
1331
|
+
async function readSessionPointerForRecovery(context) {
|
|
1332
|
+
let sourceStat;
|
|
656
1333
|
try {
|
|
657
|
-
await transactionDependencies.fs.lstat(
|
|
658
|
-
return { ...claimed, action: 'none', recovered: false, reason: 'Recovery quarantine path already exists.' };
|
|
1334
|
+
sourceStat = await transactionDependencies.fs.lstat(context.sessionPath);
|
|
659
1335
|
}
|
|
660
1336
|
catch (error) {
|
|
661
|
-
if (
|
|
662
|
-
return {
|
|
1337
|
+
if (isNotFound(error))
|
|
1338
|
+
return { status: 'absent' };
|
|
1339
|
+
throw error;
|
|
663
1340
|
}
|
|
1341
|
+
if (sourceStat.isSymbolicLink() || !sourceStat.isFile())
|
|
1342
|
+
return { status: 'malformed' };
|
|
1343
|
+
const bytes = await transactionDependencies.fs.readBytes(context.sessionPath);
|
|
1344
|
+
const sourceAfterRead = await transactionDependencies.fs.lstat(context.sessionPath);
|
|
1345
|
+
if (!sameRecoveryIdentity(sourceAfterRead, { dev: sourceStat.dev, ino: sourceStat.ino }, 'file')) {
|
|
1346
|
+
throw new Error('Selected session pointer changed while its recovery snapshot was read.');
|
|
1347
|
+
}
|
|
1348
|
+
const raw = bytes.toString('utf8');
|
|
664
1349
|
try {
|
|
665
|
-
|
|
1350
|
+
return { ...classifyParsedSessionPointerForRecovery(JSON.parse(raw), raw), bytes };
|
|
666
1351
|
}
|
|
667
|
-
catch {
|
|
668
|
-
|
|
1352
|
+
catch (error) {
|
|
1353
|
+
if (error instanceof SyntaxError)
|
|
1354
|
+
return { status: 'malformed', raw, bytes };
|
|
1355
|
+
throw error;
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
function malformedRecoveryPointerReason(state) {
|
|
1359
|
+
if (!normalizeSessionId(state.session_id))
|
|
1360
|
+
return 'session_id is missing or invalid';
|
|
1361
|
+
if (typeof state.started_at !== 'string' || !state.started_at.trim() || !Number.isFinite(Date.parse(state.started_at))) {
|
|
1362
|
+
return 'started_at is missing or invalid';
|
|
1363
|
+
}
|
|
1364
|
+
if (typeof state.cwd !== 'string' || !state.cwd.trim())
|
|
1365
|
+
return 'cwd is missing or invalid';
|
|
1366
|
+
if (!Number.isInteger(state.pid) || state.pid <= 0)
|
|
1367
|
+
return 'pid is missing or invalid';
|
|
1368
|
+
if (Object.prototype.hasOwnProperty.call(state, 'state_root')
|
|
1369
|
+
&& (typeof state.state_root !== 'string' || !state.state_root.trim()))
|
|
1370
|
+
return 'state_root is invalid';
|
|
1371
|
+
if (state.platform !== undefined && !['linux', 'darwin', 'win32'].includes(state.platform))
|
|
1372
|
+
return 'platform is invalid';
|
|
1373
|
+
if (state.pid_start_ticks !== undefined && !isValidStartTicks(state.pid_start_ticks))
|
|
1374
|
+
return 'pid_start_ticks is invalid';
|
|
1375
|
+
if (state.pid_cmdline !== undefined && typeof state.pid_cmdline !== 'string')
|
|
1376
|
+
return 'pid_cmdline is invalid';
|
|
1377
|
+
const optionalStrings = [
|
|
1378
|
+
'native_session_id',
|
|
1379
|
+
'previous_native_session_id',
|
|
1380
|
+
'native_session_switched_at',
|
|
1381
|
+
'owner_omx_session_id',
|
|
1382
|
+
'owner_codex_session_id',
|
|
1383
|
+
'codex_session_id',
|
|
1384
|
+
'tmux_session_name',
|
|
1385
|
+
'tmux_pane_id',
|
|
1386
|
+
];
|
|
1387
|
+
for (const field of optionalStrings) {
|
|
1388
|
+
const value = state[field];
|
|
1389
|
+
if (value !== undefined && (typeof value !== 'string' || !value.trim()))
|
|
1390
|
+
return `${field} is invalid`;
|
|
1391
|
+
}
|
|
1392
|
+
if (state.launch_lineage_token !== undefined && !isValidToken(state.launch_lineage_token)) {
|
|
1393
|
+
return 'launch_lineage_token is invalid';
|
|
669
1394
|
}
|
|
1395
|
+
if (recordedIdentityForState(state, transactionDependencies.runtimePlatform) === 'invalid')
|
|
1396
|
+
return 'process identity metadata is invalid';
|
|
1397
|
+
return undefined;
|
|
1398
|
+
}
|
|
1399
|
+
function classifySessionProcessForRecovery(state) {
|
|
1400
|
+
const hasPidMetadata = Number.isInteger(state.pid) && state.pid > 0;
|
|
1401
|
+
const hasIdentityMetadata = typeof state.pid_start_ticks === 'number'
|
|
1402
|
+
|| typeof state.pid_cmdline === 'string'
|
|
1403
|
+
|| state.identity_schema_version !== undefined
|
|
1404
|
+
|| state.process_identity !== undefined;
|
|
1405
|
+
if (!hasPidMetadata && !hasIdentityMetadata)
|
|
1406
|
+
return 'usable';
|
|
1407
|
+
if (!hasPidMetadata)
|
|
1408
|
+
return 'identity-indeterminate';
|
|
1409
|
+
const recordedPlatform = state.process_identity?.platform ?? state.platform;
|
|
1410
|
+
if (recordedPlatform !== undefined && recordedPlatform !== transactionDependencies.runtimePlatform) {
|
|
1411
|
+
return 'identity-indeterminate';
|
|
1412
|
+
}
|
|
1413
|
+
const recorded = recordedIdentityForState(state, transactionDependencies.runtimePlatform);
|
|
1414
|
+
if (recorded === 'invalid')
|
|
1415
|
+
return 'identity-indeterminate';
|
|
1416
|
+
if (!recorded) {
|
|
1417
|
+
let probe;
|
|
1418
|
+
try {
|
|
1419
|
+
probe = transactionDependencies.probePid(state.pid);
|
|
1420
|
+
}
|
|
1421
|
+
catch {
|
|
1422
|
+
return 'identity-indeterminate';
|
|
1423
|
+
}
|
|
1424
|
+
return probe === 'dead' ? 'dead' : 'identity-indeterminate';
|
|
1425
|
+
}
|
|
1426
|
+
const classification = classifyRecordedIdentity(recorded, transactionDependencies.runtimePlatform, transactionDependencies, state.pid);
|
|
1427
|
+
if (classification.status === 'gone')
|
|
1428
|
+
return 'dead';
|
|
1429
|
+
if (classification.status === 'birth-mismatch')
|
|
1430
|
+
return 'reused';
|
|
1431
|
+
if (classification.status === 'match')
|
|
1432
|
+
return 'usable';
|
|
1433
|
+
return 'identity-indeterminate';
|
|
1434
|
+
}
|
|
1435
|
+
function pointerRecoveryRefusal(context, status, reason, sessionId) {
|
|
1436
|
+
return {
|
|
1437
|
+
status,
|
|
1438
|
+
pointerPath: context.sessionPath,
|
|
1439
|
+
action: 'none',
|
|
1440
|
+
recovered: false,
|
|
1441
|
+
reason,
|
|
1442
|
+
...(sessionId ? { sessionId } : {}),
|
|
1443
|
+
};
|
|
1444
|
+
}
|
|
1445
|
+
/**
|
|
1446
|
+
* Explicitly quarantine one authoritative, positively dead selected pointer.
|
|
1447
|
+
* The canonical pointer lock serializes the final classification and move with
|
|
1448
|
+
* SessionStart/SessionEnd, while the no-replace archive preserves exact bytes.
|
|
1449
|
+
*/
|
|
1450
|
+
export async function recoverDeadSessionPointer(cwd) {
|
|
1451
|
+
const context = resolveSessionPointerContext(cwd);
|
|
670
1452
|
try {
|
|
671
|
-
await
|
|
1453
|
+
if ((await readSessionPointerForRecovery(context)).status === 'absent') {
|
|
1454
|
+
return pointerRecoveryRefusal(context, 'absent', 'No selected session pointer exists.');
|
|
1455
|
+
}
|
|
672
1456
|
}
|
|
673
1457
|
catch (error) {
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
1458
|
+
return pointerRecoveryRefusal(context, 'io-error', `Unable to read the selected session pointer (${errorCode(error) ?? errorMessage(error)}).`);
|
|
1459
|
+
}
|
|
1460
|
+
const tracker = { degraded: false };
|
|
1461
|
+
let lock;
|
|
1462
|
+
try {
|
|
1463
|
+
lock = await acquirePointerLock(context, undefined, DEFAULT_POINTER_TIMEOUT_MS, process.platform, tracker);
|
|
1464
|
+
}
|
|
1465
|
+
catch (error) {
|
|
1466
|
+
const reason = isSessionPointerLaunchAbort(error) ? error.reason : errorMessage(error);
|
|
1467
|
+
return pointerRecoveryRefusal(context, 'lock-unavailable', `Unable to acquire the selected pointer transaction lock (${reason}).`);
|
|
1468
|
+
}
|
|
1469
|
+
let result;
|
|
1470
|
+
try {
|
|
1471
|
+
let pointer;
|
|
1472
|
+
try {
|
|
1473
|
+
pointer = await readSessionPointerForRecovery(context);
|
|
1474
|
+
}
|
|
1475
|
+
catch (error) {
|
|
1476
|
+
result = pointerRecoveryRefusal(context, 'io-error', `Unable to read the selected session pointer (${errorCode(error) ?? errorMessage(error)}).`);
|
|
1477
|
+
return result;
|
|
1478
|
+
}
|
|
1479
|
+
if (pointer.status === 'absent') {
|
|
1480
|
+
result = pointerRecoveryRefusal(context, 'absent', 'No selected session pointer exists.');
|
|
1481
|
+
return result;
|
|
1482
|
+
}
|
|
1483
|
+
const sessionId = normalizeSessionId(pointer.state?.session_id);
|
|
1484
|
+
if (pointer.status !== 'stale-dead' || !pointer.state || !pointer.raw || !pointer.bytes || !sessionId) {
|
|
1485
|
+
const reason = pointer.status === 'usable'
|
|
1486
|
+
? 'The selected session pointer owner is usable or live.'
|
|
1487
|
+
: pointer.status === 'identity-indeterminate'
|
|
1488
|
+
? 'The selected session pointer owner identity is indeterminate.'
|
|
1489
|
+
: pointer.status === 'foreign-cwd'
|
|
1490
|
+
? 'The selected session pointer belongs to a different working directory.'
|
|
1491
|
+
: 'The selected session pointer is malformed.';
|
|
1492
|
+
const status = pointer.status === 'stale-dead' ? 'malformed' : pointer.status;
|
|
1493
|
+
result = pointerRecoveryRefusal(context, status, reason, sessionId);
|
|
1494
|
+
return result;
|
|
1495
|
+
}
|
|
1496
|
+
const malformedReason = malformedRecoveryPointerReason(pointer.state);
|
|
1497
|
+
if (malformedReason) {
|
|
1498
|
+
result = pointerRecoveryRefusal(context, 'malformed', `The selected session pointer is malformed (${malformedReason}).`, sessionId);
|
|
1499
|
+
return result;
|
|
1500
|
+
}
|
|
1501
|
+
const authorityFailure = selectedRootAuthorityFailure(context, pointer.state);
|
|
1502
|
+
if (authorityFailure) {
|
|
1503
|
+
const reason = authorityFailure === 'foreign-cwd'
|
|
1504
|
+
? 'The selected session pointer belongs to a different working-directory lineage.'
|
|
1505
|
+
: 'The selected session pointer does not have exact state_root authority for the selected root.';
|
|
1506
|
+
result = pointerRecoveryRefusal(context, authorityFailure, reason, sessionId);
|
|
1507
|
+
return result;
|
|
1508
|
+
}
|
|
1509
|
+
const recoveryLiveness = classifySessionProcessForRecovery(pointer.state);
|
|
1510
|
+
if (recoveryLiveness !== 'dead') {
|
|
1511
|
+
const status = recoveryLiveness === 'reused' ? 'reused' : recoveryLiveness;
|
|
1512
|
+
const reason = recoveryLiveness === 'reused'
|
|
1513
|
+
? 'The selected session pointer PID was reused or its recorded birth identity mismatched.'
|
|
1514
|
+
: recoveryLiveness === 'usable'
|
|
1515
|
+
? 'The selected session pointer owner is usable or live.'
|
|
1516
|
+
: 'The selected session pointer owner identity is indeterminate.';
|
|
1517
|
+
result = pointerRecoveryRefusal(context, status, reason, sessionId);
|
|
1518
|
+
return result;
|
|
1519
|
+
}
|
|
1520
|
+
let sourceStat;
|
|
1521
|
+
try {
|
|
1522
|
+
sourceStat = await transactionDependencies.fs.lstat(context.sessionPath);
|
|
1523
|
+
}
|
|
1524
|
+
catch (error) {
|
|
1525
|
+
result = pointerRecoveryRefusal(context, 'io-error', `Unable to inspect the selected session pointer (${errorCode(error) ?? errorMessage(error)}).`, sessionId);
|
|
1526
|
+
return result;
|
|
1527
|
+
}
|
|
1528
|
+
if (sourceStat.isSymbolicLink() || !sourceStat.isFile()) {
|
|
1529
|
+
result = pointerRecoveryRefusal(context, 'malformed', 'The selected session pointer is not a regular file.', sessionId);
|
|
1530
|
+
return result;
|
|
1531
|
+
}
|
|
1532
|
+
const recoveryToken = transactionDependencies.token();
|
|
1533
|
+
if (!isValidToken(recoveryToken)) {
|
|
1534
|
+
result = pointerRecoveryRefusal(context, 'io-error', 'Unable to create a valid selected pointer quarantine token.', sessionId);
|
|
1535
|
+
return result;
|
|
1536
|
+
}
|
|
1537
|
+
const quarantinePath = `${context.sessionPath}.quarantine.${sessionId}.${recoveryToken}`;
|
|
1538
|
+
let quarantineMoveCommitted = false;
|
|
1539
|
+
try {
|
|
1540
|
+
const collision = await lstatRecoveryPath(quarantinePath);
|
|
1541
|
+
if (collision) {
|
|
1542
|
+
result = pointerRecoveryRefusal(context, 'collision', 'The selected pointer quarantine destination already exists.', sessionId);
|
|
1543
|
+
return result;
|
|
1544
|
+
}
|
|
1545
|
+
const currentStat = await transactionDependencies.fs.lstat(context.sessionPath);
|
|
1546
|
+
const currentBytes = await transactionDependencies.fs.readBytes(context.sessionPath);
|
|
1547
|
+
const currentAfterRead = await transactionDependencies.fs.lstat(context.sessionPath);
|
|
1548
|
+
const currentRaw = currentBytes.toString('utf8');
|
|
1549
|
+
let current;
|
|
1550
|
+
try {
|
|
1551
|
+
current = classifyParsedSessionPointerForRecovery(JSON.parse(currentRaw), currentRaw);
|
|
1552
|
+
}
|
|
1553
|
+
catch {
|
|
1554
|
+
result = pointerRecoveryRefusal(context, 'race', 'The selected session pointer changed before quarantine.', sessionId);
|
|
1555
|
+
return result;
|
|
1556
|
+
}
|
|
1557
|
+
if (!sameRecoveryIdentity(currentStat, { dev: sourceStat.dev, ino: sourceStat.ino }, 'file')
|
|
1558
|
+
|| !sameRecoveryIdentity(currentAfterRead, { dev: sourceStat.dev, ino: sourceStat.ino }, 'file')
|
|
1559
|
+
|| !currentBytes.equals(pointer.bytes)
|
|
1560
|
+
|| current.status !== 'stale-dead'
|
|
1561
|
+
|| !current.state
|
|
1562
|
+
|| selectedRootAuthorityFailure(context, current.state) !== undefined
|
|
1563
|
+
|| classifySessionProcessForRecovery(current.state) !== 'dead') {
|
|
1564
|
+
result = pointerRecoveryRefusal(context, 'race', 'The selected session pointer changed before quarantine.', sessionId);
|
|
1565
|
+
return result;
|
|
1566
|
+
}
|
|
1567
|
+
const moved = await moveRecoveryPathNoReplace(context.sessionPath, quarantinePath, { dev: sourceStat.dev, ino: sourceStat.ino }, 'file', () => { quarantineMoveCommitted = true; });
|
|
1568
|
+
if (!moved.moved) {
|
|
1569
|
+
const destinationExists = await lstatRecoveryPath(quarantinePath);
|
|
1570
|
+
const sourceExists = await lstatRecoveryPath(context.sessionPath);
|
|
1571
|
+
const capturedDestination = destinationExists
|
|
1572
|
+
&& sameRecoveryIdentity(destinationExists, { dev: sourceStat.dev, ino: sourceStat.ino }, 'file');
|
|
1573
|
+
if (capturedDestination) {
|
|
1574
|
+
result = {
|
|
1575
|
+
status: 'recovery-required',
|
|
1576
|
+
pointerPath: context.sessionPath,
|
|
1577
|
+
action: 'quarantined',
|
|
1578
|
+
recovered: false,
|
|
1579
|
+
reason: sourceExists
|
|
1580
|
+
? 'The atomic move outcome was ambiguous; the captured selected pointer is present at quarantine and a successor occupies the canonical path, so both were preserved for explicit recovery.'
|
|
1581
|
+
: 'The atomic move outcome was ambiguous, but the captured selected pointer is present at quarantine; forensic residue was preserved for explicit recovery.',
|
|
1582
|
+
sessionId,
|
|
1583
|
+
quarantinePath,
|
|
1584
|
+
};
|
|
1585
|
+
return result;
|
|
1586
|
+
}
|
|
1587
|
+
const foreignDestination = destinationExists
|
|
1588
|
+
&& !capturedDestination;
|
|
1589
|
+
result = pointerRecoveryRefusal(context, moved.unsupported ? 'unsupported' : foreignDestination && !sourceExists ? 'race' : destinationExists ? 'collision' : 'race', moved.reason ?? 'The selected session pointer could not be quarantined atomically.', sessionId);
|
|
1590
|
+
return result;
|
|
1591
|
+
}
|
|
1592
|
+
const archivedStat = await lstatRecoveryPath(quarantinePath);
|
|
1593
|
+
const sourceAfterMove = await lstatRecoveryPath(context.sessionPath);
|
|
1594
|
+
const archivedBytes = archivedStat && sameRecoveryIdentity(archivedStat, { dev: sourceStat.dev, ino: sourceStat.ino }, 'file')
|
|
1595
|
+
? await transactionDependencies.fs.readBytes(quarantinePath)
|
|
1596
|
+
: undefined;
|
|
1597
|
+
const archivedRaw = archivedBytes?.toString('utf8');
|
|
1598
|
+
let archivedPointer;
|
|
1599
|
+
if (archivedRaw !== undefined) {
|
|
1600
|
+
try {
|
|
1601
|
+
archivedPointer = classifyParsedSessionPointerForRecovery(JSON.parse(archivedRaw), archivedRaw);
|
|
1602
|
+
}
|
|
1603
|
+
catch { /* handled below */ }
|
|
1604
|
+
}
|
|
1605
|
+
if (sourceAfterMove
|
|
1606
|
+
|| !archivedBytes?.equals(pointer.bytes)
|
|
1607
|
+
|| archivedPointer?.status !== 'stale-dead'
|
|
1608
|
+
|| !archivedPointer.state
|
|
1609
|
+
|| selectedRootAuthorityFailure(context, archivedPointer.state) !== undefined
|
|
1610
|
+
|| classifySessionProcessForRecovery(archivedPointer.state) !== 'dead') {
|
|
1611
|
+
if (!sourceAfterMove && archivedBytes?.equals(pointer.bytes) && archivedStat
|
|
1612
|
+
&& sameRecoveryIdentity(archivedStat, { dev: sourceStat.dev, ino: sourceStat.ino }, 'file')) {
|
|
1613
|
+
const rollback = await moveRecoveryPathNoReplace(quarantinePath, context.sessionPath, { dev: sourceStat.dev, ino: sourceStat.ino }, 'file');
|
|
1614
|
+
if (rollback.moved) {
|
|
1615
|
+
quarantineMoveCommitted = false;
|
|
1616
|
+
result = pointerRecoveryRefusal(context, 'race', 'The selected pointer became uncertain after quarantine and was restored without losing forensic bytes.', sessionId);
|
|
1617
|
+
return result;
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1620
|
+
result = {
|
|
1621
|
+
status: 'recovery-required',
|
|
1622
|
+
pointerPath: context.sessionPath,
|
|
1623
|
+
action: 'quarantined',
|
|
1624
|
+
recovered: false,
|
|
1625
|
+
reason: 'The selected pointer changed or became uncertain after quarantine; forensic residue was preserved for explicit recovery.',
|
|
1626
|
+
sessionId,
|
|
1627
|
+
quarantinePath,
|
|
1628
|
+
};
|
|
1629
|
+
return result;
|
|
1630
|
+
}
|
|
1631
|
+
result = {
|
|
1632
|
+
status: 'recovered',
|
|
1633
|
+
pointerPath: context.sessionPath,
|
|
1634
|
+
action: 'quarantined',
|
|
1635
|
+
recovered: true,
|
|
1636
|
+
reason: 'Verified-dead selected session pointer quarantined with exact forensic bytes preserved.',
|
|
1637
|
+
sessionId,
|
|
680
1638
|
quarantinePath,
|
|
681
1639
|
};
|
|
1640
|
+
return result;
|
|
1641
|
+
}
|
|
1642
|
+
catch (error) {
|
|
1643
|
+
if (quarantineMoveCommitted) {
|
|
1644
|
+
result = {
|
|
1645
|
+
status: 'recovery-required',
|
|
1646
|
+
pointerPath: context.sessionPath,
|
|
1647
|
+
action: 'quarantined',
|
|
1648
|
+
recovered: false,
|
|
1649
|
+
reason: `The selected pointer was quarantined, but post-move verification failed (${errorCode(error) ?? errorMessage(error)}); forensic residue was preserved for explicit recovery.`,
|
|
1650
|
+
sessionId,
|
|
1651
|
+
quarantinePath,
|
|
1652
|
+
};
|
|
1653
|
+
return result;
|
|
1654
|
+
}
|
|
1655
|
+
result = pointerRecoveryRefusal(context, 'io-error', `Unable to quarantine the selected session pointer (${errorCode(error) ?? errorMessage(error)}).`, sessionId);
|
|
1656
|
+
return result;
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
finally {
|
|
1660
|
+
const failures = await releasePointerLock(lock);
|
|
1661
|
+
if (failures.length > 0 && result) {
|
|
1662
|
+
result.status = 'recovery-required';
|
|
1663
|
+
result.recovered = false;
|
|
1664
|
+
result.reason = `${result.reason} Pointer lock release requires explicit lock recovery.`;
|
|
682
1665
|
}
|
|
683
1666
|
}
|
|
684
|
-
return { ...inspection, action: 'quarantined', recovered: true, reason: 'Dead session pointer lock was atomically claimed and quarantined.', quarantinePath };
|
|
685
1667
|
}
|
|
686
1668
|
function buildLockOwner(token) {
|
|
687
|
-
|
|
1669
|
+
const runtimePlatform = transactionDependencies.runtimePlatform;
|
|
1670
|
+
let observation;
|
|
688
1671
|
try {
|
|
689
|
-
|
|
1672
|
+
observation = transactionDependencies.observeProcess(process.pid, runtimePlatform);
|
|
690
1673
|
}
|
|
691
1674
|
catch {
|
|
692
1675
|
// Publication remains valid with optional identity metadata omitted.
|
|
693
1676
|
}
|
|
694
|
-
|
|
695
|
-
|
|
1677
|
+
const identity = observation?.kind === 'identity' && isValidProcessIdentity(observation.identity)
|
|
1678
|
+
&& observation.identity.platform === runtimePlatform
|
|
1679
|
+
? observation.identity
|
|
1680
|
+
: undefined;
|
|
1681
|
+
const legacyStartTicks = identity?.platform === 'linux' && Number.isSafeInteger(Number(identity.birth))
|
|
1682
|
+
? Number(identity.birth)
|
|
1683
|
+
: undefined;
|
|
1684
|
+
const legacyHash = identity?.cmdline_hash && SHA256_PATTERN.test(identity.cmdline_hash)
|
|
1685
|
+
? identity.cmdline_hash
|
|
1686
|
+
: undefined;
|
|
1687
|
+
const common = {
|
|
696
1688
|
token,
|
|
697
1689
|
pid: process.pid,
|
|
698
|
-
platform:
|
|
699
|
-
...(
|
|
700
|
-
|
|
701
|
-
: {}),
|
|
702
|
-
...(identity?.status === 'matching' && identity.cmdlineHash && SHA256_PATTERN.test(identity.cmdlineHash)
|
|
703
|
-
? { pid_cmdline_hash: identity.cmdlineHash }
|
|
704
|
-
: {}),
|
|
1690
|
+
platform: runtimePlatform,
|
|
1691
|
+
...(legacyStartTicks !== undefined ? { pid_start_ticks: legacyStartTicks } : {}),
|
|
1692
|
+
...(legacyHash ? { pid_cmdline_hash: legacyHash } : {}),
|
|
705
1693
|
created_at: new Date(transactionDependencies.nowMs()).toISOString(),
|
|
706
1694
|
};
|
|
1695
|
+
return identity
|
|
1696
|
+
? { version: 2, ...common, process_identity: identity }
|
|
1697
|
+
: { version: 1, ...common };
|
|
707
1698
|
}
|
|
708
1699
|
async function removeOwnedPath(path) {
|
|
709
1700
|
try {
|
|
@@ -942,7 +1933,7 @@ async function releasePointerLock(lock) {
|
|
|
942
1933
|
if (!isNotFound(error)) {
|
|
943
1934
|
return [{
|
|
944
1935
|
operation: 'lock-release',
|
|
945
|
-
phase: 'remove-release-
|
|
1936
|
+
phase: 'remove-release-owner',
|
|
946
1937
|
ownership: 'released',
|
|
947
1938
|
message: `Unable to remove released session pointer lock owner: ${errorMessage(error)}`,
|
|
948
1939
|
cause: error,
|
|
@@ -965,6 +1956,10 @@ async function releasePointerLock(lock) {
|
|
|
965
1956
|
}];
|
|
966
1957
|
}
|
|
967
1958
|
}
|
|
1959
|
+
/** @internal Test-only release of a held pointer lock; do not use outside session tests. */
|
|
1960
|
+
export async function __releasePointerLockForTests(cwd, token) {
|
|
1961
|
+
return await releasePointerLock({ context: resolveSessionPointerContext(cwd), token });
|
|
1962
|
+
}
|
|
968
1963
|
function recoveryAbort(context, primary, failures, operation) {
|
|
969
1964
|
return resolvedAbort(context, {
|
|
970
1965
|
code: 'session_pointer_lock_recovery_required',
|
|
@@ -979,9 +1974,12 @@ function recoveryAbort(context, primary, failures, operation) {
|
|
|
979
1974
|
secondaryFailures: failures,
|
|
980
1975
|
});
|
|
981
1976
|
}
|
|
982
|
-
function releaseFailureError(failures) {
|
|
983
|
-
const error = new
|
|
984
|
-
|
|
1977
|
+
function releaseFailureError(failures, context, value) {
|
|
1978
|
+
const error = new CommittedLaunchBlockedError(failures);
|
|
1979
|
+
if (context)
|
|
1980
|
+
error.context = context;
|
|
1981
|
+
if (value !== undefined)
|
|
1982
|
+
error.value = value;
|
|
985
1983
|
return error;
|
|
986
1984
|
}
|
|
987
1985
|
async function finalizePrecommitAbort(lock, primary, pointerTempPath) {
|
|
@@ -1023,7 +2021,7 @@ function ownerConflictAbort(context, candidateSessionId, state, cause) {
|
|
|
1023
2021
|
...(cause ? { cause } : {}),
|
|
1024
2022
|
});
|
|
1025
2023
|
}
|
|
1026
|
-
async function writePointerTransaction(cwd, candidateSessionId, options, timeoutMs, transition, pointerState) {
|
|
2024
|
+
async function writePointerTransaction(cwd, candidateSessionId, options, timeoutMs, transition, pointerState, beforeCommit, afterCommit) {
|
|
1027
2025
|
let context;
|
|
1028
2026
|
try {
|
|
1029
2027
|
context = options.context ?? resolveSessionPointerContext(cwd);
|
|
@@ -1059,6 +2057,7 @@ async function writePointerTransaction(cwd, candidateSessionId, options, timeout
|
|
|
1059
2057
|
const lock = await acquirePointerLock(context, candidateSessionId, timeoutMs, platform, tracker, options.regularFileSync);
|
|
1060
2058
|
const pointerTempPath = `${context.sessionPath}.tmp-${lock.token}`;
|
|
1061
2059
|
let pointerCommitted = false;
|
|
2060
|
+
let pointerTempWritten = false;
|
|
1062
2061
|
try {
|
|
1063
2062
|
let pointer;
|
|
1064
2063
|
try {
|
|
@@ -1076,7 +2075,7 @@ async function writePointerTransaction(cwd, candidateSessionId, options, timeout
|
|
|
1076
2075
|
}
|
|
1077
2076
|
let next;
|
|
1078
2077
|
try {
|
|
1079
|
-
next = transition(pointer, context);
|
|
2078
|
+
next = await transition(pointer, context);
|
|
1080
2079
|
}
|
|
1081
2080
|
catch (error) {
|
|
1082
2081
|
if (isSessionPointerLaunchAbort(error))
|
|
@@ -1091,6 +2090,7 @@ async function writePointerTransaction(cwd, candidateSessionId, options, timeout
|
|
|
1091
2090
|
}
|
|
1092
2091
|
const serialized = JSON.stringify(pointerState(next), null, 2);
|
|
1093
2092
|
try {
|
|
2093
|
+
pointerTempWritten = true;
|
|
1094
2094
|
await transactionDependencies.fs.writeFile(pointerTempPath, serialized, { mode: 0o600, flag: 'wx' });
|
|
1095
2095
|
}
|
|
1096
2096
|
catch (error) {
|
|
@@ -1116,11 +2116,31 @@ async function writePointerTransaction(cwd, candidateSessionId, options, timeout
|
|
|
1116
2116
|
cause: error,
|
|
1117
2117
|
});
|
|
1118
2118
|
}
|
|
2119
|
+
if (beforeCommit)
|
|
2120
|
+
await beforeCommit(context);
|
|
1119
2121
|
try {
|
|
1120
2122
|
await transactionDependencies.fs.rename(pointerTempPath, context.sessionPath);
|
|
1121
2123
|
pointerCommitted = true;
|
|
2124
|
+
if (afterCommit) {
|
|
2125
|
+
try {
|
|
2126
|
+
await afterCommit(context);
|
|
2127
|
+
}
|
|
2128
|
+
catch (error) {
|
|
2129
|
+
const releaseFailures = await releasePointerLock(lock);
|
|
2130
|
+
throw releaseFailureError([
|
|
2131
|
+
{
|
|
2132
|
+
operation: 'lock-release', phase: 'rename', ownership: 'uncertain',
|
|
2133
|
+
message: `Selected state root identity changed after pointer publication: ${errorMessage(error)}`,
|
|
2134
|
+
cause: error, evidencePath: context.sessionPath,
|
|
2135
|
+
},
|
|
2136
|
+
...releaseFailures,
|
|
2137
|
+
], context, next);
|
|
2138
|
+
}
|
|
2139
|
+
}
|
|
1122
2140
|
}
|
|
1123
2141
|
catch (error) {
|
|
2142
|
+
if (error instanceof CommittedLaunchBlockedError)
|
|
2143
|
+
throw error;
|
|
1124
2144
|
throw resolvedAbort(context, {
|
|
1125
2145
|
code: 'session_pointer_io_failure',
|
|
1126
2146
|
operation: 'pointer-rename',
|
|
@@ -1132,7 +2152,7 @@ async function writePointerTransaction(cwd, candidateSessionId, options, timeout
|
|
|
1132
2152
|
}
|
|
1133
2153
|
const releaseFailures = await releasePointerLock(lock);
|
|
1134
2154
|
if (releaseFailures.length > 0)
|
|
1135
|
-
throw releaseFailureError(releaseFailures);
|
|
2155
|
+
throw releaseFailureError(releaseFailures, context, next);
|
|
1136
2156
|
emitDegradedDurabilityWarning('session pointer start/reconcile', tracker);
|
|
1137
2157
|
return { context, value: next };
|
|
1138
2158
|
}
|
|
@@ -1149,21 +2169,44 @@ async function writePointerTransaction(cwd, candidateSessionId, options, timeout
|
|
|
1149
2169
|
reason: `Session pointer transition failed before commit: ${errorMessage(error)}`,
|
|
1150
2170
|
cause: error,
|
|
1151
2171
|
});
|
|
1152
|
-
const ownsPointerTemp =
|
|
1153
|
-
|| primary.operation === 'pointer-fsync'
|
|
1154
|
-
|| primary.operation === 'pointer-rename';
|
|
2172
|
+
const ownsPointerTemp = pointerTempWritten;
|
|
1155
2173
|
throw await finalizePrecommitAbort(lock, primary, ownsPointerTemp ? pointerTempPath : undefined);
|
|
1156
2174
|
}
|
|
1157
2175
|
}
|
|
1158
|
-
function startPointerTransition(requestedSessionId, options) {
|
|
2176
|
+
function startPointerTransition(requestedSessionId, options, requireLaunchLineageToken = false, requireAbsent = false) {
|
|
1159
2177
|
return (pointer, context) => {
|
|
1160
|
-
if (
|
|
2178
|
+
if (requireAbsent && pointer.status !== 'absent') {
|
|
2179
|
+
// Same read-vs-authority split: an occupied pointer is an owner conflict even when its
|
|
2180
|
+
// liveness is indeterminate, which is the only classification a live pointer can get on a
|
|
2181
|
+
// platform without process-birth evidence.
|
|
2182
|
+
if (pointer.state && (pointer.status === 'usable' || isUnobservableLivePointerState(pointer.state))) {
|
|
2183
|
+
throw ownerConflictAbort(context, requestedSessionId, pointer.state);
|
|
2184
|
+
}
|
|
2185
|
+
throw unusablePointerAbort(context, requestedSessionId, pointer);
|
|
2186
|
+
}
|
|
2187
|
+
// Read semantics, not authority: `identity-indeterminate` means "someone may still be here",
|
|
2188
|
+
// and on platforms that record no process-birth evidence EVERY live pointer classifies that way
|
|
2189
|
+
// (see probeIdentitylessProcess). Rejecting it outright refused legitimate sessions with a
|
|
2190
|
+
// generic unusable-pointer abort and skipped lineage/metadata preservation. It is admitted as
|
|
2191
|
+
// existing evidence; the ownership/compatibility checks below still refuse any pointer that does
|
|
2192
|
+
// not match, so a foreign claim gets the exact owner-conflict abort. Genuinely unusable evidence
|
|
2193
|
+
// (malformed, foreign-cwd) is still refused, and authority to own writes still requires 'usable'.
|
|
2194
|
+
if (pointer.status !== 'absent'
|
|
2195
|
+
&& pointer.status !== 'stale-dead'
|
|
2196
|
+
&& pointer.status !== 'usable'
|
|
2197
|
+
&& !(pointer.status === 'identity-indeterminate' && isUnobservableLivePointerState(pointer.state))) {
|
|
1161
2198
|
throw unusablePointerAbort(context, requestedSessionId, pointer);
|
|
1162
2199
|
}
|
|
1163
|
-
const existing = pointer.status === 'usable'
|
|
2200
|
+
const existing = pointer.status === 'usable'
|
|
2201
|
+
|| (pointer.status === 'identity-indeterminate' && isUnobservableLivePointerState(pointer.state))
|
|
2202
|
+
? pointer.state
|
|
2203
|
+
: undefined;
|
|
1164
2204
|
if (existing && !isStartCompatible(existing, requestedSessionId)) {
|
|
1165
2205
|
throw ownerConflictAbort(context, requestedSessionId, existing);
|
|
1166
2206
|
}
|
|
2207
|
+
if (requireLaunchLineageToken && existing && !isValidToken(existing.launch_lineage_token)) {
|
|
2208
|
+
throw ownerConflictAbort(context, requestedSessionId, existing);
|
|
2209
|
+
}
|
|
1167
2210
|
const canonicalSessionId = existing?.session_id ?? requestedSessionId;
|
|
1168
2211
|
const pid = resolvePid(options);
|
|
1169
2212
|
const platform = options.platform ?? process.platform;
|
|
@@ -1180,20 +2223,51 @@ function startPointerTransition(requestedSessionId, options) {
|
|
|
1180
2223
|
pid,
|
|
1181
2224
|
}, error);
|
|
1182
2225
|
}
|
|
1183
|
-
|
|
2226
|
+
const state = createSessionState(context.cwd, context.baseStateDir, canonicalSessionId, pid, platform, sessionIdentityFor(pid, platform), {
|
|
1184
2227
|
nativeSessionId: options.nativeSessionId ?? existing?.native_session_id,
|
|
1185
2228
|
previousNativeSessionId: options.previousNativeSessionId ?? existing?.previous_native_session_id,
|
|
1186
2229
|
nativeSessionSwitchedAt: options.nativeSessionSwitchedAt ?? existing?.native_session_switched_at,
|
|
1187
2230
|
...(ownerOmxSessionId ? { ownerOmxSessionId } : {}),
|
|
2231
|
+
launchLineageToken: existing
|
|
2232
|
+
? (isValidToken(existing.launch_lineage_token) ? existing.launch_lineage_token : undefined)
|
|
2233
|
+
: transactionDependencies.token(),
|
|
1188
2234
|
tmuxSessionName: options.tmuxSessionName ?? existing?.tmux_session_name,
|
|
1189
2235
|
tmuxPaneId: options.tmuxPaneId ?? existing?.tmux_pane_id,
|
|
1190
2236
|
});
|
|
2237
|
+
return preserveExistingLaunchLineageToken(existing, state);
|
|
1191
2238
|
};
|
|
1192
2239
|
}
|
|
1193
|
-
/**
|
|
2240
|
+
/** Create a wrapper-owned canonical pointer only when the selected pointer is absent. */
|
|
1194
2241
|
export async function writeSessionStart(cwd, sessionId, options = {}) {
|
|
2242
|
+
const candidateSessionId = normalizeSessionId(sessionId);
|
|
2243
|
+
let context;
|
|
2244
|
+
try {
|
|
2245
|
+
context = options.context ?? resolveSessionPointerContext(cwd);
|
|
2246
|
+
}
|
|
2247
|
+
catch (error) {
|
|
2248
|
+
throw contextAbort(cwd, candidateSessionId, error);
|
|
2249
|
+
}
|
|
2250
|
+
let pointer;
|
|
2251
|
+
try {
|
|
2252
|
+
pointer = await readSessionPointer(context);
|
|
2253
|
+
}
|
|
2254
|
+
catch (error) {
|
|
2255
|
+
throw resolvedAbort(context, {
|
|
2256
|
+
code: 'session_pointer_io_failure', operation: 'pointer-read', candidateSessionId,
|
|
2257
|
+
lockPath: context.lockPath, reason: `Unable to read selected session pointer: ${errorMessage(error)}`, cause: error,
|
|
2258
|
+
});
|
|
2259
|
+
}
|
|
2260
|
+
if (pointer.status !== 'absent') {
|
|
2261
|
+
if (pointer.status === 'usable' && pointer.state) {
|
|
2262
|
+
throw ownerConflictAbort(context, candidateSessionId ?? sessionId, pointer.state);
|
|
2263
|
+
}
|
|
2264
|
+
throw unusablePointerAbort(context, candidateSessionId ?? sessionId, pointer);
|
|
2265
|
+
}
|
|
2266
|
+
return await writeSessionStartTransition(cwd, sessionId, { ...options, context });
|
|
2267
|
+
}
|
|
2268
|
+
async function writeSessionStartTransition(cwd, sessionId, options, requireLaunchLineageToken = false, requireAbsent = false, beforeCommit, afterCommit) {
|
|
1195
2269
|
const requestedSessionId = normalizeSessionId(sessionId);
|
|
1196
|
-
const result = await writePointerTransaction(cwd, requestedSessionId, options, DEFAULT_POINTER_TIMEOUT_MS, startPointerTransition(requestedSessionId ?? sessionId, options), (state) => state);
|
|
2270
|
+
const result = await writePointerTransaction(cwd, requestedSessionId, options, DEFAULT_POINTER_TIMEOUT_MS, startPointerTransition(requestedSessionId ?? sessionId, options, requireLaunchLineageToken, requireAbsent), (state) => state, beforeCommit, afterCommit);
|
|
1197
2271
|
await appendToLogAtContext(result.context, {
|
|
1198
2272
|
event: 'session_start',
|
|
1199
2273
|
session_id: result.value.session_id,
|
|
@@ -1203,51 +2277,613 @@ export async function writeSessionStart(cwd, sessionId, options = {}) {
|
|
|
1203
2277
|
}).catch(() => { });
|
|
1204
2278
|
return result.value;
|
|
1205
2279
|
}
|
|
2280
|
+
const DIRECTORY_CAPABILITY_PLATFORMS = new Set([
|
|
2281
|
+
'linux', 'darwin', 'freebsd', 'openbsd', 'netbsd', 'sunos',
|
|
2282
|
+
]);
|
|
2283
|
+
export class LaunchContextResolutionError extends Error {
|
|
2284
|
+
cleanup;
|
|
2285
|
+
name = 'LaunchContextResolutionError';
|
|
2286
|
+
constructor(cleanup, cause) {
|
|
2287
|
+
super(`Unable to establish launch directory capability: ${errorMessage(cause)}`, { cause });
|
|
2288
|
+
this.cleanup = cleanup;
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
const bindingLeases = new WeakMap();
|
|
2292
|
+
const bindingFinalizations = new WeakMap();
|
|
2293
|
+
function safeError(error) {
|
|
2294
|
+
return {
|
|
2295
|
+
name: error instanceof Error ? error.name : 'Error',
|
|
2296
|
+
message: errorMessage(error),
|
|
2297
|
+
...(errorCode(error) ? { code: errorCode(error) } : {}),
|
|
2298
|
+
};
|
|
2299
|
+
}
|
|
2300
|
+
function lockDisposition(failures) {
|
|
2301
|
+
if (failures.some((failure) => failure.ownership === 'uncertain'))
|
|
2302
|
+
return 'uncertain';
|
|
2303
|
+
return failures.some((failure) => failure.ownership === 'held') ? 'held' : 'released-with-residue';
|
|
2304
|
+
}
|
|
2305
|
+
async function acquireDirectoryCapability(cwd, platform) {
|
|
2306
|
+
const cleanup = [];
|
|
2307
|
+
let canonicalRealpath;
|
|
2308
|
+
try {
|
|
2309
|
+
canonicalRealpath = await realpath(cwd);
|
|
2310
|
+
}
|
|
2311
|
+
catch (error) {
|
|
2312
|
+
throw new LaunchContextResolutionError({ capability: cleanup }, error);
|
|
2313
|
+
}
|
|
2314
|
+
if (!DIRECTORY_CAPABILITY_PLATFORMS.has(platform)) {
|
|
2315
|
+
return { canonicalRealpath, identity: { kind: 'unsupported', reason: 'platform' }, lease: { closed: false }, cleanup };
|
|
2316
|
+
}
|
|
2317
|
+
let handle;
|
|
2318
|
+
try {
|
|
2319
|
+
handle = await open(canonicalRealpath, 'r');
|
|
2320
|
+
}
|
|
2321
|
+
catch (error) {
|
|
2322
|
+
throw new LaunchContextResolutionError({ capability: cleanup }, error);
|
|
2323
|
+
}
|
|
2324
|
+
let statFailure;
|
|
2325
|
+
try {
|
|
2326
|
+
const stats = await handle.stat({ bigint: true });
|
|
2327
|
+
if (!stats.isDirectory() || typeof stats.dev !== 'bigint' || typeof stats.ino !== 'bigint' || stats.dev <= 0n || stats.ino <= 0n) {
|
|
2328
|
+
try {
|
|
2329
|
+
await handle.close();
|
|
2330
|
+
cleanup.push({ role: 'acquisition', phase: 'before-authorization', status: 'closed' });
|
|
2331
|
+
}
|
|
2332
|
+
catch (closeError) {
|
|
2333
|
+
cleanup.push({ role: 'acquisition', phase: 'before-authorization', status: 'failed', error: safeError(closeError) });
|
|
2334
|
+
throw new LaunchContextResolutionError({ capability: cleanup }, closeError);
|
|
2335
|
+
}
|
|
2336
|
+
return { canonicalRealpath, identity: { kind: 'unsupported', reason: 'inadequate-identity' }, lease: { closed: false }, cleanup };
|
|
2337
|
+
}
|
|
2338
|
+
return { canonicalRealpath, identity: { kind: 'supported', dev: stats.dev, ino: stats.ino }, lease: { handle, closed: false }, cleanup };
|
|
2339
|
+
}
|
|
2340
|
+
catch (error) {
|
|
2341
|
+
if (error instanceof LaunchContextResolutionError)
|
|
2342
|
+
throw error;
|
|
2343
|
+
statFailure = error;
|
|
2344
|
+
}
|
|
2345
|
+
try {
|
|
2346
|
+
await handle.close();
|
|
2347
|
+
cleanup.push({ role: 'acquisition', phase: 'before-authorization', status: 'closed' });
|
|
2348
|
+
}
|
|
2349
|
+
catch (closeError) {
|
|
2350
|
+
cleanup.push({ role: 'acquisition', phase: 'before-authorization', status: 'failed', error: safeError(closeError) });
|
|
2351
|
+
throw new LaunchContextResolutionError({ capability: cleanup }, closeError);
|
|
2352
|
+
}
|
|
2353
|
+
const code = errorCode(statFailure);
|
|
2354
|
+
if (code === 'ENOSYS' || code === 'ENOTSUP' || code === 'EOPNOTSUPP' || code === 'EINVAL') {
|
|
2355
|
+
return { canonicalRealpath, identity: { kind: 'unsupported', reason: 'stat-feature' }, lease: { closed: false }, cleanup };
|
|
2356
|
+
}
|
|
2357
|
+
throw new LaunchContextResolutionError({ capability: cleanup }, statFailure);
|
|
2358
|
+
}
|
|
2359
|
+
export async function closeLaunchSessionBindingOnce(binding, phase = 'post-finalization') {
|
|
2360
|
+
const lease = bindingLeases.get(binding);
|
|
2361
|
+
if (!lease || lease.closed)
|
|
2362
|
+
return lease?.closeEvidence ?? { role: 'original-retained', phase, status: 'not-needed' };
|
|
2363
|
+
lease.closed = true;
|
|
2364
|
+
try {
|
|
2365
|
+
await lease.handle?.close();
|
|
2366
|
+
return lease.closeEvidence = { role: 'original-retained', phase, status: 'closed' };
|
|
2367
|
+
}
|
|
2368
|
+
catch (error) {
|
|
2369
|
+
return lease.closeEvidence = { role: 'original-retained', phase, status: 'failed', error: safeError(error) };
|
|
2370
|
+
}
|
|
2371
|
+
}
|
|
2372
|
+
export async function establishLaunchSessionBinding(cwd, requestedSessionId, options = {}) {
|
|
2373
|
+
const platform = options.platform ?? process.platform;
|
|
2374
|
+
let context;
|
|
2375
|
+
try {
|
|
2376
|
+
context = options.context ?? resolveSessionPointerContext(cwd);
|
|
2377
|
+
await transactionDependencies.fs.mkdir(context.baseStateDir, { recursive: true });
|
|
2378
|
+
}
|
|
2379
|
+
catch (error) {
|
|
2380
|
+
throw new LaunchContextResolutionError({ capability: [] }, error);
|
|
2381
|
+
}
|
|
2382
|
+
const acquired = await acquireDirectoryCapability(context.baseStateDir, platform);
|
|
2383
|
+
const cleanup = () => ({ capability: acquired.cleanup });
|
|
2384
|
+
const revalidateSelectedRoot = async (lockedContext) => {
|
|
2385
|
+
if (acquired.identity.kind !== 'supported')
|
|
2386
|
+
return;
|
|
2387
|
+
const retained = await acquired.lease.handle?.stat({ bigint: true });
|
|
2388
|
+
if (!retained || !retained.isDirectory() || retained.dev !== acquired.identity.dev || retained.ino !== acquired.identity.ino) {
|
|
2389
|
+
throw resolvedAbort(lockedContext, {
|
|
2390
|
+
code: 'session_pointer_io_failure', operation: 'pointer-classify', candidateSessionId: normalizeSessionId(requestedSessionId),
|
|
2391
|
+
lockPath: lockedContext.lockPath, reason: 'Selected state root identity changed during pointer publication.',
|
|
2392
|
+
});
|
|
2393
|
+
}
|
|
2394
|
+
const canonical = await realpath(lockedContext.baseStateDir);
|
|
2395
|
+
const fresh = await open(canonical, 'r');
|
|
2396
|
+
try {
|
|
2397
|
+
const stats = await fresh.stat({ bigint: true });
|
|
2398
|
+
if (canonical !== acquired.canonicalRealpath || !stats.isDirectory() || stats.dev !== acquired.identity.dev || stats.ino !== acquired.identity.ino) {
|
|
2399
|
+
throw resolvedAbort(lockedContext, {
|
|
2400
|
+
code: 'session_pointer_io_failure', operation: 'pointer-classify', candidateSessionId: normalizeSessionId(requestedSessionId),
|
|
2401
|
+
lockPath: lockedContext.lockPath, reason: 'Selected state root was replaced during pointer publication.',
|
|
2402
|
+
});
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
finally {
|
|
2406
|
+
await fresh.close();
|
|
2407
|
+
}
|
|
2408
|
+
};
|
|
2409
|
+
try {
|
|
2410
|
+
const state = await writeSessionStartTransition(context.cwd, requestedSessionId, { ...options, context }, true, true, revalidateSelectedRoot, revalidateSelectedRoot);
|
|
2411
|
+
const token = state.launch_lineage_token;
|
|
2412
|
+
if (!isValidToken(token)) {
|
|
2413
|
+
const close = await closeLease(acquired.lease, 'before-authorization');
|
|
2414
|
+
if (close)
|
|
2415
|
+
acquired.cleanup.push(close);
|
|
2416
|
+
throw new LaunchContextResolutionError(cleanup(), new Error('Committed session pointer lacks a valid lineage token.'));
|
|
2417
|
+
}
|
|
2418
|
+
const binding = Object.freeze({
|
|
2419
|
+
context: Object.freeze({ ...context }), canonicalRealpath: acquired.canonicalRealpath,
|
|
2420
|
+
directoryIdentity: acquired.identity, canonicalSessionId: state.session_id,
|
|
2421
|
+
...(state.owner_omx_session_id ? { ownerOmxSessionId: state.owner_omx_session_id } : {}),
|
|
2422
|
+
...(state.native_session_id ? { nativeSessionId: state.native_session_id } : {}),
|
|
2423
|
+
startedAt: state.started_at, launchLineageToken: token,
|
|
2424
|
+
});
|
|
2425
|
+
bindingLeases.set(binding, acquired.lease);
|
|
2426
|
+
return { kind: 'committed-released', binding, cleanup: cleanup() };
|
|
2427
|
+
}
|
|
2428
|
+
catch (error) {
|
|
2429
|
+
if (error instanceof CommittedLaunchBlockedError) {
|
|
2430
|
+
const state = error.value;
|
|
2431
|
+
const errorContext = error.context ?? context;
|
|
2432
|
+
const close = await closeLease(acquired.lease, 'before-authorization');
|
|
2433
|
+
if (close)
|
|
2434
|
+
acquired.cleanup.push(close);
|
|
2435
|
+
return {
|
|
2436
|
+
kind: 'committed-release-failed', evidence: { context: errorContext, canonicalSessionId: state?.session_id ?? requestedSessionId },
|
|
2437
|
+
error, lockDisposition: lockDisposition(error.secondaryFailures), secondaryFailures: error.secondaryFailures, cleanup: cleanup(),
|
|
2438
|
+
};
|
|
2439
|
+
}
|
|
2440
|
+
const close = await closeLease(acquired.lease, 'before-authorization');
|
|
2441
|
+
if (close)
|
|
2442
|
+
acquired.cleanup.push(close);
|
|
2443
|
+
if (isSessionPointerLaunchAbort(error))
|
|
2444
|
+
return { kind: 'precommit-aborted', abort: error, cleanup: cleanup() };
|
|
2445
|
+
throw error;
|
|
2446
|
+
}
|
|
2447
|
+
}
|
|
2448
|
+
async function closeLease(lease, phase) {
|
|
2449
|
+
if (lease.closed)
|
|
2450
|
+
return undefined;
|
|
2451
|
+
lease.closed = true;
|
|
2452
|
+
try {
|
|
2453
|
+
await lease.handle?.close();
|
|
2454
|
+
return { role: 'acquisition', phase, status: 'closed' };
|
|
2455
|
+
}
|
|
2456
|
+
catch (error) {
|
|
2457
|
+
return { role: 'acquisition', phase, status: 'failed', error: safeError(error) };
|
|
2458
|
+
}
|
|
2459
|
+
}
|
|
2460
|
+
export async function updateDetachedSessionMetadata(binding, patch) {
|
|
2461
|
+
const capability = [];
|
|
2462
|
+
const cleanup = { capability };
|
|
2463
|
+
try {
|
|
2464
|
+
let pointerBeforeTransaction;
|
|
2465
|
+
try {
|
|
2466
|
+
pointerBeforeTransaction = await readSessionPointer(binding.context);
|
|
2467
|
+
}
|
|
2468
|
+
catch (error) {
|
|
2469
|
+
throw resolvedAbort(binding.context, {
|
|
2470
|
+
code: 'session_pointer_io_failure', operation: 'pointer-read', candidateSessionId: binding.canonicalSessionId,
|
|
2471
|
+
lockPath: binding.context.lockPath, reason: `Unable to read selected session pointer: ${errorMessage(error)}`, cause: error,
|
|
2472
|
+
});
|
|
2473
|
+
}
|
|
2474
|
+
// Read against an ALREADY-authorized binding: isAuthorizedBoundPointer verifies the pointer
|
|
2475
|
+
// belongs to this binding (canonical session id + launch lineage token), which is a stricter
|
|
2476
|
+
// check than liveness. Admitting an unobservable-live pointer here only stops a running session
|
|
2477
|
+
// on a platform without process-birth evidence from reading as absent; it grants no new
|
|
2478
|
+
// authority, because the binding authorization still has to match.
|
|
2479
|
+
if ((pointerBeforeTransaction.status !== 'usable'
|
|
2480
|
+
&& !(pointerBeforeTransaction.status === 'identity-indeterminate'
|
|
2481
|
+
&& isUnobservableLivePointerState(pointerBeforeTransaction.state)))
|
|
2482
|
+
|| !isAuthorizedBoundPointer(binding, binding.context, binding.canonicalSessionId, pointerBeforeTransaction.state)) {
|
|
2483
|
+
throw unusablePointerAbort(binding.context, binding.canonicalSessionId, pointerBeforeTransaction);
|
|
2484
|
+
}
|
|
2485
|
+
const authorization = await authorizeBoundDirectoryBeforeTransaction(binding, pointerBeforeTransaction, binding.context.cwd);
|
|
2486
|
+
capability.push(...authorization.capability);
|
|
2487
|
+
const result = await writePointerTransaction(binding.context.cwd, binding.canonicalSessionId, { context: binding.context }, DEFAULT_POINTER_TIMEOUT_MS, async (pointer, context) => {
|
|
2488
|
+
const state = pointer.status === 'usable'
|
|
2489
|
+
|| (pointer.status === 'identity-indeterminate' && isUnobservableLivePointerState(pointer.state))
|
|
2490
|
+
? pointer.state
|
|
2491
|
+
: undefined;
|
|
2492
|
+
if (!state || !isAuthorizedBoundPointer(binding, context, binding.canonicalSessionId, state)) {
|
|
2493
|
+
throw unusablePointerAbort(context, binding.canonicalSessionId, pointer);
|
|
2494
|
+
}
|
|
2495
|
+
await consumeBoundDirectoryAuthorizationUnderLock(binding, authorization, pointer);
|
|
2496
|
+
return {
|
|
2497
|
+
...state,
|
|
2498
|
+
...(patch.tmuxSessionName !== undefined ? { tmux_session_name: patch.tmuxSessionName } : {}),
|
|
2499
|
+
...(patch.tmuxPaneId !== undefined ? { tmux_pane_id: patch.tmuxPaneId } : {}),
|
|
2500
|
+
};
|
|
2501
|
+
}, (state) => state);
|
|
2502
|
+
return { kind: 'committed-released', evidence: { context: result.context, canonicalSessionId: binding.canonicalSessionId }, cleanup };
|
|
2503
|
+
}
|
|
2504
|
+
catch (error) {
|
|
2505
|
+
if (error instanceof CommittedLaunchBlockedError) {
|
|
2506
|
+
const context = error.context ?? binding.context;
|
|
2507
|
+
return { kind: 'committed-release-failed', evidence: { context, canonicalSessionId: binding.canonicalSessionId }, error,
|
|
2508
|
+
lockDisposition: lockDisposition(error.secondaryFailures), secondaryFailures: error.secondaryFailures, cleanup };
|
|
2509
|
+
}
|
|
2510
|
+
if (isSessionPointerLaunchAbort(error))
|
|
2511
|
+
return { kind: 'precommit-aborted', abort: error, cleanup };
|
|
2512
|
+
throw error;
|
|
2513
|
+
}
|
|
2514
|
+
}
|
|
2515
|
+
function isAuthorizedBoundPointer(binding, context, candidateSessionId, state) {
|
|
2516
|
+
traceSessionFinalizationOperation('auth-function-enter');
|
|
2517
|
+
traceSessionFinalizationOperation('lease-lookup-start');
|
|
2518
|
+
const lease = bindingLeases.get(binding);
|
|
2519
|
+
traceSessionFinalizationOperation('lease-lookup-done');
|
|
2520
|
+
traceSessionFinalizationOperation(`state-present:${state ? 'true' : 'false'}`);
|
|
2521
|
+
if (!lease || (binding.directoryIdentity.kind === 'supported' && lease.closed) || !state) {
|
|
2522
|
+
traceSessionFinalizationOperation('bound-auth-fail:lease-or-state');
|
|
2523
|
+
return false;
|
|
2524
|
+
}
|
|
2525
|
+
if (!isValidToken(binding.launchLineageToken) || !isValidToken(state.launch_lineage_token)) {
|
|
2526
|
+
traceSessionFinalizationOperation('bound-auth-fail:token-shape');
|
|
2527
|
+
return false;
|
|
2528
|
+
}
|
|
2529
|
+
if (candidateSessionId !== binding.canonicalSessionId) {
|
|
2530
|
+
traceSessionFinalizationOperation('bound-auth-fail:candidate-session');
|
|
2531
|
+
return false;
|
|
2532
|
+
}
|
|
2533
|
+
if (state.launch_lineage_token !== binding.launchLineageToken) {
|
|
2534
|
+
traceSessionFinalizationOperation('bound-auth-fail:lineage-token');
|
|
2535
|
+
return false;
|
|
2536
|
+
}
|
|
2537
|
+
if (state.started_at !== binding.startedAt) {
|
|
2538
|
+
traceSessionFinalizationOperation('bound-auth-fail:started-at');
|
|
2539
|
+
return false;
|
|
2540
|
+
}
|
|
2541
|
+
const directCanonicalIdentity = state.session_id === binding.canonicalSessionId
|
|
2542
|
+
&& ((state.owner_omx_session_id ?? undefined) === binding.ownerOmxSessionId
|
|
2543
|
+
|| (binding.ownerOmxSessionId === undefined && state.owner_omx_session_id === binding.canonicalSessionId))
|
|
2544
|
+
&& (binding.nativeSessionId === undefined || state.native_session_id === binding.nativeSessionId);
|
|
2545
|
+
const replacedNativeIdentity = state.owner_omx_session_id === binding.canonicalSessionId
|
|
2546
|
+
&& normalizeSessionId(state.session_id) !== undefined
|
|
2547
|
+
&& state.native_session_id === state.session_id;
|
|
2548
|
+
if (!directCanonicalIdentity && !replacedNativeIdentity) {
|
|
2549
|
+
traceSessionFinalizationOperation('bound-auth-fail:session-identity');
|
|
2550
|
+
return false;
|
|
2551
|
+
}
|
|
2552
|
+
if (context.rootSource !== binding.context.rootSource)
|
|
2553
|
+
traceSessionFinalizationOperation('bound-auth-fail:root-source');
|
|
2554
|
+
else if (context.baseStateDir !== binding.context.baseStateDir)
|
|
2555
|
+
traceSessionFinalizationOperation('bound-auth-fail:base-state-dir');
|
|
2556
|
+
else if (context.sessionPath !== binding.context.sessionPath)
|
|
2557
|
+
traceSessionFinalizationOperation('bound-auth-fail:session-path');
|
|
2558
|
+
else if (context.cwd !== binding.context.cwd)
|
|
2559
|
+
traceSessionFinalizationOperation('bound-auth-fail:context-cwd');
|
|
2560
|
+
else if (state.cwd !== binding.context.cwd)
|
|
2561
|
+
traceSessionFinalizationOperation('bound-auth-fail:state-cwd');
|
|
2562
|
+
else {
|
|
2563
|
+
traceSessionFinalizationOperation('bound-auth-success');
|
|
2564
|
+
return true;
|
|
2565
|
+
}
|
|
2566
|
+
return false;
|
|
2567
|
+
}
|
|
2568
|
+
function isAuthorizedBoundStaleDeadPointer(binding, context, candidateSessionId, state) {
|
|
2569
|
+
return isAuthorizedBoundPointer(binding, context, candidateSessionId, state)
|
|
2570
|
+
&& Boolean(normalizeSessionId(state?.native_session_id));
|
|
2571
|
+
}
|
|
2572
|
+
class BoundDirectoryComparisonDenied extends Error {
|
|
2573
|
+
comparison;
|
|
2574
|
+
capability;
|
|
2575
|
+
constructor(comparison, capability) {
|
|
2576
|
+
super(comparison.reason);
|
|
2577
|
+
this.comparison = comparison;
|
|
2578
|
+
this.capability = capability;
|
|
2579
|
+
}
|
|
2580
|
+
}
|
|
2581
|
+
function isAuthorizedBoundPointerForFinalization(binding, context, candidateSessionId, pointer, phase) {
|
|
2582
|
+
traceSessionFinalizationOperation(`${phase}-status:${pointer.status}`);
|
|
2583
|
+
if (pointer.status === 'usable') {
|
|
2584
|
+
traceSessionFinalizationOperation(`${phase}-usable-branch-enter`);
|
|
2585
|
+
return isAuthorizedBoundPointer(binding, context, candidateSessionId, pointer.state);
|
|
2586
|
+
}
|
|
2587
|
+
if (pointer.status === 'stale-dead') {
|
|
2588
|
+
traceSessionFinalizationOperation(`${phase}-stale-dead-branch-enter`);
|
|
2589
|
+
return isAuthorizedBoundStaleDeadPointer(binding, context, candidateSessionId, pointer.state);
|
|
2590
|
+
}
|
|
2591
|
+
if (pointer.status === 'identity-indeterminate') {
|
|
2592
|
+
traceSessionFinalizationOperation(`${phase}-identity-indeterminate-branch-enter`);
|
|
2593
|
+
const sameProcess = pointer.state?.pid === process.pid;
|
|
2594
|
+
traceSessionFinalizationOperation(`${phase}-identity-indeterminate-current-pid:${sameProcess ? 'true' : 'false'}`);
|
|
2595
|
+
return sameProcess && isAuthorizedBoundPointer(binding, context, candidateSessionId, pointer.state);
|
|
2596
|
+
}
|
|
2597
|
+
traceSessionFinalizationOperation(`${phase}-unsupported-status`);
|
|
2598
|
+
return false;
|
|
2599
|
+
}
|
|
2600
|
+
async function authorizeBoundDirectoryBeforeTransaction(binding, pointer, postLaunchCwd) {
|
|
2601
|
+
const capability = [];
|
|
2602
|
+
const lease = bindingLeases.get(binding);
|
|
2603
|
+
if (!lease || lease.closed) {
|
|
2604
|
+
throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: 'binding-lease-closed' }, capability);
|
|
2605
|
+
}
|
|
2606
|
+
try {
|
|
2607
|
+
if (!sameFilePath(binding.context.cwd, postLaunchCwd)
|
|
2608
|
+
|| !pointer.state?.cwd
|
|
2609
|
+
|| !sameFilePath(binding.context.cwd, pointer.state.cwd)) {
|
|
2610
|
+
throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: 'cwd-identity-mismatch' }, capability);
|
|
2611
|
+
}
|
|
2612
|
+
if (binding.directoryIdentity.kind !== 'supported') {
|
|
2613
|
+
return {
|
|
2614
|
+
comparison: { status: 'matched', reason: `unsupported-directory-capability:${binding.directoryIdentity.reason}` },
|
|
2615
|
+
capability,
|
|
2616
|
+
pointerStatus: pointer.status,
|
|
2617
|
+
pointerRaw: pointer.raw,
|
|
2618
|
+
};
|
|
2619
|
+
}
|
|
2620
|
+
const retained = fstatSync(lease.handle.fd, { bigint: true });
|
|
2621
|
+
if (!retained || !retained.isDirectory() || retained.dev !== binding.directoryIdentity.dev || retained.ino !== binding.directoryIdentity.ino) {
|
|
2622
|
+
throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: 'retained-directory-identity-mismatch' }, capability);
|
|
2623
|
+
}
|
|
2624
|
+
const canonical = await realpath(binding.context.baseStateDir);
|
|
2625
|
+
const fd = openSync(canonical, 'r');
|
|
2626
|
+
let matched = false;
|
|
2627
|
+
try {
|
|
2628
|
+
const stats = fstatSync(fd, { bigint: true });
|
|
2629
|
+
matched = canonical === binding.canonicalRealpath
|
|
2630
|
+
&& stats.isDirectory()
|
|
2631
|
+
&& stats.dev === binding.directoryIdentity.dev
|
|
2632
|
+
&& stats.ino === binding.directoryIdentity.ino;
|
|
2633
|
+
}
|
|
2634
|
+
finally {
|
|
2635
|
+
try {
|
|
2636
|
+
closeSync(fd);
|
|
2637
|
+
capability.push({ role: 'fresh-comparison', phase: 'before-authorization', status: 'closed' });
|
|
2638
|
+
}
|
|
2639
|
+
catch (error) {
|
|
2640
|
+
capability.push({ role: 'fresh-comparison', phase: 'before-authorization', status: 'failed', error: safeError(error) });
|
|
2641
|
+
throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: 'fresh-comparison-close-failed' }, capability);
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
if (!matched)
|
|
2645
|
+
throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: 'directory-identity-mismatch' }, capability);
|
|
2646
|
+
return { comparison: { status: 'matched' }, capability, pointerStatus: pointer.status, pointerRaw: pointer.raw };
|
|
2647
|
+
}
|
|
2648
|
+
catch (error) {
|
|
2649
|
+
if (error instanceof BoundDirectoryComparisonDenied)
|
|
2650
|
+
throw error;
|
|
2651
|
+
throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: errorMessage(error) }, capability);
|
|
2652
|
+
}
|
|
2653
|
+
}
|
|
2654
|
+
async function consumeBoundDirectoryAuthorizationUnderLock(binding, authorization, pointer) {
|
|
2655
|
+
if (authorization.comparison?.status !== 'matched' || binding.directoryIdentity.kind !== 'supported')
|
|
2656
|
+
return authorization;
|
|
2657
|
+
const lease = bindingLeases.get(binding);
|
|
2658
|
+
try {
|
|
2659
|
+
const retained = lease?.handle ? fstatSync(lease.handle.fd, { bigint: true }) : undefined;
|
|
2660
|
+
if (!retained || !retained.isDirectory() || retained.dev !== binding.directoryIdentity.dev || retained.ino !== binding.directoryIdentity.ino) {
|
|
2661
|
+
throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: 'retained-directory-identity-mismatch' }, authorization.capability);
|
|
2662
|
+
}
|
|
2663
|
+
if (!pointer.state?.cwd || !sameFilePath(binding.context.cwd, pointer.state.cwd)) {
|
|
2664
|
+
throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: 'cwd-identity-mismatch-under-lock' }, authorization.capability);
|
|
2665
|
+
}
|
|
2666
|
+
const canonical = await realpath(binding.context.baseStateDir);
|
|
2667
|
+
const fd = openSync(canonical, 'r');
|
|
2668
|
+
let matched = false;
|
|
2669
|
+
try {
|
|
2670
|
+
const stats = fstatSync(fd, { bigint: true });
|
|
2671
|
+
matched = canonical === binding.canonicalRealpath
|
|
2672
|
+
&& stats.isDirectory()
|
|
2673
|
+
&& stats.dev === binding.directoryIdentity.dev
|
|
2674
|
+
&& stats.ino === binding.directoryIdentity.ino;
|
|
2675
|
+
}
|
|
2676
|
+
finally {
|
|
2677
|
+
try {
|
|
2678
|
+
closeSync(fd);
|
|
2679
|
+
authorization.capability.push({ role: 'fresh-comparison', phase: 'before-authorization', status: 'closed' });
|
|
2680
|
+
}
|
|
2681
|
+
catch (error) {
|
|
2682
|
+
authorization.capability.push({ role: 'fresh-comparison', phase: 'before-authorization', status: 'failed', error: safeError(error) });
|
|
2683
|
+
throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: 'fresh-comparison-close-failed-under-lock' }, authorization.capability);
|
|
2684
|
+
}
|
|
2685
|
+
}
|
|
2686
|
+
if (!matched)
|
|
2687
|
+
throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: 'directory-identity-mismatch-under-lock' }, authorization.capability);
|
|
2688
|
+
if (pointer.status !== authorization.pointerStatus || pointer.raw !== authorization.pointerRaw) {
|
|
2689
|
+
throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: 'pointer-changed-before-consume' }, authorization.capability);
|
|
2690
|
+
}
|
|
2691
|
+
return authorization;
|
|
2692
|
+
}
|
|
2693
|
+
catch (error) {
|
|
2694
|
+
if (error instanceof BoundDirectoryComparisonDenied)
|
|
2695
|
+
throw error;
|
|
2696
|
+
throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: errorMessage(error) }, authorization.capability);
|
|
2697
|
+
}
|
|
2698
|
+
}
|
|
2699
|
+
export function finalizeBoundOnce(binding, _reason, postLaunchCwd = binding.context.cwd) {
|
|
2700
|
+
const existing = bindingFinalizations.get(binding);
|
|
2701
|
+
if (existing)
|
|
2702
|
+
return existing;
|
|
2703
|
+
const finalization = (async () => {
|
|
2704
|
+
try {
|
|
2705
|
+
const revalidation = await writeSessionEnd(binding.context.cwd, binding.canonicalSessionId, {
|
|
2706
|
+
context: binding.context, binding, postLaunchCwd,
|
|
2707
|
+
});
|
|
2708
|
+
return { finalized: true, cleanup: { capability: revalidation.capability, comparison: revalidation.comparison } };
|
|
2709
|
+
}
|
|
2710
|
+
catch (error) {
|
|
2711
|
+
if (error instanceof BoundDirectoryComparisonDenied) {
|
|
2712
|
+
return { finalized: false, cleanup: { capability: error.capability, comparison: error.comparison } };
|
|
2713
|
+
}
|
|
2714
|
+
if (isSessionPointerLaunchAbort(error)) {
|
|
2715
|
+
return { finalized: false, cleanup: { capability: [], comparison: { status: 'denied', reason: error.code } } };
|
|
2716
|
+
}
|
|
2717
|
+
throw error;
|
|
2718
|
+
}
|
|
2719
|
+
})();
|
|
2720
|
+
bindingFinalizations.set(binding, finalization);
|
|
2721
|
+
return finalization;
|
|
2722
|
+
}
|
|
2723
|
+
function nativeSessionOwnerTransition(nativeSessionId, options) {
|
|
2724
|
+
return (pointer, context) => {
|
|
2725
|
+
// Read semantics, not authority: `identity-indeterminate` means "someone may still be here",
|
|
2726
|
+
// and on platforms that record no process-birth evidence EVERY live pointer classifies that way
|
|
2727
|
+
// (see probeIdentitylessProcess). Rejecting it outright refused legitimate sessions with a
|
|
2728
|
+
// generic unusable-pointer abort and skipped lineage/metadata preservation. It is admitted as
|
|
2729
|
+
// existing evidence; the ownership/compatibility checks below still refuse any pointer that does
|
|
2730
|
+
// not match, so a foreign claim gets the exact owner-conflict abort. Genuinely unusable evidence
|
|
2731
|
+
// (malformed, foreign-cwd) is still refused, and authority to own writes still requires 'usable'.
|
|
2732
|
+
if (pointer.status !== 'absent'
|
|
2733
|
+
&& pointer.status !== 'stale-dead'
|
|
2734
|
+
&& pointer.status !== 'usable'
|
|
2735
|
+
&& !(pointer.status === 'identity-indeterminate' && isUnobservableLivePointerState(pointer.state))) {
|
|
2736
|
+
throw unusablePointerAbort(context, nativeSessionId, pointer);
|
|
2737
|
+
}
|
|
2738
|
+
const pid = resolvePid(options);
|
|
2739
|
+
const platform = options.platform ?? process.platform;
|
|
2740
|
+
const linuxIdentity = sessionIdentityFor(pid, platform);
|
|
2741
|
+
const existing = pointer.status === 'usable'
|
|
2742
|
+
|| (pointer.status === 'identity-indeterminate' && isUnobservableLivePointerState(pointer.state))
|
|
2743
|
+
? pointer.state
|
|
2744
|
+
: undefined;
|
|
2745
|
+
if (existing && (normalizeSessionId(existing.session_id) !== nativeSessionId
|
|
2746
|
+
|| normalizeSessionId(existing.native_session_id) !== nativeSessionId
|
|
2747
|
+
|| existing.platform !== platform
|
|
2748
|
+
|| !isSessionStateAuthoritativeForCwd(existing, context.cwd)
|
|
2749
|
+
|| existing.pid !== pid)) {
|
|
2750
|
+
throw ownerConflictAbort(context, nativeSessionId, existing);
|
|
2751
|
+
}
|
|
2752
|
+
return createSessionState(context.cwd, context.baseStateDir, nativeSessionId, pid, platform, linuxIdentity, {
|
|
2753
|
+
nativeSessionId,
|
|
2754
|
+
startedAt: existing?.started_at,
|
|
2755
|
+
tmuxSessionName: options.tmuxSessionName ?? existing?.tmux_session_name,
|
|
2756
|
+
tmuxPaneId: options.tmuxPaneId ?? existing?.tmux_pane_id,
|
|
2757
|
+
});
|
|
2758
|
+
};
|
|
2759
|
+
}
|
|
2760
|
+
export async function writeNativeSessionOwner(cwd, nativeSessionId, options = {}) {
|
|
2761
|
+
const normalized = normalizeSessionId(nativeSessionId);
|
|
2762
|
+
const context = resolveNativeSessionOwnerContext(cwd, nativeSessionId);
|
|
2763
|
+
const result = await writePointerTransaction(cwd, normalized, { context }, NATIVE_POINTER_TIMEOUT_MS, nativeSessionOwnerTransition(normalized ?? nativeSessionId, options), (state) => state);
|
|
2764
|
+
return result.value;
|
|
2765
|
+
}
|
|
2766
|
+
/** Read and classify the exact native session-owner sidecar without consulting alternate roots. */
|
|
2767
|
+
export async function readNativeSessionOwnerEvidence(cwd, nativeSessionId) {
|
|
2768
|
+
const normalized = normalizeSessionId(nativeSessionId);
|
|
2769
|
+
if (!normalized)
|
|
2770
|
+
return { status: 'malformed' };
|
|
2771
|
+
const context = resolveNativeSessionOwnerContext(cwd, normalized);
|
|
2772
|
+
const nativeOwnerDir = dirname(context.sessionPath);
|
|
2773
|
+
const sessionsDir = dirname(nativeOwnerDir);
|
|
2774
|
+
const stateRootDir = dirname(sessionsDir);
|
|
2775
|
+
const directoryPaths = [stateRootDir, sessionsDir, nativeOwnerDir];
|
|
2776
|
+
const directoryIdentities = [];
|
|
2777
|
+
try {
|
|
2778
|
+
for (const directoryPath of directoryPaths) {
|
|
2779
|
+
const stat = await transactionDependencies.fs.lstat(directoryPath);
|
|
2780
|
+
if (stat.isSymbolicLink() || !stat.isDirectory())
|
|
2781
|
+
return { status: 'malformed' };
|
|
2782
|
+
directoryIdentities.push({ path: directoryPath, dev: stat.dev, ino: stat.ino });
|
|
2783
|
+
}
|
|
2784
|
+
const pathStat = await transactionDependencies.fs.lstat(context.sessionPath);
|
|
2785
|
+
if (pathStat.isSymbolicLink() || !pathStat.isFile())
|
|
2786
|
+
return { status: 'malformed' };
|
|
2787
|
+
}
|
|
2788
|
+
catch (error) {
|
|
2789
|
+
if (isNotFound(error))
|
|
2790
|
+
return { status: 'absent' };
|
|
2791
|
+
throw error;
|
|
2792
|
+
}
|
|
2793
|
+
const handle = await open(context.sessionPath, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
|
|
2794
|
+
let raw;
|
|
2795
|
+
try {
|
|
2796
|
+
const openedStat = await handle.stat();
|
|
2797
|
+
if (!openedStat.isFile())
|
|
2798
|
+
return { status: 'malformed' };
|
|
2799
|
+
raw = await handle.readFile({ encoding: 'utf8' });
|
|
2800
|
+
const finalPathStat = await transactionDependencies.fs.lstat(context.sessionPath);
|
|
2801
|
+
if (finalPathStat.isSymbolicLink()
|
|
2802
|
+
|| finalPathStat.dev !== openedStat.dev
|
|
2803
|
+
|| finalPathStat.ino !== openedStat.ino) {
|
|
2804
|
+
return { status: 'malformed', raw };
|
|
2805
|
+
}
|
|
2806
|
+
for (const identity of directoryIdentities) {
|
|
2807
|
+
const finalDirectoryStat = await transactionDependencies.fs.lstat(identity.path);
|
|
2808
|
+
if (finalDirectoryStat.isSymbolicLink()
|
|
2809
|
+
|| !finalDirectoryStat.isDirectory()
|
|
2810
|
+
|| finalDirectoryStat.dev !== identity.dev
|
|
2811
|
+
|| finalDirectoryStat.ino !== identity.ino) {
|
|
2812
|
+
return { status: 'malformed', raw };
|
|
2813
|
+
}
|
|
2814
|
+
}
|
|
2815
|
+
}
|
|
2816
|
+
finally {
|
|
2817
|
+
await handle.close();
|
|
2818
|
+
}
|
|
2819
|
+
let pointer;
|
|
2820
|
+
try {
|
|
2821
|
+
pointer = classifyParsedSessionPointer(context, JSON.parse(raw), raw);
|
|
2822
|
+
}
|
|
2823
|
+
catch (error) {
|
|
2824
|
+
if (error instanceof SyntaxError)
|
|
2825
|
+
return { status: 'malformed', raw };
|
|
2826
|
+
throw error;
|
|
2827
|
+
}
|
|
2828
|
+
if (pointer.status === 'absent' || pointer.status === 'foreign-cwd' || pointer.status === 'malformed' || pointer.status === 'identity-indeterminate') {
|
|
2829
|
+
return pointer;
|
|
2830
|
+
}
|
|
2831
|
+
const state = pointer.state;
|
|
2832
|
+
return state?.session_id === normalized
|
|
2833
|
+
&& state.native_session_id === normalized
|
|
2834
|
+
&& state.platform === transactionDependencies.runtimePlatform
|
|
2835
|
+
&& isSessionStateAuthoritativeForCwd(state, context.cwd)
|
|
2836
|
+
? pointer
|
|
2837
|
+
: { status: 'malformed', state, raw: pointer.raw };
|
|
2838
|
+
}
|
|
2839
|
+
export async function readNativeSessionOwner(cwd, nativeSessionId) {
|
|
2840
|
+
try {
|
|
2841
|
+
const evidence = await readNativeSessionOwnerEvidence(cwd, nativeSessionId);
|
|
2842
|
+
return evidence.status === 'usable' ? evidence.state ?? null : null;
|
|
2843
|
+
}
|
|
2844
|
+
catch {
|
|
2845
|
+
return null;
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
1206
2848
|
function reconcileNativeTransition(nativeSessionId, options) {
|
|
1207
2849
|
return (pointer, context) => {
|
|
1208
|
-
|
|
2850
|
+
// Read semantics, not authority: `identity-indeterminate` means "someone may still be here",
|
|
2851
|
+
// and on platforms that record no process-birth evidence EVERY live pointer classifies that way
|
|
2852
|
+
// (see probeIdentitylessProcess). Rejecting it outright refused legitimate sessions with a
|
|
2853
|
+
// generic unusable-pointer abort and skipped lineage/metadata preservation. It is admitted as
|
|
2854
|
+
// existing evidence; the ownership/compatibility checks below still refuse any pointer that does
|
|
2855
|
+
// not match, so a foreign claim gets the exact owner-conflict abort. Genuinely unusable evidence
|
|
2856
|
+
// (malformed, foreign-cwd) is still refused, and authority to own writes still requires 'usable'.
|
|
2857
|
+
if (pointer.status !== 'absent'
|
|
2858
|
+
&& pointer.status !== 'stale-dead'
|
|
2859
|
+
&& pointer.status !== 'usable'
|
|
2860
|
+
&& !(pointer.status === 'identity-indeterminate' && isUnobservableLivePointerState(pointer.state))) {
|
|
1209
2861
|
throw unusablePointerAbort(context, nativeSessionId, pointer);
|
|
1210
2862
|
}
|
|
1211
2863
|
const pid = resolvePid(options);
|
|
1212
2864
|
const platform = options.platform ?? process.platform;
|
|
1213
2865
|
const linuxIdentity = sessionIdentityFor(pid, platform);
|
|
1214
|
-
const
|
|
1215
|
-
const
|
|
2866
|
+
const livenessProven = pointer.status === 'usable';
|
|
2867
|
+
const existing = livenessProven
|
|
2868
|
+
|| (pointer.status === 'identity-indeterminate' && isUnobservableLivePointerState(pointer.state))
|
|
2869
|
+
? pointer.state
|
|
2870
|
+
: undefined;
|
|
2871
|
+
if (existing && !livenessProven && !mayAdoptUnprovenPointer(existing, context.cwd)) {
|
|
2872
|
+
throw ownerConflictAbort(context, nativeSessionId, existing);
|
|
2873
|
+
}
|
|
1216
2874
|
if (!existing) {
|
|
1217
2875
|
const ownerCandidate = verifiedOwnerCandidate(context, options);
|
|
1218
2876
|
const ownerOmxSessionId = ownerCandidate;
|
|
1219
|
-
return {
|
|
1220
|
-
|
|
1221
|
-
|
|
1222
|
-
|
|
1223
|
-
}),
|
|
1224
|
-
};
|
|
2877
|
+
return createSessionState(context.cwd, context.baseStateDir, nativeSessionId, pid, platform, linuxIdentity, {
|
|
2878
|
+
nativeSessionId,
|
|
2879
|
+
...(ownerOmxSessionId ? { ownerOmxSessionId } : {}),
|
|
2880
|
+
});
|
|
1225
2881
|
}
|
|
1226
2882
|
const existingNativeSessionId = normalizeSessionId(existing.native_session_id);
|
|
1227
2883
|
if (existingNativeSessionId && existingNativeSessionId !== nativeSessionId) {
|
|
1228
|
-
|
|
1229
|
-
return {
|
|
1230
|
-
state: createSessionState(context.cwd, nativeSessionId, pid, platform, linuxIdentity, {
|
|
1231
|
-
nativeSessionId,
|
|
1232
|
-
...(ownerOmxSessionId ? {
|
|
1233
|
-
previousNativeSessionId: existingNativeSessionId,
|
|
1234
|
-
nativeSessionSwitchedAt: nowIso,
|
|
1235
|
-
ownerOmxSessionId,
|
|
1236
|
-
} : {}),
|
|
1237
|
-
}),
|
|
1238
|
-
...(ownerOmxSessionId ? {
|
|
1239
|
-
replacementLog: {
|
|
1240
|
-
event: 'native_session_replaced',
|
|
1241
|
-
session_id: ownerOmxSessionId,
|
|
1242
|
-
...(existing.session_id !== ownerOmxSessionId ? { active_session_id: existing.session_id } : {}),
|
|
1243
|
-
previous_native_session_id: existingNativeSessionId,
|
|
1244
|
-
replaced_by_native_session_id: nativeSessionId,
|
|
1245
|
-
pid,
|
|
1246
|
-
timestamp: nowIso,
|
|
1247
|
-
},
|
|
1248
|
-
} : {}),
|
|
1249
|
-
};
|
|
2884
|
+
throw ownerConflictAbort(context, nativeSessionId, existing);
|
|
1250
2885
|
}
|
|
2886
|
+
const nowIso = new Date().toISOString();
|
|
1251
2887
|
const ownerCandidate = verifiedOwnerCandidate(context, options);
|
|
1252
2888
|
let ownerOmxSessionId;
|
|
1253
2889
|
try {
|
|
@@ -1256,39 +2892,37 @@ function reconcileNativeTransition(nativeSessionId, options) {
|
|
|
1256
2892
|
catch (error) {
|
|
1257
2893
|
throw ownerConflictAbort(context, nativeSessionId, existing, error);
|
|
1258
2894
|
}
|
|
1259
|
-
return {
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
2895
|
+
return preserveExistingLaunchLineageToken(existing, createSessionState(context.cwd, context.baseStateDir, existing.session_id, pid, platform, linuxIdentity, {
|
|
2896
|
+
nowIso,
|
|
2897
|
+
nativeSessionId,
|
|
2898
|
+
previousNativeSessionId: existing.previous_native_session_id,
|
|
2899
|
+
nativeSessionSwitchedAt: existing.native_session_switched_at,
|
|
2900
|
+
...(ownerOmxSessionId ? { ownerOmxSessionId } : {}),
|
|
2901
|
+
startedAt: existing.started_at,
|
|
2902
|
+
tmuxSessionName: existing.tmux_session_name,
|
|
2903
|
+
tmuxPaneId: existing.tmux_pane_id,
|
|
2904
|
+
launchLineageToken: isValidToken(existing.launch_lineage_token)
|
|
2905
|
+
? existing.launch_lineage_token
|
|
2906
|
+
: undefined,
|
|
2907
|
+
}));
|
|
1271
2908
|
};
|
|
1272
2909
|
}
|
|
1273
2910
|
/**
|
|
1274
|
-
* Reconcile
|
|
1275
|
-
*
|
|
1276
|
-
*
|
|
2911
|
+
* Reconcile native SessionStart only when the selected pointer is absent, stale,
|
|
2912
|
+
* or already belongs to that native session. A different live native ID is
|
|
2913
|
+
* authoritative owner evidence and must never be replaced.
|
|
1277
2914
|
*/
|
|
1278
2915
|
export async function reconcileNativeSessionStart(cwd, nativeSessionId, options = {}) {
|
|
1279
2916
|
const normalizedNativeSessionId = normalizeSessionId(nativeSessionId);
|
|
1280
|
-
const result = await writePointerTransaction(cwd, normalizedNativeSessionId, options, NATIVE_POINTER_TIMEOUT_MS, reconcileNativeTransition(normalizedNativeSessionId ?? nativeSessionId, options), (
|
|
1281
|
-
if (result.value.replacementLog) {
|
|
1282
|
-
await appendToLogAtContext(result.context, result.value.replacementLog).catch(() => { });
|
|
1283
|
-
}
|
|
2917
|
+
const result = await writePointerTransaction(cwd, normalizedNativeSessionId, options, NATIVE_POINTER_TIMEOUT_MS, reconcileNativeTransition(normalizedNativeSessionId ?? nativeSessionId, options), (state) => state);
|
|
1284
2918
|
await appendToLogAtContext(result.context, {
|
|
1285
|
-
event:
|
|
1286
|
-
session_id: result.value.
|
|
2919
|
+
event: 'session_start_reconciled',
|
|
2920
|
+
session_id: result.value.session_id,
|
|
1287
2921
|
native_session_id: normalizedNativeSessionId ?? nativeSessionId,
|
|
1288
|
-
pid: result.value.
|
|
1289
|
-
timestamp:
|
|
2922
|
+
pid: result.value.pid,
|
|
2923
|
+
timestamp: new Date().toISOString(),
|
|
1290
2924
|
}).catch(() => { });
|
|
1291
|
-
return result.value
|
|
2925
|
+
return result.value;
|
|
1292
2926
|
}
|
|
1293
2927
|
function historyDirectory(context) {
|
|
1294
2928
|
return join(dirname(context.baseStateDir), 'logs');
|
|
@@ -1334,27 +2968,45 @@ export async function writeSessionEnd(cwd, sessionId, options = {}) {
|
|
|
1334
2968
|
cause,
|
|
1335
2969
|
});
|
|
1336
2970
|
}
|
|
2971
|
+
if (!options.binding) {
|
|
2972
|
+
throw resolvedAbort(context, {
|
|
2973
|
+
code: 'session_pointer_io_failure', operation: 'pointer-classify', candidateSessionId,
|
|
2974
|
+
lockPath: context.lockPath, reason: 'A live launch binding is required to end a session pointer.',
|
|
2975
|
+
});
|
|
2976
|
+
}
|
|
2977
|
+
let preLockPointer;
|
|
2978
|
+
traceSessionFinalizationOperation('preLock-read-start');
|
|
1337
2979
|
try {
|
|
1338
|
-
await
|
|
2980
|
+
preLockPointer = await readSessionPointer(context);
|
|
2981
|
+
traceSessionFinalizationOperation(`preLock-read-done:${preLockPointer.status}`);
|
|
1339
2982
|
}
|
|
1340
2983
|
catch (error) {
|
|
1341
2984
|
throw resolvedAbort(context, {
|
|
1342
|
-
code: 'session_pointer_io_failure',
|
|
1343
|
-
|
|
1344
|
-
candidateSessionId,
|
|
1345
|
-
lockPath: context.lockPath,
|
|
1346
|
-
reason: `Unable to create selected session state directory: ${errorMessage(error)}`,
|
|
1347
|
-
cause: error,
|
|
2985
|
+
code: 'session_pointer_io_failure', operation: 'pointer-read', candidateSessionId,
|
|
2986
|
+
lockPath: context.lockPath, reason: `Unable to read selected session pointer: ${errorMessage(error)}`, cause: error,
|
|
1348
2987
|
});
|
|
1349
2988
|
}
|
|
2989
|
+
const preLockAuthorized = isAuthorizedBoundPointerForFinalization(options.binding, context, candidateSessionId, preLockPointer, 'preLock');
|
|
2990
|
+
if (!preLockAuthorized)
|
|
2991
|
+
throw unusablePointerAbort(context, candidateSessionId, preLockPointer);
|
|
2992
|
+
traceSessionFinalizationOperation('pre-transaction-auth-start');
|
|
2993
|
+
await authorizeBoundDirectoryBeforeTransaction(options.binding, preLockPointer, options.postLaunchCwd ?? options.binding.context.cwd);
|
|
2994
|
+
traceSessionFinalizationOperation('pre-transaction-auth-done');
|
|
1350
2995
|
const platform = options.platform ?? process.platform;
|
|
1351
2996
|
const tracker = { degraded: false };
|
|
2997
|
+
traceSessionFinalizationOperation('acquirePointerLock-start');
|
|
1352
2998
|
const lock = await acquirePointerLock(context, candidateSessionId, DEFAULT_POINTER_TIMEOUT_MS, platform, tracker, options.regularFileSync);
|
|
2999
|
+
traceSessionFinalizationOperation('acquirePointerLock-done');
|
|
1353
3000
|
let primary;
|
|
3001
|
+
let revalidation = {
|
|
3002
|
+
comparison: { status: 'not-run' }, capability: [],
|
|
3003
|
+
};
|
|
1354
3004
|
try {
|
|
1355
3005
|
let pointer;
|
|
3006
|
+
traceSessionFinalizationOperation('under-lock-read-start');
|
|
1356
3007
|
try {
|
|
1357
3008
|
pointer = await readSessionPointer(context);
|
|
3009
|
+
traceSessionFinalizationOperation(`under-lock-read-done:${pointer.status}`);
|
|
1358
3010
|
}
|
|
1359
3011
|
catch (error) {
|
|
1360
3012
|
throw resolvedAbort(context, {
|
|
@@ -1366,14 +3018,17 @@ export async function writeSessionEnd(cwd, sessionId, options = {}) {
|
|
|
1366
3018
|
cause: error,
|
|
1367
3019
|
});
|
|
1368
3020
|
}
|
|
1369
|
-
|
|
3021
|
+
const authorized = isAuthorizedBoundPointerForFinalization(options.binding, context, candidateSessionId, pointer, 'under-lock');
|
|
3022
|
+
if (!authorized)
|
|
1370
3023
|
throw unusablePointerAbort(context, candidateSessionId, pointer);
|
|
1371
|
-
|
|
3024
|
+
traceSessionFinalizationOperation('under-lock-auth-start');
|
|
3025
|
+
const authorization = await authorizeBoundDirectoryBeforeTransaction(options.binding, pointer, options.postLaunchCwd ?? options.binding.context.cwd);
|
|
3026
|
+
traceSessionFinalizationOperation('under-lock-auth-done');
|
|
3027
|
+
traceSessionFinalizationOperation('under-lock-consume-start');
|
|
3028
|
+
revalidation = await consumeBoundDirectoryAuthorizationUnderLock(options.binding, authorization, pointer);
|
|
3029
|
+
traceSessionFinalizationOperation('under-lock-consume-done');
|
|
1372
3030
|
const state = pointer.state;
|
|
1373
|
-
const ownsCurrentSessionFile =
|
|
1374
|
-
|| state.session_id === candidateSessionId
|
|
1375
|
-
|| state.native_session_id === candidateSessionId
|
|
1376
|
-
|| state.owner_omx_session_id === candidateSessionId;
|
|
3031
|
+
const ownsCurrentSessionFile = true;
|
|
1377
3032
|
const endTime = new Date().toISOString();
|
|
1378
3033
|
const historyEntry = {
|
|
1379
3034
|
session_id: ownsCurrentSessionFile
|
|
@@ -1389,12 +3044,19 @@ export async function writeSessionEnd(cwd, sessionId, options = {}) {
|
|
|
1389
3044
|
: {}),
|
|
1390
3045
|
...(!ownsCurrentSessionFile && state?.session_id ? { preserved_active_session_id: state.session_id } : {}),
|
|
1391
3046
|
};
|
|
3047
|
+
traceSessionFinalizationOperation('history-mkdir-start');
|
|
1392
3048
|
await nodeMkdir(historyDirectory(context), { recursive: true });
|
|
3049
|
+
traceSessionFinalizationOperation('history-mkdir-done');
|
|
3050
|
+
traceSessionFinalizationOperation('history-append-start');
|
|
1393
3051
|
await appendFile(historyPath(context), `${JSON.stringify(historyEntry)}\n`);
|
|
3052
|
+
traceSessionFinalizationOperation('history-append-done');
|
|
3053
|
+
traceSessionFinalizationOperation('hud-cleanup-start');
|
|
1394
3054
|
await removeDeadSessionHudState(context, [
|
|
1395
3055
|
...(ownsCurrentSessionFile ? [state?.session_id, state?.native_session_id] : []),
|
|
1396
3056
|
candidateSessionId,
|
|
1397
3057
|
]);
|
|
3058
|
+
traceSessionFinalizationOperation('hud-cleanup-done');
|
|
3059
|
+
traceSessionFinalizationOperation('session-unlink-start');
|
|
1398
3060
|
if (ownsCurrentSessionFile) {
|
|
1399
3061
|
try {
|
|
1400
3062
|
await transactionDependencies.fs.unlink(context.sessionPath);
|
|
@@ -1404,6 +3066,8 @@ export async function writeSessionEnd(cwd, sessionId, options = {}) {
|
|
|
1404
3066
|
throw error;
|
|
1405
3067
|
}
|
|
1406
3068
|
}
|
|
3069
|
+
traceSessionFinalizationOperation('session-unlink-done');
|
|
3070
|
+
traceSessionFinalizationOperation('session-end-log-start');
|
|
1407
3071
|
await appendToLogAtContext(context, {
|
|
1408
3072
|
event: 'session_end',
|
|
1409
3073
|
session_id: historyEntry.session_id,
|
|
@@ -1412,11 +3076,14 @@ export async function writeSessionEnd(cwd, sessionId, options = {}) {
|
|
|
1412
3076
|
...(historyEntry.preserved_active_session_id ? { preserved_active_session_id: historyEntry.preserved_active_session_id } : {}),
|
|
1413
3077
|
timestamp: endTime,
|
|
1414
3078
|
}).catch(() => { });
|
|
3079
|
+
traceSessionFinalizationOperation('session-end-log-done');
|
|
1415
3080
|
}
|
|
1416
3081
|
catch (error) {
|
|
1417
3082
|
primary = error;
|
|
1418
3083
|
}
|
|
3084
|
+
traceSessionFinalizationOperation('lock-release-start');
|
|
1419
3085
|
const releaseFailures = await releasePointerLock(lock);
|
|
3086
|
+
traceSessionFinalizationOperation('lock-release-done');
|
|
1420
3087
|
if (primary) {
|
|
1421
3088
|
if (isSessionPointerLaunchAbort(primary) && releaseFailures.length > 0) {
|
|
1422
3089
|
throw recoveryAbort(context, primary, releaseFailures, 'lock-release');
|
|
@@ -1426,6 +3093,7 @@ export async function writeSessionEnd(cwd, sessionId, options = {}) {
|
|
|
1426
3093
|
if (releaseFailures.length > 0)
|
|
1427
3094
|
throw releaseFailureError(releaseFailures);
|
|
1428
3095
|
emitDegradedDurabilityWarning('session pointer end', tracker);
|
|
3096
|
+
return revalidation;
|
|
1429
3097
|
}
|
|
1430
3098
|
/** Reset session-scoped HUD/metrics files at launch. */
|
|
1431
3099
|
export async function resetSessionMetrics(cwd, sessionId) {
|