oh-my-codex 0.21.0 → 0.21.1

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 (311) hide show
  1. package/Cargo.toml +1 -1
  2. package/README.md +2 -2
  3. package/dist/cli/__tests__/detached-launch-diagnostics-3578.test.d.ts +2 -0
  4. package/dist/cli/__tests__/detached-launch-diagnostics-3578.test.d.ts.map +1 -0
  5. package/dist/cli/__tests__/detached-launch-diagnostics-3578.test.js +592 -0
  6. package/dist/cli/__tests__/detached-launch-diagnostics-3578.test.js.map +1 -0
  7. package/dist/cli/__tests__/detached-leader-teardown.test.js +270 -33
  8. package/dist/cli/__tests__/detached-leader-teardown.test.js.map +1 -1
  9. package/dist/cli/__tests__/doctor-warning-copy.test.js +133 -5
  10. package/dist/cli/__tests__/doctor-warning-copy.test.js.map +1 -1
  11. package/dist/cli/__tests__/index.test.js +543 -2
  12. package/dist/cli/__tests__/index.test.js.map +1 -1
  13. package/dist/cli/__tests__/issue-windows-cancel-identity.test.d.ts +2 -0
  14. package/dist/cli/__tests__/issue-windows-cancel-identity.test.d.ts.map +1 -0
  15. package/dist/cli/__tests__/issue-windows-cancel-identity.test.js +17 -0
  16. package/dist/cli/__tests__/issue-windows-cancel-identity.test.js.map +1 -0
  17. package/dist/cli/__tests__/launch-fallback.test.js +15 -3
  18. package/dist/cli/__tests__/launch-fallback.test.js.map +1 -1
  19. package/dist/cli/__tests__/package-bin-contract.test.js +20 -0
  20. package/dist/cli/__tests__/package-bin-contract.test.js.map +1 -1
  21. package/dist/cli/__tests__/plugin-cache-stale-lock.test.d.ts +2 -0
  22. package/dist/cli/__tests__/plugin-cache-stale-lock.test.d.ts.map +1 -0
  23. package/dist/cli/__tests__/plugin-cache-stale-lock.test.js +76 -0
  24. package/dist/cli/__tests__/plugin-cache-stale-lock.test.js.map +1 -0
  25. package/dist/cli/__tests__/plugin-cache-symlink-provenance-3552.test.d.ts +2 -0
  26. package/dist/cli/__tests__/plugin-cache-symlink-provenance-3552.test.d.ts.map +1 -0
  27. package/dist/cli/__tests__/plugin-cache-symlink-provenance-3552.test.js +1662 -0
  28. package/dist/cli/__tests__/plugin-cache-symlink-provenance-3552.test.js.map +1 -0
  29. package/dist/cli/__tests__/plugin-launcher-provenance-3558.test.d.ts +2 -0
  30. package/dist/cli/__tests__/plugin-launcher-provenance-3558.test.d.ts.map +1 -0
  31. package/dist/cli/__tests__/plugin-launcher-provenance-3558.test.js +522 -0
  32. package/dist/cli/__tests__/plugin-launcher-provenance-3558.test.js.map +1 -0
  33. package/dist/cli/__tests__/ralplan-advisory.test.d.ts +2 -0
  34. package/dist/cli/__tests__/ralplan-advisory.test.d.ts.map +1 -0
  35. package/dist/cli/__tests__/ralplan-advisory.test.js +301 -0
  36. package/dist/cli/__tests__/ralplan-advisory.test.js.map +1 -0
  37. package/dist/cli/__tests__/ralplan.test.js +39 -0
  38. package/dist/cli/__tests__/ralplan.test.js.map +1 -1
  39. package/dist/cli/__tests__/resume.test.js +81 -6
  40. package/dist/cli/__tests__/resume.test.js.map +1 -1
  41. package/dist/cli/__tests__/session-scoped-runtime.test.js +423 -4
  42. package/dist/cli/__tests__/session-scoped-runtime.test.js.map +1 -1
  43. package/dist/cli/__tests__/setup-install-mode.test.js +60 -3
  44. package/dist/cli/__tests__/setup-install-mode.test.js.map +1 -1
  45. package/dist/cli/__tests__/setup-refresh.test.js +9 -0
  46. package/dist/cli/__tests__/setup-refresh.test.js.map +1 -1
  47. package/dist/cli/__tests__/sparkshell-cli.test.js +4 -0
  48. package/dist/cli/__tests__/sparkshell-cli.test.js.map +1 -1
  49. package/dist/cli/__tests__/ultragoal.test.js +71 -0
  50. package/dist/cli/__tests__/ultragoal.test.js.map +1 -1
  51. package/dist/cli/__tests__/windows-popup-loop-contract.test.js +30 -4
  52. package/dist/cli/__tests__/windows-popup-loop-contract.test.js.map +1 -1
  53. package/dist/cli/doctor.d.ts.map +1 -1
  54. package/dist/cli/doctor.js +57 -8
  55. package/dist/cli/doctor.js.map +1 -1
  56. package/dist/cli/index.d.ts +113 -7
  57. package/dist/cli/index.d.ts.map +1 -1
  58. package/dist/cli/index.js +1850 -185
  59. package/dist/cli/index.js.map +1 -1
  60. package/dist/cli/plugin-marketplace.d.ts +89 -4
  61. package/dist/cli/plugin-marketplace.d.ts.map +1 -1
  62. package/dist/cli/plugin-marketplace.js +2135 -118
  63. package/dist/cli/plugin-marketplace.js.map +1 -1
  64. package/dist/cli/ralplan.d.ts +2 -1
  65. package/dist/cli/ralplan.d.ts.map +1 -1
  66. package/dist/cli/ralplan.js +149 -9
  67. package/dist/cli/ralplan.js.map +1 -1
  68. package/dist/cli/setup.d.ts.map +1 -1
  69. package/dist/cli/setup.js +7 -1
  70. package/dist/cli/setup.js.map +1 -1
  71. package/dist/cli/sparkshell.d.ts.map +1 -1
  72. package/dist/cli/sparkshell.js +9 -10
  73. package/dist/cli/sparkshell.js.map +1 -1
  74. package/dist/config/__tests__/generator-idempotent.test.js +118 -0
  75. package/dist/config/__tests__/generator-idempotent.test.js.map +1 -1
  76. package/dist/config/generator.d.ts.map +1 -1
  77. package/dist/config/generator.js +74 -4
  78. package/dist/config/generator.js.map +1 -1
  79. package/dist/hooks/__tests__/keyword-detector.test.js +74 -0
  80. package/dist/hooks/__tests__/keyword-detector.test.js.map +1 -1
  81. package/dist/hooks/__tests__/ralplan-advisory-keyword.test.d.ts +2 -0
  82. package/dist/hooks/__tests__/ralplan-advisory-keyword.test.d.ts.map +1 -0
  83. package/dist/hooks/__tests__/ralplan-advisory-keyword.test.js +250 -0
  84. package/dist/hooks/__tests__/ralplan-advisory-keyword.test.js.map +1 -0
  85. package/dist/hooks/__tests__/session.test.js +10 -1
  86. package/dist/hooks/__tests__/session.test.js.map +1 -1
  87. package/dist/hooks/keyword-detector.d.ts +8 -1
  88. package/dist/hooks/keyword-detector.d.ts.map +1 -1
  89. package/dist/hooks/keyword-detector.js +130 -9
  90. package/dist/hooks/keyword-detector.js.map +1 -1
  91. package/dist/hooks/session.d.ts +5 -0
  92. package/dist/hooks/session.d.ts.map +1 -1
  93. package/dist/hooks/session.js +17 -3
  94. package/dist/hooks/session.js.map +1 -1
  95. package/dist/hud/__tests__/index.test.js +16 -1
  96. package/dist/hud/__tests__/index.test.js.map +1 -1
  97. package/dist/hud/__tests__/reconcile.test.js +134 -7
  98. package/dist/hud/__tests__/reconcile.test.js.map +1 -1
  99. package/dist/hud/__tests__/session-attached.test.d.ts +2 -0
  100. package/dist/hud/__tests__/session-attached.test.d.ts.map +1 -0
  101. package/dist/hud/__tests__/session-attached.test.js +82 -0
  102. package/dist/hud/__tests__/session-attached.test.js.map +1 -0
  103. package/dist/hud/__tests__/tmux-split-realtmux.test.js +169 -2
  104. package/dist/hud/__tests__/tmux-split-realtmux.test.js.map +1 -1
  105. package/dist/hud/__tests__/tmux.test.js +104 -22
  106. package/dist/hud/__tests__/tmux.test.js.map +1 -1
  107. package/dist/hud/__tests__/watch-detached.test.d.ts +2 -0
  108. package/dist/hud/__tests__/watch-detached.test.d.ts.map +1 -0
  109. package/dist/hud/__tests__/watch-detached.test.js +383 -0
  110. package/dist/hud/__tests__/watch-detached.test.js.map +1 -0
  111. package/dist/hud/__tests__/watch-realtmux.test.d.ts +2 -0
  112. package/dist/hud/__tests__/watch-realtmux.test.d.ts.map +1 -0
  113. package/dist/hud/__tests__/watch-realtmux.test.js +322 -0
  114. package/dist/hud/__tests__/watch-realtmux.test.js.map +1 -0
  115. package/dist/hud/index.d.ts +3 -0
  116. package/dist/hud/index.d.ts.map +1 -1
  117. package/dist/hud/index.js +98 -20
  118. package/dist/hud/index.js.map +1 -1
  119. package/dist/hud/reconcile.d.ts.map +1 -1
  120. package/dist/hud/reconcile.js +68 -24
  121. package/dist/hud/reconcile.js.map +1 -1
  122. package/dist/hud/session-attached.d.ts +15 -0
  123. package/dist/hud/session-attached.d.ts.map +1 -0
  124. package/dist/hud/session-attached.js +47 -0
  125. package/dist/hud/session-attached.js.map +1 -0
  126. package/dist/hud/tmux.d.ts +4 -0
  127. package/dist/hud/tmux.d.ts.map +1 -1
  128. package/dist/hud/tmux.js +86 -23
  129. package/dist/hud/tmux.js.map +1 -1
  130. package/dist/modes/base.d.ts +15 -2
  131. package/dist/modes/base.d.ts.map +1 -1
  132. package/dist/modes/base.js +105 -16
  133. package/dist/modes/base.js.map +1 -1
  134. package/dist/question/autopilot-wait.js +2 -2
  135. package/dist/question/autopilot-wait.js.map +1 -1
  136. package/dist/question/deep-interview.js +2 -2
  137. package/dist/question/deep-interview.js.map +1 -1
  138. package/dist/ralplan/__tests__/advisory-activation.test.d.ts +2 -0
  139. package/dist/ralplan/__tests__/advisory-activation.test.d.ts.map +1 -0
  140. package/dist/ralplan/__tests__/advisory-activation.test.js +700 -0
  141. package/dist/ralplan/__tests__/advisory-activation.test.js.map +1 -0
  142. package/dist/ralplan/__tests__/advisory-detached-transition.test.d.ts +2 -0
  143. package/dist/ralplan/__tests__/advisory-detached-transition.test.d.ts.map +1 -0
  144. package/dist/ralplan/__tests__/advisory-detached-transition.test.js +88 -0
  145. package/dist/ralplan/__tests__/advisory-detached-transition.test.js.map +1 -0
  146. package/dist/ralplan/__tests__/advisory.test.d.ts +2 -0
  147. package/dist/ralplan/__tests__/advisory.test.d.ts.map +1 -0
  148. package/dist/ralplan/__tests__/advisory.test.js +1087 -0
  149. package/dist/ralplan/__tests__/advisory.test.js.map +1 -0
  150. package/dist/ralplan/__tests__/runtime.test.js +416 -7
  151. package/dist/ralplan/__tests__/runtime.test.js.map +1 -1
  152. package/dist/ralplan/advisory-activation-verifier.d.ts +19 -0
  153. package/dist/ralplan/advisory-activation-verifier.d.ts.map +1 -0
  154. package/dist/ralplan/advisory-activation-verifier.js +77 -0
  155. package/dist/ralplan/advisory-activation-verifier.js.map +1 -0
  156. package/dist/ralplan/advisory-activation.d.ts +34 -0
  157. package/dist/ralplan/advisory-activation.d.ts.map +1 -0
  158. package/dist/ralplan/advisory-activation.js +205 -0
  159. package/dist/ralplan/advisory-activation.js.map +1 -0
  160. package/dist/ralplan/advisory-contract.d.ts +103 -0
  161. package/dist/ralplan/advisory-contract.d.ts.map +1 -0
  162. package/dist/ralplan/advisory-contract.js +83 -0
  163. package/dist/ralplan/advisory-contract.js.map +1 -0
  164. package/dist/ralplan/advisory-detached-transition.d.ts +11 -0
  165. package/dist/ralplan/advisory-detached-transition.d.ts.map +1 -0
  166. package/dist/ralplan/advisory-detached-transition.js +21 -0
  167. package/dist/ralplan/advisory-detached-transition.js.map +1 -0
  168. package/dist/ralplan/advisory-evidence.d.ts +91 -0
  169. package/dist/ralplan/advisory-evidence.d.ts.map +1 -0
  170. package/dist/ralplan/advisory-evidence.js +393 -0
  171. package/dist/ralplan/advisory-evidence.js.map +1 -0
  172. package/dist/ralplan/advisory-lifecycle-validation.d.ts +11 -0
  173. package/dist/ralplan/advisory-lifecycle-validation.d.ts.map +1 -0
  174. package/dist/ralplan/advisory-lifecycle-validation.js +72 -0
  175. package/dist/ralplan/advisory-lifecycle-validation.js.map +1 -0
  176. package/dist/ralplan/advisory-postconditions.d.ts +3 -0
  177. package/dist/ralplan/advisory-postconditions.d.ts.map +1 -0
  178. package/dist/ralplan/advisory-postconditions.js +33 -0
  179. package/dist/ralplan/advisory-postconditions.js.map +1 -0
  180. package/dist/ralplan/advisory-recovery-journal.d.ts +56 -0
  181. package/dist/ralplan/advisory-recovery-journal.d.ts.map +1 -0
  182. package/dist/ralplan/advisory-recovery-journal.js +136 -0
  183. package/dist/ralplan/advisory-recovery-journal.js.map +1 -0
  184. package/dist/ralplan/advisory-storage.d.ts +24 -0
  185. package/dist/ralplan/advisory-storage.d.ts.map +1 -0
  186. package/dist/ralplan/advisory-storage.js +106 -0
  187. package/dist/ralplan/advisory-storage.js.map +1 -0
  188. package/dist/ralplan/advisory.d.ts +99 -0
  189. package/dist/ralplan/advisory.d.ts.map +1 -0
  190. package/dist/ralplan/advisory.js +914 -0
  191. package/dist/ralplan/advisory.js.map +1 -0
  192. package/dist/ralplan/documented-leader-preflight.d.ts +8 -0
  193. package/dist/ralplan/documented-leader-preflight.d.ts.map +1 -1
  194. package/dist/ralplan/documented-leader-preflight.js +60 -2
  195. package/dist/ralplan/documented-leader-preflight.js.map +1 -1
  196. package/dist/ralplan/runtime-advisory-lifecycle.d.ts +74 -0
  197. package/dist/ralplan/runtime-advisory-lifecycle.d.ts.map +1 -0
  198. package/dist/ralplan/runtime-advisory-lifecycle.js +352 -0
  199. package/dist/ralplan/runtime-advisory-lifecycle.js.map +1 -0
  200. package/dist/ralplan/runtime-contract.d.ts +126 -0
  201. package/dist/ralplan/runtime-contract.d.ts.map +1 -0
  202. package/dist/ralplan/runtime-contract.js +2 -0
  203. package/dist/ralplan/runtime-contract.js.map +1 -0
  204. package/dist/ralplan/runtime.d.ts +6 -109
  205. package/dist/ralplan/runtime.d.ts.map +1 -1
  206. package/dist/ralplan/runtime.js +174 -56
  207. package/dist/ralplan/runtime.js.map +1 -1
  208. package/dist/scripts/__tests__/ralplan-advisory-hook.test.d.ts +2 -0
  209. package/dist/scripts/__tests__/ralplan-advisory-hook.test.d.ts.map +1 -0
  210. package/dist/scripts/__tests__/ralplan-advisory-hook.test.js +299 -0
  211. package/dist/scripts/__tests__/ralplan-advisory-hook.test.js.map +1 -0
  212. package/dist/scripts/__tests__/smoke-packed-install.test.js +102 -8
  213. package/dist/scripts/__tests__/smoke-packed-install.test.js.map +1 -1
  214. package/dist/scripts/codex-native-hook.d.ts +20 -0
  215. package/dist/scripts/codex-native-hook.d.ts.map +1 -1
  216. package/dist/scripts/codex-native-hook.js +226 -15
  217. package/dist/scripts/codex-native-hook.js.map +1 -1
  218. package/dist/scripts/smoke-packed-install.d.ts +21 -5
  219. package/dist/scripts/smoke-packed-install.d.ts.map +1 -1
  220. package/dist/scripts/smoke-packed-install.js +157 -57
  221. package/dist/scripts/smoke-packed-install.js.map +1 -1
  222. package/dist/state/__tests__/mode-binding-lease.test.d.ts +2 -0
  223. package/dist/state/__tests__/mode-binding-lease.test.d.ts.map +1 -0
  224. package/dist/state/__tests__/mode-binding-lease.test.js +498 -0
  225. package/dist/state/__tests__/mode-binding-lease.test.js.map +1 -0
  226. package/dist/state/__tests__/operations.test.js +1 -0
  227. package/dist/state/__tests__/operations.test.js.map +1 -1
  228. package/dist/state/__tests__/pinned-atomic-file.test.d.ts +2 -0
  229. package/dist/state/__tests__/pinned-atomic-file.test.d.ts.map +1 -0
  230. package/dist/state/__tests__/pinned-atomic-file.test.js +438 -0
  231. package/dist/state/__tests__/pinned-atomic-file.test.js.map +1 -0
  232. package/dist/state/__tests__/skill-active-exact-session-transaction.test.d.ts +2 -0
  233. package/dist/state/__tests__/skill-active-exact-session-transaction.test.d.ts.map +1 -0
  234. package/dist/state/__tests__/skill-active-exact-session-transaction.test.js +179 -0
  235. package/dist/state/__tests__/skill-active-exact-session-transaction.test.js.map +1 -0
  236. package/dist/state/__tests__/skill-active.test.js +29 -1
  237. package/dist/state/__tests__/skill-active.test.js.map +1 -1
  238. package/dist/state/mode-binding-lease-helper.d.ts +2 -0
  239. package/dist/state/mode-binding-lease-helper.d.ts.map +1 -0
  240. package/dist/state/mode-binding-lease-helper.js +623 -0
  241. package/dist/state/mode-binding-lease-helper.js.map +1 -0
  242. package/dist/state/mode-binding-lease.d.ts +25 -0
  243. package/dist/state/mode-binding-lease.d.ts.map +1 -0
  244. package/dist/state/mode-binding-lease.js +380 -0
  245. package/dist/state/mode-binding-lease.js.map +1 -0
  246. package/dist/state/operations.d.ts +29 -1
  247. package/dist/state/operations.d.ts.map +1 -1
  248. package/dist/state/operations.js +203 -122
  249. package/dist/state/operations.js.map +1 -1
  250. package/dist/state/pinned-atomic-file-darwin-client.d.ts +34 -0
  251. package/dist/state/pinned-atomic-file-darwin-client.d.ts.map +1 -0
  252. package/dist/state/pinned-atomic-file-darwin-client.js +174 -0
  253. package/dist/state/pinned-atomic-file-darwin-client.js.map +1 -0
  254. package/dist/state/pinned-atomic-file-darwin-helper.d.ts +2 -0
  255. package/dist/state/pinned-atomic-file-darwin-helper.d.ts.map +1 -0
  256. package/dist/state/pinned-atomic-file-darwin-helper.js +275 -0
  257. package/dist/state/pinned-atomic-file-darwin-helper.js.map +1 -0
  258. package/dist/state/pinned-atomic-file-darwin-janitor.d.ts +2 -0
  259. package/dist/state/pinned-atomic-file-darwin-janitor.d.ts.map +1 -0
  260. package/dist/state/pinned-atomic-file-darwin-janitor.js +272 -0
  261. package/dist/state/pinned-atomic-file-darwin-janitor.js.map +1 -0
  262. package/dist/state/pinned-atomic-file.d.ts +29 -0
  263. package/dist/state/pinned-atomic-file.d.ts.map +1 -0
  264. package/dist/state/pinned-atomic-file.js +536 -0
  265. package/dist/state/pinned-atomic-file.js.map +1 -0
  266. package/dist/state/process-identity.d.ts +30 -0
  267. package/dist/state/process-identity.d.ts.map +1 -0
  268. package/dist/state/process-identity.js +99 -0
  269. package/dist/state/process-identity.js.map +1 -0
  270. package/dist/state/skill-active.d.ts +18 -1
  271. package/dist/state/skill-active.d.ts.map +1 -1
  272. package/dist/state/skill-active.js +82 -5
  273. package/dist/state/skill-active.js.map +1 -1
  274. package/dist/state/workflow-transition-reconcile.js +1 -1
  275. package/dist/state/workflow-transition-reconcile.js.map +1 -1
  276. package/dist/team/__tests__/tmux-session.test.js +109 -0
  277. package/dist/team/__tests__/tmux-session.test.js.map +1 -1
  278. package/dist/team/tmux-session.d.ts.map +1 -1
  279. package/dist/team/tmux-session.js +10 -1
  280. package/dist/team/tmux-session.js.map +1 -1
  281. package/dist/utils/__tests__/file-durability.test.js +20 -0
  282. package/dist/utils/__tests__/file-durability.test.js.map +1 -1
  283. package/dist/utils/file-durability.d.ts +1 -1
  284. package/dist/utils/file-durability.d.ts.map +1 -1
  285. package/dist/utils/file-durability.js.map +1 -1
  286. package/dist/verification/__tests__/ci-rust-gates.test.js +12 -4
  287. package/dist/verification/__tests__/ci-rust-gates.test.js.map +1 -1
  288. package/dist/verification/__tests__/ci-trusted-publish.test.d.ts +2 -0
  289. package/dist/verification/__tests__/ci-trusted-publish.test.d.ts.map +1 -0
  290. package/dist/verification/__tests__/ci-trusted-publish.test.js +205 -0
  291. package/dist/verification/__tests__/ci-trusted-publish.test.js.map +1 -0
  292. package/dist/verification/__tests__/explore-harness-release-workflow.test.js +10 -5
  293. package/dist/verification/__tests__/explore-harness-release-workflow.test.js.map +1 -1
  294. package/dist/verification/__tests__/publish-npm-manual-retired.test.d.ts +2 -0
  295. package/dist/verification/__tests__/publish-npm-manual-retired.test.d.ts.map +1 -0
  296. package/dist/verification/__tests__/publish-npm-manual-retired.test.js +22 -0
  297. package/dist/verification/__tests__/publish-npm-manual-retired.test.js.map +1 -0
  298. package/package.json +1 -1
  299. package/plugins/oh-my-codex/.codex-plugin/plugin.json +1 -1
  300. package/plugins/oh-my-codex/skills/doctor/SKILL.md +1 -1
  301. package/plugins/oh-my-codex/skills/omx-setup/SKILL.md +1 -1
  302. package/plugins/oh-my-codex/skills/ralplan/SKILL.md +10 -1
  303. package/plugins/oh-my-codex/skills/team/SKILL.md +2 -0
  304. package/skills/doctor/SKILL.md +1 -1
  305. package/skills/omx-setup/SKILL.md +1 -1
  306. package/skills/ralplan/SKILL.md +10 -1
  307. package/skills/team/SKILL.md +2 -0
  308. package/src/scripts/__tests__/ralplan-advisory-hook.test.ts +320 -0
  309. package/src/scripts/__tests__/smoke-packed-install.test.ts +120 -7
  310. package/src/scripts/codex-native-hook.ts +220 -10
  311. package/src/scripts/smoke-packed-install.ts +175 -70
package/Cargo.toml CHANGED
@@ -11,7 +11,7 @@ resolver = "2"
11
11
 
12
12
  [workspace.package]
13
13
 
14
- version = "0.21.0"
14
+ version = "0.21.1"
15
15
 
16
16
  edition = "2021"
17
17
  rust-version = "1.73"
package/README.md CHANGED
@@ -12,13 +12,13 @@
12
12
  [![npm version](https://img.shields.io/npm/v/oh-my-codex)](https://www.npmjs.com/package/oh-my-codex)
13
13
  [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
14
14
  [![Node.js](https://img.shields.io/badge/node-%3E%3D20-brightgreen)](https://nodejs.org)
15
- [![Discord](https://img.shields.io/discord/1452487457085063218?color=5865F2&logo=discord&logoColor=white&label=Discord)](https://discord.gg/jq6jnSGABY)
15
+ [![Discord](https://img.shields.io/discord/1452487457085063218?color=5865F2&logo=discord&logoColor=white&label=Discord)](https://discord.gg/wSyUQYfhAw)
16
16
 
17
17
  **Website:** https://yeachan-heo.github.io/oh-my-codex-website/
18
18
 
19
19
  **Docs:** [Getting Started](./docs/getting-started.html) · [Agents](./docs/agents.html) · [Skills](./docs/skills.html) · [Integrations](./docs/integrations.html) · [Demo](./DEMO.md) · [OpenClaw guide](./docs/openclaw-integration.md)
20
20
 
21
- **Community:** [Discord](https://discord.gg/jq6jnSGABY) — shared OMX/community server for oh-my-codex and related tooling.
21
+ **Community:** [Discord](https://discord.gg/wSyUQYfhAw) — shared Gajae community server for oh-my-codex, [gajae-code](https://github.com/Yeachan-Heo/gajae-code), and related tooling.
22
22
 
23
23
  ## Official project and package
24
24
 
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=detached-launch-diagnostics-3578.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detached-launch-diagnostics-3578.test.d.ts","sourceRoot":"","sources":["../../../src/cli/__tests__/detached-launch-diagnostics-3578.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,592 @@
1
+ import assert from 'node:assert/strict';
2
+ import { mkdtempSync, rmSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import { describe, it } from 'node:test';
6
+ import { buildDetachedSessionRollbackSteps, cleanupDetachedPreReportSession, cleanupDetachedPreReportSessionForTest, cleanupDetachedLeaderSessionAfterFailureForTest, detachedLeaderFailureErrorForTest, DetachedLaunchSafetyError, isDetachedFailedReportAuthorizedForTest, isDetachedReadyReportAuthorized, isDetachedSessionPointerAbortCarried, reportDetachedSessionPointerGuidance, resolveDetachedLeaderPaneForTest, } from '../index.js';
7
+ import { isRealTmuxAvailable, withTempTmuxSession } from '../../team/__tests__/tmux-test-fixture.js';
8
+ function skipUnlessTmux(t) {
9
+ if (process.platform === 'win32') {
10
+ t.skip('detached tmux leader tests are not supported on win32');
11
+ return false;
12
+ }
13
+ if (isRealTmuxAvailable())
14
+ return true;
15
+ assert.equal(process.env.CI, undefined, 'CI must provide tmux for #3578 detached diagnostics tests');
16
+ t.skip('tmux is not installed');
17
+ return false;
18
+ }
19
+ const emptyReport = () => ({ transitions: ['D0'], rollback: { attempted: [], failures: [] } });
20
+ describe('#3578 detached launch diagnostics', () => {
21
+ describe('Windows detached leader identity', () => {
22
+ it('accepts an inherited pane when the tmux session id differs from the OMX session id', () => {
23
+ assert.equal(resolveDetachedLeaderPaneForTest({
24
+ sessionName: 'omx-session',
25
+ inheritedPane: '%42',
26
+ platform: 'win32',
27
+ leaderPid: 901,
28
+ requireInheritedPane: false,
29
+ identityOutput: 'omx-session\t$7\t%42',
30
+ paneSnapshot: '%42\t0\t901',
31
+ }), '%42');
32
+ assert.throws(() => resolveDetachedLeaderPaneForTest({
33
+ sessionName: 'omx-session',
34
+ expectedTmuxSessionId: '$1',
35
+ inheritedPane: '%42',
36
+ platform: 'win32',
37
+ leaderPid: 901,
38
+ requireInheritedPane: false,
39
+ identityOutput: 'omx-session\t$7\t%42',
40
+ paneSnapshot: '%42\t0\t901',
41
+ }), /inherited pane identity is unavailable/);
42
+ });
43
+ it('authorizes Windows ready and failed reports by pane identity when process PIDs differ', () => {
44
+ const expected = {
45
+ nonce: 'windows-nonce',
46
+ sessionId: 'omx-session-id',
47
+ sessionName: 'omx-session',
48
+ shouldAttach: true,
49
+ leaderPaneId: '%42',
50
+ leaderPanePid: 901,
51
+ };
52
+ const ready = { version: 1, kind: 'ready', ...expected, paneId: '%42', leaderPid: 1201 };
53
+ const failed = { version: 1, kind: 'failed', ...expected, paneId: '%42', leaderPid: 1201 };
54
+ assert.equal(isDetachedReadyReportAuthorized(ready, expected, 'win32'), true);
55
+ assert.equal(isDetachedReadyReportAuthorized(ready, expected, 'linux'), false);
56
+ assert.equal(isDetachedFailedReportAuthorizedForTest(failed, expected, 'win32'), true);
57
+ assert.equal(isDetachedFailedReportAuthorizedForTest(failed, expected, 'linux'), false);
58
+ assert.equal(isDetachedFailedReportAuthorizedForTest({ ...failed, paneId: '%43' }, expected, 'win32'), false);
59
+ });
60
+ });
61
+ describe('exact failing-step and cause reporting', () => {
62
+ it('carries the exact bootstrap step name next to the coarse phase', () => {
63
+ const cause = new Error('leader authority blocked tmux mutation history-limit');
64
+ const stepError = new DetachedLaunchSafetyError('pane-id', cause, emptyReport(), 'tag-session');
65
+ assert.equal(stepError.step, 'tag-session');
66
+ assert.match(stepError.message, /during pane-id \(tag-session\)/);
67
+ assert.match(stepError.message, /history-limit/);
68
+ // The state machine's rewrap preserves the step across propagation.
69
+ const rewrapped = new DetachedLaunchSafetyError(stepError.phase, stepError.cause, emptyReport(), stepError.step);
70
+ assert.equal(rewrapped.step, 'tag-session');
71
+ });
72
+ it('keeps distinct step names for post-new-session mutations that previously collapsed into pane-id', () => {
73
+ for (const step of ['tag-session', 'split-and-capture-hud-pane', 'register-resize-hook', 'schedule-delayed-resize']) {
74
+ const error = new DetachedLaunchSafetyError('pane-id', new Error('boom'), emptyReport(), step);
75
+ assert.equal(error.step, step);
76
+ assert.match(error.message, new RegExp(`\\(${step.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\)`));
77
+ }
78
+ // new-session keeps the legacy inert-session phase and needs no step suffix.
79
+ const inert = new DetachedLaunchSafetyError('inert-session', new Error('no pane id'), emptyReport(), 'new-session');
80
+ assert.match(inert.message, /during inert-session \(new-session\)/);
81
+ });
82
+ it('renders the step only when it differs from the phase', () => {
83
+ const withoutStep = new DetachedLaunchSafetyError('completion', new Error('timed out'), emptyReport());
84
+ assert.equal(withoutStep.step, undefined);
85
+ assert.doesNotMatch(withoutStep.message, /\(/);
86
+ const report = JSON.parse(`{"phase":"pane-id","step":"split-and-capture-hud-pane","failure":"boom"}`);
87
+ assert.equal(report.step, 'split-and-capture-hud-pane');
88
+ });
89
+ });
90
+ describe('bounded abort-code propagation through AggregateError', () => {
91
+ it('rebuilds the bounded abort marker from a validated failed report', () => {
92
+ const carried = detachedLeaderFailureErrorForTest({
93
+ error: 'Session pointer sess-live-owner conflicts with an active session; launch aborted',
94
+ abortCode: 'session_pointer_owner_conflict',
95
+ });
96
+ assert.equal(isDetachedSessionPointerAbortCarried(carried), true);
97
+ assert.equal(carried.code, 'session_pointer_owner_conflict');
98
+ const plain = detachedLeaderFailureErrorForTest({ error: 'detached leader setup failed' });
99
+ assert.equal(isDetachedSessionPointerAbortCarried(plain), false);
100
+ });
101
+ it('finds the abort marker inside the D2 AggregateError wrap', () => {
102
+ // executeDetachedLaunchStateMachine wraps completion failures as
103
+ // AggregateError([leaderError]) before the outer launcher sees them.
104
+ const leaderError = detachedLeaderFailureErrorForTest({ abortCode: 'session_pointer_unusable' });
105
+ const wrapped = new AggregateError([leaderError], 'preLaunch session-instructions failed: unusable');
106
+ assert.equal(isDetachedSessionPointerAbortCarried(wrapped), true);
107
+ // Negative: ordinary nested errors never carry the marker.
108
+ assert.equal(isDetachedSessionPointerAbortCarried(new AggregateError([new Error('x')], 'y')), false);
109
+ assert.equal(isDetachedSessionPointerAbortCarried(new Error('session_pointer_owner_conflict')), false);
110
+ });
111
+ });
112
+ describe('detached session-pointer guidance', () => {
113
+ it('prints the ordinary OMX_ROOT guidance for a carried owner conflict, gated on cwd-default', () => {
114
+ const wd = mkdtempSync(join(tmpdir(), 'omx-3578-guidance-'));
115
+ const cwd = join(wd, 'checkout');
116
+ try {
117
+ const carried = detachedLeaderFailureErrorForTest({ abortCode: 'session_pointer_owner_conflict' });
118
+ const wrapped = new AggregateError([carried], 'preLaunch session-instructions failed');
119
+ const captured = [];
120
+ const originalWrite = process.stderr.write.bind(process.stderr);
121
+ process.stderr.write = (chunk) => {
122
+ captured.push(String(chunk));
123
+ return true;
124
+ };
125
+ try {
126
+ reportDetachedSessionPointerGuidance(wrapped, cwd);
127
+ }
128
+ finally {
129
+ process.stderr.write = originalWrite;
130
+ }
131
+ const output = captured.join('');
132
+ assert.match(output, /concurrent conversations in this checkout require distinct user-specified OMX_ROOT values/);
133
+ assert.match(output, /POSIX: OMX_ROOT="\$HOME\/\.omx\/instances\/second-conversation" omx/);
134
+ assert.match(output, /OMX does not reroute or allocate one automatically/);
135
+ }
136
+ finally {
137
+ rmSync(wd, { recursive: true, force: true });
138
+ }
139
+ });
140
+ it('points stale/unusable pointers at doctor instead of OMX_ROOT rerouting', () => {
141
+ const carried = detachedLeaderFailureErrorForTest({ abortCode: 'session_pointer_unusable' });
142
+ const captured = [];
143
+ const originalWrite = process.stderr.write.bind(process.stderr);
144
+ process.stderr.write = (chunk) => {
145
+ captured.push(String(chunk));
146
+ return true;
147
+ };
148
+ try {
149
+ reportDetachedSessionPointerGuidance(carried, '/tmp');
150
+ }
151
+ finally {
152
+ process.stderr.write = originalWrite;
153
+ }
154
+ const output = captured.join('');
155
+ assert.match(output, /run `omx doctor` for the exact pointer status/);
156
+ assert.doesNotMatch(output, /distinct user-specified OMX_ROOT/);
157
+ // Negative: an ordinary detached failure prints no pointer guidance at all.
158
+ captured.length = 0;
159
+ const restore = process.stderr.write.bind(process.stderr);
160
+ process.stderr.write = (chunk) => {
161
+ captured.push(String(chunk));
162
+ return true;
163
+ };
164
+ try {
165
+ reportDetachedSessionPointerGuidance(new Error('detached leader readiness timed out'), '/tmp');
166
+ }
167
+ finally {
168
+ process.stderr.write = restore;
169
+ }
170
+ assert.equal(captured.length, 0);
171
+ });
172
+ });
173
+ describe('identity-fenced cleanup when the leader pane is absent', () => {
174
+ it('lets a failed leader clean its exact owned session after the parent exits', async (t) => {
175
+ if (!skipUnlessTmux(t))
176
+ return;
177
+ await withTempTmuxSession(async (fixture) => {
178
+ const sessionName = 'omx-3578-leader-owned-failure-cleanup';
179
+ fixture.run(['new-session', '-d', '-s', sessionName, '-c', fixture.sessionName, 'sleep 300']);
180
+ fixture.run(['split-window', '-d', '-P', '-F', '#{pane_id}', '-t', sessionName, 'sleep 300']);
181
+ const authority = captureSession(fixture, sessionName, 'leader-failure-owner');
182
+ fixture.run(['set-option', '-t', sessionName, '@omx_instance_id', authority.ownerId]);
183
+ const foreignSessionName = `${sessionName}-foreign`;
184
+ fixture.run(['new-session', '-d', '-s', foreignSessionName, '-c', fixture.sessionName, 'sleep 300']);
185
+ cleanupDetachedLeaderSessionAfterFailureForTest(authority, authority.ownerId);
186
+ const sessions = fixture.run(['list-sessions', '-F', '#{session_name}']).split('\n');
187
+ assert.equal(sessions.includes(sessionName), false, 'failed leader must clean its exact owned session');
188
+ assert.equal(sessions.includes(foreignSessionName), true, 'failed leader cleanup must not touch unrelated sessions');
189
+ });
190
+ });
191
+ it('rejects empty ownership after the tag phase was attempted', async (t) => {
192
+ if (!skipUnlessTmux(t))
193
+ return;
194
+ await withTempTmuxSession(async (fixture) => {
195
+ const sessionName = 'omx-3578-owner-phase-fence';
196
+ fixture.run(['new-session', '-d', '-s', sessionName, '-c', fixture.sessionName, 'sleep 300']);
197
+ fixture.run(['split-window', '-d', '-P', '-F', '#{pane_id}', '-t', sessionName, 'sleep 300']);
198
+ const authority = captureSession(fixture, sessionName, 'owner-phase-fence');
199
+ fixture.run(['set-option', '-t', sessionName, '@omx_detached_owner_tag_attempted', '1']);
200
+ cleanupDetachedLeaderSessionAfterFailureForTest(authority, authority.ownerId);
201
+ assert.equal(fixture.run(['list-sessions', '-F', '#{session_name}']).split('\n').includes(sessionName), true, 'empty owner after tag attempt must preserve');
202
+ });
203
+ });
204
+ it('rechecks the tag-attempt marker inside the destructive predicate', async (t) => {
205
+ if (!skipUnlessTmux(t))
206
+ return;
207
+ await withTempTmuxSession(async (fixture) => {
208
+ const sessionName = 'omx-3578-owner-phase-interposition';
209
+ fixture.run(['new-session', '-d', '-s', sessionName, '-c', fixture.sessionName, 'sleep 300']);
210
+ fixture.run(['split-window', '-d', '-P', '-F', '#{pane_id}', '-t', sessionName, 'sleep 300']);
211
+ const authority = captureSession(fixture, sessionName, 'owner-phase-interposition');
212
+ cleanupDetachedLeaderSessionAfterFailureForTest(authority, authority.ownerId, () => {
213
+ // Simulate tag-session completing after any earlier observation but
214
+ // before the single queued destructive predicate evaluates.
215
+ fixture.run(['set-option', '-t', sessionName, '@omx_detached_owner_tag_attempted', '1']);
216
+ });
217
+ assert.equal(fixture.run(['list-sessions', '-F', '#{session_name}']).split('\n').includes(sessionName), true, 'same-command marker recheck must preserve');
218
+ });
219
+ });
220
+ const captureSession = (fixture, sessionName, ownerId) => {
221
+ const [paneId, panePidRaw, , sessionId, sessionCreated, windowIndex, windowId] = fixture.run([
222
+ 'list-panes', '-t', sessionName, '-F',
223
+ '#{pane_id}\t#{pane_pid}\t#{session_name}\t#{session_id}\t#{session_created}\t#{window_index}\t#{window_id}',
224
+ ]).trim().split('\t');
225
+ assert.ok(paneId && panePidRaw && sessionId && sessionCreated && windowId);
226
+ return {
227
+ paneId: paneId,
228
+ panePid: Number(panePidRaw),
229
+ sessionName,
230
+ sessionId: sessionId,
231
+ sessionCreated: sessionCreated,
232
+ windowIndex: (windowIndex ?? '0'),
233
+ windowId: windowId,
234
+ ownerId,
235
+ };
236
+ };
237
+ it('cleans up through the session fence after a normal leader exit removed the pane', async (t) => {
238
+ if (!skipUnlessTmux(t))
239
+ return;
240
+ await withTempTmuxSession(async (fixture) => {
241
+ const sessionName = 'omx-3578-leader-gone';
242
+ fixture.run(['new-session', '-d', '-s', sessionName, '-c', fixture.sessionName, 'sleep 5']);
243
+ const hudPaneId = fixture.run([
244
+ 'split-window', '-d', '-P', '-F', '#{pane_id}', '-t', sessionName, 'sleep 300',
245
+ ]);
246
+ const authority = captureSession(fixture, sessionName, 'owner-3578-a');
247
+ fixture.run(['set-option', '-t', sessionName, '@omx_instance_id', authority.ownerId]);
248
+ // Normal leader exit with remain-on-exit off removes the leader pane; the
249
+ // HUD-only session is exactly the leak #3578 reports.
250
+ await new Promise((resolve) => setTimeout(resolve, 5_500));
251
+ assert.equal(fixture.run(['list-panes', '-s', '-t', sessionName, '-F', '#{pane_id}']).split('\n').includes(authority.paneId), false, 'fixture must reproduce the removed-leader-pane topology');
252
+ assert.equal(fixture.run(['list-panes', '-s', '-t', sessionName, '-F', '#{pane_id}']).split('\n').includes(hudPaneId), true);
253
+ cleanupDetachedPreReportSession(authority);
254
+ await new Promise((resolve) => setTimeout(resolve, 300));
255
+ // has-session exits 1 once the exact owned session is destroyed: the
256
+ // leak #3578 reported is its continued survival, so non-zero is the pass.
257
+ assert.equal(fixture.run(['list-sessions', '-F', '#{session_name}']).split('\n').includes(sessionName), false, 'the exact owned HUD-only session must be destroyed');
258
+ });
259
+ });
260
+ it('cleans a retained dead leader before the session owner tag is installed', async (t) => {
261
+ if (!skipUnlessTmux(t))
262
+ return;
263
+ await withTempTmuxSession(async (fixture) => {
264
+ const sessionName = 'omx-3578-pre-tag-retained';
265
+ fixture.run(['new-session', '-d', '-s', sessionName, '-c', fixture.sessionName, 'sleep 5']);
266
+ const hudPaneId = fixture.run([
267
+ 'split-window', '-d', '-P', '-F', '#{pane_id}', '-t', sessionName, 'sleep 300',
268
+ ]);
269
+ const authority = captureSession(fixture, sessionName, 'pre-tag-owner');
270
+ fixture.run(['set-option', '-t', sessionName, 'remain-on-exit', 'on']);
271
+ await new Promise((resolve) => setTimeout(resolve, 5_500));
272
+ assert.equal(fixture.run(['display-message', '-p', '-t', authority.paneId, '#{pane_dead}']), '1', 'the retained dead leader must reproduce the pre-tag rollback topology');
273
+ assert.equal(fixture.run(['display-message', '-p', '-t', sessionName, '#{@omx_instance_id}']).trim(), '', 'the pre-tag fixture must not install an owner tag');
274
+ const foreignSessionName = `${sessionName}-foreign`;
275
+ fixture.run(['new-session', '-d', '-s', foreignSessionName, '-c', fixture.sessionName, 'sleep 300']);
276
+ cleanupDetachedPreReportSession(authority, true);
277
+ await new Promise((resolve) => setTimeout(resolve, 300));
278
+ assert.equal(fixture.run(['list-sessions', '-F', '#{session_name}']).split('\n').includes(sessionName), false, 'the exact pre-tag session must be destroyed');
279
+ assert.equal(fixture.run(['list-sessions', '-F', '#{session_name}']).split('\n').includes(foreignSessionName), true, 'the unrelated session must survive');
280
+ assert.equal(fixture.run(['list-panes', '-s', '-t', foreignSessionName, '-F', '#{pane_id}']).includes(hudPaneId), false);
281
+ });
282
+ });
283
+ it('refuses cleanup when a replacement session reuses the exact session name (negative identity race)', async (t) => {
284
+ if (!skipUnlessTmux(t))
285
+ return;
286
+ await withTempTmuxSession(async (fixture) => {
287
+ const sessionName = 'omx-3578-reuse-race';
288
+ fixture.run(['new-session', '-d', '-s', sessionName, '-c', fixture.sessionName, 'sleep 5']);
289
+ const authority = captureSession(fixture, sessionName, 'owner-3578-b');
290
+ await new Promise((resolve) => setTimeout(resolve, 5_500));
291
+ // Race: a replacement session takes the same name with different
292
+ // session_id/session_created before cleanup runs, while the original
293
+ // session is still in the pre-tag owner-unset state.
294
+ fixture.run(['new-session', '-d', '-s', sessionName, '-c', fixture.sessionName, 'sleep 300']);
295
+ assert.throws(() => cleanupDetachedPreReportSession(authority, true), /topology changed before cleanup/, 'a name-reusing replacement session must never be killed');
296
+ assert.equal(fixture.run(['list-sessions', '-F', '#{session_name}']).split('\n').includes(sessionName), true, 'the replacement session must survive');
297
+ });
298
+ });
299
+ it('refuses cleanup when the session identity matches but the owner tag is foreign (negative identity)', async (t) => {
300
+ if (!skipUnlessTmux(t))
301
+ return;
302
+ await withTempTmuxSession(async (fixture) => {
303
+ const sessionName = 'omx-3578-foreign-owner';
304
+ fixture.run(['new-session', '-d', '-s', sessionName, '-c', fixture.sessionName, 'sleep 5']);
305
+ // A surviving HUD pane keeps the session alive after the leader pane is
306
+ // gone, so cleanup must route through the session fence and be refused
307
+ // there by the foreign owner tag.
308
+ fixture.run(['split-window', '-d', '-P', '-F', '#{pane_id}', '-t', sessionName, 'sleep 300']);
309
+ const authority = captureSession(fixture, sessionName, 'owner-3578-c');
310
+ fixture.run(['set-option', '-t', sessionName, '@omx_instance_id', 'foreign-owner']);
311
+ await new Promise((resolve) => setTimeout(resolve, 5_500));
312
+ assert.throws(() => cleanupDetachedPreReportSession(authority), /topology changed before cleanup/, 'a foreign owner tag must fail the session fence closed');
313
+ // The HUD-only session is deliberately preserved: identity could not be proven.
314
+ const survivors = fixture.run(['list-panes', '-s', '-t', sessionName, '-F', '#{pane_id}']).trim().split('\n').filter(Boolean);
315
+ assert.equal(survivors.length > 0, true, 'the unproven session must be preserved');
316
+ });
317
+ });
318
+ it('refuses cleanup when a tagged session loses ownership before rollback (post-tag unset)', async (t) => {
319
+ if (!skipUnlessTmux(t))
320
+ return;
321
+ await withTempTmuxSession(async (fixture) => {
322
+ const sessionName = 'omx-3578-post-tag-unset';
323
+ fixture.run(['new-session', '-d', '-s', sessionName, '-c', fixture.sessionName, 'sleep 5']);
324
+ fixture.run(['split-window', '-d', '-P', '-F', '#{pane_id}', '-t', sessionName, 'sleep 300']);
325
+ const authority = captureSession(fixture, sessionName, 'post-tag-owner');
326
+ fixture.run(['set-option', '-t', sessionName, '@omx_instance_id', authority.ownerId]);
327
+ fixture.run(['set-option', '-t', sessionName, 'remain-on-exit', 'on']);
328
+ await new Promise((resolve) => setTimeout(resolve, 5_500));
329
+ fixture.run(['set-option', '-u', '-t', sessionName, '@omx_instance_id']);
330
+ const foreignSessionName = `${sessionName}-foreign`;
331
+ fixture.run(['new-session', '-d', '-s', foreignSessionName, '-c', fixture.sessionName, 'sleep 300']);
332
+ assert.throws(() => cleanupDetachedPreReportSession(authority), /topology changed before cleanup/, 'an owner tag removed after tag-session must not be treated as pre-tag state');
333
+ const sessions = fixture.run(['list-sessions', '-F', '#{session_name}']).split('\n');
334
+ assert.equal(sessions.includes(sessionName), true, 'the unowned exact session must survive');
335
+ assert.equal(sessions.includes(foreignSessionName), true, 'the unrelated session must survive');
336
+ });
337
+ });
338
+ it('keeps the rollback step builder targeting the named session only', () => {
339
+ const steps = buildDetachedSessionRollbackSteps('omx-3578-target', null, null, null);
340
+ const kill = steps.find((step) => step.name === 'kill-session');
341
+ assert.ok(kill);
342
+ assert.deepEqual(kill.args, ['kill-session', '-t', 'omx-3578-target']);
343
+ });
344
+ it('preserves without mutation when the exact session disappears before cleanup (destroyed session)', async (t) => {
345
+ if (!skipUnlessTmux(t))
346
+ return;
347
+ await withTempTmuxSession(async (fixture) => {
348
+ const sessionName = 'omx-3578-destroyed';
349
+ fixture.run(['new-session', '-d', '-s', sessionName, '-c', fixture.sessionName, 'sleep 300']);
350
+ const authority = captureSession(fixture, sessionName, 'owner-destroyed');
351
+ fixture.run(['set-option', '-t', sessionName, '@omx_instance_id', authority.ownerId]);
352
+ fixture.run(['kill-session', '-t', sessionName]);
353
+ assert.throws(() => cleanupDetachedPreReportSession(authority), /topology changed before cleanup/, 'destroyed exact session must fail closed without mutation');
354
+ fixture.run(['new-session', '-d', '-s', sessionName, '-c', fixture.sessionName, 'sleep 300']);
355
+ fixture.run(['set-option', '-t', sessionName, '@omx_instance_id', 'replacement-after-destroy']);
356
+ assert.throws(() => cleanupDetachedPreReportSession(authority), /topology changed before cleanup/, 'replacement after destroyed session must never be killed');
357
+ assert.equal(fixture.run(['list-sessions', '-F', '#{session_name}']).split('\n').includes(sessionName), true, 'replacement session must survive');
358
+ });
359
+ });
360
+ it('preserves the exact session when ownership drifts after the topology probe and before cleanup (race)', async (t) => {
361
+ if (!skipUnlessTmux(t))
362
+ return;
363
+ await withTempTmuxSession(async (fixture) => {
364
+ const sessionName = 'omx-3578-probe-sink-race';
365
+ fixture.run(['new-session', '-d', '-s', sessionName, '-c', fixture.sessionName, 'sleep 5']);
366
+ const hudPaneId = fixture.run([
367
+ 'split-window', '-d', '-P', '-F', '#{pane_id}', '-t', sessionName, 'sleep 300',
368
+ ]);
369
+ const authority = captureSession(fixture, sessionName, 'owner-race');
370
+ fixture.run(['set-option', '-t', sessionName, '@omx_instance_id', authority.ownerId]);
371
+ fixture.run(['set-option', '-t', sessionName, 'remain-on-exit', 'on']);
372
+ await new Promise((resolve) => setTimeout(resolve, 5_500));
373
+ assert.equal(fixture.run(['display-message', '-p', '-t', authority.paneId, '#{pane_dead}']), '1', 'the captured leader pane must be retained dead so the old pane sink would be eligible');
374
+ const foreignSessionName = `${sessionName}-foreign`;
375
+ fixture.run(['new-session', '-d', '-s', foreignSessionName, '-c', fixture.sessionName, 'sleep 300']);
376
+ assert.throws(() => cleanupDetachedPreReportSessionForTest(authority, () => {
377
+ // This callback is the deterministic interposition point between
378
+ // detachedPreReportLeaderPaneState and the destructive sink.
379
+ fixture.run(['set-option', '-t', sessionName, '@omx_instance_id', 'foreign-race-owner']);
380
+ }), /topology changed before cleanup/, 'ownership drift in the exact probe-to-sink interval must fail closed');
381
+ assert.equal(fixture.run(['list-panes', '-s', '-t', sessionName, '-F', '#{pane_id}']).split('\n').includes(hudPaneId), true, 'HUD pane must survive race');
382
+ assert.equal(fixture.run(['list-panes', '-s', '-t', sessionName, '-F', '#{pane_id}']).split('\n').includes(authority.paneId), true, 'leader pane must survive race');
383
+ assert.equal(fixture.run(['list-sessions', '-F', '#{session_name}']).split('\n').includes(sessionName), true, 'HUD session must survive race');
384
+ assert.equal(fixture.run(['list-sessions', '-F', '#{session_name}']).split('\n').includes(foreignSessionName), true, 'unrelated session must survive race');
385
+ });
386
+ });
387
+ it('preserves the session when authenticated readiness arrives at the destructive boundary', async (t) => {
388
+ if (!skipUnlessTmux(t))
389
+ return;
390
+ await withTempTmuxSession(async (fixture) => {
391
+ const sessionName = 'omx-3578-ready-handoff-race';
392
+ fixture.run(['new-session', '-d', '-s', sessionName, '-c', fixture.sessionName, 'sleep 300']);
393
+ fixture.run(['split-window', '-d', '-P', '-F', '#{pane_id}', '-t', sessionName, 'sleep 300']);
394
+ const authority = captureSession(fixture, sessionName, 'ready-handoff-owner');
395
+ fixture.run(['set-option', '-t', sessionName, '@omx_instance_id', authority.ownerId]);
396
+ assert.equal(fixture.run(['display-message', '-p', '-t', authority.paneId, '#{pane_dead}']), '0');
397
+ const foreignSessionName = `${sessionName}-foreign`;
398
+ fixture.run(['new-session', '-d', '-s', foreignSessionName, '-c', fixture.sessionName, 'sleep 300']);
399
+ let lateReadyReport;
400
+ const expected = {
401
+ nonce: 'ready-handoff-nonce',
402
+ sessionId: authority.sessionId,
403
+ sessionName,
404
+ shouldAttach: false,
405
+ leaderPaneId: authority.paneId,
406
+ leaderPanePid: authority.panePid,
407
+ };
408
+ const cleanupStartedAt = Date.now();
409
+ cleanupDetachedPreReportSessionForTest(authority, () => { }, false, () => false, () => {
410
+ // Completion has already read false. A valid ready report lands
411
+ // after that read; the live-pane branch must preserve immediately
412
+ // without entering the failed-report retry wait.
413
+ lateReadyReport = {
414
+ version: 1,
415
+ kind: 'ready',
416
+ nonce: expected.nonce,
417
+ sessionId: expected.sessionId,
418
+ sessionName: expected.sessionName,
419
+ paneId: authority.paneId,
420
+ leaderPid: authority.panePid,
421
+ };
422
+ });
423
+ assert.ok(Date.now() - cleanupStartedAt < 1_000, 'ordinary timeout must not wait for failed-report retry');
424
+ assert.equal(isDetachedReadyReportAuthorized(lateReadyReport, expected), true, 'the late report must be authenticated');
425
+ assert.equal(fixture.run(['list-sessions', '-F', '#{session_name}']).split('\n').includes(sessionName), true, 'ready session must survive');
426
+ assert.equal(fixture.run(['list-sessions', '-F', '#{session_name}']).split('\n').includes(foreignSessionName), true, 'unrelated session must survive');
427
+ });
428
+ });
429
+ it('keeps timeout rollback nonblocking and lets the leader clean a later authenticated failure', async (t) => {
430
+ if (!skipUnlessTmux(t))
431
+ return;
432
+ await withTempTmuxSession(async (fixture) => {
433
+ const sessionName = 'omx-3578-late-failure-async';
434
+ fixture.run(['new-session', '-d', '-s', sessionName, '-c', fixture.sessionName, 'sleep 3']);
435
+ fixture.run(['split-window', '-d', '-P', '-F', '#{pane_id}', '-t', sessionName, 'sleep 300']);
436
+ const authority = captureSession(fixture, sessionName, 'late-failure-owner');
437
+ fixture.run(['set-option', '-t', sessionName, '@omx_instance_id', authority.ownerId]);
438
+ const foreignSessionName = `${sessionName}-foreign`;
439
+ fixture.run(['new-session', '-d', '-s', foreignSessionName, '-c', fixture.sessionName, 'sleep 300']);
440
+ const expected = {
441
+ nonce: 'late-failure-nonce',
442
+ sessionId: authority.sessionId,
443
+ sessionName,
444
+ leaderPaneId: authority.paneId,
445
+ leaderPanePid: authority.panePid,
446
+ };
447
+ let failedReport;
448
+ const timeoutStartedAt = Date.now();
449
+ cleanupDetachedPreReportSessionForTest(authority, () => { }, false, () => false);
450
+ assert.ok(Date.now() - timeoutStartedAt < 1_000, 'plain timeout must return without synchronous retry wait');
451
+ assert.equal(fixture.run(['list-sessions', '-F', '#{session_name}']).split('\n').includes(sessionName), true);
452
+ setTimeout(() => {
453
+ failedReport = {
454
+ version: 1,
455
+ kind: 'failed',
456
+ nonce: expected.nonce,
457
+ sessionId: expected.sessionId,
458
+ sessionName,
459
+ paneId: authority.paneId,
460
+ leaderPid: authority.panePid,
461
+ };
462
+ cleanupDetachedLeaderSessionAfterFailureForTest(authority, authority.ownerId);
463
+ }, 100);
464
+ for (let attempt = 0; attempt < 100 && fixture.run(['list-sessions', '-F', '#{session_name}']).split('\n').includes(sessionName); attempt += 1) {
465
+ await new Promise((resolve) => setTimeout(resolve, 20));
466
+ }
467
+ assert.equal(isDetachedFailedReportAuthorizedForTest(failedReport, expected, 'linux'), true, 'late failure must be authenticated');
468
+ const sessions = fixture.run(['list-sessions', '-F', '#{session_name}']).split('\n');
469
+ assert.equal(sessions.includes(sessionName), false, 'the leader-owned late-failed session must be cleaned');
470
+ assert.equal(sessions.includes(foreignSessionName), true, 'the unrelated session must survive');
471
+ });
472
+ });
473
+ it('retries cleanup after an authenticated failed report while the leader is still live', async (t) => {
474
+ if (!skipUnlessTmux(t))
475
+ return;
476
+ await withTempTmuxSession(async (fixture) => {
477
+ const sessionName = 'omx-3578-failed-live-retry';
478
+ fixture.run(['new-session', '-d', '-s', sessionName, '-c', fixture.sessionName, 'sleep 5']);
479
+ fixture.run(['split-window', '-d', '-P', '-F', '#{pane_id}', '-t', sessionName, 'sleep 300']);
480
+ const authority = captureSession(fixture, sessionName, 'failed-live-owner');
481
+ fixture.run(['set-option', '-t', sessionName, '@omx_instance_id', authority.ownerId]);
482
+ assert.equal(fixture.run(['display-message', '-p', '-t', authority.paneId, '#{pane_dead}']), '0');
483
+ const foreignSessionName = `${sessionName}-foreign`;
484
+ fixture.run(['new-session', '-d', '-s', foreignSessionName, '-c', fixture.sessionName, 'sleep 300']);
485
+ let failedReport;
486
+ const failedExpected = {
487
+ nonce: 'failed-live-nonce',
488
+ sessionId: authority.sessionId,
489
+ sessionName,
490
+ leaderPaneId: authority.paneId,
491
+ leaderPanePid: authority.panePid,
492
+ };
493
+ cleanupDetachedPreReportSessionForTest(authority, () => {
494
+ // The authenticated failed report is observable before the leader
495
+ // exits. Cleanup must retain responsibility instead of returning
496
+ // success and deleting the marker.
497
+ failedReport = {
498
+ version: 1,
499
+ kind: 'failed',
500
+ nonce: failedExpected.nonce,
501
+ sessionId: failedExpected.sessionId,
502
+ sessionName: failedExpected.sessionName,
503
+ paneId: authority.paneId,
504
+ // Native Windows reports the spawned Node PID, not the
505
+ // PowerShell-owned tmux pane PID.
506
+ leaderPid: authority.panePid + 1,
507
+ };
508
+ }, false, () => false, undefined, true, () => isDetachedFailedReportAuthorizedForTest(failedReport, failedExpected, 'win32'));
509
+ assert.equal(isDetachedFailedReportAuthorizedForTest(failedReport, failedExpected, 'win32'), true);
510
+ assert.equal(isDetachedFailedReportAuthorizedForTest(failedReport, failedExpected, 'linux'), false);
511
+ const sessions = fixture.run(['list-sessions', '-F', '#{session_name}']).split('\n');
512
+ assert.equal(sessions.includes(sessionName), false, 'the exact failed session must be cleaned after leader exit');
513
+ assert.equal(sessions.includes(foreignSessionName), true, 'the unrelated session must survive retry');
514
+ });
515
+ });
516
+ it('preserves a respawned leader when its pane is revived after the dead-pane probe', async (t) => {
517
+ if (!skipUnlessTmux(t))
518
+ return;
519
+ await withTempTmuxSession(async (fixture) => {
520
+ const sessionName = 'omx-3578-respawn-race';
521
+ fixture.run(['new-session', '-d', '-s', sessionName, '-c', fixture.sessionName, 'sleep 5']);
522
+ fixture.run(['split-window', '-d', '-P', '-F', '#{pane_id}', '-t', sessionName, 'sleep 300']);
523
+ const authority = captureSession(fixture, sessionName, 'respawn-race-owner');
524
+ fixture.run(['set-option', '-t', sessionName, '@omx_instance_id', authority.ownerId]);
525
+ fixture.run(['set-option', '-t', sessionName, 'remain-on-exit', 'on']);
526
+ await new Promise((resolve) => setTimeout(resolve, 5_500));
527
+ assert.equal(fixture.run(['display-message', '-p', '-t', authority.paneId, '#{pane_dead}']), '1');
528
+ const foreignSessionName = `${sessionName}-foreign`;
529
+ fixture.run(['new-session', '-d', '-s', foreignSessionName, '-c', fixture.sessionName, 'sleep 300']);
530
+ let lateReadyReport;
531
+ const expected = {
532
+ nonce: 'respawn-race-nonce',
533
+ sessionId: authority.sessionId,
534
+ sessionName,
535
+ shouldAttach: false,
536
+ leaderPaneId: authority.paneId,
537
+ leaderPanePid: authority.panePid,
538
+ };
539
+ assert.throws(() => cleanupDetachedPreReportSessionForTest(authority, () => {
540
+ // respawn-pane retains the captured pane id but changes its
541
+ // process identity only after the non-atomic dead-pane probe.
542
+ fixture.run(['respawn-pane', '-k', '-t', authority.paneId, 'sleep 300']);
543
+ }, false, () => {
544
+ assert.equal(lateReadyReport, undefined);
545
+ return false;
546
+ }, () => {
547
+ const respawnedPid = Number(fixture.run(['display-message', '-p', '-t', authority.paneId, '#{pane_pid}']));
548
+ lateReadyReport = {
549
+ version: 1,
550
+ kind: 'ready',
551
+ nonce: expected.nonce,
552
+ sessionId: expected.sessionId,
553
+ sessionName: expected.sessionName,
554
+ paneId: authority.paneId,
555
+ leaderPid: respawnedPid,
556
+ };
557
+ }), /topology changed before cleanup/, 'a respawned pane with a changed PID must fail closed');
558
+ assert.equal(isDetachedReadyReportAuthorized(lateReadyReport, expected), true, 'the respawned ready report must still be pane-authenticated');
559
+ const sessions = fixture.run(['list-sessions', '-F', '#{session_name}']).split('\n');
560
+ assert.equal(sessions.includes(sessionName), true, 'the respawned live session must survive');
561
+ assert.equal(sessions.includes(foreignSessionName), true, 'the unrelated session must survive');
562
+ });
563
+ });
564
+ it('preserves a replacement session when the captured leader disappears and its name is reused after the probe', async (t) => {
565
+ if (!skipUnlessTmux(t))
566
+ return;
567
+ await withTempTmuxSession(async (fixture) => {
568
+ const sessionName = 'omx-3578-disappear-reuse-race';
569
+ fixture.run(['new-session', '-d', '-s', sessionName, '-c', fixture.sessionName, 'sleep 5']);
570
+ fixture.run(['split-window', '-d', '-P', '-F', '#{pane_id}', '-t', sessionName, 'sleep 300']);
571
+ const authority = captureSession(fixture, sessionName, 'owner-disappear-reuse');
572
+ fixture.run(['set-option', '-t', sessionName, '@omx_instance_id', authority.ownerId]);
573
+ fixture.run(['set-option', '-t', sessionName, 'remain-on-exit', 'on']);
574
+ await new Promise((resolve) => setTimeout(resolve, 5_500));
575
+ assert.equal(fixture.run(['display-message', '-p', '-t', authority.paneId, '#{pane_dead}']), '1', 'the captured leader pane must be dead before the interposed reuse');
576
+ const foreignSessionName = `${sessionName}-foreign`;
577
+ fixture.run(['new-session', '-d', '-s', foreignSessionName, '-c', fixture.sessionName, 'sleep 300']);
578
+ assert.throws(() => cleanupDetachedPreReportSessionForTest(authority, () => {
579
+ // The exact leader pane/session disappears only after the
580
+ // non-atomic topology probe has completed. The name is immediately
581
+ // reused by a different session, which must not receive cleanup.
582
+ fixture.run(['kill-session', '-t', sessionName]);
583
+ fixture.run(['new-session', '-d', '-s', sessionName, '-c', fixture.sessionName, 'sleep 300']);
584
+ fixture.run(['set-option', '-t', sessionName, '@omx_instance_id', 'replacement-owner']);
585
+ }), /topology changed before cleanup/, 'a vanished and name-reused leader must fail closed');
586
+ assert.equal(fixture.run(['list-sessions', '-F', '#{session_name}']).split('\n').includes(sessionName), true, 'replacement session must survive race');
587
+ assert.equal(fixture.run(['list-sessions', '-F', '#{session_name}']).split('\n').includes(foreignSessionName), true, 'unrelated session must survive race');
588
+ });
589
+ });
590
+ });
591
+ });
592
+ //# sourceMappingURL=detached-launch-diagnostics-3578.test.js.map