oh-my-codex 0.20.1 → 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.
Files changed (654) hide show
  1. package/Cargo.lock +6 -6
  2. package/Cargo.toml +1 -1
  3. package/README.md +10 -1
  4. package/dist/adapt/__tests__/hermes.test.js.map +1 -1
  5. package/dist/adapt/contracts.d.ts.map +1 -1
  6. package/dist/agents/native-config.js.map +1 -1
  7. package/dist/auth/__tests__/hotswap.test.d.ts +2 -0
  8. package/dist/auth/__tests__/hotswap.test.d.ts.map +1 -0
  9. package/dist/auth/__tests__/hotswap.test.js +183 -0
  10. package/dist/auth/__tests__/hotswap.test.js.map +1 -0
  11. package/dist/auth/hotswap.d.ts.map +1 -1
  12. package/dist/auth/hotswap.js +17 -4
  13. package/dist/auth/hotswap.js.map +1 -1
  14. package/dist/auth/paths.js.map +1 -1
  15. package/dist/auth/storage.js.map +1 -1
  16. package/dist/autopilot/__tests__/ralplan-gate.test.js +4 -4
  17. package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
  18. package/dist/autopilot/completion-gate.js.map +1 -1
  19. package/dist/autopilot/fsm.d.ts +1 -1
  20. package/dist/autopilot/fsm.d.ts.map +1 -1
  21. package/dist/autoresearch/goal.js.map +1 -1
  22. package/dist/autoresearch/runtime.js.map +1 -1
  23. package/dist/capabilities/lockfile.js.map +1 -1
  24. package/dist/catalog/reader.js.map +1 -1
  25. package/dist/catalog/skill-mirror.js.map +1 -1
  26. package/dist/cli/__tests__/adapt-help.test.js.map +1 -1
  27. package/dist/cli/__tests__/agents-init.test.js.map +1 -1
  28. package/dist/cli/__tests__/agents.test.js.map +1 -1
  29. package/dist/cli/__tests__/api.test.js.map +1 -1
  30. package/dist/cli/__tests__/ask.test.js.map +1 -1
  31. package/dist/cli/__tests__/auth.test.js.map +1 -1
  32. package/dist/cli/__tests__/autoresearch-guided.test.js.map +1 -1
  33. package/dist/cli/__tests__/autoresearch.test.js.map +1 -1
  34. package/dist/cli/__tests__/capabilities.test.js.map +1 -1
  35. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  36. package/dist/cli/__tests__/doctor-context-window-warning.test.js.map +1 -1
  37. package/dist/cli/__tests__/doctor-invalid-config.test.js +47 -4
  38. package/dist/cli/__tests__/doctor-invalid-config.test.js.map +1 -1
  39. package/dist/cli/__tests__/doctor-team.test.js.map +1 -1
  40. package/dist/cli/__tests__/doctor-warning-copy.test.js +650 -15
  41. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  42. package/dist/cli/__tests__/exec.test.js.map +1 -1
  43. package/dist/cli/__tests__/imagegen-continuation.test.js.map +1 -1
  44. package/dist/cli/__tests__/index.test.js +216 -11
  45. package/dist/cli/__tests__/index.test.js.map +1 -1
  46. package/dist/cli/__tests__/launch-fallback.test.js +49 -3
  47. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  48. package/dist/cli/__tests__/mcp-parity.test.js +84 -12
  49. package/dist/cli/__tests__/mcp-parity.test.js.map +1 -1
  50. package/dist/cli/__tests__/native-hook-claim-journal.test.d.ts +2 -0
  51. package/dist/cli/__tests__/native-hook-claim-journal.test.d.ts.map +1 -0
  52. package/dist/cli/__tests__/native-hook-claim-journal.test.js +128 -0
  53. package/dist/cli/__tests__/native-hook-claim-journal.test.js.map +1 -0
  54. package/dist/cli/__tests__/nested-help-routing.test.js.map +1 -1
  55. package/dist/cli/__tests__/question.test.js.map +1 -1
  56. package/dist/cli/__tests__/ralph-deslop-contract.test.js.map +1 -1
  57. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +6 -0
  58. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -1
  59. package/dist/cli/__tests__/ralph-prd-deep-interview.test.js.map +1 -1
  60. package/dist/cli/__tests__/ralph-prd-smoke.test.js.map +1 -1
  61. package/dist/cli/__tests__/ralph.test.js +8 -3
  62. package/dist/cli/__tests__/ralph.test.js.map +1 -1
  63. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.d.ts +2 -0
  64. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.d.ts.map +1 -0
  65. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js +187 -0
  66. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js.map +1 -0
  67. package/dist/cli/__tests__/ralplan.test.d.ts +2 -0
  68. package/dist/cli/__tests__/ralplan.test.d.ts.map +1 -0
  69. package/dist/cli/__tests__/ralplan.test.js +129 -0
  70. package/dist/cli/__tests__/ralplan.test.js.map +1 -0
  71. package/dist/cli/__tests__/resume.test.js +6 -1
  72. package/dist/cli/__tests__/resume.test.js.map +1 -1
  73. package/dist/cli/__tests__/session-scoped-runtime.test.js +25 -0
  74. package/dist/cli/__tests__/session-scoped-runtime.test.js.map +1 -1
  75. package/dist/cli/__tests__/session-search-help.test.js.map +1 -1
  76. package/dist/cli/__tests__/session-search.test.js.map +1 -1
  77. package/dist/cli/__tests__/setup-agents-overwrite.test.js +79 -0
  78. package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
  79. package/dist/cli/__tests__/setup-gh-star.test.js.map +1 -1
  80. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js +77 -3
  81. package/dist/cli/__tests__/setup-hooks-shared-ownership.test.js.map +1 -1
  82. package/dist/cli/__tests__/setup-hooks-trust-e2e.test.d.ts +2 -0
  83. package/dist/cli/__tests__/setup-hooks-trust-e2e.test.d.ts.map +1 -0
  84. package/dist/cli/__tests__/setup-hooks-trust-e2e.test.js +285 -0
  85. package/dist/cli/__tests__/setup-hooks-trust-e2e.test.js.map +1 -0
  86. package/dist/cli/__tests__/setup-install-mode.test.js +2261 -14
  87. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  88. package/dist/cli/__tests__/setup-preferences.test.d.ts +2 -0
  89. package/dist/cli/__tests__/setup-preferences.test.d.ts.map +1 -0
  90. package/dist/cli/__tests__/setup-preferences.test.js +79 -0
  91. package/dist/cli/__tests__/setup-preferences.test.js.map +1 -0
  92. package/dist/cli/__tests__/setup-refresh.test.js +33 -2
  93. package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
  94. package/dist/cli/__tests__/setup-scope.test.js +46 -3
  95. package/dist/cli/__tests__/setup-scope.test.js.map +1 -1
  96. package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
  97. package/dist/cli/__tests__/state.test.js +37 -0
  98. package/dist/cli/__tests__/state.test.js.map +1 -1
  99. package/dist/cli/__tests__/team.test.js.map +1 -1
  100. package/dist/cli/__tests__/ultragoal.test.js +61 -1
  101. package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
  102. package/dist/cli/__tests__/uninstall.test.js +1771 -24
  103. package/dist/cli/__tests__/uninstall.test.js.map +1 -1
  104. package/dist/cli/__tests__/update.test.js +58 -0
  105. package/dist/cli/__tests__/update.test.js.map +1 -1
  106. package/dist/cli/__tests__/url.test.js.map +1 -1
  107. package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
  108. package/dist/cli/adapt.js.map +1 -1
  109. package/dist/cli/agents-init.js.map +1 -1
  110. package/dist/cli/agents.js.map +1 -1
  111. package/dist/cli/api.js.map +1 -1
  112. package/dist/cli/ask.d.ts +1 -1
  113. package/dist/cli/ask.d.ts.map +1 -1
  114. package/dist/cli/ask.js.map +1 -1
  115. package/dist/cli/auth.js.map +1 -1
  116. package/dist/cli/autoresearch-guided.js.map +1 -1
  117. package/dist/cli/autoresearch-intake.js.map +1 -1
  118. package/dist/cli/capabilities.js.map +1 -1
  119. package/dist/cli/cleanup.js.map +1 -1
  120. package/dist/cli/codex-feature-probe.js.map +1 -1
  121. package/dist/cli/codex-home.js.map +1 -1
  122. package/dist/cli/doctor.d.ts +18 -0
  123. package/dist/cli/doctor.d.ts.map +1 -1
  124. package/dist/cli/doctor.js +594 -130
  125. package/dist/cli/doctor.js.map +1 -1
  126. package/dist/cli/explore.js.map +1 -1
  127. package/dist/cli/index.d.ts +11 -3
  128. package/dist/cli/index.d.ts.map +1 -1
  129. package/dist/cli/index.js +108 -20
  130. package/dist/cli/index.js.map +1 -1
  131. package/dist/cli/mcp-serve.js.map +1 -1
  132. package/dist/cli/mission.js.map +1 -1
  133. package/dist/cli/native-assets.js.map +1 -1
  134. package/dist/cli/native-hook-claim-journal.d.ts +18 -0
  135. package/dist/cli/native-hook-claim-journal.d.ts.map +1 -0
  136. package/dist/cli/native-hook-claim-journal.js +239 -0
  137. package/dist/cli/native-hook-claim-journal.js.map +1 -0
  138. package/dist/cli/omx.js.map +1 -1
  139. package/dist/cli/plugin-marketplace.js.map +1 -1
  140. package/dist/cli/project-runtime-codex-homes.js.map +1 -1
  141. package/dist/cli/question.js.map +1 -1
  142. package/dist/cli/ralph.d.ts.map +1 -1
  143. package/dist/cli/ralph.js +3 -2
  144. package/dist/cli/ralph.js.map +1 -1
  145. package/dist/cli/ralplan.d.ts +14 -0
  146. package/dist/cli/ralplan.d.ts.map +1 -0
  147. package/dist/cli/ralplan.js +203 -0
  148. package/dist/cli/ralplan.js.map +1 -0
  149. package/dist/cli/setup-preferences.d.ts +11 -0
  150. package/dist/cli/setup-preferences.d.ts.map +1 -1
  151. package/dist/cli/setup-preferences.js +32 -0
  152. package/dist/cli/setup-preferences.js.map +1 -1
  153. package/dist/cli/setup.d.ts +53 -0
  154. package/dist/cli/setup.d.ts.map +1 -1
  155. package/dist/cli/setup.js +1401 -398
  156. package/dist/cli/setup.js.map +1 -1
  157. package/dist/cli/sparkshell.js.map +1 -1
  158. package/dist/cli/star-prompt.js.map +1 -1
  159. package/dist/cli/state.d.ts.map +1 -1
  160. package/dist/cli/state.js +2 -1
  161. package/dist/cli/state.js.map +1 -1
  162. package/dist/cli/team.js.map +1 -1
  163. package/dist/cli/ultragoal.d.ts +5 -1
  164. package/dist/cli/ultragoal.d.ts.map +1 -1
  165. package/dist/cli/ultragoal.js +34 -2
  166. package/dist/cli/ultragoal.js.map +1 -1
  167. package/dist/cli/uninstall.d.ts +8 -0
  168. package/dist/cli/uninstall.d.ts.map +1 -1
  169. package/dist/cli/uninstall.js +944 -95
  170. package/dist/cli/uninstall.js.map +1 -1
  171. package/dist/cli/update.d.ts.map +1 -1
  172. package/dist/cli/update.js +8 -1
  173. package/dist/cli/update.js.map +1 -1
  174. package/dist/cli/url.js.map +1 -1
  175. package/dist/compat/__tests__/doctor-contract.test.js +5 -1
  176. package/dist/compat/__tests__/doctor-contract.test.js.map +1 -1
  177. package/dist/compat/__tests__/rust-runtime-compat.test.js.map +1 -1
  178. package/dist/config/__tests__/codex-hooks.test.js +1287 -11
  179. package/dist/config/__tests__/codex-hooks.test.js.map +1 -1
  180. package/dist/config/__tests__/generator-idempotent.test.js +850 -32
  181. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  182. package/dist/config/__tests__/generator-notify.test.js +6 -1
  183. package/dist/config/__tests__/generator-notify.test.js.map +1 -1
  184. package/dist/config/codex-feature-flags.d.ts.map +1 -1
  185. package/dist/config/codex-feature-flags.js.map +1 -1
  186. package/dist/config/codex-hooks.d.ts +124 -1
  187. package/dist/config/codex-hooks.d.ts.map +1 -1
  188. package/dist/config/codex-hooks.js +2142 -265
  189. package/dist/config/codex-hooks.js.map +1 -1
  190. package/dist/config/commit-lore-guard.js.map +1 -1
  191. package/dist/config/generator.d.ts +21 -16
  192. package/dist/config/generator.d.ts.map +1 -1
  193. package/dist/config/generator.js +991 -137
  194. package/dist/config/generator.js.map +1 -1
  195. package/dist/config/mcp-registry.js.map +1 -1
  196. package/dist/config/models.d.ts +2 -2
  197. package/dist/config/models.d.ts.map +1 -1
  198. package/dist/config/models.js.map +1 -1
  199. package/dist/config/omx-first-party-mcp.js.map +1 -1
  200. package/dist/config/team-mode.d.ts.map +1 -1
  201. package/dist/document-refresh/__tests__/enforcer.test.js.map +1 -1
  202. package/dist/document-refresh/enforcer.js.map +1 -1
  203. package/dist/exec/followup.js.map +1 -1
  204. package/dist/goal-workflows/artifacts.js.map +1 -1
  205. package/dist/hooks/__tests__/agents-overlay.test.js +7 -4
  206. package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
  207. package/dist/hooks/__tests__/analyze-routing-contract.test.js.map +1 -1
  208. package/dist/hooks/__tests__/analyze-skill-contract.test.js.map +1 -1
  209. package/dist/hooks/__tests__/anti-slop-workflow.test.js.map +1 -1
  210. package/dist/hooks/__tests__/autopilot-skill-contract.test.js +9 -3
  211. package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
  212. package/dist/hooks/__tests__/code-review-skill-contract.test.js.map +1 -1
  213. package/dist/hooks/__tests__/consensus-execution-handoff.test.js.map +1 -1
  214. package/dist/hooks/__tests__/deep-interview-contract.test.js.map +1 -1
  215. package/dist/hooks/__tests__/design-skill.test.js.map +1 -1
  216. package/dist/hooks/__tests__/keyword-detector.test.js +1372 -2
  217. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  218. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +14 -0
  219. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  220. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
  221. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +669 -1
  222. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
  223. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js +2 -0
  224. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js.map +1 -1
  225. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js +142 -1
  226. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js.map +1 -1
  227. package/dist/hooks/__tests__/notify-hook-modules.test.js.map +1 -1
  228. package/dist/hooks/__tests__/notify-hook-non-omx-guard.test.js +48 -2
  229. package/dist/hooks/__tests__/notify-hook-non-omx-guard.test.js.map +1 -1
  230. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js +61 -44
  231. package/dist/hooks/__tests__/notify-hook-ralph-resume.test.js.map +1 -1
  232. package/dist/hooks/__tests__/notify-hook-regression-205.test.js.map +1 -1
  233. package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.js +3 -2
  234. package/dist/hooks/__tests__/notify-hook-session-idle-dedupe.test.js.map +1 -1
  235. package/dist/hooks/__tests__/notify-hook-session-scope.test.js +243 -4
  236. package/dist/hooks/__tests__/notify-hook-session-scope.test.js.map +1 -1
  237. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
  238. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +8 -1
  239. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  240. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
  241. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
  242. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js.map +1 -1
  243. package/dist/hooks/__tests__/notify-hook-visual-verdict.test.js.map +1 -1
  244. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
  245. package/dist/hooks/__tests__/openclaw-setup-contract.test.js.map +1 -1
  246. package/dist/hooks/__tests__/pre-context-gate-skills.test.js.map +1 -1
  247. package/dist/hooks/__tests__/prometheus-strict-contract.test.js.map +1 -1
  248. package/dist/hooks/__tests__/prompt-guidance-fragments.test.js.map +1 -1
  249. package/dist/hooks/__tests__/prompt-guidance-test-helpers.js.map +1 -1
  250. package/dist/hooks/__tests__/prompt-session-provenance.test.d.ts +2 -0
  251. package/dist/hooks/__tests__/prompt-session-provenance.test.d.ts.map +1 -0
  252. package/dist/hooks/__tests__/prompt-session-provenance.test.js +82 -0
  253. package/dist/hooks/__tests__/prompt-session-provenance.test.js.map +1 -0
  254. package/dist/hooks/__tests__/session.test.js +532 -11
  255. package/dist/hooks/__tests__/session.test.js.map +1 -1
  256. package/dist/hooks/__tests__/skill-catalog-hygiene.test.js.map +1 -1
  257. package/dist/hooks/__tests__/team-runtime-gating-docs-contract.test.js.map +1 -1
  258. package/dist/hooks/__tests__/visual-ralph-skill.test.js.map +1 -1
  259. package/dist/hooks/__tests__/visual-verdict-loop.test.js.map +1 -1
  260. package/dist/hooks/agents-overlay.js +5 -5
  261. package/dist/hooks/agents-overlay.js.map +1 -1
  262. package/dist/hooks/code-simplifier/index.js.map +1 -1
  263. package/dist/hooks/explore-routing.js.map +1 -1
  264. package/dist/hooks/extensibility/dispatcher.js.map +1 -1
  265. package/dist/hooks/extensibility/events.js.map +1 -1
  266. package/dist/hooks/extensibility/loader.js.map +1 -1
  267. package/dist/hooks/extensibility/plugin-runner-stdin.js.map +1 -1
  268. package/dist/hooks/extensibility/sdk/logging.js.map +1 -1
  269. package/dist/hooks/keyword-detector.d.ts +33 -7
  270. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  271. package/dist/hooks/keyword-detector.js +2228 -119
  272. package/dist/hooks/keyword-detector.js.map +1 -1
  273. package/dist/hooks/keyword-registry.d.ts +12 -0
  274. package/dist/hooks/keyword-registry.d.ts.map +1 -1
  275. package/dist/hooks/keyword-registry.js +41 -0
  276. package/dist/hooks/keyword-registry.js.map +1 -1
  277. package/dist/hooks/prompt-session-provenance.d.ts +87 -0
  278. package/dist/hooks/prompt-session-provenance.d.ts.map +1 -0
  279. package/dist/hooks/prompt-session-provenance.js +180 -0
  280. package/dist/hooks/prompt-session-provenance.js.map +1 -0
  281. package/dist/hooks/session.d.ts +139 -34
  282. package/dist/hooks/session.d.ts.map +1 -1
  283. package/dist/hooks/session.js +1239 -317
  284. package/dist/hooks/session.js.map +1 -1
  285. package/dist/hooks/task-size-detector.js.map +1 -1
  286. package/dist/hud/__tests__/state.test.js.map +1 -1
  287. package/dist/hud/authority.js.map +1 -1
  288. package/dist/hud/constants.js.map +1 -1
  289. package/dist/hud/index.js.map +1 -1
  290. package/dist/hud/reconcile.js.map +1 -1
  291. package/dist/hud/render.js.map +1 -1
  292. package/dist/hud/state.js.map +1 -1
  293. package/dist/hud/tmux.js.map +1 -1
  294. package/dist/leader/__tests__/contract.test.js +103 -1
  295. package/dist/leader/__tests__/contract.test.js.map +1 -1
  296. package/dist/leader/contract.d.ts +26 -5
  297. package/dist/leader/contract.d.ts.map +1 -1
  298. package/dist/leader/contract.js +109 -3
  299. package/dist/leader/contract.js.map +1 -1
  300. package/dist/mcp/__tests__/bootstrap.test.js.map +1 -1
  301. package/dist/mcp/__tests__/server-lifecycle.test.js +15 -2
  302. package/dist/mcp/__tests__/server-lifecycle.test.js.map +1 -1
  303. package/dist/mcp/__tests__/state-paths.test.js +98 -1
  304. package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
  305. package/dist/mcp/bootstrap.js.map +1 -1
  306. package/dist/mcp/code-intel-server.d.ts +58 -58
  307. package/dist/mcp/code-intel-server.d.ts.map +1 -1
  308. package/dist/mcp/code-intel-server.js.map +1 -1
  309. package/dist/mcp/hermes-bridge.js.map +1 -1
  310. package/dist/mcp/hermes-server.d.ts +70 -70
  311. package/dist/mcp/hermes-server.d.ts.map +1 -1
  312. package/dist/mcp/lifecycle-telemetry.js.map +1 -1
  313. package/dist/mcp/memory-server.d.ts +34 -34
  314. package/dist/mcp/memory-server.d.ts.map +1 -1
  315. package/dist/mcp/memory-server.js.map +1 -1
  316. package/dist/mcp/state-paths.d.ts +14 -0
  317. package/dist/mcp/state-paths.d.ts.map +1 -1
  318. package/dist/mcp/state-paths.js +96 -20
  319. package/dist/mcp/state-paths.js.map +1 -1
  320. package/dist/mcp/state-server.d.ts +47 -47
  321. package/dist/mcp/state-server.d.ts.map +1 -1
  322. package/dist/mcp/trace-server.d.ts +4 -4
  323. package/dist/mcp/trace-server.d.ts.map +1 -1
  324. package/dist/mcp/trace-server.js.map +1 -1
  325. package/dist/mcp/wiki-server.d.ts +23 -23
  326. package/dist/mcp/wiki-server.d.ts.map +1 -1
  327. package/dist/mcp/wiki-server.js.map +1 -1
  328. package/dist/modes/__tests__/base-session-scope.test.js +53 -1
  329. package/dist/modes/__tests__/base-session-scope.test.js.map +1 -1
  330. package/dist/modes/base.d.ts.map +1 -1
  331. package/dist/modes/base.js +5 -5
  332. package/dist/modes/base.js.map +1 -1
  333. package/dist/notifications/__tests__/lifecycle-dedupe.test.js.map +1 -1
  334. package/dist/notifications/__tests__/reply-listener.test.js.map +1 -1
  335. package/dist/notifications/__tests__/session-idle-tail-dedupe.test.js +177 -85
  336. package/dist/notifications/__tests__/session-idle-tail-dedupe.test.js.map +1 -1
  337. package/dist/notifications/__tests__/session-registry.test.js.map +1 -1
  338. package/dist/notifications/__tests__/template-engine.test.js.map +1 -1
  339. package/dist/notifications/__tests__/verbosity.test.js.map +1 -1
  340. package/dist/notifications/http-client.js.map +1 -1
  341. package/dist/notifications/index.d.ts +4 -1
  342. package/dist/notifications/index.d.ts.map +1 -1
  343. package/dist/notifications/index.js +3 -9
  344. package/dist/notifications/index.js.map +1 -1
  345. package/dist/notifications/lifecycle-dedupe.js.map +1 -1
  346. package/dist/notifications/reply-listener.d.ts.map +1 -1
  347. package/dist/notifications/reply-listener.js.map +1 -1
  348. package/dist/notifications/session-registry.js.map +1 -1
  349. package/dist/notifications/session-status.js.map +1 -1
  350. package/dist/notifications/temp-contract.js.map +1 -1
  351. package/dist/notifications/tmux-detector.js.map +1 -1
  352. package/dist/notifications/tmux.js.map +1 -1
  353. package/dist/openclaw/config.js.map +1 -1
  354. package/dist/openclaw/dispatcher.js.map +1 -1
  355. package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
  356. package/dist/pipeline/__tests__/stages.test.js +8 -5
  357. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  358. package/dist/pipeline/stages/code-review.js.map +1 -1
  359. package/dist/pipeline/stages/ralph-verify.js.map +1 -1
  360. package/dist/pipeline/stages/ralplan.js.map +1 -1
  361. package/dist/pipeline/stages/team-exec.js.map +1 -1
  362. package/dist/pipeline/stages/ultragoal.js.map +1 -1
  363. package/dist/pipeline/stages/ultraqa.js.map +1 -1
  364. package/dist/planning/__tests__/approved-execution-lifecycle-matrix.test.js.map +1 -1
  365. package/dist/planning/artifact-names.js.map +1 -1
  366. package/dist/planning/artifacts.js.map +1 -1
  367. package/dist/question/__tests__/state.test.js.map +1 -1
  368. package/dist/question/__tests__/ui.test.js.map +1 -1
  369. package/dist/question/autopilot-wait.js.map +1 -1
  370. package/dist/question/client.d.ts.map +1 -1
  371. package/dist/question/client.js.map +1 -1
  372. package/dist/question/deep-interview.js.map +1 -1
  373. package/dist/question/events.js.map +1 -1
  374. package/dist/question/renderer.js.map +1 -1
  375. package/dist/question/state.d.ts.map +1 -1
  376. package/dist/question/state.js.map +1 -1
  377. package/dist/question/ui.js.map +1 -1
  378. package/dist/ralph/contract.d.ts +1 -1
  379. package/dist/ralph/contract.d.ts.map +1 -1
  380. package/dist/ralplan/__tests__/consensus-gate.test.js +403 -8
  381. package/dist/ralplan/__tests__/consensus-gate.test.js.map +1 -1
  382. package/dist/ralplan/__tests__/runtime.test.js +112 -14
  383. package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
  384. package/dist/ralplan/consensus-gate.d.ts +3 -3
  385. package/dist/ralplan/consensus-gate.d.ts.map +1 -1
  386. package/dist/ralplan/consensus-gate.js +155 -60
  387. package/dist/ralplan/consensus-gate.js.map +1 -1
  388. package/dist/ralplan/runtime.d.ts +2 -2
  389. package/dist/ralplan/runtime.d.ts.map +1 -1
  390. package/dist/ralplan/runtime.js +1 -2
  391. package/dist/ralplan/runtime.js.map +1 -1
  392. package/dist/runtime/bridge.d.ts.map +1 -1
  393. package/dist/runtime/bridge.js.map +1 -1
  394. package/dist/runtime/process-tree.js.map +1 -1
  395. package/dist/runtime/run-loop.js.map +1 -1
  396. package/dist/runtime/run-outcome.d.ts +3 -3
  397. package/dist/runtime/run-outcome.d.ts.map +1 -1
  398. package/dist/runtime/run-outcome.js.map +1 -1
  399. package/dist/runtime/run-state.js.map +1 -1
  400. package/dist/scripts/__tests__/codex-native-hook.test.js +4121 -592
  401. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  402. package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.d.ts +2 -0
  403. package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.d.ts.map +1 -0
  404. package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.js +634 -0
  405. package/dist/scripts/__tests__/fixtures/issue-3138-installed-smoke.js.map +1 -0
  406. package/dist/scripts/__tests__/generate-release-body.test.js.map +1 -1
  407. package/dist/scripts/__tests__/hook-derived-watcher.test.js.map +1 -1
  408. package/dist/scripts/__tests__/notify-dispatcher.test.js.map +1 -1
  409. package/dist/scripts/__tests__/notify-fallback-delivery.test.d.ts +2 -0
  410. package/dist/scripts/__tests__/notify-fallback-delivery.test.d.ts.map +1 -0
  411. package/dist/scripts/__tests__/notify-fallback-delivery.test.js +174 -0
  412. package/dist/scripts/__tests__/notify-fallback-delivery.test.js.map +1 -0
  413. package/dist/scripts/__tests__/notify-state-io.test.js +57 -1
  414. package/dist/scripts/__tests__/notify-state-io.test.js.map +1 -1
  415. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.d.ts +2 -0
  416. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.d.ts.map +1 -0
  417. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js +205 -0
  418. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js.map +1 -0
  419. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.d.ts +2 -0
  420. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.d.ts.map +1 -0
  421. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js +175 -0
  422. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js.map +1 -0
  423. package/dist/scripts/__tests__/run-test-files.test.js.map +1 -1
  424. package/dist/scripts/__tests__/smoke-packed-install.test.js +848 -4
  425. package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
  426. package/dist/scripts/__tests__/test-reply-listener-live.test.js.map +1 -1
  427. package/dist/scripts/build-api.js.map +1 -1
  428. package/dist/scripts/build-explore-harness.js.map +1 -1
  429. package/dist/scripts/build-sparkshell.js.map +1 -1
  430. package/dist/scripts/cleanup-explore-harness.js.map +1 -1
  431. package/dist/scripts/codex-execution-surface.js.map +1 -1
  432. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  433. package/dist/scripts/codex-native-hook.js +689 -156
  434. package/dist/scripts/codex-native-hook.js.map +1 -1
  435. package/dist/scripts/generate-native-release-manifest.js.map +1 -1
  436. package/dist/scripts/generate-release-body.js.map +1 -1
  437. package/dist/scripts/hook-payload-guard.d.ts.map +1 -1
  438. package/dist/scripts/hook-payload-guard.js.map +1 -1
  439. package/dist/scripts/notify-fallback-delivery.d.ts +48 -0
  440. package/dist/scripts/notify-fallback-delivery.d.ts.map +1 -0
  441. package/dist/scripts/notify-fallback-delivery.js +419 -0
  442. package/dist/scripts/notify-fallback-delivery.js.map +1 -0
  443. package/dist/scripts/notify-fallback-watcher.js +136 -30
  444. package/dist/scripts/notify-fallback-watcher.js.map +1 -1
  445. package/dist/scripts/notify-hook/auto-nudge.d.ts +18 -9
  446. package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
  447. package/dist/scripts/notify-hook/auto-nudge.js +39 -13
  448. package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
  449. package/dist/scripts/notify-hook/managed-tmux.d.ts +23 -0
  450. package/dist/scripts/notify-hook/managed-tmux.d.ts.map +1 -1
  451. package/dist/scripts/notify-hook/managed-tmux.js +122 -12
  452. package/dist/scripts/notify-hook/managed-tmux.js.map +1 -1
  453. package/dist/scripts/notify-hook/orchestration-intent.d.ts +7 -7
  454. package/dist/scripts/notify-hook/orchestration-intent.d.ts.map +1 -1
  455. package/dist/scripts/notify-hook/ralph-session-resume.d.ts +5 -2
  456. package/dist/scripts/notify-hook/ralph-session-resume.d.ts.map +1 -1
  457. package/dist/scripts/notify-hook/ralph-session-resume.js +33 -37
  458. package/dist/scripts/notify-hook/ralph-session-resume.js.map +1 -1
  459. package/dist/scripts/notify-hook/state-io.d.ts +17 -0
  460. package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
  461. package/dist/scripts/notify-hook/state-io.js +73 -11
  462. package/dist/scripts/notify-hook/state-io.js.map +1 -1
  463. package/dist/scripts/notify-hook/team-dispatch.d.ts +1 -1
  464. package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
  465. package/dist/scripts/notify-hook/team-leader-nudge.d.ts +2 -2
  466. package/dist/scripts/notify-hook/team-tmux-guard.d.ts +4 -4
  467. package/dist/scripts/notify-hook/team-worker-posttooluse.js.map +1 -1
  468. package/dist/scripts/notify-hook/team-worker-stop.d.ts +1 -1
  469. package/dist/scripts/notify-hook/team-worker.d.ts +2 -2
  470. package/dist/scripts/notify-hook/tmux-injection.d.ts +8 -1
  471. package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
  472. package/dist/scripts/notify-hook/tmux-injection.js +25 -16
  473. package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
  474. package/dist/scripts/notify-hook/visual-verdict.d.ts +1 -1
  475. package/dist/scripts/notify-hook/visual-verdict.d.ts.map +1 -1
  476. package/dist/scripts/notify-hook/visual-verdict.js +15 -13
  477. package/dist/scripts/notify-hook/visual-verdict.js.map +1 -1
  478. package/dist/scripts/notify-hook.d.ts +24 -1
  479. package/dist/scripts/notify-hook.d.ts.map +1 -1
  480. package/dist/scripts/notify-hook.js +266 -90
  481. package/dist/scripts/notify-hook.js.map +1 -1
  482. package/dist/scripts/postinstall.js.map +1 -1
  483. package/dist/scripts/prompt-inventory.js.map +1 -1
  484. package/dist/scripts/smoke-packed-install.d.ts +879 -2
  485. package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
  486. package/dist/scripts/smoke-packed-install.js +1528 -7
  487. package/dist/scripts/smoke-packed-install.js.map +1 -1
  488. package/dist/scripts/sync-plugin-mirror.js.map +1 -1
  489. package/dist/scripts/test-reply-listener-live.js.map +1 -1
  490. package/dist/scripts/test-sparkshell.js.map +1 -1
  491. package/dist/scripts/verify-native-agents.js.map +1 -1
  492. package/dist/session-history/friction.js.map +1 -1
  493. package/dist/session-history/search.js.map +1 -1
  494. package/dist/sidecar/__tests__/boundary.test.js.map +1 -1
  495. package/dist/sidecar/collector.js.map +1 -1
  496. package/dist/sidecar/index.js.map +1 -1
  497. package/dist/sidecar/render.js.map +1 -1
  498. package/dist/sidecar/tmux.js.map +1 -1
  499. package/dist/state/__tests__/operations.test.js +64 -15
  500. package/dist/state/__tests__/operations.test.js.map +1 -1
  501. package/dist/state/__tests__/skill-active.test.js +19 -1
  502. package/dist/state/__tests__/skill-active.test.js.map +1 -1
  503. package/dist/state/__tests__/workflow-transition.test.js +61 -0
  504. package/dist/state/__tests__/workflow-transition.test.js.map +1 -1
  505. package/dist/state/mode-state-context.js.map +1 -1
  506. package/dist/state/operations.d.ts +1 -1
  507. package/dist/state/operations.d.ts.map +1 -1
  508. package/dist/state/operations.js +49 -55
  509. package/dist/state/operations.js.map +1 -1
  510. package/dist/state/paths.d.ts +1 -1
  511. package/dist/state/paths.d.ts.map +1 -1
  512. package/dist/state/paths.js +1 -1
  513. package/dist/state/paths.js.map +1 -1
  514. package/dist/state/skill-active.d.ts +13 -1
  515. package/dist/state/skill-active.d.ts.map +1 -1
  516. package/dist/state/skill-active.js +43 -4
  517. package/dist/state/skill-active.js.map +1 -1
  518. package/dist/state/terminal-normalization.js.map +1 -1
  519. package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
  520. package/dist/state/workflow-transition-reconcile.js +20 -4
  521. package/dist/state/workflow-transition-reconcile.js.map +1 -1
  522. package/dist/state/workflow-transition.d.ts +1 -1
  523. package/dist/state/workflow-transition.d.ts.map +1 -1
  524. package/dist/state/workflow-transition.js.map +1 -1
  525. package/dist/subagents/__tests__/adapted-role-binding.test.d.ts +2 -0
  526. package/dist/subagents/__tests__/adapted-role-binding.test.d.ts.map +1 -0
  527. package/dist/subagents/__tests__/adapted-role-binding.test.js +988 -0
  528. package/dist/subagents/__tests__/adapted-role-binding.test.js.map +1 -0
  529. package/dist/subagents/__tests__/leader-bootstrap-3181.test.d.ts +2 -0
  530. package/dist/subagents/__tests__/leader-bootstrap-3181.test.d.ts.map +1 -0
  531. package/dist/subagents/__tests__/leader-bootstrap-3181.test.js +273 -0
  532. package/dist/subagents/__tests__/leader-bootstrap-3181.test.js.map +1 -0
  533. package/dist/subagents/__tests__/tracker.test.js +1550 -10
  534. package/dist/subagents/__tests__/tracker.test.js.map +1 -1
  535. package/dist/subagents/adapted-role-binding.d.ts +17 -0
  536. package/dist/subagents/adapted-role-binding.d.ts.map +1 -0
  537. package/dist/subagents/adapted-role-binding.js +75 -0
  538. package/dist/subagents/adapted-role-binding.js.map +1 -0
  539. package/dist/subagents/role-routing-marker.d.ts +10 -0
  540. package/dist/subagents/role-routing-marker.d.ts.map +1 -0
  541. package/dist/subagents/role-routing-marker.js +136 -0
  542. package/dist/subagents/role-routing-marker.js.map +1 -0
  543. package/dist/subagents/tracker.d.ts +172 -0
  544. package/dist/subagents/tracker.d.ts.map +1 -1
  545. package/dist/subagents/tracker.js +1077 -61
  546. package/dist/subagents/tracker.js.map +1 -1
  547. package/dist/team/__tests__/approved-execution.test.js.map +1 -1
  548. package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
  549. package/dist/team/__tests__/model-contract.test.js +231 -1
  550. package/dist/team/__tests__/model-contract.test.js.map +1 -1
  551. package/dist/team/__tests__/role-router.test.js.map +1 -1
  552. package/dist/team/__tests__/runtime.test.js +401 -1
  553. package/dist/team/__tests__/runtime.test.js.map +1 -1
  554. package/dist/team/__tests__/scaling.test.js +181 -0
  555. package/dist/team/__tests__/scaling.test.js.map +1 -1
  556. package/dist/team/__tests__/state-root.test.js.map +1 -1
  557. package/dist/team/__tests__/tmux-session.test.js +434 -4
  558. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  559. package/dist/team/__tests__/tmux-test-fixture.js.map +1 -1
  560. package/dist/team/api-interop.d.ts +2 -2
  561. package/dist/team/api-interop.d.ts.map +1 -1
  562. package/dist/team/api-interop.js.map +1 -1
  563. package/dist/team/commit-hygiene.js.map +1 -1
  564. package/dist/team/contracts.d.ts +5 -5
  565. package/dist/team/contracts.d.ts.map +1 -1
  566. package/dist/team/delivery-log.js.map +1 -1
  567. package/dist/team/followup-planner.js.map +1 -1
  568. package/dist/team/goal-workflow.js.map +1 -1
  569. package/dist/team/idle-nudge.d.ts.map +1 -1
  570. package/dist/team/model-contract.d.ts +20 -1
  571. package/dist/team/model-contract.d.ts.map +1 -1
  572. package/dist/team/model-contract.js +267 -30
  573. package/dist/team/model-contract.js.map +1 -1
  574. package/dist/team/orchestrator.js.map +1 -1
  575. package/dist/team/pane-status.js.map +1 -1
  576. package/dist/team/phase-controller.js.map +1 -1
  577. package/dist/team/reminder-intents.d.ts +1 -1
  578. package/dist/team/reminder-intents.d.ts.map +1 -1
  579. package/dist/team/reminder-intents.js.map +1 -1
  580. package/dist/team/runtime-cli.js.map +1 -1
  581. package/dist/team/runtime.d.ts.map +1 -1
  582. package/dist/team/runtime.js +83 -52
  583. package/dist/team/runtime.js.map +1 -1
  584. package/dist/team/scaling.d.ts +9 -7
  585. package/dist/team/scaling.d.ts.map +1 -1
  586. package/dist/team/scaling.js +69 -28
  587. package/dist/team/scaling.js.map +1 -1
  588. package/dist/team/state/dispatch-lock.js.map +1 -1
  589. package/dist/team/state/dispatch.js.map +1 -1
  590. package/dist/team/state/events.js.map +1 -1
  591. package/dist/team/state/tasks.js.map +1 -1
  592. package/dist/team/state-root.js.map +1 -1
  593. package/dist/team/state.js.map +1 -1
  594. package/dist/team/team-identity.d.ts.map +1 -1
  595. package/dist/team/team-identity.js.map +1 -1
  596. package/dist/team/tmux-session.d.ts +2 -0
  597. package/dist/team/tmux-session.d.ts.map +1 -1
  598. package/dist/team/tmux-session.js +90 -42
  599. package/dist/team/tmux-session.js.map +1 -1
  600. package/dist/team/worker-bootstrap.js.map +1 -1
  601. package/dist/team/worktree.js.map +1 -1
  602. package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
  603. package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
  604. package/dist/ultragoal/artifacts.d.ts.map +1 -1
  605. package/dist/ultragoal/artifacts.js +4 -1
  606. package/dist/ultragoal/artifacts.js.map +1 -1
  607. package/dist/url-reader/index.js.map +1 -1
  608. package/dist/utils/agents-model-table.js.map +1 -1
  609. package/dist/utils/package.js.map +1 -1
  610. package/dist/utils/paths.js.map +1 -1
  611. package/dist/utils/platform-command.js.map +1 -1
  612. package/dist/utils/repo-deps.js.map +1 -1
  613. package/dist/utils/version.js.map +1 -1
  614. package/dist/verification/__tests__/ci-rust-gates.test.js +1 -1
  615. package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js.map +1 -1
  616. package/dist/verification/verifier.js.map +1 -1
  617. package/dist/visual/constants.d.ts +1 -1
  618. package/dist/visual/constants.d.ts.map +1 -1
  619. package/dist/vscode/__tests__/index.test.js.map +1 -1
  620. package/dist/vscode/index.js.map +1 -1
  621. package/dist/wiki/__tests__/cjk-tokenize.test.js.map +1 -1
  622. package/dist/wiki/__tests__/lint.test.js.map +1 -1
  623. package/dist/wiki/__tests__/query.test.js.map +1 -1
  624. package/dist/wiki/__tests__/storage.test.js.map +1 -1
  625. package/dist/wiki/__tests__/test-helpers.d.ts.map +1 -1
  626. package/dist/wiki/lint.js.map +1 -1
  627. package/dist/wiki/query.js.map +1 -1
  628. package/dist/wiki/storage.js.map +1 -1
  629. package/package.json +2 -2
  630. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  631. package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +10 -5
  632. package/plugins/oh-my-codex/skills/ralph/SKILL.md +4 -2
  633. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +5 -2
  634. package/skills/omx-setup/SKILL.md +10 -5
  635. package/skills/ralph/SKILL.md +4 -2
  636. package/skills/ralplan/SKILL.md +5 -2
  637. package/src/scripts/__tests__/codex-native-hook.test.ts +17522 -10630
  638. package/src/scripts/__tests__/fixtures/issue-3138-installed-smoke.ts +734 -0
  639. package/src/scripts/__tests__/notify-fallback-delivery.test.ts +177 -0
  640. package/src/scripts/__tests__/notify-state-io.test.ts +73 -0
  641. package/src/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.ts +215 -0
  642. package/src/scripts/__tests__/role-intent-durable-recovery-3181.test.ts +193 -0
  643. package/src/scripts/__tests__/smoke-packed-install.test.ts +991 -4
  644. package/src/scripts/codex-native-hook.ts +779 -159
  645. package/src/scripts/notify-fallback-delivery.ts +416 -0
  646. package/src/scripts/notify-fallback-watcher.ts +132 -40
  647. package/src/scripts/notify-hook/auto-nudge.ts +43 -12
  648. package/src/scripts/notify-hook/managed-tmux.ts +149 -12
  649. package/src/scripts/notify-hook/ralph-session-resume.ts +37 -36
  650. package/src/scripts/notify-hook/state-io.ts +100 -13
  651. package/src/scripts/notify-hook/tmux-injection.ts +38 -21
  652. package/src/scripts/notify-hook/visual-verdict.ts +22 -12
  653. package/src/scripts/notify-hook.ts +335 -90
  654. package/src/scripts/smoke-packed-install.ts +1829 -6
@@ -1,202 +1,249 @@
1
1
  /**
2
- * Session Lifecycle Manager for oh-my-codex
2
+ * Session lifecycle management.
3
3
  *
4
- * Tracks session start/end, detects stale sessions from crashed launches,
5
- * and provides structured logging for session events.
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, writeFile, mkdir, unlink, appendFile, rm } from 'fs/promises';
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 { existsSync, readFileSync } from 'fs';
10
- import { omxRoot, omxStateDir, omxLogsDir, sameFilePath } from '../utils/paths.js';
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
- // No age-based threshold: staleness is determined by PID liveness/identity.
16
- // Long-running sessions (>2h) are legitimate and should not be reaped.
17
- function sessionPath(cwd) {
18
- return join(omxStateDir(cwd), SESSION_FILE);
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
- * Reset session-scoped HUD/metrics files at launch so stale values do not leak
42
- * into a new Codex session.
23
+ * Convert arbitrary input into a valid session ID without exposing validator
24
+ * exceptions to lifecycle or hook inputs.
43
25
  */
44
- export async function resetSessionMetrics(cwd, sessionId) {
45
- const omxDir = omxRoot(cwd);
46
- const stateDir = omxStateDir(cwd);
47
- await mkdir(omxDir, { recursive: true });
48
- await mkdir(stateDir, { recursive: true });
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
- * Read current session state. Returns null if no session file exists.
71
- */
72
- export async function readSessionState(cwd) {
73
- const path = sessionPath(cwd);
74
- if (!existsSync(path))
75
- return null;
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
- const content = await readFile(path, 'utf-8');
78
- return JSON.parse(content);
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 null;
56
+ return 'unresolved';
82
57
  }
83
58
  }
84
- export function isSessionStateAuthoritativeForCwd(state, cwd) {
85
- if (!SESSION_ID_PATTERN.test(state.session_id))
86
- return false;
87
- if (typeof state.cwd === 'string' && state.cwd.trim() !== '') {
88
- return sameFilePath(state.cwd, cwd);
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 isSessionStateUsable(state, cwd, options = {}) {
93
- if (!isSessionStateAuthoritativeForCwd(state, cwd))
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 true;
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
- export async function readUsableSessionState(cwd, options = {}) {
104
- const state = await readSessionState(cwd);
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 defaultIsPidAlive(pid) {
110
- try {
111
- process.kill(pid, 0);
112
- return true;
113
- }
114
- catch {
115
- return false;
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 remainder = statContent.slice(commandEnd + 1).trim();
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.isFinite(startTicks) ? startTicks : null;
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.length > 0 ? normalized : null;
215
+ return normalized || null;
134
216
  }
135
217
  function readLinuxProcessIdentity(pid) {
136
218
  try {
137
- const stat = readFileSync(`/proc/${pid}/stat`, 'utf-8');
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 = null;
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 createSessionState(cwd, sessionId, pid, platform, linuxIdentity, options = {}) {
160
- const nowIso = options.nowIso ?? new Date().toISOString();
161
- const nativeSessionId = typeof options.nativeSessionId === 'string' && options.nativeSessionId.trim()
162
- ? options.nativeSessionId.trim()
163
- : undefined;
164
- const tmuxSessionName = typeof options.tmuxSessionName === 'string' && options.tmuxSessionName.trim()
165
- ? options.tmuxSessionName.trim()
166
- : undefined;
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
- * Check if a session is stale.
197
- * - If the owning PID is dead, it is stale.
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 readIdentity = options.readLinuxIdentity ?? readLinuxProcessIdentity;
211
- const liveIdentity = readIdentity(state.pid);
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
- const liveCmdline = normalizeCmdline(liveIdentity.cmdline);
221
- if (!liveCmdline || liveCmdline !== expectedCmdline)
222
- return true;
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
- * Write session start state.
228
- */
229
- export async function writeSessionStart(cwd, sessionId, options = {}) {
230
- const stateDir = omxStateDir(cwd);
231
- await mkdir(stateDir, { recursive: true });
232
- const existing = await readSessionState(cwd);
233
- const sameSession = existing?.session_id === sessionId;
234
- const pid = Number.isInteger(options.pid) && options.pid && options.pid > 0
235
- ? options.pid
236
- : process.pid;
237
- const platform = options.platform ?? process.platform;
238
- const linuxIdentity = platform === 'linux'
239
- ? readLinuxProcessIdentity(pid)
240
- : null;
241
- const state = createSessionState(cwd, sessionId, pid, platform, linuxIdentity, {
242
- nativeSessionId: options.nativeSessionId ?? (sameSession ? existing?.native_session_id : undefined),
243
- previousNativeSessionId: options.previousNativeSessionId ?? (sameSession ? existing?.previous_native_session_id : undefined),
244
- nativeSessionSwitchedAt: options.nativeSessionSwitchedAt ?? (sameSession ? existing?.native_session_switched_at : undefined),
245
- ownerOmxSessionId: options.ownerOmxSessionId ?? (sameSession ? existing?.owner_omx_session_id : undefined),
246
- tmuxSessionName: options.tmuxSessionName ?? (sameSession ? existing?.tmux_session_name : undefined),
247
- tmuxPaneId: options.tmuxPaneId ?? (sameSession ? existing?.tmux_pane_id : undefined),
248
- });
249
- await writeFile(sessionPath(cwd), JSON.stringify(state, null, 2));
250
- await appendToLog(cwd, {
251
- event: 'session_start',
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
- ...(state.native_session_id ? { native_session_id: state.native_session_id } : {}),
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
- timestamp: state.started_at,
256
- });
257
- return state;
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
- if (typeof state.owner_omx_session_id === 'string' && state.owner_omx_session_id.startsWith('omx-')) {
263
- return state.owner_omx_session_id;
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
- * Reconcile a native/Codex SessionStart with the canonical OMX launch session.
269
- * Same-native restarts preserve the current logical session and refresh
270
- * PID/native metadata. Native-session replacements start a fresh native-scoped
271
- * session to avoid inheriting stale task-scoped state; when the replaced session
272
- * belongs to an OMX launch wrapper, retain that wrapper as owner for later
273
- * archive/cleanup and log the replacement chain.
274
- */
275
- export async function reconcileNativeSessionStart(cwd, nativeSessionId, options = {}) {
276
- const existing = await readUsableSessionState(cwd, {
277
- ...(options.platform ? { platform: options.platform } : {}),
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
- if (!existing) {
280
- return await writeSessionStart(cwd, nativeSessionId, {
281
- ...options,
282
- nativeSessionId,
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
- const existingNativeSessionId = typeof existing.native_session_id === 'string'
286
- ? existing.native_session_id.trim()
287
- : '';
288
- if (existingNativeSessionId && existingNativeSessionId !== nativeSessionId) {
289
- const ownerOmxSessionId = getOmxLaunchSessionId(existing);
290
- if (ownerOmxSessionId) {
291
- const pid = Number.isInteger(options.pid) && options.pid && options.pid > 0
292
- ? options.pid
293
- : process.pid;
294
- const nowIso = new Date().toISOString();
295
- await appendToLog(cwd, {
296
- event: 'native_session_replaced',
297
- session_id: ownerOmxSessionId,
298
- ...(existing.session_id !== ownerOmxSessionId ? { active_session_id: existing.session_id } : {}),
299
- previous_native_session_id: existingNativeSessionId,
300
- replaced_by_native_session_id: nativeSessionId,
301
- pid,
302
- timestamp: nowIso,
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
- return await writeSessionStart(cwd, nativeSessionId, {
305
- ...options,
306
- nativeSessionId,
307
- previousNativeSessionId: existingNativeSessionId,
308
- nativeSessionSwitchedAt: nowIso,
309
- ownerOmxSessionId,
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
- return await writeSessionStart(cwd, nativeSessionId, {
313
- ...options,
314
- nativeSessionId,
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
- const pid = Number.isInteger(options.pid) && options.pid && options.pid > 0
318
- ? options.pid
319
- : process.pid;
320
- const platform = options.platform ?? process.platform;
321
- const linuxIdentity = platform === 'linux'
322
- ? readLinuxProcessIdentity(pid)
323
- : null;
324
- const nowIso = new Date().toISOString();
325
- const state = createSessionState(cwd, existing.session_id, pid, platform, linuxIdentity, {
326
- nowIso,
327
- nativeSessionId,
328
- previousNativeSessionId: existing.previous_native_session_id,
329
- nativeSessionSwitchedAt: existing.native_session_switched_at,
330
- ownerOmxSessionId: existing.owner_omx_session_id,
331
- startedAt: existing.started_at,
332
- tmuxSessionName: existing.tmux_session_name,
333
- tmuxPaneId: existing.tmux_pane_id,
334
- });
335
- await writeFile(sessionPath(cwd), JSON.stringify(state, null, 2));
336
- await appendToLog(cwd, {
337
- event: 'session_start_reconciled',
338
- session_id: state.session_id,
339
- native_session_id: nativeSessionId,
340
- pid,
341
- timestamp: nowIso,
342
- });
343
- return state;
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
- * Write session end: archive to history, delete session.json.
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 state = await readSessionState(cwd);
350
- const endTime = new Date().toISOString();
351
- const ownsCurrentSessionFile = state == null
352
- || state.session_id === sessionId
353
- || state.native_session_id === sessionId
354
- || state.owner_omx_session_id === sessionId;
355
- // Archive to session history
356
- const logsDir = omxLogsDir(cwd);
357
- await mkdir(logsDir, { recursive: true });
358
- const historyEntry = {
359
- session_id: ownsCurrentSessionFile
360
- ? (state?.owner_omx_session_id === sessionId ? sessionId : state?.session_id || sessionId)
361
- : sessionId,
362
- ...(ownsCurrentSessionFile && state?.native_session_id ? { native_session_id: state.native_session_id } : {}),
363
- started_at: ownsCurrentSessionFile ? state?.started_at || 'unknown' : 'unknown',
364
- ended_at: endTime,
365
- cwd,
366
- pid: ownsCurrentSessionFile ? state?.pid || process.pid : process.pid,
367
- ...(ownsCurrentSessionFile && state?.owner_omx_session_id === sessionId && state?.session_id
368
- ? { active_session_id: state.session_id }
369
- : {}),
370
- ...(!ownsCurrentSessionFile && state?.session_id
371
- ? { preserved_active_session_id: state.session_id }
372
- : {}),
373
- };
374
- await appendFile(historyPath(cwd), JSON.stringify(historyEntry) + '\n');
375
- await removeDeadSessionHudState(cwd, [
376
- ...(ownsCurrentSessionFile ? [state?.session_id, state?.native_session_id] : []),
377
- sessionId,
378
- ]);
379
- // Delete session.json only when this end event owns the current pointer.
380
- if (ownsCurrentSessionFile) {
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 unlink(sessionPath(cwd));
383
- }
384
- catch { /* already gone */ }
385
- }
386
- await appendToLog(cwd, {
387
- event: 'session_end',
388
- session_id: ownsCurrentSessionFile
389
- ? (state?.owner_omx_session_id === sessionId ? sessionId : state?.session_id || sessionId)
390
- : sessionId,
391
- ...(ownsCurrentSessionFile && state?.native_session_id ? { native_session_id: state.native_session_id } : {}),
392
- ...(ownsCurrentSessionFile && state?.owner_omx_session_id === sessionId && state?.session_id
393
- ? { active_session_id: state.session_id }
394
- : {}),
395
- ...(!ownsCurrentSessionFile && state?.session_id
396
- ? { preserved_active_session_id: state.session_id }
397
- : {}),
398
- timestamp: endTime,
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 structured JSONL entry to the daily log file.
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 mkdir(logsDir, { recursive: true });
407
- const date = new Date().toISOString().slice(0, 10); // YYYY-MM-DD
408
- const logFile = join(logsDir, `omx-${date}.jsonl`);
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