@songsid/agend 2.3.5-alpha.4 → 2.3.5-beta.1

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