oh-my-codex 0.20.3 → 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 (661) hide show
  1. package/Cargo.lock +35 -18
  2. package/Cargo.toml +1 -1
  3. package/README.md +2 -2
  4. package/crates/omx-runtime/Cargo.toml +3 -0
  5. package/crates/omx-runtime/src/main.rs +390 -0
  6. package/crates/omx-runtime/tests/execution.rs +264 -0
  7. package/dist/adapt/__tests__/herdr-target.test.d.ts +2 -0
  8. package/dist/adapt/__tests__/herdr-target.test.d.ts.map +1 -0
  9. package/dist/adapt/__tests__/herdr-target.test.js +72 -0
  10. package/dist/adapt/__tests__/herdr-target.test.js.map +1 -0
  11. package/dist/adapt/contracts.d.ts +1 -1
  12. package/dist/adapt/contracts.d.ts.map +1 -1
  13. package/dist/adapt/contracts.js +1 -1
  14. package/dist/adapt/contracts.js.map +1 -1
  15. package/dist/adapt/herdr/__tests__/authority.test.d.ts +2 -0
  16. package/dist/adapt/herdr/__tests__/authority.test.d.ts.map +1 -0
  17. package/dist/adapt/herdr/__tests__/authority.test.js +49 -0
  18. package/dist/adapt/herdr/__tests__/authority.test.js.map +1 -0
  19. package/dist/adapt/herdr/__tests__/bridge.test.d.ts +2 -0
  20. package/dist/adapt/herdr/__tests__/bridge.test.d.ts.map +1 -0
  21. package/dist/adapt/herdr/__tests__/bridge.test.js +176 -0
  22. package/dist/adapt/herdr/__tests__/bridge.test.js.map +1 -0
  23. package/dist/adapt/herdr/__tests__/sanitize.test.d.ts +2 -0
  24. package/dist/adapt/herdr/__tests__/sanitize.test.d.ts.map +1 -0
  25. package/dist/adapt/herdr/__tests__/sanitize.test.js +45 -0
  26. package/dist/adapt/herdr/__tests__/sanitize.test.js.map +1 -0
  27. package/dist/adapt/herdr/__tests__/semantic.test.d.ts +2 -0
  28. package/dist/adapt/herdr/__tests__/semantic.test.d.ts.map +1 -0
  29. package/dist/adapt/herdr/__tests__/semantic.test.js +130 -0
  30. package/dist/adapt/herdr/__tests__/semantic.test.js.map +1 -0
  31. package/dist/adapt/herdr/__tests__/seq-store.test.d.ts +2 -0
  32. package/dist/adapt/herdr/__tests__/seq-store.test.d.ts.map +1 -0
  33. package/dist/adapt/herdr/__tests__/seq-store.test.js +54 -0
  34. package/dist/adapt/herdr/__tests__/seq-store.test.js.map +1 -0
  35. package/dist/adapt/herdr/__tests__/socket-live.test.d.ts +2 -0
  36. package/dist/adapt/herdr/__tests__/socket-live.test.d.ts.map +1 -0
  37. package/dist/adapt/herdr/__tests__/socket-live.test.js +104 -0
  38. package/dist/adapt/herdr/__tests__/socket-live.test.js.map +1 -0
  39. package/dist/adapt/herdr/__tests__/team-map.test.d.ts +2 -0
  40. package/dist/adapt/herdr/__tests__/team-map.test.d.ts.map +1 -0
  41. package/dist/adapt/herdr/__tests__/team-map.test.js +46 -0
  42. package/dist/adapt/herdr/__tests__/team-map.test.js.map +1 -0
  43. package/dist/adapt/herdr/__tests__/transport.test.d.ts +2 -0
  44. package/dist/adapt/herdr/__tests__/transport.test.d.ts.map +1 -0
  45. package/dist/adapt/herdr/__tests__/transport.test.js +160 -0
  46. package/dist/adapt/herdr/__tests__/transport.test.js.map +1 -0
  47. package/dist/adapt/herdr/__tests__/wiring.test.d.ts +2 -0
  48. package/dist/adapt/herdr/__tests__/wiring.test.d.ts.map +1 -0
  49. package/dist/adapt/herdr/__tests__/wiring.test.js +118 -0
  50. package/dist/adapt/herdr/__tests__/wiring.test.js.map +1 -0
  51. package/dist/adapt/herdr/authority.d.ts +34 -0
  52. package/dist/adapt/herdr/authority.d.ts.map +1 -0
  53. package/dist/adapt/herdr/authority.js +71 -0
  54. package/dist/adapt/herdr/authority.js.map +1 -0
  55. package/dist/adapt/herdr/bridge.d.ts +84 -0
  56. package/dist/adapt/herdr/bridge.d.ts.map +1 -0
  57. package/dist/adapt/herdr/bridge.js +222 -0
  58. package/dist/adapt/herdr/bridge.js.map +1 -0
  59. package/dist/adapt/herdr/sanitize.d.ts +20 -0
  60. package/dist/adapt/herdr/sanitize.d.ts.map +1 -0
  61. package/dist/adapt/herdr/sanitize.js +70 -0
  62. package/dist/adapt/herdr/sanitize.js.map +1 -0
  63. package/dist/adapt/herdr/semantic.d.ts +67 -0
  64. package/dist/adapt/herdr/semantic.d.ts.map +1 -0
  65. package/dist/adapt/herdr/semantic.js +170 -0
  66. package/dist/adapt/herdr/semantic.js.map +1 -0
  67. package/dist/adapt/herdr/seq-store.d.ts +32 -0
  68. package/dist/adapt/herdr/seq-store.d.ts.map +1 -0
  69. package/dist/adapt/herdr/seq-store.js +126 -0
  70. package/dist/adapt/herdr/seq-store.js.map +1 -0
  71. package/dist/adapt/herdr/team-map.d.ts +30 -0
  72. package/dist/adapt/herdr/team-map.d.ts.map +1 -0
  73. package/dist/adapt/herdr/team-map.js +37 -0
  74. package/dist/adapt/herdr/team-map.js.map +1 -0
  75. package/dist/adapt/herdr/transport.d.ts +115 -0
  76. package/dist/adapt/herdr/transport.d.ts.map +1 -0
  77. package/dist/adapt/herdr/transport.js +260 -0
  78. package/dist/adapt/herdr/transport.js.map +1 -0
  79. package/dist/adapt/herdr/wiring.d.ts +24 -0
  80. package/dist/adapt/herdr/wiring.d.ts.map +1 -0
  81. package/dist/adapt/herdr/wiring.js +62 -0
  82. package/dist/adapt/herdr/wiring.js.map +1 -0
  83. package/dist/adapt/herdr.d.ts +19 -0
  84. package/dist/adapt/herdr.d.ts.map +1 -0
  85. package/dist/adapt/herdr.js +115 -0
  86. package/dist/adapt/herdr.js.map +1 -0
  87. package/dist/adapt/index.d.ts +4 -0
  88. package/dist/adapt/index.d.ts.map +1 -1
  89. package/dist/adapt/index.js +48 -0
  90. package/dist/adapt/index.js.map +1 -1
  91. package/dist/adapt/registry.d.ts.map +1 -1
  92. package/dist/adapt/registry.js +11 -0
  93. package/dist/adapt/registry.js.map +1 -1
  94. package/dist/auth/__tests__/hotswap.test.js +60 -1
  95. package/dist/auth/__tests__/hotswap.test.js.map +1 -1
  96. package/dist/auth/__tests__/storage.test.js +33 -1
  97. package/dist/auth/__tests__/storage.test.js.map +1 -1
  98. package/dist/auth/hotswap.d.ts +16 -2
  99. package/dist/auth/hotswap.d.ts.map +1 -1
  100. package/dist/auth/hotswap.js +22 -7
  101. package/dist/auth/hotswap.js.map +1 -1
  102. package/dist/auth/storage.js +1 -1
  103. package/dist/auth/storage.js.map +1 -1
  104. package/dist/autopilot/__tests__/ralplan-gate.test.js +86 -1333
  105. package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
  106. package/dist/autopilot/ralplan-gate.d.ts.map +1 -1
  107. package/dist/autopilot/ralplan-gate.js +7 -6
  108. package/dist/autopilot/ralplan-gate.js.map +1 -1
  109. package/dist/cli/__tests__/agents-init.test.js +4 -23
  110. package/dist/cli/__tests__/agents-init.test.js.map +1 -1
  111. package/dist/cli/__tests__/api.test.js +61 -24
  112. package/dist/cli/__tests__/api.test.js.map +1 -1
  113. package/dist/cli/__tests__/auth.test.js +3 -1
  114. package/dist/cli/__tests__/auth.test.js.map +1 -1
  115. package/dist/cli/__tests__/codex-feature-probe.test.js +66 -1
  116. package/dist/cli/__tests__/codex-feature-probe.test.js.map +1 -1
  117. package/dist/cli/__tests__/codex-plugin-layout.test.js +70 -2
  118. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  119. package/dist/cli/__tests__/detached-leader-teardown.test.d.ts +2 -0
  120. package/dist/cli/__tests__/detached-leader-teardown.test.d.ts.map +1 -0
  121. package/dist/cli/__tests__/detached-leader-teardown.test.js +473 -0
  122. package/dist/cli/__tests__/detached-leader-teardown.test.js.map +1 -0
  123. package/dist/cli/__tests__/doctor-process-identity-readiness.test.d.ts +2 -0
  124. package/dist/cli/__tests__/doctor-process-identity-readiness.test.d.ts.map +1 -0
  125. package/dist/cli/__tests__/doctor-process-identity-readiness.test.js +103 -0
  126. package/dist/cli/__tests__/doctor-process-identity-readiness.test.js.map +1 -0
  127. package/dist/cli/__tests__/doctor-state-root-binding.test.d.ts +2 -0
  128. package/dist/cli/__tests__/doctor-state-root-binding.test.d.ts.map +1 -0
  129. package/dist/cli/__tests__/doctor-state-root-binding.test.js +139 -0
  130. package/dist/cli/__tests__/doctor-state-root-binding.test.js.map +1 -0
  131. package/dist/cli/__tests__/doctor-warning-copy.test.js +35 -3
  132. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  133. package/dist/cli/__tests__/exec.test.js +75 -0
  134. package/dist/cli/__tests__/exec.test.js.map +1 -1
  135. package/dist/cli/__tests__/index.test.js +621 -460
  136. package/dist/cli/__tests__/index.test.js.map +1 -1
  137. package/dist/cli/__tests__/launch-fallback.test.js +1020 -143
  138. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  139. package/dist/cli/__tests__/mcp-parity.test.js +4 -3
  140. package/dist/cli/__tests__/mcp-parity.test.js.map +1 -1
  141. package/dist/cli/__tests__/native-assets.test.js +398 -16
  142. package/dist/cli/__tests__/native-assets.test.js.map +1 -1
  143. package/dist/cli/__tests__/native-hook-claim-journal.test.js +53 -5
  144. package/dist/cli/__tests__/native-hook-claim-journal.test.js.map +1 -1
  145. package/dist/cli/__tests__/ralph.test.js +2 -2
  146. package/dist/cli/__tests__/ralph.test.js.map +1 -1
  147. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js +57 -57
  148. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js.map +1 -1
  149. package/dist/cli/__tests__/ralplan-child-binding-3212.test.d.ts +2 -0
  150. package/dist/cli/__tests__/ralplan-child-binding-3212.test.d.ts.map +1 -0
  151. package/dist/cli/__tests__/ralplan-child-binding-3212.test.js +56 -0
  152. package/dist/cli/__tests__/ralplan-child-binding-3212.test.js.map +1 -0
  153. package/dist/cli/__tests__/ralplan.test.js +84 -12
  154. package/dist/cli/__tests__/ralplan.test.js.map +1 -1
  155. package/dist/cli/__tests__/session-scoped-runtime.test.js +1118 -43
  156. package/dist/cli/__tests__/session-scoped-runtime.test.js.map +1 -1
  157. package/dist/cli/__tests__/setup-agents-overwrite.test.js +8 -55
  158. package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
  159. package/dist/cli/__tests__/setup-install-mode.test.js +183 -4
  160. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  161. package/dist/cli/__tests__/sparkshell-cli.test.js +93 -41
  162. package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
  163. package/dist/cli/__tests__/team.test.js +45 -1
  164. package/dist/cli/__tests__/team.test.js.map +1 -1
  165. package/dist/cli/__tests__/ultragoal.test.js +117 -2
  166. package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
  167. package/dist/cli/__tests__/uninstall.test.js +50 -5
  168. package/dist/cli/__tests__/uninstall.test.js.map +1 -1
  169. package/dist/cli/__tests__/update.test.js +669 -177
  170. package/dist/cli/__tests__/update.test.js.map +1 -1
  171. package/dist/cli/__tests__/windows-popup-loop-contract.test.js +32 -2
  172. package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
  173. package/dist/cli/api.d.ts.map +1 -1
  174. package/dist/cli/api.js +9 -10
  175. package/dist/cli/api.js.map +1 -1
  176. package/dist/cli/codex-feature-probe.d.ts +16 -0
  177. package/dist/cli/codex-feature-probe.d.ts.map +1 -1
  178. package/dist/cli/codex-feature-probe.js +44 -0
  179. package/dist/cli/codex-feature-probe.js.map +1 -1
  180. package/dist/cli/codex-home.d.ts.map +1 -1
  181. package/dist/cli/codex-home.js +7 -4
  182. package/dist/cli/codex-home.js.map +1 -1
  183. package/dist/cli/doctor.d.ts +17 -0
  184. package/dist/cli/doctor.d.ts.map +1 -1
  185. package/dist/cli/doctor.js +277 -3
  186. package/dist/cli/doctor.js.map +1 -1
  187. package/dist/cli/index.d.ts +231 -12
  188. package/dist/cli/index.d.ts.map +1 -1
  189. package/dist/cli/index.js +2298 -735
  190. package/dist/cli/index.js.map +1 -1
  191. package/dist/cli/native-assets.d.ts +25 -0
  192. package/dist/cli/native-assets.d.ts.map +1 -1
  193. package/dist/cli/native-assets.js +448 -51
  194. package/dist/cli/native-assets.js.map +1 -1
  195. package/dist/cli/native-hook-claim-journal.d.ts +8 -4
  196. package/dist/cli/native-hook-claim-journal.d.ts.map +1 -1
  197. package/dist/cli/native-hook-claim-journal.js +30 -8
  198. package/dist/cli/native-hook-claim-journal.js.map +1 -1
  199. package/dist/cli/package-manager-ownership.d.ts +63 -0
  200. package/dist/cli/package-manager-ownership.d.ts.map +1 -0
  201. package/dist/cli/package-manager-ownership.js +323 -0
  202. package/dist/cli/package-manager-ownership.js.map +1 -0
  203. package/dist/cli/ralph.js +2 -2
  204. package/dist/cli/ralph.js.map +1 -1
  205. package/dist/cli/ralplan.d.ts +4 -2
  206. package/dist/cli/ralplan.d.ts.map +1 -1
  207. package/dist/cli/ralplan.js +76 -34
  208. package/dist/cli/ralplan.js.map +1 -1
  209. package/dist/cli/setup-preferences.d.ts +15 -0
  210. package/dist/cli/setup-preferences.d.ts.map +1 -1
  211. package/dist/cli/setup-preferences.js +27 -1
  212. package/dist/cli/setup-preferences.js.map +1 -1
  213. package/dist/cli/setup.d.ts +3 -0
  214. package/dist/cli/setup.d.ts.map +1 -1
  215. package/dist/cli/setup.js +55 -32
  216. package/dist/cli/setup.js.map +1 -1
  217. package/dist/cli/sparkshell.d.ts.map +1 -1
  218. package/dist/cli/sparkshell.js +52 -9
  219. package/dist/cli/sparkshell.js.map +1 -1
  220. package/dist/cli/ultragoal.d.ts.map +1 -1
  221. package/dist/cli/ultragoal.js +2 -2
  222. package/dist/cli/ultragoal.js.map +1 -1
  223. package/dist/cli/uninstall.d.ts.map +1 -1
  224. package/dist/cli/uninstall.js +27 -28
  225. package/dist/cli/uninstall.js.map +1 -1
  226. package/dist/cli/update-worker.d.ts +2 -0
  227. package/dist/cli/update-worker.d.ts.map +1 -0
  228. package/dist/cli/update-worker.js +142 -0
  229. package/dist/cli/update-worker.js.map +1 -0
  230. package/dist/cli/update.d.ts +14 -22
  231. package/dist/cli/update.d.ts.map +1 -1
  232. package/dist/cli/update.js +192 -241
  233. package/dist/cli/update.js.map +1 -1
  234. package/dist/config/__tests__/generator-idempotent.test.js +173 -29
  235. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  236. package/dist/config/__tests__/generator-notify.test.js +3 -1
  237. package/dist/config/__tests__/generator-notify.test.js.map +1 -1
  238. package/dist/config/generator.d.ts +25 -3
  239. package/dist/config/generator.d.ts.map +1 -1
  240. package/dist/config/generator.js +113 -35
  241. package/dist/config/generator.js.map +1 -1
  242. package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js +25 -0
  243. package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js.map +1 -1
  244. package/dist/goal-workflows/codex-goal-snapshot.d.ts +1 -1
  245. package/dist/goal-workflows/codex-goal-snapshot.d.ts.map +1 -1
  246. package/dist/goal-workflows/codex-goal-snapshot.js +2 -0
  247. package/dist/goal-workflows/codex-goal-snapshot.js.map +1 -1
  248. package/dist/hooks/__tests__/agents-overlay.test.js +3 -1
  249. package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
  250. package/dist/hooks/__tests__/autopilot-skill-contract.test.js +27 -4
  251. package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
  252. package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.d.ts +2 -0
  253. package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.d.ts.map +1 -0
  254. package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.js +172 -0
  255. package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.js.map +1 -0
  256. package/dist/hooks/__tests__/keyword-detector.test.js +172 -33
  257. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  258. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +173 -11
  259. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  260. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +1 -0
  261. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
  262. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +65 -17
  263. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
  264. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js +27 -0
  265. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js.map +1 -1
  266. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js +10 -118
  267. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js.map +1 -1
  268. package/dist/hooks/__tests__/notify-hook-regression-205.test.js +7 -0
  269. package/dist/hooks/__tests__/notify-hook-regression-205.test.js.map +1 -1
  270. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +2 -2
  271. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
  272. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +4 -1
  273. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  274. package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.js +2 -1
  275. package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.js.map +1 -1
  276. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +1 -0
  277. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
  278. package/dist/hooks/__tests__/prompt-guidance-contract.test.js +9 -0
  279. package/dist/hooks/__tests__/prompt-guidance-contract.test.js.map +1 -1
  280. package/dist/hooks/__tests__/session.test.js +2976 -557
  281. package/dist/hooks/__tests__/session.test.js.map +1 -1
  282. package/dist/hooks/agents-overlay.js +1 -1
  283. package/dist/hooks/agents-overlay.js.map +1 -1
  284. package/dist/hooks/extensibility/__tests__/dispatcher.test.js +108 -2
  285. package/dist/hooks/extensibility/__tests__/dispatcher.test.js.map +1 -1
  286. package/dist/hooks/extensibility/__tests__/sdk.test.js +151 -1
  287. package/dist/hooks/extensibility/__tests__/sdk.test.js.map +1 -1
  288. package/dist/hooks/extensibility/dispatcher.d.ts.map +1 -1
  289. package/dist/hooks/extensibility/dispatcher.js +20 -3
  290. package/dist/hooks/extensibility/dispatcher.js.map +1 -1
  291. package/dist/hooks/extensibility/plugin-runner.js +1 -0
  292. package/dist/hooks/extensibility/plugin-runner.js.map +1 -1
  293. package/dist/hooks/extensibility/runtime.d.ts.map +1 -1
  294. package/dist/hooks/extensibility/runtime.js.map +1 -1
  295. package/dist/hooks/extensibility/sdk/paths.d.ts +1 -1
  296. package/dist/hooks/extensibility/sdk/paths.d.ts.map +1 -1
  297. package/dist/hooks/extensibility/sdk/paths.js +2 -2
  298. package/dist/hooks/extensibility/sdk/paths.js.map +1 -1
  299. package/dist/hooks/extensibility/sdk/runtime-state.d.ts +1 -1
  300. package/dist/hooks/extensibility/sdk/runtime-state.d.ts.map +1 -1
  301. package/dist/hooks/extensibility/sdk/runtime-state.js +55 -15
  302. package/dist/hooks/extensibility/sdk/runtime-state.js.map +1 -1
  303. package/dist/hooks/extensibility/sdk.d.ts +1 -0
  304. package/dist/hooks/extensibility/sdk.d.ts.map +1 -1
  305. package/dist/hooks/extensibility/sdk.js +1 -1
  306. package/dist/hooks/extensibility/sdk.js.map +1 -1
  307. package/dist/hooks/extensibility/types.d.ts +7 -0
  308. package/dist/hooks/extensibility/types.d.ts.map +1 -1
  309. package/dist/hooks/keyword-detector.d.ts +10 -3
  310. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  311. package/dist/hooks/keyword-detector.js +116 -20
  312. package/dist/hooks/keyword-detector.js.map +1 -1
  313. package/dist/hooks/session.d.ts +180 -14
  314. package/dist/hooks/session.d.ts.map +1 -1
  315. package/dist/hooks/session.js +1402 -281
  316. package/dist/hooks/session.js.map +1 -1
  317. package/dist/hud/__tests__/hud-tmux-injection.test.js +6 -6
  318. package/dist/hud/__tests__/hud-tmux-injection.test.js.map +1 -1
  319. package/dist/hud/__tests__/index.test.js +26 -16
  320. package/dist/hud/__tests__/index.test.js.map +1 -1
  321. package/dist/hud/__tests__/reconcile.test.js +83 -78
  322. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  323. package/dist/hud/__tests__/state.test.js +36 -31
  324. package/dist/hud/__tests__/state.test.js.map +1 -1
  325. package/dist/hud/__tests__/tmux.test.js +155 -198
  326. package/dist/hud/__tests__/tmux.test.js.map +1 -1
  327. package/dist/hud/tmux.d.ts +42 -4
  328. package/dist/hud/tmux.d.ts.map +1 -1
  329. package/dist/hud/tmux.js +1017 -139
  330. package/dist/hud/tmux.js.map +1 -1
  331. package/dist/leader/__tests__/contract.test.js +71 -34
  332. package/dist/leader/__tests__/contract.test.js.map +1 -1
  333. package/dist/leader/contract.d.ts +1 -6
  334. package/dist/leader/contract.d.ts.map +1 -1
  335. package/dist/leader/contract.js +25 -24
  336. package/dist/leader/contract.js.map +1 -1
  337. package/dist/mcp/__tests__/hermes-bridge.test.js +17 -0
  338. package/dist/mcp/__tests__/hermes-bridge.test.js.map +1 -1
  339. package/dist/mcp/__tests__/server-lifecycle.test.js +4 -4
  340. package/dist/mcp/__tests__/state-paths.test.js +735 -4
  341. package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
  342. package/dist/mcp/__tests__/state-server.test.js +1 -1
  343. package/dist/mcp/__tests__/state-server.test.js.map +1 -1
  344. package/dist/mcp/__tests__/trace-server.test.js +2 -2
  345. package/dist/mcp/__tests__/trace-server.test.js.map +1 -1
  346. package/dist/mcp/hermes-bridge.d.ts.map +1 -1
  347. package/dist/mcp/hermes-bridge.js +2 -2
  348. package/dist/mcp/hermes-bridge.js.map +1 -1
  349. package/dist/mcp/state-paths.d.ts +78 -4
  350. package/dist/mcp/state-paths.d.ts.map +1 -1
  351. package/dist/mcp/state-paths.js +543 -40
  352. package/dist/mcp/state-paths.js.map +1 -1
  353. package/dist/mcp/state-server.d.ts +4 -4
  354. package/dist/mcp/state-server.d.ts.map +1 -1
  355. package/dist/modes/__tests__/base-autopilot-gates.test.js +36 -7
  356. package/dist/modes/__tests__/base-autopilot-gates.test.js.map +1 -1
  357. package/dist/modes/__tests__/base-multi-state-compat.test.js +1 -1
  358. package/dist/modes/__tests__/base-multi-state-compat.test.js.map +1 -1
  359. package/dist/modes/__tests__/base-session-scope.test.js +17 -8
  360. package/dist/modes/__tests__/base-session-scope.test.js.map +1 -1
  361. package/dist/modes/base.d.ts +5 -5
  362. package/dist/modes/base.d.ts.map +1 -1
  363. package/dist/modes/base.js +76 -21
  364. package/dist/modes/base.js.map +1 -1
  365. package/dist/native-assets/archive.d.ts +17 -0
  366. package/dist/native-assets/archive.d.ts.map +1 -0
  367. package/dist/native-assets/archive.js +238 -0
  368. package/dist/native-assets/archive.js.map +1 -0
  369. package/dist/native-assets/policy.d.ts +42 -0
  370. package/dist/native-assets/policy.d.ts.map +1 -0
  371. package/dist/native-assets/policy.js +155 -0
  372. package/dist/native-assets/policy.js.map +1 -0
  373. package/dist/pipeline/__tests__/orchestrator.test.js +162 -40
  374. package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
  375. package/dist/pipeline/__tests__/stages.test.js +66 -149
  376. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  377. package/dist/pipeline/orchestrator.d.ts.map +1 -1
  378. package/dist/pipeline/orchestrator.js +124 -18
  379. package/dist/pipeline/orchestrator.js.map +1 -1
  380. package/dist/pipeline/stages/ralplan.d.ts.map +1 -1
  381. package/dist/pipeline/stages/ralplan.js +36 -8
  382. package/dist/pipeline/stages/ralplan.js.map +1 -1
  383. package/dist/ralplan/__tests__/consensus-gate.test.js +70 -1437
  384. package/dist/ralplan/__tests__/consensus-gate.test.js.map +1 -1
  385. package/dist/ralplan/__tests__/documented-leader-preflight.test.js +328 -39
  386. package/dist/ralplan/__tests__/documented-leader-preflight.test.js.map +1 -1
  387. package/dist/ralplan/__tests__/runtime.test.js +82 -102
  388. package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
  389. package/dist/ralplan/consensus-gate.d.ts +9 -1
  390. package/dist/ralplan/consensus-gate.d.ts.map +1 -1
  391. package/dist/ralplan/consensus-gate.js +40 -304
  392. package/dist/ralplan/consensus-gate.js.map +1 -1
  393. package/dist/ralplan/documented-leader-preflight.d.ts +15 -8
  394. package/dist/ralplan/documented-leader-preflight.d.ts.map +1 -1
  395. package/dist/ralplan/documented-leader-preflight.js +356 -54
  396. package/dist/ralplan/documented-leader-preflight.js.map +1 -1
  397. package/dist/ralplan/runtime.d.ts +1 -1
  398. package/dist/ralplan/runtime.d.ts.map +1 -1
  399. package/dist/ralplan/runtime.js +68 -173
  400. package/dist/ralplan/runtime.js.map +1 -1
  401. package/dist/runtime/run-state.d.ts +5 -1
  402. package/dist/runtime/run-state.d.ts.map +1 -1
  403. package/dist/runtime/run-state.js +15 -8
  404. package/dist/runtime/run-state.js.map +1 -1
  405. package/dist/scripts/__tests__/codex-native-hook.test.js +14071 -8859
  406. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  407. package/dist/scripts/__tests__/issue-3293-callsite-parity.test.d.ts +2 -0
  408. package/dist/scripts/__tests__/issue-3293-callsite-parity.test.d.ts.map +1 -0
  409. package/dist/scripts/__tests__/issue-3293-callsite-parity.test.js +225 -0
  410. package/dist/scripts/__tests__/issue-3293-callsite-parity.test.js.map +1 -0
  411. package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.d.ts +2 -0
  412. package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.d.ts.map +1 -0
  413. package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.js +188 -0
  414. package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.js.map +1 -0
  415. package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.d.ts +2 -0
  416. package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.d.ts.map +1 -0
  417. package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.js +259 -0
  418. package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.js.map +1 -0
  419. package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.d.ts +2 -0
  420. package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.d.ts.map +1 -0
  421. package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.js +491 -0
  422. package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.js.map +1 -0
  423. package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.d.ts +2 -0
  424. package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.d.ts.map +1 -0
  425. package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.js +708 -0
  426. package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.js.map +1 -0
  427. package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.d.ts +2 -0
  428. package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.d.ts.map +1 -0
  429. package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.js +208 -0
  430. package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.js.map +1 -0
  431. package/dist/scripts/__tests__/notify-state-io.test.js +26 -0
  432. package/dist/scripts/__tests__/notify-state-io.test.js.map +1 -1
  433. package/dist/scripts/__tests__/notify-tmux-injection.test.js +23 -1
  434. package/dist/scripts/__tests__/notify-tmux-injection.test.js.map +1 -1
  435. package/dist/scripts/__tests__/postinstall.test.js +19 -1
  436. package/dist/scripts/__tests__/postinstall.test.js.map +1 -1
  437. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js +25 -8
  438. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js.map +1 -1
  439. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js +60 -25
  440. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js.map +1 -1
  441. package/dist/scripts/__tests__/smoke-packed-install.test.js +155 -16
  442. package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
  443. package/dist/scripts/__tests__/verify-native-release-assets.test.d.ts +2 -0
  444. package/dist/scripts/__tests__/verify-native-release-assets.test.d.ts.map +1 -0
  445. package/dist/scripts/__tests__/verify-native-release-assets.test.js +235 -0
  446. package/dist/scripts/__tests__/verify-native-release-assets.test.js.map +1 -0
  447. package/dist/scripts/codex-native-hook.d.ts +50 -1
  448. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  449. package/dist/scripts/codex-native-hook.js +3559 -451
  450. package/dist/scripts/codex-native-hook.js.map +1 -1
  451. package/dist/scripts/codex-native-pre-post.d.ts +9 -0
  452. package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
  453. package/dist/scripts/codex-native-pre-post.js +35 -12
  454. package/dist/scripts/codex-native-pre-post.js.map +1 -1
  455. package/dist/scripts/generate-native-release-manifest.js +80 -51
  456. package/dist/scripts/generate-native-release-manifest.js.map +1 -1
  457. package/dist/scripts/notify-fallback-watcher.js +28 -39
  458. package/dist/scripts/notify-fallback-watcher.js.map +1 -1
  459. package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
  460. package/dist/scripts/notify-hook/state-io.js +5 -0
  461. package/dist/scripts/notify-hook/state-io.js.map +1 -1
  462. package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
  463. package/dist/scripts/notify-hook/team-leader-nudge.js +32 -3
  464. package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
  465. package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
  466. package/dist/scripts/notify-hook/team-worker-stop.js +23 -3
  467. package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
  468. package/dist/scripts/notify-hook/team-worker.d.ts.map +1 -1
  469. package/dist/scripts/notify-hook/team-worker.js +45 -5
  470. package/dist/scripts/notify-hook/team-worker.js.map +1 -1
  471. package/dist/scripts/notify-hook/tmux-injection.d.ts +1 -0
  472. package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
  473. package/dist/scripts/notify-hook/tmux-injection.js +4 -1
  474. package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
  475. package/dist/scripts/notify-hook.js +78 -82
  476. package/dist/scripts/notify-hook.js.map +1 -1
  477. package/dist/scripts/postinstall-advisory.d.ts +14 -0
  478. package/dist/scripts/postinstall-advisory.d.ts.map +1 -0
  479. package/dist/scripts/postinstall-advisory.js +49 -0
  480. package/dist/scripts/postinstall-advisory.js.map +1 -0
  481. package/dist/scripts/postinstall.d.ts +1 -1
  482. package/dist/scripts/postinstall.d.ts.map +1 -1
  483. package/dist/scripts/postinstall.js +1 -1
  484. package/dist/scripts/postinstall.js.map +1 -1
  485. package/dist/scripts/smoke-packed-install.d.ts +32 -1
  486. package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
  487. package/dist/scripts/smoke-packed-install.js +413 -60
  488. package/dist/scripts/smoke-packed-install.js.map +1 -1
  489. package/dist/scripts/sync-plugin-mirror.d.ts.map +1 -1
  490. package/dist/scripts/sync-plugin-mirror.js +16 -1
  491. package/dist/scripts/sync-plugin-mirror.js.map +1 -1
  492. package/dist/scripts/verify-native-release-assets.js +37 -23
  493. package/dist/scripts/verify-native-release-assets.js.map +1 -1
  494. package/dist/state/__tests__/mode-state-context.test.js +12 -5
  495. package/dist/state/__tests__/mode-state-context.test.js.map +1 -1
  496. package/dist/state/__tests__/operations.test.js +460 -850
  497. package/dist/state/__tests__/operations.test.js.map +1 -1
  498. package/dist/state/__tests__/skill-active.test.js +400 -2
  499. package/dist/state/__tests__/skill-active.test.js.map +1 -1
  500. package/dist/state/mode-state-context.d.ts +3 -0
  501. package/dist/state/mode-state-context.d.ts.map +1 -1
  502. package/dist/state/mode-state-context.js +27 -13
  503. package/dist/state/mode-state-context.js.map +1 -1
  504. package/dist/state/operations.d.ts +3 -0
  505. package/dist/state/operations.d.ts.map +1 -1
  506. package/dist/state/operations.js +144 -40
  507. package/dist/state/operations.js.map +1 -1
  508. package/dist/state/skill-active.d.ts +18 -1
  509. package/dist/state/skill-active.d.ts.map +1 -1
  510. package/dist/state/skill-active.js +420 -23
  511. package/dist/state/skill-active.js.map +1 -1
  512. package/dist/state/workflow-transition-reconcile.d.ts +3 -0
  513. package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
  514. package/dist/state/workflow-transition-reconcile.js +10 -4
  515. package/dist/state/workflow-transition-reconcile.js.map +1 -1
  516. package/dist/state/workflow-transition.d.ts.map +1 -1
  517. package/dist/state/workflow-transition.js +3 -1
  518. package/dist/state/workflow-transition.js.map +1 -1
  519. package/dist/subagents/__tests__/leader-bootstrap-3181.test.js +27 -0
  520. package/dist/subagents/__tests__/leader-bootstrap-3181.test.js.map +1 -1
  521. package/dist/subagents/__tests__/tracker.test.js +837 -1048
  522. package/dist/subagents/__tests__/tracker.test.js.map +1 -1
  523. package/dist/subagents/tracker.d.ts +64 -60
  524. package/dist/subagents/tracker.d.ts.map +1 -1
  525. package/dist/subagents/tracker.js +809 -358
  526. package/dist/subagents/tracker.js.map +1 -1
  527. package/dist/team/__tests__/delivery-e2e-smoke.test.js +6 -7
  528. package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
  529. package/dist/team/__tests__/mcp-comm.test.js +104 -3
  530. package/dist/team/__tests__/mcp-comm.test.js.map +1 -1
  531. package/dist/team/__tests__/notice-ledger.test.d.ts +2 -0
  532. package/dist/team/__tests__/notice-ledger.test.d.ts.map +1 -0
  533. package/dist/team/__tests__/notice-ledger.test.js +131 -0
  534. package/dist/team/__tests__/notice-ledger.test.js.map +1 -0
  535. package/dist/team/__tests__/pane-teardown-residue-e2e.test.d.ts +2 -0
  536. package/dist/team/__tests__/pane-teardown-residue-e2e.test.d.ts.map +1 -0
  537. package/dist/team/__tests__/pane-teardown-residue-e2e.test.js +410 -0
  538. package/dist/team/__tests__/pane-teardown-residue-e2e.test.js.map +1 -0
  539. package/dist/team/__tests__/phase-controller.test.js +20 -0
  540. package/dist/team/__tests__/phase-controller.test.js.map +1 -1
  541. package/dist/team/__tests__/runtime.test.js +725 -59
  542. package/dist/team/__tests__/runtime.test.js.map +1 -1
  543. package/dist/team/__tests__/scaling.test.js +222 -237
  544. package/dist/team/__tests__/scaling.test.js.map +1 -1
  545. package/dist/team/__tests__/state-root.test.js +117 -2
  546. package/dist/team/__tests__/state-root.test.js.map +1 -1
  547. package/dist/team/__tests__/tmux-session.test.js +887 -92
  548. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  549. package/dist/team/__tests__/tmux-source-authority-realtmux.test.d.ts +2 -0
  550. package/dist/team/__tests__/tmux-source-authority-realtmux.test.d.ts.map +1 -0
  551. package/dist/team/__tests__/tmux-source-authority-realtmux.test.js +149 -0
  552. package/dist/team/__tests__/tmux-source-authority-realtmux.test.js.map +1 -0
  553. package/dist/team/__tests__/tmux-test-fixture.d.ts +69 -0
  554. package/dist/team/__tests__/tmux-test-fixture.d.ts.map +1 -1
  555. package/dist/team/__tests__/tmux-test-fixture.js +242 -15
  556. package/dist/team/__tests__/tmux-test-fixture.js.map +1 -1
  557. package/dist/team/__tests__/tmux-test-fixture.test.js +177 -1
  558. package/dist/team/__tests__/tmux-test-fixture.test.js.map +1 -1
  559. package/dist/team/__tests__/worker-runtime-identity.test.js +45 -72
  560. package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
  561. package/dist/team/notice-ledger.d.ts +54 -0
  562. package/dist/team/notice-ledger.d.ts.map +1 -0
  563. package/dist/team/notice-ledger.js +259 -0
  564. package/dist/team/notice-ledger.js.map +1 -0
  565. package/dist/team/phase-controller.d.ts.map +1 -1
  566. package/dist/team/phase-controller.js +2 -0
  567. package/dist/team/phase-controller.js.map +1 -1
  568. package/dist/team/runtime.d.ts +3 -1
  569. package/dist/team/runtime.d.ts.map +1 -1
  570. package/dist/team/runtime.js +314 -142
  571. package/dist/team/runtime.js.map +1 -1
  572. package/dist/team/scaling.d.ts.map +1 -1
  573. package/dist/team/scaling.js +161 -91
  574. package/dist/team/scaling.js.map +1 -1
  575. package/dist/team/state/monitor.d.ts +10 -0
  576. package/dist/team/state/monitor.d.ts.map +1 -1
  577. package/dist/team/state/monitor.js +13 -0
  578. package/dist/team/state/monitor.js.map +1 -1
  579. package/dist/team/state/types.d.ts +15 -0
  580. package/dist/team/state/types.d.ts.map +1 -1
  581. package/dist/team/state/types.js.map +1 -1
  582. package/dist/team/state-root.d.ts +12 -0
  583. package/dist/team/state-root.d.ts.map +1 -1
  584. package/dist/team/state-root.js +177 -0
  585. package/dist/team/state-root.js.map +1 -1
  586. package/dist/team/state.d.ts +18 -0
  587. package/dist/team/state.d.ts.map +1 -1
  588. package/dist/team/state.js +36 -0
  589. package/dist/team/state.js.map +1 -1
  590. package/dist/team/tmux-session.d.ts +40 -4
  591. package/dist/team/tmux-session.d.ts.map +1 -1
  592. package/dist/team/tmux-session.js +513 -166
  593. package/dist/team/tmux-session.js.map +1 -1
  594. package/dist/ultragoal/__tests__/artifacts.test.js +950 -2
  595. package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
  596. package/dist/ultragoal/__tests__/docs-contract.test.js +1 -0
  597. package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
  598. package/dist/ultragoal/artifacts.d.ts +35 -1
  599. package/dist/ultragoal/artifacts.d.ts.map +1 -1
  600. package/dist/ultragoal/artifacts.js +232 -46
  601. package/dist/ultragoal/artifacts.js.map +1 -1
  602. package/dist/utils/__tests__/file-durability.test.js +58 -1
  603. package/dist/utils/__tests__/file-durability.test.js.map +1 -1
  604. package/dist/utils/__tests__/paths.test.js +21 -1
  605. package/dist/utils/__tests__/paths.test.js.map +1 -1
  606. package/dist/utils/file-durability.d.ts +10 -0
  607. package/dist/utils/file-durability.d.ts.map +1 -1
  608. package/dist/utils/file-durability.js +34 -4
  609. package/dist/utils/file-durability.js.map +1 -1
  610. package/dist/utils/paths.d.ts.map +1 -1
  611. package/dist/utils/paths.js +14 -6
  612. package/dist/utils/paths.js.map +1 -1
  613. package/dist/verification/__tests__/ci-rust-gates.test.js +49 -4
  614. package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
  615. package/dist/verification/__tests__/explore-harness-release-workflow.test.js +60 -0
  616. package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +1 -1
  617. package/dist/verification/__tests__/native-release-manifest.test.js +157 -4
  618. package/dist/verification/__tests__/native-release-manifest.test.js.map +1 -1
  619. package/package.json +13 -5
  620. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  621. package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +81 -17
  622. package/plugins/oh-my-codex/skills/autopilot/SKILL.md +13 -5
  623. package/plugins/oh-my-codex/skills/cancel/SKILL.md +30 -85
  624. package/plugins/oh-my-codex/skills/ralph/SKILL.md +5 -5
  625. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +15 -17
  626. package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +3 -2
  627. package/skills/autopilot/SKILL.md +13 -5
  628. package/skills/cancel/SKILL.md +30 -85
  629. package/skills/ralph/SKILL.md +5 -5
  630. package/skills/ralplan/SKILL.md +15 -17
  631. package/skills/ultragoal/SKILL.md +3 -2
  632. package/src/scripts/__tests__/codex-native-hook.test.ts +6091 -540
  633. package/src/scripts/__tests__/issue-3293-callsite-parity.test.ts +187 -0
  634. package/src/scripts/__tests__/issue-3293-cancel-transaction.test.ts +189 -0
  635. package/src/scripts/__tests__/issue-3293-hook-owned-cancel.test.ts +187 -0
  636. package/src/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.ts +591 -0
  637. package/src/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.ts +701 -0
  638. package/src/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.ts +211 -0
  639. package/src/scripts/__tests__/notify-state-io.test.ts +29 -0
  640. package/src/scripts/__tests__/notify-tmux-injection.test.ts +26 -1
  641. package/src/scripts/__tests__/postinstall.test.ts +23 -1
  642. package/src/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.ts +27 -8
  643. package/src/scripts/__tests__/role-intent-durable-recovery-3181.test.ts +61 -27
  644. package/src/scripts/__tests__/smoke-packed-install.test.ts +209 -18
  645. package/src/scripts/__tests__/verify-native-release-assets.test.ts +240 -0
  646. package/src/scripts/codex-native-hook.ts +3638 -475
  647. package/src/scripts/codex-native-pre-post.ts +43 -12
  648. package/src/scripts/generate-native-release-manifest.ts +83 -79
  649. package/src/scripts/notify-fallback-watcher.ts +38 -38
  650. package/src/scripts/notify-hook/state-io.ts +11 -0
  651. package/src/scripts/notify-hook/team-leader-nudge.ts +32 -4
  652. package/src/scripts/notify-hook/team-worker-stop.ts +23 -4
  653. package/src/scripts/notify-hook/team-worker.ts +43 -5
  654. package/src/scripts/notify-hook/tmux-injection.ts +5 -1
  655. package/src/scripts/notify-hook.ts +83 -82
  656. package/src/scripts/postinstall-advisory.ts +67 -0
  657. package/src/scripts/postinstall.ts +1 -1
  658. package/src/scripts/smoke-packed-install.ts +446 -59
  659. package/src/scripts/sync-plugin-mirror.ts +17 -1
  660. package/src/scripts/verify-native-release-assets.ts +39 -31
  661. package/templates/AGENTS.md +1 -0
@@ -5,14 +5,26 @@
5
5
  * serialized by an adjacent lock directory and become visible only after an
6
6
  * atomic rename of a transaction-owned temporary file.
7
7
  */
8
- import { appendFile, lstat as nodeLstat, mkdir as nodeMkdir, open, readFile as nodeReadFile, readdir as nodeReaddir, rename as nodeRename, rmdir as nodeRmdir, rm, unlink as nodeUnlink, writeFile as nodeWriteFile, } from 'fs/promises';
9
- import { readFileSync } from 'fs';
8
+ import { execFileSync as nodeExecFileSync } from 'node:child_process';
9
+ import { appendFile, mkdtemp as nodeMkdtemp, link as nodeLink, lstat as nodeLstat, mkdir as nodeMkdir, open, readFile as nodeReadFile, readdir as nodeReaddir, realpath, rename as nodeRename, rmdir as nodeRmdir, rm, unlink as nodeUnlink, writeFile as nodeWriteFile, } from 'fs/promises';
10
+ import { appendFileSync, closeSync, constants as fsConstants, fstatSync, openSync, readFileSync } from 'fs';
11
+ import { tmpdir } from 'node:os';
10
12
  import { createHash, randomUUID } from 'crypto';
11
- import { dirname, join } from 'path';
13
+ import { basename, dirname, isAbsolute, join } from 'path';
12
14
  import { omxRoot, omxLogsDir, sameFilePath } from '../utils/paths.js';
15
+ import { resolveRuntimeBinaryPath } from '../runtime/bridge.js';
13
16
  import { getBaseStateDirWithSource, resolveWorkingDirectoryForState, } from '../mcp/state-paths.js';
14
17
  import { emitDegradedDurabilityWarning, recordRegularFileSyncOutcome, syncRegularFile, } from '../utils/file-durability.js';
18
+ export class CommittedLaunchBlockedError extends Error {
19
+ secondaryFailures;
20
+ name = 'CommittedLaunchBlockedError';
21
+ constructor(secondaryFailures) {
22
+ super('Session pointer committed, but lock release left recovery evidence.');
23
+ this.secondaryFailures = secondaryFailures;
24
+ }
25
+ }
15
26
  const SESSION_FILE = 'session.json';
27
+ const SESSION_OWNER_FILE = 'session-owner.json';
16
28
  const HISTORY_FILE = 'session-history.jsonl';
17
29
  const SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
18
30
  const SESSION_POINTER_TOKEN_PATTERN = /^[A-Za-z0-9_-]{16,128}$/;
@@ -20,6 +32,14 @@ const SHA256_PATTERN = /^[a-f0-9]{64}$/;
20
32
  const LOCK_RETRY_DELAYS_MS = [25, 50, 100];
21
33
  const DEFAULT_POINTER_TIMEOUT_MS = 5_000;
22
34
  const NATIVE_POINTER_TIMEOUT_MS = 2_000;
35
+ function traceSessionFinalizationOperation(operation) {
36
+ if (process.env.OMX_TEST_DETACHED_TRACE !== '1')
37
+ return;
38
+ const tracePath = process.env.OMX_TEST_DETACHED_TRACE_PATH;
39
+ if (!tracePath)
40
+ return;
41
+ appendFileSync(tracePath, `[omx-test-session-finalization] ${operation}\n`);
42
+ }
23
43
  /**
24
44
  * Convert arbitrary input into a valid session ID without exposing validator
25
45
  * exceptions to lifecycle or hook inputs.
@@ -31,9 +51,9 @@ export function normalizeSessionId(value) {
31
51
  return SESSION_ID_PATTERN.test(trimmed) ? trimmed : undefined;
32
52
  }
33
53
  /** Resolve the one exact pointer path used by an operation. */
34
- export function resolveSessionPointerContext(cwd) {
35
- const normalizedCwd = resolveWorkingDirectoryForState(cwd);
36
- const { baseStateDir, rootSource } = getBaseStateDirWithSource(normalizedCwd);
54
+ export function resolveSessionPointerContext(cwd, env = process.env) {
55
+ const normalizedCwd = resolveWorkingDirectoryForState(cwd, env);
56
+ const { baseStateDir, rootSource } = getBaseStateDirWithSource(normalizedCwd, env);
37
57
  const pointerPath = join(baseStateDir, SESSION_FILE);
38
58
  return {
39
59
  cwd: normalizedCwd,
@@ -43,6 +63,26 @@ export function resolveSessionPointerContext(cwd) {
43
63
  lockPath: `${pointerPath}.lock`,
44
64
  };
45
65
  }
66
+ function resolveNativeSessionOwnerContext(cwd, nativeSessionId) {
67
+ const root = resolveSessionPointerContext(cwd);
68
+ const normalized = normalizeSessionId(nativeSessionId);
69
+ if (!normalized) {
70
+ throw resolvedAbort(root, {
71
+ code: 'session_pointer_io_failure',
72
+ operation: 'pointer-classify',
73
+ lockPath: root.lockPath,
74
+ reason: 'A valid native session ID is required for owner evidence.',
75
+ });
76
+ }
77
+ const baseStateDir = join(root.baseStateDir, 'sessions', normalized);
78
+ const sessionPath = join(baseStateDir, SESSION_OWNER_FILE);
79
+ return {
80
+ ...root,
81
+ baseStateDir,
82
+ sessionPath,
83
+ lockPath: `${sessionPath}.lock`,
84
+ };
85
+ }
46
86
  function attemptedStateRootSource() {
47
87
  try {
48
88
  if (process.env.OMX_TEAM_STATE_ROOT?.trim())
@@ -139,6 +179,9 @@ const defaultFsDependencies = {
139
179
  rename: async (from, to) => {
140
180
  await nodeRename(from, to);
141
181
  },
182
+ link: async (path, dest) => {
183
+ await nodeLink(path, dest);
184
+ },
142
185
  unlink: async (path) => {
143
186
  await nodeUnlink(path);
144
187
  },
@@ -146,6 +189,28 @@ const defaultFsDependencies = {
146
189
  await nodeRmdir(path);
147
190
  },
148
191
  };
192
+ async function defaultRecoveryRenameNoReplace(from, to) {
193
+ if (!from.trim() || !to.trim() || !isAbsolute(from) || !isAbsolute(to))
194
+ return 'unsupported';
195
+ try {
196
+ const stdout = nodeExecFileSync(resolveRuntimeBinaryPath(), ['fs-rename-no-replace', from, to], {
197
+ encoding: 'utf8',
198
+ maxBuffer: 64 * 1024,
199
+ shell: false,
200
+ stdio: ['ignore', 'pipe', 'pipe'],
201
+ timeout: 10_000,
202
+ });
203
+ const parsed = JSON.parse(String(stdout));
204
+ if (parsed.outcome === 'moved' || parsed.outcome === 'not-moved' || parsed.outcome === 'unsupported') {
205
+ return parsed.outcome;
206
+ }
207
+ }
208
+ catch {
209
+ // Missing binaries, spawn failures, hard runtime errors, and malformed output
210
+ // all fail closed; recovery must never fall back to an overwriting rename.
211
+ }
212
+ return 'unsupported';
213
+ }
149
214
  /** @internal Exposed only so source-module tests can verify default ESRCH handling. */
150
215
  export function __createDefaultPidProbeForTests(killZero) {
151
216
  return (pid) => {
@@ -163,26 +228,40 @@ function defaultProbePid(pid) {
163
228
  process.kill(targetPid, 0);
164
229
  })(pid);
165
230
  }
166
- function defaultReadProcessIdentity(pid, platform) {
167
- if (platform !== 'linux')
168
- return { status: 'indeterminate' };
169
- const identity = readLinuxProcessIdentity(pid);
170
- if (!identity)
171
- return { status: 'indeterminate' };
172
- const cmdlineHash = hashCmdline(identity.cmdline);
173
- return {
174
- status: 'matching',
175
- startTicks: identity.startTicks,
176
- ...(cmdlineHash ? { cmdlineHash } : {}),
177
- };
231
+ function defaultObserveProcess(pid, platform) {
232
+ if (platform === 'linux') {
233
+ return observeLinuxProcess(pid);
234
+ }
235
+ if (platform !== 'darwin' && platform !== 'win32') {
236
+ return { kind: 'unsupported' };
237
+ }
238
+ try {
239
+ const stdout = nodeExecFileSync(resolveRuntimeBinaryPath(), ['process-identity', String(pid)], {
240
+ encoding: 'utf8',
241
+ timeout: 3000,
242
+ maxBuffer: 64 * 1024,
243
+ shell: false,
244
+ stdio: ['ignore', 'pipe', 'pipe'],
245
+ });
246
+ return parseProcessIdentityOutput(stdout);
247
+ }
248
+ catch {
249
+ return { kind: 'unsupported' };
250
+ }
178
251
  }
179
252
  const defaultTransactionDependencies = {
180
253
  fs: defaultFsDependencies,
181
254
  nowMs: () => Date.now(),
182
255
  sleep: async (ms) => await new Promise((resolve) => setTimeout(resolve, ms)),
183
256
  token: () => randomUUID(),
257
+ runtimePlatform: process.platform,
184
258
  probePid: defaultProbePid,
185
- readProcessIdentity: defaultReadProcessIdentity,
259
+ observeProcess: defaultObserveProcess,
260
+ atomicRenameNoReplace: defaultRecoveryRenameNoReplace,
261
+ };
262
+ export const defaultProcessInspectionProvider = {
263
+ probePid: defaultProbePid,
264
+ observeProcess: defaultObserveProcess,
186
265
  };
187
266
  let transactionDependencies = defaultTransactionDependencies;
188
267
  /** @internal Source-module test harness. Always reset after a test. */
@@ -234,13 +313,118 @@ function readLinuxProcessIdentity(pid) {
234
313
  return null;
235
314
  }
236
315
  }
237
- function defaultIsPidAlive(pid) {
316
+ const NODE_PLATFORMS = new Set([
317
+ 'aix', 'android', 'darwin', 'freebsd', 'haiku', 'linux', 'openbsd', 'sunos', 'win32', 'cygwin', 'netbsd',
318
+ ]);
319
+ const PROCESS_BIRTH_PATTERN = /^\d+(?:\.\d+)?$/;
320
+ function isNodePlatform(value) {
321
+ return typeof value === 'string' && NODE_PLATFORMS.has(value);
322
+ }
323
+ function isValidBirth(value) {
324
+ return typeof value === 'string' && PROCESS_BIRTH_PATTERN.test(value);
325
+ }
326
+ export function isValidProcessIdentity(value) {
327
+ if (!value || typeof value !== 'object' || Array.isArray(value))
328
+ return false;
329
+ const identity = value;
330
+ if (Object.keys(identity).some((key) => key !== 'platform' && key !== 'birth' && key !== 'cmdline_hash'))
331
+ return false;
332
+ return isNodePlatform(identity.platform)
333
+ && isValidBirth(identity.birth)
334
+ && (identity.cmdline_hash === undefined
335
+ || typeof identity.cmdline_hash === 'string' && SHA256_PATTERN.test(identity.cmdline_hash));
336
+ }
337
+ function isComparableProcessIdentity(value) {
338
+ if (!value || typeof value !== 'object' || Array.isArray(value))
339
+ return false;
340
+ const identity = value;
341
+ return isNodePlatform(identity.platform)
342
+ && typeof identity.birth === 'string'
343
+ && identity.birth.length > 0
344
+ && (identity.cmdline_hash === undefined
345
+ || typeof identity.cmdline_hash === 'string' && SHA256_PATTERN.test(identity.cmdline_hash));
346
+ }
347
+ function observeLinuxProcess(pid) {
348
+ if (!Number.isInteger(pid) || pid <= 0)
349
+ return { kind: 'error' };
350
+ const identity = readLinuxProcessIdentity(pid);
351
+ if (identity) {
352
+ const cmdlineHash = hashCmdline(identity.cmdline);
353
+ return {
354
+ kind: 'identity',
355
+ identity: {
356
+ platform: 'linux',
357
+ birth: String(identity.startTicks),
358
+ ...(cmdlineHash ? { cmdline_hash: cmdlineHash } : {}),
359
+ },
360
+ };
361
+ }
362
+ let statContent;
238
363
  try {
239
- process.kill(pid, 0);
240
- return true;
364
+ statContent = readFileSync(`/proc/${pid}/stat`, 'utf8');
365
+ }
366
+ catch (error) {
367
+ const code = errorCode(error);
368
+ if (code === 'ENOENT' || code === 'ESRCH')
369
+ return { kind: 'gone' };
370
+ if (code === 'EACCES' || code === 'EPERM')
371
+ return { kind: 'denied' };
372
+ return { kind: 'error' };
373
+ }
374
+ if (!statContent)
375
+ return { kind: 'error' };
376
+ const probe = defaultProbePid(pid);
377
+ if (probe === 'dead')
378
+ return { kind: 'gone' };
379
+ if (probe === 'indeterminate')
380
+ return { kind: 'denied' };
381
+ return { kind: 'error' };
382
+ }
383
+ function parseProcessIdentityOutput(stdout) {
384
+ try {
385
+ const parsed = JSON.parse(stdout);
386
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
387
+ return { kind: 'error' };
388
+ const value = parsed;
389
+ const keys = Object.keys(value);
390
+ const outcome = value.outcome;
391
+ if (outcome !== undefined) {
392
+ if (typeof outcome !== 'string')
393
+ return { kind: 'error' };
394
+ if (outcome === 'gone' || outcome === 'denied' || outcome === 'unsupported') {
395
+ if (keys.length !== 1)
396
+ return { kind: 'error' };
397
+ if (outcome === 'gone')
398
+ return { kind: 'gone' };
399
+ if (outcome === 'denied')
400
+ return { kind: 'denied' };
401
+ return { kind: 'unsupported' };
402
+ }
403
+ if (outcome === 'error')
404
+ return { kind: 'error' };
405
+ return { kind: 'error' };
406
+ }
407
+ if (keys.some((key) => key !== 'platform' && key !== 'birth' && key !== 'cmdline')) {
408
+ return { kind: 'error' };
409
+ }
410
+ const platform = value.platform;
411
+ const birth = value.birth;
412
+ if (!isNodePlatform(platform) || !isValidBirth(birth))
413
+ return { kind: 'error' };
414
+ if (value.cmdline !== undefined && typeof value.cmdline !== 'string')
415
+ return { kind: 'error' };
416
+ const cmdlineHash = hashCmdline(value.cmdline);
417
+ return {
418
+ kind: 'identity',
419
+ identity: {
420
+ platform,
421
+ birth,
422
+ ...(cmdlineHash ? { cmdline_hash: cmdlineHash } : {}),
423
+ },
424
+ };
241
425
  }
242
426
  catch {
243
- return false;
427
+ return { kind: 'error' };
244
428
  }
245
429
  }
246
430
  /**
@@ -250,22 +434,122 @@ function defaultIsPidAlive(pid) {
250
434
  export function isSessionStale(state, options = {}) {
251
435
  if (!Number.isInteger(state.pid) || state.pid <= 0)
252
436
  return true;
253
- const isPidAlive = options.isPidAlive ?? defaultIsPidAlive;
254
- if (!isPidAlive(state.pid))
255
- return true;
256
- const platform = options.platform ?? process.platform;
257
- if (platform !== 'linux')
258
- return false;
259
- const liveIdentity = (options.readLinuxIdentity ?? readLinuxProcessIdentity)(state.pid);
260
- if (!liveIdentity || typeof state.pid_start_ticks !== 'number')
261
- return true;
262
- if (state.pid_start_ticks !== liveIdentity.startTicks)
263
- return true;
264
- const expectedCmdline = normalizeCmdline(state.pid_cmdline);
265
- if (!expectedCmdline)
266
- return false;
267
- const liveCmdline = normalizeCmdline(liveIdentity.cmdline);
268
- return !liveCmdline || liveCmdline !== expectedCmdline;
437
+ const runtimePlatform = options.platform ?? transactionDependencies.runtimePlatform ?? process.platform;
438
+ const probePid = options.probePid ?? (options.isPidAlive
439
+ ? (pid) => options.isPidAlive(pid) ? 'alive' : 'dead'
440
+ : transactionDependencies.probePid);
441
+ const observeProcess = options.observeProcess ?? (options.readLinuxIdentity
442
+ ? (pid, platform) => {
443
+ if (platform !== 'linux')
444
+ return { kind: 'unsupported' };
445
+ const identity = options.readLinuxIdentity(pid);
446
+ if (!identity)
447
+ return { kind: 'error' };
448
+ const cmdlineHash = hashCmdline(identity.cmdline);
449
+ return {
450
+ kind: 'identity',
451
+ identity: {
452
+ platform: 'linux',
453
+ birth: String(identity.startTicks),
454
+ ...(cmdlineHash ? { cmdline_hash: cmdlineHash } : {}),
455
+ },
456
+ };
457
+ }
458
+ : transactionDependencies.observeProcess);
459
+ const dependencies = {
460
+ ...transactionDependencies,
461
+ runtimePlatform,
462
+ probePid,
463
+ observeProcess,
464
+ };
465
+ return classifySessionProcess(state, dependencies) !== 'usable';
466
+ }
467
+ function safeObserve(provider, pid, runtimePlatform) {
468
+ try {
469
+ return provider.observeProcess(pid, runtimePlatform);
470
+ }
471
+ catch {
472
+ return { kind: 'error' };
473
+ }
474
+ }
475
+ function compareObservation(recorded, observation, runtimePlatform) {
476
+ if (!observation || typeof observation !== 'object' || observation.kind !== 'identity') {
477
+ return 'identity-unavailable';
478
+ }
479
+ const identity = observation.identity;
480
+ if (!isComparableProcessIdentity(identity) || identity.platform !== runtimePlatform) {
481
+ return 'identity-unavailable';
482
+ }
483
+ if (identity.birth !== recorded.birth)
484
+ return 'birth-mismatch-retryable';
485
+ if (recorded.cmdline_hash !== undefined && identity.cmdline_hash !== recorded.cmdline_hash) {
486
+ return 'identity-unavailable';
487
+ }
488
+ return 'match';
489
+ }
490
+ /** Compare recorded process birth evidence with a fresh provider observation. */
491
+ export function classifyRecordedIdentity(recorded, runtimePlatform, provider, pid) {
492
+ if (!isComparableProcessIdentity(recorded) || recorded.platform !== runtimePlatform || !Number.isInteger(pid) || pid <= 0) {
493
+ return { status: 'identity-unavailable' };
494
+ }
495
+ let probe;
496
+ try {
497
+ probe = provider.probePid(pid);
498
+ }
499
+ catch {
500
+ return { status: 'identity-unavailable' };
501
+ }
502
+ if (probe === 'dead')
503
+ return { status: 'gone' };
504
+ if (probe !== 'alive')
505
+ return { status: 'identity-unavailable' };
506
+ const first = compareObservation(recorded, safeObserve(provider, pid, runtimePlatform), runtimePlatform);
507
+ if (first === 'birth-mismatch-retryable') {
508
+ const second = compareObservation(recorded, safeObserve(provider, pid, runtimePlatform), runtimePlatform);
509
+ if (second === 'match')
510
+ return { status: 'match' };
511
+ if (second === 'birth-mismatch-retryable' || second === 'birth-mismatch')
512
+ return { status: 'birth-mismatch' };
513
+ return { status: 'identity-unavailable' };
514
+ }
515
+ if (first === 'match')
516
+ return { status: 'match' };
517
+ if (first === 'birth-mismatch')
518
+ return { status: 'birth-mismatch' };
519
+ return { status: 'identity-unavailable' };
520
+ }
521
+ function recordedIdentityForState(state, runtimePlatform) {
522
+ if (state.identity_schema_version !== undefined && state.identity_schema_version !== 2)
523
+ return 'invalid';
524
+ if (state.process_identity !== undefined) {
525
+ if (state.identity_schema_version !== 2 || !isValidProcessIdentity(state.process_identity))
526
+ return 'invalid';
527
+ if (state.platform !== undefined && state.platform !== state.process_identity.platform)
528
+ return 'invalid';
529
+ return state.process_identity;
530
+ }
531
+ if (state.identity_schema_version === 2)
532
+ return 'invalid';
533
+ const recordedPlatform = state.platform ?? runtimePlatform;
534
+ if (recordedPlatform === 'linux' && isValidStartTicks(state.pid_start_ticks)) {
535
+ const cmdlineHash = hashCmdline(state.pid_cmdline);
536
+ return {
537
+ platform: 'linux',
538
+ birth: String(state.pid_start_ticks),
539
+ ...(cmdlineHash ? { cmdline_hash: cmdlineHash } : {}),
540
+ };
541
+ }
542
+ return null;
543
+ }
544
+ function probeIdentitylessProcess(pid, dependencies) {
545
+ let probe;
546
+ try {
547
+ probe = dependencies.probePid(pid);
548
+ }
549
+ catch {
550
+ return 'identity-indeterminate';
551
+ }
552
+ return probe === 'dead' ? 'stale-dead' : 'identity-indeterminate';
269
553
  }
270
554
  export function isSessionStateAuthoritativeForCwd(state, cwd) {
271
555
  if (!normalizeSessionId(state.session_id))
@@ -285,54 +569,47 @@ export function isSessionStateUsable(state, cwd, options = {}) {
285
569
  if (typeof state.cwd === 'string' && state.cwd.trim() && !isSessionStateAuthoritativeForCwd(state, cwd))
286
570
  return false;
287
571
  const hasPidMetadata = Number.isInteger(state.pid) && state.pid > 0;
288
- const hasLinuxIdentityMetadata = typeof state.pid_start_ticks === 'number'
289
- || typeof state.pid_cmdline === 'string';
290
- return !hasPidMetadata && !hasLinuxIdentityMetadata || !isSessionStale(state, options);
572
+ const hasIdentityMetadata = typeof state.pid_start_ticks === 'number'
573
+ || typeof state.pid_cmdline === 'string'
574
+ || state.identity_schema_version !== undefined
575
+ || state.process_identity !== undefined;
576
+ return !hasPidMetadata && !hasIdentityMetadata || !isSessionStale(state, options);
291
577
  }
292
578
  function isValidStartTicks(value) {
293
579
  return typeof value === 'number' && Number.isInteger(value) && value >= 0;
294
580
  }
295
581
  function classifySessionProcess(state, dependencies) {
296
582
  const hasPidMetadata = Number.isInteger(state.pid) && state.pid > 0;
297
- const hasIdentityMetadata = typeof state.pid_start_ticks === 'number' || typeof state.pid_cmdline === 'string';
583
+ const hasIdentityMetadata = typeof state.pid_start_ticks === 'number'
584
+ || typeof state.pid_cmdline === 'string'
585
+ || state.identity_schema_version !== undefined
586
+ || state.process_identity !== undefined;
298
587
  if (!hasPidMetadata && !hasIdentityMetadata)
299
588
  return 'usable';
300
589
  if (!hasPidMetadata)
301
590
  return 'identity-indeterminate';
302
- let pidStatus;
303
- try {
304
- pidStatus = dependencies.probePid(state.pid);
305
- }
306
- catch {
307
- return 'identity-indeterminate';
308
- }
309
- if (pidStatus === 'dead')
310
- return 'stale-dead';
311
- if (pidStatus !== 'alive')
312
- return 'identity-indeterminate';
313
- const platform = state.platform ?? process.platform;
314
- if (platform !== 'linux')
315
- return 'usable';
316
- if (!isValidStartTicks(state.pid_start_ticks))
317
- return 'identity-indeterminate';
318
- let liveIdentity;
319
- try {
320
- liveIdentity = dependencies.readProcessIdentity(state.pid, platform);
321
- }
322
- catch {
323
- return 'identity-indeterminate';
324
- }
325
- if (!liveIdentity || !isValidStartTicks(liveIdentity.startTicks))
326
- return 'identity-indeterminate';
327
- if (liveIdentity.startTicks !== state.pid_start_ticks)
328
- return 'stale-dead';
329
- if (liveIdentity.status !== 'matching')
330
- return 'identity-indeterminate';
331
- const expectedCmdlineHash = hashCmdline(state.pid_cmdline);
332
- if (expectedCmdlineHash && liveIdentity.cmdlineHash !== expectedCmdlineHash) {
591
+ const runtimePlatform = dependencies.runtimePlatform;
592
+ const recorded = recordedIdentityForState(state, runtimePlatform);
593
+ if (recorded === 'invalid')
333
594
  return 'identity-indeterminate';
595
+ if (!recorded)
596
+ return probeIdentitylessProcess(state.pid, dependencies);
597
+ const classification = classifyRecordedIdentity(recorded, runtimePlatform, dependencies, state.pid);
598
+ switch (classification.status) {
599
+ case 'match': return 'usable';
600
+ case 'gone':
601
+ case 'birth-mismatch': return 'stale-dead';
602
+ case 'identity-unavailable': return 'identity-indeterminate';
334
603
  }
335
- return 'usable';
604
+ }
605
+ /**
606
+ * Classify process liveness for an already-parsed selected session pointer.
607
+ * Callers that need cwd/state_root authority must validate those against the
608
+ * SAME snapshot themselves; this evaluates only pid/start-tick evidence so a
609
+ * single immutable snapshot can drive both decisions.
610
+ */
611
+ export function classifySessionStateLiveness(state) {
612
+ return classifySessionProcess(state, transactionDependencies);
336
613
  }
337
614
  function classifyParsedSessionPointer(context, value, raw) {
338
615
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
@@ -344,6 +621,16 @@ function classifyParsedSessionPointer(context, value, raw) {
344
621
  if (typeof state.cwd === 'string' && state.cwd.trim() && !isSessionStateAuthoritativeForCwd(state, context.cwd)) {
345
622
  return { status: 'foreign-cwd', raw, state };
346
623
  }
624
+ if (state.identity_schema_version !== undefined && state.identity_schema_version !== 2)
625
+ return { status: 'identity-indeterminate', raw, state };
626
+ if (state.process_identity !== undefined
627
+ && (state.identity_schema_version !== 2
628
+ || !isValidProcessIdentity(state.process_identity)
629
+ || state.platform !== undefined && state.platform !== state.process_identity.platform)) {
630
+ return { status: 'malformed', raw, state };
631
+ }
632
+ if (state.identity_schema_version === 2 && state.process_identity === undefined)
633
+ return { status: 'malformed', raw, state };
347
634
  const processStatus = classifySessionProcess(state, transactionDependencies);
348
635
  return { status: processStatus, state, raw };
349
636
  }
@@ -382,9 +669,9 @@ export async function readUsableSessionStateFromContext(context) {
382
669
  return state && isSessionStateUsable(state, context.cwd) ? state : null;
383
670
  }
384
671
  /** Read current session state from the exact selected root. */
385
- export async function readSessionState(cwd) {
672
+ export async function readSessionState(cwd, env = process.env) {
386
673
  try {
387
- return await readSessionStateFromContext(resolveSessionPointerContext(cwd));
674
+ return await readSessionStateFromContext(resolveSessionPointerContext(cwd, env));
388
675
  }
389
676
  catch {
390
677
  return null;
@@ -400,7 +687,7 @@ export async function readUsableSessionState(cwd, options = {}) {
400
687
  return null;
401
688
  }
402
689
  }
403
- function createSessionState(cwd, sessionId, pid, platform, linuxIdentity, options = {}) {
690
+ function createSessionState(cwd, stateRoot, sessionId, pid, platform, identity, options = {}) {
404
691
  const nowIso = options.nowIso ?? new Date().toISOString();
405
692
  const nativeSessionId = normalizeNonempty(options.nativeSessionId);
406
693
  const previousNativeSessionId = normalizeNonempty(options.previousNativeSessionId);
@@ -408,6 +695,27 @@ function createSessionState(cwd, sessionId, pid, platform, linuxIdentity, option
408
695
  const ownerOmxSessionId = normalizeSessionId(options.ownerOmxSessionId);
409
696
  const tmuxSessionName = normalizeNonempty(options.tmuxSessionName);
410
697
  const tmuxPaneId = normalizeNonempty(options.tmuxPaneId);
698
+ const processIdentity = identity && 'birth' in identity
699
+ ? identity
700
+ : identity
701
+ ? {
702
+ platform: 'linux',
703
+ birth: String(identity.startTicks),
704
+ ...(hashCmdline(identity.cmdline) ? { cmdline_hash: hashCmdline(identity.cmdline) } : {}),
705
+ }
706
+ : undefined;
707
+ const publishIdentity = processIdentity && isValidProcessIdentity(processIdentity) && processIdentity.platform === platform
708
+ ? processIdentity
709
+ : undefined;
710
+ const legacyStartTicks = identity && 'startTicks' in identity
711
+ ? identity.startTicks
712
+ : publishIdentity?.platform === 'linux' && Number.isSafeInteger(Number(publishIdentity.birth))
713
+ ? Number(publishIdentity.birth)
714
+ : undefined;
715
+ const legacyLinuxIdentity = publishIdentity?.platform === 'linux' && publishIdentity.birth === String(legacyStartTicks)
716
+ ? readLinuxProcessIdentity(pid)
717
+ : null;
718
+ const legacyCmdline = identity && 'cmdline' in identity ? identity.cmdline : legacyLinuxIdentity?.cmdline;
411
719
  return {
412
720
  session_id: sessionId,
413
721
  ...(nativeSessionId ? { native_session_id: nativeSessionId } : {}),
@@ -415,15 +723,23 @@ function createSessionState(cwd, sessionId, pid, platform, linuxIdentity, option
415
723
  ...(nativeSessionSwitchedAt ? { native_session_switched_at: nativeSessionSwitchedAt } : {}),
416
724
  ...(ownerOmxSessionId ? { owner_omx_session_id: ownerOmxSessionId } : {}),
417
725
  started_at: options.startedAt ?? nowIso,
726
+ ...(isValidToken(options.launchLineageToken) ? { launch_lineage_token: options.launchLineageToken } : {}),
418
727
  cwd,
728
+ state_root: stateRoot,
419
729
  pid,
420
730
  platform,
421
- ...(linuxIdentity ? { pid_start_ticks: linuxIdentity.startTicks } : {}),
422
- ...(linuxIdentity?.cmdline ? { pid_cmdline: linuxIdentity.cmdline } : {}),
731
+ ...(publishIdentity ? { identity_schema_version: 2, process_identity: publishIdentity } : {}),
732
+ ...(legacyStartTicks !== undefined ? { pid_start_ticks: legacyStartTicks } : {}),
733
+ ...(legacyCmdline ? { pid_cmdline: legacyCmdline } : {}),
423
734
  ...(tmuxSessionName ? { tmux_session_name: tmuxSessionName } : {}),
424
735
  ...(tmuxPaneId ? { tmux_pane_id: tmuxPaneId } : {}),
425
736
  };
426
737
  }
738
+ function preserveExistingLaunchLineageToken(existing, state) {
739
+ return existing && Object.hasOwn(existing, 'launch_lineage_token')
740
+ ? { ...state, launch_lineage_token: existing.launch_lineage_token }
741
+ : state;
742
+ }
427
743
  function normalizeNonempty(value) {
428
744
  return typeof value === 'string' && value.trim() ? value.trim() : undefined;
429
745
  }
@@ -431,7 +747,15 @@ function resolvePid(options) {
431
747
  return Number.isInteger(options.pid) && options.pid && options.pid > 0 ? options.pid : process.pid;
432
748
  }
433
749
  function sessionIdentityFor(pid, platform) {
434
- return platform === 'linux' ? readLinuxProcessIdentity(pid) : null;
750
+ try {
751
+ const observation = transactionDependencies.observeProcess(pid, platform);
752
+ return observation.kind === 'identity' && isValidProcessIdentity(observation.identity) && observation.identity.platform === platform
753
+ ? observation.identity
754
+ : null;
755
+ }
756
+ catch {
757
+ return null;
758
+ }
435
759
  }
436
760
  function currentOwnerAlias(state) {
437
761
  return normalizeSessionId(state.owner_omx_session_id);
@@ -458,29 +782,38 @@ function isStartCompatible(existing, requestedSessionId) {
458
782
  || existing.native_session_id === requestedSessionId
459
783
  || currentOwnerAlias(existing) === requestedSessionId;
460
784
  }
461
- function getOmxLaunchSessionId(state) {
462
- if (state.session_id.startsWith('omx-'))
463
- return state.session_id;
464
- const owner = currentOwnerAlias(state);
465
- return owner?.startsWith('omx-') ? owner : undefined;
466
- }
467
785
  function parseLockOwner(raw) {
468
786
  try {
469
- const value = JSON.parse(raw);
470
- if (!value || typeof value !== 'object' || Array.isArray(value))
787
+ const parsed = JSON.parse(raw);
788
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
471
789
  return null;
472
- if (value.version !== 1 || !isValidToken(value.token) || typeof value.pid !== 'number' || !Number.isInteger(value.pid) || value.pid <= 0)
790
+ const value = parsed;
791
+ if (!isValidToken(value.token) || typeof value.pid !== 'number' || !Number.isInteger(value.pid) || value.pid <= 0)
473
792
  return null;
474
- if (typeof value.platform !== 'string' || !value.platform || typeof value.created_at !== 'string' || !value.created_at) {
793
+ if (!isNodePlatform(value.platform) || typeof value.created_at !== 'string' || !value.created_at)
475
794
  return null;
795
+ if (value.version === 1) {
796
+ if (value.pid_start_ticks !== undefined && !isValidStartTicks(value.pid_start_ticks))
797
+ return null;
798
+ if (value.pid_cmdline_hash !== undefined
799
+ && (typeof value.pid_cmdline_hash !== 'string' || !SHA256_PATTERN.test(value.pid_cmdline_hash))) {
800
+ return null;
801
+ }
802
+ return value;
476
803
  }
477
- if (value.pid_start_ticks !== undefined && !isValidStartTicks(value.pid_start_ticks))
478
- return null;
479
- if (value.pid_cmdline_hash !== undefined
480
- && (typeof value.pid_cmdline_hash !== 'string' || !SHA256_PATTERN.test(value.pid_cmdline_hash))) {
481
- return null;
804
+ if (value.version === 2) {
805
+ const processIdentity = value.process_identity;
806
+ if (!isValidProcessIdentity(processIdentity) || processIdentity.platform !== value.platform)
807
+ return null;
808
+ if (value.pid_start_ticks !== undefined && !isValidStartTicks(value.pid_start_ticks))
809
+ return null;
810
+ if (value.pid_cmdline_hash !== undefined
811
+ && (typeof value.pid_cmdline_hash !== 'string' || !SHA256_PATTERN.test(value.pid_cmdline_hash))) {
812
+ return null;
813
+ }
814
+ return value;
482
815
  }
483
- return value;
816
+ return null;
484
817
  }
485
818
  catch {
486
819
  return null;
@@ -489,6 +822,17 @@ function parseLockOwner(raw) {
489
822
  function isValidToken(value) {
490
823
  return typeof value === 'string' && SESSION_POINTER_TOKEN_PATTERN.test(value);
491
824
  }
825
+ function recordedIdentityForLockOwner(owner) {
826
+ if (owner.version === 2)
827
+ return owner.process_identity;
828
+ if (owner.platform !== 'linux' || !isValidStartTicks(owner.pid_start_ticks))
829
+ return null;
830
+ return {
831
+ platform: 'linux',
832
+ birth: String(owner.pid_start_ticks),
833
+ ...(owner.pid_cmdline_hash ? { cmdline_hash: owner.pid_cmdline_hash } : {}),
834
+ };
835
+ }
492
836
  async function inspectLockOwnerFile(ownerPath, missingStatus = 'missing') {
493
837
  let raw;
494
838
  try {
@@ -500,38 +844,26 @@ async function inspectLockOwnerFile(ownerPath, missingStatus = 'missing') {
500
844
  const owner = parseLockOwner(raw);
501
845
  if (!owner)
502
846
  return { status: 'malformed' };
503
- let pidStatus;
504
- try {
505
- pidStatus = transactionDependencies.probePid(owner.pid);
506
- }
507
- catch {
508
- return { status: 'identity-indeterminate', owner };
509
- }
510
- if (pidStatus === 'dead')
511
- return { status: 'dead', owner };
512
- if (pidStatus !== 'alive')
513
- return { status: 'identity-indeterminate', owner };
514
- if (owner.platform !== 'linux' || !isValidStartTicks(owner.pid_start_ticks)) {
515
- return { status: 'identity-indeterminate', owner };
516
- }
517
- let liveIdentity;
518
- try {
519
- liveIdentity = transactionDependencies.readProcessIdentity(owner.pid, owner.platform);
520
- }
521
- catch {
522
- return { status: 'identity-indeterminate', owner };
523
- }
524
- if (!liveIdentity || !isValidStartTicks(liveIdentity.startTicks)) {
525
- return { status: 'identity-indeterminate', owner };
847
+ const recorded = recordedIdentityForLockOwner(owner);
848
+ if (!recorded) {
849
+ let probe;
850
+ try {
851
+ probe = transactionDependencies.probePid(owner.pid);
852
+ }
853
+ catch {
854
+ return { status: 'identity-indeterminate', owner };
855
+ }
856
+ return probe === 'dead'
857
+ ? { status: 'dead', owner }
858
+ : { status: 'identity-indeterminate', owner };
526
859
  }
527
- if (liveIdentity.startTicks !== owner.pid_start_ticks)
528
- return { status: 'reused', owner };
529
- if (liveIdentity.status !== 'matching')
530
- return { status: 'identity-indeterminate', owner };
531
- if (owner.pid_cmdline_hash && owner.pid_cmdline_hash !== liveIdentity.cmdlineHash) {
532
- return { status: 'identity-indeterminate', owner };
860
+ const classification = classifyRecordedIdentity(recorded, transactionDependencies.runtimePlatform, transactionDependencies, owner.pid);
861
+ switch (classification.status) {
862
+ case 'match': return { status: 'live', owner };
863
+ case 'gone': return { status: 'dead', owner };
864
+ case 'birth-mismatch': return { status: 'reused', owner };
865
+ case 'identity-unavailable': return { status: 'identity-indeterminate', owner };
533
866
  }
534
- return { status: 'live', owner };
535
867
  }
536
868
  async function inspectLockOwner(lockPath) {
537
869
  return await inspectLockOwnerFile(join(lockPath, 'owner.json'));
@@ -586,124 +918,292 @@ async function inspectSessionPointerLockAtContext(context) {
586
918
  status,
587
919
  lockPath: context.lockPath,
588
920
  evidenceSource,
589
- safeToRecover: status === 'dead' && evidenceSource === 'owner-temp',
921
+ // Recoverable only on positively dead evidence (ESRCH), for canonical
922
+ // owner.json and pre-rename temp evidence alike; the atomic
923
+ // claim/quarantine protocol revalidates the exact entry, still-dead
924
+ // status, and owner token before quarantining. Live, reused,
925
+ // identity-indeterminate, ambiguous, symlink, malformed, unexpected,
926
+ // and io-error states stay fail-closed.
927
+ safeToRecover: status === 'dead',
590
928
  };
591
929
  }
592
930
  /** Inspect only exact, regular owner evidence in the selected pointer lock. */
593
931
  export async function inspectSessionPointerLock(cwd) {
594
932
  return await inspectSessionPointerLockAtContext(resolveSessionPointerContext(cwd));
595
933
  }
596
- /** Explicitly quarantine only a dead, atomically claimed pointer lock; never force recovery. */
597
- export async function recoverSessionPointerLock(cwd) {
598
- const context = resolveSessionPointerContext(cwd);
599
- const inspection = await inspectSessionPointerLockAtContext(context);
600
- if (!inspection.safeToRecover) {
601
- return { ...inspection, action: 'none', recovered: false, reason: inspection.status === 'absent' ? 'No session pointer lock exists.' : `Session pointer lock is not safe to recover (${inspection.status}).` };
602
- }
603
- let entries;
934
+ function sameRecoveryIdentity(stat, identity, kind) {
935
+ return !stat.isSymbolicLink() && (kind === 'file' ? stat.isFile() : stat.isDirectory()) && stat.dev === identity.dev && stat.ino === identity.ino;
936
+ }
937
+ async function lstatRecoveryPath(path) {
604
938
  try {
605
- entries = await transactionDependencies.fs.readdir(context.lockPath);
606
- }
607
- catch {
608
- return { ...inspection, action: 'none', recovered: false, reason: 'Unable to re-read session pointer lock evidence.' };
609
- }
610
- const evidenceName = inspection.evidenceSource === 'owner.json' ? 'owner.json' : entries.find((entry) => /^owner\.([A-Za-z0-9_-]{16,128})\.tmp$/.test(entry));
611
- if (!evidenceName || entries.length !== 1) {
612
- return { ...inspection, action: 'none', recovered: false, reason: 'Session pointer lock evidence changed before recovery claim.' };
939
+ return await transactionDependencies.fs.lstat(path);
613
940
  }
614
- const owner = await inspectLockOwnerFile(join(context.lockPath, evidenceName));
615
- if (owner.status !== 'dead' || !owner.owner) {
616
- return { ...inspection, action: 'none', recovered: false, reason: 'Session pointer lock evidence changed before recovery claim.' };
941
+ catch (error) {
942
+ if (isNotFound(error))
943
+ return undefined;
944
+ throw error;
617
945
  }
618
- let claimToken;
946
+ }
947
+ async function probeRecoveryRenameCapability() {
948
+ let probeDir;
619
949
  try {
620
- claimToken = transactionDependencies.token();
950
+ probeDir = await nodeMkdtemp(join(tmpdir(), 'omx-session-recovery-probe-'));
951
+ const from = join(probeDir, 'from');
952
+ const to = join(probeDir, 'to');
953
+ await nodeWriteFile(from, 'omx-recovery-capability-probe', { flag: 'wx' });
954
+ const outcome = await transactionDependencies.atomicRenameNoReplace(from, to);
955
+ return outcome === 'moved' || outcome === 'not-moved' || outcome === 'unsupported'
956
+ ? outcome
957
+ : 'unsupported';
621
958
  }
622
959
  catch {
623
- return { ...inspection, action: 'none', recovered: false, reason: 'Unable to create recovery claim token.' };
960
+ return 'unsupported';
624
961
  }
625
- if (!isValidToken(claimToken))
626
- return { ...inspection, action: 'none', recovered: false, reason: 'Recovery claim token is invalid.' };
627
- const claimName = `owner.${owner.owner.token}.${claimToken}.recovery`;
628
- const claimPath = join(context.lockPath, claimName);
962
+ finally {
963
+ if (probeDir) {
964
+ try {
965
+ await rm(probeDir, { recursive: true, force: true });
966
+ }
967
+ catch { /* best effort */ }
968
+ }
969
+ }
970
+ }
971
+ async function moveRecoveryPathNoReplace(from, to, identity, kind) {
629
972
  try {
630
- await transactionDependencies.fs.lstat(claimPath);
631
- return { ...inspection, action: 'none', recovered: false, reason: 'Recovery claim path already exists.' };
973
+ const outcome = await transactionDependencies.atomicRenameNoReplace(from, to);
974
+ if (outcome === 'unsupported')
975
+ return { moved: false, unsupported: true, reason: 'Atomic no-replace recovery rename is unsupported on this platform.' };
976
+ if (outcome === 'not-moved')
977
+ return { moved: false, reason: 'Atomic recovery move left its source pathname in place.' };
632
978
  }
633
979
  catch (error) {
634
- if (!isNotFound(error))
635
- return { ...inspection, action: 'none', recovered: false, reason: 'Unable to inspect recovery claim path.' };
636
- }
637
- try {
638
- await transactionDependencies.fs.rename(join(context.lockPath, evidenceName), claimPath);
980
+ return { moved: false, reason: `Atomic no-replace recovery rename failed (${errorCode(error) ?? 'unknown'}).` };
981
+ }
982
+ const destination = await lstatRecoveryPath(to);
983
+ const source = await lstatRecoveryPath(from);
984
+ if (destination && sameRecoveryIdentity(destination, identity, kind) && !source)
985
+ return { moved: true };
986
+ // A successor may have appeared at the canonical pathname after our move.
987
+ // Attempt a no-replace rollback only for the captured object; an occupied or
988
+ // foreign destination makes the rollback fail closed and leaves both residues.
989
+ if (destination && sameRecoveryIdentity(destination, identity, kind)) {
990
+ try {
991
+ await transactionDependencies.atomicRenameNoReplace(to, from);
992
+ }
993
+ catch { /* durable residue is safer than cleanup */ }
639
994
  }
640
- catch {
641
- return { ...inspection, action: 'none', recovered: false, reason: 'Session pointer lock evidence changed before recovery claim.' };
995
+ return { moved: false, reason: 'Atomic recovery move did not leave the captured object at its token-bound destination.' };
996
+ }
997
+ async function completeRecoveryCheckpoint(checkpointPath, checkpointBytes, checkpointIdentity) {
998
+ const completedPath = checkpointPath.replace(/\.json$/, '.completed');
999
+ const current = await lstatRecoveryPath(checkpointPath);
1000
+ if (!current || !sameRecoveryIdentity(current, checkpointIdentity, 'file') || await transactionDependencies.fs.readFile(checkpointPath, 'utf8') !== checkpointBytes) {
1001
+ return { completed: false, reason: 'Recovery checkpoint was replaced before completion; it was preserved as residue.' };
642
1002
  }
643
- const claimed = await inspectSessionPointerLockAtContext(context);
644
- let claimedEntries;
1003
+ const moved = await moveRecoveryPathNoReplace(checkpointPath, completedPath, checkpointIdentity, 'file');
1004
+ if (!moved.moved)
1005
+ return { completed: false, reason: moved.reason };
1006
+ const completedBytes = await transactionDependencies.fs.readFile(completedPath, 'utf8');
1007
+ return completedBytes === checkpointBytes ? { completed: true } : { completed: false, reason: 'Completed recovery receipt bytes changed and were preserved as residue.' };
1008
+ }
1009
+ async function resumeRecoveryCheckpoint(context, checkpointPath, checkpointName) {
1010
+ const match = new RegExp(`^${basename(context.lockPath).replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\.recovery\\.([A-Za-z0-9_-]{16,128})\\.([A-Za-z0-9_-]{16,128})\\.json$`).exec(checkpointName);
645
1011
  try {
646
- claimedEntries = await transactionDependencies.fs.readdir(context.lockPath);
647
- }
648
- catch {
649
- return { ...claimed, action: 'none', recovered: false, reason: 'Unable to revalidate recovery claim.' };
1012
+ const checkpointBytes = await transactionDependencies.fs.readFile(checkpointPath, 'utf8');
1013
+ const checkpointStat = await transactionDependencies.fs.lstat(checkpointPath);
1014
+ const checkpoint = JSON.parse(checkpointBytes);
1015
+ const ownerToken = match?.[1];
1016
+ const recoveryToken = match?.[2];
1017
+ const expectedSource = ownerToken && (checkpoint.sourcePath === join(context.lockPath, 'owner.json') || checkpoint.sourcePath === join(context.lockPath, `owner.${ownerToken}.tmp`));
1018
+ const expectedPark = `${context.lockPath}.parked-lock-${recoveryToken}`;
1019
+ const isLegacyV1 = checkpoint.version === 1 && checkpoint.lockIdentity === undefined && checkpoint.lockParkPath === undefined;
1020
+ if (!match || !expectedSource || !checkpoint.identity || !['evidence-pending', 'evidence-quarantined', 'directory-pending'].includes(checkpoint.phase) || ![1, 2, 3].includes(checkpoint.version) || !isLegacyV1 && checkpoint.lockParkPath !== expectedPark || !sameRecoveryIdentity(checkpointStat, { dev: checkpointStat.dev, ino: checkpointStat.ino }, 'file'))
1021
+ throw new Error('invalid checkpoint');
1022
+ const lockParkPath = checkpoint.lockParkPath ?? expectedPark;
1023
+ const evidenceIdentity = checkpoint.evidenceIdentity ?? checkpoint.identity;
1024
+ const claimPath = join(context.lockPath, `owner.${ownerToken}.${recoveryToken}.recovery`);
1025
+ const lock = await lstatRecoveryPath(context.lockPath);
1026
+ const parkedLock = await lstatRecoveryPath(lockParkPath);
1027
+ if (lock && parkedLock)
1028
+ throw new Error('checkpoint lock paths are both present');
1029
+ if (parkedLock) {
1030
+ if (!checkpoint.lockIdentity || !sameRecoveryIdentity(parkedLock, checkpoint.lockIdentity, 'directory'))
1031
+ throw new Error('checkpoint parked lock identity mismatch');
1032
+ const quarantinePath = `${context.lockPath}.quarantine.${ownerToken}.${recoveryToken}`;
1033
+ const completed = await completeRecoveryCheckpoint(checkpointPath, checkpointBytes, { dev: checkpointStat.dev, ino: checkpointStat.ino });
1034
+ if (!completed.completed)
1035
+ throw new Error(completed.reason);
1036
+ return { status: 'dead', lockPath: context.lockPath, evidenceSource: 'owner.json', safeToRecover: true, action: 'quarantined', recovered: true, reason: 'Dead session pointer lock recovery checkpoint resumed.', quarantinePath };
1037
+ }
1038
+ if (!lock || lock.isSymbolicLink() || !lock.isDirectory())
1039
+ throw new Error('checkpoint lock is missing or foreign');
1040
+ const source = await lstatRecoveryPath(checkpoint.sourcePath);
1041
+ const claim = await lstatRecoveryPath(claimPath);
1042
+ const evidencePath = source && sameRecoveryIdentity(source, evidenceIdentity, 'file') ? checkpoint.sourcePath
1043
+ : claim && sameRecoveryIdentity(claim, evidenceIdentity, 'file') ? claimPath : undefined;
1044
+ if (!evidencePath)
1045
+ throw new Error('checkpoint evidence is missing or foreign');
1046
+ if (claim && !sameRecoveryIdentity(claim, evidenceIdentity, 'file'))
1047
+ throw new Error('checkpoint claim is foreign');
1048
+ // v1/v2 did not persist the directory identity. It is safe to derive only
1049
+ // while the original directory still contains the exact recorded evidence.
1050
+ const lockIdentity = checkpoint.lockIdentity ?? { dev: lock.dev, ino: lock.ino };
1051
+ if (!sameRecoveryIdentity(lock, lockIdentity, 'directory'))
1052
+ throw new Error('checkpoint lock identity mismatch');
1053
+ const ownerBytes = await transactionDependencies.fs.readFile(evidencePath, 'utf8');
1054
+ const owner = await inspectLockOwnerFile(evidencePath);
1055
+ if (owner.status !== 'dead' || owner.owner?.token !== ownerToken || checkpoint.evidenceBytes !== undefined && checkpoint.evidenceBytes !== ownerBytes)
1056
+ throw new Error('checkpoint owner evidence changed');
1057
+ const quarantinePath = `${context.lockPath}.quarantine.${ownerToken}.${recoveryToken}`;
1058
+ const quarantine = await lstatRecoveryPath(quarantinePath);
1059
+ if (quarantine) {
1060
+ if (!sameRecoveryIdentity(quarantine, evidenceIdentity, 'file'))
1061
+ throw new Error('checkpoint quarantine is foreign');
1062
+ }
1063
+ else {
1064
+ await transactionDependencies.fs.link(evidencePath, quarantinePath);
1065
+ const linked = await lstatRecoveryPath(quarantinePath);
1066
+ if (!linked || !sameRecoveryIdentity(linked, evidenceIdentity, 'file'))
1067
+ throw new Error('checkpoint quarantine link mismatch');
1068
+ }
1069
+ // Revalidate the bytes, dead owner and identities immediately before moving
1070
+ // the entire directory incarnation out of the canonical lock pathname.
1071
+ const currentLock = await lstatRecoveryPath(context.lockPath);
1072
+ const currentEvidence = await lstatRecoveryPath(evidencePath);
1073
+ const currentBytes = currentEvidence && sameRecoveryIdentity(currentEvidence, evidenceIdentity, 'file') ? await transactionDependencies.fs.readFile(evidencePath, 'utf8') : undefined;
1074
+ const currentOwner = currentBytes === undefined ? undefined : await inspectLockOwnerFile(evidencePath);
1075
+ if (!currentLock || !sameRecoveryIdentity(currentLock, lockIdentity, 'directory') || currentBytes !== ownerBytes || currentOwner?.status !== 'dead' || currentOwner.owner?.token !== ownerToken)
1076
+ throw new Error('checkpoint owner evidence changed before directory quarantine');
1077
+ const lockEntries = await transactionDependencies.fs.readdir(context.lockPath);
1078
+ const allowedEntries = new Set([basename(checkpoint.sourcePath), basename(claimPath)]);
1079
+ if (lockEntries.some((entry) => !allowedEntries.has(entry)))
1080
+ throw new Error('checkpoint lock contains foreign recovery evidence');
1081
+ const moved = await moveRecoveryPathNoReplace(context.lockPath, lockParkPath, lockIdentity, 'directory');
1082
+ if (!moved.moved)
1083
+ throw new Error(moved.reason);
1084
+ const completed = await completeRecoveryCheckpoint(checkpointPath, checkpointBytes, { dev: checkpointStat.dev, ino: checkpointStat.ino });
1085
+ if (!completed.completed)
1086
+ throw new Error(completed.reason);
1087
+ return { status: 'dead', lockPath: context.lockPath, evidenceSource: 'owner.json', safeToRecover: true, action: 'quarantined', recovered: true, reason: 'Dead session pointer lock recovered into durable quarantine residues.', quarantinePath };
650
1088
  }
651
- const claimOwner = await inspectLockOwnerFile(claimPath);
652
- if (claimedEntries.length !== 1 || claimedEntries[0] !== claimName || claimOwner.status !== 'dead' || claimOwner.owner?.token !== owner.owner.token || claimed.status !== 'unexpected') {
653
- return { ...claimed, action: 'none', recovered: false, reason: 'Recovery claim revalidation failed.' };
1089
+ catch (error) {
1090
+ const detail = error instanceof Error ? error.message : 'unknown';
1091
+ return { status: 'unexpected', lockPath: context.lockPath, evidenceSource: 'none', safeToRecover: false, action: 'none', recovered: false, reason: `Recovery checkpoint is malformed or no longer identifies its recorded recovery state (${detail}).` };
654
1092
  }
655
- const quarantinePath = `${context.lockPath}.quarantine.${owner.owner.token}.${claimToken}`;
1093
+ }
1094
+ /** Explicitly quarantine only a dead lock by moving its entire directory incarnation. */
1095
+ export async function recoverSessionPointerLock(cwd) {
1096
+ const context = resolveSessionPointerContext(cwd);
1097
+ const checkpointPrefix = `${basename(context.lockPath)}.recovery.`;
1098
+ let checkpointNames;
656
1099
  try {
657
- await transactionDependencies.fs.lstat(quarantinePath);
658
- return { ...claimed, action: 'none', recovered: false, reason: 'Recovery quarantine path already exists.' };
1100
+ checkpointNames = (await transactionDependencies.fs.readdir(dirname(context.lockPath))).filter((name) => name.startsWith(checkpointPrefix) && name.endsWith('.json'));
659
1101
  }
660
1102
  catch (error) {
661
1103
  if (!isNotFound(error))
662
- return { ...claimed, action: 'none', recovered: false, reason: 'Unable to inspect recovery quarantine path.' };
663
- }
664
- try {
665
- await transactionDependencies.fs.rename(claimPath, quarantinePath);
666
- }
667
- catch {
668
- return { ...claimed, action: 'none', recovered: false, reason: 'Unable to quarantine the exact session pointer recovery claim.' };
1104
+ return { status: 'io-error', lockPath: context.lockPath, evidenceSource: 'none', safeToRecover: false, action: 'none', recovered: false, reason: 'Unable to enumerate session pointer recovery checkpoints.' };
1105
+ checkpointNames = [];
669
1106
  }
1107
+ if (checkpointNames.length > 1)
1108
+ return { status: 'unexpected', lockPath: context.lockPath, evidenceSource: 'none', safeToRecover: false, action: 'none', recovered: false, reason: 'Multiple recovery checkpoints exist.' };
1109
+ if (checkpointNames.length === 1)
1110
+ return await resumeRecoveryCheckpoint(context, join(dirname(context.lockPath), checkpointNames[0]), checkpointNames[0]);
1111
+ const inspection = await inspectSessionPointerLockAtContext(context);
1112
+ if (!inspection.safeToRecover)
1113
+ return { ...inspection, action: 'none', recovered: false, reason: inspection.status === 'absent' ? 'No session pointer lock exists.' : `Session pointer lock is not safe to recover (${inspection.status}).` };
670
1114
  try {
671
- await transactionDependencies.fs.rmdir(context.lockPath);
672
- }
673
- catch (error) {
674
- if (!isNotFound(error)) {
1115
+ const evidenceName = inspection.evidenceSource === 'owner.json'
1116
+ ? 'owner.json'
1117
+ : (await transactionDependencies.fs.readdir(context.lockPath)).find((entry) => /^owner\.([A-Za-z0-9_-]{16,128})\.tmp$/.test(entry));
1118
+ if (!evidenceName)
1119
+ return { ...inspection, action: 'none', recovered: false, reason: 'Session pointer lock evidence changed before recovery claim.' };
1120
+ const evidencePath = join(context.lockPath, evidenceName);
1121
+ const evidence = await lstatRecoveryPath(evidencePath);
1122
+ const lock = await lstatRecoveryPath(context.lockPath);
1123
+ const ownerBytes = evidence && sameRecoveryIdentity(evidence, { dev: evidence.dev, ino: evidence.ino }, 'file')
1124
+ ? await transactionDependencies.fs.readFile(evidencePath, 'utf8')
1125
+ : undefined;
1126
+ const owner = ownerBytes === undefined ? undefined : await inspectLockOwnerFile(evidencePath);
1127
+ if (!lock || lock.isSymbolicLink() || !lock.isDirectory() || !evidence
1128
+ || !sameRecoveryIdentity(evidence, { dev: evidence.dev, ino: evidence.ino }, 'file')
1129
+ || owner?.status !== 'dead' || !owner.owner) {
1130
+ return { ...inspection, action: 'none', recovered: false, reason: 'Session pointer lock evidence changed before recovery claim.' };
1131
+ }
1132
+ // Fresh recovery probes before checkpoint creation; an unsupported host leaves the
1133
+ // canonical lock and owner evidence untouched. Existing checkpoints are resumed
1134
+ // independently and may remain as durable recovery residue.
1135
+ const capability = await probeRecoveryRenameCapability();
1136
+ if (capability === 'unsupported') {
675
1137
  return {
676
- ...claimed,
1138
+ ...inspection,
677
1139
  action: 'none',
678
1140
  recovered: false,
679
- reason: 'The exact recovery claim was quarantined, but the canonical lock directory was not empty.',
680
- quarantinePath,
1141
+ reason: 'Atomic no-replace recovery rename is unsupported on this platform.',
681
1142
  };
682
1143
  }
1144
+ const recoveryToken = transactionDependencies.token();
1145
+ if (!isValidToken(recoveryToken))
1146
+ return { ...inspection, action: 'none', recovered: false, reason: 'Recovery claim token is invalid.' };
1147
+ const checkpointPath = `${context.lockPath}.recovery.${owner.owner.token}.${recoveryToken}.json`;
1148
+ const checkpoint = {
1149
+ version: 3,
1150
+ sourcePath: evidencePath,
1151
+ identity: { dev: evidence.dev, ino: evidence.ino },
1152
+ evidenceIdentity: { dev: evidence.dev, ino: evidence.ino },
1153
+ evidenceBytes: ownerBytes,
1154
+ lockIdentity: { dev: lock.dev, ino: lock.ino },
1155
+ lockParkPath: `${context.lockPath}.parked-lock-${recoveryToken}`,
1156
+ phase: 'evidence-pending',
1157
+ };
1158
+ try {
1159
+ await transactionDependencies.fs.writeFile(checkpointPath, JSON.stringify(checkpoint), { flag: 'wx' });
1160
+ }
1161
+ catch (error) {
1162
+ return { ...inspection, action: 'none', recovered: false, reason: `Unable to create recovery checkpoint (${errorCode(error) ?? 'unknown'}).` };
1163
+ }
1164
+ return await resumeRecoveryCheckpoint(context, checkpointPath, basename(checkpointPath));
1165
+ }
1166
+ catch (error) {
1167
+ return {
1168
+ ...inspection,
1169
+ status: 'io-error',
1170
+ safeToRecover: false,
1171
+ action: 'none',
1172
+ recovered: false,
1173
+ reason: `Unable to inspect session pointer lock recovery state (${errorCode(error) ?? errorMessage(error)}).`,
1174
+ };
683
1175
  }
684
- return { ...inspection, action: 'quarantined', recovered: true, reason: 'Dead session pointer lock was atomically claimed and quarantined.', quarantinePath };
685
1176
  }
686
1177
  function buildLockOwner(token) {
687
- let identity;
1178
+ const runtimePlatform = transactionDependencies.runtimePlatform;
1179
+ let observation;
688
1180
  try {
689
- identity = transactionDependencies.readProcessIdentity(process.pid, process.platform);
1181
+ observation = transactionDependencies.observeProcess(process.pid, runtimePlatform);
690
1182
  }
691
1183
  catch {
692
1184
  // Publication remains valid with optional identity metadata omitted.
693
1185
  }
694
- return {
695
- version: 1,
1186
+ const identity = observation?.kind === 'identity' && isValidProcessIdentity(observation.identity)
1187
+ && observation.identity.platform === runtimePlatform
1188
+ ? observation.identity
1189
+ : undefined;
1190
+ const legacyStartTicks = identity?.platform === 'linux' && Number.isSafeInteger(Number(identity.birth))
1191
+ ? Number(identity.birth)
1192
+ : undefined;
1193
+ const legacyHash = identity?.cmdline_hash && SHA256_PATTERN.test(identity.cmdline_hash)
1194
+ ? identity.cmdline_hash
1195
+ : undefined;
1196
+ const common = {
696
1197
  token,
697
1198
  pid: process.pid,
698
- platform: process.platform,
699
- ...(identity?.status === 'matching' && isValidStartTicks(identity.startTicks)
700
- ? { pid_start_ticks: identity.startTicks }
701
- : {}),
702
- ...(identity?.status === 'matching' && identity.cmdlineHash && SHA256_PATTERN.test(identity.cmdlineHash)
703
- ? { pid_cmdline_hash: identity.cmdlineHash }
704
- : {}),
1199
+ platform: runtimePlatform,
1200
+ ...(legacyStartTicks !== undefined ? { pid_start_ticks: legacyStartTicks } : {}),
1201
+ ...(legacyHash ? { pid_cmdline_hash: legacyHash } : {}),
705
1202
  created_at: new Date(transactionDependencies.nowMs()).toISOString(),
706
1203
  };
1204
+ return identity
1205
+ ? { version: 2, ...common, process_identity: identity }
1206
+ : { version: 1, ...common };
707
1207
  }
708
1208
  async function removeOwnedPath(path) {
709
1209
  try {
@@ -942,7 +1442,7 @@ async function releasePointerLock(lock) {
942
1442
  if (!isNotFound(error)) {
943
1443
  return [{
944
1444
  operation: 'lock-release',
945
- phase: 'remove-release-dir',
1445
+ phase: 'remove-release-owner',
946
1446
  ownership: 'released',
947
1447
  message: `Unable to remove released session pointer lock owner: ${errorMessage(error)}`,
948
1448
  cause: error,
@@ -965,6 +1465,10 @@ async function releasePointerLock(lock) {
965
1465
  }];
966
1466
  }
967
1467
  }
1468
+ /** @internal Test-only release of a held pointer lock; do not use outside session tests. */
1469
+ export async function __releasePointerLockForTests(cwd, token) {
1470
+ return await releasePointerLock({ context: resolveSessionPointerContext(cwd), token });
1471
+ }
968
1472
  function recoveryAbort(context, primary, failures, operation) {
969
1473
  return resolvedAbort(context, {
970
1474
  code: 'session_pointer_lock_recovery_required',
@@ -979,9 +1483,12 @@ function recoveryAbort(context, primary, failures, operation) {
979
1483
  secondaryFailures: failures,
980
1484
  });
981
1485
  }
982
- function releaseFailureError(failures) {
983
- const error = new Error('Session pointer committed, but lock release left recovery evidence.');
984
- Object.assign(error, { secondaryFailures: failures });
1486
+ function releaseFailureError(failures, context, value) {
1487
+ const error = new CommittedLaunchBlockedError(failures);
1488
+ if (context)
1489
+ error.context = context;
1490
+ if (value !== undefined)
1491
+ error.value = value;
985
1492
  return error;
986
1493
  }
987
1494
  async function finalizePrecommitAbort(lock, primary, pointerTempPath) {
@@ -1023,7 +1530,7 @@ function ownerConflictAbort(context, candidateSessionId, state, cause) {
1023
1530
  ...(cause ? { cause } : {}),
1024
1531
  });
1025
1532
  }
1026
- async function writePointerTransaction(cwd, candidateSessionId, options, timeoutMs, transition, pointerState) {
1533
+ async function writePointerTransaction(cwd, candidateSessionId, options, timeoutMs, transition, pointerState, beforeCommit, afterCommit) {
1027
1534
  let context;
1028
1535
  try {
1029
1536
  context = options.context ?? resolveSessionPointerContext(cwd);
@@ -1059,6 +1566,7 @@ async function writePointerTransaction(cwd, candidateSessionId, options, timeout
1059
1566
  const lock = await acquirePointerLock(context, candidateSessionId, timeoutMs, platform, tracker, options.regularFileSync);
1060
1567
  const pointerTempPath = `${context.sessionPath}.tmp-${lock.token}`;
1061
1568
  let pointerCommitted = false;
1569
+ let pointerTempWritten = false;
1062
1570
  try {
1063
1571
  let pointer;
1064
1572
  try {
@@ -1076,7 +1584,7 @@ async function writePointerTransaction(cwd, candidateSessionId, options, timeout
1076
1584
  }
1077
1585
  let next;
1078
1586
  try {
1079
- next = transition(pointer, context);
1587
+ next = await transition(pointer, context);
1080
1588
  }
1081
1589
  catch (error) {
1082
1590
  if (isSessionPointerLaunchAbort(error))
@@ -1091,6 +1599,7 @@ async function writePointerTransaction(cwd, candidateSessionId, options, timeout
1091
1599
  }
1092
1600
  const serialized = JSON.stringify(pointerState(next), null, 2);
1093
1601
  try {
1602
+ pointerTempWritten = true;
1094
1603
  await transactionDependencies.fs.writeFile(pointerTempPath, serialized, { mode: 0o600, flag: 'wx' });
1095
1604
  }
1096
1605
  catch (error) {
@@ -1116,11 +1625,31 @@ async function writePointerTransaction(cwd, candidateSessionId, options, timeout
1116
1625
  cause: error,
1117
1626
  });
1118
1627
  }
1628
+ if (beforeCommit)
1629
+ await beforeCommit(context);
1119
1630
  try {
1120
1631
  await transactionDependencies.fs.rename(pointerTempPath, context.sessionPath);
1121
1632
  pointerCommitted = true;
1633
+ if (afterCommit) {
1634
+ try {
1635
+ await afterCommit(context);
1636
+ }
1637
+ catch (error) {
1638
+ const releaseFailures = await releasePointerLock(lock);
1639
+ throw releaseFailureError([
1640
+ {
1641
+ operation: 'lock-release', phase: 'rename', ownership: 'uncertain',
1642
+ message: `Selected state root identity changed after pointer publication: ${errorMessage(error)}`,
1643
+ cause: error, evidencePath: context.sessionPath,
1644
+ },
1645
+ ...releaseFailures,
1646
+ ], context, next);
1647
+ }
1648
+ }
1122
1649
  }
1123
1650
  catch (error) {
1651
+ if (error instanceof CommittedLaunchBlockedError)
1652
+ throw error;
1124
1653
  throw resolvedAbort(context, {
1125
1654
  code: 'session_pointer_io_failure',
1126
1655
  operation: 'pointer-rename',
@@ -1132,7 +1661,7 @@ async function writePointerTransaction(cwd, candidateSessionId, options, timeout
1132
1661
  }
1133
1662
  const releaseFailures = await releasePointerLock(lock);
1134
1663
  if (releaseFailures.length > 0)
1135
- throw releaseFailureError(releaseFailures);
1664
+ throw releaseFailureError(releaseFailures, context, next);
1136
1665
  emitDegradedDurabilityWarning('session pointer start/reconcile', tracker);
1137
1666
  return { context, value: next };
1138
1667
  }
@@ -1149,14 +1678,18 @@ async function writePointerTransaction(cwd, candidateSessionId, options, timeout
1149
1678
  reason: `Session pointer transition failed before commit: ${errorMessage(error)}`,
1150
1679
  cause: error,
1151
1680
  });
1152
- const ownsPointerTemp = primary.operation === 'pointer-temp-write'
1153
- || primary.operation === 'pointer-fsync'
1154
- || primary.operation === 'pointer-rename';
1681
+ const ownsPointerTemp = pointerTempWritten;
1155
1682
  throw await finalizePrecommitAbort(lock, primary, ownsPointerTemp ? pointerTempPath : undefined);
1156
1683
  }
1157
1684
  }
1158
- function startPointerTransition(requestedSessionId, options) {
1685
+ function startPointerTransition(requestedSessionId, options, requireLaunchLineageToken = false, requireAbsent = false) {
1159
1686
  return (pointer, context) => {
1687
+ if (requireAbsent && pointer.status !== 'absent') {
1688
+ if (pointer.status === 'usable' && pointer.state) {
1689
+ throw ownerConflictAbort(context, requestedSessionId, pointer.state);
1690
+ }
1691
+ throw unusablePointerAbort(context, requestedSessionId, pointer);
1692
+ }
1160
1693
  if (pointer.status !== 'absent' && pointer.status !== 'stale-dead' && pointer.status !== 'usable') {
1161
1694
  throw unusablePointerAbort(context, requestedSessionId, pointer);
1162
1695
  }
@@ -1164,6 +1697,9 @@ function startPointerTransition(requestedSessionId, options) {
1164
1697
  if (existing && !isStartCompatible(existing, requestedSessionId)) {
1165
1698
  throw ownerConflictAbort(context, requestedSessionId, existing);
1166
1699
  }
1700
+ if (requireLaunchLineageToken && existing && !isValidToken(existing.launch_lineage_token)) {
1701
+ throw ownerConflictAbort(context, requestedSessionId, existing);
1702
+ }
1167
1703
  const canonicalSessionId = existing?.session_id ?? requestedSessionId;
1168
1704
  const pid = resolvePid(options);
1169
1705
  const platform = options.platform ?? process.platform;
@@ -1180,20 +1716,51 @@ function startPointerTransition(requestedSessionId, options) {
1180
1716
  pid,
1181
1717
  }, error);
1182
1718
  }
1183
- return createSessionState(context.cwd, canonicalSessionId, pid, platform, sessionIdentityFor(pid, platform), {
1719
+ const state = createSessionState(context.cwd, context.baseStateDir, canonicalSessionId, pid, platform, sessionIdentityFor(pid, platform), {
1184
1720
  nativeSessionId: options.nativeSessionId ?? existing?.native_session_id,
1185
1721
  previousNativeSessionId: options.previousNativeSessionId ?? existing?.previous_native_session_id,
1186
1722
  nativeSessionSwitchedAt: options.nativeSessionSwitchedAt ?? existing?.native_session_switched_at,
1187
1723
  ...(ownerOmxSessionId ? { ownerOmxSessionId } : {}),
1724
+ launchLineageToken: existing
1725
+ ? (isValidToken(existing.launch_lineage_token) ? existing.launch_lineage_token : undefined)
1726
+ : transactionDependencies.token(),
1188
1727
  tmuxSessionName: options.tmuxSessionName ?? existing?.tmux_session_name,
1189
1728
  tmuxPaneId: options.tmuxPaneId ?? existing?.tmux_pane_id,
1190
1729
  });
1730
+ return preserveExistingLaunchLineageToken(existing, state);
1191
1731
  };
1192
1732
  }
1193
- /** Write or merge a wrapper-owned canonical pointer through the exact-root transaction. */
1733
+ /** Create a wrapper-owned canonical pointer only when the selected pointer is absent. */
1194
1734
  export async function writeSessionStart(cwd, sessionId, options = {}) {
1735
+ const candidateSessionId = normalizeSessionId(sessionId);
1736
+ let context;
1737
+ try {
1738
+ context = options.context ?? resolveSessionPointerContext(cwd);
1739
+ }
1740
+ catch (error) {
1741
+ throw contextAbort(cwd, candidateSessionId, error);
1742
+ }
1743
+ let pointer;
1744
+ try {
1745
+ pointer = await readSessionPointer(context);
1746
+ }
1747
+ catch (error) {
1748
+ throw resolvedAbort(context, {
1749
+ code: 'session_pointer_io_failure', operation: 'pointer-read', candidateSessionId,
1750
+ lockPath: context.lockPath, reason: `Unable to read selected session pointer: ${errorMessage(error)}`, cause: error,
1751
+ });
1752
+ }
1753
+ if (pointer.status !== 'absent') {
1754
+ if (pointer.status === 'usable' && pointer.state) {
1755
+ throw ownerConflictAbort(context, candidateSessionId ?? sessionId, pointer.state);
1756
+ }
1757
+ throw unusablePointerAbort(context, candidateSessionId ?? sessionId, pointer);
1758
+ }
1759
+ return await writeSessionStartTransition(cwd, sessionId, { ...options, context });
1760
+ }
1761
+ async function writeSessionStartTransition(cwd, sessionId, options, requireLaunchLineageToken = false, requireAbsent = false, beforeCommit, afterCommit) {
1195
1762
  const requestedSessionId = normalizeSessionId(sessionId);
1196
- const result = await writePointerTransaction(cwd, requestedSessionId, options, DEFAULT_POINTER_TIMEOUT_MS, startPointerTransition(requestedSessionId ?? sessionId, options), (state) => state);
1763
+ const result = await writePointerTransaction(cwd, requestedSessionId, options, DEFAULT_POINTER_TIMEOUT_MS, startPointerTransition(requestedSessionId ?? sessionId, options, requireLaunchLineageToken, requireAbsent), (state) => state, beforeCommit, afterCommit);
1197
1764
  await appendToLogAtContext(result.context, {
1198
1765
  event: 'session_start',
1199
1766
  session_id: result.value.session_id,
@@ -1203,6 +1770,551 @@ export async function writeSessionStart(cwd, sessionId, options = {}) {
1203
1770
  }).catch(() => { });
1204
1771
  return result.value;
1205
1772
  }
1773
+ const DIRECTORY_CAPABILITY_PLATFORMS = new Set([
1774
+ 'linux', 'darwin', 'freebsd', 'openbsd', 'netbsd', 'sunos',
1775
+ ]);
1776
+ export class LaunchContextResolutionError extends Error {
1777
+ cleanup;
1778
+ name = 'LaunchContextResolutionError';
1779
+ constructor(cleanup, cause) {
1780
+ super(`Unable to establish launch directory capability: ${errorMessage(cause)}`, { cause });
1781
+ this.cleanup = cleanup;
1782
+ }
1783
+ }
1784
+ const bindingLeases = new WeakMap();
1785
+ const bindingFinalizations = new WeakMap();
1786
+ function safeError(error) {
1787
+ return {
1788
+ name: error instanceof Error ? error.name : 'Error',
1789
+ message: errorMessage(error),
1790
+ ...(errorCode(error) ? { code: errorCode(error) } : {}),
1791
+ };
1792
+ }
1793
+ function lockDisposition(failures) {
1794
+ if (failures.some((failure) => failure.ownership === 'uncertain'))
1795
+ return 'uncertain';
1796
+ return failures.some((failure) => failure.ownership === 'held') ? 'held' : 'released-with-residue';
1797
+ }
1798
+ async function acquireDirectoryCapability(cwd, platform) {
1799
+ const cleanup = [];
1800
+ let canonicalRealpath;
1801
+ try {
1802
+ canonicalRealpath = await realpath(cwd);
1803
+ }
1804
+ catch (error) {
1805
+ throw new LaunchContextResolutionError({ capability: cleanup }, error);
1806
+ }
1807
+ if (!DIRECTORY_CAPABILITY_PLATFORMS.has(platform)) {
1808
+ return { canonicalRealpath, identity: { kind: 'unsupported', reason: 'platform' }, lease: { closed: false }, cleanup };
1809
+ }
1810
+ let handle;
1811
+ try {
1812
+ handle = await open(canonicalRealpath, 'r');
1813
+ }
1814
+ catch (error) {
1815
+ throw new LaunchContextResolutionError({ capability: cleanup }, error);
1816
+ }
1817
+ let statFailure;
1818
+ try {
1819
+ const stats = await handle.stat({ bigint: true });
1820
+ if (!stats.isDirectory() || typeof stats.dev !== 'bigint' || typeof stats.ino !== 'bigint' || stats.dev <= 0n || stats.ino <= 0n) {
1821
+ try {
1822
+ await handle.close();
1823
+ cleanup.push({ role: 'acquisition', phase: 'before-authorization', status: 'closed' });
1824
+ }
1825
+ catch (closeError) {
1826
+ cleanup.push({ role: 'acquisition', phase: 'before-authorization', status: 'failed', error: safeError(closeError) });
1827
+ throw new LaunchContextResolutionError({ capability: cleanup }, closeError);
1828
+ }
1829
+ return { canonicalRealpath, identity: { kind: 'unsupported', reason: 'inadequate-identity' }, lease: { closed: false }, cleanup };
1830
+ }
1831
+ return { canonicalRealpath, identity: { kind: 'supported', dev: stats.dev, ino: stats.ino }, lease: { handle, closed: false }, cleanup };
1832
+ }
1833
+ catch (error) {
1834
+ if (error instanceof LaunchContextResolutionError)
1835
+ throw error;
1836
+ statFailure = error;
1837
+ }
1838
+ try {
1839
+ await handle.close();
1840
+ cleanup.push({ role: 'acquisition', phase: 'before-authorization', status: 'closed' });
1841
+ }
1842
+ catch (closeError) {
1843
+ cleanup.push({ role: 'acquisition', phase: 'before-authorization', status: 'failed', error: safeError(closeError) });
1844
+ throw new LaunchContextResolutionError({ capability: cleanup }, closeError);
1845
+ }
1846
+ const code = errorCode(statFailure);
1847
+ if (code === 'ENOSYS' || code === 'ENOTSUP' || code === 'EOPNOTSUPP' || code === 'EINVAL') {
1848
+ return { canonicalRealpath, identity: { kind: 'unsupported', reason: 'stat-feature' }, lease: { closed: false }, cleanup };
1849
+ }
1850
+ throw new LaunchContextResolutionError({ capability: cleanup }, statFailure);
1851
+ }
1852
+ export async function closeLaunchSessionBindingOnce(binding, phase = 'post-finalization') {
1853
+ const lease = bindingLeases.get(binding);
1854
+ if (!lease || lease.closed)
1855
+ return lease?.closeEvidence ?? { role: 'original-retained', phase, status: 'not-needed' };
1856
+ lease.closed = true;
1857
+ try {
1858
+ await lease.handle?.close();
1859
+ return lease.closeEvidence = { role: 'original-retained', phase, status: 'closed' };
1860
+ }
1861
+ catch (error) {
1862
+ return lease.closeEvidence = { role: 'original-retained', phase, status: 'failed', error: safeError(error) };
1863
+ }
1864
+ }
1865
+ export async function establishLaunchSessionBinding(cwd, requestedSessionId, options = {}) {
1866
+ const platform = options.platform ?? process.platform;
1867
+ let context;
1868
+ try {
1869
+ context = options.context ?? resolveSessionPointerContext(cwd);
1870
+ await transactionDependencies.fs.mkdir(context.baseStateDir, { recursive: true });
1871
+ }
1872
+ catch (error) {
1873
+ throw new LaunchContextResolutionError({ capability: [] }, error);
1874
+ }
1875
+ const acquired = await acquireDirectoryCapability(context.baseStateDir, platform);
1876
+ const cleanup = () => ({ capability: acquired.cleanup });
1877
+ const revalidateSelectedRoot = async (lockedContext) => {
1878
+ if (acquired.identity.kind !== 'supported')
1879
+ return;
1880
+ const retained = await acquired.lease.handle?.stat({ bigint: true });
1881
+ if (!retained || !retained.isDirectory() || retained.dev !== acquired.identity.dev || retained.ino !== acquired.identity.ino) {
1882
+ throw resolvedAbort(lockedContext, {
1883
+ code: 'session_pointer_io_failure', operation: 'pointer-classify', candidateSessionId: normalizeSessionId(requestedSessionId),
1884
+ lockPath: lockedContext.lockPath, reason: 'Selected state root identity changed during pointer publication.',
1885
+ });
1886
+ }
1887
+ const canonical = await realpath(lockedContext.baseStateDir);
1888
+ const fresh = await open(canonical, 'r');
1889
+ try {
1890
+ const stats = await fresh.stat({ bigint: true });
1891
+ if (canonical !== acquired.canonicalRealpath || !stats.isDirectory() || stats.dev !== acquired.identity.dev || stats.ino !== acquired.identity.ino) {
1892
+ throw resolvedAbort(lockedContext, {
1893
+ code: 'session_pointer_io_failure', operation: 'pointer-classify', candidateSessionId: normalizeSessionId(requestedSessionId),
1894
+ lockPath: lockedContext.lockPath, reason: 'Selected state root was replaced during pointer publication.',
1895
+ });
1896
+ }
1897
+ }
1898
+ finally {
1899
+ await fresh.close();
1900
+ }
1901
+ };
1902
+ try {
1903
+ const state = await writeSessionStartTransition(context.cwd, requestedSessionId, { ...options, context }, true, true, revalidateSelectedRoot, revalidateSelectedRoot);
1904
+ const token = state.launch_lineage_token;
1905
+ if (!isValidToken(token)) {
1906
+ const close = await closeLease(acquired.lease, 'before-authorization');
1907
+ if (close)
1908
+ acquired.cleanup.push(close);
1909
+ throw new LaunchContextResolutionError(cleanup(), new Error('Committed session pointer lacks a valid lineage token.'));
1910
+ }
1911
+ const binding = Object.freeze({
1912
+ context: Object.freeze({ ...context }), canonicalRealpath: acquired.canonicalRealpath,
1913
+ directoryIdentity: acquired.identity, canonicalSessionId: state.session_id,
1914
+ ...(state.owner_omx_session_id ? { ownerOmxSessionId: state.owner_omx_session_id } : {}),
1915
+ ...(state.native_session_id ? { nativeSessionId: state.native_session_id } : {}),
1916
+ startedAt: state.started_at, launchLineageToken: token,
1917
+ });
1918
+ bindingLeases.set(binding, acquired.lease);
1919
+ return { kind: 'committed-released', binding, cleanup: cleanup() };
1920
+ }
1921
+ catch (error) {
1922
+ if (error instanceof CommittedLaunchBlockedError) {
1923
+ const state = error.value;
1924
+ const errorContext = error.context ?? context;
1925
+ const close = await closeLease(acquired.lease, 'before-authorization');
1926
+ if (close)
1927
+ acquired.cleanup.push(close);
1928
+ return {
1929
+ kind: 'committed-release-failed', evidence: { context: errorContext, canonicalSessionId: state?.session_id ?? requestedSessionId },
1930
+ error, lockDisposition: lockDisposition(error.secondaryFailures), secondaryFailures: error.secondaryFailures, cleanup: cleanup(),
1931
+ };
1932
+ }
1933
+ const close = await closeLease(acquired.lease, 'before-authorization');
1934
+ if (close)
1935
+ acquired.cleanup.push(close);
1936
+ if (isSessionPointerLaunchAbort(error))
1937
+ return { kind: 'precommit-aborted', abort: error, cleanup: cleanup() };
1938
+ throw error;
1939
+ }
1940
+ }
1941
+ async function closeLease(lease, phase) {
1942
+ if (lease.closed)
1943
+ return undefined;
1944
+ lease.closed = true;
1945
+ try {
1946
+ await lease.handle?.close();
1947
+ return { role: 'acquisition', phase, status: 'closed' };
1948
+ }
1949
+ catch (error) {
1950
+ return { role: 'acquisition', phase, status: 'failed', error: safeError(error) };
1951
+ }
1952
+ }
1953
+ export async function updateDetachedSessionMetadata(binding, patch) {
1954
+ const capability = [];
1955
+ const cleanup = { capability };
1956
+ try {
1957
+ let pointerBeforeTransaction;
1958
+ try {
1959
+ pointerBeforeTransaction = await readSessionPointer(binding.context);
1960
+ }
1961
+ catch (error) {
1962
+ throw resolvedAbort(binding.context, {
1963
+ code: 'session_pointer_io_failure', operation: 'pointer-read', candidateSessionId: binding.canonicalSessionId,
1964
+ lockPath: binding.context.lockPath, reason: `Unable to read selected session pointer: ${errorMessage(error)}`, cause: error,
1965
+ });
1966
+ }
1967
+ if (pointerBeforeTransaction.status !== 'usable'
1968
+ || !isAuthorizedBoundPointer(binding, binding.context, binding.canonicalSessionId, pointerBeforeTransaction.state)) {
1969
+ throw unusablePointerAbort(binding.context, binding.canonicalSessionId, pointerBeforeTransaction);
1970
+ }
1971
+ const authorization = await authorizeBoundDirectoryBeforeTransaction(binding, pointerBeforeTransaction, binding.context.cwd);
1972
+ capability.push(...authorization.capability);
1973
+ const result = await writePointerTransaction(binding.context.cwd, binding.canonicalSessionId, { context: binding.context }, DEFAULT_POINTER_TIMEOUT_MS, async (pointer, context) => {
1974
+ const state = pointer.status === 'usable' ? pointer.state : undefined;
1975
+ if (!state || !isAuthorizedBoundPointer(binding, context, binding.canonicalSessionId, state)) {
1976
+ throw unusablePointerAbort(context, binding.canonicalSessionId, pointer);
1977
+ }
1978
+ await consumeBoundDirectoryAuthorizationUnderLock(binding, authorization, pointer);
1979
+ return {
1980
+ ...state,
1981
+ ...(patch.tmuxSessionName !== undefined ? { tmux_session_name: patch.tmuxSessionName } : {}),
1982
+ ...(patch.tmuxPaneId !== undefined ? { tmux_pane_id: patch.tmuxPaneId } : {}),
1983
+ };
1984
+ }, (state) => state);
1985
+ return { kind: 'committed-released', evidence: { context: result.context, canonicalSessionId: binding.canonicalSessionId }, cleanup };
1986
+ }
1987
+ catch (error) {
1988
+ if (error instanceof CommittedLaunchBlockedError) {
1989
+ const context = error.context ?? binding.context;
1990
+ return { kind: 'committed-release-failed', evidence: { context, canonicalSessionId: binding.canonicalSessionId }, error,
1991
+ lockDisposition: lockDisposition(error.secondaryFailures), secondaryFailures: error.secondaryFailures, cleanup };
1992
+ }
1993
+ if (isSessionPointerLaunchAbort(error))
1994
+ return { kind: 'precommit-aborted', abort: error, cleanup };
1995
+ throw error;
1996
+ }
1997
+ }
1998
+ function isAuthorizedBoundPointer(binding, context, candidateSessionId, state) {
1999
+ traceSessionFinalizationOperation('auth-function-enter');
2000
+ traceSessionFinalizationOperation('lease-lookup-start');
2001
+ const lease = bindingLeases.get(binding);
2002
+ traceSessionFinalizationOperation('lease-lookup-done');
2003
+ traceSessionFinalizationOperation(`state-present:${state ? 'true' : 'false'}`);
2004
+ if (!lease || (binding.directoryIdentity.kind === 'supported' && lease.closed) || !state) {
2005
+ traceSessionFinalizationOperation('bound-auth-fail:lease-or-state');
2006
+ return false;
2007
+ }
2008
+ if (!isValidToken(binding.launchLineageToken) || !isValidToken(state.launch_lineage_token)) {
2009
+ traceSessionFinalizationOperation('bound-auth-fail:token-shape');
2010
+ return false;
2011
+ }
2012
+ if (candidateSessionId !== binding.canonicalSessionId) {
2013
+ traceSessionFinalizationOperation('bound-auth-fail:candidate-session');
2014
+ return false;
2015
+ }
2016
+ if (state.launch_lineage_token !== binding.launchLineageToken) {
2017
+ traceSessionFinalizationOperation('bound-auth-fail:lineage-token');
2018
+ return false;
2019
+ }
2020
+ if (state.started_at !== binding.startedAt) {
2021
+ traceSessionFinalizationOperation('bound-auth-fail:started-at');
2022
+ return false;
2023
+ }
2024
+ const directCanonicalIdentity = state.session_id === binding.canonicalSessionId
2025
+ && ((state.owner_omx_session_id ?? undefined) === binding.ownerOmxSessionId
2026
+ || (binding.ownerOmxSessionId === undefined && state.owner_omx_session_id === binding.canonicalSessionId))
2027
+ && (binding.nativeSessionId === undefined || state.native_session_id === binding.nativeSessionId);
2028
+ const replacedNativeIdentity = state.owner_omx_session_id === binding.canonicalSessionId
2029
+ && normalizeSessionId(state.session_id) !== undefined
2030
+ && state.native_session_id === state.session_id;
2031
+ if (!directCanonicalIdentity && !replacedNativeIdentity) {
2032
+ traceSessionFinalizationOperation('bound-auth-fail:session-identity');
2033
+ return false;
2034
+ }
2035
+ if (context.rootSource !== binding.context.rootSource)
2036
+ traceSessionFinalizationOperation('bound-auth-fail:root-source');
2037
+ else if (context.baseStateDir !== binding.context.baseStateDir)
2038
+ traceSessionFinalizationOperation('bound-auth-fail:base-state-dir');
2039
+ else if (context.sessionPath !== binding.context.sessionPath)
2040
+ traceSessionFinalizationOperation('bound-auth-fail:session-path');
2041
+ else if (context.cwd !== binding.context.cwd)
2042
+ traceSessionFinalizationOperation('bound-auth-fail:context-cwd');
2043
+ else if (state.cwd !== binding.context.cwd)
2044
+ traceSessionFinalizationOperation('bound-auth-fail:state-cwd');
2045
+ else {
2046
+ traceSessionFinalizationOperation('bound-auth-success');
2047
+ return true;
2048
+ }
2049
+ return false;
2050
+ }
2051
+ function isAuthorizedBoundStaleDeadPointer(binding, context, candidateSessionId, state) {
2052
+ return isAuthorizedBoundPointer(binding, context, candidateSessionId, state)
2053
+ && Boolean(normalizeSessionId(state?.native_session_id));
2054
+ }
2055
+ class BoundDirectoryComparisonDenied extends Error {
2056
+ comparison;
2057
+ capability;
2058
+ constructor(comparison, capability) {
2059
+ super(comparison.reason);
2060
+ this.comparison = comparison;
2061
+ this.capability = capability;
2062
+ }
2063
+ }
2064
+ function isAuthorizedBoundPointerForFinalization(binding, context, candidateSessionId, pointer, phase) {
2065
+ traceSessionFinalizationOperation(`${phase}-status:${pointer.status}`);
2066
+ if (pointer.status === 'usable') {
2067
+ traceSessionFinalizationOperation(`${phase}-usable-branch-enter`);
2068
+ return isAuthorizedBoundPointer(binding, context, candidateSessionId, pointer.state);
2069
+ }
2070
+ if (pointer.status === 'stale-dead') {
2071
+ traceSessionFinalizationOperation(`${phase}-stale-dead-branch-enter`);
2072
+ return isAuthorizedBoundStaleDeadPointer(binding, context, candidateSessionId, pointer.state);
2073
+ }
2074
+ if (pointer.status === 'identity-indeterminate') {
2075
+ traceSessionFinalizationOperation(`${phase}-identity-indeterminate-branch-enter`);
2076
+ const sameProcess = pointer.state?.pid === process.pid;
2077
+ traceSessionFinalizationOperation(`${phase}-identity-indeterminate-current-pid:${sameProcess ? 'true' : 'false'}`);
2078
+ return sameProcess && isAuthorizedBoundPointer(binding, context, candidateSessionId, pointer.state);
2079
+ }
2080
+ traceSessionFinalizationOperation(`${phase}-unsupported-status`);
2081
+ return false;
2082
+ }
2083
+ async function authorizeBoundDirectoryBeforeTransaction(binding, pointer, postLaunchCwd) {
2084
+ const capability = [];
2085
+ const lease = bindingLeases.get(binding);
2086
+ if (!lease || lease.closed) {
2087
+ throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: 'binding-lease-closed' }, capability);
2088
+ }
2089
+ try {
2090
+ if (!sameFilePath(binding.context.cwd, postLaunchCwd)
2091
+ || !pointer.state?.cwd
2092
+ || !sameFilePath(binding.context.cwd, pointer.state.cwd)) {
2093
+ throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: 'cwd-identity-mismatch' }, capability);
2094
+ }
2095
+ if (binding.directoryIdentity.kind !== 'supported') {
2096
+ return {
2097
+ comparison: { status: 'matched', reason: `unsupported-directory-capability:${binding.directoryIdentity.reason}` },
2098
+ capability,
2099
+ pointerStatus: pointer.status,
2100
+ pointerRaw: pointer.raw,
2101
+ };
2102
+ }
2103
+ const retained = fstatSync(lease.handle.fd, { bigint: true });
2104
+ if (!retained || !retained.isDirectory() || retained.dev !== binding.directoryIdentity.dev || retained.ino !== binding.directoryIdentity.ino) {
2105
+ throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: 'retained-directory-identity-mismatch' }, capability);
2106
+ }
2107
+ const canonical = await realpath(binding.context.baseStateDir);
2108
+ const fd = openSync(canonical, 'r');
2109
+ let matched = false;
2110
+ try {
2111
+ const stats = fstatSync(fd, { bigint: true });
2112
+ matched = canonical === binding.canonicalRealpath
2113
+ && stats.isDirectory()
2114
+ && stats.dev === binding.directoryIdentity.dev
2115
+ && stats.ino === binding.directoryIdentity.ino;
2116
+ }
2117
+ finally {
2118
+ try {
2119
+ closeSync(fd);
2120
+ capability.push({ role: 'fresh-comparison', phase: 'before-authorization', status: 'closed' });
2121
+ }
2122
+ catch (error) {
2123
+ capability.push({ role: 'fresh-comparison', phase: 'before-authorization', status: 'failed', error: safeError(error) });
2124
+ throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: 'fresh-comparison-close-failed' }, capability);
2125
+ }
2126
+ }
2127
+ if (!matched)
2128
+ throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: 'directory-identity-mismatch' }, capability);
2129
+ return { comparison: { status: 'matched' }, capability, pointerStatus: pointer.status, pointerRaw: pointer.raw };
2130
+ }
2131
+ catch (error) {
2132
+ if (error instanceof BoundDirectoryComparisonDenied)
2133
+ throw error;
2134
+ throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: errorMessage(error) }, capability);
2135
+ }
2136
+ }
2137
+ async function consumeBoundDirectoryAuthorizationUnderLock(binding, authorization, pointer) {
2138
+ if (authorization.comparison?.status !== 'matched' || binding.directoryIdentity.kind !== 'supported')
2139
+ return authorization;
2140
+ const lease = bindingLeases.get(binding);
2141
+ try {
2142
+ const retained = lease?.handle ? fstatSync(lease.handle.fd, { bigint: true }) : undefined;
2143
+ if (!retained || !retained.isDirectory() || retained.dev !== binding.directoryIdentity.dev || retained.ino !== binding.directoryIdentity.ino) {
2144
+ throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: 'retained-directory-identity-mismatch' }, authorization.capability);
2145
+ }
2146
+ if (!pointer.state?.cwd || !sameFilePath(binding.context.cwd, pointer.state.cwd)) {
2147
+ throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: 'cwd-identity-mismatch-under-lock' }, authorization.capability);
2148
+ }
2149
+ const canonical = await realpath(binding.context.baseStateDir);
2150
+ const fd = openSync(canonical, 'r');
2151
+ let matched = false;
2152
+ try {
2153
+ const stats = fstatSync(fd, { bigint: true });
2154
+ matched = canonical === binding.canonicalRealpath
2155
+ && stats.isDirectory()
2156
+ && stats.dev === binding.directoryIdentity.dev
2157
+ && stats.ino === binding.directoryIdentity.ino;
2158
+ }
2159
+ finally {
2160
+ try {
2161
+ closeSync(fd);
2162
+ authorization.capability.push({ role: 'fresh-comparison', phase: 'before-authorization', status: 'closed' });
2163
+ }
2164
+ catch (error) {
2165
+ authorization.capability.push({ role: 'fresh-comparison', phase: 'before-authorization', status: 'failed', error: safeError(error) });
2166
+ throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: 'fresh-comparison-close-failed-under-lock' }, authorization.capability);
2167
+ }
2168
+ }
2169
+ if (!matched)
2170
+ throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: 'directory-identity-mismatch-under-lock' }, authorization.capability);
2171
+ if (pointer.status !== authorization.pointerStatus || pointer.raw !== authorization.pointerRaw) {
2172
+ throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: 'pointer-changed-before-consume' }, authorization.capability);
2173
+ }
2174
+ return authorization;
2175
+ }
2176
+ catch (error) {
2177
+ if (error instanceof BoundDirectoryComparisonDenied)
2178
+ throw error;
2179
+ throw new BoundDirectoryComparisonDenied({ status: 'denied', reason: errorMessage(error) }, authorization.capability);
2180
+ }
2181
+ }
2182
+ export function finalizeBoundOnce(binding, _reason, postLaunchCwd = binding.context.cwd) {
2183
+ const existing = bindingFinalizations.get(binding);
2184
+ if (existing)
2185
+ return existing;
2186
+ const finalization = (async () => {
2187
+ try {
2188
+ const revalidation = await writeSessionEnd(binding.context.cwd, binding.canonicalSessionId, {
2189
+ context: binding.context, binding, postLaunchCwd,
2190
+ });
2191
+ return { finalized: true, cleanup: { capability: revalidation.capability, comparison: revalidation.comparison } };
2192
+ }
2193
+ catch (error) {
2194
+ if (error instanceof BoundDirectoryComparisonDenied) {
2195
+ return { finalized: false, cleanup: { capability: error.capability, comparison: error.comparison } };
2196
+ }
2197
+ if (isSessionPointerLaunchAbort(error)) {
2198
+ return { finalized: false, cleanup: { capability: [], comparison: { status: 'denied', reason: error.code } } };
2199
+ }
2200
+ throw error;
2201
+ }
2202
+ })();
2203
+ bindingFinalizations.set(binding, finalization);
2204
+ return finalization;
2205
+ }
2206
+ function nativeSessionOwnerTransition(nativeSessionId, options) {
2207
+ return (pointer, context) => {
2208
+ if (pointer.status !== 'absent' && pointer.status !== 'stale-dead' && pointer.status !== 'usable') {
2209
+ throw unusablePointerAbort(context, nativeSessionId, pointer);
2210
+ }
2211
+ const pid = resolvePid(options);
2212
+ const platform = options.platform ?? process.platform;
2213
+ const linuxIdentity = sessionIdentityFor(pid, platform);
2214
+ const existing = pointer.status === 'usable' ? pointer.state : undefined;
2215
+ if (existing && (normalizeSessionId(existing.session_id) !== nativeSessionId
2216
+ || normalizeSessionId(existing.native_session_id) !== nativeSessionId
2217
+ || existing.platform !== platform
2218
+ || !isSessionStateAuthoritativeForCwd(existing, context.cwd)
2219
+ || existing.pid !== pid)) {
2220
+ throw ownerConflictAbort(context, nativeSessionId, existing);
2221
+ }
2222
+ return createSessionState(context.cwd, context.baseStateDir, nativeSessionId, pid, platform, linuxIdentity, {
2223
+ nativeSessionId,
2224
+ startedAt: existing?.started_at,
2225
+ tmuxSessionName: options.tmuxSessionName ?? existing?.tmux_session_name,
2226
+ tmuxPaneId: options.tmuxPaneId ?? existing?.tmux_pane_id,
2227
+ });
2228
+ };
2229
+ }
2230
+ export async function writeNativeSessionOwner(cwd, nativeSessionId, options = {}) {
2231
+ const normalized = normalizeSessionId(nativeSessionId);
2232
+ const context = resolveNativeSessionOwnerContext(cwd, nativeSessionId);
2233
+ const result = await writePointerTransaction(cwd, normalized, { context }, NATIVE_POINTER_TIMEOUT_MS, nativeSessionOwnerTransition(normalized ?? nativeSessionId, options), (state) => state);
2234
+ return result.value;
2235
+ }
2236
+ /** Read and classify the exact native session-owner sidecar without consulting alternate roots. */
2237
+ export async function readNativeSessionOwnerEvidence(cwd, nativeSessionId) {
2238
+ const normalized = normalizeSessionId(nativeSessionId);
2239
+ if (!normalized)
2240
+ return { status: 'malformed' };
2241
+ const context = resolveNativeSessionOwnerContext(cwd, normalized);
2242
+ const nativeOwnerDir = dirname(context.sessionPath);
2243
+ const sessionsDir = dirname(nativeOwnerDir);
2244
+ const stateRootDir = dirname(sessionsDir);
2245
+ const directoryPaths = [stateRootDir, sessionsDir, nativeOwnerDir];
2246
+ const directoryIdentities = [];
2247
+ try {
2248
+ for (const directoryPath of directoryPaths) {
2249
+ const stat = await transactionDependencies.fs.lstat(directoryPath);
2250
+ if (stat.isSymbolicLink() || !stat.isDirectory())
2251
+ return { status: 'malformed' };
2252
+ directoryIdentities.push({ path: directoryPath, dev: stat.dev, ino: stat.ino });
2253
+ }
2254
+ const pathStat = await transactionDependencies.fs.lstat(context.sessionPath);
2255
+ if (pathStat.isSymbolicLink() || !pathStat.isFile())
2256
+ return { status: 'malformed' };
2257
+ }
2258
+ catch (error) {
2259
+ if (isNotFound(error))
2260
+ return { status: 'absent' };
2261
+ throw error;
2262
+ }
2263
+ const handle = await open(context.sessionPath, fsConstants.O_RDONLY | fsConstants.O_NOFOLLOW);
2264
+ let raw;
2265
+ try {
2266
+ const openedStat = await handle.stat();
2267
+ if (!openedStat.isFile())
2268
+ return { status: 'malformed' };
2269
+ raw = await handle.readFile({ encoding: 'utf8' });
2270
+ const finalPathStat = await transactionDependencies.fs.lstat(context.sessionPath);
2271
+ if (finalPathStat.isSymbolicLink()
2272
+ || finalPathStat.dev !== openedStat.dev
2273
+ || finalPathStat.ino !== openedStat.ino) {
2274
+ return { status: 'malformed', raw };
2275
+ }
2276
+ for (const identity of directoryIdentities) {
2277
+ const finalDirectoryStat = await transactionDependencies.fs.lstat(identity.path);
2278
+ if (finalDirectoryStat.isSymbolicLink()
2279
+ || !finalDirectoryStat.isDirectory()
2280
+ || finalDirectoryStat.dev !== identity.dev
2281
+ || finalDirectoryStat.ino !== identity.ino) {
2282
+ return { status: 'malformed', raw };
2283
+ }
2284
+ }
2285
+ }
2286
+ finally {
2287
+ await handle.close();
2288
+ }
2289
+ let pointer;
2290
+ try {
2291
+ pointer = classifyParsedSessionPointer(context, JSON.parse(raw), raw);
2292
+ }
2293
+ catch (error) {
2294
+ if (error instanceof SyntaxError)
2295
+ return { status: 'malformed', raw };
2296
+ throw error;
2297
+ }
2298
+ if (pointer.status === 'absent' || pointer.status === 'foreign-cwd' || pointer.status === 'malformed' || pointer.status === 'identity-indeterminate') {
2299
+ return pointer;
2300
+ }
2301
+ const state = pointer.state;
2302
+ return state?.session_id === normalized
2303
+ && state.native_session_id === normalized
2304
+ && state.platform === transactionDependencies.runtimePlatform
2305
+ && isSessionStateAuthoritativeForCwd(state, context.cwd)
2306
+ ? pointer
2307
+ : { status: 'malformed', state, raw: pointer.raw };
2308
+ }
2309
+ export async function readNativeSessionOwner(cwd, nativeSessionId) {
2310
+ try {
2311
+ const evidence = await readNativeSessionOwnerEvidence(cwd, nativeSessionId);
2312
+ return evidence.status === 'usable' ? evidence.state ?? null : null;
2313
+ }
2314
+ catch {
2315
+ return null;
2316
+ }
2317
+ }
1206
2318
  function reconcileNativeTransition(nativeSessionId, options) {
1207
2319
  return (pointer, context) => {
1208
2320
  if (pointer.status !== 'absent' && pointer.status !== 'stale-dead' && pointer.status !== 'usable') {
@@ -1212,42 +2324,19 @@ function reconcileNativeTransition(nativeSessionId, options) {
1212
2324
  const platform = options.platform ?? process.platform;
1213
2325
  const linuxIdentity = sessionIdentityFor(pid, platform);
1214
2326
  const existing = pointer.status === 'usable' ? pointer.state : undefined;
1215
- const nowIso = new Date().toISOString();
1216
2327
  if (!existing) {
1217
2328
  const ownerCandidate = verifiedOwnerCandidate(context, options);
1218
2329
  const ownerOmxSessionId = ownerCandidate;
1219
- return {
1220
- state: createSessionState(context.cwd, nativeSessionId, pid, platform, linuxIdentity, {
1221
- nativeSessionId,
1222
- ...(ownerOmxSessionId ? { ownerOmxSessionId } : {}),
1223
- }),
1224
- };
2330
+ return createSessionState(context.cwd, context.baseStateDir, nativeSessionId, pid, platform, linuxIdentity, {
2331
+ nativeSessionId,
2332
+ ...(ownerOmxSessionId ? { ownerOmxSessionId } : {}),
2333
+ });
1225
2334
  }
1226
2335
  const existingNativeSessionId = normalizeSessionId(existing.native_session_id);
1227
2336
  if (existingNativeSessionId && existingNativeSessionId !== nativeSessionId) {
1228
- const ownerOmxSessionId = getOmxLaunchSessionId(existing);
1229
- return {
1230
- state: createSessionState(context.cwd, nativeSessionId, pid, platform, linuxIdentity, {
1231
- nativeSessionId,
1232
- ...(ownerOmxSessionId ? {
1233
- previousNativeSessionId: existingNativeSessionId,
1234
- nativeSessionSwitchedAt: nowIso,
1235
- ownerOmxSessionId,
1236
- } : {}),
1237
- }),
1238
- ...(ownerOmxSessionId ? {
1239
- replacementLog: {
1240
- event: 'native_session_replaced',
1241
- session_id: ownerOmxSessionId,
1242
- ...(existing.session_id !== ownerOmxSessionId ? { active_session_id: existing.session_id } : {}),
1243
- previous_native_session_id: existingNativeSessionId,
1244
- replaced_by_native_session_id: nativeSessionId,
1245
- pid,
1246
- timestamp: nowIso,
1247
- },
1248
- } : {}),
1249
- };
2337
+ throw ownerConflictAbort(context, nativeSessionId, existing);
1250
2338
  }
2339
+ const nowIso = new Date().toISOString();
1251
2340
  const ownerCandidate = verifiedOwnerCandidate(context, options);
1252
2341
  let ownerOmxSessionId;
1253
2342
  try {
@@ -1256,39 +2345,37 @@ function reconcileNativeTransition(nativeSessionId, options) {
1256
2345
  catch (error) {
1257
2346
  throw ownerConflictAbort(context, nativeSessionId, existing, error);
1258
2347
  }
1259
- return {
1260
- state: createSessionState(context.cwd, existing.session_id, pid, platform, linuxIdentity, {
1261
- nowIso,
1262
- nativeSessionId,
1263
- previousNativeSessionId: existing.previous_native_session_id,
1264
- nativeSessionSwitchedAt: existing.native_session_switched_at,
1265
- ...(ownerOmxSessionId ? { ownerOmxSessionId } : {}),
1266
- startedAt: existing.started_at,
1267
- tmuxSessionName: existing.tmux_session_name,
1268
- tmuxPaneId: existing.tmux_pane_id,
1269
- }),
1270
- };
2348
+ return preserveExistingLaunchLineageToken(existing, createSessionState(context.cwd, context.baseStateDir, existing.session_id, pid, platform, linuxIdentity, {
2349
+ nowIso,
2350
+ nativeSessionId,
2351
+ previousNativeSessionId: existing.previous_native_session_id,
2352
+ nativeSessionSwitchedAt: existing.native_session_switched_at,
2353
+ ...(ownerOmxSessionId ? { ownerOmxSessionId } : {}),
2354
+ startedAt: existing.started_at,
2355
+ tmuxSessionName: existing.tmux_session_name,
2356
+ tmuxPaneId: existing.tmux_pane_id,
2357
+ launchLineageToken: isValidToken(existing.launch_lineage_token)
2358
+ ? existing.launch_lineage_token
2359
+ : undefined,
2360
+ }));
1271
2361
  };
1272
2362
  }
1273
2363
  /**
1274
- * Reconcile a native SessionStart without borrowing another root's pointer.
1275
- * A different native ID retains the existing OMX owner chain, but never binds a
1276
- * new owner alias during that replacement transition.
2364
+ * Reconcile native SessionStart only when the selected pointer is absent, stale,
2365
+ * or already belongs to that native session. A different live native ID is
2366
+ * authoritative owner evidence and must never be replaced.
1277
2367
  */
1278
2368
  export async function reconcileNativeSessionStart(cwd, nativeSessionId, options = {}) {
1279
2369
  const normalizedNativeSessionId = normalizeSessionId(nativeSessionId);
1280
- const result = await writePointerTransaction(cwd, normalizedNativeSessionId, options, NATIVE_POINTER_TIMEOUT_MS, reconcileNativeTransition(normalizedNativeSessionId ?? nativeSessionId, options), (transition) => transition.state);
1281
- if (result.value.replacementLog) {
1282
- await appendToLogAtContext(result.context, result.value.replacementLog).catch(() => { });
1283
- }
2370
+ const result = await writePointerTransaction(cwd, normalizedNativeSessionId, options, NATIVE_POINTER_TIMEOUT_MS, reconcileNativeTransition(normalizedNativeSessionId ?? nativeSessionId, options), (state) => state);
1284
2371
  await appendToLogAtContext(result.context, {
1285
- event: result.value.replacementLog ? 'session_start' : 'session_start_reconciled',
1286
- session_id: result.value.state.session_id,
2372
+ event: 'session_start_reconciled',
2373
+ session_id: result.value.session_id,
1287
2374
  native_session_id: normalizedNativeSessionId ?? nativeSessionId,
1288
- pid: result.value.state.pid,
1289
- timestamp: result.value.state.native_session_switched_at ?? new Date().toISOString(),
2375
+ pid: result.value.pid,
2376
+ timestamp: new Date().toISOString(),
1290
2377
  }).catch(() => { });
1291
- return result.value.state;
2378
+ return result.value;
1292
2379
  }
1293
2380
  function historyDirectory(context) {
1294
2381
  return join(dirname(context.baseStateDir), 'logs');
@@ -1334,27 +2421,45 @@ export async function writeSessionEnd(cwd, sessionId, options = {}) {
1334
2421
  cause,
1335
2422
  });
1336
2423
  }
2424
+ if (!options.binding) {
2425
+ throw resolvedAbort(context, {
2426
+ code: 'session_pointer_io_failure', operation: 'pointer-classify', candidateSessionId,
2427
+ lockPath: context.lockPath, reason: 'A live launch binding is required to end a session pointer.',
2428
+ });
2429
+ }
2430
+ let preLockPointer;
2431
+ traceSessionFinalizationOperation('preLock-read-start');
1337
2432
  try {
1338
- await transactionDependencies.fs.mkdir(context.baseStateDir, { recursive: true });
2433
+ preLockPointer = await readSessionPointer(context);
2434
+ traceSessionFinalizationOperation(`preLock-read-done:${preLockPointer.status}`);
1339
2435
  }
1340
2436
  catch (error) {
1341
2437
  throw resolvedAbort(context, {
1342
- code: 'session_pointer_io_failure',
1343
- operation: 'state-dir-create',
1344
- candidateSessionId,
1345
- lockPath: context.lockPath,
1346
- reason: `Unable to create selected session state directory: ${errorMessage(error)}`,
1347
- cause: error,
2438
+ code: 'session_pointer_io_failure', operation: 'pointer-read', candidateSessionId,
2439
+ lockPath: context.lockPath, reason: `Unable to read selected session pointer: ${errorMessage(error)}`, cause: error,
1348
2440
  });
1349
2441
  }
2442
+ const preLockAuthorized = isAuthorizedBoundPointerForFinalization(options.binding, context, candidateSessionId, preLockPointer, 'preLock');
2443
+ if (!preLockAuthorized)
2444
+ throw unusablePointerAbort(context, candidateSessionId, preLockPointer);
2445
+ traceSessionFinalizationOperation('pre-transaction-auth-start');
2446
+ await authorizeBoundDirectoryBeforeTransaction(options.binding, preLockPointer, options.postLaunchCwd ?? options.binding.context.cwd);
2447
+ traceSessionFinalizationOperation('pre-transaction-auth-done');
1350
2448
  const platform = options.platform ?? process.platform;
1351
2449
  const tracker = { degraded: false };
2450
+ traceSessionFinalizationOperation('acquirePointerLock-start');
1352
2451
  const lock = await acquirePointerLock(context, candidateSessionId, DEFAULT_POINTER_TIMEOUT_MS, platform, tracker, options.regularFileSync);
2452
+ traceSessionFinalizationOperation('acquirePointerLock-done');
1353
2453
  let primary;
2454
+ let revalidation = {
2455
+ comparison: { status: 'not-run' }, capability: [],
2456
+ };
1354
2457
  try {
1355
2458
  let pointer;
2459
+ traceSessionFinalizationOperation('under-lock-read-start');
1356
2460
  try {
1357
2461
  pointer = await readSessionPointer(context);
2462
+ traceSessionFinalizationOperation(`under-lock-read-done:${pointer.status}`);
1358
2463
  }
1359
2464
  catch (error) {
1360
2465
  throw resolvedAbort(context, {
@@ -1366,14 +2471,17 @@ export async function writeSessionEnd(cwd, sessionId, options = {}) {
1366
2471
  cause: error,
1367
2472
  });
1368
2473
  }
1369
- if (pointer.status !== 'absent' && pointer.status !== 'usable') {
2474
+ const authorized = isAuthorizedBoundPointerForFinalization(options.binding, context, candidateSessionId, pointer, 'under-lock');
2475
+ if (!authorized)
1370
2476
  throw unusablePointerAbort(context, candidateSessionId, pointer);
1371
- }
2477
+ traceSessionFinalizationOperation('under-lock-auth-start');
2478
+ const authorization = await authorizeBoundDirectoryBeforeTransaction(options.binding, pointer, options.postLaunchCwd ?? options.binding.context.cwd);
2479
+ traceSessionFinalizationOperation('under-lock-auth-done');
2480
+ traceSessionFinalizationOperation('under-lock-consume-start');
2481
+ revalidation = await consumeBoundDirectoryAuthorizationUnderLock(options.binding, authorization, pointer);
2482
+ traceSessionFinalizationOperation('under-lock-consume-done');
1372
2483
  const state = pointer.state;
1373
- const ownsCurrentSessionFile = state == null
1374
- || state.session_id === candidateSessionId
1375
- || state.native_session_id === candidateSessionId
1376
- || state.owner_omx_session_id === candidateSessionId;
2484
+ const ownsCurrentSessionFile = true;
1377
2485
  const endTime = new Date().toISOString();
1378
2486
  const historyEntry = {
1379
2487
  session_id: ownsCurrentSessionFile
@@ -1389,12 +2497,19 @@ export async function writeSessionEnd(cwd, sessionId, options = {}) {
1389
2497
  : {}),
1390
2498
  ...(!ownsCurrentSessionFile && state?.session_id ? { preserved_active_session_id: state.session_id } : {}),
1391
2499
  };
2500
+ traceSessionFinalizationOperation('history-mkdir-start');
1392
2501
  await nodeMkdir(historyDirectory(context), { recursive: true });
2502
+ traceSessionFinalizationOperation('history-mkdir-done');
2503
+ traceSessionFinalizationOperation('history-append-start');
1393
2504
  await appendFile(historyPath(context), `${JSON.stringify(historyEntry)}\n`);
2505
+ traceSessionFinalizationOperation('history-append-done');
2506
+ traceSessionFinalizationOperation('hud-cleanup-start');
1394
2507
  await removeDeadSessionHudState(context, [
1395
2508
  ...(ownsCurrentSessionFile ? [state?.session_id, state?.native_session_id] : []),
1396
2509
  candidateSessionId,
1397
2510
  ]);
2511
+ traceSessionFinalizationOperation('hud-cleanup-done');
2512
+ traceSessionFinalizationOperation('session-unlink-start');
1398
2513
  if (ownsCurrentSessionFile) {
1399
2514
  try {
1400
2515
  await transactionDependencies.fs.unlink(context.sessionPath);
@@ -1404,6 +2519,8 @@ export async function writeSessionEnd(cwd, sessionId, options = {}) {
1404
2519
  throw error;
1405
2520
  }
1406
2521
  }
2522
+ traceSessionFinalizationOperation('session-unlink-done');
2523
+ traceSessionFinalizationOperation('session-end-log-start');
1407
2524
  await appendToLogAtContext(context, {
1408
2525
  event: 'session_end',
1409
2526
  session_id: historyEntry.session_id,
@@ -1412,11 +2529,14 @@ export async function writeSessionEnd(cwd, sessionId, options = {}) {
1412
2529
  ...(historyEntry.preserved_active_session_id ? { preserved_active_session_id: historyEntry.preserved_active_session_id } : {}),
1413
2530
  timestamp: endTime,
1414
2531
  }).catch(() => { });
2532
+ traceSessionFinalizationOperation('session-end-log-done');
1415
2533
  }
1416
2534
  catch (error) {
1417
2535
  primary = error;
1418
2536
  }
2537
+ traceSessionFinalizationOperation('lock-release-start');
1419
2538
  const releaseFailures = await releasePointerLock(lock);
2539
+ traceSessionFinalizationOperation('lock-release-done');
1420
2540
  if (primary) {
1421
2541
  if (isSessionPointerLaunchAbort(primary) && releaseFailures.length > 0) {
1422
2542
  throw recoveryAbort(context, primary, releaseFailures, 'lock-release');
@@ -1426,6 +2546,7 @@ export async function writeSessionEnd(cwd, sessionId, options = {}) {
1426
2546
  if (releaseFailures.length > 0)
1427
2547
  throw releaseFailureError(releaseFailures);
1428
2548
  emitDegradedDurabilityWarning('session pointer end', tracker);
2549
+ return revalidation;
1429
2550
  }
1430
2551
  /** Reset session-scoped HUD/metrics files at launch. */
1431
2552
  export async function resetSessionMetrics(cwd, sessionId) {