agentbox-app 0.1.13 → 0.1.16

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 (278) hide show
  1. package/README.md +24 -9
  2. package/bin/agentbox.mjs +23 -1
  3. package/briefs/message-rules.md +36 -8
  4. package/briefs/worker.md +46 -23
  5. package/main/account-discovery.mjs +122 -30
  6. package/main/account-tooling.mjs +29 -1
  7. package/main/account-usage.mjs +46 -0
  8. package/main/agent-sessions.mjs +282 -268
  9. package/main/agents.mjs +179 -466
  10. package/main/analytics.mjs +12 -2
  11. package/main/answer-modes.mjs +103 -37
  12. package/main/approval-prompt-core.mjs +197 -0
  13. package/main/approval-prompt-server.mjs +4 -138
  14. package/main/approvals.mjs +101 -297
  15. package/main/artifact-path.mjs +47 -18
  16. package/main/claude-input.mjs +48 -4
  17. package/main/claude-plan.mjs +48 -0
  18. package/main/claude-session-open.mjs +28 -0
  19. package/main/claude-usage-process.mjs +40 -0
  20. package/main/claude-usage.mjs +18 -40
  21. package/main/cloud/account.mjs +179 -0
  22. package/main/cloud/aws.mjs +127 -0
  23. package/main/cloud/browser-return.mjs +58 -0
  24. package/main/cloud/clients.mjs +15 -0
  25. package/main/cloud/codex-cloud.mjs +53 -0
  26. package/main/cloud/config.mjs +107 -0
  27. package/main/cloud/docs.mjs +219 -0
  28. package/main/cloud/exec.mjs +27 -0
  29. package/main/cloud/host.mjs +165 -0
  30. package/main/cloud/kit.mjs +80 -0
  31. package/main/cloud/logins.mjs +543 -0
  32. package/main/cloud/managed.mjs +115 -0
  33. package/main/cloud/marker.mjs +104 -0
  34. package/main/cloud/org.mjs +53 -0
  35. package/main/cloud/reuse.mjs +208 -0
  36. package/main/cloud/run-cli.mjs +177 -0
  37. package/main/cloud/run.mjs +1934 -0
  38. package/main/cloud/usage.mjs +76 -0
  39. package/main/cloud-permission.mjs +51 -0
  40. package/main/codex-account.mjs +78 -0
  41. package/main/codex-app-server.mjs +276 -409
  42. package/main/codex-approvals.mjs +148 -433
  43. package/main/codex-models.mjs +20 -2
  44. package/main/codex-session.mjs +212 -507
  45. package/main/codex-threads-worker.mjs +12 -0
  46. package/main/codex-threads.mjs +71 -3
  47. package/main/codex-usage-file.mjs +5 -3
  48. package/main/codex-usage.mjs +56 -7
  49. package/main/codex-watch.mjs +17 -3
  50. package/main/codex.mjs +95 -363
  51. package/main/config.mjs +11 -1
  52. package/main/corner-tag.mjs +340 -19
  53. package/main/dev-window.mjs +1 -1
  54. package/main/engine-bin.mjs +232 -282
  55. package/main/engine-setup.mjs +21 -9
  56. package/main/feedback.mjs +3 -25
  57. package/main/folder-pictures.mjs +2 -2
  58. package/main/freeze-recorder-thread.mjs +86 -0
  59. package/main/freeze-recorder.mjs +146 -0
  60. package/main/fresh-user.mjs +20 -9
  61. package/main/git-change-offthread.mjs +17 -3
  62. package/main/git-change-thread.mjs +5 -2
  63. package/main/harnesses/claude.mjs +32 -11
  64. package/main/harnesses/codex.mjs +9 -2
  65. package/main/importable-threads.mjs +64 -6
  66. package/main/ipc.mjs +362 -111
  67. package/main/leftovers.mjs +117 -25
  68. package/main/liquid-glass.mjs +26 -0
  69. package/main/live-replies.mjs +10 -4
  70. package/main/main-went-red.mjs +330 -0
  71. package/main/main.mjs +199 -84
  72. package/main/memory-gate-history.mjs +36 -10
  73. package/main/memory-gate-server.mjs +62 -0
  74. package/main/memory-gate.mjs +14 -1
  75. package/main/menu.mjs +4 -2
  76. package/main/message-priority.mjs +2 -2
  77. package/main/mic.mjs +38 -0
  78. package/main/native/liquid-glass/build/liquid-glass.node +0 -0
  79. package/main/new-chats-watch.mjs +110 -0
  80. package/main/new-user-window.mjs +25 -0
  81. package/main/notify.mjs +24 -8
  82. package/main/phone-alerts.mjs +117 -0
  83. package/main/phone-inbox.mjs +625 -0
  84. package/main/phone-link.mjs +29 -9
  85. package/main/phone-uploads.mjs +476 -0
  86. package/main/picture-shape.mjs +114 -0
  87. package/main/quit-sequence.mjs +143 -0
  88. package/main/refresh-agent-models.mjs +15 -2
  89. package/main/relay-client.mjs +69 -1
  90. package/main/repeats.mjs +10 -4
  91. package/main/row-label.mjs +12 -41
  92. package/main/run-change.mjs +13 -0
  93. package/main/run-ledger.mjs +135 -0
  94. package/main/save-attachments.mjs +41 -0
  95. package/main/serve.mjs +45 -3
  96. package/main/settings.mjs +220 -44
  97. package/main/shell-path.mjs +185 -37
  98. package/main/ship-conflict-brief.mjs +119 -0
  99. package/main/ship-queue.mjs +115 -19
  100. package/main/source-updater.mjs +18 -10
  101. package/main/stage-attachment.mjs +68 -0
  102. package/main/stall-watch.mjs +50 -0
  103. package/main/store/home.mjs +87 -232
  104. package/main/store/paths.mjs +11 -0
  105. package/main/store/teammates-files.mjs +99 -0
  106. package/main/store/work-items.mjs +231 -13
  107. package/main/store.mjs +226 -52
  108. package/main/supervisor.mjs +3103 -341
  109. package/main/task-folders.mjs +430 -30
  110. package/main/task-terminals.mjs +110 -34
  111. package/main/team/index.mjs +114 -10
  112. package/main/team/memory-cloud.mjs +183 -1
  113. package/main/team/room-hours.mjs +48 -0
  114. package/main/team/room-setup.mjs +31 -0
  115. package/main/team/rooms-memory.mjs +454 -0
  116. package/main/team/rooms-shape.mjs +42 -0
  117. package/main/team/rooms-supabase.mjs +324 -0
  118. package/main/team/rooms.mjs +575 -0
  119. package/main/team/session.mjs +96 -26
  120. package/main/team/supabase-backend.mjs +180 -0
  121. package/main/team/sync.mjs +109 -1
  122. package/main/teammates.mjs +580 -0
  123. package/main/terminal-host.mjs +143 -0
  124. package/main/terminal-hosts.mjs +261 -0
  125. package/main/transcript-search.mjs +80 -0
  126. package/main/webview-guard.mjs +45 -0
  127. package/main/worktree-include.mjs +90 -219
  128. package/mcp/core/documents.mjs +3 -1
  129. package/mcp/core/repeats.mjs +88 -0
  130. package/mcp/core/teammates.mjs +222 -0
  131. package/mcp/core/work.mjs +76 -4
  132. package/mcp/stdio.mjs +24 -11
  133. package/mcp/tools.mjs +116 -8
  134. package/package.json +21 -5
  135. package/renderer/dist/assets/{abnfDiagram-YUKMZFIV-DPPuMWt-.js → abnfDiagram-YUKMZFIV-CoWB6YUK.js} +1 -1
  136. package/renderer/dist/assets/{arc-HmFfdseH.js → arc-Ch0NrCul.js} +1 -1
  137. package/renderer/dist/assets/{architectureDiagram-47P4ROYG-DxJ3zHrH.js → architectureDiagram-47P4ROYG-CXz9M5tQ.js} +1 -1
  138. package/renderer/dist/assets/{blockDiagram-E7TT5TSB-CbTXzOqQ.js → blockDiagram-E7TT5TSB-DchGUuHH.js} +1 -1
  139. package/renderer/dist/assets/{c4Diagram-CRA5TL53-X5ItFyLn.js → c4Diagram-CRA5TL53-Cip-zNyr.js} +1 -1
  140. package/renderer/dist/assets/channel-BQuQSL--.js +1 -0
  141. package/renderer/dist/assets/{chunk-2Q5K7J3B-DC12Ho_L.js → chunk-2Q5K7J3B-BoXid_2r.js} +1 -1
  142. package/renderer/dist/assets/{chunk-5VM5RSS4-ClVtfPd0.js → chunk-5VM5RSS4-Cpez1rRz.js} +1 -1
  143. package/renderer/dist/assets/{chunk-7TKQ45FW-DgcOm0HT.js → chunk-7TKQ45FW-DKA84YkE.js} +1 -1
  144. package/renderer/dist/assets/{chunk-AW2ZBBNX-C01s4yoF.js → chunk-AW2ZBBNX-Dg-SumPG.js} +1 -1
  145. package/renderer/dist/assets/{chunk-FVRAUYC3-Bj0zxhR4.js → chunk-FVRAUYC3-06TCvPm1.js} +2 -2
  146. package/renderer/dist/assets/{chunk-HTAEGDNF-DWDeEhLU.js → chunk-HTAEGDNF-CMEBy1Td.js} +1 -1
  147. package/renderer/dist/assets/{chunk-JWPE2WC7-Dsqwv3eW.js → chunk-JWPE2WC7-BJ2ObmV-.js} +1 -1
  148. package/renderer/dist/assets/{chunk-KQW6MTUR-CE86ANL2.js → chunk-KQW6MTUR-CGWOYXUE.js} +1 -1
  149. package/renderer/dist/assets/{chunk-XXDRQBXY-CoNTASfs.js → chunk-XXDRQBXY-CiZp07XC.js} +1 -1
  150. package/renderer/dist/assets/{classDiagram-v2-K4WV4PDN-2wprLqtw.js → classDiagram-v2-K4WV4PDN-DJNIX719.js} +1 -1
  151. package/renderer/dist/assets/cornerTag-CPgvu-q3.js +1 -0
  152. package/renderer/dist/assets/cornerTag-oMfEjzCO.css +1 -0
  153. package/renderer/dist/assets/{cose-bilkent-JH36ORCC-enqMiYbn.js → cose-bilkent-JH36ORCC-CbkyZOUH.js} +1 -1
  154. package/renderer/dist/assets/{cynefin-EF2NZ3EQ-DHngLxvQ.js → cynefin-EF2NZ3EQ-78iOroZO.js} +1 -1
  155. package/renderer/dist/assets/{cynefinDiagram-3GCD6N5R-C0rZZIl3.js → cynefinDiagram-3GCD6N5R-GcCopMs7.js} +1 -1
  156. package/renderer/dist/assets/{dagre-W4DXFKR2-DQnaEWO7.js → dagre-W4DXFKR2-DHLlXLkz.js} +1 -1
  157. package/renderer/dist/assets/{diagram-2UJZ2QOL-CS8AlGPx.js → diagram-2UJZ2QOL-DuZSUHcH.js} +1 -1
  158. package/renderer/dist/assets/{diagram-OVF4WLC6-yiDMHIkY.js → diagram-OVF4WLC6-Ckxu3cqH.js} +1 -1
  159. package/renderer/dist/assets/{diagram-PFPMY2P6-XbQKzbBA.js → diagram-PFPMY2P6-CsTw9Ycn.js} +1 -1
  160. package/renderer/dist/assets/{diagram-UMYRVEAY-dhVvlfl_.js → diagram-UMYRVEAY-CTXd8qVg.js} +1 -1
  161. package/renderer/dist/assets/{diagram-YEKJPTXX-DZHe8SCf.js → diagram-YEKJPTXX-CJs1T_9m.js} +1 -1
  162. package/renderer/dist/assets/{diagram-ZIFT7M5P-B5h6aXVR.js → diagram-ZIFT7M5P-CvjI23mf.js} +1 -1
  163. package/renderer/dist/assets/{ebnfDiagram-VR2GEFS7-PR9ScDco.js → ebnfDiagram-VR2GEFS7-d_IeAzOJ.js} +1 -1
  164. package/renderer/dist/assets/{elk-IJKZMXRS-Cg-yQXPd.js → elk-IJKZMXRS-zo9KFP3N.js} +1 -1
  165. package/renderer/dist/assets/{erDiagram-O2IAPWRE-omOrJMxu.js → erDiagram-O2IAPWRE-DqfYf5Dt.js} +1 -1
  166. package/renderer/dist/assets/{flowDiagram-OXPTDLAJ-aOX2hzvT.js → flowDiagram-OXPTDLAJ-DAWsMLmo.js} +1 -1
  167. package/renderer/dist/assets/{ganttDiagram-R7TSEDQI-B1H3pWyI.js → ganttDiagram-R7TSEDQI-4ml_bhwR.js} +1 -1
  168. package/renderer/dist/assets/{gitGraphDiagram-XJZIOB7I-Cvw304HC.js → gitGraphDiagram-XJZIOB7I-B_WcqSId.js} +1 -1
  169. package/renderer/dist/assets/{infoDiagram-5W2HQ5XZ-_ZXcNViH.js → infoDiagram-5W2HQ5XZ-CI-jYqzc.js} +1 -1
  170. package/renderer/dist/assets/{ishikawaDiagram-K3B6WC7H-C7Mx_qQk.js → ishikawaDiagram-K3B6WC7H-B56VWRlV.js} +1 -1
  171. package/renderer/dist/assets/{journeyDiagram-COXZFDF6-fr62QoAt.js → journeyDiagram-COXZFDF6-D1-p09Lr.js} +1 -1
  172. package/renderer/dist/assets/{kanban-definition-P3RFRI5V-J8zsabCe.js → kanban-definition-P3RFRI5V-Cv3w7JoN.js} +1 -1
  173. package/renderer/dist/assets/{linear-BmNdEtIi.js → linear-C35V2Spi.js} +1 -1
  174. package/renderer/dist/assets/main-BcoBiGwv.css +32 -0
  175. package/renderer/dist/assets/main-CLnIOulD.js +1191 -0
  176. package/renderer/dist/assets/{mermaid.core-CnYzvFkz.js → mermaid.core-BzGT5Vo9.js} +4 -4
  177. package/renderer/dist/assets/{mindmap-definition-LZFPQGKD-Bfcxc7PM.js → mindmap-definition-LZFPQGKD-BZUfuymC.js} +1 -1
  178. package/renderer/dist/assets/{pegDiagram-BGZESJAR-CwUJOq6X.js → pegDiagram-BGZESJAR-DFh9XNGe.js} +1 -1
  179. package/renderer/dist/assets/{pieDiagram-CAPJLFHJ-BrwMmqfC.js → pieDiagram-CAPJLFHJ-C65tPVsy.js} +1 -1
  180. package/renderer/dist/assets/{product-name-CTKLYNWY.js → product-name-CSNW6d7s.js} +1 -1
  181. package/renderer/dist/assets/{quadrantDiagram-GDMTTRAM-BN4Stoj-.js → quadrantDiagram-GDMTTRAM-Cushj_nk.js} +1 -1
  182. package/renderer/dist/assets/{railroadDiagram-SM67HX2B-CPZ5bbeR.js → railroadDiagram-SM67HX2B-DZGebGx5.js} +1 -1
  183. package/renderer/dist/assets/{requirementDiagram-X7JNWC4A-CgRnimVe.js → requirementDiagram-X7JNWC4A-DpAB0MNo.js} +1 -1
  184. package/renderer/dist/assets/{sankeyDiagram-UM26HJRW-Bt2rej__.js → sankeyDiagram-UM26HJRW-D-29rHra.js} +1 -1
  185. package/renderer/dist/assets/{sequenceDiagram-ZO4K6R2Y-Dd5nKe9G.js → sequenceDiagram-ZO4K6R2Y-FfOQAZol.js} +1 -1
  186. package/renderer/dist/assets/{sizeCapture-INFHLROL-usyNyXyh.js → sizeCapture-INFHLROL-C_n6fDBY.js} +1 -1
  187. package/renderer/dist/assets/{stateDiagram-v2-TBUQTH76-CRzUBb0u.js → stateDiagram-v2-TBUQTH76-BZjenzJG.js} +1 -1
  188. package/renderer/dist/assets/{swimlanes-N4OXWK64-C1GJvMS0.js → swimlanes-N4OXWK64-DxaMU9vX.js} +1 -1
  189. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cb2kM6rr.js +8 -0
  190. package/renderer/dist/assets/{timeline-definition-YOQAKGHF-caeyPlcc.js → timeline-definition-YOQAKGHF-DTg5HJKz.js} +1 -1
  191. package/renderer/dist/assets/{usecaseDiagram-VIAY4XPW-BN_SV6PC.js → usecaseDiagram-VIAY4XPW-Bc3MZDsl.js} +1 -1
  192. package/renderer/dist/assets/{vennDiagram-BLWOH2XV-BLGuaUX-.js → vennDiagram-BLWOH2XV-GIVGk9Id.js} +1 -1
  193. package/renderer/dist/assets/{wardleyDiagram-YMQ3BMBF-Da0cJeNc.js → wardleyDiagram-YMQ3BMBF-NmJnbQOg.js} +1 -1
  194. package/renderer/dist/assets/{xychartDiagram-TAQBALBS-cGfUffMk.js → xychartDiagram-TAQBALBS-p-kfGfnW.js} +1 -1
  195. package/renderer/dist/corner-tag.html +3 -3
  196. package/renderer/dist/index.html +3 -3
  197. package/scripts/after-install.mjs +30 -8
  198. package/scripts/cloud-shim.pl +57 -0
  199. package/shared/account-names.d.mts +6 -0
  200. package/shared/account-names.mjs +15 -0
  201. package/shared/agent-hours.mjs +136 -0
  202. package/shared/agent-import.mjs +60 -34
  203. package/shared/agents.mjs +126 -522
  204. package/shared/analytics-events.mjs +12 -0
  205. package/shared/approval-card.mjs +36 -0
  206. package/shared/bridge-map.mjs +27 -1
  207. package/shared/claude-code-shape.mjs +6 -0
  208. package/shared/claude-usage.d.mts +1 -0
  209. package/shared/claude-usage.mjs +42 -1
  210. package/shared/cloud-hand-back.d.mts +7 -0
  211. package/shared/cloud-hand-back.mjs +20 -0
  212. package/shared/cloud-kit.d.mts +219 -0
  213. package/shared/cloud-kit.mjs +341 -0
  214. package/shared/cloud-login-name.mjs +4 -0
  215. package/shared/cloud-start.mjs +17 -0
  216. package/shared/cloud-versions.mjs +23 -0
  217. package/shared/codex-import.mjs +2 -2
  218. package/shared/codex-modes.d.mts +22 -9
  219. package/shared/codex-modes.mjs +67 -11
  220. package/shared/codex-usage.mjs +108 -2
  221. package/shared/corner-tag.d.mts +23 -1
  222. package/shared/corner-tag.mjs +284 -29
  223. package/shared/dead-run-trace.mjs +19 -2
  224. package/shared/engines.mjs +70 -309
  225. package/shared/feedback.d.mts +7 -0
  226. package/shared/feedback.mjs +25 -0
  227. package/shared/first-run-practice.mjs +17 -24
  228. package/shared/harness-definitions.mjs +8 -0
  229. package/shared/instruction-defaults.json +2 -2
  230. package/shared/key-caps.d.mts +14 -0
  231. package/shared/key-caps.mjs +68 -0
  232. package/shared/message-edits.d.mts +7 -0
  233. package/shared/message-edits.mjs +35 -0
  234. package/shared/move-thread.d.mts +24 -0
  235. package/shared/move-thread.mjs +112 -0
  236. package/shared/named-threads.mjs +72 -0
  237. package/shared/not-shipped.d.mts +22 -0
  238. package/shared/not-shipped.mjs +89 -0
  239. package/shared/notify-rules.d.mts +6 -0
  240. package/shared/notify-rules.mjs +36 -8
  241. package/shared/phone-files.mjs +28 -0
  242. package/shared/phone-settings.mjs +118 -0
  243. package/shared/phone-snapshot.d.mts +16 -0
  244. package/shared/phone-snapshot.mjs +348 -0
  245. package/shared/picture-formats.d.mts +1 -0
  246. package/shared/picture-formats.mjs +13 -0
  247. package/shared/rank.d.mts +2 -1
  248. package/shared/rank.mjs +32 -19
  249. package/shared/repeats.d.mts +8 -0
  250. package/shared/repeats.mjs +33 -2
  251. package/shared/room-cards.mjs +167 -0
  252. package/shared/room-links.d.mts +4 -0
  253. package/shared/room-links.mjs +21 -0
  254. package/shared/runs-on.d.mts +3 -3
  255. package/shared/runs-on.mjs +8 -5
  256. package/shared/shipped-note.d.mts +4 -0
  257. package/shared/shipped-note.mjs +6 -0
  258. package/shared/shortcuts.mjs +14 -6
  259. package/shared/spawn-trouble.d.mts +4 -0
  260. package/shared/spawn-trouble.mjs +443 -40
  261. package/shared/team-rules.mjs +9 -2
  262. package/shared/teammates.d.mts +19 -0
  263. package/shared/teammates.mjs +756 -0
  264. package/shared/terminal-focus.d.mts +1 -0
  265. package/shared/terminal-focus.mjs +13 -0
  266. package/shared/thread-cards.mjs +4 -1
  267. package/shared/usage.d.mts +9 -0
  268. package/shared/usage.mjs +49 -3
  269. package/shared/websites.d.mts +15 -0
  270. package/shared/websites.mjs +61 -0
  271. package/shared/work-items.mjs +127 -30
  272. package/shared/work-lines.mjs +92 -244
  273. package/renderer/dist/assets/channel-BlT2g9Sz.js +0 -1
  274. package/renderer/dist/assets/cornerTag-BLrxiDE5.css +0 -1
  275. package/renderer/dist/assets/cornerTag-BOT6u3Tn.js +0 -1
  276. package/renderer/dist/assets/main-ChNdnUvt.js +0 -1132
  277. package/renderer/dist/assets/main-nj0QR5kg.css +0 -32
  278. package/renderer/dist/assets/swimlanesDiagram-K3J5GTZL-Cy4Av2_p.js +0 -8
@@ -1,278 +1,176 @@
1
- // The transport to `codex app-server`: one child process, many work items, and
2
- // an approval that always gets an answer or takes the connection down trying.
1
+ // The transport to `codex app-server`: one child process for many work items.
2
+ // It speaks newline-delimited JSON-RPC 2.0 over stdio, and when a turn needs an
3
+ // approval the server sends us a request with an id that we must answer.
3
4
  //
4
- // THIS IS THE PART THAT WAS NEVER SOLVED.The removed build drove `codex exec`,
5
- // a one-shot run over a single JSON stream, and `codex exec` has no channel
6
- // back to the caller. A Claude Code worker that wants something outside its
7
- // grants raises a card through main/approval-prompt-server.mjs and waits in
8
- // place; a Codex worker could not raise anything, so it either ran ungated or
9
- // it stopped. That was the blocker, and `git show 258d71d^:main/codex.mjs` is
10
- // worth reading for what else was tried, but ITS EVENT PARSING IS OBSOLETE and
11
- // none of it is carried over here.
5
+ // main/supervisor.mjs spawns the process and holds the client,
6
+ // main/codex-session.mjs wraps one thread of it per work item, and
7
+ // main/codex-approvals.mjs is the `onApproval` handler. This file does
8
+ // transport and routing only. The transport is injected so the tests run
9
+ // without `codex` installed.
12
10
  //
13
- // `codex app-server` is the fix, verified by running it rather than by reading
14
- // about it. It speaks newline-delimited JSON-RPC 2.0 over the child's stdio,
15
- // and when a turn wants to run a gated command the SERVER sends US a request,
16
- // with an id, that we must answer. That is the missing channel, and it is the
17
- // same shape the approvals spool already has: a question, a wait, an answer.
11
+ // Deny is the default in every failure mode, and silence is not a refusal:
12
+ // - A card waits for the person it is addressed to and is never denied on a
13
+ // clock; main/codex-approvals.mjs holds the whole argument. It is settled by
14
+ // their answer, by the worker that raised it stopping, or by the app closing,
15
+ // and a late answer is dropped, because one request id cannot be answered
16
+ // twice.
17
+ // - A write that fails, at once or in its callback, is a broken connection.
18
+ // Every waiter is settled and the child is killed.
19
+ // - A stdout line that will not parse may have been an approval request, so it
20
+ // is fatal. Tracing goes to stderr, and blank lines are skipped.
18
21
  //
19
- // AND IT MOVES THE FAILURE SOMEWHERE NEW, WHICH IS WHAT THIS FILE IS ABOUT. A
20
- // request with an id that nobody ever answers is not an error. It is a worker
21
- // parked forever, with no exit code, no message and nothing on the row. CLAUDE.md
22
- // already has that incident under another name -- "A WORKER THAT DID NOT FINISH
23
- // DELIVERED NOTHING, HOWEVER LONG IT TOOK" -- three items stranded for two, two
24
- // and six hours. So the posture here is copied from
25
- // main/approval-prompt-server.mjs and not re-argued:
26
- //
27
- // DENY IS THE DEFAULT IN EVERY FAILURE MODE, AND SILENCE IS NOT A REFUSAL.
28
- //
29
- // That sentence has three consequences, and each one cost a draft of this file.
30
- //
31
- // A CARD SHE NEVER ANSWERS DENIES ITSELF, AFTER FIFTEEN MINUTES. The first
32
- // draft waited forever, on the theory that the caller owns the timeout policy.
33
- // It does not: main/approval-prompt-server.mjs is the same channel for the same
34
- // founder and it has `TIMEOUT_MS = 15 * 60_000` and denies on expiry. Waiting
35
- // forever is not neutrality, it is a worker parked for the rest of the session
36
- // because she went to lunch. The deadline is injectable so that tests do not
37
- // sleep, and a late answer arriving after it has already denied is DROPPED --
38
- // the turn was told no and has moved on, and a second answer to one request id
39
- // is a protocol violation on top of an approval nobody is waiting for.
40
- //
41
- // A REFUSAL THAT CANNOT BE WRITTEN IS SILENCE. So a write that throws, a write
42
- // that fails afterwards in its callback, and an error raised on the pipe itself
43
- // are all treated as a broken connection: settle everything, tell every thread,
44
- // take the child down. There is no way to answer a request over a dead pipe,
45
- // and a supervisor can see a dead worker. It cannot see a hung one.
46
- //
47
- // A FRAME WE CANNOT READ MIGHT HAVE BEEN A CARD. stdout is the protocol here --
48
- // the child's tracing goes to stderr, measured, and the live run read zero
49
- // unreadable lines. A line on stdout that will not parse is therefore not noise
50
- // to step over: it is a frame whose id has just been lost, and if it was an
51
- // approval request then the turn that raised it waits forever. Nothing can tell
52
- // whether it was, which is exactly why tolerating it is unsafe. Unreadable or
53
- // unbounded input is fatal, loudly. Blank lines are framing rather than content
54
- // and are the one thing here that may be skipped.
55
- //
56
- // ------------------------------------------------------------------------
57
- // WHAT WAS MEASURED, ON THIS MAC, 2026-09-04, codex-cli 0.148.0. Some of this
58
- // contradicts what was written down before; where it does, the machine wins.
59
- //
60
- // THE THREAD ID IS AT `result.thread.id`. Off a live `thread/start` response:
61
- // `result.thread.id` was "01a068f1-be4d-7701-98e3-16137d16dcdf" and
62
- // `result.threadId` was `undefined`. The obvious guess fails silently -- every
63
- // thread ends up filed under the id `undefined`, which is one shared bucket --
64
- // so it is asserted rather than trusted.
65
- //
66
- // SERVER REQUEST IDS START AT 0 AND ARE A SEPARATE NUMBER SPACE FROM OURS. In
67
- // the captured session the first approval carried `id: 0` while our own
68
- // `initialize` carried `id: 1`. A classifier written `if (msg.id)` drops the
69
- // first approval of every session, because 0 is falsy. Everything below tests
70
- // `!== undefined`, and the two spaces never share a map.
71
- //
72
- // A REFUSAL IS ACCEPTED EVEN WHEN THE SERVER DID NOT OFFER IT. The live
73
- // approval arrived with `availableDecisions: ["accept",
74
- // {acceptWithExecpolicyAmendment: ...}, "cancel"]` -- no "decline" in the list.
75
- // Answering `{decision: 'decline'}` anyway worked: the item completed with
76
- // `status: "declined"`, `serverRequest/resolved` fired, and the turn COMPLETED
77
- // 4.0 seconds later. `availableDecisions` is a hint about which buttons a UI
78
- // should draw, not a whitelist the answer is validated against. That is the
79
- // measurement that makes an unconditional refusal safe, and without it the
80
- // deny-by-default rule above would have been a guess.
81
- //
82
- // A JSON-RPC ERROR FRAME REFUSES TOO, MEASURED ON ONE METHOD. The same
83
- // `item/commandExecution/requestApproval` answered with `{error: {code: -32001,
84
- // message}}` left the item at `status: "failed"` and the turn still COMPLETED,
85
- // 5.5 seconds later. BE PRECISE ABOUT THE SCOPE OF THAT: it was measured for
86
- // command approval and for nothing else, so using it as the refusal for other
87
- // server requests is a REASONED EXTENSION rather than a measured fact. It is
88
- // the best one available -- a well-formed error is the only answer that is
89
- // valid for a result shape nobody has measured -- but the next slice to lean on
90
- // it for a specific method should measure that method.
91
- //
92
- // NO HEAD-OF-LINE BLOCKING. Proven separately: an approval on thread A that is
93
- // deliberately never answered does not stop thread B from raising its own
94
- // approval, having it answered, and completing its turn. That is what makes one
95
- // process for the whole fleet safe, and it is a promise this file has to keep --
96
- // hence the read loop below never awaits anything.
97
- //
98
- // `ephemeral: false` IS THE DEFAULT AND HAS TO BE. `thread/resume` fails with
99
- // "no rollout found for thread id" on an ephemeral thread, so a thread started
100
- // ephemerally cannot carry a continuation. the app's whole reply path is
101
- // continuations, so the default is the one that can come back.
102
- //
103
- // TWO THINGS THE WRITTEN BRIEF HAD WRONG, both from the CLI's own
104
- // `codex app-server generate-json-schema` dump:
105
- //
106
- // `abort` IS NOT A v2 APPROVAL DECISION. CommandExecutionApprovalDecision and
107
- // FileChangeApprovalDecision are accept / acceptForSession / decline / cancel,
108
- // plus two amendment OBJECTS (`acceptWithExecpolicyAmendment` and
109
- // `applyNetworkPolicyAmendment`, command approvals only). `abort` belongs to
110
- // the LEGACY `ReviewDecision` used by `execCommandApproval` and
111
- // `applyPatchApproval`. Sending it to a v2 approval is a malformed answer, so
112
- // it is treated as junk here.
113
- //
114
- // `item/permissions/requestApproval` TAKES NO `decision` AT ALL. Its response
115
- // is PermissionsRequestApprovalResponse: `{ permissions, scope?,
116
- // strictAutoReview? }`, a granted profile rather than a word.
117
- //
118
- // AND THERE ARE TEN SERVER-TO-CLIENT REQUEST METHODS IN THE STABLE SCHEMA, NOT
119
- // THREE. Beyond the three approvals: item/tool/requestUserInput, item/tool/call,
120
- // mcpServer/elicitation/request, account/chatgptAuthTokens/refresh,
121
- // attestation/generate, and the legacy applyPatchApproval and
122
- // execCommandApproval, both of which are still in the stable dump. The
123
- // experimental schema adds more (currentTime/read), so "ten" is a fact about
124
- // the stable dump on 0.148.0 and not a ceiling. Every one of them hangs
125
- // whatever raised it if it is left open, so ANY server request this file does
126
- // not know how to answer gets the error frame. That is the honest answer and
127
- // not a placeholder: refusing `account/chatgptAuthTokens/refresh` may well cost
128
- // a long-lived process its token refresh, and the slice that wants that
129
- // capability is the slice that gets to measure the shape and answer it
130
- // properly. A refusal is a bug somebody can see; silence is not.
131
- //
132
- // ------------------------------------------------------------------------
133
- // SCOPE. Transport and routing, nothing else. Nothing here turns an event into
134
- // Agentbox's work lines or its trace, nothing here touches the approvals spool,
135
- // and nothing here decides which engine a row runs on.
136
- //
137
- // IT IS NO LONGER INERT, AND THIS HEADER SAID IT WAS FOR A WHOLE SLICE AFTER IT
138
- // HAD STOPPED BEING ONE. Three files depend on it now: main/supervisor.mjs spawns the
139
- // process and holds the client, main/codex-session.mjs is the per-work-item
140
- // facade over one thread of it, and main/codex-approvals.mjs is the handler on
141
- // the other end of `onApproval` that this file was written around. The three
142
- // sentences above still hold; the fourth was a fact about a Tuesday.
143
- //
144
- // The transport is injected so this whole file can be tested without `codex`
145
- // being installed. That is not tidiness. Four tests in this suite already fail
146
- // on any machine whose clock or installs differ from hers, and a transport test
147
- // that shelled out to the real binary would be the fifth.
22
+ // Protocol facts, measured on codex-cli 0.148.0:
23
+ // - The thread id is at `result.thread.id`; `result.threadId` is undefined.
24
+ // - Server request ids start at 0 and are a separate number space from ours,
25
+ // so ids are tested with `!== undefined`.
26
+ // - `decline` is accepted even when `availableDecisions` does not list it,
27
+ // which is what makes an unconditional refusal safe.
28
+ // - An error frame also refuses a command approval. That was measured for that
29
+ // method only; other server requests get it because it is the one answer
30
+ // valid for any result shape.
31
+ // - An open approval on one thread does not block the others, as long as the
32
+ // read loop never awaits.
33
+ // - `thread/resume` fails on an ephemeral thread, so `ephemeral` defaults to
34
+ // false.
35
+ // - `abort` is a word of the legacy `ReviewDecision`, not a v2 approval
36
+ // decision.
37
+ // - `item/permissions/requestApproval` takes a granted profile, not a decision.
38
+ // - Any server request not in APPROVAL_SHAPE gets the error frame, because one
39
+ // left open hangs whatever raised it.
148
40
 
149
41
  import { StringDecoder } from 'node:string_decoder';
150
42
  import { NAME, Name, nameSlug } from '../shared/product-name.mjs';
151
43
 
152
44
  /**
153
- * How this client names itself to the server. It comes back on `initialize` as
154
- * part of `userAgent` -- measured live as "astral/0.148.0 (Mac OS 15.6.1;
155
- * arm64) (astral; 1.0.0)", which is the app's name of the day rather than a
156
- * constant -- and it is what appears in Codex's own session records, so it says
157
- * this app rather than the name of whatever spike wrote it. READ, NEVER TYPED:
158
- * that measured line is why, it is a record written months ago under a name the
159
- * app no longer has.
45
+ * How this client names itself to the server. It appears in `userAgent` and in
46
+ * Codex's own session records, so it comes from shared/product-name.mjs rather
47
+ * than being typed here.
160
48
  */
161
49
  export const CLIENT_INFO = Object.freeze({ name: nameSlug, title: NAME, version: '1.0.0' });
162
50
 
163
51
  /**
164
- * The four SCALAR answers this callback supports, read off the CLI's schema
165
- * dump rather than remembered.
166
- *
167
- * Not the whole v2 vocabulary: command approvals also accept two structured
168
- * amendment objects, which widen the execpolicy or pin a network rule for
169
- * future commands. Those are standing policy changes rather than an answer to
170
- * the question that was asked, they cannot be expressed as a word, and nothing
171
- * in Agentbox can produce one yet -- so a handler returning one is refused. That
172
- * is conservative in the safe direction, and it is a deliberate omission rather
173
- * than an oversight.
174
- *
175
- * `abort` is NOT here and reads like it should be. It is a legacy word.
52
+ * The scalar answers a v2 approval takes, from the CLI's schema dump. Command
53
+ * approvals also take two amendment objects, which are standing policy changes
54
+ * rather than an answer, so a handler returning one is refused. `abort` is a
55
+ * legacy word and is not here.
176
56
  */
177
57
  export const APPROVAL_DECISIONS = Object.freeze(['accept', 'acceptForSession', 'decline', 'cancel']);
178
58
 
179
59
  /**
180
- * The three words an MCP elicitation takes, which is a SHORTER list than the
181
- * one above and not a subset by accident: `McpServerElicitationRequestResponse`
182
- * has no `acceptForSession`, so an approval that lasts a session cannot be
183
- * expressed here at all. Read off the CLI's schema dump 2026-09-22.
60
+ * The actions an MCP elicitation takes. `McpServerElicitationRequestResponse`
61
+ * has no `acceptForSession`.
184
62
  */
185
63
  export const ELICITATION_ACTIONS = Object.freeze(['accept', 'decline', 'cancel']);
186
64
 
187
65
  /**
188
- * The refusal. `decline` and not `cancel`, deliberately: measured, `decline`
189
- * leaves the item `declined` and lets the agent carry on and try something
190
- * else, while `cancel` also interrupts the whole turn. A failure to reach the
191
- * founder should cost the command, not the work item.
66
+ * The refusal. `decline` lets the agent carry on and try something else, while
67
+ * `cancel` also interrupts the whole turn.
192
68
  */
193
69
  export const REFUSAL_DECISION = 'decline';
194
70
 
195
- /** Her deadline, matching main/approval-prompt-server.mjs exactly. */
196
- export const APPROVAL_TIMEOUT_MS = 15 * 60_000;
197
-
198
71
  /**
199
- * How long ONE FRAME may be before the connection is called corrupt. Generous,
200
- * because `turn/diff/updated` carries a git-format diff of the whole turn and a
201
- * big refactor is a big diff; bounded, because an unterminated line is
202
- * otherwise a memory leak that ends in a dead app.
203
- *
204
- * IT APPLIES TO A TERMINATED LINE AS WELL AS TO A RUNAWAY ONE, and it did not.
205
- * The check ran on the residual buffer AFTER every newline-delimited frame had
206
- * been taken out of it, so it could only ever see a line with no end. A single
207
- * complete ten-megabyte line -- which is precisely what an enormous
208
- * `turn/diff/updated` is -- went straight past the bound to `JSON.parse` and on
209
- * to the handlers. Both halves are checked now: `read` refuses an oversized
210
- * line before it parses it, and the residual check still catches the one that
211
- * never terminates.
72
+ * The longest frame allowed before the connection is called corrupt. Large,
73
+ * because `turn/diff/updated` carries the whole turn's diff. Bounded, because
74
+ * an unterminated line would otherwise grow until the app dies. `read` checks
75
+ * each complete line, and the data handler checks the unterminated rest.
212
76
  */
213
77
  export const MAX_FRAME_LENGTH = 16 * 1024 * 1024;
214
78
 
215
79
  /**
216
- * HOW LONG AN ORDINARY REQUEST MAY GO UNANSWERED.
217
- *
218
- * The approval had a deadline from the first draft of this file, for the reason
219
- * written at the top: a request with an id that nobody answers is a worker
220
- * parked forever with no exit code and nothing on the row. EVERY REQUEST HAS
221
- * THAT SHAPE, and the ones that get a worker as far as raising an approval --
222
- * `initialize`, `config/read`, `thread/start`, `thread/resume`, `turn/start` --
223
- * had nothing to settle them at all. A live app-server that accepts the bytes
224
- * and never answers holds a work item and a fleet slot for the rest of the
225
- * session; the pipe is not broken, so nothing else here fires either.
226
- *
227
- * Two minutes, which is far above anything measured (a handshake and a
228
- * `config/read` answer in milliseconds, and `turn/start` resolves when the turn
229
- * is ACCEPTED rather than when it finishes) and far below a founder noticing a
230
- * row is stuck. The turn itself is not on this clock; only the request that
231
- * starts it is.
232
- *
233
- * ONE REQUEST GOES, NOT THE CONNECTION. A rejection frees the row and the slot,
234
- * which is the whole failure being closed, and the worker's own chain turns it
235
- * into a run that says why on stderr. Taking the shared process down over one
236
- * slow answer would charge every other thread for it.
80
+ * How long an ordinary request may go unanswered. A live server that accepts a
81
+ * request and never answers would otherwise hold a work item forever, since the
82
+ * pipe is not broken. `turn/start` resolves when the turn is accepted, so the
83
+ * turn itself is not on this clock. A timeout rejects that one request and
84
+ * leaves the connection up for the other threads.
237
85
  */
238
86
  export const REQUEST_TIMEOUT_MS = 2 * 60_000;
239
87
 
240
88
  /**
241
- * The server requests we know how to answer, and in which of the three shapes.
242
- *
243
- * `mcpServer/elicitation/request` IS AN APPROVAL AND NOT ONLY A FORM, which is
244
- * the correction this table carries. It is how Codex asks whether an MCP TOOL
245
- * CALL may run. Measured on this Mac 2026-09-22 against codex-cli 0.153.4, a
246
- * thread started exactly as `workerThreadParams` starts one, with one stdio MCP
247
- * server on it and the model asked to call its single tool:
89
+ * The server requests we answer, and the shape each answer takes.
248
90
  *
249
- * item/started mcpToolCall server=probe tool=ping status=inProgress
250
- * REQUEST mcpServer/elicitation/request
251
- * { serverName: "probe", mode: "form",
252
- * message: 'Allow the probe MCP server to run tool "ping"?',
253
- * _meta: { codex_approval_kind: "mcp_tool_call",
254
- * persist: ["session","always"],
255
- * tool_description: "...", tool_params: {} } }
256
- *
257
- * The method was not on this table, so it took the error frame below, and the
258
- * measured consequence was the whole integration rather than one lost call:
259
- *
260
- * answered with the error frame -> item/completed mcpToolCall status=failed,
261
- * error "user rejected MCP tool call"
262
- * answered `{action:"accept"}` -> the tool ran and returned its result
263
- *
264
- * EVERY STORE TOOL A CODEX WORKER HAS IS AN MCP TOOL CALL, so the first row is
265
- * a worker that cannot claim, checkpoint or finish anything, and the only words
266
- * anybody sees are that sentence: a rejection nobody made, over a card that was
267
- * never drawn. `McpServerElicitationRequestResponse` is `{ action: "accept" |
268
- * "decline" | "cancel", content?, _meta? }`, which is a third answer shape, and
269
- * its vocabulary happens to be three of the words the decision shape speaks.
91
+ * `mcpServer/elicitation/request` is how Codex asks whether an MCP tool call
92
+ * may run (`_meta.codex_approval_kind: "mcp_tool_call"`), and every store tool
93
+ * is one. Refused with the error frame, the call fails as "user rejected MCP
94
+ * tool call", so it must reach the handler. Its answer is `{ action }`.
270
95
  */
271
96
  const APPROVAL_SHAPE = new Map([
272
97
  ['item/commandExecution/requestApproval', 'decision'],
273
98
  ['item/fileChange/requestApproval', 'decision'],
274
99
  ['item/permissions/requestApproval', 'grant'],
275
100
  ['mcpServer/elicitation/request', 'elicitation'],
101
+ // THE SAME TWO APPROVALS UNDER THEIR v1 NAMES (w-36c51443df). These are the
102
+ // older spellings of the first two rows, and they used to fall through to a
103
+ // protocol error -- so a Codex speaking v1 on any path would have had every
104
+ // command refused rather than carded. Their answer is `{ decision:
105
+ // ReviewDecision }` and that vocabulary is NOT the v2 one, which is why they
106
+ // get a shape of their own rather than sharing `decision`. See
107
+ // LEGACY_DECISIONS.
108
+ ['execCommandApproval', 'legacy'],
109
+ ['applyPatchApproval', 'legacy'],
110
+ ]);
111
+
112
+ /**
113
+ * THE v1 WORD FOR EACH OF OUR FOUR, read off `ExecCommandApprovalResponse.json`
114
+ * in the 0.160.0 schema dump, 2026-10-09.
115
+ *
116
+ * `ReviewDecision` is `approved` | `approved_for_session` |
117
+ * `approved_mcp_policy_amendment` | `timed_out` | `abort` as bare words, plus
118
+ * `{denied:{rejection}}` and two amendment objects. NOTE THAT `decline` IS NOT
119
+ * IN IT: forwarding our own v2 word would have been refused by the server, and
120
+ * the refusal is an OBJECT here rather than a word, which is exactly the kind of
121
+ * difference that a shared mapping hides. The amendments are left out for the
122
+ * reason APPROVAL_DECISIONS gives: they are standing policy changes rather than
123
+ * an answer to the question asked, and nothing here can produce one.
124
+ */
125
+ export const LEGACY_DECISIONS = Object.freeze({
126
+ accept: 'approved',
127
+ acceptForSession: 'approved_for_session',
128
+ cancel: 'abort',
129
+ // The one place a refusal gets to say why, so it says why.
130
+ decline: { denied: { rejection: `${Name} did not approve this. Carry on with what you are already allowed to do and leave the rest for her to look at.` } },
131
+ });
132
+
133
+ /**
134
+ * EVERY REQUEST THE SERVER MAY SEND A CLIENT, so the list lives somewhere a
135
+ * reader can check it against the schema rather than inferring it from which
136
+ * branch happens to catch what.
137
+ *
138
+ * Read off every `method` const in `ServerRequest.json`, codex-cli 0.160.0,
139
+ * 2026-10-09. Six raise a card (APPROVAL_SHAPE above) and four cannot be served
140
+ * at all (CANNOT_SERVE below). A method in neither list is NEW, and `onUnknownRequest`
141
+ * exists so that it is news rather than a silence.
142
+ */
143
+ export const SERVER_REQUESTS = Object.freeze([
144
+ 'account/chatgptAuthTokens/refresh',
145
+ 'applyPatchApproval',
146
+ 'attestation/generate',
147
+ 'execCommandApproval',
148
+ 'item/commandExecution/requestApproval',
149
+ 'item/fileChange/requestApproval',
150
+ 'item/permissions/requestApproval',
151
+ 'item/tool/call',
152
+ 'item/tool/requestUserInput',
153
+ 'mcpServer/elicitation/request',
154
+ ]);
155
+
156
+ /**
157
+ * THE FOUR THAT ARE REFUSED ON PURPOSE, each with the reason in its own words.
158
+ *
159
+ * They used to share one anonymous error with the approvals, which is how
160
+ * nobody noticed they existed. All four ask the CLIENT to do something rather
161
+ * than asking a person to decide something, and there is no answer here to
162
+ * invent for any of them.
163
+ */
164
+ const CANNOT_SERVE = new Map([
165
+ ['item/tool/call', `it asks this app to run a tool of its own, and ${Name} offers the model no such tool`],
166
+ ['account/chatgptAuthTokens/refresh', `it asks this app to mint a fresh ChatGPT login token, which ${Name} does not hold and must not forge`],
167
+ ['attestation/generate', `it asks this app to sign an attestation, which ${Name} cannot produce`],
168
+ // THE ONE THAT IS A QUESTION FOR A PERSON, and the one worth coming back to.
169
+ // Its answer is a map of question id to typed answers
170
+ // (`ToolRequestUserInputResponse`), Agentbox has no form to put on screen, and a
171
+ // question may be flagged `isSecret` -- so an invented answer would be worse
172
+ // than a no. Pointing the model at its reply is the road that does reach her.
173
+ ['item/tool/requestUserInput', `it asks a question that needs a typed answer and ${Name} has no way to put that question on screen; ask it in your reply instead, where she will read it`],
276
174
  ]);
277
175
 
278
176
  const REFUSAL_ERROR = Object.freeze({
@@ -280,6 +178,46 @@ const REFUSAL_ERROR = Object.freeze({
280
178
  message: `${Name} did not approve this request. Continue with what you are already allowed to do, and leave the rest for the founder to look at.`,
281
179
  });
282
180
 
181
+ /** A refusal that names what was asked, so it is one somebody can act on. */
182
+ const cannotServe = (method, why) => Object.freeze({
183
+ code: -32001,
184
+ message: `${Name} cannot answer ${method}: ${why}.`,
185
+ });
186
+
187
+ const UNKNOWN_REQUEST = `${Name} does not recognise this request, so it is refused rather than guessed at`;
188
+
189
+ // Refuse in the method's own vocabulary where it has one. A declined
190
+ // elicitation leaves the turn running; the error frame is a protocol failure.
191
+ const refusal = (shape) => {
192
+ if (shape === 'decision') return { result: { decision: REFUSAL_DECISION } };
193
+ if (shape === 'elicitation') return { result: { action: REFUSAL_DECISION } };
194
+ // v1 SAYS NO WITH AN OBJECT, NOT A WORD. `decline` is not in its
195
+ // vocabulary at all, so a timed-out or unowned legacy card answered in our
196
+ // own word would be a protocol error on a question the server is waiting on.
197
+ if (shape === 'legacy') return { result: { decision: LEGACY_DECISIONS.decline } };
198
+ return { error: { ...REFUSAL_ERROR } };
199
+ };
200
+
201
+ /** This client's yes to `method`, without the JSON-RPC envelope, or null for a request a yes does not answer. */
202
+ export function yesTo(method) {
203
+ const shape = APPROVAL_SHAPE.get(method);
204
+ if (shape === 'decision') return { result: { decision: 'accept' } };
205
+ if (shape === 'elicitation') return { result: { action: 'accept' } };
206
+ return null;
207
+ }
208
+
209
+ /**
210
+ * The no this client sends `method` when nobody can be asked, without the
211
+ * JSON-RPC envelope: for the cloud server driving a Codex run with the
212
+ * computer shut (cloud/gateway/phone.mjs), so it refuses in these words.
213
+ */
214
+ export function refusalTo(method) {
215
+ const cannot = CANNOT_SERVE.get(method);
216
+ if (cannot) return { error: cannotServe(method, cannot) };
217
+ const shape = APPROVAL_SHAPE.get(method);
218
+ return shape ? refusal(shape) : { error: cannotServe(method, UNKNOWN_REQUEST) };
219
+ }
220
+
283
221
  const plain = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
284
222
  const only = (keys, allowed) => keys.every((k) => allowed.includes(k));
285
223
 
@@ -288,22 +226,22 @@ const only = (keys, allowed) => keys.every((k) => allowed.includes(k));
288
226
  *
289
227
  * Give it either a `transport` (anything child-process-shaped: `stdin.write`,
290
228
  * `stdin.end`, `stdout.on('data')`, an optional `stderr`, `on('exit')`,
291
- * `on('error')`, `kill`) or a `spawn` function that returns one. Production
292
- * passes a real child; the tests pass a notepad.
229
+ * `on('error')`, `kill`) or a `spawn` function that returns one.
293
230
  *
294
- * `onNotification` catches every line that belongs to no watched thread: a
295
- * thread we have not been told about yet, and the notifications that carry no
296
- * thread at all. There are eighteen of those in the 0.148.0 stable schema --
297
- * account, config, process, filesystem and remote-control news, among them
298
- * `account/rateLimits/updated` and `remoteControl/status/changed` -- so this is
299
- * an ordinary channel rather than an error path.
231
+ * `onNotification` gets every line for no watched thread, including the many
232
+ * account and config notifications that carry no thread at all.
300
233
  */
301
234
  export function createCodexAppServer({
302
235
  transport = null,
303
236
  spawn = null,
304
237
  clientInfo = CLIENT_INFO,
305
238
  onNotification = null,
306
- approvalTimeoutMs = APPROVAL_TIMEOUT_MS,
239
+ // A SERVER REQUEST THIS FILE HAS NEVER HEARD OF (w-36c51443df). Six known
240
+ // methods once shared one anonymous refusal, so a NEW one would have been
241
+ // indistinguishable from them and from nothing at all. Handed up rather than
242
+ // logged here, because what to do with it -- a line on the row, a report --
243
+ // belongs to the caller.
244
+ onUnknownRequest = null,
307
245
  requestTimeoutMs = REQUEST_TIMEOUT_MS,
308
246
  maxFrameLength = MAX_FRAME_LENGTH,
309
247
  stderrLimit = 64 * 1024,
@@ -313,7 +251,7 @@ export function createCodexAppServer({
313
251
 
314
252
  const pending = new Map(); // OUR request id -> { resolve, reject, method, onResult }
315
253
  const threads = new Map(); // thread id -> handlers
316
- const openCards = new Map(); // THEIR request id -> the deadline timer
254
+ const openCards = new Set(); // THEIR request ids, each waiting on a person
317
255
  const decoder = new StringDecoder('utf8');
318
256
  let nextId = 1;
319
257
  let buffer = '';
@@ -321,20 +259,13 @@ export function createCodexAppServer({
321
259
  let gone = null; // null while live; the closing reason once not
322
260
  let drained = false; // stdout has ended, so the diagnostics are complete
323
261
  let finished = false;
324
-
325
- /* ------------------------------- writing -------------------------------- */
262
+ let resumeLeavesTurns = true; // false once this Codex refuses `excludeTurns`
326
263
 
327
264
  /**
328
- * Put one message on the wire, or take the connection down.
329
- *
330
- * Three ways a real pipe fails and all three end here: it throws where it
331
- * stands, it accepts the bytes and reports the failure in the write callback,
332
- * or it raises `error` later. The callback is the one that is easy to miss
333
- * and the one that matters most, because a refusal that was "written" and
334
- * never arrived is indistinguishable from an approval.
335
- *
336
- * A `false` return is NOT a failure: that is ordinary backpressure and the
337
- * bytes are still queued. Only the callback and the throw are failures.
265
+ * Put one message on the wire, or take the connection down. A pipe can fail
266
+ * by throwing or in the write callback (or later with `error`). The callback
267
+ * matters most: a refusal that never arrived looks the same as no answer. A
268
+ * `false` return is only backpressure, not a failure.
338
269
  */
339
270
  function write(msg) {
340
271
  if (gone) return false;
@@ -358,8 +289,7 @@ export function createCodexAppServer({
358
289
  if (gone) return Promise.reject(new Error(`${method}: codex app-server is no longer running (${gone.reason})`));
359
290
  const id = nextId++;
360
291
  return new Promise((resolve, reject) => {
361
- // The deadline is armed BEFORE the write, so a request that is accepted
362
- // and then never answered cannot outlive it. See REQUEST_TIMEOUT_MS.
292
+ // Armed before the write. See REQUEST_TIMEOUT_MS.
363
293
  const timer = setTimeout(() => {
364
294
  if (!pending.delete(id)) return;
365
295
  reject(new Error(`${method}: codex app-server accepted this request and never answered it (${Math.round(requestTimeoutMs / 1000)}s)`));
@@ -376,11 +306,7 @@ export function createCodexAppServer({
376
306
 
377
307
  const notify = (method) => write({ jsonrpc: '2.0', method });
378
308
 
379
- /* ------------------------------- reading -------------------------------- */
380
-
381
- // The decoder rather than `chunk.toString`: a chunk boundary can fall in the
382
- // middle of a UTF-8 sequence, and agent messages stream through here one
383
- // delta at a time in whatever language the repo is written in.
309
+ // A StringDecoder, because a chunk can end in the middle of a UTF-8 sequence.
384
310
  child.stdout.on('data', (chunk) => {
385
311
  if (gone) return;
386
312
  buffer += typeof chunk === 'string' ? chunk : decoder.write(chunk);
@@ -395,13 +321,8 @@ export function createCodexAppServer({
395
321
  }
396
322
  });
397
323
 
398
- // The child writes tracing here, and the last line of it is usually the
399
- // reason something failed -- that is where "Rejected" showed up in the
400
- // measurements above. Worth keeping for a cause report, and NEVER worth
401
- // parsing: nothing on this pipe is protocol, however badly formed it looks,
402
- // so garbage here must not cost the founder a fleet. Only the tail is kept,
403
- // because a long run's tracing is unbounded and the useful part is always at
404
- // the end.
324
+ // The child's tracing. Its last lines usually say why something failed, so
325
+ // the tail is kept for cause reports. It is not protocol and is never parsed.
405
326
  child.stderr?.on('data', (chunk) => {
406
327
  stderrText += typeof chunk === 'string' ? chunk : chunk.toString();
407
328
  if (stderrText.length > stderrLimit) stderrText = stderrText.slice(-stderrLimit);
@@ -415,9 +336,7 @@ export function createCodexAppServer({
415
336
  for (const event of ['end', 'close']) child.stdout.on?.(event, () => { drained = true; if (gone) finish(); });
416
337
 
417
338
  function read(line) {
418
- // THE BOUND APPLIES TO A LINE THAT ENDED, TOO. The residual check below the
419
- // split can only ever see a frame with no newline in it, so a single
420
- // complete frame of any size walked past it into `JSON.parse`. See
339
+ // The data handler's check only sees an unterminated line. See
421
340
  // MAX_FRAME_LENGTH.
422
341
  if (line.length > maxFrameLength) {
423
342
  return fail('protocol', { note: `a frame of ${line.length} characters passed the ${maxFrameLength} this connection allows` });
@@ -435,8 +354,7 @@ export function createCodexAppServer({
435
354
  return fail('protocol', { note: 'a line on stdout could not be read as a message, and it may have been an approval request' });
436
355
  }
437
356
 
438
- // Three kinds, and the test is `!== undefined` in both places because the
439
- // server's ids start at zero.
357
+ // `!== undefined`, because the server's ids start at zero.
440
358
  if (msg.id !== undefined && msg.method !== undefined) return serverRequest(msg);
441
359
  if (msg.id !== undefined) return response(msg);
442
360
  if (typeof msg.method === 'string') return notification(msg);
@@ -452,11 +370,9 @@ export function createCodexAppServer({
452
370
  waiting.reject(new Error(`codex app-server refused ${waiting.method}: ${msg.error.message ?? JSON.stringify(msg.error)}`));
453
371
  return;
454
372
  }
455
- // Synchronously, INSIDE the read loop, before the next line of this chunk
456
- // is looked at. A promise resolves on a microtask and the rest of the chunk
457
- // is read on this one, so registering the thread's handlers off the
458
- // resolved promise would miss any notification the server packed in behind
459
- // the response -- which for `thread/start` is the thread's own first event.
373
+ // `onResult` runs before the next line is read. Registering a thread off
374
+ // the resolved promise would miss notifications sent right behind the
375
+ // response, such as the thread's first event.
460
376
  try { waiting.onResult?.(msg.result); } catch { /* the caller's bookkeeping, not ours */ }
461
377
  waiting.resolve(msg.result);
462
378
  }
@@ -468,94 +384,60 @@ export function createCodexAppServer({
468
384
  else call(() => onNotification?.(msg.method, msg.params));
469
385
  }
470
386
 
471
- /* ------------------------------- approvals ------------------------------ */
472
-
473
387
  /**
474
- * Answer a server request. NEVER awaits: the handler's promise is left to
475
- * settle on its own, because the founder may take fifteen minutes and every
476
- * other thread on this process reads through the same loop.
388
+ * Answer a server request. Never awaits the handler, because one card waits
389
+ * for a person and every thread reads through this loop.
477
390
  */
478
391
  function serverRequest(msg) {
479
- // A SECOND REQUEST ON AN ID THAT IS STILL OPEN IS NOT A SECOND CARD.
480
- //
481
- // `openCards` is keyed by the server's request id, so a duplicate used to
482
- // overwrite the entry without clearing the timer it replaced. The first
483
- // card's deadline then went on running and denied the SECOND card at its
484
- // own hour, and the first card's real answer -- her click -- landed on a map
485
- // entry that had already been swapped and was dropped.
486
- //
487
- // There is no way to answer two open requests that share one id: whichever
488
- // we write, the server matches it to one of them and the other hangs. That
489
- // is the same unresolvable input as an unreadable frame, so it gets the same
490
- // answer this file gives every one of those. Loud, and with `finish` denying
491
- // every card that was open, so nothing is left waiting on a decision it can
492
- // never receive.
392
+ // Two open requests on one id cannot both be answered, so treat it like an
393
+ // unreadable frame and end the connection.
493
394
  if (openCards.has(msg.id)) {
494
395
  return fail('protocol', { note: `codex app-server raised a second request on id ${msg.id} while the first was still open, and one answer cannot settle two` });
495
396
  }
397
+ // THREE ROADS NOW, AND A REQUEST TAKES ONE OF THEM BY DECISION (w-36c51443df).
398
+ //
399
+ // There used to be two: a card, or one anonymous error for everything else.
400
+ // Six of the ten requests the schema allows took the second, with nothing
401
+ // anywhere naming which -- so the only way to learn that
402
+ // `account/chatgptAuthTokens/refresh` was being refused was to read the
403
+ // schema by hand. Now a refusal says what it refused, and a method in
404
+ // neither table is REPORTED, because a new one arriving in silence is how
405
+ // this lasted.
406
+ const cannot = CANNOT_SERVE.get(msg.method);
407
+ if (cannot) return write({ jsonrpc: '2.0', id: msg.id, error: cannotServe(msg.method, cannot) });
408
+
496
409
  const shape = APPROVAL_SHAPE.get(msg.method);
497
- if (!shape) return write({ jsonrpc: '2.0', id: msg.id, error: { ...REFUSAL_ERROR } });
410
+ if (!shape) {
411
+ call(() => onUnknownRequest?.(msg.method, msg.params));
412
+ return write({ jsonrpc: '2.0', id: msg.id, error: cannotServe(msg.method, UNKNOWN_REQUEST) });
413
+ }
498
414
 
499
415
  const id = threadOf(msg.params);
500
416
  const handler = id === null ? null : threads.get(id)?.onApproval;
501
- // A refusal that is already certain does not wait out the clock: there is
502
- // nobody to ask, so the turn hears no now rather than in fifteen minutes.
417
+ // Nobody to ask, so refuse now rather than at the deadline.
503
418
  if (typeof handler !== 'function') return write({ jsonrpc: '2.0', id: msg.id, ...refusal(shape) });
504
419
 
505
- openCard(msg.id, shape);
420
+ openCards.add(msg.id);
506
421
  Promise.resolve()
507
422
  .then(() => handler(msg.method, msg.params))
508
423
  .then((answered) => answerCard(msg.id, translate(shape, answered) ?? refusal(shape)))
509
424
  .catch(() => answerCard(msg.id, refusal(shape)));
510
425
  }
511
426
 
512
- /** Start her deadline. Expiry denies, once, and closes the card. */
513
- function openCard(id, shape) {
514
- const timer = setTimeout(() => {
515
- if (openCards.delete(id)) write({ jsonrpc: '2.0', id, ...refusal(shape) });
516
- }, approvalTimeoutMs);
517
- timer?.unref?.();
518
- openCards.set(id, timer);
519
- }
520
-
521
427
  /**
522
- * The one answer this card will ever get.
523
- *
524
- * A card the deadline has already denied is CLOSED, so her late click lands
525
- * here and is dropped rather than approving something the turn stopped
526
- * waiting for twenty minutes ago. Same for a card whose connection died.
428
+ * The only answer a card gets. A card on a dead connection is already closed,
429
+ * so a late answer is dropped.
527
430
  */
528
431
  function answerCard(id, body) {
529
- if (!openCards.has(id)) return;
530
- clearTimeout(openCards.get(id));
531
- openCards.delete(id);
432
+ if (!openCards.delete(id)) return;
532
433
  write({ jsonrpc: '2.0', id, ...body });
533
434
  }
534
435
 
535
- // A REFUSAL IS SAID IN THE VOCABULARY THE METHOD TAKES, which for an
536
- // elicitation is `decline` rather than an error frame. Both reach the model
537
- // as a no; the difference is that a declined elicitation leaves the tool call
538
- // declined and the turn running, while the error frame is a protocol failure
539
- // on a request the server expected an answer to.
540
- const refusal = (shape) => {
541
- if (shape === 'decision') return { result: { decision: REFUSAL_DECISION } };
542
- if (shape === 'elicitation') return { result: { action: REFUSAL_DECISION } };
543
- return { error: { ...REFUSAL_ERROR } };
544
- };
545
-
546
436
  /**
547
- * The handler's answer, in the shape the method actually takes, or null if it
548
- * is not one this protocol version can express. Null is the whole
549
- * deny-by-default rule: everything unrecognised lands there.
550
- *
551
- * The elicitation's three actions are `accept`, `decline` and `cancel`, read
552
- * off `McpServerElicitationRequestResponse` in the CLI's own schema dump on
553
- * 2026-09-22. `acceptForSession` is NOT among them, so a handler that answers
554
- * a tool call with the word a command approval uses is refused rather than
555
- * quietly turned into a yes. `content` is omitted: the accept this app sends
556
- * answers a permission question whose `requestedSchema` has no properties,
557
- * and a form that really does ask for typed input is not something Agentbox can
558
- * fill in for her.
437
+ * The handler's answer in the shape the method takes, or null, which means
438
+ * refuse. An elicitation has no `acceptForSession`, so that word is refused
439
+ * rather than turned into a yes. `content` is never sent, because Agentbox
440
+ * only answers elicitations that ask for no input.
559
441
  */
560
442
  function translate(shape, answered) {
561
443
  if (shape === 'decision') {
@@ -564,27 +446,24 @@ export function createCodexAppServer({
564
446
  if (shape === 'elicitation') {
565
447
  return ELICITATION_ACTIONS.includes(answered) ? { result: { action: answered } } : null;
566
448
  }
449
+ if (shape === 'legacy') {
450
+ // Our word in, Codex's older word out. A word that is not one of our four
451
+ // answers null, which lands on the deny-by-default rule exactly as it
452
+ // does for every other shape.
453
+ const said = Object.hasOwn(LEGACY_DECISIONS, answered) ? LEGACY_DECISIONS[answered] : null;
454
+ return said ? { result: { decision: said } } : null;
455
+ }
567
456
  const grant = grantFrom(answered);
568
457
  return grant ? { result: grant } : null;
569
458
  }
570
459
 
571
460
  /**
572
- * A permissions grant, CHECKED FIELD BY FIELD AND THEN BUILT FRESH.
573
- *
574
- * This is the one server request whose answer is a document rather than a
575
- * word, and the document widens a sandbox. Forwarding the caller's object
576
- * would mean checking one value and sending another: a `toJSON`, or a getter,
577
- * runs again inside JSON.stringify, and what reaches Codex is whatever it
578
- * returns the second time. So nothing the handler returned reaches the wire
579
- * by reference, every value is read exactly once, and an unknown key anywhere
580
- * -- on the object, in the profile, or in either area -- is a refusal rather
581
- * than something to pass along. Inherited properties do not count, so a grant
582
- * on a prototype is not a grant.
583
- *
584
- * The `entries` form of the filesystem profile is deliberately NOT accepted.
585
- * Its shape has three nested variants, none of them measured, and a grant
586
- * this file cannot check is a grant it has no business sending. The slice
587
- * that needs it measures it.
461
+ * A permissions grant, checked field by field and built fresh. This answer
462
+ * widens a sandbox, so nothing the handler returned is sent by reference: a
463
+ * getter or `toJSON` could give JSON.stringify a different value from the one
464
+ * checked. Any unknown key is a refusal, and inherited properties do not
465
+ * count. The `entries` form of the filesystem profile is refused because its
466
+ * shape is unmeasured.
588
467
  */
589
468
  function grantFrom(answered) {
590
469
  if (!plain(answered)) return null;
@@ -626,8 +505,7 @@ export function createCodexAppServer({
626
505
  }
627
506
 
628
507
  const grant = { permissions };
629
- // `turn` when nobody said, because it is the narrower of the two and a
630
- // grant that outlives the question it answered is not that answer.
508
+ // Default to `turn`, the narrower scope.
631
509
  if (top.includes('scope')) {
632
510
  if (answered.scope !== 'turn' && answered.scope !== 'session') return null;
633
511
  grant.scope = answered.scope;
@@ -641,9 +519,8 @@ export function createCodexAppServer({
641
519
  return grant;
642
520
  }
643
521
 
644
- // Almost every line carries `params.threadId`. `thread/started` is the
645
- // exception and keeps it at `params.thread.id`, which is the thread's own
646
- // first event and therefore the worst one to lose.
522
+ // Most lines carry `params.threadId`, but `thread/started` has
523
+ // `params.thread.id`.
647
524
  function threadOf(params) {
648
525
  if (!plain(params)) return null;
649
526
  if (typeof params.threadId === 'string') return params.threadId;
@@ -651,17 +528,14 @@ export function createCodexAppServer({
651
528
  return null;
652
529
  }
653
530
 
654
- // A listener that throws must not take the read loop with it: one thread's
655
- // bad handler would otherwise stop every other thread on this process.
531
+ // A listener that throws must not stop the read loop for every other thread.
656
532
  function call(fn) {
657
533
  try { fn(); } catch { /* the caller's problem, and not worth the fleet */ }
658
534
  }
659
535
 
660
- /* ------------------------------ the ending ------------------------------ */
661
-
662
536
  /** Why this connection ended, in a sentence that names something actionable. */
663
- function cause() {
664
- const tail = stderrText.trim().split('\n').slice(-3).join(' ').trim();
537
+ function cause({ withTail = true } = {}) {
538
+ const tail = withTail ? stderrText.trim().split('\n').slice(-3).join(' ').trim() : '';
665
539
  const said = tail ? `: ${tail}` : '';
666
540
  if (!gone) return `the connection to codex app-server failed on write${said}`;
667
541
  switch (gone.reason) {
@@ -674,14 +548,9 @@ export function createCodexAppServer({
674
548
  }
675
549
 
676
550
  /**
677
- * The connection is over, whatever ended it.
678
- *
679
- * Marked SYNCHRONOUSLY, so that from this instant no further request is
680
- * accepted and no late approval can be written. Finished once stdout has
681
- * drained, so the tracing tail that explains it is complete rather than
682
- * whatever happened to have arrived by the moment `exit` fired -- exit
683
- * routinely beats the last of the pipe, and "exited with code 1" on its own
684
- * is not a cause anybody can act on.
551
+ * The connection is over. It is marked at once, so no further request or late
552
+ * approval is written, and finished once stdout drains, because `exit` often
553
+ * arrives before the last of the tracing that explains it.
685
554
  */
686
555
  function fail(reason, detail = {}) {
687
556
  if (gone) return;
@@ -698,21 +567,17 @@ export function createCodexAppServer({
698
567
  }
699
568
 
700
569
  /**
701
- * Everything that was waiting finds out, once, and nothing stays registered.
702
- *
703
- * A promise nobody settles is exactly how a work item strands, so an
704
- * in-flight request rejects rather than staying pending, and every watched
705
- * thread is told rather than left listening to a dead pipe. Deadlines are
706
- * cancelled and registrations dropped: a card that can never be answered and
707
- * a thread that can never be spoken to are only holding this client alive.
570
+ * Settles everything once. Pending requests reject, watched threads get
571
+ * `onClosed`, and every timer and registration is dropped, so no work item is
572
+ * left waiting and nothing keeps the client alive.
708
573
  */
709
574
  function finish() {
710
575
  if (finished) return;
711
576
  finished = true;
712
577
  gone.stderr = stderrText;
578
+ gone.why = cause({ withTail: false });
713
579
  const why = cause();
714
580
 
715
- for (const timer of openCards.values()) clearTimeout(timer);
716
581
  openCards.clear();
717
582
 
718
583
  for (const [id, waiting] of pending) {
@@ -725,8 +590,6 @@ export function createCodexAppServer({
725
590
  for (const handlers of listening) call(() => handlers.onClosed?.(gone));
726
591
  }
727
592
 
728
- /* -------------------------------- the API ------------------------------- */
729
-
730
593
  const watch = (threadId, handlers = {}) => { threads.set(threadId, handlers); return threadId; };
731
594
 
732
595
  return {
@@ -734,15 +597,10 @@ export function createCodexAppServer({
734
597
  request: (method, params) => send(method, params),
735
598
 
736
599
  /**
737
- * The whole handshake, not half of it: the request, and then `initialized`
738
- * once the server has answered.
739
- *
740
- * `initialized` is the only client notification in the 0.148.0 schema and
741
- * the documented lifecycle puts it before any other operation. 0.148
742
- * tolerates its absence, which is why the first draft never noticed it was
743
- * missing; a later one need not. It is sent from the response hook, so it
744
- * can never overtake the response, and it is never sent at all if the
745
- * server refused the handshake.
600
+ * The handshake: `initialize`, then the `initialized` notification once the
601
+ * server answers. The protocol wants it before any other call, though 0.148
602
+ * does not enforce that. Sent from the response hook, so it cannot overtake
603
+ * the response and is not sent if the server refuses.
746
604
  */
747
605
  initialize: () => send('initialize', { clientInfo }, () => notify('initialized')),
748
606
 
@@ -760,27 +618,36 @@ export function createCodexAppServer({
760
618
  ).then((result) => ({ threadId: result?.thread?.id ?? null, thread: result?.thread ?? null, result })),
761
619
 
762
620
  /**
763
- * Continue a thread we already know the id of, so the watch goes on first
764
- * and no event of the resumed thread can arrive unowned.
621
+ * Continue a known thread. The watch goes on before the request, so no
622
+ * event of the resumed thread arrives unowned, and comes off if the server
623
+ * refuses the resume.
765
624
  *
766
- * A resume the server REFUSES never became a thread, so the watch is rolled
767
- * back. Left installed, it would hand every later line carrying that id to
768
- * a listener for a session that does not exist.
625
+ * Without `excludeTurns` the answer carries every turn in one frame, and a
626
+ * long thread passes `maxFrameLength`, which ends this connection for every
627
+ * thread on it. Codex before 0.155 refuses the field without the
628
+ * experimental API, so that Codex is asked again in full.
769
629
  */
770
630
  resumeThread: (params = {}, handlers = {}) => {
771
631
  const id = typeof params.threadId === 'string' ? params.threadId : null;
772
632
  if (id) watch(id, handlers);
773
- return send('thread/resume', params).catch((err) => {
774
- if (id && threads.get(id) === handlers) threads.delete(id);
775
- throw err;
776
- });
633
+ const lean = resumeLeavesTurns;
634
+ return send('thread/resume', lean ? { ...params, excludeTurns: true } : params)
635
+ .catch((err) => {
636
+ if (!lean || !/excludeTurns/.test(err?.message ?? '')) throw err;
637
+ resumeLeavesTurns = false;
638
+ return send('thread/resume', params);
639
+ })
640
+ .catch((err) => {
641
+ if (id && threads.get(id) === handlers) threads.delete(id);
642
+ throw err;
643
+ });
777
644
  },
778
645
 
779
646
  startTurn: (threadId, params = {}) => send('turn/start', { threadId, ...params }),
780
647
 
781
648
  /** Both ids: a turn is not identified by either one alone. */
782
649
  interruptTurn: (threadId, turnId) => send('turn/interrupt', { threadId, turnId }),
783
- steerTurn: (threadId, expectedTurnId, text) => send('turn/steer', {threadId, expectedTurnId, input:[{type:'text',text}]}),
650
+ steerTurn: (threadId, expectedTurnId, text, pictures = []) => send('turn/steer', {threadId, expectedTurnId, input:[{type:'text',text}, ...pictures.map((p) => ({type:'localImage', path:p}))]}),
784
651
 
785
652
  watch,
786
653
  unwatch: (threadId) => threads.delete(threadId),