cli-jaw 2.2.15 → 2.2.17

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 (425) hide show
  1. package/README.md +23 -2
  2. package/dist/bin/cli-jaw.js +39 -0
  3. package/dist/bin/cli-jaw.js.map +1 -1
  4. package/dist/bin/commands/_shared/dashboard-client.js +42 -0
  5. package/dist/bin/commands/_shared/dashboard-client.js.map +1 -0
  6. package/dist/bin/commands/_shared/search-format.js +40 -0
  7. package/dist/bin/commands/_shared/search-format.js.map +1 -0
  8. package/dist/bin/commands/chat-search.js +5 -3
  9. package/dist/bin/commands/chat-search.js.map +1 -1
  10. package/dist/bin/commands/dashboard-chat.js +14 -31
  11. package/dist/bin/commands/dashboard-chat.js.map +1 -1
  12. package/dist/bin/commands/dashboard-memory.js +12 -49
  13. package/dist/bin/commands/dashboard-memory.js.map +1 -1
  14. package/dist/bin/commands/doctor.js +56 -11
  15. package/dist/bin/commands/doctor.js.map +1 -1
  16. package/dist/bin/commands/init.js +31 -1
  17. package/dist/bin/commands/init.js.map +1 -1
  18. package/dist/bin/commands/memory.js +5 -1
  19. package/dist/bin/commands/memory.js.map +1 -1
  20. package/dist/bin/commands/provider.js +23 -0
  21. package/dist/bin/commands/provider.js.map +1 -1
  22. package/dist/bin/commands/slack.js +57 -3
  23. package/dist/bin/commands/slack.js.map +1 -1
  24. package/dist/bin/commands/tui/input-handler.js +1 -1
  25. package/dist/bin/commands/tui/input-handler.js.map +1 -1
  26. package/dist/bin/commands/tui/slash-command-runner.js +4 -5
  27. package/dist/bin/commands/tui/slash-command-runner.js.map +1 -1
  28. package/dist/bin/postinstall.js +16 -18
  29. package/dist/bin/postinstall.js.map +1 -1
  30. package/dist/lib/media-kind.js +28 -0
  31. package/dist/lib/media-kind.js.map +1 -0
  32. package/dist/lib/upload.js +27 -10
  33. package/dist/lib/upload.js.map +1 -1
  34. package/dist/server.js +35 -51
  35. package/dist/server.js.map +1 -1
  36. package/dist/src/agent/args.js +34 -0
  37. package/dist/src/agent/args.js.map +1 -1
  38. package/dist/src/agent/codex-app-client.js +1016 -135
  39. package/dist/src/agent/codex-app-client.js.map +1 -1
  40. package/dist/src/agent/codex-app-events.js +72 -1
  41. package/dist/src/agent/codex-app-events.js.map +1 -1
  42. package/dist/src/agent/codex-host-pool.js +617 -0
  43. package/dist/src/agent/codex-host-pool.js.map +1 -0
  44. package/dist/src/agent/jwc-runtime.js +12 -4
  45. package/dist/src/agent/jwc-runtime.js.map +1 -1
  46. package/dist/src/agent/kiro-auth.js +10 -4
  47. package/dist/src/agent/kiro-auth.js.map +1 -1
  48. package/dist/src/agent/kiro-runtime.js +45 -8
  49. package/dist/src/agent/kiro-runtime.js.map +1 -1
  50. package/dist/src/agent/lifecycle-handler.js +75 -52
  51. package/dist/src/agent/lifecycle-handler.js.map +1 -1
  52. package/dist/src/agent/memory-flush-controller.js +241 -33
  53. package/dist/src/agent/memory-flush-controller.js.map +1 -1
  54. package/dist/src/agent/prompt-context.js +4 -0
  55. package/dist/src/agent/prompt-context.js.map +1 -1
  56. package/dist/src/agent/runtime-pool.js +64 -34
  57. package/dist/src/agent/runtime-pool.js.map +1 -1
  58. package/dist/src/agent/session-persistence.js +71 -15
  59. package/dist/src/agent/session-persistence.js.map +1 -1
  60. package/dist/src/agent/spawn/queue.js +319 -162
  61. package/dist/src/agent/spawn/queue.js.map +1 -1
  62. package/dist/src/agent/spawn.js +581 -268
  63. package/dist/src/agent/spawn.js.map +1 -1
  64. package/dist/src/bgtask/types.js +1 -1
  65. package/dist/src/bgtask/types.js.map +1 -1
  66. package/dist/src/browser/adaptive-fetch/safety.js +25 -0
  67. package/dist/src/browser/adaptive-fetch/safety.js.map +1 -1
  68. package/dist/src/browser/connection.js +2 -8
  69. package/dist/src/browser/connection.js.map +1 -1
  70. package/dist/src/cli/capability-probe-worker.js +172 -0
  71. package/dist/src/cli/capability-probe-worker.js.map +1 -0
  72. package/dist/src/cli/capability-probe.js +83 -0
  73. package/dist/src/cli/capability-probe.js.map +1 -0
  74. package/dist/src/cli/claude-models.js +2 -2
  75. package/dist/src/cli/claude-models.js.map +1 -1
  76. package/dist/src/cli/cli-status-worker.js +296 -0
  77. package/dist/src/cli/cli-status-worker.js.map +1 -0
  78. package/dist/src/cli/cli-status.js +81 -0
  79. package/dist/src/cli/cli-status.js.map +1 -0
  80. package/dist/src/cli/command-context.js +17 -5
  81. package/dist/src/cli/command-context.js.map +1 -1
  82. package/dist/src/cli/compact.js +56 -19
  83. package/dist/src/cli/compact.js.map +1 -1
  84. package/dist/src/cli/handlers-runtime.js +6 -4
  85. package/dist/src/cli/handlers-runtime.js.map +1 -1
  86. package/dist/src/cli/handlers.js +7 -2
  87. package/dist/src/cli/handlers.js.map +1 -1
  88. package/dist/src/cli/opencodex-models.js +5 -32
  89. package/dist/src/cli/opencodex-models.js.map +1 -1
  90. package/dist/src/cli/opencodex-runtime.js +82 -0
  91. package/dist/src/cli/opencodex-runtime.js.map +1 -0
  92. package/dist/src/cli/readiness.js +35 -18
  93. package/dist/src/cli/readiness.js.map +1 -1
  94. package/dist/src/cli/registry-live.js +8 -2
  95. package/dist/src/cli/registry-live.js.map +1 -1
  96. package/dist/src/cli/registry.js +1 -1
  97. package/dist/src/cli/registry.js.map +1 -1
  98. package/dist/src/core/browser-open-default.js +6 -4
  99. package/dist/src/core/browser-open-default.js.map +1 -1
  100. package/dist/src/core/browser-open.js +11 -15
  101. package/dist/src/core/browser-open.js.map +1 -1
  102. package/dist/src/core/bus.js +7 -1
  103. package/dist/src/core/bus.js.map +1 -1
  104. package/dist/src/core/chat-sessions.js +121 -12
  105. package/dist/src/core/chat-sessions.js.map +1 -1
  106. package/dist/src/core/cli-detect.js +148 -4
  107. package/dist/src/core/cli-detect.js.map +1 -1
  108. package/dist/src/core/codex-config.js +21 -0
  109. package/dist/src/core/codex-config.js.map +1 -1
  110. package/dist/src/core/compact.js +73 -21
  111. package/dist/src/core/compact.js.map +1 -1
  112. package/dist/src/core/config.js +449 -20
  113. package/dist/src/core/config.js.map +1 -1
  114. package/dist/src/core/db.js +225 -0
  115. package/dist/src/core/db.js.map +1 -1
  116. package/dist/src/core/event-bus.js +49 -4
  117. package/dist/src/core/event-bus.js.map +1 -1
  118. package/dist/src/core/event-scope.js +100 -0
  119. package/dist/src/core/event-scope.js.map +1 -0
  120. package/dist/src/core/install-integrity.js +176 -0
  121. package/dist/src/core/install-integrity.js.map +1 -0
  122. package/dist/src/core/main-session.js +64 -21
  123. package/dist/src/core/main-session.js.map +1 -1
  124. package/dist/src/core/message-summary.js +12 -2
  125. package/dist/src/core/message-summary.js.map +1 -1
  126. package/dist/src/core/platform-kind.js +139 -0
  127. package/dist/src/core/platform-kind.js.map +1 -0
  128. package/dist/src/core/rate-limit.js +156 -0
  129. package/dist/src/core/rate-limit.js.map +1 -0
  130. package/dist/src/core/runtime-settings.js +165 -17
  131. package/dist/src/core/runtime-settings.js.map +1 -1
  132. package/dist/src/core/session-context.js +7 -0
  133. package/dist/src/core/session-context.js.map +1 -0
  134. package/dist/src/core/session-ops.js +53 -9
  135. package/dist/src/core/session-ops.js.map +1 -1
  136. package/dist/src/core/settings-merge.js +105 -3
  137. package/dist/src/core/settings-merge.js.map +1 -1
  138. package/dist/src/core/settings-watch.js +29 -5
  139. package/dist/src/core/settings-watch.js.map +1 -1
  140. package/dist/src/core/widget-watcher.js +15 -2
  141. package/dist/src/core/widget-watcher.js.map +1 -1
  142. package/dist/src/discord/bot.js +5 -2
  143. package/dist/src/discord/bot.js.map +1 -1
  144. package/dist/src/discord/commands.js +4 -4
  145. package/dist/src/discord/commands.js.map +1 -1
  146. package/dist/src/goal/heartbeat.js +1 -1
  147. package/dist/src/goal/heartbeat.js.map +1 -1
  148. package/dist/src/goal/runtime.js +1 -1
  149. package/dist/src/goal/runtime.js.map +1 -1
  150. package/dist/src/goal-run/controller.js +2 -2
  151. package/dist/src/goal-run/controller.js.map +1 -1
  152. package/dist/src/manager/internal-fetch.js +15 -0
  153. package/dist/src/manager/internal-fetch.js.map +1 -0
  154. package/dist/src/manager/logs.js +2 -1
  155. package/dist/src/manager/logs.js.map +1 -1
  156. package/dist/src/manager/memory/chat-federation.js +104 -11
  157. package/dist/src/manager/memory/chat-federation.js.map +1 -1
  158. package/dist/src/manager/memory/federation.js +100 -0
  159. package/dist/src/manager/memory/federation.js.map +1 -1
  160. package/dist/src/manager/metadata.js +4 -1
  161. package/dist/src/manager/metadata.js.map +1 -1
  162. package/dist/src/manager/notes/assets.js +1 -1
  163. package/dist/src/manager/notes/assets.js.map +1 -1
  164. package/dist/src/manager/notes/frontmatter.js +5 -49
  165. package/dist/src/manager/notes/frontmatter.js.map +1 -1
  166. package/dist/src/manager/notes/link-resolver.js +2 -2
  167. package/dist/src/manager/notes/link-resolver.js.map +1 -1
  168. package/dist/src/manager/notes/remote-assets.js +1 -1
  169. package/dist/src/manager/notes/remote-assets.js.map +1 -1
  170. package/dist/src/manager/notes/routes.js +2 -2
  171. package/dist/src/manager/notes/routes.js.map +1 -1
  172. package/dist/src/manager/notes/store.js +2 -2
  173. package/dist/src/manager/notes/store.js.map +1 -1
  174. package/dist/src/manager/notes/trash.js +1 -1
  175. package/dist/src/manager/notes/trash.js.map +1 -1
  176. package/dist/src/manager/notes/vault-index.js +2 -2
  177. package/dist/src/manager/notes/vault-index.js.map +1 -1
  178. package/dist/src/manager/notes/wiki-links.js +1 -1
  179. package/dist/src/manager/notes/wiki-links.js.map +1 -1
  180. package/dist/src/manager/notes/wiki-routes.js +84 -0
  181. package/dist/src/manager/notes/wiki-routes.js.map +1 -0
  182. package/dist/src/manager/proxy.js +1 -0
  183. package/dist/src/manager/proxy.js.map +1 -1
  184. package/dist/src/manager/routes/dashboard-memory.js +24 -1
  185. package/dist/src/manager/routes/dashboard-memory.js.map +1 -1
  186. package/dist/src/manager/routes/runtime-monitor.js +1 -1
  187. package/dist/src/manager/routes/runtime-monitor.js.map +1 -1
  188. package/dist/src/manager/scan.js +5 -3
  189. package/dist/src/manager/scan.js.map +1 -1
  190. package/dist/src/manager/server.js +20 -6
  191. package/dist/src/manager/server.js.map +1 -1
  192. package/dist/src/manager/telegram-hub/hub-bot.js +4 -3
  193. package/dist/src/manager/telegram-hub/hub-bot.js.map +1 -1
  194. package/dist/src/manager/worker-events.js +2 -1
  195. package/dist/src/manager/worker-events.js.map +1 -1
  196. package/dist/src/memory/heartbeat.js +6 -5
  197. package/dist/src/memory/heartbeat.js.map +1 -1
  198. package/dist/src/memory/indexing.js +162 -45
  199. package/dist/src/memory/indexing.js.map +1 -1
  200. package/dist/src/messaging/channel-validate.js +14 -1
  201. package/dist/src/messaging/channel-validate.js.map +1 -1
  202. package/dist/src/messaging/redact.js +2 -1
  203. package/dist/src/messaging/redact.js.map +1 -1
  204. package/dist/src/messaging/session-key.js +21 -2
  205. package/dist/src/messaging/session-key.js.map +1 -1
  206. package/dist/src/notes/constants.js.map +1 -0
  207. package/dist/src/notes/frontmatter.js +59 -0
  208. package/dist/src/notes/frontmatter.js.map +1 -0
  209. package/dist/src/notes/path-guards.js.map +1 -0
  210. package/dist/src/notes/search.js.map +1 -0
  211. package/dist/src/orchestrator/attestation.js +1 -1
  212. package/dist/src/orchestrator/attestation.js.map +1 -1
  213. package/dist/src/orchestrator/gateway.js +135 -31
  214. package/dist/src/orchestrator/gateway.js.map +1 -1
  215. package/dist/src/orchestrator/pipeline.js +53 -21
  216. package/dist/src/orchestrator/pipeline.js.map +1 -1
  217. package/dist/src/orchestrator/scope.js +36 -2
  218. package/dist/src/orchestrator/scope.js.map +1 -1
  219. package/dist/src/orchestrator/session-lanes.js +87 -0
  220. package/dist/src/orchestrator/session-lanes.js.map +1 -0
  221. package/dist/src/orchestrator/session-work.js +25 -0
  222. package/dist/src/orchestrator/session-work.js.map +1 -0
  223. package/dist/src/orchestrator/state-machine.js +1 -5
  224. package/dist/src/orchestrator/state-machine.js.map +1 -1
  225. package/dist/src/orchestrator/worker-registry.js +20 -10
  226. package/dist/src/orchestrator/worker-registry.js.map +1 -1
  227. package/dist/src/prompt/builder.js +40 -2
  228. package/dist/src/prompt/builder.js.map +1 -1
  229. package/dist/src/prompt/templates/a1-system.md +8 -1
  230. package/dist/src/routes/agent-control.js +44 -8
  231. package/dist/src/routes/agent-control.js.map +1 -1
  232. package/dist/src/routes/chat-sessions.js +34 -6
  233. package/dist/src/routes/chat-sessions.js.map +1 -1
  234. package/dist/src/routes/command.js +63 -8
  235. package/dist/src/routes/command.js.map +1 -1
  236. package/dist/src/routes/events.js +11 -2
  237. package/dist/src/routes/events.js.map +1 -1
  238. package/dist/src/routes/link-preview.js +1 -27
  239. package/dist/src/routes/link-preview.js.map +1 -1
  240. package/dist/src/routes/messages.js +6 -5
  241. package/dist/src/routes/messages.js.map +1 -1
  242. package/dist/src/routes/messaging.js +45 -1
  243. package/dist/src/routes/messaging.js.map +1 -1
  244. package/dist/src/routes/orchestrate.js +53 -33
  245. package/dist/src/routes/orchestrate.js.map +1 -1
  246. package/dist/src/routes/search.js +84 -0
  247. package/dist/src/routes/search.js.map +1 -0
  248. package/dist/src/routes/session-request.js +29 -0
  249. package/dist/src/routes/session-request.js.map +1 -0
  250. package/dist/src/routes/settings.js +110 -3
  251. package/dist/src/routes/settings.js.map +1 -1
  252. package/dist/src/routes/static.js +30 -8
  253. package/dist/src/routes/static.js.map +1 -1
  254. package/dist/src/routes/system.js +1 -1
  255. package/dist/src/routes/system.js.map +1 -1
  256. package/dist/src/routes/wiki.js +122 -0
  257. package/dist/src/routes/wiki.js.map +1 -0
  258. package/dist/src/search/contract.js +2 -0
  259. package/dist/src/search/contract.js.map +1 -0
  260. package/dist/src/search/coordinator.js +153 -0
  261. package/dist/src/search/coordinator.js.map +1 -0
  262. package/dist/src/search/provider.js +40 -0
  263. package/dist/src/search/provider.js.map +1 -0
  264. package/dist/src/search/providers/chat.js +115 -0
  265. package/dist/src/search/providers/chat.js.map +1 -0
  266. package/dist/src/search/providers/memory.js +53 -0
  267. package/dist/src/search/providers/memory.js.map +1 -0
  268. package/dist/src/search/providers/wiki.js +80 -0
  269. package/dist/src/search/providers/wiki.js.map +1 -0
  270. package/dist/src/slack/api.js +10 -1
  271. package/dist/src/slack/api.js.map +1 -1
  272. package/dist/src/slack/attachment-recovery.js +42 -0
  273. package/dist/src/slack/attachment-recovery.js.map +1 -0
  274. package/dist/src/slack/bot.js +120 -41
  275. package/dist/src/slack/bot.js.map +1 -1
  276. package/dist/src/slack/commands.js +24 -8
  277. package/dist/src/slack/commands.js.map +1 -1
  278. package/dist/src/slack/events.js +12 -1
  279. package/dist/src/slack/events.js.map +1 -1
  280. package/dist/src/slack/history.js +108 -0
  281. package/dist/src/slack/history.js.map +1 -0
  282. package/dist/src/slack/inbound-file.js +242 -0
  283. package/dist/src/slack/inbound-file.js.map +1 -0
  284. package/dist/src/slack/inbound-url.js +39 -0
  285. package/dist/src/slack/inbound-url.js.map +1 -0
  286. package/dist/src/slack/ingress.js +140 -0
  287. package/dist/src/slack/ingress.js.map +1 -0
  288. package/dist/src/slack/manifest.js +3 -0
  289. package/dist/src/slack/manifest.js.map +1 -1
  290. package/dist/src/slack/socket.js +1 -1
  291. package/dist/src/slack/socket.js.map +1 -1
  292. package/dist/src/slack/thread-tracker.js +86 -0
  293. package/dist/src/slack/thread-tracker.js.map +1 -0
  294. package/dist/src/telegram/bot.js +21 -11
  295. package/dist/src/telegram/bot.js.map +1 -1
  296. package/dist/src/wiki/config.js +193 -0
  297. package/dist/src/wiki/config.js.map +1 -0
  298. package/dist/src/wiki/entities.js +61 -0
  299. package/dist/src/wiki/entities.js.map +1 -0
  300. package/dist/src/wiki/ontology.js +72 -0
  301. package/dist/src/wiki/ontology.js.map +1 -0
  302. package/dist/src/wiki/prompt.js +172 -0
  303. package/dist/src/wiki/prompt.js.map +1 -0
  304. package/dist/src/wiki/safe-traversal.js +198 -0
  305. package/dist/src/wiki/safe-traversal.js.map +1 -0
  306. package/dist/src/wiki/scaffold.js +77 -0
  307. package/dist/src/wiki/scaffold.js.map +1 -0
  308. package/package.json +6 -5
  309. package/public/css/session-hub.css +266 -0
  310. package/public/dist/assets/AdvancedExport-DYGcCAUq.js +1 -0
  311. package/public/dist/assets/Agent-CNNQ7KkJ.js +1 -0
  312. package/public/dist/assets/{DesignPanel-BGMPhZSx.js → DesignPanel-DcL369nB.js} +1 -1
  313. package/public/dist/assets/{DocPanel-Dz-J_l5M.js → DocPanel-K8A2pm_1.js} +1 -1
  314. package/public/dist/assets/{FolderPanel-DbPhzzvi.js → FolderPanel-CV52EzwG.js} +1 -1
  315. package/public/dist/assets/{MilkdownWysiwygEditor-Em7iC6y9.js → MilkdownWysiwygEditor-BKOCDI4Z.js} +1 -1
  316. package/public/dist/assets/api-B25zNNFJ.js +1 -0
  317. package/public/dist/assets/{app-DX4e6isX.css → app-DjgGeXhJ.css} +1 -1
  318. package/public/dist/assets/app-Dsgp4xpU.js +55 -0
  319. package/public/dist/assets/bgtask-badge-C6h5C-o4.js +1 -0
  320. package/public/dist/assets/{bgtask-badge-qMkKOZx0.js → bgtask-badge-CpavV0Jc.js} +4 -4
  321. package/public/dist/assets/{employees-BnobWM_2.js → employees-BJyEgim7.js} +4 -4
  322. package/public/dist/assets/idb-cache-B5PX63p7.js +1 -0
  323. package/public/dist/assets/{idb-cache-BzzftUET.js → idb-cache-lOntwU3S.js} +1 -1
  324. package/public/dist/assets/{iframe-renderer-C5vRbbl9.js → iframe-renderer-B8k3AvTv.js} +12 -12
  325. package/public/dist/assets/iframe-renderer-BO_GgCTR.js +1 -0
  326. package/public/dist/assets/manager-LkGs6qEJ.css +1 -0
  327. package/public/dist/assets/manager-kXYQyMhR.js +13 -0
  328. package/public/dist/assets/{memory-DF4kPwwC.js → memory-D2RbBVO-.js} +11 -11
  329. package/public/dist/assets/memory-amsvg4Ey.js +1 -0
  330. package/public/dist/assets/message-history-BGHZxvgV.js +1 -0
  331. package/public/dist/assets/message-history-Bw2E_b8L.js +1 -0
  332. package/public/dist/assets/pi-settings-5HVg1sXJ.js +1 -0
  333. package/public/dist/assets/render-BkGcpH7z.js +157 -0
  334. package/public/dist/assets/settings-B1FPORO2.js +56 -0
  335. package/public/dist/assets/settings-CMN-zN4Q.js +1 -0
  336. package/public/dist/assets/settings-core-B_OjGYVN.js +1 -0
  337. package/public/dist/assets/settings-core-CxRwqx86.js +144 -0
  338. package/public/dist/assets/sidebar-DW78DANx.js +49 -0
  339. package/public/dist/assets/skills-Dz-9Yrxr.js +1 -0
  340. package/public/dist/assets/{skills-IGquen_j.js → skills-lWi0ZFiV.js} +6 -6
  341. package/public/dist/assets/{slash-commands-ZRDOL99E.js → slash-commands-DYOI6nKj.js} +1 -1
  342. package/public/dist/assets/slash-commands-xJFCdTTM.js +1 -0
  343. package/public/dist/assets/{trace-drawer-D8To8fYt.js → trace-drawer-DBX9xzmt.js} +1 -1
  344. package/public/dist/assets/{ui-Hx_mNoDf.js → ui-Bx7eqnXt.js} +1 -1
  345. package/public/dist/assets/ui-JOcrYeG-.js +1 -0
  346. package/public/dist/assets/{wiki-link-suggestions-DZQc78gj.js → wiki-link-suggestions-Dp6kBQVu.js} +1 -1
  347. package/public/dist/index.html +4 -4
  348. package/public/dist/manager/index.html +2 -2
  349. package/public/index.html +3 -2
  350. package/public/js/event-channel.ts +25 -0
  351. package/public/js/features/channel-onboarding-flow.ts +16 -1
  352. package/public/js/features/channel-onboarding.ts +20 -2
  353. package/public/js/features/chat-messages.ts +3 -1
  354. package/public/js/features/chat-search.ts +2 -1
  355. package/public/js/features/chat.ts +56 -18
  356. package/public/js/features/elicitation.ts +15 -0
  357. package/public/js/features/idb-cache.ts +34 -0
  358. package/public/js/features/message-history.ts +2 -1
  359. package/public/js/features/session-hub.ts +334 -0
  360. package/public/js/features/settings-cli-status-render.ts +1 -1
  361. package/public/js/features/settings-cli-status.ts +25 -11
  362. package/public/js/features/settings-core.ts +72 -3
  363. package/public/js/features/settings-types.ts +32 -0
  364. package/public/js/features/ui-status.ts +2 -1
  365. package/public/js/main.ts +20 -0
  366. package/public/js/render/compose-block.ts +7 -0
  367. package/public/js/ws.ts +22 -2
  368. package/public/locales/en.json +46 -4
  369. package/public/locales/ja.json +46 -4
  370. package/public/locales/ko.json +46 -4
  371. package/public/locales/zh.json +46 -4
  372. package/public/manager/src/App.tsx +1 -1
  373. package/public/manager/src/InstancePreview.tsx +5 -1
  374. package/public/manager/src/api.ts +2 -1
  375. package/public/manager/src/components/InstanceGroups.tsx +10 -0
  376. package/public/manager/src/components/InstanceListContent.tsx +24 -1
  377. package/public/manager/src/components/InstanceRow.tsx +33 -3
  378. package/public/manager/src/components/InstanceSessionList.tsx +132 -0
  379. package/public/manager/src/hooks/useActiveSessionDisclosure.ts +45 -0
  380. package/public/manager/src/lib/resilient-fetch.ts +93 -0
  381. package/public/manager/src/lib/session-store.ts +216 -0
  382. package/public/manager/src/manager-components.css +97 -0
  383. package/public/manager/src/settings/pages/AdvancedExport.tsx +12 -2
  384. package/public/manager/src/settings/pages/Agent.tsx +113 -1
  385. package/public/manager/src/settings/pages/components/agent/agent-meta.ts +1 -1
  386. package/scripts/bundle-sidecar.sh +50 -2
  387. package/scripts/burst-scenario.mjs +72 -0
  388. package/scripts/check-redaction-sinks.mjs +1 -0
  389. package/scripts/check-strict-baseline.mjs +1 -1
  390. package/scripts/ensure-native-modules.cjs +50 -3
  391. package/scripts/install-risk-gate.mjs +8 -0
  392. package/scripts/install-wsl.sh +27 -2
  393. package/scripts/install.ps1 +121 -0
  394. package/scripts/install.sh +28 -2
  395. package/scripts/postinstall-guard.cjs +44 -2
  396. package/scripts/release-gates.mjs +12 -0
  397. package/dist/src/manager/notes/constants.js.map +0 -1
  398. package/dist/src/manager/notes/path-guards.js.map +0 -1
  399. package/dist/src/manager/notes/search.js.map +0 -1
  400. package/public/dist/assets/AdvancedExport-DsDpqXoH.js +0 -1
  401. package/public/dist/assets/Agent-BjbZVZ9p.js +0 -1
  402. package/public/dist/assets/api-C9dOxby7.js +0 -1
  403. package/public/dist/assets/app-BzD_ZLvt.js +0 -55
  404. package/public/dist/assets/bgtask-badge-Dqbp-Y4e.js +0 -1
  405. package/public/dist/assets/idb-cache-ZH6lOebk.js +0 -1
  406. package/public/dist/assets/iframe-renderer-Df4KIVyx.js +0 -1
  407. package/public/dist/assets/manager-C4eNDNFt.css +0 -1
  408. package/public/dist/assets/manager-DaN201mN.js +0 -13
  409. package/public/dist/assets/memory-D-bULTHl.js +0 -1
  410. package/public/dist/assets/message-history-C4WOisGx.js +0 -1
  411. package/public/dist/assets/message-history-ZBoza6ew.js +0 -1
  412. package/public/dist/assets/pi-settings-CBUGDAuT.js +0 -1
  413. package/public/dist/assets/render-DYX6GRQ0.js +0 -122
  414. package/public/dist/assets/settings-DbJpGoaX.js +0 -55
  415. package/public/dist/assets/settings-SreihUje.js +0 -1
  416. package/public/dist/assets/settings-core-B1Y5Nb4Z.js +0 -1
  417. package/public/dist/assets/settings-core-DobZWIZ7.js +0 -140
  418. package/public/dist/assets/sidebar-Cp22g0Gv.js +0 -49
  419. package/public/dist/assets/skills-DwPIP08e.js +0 -1
  420. package/public/dist/assets/slash-commands-BBVfwcnW.js +0 -1
  421. package/public/dist/assets/ui-CAicpUZc.js +0 -1
  422. /package/dist/src/{manager/notes → notes}/constants.js +0 -0
  423. /package/dist/src/{manager/notes → notes}/path-guards.js +0 -0
  424. /package/dist/src/{manager/notes → notes}/search.js +0 -0
  425. /package/public/dist/assets/{pi-settings-Dh5lgb87.js → pi-settings-DEuKgvIj.js} +0 -0
@@ -6,68 +6,221 @@ import { EventEmitter } from 'events';
6
6
  import { createInterface } from 'readline';
7
7
  // A bogus thread/resume on codex-cli 0.146.0 returns "no rollout found for thread id ...".
8
8
  const RECOVERABLE_RESUME_RE = /not found|no rollout found|unknown thread|no such thread|invalid thread|thread.*missing/i;
9
+ const TERMINAL_INTERRUPT_RACE_RE = /turn not active|already completed|no active turn|unknown turn/i;
10
+ const PENDING_NOTIFICATION_LIMIT = 128;
11
+ const PENDING_NOTIFICATION_TTL_MS = 5_000;
12
+ const TURN_OWNED_METHODS = new Set([
13
+ 'thread/tokenUsage/updated',
14
+ 'turn/diff/updated',
15
+ 'turn/plan/updated',
16
+ 'item/started',
17
+ 'item/autoApprovalReview/started',
18
+ 'item/autoApprovalReview/completed',
19
+ 'item/completed',
20
+ 'rawResponseItem/completed',
21
+ 'rawResponse/completed',
22
+ 'item/agentMessage/delta',
23
+ 'item/plan/delta',
24
+ 'item/commandExecution/outputDelta',
25
+ 'item/commandExecution/terminalInteraction',
26
+ 'item/fileChange/outputDelta',
27
+ 'item/fileChange/patchUpdated',
28
+ 'item/mcpToolCall/progress',
29
+ 'item/reasoning/summaryTextDelta',
30
+ 'item/reasoning/summaryPartAdded',
31
+ 'item/reasoning/textDelta',
32
+ 'thread/compacted',
33
+ 'model/rerouted',
34
+ 'model/verification',
35
+ 'turn/moderationMetadata',
36
+ 'model/safetyBuffering/updated',
37
+ ]);
38
+ const NULLABLE_TURN_METHODS = new Set([
39
+ 'thread/goal/updated',
40
+ 'hook/started',
41
+ 'hook/completed',
42
+ ]);
43
+ const THREAD_OWNED_METHODS = new Set([
44
+ 'thread/status/changed',
45
+ 'thread/archived',
46
+ 'thread/deleted',
47
+ 'thread/unarchived',
48
+ 'thread/closed',
49
+ 'thread/name/updated',
50
+ 'thread/goal/cleared',
51
+ 'thread/environment/connected',
52
+ 'thread/environment/disconnected',
53
+ 'thread/settings/updated',
54
+ 'serverRequest/resolved',
55
+ 'guardianWarning',
56
+ ]);
57
+ const REALTIME_THREAD_METHODS = new Set([
58
+ 'thread/realtime/started',
59
+ 'thread/realtime/itemAdded',
60
+ 'thread/realtime/transcript/delta',
61
+ 'thread/realtime/transcript/done',
62
+ 'thread/realtime/outputAudio/delta',
63
+ 'thread/realtime/sdp',
64
+ 'thread/realtime/error',
65
+ 'thread/realtime/closed',
66
+ ]);
67
+ const NULLABLE_THREAD_METHODS = new Set([
68
+ 'mcpServer/oauthLogin/completed',
69
+ 'mcpServer/startupStatus/updated',
70
+ 'warning',
71
+ ]);
72
+ const REQUEST_HOST_METHODS = new Set([
73
+ 'command/exec/outputDelta',
74
+ 'process/outputDelta',
75
+ 'process/exited',
76
+ 'externalAgentConfig/import/progress',
77
+ 'externalAgentConfig/import/completed',
78
+ 'fs/changed',
79
+ 'fuzzyFileSearch/sessionUpdated',
80
+ 'fuzzyFileSearch/sessionCompleted',
81
+ ]);
82
+ const PROCESS_HOST_METHODS = new Set([
83
+ 'skills/changed',
84
+ 'account/updated',
85
+ 'account/rateLimits/updated',
86
+ 'app/list/updated',
87
+ 'remoteControl/status/changed',
88
+ 'deprecationNotice',
89
+ 'configWarning',
90
+ 'windows/worldWritableWarning',
91
+ 'windowsSandbox/setupCompleted',
92
+ 'account/login/completed',
93
+ ]);
94
+ const TERMINAL_NOTIFICATION_METHODS = new Set([
95
+ 'error',
96
+ 'turn/completed',
97
+ 'thread/closed',
98
+ 'thread/deleted',
99
+ ]);
9
100
  export function isRecoverableResumeError(message) {
10
101
  return RECOVERABLE_RESUME_RE.test(message);
11
102
  }
12
- const TERMINAL_INTERRUPT_RACE_RE = /turn not active|already completed|no active turn|unknown turn/i;
13
103
  function isTerminalInterruptRaceError(err) {
14
104
  return TERMINAL_INTERRUPT_RACE_RE.test(err.message);
15
105
  }
16
106
  export class CodexAppClient extends EventEmitter {
17
107
  proc = null;
18
- threadId = null;
19
- activeTurnId = null;
20
108
  binary;
21
109
  workDir;
22
110
  spawnEnv;
23
- model;
24
- effort;
25
- fastMode;
111
+ unknownNotificationPolicy;
112
+ scopes = new Map();
113
+ threadToScope = new Map();
114
+ turnToScope = new Map();
115
+ scopeDisposers = new Map();
116
+ pendingNotifications = [];
117
+ pendingNotificationSequence = 0;
118
+ pendingNotificationTimer = null;
119
+ scopedNotificationHandoffs = new Map();
120
+ hostNotificationHandoff = {
121
+ consumers: 0,
122
+ replaying: false,
123
+ buffer: [],
124
+ };
125
+ pendingNotificationLimit = PENDING_NOTIFICATION_LIMIT;
126
+ pendingNotificationTtlMs = PENDING_NOTIFICATION_TTL_MS;
26
127
  nextId = 1;
27
128
  pending = new Map();
28
129
  rl = null;
29
130
  cleaned = false;
30
- pendingInterrupt = false;
131
+ terminal = false;
31
132
  constructor(options = {}) {
32
133
  super();
33
134
  this.binary = options.binary || 'codex';
34
135
  this.workDir = options.workDir || process.cwd();
35
136
  this.spawnEnv = options.env || process.env;
36
- this.model = options.model || 'gpt-5.5';
37
- this.effort = options.effort || 'medium';
38
- this.fastMode = options.fastMode ?? false;
137
+ this.unknownNotificationPolicy = options.unknownNotificationPolicy ?? 'legacy-raw';
39
138
  }
40
139
  get alive() {
41
140
  return this.proc !== null && this.proc.exitCode === null && !this.proc.killed;
42
141
  }
43
- listenTurn(handlers) {
44
- const onNotification = (method, params) => {
45
- handlers.onNotification(method, params);
142
+ getThreadId(scope) {
143
+ return this.scopes.get(scope)?.threadId ?? null;
144
+ }
145
+ getActiveTurnId(scope) {
146
+ return this.scopes.get(scope)?.activeTurnId ?? null;
147
+ }
148
+ listenTurn(scope, handlers) {
149
+ const handoff = this.ensureScopedNotificationHandoff(scope);
150
+ const role = handlers.role;
151
+ const onNotification = (method, params, owner) => {
152
+ if (role === 'consumer' && handoff?.replaying)
153
+ return;
154
+ handlers.onNotification(method, params, owner);
46
155
  };
47
156
  const onStderr = (text) => { handlers.onStderr(text); };
48
157
  const onExit = (code, signal) => { handlers.onExit?.(code, signal); };
49
158
  const onError = (err) => { handlers.onError?.(err); };
50
159
  const onInterruptFailed = (err) => { handlers.onInterruptFailed?.(err); };
51
- this.on('notification', onNotification);
160
+ this.on(`notification:${scope}`, onNotification);
52
161
  this.on('stderr', onStderr);
53
162
  this.on('exit', onExit);
54
163
  this.on('error', onError);
55
- this.on('interrupt-failed', onInterruptFailed);
164
+ this.on(`interrupt-failed:${scope}`, onInterruptFailed);
165
+ const scopedConsumerWasIdle = role === 'consumer' && handoff?.consumers === 0;
166
+ if (role === 'consumer' && handoff)
167
+ handoff.consumers += 1;
56
168
  let disposed = false;
57
- return {
58
- dispose: () => {
59
- if (disposed)
60
- return;
61
- disposed = true;
62
- this.off('notification', onNotification);
63
- this.off('stderr', onStderr);
64
- this.off('exit', onExit);
65
- this.off('error', onError);
66
- this.off('interrupt-failed', onInterruptFailed);
67
- },
169
+ const dispose = () => {
170
+ if (disposed)
171
+ return;
172
+ disposed = true;
173
+ this.off(`notification:${scope}`, onNotification);
174
+ this.off('stderr', onStderr);
175
+ this.off('exit', onExit);
176
+ this.off('error', onError);
177
+ this.off(`interrupt-failed:${scope}`, onInterruptFailed);
178
+ if (role === 'consumer' && handoff) {
179
+ handoff.consumers = Math.max(0, handoff.consumers - 1);
180
+ }
181
+ const disposers = this.scopeDisposers.get(scope);
182
+ disposers?.delete(dispose);
183
+ if (disposers?.size === 0)
184
+ this.scopeDisposers.delete(scope);
68
185
  };
186
+ const disposers = this.scopeDisposers.get(scope) ?? new Set();
187
+ disposers.add(dispose);
188
+ this.scopeDisposers.set(scope, disposers);
189
+ // The handover runs a caller-supplied callback, so it happens only after
190
+ // registration is complete and a disposer exists. A throwing handler
191
+ // would otherwise leave the listener attached with no way to detach it,
192
+ // and with queueing switched off for good.
193
+ if (scopedConsumerWasIdle && handoff && handoff.buffer.length > 0) {
194
+ this.replayNotificationHandoff(handoff, handlers.onNotification, dispose);
195
+ }
196
+ return { dispose };
197
+ }
198
+ listenHostNotifications(handlers) {
199
+ const listener = (method, params) => {
200
+ if (this.hostNotificationHandoff.replaying)
201
+ return;
202
+ handlers.onNotification(method, params);
203
+ };
204
+ this.on('host-notification', listener);
205
+ this.on('notification', listener);
206
+ const wasIdle = this.hostNotificationHandoff.consumers === 0;
207
+ this.hostNotificationHandoff.consumers += 1;
208
+ let disposed = false;
209
+ const dispose = () => {
210
+ if (disposed)
211
+ return;
212
+ disposed = true;
213
+ this.off('host-notification', listener);
214
+ this.off('notification', listener);
215
+ this.hostNotificationHandoff.consumers = Math.max(0, this.hostNotificationHandoff.consumers - 1);
216
+ };
217
+ if (wasIdle && this.hostNotificationHandoff.buffer.length > 0) {
218
+ this.replayNotificationHandoff(this.hostNotificationHandoff, handlers.onNotification, dispose);
219
+ }
220
+ return { dispose };
69
221
  }
70
222
  spawn() {
223
+ this.assertReusable();
71
224
  const isCmdShim = process.platform === 'win32' && !this.binary.toLowerCase().endsWith('.exe');
72
225
  this.proc = spawn(this.binary, ['app-server', '--listen', 'stdio://'], {
73
226
  cwd: this.workDir,
@@ -81,13 +234,11 @@ export class CodexAppClient extends EventEmitter {
81
234
  this.proc.stderr?.on('data', (chunk) => {
82
235
  this.emit('stderr', chunk.toString());
83
236
  });
84
- this.proc.on('error', (err) => this.emit('error', err));
85
- this.proc.on('exit', (code, signal) => {
86
- this.rejectAllPending('Process exited');
87
- this.emit('exit', code, signal);
88
- });
237
+ this.proc.on('error', (err) => { this.handleProcessError(err); });
238
+ this.proc.on('exit', (code, signal) => { this.handleProcessExit(code, signal); });
89
239
  }
90
240
  async initialize() {
241
+ this.assertReusable();
91
242
  const result = await this.request('initialize', {
92
243
  clientInfo: {
93
244
  name: 'cli_jaw_codex_app_server',
@@ -105,60 +256,42 @@ export class CodexAppClient extends EventEmitter {
105
256
  this.notify('initialized', {});
106
257
  return result;
107
258
  }
108
- async startThread(options = {}) {
109
- const result = await this.request('thread/start', {
110
- model: this.model,
111
- approvalPolicy: 'never',
112
- sandbox: 'danger-full-access',
113
- cwd: options.cwd || this.workDir,
114
- config: this.reasoningConfig(),
115
- ...(options.instructions ? { developerInstructions: options.instructions } : {}),
116
- });
117
- this.threadId = result.thread.id;
118
- return this.threadId;
259
+ async startThread(scope, options) {
260
+ return this.startThreadScoped(scope, options);
119
261
  }
120
- async resumeThread(threadId, options = {}) {
121
- const result = await this.request('thread/resume', {
122
- threadId,
123
- model: this.model,
124
- approvalPolicy: 'never',
125
- sandbox: 'danger-full-access',
126
- cwd: this.workDir,
127
- config: this.reasoningConfig(),
128
- excludeTurns: true,
129
- ...(options.instructions ? { developerInstructions: options.instructions } : {}),
130
- });
131
- this.threadId = result.thread.id;
132
- return this.threadId;
133
- }
134
- async startTurn(prompt) {
135
- if (!this.threadId)
136
- throw new Error('No active thread');
137
- const result = await this.request('turn/start', {
138
- threadId: this.threadId,
139
- input: [{
140
- type: 'text',
141
- text: prompt,
142
- text_elements: [],
143
- }],
144
- effort: this.effort || undefined,
145
- summary: 'detailed',
146
- });
147
- this.setActiveTurnId(result.turn?.id ?? null);
262
+ async resumeThread(scope, threadId, options) {
263
+ return this.resumeThreadScoped(scope, threadId, options);
264
+ }
265
+ async startTurn(scope, prompt) {
266
+ return this.startTurnScoped(scope, prompt);
148
267
  }
149
- async interruptTurn() {
150
- if (!this.threadId)
268
+ async interruptTurn(scope) {
269
+ const state = this.requireScope(scope);
270
+ if (!state.threadId)
151
271
  return;
152
- if (!this.activeTurnId) {
153
- this.pendingInterrupt = true;
272
+ if (!state.activeTurnId) {
273
+ state.pendingInterrupt = true;
154
274
  return;
155
275
  }
156
- await this.request('turn/interrupt', {
157
- threadId: this.threadId,
158
- turnId: this.activeTurnId,
159
- });
276
+ await this.sendInterrupt(state, state.activeTurnId);
277
+ }
278
+ async closeScope(scope) {
279
+ const state = this.requireScope(scope);
280
+ if (state.operation !== 'idle' || state.activeTurnId) {
281
+ throw new Error(`Cannot close active scope ${scope}`);
282
+ }
283
+ state.operation = 'closing';
284
+ try {
285
+ if (!this.terminal && state.threadId) {
286
+ await this.request('thread/unsubscribe', { threadId: state.threadId }).catch(() => { });
287
+ }
288
+ }
289
+ finally {
290
+ this.removeScope(scope);
291
+ }
160
292
  }
161
293
  async listModels(options = {}) {
294
+ this.assertReusable();
162
295
  const all = [];
163
296
  let cursor = null;
164
297
  do {
@@ -173,20 +306,21 @@ export class CodexAppClient extends EventEmitter {
173
306
  }
174
307
  async closeGracefully() {
175
308
  try {
176
- if (this.threadId) {
177
- await this.request('thread/unsubscribe', { threadId: this.threadId }).catch(() => { });
309
+ if (!this.terminal) {
310
+ const ids = new Set([...this.scopes.values()].flatMap((state) => state.threadId ? [state.threadId] : []));
311
+ await Promise.all([...ids].map((threadId) => this.request('thread/unsubscribe', { threadId }).catch(() => { })));
178
312
  }
179
313
  }
180
314
  catch { /* best effort */ }
181
315
  this.proc?.stdin?.end();
182
- await new Promise((r) => {
183
- const t = setTimeout(() => { this.kill(); r(); }, 3000);
316
+ await new Promise((resolve) => {
317
+ const timer = setTimeout(() => { this.kill(); resolve(); }, 3_000);
184
318
  if (this.proc) {
185
- this.proc.once('exit', () => { clearTimeout(t); r(); });
319
+ this.proc.once('exit', () => { clearTimeout(timer); resolve(); });
186
320
  }
187
321
  else {
188
- clearTimeout(t);
189
- r();
322
+ clearTimeout(timer);
323
+ resolve();
190
324
  }
191
325
  });
192
326
  }
@@ -196,19 +330,718 @@ export class CodexAppClient extends EventEmitter {
196
330
  setTimeout(() => {
197
331
  if (this.proc && !this.proc.killed)
198
332
  this.proc.kill('SIGKILL');
199
- }, 2000);
333
+ }, 2_000);
200
334
  }
201
335
  }
202
336
  cleanup() {
203
337
  if (this.cleaned)
204
338
  return;
205
339
  this.cleaned = true;
206
- this.rejectAllPending('Client cleanup');
340
+ if (this.beginProcessDeath('Client cleanup'))
341
+ this.finalizeProcessDeath();
207
342
  this.rl?.close();
208
343
  this.rl = null;
209
344
  this.removeAllListeners();
210
345
  }
211
- // ─── Internal ─────────────────────────────────
346
+ // ─── Scoped operations ───────────────────────────
347
+ async startThreadScoped(scope, options) {
348
+ const state = this.beginScopeRebind(scope, options);
349
+ const operation = this.beginPendingOperation(state, 'binding');
350
+ try {
351
+ const result = await Promise.race([
352
+ this.request('thread/start', {
353
+ model: state.model,
354
+ approvalPolicy: 'never',
355
+ sandbox: 'danger-full-access',
356
+ cwd: state.cwd,
357
+ config: this.reasoningConfig(state),
358
+ ...(state.instructions ? { developerInstructions: state.instructions } : {}),
359
+ }),
360
+ operation.failure,
361
+ ]);
362
+ this.bindThread(scope, result.thread.id);
363
+ this.throwIfOperationFailed(operation);
364
+ state.operation = 'idle';
365
+ return result.thread.id;
366
+ }
367
+ catch (err) {
368
+ if (state.operation !== 'terminal')
369
+ state.operation = 'idle';
370
+ throw err;
371
+ }
372
+ finally {
373
+ if (state.pendingOperation === operation)
374
+ state.pendingOperation = null;
375
+ }
376
+ }
377
+ async resumeThreadScoped(scope, threadId, options) {
378
+ const state = this.beginScopeRebind(scope, options);
379
+ const operation = this.beginPendingOperation(state, 'binding');
380
+ try {
381
+ const result = await Promise.race([
382
+ this.request('thread/resume', {
383
+ threadId,
384
+ model: state.model,
385
+ approvalPolicy: 'never',
386
+ sandbox: 'danger-full-access',
387
+ cwd: state.cwd,
388
+ config: this.reasoningConfig(state),
389
+ excludeTurns: true,
390
+ ...(state.instructions ? { developerInstructions: state.instructions } : {}),
391
+ }),
392
+ operation.failure,
393
+ ]);
394
+ this.bindThread(scope, result.thread.id);
395
+ this.throwIfOperationFailed(operation);
396
+ state.operation = 'idle';
397
+ return result.thread.id;
398
+ }
399
+ catch (err) {
400
+ if (state.operation !== 'terminal')
401
+ state.operation = 'idle';
402
+ throw err;
403
+ }
404
+ finally {
405
+ if (state.pendingOperation === operation)
406
+ state.pendingOperation = null;
407
+ }
408
+ }
409
+ async startTurnScoped(scope, prompt) {
410
+ this.assertReusable();
411
+ const state = this.requireScope(scope);
412
+ if (state.operation !== 'idle') {
413
+ throw new Error(`Scope ${scope} already has ${state.operation} operation`);
414
+ }
415
+ if (!state.threadId)
416
+ throw new Error(`No active thread for scope ${scope}`);
417
+ state.operation = 'turn';
418
+ const operation = this.beginPendingOperation(state, 'turn');
419
+ try {
420
+ const result = await Promise.race([
421
+ this.request('turn/start', {
422
+ threadId: state.threadId,
423
+ input: [{
424
+ type: 'text',
425
+ text: prompt,
426
+ text_elements: [],
427
+ }],
428
+ ...(state.effort ? { effort: state.effort } : {}),
429
+ summary: 'detailed',
430
+ }),
431
+ operation.failure,
432
+ ]);
433
+ const turnId = result.turn?.id;
434
+ if (!turnId)
435
+ throw new Error(`turn/start returned no turn id for scope ${scope}`);
436
+ // The notification stream may have started and finished this turn
437
+ // while the response was still travelling. Rebinding it here would
438
+ // make a completed turn active again and block every turn after it.
439
+ if (operation.settledTurnIds.has(turnId)) {
440
+ this.throwIfOperationFailed(operation);
441
+ return;
442
+ }
443
+ if (state.activeTurnId && state.activeTurnId !== turnId) {
444
+ throw new Error(`turn/start response ${turnId} conflicts with active turn ${state.activeTurnId} for scope ${scope}`);
445
+ }
446
+ if (!state.activeTurnId)
447
+ this.bindTurn(scope, turnId);
448
+ this.throwIfOperationFailed(operation);
449
+ }
450
+ catch (err) {
451
+ // A terminal notification can end this turn and let the next one
452
+ // start before this request settles. Cleaning up unconditionally
453
+ // would then tear down a turn that is already running, so only the
454
+ // operation still owned by the lane may touch its state.
455
+ if (!this.terminal && state.pendingOperation === operation) {
456
+ this.clearActiveTurn(state);
457
+ state.operation = 'idle';
458
+ }
459
+ throw err;
460
+ }
461
+ finally {
462
+ if (state.pendingOperation === operation)
463
+ state.pendingOperation = null;
464
+ }
465
+ }
466
+ // ─── Lane lifecycle ──────────────────────────────
467
+ beginScopeRebind(scope, options) {
468
+ this.assertReusable();
469
+ const current = this.scopes.get(scope);
470
+ if (current && (current.operation !== 'idle' || current.activeTurnId)) {
471
+ throw new Error(`Scope ${scope} already has ${current.operation} operation`);
472
+ }
473
+ if (current)
474
+ this.clearScopeBinding(current);
475
+ const state = current ?? this.createScope(scope, options);
476
+ state.model = options.model;
477
+ state.effort = options.effort;
478
+ state.cwd = options.cwd;
479
+ state.fastMode = options.fastMode;
480
+ if (options.instructions !== undefined)
481
+ state.instructions = options.instructions;
482
+ else
483
+ delete state.instructions;
484
+ state.threadId = null;
485
+ state.activeTurnId = null;
486
+ state.pendingInterrupt = false;
487
+ state.operation = 'binding';
488
+ state.pendingOperation = null;
489
+ this.scopes.set(scope, state);
490
+ this.removeBufferedForScope(scope);
491
+ return state;
492
+ }
493
+ bindThread(scope, threadId) {
494
+ const state = this.requireScope(scope);
495
+ const owner = this.threadToScope.get(threadId);
496
+ if (owner && owner !== scope) {
497
+ throw new Error(`Thread ${threadId} is already bound to scope ${owner}`);
498
+ }
499
+ if (state.threadId && state.threadId !== threadId)
500
+ this.clearScopeBinding(state);
501
+ state.threadId = threadId;
502
+ this.threadToScope.set(threadId, scope);
503
+ this.replayPendingForScope(scope);
504
+ }
505
+ bindTurn(scope, turnId, replayPending = true) {
506
+ const state = this.requireScope(scope);
507
+ if (!state.threadId)
508
+ throw new Error(`No active thread for scope ${scope}`);
509
+ const owner = this.turnToScope.get(turnId);
510
+ if (owner && owner !== scope) {
511
+ throw new Error(`Turn ${turnId} is already bound to scope ${owner}`);
512
+ }
513
+ if (state.activeTurnId && state.activeTurnId !== turnId) {
514
+ this.turnToScope.delete(state.activeTurnId);
515
+ }
516
+ state.activeTurnId = turnId;
517
+ this.turnToScope.set(turnId, scope);
518
+ if (state.pendingInterrupt) {
519
+ state.pendingInterrupt = false;
520
+ void this.sendInterrupt(state, turnId).catch(() => { });
521
+ }
522
+ if (replayPending)
523
+ this.replayPendingForScope(scope);
524
+ }
525
+ clearActiveTurn(state) {
526
+ // Record the id before dropping it. The turn/start response may still be
527
+ // in flight, and it must not resurrect a turn that has already ended.
528
+ if (state.activeTurnId && state.pendingOperation?.kind === 'turn') {
529
+ state.pendingOperation.settledTurnIds.add(state.activeTurnId);
530
+ }
531
+ if (state.activeTurnId && this.turnToScope.get(state.activeTurnId) === state.scope) {
532
+ this.turnToScope.delete(state.activeTurnId);
533
+ }
534
+ state.activeTurnId = null;
535
+ state.pendingInterrupt = false;
536
+ if (state.operation === 'turn')
537
+ state.operation = 'idle';
538
+ }
539
+ clearScopeBinding(state) {
540
+ if (state.threadId && this.threadToScope.get(state.threadId) === state.scope) {
541
+ this.threadToScope.delete(state.threadId);
542
+ }
543
+ if (state.activeTurnId && this.turnToScope.get(state.activeTurnId) === state.scope) {
544
+ this.turnToScope.delete(state.activeTurnId);
545
+ }
546
+ state.threadId = null;
547
+ state.activeTurnId = null;
548
+ state.pendingInterrupt = false;
549
+ }
550
+ removeScope(scope) {
551
+ const state = this.scopes.get(scope);
552
+ if (state)
553
+ this.clearScopeBinding(state);
554
+ for (const dispose of [...(this.scopeDisposers.get(scope) ?? [])])
555
+ dispose();
556
+ this.scopeDisposers.delete(scope);
557
+ this.removeAllListeners(`notification:${scope}`);
558
+ this.removeAllListeners(`interrupt-failed:${scope}`);
559
+ this.removeBufferedForScope(scope);
560
+ this.scopedNotificationHandoffs.delete(scope);
561
+ this.scopes.delete(scope);
562
+ }
563
+ createScope(scope, options) {
564
+ return {
565
+ ...options,
566
+ scope,
567
+ threadId: null,
568
+ activeTurnId: null,
569
+ pendingInterrupt: false,
570
+ operation: 'idle',
571
+ pendingOperation: null,
572
+ };
573
+ }
574
+ requireScope(scope) {
575
+ this.assertReusable();
576
+ const state = this.scopes.get(scope);
577
+ if (!state)
578
+ throw new Error(`Unknown scope ${scope}`);
579
+ if (state.operation === 'terminal')
580
+ throw new Error(`Scope ${scope} is terminal`);
581
+ return state;
582
+ }
583
+ beginPendingOperation(state, kind) {
584
+ let reject;
585
+ const failure = new Promise((_resolve, rejectPromise) => {
586
+ reject = rejectPromise;
587
+ });
588
+ const operation = {
589
+ kind, failure, reject, failed: null, settledTurnIds: new Set(),
590
+ };
591
+ state.pendingOperation = operation;
592
+ return operation;
593
+ }
594
+ failPendingOperation(scope, err) {
595
+ const state = this.scopes.get(scope);
596
+ const operation = state?.pendingOperation;
597
+ if (!state || !operation || operation.failed)
598
+ return false;
599
+ operation.failed = err;
600
+ operation.reject(err);
601
+ return true;
602
+ }
603
+ throwIfOperationFailed(operation) {
604
+ if (operation.failed)
605
+ throw operation.failed;
606
+ }
607
+ assertReusable() {
608
+ if (this.terminal)
609
+ throw new Error('Codex App client is terminal');
610
+ }
611
+ // ─── Notification routing ────────────────────────
612
+ routeNotification(method, params, allowBuffer = true) {
613
+ this.expirePendingNotifications();
614
+ if (method === 'error') {
615
+ this.routeError(params, allowBuffer);
616
+ return;
617
+ }
618
+ if (method === 'turn/started' || method === 'turn/completed') {
619
+ this.routeTurnLifecycle(method, params, allowBuffer);
620
+ return;
621
+ }
622
+ if (TURN_OWNED_METHODS.has(method)) {
623
+ this.routeTurnOwned(method, params, allowBuffer);
624
+ return;
625
+ }
626
+ if (NULLABLE_TURN_METHODS.has(method)) {
627
+ this.routeNullableTurn(method, params, allowBuffer);
628
+ return;
629
+ }
630
+ if (method === 'thread/started') {
631
+ this.routeThreadStarted(params, allowBuffer);
632
+ return;
633
+ }
634
+ if (THREAD_OWNED_METHODS.has(method) || REALTIME_THREAD_METHODS.has(method)) {
635
+ this.routeThreadOwned(method, params, allowBuffer);
636
+ return;
637
+ }
638
+ if (NULLABLE_THREAD_METHODS.has(method)) {
639
+ this.routeNullableThread(method, params, allowBuffer);
640
+ return;
641
+ }
642
+ if (REQUEST_HOST_METHODS.has(method) || PROCESS_HOST_METHODS.has(method)) {
643
+ this.emitHostNotification(method, params);
644
+ return;
645
+ }
646
+ this.emitUnrouted(method, params, 'unknown-method');
647
+ if (this.unknownNotificationPolicy === 'legacy-raw') {
648
+ this.recordHostNotification(method, params);
649
+ this.emit('notification', method, params);
650
+ }
651
+ }
652
+ routeError(params, allowBuffer) {
653
+ const threadId = this.stringField(params, 'threadId');
654
+ const turnId = this.stringField(params, 'turnId');
655
+ if (!threadId || !turnId) {
656
+ this.emitHostNotification('error', params);
657
+ this.emit('server-error', params);
658
+ return;
659
+ }
660
+ const scope = this.resolveTurnOwner(threadId, turnId);
661
+ if (!scope) {
662
+ if (allowBuffer && this.shouldBufferTurn(threadId)) {
663
+ this.bufferNotification('error', params, threadId, turnId);
664
+ }
665
+ else {
666
+ this.emitUnrouted('error', params, 'turn-owner-mismatch');
667
+ }
668
+ return;
669
+ }
670
+ this.emitLaneNotification(scope, 'error', params, { threadId, turnId });
671
+ if (params['willRetry'] !== true) {
672
+ const state = this.scopes.get(scope);
673
+ if (state)
674
+ this.clearActiveTurn(state);
675
+ }
676
+ }
677
+ routeTurnLifecycle(method, params, allowBuffer) {
678
+ const threadId = this.stringField(params, 'threadId');
679
+ const turn = this.recordField(params, 'turn');
680
+ const turnId = turn ? this.stringField(turn, 'id') : null;
681
+ if (!threadId || !turnId) {
682
+ this.emitUnrouted(method, params, 'malformed-turn-lifecycle');
683
+ return;
684
+ }
685
+ if (method === 'turn/started') {
686
+ const threadScope = this.threadToScope.get(threadId);
687
+ const state = threadScope ? this.scopes.get(threadScope) : undefined;
688
+ if (threadScope
689
+ && state?.threadId === threadId
690
+ && (state.operation === 'turn' || state.pendingInterrupt)
691
+ && (!state.activeTurnId || state.activeTurnId === turnId)) {
692
+ if (state.operation === 'idle')
693
+ state.operation = 'turn';
694
+ if (!state.activeTurnId)
695
+ this.bindTurn(threadScope, turnId, allowBuffer);
696
+ if (this.resolveTurnOwner(threadId, turnId) === threadScope) {
697
+ this.emitLaneNotification(threadScope, method, params, { threadId, turnId });
698
+ }
699
+ return;
700
+ }
701
+ if (allowBuffer && this.shouldBufferTurn(threadId)) {
702
+ this.bufferNotification(method, params, threadId, turnId);
703
+ }
704
+ else {
705
+ this.emitUnrouted(method, params, 'turn-start-owner-mismatch');
706
+ }
707
+ return;
708
+ }
709
+ const scope = this.resolveTurnOwner(threadId, turnId);
710
+ if (!scope) {
711
+ if (allowBuffer && this.shouldBufferTurn(threadId)) {
712
+ this.bufferNotification(method, params, threadId, turnId);
713
+ }
714
+ else {
715
+ this.emitUnrouted(method, params, 'turn-complete-owner-mismatch');
716
+ }
717
+ return;
718
+ }
719
+ this.emitLaneNotification(scope, method, params, { threadId, turnId });
720
+ const state = this.scopes.get(scope);
721
+ if (state)
722
+ this.clearActiveTurn(state);
723
+ }
724
+ routeTurnOwned(method, params, allowBuffer) {
725
+ const threadId = this.stringField(params, 'threadId');
726
+ const turnId = this.stringField(params, 'turnId');
727
+ if (!threadId || !turnId) {
728
+ this.emitUnrouted(method, params, 'malformed-turn-owner');
729
+ return;
730
+ }
731
+ const scope = this.resolveTurnOwner(threadId, turnId);
732
+ if (scope) {
733
+ this.emitLaneNotification(scope, method, params, { threadId, turnId });
734
+ return;
735
+ }
736
+ if (allowBuffer && this.shouldBufferTurn(threadId)) {
737
+ this.bufferNotification(method, params, threadId, turnId);
738
+ }
739
+ else {
740
+ this.emitUnrouted(method, params, 'turn-owner-mismatch');
741
+ }
742
+ }
743
+ routeNullableTurn(method, params, allowBuffer) {
744
+ const threadId = this.stringField(params, 'threadId');
745
+ if (!threadId || !Object.hasOwn(params, 'turnId')) {
746
+ this.emitUnrouted(method, params, 'malformed-nullable-turn-owner');
747
+ return;
748
+ }
749
+ if (params['turnId'] === null) {
750
+ this.routeThreadIdentity(method, params, threadId, allowBuffer);
751
+ return;
752
+ }
753
+ const turnId = this.stringField(params, 'turnId');
754
+ if (!turnId) {
755
+ this.emitUnrouted(method, params, 'malformed-nullable-turn-owner');
756
+ return;
757
+ }
758
+ const scope = this.resolveTurnOwner(threadId, turnId);
759
+ if (scope) {
760
+ this.emitLaneNotification(scope, method, params, { threadId, turnId });
761
+ }
762
+ else if (allowBuffer && this.shouldBufferTurn(threadId)) {
763
+ this.bufferNotification(method, params, threadId, turnId);
764
+ }
765
+ else {
766
+ this.emitUnrouted(method, params, 'turn-owner-mismatch');
767
+ }
768
+ }
769
+ routeThreadStarted(params, allowBuffer) {
770
+ const thread = this.recordField(params, 'thread');
771
+ const threadId = thread ? this.stringField(thread, 'id') : null;
772
+ if (!threadId) {
773
+ this.emitUnrouted('thread/started', params, 'malformed-thread-lifecycle');
774
+ return;
775
+ }
776
+ this.routeThreadIdentity('thread/started', params, threadId, allowBuffer);
777
+ }
778
+ routeThreadOwned(method, params, allowBuffer) {
779
+ const threadId = this.stringField(params, 'threadId');
780
+ if (!threadId) {
781
+ this.emitUnrouted(method, params, 'malformed-thread-owner');
782
+ return;
783
+ }
784
+ this.routeThreadIdentity(method, params, threadId, allowBuffer);
785
+ }
786
+ routeThreadIdentity(method, params, threadId, allowBuffer) {
787
+ const scope = this.threadToScope.get(threadId);
788
+ const state = scope ? this.scopes.get(scope) : undefined;
789
+ if (scope && state?.threadId === threadId) {
790
+ this.emitLaneNotification(scope, method, params, { threadId, turnId: null });
791
+ if (method === 'thread/closed' || method === 'thread/deleted') {
792
+ const pendingKind = state.pendingOperation?.kind;
793
+ this.clearScopeBinding(state);
794
+ if (state.operation !== 'terminal')
795
+ state.operation = 'idle';
796
+ if (pendingKind) {
797
+ this.failPendingOperation(scope, new Error(`Thread ${threadId} ${method === 'thread/closed' ? 'closed' : 'was deleted'} during ${pendingKind}`));
798
+ }
799
+ }
800
+ return;
801
+ }
802
+ if (allowBuffer && this.hasPendingBinding()) {
803
+ this.bufferNotification(method, params, threadId, null);
804
+ }
805
+ else {
806
+ this.emitUnrouted(method, params, 'thread-owner-mismatch');
807
+ }
808
+ }
809
+ routeNullableThread(method, params, allowBuffer) {
810
+ if (!Object.hasOwn(params, 'threadId')) {
811
+ this.emitUnrouted(method, params, 'malformed-nullable-thread-owner');
812
+ return;
813
+ }
814
+ if (params['threadId'] === null) {
815
+ this.emitHostNotification(method, params);
816
+ return;
817
+ }
818
+ const threadId = this.stringField(params, 'threadId');
819
+ if (!threadId) {
820
+ this.emitUnrouted(method, params, 'malformed-nullable-thread-owner');
821
+ return;
822
+ }
823
+ this.routeThreadIdentity(method, params, threadId, allowBuffer);
824
+ }
825
+ resolveTurnOwner(threadId, turnId) {
826
+ const threadScope = this.threadToScope.get(threadId);
827
+ const turnScope = this.turnToScope.get(turnId);
828
+ if (!threadScope || threadScope !== turnScope)
829
+ return null;
830
+ const state = this.scopes.get(threadScope);
831
+ if (state?.threadId !== threadId || state.activeTurnId !== turnId)
832
+ return null;
833
+ return threadScope;
834
+ }
835
+ shouldBufferTurn(threadId) {
836
+ const scope = this.threadToScope.get(threadId);
837
+ if (!scope)
838
+ return false;
839
+ const state = this.scopes.get(scope);
840
+ return state?.threadId === threadId
841
+ && state.operation === 'turn'
842
+ && state.pendingOperation?.kind === 'turn';
843
+ }
844
+ emitLaneNotification(scope, method, params, owner) {
845
+ this.recordScopedNotification(scope, method, params, owner);
846
+ this.emit(`notification:${scope}`, method, params, owner);
847
+ }
848
+ emitHostNotification(method, params) {
849
+ this.recordHostNotification(method, params);
850
+ this.emit('host-notification', method, params);
851
+ }
852
+ emitUnrouted(method, params, reason) {
853
+ this.emit('unrouted-notification', { method, params, reason });
854
+ }
855
+ ensureScopedNotificationHandoff(scope) {
856
+ const current = this.scopedNotificationHandoffs.get(scope);
857
+ if (current)
858
+ return current;
859
+ const handoff = { consumers: 0, replaying: false, buffer: [] };
860
+ this.scopedNotificationHandoffs.set(scope, handoff);
861
+ return handoff;
862
+ }
863
+ recordScopedNotification(scope, method, params, owner) {
864
+ const handoff = this.ensureScopedNotificationHandoff(scope);
865
+ if (handoff.consumers > 0 && !handoff.replaying)
866
+ return;
867
+ this.bufferHandoffNotification(handoff, { method, params, owner });
868
+ }
869
+ recordHostNotification(method, params) {
870
+ const handoff = this.hostNotificationHandoff;
871
+ if (handoff.consumers > 0 && !handoff.replaying)
872
+ return;
873
+ this.bufferHandoffNotification(handoff, { method, params, owner: undefined });
874
+ }
875
+ bufferHandoffNotification(handoff, entry, diagnoseOverflow = true) {
876
+ if (handoff.buffer.length >= this.pendingNotificationLimit) {
877
+ const evicted = handoff.buffer.shift();
878
+ if (evicted && diagnoseOverflow) {
879
+ this.emitUnrouted(evicted.method, evicted.params, 'pre-listener-overflow');
880
+ }
881
+ }
882
+ handoff.buffer.push(entry);
883
+ }
884
+ replayNotificationHandoff(handoff, deliver, dispose) {
885
+ handoff.replaying = true;
886
+ try {
887
+ while (handoff.buffer.length > 0) {
888
+ const entry = handoff.buffer.shift();
889
+ try {
890
+ deliver(entry.method, entry.params, entry.owner);
891
+ }
892
+ catch (err) {
893
+ if (handoff.buffer.length >= this.pendingNotificationLimit) {
894
+ const evicted = handoff.buffer.shift();
895
+ if (evicted) {
896
+ this.emitUnrouted(evicted.method, evicted.params, 'pre-listener-overflow');
897
+ }
898
+ }
899
+ handoff.buffer.unshift(entry);
900
+ dispose();
901
+ throw err;
902
+ }
903
+ }
904
+ }
905
+ finally {
906
+ handoff.replaying = false;
907
+ }
908
+ }
909
+ // ─── Bounded pending notification replay ─────────
910
+ bufferNotification(method, params, threadId, turnId) {
911
+ this.expirePendingNotifications();
912
+ const terminal = TERMINAL_NOTIFICATION_METHODS.has(method);
913
+ if (this.pendingNotifications.length >= this.pendingNotificationLimit) {
914
+ const oldestNonTerminal = this.pendingNotifications.findIndex((entry) => !entry.terminal);
915
+ if (oldestNonTerminal >= 0) {
916
+ const [evicted] = this.pendingNotifications.splice(oldestNonTerminal, 1);
917
+ if (evicted)
918
+ this.diagnoseBuffered(evicted, 'capacity-evicted');
919
+ }
920
+ else if (terminal) {
921
+ this.failBufferedOperation(this.makeBufferedNotification(method, params, threadId, turnId, terminal), 'capacity-terminal-full');
922
+ return;
923
+ }
924
+ else {
925
+ this.emitUnrouted(method, params, 'capacity-terminal-reserved');
926
+ return;
927
+ }
928
+ }
929
+ this.pendingNotifications.push(this.makeBufferedNotification(method, params, threadId, turnId, terminal));
930
+ this.schedulePendingExpiry();
931
+ }
932
+ makeBufferedNotification(method, params, threadId, turnId, terminal) {
933
+ const candidateScopes = new Set();
934
+ if (threadId) {
935
+ const scope = this.threadToScope.get(threadId);
936
+ if (scope)
937
+ candidateScopes.add(scope);
938
+ }
939
+ if (candidateScopes.size === 0) {
940
+ for (const state of this.scopes.values()) {
941
+ if (state.pendingOperation)
942
+ candidateScopes.add(state.scope);
943
+ }
944
+ }
945
+ return {
946
+ sequence: this.pendingNotificationSequence++,
947
+ receivedAt: Date.now(),
948
+ method,
949
+ params,
950
+ threadId,
951
+ turnId,
952
+ terminal,
953
+ candidateScopes,
954
+ };
955
+ }
956
+ replayPendingForScope(scope) {
957
+ this.expirePendingNotifications();
958
+ const state = this.scopes.get(scope);
959
+ if (!state?.threadId)
960
+ return;
961
+ const replay = this.pendingNotifications
962
+ .filter((entry) => {
963
+ if (entry.threadId !== state.threadId)
964
+ return false;
965
+ if (!entry.turnId)
966
+ return true;
967
+ return entry.method === 'turn/started' || entry.turnId === state.activeTurnId;
968
+ })
969
+ .sort((a, b) => a.sequence - b.sequence);
970
+ if (replay.length === 0)
971
+ return;
972
+ const replayed = new Set(replay);
973
+ this.pendingNotifications = this.pendingNotifications.filter((entry) => !replayed.has(entry));
974
+ for (const entry of replay)
975
+ this.routeNotification(entry.method, entry.params, false);
976
+ this.schedulePendingExpiry();
977
+ }
978
+ expirePendingNotifications() {
979
+ if (this.pendingNotifications.length === 0) {
980
+ this.clearPendingTimer();
981
+ return;
982
+ }
983
+ const cutoff = Date.now() - this.pendingNotificationTtlMs;
984
+ const expired = this.pendingNotifications.filter((entry) => entry.receivedAt <= cutoff);
985
+ if (expired.length === 0)
986
+ return;
987
+ const expiredSet = new Set(expired);
988
+ this.pendingNotifications = this.pendingNotifications.filter((entry) => !expiredSet.has(entry));
989
+ for (const entry of expired) {
990
+ if (entry.terminal)
991
+ this.failBufferedOperation(entry, 'ttl-terminal-expired');
992
+ else
993
+ this.diagnoseBuffered(entry, 'ttl-expired');
994
+ }
995
+ this.schedulePendingExpiry();
996
+ }
997
+ schedulePendingExpiry() {
998
+ this.clearPendingTimer();
999
+ if (this.pendingNotifications.length === 0)
1000
+ return;
1001
+ const oldest = Math.min(...this.pendingNotifications.map((entry) => entry.receivedAt));
1002
+ const delay = Math.max(0, oldest + this.pendingNotificationTtlMs - Date.now());
1003
+ this.pendingNotificationTimer = setTimeout(() => {
1004
+ this.pendingNotificationTimer = null;
1005
+ this.expirePendingNotifications();
1006
+ this.schedulePendingExpiry();
1007
+ }, delay);
1008
+ this.pendingNotificationTimer.unref();
1009
+ }
1010
+ clearPendingTimer() {
1011
+ if (!this.pendingNotificationTimer)
1012
+ return;
1013
+ clearTimeout(this.pendingNotificationTimer);
1014
+ this.pendingNotificationTimer = null;
1015
+ }
1016
+ failBufferedOperation(entry, reason) {
1017
+ const error = new Error(`Pending Codex notification ${entry.method} could not be preserved (${reason})`);
1018
+ let failed = false;
1019
+ for (const scope of entry.candidateScopes) {
1020
+ failed = this.failPendingOperation(scope, error) || failed;
1021
+ }
1022
+ if (!failed) {
1023
+ for (const state of this.scopes.values()) {
1024
+ if (state.pendingOperation) {
1025
+ failed = this.failPendingOperation(state.scope, error) || failed;
1026
+ }
1027
+ }
1028
+ }
1029
+ this.diagnoseBuffered(entry, failed ? reason : `${reason}-no-operation`);
1030
+ }
1031
+ diagnoseBuffered(entry, reason) {
1032
+ this.emitUnrouted(entry.method, entry.params, reason);
1033
+ }
1034
+ removeBufferedForScope(scope) {
1035
+ this.pendingNotifications = this.pendingNotifications.filter((entry) => {
1036
+ entry.candidateScopes.delete(scope);
1037
+ return entry.candidateScopes.size > 0;
1038
+ });
1039
+ this.schedulePendingExpiry();
1040
+ }
1041
+ hasPendingBinding() {
1042
+ return [...this.scopes.values()].some((state) => state.pendingOperation?.kind === 'binding');
1043
+ }
1044
+ // ─── Transport and process lifecycle ─────────────
212
1045
  rejectAllPending(reason) {
213
1046
  if (this.pending.size === 0)
214
1047
  return;
@@ -218,6 +1051,7 @@ export class CodexAppClient extends EventEmitter {
218
1051
  this.pending.clear();
219
1052
  }
220
1053
  request(method, params) {
1054
+ this.assertReusable();
221
1055
  return new Promise((resolve, reject) => {
222
1056
  const id = this.nextId++;
223
1057
  this.pending.set(id, { resolve, reject });
@@ -242,54 +1076,46 @@ export class CodexAppClient extends EventEmitter {
242
1076
  return false;
243
1077
  }
244
1078
  }
245
- reasoningConfig() {
1079
+ reasoningConfig(state) {
246
1080
  return {
247
- ...(this.effort ? { model_reasoning_effort: this.effort } : {}),
1081
+ ...(state.effort ? { model_reasoning_effort: state.effort } : {}),
248
1082
  model_reasoning_summary: 'detailed',
249
1083
  hide_agent_reasoning: false,
250
1084
  show_raw_agent_reasoning: true,
251
- service_tier: this.fastMode ? 'fast' : 'default',
1085
+ service_tier: state.fastMode ? 'fast' : 'default',
252
1086
  };
253
1087
  }
254
1088
  handleLine(line) {
255
1089
  if (!line.trim())
256
1090
  return;
1091
+ let msg;
257
1092
  try {
258
- const msg = JSON.parse(line);
259
- if (msg.id != null && this.pending.has(msg.id)) {
260
- const handler = this.pending.get(msg.id);
261
- this.pending.delete(msg.id);
262
- if (msg.error) {
263
- handler.reject(new Error(`JSON-RPC error ${msg.error.code}: ${msg.error.message}`));
264
- }
265
- else {
266
- handler.resolve(msg.result);
267
- }
268
- return;
269
- }
270
- if (msg.id != null && msg.method) {
271
- this.handleServerRequest(msg.id, msg.method, msg.params || {});
272
- return;
273
- }
274
- if (msg.method) {
275
- if (msg.method === 'turn/started') {
276
- this.setActiveTurnId(msg.params?.turn?.id ?? null);
277
- }
278
- else if (msg.method === 'turn/completed') {
279
- this.setActiveTurnId(null);
280
- }
281
- this.emit('notification', msg.method, msg.params || {});
282
- if (msg.method === 'error') {
283
- this.emit('server-error', msg.params || {});
284
- }
285
- else {
286
- this.emit(msg.method, msg.params || {});
287
- }
288
- return;
289
- }
1093
+ msg = JSON.parse(line);
290
1094
  }
291
1095
  catch {
292
1096
  this.emit('parse_error', line);
1097
+ return;
1098
+ }
1099
+ const id = msg['id'];
1100
+ const method = typeof msg['method'] === 'string' ? msg['method'] : null;
1101
+ if (id != null && typeof id === 'number' && this.pending.has(id)) {
1102
+ const handler = this.pending.get(id);
1103
+ this.pending.delete(id);
1104
+ const error = this.recordField(msg, 'error');
1105
+ if (error) {
1106
+ handler.reject(new Error(`JSON-RPC error ${String(error['code'] ?? '')}: ${String(error['message'] ?? '')}`));
1107
+ }
1108
+ else {
1109
+ handler.resolve(msg['result']);
1110
+ }
1111
+ return;
1112
+ }
1113
+ if (id != null && method) {
1114
+ this.handleServerRequest(typeof id === 'number' || typeof id === 'string' ? id : String(id), method, this.recordField(msg, 'params') ?? {});
1115
+ return;
1116
+ }
1117
+ if (method) {
1118
+ this.routeNotification(method, this.recordField(msg, 'params') ?? {});
293
1119
  }
294
1120
  }
295
1121
  handleServerRequest(id, method, params) {
@@ -307,19 +1133,74 @@ export class CodexAppClient extends EventEmitter {
307
1133
  const result = declineResponses[method] || {};
308
1134
  this.trySend({ jsonrpc: '2.0', id, result });
309
1135
  }
310
- setActiveTurnId(id) {
311
- this.activeTurnId = id;
312
- if (id && this.pendingInterrupt) {
313
- this.pendingInterrupt = false;
314
- void this.request('turn/interrupt', { threadId: this.threadId, turnId: id })
315
- .catch((err) => {
316
- if (!isTerminalInterruptRaceError(err)) {
317
- this.emit('interrupt-failed', err);
318
- }
319
- });
1136
+ async sendInterrupt(state, turnId) {
1137
+ try {
1138
+ await this.request('turn/interrupt', { threadId: state.threadId, turnId });
1139
+ }
1140
+ catch (err) {
1141
+ const error = err;
1142
+ if (isTerminalInterruptRaceError(error))
1143
+ return;
1144
+ this.emit(`interrupt-failed:${state.scope}`, error);
1145
+ throw error;
1146
+ }
1147
+ }
1148
+ handleProcessError(err) {
1149
+ if (!this.beginProcessDeath(`Process error: ${err.message}`))
1150
+ return;
1151
+ try {
1152
+ this.emit('error', err);
1153
+ }
1154
+ finally {
1155
+ this.finalizeProcessDeath();
1156
+ }
1157
+ }
1158
+ handleProcessExit(code, signal) {
1159
+ if (!this.beginProcessDeath('Process exited'))
1160
+ return;
1161
+ try {
1162
+ this.emit('exit', code, signal);
1163
+ }
1164
+ finally {
1165
+ this.finalizeProcessDeath();
320
1166
  }
321
- if (!id)
322
- this.pendingInterrupt = false;
1167
+ }
1168
+ beginProcessDeath(reason) {
1169
+ if (this.terminal)
1170
+ return false;
1171
+ this.terminal = true;
1172
+ this.rejectAllPending(reason);
1173
+ const error = new Error(reason);
1174
+ for (const state of this.scopes.values()) {
1175
+ if (state.pendingOperation && !state.pendingOperation.failed) {
1176
+ state.pendingOperation.failed = error;
1177
+ state.pendingOperation.reject(error);
1178
+ }
1179
+ state.operation = 'terminal';
1180
+ }
1181
+ return true;
1182
+ }
1183
+ finalizeProcessDeath() {
1184
+ for (const scope of [...this.scopes.keys()])
1185
+ this.removeScope(scope);
1186
+ this.threadToScope.clear();
1187
+ this.turnToScope.clear();
1188
+ this.pendingNotifications = [];
1189
+ this.scopedNotificationHandoffs.clear();
1190
+ this.hostNotificationHandoff = { consumers: 0, replaying: false, buffer: [] };
1191
+ this.clearPendingTimer();
1192
+ this.scopeDisposers.clear();
1193
+ this.removeAllListeners();
1194
+ }
1195
+ stringField(record, key) {
1196
+ const value = record[key];
1197
+ return typeof value === 'string' && value.length > 0 ? value : null;
1198
+ }
1199
+ recordField(record, key) {
1200
+ const value = record[key];
1201
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
1202
+ ? value
1203
+ : null;
323
1204
  }
324
1205
  }
325
1206
  //# sourceMappingURL=codex-app-client.js.map