@songsid/agend 2.3.1 → 2.3.2-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 (338) hide show
  1. package/THIRD_PARTY_NOTICES.md +40 -0
  2. package/dist/agent-cli.js +21 -2
  3. package/dist/agent-cli.js.map +1 -1
  4. package/dist/agent-endpoint.js +5 -0
  5. package/dist/agent-endpoint.js.map +1 -1
  6. package/dist/antigravity-usage-scan.d.ts +58 -0
  7. package/dist/antigravity-usage-scan.js +257 -0
  8. package/dist/antigravity-usage-scan.js.map +1 -0
  9. package/dist/antigravity-usage-worker.d.ts +1 -0
  10. package/dist/antigravity-usage-worker.js +10 -0
  11. package/dist/antigravity-usage-worker.js.map +1 -0
  12. package/dist/auth-api.js +4 -0
  13. package/dist/auth-api.js.map +1 -1
  14. package/dist/authority/authority-state.d.ts +4 -0
  15. package/dist/authority/authority-state.js +16 -10
  16. package/dist/authority/authority-state.js.map +1 -1
  17. package/dist/authority/session-authorizations.d.ts +7 -1
  18. package/dist/authority/session-authorizations.js +6 -4
  19. package/dist/authority/session-authorizations.js.map +1 -1
  20. package/dist/authority/web-authority.d.ts +19 -6
  21. package/dist/authority/web-authority.js +22 -8
  22. package/dist/authority/web-authority.js.map +1 -1
  23. package/dist/backend/kiro-identity.d.ts +64 -0
  24. package/dist/backend/kiro-identity.js +101 -0
  25. package/dist/backend/kiro-identity.js.map +1 -1
  26. package/dist/backend/kiro-retired-model.d.ts +62 -0
  27. package/dist/backend/kiro-retired-model.js +160 -0
  28. package/dist/backend/kiro-retired-model.js.map +1 -0
  29. package/dist/backend/kiro.d.ts +71 -2
  30. package/dist/backend/kiro.js +352 -11
  31. package/dist/backend/kiro.js.map +1 -1
  32. package/dist/backend/types.d.ts +59 -0
  33. package/dist/backend/types.js.map +1 -1
  34. package/dist/backend-signin-guide.d.ts +1 -0
  35. package/dist/backend-signin-guide.js +9 -0
  36. package/dist/backend-signin-guide.js.map +1 -0
  37. package/dist/backend-signin-pending.d.ts +10 -0
  38. package/dist/backend-signin-pending.js +44 -0
  39. package/dist/backend-signin-pending.js.map +1 -0
  40. package/dist/browser-backend-signins.d.ts +56 -0
  41. package/dist/browser-backend-signins.js +189 -0
  42. package/dist/browser-backend-signins.js.map +1 -0
  43. package/dist/cache-ledger.d.ts +38 -2
  44. package/dist/cache-ledger.js +133 -8
  45. package/dist/cache-ledger.js.map +1 -1
  46. package/dist/cache-prices.d.ts +2 -1
  47. package/dist/cache-prices.js +5 -2
  48. package/dist/cache-prices.js.map +1 -1
  49. package/dist/cache-service.d.ts +68 -0
  50. package/dist/cache-service.js +355 -13
  51. package/dist/cache-service.js.map +1 -1
  52. package/dist/channel/adapters/discord.d.ts +13 -0
  53. package/dist/channel/adapters/discord.js +103 -16
  54. package/dist/channel/adapters/discord.js.map +1 -1
  55. package/dist/channel/mcp-tools.js +34 -7
  56. package/dist/channel/mcp-tools.js.map +1 -1
  57. package/dist/channel/tool-router.js +2 -1
  58. package/dist/channel/tool-router.js.map +1 -1
  59. package/dist/channel/types.d.ts +11 -0
  60. package/dist/classic-channel-manager.d.ts +11 -0
  61. package/dist/classic-channel-manager.js +39 -1
  62. package/dist/classic-channel-manager.js.map +1 -1
  63. package/dist/cli.js +21 -2
  64. package/dist/cli.js.map +1 -1
  65. package/dist/config-validator.d.ts +7 -1
  66. package/dist/config-validator.js +92 -12
  67. package/dist/config-validator.js.map +1 -1
  68. package/dist/config.d.ts +19 -0
  69. package/dist/config.js +31 -1
  70. package/dist/config.js.map +1 -1
  71. package/dist/cpu-quota.d.ts +23 -0
  72. package/dist/cpu-quota.js +86 -0
  73. package/dist/cpu-quota.js.map +1 -0
  74. package/dist/daemon.d.ts +57 -3
  75. package/dist/daemon.js +330 -50
  76. package/dist/daemon.js.map +1 -1
  77. package/dist/dashboard-permissions.d.ts +20 -6
  78. package/dist/dashboard-permissions.js +58 -9
  79. package/dist/dashboard-permissions.js.map +1 -1
  80. package/dist/delivery-outbox.d.ts +12 -1
  81. package/dist/delivery-outbox.js +67 -15
  82. package/dist/delivery-outbox.js.map +1 -1
  83. package/dist/doctor.d.ts +3 -0
  84. package/dist/doctor.js +11 -1
  85. package/dist/doctor.js.map +1 -1
  86. package/dist/event-log.d.ts +5 -0
  87. package/dist/event-log.js +13 -0
  88. package/dist/event-log.js.map +1 -1
  89. package/dist/fleet-manager.d.ts +212 -0
  90. package/dist/fleet-manager.js +914 -111
  91. package/dist/fleet-manager.js.map +1 -1
  92. package/dist/fleet-process-handlers.d.ts +1 -0
  93. package/dist/fleet-process-handlers.js +9 -1
  94. package/dist/fleet-process-handlers.js.map +1 -1
  95. package/dist/forged-envelope-scan.d.ts +9 -0
  96. package/dist/forged-envelope-scan.js +30 -5
  97. package/dist/forged-envelope-scan.js.map +1 -1
  98. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +5 -2
  99. package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +14 -1
  100. package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +28 -5
  101. package/dist/general-knowledge/skills/web-appearance/SKILL.md +31 -8
  102. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +138 -59
  103. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +145 -61
  104. package/dist/guest-seen-people.d.ts +38 -0
  105. package/dist/guest-seen-people.js +78 -0
  106. package/dist/guest-seen-people.js.map +1 -0
  107. package/dist/instance-config-impact.js +14 -6
  108. package/dist/instance-config-impact.js.map +1 -1
  109. package/dist/instance-lifecycle.d.ts +20 -0
  110. package/dist/instance-lifecycle.js +71 -13
  111. package/dist/instance-lifecycle.js.map +1 -1
  112. package/dist/kiro-db-reader.d.ts +5 -0
  113. package/dist/kiro-db-reader.js +18 -0
  114. package/dist/kiro-db-reader.js.map +1 -1
  115. package/dist/kiro-engine-status.d.ts +2 -0
  116. package/dist/kiro-engine-status.js +9 -0
  117. package/dist/kiro-engine-status.js.map +1 -1
  118. package/dist/kiro-migrate.d.ts +49 -0
  119. package/dist/kiro-migrate.js +105 -0
  120. package/dist/kiro-migrate.js.map +1 -0
  121. package/dist/kiro-transcript-lane.d.ts +2 -0
  122. package/dist/kiro-transcript-lane.js.map +1 -1
  123. package/dist/kiro-transcript-worker.js +1 -0
  124. package/dist/kiro-transcript-worker.js.map +1 -1
  125. package/dist/kiro-tui-forensic.d.ts +7 -0
  126. package/dist/kiro-tui-forensic.js +110 -0
  127. package/dist/kiro-tui-forensic.js.map +1 -0
  128. package/dist/kiro-usage-run.d.ts +7 -0
  129. package/dist/kiro-usage-run.js +39 -0
  130. package/dist/kiro-usage-run.js.map +1 -0
  131. package/dist/kiro-usage-scan.d.ts +49 -0
  132. package/dist/kiro-usage-scan.js +223 -0
  133. package/dist/kiro-usage-scan.js.map +1 -0
  134. package/dist/kiro-usage-worker.d.ts +1 -0
  135. package/dist/kiro-usage-worker.js +10 -0
  136. package/dist/kiro-usage-worker.js.map +1 -0
  137. package/dist/kiro-usage.d.ts +58 -0
  138. package/dist/kiro-usage.js +310 -0
  139. package/dist/kiro-usage.js.map +1 -0
  140. package/dist/locale.js +120 -12
  141. package/dist/locale.js.map +1 -1
  142. package/dist/log-service.js +1 -1
  143. package/dist/log-service.js.map +1 -1
  144. package/dist/login-url.d.ts +4 -0
  145. package/dist/login-url.js +22 -0
  146. package/dist/login-url.js.map +1 -0
  147. package/dist/mac-auto-restore.d.ts +117 -0
  148. package/dist/mac-auto-restore.js +294 -0
  149. package/dist/mac-auto-restore.js.map +1 -0
  150. package/dist/needs-you-hub.d.ts +2 -1
  151. package/dist/needs-you-hub.js +1 -1
  152. package/dist/needs-you-hub.js.map +1 -1
  153. package/dist/needs-you.d.ts +8 -2
  154. package/dist/needs-you.js +6 -0
  155. package/dist/needs-you.js.map +1 -1
  156. package/dist/notice-delivery.d.ts +16 -0
  157. package/dist/notice-delivery.js +35 -0
  158. package/dist/notice-delivery.js.map +1 -0
  159. package/dist/office-share-page.d.ts +16 -0
  160. package/dist/office-share-page.js +85 -0
  161. package/dist/office-share-page.js.map +1 -0
  162. package/dist/office-share-service.d.ts +91 -0
  163. package/dist/office-share-service.js +176 -0
  164. package/dist/office-share-service.js.map +1 -0
  165. package/dist/office-share.d.ts +258 -0
  166. package/dist/office-share.js +549 -0
  167. package/dist/office-share.js.map +1 -0
  168. package/dist/opencode-usage-scan.d.ts +28 -0
  169. package/dist/opencode-usage-scan.js +92 -0
  170. package/dist/opencode-usage-scan.js.map +1 -0
  171. package/dist/opencode-usage-worker.d.ts +1 -0
  172. package/dist/opencode-usage-worker.js +10 -0
  173. package/dist/opencode-usage-worker.js.map +1 -0
  174. package/dist/outbound-handlers.js +22 -2
  175. package/dist/outbound-handlers.js.map +1 -1
  176. package/dist/outbound-schemas.d.ts +28 -2
  177. package/dist/outbound-schemas.js +29 -2
  178. package/dist/outbound-schemas.js.map +1 -1
  179. package/dist/public-web-gateway.js +16 -2
  180. package/dist/public-web-gateway.js.map +1 -1
  181. package/dist/quickstart-api.js +13 -2
  182. package/dist/quickstart-api.js.map +1 -1
  183. package/dist/quickstart.js +2 -1
  184. package/dist/quickstart.js.map +1 -1
  185. package/dist/remote-card-answer.d.ts +83 -0
  186. package/dist/remote-card-answer.js +208 -0
  187. package/dist/remote-card-answer.js.map +1 -0
  188. package/dist/reply-triggers.d.ts +2 -0
  189. package/dist/reply-triggers.js +3 -0
  190. package/dist/reply-triggers.js.map +1 -0
  191. package/dist/settings-api.d.ts +28 -0
  192. package/dist/settings-api.js +148 -17
  193. package/dist/settings-api.js.map +1 -1
  194. package/dist/settings-change.js +54 -17
  195. package/dist/settings-change.js.map +1 -1
  196. package/dist/settings-effect.js +12 -3
  197. package/dist/settings-effect.js.map +1 -1
  198. package/dist/settings-instance-patch.js +4 -1
  199. package/dist/settings-instance-patch.js.map +1 -1
  200. package/dist/setup-form.d.ts +1 -1
  201. package/dist/setup-form.js +55 -6
  202. package/dist/setup-form.js.map +1 -1
  203. package/dist/setup-host.d.ts +9 -0
  204. package/dist/setup-host.js +127 -6
  205. package/dist/setup-host.js.map +1 -1
  206. package/dist/setup-signin-assets.d.ts +2 -0
  207. package/dist/setup-signin-assets.js +6 -0
  208. package/dist/setup-signin-assets.js.map +1 -0
  209. package/dist/setup-wizard.js +2 -0
  210. package/dist/setup-wizard.js.map +1 -1
  211. package/dist/start-attempt.d.ts +9 -5
  212. package/dist/start-attempt.js +27 -8
  213. package/dist/start-attempt.js.map +1 -1
  214. package/dist/start-failure-chat.d.ts +4 -0
  215. package/dist/start-failure-chat.js +23 -6
  216. package/dist/start-failure-chat.js.map +1 -1
  217. package/dist/start-failure-notice.d.ts +4 -0
  218. package/dist/start-failure-notice.js +8 -0
  219. package/dist/start-failure-notice.js.map +1 -1
  220. package/dist/start-failure.d.ts +25 -0
  221. package/dist/start-failure.js +75 -2
  222. package/dist/start-failure.js.map +1 -1
  223. package/dist/structured-signin-transport.d.ts +8 -0
  224. package/dist/structured-signin-transport.js +79 -0
  225. package/dist/structured-signin-transport.js.map +1 -0
  226. package/dist/structured-signin.d.ts +131 -0
  227. package/dist/structured-signin.js +388 -0
  228. package/dist/structured-signin.js.map +1 -0
  229. package/dist/tool-permissions.js +12 -0
  230. package/dist/tool-permissions.js.map +1 -1
  231. package/dist/topic-commands.js +6 -2
  232. package/dist/topic-commands.js.map +1 -1
  233. package/dist/transcript-sources.d.ts +26 -2
  234. package/dist/transcript-sources.js +103 -40
  235. package/dist/transcript-sources.js.map +1 -1
  236. package/dist/turn-reply-guard.d.ts +25 -0
  237. package/dist/turn-reply-guard.js +111 -12
  238. package/dist/turn-reply-guard.js.map +1 -1
  239. package/dist/types.d.ts +46 -9
  240. package/dist/ui/backend-signin.js +68 -12
  241. package/dist/ui/chat-ambient.js +1 -1
  242. package/dist/ui/chat-effects.js +4 -4
  243. package/dist/ui/chat-render.js +5 -2
  244. package/dist/ui/chat-strings.js +4 -4
  245. package/dist/ui/chat-thread.js +2 -2
  246. package/dist/ui/first-run.js +2 -2
  247. package/dist/ui/fleet-canvas-view.js +114 -0
  248. package/dist/ui/fleet-canvas.js +15 -77
  249. package/dist/ui/fleet-office-editor.js +94 -14
  250. package/dist/ui/fleet-office-layout.js +235 -0
  251. package/dist/ui/fleet-office-model.js +12 -6
  252. package/dist/ui/fleet-office-overview.js +167 -0
  253. package/dist/ui/fleet-office-pack.d.ts +44 -0
  254. package/dist/ui/fleet-office-pack.js +352 -37
  255. package/dist/ui/fleet-office-skin.js +220 -36
  256. package/dist/ui/fleet-office-store.js +112 -7
  257. package/dist/ui/fleet-office-view.js +155 -0
  258. package/dist/ui/fleet-office-zones.js +264 -42
  259. package/dist/ui/fleet-office.js +957 -160
  260. package/dist/ui/fleet-usage.js +26 -21
  261. package/dist/ui/office-share-dialog.js +145 -0
  262. package/dist/ui/office-share-page.js +164 -0
  263. package/dist/ui/panel-chat.js +9 -2
  264. package/dist/ui/panel-details.js +2 -2
  265. package/dist/ui/panel-fleet.js +11 -77
  266. package/dist/ui/panel-log.js +4 -4
  267. package/dist/ui/panel-needs.js +4 -4
  268. package/dist/ui/panel-settings.js +608 -167
  269. package/dist/ui/search-dialog.js +26 -7
  270. package/dist/ui/settings-appearance.js +82 -29
  271. package/dist/ui/settings-apply.js +42 -7
  272. package/dist/ui/settings-dialogs.js +76 -23
  273. package/dist/ui/settings-draft.js +51 -0
  274. package/dist/ui/settings-guests.js +255 -79
  275. package/dist/ui/settings-learn.js +2 -2
  276. package/dist/ui/settings-model.js +16 -6
  277. package/dist/ui/settings-registry.js +146 -0
  278. package/dist/ui/settings-strings.js +116 -44
  279. package/dist/ui/settings-web-authority.js +34 -23
  280. package/dist/ui/shared/app-i18n.js +18 -2
  281. package/dist/ui/shared/app-nav.js +10 -5
  282. package/dist/ui/shared/app-prefs.js +80 -2
  283. package/dist/ui/shared/app-route.js +39 -5
  284. package/dist/ui/shared/app-shell.js +73 -10
  285. package/dist/ui/shared/app-stream.js +3 -1
  286. package/dist/ui/shared/app.css +307 -17
  287. package/dist/ui/shared/app.js +3 -0
  288. package/dist/ui/shared/effect-stock-trust.js +1 -1
  289. package/dist/ui/shared/identity-color.js +4 -4
  290. package/dist/ui/shared/office/cozy.png +0 -0
  291. package/dist/ui/shared/side-panel.js +95 -0
  292. package/dist/ui/shared/signin.js +2 -2
  293. package/dist/ui/shared/ui-dialog.js +6 -1
  294. package/dist/ui/shared/ui-icons.js +2 -0
  295. package/dist/ui/shared/web-authorize.js +13 -12
  296. package/dist/ui/structured-signin.js +115 -0
  297. package/dist/usage-report.d.ts +20 -0
  298. package/dist/usage-report.js +54 -17
  299. package/dist/usage-report.js.map +1 -1
  300. package/dist/usage-rollup.d.ts +9 -1
  301. package/dist/usage-rollup.js +20 -4
  302. package/dist/usage-rollup.js.map +1 -1
  303. package/dist/web-api.d.ts +26 -2
  304. package/dist/web-api.js +361 -5
  305. package/dist/web-api.js.map +1 -1
  306. package/dist/web-auth.js +8 -2
  307. package/dist/web-auth.js.map +1 -1
  308. package/dist/web-backend-login.d.ts +1 -7
  309. package/dist/web-backend-login.js +2 -24
  310. package/dist/web-backend-login.js.map +1 -1
  311. package/dist/web-experimental.d.ts +1 -1
  312. package/dist/web-experimental.js +1 -1
  313. package/dist/web-experimental.js.map +1 -1
  314. package/dist/web-mod-agent.js +2 -2
  315. package/dist/web-mod-agent.js.map +1 -1
  316. package/dist/web-office-acts.d.ts +173 -0
  317. package/dist/web-office-acts.js +285 -0
  318. package/dist/web-office-acts.js.map +1 -0
  319. package/dist/web-office-skins.d.ts +109 -0
  320. package/dist/web-office-skins.js +299 -0
  321. package/dist/web-office-skins.js.map +1 -0
  322. package/dist/web-office-store.d.ts +8 -2
  323. package/dist/web-office-store.js +77 -14
  324. package/dist/web-office-store.js.map +1 -1
  325. package/dist/web-settings-patch.d.ts +8 -1
  326. package/dist/web-settings-patch.js +65 -5
  327. package/dist/web-settings-patch.js.map +1 -1
  328. package/dist/web-shell-routes.d.ts +4 -2
  329. package/dist/web-shell-routes.js +17 -7
  330. package/dist/web-shell-routes.js.map +1 -1
  331. package/dist/working-recovery.d.ts +104 -0
  332. package/dist/working-recovery.js +298 -0
  333. package/dist/working-recovery.js.map +1 -0
  334. package/dist/working-state-journal.d.ts +5 -1
  335. package/dist/working-state-journal.js +31 -3
  336. package/dist/working-state-journal.js.map +1 -1
  337. package/package.json +3 -2
  338. package/templates/windows-shortcuts.ps1 +155 -140
package/dist/daemon.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { dashboardAccessOf } from "./dashboard-permissions.js";
1
2
  import { agentWhatsNew, agendVersion, planWhatsNew, readWhatsNewRecord, recordWhatsNew, whatsNewNotice } from "./agent-whats-new.js";
2
3
  import { join, dirname, resolve } from "node:path";
3
4
  import { checkoutTarget, readCheckouts, releaseCandidate, reposRoot, verifyWorktree, writeCheckouts } from "./repo-worktree-guard.js";
@@ -42,11 +43,12 @@ import { parseReplyButtons, replyButtonsFallbackText } from "./reply-buttons.js"
42
43
  import { HangDetector } from "./hang-detector.js";
43
44
  import { writeSecretFile } from "./secret-file.js";
44
45
  import { PaneWriteLock } from "./pane-write-lock.js";
46
+ import { answerRemoteCard } from "./remote-card-answer.js";
45
47
  import { buildFleetInstructions } from "./instructions.js";
46
48
  import { formatHumanInboundMessage, prepareDeliveryWire, steerInboundEnvelope, BTW_INBOUND_PREFIX, humanBodyHash, humanOwnerObjectId } from "./human-delivery-receipts.js";
47
49
  import { formatCrossInstanceInboundMessage, renderCrossInstanceHandoffMetadata, } from "./cross-instance-envelope.js";
48
50
  import { agendMessageInInput, bottomRowIsReady, inputAreaText, inputShowsPastedText, pastedTextSignature, pasteLeftInInput, strandedAgendMessageInInput } from "./pane-input-residue.js";
49
- import { TurnReplyGuard } from "./turn-reply-guard.js";
51
+ import { TurnReplyGuard, formatReplyDropDetection, turnExpectsPersonReply } from "./turn-reply-guard.js";
50
52
  import { t } from "./locale.js";
51
53
  import { MuseUsageRelay, clearMuseUsageSnapshot } from "./muse-usage-relay.js";
52
54
  import { deliveryContentDigest } from "./delivery-outbox.js";
@@ -86,6 +88,8 @@ const WEB_THEME_IPC = Object.freeze({
86
88
  apply_web_effect: "fleet_apply_web_effect",
87
89
  remove_web_effect: "fleet_remove_web_effect",
88
90
  delete_web_effect: "fleet_delete_web_effect",
91
+ // #2578: its own character in the Office view — the args as they are (the fleet checks them).
92
+ office_view: "fleet_office_view", office_set: "fleet_office_set",
89
93
  });
90
94
  /** The largest picture any slot may take (a card slot's ceiling; the fleet checks the limit in force): read no more. */
91
95
  const WEB_THEME_ASSET_MAX_BYTES = ASSET_UPLOAD_MAX_BYTES;
@@ -570,6 +574,8 @@ export const OWN_SWITCH_WAIT_MS = 6_000;
570
574
  const OWN_SWITCH_SETTLE_MS = 3_000;
571
575
  /** A blocking dialog still on screen this long after first being seen is reported for a human. */
572
576
  const DIALOG_PARKED_NOTIFY_MS = 60_000;
577
+ /** #2675: a card a person can answer from the chat is posted once it has been on screen, unchanged, this long. */
578
+ const REMOTE_CARD_STABLE_MS = 1_500;
573
579
  /** #1691: a prompt no table knows must sit unchanged this long (monotonic) before it holds deliveries and is reported. */
574
580
  export const UNRECOGNISED_PROMPT_STABLE_MS = 60_000;
575
581
  /** #1691: the rows under a prompt a backend without a composer reader shows its ready chrome in (status bar, hint). */
@@ -1245,6 +1251,8 @@ export class Daemon extends EventEmitter {
1245
1251
  * skipResume — the #1209 gate reads this, never the resettable flag.
1246
1252
  */
1247
1253
  bootSkippedResume = false;
1254
+ recoveryCrashLoopFresh = false;
1255
+ recoveryFreshChoice = false;
1248
1256
  startupAborted = false;
1249
1257
  /** First time the current on-screen blocking dialog was seen (0 = none); drives the parked report. */
1250
1258
  dialogParkedSince = 0;
@@ -1252,6 +1260,12 @@ export class Daemon extends EventEmitter {
1252
1260
  dialogParkedKey = null;
1253
1261
  dialogParkedInteractionEpisode = null;
1254
1262
  dialogParkedReported = false;
1263
+ /** #2675: the card on screen that a person can answer from the chat — what it asks, since when, and whether it was posted. */
1264
+ remoteCardShown = null;
1265
+ /** #2675: when (performance.now, taken before its capture) the newest read the card tracker used was started. */
1266
+ remoteCardReadAt = -Infinity;
1267
+ /** #2675: an answer from the chat is being keyed in (one at a time). */
1268
+ remoteCardAnswering = false;
1255
1269
  /**
1256
1270
  * #1691: the prompt at the bottom of the pane that no backend table knows — its block, since when (performance.now)
1257
1271
  * it has been unchanged, and whether that has lasted UNRECOGNISED_PROMPT_STABLE_MS. Updated by the runtime scan.
@@ -1747,6 +1761,11 @@ export class Daemon extends EventEmitter {
1747
1761
  return `${ep.type}:${ep.pattern.source}`;
1748
1762
  }
1749
1763
  /** Read the occurrence baseline so a lifecycle-owned restart can carry it forward. */
1764
+ /** #2648: kiro only — what a model error on the current launch should do (see kiro-retired-model.ts). */
1765
+ kiroModelError() {
1766
+ const backend = this.backend;
1767
+ return typeof backend?.onModelError === "function" ? backend.onModelError() : null;
1768
+ }
1750
1769
  getErrorPatternOccurrenceCount(type, pattern) {
1751
1770
  return this.lastErrorCount.get(`${type}:${pattern.source}`) ?? 0;
1752
1771
  }
@@ -1823,6 +1842,9 @@ export class Daemon extends EventEmitter {
1823
1842
  this.spawnGate = spawnGate;
1824
1843
  this.stormWindow = stormWindow;
1825
1844
  this.backendOutage = backendOutage;
1845
+ // The live process owns its adopted configuration. Fleet defaults may be re-resolved in place after a save;
1846
+ // sharing that object would activate cold changes before the normal restart/apply boundary.
1847
+ this.config = structuredClone(config);
1826
1848
  if (!rootLogger)
1827
1849
  throw new Error("Daemon requires a shared root logger");
1828
1850
  this.runtimeIdentity ??= {
@@ -2203,6 +2225,7 @@ export class Daemon extends EventEmitter {
2203
2225
  }
2204
2226
  async startAdmitted() {
2205
2227
  this.assertSessionOomAdmission();
2228
+ this.recoveryBootWorkPending = true;
2206
2229
  ensureInstanceDir(this.instanceDir);
2207
2230
  writeFileSync(join(this.instanceDir, "daemon.pid"), String(process.pid));
2208
2231
  this.logger.info(`Starting ${this.name}`);
@@ -2211,6 +2234,8 @@ export class Daemon extends EventEmitter {
2211
2234
  // even when the JSON is corrupt so it cannot remain forever.
2212
2235
  const crashStatePath = join(this.instanceDir, "crash-state.json");
2213
2236
  this.bootSkippedResume = false;
2237
+ this.recoveryCrashLoopFresh = false;
2238
+ this.recoveryFreshChoice = false;
2214
2239
  try {
2215
2240
  if (existsSync(crashStatePath)) {
2216
2241
  const content = readFileSync(crashStatePath, "utf-8");
@@ -2219,6 +2244,10 @@ export class Daemon extends EventEmitter {
2219
2244
  if (state?.resumeDisabled === true) {
2220
2245
  this.skipResume = true;
2221
2246
  this.bootSkippedResume = true;
2247
+ this.recoveryCrashLoopFresh = (state.reason === undefined || state.reason === "crash_loop")
2248
+ && Number.isInteger(state.crashesInWindow) && state.crashesInWindow >= 3
2249
+ && Number.isFinite(state.lastCrashAt) && state.lastCrashAt > 0;
2250
+ this.recoveryFreshChoice = !this.recoveryCrashLoopFresh;
2222
2251
  this.logger.warn("Previous crash loop detected — starting without resume");
2223
2252
  }
2224
2253
  }
@@ -2367,12 +2396,19 @@ export class Daemon extends EventEmitter {
2367
2396
  // #1209: conditional continuation for the turn interrupted by the last
2368
2397
  // restart, if the gates allow. Detached like the warmup notice; the gate
2369
2398
  // consumes its one-shot marker first and never throws into boot.
2370
- void this.maybeResumeInterruptedTurn().catch(() => { });
2399
+ const legacyResume = this.maybeResumeInterruptedTurn().catch(() => "unknown");
2371
2400
  // Warmup: wait for CLI idle, then trigger steering reload — but only when
2372
2401
  // the instructions actually changed since the agent last saw them.
2373
2402
  // Skipping the no-op reload saves 10-30s of agent time on every restart
2374
2403
  // where instructions are unchanged.
2375
- void this.runWarmupInstructionNotice();
2404
+ const warmup = this.runWarmupInstructionNotice();
2405
+ const recoveryOwner = JSON.stringify(this.interactionOwner());
2406
+ void Promise.allSettled([legacyResume, warmup]).then(() => {
2407
+ if (JSON.stringify(this.interactionOwner()) === recoveryOwner) {
2408
+ this.recoveryBootWorkPending = false;
2409
+ this.recoveryBootSettled = true;
2410
+ }
2411
+ });
2376
2412
  if (!this.config.lightweight) {
2377
2413
  // 3. Pipe-pane for prompt detection. Rotate first so a ballooned log from a
2378
2414
  // previous stuck splash (hundreds of MB of ANSI frames) is truncated before
@@ -2384,7 +2420,7 @@ export class Daemon extends EventEmitter {
2384
2420
  // (statusline transcript); codex/kiro/opencode read their CLI's own
2385
2421
  // conversation store via a pluggable source. Backends with no known
2386
2422
  // source stay inert exactly as before.
2387
- this.transcriptMonitor = this.buildTranscriptMonitor(createTranscriptSource(this.config.backend ?? "claude-code", this.config.working_directory, this.credentialProfileStore()));
2423
+ this.transcriptMonitor = this.buildTranscriptMonitor(createTranscriptSource(this.config.backend ?? "claude-code", this.config.working_directory, this.credentialProfileStore(), () => this.backend?.transcriptLaunch?.() ?? null));
2388
2424
  const transcriptMonitor = this.transcriptMonitor;
2389
2425
  const transcriptFence = this.launchFenceEpoch;
2390
2426
  // Baseline in the isolate before this daemon accepts new work. A
@@ -3716,6 +3752,7 @@ export class Daemon extends EventEmitter {
3716
3752
  if (!answersBackedOff)
3717
3753
  this.dialogAnswers = null;
3718
3754
  this.trackDialogParked(blockingSeen);
3755
+ this.trackRemoteCard(blockingSeen, pane, captureMono);
3719
3756
  if (blockingSeen)
3720
3757
  return; // held dialog: skip error checks this cycle
3721
3758
  this.evaluateErrorPatterns(pane, patterns, readyPattern, Date.now(), busyPattern);
@@ -3974,6 +4011,7 @@ export class Daemon extends EventEmitter {
3974
4011
  clearPendingDeliveries(fleetEpoch) {
3975
4012
  // FleetManager calls this synchronously before sendEscape. Cancel only the
3976
4013
  // current human turn; leave already-forwarded tool requests to settle.
4014
+ this.recoveryCancelled = true;
3977
4015
  this.turnReplyGuard.cancelByUser();
3978
4016
  // #1241: a cancelled turn never confirms — its next edge completes it as
3979
4017
  // intentional. Drop a pending confirmation so it cannot promote afterwards.
@@ -3985,6 +4023,7 @@ export class Daemon extends EventEmitter {
3985
4023
  this.deliveryEpoch = fleetEpoch === undefined
3986
4024
  ? this.deliveryEpoch + 1
3987
4025
  : Math.max(this.deliveryEpoch, fleetEpoch);
4026
+ this.cancelCapacityNudge("the user cancelled");
3988
4027
  this.publishWorkingState("cancelled");
3989
4028
  this.logger.info({ deliveryEpoch: this.deliveryEpoch }, "Pending delivery queue cleared by user cancel");
3990
4029
  }
@@ -4703,6 +4742,88 @@ export class Daemon extends EventEmitter {
4703
4742
  getInteractionSnapshot() {
4704
4743
  return this.interactionObservation.snapshot(this.interactionOwner());
4705
4744
  }
4745
+ ownedWorkingSession = null;
4746
+ recoveryBootSettled = false;
4747
+ recoveryBootWorkPending = false;
4748
+ recoveryLegacyCovered = false;
4749
+ recoveryNativeWork = false;
4750
+ recoveryContinuationId = null;
4751
+ recoveryCancelled = false;
4752
+ workingRecoveryInputOwner = null;
4753
+ noteLegacyRestartReminder() { this.recoveryLegacyCovered = true; }
4754
+ getWorkingRecoveryState() {
4755
+ const observation = this.getWorkingStateObservation();
4756
+ const owner = JSON.stringify(observation.owner), ownsInput = this.workingRecoveryInputOwner === owner;
4757
+ return { owner, configuration: observation.configuration,
4758
+ sessionId: this.ownedWorkingSession, idle: (ownsInput || this.instanceState === "idle") && this.pasteQueueDepth === 0,
4759
+ paused: this.isPaused, stopped: this.isSessionOomStopped(), legacyPending: !this.recoveryBootSettled,
4760
+ legacyCovered: this.recoveryLegacyCovered, nativeWork: !ownsInput && this.recoveryNativeWork,
4761
+ freshChoice: this.recoveryFreshChoice || this.isNewSession && !this.bootSkippedResume, crashLoopFresh: this.recoveryCrashLoopFresh,
4762
+ cancelled: this.recoveryCancelled, background: this.workingBackgroundObservation };
4763
+ }
4764
+ /** Local system work: no user turn, channel reply target, wake or replay of an original request. */
4765
+ async deliverWorkingRecovery(content, delivery, admitted) {
4766
+ const tmux = this.tmux, owner = JSON.stringify(this.interactionOwner()), epoch = this.deliveryEpoch;
4767
+ const current = () => this.tmux === tmux && owner === JSON.stringify(this.interactionOwner())
4768
+ && this.deliveryEpoch === epoch && !this.deliveryWritesStopping && !this.runtimeMonitorsFrozen
4769
+ && !this.spawning && !this.startupAborted && !this.isPaused && !this.healthCheckPaused
4770
+ && this.processStatus === "running" && this.pasteQueueDepth === 0
4771
+ && (this.workingRecoveryInputOwner === owner || this.instanceState === "idle" && !this.recoveryNativeWork)
4772
+ && this.recoveryBootSettled && !this.recoveryCancelled && !this.isSessionOomStopped() && admitted();
4773
+ let begun = false;
4774
+ try {
4775
+ await this.paneWriteLock.run(async () => {
4776
+ if (!tmux || !current())
4777
+ return;
4778
+ const pane = await tmux.capturePane();
4779
+ const input = this.backend?.readInputRow?.(pane);
4780
+ if (!current() || !input || input.text !== "" || input.collapsedPastes !== 0 || input.queued
4781
+ || !this.paneAuthoritativelyIdle(pane) || this.inputTransientInPane(pane)
4782
+ || this.backend?.backgroundWorkRunning?.(pane) === true)
4783
+ return;
4784
+ if (await tmux.getPaneInputMode?.() !== "raw" || !current())
4785
+ return;
4786
+ const evidence = await this.durableAttemptEvidence(tmux.getWindowId(), false, false);
4787
+ if (!current())
4788
+ return;
4789
+ const signature = this.submissionSignature(content), baseline = this.paneEvidence(pane, signature);
4790
+ const attempt = { ...delivery, contentSha256: deliveryContentDigest(content) };
4791
+ if (!this.beginDurableDelivery(attempt, evidence))
4792
+ return;
4793
+ begun = true;
4794
+ // Own PTY input immediately produces a working observation. It must not
4795
+ // cancel this same fenced write; external queue/cancel/owner guards stay.
4796
+ this.workingRecoveryInputOwner = owner;
4797
+ // Conservatively commit possible input BEFORE paste, not merely before Enter.
4798
+ // A process death at any later boundary must never replay this continuation.
4799
+ if (!this.deliveryOutbox?.markEnterStarted(delivery.deliveryId, this.bootId, delivery.attemptNo))
4800
+ return;
4801
+ if (!current() || !await tmux.pasteBuffer(content, { guard: current }) || !current())
4802
+ return;
4803
+ await new Promise(resolve => setTimeout(resolve, NORMAL_ENTER_SETTLE_MS));
4804
+ if (!current() || !await this.sendDeliveryEnterAfterAvailability("working-recovery", current, attempt))
4805
+ return;
4806
+ const proof = await this.confirmSubmitted(signature, baseline);
4807
+ const committed = this.deliveryOutbox.complete(delivery.deliveryId, this.bootId, delivery.attemptNo, proof === "submitted" ? "delivered" : "uncertain", "local continuation Enter attempted once; no replay");
4808
+ if (committed && proof === "submitted" && current()) {
4809
+ this.recoveryContinuationId = delivery.deliveryId;
4810
+ this.publishWorkingState("execution");
4811
+ }
4812
+ });
4813
+ }
4814
+ catch { /* The durable fence below owns ambiguity; never retry a begun write. */ }
4815
+ finally {
4816
+ if (this.workingRecoveryInputOwner === owner) {
4817
+ this.workingRecoveryInputOwner = null;
4818
+ if (this.tmux === tmux && owner === JSON.stringify(this.interactionOwner()))
4819
+ this.recoveryNativeWork = true;
4820
+ }
4821
+ if (begun)
4822
+ this.deliveryOutbox?.complete(delivery.deliveryId, this.bootId, delivery.attemptNo, "uncertain", "local continuation may have reached input; not replayed");
4823
+ else
4824
+ this.deliveryOutbox?.retryBeforeBegin(delivery.deliveryId, this.bootId, delivery.attemptNo, "working recovery prerequisite changed before input", 5_000);
4825
+ }
4826
+ }
4706
4827
  workingTurnDelivery = null;
4707
4828
  workingBackgroundObservation = null;
4708
4829
  /** Pure metadata read: never discovers sessions, captures a pane or reads backend files. */
@@ -4714,7 +4835,8 @@ export class Daemon extends EventEmitter {
4714
4835
  owner: this.interactionOwner(), backend,
4715
4836
  configuration: createHash("sha256").update(JSON.stringify([backend, this.config.working_directory, profile])).digest("hex"),
4716
4837
  executionState: this.processStatus === "running" ? this.isPaused ? "paused" : this.instanceState : null,
4717
- processStatus: this.getProcessStatus(), sessionId: null, backendTurnId: null,
4838
+ processStatus: this.getProcessStatus(), sessionId: this.ownedWorkingSession, backendTurnId: null,
4839
+ ...(this.recoveryContinuationId ? { recoveryDeliveryId: this.recoveryContinuationId } : {}),
4718
4840
  backgroundWork: this.workingBackgroundObservation,
4719
4841
  turn: turn ? { generation: turn.generation,
4720
4842
  deliveryId: this.workingTurnDelivery?.generation === turn.generation ? this.workingTurnDelivery.deliveryId : null,
@@ -5174,6 +5296,9 @@ export class Daemon extends EventEmitter {
5174
5296
  if (unblockedTurn && unblockedTurn.phase === "awaiting")
5175
5297
  this.maybeProxyReplyOnTurnEnd(pane);
5176
5298
  }
5299
+ if (snapshot.state === "working" && this.recoveryBootSettled
5300
+ && this.workingRecoveryInputOwner !== JSON.stringify(this.interactionOwner()))
5301
+ this.recoveryNativeWork = true;
5177
5302
  this.instanceState = snapshot.state;
5178
5303
  this.acceptedInstanceSnapshot = { ...snapshot };
5179
5304
  // Reuse an already-captured pane and the backend's pure parser, never capture for the journal.
@@ -5290,14 +5415,21 @@ export class Daemon extends EventEmitter {
5290
5415
  // Its late success must not re-arm the turn that the user just cancelled.
5291
5416
  if (!this.isDeliveryEpochCurrent(deliveryEpoch))
5292
5417
  return;
5293
- // Channel turns only. A cross-instance inbound (from_instance, empty
5294
- // chat_id) does not update lastChatId, so its proxy reply would land in
5295
- // whatever USER topic spoke to this instance last — the wrong audience for
5296
- // a task result, and a stale one (sol's review of #515).
5297
- // A web message on a fleet with no chat platform has no chat_id (its reply goes to the web chat through the fleet's
5298
- // web-only sink), but it is a person waiting for an answer all the same: the guard arms for it (alpha.2).
5299
- if (meta.from_instance || (!meta.chat_id && meta.source !== "web"))
5418
+ // Channel turns started by a person. Cross-instance, schedules (silent or
5419
+ // not), and AgEnD system notices must not expect a reply to a person (#2544):
5420
+ // a proxy or recovery notice would land in the last USER topic, or flag a
5421
+ // heartbeat that was told not to report. A web message on a fleet with no
5422
+ // chat platform has no chat_id (its reply goes to the web chat through the
5423
+ // fleet's web-only sink), but it is a person waiting for an answer all the
5424
+ // same: the guard arms for it (alpha.2).
5425
+ if (!turnExpectsPersonReply(meta)) {
5426
+ this.logger.debug({
5427
+ source: meta.source || null,
5428
+ user: meta.user || null,
5429
+ from_instance: meta.from_instance || null,
5430
+ }, "Reply guard skipped — turn was not started by a person");
5300
5431
  return;
5432
+ }
5301
5433
  // The last non-empty line of what we pasted: everything on screen after it
5302
5434
  // is the agent's own output.
5303
5435
  const inboundMarker = deliveredText.split(/\r?\n/).map(l => l.trim()).filter(Boolean).pop();
@@ -5314,7 +5446,8 @@ export class Daemon extends EventEmitter {
5314
5446
  deliveredText,
5315
5447
  deliveredAt,
5316
5448
  }, this.pendingWork.lastInboundTimestamp());
5317
- this.workingTurnDelivery = { generation: this.turnReplyGuard.snapshot().generation, deliveryId: meta.delivery_id || null };
5449
+ const armed = this.turnReplyGuard.snapshot();
5450
+ this.workingTurnDelivery = { generation: armed.generation, deliveryId: meta.delivery_id || null };
5318
5451
  this.publishWorkingState("turn_started");
5319
5452
  // #1209: persist the in-flight turn with a seam checkpoint. A marker
5320
5453
  // that survives a restart is an interrupted turn by construction —
@@ -5323,7 +5456,9 @@ export class Daemon extends EventEmitter {
5323
5456
  // the interruption. Reads are windowed/bounded (see turn-resume.ts).
5324
5457
  // A resumed continuation never re-arms (P3a): episode-once means a crash
5325
5458
  // during the continuation boots clean instead of chaining resumes.
5326
- if (!meta.resumedContinuationOf) {
5459
+ // #2544: a reply that settled before this arm already answered the person —
5460
+ // writing a marker would re-inject a continuation for a finished turn.
5461
+ if (!meta.resumedContinuationOf && !armed.completionDelivered) {
5327
5462
  const resumeBackend = this.config.backend ?? this.backend?.binaryName ?? this.runtimeIdentity?.backend;
5328
5463
  const resumeCwd = this.config.working_directory ?? "";
5329
5464
  writeInFlightTurnMarker(this.instanceDir, {
@@ -5371,6 +5506,7 @@ export class Daemon extends EventEmitter {
5371
5506
  const marker = consumeInFlightTurnMarker(this.instanceDir);
5372
5507
  if (!marker)
5373
5508
  return "none";
5509
+ this.recoveryLegacyCovered = true;
5374
5510
  // P1 fence: capture before any await. A cancel (deliveryEpoch),
5375
5511
  // respawn (spawnGeneration) or stop/pause (launchFenceEpoch) during the
5376
5512
  // grace hold must stale this gate — the push below rechecks, and the
@@ -5736,8 +5872,8 @@ export class Daemon extends EventEmitter {
5736
5872
  generation: turn.generation,
5737
5873
  reason,
5738
5874
  recoveryStarted: true,
5875
+ message: formatReplyDropDetection(turn, reason),
5739
5876
  });
5740
- void this.deliverDaemonReply(t("inst.reply_drop_retrying"), "replydrop", "Reply-drop status", turn.target, true);
5741
5877
  this.queueReplyRecoveryPrompt(turn);
5742
5878
  }
5743
5879
  reportReplyDropWithoutRetry(turn, reason) {
@@ -5747,8 +5883,8 @@ export class Daemon extends EventEmitter {
5747
5883
  generation: turn.generation,
5748
5884
  reason,
5749
5885
  recoveryStarted: false,
5886
+ message: formatReplyDropDetection(turn, reason),
5750
5887
  });
5751
- void this.deliverDaemonReply(t("inst.reply_drop_unknown"), "replydrop", "Unconfirmed reply status", turn.target, true);
5752
5888
  }
5753
5889
  queueReplyRecoveryPrompt(turn) {
5754
5890
  const deliveryEpoch = this.deliveryEpoch;
@@ -5840,13 +5976,22 @@ export class Daemon extends EventEmitter {
5840
5976
  correlationId: turn.target.correlationId,
5841
5977
  generation: turn.generation,
5842
5978
  reason,
5979
+ message: formatReplyDropDetection(turn, reason),
5843
5980
  });
5844
- const now = Date.now();
5845
- if (this.lastReplyDropWarningAt !== 0
5846
- && now - this.lastReplyDropWarningAt < REPLY_DROP_WARNING_COOLDOWN_MS)
5847
- return;
5848
- this.lastReplyDropWarningAt = now;
5849
- void this.deliverDaemonReply(t("inst.reply_drop_unrecovered"), "replydropwarn", "Reply-drop recovery warning", turn.target, true);
5981
+ this.deliverReplyDropNotice("inst.reply_drop_unrecovered", "replydropwarn", "Reply-drop recovery warning", turn, reason, true);
5982
+ }
5983
+ /** Channel notice for a reply-drop. `rateLimit` shares the per-instance cooldown (#2544). */
5984
+ deliverReplyDropNotice(key, kind, title, turn, reason, rateLimit) {
5985
+ if (rateLimit) {
5986
+ const now = Date.now();
5987
+ if (this.lastReplyDropWarningAt !== 0
5988
+ && now - this.lastReplyDropWarningAt < REPLY_DROP_WARNING_COOLDOWN_MS) {
5989
+ this.logger.debug({ reason, kind }, "Reply-drop channel notice rate-limited");
5990
+ return;
5991
+ }
5992
+ this.lastReplyDropWarningAt = now;
5993
+ }
5994
+ void this.deliverDaemonReply(t(key), kind, title, turn.target, true);
5850
5995
  }
5851
5996
  /** Relay the pane's final text to the channel, marked as a daemon proxy reply. */
5852
5997
  async sendProxyReply(pane, target) {
@@ -6444,6 +6589,7 @@ export class Daemon extends EventEmitter {
6444
6589
  }
6445
6590
  /** Stop every runtime poller/watcher while preserving IPC and daemon state. */
6446
6591
  freezeRuntimeMonitors() {
6592
+ this.cancelCapacityNudge("the instance was stopped, paused or respawned");
6447
6593
  this.runtimeMonitorsFrozen = true;
6448
6594
  this.launchFenceEpoch++;
6449
6595
  this.interactionObservation.reset(this.interactionOwner());
@@ -6542,6 +6688,10 @@ export class Daemon extends EventEmitter {
6542
6688
  if (update.tool_progress === null || ["off", "standard", "verbose"].includes(String(update.tool_progress))) {
6543
6689
  this.updateToolProgress(update.tool_progress === null ? undefined : update.tool_progress);
6544
6690
  }
6691
+ if (typeof update.reply_triggers === "boolean")
6692
+ this.config.reply_triggers = update.reply_triggers;
6693
+ else if (update.reply_triggers === null)
6694
+ delete this.config.reply_triggers;
6545
6695
  if (typeof update.reply_completion_guard === "boolean") {
6546
6696
  this.config.reply_completion_guard = update.reply_completion_guard;
6547
6697
  }
@@ -6556,6 +6706,10 @@ export class Daemon extends EventEmitter {
6556
6706
  this.config.warm_cap = update.warm_cap;
6557
6707
  else if (update.warm_cap === null)
6558
6708
  delete this.config.warm_cap;
6709
+ if (typeof update.resume_interrupted_work === "boolean")
6710
+ this.config.resume_interrupted_work = update.resume_interrupted_work;
6711
+ else if (update.resume_interrupted_work === null)
6712
+ delete this.config.resume_interrupted_work;
6559
6713
  const nextAutoPause = typeof update.auto_pause_after === "number"
6560
6714
  && Number.isFinite(update.auto_pause_after)
6561
6715
  && update.auto_pause_after >= 0
@@ -6591,6 +6745,10 @@ export class Daemon extends EventEmitter {
6591
6745
  else if (update.board_cards_at_once === null)
6592
6746
  delete this.config.board_cards_at_once;
6593
6747
  // The fleet reads it at each tool call; kept current here for the next spawn's tool menu.
6748
+ if (typeof update.office_access === "boolean")
6749
+ this.config.office_access = update.office_access;
6750
+ else if (update.office_access === null)
6751
+ delete this.config.office_access;
6594
6752
  if (update.dashboard && typeof update.dashboard === "object" && !dashboardAccessProblem(update.dashboard))
6595
6753
  this.config.dashboard = update.dashboard;
6596
6754
  else if (update.dashboard === null)
@@ -7820,15 +7978,18 @@ export class Daemon extends EventEmitter {
7820
7978
  && dialogs.some(other => other !== dialog && Daemon.dialogMatches(other, pane)))
7821
7979
  continue;
7822
7980
  this.trackDialogParked(dialog);
7981
+ this.trackRemoteCard(dialog, pane, probeMono);
7823
7982
  return { state: "dialog", dialog };
7824
7983
  }
7825
7984
  this.trackDialogParked(null);
7985
+ this.trackRemoteCard(null, pane, probeMono);
7826
7986
  return { state: "clear" };
7827
7987
  }
7828
7988
  /**
7829
7989
  * Arm the "keep going" nudge for the error occurrence just reported (`pattern` must be the one the lifecycle was
7830
7990
  * handed). The nudge is sent once, `delayMs` from now on the MONOTONIC clock (a wall-clock step changes nothing),
7831
- * and only if nothing has changed by then — see {@link tickCapacityNudge}. Returns false when there is nothing to arm.
7991
+ * and only if the failed turn still owns an empty idle composer — see {@link tickCapacityNudge}.
7992
+ * Cosmetic footer redraws are not new work. Returns false when there is nothing to arm.
7832
7993
  */
7833
7994
  armCapacityNudge(pattern, delayMs) {
7834
7995
  const episode = this.lastErrorEpisode;
@@ -7847,9 +8008,12 @@ export class Daemon extends EventEmitter {
7847
8008
  // lookahead on the counting pattern: whole-pane dedup would max at 1 and miss later hits. See #1215.)
7848
8009
  if (!codexLiveRowMatches(episode.pane, pattern))
7849
8010
  return false;
8011
+ this.cancelCapacityNudge("the instance has work of its own");
7850
8012
  this.capacityNudge = {
7851
8013
  pane: episode.pane, pattern, spawn: episode.spawn, fence: episode.fence,
7852
8014
  epoch: this.deliveryEpoch, dueAt: performance.now() + delayMs,
8015
+ episode,
8016
+ promiseTarget: null,
7853
8017
  };
7854
8018
  this.logger.info({ delayMs }, "Model-capacity nudge armed");
7855
8019
  return true;
@@ -7864,7 +8028,7 @@ export class Daemon extends EventEmitter {
7864
8028
  const nudge = this.capacityNudge, episode = this.lastErrorEpisode;
7865
8029
  const tmux = this.tmux, attempt = this.launchAttempt, boot = this.bootId;
7866
8030
  this.cancelCapacityNudge("retries exhausted");
7867
- if (!nudge || !episode || !tmux)
8031
+ if (!nudge || !episode || nudge.episode !== episode || !tmux)
7868
8032
  return null;
7869
8033
  const window = tmux.getWindowId?.();
7870
8034
  const current = () => this.tmux === tmux && tmux.getWindowId?.() === window
@@ -7872,9 +8036,7 @@ export class Daemon extends EventEmitter {
7872
8036
  && episode === this.lastErrorEpisode && this.isDeliveryEpochCurrent(nudge.epoch)
7873
8037
  && !this.startupAborted && !this.spawning && !this.isPaused
7874
8038
  && this.getProcessStatus() === "running" && this.instanceState === "idle" && this.pasteQueueDepth === 0;
7875
- const accepts = (pane) => pane === nudge.pane && this.paneAuthoritativelyIdle(pane)
7876
- && this.backend?.isPeriodicRedrawIdlePane?.(pane) === true
7877
- && codexLiveRowMatches(pane, nudge.pattern) && !this.backend?.backgroundWorkRunning?.(pane);
8039
+ const accepts = (pane) => this.capacityNudgePaneCurrent(nudge, pane);
7878
8040
  const revalidate = () => this.paneWriteLock.run(async () => {
7879
8041
  if (!current())
7880
8042
  return false;
@@ -7896,10 +8058,35 @@ export class Daemon extends EventEmitter {
7896
8058
  }
7897
8059
  /** Drop an armed nudge (the user stopped / paused the instance, or the lifecycle gave up on it). */
7898
8060
  cancelCapacityNudge(reason) {
7899
- if (!this.capacityNudge)
8061
+ const nudge = this.capacityNudge;
8062
+ if (!nudge)
7900
8063
  return;
7901
8064
  this.capacityNudge = null;
7902
8065
  this.logger.info({ reason }, "Model-capacity nudge cancelled");
8066
+ this.reportCapacityNudgeCancelled(nudge, reason);
8067
+ }
8068
+ /** Only a dispatched channel promise needs a later cancellation notice. */
8069
+ markCapacityNudgePromised(target) {
8070
+ if (this.capacityNudge)
8071
+ this.capacityNudge.promiseTarget = target;
8072
+ }
8073
+ reportCapacityNudgeCancelled(nudge, reason) {
8074
+ const target = nudge.promiseTarget;
8075
+ if (!target)
8076
+ return;
8077
+ nudge.promiseTarget = null;
8078
+ this.emit("capacity_nudge_cancelled", { name: this.name, reason, target });
8079
+ }
8080
+ /**
8081
+ * A failed turn must still be the last live transcript item, with no new
8082
+ * error occurrence, draft, queue, busy status or background work. Codex's
8083
+ * own periodic-redraw idle classifier recognises its empty composer/footer;
8084
+ * never infer inactivity from byte equality or a cached idle state alone.
8085
+ */
8086
+ capacityNudgePaneCurrent(nudge, pane) {
8087
+ return this.paneAuthoritativelyIdle(pane) && this.backend?.isPeriodicRedrawIdlePane?.(pane) === true
8088
+ && !this.backend?.backgroundWorkRunning?.(pane) && codexLiveRowMatches(pane, nudge.pattern)
8089
+ && Daemon.countOccurrences(nudge.pattern, pane) === Daemon.countOccurrences(nudge.pattern, nudge.pane);
7903
8090
  }
7904
8091
  /** The first live composer of a spawn fixes what is history (see {@link nudgeBaselines}). */
7905
8092
  takeNudgeBaselines(pane, patterns) {
@@ -7922,8 +8109,8 @@ export class Daemon extends EventEmitter {
7922
8109
  }
7923
8110
  /**
7924
8111
  * One monitor poll's look at an armed nudge. It is CANCELLED — never postponed — when anything about the instance
7925
- * moved: a stop / pause / respawn (spawn generation or monitor epoch), a user cancel (delivery epoch), the pane no
7926
- * longer being exactly the one the error was seen on (it moved on by itself, a person typed, another dialog), the
8112
+ * moved: a stop / pause / respawn (spawn generation or monitor epoch), a user cancel (delivery epoch), the pane
8113
+ * no longer belonging to the failed turn (new output, a draft, another dialog), the
7927
8114
  * agent working, or a message waiting for delivery. Only when it is due and still untouched is it fired — once — and
7928
8115
  * the injection re-verifies everything under the pane-write lock.
7929
8116
  */
@@ -7933,10 +8120,11 @@ export class Daemon extends EventEmitter {
7933
8120
  return;
7934
8121
  const gone = nudge.spawn !== this.spawnGeneration || nudge.fence !== this.launchFenceEpoch ? "the instance was stopped, paused or respawned"
7935
8122
  : !this.isDeliveryEpochCurrent(nudge.epoch) ? "the user cancelled"
7936
- : this.isPaused || this.healthCheckPaused || this.getProcessStatus() === "stopped" ? "the instance is not running"
7937
- : pane !== nudge.pane ? "the screen changed"
8123
+ : nudge.episode !== this.lastErrorEpisode ? "the instance has work of its own"
8124
+ : this.isPaused || this.healthCheckPaused || this.getProcessStatus() === "stopped" ? "the instance is not running"
7938
8125
  : this.instanceState === "working" || this.pasteQueueDepth > 0 ? "the instance has work of its own"
7939
- : null;
8126
+ : !this.capacityNudgePaneCurrent(nudge, pane) ? "the failed turn is no longer idle with an empty input"
8127
+ : null;
7940
8128
  if (gone) {
7941
8129
  this.cancelCapacityNudge(gone);
7942
8130
  return;
@@ -7944,29 +8132,38 @@ export class Daemon extends EventEmitter {
7944
8132
  if (performance.now() < nudge.dueAt)
7945
8133
  return;
7946
8134
  this.capacityNudge = null; // fired: once per episode
7947
- void this.sendCapacityNudge(nudge).catch(err => this.logger.warn({ err: err.message }, "Model-capacity nudge failed"));
8135
+ void this.sendCapacityNudge(nudge).catch(err => {
8136
+ this.logger.warn({ err: err.message }, "Model-capacity nudge failed");
8137
+ this.reportCapacityNudgeCancelled(nudge, "the retry could not be submitted");
8138
+ });
7948
8139
  }
7949
8140
  async sendCapacityNudge(nudge) {
7950
8141
  // Everything the nudge was armed under, asked again after EVERY await that precedes a write — inside the paste
7951
8142
  // primitive too (its capture, its paste, its pause, its Enter, its retry): once any of it is false nothing more is
7952
8143
  // written. A message queued behind the lock, or the agent starting a turn, is "work of its own" like at the poll.
7953
8144
  const current = () => nudge.spawn === this.spawnGeneration && nudge.fence === this.launchFenceEpoch
8145
+ && nudge.episode === this.lastErrorEpisode
7954
8146
  && this.isDeliveryEpochCurrent(nudge.epoch) && !this.isPaused && !this.healthCheckPaused
7955
8147
  && !this.startupAborted && this.getProcessStatus() !== "stopped"
7956
8148
  && this.pasteQueueDepth === 0 && this.instanceState !== "working";
7957
8149
  // Exclusive with every delivery: nothing can paste a turn between the verdict below and the nudge.
7958
- await this.paneWriteLock.run(async () => {
7959
- if (!this.tmux)
7960
- return;
7961
- // ONE capture decides: it must be exactly the screen the error was seen on AND an idle composer. (A second picture
7962
- // vouching for the first — "same screen" here, "idle" there — would let a half-typed draft through.)
7963
- const sent = await this.submitSystemPaste(t("inst.codex_capacity_nudge_text"), "capacity-continue", {
7964
- current,
7965
- accept: pane => pane === nudge.pane && this.paneAuthoritativelyIdle(pane)
7966
- && codexLiveRowMatches(pane, nudge.pattern),
8150
+ let sent = false;
8151
+ try {
8152
+ await this.paneWriteLock.run(async () => {
8153
+ if (!this.tmux)
8154
+ return;
8155
+ // ONE capture decides failed-turn ownership and empty/idle input.
8156
+ sent = await this.submitSystemPaste(t("inst.codex_capacity_nudge_text"), "capacity-continue", {
8157
+ current,
8158
+ accept: pane => this.capacityNudgePaneCurrent(nudge, pane),
8159
+ });
8160
+ this.logger.info({ sent }, "Model-capacity nudge attempted");
7967
8161
  });
7968
- this.logger.info({ sent }, "Model-capacity nudge attempted");
7969
- });
8162
+ }
8163
+ finally {
8164
+ if (!sent)
8165
+ this.reportCapacityNudgeCancelled(nudge, "the retry could not be submitted");
8166
+ }
7970
8167
  }
7971
8168
  /** What identifies the request on screen: the backend's own identity when it has one, else the whole screen verbatim. */
7972
8169
  static screenOf(dialog, pane) {
@@ -8026,6 +8223,73 @@ export class Daemon extends EventEmitter {
8026
8223
  this.logger.warn({ dialog: dialog.description, attempts: episode.ignored, holdsDeliveries }, "Runtime dialog is ignoring AgEnD's answer — reporting for a human");
8027
8224
  this.emit("dialog_answer_ignored", { name: this.name, description: dialog.description, attempts: episode.ignored, holdsDeliveries });
8028
8225
  }
8226
+ /**
8227
+ * #2675: a held card a person can answer from the chat (the dialog's remoteCard reader) is posted once it has stayed
8228
+ * the same card for REMOTE_CARD_STABLE_MS — `remote_card` with what it asks; the fleet posts it with its buttons. When
8229
+ * it closes or another card replaces it, `remote_card_gone` retires that post. Moving through the card (ours or the
8230
+ * person's in the pane) is the same card: its identity has no cursor. `readAt` is when the pane's capture was started
8231
+ * (performance.now): a read that started before the newest one used here finishes late and says nothing about now —
8232
+ * it may neither retire a newer card nor bring an older one back (#2677 review).
8233
+ */
8234
+ trackRemoteCard(dialog, pane, readAt) {
8235
+ if (readAt < this.remoteCardReadAt)
8236
+ return;
8237
+ this.remoteCardReadAt = readAt;
8238
+ const card = dialog?.remoteCard?.(pane) ?? null;
8239
+ const shown = this.remoteCardShown;
8240
+ if (shown && shown.identity !== card?.identity) {
8241
+ this.remoteCardShown = null;
8242
+ if (shown.posted)
8243
+ this.emit("remote_card_gone", { name: this.name, identity: shown.identity });
8244
+ }
8245
+ if (!card)
8246
+ return;
8247
+ if (!this.remoteCardShown) {
8248
+ this.remoteCardShown = { identity: card.identity, since: performance.now(), posted: false };
8249
+ return;
8250
+ }
8251
+ if (this.remoteCardShown.posted || performance.now() - this.remoteCardShown.since < REMOTE_CARD_STABLE_MS)
8252
+ return;
8253
+ this.remoteCardShown.posted = true;
8254
+ this.logger.info({ kind: card.kind }, "A card a person can answer from the chat is on screen — posting it");
8255
+ this.emit("remote_card", { name: this.name, card });
8256
+ }
8257
+ /**
8258
+ * #2675: answer the card on screen from the chat, as the person chose: only while it is the card they saw
8259
+ * (`identity`), on a verified cursor, under the pane write lock (remote-card-answer.ts). Never logs the answer.
8260
+ */
8261
+ async answerRemoteCard(identity, action) {
8262
+ const tmux = this.tmux;
8263
+ const readers = this.runtimeDialogs().filter(dialog => dialog.remoteCard);
8264
+ if (!tmux || readers.length === 0)
8265
+ return { outcome: "stale" };
8266
+ if (this.remoteCardAnswering)
8267
+ return { outcome: "refused", reason: "another answer to this card is being keyed in" };
8268
+ this.remoteCardAnswering = true;
8269
+ try {
8270
+ const outcome = await answerRemoteCard({
8271
+ locked: fn => this.paneWriteLock.run(fn),
8272
+ // An unreadable screen (a failed capture, another spawn's pane) is unknown — never "the card closed".
8273
+ read: () => this.tmux === tmux ? tmux.capturePane().catch(() => null) : Promise.resolve(null),
8274
+ key: key => this.tmux !== tmux ? Promise.resolve(false) : key === "Space" ? tmux.sendKeys(" ") : tmux.sendSpecialKey(key),
8275
+ paste: text => this.tmux !== tmux ? Promise.resolve(false) : tmux.pasteBuffer(text),
8276
+ sleep: ms => new Promise(resolve => setTimeout(resolve, ms)),
8277
+ now: () => performance.now(),
8278
+ }, pane => {
8279
+ for (const dialog of readers) {
8280
+ const card = dialog.remoteCard(pane);
8281
+ if (card)
8282
+ return card;
8283
+ }
8284
+ return null;
8285
+ }, identity, action);
8286
+ this.logger.info({ action: action.kind, ...outcome }, "Answered a card from the chat");
8287
+ return outcome;
8288
+ }
8289
+ finally {
8290
+ this.remoteCardAnswering = false;
8291
+ }
8292
+ }
8029
8293
  /**
8030
8294
  * Remember how long a blocking dialog has been on screen. Once it has
8031
8295
  * outlived DIALOG_PARKED_NOTIFY_MS — the auto-dismiss did not take, or the
@@ -10233,7 +10497,9 @@ export class Daemon extends EventEmitter {
10233
10497
  // false and must not clear the window either.
10234
10498
  if (replyAttempt?.completionAction) {
10235
10499
  const snap = this.turnReplyGuard.snapshot();
10236
- if (snap?.generation === replyAttempt.generation && snap.phase === "awaiting"
10500
+ // generation 0 is a pre-arm token applied to the live generation (#2544).
10501
+ if (snap && (snap.generation === replyAttempt.generation || replyAttempt.generation === 0)
10502
+ && snap.phase === "awaiting"
10237
10503
  && snap.completionDelivered && !snap.cancelledByUser) {
10238
10504
  this.clearReplyGuardConfirm();
10239
10505
  }
@@ -10654,7 +10920,7 @@ export class Daemon extends EventEmitter {
10654
10920
  fleetCwd: process.cwd(),
10655
10921
  onWarning: (warning) => {
10656
10922
  if (warning.problem === "fleet_dir_fallback") {
10657
- this.logger.warn({ field: warning.field, path: warning.path, legacyPath: warning.legacyPath }, `${warning.field} file: ref is not under the instance's working directory (${warning.path}); read ${warning.legacyPath} from the fleet directory instead — make the path absolute or move the file. The fallback is removed in 2.2.`);
10923
+ this.logger.warn({ field: warning.field, path: warning.path, legacyPath: warning.legacyPath }, `${warning.field} file: ref is not under the instance's working directory (${warning.path}); read ${warning.legacyPath} from the fleet directory instead — make the path absolute or move the file. A later release will remove this fallback.`);
10658
10924
  const key = `${warning.field}:${warning.path}`;
10659
10925
  if (!this.promptFileFallbackNoticed.has(key)) {
10660
10926
  this.promptFileFallbackNoticed.add(key);
@@ -10742,7 +11008,7 @@ export class Daemon extends EventEmitter {
10742
11008
  mcpEnv.AGEND_TOOL_SET = toolSet;
10743
11009
  // The dashboard features the person switched off: out of the tool menu from this start (the fleet refuses them
10744
11010
  // at once, whatever the menu still shows).
10745
- const dashboardOff = dashboardFeaturesOff(this.config.dashboard);
11011
+ const dashboardOff = dashboardFeaturesOff(dashboardAccessOf(this.config));
10746
11012
  if (dashboardOff.length)
10747
11013
  mcpEnv.AGEND_DASHBOARD_OFF = dashboardOff.join(",");
10748
11014
  if (this.config.display_name)
@@ -10883,9 +11149,14 @@ export class Daemon extends EventEmitter {
10883
11149
  beginSpawn() {
10884
11150
  this.unclearOomStopped = false;
10885
11151
  this.publishWorkingState("before_spawn");
11152
+ this.recoveryContinuationId = null;
10886
11153
  this.workingBackgroundObservation = null;
11154
+ this.ownedWorkingSession = null;
11155
+ this.recoveryBootSettled = false;
11156
+ this.recoveryNativeWork = false;
11157
+ this.recoveryCancelled = false;
10887
11158
  this.dialogAnswers = null;
10888
- this.capacityNudge = null;
11159
+ this.cancelCapacityNudge("the instance was stopped, paused or respawned");
10889
11160
  this.lastErrorEpisode = null;
10890
11161
  this.nudgeBaselines = null;
10891
11162
  // A relaunch settles any outstanding quit watch: the CLI came back.
@@ -10916,6 +11187,8 @@ export class Daemon extends EventEmitter {
10916
11187
  this.spawnDepth = Math.max(0, this.spawnDepth - 1);
10917
11188
  if (this.spawnDepth > 0)
10918
11189
  return; // an enclosing spawn is still running
11190
+ if (!this.recoveryBootWorkPending)
11191
+ this.recoveryBootSettled = true;
10919
11192
  this.spawning = false;
10920
11193
  const resolve = this.resolveSpawnSettled;
10921
11194
  this.spawnSettled = null;
@@ -11174,7 +11447,7 @@ export class Daemon extends EventEmitter {
11174
11447
  // out loud — this start does NOT continue the previous conversation.
11175
11448
  this.logger.error({ attempts: failures }, "Giving up on resuming after repeated startup failures — starting fresh. "
11176
11449
  + "The previous conversation is NOT continued; its context is lost to this session.");
11177
- this.emit("context_lost", this.name, "resume_repeatedly_failed");
11450
+ this.emit("context_lost", this.name, "resume_repeatedly_failed", failures);
11178
11451
  }
11179
11452
  this.setSessionAside(proven ? "cli_reported_no_conversation" : "resume_failed_repeatedly");
11180
11453
  this.skipResume = true;
@@ -12369,6 +12642,13 @@ export class Daemon extends EventEmitter {
12369
12642
  const sid = this.backend?.getSessionId();
12370
12643
  if (!sid)
12371
12644
  return;
12645
+ if ((this.config.backend ?? "claude-code") === "claude-code" && Buffer.byteLength(sid) <= 256
12646
+ && !/[\u0000-\u001f\u007f]/.test(sid) && this.ownedWorkingSession !== sid) {
12647
+ this.ownedWorkingSession = sid;
12648
+ // Reuse this existing owned checkpoint. An idle instance may have no
12649
+ // later state edge before an OOM; its journal must not stay unknown.
12650
+ this.publishWorkingState("process");
12651
+ }
12372
12652
  const path = join(this.instanceDir, "session-id");
12373
12653
  // Idle observations happen after turns. Avoid needless writes (and mtime
12374
12654
  // churn) while still updating the marker when /new changes the session.