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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (298) hide show
  1. package/THIRD_PARTY_NOTICES.md +40 -0
  2. package/dist/agent-cli.js +69 -2
  3. package/dist/agent-cli.js.map +1 -1
  4. package/dist/agent-endpoint.js +11 -0
  5. package/dist/agent-endpoint.js.map +1 -1
  6. package/dist/agent-whats-new.js +3 -0
  7. package/dist/agent-whats-new.js.map +1 -1
  8. package/dist/antigravity-usage-scan.d.ts +58 -0
  9. package/dist/antigravity-usage-scan.js +257 -0
  10. package/dist/antigravity-usage-scan.js.map +1 -0
  11. package/dist/antigravity-usage-worker.d.ts +1 -0
  12. package/dist/antigravity-usage-worker.js +10 -0
  13. package/dist/antigravity-usage-worker.js.map +1 -0
  14. package/dist/auth-api.js +4 -0
  15. package/dist/auth-api.js.map +1 -1
  16. package/dist/backend/kiro-classic-archive.d.ts +52 -0
  17. package/dist/backend/kiro-classic-archive.js +444 -0
  18. package/dist/backend/kiro-classic-archive.js.map +1 -0
  19. package/dist/backend/kiro-identity.d.ts +83 -0
  20. package/dist/backend/kiro-identity.js +144 -0
  21. package/dist/backend/kiro-identity.js.map +1 -1
  22. package/dist/backend/kiro-migration-verify.d.ts +10 -0
  23. package/dist/backend/kiro-migration-verify.js +52 -0
  24. package/dist/backend/kiro-migration-verify.js.map +1 -0
  25. package/dist/backend/kiro-retired-model.d.ts +62 -0
  26. package/dist/backend/kiro-retired-model.js +160 -0
  27. package/dist/backend/kiro-retired-model.js.map +1 -0
  28. package/dist/backend/kiro-v2-store-worker.js +21 -1
  29. package/dist/backend/kiro-v2-store-worker.js.map +1 -1
  30. package/dist/backend/kiro-v2-store.d.ts +8 -0
  31. package/dist/backend/kiro-v2-store.js.map +1 -1
  32. package/dist/backend/kiro.d.ts +169 -2
  33. package/dist/backend/kiro.js +703 -28
  34. package/dist/backend/kiro.js.map +1 -1
  35. package/dist/backend/types.d.ts +108 -0
  36. package/dist/backend/types.js +9 -0
  37. package/dist/backend/types.js.map +1 -1
  38. package/dist/backend-signin-guide.d.ts +1 -0
  39. package/dist/backend-signin-guide.js +9 -0
  40. package/dist/backend-signin-guide.js.map +1 -0
  41. package/dist/backend-signin-pending.d.ts +10 -0
  42. package/dist/backend-signin-pending.js +44 -0
  43. package/dist/backend-signin-pending.js.map +1 -0
  44. package/dist/browser-backend-signins.d.ts +56 -0
  45. package/dist/browser-backend-signins.js +189 -0
  46. package/dist/browser-backend-signins.js.map +1 -0
  47. package/dist/cache-ledger.d.ts +38 -2
  48. package/dist/cache-ledger.js +133 -8
  49. package/dist/cache-ledger.js.map +1 -1
  50. package/dist/cache-prices.d.ts +2 -1
  51. package/dist/cache-prices.js +5 -2
  52. package/dist/cache-prices.js.map +1 -1
  53. package/dist/cache-service.d.ts +68 -0
  54. package/dist/cache-service.js +355 -13
  55. package/dist/cache-service.js.map +1 -1
  56. package/dist/channel/adapters/discord.d.ts +11 -0
  57. package/dist/channel/adapters/discord.js +71 -14
  58. package/dist/channel/adapters/discord.js.map +1 -1
  59. package/dist/channel/ipc-timeouts.js +3 -0
  60. package/dist/channel/ipc-timeouts.js.map +1 -1
  61. package/dist/channel/mcp-tools.js +41 -2
  62. package/dist/channel/mcp-tools.js.map +1 -1
  63. package/dist/channel/tool-router.js +2 -1
  64. package/dist/channel/tool-router.js.map +1 -1
  65. package/dist/classic-channel-manager.d.ts +11 -0
  66. package/dist/classic-channel-manager.js +39 -1
  67. package/dist/classic-channel-manager.js.map +1 -1
  68. package/dist/cli.js +54 -2
  69. package/dist/cli.js.map +1 -1
  70. package/dist/config-validator.d.ts +7 -1
  71. package/dist/config-validator.js +55 -10
  72. package/dist/config-validator.js.map +1 -1
  73. package/dist/config.d.ts +19 -0
  74. package/dist/config.js +33 -1
  75. package/dist/config.js.map +1 -1
  76. package/dist/cpu-quota.d.ts +23 -0
  77. package/dist/cpu-quota.js +86 -0
  78. package/dist/cpu-quota.js.map +1 -0
  79. package/dist/daemon.d.ts +68 -0
  80. package/dist/daemon.js +448 -26
  81. package/dist/daemon.js.map +1 -1
  82. package/dist/dashboard-permissions.d.ts +31 -6
  83. package/dist/dashboard-permissions.js +84 -9
  84. package/dist/dashboard-permissions.js.map +1 -1
  85. package/dist/doctor.js +2 -1
  86. package/dist/doctor.js.map +1 -1
  87. package/dist/fleet-control-audit.d.ts +1 -1
  88. package/dist/fleet-control-audit.js +3 -0
  89. package/dist/fleet-control-audit.js.map +1 -1
  90. package/dist/fleet-manager.d.ts +133 -2
  91. package/dist/fleet-manager.js +905 -94
  92. package/dist/fleet-manager.js.map +1 -1
  93. package/dist/forged-envelope-scan.d.ts +9 -0
  94. package/dist/forged-envelope-scan.js +30 -5
  95. package/dist/forged-envelope-scan.js.map +1 -1
  96. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +5 -2
  97. package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +24 -1
  98. package/dist/general-knowledge/skills/web-appearance/SKILL.canvas-worker-off.md +49 -1
  99. package/dist/general-knowledge/skills/web-appearance/SKILL.md +49 -1
  100. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +47 -9
  101. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +54 -11
  102. package/dist/instance-config-impact.js +10 -5
  103. package/dist/instance-config-impact.js.map +1 -1
  104. package/dist/instance-lifecycle.d.ts +23 -0
  105. package/dist/instance-lifecycle.js +62 -12
  106. package/dist/instance-lifecycle.js.map +1 -1
  107. package/dist/kiro-db-reader.d.ts +5 -0
  108. package/dist/kiro-db-reader.js +18 -0
  109. package/dist/kiro-db-reader.js.map +1 -1
  110. package/dist/kiro-engine-status.d.ts +2 -0
  111. package/dist/kiro-engine-status.js +9 -0
  112. package/dist/kiro-engine-status.js.map +1 -1
  113. package/dist/kiro-migrate.d.ts +60 -0
  114. package/dist/kiro-migrate.js +160 -0
  115. package/dist/kiro-migrate.js.map +1 -0
  116. package/dist/kiro-session-proof.d.ts +41 -0
  117. package/dist/kiro-session-proof.js +89 -0
  118. package/dist/kiro-session-proof.js.map +1 -0
  119. package/dist/kiro-transcript-lane.d.ts +2 -0
  120. package/dist/kiro-transcript-lane.js.map +1 -1
  121. package/dist/kiro-transcript-worker.js +1 -0
  122. package/dist/kiro-transcript-worker.js.map +1 -1
  123. package/dist/kiro-tui-forensic.d.ts +12 -0
  124. package/dist/kiro-tui-forensic.js +110 -0
  125. package/dist/kiro-tui-forensic.js.map +1 -0
  126. package/dist/kiro-usage-run.d.ts +7 -0
  127. package/dist/kiro-usage-run.js +39 -0
  128. package/dist/kiro-usage-run.js.map +1 -0
  129. package/dist/kiro-usage-scan.d.ts +49 -0
  130. package/dist/kiro-usage-scan.js +223 -0
  131. package/dist/kiro-usage-scan.js.map +1 -0
  132. package/dist/kiro-usage-worker.d.ts +1 -0
  133. package/dist/kiro-usage-worker.js +10 -0
  134. package/dist/kiro-usage-worker.js.map +1 -0
  135. package/dist/kiro-usage.d.ts +58 -0
  136. package/dist/kiro-usage.js +310 -0
  137. package/dist/kiro-usage.js.map +1 -0
  138. package/dist/locale.js +230 -8
  139. package/dist/locale.js.map +1 -1
  140. package/dist/log-service.js +1 -1
  141. package/dist/log-service.js.map +1 -1
  142. package/dist/login-url.d.ts +4 -0
  143. package/dist/login-url.js +22 -0
  144. package/dist/login-url.js.map +1 -0
  145. package/dist/needs-you-hub.d.ts +2 -1
  146. package/dist/needs-you-hub.js +1 -1
  147. package/dist/needs-you-hub.js.map +1 -1
  148. package/dist/needs-you.d.ts +10 -2
  149. package/dist/needs-you.js +10 -2
  150. package/dist/needs-you.js.map +1 -1
  151. package/dist/notice-delivery.d.ts +16 -0
  152. package/dist/notice-delivery.js +35 -0
  153. package/dist/notice-delivery.js.map +1 -0
  154. package/dist/office-share-page.d.ts +16 -0
  155. package/dist/office-share-page.js +85 -0
  156. package/dist/office-share-page.js.map +1 -0
  157. package/dist/office-share-service.d.ts +91 -0
  158. package/dist/office-share-service.js +176 -0
  159. package/dist/office-share-service.js.map +1 -0
  160. package/dist/office-share.d.ts +268 -0
  161. package/dist/office-share.js +574 -0
  162. package/dist/office-share.js.map +1 -0
  163. package/dist/opencode-usage-scan.d.ts +28 -0
  164. package/dist/opencode-usage-scan.js +92 -0
  165. package/dist/opencode-usage-scan.js.map +1 -0
  166. package/dist/opencode-usage-worker.d.ts +1 -0
  167. package/dist/opencode-usage-worker.js +10 -0
  168. package/dist/opencode-usage-worker.js.map +1 -0
  169. package/dist/outbound-handlers.js +22 -2
  170. package/dist/outbound-handlers.js.map +1 -1
  171. package/dist/outbound-schemas.d.ts +65 -2
  172. package/dist/outbound-schemas.js +56 -2
  173. package/dist/outbound-schemas.js.map +1 -1
  174. package/dist/public-web-gateway.js +7 -1
  175. package/dist/public-web-gateway.js.map +1 -1
  176. package/dist/quickstart-api.js +13 -2
  177. package/dist/quickstart-api.js.map +1 -1
  178. package/dist/quickstart.js +2 -1
  179. package/dist/quickstart.js.map +1 -1
  180. package/dist/remote-card-answer.d.ts +83 -0
  181. package/dist/remote-card-answer.js +208 -0
  182. package/dist/remote-card-answer.js.map +1 -0
  183. package/dist/remote-card-prompt.d.ts +38 -0
  184. package/dist/remote-card-prompt.js +86 -0
  185. package/dist/remote-card-prompt.js.map +1 -0
  186. package/dist/reply-triggers.d.ts +2 -0
  187. package/dist/reply-triggers.js +3 -0
  188. package/dist/reply-triggers.js.map +1 -0
  189. package/dist/settings-api.d.ts +2 -0
  190. package/dist/settings-api.js +41 -16
  191. package/dist/settings-api.js.map +1 -1
  192. package/dist/settings-change.js +20 -14
  193. package/dist/settings-change.js.map +1 -1
  194. package/dist/settings-effect.js +7 -1
  195. package/dist/settings-effect.js.map +1 -1
  196. package/dist/settings-instance-patch.js +4 -1
  197. package/dist/settings-instance-patch.js.map +1 -1
  198. package/dist/setup-form.d.ts +1 -1
  199. package/dist/setup-form.js +55 -6
  200. package/dist/setup-form.js.map +1 -1
  201. package/dist/setup-host.d.ts +9 -0
  202. package/dist/setup-host.js +127 -6
  203. package/dist/setup-host.js.map +1 -1
  204. package/dist/setup-signin-assets.d.ts +2 -0
  205. package/dist/setup-signin-assets.js +6 -0
  206. package/dist/setup-signin-assets.js.map +1 -0
  207. package/dist/setup-wizard.js +2 -0
  208. package/dist/setup-wizard.js.map +1 -1
  209. package/dist/structured-signin-transport.d.ts +8 -0
  210. package/dist/structured-signin-transport.js +79 -0
  211. package/dist/structured-signin-transport.js.map +1 -0
  212. package/dist/structured-signin.d.ts +131 -0
  213. package/dist/structured-signin.js +388 -0
  214. package/dist/structured-signin.js.map +1 -0
  215. package/dist/tool-permissions.js +29 -0
  216. package/dist/tool-permissions.js.map +1 -1
  217. package/dist/topic-commands.js +6 -2
  218. package/dist/topic-commands.js.map +1 -1
  219. package/dist/transcript-sources.d.ts +27 -2
  220. package/dist/transcript-sources.js +104 -40
  221. package/dist/transcript-sources.js.map +1 -1
  222. package/dist/turn-reply-guard.d.ts +1 -1
  223. package/dist/turn-reply-guard.js +1 -1
  224. package/dist/turn-reply-guard.js.map +1 -1
  225. package/dist/types.d.ts +33 -8
  226. package/dist/ui/backend-signin.js +63 -10
  227. package/dist/ui/chat-store.js +3 -2
  228. package/dist/ui/chat-strings.js +4 -4
  229. package/dist/ui/fleet-office-editor.js +68 -11
  230. package/dist/ui/fleet-office-layout.js +110 -35
  231. package/dist/ui/fleet-office-model.js +6 -3
  232. package/dist/ui/fleet-office-overview.js +167 -0
  233. package/dist/ui/fleet-office-pack.d.ts +13 -3
  234. package/dist/ui/fleet-office-pack.js +214 -24
  235. package/dist/ui/fleet-office-skin.js +98 -23
  236. package/dist/ui/fleet-office-store.js +82 -5
  237. package/dist/ui/fleet-office-view.js +5 -4
  238. package/dist/ui/fleet-office-zones.js +232 -21
  239. package/dist/ui/fleet-office.js +549 -186
  240. package/dist/ui/fleet-usage.js +19 -15
  241. package/dist/ui/office-share-dialog.js +145 -0
  242. package/dist/ui/office-share-page.js +164 -0
  243. package/dist/ui/panel-chat.js +22 -3
  244. package/dist/ui/panel-log.js +4 -4
  245. package/dist/ui/panel-needs.js +5 -5
  246. package/dist/ui/panel-settings.js +38 -12
  247. package/dist/ui/settings-dialogs.js +8 -1
  248. package/dist/ui/settings-guests.js +17 -7
  249. package/dist/ui/settings-model.js +22 -6
  250. package/dist/ui/settings-registry.js +4 -0
  251. package/dist/ui/settings-strings.js +18 -2
  252. package/dist/ui/shared/app-i18n.js +4 -2
  253. package/dist/ui/shared/app-shell.js +14 -6
  254. package/dist/ui/shared/app-stream.js +2 -1
  255. package/dist/ui/shared/app.css +85 -4
  256. package/dist/ui/shared/office/cozy.png +0 -0
  257. package/dist/ui/shared/side-panel.js +95 -0
  258. package/dist/ui/structured-signin.js +115 -0
  259. package/dist/usage-report.d.ts +20 -0
  260. package/dist/usage-report.js +54 -17
  261. package/dist/usage-report.js.map +1 -1
  262. package/dist/usage-rollup.d.ts +9 -1
  263. package/dist/usage-rollup.js +20 -4
  264. package/dist/usage-rollup.js.map +1 -1
  265. package/dist/web-api.d.ts +23 -1
  266. package/dist/web-api.js +292 -3
  267. package/dist/web-api.js.map +1 -1
  268. package/dist/web-auth.js +1 -1
  269. package/dist/web-auth.js.map +1 -1
  270. package/dist/web-backend-login.d.ts +1 -7
  271. package/dist/web-backend-login.js +2 -24
  272. package/dist/web-backend-login.js.map +1 -1
  273. package/dist/web-office-acts.d.ts +173 -0
  274. package/dist/web-office-acts.js +285 -0
  275. package/dist/web-office-acts.js.map +1 -0
  276. package/dist/web-office-pack-read.d.ts +35 -0
  277. package/dist/web-office-pack-read.js +252 -0
  278. package/dist/web-office-pack-read.js.map +1 -0
  279. package/dist/web-office-pack-tools.d.ts +232 -0
  280. package/dist/web-office-pack-tools.js +754 -0
  281. package/dist/web-office-pack-tools.js.map +1 -0
  282. package/dist/web-office-skins.d.ts +71 -6
  283. package/dist/web-office-skins.js +321 -35
  284. package/dist/web-office-skins.js.map +1 -1
  285. package/dist/web-office-store.d.ts +5 -2
  286. package/dist/web-office-store.js +69 -10
  287. package/dist/web-office-store.js.map +1 -1
  288. package/dist/web-settings-patch.d.ts +2 -1
  289. package/dist/web-settings-patch.js +4 -1
  290. package/dist/web-settings-patch.js.map +1 -1
  291. package/dist/working-recovery.d.ts +2 -2
  292. package/dist/working-recovery.js +43 -8
  293. package/dist/working-recovery.js.map +1 -1
  294. package/dist/working-state-journal.d.ts +2 -0
  295. package/dist/working-state-journal.js +16 -1
  296. package/dist/working-state-journal.js.map +1 -1
  297. package/package.json +3 -2
  298. package/templates/windows-shortcuts.ps1 +155 -140
package/dist/daemon.js CHANGED
@@ -1,9 +1,13 @@
1
+ import { readActiveKiroModel } from "./backend/kiro.js";
2
+ import { dashboardAccessOf } from "./dashboard-permissions.js";
3
+ import { sendPackSources } from "./web-office-pack-read.js";
1
4
  import { agentWhatsNew, agendVersion, planWhatsNew, readWhatsNewRecord, recordWhatsNew, whatsNewNotice } from "./agent-whats-new.js";
2
5
  import { join, dirname, resolve } from "node:path";
3
6
  import { checkoutTarget, readCheckouts, releaseCandidate, reposRoot, verifyWorktree, writeCheckouts } from "./repo-worktree-guard.js";
4
7
  import { mkdirSync, writeFileSync, readFileSync, readdirSync, existsSync, unlinkSync, rmSync, appendFileSync, statSync, renameSync } from "node:fs";
5
8
  import { COMPACT_WATCH_TIMEOUT_MS, launchIdentity } from "./compact-watch.js";
6
9
  import { KIRO_COMPACT_PANE_LINES, KiroCompactPaneFollower } from "./kiro-compact-pane.js";
10
+ import { clearKiroSessionProof, proveKiroSessionId, recordKiroSessionProof } from "./kiro-session-proof.js";
7
11
  import { fileURLToPath } from "node:url";
8
12
  import { ensureInstanceDir } from "./private-dir.js";
9
13
  import { createHash, randomBytes, randomUUID } from "node:crypto";
@@ -35,13 +39,14 @@ import { ContextGuardian } from "./context-guardian.js";
35
39
  import { IpcServer } from "./channel/ipc-bridge.js";
36
40
  import { daemonBudgetMs } from "./channel/ipc-timeouts.js";
37
41
  import { MessageBus } from "./channel/message-bus.js";
38
- import { shellQuote, UnsupportedCliError } from "./backend/types.js";
42
+ import { shellQuote, UnsupportedCliError, ResumeBlockedError } from "./backend/types.js";
39
43
  import { getTmuxSession } from "./config.js";
40
44
  import { routeToolCall } from "./channel/tool-router.js";
41
45
  import { parseReplyButtons, replyButtonsFallbackText } from "./reply-buttons.js";
42
46
  import { HangDetector } from "./hang-detector.js";
43
47
  import { writeSecretFile } from "./secret-file.js";
44
48
  import { PaneWriteLock } from "./pane-write-lock.js";
49
+ import { answerRemoteCard } from "./remote-card-answer.js";
45
50
  import { buildFleetInstructions } from "./instructions.js";
46
51
  import { formatHumanInboundMessage, prepareDeliveryWire, steerInboundEnvelope, BTW_INBOUND_PREFIX, humanBodyHash, humanOwnerObjectId } from "./human-delivery-receipts.js";
47
52
  import { formatCrossInstanceInboundMessage, renderCrossInstanceHandoffMetadata, } from "./cross-instance-envelope.js";
@@ -86,6 +91,12 @@ const WEB_THEME_IPC = Object.freeze({
86
91
  apply_web_effect: "fleet_apply_web_effect",
87
92
  remove_web_effect: "fleet_remove_web_effect",
88
93
  delete_web_effect: "fleet_delete_web_effect",
94
+ // #2578: its own character in the Office view — the args as they are (the fleet checks them).
95
+ office_view: "fleet_office_view", office_set: "fleet_office_set",
96
+ // #2692: the skin packs — check and upload read the pack here (web-office-pack-read.ts) and send its bytes in parts.
97
+ office_pack_check: "fleet_office_pack_check", office_pack_upload: "fleet_office_pack_upload",
98
+ office_pack_delete: "fleet_office_pack_delete", office_pack_restore: "fleet_office_pack_restore",
99
+ office_pack_list: "fleet_office_pack_list", office_set_style: "fleet_office_set_style",
89
100
  });
90
101
  /** The largest picture any slot may take (a card slot's ceiling; the fleet checks the limit in force): read no more. */
91
102
  const WEB_THEME_ASSET_MAX_BYTES = ASSET_UPLOAD_MAX_BYTES;
@@ -570,6 +581,8 @@ export const OWN_SWITCH_WAIT_MS = 6_000;
570
581
  const OWN_SWITCH_SETTLE_MS = 3_000;
571
582
  /** A blocking dialog still on screen this long after first being seen is reported for a human. */
572
583
  const DIALOG_PARKED_NOTIFY_MS = 60_000;
584
+ /** #2675: a card a person can answer from the chat is posted once it has been on screen, unchanged, this long. */
585
+ const REMOTE_CARD_STABLE_MS = 1_500;
573
586
  /** #1691: a prompt no table knows must sit unchanged this long (monotonic) before it holds deliveries and is reported. */
574
587
  export const UNRECOGNISED_PROMPT_STABLE_MS = 60_000;
575
588
  /** #1691: the rows under a prompt a backend without a composer reader shows its ready chrome in (status bar, hint). */
@@ -698,6 +711,18 @@ const FIRST_DELIVERY_WINDOW_MS = 5_000;
698
711
  const NATIVE_QUEUE_PASTE_VERIFY_MS = 2_000;
699
712
  /** #1405: how long a steer's paste gets to show in a mode-switched composer before its one Enter (captures included). */
700
713
  const STEER_PASTE_VISIBLE_MS = 2_000;
714
+ /**
715
+ * #2676: the longest one-line paste kiro's TUI composer still shows as typed text; longer, it collapses to a
716
+ * `N chars ▸` chip (2.34.0 tui.js: more than 10 lines or 500 characters collapse).
717
+ */
718
+ const KIRO_COLLAPSE_CHARS = 500;
719
+ /**
720
+ * #1405/#2676: whether a mode-switched composer in `mode` may take a busy hand-off: a steer only on "steer", an ordinary
721
+ * message on "steer" or "queue" (kiro takes either into its tray), and "idle" — the turn over — only where `allowIdle`.
722
+ */
723
+ function composerModeAdmits(mode, allowIdle, steerOnly) {
724
+ return mode === "steer" || (!steerOnly && mode === "queue") || (allowIdle && mode === "idle");
725
+ }
701
726
  /** `promise`'s value if it settles within `ms`, else undefined — the late answer is dropped, never awaited (#1405). */
702
727
  async function withinMs(promise, ms) {
703
728
  if (ms <= 0) {
@@ -1135,6 +1160,29 @@ export class Daemon extends EventEmitter {
1135
1160
  logTranscriptPath() { return this.transcriptMonitor?.currentPath() ?? null; }
1136
1161
  /** #1218 (kiro): the launch this daemon's backend last built, for the Log's reason only (kiro-log-gate.ts). */
1137
1162
  logLaunch() { return this.backend?.logLaunch?.() ?? null; }
1163
+ async kiroMigrationIdle() {
1164
+ const tmux = this.tmux, owner = this.interactionOwner();
1165
+ if (!tmux || this.spawning || this.runtimeMonitorsFrozen || this.processStatus !== "running")
1166
+ return false;
1167
+ const pane = await tmux.capturePane(2000).catch(() => null);
1168
+ return pane !== null && tmux === this.tmux && sameInteractionOwner(owner, this.interactionOwner())
1169
+ && this.processStatus === "running" && !this.spawning && !this.runtimeMonitorsFrozen && this.paneAuthoritativelyIdle(pane);
1170
+ }
1171
+ /** One read-only migration observation. Never types into a pane or selects a picker. */
1172
+ async kiroMigrationObservation() {
1173
+ const tmux = this.tmux, owner = this.interactionOwner(), backend = this.backend;
1174
+ if (!tmux || backend?.binaryName !== "kiro-cli" || this.processStatus !== "running" || this.spawning || this.runtimeMonitorsFrozen)
1175
+ return null;
1176
+ const pane = await tmux.capturePane(2000).catch(() => null);
1177
+ if (pane === null || tmux !== this.tmux || !sameInteractionOwner(owner, this.interactionOwner())
1178
+ || this.processStatus !== "running" || this.spawning || this.runtimeMonitorsFrozen || !this.paneAuthoritativelyIdle(pane)
1179
+ || backend.readSteerComposer?.(pane) !== "idle")
1180
+ return null;
1181
+ const launch = backend.transcriptLaunch?.(), path = this.logTranscriptPath();
1182
+ // Agent switching is separately disabled for saved-agent resumes; it is not an observation API.
1183
+ const model = readActiveKiroModel(pane);
1184
+ return launch?.engine === "v2" && launch.sessionId && model && path ? { sessionId: launch.sessionId, model, transcript: path } : null;
1185
+ }
1138
1186
  guardian = null;
1139
1187
  adapter = null;
1140
1188
  pendingIpcRequests = new Map();
@@ -1254,6 +1302,12 @@ export class Daemon extends EventEmitter {
1254
1302
  dialogParkedKey = null;
1255
1303
  dialogParkedInteractionEpisode = null;
1256
1304
  dialogParkedReported = false;
1305
+ /** #2675: the card on screen that a person can answer from the chat — what it asks, since when, and whether it was posted. */
1306
+ remoteCardShown = null;
1307
+ /** #2675: when (performance.now, taken before its capture) the newest read the card tracker used was started. */
1308
+ remoteCardReadAt = -Infinity;
1309
+ /** #2675: an answer from the chat is being keyed in (one at a time). */
1310
+ remoteCardAnswering = false;
1257
1311
  /**
1258
1312
  * #1691: the prompt at the bottom of the pane that no backend table knows — its block, since when (performance.now)
1259
1313
  * it has been unchanged, and whether that has lasted UNRECOGNISED_PROMPT_STABLE_MS. Updated by the runtime scan.
@@ -1749,6 +1803,11 @@ export class Daemon extends EventEmitter {
1749
1803
  return `${ep.type}:${ep.pattern.source}`;
1750
1804
  }
1751
1805
  /** Read the occurrence baseline so a lifecycle-owned restart can carry it forward. */
1806
+ /** #2648: kiro only — what a model error on the current launch should do (see kiro-retired-model.ts). */
1807
+ kiroModelError() {
1808
+ const backend = this.backend;
1809
+ return typeof backend?.onModelError === "function" ? backend.onModelError() : null;
1810
+ }
1752
1811
  getErrorPatternOccurrenceCount(type, pattern) {
1753
1812
  return this.lastErrorCount.get(`${type}:${pattern.source}`) ?? 0;
1754
1813
  }
@@ -1825,6 +1884,9 @@ export class Daemon extends EventEmitter {
1825
1884
  this.spawnGate = spawnGate;
1826
1885
  this.stormWindow = stormWindow;
1827
1886
  this.backendOutage = backendOutage;
1887
+ // The live process owns its adopted configuration. Fleet defaults may be re-resolved in place after a save;
1888
+ // sharing that object would activate cold changes before the normal restart/apply boundary.
1889
+ this.config = structuredClone(config);
1828
1890
  if (!rootLogger)
1829
1891
  throw new Error("Daemon requires a shared root logger");
1830
1892
  this.runtimeIdentity ??= {
@@ -2015,6 +2077,11 @@ export class Daemon extends EventEmitter {
2015
2077
  this.finishDurableDelivery(delivery, "delivered", `steer-accepted; composer emptied (${verdict.proof.slice("steer-composer-".length)})`, verdict);
2016
2078
  return;
2017
2079
  }
2080
+ // #2676: an ordinary message typed into that busy composer, proven the same way: taken into the CLI's tray.
2081
+ if (verdict.proof?.startsWith("handoff-composer-")) {
2082
+ this.finishDurableDelivery(delivery, "delivered", `composer-handoff-accepted; composer emptied (${verdict.proof.slice("handoff-composer-".length)})`, verdict);
2083
+ return;
2084
+ }
2018
2085
  // A backend whose input row cannot be read: all that vouched for this delivery is that the pane printed something
2019
2086
  // after Enter, which a redraw that wiped the paste satisfies too (#758). Its transcript can say more.
2020
2087
  if (verdict.proof === "output-edge") {
@@ -2400,7 +2467,7 @@ export class Daemon extends EventEmitter {
2400
2467
  // (statusline transcript); codex/kiro/opencode read their CLI's own
2401
2468
  // conversation store via a pluggable source. Backends with no known
2402
2469
  // source stay inert exactly as before.
2403
- this.transcriptMonitor = this.buildTranscriptMonitor(createTranscriptSource(this.config.backend ?? "claude-code", this.config.working_directory, this.credentialProfileStore()));
2470
+ this.transcriptMonitor = this.buildTranscriptMonitor(createTranscriptSource(this.config.backend ?? "claude-code", this.config.working_directory, this.credentialProfileStore(), () => this.backend?.transcriptLaunch?.() ?? null));
2404
2471
  const transcriptMonitor = this.transcriptMonitor;
2405
2472
  const transcriptFence = this.launchFenceEpoch;
2406
2473
  // Baseline in the isolate before this daemon accepts new work. A
@@ -3421,6 +3488,11 @@ export class Daemon extends EventEmitter {
3421
3488
  if (!(err instanceof UnsupportedCliError))
3422
3489
  return false;
3423
3490
  this.healthCheckPaused = true;
3491
+ if (err instanceof ResumeBlockedError) {
3492
+ this.logger.error({ reason: err.message }, "Not respawning: retained session needs store repair");
3493
+ this.emitSupervisionEnded(err.message, err.remedy, undefined, "resume_blocked");
3494
+ return true;
3495
+ }
3424
3496
  this.logger.error({ reason: err.message }, "Not respawning: the installed CLI cannot run this instance as configured");
3425
3497
  this.emitSupervisionEnded(err.message, "Install a CLI version that supports this instance's configuration, then start it again.", undefined, "cli_unsupported");
3426
3498
  return true;
@@ -3732,6 +3804,7 @@ export class Daemon extends EventEmitter {
3732
3804
  if (!answersBackedOff)
3733
3805
  this.dialogAnswers = null;
3734
3806
  this.trackDialogParked(blockingSeen);
3807
+ this.trackRemoteCard(blockingSeen, pane, captureMono);
3735
3808
  if (blockingSeen)
3736
3809
  return; // held dialog: skip error checks this cycle
3737
3810
  this.evaluateErrorPatterns(pane, patterns, readyPattern, Date.now(), busyPattern);
@@ -5851,8 +5924,8 @@ export class Daemon extends EventEmitter {
5851
5924
  generation: turn.generation,
5852
5925
  reason,
5853
5926
  recoveryStarted: true,
5927
+ message: formatReplyDropDetection(turn, reason),
5854
5928
  });
5855
- this.deliverReplyDropNotice("inst.reply_drop_retrying", "replydrop", "Reply-drop status", turn, reason, false);
5856
5929
  this.queueReplyRecoveryPrompt(turn);
5857
5930
  }
5858
5931
  reportReplyDropWithoutRetry(turn, reason) {
@@ -5862,8 +5935,8 @@ export class Daemon extends EventEmitter {
5862
5935
  generation: turn.generation,
5863
5936
  reason,
5864
5937
  recoveryStarted: false,
5938
+ message: formatReplyDropDetection(turn, reason),
5865
5939
  });
5866
- this.deliverReplyDropNotice("inst.reply_drop_unknown", "replydrop", "Unconfirmed reply status", turn, reason, true);
5867
5940
  }
5868
5941
  queueReplyRecoveryPrompt(turn) {
5869
5942
  const deliveryEpoch = this.deliveryEpoch;
@@ -5955,6 +6028,7 @@ export class Daemon extends EventEmitter {
5955
6028
  correlationId: turn.target.correlationId,
5956
6029
  generation: turn.generation,
5957
6030
  reason,
6031
+ message: formatReplyDropDetection(turn, reason),
5958
6032
  });
5959
6033
  this.deliverReplyDropNotice("inst.reply_drop_unrecovered", "replydropwarn", "Reply-drop recovery warning", turn, reason, true);
5960
6034
  }
@@ -5969,7 +6043,7 @@ export class Daemon extends EventEmitter {
5969
6043
  }
5970
6044
  this.lastReplyDropWarningAt = now;
5971
6045
  }
5972
- void this.deliverDaemonReply(t(key, formatReplyDropDetection(turn, reason)), kind, title, turn.target, true);
6046
+ void this.deliverDaemonReply(t(key), kind, title, turn.target, true);
5973
6047
  }
5974
6048
  /** Relay the pane's final text to the channel, marked as a daemon proxy reply. */
5975
6049
  async sendProxyReply(pane, target) {
@@ -6666,6 +6740,10 @@ export class Daemon extends EventEmitter {
6666
6740
  if (update.tool_progress === null || ["off", "standard", "verbose"].includes(String(update.tool_progress))) {
6667
6741
  this.updateToolProgress(update.tool_progress === null ? undefined : update.tool_progress);
6668
6742
  }
6743
+ if (typeof update.reply_triggers === "boolean")
6744
+ this.config.reply_triggers = update.reply_triggers;
6745
+ else if (update.reply_triggers === null)
6746
+ delete this.config.reply_triggers;
6669
6747
  if (typeof update.reply_completion_guard === "boolean") {
6670
6748
  this.config.reply_completion_guard = update.reply_completion_guard;
6671
6749
  }
@@ -6719,6 +6797,14 @@ export class Daemon extends EventEmitter {
6719
6797
  else if (update.board_cards_at_once === null)
6720
6798
  delete this.config.board_cards_at_once;
6721
6799
  // The fleet reads it at each tool call; kept current here for the next spawn's tool menu.
6800
+ if (typeof update.office_access === "boolean")
6801
+ this.config.office_access = update.office_access;
6802
+ else if (update.office_access === null)
6803
+ delete this.config.office_access;
6804
+ if (typeof update.office_pack_access === "boolean")
6805
+ this.config.office_pack_access = update.office_pack_access;
6806
+ else if (update.office_pack_access === null)
6807
+ delete this.config.office_pack_access;
6722
6808
  if (update.dashboard && typeof update.dashboard === "object" && !dashboardAccessProblem(update.dashboard))
6723
6809
  this.config.dashboard = update.dashboard;
6724
6810
  else if (update.dashboard === null)
@@ -7326,6 +7412,7 @@ export class Daemon extends EventEmitter {
7326
7412
  return false;
7327
7413
  let windowId = this.getWindowId();
7328
7414
  const supportsQueuedInput = this.backend?.supportsQueuedInput?.() === true;
7415
+ const composerQueue = !opts?.steer && this.composerTakesBusyInput() && this.composerCanShowPaste(formatted);
7329
7416
  let handingOffToNativeQueue = false;
7330
7417
  // If the CLI is busy, either hand the complete submission to its native input
7331
7418
  // queue or wait for idle. Native queue support is an explicit backend capability:
@@ -7342,11 +7429,13 @@ export class Daemon extends EventEmitter {
7342
7429
  // press Enter at once. A dialog painted a moment ago is still "busy" to
7343
7430
  // the silence gate, so the probe is mandatory here; "dialog"/"unknown"
7344
7431
  // fall through to the wait.
7345
- const canHandOff = (supportsQueuedInput || opts?.steer)
7432
+ // #2676: an ordinary message goes into a mode-switched composer (kiro TUI) mid-turn too, on a launch whose busy
7433
+ // composer AgEnD drives, by the steer path's own gates and proof — when its box can show it is this paste.
7434
+ const canHandOff = (supportsQueuedInput || opts?.steer || composerQueue)
7346
7435
  && readiness === "busy"
7347
7436
  && (await this.probeBlockingDialog()).state === "clear"
7348
7437
  && await this.hasPositiveDeliveryInput(true)
7349
- && (!opts?.steer || (this.launchTakesSteer() && await this.steerComposerAdmits(false)));
7438
+ && ((!opts?.steer && !composerQueue) || (this.launchTakesSteer() && await this.steerComposerAdmits(false, opts?.steer === true)));
7350
7439
  if (canHandOff) {
7351
7440
  // Native queue (codex), or an explicit /steer: hand the complete
7352
7441
  // paste+Enter transaction to the busy CLI now. For steer this is the
@@ -7354,7 +7443,9 @@ export class Daemon extends EventEmitter {
7354
7443
  // visibility check + idle-gated fallback catch TUIs that swallow
7355
7444
  // busy input (see steerMessage).
7356
7445
  handingOffToNativeQueue = true;
7357
- this.logger.debug(opts?.steer ? "CLI busy — steering into the running turn" : "CLI busy — handing message to backend-native input queue");
7446
+ this.logger.debug(opts?.steer ? "CLI busy — steering into the running turn"
7447
+ : composerQueue ? "CLI busy — typing message into the busy composer"
7448
+ : "CLI busy — handing message to backend-native input queue");
7358
7449
  }
7359
7450
  else {
7360
7451
  this.logger.debug("CLI busy — queuing message until idle");
@@ -7448,9 +7539,9 @@ export class Daemon extends EventEmitter {
7448
7539
  // #1405: a launch that no longer takes a steer is not the one the gate saw — redo from the top (bounded), where
7449
7540
  // the gate sends it down the idle path. The composer can also have changed mode since (the user's Ctrl+S), or
7450
7541
  // the turn ended.
7451
- if (handingOffToNativeQueue && opts?.steer && !this.launchTakesSteer())
7542
+ if (handingOffToNativeQueue && (opts?.steer || composerQueue) && !this.launchTakesSteer())
7452
7543
  return "spawn-started";
7453
- if (handingOffToNativeQueue && opts?.steer && !(await this.steerComposerAdmits(true)))
7544
+ if (handingOffToNativeQueue && (opts?.steer || composerQueue) && !(await this.steerComposerAdmits(true, opts?.steer === true)))
7454
7545
  return "dialog";
7455
7546
  // #829: a CLI that restores a cancelled prompt into its input box would
7456
7547
  // have this message pasted onto it and both submitted as one. Clear it
@@ -7948,9 +8039,11 @@ export class Daemon extends EventEmitter {
7948
8039
  && dialogs.some(other => other !== dialog && Daemon.dialogMatches(other, pane)))
7949
8040
  continue;
7950
8041
  this.trackDialogParked(dialog);
8042
+ this.trackRemoteCard(dialog, pane, probeMono);
7951
8043
  return { state: "dialog", dialog };
7952
8044
  }
7953
8045
  this.trackDialogParked(null);
8046
+ this.trackRemoteCard(null, pane, probeMono);
7954
8047
  return { state: "clear" };
7955
8048
  }
7956
8049
  /**
@@ -8191,6 +8284,77 @@ export class Daemon extends EventEmitter {
8191
8284
  this.logger.warn({ dialog: dialog.description, attempts: episode.ignored, holdsDeliveries }, "Runtime dialog is ignoring AgEnD's answer — reporting for a human");
8192
8285
  this.emit("dialog_answer_ignored", { name: this.name, description: dialog.description, attempts: episode.ignored, holdsDeliveries });
8193
8286
  }
8287
+ /**
8288
+ * #2675: a held card a person can answer from the chat (the dialog's remoteCard reader) is posted once it has stayed
8289
+ * the same card for REMOTE_CARD_STABLE_MS — `remote_card` with what it asks; the fleet posts it with its buttons. When
8290
+ * it closes or another card replaces it, `remote_card_gone` retires that post. Moving through the card (ours or the
8291
+ * person's in the pane) is the same card: its identity has no cursor. `readAt` is when the pane's capture was started
8292
+ * (performance.now): a read that started before the newest one used here finishes late and says nothing about now —
8293
+ * it may neither retire a newer card nor bring an older one back (#2677 review).
8294
+ */
8295
+ trackRemoteCard(dialog, pane, readAt) {
8296
+ if (readAt < this.remoteCardReadAt)
8297
+ return;
8298
+ this.remoteCardReadAt = readAt;
8299
+ const card = dialog?.remoteCard?.(pane) ?? null;
8300
+ const shown = this.remoteCardShown;
8301
+ if (shown && shown.identity !== card?.identity) {
8302
+ this.remoteCardShown = null;
8303
+ if (shown.posted)
8304
+ this.emit("remote_card_gone", { name: this.name, identity: shown.identity });
8305
+ }
8306
+ if (!card)
8307
+ return;
8308
+ if (!this.remoteCardShown) {
8309
+ this.remoteCardShown = { identity: card.identity, since: performance.now(), posted: false };
8310
+ return;
8311
+ }
8312
+ if (this.remoteCardShown.posted || performance.now() - this.remoteCardShown.since < REMOTE_CARD_STABLE_MS)
8313
+ return;
8314
+ this.remoteCardShown.posted = true;
8315
+ this.logger.info({ kind: card.kind }, "A card a person can answer from the chat is on screen — posting it");
8316
+ this.emit("remote_card", { name: this.name, card });
8317
+ }
8318
+ /** #2675: the card's post was used up without answering it (refused, stopped part-way): post it again once it holds still. */
8319
+ repostRemoteCard() {
8320
+ this.remoteCardShown = null;
8321
+ }
8322
+ /**
8323
+ * #2675: answer the card on screen from the chat, as the person chose: only while it is the card they saw
8324
+ * (`identity`), on a verified cursor, under the pane write lock (remote-card-answer.ts). Never logs the answer.
8325
+ */
8326
+ async answerRemoteCard(identity, action) {
8327
+ const tmux = this.tmux;
8328
+ const readers = this.runtimeDialogs().filter(dialog => dialog.remoteCard);
8329
+ if (!tmux || readers.length === 0)
8330
+ return { outcome: "stale" };
8331
+ if (this.remoteCardAnswering)
8332
+ return { outcome: "refused", reason: "another answer to this card is being keyed in" };
8333
+ this.remoteCardAnswering = true;
8334
+ try {
8335
+ const outcome = await answerRemoteCard({
8336
+ locked: fn => this.paneWriteLock.run(fn),
8337
+ // An unreadable screen (a failed capture, another spawn's pane) is unknown — never "the card closed".
8338
+ read: () => this.tmux === tmux ? tmux.capturePane().catch(() => null) : Promise.resolve(null),
8339
+ key: key => this.tmux !== tmux ? Promise.resolve(false) : key === "Space" ? tmux.sendKeys(" ") : tmux.sendSpecialKey(key),
8340
+ paste: text => this.tmux !== tmux ? Promise.resolve(false) : tmux.pasteBuffer(text),
8341
+ sleep: ms => new Promise(resolve => setTimeout(resolve, ms)),
8342
+ now: () => performance.now(),
8343
+ }, pane => {
8344
+ for (const dialog of readers) {
8345
+ const card = dialog.remoteCard(pane);
8346
+ if (card)
8347
+ return card;
8348
+ }
8349
+ return null;
8350
+ }, identity, action);
8351
+ this.logger.info({ action: action.kind, ...outcome }, "Answered a card from the chat");
8352
+ return outcome;
8353
+ }
8354
+ finally {
8355
+ this.remoteCardAnswering = false;
8356
+ }
8357
+ }
8194
8358
  /**
8195
8359
  * Remember how long a blocking dialog has been on screen. Once it has
8196
8360
  * outlived DIALOG_PARKED_NOTIFY_MS — the auto-dismiss did not take, or the
@@ -8294,21 +8458,48 @@ export class Daemon extends EventEmitter {
8294
8458
  * while a fresh capture reads its composer as "steer" — or "idle" once the turn has ended (`allowIdle`, the pre-write
8295
8459
  * re-check). "queue" (the user's choice), text in the box, or a screen it cannot read: not now — the caller waits for
8296
8460
  * the idle prompt as for any message. AgEnD never switches the mode. Backends without the reader are not asked.
8461
+ * #2676: an ordinary message (`steerOnly` false) goes in on "queue" too — kiro takes it into the same tray either way,
8462
+ * held for the turn's end instead of steered into it.
8297
8463
  */
8298
- async steerComposerAdmits(allowIdle) {
8464
+ async steerComposerAdmits(allowIdle, steerOnly = true) {
8299
8465
  const read = this.backend?.readSteerComposer;
8300
8466
  if (!read)
8301
8467
  return true;
8302
8468
  if (!this.tmux)
8303
8469
  return false;
8304
- let mode;
8470
+ let pane;
8305
8471
  try {
8306
- mode = read.call(this.backend, await this.tmux.capturePane());
8472
+ pane = await this.tmux.capturePane();
8307
8473
  }
8308
8474
  catch {
8309
8475
  return false;
8310
8476
  }
8311
- return mode === "steer" || (allowIdle && mode === "idle");
8477
+ return composerModeAdmits(read.call(this.backend, pane), allowIdle, steerOnly) && this.busyInputReachesTurn(pane);
8478
+ }
8479
+ /** #2682: a busy composer's typed input reaches the CLI's own turn (busyTurnTakesInput); an idle box, or no reader: yes. */
8480
+ busyInputReachesTurn(pane) {
8481
+ const mode = this.backend?.readSteerComposer?.(pane);
8482
+ if (mode !== "steer" && mode !== "queue")
8483
+ return true;
8484
+ return this.backend?.busyTurnTakesInput?.(pane) !== false;
8485
+ }
8486
+ /**
8487
+ * #2676: whether an ordinary message may be typed into this CLI's busy composer — a mode-switched one (kiro TUI) on a
8488
+ * launch whose busy composer AgEnD drives (the same evidence as a steer: KIRO_STEER_VERIFIED). The legacy UI, an
8489
+ * unlisted version and every backend without the reader answer false; claude and codex hand off by
8490
+ * supportsQueuedInput instead.
8491
+ */
8492
+ composerTakesBusyInput() {
8493
+ return !!this.backend?.readSteerComposer && this.backend.supportsSteer?.() === true;
8494
+ }
8495
+ /**
8496
+ * #2676: whether the box can show this payload as this delivery's paste before the Enter (steerPasteIsOurs): its
8497
+ * first line, or a collapsed paste's line count. A one-line payload kiro would collapse (over 500 characters) shows only
8498
+ * a character count, which kiro's own normalisation (tabs widened) makes no proof: it waits for the idle prompt.
8499
+ * kiro measures after widening each tab to its 2-column stop, so a tab is counted as 2 here (at most what it becomes).
8500
+ */
8501
+ composerCanShowPaste(formatted) {
8502
+ return /[\r\n]/.test(formatted) || formatted.replace(/\t/g, " ").length <= KIRO_COLLAPSE_CHARS;
8312
8503
  }
8313
8504
  /**
8314
8505
  * #1405: one look at a mode-switched composer for a steer's own proof: on the tmux of this write, inside `deadline`
@@ -8342,10 +8533,18 @@ export class Daemon extends EventEmitter {
8342
8533
  if (first === "")
8343
8534
  return false;
8344
8535
  const row = norm(shown);
8536
+ // #2676: a long paste is collapsed to a chip naming its line count (kiro TUI: more than 10 lines or 500 characters);
8537
+ // it is this paste when that count is exactly this payload's as kiro counts it. tmux pastes every LF as CR (no -r),
8538
+ // so a CRLF arrives as CR CR, and kiro turns each CR into a line break: every CR and every LF is one.
8539
+ const collapsed = this.backend?.readSteerComposerPasteLines?.(shown);
8540
+ if (collapsed != null)
8541
+ return collapsed > 1 && collapsed === formatted.split(/\r|\n/).length;
8345
8542
  // The marker line holds nothing but the marker: the row is exactly it.
8346
8543
  if (/^\[agend-(?:delivery|human)-id:[^\]\s]+\]$/.test(first))
8347
8544
  return row === first;
8348
- return row === first || row.startsWith(`${first} `);
8545
+ // #2676: a first line wider than the box is word-wrapped onto indented rows (live, 2.34.0): the row is its start, cut
8546
+ // at a word boundary.
8547
+ return row === first || row.startsWith(`${first} `) || first.startsWith(`${row} `);
8349
8548
  }
8350
8549
  /**
8351
8550
  * Codex can paint a prompt before the TTY enters raw mode. Both are required.
@@ -8492,6 +8691,18 @@ export class Daemon extends EventEmitter {
8492
8691
  if (this.needsStartupInputProof()) {
8493
8692
  return await this.hasPositiveDeliveryInput() ? "ready" : "transient";
8494
8693
  }
8694
+ // #2682: a backend that can read its own idle input box (kiro's TUI) must show it before an idle paste. Anything
8695
+ // else — another program owning the pane, a frame it cannot read, a capture that fails — is not one to type into.
8696
+ if (this.backend?.idleComposerReady && this.tmux) {
8697
+ try {
8698
+ if (!this.backend.idleComposerReady(await this.tmux.capturePane()))
8699
+ return "busy";
8700
+ }
8701
+ catch (err) {
8702
+ this.logger.debug({ err }, "capture-pane failed during the composer check — treating the pane as not ready");
8703
+ return "busy";
8704
+ }
8705
+ }
8495
8706
  if (this.backend?.dropsEnterWhileBusy?.() !== true)
8496
8707
  return "ready";
8497
8708
  const prompt = this.backend.getBottomReadyPattern?.();
@@ -9135,15 +9346,16 @@ export class Daemon extends EventEmitter {
9135
9346
  }
9136
9347
  // #1405: on that same last capture a mode-switched composer (kiro TUI) must still read "steer" — or "idle", the
9137
9348
  // turn over. "queue", text in the box or an unreadable row: not written; handled as the unread box above.
9138
- if (handingOffToNativeQueue && steer && this.backend?.readSteerComposer
9139
- && pasteBaseline?.steerComposer !== "steer" && pasteBaseline?.steerComposer !== "idle") {
9349
+ // #2676: an ordinary message typed into it (every other hand-off there) may also find "queue".
9350
+ if (handingOffToNativeQueue && this.backend?.readSteerComposer
9351
+ && (!composerModeAdmits(pasteBaseline?.steerComposer ?? null, true, steer) || pasteBaseline?.busyInputReachesTurn === false)) {
9140
9352
  if (!verdict.durableBeginCommitted)
9141
9353
  return "handoff-box-unread";
9142
- return this.failDelivery(verdict, status, "paste", "steer-composer-not-steering");
9354
+ return this.failDelivery(verdict, status, "paste", steer ? "steer-composer-not-steering" : "composer-not-empty");
9143
9355
  }
9144
9356
  // …and the launch must still take a steer (see launchTakesSteer). Not written: before the durable begin the row
9145
- // is retried, as a steer the gate will then send down the idle path.
9146
- if (handingOffToNativeQueue && steer && !this.launchTakesSteer()) {
9357
+ // is retried, as a steer the gate will then send down the idle path (#2676: so is a message typed into the box).
9358
+ if (handingOffToNativeQueue && (steer || this.backend?.readSteerComposer) && !this.launchTakesSteer()) {
9147
9359
  verdict.fenced = !verdict.durableBeginCommitted;
9148
9360
  return false;
9149
9361
  }
@@ -9273,7 +9485,8 @@ export class Daemon extends EventEmitter {
9273
9485
  // #1405: a steer into a mode-switched composer (kiro TUI) is proven by the box alone — before the Enter it must
9274
9486
  // positively hold typed text, and after it be the empty placeholder again. The empty placeholder here means the
9275
9487
  // paste never reached the box; an unreadable box (a failed or late capture, a dialog) proves nothing: no Enter.
9276
- const steerComposerProof = handingOffToNativeQueue && steer && !!this.backend?.readSteerComposer && tmuxAtWrite !== null;
9488
+ // #2676: an ordinary message typed into the busy box is proven the same way.
9489
+ const steerComposerProof = handingOffToNativeQueue && !!this.backend?.readSteerComposer && tmuxAtWrite !== null;
9277
9490
  let humanComposerOwned = false;
9278
9491
  let steerEnterDeadline = Infinity;
9279
9492
  if (steerComposerProof) {
@@ -9374,8 +9587,8 @@ export class Daemon extends EventEmitter {
9374
9587
  if (humanComposerOwned)
9375
9588
  signature.humanPositive?.(after === "steer" ? "composer-empty-steer"
9376
9589
  : after === "queue" ? "composer-empty-queue" : "composer-empty-idle");
9377
- verdict.phase = "steer-accepted";
9378
- verdict.proof = `steer-composer-${after}`;
9590
+ verdict.phase = steer ? "steer-accepted" : "composer-accepted";
9591
+ verdict.proof = `${steer ? "steer" : "handoff"}-composer-${after}`;
9379
9592
  if (status)
9380
9593
  this.emit("message_confirmed", status); // ✅ taken from the box
9381
9594
  return true;
@@ -10066,6 +10279,7 @@ export class Daemon extends EventEmitter {
10066
10279
  collapsedPastes: input?.collapsedPastes ?? 0,
10067
10280
  inputReadable: input != null,
10068
10281
  steerComposer: this.backend?.readSteerComposer?.(pane) ?? null,
10282
+ busyInputReachesTurn: this.busyInputReachesTurn(pane),
10069
10283
  };
10070
10284
  }
10071
10285
  /**
@@ -10561,6 +10775,12 @@ export class Daemon extends EventEmitter {
10561
10775
  reply(failed ? null : answer, failed);
10562
10776
  return;
10563
10777
  }
10778
+ if (tool === "office_pack_check" || tool === "office_pack_upload") {
10779
+ // #2692: the folder (or the manifest's files) is read here, on the agent's side; the fleet gets bytes in parts.
10780
+ const answer = await sendPackSources(args, { workspace: this.config.working_directory, configuredWorkspace: this.config.working_directory, stateDir: getAgendHome(), instance: this.name }, async (payload) => { const r = await ask(payload); return r.error ? { error: "fleet", message: r.error } : (r.result && typeof r.result === "object" ? r.result : {}); });
10781
+ reply(answer, undefined);
10782
+ return;
10783
+ }
10564
10784
  if (tool === "upload_web_effect") {
10565
10785
  const owner = { fence: this.launchFenceEpoch, spawn: this.spawnGeneration, boot: this.bootId, ipc: this.ipcServer };
10566
10786
  const current = () => !!owner.ipc && owner.ipc === this.ipcServer && owner.fence === this.launchFenceEpoch && owner.spawn === this.spawnGeneration && owner.boot === this.bootId && !this.deliveryWritesStopping && !this.startupAborted;
@@ -10821,7 +11041,7 @@ export class Daemon extends EventEmitter {
10821
11041
  fleetCwd: process.cwd(),
10822
11042
  onWarning: (warning) => {
10823
11043
  if (warning.problem === "fleet_dir_fallback") {
10824
- this.logger.warn({ field: warning.field, path: warning.path, legacyPath: warning.legacyPath }, `${warning.field} file: ref is not under the instance's working directory (${warning.path}); read ${warning.legacyPath} from the fleet directory instead — make the path absolute or move the file. The fallback is removed in 2.2.`);
11044
+ this.logger.warn({ field: warning.field, path: warning.path, legacyPath: warning.legacyPath }, `${warning.field} file: ref is not under the instance's working directory (${warning.path}); read ${warning.legacyPath} from the fleet directory instead — make the path absolute or move the file. A later release will remove this fallback.`);
10825
11045
  const key = `${warning.field}:${warning.path}`;
10826
11046
  if (!this.promptFileFallbackNoticed.has(key)) {
10827
11047
  this.promptFileFallbackNoticed.add(key);
@@ -10909,7 +11129,7 @@ export class Daemon extends EventEmitter {
10909
11129
  mcpEnv.AGEND_TOOL_SET = toolSet;
10910
11130
  // The dashboard features the person switched off: out of the tool menu from this start (the fleet refuses them
10911
11131
  // at once, whatever the menu still shows).
10912
- const dashboardOff = dashboardFeaturesOff(this.config.dashboard);
11132
+ const dashboardOff = dashboardFeaturesOff(dashboardAccessOf(this.config));
10913
11133
  if (dashboardOff.length)
10914
11134
  mcpEnv.AGEND_DASHBOARD_OFF = dashboardOff.join(",");
10915
11135
  if (this.config.display_name)
@@ -11331,6 +11551,23 @@ export class Daemon extends EventEmitter {
11331
11551
  paneText = await this.tmux?.capturePaneWithHistory(50);
11332
11552
  }
11333
11553
  catch { /* pane may be gone */ }
11554
+ if (paneText && this.backend.resumeBlockedPattern?.().test(paneText)) {
11555
+ // #2598: a duplicate id proves the conversation exists. The generic retry escape hatch must not
11556
+ // abandon that pin or turn a store-selection error into a fresh conversation, even after many retries.
11557
+ await this.killProcessTree("SIGTERM", "spawn: retained session is blocked");
11558
+ this.startupAdmission?.();
11559
+ await this.tmux.killWindow();
11560
+ this.startupAdmission?.();
11561
+ throw new ResumeBlockedError(t("inst.kiro_resume_ambiguous"), t("inst.kiro_resume_ambiguous_remedy"));
11562
+ }
11563
+ const retained = this.backend.retainedResumeBlock?.();
11564
+ if (retained) {
11565
+ await this.killProcessTree("SIGTERM", "spawn: carried session must be preserved");
11566
+ this.startupAdmission?.();
11567
+ await this.tmux.killWindow();
11568
+ this.startupAdmission?.();
11569
+ throw new ResumeBlockedError(retained.reason, retained.remedy);
11570
+ }
11334
11571
  const proven = this.paneSaysNoConversation(paneText);
11335
11572
  if (!proven) {
11336
11573
  const failures = this.bumpUnprovenResumeFailures();
@@ -11348,7 +11585,7 @@ export class Daemon extends EventEmitter {
11348
11585
  // out loud — this start does NOT continue the previous conversation.
11349
11586
  this.logger.error({ attempts: failures }, "Giving up on resuming after repeated startup failures — starting fresh. "
11350
11587
  + "The previous conversation is NOT continued; its context is lost to this session.");
11351
- this.emit("context_lost", this.name, "resume_repeatedly_failed");
11588
+ this.emit("context_lost", this.name, "resume_repeatedly_failed", failures);
11352
11589
  }
11353
11590
  this.setSessionAside(proven ? "cli_reported_no_conversation" : "resume_failed_repeatedly");
11354
11591
  this.skipResume = true;
@@ -11523,6 +11760,188 @@ export class Daemon extends EventEmitter {
11523
11760
  this.logger.warn({ err }, "The agent switch after resume failed — the shared entries stay; the next launch tries again");
11524
11761
  }
11525
11762
  }
11763
+ /**
11764
+ * M-2025: after a tui/V3 launch is ready, type `/session-id` and `/title` once
11765
+ * each, under the pane-write lock, with the same fences as {@link ensureBackendAgent}.
11766
+ * Does not touch the delivery epoch. A mismatch with the #2617 pin is surfaced;
11767
+ * the pin is never replaced by a file-store guess. Legacy backends return null
11768
+ * from sessionProof().
11769
+ *
11770
+ * #2683: these are AgEnD's own housekeeping, so unlike a delivery (which may join
11771
+ * a person's draft) each one goes only into a composer the final capture shows
11772
+ * positively EMPTY; Enter only once a capture shows exactly the command in the box;
11773
+ * and it counts as run only once the box is empty again — a command left in the
11774
+ * box stops here (it is taken back when the box shows exactly it), and nothing
11775
+ * else is typed after it. The id is taken only from kiro's `/session-id` alert
11776
+ * (readSessionIdAlert) on a capture after the command ran, and never when that
11777
+ * alert was already on the final capture before it was typed: an announcement
11778
+ * that was there before is not an answer to this request.
11779
+ */
11780
+ async ensureKiroSessionProof() {
11781
+ let proof = null;
11782
+ try {
11783
+ proof = this.backend?.sessionProof?.() ?? null;
11784
+ }
11785
+ catch {
11786
+ proof = null;
11787
+ }
11788
+ const tmux = this.tmux;
11789
+ if (!proof || !tmux || !this.backend)
11790
+ return;
11791
+ const windowId = tmux.getWindowId();
11792
+ const spawn = this.spawnGeneration, fence = this.launchFenceEpoch, phase = this.pauseWakeState;
11793
+ const fenced = () => !this.startupAborted && !this.deliveryWritesStopping && !this.fatalStartupBlocked
11794
+ && (phase === "active" || phase === "waking") && this.pauseWakeState === phase
11795
+ && spawn === this.spawnGeneration && fence === this.launchFenceEpoch && this.tmux === tmux;
11796
+ const deadline = performance.now() + Daemon.AGENT_SWITCH_BUDGET_MS;
11797
+ const live = () => { this.startupAdmission?.(); return fenced() && performance.now() < deadline; };
11798
+ if (!live())
11799
+ return;
11800
+ // A proof belongs to the launch that read it: an earlier launch's never stands for this one.
11801
+ try {
11802
+ clearKiroSessionProof(this.instanceDir);
11803
+ }
11804
+ catch { /* recorded again below only on a fresh match */ }
11805
+ const TIMEOUT = Symbol("deadline");
11806
+ const within = (work) => {
11807
+ const left = deadline - performance.now();
11808
+ if (left <= 0) {
11809
+ work.catch(() => { });
11810
+ return Promise.resolve(TIMEOUT);
11811
+ }
11812
+ let timer;
11813
+ return Promise.race([work, new Promise(r => { timer = setTimeout(() => r(TIMEOUT), left); })])
11814
+ .finally(() => clearTimeout(timer));
11815
+ };
11816
+ const capture = async () => {
11817
+ const pane = await within(tmux.capturePane().catch(() => null));
11818
+ return pane === TIMEOUT ? null : pane;
11819
+ };
11820
+ const pause = (ms) => within(new Promise(r => setTimeout(r, ms)));
11821
+ const op = Daemon.AGENT_SWITCH_TMUX_OP_MS;
11822
+ const box = async () => {
11823
+ const pane = await capture();
11824
+ return pane === null ? null : proof.readComposer(pane);
11825
+ };
11826
+ /**
11827
+ * Our command, and only it, is in the box: take it back — also past the deadline, on its own bounded capture.
11828
+ * Anything else there is left as it is.
11829
+ */
11830
+ const takeBack = async (command) => {
11831
+ let timer;
11832
+ const pane = await Promise.race([tmux.capturePane().catch(() => null), new Promise(r => { timer = setTimeout(() => r(null), op); })])
11833
+ .finally(() => clearTimeout(timer));
11834
+ if (pane !== null && fenced() && proof.readComposer(pane) === command)
11835
+ await tmux.deleteBackward(command.length, op);
11836
+ };
11837
+ /**
11838
+ * "ran": typed, Enter, and the box is empty again; `after` then read the pane with the capture from before the
11839
+ * paste, still under the lock. "stuck": typed but not run — nothing more may be typed. null: nothing typed.
11840
+ */
11841
+ const typeOnce = async (command, after) => {
11842
+ const wrote = await within(this.paneWriteLock.run(async () => {
11843
+ if (!live())
11844
+ return null;
11845
+ const readiness = await within(this.paneReadinessForDelivery(windowId));
11846
+ if (readiness !== "ready" || !live())
11847
+ return null;
11848
+ const before = await capture();
11849
+ if (before === null || !live())
11850
+ return null;
11851
+ if (this.inputTransientInPane(before) || !this.paneAuthoritativelyIdle(before))
11852
+ return null;
11853
+ if (proof.readComposer(before) !== "")
11854
+ return null; // a draft, or a box that cannot be read
11855
+ if (deadline - performance.now() < Daemon.AGENT_SWITCH_WRITE_RESERVE_MS)
11856
+ return null;
11857
+ if (!await tmux.pasteBuffer(command, { guard: live, timeoutMs: op }))
11858
+ return "stuck";
11859
+ let shown = null;
11860
+ for (let i = 0; i < 4 && live() && shown !== command; i++) {
11861
+ await pause(i === 0 ? Daemon.AGENT_SWITCH_PASTE_SETTLE_MS : Daemon.AGENT_SWITCH_POLL_MS);
11862
+ shown = await box();
11863
+ }
11864
+ if (shown !== command || !live()) {
11865
+ await takeBack(command);
11866
+ return "stuck";
11867
+ }
11868
+ if (!await tmux.sendSpecialKey("Enter", op)) {
11869
+ await takeBack(command);
11870
+ return "stuck";
11871
+ }
11872
+ let left = command;
11873
+ for (let i = 0; i < 6 && left !== ""; i++) {
11874
+ await pause(Daemon.AGENT_SWITCH_POLL_MS);
11875
+ if (!fenced())
11876
+ return "stuck";
11877
+ left = await box();
11878
+ }
11879
+ if (left !== "") {
11880
+ await takeBack(command);
11881
+ return "stuck";
11882
+ }
11883
+ return { ran: true, value: await after(before) };
11884
+ }));
11885
+ return wrote === TIMEOUT ? "stuck" : wrote;
11886
+ };
11887
+ try {
11888
+ const sid = await typeOnce(proof.sessionIdCommand, async (before) => {
11889
+ if (proof.readSessionIdAlert(before) !== null)
11890
+ return null; // already there: ours cannot be told apart
11891
+ for (let i = 0; i < 6 && live(); i++) {
11892
+ const pane = await capture();
11893
+ if (pane === null)
11894
+ break;
11895
+ const id = proof.readSessionIdAlert(pane);
11896
+ if (id !== null)
11897
+ return id;
11898
+ await pause(Daemon.AGENT_SWITCH_POLL_MS);
11899
+ }
11900
+ return null;
11901
+ });
11902
+ if (!live())
11903
+ return;
11904
+ if (sid === "stuck") {
11905
+ this.logger.warn("kiro /session-id did not run from an empty composer; /title is not typed after it");
11906
+ return;
11907
+ }
11908
+ // Unread/mismatch are about kiro's answer after we ran /session-id.
11909
+ // A composer that never read empty is not unread — we never asked.
11910
+ if (sid) {
11911
+ const verdict = proveKiroSessionId(proof.pinned, sid.value);
11912
+ if (verdict.kind === "mismatch") {
11913
+ const message = t("kiro.session_id_mismatch", verdict.pinned, verdict.observed);
11914
+ this.logger.warn({ pinned: verdict.pinned, observed: verdict.observed }, "kiro /session-id does not match the pinned conversation");
11915
+ this.emit("backend_launch_warning", { name: this.name, message });
11916
+ }
11917
+ else if (verdict.kind === "unread") {
11918
+ const message = t("kiro.session_id_unread");
11919
+ this.logger.warn("kiro /session-id did not answer on the pane; not guessing from files");
11920
+ this.emit("backend_launch_warning", { name: this.name, message });
11921
+ }
11922
+ else if (verdict.kind === "match" || verdict.kind === "fresh") {
11923
+ this.logger.info({ id: verdict.id, kind: verdict.kind }, "kiro /session-id on the pane");
11924
+ try {
11925
+ recordKiroSessionProof(this.instanceDir, { sessionId: verdict.id, kind: verdict.kind });
11926
+ }
11927
+ catch (err) {
11928
+ this.logger.warn({ err, id: verdict.id }, "kiro /session-id was proven on the pane but could not be recorded");
11929
+ }
11930
+ }
11931
+ }
11932
+ if (!live() || !proof.titleCommand)
11933
+ return;
11934
+ const titled = await typeOnce(proof.titleCommand, async () => true);
11935
+ if (titled && titled !== "stuck")
11936
+ this.logger.info({ title: proof.titleCommand }, "kiro /title sent for this instance");
11937
+ else if (titled === "stuck")
11938
+ this.logger.warn({ title: proof.titleCommand }, "kiro /title did not run from an empty composer");
11939
+ }
11940
+ catch (err) {
11941
+ this.startupAdmission?.();
11942
+ this.logger.warn({ err }, "kiro /session-id or /title after launch failed — the pin was not guessed from files");
11943
+ }
11944
+ }
11526
11945
  /**
11527
11946
  * Startup budget for this launch: the backend's override (resume-aware), never
11528
11947
  * below a user-configured startup_timeout_ms; undefined = trySpawn's default.
@@ -11685,6 +12104,9 @@ export class Daemon extends EventEmitter {
11685
12104
  if (ready)
11686
12105
  await this.ensureBackendAgent();
11687
12106
  this.startupAdmission?.();
12107
+ if (ready)
12108
+ await this.ensureKiroSessionProof();
12109
+ this.startupAdmission?.();
11688
12110
  return ready;
11689
12111
  }
11690
12112
  /**