@songsid/agend 2.2.0-beta.1 → 2.2.0-beta.11

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 (401) hide show
  1. package/README.md +1 -1
  2. package/README.zh-TW.md +1 -1
  3. package/dist/agent-cli-instructions.md +16 -9
  4. package/dist/agent-cli.js +15 -7
  5. package/dist/agent-cli.js.map +1 -1
  6. package/dist/agent-endpoint.js +24 -9
  7. package/dist/agent-endpoint.js.map +1 -1
  8. package/dist/agent-whats-new.d.ts +47 -0
  9. package/dist/agent-whats-new.js +147 -0
  10. package/dist/agent-whats-new.js.map +1 -0
  11. package/dist/auth-api.d.ts +5 -3
  12. package/dist/auth-api.js +19 -3
  13. package/dist/auth-api.js.map +1 -1
  14. package/dist/backend/antigravity.d.ts +8 -0
  15. package/dist/backend/antigravity.js +32 -9
  16. package/dist/backend/antigravity.js.map +1 -1
  17. package/dist/backend/claude-carry.d.ts +22 -0
  18. package/dist/backend/claude-carry.js +170 -0
  19. package/dist/backend/claude-carry.js.map +1 -0
  20. package/dist/backend/claude-code.d.ts +65 -2
  21. package/dist/backend/claude-code.js +226 -33
  22. package/dist/backend/claude-code.js.map +1 -1
  23. package/dist/backend/codex-metadata.d.ts +33 -0
  24. package/dist/backend/codex-metadata.js +122 -18
  25. package/dist/backend/codex-metadata.js.map +1 -1
  26. package/dist/backend/codex.d.ts +3 -0
  27. package/dist/backend/codex.js +17 -1
  28. package/dist/backend/codex.js.map +1 -1
  29. package/dist/backend/credential-profile.d.ts +40 -3
  30. package/dist/backend/credential-profile.js +113 -5
  31. package/dist/backend/credential-profile.js.map +1 -1
  32. package/dist/backend/effort-metadata.d.ts +37 -1
  33. package/dist/backend/effort-metadata.js +86 -15
  34. package/dist/backend/effort-metadata.js.map +1 -1
  35. package/dist/backend/grok.d.ts +4 -0
  36. package/dist/backend/grok.js +14 -0
  37. package/dist/backend/grok.js.map +1 -1
  38. package/dist/backend/kiro-engine-ledger.d.ts +5 -0
  39. package/dist/backend/kiro-engine-ledger.js +20 -0
  40. package/dist/backend/kiro-engine-ledger.js.map +1 -1
  41. package/dist/backend/kiro-identity.d.ts +6 -0
  42. package/dist/backend/kiro-identity.js +19 -5
  43. package/dist/backend/kiro-identity.js.map +1 -1
  44. package/dist/backend/kiro.d.ts +39 -3
  45. package/dist/backend/kiro.js +143 -19
  46. package/dist/backend/kiro.js.map +1 -1
  47. package/dist/backend/launch-effort.d.ts +61 -0
  48. package/dist/backend/launch-effort.js +170 -0
  49. package/dist/backend/launch-effort.js.map +1 -0
  50. package/dist/backend/muse.d.ts +84 -0
  51. package/dist/backend/muse.js +449 -53
  52. package/dist/backend/muse.js.map +1 -1
  53. package/dist/backend/types.d.ts +47 -4
  54. package/dist/backend/types.js +12 -1
  55. package/dist/backend/types.js.map +1 -1
  56. package/dist/backend-command.d.ts +91 -0
  57. package/dist/backend-command.js +122 -0
  58. package/dist/backend-command.js.map +1 -0
  59. package/dist/background-work-ended.d.ts +18 -0
  60. package/dist/background-work-ended.js +30 -0
  61. package/dist/background-work-ended.js.map +1 -0
  62. package/dist/cache-service.d.ts +2 -0
  63. package/dist/cache-service.js +2 -1
  64. package/dist/cache-service.js.map +1 -1
  65. package/dist/channel/adapters/discord.d.ts +36 -1
  66. package/dist/channel/adapters/discord.js +126 -4
  67. package/dist/channel/adapters/discord.js.map +1 -1
  68. package/dist/channel/adapters/telegram.d.ts +42 -2
  69. package/dist/channel/adapters/telegram.js +142 -23
  70. package/dist/channel/adapters/telegram.js.map +1 -1
  71. package/dist/channel/mcp-server.js +9 -1
  72. package/dist/channel/mcp-server.js.map +1 -1
  73. package/dist/channel/mcp-tools.d.ts +11 -0
  74. package/dist/channel/mcp-tools.js +26 -13
  75. package/dist/channel/mcp-tools.js.map +1 -1
  76. package/dist/channel/message-edit-errors.d.ts +5 -0
  77. package/dist/channel/message-edit-errors.js +24 -0
  78. package/dist/channel/message-edit-errors.js.map +1 -0
  79. package/dist/channel/telegram-markdown.d.ts +6 -0
  80. package/dist/channel/telegram-markdown.js +104 -0
  81. package/dist/channel/telegram-markdown.js.map +1 -0
  82. package/dist/channel/tool-router.d.ts +2 -0
  83. package/dist/channel/tool-router.js +10 -4
  84. package/dist/channel/tool-router.js.map +1 -1
  85. package/dist/channel/types.d.ts +55 -3
  86. package/dist/cli.js +167 -39
  87. package/dist/cli.js.map +1 -1
  88. package/dist/command-table.d.ts +2 -2
  89. package/dist/command-table.js +6 -3
  90. package/dist/command-table.js.map +1 -1
  91. package/dist/config-validator.js +59 -0
  92. package/dist/config-validator.js.map +1 -1
  93. package/dist/cross-instance-envelope.d.ts +15 -2
  94. package/dist/cross-instance-envelope.js +29 -5
  95. package/dist/cross-instance-envelope.js.map +1 -1
  96. package/dist/daemon.d.ts +156 -1
  97. package/dist/daemon.js +773 -89
  98. package/dist/daemon.js.map +1 -1
  99. package/dist/delivery-outbox.d.ts +70 -8
  100. package/dist/delivery-outbox.js +315 -24
  101. package/dist/delivery-outbox.js.map +1 -1
  102. package/dist/delivery-reconciliation.d.ts +18 -1
  103. package/dist/delivery-reconciliation.js +67 -2
  104. package/dist/delivery-reconciliation.js.map +1 -1
  105. package/dist/fleet-context.d.ts +10 -1
  106. package/dist/fleet-manager.d.ts +404 -13
  107. package/dist/fleet-manager.js +2039 -210
  108. package/dist/fleet-manager.js.map +1 -1
  109. package/dist/general-knowledge/skills/backend-providers/SKILL.md +1 -1
  110. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +92 -21
  111. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +27 -3
  112. package/dist/general-knowledge/skills/delegation-playbook/SKILL.md +8 -8
  113. package/dist/general-knowledge/skills/fleet-config/SKILL.md +5 -5
  114. package/dist/general-knowledge/skills/fleet-health/SKILL.md +4 -4
  115. package/dist/general-knowledge/skills/fleet-restart/SKILL.md +13 -13
  116. package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +3 -2
  117. package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +6 -0
  118. package/dist/general-knowledge/skills/model-discovery/SKILL.md +12 -9
  119. package/dist/general-knowledge/skills/multi-channel/SKILL.md +8 -2
  120. package/dist/general-knowledge/skills/scheduling/SKILL.md +5 -3
  121. package/dist/general-knowledge/skills/session-management/SKILL.md +6 -5
  122. package/dist/general-knowledge/skills/tui-effort/SKILL.md +2 -2
  123. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +8 -3
  124. package/dist/general-knowledge/steering/core-rules.md +8 -7
  125. package/dist/host-memory.d.ts +10 -0
  126. package/dist/host-memory.js +33 -1
  127. package/dist/host-memory.js.map +1 -1
  128. package/dist/human-delivery-receipts.d.ts +83 -0
  129. package/dist/human-delivery-receipts.js +194 -0
  130. package/dist/human-delivery-receipts.js.map +1 -0
  131. package/dist/instance-avatar.d.ts +36 -0
  132. package/dist/instance-avatar.js +57 -0
  133. package/dist/instance-avatar.js.map +1 -0
  134. package/dist/instance-config-impact.js +6 -0
  135. package/dist/instance-config-impact.js.map +1 -1
  136. package/dist/instance-lifecycle.d.ts +29 -6
  137. package/dist/instance-lifecycle.js +79 -28
  138. package/dist/instance-lifecycle.js.map +1 -1
  139. package/dist/instructions.d.ts +3 -1
  140. package/dist/instructions.js +11 -8
  141. package/dist/instructions.js.map +1 -1
  142. package/dist/kiro-log-gate.d.ts +42 -0
  143. package/dist/kiro-log-gate.js +39 -0
  144. package/dist/kiro-log-gate.js.map +1 -0
  145. package/dist/locale.js +226 -44
  146. package/dist/locale.js.map +1 -1
  147. package/dist/log-service.d.ts +99 -0
  148. package/dist/log-service.js +243 -0
  149. package/dist/log-service.js.map +1 -0
  150. package/dist/log-steps.d.ts +169 -0
  151. package/dist/log-steps.js +858 -0
  152. package/dist/log-steps.js.map +1 -0
  153. package/dist/login-controller.d.ts +20 -3
  154. package/dist/login-controller.js +71 -19
  155. package/dist/login-controller.js.map +1 -1
  156. package/dist/login-flows.d.ts +5 -2
  157. package/dist/login-flows.js +5 -4
  158. package/dist/login-flows.js.map +1 -1
  159. package/dist/memory-pressure.d.ts +13 -1
  160. package/dist/memory-pressure.js +50 -10
  161. package/dist/memory-pressure.js.map +1 -1
  162. package/dist/needs-you-hub.js +2 -0
  163. package/dist/needs-you-hub.js.map +1 -1
  164. package/dist/needs-you-live.d.ts +6 -0
  165. package/dist/needs-you-live.js +23 -3
  166. package/dist/needs-you-live.js.map +1 -1
  167. package/dist/next-steps.js +1 -1
  168. package/dist/next-steps.js.map +1 -1
  169. package/dist/outbound-handlers.d.ts +100 -0
  170. package/dist/outbound-handlers.js +284 -97
  171. package/dist/outbound-handlers.js.map +1 -1
  172. package/dist/outbound-schemas.d.ts +38 -15
  173. package/dist/outbound-schemas.js +36 -8
  174. package/dist/outbound-schemas.js.map +1 -1
  175. package/dist/public-link-progress.d.ts +1 -1
  176. package/dist/public-link-progress.js +8 -2
  177. package/dist/public-link-progress.js.map +1 -1
  178. package/dist/public-preview.d.ts +101 -0
  179. package/dist/public-preview.js +236 -0
  180. package/dist/public-preview.js.map +1 -0
  181. package/dist/public-web-gateway.js +14 -4
  182. package/dist/public-web-gateway.js.map +1 -1
  183. package/dist/public-web-link.d.ts +24 -1
  184. package/dist/public-web-link.js +63 -9
  185. package/dist/public-web-link.js.map +1 -1
  186. package/dist/reply-buttons-controller.d.ts +89 -5
  187. package/dist/reply-buttons-controller.js +273 -8
  188. package/dist/reply-buttons-controller.js.map +1 -1
  189. package/dist/reply-buttons.d.ts +48 -3
  190. package/dist/reply-buttons.js +105 -10
  191. package/dist/reply-buttons.js.map +1 -1
  192. package/dist/reply-polls.d.ts +85 -0
  193. package/dist/reply-polls.js +154 -0
  194. package/dist/reply-polls.js.map +1 -0
  195. package/dist/restart-guard.d.ts +6 -3
  196. package/dist/restart-guard.js +6 -3
  197. package/dist/restart-guard.js.map +1 -1
  198. package/dist/restart-progress.d.ts +33 -0
  199. package/dist/restart-progress.js +30 -0
  200. package/dist/restart-progress.js.map +1 -1
  201. package/dist/service-activation.d.ts +15 -7
  202. package/dist/service-activation.js +46 -29
  203. package/dist/service-activation.js.map +1 -1
  204. package/dist/service-installer.d.ts +31 -0
  205. package/dist/service-installer.js +41 -2
  206. package/dist/service-installer.js.map +1 -1
  207. package/dist/settings-api.js +85 -25
  208. package/dist/settings-api.js.map +1 -1
  209. package/dist/settings-change.js +72 -1
  210. package/dist/settings-change.js.map +1 -1
  211. package/dist/settings-effect.js +28 -4
  212. package/dist/settings-effect.js.map +1 -1
  213. package/dist/settings-http-confirmation.js +7 -0
  214. package/dist/settings-http-confirmation.js.map +1 -1
  215. package/dist/setup-form.d.ts +1 -1
  216. package/dist/setup-form.js +1 -1
  217. package/dist/spawn-gate.js +5 -1
  218. package/dist/spawn-gate.js.map +1 -1
  219. package/dist/spawn-hold-reply.d.ts +12 -0
  220. package/dist/spawn-hold-reply.js +39 -0
  221. package/dist/spawn-hold-reply.js.map +1 -0
  222. package/dist/status-emojis.d.ts +10 -3
  223. package/dist/status-emojis.js +11 -2
  224. package/dist/status-emojis.js.map +1 -1
  225. package/dist/systemd-peer-bus.d.ts +95 -0
  226. package/dist/systemd-peer-bus.js +430 -0
  227. package/dist/systemd-peer-bus.js.map +1 -0
  228. package/dist/systemd-runtime.d.ts +27 -1
  229. package/dist/systemd-runtime.js +69 -7
  230. package/dist/systemd-runtime.js.map +1 -1
  231. package/dist/tips.js +4 -4
  232. package/dist/tips.js.map +1 -1
  233. package/dist/tool-permissions.js +3 -3
  234. package/dist/tool-permissions.js.map +1 -1
  235. package/dist/tool-progress.js +3 -2
  236. package/dist/tool-progress.js.map +1 -1
  237. package/dist/topic-commands.d.ts +42 -2
  238. package/dist/topic-commands.js +101 -11
  239. package/dist/topic-commands.js.map +1 -1
  240. package/dist/transcript-delta-reader.d.ts +3 -3
  241. package/dist/transcript-delta-reader.js +8 -8
  242. package/dist/transcript-delta-reader.js.map +1 -1
  243. package/dist/transcript-jsonl.d.ts +80 -3
  244. package/dist/transcript-jsonl.js +147 -10
  245. package/dist/transcript-jsonl.js.map +1 -1
  246. package/dist/transcript-monitor.d.ts +19 -0
  247. package/dist/transcript-monitor.js +87 -15
  248. package/dist/transcript-monitor.js.map +1 -1
  249. package/dist/transcript-sources.d.ts +78 -0
  250. package/dist/transcript-sources.js +375 -64
  251. package/dist/transcript-sources.js.map +1 -1
  252. package/dist/transcript-turns.d.ts +15 -2
  253. package/dist/transcript-turns.js +15 -2
  254. package/dist/transcript-turns.js.map +1 -1
  255. package/dist/tunnel/cloudflared.d.ts +7 -15
  256. package/dist/tunnel/cloudflared.js +36 -1
  257. package/dist/tunnel/cloudflared.js.map +1 -1
  258. package/dist/tunnel/purpose-lane.d.ts +5 -1
  259. package/dist/tunnel/purpose-lane.js +14 -7
  260. package/dist/tunnel/purpose-lane.js.map +1 -1
  261. package/dist/tunnel/types.d.ts +5 -1
  262. package/dist/tunnel/types.js.map +1 -1
  263. package/dist/turn-resume.d.ts +2 -0
  264. package/dist/turn-resume.js +1 -1
  265. package/dist/turn-resume.js.map +1 -1
  266. package/dist/types.d.ts +25 -0
  267. package/dist/ui/chat-commands.js +1 -0
  268. package/dist/ui/chat-emojis.js +33 -0
  269. package/dist/ui/chat-mentions.js +63 -0
  270. package/dist/ui/chat-render.js +61 -4
  271. package/dist/ui/chat-store.js +37 -3
  272. package/dist/ui/chat-strings.js +40 -20
  273. package/dist/ui/chat-thread.js +111 -12
  274. package/dist/ui/chat-time.js +80 -0
  275. package/dist/ui/panel-chat.js +41 -10
  276. package/dist/ui/panel-details.js +2 -0
  277. package/dist/ui/panel-fleet.js +199 -57
  278. package/dist/ui/panel-log.js +428 -0
  279. package/dist/ui/panel-needs.js +6 -2
  280. package/dist/ui/panel-settings.js +184 -32
  281. package/dist/ui/preview-panel.js +55 -15
  282. package/dist/ui/preview-status.js +28 -0
  283. package/dist/ui/preview.js +44 -3
  284. package/dist/ui/settings-dialogs.js +7 -2
  285. package/dist/ui/settings-model.js +2 -1
  286. package/dist/ui/settings-strings.js +56 -4
  287. package/dist/ui/settings-token.js +1 -1
  288. package/dist/ui/shared/app-i18n.js +43 -17
  289. package/dist/ui/shared/app-prefs.js +250 -0
  290. package/dist/ui/shared/app-route.js +7 -2
  291. package/dist/ui/shared/app-shell.js +165 -8
  292. package/dist/ui/shared/app-store.js +5 -1
  293. package/dist/ui/shared/app-stream.js +26 -5
  294. package/dist/ui/shared/app.css +163 -22
  295. package/dist/ui/shared/app.js +11 -1
  296. package/dist/ui/shared/fonts/README.md +1 -1
  297. package/dist/ui/shared/header-tools.js +55 -5
  298. package/dist/ui/shared/instance-nav.js +241 -45
  299. package/dist/ui/shared/instance-switch.js +4 -4
  300. package/dist/ui/shared/panel-view.js +6 -0
  301. package/dist/ui/shared/signin.js +7 -7
  302. package/dist/ui/shared/theme.js +39 -7
  303. package/dist/ui/shared/tokens.css +1 -1
  304. package/dist/ui/shared/ui-icons.js +1 -0
  305. package/dist/ui/shared/usage-dialog.js +23 -5
  306. package/dist/ui/shared/view-strings.js +32 -4
  307. package/dist/ui/signin.html +1 -1
  308. package/dist/unrecognised-prompt.d.ts +23 -0
  309. package/dist/unrecognised-prompt.js +74 -0
  310. package/dist/unrecognised-prompt.js.map +1 -0
  311. package/dist/update-check.d.ts +31 -0
  312. package/dist/update-check.js +47 -0
  313. package/dist/update-check.js.map +1 -1
  314. package/dist/update-install.d.ts +28 -2
  315. package/dist/update-install.js +22 -4
  316. package/dist/update-install.js.map +1 -1
  317. package/dist/update-launch.d.ts +46 -1
  318. package/dist/update-launch.js +146 -3
  319. package/dist/update-launch.js.map +1 -1
  320. package/dist/update-marker.d.ts +51 -0
  321. package/dist/update-marker.js +61 -0
  322. package/dist/update-marker.js.map +1 -1
  323. package/dist/update-preflight.d.ts +100 -0
  324. package/dist/update-preflight.js +264 -0
  325. package/dist/update-preflight.js.map +1 -0
  326. package/dist/updater-caller.d.ts +31 -0
  327. package/dist/updater-caller.js +109 -0
  328. package/dist/updater-caller.js.map +1 -0
  329. package/dist/usage/i18n-keys.d.ts +1 -1
  330. package/dist/usage/i18n-keys.js +1 -1
  331. package/dist/usage/i18n-keys.js.map +1 -1
  332. package/dist/usage/providers.d.ts +34 -5
  333. package/dist/usage/providers.js +160 -27
  334. package/dist/usage/providers.js.map +1 -1
  335. package/dist/usage/statusline-usage.d.ts +6 -2
  336. package/dist/usage/statusline-usage.js +10 -4
  337. package/dist/usage/statusline-usage.js.map +1 -1
  338. package/dist/view-api.d.ts +1 -1
  339. package/dist/view-api.js +38 -8
  340. package/dist/view-api.js.map +1 -1
  341. package/dist/wake-coordinator.d.ts +11 -0
  342. package/dist/wake-coordinator.js +38 -4
  343. package/dist/wake-coordinator.js.map +1 -1
  344. package/dist/web-api.d.ts +89 -4
  345. package/dist/web-api.js +551 -45
  346. package/dist/web-api.js.map +1 -1
  347. package/dist/web-auth.d.ts +2 -2
  348. package/dist/web-auth.js +9 -4
  349. package/dist/web-auth.js.map +1 -1
  350. package/dist/web-chat-history.d.ts +12 -1
  351. package/dist/web-chat-history.js +29 -7
  352. package/dist/web-chat-history.js.map +1 -1
  353. package/dist/web-commands.d.ts +27 -4
  354. package/dist/web-commands.js +19 -4
  355. package/dist/web-commands.js.map +1 -1
  356. package/dist/web-host-guard.d.ts +3 -11
  357. package/dist/web-host-guard.js +3 -12
  358. package/dist/web-host-guard.js.map +1 -1
  359. package/dist/web-images.d.ts +47 -0
  360. package/dist/web-images.js +169 -0
  361. package/dist/web-images.js.map +1 -0
  362. package/dist/web-login.d.ts +14 -0
  363. package/dist/web-login.js +4 -2
  364. package/dist/web-login.js.map +1 -1
  365. package/dist/web-mentions.d.ts +44 -0
  366. package/dist/web-mentions.js +75 -0
  367. package/dist/web-mentions.js.map +1 -0
  368. package/dist/web-prefs.d.ts +81 -0
  369. package/dist/web-prefs.js +297 -0
  370. package/dist/web-prefs.js.map +1 -0
  371. package/dist/web-preview.d.ts +14 -1
  372. package/dist/web-preview.js +35 -13
  373. package/dist/web-preview.js.map +1 -1
  374. package/dist/web-profile.d.ts +32 -0
  375. package/dist/web-profile.js +132 -0
  376. package/dist/web-profile.js.map +1 -0
  377. package/dist/web-reply-files.d.ts +45 -0
  378. package/dist/web-reply-files.js +223 -0
  379. package/dist/web-reply-files.js.map +1 -0
  380. package/dist/web-schedules.d.ts +12 -0
  381. package/dist/web-schedules.js +34 -0
  382. package/dist/web-schedules.js.map +1 -0
  383. package/dist/web-server-drain.d.ts +10 -0
  384. package/dist/web-server-drain.js +50 -0
  385. package/dist/web-server-drain.js.map +1 -0
  386. package/dist/web-session.d.ts +50 -1
  387. package/dist/web-session.js +63 -6
  388. package/dist/web-session.js.map +1 -1
  389. package/dist/web-settings-patch.d.ts +23 -0
  390. package/dist/web-settings-patch.js +77 -0
  391. package/dist/web-settings-patch.js.map +1 -0
  392. package/dist/web-shell-routes.d.ts +5 -1
  393. package/dist/web-shell-routes.js +10 -1
  394. package/dist/web-shell-routes.js.map +1 -1
  395. package/dist/web-upload.d.ts +17 -4
  396. package/dist/web-upload.js +64 -13
  397. package/dist/web-upload.js.map +1 -1
  398. package/dist/workflow-templates/default.md +1 -1
  399. package/package.json +1 -1
  400. package/templates/launchd.plist.ejs +3 -1
  401. package/templates/systemd.service.ejs +2 -1
package/dist/daemon.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { AGENT_WHATS_NEW, agendVersion, planWhatsNew, readWhatsNewRecord, recordWhatsNew, whatsNewNotice } from "./agent-whats-new.js";
1
2
  import { join, dirname, basename, resolve } from "node:path";
2
3
  import { mkdirSync, writeFileSync, readFileSync, readdirSync, existsSync, unlinkSync, rmSync, appendFileSync, statSync, renameSync } from "node:fs";
3
4
  import { fileURLToPath } from "node:url";
@@ -8,6 +9,7 @@ import { rotateLogIfNeededAsync } from "./logger.js";
8
9
  import { coredumpFilterLaunchPrefix } from "./coredump-filter.js";
9
10
  import { mcpServerState } from "./mcp-liveness.js";
10
11
  import { clearPausedMarker, writePausedMarker } from "./pause-marker.js";
12
+ import { writeBackgroundWorkEndedMarker } from "./background-work-ended.js";
11
13
  import { TmuxManager, resolveTmuxLogicalSize } from "./tmux-manager.js";
12
14
  import { TranscriptMonitor } from "./transcript-monitor.js";
13
15
  import { createTranscriptSource } from "./transcript-sources.js";
@@ -31,6 +33,7 @@ import { HangDetector } from "./hang-detector.js";
31
33
  import { writeSecretFile } from "./secret-file.js";
32
34
  import { PaneWriteLock } from "./pane-write-lock.js";
33
35
  import { buildFleetInstructions } from "./instructions.js";
36
+ import { formatHumanInboundMessage, prepareDeliveryWire, steerInboundEnvelope, BTW_INBOUND_PREFIX, humanBodyHash, humanOwnerObjectId } from "./human-delivery-receipts.js";
34
37
  import { formatCrossInstanceInboundMessage, renderCrossInstanceHandoffMetadata, } from "./cross-instance-envelope.js";
35
38
  import { agendMessageInInput, bottomRowIsReady, inputAreaText, inputShowsPastedText, pastedTextSignature, pasteLeftInInput, strandedAgendMessageInInput } from "./pane-input-residue.js";
36
39
  import { TurnReplyGuard } from "./turn-reply-guard.js";
@@ -44,7 +47,9 @@ import { measureSyncWork, noteSyncWork } from "./sync-work-attribution.js";
44
47
  import { TranscriptDeltaReader } from "./transcript-delta-reader.js";
45
48
  import { buildResumeContinuation, clearInFlightTurnMarker, consumeInFlightTurnMarker, decideTurnResume, normalizeResumeBackend, readResumeFingerprint, RESUME_BLOCKING_DELIVERY_STATES, writeInFlightTurnMarker, } from "./turn-resume.js";
46
49
  import { codexLiveRowMatches } from "./backend/codex.js";
50
+ import { claudeConfigDirFor, claudeProfileConfigDir, claudeSwitchDialog, claudeSwitchTargetMatches } from "./backend/claude-code.js";
47
51
  import { InteractionObservation, INTERACTION_CONFIRM_MS, sameInteractionOwner } from "./interaction-observation.js";
52
+ import { unrecognisedPromptShape } from "./unrecognised-prompt.js";
48
53
  const __filename = fileURLToPath(import.meta.url);
49
54
  const __dirname = dirname(__filename);
50
55
  // Tool routing sets — module-level to avoid re-creation on every handleToolCall
@@ -77,10 +82,18 @@ const REPLY_GUARD_IDLE_CONFIRM_MS = 60_000;
77
82
  * down like a turn the transcript cannot place — never a recovery.
78
83
  */
79
84
  const TRANSCRIPT_RUNNING_HOLD_MAX_MS = 30 * 60_000;
85
+ /**
86
+ * #1641: an open turn in the CLI's transcript holds the instance busy (no paste, whatever the pane shows) only while the
87
+ * transcript is still moving: a turn whose end was missed must not pin the instance busy forever. A live turn writes
88
+ * often; a long silent tool run shows its own busy row on the pane.
89
+ */
90
+ export const TRANSCRIPT_OPEN_TURN_QUIET_MS = 15 * 60_000;
80
91
  const REPLY_RECOVERY_PROMPT = "[system:reply-required] The previous human-facing turn ended without a successfully delivered reply. Do not redo the work. React with an emoji or use the reply tool exactly once now to send the user a concise conclusion. If no substantive answer is needed, a brief react is sufficient. If you already replied to this message, do nothing. Do not reply to this system instruction except through the react or reply tool.";
81
92
  /** Point a resumed CLI at its one backend-native instruction source. */
82
- /** The outbox evidence for a delivery the CLI's transcript shows taken (#758; #1201 absorbed). */
83
- function transcriptMarkerEvidence(kind) {
93
+ /** The outbox evidence for a delivery the CLI's transcript shows taken (#758; #1201 absorbed; #1647 a raw paste's text). */
94
+ function transcriptMarkerEvidence(kind, rawPaste = false) {
95
+ if (rawPaste)
96
+ return "transcript-turn-user-entry-raw-paste-text";
84
97
  return kind === "user" ? "transcript-marker" : kind === "absorbed" ? "transcript-marker-absorbed" : "transcript-marker-queued";
85
98
  }
86
99
  export function buildInstructionReloadNotice(binaryName, instanceName, instanceDir, backendSource) {
@@ -472,6 +485,16 @@ const POST_ENTER_PROOF_WINDOW_MS = 3_000;
472
485
  /** A final bounded observation before an ambiguous Codex submit can be classified. */
473
486
  const CODEX_LATE_PROOF_MS = 5_000;
474
487
  const POST_ENTER_PROOF_POLL_MS = 250;
488
+ /** #1647: a raw paste a pane with no transcript showed taken — its text left the input row (or the CLI's own busy marker
489
+ * replaced it), with output after the Enter. */
490
+ const RAW_PASTE_LEFT_INPUT = "raw-paste-left-input-after-output";
491
+ /**
492
+ * #1647/#1663 P1-2: backends whose transcript AgEnD holds for this instance alone, so a raw paste's exact text in it is
493
+ * this paste's turn. claude-code: the path is its own statusline.json's transcript_path (written by this instance's
494
+ * CLI). Not codex: its rollout is chosen by working directory, which another instance can share, and AgEnD holds no
495
+ * session id to bind it — so a codex raw paste the pane could not prove stays uncertain.
496
+ */
497
+ const RAW_TEXT_TRANSCRIPT_BACKENDS = ["claude-code"];
475
498
  /**
476
499
  * A readerless CLI's delivery is proven by its own transcript, which lags the pane by a flush. Only a delta that was
477
500
  * read and carried no marker for this long is held against the delivery — a late flush must not raise a false ⚠️.
@@ -503,8 +526,16 @@ const DIALOG_PROBE_UNKNOWN_MAX = 40;
503
526
  /** Startup dialog scan: poll cadence and wall-clock budget (keys and waits included). */
504
527
  const STARTUP_DIALOG_POLL_MS = 500;
505
528
  const STARTUP_DIALOG_BUDGET_MS = 30_000;
529
+ /** #1690: after AgEnD's own `/model`, how long the pane is read for claude's "Switch model?" or its result. */
530
+ export const OWN_SWITCH_WAIT_MS = 6_000;
531
+ /** #1690: after an answer to that dialog, how long it gets to go away. */
532
+ const OWN_SWITCH_SETTLE_MS = 3_000;
506
533
  /** A blocking dialog still on screen this long after first being seen is reported for a human. */
507
534
  const DIALOG_PARKED_NOTIFY_MS = 60_000;
535
+ /** #1691: a prompt no table knows must sit unchanged this long (monotonic) before it holds deliveries and is reported. */
536
+ export const UNRECOGNISED_PROMPT_STABLE_MS = 60_000;
537
+ /** #1691: the rows under a prompt a backend without a composer reader shows its ready chrome in (status bar, hint). */
538
+ const COMPOSER_TAIL_ROWS = 4;
508
539
  /** After a runtime dialog's keys, how long the pane gets to repaint before it is read again to see whether they took. */
509
540
  const DIALOG_ANSWER_SETTLE_MS = 300;
510
541
  /** Consecutive answers one runtime dialog ignored (still on screen right after the keys) before it is reported for a human. */
@@ -614,6 +645,10 @@ export function channelStatus(meta) {
614
645
  // they are the dashboard's ticks (the fleet routes them by the message id, never to a platform).
615
646
  if (!messageId || (!chatId && meta.source !== "web"))
616
647
  return undefined;
648
+ // #1823: a notice from AgEnD itself (a poll's results) names its chat so the answer goes there, but its message id
649
+ // is AgEnD's own, not a platform message: there is nothing to react to.
650
+ if (meta.source === "agend-system")
651
+ return undefined;
617
652
  if (!chatId)
618
653
  return { chatId: "", messageId };
619
654
  const threadId = meta.thread_id || undefined;
@@ -1058,6 +1093,10 @@ export class Daemon extends EventEmitter {
1058
1093
  ipcServer = null;
1059
1094
  messageBus;
1060
1095
  transcriptMonitor = null;
1096
+ /** #1218: the transcript this instance's monitor follows now (the Log tab reads it on demand; nothing runs for it here). */
1097
+ logTranscriptPath() { return this.transcriptMonitor?.currentPath() ?? null; }
1098
+ /** #1218 (kiro): the launch this daemon's backend last built, for the Log's reason only (kiro-log-gate.ts). */
1099
+ logLaunch() { return this.backend?.logLaunch?.() ?? null; }
1061
1100
  guardian = null;
1062
1101
  adapter = null;
1063
1102
  pendingIpcRequests = new Map();
@@ -1144,6 +1183,34 @@ export class Daemon extends EventEmitter {
1144
1183
  dialogParkedKey = null;
1145
1184
  dialogParkedInteractionEpisode = null;
1146
1185
  dialogParkedReported = false;
1186
+ /**
1187
+ * #1691: the prompt at the bottom of the pane that no backend table knows — its block, since when (performance.now)
1188
+ * it has been unchanged, and whether that has lasted UNRECOGNISED_PROMPT_STABLE_MS. Updated by the runtime scan.
1189
+ */
1190
+ unrecognisedPrompt = null;
1191
+ /**
1192
+ * #1691: when (performance.now) the newest pane read that decided the evidence above was taken — a scan or a fresh
1193
+ * read, confirming, restarting or ending it. A read taken before it finishes late and says nothing about now: it may
1194
+ * neither clear, restart nor confirm (#1695 r4).
1195
+ */
1196
+ unrecognisedPromptReadAt = -Infinity;
1197
+ /**
1198
+ * #1691: the catch-all for that prompt, as a hold-only runtime dialog — so the delivery hold, awaiting_input, the
1199
+ * Needs-you item and the single parked report are the existing ones. Never answered (hold-only, no keys).
1200
+ */
1201
+ unrecognisedPromptDialog = {
1202
+ pattern: /(?!)unrecognised-prompt-1691/,
1203
+ keys: [],
1204
+ description: "a prompt AgEnD does not recognise",
1205
+ holdOnly: true,
1206
+ isActive: (pane) => this.unrecognisedPromptActive(pane),
1207
+ requestIdentity: () => this.unrecognisedPrompt?.block ?? null,
1208
+ };
1209
+ /**
1210
+ * #1690: AgEnD's own runtime `/model` in flight — from the moment its paste went in until its "Switch model?" dialog
1211
+ * (if any) is answered. Every delivery is held meanwhile: the dialog's default cursor is Yes, a stray Enter confirms it.
1212
+ */
1213
+ ownSwitch = null;
1147
1214
  /**
1148
1215
  * The runtime dialog the monitor is answering, and how many of its answers in a row did nothing (the dialog was
1149
1216
  * still on screen right after the keys). Not the on-screen duration: a burst of FRESH prompts, each answered, has a
@@ -1237,6 +1304,11 @@ export class Daemon extends EventEmitter {
1237
1304
  turnReplyGuard = new TurnReplyGuard();
1238
1305
  /** #1510: turn boundaries from the CLI's transcript, for a backend whose guard reads its turn end there. */
1239
1306
  transcriptTurns = new TranscriptTurnLedger();
1307
+ /** #1641: when this launch's transcript monitor last read anything (a turn boundary, a tool, assistant text); performance.now(). */
1308
+ transcriptActivityAt = 0;
1309
+ /** #1641: the launch and composer a stuck pane's idle evidence is about, and since when (performance.now()); see recoverStuck. */
1310
+ stuckRecoveryKey = null;
1311
+ stuckRecoverySince = 0;
1240
1312
  /** #1510: since when (monotonic) this generation has been held as running in the transcript. */
1241
1313
  transcriptRunningSince = null;
1242
1314
  /** Prevent a visible stale XML fragment from being recovered on later turns. */
@@ -1388,6 +1460,8 @@ export class Daemon extends EventEmitter {
1388
1460
  recentPastes = [];
1389
1461
  pendingInstructionsUpdate;
1390
1462
  pendingInstructionsNotice = false;
1463
+ /** #1633: the one-time "what's new for you" notice, pasted before the next real delivery; recorded once pasted. */
1464
+ pendingWhatsNew = null;
1391
1465
  // Whether the warmup steering-reload notice should be injected after spawn.
1392
1466
  // Set in trySpawn by comparing the freshly-built instructions against the
1393
1467
  // last value the agent was told about (prev-instructions). Skipped when
@@ -1856,8 +1930,14 @@ export class Daemon extends EventEmitter {
1856
1930
  this.finishDurableDelivery(delivery, "delivered", OUTPUT_EDGE_ONLY_EVIDENCE);
1857
1931
  return;
1858
1932
  }
1933
+ // #1647: a raw paste on a pane with no transcript (kiro), taken by the CLI as its own input row shows.
1934
+ if (verdict.proof === RAW_PASTE_LEFT_INPUT) {
1935
+ this.finishDurableDelivery(delivery, "delivered", verdict.proof, verdict);
1936
+ return;
1937
+ }
1859
1938
  this.finishDurableDelivery(delivery, "delivered", verdict.transcriptProof
1860
- ? `positive submission proof; ${transcriptMarkerEvidence(verdict.transcriptProof)}` : "positive submission proof", verdict);
1939
+ ? `positive submission proof; ${transcriptMarkerEvidence(verdict.transcriptProof, verdict.submissionMode === "raw_paste")}`
1940
+ : "positive submission proof", verdict);
1861
1941
  }
1862
1942
  /**
1863
1943
  * Finish a delivery that only the output edge vouched for, by the delivery's own marker in the CLI's transcript (#758).
@@ -1873,23 +1953,28 @@ export class Daemon extends EventEmitter {
1873
1953
  * TRANSCRIPT_PROOF_WINDOW_MS). It never finishes a row that stop has handed to reconciliation; a pause or respawn
1874
1954
  * only ends the wait, falling back to today's outcome.
1875
1955
  */
1876
- async proveDeliveryFromTranscript(delivery, checkpoint) {
1956
+ async proveDeliveryFromTranscript(delivery, checkpoint, rawPaste) {
1877
1957
  const spawn = this.spawnGeneration;
1878
1958
  const fence = this.launchFenceEpoch;
1879
1959
  let last = "unavailable";
1880
1960
  let outcome = "delivered";
1881
1961
  let evidence = OUTPUT_EDGE_ONLY_EVIDENCE;
1962
+ // #1647: a raw paste has no marker; its text is the needle, and its pane proof already failed — so without the turn
1963
+ // in the transcript it stays uncertain (with the pane's reason), never the output-edge outcome.
1964
+ const needle = rawPaste ? { rawText: rawPaste.text } : delivery.deliveryId;
1965
+ let found = false;
1882
1966
  try {
1883
1967
  for (let polls = 0; polls <= TRANSCRIPT_PROOF_WINDOW_MS / TRANSCRIPT_PROOF_POLL_MS; polls++) {
1884
1968
  if (this.deliveryWritesStopping)
1885
1969
  return; // reconciliation owns the row now
1886
- const found = await scanTranscriptForDeliveryMarker(checkpoint.path, checkpoint.offset, checkpoint.backend, delivery.deliveryId);
1887
- if (found === "user" || found === "absorbed" || found === "queued") {
1970
+ const look = await scanTranscriptForDeliveryMarker(checkpoint.path, checkpoint.offset, checkpoint.backend, needle);
1971
+ if (look === "user" || look === "absorbed" || look === "queued") {
1972
+ found = true;
1888
1973
  outcome = "delivered";
1889
- evidence = transcriptMarkerEvidence(found);
1974
+ evidence = transcriptMarkerEvidence(look, !!rawPaste);
1890
1975
  break;
1891
1976
  }
1892
- last = found;
1977
+ last = look;
1893
1978
  if (spawn !== this.spawnGeneration || fence !== this.launchFenceEpoch) {
1894
1979
  last = "unavailable";
1895
1980
  break;
@@ -1909,6 +1994,11 @@ export class Daemon extends EventEmitter {
1909
1994
  catch (err) {
1910
1995
  this.logger.debug({ err }, "Transcript proof of a delivery failed — keeping the output-edge outcome");
1911
1996
  }
1997
+ if (rawPaste && !found) {
1998
+ outcome = "uncertain";
1999
+ evidence = `${rawPaste.unproven}; no-transcript-turn (${last})`;
2000
+ this.logger.warn({ deliveryId: delivery.deliveryId, cause: evidence }, "Raw paste outcome uncertain — not re-pasting");
2001
+ }
1912
2002
  // The settlement itself can throw (e.g. the outbox store is unhealthy), and this runs on a detached `void`
1913
2003
  // promise: an uncaught throw is an unhandled rejection, which the fleet restart path turns into a global fault.
1914
2004
  // Settle observably and leave the row for reconciliation — never a false completion, never another paste (#1207 P2).
@@ -1922,6 +2012,7 @@ export class Daemon extends EventEmitter {
1922
2012
  }
1923
2013
  }
1924
2014
  deferDurableDelivery(meta, reason) {
2015
+ this.finishHumanReceipt(meta, { reached: false });
1925
2016
  const delivery = this.durableDeliveryAttempt(meta);
1926
2017
  if (delivery === null || delivery === false)
1927
2018
  return;
@@ -3174,7 +3265,6 @@ export class Daemon extends EventEmitter {
3174
3265
  if (this.runtimeMonitorsFrozen || this.errorMonitorTimer)
3175
3266
  return;
3176
3267
  const patterns = this.backend?.getErrorPatterns?.() ?? [];
3177
- const dialogs = this.backend?.getRuntimeDialogs?.() ?? [];
3178
3268
  if (!this.tmux)
3179
3269
  return;
3180
3270
  if (!this.backend)
@@ -3210,6 +3300,9 @@ export class Daemon extends EventEmitter {
3210
3300
  scanStartedAt = performance.now();
3211
3301
  if (stale())
3212
3302
  return;
3303
+ this.trackUnrecognisedPrompt(pane, captureMono, this.instanceStateLastOutputAt > 0 && this.instanceStateLastOutputAt >= captureAt);
3304
+ // The backend's dialogs, and #1691's catch-all while an unrecognised prompt is active (decided just above).
3305
+ const dialogs = this.runtimeDialogs();
3213
3306
  if (this.instanceStateLastOutputAt > 0 && this.instanceStateLastOutputAt >= captureAt) {
3214
3307
  this.unverifyInteraction(captureMono, captureOrder);
3215
3308
  }
@@ -3689,14 +3782,21 @@ export class Daemon extends EventEmitter {
3689
3782
  isDeliveryEpochCurrent(epoch) {
3690
3783
  return epoch === this.deliveryEpoch;
3691
3784
  }
3692
- queueRawPaste(rawText, deliveryEpoch = this.deliveryEpoch, confirmClear = false, durableAttempt) {
3785
+ queueRawPaste(rawText, deliveryEpoch = this.deliveryEpoch, confirmClear = false, durableAttempt, ownSwitch) {
3786
+ const reportSwitch = (result) => {
3787
+ if (!ownSwitch)
3788
+ return;
3789
+ this.ipcServer?.broadcast({ type: "runtime_switch_result", id: ownSwitch.id, ...result });
3790
+ };
3693
3791
  if (!this.tmux || !this.isDeliveryEpochCurrent(deliveryEpoch)) {
3694
3792
  if (durableAttempt)
3695
3793
  this.retryDurableDeliveryBeforeBegin(durableAttempt, "raw_paste target unavailable before pane queue");
3794
+ reportSwitch({ outcome: "not_sent" });
3696
3795
  return;
3697
3796
  }
3698
3797
  this.pasteLock = this.pasteLock.then(async () => {
3699
3798
  if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
3799
+ reportSwitch({ outcome: "not_sent" });
3700
3800
  this.logger.info("Pending raw delivery dropped by user cancel");
3701
3801
  if (durableAttempt)
3702
3802
  this.retryDurableDeliveryBeforeBegin(durableAttempt, "raw_paste epoch changed while queued");
@@ -3708,6 +3808,7 @@ export class Daemon extends EventEmitter {
3708
3808
  if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
3709
3809
  if (durableAttempt)
3710
3810
  this.retryDurableDeliveryBeforeBegin(durableAttempt, "raw_paste epoch changed while waking");
3811
+ reportSwitch({ outcome: "not_sent" }); // #1690: cancelled before anything was typed — said at once, nothing saved
3711
3812
  return;
3712
3813
  }
3713
3814
  const delivered = await this.deliverMessage(rawText, undefined, {
@@ -3715,11 +3816,25 @@ export class Daemon extends EventEmitter {
3715
3816
  verdict,
3716
3817
  durableAttempt,
3717
3818
  preserveExactBytes: true,
3819
+ // #1690: our own /model holds every other delivery from its write on — before the lock is released, so a steer
3820
+ // or /btw waiting on it cannot type into the gap before claude paints "Switch model?" (default cursor: Yes).
3821
+ ...(ownSwitch ? { onPaneWritten: () => { this.ownSwitch = ownSwitch; } } : {}),
3718
3822
  });
3719
3823
  if (delivered)
3720
3824
  this.finishDurableSubmission(durableAttempt ?? null, verdict);
3721
3825
  else if (durableAttempt && verdict.paneWriteStarted) {
3722
- this.finishDurableDelivery(durableAttempt, "uncertain", `${verdict.phase ?? "raw-paste"}:${verdict.proof ?? "unverified"}`);
3826
+ const unproven = `${verdict.phase ?? "raw-paste"}:${verdict.proof ?? "unverified"}`;
3827
+ const checkpoint = verdict.transcriptCheckpoint;
3828
+ if (checkpoint && RAW_TEXT_TRANSCRIPT_BACKENDS.includes(checkpoint.backend)) {
3829
+ // #1647: the pane could not show it, but the CLI's transcript records the turn the paste started — later
3830
+ // than the pane window sometimes. Wait for it there (bounded), off the paste lock.
3831
+ this.logger.info({ deliveryId: durableAttempt.deliveryId, unproven }, "Raw paste not proven on the pane — asking the CLI's transcript for the turn it started");
3832
+ void this.proveDeliveryFromTranscript(durableAttempt, checkpoint, { text: rawText, unproven });
3833
+ }
3834
+ else {
3835
+ this.logger.warn({ deliveryId: durableAttempt.deliveryId, cause: unproven }, "Raw paste outcome uncertain — not re-pasting");
3836
+ this.finishDurableDelivery(durableAttempt, "uncertain", unproven);
3837
+ }
3723
3838
  }
3724
3839
  else if (durableAttempt && verdict.durableBeginCommitted) {
3725
3840
  this.abortDurableDelivery(durableAttempt, `${verdict.phase ?? "raw-paste"}:${verdict.proof ?? "no-pane-write"}`);
@@ -3736,10 +3851,31 @@ export class Daemon extends EventEmitter {
3736
3851
  else if (durableAttempt)
3737
3852
  this.retryDurableDeliveryBeforeBegin(durableAttempt, err.message);
3738
3853
  this.logger.warn({ err: err.message }, "raw_paste delivery error");
3854
+ if (ownSwitch) {
3855
+ if (this.ownSwitch === ownSwitch)
3856
+ this.ownSwitch = null;
3857
+ reportSwitch({ outcome: "not_confirmed" });
3858
+ }
3739
3859
  return;
3740
3860
  }
3741
3861
  if (confirmClear)
3742
3862
  await this.confirmBackendClearDialog();
3863
+ if (ownSwitch) {
3864
+ if (this.ownSwitch !== ownSwitch)
3865
+ reportSwitch({ outcome: "not_sent" }); // its write never began
3866
+ else {
3867
+ try {
3868
+ reportSwitch(await this.answerOwnSwitchDialog(ownSwitch));
3869
+ }
3870
+ catch (err) {
3871
+ this.logger.warn({ err: err.message }, "Answering our own model switch failed");
3872
+ reportSwitch({ outcome: "not_confirmed" });
3873
+ }
3874
+ finally {
3875
+ this.ownSwitch = null;
3876
+ }
3877
+ }
3878
+ }
3743
3879
  this.logger.debug({ text: rawText.slice(0, 100) }, "Raw paste delivered");
3744
3880
  });
3745
3881
  this.pasteLock = this.pasteLock.catch(err => {
@@ -3758,7 +3894,93 @@ export class Daemon extends EventEmitter {
3758
3894
  this.deferDurableDelivery(deliveryMeta, "invalid raw_paste delivery attempt metadata");
3759
3895
  return;
3760
3896
  }
3761
- this.queueRawPaste(msg.content, this.captureDeliveryEpoch(msg.delivery_epoch), msg.confirm_clear === true, durableAttempt ? { ...durableAttempt, submissionMode: "raw_paste" } : undefined);
3897
+ const sw = msg.runtime_switch;
3898
+ const ownSwitch = sw && typeof sw.id === "string" && sw.kind === "model" && typeof sw.requested === "string"
3899
+ ? { id: sw.id, kind: "model", requested: sw.requested } : undefined;
3900
+ this.queueRawPaste(msg.content, this.captureDeliveryEpoch(msg.delivery_epoch), msg.confirm_clear === true, durableAttempt ? { ...durableAttempt, submissionMode: "raw_paste" } : undefined, ownSwitch);
3901
+ }
3902
+ /**
3903
+ * #1690 (the user's choice A): after AgEnD's own `/model <name>`, claude may ask "Switch model?" (a warm cache for
3904
+ * another model). When the model it names is the one asked for, answer Yes; otherwise No (Escape — `Kept model as …`),
3905
+ * and say so. Every key goes on a freshly read, verified dialog under the pane write lock: the same target, and for
3906
+ * Enter the cursor on "1. Yes"; the cursor is walked there with Up, one verified step at a time, never a blind key.
3907
+ * Without the dialog, claude's own "Set model to …" line after the command is the result.
3908
+ */
3909
+ async answerOwnSwitchDialog(spec) {
3910
+ const tmux = this.tmux;
3911
+ if (!tmux)
3912
+ return { outcome: "not_confirmed" };
3913
+ const read = () => tmux.capturePane().catch(() => "");
3914
+ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
3915
+ /** One key on a dialog re-read under the lock: still the same target, and (when given) the cursor where expected. */
3916
+ const press = (target, cursor, key) => this.paneWriteLock.run(async () => {
3917
+ if (this.tmux !== tmux)
3918
+ return false;
3919
+ const now = claudeSwitchDialog(await read());
3920
+ if (!now || now.kind !== "model" || now.target !== target || (cursor !== null && now.cursor !== cursor))
3921
+ return false;
3922
+ return tmux.sendSpecialKey(key);
3923
+ });
3924
+ const gone = async () => {
3925
+ const until = performance.now() + OWN_SWITCH_SETTLE_MS;
3926
+ while (performance.now() < until) {
3927
+ if (!claudeSwitchDialog(await read()))
3928
+ return true;
3929
+ await sleep(150);
3930
+ }
3931
+ return false;
3932
+ };
3933
+ const deadline = performance.now() + OWN_SWITCH_WAIT_MS;
3934
+ while (performance.now() < deadline && this.tmux === tmux) {
3935
+ const pane = await read();
3936
+ const dialog = claudeSwitchDialog(pane);
3937
+ if (dialog && dialog.kind === "model") {
3938
+ const shown = dialog.target;
3939
+ if (!claudeSwitchTargetMatches(spec.requested, shown)) {
3940
+ const answered = await press(shown, null, "Escape") && await gone();
3941
+ this.logger.warn({ requested: spec.requested, shown, answered }, "claude would switch to another model than asked — answered No");
3942
+ return { outcome: "mismatch", shown, answered };
3943
+ }
3944
+ // Walk the cursor to "1. Yes" (it starts there; a human may have moved it): Up from row 2, re-read each time.
3945
+ let current = dialog;
3946
+ for (let step = 0; step < 2 && current.cursor === 2; step++) {
3947
+ if (!await press(shown, 2, "Up"))
3948
+ break;
3949
+ await sleep(120);
3950
+ const next = claudeSwitchDialog(await read());
3951
+ if (!next || next.target !== shown)
3952
+ break;
3953
+ current = next;
3954
+ }
3955
+ if (current.cursor !== 1 || !await press(shown, 1, "Enter") || !await gone()) {
3956
+ this.logger.warn({ requested: spec.requested, shown, cursor: current.cursor }, "Our own model switch dialog could not be confirmed on a verified cursor — left for a human");
3957
+ return { outcome: "not_confirmed", shown };
3958
+ }
3959
+ this.logger.info({ requested: spec.requested, shown }, "Confirmed our own model switch (claude: the next reply re-reads the history)");
3960
+ return { outcome: "switched", confirmed: true, shown };
3961
+ }
3962
+ if (Daemon.switchResultAfterCommand(pane, spec.requested))
3963
+ return { outcome: "switched", confirmed: false };
3964
+ await sleep(200);
3965
+ }
3966
+ return { outcome: "not_confirmed" };
3967
+ }
3968
+ /**
3969
+ * #1690: claude's "Set model to …" line under our `/model <requested>` (no dialog was needed). Only the newest command
3970
+ * on the pane counts: an older identical `/model` with its result further up (switched away and back) must not pass
3971
+ * for this one before it has even painted — that left its dialog unanswered (live, 2.1.296).
3972
+ */
3973
+ static switchResultAfterCommand(pane, requested) {
3974
+ const rows = pane.replace(/\r/g, "").split("\n");
3975
+ let last = -1;
3976
+ for (let i = rows.length - 1; i >= 0; i--)
3977
+ if (/^❯[ \t\u00a0]+\S/.test(rows[i])) {
3978
+ last = i;
3979
+ break;
3980
+ }
3981
+ if (last < 0 || rows[last].replace(/^❯[ \t\u00a0]+/, "").trimEnd() !== `/model ${requested}`)
3982
+ return false;
3983
+ return rows.slice(last + 1).some(row => /^\s*⎿?\s*Set model to /.test(row));
3762
3984
  }
3763
3985
  /**
3764
3986
  * Confirm a backend's terminal-level /clear prompt after the channel-level
@@ -4021,6 +4243,13 @@ export class Daemon extends EventEmitter {
4021
4243
  if (!backend.quitBlockedByDialog(pane))
4022
4244
  return false;
4023
4245
  this.logger.warn({ reason }, "The CLI asked to confirm its exit (background work is running) — cancelling that prompt and stopping the process, not answering it");
4246
+ // #1716: the background work is about to be ended — record it where the
4247
+ // stop/restart reply and the instance topic notice can report it, so the
4248
+ // kill is never silent.
4249
+ try {
4250
+ writeBackgroundWorkEndedMarker(this.instanceDir, reason);
4251
+ }
4252
+ catch { /* reporting only */ }
4024
4253
  await this.tmux.sendSpecialKey("Escape").catch(() => false);
4025
4254
  return true;
4026
4255
  }
@@ -4222,7 +4451,9 @@ export class Daemon extends EventEmitter {
4222
4451
  // Bare backend regexes are not proof of a live modal. The canonical
4223
4452
  // isActive predicate replaces pattern.test(), preserving table order.
4224
4453
  const dialog = dialogs.find(d => d.isActive && (d.inputBlocked || d.blocksDelivery || d.holdOnly)
4225
- && Daemon.dialogMatches(d, pane));
4454
+ && Daemon.dialogMatches(d, pane)
4455
+ // #1690: AgEnD's own switch dialog, being answered right now, is nobody's to answer.
4456
+ && !(this.ownSwitch && claudeSwitchDialog(pane) !== null));
4226
4457
  if (dialog) {
4227
4458
  const identity = dialog.requestIdentity?.(pane) ?? pane.replace(/\r/g, "")
4228
4459
  .split("\n").map(row => row.replace(/^([ \t]*)[❯›]([ \t]+)(?=\d\.|Yes\b|No\b|Resume\b)/, "$1$2").trimEnd())
@@ -4238,7 +4469,7 @@ export class Daemon extends EventEmitter {
4238
4469
  // No captured text leaves this function or the private observer identity.
4239
4470
  return evidence ? { ...evidence, identity: createHash("sha256").update(evidence.identity).digest("hex") } : null;
4240
4471
  }
4241
- observeInteractionPane(pane, observedAt, monotonicAt, dialogs = this.backend?.getRuntimeDialogs?.() ?? [], order = ++this.interactionCaptureSerial, allowConfirmation = true) {
4472
+ observeInteractionPane(pane, observedAt, monotonicAt, dialogs = this.runtimeDialogs(), order = ++this.interactionCaptureSerial, allowConfirmation = true) {
4242
4473
  const owner = this.interactionOwner();
4243
4474
  const evidence = this.interactionEvidence(pane, dialogs);
4244
4475
  if (!this.interactionObservation.observe(evidence, owner, observedAt, monotonicAt, this.interactionOutputRevision !== this.interactionObservedOutputRevision, order))
@@ -4351,6 +4582,39 @@ export class Daemon extends EventEmitter {
4351
4582
  this.pauseRequested = false;
4352
4583
  return;
4353
4584
  }
4585
+ // #1717: an idle pause is AgEnD-initiated and silent — it must not kill
4586
+ // the user's running background work the way an explicit stop may. Defer
4587
+ // this cycle while the backend reports background work; the next idle
4588
+ // tick retries once the task ends. A pane-capture failure proceeds (the
4589
+ // quit-menu handler below stays the backstop), never wedges unpaused.
4590
+ if (this.backend?.backgroundWorkRunning && this.tmux) {
4591
+ const tmux = this.tmux;
4592
+ const windowId = tmux.getWindowId?.();
4593
+ const generation = this.spawnGeneration;
4594
+ let pane = null;
4595
+ try {
4596
+ pane = await tmux.capturePane();
4597
+ }
4598
+ catch { /* capture failed — pause as before, after revalidation */ }
4599
+ // #1730 P2-2: the await yields — the pane read is only valid for the
4600
+ // world it was taken from. Revalidate owner, phase, state and queue
4601
+ // before acting on it; anything that moved defers this cycle.
4602
+ if (this.tmux !== tmux
4603
+ || this.tmux?.getWindowId?.() !== windowId
4604
+ || this.spawnGeneration !== generation
4605
+ || this.getPauseWakeState() !== "active"
4606
+ || !(this.instanceState === "idle" || (allowStuck && this.instanceState === "stuck"))
4607
+ || this.pasteQueueDepth > 0) {
4608
+ this.logger.info("Deferring auto-pause: pause preconditions changed during the pane read");
4609
+ this.pauseRequested = false;
4610
+ return;
4611
+ }
4612
+ if (pane !== null && this.backend?.backgroundWorkRunning?.(pane)) {
4613
+ this.logger.info("Deferring auto-pause: background work is still running");
4614
+ this.pauseRequested = false;
4615
+ return;
4616
+ }
4617
+ }
4354
4618
  this.pauseWakeState = "pausing";
4355
4619
  this.healthCheckPaused = true;
4356
4620
  this.freezeRuntimeMonitors();
@@ -4723,10 +4987,7 @@ export class Daemon extends EventEmitter {
4723
4987
  backend: resumeBackend ?? "unknown",
4724
4988
  cwd: resumeCwd,
4725
4989
  armedAt: Date.now(),
4726
- before: readResumeFingerprint(normalizeResumeBackend(resumeBackend), {
4727
- instanceDir: this.instanceDir,
4728
- workingDirectory: resumeCwd,
4729
- }),
4990
+ before: readResumeFingerprint(normalizeResumeBackend(resumeBackend), this.resumeRoots(resumeCwd)),
4730
4991
  });
4731
4992
  }
4732
4993
  }
@@ -4772,7 +5033,7 @@ export class Daemon extends EventEmitter {
4772
5033
  && this.spawnGeneration === gateSpawn
4773
5034
  && this.launchFenceEpoch === gateFence;
4774
5035
  const backend = this.resolveResumeBackend() ?? normalizeResumeBackend(marker.backend);
4775
- const roots = { instanceDir: this.instanceDir, workingDirectory: this.config.working_directory ?? "" };
5036
+ const roots = this.resumeRoots(this.config.working_directory ?? "");
4776
5037
  // This boot's CLI spawn is what the daemon itself caused: pre-restart
4777
5038
  // writes (including pre-crash progress) are excluded, and the flush
4778
5039
  // grace holds the decision until post-spawn writes are observable.
@@ -5041,9 +5302,68 @@ export class Daemon extends EventEmitter {
5041
5302
  buildTranscriptMonitor(source) {
5042
5303
  const monitor = new TranscriptMonitor(this.instanceDir, this.logger, source);
5043
5304
  this.transcriptTurns.reset();
5044
- monitor.on("turns", (turns) => this.transcriptTurns.observe(turns));
5305
+ this.transcriptActivityAt = 0;
5306
+ // Monotonic: a wall-clock correction must neither end nor extend the open-turn hold (#1641 R1).
5307
+ const touch = () => { this.transcriptActivityAt = performance.now(); };
5308
+ monitor.on("turns", (turns) => { this.transcriptTurns.observe(turns, performance.now()); touch(); });
5309
+ // #1641 R2: a re-baselined source starts over, and so does what it said (an open turn read before is withdrawn).
5310
+ monitor.on("reset", () => { if (this.transcriptMonitor === monitor) {
5311
+ this.transcriptTurns.reset();
5312
+ this.transcriptActivityAt = 0;
5313
+ } });
5314
+ for (const event of ["tool_use", "tool_result", "assistant_text"])
5315
+ monitor.on(event, touch);
5045
5316
  return monitor;
5046
5317
  }
5318
+ /**
5319
+ * #1641: what the CLI's own transcript (codex's rollout) says about its turn. `open`: a turn started and has not ended,
5320
+ * and the transcript moved within TRANSCRIPT_OPEN_TURN_QUIET_MS. `closed`: every turn read since this launch's monitor
5321
+ * began has ended. `unknown`: not a transcript backend, nothing followed, or an open turn gone quiet (a missed end).
5322
+ */
5323
+ transcriptTurnState(now = performance.now()) {
5324
+ if (this.backend?.turnEndFromTranscript !== true || this.transcriptMonitor?.following() !== true)
5325
+ return "unknown";
5326
+ // #1641 R2: an empty ledger after a baseline that could not see the current turn's start is not "closed".
5327
+ if (this.transcriptMonitor.turnStateKnown?.() !== true)
5328
+ return "unknown";
5329
+ const since = this.transcriptTurns.openSince();
5330
+ if (since === null)
5331
+ return "closed";
5332
+ return now - Math.max(since, this.transcriptActivityAt) < TRANSCRIPT_OPEN_TURN_QUIET_MS ? "open" : "unknown";
5333
+ }
5334
+ /**
5335
+ * #1641: a stuck pane that is in fact an idle CLI (a footer this build cannot read) must not stay stuck for hours with
5336
+ * every delivery parked behind it. It recovers when the backend sees its own live, EMPTY composer with no busy, queued
5337
+ * or dialog marker (isStableUnknownLayoutIdlePane) AND the transcript shows no open turn — or, with no transcript to
5338
+ * ask, that same composer held unchanged for UNKNOWN_LAYOUT_STABLE_MS (#978's rule). An open turn never recovers.
5339
+ */
5340
+ recoverStuck(pane, snapshot, changeAt, turn) {
5341
+ const machine = this.instanceStateMachine;
5342
+ const idleComposer = this.backend?.isStableUnknownLayoutIdlePane?.(pane) === true;
5343
+ // The resume load (#1031: `Resuming session…` over the same composer) is input that is not open yet (#1641 R1).
5344
+ if (!machine || !idleComposer || turn === "open" || this.inputTransientInPane(pane)) {
5345
+ this.stuckRecoveryKey = null;
5346
+ return snapshot;
5347
+ }
5348
+ // The evidence is about this launch and this screen. Once it holds, it keeps holding — a still pane reads stuck
5349
+ // again at the next sweep (its footer is still unreadable) and must not start the window over (#1641 R1).
5350
+ const mono = performance.now();
5351
+ const key = `${this.spawnGeneration}:${this.launchAttempt}\n${pane.replace(/⋆/gu, "").replace(/[ \t]+$/gmu, "")}`;
5352
+ if (key !== this.stuckRecoveryKey) {
5353
+ this.stuckRecoveryKey = key;
5354
+ this.stuckRecoverySince = mono;
5355
+ }
5356
+ if (turn === "unknown" && mono - this.stuckRecoverySince < UNKNOWN_LAYOUT_STABLE_MS)
5357
+ return snapshot;
5358
+ // A footer this build cannot read stays unreadable: the same still pane reads stuck again at every sweep and is
5359
+ // recovered again. Only the edge into idle is news.
5360
+ const fields = { evidence: turn === "closed" ? "transcript_no_open_turn" : "stable_empty_composer", unchangedForMs: snapshot.unchangedForMs };
5361
+ if (this.instanceState !== "idle")
5362
+ this.logger.info(fields, "Stuck instance recovered — the CLI shows its idle composer and is not running a turn (#1641)");
5363
+ else
5364
+ this.logger.debug(fields, "Still idle on its unreadable footer — the stuck reading was recovered again (#1641)");
5365
+ return machine.observe(pane, Date.now(), { settled: true, changeAt, forceReady: true });
5366
+ }
5047
5367
  replyCompletionGuardEnabled() {
5048
5368
  return this.config.reply_completion_guard !== false
5049
5369
  && this.backend?.replyCompletionGuard === true;
@@ -5425,7 +5745,9 @@ export class Daemon extends EventEmitter {
5425
5745
  // a pair every ~1.6s for as long as the repaint lasted (#1116). Real
5426
5746
  // work cannot hide behind this: it changes the layout, the next
5427
5747
  // output arms another probe, and the debounce capture still fires.
5428
- if (this.instanceState === "idle" && this.backend?.isPeriodicRedrawIdlePane?.(pane) === true)
5748
+ // #1641: never while the transcript shows a turn running — that idle-looking screen is mid-turn.
5749
+ if (this.instanceState === "idle" && this.backend?.isPeriodicRedrawIdlePane?.(pane) === true
5750
+ && this.transcriptTurnState() !== "open")
5429
5751
  return;
5430
5752
  // A second burst overtook the probe. Do not bless its stale capture as
5431
5753
  // idle: continuous output is the safer working signal, while the next
@@ -5440,6 +5762,8 @@ export class Daemon extends EventEmitter {
5440
5762
  return;
5441
5763
  if (!this.observeInteractionPane(pane, captureStartedAt, interactionCaptureAt, undefined, interactionCaptureOrder, reason !== "interaction_confirmation"))
5442
5764
  return;
5765
+ // #1691: an accepted capture of this launch is a fresh read for the unrecognised-prompt evidence too (#1695 r3).
5766
+ this.noteFreshPaneRead(pane, interactionCaptureAt);
5443
5767
  this.updateInputBlockedState(pane);
5444
5768
  // Advance the shared 10s timer only for a fresh capture of this launch.
5445
5769
  const deliveryFallback = deliveryCandidate === true
@@ -5458,6 +5782,10 @@ export class Daemon extends EventEmitter {
5458
5782
  // decisions about *now*. The old double-observe expressed that by calling
5459
5783
  // observe twice, which silently disabled the "content moved" branch.
5460
5784
  const structuredPeriodicIdle = this.backend?.isPeriodicRedrawIdlePane;
5785
+ // #1641: the transcript outranks the pane: while it shows the turn running, no reading of this pane is idle (codex
5786
+ // hides its status row while streaming, leaving the idle composer and footer in view mid-turn).
5787
+ const transcriptTurn = this.transcriptTurnState();
5788
+ const transcriptBusy = transcriptTurn === "open";
5461
5789
  let snapshot;
5462
5790
  if (deliveryFallback !== null) {
5463
5791
  // A missing footer cannot fall through to a broad ready match. All
@@ -5466,7 +5794,7 @@ export class Daemon extends EventEmitter {
5466
5794
  settled: true,
5467
5795
  changeAt: observedChangeAt,
5468
5796
  forceReady: deliveryFallback,
5469
- forceBusy: !deliveryFallback || paneActivity !== null,
5797
+ forceBusy: !deliveryFallback || paneActivity !== null || transcriptBusy,
5470
5798
  });
5471
5799
  }
5472
5800
  else if (structuredPeriodicIdle && reason === "output_probe") {
@@ -5488,6 +5816,7 @@ export class Daemon extends EventEmitter {
5488
5816
  snapshot = this.instanceStateMachine.observe(pane, Date.now(), {
5489
5817
  settled: true,
5490
5818
  changeAt: observedChangeAt,
5819
+ forceBusy: transcriptBusy,
5491
5820
  });
5492
5821
  }
5493
5822
  }
@@ -5521,6 +5850,7 @@ export class Daemon extends EventEmitter {
5521
5850
  settled: true,
5522
5851
  changeAt: observedChangeAt,
5523
5852
  forceReady: true,
5853
+ forceBusy: transcriptBusy,
5524
5854
  });
5525
5855
  }
5526
5856
  else {
@@ -5569,6 +5899,7 @@ export class Daemon extends EventEmitter {
5569
5899
  snapshot = this.instanceStateMachine.observe(pane, Date.now(), {
5570
5900
  settled: true,
5571
5901
  changeAt: observedChangeAt,
5902
+ forceBusy: transcriptBusy,
5572
5903
  });
5573
5904
  }
5574
5905
  else {
@@ -5586,9 +5917,11 @@ export class Daemon extends EventEmitter {
5586
5917
  snapshot = this.instanceStateMachine.observe(pane, Date.now(), {
5587
5918
  settled,
5588
5919
  changeAt: observedChangeAt,
5589
- forceBusy: paneActivity !== null,
5920
+ forceBusy: paneActivity !== null || transcriptBusy,
5590
5921
  });
5591
5922
  }
5923
+ if (snapshot.state === "stuck")
5924
+ snapshot = this.recoverStuck(pane, snapshot, observedChangeAt, transcriptTurn);
5592
5925
  this.applyInstanceStateSnapshot(snapshot, pane);
5593
5926
  // Backends without a transcript feed can still say what they are doing, if
5594
5927
  // their TUI names it. Free-riding on a capture that already happened: no
@@ -5999,11 +6332,7 @@ export class Daemon extends EventEmitter {
5999
6332
  * is a durable delivery) must LEAD the persisted user message — restart reconciliation recognises a delivery only by a
6000
6333
  * marker at the start of the body (#1197) — so the banner goes right after it, not in front of it.
6001
6334
  */
6002
- static steerEnvelope(inbound) {
6003
- const banner = "[STEERING — mid-task course correction. Fold this into the CURRENT work if one is active.]\n";
6004
- const marker = inbound.match(/^\[agend-delivery-id:[^\]\r\n]+\]\r?\n/);
6005
- return marker ? marker[0] + banner + inbound.slice(marker[0].length) : banner + inbound;
6006
- }
6335
+ static steerEnvelope(inbound) { return steerInboundEnvelope(inbound); }
6007
6336
  /**
6008
6337
  * The one place an inbound message grows its metadata wrapper ([user:]/
6009
6338
  * [from:] prefix, pending reactions, handoff metadata, reply instructions).
@@ -6011,33 +6340,68 @@ export class Daemon extends EventEmitter {
6011
6340
  * here — a steered message must read EXACTLY like a queued one to the
6012
6341
  * agent, or the two drift apart in what the agent is told about replying.
6013
6342
  */
6343
+ /** #1650: how this instance talks to the fleet — MCP tools, or agend-agent commands (agent_mode: cli). */
6344
+ agentMode() { return this.config.agent_mode === "cli" ? "cli" : "mcp"; }
6014
6345
  formatInboundMessage(content, meta) {
6015
- const user = meta.user || "unknown";
6016
- const fromInstance = meta.from_instance;
6017
- let formatted;
6018
- if (fromInstance) {
6019
- formatted = formatCrossInstanceInboundMessage(content, meta);
6346
+ if (!meta.from_instance)
6347
+ return formatHumanInboundMessage(content, meta, this.agentMode());
6348
+ let formatted = formatCrossInstanceInboundMessage(content, meta, this.agentMode());
6349
+ if (meta.reply_to_text)
6350
+ formatted += '\n(reply_to: "' + meta.reply_to_text + '")';
6351
+ if (meta.delivery_id)
6352
+ formatted = "[agend-delivery-id:" + meta.delivery_id + "]\n" + formatted;
6353
+ return formatted;
6354
+ }
6355
+ /** Pure cached owner: no pane/process query on ingress. */
6356
+ captureHumanReceiptOwner() {
6357
+ try {
6358
+ if (this.deliveryWritesStopping || this.spawning || !this.tmux)
6359
+ return null;
6360
+ const o = this.interactionOwner();
6361
+ const window = this.tmux.getWindowId();
6362
+ if (!window || !o.bootId || ![o.spawnGeneration, o.launchAttempt, o.launchFenceEpoch, this.deliveryEpoch]
6363
+ .every(n => Number.isSafeInteger(n) && n >= 0))
6364
+ return null;
6365
+ return JSON.stringify([o.bootId, o.spawnGeneration, o.launchAttempt, o.launchFenceEpoch,
6366
+ this.deliveryEpoch, humanOwnerObjectId(this.tmux), window]);
6020
6367
  }
6021
- else {
6022
- const via = meta.source ? ` via ${meta.source}` : "";
6023
- const idTag = meta.user_id ? `, id:${meta.user_id}` : "";
6024
- formatted = `[user:${user}${via}${idTag}] ${content}`;
6025
- // Reactions queued since the last real message (#432). One leading line of
6026
- // context, present only when something is pending — a reaction no longer
6027
- // costs a turn of its own.
6028
- if (meta.pending_reactions) {
6029
- formatted = `[Recent reactions: ${meta.pending_reactions}]\n${formatted}`;
6030
- }
6031
- formatted += renderHandoffMetadata(meta);
6032
- formatted += "\n(Reply using the reply tool — do NOT respond with direct text)";
6368
+ catch {
6369
+ return null;
6033
6370
  }
6034
- if (meta.reply_to_text) {
6035
- formatted += `\n(reply_to: "${meta.reply_to_text}")`;
6371
+ }
6372
+ /** The same formatter/sanitizer used by the actual write; snapshot only. */
6373
+ humanReceiptWire(content, meta, kind) {
6374
+ let wire = kind === "raw_paste" ? content
6375
+ : !meta.from_instance && content.startsWith("/raw ") && kind === "fleet_inbound"
6376
+ ? content.slice(5) : this.formatInboundMessage(content, meta);
6377
+ if (kind === "steer")
6378
+ wire = Daemon.steerEnvelope(wire);
6379
+ else if (kind === "btw")
6380
+ wire = BTW_INBOUND_PREFIX + wire;
6381
+ return prepareDeliveryWire(wire, kind === "raw_paste");
6382
+ }
6383
+ humanReceiptAttempt(meta, wire) {
6384
+ const id = meta.human_receipt_id;
6385
+ const daemonOwner = meta.human_daemon_owner;
6386
+ if (!id || !/^[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}$/.test(id) || meta.verification_id !== "hmsg-" + id
6387
+ || !daemonOwner || !meta.human_owner_key || !/^[a-f0-9]{64}$/.test(meta.human_wire_sha256 ?? "")
6388
+ || humanBodyHash(prepareDeliveryWire(wire)) !== meta.human_wire_sha256)
6389
+ return undefined;
6390
+ return { receiptId: id, verificationId: meta.verification_id, expectedWireHash: meta.human_wire_sha256,
6391
+ ownerKey: meta.human_owner_key, current: () => this.captureHumanReceiptOwner() === daemonOwner };
6392
+ }
6393
+ finishHumanReceipt(meta, verdict) {
6394
+ if (!meta.human_receipt_id || !meta.human_owner_key)
6395
+ return;
6396
+ const attempt = verdict.humanReceipt;
6397
+ const proof = attempt?.current() && !["unverified", "output-edge", "unverifiable"].includes(verdict.proof ?? "")
6398
+ ? attempt.proof ?? null : null;
6399
+ try {
6400
+ this.emit("human_receipt_result", { receipt_id: meta.human_receipt_id, owner_key: meta.human_owner_key, proof });
6036
6401
  }
6037
- if (meta.delivery_id) {
6038
- formatted = `[agend-delivery-id:${meta.delivery_id}]\n${formatted}`;
6402
+ catch (err) {
6403
+ this.logger.error({ err }, "Could not publish human delivery evidence");
6039
6404
  }
6040
- return formatted;
6041
6405
  }
6042
6406
  /**
6043
6407
  * Whether ONE delivery reached a verdict, carried by that delivery.
@@ -6130,12 +6494,13 @@ export class Daemon extends EventEmitter {
6130
6494
  this.deferDurableDelivery(meta, "delivery epoch changed while waking for steer");
6131
6495
  return;
6132
6496
  }
6133
- const verdict = { reached: false };
6497
+ const verdict = { reached: false, humanReceipt: this.humanReceiptAttempt(meta, formatted) };
6134
6498
  try {
6135
6499
  if (await this.deliverMessage(formatted, status, {
6136
6500
  steer: true,
6137
6501
  deliveryEpoch,
6138
6502
  submissionId: meta.message_id,
6503
+ ...(meta.verification_id ? { humanVerificationId: meta.verification_id } : {}),
6139
6504
  verdict,
6140
6505
  durableAttempt: durableAttempt || undefined,
6141
6506
  })) {
@@ -6166,6 +6531,9 @@ export class Daemon extends EventEmitter {
6166
6531
  this.retryDurableDeliveryBeforeBegin(durableAttempt, err.message);
6167
6532
  throw err;
6168
6533
  }
6534
+ finally {
6535
+ this.finishHumanReceipt(meta, verdict);
6536
+ }
6169
6537
  }).catch(err => {
6170
6538
  this.deferDurableDelivery(meta, `target wake/steer failed before pane submission: ${err.message}`);
6171
6539
  this.logger.warn({ err: err.message }, "steer delivery error");
@@ -6181,24 +6549,37 @@ export class Daemon extends EventEmitter {
6181
6549
  * so channel delivery intentionally uses a normal wrapped inbound instead.
6182
6550
  */
6183
6551
  btwMessage(content, meta, deliveryEpoch = this.deliveryEpoch) {
6184
- if (!this.isDeliveryEpochCurrent(deliveryEpoch))
6552
+ if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
6553
+ this.finishHumanReceipt(meta, { reached: false });
6185
6554
  return;
6555
+ }
6186
6556
  this.updateLastChat(meta.chat_id, meta.thread_id, meta.adapter_id);
6187
6557
  this.pendingWork.recordInbound();
6188
6558
  this.recordRecentUserMessage(content, meta);
6189
- const formatted = "[BTW — side question from the user. Answer it separately without changing or interrupting the CURRENT work.]\n"
6190
- + this.formatInboundMessage(content, meta);
6559
+ const formatted = BTW_INBOUND_PREFIX + this.formatInboundMessage(content, meta);
6191
6560
  const status = channelStatus(meta);
6192
6561
  this.steerLock = this.steerLock.then(async () => {
6193
- if (!this.isDeliveryEpochCurrent(deliveryEpoch))
6562
+ if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
6563
+ this.finishHumanReceipt(meta, { reached: false });
6194
6564
  return;
6565
+ }
6195
6566
  await this.wake();
6196
- if (!this.isDeliveryEpochCurrent(deliveryEpoch))
6567
+ if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
6568
+ this.finishHumanReceipt(meta, { reached: false });
6197
6569
  return;
6198
- if (await this.deliverMessage(formatted, status, { steer: true, deliveryEpoch, submissionId: meta.message_id })) {
6199
- this.markTurnStarted(meta, formatted, deliveryEpoch);
6570
+ }
6571
+ const verdict = { reached: false, humanReceipt: this.humanReceiptAttempt(meta, formatted) };
6572
+ try {
6573
+ if (await this.deliverMessage(formatted, status, { steer: true, deliveryEpoch, submissionId: meta.message_id,
6574
+ ...(meta.verification_id ? { humanVerificationId: meta.verification_id } : {}), verdict })) {
6575
+ this.markTurnStarted(meta, formatted, deliveryEpoch);
6576
+ }
6577
+ }
6578
+ finally {
6579
+ this.finishHumanReceipt(meta, verdict);
6200
6580
  }
6201
6581
  }).catch(err => {
6582
+ this.finishHumanReceipt(meta, { reached: false });
6202
6583
  this.logger.warn({ err: err.message }, "btw delivery error");
6203
6584
  });
6204
6585
  }
@@ -6291,11 +6672,28 @@ export class Daemon extends EventEmitter {
6291
6672
  if (!this.isDeliveryEpochCurrent(deliveryEpoch))
6292
6673
  return;
6293
6674
  }
6675
+ const whatsNew = this.pendingWhatsNew ? this.currentWhatsNew(this.pendingWhatsNew) : null;
6676
+ if (whatsNew) {
6677
+ // #1633: here, inside the delivery lock and ahead of a real delivery — never into a busy turn, never to an
6678
+ // instance nobody writes to. Recorded only once pasted: a failed or cancelled paste stays pending.
6679
+ const pasted = await this.deliverMessage(whatsNew.notice, undefined, { deliveryEpoch });
6680
+ if (pasted && this.pendingWhatsNew === whatsNew) {
6681
+ this.pendingWhatsNew = null;
6682
+ try {
6683
+ recordWhatsNew(this.instanceDir, whatsNew.ids, whatsNew.version);
6684
+ }
6685
+ catch (err) {
6686
+ this.logger.warn({ err }, "what's-new record not written — the notice may repeat once");
6687
+ }
6688
+ }
6689
+ if (!this.isDeliveryEpochCurrent(deliveryEpoch))
6690
+ return;
6691
+ }
6294
6692
  const status = channelStatus(meta);
6295
6693
  // A fresh delivery begins a fresh turn — its bubble must not inherit
6296
6694
  // the previous turn's tool list.
6297
6695
  this.resetToolProgress();
6298
- verdict = { reached: false };
6696
+ verdict = { reached: false, humanReceipt: this.humanReceiptAttempt(meta, formatted) };
6299
6697
  const durableAttempt = this.durableDeliveryAttempt(meta, content);
6300
6698
  if (durableAttempt === false) {
6301
6699
  this.deferDurableDelivery(meta, "invalid durable delivery attempt metadata");
@@ -6305,6 +6703,7 @@ export class Daemon extends EventEmitter {
6305
6703
  if (await this.deliverMessage(formatted, status, {
6306
6704
  deliveryEpoch,
6307
6705
  submissionId: meta.message_id,
6706
+ ...(meta.verification_id ? { humanVerificationId: meta.verification_id } : {}),
6308
6707
  verdict,
6309
6708
  durableAttempt: durableAttempt || undefined,
6310
6709
  })) {
@@ -6352,6 +6751,7 @@ export class Daemon extends EventEmitter {
6352
6751
  this.logger.warn({ err: err.message }, "Durable pane delivery deferred after exception");
6353
6752
  }
6354
6753
  finally {
6754
+ this.finishHumanReceipt(meta, verdict);
6355
6755
  this.pasteQueueDepth--;
6356
6756
  }
6357
6757
  }).catch(err => {
@@ -6388,12 +6788,7 @@ export class Daemon extends EventEmitter {
6388
6788
  && !this.isDeliveryEpochCurrent(opts.deliveryEpoch);
6389
6789
  if (cancelled() || this.stormWindow?.isStopped())
6390
6790
  return false;
6391
- // Sanitize unclosed code fences — they cause CLI to wait for closure on Enter
6392
- const fenceCount = (formatted.match(/```/g) || []).length;
6393
- if (!opts?.preserveExactBytes && fenceCount % 2 !== 0) {
6394
- // Odd number of fences = unclosed. Remove all code fences from the message.
6395
- formatted = formatted.replace(/```/g, "");
6396
- }
6791
+ formatted = prepareDeliveryWire(formatted, opts?.preserveExactBytes);
6397
6792
  // Direct paths such as /steer, /btw, /raw and schedules do not pass the
6398
6793
  // FleetManager idle gate. Hold them at the final common pane-write path so
6399
6794
  // a tmux storm cannot force any text into a half-booted CLI.
@@ -6566,7 +6961,10 @@ export class Daemon extends EventEmitter {
6566
6961
  return stale();
6567
6962
  if (residue !== "clear")
6568
6963
  return this.failDelivery(verdict, status, "input-residue", residue);
6569
- const written = await this.writeMessageToPane(formatted, windowId, handingOffToNativeQueue, status, opts?.submissionId, verdict, opts?.durableAttempt, opts?.steer === true, writeCurrent);
6964
+ const written = await this.writeMessageToPane(formatted, windowId, handingOffToNativeQueue, status, opts?.submissionId, verdict, opts?.durableAttempt, opts?.steer === true, writeCurrent, opts?.humanVerificationId);
6965
+ // #1690: still under the lock — whatever waits on it next must already see what this write started.
6966
+ if (verdict.paneWriteStarted)
6967
+ opts?.onPaneWritten?.();
6570
6968
  // The hand-off's last capture had no box: nothing was begun or written — wait for readiness, then try again.
6571
6969
  if (written === "handoff-box-unread")
6572
6970
  return "dialog";
@@ -6614,7 +7012,148 @@ export class Daemon extends EventEmitter {
6614
7012
  * on purpose ("I trust", "Yes, continue") and would match ordinary prose.
6615
7013
  */
6616
7014
  deliveryBlockingDialogs() {
6617
- return (this.backend?.getRuntimeDialogs?.() ?? []).filter(d => d.blocksDelivery || d.holdOnly);
7015
+ return this.runtimeDialogs().filter(d => d.blocksDelivery || d.holdOnly);
7016
+ }
7017
+ /**
7018
+ * The backend's runtime dialogs, then #1691's catch-all for a prompt none of them knows (last: known ones win). The
7019
+ * catch-all is listed only while such a prompt is active: otherwise it could match nothing, and a backend with no
7020
+ * dialogs keeps its empty list (its delivery path skips the dialog probe, as before).
7021
+ */
7022
+ runtimeDialogs() {
7023
+ const dialogs = this.backend?.getRuntimeDialogs?.() ?? [];
7024
+ return this.unrecognisedPromptCurrent()?.active ? [...dialogs, this.unrecognisedPromptDialog] : dialogs;
7025
+ }
7026
+ /**
7027
+ * #1691: is the pane at the CLI's own idle composer? Then whatever looks like a prompt above it is transcript output
7028
+ * (an agent's numbered list, a quoted "[y/n]"), not a prompt. The backend's own reading, most specific first.
7029
+ */
7030
+ cliComposerShowing(pane, shape) {
7031
+ const backend = this.backend;
7032
+ if (!backend)
7033
+ return false;
7034
+ if (backend.readInputRow)
7035
+ return backend.readInputRow(pane) !== null;
7036
+ // muse: its live input box — null when the bottom of the screen is not its layout (#829), e.g. a selection menu.
7037
+ if (backend.inputDraft)
7038
+ return backend.inputDraft(pane) !== null;
7039
+ if (backend.isDeliveryInputReadyPane)
7040
+ return backend.isDeliveryInputReadyPane(pane);
7041
+ const bottom = backend.getBottomReadyPattern?.();
7042
+ if (bottom) {
7043
+ bottom.lastIndex = 0;
7044
+ return bottom.test(pane);
7045
+ }
7046
+ // No reader of its own (opencode, grok, …): its ready chrome BELOW the prompt rows — a status bar or prompt hint drawn
7047
+ // under its input box (opencode's "ctrl+p commands"), which a prompt shown in place of the input has nothing of. Not
7048
+ // the whole pane (a reply quoting "ctrl+p commands" higher up), and never the prompt's own rows: a ready pattern that
7049
+ // is just a cursor glyph would otherwise read the menu's own "❯ 1." as the CLI's prompt (#1695 r2).
7050
+ const rows = pane.replace(/\r/g, "").split("\n").map(r => r.trimEnd());
7051
+ const lastOwn = (shape.block.split("\n").at(-1) ?? "").trim();
7052
+ let end = -1;
7053
+ for (let i = rows.length - 1; i >= 0; i--)
7054
+ if (rows[i].trim() === lastOwn) {
7055
+ end = i;
7056
+ break;
7057
+ }
7058
+ if (end < 0)
7059
+ return false;
7060
+ const below = rows.slice(end + 1).filter(r => r.trim() !== "").slice(0, COMPOSER_TAIL_ROWS).join("\n");
7061
+ const ready = backend.getReadyPattern();
7062
+ ready.lastIndex = 0;
7063
+ return below !== "" && ready.test(below);
7064
+ }
7065
+ /**
7066
+ * #1691: the prompt shape this pane shows, when nothing else explains it (no known dialog, login, idle composer, and
7067
+ * the CLI is not showing its busy indicator).
7068
+ */
7069
+ unrecognisedPromptCandidate(pane) {
7070
+ const shape = unrecognisedPromptShape(pane);
7071
+ if (!shape)
7072
+ return null;
7073
+ const busy = this.backend?.getBusyPattern?.() ?? null;
7074
+ if (busy) {
7075
+ busy.lastIndex = 0;
7076
+ if (busy.test(pane))
7077
+ return null;
7078
+ }
7079
+ if ((this.backend?.getRuntimeDialogs?.() ?? []).some(dialog => Daemon.dialogMatches(dialog, pane)))
7080
+ return null;
7081
+ if (LOGIN_FLOWS[this.config.backend ?? "claude-code"]?.loginScreenActive?.(pane))
7082
+ return null;
7083
+ if (this.cliComposerShowing(pane, shape))
7084
+ return null;
7085
+ return shape;
7086
+ }
7087
+ /**
7088
+ * #1691, from each runtime scan: the same unexplained prompt block unchanged for UNRECOGNISED_PROMPT_STABLE_MS
7089
+ * becomes active (held, awaiting_input, reported once by trackDialogParked). Any other screen ends it. A turn keeps
7090
+ * changing the pane (spinner, timer, streamed text), so its clock never completes mid-turn.
7091
+ */
7092
+ trackUnrecognisedPrompt(pane, mono, outputMoved) {
7093
+ if (mono < this.unrecognisedPromptReadAt)
7094
+ return;
7095
+ this.unrecognisedPromptReadAt = mono;
7096
+ const shape = this.unrecognisedPromptCandidate(pane);
7097
+ if (!shape) {
7098
+ this.unrecognisedPrompt = null;
7099
+ return;
7100
+ }
7101
+ const screen = Daemon.stableScreen(pane);
7102
+ const owner = this.interactionOwner();
7103
+ const current = this.unrecognisedPromptCurrent();
7104
+ // Restart the minute on any change: another prompt, motion anywhere on the screen, output that arrived while this
7105
+ // capture was taken, or evidence from another launch (#1695 review).
7106
+ if (!current || current.block !== shape.block || current.screen !== screen || outputMoved) {
7107
+ this.unrecognisedPrompt = { block: shape.block, shape, since: mono, active: false, reportedElsewhere: false, screen, owner };
7108
+ return;
7109
+ }
7110
+ if (!current.active && mono - current.since >= UNRECOGNISED_PROMPT_STABLE_MS) {
7111
+ current.active = true;
7112
+ // A [y/n] the interactive-prompt detector knows was already told (interactive_prompt, after 10 s): held and
7113
+ // awaiting input from here on, but not reported a second time.
7114
+ current.reportedElsewhere = shape.kind === "yes-no" && this.interactivePromptDetector.match(pane) !== null;
7115
+ this.logger.warn({ shape: shape.kind, options: shape.options, question: shape.question, unchangedForMs: Math.round(mono - current.since) }, "CLI is waiting on a prompt AgEnD does not recognise — holding deliveries, reporting for a human (capture this pane for the per-version audit, ffb8104e)");
7116
+ }
7117
+ }
7118
+ /** #1691: the tracked evidence, if it still belongs to this launch (else it is dropped). */
7119
+ unrecognisedPromptCurrent() {
7120
+ const current = this.unrecognisedPrompt;
7121
+ if (current && (this.runtimeMonitorsFrozen || !sameInteractionOwner(current.owner, this.interactionOwner()))) {
7122
+ this.unrecognisedPrompt = null;
7123
+ return null;
7124
+ }
7125
+ return current;
7126
+ }
7127
+ /**
7128
+ * #1691: the catch-all's predicate — the tracked prompt is active and this pane still shows exactly it (any reader:
7129
+ * the scan, a probe, an interaction re-capture). Ending the evidence on a read without it is noteFreshPaneRead's.
7130
+ */
7131
+ unrecognisedPromptActive(pane) {
7132
+ const current = this.unrecognisedPromptCurrent();
7133
+ return !!current?.active && this.unrecognisedPromptCandidate(pane)?.block === current.block && Daemon.stableScreen(pane) === current.screen;
7134
+ }
7135
+ /**
7136
+ * #1691: a fresh pane read outside the scan (a delivery probe, an accepted state re-capture), taken at `mono`. When it
7137
+ * no longer shows exactly the tracked prompt, the evidence ends — counting or active, so a return needs a fresh minute
7138
+ * (#1695 r2). When it does, it is the newest confirmation. Either way only a read newer than the newest read that
7139
+ * decided the evidence counts: an older one, finishing late, says nothing about now (#1695 r3, r4).
7140
+ */
7141
+ noteFreshPaneRead(pane, mono) {
7142
+ if (mono < this.unrecognisedPromptReadAt)
7143
+ return;
7144
+ // Advanced even with nothing tracked: a scan whose capture is older must not start evidence this read contradicts
7145
+ // (#1695 r5).
7146
+ this.unrecognisedPromptReadAt = mono;
7147
+ const current = this.unrecognisedPromptCurrent();
7148
+ if (!current)
7149
+ return;
7150
+ if (this.unrecognisedPromptCandidate(pane)?.block === current.block && Daemon.stableScreen(pane) === current.screen)
7151
+ return;
7152
+ this.unrecognisedPrompt = null;
7153
+ }
7154
+ /** #1691: the screen as the stability clock compares it (trailing blanks are not motion). */
7155
+ static stableScreen(pane) {
7156
+ return pane.replace(/\r/g, "").split("\n").map(r => r.trimEnd()).join("\n").trimEnd();
6618
7157
  }
6619
7158
  /** Passive input-unavailable phases relevant to the current spawn only. */
6620
7159
  guardedInputTransients() {
@@ -6782,7 +7321,7 @@ export class Daemon extends EventEmitter {
6782
7321
  return dialog.isActive ? dialog.isActive(pane) : dialog.pattern.test(pane);
6783
7322
  }
6784
7323
  /** Refresh the separate stdin-blocked observation from one pane capture. */
6785
- updateInputBlockedState(pane, dialogs = this.backend?.getRuntimeDialogs?.() ?? []) {
7324
+ updateInputBlockedState(pane, dialogs = this.runtimeDialogs()) {
6786
7325
  const active = dialogs.find(dialog => dialog.inputBlocked && Daemon.dialogMatches(dialog, pane)) ?? null;
6787
7326
  const nextKey = active ? Daemon.dialogKey(active) : null;
6788
7327
  if (nextKey !== this.inputBlockedDialogKey) {
@@ -6870,9 +7409,15 @@ export class Daemon extends EventEmitter {
6870
7409
  async probeBlockingDialog() {
6871
7410
  if (!this.tmux || !this.backend)
6872
7411
  return { state: "clear" };
7412
+ // #1691: a probe is a fresh read — it may end unrecognised-prompt evidence even when no dialog is listed.
7413
+ const tracking = this.unrecognisedPrompt !== null;
6873
7414
  const dialogs = this.deliveryBlockingDialogs();
6874
- if (dialogs.length === 0)
7415
+ // #1690: our own switch is being answered — nothing else types into the pane until it has settled.
7416
+ if (this.ownSwitch && dialogs.length > 0)
7417
+ return { state: "dialog", dialog: dialogs[0] };
7418
+ if (dialogs.length === 0 && !tracking)
6875
7419
  return { state: "clear" };
7420
+ const probeMono = performance.now();
6876
7421
  let pane;
6877
7422
  try {
6878
7423
  pane = await this.tmux.capturePane();
@@ -6881,6 +7426,7 @@ export class Daemon extends EventEmitter {
6881
7426
  this.logger.debug({ err }, "capture-pane failed during the dialog probe — pane state unknown");
6882
7427
  return { state: "unknown" };
6883
7428
  }
7429
+ this.noteFreshPaneRead(pane, probeMono);
6884
7430
  for (const dialog of dialogs) {
6885
7431
  if (!Daemon.dialogMatches(dialog, pane))
6886
7432
  continue;
@@ -7090,11 +7636,16 @@ export class Daemon extends EventEmitter {
7090
7636
  this.dialogParkedReported = false;
7091
7637
  return;
7092
7638
  }
7093
- if (!this.dialogParkedReported && Date.now() - this.dialogParkedSince >= DIALOG_PARKED_NOTIFY_MS) {
7639
+ // #1691: the catch-all waited its minute before it became active; it is reported on the next scan.
7640
+ const unrecognised = dialog === this.unrecognisedPromptDialog;
7641
+ if (unrecognised && this.unrecognisedPrompt?.reportedElsewhere)
7642
+ this.dialogParkedReported = true;
7643
+ if (!this.dialogParkedReported && Date.now() - this.dialogParkedSince >= (unrecognised ? 0 : DIALOG_PARKED_NOTIFY_MS)) {
7094
7644
  this.dialogParkedReported = true;
7095
7645
  this.logger.warn({ dialog: dialog.description, parkedForMs: Date.now() - this.dialogParkedSince }, "CLI dialog is still on screen — not auto-answering, reporting for a human");
7096
7646
  this.emit("dialog_parked", { name: this.name, description: dialog.description, holdOnly: dialog.holdOnly === true,
7097
- ...(episode !== null && interaction.kind ? { kind: interaction.kind, episode, backend: this.config.backend ?? "claude-code" } : {}) });
7647
+ ...(episode !== null && interaction.kind ? { kind: interaction.kind, episode, backend: this.config.backend ?? "claude-code" } : {}),
7648
+ ...(unrecognised ? { unrecognised: true, question: this.unrecognisedPrompt?.shape.question ?? null } : {}) });
7098
7649
  }
7099
7650
  }
7100
7651
  /**
@@ -7205,7 +7756,7 @@ export class Daemon extends EventEmitter {
7205
7756
  return false;
7206
7757
  const row = norm(shown);
7207
7758
  // The marker line holds nothing but the marker: the row is exactly it.
7208
- if (/^\[agend-delivery-id:[^\]\s]+\]$/.test(first))
7759
+ if (/^\[agend-(?:delivery|human)-id:[^\]\s]+\]$/.test(first))
7209
7760
  return row === first;
7210
7761
  return row === first || row.startsWith(`${first} `);
7211
7762
  }
@@ -7663,6 +8214,33 @@ export class Daemon extends EventEmitter {
7663
8214
  this.logger.warn("Could not read the pane after Enter — not confirming the submission");
7664
8215
  return sawOutput && residue === "absent";
7665
8216
  }
8217
+ /**
8218
+ * #1647: whether a raw paste on a pane with no transcript (kiro) was taken: its text gone from the input row (or the
8219
+ * CLI's own busy marker shown instead — pasteResidue's rule) with output after the Enter. That is the confirmation's
8220
+ * own rule, looked at more than once: kiro repaints its composer late, so the single look right after Enter misread
8221
+ * pastes it took. A generic `working` reading is not evidence (#1663 P1-3: a repaint looks like work).
8222
+ *
8223
+ * Bounded by the budget that confirmation already has, measured from the Enter (#1663 P2-1): the busy wait plus the
8224
+ * post-Enter proof window. Every reading is asked again after its await — still this write's owner (`current`: the
8225
+ * caller's fence, no stop, the same tmux and spawn, #1663 P2-2) and still inside the budget — before it counts; a late
8226
+ * or fenced answer proves nothing. Null: not shown — the caller settles uncertain, never a second Enter.
8227
+ */
8228
+ async rawPasteTakenOnPane(windowId, enterAt, enterMono, formatted, current) {
8229
+ const deadline = enterMono + CONFIRM_BUSY_POLLS * CONFIRM_BUSY_POLL_MS + POST_ENTER_PROOF_WINDOW_MS;
8230
+ const live = () => current() && performance.now() <= deadline;
8231
+ for (;;) {
8232
+ if (!live())
8233
+ return null;
8234
+ if (this.controlClient?.hasOutputSince?.(windowId, enterAt)) {
8235
+ const residue = await this.pasteResidue(formatted);
8236
+ if (!live())
8237
+ return null;
8238
+ if (residue === "absent")
8239
+ return RAW_PASTE_LEFT_INPUT;
8240
+ }
8241
+ await new Promise(r => setTimeout(r, POST_ENTER_PROOF_POLL_MS));
8242
+ }
8243
+ }
7666
8244
  /** Once Kiro returns to its prompt, its unique envelope id in history proves submission. */
7667
8245
  async kiroSubmissionEvidence(windowId, formatted, signature) {
7668
8246
  if (!this.tmux)
@@ -7720,6 +8298,8 @@ export class Daemon extends EventEmitter {
7720
8298
  if (!current())
7721
8299
  return false;
7722
8300
  if (evidence === "submitted") {
8301
+ if (pending.signature.unique)
8302
+ pending.signature.humanPositive?.("unique-id-pane");
7723
8303
  if (status)
7724
8304
  this.emit("message_confirmed", status);
7725
8305
  return true;
@@ -7755,6 +8335,8 @@ export class Daemon extends EventEmitter {
7755
8335
  if (retry === "cancelled")
7756
8336
  return false;
7757
8337
  if (retry === "submitted") {
8338
+ if (pending.signature.unique)
8339
+ pending.signature.humanPositive?.("unique-id-pane");
7758
8340
  if (status)
7759
8341
  this.emit("message_confirmed", status);
7760
8342
  return true;
@@ -7895,9 +8477,24 @@ export class Daemon extends EventEmitter {
7895
8477
  verdict = { reached: false }, durableAttempt, steer = false,
7896
8478
  // #829: the caller's fence, asked once more after the last await before the
7897
8479
  // first side effect. Stale means unwritten: false, with no failure verdict.
7898
- stillCurrent) {
7899
- const signature = this.submissionSignature(formatted, submissionId);
8480
+ stillCurrent, humanVerificationId) {
8481
+ const signature = this.submissionSignature(formatted, humanVerificationId ?? submissionId, humanVerificationId !== undefined);
7900
8482
  const rawPaste = durableAttempt?.submissionMode === "raw_paste";
8483
+ const humanAttempt = verdict.humanReceipt;
8484
+ const humanMode = rawPaste ? "raw_paste" : steer ? "steer"
8485
+ : handingOffToNativeQueue ? "native_queue_handoff" : "idle_submit";
8486
+ if (humanAttempt && signature.unique && signature.value === "agend-human-id:" + humanAttempt.verificationId
8487
+ && humanBodyHash(formatted) === humanAttempt.expectedWireHash) {
8488
+ signature.humanPositive = tag => {
8489
+ if (!humanAttempt.current() || (stillCurrent && !stillCurrent()))
8490
+ return;
8491
+ humanAttempt.proof = {
8492
+ receipt_id: humanAttempt.receiptId, verification_id: humanAttempt.verificationId,
8493
+ wire_sha256: humanBodyHash(formatted), owner_key: humanAttempt.ownerKey,
8494
+ mode: humanMode, tag,
8495
+ };
8496
+ };
8497
+ }
7901
8498
  let windowId = initialWindowId;
7902
8499
  // The spawn this write belongs to: acceptance evidence is only read while it is still the current one (#1197).
7903
8500
  const spawnAtWrite = this.spawnGeneration;
@@ -7975,9 +8572,11 @@ export class Daemon extends EventEmitter {
7975
8572
  offset: attemptEvidence.transcriptOffset,
7976
8573
  };
7977
8574
  // From here every proof of this write may also ask the CLI's transcript (the pane can lose the echo to the
7978
- // reply, or still be painting the paste) — this delivery's exact marker only.
7979
- if (["claude-code", "codex"].includes(attemptEvidence.backend)) {
7980
- signature.transcript = this.transcriptProofFor(verdict.transcriptCheckpoint, durableAttempt.deliveryId, spawnAtWrite, stillCurrent, kind => { verdict.transcriptProof = kind; });
8575
+ // reply, or still be painting the paste) — this delivery's exact marker only, or a raw paste's exact text (#1647).
8576
+ // A raw paste's text only where the transcript is this instance's alone (RAW_TEXT_TRANSCRIPT_BACKENDS).
8577
+ if (["claude-code", "codex"].includes(attemptEvidence.backend)
8578
+ && (!rawPaste || RAW_TEXT_TRANSCRIPT_BACKENDS.includes(attemptEvidence.backend))) {
8579
+ signature.transcript = this.transcriptProofFor(verdict.transcriptCheckpoint, rawPaste ? { rawText: formatted } : durableAttempt.deliveryId, spawnAtWrite, stillCurrent, kind => { verdict.transcriptProof = kind; });
7981
8580
  }
7982
8581
  }
7983
8582
  }
@@ -8041,6 +8640,7 @@ export class Daemon extends EventEmitter {
8041
8640
  // positively hold typed text, and after it be the empty placeholder again. The empty placeholder here means the
8042
8641
  // paste never reached the box; an unreadable box (a failed or late capture, a dialog) proves nothing: no Enter.
8043
8642
  const steerComposerProof = handingOffToNativeQueue && steer && !!this.backend?.readSteerComposer && tmuxAtWrite !== null;
8643
+ let humanComposerOwned = false;
8044
8644
  let steerEnterDeadline = Infinity;
8045
8645
  if (steerComposerProof) {
8046
8646
  const deadline = performance.now() + STEER_PASTE_VISIBLE_MS;
@@ -8064,10 +8664,13 @@ export class Daemon extends EventEmitter {
8064
8664
  return this.failDelivery(verdict, status, "steer-paste", before === null ? "box-unread" : `box-still-${before.mode}`);
8065
8665
  if (!this.steerPasteIsOurs(before.pane, formatted))
8066
8666
  return this.failDelivery(verdict, status, "steer-paste", "box-not-this-paste");
8667
+ const shown = this.backend?.readSteerComposerText?.(before.pane)?.replace(/\s+/g, " ").trim();
8668
+ humanComposerOwned = !!humanAttempt && shown === "[agend-human-id:" + humanAttempt.verificationId + "]";
8067
8669
  // …and so is the Enter: the key is sent only while the reading is still within it.
8068
8670
  steerEnterDeadline = deadline;
8069
8671
  }
8070
8672
  let enterAt = Date.now();
8673
+ const enterMono = performance.now();
8071
8674
  const steerEnterCurrent = () => steerCurrent() && performance.now() <= steerEnterDeadline;
8072
8675
  if (!(await this.sendDeliveryEnter("initial-submit", steerComposerProof ? steerEnterCurrent : undefined, durableAttempt))) {
8073
8676
  if (steerComposerProof && !steerCurrent()) {
@@ -8134,6 +8737,9 @@ export class Daemon extends EventEmitter {
8134
8737
  if (after === "steer" || after === "queue" || after === "idle") {
8135
8738
  const probe = await withinMs(this.probeBlockingDialog(), deadline - performance.now());
8136
8739
  if (probe?.state === "clear" && performance.now() <= deadline && steerCurrent() && this.getWindowId() === windowId) {
8740
+ if (humanComposerOwned)
8741
+ signature.humanPositive?.(after === "steer" ? "composer-empty-steer"
8742
+ : after === "queue" ? "composer-empty-queue" : "composer-empty-idle");
8137
8743
  verdict.phase = "steer-accepted";
8138
8744
  verdict.proof = `steer-composer-${after}`;
8139
8745
  if (status)
@@ -8313,12 +8919,20 @@ export class Daemon extends EventEmitter {
8313
8919
  return { kind: "kiro-pending", windowId, spawnGeneration: this.spawnGeneration, signature };
8314
8920
  }
8315
8921
  }
8316
- if (!submitted) {
8317
- if (rawPaste) {
8922
+ if (!submitted && rawPaste) {
8923
+ // #1647: kiro paints output at once but its composer late, so one residue look right after Enter misreads a
8924
+ // paste it took. Keep looking within the confirmation's own budget from the Enter, under this write's fence;
8925
+ // never a second Enter (a raw command must not run twice).
8926
+ const taken = await this.rawPasteTakenOnPane(windowId, enterAt, enterMono, formatted, steerCurrent);
8927
+ if (!taken) {
8318
8928
  verdict.phase = "raw-paste-submit-proof";
8319
8929
  verdict.proof = "no-positive-acceptance-proof";
8320
8930
  return false;
8321
8931
  }
8932
+ verdict.proof = taken;
8933
+ submitted = true;
8934
+ }
8935
+ if (!submitted) {
8322
8936
  // Legacy system pastes have no unique envelope id. Preserve their
8323
8937
  // existing bounded retry until they can be tied to a specific turn.
8324
8938
  this.logger.warn("Message not submitted after Enter — waiting for the prompt, then re-sending Enter once");
@@ -8631,10 +9245,10 @@ export class Daemon extends EventEmitter {
8631
9245
  * A write's transcript proof: bounded looks from its checkpoint, fenced to the write — its spawn and launch, a stop,
8632
9246
  * and the caller's own fence — so a look that lands after any of them is never used (#1380 review).
8633
9247
  */
8634
- transcriptProofFor(checkpoint, deliveryId, spawnAtWrite, stillCurrent, onProof) {
9248
+ transcriptProofFor(checkpoint, needle, spawnAtWrite, stillCurrent, onProof) {
8635
9249
  const launchAtWrite = this.launchFenceEpoch;
8636
9250
  return {
8637
- reader: new TranscriptDeltaReader(checkpoint.path, checkpoint.offset, checkpoint.backend, deliveryId),
9251
+ reader: new TranscriptDeltaReader(checkpoint.path, checkpoint.offset, checkpoint.backend, needle),
8638
9252
  current: () => !this.deliveryWritesStopping && this.spawnGeneration === spawnAtWrite
8639
9253
  && this.launchFenceEpoch === launchAtWrite && (stillCurrent?.() ?? true),
8640
9254
  onProof,
@@ -8712,8 +9326,11 @@ export class Daemon extends EventEmitter {
8712
9326
  // the input row is proof in itself — which also means a momentary
8713
9327
  // failure to read the pane BEFORE pasting cannot turn a delivered
8714
9328
  // message into a re-paste.
8715
- if (signature.unique && after.payload > 0)
9329
+ if (signature.unique && after.payload > 0) {
9330
+ if (after.inputReadable)
9331
+ signature.humanPositive?.("unique-id-pane");
8716
9332
  return this.submittedProof();
9333
+ }
8717
9334
  if (unattributedResidue)
8718
9335
  return "unverifiable";
8719
9336
  // 3. Otherwise the evidence must be NEW relative to the pane as it was
@@ -8850,7 +9467,7 @@ export class Daemon extends EventEmitter {
8850
9467
  * paste that never landed. System pastes carry no envelope and fall back to
8851
9468
  * the body signature, which the baseline comparison still ties to this paste.
8852
9469
  */
8853
- submissionSignature(formatted, trustedId) {
9470
+ submissionSignature(formatted, trustedId, verification = false) {
8854
9471
  // The id must come from the message's own metadata, never from scanning the
8855
9472
  // rendered text: agents and users discuss message ids in the body all the
8856
9473
  // time ("check message_id: abc"), and a body-scanned value would match an
@@ -8859,7 +9476,7 @@ export class Daemon extends EventEmitter {
8859
9476
  // the handoff metadata identifies THIS delivery.
8860
9477
  const id = trustedId?.trim();
8861
9478
  if (id)
8862
- return { value: `message_id:${id}`, unique: true };
9479
+ return { value: (verification ? "agend-human-id:" : "message_id:") + id, unique: true };
8863
9480
  return { value: pastedTextSignature(formatted), unique: false };
8864
9481
  }
8865
9482
  /**
@@ -9381,6 +9998,11 @@ export class Daemon extends EventEmitter {
9381
9998
  respond(null, `reply: ${buttons.error}`);
9382
9999
  return;
9383
10000
  }
10001
+ // #1823: a poll's results need the fleet's store and outbox; as text it would never close.
10002
+ if (args.poll !== undefined && args.poll !== null) {
10003
+ respond(null, "reply: a poll needs AgEnD's fleet; send the question without poll");
10004
+ return;
10005
+ }
9384
10006
  if (buttons) {
9385
10007
  routed = { ...args, text: `${String(args.text)}\n\n${replyButtonsFallbackText(buttons.buttons)}` };
9386
10008
  delete routed.buttons;
@@ -9966,7 +10588,7 @@ export class Daemon extends EventEmitter {
9966
10588
  * the same tmux window, not aborted, writes not stopping — re-checked after every await, and between the paste and
9967
10589
  * its Enter. The whole step, every await included, ends at one monotonic deadline: a late capture never confirms
9968
10590
  * and a late admission never writes; a paste whose Enter would land past it is taken back. Not confirmed in time:
9969
- * the old setup stays, a launch warning says so, the next launch tries again. A stop, pause or respawn ends it
10591
+ * the old setup stays, the next launch tries again, and a quiet launch warning (#1794: logged and on the Log tab, not posted) says so. A stop, pause or respawn ends it
9970
10592
  * silently, confirming and removing nothing. Never throws into the spawn.
9971
10593
  */
9972
10594
  async ensureBackendAgent() {
@@ -10012,8 +10634,10 @@ export class Daemon extends EventEmitter {
10012
10634
  if (active === sw.agent && (wroteAt === null || capturedAt > wroteAt)) {
10013
10635
  if (sw.alreadyConfirmed && wroteAt === null)
10014
10636
  return; // already ours, already on record
10015
- for (const warning of sw.confirm())
10016
- this.emit("backend_launch_warning", { name: this.name, message: warning });
10637
+ for (const { message, quiet } of sw.confirm()) {
10638
+ this.logger.warn({ agent: sw.agent, warning: message }, "The agent switch was confirmed with a warning");
10639
+ this.emit("backend_launch_warning", { name: this.name, message, quiet: quiet === true });
10640
+ }
10017
10641
  this.logger.info({ agent: sw.agent, switched: wroteAt !== null }, "The resumed conversation runs as this instance's agent");
10018
10642
  return;
10019
10643
  }
@@ -10062,7 +10686,9 @@ export class Daemon extends EventEmitter {
10062
10686
  if (!fenced())
10063
10687
  return; // a stop, pause or respawn: nothing to say, nothing to do
10064
10688
  this.logger.warn({ agent: sw.agent, typed: wroteAt !== null }, "The resumed conversation did not switch to this instance's agent in time");
10065
- this.emit("backend_launch_warning", { name: this.name, message: t("kiro.switch_timeout", Daemon.AGENT_SWITCH_BUDGET_MS / 1000) });
10689
+ // #1794: quiet — the conversation keeps running as its saved agent with the shared entries, and the next launch
10690
+ // tries again; the user loses nothing and has nothing to do. Logged above and kept on the Log tab only.
10691
+ this.emit("backend_launch_warning", { name: this.name, message: t("kiro.switch_timeout", Daemon.AGENT_SWITCH_BUDGET_MS / 1000), quiet: true });
10066
10692
  }
10067
10693
  catch (err) {
10068
10694
  this.startupAdmission?.();
@@ -10426,6 +11052,49 @@ export class Daemon extends EventEmitter {
10426
11052
  }
10427
11053
  catch { /* non-fatal */ }
10428
11054
  }
11055
+ /**
11056
+ * #1633 (#1639 review): the pending notice as the record stands NOW, just before the paste — it was decided at launch,
11057
+ * and the record may have changed while the delivery waited. Unreadable → nothing is sent (and nothing pending);
11058
+ * entries recorded meanwhile are dropped from it; none left → nothing.
11059
+ */
11060
+ currentWhatsNew(pending) {
11061
+ const record = readWhatsNewRecord(this.instanceDir);
11062
+ if (record === "unreadable") {
11063
+ this.pendingWhatsNew = null;
11064
+ return null;
11065
+ }
11066
+ const known = new Set(record?.announced ?? []);
11067
+ const ids = pending.ids.filter(id => !known.has(id));
11068
+ if (!ids.length) {
11069
+ this.pendingWhatsNew = null;
11070
+ return null;
11071
+ }
11072
+ if (ids.length === pending.ids.length)
11073
+ return pending;
11074
+ const narrowed = { notice: whatsNewNotice(pending.version, AGENT_WHATS_NEW.filter(e => ids.includes(e.id))), ids, version: pending.version };
11075
+ this.pendingWhatsNew = narrowed;
11076
+ return narrowed;
11077
+ }
11078
+ /** #1633: what this instance has not been told about its AgEnD version yet (see agent-whats-new.ts). */
11079
+ prepareWhatsNew(fresh, version = agendVersion()) {
11080
+ try {
11081
+ const plan = planWhatsNew({ record: readWhatsNewRecord(this.instanceDir), fresh, version });
11082
+ if (!plan) {
11083
+ this.pendingWhatsNew = null;
11084
+ return;
11085
+ }
11086
+ if ("silent" in plan) {
11087
+ recordWhatsNew(this.instanceDir, plan.silent, plan.version);
11088
+ this.pendingWhatsNew = null;
11089
+ return;
11090
+ }
11091
+ this.pendingWhatsNew = plan;
11092
+ this.logger.info({ entries: plan.ids }, "what's-new notice pending for the next delivery");
11093
+ }
11094
+ catch (err) {
11095
+ this.logger.debug({ err }, "what's-new not prepared");
11096
+ }
11097
+ }
10429
11098
  async trySpawnInsideGate(reuseWindow = false, startupTimeoutMs) {
10430
11099
  this.startupAdmission?.();
10431
11100
  const launchFence = this.launchFenceEpoch;
@@ -10435,6 +11104,8 @@ export class Daemon extends EventEmitter {
10435
11104
  // instructionsReloadedOnResume) so the warmup steering-reload can be
10436
11105
  // skipped when nothing changed.
10437
11106
  this.lastBuiltInstructions = backendConfig.instructions ?? "";
11107
+ // #1633: decided before prev-instructions is first written — its absence is what says "never launched before".
11108
+ this.prepareWhatsNew(!existsSync(join(this.instanceDir, "prev-instructions")));
10438
11109
  {
10439
11110
  const prevFile = join(this.instanceDir, "prev-instructions");
10440
11111
  let prev = "";
@@ -10476,7 +11147,7 @@ export class Daemon extends EventEmitter {
10476
11147
  if (launchFence !== this.launchFenceEpoch)
10477
11148
  throw new Error("Launch cancelled: the instance was stopped or paused while the launch was being prepared");
10478
11149
  this.backend.writeConfig(backendConfig);
10479
- this.backend.preTrust?.(this.config.working_directory);
11150
+ this.backend.preTrust?.(this.config.working_directory, backendConfig);
10480
11151
  // Resolve working directory (e.g. symlink for hidden paths)
10481
11152
  const resolvedCwd = this.backend.resolveWorkingDirectory?.(this.config.working_directory, this.name) ?? this.config.working_directory;
10482
11153
  // Generate a fresh per-instance agent token each spawn. agent-cli reads
@@ -10546,6 +11217,11 @@ export class Daemon extends EventEmitter {
10546
11217
  this.logger.warn({ warning: launchWarning }, "Backend launch fell back");
10547
11218
  this.emit("backend_launch_warning", { name: this.name, message: launchWarning });
10548
11219
  }
11220
+ // #1811: what the launch left out and why (an effort this CLI or model does not take) — the Log tab, not the chat.
11221
+ for (const note of this.backend.consumeQuietLaunchNotes?.() ?? []) {
11222
+ this.logger.info({ note }, "Launch note");
11223
+ this.emit("backend_launch_warning", { name: this.name, message: note, quiet: true });
11224
+ }
10549
11225
  // Ensure tmux session exists (may have been destroyed if all windows died)
10550
11226
  await TmuxManager.ensureSession(this.tmuxSessionName);
10551
11227
  if (this.stormWindow?.observeServerAlive(await TmuxManager.getServerPid(this.tmuxSessionName))) {
@@ -11116,6 +11792,14 @@ export class Daemon extends EventEmitter {
11116
11792
  }, "Snapshot written");
11117
11793
  return snapshot;
11118
11794
  }
11795
+ /** #1699: the claude config dir this instance's CLI uses — its credential profile's, else exactly as before. */
11796
+ claudeConfigDir() {
11797
+ return claudeConfigDirFor(claudeProfileConfigDir(this.config.backend_options?.["claude-code"]));
11798
+ }
11799
+ /** Where this instance's CLI keeps the conversation a turn resume is judged on (#1699: claude's follows the profile). */
11800
+ resumeRoots(workingDirectory) {
11801
+ return { instanceDir: this.instanceDir, workingDirectory, claudeConfigDir: this.claudeConfigDir() };
11802
+ }
11119
11803
  /**
11120
11804
  * The credential profile's store this instance's CLI writes to, if any.
11121
11805
  *