oh-my-codex 0.20.2 → 0.20.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (805) hide show
  1. package/Cargo.lock +36 -18
  2. package/Cargo.toml +1 -1
  3. package/README.md +28 -2
  4. package/crates/omx-runtime/Cargo.toml +4 -0
  5. package/crates/omx-runtime/src/main.rs +415 -2
  6. package/crates/omx-runtime/tests/execution.rs +483 -0
  7. package/crates/omx-runtime-core/src/dispatch.rs +96 -10
  8. package/crates/omx-runtime-core/src/engine.rs +839 -29
  9. package/crates/omx-runtime-core/src/lib.rs +8 -0
  10. package/dist/adapt/__tests__/herdr-target.test.d.ts +2 -0
  11. package/dist/adapt/__tests__/herdr-target.test.d.ts.map +1 -0
  12. package/dist/adapt/__tests__/herdr-target.test.js +72 -0
  13. package/dist/adapt/__tests__/herdr-target.test.js.map +1 -0
  14. package/dist/adapt/contracts.d.ts +1 -1
  15. package/dist/adapt/contracts.d.ts.map +1 -1
  16. package/dist/adapt/contracts.js +1 -1
  17. package/dist/adapt/contracts.js.map +1 -1
  18. package/dist/adapt/herdr/__tests__/authority.test.d.ts +2 -0
  19. package/dist/adapt/herdr/__tests__/authority.test.d.ts.map +1 -0
  20. package/dist/adapt/herdr/__tests__/authority.test.js +49 -0
  21. package/dist/adapt/herdr/__tests__/authority.test.js.map +1 -0
  22. package/dist/adapt/herdr/__tests__/bridge.test.d.ts +2 -0
  23. package/dist/adapt/herdr/__tests__/bridge.test.d.ts.map +1 -0
  24. package/dist/adapt/herdr/__tests__/bridge.test.js +176 -0
  25. package/dist/adapt/herdr/__tests__/bridge.test.js.map +1 -0
  26. package/dist/adapt/herdr/__tests__/sanitize.test.d.ts +2 -0
  27. package/dist/adapt/herdr/__tests__/sanitize.test.d.ts.map +1 -0
  28. package/dist/adapt/herdr/__tests__/sanitize.test.js +45 -0
  29. package/dist/adapt/herdr/__tests__/sanitize.test.js.map +1 -0
  30. package/dist/adapt/herdr/__tests__/semantic.test.d.ts +2 -0
  31. package/dist/adapt/herdr/__tests__/semantic.test.d.ts.map +1 -0
  32. package/dist/adapt/herdr/__tests__/semantic.test.js +130 -0
  33. package/dist/adapt/herdr/__tests__/semantic.test.js.map +1 -0
  34. package/dist/adapt/herdr/__tests__/seq-store.test.d.ts +2 -0
  35. package/dist/adapt/herdr/__tests__/seq-store.test.d.ts.map +1 -0
  36. package/dist/adapt/herdr/__tests__/seq-store.test.js +54 -0
  37. package/dist/adapt/herdr/__tests__/seq-store.test.js.map +1 -0
  38. package/dist/adapt/herdr/__tests__/socket-live.test.d.ts +2 -0
  39. package/dist/adapt/herdr/__tests__/socket-live.test.d.ts.map +1 -0
  40. package/dist/adapt/herdr/__tests__/socket-live.test.js +104 -0
  41. package/dist/adapt/herdr/__tests__/socket-live.test.js.map +1 -0
  42. package/dist/adapt/herdr/__tests__/team-map.test.d.ts +2 -0
  43. package/dist/adapt/herdr/__tests__/team-map.test.d.ts.map +1 -0
  44. package/dist/adapt/herdr/__tests__/team-map.test.js +46 -0
  45. package/dist/adapt/herdr/__tests__/team-map.test.js.map +1 -0
  46. package/dist/adapt/herdr/__tests__/transport.test.d.ts +2 -0
  47. package/dist/adapt/herdr/__tests__/transport.test.d.ts.map +1 -0
  48. package/dist/adapt/herdr/__tests__/transport.test.js +160 -0
  49. package/dist/adapt/herdr/__tests__/transport.test.js.map +1 -0
  50. package/dist/adapt/herdr/__tests__/wiring.test.d.ts +2 -0
  51. package/dist/adapt/herdr/__tests__/wiring.test.d.ts.map +1 -0
  52. package/dist/adapt/herdr/__tests__/wiring.test.js +118 -0
  53. package/dist/adapt/herdr/__tests__/wiring.test.js.map +1 -0
  54. package/dist/adapt/herdr/authority.d.ts +34 -0
  55. package/dist/adapt/herdr/authority.d.ts.map +1 -0
  56. package/dist/adapt/herdr/authority.js +71 -0
  57. package/dist/adapt/herdr/authority.js.map +1 -0
  58. package/dist/adapt/herdr/bridge.d.ts +84 -0
  59. package/dist/adapt/herdr/bridge.d.ts.map +1 -0
  60. package/dist/adapt/herdr/bridge.js +222 -0
  61. package/dist/adapt/herdr/bridge.js.map +1 -0
  62. package/dist/adapt/herdr/sanitize.d.ts +20 -0
  63. package/dist/adapt/herdr/sanitize.d.ts.map +1 -0
  64. package/dist/adapt/herdr/sanitize.js +70 -0
  65. package/dist/adapt/herdr/sanitize.js.map +1 -0
  66. package/dist/adapt/herdr/semantic.d.ts +67 -0
  67. package/dist/adapt/herdr/semantic.d.ts.map +1 -0
  68. package/dist/adapt/herdr/semantic.js +170 -0
  69. package/dist/adapt/herdr/semantic.js.map +1 -0
  70. package/dist/adapt/herdr/seq-store.d.ts +32 -0
  71. package/dist/adapt/herdr/seq-store.d.ts.map +1 -0
  72. package/dist/adapt/herdr/seq-store.js +126 -0
  73. package/dist/adapt/herdr/seq-store.js.map +1 -0
  74. package/dist/adapt/herdr/team-map.d.ts +30 -0
  75. package/dist/adapt/herdr/team-map.d.ts.map +1 -0
  76. package/dist/adapt/herdr/team-map.js +37 -0
  77. package/dist/adapt/herdr/team-map.js.map +1 -0
  78. package/dist/adapt/herdr/transport.d.ts +115 -0
  79. package/dist/adapt/herdr/transport.d.ts.map +1 -0
  80. package/dist/adapt/herdr/transport.js +260 -0
  81. package/dist/adapt/herdr/transport.js.map +1 -0
  82. package/dist/adapt/herdr/wiring.d.ts +24 -0
  83. package/dist/adapt/herdr/wiring.d.ts.map +1 -0
  84. package/dist/adapt/herdr/wiring.js +62 -0
  85. package/dist/adapt/herdr/wiring.js.map +1 -0
  86. package/dist/adapt/herdr.d.ts +19 -0
  87. package/dist/adapt/herdr.d.ts.map +1 -0
  88. package/dist/adapt/herdr.js +115 -0
  89. package/dist/adapt/herdr.js.map +1 -0
  90. package/dist/adapt/index.d.ts +4 -0
  91. package/dist/adapt/index.d.ts.map +1 -1
  92. package/dist/adapt/index.js +48 -0
  93. package/dist/adapt/index.js.map +1 -1
  94. package/dist/adapt/registry.d.ts.map +1 -1
  95. package/dist/adapt/registry.js +11 -0
  96. package/dist/adapt/registry.js.map +1 -1
  97. package/dist/agents/__tests__/definitions.test.js +81 -0
  98. package/dist/agents/__tests__/definitions.test.js.map +1 -1
  99. package/dist/agents/__tests__/native-config.test.js +25 -0
  100. package/dist/agents/__tests__/native-config.test.js.map +1 -1
  101. package/dist/agents/native-config.d.ts +2 -1
  102. package/dist/agents/native-config.d.ts.map +1 -1
  103. package/dist/agents/native-config.js.map +1 -1
  104. package/dist/auth/__tests__/hotswap.test.js +60 -1
  105. package/dist/auth/__tests__/hotswap.test.js.map +1 -1
  106. package/dist/auth/__tests__/storage.test.js +33 -1
  107. package/dist/auth/__tests__/storage.test.js.map +1 -1
  108. package/dist/auth/hotswap.d.ts +22 -2
  109. package/dist/auth/hotswap.d.ts.map +1 -1
  110. package/dist/auth/hotswap.js +107 -22
  111. package/dist/auth/hotswap.js.map +1 -1
  112. package/dist/auth/storage.js +1 -1
  113. package/dist/auth/storage.js.map +1 -1
  114. package/dist/autopilot/__tests__/ralplan-gate.test.js +86 -1333
  115. package/dist/autopilot/__tests__/ralplan-gate.test.js.map +1 -1
  116. package/dist/autopilot/ralplan-gate.d.ts.map +1 -1
  117. package/dist/autopilot/ralplan-gate.js +7 -6
  118. package/dist/autopilot/ralplan-gate.js.map +1 -1
  119. package/dist/cli/__tests__/agents-init.test.js +4 -23
  120. package/dist/cli/__tests__/agents-init.test.js.map +1 -1
  121. package/dist/cli/__tests__/api.test.js +61 -24
  122. package/dist/cli/__tests__/api.test.js.map +1 -1
  123. package/dist/cli/__tests__/auth.test.js +63 -7
  124. package/dist/cli/__tests__/auth.test.js.map +1 -1
  125. package/dist/cli/__tests__/codex-feature-probe.test.js +66 -1
  126. package/dist/cli/__tests__/codex-feature-probe.test.js.map +1 -1
  127. package/dist/cli/__tests__/codex-plugin-layout.test.js +150 -2
  128. package/dist/cli/__tests__/codex-plugin-layout.test.js.map +1 -1
  129. package/dist/cli/__tests__/detached-leader-teardown.test.d.ts +2 -0
  130. package/dist/cli/__tests__/detached-leader-teardown.test.d.ts.map +1 -0
  131. package/dist/cli/__tests__/detached-leader-teardown.test.js +473 -0
  132. package/dist/cli/__tests__/detached-leader-teardown.test.js.map +1 -0
  133. package/dist/cli/__tests__/doctor-process-identity-readiness.test.d.ts +2 -0
  134. package/dist/cli/__tests__/doctor-process-identity-readiness.test.d.ts.map +1 -0
  135. package/dist/cli/__tests__/doctor-process-identity-readiness.test.js +103 -0
  136. package/dist/cli/__tests__/doctor-process-identity-readiness.test.js.map +1 -0
  137. package/dist/cli/__tests__/doctor-state-root-binding.test.d.ts +2 -0
  138. package/dist/cli/__tests__/doctor-state-root-binding.test.d.ts.map +1 -0
  139. package/dist/cli/__tests__/doctor-state-root-binding.test.js +139 -0
  140. package/dist/cli/__tests__/doctor-state-root-binding.test.js.map +1 -0
  141. package/dist/cli/__tests__/doctor-warning-copy.test.js +135 -1
  142. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  143. package/dist/cli/__tests__/exec.test.js +211 -0
  144. package/dist/cli/__tests__/exec.test.js.map +1 -1
  145. package/dist/cli/__tests__/index.test.js +878 -463
  146. package/dist/cli/__tests__/index.test.js.map +1 -1
  147. package/dist/cli/__tests__/launch-fallback.test.js +1290 -127
  148. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  149. package/dist/cli/__tests__/mcp-parity.test.js +8 -4
  150. package/dist/cli/__tests__/mcp-parity.test.js.map +1 -1
  151. package/dist/cli/__tests__/native-assets.test.js +398 -16
  152. package/dist/cli/__tests__/native-assets.test.js.map +1 -1
  153. package/dist/cli/__tests__/native-hook-claim-journal.test.js +132 -6
  154. package/dist/cli/__tests__/native-hook-claim-journal.test.js.map +1 -1
  155. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js +3 -3
  156. package/dist/cli/__tests__/ralph-goal-mode-contract.test.js.map +1 -1
  157. package/dist/cli/__tests__/ralph.test.js +3 -3
  158. package/dist/cli/__tests__/ralph.test.js.map +1 -1
  159. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js +49 -164
  160. package/dist/cli/__tests__/ralplan-bootstrap-3181.test.js.map +1 -1
  161. package/dist/cli/__tests__/ralplan-child-binding-3212.test.d.ts +2 -0
  162. package/dist/cli/__tests__/ralplan-child-binding-3212.test.d.ts.map +1 -0
  163. package/dist/cli/__tests__/ralplan-child-binding-3212.test.js +56 -0
  164. package/dist/cli/__tests__/ralplan-child-binding-3212.test.js.map +1 -0
  165. package/dist/cli/__tests__/ralplan.test.js +107 -109
  166. package/dist/cli/__tests__/ralplan.test.js.map +1 -1
  167. package/dist/cli/__tests__/resume.test.js +11 -10
  168. package/dist/cli/__tests__/resume.test.js.map +1 -1
  169. package/dist/cli/__tests__/session-scoped-runtime.test.js +1118 -43
  170. package/dist/cli/__tests__/session-scoped-runtime.test.js.map +1 -1
  171. package/dist/cli/__tests__/session-search-help.test.js +25 -1
  172. package/dist/cli/__tests__/session-search-help.test.js.map +1 -1
  173. package/dist/cli/__tests__/setup-agents-overwrite.test.js +8 -55
  174. package/dist/cli/__tests__/setup-agents-overwrite.test.js.map +1 -1
  175. package/dist/cli/__tests__/setup-install-mode.test.js +224 -7
  176. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  177. package/dist/cli/__tests__/sparkshell-cli.test.js +93 -41
  178. package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
  179. package/dist/cli/__tests__/team.test.js +150 -8
  180. package/dist/cli/__tests__/team.test.js.map +1 -1
  181. package/dist/cli/__tests__/ultragoal.test.js +117 -2
  182. package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
  183. package/dist/cli/__tests__/uninstall.test.js +95 -4
  184. package/dist/cli/__tests__/uninstall.test.js.map +1 -1
  185. package/dist/cli/__tests__/update.test.js +669 -177
  186. package/dist/cli/__tests__/update.test.js.map +1 -1
  187. package/dist/cli/__tests__/windows-popup-loop-contract.test.js +33 -2
  188. package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
  189. package/dist/cli/api.d.ts.map +1 -1
  190. package/dist/cli/api.js +9 -10
  191. package/dist/cli/api.js.map +1 -1
  192. package/dist/cli/codex-feature-probe.d.ts +16 -0
  193. package/dist/cli/codex-feature-probe.d.ts.map +1 -1
  194. package/dist/cli/codex-feature-probe.js +44 -0
  195. package/dist/cli/codex-feature-probe.js.map +1 -1
  196. package/dist/cli/codex-home.d.ts.map +1 -1
  197. package/dist/cli/codex-home.js +7 -4
  198. package/dist/cli/codex-home.js.map +1 -1
  199. package/dist/cli/doctor.d.ts +20 -0
  200. package/dist/cli/doctor.d.ts.map +1 -1
  201. package/dist/cli/doctor.js +290 -3
  202. package/dist/cli/doctor.js.map +1 -1
  203. package/dist/cli/index.d.ts +234 -12
  204. package/dist/cli/index.d.ts.map +1 -1
  205. package/dist/cli/index.js +2409 -746
  206. package/dist/cli/index.js.map +1 -1
  207. package/dist/cli/mcp-parity.js +2 -2
  208. package/dist/cli/mcp-parity.js.map +1 -1
  209. package/dist/cli/native-assets.d.ts +25 -0
  210. package/dist/cli/native-assets.d.ts.map +1 -1
  211. package/dist/cli/native-assets.js +448 -51
  212. package/dist/cli/native-assets.js.map +1 -1
  213. package/dist/cli/native-hook-claim-journal.d.ts +20 -5
  214. package/dist/cli/native-hook-claim-journal.d.ts.map +1 -1
  215. package/dist/cli/native-hook-claim-journal.js +67 -32
  216. package/dist/cli/native-hook-claim-journal.js.map +1 -1
  217. package/dist/cli/package-manager-ownership.d.ts +63 -0
  218. package/dist/cli/package-manager-ownership.d.ts.map +1 -0
  219. package/dist/cli/package-manager-ownership.js +323 -0
  220. package/dist/cli/package-manager-ownership.js.map +1 -0
  221. package/dist/cli/ralph.js +2 -2
  222. package/dist/cli/ralph.js.map +1 -1
  223. package/dist/cli/ralplan.d.ts +5 -7
  224. package/dist/cli/ralplan.d.ts.map +1 -1
  225. package/dist/cli/ralplan.js +83 -134
  226. package/dist/cli/ralplan.js.map +1 -1
  227. package/dist/cli/session-search.d.ts +5 -0
  228. package/dist/cli/session-search.d.ts.map +1 -1
  229. package/dist/cli/session-search.js +87 -0
  230. package/dist/cli/session-search.js.map +1 -1
  231. package/dist/cli/setup-preferences.d.ts +15 -0
  232. package/dist/cli/setup-preferences.d.ts.map +1 -1
  233. package/dist/cli/setup-preferences.js +27 -1
  234. package/dist/cli/setup-preferences.js.map +1 -1
  235. package/dist/cli/setup.d.ts +8 -0
  236. package/dist/cli/setup.d.ts.map +1 -1
  237. package/dist/cli/setup.js +119 -47
  238. package/dist/cli/setup.js.map +1 -1
  239. package/dist/cli/sparkshell.d.ts.map +1 -1
  240. package/dist/cli/sparkshell.js +52 -9
  241. package/dist/cli/sparkshell.js.map +1 -1
  242. package/dist/cli/ultragoal.d.ts.map +1 -1
  243. package/dist/cli/ultragoal.js +2 -2
  244. package/dist/cli/ultragoal.js.map +1 -1
  245. package/dist/cli/uninstall.d.ts +9 -0
  246. package/dist/cli/uninstall.d.ts.map +1 -1
  247. package/dist/cli/uninstall.js +71 -30
  248. package/dist/cli/uninstall.js.map +1 -1
  249. package/dist/cli/update-worker.d.ts +2 -0
  250. package/dist/cli/update-worker.d.ts.map +1 -0
  251. package/dist/cli/update-worker.js +142 -0
  252. package/dist/cli/update-worker.js.map +1 -0
  253. package/dist/cli/update.d.ts +14 -22
  254. package/dist/cli/update.d.ts.map +1 -1
  255. package/dist/cli/update.js +192 -241
  256. package/dist/cli/update.js.map +1 -1
  257. package/dist/compat/__tests__/rust-runtime-compat.test.js +1 -1
  258. package/dist/config/__tests__/generator-idempotent.test.js +503 -30
  259. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  260. package/dist/config/__tests__/generator-notify.test.js +5 -3
  261. package/dist/config/__tests__/generator-notify.test.js.map +1 -1
  262. package/dist/config/__tests__/models.test.js +67 -3
  263. package/dist/config/__tests__/models.test.js.map +1 -1
  264. package/dist/config/generator.d.ts +27 -21
  265. package/dist/config/generator.d.ts.map +1 -1
  266. package/dist/config/generator.js +610 -152
  267. package/dist/config/generator.js.map +1 -1
  268. package/dist/config/models.d.ts +12 -2
  269. package/dist/config/models.d.ts.map +1 -1
  270. package/dist/config/models.js +15 -1
  271. package/dist/config/models.js.map +1 -1
  272. package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js +25 -0
  273. package/dist/goal-workflows/__tests__/codex-goal-snapshot.test.js.map +1 -1
  274. package/dist/goal-workflows/codex-goal-snapshot.d.ts +1 -1
  275. package/dist/goal-workflows/codex-goal-snapshot.d.ts.map +1 -1
  276. package/dist/goal-workflows/codex-goal-snapshot.js +2 -0
  277. package/dist/goal-workflows/codex-goal-snapshot.js.map +1 -1
  278. package/dist/hooks/__tests__/agents-overlay.test.js +5 -3
  279. package/dist/hooks/__tests__/agents-overlay.test.js.map +1 -1
  280. package/dist/hooks/__tests__/autopilot-skill-contract.test.js +27 -4
  281. package/dist/hooks/__tests__/autopilot-skill-contract.test.js.map +1 -1
  282. package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.d.ts +2 -0
  283. package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.d.ts.map +1 -0
  284. package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.js +172 -0
  285. package/dist/hooks/__tests__/issue-3293-autopilot-handoff.test.js.map +1 -0
  286. package/dist/hooks/__tests__/keyword-detector.test.js +176 -37
  287. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  288. package/dist/hooks/__tests__/notify-fallback-watcher.test.js +402 -545
  289. package/dist/hooks/__tests__/notify-fallback-watcher.test.js.map +1 -1
  290. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js +39 -4
  291. package/dist/hooks/__tests__/notify-hook-all-workers-idle.test.js.map +1 -1
  292. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js +161 -26
  293. package/dist/hooks/__tests__/notify-hook-auto-nudge.test.js.map +1 -1
  294. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js +27 -0
  295. package/dist/hooks/__tests__/notify-hook-cross-worktree-heartbeat.test.js.map +1 -1
  296. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js +10 -118
  297. package/dist/hooks/__tests__/notify-hook-managed-tmux.test.js.map +1 -1
  298. package/dist/hooks/__tests__/notify-hook-regression-205.test.js +7 -0
  299. package/dist/hooks/__tests__/notify-hook-regression-205.test.js.map +1 -1
  300. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js +533 -14
  301. package/dist/hooks/__tests__/notify-hook-team-dispatch.test.js.map +1 -1
  302. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js +159 -24
  303. package/dist/hooks/__tests__/notify-hook-team-leader-nudge.test.js.map +1 -1
  304. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js +482 -32
  305. package/dist/hooks/__tests__/notify-hook-team-tmux-guard.test.js.map +1 -1
  306. package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.js +2 -1
  307. package/dist/hooks/__tests__/notify-hook-team-worker-fail-closed.test.js.map +1 -1
  308. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js +68 -1
  309. package/dist/hooks/__tests__/notify-hook-tmux-heal.test.js.map +1 -1
  310. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js +7 -1
  311. package/dist/hooks/__tests__/notify-hook-tmux-scrollback.test.js.map +1 -1
  312. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js +44 -4
  313. package/dist/hooks/__tests__/notify-hook-worker-idle.test.js.map +1 -1
  314. package/dist/hooks/__tests__/prompt-guidance-contract.test.js +9 -0
  315. package/dist/hooks/__tests__/prompt-guidance-contract.test.js.map +1 -1
  316. package/dist/hooks/__tests__/session.test.js +3130 -365
  317. package/dist/hooks/__tests__/session.test.js.map +1 -1
  318. package/dist/hooks/agents-overlay.js +1 -1
  319. package/dist/hooks/agents-overlay.js.map +1 -1
  320. package/dist/hooks/extensibility/__tests__/dispatcher.test.js +108 -2
  321. package/dist/hooks/extensibility/__tests__/dispatcher.test.js.map +1 -1
  322. package/dist/hooks/extensibility/__tests__/sdk.test.js +151 -1
  323. package/dist/hooks/extensibility/__tests__/sdk.test.js.map +1 -1
  324. package/dist/hooks/extensibility/dispatcher.d.ts.map +1 -1
  325. package/dist/hooks/extensibility/dispatcher.js +20 -3
  326. package/dist/hooks/extensibility/dispatcher.js.map +1 -1
  327. package/dist/hooks/extensibility/plugin-runner.js +1 -0
  328. package/dist/hooks/extensibility/plugin-runner.js.map +1 -1
  329. package/dist/hooks/extensibility/runtime.d.ts.map +1 -1
  330. package/dist/hooks/extensibility/runtime.js.map +1 -1
  331. package/dist/hooks/extensibility/sdk/paths.d.ts +1 -1
  332. package/dist/hooks/extensibility/sdk/paths.d.ts.map +1 -1
  333. package/dist/hooks/extensibility/sdk/paths.js +2 -2
  334. package/dist/hooks/extensibility/sdk/paths.js.map +1 -1
  335. package/dist/hooks/extensibility/sdk/runtime-state.d.ts +1 -1
  336. package/dist/hooks/extensibility/sdk/runtime-state.d.ts.map +1 -1
  337. package/dist/hooks/extensibility/sdk/runtime-state.js +55 -15
  338. package/dist/hooks/extensibility/sdk/runtime-state.js.map +1 -1
  339. package/dist/hooks/extensibility/sdk.d.ts +1 -0
  340. package/dist/hooks/extensibility/sdk.d.ts.map +1 -1
  341. package/dist/hooks/extensibility/sdk.js +1 -1
  342. package/dist/hooks/extensibility/sdk.js.map +1 -1
  343. package/dist/hooks/extensibility/types.d.ts +7 -0
  344. package/dist/hooks/extensibility/types.d.ts.map +1 -1
  345. package/dist/hooks/keyword-detector.d.ts +10 -3
  346. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  347. package/dist/hooks/keyword-detector.js +116 -20
  348. package/dist/hooks/keyword-detector.js.map +1 -1
  349. package/dist/hooks/session.d.ts +204 -14
  350. package/dist/hooks/session.d.ts.map +1 -1
  351. package/dist/hooks/session.js +1500 -223
  352. package/dist/hooks/session.js.map +1 -1
  353. package/dist/hud/__tests__/hud-tmux-injection.test.js +6 -6
  354. package/dist/hud/__tests__/hud-tmux-injection.test.js.map +1 -1
  355. package/dist/hud/__tests__/index.test.js +26 -16
  356. package/dist/hud/__tests__/index.test.js.map +1 -1
  357. package/dist/hud/__tests__/reconcile.test.js +83 -78
  358. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  359. package/dist/hud/__tests__/state.test.js +36 -31
  360. package/dist/hud/__tests__/state.test.js.map +1 -1
  361. package/dist/hud/__tests__/tmux.test.js +155 -198
  362. package/dist/hud/__tests__/tmux.test.js.map +1 -1
  363. package/dist/hud/tmux.d.ts +42 -4
  364. package/dist/hud/tmux.d.ts.map +1 -1
  365. package/dist/hud/tmux.js +1017 -139
  366. package/dist/hud/tmux.js.map +1 -1
  367. package/dist/leader/__tests__/contract.test.js +124 -37
  368. package/dist/leader/__tests__/contract.test.js.map +1 -1
  369. package/dist/leader/contract.d.ts +17 -6
  370. package/dist/leader/contract.d.ts.map +1 -1
  371. package/dist/leader/contract.js +102 -26
  372. package/dist/leader/contract.js.map +1 -1
  373. package/dist/mcp/__tests__/code-intel-server.test.js +31 -3
  374. package/dist/mcp/__tests__/code-intel-server.test.js.map +1 -1
  375. package/dist/mcp/__tests__/hermes-bridge.test.js +17 -0
  376. package/dist/mcp/__tests__/hermes-bridge.test.js.map +1 -1
  377. package/dist/mcp/__tests__/server-lifecycle.test.js +4 -4
  378. package/dist/mcp/__tests__/state-paths.test.js +735 -4
  379. package/dist/mcp/__tests__/state-paths.test.js.map +1 -1
  380. package/dist/mcp/__tests__/state-server.test.js +1 -1
  381. package/dist/mcp/__tests__/state-server.test.js.map +1 -1
  382. package/dist/mcp/__tests__/trace-server.test.js +2 -2
  383. package/dist/mcp/__tests__/trace-server.test.js.map +1 -1
  384. package/dist/mcp/__tests__/wiki-server.test.js +18 -0
  385. package/dist/mcp/__tests__/wiki-server.test.js.map +1 -1
  386. package/dist/mcp/code-intel-server.d.ts.map +1 -1
  387. package/dist/mcp/code-intel-server.js +13 -3
  388. package/dist/mcp/code-intel-server.js.map +1 -1
  389. package/dist/mcp/hermes-bridge.d.ts.map +1 -1
  390. package/dist/mcp/hermes-bridge.js +2 -2
  391. package/dist/mcp/hermes-bridge.js.map +1 -1
  392. package/dist/mcp/state-paths.d.ts +78 -4
  393. package/dist/mcp/state-paths.d.ts.map +1 -1
  394. package/dist/mcp/state-paths.js +543 -40
  395. package/dist/mcp/state-paths.js.map +1 -1
  396. package/dist/mcp/state-server.d.ts +4 -4
  397. package/dist/mcp/state-server.d.ts.map +1 -1
  398. package/dist/mcp/wiki-server.d.ts +11 -0
  399. package/dist/mcp/wiki-server.d.ts.map +1 -1
  400. package/dist/mcp/wiki-server.js +9 -2
  401. package/dist/mcp/wiki-server.js.map +1 -1
  402. package/dist/modes/__tests__/base-autopilot-gates.test.js +36 -7
  403. package/dist/modes/__tests__/base-autopilot-gates.test.js.map +1 -1
  404. package/dist/modes/__tests__/base-multi-state-compat.test.js +1 -1
  405. package/dist/modes/__tests__/base-multi-state-compat.test.js.map +1 -1
  406. package/dist/modes/__tests__/base-session-scope.test.js +17 -8
  407. package/dist/modes/__tests__/base-session-scope.test.js.map +1 -1
  408. package/dist/modes/__tests__/base-tmux-pane.test.js +4 -4
  409. package/dist/modes/__tests__/base-tmux-pane.test.js.map +1 -1
  410. package/dist/modes/base.d.ts +5 -5
  411. package/dist/modes/base.d.ts.map +1 -1
  412. package/dist/modes/base.js +76 -21
  413. package/dist/modes/base.js.map +1 -1
  414. package/dist/native-assets/archive.d.ts +17 -0
  415. package/dist/native-assets/archive.d.ts.map +1 -0
  416. package/dist/native-assets/archive.js +238 -0
  417. package/dist/native-assets/archive.js.map +1 -0
  418. package/dist/native-assets/policy.d.ts +42 -0
  419. package/dist/native-assets/policy.d.ts.map +1 -0
  420. package/dist/native-assets/policy.js +155 -0
  421. package/dist/native-assets/policy.js.map +1 -0
  422. package/dist/notifications/temp-contract.d.ts.map +1 -1
  423. package/dist/notifications/temp-contract.js +4 -0
  424. package/dist/notifications/temp-contract.js.map +1 -1
  425. package/dist/pipeline/__tests__/orchestrator.test.js +164 -42
  426. package/dist/pipeline/__tests__/orchestrator.test.js.map +1 -1
  427. package/dist/pipeline/__tests__/stages.test.js +72 -155
  428. package/dist/pipeline/__tests__/stages.test.js.map +1 -1
  429. package/dist/pipeline/orchestrator.d.ts.map +1 -1
  430. package/dist/pipeline/orchestrator.js +124 -18
  431. package/dist/pipeline/orchestrator.js.map +1 -1
  432. package/dist/pipeline/stages/ralplan.d.ts.map +1 -1
  433. package/dist/pipeline/stages/ralplan.js +36 -8
  434. package/dist/pipeline/stages/ralplan.js.map +1 -1
  435. package/dist/ralplan/__tests__/consensus-gate.test.js +73 -1316
  436. package/dist/ralplan/__tests__/consensus-gate.test.js.map +1 -1
  437. package/dist/ralplan/__tests__/documented-leader-preflight.test.d.ts +2 -0
  438. package/dist/ralplan/__tests__/documented-leader-preflight.test.d.ts.map +1 -0
  439. package/dist/ralplan/__tests__/documented-leader-preflight.test.js +335 -0
  440. package/dist/ralplan/__tests__/documented-leader-preflight.test.js.map +1 -0
  441. package/dist/ralplan/__tests__/runtime.test.js +85 -101
  442. package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
  443. package/dist/ralplan/consensus-gate.d.ts +9 -1
  444. package/dist/ralplan/consensus-gate.d.ts.map +1 -1
  445. package/dist/ralplan/consensus-gate.js +82 -338
  446. package/dist/ralplan/consensus-gate.js.map +1 -1
  447. package/dist/ralplan/documented-leader-preflight.d.ts +41 -0
  448. package/dist/ralplan/documented-leader-preflight.d.ts.map +1 -0
  449. package/dist/ralplan/documented-leader-preflight.js +360 -0
  450. package/dist/ralplan/documented-leader-preflight.js.map +1 -0
  451. package/dist/ralplan/runtime.d.ts +2 -1
  452. package/dist/ralplan/runtime.d.ts.map +1 -1
  453. package/dist/ralplan/runtime.js +67 -168
  454. package/dist/ralplan/runtime.js.map +1 -1
  455. package/dist/runtime/__tests__/bridge.test.js +42 -1
  456. package/dist/runtime/__tests__/bridge.test.js.map +1 -1
  457. package/dist/runtime/bridge.d.ts +12 -0
  458. package/dist/runtime/bridge.d.ts.map +1 -1
  459. package/dist/runtime/bridge.js +52 -1
  460. package/dist/runtime/bridge.js.map +1 -1
  461. package/dist/runtime/run-state.d.ts +5 -1
  462. package/dist/runtime/run-state.d.ts.map +1 -1
  463. package/dist/runtime/run-state.js +15 -8
  464. package/dist/runtime/run-state.js.map +1 -1
  465. package/dist/scripts/__tests__/codex-native-hook.test.js +16323 -8978
  466. package/dist/scripts/__tests__/codex-native-hook.test.js.map +1 -1
  467. package/dist/scripts/__tests__/issue-3293-callsite-parity.test.d.ts +2 -0
  468. package/dist/scripts/__tests__/issue-3293-callsite-parity.test.d.ts.map +1 -0
  469. package/dist/scripts/__tests__/issue-3293-callsite-parity.test.js +225 -0
  470. package/dist/scripts/__tests__/issue-3293-callsite-parity.test.js.map +1 -0
  471. package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.d.ts +2 -0
  472. package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.d.ts.map +1 -0
  473. package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.js +188 -0
  474. package/dist/scripts/__tests__/issue-3293-cancel-transaction.test.js.map +1 -0
  475. package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.d.ts +2 -0
  476. package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.d.ts.map +1 -0
  477. package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.js +259 -0
  478. package/dist/scripts/__tests__/issue-3293-hook-owned-cancel.test.js.map +1 -0
  479. package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.d.ts +2 -0
  480. package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.d.ts.map +1 -0
  481. package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.js +491 -0
  482. package/dist/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.js.map +1 -0
  483. package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.d.ts +2 -0
  484. package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.d.ts.map +1 -0
  485. package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.js +708 -0
  486. package/dist/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.js.map +1 -0
  487. package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.d.ts +2 -0
  488. package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.d.ts.map +1 -0
  489. package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.js +208 -0
  490. package/dist/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.js.map +1 -0
  491. package/dist/scripts/__tests__/notify-state-io.test.js +26 -0
  492. package/dist/scripts/__tests__/notify-state-io.test.js.map +1 -1
  493. package/dist/scripts/__tests__/notify-tmux-injection.test.js +23 -1
  494. package/dist/scripts/__tests__/notify-tmux-injection.test.js.map +1 -1
  495. package/dist/scripts/__tests__/postinstall.test.js +19 -1
  496. package/dist/scripts/__tests__/postinstall.test.js.map +1 -1
  497. package/dist/scripts/__tests__/reasoning-artifact-contract.test.d.ts +2 -0
  498. package/dist/scripts/__tests__/reasoning-artifact-contract.test.d.ts.map +1 -0
  499. package/dist/scripts/__tests__/reasoning-artifact-contract.test.js +108 -0
  500. package/dist/scripts/__tests__/reasoning-artifact-contract.test.js.map +1 -0
  501. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js +63 -173
  502. package/dist/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.js.map +1 -1
  503. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js +74 -159
  504. package/dist/scripts/__tests__/role-intent-durable-recovery-3181.test.js.map +1 -1
  505. package/dist/scripts/__tests__/smoke-packed-install.test.js +404 -13
  506. package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
  507. package/dist/scripts/__tests__/verify-native-release-assets.test.d.ts +2 -0
  508. package/dist/scripts/__tests__/verify-native-release-assets.test.d.ts.map +1 -0
  509. package/dist/scripts/__tests__/verify-native-release-assets.test.js +235 -0
  510. package/dist/scripts/__tests__/verify-native-release-assets.test.js.map +1 -0
  511. package/dist/scripts/codex-native-hook.d.ts +53 -1
  512. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  513. package/dist/scripts/codex-native-hook.js +14448 -1480
  514. package/dist/scripts/codex-native-hook.js.map +1 -1
  515. package/dist/scripts/codex-native-pre-post.d.ts +9 -0
  516. package/dist/scripts/codex-native-pre-post.d.ts.map +1 -1
  517. package/dist/scripts/codex-native-pre-post.js +35 -12
  518. package/dist/scripts/codex-native-pre-post.js.map +1 -1
  519. package/dist/scripts/generate-native-release-manifest.js +80 -51
  520. package/dist/scripts/generate-native-release-manifest.js.map +1 -1
  521. package/dist/scripts/notify-fallback-watcher.js +78 -115
  522. package/dist/scripts/notify-fallback-watcher.js.map +1 -1
  523. package/dist/scripts/notify-hook/auto-nudge.d.ts.map +1 -1
  524. package/dist/scripts/notify-hook/auto-nudge.js +74 -3
  525. package/dist/scripts/notify-hook/auto-nudge.js.map +1 -1
  526. package/dist/scripts/notify-hook/state-io.d.ts.map +1 -1
  527. package/dist/scripts/notify-hook/state-io.js +5 -0
  528. package/dist/scripts/notify-hook/state-io.js.map +1 -1
  529. package/dist/scripts/notify-hook/team-dispatch.d.ts +9 -0
  530. package/dist/scripts/notify-hook/team-dispatch.d.ts.map +1 -1
  531. package/dist/scripts/notify-hook/team-dispatch.js +234 -79
  532. package/dist/scripts/notify-hook/team-dispatch.js.map +1 -1
  533. package/dist/scripts/notify-hook/team-leader-nudge.d.ts.map +1 -1
  534. package/dist/scripts/notify-hook/team-leader-nudge.js +71 -16
  535. package/dist/scripts/notify-hook/team-leader-nudge.js.map +1 -1
  536. package/dist/scripts/notify-hook/team-tmux-guard.d.ts +12 -4
  537. package/dist/scripts/notify-hook/team-tmux-guard.d.ts.map +1 -1
  538. package/dist/scripts/notify-hook/team-tmux-guard.js +365 -44
  539. package/dist/scripts/notify-hook/team-tmux-guard.js.map +1 -1
  540. package/dist/scripts/notify-hook/team-worker-stop.d.ts.map +1 -1
  541. package/dist/scripts/notify-hook/team-worker-stop.js +42 -21
  542. package/dist/scripts/notify-hook/team-worker-stop.js.map +1 -1
  543. package/dist/scripts/notify-hook/team-worker.d.ts +3 -0
  544. package/dist/scripts/notify-hook/team-worker.d.ts.map +1 -1
  545. package/dist/scripts/notify-hook/team-worker.js +82 -33
  546. package/dist/scripts/notify-hook/team-worker.js.map +1 -1
  547. package/dist/scripts/notify-hook/tmux-injection.d.ts +1 -0
  548. package/dist/scripts/notify-hook/tmux-injection.d.ts.map +1 -1
  549. package/dist/scripts/notify-hook/tmux-injection.js +32 -3
  550. package/dist/scripts/notify-hook/tmux-injection.js.map +1 -1
  551. package/dist/scripts/notify-hook.js +79 -82
  552. package/dist/scripts/notify-hook.js.map +1 -1
  553. package/dist/scripts/postinstall-advisory.d.ts +14 -0
  554. package/dist/scripts/postinstall-advisory.d.ts.map +1 -0
  555. package/dist/scripts/postinstall-advisory.js +49 -0
  556. package/dist/scripts/postinstall-advisory.js.map +1 -0
  557. package/dist/scripts/postinstall.d.ts +1 -1
  558. package/dist/scripts/postinstall.d.ts.map +1 -1
  559. package/dist/scripts/postinstall.js +1 -1
  560. package/dist/scripts/postinstall.js.map +1 -1
  561. package/dist/scripts/smoke-packed-install.d.ts +80 -4
  562. package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
  563. package/dist/scripts/smoke-packed-install.js +3162 -26
  564. package/dist/scripts/smoke-packed-install.js.map +1 -1
  565. package/dist/scripts/sync-plugin-mirror.d.ts.map +1 -1
  566. package/dist/scripts/sync-plugin-mirror.js +16 -1
  567. package/dist/scripts/sync-plugin-mirror.js.map +1 -1
  568. package/dist/scripts/verify-native-release-assets.js +37 -23
  569. package/dist/scripts/verify-native-release-assets.js.map +1 -1
  570. package/dist/state/__tests__/mode-state-context.test.js +61 -0
  571. package/dist/state/__tests__/mode-state-context.test.js.map +1 -1
  572. package/dist/state/__tests__/operations.test.js +462 -850
  573. package/dist/state/__tests__/operations.test.js.map +1 -1
  574. package/dist/state/__tests__/skill-active.test.js +400 -2
  575. package/dist/state/__tests__/skill-active.test.js.map +1 -1
  576. package/dist/state/mode-state-context.d.ts +8 -0
  577. package/dist/state/mode-state-context.d.ts.map +1 -1
  578. package/dist/state/mode-state-context.js +73 -0
  579. package/dist/state/mode-state-context.js.map +1 -1
  580. package/dist/state/operations.d.ts +3 -0
  581. package/dist/state/operations.d.ts.map +1 -1
  582. package/dist/state/operations.js +144 -40
  583. package/dist/state/operations.js.map +1 -1
  584. package/dist/state/skill-active.d.ts +18 -1
  585. package/dist/state/skill-active.d.ts.map +1 -1
  586. package/dist/state/skill-active.js +420 -23
  587. package/dist/state/skill-active.js.map +1 -1
  588. package/dist/state/workflow-transition-reconcile.d.ts +3 -0
  589. package/dist/state/workflow-transition-reconcile.d.ts.map +1 -1
  590. package/dist/state/workflow-transition-reconcile.js +10 -4
  591. package/dist/state/workflow-transition-reconcile.js.map +1 -1
  592. package/dist/state/workflow-transition.d.ts.map +1 -1
  593. package/dist/state/workflow-transition.js +3 -1
  594. package/dist/state/workflow-transition.js.map +1 -1
  595. package/dist/subagents/__tests__/leader-bootstrap-3181.test.js +53 -247
  596. package/dist/subagents/__tests__/leader-bootstrap-3181.test.js.map +1 -1
  597. package/dist/subagents/__tests__/tracker.test.js +837 -1048
  598. package/dist/subagents/__tests__/tracker.test.js.map +1 -1
  599. package/dist/subagents/tracker.d.ts +58 -135
  600. package/dist/subagents/tracker.d.ts.map +1 -1
  601. package/dist/subagents/tracker.js +811 -632
  602. package/dist/subagents/tracker.js.map +1 -1
  603. package/dist/team/__tests__/api-interop.test.js +91 -1
  604. package/dist/team/__tests__/api-interop.test.js.map +1 -1
  605. package/dist/team/__tests__/delivery-e2e-smoke.test.js +58 -17
  606. package/dist/team/__tests__/delivery-e2e-smoke.test.js.map +1 -1
  607. package/dist/team/__tests__/idle-nudge.test.js +45 -15
  608. package/dist/team/__tests__/idle-nudge.test.js.map +1 -1
  609. package/dist/team/__tests__/mcp-comm.test.js +197 -30
  610. package/dist/team/__tests__/mcp-comm.test.js.map +1 -1
  611. package/dist/team/__tests__/model-contract.test.js +110 -3
  612. package/dist/team/__tests__/model-contract.test.js.map +1 -1
  613. package/dist/team/__tests__/notice-ledger.test.d.ts +2 -0
  614. package/dist/team/__tests__/notice-ledger.test.d.ts.map +1 -0
  615. package/dist/team/__tests__/notice-ledger.test.js +131 -0
  616. package/dist/team/__tests__/notice-ledger.test.js.map +1 -0
  617. package/dist/team/__tests__/pane-teardown-residue-e2e.test.d.ts +2 -0
  618. package/dist/team/__tests__/pane-teardown-residue-e2e.test.d.ts.map +1 -0
  619. package/dist/team/__tests__/pane-teardown-residue-e2e.test.js +410 -0
  620. package/dist/team/__tests__/pane-teardown-residue-e2e.test.js.map +1 -0
  621. package/dist/team/__tests__/phase-controller.test.js +20 -0
  622. package/dist/team/__tests__/phase-controller.test.js.map +1 -1
  623. package/dist/team/__tests__/runtime-cli.test.js +9 -0
  624. package/dist/team/__tests__/runtime-cli.test.js.map +1 -1
  625. package/dist/team/__tests__/runtime.test.js +3963 -428
  626. package/dist/team/__tests__/runtime.test.js.map +1 -1
  627. package/dist/team/__tests__/scaling.test.js +2405 -203
  628. package/dist/team/__tests__/scaling.test.js.map +1 -1
  629. package/dist/team/__tests__/state-root.test.js +117 -2
  630. package/dist/team/__tests__/state-root.test.js.map +1 -1
  631. package/dist/team/__tests__/state.test.js +804 -36
  632. package/dist/team/__tests__/state.test.js.map +1 -1
  633. package/dist/team/__tests__/team-ops-contract.test.js +6 -0
  634. package/dist/team/__tests__/team-ops-contract.test.js.map +1 -1
  635. package/dist/team/__tests__/tmux-session.test.js +4099 -1039
  636. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  637. package/dist/team/__tests__/tmux-source-authority-realtmux.test.d.ts +2 -0
  638. package/dist/team/__tests__/tmux-source-authority-realtmux.test.d.ts.map +1 -0
  639. package/dist/team/__tests__/tmux-source-authority-realtmux.test.js +149 -0
  640. package/dist/team/__tests__/tmux-source-authority-realtmux.test.js.map +1 -0
  641. package/dist/team/__tests__/tmux-test-fixture.d.ts +69 -0
  642. package/dist/team/__tests__/tmux-test-fixture.d.ts.map +1 -1
  643. package/dist/team/__tests__/tmux-test-fixture.js +242 -15
  644. package/dist/team/__tests__/tmux-test-fixture.js.map +1 -1
  645. package/dist/team/__tests__/tmux-test-fixture.test.js +177 -1
  646. package/dist/team/__tests__/tmux-test-fixture.test.js.map +1 -1
  647. package/dist/team/__tests__/worker-runtime-identity.test.js +53 -44
  648. package/dist/team/__tests__/worker-runtime-identity.test.js.map +1 -1
  649. package/dist/team/__tests__/worktree.test.js +6 -0
  650. package/dist/team/__tests__/worktree.test.js.map +1 -1
  651. package/dist/team/api-interop.d.ts.map +1 -1
  652. package/dist/team/api-interop.js +15 -9
  653. package/dist/team/api-interop.js.map +1 -1
  654. package/dist/team/exact-pane.d.ts +23 -0
  655. package/dist/team/exact-pane.d.ts.map +1 -0
  656. package/dist/team/exact-pane.js +133 -0
  657. package/dist/team/exact-pane.js.map +1 -0
  658. package/dist/team/idle-nudge.d.ts +12 -1
  659. package/dist/team/idle-nudge.d.ts.map +1 -1
  660. package/dist/team/idle-nudge.js +41 -14
  661. package/dist/team/idle-nudge.js.map +1 -1
  662. package/dist/team/model-contract.d.ts +2 -5
  663. package/dist/team/model-contract.d.ts.map +1 -1
  664. package/dist/team/model-contract.js +59 -17
  665. package/dist/team/model-contract.js.map +1 -1
  666. package/dist/team/notice-ledger.d.ts +54 -0
  667. package/dist/team/notice-ledger.d.ts.map +1 -0
  668. package/dist/team/notice-ledger.js +259 -0
  669. package/dist/team/notice-ledger.js.map +1 -0
  670. package/dist/team/phase-controller.d.ts.map +1 -1
  671. package/dist/team/phase-controller.js +2 -0
  672. package/dist/team/phase-controller.js.map +1 -1
  673. package/dist/team/runtime.d.ts +4 -1
  674. package/dist/team/runtime.d.ts.map +1 -1
  675. package/dist/team/runtime.js +1527 -291
  676. package/dist/team/runtime.js.map +1 -1
  677. package/dist/team/scaling.d.ts +13 -1
  678. package/dist/team/scaling.d.ts.map +1 -1
  679. package/dist/team/scaling.js +1503 -387
  680. package/dist/team/scaling.js.map +1 -1
  681. package/dist/team/state/dispatch.d.ts.map +1 -1
  682. package/dist/team/state/dispatch.js +26 -8
  683. package/dist/team/state/dispatch.js.map +1 -1
  684. package/dist/team/state/locks.d.ts.map +1 -1
  685. package/dist/team/state/locks.js +61 -13
  686. package/dist/team/state/locks.js.map +1 -1
  687. package/dist/team/state/monitor.d.ts +10 -0
  688. package/dist/team/state/monitor.d.ts.map +1 -1
  689. package/dist/team/state/monitor.js +13 -0
  690. package/dist/team/state/monitor.js.map +1 -1
  691. package/dist/team/state/tasks.d.ts.map +1 -1
  692. package/dist/team/state/tasks.js +4 -3
  693. package/dist/team/state/tasks.js.map +1 -1
  694. package/dist/team/state/types.d.ts +15 -0
  695. package/dist/team/state/types.d.ts.map +1 -1
  696. package/dist/team/state/types.js.map +1 -1
  697. package/dist/team/state-root.d.ts +12 -0
  698. package/dist/team/state-root.d.ts.map +1 -1
  699. package/dist/team/state-root.js +177 -0
  700. package/dist/team/state-root.js.map +1 -1
  701. package/dist/team/state.d.ts +85 -1
  702. package/dist/team/state.d.ts.map +1 -1
  703. package/dist/team/state.js +672 -129
  704. package/dist/team/state.js.map +1 -1
  705. package/dist/team/team-ops.d.ts +5 -0
  706. package/dist/team/team-ops.d.ts.map +1 -1
  707. package/dist/team/team-ops.js +4 -0
  708. package/dist/team/team-ops.js.map +1 -1
  709. package/dist/team/tmux-session.d.ts +163 -27
  710. package/dist/team/tmux-session.d.ts.map +1 -1
  711. package/dist/team/tmux-session.js +1682 -376
  712. package/dist/team/tmux-session.js.map +1 -1
  713. package/dist/team/worktree.d.ts.map +1 -1
  714. package/dist/team/worktree.js +4 -0
  715. package/dist/team/worktree.js.map +1 -1
  716. package/dist/ultragoal/__tests__/artifacts.test.js +950 -2
  717. package/dist/ultragoal/__tests__/artifacts.test.js.map +1 -1
  718. package/dist/ultragoal/__tests__/docs-contract.test.js +1 -0
  719. package/dist/ultragoal/__tests__/docs-contract.test.js.map +1 -1
  720. package/dist/ultragoal/artifacts.d.ts +35 -1
  721. package/dist/ultragoal/artifacts.d.ts.map +1 -1
  722. package/dist/ultragoal/artifacts.js +232 -46
  723. package/dist/ultragoal/artifacts.js.map +1 -1
  724. package/dist/utils/__tests__/file-durability.test.d.ts +2 -0
  725. package/dist/utils/__tests__/file-durability.test.d.ts.map +1 -0
  726. package/dist/utils/__tests__/file-durability.test.js +114 -0
  727. package/dist/utils/__tests__/file-durability.test.js.map +1 -0
  728. package/dist/utils/__tests__/paths.test.js +21 -1
  729. package/dist/utils/__tests__/paths.test.js.map +1 -1
  730. package/dist/utils/file-durability.d.ts +25 -0
  731. package/dist/utils/file-durability.d.ts.map +1 -0
  732. package/dist/utils/file-durability.js +67 -0
  733. package/dist/utils/file-durability.js.map +1 -0
  734. package/dist/utils/paths.d.ts.map +1 -1
  735. package/dist/utils/paths.js +14 -6
  736. package/dist/utils/paths.js.map +1 -1
  737. package/dist/verification/__tests__/ci-rust-gates.test.js +49 -4
  738. package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
  739. package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js +3 -0
  740. package/dist/verification/__tests__/dev-merge-issue-close-workflow.test.js.map +1 -1
  741. package/dist/verification/__tests__/explore-harness-release-workflow.test.js +60 -0
  742. package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +1 -1
  743. package/dist/verification/__tests__/native-release-manifest.test.js +157 -4
  744. package/dist/verification/__tests__/native-release-manifest.test.js.map +1 -1
  745. package/dist/wiki/lint.d.ts +4 -1
  746. package/dist/wiki/lint.d.ts.map +1 -1
  747. package/dist/wiki/lint.js +3 -2
  748. package/dist/wiki/lint.js.map +1 -1
  749. package/package.json +13 -5
  750. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  751. package/plugins/oh-my-codex/hooks/codex-native-hook.mjs +105 -17
  752. package/plugins/oh-my-codex/skills/autopilot/SKILL.md +13 -5
  753. package/plugins/oh-my-codex/skills/cancel/SKILL.md +30 -85
  754. package/plugins/oh-my-codex/skills/ralph/SKILL.md +6 -4
  755. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +16 -17
  756. package/plugins/oh-my-codex/skills/team/SKILL.md +13 -9
  757. package/plugins/oh-my-codex/skills/ultragoal/SKILL.md +3 -2
  758. package/skills/autopilot/SKILL.md +13 -5
  759. package/skills/cancel/SKILL.md +30 -85
  760. package/skills/ralph/SKILL.md +6 -4
  761. package/skills/ralplan/SKILL.md +16 -17
  762. package/skills/team/SKILL.md +13 -9
  763. package/skills/ultragoal/SKILL.md +3 -2
  764. package/src/scripts/__tests__/codex-native-hook.test.ts +8831 -951
  765. package/src/scripts/__tests__/issue-3293-callsite-parity.test.ts +187 -0
  766. package/src/scripts/__tests__/issue-3293-cancel-transaction.test.ts +189 -0
  767. package/src/scripts/__tests__/issue-3293-hook-owned-cancel.test.ts +187 -0
  768. package/src/scripts/__tests__/issue-3311-ultragoal-native-outside-tmux.test.ts +591 -0
  769. package/src/scripts/__tests__/issue-3358-ultragoal-cancel-lifecycle.test.ts +701 -0
  770. package/src/scripts/__tests__/issue-3369-autopilot-ralplan-state-machine.test.ts +211 -0
  771. package/src/scripts/__tests__/notify-state-io.test.ts +29 -0
  772. package/src/scripts/__tests__/notify-tmux-injection.test.ts +26 -1
  773. package/src/scripts/__tests__/postinstall.test.ts +23 -1
  774. package/src/scripts/__tests__/reasoning-artifact-contract.test.ts +129 -0
  775. package/src/scripts/__tests__/role-intent-bootstrap-e2e-3181.test.ts +67 -185
  776. package/src/scripts/__tests__/role-intent-durable-recovery-3181.test.ts +79 -178
  777. package/src/scripts/__tests__/smoke-packed-install.test.ts +525 -16
  778. package/src/scripts/__tests__/verify-native-release-assets.test.ts +240 -0
  779. package/src/scripts/codex-native-hook.ts +14238 -1429
  780. package/src/scripts/codex-native-pre-post.ts +43 -12
  781. package/src/scripts/generate-native-release-manifest.ts +83 -79
  782. package/src/scripts/notify-fallback-watcher.ts +91 -122
  783. package/src/scripts/notify-hook/auto-nudge.ts +71 -3
  784. package/src/scripts/notify-hook/state-io.ts +11 -0
  785. package/src/scripts/notify-hook/team-dispatch.ts +242 -75
  786. package/src/scripts/notify-hook/team-leader-nudge.ts +72 -22
  787. package/src/scripts/notify-hook/team-tmux-guard.ts +366 -39
  788. package/src/scripts/notify-hook/team-worker-stop.ts +43 -20
  789. package/src/scripts/notify-hook/team-worker.ts +82 -31
  790. package/src/scripts/notify-hook/tmux-injection.ts +33 -3
  791. package/src/scripts/notify-hook.ts +84 -82
  792. package/src/scripts/postinstall-advisory.ts +67 -0
  793. package/src/scripts/postinstall.ts +1 -1
  794. package/src/scripts/smoke-packed-install.ts +3608 -39
  795. package/src/scripts/sync-plugin-mirror.ts +17 -1
  796. package/src/scripts/verify-native-release-assets.ts +39 -31
  797. package/templates/AGENTS.md +1 -0
  798. package/dist/subagents/__tests__/adapted-role-binding.test.d.ts +0 -2
  799. package/dist/subagents/__tests__/adapted-role-binding.test.d.ts.map +0 -1
  800. package/dist/subagents/__tests__/adapted-role-binding.test.js +0 -988
  801. package/dist/subagents/__tests__/adapted-role-binding.test.js.map +0 -1
  802. package/dist/subagents/adapted-role-binding.d.ts +0 -17
  803. package/dist/subagents/adapted-role-binding.d.ts.map +0 -1
  804. package/dist/subagents/adapted-role-binding.js +0 -75
  805. package/dist/subagents/adapted-role-binding.js.map +0 -1
@@ -5,13 +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, 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';
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
+ }
14
26
  const SESSION_FILE = 'session.json';
27
+ const SESSION_OWNER_FILE = 'session-owner.json';
15
28
  const HISTORY_FILE = 'session-history.jsonl';
16
29
  const SESSION_ID_PATTERN = /^[A-Za-z0-9_-]{1,64}$/;
17
30
  const SESSION_POINTER_TOKEN_PATTERN = /^[A-Za-z0-9_-]{16,128}$/;
@@ -19,6 +32,14 @@ const SHA256_PATTERN = /^[a-f0-9]{64}$/;
19
32
  const LOCK_RETRY_DELAYS_MS = [25, 50, 100];
20
33
  const DEFAULT_POINTER_TIMEOUT_MS = 5_000;
21
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
+ }
22
43
  /**
23
44
  * Convert arbitrary input into a valid session ID without exposing validator
24
45
  * exceptions to lifecycle or hook inputs.
@@ -30,9 +51,9 @@ export function normalizeSessionId(value) {
30
51
  return SESSION_ID_PATTERN.test(trimmed) ? trimmed : undefined;
31
52
  }
32
53
  /** Resolve the one exact pointer path used by an operation. */
33
- export function resolveSessionPointerContext(cwd) {
34
- const normalizedCwd = resolveWorkingDirectoryForState(cwd);
35
- const { baseStateDir, rootSource } = getBaseStateDirWithSource(normalizedCwd);
54
+ export function resolveSessionPointerContext(cwd, env = process.env) {
55
+ const normalizedCwd = resolveWorkingDirectoryForState(cwd, env);
56
+ const { baseStateDir, rootSource } = getBaseStateDirWithSource(normalizedCwd, env);
36
57
  const pointerPath = join(baseStateDir, SESSION_FILE);
37
58
  return {
38
59
  cwd: normalizedCwd,
@@ -42,6 +63,26 @@ export function resolveSessionPointerContext(cwd) {
42
63
  lockPath: `${pointerPath}.lock`,
43
64
  };
44
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
+ }
45
86
  function attemptedStateRootSource() {
46
87
  try {
47
88
  if (process.env.OMX_TEAM_STATE_ROOT?.trim())
@@ -122,13 +163,14 @@ const defaultFsDependencies = {
122
163
  },
123
164
  readdir: async (path) => await nodeReaddir(path),
124
165
  readFile: async (path, encoding) => await nodeReadFile(path, encoding),
166
+ lstat: async (path) => await nodeLstat(path),
125
167
  writeFile: async (path, data, options) => {
126
168
  await nodeWriteFile(path, data, options);
127
169
  },
128
- openAndSync: async (path) => {
170
+ openAndSync: async (path, platform, regularFileSync) => {
129
171
  const handle = await open(path, 'r');
130
172
  try {
131
- await handle.sync();
173
+ return await syncRegularFile(regularFileSync ? { sync: () => regularFileSync(platform) } : handle, platform);
132
174
  }
133
175
  finally {
134
176
  await handle.close();
@@ -137,6 +179,9 @@ const defaultFsDependencies = {
137
179
  rename: async (from, to) => {
138
180
  await nodeRename(from, to);
139
181
  },
182
+ link: async (path, dest) => {
183
+ await nodeLink(path, dest);
184
+ },
140
185
  unlink: async (path) => {
141
186
  await nodeUnlink(path);
142
187
  },
@@ -144,6 +189,28 @@ const defaultFsDependencies = {
144
189
  await nodeRmdir(path);
145
190
  },
146
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
+ }
147
214
  /** @internal Exposed only so source-module tests can verify default ESRCH handling. */
148
215
  export function __createDefaultPidProbeForTests(killZero) {
149
216
  return (pid) => {
@@ -161,26 +228,40 @@ function defaultProbePid(pid) {
161
228
  process.kill(targetPid, 0);
162
229
  })(pid);
163
230
  }
164
- function defaultReadProcessIdentity(pid, platform) {
165
- if (platform !== 'linux')
166
- return { status: 'indeterminate' };
167
- const identity = readLinuxProcessIdentity(pid);
168
- if (!identity)
169
- return { status: 'indeterminate' };
170
- const cmdlineHash = hashCmdline(identity.cmdline);
171
- return {
172
- status: 'matching',
173
- startTicks: identity.startTicks,
174
- ...(cmdlineHash ? { cmdlineHash } : {}),
175
- };
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
+ }
176
251
  }
177
252
  const defaultTransactionDependencies = {
178
253
  fs: defaultFsDependencies,
179
254
  nowMs: () => Date.now(),
180
255
  sleep: async (ms) => await new Promise((resolve) => setTimeout(resolve, ms)),
181
256
  token: () => randomUUID(),
257
+ runtimePlatform: process.platform,
182
258
  probePid: defaultProbePid,
183
- readProcessIdentity: defaultReadProcessIdentity,
259
+ observeProcess: defaultObserveProcess,
260
+ atomicRenameNoReplace: defaultRecoveryRenameNoReplace,
261
+ };
262
+ export const defaultProcessInspectionProvider = {
263
+ probePid: defaultProbePid,
264
+ observeProcess: defaultObserveProcess,
184
265
  };
185
266
  let transactionDependencies = defaultTransactionDependencies;
186
267
  /** @internal Source-module test harness. Always reset after a test. */
@@ -232,13 +313,118 @@ function readLinuxProcessIdentity(pid) {
232
313
  return null;
233
314
  }
234
315
  }
235
- 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;
236
363
  try {
237
- process.kill(pid, 0);
238
- 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
+ };
239
425
  }
240
426
  catch {
241
- return false;
427
+ return { kind: 'error' };
242
428
  }
243
429
  }
244
430
  /**
@@ -248,22 +434,122 @@ function defaultIsPidAlive(pid) {
248
434
  export function isSessionStale(state, options = {}) {
249
435
  if (!Number.isInteger(state.pid) || state.pid <= 0)
250
436
  return true;
251
- const isPidAlive = options.isPidAlive ?? defaultIsPidAlive;
252
- if (!isPidAlive(state.pid))
253
- return true;
254
- const platform = options.platform ?? process.platform;
255
- if (platform !== 'linux')
256
- return false;
257
- const liveIdentity = (options.readLinuxIdentity ?? readLinuxProcessIdentity)(state.pid);
258
- if (!liveIdentity || typeof state.pid_start_ticks !== 'number')
259
- return true;
260
- if (state.pid_start_ticks !== liveIdentity.startTicks)
261
- return true;
262
- const expectedCmdline = normalizeCmdline(state.pid_cmdline);
263
- if (!expectedCmdline)
264
- return false;
265
- const liveCmdline = normalizeCmdline(liveIdentity.cmdline);
266
- 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';
267
553
  }
268
554
  export function isSessionStateAuthoritativeForCwd(state, cwd) {
269
555
  if (!normalizeSessionId(state.session_id))
@@ -283,54 +569,47 @@ export function isSessionStateUsable(state, cwd, options = {}) {
283
569
  if (typeof state.cwd === 'string' && state.cwd.trim() && !isSessionStateAuthoritativeForCwd(state, cwd))
284
570
  return false;
285
571
  const hasPidMetadata = Number.isInteger(state.pid) && state.pid > 0;
286
- const hasLinuxIdentityMetadata = typeof state.pid_start_ticks === 'number'
287
- || typeof state.pid_cmdline === 'string';
288
- return !hasPidMetadata && !hasLinuxIdentityMetadata || !isSessionStale(state, options);
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);
289
577
  }
290
578
  function isValidStartTicks(value) {
291
579
  return typeof value === 'number' && Number.isInteger(value) && value >= 0;
292
580
  }
293
581
  function classifySessionProcess(state, dependencies) {
294
582
  const hasPidMetadata = Number.isInteger(state.pid) && state.pid > 0;
295
- 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;
296
587
  if (!hasPidMetadata && !hasIdentityMetadata)
297
588
  return 'usable';
298
589
  if (!hasPidMetadata)
299
590
  return 'identity-indeterminate';
300
- let pidStatus;
301
- try {
302
- pidStatus = dependencies.probePid(state.pid);
303
- }
304
- catch {
305
- return 'identity-indeterminate';
306
- }
307
- if (pidStatus === 'dead')
308
- return 'stale-dead';
309
- if (pidStatus !== 'alive')
310
- return 'identity-indeterminate';
311
- const platform = state.platform ?? process.platform;
312
- if (platform !== 'linux')
313
- return 'usable';
314
- if (!isValidStartTicks(state.pid_start_ticks))
315
- return 'identity-indeterminate';
316
- let liveIdentity;
317
- try {
318
- liveIdentity = dependencies.readProcessIdentity(state.pid, platform);
319
- }
320
- catch {
591
+ const runtimePlatform = dependencies.runtimePlatform;
592
+ const recorded = recordedIdentityForState(state, runtimePlatform);
593
+ if (recorded === 'invalid')
321
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';
322
603
  }
323
- if (!liveIdentity || !isValidStartTicks(liveIdentity.startTicks))
324
- return 'identity-indeterminate';
325
- if (liveIdentity.startTicks !== state.pid_start_ticks)
326
- return 'stale-dead';
327
- if (liveIdentity.status !== 'matching')
328
- return 'identity-indeterminate';
329
- const expectedCmdlineHash = hashCmdline(state.pid_cmdline);
330
- if (expectedCmdlineHash && liveIdentity.cmdlineHash !== expectedCmdlineHash) {
331
- return 'identity-indeterminate';
332
- }
333
- return 'usable';
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);
334
613
  }
335
614
  function classifyParsedSessionPointer(context, value, raw) {
336
615
  if (!value || typeof value !== 'object' || Array.isArray(value)) {
@@ -342,6 +621,16 @@ function classifyParsedSessionPointer(context, value, raw) {
342
621
  if (typeof state.cwd === 'string' && state.cwd.trim() && !isSessionStateAuthoritativeForCwd(state, context.cwd)) {
343
622
  return { status: 'foreign-cwd', raw, state };
344
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 };
345
634
  const processStatus = classifySessionProcess(state, transactionDependencies);
346
635
  return { status: processStatus, state, raw };
347
636
  }
@@ -380,9 +669,9 @@ export async function readUsableSessionStateFromContext(context) {
380
669
  return state && isSessionStateUsable(state, context.cwd) ? state : null;
381
670
  }
382
671
  /** Read current session state from the exact selected root. */
383
- export async function readSessionState(cwd) {
672
+ export async function readSessionState(cwd, env = process.env) {
384
673
  try {
385
- return await readSessionStateFromContext(resolveSessionPointerContext(cwd));
674
+ return await readSessionStateFromContext(resolveSessionPointerContext(cwd, env));
386
675
  }
387
676
  catch {
388
677
  return null;
@@ -398,7 +687,7 @@ export async function readUsableSessionState(cwd, options = {}) {
398
687
  return null;
399
688
  }
400
689
  }
401
- function createSessionState(cwd, sessionId, pid, platform, linuxIdentity, options = {}) {
690
+ function createSessionState(cwd, stateRoot, sessionId, pid, platform, identity, options = {}) {
402
691
  const nowIso = options.nowIso ?? new Date().toISOString();
403
692
  const nativeSessionId = normalizeNonempty(options.nativeSessionId);
404
693
  const previousNativeSessionId = normalizeNonempty(options.previousNativeSessionId);
@@ -406,6 +695,27 @@ function createSessionState(cwd, sessionId, pid, platform, linuxIdentity, option
406
695
  const ownerOmxSessionId = normalizeSessionId(options.ownerOmxSessionId);
407
696
  const tmuxSessionName = normalizeNonempty(options.tmuxSessionName);
408
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;
409
719
  return {
410
720
  session_id: sessionId,
411
721
  ...(nativeSessionId ? { native_session_id: nativeSessionId } : {}),
@@ -413,15 +723,23 @@ function createSessionState(cwd, sessionId, pid, platform, linuxIdentity, option
413
723
  ...(nativeSessionSwitchedAt ? { native_session_switched_at: nativeSessionSwitchedAt } : {}),
414
724
  ...(ownerOmxSessionId ? { owner_omx_session_id: ownerOmxSessionId } : {}),
415
725
  started_at: options.startedAt ?? nowIso,
726
+ ...(isValidToken(options.launchLineageToken) ? { launch_lineage_token: options.launchLineageToken } : {}),
416
727
  cwd,
728
+ state_root: stateRoot,
417
729
  pid,
418
730
  platform,
419
- ...(linuxIdentity ? { pid_start_ticks: linuxIdentity.startTicks } : {}),
420
- ...(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 } : {}),
421
734
  ...(tmuxSessionName ? { tmux_session_name: tmuxSessionName } : {}),
422
735
  ...(tmuxPaneId ? { tmux_pane_id: tmuxPaneId } : {}),
423
736
  };
424
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
+ }
425
743
  function normalizeNonempty(value) {
426
744
  return typeof value === 'string' && value.trim() ? value.trim() : undefined;
427
745
  }
@@ -429,7 +747,15 @@ function resolvePid(options) {
429
747
  return Number.isInteger(options.pid) && options.pid && options.pid > 0 ? options.pid : process.pid;
430
748
  }
431
749
  function sessionIdentityFor(pid, platform) {
432
- 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
+ }
433
759
  }
434
760
  function currentOwnerAlias(state) {
435
761
  return normalizeSessionId(state.owner_omx_session_id);
@@ -456,29 +782,38 @@ function isStartCompatible(existing, requestedSessionId) {
456
782
  || existing.native_session_id === requestedSessionId
457
783
  || currentOwnerAlias(existing) === requestedSessionId;
458
784
  }
459
- function getOmxLaunchSessionId(state) {
460
- if (state.session_id.startsWith('omx-'))
461
- return state.session_id;
462
- const owner = currentOwnerAlias(state);
463
- return owner?.startsWith('omx-') ? owner : undefined;
464
- }
465
785
  function parseLockOwner(raw) {
466
786
  try {
467
- const value = JSON.parse(raw);
468
- if (!value || typeof value !== 'object' || Array.isArray(value))
787
+ const parsed = JSON.parse(raw);
788
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed))
469
789
  return null;
470
- 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)
471
792
  return null;
472
- 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)
473
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;
474
803
  }
475
- if (value.pid_start_ticks !== undefined && !isValidStartTicks(value.pid_start_ticks))
476
- return null;
477
- if (value.pid_cmdline_hash !== undefined
478
- && (typeof value.pid_cmdline_hash !== 'string' || !SHA256_PATTERN.test(value.pid_cmdline_hash))) {
479
- return null;
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;
480
815
  }
481
- return value;
816
+ return null;
482
817
  }
483
818
  catch {
484
819
  return null;
@@ -487,73 +822,388 @@ function parseLockOwner(raw) {
487
822
  function isValidToken(value) {
488
823
  return typeof value === 'string' && SESSION_POINTER_TOKEN_PATTERN.test(value);
489
824
  }
490
- async function inspectLockOwner(lockPath) {
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
+ }
836
+ async function inspectLockOwnerFile(ownerPath, missingStatus = 'missing') {
491
837
  let raw;
492
838
  try {
493
- raw = await transactionDependencies.fs.readFile(join(lockPath, 'owner.json'), 'utf8');
839
+ raw = await transactionDependencies.fs.readFile(ownerPath, 'utf8');
494
840
  }
495
841
  catch (error) {
496
- return isNotFound(error) ? { status: 'missing' } : { status: 'identity-indeterminate' };
842
+ return isNotFound(error) ? { status: missingStatus } : { status: 'identity-indeterminate' };
497
843
  }
498
844
  const owner = parseLockOwner(raw);
499
845
  if (!owner)
500
846
  return { status: 'malformed' };
501
- let pidStatus;
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 };
859
+ }
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 };
866
+ }
867
+ }
868
+ async function inspectLockOwner(lockPath) {
869
+ return await inspectLockOwnerFile(join(lockPath, 'owner.json'));
870
+ }
871
+ async function inspectSessionPointerLockAtContext(context) {
872
+ const absent = () => ({ status: 'absent', lockPath: context.lockPath, evidenceSource: 'none', safeToRecover: false });
873
+ let lockStat;
874
+ try {
875
+ lockStat = await transactionDependencies.fs.lstat(context.lockPath);
876
+ }
877
+ catch (error) {
878
+ return isNotFound(error) ? absent() : { status: 'io-error', lockPath: context.lockPath, evidenceSource: 'none', safeToRecover: false };
879
+ }
880
+ if (lockStat.isSymbolicLink())
881
+ return { status: 'symlink', lockPath: context.lockPath, evidenceSource: 'none', safeToRecover: false };
882
+ if (!lockStat.isDirectory())
883
+ return { status: 'unexpected', lockPath: context.lockPath, evidenceSource: 'none', safeToRecover: false };
884
+ let entries;
502
885
  try {
503
- pidStatus = transactionDependencies.probePid(owner.pid);
886
+ entries = await transactionDependencies.fs.readdir(context.lockPath);
504
887
  }
505
888
  catch {
506
- return { status: 'identity-indeterminate', owner };
507
- }
508
- if (pidStatus === 'dead')
509
- return { status: 'dead', owner };
510
- if (pidStatus !== 'alive')
511
- return { status: 'identity-indeterminate', owner };
512
- // Without Linux immutable start metadata, a live PID cannot be proved to be
513
- // the same process. Preserve the lock instead of guessing.
514
- if (owner.platform !== 'linux' || !isValidStartTicks(owner.pid_start_ticks)) {
515
- return { status: 'identity-indeterminate', owner };
516
- }
517
- let liveIdentity;
889
+ return { status: 'io-error', lockPath: context.lockPath, evidenceSource: 'none', safeToRecover: false };
890
+ }
891
+ const tempEntries = entries.filter((entry) => /^owner\.([A-Za-z0-9_-]{16,128})\.tmp$/.test(entry));
892
+ const hasCanonical = entries.includes('owner.json');
893
+ if (entries.length === 0)
894
+ return { status: 'missing-owner', lockPath: context.lockPath, evidenceSource: 'none', safeToRecover: false };
895
+ if ((hasCanonical && entries.length !== 1) || (!hasCanonical && tempEntries.length !== 1) || entries.length !== 1) {
896
+ return { status: entries.length > 1 && (hasCanonical || tempEntries.length > 0) ? 'ambiguous' : 'unexpected', lockPath: context.lockPath, evidenceSource: 'none', safeToRecover: false };
897
+ }
898
+ const evidenceName = hasCanonical ? 'owner.json' : tempEntries[0];
899
+ const evidenceSource = hasCanonical ? 'owner.json' : 'owner-temp';
900
+ const evidencePath = join(context.lockPath, evidenceName);
901
+ let evidenceStat;
518
902
  try {
519
- liveIdentity = transactionDependencies.readProcessIdentity(owner.pid, owner.platform);
903
+ evidenceStat = await transactionDependencies.fs.lstat(evidencePath);
520
904
  }
521
905
  catch {
522
- return { status: 'identity-indeterminate', owner };
906
+ return { status: 'io-error', lockPath: context.lockPath, evidenceSource, safeToRecover: false };
907
+ }
908
+ if (evidenceStat.isSymbolicLink())
909
+ return { status: 'symlink', lockPath: context.lockPath, evidenceSource, safeToRecover: false };
910
+ if (!evidenceStat.isFile())
911
+ return { status: 'unexpected', lockPath: context.lockPath, evidenceSource, safeToRecover: false };
912
+ const owner = await inspectLockOwnerFile(evidencePath, 'missing');
913
+ if (evidenceSource === 'owner-temp' && owner.owner?.token !== /^owner\.([A-Za-z0-9_-]{16,128})\.tmp$/.exec(evidenceName)?.[1]) {
914
+ return { status: 'malformed', lockPath: context.lockPath, evidenceSource, safeToRecover: false };
915
+ }
916
+ const status = owner.status === 'missing' ? 'io-error' : owner.status;
917
+ return {
918
+ status,
919
+ lockPath: context.lockPath,
920
+ evidenceSource,
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',
928
+ };
929
+ }
930
+ /** Inspect only exact, regular owner evidence in the selected pointer lock. */
931
+ export async function inspectSessionPointerLock(cwd) {
932
+ return await inspectSessionPointerLockAtContext(resolveSessionPointerContext(cwd));
933
+ }
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) {
938
+ try {
939
+ return await transactionDependencies.fs.lstat(path);
940
+ }
941
+ catch (error) {
942
+ if (isNotFound(error))
943
+ return undefined;
944
+ throw error;
945
+ }
946
+ }
947
+ async function probeRecoveryRenameCapability() {
948
+ let probeDir;
949
+ try {
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';
958
+ }
959
+ catch {
960
+ return 'unsupported';
961
+ }
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) {
972
+ try {
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.' };
978
+ }
979
+ catch (error) {
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 */ }
994
+ }
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.' };
1002
+ }
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);
1011
+ try {
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 };
523
1088
  }
524
- if (!liveIdentity || !isValidStartTicks(liveIdentity.startTicks)) {
525
- return { status: 'identity-indeterminate', owner };
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}).` };
526
1092
  }
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 };
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;
1099
+ try {
1100
+ checkpointNames = (await transactionDependencies.fs.readdir(dirname(context.lockPath))).filter((name) => name.startsWith(checkpointPrefix) && name.endsWith('.json'));
1101
+ }
1102
+ catch (error) {
1103
+ if (!isNotFound(error))
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 = [];
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}).` };
1114
+ try {
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') {
1137
+ return {
1138
+ ...inspection,
1139
+ action: 'none',
1140
+ recovered: false,
1141
+ reason: 'Atomic no-replace recovery rename is unsupported on this platform.',
1142
+ };
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
+ };
533
1175
  }
534
- return { status: 'live', owner };
535
1176
  }
536
1177
  function buildLockOwner(token) {
537
- let identity;
1178
+ const runtimePlatform = transactionDependencies.runtimePlatform;
1179
+ let observation;
538
1180
  try {
539
- identity = transactionDependencies.readProcessIdentity(process.pid, process.platform);
1181
+ observation = transactionDependencies.observeProcess(process.pid, runtimePlatform);
540
1182
  }
541
1183
  catch {
542
1184
  // Publication remains valid with optional identity metadata omitted.
543
1185
  }
544
- return {
545
- 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 = {
546
1197
  token,
547
1198
  pid: process.pid,
548
- platform: process.platform,
549
- ...(identity?.status === 'matching' && isValidStartTicks(identity.startTicks)
550
- ? { pid_start_ticks: identity.startTicks }
551
- : {}),
552
- ...(identity?.status === 'matching' && identity.cmdlineHash && SHA256_PATTERN.test(identity.cmdlineHash)
553
- ? { pid_cmdline_hash: identity.cmdlineHash }
554
- : {}),
1199
+ platform: runtimePlatform,
1200
+ ...(legacyStartTicks !== undefined ? { pid_start_ticks: legacyStartTicks } : {}),
1201
+ ...(legacyHash ? { pid_cmdline_hash: legacyHash } : {}),
555
1202
  created_at: new Date(transactionDependencies.nowMs()).toISOString(),
556
1203
  };
1204
+ return identity
1205
+ ? { version: 2, ...common, process_identity: identity }
1206
+ : { version: 1, ...common };
557
1207
  }
558
1208
  async function removeOwnedPath(path) {
559
1209
  try {
@@ -639,7 +1289,7 @@ async function rollbackUnpublishedLock(context, ownerTempPath) {
639
1289
  }
640
1290
  return failures;
641
1291
  }
642
- async function acquirePointerLock(context, candidateSessionId, timeoutMs) {
1292
+ async function acquirePointerLock(context, candidateSessionId, timeoutMs, platform, tracker, regularFileSync) {
643
1293
  const deadline = transactionDependencies.nowMs() + timeoutMs;
644
1294
  let attempt = 0;
645
1295
  while (true) {
@@ -734,7 +1384,7 @@ async function acquirePointerLock(context, candidateSessionId, timeoutMs) {
734
1384
  const ownerPath = join(context.lockPath, 'owner.json');
735
1385
  try {
736
1386
  await transactionDependencies.fs.writeFile(ownerTempPath, JSON.stringify(buildLockOwner(token)), { mode: 0o600 });
737
- await transactionDependencies.fs.openAndSync(ownerTempPath);
1387
+ recordRegularFileSyncOutcome(tracker, await transactionDependencies.fs.openAndSync(ownerTempPath, platform, regularFileSync));
738
1388
  await transactionDependencies.fs.rename(ownerTempPath, ownerPath);
739
1389
  }
740
1390
  catch (error) {
@@ -792,7 +1442,7 @@ async function releasePointerLock(lock) {
792
1442
  if (!isNotFound(error)) {
793
1443
  return [{
794
1444
  operation: 'lock-release',
795
- phase: 'remove-release-dir',
1445
+ phase: 'remove-release-owner',
796
1446
  ownership: 'released',
797
1447
  message: `Unable to remove released session pointer lock owner: ${errorMessage(error)}`,
798
1448
  cause: error,
@@ -815,6 +1465,10 @@ async function releasePointerLock(lock) {
815
1465
  }];
816
1466
  }
817
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
+ }
818
1472
  function recoveryAbort(context, primary, failures, operation) {
819
1473
  return resolvedAbort(context, {
820
1474
  code: 'session_pointer_lock_recovery_required',
@@ -829,9 +1483,12 @@ function recoveryAbort(context, primary, failures, operation) {
829
1483
  secondaryFailures: failures,
830
1484
  });
831
1485
  }
832
- function releaseFailureError(failures) {
833
- const error = new Error('Session pointer committed, but lock release left recovery evidence.');
834
- 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;
835
1492
  return error;
836
1493
  }
837
1494
  async function finalizePrecommitAbort(lock, primary, pointerTempPath) {
@@ -873,7 +1530,7 @@ function ownerConflictAbort(context, candidateSessionId, state, cause) {
873
1530
  ...(cause ? { cause } : {}),
874
1531
  });
875
1532
  }
876
- async function writePointerTransaction(cwd, candidateSessionId, options, timeoutMs, transition, pointerState) {
1533
+ async function writePointerTransaction(cwd, candidateSessionId, options, timeoutMs, transition, pointerState, beforeCommit, afterCommit) {
877
1534
  let context;
878
1535
  try {
879
1536
  context = options.context ?? resolveSessionPointerContext(cwd);
@@ -904,9 +1561,12 @@ async function writePointerTransaction(cwd, candidateSessionId, options, timeout
904
1561
  cause: error,
905
1562
  });
906
1563
  }
907
- const lock = await acquirePointerLock(context, candidateSessionId, timeoutMs);
1564
+ const platform = options.platform ?? process.platform;
1565
+ const tracker = { degraded: false };
1566
+ const lock = await acquirePointerLock(context, candidateSessionId, timeoutMs, platform, tracker, options.regularFileSync);
908
1567
  const pointerTempPath = `${context.sessionPath}.tmp-${lock.token}`;
909
1568
  let pointerCommitted = false;
1569
+ let pointerTempWritten = false;
910
1570
  try {
911
1571
  let pointer;
912
1572
  try {
@@ -924,7 +1584,7 @@ async function writePointerTransaction(cwd, candidateSessionId, options, timeout
924
1584
  }
925
1585
  let next;
926
1586
  try {
927
- next = transition(pointer, context);
1587
+ next = await transition(pointer, context);
928
1588
  }
929
1589
  catch (error) {
930
1590
  if (isSessionPointerLaunchAbort(error))
@@ -939,6 +1599,7 @@ async function writePointerTransaction(cwd, candidateSessionId, options, timeout
939
1599
  }
940
1600
  const serialized = JSON.stringify(pointerState(next), null, 2);
941
1601
  try {
1602
+ pointerTempWritten = true;
942
1603
  await transactionDependencies.fs.writeFile(pointerTempPath, serialized, { mode: 0o600, flag: 'wx' });
943
1604
  }
944
1605
  catch (error) {
@@ -952,7 +1613,7 @@ async function writePointerTransaction(cwd, candidateSessionId, options, timeout
952
1613
  });
953
1614
  }
954
1615
  try {
955
- await transactionDependencies.fs.openAndSync(pointerTempPath);
1616
+ recordRegularFileSyncOutcome(tracker, await transactionDependencies.fs.openAndSync(pointerTempPath, platform, options.regularFileSync));
956
1617
  }
957
1618
  catch (error) {
958
1619
  throw resolvedAbort(context, {
@@ -964,11 +1625,31 @@ async function writePointerTransaction(cwd, candidateSessionId, options, timeout
964
1625
  cause: error,
965
1626
  });
966
1627
  }
1628
+ if (beforeCommit)
1629
+ await beforeCommit(context);
967
1630
  try {
968
1631
  await transactionDependencies.fs.rename(pointerTempPath, context.sessionPath);
969
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
+ }
970
1649
  }
971
1650
  catch (error) {
1651
+ if (error instanceof CommittedLaunchBlockedError)
1652
+ throw error;
972
1653
  throw resolvedAbort(context, {
973
1654
  code: 'session_pointer_io_failure',
974
1655
  operation: 'pointer-rename',
@@ -980,7 +1661,8 @@ async function writePointerTransaction(cwd, candidateSessionId, options, timeout
980
1661
  }
981
1662
  const releaseFailures = await releasePointerLock(lock);
982
1663
  if (releaseFailures.length > 0)
983
- throw releaseFailureError(releaseFailures);
1664
+ throw releaseFailureError(releaseFailures, context, next);
1665
+ emitDegradedDurabilityWarning('session pointer start/reconcile', tracker);
984
1666
  return { context, value: next };
985
1667
  }
986
1668
  catch (error) {
@@ -996,14 +1678,18 @@ async function writePointerTransaction(cwd, candidateSessionId, options, timeout
996
1678
  reason: `Session pointer transition failed before commit: ${errorMessage(error)}`,
997
1679
  cause: error,
998
1680
  });
999
- const ownsPointerTemp = primary.operation === 'pointer-temp-write'
1000
- || primary.operation === 'pointer-fsync'
1001
- || primary.operation === 'pointer-rename';
1681
+ const ownsPointerTemp = pointerTempWritten;
1002
1682
  throw await finalizePrecommitAbort(lock, primary, ownsPointerTemp ? pointerTempPath : undefined);
1003
1683
  }
1004
1684
  }
1005
- function startPointerTransition(requestedSessionId, options) {
1685
+ function startPointerTransition(requestedSessionId, options, requireLaunchLineageToken = false, requireAbsent = false) {
1006
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
+ }
1007
1693
  if (pointer.status !== 'absent' && pointer.status !== 'stale-dead' && pointer.status !== 'usable') {
1008
1694
  throw unusablePointerAbort(context, requestedSessionId, pointer);
1009
1695
  }
@@ -1011,6 +1697,9 @@ function startPointerTransition(requestedSessionId, options) {
1011
1697
  if (existing && !isStartCompatible(existing, requestedSessionId)) {
1012
1698
  throw ownerConflictAbort(context, requestedSessionId, existing);
1013
1699
  }
1700
+ if (requireLaunchLineageToken && existing && !isValidToken(existing.launch_lineage_token)) {
1701
+ throw ownerConflictAbort(context, requestedSessionId, existing);
1702
+ }
1014
1703
  const canonicalSessionId = existing?.session_id ?? requestedSessionId;
1015
1704
  const pid = resolvePid(options);
1016
1705
  const platform = options.platform ?? process.platform;
@@ -1027,20 +1716,51 @@ function startPointerTransition(requestedSessionId, options) {
1027
1716
  pid,
1028
1717
  }, error);
1029
1718
  }
1030
- return createSessionState(context.cwd, canonicalSessionId, pid, platform, sessionIdentityFor(pid, platform), {
1719
+ const state = createSessionState(context.cwd, context.baseStateDir, canonicalSessionId, pid, platform, sessionIdentityFor(pid, platform), {
1031
1720
  nativeSessionId: options.nativeSessionId ?? existing?.native_session_id,
1032
1721
  previousNativeSessionId: options.previousNativeSessionId ?? existing?.previous_native_session_id,
1033
1722
  nativeSessionSwitchedAt: options.nativeSessionSwitchedAt ?? existing?.native_session_switched_at,
1034
1723
  ...(ownerOmxSessionId ? { ownerOmxSessionId } : {}),
1724
+ launchLineageToken: existing
1725
+ ? (isValidToken(existing.launch_lineage_token) ? existing.launch_lineage_token : undefined)
1726
+ : transactionDependencies.token(),
1035
1727
  tmuxSessionName: options.tmuxSessionName ?? existing?.tmux_session_name,
1036
1728
  tmuxPaneId: options.tmuxPaneId ?? existing?.tmux_pane_id,
1037
1729
  });
1730
+ return preserveExistingLaunchLineageToken(existing, state);
1038
1731
  };
1039
1732
  }
1040
- /** 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. */
1041
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) {
1042
1762
  const requestedSessionId = normalizeSessionId(sessionId);
1043
- 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);
1044
1764
  await appendToLogAtContext(result.context, {
1045
1765
  event: 'session_start',
1046
1766
  session_id: result.value.session_id,
@@ -1050,6 +1770,551 @@ export async function writeSessionStart(cwd, sessionId, options = {}) {
1050
1770
  }).catch(() => { });
1051
1771
  return result.value;
1052
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
+ }
1053
2318
  function reconcileNativeTransition(nativeSessionId, options) {
1054
2319
  return (pointer, context) => {
1055
2320
  if (pointer.status !== 'absent' && pointer.status !== 'stale-dead' && pointer.status !== 'usable') {
@@ -1059,42 +2324,19 @@ function reconcileNativeTransition(nativeSessionId, options) {
1059
2324
  const platform = options.platform ?? process.platform;
1060
2325
  const linuxIdentity = sessionIdentityFor(pid, platform);
1061
2326
  const existing = pointer.status === 'usable' ? pointer.state : undefined;
1062
- const nowIso = new Date().toISOString();
1063
2327
  if (!existing) {
1064
2328
  const ownerCandidate = verifiedOwnerCandidate(context, options);
1065
2329
  const ownerOmxSessionId = ownerCandidate;
1066
- return {
1067
- state: createSessionState(context.cwd, nativeSessionId, pid, platform, linuxIdentity, {
1068
- nativeSessionId,
1069
- ...(ownerOmxSessionId ? { ownerOmxSessionId } : {}),
1070
- }),
1071
- };
2330
+ return createSessionState(context.cwd, context.baseStateDir, nativeSessionId, pid, platform, linuxIdentity, {
2331
+ nativeSessionId,
2332
+ ...(ownerOmxSessionId ? { ownerOmxSessionId } : {}),
2333
+ });
1072
2334
  }
1073
2335
  const existingNativeSessionId = normalizeSessionId(existing.native_session_id);
1074
2336
  if (existingNativeSessionId && existingNativeSessionId !== nativeSessionId) {
1075
- const ownerOmxSessionId = getOmxLaunchSessionId(existing);
1076
- return {
1077
- state: createSessionState(context.cwd, nativeSessionId, pid, platform, linuxIdentity, {
1078
- nativeSessionId,
1079
- ...(ownerOmxSessionId ? {
1080
- previousNativeSessionId: existingNativeSessionId,
1081
- nativeSessionSwitchedAt: nowIso,
1082
- ownerOmxSessionId,
1083
- } : {}),
1084
- }),
1085
- ...(ownerOmxSessionId ? {
1086
- replacementLog: {
1087
- event: 'native_session_replaced',
1088
- session_id: ownerOmxSessionId,
1089
- ...(existing.session_id !== ownerOmxSessionId ? { active_session_id: existing.session_id } : {}),
1090
- previous_native_session_id: existingNativeSessionId,
1091
- replaced_by_native_session_id: nativeSessionId,
1092
- pid,
1093
- timestamp: nowIso,
1094
- },
1095
- } : {}),
1096
- };
2337
+ throw ownerConflictAbort(context, nativeSessionId, existing);
1097
2338
  }
2339
+ const nowIso = new Date().toISOString();
1098
2340
  const ownerCandidate = verifiedOwnerCandidate(context, options);
1099
2341
  let ownerOmxSessionId;
1100
2342
  try {
@@ -1103,39 +2345,37 @@ function reconcileNativeTransition(nativeSessionId, options) {
1103
2345
  catch (error) {
1104
2346
  throw ownerConflictAbort(context, nativeSessionId, existing, error);
1105
2347
  }
1106
- return {
1107
- state: createSessionState(context.cwd, existing.session_id, pid, platform, linuxIdentity, {
1108
- nowIso,
1109
- nativeSessionId,
1110
- previousNativeSessionId: existing.previous_native_session_id,
1111
- nativeSessionSwitchedAt: existing.native_session_switched_at,
1112
- ...(ownerOmxSessionId ? { ownerOmxSessionId } : {}),
1113
- startedAt: existing.started_at,
1114
- tmuxSessionName: existing.tmux_session_name,
1115
- tmuxPaneId: existing.tmux_pane_id,
1116
- }),
1117
- };
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
+ }));
1118
2361
  };
1119
2362
  }
1120
2363
  /**
1121
- * Reconcile a native SessionStart without borrowing another root's pointer.
1122
- * A different native ID retains the existing OMX owner chain, but never binds a
1123
- * new owner alias during that replacement transition.
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.
1124
2367
  */
1125
2368
  export async function reconcileNativeSessionStart(cwd, nativeSessionId, options = {}) {
1126
2369
  const normalizedNativeSessionId = normalizeSessionId(nativeSessionId);
1127
- const result = await writePointerTransaction(cwd, normalizedNativeSessionId, options, NATIVE_POINTER_TIMEOUT_MS, reconcileNativeTransition(normalizedNativeSessionId ?? nativeSessionId, options), (transition) => transition.state);
1128
- if (result.value.replacementLog) {
1129
- await appendToLogAtContext(result.context, result.value.replacementLog).catch(() => { });
1130
- }
2370
+ const result = await writePointerTransaction(cwd, normalizedNativeSessionId, options, NATIVE_POINTER_TIMEOUT_MS, reconcileNativeTransition(normalizedNativeSessionId ?? nativeSessionId, options), (state) => state);
1131
2371
  await appendToLogAtContext(result.context, {
1132
- event: result.value.replacementLog ? 'session_start' : 'session_start_reconciled',
1133
- session_id: result.value.state.session_id,
2372
+ event: 'session_start_reconciled',
2373
+ session_id: result.value.session_id,
1134
2374
  native_session_id: normalizedNativeSessionId ?? nativeSessionId,
1135
- pid: result.value.state.pid,
1136
- timestamp: result.value.state.native_session_switched_at ?? new Date().toISOString(),
2375
+ pid: result.value.pid,
2376
+ timestamp: new Date().toISOString(),
1137
2377
  }).catch(() => { });
1138
- return result.value.state;
2378
+ return result.value;
1139
2379
  }
1140
2380
  function historyDirectory(context) {
1141
2381
  return join(dirname(context.baseStateDir), 'logs');
@@ -1181,25 +2421,45 @@ export async function writeSessionEnd(cwd, sessionId, options = {}) {
1181
2421
  cause,
1182
2422
  });
1183
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');
1184
2432
  try {
1185
- await transactionDependencies.fs.mkdir(context.baseStateDir, { recursive: true });
2433
+ preLockPointer = await readSessionPointer(context);
2434
+ traceSessionFinalizationOperation(`preLock-read-done:${preLockPointer.status}`);
1186
2435
  }
1187
2436
  catch (error) {
1188
2437
  throw resolvedAbort(context, {
1189
- code: 'session_pointer_io_failure',
1190
- operation: 'state-dir-create',
1191
- candidateSessionId,
1192
- lockPath: context.lockPath,
1193
- reason: `Unable to create selected session state directory: ${errorMessage(error)}`,
1194
- cause: error,
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,
1195
2440
  });
1196
2441
  }
1197
- const lock = await acquirePointerLock(context, candidateSessionId, DEFAULT_POINTER_TIMEOUT_MS);
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');
2448
+ const platform = options.platform ?? process.platform;
2449
+ const tracker = { degraded: false };
2450
+ traceSessionFinalizationOperation('acquirePointerLock-start');
2451
+ const lock = await acquirePointerLock(context, candidateSessionId, DEFAULT_POINTER_TIMEOUT_MS, platform, tracker, options.regularFileSync);
2452
+ traceSessionFinalizationOperation('acquirePointerLock-done');
1198
2453
  let primary;
2454
+ let revalidation = {
2455
+ comparison: { status: 'not-run' }, capability: [],
2456
+ };
1199
2457
  try {
1200
2458
  let pointer;
2459
+ traceSessionFinalizationOperation('under-lock-read-start');
1201
2460
  try {
1202
2461
  pointer = await readSessionPointer(context);
2462
+ traceSessionFinalizationOperation(`under-lock-read-done:${pointer.status}`);
1203
2463
  }
1204
2464
  catch (error) {
1205
2465
  throw resolvedAbort(context, {
@@ -1211,14 +2471,17 @@ export async function writeSessionEnd(cwd, sessionId, options = {}) {
1211
2471
  cause: error,
1212
2472
  });
1213
2473
  }
1214
- if (pointer.status !== 'absent' && pointer.status !== 'usable') {
2474
+ const authorized = isAuthorizedBoundPointerForFinalization(options.binding, context, candidateSessionId, pointer, 'under-lock');
2475
+ if (!authorized)
1215
2476
  throw unusablePointerAbort(context, candidateSessionId, pointer);
1216
- }
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');
1217
2483
  const state = pointer.state;
1218
- const ownsCurrentSessionFile = state == null
1219
- || state.session_id === candidateSessionId
1220
- || state.native_session_id === candidateSessionId
1221
- || state.owner_omx_session_id === candidateSessionId;
2484
+ const ownsCurrentSessionFile = true;
1222
2485
  const endTime = new Date().toISOString();
1223
2486
  const historyEntry = {
1224
2487
  session_id: ownsCurrentSessionFile
@@ -1234,12 +2497,19 @@ export async function writeSessionEnd(cwd, sessionId, options = {}) {
1234
2497
  : {}),
1235
2498
  ...(!ownsCurrentSessionFile && state?.session_id ? { preserved_active_session_id: state.session_id } : {}),
1236
2499
  };
2500
+ traceSessionFinalizationOperation('history-mkdir-start');
1237
2501
  await nodeMkdir(historyDirectory(context), { recursive: true });
2502
+ traceSessionFinalizationOperation('history-mkdir-done');
2503
+ traceSessionFinalizationOperation('history-append-start');
1238
2504
  await appendFile(historyPath(context), `${JSON.stringify(historyEntry)}\n`);
2505
+ traceSessionFinalizationOperation('history-append-done');
2506
+ traceSessionFinalizationOperation('hud-cleanup-start');
1239
2507
  await removeDeadSessionHudState(context, [
1240
2508
  ...(ownsCurrentSessionFile ? [state?.session_id, state?.native_session_id] : []),
1241
2509
  candidateSessionId,
1242
2510
  ]);
2511
+ traceSessionFinalizationOperation('hud-cleanup-done');
2512
+ traceSessionFinalizationOperation('session-unlink-start');
1243
2513
  if (ownsCurrentSessionFile) {
1244
2514
  try {
1245
2515
  await transactionDependencies.fs.unlink(context.sessionPath);
@@ -1249,6 +2519,8 @@ export async function writeSessionEnd(cwd, sessionId, options = {}) {
1249
2519
  throw error;
1250
2520
  }
1251
2521
  }
2522
+ traceSessionFinalizationOperation('session-unlink-done');
2523
+ traceSessionFinalizationOperation('session-end-log-start');
1252
2524
  await appendToLogAtContext(context, {
1253
2525
  event: 'session_end',
1254
2526
  session_id: historyEntry.session_id,
@@ -1257,11 +2529,14 @@ export async function writeSessionEnd(cwd, sessionId, options = {}) {
1257
2529
  ...(historyEntry.preserved_active_session_id ? { preserved_active_session_id: historyEntry.preserved_active_session_id } : {}),
1258
2530
  timestamp: endTime,
1259
2531
  }).catch(() => { });
2532
+ traceSessionFinalizationOperation('session-end-log-done');
1260
2533
  }
1261
2534
  catch (error) {
1262
2535
  primary = error;
1263
2536
  }
2537
+ traceSessionFinalizationOperation('lock-release-start');
1264
2538
  const releaseFailures = await releasePointerLock(lock);
2539
+ traceSessionFinalizationOperation('lock-release-done');
1265
2540
  if (primary) {
1266
2541
  if (isSessionPointerLaunchAbort(primary) && releaseFailures.length > 0) {
1267
2542
  throw recoveryAbort(context, primary, releaseFailures, 'lock-release');
@@ -1270,6 +2545,8 @@ export async function writeSessionEnd(cwd, sessionId, options = {}) {
1270
2545
  }
1271
2546
  if (releaseFailures.length > 0)
1272
2547
  throw releaseFailureError(releaseFailures);
2548
+ emitDegradedDurabilityWarning('session pointer end', tracker);
2549
+ return revalidation;
1273
2550
  }
1274
2551
  /** Reset session-scoped HUD/metrics files at launch. */
1275
2552
  export async function resetSessionMetrics(cwd, sessionId) {