oh-my-codex 0.20.2 → 0.20.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (805) hide show
  1. package/Cargo.lock +36 -18
  2. package/Cargo.toml +1 -1
  3. package/README.md +28 -2
  4. package/crates/omx-runtime/Cargo.toml +4 -0
  5. package/crates/omx-runtime/src/main.rs +415 -2
  6. package/crates/omx-runtime/tests/execution.rs +483 -0
  7. package/crates/omx-runtime-core/src/dispatch.rs +96 -10
  8. package/crates/omx-runtime-core/src/engine.rs +839 -29
  9. package/crates/omx-runtime-core/src/lib.rs +8 -0
  10. package/dist/adapt/__tests__/herdr-target.test.d.ts +2 -0
  11. package/dist/adapt/__tests__/herdr-target.test.d.ts.map +1 -0
  12. package/dist/adapt/__tests__/herdr-target.test.js +72 -0
  13. package/dist/adapt/__tests__/herdr-target.test.js.map +1 -0
  14. package/dist/adapt/contracts.d.ts +1 -1
  15. package/dist/adapt/contracts.d.ts.map +1 -1
  16. package/dist/adapt/contracts.js +1 -1
  17. package/dist/adapt/contracts.js.map +1 -1
  18. package/dist/adapt/herdr/__tests__/authority.test.d.ts +2 -0
  19. package/dist/adapt/herdr/__tests__/authority.test.d.ts.map +1 -0
  20. package/dist/adapt/herdr/__tests__/authority.test.js +49 -0
  21. package/dist/adapt/herdr/__tests__/authority.test.js.map +1 -0
  22. package/dist/adapt/herdr/__tests__/bridge.test.d.ts +2 -0
  23. package/dist/adapt/herdr/__tests__/bridge.test.d.ts.map +1 -0
  24. package/dist/adapt/herdr/__tests__/bridge.test.js +176 -0
  25. package/dist/adapt/herdr/__tests__/bridge.test.js.map +1 -0
  26. package/dist/adapt/herdr/__tests__/sanitize.test.d.ts +2 -0
  27. package/dist/adapt/herdr/__tests__/sanitize.test.d.ts.map +1 -0
  28. package/dist/adapt/herdr/__tests__/sanitize.test.js +45 -0
  29. package/dist/adapt/herdr/__tests__/sanitize.test.js.map +1 -0
  30. package/dist/adapt/herdr/__tests__/semantic.test.d.ts +2 -0
  31. package/dist/adapt/herdr/__tests__/semantic.test.d.ts.map +1 -0
  32. package/dist/adapt/herdr/__tests__/semantic.test.js +130 -0
  33. package/dist/adapt/herdr/__tests__/semantic.test.js.map +1 -0
  34. package/dist/adapt/herdr/__tests__/seq-store.test.d.ts +2 -0
  35. package/dist/adapt/herdr/__tests__/seq-store.test.d.ts.map +1 -0
  36. package/dist/adapt/herdr/__tests__/seq-store.test.js +54 -0
  37. package/dist/adapt/herdr/__tests__/seq-store.test.js.map +1 -0
  38. package/dist/adapt/herdr/__tests__/socket-live.test.d.ts +2 -0
  39. package/dist/adapt/herdr/__tests__/socket-live.test.d.ts.map +1 -0
  40. package/dist/adapt/herdr/__tests__/socket-live.test.js +104 -0
  41. package/dist/adapt/herdr/__tests__/socket-live.test.js.map +1 -0
  42. package/dist/adapt/herdr/__tests__/team-map.test.d.ts +2 -0
  43. package/dist/adapt/herdr/__tests__/team-map.test.d.ts.map +1 -0
  44. package/dist/adapt/herdr/__tests__/team-map.test.js +46 -0
  45. package/dist/adapt/herdr/__tests__/team-map.test.js.map +1 -0
  46. package/dist/adapt/herdr/__tests__/transport.test.d.ts +2 -0
  47. package/dist/adapt/herdr/__tests__/transport.test.d.ts.map +1 -0
  48. package/dist/adapt/herdr/__tests__/transport.test.js +160 -0
  49. package/dist/adapt/herdr/__tests__/transport.test.js.map +1 -0
  50. package/dist/adapt/herdr/__tests__/wiring.test.d.ts +2 -0
  51. package/dist/adapt/herdr/__tests__/wiring.test.d.ts.map +1 -0
  52. package/dist/adapt/herdr/__tests__/wiring.test.js +118 -0
  53. package/dist/adapt/herdr/__tests__/wiring.test.js.map +1 -0
  54. package/dist/adapt/herdr/authority.d.ts +34 -0
  55. package/dist/adapt/herdr/authority.d.ts.map +1 -0
  56. package/dist/adapt/herdr/authority.js +71 -0
  57. package/dist/adapt/herdr/authority.js.map +1 -0
  58. package/dist/adapt/herdr/bridge.d.ts +84 -0
  59. package/dist/adapt/herdr/bridge.d.ts.map +1 -0
  60. package/dist/adapt/herdr/bridge.js +222 -0
  61. package/dist/adapt/herdr/bridge.js.map +1 -0
  62. package/dist/adapt/herdr/sanitize.d.ts +20 -0
  63. package/dist/adapt/herdr/sanitize.d.ts.map +1 -0
  64. package/dist/adapt/herdr/sanitize.js +70 -0
  65. package/dist/adapt/herdr/sanitize.js.map +1 -0
  66. package/dist/adapt/herdr/semantic.d.ts +67 -0
  67. package/dist/adapt/herdr/semantic.d.ts.map +1 -0
  68. package/dist/adapt/herdr/semantic.js +170 -0
  69. package/dist/adapt/herdr/semantic.js.map +1 -0
  70. package/dist/adapt/herdr/seq-store.d.ts +32 -0
  71. package/dist/adapt/herdr/seq-store.d.ts.map +1 -0
  72. package/dist/adapt/herdr/seq-store.js +126 -0
  73. package/dist/adapt/herdr/seq-store.js.map +1 -0
  74. package/dist/adapt/herdr/team-map.d.ts +30 -0
  75. package/dist/adapt/herdr/team-map.d.ts.map +1 -0
  76. package/dist/adapt/herdr/team-map.js +37 -0
  77. package/dist/adapt/herdr/team-map.js.map +1 -0
  78. package/dist/adapt/herdr/transport.d.ts +115 -0
  79. package/dist/adapt/herdr/transport.d.ts.map +1 -0
  80. package/dist/adapt/herdr/transport.js +260 -0
  81. package/dist/adapt/herdr/transport.js.map +1 -0
  82. package/dist/adapt/herdr/wiring.d.ts +24 -0
  83. package/dist/adapt/herdr/wiring.d.ts.map +1 -0
  84. package/dist/adapt/herdr/wiring.js +62 -0
  85. package/dist/adapt/herdr/wiring.js.map +1 -0
  86. package/dist/adapt/herdr.d.ts +19 -0
  87. package/dist/adapt/herdr.d.ts.map +1 -0
  88. package/dist/adapt/herdr.js +115 -0
  89. package/dist/adapt/herdr.js.map +1 -0
  90. package/dist/adapt/index.d.ts +4 -0
  91. package/dist/adapt/index.d.ts.map +1 -1
  92. package/dist/adapt/index.js +48 -0
  93. package/dist/adapt/index.js.map +1 -1
  94. package/dist/adapt/registry.d.ts.map +1 -1
  95. package/dist/adapt/registry.js +11 -0
  96. package/dist/adapt/registry.js.map +1 -1
  97. package/dist/agents/__tests__/definitions.test.js +81 -0
  98. package/dist/agents/__tests__/definitions.test.js.map +1 -1
  99. package/dist/agents/__tests__/native-config.test.js +25 -0
  100. package/dist/agents/__tests__/native-config.test.js.map +1 -1
  101. package/dist/agents/native-config.d.ts +2 -1
  102. package/dist/agents/native-config.d.ts.map +1 -1
  103. package/dist/agents/native-config.js.map +1 -1
  104. package/dist/auth/__tests__/hotswap.test.js +60 -1
  105. package/dist/auth/__tests__/hotswap.test.js.map +1 -1
  106. package/dist/auth/__tests__/storage.test.js +33 -1
  107. package/dist/auth/__tests__/storage.test.js.map +1 -1
  108. package/dist/auth/hotswap.d.ts +22 -2
  109. package/dist/auth/hotswap.d.ts.map +1 -1
  110. package/dist/auth/hotswap.js +107 -22
  111. package/dist/auth/hotswap.js.map +1 -1
  112. package/dist/auth/storage.js +1 -1
  113. package/dist/auth/storage.js.map +1 -1
  114. package/dist/autopilot/__tests__/ralplan-gate.test.js +86 -1333
  115. package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
  116. package/dist/autopilot/ralplan-gate.d.ts.map +1 -1
  117. package/dist/autopilot/ralplan-gate.js +7 -6
  118. package/dist/autopilot/ralplan-gate.js.map +1 -1
  119. package/dist/cli/__tests__/agents-init.test.js +4 -23
  120. package/dist/cli/__tests__/agents-init.test.js.map +1 -1
  121. package/dist/cli/__tests__/api.test.js +61 -24
  122. package/dist/cli/__tests__/api.test.js.map +1 -1
  123. package/dist/cli/__tests__/auth.test.js +63 -7
  124. package/dist/cli/__tests__/auth.test.js.map +1 -1
  125. package/dist/cli/__tests__/codex-feature-probe.test.js +66 -1
  126. package/dist/cli/__tests__/codex-feature-probe.test.js.map +1 -1
  127. package/dist/cli/__tests__/codex-plugin-layout.test.js +150 -2
  128. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  129. package/dist/cli/__tests__/detached-leader-teardown.test.d.ts +2 -0
  130. package/dist/cli/__tests__/detached-leader-teardown.test.d.ts.map +1 -0
  131. package/dist/cli/__tests__/detached-leader-teardown.test.js +473 -0
  132. package/dist/cli/__tests__/detached-leader-teardown.test.js.map +1 -0
  133. package/dist/cli/__tests__/doctor-process-identity-readiness.test.d.ts +2 -0
  134. package/dist/cli/__tests__/doctor-process-identity-readiness.test.d.ts.map +1 -0
  135. package/dist/cli/__tests__/doctor-process-identity-readiness.test.js +103 -0
  136. package/dist/cli/__tests__/doctor-process-identity-readiness.test.js.map +1 -0
  137. package/dist/cli/__tests__/doctor-state-root-binding.test.d.ts +2 -0
  138. package/dist/cli/__tests__/doctor-state-root-binding.test.d.ts.map +1 -0
  139. package/dist/cli/__tests__/doctor-state-root-binding.test.js +139 -0
  140. package/dist/cli/__tests__/doctor-state-root-binding.test.js.map +1 -0
  141. package/dist/cli/__tests__/doctor-warning-copy.test.js +135 -1
  142. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  143. package/dist/cli/__tests__/exec.test.js +211 -0
  144. package/dist/cli/__tests__/exec.test.js.map +1 -1
  145. package/dist/cli/__tests__/index.test.js +878 -463
  146. package/dist/cli/__tests__/index.test.js.map +1 -1
  147. package/dist/cli/__tests__/launch-fallback.test.js +1290 -127
  148. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  149. package/dist/cli/__tests__/mcp-parity.test.js +8 -4
  150. package/dist/cli/__tests__/mcp-parity.test.js.map +1 -1
  151. package/dist/cli/__tests__/native-assets.test.js +398 -16
  152. package/dist/cli/__tests__/native-assets.test.js.map +1 -1
  153. package/dist/cli/__tests__/native-hook-claim-journal.test.js +132 -6
  154. package/dist/cli/__tests__/native-hook-claim-journal.test.js.map +1 -1
  155. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +3 -3
  156. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -1
  157. package/dist/cli/__tests__/ralph.test.js +3 -3
  158. package/dist/cli/__tests__/ralph.test.js.map +1 -1
  159. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js +49 -164
  160. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js.map +1 -1
  161. package/dist/cli/__tests__/ralplan-child-binding-3212.test.d.ts +2 -0
  162. package/dist/cli/__tests__/ralplan-child-binding-3212.test.d.ts.map +1 -0
  163. package/dist/cli/__tests__/ralplan-child-binding-3212.test.js +56 -0
  164. package/dist/cli/__tests__/ralplan-child-binding-3212.test.js.map +1 -0
  165. package/dist/cli/__tests__/ralplan.test.js +107 -109
  166. package/dist/cli/__tests__/ralplan.test.js.map +1 -1
  167. package/dist/cli/__tests__/resume.test.js +11 -10
  168. package/dist/cli/__tests__/resume.test.js.map +1 -1
  169. package/dist/cli/__tests__/session-scoped-runtime.test.js +1118 -43
  170. package/dist/cli/__tests__/session-scoped-runtime.test.js.map +1 -1
  171. package/dist/cli/__tests__/session-search-help.test.js +25 -1
  172. package/dist/cli/__tests__/session-search-help.test.js.map +1 -1
  173. package/dist/cli/__tests__/setup-agents-overwrite.test.js +8 -55
  174. package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
  175. package/dist/cli/__tests__/setup-install-mode.test.js +224 -7
  176. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  177. package/dist/cli/__tests__/sparkshell-cli.test.js +93 -41
  178. package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
  179. package/dist/cli/__tests__/team.test.js +150 -8
  180. package/dist/cli/__tests__/team.test.js.map +1 -1
  181. package/dist/cli/__tests__/ultragoal.test.js +117 -2
  182. package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
  183. package/dist/cli/__tests__/uninstall.test.js +95 -4
  184. package/dist/cli/__tests__/uninstall.test.js.map +1 -1
  185. package/dist/cli/__tests__/update.test.js +669 -177
  186. package/dist/cli/__tests__/update.test.js.map +1 -1
  187. package/dist/cli/__tests__/windows-popup-loop-contract.test.js +33 -2
  188. package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
  189. package/dist/cli/api.d.ts.map +1 -1
  190. package/dist/cli/api.js +9 -10
  191. package/dist/cli/api.js.map +1 -1
  192. package/dist/cli/codex-feature-probe.d.ts +16 -0
  193. package/dist/cli/codex-feature-probe.d.ts.map +1 -1
  194. package/dist/cli/codex-feature-probe.js +44 -0
  195. package/dist/cli/codex-feature-probe.js.map +1 -1
  196. package/dist/cli/codex-home.d.ts.map +1 -1
  197. package/dist/cli/codex-home.js +7 -4
  198. package/dist/cli/codex-home.js.map +1 -1
  199. package/dist/cli/doctor.d.ts +20 -0
  200. package/dist/cli/doctor.d.ts.map +1 -1
  201. package/dist/cli/doctor.js +290 -3
  202. package/dist/cli/doctor.js.map +1 -1
  203. package/dist/cli/index.d.ts +234 -12
  204. package/dist/cli/index.d.ts.map +1 -1
  205. package/dist/cli/index.js +2409 -746
  206. package/dist/cli/index.js.map +1 -1
  207. package/dist/cli/mcp-parity.js +2 -2
  208. package/dist/cli/mcp-parity.js.map +1 -1
  209. package/dist/cli/native-assets.d.ts +25 -0
  210. package/dist/cli/native-assets.d.ts.map +1 -1
  211. package/dist/cli/native-assets.js +448 -51
  212. package/dist/cli/native-assets.js.map +1 -1
  213. package/dist/cli/native-hook-claim-journal.d.ts +20 -5
  214. package/dist/cli/native-hook-claim-journal.d.ts.map +1 -1
  215. package/dist/cli/native-hook-claim-journal.js +67 -32
  216. package/dist/cli/native-hook-claim-journal.js.map +1 -1
  217. package/dist/cli/package-manager-ownership.d.ts +63 -0
  218. package/dist/cli/package-manager-ownership.d.ts.map +1 -0
  219. package/dist/cli/package-manager-ownership.js +323 -0
  220. package/dist/cli/package-manager-ownership.js.map +1 -0
  221. package/dist/cli/ralph.js +2 -2
  222. package/dist/cli/ralph.js.map +1 -1
  223. package/dist/cli/ralplan.d.ts +5 -7
  224. package/dist/cli/ralplan.d.ts.map +1 -1
  225. package/dist/cli/ralplan.js +83 -134
  226. package/dist/cli/ralplan.js.map +1 -1
  227. package/dist/cli/session-search.d.ts +5 -0
  228. package/dist/cli/session-search.d.ts.map +1 -1
  229. package/dist/cli/session-search.js +87 -0
  230. package/dist/cli/session-search.js.map +1 -1
  231. package/dist/cli/setup-preferences.d.ts +15 -0
  232. package/dist/cli/setup-preferences.d.ts.map +1 -1
  233. package/dist/cli/setup-preferences.js +27 -1
  234. package/dist/cli/setup-preferences.js.map +1 -1
  235. package/dist/cli/setup.d.ts +8 -0
  236. package/dist/cli/setup.d.ts.map +1 -1
  237. package/dist/cli/setup.js +119 -47
  238. package/dist/cli/setup.js.map +1 -1
  239. package/dist/cli/sparkshell.d.ts.map +1 -1
  240. package/dist/cli/sparkshell.js +52 -9
  241. package/dist/cli/sparkshell.js.map +1 -1
  242. package/dist/cli/ultragoal.d.ts.map +1 -1
  243. package/dist/cli/ultragoal.js +2 -2
  244. package/dist/cli/ultragoal.js.map +1 -1
  245. package/dist/cli/uninstall.d.ts +9 -0
  246. package/dist/cli/uninstall.d.ts.map +1 -1
  247. package/dist/cli/uninstall.js +71 -30
  248. package/dist/cli/uninstall.js.map +1 -1
  249. package/dist/cli/update-worker.d.ts +2 -0
  250. package/dist/cli/update-worker.d.ts.map +1 -0
  251. package/dist/cli/update-worker.js +142 -0
  252. package/dist/cli/update-worker.js.map +1 -0
  253. package/dist/cli/update.d.ts +14 -22
  254. package/dist/cli/update.d.ts.map +1 -1
  255. package/dist/cli/update.js +192 -241
  256. package/dist/cli/update.js.map +1 -1
  257. package/dist/compat/__tests__/rust-runtime-compat.test.js +1 -1
  258. package/dist/config/__tests__/generator-idempotent.test.js +503 -30
  259. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  260. package/dist/config/__tests__/generator-notify.test.js +5 -3
  261. package/dist/config/__tests__/generator-notify.test.js.map +1 -1
  262. package/dist/config/__tests__/models.test.js +67 -3
  263. package/dist/config/__tests__/models.test.js.map +1 -1
  264. package/dist/config/generator.d.ts +27 -21
  265. package/dist/config/generator.d.ts.map +1 -1
  266. package/dist/config/generator.js +610 -152
  267. package/dist/config/generator.js.map +1 -1
  268. package/dist/config/models.d.ts +12 -2
  269. package/dist/config/models.d.ts.map +1 -1
  270. package/dist/config/models.js +15 -1
  271. package/dist/config/models.js.map +1 -1
  272. package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js +25 -0
  273. package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js.map +1 -1
  274. package/dist/goal-workflows/codex-goal-snapshot.d.ts +1 -1
  275. package/dist/goal-workflows/codex-goal-snapshot.d.ts.map +1 -1
  276. package/dist/goal-workflows/codex-goal-snapshot.js +2 -0
  277. package/dist/goal-workflows/codex-goal-snapshot.js.map +1 -1
  278. package/dist/hooks/__tests__/agents-overlay.test.js +5 -3
  279. package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
  280. package/dist/hooks/__tests__/autopilot-skill-contract.test.js +27 -4
  281. package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
  282. package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.d.ts +2 -0
  283. package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.d.ts.map +1 -0
  284. package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.js +172 -0
  285. package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.js.map +1 -0
  286. package/dist/hooks/__tests__/keyword-detector.test.js +176 -37
  287. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  288. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +402 -545
  289. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  290. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +39 -4
  291. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
  292. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +161 -26
  293. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
  294. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js +27 -0
  295. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js.map +1 -1
  296. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js +10 -118
  297. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js.map +1 -1
  298. package/dist/hooks/__tests__/notify-hook-regression-205.test.js +7 -0
  299. package/dist/hooks/__tests__/notify-hook-regression-205.test.js.map +1 -1
  300. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +533 -14
  301. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
  302. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +159 -24
  303. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  304. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js +482 -32
  305. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
  306. package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.js +2 -1
  307. package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.js.map +1 -1
  308. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +68 -1
  309. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
  310. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js +7 -1
  311. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js.map +1 -1
  312. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +44 -4
  313. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
  314. package/dist/hooks/__tests__/prompt-guidance-contract.test.js +9 -0
  315. package/dist/hooks/__tests__/prompt-guidance-contract.test.js.map +1 -1
  316. package/dist/hooks/__tests__/session.test.js +3130 -365
  317. package/dist/hooks/__tests__/session.test.js.map +1 -1
  318. package/dist/hooks/agents-overlay.js +1 -1
  319. package/dist/hooks/agents-overlay.js.map +1 -1
  320. package/dist/hooks/extensibility/__tests__/dispatcher.test.js +108 -2
  321. package/dist/hooks/extensibility/__tests__/dispatcher.test.js.map +1 -1
  322. package/dist/hooks/extensibility/__tests__/sdk.test.js +151 -1
  323. package/dist/hooks/extensibility/__tests__/sdk.test.js.map +1 -1
  324. package/dist/hooks/extensibility/dispatcher.d.ts.map +1 -1
  325. package/dist/hooks/extensibility/dispatcher.js +20 -3
  326. package/dist/hooks/extensibility/dispatcher.js.map +1 -1
  327. package/dist/hooks/extensibility/plugin-runner.js +1 -0
  328. package/dist/hooks/extensibility/plugin-runner.js.map +1 -1
  329. package/dist/hooks/extensibility/runtime.d.ts.map +1 -1
  330. package/dist/hooks/extensibility/runtime.js.map +1 -1
  331. package/dist/hooks/extensibility/sdk/paths.d.ts +1 -1
  332. package/dist/hooks/extensibility/sdk/paths.d.ts.map +1 -1
  333. package/dist/hooks/extensibility/sdk/paths.js +2 -2
  334. package/dist/hooks/extensibility/sdk/paths.js.map +1 -1
  335. package/dist/hooks/extensibility/sdk/runtime-state.d.ts +1 -1
  336. package/dist/hooks/extensibility/sdk/runtime-state.d.ts.map +1 -1
  337. package/dist/hooks/extensibility/sdk/runtime-state.js +55 -15
  338. package/dist/hooks/extensibility/sdk/runtime-state.js.map +1 -1
  339. package/dist/hooks/extensibility/sdk.d.ts +1 -0
  340. package/dist/hooks/extensibility/sdk.d.ts.map +1 -1
  341. package/dist/hooks/extensibility/sdk.js +1 -1
  342. package/dist/hooks/extensibility/sdk.js.map +1 -1
  343. package/dist/hooks/extensibility/types.d.ts +7 -0
  344. package/dist/hooks/extensibility/types.d.ts.map +1 -1
  345. package/dist/hooks/keyword-detector.d.ts +10 -3
  346. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  347. package/dist/hooks/keyword-detector.js +116 -20
  348. package/dist/hooks/keyword-detector.js.map +1 -1
  349. package/dist/hooks/session.d.ts +204 -14
  350. package/dist/hooks/session.d.ts.map +1 -1
  351. package/dist/hooks/session.js +1500 -223
  352. package/dist/hooks/session.js.map +1 -1
  353. package/dist/hud/__tests__/hud-tmux-injection.test.js +6 -6
  354. package/dist/hud/__tests__/hud-tmux-injection.test.js.map +1 -1
  355. package/dist/hud/__tests__/index.test.js +26 -16
  356. package/dist/hud/__tests__/index.test.js.map +1 -1
  357. package/dist/hud/__tests__/reconcile.test.js +83 -78
  358. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  359. package/dist/hud/__tests__/state.test.js +36 -31
  360. package/dist/hud/__tests__/state.test.js.map +1 -1
  361. package/dist/hud/__tests__/tmux.test.js +155 -198
  362. package/dist/hud/__tests__/tmux.test.js.map +1 -1
  363. package/dist/hud/tmux.d.ts +42 -4
  364. package/dist/hud/tmux.d.ts.map +1 -1
  365. package/dist/hud/tmux.js +1017 -139
  366. package/dist/hud/tmux.js.map +1 -1
  367. package/dist/leader/__tests__/contract.test.js +124 -37
  368. package/dist/leader/__tests__/contract.test.js.map +1 -1
  369. package/dist/leader/contract.d.ts +17 -6
  370. package/dist/leader/contract.d.ts.map +1 -1
  371. package/dist/leader/contract.js +102 -26
  372. package/dist/leader/contract.js.map +1 -1
  373. package/dist/mcp/__tests__/code-intel-server.test.js +31 -3
  374. package/dist/mcp/__tests__/code-intel-server.test.js.map +1 -1
  375. package/dist/mcp/__tests__/hermes-bridge.test.js +17 -0
  376. package/dist/mcp/__tests__/hermes-bridge.test.js.map +1 -1
  377. package/dist/mcp/__tests__/server-lifecycle.test.js +4 -4
  378. package/dist/mcp/__tests__/state-paths.test.js +735 -4
  379. package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
  380. package/dist/mcp/__tests__/state-server.test.js +1 -1
  381. package/dist/mcp/__tests__/state-server.test.js.map +1 -1
  382. package/dist/mcp/__tests__/trace-server.test.js +2 -2
  383. package/dist/mcp/__tests__/trace-server.test.js.map +1 -1
  384. package/dist/mcp/__tests__/wiki-server.test.js +18 -0
  385. package/dist/mcp/__tests__/wiki-server.test.js.map +1 -1
  386. package/dist/mcp/code-intel-server.d.ts.map +1 -1
  387. package/dist/mcp/code-intel-server.js +13 -3
  388. package/dist/mcp/code-intel-server.js.map +1 -1
  389. package/dist/mcp/hermes-bridge.d.ts.map +1 -1
  390. package/dist/mcp/hermes-bridge.js +2 -2
  391. package/dist/mcp/hermes-bridge.js.map +1 -1
  392. package/dist/mcp/state-paths.d.ts +78 -4
  393. package/dist/mcp/state-paths.d.ts.map +1 -1
  394. package/dist/mcp/state-paths.js +543 -40
  395. package/dist/mcp/state-paths.js.map +1 -1
  396. package/dist/mcp/state-server.d.ts +4 -4
  397. package/dist/mcp/state-server.d.ts.map +1 -1
  398. package/dist/mcp/wiki-server.d.ts +11 -0
  399. package/dist/mcp/wiki-server.d.ts.map +1 -1
  400. package/dist/mcp/wiki-server.js +9 -2
  401. package/dist/mcp/wiki-server.js.map +1 -1
  402. package/dist/modes/__tests__/base-autopilot-gates.test.js +36 -7
  403. package/dist/modes/__tests__/base-autopilot-gates.test.js.map +1 -1
  404. package/dist/modes/__tests__/base-multi-state-compat.test.js +1 -1
  405. package/dist/modes/__tests__/base-multi-state-compat.test.js.map +1 -1
  406. package/dist/modes/__tests__/base-session-scope.test.js +17 -8
  407. package/dist/modes/__tests__/base-session-scope.test.js.map +1 -1
  408. package/dist/modes/__tests__/base-tmux-pane.test.js +4 -4
  409. package/dist/modes/__tests__/base-tmux-pane.test.js.map +1 -1
  410. package/dist/modes/base.d.ts +5 -5
  411. package/dist/modes/base.d.ts.map +1 -1
  412. package/dist/modes/base.js +76 -21
  413. package/dist/modes/base.js.map +1 -1
  414. package/dist/native-assets/archive.d.ts +17 -0
  415. package/dist/native-assets/archive.d.ts.map +1 -0
  416. package/dist/native-assets/archive.js +238 -0
  417. package/dist/native-assets/archive.js.map +1 -0
  418. package/dist/native-assets/policy.d.ts +42 -0
  419. package/dist/native-assets/policy.d.ts.map +1 -0
  420. package/dist/native-assets/policy.js +155 -0
  421. package/dist/native-assets/policy.js.map +1 -0
  422. package/dist/notifications/temp-contract.d.ts.map +1 -1
  423. package/dist/notifications/temp-contract.js +4 -0
  424. package/dist/notifications/temp-contract.js.map +1 -1
  425. package/dist/pipeline/__tests__/orchestrator.test.js +164 -42
  426. package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
  427. package/dist/pipeline/__tests__/stages.test.js +72 -155
  428. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  429. package/dist/pipeline/orchestrator.d.ts.map +1 -1
  430. package/dist/pipeline/orchestrator.js +124 -18
  431. package/dist/pipeline/orchestrator.js.map +1 -1
  432. package/dist/pipeline/stages/ralplan.d.ts.map +1 -1
  433. package/dist/pipeline/stages/ralplan.js +36 -8
  434. package/dist/pipeline/stages/ralplan.js.map +1 -1
  435. package/dist/ralplan/__tests__/consensus-gate.test.js +73 -1316
  436. package/dist/ralplan/__tests__/consensus-gate.test.js.map +1 -1
  437. package/dist/ralplan/__tests__/documented-leader-preflight.test.d.ts +2 -0
  438. package/dist/ralplan/__tests__/documented-leader-preflight.test.d.ts.map +1 -0
  439. package/dist/ralplan/__tests__/documented-leader-preflight.test.js +335 -0
  440. package/dist/ralplan/__tests__/documented-leader-preflight.test.js.map +1 -0
  441. package/dist/ralplan/__tests__/runtime.test.js +85 -101
  442. package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
  443. package/dist/ralplan/consensus-gate.d.ts +9 -1
  444. package/dist/ralplan/consensus-gate.d.ts.map +1 -1
  445. package/dist/ralplan/consensus-gate.js +82 -338
  446. package/dist/ralplan/consensus-gate.js.map +1 -1
  447. package/dist/ralplan/documented-leader-preflight.d.ts +41 -0
  448. package/dist/ralplan/documented-leader-preflight.d.ts.map +1 -0
  449. package/dist/ralplan/documented-leader-preflight.js +360 -0
  450. package/dist/ralplan/documented-leader-preflight.js.map +1 -0
  451. package/dist/ralplan/runtime.d.ts +2 -1
  452. package/dist/ralplan/runtime.d.ts.map +1 -1
  453. package/dist/ralplan/runtime.js +67 -168
  454. package/dist/ralplan/runtime.js.map +1 -1
  455. package/dist/runtime/__tests__/bridge.test.js +42 -1
  456. package/dist/runtime/__tests__/bridge.test.js.map +1 -1
  457. package/dist/runtime/bridge.d.ts +12 -0
  458. package/dist/runtime/bridge.d.ts.map +1 -1
  459. package/dist/runtime/bridge.js +52 -1
  460. package/dist/runtime/bridge.js.map +1 -1
  461. package/dist/runtime/run-state.d.ts +5 -1
  462. package/dist/runtime/run-state.d.ts.map +1 -1
  463. package/dist/runtime/run-state.js +15 -8
  464. package/dist/runtime/run-state.js.map +1 -1
  465. package/dist/scripts/__tests__/codex-native-hook.test.js +16323 -8978
  466. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  467. package/dist/scripts/__tests__/issue-3293-callsite-parity.test.d.ts +2 -0
  468. package/dist/scripts/__tests__/issue-3293-callsite-parity.test.d.ts.map +1 -0
  469. package/dist/scripts/__tests__/issue-3293-callsite-parity.test.js +225 -0
  470. package/dist/scripts/__tests__/issue-3293-callsite-parity.test.js.map +1 -0
  471. package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.d.ts +2 -0
  472. package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.d.ts.map +1 -0
  473. package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.js +188 -0
  474. package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.js.map +1 -0
  475. package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.d.ts +2 -0
  476. package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.d.ts.map +1 -0
  477. package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.js +259 -0
  478. package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.js.map +1 -0
  479. package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.d.ts +2 -0
  480. package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.d.ts.map +1 -0
  481. package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.js +491 -0
  482. package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.js.map +1 -0
  483. package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.d.ts +2 -0
  484. package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.d.ts.map +1 -0
  485. package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.js +708 -0
  486. package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.js.map +1 -0
  487. package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.d.ts +2 -0
  488. package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.d.ts.map +1 -0
  489. package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.js +208 -0
  490. package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.js.map +1 -0
  491. package/dist/scripts/__tests__/notify-state-io.test.js +26 -0
  492. package/dist/scripts/__tests__/notify-state-io.test.js.map +1 -1
  493. package/dist/scripts/__tests__/notify-tmux-injection.test.js +23 -1
  494. package/dist/scripts/__tests__/notify-tmux-injection.test.js.map +1 -1
  495. package/dist/scripts/__tests__/postinstall.test.js +19 -1
  496. package/dist/scripts/__tests__/postinstall.test.js.map +1 -1
  497. package/dist/scripts/__tests__/reasoning-artifact-contract.test.d.ts +2 -0
  498. package/dist/scripts/__tests__/reasoning-artifact-contract.test.d.ts.map +1 -0
  499. package/dist/scripts/__tests__/reasoning-artifact-contract.test.js +108 -0
  500. package/dist/scripts/__tests__/reasoning-artifact-contract.test.js.map +1 -0
  501. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js +63 -173
  502. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js.map +1 -1
  503. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js +74 -159
  504. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js.map +1 -1
  505. package/dist/scripts/__tests__/smoke-packed-install.test.js +404 -13
  506. package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
  507. package/dist/scripts/__tests__/verify-native-release-assets.test.d.ts +2 -0
  508. package/dist/scripts/__tests__/verify-native-release-assets.test.d.ts.map +1 -0
  509. package/dist/scripts/__tests__/verify-native-release-assets.test.js +235 -0
  510. package/dist/scripts/__tests__/verify-native-release-assets.test.js.map +1 -0
  511. package/dist/scripts/codex-native-hook.d.ts +53 -1
  512. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  513. package/dist/scripts/codex-native-hook.js +14448 -1480
  514. package/dist/scripts/codex-native-hook.js.map +1 -1
  515. package/dist/scripts/codex-native-pre-post.d.ts +9 -0
  516. package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
  517. package/dist/scripts/codex-native-pre-post.js +35 -12
  518. package/dist/scripts/codex-native-pre-post.js.map +1 -1
  519. package/dist/scripts/generate-native-release-manifest.js +80 -51
  520. package/dist/scripts/generate-native-release-manifest.js.map +1 -1
  521. package/dist/scripts/notify-fallback-watcher.js +78 -115
  522. package/dist/scripts/notify-fallback-watcher.js.map +1 -1
  523. package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
  524. package/dist/scripts/notify-hook/auto-nudge.js +74 -3
  525. package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
  526. package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
  527. package/dist/scripts/notify-hook/state-io.js +5 -0
  528. package/dist/scripts/notify-hook/state-io.js.map +1 -1
  529. package/dist/scripts/notify-hook/team-dispatch.d.ts +9 -0
  530. package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
  531. package/dist/scripts/notify-hook/team-dispatch.js +234 -79
  532. package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
  533. package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
  534. package/dist/scripts/notify-hook/team-leader-nudge.js +71 -16
  535. package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
  536. package/dist/scripts/notify-hook/team-tmux-guard.d.ts +12 -4
  537. package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
  538. package/dist/scripts/notify-hook/team-tmux-guard.js +365 -44
  539. package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
  540. package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
  541. package/dist/scripts/notify-hook/team-worker-stop.js +42 -21
  542. package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
  543. package/dist/scripts/notify-hook/team-worker.d.ts +3 -0
  544. package/dist/scripts/notify-hook/team-worker.d.ts.map +1 -1
  545. package/dist/scripts/notify-hook/team-worker.js +82 -33
  546. package/dist/scripts/notify-hook/team-worker.js.map +1 -1
  547. package/dist/scripts/notify-hook/tmux-injection.d.ts +1 -0
  548. package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
  549. package/dist/scripts/notify-hook/tmux-injection.js +32 -3
  550. package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
  551. package/dist/scripts/notify-hook.js +79 -82
  552. package/dist/scripts/notify-hook.js.map +1 -1
  553. package/dist/scripts/postinstall-advisory.d.ts +14 -0
  554. package/dist/scripts/postinstall-advisory.d.ts.map +1 -0
  555. package/dist/scripts/postinstall-advisory.js +49 -0
  556. package/dist/scripts/postinstall-advisory.js.map +1 -0
  557. package/dist/scripts/postinstall.d.ts +1 -1
  558. package/dist/scripts/postinstall.d.ts.map +1 -1
  559. package/dist/scripts/postinstall.js +1 -1
  560. package/dist/scripts/postinstall.js.map +1 -1
  561. package/dist/scripts/smoke-packed-install.d.ts +80 -4
  562. package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
  563. package/dist/scripts/smoke-packed-install.js +3162 -26
  564. package/dist/scripts/smoke-packed-install.js.map +1 -1
  565. package/dist/scripts/sync-plugin-mirror.d.ts.map +1 -1
  566. package/dist/scripts/sync-plugin-mirror.js +16 -1
  567. package/dist/scripts/sync-plugin-mirror.js.map +1 -1
  568. package/dist/scripts/verify-native-release-assets.js +37 -23
  569. package/dist/scripts/verify-native-release-assets.js.map +1 -1
  570. package/dist/state/__tests__/mode-state-context.test.js +61 -0
  571. package/dist/state/__tests__/mode-state-context.test.js.map +1 -1
  572. package/dist/state/__tests__/operations.test.js +462 -850
  573. package/dist/state/__tests__/operations.test.js.map +1 -1
  574. package/dist/state/__tests__/skill-active.test.js +400 -2
  575. package/dist/state/__tests__/skill-active.test.js.map +1 -1
  576. package/dist/state/mode-state-context.d.ts +8 -0
  577. package/dist/state/mode-state-context.d.ts.map +1 -1
  578. package/dist/state/mode-state-context.js +73 -0
  579. package/dist/state/mode-state-context.js.map +1 -1
  580. package/dist/state/operations.d.ts +3 -0
  581. package/dist/state/operations.d.ts.map +1 -1
  582. package/dist/state/operations.js +144 -40
  583. package/dist/state/operations.js.map +1 -1
  584. package/dist/state/skill-active.d.ts +18 -1
  585. package/dist/state/skill-active.d.ts.map +1 -1
  586. package/dist/state/skill-active.js +420 -23
  587. package/dist/state/skill-active.js.map +1 -1
  588. package/dist/state/workflow-transition-reconcile.d.ts +3 -0
  589. package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
  590. package/dist/state/workflow-transition-reconcile.js +10 -4
  591. package/dist/state/workflow-transition-reconcile.js.map +1 -1
  592. package/dist/state/workflow-transition.d.ts.map +1 -1
  593. package/dist/state/workflow-transition.js +3 -1
  594. package/dist/state/workflow-transition.js.map +1 -1
  595. package/dist/subagents/__tests__/leader-bootstrap-3181.test.js +53 -247
  596. package/dist/subagents/__tests__/leader-bootstrap-3181.test.js.map +1 -1
  597. package/dist/subagents/__tests__/tracker.test.js +837 -1048
  598. package/dist/subagents/__tests__/tracker.test.js.map +1 -1
  599. package/dist/subagents/tracker.d.ts +58 -135
  600. package/dist/subagents/tracker.d.ts.map +1 -1
  601. package/dist/subagents/tracker.js +811 -632
  602. package/dist/subagents/tracker.js.map +1 -1
  603. package/dist/team/__tests__/api-interop.test.js +91 -1
  604. package/dist/team/__tests__/api-interop.test.js.map +1 -1
  605. package/dist/team/__tests__/delivery-e2e-smoke.test.js +58 -17
  606. package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
  607. package/dist/team/__tests__/idle-nudge.test.js +45 -15
  608. package/dist/team/__tests__/idle-nudge.test.js.map +1 -1
  609. package/dist/team/__tests__/mcp-comm.test.js +197 -30
  610. package/dist/team/__tests__/mcp-comm.test.js.map +1 -1
  611. package/dist/team/__tests__/model-contract.test.js +110 -3
  612. package/dist/team/__tests__/model-contract.test.js.map +1 -1
  613. package/dist/team/__tests__/notice-ledger.test.d.ts +2 -0
  614. package/dist/team/__tests__/notice-ledger.test.d.ts.map +1 -0
  615. package/dist/team/__tests__/notice-ledger.test.js +131 -0
  616. package/dist/team/__tests__/notice-ledger.test.js.map +1 -0
  617. package/dist/team/__tests__/pane-teardown-residue-e2e.test.d.ts +2 -0
  618. package/dist/team/__tests__/pane-teardown-residue-e2e.test.d.ts.map +1 -0
  619. package/dist/team/__tests__/pane-teardown-residue-e2e.test.js +410 -0
  620. package/dist/team/__tests__/pane-teardown-residue-e2e.test.js.map +1 -0
  621. package/dist/team/__tests__/phase-controller.test.js +20 -0
  622. package/dist/team/__tests__/phase-controller.test.js.map +1 -1
  623. package/dist/team/__tests__/runtime-cli.test.js +9 -0
  624. package/dist/team/__tests__/runtime-cli.test.js.map +1 -1
  625. package/dist/team/__tests__/runtime.test.js +3963 -428
  626. package/dist/team/__tests__/runtime.test.js.map +1 -1
  627. package/dist/team/__tests__/scaling.test.js +2405 -203
  628. package/dist/team/__tests__/scaling.test.js.map +1 -1
  629. package/dist/team/__tests__/state-root.test.js +117 -2
  630. package/dist/team/__tests__/state-root.test.js.map +1 -1
  631. package/dist/team/__tests__/state.test.js +804 -36
  632. package/dist/team/__tests__/state.test.js.map +1 -1
  633. package/dist/team/__tests__/team-ops-contract.test.js +6 -0
  634. package/dist/team/__tests__/team-ops-contract.test.js.map +1 -1
  635. package/dist/team/__tests__/tmux-session.test.js +4099 -1039
  636. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  637. package/dist/team/__tests__/tmux-source-authority-realtmux.test.d.ts +2 -0
  638. package/dist/team/__tests__/tmux-source-authority-realtmux.test.d.ts.map +1 -0
  639. package/dist/team/__tests__/tmux-source-authority-realtmux.test.js +149 -0
  640. package/dist/team/__tests__/tmux-source-authority-realtmux.test.js.map +1 -0
  641. package/dist/team/__tests__/tmux-test-fixture.d.ts +69 -0
  642. package/dist/team/__tests__/tmux-test-fixture.d.ts.map +1 -1
  643. package/dist/team/__tests__/tmux-test-fixture.js +242 -15
  644. package/dist/team/__tests__/tmux-test-fixture.js.map +1 -1
  645. package/dist/team/__tests__/tmux-test-fixture.test.js +177 -1
  646. package/dist/team/__tests__/tmux-test-fixture.test.js.map +1 -1
  647. package/dist/team/__tests__/worker-runtime-identity.test.js +53 -44
  648. package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
  649. package/dist/team/__tests__/worktree.test.js +6 -0
  650. package/dist/team/__tests__/worktree.test.js.map +1 -1
  651. package/dist/team/api-interop.d.ts.map +1 -1
  652. package/dist/team/api-interop.js +15 -9
  653. package/dist/team/api-interop.js.map +1 -1
  654. package/dist/team/exact-pane.d.ts +23 -0
  655. package/dist/team/exact-pane.d.ts.map +1 -0
  656. package/dist/team/exact-pane.js +133 -0
  657. package/dist/team/exact-pane.js.map +1 -0
  658. package/dist/team/idle-nudge.d.ts +12 -1
  659. package/dist/team/idle-nudge.d.ts.map +1 -1
  660. package/dist/team/idle-nudge.js +41 -14
  661. package/dist/team/idle-nudge.js.map +1 -1
  662. package/dist/team/model-contract.d.ts +2 -5
  663. package/dist/team/model-contract.d.ts.map +1 -1
  664. package/dist/team/model-contract.js +59 -17
  665. package/dist/team/model-contract.js.map +1 -1
  666. package/dist/team/notice-ledger.d.ts +54 -0
  667. package/dist/team/notice-ledger.d.ts.map +1 -0
  668. package/dist/team/notice-ledger.js +259 -0
  669. package/dist/team/notice-ledger.js.map +1 -0
  670. package/dist/team/phase-controller.d.ts.map +1 -1
  671. package/dist/team/phase-controller.js +2 -0
  672. package/dist/team/phase-controller.js.map +1 -1
  673. package/dist/team/runtime.d.ts +4 -1
  674. package/dist/team/runtime.d.ts.map +1 -1
  675. package/dist/team/runtime.js +1527 -291
  676. package/dist/team/runtime.js.map +1 -1
  677. package/dist/team/scaling.d.ts +13 -1
  678. package/dist/team/scaling.d.ts.map +1 -1
  679. package/dist/team/scaling.js +1503 -387
  680. package/dist/team/scaling.js.map +1 -1
  681. package/dist/team/state/dispatch.d.ts.map +1 -1
  682. package/dist/team/state/dispatch.js +26 -8
  683. package/dist/team/state/dispatch.js.map +1 -1
  684. package/dist/team/state/locks.d.ts.map +1 -1
  685. package/dist/team/state/locks.js +61 -13
  686. package/dist/team/state/locks.js.map +1 -1
  687. package/dist/team/state/monitor.d.ts +10 -0
  688. package/dist/team/state/monitor.d.ts.map +1 -1
  689. package/dist/team/state/monitor.js +13 -0
  690. package/dist/team/state/monitor.js.map +1 -1
  691. package/dist/team/state/tasks.d.ts.map +1 -1
  692. package/dist/team/state/tasks.js +4 -3
  693. package/dist/team/state/tasks.js.map +1 -1
  694. package/dist/team/state/types.d.ts +15 -0
  695. package/dist/team/state/types.d.ts.map +1 -1
  696. package/dist/team/state/types.js.map +1 -1
  697. package/dist/team/state-root.d.ts +12 -0
  698. package/dist/team/state-root.d.ts.map +1 -1
  699. package/dist/team/state-root.js +177 -0
  700. package/dist/team/state-root.js.map +1 -1
  701. package/dist/team/state.d.ts +85 -1
  702. package/dist/team/state.d.ts.map +1 -1
  703. package/dist/team/state.js +672 -129
  704. package/dist/team/state.js.map +1 -1
  705. package/dist/team/team-ops.d.ts +5 -0
  706. package/dist/team/team-ops.d.ts.map +1 -1
  707. package/dist/team/team-ops.js +4 -0
  708. package/dist/team/team-ops.js.map +1 -1
  709. package/dist/team/tmux-session.d.ts +163 -27
  710. package/dist/team/tmux-session.d.ts.map +1 -1
  711. package/dist/team/tmux-session.js +1682 -376
  712. package/dist/team/tmux-session.js.map +1 -1
  713. package/dist/team/worktree.d.ts.map +1 -1
  714. package/dist/team/worktree.js +4 -0
  715. package/dist/team/worktree.js.map +1 -1
  716. package/dist/ultragoal/__tests__/artifacts.test.js +950 -2
  717. package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
  718. package/dist/ultragoal/__tests__/docs-contract.test.js +1 -0
  719. package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
  720. package/dist/ultragoal/artifacts.d.ts +35 -1
  721. package/dist/ultragoal/artifacts.d.ts.map +1 -1
  722. package/dist/ultragoal/artifacts.js +232 -46
  723. package/dist/ultragoal/artifacts.js.map +1 -1
  724. package/dist/utils/__tests__/file-durability.test.d.ts +2 -0
  725. package/dist/utils/__tests__/file-durability.test.d.ts.map +1 -0
  726. package/dist/utils/__tests__/file-durability.test.js +114 -0
  727. package/dist/utils/__tests__/file-durability.test.js.map +1 -0
  728. package/dist/utils/__tests__/paths.test.js +21 -1
  729. package/dist/utils/__tests__/paths.test.js.map +1 -1
  730. package/dist/utils/file-durability.d.ts +25 -0
  731. package/dist/utils/file-durability.d.ts.map +1 -0
  732. package/dist/utils/file-durability.js +67 -0
  733. package/dist/utils/file-durability.js.map +1 -0
  734. package/dist/utils/paths.d.ts.map +1 -1
  735. package/dist/utils/paths.js +14 -6
  736. package/dist/utils/paths.js.map +1 -1
  737. package/dist/verification/__tests__/ci-rust-gates.test.js +49 -4
  738. package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
  739. package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js +3 -0
  740. package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js.map +1 -1
  741. package/dist/verification/__tests__/explore-harness-release-workflow.test.js +60 -0
  742. package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +1 -1
  743. package/dist/verification/__tests__/native-release-manifest.test.js +157 -4
  744. package/dist/verification/__tests__/native-release-manifest.test.js.map +1 -1
  745. package/dist/wiki/lint.d.ts +4 -1
  746. package/dist/wiki/lint.d.ts.map +1 -1
  747. package/dist/wiki/lint.js +3 -2
  748. package/dist/wiki/lint.js.map +1 -1
  749. package/package.json +13 -5
  750. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  751. package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +105 -17
  752. package/plugins/oh-my-codex/skills/autopilot/SKILL.md +13 -5
  753. package/plugins/oh-my-codex/skills/cancel/SKILL.md +30 -85
  754. package/plugins/oh-my-codex/skills/ralph/SKILL.md +6 -4
  755. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +16 -17
  756. package/plugins/oh-my-codex/skills/team/SKILL.md +13 -9
  757. package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +3 -2
  758. package/skills/autopilot/SKILL.md +13 -5
  759. package/skills/cancel/SKILL.md +30 -85
  760. package/skills/ralph/SKILL.md +6 -4
  761. package/skills/ralplan/SKILL.md +16 -17
  762. package/skills/team/SKILL.md +13 -9
  763. package/skills/ultragoal/SKILL.md +3 -2
  764. package/src/scripts/__tests__/codex-native-hook.test.ts +8831 -951
  765. package/src/scripts/__tests__/issue-3293-callsite-parity.test.ts +187 -0
  766. package/src/scripts/__tests__/issue-3293-cancel-transaction.test.ts +189 -0
  767. package/src/scripts/__tests__/issue-3293-hook-owned-cancel.test.ts +187 -0
  768. package/src/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.ts +591 -0
  769. package/src/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.ts +701 -0
  770. package/src/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.ts +211 -0
  771. package/src/scripts/__tests__/notify-state-io.test.ts +29 -0
  772. package/src/scripts/__tests__/notify-tmux-injection.test.ts +26 -1
  773. package/src/scripts/__tests__/postinstall.test.ts +23 -1
  774. package/src/scripts/__tests__/reasoning-artifact-contract.test.ts +129 -0
  775. package/src/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.ts +67 -185
  776. package/src/scripts/__tests__/role-intent-durable-recovery-3181.test.ts +79 -178
  777. package/src/scripts/__tests__/smoke-packed-install.test.ts +525 -16
  778. package/src/scripts/__tests__/verify-native-release-assets.test.ts +240 -0
  779. package/src/scripts/codex-native-hook.ts +14238 -1429
  780. package/src/scripts/codex-native-pre-post.ts +43 -12
  781. package/src/scripts/generate-native-release-manifest.ts +83 -79
  782. package/src/scripts/notify-fallback-watcher.ts +91 -122
  783. package/src/scripts/notify-hook/auto-nudge.ts +71 -3
  784. package/src/scripts/notify-hook/state-io.ts +11 -0
  785. package/src/scripts/notify-hook/team-dispatch.ts +242 -75
  786. package/src/scripts/notify-hook/team-leader-nudge.ts +72 -22
  787. package/src/scripts/notify-hook/team-tmux-guard.ts +366 -39
  788. package/src/scripts/notify-hook/team-worker-stop.ts +43 -20
  789. package/src/scripts/notify-hook/team-worker.ts +82 -31
  790. package/src/scripts/notify-hook/tmux-injection.ts +33 -3
  791. package/src/scripts/notify-hook.ts +84 -82
  792. package/src/scripts/postinstall-advisory.ts +67 -0
  793. package/src/scripts/postinstall.ts +1 -1
  794. package/src/scripts/smoke-packed-install.ts +3608 -39
  795. package/src/scripts/sync-plugin-mirror.ts +17 -1
  796. package/src/scripts/verify-native-release-assets.ts +39 -31
  797. package/templates/AGENTS.md +1 -0
  798. package/dist/subagents/__tests__/adapted-role-binding.test.d.ts +0 -2
  799. package/dist/subagents/__tests__/adapted-role-binding.test.d.ts.map +0 -1
  800. package/dist/subagents/__tests__/adapted-role-binding.test.js +0 -988
  801. package/dist/subagents/__tests__/adapted-role-binding.test.js.map +0 -1
  802. package/dist/subagents/adapted-role-binding.d.ts +0 -17
  803. package/dist/subagents/adapted-role-binding.d.ts.map +0 -1
  804. package/dist/subagents/adapted-role-binding.js +0 -75
  805. package/dist/subagents/adapted-role-binding.js.map +0 -1
@@ -1,10 +1,12 @@
1
1
  import { describe, it } from 'node:test';
2
2
  import assert from 'node:assert/strict';
3
- import { mkdir, mkdtemp, readFile, readdir, rename, rm, symlink, writeFile } from 'node:fs/promises';
3
+ import { spawn } from 'node:child_process';
4
+ import { once } from 'node:events';
4
5
  import { existsSync } from 'node:fs';
6
+ import { link, lstat, mkdir, mkdtemp, readFile, readdir, readlink, rename, rm, symlink, writeFile } from 'node:fs/promises';
5
7
  import { tmpdir } from 'node:os';
6
8
  import { join } from 'node:path';
7
- import { __createDefaultPidProbeForTests, appendPromptSessionProvenanceRejection, __resetSessionPointerTransactionDependenciesForTests, __setSessionPointerTransactionDependenciesForTests, isSessionPointerLaunchAbort, isSessionStale, readSessionPointer, readSessionState, readUsableSessionState, reconcileNativeSessionStart, resetSessionMetrics, resolveSessionPointerContext, writeSessionEnd, writeSessionStart, } from '../session.js';
9
+ import { __createDefaultPidProbeForTests, __releasePointerLockForTests, appendPromptSessionProvenanceRejection, closeLaunchSessionBindingOnce, establishLaunchSessionBinding, finalizeBoundOnce, __resetSessionPointerTransactionDependenciesForTests, __setSessionPointerTransactionDependenciesForTests, inspectSessionPointerLock, isSessionPointerLaunchAbort, isSessionStale, readSessionPointer, readSessionState, readUsableSessionState, readNativeSessionOwner, readNativeSessionOwnerEvidence, reconcileNativeSessionStart, recoverSessionPointerLock, resetSessionMetrics, resolveSessionPointerContext, writeSessionEnd, writeNativeSessionOwner, updateDetachedSessionMetadata, writeSessionStart, } from '../session.js';
8
10
  function todayIsoDate() {
9
11
  return new Date().toISOString().slice(0, 10);
10
12
  }
@@ -23,8 +25,17 @@ const SUCCESSOR_TOKEN = 'successor_token_123456789';
23
25
  function codedError(code) {
24
26
  return Object.assign(new Error(code), { code });
25
27
  }
28
+ function isOwnerConflict(error) {
29
+ return isSessionPointerLaunchAbort(error)
30
+ && error.code === 'session_pointer_owner_conflict';
31
+ }
26
32
  async function withPointerDependencies(overrides, run) {
27
- __setSessionPointerTransactionDependenciesForTests(overrides);
33
+ const runtimePlatform = overrides.runtimePlatform ?? process.platform;
34
+ __setSessionPointerTransactionDependenciesForTests({
35
+ atomicRenameNoReplace: defaultTestAtomicRenameNoReplace,
36
+ ...overrides,
37
+ runtimePlatform,
38
+ });
28
39
  try {
29
40
  await run();
30
41
  }
@@ -48,8 +59,11 @@ async function withOwnerEnvironment(sessionId, run) {
48
59
  process.env.OMX_SESSION_ID = previous;
49
60
  }
50
61
  }
51
- function matchingProcessIdentity() {
52
- return { status: 'matching', startTicks: 1 };
62
+ function matchingObservation(platform = process.platform) {
63
+ return { kind: 'identity', identity: { platform, birth: '1' } };
64
+ }
65
+ function matchingWin32Observation() {
66
+ return { kind: 'identity', identity: { platform: 'win32', birth: '1' } };
53
67
  }
54
68
  async function writeLockOwner(cwd, owner) {
55
69
  const context = resolveSessionPointerContext(cwd);
@@ -58,16 +72,73 @@ async function writeLockOwner(cwd, owner) {
58
72
  return context.lockPath;
59
73
  }
60
74
  function validLockOwner(overrides = {}) {
75
+ const platform = (overrides.platform ?? 'linux');
76
+ const version = overrides.version === 2 || overrides.process_identity !== undefined ? 2 : 1;
77
+ const identity = overrides.process_identity ?? (version === 2 ? { platform, birth: '1' } : undefined);
61
78
  return {
62
- version: 1,
79
+ version,
63
80
  token: TEST_TOKEN,
64
81
  pid: process.pid,
65
- platform: 'linux',
66
- pid_start_ticks: 1,
82
+ platform,
83
+ ...(version === 2 && identity ? { process_identity: identity } : {}),
84
+ ...(version === 1 ? { pid_start_ticks: 1 } : {}),
67
85
  created_at: '2026-07-14T00:00:00.000Z',
68
86
  ...overrides,
69
87
  };
70
88
  }
89
+ /** Safe test seam for capability probes: no-replace rename without overwriting destinations. */
90
+ async function defaultTestAtomicRenameNoReplace(from, to) {
91
+ if (existsSync(to))
92
+ return 'not-moved';
93
+ await rename(from, to);
94
+ return 'moved';
95
+ }
96
+ /** Wrap a lock-directory-focused seam so capability probes keep working. */
97
+ function withProbeFallback(contextLockPath, move) {
98
+ return async (from, to) => {
99
+ if (from === contextLockPath
100
+ || to === contextLockPath
101
+ || from.startsWith(`${contextLockPath}.`)
102
+ || to.startsWith(`${contextLockPath}.`)) {
103
+ return await move(from, to);
104
+ }
105
+ return await defaultTestAtomicRenameNoReplace(from, to);
106
+ };
107
+ }
108
+ async function assertDurableDirectoryRecovery(cwd, options = {}) {
109
+ const context = resolveSessionPointerContext(cwd);
110
+ await writeLockOwner(cwd, validLockOwner());
111
+ await withPointerDependencies({
112
+ token: () => SUCCESSOR_TOKEN,
113
+ probePid: () => 'dead',
114
+ atomicRenameNoReplace: options.atomicRenameNoReplace ?? defaultTestAtomicRenameNoReplace,
115
+ }, async () => {
116
+ const recovered = await recoverSessionPointerLock(cwd);
117
+ assert.equal(recovered.recovered, true, recovered.reason);
118
+ assert.equal(recovered.action, 'quarantined');
119
+ const parked = `${context.lockPath}.parked-lock-${SUCCESSOR_TOKEN}`;
120
+ const completed = `${context.lockPath}.recovery.${TEST_TOKEN}.${SUCCESSOR_TOKEN}.completed`;
121
+ assert.equal(existsSync(context.lockPath), false);
122
+ assert.equal(existsSync(parked), true);
123
+ assert.equal(existsSync(completed), true);
124
+ assert.equal(existsSync(recovered.quarantinePath), true);
125
+ const quarantine = await lstat(recovered.quarantinePath);
126
+ const parkedOwner = await lstat(join(parked, 'owner.json'));
127
+ assert.deepEqual({ dev: quarantine.dev, ino: quarantine.ino }, { dev: parkedOwner.dev, ino: parkedOwner.ino });
128
+ const repeated = await recoverSessionPointerLock(cwd);
129
+ assert.equal(repeated.recovered, false);
130
+ assert.equal(repeated.status, 'absent');
131
+ });
132
+ }
133
+ async function linuxProcessStartTicks(pid) {
134
+ const stat = await readFile(`/proc/${pid}/stat`, 'utf-8');
135
+ const closingParen = stat.lastIndexOf(')');
136
+ assert.notEqual(closingParen, -1);
137
+ const fieldsAfterCommand = stat.slice(closingParen + 2).trim().split(/\s+/);
138
+ const startTicks = Number.parseInt(fieldsAfterCommand[19] ?? '', 10);
139
+ assert.equal(Number.isInteger(startTicks), true);
140
+ return startTicks;
141
+ }
71
142
  describe('session lifecycle manager', () => {
72
143
  it('resets session metrics files with zeroed counters', async () => {
73
144
  const cwd = await mkdtemp(join(tmpdir(), 'omx-session-metrics-'));
@@ -121,8 +192,13 @@ describe('session lifecycle manager', () => {
121
192
  it('writes session start/end lifecycle artifacts and archives session history', async () => {
122
193
  const cwd = await mkdtemp(join(tmpdir(), 'omx-session-lifecycle-'));
123
194
  const sessionId = 'sess-lifecycle-1';
195
+ let binding;
124
196
  try {
125
- await writeSessionStart(cwd, sessionId);
197
+ const established = await establishLaunchSessionBinding(cwd, sessionId);
198
+ assert.equal(established.kind, 'committed-released');
199
+ if (established.kind !== 'committed-released')
200
+ return;
201
+ binding = established.binding;
126
202
  const state = await readSessionState(cwd);
127
203
  assert.ok(state);
128
204
  assert.equal(state.session_id, sessionId);
@@ -131,7 +207,7 @@ describe('session lifecycle manager', () => {
131
207
  assert.equal(isSessionStale(state), false);
132
208
  const sessionPath = join(cwd, '.omx', 'state', 'session.json');
133
209
  assert.equal(existsSync(sessionPath), true);
134
- await writeSessionEnd(cwd, sessionId);
210
+ await finalizeBoundOnce(binding, 'test');
135
211
  assert.equal(existsSync(sessionPath), false);
136
212
  const historyPath = join(cwd, '.omx', 'logs', 'session-history.jsonl');
137
213
  assert.equal(existsSync(historyPath), true);
@@ -152,6 +228,78 @@ describe('session lifecycle manager', () => {
152
228
  assert.match(dailyLog, /"event":"session_end"/);
153
229
  }
154
230
  finally {
231
+ if (binding)
232
+ await closeLaunchSessionBindingOnce(binding).catch(() => { });
233
+ await rm(cwd, { recursive: true, force: true });
234
+ }
235
+ });
236
+ it('emits the session-end warning only after successful end finalization', async () => {
237
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-end-durability-'));
238
+ const originalWrite = process.stderr.write;
239
+ const warnings = [];
240
+ process.stderr.write = ((value) => {
241
+ warnings.push(value);
242
+ return true;
243
+ });
244
+ let binding;
245
+ try {
246
+ const established = await establishLaunchSessionBinding(cwd, 'sess-end-durability');
247
+ assert.equal(established.kind, 'committed-released');
248
+ if (established.kind !== 'committed-released')
249
+ return;
250
+ binding = established.binding;
251
+ await writeSessionEnd(cwd, 'sess-end-durability', {
252
+ binding,
253
+ context: binding.context,
254
+ platform: 'win32',
255
+ regularFileSync: async () => { throw codedError('EPERM'); },
256
+ });
257
+ assert.deepEqual(warnings, [
258
+ '[omx] warning: Windows EPERM regular-file fsync unsupported in session pointer end; operation succeeded with degraded durability.\n',
259
+ ]);
260
+ }
261
+ finally {
262
+ if (binding)
263
+ await closeLaunchSessionBindingOnce(binding).catch(() => { });
264
+ process.stderr.write = originalWrite;
265
+ await rm(cwd, { recursive: true, force: true });
266
+ }
267
+ });
268
+ it('keeps session-end durability warnings silent when finalization or release fails', async () => {
269
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-end-durability-failure-'));
270
+ const originalWrite = process.stderr.write;
271
+ const warnings = [];
272
+ process.stderr.write = ((value) => {
273
+ warnings.push(value);
274
+ return true;
275
+ });
276
+ const regularFileSync = async () => { throw codedError('EPERM'); };
277
+ let binding;
278
+ try {
279
+ const established = await establishLaunchSessionBinding(cwd, 'sess-end-release-failure');
280
+ assert.equal(established.kind, 'committed-released');
281
+ if (established.kind !== 'committed-released')
282
+ return;
283
+ binding = established.binding;
284
+ const establishedBinding = binding;
285
+ await withPointerDependencies({
286
+ fs: {
287
+ rmdir: async () => { throw new Error('release failure'); },
288
+ },
289
+ }, async () => {
290
+ await assert.rejects(writeSessionEnd(cwd, 'sess-end-release-failure', {
291
+ binding: establishedBinding,
292
+ context: establishedBinding.context,
293
+ platform: 'win32',
294
+ regularFileSync,
295
+ }));
296
+ });
297
+ assert.deepEqual(warnings, []);
298
+ }
299
+ finally {
300
+ if (binding)
301
+ await closeLaunchSessionBindingOnce(binding).catch(() => { });
302
+ process.stderr.write = originalWrite;
155
303
  await rm(cwd, { recursive: true, force: true });
156
304
  }
157
305
  });
@@ -159,6 +307,7 @@ describe('session lifecycle manager', () => {
159
307
  const cwd = await mkdtemp(join(tmpdir(), 'omx-session-end-owner-'));
160
308
  try {
161
309
  await writeSessionStart(cwd, 'sess-current');
310
+ const beforePointer = await readFile(resolveSessionPointerContext(cwd).sessionPath, 'utf-8');
162
311
  const stateDir = join(cwd, '.omx', 'state');
163
312
  const sessionPath = join(stateDir, 'session.json');
164
313
  const currentHudPath = join(stateDir, 'sessions', 'sess-current', 'hud-state.json');
@@ -167,21 +316,13 @@ describe('session lifecycle manager', () => {
167
316
  await mkdir(join(stateDir, 'sessions', 'sess-ending'), { recursive: true });
168
317
  await writeFile(currentHudPath, JSON.stringify({ turn_count: 2 }), 'utf-8');
169
318
  await writeFile(endingHudPath, JSON.stringify({ turn_count: 1 }), 'utf-8');
170
- await writeSessionEnd(cwd, 'sess-ending');
319
+ await assert.rejects(() => writeSessionEnd(cwd, 'sess-ending'), isSessionPointerLaunchAbort);
171
320
  const state = await readSessionState(cwd);
172
321
  assert.equal(state?.session_id, 'sess-current');
173
- assert.equal(existsSync(sessionPath), true);
322
+ assert.equal(await readFile(sessionPath, 'utf-8'), beforePointer);
174
323
  assert.equal(existsSync(currentHudPath), true);
175
- assert.equal(existsSync(endingHudPath), false);
176
- const historyLines = (await readFile(join(cwd, '.omx', 'logs', 'session-history.jsonl'), 'utf-8'))
177
- .trim()
178
- .split('\n')
179
- .filter(Boolean);
180
- assert.equal(historyLines.length, 1);
181
- const historyEntry = JSON.parse(historyLines[0]);
182
- assert.equal(historyEntry.session_id, 'sess-ending');
183
- assert.equal(historyEntry.started_at, 'unknown');
184
- assert.equal(historyEntry.preserved_active_session_id, 'sess-current');
324
+ assert.equal(existsSync(endingHudPath), true);
325
+ assert.equal(existsSync(join(cwd, '.omx', 'logs', 'session-history.jsonl')), false);
185
326
  }
186
327
  finally {
187
328
  await rm(cwd, { recursive: true, force: true });
@@ -191,8 +332,13 @@ describe('session lifecycle manager', () => {
191
332
  const cwd = await mkdtemp(join(tmpdir(), 'omx-session-end-hud-cleanup-'));
192
333
  const canonicalSessionId = 'omx-launch-hud';
193
334
  const nativeSessionId = 'codex-native-hud';
335
+ let binding;
194
336
  try {
195
- await writeSessionStart(cwd, canonicalSessionId, { nativeSessionId });
337
+ const established = await establishLaunchSessionBinding(cwd, canonicalSessionId, { nativeSessionId });
338
+ assert.equal(established.kind, 'committed-released');
339
+ if (established.kind !== 'committed-released')
340
+ return;
341
+ binding = established.binding;
196
342
  const stateDir = join(cwd, '.omx', 'state');
197
343
  const rootHudPath = join(stateDir, 'hud-state.json');
198
344
  const canonicalHudPath = join(stateDir, 'sessions', canonicalSessionId, 'hud-state.json');
@@ -202,19 +348,21 @@ describe('session lifecycle manager', () => {
202
348
  await writeFile(rootHudPath, JSON.stringify({ last_turn_at: 'root', turn_count: 1 }), 'utf-8');
203
349
  await writeFile(canonicalHudPath, JSON.stringify({ last_turn_at: 'canonical', turn_count: 2 }), 'utf-8');
204
350
  await writeFile(nativeHudPath, JSON.stringify({ last_turn_at: 'native', turn_count: 9 }), 'utf-8');
205
- await writeSessionEnd(cwd, canonicalSessionId);
351
+ await finalizeBoundOnce(binding, 'test');
206
352
  assert.equal(existsSync(rootHudPath), false);
207
353
  assert.equal(existsSync(canonicalHudPath), false);
208
354
  assert.equal(existsSync(nativeHudPath), false);
209
355
  }
210
356
  finally {
357
+ if (binding)
358
+ await closeLaunchSessionBindingOnce(binding).catch(() => { });
211
359
  await rm(cwd, { recursive: true, force: true });
212
360
  }
213
361
  });
214
362
  it('preserves canonical session id while reconciling native SessionStart metadata', async () => {
215
363
  const cwd = await mkdtemp(join(tmpdir(), 'omx-session-native-reconcile-'));
216
364
  try {
217
- await writeSessionStart(cwd, 'omx-launch-1');
365
+ const established = await writeSessionStart(cwd, 'omx-launch-1');
218
366
  const reconciled = await reconcileNativeSessionStart(cwd, 'codex-native-1', {
219
367
  pid: 54321,
220
368
  platform: 'win32',
@@ -223,10 +371,12 @@ describe('session lifecycle manager', () => {
223
371
  assert.equal(reconciled.native_session_id, 'codex-native-1');
224
372
  assert.equal(reconciled.pid, 54321);
225
373
  assert.equal(reconciled.platform, 'win32');
374
+ assert.equal(reconciled.launch_lineage_token, established.launch_lineage_token);
226
375
  const persisted = await readSessionState(cwd);
227
376
  assert.equal(persisted?.session_id, 'omx-launch-1');
228
377
  assert.equal(persisted?.native_session_id, 'codex-native-1');
229
378
  assert.equal(persisted?.pid, 54321);
379
+ assert.equal(persisted?.launch_lineage_token, established.launch_lineage_token);
230
380
  const dailyLogPath = join(cwd, '.omx', 'logs', `omx-${todayIsoDate()}.jsonl`);
231
381
  const dailyLog = await readFile(dailyLogPath, 'utf-8');
232
382
  assert.match(dailyLog, /"event":"session_start_reconciled"/);
@@ -236,74 +386,76 @@ describe('session lifecycle manager', () => {
236
386
  await rm(cwd, { recursive: true, force: true });
237
387
  }
238
388
  });
239
- it('starts a fresh native session while retaining the owner OMX launch session when native SessionStart changes', async () => {
389
+ it('rejects replacing a live native session pointer with another native SessionStart', async () => {
240
390
  const cwd = await mkdtemp(join(tmpdir(), 'omx-session-native-fresh-'));
241
391
  try {
242
392
  await writeSessionStart(cwd, 'omx-old-session', {
243
393
  nativeSessionId: 'codex-native-old',
244
394
  });
245
- const reconciled = await reconcileNativeSessionStart(cwd, 'codex-native-new', {
395
+ await assert.rejects(reconcileNativeSessionStart(cwd, 'codex-native-new', {
246
396
  pid: 54321,
247
397
  platform: 'win32',
248
- });
249
- assert.equal(reconciled.session_id, 'codex-native-new');
250
- assert.equal(reconciled.native_session_id, 'codex-native-new');
251
- assert.equal(reconciled.previous_native_session_id, 'codex-native-old');
252
- assert.equal(reconciled.owner_omx_session_id, 'omx-old-session');
253
- assert.match(reconciled.native_session_switched_at ?? '', /^\d{4}-\d{2}-\d{2}T/);
254
- assert.equal(reconciled.pid, 54321);
398
+ }), isOwnerConflict);
255
399
  const persisted = await readSessionState(cwd);
256
- assert.equal(persisted?.session_id, 'codex-native-new');
257
- assert.equal(persisted?.native_session_id, 'codex-native-new');
258
- assert.equal(persisted?.previous_native_session_id, 'codex-native-old');
259
- assert.equal(persisted?.owner_omx_session_id, 'omx-old-session');
260
- const dailyLogPath = join(cwd, '.omx', 'logs', `omx-${todayIsoDate()}.jsonl`);
261
- const dailyLog = await readFile(dailyLogPath, 'utf-8');
262
- assert.match(dailyLog, /"event":"native_session_replaced"/);
263
- assert.match(dailyLog, /"event":"session_start"/);
264
- assert.match(dailyLog, /"previous_native_session_id":"codex-native-old"/);
265
- assert.match(dailyLog, /"native_session_id":"codex-native-new"/);
400
+ assert.equal(persisted?.session_id, 'omx-old-session');
401
+ assert.equal(persisted?.native_session_id, 'codex-native-old');
266
402
  }
267
403
  finally {
268
404
  await rm(cwd, { recursive: true, force: true });
269
405
  }
270
406
  });
271
- it('preserves existing native and tmux bindings on same-session start updates', async () => {
407
+ it('routes existing detached metadata updates through the exact binding', async () => {
272
408
  const cwd = await mkdtemp(join(tmpdir(), 'omx-session-binding-preserve-'));
409
+ let binding;
273
410
  try {
274
- await writeSessionStart(cwd, 'omx-launch-1', {
411
+ const established = await establishLaunchSessionBinding(cwd, 'omx-launch-1', {
275
412
  nativeSessionId: 'codex-native-1',
276
413
  tmuxSessionName: 'omx-detached-demo',
277
414
  });
278
- const withPane = await writeSessionStart(cwd, 'omx-launch-1', {
415
+ assert.equal(established.kind, 'committed-released');
416
+ if (established.kind !== 'committed-released')
417
+ return;
418
+ binding = established.binding;
419
+ const withPaneResult = await updateDetachedSessionMetadata(binding, {
279
420
  tmuxSessionName: 'omx-detached-demo',
280
421
  tmuxPaneId: '%42',
281
422
  });
282
- assert.equal(withPane.native_session_id, 'codex-native-1');
283
- assert.equal(withPane.tmux_session_name, 'omx-detached-demo');
284
- assert.equal(withPane.tmux_pane_id, '%42');
285
- const withoutPane = await writeSessionStart(cwd, 'omx-launch-1', {
423
+ assert.equal(withPaneResult.kind, 'committed-released');
424
+ const withPane = await readSessionState(cwd);
425
+ assert.equal(withPane?.native_session_id, 'codex-native-1');
426
+ assert.equal(withPane?.tmux_session_name, 'omx-detached-demo');
427
+ assert.equal(withPane?.tmux_pane_id, '%42');
428
+ const withoutPaneResult = await updateDetachedSessionMetadata(binding, {
286
429
  tmuxSessionName: 'omx-detached-demo',
287
430
  });
288
- assert.equal(withoutPane.native_session_id, 'codex-native-1');
289
- assert.equal(withoutPane.tmux_session_name, 'omx-detached-demo');
290
- assert.equal(withoutPane.tmux_pane_id, '%42');
431
+ assert.equal(withoutPaneResult.kind, 'committed-released');
432
+ const withoutPane = await readSessionState(cwd);
433
+ assert.equal(withoutPane?.native_session_id, 'codex-native-1');
434
+ assert.equal(withoutPane?.tmux_session_name, 'omx-detached-demo');
435
+ assert.equal(withoutPane?.tmux_pane_id, '%42');
291
436
  }
292
437
  finally {
438
+ if (binding)
439
+ await closeLaunchSessionBindingOnce(binding).catch(() => { });
293
440
  await rm(cwd, { recursive: true, force: true });
294
441
  }
295
442
  });
296
- it('lets an owner OMX launch session end the fresh native session it spawned', async () => {
443
+ it('lets an owner OMX launch session end after rejecting an unrelated native replacement', async () => {
297
444
  const cwd = await mkdtemp(join(tmpdir(), 'omx-session-native-owner-end-'));
445
+ let binding;
298
446
  try {
299
- await writeSessionStart(cwd, 'omx-owner-session', {
447
+ const established = await establishLaunchSessionBinding(cwd, 'omx-owner-session', {
300
448
  nativeSessionId: 'codex-native-old',
301
449
  });
302
- await reconcileNativeSessionStart(cwd, 'codex-native-new', {
450
+ assert.equal(established.kind, 'committed-released');
451
+ if (established.kind !== 'committed-released')
452
+ return;
453
+ binding = established.binding;
454
+ await assert.rejects(reconcileNativeSessionStart(cwd, 'codex-native-new', {
303
455
  pid: process.pid,
304
456
  platform: 'win32',
305
- });
306
- await writeSessionEnd(cwd, 'omx-owner-session');
457
+ }), isOwnerConflict);
458
+ await finalizeBoundOnce(binding, 'test');
307
459
  assert.equal(await readSessionState(cwd), null);
308
460
  const historyLines = (await readFile(join(cwd, '.omx', 'logs', 'session-history.jsonl'), 'utf-8'))
309
461
  .trim()
@@ -312,64 +464,73 @@ describe('session lifecycle manager', () => {
312
464
  assert.equal(historyLines.length, 1);
313
465
  const historyEntry = JSON.parse(historyLines[0]);
314
466
  assert.equal(historyEntry.session_id, 'omx-owner-session');
315
- assert.equal(historyEntry.native_session_id, 'codex-native-new');
316
- assert.equal(historyEntry.active_session_id, 'codex-native-new');
467
+ assert.equal(historyEntry.native_session_id, 'codex-native-old');
468
+ assert.equal(historyEntry.active_session_id, undefined);
317
469
  }
318
470
  finally {
471
+ if (binding)
472
+ await closeLaunchSessionBindingOnce(binding).catch(() => { });
319
473
  await rm(cwd, { recursive: true, force: true });
320
474
  }
321
475
  });
322
- it('preserves owner OMX metadata when reconciling the same fresh native session', async () => {
476
+ it('preserves canonical session metadata when reconciling the same native session', async () => {
323
477
  const cwd = await mkdtemp(join(tmpdir(), 'omx-session-native-owner-reconcile-'));
478
+ let binding;
324
479
  try {
325
- await writeSessionStart(cwd, 'omx-owner-session', {
326
- nativeSessionId: 'codex-native-old',
327
- });
328
- await reconcileNativeSessionStart(cwd, 'codex-native-new', {
329
- pid: process.pid,
330
- platform: 'win32',
331
- });
332
- const reconciled = await reconcileNativeSessionStart(cwd, 'codex-native-new', {
333
- pid: process.pid,
334
- platform: 'win32',
480
+ await withPointerDependencies({
481
+ runtimePlatform: 'win32',
482
+ observeProcess: () => matchingWin32Observation(),
483
+ }, async () => {
484
+ const established = await establishLaunchSessionBinding(cwd, 'omx-owner-session', {
485
+ nativeSessionId: 'codex-native-old',
486
+ platform: 'win32',
487
+ });
488
+ assert.equal(established.kind, 'committed-released');
489
+ if (established.kind !== 'committed-released')
490
+ return;
491
+ binding = established.binding;
492
+ const reconciled = await reconcileNativeSessionStart(cwd, 'codex-native-old', {
493
+ pid: process.pid,
494
+ platform: 'win32',
495
+ });
496
+ assert.equal(reconciled.session_id, 'omx-owner-session');
497
+ assert.equal(reconciled.native_session_id, 'codex-native-old');
498
+ assert.equal(reconciled.previous_native_session_id, undefined);
499
+ assert.equal(reconciled.owner_omx_session_id, undefined);
500
+ assert.equal(reconciled.pid, process.pid);
501
+ await finalizeBoundOnce(binding, 'test');
502
+ assert.equal(await readSessionState(cwd), null);
335
503
  });
336
- assert.equal(reconciled.session_id, 'codex-native-new');
337
- assert.equal(reconciled.native_session_id, 'codex-native-new');
338
- assert.equal(reconciled.previous_native_session_id, 'codex-native-old');
339
- assert.equal(reconciled.owner_omx_session_id, 'omx-owner-session');
340
- assert.equal(reconciled.pid, process.pid);
341
- await writeSessionEnd(cwd, 'omx-owner-session');
342
- assert.equal(await readSessionState(cwd), null);
343
504
  }
344
505
  finally {
506
+ if (binding)
507
+ await closeLaunchSessionBindingOnce(binding).catch(() => { });
345
508
  await rm(cwd, { recursive: true, force: true });
346
509
  }
347
510
  });
348
- it('carries the owner OMX launch session across chained native SessionStart replacements', async () => {
511
+ it('rejects chained native SessionStart replacements and preserves the original pointer', async () => {
349
512
  const cwd = await mkdtemp(join(tmpdir(), 'omx-session-native-owner-chain-'));
513
+ let binding;
350
514
  try {
351
- await writeSessionStart(cwd, 'omx-owner-session', {
515
+ const established = await establishLaunchSessionBinding(cwd, 'omx-owner-session', {
352
516
  nativeSessionId: 'codex-native-a',
353
517
  });
354
- await reconcileNativeSessionStart(cwd, 'codex-native-b', {
518
+ assert.equal(established.kind, 'committed-released');
519
+ if (established.kind !== 'committed-released')
520
+ return;
521
+ binding = established.binding;
522
+ await assert.rejects(reconcileNativeSessionStart(cwd, 'codex-native-b', {
355
523
  pid: process.pid,
356
524
  platform: 'win32',
357
- });
358
- const reconciled = await reconcileNativeSessionStart(cwd, 'codex-native-c', {
525
+ }), isOwnerConflict);
526
+ await assert.rejects(reconcileNativeSessionStart(cwd, 'codex-native-c', {
359
527
  pid: process.pid,
360
528
  platform: 'win32',
361
- });
362
- assert.equal(reconciled.session_id, 'codex-native-c');
363
- assert.equal(reconciled.native_session_id, 'codex-native-c');
364
- assert.equal(reconciled.previous_native_session_id, 'codex-native-b');
365
- assert.equal(reconciled.owner_omx_session_id, 'omx-owner-session');
366
- const dailyLogPath = join(cwd, '.omx', 'logs', `omx-${todayIsoDate()}.jsonl`);
367
- const dailyLog = await readFile(dailyLogPath, 'utf-8');
368
- assert.match(dailyLog, /"session_id":"omx-owner-session"/);
369
- assert.match(dailyLog, /"active_session_id":"codex-native-b"/);
370
- assert.match(dailyLog, /"previous_native_session_id":"codex-native-b"/);
371
- assert.match(dailyLog, /"replaced_by_native_session_id":"codex-native-c"/);
372
- await writeSessionEnd(cwd, 'omx-owner-session');
529
+ }), isOwnerConflict);
530
+ const persisted = await readSessionState(cwd);
531
+ assert.equal(persisted?.session_id, 'omx-owner-session');
532
+ assert.equal(persisted?.native_session_id, 'codex-native-a');
533
+ await finalizeBoundOnce(binding, 'test');
373
534
  assert.equal(await readSessionState(cwd), null);
374
535
  const historyLines = (await readFile(join(cwd, '.omx', 'logs', 'session-history.jsonl'), 'utf-8'))
375
536
  .trim()
@@ -377,30 +538,28 @@ describe('session lifecycle manager', () => {
377
538
  .filter(Boolean);
378
539
  const historyEntry = JSON.parse(historyLines.at(-1) ?? '{}');
379
540
  assert.equal(historyEntry.session_id, 'omx-owner-session');
380
- assert.equal(historyEntry.native_session_id, 'codex-native-c');
381
- assert.equal(historyEntry.active_session_id, 'codex-native-c');
541
+ assert.equal(historyEntry.native_session_id, 'codex-native-a');
542
+ assert.equal(historyEntry.active_session_id, undefined);
382
543
  }
383
544
  finally {
545
+ if (binding)
546
+ await closeLaunchSessionBindingOnce(binding).catch(() => { });
384
547
  await rm(cwd, { recursive: true, force: true });
385
548
  }
386
549
  });
387
- it('starts a fresh canonical session when a non-OMX native session is replaced', async () => {
550
+ it('rejects replacing a live wrapper-owned native session', async () => {
388
551
  const cwd = await mkdtemp(join(tmpdir(), 'omx-session-native-non-omx-fresh-'));
389
552
  try {
390
553
  await writeSessionStart(cwd, 'codex-native-old', {
391
554
  nativeSessionId: 'codex-native-old',
392
555
  });
393
- const reconciled = await reconcileNativeSessionStart(cwd, 'codex-native-new', {
556
+ await assert.rejects(reconcileNativeSessionStart(cwd, 'codex-native-new', {
394
557
  pid: 54321,
395
558
  platform: 'win32',
396
- });
397
- assert.equal(reconciled.session_id, 'codex-native-new');
398
- assert.equal(reconciled.native_session_id, 'codex-native-new');
399
- assert.equal(reconciled.previous_native_session_id, undefined);
400
- assert.equal(reconciled.pid, 54321);
559
+ }), isOwnerConflict);
401
560
  const persisted = await readSessionState(cwd);
402
- assert.equal(persisted?.session_id, 'codex-native-new');
403
- assert.equal(persisted?.native_session_id, 'codex-native-new');
561
+ assert.equal(persisted?.session_id, 'codex-native-old');
562
+ assert.equal(persisted?.native_session_id, 'codex-native-old');
404
563
  assert.equal(persisted?.previous_native_session_id, undefined);
405
564
  }
406
565
  finally {
@@ -546,14 +705,29 @@ describe('isSessionStale', () => {
546
705
  });
547
706
  assert.equal(stale, true);
548
707
  });
549
- it('falls back to PID liveness on non-Linux platforms', () => {
550
- const state = makeState();
551
- const stale = isSessionStale(state, {
552
- platform: 'darwin',
553
- isPidAlive: () => true,
554
- readLinuxIdentity: () => null,
555
- });
556
- assert.equal(stale, false);
708
+ it('classifies identity-less live non-Linux pointers as identity-indeterminate', async () => {
709
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-non-linux-identityless-'));
710
+ try {
711
+ const context = resolveSessionPointerContext(cwd);
712
+ await mkdir(context.baseStateDir, { recursive: true });
713
+ await writeFile(context.sessionPath, JSON.stringify({
714
+ session_id: 'sess-darwin-identityless',
715
+ started_at: '2026-07-14T00:00:00.000Z',
716
+ cwd,
717
+ pid: process.pid,
718
+ platform: 'darwin',
719
+ }), 'utf-8');
720
+ await withPointerDependencies({
721
+ runtimePlatform: 'darwin',
722
+ probePid: () => 'alive',
723
+ observeProcess: () => ({ kind: 'unsupported' }),
724
+ }, async () => {
725
+ assert.equal((await readSessionPointer(context)).status, 'identity-indeterminate');
726
+ });
727
+ }
728
+ finally {
729
+ await rm(cwd, { recursive: true, force: true });
730
+ }
557
731
  });
558
732
  });
559
733
  describe('session pointer transaction', () => {
@@ -563,13 +737,39 @@ describe('session pointer transaction', () => {
563
737
  && error.operation === 'pointer-context-resolve'
564
738
  && !('pointerPath' in error));
565
739
  });
740
+ it('keeps the default synthetic identity aligned with the host transaction platform', async () => {
741
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-platform-fixture-'));
742
+ try {
743
+ const context = resolveSessionPointerContext(cwd);
744
+ await mkdir(context.baseStateDir, { recursive: true });
745
+ await writeFile(context.sessionPath, JSON.stringify({
746
+ session_id: 'sess-platform-fixture',
747
+ started_at: '2026-07-14T00:00:00.000Z',
748
+ cwd,
749
+ pid: process.pid,
750
+ platform: process.platform,
751
+ identity_schema_version: 2,
752
+ process_identity: { platform: process.platform, birth: '1' },
753
+ }), 'utf-8');
754
+ await withPointerDependencies({
755
+ probePid: () => 'alive',
756
+ observeProcess: () => matchingObservation(),
757
+ }, async () => {
758
+ assert.equal((await readSessionPointer(context)).status, 'usable');
759
+ });
760
+ }
761
+ finally {
762
+ __resetSessionPointerTransactionDependenciesForTests();
763
+ await rm(cwd, { recursive: true, force: true });
764
+ }
765
+ });
566
766
  it('classifies only the exact selected pointer as absent, usable, stale, indeterminate, malformed, or foreign', async () => {
567
767
  const cwd = await mkdtemp(join(tmpdir(), 'omx-session-pointer-status-'));
568
768
  try {
569
769
  const context = resolveSessionPointerContext(cwd);
570
770
  await withPointerDependencies({
571
771
  probePid: () => 'alive',
572
- readProcessIdentity: () => matchingProcessIdentity(),
772
+ observeProcess: () => matchingObservation(),
573
773
  }, async () => {
574
774
  assert.equal((await readSessionPointer(context)).status, 'absent');
575
775
  await mkdir(context.baseStateDir, { recursive: true });
@@ -584,12 +784,12 @@ describe('session pointer transaction', () => {
584
784
  assert.equal((await readSessionPointer(context)).status, 'usable');
585
785
  __setSessionPointerTransactionDependenciesForTests({
586
786
  probePid: () => 'dead',
587
- readProcessIdentity: () => matchingProcessIdentity(),
787
+ observeProcess: () => matchingObservation(),
588
788
  });
589
789
  assert.equal((await readSessionPointer(context)).status, 'stale-dead');
590
790
  __setSessionPointerTransactionDependenciesForTests({
591
791
  probePid: () => 'indeterminate',
592
- readProcessIdentity: () => matchingProcessIdentity(),
792
+ observeProcess: () => matchingObservation(),
593
793
  });
594
794
  assert.equal((await readSessionPointer(context)).status, 'identity-indeterminate');
595
795
  await writeFile(context.sessionPath, '{ not-json', 'utf-8');
@@ -609,6 +809,277 @@ describe('session pointer transaction', () => {
609
809
  await rm(cwd, { recursive: true, force: true });
610
810
  }
611
811
  });
812
+ it('covers the process identity decision table through pointer and lock inspection', async () => {
813
+ const hashA = 'aaa'.repeat(21) + 'a';
814
+ const hashB = 'bbb'.repeat(21) + 'b';
815
+ const identityOwner = (identity) => validLockOwner({
816
+ pid: 4242,
817
+ version: 2,
818
+ platform: identity.platform,
819
+ process_identity: identity,
820
+ });
821
+ const legacyOwner = (platform = 'linux', startTicks = 1) => validLockOwner({
822
+ pid: 4242,
823
+ platform,
824
+ pid_start_ticks: startTicks,
825
+ });
826
+ const identitylessOwner = (platform) => validLockOwner({
827
+ pid: 4242,
828
+ platform,
829
+ pid_start_ticks: undefined,
830
+ });
831
+ const identity = (platform, birth, cmdline_hash) => ({
832
+ kind: 'identity',
833
+ identity: { platform, birth, ...(cmdline_hash ? { cmdline_hash } : {}) },
834
+ });
835
+ const classifierCases = [
836
+ {
837
+ name: 'usable match',
838
+ recordedState: {
839
+ platform: 'linux', pid_start_ticks: 1, identity_schema_version: 2,
840
+ process_identity: { platform: 'linux', birth: '1' },
841
+ },
842
+ runtimePlatform: 'linux', probePid: 'alive', observations: [identity('linux', '1')],
843
+ lockOwner: identityOwner({ platform: 'linux', birth: '1' }),
844
+ expectedPointerStatus: 'usable', expectedLockStatus: 'live',
845
+ },
846
+ {
847
+ name: 'darwin match',
848
+ recordedState: { identity_schema_version: 2, process_identity: { platform: 'darwin', birth: '1234.567' } },
849
+ runtimePlatform: 'darwin', probePid: 'alive', observations: [identity('darwin', '1234.567')],
850
+ lockOwner: identityOwner({ platform: 'darwin', birth: '1234.567' }),
851
+ expectedPointerStatus: 'usable', expectedLockStatus: 'live',
852
+ },
853
+ {
854
+ name: 'windows exact FILETIME',
855
+ recordedState: { identity_schema_version: 2, process_identity: { platform: 'win32', birth: '132580896000000000' } },
856
+ runtimePlatform: 'win32', probePid: 'alive', observations: [identity('win32', '132580896000000000')],
857
+ lockOwner: identityOwner({ platform: 'win32', birth: '132580896000000000' }),
858
+ expectedPointerStatus: 'usable', expectedLockStatus: 'live',
859
+ },
860
+ {
861
+ name: 'dead PID',
862
+ recordedState: { platform: 'linux', pid_start_ticks: 1 },
863
+ runtimePlatform: 'linux', probePid: 'dead', observations: [],
864
+ lockOwner: legacyOwner(),
865
+ expectedPointerStatus: 'stale-dead', expectedLockStatus: 'dead',
866
+ },
867
+ {
868
+ name: 'birth mismatch',
869
+ recordedState: { platform: 'linux', pid_start_ticks: 1 },
870
+ runtimePlatform: 'linux', probePid: 'alive', observations: [identity('linux', '2'), identity('linux', '2')],
871
+ lockOwner: legacyOwner(),
872
+ expectedPointerStatus: 'stale-dead', expectedLockStatus: 'reused',
873
+ },
874
+ {
875
+ name: 'TOCTOU retry agree',
876
+ recordedState: { platform: 'linux', pid_start_ticks: 1 },
877
+ runtimePlatform: 'linux', probePid: 'alive', observations: [identity('linux', '2'), identity('linux', '1')],
878
+ lockOwner: legacyOwner(),
879
+ expectedPointerStatus: 'usable', expectedLockStatus: 'live',
880
+ },
881
+ {
882
+ name: 'TOCTOU retry disagree',
883
+ recordedState: { platform: 'linux', pid_start_ticks: 1 },
884
+ runtimePlatform: 'linux', probePid: 'alive', observations: [identity('linux', '2'), identity('linux', '3')],
885
+ lockOwner: legacyOwner(),
886
+ expectedPointerStatus: 'stale-dead', expectedLockStatus: 'reused',
887
+ },
888
+ {
889
+ name: 'EPERM probePid',
890
+ recordedState: { platform: 'linux', pid_start_ticks: 1 },
891
+ runtimePlatform: 'linux', probePid: 'indeterminate', observations: [],
892
+ lockOwner: legacyOwner(),
893
+ expectedPointerStatus: 'identity-indeterminate', expectedLockStatus: 'identity-indeterminate',
894
+ },
895
+ {
896
+ name: 'provider denied',
897
+ recordedState: { platform: 'linux', pid_start_ticks: 1 },
898
+ runtimePlatform: 'linux', probePid: 'alive', observations: [{ kind: 'denied' }],
899
+ lockOwner: legacyOwner(),
900
+ expectedPointerStatus: 'identity-indeterminate', expectedLockStatus: 'identity-indeterminate',
901
+ },
902
+ {
903
+ name: 'provider error',
904
+ recordedState: { platform: 'linux', pid_start_ticks: 1 },
905
+ runtimePlatform: 'linux', probePid: 'alive', observations: [{ kind: 'error' }],
906
+ lockOwner: legacyOwner(),
907
+ expectedPointerStatus: 'identity-indeterminate', expectedLockStatus: 'identity-indeterminate',
908
+ },
909
+ {
910
+ name: 'provider unsupported',
911
+ recordedState: { identity_schema_version: 2, process_identity: { platform: 'freebsd', birth: '1' } },
912
+ runtimePlatform: 'freebsd', probePid: 'alive', observations: [{ kind: 'unsupported' }],
913
+ lockOwner: identityOwner({ platform: 'freebsd', birth: '1' }),
914
+ expectedPointerStatus: 'identity-indeterminate', expectedLockStatus: 'identity-indeterminate',
915
+ },
916
+ {
917
+ name: 'foreign platform',
918
+ recordedState: { identity_schema_version: 2, process_identity: { platform: 'darwin', birth: '1' } },
919
+ runtimePlatform: 'linux', probePid: 'alive', observations: [identity('linux', '1')],
920
+ lockOwner: identityOwner({ platform: 'darwin', birth: '1' }),
921
+ expectedPointerStatus: 'identity-indeterminate', expectedLockStatus: 'identity-indeterminate',
922
+ },
923
+ {
924
+ name: 'cmdline hash match',
925
+ recordedState: {
926
+ identity_schema_version: 2,
927
+ process_identity: { platform: 'linux', birth: '1', cmdline_hash: hashA },
928
+ },
929
+ runtimePlatform: 'linux', probePid: 'alive', observations: [identity('linux', '1', hashA)],
930
+ lockOwner: identityOwner({ platform: 'linux', birth: '1', cmdline_hash: hashA }),
931
+ expectedPointerStatus: 'usable', expectedLockStatus: 'live',
932
+ },
933
+ {
934
+ name: 'cmdline hash mismatch',
935
+ recordedState: {
936
+ identity_schema_version: 2,
937
+ process_identity: { platform: 'linux', birth: '1', cmdline_hash: hashA },
938
+ },
939
+ runtimePlatform: 'linux', probePid: 'alive', observations: [identity('linux', '1', hashB)],
940
+ lockOwner: identityOwner({ platform: 'linux', birth: '1', cmdline_hash: hashA }),
941
+ expectedPointerStatus: 'identity-indeterminate', expectedLockStatus: 'identity-indeterminate',
942
+ },
943
+ {
944
+ name: 'legacy v1 upgrade Linux',
945
+ recordedState: { platform: 'linux', pid_start_ticks: 111 },
946
+ runtimePlatform: 'linux', probePid: 'alive', observations: [identity('linux', '111')],
947
+ lockOwner: legacyOwner('linux', 111),
948
+ expectedPointerStatus: 'usable', expectedLockStatus: 'live',
949
+ },
950
+ {
951
+ name: 'legacy v1 mismatch Linux',
952
+ recordedState: { platform: 'linux', pid_start_ticks: 111 },
953
+ runtimePlatform: 'linux', probePid: 'alive', observations: [identity('linux', '222'), identity('linux', '222')],
954
+ lockOwner: legacyOwner('linux', 111),
955
+ expectedPointerStatus: 'stale-dead', expectedLockStatus: 'reused',
956
+ },
957
+ {
958
+ name: 'legacy identity-less live non-Linux',
959
+ recordedState: { platform: 'darwin' },
960
+ runtimePlatform: 'darwin', probePid: 'alive', observations: [{ kind: 'unsupported' }],
961
+ lockOwner: identitylessOwner('darwin'),
962
+ expectedPointerStatus: 'identity-indeterminate', expectedLockStatus: 'identity-indeterminate',
963
+ },
964
+ {
965
+ name: 'future schema v3',
966
+ recordedState: { identity_schema_version: 3, process_identity: { platform: 'linux', birth: '1' } },
967
+ runtimePlatform: 'linux', probePid: 'alive', observations: [identity('linux', '1')],
968
+ lockOwner: identitylessOwner('darwin'),
969
+ expectedPointerStatus: 'identity-indeterminate', expectedLockStatus: 'identity-indeterminate',
970
+ },
971
+ {
972
+ name: 'malformed JSON',
973
+ rawPointer: '{not-json',
974
+ runtimePlatform: 'linux', probePid: 'alive', observations: [], lockOwner: 'malformed',
975
+ expectedPointerStatus: 'malformed', expectedLockStatus: 'malformed',
976
+ },
977
+ {
978
+ name: 'same-second Darwin collision',
979
+ recordedState: { identity_schema_version: 2, process_identity: { platform: 'darwin', birth: '1000.500' } },
980
+ runtimePlatform: 'darwin', probePid: 'alive', observations: [identity('darwin', '1000.999'), identity('darwin', '1000.999')],
981
+ lockOwner: identityOwner({ platform: 'darwin', birth: '1000.500' }),
982
+ expectedPointerStatus: 'stale-dead', expectedLockStatus: 'reused',
983
+ },
984
+ {
985
+ name: 'absent pointer',
986
+ runtimePlatform: 'linux', probePid: 'alive', observations: [],
987
+ expectedPointerStatus: 'absent', expectedLockStatus: 'absent',
988
+ },
989
+ ];
990
+ for (const testCase of classifierCases) {
991
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-classifier-'));
992
+ try {
993
+ const context = resolveSessionPointerContext(cwd);
994
+ await mkdir(context.baseStateDir, { recursive: true });
995
+ if (testCase.rawPointer !== undefined) {
996
+ await writeFile(context.sessionPath, testCase.rawPointer, 'utf-8');
997
+ }
998
+ else if (testCase.recordedState) {
999
+ await writeFile(context.sessionPath, JSON.stringify({
1000
+ session_id: 'sess-classifier',
1001
+ started_at: '2026-07-14T00:00:00.000Z',
1002
+ cwd,
1003
+ pid: 4242,
1004
+ ...testCase.recordedState,
1005
+ }), 'utf-8');
1006
+ }
1007
+ if (testCase.lockOwner !== undefined) {
1008
+ await mkdir(context.lockPath, { recursive: true });
1009
+ await writeFile(join(context.lockPath, 'owner.json'), testCase.lockOwner === 'malformed' ? '{not-json' : JSON.stringify(testCase.lockOwner), 'utf-8');
1010
+ }
1011
+ let observerCalls = 0;
1012
+ const observer = () => {
1013
+ const index = observerCalls++;
1014
+ return testCase.observations[Math.min(index, Math.max(testCase.observations.length - 1, 0))] ?? { kind: 'error' };
1015
+ };
1016
+ await withPointerDependencies({
1017
+ runtimePlatform: testCase.runtimePlatform,
1018
+ probePid: () => testCase.probePid,
1019
+ observeProcess: observer,
1020
+ }, async () => {
1021
+ assert.equal((await readSessionPointer(context)).status, testCase.expectedPointerStatus, testCase.name);
1022
+ });
1023
+ let lockObserverCalls = 0;
1024
+ const lockObserver = () => {
1025
+ const index = lockObserverCalls++;
1026
+ return testCase.observations[Math.min(index, Math.max(testCase.observations.length - 1, 0))] ?? { kind: 'error' };
1027
+ };
1028
+ await withPointerDependencies({
1029
+ runtimePlatform: testCase.runtimePlatform,
1030
+ probePid: () => testCase.probePid,
1031
+ observeProcess: lockObserver,
1032
+ }, async () => {
1033
+ assert.equal((await inspectSessionPointerLock(cwd)).status, testCase.expectedLockStatus, testCase.name);
1034
+ });
1035
+ }
1036
+ finally {
1037
+ await rm(cwd, { recursive: true, force: true });
1038
+ }
1039
+ }
1040
+ });
1041
+ it('rejects every schema version other than v2 as identity-indeterminate', async () => {
1042
+ const schemaVersions = [0, 1, -1, null, '2', 'bad', 3];
1043
+ for (const schemaVersion of schemaVersions) {
1044
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-identity-schema-rejection-'));
1045
+ try {
1046
+ const context = resolveSessionPointerContext(cwd);
1047
+ await mkdir(context.baseStateDir, { recursive: true });
1048
+ await writeFile(context.sessionPath, JSON.stringify({
1049
+ session_id: 'sess-schema-rejection',
1050
+ started_at: '2026-07-14T00:00:00.000Z',
1051
+ cwd,
1052
+ pid: 4242,
1053
+ platform: 'linux',
1054
+ pid_start_ticks: 1,
1055
+ identity_schema_version: schemaVersion,
1056
+ }), 'utf-8');
1057
+ await mkdir(context.lockPath, { recursive: true });
1058
+ await writeFile(join(context.lockPath, 'owner.json'), JSON.stringify(validLockOwner({
1059
+ pid: 4242,
1060
+ platform: 'darwin',
1061
+ pid_start_ticks: undefined,
1062
+ })), 'utf-8');
1063
+ await withPointerDependencies({
1064
+ runtimePlatform: 'linux',
1065
+ probePid: () => 'alive',
1066
+ observeProcess: () => matchingObservation('linux'),
1067
+ }, async () => {
1068
+ assert.equal((await readSessionPointer(context)).status, 'identity-indeterminate', String(schemaVersion));
1069
+ });
1070
+ await withPointerDependencies({
1071
+ runtimePlatform: 'linux',
1072
+ probePid: () => 'alive',
1073
+ observeProcess: () => matchingObservation('linux'),
1074
+ }, async () => {
1075
+ assert.equal((await inspectSessionPointerLock(cwd)).status, 'identity-indeterminate', String(schemaVersion));
1076
+ });
1077
+ }
1078
+ finally {
1079
+ await rm(cwd, { recursive: true, force: true });
1080
+ }
1081
+ }
1082
+ });
612
1083
  it('maps only ESRCH to a definitely dead default PID probe', () => {
613
1084
  const alive = __createDefaultPidProbeForTests(() => { });
614
1085
  const esrch = __createDefaultPidProbeForTests(() => { throw codedError('ESRCH'); });
@@ -626,11 +1097,11 @@ describe('session pointer transaction', () => {
626
1097
  it('never reaps dead, reused, indeterminate, missing, or malformed lock evidence', async () => {
627
1098
  const cases = [
628
1099
  { name: 'dead', owner: validLockOwner(), probePid: 'dead', expected: 'dead' },
629
- { name: 'reused', owner: validLockOwner(), probePid: 'alive', identity: { status: 'reused', startTicks: 2 }, expected: 'reused' },
1100
+ { name: 'reused', owner: validLockOwner(), probePid: 'alive', observation: { kind: 'identity', identity: { platform: 'linux', birth: '2' } }, expected: 'reused' },
630
1101
  { name: 'indeterminate-probe', owner: validLockOwner(), probePid: 'indeterminate', expected: 'identity-indeterminate' },
631
- { name: 'indeterminate-identity', owner: validLockOwner(), probePid: 'alive', identity: { status: 'indeterminate' }, expected: 'identity-indeterminate' },
632
- { name: 'unsubstantiated-reuse', owner: validLockOwner(), probePid: 'alive', identity: { status: 'reused', startTicks: 1 }, expected: 'identity-indeterminate' },
633
- { name: 'missing-required-hash', owner: validLockOwner({ pid_cmdline_hash: 'a'.repeat(64) }), probePid: 'alive', identity: matchingProcessIdentity(), expected: 'identity-indeterminate' },
1102
+ { name: 'indeterminate-identity', owner: validLockOwner(), probePid: 'alive', observation: { kind: 'error' }, expected: 'identity-indeterminate' },
1103
+ { name: 'foreign-identity', owner: validLockOwner(), probePid: 'alive', observation: { kind: 'identity', identity: { platform: 'darwin', birth: '1' } }, expected: 'identity-indeterminate' },
1104
+ { name: 'missing-required-hash', owner: validLockOwner({ pid_cmdline_hash: 'a'.repeat(64) }), probePid: 'alive', observation: matchingObservation(), expected: 'identity-indeterminate' },
634
1105
  { name: 'missing', probePid: 'alive', expected: 'missing' },
635
1106
  { name: 'malformed', owner: { version: 1 }, probePid: 'alive', expected: 'malformed' },
636
1107
  ];
@@ -646,7 +1117,7 @@ describe('session pointer transaction', () => {
646
1117
  await withPointerDependencies({
647
1118
  token: () => TEST_TOKEN,
648
1119
  probePid: () => testCase.probePid,
649
- readProcessIdentity: () => testCase.identity ?? matchingProcessIdentity(),
1120
+ observeProcess: () => testCase.observation ?? matchingObservation(),
650
1121
  }, async () => {
651
1122
  await assert.rejects(writeSessionStart(cwd, 'sess-lock', { platform: 'win32' }), (error) => isSessionPointerLaunchAbort(error)
652
1123
  && error.code === 'session_pointer_lock_recovery_required'
@@ -659,335 +1130,2629 @@ describe('session pointer transaction', () => {
659
1130
  }
660
1131
  }
661
1132
  });
662
- it('retries a positively matching live owner with the bounded 25/50/100 schedule before timing out', async () => {
663
- const cwd = await mkdtemp(join(tmpdir(), 'omx-session-lock-timeout-'));
1133
+ it('atomically quarantines dead pre-rename temporary owner evidence and stays idempotent', async () => {
1134
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-lock-recovery-dead-temp-'));
1135
+ try {
1136
+ const context = resolveSessionPointerContext(cwd);
1137
+ await mkdir(context.lockPath, { recursive: true });
1138
+ await writeFile(join(context.lockPath, `owner.${TEST_TOKEN}.tmp`), JSON.stringify(validLockOwner()), 'utf-8');
1139
+ await withPointerDependencies({ token: () => SUCCESSOR_TOKEN, probePid: () => 'dead' }, async () => {
1140
+ const inspected = await inspectSessionPointerLock(cwd);
1141
+ assert.equal(inspected.status, 'dead');
1142
+ assert.equal(inspected.evidenceSource, 'owner-temp');
1143
+ assert.equal(inspected.safeToRecover, true);
1144
+ const recovered = await recoverSessionPointerLock(cwd);
1145
+ assert.equal(recovered.recovered, true);
1146
+ assert.equal(recovered.action, 'quarantined');
1147
+ assert.equal(existsSync(context.lockPath), false);
1148
+ assert.equal(existsSync(recovered.quarantinePath), true);
1149
+ const repeated = await recoverSessionPointerLock(cwd);
1150
+ assert.equal(repeated.recovered, false);
1151
+ assert.equal(repeated.action, 'none');
1152
+ assert.equal(repeated.status, 'absent');
1153
+ });
1154
+ }
1155
+ finally {
1156
+ await rm(cwd, { recursive: true, force: true });
1157
+ }
1158
+ });
1159
+ it('atomically quarantines dead canonical owner evidence and stays idempotent', async () => {
1160
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-lock-recovery-preserved-'));
1161
+ try {
1162
+ await assertDurableDirectoryRecovery(cwd);
1163
+ }
1164
+ finally {
1165
+ await rm(cwd, { recursive: true, force: true });
1166
+ }
1167
+ });
1168
+ it('launches only after explicit recovery of a dead canonical owner lock', async () => {
1169
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-lock-recover-launch-'));
664
1170
  try {
1171
+ const context = resolveSessionPointerContext(cwd);
665
1172
  await writeLockOwner(cwd, validLockOwner());
666
- const delays = [];
667
- let now = 0;
1173
+ await withPointerDependencies({ token: () => SUCCESSOR_TOKEN, probePid: () => 'dead' }, async () => {
1174
+ await assert.rejects(writeSessionStart(cwd, 'sess-blocked', { platform: 'win32' }), (error) => isSessionPointerLaunchAbort(error)
1175
+ && error.code === 'session_pointer_lock_recovery_required'
1176
+ && error.lockOwnerStatus === 'dead');
1177
+ assert.equal(existsSync(context.lockPath), true);
1178
+ const recovered = await recoverSessionPointerLock(cwd);
1179
+ assert.equal(recovered.recovered, true);
1180
+ assert.equal(recovered.action, 'quarantined');
1181
+ await writeSessionStart(cwd, 'sess-after-recovery', { platform: 'win32' });
1182
+ assert.equal((await readSessionState(cwd))?.session_id, 'sess-after-recovery');
1183
+ assert.equal(existsSync(context.lockPath), false);
1184
+ });
1185
+ }
1186
+ finally {
1187
+ await rm(cwd, { recursive: true, force: true });
1188
+ }
1189
+ });
1190
+ it('does not claim a successor lock when the inspected dead canonical owner is displaced before the exact rename', async () => {
1191
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-lock-recovery-canonical-race-'));
1192
+ try {
1193
+ const context = resolveSessionPointerContext(cwd);
1194
+ const ownerPath = join(context.lockPath, 'owner.json');
1195
+ const successorTemp = join(context.lockPath, `owner.${SUCCESSOR_TOKEN}.tmp`);
1196
+ const displacedPath = `${context.lockPath}.displaced`;
1197
+ const staleOwner = JSON.stringify(validLockOwner());
1198
+ const successorOwner = JSON.stringify(validLockOwner({ token: SUCCESSOR_TOKEN }));
1199
+ await writeLockOwner(cwd, validLockOwner());
1200
+ let displaced = false;
668
1201
  await withPointerDependencies({
669
- nowMs: () => now,
670
- sleep: async (ms) => { delays.push(ms); now += ms; },
671
- token: () => TEST_TOKEN,
672
- probePid: () => 'alive',
673
- readProcessIdentity: () => matchingProcessIdentity(),
1202
+ token: () => SUCCESSOR_TOKEN,
1203
+ probePid: () => 'dead',
1204
+ atomicRenameNoReplace: async (from, to) => {
1205
+ if (from === context.lockPath && to === `${context.lockPath}.parked-lock-${SUCCESSOR_TOKEN}`) {
1206
+ displaced = true;
1207
+ await rename(from, displacedPath);
1208
+ await mkdir(context.lockPath);
1209
+ await writeFile(successorTemp, successorOwner, 'utf-8');
1210
+ return 'not-moved';
1211
+ }
1212
+ if (existsSync(to))
1213
+ return 'not-moved';
1214
+ await rename(from, to);
1215
+ return 'moved';
1216
+ },
674
1217
  }, async () => {
675
- await assert.rejects(writeSessionStart(cwd, 'sess-timeout', { platform: 'win32' }), (error) => isSessionPointerLaunchAbort(error)
676
- && error.code === 'session_pointer_lock_timeout'
677
- && error.lockOwnerStatus === 'live');
1218
+ const recovered = await recoverSessionPointerLock(cwd);
1219
+ assert.equal(recovered.recovered, false);
1220
+ assert.equal(recovered.action, 'none');
1221
+ assert.match(recovered.reason, /Atomic recovery move|recovery checkpoint/i);
678
1222
  });
679
- assert.deepEqual(delays.slice(0, 3), [25, 50, 100]);
680
- assert.equal(delays.at(-1), 25);
681
- assert.equal(existsSync(resolveSessionPointerContext(cwd).lockPath), true);
1223
+ assert.equal(displaced, true);
1224
+ assert.equal(await readFile(join(displacedPath, 'owner.json'), 'utf-8'), staleOwner);
1225
+ assert.deepEqual(await readdir(context.lockPath), [`owner.${SUCCESSOR_TOKEN}.tmp`]);
1226
+ assert.equal(await readFile(successorTemp, 'utf-8'), successorOwner);
1227
+ assert.equal(existsSync(ownerPath), false);
682
1228
  }
683
1229
  finally {
684
1230
  await rm(cwd, { recursive: true, force: true });
685
1231
  }
686
1232
  });
687
- it('rolls back only its unpublished owner artifacts and reports rollback ambiguity', async () => {
688
- const cwd = await mkdtemp(join(tmpdir(), 'omx-session-owner-publish-'));
1233
+ it('rolls back a claim that lands on a displaced successor canonical owner', async () => {
1234
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-lock-recovery-canonical-claim-race-'));
689
1235
  try {
690
1236
  const context = resolveSessionPointerContext(cwd);
1237
+ const ownerPath = join(context.lockPath, 'owner.json');
1238
+ const parkedPath = `${context.lockPath}.parked-lock-${SUCCESSOR_TOKEN}`;
1239
+ const quarantinePath = `${context.lockPath}.quarantine.${TEST_TOKEN}.${SUCCESSOR_TOKEN}`;
1240
+ const checkpointPath = `${context.lockPath}.recovery.${TEST_TOKEN}.${SUCCESSOR_TOKEN}.json`;
1241
+ const staleOwner = JSON.stringify(validLockOwner());
1242
+ const successorPid = process.pid + 100_000;
1243
+ const successorOwner = JSON.stringify(validLockOwner({ token: SUCCESSOR_TOKEN, pid: successorPid }));
1244
+ await writeLockOwner(cwd, validLockOwner());
1245
+ let moved = false;
1246
+ let rollbackAttempted = false;
691
1247
  await withPointerDependencies({
692
- token: () => TEST_TOKEN,
693
- fs: {
694
- writeFile: async (path, data, options) => {
695
- if (path.endsWith(`owner.${TEST_TOKEN}.tmp`))
696
- throw new Error('owner write failure');
697
- await writeFile(path, data, options);
698
- },
1248
+ token: () => SUCCESSOR_TOKEN,
1249
+ probePid: () => 'dead',
1250
+ atomicRenameNoReplace: async (from, to) => {
1251
+ if (from === context.lockPath && to === parkedPath) {
1252
+ await rename(from, to);
1253
+ await mkdir(context.lockPath);
1254
+ await writeFile(ownerPath, successorOwner, 'utf-8');
1255
+ moved = true;
1256
+ return 'moved';
1257
+ }
1258
+ if (from === parkedPath && to === context.lockPath) {
1259
+ rollbackAttempted = true;
1260
+ return 'not-moved';
1261
+ }
1262
+ if (existsSync(to))
1263
+ return 'not-moved';
1264
+ await rename(from, to);
1265
+ return 'moved';
699
1266
  },
700
1267
  }, async () => {
701
- await assert.rejects(writeSessionStart(cwd, 'sess-owner-publish', { platform: 'win32' }), (error) => isSessionPointerLaunchAbort(error)
702
- && error.code === 'session_pointer_io_failure'
703
- && error.operation === 'lock-owner-publish');
1268
+ const recovered = await recoverSessionPointerLock(cwd);
1269
+ assert.equal(recovered.recovered, false);
1270
+ assert.equal(recovered.action, 'none');
1271
+ assert.match(recovered.reason, /captured object|token-bound|source pathname/i);
1272
+ });
1273
+ assert.equal(moved, true);
1274
+ assert.equal(rollbackAttempted, true);
1275
+ assert.equal(await readFile(ownerPath, 'utf-8'), successorOwner);
1276
+ assert.equal(await readFile(join(parkedPath, 'owner.json'), 'utf-8'), staleOwner);
1277
+ assert.equal(await readFile(quarantinePath, 'utf-8'), staleOwner);
1278
+ assert.equal(existsSync(checkpointPath), true);
1279
+ }
1280
+ finally {
1281
+ await rm(cwd, { recursive: true, force: true });
1282
+ }
1283
+ });
1284
+ it('live canonical successor survives a refused recovery claim and stays recognizable, releasable, and acquirable', async () => {
1285
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-lock-recovery-live-successor-'));
1286
+ try {
1287
+ const context = resolveSessionPointerContext(cwd);
1288
+ const ownerPath = join(context.lockPath, 'owner.json');
1289
+ const parkedPath = `${context.lockPath}.parked-lock-${SUCCESSOR_TOKEN}`;
1290
+ const successorPid = process.pid + 100_000;
1291
+ const successorOwner = JSON.stringify(validLockOwner({ token: SUCCESSOR_TOKEN, pid: successorPid }));
1292
+ await writeLockOwner(cwd, validLockOwner());
1293
+ let rollbackAttempted = false;
1294
+ await withPointerDependencies({
1295
+ token: () => SUCCESSOR_TOKEN,
1296
+ probePid: (pid) => pid === successorPid ? 'alive' : 'dead',
1297
+ observeProcess: () => matchingObservation(),
1298
+ atomicRenameNoReplace: async (from, to) => {
1299
+ if (from === context.lockPath && to === parkedPath) {
1300
+ await rename(from, to);
1301
+ await mkdir(context.lockPath);
1302
+ await writeFile(ownerPath, successorOwner, 'utf-8');
1303
+ return 'moved';
1304
+ }
1305
+ if (from === parkedPath && to === context.lockPath) {
1306
+ rollbackAttempted = true;
1307
+ return 'not-moved';
1308
+ }
1309
+ if (existsSync(to))
1310
+ return 'not-moved';
1311
+ await rename(from, to);
1312
+ return 'moved';
1313
+ },
1314
+ }, async () => {
1315
+ const refused = await recoverSessionPointerLock(cwd);
1316
+ assert.equal(refused.recovered, false);
1317
+ assert.equal(refused.action, 'none');
1318
+ const inspected = await inspectSessionPointerLock(cwd);
1319
+ assert.equal(inspected.status, 'live');
1320
+ assert.equal(inspected.safeToRecover, false);
1321
+ assert.equal(await readFile(ownerPath, 'utf-8'), successorOwner);
1322
+ assert.deepEqual(await __releasePointerLockForTests(cwd, SUCCESSOR_TOKEN), []);
1323
+ assert.equal(existsSync(context.lockPath), false);
1324
+ const started = await writeSessionStart(cwd, 'sess-after-survival', { platform: 'win32' });
1325
+ assert.equal(started.session_id, 'sess-after-survival');
1326
+ assert.equal(existsSync(context.lockPath), false);
1327
+ });
1328
+ assert.equal(rollbackAttempted, true);
1329
+ assert.equal(existsSync(parkedPath), true);
1330
+ }
1331
+ finally {
1332
+ await rm(cwd, { recursive: true, force: true });
1333
+ }
1334
+ });
1335
+ it('fails closed without mutating a foreign target when the lock directory is replaced by a symlink before the claim rename', async () => {
1336
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-lock-recovery-lock-symlink-'));
1337
+ const foreign = await mkdtemp(join(tmpdir(), 'omx-session-lock-recovery-foreign-'));
1338
+ try {
1339
+ const context = resolveSessionPointerContext(cwd);
1340
+ const ownerPath = join(context.lockPath, 'owner.json');
1341
+ const foreignOwner = join(foreign, 'owner.json');
1342
+ const marker = JSON.stringify(validLockOwner({ token: SUCCESSOR_TOKEN }));
1343
+ let replaced = false;
1344
+ await writeLockOwner(cwd, validLockOwner());
1345
+ await withPointerDependencies({ token: () => SUCCESSOR_TOKEN, probePid: () => 'dead', fs: { link: async (from, to) => {
1346
+ if (!replaced && from === ownerPath) {
1347
+ replaced = true;
1348
+ await rm(context.lockPath, { recursive: true });
1349
+ await writeFile(foreignOwner, marker);
1350
+ await symlink(foreign, context.lockPath);
1351
+ }
1352
+ await link(from, to);
1353
+ } } }, async () => assert.equal((await recoverSessionPointerLock(cwd)).recovered, false));
1354
+ assert.equal(await readFile(foreignOwner, 'utf-8'), marker);
1355
+ assert.equal((await readdir(foreign)).some((entry) => entry.includes('.quarantine.')), false);
1356
+ }
1357
+ finally {
1358
+ await rm(cwd, { recursive: true, force: true });
1359
+ await rm(foreign, { recursive: true, force: true });
1360
+ }
1361
+ });
1362
+ it('does not follow an evidence symlink swapped in before the claim rename', async () => {
1363
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-lock-recovery-evidence-symlink-'));
1364
+ try {
1365
+ const context = resolveSessionPointerContext(cwd);
1366
+ const ownerPath = join(context.lockPath, 'owner.json');
1367
+ const foreign = join(cwd, 'foreign-owner');
1368
+ const marker = 'foreign evidence marker';
1369
+ let replaced = false;
1370
+ await writeLockOwner(cwd, validLockOwner());
1371
+ await writeFile(foreign, marker);
1372
+ await withPointerDependencies({ token: () => SUCCESSOR_TOKEN, probePid: () => 'dead', fs: { link: async (from, to) => {
1373
+ if (!replaced && from === ownerPath) {
1374
+ replaced = true;
1375
+ await rm(ownerPath);
1376
+ await symlink(foreign, ownerPath);
1377
+ }
1378
+ await link(from, to);
1379
+ } } }, async () => assert.equal((await recoverSessionPointerLock(cwd)).recovered, false));
1380
+ assert.equal(await readFile(foreign, 'utf-8'), marker);
1381
+ assert.equal(await readlink(ownerPath), foreign);
1382
+ }
1383
+ finally {
1384
+ await rm(cwd, { recursive: true, force: true });
1385
+ }
1386
+ });
1387
+ it('preserves a non-file evidence replacement as exact recovery residue without clobbering', async () => {
1388
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-lock-recovery-evidence-directory-'));
1389
+ try {
1390
+ const context = resolveSessionPointerContext(cwd);
1391
+ const ownerPath = join(context.lockPath, 'owner.json');
1392
+ const marker = join(ownerPath, 'marker');
1393
+ let replaced = false;
1394
+ await writeLockOwner(cwd, validLockOwner());
1395
+ await withPointerDependencies({ token: () => SUCCESSOR_TOKEN, probePid: () => 'dead', fs: { link: async (from, to) => {
1396
+ if (!replaced && from === ownerPath) {
1397
+ replaced = true;
1398
+ await rm(ownerPath);
1399
+ await mkdir(ownerPath);
1400
+ await writeFile(marker, 'directory marker');
1401
+ }
1402
+ await link(from, to);
1403
+ } } }, async () => assert.equal((await recoverSessionPointerLock(cwd)).recovered, false));
1404
+ assert.equal(await readFile(marker, 'utf-8'), 'directory marker');
1405
+ }
1406
+ finally {
1407
+ await rm(cwd, { recursive: true, force: true });
1408
+ }
1409
+ });
1410
+ it('retries a transient token-bound park collision without allocating a second parked name', async () => {
1411
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-lock-recovery-park-retry-'));
1412
+ try {
1413
+ const context = resolveSessionPointerContext(cwd);
1414
+ const parkedPath = `${context.lockPath}.parked-lock-${SUCCESSOR_TOKEN}`;
1415
+ const quarantinePath = `${context.lockPath}.quarantine.${TEST_TOKEN}.${SUCCESSOR_TOKEN}`;
1416
+ const checkpointPath = `${context.lockPath}.recovery.${TEST_TOKEN}.${SUCCESSOR_TOKEN}.json`;
1417
+ const foreignMarker = 'foreign transient park destination';
1418
+ const parkAttempts = [];
1419
+ let tokenCalls = 0;
1420
+ let collision = true;
1421
+ await writeLockOwner(cwd, validLockOwner());
1422
+ await withPointerDependencies({
1423
+ token: () => {
1424
+ tokenCalls += 1;
1425
+ return SUCCESSOR_TOKEN;
1426
+ },
1427
+ probePid: () => 'dead',
1428
+ atomicRenameNoReplace: async (from, to) => {
1429
+ if (from === context.lockPath && to === parkedPath) {
1430
+ parkAttempts.push(to);
1431
+ if (collision) {
1432
+ collision = false;
1433
+ await mkdir(to);
1434
+ await writeFile(join(to, 'foreign'), foreignMarker, 'utf-8');
1435
+ return 'not-moved';
1436
+ }
1437
+ }
1438
+ if (existsSync(to))
1439
+ return 'not-moved';
1440
+ await rename(from, to);
1441
+ return 'moved';
1442
+ },
1443
+ }, async () => {
1444
+ const refused = await recoverSessionPointerLock(cwd);
1445
+ assert.equal(refused.recovered, false);
1446
+ assert.equal(refused.action, 'none');
1447
+ assert.equal(existsSync(checkpointPath), true);
1448
+ assert.equal(existsSync(context.lockPath), true);
1449
+ assert.equal(await readFile(join(parkedPath, 'foreign'), 'utf-8'), foreignMarker);
1450
+ await rm(parkedPath, { recursive: true, force: true });
1451
+ const recovered = await recoverSessionPointerLock(cwd);
1452
+ assert.equal(recovered.recovered, true, recovered.reason);
1453
+ assert.equal(recovered.action, 'quarantined');
1454
+ });
1455
+ assert.equal(tokenCalls, 1);
1456
+ assert.deepEqual(parkAttempts, [parkedPath, parkedPath]);
1457
+ assert.equal(existsSync(context.lockPath), false);
1458
+ assert.equal(existsSync(parkedPath), true);
1459
+ assert.equal(existsSync(quarantinePath), true);
1460
+ assert.equal(existsSync(checkpointPath), false);
1461
+ assert.equal(existsSync(checkpointPath.replace(/\.json$/, '.completed')), true);
1462
+ }
1463
+ finally {
1464
+ await rm(cwd, { recursive: true, force: true });
1465
+ }
1466
+ });
1467
+ it('rolls back a claim when checkpoint creation fails', async () => {
1468
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-lock-recovery-checkpoint-write-fail-'));
1469
+ try {
1470
+ const context = resolveSessionPointerContext(cwd);
1471
+ await writeLockOwner(cwd, validLockOwner());
1472
+ let fail = true;
1473
+ await withPointerDependencies({ token: () => SUCCESSOR_TOKEN, probePid: () => 'dead', fs: { writeFile: async (path, data, options) => { if (fail && path.includes('.recovery.')) {
1474
+ fail = false;
1475
+ throw codedError('EBUSY');
1476
+ } await writeFile(path, data, options); } } }, async () => {
1477
+ assert.equal((await recoverSessionPointerLock(cwd)).recovered, false);
1478
+ assert.deepEqual(await readdir(context.lockPath), ['owner.json']);
1479
+ assert.equal((await recoverSessionPointerLock(cwd)).recovered, true);
1480
+ });
1481
+ }
1482
+ finally {
1483
+ await rm(cwd, { recursive: true, force: true });
1484
+ }
1485
+ });
1486
+ it('rolls back a double-EBUSY evidence park failure so a normal retry succeeds', async () => {
1487
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-lock-recovery-checkpoint-'));
1488
+ try {
1489
+ const context = resolveSessionPointerContext(cwd);
1490
+ const ownerPath = join(context.lockPath, 'owner.json');
1491
+ const quarantinePath = `${context.lockPath}.quarantine.${TEST_TOKEN}.${SUCCESSOR_TOKEN}`;
1492
+ const checkpointPath = `${context.lockPath}.recovery.${TEST_TOKEN}.${SUCCESSOR_TOKEN}.json`;
1493
+ const staleOwner = JSON.stringify(validLockOwner());
1494
+ let failures = 2;
1495
+ let tokenCalls = 0;
1496
+ let linkAttempts = 0;
1497
+ await writeLockOwner(cwd, validLockOwner());
1498
+ await withPointerDependencies({
1499
+ token: () => {
1500
+ tokenCalls += 1;
1501
+ return SUCCESSOR_TOKEN;
1502
+ },
1503
+ probePid: () => 'dead',
1504
+ fs: {
1505
+ link: async (from, to) => {
1506
+ if (to === quarantinePath && failures > 0) {
1507
+ failures -= 1;
1508
+ linkAttempts += 1;
1509
+ throw codedError('EBUSY');
1510
+ }
1511
+ await link(from, to);
1512
+ },
1513
+ },
1514
+ }, async () => {
1515
+ const first = await recoverSessionPointerLock(cwd);
1516
+ assert.equal(first.recovered, false);
1517
+ assert.equal(first.action, 'none');
1518
+ assert.equal(existsSync(checkpointPath), true);
1519
+ assert.equal(await readFile(ownerPath, 'utf-8'), staleOwner);
1520
+ assert.equal(existsSync(quarantinePath), false);
1521
+ const second = await recoverSessionPointerLock(cwd);
1522
+ assert.equal(second.recovered, false);
1523
+ assert.equal(second.action, 'none');
1524
+ assert.equal(existsSync(checkpointPath), true);
1525
+ assert.equal(await readFile(ownerPath, 'utf-8'), staleOwner);
1526
+ assert.equal(existsSync(quarantinePath), false);
1527
+ const recovered = await recoverSessionPointerLock(cwd);
1528
+ assert.equal(recovered.recovered, true, recovered.reason);
1529
+ assert.equal(recovered.action, 'quarantined');
1530
+ });
1531
+ assert.equal(tokenCalls, 1);
1532
+ assert.equal(linkAttempts, 2);
1533
+ assert.equal(existsSync(context.lockPath), false);
1534
+ assert.equal(await readFile(quarantinePath, 'utf-8'), staleOwner);
1535
+ assert.equal(existsSync(checkpointPath), false);
1536
+ assert.equal(existsSync(checkpointPath.replace(/\.json$/, '.completed')), true);
1537
+ }
1538
+ finally {
1539
+ await rm(cwd, { recursive: true, force: true });
1540
+ }
1541
+ });
1542
+ it('resumes an evidence-pending checkpoint without minting another recovery token', async () => {
1543
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-lock-recovery-resume-'));
1544
+ try {
1545
+ const context = resolveSessionPointerContext(cwd);
1546
+ const ownerPath = join(context.lockPath, 'owner.json');
1547
+ const claimPath = join(context.lockPath, `owner.${TEST_TOKEN}.${SUCCESSOR_TOKEN}.recovery`);
1548
+ const parkPath = `${context.lockPath}.parked-${SUCCESSOR_TOKEN}`;
1549
+ const owner = JSON.stringify(validLockOwner());
1550
+ await writeLockOwner(cwd, validLockOwner());
1551
+ await link(ownerPath, claimPath);
1552
+ await rename(ownerPath, parkPath);
1553
+ const parked = await lstat(parkPath);
1554
+ const lock = await lstat(context.lockPath);
1555
+ await writeFile(`${context.lockPath}.recovery.${TEST_TOKEN}.${SUCCESSOR_TOKEN}.json`, JSON.stringify({ version: 1, sourcePath: ownerPath, parkPath, lockParkPath: `${context.lockPath}.parked-lock-${SUCCESSOR_TOKEN}`, identity: { dev: parked.dev, ino: parked.ino }, lockIdentity: { dev: lock.dev, ino: lock.ino }, phase: 'evidence-pending' }));
1556
+ await withPointerDependencies({ token: () => { throw new Error('resume must reuse checkpoint token'); }, probePid: () => 'dead' }, async () => {
1557
+ const resumed = await recoverSessionPointerLock(cwd);
1558
+ assert.equal(resumed.recovered, true);
1559
+ });
1560
+ assert.equal(await readFile(`${context.lockPath}.quarantine.${TEST_TOKEN}.${SUCCESSOR_TOKEN}`, 'utf-8'), owner);
1561
+ assert.equal(existsSync(`${context.lockPath}.recovery.${TEST_TOKEN}.${SUCCESSOR_TOKEN}.json`), false);
1562
+ }
1563
+ finally {
1564
+ await rm(cwd, { recursive: true, force: true });
1565
+ }
1566
+ });
1567
+ it('resumes the actual distinct-token evidence park path after an interruption', async () => {
1568
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-lock-recovery-distinct-park-token-'));
1569
+ try {
1570
+ const context = resolveSessionPointerContext(cwd);
1571
+ const ownerPath = join(context.lockPath, 'owner.json');
1572
+ const claimToken = 'claim_token_123456789';
1573
+ const parkToken = 'park_token_123456789';
1574
+ const claimPath = join(context.lockPath, `owner.${TEST_TOKEN}.${claimToken}.recovery`);
1575
+ const parkPath = `${context.lockPath}.parked-${parkToken}`;
1576
+ await writeLockOwner(cwd, validLockOwner());
1577
+ await link(ownerPath, claimPath);
1578
+ await rename(ownerPath, parkPath);
1579
+ const parked = await lstat(parkPath);
1580
+ const lock = await lstat(context.lockPath);
1581
+ await writeFile(`${context.lockPath}.recovery.${TEST_TOKEN}.${claimToken}.json`, JSON.stringify({ version: 1, sourcePath: ownerPath, parkPath, lockParkPath: `${context.lockPath}.parked-lock-${claimToken}`, identity: { dev: parked.dev, ino: parked.ino }, lockIdentity: { dev: lock.dev, ino: lock.ino }, phase: 'evidence-pending' }));
1582
+ await withPointerDependencies({ token: () => { throw new Error('resume must not mint a replacement token'); }, probePid: () => 'dead' }, async () => assert.equal((await recoverSessionPointerLock(cwd)).recovered, true));
1583
+ assert.equal(await readFile(`${context.lockPath}.quarantine.${TEST_TOKEN}.${claimToken}`, 'utf-8'), JSON.stringify(validLockOwner()));
1584
+ }
1585
+ finally {
1586
+ await rm(cwd, { recursive: true, force: true });
1587
+ }
1588
+ });
1589
+ it('resumes a checkpoint written before evidence parking', async () => {
1590
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-lock-recovery-source-checkpoint-'));
1591
+ try {
1592
+ const context = resolveSessionPointerContext(cwd);
1593
+ const ownerPath = join(context.lockPath, 'owner.json');
1594
+ const claimPath = join(context.lockPath, `owner.${TEST_TOKEN}.${SUCCESSOR_TOKEN}.recovery`);
1595
+ const parkPath = `${context.lockPath}.parked-${SUCCESSOR_TOKEN}`;
1596
+ const checkpointPath = `${context.lockPath}.recovery.${TEST_TOKEN}.${SUCCESSOR_TOKEN}.json`;
1597
+ await writeLockOwner(cwd, validLockOwner());
1598
+ await link(ownerPath, claimPath);
1599
+ const owner = await lstat(ownerPath);
1600
+ const lock = await lstat(context.lockPath);
1601
+ await writeFile(checkpointPath, JSON.stringify({ version: 1, sourcePath: ownerPath, parkPath, lockParkPath: `${context.lockPath}.parked-lock-${SUCCESSOR_TOKEN}`, identity: { dev: owner.dev, ino: owner.ino }, lockIdentity: { dev: lock.dev, ino: lock.ino }, phase: 'evidence-pending' }));
1602
+ await withPointerDependencies({ token: () => { throw new Error('resume must not mint token'); }, probePid: () => 'dead' }, async () => assert.equal((await recoverSessionPointerLock(cwd)).recovered, true));
1603
+ assert.equal(existsSync(context.lockPath), false);
1604
+ assert.equal(existsSync(checkpointPath), false);
1605
+ }
1606
+ finally {
1607
+ await rm(cwd, { recursive: true, force: true });
1608
+ }
1609
+ });
1610
+ it('resumes after quarantine hard-link creation but before claim and park cleanup', async () => {
1611
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-lock-recovery-dual-evidence-checkpoint-'));
1612
+ try {
1613
+ const context = resolveSessionPointerContext(cwd);
1614
+ const ownerPath = join(context.lockPath, 'owner.json');
1615
+ const claimPath = join(context.lockPath, `owner.${TEST_TOKEN}.${SUCCESSOR_TOKEN}.recovery`);
1616
+ const parkPath = `${context.lockPath}.parked-${SUCCESSOR_TOKEN}`;
1617
+ const quarantinePath = `${context.lockPath}.quarantine.${TEST_TOKEN}.${SUCCESSOR_TOKEN}`;
1618
+ const checkpointPath = `${context.lockPath}.recovery.${TEST_TOKEN}.${SUCCESSOR_TOKEN}.json`;
1619
+ await writeLockOwner(cwd, validLockOwner());
1620
+ await link(ownerPath, claimPath);
1621
+ await rename(ownerPath, parkPath);
1622
+ await link(parkPath, quarantinePath);
1623
+ const parked = await lstat(parkPath);
1624
+ const lock = await lstat(context.lockPath);
1625
+ await writeFile(checkpointPath, JSON.stringify({ version: 1, sourcePath: ownerPath, parkPath, lockParkPath: `${context.lockPath}.parked-lock-${SUCCESSOR_TOKEN}`, identity: { dev: parked.dev, ino: parked.ino }, lockIdentity: { dev: lock.dev, ino: lock.ino }, phase: 'evidence-pending' }));
1626
+ await withPointerDependencies({ token: () => { throw new Error('resume must not mint token'); }, probePid: () => 'dead' }, async () => assert.equal((await recoverSessionPointerLock(cwd)).recovered, true));
1627
+ assert.equal(await readFile(quarantinePath, 'utf-8'), JSON.stringify(validLockOwner()));
1628
+ assert.equal(existsSync(checkpointPath), false);
1629
+ }
1630
+ finally {
1631
+ await rm(cwd, { recursive: true, force: true });
1632
+ }
1633
+ });
1634
+ it('reports absent recovery on a fresh workspace without a state directory', async () => {
1635
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-lock-recovery-fresh-'));
1636
+ try {
1637
+ const recovered = await recoverSessionPointerLock(cwd);
1638
+ assert.equal(recovered.recovered, false);
1639
+ assert.equal(recovered.status, 'absent');
1640
+ assert.match(recovered.reason, /No session pointer lock exists/);
1641
+ }
1642
+ finally {
1643
+ await rm(cwd, { recursive: true, force: true });
1644
+ }
1645
+ });
1646
+ it('fails closed when recovery checkpoint discovery cannot be enumerated', async () => {
1647
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-lock-recovery-checkpoint-readdir-error-'));
1648
+ try {
1649
+ const context = resolveSessionPointerContext(cwd);
1650
+ await writeLockOwner(cwd, validLockOwner());
1651
+ await withPointerDependencies({ probePid: () => 'dead', fs: { readdir: async () => { throw codedError('EACCES'); } } }, async () => {
1652
+ const recovered = await recoverSessionPointerLock(cwd);
1653
+ assert.equal(recovered.recovered, false);
1654
+ assert.equal(recovered.status, 'io-error');
1655
+ assert.match(recovered.reason, /enumerate.*checkpoints/i);
1656
+ });
1657
+ assert.deepEqual(await readdir(context.lockPath), ['owner.json']);
1658
+ }
1659
+ finally {
1660
+ await rm(cwd, { recursive: true, force: true });
1661
+ }
1662
+ });
1663
+ it('fails closed for malformed checkpoint JSON without minting a recovery token', async () => {
1664
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-lock-recovery-malformed-checkpoint-'));
1665
+ try {
1666
+ const context = resolveSessionPointerContext(cwd);
1667
+ await writeLockOwner(cwd, validLockOwner());
1668
+ const checkpointPath = `${context.lockPath}.recovery.${TEST_TOKEN}.${SUCCESSOR_TOKEN}.json`;
1669
+ await writeFile(checkpointPath, '{not json', 'utf-8');
1670
+ await withPointerDependencies({ token: () => { throw new Error('must not mint token'); }, probePid: () => 'dead' }, async () => {
1671
+ const recovered = await recoverSessionPointerLock(cwd);
1672
+ assert.equal(recovered.recovered, false);
1673
+ assert.equal(recovered.safeToRecover, false);
1674
+ });
1675
+ assert.equal(await readFile(checkpointPath, 'utf-8'), '{not json');
1676
+ assert.equal(await readFile(join(context.lockPath, 'owner.json'), 'utf-8'), JSON.stringify(validLockOwner()));
1677
+ }
1678
+ finally {
1679
+ await rm(cwd, { recursive: true, force: true });
1680
+ }
1681
+ });
1682
+ for (const [label, checkpoint] of [
1683
+ ['wrong version', { version: 2, phase: 'evidence-pending', sourcePath: 'x', parkPath: 'x', identity: { dev: 0, ino: 0 } }],
1684
+ ['wrong phase', { version: 1, phase: 'claim-pending', sourcePath: 'x', parkPath: 'x', identity: { dev: 0, ino: 0 } }],
1685
+ ['out-of-root path', { version: 1, phase: 'evidence-pending', sourcePath: '/foreign/owner.json', parkPath: '/foreign/parked', identity: { dev: 0, ino: 0 } }],
1686
+ ])
1687
+ it(`fails closed for ${label} checkpoint`, async () => {
1688
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-lock-recovery-invalid-checkpoint-'));
1689
+ try {
1690
+ const context = resolveSessionPointerContext(cwd);
1691
+ await writeLockOwner(cwd, validLockOwner());
1692
+ const path = `${context.lockPath}.recovery.${TEST_TOKEN}.${SUCCESSOR_TOKEN}.json`;
1693
+ await writeFile(path, JSON.stringify(checkpoint), 'utf-8');
1694
+ await withPointerDependencies({ token: () => { throw new Error('must not mint token'); }, probePid: () => 'dead' }, async () => assert.equal((await recoverSessionPointerLock(cwd)).recovered, false));
1695
+ assert.equal(await readFile(path, 'utf-8'), JSON.stringify(checkpoint));
1696
+ assert.equal(await readFile(join(context.lockPath, 'owner.json'), 'utf-8'), JSON.stringify(validLockOwner()));
1697
+ }
1698
+ finally {
1699
+ await rm(cwd, { recursive: true, force: true });
1700
+ }
1701
+ });
1702
+ it('refuses multiple checkpoints without mutating either', async () => {
1703
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-lock-recovery-multiple-checkpoints-'));
1704
+ try {
1705
+ const context = resolveSessionPointerContext(cwd);
1706
+ await writeLockOwner(cwd, validLockOwner());
1707
+ for (const token of [SUCCESSOR_TOKEN, FOREIGN_TOKEN])
1708
+ await writeFile(`${context.lockPath}.recovery.${TEST_TOKEN}.${token}.json`, '{}', 'utf-8');
1709
+ await withPointerDependencies({ token: () => { throw new Error('must not mint token'); }, probePid: () => 'dead' }, async () => assert.match((await recoverSessionPointerLock(cwd)).reason, /Multiple recovery checkpoints/));
1710
+ assert.equal(existsSync(join(context.lockPath, 'owner.json')), true);
1711
+ }
1712
+ finally {
1713
+ await rm(cwd, { recursive: true, force: true });
1714
+ }
1715
+ });
1716
+ it('preserves a checkpoint when its claim is missing', async () => {
1717
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-lock-recovery-missing-claim-'));
1718
+ try {
1719
+ const context = resolveSessionPointerContext(cwd);
1720
+ await mkdir(context.lockPath, { recursive: true });
1721
+ const parkPath = `${context.lockPath}.parked-${SUCCESSOR_TOKEN}`;
1722
+ await writeFile(parkPath, 'stale', 'utf-8');
1723
+ const stat = await lstat(parkPath);
1724
+ const checkpointPath = `${context.lockPath}.recovery.${TEST_TOKEN}.${SUCCESSOR_TOKEN}.json`;
1725
+ const lock = await lstat(context.lockPath);
1726
+ await writeFile(checkpointPath, JSON.stringify({ version: 1, sourcePath: join(context.lockPath, 'owner.json'), parkPath, lockParkPath: `${context.lockPath}.parked-lock-${SUCCESSOR_TOKEN}`, identity: { dev: stat.dev, ino: stat.ino }, lockIdentity: { dev: lock.dev, ino: lock.ino }, phase: 'evidence-pending' }));
1727
+ await withPointerDependencies({ token: () => { throw new Error('must not mint token'); }, probePid: () => 'dead' }, async () => assert.equal((await recoverSessionPointerLock(cwd)).recovered, false));
1728
+ assert.equal(await readFile(checkpointPath, 'utf-8').then(Boolean), true);
1729
+ assert.equal(await readFile(parkPath, 'utf-8'), 'stale');
1730
+ }
1731
+ finally {
1732
+ await rm(cwd, { recursive: true, force: true });
1733
+ }
1734
+ });
1735
+ it('refuses an advanced checkpoint when a live successor replaces the bound lock directory', async () => {
1736
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-lock-recovery-checkpoint-successor-swap-'));
1737
+ try {
1738
+ const context = resolveSessionPointerContext(cwd);
1739
+ const ownerPath = join(context.lockPath, 'owner.json');
1740
+ const claimPath = join(context.lockPath, `owner.${TEST_TOKEN}.${SUCCESSOR_TOKEN}.recovery`);
1741
+ const parkPath = `${context.lockPath}.parked-${SUCCESSOR_TOKEN}`;
1742
+ const checkpointPath = `${context.lockPath}.recovery.${TEST_TOKEN}.${SUCCESSOR_TOKEN}.json`;
1743
+ const quarantinePath = `${context.lockPath}.quarantine.${TEST_TOKEN}.${SUCCESSOR_TOKEN}`;
1744
+ await writeLockOwner(cwd, validLockOwner());
1745
+ await link(ownerPath, claimPath);
1746
+ await rename(ownerPath, parkPath);
1747
+ const parked = await lstat(parkPath);
1748
+ const lock = await lstat(context.lockPath);
1749
+ await writeFile(checkpointPath, JSON.stringify({ version: 1, sourcePath: ownerPath, parkPath, lockParkPath: `${context.lockPath}.parked-lock-${SUCCESSOR_TOKEN}`, identity: { dev: parked.dev, ino: parked.ino }, lockIdentity: { dev: lock.dev, ino: lock.ino }, phase: 'evidence-pending' }));
1750
+ await rename(parkPath, quarantinePath);
1751
+ await rm(claimPath);
1752
+ await rename(context.lockPath, `${context.lockPath}.displaced`);
1753
+ const successorPid = process.pid + 100_000;
1754
+ const successor = JSON.stringify(validLockOwner({ token: FOREIGN_TOKEN, pid: successorPid }));
1755
+ await mkdir(context.lockPath);
1756
+ await writeFile(ownerPath, successor, 'utf-8');
1757
+ await withPointerDependencies({ token: () => { throw new Error('resume must not mint'); }, probePid: (pid) => pid === successorPid ? 'alive' : 'dead', observeProcess: () => matchingObservation() }, async () => {
1758
+ const refused = await recoverSessionPointerLock(cwd);
1759
+ assert.equal(refused.recovered, false);
1760
+ assert.match(refused.reason, /recovery state/i);
1761
+ assert.deepEqual(await readdir(context.lockPath), ['owner.json']);
1762
+ assert.equal(await readFile(ownerPath, 'utf-8'), successor);
1763
+ assert.deepEqual(await __releasePointerLockForTests(cwd, FOREIGN_TOKEN), []);
1764
+ assert.equal(existsSync(context.lockPath), false);
1765
+ });
1766
+ assert.equal(await readFile(quarantinePath, 'utf-8'), JSON.stringify(validLockOwner()));
1767
+ assert.equal(existsSync(checkpointPath), true);
1768
+ }
1769
+ finally {
1770
+ await rm(cwd, { recursive: true, force: true });
1771
+ }
1772
+ });
1773
+ it('does not remove a live successor swapped in before final checkpoint lock removal', async () => {
1774
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-lock-recovery-final-successor-'));
1775
+ try {
1776
+ const context = resolveSessionPointerContext(cwd);
1777
+ const ownerPath = join(context.lockPath, 'owner.json');
1778
+ const checkpointPath = `${context.lockPath}.recovery.${TEST_TOKEN}.${SUCCESSOR_TOKEN}.json`;
1779
+ const lockPark = `${context.lockPath}.parked-lock-${SUCCESSOR_TOKEN}`;
1780
+ await writeLockOwner(cwd, validLockOwner());
1781
+ const owner = await lstat(ownerPath);
1782
+ const lock = await lstat(context.lockPath);
1783
+ await writeFile(checkpointPath, JSON.stringify({ version: 3, sourcePath: ownerPath, identity: { dev: owner.dev, ino: owner.ino }, evidenceIdentity: { dev: owner.dev, ino: owner.ino }, evidenceBytes: JSON.stringify(validLockOwner()), lockIdentity: { dev: lock.dev, ino: lock.ino }, lockParkPath: lockPark, phase: 'evidence-pending' }));
1784
+ const successor = JSON.stringify(validLockOwner({ token: FOREIGN_TOKEN, pid: process.pid + 100_000 }));
1785
+ let moved = false;
1786
+ await withPointerDependencies({ token: () => { throw new Error('resume must not mint'); }, probePid: () => 'dead', atomicRenameNoReplace: async (from, to) => {
1787
+ if (from === context.lockPath && to === lockPark) {
1788
+ await rename(from, to);
1789
+ await mkdir(context.lockPath);
1790
+ await writeFile(ownerPath, successor);
1791
+ moved = true;
1792
+ return 'moved';
1793
+ }
1794
+ return 'not-moved';
1795
+ } }, async () => assert.equal((await recoverSessionPointerLock(cwd)).recovered, false));
1796
+ assert.equal(moved, true);
1797
+ assert.equal(await readFile(ownerPath, 'utf8'), successor);
1798
+ assert.deepEqual(await __releasePointerLockForTests(cwd, FOREIGN_TOKEN), []);
1799
+ assert.equal(existsSync(context.lockPath), false);
1800
+ assert.equal(existsSync(lockPark), true);
1801
+ }
1802
+ finally {
1803
+ await rm(cwd, { recursive: true, force: true });
1804
+ }
1805
+ });
1806
+ it('refuses a quarantine path that appears after its preflight check without overwriting it', async () => {
1807
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-lock-recovery-quarantine-race-'));
1808
+ try {
1809
+ const context = resolveSessionPointerContext(cwd);
1810
+ const ownerPath = join(context.lockPath, 'owner.json');
1811
+ const quarantine = `${context.lockPath}.quarantine.${TEST_TOKEN}.${SUCCESSOR_TOKEN}`;
1812
+ const foreign = 'foreign quarantine marker';
1813
+ await writeLockOwner(cwd, validLockOwner());
1814
+ await withPointerDependencies({ token: () => SUCCESSOR_TOKEN, probePid: () => 'dead', fs: { link: async (from, to) => { if (to === quarantine)
1815
+ await writeFile(to, foreign); await link(from, to); } } }, async () => {
1816
+ const result = await recoverSessionPointerLock(cwd);
1817
+ assert.equal(result.recovered, false);
1818
+ assert.match(result.reason, /EEXIST|quarantine/i);
1819
+ });
1820
+ assert.equal(await readFile(ownerPath, 'utf8'), JSON.stringify(validLockOwner()));
1821
+ assert.equal(await readFile(quarantine, 'utf8'), foreign);
1822
+ }
1823
+ finally {
1824
+ await rm(cwd, { recursive: true, force: true });
1825
+ }
1826
+ });
1827
+ it('fails closed without mutation when a recovery claim destination appears before the no-clobber claim link', async () => {
1828
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-lock-recovery-claim-exist-'));
1829
+ try {
1830
+ const context = resolveSessionPointerContext(cwd);
1831
+ const ownerPath = join(context.lockPath, 'owner.json');
1832
+ const claim = join(context.lockPath, `owner.${TEST_TOKEN}.${SUCCESSOR_TOKEN}.recovery`);
1833
+ let moved = false;
1834
+ await writeLockOwner(cwd, validLockOwner());
1835
+ await withPointerDependencies({
1836
+ token: () => SUCCESSOR_TOKEN,
1837
+ probePid: () => 'dead',
1838
+ fs: {
1839
+ writeFile: async (path, data, options) => {
1840
+ await writeFile(path, data, options);
1841
+ if (path.includes('.recovery.') && path.endsWith('.json'))
1842
+ await writeFile(claim, 'foreign claim');
1843
+ },
1844
+ },
1845
+ atomicRenameNoReplace: withProbeFallback(context.lockPath, async () => {
1846
+ moved = true;
1847
+ return 'moved';
1848
+ }),
1849
+ }, async () => {
1850
+ const recovered = await recoverSessionPointerLock(cwd);
1851
+ assert.equal(recovered.recovered, false, recovered.reason);
1852
+ assert.match(recovered.reason, /foreign|changed|claim/i);
1853
+ });
1854
+ assert.equal(moved, false);
1855
+ assert.equal(await readFile(ownerPath, 'utf8'), JSON.stringify(validLockOwner()));
1856
+ assert.equal(await readFile(claim, 'utf8'), 'foreign claim');
1857
+ }
1858
+ finally {
1859
+ await rm(cwd, { recursive: true, force: true });
1860
+ }
1861
+ });
1862
+ it('preserves a swapped parked quarantine claim rather than unlinking it', async () => {
1863
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-lock-recovery-parked-claim-swap-'));
1864
+ try {
1865
+ const context = resolveSessionPointerContext(cwd);
1866
+ const ownerPath = join(context.lockPath, 'owner.json');
1867
+ const lockPark = `${context.lockPath}.parked-lock-${SUCCESSOR_TOKEN}`;
1868
+ const claim = join(context.lockPath, `owner.${TEST_TOKEN}.${SUCCESSOR_TOKEN}.recovery`);
1869
+ const quarantine = `${context.lockPath}.quarantine.${TEST_TOKEN}.${SUCCESSOR_TOKEN}`;
1870
+ const checkpoint = `${context.lockPath}.recovery.${TEST_TOKEN}.${SUCCESSOR_TOKEN}.json`;
1871
+ await writeLockOwner(cwd, validLockOwner());
1872
+ const owner = await lstat(ownerPath);
1873
+ const lock = await lstat(context.lockPath);
1874
+ await link(ownerPath, claim);
1875
+ await link(ownerPath, quarantine);
1876
+ await rm(claim);
1877
+ await writeFile(claim, 'foreign parked claim');
1878
+ await rename(context.lockPath, lockPark);
1879
+ await writeFile(checkpoint, JSON.stringify({ version: 2, sourcePath: ownerPath, identity: { dev: owner.dev, ino: owner.ino }, lockIdentity: { dev: lock.dev, ino: lock.ino }, lockParkPath: lockPark, phase: 'evidence-quarantined' }));
1880
+ await withPointerDependencies({ token: () => { throw new Error('resume must not mint'); }, probePid: () => 'dead', atomicRenameNoReplace: async (from, to) => { if (from === checkpoint) {
1881
+ await rename(from, to);
1882
+ return 'moved';
1883
+ } throw new Error('directory move must not occur'); } }, async () => assert.equal((await recoverSessionPointerLock(cwd)).recovered, true));
1884
+ assert.equal(await readFile(join(lockPark, `owner.${TEST_TOKEN}.${SUCCESSOR_TOKEN}.recovery`), 'utf8'), 'foreign parked claim');
1885
+ assert.equal(await readFile(quarantine, 'utf8'), JSON.stringify(validLockOwner()));
1886
+ assert.equal(existsSync(lockPark), true);
1887
+ }
1888
+ finally {
1889
+ await rm(cwd, { recursive: true, force: true });
1890
+ }
1891
+ });
1892
+ it('fails closed without mutation when evidence lstat fails non-ENOENT before the claim', async () => {
1893
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-lock-recovery-evidence-eacces-'));
1894
+ try {
1895
+ const context = resolveSessionPointerContext(cwd);
1896
+ const ownerPath = join(context.lockPath, 'owner.json');
1897
+ const quarantine = `${context.lockPath}.quarantine.${TEST_TOKEN}.${SUCCESSOR_TOKEN}`;
1898
+ let armed = false;
1899
+ let moved = false;
1900
+ await writeLockOwner(cwd, validLockOwner());
1901
+ await withPointerDependencies({
1902
+ token: () => SUCCESSOR_TOKEN,
1903
+ probePid: () => 'dead',
1904
+ fs: {
1905
+ link: async (from, to) => {
1906
+ await link(from, to);
1907
+ if (to === quarantine)
1908
+ armed = true;
1909
+ },
1910
+ lstat: async (path) => {
1911
+ if (armed && path === ownerPath)
1912
+ throw codedError('EACCES');
1913
+ return await lstat(path);
1914
+ },
1915
+ },
1916
+ atomicRenameNoReplace: withProbeFallback(context.lockPath, async () => {
1917
+ moved = true;
1918
+ return 'moved';
1919
+ }),
1920
+ }, async () => {
1921
+ const recovered = await recoverSessionPointerLock(cwd);
1922
+ assert.equal(recovered.recovered, false, recovered.reason);
1923
+ });
1924
+ assert.equal(moved, false);
1925
+ assert.equal(await readFile(ownerPath, 'utf8'), JSON.stringify(validLockOwner()));
1926
+ }
1927
+ finally {
1928
+ await rm(cwd, { recursive: true, force: true });
1929
+ }
1930
+ });
1931
+ it('restores evidence and keeps the exact quarantine inspect diagnostic when quarantine lstat fails non-ENOENT', async () => {
1932
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-lock-recovery-quarantine-eacces-'));
1933
+ try {
1934
+ const context = resolveSessionPointerContext(cwd);
1935
+ const ownerPath = join(context.lockPath, 'owner.json');
1936
+ const quarantine = `${context.lockPath}.quarantine.${TEST_TOKEN}.${SUCCESSOR_TOKEN}`;
1937
+ const checkpoint = `${context.lockPath}.recovery.${TEST_TOKEN}.${SUCCESSOR_TOKEN}.json`;
1938
+ let armed = false;
1939
+ await writeLockOwner(cwd, validLockOwner());
1940
+ await withPointerDependencies({ token: () => SUCCESSOR_TOKEN, probePid: () => 'dead', fs: { link: async (from, to) => { await link(from, to); if (to === quarantine)
1941
+ armed = true; }, lstat: async (path) => { if (armed && path === quarantine)
1942
+ throw codedError('EACCES'); return await lstat(path); } } }, async () => { const result = await recoverSessionPointerLock(cwd); assert.equal(result.recovered, false); assert.match(result.reason, /EACCES/); });
1943
+ assert.equal(await readFile(ownerPath, 'utf8'), JSON.stringify(validLockOwner()));
1944
+ assert.equal(await readFile(quarantine, 'utf8'), JSON.stringify(validLockOwner()));
1945
+ assert.equal(existsSync(checkpoint), true);
1946
+ }
1947
+ finally {
1948
+ await rm(cwd, { recursive: true, force: true });
1949
+ }
1950
+ });
1951
+ it('preserves a swapped parked lock directory rather than removing it', async () => {
1952
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-lock-recovery-directory-swap-'));
1953
+ try {
1954
+ const context = resolveSessionPointerContext(cwd);
1955
+ const parked = `${context.lockPath}.parked-lock-${SUCCESSOR_TOKEN}`;
1956
+ const original = `${parked}.original`;
1957
+ await writeLockOwner(cwd, validLockOwner());
1958
+ await withPointerDependencies({
1959
+ token: () => SUCCESSOR_TOKEN,
1960
+ probePid: () => 'dead',
1961
+ atomicRenameNoReplace: withProbeFallback(context.lockPath, async (from, to) => {
1962
+ if (from === context.lockPath && to === parked) {
1963
+ await rename(from, original);
1964
+ await mkdir(from);
1965
+ await writeFile(join(from, 'foreign'), 'foreign');
1966
+ await rename(from, to);
1967
+ return 'moved';
1968
+ }
1969
+ return await defaultTestAtomicRenameNoReplace(from, to);
1970
+ }),
1971
+ }, async () => {
1972
+ assert.equal((await recoverSessionPointerLock(cwd)).recovered, false);
1973
+ });
1974
+ assert.equal(await readFile(join(parked, 'foreign'), 'utf8'), 'foreign');
1975
+ assert.equal(existsSync(join(original, 'owner.json')), true);
1976
+ }
1977
+ finally {
1978
+ await rm(cwd, { recursive: true, force: true });
1979
+ }
1980
+ });
1981
+ for (const [label, checkpoint] of [
1982
+ ['wrong version', { version: 99, phase: 'evidence-pending', sourcePath: 'x', identity: { dev: 0, ino: 0 } }],
1983
+ ['wrong phase', { version: 1, phase: 'claim-pending', sourcePath: 'x', identity: { dev: 0, ino: 0 } }],
1984
+ ['out-of-root path', { version: 1, phase: 'evidence-pending', sourcePath: '/foreign/owner.json', identity: { dev: 0, ino: 0 } }],
1985
+ ])
1986
+ it(`fails closed for ${label} checkpoint`, async () => {
1987
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-lock-recovery-invalid-checkpoint-'));
1988
+ try {
1989
+ const context = resolveSessionPointerContext(cwd);
1990
+ const path = `${context.lockPath}.recovery.${TEST_TOKEN}.${SUCCESSOR_TOKEN}.json`;
1991
+ await writeLockOwner(cwd, validLockOwner());
1992
+ await writeFile(path, JSON.stringify(checkpoint));
1993
+ await withPointerDependencies({ token: () => { throw new Error('must not mint'); }, probePid: () => 'dead' }, async () => assert.equal((await recoverSessionPointerLock(cwd)).recovered, false));
1994
+ assert.equal(await readFile(path, 'utf8'), JSON.stringify(checkpoint));
1995
+ assert.equal(await readFile(join(context.lockPath, 'owner.json'), 'utf8'), JSON.stringify(validLockOwner()));
1996
+ }
1997
+ finally {
1998
+ await rm(cwd, { recursive: true, force: true });
1999
+ }
2000
+ });
2001
+ for (const fault of ['EBUSY', 'ENOTEMPTY'])
2002
+ it(`completes a checkpoint resume retry after one-shot atomic directory move failure ${fault}`, async () => {
2003
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-lock-recovery-atomic-retry-'));
2004
+ try {
2005
+ const context = resolveSessionPointerContext(cwd);
2006
+ const checkpoint = `${context.lockPath}.recovery.${TEST_TOKEN}.${SUCCESSOR_TOKEN}.json`;
2007
+ let fail = true;
2008
+ await writeLockOwner(cwd, validLockOwner());
2009
+ await withPointerDependencies({ token: () => SUCCESSOR_TOKEN, probePid: () => 'dead', atomicRenameNoReplace: async (from, to) => { if (from === context.lockPath && fail) {
2010
+ fail = false;
2011
+ throw codedError(fault);
2012
+ } if (existsSync(to))
2013
+ return 'not-moved'; await rename(from, to); return 'moved'; } }, async () => { assert.equal((await recoverSessionPointerLock(cwd)).recovered, false); assert.equal(existsSync(checkpoint), true); assert.equal(existsSync(context.lockPath), true); assert.equal((await recoverSessionPointerLock(cwd)).recovered, true); });
2014
+ }
2015
+ finally {
2016
+ await rm(cwd, { recursive: true, force: true });
2017
+ }
2018
+ });
2019
+ it('keeps a foreign atomic destination and the source lock when no directory move occurs', async () => {
2020
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-recovery-atomic-destination-'));
2021
+ try {
2022
+ const context = resolveSessionPointerContext(cwd);
2023
+ await writeLockOwner(cwd, validLockOwner());
2024
+ const parked = `${context.lockPath}.parked-lock-${SUCCESSOR_TOKEN}`;
2025
+ await withPointerDependencies({ token: () => SUCCESSOR_TOKEN, probePid: () => 'dead', atomicRenameNoReplace: async (_from, to) => {
2026
+ await mkdir(to);
2027
+ await writeFile(join(to, 'foreign'), 'foreign');
2028
+ return 'not-moved';
2029
+ } }, async () => assert.equal((await recoverSessionPointerLock(cwd)).recovered, false));
2030
+ assert.equal(await readFile(join(parked, 'foreign'), 'utf8'), 'foreign');
2031
+ assert.equal(existsSync(context.lockPath), true);
2032
+ }
2033
+ finally {
2034
+ await rm(cwd, { recursive: true, force: true });
2035
+ }
2036
+ });
2037
+ it('preserves a foreign directory swapped into the atomic move and rollback residue', async () => {
2038
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-recovery-atomic-source-swap-'));
2039
+ try {
2040
+ const context = resolveSessionPointerContext(cwd);
2041
+ const original = `${context.lockPath}.original`;
2042
+ const parked = `${context.lockPath}.parked-lock-${SUCCESSOR_TOKEN}`;
2043
+ await writeLockOwner(cwd, validLockOwner());
2044
+ await withPointerDependencies({
2045
+ token: () => SUCCESSOR_TOKEN,
2046
+ probePid: () => 'dead',
2047
+ atomicRenameNoReplace: withProbeFallback(context.lockPath, async (from, to) => {
2048
+ if (from === context.lockPath && to === parked) {
2049
+ await rename(from, original);
2050
+ await mkdir(from);
2051
+ await writeFile(join(from, 'foreign'), 'foreign');
2052
+ await rename(from, to);
2053
+ return 'moved';
2054
+ }
2055
+ if (from === parked && to === context.lockPath)
2056
+ return 'not-moved';
2057
+ return await defaultTestAtomicRenameNoReplace(from, to);
2058
+ }),
2059
+ }, async () => {
2060
+ assert.equal((await recoverSessionPointerLock(cwd)).recovered, false);
2061
+ });
2062
+ assert.equal(existsSync(join(original, 'owner.json')), true);
2063
+ assert.equal(await readFile(join(parked, 'foreign'), 'utf8'), 'foreign');
2064
+ }
2065
+ finally {
2066
+ await rm(cwd, { recursive: true, force: true });
2067
+ }
2068
+ });
2069
+ it('does not overwrite a rollback destination that appears before the no-clobber rollback link', async () => {
2070
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-recovery-rollback-destination-race-'));
2071
+ try {
2072
+ const context = resolveSessionPointerContext(cwd);
2073
+ const parked = `${context.lockPath}.parked-lock-${SUCCESSOR_TOKEN}`;
2074
+ const foreign = 'foreign rollback destination';
2075
+ let rollback = false;
2076
+ await writeLockOwner(cwd, validLockOwner());
2077
+ await withPointerDependencies({
2078
+ token: () => SUCCESSOR_TOKEN,
2079
+ probePid: () => 'dead',
2080
+ atomicRenameNoReplace: async (from, to) => {
2081
+ if (from === context.lockPath) {
2082
+ await rename(from, to);
2083
+ await mkdir(context.lockPath);
2084
+ await writeFile(join(context.lockPath, 'foreign'), foreign);
2085
+ return 'moved';
2086
+ }
2087
+ if (from === parked && to === context.lockPath) {
2088
+ rollback = true;
2089
+ return 'not-moved';
2090
+ }
2091
+ if (existsSync(to))
2092
+ return 'not-moved';
2093
+ await rename(from, to);
2094
+ return 'moved';
2095
+ },
2096
+ }, async () => {
2097
+ const recovered = await recoverSessionPointerLock(cwd);
2098
+ assert.equal(recovered.recovered, false);
2099
+ assert.equal(recovered.action, 'none');
2100
+ assert.match(recovered.reason, /captured object|token-bound|source pathname/i);
2101
+ });
2102
+ assert.equal(rollback, true);
2103
+ assert.equal(await readFile(join(context.lockPath, 'foreign'), 'utf-8'), foreign);
2104
+ assert.equal(existsSync(join(parked, 'owner.json')), true);
2105
+ }
2106
+ finally {
2107
+ await rm(cwd, { recursive: true, force: true });
2108
+ }
2109
+ });
2110
+ it('preserves a checkpoint replacement and foreign completed receipt collision', async () => {
2111
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-recovery-completion-collision-'));
2112
+ try {
2113
+ const context = resolveSessionPointerContext(cwd);
2114
+ await writeLockOwner(cwd, validLockOwner());
2115
+ const checkpoint = `${context.lockPath}.recovery.${TEST_TOKEN}.${SUCCESSOR_TOKEN}.json`;
2116
+ await withPointerDependencies({ token: () => SUCCESSOR_TOKEN, probePid: () => 'dead', atomicRenameNoReplace: async (from, to) => {
2117
+ if (from === checkpoint) {
2118
+ await writeFile(to, 'foreign receipt');
2119
+ return 'not-moved';
2120
+ }
2121
+ await rename(from, to);
2122
+ return 'moved';
2123
+ } }, async () => assert.equal((await recoverSessionPointerLock(cwd)).recovered, false));
2124
+ assert.equal(existsSync(checkpoint), true);
2125
+ assert.equal(await readFile(checkpoint.replace(/\.json$/, '.completed'), 'utf8'), 'foreign receipt');
2126
+ }
2127
+ finally {
2128
+ await rm(cwd, { recursive: true, force: true });
2129
+ }
2130
+ });
2131
+ it('fails closed before moving a lock directory with a foreign extra entry', async () => {
2132
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-recovery-foreign-entry-'));
2133
+ try {
2134
+ const context = resolveSessionPointerContext(cwd);
2135
+ await writeLockOwner(cwd, validLockOwner());
2136
+ await writeFile(join(context.lockPath, 'foreign'), 'foreign');
2137
+ let moved = false;
2138
+ await withPointerDependencies({ token: () => SUCCESSOR_TOKEN, probePid: () => 'dead', atomicRenameNoReplace: async () => { moved = true; return 'moved'; } }, async () => assert.equal((await recoverSessionPointerLock(cwd)).recovered, false));
2139
+ assert.equal(moved, false);
2140
+ assert.equal(await readFile(join(context.lockPath, 'foreign'), 'utf8'), 'foreign');
2141
+ }
2142
+ finally {
2143
+ await rm(cwd, { recursive: true, force: true });
2144
+ }
2145
+ });
2146
+ it('resumes legacy base-v1 recovery and v2 source and parked partial recovery', async () => {
2147
+ for (const state of ['v1-source', 'v2-source', 'v2-parked']) {
2148
+ const cwd = await mkdtemp(join(tmpdir(), `omx-session-recovery-legacy-${state}-`));
2149
+ try {
2150
+ const context = resolveSessionPointerContext(cwd);
2151
+ const ownerPath = join(context.lockPath, 'owner.json');
2152
+ const checkpoint = `${context.lockPath}.recovery.${TEST_TOKEN}.${SUCCESSOR_TOKEN}.json`;
2153
+ const lockPark = `${context.lockPath}.parked-lock-${SUCCESSOR_TOKEN}`;
2154
+ await writeLockOwner(cwd, validLockOwner());
2155
+ const owner = await lstat(ownerPath);
2156
+ const lock = await lstat(context.lockPath);
2157
+ const base = { version: state === 'v1-source' ? 1 : 2, sourcePath: ownerPath, identity: { dev: owner.dev, ino: owner.ino }, phase: 'evidence-pending' };
2158
+ if (state === 'v2-parked') {
2159
+ await link(ownerPath, join(context.lockPath, `owner.${TEST_TOKEN}.${SUCCESSOR_TOKEN}.recovery`));
2160
+ await link(ownerPath, `${context.lockPath}.quarantine.${TEST_TOKEN}.${SUCCESSOR_TOKEN}`);
2161
+ await rename(context.lockPath, lockPark);
2162
+ }
2163
+ await writeFile(checkpoint, JSON.stringify(state === 'v1-source' ? base : { ...base, lockIdentity: { dev: lock.dev, ino: lock.ino }, lockParkPath: lockPark, phase: state === 'v2-parked' ? 'evidence-quarantined' : 'evidence-pending' }));
2164
+ await withPointerDependencies({ token: () => { throw new Error('legacy resume must not mint'); }, probePid: () => 'dead', atomicRenameNoReplace: async (from, to) => { if (existsSync(to))
2165
+ return 'not-moved'; await rename(from, to); return 'moved'; } }, async () => {
2166
+ const recovered = await recoverSessionPointerLock(cwd);
2167
+ assert.equal(recovered.recovered, true, recovered.reason);
2168
+ });
2169
+ assert.equal(existsSync(checkpoint.replace(/\.json$/, '.completed')), true);
2170
+ assert.equal(existsSync(lockPark), true);
2171
+ }
2172
+ finally {
2173
+ await rm(cwd, { recursive: true, force: true });
2174
+ }
2175
+ }
2176
+ });
2177
+ it('preserves recovery evidence when atomic directory quarantine is unsupported', async () => {
2178
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-recovery-unsupported-'));
2179
+ try {
2180
+ const context = resolveSessionPointerContext(cwd);
2181
+ await writeLockOwner(cwd, validLockOwner());
2182
+ await withPointerDependencies({
2183
+ token: () => SUCCESSOR_TOKEN,
2184
+ probePid: () => 'dead',
2185
+ atomicRenameNoReplace: async () => 'unsupported',
2186
+ }, async () => {
2187
+ const recovered = await recoverSessionPointerLock(cwd);
2188
+ assert.equal(recovered.recovered, false);
2189
+ assert.match(recovered.reason, /unsupported/i);
2190
+ });
2191
+ // Capability is probed before checkpoint/quarantine mutations.
2192
+ assert.equal(existsSync(context.lockPath), true);
2193
+ assert.equal(await readFile(join(context.lockPath, 'owner.json'), 'utf8'), JSON.stringify(validLockOwner()));
2194
+ assert.equal(existsSync(`${context.lockPath}.quarantine.${TEST_TOKEN}.${SUCCESSOR_TOKEN}`), false);
2195
+ assert.equal(existsSync(`${context.lockPath}.recovery.${TEST_TOKEN}.${SUCCESSOR_TOKEN}.json`), false);
2196
+ }
2197
+ finally {
2198
+ await rm(cwd, { recursive: true, force: true });
2199
+ }
2200
+ });
2201
+ it('distinguishes a paused live pre-rename owner from the same owner after SIGKILL', async (t) => {
2202
+ if (process.platform !== 'linux') {
2203
+ t.skip('Linux process start identity is required for deterministic PID reuse protection.');
2204
+ return;
2205
+ }
2206
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-lock-recovery-sigkill-'));
2207
+ const child = spawn(process.execPath, ['-e', 'setInterval(() => {}, 1_000)'], { stdio: 'ignore' });
2208
+ try {
2209
+ assert.ok(child.pid);
2210
+ await once(child, 'spawn');
2211
+ const context = resolveSessionPointerContext(cwd);
2212
+ await mkdir(context.lockPath, { recursive: true });
2213
+ await writeFile(join(context.lockPath, `owner.${TEST_TOKEN}.tmp`), JSON.stringify(validLockOwner({
2214
+ pid: child.pid,
2215
+ pid_start_ticks: await linuxProcessStartTicks(child.pid),
2216
+ })), 'utf-8');
2217
+ const paused = await recoverSessionPointerLock(cwd);
2218
+ assert.equal(paused.status, 'live');
2219
+ assert.equal(paused.recovered, false);
2220
+ assert.equal(paused.action, 'none');
2221
+ assert.equal(existsSync(context.lockPath), true);
2222
+ child.kill('SIGKILL');
2223
+ const [exitCode, signal] = await once(child, 'exit');
2224
+ assert.equal(exitCode, null);
2225
+ assert.equal(signal, 'SIGKILL');
2226
+ let orphaned;
2227
+ await withPointerDependencies({
2228
+ atomicRenameNoReplace: async (from, to) => {
2229
+ await rename(from, to);
2230
+ return 'moved';
2231
+ },
2232
+ }, async () => { orphaned = await recoverSessionPointerLock(cwd); });
2233
+ assert.equal(orphaned.status, 'dead');
2234
+ assert.equal(orphaned.recovered, true);
2235
+ assert.equal(orphaned.action, 'quarantined');
2236
+ assert.equal(existsSync(context.lockPath), false);
2237
+ }
2238
+ finally {
2239
+ if (child.exitCode === null && child.signalCode === null)
2240
+ child.kill('SIGKILL');
2241
+ await rm(cwd, { recursive: true, force: true });
2242
+ }
2243
+ });
2244
+ it('fails closed for live pre-rename, PID reuse, malformed, and ambiguous lock evidence', async () => {
2245
+ const cases = [
2246
+ { name: 'live-temp', files: [[`owner.${TEST_TOKEN}.tmp`, JSON.stringify(validLockOwner())]], probePid: 'alive', observation: matchingObservation() },
2247
+ { name: 'reused', files: [['owner.json', JSON.stringify(validLockOwner())]], probePid: 'alive', observation: { kind: 'identity', identity: { platform: 'linux', birth: '2' } } },
2248
+ { name: 'malformed', files: [['owner.json', '{']], probePid: 'dead' },
2249
+ { name: 'ambiguous', files: [['owner.json', JSON.stringify(validLockOwner())], [`owner.${SUCCESSOR_TOKEN}.tmp`, JSON.stringify(validLockOwner({ token: SUCCESSOR_TOKEN }))]], probePid: 'dead' },
2250
+ ];
2251
+ for (const testCase of cases) {
2252
+ const cwd = await mkdtemp(join(tmpdir(), `omx-session-lock-recovery-${testCase.name}-`));
2253
+ try {
2254
+ const context = resolveSessionPointerContext(cwd);
2255
+ await mkdir(context.lockPath, { recursive: true });
2256
+ await Promise.all(testCase.files.map(async ([name, contents]) => await writeFile(join(context.lockPath, name), contents, 'utf-8')));
2257
+ await withPointerDependencies({
2258
+ token: () => SUCCESSOR_TOKEN,
2259
+ probePid: () => testCase.probePid,
2260
+ observeProcess: () => testCase.observation ?? matchingObservation(),
2261
+ }, async () => {
2262
+ const before = await readdir(context.lockPath);
2263
+ const recovered = await recoverSessionPointerLock(cwd);
2264
+ assert.equal(recovered.recovered, false);
2265
+ assert.equal(recovered.action, 'none');
2266
+ assert.equal(recovered.safeToRecover, false);
2267
+ assert.deepEqual(await readdir(context.lockPath), before);
2268
+ });
2269
+ }
2270
+ finally {
2271
+ await rm(cwd, { recursive: true, force: true });
2272
+ }
2273
+ }
2274
+ });
2275
+ it('claims stale evidence before quarantine so a successor cannot be mistaken for it', async () => {
2276
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-lock-recovery-race-'));
2277
+ try {
2278
+ const context = resolveSessionPointerContext(cwd);
2279
+ await mkdir(context.lockPath, { recursive: true });
2280
+ await writeFile(join(context.lockPath, `owner.${TEST_TOKEN}.tmp`), JSON.stringify(validLockOwner()), 'utf-8');
2281
+ const links = [];
2282
+ let successorBlocked = false;
2283
+ await withPointerDependencies({
2284
+ token: () => SUCCESSOR_TOKEN,
2285
+ probePid: () => 'dead',
2286
+ fs: {
2287
+ link: async (from, to) => {
2288
+ links.push([from, to]);
2289
+ await link(from, to);
2290
+ },
2291
+ },
2292
+ atomicRenameNoReplace: async (from, to) => {
2293
+ if (from === context.lockPath) {
2294
+ await assert.rejects(mkdir(context.lockPath), { code: 'EEXIST' });
2295
+ successorBlocked = true;
2296
+ }
2297
+ if (existsSync(to))
2298
+ return 'not-moved';
2299
+ await rename(from, to);
2300
+ return 'moved';
2301
+ },
2302
+ }, async () => {
2303
+ const recovered = await recoverSessionPointerLock(cwd);
2304
+ assert.equal(recovered.recovered, true);
2305
+ });
2306
+ assert.equal(successorBlocked, true);
2307
+ assert.equal(links.length, 1);
2308
+ assert.match(links[0][0], /owner\.transaction_token_123456\.tmp$/);
2309
+ assert.equal(links[0][1].includes('.quarantine.'), true);
2310
+ assert.equal(existsSync(`${context.lockPath}.parked-lock-${SUCCESSOR_TOKEN}`), true);
2311
+ }
2312
+ finally {
2313
+ await rm(cwd, { recursive: true, force: true });
2314
+ }
2315
+ });
2316
+ it('does not claim a successor lock when the inspected orphan is displaced before the exact temp rename', async () => {
2317
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-lock-recovery-successor-race-'));
2318
+ try {
2319
+ const context = resolveSessionPointerContext(cwd);
2320
+ const staleTemp = join(context.lockPath, `owner.${TEST_TOKEN}.tmp`);
2321
+ const successorTemp = join(context.lockPath, `owner.${SUCCESSOR_TOKEN}.tmp`);
2322
+ const displacedPath = `${context.lockPath}.displaced`;
2323
+ await mkdir(context.lockPath, { recursive: true });
2324
+ await writeFile(staleTemp, JSON.stringify(validLockOwner()), 'utf-8');
2325
+ let displaced = false;
2326
+ await withPointerDependencies({
2327
+ token: () => SUCCESSOR_TOKEN,
2328
+ probePid: () => 'dead',
2329
+ fs: {
2330
+ link: async (from, to) => {
2331
+ if (!displaced && from === staleTemp) {
2332
+ displaced = true;
2333
+ await rename(context.lockPath, displacedPath);
2334
+ await mkdir(context.lockPath);
2335
+ await writeFile(successorTemp, JSON.stringify(validLockOwner({ token: SUCCESSOR_TOKEN })), 'utf-8');
2336
+ }
2337
+ await link(from, to);
2338
+ },
2339
+ },
2340
+ }, async () => {
2341
+ const recovered = await recoverSessionPointerLock(cwd);
2342
+ assert.equal(recovered.recovered, false);
2343
+ assert.equal(recovered.action, 'none');
2344
+ assert.match(recovered.reason, /Recovery checkpoint.*ENOENT/i);
2345
+ });
2346
+ assert.equal(displaced, true);
2347
+ assert.deepEqual(await readdir(context.lockPath), [`owner.${SUCCESSOR_TOKEN}.tmp`]);
2348
+ assert.equal(await readFile(successorTemp, 'utf-8'), JSON.stringify(validLockOwner({ token: SUCCESSOR_TOKEN })));
2349
+ }
2350
+ finally {
2351
+ await rm(cwd, { recursive: true, force: true });
2352
+ }
2353
+ });
2354
+ it('does not report recovery after a successor live lock appears during parked-lock removal', async () => {
2355
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-lock-recovery-post-claim-race-'));
2356
+ try {
2357
+ const context = resolveSessionPointerContext(cwd);
2358
+ const staleTemp = join(context.lockPath, `owner.${TEST_TOKEN}.tmp`);
2359
+ const successorTemp = join(context.lockPath, `owner.${SUCCESSOR_TOKEN}.tmp`);
2360
+ await mkdir(context.lockPath, { recursive: true });
2361
+ await writeFile(staleTemp, JSON.stringify(validLockOwner()), 'utf-8');
2362
+ let displaced = false;
2363
+ await withPointerDependencies({
2364
+ token: () => SUCCESSOR_TOKEN,
2365
+ probePid: () => 'dead',
2366
+ atomicRenameNoReplace: async (from, to) => {
2367
+ if (existsSync(to))
2368
+ return 'not-moved';
2369
+ await rename(from, to);
2370
+ if (!displaced && from === context.lockPath) {
2371
+ displaced = true;
2372
+ await mkdir(context.lockPath);
2373
+ await writeFile(successorTemp, JSON.stringify(validLockOwner({ token: SUCCESSOR_TOKEN })), 'utf-8');
2374
+ }
2375
+ return 'moved';
2376
+ },
2377
+ }, async () => {
2378
+ const recovered = await recoverSessionPointerLock(cwd);
2379
+ assert.equal(recovered.recovered, false);
2380
+ assert.equal(recovered.action, 'none');
2381
+ assert.match(recovered.reason, /did not leave the captured object/i);
2382
+ });
2383
+ assert.equal(displaced, true);
2384
+ assert.deepEqual(await readdir(context.lockPath), [`owner.${SUCCESSOR_TOKEN}.tmp`]);
2385
+ assert.equal(await readFile(successorTemp, 'utf-8'), JSON.stringify(validLockOwner({ token: SUCCESSOR_TOKEN })));
2386
+ }
2387
+ finally {
2388
+ await rm(cwd, { recursive: true, force: true });
2389
+ }
2390
+ });
2391
+ it('retries a positively matching live owner with the bounded 25/50/100 schedule before timing out', async () => {
2392
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-lock-timeout-'));
2393
+ try {
2394
+ await writeLockOwner(cwd, validLockOwner());
2395
+ const delays = [];
2396
+ let now = 0;
2397
+ await withPointerDependencies({
2398
+ nowMs: () => now,
2399
+ sleep: async (ms) => { delays.push(ms); now += ms; },
2400
+ token: () => TEST_TOKEN,
2401
+ probePid: () => 'alive',
2402
+ observeProcess: () => matchingObservation(),
2403
+ }, async () => {
2404
+ await assert.rejects(writeSessionStart(cwd, 'sess-timeout', { platform: 'win32' }), (error) => isSessionPointerLaunchAbort(error)
2405
+ && error.code === 'session_pointer_lock_timeout'
2406
+ && error.lockOwnerStatus === 'live');
2407
+ });
2408
+ assert.deepEqual(delays.slice(0, 3), [25, 50, 100]);
2409
+ assert.equal(delays.at(-1), 25);
2410
+ assert.equal(existsSync(resolveSessionPointerContext(cwd).lockPath), true);
2411
+ }
2412
+ finally {
2413
+ await rm(cwd, { recursive: true, force: true });
2414
+ }
2415
+ });
2416
+ it('rolls back only its unpublished owner artifacts and reports rollback ambiguity', async () => {
2417
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-owner-publish-'));
2418
+ try {
2419
+ const context = resolveSessionPointerContext(cwd);
2420
+ await withPointerDependencies({
2421
+ token: () => TEST_TOKEN,
2422
+ fs: {
2423
+ writeFile: async (path, data, options) => {
2424
+ if (path.endsWith(`owner.${TEST_TOKEN}.tmp`))
2425
+ throw new Error('owner write failure');
2426
+ await writeFile(path, data, options);
2427
+ },
2428
+ },
2429
+ }, async () => {
2430
+ await assert.rejects(writeSessionStart(cwd, 'sess-owner-publish', { platform: 'win32' }), (error) => isSessionPointerLaunchAbort(error)
2431
+ && error.code === 'session_pointer_io_failure'
2432
+ && error.operation === 'lock-owner-publish');
2433
+ });
2434
+ assert.equal(existsSync(context.lockPath), false);
2435
+ await withPointerDependencies({
2436
+ token: () => TEST_TOKEN,
2437
+ fs: {
2438
+ writeFile: async (path, data, options) => {
2439
+ if (path.endsWith(`owner.${TEST_TOKEN}.tmp`))
2440
+ throw new Error('owner write failure');
2441
+ await writeFile(path, data, options);
2442
+ },
2443
+ rmdir: async (path) => {
2444
+ if (path === context.lockPath)
2445
+ throw new Error('keep lock evidence');
2446
+ await rm(path, { recursive: false });
2447
+ },
2448
+ },
2449
+ }, async () => {
2450
+ await assert.rejects(writeSessionStart(cwd, 'sess-owner-residue', { platform: 'win32' }), (error) => isSessionPointerLaunchAbort(error)
2451
+ && error.code === 'session_pointer_lock_recovery_required'
2452
+ && error.primaryOperation === 'lock-owner-publish'
2453
+ && error.secondaryFailures?.[0]?.phase === 'remove-unpublished-lock');
2454
+ });
2455
+ assert.equal(existsSync(context.lockPath), true);
2456
+ }
2457
+ finally {
2458
+ await rm(cwd, { recursive: true, force: true });
2459
+ }
2460
+ });
2461
+ it('publishes a legacy-like Linux state when process identity capture fails', async (t) => {
2462
+ if (process.platform !== 'linux') {
2463
+ t.skip('Linux identity publication semantics are under test.');
2464
+ return;
2465
+ }
2466
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-identity-capture-failure-'));
2467
+ try {
2468
+ await withPointerDependencies({
2469
+ runtimePlatform: 'linux',
2470
+ observeProcess: () => ({ kind: 'error' }),
2471
+ }, async () => {
2472
+ const state = await writeSessionStart(cwd, 'sess-identity-capture-failure', { platform: 'linux' });
2473
+ assert.equal(state.platform, 'linux');
2474
+ assert.equal(Object.hasOwn(state, 'identity_schema_version'), false);
2475
+ assert.equal(Object.hasOwn(state, 'process_identity'), false);
2476
+ assert.equal(Object.hasOwn(state, 'pid_start_ticks'), false);
2477
+ const persisted = await readSessionState(cwd);
2478
+ assert.ok(persisted);
2479
+ assert.equal(Object.hasOwn(persisted, 'identity_schema_version'), false);
2480
+ assert.equal(Object.hasOwn(persisted, 'process_identity'), false);
2481
+ });
2482
+ }
2483
+ finally {
2484
+ await rm(cwd, { recursive: true, force: true });
2485
+ }
2486
+ });
2487
+ it('publishes the owner and pointer on Windows when regular-file fsync reports EPERM', async () => {
2488
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-windows-eperm-'));
2489
+ let syncCalls = 0;
2490
+ const regularFileSync = async (platform) => {
2491
+ assert.equal(platform, 'win32');
2492
+ syncCalls += 1;
2493
+ throw codedError('EPERM');
2494
+ };
2495
+ try {
2496
+ const state = await writeSessionStart(cwd, 'sess-windows-eperm', {
2497
+ platform: 'win32',
2498
+ regularFileSync,
2499
+ });
2500
+ const context = resolveSessionPointerContext(cwd);
2501
+ assert.equal(state.session_id, 'sess-windows-eperm');
2502
+ assert.equal(syncCalls, 2, 'owner publication and pointer publication must both attempt fsync');
2503
+ assert.equal(existsSync(join(context.lockPath, 'owner.json')), false, 'lock owner is removed after commit');
2504
+ assert.equal(JSON.parse(await readFile(context.sessionPath, 'utf8')).session_id, 'sess-windows-eperm');
2505
+ }
2506
+ finally {
2507
+ await rm(cwd, { recursive: true, force: true });
2508
+ }
2509
+ });
2510
+ it('emits one post-release warning for a degraded session start and stays silent for synced or failed transactions', async () => {
2511
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-durability-warning-'));
2512
+ const originalWrite = process.stderr.write;
2513
+ const warnings = [];
2514
+ let warningCwd = cwd;
2515
+ process.stderr.write = ((value) => {
2516
+ warnings.push({ value, lockExists: existsSync(resolveSessionPointerContext(warningCwd).lockPath) });
2517
+ return true;
2518
+ });
2519
+ const regularFileSync = async () => { throw codedError('EPERM'); };
2520
+ try {
2521
+ await writeSessionStart(cwd, 'sess-degraded-start', { platform: 'win32', regularFileSync });
2522
+ assert.deepEqual(warnings, [{
2523
+ value: '[omx] warning: Windows EPERM regular-file fsync unsupported in session pointer start/reconcile; operation succeeded with degraded durability.\n',
2524
+ lockExists: false,
2525
+ }]);
2526
+ warnings.length = 0;
2527
+ const syncedCwd = join(cwd, 'synced');
2528
+ await mkdir(syncedCwd);
2529
+ warningCwd = syncedCwd;
2530
+ await writeSessionStart(syncedCwd, 'sess-synced-start', {
2531
+ platform: 'win32',
2532
+ regularFileSync: async () => { },
2533
+ });
2534
+ assert.deepEqual(warnings, []);
2535
+ const failedCwd = join(cwd, 'failed');
2536
+ await mkdir(failedCwd);
2537
+ warningCwd = failedCwd;
2538
+ await withPointerDependencies({
2539
+ fs: {
2540
+ rename: async (from, to) => {
2541
+ if (to === resolveSessionPointerContext(failedCwd).sessionPath)
2542
+ throw new Error('rename failure');
2543
+ await rename(from, to);
2544
+ },
2545
+ },
2546
+ }, async () => {
2547
+ await assert.rejects(writeSessionStart(failedCwd, 'sess-failed-start', { platform: 'win32', regularFileSync }));
2548
+ });
2549
+ assert.deepEqual(warnings, []);
2550
+ }
2551
+ finally {
2552
+ process.stderr.write = originalWrite;
2553
+ await rm(cwd, { recursive: true, force: true });
2554
+ }
2555
+ });
2556
+ it('types state-directory, pointer-read, owner-sync, owner-rename, and invalid-token failures before commit', async () => {
2557
+ const failures = [
2558
+ {
2559
+ name: 'state-dir',
2560
+ operation: 'state-dir-create',
2561
+ dependencies: (context) => ({
2562
+ fs: {
2563
+ mkdir: async (path, options) => {
2564
+ if (path === context.baseStateDir)
2565
+ throw new Error('state directory failure');
2566
+ await mkdir(path, options);
2567
+ },
2568
+ },
2569
+ }),
2570
+ },
2571
+ {
2572
+ name: 'pointer-read',
2573
+ operation: 'pointer-read',
2574
+ dependencies: (context) => ({
2575
+ token: () => TEST_TOKEN,
2576
+ fs: {
2577
+ readFile: async (path, encoding) => {
2578
+ if (path === context.sessionPath)
2579
+ throw new Error('pointer read failure');
2580
+ return await readFile(path, encoding);
2581
+ },
2582
+ },
2583
+ }),
2584
+ },
2585
+ {
2586
+ name: 'owner-sync',
2587
+ operation: 'lock-owner-publish',
2588
+ dependencies: (context) => ({
2589
+ token: () => TEST_TOKEN,
2590
+ fs: {
2591
+ openAndSync: async (path) => {
2592
+ if (path === join(context.lockPath, `owner.${TEST_TOKEN}.tmp`))
2593
+ throw new Error('owner sync failure');
2594
+ return 'synced';
2595
+ },
2596
+ },
2597
+ }),
2598
+ },
2599
+ {
2600
+ name: 'owner-rename',
2601
+ operation: 'lock-owner-publish',
2602
+ dependencies: (context) => ({
2603
+ token: () => TEST_TOKEN,
2604
+ fs: {
2605
+ rename: async (from, to) => {
2606
+ if (from === join(context.lockPath, `owner.${TEST_TOKEN}.tmp`))
2607
+ throw new Error('owner rename failure');
2608
+ await rename(from, to);
2609
+ },
2610
+ },
2611
+ }),
2612
+ },
2613
+ ];
2614
+ for (const failure of failures) {
2615
+ const cwd = await mkdtemp(join(tmpdir(), `omx-session-${failure.name}-`));
2616
+ try {
2617
+ const context = resolveSessionPointerContext(cwd);
2618
+ await withPointerDependencies(failure.dependencies(context), async () => {
2619
+ await assert.rejects(writeSessionStart(cwd, 'sess-precommit', { platform: 'win32' }), (error) => isSessionPointerLaunchAbort(error)
2620
+ && error.code === 'session_pointer_io_failure'
2621
+ && error.operation === failure.operation);
2622
+ });
2623
+ assert.equal(existsSync(context.lockPath), false);
2624
+ }
2625
+ finally {
2626
+ await rm(cwd, { recursive: true, force: true });
2627
+ }
2628
+ }
2629
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-invalid-token-'));
2630
+ try {
2631
+ const context = resolveSessionPointerContext(cwd);
2632
+ await withPointerDependencies({ token: () => 'bad' }, async () => {
2633
+ await assert.rejects(writeSessionStart(cwd, 'sess-invalid-token', { platform: 'win32' }), (error) => isSessionPointerLaunchAbort(error)
2634
+ && error.code === 'session_pointer_io_failure'
2635
+ && error.operation === 'lock-owner-publish');
704
2636
  });
705
2637
  assert.equal(existsSync(context.lockPath), false);
2638
+ assert.equal(existsSync(`${context.sessionPath}.tmp-bad`), false);
2639
+ }
2640
+ finally {
2641
+ await rm(cwd, { recursive: true, force: true });
2642
+ }
2643
+ });
2644
+ it('types pointer write, sync, and rename failures and removes only the owned temporary file', async () => {
2645
+ const phases = [
2646
+ { operation: 'pointer-temp-write', fail: 'write' },
2647
+ { operation: 'pointer-fsync', fail: 'sync' },
2648
+ { operation: 'pointer-rename', fail: 'rename' },
2649
+ ];
2650
+ for (const phase of phases) {
2651
+ const cwd = await mkdtemp(join(tmpdir(), `omx-session-${phase.fail}-`));
2652
+ try {
2653
+ const context = resolveSessionPointerContext(cwd);
2654
+ const tempPath = `${context.sessionPath}.tmp-${TEST_TOKEN}`;
2655
+ await withPointerDependencies({
2656
+ token: () => TEST_TOKEN,
2657
+ fs: {
2658
+ writeFile: async (path, data, options) => {
2659
+ await writeFile(path, data, options);
2660
+ if (phase.fail === 'write' && path === tempPath)
2661
+ throw new Error('pointer write failure');
2662
+ },
2663
+ openAndSync: async (path) => {
2664
+ if (phase.fail === 'sync' && path === tempPath)
2665
+ throw new Error('pointer sync failure');
2666
+ return 'synced';
2667
+ },
2668
+ rename: async (from, to) => {
2669
+ if (phase.fail === 'rename' && from === tempPath && to === context.sessionPath) {
2670
+ throw new Error('pointer rename failure');
2671
+ }
2672
+ await rename(from, to);
2673
+ },
2674
+ },
2675
+ }, async () => {
2676
+ await assert.rejects(writeSessionStart(cwd, 'sess-failure', { platform: 'win32' }), (error) => isSessionPointerLaunchAbort(error)
2677
+ && error.code === 'session_pointer_io_failure'
2678
+ && error.operation === phase.operation);
2679
+ });
2680
+ assert.equal(existsSync(tempPath), false);
2681
+ assert.equal(existsSync(context.lockPath), false);
2682
+ }
2683
+ finally {
2684
+ await rm(cwd, { recursive: true, force: true });
2685
+ }
2686
+ }
2687
+ });
2688
+ it('preserves owned pointer residue and ordered cleanup evidence when cleanup or release cannot complete', async () => {
2689
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-pointer-residue-'));
2690
+ try {
2691
+ const context = resolveSessionPointerContext(cwd);
2692
+ const tempPath = `${context.sessionPath}.tmp-${TEST_TOKEN}`;
2693
+ await withPointerDependencies({
2694
+ token: () => TEST_TOKEN,
2695
+ fs: {
2696
+ writeFile: async (path, data, options) => {
2697
+ await writeFile(path, data, options);
2698
+ if (path === tempPath)
2699
+ throw new Error('pointer write failure');
2700
+ },
2701
+ unlink: async (path) => {
2702
+ if (path === tempPath)
2703
+ throw new Error('pointer temp cleanup failure');
2704
+ await rm(path, { force: false });
2705
+ },
2706
+ rmdir: async (path) => {
2707
+ if (path.endsWith(`.release-${TEST_TOKEN}`))
2708
+ throw new Error('release cleanup failure');
2709
+ await rm(path, { recursive: false });
2710
+ },
2711
+ },
2712
+ }, async () => {
2713
+ await assert.rejects(writeSessionStart(cwd, 'sess-residue', { platform: 'win32' }), (error) => isSessionPointerLaunchAbort(error)
2714
+ && error.code === 'session_pointer_lock_recovery_required'
2715
+ && error.primaryOperation === 'pointer-temp-write'
2716
+ && error.secondaryFailures?.map((failure) => failure.phase).join(',') === 'remove-pointer-temp,remove-release-dir'
2717
+ && error.secondaryFailures?.[0]?.evidencePath === tempPath);
2718
+ });
2719
+ assert.equal(existsSync(tempPath), true);
2720
+ assert.equal(existsSync(`${context.lockPath}.release-${TEST_TOKEN}`), true);
2721
+ }
2722
+ finally {
2723
+ await rm(cwd, { recursive: true, force: true });
2724
+ }
2725
+ });
2726
+ it('leaves foreign pointer temps inert and a successor commits after explicit lock recovery', async () => {
2727
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-pointer-successor-'));
2728
+ try {
2729
+ const context = resolveSessionPointerContext(cwd);
2730
+ const ownTempPath = `${context.sessionPath}.tmp-${TEST_TOKEN}`;
2731
+ const foreignTempPath = `${context.sessionPath}.tmp-${FOREIGN_TOKEN}`;
2732
+ await mkdir(context.baseStateDir, { recursive: true });
2733
+ await writeFile(foreignTempPath, 'foreign transaction evidence', 'utf-8');
706
2734
  await withPointerDependencies({
707
2735
  token: () => TEST_TOKEN,
708
2736
  fs: {
709
2737
  writeFile: async (path, data, options) => {
710
- if (path.endsWith(`owner.${TEST_TOKEN}.tmp`))
711
- throw new Error('owner write failure');
712
2738
  await writeFile(path, data, options);
2739
+ if (path === ownTempPath)
2740
+ throw new Error('pointer write failure');
713
2741
  },
714
- rmdir: async (path) => {
715
- if (path === context.lockPath)
716
- throw new Error('keep lock evidence');
717
- await rm(path, { recursive: false });
2742
+ unlink: async (path) => {
2743
+ if (path === ownTempPath)
2744
+ throw new Error('keep own residue');
2745
+ await rm(path, { force: false });
2746
+ },
2747
+ rename: async (from, to) => {
2748
+ if (from === context.lockPath)
2749
+ throw new Error('preserve canonical lock');
2750
+ await rename(from, to);
718
2751
  },
719
2752
  },
720
2753
  }, async () => {
721
- await assert.rejects(writeSessionStart(cwd, 'sess-owner-residue', { platform: 'win32' }), (error) => isSessionPointerLaunchAbort(error)
722
- && error.code === 'session_pointer_lock_recovery_required'
723
- && error.primaryOperation === 'lock-owner-publish'
724
- && error.secondaryFailures?.[0]?.phase === 'remove-unpublished-lock');
2754
+ await assert.rejects(writeSessionStart(cwd, 'sess-first', { platform: 'win32' }), isSessionPointerLaunchAbort);
725
2755
  });
2756
+ assert.equal(existsSync(foreignTempPath), true);
726
2757
  assert.equal(existsSync(context.lockPath), true);
2758
+ // This is documented stopped-session operator recovery, deliberately not
2759
+ // product transaction behavior.
2760
+ await rm(context.lockPath, { recursive: true, force: true });
2761
+ await withPointerDependencies({ token: () => SUCCESSOR_TOKEN }, async () => {
2762
+ await writeSessionStart(cwd, 'sess-successor', { platform: 'win32' });
2763
+ });
2764
+ assert.equal((await readSessionState(cwd))?.session_id, 'sess-successor');
2765
+ assert.equal(existsSync(ownTempPath), true);
2766
+ assert.equal(existsSync(foreignTempPath), true);
2767
+ assert.equal(existsSync(context.lockPath), false);
2768
+ }
2769
+ finally {
2770
+ await rm(cwd, { recursive: true, force: true });
2771
+ }
2772
+ });
2773
+ it('binds an owner alias only on a verified same-native/absent transition and never during replacement', async () => {
2774
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-owner-alias-'));
2775
+ try {
2776
+ await withPointerDependencies({
2777
+ runtimePlatform: 'win32',
2778
+ observeProcess: () => matchingWin32Observation(),
2779
+ }, async () => {
2780
+ const nativeOnly = await reconcileNativeSessionStart(cwd, 'native-first', {
2781
+ platform: 'win32',
2782
+ ownerOmxSessionId: 'omx-owner',
2783
+ });
2784
+ assert.equal(nativeOnly.owner_omx_session_id, undefined);
2785
+ await withOwnerEnvironment('omx-owner', async () => {
2786
+ const bound = await reconcileNativeSessionStart(cwd, 'native-first', {
2787
+ platform: 'win32',
2788
+ ownerAliasVerified: true,
2789
+ });
2790
+ assert.equal(bound.session_id, 'native-first');
2791
+ assert.equal(bound.owner_omx_session_id, 'omx-owner');
2792
+ await assert.rejects(reconcileNativeSessionStart(cwd, 'native-second', {
2793
+ platform: 'win32',
2794
+ ownerAliasVerified: true,
2795
+ }), isOwnerConflict);
2796
+ const persisted = await readSessionState(cwd);
2797
+ assert.equal(persisted?.session_id, 'native-first');
2798
+ assert.equal(persisted?.owner_omx_session_id, 'omx-owner');
2799
+ });
2800
+ });
2801
+ }
2802
+ finally {
2803
+ await rm(cwd, { recursive: true, force: true });
2804
+ }
2805
+ });
2806
+ it('keeps native session owner sidecars isolated and rejects live cross-process reuse', async () => {
2807
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-owner-sidecar-'));
2808
+ try {
2809
+ await withPointerDependencies({
2810
+ runtimePlatform: 'win32',
2811
+ observeProcess: () => matchingWin32Observation(),
2812
+ probePid: () => 'alive',
2813
+ }, async () => {
2814
+ const first = await writeNativeSessionOwner(cwd, 'native-owner-a', { pid: 11, platform: 'win32' });
2815
+ const second = await writeNativeSessionOwner(cwd, 'native-owner-b', { pid: 22, platform: 'win32' });
2816
+ assert.equal(first.pid, 11);
2817
+ assert.equal(second.pid, 22);
2818
+ const firstPath = join(cwd, '.omx', 'state', 'sessions', 'native-owner-a', 'session-owner.json');
2819
+ const secondPath = join(cwd, '.omx', 'state', 'sessions', 'native-owner-b', 'session-owner.json');
2820
+ assert.equal(JSON.parse(await readFile(firstPath, 'utf-8')).pid, 11);
2821
+ assert.equal(JSON.parse(await readFile(secondPath, 'utf-8')).pid, 22);
2822
+ await writeNativeSessionOwner(cwd, 'native-owner-current', {
2823
+ pid: process.pid,
2824
+ platform: 'win32',
2825
+ });
2826
+ assert.equal((await readNativeSessionOwner(cwd, 'native-owner-current'))?.pid, process.pid);
2827
+ await assert.rejects(writeNativeSessionOwner(cwd, 'native-owner-a', { pid: 22, platform: 'win32' }), (error) => isSessionPointerLaunchAbort(error)
2828
+ && error.code === 'session_pointer_owner_conflict');
2829
+ });
2830
+ }
2831
+ finally {
2832
+ await rm(cwd, { recursive: true, force: true });
2833
+ }
2834
+ });
2835
+ it('rejects a symlinked native owner state root', async () => {
2836
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-owner-root-symlink-'));
2837
+ try {
2838
+ const externalState = join(cwd, 'external-state');
2839
+ const ownerDir = join(externalState, 'sessions', 'native-owner-root-link');
2840
+ await mkdir(ownerDir, { recursive: true });
2841
+ await writeFile(join(ownerDir, 'session-owner.json'), JSON.stringify({
2842
+ session_id: 'native-owner-root-link',
2843
+ native_session_id: 'native-owner-root-link',
2844
+ cwd,
2845
+ platform: process.platform,
2846
+ }));
2847
+ await mkdir(join(cwd, '.omx'), { recursive: true });
2848
+ await symlink(externalState, join(cwd, '.omx', 'state'));
2849
+ assert.equal((await readNativeSessionOwnerEvidence(cwd, 'native-owner-root-link')).status, 'malformed');
2850
+ }
2851
+ finally {
2852
+ await rm(cwd, { recursive: true, force: true });
2853
+ }
2854
+ });
2855
+ it('rejects symlinked native owner directories and sidecars', async () => {
2856
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-owner-symlink-'));
2857
+ try {
2858
+ const stateDir = join(cwd, '.omx', 'state');
2859
+ const sessionsDir = join(stateDir, 'sessions');
2860
+ const externalDir = join(cwd, 'external-owner');
2861
+ await mkdir(sessionsDir, { recursive: true });
2862
+ await mkdir(externalDir, { recursive: true });
2863
+ await writeFile(join(externalDir, 'session-owner.json'), JSON.stringify({
2864
+ session_id: 'native-owner-dir-link',
2865
+ native_session_id: 'native-owner-dir-link',
2866
+ cwd,
2867
+ platform: process.platform,
2868
+ }));
2869
+ await symlink(externalDir, join(sessionsDir, 'native-owner-dir-link'));
2870
+ assert.equal((await readNativeSessionOwnerEvidence(cwd, 'native-owner-dir-link')).status, 'malformed');
2871
+ const fileLinkDir = join(sessionsDir, 'native-owner-file-link');
2872
+ await mkdir(fileLinkDir, { recursive: true });
2873
+ const externalFile = join(cwd, 'external-session-owner.json');
2874
+ await writeFile(externalFile, JSON.stringify({
2875
+ session_id: 'native-owner-file-link',
2876
+ native_session_id: 'native-owner-file-link',
2877
+ cwd,
2878
+ platform: process.platform,
2879
+ }));
2880
+ await symlink(externalFile, join(fileLinkDir, 'session-owner.json'));
2881
+ assert.equal((await readNativeSessionOwnerEvidence(cwd, 'native-owner-file-link')).status, 'malformed');
2882
+ const swappedOwnerId = 'native-owner-swapped-dir';
2883
+ const swappedOwnerDir = join(sessionsDir, swappedOwnerId);
2884
+ await mkdir(swappedOwnerDir, { recursive: true });
2885
+ await writeFile(join(swappedOwnerDir, 'session-owner.json'), JSON.stringify({
2886
+ session_id: swappedOwnerId,
2887
+ native_session_id: swappedOwnerId,
2888
+ cwd,
2889
+ platform: process.platform,
2890
+ }));
2891
+ let swappedOwnerLstatCalls = 0;
2892
+ await withPointerDependencies({
2893
+ fs: {
2894
+ lstat: async (path) => {
2895
+ const stat = await lstat(path);
2896
+ const simulateSwap = path === swappedOwnerDir && ++swappedOwnerLstatCalls === 2;
2897
+ return {
2898
+ dev: Number(stat.dev) + (simulateSwap ? 1 : 0),
2899
+ ino: Number(stat.ino),
2900
+ isDirectory: () => stat.isDirectory(),
2901
+ isFile: () => stat.isFile(),
2902
+ isSymbolicLink: () => stat.isSymbolicLink(),
2903
+ };
2904
+ },
2905
+ },
2906
+ }, async () => {
2907
+ assert.equal((await readNativeSessionOwnerEvidence(cwd, swappedOwnerId)).status, 'malformed');
2908
+ });
2909
+ }
2910
+ finally {
2911
+ await rm(cwd, { recursive: true, force: true });
2912
+ }
2913
+ });
2914
+ it('rejects owner sidecars recorded for another platform', async () => {
2915
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-owner-platform-'));
2916
+ try {
2917
+ const ownerDir = join(cwd, '.omx', 'state', 'sessions', 'native-owner-platform');
2918
+ await mkdir(ownerDir, { recursive: true });
2919
+ const ownerPath = join(ownerDir, 'session-owner.json');
2920
+ const forgedPlatform = process.platform === 'win32'
2921
+ ? 'darwin'
2922
+ : 'win32';
2923
+ await writeFile(ownerPath, JSON.stringify({
2924
+ session_id: 'native-owner-platform',
2925
+ native_session_id: 'native-owner-platform',
2926
+ started_at: new Date().toISOString(),
2927
+ cwd,
2928
+ pid: process.pid,
2929
+ platform: forgedPlatform,
2930
+ }), 'utf-8');
2931
+ const before = await readFile(ownerPath, 'utf-8');
2932
+ assert.equal((await readNativeSessionOwnerEvidence(cwd, 'native-owner-platform')).status, 'identity-indeterminate');
2933
+ assert.equal(await readNativeSessionOwner(cwd, 'native-owner-platform'), null);
2934
+ await assert.rejects(writeNativeSessionOwner(cwd, 'native-owner-platform', {
2935
+ pid: process.pid,
2936
+ }), (error) => isSessionPointerLaunchAbort(error)
2937
+ && error.code === 'session_pointer_unusable');
2938
+ assert.equal(await readFile(ownerPath, 'utf-8'), before);
2939
+ }
2940
+ finally {
2941
+ await rm(cwd, { recursive: true, force: true });
2942
+ }
2943
+ });
2944
+ it('replaces only stale-dead owner evidence and preserves malformed evidence', async () => {
2945
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-owner-recovery-'));
2946
+ try {
2947
+ assert.equal((await readNativeSessionOwnerEvidence(cwd, 'native-owner-absent')).status, 'absent');
2948
+ await withPointerDependencies({
2949
+ probePid: (pid) => pid === 11 ? 'dead' : 'alive',
2950
+ observeProcess: () => ({ kind: 'identity', identity: { platform: 'linux', birth: '1' } }),
2951
+ }, async () => {
2952
+ await writeNativeSessionOwner(cwd, 'native-owner-recovery', { pid: 11, platform: 'linux' });
2953
+ assert.equal((await readNativeSessionOwnerEvidence(cwd, 'native-owner-recovery')).status, 'stale-dead');
2954
+ const recovered = await writeNativeSessionOwner(cwd, 'native-owner-recovery', { pid: 22, platform: 'linux' });
2955
+ assert.equal(recovered.pid, 22);
2956
+ const malformedDir = join(cwd, '.omx', 'state', 'sessions', 'native-owner-malformed');
2957
+ await mkdir(malformedDir, { recursive: true });
2958
+ const malformedPath = join(malformedDir, 'session-owner.json');
2959
+ await writeFile(malformedPath, '{ malformed', 'utf-8');
2960
+ assert.equal(await readNativeSessionOwner(cwd, 'native-owner-malformed'), null);
2961
+ assert.equal((await readNativeSessionOwnerEvidence(cwd, 'native-owner-malformed')).status, 'malformed');
2962
+ await assert.rejects(writeNativeSessionOwner(cwd, 'native-owner-malformed', { pid: 22, platform: 'win32' }), (error) => isSessionPointerLaunchAbort(error)
2963
+ && error.code === 'session_pointer_unusable'
2964
+ && error.pointerStatus === 'malformed');
2965
+ const forgedDir = join(cwd, '.omx', 'state', 'sessions', 'native-owner-forged');
2966
+ await mkdir(forgedDir, { recursive: true });
2967
+ const forgedPath = join(forgedDir, 'session-owner.json');
2968
+ await writeFile(forgedPath, JSON.stringify({
2969
+ session_id: 'native-owner-other',
2970
+ native_session_id: 'native-owner-other',
2971
+ started_at: new Date().toISOString(),
2972
+ cwd,
2973
+ pid: 22,
2974
+ platform: 'linux',
2975
+ pid_start_ticks: 1,
2976
+ }), 'utf-8');
2977
+ const forgedBefore = await readFile(forgedPath, 'utf-8');
2978
+ assert.equal(await readNativeSessionOwner(cwd, 'native-owner-forged'), null);
2979
+ await assert.rejects(writeNativeSessionOwner(cwd, 'native-owner-forged', { pid: 22, platform: 'linux' }), (error) => isSessionPointerLaunchAbort(error)
2980
+ && error.code === 'session_pointer_owner_conflict');
2981
+ assert.equal(await readFile(forgedPath, 'utf-8'), forgedBefore);
2982
+ const missingCwdDir = join(cwd, '.omx', 'state', 'sessions', 'native-owner-missing-cwd');
2983
+ await mkdir(missingCwdDir, { recursive: true });
2984
+ const missingCwdPath = join(missingCwdDir, 'session-owner.json');
2985
+ await writeFile(missingCwdPath, JSON.stringify({
2986
+ session_id: 'native-owner-missing-cwd',
2987
+ native_session_id: 'native-owner-missing-cwd',
2988
+ started_at: new Date().toISOString(),
2989
+ pid: 22,
2990
+ platform: 'linux',
2991
+ pid_start_ticks: 1,
2992
+ }), 'utf-8');
2993
+ const missingCwdBefore = await readFile(missingCwdPath, 'utf-8');
2994
+ assert.equal(await readNativeSessionOwner(cwd, 'native-owner-missing-cwd'), null);
2995
+ await assert.rejects(writeNativeSessionOwner(cwd, 'native-owner-missing-cwd', { pid: 22, platform: 'linux' }), (error) => isSessionPointerLaunchAbort(error)
2996
+ && error.code === 'session_pointer_owner_conflict');
2997
+ assert.equal(await readFile(missingCwdPath, 'utf-8'), missingCwdBefore);
2998
+ });
2999
+ }
3000
+ finally {
3001
+ await rm(cwd, { recursive: true, force: true });
3002
+ }
3003
+ });
3004
+ it('rejects cross-process native reconciliation while preserving the live selected pointer', async () => {
3005
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-cross-process-selected-'));
3006
+ try {
3007
+ await withPointerDependencies({
3008
+ runtimePlatform: 'win32',
3009
+ observeProcess: () => matchingWin32Observation(),
3010
+ probePid: () => 'alive',
3011
+ }, async () => {
3012
+ await writeSessionStart(cwd, 'native-selected-a', {
3013
+ nativeSessionId: 'native-selected-a',
3014
+ pid: 11,
3015
+ platform: 'win32',
3016
+ });
3017
+ const context = resolveSessionPointerContext(cwd);
3018
+ const before = await readFile(context.sessionPath, 'utf-8');
3019
+ await assert.rejects(reconcileNativeSessionStart(cwd, 'native-selected-b', { pid: 22, platform: 'win32' }), (error) => isSessionPointerLaunchAbort(error)
3020
+ && error.code === 'session_pointer_owner_conflict');
3021
+ assert.equal(await readFile(context.sessionPath, 'utf-8'), before);
3022
+ });
3023
+ }
3024
+ finally {
3025
+ await rm(cwd, { recursive: true, force: true });
3026
+ }
3027
+ });
3028
+ it('preserves pointer evidence when history cannot be appended and rejects unusable end states before history or HUD cleanup', async () => {
3029
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-end-preserve-'));
3030
+ try {
3031
+ await withPointerDependencies({
3032
+ runtimePlatform: 'win32',
3033
+ observeProcess: () => matchingWin32Observation(),
3034
+ }, async () => {
3035
+ const context = resolveSessionPointerContext(cwd);
3036
+ const established = await establishLaunchSessionBinding(cwd, 'sess-history', { platform: 'win32' });
3037
+ assert.equal(established.kind, 'committed-released');
3038
+ if (established.kind !== 'committed-released')
3039
+ return;
3040
+ await mkdir(join(cwd, '.omx', 'logs'), { recursive: true });
3041
+ await rm(join(cwd, '.omx', 'logs'), { recursive: true, force: true });
3042
+ await writeFile(join(cwd, '.omx', 'logs'), 'not-a-directory', 'utf-8');
3043
+ await assert.rejects(finalizeBoundOnce(established.binding, 'history-failure'));
3044
+ assert.equal((await readSessionState(cwd))?.session_id, 'sess-history');
3045
+ await rm(join(cwd, '.omx', 'logs'), { force: true });
3046
+ await writeFile(context.sessionPath, '{ malformed', 'utf-8');
3047
+ await assert.rejects(writeSessionEnd(cwd, 'sess-history', { binding: established.binding }), (error) => isSessionPointerLaunchAbort(error)
3048
+ && error.code === 'session_pointer_unusable'
3049
+ && error.pointerStatus === 'malformed');
3050
+ assert.equal(await readFile(context.sessionPath, 'utf-8'), '{ malformed');
3051
+ assert.equal(existsSync(join(cwd, '.omx', 'logs', 'session-history.jsonl')), false);
3052
+ });
3053
+ }
3054
+ finally {
3055
+ await rm(cwd, { recursive: true, force: true });
3056
+ }
3057
+ });
3058
+ });
3059
+ describe('bound launch authority', () => {
3060
+ it('returns one private binding and only capability-free metadata updates', async () => {
3061
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-binding-authority-'));
3062
+ try {
3063
+ const established = await establishLaunchSessionBinding(cwd, 'sess-binding-authority');
3064
+ assert.equal(established.kind, 'committed-released');
3065
+ if (established.kind !== 'committed-released')
3066
+ return;
3067
+ assert.match(established.binding.launchLineageToken, /^[A-Za-z0-9_-]{16,128}$/);
3068
+ assert.equal('handle' in established.binding, false);
3069
+ const metadata = await updateDetachedSessionMetadata(established.binding, {
3070
+ tmuxSessionName: 'omx-authority', tmuxPaneId: '%42',
3071
+ });
3072
+ assert.equal(metadata.kind, 'committed-released');
3073
+ assert.equal((await readSessionState(cwd))?.launch_lineage_token, established.binding.launchLineageToken);
3074
+ const firstFinalization = finalizeBoundOnce(established.binding, 'test');
3075
+ const secondFinalization = finalizeBoundOnce(established.binding, 'duplicate');
3076
+ assert.equal(firstFinalization, secondFinalization);
3077
+ const finalized = await firstFinalization;
3078
+ assert.equal(finalized.finalized, true);
3079
+ assert.equal(finalized.cleanup.comparison?.status, 'matched');
3080
+ assert.equal(existsSync(resolveSessionPointerContext(cwd).sessionPath), false);
3081
+ assert.equal((await closeLaunchSessionBindingOnce(established.binding)).status, 'closed');
3082
+ assert.equal((await closeLaunchSessionBindingOnce(established.binding)).status, 'closed');
3083
+ }
3084
+ finally {
3085
+ await rm(cwd, { recursive: true, force: true });
3086
+ }
3087
+ });
3088
+ it('preserves process_identity across detached metadata updates', async (t) => {
3089
+ if (process.platform !== 'linux') {
3090
+ t.skip('Linux v2 process identity publication semantics are under test.');
3091
+ return;
3092
+ }
3093
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-binding-identity-preserve-'));
3094
+ let binding;
3095
+ try {
3096
+ await withPointerDependencies({
3097
+ runtimePlatform: 'linux',
3098
+ observeProcess: () => matchingObservation('linux'),
3099
+ }, async () => {
3100
+ const established = await establishLaunchSessionBinding(cwd, 'sess-binding-identity-preserve', { platform: 'linux' });
3101
+ assert.equal(established.kind, 'committed-released');
3102
+ if (established.kind !== 'committed-released')
3103
+ return;
3104
+ const establishedBinding = established.binding;
3105
+ binding = establishedBinding;
3106
+ const before = await readSessionState(cwd);
3107
+ assert.ok(before?.process_identity);
3108
+ const metadata = await updateDetachedSessionMetadata(establishedBinding, { tmuxPaneId: '%identity-preserved' });
3109
+ assert.equal(metadata.kind, 'committed-released');
3110
+ const after = await readSessionState(cwd);
3111
+ assert.deepEqual(after?.process_identity, before?.process_identity);
3112
+ assert.equal(after?.identity_schema_version, 2);
3113
+ assert.equal(after?.tmux_pane_id, '%identity-preserved');
3114
+ assert.equal((await finalizeBoundOnce(establishedBinding, 'identity-preserved')).finalized, true);
3115
+ });
3116
+ }
3117
+ finally {
3118
+ if (binding)
3119
+ await closeLaunchSessionBindingOnce(binding).catch(() => { });
3120
+ await rm(cwd, { recursive: true, force: true });
3121
+ }
3122
+ });
3123
+ it('anchors capability acquisition and finalization to context.baseStateDir rather than cwd', async () => {
3124
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-binding-selected-root-'));
3125
+ let binding;
3126
+ try {
3127
+ const baseStateDir = join(cwd, 'selected', 'state');
3128
+ const context = {
3129
+ cwd,
3130
+ baseStateDir,
3131
+ rootSource: 'cwd-default',
3132
+ sessionPath: join(baseStateDir, 'session.json'),
3133
+ lockPath: join(baseStateDir, 'session.json.lock'),
3134
+ };
3135
+ const established = await establishLaunchSessionBinding(cwd, 'sess-selected-root', { context });
3136
+ assert.equal(established.kind, 'committed-released');
3137
+ if (established.kind !== 'committed-released')
3138
+ return;
3139
+ binding = established.binding;
3140
+ assert.equal(binding.context.baseStateDir, baseStateDir);
3141
+ assert.equal(existsSync(context.sessionPath), true);
3142
+ assert.equal(existsSync(join(cwd, '.omx', 'state', 'session.json')), false);
3143
+ assert.equal((await finalizeBoundOnce(binding, 'selected-root')).finalized, true);
3144
+ assert.equal(existsSync(context.sessionPath), false);
3145
+ }
3146
+ finally {
3147
+ if (binding)
3148
+ await closeLaunchSessionBindingOnce(binding).catch(() => { });
3149
+ await rm(cwd, { recursive: true, force: true });
3150
+ }
3151
+ });
3152
+ it('rejects forged and changed-incarnation detached metadata before lifecycle mutation', async () => {
3153
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-binding-metadata-hostile-'));
3154
+ let binding;
3155
+ try {
3156
+ const established = await establishLaunchSessionBinding(cwd, 'sess-binding-metadata-hostile');
3157
+ assert.equal(established.kind, 'committed-released');
3158
+ if (established.kind !== 'committed-released')
3159
+ return;
3160
+ binding = established.binding;
3161
+ const context = resolveSessionPointerContext(cwd);
3162
+ const original = await readFile(context.sessionPath, 'utf-8');
3163
+ let mkdirCalls = 0;
3164
+ await withPointerDependencies({
3165
+ fs: { mkdir: async (path, options) => { mkdirCalls += 1; await mkdir(path, options); } },
3166
+ }, async () => {
3167
+ const forged = await updateDetachedSessionMetadata({ ...binding }, { tmuxPaneId: '%forged' });
3168
+ assert.equal(forged.kind, 'precommit-aborted');
3169
+ });
3170
+ assert.equal(mkdirCalls, 0);
3171
+ assert.equal(await readFile(context.sessionPath, 'utf-8'), original);
3172
+ assert.equal(existsSync(context.lockPath), false);
3173
+ const parsed = JSON.parse(original);
3174
+ const state = { ...parsed, session_id: binding.canonicalSessionId };
3175
+ const changed = JSON.stringify({ ...state, started_at: '1999-01-01T00:00:00.000Z' }, null, 2);
3176
+ await writeFile(context.sessionPath, changed, 'utf-8');
3177
+ const denied = await updateDetachedSessionMetadata(binding, { tmuxPaneId: '%changed' });
3178
+ assert.equal(denied.kind, 'precommit-aborted');
3179
+ assert.equal(await readFile(context.sessionPath, 'utf-8'), changed);
3180
+ assert.equal(existsSync(context.lockPath), false);
3181
+ }
3182
+ finally {
3183
+ if (binding)
3184
+ await closeLaunchSessionBindingOnce(binding).catch(() => { });
3185
+ await rm(cwd, { recursive: true, force: true });
3186
+ }
3187
+ });
3188
+ it('rechecks supported directory identity under the metadata lock before pointer mutation', async () => {
3189
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-binding-metadata-race-'));
3190
+ const retainedCwd = `${cwd}-retained`;
3191
+ let binding;
3192
+ try {
3193
+ const established = await establishLaunchSessionBinding(cwd, 'sess-binding-metadata-race');
3194
+ assert.equal(established.kind, 'committed-released');
3195
+ if (established.kind !== 'committed-released')
3196
+ return;
3197
+ binding = established.binding;
3198
+ const context = resolveSessionPointerContext(cwd);
3199
+ const original = await readFile(context.sessionPath, 'utf-8');
3200
+ let replaced = false;
3201
+ let result;
3202
+ await withPointerDependencies({
3203
+ fs: {
3204
+ mkdir: async (path, options) => {
3205
+ if (!replaced && String(path) === context.baseStateDir) {
3206
+ replaced = true;
3207
+ await rename(cwd, retainedCwd);
3208
+ await mkdir(context.baseStateDir, { recursive: true });
3209
+ await writeFile(context.sessionPath, original, 'utf-8');
3210
+ return;
3211
+ }
3212
+ await mkdir(path, options);
3213
+ },
3214
+ },
3215
+ }, async () => { result = await updateDetachedSessionMetadata(binding, { tmuxPaneId: '%race' }); });
3216
+ assert.equal(result?.kind, 'precommit-aborted');
3217
+ assert.equal(await readFile(context.sessionPath, 'utf-8'), original);
3218
+ assert.equal(await readFile(join(retainedCwd, '.omx', 'state', 'session.json'), 'utf-8'), original);
3219
+ assert.equal(existsSync(context.lockPath), false);
3220
+ }
3221
+ finally {
3222
+ if (binding)
3223
+ await closeLaunchSessionBindingOnce(binding).catch(() => { });
3224
+ await rm(cwd, { recursive: true, force: true });
3225
+ await rm(retainedCwd, { recursive: true, force: true });
3226
+ }
3227
+ });
3228
+ it('permits an ordinary absent launch when directory identity is unsupported', async () => {
3229
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-binding-unsupported-'));
3230
+ let binding;
3231
+ try {
3232
+ const established = await establishLaunchSessionBinding(cwd, 'sess-binding-unsupported', { platform: 'win32' });
3233
+ assert.equal(established.kind, 'committed-released');
3234
+ if (established.kind !== 'committed-released')
3235
+ return;
3236
+ binding = established.binding;
3237
+ assert.deepEqual(binding.directoryIdentity, { kind: 'unsupported', reason: 'platform' });
3238
+ assert.equal((await readSessionState(cwd))?.session_id, 'sess-binding-unsupported');
3239
+ }
3240
+ finally {
3241
+ if (binding)
3242
+ await closeLaunchSessionBindingOnce(binding).catch(() => { });
3243
+ await rm(cwd, { recursive: true, force: true });
3244
+ }
3245
+ });
3246
+ it('establishment preflight is absent-only under the selected pointer lock and preserves every existing pointer byte', async () => {
3247
+ const root = await mkdtemp(join(tmpdir(), 'omx-session-binding-preflight-'));
3248
+ try {
3249
+ const cases = [
3250
+ { name: 'usable', raw: JSON.stringify(makeState({ cwd: join(root, 'usable'), pid: process.pid, platform: 'linux', pid_start_ticks: 1 })) },
3251
+ { name: 'stale-dead', raw: JSON.stringify(makeState({ cwd: join(root, 'stale-dead'), pid: 424242, platform: 'linux', pid_start_ticks: 1 })), probePid: 'dead' },
3252
+ { name: 'malformed', raw: '{ malformed pointer' },
3253
+ { name: 'foreign', raw: JSON.stringify(makeState({ cwd: join(root, 'foreign', 'other'), platform: 'win32' })) },
3254
+ { name: 'indeterminate', raw: JSON.stringify(makeState({ cwd: join(root, 'indeterminate'), pid: 424242, platform: 'linux', pid_start_ticks: 1 })), probePid: 'indeterminate' },
3255
+ ];
3256
+ for (const testCase of cases) {
3257
+ const cwd = join(root, testCase.name);
3258
+ const context = resolveSessionPointerContext(cwd);
3259
+ const order = [];
3260
+ await mkdir(context.baseStateDir, { recursive: true });
3261
+ await writeFile(context.sessionPath, testCase.raw, 'utf-8');
3262
+ await withPointerDependencies({
3263
+ probePid: () => testCase.probePid ?? 'alive',
3264
+ observeProcess: () => matchingObservation(),
3265
+ fs: {
3266
+ mkdir: async (path, options) => { order.push(`mkdir:${path}`); await mkdir(path, options); },
3267
+ readFile: async (path, encoding) => { order.push(`read:${path}`); return await readFile(path, encoding); },
3268
+ rename: async (from, to) => { order.push(`rename:${from}:${to}`); await rename(from, to); },
3269
+ unlink: async (path) => { order.push(`unlink:${path}`); await rm(path, { force: false }); },
3270
+ },
3271
+ }, async () => {
3272
+ const established = await establishLaunchSessionBinding(cwd, `sess-preflight-${testCase.name}`);
3273
+ assert.equal(established.kind, 'precommit-aborted', testCase.name);
3274
+ });
3275
+ assert.equal(await readFile(context.sessionPath, 'utf-8'), testCase.raw, testCase.name);
3276
+ assert.ok(order.indexOf(`mkdir:${context.lockPath}`) < order.indexOf(`read:${context.sessionPath}`), testCase.name);
3277
+ assert.equal(order.filter((entry) => entry === `rename:${context.sessionPath}`).length, 0, testCase.name);
3278
+ assert.equal(order.filter((entry) => entry === `unlink:${context.sessionPath}`).length, 0, testCase.name);
3279
+ assert.equal(existsSync(context.lockPath), false, testCase.name);
3280
+ }
3281
+ }
3282
+ finally {
3283
+ await rm(root, { recursive: true, force: true });
3284
+ }
3285
+ });
3286
+ it('denies an absent bound pointer without creating synthetic terminal history', async () => {
3287
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-binding-absent-finalization-'));
3288
+ let binding;
3289
+ try {
3290
+ const established = await establishLaunchSessionBinding(cwd, 'sess-binding-absent-finalization');
3291
+ assert.equal(established.kind, 'committed-released');
3292
+ if (established.kind !== 'committed-released')
3293
+ return;
3294
+ binding = established.binding;
3295
+ const context = resolveSessionPointerContext(cwd);
3296
+ await rm(context.sessionPath);
3297
+ const finalized = await finalizeBoundOnce(binding, 'absent');
3298
+ assert.equal(finalized.finalized, false);
3299
+ assert.equal(existsSync(join(cwd, '.omx', 'logs', 'session-history.jsonl')), false);
3300
+ assert.equal(existsSync(join(cwd, '.omx', 'logs', `omx-${todayIsoDate()}.jsonl`)), true);
3301
+ }
3302
+ finally {
3303
+ if (binding)
3304
+ await closeLaunchSessionBindingOnce(binding).catch(() => { });
3305
+ await rm(cwd, { recursive: true, force: true });
3306
+ }
3307
+ });
3308
+ it('rejects unbound usable finalization before changing any lifecycle bytes', async () => {
3309
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-binding-unbound-'));
3310
+ let binding;
3311
+ try {
3312
+ const established = await establishLaunchSessionBinding(cwd, 'sess-binding-unbound');
3313
+ assert.equal(established.kind, 'committed-released');
3314
+ if (established.kind !== 'committed-released')
3315
+ return;
3316
+ binding = established.binding;
3317
+ const context = resolveSessionPointerContext(cwd);
3318
+ const historyPath = join(cwd, '.omx', 'logs', 'session-history.jsonl');
3319
+ const dailyPath = join(cwd, '.omx', 'logs', `omx-${todayIsoDate()}.jsonl`);
3320
+ const hudPath = join(cwd, '.omx', 'state', 'hud-state.json');
3321
+ const metricsPath = join(cwd, '.omx', 'metrics.json');
3322
+ await writeFile(historyPath, 'history-sentinel\n', 'utf-8');
3323
+ await writeFile(hudPath, 'hud-sentinel\n', 'utf-8');
3324
+ await writeFile(metricsPath, 'metrics-sentinel\n', 'utf-8');
3325
+ const before = {
3326
+ pointer: await readFile(context.sessionPath, 'utf-8'),
3327
+ history: await readFile(historyPath, 'utf-8'),
3328
+ daily: await readFile(dailyPath, 'utf-8'),
3329
+ hud: await readFile(hudPath, 'utf-8'),
3330
+ metrics: await readFile(metricsPath, 'utf-8'),
3331
+ };
3332
+ await assert.rejects(() => writeSessionEnd(cwd, 'sess-binding-unbound'), isSessionPointerLaunchAbort);
3333
+ assert.equal(await readFile(context.sessionPath, 'utf-8'), before.pointer);
3334
+ assert.equal(await readFile(historyPath, 'utf-8'), before.history);
3335
+ assert.equal(await readFile(dailyPath, 'utf-8'), before.daily);
3336
+ assert.equal(await readFile(hudPath, 'utf-8'), before.hud);
3337
+ assert.equal(await readFile(metricsPath, 'utf-8'), before.metrics);
3338
+ assert.equal(existsSync(context.lockPath), false);
3339
+ }
3340
+ finally {
3341
+ if (binding)
3342
+ await closeLaunchSessionBindingOnce(binding).catch(() => { });
3343
+ await rm(cwd, { recursive: true, force: true });
3344
+ }
3345
+ });
3346
+ it('preserves a compatible tokenless pointer and refuses to establish cleanup authority from it', async () => {
3347
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-binding-tokenless-'));
3348
+ try {
3349
+ const context = resolveSessionPointerContext(cwd);
3350
+ await reconcileNativeSessionStart(cwd, 'native-compatible-tokenless');
3351
+ const beforeUpdate = await readFile(context.sessionPath, 'utf-8');
3352
+ await assert.rejects(() => writeSessionStart(cwd, 'native-compatible-tokenless', { tmuxPaneId: '%42' }), isSessionPointerLaunchAbort);
3353
+ assert.equal((await readSessionState(cwd))?.launch_lineage_token, undefined);
3354
+ const afterUpdate = await readFile(context.sessionPath, 'utf-8');
3355
+ assert.equal(afterUpdate, beforeUpdate);
3356
+ const established = await establishLaunchSessionBinding(cwd, 'native-compatible-tokenless');
3357
+ assert.equal(established.kind, 'precommit-aborted');
3358
+ if (established.kind !== 'precommit-aborted')
3359
+ return;
3360
+ assert.equal(established.abort.committed, false);
3361
+ assert.equal(established.abort.code, 'session_pointer_owner_conflict');
3362
+ assert.equal((await readSessionState(cwd))?.launch_lineage_token, undefined);
3363
+ assert.equal(afterUpdate.includes('launch_lineage_token'), false);
3364
+ assert.equal(await readFile(context.sessionPath, 'utf-8'), afterUpdate);
3365
+ }
3366
+ finally {
3367
+ await rm(cwd, { recursive: true, force: true });
3368
+ }
3369
+ });
3370
+ it('preserves a compatible malformed lineage token without granting cleanup authority', async () => {
3371
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-binding-malformed-token-'));
3372
+ try {
3373
+ const context = resolveSessionPointerContext(cwd);
3374
+ const tokenless = await reconcileNativeSessionStart(cwd, 'native-compatible-malformed-token');
3375
+ await writeFile(context.sessionPath, JSON.stringify({ ...tokenless, launch_lineage_token: 'bad' }), 'utf-8');
3376
+ const beforeUpdate = await readFile(context.sessionPath, 'utf-8');
3377
+ await assert.rejects(() => writeSessionStart(cwd, 'native-compatible-malformed-token', { tmuxPaneId: '%42' }), isSessionPointerLaunchAbort);
3378
+ const afterUpdate = await readFile(context.sessionPath, 'utf-8');
3379
+ assert.equal(afterUpdate, beforeUpdate);
3380
+ assert.match(afterUpdate, /"launch_lineage_token":"bad"|"launch_lineage_token": "bad"/);
3381
+ assert.equal((await reconcileNativeSessionStart(cwd, 'native-compatible-malformed-token')).launch_lineage_token, 'bad');
3382
+ const beforeEstablishment = await readFile(context.sessionPath, 'utf-8');
3383
+ const established = await establishLaunchSessionBinding(cwd, 'native-compatible-malformed-token');
3384
+ assert.equal(established.kind, 'precommit-aborted');
3385
+ if (established.kind !== 'precommit-aborted')
3386
+ return;
3387
+ assert.equal(established.abort.committed, false);
3388
+ assert.equal(await readFile(context.sessionPath, 'utf-8'), beforeEstablishment);
3389
+ }
3390
+ finally {
3391
+ await rm(cwd, { recursive: true, force: true });
3392
+ }
3393
+ });
3394
+ it('preserves malformed pointer bytes when binding establishment aborts before commit', async () => {
3395
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-binding-malformed-'));
3396
+ try {
3397
+ const context = resolveSessionPointerContext(cwd);
3398
+ const malformed = '{ malformed pointer';
3399
+ await mkdir(context.baseStateDir, { recursive: true });
3400
+ await writeFile(context.sessionPath, malformed, 'utf-8');
3401
+ const established = await establishLaunchSessionBinding(cwd, 'sess-binding-malformed');
3402
+ assert.equal(established.kind, 'precommit-aborted');
3403
+ if (established.kind !== 'precommit-aborted')
3404
+ return;
3405
+ assert.equal(established.abort.committed, false);
3406
+ assert.equal(established.abort.code, 'session_pointer_unusable');
3407
+ assert.equal(established.abort.pointerStatus, 'malformed');
3408
+ assert.equal(await readFile(context.sessionPath, 'utf-8'), malformed);
727
3409
  }
728
3410
  finally {
729
3411
  await rm(cwd, { recursive: true, force: true });
730
3412
  }
731
3413
  });
732
- it('types state-directory, pointer-read, owner-sync, owner-rename, and invalid-token failures before commit', async () => {
733
- const failures = [
734
- {
735
- name: 'state-dir',
736
- operation: 'state-dir-create',
737
- dependencies: (context) => ({
738
- fs: {
739
- mkdir: async (path, options) => {
740
- if (path === context.baseStateDir)
741
- throw new Error('state directory failure');
742
- await mkdir(path, options);
743
- },
744
- },
745
- }),
746
- },
747
- {
748
- name: 'pointer-read',
749
- operation: 'pointer-read',
750
- dependencies: (context) => ({
751
- token: () => TEST_TOKEN,
752
- fs: {
753
- readFile: async (path, encoding) => {
754
- if (path === context.sessionPath)
755
- throw new Error('pointer read failure');
756
- return await readFile(path, encoding);
757
- },
758
- },
759
- }),
760
- },
761
- {
762
- name: 'owner-sync',
763
- operation: 'lock-owner-publish',
764
- dependencies: (context) => ({
765
- token: () => TEST_TOKEN,
766
- fs: {
767
- openAndSync: async (path) => {
768
- if (path === join(context.lockPath, `owner.${TEST_TOKEN}.tmp`))
769
- throw new Error('owner sync failure');
770
- },
771
- },
772
- }),
773
- },
774
- {
775
- name: 'owner-rename',
776
- operation: 'lock-owner-publish',
777
- dependencies: (context) => ({
778
- token: () => TEST_TOKEN,
779
- fs: {
780
- rename: async (from, to) => {
781
- if (from === join(context.lockPath, `owner.${TEST_TOKEN}.tmp`))
782
- throw new Error('owner rename failure');
783
- await rename(from, to);
784
- },
785
- },
786
- }),
787
- },
788
- ];
789
- for (const failure of failures) {
790
- const cwd = await mkdtemp(join(tmpdir(), `omx-session-${failure.name}-`));
791
- try {
792
- const context = resolveSessionPointerContext(cwd);
793
- await withPointerDependencies(failure.dependencies(context), async () => {
794
- await assert.rejects(writeSessionStart(cwd, 'sess-precommit', { platform: 'win32' }), (error) => isSessionPointerLaunchAbort(error)
795
- && error.code === 'session_pointer_io_failure'
796
- && error.operation === failure.operation);
797
- });
798
- assert.equal(existsSync(context.lockPath), false);
799
- }
800
- finally {
801
- await rm(cwd, { recursive: true, force: true });
802
- }
3414
+ it('finalizes only its exact stale-dead native pointer after history is retained', async () => {
3415
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-binding-stale-dead-'));
3416
+ try {
3417
+ const established = await establishLaunchSessionBinding(cwd, 'sess-binding-stale-dead');
3418
+ assert.equal(established.kind, 'committed-released');
3419
+ if (established.kind !== 'committed-released')
3420
+ return;
3421
+ await reconcileNativeSessionStart(cwd, 'native-binding-stale-dead', { pid: 424242 });
3422
+ await withPointerDependencies({ probePid: () => 'dead' }, async () => {
3423
+ const finalized = await finalizeBoundOnce(established.binding, 'test');
3424
+ assert.equal(finalized.finalized, true);
3425
+ });
3426
+ await closeLaunchSessionBindingOnce(established.binding);
3427
+ assert.equal(existsSync(resolveSessionPointerContext(cwd).sessionPath), false);
3428
+ const history = await readFile(join(cwd, '.omx', 'logs', 'session-history.jsonl'), 'utf-8');
3429
+ assert.match(history, /native-binding-stale-dead/);
803
3430
  }
804
- const cwd = await mkdtemp(join(tmpdir(), 'omx-session-invalid-token-'));
3431
+ finally {
3432
+ await rm(cwd, { recursive: true, force: true });
3433
+ }
3434
+ });
3435
+ it('preserves a stale-dead pointer whose bound lineage token does not match', async () => {
3436
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-binding-stale-mismatch-'));
3437
+ let binding;
805
3438
  try {
3439
+ const established = await establishLaunchSessionBinding(cwd, 'sess-binding-stale-mismatch');
3440
+ assert.equal(established.kind, 'committed-released');
3441
+ if (established.kind !== 'committed-released')
3442
+ return;
3443
+ binding = established.binding;
806
3444
  const context = resolveSessionPointerContext(cwd);
807
- await withPointerDependencies({ token: () => 'bad' }, async () => {
808
- await assert.rejects(writeSessionStart(cwd, 'sess-invalid-token', { platform: 'win32' }), (error) => isSessionPointerLaunchAbort(error)
809
- && error.code === 'session_pointer_io_failure'
810
- && error.operation === 'lock-owner-publish');
3445
+ const state = await reconcileNativeSessionStart(cwd, 'native-binding-stale-mismatch', { pid: 424242 });
3446
+ await writeFile(context.sessionPath, JSON.stringify({ ...state, launch_lineage_token: FOREIGN_TOKEN }), 'utf-8');
3447
+ await withPointerDependencies({ probePid: () => 'dead' }, async () => {
3448
+ const denied = await finalizeBoundOnce(binding, 'test');
3449
+ assert.equal(denied.finalized, false);
811
3450
  });
812
- assert.equal(existsSync(context.lockPath), false);
813
- assert.equal(existsSync(`${context.sessionPath}.tmp-bad`), false);
3451
+ assert.equal(existsSync(context.sessionPath), true);
3452
+ assert.equal(existsSync(join(cwd, '.omx', 'logs', 'session-history.jsonl')), false);
814
3453
  }
815
3454
  finally {
3455
+ if (binding)
3456
+ await closeLaunchSessionBindingOnce(binding).catch(() => { });
816
3457
  await rm(cwd, { recursive: true, force: true });
817
3458
  }
818
3459
  });
819
- it('types pointer write, sync, and rename failures and removes only the owned temporary file', async () => {
820
- const phases = [
821
- { operation: 'pointer-temp-write', fail: 'write' },
822
- { operation: 'pointer-fsync', fail: 'sync' },
823
- { operation: 'pointer-rename', fail: 'rename' },
824
- ];
825
- for (const phase of phases) {
826
- const cwd = await mkdtemp(join(tmpdir(), `omx-session-${phase.fail}-`));
827
- try {
828
- const context = resolveSessionPointerContext(cwd);
829
- const tempPath = `${context.sessionPath}.tmp-${TEST_TOKEN}`;
3460
+ it('finalizes an exact bound current process when identity observation is unavailable', async () => {
3461
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-binding-indeterminate-current-'));
3462
+ let binding;
3463
+ try {
3464
+ const established = await establishLaunchSessionBinding(cwd, 'sess-binding-indeterminate-current');
3465
+ assert.equal(established.kind, 'committed-released');
3466
+ if (established.kind !== 'committed-released')
3467
+ return;
3468
+ binding = established.binding;
3469
+ await withPointerDependencies({
3470
+ probePid: () => 'indeterminate',
3471
+ observeProcess: () => ({ kind: 'error' }),
3472
+ }, async () => {
3473
+ await writeSessionEnd(cwd, binding.canonicalSessionId, { binding });
3474
+ });
3475
+ assert.equal(await readSessionState(cwd), null);
3476
+ }
3477
+ finally {
3478
+ if (binding)
3479
+ await closeLaunchSessionBindingOnce(binding).catch(() => { });
3480
+ await rm(cwd, { recursive: true, force: true });
3481
+ }
3482
+ });
3483
+ it('preserves usable and stale bound pointers without the exact valid lineage token before any mutation', async () => {
3484
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-binding-token-gate-'));
3485
+ let binding;
3486
+ try {
3487
+ const established = await establishLaunchSessionBinding(cwd, 'sess-binding-token-gate');
3488
+ assert.equal(established.kind, 'committed-released');
3489
+ if (established.kind !== 'committed-released')
3490
+ return;
3491
+ binding = established.binding;
3492
+ const context = resolveSessionPointerContext(cwd);
3493
+ const state = await readSessionState(cwd);
3494
+ assert.ok(state);
3495
+ const historyPath = join(cwd, '.omx', 'logs', 'session-history.jsonl');
3496
+ const dailyPath = join(cwd, '.omx', 'logs', `omx-${todayIsoDate()}.jsonl`);
3497
+ const hudPath = join(cwd, '.omx', 'state', 'hud-state.json');
3498
+ const metricsPath = join(cwd, '.omx', 'metrics.json');
3499
+ await writeFile(historyPath, 'history-sentinel\n', 'utf-8');
3500
+ await writeFile(hudPath, 'hud-sentinel\n', 'utf-8');
3501
+ await writeFile(metricsPath, 'metrics-sentinel\n', 'utf-8');
3502
+ const lifecycleBytes = {
3503
+ history: await readFile(historyPath, 'utf-8'),
3504
+ daily: await readFile(dailyPath, 'utf-8'),
3505
+ hud: await readFile(hudPath, 'utf-8'),
3506
+ metrics: await readFile(metricsPath, 'utf-8'),
3507
+ };
3508
+ const cases = [
3509
+ { name: 'tokenless usable', state: (() => { const { launch_lineage_token: _, ...tokenless } = state; return tokenless; })() },
3510
+ { name: 'malformed usable', state: { ...state, launch_lineage_token: 'bad' } },
3511
+ { name: 'mismatched usable', state: { ...state, launch_lineage_token: FOREIGN_TOKEN } },
3512
+ { name: 'replaced usable', state: { ...state, launch_lineage_token: SUCCESSOR_TOKEN } },
3513
+ { name: 'same-token changed incarnation', state: { ...state, started_at: '1999-01-01T00:00:00.000Z' } },
3514
+ { name: 'unknown identity', state: { ...state, pid: 424242 }, probePid: 'indeterminate' },
3515
+ {
3516
+ name: 'tokenless stale native',
3517
+ state: (() => {
3518
+ const { launch_lineage_token: _, ...tokenless } = state;
3519
+ return { ...tokenless, native_session_id: 'native-tokenless-stale', pid: 424242 };
3520
+ })(),
3521
+ probePid: 'dead',
3522
+ },
3523
+ ];
3524
+ for (const testCase of cases) {
3525
+ const raw = JSON.stringify(testCase.state);
3526
+ let mkdirCalls = 0;
3527
+ let unlinkCalls = 0;
3528
+ await writeFile(context.sessionPath, raw, 'utf-8');
830
3529
  await withPointerDependencies({
831
- token: () => TEST_TOKEN,
3530
+ probePid: () => testCase.probePid ?? 'alive',
832
3531
  fs: {
833
- writeFile: async (path, data, options) => {
834
- await writeFile(path, data, options);
835
- if (phase.fail === 'write' && path === tempPath)
836
- throw new Error('pointer write failure');
837
- },
838
- openAndSync: async (path) => {
839
- if (phase.fail === 'sync' && path === tempPath)
840
- throw new Error('pointer sync failure');
841
- },
842
- rename: async (from, to) => {
843
- if (phase.fail === 'rename' && from === tempPath && to === context.sessionPath) {
844
- throw new Error('pointer rename failure');
845
- }
846
- await rename(from, to);
847
- },
3532
+ mkdir: async (path, options) => { mkdirCalls += 1; await mkdir(path, options); },
3533
+ unlink: async (path) => { if (path === context.sessionPath)
3534
+ unlinkCalls += 1; await rm(path, { force: false }); },
848
3535
  },
849
3536
  }, async () => {
850
- await assert.rejects(writeSessionStart(cwd, 'sess-failure', { platform: 'win32' }), (error) => isSessionPointerLaunchAbort(error)
851
- && error.code === 'session_pointer_io_failure'
852
- && error.operation === phase.operation);
3537
+ await assert.rejects(writeSessionEnd(cwd, binding.canonicalSessionId, { binding }), isSessionPointerLaunchAbort);
853
3538
  });
854
- assert.equal(existsSync(tempPath), false);
855
- assert.equal(existsSync(context.lockPath), false);
856
- }
857
- finally {
858
- await rm(cwd, { recursive: true, force: true });
3539
+ assert.equal(await readFile(context.sessionPath, 'utf-8'), raw, testCase.name);
3540
+ assert.ok(mkdirCalls <= 1, testCase.name);
3541
+ assert.equal(unlinkCalls, 0, testCase.name);
3542
+ assert.equal(existsSync(context.lockPath), false, testCase.name);
3543
+ assert.equal(await readFile(historyPath, 'utf-8'), lifecycleBytes.history, testCase.name);
3544
+ assert.equal(await readFile(dailyPath, 'utf-8'), lifecycleBytes.daily, testCase.name);
3545
+ assert.equal(await readFile(hudPath, 'utf-8'), lifecycleBytes.hud, testCase.name);
3546
+ assert.equal(await readFile(metricsPath, 'utf-8'), lifecycleBytes.metrics, testCase.name);
859
3547
  }
860
3548
  }
3549
+ finally {
3550
+ if (binding)
3551
+ await closeLaunchSessionBindingOnce(binding).catch(() => { });
3552
+ await rm(cwd, { recursive: true, force: true });
3553
+ }
861
3554
  });
862
- it('preserves owned pointer residue and ordered cleanup evidence when cleanup or release cannot complete', async () => {
863
- const cwd = await mkdtemp(join(tmpdir(), 'omx-session-pointer-residue-'));
3555
+ it('denies selected-root replacement under lock before pointer publication', async () => {
3556
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-establish-root-race-'));
3557
+ const displacedRoot = join(cwd, 'displaced-state');
864
3558
  try {
865
3559
  const context = resolveSessionPointerContext(cwd);
866
- const tempPath = `${context.sessionPath}.tmp-${TEST_TOKEN}`;
3560
+ let replaced = false;
3561
+ let established;
867
3562
  await withPointerDependencies({
868
- token: () => TEST_TOKEN,
869
3563
  fs: {
870
- writeFile: async (path, data, options) => {
871
- await writeFile(path, data, options);
872
- if (path === tempPath)
873
- throw new Error('pointer write failure');
874
- },
875
- unlink: async (path) => {
876
- if (path === tempPath)
877
- throw new Error('pointer temp cleanup failure');
878
- await rm(path, { force: false });
879
- },
880
- rmdir: async (path) => {
881
- if (path.endsWith(`.release-${TEST_TOKEN}`))
882
- throw new Error('release cleanup failure');
883
- await rm(path, { recursive: false });
3564
+ openAndSync: async (path) => {
3565
+ if (!replaced && path.startsWith(`${context.sessionPath}.tmp-`)) {
3566
+ replaced = true;
3567
+ await rename(context.baseStateDir, displacedRoot);
3568
+ await mkdir(context.baseStateDir, { recursive: true });
3569
+ }
3570
+ return 'synced';
884
3571
  },
885
3572
  },
886
- }, async () => {
887
- await assert.rejects(writeSessionStart(cwd, 'sess-residue', { platform: 'win32' }), (error) => isSessionPointerLaunchAbort(error)
888
- && error.code === 'session_pointer_lock_recovery_required'
889
- && error.primaryOperation === 'pointer-temp-write'
890
- && error.secondaryFailures?.map((failure) => failure.phase).join(',') === 'remove-pointer-temp,remove-release-dir'
891
- && error.secondaryFailures?.[0]?.evidencePath === tempPath);
892
- });
893
- assert.equal(existsSync(tempPath), true);
894
- assert.equal(existsSync(`${context.lockPath}.release-${TEST_TOKEN}`), true);
3573
+ }, async () => { established = await establishLaunchSessionBinding(cwd, 'sess-establish-root-race'); });
3574
+ assert.ok(established);
3575
+ assert.notEqual(established.kind, 'committed-released');
3576
+ assert.equal(existsSync(context.sessionPath), false);
3577
+ assert.equal(existsSync(join(displacedRoot, 'session.json')), false);
895
3578
  }
896
3579
  finally {
897
3580
  await rm(cwd, { recursive: true, force: true });
898
3581
  }
899
3582
  });
900
- it('leaves foreign pointer temps inert and a successor commits after explicit lock recovery', async () => {
901
- const cwd = await mkdtemp(join(tmpdir(), 'omx-session-pointer-successor-'));
3583
+ it('returns committed blocked evidence when selected root changes after pointer rename', async () => {
3584
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-establish-post-rename-race-'));
3585
+ const displacedRoot = join(cwd, 'displaced-after-rename');
902
3586
  try {
903
3587
  const context = resolveSessionPointerContext(cwd);
904
- const ownTempPath = `${context.sessionPath}.tmp-${TEST_TOKEN}`;
905
- const foreignTempPath = `${context.sessionPath}.tmp-${FOREIGN_TOKEN}`;
906
- await mkdir(context.baseStateDir, { recursive: true });
907
- await writeFile(foreignTempPath, 'foreign transaction evidence', 'utf-8');
3588
+ let replaced = false;
3589
+ let established;
908
3590
  await withPointerDependencies({
909
- token: () => TEST_TOKEN,
910
3591
  fs: {
911
- writeFile: async (path, data, options) => {
912
- await writeFile(path, data, options);
913
- if (path === ownTempPath)
914
- throw new Error('pointer write failure');
915
- },
916
- unlink: async (path) => {
917
- if (path === ownTempPath)
918
- throw new Error('keep own residue');
919
- await rm(path, { force: false });
920
- },
921
3592
  rename: async (from, to) => {
922
- if (from === context.lockPath)
923
- throw new Error('preserve canonical lock');
924
3593
  await rename(from, to);
3594
+ if (!replaced && to === context.sessionPath) {
3595
+ replaced = true;
3596
+ await rename(context.baseStateDir, displacedRoot);
3597
+ await mkdir(context.baseStateDir, { recursive: true });
3598
+ }
925
3599
  },
926
3600
  },
927
- }, async () => {
928
- await assert.rejects(writeSessionStart(cwd, 'sess-first', { platform: 'win32' }), isSessionPointerLaunchAbort);
929
- });
930
- assert.equal(existsSync(foreignTempPath), true);
931
- assert.equal(existsSync(context.lockPath), true);
932
- // This is documented stopped-session operator recovery, deliberately not
933
- // product transaction behavior.
934
- await rm(context.lockPath, { recursive: true, force: true });
935
- await withPointerDependencies({ token: () => SUCCESSOR_TOKEN }, async () => {
936
- await writeSessionStart(cwd, 'sess-successor', { platform: 'win32' });
937
- });
938
- assert.equal((await readSessionState(cwd))?.session_id, 'sess-successor');
939
- assert.equal(existsSync(ownTempPath), true);
940
- assert.equal(existsSync(foreignTempPath), true);
3601
+ }, async () => { established = await establishLaunchSessionBinding(cwd, 'sess-establish-post-rename-race'); });
3602
+ assert.ok(established);
3603
+ assert.equal(established.kind, 'committed-release-failed');
3604
+ assert.equal(existsSync(context.sessionPath), false);
3605
+ assert.equal(existsSync(join(displacedRoot, 'session.json')), true);
3606
+ }
3607
+ finally {
3608
+ await rm(cwd, { recursive: true, force: true });
3609
+ }
3610
+ });
3611
+ it('denies same-path replacement before creating state, lock, history, or HUD artifacts', async () => {
3612
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-binding-replaced-path-'));
3613
+ const retainedCwd = `${cwd}-retained`;
3614
+ let binding;
3615
+ try {
3616
+ const established = await establishLaunchSessionBinding(cwd, 'sess-binding-replaced-path');
3617
+ assert.equal(established.kind, 'committed-released');
3618
+ if (established.kind !== 'committed-released')
3619
+ return;
3620
+ binding = established.binding;
3621
+ await rename(cwd, retainedCwd);
3622
+ await mkdir(cwd);
3623
+ const context = resolveSessionPointerContext(cwd);
3624
+ const finalized = await finalizeBoundOnce(binding, 'test');
3625
+ assert.equal(finalized.finalized, false);
3626
+ assert.equal(finalized.cleanup.comparison?.status, 'denied');
3627
+ assert.equal(existsSync(context.baseStateDir), false);
941
3628
  assert.equal(existsSync(context.lockPath), false);
3629
+ assert.equal(existsSync(join(cwd, '.omx', 'logs', 'session-history.jsonl')), false);
3630
+ assert.equal(existsSync(join(cwd, '.omx', 'state', 'hud-state.json')), false);
942
3631
  }
943
3632
  finally {
3633
+ if (binding)
3634
+ await closeLaunchSessionBindingOnce(binding).catch(() => { });
944
3635
  await rm(cwd, { recursive: true, force: true });
3636
+ await rm(retainedCwd, { recursive: true, force: true });
945
3637
  }
946
3638
  });
947
- it('binds an owner alias only on a verified same-native/absent transition and never during replacement', async () => {
948
- const cwd = await mkdtemp(join(tmpdir(), 'omx-session-owner-alias-'));
3639
+ it('keeps native absent reconciliation tokenless without backfill', async () => {
3640
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-native-tokenless-'));
949
3641
  try {
950
- const nativeOnly = await reconcileNativeSessionStart(cwd, 'native-first', {
951
- platform: 'win32',
952
- ownerOmxSessionId: 'omx-owner',
953
- });
954
- assert.equal(nativeOnly.owner_omx_session_id, undefined);
955
- await withOwnerEnvironment('omx-owner', async () => {
956
- const bound = await reconcileNativeSessionStart(cwd, 'native-first', {
3642
+ assert.equal((await reconcileNativeSessionStart(cwd, 'native-tokenless')).launch_lineage_token, undefined);
3643
+ assert.equal((await reconcileNativeSessionStart(cwd, 'native-tokenless')).launch_lineage_token, undefined);
3644
+ assert.equal((await readSessionState(cwd))?.launch_lineage_token, undefined);
3645
+ }
3646
+ finally {
3647
+ await rm(cwd, { recursive: true, force: true });
3648
+ }
3649
+ });
3650
+ it('allows authorized unsupported-capability stale-dead finalization without weakening bound lineage checks', async () => {
3651
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-binding-unsupported-lifecycle-'));
3652
+ let ordinary;
3653
+ let stale;
3654
+ try {
3655
+ await withPointerDependencies({
3656
+ runtimePlatform: 'win32',
3657
+ observeProcess: () => matchingWin32Observation(),
3658
+ }, async () => {
3659
+ const ordinaryEstablished = await establishLaunchSessionBinding(cwd, 'sess-unsupported-ordinary', { platform: 'win32' });
3660
+ assert.equal(ordinaryEstablished.kind, 'committed-released');
3661
+ if (ordinaryEstablished.kind !== 'committed-released')
3662
+ return;
3663
+ ordinary = ordinaryEstablished.binding;
3664
+ assert.deepEqual(ordinary.directoryIdentity, { kind: 'unsupported', reason: 'platform' });
3665
+ const ordinaryFinalized = await finalizeBoundOnce(ordinary, 'ordinary-unsupported');
3666
+ assert.equal(ordinaryFinalized.finalized, true);
3667
+ assert.equal(existsSync(resolveSessionPointerContext(cwd).sessionPath), false);
3668
+ assert.equal((await closeLaunchSessionBindingOnce(ordinary)).status, 'closed');
3669
+ const staleEstablished = await establishLaunchSessionBinding(cwd, 'sess-unsupported-stale', {
3670
+ nativeSessionId: 'native-unsupported-stale',
957
3671
  platform: 'win32',
958
- ownerAliasVerified: true,
959
3672
  });
960
- assert.equal(bound.session_id, 'native-first');
961
- assert.equal(bound.owner_omx_session_id, 'omx-owner');
962
- const replacement = await reconcileNativeSessionStart(cwd, 'native-second', {
963
- platform: 'win32',
964
- ownerAliasVerified: true,
3673
+ assert.equal(staleEstablished.kind, 'committed-released');
3674
+ if (staleEstablished.kind !== 'committed-released')
3675
+ return;
3676
+ stale = staleEstablished.binding;
3677
+ await reconcileNativeSessionStart(cwd, 'native-unsupported-stale', { pid: 424242, platform: 'win32' });
3678
+ const context = resolveSessionPointerContext(cwd);
3679
+ const historyPath = join(cwd, '.omx', 'logs', 'session-history.jsonl');
3680
+ await withPointerDependencies({
3681
+ runtimePlatform: 'win32',
3682
+ observeProcess: () => matchingWin32Observation(),
3683
+ probePid: () => 'dead',
3684
+ }, async () => {
3685
+ const finalized = await finalizeBoundOnce(stale, 'stale-unsupported');
3686
+ assert.equal(finalized.finalized, true);
3687
+ assert.equal(finalized.cleanup.comparison?.status, 'matched');
3688
+ assert.match(finalized.cleanup.comparison?.reason ?? '', /unsupported-directory-capability:platform/);
965
3689
  });
966
- assert.equal(replacement.session_id, 'native-second');
967
- assert.equal(replacement.owner_omx_session_id, 'omx-owner');
3690
+ assert.equal(existsSync(context.sessionPath), false);
3691
+ const history = await readFile(historyPath, 'utf-8');
3692
+ assert.match(history, /native-unsupported-stale/);
968
3693
  });
969
3694
  }
970
3695
  finally {
3696
+ if (ordinary)
3697
+ await closeLaunchSessionBindingOnce(ordinary).catch(() => { });
3698
+ if (stale)
3699
+ await closeLaunchSessionBindingOnce(stale).catch(() => { });
971
3700
  await rm(cwd, { recursive: true, force: true });
972
3701
  }
973
3702
  });
974
- it('preserves pointer evidence when history cannot be appended and rejects unusable end states before history or HUD cleanup', async () => {
975
- const cwd = await mkdtemp(join(tmpdir(), 'omx-session-end-preserve-'));
3703
+ it('preserves native lineage and tmux metadata when rejecting a live native-ID replacement', async () => {
3704
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-native-lineage-metadata-'));
3705
+ let binding;
3706
+ try {
3707
+ const established = await establishLaunchSessionBinding(cwd, 'sess-native-lineage-metadata', {
3708
+ nativeSessionId: 'native-before',
3709
+ tmuxSessionName: 'omx-native-lineage',
3710
+ tmuxPaneId: '%88',
3711
+ });
3712
+ assert.equal(established.kind, 'committed-released');
3713
+ if (established.kind !== 'committed-released')
3714
+ return;
3715
+ binding = established.binding;
3716
+ await assert.rejects(reconcileNativeSessionStart(cwd, 'native-after', { pid: process.pid }), isOwnerConflict);
3717
+ const persisted = await readSessionState(cwd);
3718
+ assert.equal(persisted?.session_id, binding.canonicalSessionId);
3719
+ assert.equal(persisted?.native_session_id, 'native-before');
3720
+ assert.equal(persisted?.previous_native_session_id, undefined);
3721
+ assert.equal(persisted?.launch_lineage_token, binding.launchLineageToken);
3722
+ assert.equal(persisted?.tmux_session_name, 'omx-native-lineage');
3723
+ assert.equal(persisted?.tmux_pane_id, '%88');
3724
+ }
3725
+ finally {
3726
+ if (binding)
3727
+ await closeLaunchSessionBindingOnce(binding).catch(() => { });
3728
+ await rm(cwd, { recursive: true, force: true });
3729
+ }
3730
+ });
3731
+ it('reports owner unlink residue and does not rmdir it', async () => {
3732
+ const cwd = await mkdtemp(join(tmpdir(), 'omx-session-owner-unlink-'));
976
3733
  try {
977
3734
  const context = resolveSessionPointerContext(cwd);
978
- await writeSessionStart(cwd, 'sess-history', { platform: 'win32' });
979
- await mkdir(join(cwd, '.omx', 'logs'), { recursive: true });
980
- await rm(join(cwd, '.omx', 'logs'), { recursive: true, force: true });
981
- await writeFile(join(cwd, '.omx', 'logs'), 'not-a-directory', 'utf-8');
982
- await assert.rejects(writeSessionEnd(cwd, 'sess-history'));
983
- assert.equal((await readSessionState(cwd))?.session_id, 'sess-history');
984
- await rm(join(cwd, '.omx', 'logs'), { force: true });
985
- await writeFile(context.sessionPath, '{ malformed', 'utf-8');
986
- await assert.rejects(writeSessionEnd(cwd, 'sess-history'), (error) => isSessionPointerLaunchAbort(error)
987
- && error.code === 'session_pointer_unusable'
988
- && error.pointerStatus === 'malformed');
989
- assert.equal(await readFile(context.sessionPath, 'utf-8'), '{ malformed');
990
- assert.equal(existsSync(join(cwd, '.omx', 'logs', 'session-history.jsonl')), false);
3735
+ let rmdirCalls = 0;
3736
+ await withPointerDependencies({
3737
+ token: () => TEST_TOKEN,
3738
+ fs: {
3739
+ unlink: async (path) => {
3740
+ if (path.endsWith(`.release-${TEST_TOKEN}/owner.json`))
3741
+ throw new Error('owner unlink failure');
3742
+ await rm(path, { force: false });
3743
+ },
3744
+ rmdir: async (path) => { rmdirCalls += 1; await rm(path, { recursive: false }); },
3745
+ },
3746
+ }, async () => {
3747
+ const result = await establishLaunchSessionBinding(cwd, 'sess-owner-unlink');
3748
+ assert.equal(result.kind, 'committed-release-failed');
3749
+ if (result.kind === 'committed-release-failed') {
3750
+ assert.equal(result.secondaryFailures[0]?.phase, 'remove-release-owner');
3751
+ assert.equal(result.lockDisposition, 'released-with-residue');
3752
+ }
3753
+ });
3754
+ assert.equal(rmdirCalls, 0);
3755
+ assert.equal(existsSync(`${context.lockPath}.release-${TEST_TOKEN}`), true);
991
3756
  }
992
3757
  finally {
993
3758
  await rm(cwd, { recursive: true, force: true });