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