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,196 +1,83 @@
1
- // ONE CODEX WORK ITEM, SHAPED LIKE A CHILD PROCESS.
1
+ // One Codex work item, shaped like a child process.
2
2
  //
3
- // Agentbox spawns one child per work item and hangs everything off it: the
4
- // supervisor's `session.child` is a ChildProcess and every other part of
5
- // `spawnWorker` is written against that. A Codex worker has no per-item
6
- // process. It is ONE shared `codex app-server` plus a thread inside it, proven
7
- // on 2026-09-04 by running several concurrently: independent thread ids,
8
- // interleaved progress, and no head-of-line blocking when one of them parked on
9
- // an unanswered approval.
3
+ // A Codex worker is a thread inside one shared `codex app-server`, not a process
4
+ // of its own. The supervisor uses five things on a worker's child:
5
+ // `once('spawn')`, `stdout.on('data')`, `stderr.on('data')`, `on('exit')` and
6
+ // `kill`. This module is a facade over one thread that provides them.
10
7
  //
11
- // So the question this file answers is whether the supervisor has to learn
12
- // about threads, and the measured answer is no. `session.child` is referenced
13
- // in exactly ONE place in the whole 4,273-line supervisor
14
- // (`session.child.kill`), and the local `child` handle inside `spawnWorker` is
15
- // used in exactly five ways:
8
+ // It emits `event(method, params)` instead of faking JSON lines on stdout, so
9
+ // the field-name differences between app-server and `codex exec` stay visible.
10
+ // The supervisor swaps its readers per session (`readersFor` in
11
+ // main/supervisor.mjs).
16
12
  //
17
- // child.once('spawn') child.stderr.on('data') child.kill
18
- // child.stdout.on('data') child.on('exit')
13
+ // stderr is real. The supervisor classifies a dead run from its stderr lines
14
+ // (`troubleCause`), and Codex's own refusal wording is what lets it say why.
19
15
  //
20
- // That five-method contract is the entire seam, and this module is a facade
21
- // over one thread that satisfies it. The supervisor keeps being a file about
22
- // running sessions rather than a file about two vendors' flag names, which is
23
- // the sentence the deleted 08-25 design used to justify the same choice.
24
- //
25
- // FOUR OF THE FIVE ARE REAL AND ONE IS DELIBERATELY MISSING.
26
- //
27
- // `stdout` IS NOT FAKED. It would be easy to re-serialise every notification
28
- // into a JSON line so the existing readers worked untouched, and it would be
29
- // objects -> JSONL -> objects for no gain: it would also hide the real
30
- // camelCase/snake_case divergence between `codex app-server` and the
31
- // `codex exec` stream the removed August build parsed, which is precisely the
32
- // thing that must stay visible. So this facade emits `event(method, params)`
33
- // and the supervisor swaps its three readers once per session instead of once
34
- // per line. See `readersFor` in main/supervisor.mjs.
35
- //
36
- // `stderr` IS REAL, and it is why this file writes to it at all. The supervisor
37
- // classifies a dead run by reading the `stderr:` lines off the session tail
38
- // (`sayTheRunDied` -> `troubleCause`), and Codex's own refusal wording -- "Not
39
- // inside a trusted directory and --skip-git-repo-check was not specified" -- is
40
- // what makes the difference between her reading "the folder is not one Codex
41
- // will work in" and reading the generic sentence. A failure that never reaches
42
- // this pipe is a failure filed as 'unknown'.
43
- //
44
- // AND THE ONE THING THIS FILE OWES `settleDelivery`: AN ENDING THAT WAS NOT THE
45
- // WORK ITEM'S FAULT MUST BE TELLABLE FROM ONE THAT WAS. CLAUDE.md's rule is that a session WE killed does not
46
- // spend one of an item's three delivery attempts, "because every restart kills
47
- // the fleet and charging that to the item is how restarts strand work". The
48
- // supervisor's own `_kill` writes `stoppedByUs` on the session, and this facade
49
- // ALSO sets `signal` on the exit -- both halves, deliberately, because
50
- // `settleDelivery` reads `signal || session.stoppedByUs` and a facade that
51
- // reported a clean `null` signal on an interrupt would be lying about the one
52
- // fact that protects her work.
53
- //
54
- // THE EXIT THEREFORE CARRIES A THIRD ARGUMENT, which a real ChildProcess never
55
- // sends and the supervisor reads as undefined for every Claude worker. It says
56
- // one thing today: the shared app-server died under this thread. ONE process
57
- // carries the whole Codex fleet, so a single unreadable frame reaches every live
58
- // thread at once, and each of those threads is a work item -- ending them with a
59
- // bare `end(1, null)` spent one of every item's three delivery attempts on a
60
- // fault none of them caused. See `onClosed` below.
16
+ // The exit sets `signal` on our own kill, because `settleDelivery` reads it to
17
+ // keep a kill from spending a delivery attempt. It also carries a third
18
+ // argument a real ChildProcess never sends: `{ transportFault: true }` when the
19
+ // shared app-server died under this thread. See `onClosed` below.
61
20
 
62
21
  import { EventEmitter } from 'node:events';
63
22
  import fs from 'node:fs';
64
23
  import os from 'node:os';
65
24
  import path from 'node:path';
66
25
  import { Name } from '../shared/product-name.mjs';
67
- import { codexPosture } from '../shared/codex-modes.mjs';
26
+ import { codexPosture, codexReviewer } from '../shared/codex-modes.mjs';
68
27
 
69
28
  /**
70
- * WHAT A WORKER THREAD IS ALLOWED TO DO. The sandbox is the wall; the approval
71
- * policy only decides who gets interrupted.
72
- *
73
- * THIS WAS `untrusted` UNTIL 2026-09-23 AND IT MADE CODEX UNUSABLE. The founder
74
- * reported that Codex asked about nearly everything it wanted to run, to the
75
- * point that the only viable way to use it was to leave this app and drive the
76
- * Codex app directly (w-07f98d5a1c).
77
- *
78
- * She was right, and the note that stood here was wrong in a way worth writing
79
- * down because it is easy to make twice. It said the other policies "did not
80
- * card a command" and concluded they gated nothing. NOT CARDING AND NOT
81
- * STOPPING ARE DIFFERENT THINGS. Under `workspace-write` the sandbox refuses on
82
- * its own, silently, and a card was never what held the wall up.
83
- *
84
- * MEASURED 2026-09-23 on codex-cli 0.153.4. Both scripts are kept:
85
- *
86
- * scripts/count-what-codex-asks.mjs -- one ordinary task: read a file, edit
87
- * it, run node to check the result.
88
- * untrusted 3 interruptions, task completed
89
- * on-request 0 interruptions, task completed, same edit
90
- *
91
- * scripts/can-codex-escape-on-request.mjs -- the counter-test: write to $HOME
92
- * and curl the open internet.
93
- * untrusted 2 cards, both denied, nothing escaped
94
- * on-request 0 cards, AND NOTHING ESCAPED EITHER. The sandbox answered
95
- * "operation not permitted" for the write and "could not
96
- * resolve host" for the curl.
97
- *
98
- * So `untrusted` bought three interruptions on ordinary work and no containment
99
- * `workspace-write` was not already giving us.
100
- *
101
- * WHAT THE SCHEMA SAYS, read off `codex app-server generate-json-schema` on that
102
- * same version rather than remembered: `AskForApproval` is
103
- * `untrusted | on-request | never | {granular:{...}}`, and
104
- * `SandboxWorkspaceWrite.network_access` DEFAULTS TO FALSE. Note that the
105
- * interactive CLI has dropped `untrusted` from `--ask-for-approval` altogether
106
- * and offers only `on-request` and `never`; the app-server protocol still
107
- * accepts it, which is how this went on working while being the wrong choice.
108
- *
109
- * `on-request` IS WHAT THE CODEX APP ITSELF RUNS, which is the whole of what she
110
- * asked for: work inside the sandbox unasked, ask only to get out. It keeps the
111
- * escape hatch rather than removing it, so a run that genuinely needs the
112
- * network can still request it and she still gets a card. One caveat stated
113
- * rather than implied: the escape probe TOLD the model not to work around a
114
- * refusal, so its zero cards prove the sandbox refuses by default, not that
115
- * `on-request` never asks for escalation.
116
- *
117
- * THE PATCH IS STILL CARDED WHERE IT MATTERS and that machinery is untouched:
118
- * `FileChangeRequestApprovalParams` carries no path and no diff, so
119
- * main/codex-approvals.mjs draws the card off the `item/started` for the same
120
- * itemId, which arrives first and carries the paths and the change.
121
- *
122
- * `workspace-write` is the sandbox and it does not move: a worker may write in
123
- * the folder it was given and nowhere else, and has no network. That is what
124
- * `--permission-mode acceptEdits` buys on the other engine.
29
+ * What a worker thread may do. The sandbox is the wall; the approval policy
30
+ * only decides who gets interrupted.
31
+ *
32
+ * `workspace-write` lets a worker write in its own folder and nowhere else,
33
+ * with no network (`network_access` defaults to false). The sandbox refuses
34
+ * anything else on its own, without a card. `on-request` is what the Codex app
35
+ * runs: work inside the sandbox without asking, and ask only to get out.
36
+ * `untrusted` asked about ordinary commands and contained nothing more.
37
+ *
38
+ * The sandbox refusing is only half of it: the model then asks to be let out,
39
+ * and who answers that ask is `approvalsReviewer`, not this policy. See
40
+ * `workerThreadParams`, which names it on every start.
41
+ *
42
+ * File changes are still carded. The approval request carries no path or diff,
43
+ * so main/codex-approvals.mjs draws the card from the `item/started` for the
44
+ * same itemId.
125
45
  */
126
46
  export const WORKER_APPROVAL_POLICY = 'on-request';
127
47
  export const WORKER_SANDBOX = 'workspace-write';
128
48
 
129
49
  /**
130
50
  * How long a worker waits for its store server before giving up on the run.
131
- *
132
- * Measured 2026-09-04: a working server went from `starting` to `ready` in well
133
- * under a second, and a broken one failed, was retried by Codex, and failed
134
- * again inside twenty-five seconds. Thirty covers the retry and still costs a
135
- * genuinely dead machine half a minute rather than her fifteen.
51
+ * Codex retries a server that fails to start, and this leaves room for the retry.
136
52
  */
137
53
  export const MCP_READY_MS = 30_000;
138
54
 
139
- /* * * TURN THE FOUNDER'S OWN MCP SERVERS OFF FOR A WORKER, BY NAME. * * A bare `thread/start`
140
- inherits every MCP server in her `~/.codex/config.toml`. * Measured 2026-09-04 on codex-cli
141
- 0.148.0, thread start only and no turn: a * plain start reported
142
- `mcpServer/startupStatus/updated` for `playwright`, * `context7` and `codex_apps`, so a
143
- the app worker would silently launch two npx * processes of hers on every spawn. Nobody asked
144
- for that and nothing on her * screen would say it happened. * * THE OBVIOUS FIX DOES NOT
145
- WORK, WHICH IS WHY THIS IS A LIST OF NAMES AND NOT * AN EMPTY TABLE. All measured the same
146
- day, same probe: * * `config: { mcp_servers: {} }` -> all three still started. It used to
147
- also be what an * unreadable `config/read` degraded to, and that was the whole of the bug:
148
- an * empty list is `{}`, `{}` is the no-op measured above, and the no-op starts * every
149
- server she has inside a headless worker. * * `codex_apps` IS NOT ON THIS TABLE AND CANNOT
150
- BE, measured 2026-09-04 on * codex-cli 0.148.0, thread start only: * * with `codex_apps:
151
- {enabled:false}` beside her two -> `thread/start` REFUSED, * -32600 "failed to load
152
- configuration: invalid transport in * `mcp_servers.codex_apps`" * with her two named off
153
- and `codex_apps` left alone -> the only startup * notifications on the thread were
154
- `codex_apps=starting`, `codex_apps=ready` * * An `enabled: false` entry is a MERGE onto a
155
- server that already exists in her * config; Codex's own built-in app connector is not in
156
- her config and has no * transport to merge onto, so naming it refuses the thread rather
157
- than * disabling it. It therefore stays enabled on every worker, deliberately. It is *
158
- Codex's own connector rather than a process of hers, so nothing of hers runs * because of
159
- it -- but it is not nothing, and the slice that needs it off is * the slice that finds the
160
- switch Codex actually offers for it. * * That measurement is also why the names must be
161
- READ AT EVERY START rather * than cached for the app-server's life: a name that is stale
162
- because she * deleted the server does not merely fail to disable anything, it refuses the *
163
- thread outright. * * AND THE STORE SERVER GOES BACK IN THROUGH THE SAME TABLE. Switching
164
- every * server off and adding nothing is what the first slice shipped, and it left a *
165
- Codex worker unable to claim, checkpoint or finish a row: CLAUDE.md's * 2026-08-05
166
- incident, "five sessions of analysis written to nowhere", arriving * through a second
167
- engine. Measured 2026-09-04 on codex-cli 0.148.0, one * `thread/start` and no turn: an
168
- entry added BESIDE the `enabled: false` ones * started and reported ready while
169
- `playwright` and `context7` still produced * nothing at all, so one table carries both
170
- halves and the isolation is intact. * * Ours is written LAST, deliberately. If she ever
171
- runs a server of her own * under this name the store is the one that has to win, because a
172
- worker with * no store is worse than a worker without her tool.
173
- */
55
+ /**
56
+ * The `mcp_servers` override for a worker: every server in the user's Codex
57
+ * config switched off by name, plus the store server.
58
+ *
59
+ * Codex merges this onto the user's config, so an empty table switches nothing
60
+ * off and each server has to be named with `enabled: false`. Only names that
61
+ * exist may be used: an `enabled: false` entry with no server under it makes
62
+ * `thread/start` refuse with "invalid transport". That is why the names are
63
+ * read at every start, and why Codex's built-in `codex_apps` is not named here
64
+ * (`features.apps` in `workerThreadParams` turns it off).
65
+ *
66
+ * The store server is written last, so it wins over a server of the user's
67
+ * with the same name.
68
+ */
174
69
  export function mcpIsolation(names = [], storeServer = null) {
175
- // NULL-PROTOTYPE, BECAUSE ONE OF HER SERVERS MAY BE CALLED `__proto__`.
176
- // `{}[('__proto__')] = x` reaches Object.prototype's setter: the assignment
177
- // appears to work, `Object.keys` does not list it, and JSON.stringify does
178
- // not emit it -- so the one server named after the thing that hides it would
179
- // be the one server left running inside a worker. Nothing else here changes:
180
- // JSON.stringify treats a null-prototype object exactly as a plain one.
70
+ // Null prototype, because a server may be named `__proto__`. On a plain
71
+ // object that key hits the prototype setter, is never serialised, and the
72
+ // server would stay on.
181
73
  const mcp_servers = Object.create(null);
182
74
  for (const name of names) {
183
75
  if (typeof name === 'string' && name) mcp_servers[name] = { enabled: false };
184
76
  }
185
77
  if (storeServer?.name && storeServer.command) {
186
78
  const entry = { command: storeServer.command, env: storeServer.env ?? {} };
187
- // `args` IS CARRIED WHEN THERE IS ONE, and Agentbox's own launcher has none:
188
- // `storeMcpCommand` is a single executable, exactly as the Claude path
189
- // passes it. It is here because dropping a caller's argv in silence is a
190
- // server that starts and never handshakes, which was measured against a
191
- // real app-server while writing this -- the process came up, waited on
192
- // stdin forever, and the only symptom was a run that stopped saying the
193
- // store never reported ready.
79
+ // Agentbox's own launcher has no args, but a server whose args are dropped
80
+ // starts and never completes its handshake, so pass them when given.
194
81
  if (Array.isArray(storeServer.args) && storeServer.args.length) entry.args = [...storeServer.args];
195
82
  mcp_servers[storeServer.name] = entry;
196
83
  }
@@ -198,87 +85,36 @@ export function mcpIsolation(names = [], storeServer = null) {
198
85
  }
199
86
 
200
87
  /**
201
- * THE ONE FOLDER THIS THREAD RUNS IN, DECLARED UNTRUSTED TO CODEX.
202
- *
203
- * `mcpIsolation` above switches off the servers named in HER config, and that
204
- * is all it can do, because those are the only names `config/read` returns. A
205
- * `.codex/config.toml` sitting in the product folder is a config LAYER of
206
- * Codex's own, and the per-thread override is a deep merge, so a server that
207
- * layer declares was never named and never switched off. Measured on this Mac
208
- * 2026-09-05, codex-cli 0.148.0, real `codex app-server`, scratch CODEX_HOME:
209
- *
210
- * proj/.codex/config.toml declares `planted` = sh -c "echo pwned > MARK"
211
- * thread/start cwd=proj, mcp_servers = { herone: { enabled: false } }
212
- * -> mcpServer/startupStatus/updated planted=starting, and MARK was on
213
- * disk. The plant RAN.
214
- *
215
- * So a repository she cloned -- or an earlier worker writing inside one --
216
- * plants a command the next Codex thread LAUNCHES, outside the command sandbox
217
- * and outside the approval path, with no card and nothing on her screen.
218
- *
219
- * NAMING IT THE WAY `mcpIsolation` NAMES HERS DOES NOT WORK. Measured the same
220
- * hour: `{ planted: { enabled: false } }` answered -32600 "failed to load
221
- * configuration: invalid transport in `mcp_servers.planted`", because an
222
- * `enabled: false` entry is a merge onto a server in the TRUSTED config and an
223
- * untrusted project layer is not in it. Asking `config/read` WITH the thread's
224
- * cwd is no help either: it returned the same home names, and only
225
- * `includeLayers: true` showed the layer at all -- carrying a `disabledReason`
226
- * while `thread/start` went ahead and started the server regardless.
227
- *
228
- * CODEX'S OWN PROJECT-TRUST GATE IS THE SWITCH, AND IT IS PER THREAD. Measured
229
- * on the same probe:
230
- *
231
- * config.projects[<cwd>].trust_level = "untrusted"
232
- * -> no startup notification at all, MARK never written, and the store
233
- * server Agentbox puts there itself still started beside it.
234
- * with [projects."<proj>"] trust_level = "trusted" already in the home
235
- * config.toml, the per-thread untrusted STILL won.
236
- * cwd two levels below the `.codex` folder: unguarded the plant ran, and
237
- * naming only the cwd stopped it.
238
- *
239
- * AND IT STOPS AGENTBOX WRITING IN HER CODEX CONFIG, which nothing had noticed.
240
- * Without this, `thread/start` appended [projects."<cwd>"] trust_level =
241
- * "trusted" to CODEX_HOME's own config.toml -- permanently, and for her
242
- * terminal too, so the first Agentbox run in a folder quietly made that folder's
243
- * `.codex` live for every `codex` she types afterwards. With it, nothing was
244
- * written.
245
- *
246
- * A cwd that is not a real string answers `{}` rather than a guess: a table
247
- * with an empty key is a table Codex refuses, and that would cost the run over
248
- * a fact nobody stated.
88
+ * Marks the thread's folder untrusted to Codex, so a `.codex/config.toml` inside
89
+ * it cannot add MCP servers.
90
+ *
91
+ * That file is a config layer of its own. `config/read` does not return its
92
+ * servers, and naming one with `enabled: false` makes `thread/start` refuse.
93
+ * Without this, a cloned repository can plant a command that Codex launches
94
+ * outside the sandbox and the approval path. A per-thread `trust_level` of
95
+ * "untrusted" stops it, wins over a "trusted" entry in the home config, and
96
+ * works even when the `.codex` folder sits above the cwd. It also stops
97
+ * `thread/start` from writing a "trusted" entry for the cwd into the user's own
98
+ * Codex config.
99
+ *
100
+ * A cwd that is not a non-empty string gives `{}`, because Codex refuses a
101
+ * table with an empty key.
249
102
  */
250
103
  export function projectIsolation(cwd) {
251
- // NULL-PROTOTYPE for the reason `mcpIsolation` gives: a path is any string,
252
- // `__proto__` included, and a plain `{}` drops that one key in silence --
253
- // leaving the folder named after the thing that hides it the one folder that
254
- // is still trusted.
104
+ // Null prototype for the reason `mcpIsolation` gives: a path can be `__proto__`.
255
105
  const projects = Object.create(null);
256
106
  if (typeof cwd === 'string' && cwd) projects[cwd] = { trust_level: 'untrusted' };
257
107
  return projects;
258
108
  }
259
109
 
260
110
  /**
261
- * THE NAMES IN A `config/read` RESULT, OR A REFUSAL TO GUESS.
262
- *
263
- * This used to answer `[]` for every shape it did not recognise, on the reading
264
- * that isolation which cannot read her config should degrade to "turn nothing
265
- * off". IT DEGRADED TO THE OPPOSITE. `[]` builds `{}`, and `{}` is the deep
266
- * merge measured above that disables nothing at all -- so the failure path
267
- * started every MCP server she has inside a headless worker, on this Mac two
268
- * `npx` processes per spawn, with nothing on her screen saying it happened.
269
- *
270
- * So there are two answers now and they are not the same answer:
111
+ * The server names in a `config/read` result.
271
112
  *
272
- * SHE HAS NONE -> `[]`. An empty table, no `mcp_servers` key at all, or an
273
- * explicit null. The empty override really is correct here, because there
274
- * is nothing of hers for it to fail to disable.
275
- * WE COULD NOT TELL -> THROW. A read that errored, a result with no `config`,
276
- * an `mcp_servers` that is a list or a string. The caller stops the worker
277
- * before `thread/start` rather than starting one that inherits everything.
278
- *
279
- * A throw is safe on the spawn path by construction: `createCodexWorker` awaits
280
- * this inside its own promise chain, and the `catch` at the end of that chain
281
- * ends the worker with the message on the stderr `troubleCause` reads.
113
+ * No servers at all (no `mcp_servers`, null, or an empty table) gives `[]`.
114
+ * Any shape we cannot read is a THROW rather than a guess, because `[]` would
115
+ * build an empty override that switches nothing off and the worker would start
116
+ * every server the user has. `createCodexWorker` catches the throw and ends the
117
+ * worker with the message on stderr.
282
118
  */
283
119
  export function mcpServerNames(configRead) {
284
120
  const cannot = () => {
@@ -296,60 +132,37 @@ const plainObject = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
296
132
  /**
297
133
  * The `thread/start` params for one worker.
298
134
  *
299
- * `ephemeral: false` IS NOT A DEFAULT WORTH RELYING ON HERE, even though
300
- * main/codex-app-server.mjs supplies the same value. It is the difference
301
- * between a row that can be replied to and one that cannot: measured
302
- * 2026-09-04, `thread/resume` on an ephemeral thread fails `no rollout found`,
303
- * and continuation-on-reply is most of what this app does. Written where a
304
- * reader of this file can see it rather than inherited from another one.
305
- *
306
- * `projects` IS THE SECOND HALF OF THE ISOLATION AND NOT AN EXTRA. `mcp_servers`
307
- * can only name the servers in HER config; `projects` is what keeps the config
308
- * layer inside the folder the thread runs in from contributing any of its own.
309
- * See `projectIsolation` for the measurement.
310
- *
311
- * `features.apps` IS THE THIRD, AND IT IS THE SWITCH `mcpIsolation` SPENT A
312
- * SLICE FAILING TO FIND. `codex_apps` cannot be named in `mcp_servers` -- that
313
- * refuses the thread, measured 2026-09-04 -- because it is not one of her
314
- * servers at all: it is Codex's own connector to the apps she has linked to her
315
- * ChatGPT account, and it is gated by a FEATURE. Measured 2026-09-05,
316
- * codex-cli 0.148.0, real app-server:
317
- *
318
- * experimentalFeature/list -> { name: "apps", stage: "stable",
319
- * enabled: true, defaultEnabled: true }
320
- * thread/start config.features.apps = false -> accepted, no refusal
321
- * experimentalFeature/list { threadId } for that thread -> apps enabled FALSE
322
- * the same call for a thread started without it -> apps enabled true
323
- *
324
- * (`config.apps` is not it and was tried first: `{ apps: { enabled: false } }`
325
- * answered "invalid type: boolean `false`, expected struct AppConfig in
326
- * `apps`".)
327
- *
328
- * WHAT IS NOT MEASURED, said plainly rather than implied away: the connector
329
- * needs a signed-in ChatGPT account and every probe ran against a scratch
330
- * CODEX_HOME that is signed out, so `codex_apps` never started in any of them.
331
- * What was watched is Codex's own registry reporting the flag off for the
332
- * thread, not the connector process failing to appear. Her real `~/.codex` was
333
- * not opened to close that gap. `createCodexWorker` therefore also WATCHES the
334
- * result -- see `sayUnasked` below -- so a flag that stops working is a line on
335
- * the run rather than a silence.
135
+ * `ephemeral: false` is set here on purpose: `thread/resume` on an ephemeral
136
+ * thread fails with "no rollout found", and every reply is a resume.
137
+ *
138
+ * The isolation has three parts. `mcp_servers` switches off the user's
139
+ * servers, `projects` blocks the folder's own config layer (see
140
+ * `projectIsolation`), and `features.apps: false` turns off Codex's connector
141
+ * to the apps linked to the user's ChatGPT account, which cannot be named in
142
+ * `mcp_servers`. (`config.apps` is not that switch; Codex rejects a boolean
143
+ * there.) The `apps` flag was only checked against Codex's own feature list
144
+ * while signed out, so `createCodexWorker` also reports any server that starts
145
+ * unasked (`sayUnasked`).
336
146
  */
337
- export function workerThreadParams({ cwd, model = null, instructions = null, mcpServers = [], storeServer = null, mode = null } = {}) {
338
- // THE MODE SHE PICKED, OR THE PAIR OF CONSTANTS. `mode` is one of the three in
339
- // shared/codex-modes.mjs, and it decides BOTH the sandbox and the approval
340
- // policy, because on this engine those two are one choice rather than two
341
- // (that file says why). Passing nothing means the default posture, which is
342
- // what every caller did before there was a picker, so a caller that has not
343
- // been updated keeps the behaviour it had.
147
+ export function workerThreadParams({ cwd, model = null, instructions = null, mcpServers = [], storeServer = null, mode = null, reviewer = null } = {}) {
148
+ // `mode` is one of shared/codex-modes.mjs and sets both the sandbox and the
149
+ // approval policy. No mode means the default posture.
344
150
  const posture = mode
345
- ? codexPosture(mode)
346
- : { approvalPolicy: WORKER_APPROVAL_POLICY, sandbox: WORKER_SANDBOX };
151
+ ? codexPosture(mode, { reviewer })
152
+ : { approvalPolicy: WORKER_APPROVAL_POLICY, sandbox: WORKER_SANDBOX, approvalsReviewer: codexReviewer(reviewer) };
347
153
  const params = {
348
154
  cwd,
349
155
  ephemeral: false,
350
156
  approvalPolicy: posture.approvalPolicy,
351
157
  sandbox: posture.sandbox,
352
- ...(posture.approvalsReviewer ? { approvalsReviewer: posture.approvalsReviewer } : {}),
158
+ // Always sent, never conditional. Omitted, it is not a neutral field:
159
+ // Codex reads `approvals_reviewer` from the top of whichever CODEX_HOME
160
+ // bills the run, and a home that says `auto_review` has its own subagent
161
+ // answer the escalation. Measured 2026-10-09 on codex-cli 0.160.0: a
162
+ // worker wrote into the home directory, reached three external sites, and
163
+ // raised zero cards. `scripts/qa-codex-live.mjs approvals default home`
164
+ // is that run; a third argument of `user` shows the card it suppressed.
165
+ approvalsReviewer: posture.approvalsReviewer,
353
166
  config: {
354
167
  mcp_servers: mcpIsolation(mcpServers, storeServer),
355
168
  projects: projectIsolation(cwd),
@@ -359,39 +172,17 @@ export function workerThreadParams({ cwd, model = null, instructions = null, mcp
359
172
  },
360
173
  };
361
174
  if (model) params.model = model;
362
- // Her standing instructions and the project's, in the slot that is this
363
- // engine's `--append-system-prompt`: a rule pasted above a 200-line brief is
364
- // a rule that gets buried, which is the reason the Claude path uses a system
365
- // prompt too.
175
+ // Standing instructions go in this engine's equivalent of
176
+ // `--append-system-prompt`, not in the prompt.
366
177
  if (instructions) params.developerInstructions = instructions;
367
178
  return params;
368
179
  }
369
180
 
370
181
  /**
371
- * THE SAME THREAD, CONTINUED, AND IT HAS TO CARRY THE WHOLE TABLE AGAIN.
372
- *
373
- * The harness resumed with `{threadId}` and nothing else, on the reasonable
374
- * assumption that a thread remembers what it was started with. IT DOES NOT.
375
- * Measured 2026-09-04 on codex-cli 0.148.0, one thread and three connections to
376
- * the same rollout:
377
- *
378
- * resume WITH this override -> the store server started and reported ready, and
379
- * `playwright` and `context7` stayed off.
380
- * resume with `{threadId}` -> `playwright` and `context7` BOTH started and
381
- * reported ready, and the thread had no store
382
- * server on it at all. Two npx processes of
383
- * hers were left running on the Mac by that
384
- * probe and had to be killed by hand.
385
- *
386
- * A reply she writes is a continuation, and continuations are most of what this
387
- * app does, so without this the isolation held for a row's first turn only and
388
- * every reply afterwards ran her servers and no store.
389
- *
390
- * `ephemeral` IS DROPPED RATHER THAN PASSED THROUGH. ThreadResumeParams takes
391
- * approvalPolicy, approvalsReviewer, baseInstructions, config, cwd,
392
- * developerInstructions, model, modelProvider, personality, sandbox,
393
- * serviceTier and threadId, and `ephemeral` is not among them: it is a fact
394
- * about creating a thread, and this one already exists.
182
+ * The `thread/resume` params. They carry the whole start table again, because
183
+ * a resumed thread does not remember its overrides: resumed with only
184
+ * `{threadId}`, the user's servers start and the store server is missing.
185
+ * `ephemeral` is dropped because ThreadResumeParams does not take it.
395
186
  */
396
187
  export function resumeThreadParams(threadId, params = {}) {
397
188
  const { ephemeral, ...rest } = params ?? {};
@@ -399,26 +190,13 @@ export function resumeThreadParams(threadId, params = {}) {
399
190
  }
400
191
 
401
192
  /**
402
- * WHERE CODEX PUT THE TRANSCRIPT OF ONE SESSION.
403
- *
404
- * Claude Code keeps `<home>/projects/<slug>/<id>.jsonl`, so the supervisor can
405
- * test one path and be done. Codex writes
406
- * `~/.codex/sessions/YYYY/MM/DD/rollout-<ISO>-<uuid>.jsonl` and the id is
407
- * INSIDE the filename, so finding one means looking. There were 1,700 of these
408
- * on the machine this was written on, counted with `find ~/.codex/sessions -name
409
- * 'rollout-*.jsonl' | wc -l`, so "look at all of them" is not an answer.
193
+ * Where Codex put one session's transcript, or null.
410
194
  *
411
- * NEWEST FIRST AND BOUNDED. The directory tree is already sorted by date, so
412
- * walking it year-desc, month-desc, day-desc finds a recent session in one
413
- * readdir and gives up after `days` day-folders rather than scanning the lot.
414
- * Sixty is the bound because the only caller is a "does this still exist"
415
- * check before a resume, and a session two months old is one nothing is going
416
- * back to.
417
- *
418
- * THE MATCH IS THE WHOLE FILENAME, NOT A SUBSTRING. `includes(id)` would match
419
- * a neighbouring rollout whose own uuid merely ends with the same characters,
420
- * and handing `thread/resume` the wrong thread is worse than handing it
421
- * nothing: it would resume a stranger's conversation onto her row.
195
+ * Codex writes `~/.codex/sessions/YYYY/MM/DD/rollout-<ISO>-<uuid>.jsonl`, with
196
+ * the id inside the filename, and there can be thousands. So the walk goes
197
+ * newest first and gives up after `days` day folders; the only caller checks
198
+ * that a session still exists before resuming it. The whole filename must
199
+ * match, because a substring match could resume the wrong conversation.
422
200
  */
423
201
  export function codexTranscriptFile(sessionId, { home = path.join(os.homedir(), '.codex'), days = 60 } = {}) {
424
202
  if (!sessionId || typeof sessionId !== 'string') return null;
@@ -445,36 +223,30 @@ function escapeForRegExp(text) {
445
223
  return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
446
224
  }
447
225
 
448
- // Newest first, and only the entries that are dated: a stray file or a folder
449
- // Codex renames under us must not stop the walk or reorder it.
226
+ // Newest first, numeric names only, so a stray entry cannot stop or reorder
227
+ // the walk.
450
228
  function numericDesc(dir) {
451
229
  let names = [];
452
230
  try { names = fs.readdirSync(dir); } catch { return []; }
453
231
  return names.filter((n) => /^\d+$/.test(n)).sort((a, b) => Number(b) - Number(a));
454
232
  }
455
233
 
456
- /* * * ONE WORK ITEM'S THREAD, WEARING A CHILD PROCESS'S FACE. * * `server` is a
457
- main/codex-app-server.mjs client, shared with every other * worker on this Mac. `ready` is
458
- the handshake: `initialize` must land before * `thread/start`, and awaiting it here rather
459
- than at every call site is what * lets the supervisor treat starting a Codex worker as one
460
- synchronous call * that returns a handle, exactly like `spawn`. * * WHAT IT EMITS, AND
461
- WHEN: * * 'spawn' the work is really up: the thread exists AND the * store server it needs
462
- has reported ready. Without it the transport refuses every * server request on this thread
463
- immediately -- which is still the correct * behaviour and is what a worker with nobody to
464
- ask should hear, rather than * parking for fifteen minutes. main/codex-approvals.mjs is the
465
- handler the * supervisor passes. * * `requireMcpServer` IS THE STORE, AND THE TURN WAITS
466
- FOR IT. Measured * 2026-09-04: a `mcp_servers` entry whose command does not exist leaves *
467
- `thread/start` resolving normally and the turn free to run, with the failure * arriving
468
- only as a notification. That is a worker doing a whole job it cannot * file anywhere, which
469
- CLAUDE.md prices at five sessions. So the turn does not * start until
470
- `mcpServer/startupStatus/updated` says `ready` for this server, * and if that never comes
471
- the run dies with the reason on stderr. Null when no * store server was configured, which
472
- is every downloaded copy of Agentbox: there * is nothing to wait for and a worker that
473
- waited would never run. * * ONE `failed` IS NOT THE VERDICT, THE DEADLINE IS. Codex retries
474
- a server that * failed to start -- measured, twice inside twenty-five seconds -- so a run *
475
- killed on the first failure would be a run killed on a hiccup. The last * failure's own
476
- words are kept, and they are what the run dies saying.
477
- */
234
+ /**
235
+ * One work item's thread, shaped like a child process.
236
+ *
237
+ * `server` is a main/codex-app-server.mjs client shared by every worker.
238
+ * `ready` is the handshake (`initialize` must land before `thread/start`),
239
+ * awaited here so the supervisor can start a worker with one synchronous call,
240
+ * like `spawn`.
241
+ *
242
+ * 'spawn' fires once the thread exists and the store server
243
+ * (`requireMcpServer`) has reported ready. A store server that fails to start
244
+ * does not stop `thread/start` or the turn; Codex only sends a notification.
245
+ * So the turn waits for `ready`, and if the deadline passes the run ends with
246
+ * the reason on stderr. Codex retries a failed server, so one `failed` is not
247
+ * the end, but its words are kept for that reason. With no store server
248
+ * configured, `requireMcpServer` is null and nothing waits.
249
+ */
478
250
  export function createCodexWorker({
479
251
  server,
480
252
  ready = Promise.resolve(),
@@ -494,10 +266,10 @@ export function createCodexWorker({
494
266
  worker.turnId = null;
495
267
 
496
268
  let ended = false;
497
- worker.steer = async text => {
269
+ worker.steer = async (text, pictures = []) => {
498
270
  if (ended || !worker.threadId || !worker.turnId) throw Error('The turn is not running. Send again when the conversation is ready.');
499
271
  const expected=worker.turnId;
500
- const result=await server.steerTurn(worker.threadId,expected,text);
272
+ const result=await server.steerTurn(worker.threadId,expected,text,pictures);
501
273
  if(result?.turnId!==expected)throw Error('The provider did not confirm the active turn.');
502
274
  return result;
503
275
  };
@@ -507,50 +279,25 @@ export function createCodexWorker({
507
279
  if (line) stderr.emit('data', line);
508
280
  };
509
281
 
510
- /* --------------------------- the store's gate --------------------------- */
511
-
512
- // `ready` more than once is ordinary: measured, the store server reported ready at
513
- // the thread's start and again while the turn ran. So the wait settles once
514
- // and every later notification is a no-op.
515
- //
516
- // THIS DEPENDS ON THE STARTUP NOTIFICATION ARRIVING AFTER THE RESPONSE THAT
517
- // NAMES THE THREAD, which is what was observed on every measured run: the
518
- // watch is installed inside `startThread`'s result hook, synchronously in the
519
- // read loop, so anything the server packs in BEHIND the response is caught,
520
- // and a `ready` that somehow came in AHEAD of it would belong to no watched
521
- // thread and be dropped -- costing a healthy worker its run at the deadline.
522
- // The resume road watches before sending, but an already-loaded thread does
523
- // not repeat startup notifications. On 2026-10-07, 11 resumes of one loaded
524
- // thread died at this deadline. Check that thread's current MCP connection
525
- // on resume as well as listening for startup changes.
282
+ // The store can report `ready` more than once, so the wait settles once.
283
+ // On a start, the watch goes on in `startThread`'s result hook, which catches
284
+ // a startup notification sent right behind the response; one sent before it
285
+ // would be dropped. A resumed thread that is already loaded does not repeat
286
+ // its startup notifications, so a resume also asks for the store's current
287
+ // status.
526
288
  let storeIsUp = !requireMcpServer;
527
289
  let storeTrouble = '';
528
290
  const waiting = [];
529
291
 
530
292
  /**
531
- * A SERVER CAME UP ON THIS THREAD THAT AGENTBOX DID NOT PUT THERE.
532
- *
533
- * The isolation is three tables now -- her servers off by name, the folder's
534
- * own config layer gated, and Codex's apps connector switched off by feature
535
- * -- and every one of them is a claim about a version of `codex app-server`.
536
- * A claim nobody checks is how the last two holes lasted: an empty
537
- * `mcp_servers` table "disabled" everything for a slice, and `codex_apps` was
538
- * left running for a slice after that, both without a symptom on any screen.
539
- *
540
- * So the result is watched rather than assumed. `starting` is the status that
541
- * means a process was really launched (`failed` launched nothing, and a
542
- * `ready` follows a `starting` we have already spoken about), and this is the
543
- * pipe `troubleCause` reads and the run trace records, so the line reaches her
544
- * row rather than a log nobody opens.
545
- *
546
- * ONCE PER SERVER. A line repeated on every beat is a line she stops reading,
547
- * which is the 2026-08-05 card flood in a different shape.
293
+ * Says on stderr, once per server, when a server starts on this thread that
294
+ * Agentbox did not put there. The isolation rests on Codex behaviour that can
295
+ * change between versions, so the result is checked. `failed` launched
296
+ * nothing, so only `starting` and `ready` count.
548
297
  *
549
- * AND THE WORDING IS LOAD-BEARING. `troubleCause`'s WORKSPACE list matches the
550
- * bare word "untrusted" and its INTERRUPTED list matches "connection", so a
551
- * sentence carrying either would relabel every Codex run that died of
552
- * something else. tests/a-codex-worker-does-not-reach-her-connected-apps
553
- * pins that against the real classifier rather than against this note.
298
+ * `troubleCause` matches "untrusted" and "connection", so the message must
299
+ * contain neither. Pinned by
300
+ * tests/a-codex-worker-does-not-reach-her-connected-apps.test.mjs.
554
301
  */
555
302
  const spoken = new Set();
556
303
  const sayUnasked = (params) => {
@@ -619,12 +366,8 @@ export function createCodexWorker({
619
366
  };
620
367
 
621
368
  /**
622
- * The one ending this worker will ever have.
623
- *
624
- * The thread is RELEASED here and not only on a kill: a watch left installed
625
- * on a finished thread hands every later line carrying that id to a listener
626
- * for a session that no longer exists, and the transport keeps the whole
627
- * client alive holding it.
369
+ * The only ending this worker has. It releases the thread's watch, so later
370
+ * lines for that id do not reach a dead session or keep the client alive.
628
371
  */
629
372
  const end = (code, signal = null, how = null) => {
630
373
  if (ended) return;
@@ -636,11 +379,8 @@ export function createCodexWorker({
636
379
 
637
380
  const handlers = {
638
381
  onNotification: (method, params) => {
639
- // The turn id arrives on the notification as well as on the `turn/start`
640
- // response, and this is the earlier of the two: `turn/start` resolves
641
- // when the turn is ACCEPTED, and the server can pack `turn/started` in
642
- // behind that response. `turn/interrupt` needs both ids, so the earliest
643
- // honest moment to know the second one is here.
382
+ // `turn/started` can arrive before the `turn/start` response resolves,
383
+ // and `turn/interrupt` needs the turn id, so take it from the earlier one.
644
384
  const id = params?.turn?.id ?? params?.turnId;
645
385
  if (typeof id === 'string' && id) worker.turnId = id;
646
386
 
@@ -649,10 +389,8 @@ export function createCodexWorker({
649
389
  worker.emit('event', method, params);
650
390
 
651
391
  if (method !== 'turn/completed' && method !== 'turn/failed') return;
652
- // `turn/failed` is handled even though the 0.148.0 schema does not list
653
- // it: failure arrives as `turn/completed` with `turn.status: "failed"`.
654
- // Dropping a notification means a run that reads as unfinished forever,
655
- // and being wrong in this direction is free.
392
+ // The 0.148.0 schema reports failure as `turn/completed` with status
393
+ // "failed" and has no `turn/failed`, but handling it costs nothing.
656
394
  const status = typeof params?.turn?.status === 'string'
657
395
  ? params.turn.status
658
396
  : (method === 'turn/failed' ? 'failed' : 'completed');
@@ -660,58 +398,33 @@ export function createCodexWorker({
660
398
  if (status !== 'completed') say(troubleText(params?.turn));
661
399
  end(status === 'completed' ? 0 : 1, null);
662
400
  },
663
- // THE CONNECTION DIED UNDER THIS THREAD, AND THAT IS NOT THIS ITEM'S DOING.
664
- //
665
- // The process crashed, the pipe broke, one line nobody could read took the
666
- // whole app-server down, or Agentbox closed it. `gone.stderr` is the
667
- // app-server's own tracing tail, which is where Codex's refusal wording
668
- // actually appears.
669
- //
670
- // ONE app-server carries the WHOLE fleet, so every one of these events
671
- // reaches every live thread at once. Ending them `end(1, null)` -- no
672
- // signal, nothing saying it was not the item -- had `settleDelivery` read
673
- // each of them as a run that tried and failed and spend one of that item's
674
- // three delivery attempts. Three such events and rows she had answered read
675
- // "stopped", which CLAUDE.md calls the loudest word this app has and says
676
- // must never mean "and nothing will retry". A whole-turn `turn/diff/updated`
677
- // is the biggest frame on this wire, so the fault arrives exactly when the
678
- // most work has been done.
679
- //
680
- // `transportFault` is the fact, told rather than faked. A signal would have
681
- // been the cheap way to reach the same branch and it would be a lie: nothing
682
- // signalled this worker, and `noteExitForBackoff` must still record the
683
- // failure against the Codex account, because a broken app-server IS evidence
684
- // about Codex on this Mac. Only the DELIVERY attempt is spared.
401
+ // The shared app-server died under this thread (a crash, a broken pipe, an
402
+ // unreadable frame, or Agentbox closed it). That is not this item's fault,
403
+ // and it reaches every live thread at once. `transportFault` lets
404
+ // `settleDelivery` spare the delivery attempt, while `noteExitForBackoff`
405
+ // still counts the failure against the Codex account. A fake signal would
406
+ // skip that count. `gone.stderr` is the app-server's tracing tail, where
407
+ // Codex's refusal wording appears; it goes last because the account books
408
+ // read the last line for a cause.
685
409
  onClosed: (gone) => {
410
+ say(gone?.why);
686
411
  say(gone?.stderr);
687
412
  end(1, null, { transportFault: true });
688
413
  },
689
414
  };
690
- // Only when there is somebody to ask. The transport tests for a FUNCTION and
691
- // refuses the request outright when it finds none, which is the right answer
692
- // for a worker whose card would reach nobody.
415
+ // Only when there is someone to ask. With no handler, the transport refuses
416
+ // every server request at once.
693
417
  if (typeof onApproval === 'function') handlers.onApproval = onApproval;
694
418
 
695
419
  ready
696
- // A FUNCTION, BECAUSE ONE OF THE PARAMS IS NOT KNOWN YET WHEN THE
697
- // SUPERVISOR ASKS FOR A WORKER. The MCP servers a thread must switch off
698
- // are read from the app-server itself, and her servers launch at
699
- // `thread/start` -- so the list has to be in hand before the start rather
700
- // than filled in on the second spawn. Whatever `ready` resolves to is
701
- // handed here, and a plain object is still accepted unchanged.
420
+ // `threadParams` may be a function of what `ready` resolves to, because
421
+ // the servers to switch off are read from the app-server before the start.
702
422
  .then((settled) => {
703
- // CANCELLATION IS CHECKED IN FRONT OF EVERY RPC, NOT ONLY AFTER THE FIRST
704
- // ONE. The first `ended` test used to sit AFTER the start had already been
705
- // sent, so a worker killed during the handshake still launched a thread,
706
- // still started whatever MCP processes that thread carries, and still
707
- // installed a watcher -- all for a run the supervisor had already given
708
- // up on. A quit kills the whole fleet at once, so this is the ordinary
709
- // case rather than a corner one.
423
+ // Check for a kill before every request, so a worker killed during the
424
+ // handshake starts no thread and no MCP servers.
710
425
  if (ended) return null;
711
426
  const params = typeof threadParams === 'function' ? threadParams(settled) : threadParams;
712
- // THE SAME PARAMS ON BOTH ROADS. A resume that carries only the thread id
713
- // gets her own MCP servers back and no store at all (measured; see
714
- // `resumeThreadParams`), and a reply is the commonest thing this app does.
427
+ // A resume needs the same params as a start. See `resumeThreadParams`.
715
428
  return resumeThreadId
716
429
  ? server.resumeThread(resumeThreadParams(resumeThreadId, params), handlers).then(() => ({ threadId: resumeThreadId }))
717
430
  : server.startThread(params, handlers);
@@ -719,12 +432,8 @@ export function createCodexWorker({
719
432
  .then((started) => {
720
433
  if (!started) return null;
721
434
  const threadId = started.threadId ?? resumeThreadId ?? null;
722
- // A THREAD THAT ARRIVES AFTER THE KILL IS STILL A THREAD, AND IT IS OURS
723
- // TO LET GO OF. `end` releases `worker.threadId`, and at a kill during
724
- // startup there was not one yet, so the watch this response just installed
725
- // would have outlived the worker: every later line carrying that id
726
- // handed to a listener for a session that no longer exists, with the
727
- // transport kept alive holding it.
435
+ // A thread that arrives after a kill still needs its watch released,
436
+ // because `end` ran before there was a thread id to release.
728
437
  if (ended) {
729
438
  if (threadId) { try { server.unwatch(threadId); } catch { /* already gone */ } }
730
439
  return null;
@@ -736,10 +445,8 @@ export function createCodexWorker({
736
445
  .then((up) => {
737
446
  if (ended) return null;
738
447
  if (!up) {
739
- // The loudest thing this file can do, on the pipe `troubleCause` reads.
740
- // A run that stops here has written nothing and spent nothing, which is
741
- // the whole point: the alternative is a worker doing the entire job and
742
- // filing it nowhere.
448
+ // Stop before the turn rather than let a worker do the job with no way
449
+ // to write it to the store.
743
450
  say(`the ${requireMcpServer} store server did not start (${storeTrouble || 'it never reported ready'}), so this run is stopping rather than working with no way to write to her store`);
744
451
  end(1, null);
745
452
  return null;
@@ -754,40 +461,29 @@ export function createCodexWorker({
754
461
  if (typeof id === 'string' && id) worker.turnId = id;
755
462
  })
756
463
  .catch((err) => {
757
- // A thread that never started, a resume the server refused, a turn it
758
- // would not accept. All three are a worker that did no work, and all
759
- // three carry the sentence that says why -- including the app-server's
760
- // stderr tail, which main/codex-app-server.mjs appends to every rejection.
761
- // No signal here: a kill has already ended this worker with one, and
762
- // `end` only ever fires once, so anything reaching this line is a real
763
- // failure rather than something we did.
464
+ // No thread, a refused resume, or a refused turn. The message includes the
465
+ // app-server's stderr tail. No signal: a kill has already ended this
466
+ // worker, and `end` fires only once.
764
467
  say(err?.message ?? String(err));
765
468
  end(1, null);
766
469
  });
767
470
 
768
471
  /**
769
- * OUR KILL, AND IT LOOKS LIKE ONE FROM THE OUTSIDE.
770
- *
771
- * `turn/interrupt` takes BOTH ids -- a turn is not identified by either one
772
- * alone -- so a kill before the turn id has arrived can only release the
773
- * thread, which it does. The exit is emitted immediately rather than waiting
774
- * for the server to confirm: the supervisor kills the whole fleet on quit and
775
- * on every restart, and an ending that waits on a process we are shutting
776
- * down is an ending that may never come.
472
+ * Our kill. `turn/interrupt` needs both the thread and turn ids, so before
473
+ * the turn id arrives this only releases the thread. The exit fires at once
474
+ * rather than waiting for the server, which may be shutting down too.
777
475
  *
778
- * `signal` is set here and nowhere else. That is the fact `settleDelivery`
779
- * reads to keep our own kill from spending one of an item's three delivery
780
- * attempts, and a later `turn/completed` saying `interrupted` cannot undo it
781
- * because `end` only fires once.
476
+ * `signal` is set only here. `settleDelivery` reads it so our kill does not
477
+ * spend a delivery attempt, and a later "interrupted" turn cannot undo that
478
+ * because `end` fires once.
782
479
  */
783
480
  worker.kill = () => {
784
481
  if (ended) return false;
785
482
  if (worker.threadId && worker.turnId) {
786
483
  try { server.interruptTurn(worker.threadId, worker.turnId)?.catch?.(() => {}); } catch { /* dying anyway */ }
787
484
  }
788
- // 143 is what the Claude CLI reports for the same event (it traps SIGTERM
789
- // and exits under its own power), so the two engines hand the exit handler
790
- // the same pair of numbers for the same thing.
485
+ // The Claude CLI traps SIGTERM and exits 143, so both engines report a kill
486
+ // the same way.
791
487
  end(143, 'SIGTERM');
792
488
  return true;
793
489
  };
@@ -796,30 +492,39 @@ export function createCodexWorker({
796
492
  }
797
493
 
798
494
  /**
799
- * Why a turn did not finish, in whatever words the server gave us -- AND IN THE
800
- * ONE WORD IT GIVES US THAT IS NOT PROSE.
801
- *
802
- * `TurnError` is `{ message, additionalDetails, codexErrorInfo }`, and
803
- * `CodexErrorInfo` is an enum: `usageLimitExceeded`, `unauthorized`,
804
- * `contextWindowExceeded`, `sessionBudgetExceeded`, `other`, and a handful of
805
- * nested variants carrying an HTTP status. That is off `codex app-server
806
- * generate-json-schema`, the binary's own generator, run on this Mac against
807
- * codex-cli 0.148.0 on 2026-09-05.
808
- *
809
- * THE CODE WAS BEING THROWN AWAY AND THE SENTENCE CLASSIFIED INSTEAD, which is
810
- * the wrong way round: shared/spawn-trouble.mjs has to decide whether a dead
811
- * run was a signed-out login or a limit, and a name published by the vendor
812
- * cannot drift the way a sentence can. Measured the same day: a signed-out run
813
- * does not even produce Codex's own words -- the app-server passes the raw HTTP
814
- * text through, so the prose is the transport's rather than the tool's.
815
- *
816
- * IN FRONT, NOT BEHIND. `noteExitForBackoff` classifies the LAST stderr line
817
- * cut to 300 characters, and the measured 401 message is already 200 of them
818
- * before any detail is appended; a code on the end would be the first thing
819
- * lost. Only a plain string code is carried -- a nested variant is an object and
820
- * would arrive as "[object Object]" -- and `other` is dropped, because it is the
821
- * schema's own way of saying nothing and would be noise in front of the sentence
822
- * that says something.
495
+ * A WORKER THAT WAS NEVER STARTED, AND SAYS WHY, in the shape `spawnWorker`
496
+ * holds: the sentence on stderr and an exit of 1, both on a later tick,
497
+ * because the supervisor attaches its listeners after this returns (the same
498
+ * reason `codexThreadParamsFor` throws inside the worker's chain rather than
499
+ * speaking early). No 'spawn' is emitted, so no delivery mark is written and
500
+ * no one-off permission mode is spent.
501
+ */
502
+ export function refusedCodexWorker(sentence) {
503
+ const worker = new EventEmitter();
504
+ worker.stderr = new EventEmitter();
505
+ worker.threadId = null;
506
+ worker.turnId = null;
507
+ worker.steer = async () => { throw Error('The turn is not running.'); };
508
+ let ended = false;
509
+ worker.kill = () => false;
510
+ setImmediate(() => {
511
+ if (ended) return;
512
+ ended = true;
513
+ worker.stderr.emit('data', sentence);
514
+ worker.emit('exit', 1, null);
515
+ });
516
+ return worker;
517
+ }
518
+
519
+ /**
520
+ * Why a turn did not finish: the server's message, with the `codexErrorInfo`
521
+ * code in front when it is a plain string.
522
+ *
523
+ * shared/spawn-trouble.mjs classifies on that code (for example
524
+ * `usageLimitExceeded` or `unauthorized`), which cannot drift the way prose
525
+ * can; a signed-out run carries raw HTTP text, not Codex's words. The code goes
526
+ * first because `noteExitForBackoff` cuts the last stderr line to 300
527
+ * characters. Object variants and `other` are left out.
823
528
  */
824
529
  function troubleText(turn) {
825
530
  const why = String(turn?.error?.message ?? '').trim();