@songsid/agend 2.3.4-beta.7 → 2.3.5-alpha.3

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 (457) hide show
  1. package/README.md +2 -0
  2. package/README.zh-TW.md +2 -0
  3. package/THIRD_PARTY_NOTICES.md +53 -0
  4. package/dist/agent-endpoint.d.ts +5 -1
  5. package/dist/agent-endpoint.js +12 -2
  6. package/dist/agent-endpoint.js.map +1 -1
  7. package/dist/auth-api.d.ts +21 -0
  8. package/dist/auth-api.js +89 -2
  9. package/dist/auth-api.js.map +1 -1
  10. package/dist/authority/web-authority.d.ts +12 -2
  11. package/dist/authority/web-authority.js +25 -4
  12. package/dist/authority/web-authority.js.map +1 -1
  13. package/dist/backend/claude-code.d.ts +15 -12
  14. package/dist/backend/claude-code.js +152 -133
  15. package/dist/backend/claude-code.js.map +1 -1
  16. package/dist/backend/codex-package.d.ts +5 -2
  17. package/dist/backend/codex-package.js +1 -5
  18. package/dist/backend/codex-package.js.map +1 -1
  19. package/dist/backend/codex.d.ts +136 -113
  20. package/dist/backend/codex.js +1281 -1219
  21. package/dist/backend/codex.js.map +1 -1
  22. package/dist/backend/grok-model-cache.d.ts +7 -0
  23. package/dist/backend/grok-model-cache.js +65 -0
  24. package/dist/backend/grok-model-cache.js.map +1 -0
  25. package/dist/backend/grok.d.ts +2 -0
  26. package/dist/backend/grok.js +22 -0
  27. package/dist/backend/grok.js.map +1 -1
  28. package/dist/backend/native-pane.d.ts +9 -0
  29. package/dist/backend/native-pane.js +59 -0
  30. package/dist/backend/native-pane.js.map +1 -0
  31. package/dist/backend/types.d.ts +7 -0
  32. package/dist/backend/types.js.map +1 -1
  33. package/dist/channel/ipc-bridge.d.ts +2 -0
  34. package/dist/channel/mcp-tools.js +1 -1
  35. package/dist/channel/mcp-tools.js.map +1 -1
  36. package/dist/cli.js +3 -0
  37. package/dist/cli.js.map +1 -1
  38. package/dist/config-validator.js +71 -1
  39. package/dist/config-validator.js.map +1 -1
  40. package/dist/config.js +16 -0
  41. package/dist/config.js.map +1 -1
  42. package/dist/daemon.d.ts +67 -5
  43. package/dist/daemon.js +823 -266
  44. package/dist/daemon.js.map +1 -1
  45. package/dist/delivery-consumed-watch.d.ts +15 -3
  46. package/dist/delivery-consumed-watch.js +23 -1
  47. package/dist/delivery-consumed-watch.js.map +1 -1
  48. package/dist/delivery-outbox.d.ts +5 -0
  49. package/dist/delivery-outbox.js +32 -4
  50. package/dist/delivery-outbox.js.map +1 -1
  51. package/dist/delivery-reconciliation.d.ts +2 -35
  52. package/dist/delivery-reconciliation.js +2 -156
  53. package/dist/delivery-reconciliation.js.map +1 -1
  54. package/dist/fleet-context.d.ts +2 -2
  55. package/dist/fleet-level-config.d.ts +1 -1
  56. package/dist/fleet-level-config.js +2 -0
  57. package/dist/fleet-level-config.js.map +1 -1
  58. package/dist/fleet-manager.d.ts +238 -3
  59. package/dist/fleet-manager.js +1826 -104
  60. package/dist/fleet-manager.js.map +1 -1
  61. package/dist/fleet-windows-runtime.d.ts +92 -0
  62. package/dist/fleet-windows-runtime.js +380 -0
  63. package/dist/fleet-windows-runtime.js.map +1 -0
  64. package/dist/general-binding.d.ts +85 -0
  65. package/dist/general-binding.js +298 -0
  66. package/dist/general-binding.js.map +1 -0
  67. package/dist/general-host-owner.d.ts +8 -0
  68. package/dist/general-host-owner.js +26 -0
  69. package/dist/general-host-owner.js.map +1 -0
  70. package/dist/general-instance.d.ts +7 -0
  71. package/dist/general-instance.js +20 -3
  72. package/dist/general-instance.js.map +1 -1
  73. package/dist/general-role.d.ts +5 -0
  74. package/dist/general-role.js +26 -0
  75. package/dist/general-role.js.map +1 -0
  76. package/dist/instance-config-impact.js +4 -0
  77. package/dist/instance-config-impact.js.map +1 -1
  78. package/dist/instance-lifecycle.d.ts +21 -3
  79. package/dist/instance-lifecycle.js +378 -162
  80. package/dist/instance-lifecycle.js.map +1 -1
  81. package/dist/instructions.d.ts +2 -0
  82. package/dist/instructions.js +6 -0
  83. package/dist/instructions.js.map +1 -1
  84. package/dist/local-pane-endpoint.d.ts +46 -0
  85. package/dist/local-pane-endpoint.js +189 -0
  86. package/dist/local-pane-endpoint.js.map +1 -0
  87. package/dist/local-pane-launcher.d.ts +2 -0
  88. package/dist/local-pane-launcher.js +38 -0
  89. package/dist/local-pane-launcher.js.map +1 -0
  90. package/dist/local-pane-stage.ps1 +101 -0
  91. package/dist/locale.js +85 -0
  92. package/dist/locale.js.map +1 -1
  93. package/dist/model-catalog-defaults.d.ts +14 -0
  94. package/dist/model-catalog-defaults.js +20 -0
  95. package/dist/model-catalog-defaults.js.map +1 -0
  96. package/dist/native-agent-admission.d.ts +7 -0
  97. package/dist/native-agent-admission.js +26 -0
  98. package/dist/native-agent-admission.js.map +1 -0
  99. package/dist/native-daemon-client.d.ts +13 -0
  100. package/dist/native-daemon-client.js +28 -0
  101. package/dist/native-daemon-client.js.map +1 -0
  102. package/dist/native-instance-api.d.ts +14 -0
  103. package/dist/native-instance-api.js +205 -0
  104. package/dist/native-instance-api.js.map +1 -0
  105. package/dist/native-instance-input.d.ts +84 -0
  106. package/dist/native-instance-input.js +54 -0
  107. package/dist/native-instance-input.js.map +1 -0
  108. package/dist/native-instance-registry.d.ts +32 -0
  109. package/dist/native-instance-registry.js +105 -0
  110. package/dist/native-instance-registry.js.map +1 -0
  111. package/dist/native-mcp.d.ts +40 -0
  112. package/dist/native-mcp.js +278 -0
  113. package/dist/native-mcp.js.map +1 -0
  114. package/dist/native-model-catalog.d.ts +5 -0
  115. package/dist/native-model-catalog.js +76 -0
  116. package/dist/native-model-catalog.js.map +1 -0
  117. package/dist/native-pane-crash-budget.d.ts +20 -0
  118. package/dist/native-pane-crash-budget.js +60 -0
  119. package/dist/native-pane-crash-budget.js.map +1 -0
  120. package/dist/native-pane-create.d.ts +26 -0
  121. package/dist/native-pane-create.js +53 -0
  122. package/dist/native-pane-create.js.map +1 -0
  123. package/dist/native-pane-lifecycle.d.ts +65 -0
  124. package/dist/native-pane-lifecycle.js +230 -0
  125. package/dist/native-pane-lifecycle.js.map +1 -0
  126. package/dist/native-pane-manager.d.ts +59 -0
  127. package/dist/native-pane-manager.js +584 -0
  128. package/dist/native-pane-manager.js.map +1 -0
  129. package/dist/native-pane-output-log.d.ts +15 -0
  130. package/dist/native-pane-output-log.js +108 -0
  131. package/dist/native-pane-output-log.js.map +1 -0
  132. package/dist/native-pane-reconciliation.d.ts +4 -0
  133. package/dist/native-pane-reconciliation.js +40 -0
  134. package/dist/native-pane-reconciliation.js.map +1 -0
  135. package/dist/native-pane-recovery.d.ts +12 -0
  136. package/dist/native-pane-recovery.js +10 -0
  137. package/dist/native-pane-recovery.js.map +1 -0
  138. package/dist/native-pane-tools.d.ts +2 -0
  139. package/dist/native-pane-tools.js +14 -0
  140. package/dist/native-pane-tools.js.map +1 -0
  141. package/dist/needs-you-hub.d.ts +2 -0
  142. package/dist/needs-you-hub.js +2 -1
  143. package/dist/needs-you-hub.js.map +1 -1
  144. package/dist/needs-you.d.ts +19 -1
  145. package/dist/needs-you.js +12 -2
  146. package/dist/needs-you.js.map +1 -1
  147. package/dist/outbound-handlers.d.ts +16 -2
  148. package/dist/outbound-handlers.js +67 -3
  149. package/dist/outbound-handlers.js.map +1 -1
  150. package/dist/outbound-schemas.d.ts +38 -0
  151. package/dist/outbound-schemas.js +12 -0
  152. package/dist/outbound-schemas.js.map +1 -1
  153. package/dist/pane-port.d.ts +118 -0
  154. package/dist/pane-port.js +2 -0
  155. package/dist/pane-port.js.map +1 -0
  156. package/dist/public-web-gateway.js +9 -0
  157. package/dist/public-web-gateway.js.map +1 -1
  158. package/dist/public-web-link.d.ts +5 -0
  159. package/dist/public-web-link.js +4 -0
  160. package/dist/public-web-link.js.map +1 -1
  161. package/dist/quickstart-api.js +9 -5
  162. package/dist/quickstart-api.js.map +1 -1
  163. package/dist/quickstart.d.ts +1 -0
  164. package/dist/quickstart.js +4 -1
  165. package/dist/quickstart.js.map +1 -1
  166. package/dist/runtime-api.d.ts +8 -0
  167. package/dist/runtime-api.js +96 -0
  168. package/dist/runtime-api.js.map +1 -0
  169. package/dist/runtime-assets/@xterm/addon-serialize/LICENSE +21 -0
  170. package/dist/runtime-assets/@xterm/addon-serialize/lib/addon-serialize.js +2 -0
  171. package/dist/runtime-assets/@xterm/addon-serialize/lib/addon-serialize.mjs +19 -0
  172. package/dist/runtime-assets/@xterm/addon-serialize/package.json +29 -0
  173. package/dist/runtime-assets/@xterm/headless/LICENSE +21 -0
  174. package/dist/runtime-assets/@xterm/headless/lib-headless/xterm-headless.js +2 -0
  175. package/dist/runtime-assets/@xterm/headless/lib-headless/xterm-headless.mjs +46 -0
  176. package/dist/runtime-assets/@xterm/headless/package.json +29 -0
  177. package/dist/runtime-assets/node-pty/CONPTY-LICENSE +21 -0
  178. package/dist/runtime-assets/node-pty/CONPTY-NOTICE.md +503 -0
  179. package/dist/runtime-assets/node-pty/LICENSE +69 -0
  180. package/dist/runtime-assets/node-pty/lib/conpty_console_list_agent.js +16 -0
  181. package/dist/runtime-assets/node-pty/lib/eventEmitter2.js +47 -0
  182. package/dist/runtime-assets/node-pty/lib/index.js +52 -0
  183. package/dist/runtime-assets/node-pty/lib/interfaces.js +7 -0
  184. package/dist/runtime-assets/node-pty/lib/shared/conout.js +11 -0
  185. package/dist/runtime-assets/node-pty/lib/terminal.js +190 -0
  186. package/dist/runtime-assets/node-pty/lib/types.js +7 -0
  187. package/dist/runtime-assets/node-pty/lib/unixTerminal.js +346 -0
  188. package/dist/runtime-assets/node-pty/lib/utils.js +39 -0
  189. package/dist/runtime-assets/node-pty/lib/windowsConoutConnection.js +125 -0
  190. package/dist/runtime-assets/node-pty/lib/windowsPtyAgent.js +320 -0
  191. package/dist/runtime-assets/node-pty/lib/windowsTerminal.js +199 -0
  192. package/dist/runtime-assets/node-pty/lib/worker/conoutSocketWorker.js +22 -0
  193. package/dist/runtime-assets/node-pty/package.json +64 -0
  194. package/dist/runtime-assets/node-pty/prebuilds/win32-arm64/conpty/OpenConsole.exe +0 -0
  195. package/dist/runtime-assets/node-pty/prebuilds/win32-arm64/conpty/conpty.dll +0 -0
  196. package/dist/runtime-assets/node-pty/prebuilds/win32-arm64/conpty.node +0 -0
  197. package/dist/runtime-assets/node-pty/prebuilds/win32-arm64/conpty_console_list.node +0 -0
  198. package/dist/runtime-assets/node-pty/prebuilds/win32-x64/conpty/OpenConsole.exe +0 -0
  199. package/dist/runtime-assets/node-pty/prebuilds/win32-x64/conpty/conpty.dll +0 -0
  200. package/dist/runtime-assets/node-pty/prebuilds/win32-x64/conpty.node +0 -0
  201. package/dist/runtime-assets/node-pty/prebuilds/win32-x64/conpty_console_list.node +0 -0
  202. package/dist/runtime-bundle.d.ts +17 -0
  203. package/dist/runtime-bundle.js +150 -0
  204. package/dist/runtime-bundle.js.map +1 -0
  205. package/dist/runtime-cli.d.ts +2 -0
  206. package/dist/runtime-cli.js +117 -0
  207. package/dist/runtime-cli.js.map +1 -0
  208. package/dist/runtime-client-store.d.ts +49 -0
  209. package/dist/runtime-client-store.js +292 -0
  210. package/dist/runtime-client-store.js.map +1 -0
  211. package/dist/runtime-client.d.ts +100 -0
  212. package/dist/runtime-client.js +700 -0
  213. package/dist/runtime-client.js.map +1 -0
  214. package/dist/runtime-coordinator.d.ts +82 -0
  215. package/dist/runtime-coordinator.js +864 -0
  216. package/dist/runtime-coordinator.js.map +1 -0
  217. package/dist/runtime-general-protocol.d.ts +22 -0
  218. package/dist/runtime-general-protocol.js +57 -0
  219. package/dist/runtime-general-protocol.js.map +1 -0
  220. package/dist/runtime-general-workspace.d.ts +17 -0
  221. package/dist/runtime-general-workspace.js +213 -0
  222. package/dist/runtime-general-workspace.js.map +1 -0
  223. package/dist/runtime-pane-assets.d.ts +17 -0
  224. package/dist/runtime-pane-assets.js +23 -0
  225. package/dist/runtime-pane-assets.js.map +1 -0
  226. package/dist/runtime-pane-boot.d.ts +2 -0
  227. package/dist/runtime-pane-boot.js +24 -0
  228. package/dist/runtime-pane-boot.js.map +1 -0
  229. package/dist/runtime-pane-codex-index.d.ts +1 -0
  230. package/dist/runtime-pane-codex-index.js +47 -0
  231. package/dist/runtime-pane-codex-index.js.map +1 -0
  232. package/dist/runtime-pane-config.d.ts +16 -0
  233. package/dist/runtime-pane-config.js +54 -0
  234. package/dist/runtime-pane-config.js.map +1 -0
  235. package/dist/runtime-pane-host.d.ts +44 -0
  236. package/dist/runtime-pane-host.js +424 -0
  237. package/dist/runtime-pane-host.js.map +1 -0
  238. package/dist/runtime-pane-job-owner.d.ts +1 -0
  239. package/dist/runtime-pane-job-owner.js +29 -0
  240. package/dist/runtime-pane-job-owner.js.map +1 -0
  241. package/dist/runtime-pane-job.ps1 +116 -0
  242. package/dist/runtime-pane-link.d.ts +40 -0
  243. package/dist/runtime-pane-link.js +246 -0
  244. package/dist/runtime-pane-link.js.map +1 -0
  245. package/dist/runtime-pane-mcp-protocol.d.ts +32 -0
  246. package/dist/runtime-pane-mcp-protocol.js +39 -0
  247. package/dist/runtime-pane-mcp-protocol.js.map +1 -0
  248. package/dist/runtime-pane-mcp-relay.d.ts +17 -0
  249. package/dist/runtime-pane-mcp-relay.js +70 -0
  250. package/dist/runtime-pane-mcp-relay.js.map +1 -0
  251. package/dist/runtime-pane-mcp.d.ts +51 -0
  252. package/dist/runtime-pane-mcp.js +150 -0
  253. package/dist/runtime-pane-mcp.js.map +1 -0
  254. package/dist/runtime-pane-native.d.ts +45 -0
  255. package/dist/runtime-pane-native.js +65 -0
  256. package/dist/runtime-pane-native.js.map +1 -0
  257. package/dist/runtime-pane-output.d.ts +19 -0
  258. package/dist/runtime-pane-output.js +53 -0
  259. package/dist/runtime-pane-output.js.map +1 -0
  260. package/dist/runtime-pane-port.d.ts +139 -0
  261. package/dist/runtime-pane-port.js +558 -0
  262. package/dist/runtime-pane-port.js.map +1 -0
  263. package/dist/runtime-pane-private.d.ts +14 -0
  264. package/dist/runtime-pane-private.js +51 -0
  265. package/dist/runtime-pane-private.js.map +1 -0
  266. package/dist/runtime-pane-private.ps1 +14 -0
  267. package/dist/runtime-pane-process.d.ts +4 -0
  268. package/dist/runtime-pane-process.js +108 -0
  269. package/dist/runtime-pane-process.js.map +1 -0
  270. package/dist/runtime-pane-protocol.d.ts +172 -0
  271. package/dist/runtime-pane-protocol.js +195 -0
  272. package/dist/runtime-pane-protocol.js.map +1 -0
  273. package/dist/runtime-pane-screen.d.ts +19 -0
  274. package/dist/runtime-pane-screen.js +76 -0
  275. package/dist/runtime-pane-screen.js.map +1 -0
  276. package/dist/runtime-pane-service.d.ts +11 -0
  277. package/dist/runtime-pane-service.js +221 -0
  278. package/dist/runtime-pane-service.js.map +1 -0
  279. package/dist/runtime-pane-stdio-protocol.d.ts +54 -0
  280. package/dist/runtime-pane-stdio-protocol.js +94 -0
  281. package/dist/runtime-pane-stdio-protocol.js.map +1 -0
  282. package/dist/runtime-pane-stdio.d.ts +3 -0
  283. package/dist/runtime-pane-stdio.js +123 -0
  284. package/dist/runtime-pane-stdio.js.map +1 -0
  285. package/dist/runtime-pane-store.d.ts +33 -0
  286. package/dist/runtime-pane-store.js +164 -0
  287. package/dist/runtime-pane-store.js.map +1 -0
  288. package/dist/runtime-pane-transcript-protocol.d.ts +43 -0
  289. package/dist/runtime-pane-transcript-protocol.js +58 -0
  290. package/dist/runtime-pane-transcript-protocol.js.map +1 -0
  291. package/dist/runtime-pane-transcript.d.ts +45 -0
  292. package/dist/runtime-pane-transcript.js +605 -0
  293. package/dist/runtime-pane-transcript.js.map +1 -0
  294. package/dist/runtime-pane-worker.d.ts +1 -0
  295. package/dist/runtime-pane-worker.js +154 -0
  296. package/dist/runtime-pane-worker.js.map +1 -0
  297. package/dist/runtime-protocol.d.ts +149 -0
  298. package/dist/runtime-protocol.js +19 -0
  299. package/dist/runtime-protocol.js.map +1 -0
  300. package/dist/runtime-switches.d.ts +79 -0
  301. package/dist/runtime-switches.js +90 -0
  302. package/dist/runtime-switches.js.map +1 -0
  303. package/dist/runtime-transcript-delta-reader.d.ts +23 -0
  304. package/dist/runtime-transcript-delta-reader.js +74 -0
  305. package/dist/runtime-transcript-delta-reader.js.map +1 -0
  306. package/dist/runtime-transcript-source.d.ts +36 -0
  307. package/dist/runtime-transcript-source.js +189 -0
  308. package/dist/runtime-transcript-source.js.map +1 -0
  309. package/dist/runtime-windows-bridge.d.ts +41 -0
  310. package/dist/runtime-windows-bridge.js +157 -0
  311. package/dist/runtime-windows-bridge.js.map +1 -0
  312. package/dist/settings-api.d.ts +5 -0
  313. package/dist/settings-api.js +95 -1
  314. package/dist/settings-api.js.map +1 -1
  315. package/dist/settings-baseline.d.ts +1 -0
  316. package/dist/settings-baseline.js +1 -1
  317. package/dist/settings-baseline.js.map +1 -1
  318. package/dist/settings-change.js +28 -0
  319. package/dist/settings-change.js.map +1 -1
  320. package/dist/settings-confirmation.d.ts +16 -1
  321. package/dist/settings-confirmation.js +47 -9
  322. package/dist/settings-confirmation.js.map +1 -1
  323. package/dist/settings-effect.d.ts +4 -0
  324. package/dist/settings-effect.js +58 -1
  325. package/dist/settings-effect.js.map +1 -1
  326. package/dist/settings-http-confirmation.d.ts +1 -0
  327. package/dist/settings-http-confirmation.js +15 -2
  328. package/dist/settings-http-confirmation.js.map +1 -1
  329. package/dist/settings-request-capability.d.ts +2 -0
  330. package/dist/settings-request-capability.js +4 -0
  331. package/dist/settings-request-capability.js.map +1 -1
  332. package/dist/settings-route-lifetime.d.ts +14 -0
  333. package/dist/settings-route-lifetime.js +48 -0
  334. package/dist/settings-route-lifetime.js.map +1 -0
  335. package/dist/settings-transaction.d.ts +4 -0
  336. package/dist/settings-transaction.js +9 -0
  337. package/dist/settings-transaction.js.map +1 -1
  338. package/dist/setup-form.js +6 -3
  339. package/dist/setup-form.js.map +1 -1
  340. package/dist/setup-i18n.js +3 -0
  341. package/dist/setup-i18n.js.map +1 -1
  342. package/dist/tmux-control.d.ts +2 -1
  343. package/dist/tmux-control.js.map +1 -1
  344. package/dist/tmux-manager.d.ts +2 -1
  345. package/dist/tmux-manager.js.map +1 -1
  346. package/dist/tmux-pane-host.d.ts +26 -0
  347. package/dist/tmux-pane-host.js +25 -0
  348. package/dist/tmux-pane-host.js.map +1 -0
  349. package/dist/tool-permissions.js +2 -1
  350. package/dist/tool-permissions.js.map +1 -1
  351. package/dist/topic-commands.js +3 -0
  352. package/dist/topic-commands.js.map +1 -1
  353. package/dist/transcript-delta-reader.d.ts +1 -1
  354. package/dist/transcript-delta-reader.js +1 -1
  355. package/dist/transcript-delta-reader.js.map +1 -1
  356. package/dist/transcript-monitor.d.ts +3 -0
  357. package/dist/transcript-monitor.js +22 -46
  358. package/dist/transcript-monitor.js.map +1 -1
  359. package/dist/transcript-needles.d.ts +35 -0
  360. package/dist/transcript-needles.js +158 -0
  361. package/dist/transcript-needles.js.map +1 -0
  362. package/dist/transcript-records.d.ts +21 -0
  363. package/dist/transcript-records.js +140 -0
  364. package/dist/transcript-records.js.map +1 -0
  365. package/dist/transcript-sources.d.ts +5 -0
  366. package/dist/transcript-sources.js +8 -79
  367. package/dist/transcript-sources.js.map +1 -1
  368. package/dist/types.d.ts +23 -0
  369. package/dist/ui/agent-proposals.js +90 -0
  370. package/dist/ui/agent-runtime.js +106 -0
  371. package/dist/ui/chat-store.js +13 -2
  372. package/dist/ui/chat-strings.js +48 -0
  373. package/dist/ui/cli-picker.js +42 -0
  374. package/dist/ui/general-location.js +71 -0
  375. package/dist/ui/native-instance-settings.js +47 -0
  376. package/dist/ui/native-pane-recovery.js +45 -0
  377. package/dist/ui/panel-chat.js +63 -32
  378. package/dist/ui/panel-details.js +2 -1
  379. package/dist/ui/panel-fleet.js +99 -18
  380. package/dist/ui/panel-needs.js +154 -3
  381. package/dist/ui/panel-runtimes.js +231 -0
  382. package/dist/ui/panel-settings.js +19 -9
  383. package/dist/ui/panel-windows-agent.js +86 -0
  384. package/dist/ui/panel-windows-cli.js +143 -0
  385. package/dist/ui/provider-marks.js +83 -0
  386. package/dist/ui/runtime-catalog.js +55 -0
  387. package/dist/ui/settings-catalog.js +37 -10
  388. package/dist/ui/settings-confirm.js +3 -1
  389. package/dist/ui/settings-dialogs.js +22 -7
  390. package/dist/ui/settings-model.js +2 -1
  391. package/dist/ui/settings-outside.js +176 -0
  392. package/dist/ui/settings-registry.js +6 -0
  393. package/dist/ui/settings-strings.js +16 -0
  394. package/dist/ui/shared/app-i18n.js +2 -2
  395. package/dist/ui/shared/app-route.js +6 -2
  396. package/dist/ui/shared/app-shell.js +3 -2
  397. package/dist/ui/shared/app-store.js +1 -0
  398. package/dist/ui/shared/app-stream.js +3 -1
  399. package/dist/ui/shared/app.css +301 -0
  400. package/dist/ui/shared/app.js +4 -0
  401. package/dist/ui/shared/header-tools.js +6 -4
  402. package/dist/ui/shared/instance-nav.js +33 -7
  403. package/dist/ui/shared/instance-switch.js +4 -3
  404. package/dist/ui/shared/qrcode.module.js +2237 -0
  405. package/dist/ui/shared/signin.js +39 -5
  406. package/dist/ui/shared/ui-menu.js +28 -8
  407. package/dist/ui/shared/vendor/qrcode-generator.LICENSE +27 -0
  408. package/dist/ui/shared/vendor/vendor.json +19 -2
  409. package/dist/ui/shared/view-strings.js +8 -0
  410. package/dist/ui/windows-agents-nav.js +2 -0
  411. package/dist/ui/windows-cli-catalog.js +79 -0
  412. package/dist/web-api.d.ts +37 -0
  413. package/dist/web-api.js +248 -6
  414. package/dist/web-api.js.map +1 -1
  415. package/dist/web-chat-history.d.ts +19 -2
  416. package/dist/web-chat-history.js +72 -3
  417. package/dist/web-chat-history.js.map +1 -1
  418. package/dist/web-only-fleet.d.ts +20 -0
  419. package/dist/web-only-fleet.js +21 -0
  420. package/dist/web-only-fleet.js.map +1 -1
  421. package/dist/web-remote-access.d.ts +152 -0
  422. package/dist/web-remote-access.js +368 -0
  423. package/dist/web-remote-access.js.map +1 -0
  424. package/dist/web-session.d.ts +7 -0
  425. package/dist/web-session.js +2 -0
  426. package/dist/web-session.js.map +1 -1
  427. package/dist/web-shell-routes.d.ts +4 -1
  428. package/dist/web-shell-routes.js +7 -2
  429. package/dist/web-shell-routes.js.map +1 -1
  430. package/dist/windows-agents.d.ts +124 -0
  431. package/dist/windows-agents.js +487 -0
  432. package/dist/windows-agents.js.map +1 -0
  433. package/dist/windows-cli-api.d.ts +23 -0
  434. package/dist/windows-cli-api.js +158 -0
  435. package/dist/windows-cli-api.js.map +1 -0
  436. package/dist/windows-cli-providers.d.ts +115 -0
  437. package/dist/windows-cli-providers.js +429 -0
  438. package/dist/windows-cli-providers.js.map +1 -0
  439. package/dist/windows-cli-worker.d.ts +58 -0
  440. package/dist/windows-cli-worker.js +479 -0
  441. package/dist/windows-cli-worker.js.map +1 -0
  442. package/dist/windows-cli.d.ts +115 -0
  443. package/dist/windows-cli.js +402 -0
  444. package/dist/windows-cli.js.map +1 -0
  445. package/dist/windows-native-mcp.d.ts +19 -0
  446. package/dist/windows-native-mcp.js +143 -0
  447. package/dist/windows-native-mcp.js.map +1 -0
  448. package/dist/windows-pane-mcp.d.ts +26 -0
  449. package/dist/windows-pane-mcp.js +187 -0
  450. package/dist/windows-pane-mcp.js.map +1 -0
  451. package/package.json +9 -3
  452. package/templates/runtime-assets/conpty-LICENSE +21 -0
  453. package/templates/runtime-assets/conpty-NOTICE.md +503 -0
  454. package/templates/runtime-assets/local-pane-stage.ps1 +101 -0
  455. package/templates/runtime-assets/runtime-pane-job.ps1 +116 -0
  456. package/templates/runtime-assets/runtime-pane-private.ps1 +14 -0
  457. package/templates/runtime-assets/xterm-LICENSE +21 -0
@@ -8,6 +8,8 @@ import { codexPackageWarning } from "./codex-package.js";
8
8
  import { probeCliVersion, resolveBinary, shellQuote, validateModel, validateProvider, warnIfModelMismatch } from "./types.js";
9
9
  import { credentialHomeSpec, credentialProfileHome, prepareCredentialProfileHome, resolveCredentialProfile, } from "./credential-profile.js";
10
10
  import { getAgendHome } from "../paths.js";
11
+ import { selectedGeneralHostSession } from "../general-binding.js";
12
+ import { ResumeBlockedError } from "./types.js";
11
13
  import { CODEX_MODELS_CACHE_MAX_BYTES, codexShortHomeFor, readCodexEffortLevels, readCodexModelEffortLevels, resolveCodexLaunchModel } from "./codex-metadata.js";
12
14
  import { EFFORT_CAPABILITIES } from "./effort-metadata.js";
13
15
  import { codexEffort } from "./launch-effort.js";
@@ -604,6 +606,20 @@ function codexWorkspaceLimitPromptVisible(pane) {
604
606
  }
605
607
  return false;
606
608
  }
609
+ /**
610
+ * The composer's own `›` row above the footer, or -1. Walking up from the footer, only blank rows and indented rows (the
611
+ * composer's wrapped text, however many lines a long paste takes) may come before it. The first column-zero row decides:
612
+ * a `›` there is the composer; a numbered `›` row is a selection card; any other column-zero row (`•`/`■`/`⚠` output, a
613
+ * status line) means a `›` above it is transcript history, never the composer.
614
+ */
615
+ function ownedComposerRow(rows) {
616
+ for (let i = rows.length - 1; i >= 0; i--) {
617
+ if (!rows[i].trim() || /^[ \t]/.test(rows[i]))
618
+ continue;
619
+ return /^[>›][ \t]+\S/.test(rows[i]) && !/^[>›][ \t]+\d+\./.test(rows[i]) ? i : -1;
620
+ }
621
+ return -1;
622
+ }
607
623
  /** Unknown pickers own stdin too; never type or press Enter into one. */
608
624
  function codexUnknownSelectionVisible(pane) {
609
625
  const rows = pane.replace(/\r/g, "").split("\n");
@@ -983,8 +999,15 @@ export const CODEX_RESUME_ATTEMPT_FILE = "codex-resume-attempt.json";
983
999
  export const CODEX_RESUME_FAILED_FILE = "codex-resume-failed.json";
984
1000
  /** Above this, a resumed rollout carries a note that codex may compact it first. */
985
1001
  export const CODEX_RESUME_LARGE_BYTES = 20 * 1024 * 1024;
986
- export class CodexBackend {
987
- instanceDir;
1002
+ /** Pane-only detectors: construction performs no provider, config or session discovery. */
1003
+ export class CodexPaneDetectors {
1004
+ paneDetectorContext;
1005
+ constructor(paneDetectorContext) {
1006
+ this.paneDetectorContext = paneDetectorContext;
1007
+ }
1008
+ readConfiguredStatusLineItems() { return this.paneDetectorContext?.statusLineItems()?.slice() ?? null; }
1009
+ trustPaneFields(pane) { const state = codexTrustPromptState(pane); return { promptFolder: state.folder, promptRoot: state.root, rootNote: state.rootNote }; }
1010
+ trustHoldReason(pane) { return { ...this.trustPaneFields(pane), reason: "Folder permission needs confirmation on this execution machine" }; }
988
1011
  /**
989
1012
  * #1510: the reply-completion guard covers codex, its turn end read from the rollout (task_complete /
990
1013
  * turn_aborted, recorded on 0.162.0 in tests/fixtures/reply-guard-1510): the pane alone stays busy through a tool
@@ -993,121 +1016,10 @@ export class CodexBackend {
993
1016
  replyCompletionGuard = true;
994
1017
  turnEndFromTranscript = true;
995
1018
  binaryName = "codex";
996
- binaryPath;
997
- sharedCodexHome;
998
- isolatedCodexHome;
999
- /** Which subscription this instance runs on, or null for the shared login. */
1000
- credentialProfile = null;
1001
- /** Set only after preTrust wrote and read back this instance's private config. */
1002
- authorizedTrust = null;
1003
1019
  configuredStatusLineItems;
1004
1020
  configuredStatusLinePattern;
1005
- /** Set by buildCommand when the resume plan fell back; read once by the daemon. */
1006
- launchWarning = null;
1007
1021
  /** Whether the last buildCommand launched a resume (`resume <id>` or `--last`). */
1008
1022
  lastLaunchResumes = true;
1009
- /** Set by writeConfig when the status_line could not be made verifiable (#931). */
1010
- statusLineWarning = null;
1011
- /** #2357: set by buildCommand when the codex being launched lacks a package companion (codex-code-mode-host). */
1012
- packageWarning = null;
1013
- constructor(instanceDir) {
1014
- this.instanceDir = instanceDir;
1015
- this.binaryPath = resolveBinary("codex");
1016
- this.sharedCodexHome = resolve(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex"));
1017
- this.isolatedCodexHome = CodexBackend.resolveShortHome(instanceDir);
1018
- }
1019
- /**
1020
- * Compute and (on first call for an existing instance) migrate to a SHORT
1021
- * persistent CODEX_HOME under `~/.agend/cx/<8-char-hash>/`.
1022
- *
1023
- * Background (#953): codex 0.157.0 added `app-server-control.sock` under
1024
- * CODEX_HOME. For instances with long `-t<topic_id>` suffixes the full path
1025
- * exceeds the Unix socket SUN_LEN (~107 chars). codex canonicalises the path
1026
- * before binding the socket, so a symlink workaround does not help — the real
1027
- * path must be short.
1028
- *
1029
- * Migration steps (each is idempotent; half-completed states self-heal):
1030
- * 1. If shortHome does not exist AND legacyHome is a real directory (not a
1031
- * symlink) → atomic renameSync. On EXDEV or any other error, fall back
1032
- * safely: keep using the legacy long path (instance can still start, just
1033
- * without the short-path fix).
1034
- * 2. If shortHome exists but legacyHome is missing → recreate the backward-
1035
- * compat symlink (self-heal for rename-succeeded-but-symlink-failed crash).
1036
- * 3. If both shortHome and legacyHome (as symlink) exist → nothing to do.
1037
- * 4. New instance (no legacyHome) → create shortHome directly.
1038
- *
1039
- * Fail-safe guarantee: migration failure must never make the instance worse.
1040
- * The caller always receives a valid path it can use as CODEX_HOME.
1041
- */
1042
- /** Exposed for fleet-manager to delete the short home on instance removal. */
1043
- static shortHomeFor(instanceDir) {
1044
- return codexShortHomeFor(instanceDir);
1045
- }
1046
- static resolveShortHome(instanceDir) {
1047
- const shortHome = CodexBackend.shortHomeFor(instanceDir);
1048
- const shortBase = join(getAgendHome(), "cx");
1049
- const canonical = resolve(instanceDir);
1050
- const legacyHome = resolve(instanceDir, "codex-home");
1051
- // Only migrate/create when the instance directory itself exists.
1052
- // Constructing CodexBackend for a non-existent dir (cli-env probes, test
1053
- // backends that never ran) must not litter ~/.agend/cx/ with orphan dirs.
1054
- if (!existsSync(canonical))
1055
- return shortHome;
1056
- const legacyExists = existsSync(legacyHome);
1057
- const legacyIsRealDir = legacyExists && !lstatSync(legacyHome).isSymbolicLink();
1058
- const shortExists = existsSync(shortHome);
1059
- if (!shortExists) {
1060
- mkdirSync(shortBase, { recursive: true, mode: 0o700 });
1061
- if (legacyIsRealDir) {
1062
- // Attempt atomic rename. Requires same device; fails with EXDEV otherwise.
1063
- try {
1064
- renameSync(legacyHome, shortHome);
1065
- // Log the one-time migration for observability.
1066
- try {
1067
- const logPath = join(shortBase, `${shortHome.split("/").at(-1)}.migrated`);
1068
- writeFileSync(logPath, `${new Date().toISOString()} migrated from ${legacyHome}\n`);
1069
- }
1070
- catch { /* log failure is non-fatal */ }
1071
- }
1072
- catch (err) {
1073
- // EXDEV (cross-device) or any other rename failure → fail-safe:
1074
- // fall back to the legacy long path so the instance still works.
1075
- // The socket-length bug persists for this instance but no data is lost.
1076
- const code = err.code ?? "unknown";
1077
- try {
1078
- const warnPath = join(shortBase, `${shortHome.split("/").at(-1)}.migration-failed`);
1079
- writeFileSync(warnPath, `${new Date().toISOString()} rename failed (${code}): ${err.message}\n`
1080
- + `legacy path remains in use: ${legacyHome}\n`);
1081
- }
1082
- catch { /* warn log failure is also non-fatal */ }
1083
- // Return legacy long path so the instance starts (socket may still fail
1084
- // on 0.157.0 for long names, but no data is lost or corrupted).
1085
- return legacyHome;
1086
- }
1087
- }
1088
- else if (!legacyExists) {
1089
- // New instance — no data to migrate.
1090
- mkdirSync(shortHome, { recursive: true, mode: 0o700 });
1091
- }
1092
- // If legacyExists but is already a symlink: shortHome was deleted externally
1093
- // while the symlink still points to it. Re-create shortHome as a fresh dir.
1094
- if (!existsSync(shortHome)) {
1095
- mkdirSync(shortHome, { recursive: true, mode: 0o700 });
1096
- }
1097
- }
1098
- // Backward-compat symlink: instanceDir/codex-home → shortHome.
1099
- // Also serves as self-heal: if a previous run renamed successfully but
1100
- // crashed before creating the symlink, we recreate it here.
1101
- if (!legacyExists || legacyIsRealDir) {
1102
- // legacyIsRealDir means rename just happened (now shortHome exists, legacyHome gone).
1103
- // !legacyExists means new instance or symlink was deleted — create it.
1104
- try {
1105
- symlinkSync(shortHome, legacyHome);
1106
- }
1107
- catch { /* race or already exists */ }
1108
- }
1109
- return shortHome;
1110
- }
1111
1023
  supportsQueuedInput() {
1112
1024
  return true;
1113
1025
  }
@@ -1132,24 +1044,10 @@ export class CodexBackend {
1132
1044
  }
1133
1045
  /** Observed on real Codex 0.156.0: the live input row precedes its footer. */
1134
1046
  isDeliveryInputReadyPane(pane) {
1135
- // A composer with no app-server behind it accepts text and never submits it (#1099).
1136
- if (codexAppServerDisconnected(pane))
1137
- return false;
1138
- const rows = pane.replace(/\r/g, "").split("\n");
1139
- while (rows.length && !rows[rows.length - 1].trim())
1140
- rows.pop();
1141
- const footer = rows.pop() ?? "";
1142
- // Codex preserves other configured status-line items after the context
1143
- // meter (observed on 0.156.0: "Context 100% left · GPT-6-Astra"). They
1144
- // are footer chrome, not evidence that the input row is unavailable.
1145
- const contextFooter = this.isAnyContextFooter(footer);
1146
- // A deliberately configured status_line may omit Context entirely. In
1147
- // that case require a recognized value from the actual configured item
1148
- // list after Codex's exact empty live-composer row. An arbitrary non-empty
1149
- // footer or a recent transcript echo is not a readiness signal; that was
1150
- // the false-ready hole in the old Layer 2 deny-list attempt (#931/#947).
1151
- if (!contextFooter && !this.isConfiguredStatusLineFooter(footer))
1047
+ const frame = this.composerFrame(pane);
1048
+ if (!frame)
1152
1049
  return false;
1050
+ const { rows, contextFooter } = frame;
1153
1051
  if (!contextFooter) {
1154
1052
  if (this.getBusyPattern().test(pane))
1155
1053
  return false;
@@ -1170,6 +1068,45 @@ export class CodexBackend {
1170
1068
  }
1171
1069
  return false;
1172
1070
  }
1071
+ /**
1072
+ * Complete ownership of a long pasted envelope: the `›` composer row and every wrapped row down to a context footer are
1073
+ * proven by ownedComposerRow, however many rows they take. The Enter gate accepts this as well as isDeliveryInputReadyPane,
1074
+ * never instead of the dialog, busy and expected-seq checks.
1075
+ */
1076
+ ownsComposerRows(pane) {
1077
+ const frame = this.composerFrame(pane);
1078
+ return !!frame?.contextFooter && ownedComposerRow(frame.rows) >= 0;
1079
+ }
1080
+ /**
1081
+ * The screen above Codex's footer, shared by both readers: null when the footer is not a Codex footer at all or the
1082
+ * app server behind the composer is gone (#1099). `rows` excludes the footer.
1083
+ */
1084
+ composerFrame(pane) {
1085
+ // A composer with no app-server behind it accepts text and never submits it (#1099).
1086
+ if (codexAppServerDisconnected(pane))
1087
+ return null;
1088
+ const rows = pane.replace(/\r/g, "").split("\n");
1089
+ while (rows.length && !rows[rows.length - 1].trim())
1090
+ rows.pop();
1091
+ if (this.paneDetectorContext?.windowsConpty && /^[ \t]*\? for shortcuts[ \t]*$/.test(rows.at(-1) ?? "")) {
1092
+ rows.pop();
1093
+ while (rows.length && !rows[rows.length - 1].trim())
1094
+ rows.pop();
1095
+ }
1096
+ const footer = rows.pop() ?? "";
1097
+ // Codex preserves other configured status-line items after the context
1098
+ // meter (observed on 0.156.0: "Context 100% left · GPT-6-Astra"). They
1099
+ // are footer chrome, not evidence that the input row is unavailable.
1100
+ const contextFooter = this.isAnyContextFooter(footer);
1101
+ // A deliberately configured status_line may omit Context entirely. In
1102
+ // that case require a recognized value from the actual configured item
1103
+ // list after Codex's exact empty live-composer row. An arbitrary non-empty
1104
+ // footer or a recent transcript echo is not a readiness signal; that was
1105
+ // the false-ready hole in the old Layer 2 deny-list attempt (#931/#947).
1106
+ if (!contextFooter && !this.isConfiguredStatusLineFooter(footer))
1107
+ return null;
1108
+ return { rows, contextFooter };
1109
+ }
1173
1110
  /**
1174
1111
  * Live status chrome that must veto the broad prompt/context ready match.
1175
1112
  *
@@ -1236,990 +1173,1264 @@ export class CodexBackend {
1236
1173
  return false;
1237
1174
  return true;
1238
1175
  }
1176
+ getReadyPattern() {
1177
+ // Header and context text persist in inline scrollback and even while the
1178
+ // CLI is loading or a modal owns stdin. Require the live prompt followed
1179
+ // by the Context footer at the *end* of the capture. A valid custom
1180
+ // status_line without Context gets a separate configured-item pattern;
1181
+ // unknown footer values still fail closed. getBusyPattern vetoes a working
1182
+ // turn whose empty composer remains visible.
1183
+ // U+22C6 is Codex's observed cosmetic starfield; it can be drawn in the
1184
+ // prompt, between prompt/footer, and below the footer. A drafted composer
1185
+ // is also idle once this same bottom footer proves it owns the screen.
1186
+ const nativeTail = (pattern) => this.paneDetectorContext?.windowsConpty
1187
+ ? new RegExp(pattern.source.slice(0, -1) + String.raw `(?:\r?\n[ \t]*\? for shortcuts[ \t]*)?(?:\r?\n[ \t⋆]*)*$`, pattern.flags) : pattern;
1188
+ const contextReady = nativeTail(/(?:^|\n)[>›][ \t⋆]+(?!\d+\.)\S[^\r\n]*\r?\n(?:[ \t⋆]*\r?\n){0,3}[ \t⋆]+(?:[0-9a-f-]{36}[ \t]+·[ \t]+)?Context[ \t]+(?:\d+%[ \t]+(?:left|used)|\d+…|…)[^\r\n]*(?:\r?\n[ \t⋆]*)*$/i);
1189
+ const footerPattern = this.configuredStatusLineFooterPattern();
1190
+ if (!footerPattern)
1191
+ return contextReady;
1192
+ // The no-Context alternative requires the exact empty composer followed by
1193
+ // a recognized configured Codex status_line value. If run-state is
1194
+ // configured, that value must specifically be Ready. This is positive
1195
+ // structural proof, never a deny-list of known dialogs.
1196
+ const footerSource = footerPattern.source.slice(1, -1);
1197
+ const noContextReady = nativeTail(new RegExp(String.raw `(?:^|\n)[>›][ \t⋆]*Ask Codex to do anything[ \t⋆]*\r?\n(?:[ \t⋆]*\r?\n){0,3}${footerSource}(?:\r?\n[ \t⋆]*)*$`, "iu"));
1198
+ // #978: a configured footer that carries a Context item in any position
1199
+ // is the Context footer: same drafted-composer rule as Context-first. The
1200
+ // whole row must match the configured item grammar, so an arbitrary
1201
+ // indented line that merely contains `Context N% left` cannot pass.
1202
+ const configuredContextReady = this.configuredHasContextItem()
1203
+ ? nativeTail(new RegExp(String.raw `(?:^|\n)[>›][ \t⋆]+(?!\d+\.)\S[^\r\n]*\r?\n(?:[ \t⋆]*\r?\n){0,3}(?=[^\r\n]*${CODEX_CONTEXT_ITEM})${footerSource}(?:\r?\n[ \t⋆]*)*$`, "iu"))
1204
+ : null;
1205
+ return new RegExp([contextReady.source, noContextReady.source, configuredContextReady?.source]
1206
+ .filter((source) => !!source)
1207
+ .map(source => `(?:${source})`)
1208
+ .join("|"), "iu");
1209
+ }
1210
+ configuredStatusLineFooterPattern() {
1211
+ if (this.configuredStatusLinePattern !== undefined)
1212
+ return this.configuredStatusLinePattern;
1213
+ const items = this.readConfiguredStatusLineItems();
1214
+ if (!items?.length)
1215
+ return this.configuredStatusLinePattern = null;
1216
+ const configuredPatterns = items
1217
+ .map(item => CODEX_STATUS_LINE_VALUE_PATTERNS[item])
1218
+ .filter((pattern) => pattern !== undefined);
1219
+ if (!configuredPatterns.length)
1220
+ return this.configuredStatusLinePattern = null;
1221
+ const segment = `(?:${configuredPatterns.join("|")})`;
1222
+ const separator = String.raw `[ \t]+·[ \t]+`;
1223
+ const hasRunState = items.some(item => CODEX_RUN_STATE_STATUS_ITEMS.has(item));
1224
+ // Ready must occupy its own status-line segment. Other configured items
1225
+ // may precede/follow it, as Codex joins visible values with ` · `.
1226
+ const line = hasRunState
1227
+ ? `(?:${segment}${separator})*Ready(?:${separator}${segment})*`
1228
+ : `${segment}(?:${separator}${segment})*`;
1229
+ // The configured items, then (optionally) the CLI's own native fields — the same rule as the Context-first footer, so a
1230
+ // `model · Context 46% left Goal achieved (1h 6m)` row is the footer too, and the readiness regex built from this
1231
+ // source (footerSource) says the same.
1232
+ this.configuredStatusLinePattern = new RegExp(`^[ \t⋆]*${line}(?:${CODEX_NATIVE_FOOTER_FIELDS})?[ \t⋆]*$`, "iu");
1233
+ return this.configuredStatusLinePattern;
1234
+ }
1235
+ isConfiguredStatusLineFooter(row) {
1236
+ const pattern = this.configuredStatusLineFooterPattern();
1237
+ if (!pattern)
1238
+ return false;
1239
+ pattern.lastIndex = 0;
1240
+ return pattern.test(row);
1241
+ }
1242
+ configuredHasContextItem() {
1243
+ return (this.readConfiguredStatusLineItems() ?? []).some(item => CODEX_CONTEXT_STATUS_ITEMS.has(item));
1244
+ }
1239
1245
  /**
1240
- * The row codex paints for input it has taken into its own queue instead of
1241
- * submitting: `↳ <message>` under "Messages to be submitted after next tool
1242
- * call (press esc to interrupt and send immediately)".
1246
+ * #978: a Context item in ANY position, accepted only as part of a footer
1247
+ * whose every segment matches the configured status_line items. Codex keeps
1248
+ * the user's order (`gpt-5.6-sol medium · Context 46% left · ~/x`); an
1249
+ * arbitrary indented transcript line that happens to contain the item does
1250
+ * not match that grammar. A stale config fails closed, and the daemon's
1251
+ * stable-composer escape hatch then keeps the instance from latching.
1243
1252
  */
1244
- getQueuedInputMarker() {
1245
- return /↳/;
1253
+ // The configured grammar already makes Context a whole segment, so the item
1254
+ // test only has to establish that one is present.
1255
+ isConfiguredContextFooter(row) {
1256
+ return this.configuredHasContextItem()
1257
+ && new RegExp(CODEX_CONTEXT_ITEM, "iu").test(row)
1258
+ && this.isConfiguredStatusLineFooter(row);
1246
1259
  }
1247
- /** #1811: what the last launch left out and why — the Log tab and daemon.log, never a channel. */
1248
- quietLaunchNotes = [];
1249
- consumeQuietLaunchNotes() { const notes = this.quietLaunchNotes; this.quietLaunchNotes = []; return notes; }
1250
- buildCommand(config) {
1251
- this.lastKnownModel = config.model?.trim() || null;
1252
- this.credentialProfile = this.readProfile(config);
1253
- // #2886: `--full-auto` is gone from codex (no supported version, 0.155 on, accepts it: the instance never started);
1254
- // spelled out, it was a workspace-write sandbox that asks for approval on request.
1255
- const approvalFlag = config.skipPermissions !== false
1256
- ? "--dangerously-bypass-approvals-and-sandbox"
1257
- : "--sandbox workspace-write --ask-for-approval on-request";
1258
- // #984: never trust `codex resume --last` to pick this instance's session.
1259
- // Codex 0.157 scopes it to the git REPOSITORY, so in a worktree it resumes
1260
- // the newest session of any sibling worktree (a lock screen while that
1261
- // instance lives, a silent hijack otherwise), and a new instance in a
1262
- // shared repo does not start fresh. Resume the exact-cwd thread from the
1263
- // shared state DB instead; `--last` is only the fallback for an unreadable
1264
- // DB when no other Codex instance shares the repository.
1265
- let cmd;
1266
- this.launchWarning = null;
1267
- this.lastLaunchResumes = false;
1268
- // #2357: read-only (codex is never run here); a launch is not held for it — the operator is told once per launch.
1269
- this.packageWarning = codexPackageWarning(this.binaryPath);
1270
- if (config.skipResume) {
1271
- cmd = `${this.binaryPath} ${approvalFlag}`;
1272
- }
1273
- else {
1274
- const resumeBy = (id, why, bytes) => {
1275
- this.lastLaunchResumes = true;
1276
- this.writeResumeAttempt(id);
1277
- const large = bytes !== null && bytes > CODEX_RESUME_LARGE_BYTES ? ` ${t("codex.resume_large", String(Math.round(bytes / (1024 * 1024))))}` : "";
1278
- this.launchWarning = why + large;
1279
- return `${this.binaryPath} resume ${shellQuote(id)} ${approvalFlag}`;
1280
- };
1281
- const sessionsDir = join(this.isolatedCodexHome, "sessions");
1282
- // M-2266: the previous launch was told to resume a conversation by id. Codex records a thread as it opens it,
1283
- // so a resume that did not take (codex exited with "No saved session found…") left no row: this launch starts
1284
- // fresh, once, and says so. A database that cannot be read proves nothing, and planning goes on as usual.
1285
- // A new one-shot ask comes first: it is the person's explicit choice for this launch.
1286
- const oneShot = this.takeResumeNext();
1287
- const attempt = this.takeResumeAttempt();
1288
- const failed = !oneShot && attempt !== null && codexThreadRowExists(this.stateDbPath(), attempt) === false;
1289
- if (failed) {
1290
- // Remembered, so the auto-resume never picks this conversation again on a later launch.
1291
- this.rememberFailedResume(attempt);
1292
- cmd = `${this.binaryPath} ${approvalFlag}`;
1293
- this.launchWarning = t("codex.resume_failed", config.workingDirectory, attempt);
1294
- }
1295
- else if (oneShot) {
1296
- // M-2266: validated when it was asked for (restart_instance); checked again now, as the rollout may have moved.
1297
- const check = checkResumeId(sessionsDir, config.workingDirectory, oneShot);
1298
- if (check.ok)
1299
- cmd = resumeBy(check.rollout.id, t("codex.resume_oneshot", config.workingDirectory, check.rollout.id), check.rollout.bytes);
1300
- else {
1301
- cmd = this.plannedCommand(config, approvalFlag, sessionsDir, resumeBy);
1302
- this.launchWarning = [t("codex.resume_oneshot_invalid", config.workingDirectory, oneShot, check.reason), this.launchWarning].filter(Boolean).join("\n");
1303
- }
1304
- }
1305
- else {
1306
- cmd = this.plannedCommand(config, approvalFlag, sessionsDir, resumeBy);
1307
- }
1308
- }
1309
- if (config.model) {
1310
- const model = validateModel(config.model);
1311
- warnIfModelMismatch("codex", model);
1312
- cmd += ` -c ${shellQuote(`model="${model}"`)}`;
1313
- }
1314
- if (config.backendOptions?.provider) {
1315
- const provider = validateProvider(String(config.backendOptions.provider));
1316
- cmd += ` -c ${shellQuote(`model_provider="${provider}"`)}`;
1317
- }
1318
- // #1811: the configured effort, when the catalog entry of the model that will run lists it (launch-effort.ts); else
1319
- // a quiet note. That model is proven from the command line or the config files codex reads (#1813 review).
1320
- const runs = resolveCodexLaunchModel({
1321
- isolatedHome: this.isolatedCodexHome, sharedHome: this.sharedCodexHome,
1322
- model: config.model, workingDirectory: config.workingDirectory,
1323
- });
1324
- const effort = codexEffort(config.effort, runs.proven
1325
- ? readCodexModelEffortLevels({ isolatedHome: this.isolatedCodexHome, sharedHome: this.sharedCodexHome, model: runs.model })
1326
- : { model: runs.model, levels: null, unproven: true });
1327
- if (effort.arg)
1328
- cmd += effort.arg;
1329
- this.quietLaunchNotes = effort.note ? [effort.note] : [];
1330
- // AgEnD instances are unattended processes: an interactive self-update
1331
- // picker blocks delivery and must never be enabled by a copied global or
1332
- // managed config layer. Keep this last so the CLI override is authoritative.
1333
- // Both observed Codex 0.155.0 and 0.156.1 support this launch flag. Pin
1334
- // the initial layout so a user/global fullscreen preference cannot make a
1335
- // header-only pane look ready. A later unknown layout still fails closed.
1336
- cmd += " -c check_for_update_on_startup=false --no-alt-screen";
1337
- // codex 0.159's opt-in `instant_interrupt` makes new input steer the
1338
- // running response instead of queueing behind it, which AgEnD's delivery
1339
- // and queued-input (↳) handling assume. Keep it off whatever a user or
1340
- // global config says. Codex before 0.159 ignores the key with a startup
1341
- // warning ("`features.instant_interrupt` is ignored"); nothing else.
1342
- cmd += " -c features.instant_interrupt=false";
1343
- // CODEX_HOME is the only Codex-supported way to isolate the complete base
1344
- // config. A profile only layers over the shared config and would therefore
1345
- // still load every globally registered AgEnD MCP server.
1346
- return `CODEX_HOME=${shellQuote(this.isolatedCodexHome)} ${cmd}`;
1347
- }
1348
- writeConfig(config) {
1349
- this.authorizedTrust = null;
1350
- // Set before the home is prepared: which login this instance gets is a
1351
- // property of the home, and the home is built here.
1352
- this.credentialProfile = this.readProfile(config);
1353
- this.prepareIsolatedHome();
1354
- this.cleanSharedConfig();
1355
- // Copy all user settings but no AgEnD MCP entries into a private base
1356
- // config, then append only this instance's server(s). Each instance writes
1357
- // a distinct file, eliminating the old concurrent global-config race.
1358
- let content = "";
1359
- try {
1360
- content = stripAgendMcpTables(readFileSync(join(this.sharedCodexHome, "config.toml"), "utf-8"));
1361
- }
1362
- catch { /* a first-time Codex user may have no global config */ }
1363
- if (content && !content.endsWith("\n"))
1364
- content += "\n";
1365
- for (const [name, entry] of Object.entries(config.mcpServers)) {
1366
- content += `\n${renderMcpServer(name, entry, config.instanceName)}`;
1367
- }
1368
- atomicWritePrivate(join(this.isolatedCodexHome, "config.toml"), content);
1369
- this.enableContextStatusLine();
1370
- this.disableStartupUpdateCheck();
1371
- this.hideRateLimitModelNudge();
1372
- // Write fleet instructions into AGENTS.md (additive via marker block)
1373
- if (config.instructions) {
1374
- try {
1375
- const agentsMd = join(config.workingDirectory, "AGENTS.md");
1376
- appendWithMarker(agentsMd, config.instanceName, config.instructions);
1377
- // Warn if file exceeds Codex's project_doc_max_bytes limit
1378
- try {
1379
- const size = statSync(agentsMd).size;
1380
- if (size > CODEX_PROJECT_DOC_MAX_BYTES) {
1381
- console.warn(`[agend] AGENTS.md is ${size} bytes, exceeds Codex limit of ${CODEX_PROJECT_DOC_MAX_BYTES} — instructions may be truncated`);
1382
- }
1383
- }
1384
- catch { /* stat failed — skip size check */ }
1385
- }
1386
- catch { /* best effort */ }
1387
- }
1260
+ /** Any footer that proves the Context meter owns the bottom row. */
1261
+ isAnyContextFooter(row) {
1262
+ return isCodexContextFooter(row) || this.isConfiguredContextFooter(row);
1388
1263
  }
1389
1264
  /**
1390
- * Stop Codex opening its "Update available!" picker when an instance starts.
1391
- *
1392
- * That dialog owns the input loop until someone answers it, so after a fleet
1393
- * restart every Codex instance sat waiting on a keypress nobody was going to
1394
- * send. AgEnD then reported it as a stuck pane, which is true but unhelpful.
1395
- *
1396
- * `check_for_update_on_startup = false` is Codex's own config key (verified in
1397
- * the binary), so this prevents the prompt rather than racing to dismiss it.
1398
- * There is no CLI flag for it — `codex update` is a subcommand, not a switch.
1399
- *
1400
- * Placement matters: this is a TOP-LEVEL key, so it has to go above the first
1401
- * `[section]` header or TOML would read it as belonging to that section. And a
1402
- * AgEnD's isolated home is unattended, so an inherited `true` is replaced
1403
- * rather than preserved. TOML rejects duplicate keys, so replace in place.
1265
+ * #978 escape hatch: the live, EMPTY Codex composer within the last few
1266
+ * non-blank rows, no busy marker, and none of the known selection screens.
1267
+ * Deliberately says nothing about the footer, which is what is unknown.
1404
1268
  */
1405
- disableStartupUpdateCheck() {
1406
- const configPath = join(this.isolatedCodexHome, "config.toml");
1407
- let content = "";
1408
- try {
1409
- content = readFileSync(configPath, "utf-8");
1410
- }
1411
- catch {
1412
- return;
1413
- }
1414
- const existing = /^\s*check_for_update_on_startup\s*=\s*(?:true|false)\s*(?:#.*)?$/m;
1415
- if (existing.test(content)) {
1416
- const updated = content.replace(existing, "check_for_update_on_startup = false");
1417
- if (updated !== content) {
1418
- try {
1419
- atomicWritePrivate(configPath, updated);
1420
- }
1421
- catch { /* best effort */ }
1422
- }
1423
- return;
1424
- }
1425
- const LINE = "check_for_update_on_startup = false\n";
1426
- const firstSection = content.search(/^\s*\[/m);
1427
- const updated = firstSection === -1
1428
- ? (content.length && !content.endsWith("\n") ? `${content}\n${LINE}` : `${content}${LINE}`)
1429
- : `${content.slice(0, firstSection)}${LINE}${content.slice(firstSection)}`;
1430
- try {
1431
- atomicWritePrivate(configPath, updated);
1432
- }
1433
- catch { /* best effort */ }
1269
+ isStableUnknownLayoutIdlePane(pane) {
1270
+ if (this.getBusyPattern().test(pane))
1271
+ return false;
1272
+ // The composer is live-looking and the footer is unknown — exactly this
1273
+ // proof's blind spot — but there is no server behind it (#1099).
1274
+ if (codexAppServerDisconnected(pane))
1275
+ return false;
1276
+ // (No second, wider "esc to interrupt" match here: getBusyPattern() above already reads a live status row of ANY
1277
+ // title — #964 — and anchors it to the whole row at column zero, so a reply that quotes the phrase, indented or
1278
+ // in prose, cannot pin the escape hatch shut (#991).)
1279
+ // A retained queued message (`↳ …` under "Messages to be submitted after
1280
+ // next tool call") means work is pending, whatever the composer shows.
1281
+ if (/↳|Messages to be submitted/i.test(pane))
1282
+ return false;
1283
+ if (codexUsageLimitMenuVisible(pane) || codexRateSwitchVisible(pane)
1284
+ || codexUnknownSelectionVisible(pane) || codexUpdatePickerVisible(pane))
1285
+ return false;
1286
+ const rows = pane.replace(/\r/g, "").split("\n").filter(row => row.trim() !== "");
1287
+ return rows.slice(-6).some(row => /^[>›][ \t⋆]*Ask Codex to do anything[ \t⋆]*$/.test(row));
1434
1288
  }
1435
- /**
1436
- * Suppress Codex's rate-limit model-switch reminder for unattended instances.
1437
- * Codex documents this as `notice.hide_rate_limit_model_nudge` (a boolean in
1438
- * the `[notice]` table). This changes only the per-instance config; the
1439
- * runtime holdOnly dialog remains as defense-in-depth for older or changed
1440
- * CLI behavior. No model option is selected automatically.
1441
- */
1442
- hideRateLimitModelNudge() {
1443
- const configPath = join(this.isolatedCodexHome, "config.toml");
1444
- let content = "";
1445
- try {
1446
- content = readFileSync(configPath, "utf-8");
1447
- }
1448
- catch {
1449
- return;
1450
- }
1451
- const updated = setTomlTableBoolean(content, "notice", "hide_rate_limit_model_nudge");
1452
- if (updated !== content) {
1453
- try {
1454
- atomicWritePrivate(configPath, updated);
1455
- }
1456
- catch { /* best effort */ }
1457
- }
1289
+ isCodexIdleFooter(row) {
1290
+ return isCodexContextFooter(row) || this.isConfiguredStatusLineFooter(row);
1458
1291
  }
1459
- /**
1460
- * Ensure Codex's TUI status line shows context usage: the Context item is
1461
- * both what /ctx scrapes and the footer AgEnD's readiness proofs rely on.
1462
- * Rules (never drops or reorders the user's items):
1463
- * 1. the effective `tui.status_line` already has a context item
1464
- * (context-remaining / -usage / -used) → leave the config untouched.
1465
- * 2. no context item:
1466
- * - no status_line at all → write status_line = ["context-remaining"]
1467
- * - status_line exists → prepend "context-remaining" to it
1468
- * Best-effort string edit (no TOML writer), but every candidate edit is
1469
- * parsed back and kept only if the effective `tui.status_line` becomes
1470
- * `["context-remaining", ...the user's items]`. A `status_line` key in some
1471
- * other table is not the TUI's and must not be the one edited (#931). If no
1472
- * edit can be verified, the launch warns the operator instead of leaving the
1473
- * pane silently unready.
1474
- */
1475
- enableContextStatusLine() {
1476
- this.configuredStatusLineItems = undefined;
1477
- this.configuredStatusLinePattern = undefined;
1478
- this.statusLineWarning = null;
1479
- const configPath = join(this.isolatedCodexHome, "config.toml");
1480
- // The private copy is rebuilt from the shared one at every launch, so the
1481
- // operator must be pointed at the file their status_line actually lives in.
1482
- const sharedConfigPath = join(this.sharedCodexHome, "config.toml");
1483
- let content = "";
1484
- try {
1485
- content = readFileSync(configPath, "utf-8");
1486
- }
1487
- catch { /* no file yet */ }
1488
- const ITEM = "context-remaining";
1489
- const current = effectiveTuiStatusLine(content);
1490
- if (current === "invalid") {
1491
- this.statusLineWarning = t("codex.status_line_unverifiable", "tui.status_line is not a list of item names, or config.toml does not parse", sharedConfigPath);
1492
- return;
1493
- }
1494
- // Rule 1: the TUI already shows a context item → don't touch anything.
1495
- if (current?.some(item => CODEX_CONTEXT_STATUS_ITEM_RE.test(item)))
1496
- return;
1497
- // TOML allows quoted keys: `"status_line" = [...]`, `"tui"."status_line"`,
1498
- // `['tui']`. Every textual candidate is tried; parsing decides.
1499
- const candidates = [];
1500
- if (current) {
1501
- // Rule 2b: prepend our item to the user's existing array (don't overwrite).
1502
- // First position keeps "Context N% left" at the far left of the footer so a
1503
- // long cwd/other items can't push it past 80 cols and truncate it.
1504
- for (const m of content.matchAll(/["']?status_line["']?\s*=\s*\[([^\]]*)\]/g)) {
1505
- const inner = m[1].trim().replace(/^,\s*/, "").replace(/,\s*$/, "");
1506
- const newInner = inner.length ? `"${ITEM}", ${inner}` : `"${ITEM}"`;
1507
- candidates.push(`${content.slice(0, m.index)}status_line = [${newInner}]${content.slice(m.index + m[0].length)}`);
1508
- }
1509
- }
1510
- else {
1511
- // Rule 2a: no status_line at all → add a minimal one.
1512
- const base = content.length && !content.endsWith("\n") ? `${content}\n` : content;
1513
- // Also recognise quoted section headers: `["tui"]` and `['tui']`.
1514
- if (/^\[["']?tui["']?\][^\n]*\n/m.test(base)) {
1515
- candidates.push(base.replace(/^\[["']?tui["']?\][^\n]*\n/m, h => `${h}status_line = ["${ITEM}"]\n`));
1516
- }
1517
- candidates.push(`${base}\n[tui]\nstatus_line = ["${ITEM}"]\n`);
1518
- // A config that defines tui with dotted keys cannot take a [tui] table.
1519
- candidates.push(`tui.status_line = ["${ITEM}"]\n${base}`);
1520
- // Nor can an inline `tui = { … }` table: the item goes inside it.
1521
- candidates.push(base.replace(/^([ \t]*["']?tui["']?[ \t]*=[ \t]*\{)[ \t]*\}/m, `$1 status_line = ["${ITEM}"] }`));
1522
- candidates.push(base.replace(/^([ \t]*["']?tui["']?[ \t]*=[ \t]*\{)/m, `$1 status_line = ["${ITEM}"],`));
1523
- }
1524
- const wanted = [ITEM, ...(current ?? [])];
1525
- const fixed = candidates.find(candidate => {
1526
- const items = effectiveTuiStatusLine(candidate);
1527
- return Array.isArray(items) && items.length === wanted.length && items.every((item, i) => item === wanted[i]);
1528
- });
1529
- if (!fixed) {
1530
- this.statusLineWarning = t("codex.status_line_unverifiable", "no edit of tui.status_line could be verified", sharedConfigPath);
1531
- return;
1532
- }
1533
- try {
1534
- atomicWritePrivate(configPath, fixed);
1535
- }
1536
- catch (err) {
1537
- // Never block launch on statusline config — but a verified edit that
1538
- // could not be written leaves the pane as unready as an unfixable one.
1539
- const reason = `the verified edit could not be written: ${err.code ?? "write failed"}`;
1540
- this.statusLineWarning = t("codex.status_line_unverifiable", reason, sharedConfigPath);
1541
- }
1292
+ /** A proxy reply filters chrome per line; whole-pane readiness is separate. */
1293
+ isProxyReplyChromeLine(line) {
1294
+ return /^\s*[›>]\s+Ask Codex to do anything\s*$/.test(line)
1295
+ || this.isAnyContextFooter(line);
1542
1296
  }
1543
- /** Null when the instance did not ask for a profile — today's behaviour. */
1544
- readProfile(config) {
1545
- try {
1546
- return resolveCredentialProfile(config.backendOptions);
1547
- }
1548
- catch {
1549
- // A malformed name is refused where it is written; launching is not the
1550
- // place to discover it, and falling back to the shared login is the
1551
- // behaviour every instance had before profiles existed.
1552
- return null;
1553
- }
1297
+ getErrorPatterns() {
1298
+ return [
1299
+ // Specific quota codes must precede the generic HTTP 429 classifier:
1300
+ // OpenAI returns insufficient_quota with status 429, but switching models
1301
+ // cannot repair an exhausted account and would only start a failover loop.
1302
+ {
1303
+ pattern: new RegExp(String.raw `^\s*(?:${CODEX_STATUS_GLYPH}|Error:|API Error:)\s*[\s\S]{0,240}?\b(?:insufficient_quota|billing_hard_limit_reached|exceeded\s+your\s+current\s+quota)\b|^(?:${CODEX_STATUS_GLYPH}) Quota exceeded\. Check your plan and billing details\.\r?$`, "im"),
1304
+ type: "quota",
1305
+ action: "pause",
1306
+ message: "OpenAI quota exceeded",
1307
+ notice: () => quotaPausedLine("OpenAI", "api_quota"),
1308
+ },
1309
+ {
1310
+ // Codex already exhausted HTTP retries and returned to its composer.
1311
+ // The existing conditional continuation preserves context without
1312
+ // replaying earlier tool effects. Quota remains a preceding pause.
1313
+ pattern: new RegExp(`^${CODEX_EXHAUSTED_429_ROW}$`, "im"),
1314
+ type: "rate_limit",
1315
+ action: "nudge_continue",
1316
+ message: t("inst.codex_rate_retry_stopped"),
1317
+ skipCooldown: true,
1318
+ skipRecoveryWait: true,
1319
+ },
1320
+ {
1321
+ // Pane history contains the agent's own prose, source code and search
1322
+ // results. Bare `rate limit` used to fail over an otherwise healthy
1323
+ // Codex merely for discussing this regex. Match machine-readable API
1324
+ // forms or an error-decorated terminal line instead.
1325
+ pattern: new RegExp(String.raw `^\s*unexpected\s+status\s+429\b|^(?![\s\S]{0,240}?${CODEX_EXHAUSTED_429_ROW})[ \t]*(?:${CODEX_STATUS_GLYPH}|Error:|API Error:)\s*[\s\S]{0,240}?(?:["'](?:status|code)["']\s*:\s*429\b|\b(?:rate_limit_exceeded|too_many_requests)\b|\brate limit(?:ed| exceeded| reached)?\b|\btoo many requests\b)`, "im"),
1326
+ type: "rate_limit",
1327
+ action: "failover",
1328
+ message: "OpenAI rate limit reached",
1329
+ },
1330
+ {
1331
+ // Same false-positive boundary as rate limits: `authentication` is a
1332
+ // normal English/code word. Require a structured 401/code or a
1333
+ // decorated CLI error line before pausing every instance on the shared
1334
+ // credential.
1335
+ pattern: new RegExp(String.raw `^\s*unexpected\s+status\s+401\b|^\s*(?:${CODEX_STATUS_GLYPH}|Error:|API Error:)\s*[\s\S]{0,240}?(?:["'](?:status|code)["']\s*:\s*401\b|\b(?:invalid_api_key|authentication_error)\b|\b401\s+Unauthorized\b|\bauthentication (?:failed|error)\b|\binvalid api key\b)`, "im"),
1336
+ type: "auth_error",
1337
+ action: "pause",
1338
+ message: "OpenAI authentication error",
1339
+ },
1340
+ // Codex writes the apostrophe as U+2019 ("You’ve hit your usage limit.") since
1341
+ // 0.156 and as ASCII before (0.153.4): both must match, or no usage limit
1342
+ // ever pauses the instance (#1098).
1343
+ { pattern: /you['’]ve hit your usage limit/i, type: "quota", action: "pause", message: "Codex usage limit reached — upgrade plan required",
1344
+ notice: () => quotaPausedLine("Codex", "usage") },
1345
+ {
1346
+ // The terminal stage of a lost app-server (#1099): Codex has given up
1347
+ // reconnecting and says so, with a counter that never stops. Nothing
1348
+ // recovers from here without a relaunch, and every message sent in the
1349
+ // meantime waits for an idle that cannot come. Only the LIVE row counts:
1350
+ // directly above the LAST composer with no later transcript item or composer
1351
+ // (the same test as codexAppServerDisconnected, as a lookahead), so text that
1352
+ // merely quotes it does not raise this.
1353
+ pattern: new RegExp(String.raw `^[ \t]*•[ \t]+Reconnect failed[ \t]+—[ \t]+check the endpoint, then relaunch[ \t]+\(\d+[hms](?:[ \t]+\d+[hms])*\)[ \t]*$`
1354
+ + String.raw `(?=\n(?:[ \t]*\n)*[›>][^\n]*(?:\n(?![•■⚠›>])[^\n]*)*(?![\s\S]))`, "mu"),
1355
+ type: "network",
1356
+ action: "notify",
1357
+ message: "Codex lost its app-server connection and could not reconnect — restart the instance (messages cannot be delivered until then)",
1358
+ },
1359
+ {
1360
+ // Codex reports an unknown model either as a TUI metadata fallback or
1361
+ // as a ChatGPT-account API rejection. Use whitespace-aware phrases so
1362
+ // capture-pane hard wraps do not hide either form.
1363
+ // Codex quotes the slug with BACKTICKS ("Model metadata for `x` not found.",
1364
+ // verified live on 0.160.0 and in the 0.153.4–0.160.0 binaries). The old
1365
+ // pattern only took ASCII quotes, so it could never match a real line.
1366
+ pattern: /model\s+metadata\s+for\s+['"`‘’“”][^'"`‘’“”]+['"`‘’“”]\s+not\s+found\.\s+defaulting\s+to\s+fallback\s+metadata|model\s+is\s+not\s+supported\s+when\s+using\s+codex\s+with\s+a\s+chatgpt\s+account/i,
1367
+ type: "model_error",
1368
+ action: "notify",
1369
+ message: "Codex model unavailable — use /model to switch",
1370
+ },
1371
+ {
1372
+ // A capacity rejection is a completed failed turn: Codex returns to its
1373
+ // prompt without an answer, with its context intact. Keep this anchored to
1374
+ // the exact decorated TUI line so ordinary prose about model capacity
1375
+ // cannot trigger anything on an otherwise healthy instance. The status
1376
+ // glyph is decoration that changes between Codex versions and error classes
1377
+ // (`■` U+25A0 on 0.159.2 and 0.160.0, `⚠` U+26A0 before), so any one
1378
+ // "Other Symbol" glyph (optionally with its emoji selector) opens the line —
1379
+ // never a bullet, dash, quote marker or number, which prose and the model's
1380
+ // own transcript items use (`•`, `-`, `>`). The sentence must fill the line,
1381
+ // from column 0. Box drawing and block elements (U+2500–U+259F) are
1382
+ // excluded up front: they are `So` too, but a left-border popup or
1383
+ // table row (`│ …`) is chrome, not a status line. The CLI is
1384
+ // already back at the prompt, so skipRecoveryWait avoids an extra wait.
1385
+ // action "nudge_continue": tell the user, then about a minute later tell
1386
+ // the agent to keep going (the lifecycle bounds this to 3 per 30 minutes,
1387
+ // then pauses; see #905). No restart — nothing would re-send the turn — and
1388
+ // no model switch. skipCooldown: each NEW capacity line is a new episode,
1389
+ // including a second one right after the nudge.
1390
+ pattern: /^(?![─-▟])\p{So}\uFE0F? Selected model is at capacity\. Please try a different model\.\r?$/mu,
1391
+ type: "model_error",
1392
+ action: "nudge_continue",
1393
+ message: t("inst.codex_model_capacity"),
1394
+ skipCooldown: true,
1395
+ skipRecoveryWait: true,
1396
+ },
1397
+ // Workspace (business) accounts report exhaustion differently from personal
1398
+ // ones, with codex's own rows (codex-rs/protocol error.rs and tui
1399
+ // turn_runtime.rs, unchanged 0.160.0–0.165.0):
1400
+ // member: "■ Your workspace is out of credits. Ask your workspace owner to refill in order to continue."
1401
+ // "■ You hit your spend cap set by the owner of your workspace. Ask an owner to increase your spend cap to continue."
1402
+ // owner: "■ You're out of credits. Your workspace is out of credits. Add credits to continue using Codex."
1403
+ // "■ Your workspace is out of credits. Add credits to continue."
1404
+ // "■ You hit your spend cap set in your workspace. Increase your spend cap to continue."
1405
+ // "■ Usage limit reached. You've reached your usage limit. Increase your limits to continue using codex."
1406
+ // Neither `insufficient_quota|billing` nor `you've hit your usage limit`
1407
+ // matches them.
1408
+ //
1409
+ // pause, not notify: nothing works until someone refills or raises the cap.
1410
+ // Anchored to the status glyph in COLUMN 0, because these skip the
1411
+ // usage-window check (a workspace limit is not one of those windows, so the
1412
+ // check would call it "available"): codex draws its own error cells there,
1413
+ // while an agent's message, quote or code block is indented under its `•`
1414
+ // (captured live, tests/fixtures/codex-0165-workspace-quoted.pane.txt).
1415
+ // `\s+` at each word gap: capture-pane runs without -J, so tmux's hard wrap
1416
+ // can land a newline anywhere in the sentence. A resumed transcript reprints
1417
+ // these rows, and the first live composer of a spawn takes them as history.
1418
+ {
1419
+ pattern: new RegExp(String.raw `^(?:${CODEX_STATUS_GLYPH})[ \t]+(?:You['’]re\s+out\s+of\s+credits\.\s+)?Your\s+workspace\s+is\s+out\s+of\s+credits\.`, "m"),
1420
+ type: "quota",
1421
+ action: "pause",
1422
+ message: "Codex workspace credits exhausted — workspace owner must refill",
1423
+ notice: () => quotaPausedLine("Codex", "workspace_credits"),
1424
+ skipUsageVerify: true,
1425
+ historyUntilLiveComposer: true,
1426
+ },
1427
+ {
1428
+ pattern: new RegExp(String.raw `^(?:${CODEX_STATUS_GLYPH})[ \t]+You\s+hit\s+your\s+spend\s+cap\s+set\s+(?:by\s+the\s+owner\s+of|in)\s+your\s+workspace\.`, "m"),
1429
+ type: "quota",
1430
+ action: "pause",
1431
+ message: "Codex workspace spend cap reached — workspace owner must raise it",
1432
+ notice: () => t("inst.quota_spend_cap"),
1433
+ skipUsageVerify: true,
1434
+ historyUntilLiveComposer: true,
1435
+ },
1436
+ {
1437
+ pattern: new RegExp(String.raw `^(?:${CODEX_STATUS_GLYPH})[ \t]+Usage\s+limit\s+reached\.\s+You['’]ve\s+reached\s+your\s+usage\s+limit\.\s+Increase\s+your\s+limits`, "m"),
1438
+ type: "quota",
1439
+ action: "pause",
1440
+ message: "Codex workspace usage limit reached — increase the workspace limits to continue",
1441
+ notice: () => quotaPausedLine("Codex", "workspace_usage"),
1442
+ skipUsageVerify: true,
1443
+ historyUntilLiveComposer: true,
1444
+ },
1445
+ // Codex warns at 10% and 5% remaining, and scopes the limit by period —
1446
+ // "weekly" alone missed every `monthly limit` warning.
1447
+ //
1448
+ // `\s+` at EVERY word gap (not literal spaces) because capture-pane runs without -J,
1449
+ // so tmux's hard wrap can land a newline (plus continuation-line padding)
1450
+ // at any of these word gaps. The trailing "Run /status for a breakdown."
1451
+ // is deliberately NOT part of the pattern — that tail is what actually
1452
+ // wrapped in the reported case.
1453
+ //
1454
+ // `of your <period> limit` is load-bearing: it's what keeps the pattern
1455
+ // off the agent's own prose about percentages.
1456
+ {
1457
+ pattern: /less\s+than\s+(\d+)\s*%\s+of\s+your\s+(hourly|daily|weekly|monthly)\s+limit/i,
1458
+ type: "quota",
1459
+ action: "notify",
1460
+ message: "Codex usage limit running low",
1461
+ formatMessage: (m) => `Codex ${m[2].toLowerCase()} limit: less than ${m[1]}% left`,
1462
+ notice: (m) => (m ? quotaLowLine(m[2], m[1]) : quotaLine("Codex", "usage")),
1463
+ },
1464
+ ];
1465
+ }
1466
+ getStartupDialogs() {
1467
+ const trustHold = this.trustHoldDialog();
1468
+ return [
1469
+ this.autoTrustDialog(),
1470
+ trustHold,
1471
+ this.updatePickerDialog(),
1472
+ this.rateSwitchPickerDialog(),
1473
+ ...this.sessionHoldDialogs(),
1474
+ this.unknownSelectionHoldDialog(),
1475
+ ];
1554
1476
  }
1555
1477
  /**
1556
- * Where this instance's `auth.json` comes from.
1557
- *
1558
- * The shared home unless a profile says otherwise. A profile owns the file
1559
- * itself — not a copy, not a link — so codex refreshing the token writes
1560
- * into that subscription and no other.
1478
+ * #984: session-selection screens only a human may answer. Held (never
1479
+ * keyed) so the delivery gate keeps messages queued and dialog_parked tells
1480
+ * the operator; listed before the generic selection hold for a clear notice.
1561
1481
  */
1562
- authSourceDir() {
1563
- if (!this.credentialProfile)
1564
- return this.sharedCodexHome;
1565
- const spec = credentialHomeSpec(this.binaryName);
1566
- if (!spec)
1567
- return this.sharedCodexHome;
1568
- const home = credentialProfileHome(getAgendHome(), this.binaryName, this.credentialProfile);
1569
- prepareCredentialProfileHome(spec, home);
1570
- return home;
1571
- }
1572
- preTrust(workDir) {
1573
- this.authorizedTrust = null;
1574
- const paths = codexTrustPaths(workDir);
1575
- const configPath = join(this.isolatedCodexHome, "config.toml");
1576
- let content = "";
1577
- try {
1578
- content = readFileSync(configPath, "utf-8");
1579
- }
1580
- catch { }
1581
- const updated = setProjectTrusted(content, paths.root);
1582
- if (updated !== content)
1583
- atomicWritePrivate(configPath, updated);
1584
- // Do not authorize an automatic Enter merely because the write returned:
1585
- // a stale/untrusted section in the effective isolated config must fail shut.
1586
- const onDisk = readFileSync(configPath, "utf-8");
1587
- if (effectiveProjectTrust(onDisk, paths.root) !== "trusted")
1588
- throw new Error("Codex project trust did not persist");
1589
- this.authorizedTrust = paths;
1482
+ sessionHoldDialogs() {
1483
+ return [
1484
+ {
1485
+ pattern: /This conversation is open in another app/,
1486
+ keys: [],
1487
+ description: "Codex session is open in another process (thread lock) — close the other one and press r, or fork manually",
1488
+ holdOnly: true,
1489
+ blocksDelivery: true,
1490
+ inputBlocked: true,
1491
+ remoteCard: codexSelectionCard,
1492
+ isActive: codexSessionLockVisible,
1493
+ },
1494
+ {
1495
+ pattern: /Working directory · resume/,
1496
+ keys: [],
1497
+ description: "Codex resume selected a session from another directory (sibling git worktree) — needs a human choice",
1498
+ holdOnly: true,
1499
+ blocksDelivery: true,
1500
+ inputBlocked: true,
1501
+ remoteCard: codexSelectionCard,
1502
+ isActive: codexResumeCwdPickerVisible,
1503
+ },
1504
+ ];
1590
1505
  }
1591
1506
  /**
1592
- * Preserve Codex login/session/cache sharing while isolating config.toml
1593
- * and the CODEX_HOME-scoped app-server daemon/control runtime directories.
1507
+ * The user's decision (hmsg-53600a2c): codex's folder-trust prompt in an AgEnD session is always answered Trust — the
1508
+ * workspace is the agent's own, whatever preTrust covered (preTrust stays, so the prompt rarely shows at all). Enter,
1509
+ * only with the cursor on "Trust and continue" among the known options (trustCursor); any other layout or row is
1510
+ * held (trustHoldDialog). It trusts the folder only: per-action approvals are separate dialogs and are not touched.
1594
1511
  */
1595
- /** Session dirs every instance must share with the terminal CLI (#506). */
1596
- static SHARED_SESSION_DIRS = ["sessions", "archived_sessions"];
1597
- static PRIVATE_RUNTIME_DIRS = new Set(["app-server-daemon", "app-server-control"]);
1598
- prepareIsolatedHome() {
1599
- mkdirSync(this.isolatedCodexHome, { recursive: true, mode: 0o700 });
1600
- chmodSync(this.isolatedCodexHome, 0o700);
1601
- if (this.sharedCodexHome === this.isolatedCodexHome)
1602
- return;
1603
- // #1034: Codex's private socket directory check rejects directory symlinks.
1604
- // Detach only the exact shared-home links the old mirror pass created,
1605
- // including dangling links. Leave private directories and other links
1606
- // alone; Codex will create its own real runtime directories on launch.
1607
- for (const name of CodexBackend.PRIVATE_RUNTIME_DIRS) {
1608
- const target = join(this.isolatedCodexHome, name);
1609
- try {
1610
- if (lstatSync(target).isSymbolicLink()
1611
- && readlinkSync(target) === join(this.sharedCodexHome, name)) {
1612
- unlinkSync(target);
1613
- }
1614
- }
1615
- catch { /* absent, or another startup already detached the link */ }
1616
- }
1617
- // The session dirs must exist in the SHARED home before the symlink pass:
1618
- // on a fresh install they don't yet, so no link was created, and the first
1619
- // archive made Codex create a REAL dir in the instance-private home — a
1620
- // permanent fork the terminal CLI could never see (#506). mkdir -p is
1621
- // EEXIST-safe under concurrent instance startup.
1622
- for (const dir of CodexBackend.SHARED_SESSION_DIRS) {
1623
- try {
1624
- mkdirSync(join(this.sharedCodexHome, dir), { recursive: true, mode: 0o700 });
1625
- }
1626
- catch { /* best effort */ }
1627
- }
1628
- // Heal homes that already forked: merge the private real dir back into the
1629
- // shared one (never overwriting), then replace it with the symlink.
1630
- for (const dir of CodexBackend.SHARED_SESSION_DIRS) {
1631
- this.migrateDivergedSessionDir(dir);
1632
- }
1633
- // Older versions linked SQLite's WAL/SHM files independently of their
1634
- // base database. If the base file had already been created privately,
1635
- // that split one SQLite database across two homes and caused CANTOPEN (or
1636
- // worse, cross-database journal recovery). Remove only links AgEnD made to
1637
- // the matching shared-home path; real private sidecars remain untouched.
1638
- const healedSidecars = [];
1639
- for (const name of readdirSync(this.isolatedCodexHome)) {
1640
- if (!SQLITE_SIDECAR_RE.test(name))
1641
- continue;
1642
- const target = join(this.isolatedCodexHome, name);
1643
- try {
1644
- if (!lstatSync(target).isSymbolicLink())
1645
- continue;
1646
- const linkTarget = resolve(dirname(target), readlinkSync(target));
1647
- if (linkTarget !== join(this.sharedCodexHome, name))
1648
- continue;
1649
- unlinkSync(target);
1650
- healedSidecars.push(name);
1651
- }
1652
- catch {
1653
- // A concurrent process may remove an ephemeral sidecar/link first.
1654
- }
1655
- }
1656
- if (healedSidecars.length > 0) {
1657
- console.warn(`[agend] removed unsafe Codex SQLite sidecar links: ${healedSidecars.join(", ")}`);
1658
- }
1659
- // One file, possibly from somewhere else. Everything below is unchanged:
1660
- // sessions, the thread/state/memory databases and every cache still come
1661
- // from the shared home, which is why switching subscription does not throw
1662
- // the conversation away.
1663
- this.linkAuthFile();
1664
- for (const name of readdirSync(this.sharedCodexHome)) {
1665
- if (CodexBackend.PRIVATE_RUNTIME_DIRS.has(name))
1666
- continue;
1667
- if (name === "config.toml" || name === AGEND_MCP_CLEANUP_LOCK || name.startsWith(".config.toml."))
1668
- continue;
1669
- if (name === CODEX_AUTH_FILE)
1670
- continue; // handled above, possibly from a profile
1671
- // SQLite resolves a symlinked base DB to the shared path and creates its
1672
- // own adjacent sidecars there. Linking sidecars separately is redundant
1673
- // for shared bases and corrupts the file set for private bases.
1674
- if (SQLITE_SIDECAR_RE.test(name))
1675
- continue;
1676
- const source = join(this.sharedCodexHome, name);
1677
- const target = join(this.isolatedCodexHome, name);
1678
- if (existsSync(target))
1679
- continue;
1680
- try {
1681
- const type = lstatSync(source).isDirectory() ? "dir" : "file";
1682
- symlinkSync(source, target, type);
1683
- }
1684
- catch {
1685
- // State/cache links are compatibility aids; config isolation must not
1686
- // fail merely because a concurrently-created cache entry disappeared.
1687
- }
1688
- }
1512
+ autoTrustDialog() {
1513
+ return {
1514
+ pattern: /^\s*Trust this folder\?/m,
1515
+ keys: ["Enter"],
1516
+ description: "Codex folder trust — trusting the agent's own folder",
1517
+ blocksDelivery: true,
1518
+ inputBlocked: true,
1519
+ autoResolutionKey: "codex-folder-auto-trust",
1520
+ // One Enter per prompt (claimed, then checked): a prompt still up after it is held, never pressed again (#2896 review).
1521
+ verifyAfterKeys: true,
1522
+ isActive: pane => codexTrustPromptState(pane).trustCursor,
1523
+ answerDetail: pane => ({ autoTrusted: true, ...this.trustPaneFields(pane) }),
1524
+ };
1525
+ }
1526
+ trustHoldDialog() {
1527
+ return {
1528
+ pattern: /^\s*(?:Folder access|Trust this folder\?|Do you trust the files in this folder\?)/im,
1529
+ keys: [],
1530
+ description: "Codex folder trust needs human confirmation",
1531
+ holdOnly: true,
1532
+ blocksDelivery: true,
1533
+ inputBlocked: true,
1534
+ isActive: codexTrustVariantActive,
1535
+ remoteCard: codexSelectionCard,
1536
+ heldReason: pane => this.trustHoldReason(pane),
1537
+ };
1689
1538
  }
1690
1539
  /**
1691
- * Point this home's `auth.json` at whichever login it should use.
1540
+ * #1008: the rate-limit model-switch picker parked unattended instances (it
1541
+ * waits forever and its default option changes the model). Prevention is
1542
+ * `notice.hide_rate_limit_model_nudge` (hideRateLimitModelNudge — real and
1543
+ * effective on 0.157.1 and 0.159.2, verified live); this is the answer for a
1544
+ * picker that appears anyway (config not applied, a CLI that moved the key).
1692
1545
  *
1693
- * Codex refreshes the token by writing through the link rather than replacing
1694
- * it — observed across every instance home on a machine that has been running
1695
- * this arrangement for months, with the shared file's mtime moving. That is
1696
- * behaviour, though, not a contract: a release that starts renaming over the
1697
- * file would leave a real file here and quietly fork the login, with refreshes
1698
- * landing somewhere the profile never sees. So a real file where a link
1699
- * belongs is treated as exactly that — reported, and put back.
1546
+ * Escape, not a navigated Enter: it selects nothing, so it can never take
1547
+ * option 1 (switch model) or option 3 (writes "never show again" into the
1548
+ * config). Deliberately not one-shot: if the picker swallowed the key it is
1549
+ * simply sent again on the next poll, and a repeated Escape is harmless.
1700
1550
  */
1701
- linkAuthFile() {
1702
- const source = join(this.authSourceDir(), CODEX_AUTH_FILE);
1703
- const target = join(this.isolatedCodexHome, CODEX_AUTH_FILE);
1704
- let existing = null;
1705
- try {
1706
- existing = lstatSync(target);
1707
- }
1708
- catch { /* absent */ }
1709
- if (existing?.isSymbolicLink()) {
1710
- let current = "";
1711
- try {
1712
- current = resolve(dirname(target), readlinkSync(target));
1713
- }
1714
- catch { /* unreadable */ }
1715
- if (current === source)
1716
- return;
1717
- try {
1718
- unlinkSync(target);
1719
- }
1720
- catch { /* raced */ }
1721
- }
1722
- else if (existing) {
1723
- // Not a link: codex replaced it, or an older AgEnD copied it. Either way
1724
- // the refreshes it has been collecting are stranded in this instance's
1725
- // home, so say so rather than deleting them silently.
1726
- const stranded = `${target}.replaced-${Date.now()}`;
1727
- try {
1728
- renameSync(target, stranded);
1729
- console.warn(`[agend] codex replaced ${CODEX_AUTH_FILE} in ${this.isolatedCodexHome} with a real file — `
1730
- + `its login was no longer shared. Kept a copy at ${stranded} and re-linked to ${source}.`);
1731
- }
1732
- catch {
1733
- return; // cannot move it; leave the instance working on what it has
1734
- }
1735
- }
1736
- if (!existsSync(source))
1737
- return; // not logged in yet; codex will create it
1738
- try {
1739
- symlinkSync(source, target, "file");
1740
- }
1741
- catch {
1742
- // EEXIST from a concurrent start is the ordinary case and means the link
1743
- // is already there.
1744
- }
1551
+ rateSwitchPickerDialog() {
1552
+ return {
1553
+ // Documentation only: Daemon.dialogMatches uses isActive INSTEAD of this
1554
+ // pattern, so it filters nothing here — the structural check below is the
1555
+ // whole decision, and ordinary prose about the picker fails it. The words
1556
+ // are the picker's own, kept so a reader can find the screen from the log.
1557
+ pattern: /Keep current model \(never show again\)/,
1558
+ keys: ["Escape"],
1559
+ description: "Codex rate-limit model-switch picker — Escape keeps the current model",
1560
+ blocksDelivery: true,
1561
+ inputBlocked: true,
1562
+ isActive: codexRateSwitchPickerVisible,
1563
+ };
1745
1564
  }
1746
- /**
1747
- * One-time heal for an instance whose isolated home grew a REAL session dir
1748
- * (#506): merge it into the shared home without overwriting anything, and
1749
- * only after both the copy and the removal succeed put the symlink in its
1750
- * place. Any failure keeps the private dir — sessions are never the thing
1751
- * sacrificed for tidiness — and logs what happened.
1752
- */
1753
- migrateDivergedSessionDir(name) {
1754
- const target = join(this.isolatedCodexHome, name);
1755
- let st;
1756
- try {
1757
- st = lstatSync(target);
1758
- }
1759
- catch {
1760
- return; // absent — the symlink pass below will create the link
1761
- }
1762
- if (st.isSymbolicLink())
1763
- return; // already correct
1764
- if (!st.isDirectory()) {
1765
- console.warn(`[agend] codex-home/${name} is neither a symlink nor a directory — leaving it untouched`);
1766
- return;
1767
- }
1768
- const shared = join(this.sharedCodexHome, name);
1769
- try {
1770
- mkdirSync(shared, { recursive: true, mode: 0o700 });
1771
- // cp -rn semantics: collisions keep the shared copy. Session rollout
1772
- // filenames are timestamp+uuid, so a genuine collision means the same
1773
- // file — skipping is lossless either way.
1774
- cpSync(target, shared, { recursive: true, force: false, errorOnExist: false });
1775
- rmSync(target, { recursive: true });
1776
- symlinkSync(shared, target, "dir");
1777
- console.warn(`[agend] migrated diverged codex ${name} into shared home: ${shared}`);
1778
- }
1779
- catch (err) {
1780
- // Keep whatever is left of the private dir. The copy never overwrites, so
1781
- // a retry on next start is safe; worst case is duplicated files in the
1782
- // shared home, never lost ones.
1783
- console.warn(`[agend] codex ${name} migration failed — keeping the instance-private dir (${err.message})`);
1784
- }
1565
+ updatePickerDialog() {
1566
+ return {
1567
+ // Defense in depth for config written by older AgEnD versions or a Codex
1568
+ // regression. Match the release URL as well as the banner so ordinary
1569
+ // agent prose cannot trigger a keypress.
1570
+ pattern: /Update available![\s\S]{0,200}Release notes: https:\/\/github\.com\/openai\/codex\/releases/m,
1571
+ keys: ["Escape"],
1572
+ description: "Codex startup update-available picker",
1573
+ // A delivery must never land on this screen. Its selected option is
1574
+ // `› 1. Update now (runs … curl … | sh)` — the same `›` the input row
1575
+ // uses — so an Enter here runs an installer instead of sending a message.
1576
+ // The runtime dismisser presses Escape, but it only polls every few
1577
+ // seconds and a delivery can arrive first (hit live on codex-cli 0.153.4,
1578
+ // which parks on this picker for as long as nobody answers it).
1579
+ blocksDelivery: true,
1580
+ // Daemon.dialogMatches uses isActive INSTEAD OF pattern when present.
1581
+ // Check the complete current picker here; a different Enter-only menu
1582
+ // must never receive this automatic Escape key.
1583
+ isActive: codexUpdatePickerVisible,
1584
+ };
1785
1585
  }
1786
- /**
1787
- * One-time migration for installations polluted by the old global `codex
1788
- * mcp add` path. The lock protects separate fleet processes/upgrades, and
1789
- * atomic rename prevents readers from observing a truncated config.
1790
- */
1791
- cleanSharedConfig() {
1792
- if (this.sharedCodexHome === this.isolatedCodexHome)
1793
- return;
1794
- mkdirSync(this.sharedCodexHome, { recursive: true, mode: 0o700 });
1795
- const lockPath = join(this.sharedCodexHome, AGEND_MCP_CLEANUP_LOCK);
1796
- let lockFd;
1797
- const acquire = () => {
1798
- try {
1799
- lockFd = openSync(lockPath, "wx", 0o600);
1800
- return true;
1801
- }
1802
- catch {
1803
- return false;
1804
- }
1586
+ usageLimitLunaReserveDialog() {
1587
+ // The real codex usage-limit dialog states codex has ALREADY switched to
1588
+ // Luna Reserve and labels Escape as "continue working". Pressing Escape via
1589
+ // sendSpecialKey is the ONLY safe key:
1590
+ //
1591
+ // • Escape is sent via sendSpecialKey — never via pasteText (which adds
1592
+ // an implicit bracketed-paste Enter that could confirm the highlighted
1593
+ // server action: Upgrade, Reset usage, Add Credits, …).
1594
+ // • A digit selects its row at once, and pasted its implicit Enter would
1595
+ // confirm whatever it landed on. This path is explicitly NOT used.
1596
+ //
1597
+ // verifyAfterKeys: true — confirm the menu is gone after Escape.
1598
+ // No confirmBeforeEnter / keysAfterConfirm: Escape needs no confirm gate.
1599
+ return {
1600
+ pattern: /Press enter to confirm or esc to continue working/i,
1601
+ keys: ["Escape"],
1602
+ description: "Codex usage limit — pressing Escape to continue with Luna Reserve",
1603
+ blocksDelivery: true,
1604
+ inputBlocked: true,
1605
+ isActive: codexUsageLimitMenuVisible,
1606
+ verifyAfterKeys: true,
1607
+ autoResolutionKey: "codex-usage-limit-luna-reserve",
1608
+ postDismissNotice: {
1609
+ text: t("inst.codex_usage_limit_luna_reserve", ""), // instance name injected by daemon
1610
+ label: "codex-usage-limit-selected",
1611
+ },
1805
1612
  };
1806
- if (!acquire()) {
1807
- try {
1808
- if (Date.now() - statSync(lockPath).mtimeMs > AGEND_MCP_CLEANUP_LOCK_STALE_MS) {
1809
- unlinkSync(lockPath);
1810
- if (!acquire())
1811
- return;
1812
- }
1813
- else {
1814
- return;
1815
- }
1816
- }
1817
- catch {
1818
- return;
1819
- }
1820
- }
1821
- try {
1822
- const configPath = join(this.sharedCodexHome, "config.toml");
1823
- let content;
1824
- try {
1825
- content = readFileSync(configPath, "utf-8");
1826
- }
1827
- catch {
1828
- return;
1829
- }
1830
- const cleaned = stripAgendMcpTables(content);
1831
- if (cleaned !== content)
1832
- atomicWritePrivate(configPath, cleaned);
1833
- }
1834
- finally {
1835
- if (lockFd !== undefined) {
1836
- try {
1837
- closeSync(lockFd);
1838
- }
1839
- catch { }
1840
- }
1841
- try {
1842
- unlinkSync(lockPath);
1843
- }
1844
- catch { }
1845
- }
1846
1613
  }
1847
- getReadyPattern() {
1848
- // Header and context text persist in inline scrollback and even while the
1849
- // CLI is loading or a modal owns stdin. Require the live prompt followed
1850
- // by the Context footer at the *end* of the capture. A valid custom
1851
- // status_line without Context gets a separate configured-item pattern;
1852
- // unknown footer values still fail closed. getBusyPattern vetoes a working
1853
- // turn whose empty composer remains visible.
1854
- // U+22C6 is Codex's observed cosmetic starfield; it can be drawn in the
1855
- // prompt, between prompt/footer, and below the footer. A drafted composer
1856
- // is also idle once this same bottom footer proves it owns the screen.
1857
- const contextReady = /(?:^|\n)[>›][ \t⋆]+(?!\d+\.)\S[^\r\n]*\r?\n(?:[ \t⋆]*\r?\n){0,3}[ \t⋆]+(?:[0-9a-f-]{36}[ \t]+·[ \t]+)?Context[ \t]+(?:\d+%[ \t]+(?:left|used)|\d+…|…)[^\r\n]*(?:\r?\n[ \t⋆]*)*$/i;
1858
- const footerPattern = this.configuredStatusLineFooterPattern();
1859
- if (!footerPattern)
1860
- return contextReady;
1861
- // The no-Context alternative requires the exact empty composer followed by
1862
- // a recognized configured Codex status_line value. If run-state is
1863
- // configured, that value must specifically be Ready. This is positive
1864
- // structural proof, never a deny-list of known dialogs.
1865
- const footerSource = footerPattern.source.slice(1, -1);
1866
- const noContextReady = new RegExp(String.raw `(?:^|\n)[>›][ \t⋆]*Ask Codex to do anything[ \t⋆]*\r?\n(?:[ \t⋆]*\r?\n){0,3}${footerSource}(?:\r?\n[ \t⋆]*)*$`, "iu");
1867
- // #978: a configured footer that carries a Context item in any position
1868
- // is the Context footer: same drafted-composer rule as Context-first. The
1869
- // whole row must match the configured item grammar, so an arbitrary
1870
- // indented line that merely contains `Context N% left` cannot pass.
1871
- const configuredContextReady = this.configuredHasContextItem()
1872
- ? new RegExp(String.raw `(?:^|\n)[>›][ \t⋆]+(?!\d+\.)\S[^\r\n]*\r?\n(?:[ \t⋆]*\r?\n){0,3}(?=[^\r\n]*${CODEX_CONTEXT_ITEM})${footerSource}(?:\r?\n[ \t⋆]*)*$`, "iu")
1873
- : null;
1874
- return new RegExp([contextReady.source, noContextReady.source, configuredContextReady?.source]
1875
- .filter((source) => !!source)
1876
- .map(source => `(?:${source})`)
1877
- .join("|"), "iu");
1614
+ workspaceLimitPromptDialog() {
1615
+ // Escape via sendSpecialKey, the same as the Luna Reserve menu: never a paste
1616
+ // (its implicit Enter would confirm the cursor's row) and never "y". The error
1617
+ // row codex printed above the prompt stays, and the quota pause takes it from
1618
+ // there: one notice, no parked dialog.
1619
+ return {
1620
+ pattern: /^\s*enter select\s*[·•]\s*esc back\s*$/im,
1621
+ keys: ["Escape"],
1622
+ description: "Codex workspace limit prompt — pressing Escape (never asking the owner)",
1623
+ blocksDelivery: true,
1624
+ inputBlocked: true,
1625
+ isActive: codexWorkspaceLimitPromptVisible,
1626
+ verifyAfterKeys: true,
1627
+ autoResolutionKey: "codex-workspace-limit-prompt",
1628
+ answeredByAgend: true,
1629
+ marksLiveIncident: true,
1630
+ };
1878
1631
  }
1879
- readConfiguredStatusLineItems() {
1880
- if (this.configuredStatusLineItems !== undefined)
1881
- return this.configuredStatusLineItems;
1882
- try {
1883
- const config = tomlTable(parseToml(readFileSync(join(this.isolatedCodexHome, "config.toml"), "utf-8")));
1884
- const tui = tomlTable(config?.tui);
1885
- const items = tui?.status_line;
1886
- this.configuredStatusLineItems = Array.isArray(items) && items.every(item => typeof item === "string")
1887
- ? items
1888
- : null;
1889
- }
1890
- catch {
1891
- this.configuredStatusLineItems = null;
1892
- }
1893
- return this.configuredStatusLineItems;
1632
+ unknownSelectionHoldDialog() {
1633
+ return {
1634
+ pattern: /^\s*[›❯>]\s+\S/m,
1635
+ keys: [],
1636
+ description: "Codex interactive selection needs human input",
1637
+ holdOnly: true,
1638
+ blocksDelivery: true,
1639
+ inputBlocked: true,
1640
+ remoteCard: codexSelectionCard,
1641
+ isActive: codexUnknownSelectionVisible,
1642
+ };
1894
1643
  }
1895
- configuredStatusLineFooterPattern() {
1896
- if (this.configuredStatusLinePattern !== undefined)
1897
- return this.configuredStatusLinePattern;
1898
- const items = this.readConfiguredStatusLineItems();
1899
- if (!items?.length)
1900
- return this.configuredStatusLinePattern = null;
1901
- const configuredPatterns = items
1902
- .map(item => CODEX_STATUS_LINE_VALUE_PATTERNS[item])
1903
- .filter((pattern) => pattern !== undefined);
1904
- if (!configuredPatterns.length)
1905
- return this.configuredStatusLinePattern = null;
1906
- const segment = `(?:${configuredPatterns.join("|")})`;
1907
- const separator = String.raw `[ \t]+·[ \t]+`;
1908
- const hasRunState = items.some(item => CODEX_RUN_STATE_STATUS_ITEMS.has(item));
1909
- // Ready must occupy its own status-line segment. Other configured items
1910
- // may precede/follow it, as Codex joins visible values with ` · `.
1911
- const line = hasRunState
1912
- ? `(?:${segment}${separator})*Ready(?:${separator}${segment})*`
1913
- : `${segment}(?:${separator}${segment})*`;
1914
- // The configured items, then (optionally) the CLI's own native fields — the same rule as the Context-first footer, so a
1915
- // `model · Context 46% left Goal achieved (1h 6m)` row is the footer too, and the readiness regex built from this
1916
- // source (footerSource) says the same.
1917
- this.configuredStatusLinePattern = new RegExp(`^[ \t⋆]*${line}(?:${CODEX_NATIVE_FOOTER_FIELDS})?[ \t⋆]*$`, "iu");
1918
- return this.configuredStatusLinePattern;
1644
+ getRuntimeDialogs() {
1645
+ return [
1646
+ this.autoTrustDialog(),
1647
+ this.trustHoldDialog(),
1648
+ this.rateSwitchPickerDialog(),
1649
+ {
1650
+ // Codex 0.156 may change the wording/order of this credit-cost choice.
1651
+ // Never navigate it by position: a moved option could switch to a
1652
+ // more expensive model. A live picker holds delivery and notifies a
1653
+ // human; old transcript mentions are excluded by the tail matcher.
1654
+ pattern: /(?:Approaching rate limits|Switch to [^\r\n]{1,120} for lower credit usage\?)/i,
1655
+ keys: [],
1656
+ description: "Codex rate limit model switch dialog needs human choice",
1657
+ holdOnly: true,
1658
+ blocksDelivery: true,
1659
+ inputBlocked: true,
1660
+ remoteCard: codexSelectionCard,
1661
+ isActive: codexRateSwitchVisible,
1662
+ },
1663
+ this.updatePickerDialog(),
1664
+ this.usageLimitLunaReserveDialog(),
1665
+ this.workspaceLimitPromptDialog(),
1666
+ ...this.sessionHoldDialogs(),
1667
+ this.unknownSelectionHoldDialog(),
1668
+ ];
1919
1669
  }
1920
- isConfiguredStatusLineFooter(row) {
1921
- const pattern = this.configuredStatusLineFooterPattern();
1922
- if (!pattern)
1923
- return false;
1924
- pattern.lastIndex = 0;
1925
- return pattern.test(row);
1670
+ getInputUnavailableTransients() {
1671
+ return [{
1672
+ pattern: /(?:^|\n)\s*Resuming session…\s*(?:\n|$)/,
1673
+ description: "Codex session resume in progress",
1674
+ isActive: (pane) => {
1675
+ const rows = pane.split(/\r?\n/);
1676
+ return codexBoxedResumeLoading(rows) || codexUnboxedResumeLoading(rows);
1677
+ },
1678
+ }];
1926
1679
  }
1927
- configuredHasContextItem() {
1928
- return (this.readConfiguredStatusLineItems() ?? []).some(item => CODEX_CONTEXT_STATUS_ITEMS.has(item));
1680
+ /** Only a resume paints "Resuming session…"; a fresh launch never does. */
1681
+ launchMayShowInputTransient() {
1682
+ return this.paneDetectorContext?.launchMayResume() ?? this.lastLaunchResumes;
1683
+ }
1684
+ getQuitCommand() { return "/quit"; }
1685
+ getCompactCommand() { return "/compact"; }
1686
+ getClearCommand() { return "/clear"; }
1687
+ getCancelKey() { return "Escape"; }
1688
+ }
1689
+ export class CodexBackend extends CodexPaneDetectors {
1690
+ instanceDir;
1691
+ binaryPath;
1692
+ sharedCodexHome;
1693
+ isolatedCodexHome;
1694
+ /** Which subscription this instance runs on, or null for the shared login. */
1695
+ credentialProfile = null;
1696
+ /** Set only after preTrust wrote and read back this instance's private config. */
1697
+ authorizedTrust = null;
1698
+ /** Set by buildCommand when the resume plan fell back; read once by the daemon. */
1699
+ launchWarning = null;
1700
+ /** Set by writeConfig when the status_line could not be made verifiable (#931). */
1701
+ statusLineWarning = null;
1702
+ /** #2357: set by buildCommand when the codex being launched lacks a package companion (codex-code-mode-host). */
1703
+ packageWarning = null;
1704
+ constructor(instanceDir) {
1705
+ super();
1706
+ this.instanceDir = instanceDir;
1707
+ this.binaryPath = resolveBinary("codex");
1708
+ this.sharedCodexHome = resolve(process.env.CODEX_HOME?.trim() || join(homedir(), ".codex"));
1709
+ this.isolatedCodexHome = CodexBackend.resolveShortHome(instanceDir);
1929
1710
  }
1930
1711
  /**
1931
- * #978: a Context item in ANY position, accepted only as part of a footer
1932
- * whose every segment matches the configured status_line items. Codex keeps
1933
- * the user's order (`gpt-5.6-sol medium · Context 46% left · ~/x`); an
1934
- * arbitrary indented transcript line that happens to contain the item does
1935
- * not match that grammar. A stale config fails closed, and the daemon's
1936
- * stable-composer escape hatch then keeps the instance from latching.
1712
+ * Compute and (on first call for an existing instance) migrate to a SHORT
1713
+ * persistent CODEX_HOME under `~/.agend/cx/<8-char-hash>/`.
1714
+ *
1715
+ * Background (#953): codex 0.157.0 added `app-server-control.sock` under
1716
+ * CODEX_HOME. For instances with long `-t<topic_id>` suffixes the full path
1717
+ * exceeds the Unix socket SUN_LEN (~107 chars). codex canonicalises the path
1718
+ * before binding the socket, so a symlink workaround does not help — the real
1719
+ * path must be short.
1720
+ *
1721
+ * Migration steps (each is idempotent; half-completed states self-heal):
1722
+ * 1. If shortHome does not exist AND legacyHome is a real directory (not a
1723
+ * symlink) → atomic renameSync. On EXDEV or any other error, fall back
1724
+ * safely: keep using the legacy long path (instance can still start, just
1725
+ * without the short-path fix).
1726
+ * 2. If shortHome exists but legacyHome is missing → recreate the backward-
1727
+ * compat symlink (self-heal for rename-succeeded-but-symlink-failed crash).
1728
+ * 3. If both shortHome and legacyHome (as symlink) exist → nothing to do.
1729
+ * 4. New instance (no legacyHome) → create shortHome directly.
1730
+ *
1731
+ * Fail-safe guarantee: migration failure must never make the instance worse.
1732
+ * The caller always receives a valid path it can use as CODEX_HOME.
1937
1733
  */
1938
- // The configured grammar already makes Context a whole segment, so the item
1939
- // test only has to establish that one is present.
1940
- isConfiguredContextFooter(row) {
1941
- return this.configuredHasContextItem()
1942
- && new RegExp(CODEX_CONTEXT_ITEM, "iu").test(row)
1943
- && this.isConfiguredStatusLineFooter(row);
1734
+ /** Exposed for fleet-manager to delete the short home on instance removal. */
1735
+ static shortHomeFor(instanceDir) {
1736
+ return codexShortHomeFor(instanceDir);
1944
1737
  }
1945
- /** Any footer that proves the Context meter owns the bottom row. */
1946
- isAnyContextFooter(row) {
1947
- return isCodexContextFooter(row) || this.isConfiguredContextFooter(row);
1738
+ static resolveShortHome(instanceDir) {
1739
+ const shortHome = CodexBackend.shortHomeFor(instanceDir);
1740
+ const shortBase = join(getAgendHome(), "cx");
1741
+ const canonical = resolve(instanceDir);
1742
+ const legacyHome = resolve(instanceDir, "codex-home");
1743
+ // Only migrate/create when the instance directory itself exists.
1744
+ // Constructing CodexBackend for a non-existent dir (cli-env probes, test
1745
+ // backends that never ran) must not litter ~/.agend/cx/ with orphan dirs.
1746
+ if (!existsSync(canonical))
1747
+ return shortHome;
1748
+ const legacyExists = existsSync(legacyHome);
1749
+ const legacyIsRealDir = legacyExists && !lstatSync(legacyHome).isSymbolicLink();
1750
+ const shortExists = existsSync(shortHome);
1751
+ if (!shortExists) {
1752
+ mkdirSync(shortBase, { recursive: true, mode: 0o700 });
1753
+ if (legacyIsRealDir) {
1754
+ // Attempt atomic rename. Requires same device; fails with EXDEV otherwise.
1755
+ try {
1756
+ renameSync(legacyHome, shortHome);
1757
+ // Log the one-time migration for observability.
1758
+ try {
1759
+ const logPath = join(shortBase, `${shortHome.split("/").at(-1)}.migrated`);
1760
+ writeFileSync(logPath, `${new Date().toISOString()} migrated from ${legacyHome}\n`);
1761
+ }
1762
+ catch { /* log failure is non-fatal */ }
1763
+ }
1764
+ catch (err) {
1765
+ // EXDEV (cross-device) or any other rename failure → fail-safe:
1766
+ // fall back to the legacy long path so the instance still works.
1767
+ // The socket-length bug persists for this instance but no data is lost.
1768
+ const code = err.code ?? "unknown";
1769
+ try {
1770
+ const warnPath = join(shortBase, `${shortHome.split("/").at(-1)}.migration-failed`);
1771
+ writeFileSync(warnPath, `${new Date().toISOString()} rename failed (${code}): ${err.message}\n`
1772
+ + `legacy path remains in use: ${legacyHome}\n`);
1773
+ }
1774
+ catch { /* warn log failure is also non-fatal */ }
1775
+ // Return legacy long path so the instance starts (socket may still fail
1776
+ // on 0.157.0 for long names, but no data is lost or corrupted).
1777
+ return legacyHome;
1778
+ }
1779
+ }
1780
+ else if (!legacyExists) {
1781
+ // New instance — no data to migrate.
1782
+ mkdirSync(shortHome, { recursive: true, mode: 0o700 });
1783
+ }
1784
+ // If legacyExists but is already a symlink: shortHome was deleted externally
1785
+ // while the symlink still points to it. Re-create shortHome as a fresh dir.
1786
+ if (!existsSync(shortHome)) {
1787
+ mkdirSync(shortHome, { recursive: true, mode: 0o700 });
1788
+ }
1789
+ }
1790
+ // Backward-compat symlink: instanceDir/codex-home → shortHome.
1791
+ // Also serves as self-heal: if a previous run renamed successfully but
1792
+ // crashed before creating the symlink, we recreate it here.
1793
+ if (!legacyExists || legacyIsRealDir) {
1794
+ // legacyIsRealDir means rename just happened (now shortHome exists, legacyHome gone).
1795
+ // !legacyExists means new instance or symlink was deleted — create it.
1796
+ try {
1797
+ symlinkSync(shortHome, legacyHome);
1798
+ }
1799
+ catch { /* race or already exists */ }
1800
+ }
1801
+ return shortHome;
1948
1802
  }
1949
1803
  /**
1950
- * #978 escape hatch: the live, EMPTY Codex composer within the last few
1951
- * non-blank rows, no busy marker, and none of the known selection screens.
1952
- * Deliberately says nothing about the footer, which is what is unknown.
1804
+ * The row codex paints for input it has taken into its own queue instead of
1805
+ * submitting: `↳ <message>` under "Messages to be submitted after next tool
1806
+ * call (press esc to interrupt and send immediately)".
1953
1807
  */
1954
- isStableUnknownLayoutIdlePane(pane) {
1955
- if (this.getBusyPattern().test(pane))
1956
- return false;
1957
- // The composer is live-looking and the footer is unknown — exactly this
1958
- // proof's blind spot — but there is no server behind it (#1099).
1959
- if (codexAppServerDisconnected(pane))
1960
- return false;
1961
- // (No second, wider "esc to interrupt" match here: getBusyPattern() above already reads a live status row of ANY
1962
- // title — #964 — and anchors it to the whole row at column zero, so a reply that quotes the phrase, indented or
1963
- // in prose, cannot pin the escape hatch shut (#991).)
1964
- // A retained queued message (`↳ …` under "Messages to be submitted after
1965
- // next tool call") means work is pending, whatever the composer shows.
1966
- if (/↳|Messages to be submitted/i.test(pane))
1967
- return false;
1968
- if (codexUsageLimitMenuVisible(pane) || codexRateSwitchVisible(pane)
1969
- || codexUnknownSelectionVisible(pane) || codexUpdatePickerVisible(pane))
1970
- return false;
1971
- const rows = pane.replace(/\r/g, "").split("\n").filter(row => row.trim() !== "");
1972
- return rows.slice(-6).some(row => /^[>›][ \t⋆]*Ask Codex to do anything[ \t⋆]*$/.test(row));
1973
- }
1974
- isCodexIdleFooter(row) {
1975
- return isCodexContextFooter(row) || this.isConfiguredStatusLineFooter(row);
1976
- }
1977
- /** A proxy reply filters chrome per line; whole-pane readiness is separate. */
1978
- isProxyReplyChromeLine(line) {
1979
- return /^\s*[›>]\s+Ask Codex to do anything\s*$/.test(line)
1980
- || this.isAnyContextFooter(line);
1808
+ getQueuedInputMarker() {
1809
+ return /↳/;
1981
1810
  }
1982
- getErrorPatterns() {
1983
- return [
1984
- // Specific quota codes must precede the generic HTTP 429 classifier:
1985
- // OpenAI returns insufficient_quota with status 429, but switching models
1986
- // cannot repair an exhausted account and would only start a failover loop.
1987
- {
1988
- pattern: new RegExp(String.raw `^\s*(?:${CODEX_STATUS_GLYPH}|Error:|API Error:)\s*[\s\S]{0,240}?\b(?:insufficient_quota|billing_hard_limit_reached|exceeded\s+your\s+current\s+quota)\b|^(?:${CODEX_STATUS_GLYPH}) Quota exceeded\. Check your plan and billing details\.\r?$`, "im"),
1989
- type: "quota",
1990
- action: "pause",
1991
- message: "OpenAI quota exceeded",
1992
- notice: () => quotaPausedLine("OpenAI", "api_quota"),
1993
- },
1994
- {
1995
- // Codex already exhausted HTTP retries and returned to its composer.
1996
- // The existing conditional continuation preserves context without
1997
- // replaying earlier tool effects. Quota remains a preceding pause.
1998
- pattern: new RegExp(`^${CODEX_EXHAUSTED_429_ROW}$`, "im"),
1999
- type: "rate_limit",
2000
- action: "nudge_continue",
2001
- message: t("inst.codex_rate_retry_stopped"),
2002
- skipCooldown: true,
2003
- skipRecoveryWait: true,
2004
- },
2005
- {
2006
- // Pane history contains the agent's own prose, source code and search
2007
- // results. Bare `rate limit` used to fail over an otherwise healthy
2008
- // Codex merely for discussing this regex. Match machine-readable API
2009
- // forms or an error-decorated terminal line instead.
2010
- pattern: new RegExp(String.raw `^\s*unexpected\s+status\s+429\b|^(?![\s\S]{0,240}?${CODEX_EXHAUSTED_429_ROW})[ \t]*(?:${CODEX_STATUS_GLYPH}|Error:|API Error:)\s*[\s\S]{0,240}?(?:["'](?:status|code)["']\s*:\s*429\b|\b(?:rate_limit_exceeded|too_many_requests)\b|\brate limit(?:ed| exceeded| reached)?\b|\btoo many requests\b)`, "im"),
2011
- type: "rate_limit",
2012
- action: "failover",
2013
- message: "OpenAI rate limit reached",
2014
- },
2015
- {
2016
- // Same false-positive boundary as rate limits: `authentication` is a
2017
- // normal English/code word. Require a structured 401/code or a
2018
- // decorated CLI error line before pausing every instance on the shared
2019
- // credential.
2020
- pattern: new RegExp(String.raw `^\s*unexpected\s+status\s+401\b|^\s*(?:${CODEX_STATUS_GLYPH}|Error:|API Error:)\s*[\s\S]{0,240}?(?:["'](?:status|code)["']\s*:\s*401\b|\b(?:invalid_api_key|authentication_error)\b|\b401\s+Unauthorized\b|\bauthentication (?:failed|error)\b|\binvalid api key\b)`, "im"),
2021
- type: "auth_error",
2022
- action: "pause",
2023
- message: "OpenAI authentication error",
2024
- },
2025
- // Codex writes the apostrophe as U+2019 ("You’ve hit your usage limit.") since
2026
- // 0.156 and as ASCII before (0.153.4): both must match, or no usage limit
2027
- // ever pauses the instance (#1098).
2028
- { pattern: /you['’]ve hit your usage limit/i, type: "quota", action: "pause", message: "Codex usage limit reached — upgrade plan required",
2029
- notice: () => quotaPausedLine("Codex", "usage") },
2030
- {
2031
- // The terminal stage of a lost app-server (#1099): Codex has given up
2032
- // reconnecting and says so, with a counter that never stops. Nothing
2033
- // recovers from here without a relaunch, and every message sent in the
2034
- // meantime waits for an idle that cannot come. Only the LIVE row counts:
2035
- // directly above the LAST composer with no later transcript item or composer
2036
- // (the same test as codexAppServerDisconnected, as a lookahead), so text that
2037
- // merely quotes it does not raise this.
2038
- pattern: new RegExp(String.raw `^[ \t]*•[ \t]+Reconnect failed[ \t]+—[ \t]+check the endpoint, then relaunch[ \t]+\(\d+[hms](?:[ \t]+\d+[hms])*\)[ \t]*$`
2039
- + String.raw `(?=\n(?:[ \t]*\n)*[›>][^\n]*(?:\n(?![•■⚠›>])[^\n]*)*(?![\s\S]))`, "mu"),
2040
- type: "network",
2041
- action: "notify",
2042
- message: "Codex lost its app-server connection and could not reconnect — restart the instance (messages cannot be delivered until then)",
2043
- },
2044
- {
2045
- // Codex reports an unknown model either as a TUI metadata fallback or
2046
- // as a ChatGPT-account API rejection. Use whitespace-aware phrases so
2047
- // capture-pane hard wraps do not hide either form.
2048
- // Codex quotes the slug with BACKTICKS ("Model metadata for `x` not found.",
2049
- // verified live on 0.160.0 and in the 0.153.4–0.160.0 binaries). The old
2050
- // pattern only took ASCII quotes, so it could never match a real line.
2051
- pattern: /model\s+metadata\s+for\s+['"`‘’“”][^'"`‘’“”]+['"`‘’“”]\s+not\s+found\.\s+defaulting\s+to\s+fallback\s+metadata|model\s+is\s+not\s+supported\s+when\s+using\s+codex\s+with\s+a\s+chatgpt\s+account/i,
2052
- type: "model_error",
2053
- action: "notify",
2054
- message: "Codex model unavailable — use /model to switch",
2055
- },
2056
- {
2057
- // A capacity rejection is a completed failed turn: Codex returns to its
2058
- // prompt without an answer, with its context intact. Keep this anchored to
2059
- // the exact decorated TUI line so ordinary prose about model capacity
2060
- // cannot trigger anything on an otherwise healthy instance. The status
2061
- // glyph is decoration that changes between Codex versions and error classes
2062
- // (`■` U+25A0 on 0.159.2 and 0.160.0, `⚠` U+26A0 before), so any one
2063
- // "Other Symbol" glyph (optionally with its emoji selector) opens the line —
2064
- // never a bullet, dash, quote marker or number, which prose and the model's
2065
- // own transcript items use (`•`, `-`, `>`). The sentence must fill the line,
2066
- // from column 0. Box drawing and block elements (U+2500–U+259F) are
2067
- // excluded up front: they are `So` too, but a left-border popup or
2068
- // table row (`│ …`) is chrome, not a status line. The CLI is
2069
- // already back at the prompt, so skipRecoveryWait avoids an extra wait.
2070
- // action "nudge_continue": tell the user, then about a minute later tell
2071
- // the agent to keep going (the lifecycle bounds this to 3 per 30 minutes,
2072
- // then pauses; see #905). No restart — nothing would re-send the turn — and
2073
- // no model switch. skipCooldown: each NEW capacity line is a new episode,
2074
- // including a second one right after the nudge.
2075
- pattern: /^(?![─-▟])\p{So}\uFE0F? Selected model is at capacity\. Please try a different model\.\r?$/mu,
2076
- type: "model_error",
2077
- action: "nudge_continue",
2078
- message: t("inst.codex_model_capacity"),
2079
- skipCooldown: true,
2080
- skipRecoveryWait: true,
2081
- },
2082
- // Workspace (business) accounts report exhaustion differently from personal
2083
- // ones, with codex's own rows (codex-rs/protocol error.rs and tui
2084
- // turn_runtime.rs, unchanged 0.160.0–0.165.0):
2085
- // member: "■ Your workspace is out of credits. Ask your workspace owner to refill in order to continue."
2086
- // "■ You hit your spend cap set by the owner of your workspace. Ask an owner to increase your spend cap to continue."
2087
- // owner: "■ You're out of credits. Your workspace is out of credits. Add credits to continue using Codex."
2088
- // "■ Your workspace is out of credits. Add credits to continue."
2089
- // "■ You hit your spend cap set in your workspace. Increase your spend cap to continue."
2090
- // "■ Usage limit reached. You've reached your usage limit. Increase your limits to continue using codex."
2091
- // Neither `insufficient_quota|billing` nor `you've hit your usage limit`
2092
- // matches them.
2093
- //
2094
- // pause, not notify: nothing works until someone refills or raises the cap.
2095
- // Anchored to the status glyph in COLUMN 0, because these skip the
2096
- // usage-window check (a workspace limit is not one of those windows, so the
2097
- // check would call it "available"): codex draws its own error cells there,
2098
- // while an agent's message, quote or code block is indented under its `•`
2099
- // (captured live, tests/fixtures/codex-0165-workspace-quoted.pane.txt).
2100
- // `\s+` at each word gap: capture-pane runs without -J, so tmux's hard wrap
2101
- // can land a newline anywhere in the sentence. A resumed transcript reprints
2102
- // these rows, and the first live composer of a spawn takes them as history.
2103
- {
2104
- pattern: new RegExp(String.raw `^(?:${CODEX_STATUS_GLYPH})[ \t]+(?:You['’]re\s+out\s+of\s+credits\.\s+)?Your\s+workspace\s+is\s+out\s+of\s+credits\.`, "m"),
2105
- type: "quota",
2106
- action: "pause",
2107
- message: "Codex workspace credits exhausted — workspace owner must refill",
2108
- notice: () => quotaPausedLine("Codex", "workspace_credits"),
2109
- skipUsageVerify: true,
2110
- historyUntilLiveComposer: true,
2111
- },
2112
- {
2113
- pattern: new RegExp(String.raw `^(?:${CODEX_STATUS_GLYPH})[ \t]+You\s+hit\s+your\s+spend\s+cap\s+set\s+(?:by\s+the\s+owner\s+of|in)\s+your\s+workspace\.`, "m"),
2114
- type: "quota",
2115
- action: "pause",
2116
- message: "Codex workspace spend cap reached — workspace owner must raise it",
2117
- notice: () => t("inst.quota_spend_cap"),
2118
- skipUsageVerify: true,
2119
- historyUntilLiveComposer: true,
2120
- },
2121
- {
2122
- pattern: new RegExp(String.raw `^(?:${CODEX_STATUS_GLYPH})[ \t]+Usage\s+limit\s+reached\.\s+You['’]ve\s+reached\s+your\s+usage\s+limit\.\s+Increase\s+your\s+limits`, "m"),
2123
- type: "quota",
2124
- action: "pause",
2125
- message: "Codex workspace usage limit reached — increase the workspace limits to continue",
2126
- notice: () => quotaPausedLine("Codex", "workspace_usage"),
2127
- skipUsageVerify: true,
2128
- historyUntilLiveComposer: true,
2129
- },
2130
- // Codex warns at 10% and 5% remaining, and scopes the limit by period —
2131
- // "weekly" alone missed every `monthly limit` warning.
2132
- //
2133
- // `\s+` at EVERY word gap (not literal spaces) because capture-pane runs without -J,
2134
- // so tmux's hard wrap can land a newline (plus continuation-line padding)
2135
- // at any of these word gaps. The trailing "Run /status for a breakdown."
2136
- // is deliberately NOT part of the pattern — that tail is what actually
2137
- // wrapped in the reported case.
2138
- //
2139
- // `of your <period> limit` is load-bearing: it's what keeps the pattern
2140
- // off the agent's own prose about percentages.
2141
- {
2142
- pattern: /less\s+than\s+(\d+)\s*%\s+of\s+your\s+(hourly|daily|weekly|monthly)\s+limit/i,
2143
- type: "quota",
2144
- action: "notify",
2145
- message: "Codex usage limit running low",
2146
- formatMessage: (m) => `Codex ${m[2].toLowerCase()} limit: less than ${m[1]}% left`,
2147
- notice: (m) => (m ? quotaLowLine(m[2], m[1]) : quotaLine("Codex", "usage")),
2148
- },
2149
- ];
1811
+ /** #1811: what the last launch left out and why — the Log tab and daemon.log, never a channel. */
1812
+ quietLaunchNotes = [];
1813
+ consumeQuietLaunchNotes() { const notes = this.quietLaunchNotes; this.quietLaunchNotes = []; return notes; }
1814
+ buildCommand(config) {
1815
+ this.lastKnownModel = config.model?.trim() || null;
1816
+ this.credentialProfile = this.readProfile(config);
1817
+ // #2886: `--full-auto` is gone from codex (no supported version, 0.155 on, accepts it: the instance never started);
1818
+ // spelled out, it was a workspace-write sandbox that asks for approval on request.
1819
+ const approvalFlag = config.skipPermissions !== false
1820
+ ? "--dangerously-bypass-approvals-and-sandbox"
1821
+ : "--sandbox workspace-write --ask-for-approval on-request";
1822
+ // #984: never trust `codex resume --last` to pick this instance's session.
1823
+ // Codex 0.157 scopes it to the git REPOSITORY, so in a worktree it resumes
1824
+ // the newest session of any sibling worktree (a lock screen while that
1825
+ // instance lives, a silent hijack otherwise), and a new instance in a
1826
+ // shared repo does not start fresh. Resume the exact-cwd thread from the
1827
+ // shared state DB instead; `--last` is only the fallback for an unreadable
1828
+ // DB when no other Codex instance shares the repository.
1829
+ let cmd;
1830
+ this.launchWarning = null;
1831
+ this.lastLaunchResumes = false;
1832
+ // #2357: read-only (codex is never run here); a launch is not held for it — the operator is told once per launch.
1833
+ this.packageWarning = codexPackageWarning(this.binaryPath);
1834
+ const retainedGeneral = selectedGeneralHostSession(this.instanceDir, config.instanceName, "codex", config.workingDirectory);
1835
+ if (retainedGeneral) {
1836
+ const check = checkResumeId(join(this.isolatedCodexHome, "sessions"), config.workingDirectory, retainedGeneral);
1837
+ if (!check.ok)
1838
+ throw new ResumeBlockedError(t("general_location.start_failed"), t("general_location.start_failed"));
1839
+ this.lastLaunchResumes = true;
1840
+ cmd = `${this.binaryPath} resume ${shellQuote(retainedGeneral)} ${approvalFlag}`;
1841
+ }
1842
+ else if (config.skipResume) {
1843
+ cmd = `${this.binaryPath} ${approvalFlag}`;
1844
+ }
1845
+ else {
1846
+ const resumeBy = (id, why, bytes) => {
1847
+ this.lastLaunchResumes = true;
1848
+ this.writeResumeAttempt(id);
1849
+ const large = bytes !== null && bytes > CODEX_RESUME_LARGE_BYTES ? ` ${t("codex.resume_large", String(Math.round(bytes / (1024 * 1024))))}` : "";
1850
+ this.launchWarning = why + large;
1851
+ return `${this.binaryPath} resume ${shellQuote(id)} ${approvalFlag}`;
1852
+ };
1853
+ const sessionsDir = join(this.isolatedCodexHome, "sessions");
1854
+ // M-2266: the previous launch was told to resume a conversation by id. Codex records a thread as it opens it,
1855
+ // so a resume that did not take (codex exited with "No saved session found…") left no row: this launch starts
1856
+ // fresh, once, and says so. A database that cannot be read proves nothing, and planning goes on as usual.
1857
+ // A new one-shot ask comes first: it is the person's explicit choice for this launch.
1858
+ const oneShot = this.takeResumeNext();
1859
+ const attempt = this.takeResumeAttempt();
1860
+ const failed = !oneShot && attempt !== null && codexThreadRowExists(this.stateDbPath(), attempt) === false;
1861
+ if (failed) {
1862
+ // Remembered, so the auto-resume never picks this conversation again on a later launch.
1863
+ this.rememberFailedResume(attempt);
1864
+ cmd = `${this.binaryPath} ${approvalFlag}`;
1865
+ this.launchWarning = t("codex.resume_failed", config.workingDirectory, attempt);
1866
+ }
1867
+ else if (oneShot) {
1868
+ // M-2266: validated when it was asked for (restart_instance); checked again now, as the rollout may have moved.
1869
+ const check = checkResumeId(sessionsDir, config.workingDirectory, oneShot);
1870
+ if (check.ok)
1871
+ cmd = resumeBy(check.rollout.id, t("codex.resume_oneshot", config.workingDirectory, check.rollout.id), check.rollout.bytes);
1872
+ else {
1873
+ cmd = this.plannedCommand(config, approvalFlag, sessionsDir, resumeBy);
1874
+ this.launchWarning = [t("codex.resume_oneshot_invalid", config.workingDirectory, oneShot, check.reason), this.launchWarning].filter(Boolean).join("\n");
1875
+ }
1876
+ }
1877
+ else {
1878
+ cmd = this.plannedCommand(config, approvalFlag, sessionsDir, resumeBy);
1879
+ }
1880
+ }
1881
+ if (config.model) {
1882
+ const model = validateModel(config.model);
1883
+ warnIfModelMismatch("codex", model);
1884
+ cmd += ` -c ${shellQuote(`model="${model}"`)}`;
1885
+ }
1886
+ if (config.backendOptions?.provider) {
1887
+ const provider = validateProvider(String(config.backendOptions.provider));
1888
+ cmd += ` -c ${shellQuote(`model_provider="${provider}"`)}`;
1889
+ }
1890
+ // #1811: the configured effort, when the catalog entry of the model that will run lists it (launch-effort.ts); else
1891
+ // a quiet note. That model is proven from the command line or the config files codex reads (#1813 review).
1892
+ const runs = resolveCodexLaunchModel({
1893
+ isolatedHome: this.isolatedCodexHome, sharedHome: this.sharedCodexHome,
1894
+ model: config.model, workingDirectory: config.workingDirectory,
1895
+ });
1896
+ const effort = codexEffort(config.effort, runs.proven
1897
+ ? readCodexModelEffortLevels({ isolatedHome: this.isolatedCodexHome, sharedHome: this.sharedCodexHome, model: runs.model })
1898
+ : { model: runs.model, levels: null, unproven: true });
1899
+ if (effort.arg)
1900
+ cmd += effort.arg;
1901
+ this.quietLaunchNotes = effort.note ? [effort.note] : [];
1902
+ // AgEnD instances are unattended processes: an interactive self-update
1903
+ // picker blocks delivery and must never be enabled by a copied global or
1904
+ // managed config layer. Keep this last so the CLI override is authoritative.
1905
+ // Both observed Codex 0.155.0 and 0.156.1 support this launch flag. Pin
1906
+ // the initial layout so a user/global fullscreen preference cannot make a
1907
+ // header-only pane look ready. A later unknown layout still fails closed.
1908
+ cmd += " -c check_for_update_on_startup=false --no-alt-screen";
1909
+ // codex 0.159's opt-in `instant_interrupt` makes new input steer the
1910
+ // running response instead of queueing behind it, which AgEnD's delivery
1911
+ // and queued-input (↳) handling assume. Keep it off whatever a user or
1912
+ // global config says. Codex before 0.159 ignores the key with a startup
1913
+ // warning ("`features.instant_interrupt` is ignored"); nothing else.
1914
+ cmd += " -c features.instant_interrupt=false";
1915
+ // CODEX_HOME is the only Codex-supported way to isolate the complete base
1916
+ // config. A profile only layers over the shared config and would therefore
1917
+ // still load every globally registered AgEnD MCP server.
1918
+ return `CODEX_HOME=${shellQuote(this.isolatedCodexHome)} ${cmd}`;
1919
+ }
1920
+ writeConfig(config) {
1921
+ this.authorizedTrust = null;
1922
+ // Set before the home is prepared: which login this instance gets is a
1923
+ // property of the home, and the home is built here.
1924
+ this.credentialProfile = this.readProfile(config);
1925
+ this.prepareIsolatedHome();
1926
+ this.cleanSharedConfig();
1927
+ // Copy all user settings but no AgEnD MCP entries into a private base
1928
+ // config, then append only this instance's server(s). Each instance writes
1929
+ // a distinct file, eliminating the old concurrent global-config race.
1930
+ let content = "";
1931
+ try {
1932
+ content = stripAgendMcpTables(readFileSync(join(this.sharedCodexHome, "config.toml"), "utf-8"));
1933
+ }
1934
+ catch { /* a first-time Codex user may have no global config */ }
1935
+ if (content && !content.endsWith("\n"))
1936
+ content += "\n";
1937
+ for (const [name, entry] of Object.entries(config.mcpServers)) {
1938
+ content += `\n${renderMcpServer(name, entry, config.instanceName)}`;
1939
+ }
1940
+ atomicWritePrivate(join(this.isolatedCodexHome, "config.toml"), content);
1941
+ this.enableContextStatusLine();
1942
+ this.disableStartupUpdateCheck();
1943
+ this.hideRateLimitModelNudge();
1944
+ // Write fleet instructions into AGENTS.md (additive via marker block)
1945
+ if (config.instructions) {
1946
+ try {
1947
+ const agentsMd = join(config.workingDirectory, "AGENTS.md");
1948
+ appendWithMarker(agentsMd, config.instanceName, config.instructions);
1949
+ // Warn if file exceeds Codex's project_doc_max_bytes limit
1950
+ try {
1951
+ const size = statSync(agentsMd).size;
1952
+ if (size > CODEX_PROJECT_DOC_MAX_BYTES) {
1953
+ console.warn(`[agend] AGENTS.md is ${size} bytes, exceeds Codex limit of ${CODEX_PROJECT_DOC_MAX_BYTES} — instructions may be truncated`);
1954
+ }
1955
+ }
1956
+ catch { /* stat failed — skip size check */ }
1957
+ }
1958
+ catch { /* best effort */ }
1959
+ }
1960
+ }
1961
+ /**
1962
+ * Stop Codex opening its "Update available!" picker when an instance starts.
1963
+ *
1964
+ * That dialog owns the input loop until someone answers it, so after a fleet
1965
+ * restart every Codex instance sat waiting on a keypress nobody was going to
1966
+ * send. AgEnD then reported it as a stuck pane, which is true but unhelpful.
1967
+ *
1968
+ * `check_for_update_on_startup = false` is Codex's own config key (verified in
1969
+ * the binary), so this prevents the prompt rather than racing to dismiss it.
1970
+ * There is no CLI flag for it — `codex update` is a subcommand, not a switch.
1971
+ *
1972
+ * Placement matters: this is a TOP-LEVEL key, so it has to go above the first
1973
+ * `[section]` header or TOML would read it as belonging to that section. And a
1974
+ * AgEnD's isolated home is unattended, so an inherited `true` is replaced
1975
+ * rather than preserved. TOML rejects duplicate keys, so replace in place.
1976
+ */
1977
+ disableStartupUpdateCheck() {
1978
+ const configPath = join(this.isolatedCodexHome, "config.toml");
1979
+ let content = "";
1980
+ try {
1981
+ content = readFileSync(configPath, "utf-8");
1982
+ }
1983
+ catch {
1984
+ return;
1985
+ }
1986
+ const existing = /^\s*check_for_update_on_startup\s*=\s*(?:true|false)\s*(?:#.*)?$/m;
1987
+ if (existing.test(content)) {
1988
+ const updated = content.replace(existing, "check_for_update_on_startup = false");
1989
+ if (updated !== content) {
1990
+ try {
1991
+ atomicWritePrivate(configPath, updated);
1992
+ }
1993
+ catch { /* best effort */ }
1994
+ }
1995
+ return;
1996
+ }
1997
+ const LINE = "check_for_update_on_startup = false\n";
1998
+ const firstSection = content.search(/^\s*\[/m);
1999
+ const updated = firstSection === -1
2000
+ ? (content.length && !content.endsWith("\n") ? `${content}\n${LINE}` : `${content}${LINE}`)
2001
+ : `${content.slice(0, firstSection)}${LINE}${content.slice(firstSection)}`;
2002
+ try {
2003
+ atomicWritePrivate(configPath, updated);
2004
+ }
2005
+ catch { /* best effort */ }
2006
+ }
2007
+ /**
2008
+ * Suppress Codex's rate-limit model-switch reminder for unattended instances.
2009
+ * Codex documents this as `notice.hide_rate_limit_model_nudge` (a boolean in
2010
+ * the `[notice]` table). This changes only the per-instance config; the
2011
+ * runtime holdOnly dialog remains as defense-in-depth for older or changed
2012
+ * CLI behavior. No model option is selected automatically.
2013
+ */
2014
+ hideRateLimitModelNudge() {
2015
+ const configPath = join(this.isolatedCodexHome, "config.toml");
2016
+ let content = "";
2017
+ try {
2018
+ content = readFileSync(configPath, "utf-8");
2019
+ }
2020
+ catch {
2021
+ return;
2022
+ }
2023
+ const updated = setTomlTableBoolean(content, "notice", "hide_rate_limit_model_nudge");
2024
+ if (updated !== content) {
2025
+ try {
2026
+ atomicWritePrivate(configPath, updated);
2027
+ }
2028
+ catch { /* best effort */ }
2029
+ }
2030
+ }
2031
+ /**
2032
+ * Ensure Codex's TUI status line shows context usage: the Context item is
2033
+ * both what /ctx scrapes and the footer AgEnD's readiness proofs rely on.
2034
+ * Rules (never drops or reorders the user's items):
2035
+ * 1. the effective `tui.status_line` already has a context item
2036
+ * (context-remaining / -usage / -used) → leave the config untouched.
2037
+ * 2. no context item:
2038
+ * - no status_line at all → write status_line = ["context-remaining"]
2039
+ * - status_line exists → prepend "context-remaining" to it
2040
+ * Best-effort string edit (no TOML writer), but every candidate edit is
2041
+ * parsed back and kept only if the effective `tui.status_line` becomes
2042
+ * `["context-remaining", ...the user's items]`. A `status_line` key in some
2043
+ * other table is not the TUI's and must not be the one edited (#931). If no
2044
+ * edit can be verified, the launch warns the operator instead of leaving the
2045
+ * pane silently unready.
2046
+ */
2047
+ enableContextStatusLine() {
2048
+ this.configuredStatusLineItems = undefined;
2049
+ this.configuredStatusLinePattern = undefined;
2050
+ this.statusLineWarning = null;
2051
+ const configPath = join(this.isolatedCodexHome, "config.toml");
2052
+ // The private copy is rebuilt from the shared one at every launch, so the
2053
+ // operator must be pointed at the file their status_line actually lives in.
2054
+ const sharedConfigPath = join(this.sharedCodexHome, "config.toml");
2055
+ let content = "";
2056
+ try {
2057
+ content = readFileSync(configPath, "utf-8");
2058
+ }
2059
+ catch { /* no file yet */ }
2060
+ const ITEM = "context-remaining";
2061
+ const current = effectiveTuiStatusLine(content);
2062
+ if (current === "invalid") {
2063
+ this.statusLineWarning = t("codex.status_line_unverifiable", "tui.status_line is not a list of item names, or config.toml does not parse", sharedConfigPath);
2064
+ return;
2065
+ }
2066
+ // Rule 1: the TUI already shows a context item → don't touch anything.
2067
+ if (current?.some(item => CODEX_CONTEXT_STATUS_ITEM_RE.test(item)))
2068
+ return;
2069
+ // TOML allows quoted keys: `"status_line" = [...]`, `"tui"."status_line"`,
2070
+ // `['tui']`. Every textual candidate is tried; parsing decides.
2071
+ const candidates = [];
2072
+ if (current) {
2073
+ // Rule 2b: prepend our item to the user's existing array (don't overwrite).
2074
+ // First position keeps "Context N% left" at the far left of the footer so a
2075
+ // long cwd/other items can't push it past 80 cols and truncate it.
2076
+ for (const m of content.matchAll(/["']?status_line["']?\s*=\s*\[([^\]]*)\]/g)) {
2077
+ const inner = m[1].trim().replace(/^,\s*/, "").replace(/,\s*$/, "");
2078
+ const newInner = inner.length ? `"${ITEM}", ${inner}` : `"${ITEM}"`;
2079
+ candidates.push(`${content.slice(0, m.index)}status_line = [${newInner}]${content.slice(m.index + m[0].length)}`);
2080
+ }
2081
+ }
2082
+ else {
2083
+ // Rule 2a: no status_line at all → add a minimal one.
2084
+ const base = content.length && !content.endsWith("\n") ? `${content}\n` : content;
2085
+ // Also recognise quoted section headers: `["tui"]` and `['tui']`.
2086
+ if (/^\[["']?tui["']?\][^\n]*\n/m.test(base)) {
2087
+ candidates.push(base.replace(/^\[["']?tui["']?\][^\n]*\n/m, h => `${h}status_line = ["${ITEM}"]\n`));
2088
+ }
2089
+ candidates.push(`${base}\n[tui]\nstatus_line = ["${ITEM}"]\n`);
2090
+ // A config that defines tui with dotted keys cannot take a [tui] table.
2091
+ candidates.push(`tui.status_line = ["${ITEM}"]\n${base}`);
2092
+ // Nor can an inline `tui = { … }` table: the item goes inside it.
2093
+ candidates.push(base.replace(/^([ \t]*["']?tui["']?[ \t]*=[ \t]*\{)[ \t]*\}/m, `$1 status_line = ["${ITEM}"] }`));
2094
+ candidates.push(base.replace(/^([ \t]*["']?tui["']?[ \t]*=[ \t]*\{)/m, `$1 status_line = ["${ITEM}"],`));
2095
+ }
2096
+ const wanted = [ITEM, ...(current ?? [])];
2097
+ const fixed = candidates.find(candidate => {
2098
+ const items = effectiveTuiStatusLine(candidate);
2099
+ return Array.isArray(items) && items.length === wanted.length && items.every((item, i) => item === wanted[i]);
2100
+ });
2101
+ if (!fixed) {
2102
+ this.statusLineWarning = t("codex.status_line_unverifiable", "no edit of tui.status_line could be verified", sharedConfigPath);
2103
+ return;
2104
+ }
2105
+ try {
2106
+ atomicWritePrivate(configPath, fixed);
2107
+ }
2108
+ catch (err) {
2109
+ // Never block launch on statusline config — but a verified edit that
2110
+ // could not be written leaves the pane as unready as an unfixable one.
2111
+ const reason = `the verified edit could not be written: ${err.code ?? "write failed"}`;
2112
+ this.statusLineWarning = t("codex.status_line_unverifiable", reason, sharedConfigPath);
2113
+ }
2114
+ }
2115
+ /** Null when the instance did not ask for a profile — today's behaviour. */
2116
+ readProfile(config) {
2117
+ try {
2118
+ return resolveCredentialProfile(config.backendOptions);
2119
+ }
2120
+ catch {
2121
+ // A malformed name is refused where it is written; launching is not the
2122
+ // place to discover it, and falling back to the shared login is the
2123
+ // behaviour every instance had before profiles existed.
2124
+ return null;
2125
+ }
2150
2126
  }
2151
- getStartupDialogs() {
2152
- const trustHold = this.trustHoldDialog();
2153
- return [
2154
- this.autoTrustDialog(),
2155
- trustHold,
2156
- this.updatePickerDialog(),
2157
- this.rateSwitchPickerDialog(),
2158
- ...this.sessionHoldDialogs(),
2159
- this.unknownSelectionHoldDialog(),
2160
- ];
2127
+ /**
2128
+ * Where this instance's `auth.json` comes from.
2129
+ *
2130
+ * The shared home unless a profile says otherwise. A profile owns the file
2131
+ * itself — not a copy, not a link — so codex refreshing the token writes
2132
+ * into that subscription and no other.
2133
+ */
2134
+ authSourceDir() {
2135
+ if (!this.credentialProfile)
2136
+ return this.sharedCodexHome;
2137
+ const spec = credentialHomeSpec(this.binaryName);
2138
+ if (!spec)
2139
+ return this.sharedCodexHome;
2140
+ const home = credentialProfileHome(getAgendHome(), this.binaryName, this.credentialProfile);
2141
+ prepareCredentialProfileHome(spec, home);
2142
+ return home;
2143
+ }
2144
+ preTrust(workDir) {
2145
+ this.authorizedTrust = null;
2146
+ const paths = codexTrustPaths(workDir);
2147
+ const configPath = join(this.isolatedCodexHome, "config.toml");
2148
+ let content = "";
2149
+ try {
2150
+ content = readFileSync(configPath, "utf-8");
2151
+ }
2152
+ catch { }
2153
+ const updated = setProjectTrusted(content, paths.root);
2154
+ if (updated !== content)
2155
+ atomicWritePrivate(configPath, updated);
2156
+ // Do not authorize an automatic Enter merely because the write returned:
2157
+ // a stale/untrusted section in the effective isolated config must fail shut.
2158
+ const onDisk = readFileSync(configPath, "utf-8");
2159
+ if (effectiveProjectTrust(onDisk, paths.root) !== "trusted")
2160
+ throw new Error("Codex project trust did not persist");
2161
+ this.authorizedTrust = paths;
2161
2162
  }
2162
2163
  /**
2163
- * #984: session-selection screens only a human may answer. Held (never
2164
- * keyed) so the delivery gate keeps messages queued and dialog_parked tells
2165
- * the operator; listed before the generic selection hold for a clear notice.
2164
+ * Preserve Codex login/session/cache sharing while isolating config.toml
2165
+ * and the CODEX_HOME-scoped app-server daemon/control runtime directories.
2166
2166
  */
2167
- sessionHoldDialogs() {
2168
- return [
2169
- {
2170
- pattern: /This conversation is open in another app/,
2171
- keys: [],
2172
- description: "Codex session is open in another process (thread lock) — close the other one and press r, or fork manually",
2173
- holdOnly: true,
2174
- blocksDelivery: true,
2175
- inputBlocked: true,
2176
- remoteCard: codexSelectionCard,
2177
- isActive: codexSessionLockVisible,
2178
- },
2179
- {
2180
- pattern: /Working directory · resume/,
2181
- keys: [],
2182
- description: "Codex resume selected a session from another directory (sibling git worktree) — needs a human choice",
2183
- holdOnly: true,
2184
- blocksDelivery: true,
2185
- inputBlocked: true,
2186
- remoteCard: codexSelectionCard,
2187
- isActive: codexResumeCwdPickerVisible,
2188
- },
2189
- ];
2167
+ /** Session dirs every instance must share with the terminal CLI (#506). */
2168
+ static SHARED_SESSION_DIRS = ["sessions", "archived_sessions"];
2169
+ static PRIVATE_RUNTIME_DIRS = new Set(["app-server-daemon", "app-server-control"]);
2170
+ prepareIsolatedHome() {
2171
+ mkdirSync(this.isolatedCodexHome, { recursive: true, mode: 0o700 });
2172
+ chmodSync(this.isolatedCodexHome, 0o700);
2173
+ if (this.sharedCodexHome === this.isolatedCodexHome)
2174
+ return;
2175
+ // #1034: Codex's private socket directory check rejects directory symlinks.
2176
+ // Detach only the exact shared-home links the old mirror pass created,
2177
+ // including dangling links. Leave private directories and other links
2178
+ // alone; Codex will create its own real runtime directories on launch.
2179
+ for (const name of CodexBackend.PRIVATE_RUNTIME_DIRS) {
2180
+ const target = join(this.isolatedCodexHome, name);
2181
+ try {
2182
+ if (lstatSync(target).isSymbolicLink()
2183
+ && readlinkSync(target) === join(this.sharedCodexHome, name)) {
2184
+ unlinkSync(target);
2185
+ }
2186
+ }
2187
+ catch { /* absent, or another startup already detached the link */ }
2188
+ }
2189
+ // The session dirs must exist in the SHARED home before the symlink pass:
2190
+ // on a fresh install they don't yet, so no link was created, and the first
2191
+ // archive made Codex create a REAL dir in the instance-private home — a
2192
+ // permanent fork the terminal CLI could never see (#506). mkdir -p is
2193
+ // EEXIST-safe under concurrent instance startup.
2194
+ for (const dir of CodexBackend.SHARED_SESSION_DIRS) {
2195
+ try {
2196
+ mkdirSync(join(this.sharedCodexHome, dir), { recursive: true, mode: 0o700 });
2197
+ }
2198
+ catch { /* best effort */ }
2199
+ }
2200
+ // Heal homes that already forked: merge the private real dir back into the
2201
+ // shared one (never overwriting), then replace it with the symlink.
2202
+ for (const dir of CodexBackend.SHARED_SESSION_DIRS) {
2203
+ this.migrateDivergedSessionDir(dir);
2204
+ }
2205
+ // Older versions linked SQLite's WAL/SHM files independently of their
2206
+ // base database. If the base file had already been created privately,
2207
+ // that split one SQLite database across two homes and caused CANTOPEN (or
2208
+ // worse, cross-database journal recovery). Remove only links AgEnD made to
2209
+ // the matching shared-home path; real private sidecars remain untouched.
2210
+ const healedSidecars = [];
2211
+ for (const name of readdirSync(this.isolatedCodexHome)) {
2212
+ if (!SQLITE_SIDECAR_RE.test(name))
2213
+ continue;
2214
+ const target = join(this.isolatedCodexHome, name);
2215
+ try {
2216
+ if (!lstatSync(target).isSymbolicLink())
2217
+ continue;
2218
+ const linkTarget = resolve(dirname(target), readlinkSync(target));
2219
+ if (linkTarget !== join(this.sharedCodexHome, name))
2220
+ continue;
2221
+ unlinkSync(target);
2222
+ healedSidecars.push(name);
2223
+ }
2224
+ catch {
2225
+ // A concurrent process may remove an ephemeral sidecar/link first.
2226
+ }
2227
+ }
2228
+ if (healedSidecars.length > 0) {
2229
+ console.warn(`[agend] removed unsafe Codex SQLite sidecar links: ${healedSidecars.join(", ")}`);
2230
+ }
2231
+ // One file, possibly from somewhere else. Everything below is unchanged:
2232
+ // sessions, the thread/state/memory databases and every cache still come
2233
+ // from the shared home, which is why switching subscription does not throw
2234
+ // the conversation away.
2235
+ this.linkAuthFile();
2236
+ for (const name of readdirSync(this.sharedCodexHome)) {
2237
+ if (CodexBackend.PRIVATE_RUNTIME_DIRS.has(name))
2238
+ continue;
2239
+ if (name === "config.toml" || name === AGEND_MCP_CLEANUP_LOCK || name.startsWith(".config.toml."))
2240
+ continue;
2241
+ if (name === CODEX_AUTH_FILE)
2242
+ continue; // handled above, possibly from a profile
2243
+ // SQLite resolves a symlinked base DB to the shared path and creates its
2244
+ // own adjacent sidecars there. Linking sidecars separately is redundant
2245
+ // for shared bases and corrupts the file set for private bases.
2246
+ if (SQLITE_SIDECAR_RE.test(name))
2247
+ continue;
2248
+ const source = join(this.sharedCodexHome, name);
2249
+ const target = join(this.isolatedCodexHome, name);
2250
+ if (existsSync(target))
2251
+ continue;
2252
+ try {
2253
+ const type = lstatSync(source).isDirectory() ? "dir" : "file";
2254
+ symlinkSync(source, target, type);
2255
+ }
2256
+ catch {
2257
+ // State/cache links are compatibility aids; config isolation must not
2258
+ // fail merely because a concurrently-created cache entry disappeared.
2259
+ }
2260
+ }
2261
+ }
2262
+ /**
2263
+ * Point this home's `auth.json` at whichever login it should use.
2264
+ *
2265
+ * Codex refreshes the token by writing through the link rather than replacing
2266
+ * it — observed across every instance home on a machine that has been running
2267
+ * this arrangement for months, with the shared file's mtime moving. That is
2268
+ * behaviour, though, not a contract: a release that starts renaming over the
2269
+ * file would leave a real file here and quietly fork the login, with refreshes
2270
+ * landing somewhere the profile never sees. So a real file where a link
2271
+ * belongs is treated as exactly that — reported, and put back.
2272
+ */
2273
+ linkAuthFile() {
2274
+ const source = join(this.authSourceDir(), CODEX_AUTH_FILE);
2275
+ const target = join(this.isolatedCodexHome, CODEX_AUTH_FILE);
2276
+ let existing = null;
2277
+ try {
2278
+ existing = lstatSync(target);
2279
+ }
2280
+ catch { /* absent */ }
2281
+ if (existing?.isSymbolicLink()) {
2282
+ let current = "";
2283
+ try {
2284
+ current = resolve(dirname(target), readlinkSync(target));
2285
+ }
2286
+ catch { /* unreadable */ }
2287
+ if (current === source)
2288
+ return;
2289
+ try {
2290
+ unlinkSync(target);
2291
+ }
2292
+ catch { /* raced */ }
2293
+ }
2294
+ else if (existing) {
2295
+ // Not a link: codex replaced it, or an older AgEnD copied it. Either way
2296
+ // the refreshes it has been collecting are stranded in this instance's
2297
+ // home, so say so rather than deleting them silently.
2298
+ const stranded = `${target}.replaced-${Date.now()}`;
2299
+ try {
2300
+ renameSync(target, stranded);
2301
+ console.warn(`[agend] codex replaced ${CODEX_AUTH_FILE} in ${this.isolatedCodexHome} with a real file — `
2302
+ + `its login was no longer shared. Kept a copy at ${stranded} and re-linked to ${source}.`);
2303
+ }
2304
+ catch {
2305
+ return; // cannot move it; leave the instance working on what it has
2306
+ }
2307
+ }
2308
+ if (!existsSync(source))
2309
+ return; // not logged in yet; codex will create it
2310
+ try {
2311
+ symlinkSync(source, target, "file");
2312
+ }
2313
+ catch {
2314
+ // EEXIST from a concurrent start is the ordinary case and means the link
2315
+ // is already there.
2316
+ }
2317
+ }
2318
+ /**
2319
+ * One-time heal for an instance whose isolated home grew a REAL session dir
2320
+ * (#506): merge it into the shared home without overwriting anything, and
2321
+ * only after both the copy and the removal succeed put the symlink in its
2322
+ * place. Any failure keeps the private dir — sessions are never the thing
2323
+ * sacrificed for tidiness — and logs what happened.
2324
+ */
2325
+ migrateDivergedSessionDir(name) {
2326
+ const target = join(this.isolatedCodexHome, name);
2327
+ let st;
2328
+ try {
2329
+ st = lstatSync(target);
2330
+ }
2331
+ catch {
2332
+ return; // absent — the symlink pass below will create the link
2333
+ }
2334
+ if (st.isSymbolicLink())
2335
+ return; // already correct
2336
+ if (!st.isDirectory()) {
2337
+ console.warn(`[agend] codex-home/${name} is neither a symlink nor a directory — leaving it untouched`);
2338
+ return;
2339
+ }
2340
+ const shared = join(this.sharedCodexHome, name);
2341
+ try {
2342
+ mkdirSync(shared, { recursive: true, mode: 0o700 });
2343
+ // cp -rn semantics: collisions keep the shared copy. Session rollout
2344
+ // filenames are timestamp+uuid, so a genuine collision means the same
2345
+ // file — skipping is lossless either way.
2346
+ cpSync(target, shared, { recursive: true, force: false, errorOnExist: false });
2347
+ rmSync(target, { recursive: true });
2348
+ symlinkSync(shared, target, "dir");
2349
+ console.warn(`[agend] migrated diverged codex ${name} into shared home: ${shared}`);
2350
+ }
2351
+ catch (err) {
2352
+ // Keep whatever is left of the private dir. The copy never overwrites, so
2353
+ // a retry on next start is safe; worst case is duplicated files in the
2354
+ // shared home, never lost ones.
2355
+ console.warn(`[agend] codex ${name} migration failed — keeping the instance-private dir (${err.message})`);
2356
+ }
2190
2357
  }
2191
2358
  /**
2192
- * The user's decision (hmsg-53600a2c): codex's folder-trust prompt in an AgEnD session is always answered Trust — the
2193
- * workspace is the agent's own, whatever preTrust covered (preTrust stays, so the prompt rarely shows at all). Enter,
2194
- * only with the cursor on "Trust and continue" among the known options (trustCursor); any other layout or row is
2195
- * held (trustHoldDialog). It trusts the folder only: per-action approvals are separate dialogs and are not touched.
2359
+ * One-time migration for installations polluted by the old global `codex
2360
+ * mcp add` path. The lock protects separate fleet processes/upgrades, and
2361
+ * atomic rename prevents readers from observing a truncated config.
2196
2362
  */
2197
- autoTrustDialog() {
2198
- return {
2199
- pattern: /^\s*Trust this folder\?/m,
2200
- keys: ["Enter"],
2201
- description: "Codex folder trust — trusting the agent's own folder",
2202
- blocksDelivery: true,
2203
- inputBlocked: true,
2204
- autoResolutionKey: "codex-folder-auto-trust",
2205
- // One Enter per prompt (claimed, then checked): a prompt still up after it is held, never pressed again (#2896 review).
2206
- verifyAfterKeys: true,
2207
- isActive: pane => codexTrustPromptState(pane).trustCursor,
2208
- answerDetail: pane => ({ autoTrusted: true, ...this.trustPaneFields(pane) }),
2363
+ cleanSharedConfig() {
2364
+ if (this.sharedCodexHome === this.isolatedCodexHome)
2365
+ return;
2366
+ mkdirSync(this.sharedCodexHome, { recursive: true, mode: 0o700 });
2367
+ const lockPath = join(this.sharedCodexHome, AGEND_MCP_CLEANUP_LOCK);
2368
+ let lockFd;
2369
+ const acquire = () => {
2370
+ try {
2371
+ lockFd = openSync(lockPath, "wx", 0o600);
2372
+ return true;
2373
+ }
2374
+ catch {
2375
+ return false;
2376
+ }
2209
2377
  };
2378
+ if (!acquire()) {
2379
+ try {
2380
+ if (Date.now() - statSync(lockPath).mtimeMs > AGEND_MCP_CLEANUP_LOCK_STALE_MS) {
2381
+ unlinkSync(lockPath);
2382
+ if (!acquire())
2383
+ return;
2384
+ }
2385
+ else {
2386
+ return;
2387
+ }
2388
+ }
2389
+ catch {
2390
+ return;
2391
+ }
2392
+ }
2393
+ try {
2394
+ const configPath = join(this.sharedCodexHome, "config.toml");
2395
+ let content;
2396
+ try {
2397
+ content = readFileSync(configPath, "utf-8");
2398
+ }
2399
+ catch {
2400
+ return;
2401
+ }
2402
+ const cleaned = stripAgendMcpTables(content);
2403
+ if (cleaned !== content)
2404
+ atomicWritePrivate(configPath, cleaned);
2405
+ }
2406
+ finally {
2407
+ if (lockFd !== undefined) {
2408
+ try {
2409
+ closeSync(lockFd);
2410
+ }
2411
+ catch { }
2412
+ }
2413
+ try {
2414
+ unlinkSync(lockPath);
2415
+ }
2416
+ catch { }
2417
+ }
2210
2418
  }
2211
- trustHoldDialog() {
2212
- return {
2213
- pattern: /^\s*(?:Folder access|Trust this folder\?|Do you trust the files in this folder\?)/im,
2214
- keys: [],
2215
- description: "Codex folder trust needs human confirmation",
2216
- holdOnly: true,
2217
- blocksDelivery: true,
2218
- inputBlocked: true,
2219
- isActive: codexTrustVariantActive,
2220
- remoteCard: codexSelectionCard,
2221
- heldReason: pane => this.trustHoldReason(pane),
2222
- };
2419
+ readConfiguredStatusLineItems() {
2420
+ if (this.configuredStatusLineItems !== undefined)
2421
+ return this.configuredStatusLineItems;
2422
+ try {
2423
+ const config = tomlTable(parseToml(readFileSync(join(this.isolatedCodexHome, "config.toml"), "utf-8")));
2424
+ const tui = tomlTable(config?.tui);
2425
+ const items = tui?.status_line;
2426
+ this.configuredStatusLineItems = Array.isArray(items) && items.every(item => typeof item === "string")
2427
+ ? items
2428
+ : null;
2429
+ }
2430
+ catch {
2431
+ this.configuredStatusLineItems = null;
2432
+ }
2433
+ return this.configuredStatusLineItems;
2223
2434
  }
2224
2435
  /**
2225
2436
  * #2893: why the authorized folder-trust answer did not take this prompt (log only): what the pane shows against what
@@ -2254,147 +2465,6 @@ export class CodexBackend {
2254
2465
  authorizedCwd: authorized?.cwd ?? null, authorizedRoot: authorized?.root ?? null,
2255
2466
  configPath: join(this.isolatedCodexHome, "config.toml") };
2256
2467
  }
2257
- /**
2258
- * #1008: the rate-limit model-switch picker parked unattended instances (it
2259
- * waits forever and its default option changes the model). Prevention is
2260
- * `notice.hide_rate_limit_model_nudge` (hideRateLimitModelNudge — real and
2261
- * effective on 0.157.1 and 0.159.2, verified live); this is the answer for a
2262
- * picker that appears anyway (config not applied, a CLI that moved the key).
2263
- *
2264
- * Escape, not a navigated Enter: it selects nothing, so it can never take
2265
- * option 1 (switch model) or option 3 (writes "never show again" into the
2266
- * config). Deliberately not one-shot: if the picker swallowed the key it is
2267
- * simply sent again on the next poll, and a repeated Escape is harmless.
2268
- */
2269
- rateSwitchPickerDialog() {
2270
- return {
2271
- // Documentation only: Daemon.dialogMatches uses isActive INSTEAD of this
2272
- // pattern, so it filters nothing here — the structural check below is the
2273
- // whole decision, and ordinary prose about the picker fails it. The words
2274
- // are the picker's own, kept so a reader can find the screen from the log.
2275
- pattern: /Keep current model \(never show again\)/,
2276
- keys: ["Escape"],
2277
- description: "Codex rate-limit model-switch picker — Escape keeps the current model",
2278
- blocksDelivery: true,
2279
- inputBlocked: true,
2280
- isActive: codexRateSwitchPickerVisible,
2281
- };
2282
- }
2283
- updatePickerDialog() {
2284
- return {
2285
- // Defense in depth for config written by older AgEnD versions or a Codex
2286
- // regression. Match the release URL as well as the banner so ordinary
2287
- // agent prose cannot trigger a keypress.
2288
- pattern: /Update available![\s\S]{0,200}Release notes: https:\/\/github\.com\/openai\/codex\/releases/m,
2289
- keys: ["Escape"],
2290
- description: "Codex startup update-available picker",
2291
- // A delivery must never land on this screen. Its selected option is
2292
- // `› 1. Update now (runs … curl … | sh)` — the same `›` the input row
2293
- // uses — so an Enter here runs an installer instead of sending a message.
2294
- // The runtime dismisser presses Escape, but it only polls every few
2295
- // seconds and a delivery can arrive first (hit live on codex-cli 0.153.4,
2296
- // which parks on this picker for as long as nobody answers it).
2297
- blocksDelivery: true,
2298
- // Daemon.dialogMatches uses isActive INSTEAD OF pattern when present.
2299
- // Check the complete current picker here; a different Enter-only menu
2300
- // must never receive this automatic Escape key.
2301
- isActive: codexUpdatePickerVisible,
2302
- };
2303
- }
2304
- usageLimitLunaReserveDialog() {
2305
- // The real codex usage-limit dialog states codex has ALREADY switched to
2306
- // Luna Reserve and labels Escape as "continue working". Pressing Escape via
2307
- // sendSpecialKey is the ONLY safe key:
2308
- //
2309
- // • Escape is sent via sendSpecialKey — never via pasteText (which adds
2310
- // an implicit bracketed-paste Enter that could confirm the highlighted
2311
- // server action: Upgrade, Reset usage, Add Credits, …).
2312
- // • A digit selects its row at once, and pasted its implicit Enter would
2313
- // confirm whatever it landed on. This path is explicitly NOT used.
2314
- //
2315
- // verifyAfterKeys: true — confirm the menu is gone after Escape.
2316
- // No confirmBeforeEnter / keysAfterConfirm: Escape needs no confirm gate.
2317
- return {
2318
- pattern: /Press enter to confirm or esc to continue working/i,
2319
- keys: ["Escape"],
2320
- description: "Codex usage limit — pressing Escape to continue with Luna Reserve",
2321
- blocksDelivery: true,
2322
- inputBlocked: true,
2323
- isActive: codexUsageLimitMenuVisible,
2324
- verifyAfterKeys: true,
2325
- autoResolutionKey: "codex-usage-limit-luna-reserve",
2326
- postDismissNotice: {
2327
- text: t("inst.codex_usage_limit_luna_reserve", ""), // instance name injected by daemon
2328
- label: "codex-usage-limit-selected",
2329
- },
2330
- };
2331
- }
2332
- workspaceLimitPromptDialog() {
2333
- // Escape via sendSpecialKey, the same as the Luna Reserve menu: never a paste
2334
- // (its implicit Enter would confirm the cursor's row) and never "y". The error
2335
- // row codex printed above the prompt stays, and the quota pause takes it from
2336
- // there: one notice, no parked dialog.
2337
- return {
2338
- pattern: /^\s*enter select\s*[·•]\s*esc back\s*$/im,
2339
- keys: ["Escape"],
2340
- description: "Codex workspace limit prompt — pressing Escape (never asking the owner)",
2341
- blocksDelivery: true,
2342
- inputBlocked: true,
2343
- isActive: codexWorkspaceLimitPromptVisible,
2344
- verifyAfterKeys: true,
2345
- autoResolutionKey: "codex-workspace-limit-prompt",
2346
- answeredByAgend: true,
2347
- marksLiveIncident: true,
2348
- };
2349
- }
2350
- unknownSelectionHoldDialog() {
2351
- return {
2352
- pattern: /^\s*[›❯>]\s+\S/m,
2353
- keys: [],
2354
- description: "Codex interactive selection needs human input",
2355
- holdOnly: true,
2356
- blocksDelivery: true,
2357
- inputBlocked: true,
2358
- remoteCard: codexSelectionCard,
2359
- isActive: codexUnknownSelectionVisible,
2360
- };
2361
- }
2362
- getRuntimeDialogs() {
2363
- return [
2364
- this.autoTrustDialog(),
2365
- this.trustHoldDialog(),
2366
- this.rateSwitchPickerDialog(),
2367
- {
2368
- // Codex 0.156 may change the wording/order of this credit-cost choice.
2369
- // Never navigate it by position: a moved option could switch to a
2370
- // more expensive model. A live picker holds delivery and notifies a
2371
- // human; old transcript mentions are excluded by the tail matcher.
2372
- pattern: /(?:Approaching rate limits|Switch to [^\r\n]{1,120} for lower credit usage\?)/i,
2373
- keys: [],
2374
- description: "Codex rate limit model switch dialog needs human choice",
2375
- holdOnly: true,
2376
- blocksDelivery: true,
2377
- inputBlocked: true,
2378
- remoteCard: codexSelectionCard,
2379
- isActive: codexRateSwitchVisible,
2380
- },
2381
- this.updatePickerDialog(),
2382
- this.usageLimitLunaReserveDialog(),
2383
- this.workspaceLimitPromptDialog(),
2384
- ...this.sessionHoldDialogs(),
2385
- this.unknownSelectionHoldDialog(),
2386
- ];
2387
- }
2388
- getInputUnavailableTransients() {
2389
- return [{
2390
- pattern: /(?:^|\n)\s*Resuming session…\s*(?:\n|$)/,
2391
- description: "Codex session resume in progress",
2392
- isActive: (pane) => {
2393
- const rows = pane.split(/\r?\n/);
2394
- return codexBoxedResumeLoading(rows) || codexUnboxedResumeLoading(rows);
2395
- },
2396
- }];
2397
- }
2398
2468
  getContextUsage() {
2399
2469
  return null;
2400
2470
  }
@@ -2507,10 +2577,6 @@ export class CodexBackend {
2507
2577
  return peers ? codexSiblingState(config.workingDirectory, peers) : "siblings";
2508
2578
  });
2509
2579
  }
2510
- /** Only a resume paints "Resuming session…"; a fresh launch never does. */
2511
- launchMayShowInputTransient() {
2512
- return this.lastLaunchResumes;
2513
- }
2514
2580
  consumeLaunchWarning() {
2515
2581
  const warnings = [this.packageWarning, this.statusLineWarning, this.launchWarning].filter((w) => !!w);
2516
2582
  this.packageWarning = null;
@@ -2518,10 +2584,6 @@ export class CodexBackend {
2518
2584
  this.launchWarning = null;
2519
2585
  return warnings.length ? warnings.join("\n") : null;
2520
2586
  }
2521
- getQuitCommand() { return "/quit"; }
2522
- getCompactCommand() { return "/compact"; }
2523
- getClearCommand() { return "/clear"; }
2524
- getCancelKey() { return "Escape"; }
2525
2587
  // Codex has no `/effort`; reasoning effort is the `model_reasoning_effort`
2526
2588
  // config key (settable per launch with `-c`). The TUI reads it at startup, so
2527
2589
  // a change needs a respawn — restart, not runtime.