@songsid/agend 2.3.3-beta.1 → 2.3.3-beta.10

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 (355) hide show
  1. package/dist/agent-cli.js +13 -112
  2. package/dist/agent-cli.js.map +1 -1
  3. package/dist/agent-endpoint.d.ts +0 -1
  4. package/dist/agent-endpoint.js +1 -9
  5. package/dist/agent-endpoint.js.map +1 -1
  6. package/dist/agent-whats-new.d.ts +3 -6
  7. package/dist/agent-whats-new.js +7 -18
  8. package/dist/agent-whats-new.js.map +1 -1
  9. package/dist/auth-api.d.ts +0 -7
  10. package/dist/auth-api.js +1 -15
  11. package/dist/auth-api.js.map +1 -1
  12. package/dist/authority/authority-state.js +1 -1
  13. package/dist/authority/authority-state.js.map +1 -1
  14. package/dist/authority/web-authority.d.ts +9 -4
  15. package/dist/authority/web-authority.js +10 -5
  16. package/dist/authority/web-authority.js.map +1 -1
  17. package/dist/backend/antigravity.js +6 -1
  18. package/dist/backend/antigravity.js.map +1 -1
  19. package/dist/backend/claude-code.d.ts +7 -0
  20. package/dist/backend/claude-code.js +34 -28
  21. package/dist/backend/claude-code.js.map +1 -1
  22. package/dist/backend/claude-transcript-errors.d.ts +18 -0
  23. package/dist/backend/claude-transcript-errors.js +19 -0
  24. package/dist/backend/claude-transcript-errors.js.map +1 -0
  25. package/dist/backend/codex-session-lookup.d.ts +16 -3
  26. package/dist/backend/codex-session-lookup.js +45 -11
  27. package/dist/backend/codex-session-lookup.js.map +1 -1
  28. package/dist/backend/codex.d.ts +16 -0
  29. package/dist/backend/codex.js +184 -35
  30. package/dist/backend/codex.js.map +1 -1
  31. package/dist/backend/grok.js +58 -12
  32. package/dist/backend/grok.js.map +1 -1
  33. package/dist/backend/kiro.js +2 -0
  34. package/dist/backend/kiro.js.map +1 -1
  35. package/dist/backend/muse.js +2 -1
  36. package/dist/backend/muse.js.map +1 -1
  37. package/dist/backend/types.d.ts +46 -0
  38. package/dist/backend/types.js.map +1 -1
  39. package/dist/bundled-skills.d.ts +4 -6
  40. package/dist/bundled-skills.js +10 -14
  41. package/dist/bundled-skills.js.map +1 -1
  42. package/dist/channel/adapters/discord.js +60 -17
  43. package/dist/channel/adapters/discord.js.map +1 -1
  44. package/dist/channel/mcp-server.js +5 -5
  45. package/dist/channel/mcp-server.js.map +1 -1
  46. package/dist/channel/mcp-tools.d.ts +1 -3
  47. package/dist/channel/mcp-tools.js +4 -58
  48. package/dist/channel/mcp-tools.js.map +1 -1
  49. package/dist/classic-channel-manager.d.ts +4 -1
  50. package/dist/classic-channel-manager.js +21 -7
  51. package/dist/classic-channel-manager.js.map +1 -1
  52. package/dist/cli.js +21 -4
  53. package/dist/cli.js.map +1 -1
  54. package/dist/config-history.d.ts +50 -0
  55. package/dist/config-history.js +263 -0
  56. package/dist/config-history.js.map +1 -0
  57. package/dist/config-validator.js +7 -7
  58. package/dist/config-validator.js.map +1 -1
  59. package/dist/config.d.ts +3 -0
  60. package/dist/config.js +11 -4
  61. package/dist/config.js.map +1 -1
  62. package/dist/daemon.d.ts +73 -3
  63. package/dist/daemon.js +334 -121
  64. package/dist/daemon.js.map +1 -1
  65. package/dist/daily-summary.d.ts +8 -1
  66. package/dist/daily-summary.js +8 -5
  67. package/dist/daily-summary.js.map +1 -1
  68. package/dist/dashboard-live.d.ts +1 -1
  69. package/dist/dashboard-live.js +3 -3
  70. package/dist/dashboard-live.js.map +1 -1
  71. package/dist/dashboard-permissions.d.ts +1 -1
  72. package/dist/dashboard-permissions.js +1 -3
  73. package/dist/dashboard-permissions.js.map +1 -1
  74. package/dist/delivery-outbox.d.ts +4 -0
  75. package/dist/delivery-outbox.js +12 -2
  76. package/dist/delivery-outbox.js.map +1 -1
  77. package/dist/doctor.d.ts +4 -0
  78. package/dist/doctor.js +16 -1
  79. package/dist/doctor.js.map +1 -1
  80. package/dist/fleet-level-config.js +1 -1
  81. package/dist/fleet-level-config.js.map +1 -1
  82. package/dist/fleet-manager.d.ts +70 -54
  83. package/dist/fleet-manager.js +257 -322
  84. package/dist/fleet-manager.js.map +1 -1
  85. package/dist/fleet-process-handlers.d.ts +5 -0
  86. package/dist/fleet-process-handlers.js +21 -0
  87. package/dist/fleet-process-handlers.js.map +1 -1
  88. package/dist/fleet-service-owner.d.ts +11 -0
  89. package/dist/fleet-service-owner.js +3 -0
  90. package/dist/fleet-service-owner.js.map +1 -0
  91. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +11 -1
  92. package/dist/general-knowledge/skills/session-management/SKILL.md +6 -0
  93. package/dist/general-knowledge/skills/web-appearance/SKILL.md +13 -204
  94. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +29 -67
  95. package/dist/instance-lifecycle.d.ts +19 -0
  96. package/dist/instance-lifecycle.js +52 -10
  97. package/dist/instance-lifecycle.js.map +1 -1
  98. package/dist/locale.js +92 -42
  99. package/dist/locale.js.map +1 -1
  100. package/dist/needs-you-hub.d.ts +1 -0
  101. package/dist/needs-you-hub.js +6 -3
  102. package/dist/needs-you-hub.js.map +1 -1
  103. package/dist/needs-you-live.d.ts +3 -1
  104. package/dist/needs-you-live.js +24 -2
  105. package/dist/needs-you-live.js.map +1 -1
  106. package/dist/needs-you.d.ts +13 -2
  107. package/dist/needs-you.js +11 -1
  108. package/dist/needs-you.js.map +1 -1
  109. package/dist/notify-socket.d.ts +10 -0
  110. package/dist/notify-socket.js +45 -0
  111. package/dist/notify-socket.js.map +1 -0
  112. package/dist/outbound-handlers.js +19 -7
  113. package/dist/outbound-handlers.js.map +1 -1
  114. package/dist/outbound-schemas.d.ts +0 -62
  115. package/dist/outbound-schemas.js +3 -42
  116. package/dist/outbound-schemas.js.map +1 -1
  117. package/dist/parked-notices.d.ts +87 -0
  118. package/dist/parked-notices.js +194 -0
  119. package/dist/parked-notices.js.map +1 -0
  120. package/dist/public-link-progress.d.ts +5 -2
  121. package/dist/public-link-progress.js +4 -3
  122. package/dist/public-link-progress.js.map +1 -1
  123. package/dist/public-web-gateway.js +0 -9
  124. package/dist/public-web-gateway.js.map +1 -1
  125. package/dist/public-web-link.d.ts +12 -0
  126. package/dist/public-web-link.js +29 -5
  127. package/dist/public-web-link.js.map +1 -1
  128. package/dist/quota-notice.d.ts +23 -0
  129. package/dist/quota-notice.js +22 -0
  130. package/dist/quota-notice.js.map +1 -0
  131. package/dist/scheduler/task-board.d.ts +0 -13
  132. package/dist/scheduler/task-board.js +0 -14
  133. package/dist/scheduler/task-board.js.map +1 -1
  134. package/dist/sd-notify.d.ts +10 -1
  135. package/dist/sd-notify.js +27 -2
  136. package/dist/sd-notify.js.map +1 -1
  137. package/dist/search/search-docs.d.ts +1 -1
  138. package/dist/search/search-docs.js +2 -2
  139. package/dist/search/search-docs.js.map +1 -1
  140. package/dist/settings-api.d.ts +16 -0
  141. package/dist/settings-api.js +55 -2
  142. package/dist/settings-api.js.map +1 -1
  143. package/dist/settings-change.js +3 -1
  144. package/dist/settings-change.js.map +1 -1
  145. package/dist/settings-confirmation.d.ts +17 -3
  146. package/dist/settings-confirmation.js +8 -5
  147. package/dist/settings-confirmation.js.map +1 -1
  148. package/dist/settings-http-confirmation.js +31 -1
  149. package/dist/settings-http-confirmation.js.map +1 -1
  150. package/dist/setup-wizard.d.ts +3 -0
  151. package/dist/setup-wizard.js +4 -2
  152. package/dist/setup-wizard.js.map +1 -1
  153. package/dist/task-board-waker.d.ts +4 -1
  154. package/dist/task-board-waker.js +13 -2
  155. package/dist/task-board-waker.js.map +1 -1
  156. package/dist/tool-permissions.js +3 -10
  157. package/dist/tool-permissions.js.map +1 -1
  158. package/dist/topic-commands.d.ts +27 -7
  159. package/dist/topic-commands.js +95 -36
  160. package/dist/topic-commands.js.map +1 -1
  161. package/dist/transcript-monitor.d.ts +7 -0
  162. package/dist/transcript-monitor.js +48 -3
  163. package/dist/transcript-monitor.js.map +1 -1
  164. package/dist/tunnel/cloudflared.d.ts +5 -0
  165. package/dist/tunnel/cloudflared.js +32 -1
  166. package/dist/tunnel/cloudflared.js.map +1 -1
  167. package/dist/tunnel/named-diagnostics.d.ts +2 -1
  168. package/dist/tunnel/named-diagnostics.js +4 -1
  169. package/dist/tunnel/named-diagnostics.js.map +1 -1
  170. package/dist/tunnel/named-link-test.d.ts +90 -0
  171. package/dist/tunnel/named-link-test.js +270 -0
  172. package/dist/tunnel/named-link-test.js.map +1 -0
  173. package/dist/tunnel/purpose-lane.d.ts +3 -1
  174. package/dist/tunnel/purpose-lane.js +2 -0
  175. package/dist/tunnel/purpose-lane.js.map +1 -1
  176. package/dist/types.d.ts +8 -6
  177. package/dist/ui/chat-ambient.js +4 -4
  178. package/dist/ui/chat-commands.js +3 -2
  179. package/dist/ui/chat-store.js +4 -1
  180. package/dist/ui/chat-strings.js +12 -46
  181. package/dist/ui/chat-thread.js +51 -59
  182. package/dist/ui/fleet-office-skin.js +37 -14
  183. package/dist/ui/fleet-office.js +31 -7
  184. package/dist/ui/html-attachment.js +22 -8
  185. package/dist/ui/panel-chat.js +21 -50
  186. package/dist/ui/panel-needs.js +11 -9
  187. package/dist/ui/panel-settings.js +5 -3
  188. package/dist/ui/preview-panel.js +12 -21
  189. package/dist/ui/preview.js +10 -3
  190. package/dist/ui/search-dialog.js +30 -14
  191. package/dist/ui/settings-appearance.js +1 -3
  192. package/dist/ui/settings-confirm.js +18 -0
  193. package/dist/ui/settings-guests.js +13 -4
  194. package/dist/ui/settings-learn.js +1 -1
  195. package/dist/ui/settings-mods.js +5 -12
  196. package/dist/ui/settings-named-test.js +144 -0
  197. package/dist/ui/settings-registry.js +0 -1
  198. package/dist/ui/settings-strings.js +14 -14
  199. package/dist/ui/settings-web-authority.js +4 -3
  200. package/dist/ui/shared/app-i18n.js +15 -4
  201. package/dist/ui/shared/app-needs.js +5 -1
  202. package/dist/ui/shared/app-prefs.js +17 -17
  203. package/dist/ui/shared/app-shell.js +47 -9
  204. package/dist/ui/shared/app-store.js +6 -2
  205. package/dist/ui/shared/app.css +33 -39
  206. package/dist/ui/shared/app.js +5 -4
  207. package/dist/ui/shared/instance-nav.js +9 -9
  208. package/dist/ui/shared/instance-switch.js +8 -2
  209. package/dist/ui/shared/panel-view.js +22 -8
  210. package/dist/ui/shared/view-strings.js +6 -0
  211. package/dist/ui/shared/web-authorize.js +35 -13
  212. package/dist/usage/usage-api.d.ts +3 -0
  213. package/dist/usage/usage-api.js +5 -1
  214. package/dist/usage/usage-api.js.map +1 -1
  215. package/dist/view-api.d.ts +2 -0
  216. package/dist/view-api.js +64 -0
  217. package/dist/view-api.js.map +1 -1
  218. package/dist/web-api.d.ts +9 -17
  219. package/dist/web-api.js +257 -371
  220. package/dist/web-api.js.map +1 -1
  221. package/dist/web-appearance-tools.d.ts +0 -2
  222. package/dist/web-appearance-tools.js +2 -5
  223. package/dist/web-appearance-tools.js.map +1 -1
  224. package/dist/web-appearance-view.d.ts +0 -6
  225. package/dist/web-appearance-view.js +0 -1
  226. package/dist/web-appearance-view.js.map +1 -1
  227. package/dist/web-appearance.d.ts +14 -7
  228. package/dist/web-appearance.js +5 -6
  229. package/dist/web-appearance.js.map +1 -1
  230. package/dist/web-asset-parts.d.ts +1 -17
  231. package/dist/web-asset-parts.js +1 -15
  232. package/dist/web-asset-parts.js.map +1 -1
  233. package/dist/web-auth.js +1 -7
  234. package/dist/web-auth.js.map +1 -1
  235. package/dist/web-chat-history.d.ts +3 -7
  236. package/dist/web-chat-history.js +10 -17
  237. package/dist/web-chat-history.js.map +1 -1
  238. package/dist/web-commands.d.ts +9 -1
  239. package/dist/web-commands.js +13 -0
  240. package/dist/web-commands.js.map +1 -1
  241. package/dist/web-experimental.d.ts +2 -11
  242. package/dist/web-experimental.js +8 -36
  243. package/dist/web-experimental.js.map +1 -1
  244. package/dist/web-guest-access.d.ts +40 -2
  245. package/dist/web-guest-access.js +50 -14
  246. package/dist/web-guest-access.js.map +1 -1
  247. package/dist/web-mod-bundle.d.ts +2 -45
  248. package/dist/web-mod-bundle.js +15 -63
  249. package/dist/web-mod-bundle.js.map +1 -1
  250. package/dist/web-mod-store.d.ts +3 -48
  251. package/dist/web-mod-store.js +30 -97
  252. package/dist/web-mod-store.js.map +1 -1
  253. package/dist/web-prefs.d.ts +13 -0
  254. package/dist/web-prefs.js +38 -0
  255. package/dist/web-prefs.js.map +1 -1
  256. package/dist/web-preview.d.ts +6 -13
  257. package/dist/web-preview.js +12 -23
  258. package/dist/web-preview.js.map +1 -1
  259. package/dist/web-session.d.ts +11 -3
  260. package/dist/web-session.js +22 -11
  261. package/dist/web-session.js.map +1 -1
  262. package/dist/web-settings-patch.d.ts +1 -0
  263. package/dist/web-settings-patch.js +6 -3
  264. package/dist/web-settings-patch.js.map +1 -1
  265. package/dist/web-settings-tools.d.ts +0 -13
  266. package/dist/web-settings-tools.js +12 -84
  267. package/dist/web-settings-tools.js.map +1 -1
  268. package/launcher/agend-install.cjs +7 -1
  269. package/launcher/fleet-service-owner.cjs +150 -0
  270. package/package.json +1 -1
  271. package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +0 -376
  272. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +0 -673
  273. package/dist/task-board-summary.d.ts +0 -101
  274. package/dist/task-board-summary.js +0 -177
  275. package/dist/task-board-summary.js.map +0 -1
  276. package/dist/ui/app-effects.js +0 -251
  277. package/dist/ui/chat-effect-posters.js +0 -42
  278. package/dist/ui/chat-effects.js +0 -167
  279. package/dist/ui/effect-trial-controller.js +0 -124
  280. package/dist/ui/effect-trial-offer.js +0 -2
  281. package/dist/ui/effect-trial-panel.js +0 -40
  282. package/dist/ui/effects-waker.js +0 -2
  283. package/dist/ui/settings-effects.js +0 -202
  284. package/dist/ui/settings-owner-effects.js +0 -82
  285. package/dist/ui/shared/effect-app-runtime.js +0 -299
  286. package/dist/ui/shared/effect-baseline.js +0 -64
  287. package/dist/ui/shared/effect-chat-runtime.js +0 -162
  288. package/dist/ui/shared/effect-cursor.js +0 -126
  289. package/dist/ui/shared/effect-display-metadata.js +0 -39
  290. package/dist/ui/shared/effect-frame.js +0 -146
  291. package/dist/ui/shared/effect-geometry.js +0 -137
  292. package/dist/ui/shared/effect-host-style.js +0 -10
  293. package/dist/ui/shared/effect-owner-regions.js +0 -74
  294. package/dist/ui/shared/effect-owner-source.js +0 -122
  295. package/dist/ui/shared/effect-policy.js +0 -151
  296. package/dist/ui/shared/effect-poster-reader.js +0 -98
  297. package/dist/ui/shared/effect-posters.js +0 -117
  298. package/dist/ui/shared/effect-profiler.js +0 -184
  299. package/dist/ui/shared/effect-profiles.js +0 -65
  300. package/dist/ui/shared/effect-registry.js +0 -545
  301. package/dist/ui/shared/effect-runtime-loader.js +0 -236
  302. package/dist/ui/shared/effect-safety.js +0 -153
  303. package/dist/ui/shared/effect-stock-snapshot.js +0 -43
  304. package/dist/ui/shared/effect-stock-trust.js +0 -217
  305. package/dist/ui/shared/effect-trial-shadow.js +0 -62
  306. package/dist/ui/shared/effect-trial-source.js +0 -103
  307. package/dist/ui/shared/effect-trial-stage.js +0 -170
  308. package/dist/ui/shared/effect-trial.css +0 -40
  309. package/dist/ui/shared/feature-flags.js +0 -127
  310. package/dist/ui/shared/owner-effect-prefs.js +0 -117
  311. package/dist/web-effect-bootstrap.d.ts +0 -12
  312. package/dist/web-effect-bootstrap.js +0 -49
  313. package/dist/web-effect-bootstrap.js.map +0 -1
  314. package/dist/web-effect-bundle.d.ts +0 -28
  315. package/dist/web-effect-bundle.js +0 -171
  316. package/dist/web-effect-bundle.js.map +0 -1
  317. package/dist/web-effect-canonical.d.ts +0 -8
  318. package/dist/web-effect-canonical.js +0 -27
  319. package/dist/web-effect-canonical.js.map +0 -1
  320. package/dist/web-effect-library.d.ts +0 -114
  321. package/dist/web-effect-library.js +0 -530
  322. package/dist/web-effect-library.js.map +0 -1
  323. package/dist/web-effect-playback.d.ts +0 -30
  324. package/dist/web-effect-playback.js +0 -71
  325. package/dist/web-effect-playback.js.map +0 -1
  326. package/dist/web-effect-poster.d.ts +0 -8
  327. package/dist/web-effect-poster.js +0 -52
  328. package/dist/web-effect-poster.js.map +0 -1
  329. package/dist/web-effect-profiles.d.ts +0 -118
  330. package/dist/web-effect-profiles.js +0 -181
  331. package/dist/web-effect-profiles.js.map +0 -1
  332. package/dist/web-effect-service.d.ts +0 -93
  333. package/dist/web-effect-service.js +0 -335
  334. package/dist/web-effect-service.js.map +0 -1
  335. package/dist/web-effect-tools.d.ts +0 -48
  336. package/dist/web-effect-tools.js +0 -129
  337. package/dist/web-effect-tools.js.map +0 -1
  338. package/dist/web-effect-trials.d.ts +0 -86
  339. package/dist/web-effect-trials.js +0 -204
  340. package/dist/web-effect-trials.js.map +0 -1
  341. package/dist/web-effect-upload.d.ts +0 -27
  342. package/dist/web-effect-upload.js +0 -185
  343. package/dist/web-effect-upload.js.map +0 -1
  344. package/dist/web-effect-worker.d.ts +0 -8
  345. package/dist/web-effect-worker.js +0 -170
  346. package/dist/web-effect-worker.js.map +0 -1
  347. package/dist/web-effect.d.ts +0 -103
  348. package/dist/web-effect.js +0 -153
  349. package/dist/web-effect.js.map +0 -1
  350. package/dist/web-experimental-http.d.ts +0 -2
  351. package/dist/web-experimental-http.js +0 -12
  352. package/dist/web-experimental-http.js.map +0 -1
  353. package/dist/web-mod-agent.d.ts +0 -50
  354. package/dist/web-mod-agent.js +0 -207
  355. package/dist/web-mod-agent.js.map +0 -1
package/dist/daemon.js CHANGED
@@ -25,13 +25,12 @@ import { createTranscriptSource } from "./transcript-sources.js";
25
25
  import { TranscriptTurnLedger } from "./transcript-turns.js";
26
26
  import { credentialProfileStoreHome, resolveCredentialProfile } from "./backend/credential-profile.js";
27
27
  import { resolveToolSet, toolsFor } from "./tool-permissions.js";
28
- import { TOOLS, toolsWithCanvasWorker } from "./channel/mcp-tools.js";
28
+ import { TOOLS } from "./channel/mcp-tools.js";
29
29
  import { dashboardAccessProblem, dashboardFeaturesOff, withoutFeatures } from "./dashboard-permissions.js";
30
30
  import { getAgendHome } from "./paths.js";
31
- import { readCanvasWorkerEnabled, scopedFleetConfigEnvironment } from "./web-experimental.js";
32
- import { readAgentEffect, sendEffectInParts } from "./web-effect-upload.js";
31
+ import { scopedFleetConfigEnvironment } from "./web-experimental.js";
33
32
  import { readAgentFile } from "./agent-file-read.js";
34
- import { ASSET_UPLOAD_MAX_BYTES, MOD_FILE, MOD_MAX_PARTS, MOD_UPLOAD_MAX_BYTES, receiveFiles, receivePreview, sendAssetInParts, writeModFile, writePreview } from "./web-asset-parts.js";
33
+ import { ASSET_UPLOAD_MAX_BYTES, receivePreview, sendAssetInParts, writePreview } from "./web-asset-parts.js";
35
34
  import { LOGIN_FLOWS } from "./login-flows.js";
36
35
  import { ProgressAccumulator, summarizeProgress } from "./tool-progress.js";
37
36
  import { assembleSystemPrompt, resolveWorkflowText } from "./prompt-file-ref.js";
@@ -53,7 +52,9 @@ import { formatHumanInboundMessage, prepareDeliveryWire, steerInboundEnvelope, B
53
52
  import { formatCrossInstanceInboundMessage, renderCrossInstanceHandoffMetadata, } from "./cross-instance-envelope.js";
54
53
  import { agendMessageInInput, bottomRowIsReady, inputAreaText, inputShowsPastedText, pastedTextSignature, pasteLeftInInput, strandedAgendMessageInInput } from "./pane-input-residue.js";
55
54
  import { TurnReplyGuard, formatReplyDropDetection, turnExpectsPersonReply } from "./turn-reply-guard.js";
55
+ import { CLAUDE_CREDIT_BALANCE_LOW, CLAUDE_MODEL_USAGE_LIMIT, CLAUDE_USAGE_LIMIT_PAUSE } from "./backend/claude-transcript-errors.js";
56
56
  import { t } from "./locale.js";
57
+ import { claudeResetTime, quotaPausedLine } from "./quota-notice.js";
57
58
  import { MuseUsageRelay, clearMuseUsageSnapshot } from "./muse-usage-relay.js";
58
59
  import { deliveryContentDigest } from "./delivery-outbox.js";
59
60
  import { consumedWatches } from "./delivery-consumed-watch.js";
@@ -81,15 +82,6 @@ const WEB_THEME_IPC = Object.freeze({
81
82
  set_web_widget: "fleet_set_web_widget", remove_web_widget: "fleet_remove_web_widget",
82
83
  // #2117: its own declarative ambient.
83
84
  set_web_ambient: "fleet_set_web_ambient", clear_web_ambient: "fleet_clear_web_ambient",
84
- // r4-5b: mods — an export's file is written here, a proposal's file read here (on the agent's side).
85
- export_web_mod: "fleet_export_web_mod", propose_web_mod: "fleet_propose_web_mod",
86
- upload_web_effect: "fleet_upload_web_effect",
87
- list_web_effects: "fleet_list_web_effects",
88
- preview_web_effect: "fleet_preview_web_effect",
89
- profile_web_effect: "fleet_profile_web_effect",
90
- apply_web_effect: "fleet_apply_web_effect",
91
- remove_web_effect: "fleet_remove_web_effect",
92
- delete_web_effect: "fleet_delete_web_effect",
93
85
  // #2578: its own character in the Office view — the args as they are (the fleet checks them).
94
86
  office_view: "fleet_office_view", office_set: "fleet_office_set",
95
87
  // #2692: the skin packs — check and upload read the pack here (web-office-pack-read.ts) and send its bytes in parts.
@@ -567,6 +559,8 @@ const DIALOG_ANSWER_IGNORED_MAX = 3;
567
559
  * normal pace (and a fresh count).
568
560
  */
569
561
  const DIALOG_ANSWER_BACKOFF_MS = 30_000;
562
+ /** #2844: how long a dialog AgEnD answers itself (`answeredByAgend`) stays nobody's interaction while it is on screen. */
563
+ const AGEND_ANSWER_GRACE_MS = 20_000;
570
564
  /** How many "dialog painted just before the write → wait → retry" rounds a delivery tolerates. */
571
565
  const LATE_DIALOG_WRITE_ROUNDS = 3;
572
566
  /** How many times a delivery redoes itself when a spawn starts between its settle wait and its pane write. */
@@ -1263,13 +1257,26 @@ export class Daemon extends EventEmitter {
1263
1257
  bootSkippedResume = false;
1264
1258
  recoveryCrashLoopFresh = false;
1265
1259
  recoveryFreshChoice = false;
1260
+ /** The current boot found a crash-loop marker and is attempting resume rather than going fresh immediately. */
1261
+ bootCrashLoopResume = false;
1266
1262
  startupAborted = false;
1267
1263
  /** First time the current on-screen blocking dialog was seen (0 = none); drives the parked report. */
1268
1264
  dialogParkedSince = 0;
1269
1265
  /** Identity of that dialog: its pattern, not its description (two tables may describe one screen differently). */
1270
1266
  dialogParkedKey = null;
1267
+ /**
1268
+ * #2893: the held dialog whose heldReason was last logged. Its own state, not the parked clock's: the delivery probe
1269
+ * starts that clock too, and must not use up the one log line. Cleared with the hold, so a later hold logs again.
1270
+ */
1271
+ heldReasonLoggedKey = null;
1271
1272
  dialogParkedInteractionEpisode = null;
1272
1273
  dialogParkedReported = false;
1274
+ /**
1275
+ * #2897: a "needs a human" notice is out for a dialog, and what it lives with: "parked" — the parked clock (retired when
1276
+ * that dialog leaves the pane or another replaces it); "answer" — an answer episode of a dialog the parked clock does
1277
+ * not track (a non-blocking one, whose scans pass no blocking dialog): retired when that episode ends.
1278
+ */
1279
+ parkedNoticeOut = null;
1273
1280
  /** #2675: the card on screen that a person can answer from the chat — what it asks, since when, and whether it was posted. */
1274
1281
  remoteCardShown = null;
1275
1282
  /** #2675: when (performance.now, taken before its capture) the newest read the card tracker used was started. */
@@ -1320,7 +1327,7 @@ export class Daemon extends EventEmitter {
1320
1327
  * cancels it when anything changed, and it fires at most once. The due time is on the monotonic clock.
1321
1328
  */
1322
1329
  /**
1323
- * How many occurrences of each `nudge_continue` pattern were already on screen when this spawn first showed a live
1330
+ * How many occurrences of each `nudge_continue` (or `historyUntilLiveComposer`) pattern were already on screen when this spawn first showed a live
1324
1331
  * composer (null until then): resumed scrollback, i.e. HISTORY. An occurrence is a new episode only beyond that — a
1325
1332
  * resumed session whose scrollback holds an old capacity line must not be told to "keep going" for it (#949).
1326
1333
  */
@@ -1334,6 +1341,8 @@ export class Daemon extends EventEmitter {
1334
1341
  interactionObservedOutputRevision = 0;
1335
1342
  interactionCaptureSerial = 0;
1336
1343
  interactionDialogKey = null;
1344
+ /** Since when (monotonic) the `answeredByAgend` dialog now on screen has been withheld from the interaction state. */
1345
+ agendAnswerGrace = null;
1337
1346
  acceptedInstanceSnapshot = null;
1338
1347
  /** Generation/signature fence for safety dialogs while their screen is still visible. */
1339
1348
  autoResolvedDialogKey = null;
@@ -1765,6 +1774,77 @@ export class Daemon extends EventEmitter {
1765
1774
  // old transcript, so pre-ready scans must not lower this seeded baseline.
1766
1775
  seededErrorCountBaselines = new Map();
1767
1776
  lastDetectedErrorType = null;
1777
+ /**
1778
+ * #2822: Claude's per-model usage limit (`You've reached your <model> limit.`), known only from Claude's own record of
1779
+ * the failed call in its transcript (`isApiErrorMessage`, a 429) — never from pane text, which an agent can print. It
1780
+ * names no reset time. While it holds it is told once, and the reply guard stays quiet while the transcript's newest
1781
+ * event is that error; any later assistant output (of any turn, from a person or not) ends it. The order is Claude's
1782
+ * own: a 429 is recorded within a second, often before AgEnD has armed the turn, so no AgEnD-side clock is used.
1783
+ */
1784
+ usageLimitHold = null;
1785
+ /** Transcript events in Claude's order (prompts, output, every API error): the newest, and the latest limit error. */
1786
+ transcriptEventSeq = 0;
1787
+ usageLimitErrorSeq = 0;
1788
+ static MODEL_USAGE_LIMIT = CLAUDE_MODEL_USAGE_LIMIT;
1789
+ /** #2822: the usage-limit hold, while it lasts — Needs you lists it. */
1790
+ usageHold() {
1791
+ return this.usageLimitHold ? { ...this.usageLimitHold } : null;
1792
+ }
1793
+ /** #2834: Claude's account is out of API credits — its own record; the same pause as the pane pattern it replaces. */
1794
+ static CREDIT_BALANCE_LOW = CLAUDE_CREDIT_BALANCE_LOW;
1795
+ /** #2834 (#816): Claude's usage-limit auto-continue note, as it records it (`system` / `informational`). */
1796
+ static USAGE_LIMIT_PAUSE = CLAUDE_USAGE_LIMIT_PAUSE;
1797
+ /** #2834 (#816): Claude paused itself until its usage window opens — the same notice the screen pattern gave. */
1798
+ noteClaudeInformational(text) {
1799
+ const match = Daemon.USAGE_LIMIT_PAUSE.exec(text.trim());
1800
+ if (!match)
1801
+ return;
1802
+ const when = match[1].trim();
1803
+ this.emit("pty_error", { name: this.name, type: "quota", action: "notify", verifyQuota: true, skipCooldown: true, skipRecoveryWait: true,
1804
+ message: `Claude Code usage limit reached — paused; continuing automatically ${when}`,
1805
+ notice: quotaPausedLine("Claude", "usage", claudeResetTime(when)) });
1806
+ }
1807
+ /** #2822: Claude recorded a failed API call. */
1808
+ noteClaudeApiError(text, status, errorKind = null) {
1809
+ const seq = ++this.transcriptEventSeq;
1810
+ if (errorKind === "billing_error" || (status === 400 && Daemon.CREDIT_BALANCE_LOW.test(text))) {
1811
+ this.emit("pty_error", { name: this.name, type: "quota", action: "pause", message: "Insufficient API credits", notice: quotaPausedLine("Claude", "api_credits") });
1812
+ return;
1813
+ }
1814
+ const match = status === 429 ? Daemon.MODEL_USAGE_LIMIT.exec(text.trim()) : null;
1815
+ if (!match)
1816
+ return;
1817
+ this.usageLimitErrorSeq = seq;
1818
+ if (this.usageLimitHold)
1819
+ return; // told already: one notice per hold
1820
+ const model = match[1].trim();
1821
+ this.usageLimitHold = { model, since: Date.now() };
1822
+ const message = t("inst.model_usage_limit", model);
1823
+ this.logger.warn({ model }, "Usage-limit hold started (reset time unknown)");
1824
+ this.emit("usage_hold", { name: this.name, held: true });
1825
+ this.emit("pty_error", { name: this.name, type: "quota", action: "notify", message, notice: t("inst.quota_model_hold", model), skipCooldown: true, skipRecoveryWait: true });
1826
+ }
1827
+ /** #2822: Claude produced output (text or a tool call): the model answers again, so a hold ends. */
1828
+ noteClaudeOutput() {
1829
+ ++this.transcriptEventSeq;
1830
+ if (!this.usageLimitHold)
1831
+ return;
1832
+ this.logger.info({ model: this.usageLimitHold.model }, "Usage-limit hold cleared — Claude answered again");
1833
+ this.usageLimitHold = null;
1834
+ this.emit("usage_hold", { name: this.name, held: false });
1835
+ }
1836
+ /** #2822: a prompt reached Claude (recorded in its transcript). */
1837
+ noteClaudePrompt() {
1838
+ ++this.transcriptEventSeq;
1839
+ }
1840
+ /**
1841
+ * #2822: the turn ended on the limit error — the transcript's newest event of any kind. A prompt after it (a new
1842
+ * message the error is older than), output after it (the model answered) or another API error after it (a 500, a
1843
+ * different 429) means the limit error is not this turn's end.
1844
+ */
1845
+ personTurnEndedOnUsageLimit() {
1846
+ return this.usageLimitErrorSeq > 0 && this.usageLimitErrorSeq === this.transcriptEventSeq;
1847
+ }
1768
1848
  static errorPatternKey(ep) {
1769
1849
  return `${ep.type}:${ep.pattern.source}`;
1770
1850
  }
@@ -2265,19 +2345,40 @@ export class Daemon extends EventEmitter {
2265
2345
  this.bootSkippedResume = false;
2266
2346
  this.recoveryCrashLoopFresh = false;
2267
2347
  this.recoveryFreshChoice = false;
2348
+ this.bootCrashLoopResume = false;
2268
2349
  try {
2269
2350
  if (existsSync(crashStatePath)) {
2270
2351
  const content = readFileSync(crashStatePath, "utf-8");
2271
2352
  try {
2272
2353
  const state = JSON.parse(content);
2273
- if (state?.resumeDisabled === true) {
2354
+ if (state?.resumeFailed === true) {
2355
+ // The previous start tried resume after a crash loop but the resumed
2356
+ // session died before reaching ready. Go fresh this time.
2274
2357
  this.skipResume = true;
2275
2358
  this.bootSkippedResume = true;
2276
- this.recoveryCrashLoopFresh = (state.reason === undefined || state.reason === "crash_loop")
2359
+ this.recoveryCrashLoopFresh = true;
2360
+ this.recoveryFreshChoice = false;
2361
+ this.logger.warn("Previously resumed session died quickly after a crash loop — starting fresh");
2362
+ }
2363
+ else if (state?.resumeDisabled === true) {
2364
+ // Crash loop detected, but the session is not proven broken.
2365
+ // Try resume first; if it dies before reaching ready, the next
2366
+ // boot will see resumeFailed and go fresh (PR #2809).
2367
+ const isCrashLoop = (state.reason === undefined || state.reason === "crash_loop")
2277
2368
  && Number.isInteger(state.crashesInWindow) && state.crashesInWindow >= 3
2278
2369
  && Number.isFinite(state.lastCrashAt) && state.lastCrashAt > 0;
2279
- this.recoveryFreshChoice = !this.recoveryCrashLoopFresh;
2280
- this.logger.warn("Previous crash loop detected — starting without resume");
2370
+ if (isCrashLoop) {
2371
+ this.bootCrashLoopResume = true;
2372
+ this.logger.warn("Previous crash loop detected — attempting resume (will fall back to fresh if session is broken)");
2373
+ }
2374
+ else {
2375
+ // Explicit fresh-start request (not a crash loop); honour it.
2376
+ this.skipResume = true;
2377
+ this.bootSkippedResume = true;
2378
+ this.recoveryCrashLoopFresh = false;
2379
+ this.recoveryFreshChoice = true;
2380
+ this.logger.warn("Previous crash loop detected — starting without resume");
2381
+ }
2281
2382
  }
2282
2383
  }
2283
2384
  finally {
@@ -2465,6 +2566,7 @@ export class Daemon extends EventEmitter {
2465
2566
  .catch(e => this.logger.debug({ err: e.message }, "Ack react failed"));
2466
2567
  };
2467
2568
  this.transcriptMonitor.on("tool_use", (name, input) => {
2569
+ this.noteClaudeOutput();
2468
2570
  this.logger.debug({ tool: name }, "Tool use");
2469
2571
  ackIfPending();
2470
2572
  this.recordRecentEvent({ type: "tool_use", name, preview: this.summarizeTool(name, input) });
@@ -2478,7 +2580,11 @@ export class Daemon extends EventEmitter {
2478
2580
  // show only elapsed time than to leave a stale "Bash: npm test" on screen.
2479
2581
  this.publishActivity(null);
2480
2582
  });
2583
+ this.transcriptMonitor.on("api_error", (text, status, _at, errorKind) => this.noteClaudeApiError(text, status, errorKind));
2584
+ this.transcriptMonitor.on("system_informational", (text) => this.noteClaudeInformational(text));
2585
+ this.transcriptMonitor.on("user_prompt", () => this.noteClaudePrompt());
2481
2586
  this.transcriptMonitor.on("assistant_text", (text) => {
2587
+ this.noteClaudeOutput();
2482
2588
  this.logger.debug({ text: text.slice(0, 200) }, "Claude response");
2483
2589
  ackIfPending();
2484
2590
  this.recordRecentEvent({ type: "assistant_text", preview: text.slice(0, 100) });
@@ -3626,10 +3732,21 @@ export class Daemon extends EventEmitter {
3626
3732
  if (!Daemon.dialogMatches(dialog, pane))
3627
3733
  continue;
3628
3734
  this.noteCodexReserveDialog(dialog, pane);
3735
+ this.noteLiveIncidentDialog(dialog, pane);
3629
3736
  if (dialog.blocksDelivery || dialog.holdOnly)
3630
3737
  blockingSeen = dialog;
3631
- if (dialog.holdOnly)
3632
- break; // recognised, deliberately not answered; trackDialogParked reports it
3738
+ if (dialog.holdOnly) {
3739
+ // Recognised, deliberately not answered; trackDialogParked reports it. Why an automatic answer passed it by
3740
+ // is logged once, when this hold begins (#2893).
3741
+ if (this.heldReasonLoggedKey !== Daemon.dialogKey(dialog)) {
3742
+ const reason = Daemon.heldReasonOf(dialog, pane);
3743
+ if (Object.keys(reason).length) {
3744
+ this.heldReasonLoggedKey = Daemon.dialogKey(dialog);
3745
+ this.logger.warn({ description: dialog.description, ...reason }, "Runtime dialog held — not auto-answering");
3746
+ }
3747
+ }
3748
+ break;
3749
+ }
3633
3750
  // Reported as ignoring the daemon: the SAME request (the identity the count was made on — verbatim, the
3634
3751
  // backend's own when it names one) is answered only once per DIALOG_ANSWER_BACKOFF_MS. Anything else — another
3635
3752
  // request, an unexplained difference, the dialog gone — is not held back here; the answer path below ends or
@@ -3692,7 +3809,7 @@ export class Daemon extends EventEmitter {
3692
3809
  return;
3693
3810
  if (dialog.verifyAfterKeys && autoKey)
3694
3811
  this.claimAutoDialog(dialog);
3695
- this.logger.info(`Auto-dismissing runtime dialog: ${dialog.description}`);
3812
+ this.logger.info(Daemon.answerDetailOf(dialog, currentPane), `Auto-dismissing runtime dialog: ${dialog.description}`);
3696
3813
  const SPECIAL_KEYS = new Set(["Up", "Down", "Enter", "Escape", "Right", "Left"]);
3697
3814
  for (const key of dialog.keys) {
3698
3815
  if (stale())
@@ -3701,6 +3818,9 @@ export class Daemon extends EventEmitter {
3701
3818
  if (SPECIAL_KEYS.has(key)) {
3702
3819
  sent = await pollTmux.sendSpecialKey(key);
3703
3820
  }
3821
+ else if (dialog.literalKeys) {
3822
+ sent = await pollTmux.sendKeys(key); // typed, no Enter (#2872)
3823
+ }
3704
3824
  else {
3705
3825
  sent = await pollTmux.pasteText(key, this.systemPasteOptions());
3706
3826
  }
@@ -3745,6 +3865,8 @@ export class Daemon extends EventEmitter {
3745
3865
  else {
3746
3866
  resolved = true;
3747
3867
  }
3868
+ if (dialog.dismissIncident)
3869
+ this.emitDismissIncident(dialog);
3748
3870
  }
3749
3871
  else if (dialog.keys.length > 0) {
3750
3872
  // Did the keys take? Read the pane again. The same request still there is an answer that did nothing. A request
@@ -3778,8 +3900,10 @@ export class Daemon extends EventEmitter {
3778
3900
  // Reached only when no dialog was answered this poll (none on screen, a fenced one, or one waiting out its
3779
3901
  // backoff): unless it is the one backing off, the screen that was being answered is gone, and the next dialog is
3780
3902
  // a new one.
3781
- if (!answersBackedOff)
3903
+ if (!answersBackedOff && this.dialogAnswers) {
3782
3904
  this.dialogAnswers = null;
3905
+ this.retireParkedNotice("answer");
3906
+ }
3783
3907
  this.trackDialogParked(blockingSeen);
3784
3908
  this.trackRemoteCard(blockingSeen, pane, captureMono);
3785
3909
  if (blockingSeen)
@@ -3897,9 +4021,10 @@ export class Daemon extends EventEmitter {
3897
4021
  const count = countMatches(ep.pattern);
3898
4022
  const seen = this.lastErrorCount.get(key) ?? 0;
3899
4023
  // #2282: a resumed exhausted429 row is history, not even a new
3900
- // notification/failover incident. Wait for the first live composer to
3901
- // establish its baseline; later appended occurrences remain eligible.
3902
- if (ep.type === "rate_limit" && ep.action === "nudge_continue") {
4024
+ // notification/failover incident; so is a resumed codex workspace-limit row
4025
+ // (#2844: a wake must not pause again on it). Wait for the first live
4026
+ // composer to establish its baseline; later appended occurrences remain eligible.
4027
+ if ((ep.type === "rate_limit" && ep.action === "nudge_continue") || ep.historyUntilLiveComposer) {
3903
4028
  const baseline = this.nudgeBaselines?.get(key);
3904
4029
  if (baseline === undefined || count <= baseline) {
3905
4030
  if (baseline !== undefined)
@@ -3976,6 +4101,17 @@ export class Daemon extends EventEmitter {
3976
4101
  break; // Only handle first unsuppressed new error per scan
3977
4102
  }
3978
4103
  }
4104
+ /**
4105
+ * #2872: the incident a dialog stood for, once AgEnD's own keys closed it (RuntimeDialog.dismissIncident). It skips
4106
+ * the usage check (the modal was the proof) and arms no pane gate: nothing on screen is its evidence, so nothing
4107
+ * stale can raise it again.
4108
+ */
4109
+ emitDismissIncident(dialog) {
4110
+ const inc = dialog.dismissIncident;
4111
+ this.logger.warn({ errorType: inc.type, action: inc.action, dialog: dialog.description }, `Dialog incident: ${inc.message}`);
4112
+ this.emit("pty_error", { name: this.name, type: inc.type, action: inc.action, message: inc.message, skipUsageVerify: true,
4113
+ ...(inc.notice ? { notice: inc.notice } : {}) });
4114
+ }
3979
4115
  /** Arm the recovery gates and emit one detected error pattern. */
3980
4116
  emitErrorPattern(ep, key, pane, now) {
3981
4117
  // #2795: auth text on the pane proves nothing about the credentials (22 of 22 such matches on the main host were
@@ -3994,7 +4130,9 @@ export class Daemon extends EventEmitter {
3994
4130
  const message = this.resolveErrorMessage(pane, ep);
3995
4131
  this.logger.warn({ errorType: ep.type, action: ep.action }, `PTY error detected: ${message}`);
3996
4132
  this.lastErrorEpisode = { key, pattern: ep.pattern, pane, spawn: this.spawnGeneration, fence: this.launchFenceEpoch };
3997
- this.emit("pty_error", { name: this.name, ...ep, message });
4133
+ const { notice: noticeOf, ...rest } = ep;
4134
+ const notice = noticeOf ? this.resolveErrorNotice(pane, ep, noticeOf) : undefined;
4135
+ this.emit("pty_error", { name: this.name, ...rest, message, ...(notice ? { notice } : {}) });
3998
4136
  }
3999
4137
  /**
4000
4138
  * Notification text for a detected pattern. `formatMessage` patterns build it
@@ -4004,6 +4142,18 @@ export class Daemon extends EventEmitter {
4004
4142
  * Uses the LAST match in the pane: an older warning may still be scrolled up
4005
4143
  * (10% earlier, 5% now), and the newest one is the one worth reporting.
4006
4144
  */
4145
+ /** The pattern's short notice from its last match on the pane (no match: from none); undefined if it throws. */
4146
+ resolveErrorNotice(pane, ep, noticeOf) {
4147
+ try {
4148
+ const flags = ep.pattern.flags.includes("g") ? ep.pattern.flags : ep.pattern.flags + "g";
4149
+ const matches = [...pane.matchAll(new RegExp(ep.pattern.source, flags))];
4150
+ return noticeOf(matches[matches.length - 1] ?? null);
4151
+ }
4152
+ catch (err) {
4153
+ this.logger.debug({ err, errorType: ep.type }, "notice failed — posting the standard notification");
4154
+ return undefined;
4155
+ }
4156
+ }
4007
4157
  resolveErrorMessage(pane, ep) {
4008
4158
  if (!ep.formatMessage)
4009
4159
  return ep.message;
@@ -4934,6 +5084,22 @@ export class Daemon extends EventEmitter {
4934
5084
  && Daemon.dialogMatches(d, pane)
4935
5085
  // #1690: AgEnD's own switch dialog, being answered right now, is nobody's to answer.
4936
5086
  && !(this.ownSwitch && claudeSwitchDialog(pane) !== null));
5087
+ // #2844: a dialog AgEnD is answering itself is nobody's to answer — for AGEND_ANSWER_GRACE_MS of being on screen,
5088
+ // and only until its answers are reported as ignored. No evidence at all meanwhile: the catch-alls further down
5089
+ // the table match the same screen. A dialog still there after the grace (an Escape that did not take, keys that
5090
+ // could not be sent) is a person's again.
5091
+ if (dialog?.answeredByAgend) {
5092
+ const key = Daemon.answerKey(dialog);
5093
+ const now = performance.now();
5094
+ if (this.agendAnswerGrace?.key !== key)
5095
+ this.agendAnswerGrace = { key, since: now };
5096
+ if (now - this.agendAnswerGrace.since < AGEND_ANSWER_GRACE_MS
5097
+ && !(this.dialogAnswers?.reported && this.dialogAnswers.key === key))
5098
+ return null;
5099
+ }
5100
+ else {
5101
+ this.agendAnswerGrace = null;
5102
+ }
4937
5103
  if (dialog) {
4938
5104
  const identity = dialog.requestIdentity?.(pane) ?? pane.replace(/\r/g, "")
4939
5105
  .split("\n").map(row => row.replace(/^([ \t]*)[❯›]([ \t]+)(?=\d\.|Yes\b|No\b|Resume\b)/, "$1$2").trimEnd())
@@ -5662,6 +5828,15 @@ export class Daemon extends EventEmitter {
5662
5828
  this.turnReplyGuard.complete(turn.generation);
5663
5829
  return;
5664
5830
  }
5831
+ // #2822: a turn whose newest transcript event is Claude's usage-limit error could not have replied — no recovery
5832
+ // turn, no drop report: the limit is told once (the notice and Needs you). Only that record counts: pane text and
5833
+ // an error from before this turn began do not.
5834
+ if (this.personTurnEndedOnUsageLimit()) {
5835
+ this.logger.info({ correlationId: turn.target.correlationId }, "Turn ended on Claude's usage-limit error — no reply recovery");
5836
+ this.clearReplyGuardConfirm();
5837
+ this.turnReplyGuard.complete(turn.generation);
5838
+ return;
5839
+ }
5665
5840
  // A second idle edge ends the one permitted recovery turn. Never create a
5666
5841
  // third turn or guess at terminal text; make the failure visible instead.
5667
5842
  if (turn.phase === "recovering") {
@@ -8170,13 +8345,36 @@ export class Daemon extends EventEmitter {
8170
8345
  && !this.backend?.backgroundWorkRunning?.(pane) && codexLiveRowMatches(pane, nudge.pattern)
8171
8346
  && Daemon.countOccurrences(nudge.pattern, pane) === Daemon.countOccurrences(nudge.pattern, nudge.pane);
8172
8347
  }
8348
+ /**
8349
+ * #2844: a dialog that only paints for a live incident proves the newest such error row on screen is current: its
8350
+ * baseline is held below it, so neither a first poll that sees the dialog before any live composer nor the composer
8351
+ * after it can take that row for resumed history. Older rows, of any kind, stay history.
8352
+ */
8353
+ noteLiveIncidentDialog(dialog, pane) {
8354
+ if (!dialog.marksLiveIncident)
8355
+ return;
8356
+ const baselines = this.nudgeBaselines ?? new Map();
8357
+ // Only the row nearest the dialog is the incident; every other kind on screen (an older credits row above a new
8358
+ // spend cap) stays history, so the first matching pattern cannot claim the live incident under its own name.
8359
+ const rows = (this.backend?.getErrorPatterns?.() ?? []).filter(ep => ep.historyUntilLiveComposer).map(ep => {
8360
+ const flags = ep.pattern.flags.includes("g") ? ep.pattern.flags : ep.pattern.flags + "g";
8361
+ const matches = [...pane.matchAll(new RegExp(ep.pattern.source, flags))];
8362
+ return { key: Daemon.errorPatternKey(ep), count: matches.length, last: matches.at(-1)?.index ?? -1 };
8363
+ }).filter(row => row.count > 0);
8364
+ const newest = rows.reduce((best, row) => !best || row.last > best.last ? row : best, null);
8365
+ for (const row of rows) {
8366
+ const floor = row === newest ? row.count - 1 : row.count;
8367
+ baselines.set(row.key, Math.min(baselines.get(row.key) ?? floor, floor));
8368
+ }
8369
+ this.nudgeBaselines = baselines;
8370
+ }
8173
8371
  /** The first live composer of a spawn fixes what is history (see {@link nudgeBaselines}). */
8174
8372
  takeNudgeBaselines(pane, patterns) {
8175
8373
  if (!this.isCodexLivePaneSnapshot(pane))
8176
8374
  return;
8177
8375
  const baselines = this.nudgeBaselines ?? new Map();
8178
8376
  for (const ep of patterns) {
8179
- if (ep.action !== "nudge_continue")
8377
+ if (ep.action !== "nudge_continue" && !ep.historyUntilLiveComposer)
8180
8378
  continue;
8181
8379
  const key = Daemon.errorPatternKey(ep);
8182
8380
  const count = Daemon.countOccurrences(ep.pattern, pane);
@@ -8260,6 +8458,7 @@ export class Daemon extends EventEmitter {
8260
8458
  */
8261
8459
  endDialogEpisode(clearParked = true) {
8262
8460
  this.dialogAnswers = null;
8461
+ this.retireParkedNotice("answer");
8263
8462
  if (clearParked)
8264
8463
  this.trackDialogParked(null);
8265
8464
  }
@@ -8286,8 +8485,11 @@ export class Daemon extends EventEmitter {
8286
8485
  */
8287
8486
  noteDialogAnswer(dialog, outcome, screen) {
8288
8487
  const key = Daemon.answerKey(dialog);
8289
- if (!this.dialogAnswers || this.dialogAnswers.key !== key)
8290
- this.dialogAnswers = { key, ignored: 0, reported: false, screen, lastAnswerAt: 0 }; // the exact screen being counted: every answer in the episode saw this same one
8488
+ if (!this.dialogAnswers || this.dialogAnswers.key !== key) {
8489
+ if (this.dialogAnswers)
8490
+ this.retireParkedNotice("answer"); // #2897: a different screen: the reported one is over
8491
+ this.dialogAnswers = { key, ignored: 0, reported: false, screen, lastAnswerAt: 0 };
8492
+ } // the exact screen being counted: every answer in the episode saw this same one
8291
8493
  const episode = this.dialogAnswers;
8292
8494
  if (outcome !== "ignored") {
8293
8495
  this.endDialogEpisode(outcome === "took");
@@ -8303,6 +8505,8 @@ export class Daemon extends EventEmitter {
8303
8505
  this.dialogParkedReported = true;
8304
8506
  const holdsDeliveries = dialog.blocksDelivery === true || dialog.holdOnly === true;
8305
8507
  this.logger.warn({ dialog: dialog.description, attempts: episode.ignored, holdsDeliveries }, "Runtime dialog is ignoring AgEnD's answer — reporting for a human");
8508
+ // #2897: a dialog the parked clock tracks keeps its notice until it leaves the pane; any other, until this episode ends.
8509
+ this.parkedNoticeOut = this.dialogParkedSince !== 0 && this.dialogParkedKey === Daemon.dialogKey(dialog) ? "parked" : "answer";
8306
8510
  this.emit("dialog_answer_ignored", { name: this.name, description: dialog.description, attempts: episode.ignored, holdsDeliveries });
8307
8511
  }
8308
8512
  /**
@@ -8376,6 +8580,24 @@ export class Daemon extends EventEmitter {
8376
8580
  this.remoteCardAnswering = false;
8377
8581
  }
8378
8582
  }
8583
+ /** #2893: a held dialog's own account of why no automatic answer took it (RuntimeDialog.heldReason); {} when none. */
8584
+ static heldReasonOf(dialog, pane) {
8585
+ try {
8586
+ return dialog.heldReason?.(pane) ?? {};
8587
+ }
8588
+ catch {
8589
+ return {};
8590
+ }
8591
+ }
8592
+ /** What an answered dialog says about the answer (RuntimeDialog.answerDetail); {} when it gives none. */
8593
+ static answerDetailOf(dialog, pane) {
8594
+ try {
8595
+ return dialog.answerDetail?.(pane) ?? {};
8596
+ }
8597
+ catch {
8598
+ return {};
8599
+ }
8600
+ }
8379
8601
  /**
8380
8602
  * Remember how long a blocking dialog has been on screen. Once it has
8381
8603
  * outlived DIALOG_PARKED_NOTIFY_MS — the auto-dismiss did not take, or the
@@ -8384,14 +8606,24 @@ export class Daemon extends EventEmitter {
8384
8606
  * is safe. Identity is the pattern, so the delivery gate and the runtime
8385
8607
  * scanner (same canonical list) never reset each other's clock.
8386
8608
  */
8609
+ /** #2897: the notice out for a dialog is out of date (its `owner` ended): the fleet takes it back. */
8610
+ retireParkedNotice(owner) {
8611
+ if (this.parkedNoticeOut !== owner)
8612
+ return;
8613
+ this.parkedNoticeOut = null;
8614
+ this.emit("dialog_unparked", { name: this.name });
8615
+ }
8387
8616
  trackDialogParked(dialog) {
8388
8617
  if (!dialog) {
8389
8618
  if (this.dialogParkedSince !== 0)
8390
8619
  this.logger.info({ dialog: this.dialogParkedKey }, "Blocking dialog is gone from the pane");
8391
8620
  this.dialogParkedSince = 0;
8392
8621
  this.dialogParkedKey = null;
8622
+ this.heldReasonLoggedKey = null;
8393
8623
  this.dialogParkedInteractionEpisode = null;
8394
8624
  this.dialogParkedReported = false;
8625
+ // #2897: whatever it posted is out of date — the fleet takes its notices back.
8626
+ this.retireParkedNotice("parked");
8395
8627
  return;
8396
8628
  }
8397
8629
  const key = Daemon.dialogKey(dialog);
@@ -8403,6 +8635,9 @@ export class Daemon extends EventEmitter {
8403
8635
  if (episode !== null)
8404
8636
  this.dialogParkedInteractionEpisode = episode;
8405
8637
  if (this.dialogParkedSince === 0 || this.dialogParkedKey !== key) {
8638
+ // #2897: a different dialog replaced the one being tracked — that one's notices are out of date.
8639
+ if (this.dialogParkedSince !== 0)
8640
+ this.retireParkedNotice("parked");
8406
8641
  this.dialogParkedSince = Date.now();
8407
8642
  this.dialogParkedKey = key;
8408
8643
  this.dialogParkedReported = false;
@@ -8414,8 +8649,10 @@ export class Daemon extends EventEmitter {
8414
8649
  this.dialogParkedReported = true;
8415
8650
  if (!this.dialogParkedReported && Date.now() - this.dialogParkedSince >= (unrecognised ? 0 : DIALOG_PARKED_NOTIFY_MS)) {
8416
8651
  this.dialogParkedReported = true;
8652
+ this.parkedNoticeOut = "parked";
8417
8653
  this.logger.warn({ dialog: dialog.description, parkedForMs: Date.now() - this.dialogParkedSince }, "CLI dialog is still on screen — not auto-answering, reporting for a human");
8418
8654
  this.emit("dialog_parked", { name: this.name, description: dialog.description, holdOnly: dialog.holdOnly === true,
8655
+ ...(dialog.parkedAdvice ? { advice: dialog.parkedAdvice } : {}),
8419
8656
  ...(episode !== null && interaction.kind ? { kind: interaction.kind, episode, backend: this.config.backend ?? "claude-code" } : {}),
8420
8657
  ...(unrecognised ? { unrecognised: true, question: this.unrecognisedPrompt?.shape.question ?? null } : {}) });
8421
8658
  }
@@ -10785,96 +11022,12 @@ export class Daemon extends EventEmitter {
10785
11022
  reply(written, undefined);
10786
11023
  return;
10787
11024
  }
10788
- if (tool === "export_web_mod") {
10789
- // The fleet builds the file (whole, or in parts); it is written here, in the working directory.
10790
- const first = await ask(args);
10791
- if (first.error) {
10792
- reply(null, first.error);
10793
- return;
10794
- }
10795
- let failed;
10796
- const firstResult = (first.result && typeof first.result === "object" ? first.result : {});
10797
- const got = await receiveFiles(firstResult, async (payload) => {
10798
- const r = await ask(payload);
10799
- if (r.error) {
10800
- failed = r.error;
10801
- return { error: r.error };
10802
- }
10803
- return (r.result && typeof r.result === "object" ? r.result : {});
10804
- }, { name: MOD_FILE, idKey: "transfer", what: "export", maxParts: MOD_MAX_PARTS });
10805
- if (failed) {
10806
- reply(null, failed);
10807
- return;
10808
- }
10809
- if ("error" in got) {
10810
- reply(got, undefined);
10811
- return;
10812
- }
10813
- const written = await writeModFile(got.files, { workspace: this.config.working_directory, configuredWorkspace: this.config.working_directory, stateDir: getAgendHome(), instance: this.name });
10814
- if ("error" in written) {
10815
- reply(null, `export_web_mod: ${written.error}`);
10816
- return;
10817
- }
10818
- reply({ ...written, ...(firstResult.effects_omitted ? { effects_omitted: firstResult.effects_omitted } : {}) }, undefined);
10819
- return;
10820
- }
10821
- if (tool === "propose_web_mod") {
10822
- const read = await readAgentFile(args.file, { workspace: this.config.working_directory, configuredWorkspace: this.config.working_directory, stateDir: getAgendHome(), instance: this.name, maxBytes: MOD_UPLOAD_MAX_BYTES });
10823
- const named = typeof args.file === "string" ? args.file.slice(0, 200) : "the file argument";
10824
- if (!read.ok) {
10825
- reply(null, `propose_web_mod: ${named} — ${read.reason}`);
10826
- return;
10827
- }
10828
- let failed;
10829
- const answer = await sendAssetInParts("mod", read.bytes, async (payload) => {
10830
- const r = await ask(payload);
10831
- if (r.error) {
10832
- failed = r.error;
10833
- return { error: r.error };
10834
- }
10835
- return (r.result && typeof r.result === "object" ? r.result : {});
10836
- });
10837
- reply(failed ? null : answer, failed);
10838
- return;
10839
- }
10840
11025
  if (tool === "office_pack_check" || tool === "office_pack_upload") {
10841
11026
  // #2692: the folder (or the manifest's files) is read here, on the agent's side; the fleet gets bytes in parts.
10842
11027
  const answer = await sendPackSources(args, { workspace: this.config.working_directory, configuredWorkspace: this.config.working_directory, stateDir: getAgendHome(), instance: this.name }, async (payload) => { const r = await ask(payload); return r.error ? { error: "fleet", message: r.error } : (r.result && typeof r.result === "object" ? r.result : {}); });
10843
11028
  reply(answer, undefined);
10844
11029
  return;
10845
11030
  }
10846
- if (tool === "upload_web_effect") {
10847
- const owner = { fence: this.launchFenceEpoch, spawn: this.spawnGeneration, boot: this.bootId, ipc: this.ipcServer };
10848
- const current = () => !!owner.ipc && owner.ipc === this.ipcServer && owner.fence === this.launchFenceEpoch && owner.spawn === this.spawnGeneration && owner.boot === this.bootId && !this.deliveryWritesStopping && !this.startupAborted;
10849
- if (!current()) {
10850
- reply(null, "upload_web_effect: launch no longer current");
10851
- return;
10852
- }
10853
- const read = await readAgentEffect(args.dir, { workspace: this.config.working_directory, configuredWorkspace: this.config.working_directory, stateDir: getAgendHome(), instance: this.name });
10854
- if (!current()) {
10855
- reply(null, "upload_web_effect: launch no longer current");
10856
- return;
10857
- }
10858
- if (!read.ok) {
10859
- reply(null, `upload_web_effect: ${read.reason}`);
10860
- return;
10861
- }
10862
- let failed;
10863
- const answer = await sendEffectInParts(read.files, async (payload) => {
10864
- if (!current()) {
10865
- failed = "upload_web_effect: launch no longer current";
10866
- return { error: failed };
10867
- }
10868
- const r = await ask(payload);
10869
- if (r.error) {
10870
- failed = r.error;
10871
- return { error: r.error };
10872
- }
10873
- return (r.result && typeof r.result === "object" ? r.result : {});
10874
- });
10875
- reply(failed ? null : answer, failed);
10876
- return;
10877
- }
10878
11031
  if (tool !== "set_web_theme_asset") {
10879
11032
  const r = await ask(args);
10880
11033
  reply(r.result, r.error);
@@ -11335,6 +11488,13 @@ export class Daemon extends EventEmitter {
11335
11488
  this.recoveryNativeWork = false;
11336
11489
  this.recoveryCancelled = false;
11337
11490
  this.dialogAnswers = null;
11491
+ // #2897: a relaunched CLI shows none of the old dialogs — whatever notice was out for one is out of date.
11492
+ // A dialog still tracked when the new CLI shows it again is reported afresh (its old notice is gone).
11493
+ if (this.parkedNoticeOut) {
11494
+ this.parkedNoticeOut = null;
11495
+ this.dialogParkedReported = false;
11496
+ this.emit("dialog_unparked", { name: this.name });
11497
+ }
11338
11498
  this.cancelCapacityNudge("the instance was stopped, paused or respawned");
11339
11499
  this.lastErrorEpisode = null;
11340
11500
  this.nudgeBaselines = null;
@@ -11563,6 +11723,8 @@ export class Daemon extends EventEmitter {
11563
11723
  // as this backend's and resumed by the next start.
11564
11724
  this.startupAdmission?.();
11565
11725
  this.setAsideForeignSession();
11726
+ // A crash-loop-resume boot should attempt resume even though skipResume is false:
11727
+ // bootCrashLoopResume set at boot means we had a crash loop but try resume first.
11566
11728
  const attemptedResume = !this.skipResume;
11567
11729
  // A resume launch may get a longer budget than a fresh one (kiro: the
11568
11730
  // conversation must come back from the backend before anything paints).
@@ -11647,6 +11809,20 @@ export class Daemon extends EventEmitter {
11647
11809
  }
11648
11810
  this.setSessionAside(proven ? "cli_reported_no_conversation" : "resume_failed_repeatedly");
11649
11811
  this.skipResume = true;
11812
+ // If this was a crash-loop-resume attempt, the session is now proven
11813
+ // broken. Persist resumeFailed so the next restart goes fresh without
11814
+ // attempting resume again.
11815
+ if (this.bootCrashLoopResume) {
11816
+ try {
11817
+ writeFileSync(join(this.instanceDir, "crash-state.json"), JSON.stringify({
11818
+ resumeFailed: true,
11819
+ failedAt: Date.now(),
11820
+ }));
11821
+ }
11822
+ catch { /* best effort */ }
11823
+ this.logger.warn("Crash-loop-resume: session abandoned — next boot will go fresh");
11824
+ this.bootCrashLoopResume = false;
11825
+ }
11650
11826
  }
11651
11827
  // A fresh start that also failed retries once, as before. It never clears
11652
11828
  // a session: nothing about a failed fresh launch says the stored
@@ -11664,9 +11840,37 @@ export class Daemon extends EventEmitter {
11664
11840
  this.startupAdmission?.();
11665
11841
  throw new Error("CLI failed to start after retry");
11666
11842
  }
11843
+ // Fresh fallback succeeded. Clear any resumeFailed marker written during this
11844
+ // boot's setSessionAside path, so the next normal restart is not forced fresh.
11845
+ // NOTE: bootCrashLoopResume is already false here (cleared by setSessionAside path),
11846
+ // so check the marker directly rather than relying on the flag.
11847
+ try {
11848
+ const p = join(this.instanceDir, "crash-state.json");
11849
+ if (existsSync(p)) {
11850
+ const s = JSON.parse(readFileSync(p, "utf-8"));
11851
+ if (s?.resumeFailed === true)
11852
+ unlinkSync(p);
11853
+ }
11854
+ }
11855
+ catch { /* best effort */ }
11667
11856
  }
11668
11857
  else if (attemptedResume) {
11669
11858
  resumedSuccessfully = true;
11859
+ if (this.bootCrashLoopResume) {
11860
+ // Resume succeeded after a crash loop — clear any stale resumeFailed marker
11861
+ // that might have been written by a concurrent write or a previous attempt,
11862
+ // so the next restart is not forced fresh.
11863
+ this.bootCrashLoopResume = false;
11864
+ try {
11865
+ const p = join(this.instanceDir, "crash-state.json");
11866
+ if (existsSync(p)) {
11867
+ const s = JSON.parse(readFileSync(p, "utf-8"));
11868
+ if (s?.resumeFailed === true)
11869
+ unlinkSync(p);
11870
+ }
11871
+ }
11872
+ catch { /* best effort */ }
11873
+ }
11670
11874
  this.clearUnprovenResumeFailures();
11671
11875
  // A resume needs the backend: its success is positive proof the backend
11672
11876
  // is reachable again (a fresh prompt is local and proves nothing).
@@ -12306,7 +12510,7 @@ export class Daemon extends EventEmitter {
12306
12510
  */
12307
12511
  currentWhatsNew(pending) {
12308
12512
  const plan = planWhatsNew({ record: readWhatsNewRecord(this.instanceDir), fresh: false, version: pending.version,
12309
- roles: this.whatsNewRoles(), tools: this.whatsNewTools(), canvasWorker: readCanvasWorkerEnabled(join(this.instanceDir, "..", "..")) });
12513
+ roles: this.whatsNewRoles(), tools: this.whatsNewTools() });
12310
12514
  if (!plan || "silent" in plan) {
12311
12515
  this.pendingWhatsNew = null;
12312
12516
  if (plan)
@@ -12327,13 +12531,13 @@ export class Daemon extends EventEmitter {
12327
12531
  }
12328
12532
  }
12329
12533
  /**
12330
- * #2800: the tools this instance's agend MCP server lists now — the same filters (tool set, dashboard switches, the
12331
- * Canvas Worker switch) — so a notice names only changes to tools it has.
12534
+ * #2800: the tools this instance's agend MCP server lists now — the same filters (tool set, dashboard switches) — so a
12535
+ * notice names only changes to tools it has.
12332
12536
  */
12333
12537
  whatsNewTools() {
12334
12538
  const allowed = toolsFor(resolveToolSet(this.config, this.name));
12335
12539
  const listed = withoutFeatures(TOOLS.filter(t => allowed.has(t.name)), dashboardFeaturesOff(dashboardAccessOf(this.config)));
12336
- return new Set(toolsWithCanvasWorker(readCanvasWorkerEnabled(join(this.instanceDir, "..", "..")), listed).map(t => t.name));
12540
+ return new Set(listed.map(t => t.name));
12337
12541
  }
12338
12542
  /**
12339
12543
  * #2163: who this instance is to the what's-new notices — a ClassicBot room's agent, General, or a fleet worker (with
@@ -12351,7 +12555,7 @@ export class Daemon extends EventEmitter {
12351
12555
  prepareWhatsNew(fresh, version = agendVersion()) {
12352
12556
  try {
12353
12557
  const plan = planWhatsNew({ record: readWhatsNewRecord(this.instanceDir), fresh, version, roles: this.whatsNewRoles(),
12354
- tools: this.whatsNewTools(), canvasWorker: readCanvasWorkerEnabled(join(this.instanceDir, "..", "..")) });
12558
+ tools: this.whatsNewTools() });
12355
12559
  if (!plan) {
12356
12560
  this.pendingWhatsNew = null;
12357
12561
  return;
@@ -12671,7 +12875,10 @@ export class Daemon extends EventEmitter {
12671
12875
  continue;
12672
12876
  lastDialog = dialog;
12673
12877
  this.noteCodexReserveDialog(dialog, pane);
12878
+ this.noteLiveIncidentDialog(dialog, pane);
12674
12879
  cleanReadyPolls = 0;
12880
+ // Before the parked clock below claims it: a hold seen for the first time is said once, with its reason (#2893).
12881
+ const firstSight = this.dialogParkedKey !== Daemon.dialogKey(dialog);
12675
12882
  // Start the parked clock for every delivery-blocking dialog, exact
12676
12883
  // ones included: if the auto-dismiss keeps failing here, the human
12677
12884
  // report should count from first sight, not from the runtime scan.
@@ -12700,14 +12907,16 @@ export class Daemon extends EventEmitter {
12700
12907
  }
12701
12908
  if (dialog.holdOnly) {
12702
12909
  // Recognised but deliberately not answered (see RuntimeDialog.holdOnly).
12703
- if (this.dialogParkedKey !== Daemon.dialogKey(dialog)) {
12704
- this.logger.warn({ description: dialog.description }, "Startup dialog held — not auto-answering, deliveries stay blocked");
12910
+ if (firstSight) {
12911
+ const reason = this.heldReasonLoggedKey !== Daemon.dialogKey(dialog) ? Daemon.heldReasonOf(dialog, pane) : {};
12912
+ if (Object.keys(reason).length)
12913
+ this.heldReasonLoggedKey = Daemon.dialogKey(dialog);
12914
+ this.logger.warn({ description: dialog.description, ...reason }, "Startup dialog held — not auto-answering, deliveries stay blocked");
12705
12915
  }
12706
12916
  this.trackDialogParked(dialog);
12707
12917
  matched = true;
12708
12918
  break;
12709
12919
  }
12710
- this.logger.info(`Dismissing startup dialog: ${dialog.description}`);
12711
12920
  // Restart is exactly when inbound messages pile up, and nothing gates
12712
12921
  // delivery on `spawning`. Take the pane lock for the key sequence so a
12713
12922
  // queued message cannot be pasted into a half-dismissed trust dialog.
@@ -12720,6 +12929,7 @@ export class Daemon extends EventEmitter {
12720
12929
  // a pane that is no longer this launch's is not read or keyed.
12721
12930
  if (!owned())
12722
12931
  return false;
12932
+ let answeredPane = pane;
12723
12933
  if (dialog.inputBlocked) {
12724
12934
  const currentPane = await this.tmux.capturePane();
12725
12935
  this.startupAdmission?.();
@@ -12728,7 +12938,10 @@ export class Daemon extends EventEmitter {
12728
12938
  return false;
12729
12939
  if (!Daemon.dialogMatches(dialog, currentPane))
12730
12940
  return false;
12941
+ answeredPane = currentPane;
12731
12942
  }
12943
+ // Said of the screen actually answered — the one re-read under the lock (#2896 review).
12944
+ this.logger.info(Daemon.answerDetailOf(dialog, answeredPane), `Dismissing startup dialog: ${dialog.description}`);
12732
12945
  if (dialog.autoResolutionKey) {
12733
12946
  attemptedSafetyChoices.add(dialog.autoResolutionKey);
12734
12947
  this.claimAutoDialog(dialog);