@songsid/agend 2.2.0-beta.2 → 2.2.0-beta.20

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 (389) hide show
  1. package/README.md +40 -24
  2. package/README.zh-TW.md +40 -24
  3. package/dist/agent-cli-instructions.md +6 -2
  4. package/dist/agent-cli.js +15 -7
  5. package/dist/agent-cli.js.map +1 -1
  6. package/dist/agent-endpoint.js +3 -0
  7. package/dist/agent-endpoint.js.map +1 -1
  8. package/dist/agent-whats-new.js +16 -4
  9. package/dist/agent-whats-new.js.map +1 -1
  10. package/dist/auth-api.d.ts +3 -1
  11. package/dist/auth-api.js +17 -1
  12. package/dist/auth-api.js.map +1 -1
  13. package/dist/backend/antigravity.d.ts +33 -0
  14. package/dist/backend/antigravity.js +126 -18
  15. package/dist/backend/antigravity.js.map +1 -1
  16. package/dist/backend/claude-carry.d.ts +22 -0
  17. package/dist/backend/claude-carry.js +170 -0
  18. package/dist/backend/claude-carry.js.map +1 -0
  19. package/dist/backend/claude-code.d.ts +65 -2
  20. package/dist/backend/claude-code.js +226 -33
  21. package/dist/backend/claude-code.js.map +1 -1
  22. package/dist/backend/codex-metadata.d.ts +33 -0
  23. package/dist/backend/codex-metadata.js +122 -18
  24. package/dist/backend/codex-metadata.js.map +1 -1
  25. package/dist/backend/codex.d.ts +3 -0
  26. package/dist/backend/codex.js +17 -1
  27. package/dist/backend/codex.js.map +1 -1
  28. package/dist/backend/credential-profile.d.ts +40 -3
  29. package/dist/backend/credential-profile.js +113 -5
  30. package/dist/backend/credential-profile.js.map +1 -1
  31. package/dist/backend/effort-metadata.d.ts +37 -1
  32. package/dist/backend/effort-metadata.js +86 -15
  33. package/dist/backend/effort-metadata.js.map +1 -1
  34. package/dist/backend/grok.d.ts +4 -0
  35. package/dist/backend/grok.js +14 -0
  36. package/dist/backend/grok.js.map +1 -1
  37. package/dist/backend/kiro.d.ts +27 -1
  38. package/dist/backend/kiro.js +63 -10
  39. package/dist/backend/kiro.js.map +1 -1
  40. package/dist/backend/launch-effort.d.ts +61 -0
  41. package/dist/backend/launch-effort.js +170 -0
  42. package/dist/backend/launch-effort.js.map +1 -0
  43. package/dist/backend/muse.d.ts +84 -0
  44. package/dist/backend/muse.js +449 -53
  45. package/dist/backend/muse.js.map +1 -1
  46. package/dist/backend/types.d.ts +48 -2
  47. package/dist/backend/types.js +12 -1
  48. package/dist/backend/types.js.map +1 -1
  49. package/dist/backend-command.d.ts +91 -0
  50. package/dist/backend-command.js +122 -0
  51. package/dist/backend-command.js.map +1 -0
  52. package/dist/background-work-ended.d.ts +18 -0
  53. package/dist/background-work-ended.js +30 -0
  54. package/dist/background-work-ended.js.map +1 -0
  55. package/dist/cache-service.d.ts +2 -0
  56. package/dist/cache-service.js +2 -1
  57. package/dist/cache-service.js.map +1 -1
  58. package/dist/channel/adapters/discord.d.ts +26 -1
  59. package/dist/channel/adapters/discord.js +103 -3
  60. package/dist/channel/adapters/discord.js.map +1 -1
  61. package/dist/channel/adapters/telegram.d.ts +35 -3
  62. package/dist/channel/adapters/telegram.js +122 -23
  63. package/dist/channel/adapters/telegram.js.map +1 -1
  64. package/dist/channel/mcp-tools.js +5 -3
  65. package/dist/channel/mcp-tools.js.map +1 -1
  66. package/dist/channel/message-edit-errors.d.ts +5 -0
  67. package/dist/channel/message-edit-errors.js +24 -0
  68. package/dist/channel/message-edit-errors.js.map +1 -0
  69. package/dist/channel/telegram-markdown.d.ts +6 -0
  70. package/dist/channel/telegram-markdown.js +104 -0
  71. package/dist/channel/telegram-markdown.js.map +1 -0
  72. package/dist/channel/tool-router.d.ts +2 -0
  73. package/dist/channel/tool-router.js +6 -5
  74. package/dist/channel/tool-router.js.map +1 -1
  75. package/dist/channel/types.d.ts +46 -4
  76. package/dist/cli.js +184 -34
  77. package/dist/cli.js.map +1 -1
  78. package/dist/command-table.d.ts +2 -2
  79. package/dist/command-table.js +5 -2
  80. package/dist/command-table.js.map +1 -1
  81. package/dist/compact-watch.d.ts +86 -0
  82. package/dist/compact-watch.js +288 -0
  83. package/dist/compact-watch.js.map +1 -0
  84. package/dist/config-validator.js +55 -5
  85. package/dist/config-validator.js.map +1 -1
  86. package/dist/config.d.ts +1 -0
  87. package/dist/config.js +2 -1
  88. package/dist/config.js.map +1 -1
  89. package/dist/cross-instance-envelope.js +2 -0
  90. package/dist/cross-instance-envelope.js.map +1 -1
  91. package/dist/daemon.d.ts +129 -1
  92. package/dist/daemon.js +754 -94
  93. package/dist/daemon.js.map +1 -1
  94. package/dist/delivery-outbox.d.ts +64 -8
  95. package/dist/delivery-outbox.js +315 -24
  96. package/dist/delivery-outbox.js.map +1 -1
  97. package/dist/delivery-reconciliation.d.ts +18 -1
  98. package/dist/delivery-reconciliation.js +67 -2
  99. package/dist/delivery-reconciliation.js.map +1 -1
  100. package/dist/doctor.js +7 -0
  101. package/dist/doctor.js.map +1 -1
  102. package/dist/fleet-context.d.ts +12 -1
  103. package/dist/fleet-manager.d.ts +392 -19
  104. package/dist/fleet-manager.js +2030 -275
  105. package/dist/fleet-manager.js.map +1 -1
  106. package/dist/forged-envelope-scan.d.ts +4 -3
  107. package/dist/forged-envelope-scan.js +4 -3
  108. package/dist/forged-envelope-scan.js.map +1 -1
  109. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +84 -16
  110. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +16 -1
  111. package/dist/general-knowledge/skills/model-discovery/SKILL.md +4 -1
  112. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +4 -0
  113. package/dist/host-memory.d.ts +10 -0
  114. package/dist/host-memory.js +33 -1
  115. package/dist/host-memory.js.map +1 -1
  116. package/dist/human-delivery-receipts.d.ts +83 -0
  117. package/dist/human-delivery-receipts.js +194 -0
  118. package/dist/human-delivery-receipts.js.map +1 -0
  119. package/dist/install-lock.d.ts +18 -1
  120. package/dist/install-lock.js +30 -3
  121. package/dist/install-lock.js.map +1 -1
  122. package/dist/instance-avatar.d.ts +5 -4
  123. package/dist/instance-avatar.js +12 -8
  124. package/dist/instance-avatar.js.map +1 -1
  125. package/dist/instance-config-impact.js +1 -0
  126. package/dist/instance-config-impact.js.map +1 -1
  127. package/dist/instance-lifecycle.d.ts +25 -6
  128. package/dist/instance-lifecycle.js +76 -27
  129. package/dist/instance-lifecycle.js.map +1 -1
  130. package/dist/instructions.d.ts +1 -1
  131. package/dist/instructions.js +3 -3
  132. package/dist/instructions.js.map +1 -1
  133. package/dist/kiro-compact-pane.d.ts +38 -0
  134. package/dist/kiro-compact-pane.js +57 -0
  135. package/dist/kiro-compact-pane.js.map +1 -0
  136. package/dist/kiro-log-gate.d.ts +42 -0
  137. package/dist/kiro-log-gate.js +39 -0
  138. package/dist/kiro-log-gate.js.map +1 -0
  139. package/dist/launchd-handoff.d.ts +43 -0
  140. package/dist/launchd-handoff.js +56 -0
  141. package/dist/launchd-handoff.js.map +1 -0
  142. package/dist/locale.js +268 -42
  143. package/dist/locale.js.map +1 -1
  144. package/dist/log-service.d.ts +99 -0
  145. package/dist/log-service.js +243 -0
  146. package/dist/log-service.js.map +1 -0
  147. package/dist/log-steps.d.ts +169 -0
  148. package/dist/log-steps.js +858 -0
  149. package/dist/log-steps.js.map +1 -0
  150. package/dist/login-controller.d.ts +20 -3
  151. package/dist/login-controller.js +71 -19
  152. package/dist/login-controller.js.map +1 -1
  153. package/dist/login-flows.d.ts +5 -2
  154. package/dist/login-flows.js +5 -4
  155. package/dist/login-flows.js.map +1 -1
  156. package/dist/login-shell.d.ts +21 -0
  157. package/dist/login-shell.js +98 -0
  158. package/dist/login-shell.js.map +1 -0
  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/outbound-handlers.d.ts +100 -0
  168. package/dist/outbound-handlers.js +277 -93
  169. package/dist/outbound-handlers.js.map +1 -1
  170. package/dist/outbound-schemas.d.ts +30 -15
  171. package/dist/outbound-schemas.js +23 -5
  172. package/dist/outbound-schemas.js.map +1 -1
  173. package/dist/provider-probe.d.ts +29 -0
  174. package/dist/provider-probe.js +198 -2
  175. package/dist/provider-probe.js.map +1 -1
  176. package/dist/public-link-progress.d.ts +1 -1
  177. package/dist/public-link-progress.js +8 -2
  178. package/dist/public-link-progress.js.map +1 -1
  179. package/dist/public-preview.d.ts +101 -0
  180. package/dist/public-preview.js +236 -0
  181. package/dist/public-preview.js.map +1 -0
  182. package/dist/public-web-gateway.d.ts +2 -0
  183. package/dist/public-web-gateway.js +20 -6
  184. package/dist/public-web-gateway.js.map +1 -1
  185. package/dist/public-web-link.d.ts +24 -1
  186. package/dist/public-web-link.js +63 -9
  187. package/dist/public-web-link.js.map +1 -1
  188. package/dist/quickstart-api.d.ts +1 -1
  189. package/dist/quickstart-api.js +8 -6
  190. package/dist/quickstart-api.js.map +1 -1
  191. package/dist/quickstart.d.ts +12 -0
  192. package/dist/quickstart.js +36 -6
  193. package/dist/quickstart.js.map +1 -1
  194. package/dist/reply-buttons-controller.d.ts +89 -5
  195. package/dist/reply-buttons-controller.js +273 -8
  196. package/dist/reply-buttons-controller.js.map +1 -1
  197. package/dist/reply-buttons.d.ts +48 -3
  198. package/dist/reply-buttons.js +105 -10
  199. package/dist/reply-buttons.js.map +1 -1
  200. package/dist/reply-polls.d.ts +85 -0
  201. package/dist/reply-polls.js +154 -0
  202. package/dist/reply-polls.js.map +1 -0
  203. package/dist/restart-guard.d.ts +6 -3
  204. package/dist/restart-guard.js +6 -3
  205. package/dist/restart-guard.js.map +1 -1
  206. package/dist/restart-progress.d.ts +33 -2
  207. package/dist/restart-progress.js +30 -2
  208. package/dist/restart-progress.js.map +1 -1
  209. package/dist/sd-notify.d.ts +22 -1
  210. package/dist/sd-notify.js +63 -10
  211. package/dist/sd-notify.js.map +1 -1
  212. package/dist/service-activation.d.ts +15 -7
  213. package/dist/service-activation.js +42 -26
  214. package/dist/service-activation.js.map +1 -1
  215. package/dist/service-installer.d.ts +55 -0
  216. package/dist/service-installer.js +90 -3
  217. package/dist/service-installer.js.map +1 -1
  218. package/dist/service-plan.d.ts +11 -0
  219. package/dist/service-plan.js +23 -0
  220. package/dist/service-plan.js.map +1 -1
  221. package/dist/settings-api.js +14 -4
  222. package/dist/settings-api.js.map +1 -1
  223. package/dist/settings-change.js +4 -2
  224. package/dist/settings-change.js.map +1 -1
  225. package/dist/settings-effect.js +2 -0
  226. package/dist/settings-effect.js.map +1 -1
  227. package/dist/spawn-gate.js +5 -1
  228. package/dist/spawn-gate.js.map +1 -1
  229. package/dist/spawn-hold-reply.d.ts +12 -0
  230. package/dist/spawn-hold-reply.js +39 -0
  231. package/dist/spawn-hold-reply.js.map +1 -0
  232. package/dist/systemd-peer-bus.d.ts +95 -0
  233. package/dist/systemd-peer-bus.js +430 -0
  234. package/dist/systemd-peer-bus.js.map +1 -0
  235. package/dist/systemd-runtime.d.ts +27 -1
  236. package/dist/systemd-runtime.js +69 -7
  237. package/dist/systemd-runtime.js.map +1 -1
  238. package/dist/tips.js +1 -1
  239. package/dist/tips.js.map +1 -1
  240. package/dist/tool-permissions.js +3 -3
  241. package/dist/tool-permissions.js.map +1 -1
  242. package/dist/tool-progress.js +3 -2
  243. package/dist/tool-progress.js.map +1 -1
  244. package/dist/topic-commands.d.ts +71 -4
  245. package/dist/topic-commands.js +167 -34
  246. package/dist/topic-commands.js.map +1 -1
  247. package/dist/transcript-delta-reader.d.ts +3 -3
  248. package/dist/transcript-delta-reader.js +8 -8
  249. package/dist/transcript-delta-reader.js.map +1 -1
  250. package/dist/transcript-jsonl.d.ts +85 -3
  251. package/dist/transcript-jsonl.js +156 -13
  252. package/dist/transcript-jsonl.js.map +1 -1
  253. package/dist/transcript-monitor.d.ts +11 -0
  254. package/dist/transcript-monitor.js +61 -13
  255. package/dist/transcript-monitor.js.map +1 -1
  256. package/dist/transcript-sources.d.ts +24 -0
  257. package/dist/transcript-sources.js +211 -56
  258. package/dist/transcript-sources.js.map +1 -1
  259. package/dist/tunnel/cloudflared.d.ts +7 -15
  260. package/dist/tunnel/cloudflared.js +36 -1
  261. package/dist/tunnel/cloudflared.js.map +1 -1
  262. package/dist/tunnel/purpose-lane.d.ts +5 -1
  263. package/dist/tunnel/purpose-lane.js +14 -7
  264. package/dist/tunnel/purpose-lane.js.map +1 -1
  265. package/dist/tunnel/types.d.ts +5 -1
  266. package/dist/tunnel/types.js.map +1 -1
  267. package/dist/turn-resume.d.ts +2 -0
  268. package/dist/turn-resume.js +1 -1
  269. package/dist/turn-resume.js.map +1 -1
  270. package/dist/types.d.ts +22 -0
  271. package/dist/ui/chat-commands.js +1 -0
  272. package/dist/ui/chat-mentions.js +63 -0
  273. package/dist/ui/chat-render.js +80 -10
  274. package/dist/ui/chat-store.js +70 -6
  275. package/dist/ui/chat-strings.js +62 -32
  276. package/dist/ui/chat-thread.js +199 -24
  277. package/dist/ui/chat-time.js +80 -0
  278. package/dist/ui/chat-tour.js +17 -14
  279. package/dist/ui/panel-chat.js +45 -21
  280. package/dist/ui/panel-details.js +2 -0
  281. package/dist/ui/panel-fleet.js +213 -58
  282. package/dist/ui/panel-log.js +428 -0
  283. package/dist/ui/panel-needs.js +14 -10
  284. package/dist/ui/panel-settings.js +82 -17
  285. package/dist/ui/preview-panel.js +55 -15
  286. package/dist/ui/preview-status.js +28 -0
  287. package/dist/ui/preview.js +44 -3
  288. package/dist/ui/settings-dialogs.js +11 -6
  289. package/dist/ui/settings-model.js +3 -1
  290. package/dist/ui/settings-strings.js +32 -12
  291. package/dist/ui/settings-token.js +8 -1
  292. package/dist/ui/settings-wizard.js +5 -3
  293. package/dist/ui/shared/app-i18n.js +41 -15
  294. package/dist/ui/shared/app-prefs.js +250 -0
  295. package/dist/ui/shared/app-route.js +6 -1
  296. package/dist/ui/shared/app-shell.js +165 -8
  297. package/dist/ui/shared/app-store.js +5 -1
  298. package/dist/ui/shared/app-stream.js +26 -5
  299. package/dist/ui/shared/app.css +170 -30
  300. package/dist/ui/shared/app.js +11 -1
  301. package/dist/ui/shared/fonts/README.md +1 -1
  302. package/dist/ui/shared/header-tools.js +83 -13
  303. package/dist/ui/shared/instance-nav.js +178 -51
  304. package/dist/ui/shared/instance-switch.js +8 -5
  305. package/dist/ui/shared/panel-view.js +21 -4
  306. package/dist/ui/shared/signin.js +15 -9
  307. package/dist/ui/shared/theme.js +39 -7
  308. package/dist/ui/shared/tokens.css +1 -1
  309. package/dist/ui/shared/ui-icons.js +2 -0
  310. package/dist/ui/shared/usage-dialog.js +23 -5
  311. package/dist/ui/shared/view-strings.js +40 -4
  312. package/dist/ui/signin.html +8 -3
  313. package/dist/unrecognised-prompt.d.ts +23 -0
  314. package/dist/unrecognised-prompt.js +74 -0
  315. package/dist/unrecognised-prompt.js.map +1 -0
  316. package/dist/update-check.d.ts +31 -0
  317. package/dist/update-check.js +47 -0
  318. package/dist/update-check.js.map +1 -1
  319. package/dist/update-install.d.ts +13 -1
  320. package/dist/update-install.js +16 -4
  321. package/dist/update-install.js.map +1 -1
  322. package/dist/update-launch.d.ts +48 -1
  323. package/dist/update-launch.js +159 -5
  324. package/dist/update-launch.js.map +1 -1
  325. package/dist/update-marker.d.ts +51 -0
  326. package/dist/update-marker.js +61 -0
  327. package/dist/update-marker.js.map +1 -1
  328. package/dist/usage/i18n-keys.d.ts +1 -1
  329. package/dist/usage/i18n-keys.js +1 -1
  330. package/dist/usage/i18n-keys.js.map +1 -1
  331. package/dist/usage/providers.d.ts +62 -13
  332. package/dist/usage/providers.js +256 -68
  333. package/dist/usage/providers.js.map +1 -1
  334. package/dist/usage/statusline-usage.d.ts +6 -2
  335. package/dist/usage/statusline-usage.js +10 -4
  336. package/dist/usage/statusline-usage.js.map +1 -1
  337. package/dist/view-api.js +31 -9
  338. package/dist/view-api.js.map +1 -1
  339. package/dist/web-api.d.ts +82 -4
  340. package/dist/web-api.js +632 -88
  341. package/dist/web-api.js.map +1 -1
  342. package/dist/web-auth.js +5 -2
  343. package/dist/web-auth.js.map +1 -1
  344. package/dist/web-chat-history.d.ts +26 -1
  345. package/dist/web-chat-history.js +47 -8
  346. package/dist/web-chat-history.js.map +1 -1
  347. package/dist/web-commands.d.ts +41 -5
  348. package/dist/web-commands.js +25 -7
  349. package/dist/web-commands.js.map +1 -1
  350. package/dist/web-login.d.ts +14 -0
  351. package/dist/web-login.js +4 -2
  352. package/dist/web-login.js.map +1 -1
  353. package/dist/web-mentions.d.ts +44 -0
  354. package/dist/web-mentions.js +75 -0
  355. package/dist/web-mentions.js.map +1 -0
  356. package/dist/web-prefs.d.ts +81 -0
  357. package/dist/web-prefs.js +297 -0
  358. package/dist/web-prefs.js.map +1 -0
  359. package/dist/web-preview.d.ts +14 -1
  360. package/dist/web-preview.js +35 -13
  361. package/dist/web-preview.js.map +1 -1
  362. package/dist/web-profile.d.ts +32 -0
  363. package/dist/web-profile.js +132 -0
  364. package/dist/web-profile.js.map +1 -0
  365. package/dist/web-reply-files.d.ts +45 -0
  366. package/dist/web-reply-files.js +223 -0
  367. package/dist/web-reply-files.js.map +1 -0
  368. package/dist/web-schedules.d.ts +12 -0
  369. package/dist/web-schedules.js +34 -0
  370. package/dist/web-schedules.js.map +1 -0
  371. package/dist/web-server-drain.d.ts +10 -0
  372. package/dist/web-server-drain.js +50 -0
  373. package/dist/web-server-drain.js.map +1 -0
  374. package/dist/web-session.d.ts +14 -0
  375. package/dist/web-session.js +11 -1
  376. package/dist/web-session.js.map +1 -1
  377. package/dist/web-settings-patch.d.ts +3 -1
  378. package/dist/web-settings-patch.js +4 -1
  379. package/dist/web-settings-patch.js.map +1 -1
  380. package/dist/web-shell-routes.d.ts +4 -0
  381. package/dist/web-shell-routes.js +9 -0
  382. package/dist/web-shell-routes.js.map +1 -1
  383. package/dist/web-upload.d.ts +135 -7
  384. package/dist/web-upload.js +315 -51
  385. package/dist/web-upload.js.map +1 -1
  386. package/launcher/install-admission.cjs +61 -6
  387. package/package.json +3 -3
  388. package/templates/launchd.plist.ejs +3 -1
  389. package/templates/systemd.service.ejs +2 -1
package/dist/daemon.js CHANGED
@@ -1,6 +1,8 @@
1
1
  import { AGENT_WHATS_NEW, agendVersion, planWhatsNew, readWhatsNewRecord, recordWhatsNew, whatsNewNotice } from "./agent-whats-new.js";
2
2
  import { join, dirname, basename, resolve } from "node:path";
3
3
  import { mkdirSync, writeFileSync, readFileSync, readdirSync, existsSync, unlinkSync, rmSync, appendFileSync, statSync, renameSync } from "node:fs";
4
+ import { COMPACT_WATCH_TIMEOUT_MS, launchIdentity } from "./compact-watch.js";
5
+ import { KIRO_COMPACT_PANE_LINES, KiroCompactPaneFollower } from "./kiro-compact-pane.js";
4
6
  import { fileURLToPath } from "node:url";
5
7
  import { ensureInstanceDir } from "./private-dir.js";
6
8
  import { createHash, randomBytes, randomUUID } from "node:crypto";
@@ -9,6 +11,7 @@ import { rotateLogIfNeededAsync } from "./logger.js";
9
11
  import { coredumpFilterLaunchPrefix } from "./coredump-filter.js";
10
12
  import { mcpServerState } from "./mcp-liveness.js";
11
13
  import { clearPausedMarker, writePausedMarker } from "./pause-marker.js";
14
+ import { writeBackgroundWorkEndedMarker } from "./background-work-ended.js";
12
15
  import { TmuxManager, resolveTmuxLogicalSize } from "./tmux-manager.js";
13
16
  import { TranscriptMonitor } from "./transcript-monitor.js";
14
17
  import { createTranscriptSource } from "./transcript-sources.js";
@@ -32,6 +35,7 @@ import { HangDetector } from "./hang-detector.js";
32
35
  import { writeSecretFile } from "./secret-file.js";
33
36
  import { PaneWriteLock } from "./pane-write-lock.js";
34
37
  import { buildFleetInstructions } from "./instructions.js";
38
+ import { formatHumanInboundMessage, prepareDeliveryWire, steerInboundEnvelope, BTW_INBOUND_PREFIX, humanBodyHash, humanOwnerObjectId } from "./human-delivery-receipts.js";
35
39
  import { formatCrossInstanceInboundMessage, renderCrossInstanceHandoffMetadata, } from "./cross-instance-envelope.js";
36
40
  import { agendMessageInInput, bottomRowIsReady, inputAreaText, inputShowsPastedText, pastedTextSignature, pasteLeftInInput, strandedAgendMessageInInput } from "./pane-input-residue.js";
37
41
  import { TurnReplyGuard } from "./turn-reply-guard.js";
@@ -45,7 +49,9 @@ import { measureSyncWork, noteSyncWork } from "./sync-work-attribution.js";
45
49
  import { TranscriptDeltaReader } from "./transcript-delta-reader.js";
46
50
  import { buildResumeContinuation, clearInFlightTurnMarker, consumeInFlightTurnMarker, decideTurnResume, normalizeResumeBackend, readResumeFingerprint, RESUME_BLOCKING_DELIVERY_STATES, writeInFlightTurnMarker, } from "./turn-resume.js";
47
51
  import { codexLiveRowMatches } from "./backend/codex.js";
52
+ import { claudeConfigDirFor, claudeProfileConfigDir, claudeSwitchDialog, claudeSwitchTargetMatches } from "./backend/claude-code.js";
48
53
  import { InteractionObservation, INTERACTION_CONFIRM_MS, sameInteractionOwner } from "./interaction-observation.js";
54
+ import { unrecognisedPromptShape } from "./unrecognised-prompt.js";
49
55
  const __filename = fileURLToPath(import.meta.url);
50
56
  const __dirname = dirname(__filename);
51
57
  // Tool routing sets — module-level to avoid re-creation on every handleToolCall
@@ -86,8 +92,10 @@ const TRANSCRIPT_RUNNING_HOLD_MAX_MS = 30 * 60_000;
86
92
  export const TRANSCRIPT_OPEN_TURN_QUIET_MS = 15 * 60_000;
87
93
  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.";
88
94
  /** Point a resumed CLI at its one backend-native instruction source. */
89
- /** The outbox evidence for a delivery the CLI's transcript shows taken (#758; #1201 absorbed). */
90
- function transcriptMarkerEvidence(kind) {
95
+ /** The outbox evidence for a delivery the CLI's transcript shows taken (#758; #1201 absorbed; #1647 a raw paste's text). */
96
+ function transcriptMarkerEvidence(kind, rawPaste = false) {
97
+ if (rawPaste)
98
+ return "transcript-turn-user-entry-raw-paste-text";
91
99
  return kind === "user" ? "transcript-marker" : kind === "absorbed" ? "transcript-marker-absorbed" : "transcript-marker-queued";
92
100
  }
93
101
  export function buildInstructionReloadNotice(binaryName, instanceName, instanceDir, backendSource) {
@@ -96,7 +104,7 @@ export function buildInstructionReloadNotice(binaryName, instanceName, instanceD
96
104
  : binaryName === "kiro-cli"
97
105
  ? `.kiro/steering/agend-${instanceName}.md`
98
106
  : binaryName === "agy"
99
- ? ".agents/agents.md"
107
+ ? ".agents/AGENTS.md"
100
108
  : join(instanceDir, "fleet-instructions.md");
101
109
  return `[system] Your AgEnD instructions have been updated. Reload only ${source}; do not scan other instruction directories. Do not reply to this message.`;
102
110
  }
@@ -132,6 +140,8 @@ export const DEFAULT_STATE_IDLE_DEBOUNCE_MS = 2_000;
132
140
  export const DEFAULT_STATE_SAFETY_SWEEP_MS = 60_000;
133
141
  /** Coalesce one cosmetic-redraw burst before comparing the visible pane. */
134
142
  export const PERIODIC_REDRAW_PROBE_MS = 25;
143
+ /** Board 907c7899: how often a kiro /compact's pane is read (TUI V2's "too short" shows for about ten seconds). */
144
+ const KIRO_COMPACT_PANE_POLL_MS = 1_000;
135
145
  /** Do not turn a streaming Codex turn into one capture-pane per burst. */
136
146
  export const STRUCTURED_IDLE_PROBE_MS = 500;
137
147
  /**
@@ -479,6 +489,16 @@ const POST_ENTER_PROOF_WINDOW_MS = 3_000;
479
489
  /** A final bounded observation before an ambiguous Codex submit can be classified. */
480
490
  const CODEX_LATE_PROOF_MS = 5_000;
481
491
  const POST_ENTER_PROOF_POLL_MS = 250;
492
+ /** #1647: a raw paste a pane with no transcript showed taken — its text left the input row (or the CLI's own busy marker
493
+ * replaced it), with output after the Enter. */
494
+ const RAW_PASTE_LEFT_INPUT = "raw-paste-left-input-after-output";
495
+ /**
496
+ * #1647/#1663 P1-2: backends whose transcript AgEnD holds for this instance alone, so a raw paste's exact text in it is
497
+ * this paste's turn. claude-code: the path is its own statusline.json's transcript_path (written by this instance's
498
+ * CLI). Not codex: its rollout is chosen by working directory, which another instance can share, and AgEnD holds no
499
+ * session id to bind it — so a codex raw paste the pane could not prove stays uncertain.
500
+ */
501
+ const RAW_TEXT_TRANSCRIPT_BACKENDS = ["claude-code"];
482
502
  /**
483
503
  * A readerless CLI's delivery is proven by its own transcript, which lags the pane by a flush. Only a delta that was
484
504
  * read and carried no marker for this long is held against the delivery — a late flush must not raise a false ⚠️.
@@ -510,8 +530,16 @@ const DIALOG_PROBE_UNKNOWN_MAX = 40;
510
530
  /** Startup dialog scan: poll cadence and wall-clock budget (keys and waits included). */
511
531
  const STARTUP_DIALOG_POLL_MS = 500;
512
532
  const STARTUP_DIALOG_BUDGET_MS = 30_000;
533
+ /** #1690: after AgEnD's own `/model`, how long the pane is read for claude's "Switch model?" or its result. */
534
+ export const OWN_SWITCH_WAIT_MS = 6_000;
535
+ /** #1690: after an answer to that dialog, how long it gets to go away. */
536
+ const OWN_SWITCH_SETTLE_MS = 3_000;
513
537
  /** A blocking dialog still on screen this long after first being seen is reported for a human. */
514
538
  const DIALOG_PARKED_NOTIFY_MS = 60_000;
539
+ /** #1691: a prompt no table knows must sit unchanged this long (monotonic) before it holds deliveries and is reported. */
540
+ export const UNRECOGNISED_PROMPT_STABLE_MS = 60_000;
541
+ /** #1691: the rows under a prompt a backend without a composer reader shows its ready chrome in (status bar, hint). */
542
+ const COMPOSER_TAIL_ROWS = 4;
515
543
  /** After a runtime dialog's keys, how long the pane gets to repaint before it is read again to see whether they took. */
516
544
  const DIALOG_ANSWER_SETTLE_MS = 300;
517
545
  /** Consecutive answers one runtime dialog ignored (still on screen right after the keys) before it is reported for a human. */
@@ -621,6 +649,10 @@ export function channelStatus(meta) {
621
649
  // they are the dashboard's ticks (the fleet routes them by the message id, never to a platform).
622
650
  if (!messageId || (!chatId && meta.source !== "web"))
623
651
  return undefined;
652
+ // #1823: a notice from AgEnD itself (a poll's results) names its chat so the answer goes there, but its message id
653
+ // is AgEnD's own, not a platform message: there is nothing to react to.
654
+ if (meta.source === "agend-system")
655
+ return undefined;
624
656
  if (!chatId)
625
657
  return { chatId: "", messageId };
626
658
  const threadId = meta.thread_id || undefined;
@@ -1065,6 +1097,10 @@ export class Daemon extends EventEmitter {
1065
1097
  ipcServer = null;
1066
1098
  messageBus;
1067
1099
  transcriptMonitor = null;
1100
+ /** #1218: the transcript this instance's monitor follows now (the Log tab reads it on demand; nothing runs for it here). */
1101
+ logTranscriptPath() { return this.transcriptMonitor?.currentPath() ?? null; }
1102
+ /** #1218 (kiro): the launch this daemon's backend last built, for the Log's reason only (kiro-log-gate.ts). */
1103
+ logLaunch() { return this.backend?.logLaunch?.() ?? null; }
1068
1104
  guardian = null;
1069
1105
  adapter = null;
1070
1106
  pendingIpcRequests = new Map();
@@ -1151,6 +1187,34 @@ export class Daemon extends EventEmitter {
1151
1187
  dialogParkedKey = null;
1152
1188
  dialogParkedInteractionEpisode = null;
1153
1189
  dialogParkedReported = false;
1190
+ /**
1191
+ * #1691: the prompt at the bottom of the pane that no backend table knows — its block, since when (performance.now)
1192
+ * it has been unchanged, and whether that has lasted UNRECOGNISED_PROMPT_STABLE_MS. Updated by the runtime scan.
1193
+ */
1194
+ unrecognisedPrompt = null;
1195
+ /**
1196
+ * #1691: when (performance.now) the newest pane read that decided the evidence above was taken — a scan or a fresh
1197
+ * read, confirming, restarting or ending it. A read taken before it finishes late and says nothing about now: it may
1198
+ * neither clear, restart nor confirm (#1695 r4).
1199
+ */
1200
+ unrecognisedPromptReadAt = -Infinity;
1201
+ /**
1202
+ * #1691: the catch-all for that prompt, as a hold-only runtime dialog — so the delivery hold, awaiting_input, the
1203
+ * Needs-you item and the single parked report are the existing ones. Never answered (hold-only, no keys).
1204
+ */
1205
+ unrecognisedPromptDialog = {
1206
+ pattern: /(?!)unrecognised-prompt-1691/,
1207
+ keys: [],
1208
+ description: "a prompt AgEnD does not recognise",
1209
+ holdOnly: true,
1210
+ isActive: (pane) => this.unrecognisedPromptActive(pane),
1211
+ requestIdentity: () => this.unrecognisedPrompt?.block ?? null,
1212
+ };
1213
+ /**
1214
+ * #1690: AgEnD's own runtime `/model` in flight — from the moment its paste went in until its "Switch model?" dialog
1215
+ * (if any) is answered. Every delivery is held meanwhile: the dialog's default cursor is Yes, a stray Enter confirms it.
1216
+ */
1217
+ ownSwitch = null;
1154
1218
  /**
1155
1219
  * The runtime dialog the monitor is answering, and how many of its answers in a row did nothing (the dialog was
1156
1220
  * still on screen right after the keys). Not the on-screen duration: a burst of FRESH prompts, each answered, has a
@@ -1870,8 +1934,14 @@ export class Daemon extends EventEmitter {
1870
1934
  this.finishDurableDelivery(delivery, "delivered", OUTPUT_EDGE_ONLY_EVIDENCE);
1871
1935
  return;
1872
1936
  }
1937
+ // #1647: a raw paste on a pane with no transcript (kiro), taken by the CLI as its own input row shows.
1938
+ if (verdict.proof === RAW_PASTE_LEFT_INPUT) {
1939
+ this.finishDurableDelivery(delivery, "delivered", verdict.proof, verdict);
1940
+ return;
1941
+ }
1873
1942
  this.finishDurableDelivery(delivery, "delivered", verdict.transcriptProof
1874
- ? `positive submission proof; ${transcriptMarkerEvidence(verdict.transcriptProof)}` : "positive submission proof", verdict);
1943
+ ? `positive submission proof; ${transcriptMarkerEvidence(verdict.transcriptProof, verdict.submissionMode === "raw_paste")}`
1944
+ : "positive submission proof", verdict);
1875
1945
  }
1876
1946
  /**
1877
1947
  * Finish a delivery that only the output edge vouched for, by the delivery's own marker in the CLI's transcript (#758).
@@ -1887,23 +1957,28 @@ export class Daemon extends EventEmitter {
1887
1957
  * TRANSCRIPT_PROOF_WINDOW_MS). It never finishes a row that stop has handed to reconciliation; a pause or respawn
1888
1958
  * only ends the wait, falling back to today's outcome.
1889
1959
  */
1890
- async proveDeliveryFromTranscript(delivery, checkpoint) {
1960
+ async proveDeliveryFromTranscript(delivery, checkpoint, rawPaste) {
1891
1961
  const spawn = this.spawnGeneration;
1892
1962
  const fence = this.launchFenceEpoch;
1893
1963
  let last = "unavailable";
1894
1964
  let outcome = "delivered";
1895
1965
  let evidence = OUTPUT_EDGE_ONLY_EVIDENCE;
1966
+ // #1647: a raw paste has no marker; its text is the needle, and its pane proof already failed — so without the turn
1967
+ // in the transcript it stays uncertain (with the pane's reason), never the output-edge outcome.
1968
+ const needle = rawPaste ? { rawText: rawPaste.text } : delivery.deliveryId;
1969
+ let found = false;
1896
1970
  try {
1897
1971
  for (let polls = 0; polls <= TRANSCRIPT_PROOF_WINDOW_MS / TRANSCRIPT_PROOF_POLL_MS; polls++) {
1898
1972
  if (this.deliveryWritesStopping)
1899
1973
  return; // reconciliation owns the row now
1900
- const found = await scanTranscriptForDeliveryMarker(checkpoint.path, checkpoint.offset, checkpoint.backend, delivery.deliveryId);
1901
- if (found === "user" || found === "absorbed" || found === "queued") {
1974
+ const look = await scanTranscriptForDeliveryMarker(checkpoint.path, checkpoint.offset, checkpoint.backend, needle);
1975
+ if (look === "user" || look === "absorbed" || look === "queued") {
1976
+ found = true;
1902
1977
  outcome = "delivered";
1903
- evidence = transcriptMarkerEvidence(found);
1978
+ evidence = transcriptMarkerEvidence(look, !!rawPaste);
1904
1979
  break;
1905
1980
  }
1906
- last = found;
1981
+ last = look;
1907
1982
  if (spawn !== this.spawnGeneration || fence !== this.launchFenceEpoch) {
1908
1983
  last = "unavailable";
1909
1984
  break;
@@ -1923,6 +1998,11 @@ export class Daemon extends EventEmitter {
1923
1998
  catch (err) {
1924
1999
  this.logger.debug({ err }, "Transcript proof of a delivery failed — keeping the output-edge outcome");
1925
2000
  }
2001
+ if (rawPaste && !found) {
2002
+ outcome = "uncertain";
2003
+ evidence = `${rawPaste.unproven}; no-transcript-turn (${last})`;
2004
+ this.logger.warn({ deliveryId: delivery.deliveryId, cause: evidence }, "Raw paste outcome uncertain — not re-pasting");
2005
+ }
1926
2006
  // The settlement itself can throw (e.g. the outbox store is unhealthy), and this runs on a detached `void`
1927
2007
  // promise: an uncaught throw is an unhandled rejection, which the fleet restart path turns into a global fault.
1928
2008
  // Settle observably and leave the row for reconciliation — never a false completion, never another paste (#1207 P2).
@@ -1936,6 +2016,7 @@ export class Daemon extends EventEmitter {
1936
2016
  }
1937
2017
  }
1938
2018
  deferDurableDelivery(meta, reason) {
2019
+ this.finishHumanReceipt(meta, { reached: false });
1939
2020
  const delivery = this.durableDeliveryAttempt(meta);
1940
2021
  if (delivery === null || delivery === false)
1941
2022
  return;
@@ -3188,7 +3269,6 @@ export class Daemon extends EventEmitter {
3188
3269
  if (this.runtimeMonitorsFrozen || this.errorMonitorTimer)
3189
3270
  return;
3190
3271
  const patterns = this.backend?.getErrorPatterns?.() ?? [];
3191
- const dialogs = this.backend?.getRuntimeDialogs?.() ?? [];
3192
3272
  if (!this.tmux)
3193
3273
  return;
3194
3274
  if (!this.backend)
@@ -3224,6 +3304,9 @@ export class Daemon extends EventEmitter {
3224
3304
  scanStartedAt = performance.now();
3225
3305
  if (stale())
3226
3306
  return;
3307
+ this.trackUnrecognisedPrompt(pane, captureMono, this.instanceStateLastOutputAt > 0 && this.instanceStateLastOutputAt >= captureAt);
3308
+ // The backend's dialogs, and #1691's catch-all while an unrecognised prompt is active (decided just above).
3309
+ const dialogs = this.runtimeDialogs();
3227
3310
  if (this.instanceStateLastOutputAt > 0 && this.instanceStateLastOutputAt >= captureAt) {
3228
3311
  this.unverifyInteraction(captureMono, captureOrder);
3229
3312
  }
@@ -3692,6 +3775,42 @@ export class Daemon extends EventEmitter {
3692
3775
  : Math.max(this.deliveryEpoch, fleetEpoch);
3693
3776
  this.logger.info({ deliveryEpoch: this.deliveryEpoch }, "Pending delivery queue cleared by user cancel");
3694
3777
  }
3778
+ /** Board 907c7899: the layout a web /compact's end can be read from on this pane (kiro classic or TUI V2), or null. */
3779
+ compactPaneUi() {
3780
+ return this.backend?.compactPaneUi?.() ?? null;
3781
+ }
3782
+ /**
3783
+ * Board 907c7899: follow a /compact written to this pane until the pane says how it ended, then say so once
3784
+ * (`compact_pane_result`). Bound to the launch it was written under: another launch, or no pane, ends the look
3785
+ * silently (the fleet's watch sees the launch change and calls it lost); the fleet's deadline is the same.
3786
+ */
3787
+ async followCompactPane(id, owner, follower) {
3788
+ const tmux = this.tmux;
3789
+ if (!tmux || owner === null)
3790
+ return;
3791
+ // Monotonic, and asked after every await: a result past the deadline is not said (the fleet's watch has timed out).
3792
+ const deadline = performance.now() + COMPACT_WATCH_TIMEOUT_MS;
3793
+ const standing = () => this.tmux === tmux && launchIdentity(this.interactionOwner()) === owner && performance.now() < deadline;
3794
+ while (standing()) {
3795
+ await new Promise(resolve => { setTimeout(resolve, KIRO_COMPACT_PANE_POLL_MS).unref?.(); });
3796
+ if (!standing())
3797
+ return;
3798
+ let pane;
3799
+ try {
3800
+ pane = await tmux.capturePaneWithHistory(KIRO_COMPACT_PANE_LINES);
3801
+ }
3802
+ catch {
3803
+ continue;
3804
+ }
3805
+ if (!standing())
3806
+ return;
3807
+ const outcome = follower.look(pane);
3808
+ if (outcome) {
3809
+ this.ipcServer?.broadcast({ type: "compact_pane_result", id, owner, outcome });
3810
+ return;
3811
+ }
3812
+ }
3813
+ }
3695
3814
  captureDeliveryEpoch(value) {
3696
3815
  const epoch = typeof value === "number" && Number.isSafeInteger(value) && value >= 0
3697
3816
  ? value
@@ -3703,14 +3822,55 @@ export class Daemon extends EventEmitter {
3703
3822
  isDeliveryEpochCurrent(epoch) {
3704
3823
  return epoch === this.deliveryEpoch;
3705
3824
  }
3706
- queueRawPaste(rawText, deliveryEpoch = this.deliveryEpoch, confirmClear = false, durableAttempt) {
3825
+ queueRawPaste(rawText, deliveryEpoch = this.deliveryEpoch, confirmClear = false, durableAttempt, ownSwitch, notifyWritten) {
3826
+ const reportSwitch = (result) => {
3827
+ if (!ownSwitch)
3828
+ return;
3829
+ this.ipcServer?.broadcast({ type: "runtime_switch_result", id: ownSwitch.id, ...result });
3830
+ };
3831
+ // Board 003648ac: the fleet follows a web /compact from just before its paste is written: the transcript's end then
3832
+ // (taken here, under this launch, before the write) and the launch it was written under ride with the notice — once.
3833
+ // The checkpoint and the launch are taken together, right before the paste (each attempt takes them again); the
3834
+ // notice carries those — never the launch at notice time, which a respawn in between would relabel.
3835
+ let told = false;
3836
+ let checkpoint = null;
3837
+ let owner = null;
3838
+ // Board 907c7899: kiro says how a /compact ended only on its own pane; read from the pane just before this write.
3839
+ let paneFollower = null;
3840
+ const tellWritten = (written) => {
3841
+ if (!notifyWritten || told)
3842
+ return;
3843
+ told = true;
3844
+ this.ipcServer?.broadcast({ type: "raw_paste_written", id: notifyWritten, outcome: written ? "written" : "not_written",
3845
+ ...(written ? { owner, checkpoint, ...(paneFollower ? { pane: true } : {}) } : {}) });
3846
+ if (written && paneFollower)
3847
+ void this.followCompactPane(notifyWritten, owner, paneFollower);
3848
+ };
3849
+ const takeCheckpoint = () => {
3850
+ owner = launchIdentity(this.interactionOwner());
3851
+ const path = this.logTranscriptPath();
3852
+ checkpoint = null;
3853
+ if (!path)
3854
+ return;
3855
+ try {
3856
+ const st = statSync(path);
3857
+ checkpoint = { path, ino: st.ino, size: st.size };
3858
+ }
3859
+ catch {
3860
+ checkpoint = null;
3861
+ }
3862
+ };
3707
3863
  if (!this.tmux || !this.isDeliveryEpochCurrent(deliveryEpoch)) {
3708
3864
  if (durableAttempt)
3709
3865
  this.retryDurableDeliveryBeforeBegin(durableAttempt, "raw_paste target unavailable before pane queue");
3866
+ reportSwitch({ outcome: "not_sent" });
3867
+ tellWritten(false);
3710
3868
  return;
3711
3869
  }
3712
3870
  this.pasteLock = this.pasteLock.then(async () => {
3713
3871
  if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
3872
+ reportSwitch({ outcome: "not_sent" });
3873
+ tellWritten(false);
3714
3874
  this.logger.info("Pending raw delivery dropped by user cancel");
3715
3875
  if (durableAttempt)
3716
3876
  this.retryDurableDeliveryBeforeBegin(durableAttempt, "raw_paste epoch changed while queued");
@@ -3722,18 +3882,44 @@ export class Daemon extends EventEmitter {
3722
3882
  if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
3723
3883
  if (durableAttempt)
3724
3884
  this.retryDurableDeliveryBeforeBegin(durableAttempt, "raw_paste epoch changed while waking");
3885
+ reportSwitch({ outcome: "not_sent" }); // #1690: cancelled before anything was typed — said at once, nothing saved
3886
+ tellWritten(false);
3725
3887
  return;
3726
3888
  }
3889
+ // Board 907c7899: the pane just before this attempt's paste — after its readiness wait, last capture and fence,
3890
+ // under the paste lock (no other input lands in between). Each attempt looks again.
3891
+ const lookBefore = async () => {
3892
+ const ui = this.backend?.compactPaneUi?.() ?? null;
3893
+ const before = ui ? await this.tmux?.capturePaneWithHistory(KIRO_COMPACT_PANE_LINES).catch(() => null) : null;
3894
+ paneFollower = ui && typeof before === "string" ? new KiroCompactPaneFollower(ui, before) : null;
3895
+ };
3727
3896
  const delivered = await this.deliverMessage(rawText, undefined, {
3728
3897
  deliveryEpoch,
3729
3898
  verdict,
3730
3899
  durableAttempt,
3731
3900
  preserveExactBytes: true,
3901
+ // #1690: our own /model holds every other delivery from its write on — before the lock is released, so a steer
3902
+ // or /btw waiting on it cannot type into the gap before claude paints "Switch model?" (default cursor: Yes).
3903
+ ...(ownSwitch || notifyWritten ? { onPaneWritten: () => { if (ownSwitch)
3904
+ this.ownSwitch = ownSwitch; tellWritten(true); } } : {}),
3905
+ ...(notifyWritten ? { beforePaneWrite: takeCheckpoint, ...(this.backend?.compactPaneUi?.() ? { lookBeforePaneWrite: lookBefore } : {}) } : {}),
3732
3906
  });
3907
+ tellWritten(false); // never written (once told, this says nothing)
3733
3908
  if (delivered)
3734
3909
  this.finishDurableSubmission(durableAttempt ?? null, verdict);
3735
3910
  else if (durableAttempt && verdict.paneWriteStarted) {
3736
- this.finishDurableDelivery(durableAttempt, "uncertain", `${verdict.phase ?? "raw-paste"}:${verdict.proof ?? "unverified"}`);
3911
+ const unproven = `${verdict.phase ?? "raw-paste"}:${verdict.proof ?? "unverified"}`;
3912
+ const checkpoint = verdict.transcriptCheckpoint;
3913
+ if (checkpoint && RAW_TEXT_TRANSCRIPT_BACKENDS.includes(checkpoint.backend)) {
3914
+ // #1647: the pane could not show it, but the CLI's transcript records the turn the paste started — later
3915
+ // than the pane window sometimes. Wait for it there (bounded), off the paste lock.
3916
+ this.logger.info({ deliveryId: durableAttempt.deliveryId, unproven }, "Raw paste not proven on the pane — asking the CLI's transcript for the turn it started");
3917
+ void this.proveDeliveryFromTranscript(durableAttempt, checkpoint, { text: rawText, unproven });
3918
+ }
3919
+ else {
3920
+ this.logger.warn({ deliveryId: durableAttempt.deliveryId, cause: unproven }, "Raw paste outcome uncertain — not re-pasting");
3921
+ this.finishDurableDelivery(durableAttempt, "uncertain", unproven);
3922
+ }
3737
3923
  }
3738
3924
  else if (durableAttempt && verdict.durableBeginCommitted) {
3739
3925
  this.abortDurableDelivery(durableAttempt, `${verdict.phase ?? "raw-paste"}:${verdict.proof ?? "no-pane-write"}`);
@@ -3750,10 +3936,32 @@ export class Daemon extends EventEmitter {
3750
3936
  else if (durableAttempt)
3751
3937
  this.retryDurableDeliveryBeforeBegin(durableAttempt, err.message);
3752
3938
  this.logger.warn({ err: err.message }, "raw_paste delivery error");
3939
+ if (ownSwitch) {
3940
+ if (this.ownSwitch === ownSwitch)
3941
+ this.ownSwitch = null;
3942
+ reportSwitch({ outcome: "not_confirmed" });
3943
+ }
3944
+ tellWritten(false);
3753
3945
  return;
3754
3946
  }
3755
3947
  if (confirmClear)
3756
3948
  await this.confirmBackendClearDialog();
3949
+ if (ownSwitch) {
3950
+ if (this.ownSwitch !== ownSwitch)
3951
+ reportSwitch({ outcome: "not_sent" }); // its write never began
3952
+ else {
3953
+ try {
3954
+ reportSwitch(await this.answerOwnSwitchDialog(ownSwitch));
3955
+ }
3956
+ catch (err) {
3957
+ this.logger.warn({ err: err.message }, "Answering our own model switch failed");
3958
+ reportSwitch({ outcome: "not_confirmed" });
3959
+ }
3960
+ finally {
3961
+ this.ownSwitch = null;
3962
+ }
3963
+ }
3964
+ }
3757
3965
  this.logger.debug({ text: rawText.slice(0, 100) }, "Raw paste delivered");
3758
3966
  });
3759
3967
  this.pasteLock = this.pasteLock.catch(err => {
@@ -3772,7 +3980,94 @@ export class Daemon extends EventEmitter {
3772
3980
  this.deferDurableDelivery(deliveryMeta, "invalid raw_paste delivery attempt metadata");
3773
3981
  return;
3774
3982
  }
3775
- this.queueRawPaste(msg.content, this.captureDeliveryEpoch(msg.delivery_epoch), msg.confirm_clear === true, durableAttempt ? { ...durableAttempt, submissionMode: "raw_paste" } : undefined);
3983
+ const sw = msg.runtime_switch;
3984
+ const ownSwitch = sw && typeof sw.id === "string" && sw.kind === "model" && typeof sw.requested === "string"
3985
+ ? { id: sw.id, kind: "model", requested: sw.requested } : undefined;
3986
+ const notifyWritten = typeof msg.notify_written === "string" && /^[0-9a-f]{16}$/.test(msg.notify_written) ? msg.notify_written : undefined;
3987
+ this.queueRawPaste(msg.content, this.captureDeliveryEpoch(msg.delivery_epoch), msg.confirm_clear === true, durableAttempt ? { ...durableAttempt, submissionMode: "raw_paste", silentSchedule: msg.silent_schedule === true } : undefined, ownSwitch, notifyWritten);
3988
+ }
3989
+ /**
3990
+ * #1690 (the user's choice A): after AgEnD's own `/model <name>`, claude may ask "Switch model?" (a warm cache for
3991
+ * another model). When the model it names is the one asked for, answer Yes; otherwise No (Escape — `Kept model as …`),
3992
+ * and say so. Every key goes on a freshly read, verified dialog under the pane write lock: the same target, and for
3993
+ * Enter the cursor on "1. Yes"; the cursor is walked there with Up, one verified step at a time, never a blind key.
3994
+ * Without the dialog, claude's own "Set model to …" line after the command is the result.
3995
+ */
3996
+ async answerOwnSwitchDialog(spec) {
3997
+ const tmux = this.tmux;
3998
+ if (!tmux)
3999
+ return { outcome: "not_confirmed" };
4000
+ const read = () => tmux.capturePane().catch(() => "");
4001
+ const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
4002
+ /** One key on a dialog re-read under the lock: still the same target, and (when given) the cursor where expected. */
4003
+ const press = (target, cursor, key) => this.paneWriteLock.run(async () => {
4004
+ if (this.tmux !== tmux)
4005
+ return false;
4006
+ const now = claudeSwitchDialog(await read());
4007
+ if (!now || now.kind !== "model" || now.target !== target || (cursor !== null && now.cursor !== cursor))
4008
+ return false;
4009
+ return tmux.sendSpecialKey(key);
4010
+ });
4011
+ const gone = async () => {
4012
+ const until = performance.now() + OWN_SWITCH_SETTLE_MS;
4013
+ while (performance.now() < until) {
4014
+ if (!claudeSwitchDialog(await read()))
4015
+ return true;
4016
+ await sleep(150);
4017
+ }
4018
+ return false;
4019
+ };
4020
+ const deadline = performance.now() + OWN_SWITCH_WAIT_MS;
4021
+ while (performance.now() < deadline && this.tmux === tmux) {
4022
+ const pane = await read();
4023
+ const dialog = claudeSwitchDialog(pane);
4024
+ if (dialog && dialog.kind === "model") {
4025
+ const shown = dialog.target;
4026
+ if (!claudeSwitchTargetMatches(spec.requested, shown)) {
4027
+ const answered = await press(shown, null, "Escape") && await gone();
4028
+ this.logger.warn({ requested: spec.requested, shown, answered }, "claude would switch to another model than asked — answered No");
4029
+ return { outcome: "mismatch", shown, answered };
4030
+ }
4031
+ // Walk the cursor to "1. Yes" (it starts there; a human may have moved it): Up from row 2, re-read each time.
4032
+ let current = dialog;
4033
+ for (let step = 0; step < 2 && current.cursor === 2; step++) {
4034
+ if (!await press(shown, 2, "Up"))
4035
+ break;
4036
+ await sleep(120);
4037
+ const next = claudeSwitchDialog(await read());
4038
+ if (!next || next.target !== shown)
4039
+ break;
4040
+ current = next;
4041
+ }
4042
+ if (current.cursor !== 1 || !await press(shown, 1, "Enter") || !await gone()) {
4043
+ 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");
4044
+ return { outcome: "not_confirmed", shown };
4045
+ }
4046
+ this.logger.info({ requested: spec.requested, shown }, "Confirmed our own model switch (claude: the next reply re-reads the history)");
4047
+ return { outcome: "switched", confirmed: true, shown };
4048
+ }
4049
+ if (Daemon.switchResultAfterCommand(pane, spec.requested))
4050
+ return { outcome: "switched", confirmed: false };
4051
+ await sleep(200);
4052
+ }
4053
+ return { outcome: "not_confirmed" };
4054
+ }
4055
+ /**
4056
+ * #1690: claude's "Set model to …" line under our `/model <requested>` (no dialog was needed). Only the newest command
4057
+ * on the pane counts: an older identical `/model` with its result further up (switched away and back) must not pass
4058
+ * for this one before it has even painted — that left its dialog unanswered (live, 2.1.296).
4059
+ */
4060
+ static switchResultAfterCommand(pane, requested) {
4061
+ const rows = pane.replace(/\r/g, "").split("\n");
4062
+ let last = -1;
4063
+ for (let i = rows.length - 1; i >= 0; i--)
4064
+ if (/^❯[ \t\u00a0]+\S/.test(rows[i])) {
4065
+ last = i;
4066
+ break;
4067
+ }
4068
+ if (last < 0 || rows[last].replace(/^❯[ \t\u00a0]+/, "").trimEnd() !== `/model ${requested}`)
4069
+ return false;
4070
+ return rows.slice(last + 1).some(row => /^\s*⎿?\s*Set model to /.test(row));
3776
4071
  }
3777
4072
  /**
3778
4073
  * Confirm a backend's terminal-level /clear prompt after the channel-level
@@ -4035,6 +4330,13 @@ export class Daemon extends EventEmitter {
4035
4330
  if (!backend.quitBlockedByDialog(pane))
4036
4331
  return false;
4037
4332
  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");
4333
+ // #1716: the background work is about to be ended — record it where the
4334
+ // stop/restart reply and the instance topic notice can report it, so the
4335
+ // kill is never silent.
4336
+ try {
4337
+ writeBackgroundWorkEndedMarker(this.instanceDir, reason);
4338
+ }
4339
+ catch { /* reporting only */ }
4038
4340
  await this.tmux.sendSpecialKey("Escape").catch(() => false);
4039
4341
  return true;
4040
4342
  }
@@ -4236,7 +4538,9 @@ export class Daemon extends EventEmitter {
4236
4538
  // Bare backend regexes are not proof of a live modal. The canonical
4237
4539
  // isActive predicate replaces pattern.test(), preserving table order.
4238
4540
  const dialog = dialogs.find(d => d.isActive && (d.inputBlocked || d.blocksDelivery || d.holdOnly)
4239
- && Daemon.dialogMatches(d, pane));
4541
+ && Daemon.dialogMatches(d, pane)
4542
+ // #1690: AgEnD's own switch dialog, being answered right now, is nobody's to answer.
4543
+ && !(this.ownSwitch && claudeSwitchDialog(pane) !== null));
4240
4544
  if (dialog) {
4241
4545
  const identity = dialog.requestIdentity?.(pane) ?? pane.replace(/\r/g, "")
4242
4546
  .split("\n").map(row => row.replace(/^([ \t]*)[❯›]([ \t]+)(?=\d\.|Yes\b|No\b|Resume\b)/, "$1$2").trimEnd())
@@ -4252,7 +4556,7 @@ export class Daemon extends EventEmitter {
4252
4556
  // No captured text leaves this function or the private observer identity.
4253
4557
  return evidence ? { ...evidence, identity: createHash("sha256").update(evidence.identity).digest("hex") } : null;
4254
4558
  }
4255
- observeInteractionPane(pane, observedAt, monotonicAt, dialogs = this.backend?.getRuntimeDialogs?.() ?? [], order = ++this.interactionCaptureSerial, allowConfirmation = true) {
4559
+ observeInteractionPane(pane, observedAt, monotonicAt, dialogs = this.runtimeDialogs(), order = ++this.interactionCaptureSerial, allowConfirmation = true) {
4256
4560
  const owner = this.interactionOwner();
4257
4561
  const evidence = this.interactionEvidence(pane, dialogs);
4258
4562
  if (!this.interactionObservation.observe(evidence, owner, observedAt, monotonicAt, this.interactionOutputRevision !== this.interactionObservedOutputRevision, order))
@@ -4365,6 +4669,39 @@ export class Daemon extends EventEmitter {
4365
4669
  this.pauseRequested = false;
4366
4670
  return;
4367
4671
  }
4672
+ // #1717: an idle pause is AgEnD-initiated and silent — it must not kill
4673
+ // the user's running background work the way an explicit stop may. Defer
4674
+ // this cycle while the backend reports background work; the next idle
4675
+ // tick retries once the task ends. A pane-capture failure proceeds (the
4676
+ // quit-menu handler below stays the backstop), never wedges unpaused.
4677
+ if (this.backend?.backgroundWorkRunning && this.tmux) {
4678
+ const tmux = this.tmux;
4679
+ const windowId = tmux.getWindowId?.();
4680
+ const generation = this.spawnGeneration;
4681
+ let pane = null;
4682
+ try {
4683
+ pane = await tmux.capturePane();
4684
+ }
4685
+ catch { /* capture failed — pause as before, after revalidation */ }
4686
+ // #1730 P2-2: the await yields — the pane read is only valid for the
4687
+ // world it was taken from. Revalidate owner, phase, state and queue
4688
+ // before acting on it; anything that moved defers this cycle.
4689
+ if (this.tmux !== tmux
4690
+ || this.tmux?.getWindowId?.() !== windowId
4691
+ || this.spawnGeneration !== generation
4692
+ || this.getPauseWakeState() !== "active"
4693
+ || !(this.instanceState === "idle" || (allowStuck && this.instanceState === "stuck"))
4694
+ || this.pasteQueueDepth > 0) {
4695
+ this.logger.info("Deferring auto-pause: pause preconditions changed during the pane read");
4696
+ this.pauseRequested = false;
4697
+ return;
4698
+ }
4699
+ if (pane !== null && this.backend?.backgroundWorkRunning?.(pane)) {
4700
+ this.logger.info("Deferring auto-pause: background work is still running");
4701
+ this.pauseRequested = false;
4702
+ return;
4703
+ }
4704
+ }
4368
4705
  this.pauseWakeState = "pausing";
4369
4706
  this.healthCheckPaused = true;
4370
4707
  this.freezeRuntimeMonitors();
@@ -4737,10 +5074,7 @@ export class Daemon extends EventEmitter {
4737
5074
  backend: resumeBackend ?? "unknown",
4738
5075
  cwd: resumeCwd,
4739
5076
  armedAt: Date.now(),
4740
- before: readResumeFingerprint(normalizeResumeBackend(resumeBackend), {
4741
- instanceDir: this.instanceDir,
4742
- workingDirectory: resumeCwd,
4743
- }),
5077
+ before: readResumeFingerprint(normalizeResumeBackend(resumeBackend), this.resumeRoots(resumeCwd)),
4744
5078
  });
4745
5079
  }
4746
5080
  }
@@ -4786,7 +5120,7 @@ export class Daemon extends EventEmitter {
4786
5120
  && this.spawnGeneration === gateSpawn
4787
5121
  && this.launchFenceEpoch === gateFence;
4788
5122
  const backend = this.resolveResumeBackend() ?? normalizeResumeBackend(marker.backend);
4789
- const roots = { instanceDir: this.instanceDir, workingDirectory: this.config.working_directory ?? "" };
5123
+ const roots = this.resumeRoots(this.config.working_directory ?? "");
4790
5124
  // This boot's CLI spawn is what the daemon itself caused: pre-restart
4791
5125
  // writes (including pre-crash progress) are excluded, and the flush
4792
5126
  // grace holds the decision until post-spawn writes are observable.
@@ -5089,8 +5423,9 @@ export class Daemon extends EventEmitter {
5089
5423
  * every delivery parked behind it. It recovers when the backend sees its own live, EMPTY composer with no busy, queued
5090
5424
  * or dialog marker (isStableUnknownLayoutIdlePane) AND the transcript shows no open turn — or, with no transcript to
5091
5425
  * ask, that same composer held unchanged for UNKNOWN_LAYOUT_STABLE_MS (#978's rule). An open turn never recovers.
5426
+ * #1865: `hold` makes the closed-turn case wait out that window too (a `working` reading, recovered before the stuck deadline).
5092
5427
  */
5093
- recoverStuck(pane, snapshot, changeAt, turn) {
5428
+ recoverStuck(pane, snapshot, changeAt, turn, hold = false) {
5094
5429
  const machine = this.instanceStateMachine;
5095
5430
  const idleComposer = this.backend?.isStableUnknownLayoutIdlePane?.(pane) === true;
5096
5431
  // The resume load (#1031: `Resuming session…` over the same composer) is input that is not open yet (#1641 R1).
@@ -5106,7 +5441,7 @@ export class Daemon extends EventEmitter {
5106
5441
  this.stuckRecoveryKey = key;
5107
5442
  this.stuckRecoverySince = mono;
5108
5443
  }
5109
- if (turn === "unknown" && mono - this.stuckRecoverySince < UNKNOWN_LAYOUT_STABLE_MS)
5444
+ if ((turn === "unknown" || hold) && mono - this.stuckRecoverySince < UNKNOWN_LAYOUT_STABLE_MS)
5110
5445
  return snapshot;
5111
5446
  // A footer this build cannot read stays unreadable: the same still pane reads stuck again at every sweep and is
5112
5447
  // recovered again. Only the edge into idle is news.
@@ -5515,6 +5850,8 @@ export class Daemon extends EventEmitter {
5515
5850
  return;
5516
5851
  if (!this.observeInteractionPane(pane, captureStartedAt, interactionCaptureAt, undefined, interactionCaptureOrder, reason !== "interaction_confirmation"))
5517
5852
  return;
5853
+ // #1691: an accepted capture of this launch is a fresh read for the unrecognised-prompt evidence too (#1695 r3).
5854
+ this.noteFreshPaneRead(pane, interactionCaptureAt);
5518
5855
  this.updateInputBlockedState(pane);
5519
5856
  // Advance the shared 10s timer only for a fresh capture of this launch.
5520
5857
  const deliveryFallback = deliveryCandidate === true
@@ -5673,6 +6010,13 @@ export class Daemon extends EventEmitter {
5673
6010
  }
5674
6011
  if (snapshot.state === "stuck")
5675
6012
  snapshot = this.recoverStuck(pane, snapshot, observedChangeAt, transcriptTurn);
6013
+ // #1865: a resumed codex can paint its footer without the Context item (sol after the beta.13 restart), so its
6014
+ // idle composer reads `working` for the whole stuck window. Its own rollout says no turn is open: the sweep needs
6015
+ // no deadline for that — the same composer held still for UNKNOWN_LAYOUT_STABLE_MS is enough.
6016
+ else if (snapshot.state === "working" && reason === "safety_sweep" && transcriptTurn === "closed"
6017
+ && this.backend?.binaryName === "codex") {
6018
+ snapshot = this.recoverStuck(pane, snapshot, observedChangeAt, transcriptTurn, true);
6019
+ }
5676
6020
  this.applyInstanceStateSnapshot(snapshot, pane);
5677
6021
  // Backends without a transcript feed can still say what they are doing, if
5678
6022
  // their TUI names it. Free-riding on a capture that already happened: no
@@ -6083,11 +6427,7 @@ export class Daemon extends EventEmitter {
6083
6427
  * is a durable delivery) must LEAD the persisted user message — restart reconciliation recognises a delivery only by a
6084
6428
  * marker at the start of the body (#1197) — so the banner goes right after it, not in front of it.
6085
6429
  */
6086
- static steerEnvelope(inbound) {
6087
- const banner = "[STEERING — mid-task course correction. Fold this into the CURRENT work if one is active.]\n";
6088
- const marker = inbound.match(/^\[agend-delivery-id:[^\]\r\n]+\]\r?\n/);
6089
- return marker ? marker[0] + banner + inbound.slice(marker[0].length) : banner + inbound;
6090
- }
6430
+ static steerEnvelope(inbound) { return steerInboundEnvelope(inbound); }
6091
6431
  /**
6092
6432
  * The one place an inbound message grows its metadata wrapper ([user:]/
6093
6433
  * [from:] prefix, pending reactions, handoff metadata, reply instructions).
@@ -6098,35 +6438,65 @@ export class Daemon extends EventEmitter {
6098
6438
  /** #1650: how this instance talks to the fleet — MCP tools, or agend-agent commands (agent_mode: cli). */
6099
6439
  agentMode() { return this.config.agent_mode === "cli" ? "cli" : "mcp"; }
6100
6440
  formatInboundMessage(content, meta) {
6101
- const user = meta.user || "unknown";
6102
- const fromInstance = meta.from_instance;
6103
- let formatted;
6104
- if (fromInstance) {
6105
- formatted = formatCrossInstanceInboundMessage(content, meta, this.agentMode());
6441
+ if (!meta.from_instance)
6442
+ return formatHumanInboundMessage(content, meta, this.agentMode());
6443
+ let formatted = formatCrossInstanceInboundMessage(content, meta, this.agentMode());
6444
+ if (meta.reply_to_text)
6445
+ formatted += '\n(reply_to: "' + meta.reply_to_text + '")';
6446
+ if (meta.delivery_id)
6447
+ formatted = "[agend-delivery-id:" + meta.delivery_id + "]\n" + formatted;
6448
+ return formatted;
6449
+ }
6450
+ /** Pure cached owner: no pane/process query on ingress. */
6451
+ captureHumanReceiptOwner() {
6452
+ try {
6453
+ if (this.deliveryWritesStopping || this.spawning || !this.tmux)
6454
+ return null;
6455
+ const o = this.interactionOwner();
6456
+ const window = this.tmux.getWindowId();
6457
+ if (!window || !o.bootId || ![o.spawnGeneration, o.launchAttempt, o.launchFenceEpoch, this.deliveryEpoch]
6458
+ .every(n => Number.isSafeInteger(n) && n >= 0))
6459
+ return null;
6460
+ return JSON.stringify([o.bootId, o.spawnGeneration, o.launchAttempt, o.launchFenceEpoch,
6461
+ this.deliveryEpoch, humanOwnerObjectId(this.tmux), window]);
6106
6462
  }
6107
- else {
6108
- const via = meta.source ? ` via ${meta.source}` : "";
6109
- const idTag = meta.user_id ? `, id:${meta.user_id}` : "";
6110
- formatted = `[user:${user}${via}${idTag}] ${content}`;
6111
- // Reactions queued since the last real message (#432). One leading line of
6112
- // context, present only when something is pending — a reaction no longer
6113
- // costs a turn of its own.
6114
- if (meta.pending_reactions) {
6115
- formatted = `[Recent reactions: ${meta.pending_reactions}]\n${formatted}`;
6116
- }
6117
- formatted += renderHandoffMetadata(meta);
6118
- // #1650: a CLI-mode agent has no reply tool — it answers with the agend-agent command.
6119
- formatted += this.agentMode() === "cli"
6120
- ? '\n(Reply with agend-agent reply "…" — do NOT respond with direct text)'
6121
- : "\n(Reply using the reply tool — do NOT respond with direct text)";
6122
- }
6123
- if (meta.reply_to_text) {
6124
- formatted += `\n(reply_to: "${meta.reply_to_text}")`;
6125
- }
6126
- if (meta.delivery_id) {
6127
- formatted = `[agend-delivery-id:${meta.delivery_id}]\n${formatted}`;
6463
+ catch {
6464
+ return null;
6465
+ }
6466
+ }
6467
+ /** The same formatter/sanitizer used by the actual write; snapshot only. */
6468
+ humanReceiptWire(content, meta, kind) {
6469
+ let wire = kind === "raw_paste" ? content
6470
+ : !meta.from_instance && content.startsWith("/raw ") && kind === "fleet_inbound"
6471
+ ? content.slice(5) : this.formatInboundMessage(content, meta);
6472
+ if (kind === "steer")
6473
+ wire = Daemon.steerEnvelope(wire);
6474
+ else if (kind === "btw")
6475
+ wire = BTW_INBOUND_PREFIX + wire;
6476
+ return prepareDeliveryWire(wire, kind === "raw_paste");
6477
+ }
6478
+ humanReceiptAttempt(meta, wire) {
6479
+ const id = meta.human_receipt_id;
6480
+ const daemonOwner = meta.human_daemon_owner;
6481
+ if (!id || !/^[a-f0-9]{8}-(?:[a-f0-9]{4}-){3}[a-f0-9]{12}$/.test(id) || meta.verification_id !== "hmsg-" + id
6482
+ || !daemonOwner || !meta.human_owner_key || !/^[a-f0-9]{64}$/.test(meta.human_wire_sha256 ?? "")
6483
+ || humanBodyHash(prepareDeliveryWire(wire)) !== meta.human_wire_sha256)
6484
+ return undefined;
6485
+ return { receiptId: id, verificationId: meta.verification_id, expectedWireHash: meta.human_wire_sha256,
6486
+ ownerKey: meta.human_owner_key, current: () => this.captureHumanReceiptOwner() === daemonOwner };
6487
+ }
6488
+ finishHumanReceipt(meta, verdict) {
6489
+ if (!meta.human_receipt_id || !meta.human_owner_key)
6490
+ return;
6491
+ const attempt = verdict.humanReceipt;
6492
+ const proof = attempt?.current() && !["unverified", "output-edge", "unverifiable"].includes(verdict.proof ?? "")
6493
+ ? attempt.proof ?? null : null;
6494
+ try {
6495
+ this.emit("human_receipt_result", { receipt_id: meta.human_receipt_id, owner_key: meta.human_owner_key, proof });
6496
+ }
6497
+ catch (err) {
6498
+ this.logger.error({ err }, "Could not publish human delivery evidence");
6128
6499
  }
6129
- return formatted;
6130
6500
  }
6131
6501
  /**
6132
6502
  * Whether ONE delivery reached a verdict, carried by that delivery.
@@ -6219,12 +6589,13 @@ export class Daemon extends EventEmitter {
6219
6589
  this.deferDurableDelivery(meta, "delivery epoch changed while waking for steer");
6220
6590
  return;
6221
6591
  }
6222
- const verdict = { reached: false };
6592
+ const verdict = { reached: false, humanReceipt: this.humanReceiptAttempt(meta, formatted) };
6223
6593
  try {
6224
6594
  if (await this.deliverMessage(formatted, status, {
6225
6595
  steer: true,
6226
6596
  deliveryEpoch,
6227
6597
  submissionId: meta.message_id,
6598
+ ...(meta.verification_id ? { humanVerificationId: meta.verification_id } : {}),
6228
6599
  verdict,
6229
6600
  durableAttempt: durableAttempt || undefined,
6230
6601
  })) {
@@ -6255,6 +6626,9 @@ export class Daemon extends EventEmitter {
6255
6626
  this.retryDurableDeliveryBeforeBegin(durableAttempt, err.message);
6256
6627
  throw err;
6257
6628
  }
6629
+ finally {
6630
+ this.finishHumanReceipt(meta, verdict);
6631
+ }
6258
6632
  }).catch(err => {
6259
6633
  this.deferDurableDelivery(meta, `target wake/steer failed before pane submission: ${err.message}`);
6260
6634
  this.logger.warn({ err: err.message }, "steer delivery error");
@@ -6270,24 +6644,37 @@ export class Daemon extends EventEmitter {
6270
6644
  * so channel delivery intentionally uses a normal wrapped inbound instead.
6271
6645
  */
6272
6646
  btwMessage(content, meta, deliveryEpoch = this.deliveryEpoch) {
6273
- if (!this.isDeliveryEpochCurrent(deliveryEpoch))
6647
+ if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
6648
+ this.finishHumanReceipt(meta, { reached: false });
6274
6649
  return;
6650
+ }
6275
6651
  this.updateLastChat(meta.chat_id, meta.thread_id, meta.adapter_id);
6276
6652
  this.pendingWork.recordInbound();
6277
6653
  this.recordRecentUserMessage(content, meta);
6278
- const formatted = "[BTW — side question from the user. Answer it separately without changing or interrupting the CURRENT work.]\n"
6279
- + this.formatInboundMessage(content, meta);
6654
+ const formatted = BTW_INBOUND_PREFIX + this.formatInboundMessage(content, meta);
6280
6655
  const status = channelStatus(meta);
6281
6656
  this.steerLock = this.steerLock.then(async () => {
6282
- if (!this.isDeliveryEpochCurrent(deliveryEpoch))
6657
+ if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
6658
+ this.finishHumanReceipt(meta, { reached: false });
6283
6659
  return;
6660
+ }
6284
6661
  await this.wake();
6285
- if (!this.isDeliveryEpochCurrent(deliveryEpoch))
6662
+ if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
6663
+ this.finishHumanReceipt(meta, { reached: false });
6286
6664
  return;
6287
- if (await this.deliverMessage(formatted, status, { steer: true, deliveryEpoch, submissionId: meta.message_id })) {
6288
- this.markTurnStarted(meta, formatted, deliveryEpoch);
6665
+ }
6666
+ const verdict = { reached: false, humanReceipt: this.humanReceiptAttempt(meta, formatted) };
6667
+ try {
6668
+ if (await this.deliverMessage(formatted, status, { steer: true, deliveryEpoch, submissionId: meta.message_id,
6669
+ ...(meta.verification_id ? { humanVerificationId: meta.verification_id } : {}), verdict })) {
6670
+ this.markTurnStarted(meta, formatted, deliveryEpoch);
6671
+ }
6672
+ }
6673
+ finally {
6674
+ this.finishHumanReceipt(meta, verdict);
6289
6675
  }
6290
6676
  }).catch(err => {
6677
+ this.finishHumanReceipt(meta, { reached: false });
6291
6678
  this.logger.warn({ err: err.message }, "btw delivery error");
6292
6679
  });
6293
6680
  }
@@ -6401,7 +6788,7 @@ export class Daemon extends EventEmitter {
6401
6788
  // A fresh delivery begins a fresh turn — its bubble must not inherit
6402
6789
  // the previous turn's tool list.
6403
6790
  this.resetToolProgress();
6404
- verdict = { reached: false };
6791
+ verdict = { reached: false, humanReceipt: this.humanReceiptAttempt(meta, formatted) };
6405
6792
  const durableAttempt = this.durableDeliveryAttempt(meta, content);
6406
6793
  if (durableAttempt === false) {
6407
6794
  this.deferDurableDelivery(meta, "invalid durable delivery attempt metadata");
@@ -6411,6 +6798,7 @@ export class Daemon extends EventEmitter {
6411
6798
  if (await this.deliverMessage(formatted, status, {
6412
6799
  deliveryEpoch,
6413
6800
  submissionId: meta.message_id,
6801
+ ...(meta.verification_id ? { humanVerificationId: meta.verification_id } : {}),
6414
6802
  verdict,
6415
6803
  durableAttempt: durableAttempt || undefined,
6416
6804
  })) {
@@ -6458,6 +6846,7 @@ export class Daemon extends EventEmitter {
6458
6846
  this.logger.warn({ err: err.message }, "Durable pane delivery deferred after exception");
6459
6847
  }
6460
6848
  finally {
6849
+ this.finishHumanReceipt(meta, verdict);
6461
6850
  this.pasteQueueDepth--;
6462
6851
  }
6463
6852
  }).catch(err => {
@@ -6494,12 +6883,7 @@ export class Daemon extends EventEmitter {
6494
6883
  && !this.isDeliveryEpochCurrent(opts.deliveryEpoch);
6495
6884
  if (cancelled() || this.stormWindow?.isStopped())
6496
6885
  return false;
6497
- // Sanitize unclosed code fences — they cause CLI to wait for closure on Enter
6498
- const fenceCount = (formatted.match(/```/g) || []).length;
6499
- if (!opts?.preserveExactBytes && fenceCount % 2 !== 0) {
6500
- // Odd number of fences = unclosed. Remove all code fences from the message.
6501
- formatted = formatted.replace(/```/g, "");
6502
- }
6886
+ formatted = prepareDeliveryWire(formatted, opts?.preserveExactBytes);
6503
6887
  // Direct paths such as /steer, /btw, /raw and schedules do not pass the
6504
6888
  // FleetManager idle gate. Hold them at the final common pane-write path so
6505
6889
  // a tmux storm cannot force any text into a half-booted CLI.
@@ -6672,7 +7056,10 @@ export class Daemon extends EventEmitter {
6672
7056
  return stale();
6673
7057
  if (residue !== "clear")
6674
7058
  return this.failDelivery(verdict, status, "input-residue", residue);
6675
- const written = await this.writeMessageToPane(formatted, windowId, handingOffToNativeQueue, status, opts?.submissionId, verdict, opts?.durableAttempt, opts?.steer === true, writeCurrent);
7059
+ const written = await this.writeMessageToPane(formatted, windowId, handingOffToNativeQueue, status, opts?.submissionId, verdict, opts?.durableAttempt, opts?.steer === true, writeCurrent, opts?.humanVerificationId, opts?.beforePaneWrite, opts?.lookBeforePaneWrite);
7060
+ // #1690: still under the lock — whatever waits on it next must already see what this write started.
7061
+ if (verdict.paneWriteStarted)
7062
+ opts?.onPaneWritten?.();
6676
7063
  // The hand-off's last capture had no box: nothing was begun or written — wait for readiness, then try again.
6677
7064
  if (written === "handoff-box-unread")
6678
7065
  return "dialog";
@@ -6720,7 +7107,148 @@ export class Daemon extends EventEmitter {
6720
7107
  * on purpose ("I trust", "Yes, continue") and would match ordinary prose.
6721
7108
  */
6722
7109
  deliveryBlockingDialogs() {
6723
- return (this.backend?.getRuntimeDialogs?.() ?? []).filter(d => d.blocksDelivery || d.holdOnly);
7110
+ return this.runtimeDialogs().filter(d => d.blocksDelivery || d.holdOnly);
7111
+ }
7112
+ /**
7113
+ * The backend's runtime dialogs, then #1691's catch-all for a prompt none of them knows (last: known ones win). The
7114
+ * catch-all is listed only while such a prompt is active: otherwise it could match nothing, and a backend with no
7115
+ * dialogs keeps its empty list (its delivery path skips the dialog probe, as before).
7116
+ */
7117
+ runtimeDialogs() {
7118
+ const dialogs = this.backend?.getRuntimeDialogs?.() ?? [];
7119
+ return this.unrecognisedPromptCurrent()?.active ? [...dialogs, this.unrecognisedPromptDialog] : dialogs;
7120
+ }
7121
+ /**
7122
+ * #1691: is the pane at the CLI's own idle composer? Then whatever looks like a prompt above it is transcript output
7123
+ * (an agent's numbered list, a quoted "[y/n]"), not a prompt. The backend's own reading, most specific first.
7124
+ */
7125
+ cliComposerShowing(pane, shape) {
7126
+ const backend = this.backend;
7127
+ if (!backend)
7128
+ return false;
7129
+ if (backend.readInputRow)
7130
+ return backend.readInputRow(pane) !== null;
7131
+ // muse: its live input box — null when the bottom of the screen is not its layout (#829), e.g. a selection menu.
7132
+ if (backend.inputDraft)
7133
+ return backend.inputDraft(pane) !== null;
7134
+ if (backend.isDeliveryInputReadyPane)
7135
+ return backend.isDeliveryInputReadyPane(pane);
7136
+ const bottom = backend.getBottomReadyPattern?.();
7137
+ if (bottom) {
7138
+ bottom.lastIndex = 0;
7139
+ return bottom.test(pane);
7140
+ }
7141
+ // No reader of its own (opencode, grok, …): its ready chrome BELOW the prompt rows — a status bar or prompt hint drawn
7142
+ // under its input box (opencode's "ctrl+p commands"), which a prompt shown in place of the input has nothing of. Not
7143
+ // the whole pane (a reply quoting "ctrl+p commands" higher up), and never the prompt's own rows: a ready pattern that
7144
+ // is just a cursor glyph would otherwise read the menu's own "❯ 1." as the CLI's prompt (#1695 r2).
7145
+ const rows = pane.replace(/\r/g, "").split("\n").map(r => r.trimEnd());
7146
+ const lastOwn = (shape.block.split("\n").at(-1) ?? "").trim();
7147
+ let end = -1;
7148
+ for (let i = rows.length - 1; i >= 0; i--)
7149
+ if (rows[i].trim() === lastOwn) {
7150
+ end = i;
7151
+ break;
7152
+ }
7153
+ if (end < 0)
7154
+ return false;
7155
+ const below = rows.slice(end + 1).filter(r => r.trim() !== "").slice(0, COMPOSER_TAIL_ROWS).join("\n");
7156
+ const ready = backend.getReadyPattern();
7157
+ ready.lastIndex = 0;
7158
+ return below !== "" && ready.test(below);
7159
+ }
7160
+ /**
7161
+ * #1691: the prompt shape this pane shows, when nothing else explains it (no known dialog, login, idle composer, and
7162
+ * the CLI is not showing its busy indicator).
7163
+ */
7164
+ unrecognisedPromptCandidate(pane) {
7165
+ const shape = unrecognisedPromptShape(pane);
7166
+ if (!shape)
7167
+ return null;
7168
+ const busy = this.backend?.getBusyPattern?.() ?? null;
7169
+ if (busy) {
7170
+ busy.lastIndex = 0;
7171
+ if (busy.test(pane))
7172
+ return null;
7173
+ }
7174
+ if ((this.backend?.getRuntimeDialogs?.() ?? []).some(dialog => Daemon.dialogMatches(dialog, pane)))
7175
+ return null;
7176
+ if (LOGIN_FLOWS[this.config.backend ?? "claude-code"]?.loginScreenActive?.(pane))
7177
+ return null;
7178
+ if (this.cliComposerShowing(pane, shape))
7179
+ return null;
7180
+ return shape;
7181
+ }
7182
+ /**
7183
+ * #1691, from each runtime scan: the same unexplained prompt block unchanged for UNRECOGNISED_PROMPT_STABLE_MS
7184
+ * becomes active (held, awaiting_input, reported once by trackDialogParked). Any other screen ends it. A turn keeps
7185
+ * changing the pane (spinner, timer, streamed text), so its clock never completes mid-turn.
7186
+ */
7187
+ trackUnrecognisedPrompt(pane, mono, outputMoved) {
7188
+ if (mono < this.unrecognisedPromptReadAt)
7189
+ return;
7190
+ this.unrecognisedPromptReadAt = mono;
7191
+ const shape = this.unrecognisedPromptCandidate(pane);
7192
+ if (!shape) {
7193
+ this.unrecognisedPrompt = null;
7194
+ return;
7195
+ }
7196
+ const screen = Daemon.stableScreen(pane);
7197
+ const owner = this.interactionOwner();
7198
+ const current = this.unrecognisedPromptCurrent();
7199
+ // Restart the minute on any change: another prompt, motion anywhere on the screen, output that arrived while this
7200
+ // capture was taken, or evidence from another launch (#1695 review).
7201
+ if (!current || current.block !== shape.block || current.screen !== screen || outputMoved) {
7202
+ this.unrecognisedPrompt = { block: shape.block, shape, since: mono, active: false, reportedElsewhere: false, screen, owner };
7203
+ return;
7204
+ }
7205
+ if (!current.active && mono - current.since >= UNRECOGNISED_PROMPT_STABLE_MS) {
7206
+ current.active = true;
7207
+ // A [y/n] the interactive-prompt detector knows was already told (interactive_prompt, after 10 s): held and
7208
+ // awaiting input from here on, but not reported a second time.
7209
+ current.reportedElsewhere = shape.kind === "yes-no" && this.interactivePromptDetector.match(pane) !== null;
7210
+ 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)");
7211
+ }
7212
+ }
7213
+ /** #1691: the tracked evidence, if it still belongs to this launch (else it is dropped). */
7214
+ unrecognisedPromptCurrent() {
7215
+ const current = this.unrecognisedPrompt;
7216
+ if (current && (this.runtimeMonitorsFrozen || !sameInteractionOwner(current.owner, this.interactionOwner()))) {
7217
+ this.unrecognisedPrompt = null;
7218
+ return null;
7219
+ }
7220
+ return current;
7221
+ }
7222
+ /**
7223
+ * #1691: the catch-all's predicate — the tracked prompt is active and this pane still shows exactly it (any reader:
7224
+ * the scan, a probe, an interaction re-capture). Ending the evidence on a read without it is noteFreshPaneRead's.
7225
+ */
7226
+ unrecognisedPromptActive(pane) {
7227
+ const current = this.unrecognisedPromptCurrent();
7228
+ return !!current?.active && this.unrecognisedPromptCandidate(pane)?.block === current.block && Daemon.stableScreen(pane) === current.screen;
7229
+ }
7230
+ /**
7231
+ * #1691: a fresh pane read outside the scan (a delivery probe, an accepted state re-capture), taken at `mono`. When it
7232
+ * no longer shows exactly the tracked prompt, the evidence ends — counting or active, so a return needs a fresh minute
7233
+ * (#1695 r2). When it does, it is the newest confirmation. Either way only a read newer than the newest read that
7234
+ * decided the evidence counts: an older one, finishing late, says nothing about now (#1695 r3, r4).
7235
+ */
7236
+ noteFreshPaneRead(pane, mono) {
7237
+ if (mono < this.unrecognisedPromptReadAt)
7238
+ return;
7239
+ // Advanced even with nothing tracked: a scan whose capture is older must not start evidence this read contradicts
7240
+ // (#1695 r5).
7241
+ this.unrecognisedPromptReadAt = mono;
7242
+ const current = this.unrecognisedPromptCurrent();
7243
+ if (!current)
7244
+ return;
7245
+ if (this.unrecognisedPromptCandidate(pane)?.block === current.block && Daemon.stableScreen(pane) === current.screen)
7246
+ return;
7247
+ this.unrecognisedPrompt = null;
7248
+ }
7249
+ /** #1691: the screen as the stability clock compares it (trailing blanks are not motion). */
7250
+ static stableScreen(pane) {
7251
+ return pane.replace(/\r/g, "").split("\n").map(r => r.trimEnd()).join("\n").trimEnd();
6724
7252
  }
6725
7253
  /** Passive input-unavailable phases relevant to the current spawn only. */
6726
7254
  guardedInputTransients() {
@@ -6888,7 +7416,7 @@ export class Daemon extends EventEmitter {
6888
7416
  return dialog.isActive ? dialog.isActive(pane) : dialog.pattern.test(pane);
6889
7417
  }
6890
7418
  /** Refresh the separate stdin-blocked observation from one pane capture. */
6891
- updateInputBlockedState(pane, dialogs = this.backend?.getRuntimeDialogs?.() ?? []) {
7419
+ updateInputBlockedState(pane, dialogs = this.runtimeDialogs()) {
6892
7420
  const active = dialogs.find(dialog => dialog.inputBlocked && Daemon.dialogMatches(dialog, pane)) ?? null;
6893
7421
  const nextKey = active ? Daemon.dialogKey(active) : null;
6894
7422
  if (nextKey !== this.inputBlockedDialogKey) {
@@ -6976,9 +7504,15 @@ export class Daemon extends EventEmitter {
6976
7504
  async probeBlockingDialog() {
6977
7505
  if (!this.tmux || !this.backend)
6978
7506
  return { state: "clear" };
7507
+ // #1691: a probe is a fresh read — it may end unrecognised-prompt evidence even when no dialog is listed.
7508
+ const tracking = this.unrecognisedPrompt !== null;
6979
7509
  const dialogs = this.deliveryBlockingDialogs();
6980
- if (dialogs.length === 0)
7510
+ // #1690: our own switch is being answered — nothing else types into the pane until it has settled.
7511
+ if (this.ownSwitch && dialogs.length > 0)
7512
+ return { state: "dialog", dialog: dialogs[0] };
7513
+ if (dialogs.length === 0 && !tracking)
6981
7514
  return { state: "clear" };
7515
+ const probeMono = performance.now();
6982
7516
  let pane;
6983
7517
  try {
6984
7518
  pane = await this.tmux.capturePane();
@@ -6987,6 +7521,7 @@ export class Daemon extends EventEmitter {
6987
7521
  this.logger.debug({ err }, "capture-pane failed during the dialog probe — pane state unknown");
6988
7522
  return { state: "unknown" };
6989
7523
  }
7524
+ this.noteFreshPaneRead(pane, probeMono);
6990
7525
  for (const dialog of dialogs) {
6991
7526
  if (!Daemon.dialogMatches(dialog, pane))
6992
7527
  continue;
@@ -7196,11 +7731,16 @@ export class Daemon extends EventEmitter {
7196
7731
  this.dialogParkedReported = false;
7197
7732
  return;
7198
7733
  }
7199
- if (!this.dialogParkedReported && Date.now() - this.dialogParkedSince >= DIALOG_PARKED_NOTIFY_MS) {
7734
+ // #1691: the catch-all waited its minute before it became active; it is reported on the next scan.
7735
+ const unrecognised = dialog === this.unrecognisedPromptDialog;
7736
+ if (unrecognised && this.unrecognisedPrompt?.reportedElsewhere)
7737
+ this.dialogParkedReported = true;
7738
+ if (!this.dialogParkedReported && Date.now() - this.dialogParkedSince >= (unrecognised ? 0 : DIALOG_PARKED_NOTIFY_MS)) {
7200
7739
  this.dialogParkedReported = true;
7201
7740
  this.logger.warn({ dialog: dialog.description, parkedForMs: Date.now() - this.dialogParkedSince }, "CLI dialog is still on screen — not auto-answering, reporting for a human");
7202
7741
  this.emit("dialog_parked", { name: this.name, description: dialog.description, holdOnly: dialog.holdOnly === true,
7203
- ...(episode !== null && interaction.kind ? { kind: interaction.kind, episode, backend: this.config.backend ?? "claude-code" } : {}) });
7742
+ ...(episode !== null && interaction.kind ? { kind: interaction.kind, episode, backend: this.config.backend ?? "claude-code" } : {}),
7743
+ ...(unrecognised ? { unrecognised: true, question: this.unrecognisedPrompt?.shape.question ?? null } : {}) });
7204
7744
  }
7205
7745
  }
7206
7746
  /**
@@ -7311,7 +7851,7 @@ export class Daemon extends EventEmitter {
7311
7851
  return false;
7312
7852
  const row = norm(shown);
7313
7853
  // The marker line holds nothing but the marker: the row is exactly it.
7314
- if (/^\[agend-delivery-id:[^\]\s]+\]$/.test(first))
7854
+ if (/^\[agend-(?:delivery|human)-id:[^\]\s]+\]$/.test(first))
7315
7855
  return row === first;
7316
7856
  return row === first || row.startsWith(`${first} `);
7317
7857
  }
@@ -7769,6 +8309,33 @@ export class Daemon extends EventEmitter {
7769
8309
  this.logger.warn("Could not read the pane after Enter — not confirming the submission");
7770
8310
  return sawOutput && residue === "absent";
7771
8311
  }
8312
+ /**
8313
+ * #1647: whether a raw paste on a pane with no transcript (kiro) was taken: its text gone from the input row (or the
8314
+ * CLI's own busy marker shown instead — pasteResidue's rule) with output after the Enter. That is the confirmation's
8315
+ * own rule, looked at more than once: kiro repaints its composer late, so the single look right after Enter misread
8316
+ * pastes it took. A generic `working` reading is not evidence (#1663 P1-3: a repaint looks like work).
8317
+ *
8318
+ * Bounded by the budget that confirmation already has, measured from the Enter (#1663 P2-1): the busy wait plus the
8319
+ * post-Enter proof window. Every reading is asked again after its await — still this write's owner (`current`: the
8320
+ * caller's fence, no stop, the same tmux and spawn, #1663 P2-2) and still inside the budget — before it counts; a late
8321
+ * or fenced answer proves nothing. Null: not shown — the caller settles uncertain, never a second Enter.
8322
+ */
8323
+ async rawPasteTakenOnPane(windowId, enterAt, enterMono, formatted, current) {
8324
+ const deadline = enterMono + CONFIRM_BUSY_POLLS * CONFIRM_BUSY_POLL_MS + POST_ENTER_PROOF_WINDOW_MS;
8325
+ const live = () => current() && performance.now() <= deadline;
8326
+ for (;;) {
8327
+ if (!live())
8328
+ return null;
8329
+ if (this.controlClient?.hasOutputSince?.(windowId, enterAt)) {
8330
+ const residue = await this.pasteResidue(formatted);
8331
+ if (!live())
8332
+ return null;
8333
+ if (residue === "absent")
8334
+ return RAW_PASTE_LEFT_INPUT;
8335
+ }
8336
+ await new Promise(r => setTimeout(r, POST_ENTER_PROOF_POLL_MS));
8337
+ }
8338
+ }
7772
8339
  /** Once Kiro returns to its prompt, its unique envelope id in history proves submission. */
7773
8340
  async kiroSubmissionEvidence(windowId, formatted, signature) {
7774
8341
  if (!this.tmux)
@@ -7826,6 +8393,8 @@ export class Daemon extends EventEmitter {
7826
8393
  if (!current())
7827
8394
  return false;
7828
8395
  if (evidence === "submitted") {
8396
+ if (pending.signature.unique)
8397
+ pending.signature.humanPositive?.("unique-id-pane");
7829
8398
  if (status)
7830
8399
  this.emit("message_confirmed", status);
7831
8400
  return true;
@@ -7861,6 +8430,8 @@ export class Daemon extends EventEmitter {
7861
8430
  if (retry === "cancelled")
7862
8431
  return false;
7863
8432
  if (retry === "submitted") {
8433
+ if (pending.signature.unique)
8434
+ pending.signature.humanPositive?.("unique-id-pane");
7864
8435
  if (status)
7865
8436
  this.emit("message_confirmed", status);
7866
8437
  return true;
@@ -8001,9 +8572,28 @@ export class Daemon extends EventEmitter {
8001
8572
  verdict = { reached: false }, durableAttempt, steer = false,
8002
8573
  // #829: the caller's fence, asked once more after the last await before the
8003
8574
  // first side effect. Stale means unwritten: false, with no failure verdict.
8004
- stillCurrent) {
8005
- const signature = this.submissionSignature(formatted, submissionId);
8575
+ stillCurrent, humanVerificationId,
8576
+ // Board 003648ac: called synchronously right before each paste — after the last await and fence of that attempt.
8577
+ beforePaste,
8578
+ // Board 907c7899: awaited each attempt after its last capture, begin and fence; the fence is asked again after it.
8579
+ lookBeforePaste) {
8580
+ const signature = this.submissionSignature(formatted, humanVerificationId ?? submissionId, humanVerificationId !== undefined);
8006
8581
  const rawPaste = durableAttempt?.submissionMode === "raw_paste";
8582
+ const humanAttempt = verdict.humanReceipt;
8583
+ const humanMode = rawPaste ? "raw_paste" : steer ? "steer"
8584
+ : handingOffToNativeQueue ? "native_queue_handoff" : "idle_submit";
8585
+ if (humanAttempt && signature.unique && signature.value === "agend-human-id:" + humanAttempt.verificationId
8586
+ && humanBodyHash(formatted) === humanAttempt.expectedWireHash) {
8587
+ signature.humanPositive = tag => {
8588
+ if (!humanAttempt.current() || (stillCurrent && !stillCurrent()))
8589
+ return;
8590
+ humanAttempt.proof = {
8591
+ receipt_id: humanAttempt.receiptId, verification_id: humanAttempt.verificationId,
8592
+ wire_sha256: humanBodyHash(formatted), owner_key: humanAttempt.ownerKey,
8593
+ mode: humanMode, tag,
8594
+ };
8595
+ };
8596
+ }
8007
8597
  let windowId = initialWindowId;
8008
8598
  // The spawn this write belongs to: acceptance evidence is only read while it is still the current one (#1197).
8009
8599
  const spawnAtWrite = this.spawnGeneration;
@@ -8081,19 +8671,49 @@ export class Daemon extends EventEmitter {
8081
8671
  offset: attemptEvidence.transcriptOffset,
8082
8672
  };
8083
8673
  // From here every proof of this write may also ask the CLI's transcript (the pane can lose the echo to the
8084
- // reply, or still be painting the paste) — this delivery's exact marker only.
8085
- if (["claude-code", "codex"].includes(attemptEvidence.backend)) {
8086
- signature.transcript = this.transcriptProofFor(verdict.transcriptCheckpoint, durableAttempt.deliveryId, spawnAtWrite, stillCurrent, kind => { verdict.transcriptProof = kind; });
8674
+ // reply, or still be painting the paste) — this delivery's exact marker only, or a raw paste's exact text (#1647).
8675
+ // A raw paste's text only where the transcript is this instance's alone (RAW_TEXT_TRANSCRIPT_BACKENDS).
8676
+ if (["claude-code", "codex"].includes(attemptEvidence.backend)
8677
+ && (!rawPaste || RAW_TEXT_TRANSCRIPT_BACKENDS.includes(attemptEvidence.backend))) {
8678
+ signature.transcript = this.transcriptProofFor(verdict.transcriptCheckpoint, rawPaste ? { rawText: formatted } : durableAttempt.deliveryId, spawnAtWrite, stillCurrent, kind => { verdict.transcriptProof = kind; });
8087
8679
  }
8088
8680
  }
8089
8681
  }
8090
- if (rawPaste)
8682
+ if (lookBeforePaste) {
8683
+ await lookBeforePaste();
8684
+ // The look awaited: this attempt is still this write's — the same fence, tmux and launch — or nothing is written.
8685
+ if (!steerCurrent()) {
8686
+ verdict.fenced = !verdict.durableBeginCommitted;
8687
+ return false;
8688
+ }
8689
+ }
8690
+ if (rawPaste) {
8091
8691
  verdict.paneWriteStarted = true;
8692
+ // #1843: this write starts silent work even if its submission proof later stays uncertain.
8693
+ // Retire the preceding human guard before the first write, not after proof: its idle callback must never
8694
+ // turn a silent heartbeat into a channel reply or a reply-required recovery prompt. This may also retire a
8695
+ // genuine missed human reply still awaiting confirmation/recovery; log that tradeoff instead of notifying.
8696
+ if (durableAttempt?.silentSchedule) {
8697
+ this.clearReplyGuardConfirm();
8698
+ const previous = this.turnReplyGuard.snapshot();
8699
+ if (previous) {
8700
+ this.turnReplyGuard.complete(previous.generation);
8701
+ this.logger.info({
8702
+ correlationId: previous.target.correlationId ?? null,
8703
+ generation: previous.generation,
8704
+ phase: previous.phase,
8705
+ reason: "silent_schedule_delivery",
8706
+ deliveryId: durableAttempt.deliveryId,
8707
+ }, "Reply guard retired before silent schedule write");
8708
+ }
8709
+ }
8710
+ }
8092
8711
  // For raw commands the paste itself may alter the live composer. Treat a
8093
8712
  // failed/throwing write as uncertain rather than retrying bytes into the
8094
8713
  // same pane; the write-ahead Enter marker later separates safe pre-Enter
8095
8714
  // recovery from a possibly executed command.
8096
8715
  const pasteGeneration = this.spawnGeneration;
8716
+ beforePaste?.();
8097
8717
  const pasted = await this.tmux.pasteBuffer(formatted);
8098
8718
  if (pasted)
8099
8719
  this.rememberPaste(formatted, pasteGeneration);
@@ -8147,6 +8767,7 @@ export class Daemon extends EventEmitter {
8147
8767
  // positively hold typed text, and after it be the empty placeholder again. The empty placeholder here means the
8148
8768
  // paste never reached the box; an unreadable box (a failed or late capture, a dialog) proves nothing: no Enter.
8149
8769
  const steerComposerProof = handingOffToNativeQueue && steer && !!this.backend?.readSteerComposer && tmuxAtWrite !== null;
8770
+ let humanComposerOwned = false;
8150
8771
  let steerEnterDeadline = Infinity;
8151
8772
  if (steerComposerProof) {
8152
8773
  const deadline = performance.now() + STEER_PASTE_VISIBLE_MS;
@@ -8170,10 +8791,13 @@ export class Daemon extends EventEmitter {
8170
8791
  return this.failDelivery(verdict, status, "steer-paste", before === null ? "box-unread" : `box-still-${before.mode}`);
8171
8792
  if (!this.steerPasteIsOurs(before.pane, formatted))
8172
8793
  return this.failDelivery(verdict, status, "steer-paste", "box-not-this-paste");
8794
+ const shown = this.backend?.readSteerComposerText?.(before.pane)?.replace(/\s+/g, " ").trim();
8795
+ humanComposerOwned = !!humanAttempt && shown === "[agend-human-id:" + humanAttempt.verificationId + "]";
8173
8796
  // …and so is the Enter: the key is sent only while the reading is still within it.
8174
8797
  steerEnterDeadline = deadline;
8175
8798
  }
8176
8799
  let enterAt = Date.now();
8800
+ const enterMono = performance.now();
8177
8801
  const steerEnterCurrent = () => steerCurrent() && performance.now() <= steerEnterDeadline;
8178
8802
  if (!(await this.sendDeliveryEnter("initial-submit", steerComposerProof ? steerEnterCurrent : undefined, durableAttempt))) {
8179
8803
  if (steerComposerProof && !steerCurrent()) {
@@ -8240,6 +8864,9 @@ export class Daemon extends EventEmitter {
8240
8864
  if (after === "steer" || after === "queue" || after === "idle") {
8241
8865
  const probe = await withinMs(this.probeBlockingDialog(), deadline - performance.now());
8242
8866
  if (probe?.state === "clear" && performance.now() <= deadline && steerCurrent() && this.getWindowId() === windowId) {
8867
+ if (humanComposerOwned)
8868
+ signature.humanPositive?.(after === "steer" ? "composer-empty-steer"
8869
+ : after === "queue" ? "composer-empty-queue" : "composer-empty-idle");
8243
8870
  verdict.phase = "steer-accepted";
8244
8871
  verdict.proof = `steer-composer-${after}`;
8245
8872
  if (status)
@@ -8419,12 +9046,20 @@ export class Daemon extends EventEmitter {
8419
9046
  return { kind: "kiro-pending", windowId, spawnGeneration: this.spawnGeneration, signature };
8420
9047
  }
8421
9048
  }
8422
- if (!submitted) {
8423
- if (rawPaste) {
9049
+ if (!submitted && rawPaste) {
9050
+ // #1647: kiro paints output at once but its composer late, so one residue look right after Enter misreads a
9051
+ // paste it took. Keep looking within the confirmation's own budget from the Enter, under this write's fence;
9052
+ // never a second Enter (a raw command must not run twice).
9053
+ const taken = await this.rawPasteTakenOnPane(windowId, enterAt, enterMono, formatted, steerCurrent);
9054
+ if (!taken) {
8424
9055
  verdict.phase = "raw-paste-submit-proof";
8425
9056
  verdict.proof = "no-positive-acceptance-proof";
8426
9057
  return false;
8427
9058
  }
9059
+ verdict.proof = taken;
9060
+ submitted = true;
9061
+ }
9062
+ if (!submitted) {
8428
9063
  // Legacy system pastes have no unique envelope id. Preserve their
8429
9064
  // existing bounded retry until they can be tied to a specific turn.
8430
9065
  this.logger.warn("Message not submitted after Enter — waiting for the prompt, then re-sending Enter once");
@@ -8737,10 +9372,10 @@ export class Daemon extends EventEmitter {
8737
9372
  * A write's transcript proof: bounded looks from its checkpoint, fenced to the write — its spawn and launch, a stop,
8738
9373
  * and the caller's own fence — so a look that lands after any of them is never used (#1380 review).
8739
9374
  */
8740
- transcriptProofFor(checkpoint, deliveryId, spawnAtWrite, stillCurrent, onProof) {
9375
+ transcriptProofFor(checkpoint, needle, spawnAtWrite, stillCurrent, onProof) {
8741
9376
  const launchAtWrite = this.launchFenceEpoch;
8742
9377
  return {
8743
- reader: new TranscriptDeltaReader(checkpoint.path, checkpoint.offset, checkpoint.backend, deliveryId),
9378
+ reader: new TranscriptDeltaReader(checkpoint.path, checkpoint.offset, checkpoint.backend, needle),
8744
9379
  current: () => !this.deliveryWritesStopping && this.spawnGeneration === spawnAtWrite
8745
9380
  && this.launchFenceEpoch === launchAtWrite && (stillCurrent?.() ?? true),
8746
9381
  onProof,
@@ -8818,8 +9453,11 @@ export class Daemon extends EventEmitter {
8818
9453
  // the input row is proof in itself — which also means a momentary
8819
9454
  // failure to read the pane BEFORE pasting cannot turn a delivered
8820
9455
  // message into a re-paste.
8821
- if (signature.unique && after.payload > 0)
9456
+ if (signature.unique && after.payload > 0) {
9457
+ if (after.inputReadable)
9458
+ signature.humanPositive?.("unique-id-pane");
8822
9459
  return this.submittedProof();
9460
+ }
8823
9461
  if (unattributedResidue)
8824
9462
  return "unverifiable";
8825
9463
  // 3. Otherwise the evidence must be NEW relative to the pane as it was
@@ -8956,7 +9594,7 @@ export class Daemon extends EventEmitter {
8956
9594
  * paste that never landed. System pastes carry no envelope and fall back to
8957
9595
  * the body signature, which the baseline comparison still ties to this paste.
8958
9596
  */
8959
- submissionSignature(formatted, trustedId) {
9597
+ submissionSignature(formatted, trustedId, verification = false) {
8960
9598
  // The id must come from the message's own metadata, never from scanning the
8961
9599
  // rendered text: agents and users discuss message ids in the body all the
8962
9600
  // time ("check message_id: abc"), and a body-scanned value would match an
@@ -8965,7 +9603,7 @@ export class Daemon extends EventEmitter {
8965
9603
  // the handoff metadata identifies THIS delivery.
8966
9604
  const id = trustedId?.trim();
8967
9605
  if (id)
8968
- return { value: `message_id:${id}`, unique: true };
9606
+ return { value: (verification ? "agend-human-id:" : "message_id:") + id, unique: true };
8969
9607
  return { value: pastedTextSignature(formatted), unique: false };
8970
9608
  }
8971
9609
  /**
@@ -9487,6 +10125,11 @@ export class Daemon extends EventEmitter {
9487
10125
  respond(null, `reply: ${buttons.error}`);
9488
10126
  return;
9489
10127
  }
10128
+ // #1823: a poll's results need the fleet's store and outbox; as text it would never close.
10129
+ if (args.poll !== undefined && args.poll !== null) {
10130
+ respond(null, "reply: a poll needs AgEnD's fleet; send the question without poll");
10131
+ return;
10132
+ }
9490
10133
  if (buttons) {
9491
10134
  routed = { ...args, text: `${String(args.text)}\n\n${replyButtonsFallbackText(buttons.buttons)}` };
9492
10135
  delete routed.buttons;
@@ -10072,7 +10715,7 @@ export class Daemon extends EventEmitter {
10072
10715
  * the same tmux window, not aborted, writes not stopping — re-checked after every await, and between the paste and
10073
10716
  * its Enter. The whole step, every await included, ends at one monotonic deadline: a late capture never confirms
10074
10717
  * and a late admission never writes; a paste whose Enter would land past it is taken back. Not confirmed in time:
10075
- * the old setup stays, a launch warning says so, the next launch tries again. A stop, pause or respawn ends it
10718
+ * 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
10076
10719
  * silently, confirming and removing nothing. Never throws into the spawn.
10077
10720
  */
10078
10721
  async ensureBackendAgent() {
@@ -10118,8 +10761,10 @@ export class Daemon extends EventEmitter {
10118
10761
  if (active === sw.agent && (wroteAt === null || capturedAt > wroteAt)) {
10119
10762
  if (sw.alreadyConfirmed && wroteAt === null)
10120
10763
  return; // already ours, already on record
10121
- for (const warning of sw.confirm())
10122
- this.emit("backend_launch_warning", { name: this.name, message: warning });
10764
+ for (const { message, quiet } of sw.confirm()) {
10765
+ this.logger.warn({ agent: sw.agent, warning: message }, "The agent switch was confirmed with a warning");
10766
+ this.emit("backend_launch_warning", { name: this.name, message, quiet: quiet === true });
10767
+ }
10123
10768
  this.logger.info({ agent: sw.agent, switched: wroteAt !== null }, "The resumed conversation runs as this instance's agent");
10124
10769
  return;
10125
10770
  }
@@ -10168,7 +10813,9 @@ export class Daemon extends EventEmitter {
10168
10813
  if (!fenced())
10169
10814
  return; // a stop, pause or respawn: nothing to say, nothing to do
10170
10815
  this.logger.warn({ agent: sw.agent, typed: wroteAt !== null }, "The resumed conversation did not switch to this instance's agent in time");
10171
- this.emit("backend_launch_warning", { name: this.name, message: t("kiro.switch_timeout", Daemon.AGENT_SWITCH_BUDGET_MS / 1000) });
10816
+ // #1794: quiet — the conversation keeps running as its saved agent with the shared entries, and the next launch
10817
+ // tries again; the user loses nothing and has nothing to do. Logged above and kept on the Log tab only.
10818
+ this.emit("backend_launch_warning", { name: this.name, message: t("kiro.switch_timeout", Daemon.AGENT_SWITCH_BUDGET_MS / 1000), quiet: true });
10172
10819
  }
10173
10820
  catch (err) {
10174
10821
  this.startupAdmission?.();
@@ -10627,7 +11274,7 @@ export class Daemon extends EventEmitter {
10627
11274
  if (launchFence !== this.launchFenceEpoch)
10628
11275
  throw new Error("Launch cancelled: the instance was stopped or paused while the launch was being prepared");
10629
11276
  this.backend.writeConfig(backendConfig);
10630
- this.backend.preTrust?.(this.config.working_directory);
11277
+ this.backend.preTrust?.(this.config.working_directory, backendConfig);
10631
11278
  // Resolve working directory (e.g. symlink for hidden paths)
10632
11279
  const resolvedCwd = this.backend.resolveWorkingDirectory?.(this.config.working_directory, this.name) ?? this.config.working_directory;
10633
11280
  // Generate a fresh per-instance agent token each spawn. agent-cli reads
@@ -10697,6 +11344,11 @@ export class Daemon extends EventEmitter {
10697
11344
  this.logger.warn({ warning: launchWarning }, "Backend launch fell back");
10698
11345
  this.emit("backend_launch_warning", { name: this.name, message: launchWarning });
10699
11346
  }
11347
+ // #1811: what the launch left out and why (an effort this CLI or model does not take) — the Log tab, not the chat.
11348
+ for (const note of this.backend.consumeQuietLaunchNotes?.() ?? []) {
11349
+ this.logger.info({ note }, "Launch note");
11350
+ this.emit("backend_launch_warning", { name: this.name, message: note, quiet: true });
11351
+ }
10700
11352
  // Ensure tmux session exists (may have been destroyed if all windows died)
10701
11353
  await TmuxManager.ensureSession(this.tmuxSessionName);
10702
11354
  if (this.stormWindow?.observeServerAlive(await TmuxManager.getServerPid(this.tmuxSessionName))) {
@@ -11267,6 +11919,14 @@ export class Daemon extends EventEmitter {
11267
11919
  }, "Snapshot written");
11268
11920
  return snapshot;
11269
11921
  }
11922
+ /** #1699: the claude config dir this instance's CLI uses — its credential profile's, else exactly as before. */
11923
+ claudeConfigDir() {
11924
+ return claudeConfigDirFor(claudeProfileConfigDir(this.config.backend_options?.["claude-code"]));
11925
+ }
11926
+ /** Where this instance's CLI keeps the conversation a turn resume is judged on (#1699: claude's follows the profile). */
11927
+ resumeRoots(workingDirectory) {
11928
+ return { instanceDir: this.instanceDir, workingDirectory, claudeConfigDir: this.claudeConfigDir() };
11929
+ }
11270
11930
  /**
11271
11931
  * The credential profile's store this instance's CLI writes to, if any.
11272
11932
  *