oh-my-codex 0.20.1 → 0.20.3
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 +7 -6
- package/Cargo.toml +1 -1
- package/README.md +36 -1
- package/crates/omx-runtime/Cargo.toml +1 -0
- package/crates/omx-runtime/src/main.rs +25 -2
- package/crates/omx-runtime/tests/execution.rs +219 -0
- package/crates/omx-runtime-core/src/dispatch.rs +96 -10
- package/crates/omx-runtime-core/src/engine.rs +839 -29
- package/crates/omx-runtime-core/src/lib.rs +8 -0
- package/dist/adapt/__tests__/hermes.test.js.map +1 -1
- package/dist/adapt/contracts.d.ts.map +1 -1
- package/dist/agents/__tests__/definitions.test.js +81 -0
- package/dist/agents/__tests__/definitions.test.js.map +1 -1
- package/dist/agents/__tests__/native-config.test.js +25 -0
- package/dist/agents/__tests__/native-config.test.js.map +1 -1
- package/dist/agents/native-config.d.ts +2 -1
- package/dist/agents/native-config.d.ts.map +1 -1
- package/dist/agents/native-config.js.map +1 -1
- package/dist/auth/__tests__/hotswap.test.d.ts +2 -0
- package/dist/auth/__tests__/hotswap.test.d.ts.map +1 -0
- package/dist/auth/__tests__/hotswap.test.js +183 -0
- package/dist/auth/__tests__/hotswap.test.js.map +1 -0
- package/dist/auth/hotswap.d.ts +6 -0
- package/dist/auth/hotswap.d.ts.map +1 -1
- package/dist/auth/hotswap.js +102 -19
- package/dist/auth/hotswap.js.map +1 -1
- package/dist/auth/paths.js.map +1 -1
- package/dist/auth/storage.js.map +1 -1
- package/dist/autopilot/__tests__/ralplan-gate.test.js +5 -5
- package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
- package/dist/autopilot/completion-gate.js.map +1 -1
- package/dist/autopilot/fsm.d.ts +1 -1
- package/dist/autopilot/fsm.d.ts.map +1 -1
- package/dist/autoresearch/goal.js.map +1 -1
- package/dist/autoresearch/runtime.js.map +1 -1
- package/dist/capabilities/lockfile.js.map +1 -1
- package/dist/catalog/reader.js.map +1 -1
- package/dist/catalog/skill-mirror.js.map +1 -1
- package/dist/cli/__tests__/adapt-help.test.js.map +1 -1
- package/dist/cli/__tests__/agents-init.test.js.map +1 -1
- package/dist/cli/__tests__/agents.test.js.map +1 -1
- package/dist/cli/__tests__/api.test.js.map +1 -1
- package/dist/cli/__tests__/ask.test.js.map +1 -1
- package/dist/cli/__tests__/auth.test.js +60 -6
- package/dist/cli/__tests__/auth.test.js.map +1 -1
- package/dist/cli/__tests__/autoresearch-guided.test.js.map +1 -1
- package/dist/cli/__tests__/autoresearch.test.js.map +1 -1
- package/dist/cli/__tests__/capabilities.test.js.map +1 -1
- package/dist/cli/__tests__/codex-plugin-layout.test.js +80 -0
- package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-context-window-warning.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-invalid-config.test.js +47 -4
- package/dist/cli/__tests__/doctor-invalid-config.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-team.test.js.map +1 -1
- package/dist/cli/__tests__/doctor-warning-copy.test.js +752 -15
- package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
- package/dist/cli/__tests__/exec.test.js +136 -0
- package/dist/cli/__tests__/exec.test.js.map +1 -1
- package/dist/cli/__tests__/imagegen-continuation.test.js.map +1 -1
- package/dist/cli/__tests__/index.test.js +473 -14
- package/dist/cli/__tests__/index.test.js.map +1 -1
- package/dist/cli/__tests__/launch-fallback.test.js +336 -4
- package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
- package/dist/cli/__tests__/mcp-parity.test.js +87 -12
- package/dist/cli/__tests__/mcp-parity.test.js.map +1 -1
- package/dist/cli/__tests__/native-hook-claim-journal.test.d.ts +2 -0
- package/dist/cli/__tests__/native-hook-claim-journal.test.d.ts.map +1 -0
- package/dist/cli/__tests__/native-hook-claim-journal.test.js +206 -0
- package/dist/cli/__tests__/native-hook-claim-journal.test.js.map +1 -0
- package/dist/cli/__tests__/nested-help-routing.test.js.map +1 -1
- package/dist/cli/__tests__/question.test.js.map +1 -1
- package/dist/cli/__tests__/ralph-deslop-contract.test.js.map +1 -1
- package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +6 -0
- package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -1
- package/dist/cli/__tests__/ralph-prd-deep-interview.test.js.map +1 -1
- package/dist/cli/__tests__/ralph-prd-smoke.test.js.map +1 -1
- package/dist/cli/__tests__/ralph.test.js +8 -3
- package/dist/cli/__tests__/ralph.test.js.map +1 -1
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.d.ts +2 -0
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.d.ts.map +1 -0
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js +72 -0
- package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js.map +1 -0
- package/dist/cli/__tests__/ralplan.test.d.ts +2 -0
- package/dist/cli/__tests__/ralplan.test.d.ts.map +1 -0
- package/dist/cli/__tests__/ralplan.test.js +55 -0
- package/dist/cli/__tests__/ralplan.test.js.map +1 -0
- package/dist/cli/__tests__/resume.test.js +17 -11
- package/dist/cli/__tests__/resume.test.js.map +1 -1
- package/dist/cli/__tests__/session-scoped-runtime.test.js +25 -0
- package/dist/cli/__tests__/session-scoped-runtime.test.js.map +1 -1
- package/dist/cli/__tests__/session-search-help.test.js +25 -1
- package/dist/cli/__tests__/session-search-help.test.js.map +1 -1
- package/dist/cli/__tests__/session-search.test.js.map +1 -1
- package/dist/cli/__tests__/setup-agents-overwrite.test.js +79 -0
- package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
- package/dist/cli/__tests__/setup-gh-star.test.js.map +1 -1
- package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js +77 -3
- package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js.map +1 -1
- package/dist/cli/__tests__/setup-hooks-trust-e2e.test.d.ts +2 -0
- package/dist/cli/__tests__/setup-hooks-trust-e2e.test.d.ts.map +1 -0
- package/dist/cli/__tests__/setup-hooks-trust-e2e.test.js +285 -0
- package/dist/cli/__tests__/setup-hooks-trust-e2e.test.js.map +1 -0
- package/dist/cli/__tests__/setup-install-mode.test.js +2579 -294
- package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
- package/dist/cli/__tests__/setup-preferences.test.d.ts +2 -0
- package/dist/cli/__tests__/setup-preferences.test.d.ts.map +1 -0
- package/dist/cli/__tests__/setup-preferences.test.js +79 -0
- package/dist/cli/__tests__/setup-preferences.test.js.map +1 -0
- package/dist/cli/__tests__/setup-refresh.test.js +33 -2
- package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
- package/dist/cli/__tests__/setup-scope.test.js +46 -3
- package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
- package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
- package/dist/cli/__tests__/state.test.js +37 -0
- package/dist/cli/__tests__/state.test.js.map +1 -1
- package/dist/cli/__tests__/team.test.js +105 -7
- package/dist/cli/__tests__/team.test.js.map +1 -1
- package/dist/cli/__tests__/ultragoal.test.js +61 -1
- package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
- package/dist/cli/__tests__/uninstall.test.js +1817 -24
- package/dist/cli/__tests__/uninstall.test.js.map +1 -1
- package/dist/cli/__tests__/update.test.js +58 -0
- package/dist/cli/__tests__/update.test.js.map +1 -1
- package/dist/cli/__tests__/url.test.js.map +1 -1
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js +2 -1
- package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
- package/dist/cli/adapt.js.map +1 -1
- package/dist/cli/agents-init.js.map +1 -1
- package/dist/cli/agents.js.map +1 -1
- package/dist/cli/api.js.map +1 -1
- package/dist/cli/ask.d.ts +1 -1
- package/dist/cli/ask.d.ts.map +1 -1
- package/dist/cli/ask.js.map +1 -1
- package/dist/cli/auth.js.map +1 -1
- package/dist/cli/autoresearch-guided.js.map +1 -1
- package/dist/cli/autoresearch-intake.js.map +1 -1
- package/dist/cli/capabilities.js.map +1 -1
- package/dist/cli/cleanup.js.map +1 -1
- package/dist/cli/codex-feature-probe.js.map +1 -1
- package/dist/cli/codex-home.js.map +1 -1
- package/dist/cli/doctor.d.ts +21 -0
- package/dist/cli/doctor.d.ts.map +1 -1
- package/dist/cli/doctor.js +607 -130
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/explore.js.map +1 -1
- package/dist/cli/index.d.ts +14 -3
- package/dist/cli/index.d.ts.map +1 -1
- package/dist/cli/index.js +245 -57
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/mcp-parity.js +2 -2
- package/dist/cli/mcp-parity.js.map +1 -1
- package/dist/cli/mcp-serve.js.map +1 -1
- package/dist/cli/mission.js.map +1 -1
- package/dist/cli/native-assets.js.map +1 -1
- package/dist/cli/native-hook-claim-journal.d.ts +29 -0
- package/dist/cli/native-hook-claim-journal.d.ts.map +1 -0
- package/dist/cli/native-hook-claim-journal.js +252 -0
- package/dist/cli/native-hook-claim-journal.js.map +1 -0
- package/dist/cli/omx.js.map +1 -1
- package/dist/cli/plugin-marketplace.js.map +1 -1
- package/dist/cli/project-runtime-codex-homes.js.map +1 -1
- package/dist/cli/question.js.map +1 -1
- package/dist/cli/ralph.d.ts.map +1 -1
- package/dist/cli/ralph.js +3 -2
- package/dist/cli/ralph.js.map +1 -1
- package/dist/cli/ralplan.d.ts +10 -0
- package/dist/cli/ralplan.d.ts.map +1 -0
- package/dist/cli/ralplan.js +110 -0
- package/dist/cli/ralplan.js.map +1 -0
- package/dist/cli/session-search.d.ts +5 -0
- package/dist/cli/session-search.d.ts.map +1 -1
- package/dist/cli/session-search.js +87 -0
- package/dist/cli/session-search.js.map +1 -1
- package/dist/cli/setup-preferences.d.ts +11 -0
- package/dist/cli/setup-preferences.d.ts.map +1 -1
- package/dist/cli/setup-preferences.js +32 -0
- package/dist/cli/setup-preferences.js.map +1 -1
- package/dist/cli/setup.d.ts +58 -0
- package/dist/cli/setup.d.ts.map +1 -1
- package/dist/cli/setup.js +1450 -398
- package/dist/cli/setup.js.map +1 -1
- package/dist/cli/sparkshell.js.map +1 -1
- package/dist/cli/star-prompt.js.map +1 -1
- package/dist/cli/state.d.ts.map +1 -1
- package/dist/cli/state.js +2 -1
- package/dist/cli/state.js.map +1 -1
- package/dist/cli/team.js.map +1 -1
- package/dist/cli/ultragoal.d.ts +5 -1
- package/dist/cli/ultragoal.d.ts.map +1 -1
- package/dist/cli/ultragoal.js +34 -2
- package/dist/cli/ultragoal.js.map +1 -1
- package/dist/cli/uninstall.d.ts +17 -0
- package/dist/cli/uninstall.d.ts.map +1 -1
- package/dist/cli/uninstall.js +986 -95
- package/dist/cli/uninstall.js.map +1 -1
- package/dist/cli/update.d.ts.map +1 -1
- package/dist/cli/update.js +8 -1
- package/dist/cli/update.js.map +1 -1
- package/dist/cli/url.js.map +1 -1
- package/dist/compat/__tests__/doctor-contract.test.js +5 -1
- package/dist/compat/__tests__/doctor-contract.test.js.map +1 -1
- package/dist/compat/__tests__/rust-runtime-compat.test.js +1 -1
- package/dist/compat/__tests__/rust-runtime-compat.test.js.map +1 -1
- package/dist/config/__tests__/codex-hooks.test.js +1287 -11
- package/dist/config/__tests__/codex-hooks.test.js.map +1 -1
- package/dist/config/__tests__/generator-idempotent.test.js +1179 -32
- package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
- package/dist/config/__tests__/generator-notify.test.js +6 -1
- package/dist/config/__tests__/generator-notify.test.js.map +1 -1
- package/dist/config/__tests__/models.test.js +67 -3
- package/dist/config/__tests__/models.test.js.map +1 -1
- package/dist/config/codex-feature-flags.d.ts.map +1 -1
- package/dist/config/codex-feature-flags.js.map +1 -1
- package/dist/config/codex-hooks.d.ts +124 -1
- package/dist/config/codex-hooks.d.ts.map +1 -1
- package/dist/config/codex-hooks.js +2142 -265
- package/dist/config/codex-hooks.js.map +1 -1
- package/dist/config/commit-lore-guard.js.map +1 -1
- package/dist/config/generator.d.ts +23 -34
- package/dist/config/generator.d.ts.map +1 -1
- package/dist/config/generator.js +1480 -246
- package/dist/config/generator.js.map +1 -1
- package/dist/config/mcp-registry.js.map +1 -1
- package/dist/config/models.d.ts +14 -4
- package/dist/config/models.d.ts.map +1 -1
- package/dist/config/models.js +15 -1
- package/dist/config/models.js.map +1 -1
- package/dist/config/omx-first-party-mcp.js.map +1 -1
- package/dist/config/team-mode.d.ts.map +1 -1
- package/dist/document-refresh/__tests__/enforcer.test.js.map +1 -1
- package/dist/document-refresh/enforcer.js.map +1 -1
- package/dist/exec/followup.js.map +1 -1
- package/dist/goal-workflows/artifacts.js.map +1 -1
- package/dist/hooks/__tests__/agents-overlay.test.js +7 -4
- package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
- package/dist/hooks/__tests__/analyze-routing-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/analyze-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/anti-slop-workflow.test.js.map +1 -1
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js +9 -3
- package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/code-review-skill-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/consensus-execution-handoff.test.js.map +1 -1
- package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/design-skill.test.js.map +1 -1
- package/dist/hooks/__tests__/keyword-detector.test.js +1376 -6
- package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js +252 -543
- package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +38 -4
- package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +765 -10
- package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js +2 -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 +142 -1
- package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-modules.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-non-omx-guard.test.js +48 -2
- package/dist/hooks/__tests__/notify-hook-non-omx-guard.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js +61 -44
- package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-regression-205.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.js +3 -2
- package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-session-scope.test.js +243 -4
- package/dist/hooks/__tests__/notify-hook-session-scope.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +531 -12
- package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +163 -24
- package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js +482 -32
- package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +68 -1
- package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js +7 -1
- package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-visual-verdict.test.js.map +1 -1
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +43 -4
- package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
- package/dist/hooks/__tests__/openclaw-setup-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/pre-context-gate-skills.test.js.map +1 -1
- package/dist/hooks/__tests__/prometheus-strict-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/prompt-guidance-fragments.test.js.map +1 -1
- package/dist/hooks/__tests__/prompt-guidance-test-helpers.js.map +1 -1
- package/dist/hooks/__tests__/prompt-session-provenance.test.d.ts +2 -0
- package/dist/hooks/__tests__/prompt-session-provenance.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/prompt-session-provenance.test.js +82 -0
- package/dist/hooks/__tests__/prompt-session-provenance.test.js.map +1 -0
- package/dist/hooks/__tests__/session.test.js +878 -11
- package/dist/hooks/__tests__/session.test.js.map +1 -1
- package/dist/hooks/__tests__/skill-catalog-hygiene.test.js.map +1 -1
- package/dist/hooks/__tests__/team-runtime-gating-docs-contract.test.js.map +1 -1
- package/dist/hooks/__tests__/visual-ralph-skill.test.js.map +1 -1
- package/dist/hooks/__tests__/visual-verdict-loop.test.js.map +1 -1
- package/dist/hooks/agents-overlay.js +5 -5
- package/dist/hooks/agents-overlay.js.map +1 -1
- package/dist/hooks/code-simplifier/index.js.map +1 -1
- package/dist/hooks/explore-routing.js.map +1 -1
- package/dist/hooks/extensibility/dispatcher.js.map +1 -1
- package/dist/hooks/extensibility/events.js.map +1 -1
- package/dist/hooks/extensibility/loader.js.map +1 -1
- package/dist/hooks/extensibility/plugin-runner-stdin.js.map +1 -1
- package/dist/hooks/extensibility/sdk/logging.js.map +1 -1
- package/dist/hooks/keyword-detector.d.ts +33 -7
- package/dist/hooks/keyword-detector.d.ts.map +1 -1
- package/dist/hooks/keyword-detector.js +2228 -119
- package/dist/hooks/keyword-detector.js.map +1 -1
- package/dist/hooks/keyword-registry.d.ts +12 -0
- package/dist/hooks/keyword-registry.d.ts.map +1 -1
- package/dist/hooks/keyword-registry.js +41 -0
- package/dist/hooks/keyword-registry.js.map +1 -1
- package/dist/hooks/prompt-session-provenance.d.ts +87 -0
- package/dist/hooks/prompt-session-provenance.d.ts.map +1 -0
- package/dist/hooks/prompt-session-provenance.js +180 -0
- package/dist/hooks/prompt-session-provenance.js.map +1 -0
- package/dist/hooks/session.d.ts +163 -34
- package/dist/hooks/session.d.ts.map +1 -1
- package/dist/hooks/session.js +1395 -317
- package/dist/hooks/session.js.map +1 -1
- package/dist/hooks/task-size-detector.js.map +1 -1
- package/dist/hud/__tests__/state.test.js.map +1 -1
- package/dist/hud/authority.js.map +1 -1
- package/dist/hud/constants.js.map +1 -1
- package/dist/hud/index.js.map +1 -1
- package/dist/hud/reconcile.js.map +1 -1
- package/dist/hud/render.js.map +1 -1
- package/dist/hud/state.js.map +1 -1
- package/dist/hud/tmux.js.map +1 -1
- package/dist/leader/__tests__/contract.test.js +153 -1
- package/dist/leader/__tests__/contract.test.js.map +1 -1
- package/dist/leader/contract.d.ts +42 -5
- package/dist/leader/contract.d.ts.map +1 -1
- package/dist/leader/contract.js +184 -3
- package/dist/leader/contract.js.map +1 -1
- package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
- package/dist/mcp/__tests__/code-intel-server.test.js +31 -3
- package/dist/mcp/__tests__/code-intel-server.test.js.map +1 -1
- package/dist/mcp/__tests__/server-lifecycle.test.js +15 -2
- package/dist/mcp/__tests__/server-lifecycle.test.js.map +1 -1
- package/dist/mcp/__tests__/state-paths.test.js +98 -1
- package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
- package/dist/mcp/__tests__/wiki-server.test.js +18 -0
- package/dist/mcp/__tests__/wiki-server.test.js.map +1 -1
- package/dist/mcp/bootstrap.js.map +1 -1
- package/dist/mcp/code-intel-server.d.ts +58 -58
- package/dist/mcp/code-intel-server.d.ts.map +1 -1
- package/dist/mcp/code-intel-server.js +13 -3
- package/dist/mcp/code-intel-server.js.map +1 -1
- package/dist/mcp/hermes-bridge.js.map +1 -1
- package/dist/mcp/hermes-server.d.ts +70 -70
- package/dist/mcp/hermes-server.d.ts.map +1 -1
- package/dist/mcp/lifecycle-telemetry.js.map +1 -1
- package/dist/mcp/memory-server.d.ts +34 -34
- package/dist/mcp/memory-server.d.ts.map +1 -1
- package/dist/mcp/memory-server.js.map +1 -1
- package/dist/mcp/state-paths.d.ts +14 -0
- package/dist/mcp/state-paths.d.ts.map +1 -1
- package/dist/mcp/state-paths.js +96 -20
- package/dist/mcp/state-paths.js.map +1 -1
- package/dist/mcp/state-server.d.ts +47 -47
- package/dist/mcp/state-server.d.ts.map +1 -1
- package/dist/mcp/trace-server.d.ts +4 -4
- package/dist/mcp/trace-server.d.ts.map +1 -1
- package/dist/mcp/trace-server.js.map +1 -1
- package/dist/mcp/wiki-server.d.ts +34 -23
- package/dist/mcp/wiki-server.d.ts.map +1 -1
- package/dist/mcp/wiki-server.js +9 -2
- package/dist/mcp/wiki-server.js.map +1 -1
- package/dist/modes/__tests__/base-session-scope.test.js +53 -1
- package/dist/modes/__tests__/base-session-scope.test.js.map +1 -1
- package/dist/modes/__tests__/base-tmux-pane.test.js +4 -4
- package/dist/modes/__tests__/base-tmux-pane.test.js.map +1 -1
- package/dist/modes/base.d.ts.map +1 -1
- package/dist/modes/base.js +5 -5
- package/dist/modes/base.js.map +1 -1
- package/dist/notifications/__tests__/lifecycle-dedupe.test.js.map +1 -1
- package/dist/notifications/__tests__/reply-listener.test.js.map +1 -1
- package/dist/notifications/__tests__/session-idle-tail-dedupe.test.js +177 -85
- package/dist/notifications/__tests__/session-idle-tail-dedupe.test.js.map +1 -1
- package/dist/notifications/__tests__/session-registry.test.js.map +1 -1
- package/dist/notifications/__tests__/template-engine.test.js.map +1 -1
- package/dist/notifications/__tests__/verbosity.test.js.map +1 -1
- package/dist/notifications/http-client.js.map +1 -1
- package/dist/notifications/index.d.ts +4 -1
- package/dist/notifications/index.d.ts.map +1 -1
- package/dist/notifications/index.js +3 -9
- package/dist/notifications/index.js.map +1 -1
- package/dist/notifications/lifecycle-dedupe.js.map +1 -1
- package/dist/notifications/reply-listener.d.ts.map +1 -1
- package/dist/notifications/reply-listener.js.map +1 -1
- package/dist/notifications/session-registry.js.map +1 -1
- package/dist/notifications/session-status.js.map +1 -1
- package/dist/notifications/temp-contract.d.ts.map +1 -1
- package/dist/notifications/temp-contract.js +4 -0
- package/dist/notifications/temp-contract.js.map +1 -1
- package/dist/notifications/tmux-detector.js.map +1 -1
- package/dist/notifications/tmux.js.map +1 -1
- package/dist/openclaw/config.js.map +1 -1
- package/dist/openclaw/dispatcher.js.map +1 -1
- package/dist/pipeline/__tests__/orchestrator.test.js +4 -4
- package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
- package/dist/pipeline/__tests__/stages.test.js +16 -13
- package/dist/pipeline/__tests__/stages.test.js.map +1 -1
- package/dist/pipeline/stages/code-review.js.map +1 -1
- package/dist/pipeline/stages/ralph-verify.js.map +1 -1
- package/dist/pipeline/stages/ralplan.js.map +1 -1
- package/dist/pipeline/stages/team-exec.js.map +1 -1
- package/dist/pipeline/stages/ultragoal.js.map +1 -1
- package/dist/pipeline/stages/ultraqa.js.map +1 -1
- package/dist/planning/__tests__/approved-execution-lifecycle-matrix.test.js.map +1 -1
- package/dist/planning/artifact-names.js.map +1 -1
- package/dist/planning/artifacts.js.map +1 -1
- package/dist/question/__tests__/state.test.js.map +1 -1
- package/dist/question/__tests__/ui.test.js.map +1 -1
- package/dist/question/autopilot-wait.js.map +1 -1
- package/dist/question/client.d.ts.map +1 -1
- package/dist/question/client.js.map +1 -1
- package/dist/question/deep-interview.js.map +1 -1
- package/dist/question/events.js.map +1 -1
- package/dist/question/renderer.js.map +1 -1
- package/dist/question/state.d.ts.map +1 -1
- package/dist/question/state.js.map +1 -1
- package/dist/question/ui.js.map +1 -1
- package/dist/ralph/contract.d.ts +1 -1
- package/dist/ralph/contract.d.ts.map +1 -1
- package/dist/ralplan/__tests__/consensus-gate.test.js +527 -8
- package/dist/ralplan/__tests__/consensus-gate.test.js.map +1 -1
- package/dist/ralplan/__tests__/documented-leader-preflight.test.d.ts +2 -0
- package/dist/ralplan/__tests__/documented-leader-preflight.test.d.ts.map +1 -0
- package/dist/ralplan/__tests__/documented-leader-preflight.test.js +46 -0
- package/dist/ralplan/__tests__/documented-leader-preflight.test.js.map +1 -0
- package/dist/ralplan/__tests__/runtime.test.js +116 -14
- package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
- package/dist/ralplan/consensus-gate.d.ts +3 -3
- package/dist/ralplan/consensus-gate.d.ts.map +1 -1
- package/dist/ralplan/consensus-gate.js +173 -70
- package/dist/ralplan/consensus-gate.js.map +1 -1
- package/dist/ralplan/documented-leader-preflight.d.ts +34 -0
- package/dist/ralplan/documented-leader-preflight.d.ts.map +1 -0
- package/dist/ralplan/documented-leader-preflight.js +58 -0
- package/dist/ralplan/documented-leader-preflight.js.map +1 -0
- package/dist/ralplan/runtime.d.ts +3 -2
- package/dist/ralplan/runtime.d.ts.map +1 -1
- package/dist/ralplan/runtime.js +12 -9
- package/dist/ralplan/runtime.js.map +1 -1
- package/dist/runtime/__tests__/bridge.test.js +42 -1
- package/dist/runtime/__tests__/bridge.test.js.map +1 -1
- package/dist/runtime/bridge.d.ts +12 -0
- package/dist/runtime/bridge.d.ts.map +1 -1
- package/dist/runtime/bridge.js +52 -1
- package/dist/runtime/bridge.js.map +1 -1
- package/dist/runtime/process-tree.js.map +1 -1
- package/dist/runtime/run-loop.js.map +1 -1
- package/dist/runtime/run-outcome.d.ts +3 -3
- package/dist/runtime/run-outcome.d.ts.map +1 -1
- package/dist/runtime/run-outcome.js.map +1 -1
- package/dist/runtime/run-state.js.map +1 -1
- package/dist/scripts/__tests__/codex-native-hook.test.js +6657 -995
- package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
- package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.d.ts +2 -0
- package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.d.ts.map +1 -0
- package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.js +634 -0
- package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.js.map +1 -0
- package/dist/scripts/__tests__/generate-release-body.test.js.map +1 -1
- package/dist/scripts/__tests__/hook-derived-watcher.test.js.map +1 -1
- package/dist/scripts/__tests__/notify-dispatcher.test.js.map +1 -1
- package/dist/scripts/__tests__/notify-fallback-delivery.test.d.ts +2 -0
- package/dist/scripts/__tests__/notify-fallback-delivery.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/notify-fallback-delivery.test.js +174 -0
- package/dist/scripts/__tests__/notify-fallback-delivery.test.js.map +1 -0
- package/dist/scripts/__tests__/notify-state-io.test.js +57 -1
- package/dist/scripts/__tests__/notify-state-io.test.js.map +1 -1
- package/dist/scripts/__tests__/reasoning-artifact-contract.test.d.ts +2 -0
- package/dist/scripts/__tests__/reasoning-artifact-contract.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/reasoning-artifact-contract.test.js +108 -0
- package/dist/scripts/__tests__/reasoning-artifact-contract.test.js.map +1 -0
- package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.d.ts +2 -0
- package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js +78 -0
- package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js.map +1 -0
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.d.ts +2 -0
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.d.ts.map +1 -0
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js +55 -0
- package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js.map +1 -0
- package/dist/scripts/__tests__/run-test-files.test.js.map +1 -1
- package/dist/scripts/__tests__/smoke-packed-install.test.js +1100 -4
- package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
- package/dist/scripts/__tests__/test-reply-listener-live.test.js.map +1 -1
- package/dist/scripts/build-api.js.map +1 -1
- package/dist/scripts/build-explore-harness.js.map +1 -1
- package/dist/scripts/build-sparkshell.js.map +1 -1
- package/dist/scripts/cleanup-explore-harness.js.map +1 -1
- package/dist/scripts/codex-execution-surface.js.map +1 -1
- package/dist/scripts/codex-native-hook.d.ts +4 -1
- package/dist/scripts/codex-native-hook.d.ts.map +1 -1
- package/dist/scripts/codex-native-hook.js +11577 -1184
- package/dist/scripts/codex-native-hook.js.map +1 -1
- package/dist/scripts/generate-native-release-manifest.js.map +1 -1
- package/dist/scripts/generate-release-body.js.map +1 -1
- package/dist/scripts/hook-payload-guard.d.ts.map +1 -1
- package/dist/scripts/hook-payload-guard.js.map +1 -1
- package/dist/scripts/notify-fallback-delivery.d.ts +48 -0
- package/dist/scripts/notify-fallback-delivery.d.ts.map +1 -0
- package/dist/scripts/notify-fallback-delivery.js +419 -0
- package/dist/scripts/notify-fallback-delivery.js.map +1 -0
- package/dist/scripts/notify-fallback-watcher.js +218 -138
- package/dist/scripts/notify-fallback-watcher.js.map +1 -1
- package/dist/scripts/notify-hook/auto-nudge.d.ts +18 -9
- package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
- package/dist/scripts/notify-hook/auto-nudge.js +112 -15
- package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
- package/dist/scripts/notify-hook/managed-tmux.d.ts +23 -0
- package/dist/scripts/notify-hook/managed-tmux.d.ts.map +1 -1
- package/dist/scripts/notify-hook/managed-tmux.js +122 -12
- package/dist/scripts/notify-hook/managed-tmux.js.map +1 -1
- package/dist/scripts/notify-hook/orchestration-intent.d.ts +7 -7
- package/dist/scripts/notify-hook/orchestration-intent.d.ts.map +1 -1
- package/dist/scripts/notify-hook/ralph-session-resume.d.ts +5 -2
- package/dist/scripts/notify-hook/ralph-session-resume.d.ts.map +1 -1
- package/dist/scripts/notify-hook/ralph-session-resume.js +33 -37
- package/dist/scripts/notify-hook/ralph-session-resume.js.map +1 -1
- package/dist/scripts/notify-hook/state-io.d.ts +17 -0
- package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
- package/dist/scripts/notify-hook/state-io.js +73 -11
- package/dist/scripts/notify-hook/state-io.js.map +1 -1
- package/dist/scripts/notify-hook/team-dispatch.d.ts +10 -1
- package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-dispatch.js +234 -79
- package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
- package/dist/scripts/notify-hook/team-leader-nudge.d.ts +2 -2
- package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-leader-nudge.js +39 -13
- package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.d.ts +16 -8
- package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-tmux-guard.js +365 -44
- package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
- package/dist/scripts/notify-hook/team-worker-posttooluse.js.map +1 -1
- package/dist/scripts/notify-hook/team-worker-stop.d.ts +1 -1
- package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-worker-stop.js +19 -18
- package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
- package/dist/scripts/notify-hook/team-worker.d.ts +5 -2
- package/dist/scripts/notify-hook/team-worker.d.ts.map +1 -1
- package/dist/scripts/notify-hook/team-worker.js +37 -28
- package/dist/scripts/notify-hook/team-worker.js.map +1 -1
- package/dist/scripts/notify-hook/tmux-injection.d.ts +8 -1
- package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
- package/dist/scripts/notify-hook/tmux-injection.js +53 -18
- package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
- package/dist/scripts/notify-hook/visual-verdict.d.ts +1 -1
- package/dist/scripts/notify-hook/visual-verdict.d.ts.map +1 -1
- package/dist/scripts/notify-hook/visual-verdict.js +15 -13
- package/dist/scripts/notify-hook/visual-verdict.js.map +1 -1
- package/dist/scripts/notify-hook.d.ts +24 -1
- package/dist/scripts/notify-hook.d.ts.map +1 -1
- package/dist/scripts/notify-hook.js +267 -90
- package/dist/scripts/notify-hook.js.map +1 -1
- package/dist/scripts/postinstall.js.map +1 -1
- package/dist/scripts/prompt-inventory.js.map +1 -1
- package/dist/scripts/smoke-packed-install.d.ts +927 -5
- package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
- package/dist/scripts/smoke-packed-install.js +4317 -13
- package/dist/scripts/smoke-packed-install.js.map +1 -1
- package/dist/scripts/sync-plugin-mirror.js.map +1 -1
- package/dist/scripts/test-reply-listener-live.js.map +1 -1
- package/dist/scripts/test-sparkshell.js.map +1 -1
- package/dist/scripts/verify-native-agents.js.map +1 -1
- package/dist/session-history/friction.js.map +1 -1
- package/dist/session-history/search.js.map +1 -1
- package/dist/sidecar/__tests__/boundary.test.js.map +1 -1
- package/dist/sidecar/collector.js.map +1 -1
- package/dist/sidecar/index.js.map +1 -1
- package/dist/sidecar/render.js.map +1 -1
- package/dist/sidecar/tmux.js.map +1 -1
- package/dist/state/__tests__/mode-state-context.test.js +54 -0
- package/dist/state/__tests__/mode-state-context.test.js.map +1 -1
- package/dist/state/__tests__/operations.test.js +66 -15
- package/dist/state/__tests__/operations.test.js.map +1 -1
- package/dist/state/__tests__/skill-active.test.js +19 -1
- package/dist/state/__tests__/skill-active.test.js.map +1 -1
- package/dist/state/__tests__/workflow-transition.test.js +61 -0
- package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
- package/dist/state/mode-state-context.d.ts +5 -0
- package/dist/state/mode-state-context.d.ts.map +1 -1
- package/dist/state/mode-state-context.js +59 -0
- package/dist/state/mode-state-context.js.map +1 -1
- package/dist/state/operations.d.ts +1 -1
- package/dist/state/operations.d.ts.map +1 -1
- package/dist/state/operations.js +49 -55
- package/dist/state/operations.js.map +1 -1
- package/dist/state/paths.d.ts +1 -1
- package/dist/state/paths.d.ts.map +1 -1
- package/dist/state/paths.js +1 -1
- package/dist/state/paths.js.map +1 -1
- package/dist/state/skill-active.d.ts +13 -1
- package/dist/state/skill-active.d.ts.map +1 -1
- package/dist/state/skill-active.js +43 -4
- package/dist/state/skill-active.js.map +1 -1
- package/dist/state/terminal-normalization.js.map +1 -1
- package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
- package/dist/state/workflow-transition-reconcile.js +20 -4
- package/dist/state/workflow-transition-reconcile.js.map +1 -1
- package/dist/state/workflow-transition.d.ts +1 -1
- package/dist/state/workflow-transition.d.ts.map +1 -1
- package/dist/state/workflow-transition.js.map +1 -1
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.d.ts +2 -0
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.d.ts.map +1 -0
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.js +52 -0
- package/dist/subagents/__tests__/leader-bootstrap-3181.test.js.map +1 -0
- package/dist/subagents/__tests__/tracker.test.js +1550 -10
- package/dist/subagents/__tests__/tracker.test.js.map +1 -1
- package/dist/subagents/role-routing-marker.d.ts +10 -0
- package/dist/subagents/role-routing-marker.d.ts.map +1 -0
- package/dist/subagents/role-routing-marker.js +136 -0
- package/dist/subagents/role-routing-marker.js.map +1 -0
- package/dist/subagents/tracker.d.ts +91 -0
- package/dist/subagents/tracker.d.ts.map +1 -1
- package/dist/subagents/tracker.js +806 -62
- package/dist/subagents/tracker.js.map +1 -1
- package/dist/team/__tests__/api-interop.test.js +91 -1
- package/dist/team/__tests__/api-interop.test.js.map +1 -1
- package/dist/team/__tests__/approved-execution.test.js.map +1 -1
- package/dist/team/__tests__/delivery-e2e-smoke.test.js +52 -10
- package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
- package/dist/team/__tests__/idle-nudge.test.js +45 -15
- package/dist/team/__tests__/idle-nudge.test.js.map +1 -1
- package/dist/team/__tests__/mcp-comm.test.js +93 -27
- package/dist/team/__tests__/mcp-comm.test.js.map +1 -1
- package/dist/team/__tests__/model-contract.test.js +341 -4
- package/dist/team/__tests__/model-contract.test.js.map +1 -1
- package/dist/team/__tests__/role-router.test.js.map +1 -1
- package/dist/team/__tests__/runtime-cli.test.js +9 -0
- package/dist/team/__tests__/runtime-cli.test.js.map +1 -1
- package/dist/team/__tests__/runtime.test.js +3624 -355
- package/dist/team/__tests__/runtime.test.js.map +1 -1
- package/dist/team/__tests__/scaling.test.js +2516 -118
- package/dist/team/__tests__/scaling.test.js.map +1 -1
- package/dist/team/__tests__/state-root.test.js.map +1 -1
- package/dist/team/__tests__/state.test.js +804 -36
- package/dist/team/__tests__/state.test.js.map +1 -1
- package/dist/team/__tests__/team-ops-contract.test.js +6 -0
- package/dist/team/__tests__/team-ops-contract.test.js.map +1 -1
- package/dist/team/__tests__/tmux-session.test.js +3664 -969
- package/dist/team/__tests__/tmux-session.test.js.map +1 -1
- package/dist/team/__tests__/tmux-test-fixture.js.map +1 -1
- package/dist/team/__tests__/worker-runtime-identity.test.js +38 -2
- package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
- package/dist/team/__tests__/worktree.test.js +6 -0
- package/dist/team/__tests__/worktree.test.js.map +1 -1
- package/dist/team/api-interop.d.ts +2 -2
- package/dist/team/api-interop.d.ts.map +1 -1
- package/dist/team/api-interop.js +15 -9
- package/dist/team/api-interop.js.map +1 -1
- package/dist/team/commit-hygiene.js.map +1 -1
- package/dist/team/contracts.d.ts +5 -5
- package/dist/team/contracts.d.ts.map +1 -1
- package/dist/team/delivery-log.js.map +1 -1
- package/dist/team/exact-pane.d.ts +23 -0
- package/dist/team/exact-pane.d.ts.map +1 -0
- package/dist/team/exact-pane.js +133 -0
- package/dist/team/exact-pane.js.map +1 -0
- package/dist/team/followup-planner.js.map +1 -1
- package/dist/team/goal-workflow.js.map +1 -1
- package/dist/team/idle-nudge.d.ts +12 -1
- package/dist/team/idle-nudge.d.ts.map +1 -1
- package/dist/team/idle-nudge.js +41 -14
- package/dist/team/idle-nudge.js.map +1 -1
- package/dist/team/model-contract.d.ts +18 -2
- package/dist/team/model-contract.d.ts.map +1 -1
- package/dist/team/model-contract.js +314 -35
- package/dist/team/model-contract.js.map +1 -1
- package/dist/team/orchestrator.js.map +1 -1
- package/dist/team/pane-status.js.map +1 -1
- package/dist/team/phase-controller.js.map +1 -1
- package/dist/team/reminder-intents.d.ts +1 -1
- package/dist/team/reminder-intents.d.ts.map +1 -1
- package/dist/team/reminder-intents.js.map +1 -1
- package/dist/team/runtime-cli.js.map +1 -1
- package/dist/team/runtime.d.ts +1 -0
- package/dist/team/runtime.d.ts.map +1 -1
- package/dist/team/runtime.js +1326 -231
- package/dist/team/runtime.js.map +1 -1
- package/dist/team/scaling.d.ts +22 -8
- package/dist/team/scaling.d.ts.map +1 -1
- package/dist/team/scaling.js +1463 -376
- package/dist/team/scaling.js.map +1 -1
- package/dist/team/state/dispatch-lock.js.map +1 -1
- package/dist/team/state/dispatch.d.ts.map +1 -1
- package/dist/team/state/dispatch.js +26 -8
- package/dist/team/state/dispatch.js.map +1 -1
- package/dist/team/state/events.js.map +1 -1
- package/dist/team/state/locks.d.ts.map +1 -1
- package/dist/team/state/locks.js +61 -13
- package/dist/team/state/locks.js.map +1 -1
- package/dist/team/state/tasks.d.ts.map +1 -1
- package/dist/team/state/tasks.js +4 -3
- package/dist/team/state/tasks.js.map +1 -1
- package/dist/team/state-root.js.map +1 -1
- package/dist/team/state.d.ts +67 -1
- package/dist/team/state.d.ts.map +1 -1
- package/dist/team/state.js +636 -129
- package/dist/team/state.js.map +1 -1
- package/dist/team/team-identity.d.ts.map +1 -1
- package/dist/team/team-identity.js.map +1 -1
- package/dist/team/team-ops.d.ts +5 -0
- package/dist/team/team-ops.d.ts.map +1 -1
- package/dist/team/team-ops.js +4 -0
- package/dist/team/team-ops.js.map +1 -1
- package/dist/team/tmux-session.d.ts +125 -23
- package/dist/team/tmux-session.d.ts.map +1 -1
- package/dist/team/tmux-session.js +1340 -333
- package/dist/team/tmux-session.js.map +1 -1
- package/dist/team/worker-bootstrap.js.map +1 -1
- package/dist/team/worktree.d.ts.map +1 -1
- package/dist/team/worktree.js +4 -0
- package/dist/team/worktree.js.map +1 -1
- package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
- package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
- package/dist/ultragoal/artifacts.d.ts.map +1 -1
- package/dist/ultragoal/artifacts.js +4 -1
- package/dist/ultragoal/artifacts.js.map +1 -1
- package/dist/url-reader/index.js.map +1 -1
- package/dist/utils/__tests__/file-durability.test.d.ts +2 -0
- package/dist/utils/__tests__/file-durability.test.d.ts.map +1 -0
- package/dist/utils/__tests__/file-durability.test.js +57 -0
- package/dist/utils/__tests__/file-durability.test.js.map +1 -0
- package/dist/utils/agents-model-table.js.map +1 -1
- package/dist/utils/file-durability.d.ts +15 -0
- package/dist/utils/file-durability.d.ts.map +1 -0
- package/dist/utils/file-durability.js +37 -0
- package/dist/utils/file-durability.js.map +1 -0
- package/dist/utils/package.js.map +1 -1
- package/dist/utils/paths.js.map +1 -1
- package/dist/utils/platform-command.js.map +1 -1
- package/dist/utils/repo-deps.js.map +1 -1
- package/dist/utils/version.js.map +1 -1
- package/dist/verification/__tests__/ci-rust-gates.test.js +1 -1
- package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js +3 -0
- package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js.map +1 -1
- package/dist/verification/verifier.js.map +1 -1
- package/dist/visual/constants.d.ts +1 -1
- package/dist/visual/constants.d.ts.map +1 -1
- package/dist/vscode/__tests__/index.test.js.map +1 -1
- package/dist/vscode/index.js.map +1 -1
- package/dist/wiki/__tests__/cjk-tokenize.test.js.map +1 -1
- package/dist/wiki/__tests__/lint.test.js.map +1 -1
- package/dist/wiki/__tests__/query.test.js.map +1 -1
- package/dist/wiki/__tests__/storage.test.js.map +1 -1
- package/dist/wiki/__tests__/test-helpers.d.ts.map +1 -1
- package/dist/wiki/lint.d.ts +4 -1
- package/dist/wiki/lint.d.ts.map +1 -1
- package/dist/wiki/lint.js +3 -2
- package/dist/wiki/lint.js.map +1 -1
- package/dist/wiki/query.js.map +1 -1
- package/dist/wiki/storage.js.map +1 -1
- package/package.json +2 -2
- package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
- package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +24 -0
- package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +10 -5
- package/plugins/oh-my-codex/skills/ralph/SKILL.md +6 -2
- package/plugins/oh-my-codex/skills/ralplan/SKILL.md +6 -2
- package/plugins/oh-my-codex/skills/team/SKILL.md +13 -9
- package/skills/omx-setup/SKILL.md +10 -5
- package/skills/ralph/SKILL.md +6 -2
- package/skills/ralplan/SKILL.md +6 -2
- package/skills/team/SKILL.md +13 -9
- package/src/scripts/__tests__/codex-native-hook.test.ts +20048 -10827
- package/src/scripts/__tests__/fixtures/issue-3138-installed-smoke.ts +734 -0
- package/src/scripts/__tests__/notify-fallback-delivery.test.ts +177 -0
- package/src/scripts/__tests__/notify-state-io.test.ts +73 -0
- package/src/scripts/__tests__/reasoning-artifact-contract.test.ts +129 -0
- package/src/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.ts +78 -0
- package/src/scripts/__tests__/role-intent-durable-recovery-3181.test.ts +60 -0
- package/src/scripts/__tests__/smoke-packed-install.test.ts +1309 -4
- package/src/scripts/codex-native-hook.ts +11429 -1163
- package/src/scripts/notify-fallback-delivery.ts +416 -0
- package/src/scripts/notify-fallback-watcher.ts +216 -155
- package/src/scripts/notify-hook/auto-nudge.ts +113 -14
- package/src/scripts/notify-hook/managed-tmux.ts +149 -12
- package/src/scripts/notify-hook/ralph-session-resume.ts +37 -36
- package/src/scripts/notify-hook/state-io.ts +100 -13
- package/src/scripts/notify-hook/team-dispatch.ts +242 -75
- package/src/scripts/notify-hook/team-leader-nudge.ts +40 -18
- package/src/scripts/notify-hook/team-tmux-guard.ts +366 -39
- package/src/scripts/notify-hook/team-worker-stop.ts +20 -16
- package/src/scripts/notify-hook/team-worker.ts +39 -26
- package/src/scripts/notify-hook/tmux-injection.ts +66 -23
- package/src/scripts/notify-hook/visual-verdict.ts +22 -12
- package/src/scripts/notify-hook.ts +336 -90
- package/src/scripts/smoke-packed-install.ts +5154 -149
package/dist/hooks/session.js
CHANGED
|
@@ -1,202 +1,251 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Session
|
|
2
|
+
* Session lifecycle management.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
* and
|
|
4
|
+
* The selected state root owns one canonical session pointer. Pointer writes are
|
|
5
|
+
* serialized by an adjacent lock directory and become visible only after an
|
|
6
|
+
* atomic rename of a transaction-owned temporary file.
|
|
6
7
|
*/
|
|
7
|
-
import {
|
|
8
|
+
import { appendFile, lstat as nodeLstat, mkdir as nodeMkdir, open, readFile as nodeReadFile, readdir as nodeReaddir, rename as nodeRename, rmdir as nodeRmdir, rm, unlink as nodeUnlink, writeFile as nodeWriteFile, } from 'fs/promises';
|
|
9
|
+
import { readFileSync } from 'fs';
|
|
10
|
+
import { createHash, randomUUID } from 'crypto';
|
|
8
11
|
import { dirname, join } from 'path';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
import {
|
|
12
|
+
import { omxRoot, omxLogsDir, sameFilePath } from '../utils/paths.js';
|
|
13
|
+
import { getBaseStateDirWithSource, resolveWorkingDirectoryForState, } from '../mcp/state-paths.js';
|
|
14
|
+
import { emitDegradedDurabilityWarning, recordRegularFileSyncOutcome, syncRegularFile, } from '../utils/file-durability.js';
|
|
12
15
|
const SESSION_FILE = 'session.json';
|
|
13
16
|
const HISTORY_FILE = 'session-history.jsonl';
|
|
14
17
|
const SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
function historyPath(cwd) {
|
|
21
|
-
return join(omxLogsDir(cwd), HISTORY_FILE);
|
|
22
|
-
}
|
|
23
|
-
async function removeDeadSessionHudState(cwd, sessionIds) {
|
|
24
|
-
const uniqueSessionIds = [...new Set(sessionIds
|
|
25
|
-
.filter((value) => typeof value === 'string' && value.trim().length > 0)
|
|
26
|
-
.map((value) => value.trim()))];
|
|
27
|
-
const candidatePaths = [
|
|
28
|
-
getStateFilePath('hud-state.json', cwd),
|
|
29
|
-
...uniqueSessionIds.map((sessionId) => getStateFilePath('hud-state.json', cwd, sessionId)),
|
|
30
|
-
];
|
|
31
|
-
await Promise.all(candidatePaths.map(async (path) => {
|
|
32
|
-
try {
|
|
33
|
-
await rm(path, { force: true });
|
|
34
|
-
}
|
|
35
|
-
catch {
|
|
36
|
-
// best effort only
|
|
37
|
-
}
|
|
38
|
-
}));
|
|
39
|
-
}
|
|
18
|
+
const SESSION_POINTER_TOKEN_PATTERN = /^[A-Za-z0-9_-]{16,128}$/;
|
|
19
|
+
const SHA256_PATTERN = /^[a-f0-9]{64}$/;
|
|
20
|
+
const LOCK_RETRY_DELAYS_MS = [25, 50, 100];
|
|
21
|
+
const DEFAULT_POINTER_TIMEOUT_MS = 5_000;
|
|
22
|
+
const NATIVE_POINTER_TIMEOUT_MS = 2_000;
|
|
40
23
|
/**
|
|
41
|
-
*
|
|
42
|
-
*
|
|
24
|
+
* Convert arbitrary input into a valid session ID without exposing validator
|
|
25
|
+
* exceptions to lifecycle or hook inputs.
|
|
43
26
|
*/
|
|
44
|
-
export
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
const now = new Date().toISOString();
|
|
50
|
-
await writeFile(join(omxDir, 'metrics.json'), JSON.stringify({
|
|
51
|
-
total_turns: 0,
|
|
52
|
-
session_turns: 0,
|
|
53
|
-
last_activity: now,
|
|
54
|
-
session_input_tokens: 0,
|
|
55
|
-
session_output_tokens: 0,
|
|
56
|
-
session_total_tokens: 0,
|
|
57
|
-
five_hour_limit_pct: 0,
|
|
58
|
-
weekly_limit_pct: 0,
|
|
59
|
-
}, null, 2));
|
|
60
|
-
const hudStatePath = getStateFilePath('hud-state.json', cwd, sessionId);
|
|
61
|
-
await mkdir(dirname(hudStatePath), { recursive: true });
|
|
62
|
-
await writeFile(hudStatePath, JSON.stringify({
|
|
63
|
-
last_turn_at: now,
|
|
64
|
-
last_progress_at: now,
|
|
65
|
-
turn_count: 0,
|
|
66
|
-
last_agent_output: '',
|
|
67
|
-
}, null, 2));
|
|
27
|
+
export function normalizeSessionId(value) {
|
|
28
|
+
if (typeof value !== 'string')
|
|
29
|
+
return undefined;
|
|
30
|
+
const trimmed = value.trim();
|
|
31
|
+
return SESSION_ID_PATTERN.test(trimmed) ? trimmed : undefined;
|
|
68
32
|
}
|
|
69
|
-
/**
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
33
|
+
/** 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);
|
|
37
|
+
const pointerPath = join(baseStateDir, SESSION_FILE);
|
|
38
|
+
return {
|
|
39
|
+
cwd: normalizedCwd,
|
|
40
|
+
baseStateDir,
|
|
41
|
+
rootSource,
|
|
42
|
+
sessionPath: pointerPath,
|
|
43
|
+
lockPath: `${pointerPath}.lock`,
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
function attemptedStateRootSource() {
|
|
76
47
|
try {
|
|
77
|
-
|
|
78
|
-
|
|
48
|
+
if (process.env.OMX_TEAM_STATE_ROOT?.trim())
|
|
49
|
+
return 'team-env';
|
|
50
|
+
if (process.env.OMX_ROOT?.trim())
|
|
51
|
+
return 'omx-root-env';
|
|
52
|
+
if (process.env.OMX_STATE_ROOT?.trim())
|
|
53
|
+
return 'omx-state-root-env';
|
|
54
|
+
return 'cwd-default';
|
|
79
55
|
}
|
|
80
56
|
catch {
|
|
81
|
-
return
|
|
57
|
+
return 'unresolved';
|
|
82
58
|
}
|
|
83
59
|
}
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
60
|
+
function contextAbort(cwd, candidateSessionId, cause) {
|
|
61
|
+
return new SessionPointerLaunchAbortError({
|
|
62
|
+
code: 'session_pointer_context_failure',
|
|
63
|
+
operation: 'pointer-context-resolve',
|
|
64
|
+
cwd,
|
|
65
|
+
...(candidateSessionId ? { candidateSessionId } : {}),
|
|
66
|
+
attemptedRootSource: attemptedStateRootSource(),
|
|
67
|
+
reason: `Unable to resolve the selected session pointer root: ${errorMessage(cause)}`,
|
|
68
|
+
cause,
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
function resolvedAbort(context, input) {
|
|
72
|
+
return new SessionPointerLaunchAbortError({
|
|
73
|
+
...input,
|
|
74
|
+
cwd: context.cwd,
|
|
75
|
+
pointerPath: context.sessionPath,
|
|
76
|
+
rootSource: context.rootSource,
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
class SessionPointerLaunchAbortError extends Error {
|
|
80
|
+
name = 'SessionPointerLaunchAbort';
|
|
81
|
+
committed = false;
|
|
82
|
+
constructor(fields) {
|
|
83
|
+
super(fields.reason);
|
|
84
|
+
Object.assign(this, fields);
|
|
89
85
|
}
|
|
90
|
-
return true;
|
|
91
86
|
}
|
|
92
|
-
export function
|
|
93
|
-
if (!
|
|
87
|
+
export function isSessionPointerLaunchAbort(error) {
|
|
88
|
+
if (!(error instanceof Error) || error.name !== 'SessionPointerLaunchAbort')
|
|
89
|
+
return false;
|
|
90
|
+
const candidate = error;
|
|
91
|
+
if (candidate.committed !== false || typeof candidate.cwd !== 'string' || typeof candidate.operation !== 'string') {
|
|
94
92
|
return false;
|
|
95
|
-
const hasPidMetadata = Number.isInteger(state.pid) && state.pid > 0;
|
|
96
|
-
const hasLinuxIdentityMetadata = typeof state.pid_start_ticks === 'number'
|
|
97
|
-
|| typeof state.pid_cmdline === 'string';
|
|
98
|
-
if (hasPidMetadata || hasLinuxIdentityMetadata) {
|
|
99
|
-
return !isSessionStale(state, options);
|
|
100
93
|
}
|
|
101
|
-
return
|
|
94
|
+
return candidate.code === 'session_pointer_context_failure'
|
|
95
|
+
|| candidate.code === 'session_pointer_lock_timeout'
|
|
96
|
+
|| candidate.code === 'session_pointer_lock_recovery_required'
|
|
97
|
+
|| candidate.code === 'session_pointer_unusable'
|
|
98
|
+
|| candidate.code === 'session_pointer_owner_conflict'
|
|
99
|
+
|| candidate.code === 'session_pointer_io_failure';
|
|
102
100
|
}
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (!state)
|
|
106
|
-
return null;
|
|
107
|
-
return isSessionStateUsable(state, cwd, options) ? state : null;
|
|
101
|
+
function errorMessage(error) {
|
|
102
|
+
return error instanceof Error ? error.message : String(error);
|
|
108
103
|
}
|
|
109
|
-
function
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
104
|
+
function errorCode(error) {
|
|
105
|
+
if (!error || typeof error !== 'object')
|
|
106
|
+
return undefined;
|
|
107
|
+
const code = error.code;
|
|
108
|
+
return typeof code === 'string' ? code : undefined;
|
|
109
|
+
}
|
|
110
|
+
function isNotFound(error) {
|
|
111
|
+
return errorCode(error) === 'ENOENT';
|
|
112
|
+
}
|
|
113
|
+
function isAlreadyExists(error) {
|
|
114
|
+
return errorCode(error) === 'EEXIST';
|
|
115
|
+
}
|
|
116
|
+
function hashCmdline(cmdline) {
|
|
117
|
+
const normalized = normalizeCmdline(cmdline);
|
|
118
|
+
return normalized ? createHash('sha256').update(normalized).digest('hex') : undefined;
|
|
119
|
+
}
|
|
120
|
+
const defaultFsDependencies = {
|
|
121
|
+
mkdir: async (path, options) => {
|
|
122
|
+
await nodeMkdir(path, options);
|
|
123
|
+
},
|
|
124
|
+
readdir: async (path) => await nodeReaddir(path),
|
|
125
|
+
readFile: async (path, encoding) => await nodeReadFile(path, encoding),
|
|
126
|
+
lstat: async (path) => await nodeLstat(path),
|
|
127
|
+
writeFile: async (path, data, options) => {
|
|
128
|
+
await nodeWriteFile(path, data, options);
|
|
129
|
+
},
|
|
130
|
+
openAndSync: async (path, platform, regularFileSync) => {
|
|
131
|
+
const handle = await open(path, 'r');
|
|
132
|
+
try {
|
|
133
|
+
return await syncRegularFile(regularFileSync ? { sync: () => regularFileSync(platform) } : handle, platform);
|
|
134
|
+
}
|
|
135
|
+
finally {
|
|
136
|
+
await handle.close();
|
|
137
|
+
}
|
|
138
|
+
},
|
|
139
|
+
rename: async (from, to) => {
|
|
140
|
+
await nodeRename(from, to);
|
|
141
|
+
},
|
|
142
|
+
unlink: async (path) => {
|
|
143
|
+
await nodeUnlink(path);
|
|
144
|
+
},
|
|
145
|
+
rmdir: async (path) => {
|
|
146
|
+
await nodeRmdir(path);
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
/** @internal Exposed only so source-module tests can verify default ESRCH handling. */
|
|
150
|
+
export function __createDefaultPidProbeForTests(killZero) {
|
|
151
|
+
return (pid) => {
|
|
152
|
+
try {
|
|
153
|
+
killZero(pid);
|
|
154
|
+
return 'alive';
|
|
155
|
+
}
|
|
156
|
+
catch (error) {
|
|
157
|
+
return errorCode(error) === 'ESRCH' ? 'dead' : 'indeterminate';
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function defaultProbePid(pid) {
|
|
162
|
+
return __createDefaultPidProbeForTests((targetPid) => {
|
|
163
|
+
process.kill(targetPid, 0);
|
|
164
|
+
})(pid);
|
|
165
|
+
}
|
|
166
|
+
function defaultReadProcessIdentity(pid, platform) {
|
|
167
|
+
if (platform !== 'linux')
|
|
168
|
+
return { status: 'indeterminate' };
|
|
169
|
+
const identity = readLinuxProcessIdentity(pid);
|
|
170
|
+
if (!identity)
|
|
171
|
+
return { status: 'indeterminate' };
|
|
172
|
+
const cmdlineHash = hashCmdline(identity.cmdline);
|
|
173
|
+
return {
|
|
174
|
+
status: 'matching',
|
|
175
|
+
startTicks: identity.startTicks,
|
|
176
|
+
...(cmdlineHash ? { cmdlineHash } : {}),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
const defaultTransactionDependencies = {
|
|
180
|
+
fs: defaultFsDependencies,
|
|
181
|
+
nowMs: () => Date.now(),
|
|
182
|
+
sleep: async (ms) => await new Promise((resolve) => setTimeout(resolve, ms)),
|
|
183
|
+
token: () => randomUUID(),
|
|
184
|
+
probePid: defaultProbePid,
|
|
185
|
+
readProcessIdentity: defaultReadProcessIdentity,
|
|
186
|
+
};
|
|
187
|
+
let transactionDependencies = defaultTransactionDependencies;
|
|
188
|
+
/** @internal Source-module test harness. Always reset after a test. */
|
|
189
|
+
export function __setSessionPointerTransactionDependenciesForTests(overrides) {
|
|
190
|
+
transactionDependencies = {
|
|
191
|
+
...defaultTransactionDependencies,
|
|
192
|
+
...overrides,
|
|
193
|
+
fs: {
|
|
194
|
+
...defaultFsDependencies,
|
|
195
|
+
...overrides.fs,
|
|
196
|
+
},
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
/** @internal Source-module test harness. */
|
|
200
|
+
export function __resetSessionPointerTransactionDependenciesForTests() {
|
|
201
|
+
transactionDependencies = defaultTransactionDependencies;
|
|
117
202
|
}
|
|
118
203
|
function parseLinuxProcStartTicks(statContent) {
|
|
119
204
|
const commandEnd = statContent.lastIndexOf(')');
|
|
120
205
|
if (commandEnd === -1)
|
|
121
206
|
return null;
|
|
122
|
-
const
|
|
123
|
-
const fields = remainder.split(/\s+/);
|
|
207
|
+
const fields = statContent.slice(commandEnd + 1).trim().split(/\s+/);
|
|
124
208
|
if (fields.length <= 19)
|
|
125
209
|
return null;
|
|
126
210
|
const startTicks = Number(fields[19]);
|
|
127
|
-
return Number.
|
|
211
|
+
return Number.isInteger(startTicks) && startTicks >= 0 ? startTicks : null;
|
|
128
212
|
}
|
|
129
213
|
function normalizeCmdline(cmdline) {
|
|
130
214
|
if (!cmdline)
|
|
131
215
|
return null;
|
|
132
216
|
const normalized = cmdline.replace(/\s+/g, ' ').trim();
|
|
133
|
-
return normalized
|
|
217
|
+
return normalized || null;
|
|
134
218
|
}
|
|
135
219
|
function readLinuxProcessIdentity(pid) {
|
|
136
220
|
try {
|
|
137
|
-
const
|
|
138
|
-
const startTicks = parseLinuxProcStartTicks(stat);
|
|
221
|
+
const startTicks = parseLinuxProcStartTicks(readFileSync(`/proc/${pid}/stat`, 'utf-8'));
|
|
139
222
|
if (startTicks == null)
|
|
140
223
|
return null;
|
|
141
224
|
let cmdline = null;
|
|
142
225
|
try {
|
|
143
|
-
cmdline = readFileSync(`/proc/${pid}/cmdline`, 'utf-8')
|
|
144
|
-
.replace(/\u0000+/g, ' ')
|
|
145
|
-
.trim();
|
|
226
|
+
cmdline = readFileSync(`/proc/${pid}/cmdline`, 'utf-8').replace(/\u0000+/g, ' ').trim();
|
|
146
227
|
}
|
|
147
228
|
catch {
|
|
148
|
-
cmdline
|
|
229
|
+
// The start tick is still useful when cmdline access is unavailable.
|
|
149
230
|
}
|
|
150
|
-
return {
|
|
151
|
-
startTicks,
|
|
152
|
-
cmdline: normalizeCmdline(cmdline),
|
|
153
|
-
};
|
|
231
|
+
return { startTicks, cmdline: normalizeCmdline(cmdline) };
|
|
154
232
|
}
|
|
155
233
|
catch {
|
|
156
234
|
return null;
|
|
157
235
|
}
|
|
158
236
|
}
|
|
159
|
-
function
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
const tmuxPaneId = typeof options.tmuxPaneId === 'string' && options.tmuxPaneId.trim()
|
|
168
|
-
? options.tmuxPaneId.trim()
|
|
169
|
-
: undefined;
|
|
170
|
-
const previousNativeSessionId = typeof options.previousNativeSessionId === 'string' && options.previousNativeSessionId.trim()
|
|
171
|
-
? options.previousNativeSessionId.trim()
|
|
172
|
-
: undefined;
|
|
173
|
-
const nativeSessionSwitchedAt = typeof options.nativeSessionSwitchedAt === 'string' && options.nativeSessionSwitchedAt.trim()
|
|
174
|
-
? options.nativeSessionSwitchedAt.trim()
|
|
175
|
-
: undefined;
|
|
176
|
-
const ownerOmxSessionId = typeof options.ownerOmxSessionId === 'string' && options.ownerOmxSessionId.trim()
|
|
177
|
-
? options.ownerOmxSessionId.trim()
|
|
178
|
-
: undefined;
|
|
179
|
-
return {
|
|
180
|
-
session_id: sessionId,
|
|
181
|
-
...(nativeSessionId ? { native_session_id: nativeSessionId } : {}),
|
|
182
|
-
...(previousNativeSessionId ? { previous_native_session_id: previousNativeSessionId } : {}),
|
|
183
|
-
...(nativeSessionSwitchedAt ? { native_session_switched_at: nativeSessionSwitchedAt } : {}),
|
|
184
|
-
...(ownerOmxSessionId ? { owner_omx_session_id: ownerOmxSessionId } : {}),
|
|
185
|
-
started_at: options.startedAt ?? nowIso,
|
|
186
|
-
cwd,
|
|
187
|
-
pid,
|
|
188
|
-
platform,
|
|
189
|
-
pid_start_ticks: linuxIdentity?.startTicks,
|
|
190
|
-
pid_cmdline: linuxIdentity?.cmdline ?? undefined,
|
|
191
|
-
...(tmuxSessionName ? { tmux_session_name: tmuxSessionName } : {}),
|
|
192
|
-
...(tmuxPaneId ? { tmux_pane_id: tmuxPaneId } : {}),
|
|
193
|
-
};
|
|
237
|
+
function defaultIsPidAlive(pid) {
|
|
238
|
+
try {
|
|
239
|
+
process.kill(pid, 0);
|
|
240
|
+
return true;
|
|
241
|
+
}
|
|
242
|
+
catch {
|
|
243
|
+
return false;
|
|
244
|
+
}
|
|
194
245
|
}
|
|
195
246
|
/**
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
* - On Linux, require process identity validation (start ticks, optional cmdline).
|
|
199
|
-
* If identity cannot be validated, treat the session as stale.
|
|
247
|
+
* Legacy boolean stale check retained for read compatibility. The pointer
|
|
248
|
+
* classifier below keeps indeterminate liveness distinct from definitely dead.
|
|
200
249
|
*/
|
|
201
250
|
export function isSessionStale(state, options = {}) {
|
|
202
251
|
if (!Number.isInteger(state.pid) || state.pid <= 0)
|
|
@@ -207,206 +256,1235 @@ export function isSessionStale(state, options = {}) {
|
|
|
207
256
|
const platform = options.platform ?? process.platform;
|
|
208
257
|
if (platform !== 'linux')
|
|
209
258
|
return false;
|
|
210
|
-
const
|
|
211
|
-
|
|
212
|
-
if (!liveIdentity)
|
|
213
|
-
return true;
|
|
214
|
-
if (typeof state.pid_start_ticks !== 'number')
|
|
259
|
+
const liveIdentity = (options.readLinuxIdentity ?? readLinuxProcessIdentity)(state.pid);
|
|
260
|
+
if (!liveIdentity || typeof state.pid_start_ticks !== 'number')
|
|
215
261
|
return true;
|
|
216
262
|
if (state.pid_start_ticks !== liveIdentity.startTicks)
|
|
217
263
|
return true;
|
|
218
264
|
const expectedCmdline = normalizeCmdline(state.pid_cmdline);
|
|
219
|
-
if (expectedCmdline)
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
265
|
+
if (!expectedCmdline)
|
|
266
|
+
return false;
|
|
267
|
+
const liveCmdline = normalizeCmdline(liveIdentity.cmdline);
|
|
268
|
+
return !liveCmdline || liveCmdline !== expectedCmdline;
|
|
269
|
+
}
|
|
270
|
+
export function isSessionStateAuthoritativeForCwd(state, cwd) {
|
|
271
|
+
if (!normalizeSessionId(state.session_id))
|
|
272
|
+
return false;
|
|
273
|
+
if (typeof state.cwd !== 'string' || !state.cwd.trim())
|
|
274
|
+
return false;
|
|
275
|
+
try {
|
|
276
|
+
return sameFilePath(state.cwd, cwd);
|
|
277
|
+
}
|
|
278
|
+
catch {
|
|
279
|
+
return false;
|
|
223
280
|
}
|
|
224
|
-
return false;
|
|
225
281
|
}
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
282
|
+
export function isSessionStateUsable(state, cwd, options = {}) {
|
|
283
|
+
if (!normalizeSessionId(state.session_id))
|
|
284
|
+
return false;
|
|
285
|
+
if (typeof state.cwd === 'string' && state.cwd.trim() && !isSessionStateAuthoritativeForCwd(state, cwd))
|
|
286
|
+
return false;
|
|
287
|
+
const hasPidMetadata = Number.isInteger(state.pid) && state.pid > 0;
|
|
288
|
+
const hasLinuxIdentityMetadata = typeof state.pid_start_ticks === 'number'
|
|
289
|
+
|| typeof state.pid_cmdline === 'string';
|
|
290
|
+
return !hasPidMetadata && !hasLinuxIdentityMetadata || !isSessionStale(state, options);
|
|
291
|
+
}
|
|
292
|
+
function isValidStartTicks(value) {
|
|
293
|
+
return typeof value === 'number' && Number.isInteger(value) && value >= 0;
|
|
294
|
+
}
|
|
295
|
+
function classifySessionProcess(state, dependencies) {
|
|
296
|
+
const hasPidMetadata = Number.isInteger(state.pid) && state.pid > 0;
|
|
297
|
+
const hasIdentityMetadata = typeof state.pid_start_ticks === 'number' || typeof state.pid_cmdline === 'string';
|
|
298
|
+
if (!hasPidMetadata && !hasIdentityMetadata)
|
|
299
|
+
return 'usable';
|
|
300
|
+
if (!hasPidMetadata)
|
|
301
|
+
return 'identity-indeterminate';
|
|
302
|
+
let pidStatus;
|
|
303
|
+
try {
|
|
304
|
+
pidStatus = dependencies.probePid(state.pid);
|
|
305
|
+
}
|
|
306
|
+
catch {
|
|
307
|
+
return 'identity-indeterminate';
|
|
308
|
+
}
|
|
309
|
+
if (pidStatus === 'dead')
|
|
310
|
+
return 'stale-dead';
|
|
311
|
+
if (pidStatus !== 'alive')
|
|
312
|
+
return 'identity-indeterminate';
|
|
313
|
+
const platform = state.platform ?? process.platform;
|
|
314
|
+
if (platform !== 'linux')
|
|
315
|
+
return 'usable';
|
|
316
|
+
if (!isValidStartTicks(state.pid_start_ticks))
|
|
317
|
+
return 'identity-indeterminate';
|
|
318
|
+
let liveIdentity;
|
|
319
|
+
try {
|
|
320
|
+
liveIdentity = dependencies.readProcessIdentity(state.pid, platform);
|
|
321
|
+
}
|
|
322
|
+
catch {
|
|
323
|
+
return 'identity-indeterminate';
|
|
324
|
+
}
|
|
325
|
+
if (!liveIdentity || !isValidStartTicks(liveIdentity.startTicks))
|
|
326
|
+
return 'identity-indeterminate';
|
|
327
|
+
if (liveIdentity.startTicks !== state.pid_start_ticks)
|
|
328
|
+
return 'stale-dead';
|
|
329
|
+
if (liveIdentity.status !== 'matching')
|
|
330
|
+
return 'identity-indeterminate';
|
|
331
|
+
const expectedCmdlineHash = hashCmdline(state.pid_cmdline);
|
|
332
|
+
if (expectedCmdlineHash && liveIdentity.cmdlineHash !== expectedCmdlineHash) {
|
|
333
|
+
return 'identity-indeterminate';
|
|
334
|
+
}
|
|
335
|
+
return 'usable';
|
|
336
|
+
}
|
|
337
|
+
function classifyParsedSessionPointer(context, value, raw) {
|
|
338
|
+
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
|
339
|
+
return { status: 'malformed', raw };
|
|
340
|
+
}
|
|
341
|
+
const state = value;
|
|
342
|
+
if (!normalizeSessionId(state.session_id))
|
|
343
|
+
return { status: 'malformed', raw };
|
|
344
|
+
if (typeof state.cwd === 'string' && state.cwd.trim() && !isSessionStateAuthoritativeForCwd(state, context.cwd)) {
|
|
345
|
+
return { status: 'foreign-cwd', raw, state };
|
|
346
|
+
}
|
|
347
|
+
const processStatus = classifySessionProcess(state, transactionDependencies);
|
|
348
|
+
return { status: processStatus, state, raw };
|
|
349
|
+
}
|
|
350
|
+
/** Read and classify only context.sessionPath; no alternate root is consulted. */
|
|
351
|
+
export async function readSessionPointer(context) {
|
|
352
|
+
let raw;
|
|
353
|
+
try {
|
|
354
|
+
raw = await transactionDependencies.fs.readFile(context.sessionPath, 'utf8');
|
|
355
|
+
}
|
|
356
|
+
catch (error) {
|
|
357
|
+
if (isNotFound(error))
|
|
358
|
+
return { status: 'absent' };
|
|
359
|
+
throw error;
|
|
360
|
+
}
|
|
361
|
+
try {
|
|
362
|
+
return classifyParsedSessionPointer(context, JSON.parse(raw), raw);
|
|
363
|
+
}
|
|
364
|
+
catch (error) {
|
|
365
|
+
if (error instanceof SyntaxError)
|
|
366
|
+
return { status: 'malformed', raw };
|
|
367
|
+
throw error;
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
export async function readSessionStateFromContext(context) {
|
|
371
|
+
try {
|
|
372
|
+
const raw = await transactionDependencies.fs.readFile(context.sessionPath, 'utf8');
|
|
373
|
+
const parsed = JSON.parse(raw);
|
|
374
|
+
return parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : null;
|
|
375
|
+
}
|
|
376
|
+
catch {
|
|
377
|
+
return null;
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
export async function readUsableSessionStateFromContext(context) {
|
|
381
|
+
const state = await readSessionStateFromContext(context);
|
|
382
|
+
return state && isSessionStateUsable(state, context.cwd) ? state : null;
|
|
383
|
+
}
|
|
384
|
+
/** Read current session state from the exact selected root. */
|
|
385
|
+
export async function readSessionState(cwd) {
|
|
386
|
+
try {
|
|
387
|
+
return await readSessionStateFromContext(resolveSessionPointerContext(cwd));
|
|
388
|
+
}
|
|
389
|
+
catch {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
export async function readUsableSessionState(cwd, options = {}) {
|
|
394
|
+
try {
|
|
395
|
+
const context = resolveSessionPointerContext(cwd);
|
|
396
|
+
const state = await readSessionStateFromContext(context);
|
|
397
|
+
return state && isSessionStateUsable(state, context.cwd, options) ? state : null;
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
function createSessionState(cwd, sessionId, pid, platform, linuxIdentity, options = {}) {
|
|
404
|
+
const nowIso = options.nowIso ?? new Date().toISOString();
|
|
405
|
+
const nativeSessionId = normalizeNonempty(options.nativeSessionId);
|
|
406
|
+
const previousNativeSessionId = normalizeNonempty(options.previousNativeSessionId);
|
|
407
|
+
const nativeSessionSwitchedAt = normalizeNonempty(options.nativeSessionSwitchedAt);
|
|
408
|
+
const ownerOmxSessionId = normalizeSessionId(options.ownerOmxSessionId);
|
|
409
|
+
const tmuxSessionName = normalizeNonempty(options.tmuxSessionName);
|
|
410
|
+
const tmuxPaneId = normalizeNonempty(options.tmuxPaneId);
|
|
411
|
+
return {
|
|
252
412
|
session_id: sessionId,
|
|
253
|
-
...(
|
|
413
|
+
...(nativeSessionId ? { native_session_id: nativeSessionId } : {}),
|
|
414
|
+
...(previousNativeSessionId ? { previous_native_session_id: previousNativeSessionId } : {}),
|
|
415
|
+
...(nativeSessionSwitchedAt ? { native_session_switched_at: nativeSessionSwitchedAt } : {}),
|
|
416
|
+
...(ownerOmxSessionId ? { owner_omx_session_id: ownerOmxSessionId } : {}),
|
|
417
|
+
started_at: options.startedAt ?? nowIso,
|
|
418
|
+
cwd,
|
|
254
419
|
pid,
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
420
|
+
platform,
|
|
421
|
+
...(linuxIdentity ? { pid_start_ticks: linuxIdentity.startTicks } : {}),
|
|
422
|
+
...(linuxIdentity?.cmdline ? { pid_cmdline: linuxIdentity.cmdline } : {}),
|
|
423
|
+
...(tmuxSessionName ? { tmux_session_name: tmuxSessionName } : {}),
|
|
424
|
+
...(tmuxPaneId ? { tmux_pane_id: tmuxPaneId } : {}),
|
|
425
|
+
};
|
|
426
|
+
}
|
|
427
|
+
function normalizeNonempty(value) {
|
|
428
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
429
|
+
}
|
|
430
|
+
function resolvePid(options) {
|
|
431
|
+
return Number.isInteger(options.pid) && options.pid && options.pid > 0 ? options.pid : process.pid;
|
|
432
|
+
}
|
|
433
|
+
function sessionIdentityFor(pid, platform) {
|
|
434
|
+
return platform === 'linux' ? readLinuxProcessIdentity(pid) : null;
|
|
435
|
+
}
|
|
436
|
+
function currentOwnerAlias(state) {
|
|
437
|
+
return normalizeSessionId(state.owner_omx_session_id);
|
|
438
|
+
}
|
|
439
|
+
function verifiedOwnerCandidate(context, options) {
|
|
440
|
+
if (context.rootSource === 'team-env' || options.ownerAliasVerified !== true)
|
|
441
|
+
return undefined;
|
|
442
|
+
return normalizeSessionId(process.env.OMX_SESSION_ID);
|
|
443
|
+
}
|
|
444
|
+
function mergeOwnerAlias(existing, candidate, verified) {
|
|
445
|
+
const current = existing && currentOwnerAlias(existing);
|
|
446
|
+
if (current) {
|
|
447
|
+
if (candidate && candidate !== current) {
|
|
448
|
+
throw new Error(`Session pointer is already bound to owner ${current}`);
|
|
449
|
+
}
|
|
450
|
+
return current;
|
|
451
|
+
}
|
|
452
|
+
if (!candidate || !verified || candidate === existing?.session_id)
|
|
453
|
+
return undefined;
|
|
454
|
+
return candidate;
|
|
455
|
+
}
|
|
456
|
+
function isStartCompatible(existing, requestedSessionId) {
|
|
457
|
+
return existing.session_id === requestedSessionId
|
|
458
|
+
|| existing.native_session_id === requestedSessionId
|
|
459
|
+
|| currentOwnerAlias(existing) === requestedSessionId;
|
|
258
460
|
}
|
|
259
461
|
function getOmxLaunchSessionId(state) {
|
|
260
462
|
if (state.session_id.startsWith('omx-'))
|
|
261
463
|
return state.session_id;
|
|
262
|
-
|
|
263
|
-
|
|
464
|
+
const owner = currentOwnerAlias(state);
|
|
465
|
+
return owner?.startsWith('omx-') ? owner : undefined;
|
|
466
|
+
}
|
|
467
|
+
function parseLockOwner(raw) {
|
|
468
|
+
try {
|
|
469
|
+
const value = JSON.parse(raw);
|
|
470
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
471
|
+
return null;
|
|
472
|
+
if (value.version !== 1 || !isValidToken(value.token) || typeof value.pid !== 'number' || !Number.isInteger(value.pid) || value.pid <= 0)
|
|
473
|
+
return null;
|
|
474
|
+
if (typeof value.platform !== 'string' || !value.platform || typeof value.created_at !== 'string' || !value.created_at) {
|
|
475
|
+
return null;
|
|
476
|
+
}
|
|
477
|
+
if (value.pid_start_ticks !== undefined && !isValidStartTicks(value.pid_start_ticks))
|
|
478
|
+
return null;
|
|
479
|
+
if (value.pid_cmdline_hash !== undefined
|
|
480
|
+
&& (typeof value.pid_cmdline_hash !== 'string' || !SHA256_PATTERN.test(value.pid_cmdline_hash))) {
|
|
481
|
+
return null;
|
|
482
|
+
}
|
|
483
|
+
return value;
|
|
484
|
+
}
|
|
485
|
+
catch {
|
|
486
|
+
return null;
|
|
264
487
|
}
|
|
265
|
-
return undefined;
|
|
266
488
|
}
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
489
|
+
function isValidToken(value) {
|
|
490
|
+
return typeof value === 'string' && SESSION_POINTER_TOKEN_PATTERN.test(value);
|
|
491
|
+
}
|
|
492
|
+
async function inspectLockOwnerFile(ownerPath, missingStatus = 'missing') {
|
|
493
|
+
let raw;
|
|
494
|
+
try {
|
|
495
|
+
raw = await transactionDependencies.fs.readFile(ownerPath, 'utf8');
|
|
496
|
+
}
|
|
497
|
+
catch (error) {
|
|
498
|
+
return isNotFound(error) ? { status: missingStatus } : { status: 'identity-indeterminate' };
|
|
499
|
+
}
|
|
500
|
+
const owner = parseLockOwner(raw);
|
|
501
|
+
if (!owner)
|
|
502
|
+
return { status: 'malformed' };
|
|
503
|
+
let pidStatus;
|
|
504
|
+
try {
|
|
505
|
+
pidStatus = transactionDependencies.probePid(owner.pid);
|
|
506
|
+
}
|
|
507
|
+
catch {
|
|
508
|
+
return { status: 'identity-indeterminate', owner };
|
|
509
|
+
}
|
|
510
|
+
if (pidStatus === 'dead')
|
|
511
|
+
return { status: 'dead', owner };
|
|
512
|
+
if (pidStatus !== 'alive')
|
|
513
|
+
return { status: 'identity-indeterminate', owner };
|
|
514
|
+
if (owner.platform !== 'linux' || !isValidStartTicks(owner.pid_start_ticks)) {
|
|
515
|
+
return { status: 'identity-indeterminate', owner };
|
|
516
|
+
}
|
|
517
|
+
let liveIdentity;
|
|
518
|
+
try {
|
|
519
|
+
liveIdentity = transactionDependencies.readProcessIdentity(owner.pid, owner.platform);
|
|
520
|
+
}
|
|
521
|
+
catch {
|
|
522
|
+
return { status: 'identity-indeterminate', owner };
|
|
523
|
+
}
|
|
524
|
+
if (!liveIdentity || !isValidStartTicks(liveIdentity.startTicks)) {
|
|
525
|
+
return { status: 'identity-indeterminate', owner };
|
|
526
|
+
}
|
|
527
|
+
if (liveIdentity.startTicks !== owner.pid_start_ticks)
|
|
528
|
+
return { status: 'reused', owner };
|
|
529
|
+
if (liveIdentity.status !== 'matching')
|
|
530
|
+
return { status: 'identity-indeterminate', owner };
|
|
531
|
+
if (owner.pid_cmdline_hash && owner.pid_cmdline_hash !== liveIdentity.cmdlineHash) {
|
|
532
|
+
return { status: 'identity-indeterminate', owner };
|
|
533
|
+
}
|
|
534
|
+
return { status: 'live', owner };
|
|
535
|
+
}
|
|
536
|
+
async function inspectLockOwner(lockPath) {
|
|
537
|
+
return await inspectLockOwnerFile(join(lockPath, 'owner.json'));
|
|
538
|
+
}
|
|
539
|
+
async function inspectSessionPointerLockAtContext(context) {
|
|
540
|
+
const absent = () => ({ status: 'absent', lockPath: context.lockPath, evidenceSource: 'none', safeToRecover: false });
|
|
541
|
+
let lockStat;
|
|
542
|
+
try {
|
|
543
|
+
lockStat = await transactionDependencies.fs.lstat(context.lockPath);
|
|
544
|
+
}
|
|
545
|
+
catch (error) {
|
|
546
|
+
return isNotFound(error) ? absent() : { status: 'io-error', lockPath: context.lockPath, evidenceSource: 'none', safeToRecover: false };
|
|
547
|
+
}
|
|
548
|
+
if (lockStat.isSymbolicLink())
|
|
549
|
+
return { status: 'symlink', lockPath: context.lockPath, evidenceSource: 'none', safeToRecover: false };
|
|
550
|
+
if (!lockStat.isDirectory())
|
|
551
|
+
return { status: 'unexpected', lockPath: context.lockPath, evidenceSource: 'none', safeToRecover: false };
|
|
552
|
+
let entries;
|
|
553
|
+
try {
|
|
554
|
+
entries = await transactionDependencies.fs.readdir(context.lockPath);
|
|
555
|
+
}
|
|
556
|
+
catch {
|
|
557
|
+
return { status: 'io-error', lockPath: context.lockPath, evidenceSource: 'none', safeToRecover: false };
|
|
558
|
+
}
|
|
559
|
+
const tempEntries = entries.filter((entry) => /^owner\.([A-Za-z0-9_-]{16,128})\.tmp$/.test(entry));
|
|
560
|
+
const hasCanonical = entries.includes('owner.json');
|
|
561
|
+
if (entries.length === 0)
|
|
562
|
+
return { status: 'missing-owner', lockPath: context.lockPath, evidenceSource: 'none', safeToRecover: false };
|
|
563
|
+
if ((hasCanonical && entries.length !== 1) || (!hasCanonical && tempEntries.length !== 1) || entries.length !== 1) {
|
|
564
|
+
return { status: entries.length > 1 && (hasCanonical || tempEntries.length > 0) ? 'ambiguous' : 'unexpected', lockPath: context.lockPath, evidenceSource: 'none', safeToRecover: false };
|
|
565
|
+
}
|
|
566
|
+
const evidenceName = hasCanonical ? 'owner.json' : tempEntries[0];
|
|
567
|
+
const evidenceSource = hasCanonical ? 'owner.json' : 'owner-temp';
|
|
568
|
+
const evidencePath = join(context.lockPath, evidenceName);
|
|
569
|
+
let evidenceStat;
|
|
570
|
+
try {
|
|
571
|
+
evidenceStat = await transactionDependencies.fs.lstat(evidencePath);
|
|
572
|
+
}
|
|
573
|
+
catch {
|
|
574
|
+
return { status: 'io-error', lockPath: context.lockPath, evidenceSource, safeToRecover: false };
|
|
575
|
+
}
|
|
576
|
+
if (evidenceStat.isSymbolicLink())
|
|
577
|
+
return { status: 'symlink', lockPath: context.lockPath, evidenceSource, safeToRecover: false };
|
|
578
|
+
if (!evidenceStat.isFile())
|
|
579
|
+
return { status: 'unexpected', lockPath: context.lockPath, evidenceSource, safeToRecover: false };
|
|
580
|
+
const owner = await inspectLockOwnerFile(evidencePath, 'missing');
|
|
581
|
+
if (evidenceSource === 'owner-temp' && owner.owner?.token !== /^owner\.([A-Za-z0-9_-]{16,128})\.tmp$/.exec(evidenceName)?.[1]) {
|
|
582
|
+
return { status: 'malformed', lockPath: context.lockPath, evidenceSource, safeToRecover: false };
|
|
583
|
+
}
|
|
584
|
+
const status = owner.status === 'missing' ? 'io-error' : owner.status;
|
|
585
|
+
return {
|
|
586
|
+
status,
|
|
587
|
+
lockPath: context.lockPath,
|
|
588
|
+
evidenceSource,
|
|
589
|
+
safeToRecover: status === 'dead' && evidenceSource === 'owner-temp',
|
|
590
|
+
};
|
|
591
|
+
}
|
|
592
|
+
/** Inspect only exact, regular owner evidence in the selected pointer lock. */
|
|
593
|
+
export async function inspectSessionPointerLock(cwd) {
|
|
594
|
+
return await inspectSessionPointerLockAtContext(resolveSessionPointerContext(cwd));
|
|
595
|
+
}
|
|
596
|
+
/** Explicitly quarantine only a dead, atomically claimed pointer lock; never force recovery. */
|
|
597
|
+
export async function recoverSessionPointerLock(cwd) {
|
|
598
|
+
const context = resolveSessionPointerContext(cwd);
|
|
599
|
+
const inspection = await inspectSessionPointerLockAtContext(context);
|
|
600
|
+
if (!inspection.safeToRecover) {
|
|
601
|
+
return { ...inspection, action: 'none', recovered: false, reason: inspection.status === 'absent' ? 'No session pointer lock exists.' : `Session pointer lock is not safe to recover (${inspection.status}).` };
|
|
602
|
+
}
|
|
603
|
+
let entries;
|
|
604
|
+
try {
|
|
605
|
+
entries = await transactionDependencies.fs.readdir(context.lockPath);
|
|
606
|
+
}
|
|
607
|
+
catch {
|
|
608
|
+
return { ...inspection, action: 'none', recovered: false, reason: 'Unable to re-read session pointer lock evidence.' };
|
|
609
|
+
}
|
|
610
|
+
const evidenceName = inspection.evidenceSource === 'owner.json' ? 'owner.json' : entries.find((entry) => /^owner\.([A-Za-z0-9_-]{16,128})\.tmp$/.test(entry));
|
|
611
|
+
if (!evidenceName || entries.length !== 1) {
|
|
612
|
+
return { ...inspection, action: 'none', recovered: false, reason: 'Session pointer lock evidence changed before recovery claim.' };
|
|
613
|
+
}
|
|
614
|
+
const owner = await inspectLockOwnerFile(join(context.lockPath, evidenceName));
|
|
615
|
+
if (owner.status !== 'dead' || !owner.owner) {
|
|
616
|
+
return { ...inspection, action: 'none', recovered: false, reason: 'Session pointer lock evidence changed before recovery claim.' };
|
|
617
|
+
}
|
|
618
|
+
let claimToken;
|
|
619
|
+
try {
|
|
620
|
+
claimToken = transactionDependencies.token();
|
|
621
|
+
}
|
|
622
|
+
catch {
|
|
623
|
+
return { ...inspection, action: 'none', recovered: false, reason: 'Unable to create recovery claim token.' };
|
|
624
|
+
}
|
|
625
|
+
if (!isValidToken(claimToken))
|
|
626
|
+
return { ...inspection, action: 'none', recovered: false, reason: 'Recovery claim token is invalid.' };
|
|
627
|
+
const claimName = `owner.${owner.owner.token}.${claimToken}.recovery`;
|
|
628
|
+
const claimPath = join(context.lockPath, claimName);
|
|
629
|
+
try {
|
|
630
|
+
await transactionDependencies.fs.lstat(claimPath);
|
|
631
|
+
return { ...inspection, action: 'none', recovered: false, reason: 'Recovery claim path already exists.' };
|
|
632
|
+
}
|
|
633
|
+
catch (error) {
|
|
634
|
+
if (!isNotFound(error))
|
|
635
|
+
return { ...inspection, action: 'none', recovered: false, reason: 'Unable to inspect recovery claim path.' };
|
|
636
|
+
}
|
|
637
|
+
try {
|
|
638
|
+
await transactionDependencies.fs.rename(join(context.lockPath, evidenceName), claimPath);
|
|
639
|
+
}
|
|
640
|
+
catch {
|
|
641
|
+
return { ...inspection, action: 'none', recovered: false, reason: 'Session pointer lock evidence changed before recovery claim.' };
|
|
642
|
+
}
|
|
643
|
+
const claimed = await inspectSessionPointerLockAtContext(context);
|
|
644
|
+
let claimedEntries;
|
|
645
|
+
try {
|
|
646
|
+
claimedEntries = await transactionDependencies.fs.readdir(context.lockPath);
|
|
647
|
+
}
|
|
648
|
+
catch {
|
|
649
|
+
return { ...claimed, action: 'none', recovered: false, reason: 'Unable to revalidate recovery claim.' };
|
|
650
|
+
}
|
|
651
|
+
const claimOwner = await inspectLockOwnerFile(claimPath);
|
|
652
|
+
if (claimedEntries.length !== 1 || claimedEntries[0] !== claimName || claimOwner.status !== 'dead' || claimOwner.owner?.token !== owner.owner.token || claimed.status !== 'unexpected') {
|
|
653
|
+
return { ...claimed, action: 'none', recovered: false, reason: 'Recovery claim revalidation failed.' };
|
|
654
|
+
}
|
|
655
|
+
const quarantinePath = `${context.lockPath}.quarantine.${owner.owner.token}.${claimToken}`;
|
|
656
|
+
try {
|
|
657
|
+
await transactionDependencies.fs.lstat(quarantinePath);
|
|
658
|
+
return { ...claimed, action: 'none', recovered: false, reason: 'Recovery quarantine path already exists.' };
|
|
659
|
+
}
|
|
660
|
+
catch (error) {
|
|
661
|
+
if (!isNotFound(error))
|
|
662
|
+
return { ...claimed, action: 'none', recovered: false, reason: 'Unable to inspect recovery quarantine path.' };
|
|
663
|
+
}
|
|
664
|
+
try {
|
|
665
|
+
await transactionDependencies.fs.rename(claimPath, quarantinePath);
|
|
666
|
+
}
|
|
667
|
+
catch {
|
|
668
|
+
return { ...claimed, action: 'none', recovered: false, reason: 'Unable to quarantine the exact session pointer recovery claim.' };
|
|
669
|
+
}
|
|
670
|
+
try {
|
|
671
|
+
await transactionDependencies.fs.rmdir(context.lockPath);
|
|
672
|
+
}
|
|
673
|
+
catch (error) {
|
|
674
|
+
if (!isNotFound(error)) {
|
|
675
|
+
return {
|
|
676
|
+
...claimed,
|
|
677
|
+
action: 'none',
|
|
678
|
+
recovered: false,
|
|
679
|
+
reason: 'The exact recovery claim was quarantined, but the canonical lock directory was not empty.',
|
|
680
|
+
quarantinePath,
|
|
681
|
+
};
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
return { ...inspection, action: 'quarantined', recovered: true, reason: 'Dead session pointer lock was atomically claimed and quarantined.', quarantinePath };
|
|
685
|
+
}
|
|
686
|
+
function buildLockOwner(token) {
|
|
687
|
+
let identity;
|
|
688
|
+
try {
|
|
689
|
+
identity = transactionDependencies.readProcessIdentity(process.pid, process.platform);
|
|
690
|
+
}
|
|
691
|
+
catch {
|
|
692
|
+
// Publication remains valid with optional identity metadata omitted.
|
|
693
|
+
}
|
|
694
|
+
return {
|
|
695
|
+
version: 1,
|
|
696
|
+
token,
|
|
697
|
+
pid: process.pid,
|
|
698
|
+
platform: process.platform,
|
|
699
|
+
...(identity?.status === 'matching' && isValidStartTicks(identity.startTicks)
|
|
700
|
+
? { pid_start_ticks: identity.startTicks }
|
|
701
|
+
: {}),
|
|
702
|
+
...(identity?.status === 'matching' && identity.cmdlineHash && SHA256_PATTERN.test(identity.cmdlineHash)
|
|
703
|
+
? { pid_cmdline_hash: identity.cmdlineHash }
|
|
704
|
+
: {}),
|
|
705
|
+
created_at: new Date(transactionDependencies.nowMs()).toISOString(),
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
async function removeOwnedPath(path) {
|
|
709
|
+
try {
|
|
710
|
+
await transactionDependencies.fs.unlink(path);
|
|
711
|
+
return undefined;
|
|
712
|
+
}
|
|
713
|
+
catch (error) {
|
|
714
|
+
if (isNotFound(error))
|
|
715
|
+
return undefined;
|
|
716
|
+
try {
|
|
717
|
+
await transactionDependencies.fs.readFile(path, 'utf8');
|
|
718
|
+
}
|
|
719
|
+
catch (readError) {
|
|
720
|
+
if (isNotFound(readError))
|
|
721
|
+
return undefined;
|
|
722
|
+
}
|
|
723
|
+
return {
|
|
724
|
+
operation: 'precommit-cleanup',
|
|
725
|
+
phase: 'remove-pointer-temp',
|
|
726
|
+
ownership: 'held',
|
|
727
|
+
message: `Unable to remove transaction-owned pointer temporary file: ${errorMessage(error)}`,
|
|
728
|
+
cause: error,
|
|
729
|
+
evidencePath: path,
|
|
730
|
+
};
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
async function rollbackUnpublishedLock(context, ownerTempPath) {
|
|
734
|
+
const failures = [];
|
|
735
|
+
if (ownerTempPath) {
|
|
736
|
+
try {
|
|
737
|
+
await transactionDependencies.fs.unlink(ownerTempPath);
|
|
738
|
+
}
|
|
739
|
+
catch (error) {
|
|
740
|
+
if (!isNotFound(error)) {
|
|
741
|
+
failures.push({
|
|
742
|
+
operation: 'precommit-cleanup',
|
|
743
|
+
phase: 'remove-owner-temp',
|
|
744
|
+
ownership: 'held',
|
|
745
|
+
message: `Unable to remove transaction-owned lock owner temporary file: ${errorMessage(error)}`,
|
|
746
|
+
cause: error,
|
|
747
|
+
evidencePath: ownerTempPath,
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
let entries;
|
|
753
|
+
try {
|
|
754
|
+
entries = await transactionDependencies.fs.readdir(context.lockPath);
|
|
755
|
+
}
|
|
756
|
+
catch (error) {
|
|
757
|
+
failures.push({
|
|
758
|
+
operation: 'precommit-cleanup',
|
|
759
|
+
phase: 'inspect-unpublished-lock',
|
|
760
|
+
ownership: 'uncertain',
|
|
761
|
+
message: `Unable to inspect unpublished session pointer lock: ${errorMessage(error)}`,
|
|
762
|
+
cause: error,
|
|
763
|
+
evidencePath: context.lockPath,
|
|
764
|
+
});
|
|
765
|
+
return failures;
|
|
766
|
+
}
|
|
767
|
+
if (entries.length > 0) {
|
|
768
|
+
failures.push({
|
|
769
|
+
operation: 'precommit-cleanup',
|
|
770
|
+
phase: 'inspect-unpublished-lock',
|
|
771
|
+
ownership: 'uncertain',
|
|
772
|
+
message: 'Unpublished session pointer lock contains unexpected evidence.',
|
|
773
|
+
evidencePath: context.lockPath,
|
|
774
|
+
});
|
|
775
|
+
return failures;
|
|
776
|
+
}
|
|
777
|
+
try {
|
|
778
|
+
await transactionDependencies.fs.rmdir(context.lockPath);
|
|
779
|
+
}
|
|
780
|
+
catch (error) {
|
|
781
|
+
failures.push({
|
|
782
|
+
operation: 'precommit-cleanup',
|
|
783
|
+
phase: 'remove-unpublished-lock',
|
|
784
|
+
ownership: 'held',
|
|
785
|
+
message: `Unable to remove empty unpublished session pointer lock: ${errorMessage(error)}`,
|
|
786
|
+
cause: error,
|
|
787
|
+
evidencePath: context.lockPath,
|
|
788
|
+
});
|
|
789
|
+
}
|
|
790
|
+
return failures;
|
|
791
|
+
}
|
|
792
|
+
async function acquirePointerLock(context, candidateSessionId, timeoutMs, platform, tracker, regularFileSync) {
|
|
793
|
+
const deadline = transactionDependencies.nowMs() + timeoutMs;
|
|
794
|
+
let attempt = 0;
|
|
795
|
+
while (true) {
|
|
796
|
+
try {
|
|
797
|
+
await transactionDependencies.fs.mkdir(context.lockPath);
|
|
798
|
+
break;
|
|
799
|
+
}
|
|
800
|
+
catch (error) {
|
|
801
|
+
if (!isAlreadyExists(error)) {
|
|
802
|
+
throw resolvedAbort(context, {
|
|
803
|
+
code: 'session_pointer_io_failure',
|
|
804
|
+
operation: 'lock-acquire',
|
|
805
|
+
...(candidateSessionId ? { candidateSessionId } : {}),
|
|
806
|
+
lockPath: context.lockPath,
|
|
807
|
+
reason: `Unable to acquire session pointer lock: ${errorMessage(error)}`,
|
|
808
|
+
cause: error,
|
|
809
|
+
});
|
|
810
|
+
}
|
|
811
|
+
const owner = await inspectLockOwner(context.lockPath);
|
|
812
|
+
if (owner.status !== 'live') {
|
|
813
|
+
throw resolvedAbort(context, {
|
|
814
|
+
code: 'session_pointer_lock_recovery_required',
|
|
815
|
+
operation: 'lock-acquire',
|
|
816
|
+
...(candidateSessionId ? { candidateSessionId } : {}),
|
|
817
|
+
lockPath: context.lockPath,
|
|
818
|
+
lockOwnerStatus: owner.status,
|
|
819
|
+
reason: `Session pointer lock requires explicit recovery (${owner.status}).`,
|
|
820
|
+
});
|
|
821
|
+
}
|
|
822
|
+
const remaining = deadline - transactionDependencies.nowMs();
|
|
823
|
+
if (remaining <= 0) {
|
|
824
|
+
throw resolvedAbort(context, {
|
|
825
|
+
code: 'session_pointer_lock_timeout',
|
|
826
|
+
operation: 'lock-acquire',
|
|
827
|
+
...(candidateSessionId ? { candidateSessionId } : {}),
|
|
828
|
+
lockPath: context.lockPath,
|
|
829
|
+
lockOwnerStatus: 'live',
|
|
830
|
+
reason: 'Timed out waiting for the live session pointer lock owner.',
|
|
831
|
+
});
|
|
832
|
+
}
|
|
833
|
+
const delay = Math.min(LOCK_RETRY_DELAYS_MS[Math.min(attempt, LOCK_RETRY_DELAYS_MS.length - 1)], remaining);
|
|
834
|
+
attempt += 1;
|
|
835
|
+
try {
|
|
836
|
+
await transactionDependencies.sleep(delay);
|
|
837
|
+
}
|
|
838
|
+
catch (sleepError) {
|
|
839
|
+
throw resolvedAbort(context, {
|
|
840
|
+
code: 'session_pointer_io_failure',
|
|
841
|
+
operation: 'lock-acquire',
|
|
842
|
+
...(candidateSessionId ? { candidateSessionId } : {}),
|
|
843
|
+
lockPath: context.lockPath,
|
|
844
|
+
reason: `Unable to wait for session pointer lock: ${errorMessage(sleepError)}`,
|
|
845
|
+
cause: sleepError,
|
|
846
|
+
});
|
|
847
|
+
}
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
let token;
|
|
851
|
+
try {
|
|
852
|
+
token = transactionDependencies.token();
|
|
853
|
+
}
|
|
854
|
+
catch (error) {
|
|
855
|
+
const primary = resolvedAbort(context, {
|
|
856
|
+
code: 'session_pointer_io_failure',
|
|
857
|
+
operation: 'lock-owner-publish',
|
|
858
|
+
...(candidateSessionId ? { candidateSessionId } : {}),
|
|
859
|
+
lockPath: context.lockPath,
|
|
860
|
+
reason: `Unable to create session pointer lock token: ${errorMessage(error)}`,
|
|
861
|
+
cause: error,
|
|
862
|
+
});
|
|
863
|
+
const failures = await rollbackUnpublishedLock(context);
|
|
864
|
+
if (failures.length === 0)
|
|
865
|
+
throw primary;
|
|
866
|
+
throw recoveryAbort(context, primary, failures, 'precommit-cleanup');
|
|
867
|
+
}
|
|
868
|
+
if (!isValidToken(token)) {
|
|
869
|
+
const cause = new Error('Session pointer transaction token is invalid.');
|
|
870
|
+
const primary = resolvedAbort(context, {
|
|
871
|
+
code: 'session_pointer_io_failure',
|
|
872
|
+
operation: 'lock-owner-publish',
|
|
873
|
+
...(candidateSessionId ? { candidateSessionId } : {}),
|
|
874
|
+
lockPath: context.lockPath,
|
|
875
|
+
reason: cause.message,
|
|
876
|
+
cause,
|
|
877
|
+
});
|
|
878
|
+
const failures = await rollbackUnpublishedLock(context);
|
|
879
|
+
if (failures.length === 0)
|
|
880
|
+
throw primary;
|
|
881
|
+
throw recoveryAbort(context, primary, failures, 'precommit-cleanup');
|
|
882
|
+
}
|
|
883
|
+
const ownerTempPath = join(context.lockPath, `owner.${token}.tmp`);
|
|
884
|
+
const ownerPath = join(context.lockPath, 'owner.json');
|
|
885
|
+
try {
|
|
886
|
+
await transactionDependencies.fs.writeFile(ownerTempPath, JSON.stringify(buildLockOwner(token)), { mode: 0o600 });
|
|
887
|
+
recordRegularFileSyncOutcome(tracker, await transactionDependencies.fs.openAndSync(ownerTempPath, platform, regularFileSync));
|
|
888
|
+
await transactionDependencies.fs.rename(ownerTempPath, ownerPath);
|
|
889
|
+
}
|
|
890
|
+
catch (error) {
|
|
891
|
+
const primary = resolvedAbort(context, {
|
|
892
|
+
code: 'session_pointer_io_failure',
|
|
893
|
+
operation: 'lock-owner-publish',
|
|
894
|
+
...(candidateSessionId ? { candidateSessionId } : {}),
|
|
895
|
+
lockPath: context.lockPath,
|
|
896
|
+
reason: `Unable to publish session pointer lock owner: ${errorMessage(error)}`,
|
|
897
|
+
cause: error,
|
|
898
|
+
});
|
|
899
|
+
const failures = await rollbackUnpublishedLock(context, ownerTempPath);
|
|
900
|
+
if (failures.length === 0)
|
|
901
|
+
throw primary;
|
|
902
|
+
throw recoveryAbort(context, primary, failures, 'precommit-cleanup');
|
|
903
|
+
}
|
|
904
|
+
return { context, token };
|
|
905
|
+
}
|
|
906
|
+
async function releasePointerLock(lock) {
|
|
907
|
+
const ownerPath = join(lock.context.lockPath, 'owner.json');
|
|
908
|
+
let owner = null;
|
|
909
|
+
try {
|
|
910
|
+
owner = parseLockOwner(await transactionDependencies.fs.readFile(ownerPath, 'utf8'));
|
|
911
|
+
}
|
|
912
|
+
catch {
|
|
913
|
+
// The error is represented as a token-check uncertainty below.
|
|
914
|
+
}
|
|
915
|
+
if (!owner || owner.token !== lock.token) {
|
|
916
|
+
return [{
|
|
917
|
+
operation: 'lock-release',
|
|
918
|
+
phase: 'token-check',
|
|
919
|
+
ownership: 'uncertain',
|
|
920
|
+
message: 'Unable to prove ownership of the canonical session pointer lock.',
|
|
921
|
+
evidencePath: lock.context.lockPath,
|
|
922
|
+
}];
|
|
923
|
+
}
|
|
924
|
+
const releasePath = `${lock.context.lockPath}.release-${lock.token}`;
|
|
925
|
+
try {
|
|
926
|
+
await transactionDependencies.fs.rename(lock.context.lockPath, releasePath);
|
|
927
|
+
}
|
|
928
|
+
catch (error) {
|
|
929
|
+
return [{
|
|
930
|
+
operation: 'lock-release',
|
|
931
|
+
phase: 'rename',
|
|
932
|
+
ownership: 'held',
|
|
933
|
+
message: `Unable to release the canonical session pointer lock: ${errorMessage(error)}`,
|
|
934
|
+
cause: error,
|
|
935
|
+
evidencePath: lock.context.lockPath,
|
|
936
|
+
}];
|
|
937
|
+
}
|
|
938
|
+
try {
|
|
939
|
+
await transactionDependencies.fs.unlink(join(releasePath, 'owner.json'));
|
|
940
|
+
}
|
|
941
|
+
catch (error) {
|
|
942
|
+
if (!isNotFound(error)) {
|
|
943
|
+
return [{
|
|
944
|
+
operation: 'lock-release',
|
|
945
|
+
phase: 'remove-release-dir',
|
|
946
|
+
ownership: 'released',
|
|
947
|
+
message: `Unable to remove released session pointer lock owner: ${errorMessage(error)}`,
|
|
948
|
+
cause: error,
|
|
949
|
+
evidencePath: releasePath,
|
|
950
|
+
}];
|
|
951
|
+
}
|
|
952
|
+
}
|
|
953
|
+
try {
|
|
954
|
+
await transactionDependencies.fs.rmdir(releasePath);
|
|
955
|
+
return [];
|
|
956
|
+
}
|
|
957
|
+
catch (error) {
|
|
958
|
+
return [{
|
|
959
|
+
operation: 'lock-release',
|
|
960
|
+
phase: 'remove-release-dir',
|
|
961
|
+
ownership: 'released',
|
|
962
|
+
message: `Unable to remove released session pointer lock evidence: ${errorMessage(error)}`,
|
|
963
|
+
cause: error,
|
|
964
|
+
evidencePath: releasePath,
|
|
965
|
+
}];
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
function recoveryAbort(context, primary, failures, operation) {
|
|
969
|
+
return resolvedAbort(context, {
|
|
970
|
+
code: 'session_pointer_lock_recovery_required',
|
|
971
|
+
operation,
|
|
972
|
+
...(primary.candidateSessionId ? { candidateSessionId: primary.candidateSessionId } : {}),
|
|
973
|
+
...(primary.canonicalSessionId ? { canonicalSessionId: primary.canonicalSessionId } : {}),
|
|
974
|
+
lockPath: context.lockPath,
|
|
975
|
+
...(primary.pointerStatus ? { pointerStatus: primary.pointerStatus } : {}),
|
|
976
|
+
reason: `Session pointer cleanup requires explicit recovery after ${primary.operation}.`,
|
|
977
|
+
cause: primary.cause,
|
|
978
|
+
primaryOperation: primary.operation,
|
|
979
|
+
secondaryFailures: failures,
|
|
980
|
+
});
|
|
981
|
+
}
|
|
982
|
+
function releaseFailureError(failures) {
|
|
983
|
+
const error = new Error('Session pointer committed, but lock release left recovery evidence.');
|
|
984
|
+
Object.assign(error, { secondaryFailures: failures });
|
|
985
|
+
return error;
|
|
986
|
+
}
|
|
987
|
+
async function finalizePrecommitAbort(lock, primary, pointerTempPath) {
|
|
988
|
+
const failures = [];
|
|
989
|
+
if (pointerTempPath) {
|
|
990
|
+
const pointerTempFailure = await removeOwnedPath(pointerTempPath);
|
|
991
|
+
if (pointerTempFailure)
|
|
992
|
+
failures.push(pointerTempFailure);
|
|
993
|
+
}
|
|
994
|
+
failures.push(...await releasePointerLock(lock));
|
|
995
|
+
if (failures.length === 0)
|
|
996
|
+
return primary;
|
|
997
|
+
return recoveryAbort(lock.context, primary, failures, pointerTempPath && failures[0]?.phase === 'remove-pointer-temp' ? 'precommit-cleanup' : 'lock-release');
|
|
998
|
+
}
|
|
999
|
+
function unusablePointerAbort(context, candidateSessionId, pointer) {
|
|
1000
|
+
const pointerStatus = pointer.status === 'malformed'
|
|
1001
|
+
|| pointer.status === 'foreign-cwd'
|
|
1002
|
+
|| pointer.status === 'identity-indeterminate'
|
|
1003
|
+
? pointer.status
|
|
1004
|
+
: undefined;
|
|
1005
|
+
return resolvedAbort(context, {
|
|
1006
|
+
code: 'session_pointer_unusable',
|
|
1007
|
+
operation: 'pointer-classify',
|
|
1008
|
+
...(candidateSessionId ? { candidateSessionId } : {}),
|
|
1009
|
+
...(pointer.state?.session_id ? { canonicalSessionId: pointer.state.session_id } : {}),
|
|
1010
|
+
lockPath: context.lockPath,
|
|
1011
|
+
...(pointerStatus ? { pointerStatus } : {}),
|
|
1012
|
+
reason: `Selected session pointer is ${pointer.status} and is preserved.`,
|
|
1013
|
+
});
|
|
1014
|
+
}
|
|
1015
|
+
function ownerConflictAbort(context, candidateSessionId, state, cause) {
|
|
1016
|
+
return resolvedAbort(context, {
|
|
1017
|
+
code: 'session_pointer_owner_conflict',
|
|
1018
|
+
operation: 'owner-conflict',
|
|
1019
|
+
candidateSessionId,
|
|
1020
|
+
canonicalSessionId: state.session_id,
|
|
1021
|
+
lockPath: context.lockPath,
|
|
1022
|
+
reason: `Session pointer ${state.session_id} conflicts with requested session ${candidateSessionId}.`,
|
|
1023
|
+
...(cause ? { cause } : {}),
|
|
278
1024
|
});
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
1025
|
+
}
|
|
1026
|
+
async function writePointerTransaction(cwd, candidateSessionId, options, timeoutMs, transition, pointerState) {
|
|
1027
|
+
let context;
|
|
1028
|
+
try {
|
|
1029
|
+
context = options.context ?? resolveSessionPointerContext(cwd);
|
|
1030
|
+
}
|
|
1031
|
+
catch (error) {
|
|
1032
|
+
throw contextAbort(cwd, candidateSessionId, error);
|
|
1033
|
+
}
|
|
1034
|
+
if (!candidateSessionId) {
|
|
1035
|
+
const cause = new Error('A valid session ID is required for pointer mutation.');
|
|
1036
|
+
throw resolvedAbort(context, {
|
|
1037
|
+
code: 'session_pointer_io_failure',
|
|
1038
|
+
operation: 'pointer-classify',
|
|
1039
|
+
lockPath: context.lockPath,
|
|
1040
|
+
reason: cause.message,
|
|
1041
|
+
cause,
|
|
283
1042
|
});
|
|
284
1043
|
}
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
1044
|
+
try {
|
|
1045
|
+
await transactionDependencies.fs.mkdir(context.baseStateDir, { recursive: true });
|
|
1046
|
+
}
|
|
1047
|
+
catch (error) {
|
|
1048
|
+
throw resolvedAbort(context, {
|
|
1049
|
+
code: 'session_pointer_io_failure',
|
|
1050
|
+
operation: 'state-dir-create',
|
|
1051
|
+
candidateSessionId,
|
|
1052
|
+
lockPath: context.lockPath,
|
|
1053
|
+
reason: `Unable to create selected session state directory: ${errorMessage(error)}`,
|
|
1054
|
+
cause: error,
|
|
1055
|
+
});
|
|
1056
|
+
}
|
|
1057
|
+
const platform = options.platform ?? process.platform;
|
|
1058
|
+
const tracker = { degraded: false };
|
|
1059
|
+
const lock = await acquirePointerLock(context, candidateSessionId, timeoutMs, platform, tracker, options.regularFileSync);
|
|
1060
|
+
const pointerTempPath = `${context.sessionPath}.tmp-${lock.token}`;
|
|
1061
|
+
let pointerCommitted = false;
|
|
1062
|
+
try {
|
|
1063
|
+
let pointer;
|
|
1064
|
+
try {
|
|
1065
|
+
pointer = await readSessionPointer(context);
|
|
1066
|
+
}
|
|
1067
|
+
catch (error) {
|
|
1068
|
+
throw resolvedAbort(context, {
|
|
1069
|
+
code: 'session_pointer_io_failure',
|
|
1070
|
+
operation: 'pointer-read',
|
|
1071
|
+
candidateSessionId,
|
|
1072
|
+
lockPath: context.lockPath,
|
|
1073
|
+
reason: `Unable to read selected session pointer: ${errorMessage(error)}`,
|
|
1074
|
+
cause: error,
|
|
303
1075
|
});
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
1076
|
+
}
|
|
1077
|
+
let next;
|
|
1078
|
+
try {
|
|
1079
|
+
next = transition(pointer, context);
|
|
1080
|
+
}
|
|
1081
|
+
catch (error) {
|
|
1082
|
+
if (isSessionPointerLaunchAbort(error))
|
|
1083
|
+
throw error;
|
|
1084
|
+
const state = pointer.state;
|
|
1085
|
+
throw ownerConflictAbort(context, candidateSessionId, state ?? {
|
|
1086
|
+
session_id: candidateSessionId,
|
|
1087
|
+
started_at: '',
|
|
1088
|
+
cwd: context.cwd,
|
|
1089
|
+
pid: 0,
|
|
1090
|
+
}, error);
|
|
1091
|
+
}
|
|
1092
|
+
const serialized = JSON.stringify(pointerState(next), null, 2);
|
|
1093
|
+
try {
|
|
1094
|
+
await transactionDependencies.fs.writeFile(pointerTempPath, serialized, { mode: 0o600, flag: 'wx' });
|
|
1095
|
+
}
|
|
1096
|
+
catch (error) {
|
|
1097
|
+
throw resolvedAbort(context, {
|
|
1098
|
+
code: 'session_pointer_io_failure',
|
|
1099
|
+
operation: 'pointer-temp-write',
|
|
1100
|
+
candidateSessionId,
|
|
1101
|
+
lockPath: context.lockPath,
|
|
1102
|
+
reason: `Unable to write session pointer temporary file: ${errorMessage(error)}`,
|
|
1103
|
+
cause: error,
|
|
1104
|
+
});
|
|
1105
|
+
}
|
|
1106
|
+
try {
|
|
1107
|
+
recordRegularFileSyncOutcome(tracker, await transactionDependencies.fs.openAndSync(pointerTempPath, platform, options.regularFileSync));
|
|
1108
|
+
}
|
|
1109
|
+
catch (error) {
|
|
1110
|
+
throw resolvedAbort(context, {
|
|
1111
|
+
code: 'session_pointer_io_failure',
|
|
1112
|
+
operation: 'pointer-fsync',
|
|
1113
|
+
candidateSessionId,
|
|
1114
|
+
lockPath: context.lockPath,
|
|
1115
|
+
reason: `Unable to sync session pointer temporary file: ${errorMessage(error)}`,
|
|
1116
|
+
cause: error,
|
|
310
1117
|
});
|
|
311
1118
|
}
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
1119
|
+
try {
|
|
1120
|
+
await transactionDependencies.fs.rename(pointerTempPath, context.sessionPath);
|
|
1121
|
+
pointerCommitted = true;
|
|
1122
|
+
}
|
|
1123
|
+
catch (error) {
|
|
1124
|
+
throw resolvedAbort(context, {
|
|
1125
|
+
code: 'session_pointer_io_failure',
|
|
1126
|
+
operation: 'pointer-rename',
|
|
1127
|
+
candidateSessionId,
|
|
1128
|
+
lockPath: context.lockPath,
|
|
1129
|
+
reason: `Unable to atomically publish session pointer: ${errorMessage(error)}`,
|
|
1130
|
+
cause: error,
|
|
1131
|
+
});
|
|
1132
|
+
}
|
|
1133
|
+
const releaseFailures = await releasePointerLock(lock);
|
|
1134
|
+
if (releaseFailures.length > 0)
|
|
1135
|
+
throw releaseFailureError(releaseFailures);
|
|
1136
|
+
emitDegradedDurabilityWarning('session pointer start/reconcile', tracker);
|
|
1137
|
+
return { context, value: next };
|
|
1138
|
+
}
|
|
1139
|
+
catch (error) {
|
|
1140
|
+
if (pointerCommitted)
|
|
1141
|
+
throw error;
|
|
1142
|
+
const primary = isSessionPointerLaunchAbort(error)
|
|
1143
|
+
? error
|
|
1144
|
+
: resolvedAbort(context, {
|
|
1145
|
+
code: 'session_pointer_io_failure',
|
|
1146
|
+
operation: 'pointer-classify',
|
|
1147
|
+
candidateSessionId,
|
|
1148
|
+
lockPath: context.lockPath,
|
|
1149
|
+
reason: `Session pointer transition failed before commit: ${errorMessage(error)}`,
|
|
1150
|
+
cause: error,
|
|
1151
|
+
});
|
|
1152
|
+
const ownsPointerTemp = primary.operation === 'pointer-temp-write'
|
|
1153
|
+
|| primary.operation === 'pointer-fsync'
|
|
1154
|
+
|| primary.operation === 'pointer-rename';
|
|
1155
|
+
throw await finalizePrecommitAbort(lock, primary, ownsPointerTemp ? pointerTempPath : undefined);
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
function startPointerTransition(requestedSessionId, options) {
|
|
1159
|
+
return (pointer, context) => {
|
|
1160
|
+
if (pointer.status !== 'absent' && pointer.status !== 'stale-dead' && pointer.status !== 'usable') {
|
|
1161
|
+
throw unusablePointerAbort(context, requestedSessionId, pointer);
|
|
1162
|
+
}
|
|
1163
|
+
const existing = pointer.status === 'usable' ? pointer.state : undefined;
|
|
1164
|
+
if (existing && !isStartCompatible(existing, requestedSessionId)) {
|
|
1165
|
+
throw ownerConflictAbort(context, requestedSessionId, existing);
|
|
1166
|
+
}
|
|
1167
|
+
const canonicalSessionId = existing?.session_id ?? requestedSessionId;
|
|
1168
|
+
const pid = resolvePid(options);
|
|
1169
|
+
const platform = options.platform ?? process.platform;
|
|
1170
|
+
const ownerCandidate = verifiedOwnerCandidate(context, options);
|
|
1171
|
+
let ownerOmxSessionId;
|
|
1172
|
+
try {
|
|
1173
|
+
ownerOmxSessionId = mergeOwnerAlias(existing, ownerCandidate, ownerCandidate !== undefined);
|
|
1174
|
+
}
|
|
1175
|
+
catch (error) {
|
|
1176
|
+
throw ownerConflictAbort(context, requestedSessionId, existing ?? {
|
|
1177
|
+
session_id: canonicalSessionId,
|
|
1178
|
+
started_at: '',
|
|
1179
|
+
cwd: context.cwd,
|
|
1180
|
+
pid,
|
|
1181
|
+
}, error);
|
|
1182
|
+
}
|
|
1183
|
+
return createSessionState(context.cwd, canonicalSessionId, pid, platform, sessionIdentityFor(pid, platform), {
|
|
1184
|
+
nativeSessionId: options.nativeSessionId ?? existing?.native_session_id,
|
|
1185
|
+
previousNativeSessionId: options.previousNativeSessionId ?? existing?.previous_native_session_id,
|
|
1186
|
+
nativeSessionSwitchedAt: options.nativeSessionSwitchedAt ?? existing?.native_session_switched_at,
|
|
1187
|
+
...(ownerOmxSessionId ? { ownerOmxSessionId } : {}),
|
|
1188
|
+
tmuxSessionName: options.tmuxSessionName ?? existing?.tmux_session_name,
|
|
1189
|
+
tmuxPaneId: options.tmuxPaneId ?? existing?.tmux_pane_id,
|
|
315
1190
|
});
|
|
1191
|
+
};
|
|
1192
|
+
}
|
|
1193
|
+
/** Write or merge a wrapper-owned canonical pointer through the exact-root transaction. */
|
|
1194
|
+
export async function writeSessionStart(cwd, sessionId, options = {}) {
|
|
1195
|
+
const requestedSessionId = normalizeSessionId(sessionId);
|
|
1196
|
+
const result = await writePointerTransaction(cwd, requestedSessionId, options, DEFAULT_POINTER_TIMEOUT_MS, startPointerTransition(requestedSessionId ?? sessionId, options), (state) => state);
|
|
1197
|
+
await appendToLogAtContext(result.context, {
|
|
1198
|
+
event: 'session_start',
|
|
1199
|
+
session_id: result.value.session_id,
|
|
1200
|
+
...(result.value.native_session_id ? { native_session_id: result.value.native_session_id } : {}),
|
|
1201
|
+
pid: result.value.pid,
|
|
1202
|
+
timestamp: result.value.started_at,
|
|
1203
|
+
}).catch(() => { });
|
|
1204
|
+
return result.value;
|
|
1205
|
+
}
|
|
1206
|
+
function reconcileNativeTransition(nativeSessionId, options) {
|
|
1207
|
+
return (pointer, context) => {
|
|
1208
|
+
if (pointer.status !== 'absent' && pointer.status !== 'stale-dead' && pointer.status !== 'usable') {
|
|
1209
|
+
throw unusablePointerAbort(context, nativeSessionId, pointer);
|
|
1210
|
+
}
|
|
1211
|
+
const pid = resolvePid(options);
|
|
1212
|
+
const platform = options.platform ?? process.platform;
|
|
1213
|
+
const linuxIdentity = sessionIdentityFor(pid, platform);
|
|
1214
|
+
const existing = pointer.status === 'usable' ? pointer.state : undefined;
|
|
1215
|
+
const nowIso = new Date().toISOString();
|
|
1216
|
+
if (!existing) {
|
|
1217
|
+
const ownerCandidate = verifiedOwnerCandidate(context, options);
|
|
1218
|
+
const ownerOmxSessionId = ownerCandidate;
|
|
1219
|
+
return {
|
|
1220
|
+
state: createSessionState(context.cwd, nativeSessionId, pid, platform, linuxIdentity, {
|
|
1221
|
+
nativeSessionId,
|
|
1222
|
+
...(ownerOmxSessionId ? { ownerOmxSessionId } : {}),
|
|
1223
|
+
}),
|
|
1224
|
+
};
|
|
1225
|
+
}
|
|
1226
|
+
const existingNativeSessionId = normalizeSessionId(existing.native_session_id);
|
|
1227
|
+
if (existingNativeSessionId && existingNativeSessionId !== nativeSessionId) {
|
|
1228
|
+
const ownerOmxSessionId = getOmxLaunchSessionId(existing);
|
|
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
|
+
};
|
|
1250
|
+
}
|
|
1251
|
+
const ownerCandidate = verifiedOwnerCandidate(context, options);
|
|
1252
|
+
let ownerOmxSessionId;
|
|
1253
|
+
try {
|
|
1254
|
+
ownerOmxSessionId = mergeOwnerAlias(existing, ownerCandidate, ownerCandidate !== undefined);
|
|
1255
|
+
}
|
|
1256
|
+
catch (error) {
|
|
1257
|
+
throw ownerConflictAbort(context, nativeSessionId, existing, error);
|
|
1258
|
+
}
|
|
1259
|
+
return {
|
|
1260
|
+
state: createSessionState(context.cwd, existing.session_id, pid, platform, linuxIdentity, {
|
|
1261
|
+
nowIso,
|
|
1262
|
+
nativeSessionId,
|
|
1263
|
+
previousNativeSessionId: existing.previous_native_session_id,
|
|
1264
|
+
nativeSessionSwitchedAt: existing.native_session_switched_at,
|
|
1265
|
+
...(ownerOmxSessionId ? { ownerOmxSessionId } : {}),
|
|
1266
|
+
startedAt: existing.started_at,
|
|
1267
|
+
tmuxSessionName: existing.tmux_session_name,
|
|
1268
|
+
tmuxPaneId: existing.tmux_pane_id,
|
|
1269
|
+
}),
|
|
1270
|
+
};
|
|
1271
|
+
};
|
|
1272
|
+
}
|
|
1273
|
+
/**
|
|
1274
|
+
* Reconcile a native SessionStart without borrowing another root's pointer.
|
|
1275
|
+
* A different native ID retains the existing OMX owner chain, but never binds a
|
|
1276
|
+
* new owner alias during that replacement transition.
|
|
1277
|
+
*/
|
|
1278
|
+
export async function reconcileNativeSessionStart(cwd, nativeSessionId, options = {}) {
|
|
1279
|
+
const normalizedNativeSessionId = normalizeSessionId(nativeSessionId);
|
|
1280
|
+
const result = await writePointerTransaction(cwd, normalizedNativeSessionId, options, NATIVE_POINTER_TIMEOUT_MS, reconcileNativeTransition(normalizedNativeSessionId ?? nativeSessionId, options), (transition) => transition.state);
|
|
1281
|
+
if (result.value.replacementLog) {
|
|
1282
|
+
await appendToLogAtContext(result.context, result.value.replacementLog).catch(() => { });
|
|
316
1283
|
}
|
|
317
|
-
|
|
318
|
-
?
|
|
319
|
-
:
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
1284
|
+
await appendToLogAtContext(result.context, {
|
|
1285
|
+
event: result.value.replacementLog ? 'session_start' : 'session_start_reconciled',
|
|
1286
|
+
session_id: result.value.state.session_id,
|
|
1287
|
+
native_session_id: normalizedNativeSessionId ?? nativeSessionId,
|
|
1288
|
+
pid: result.value.state.pid,
|
|
1289
|
+
timestamp: result.value.state.native_session_switched_at ?? new Date().toISOString(),
|
|
1290
|
+
}).catch(() => { });
|
|
1291
|
+
return result.value.state;
|
|
1292
|
+
}
|
|
1293
|
+
function historyDirectory(context) {
|
|
1294
|
+
return join(dirname(context.baseStateDir), 'logs');
|
|
1295
|
+
}
|
|
1296
|
+
function historyPath(context) {
|
|
1297
|
+
return join(historyDirectory(context), HISTORY_FILE);
|
|
1298
|
+
}
|
|
1299
|
+
async function removeDeadSessionHudState(context, sessionIds) {
|
|
1300
|
+
const uniqueSessionIds = [...new Set(sessionIds.filter((value) => Boolean(normalizeSessionId(value))))];
|
|
1301
|
+
const candidatePaths = [
|
|
1302
|
+
join(context.baseStateDir, 'hud-state.json'),
|
|
1303
|
+
...uniqueSessionIds.map((sessionId) => join(context.baseStateDir, 'sessions', sessionId, 'hud-state.json')),
|
|
1304
|
+
];
|
|
1305
|
+
await Promise.all(candidatePaths.map(async (path) => {
|
|
1306
|
+
try {
|
|
1307
|
+
await rm(path, { force: true });
|
|
1308
|
+
}
|
|
1309
|
+
catch {
|
|
1310
|
+
// HUD cleanup remains best effort after history has been recorded.
|
|
1311
|
+
}
|
|
1312
|
+
}));
|
|
344
1313
|
}
|
|
345
1314
|
/**
|
|
346
|
-
*
|
|
1315
|
+
* Archive first and remove an owned pointer only after that history write
|
|
1316
|
+
* succeeds. Present unusable pointer evidence is never repaired or archived.
|
|
347
1317
|
*/
|
|
348
|
-
export async function writeSessionEnd(cwd, sessionId) {
|
|
349
|
-
const
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
:
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
:
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
1318
|
+
export async function writeSessionEnd(cwd, sessionId, options = {}) {
|
|
1319
|
+
const candidateSessionId = normalizeSessionId(sessionId);
|
|
1320
|
+
let context;
|
|
1321
|
+
try {
|
|
1322
|
+
context = options.context ?? resolveSessionPointerContext(cwd);
|
|
1323
|
+
}
|
|
1324
|
+
catch (error) {
|
|
1325
|
+
throw contextAbort(cwd, candidateSessionId, error);
|
|
1326
|
+
}
|
|
1327
|
+
if (!candidateSessionId) {
|
|
1328
|
+
const cause = new Error('A valid session ID is required to end a session pointer.');
|
|
1329
|
+
throw resolvedAbort(context, {
|
|
1330
|
+
code: 'session_pointer_io_failure',
|
|
1331
|
+
operation: 'pointer-classify',
|
|
1332
|
+
lockPath: context.lockPath,
|
|
1333
|
+
reason: cause.message,
|
|
1334
|
+
cause,
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
try {
|
|
1338
|
+
await transactionDependencies.fs.mkdir(context.baseStateDir, { recursive: true });
|
|
1339
|
+
}
|
|
1340
|
+
catch (error) {
|
|
1341
|
+
throw resolvedAbort(context, {
|
|
1342
|
+
code: 'session_pointer_io_failure',
|
|
1343
|
+
operation: 'state-dir-create',
|
|
1344
|
+
candidateSessionId,
|
|
1345
|
+
lockPath: context.lockPath,
|
|
1346
|
+
reason: `Unable to create selected session state directory: ${errorMessage(error)}`,
|
|
1347
|
+
cause: error,
|
|
1348
|
+
});
|
|
1349
|
+
}
|
|
1350
|
+
const platform = options.platform ?? process.platform;
|
|
1351
|
+
const tracker = { degraded: false };
|
|
1352
|
+
const lock = await acquirePointerLock(context, candidateSessionId, DEFAULT_POINTER_TIMEOUT_MS, platform, tracker, options.regularFileSync);
|
|
1353
|
+
let primary;
|
|
1354
|
+
try {
|
|
1355
|
+
let pointer;
|
|
381
1356
|
try {
|
|
382
|
-
await
|
|
383
|
-
}
|
|
384
|
-
catch {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
1357
|
+
pointer = await readSessionPointer(context);
|
|
1358
|
+
}
|
|
1359
|
+
catch (error) {
|
|
1360
|
+
throw resolvedAbort(context, {
|
|
1361
|
+
code: 'session_pointer_io_failure',
|
|
1362
|
+
operation: 'pointer-read',
|
|
1363
|
+
candidateSessionId,
|
|
1364
|
+
lockPath: context.lockPath,
|
|
1365
|
+
reason: `Unable to read selected session pointer: ${errorMessage(error)}`,
|
|
1366
|
+
cause: error,
|
|
1367
|
+
});
|
|
1368
|
+
}
|
|
1369
|
+
if (pointer.status !== 'absent' && pointer.status !== 'usable') {
|
|
1370
|
+
throw unusablePointerAbort(context, candidateSessionId, pointer);
|
|
1371
|
+
}
|
|
1372
|
+
const state = pointer.state;
|
|
1373
|
+
const ownsCurrentSessionFile = state == null
|
|
1374
|
+
|| state.session_id === candidateSessionId
|
|
1375
|
+
|| state.native_session_id === candidateSessionId
|
|
1376
|
+
|| state.owner_omx_session_id === candidateSessionId;
|
|
1377
|
+
const endTime = new Date().toISOString();
|
|
1378
|
+
const historyEntry = {
|
|
1379
|
+
session_id: ownsCurrentSessionFile
|
|
1380
|
+
? (state?.owner_omx_session_id === candidateSessionId ? candidateSessionId : state?.session_id || candidateSessionId)
|
|
1381
|
+
: candidateSessionId,
|
|
1382
|
+
...(ownsCurrentSessionFile && state?.native_session_id ? { native_session_id: state.native_session_id } : {}),
|
|
1383
|
+
started_at: ownsCurrentSessionFile ? state?.started_at || 'unknown' : 'unknown',
|
|
1384
|
+
ended_at: endTime,
|
|
1385
|
+
cwd: context.cwd,
|
|
1386
|
+
pid: ownsCurrentSessionFile ? state?.pid || process.pid : process.pid,
|
|
1387
|
+
...(ownsCurrentSessionFile && state?.owner_omx_session_id === candidateSessionId && state?.session_id
|
|
1388
|
+
? { active_session_id: state.session_id }
|
|
1389
|
+
: {}),
|
|
1390
|
+
...(!ownsCurrentSessionFile && state?.session_id ? { preserved_active_session_id: state.session_id } : {}),
|
|
1391
|
+
};
|
|
1392
|
+
await nodeMkdir(historyDirectory(context), { recursive: true });
|
|
1393
|
+
await appendFile(historyPath(context), `${JSON.stringify(historyEntry)}\n`);
|
|
1394
|
+
await removeDeadSessionHudState(context, [
|
|
1395
|
+
...(ownsCurrentSessionFile ? [state?.session_id, state?.native_session_id] : []),
|
|
1396
|
+
candidateSessionId,
|
|
1397
|
+
]);
|
|
1398
|
+
if (ownsCurrentSessionFile) {
|
|
1399
|
+
try {
|
|
1400
|
+
await transactionDependencies.fs.unlink(context.sessionPath);
|
|
1401
|
+
}
|
|
1402
|
+
catch (error) {
|
|
1403
|
+
if (!isNotFound(error))
|
|
1404
|
+
throw error;
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
await appendToLogAtContext(context, {
|
|
1408
|
+
event: 'session_end',
|
|
1409
|
+
session_id: historyEntry.session_id,
|
|
1410
|
+
...(historyEntry.native_session_id ? { native_session_id: historyEntry.native_session_id } : {}),
|
|
1411
|
+
...(historyEntry.active_session_id ? { active_session_id: historyEntry.active_session_id } : {}),
|
|
1412
|
+
...(historyEntry.preserved_active_session_id ? { preserved_active_session_id: historyEntry.preserved_active_session_id } : {}),
|
|
1413
|
+
timestamp: endTime,
|
|
1414
|
+
}).catch(() => { });
|
|
1415
|
+
}
|
|
1416
|
+
catch (error) {
|
|
1417
|
+
primary = error;
|
|
1418
|
+
}
|
|
1419
|
+
const releaseFailures = await releasePointerLock(lock);
|
|
1420
|
+
if (primary) {
|
|
1421
|
+
if (isSessionPointerLaunchAbort(primary) && releaseFailures.length > 0) {
|
|
1422
|
+
throw recoveryAbort(context, primary, releaseFailures, 'lock-release');
|
|
1423
|
+
}
|
|
1424
|
+
throw primary;
|
|
1425
|
+
}
|
|
1426
|
+
if (releaseFailures.length > 0)
|
|
1427
|
+
throw releaseFailureError(releaseFailures);
|
|
1428
|
+
emitDegradedDurabilityWarning('session pointer end', tracker);
|
|
1429
|
+
}
|
|
1430
|
+
/** Reset session-scoped HUD/metrics files at launch. */
|
|
1431
|
+
export async function resetSessionMetrics(cwd, sessionId) {
|
|
1432
|
+
const context = resolveSessionPointerContext(cwd);
|
|
1433
|
+
const omxDir = omxRoot(context.cwd);
|
|
1434
|
+
await nodeMkdir(omxDir, { recursive: true });
|
|
1435
|
+
await transactionDependencies.fs.mkdir(context.baseStateDir, { recursive: true });
|
|
1436
|
+
const now = new Date().toISOString();
|
|
1437
|
+
await nodeWriteFile(join(omxDir, 'metrics.json'), JSON.stringify({
|
|
1438
|
+
total_turns: 0,
|
|
1439
|
+
session_turns: 0,
|
|
1440
|
+
last_activity: now,
|
|
1441
|
+
session_input_tokens: 0,
|
|
1442
|
+
session_output_tokens: 0,
|
|
1443
|
+
session_total_tokens: 0,
|
|
1444
|
+
five_hour_limit_pct: 0,
|
|
1445
|
+
weekly_limit_pct: 0,
|
|
1446
|
+
}, null, 2));
|
|
1447
|
+
const normalizedSessionId = normalizeSessionId(sessionId);
|
|
1448
|
+
const hudStatePath = normalizedSessionId
|
|
1449
|
+
? join(context.baseStateDir, 'sessions', normalizedSessionId, 'hud-state.json')
|
|
1450
|
+
: join(context.baseStateDir, 'hud-state.json');
|
|
1451
|
+
await nodeMkdir(dirname(hudStatePath), { recursive: true });
|
|
1452
|
+
await nodeWriteFile(hudStatePath, JSON.stringify({
|
|
1453
|
+
last_turn_at: now,
|
|
1454
|
+
last_progress_at: now,
|
|
1455
|
+
turn_count: 0,
|
|
1456
|
+
last_agent_output: '',
|
|
1457
|
+
}, null, 2));
|
|
1458
|
+
}
|
|
1459
|
+
async function appendToLogAtContext(context, entry) {
|
|
1460
|
+
const logsDir = historyDirectory(context);
|
|
1461
|
+
await nodeMkdir(logsDir, { recursive: true });
|
|
1462
|
+
const logFile = join(logsDir, `omx-${new Date().toISOString().slice(0, 10)}.jsonl`);
|
|
1463
|
+
await appendFile(logFile, `${JSON.stringify({ ...entry, _ts: new Date().toISOString() })}\n`);
|
|
1464
|
+
}
|
|
1465
|
+
/**
|
|
1466
|
+
* Append one redacted provenance rejection to the already-selected state root.
|
|
1467
|
+
* This deliberately accepts a resolved context rather than cwd, and never falls
|
|
1468
|
+
* back to the ambient root when that exact write fails.
|
|
1469
|
+
*/
|
|
1470
|
+
export async function appendPromptSessionProvenanceRejection(context, descriptor) {
|
|
1471
|
+
await appendToLogAtContext(context, {
|
|
1472
|
+
event: 'prompt_session_provenance_rejected',
|
|
1473
|
+
reason: descriptor.reason,
|
|
1474
|
+
producer: descriptor.producer,
|
|
1475
|
+
selected_root_status: descriptor.selectedRootStatus,
|
|
1476
|
+
...(descriptor.relation ? { relation: descriptor.relation } : {}),
|
|
1477
|
+
timestamp: descriptor.timestamp,
|
|
399
1478
|
});
|
|
400
1479
|
}
|
|
401
1480
|
/**
|
|
402
|
-
* Append a
|
|
1481
|
+
* Append a root log entry for callers that do not already own a pointer
|
|
1482
|
+
* context. Lifecycle transitions use appendToLogAtContext instead.
|
|
403
1483
|
*/
|
|
404
1484
|
export async function appendToLog(cwd, entry) {
|
|
405
1485
|
const logsDir = omxLogsDir(cwd);
|
|
406
|
-
await
|
|
407
|
-
const
|
|
408
|
-
|
|
409
|
-
const line = JSON.stringify({ ...entry, _ts: new Date().toISOString() }) + '\n';
|
|
410
|
-
await appendFile(logFile, line);
|
|
1486
|
+
await nodeMkdir(logsDir, { recursive: true });
|
|
1487
|
+
const logFile = join(logsDir, `omx-${new Date().toISOString().slice(0, 10)}.jsonl`);
|
|
1488
|
+
await appendFile(logFile, `${JSON.stringify({ ...entry, _ts: new Date().toISOString() })}\n`);
|
|
411
1489
|
}
|
|
412
1490
|
//# sourceMappingURL=session.js.map
|