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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (284) hide show
  1. package/THIRD_PARTY_NOTICES.md +37 -0
  2. package/dist/agend-home.d.ts +13 -0
  3. package/dist/agend-home.js +42 -0
  4. package/dist/agend-home.js.map +1 -1
  5. package/dist/agent-cli-instructions.md +1 -1
  6. package/dist/agent-credentials.d.ts +24 -0
  7. package/dist/agent-credentials.js +66 -0
  8. package/dist/agent-credentials.js.map +1 -0
  9. package/dist/agent-endpoint.d.ts +3 -1
  10. package/dist/agent-endpoint.js +13 -0
  11. package/dist/agent-endpoint.js.map +1 -1
  12. package/dist/auth-api.js +10 -2
  13. package/dist/auth-api.js.map +1 -1
  14. package/dist/authority/session-authorizations.d.ts +28 -4
  15. package/dist/authority/session-authorizations.js +63 -17
  16. package/dist/authority/session-authorizations.js.map +1 -1
  17. package/dist/backend/antigravity.js +3 -1
  18. package/dist/backend/antigravity.js.map +1 -1
  19. package/dist/backend/claude-code.d.ts +3 -0
  20. package/dist/backend/claude-code.js +89 -4
  21. package/dist/backend/claude-code.js.map +1 -1
  22. package/dist/backend/codex-footer-hints.d.ts +4 -0
  23. package/dist/backend/codex-footer-hints.js +27 -0
  24. package/dist/backend/codex-footer-hints.js.map +1 -0
  25. package/dist/backend/codex-session-lookup.d.ts +35 -0
  26. package/dist/backend/codex-session-lookup.js +93 -0
  27. package/dist/backend/codex-session-lookup.js.map +1 -1
  28. package/dist/backend/codex.d.ts +27 -0
  29. package/dist/backend/codex.js +124 -23
  30. package/dist/backend/codex.js.map +1 -1
  31. package/dist/backend/kiro-identity.d.ts +2 -0
  32. package/dist/backend/kiro-identity.js +13 -6
  33. package/dist/backend/kiro-identity.js.map +1 -1
  34. package/dist/backend/kiro.d.ts +3 -0
  35. package/dist/backend/kiro.js +68 -5
  36. package/dist/backend/kiro.js.map +1 -1
  37. package/dist/backend/muse.js +5 -3
  38. package/dist/backend/muse.js.map +1 -1
  39. package/dist/backend/types.d.ts +6 -1
  40. package/dist/backend/types.js +3 -0
  41. package/dist/backend/types.js.map +1 -1
  42. package/dist/backend-availability.d.ts +6 -0
  43. package/dist/backend-availability.js +13 -5
  44. package/dist/backend-availability.js.map +1 -1
  45. package/dist/cache-ledger.d.ts +85 -0
  46. package/dist/cache-ledger.js +236 -1
  47. package/dist/cache-ledger.js.map +1 -1
  48. package/dist/cache-service.d.ts +5 -0
  49. package/dist/cache-service.js +16 -4
  50. package/dist/cache-service.js.map +1 -1
  51. package/dist/channel/adapters/discord.js +27 -4
  52. package/dist/channel/adapters/discord.js.map +1 -1
  53. package/dist/channel/adapters/telegram.d.ts +51 -0
  54. package/dist/channel/adapters/telegram.js +238 -18
  55. package/dist/channel/adapters/telegram.js.map +1 -1
  56. package/dist/channel/inbox-temp-name.d.ts +9 -0
  57. package/dist/channel/inbox-temp-name.js +13 -0
  58. package/dist/channel/inbox-temp-name.js.map +1 -0
  59. package/dist/channel/mcp-tools.js +3 -3
  60. package/dist/channel/mcp-tools.js.map +1 -1
  61. package/dist/channel/types.d.ts +2 -0
  62. package/dist/chat-log-ids.d.ts +48 -0
  63. package/dist/chat-log-ids.js +182 -0
  64. package/dist/chat-log-ids.js.map +1 -0
  65. package/dist/classic-channel-manager.d.ts +8 -1
  66. package/dist/classic-channel-manager.js +24 -2
  67. package/dist/classic-channel-manager.js.map +1 -1
  68. package/dist/cli.js +4 -6
  69. package/dist/cli.js.map +1 -1
  70. package/dist/cross-instance-notice.d.ts +10 -0
  71. package/dist/cross-instance-notice.js +67 -5
  72. package/dist/cross-instance-notice.js.map +1 -1
  73. package/dist/daemon.d.ts +52 -0
  74. package/dist/daemon.js +530 -34
  75. package/dist/daemon.js.map +1 -1
  76. package/dist/delivery-consumed-watch.d.ts +23 -1
  77. package/dist/delivery-consumed-watch.js +30 -3
  78. package/dist/delivery-consumed-watch.js.map +1 -1
  79. package/dist/delivery-reconciliation.d.ts +7 -0
  80. package/dist/delivery-reconciliation.js +9 -1
  81. package/dist/delivery-reconciliation.js.map +1 -1
  82. package/dist/dialog-screen-text.d.ts +16 -0
  83. package/dist/dialog-screen-text.js +51 -0
  84. package/dist/dialog-screen-text.js.map +1 -0
  85. package/dist/export-import.d.ts +7 -0
  86. package/dist/export-import.js +44 -10
  87. package/dist/export-import.js.map +1 -1
  88. package/dist/fleet-context.d.ts +2 -0
  89. package/dist/fleet-manager.d.ts +147 -11
  90. package/dist/fleet-manager.js +747 -118
  91. package/dist/fleet-manager.js.map +1 -1
  92. package/dist/fleet-yaml-patch.d.ts +3 -0
  93. package/dist/fleet-yaml-patch.js +103 -0
  94. package/dist/fleet-yaml-patch.js.map +1 -0
  95. package/dist/general-knowledge/skills/concepts/SKILL.md +393 -0
  96. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +1 -0
  97. package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +3 -1
  98. package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +41 -4
  99. package/dist/general-knowledge/skills/model-discovery/SKILL.md +2 -0
  100. package/dist/general-knowledge/skills/web-appearance/SKILL.md +1 -1
  101. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +10 -4
  102. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +1 -1
  103. package/dist/install-stream.d.ts +53 -0
  104. package/dist/install-stream.js +134 -0
  105. package/dist/install-stream.js.map +1 -0
  106. package/dist/instance-lifecycle.d.ts +20 -2
  107. package/dist/instance-lifecycle.js +80 -12
  108. package/dist/instance-lifecycle.js.map +1 -1
  109. package/dist/instance-topic-delete.d.ts +77 -0
  110. package/dist/instance-topic-delete.js +123 -0
  111. package/dist/instance-topic-delete.js.map +1 -0
  112. package/dist/instructions.d.ts +1 -1
  113. package/dist/instructions.js +1 -1
  114. package/dist/instructions.js.map +1 -1
  115. package/dist/kiro-engine-status.d.ts +11 -0
  116. package/dist/kiro-engine-status.js +6 -0
  117. package/dist/kiro-engine-status.js.map +1 -1
  118. package/dist/kiro-resume-summary.d.ts +37 -0
  119. package/dist/kiro-resume-summary.js +85 -0
  120. package/dist/kiro-resume-summary.js.map +1 -0
  121. package/dist/locale.js +82 -6
  122. package/dist/locale.js.map +1 -1
  123. package/dist/log-service.d.ts +30 -0
  124. package/dist/log-service.js +36 -3
  125. package/dist/log-service.js.map +1 -1
  126. package/dist/log-steps.d.ts +4 -3
  127. package/dist/log-steps.js +32 -7
  128. package/dist/log-steps.js.map +1 -1
  129. package/dist/logger.d.ts +12 -0
  130. package/dist/logger.js +43 -1
  131. package/dist/logger.js.map +1 -1
  132. package/dist/login-controller.js +9 -1
  133. package/dist/login-controller.js.map +1 -1
  134. package/dist/login-flows.d.ts +2 -0
  135. package/dist/login-flows.js +5 -2
  136. package/dist/login-flows.js.map +1 -1
  137. package/dist/login-manager.d.ts +6 -0
  138. package/dist/login-manager.js +10 -1
  139. package/dist/login-manager.js.map +1 -1
  140. package/dist/missed-offline.d.ts +25 -0
  141. package/dist/missed-offline.js +79 -0
  142. package/dist/missed-offline.js.map +1 -0
  143. package/dist/needs-you-hub.d.ts +6 -1
  144. package/dist/needs-you-hub.js +31 -8
  145. package/dist/needs-you-hub.js.map +1 -1
  146. package/dist/needs-you.d.ts +22 -2
  147. package/dist/needs-you.js +17 -3
  148. package/dist/needs-you.js.map +1 -1
  149. package/dist/office-share-page.d.ts +1 -1
  150. package/dist/office-share-page.js +6 -2
  151. package/dist/office-share-page.js.map +1 -1
  152. package/dist/outbound-handlers.js +59 -3
  153. package/dist/outbound-handlers.js.map +1 -1
  154. package/dist/outbound-schemas.d.ts +1 -0
  155. package/dist/outbound-schemas.js +9 -3
  156. package/dist/outbound-schemas.js.map +1 -1
  157. package/dist/package-preimage.js +27 -2
  158. package/dist/package-preimage.js.map +1 -1
  159. package/dist/queued-handoffs.d.ts +33 -0
  160. package/dist/queued-handoffs.js +75 -0
  161. package/dist/queued-handoffs.js.map +1 -0
  162. package/dist/reply-buttons-controller.js +10 -0
  163. package/dist/reply-buttons-controller.js.map +1 -1
  164. package/dist/scheduler/db.d.ts +2 -1
  165. package/dist/scheduler/db.js +23 -9
  166. package/dist/scheduler/db.js.map +1 -1
  167. package/dist/selection-card.d.ts +2 -3
  168. package/dist/selection-card.js +99 -26
  169. package/dist/selection-card.js.map +1 -1
  170. package/dist/settings-api.d.ts +13 -0
  171. package/dist/settings-api.js +12 -1
  172. package/dist/settings-api.js.map +1 -1
  173. package/dist/stall-reporter.d.ts +21 -0
  174. package/dist/stall-reporter.js +82 -0
  175. package/dist/stall-reporter.js.map +1 -0
  176. package/dist/tmux-control.d.ts +2 -2
  177. package/dist/tmux-control.js +4 -2
  178. package/dist/tmux-control.js.map +1 -1
  179. package/dist/tmux-exec-helper.d.ts +6 -3
  180. package/dist/tmux-exec-helper.js +115 -53
  181. package/dist/tmux-exec-helper.js.map +1 -1
  182. package/dist/tmux-manager.d.ts +6 -0
  183. package/dist/tmux-manager.js +17 -0
  184. package/dist/tmux-manager.js.map +1 -1
  185. package/dist/tool-permissions.d.ts +5 -2
  186. package/dist/tool-permissions.js +6 -5
  187. package/dist/tool-permissions.js.map +1 -1
  188. package/dist/tool-progress.d.ts +2 -0
  189. package/dist/tool-progress.js +19 -3
  190. package/dist/tool-progress.js.map +1 -1
  191. package/dist/topic-commands.js +4 -0
  192. package/dist/topic-commands.js.map +1 -1
  193. package/dist/tunnel/cloudflared-install.d.ts +4 -0
  194. package/dist/tunnel/cloudflared-install.js +20 -1
  195. package/dist/tunnel/cloudflared-install.js.map +1 -1
  196. package/dist/tunnel/cloudflared.d.ts +8 -1
  197. package/dist/tunnel/cloudflared.js +34 -6
  198. package/dist/tunnel/cloudflared.js.map +1 -1
  199. package/dist/tunnel/lease.d.ts +3 -3
  200. package/dist/tunnel/lease.js +23 -19
  201. package/dist/tunnel/lease.js.map +1 -1
  202. package/dist/tunnel/manager.js +6 -7
  203. package/dist/tunnel/manager.js.map +1 -1
  204. package/dist/ui/chat-store.js +20 -3
  205. package/dist/ui/chat-strings.js +4 -4
  206. package/dist/ui/chat-thread.js +52 -10
  207. package/dist/ui/fleet-canvas.js +4 -3
  208. package/dist/ui/fleet-office-model.js +55 -22
  209. package/dist/ui/fleet-office-overview.js +6 -38
  210. package/dist/ui/fleet-office-pack.js +4 -3
  211. package/dist/ui/fleet-office-skin.js +114 -21
  212. package/dist/ui/fleet-office-store.js +5 -0
  213. package/dist/ui/fleet-office-zones.js +2 -2
  214. package/dist/ui/fleet-office.js +41 -9
  215. package/dist/ui/fleet-usage.js +90 -27
  216. package/dist/ui/panel-chat.js +2 -0
  217. package/dist/ui/panel-log.js +7 -1
  218. package/dist/ui/panel-needs.js +17 -8
  219. package/dist/ui/settings-strings.js +2 -2
  220. package/dist/ui/shared/app-i18n.js +4 -4
  221. package/dist/ui/shared/app-needs.js +1 -1
  222. package/dist/ui/shared/app-shell.js +4 -2
  223. package/dist/ui/shared/app-stream.js +26 -0
  224. package/dist/ui/shared/app.css +51 -3
  225. package/dist/ui/shared/office/guild.png +0 -0
  226. package/dist/ui/shared/office/school.png +0 -0
  227. package/dist/ui/shared/panel-view.js +339 -17
  228. package/dist/ui/shared/usage-dialog.js +18 -1
  229. package/dist/ui/shared/view-history.css +17 -0
  230. package/dist/ui/shared/view-history.js +438 -0
  231. package/dist/ui/shared/view-strings.js +42 -0
  232. package/dist/unattended-install.d.ts +6 -0
  233. package/dist/unattended-install.js +79 -0
  234. package/dist/unattended-install.js.map +1 -0
  235. package/dist/usage/capacity.d.ts +69 -0
  236. package/dist/usage/capacity.js +119 -0
  237. package/dist/usage/capacity.js.map +1 -0
  238. package/dist/usage/i18n-keys.d.ts +1 -1
  239. package/dist/usage/i18n-keys.js +3 -0
  240. package/dist/usage/i18n-keys.js.map +1 -1
  241. package/dist/usage/profile-timeline.d.ts +16 -0
  242. package/dist/usage/profile-timeline.js +58 -0
  243. package/dist/usage/profile-timeline.js.map +1 -0
  244. package/dist/usage/quota-history.d.ts +43 -0
  245. package/dist/usage/quota-history.js +168 -0
  246. package/dist/usage/quota-history.js.map +1 -0
  247. package/dist/usage/usage-api.d.ts +31 -0
  248. package/dist/usage/usage-api.js +80 -1
  249. package/dist/usage/usage-api.js.map +1 -1
  250. package/dist/usage-report.d.ts +25 -1
  251. package/dist/usage-report.js +55 -4
  252. package/dist/usage-report.js.map +1 -1
  253. package/dist/usage-rollup.d.ts +4 -1
  254. package/dist/usage-rollup.js +12 -3
  255. package/dist/usage-rollup.js.map +1 -1
  256. package/dist/view-api.d.ts +31 -0
  257. package/dist/view-api.js +186 -41
  258. package/dist/view-api.js.map +1 -1
  259. package/dist/view-history.d.ts +84 -0
  260. package/dist/view-history.js +116 -0
  261. package/dist/view-history.js.map +1 -0
  262. package/dist/web-api.d.ts +6 -0
  263. package/dist/web-api.js +15 -4
  264. package/dist/web-api.js.map +1 -1
  265. package/dist/web-chat-history.d.ts +20 -0
  266. package/dist/web-chat-history.js +21 -0
  267. package/dist/web-chat-history.js.map +1 -1
  268. package/dist/web-guest-access.js +5 -1
  269. package/dist/web-guest-access.js.map +1 -1
  270. package/dist/web-login.d.ts +4 -3
  271. package/dist/web-login.js +23 -14
  272. package/dist/web-login.js.map +1 -1
  273. package/dist/web-office-pack-tools.d.ts +1 -1
  274. package/dist/web-office-pack-tools.js +1 -1
  275. package/dist/web-office-pack-tools.js.map +1 -1
  276. package/dist/web-session-authorization.d.ts +13 -2
  277. package/dist/web-session-authorization.js +10 -3
  278. package/dist/web-session-authorization.js.map +1 -1
  279. package/dist/web-session.d.ts +5 -0
  280. package/dist/web-session.js +20 -8
  281. package/dist/web-session.js.map +1 -1
  282. package/launcher/agend-install.cjs +16 -2
  283. package/launcher/private-home.cjs +1 -1
  284. package/package.json +1 -1
package/dist/daemon.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { dialogScreenText } from "./dialog-screen-text.js";
1
2
  import { readActiveKiroModel } from "./backend/kiro.js";
2
3
  import { dashboardAccessOf } from "./dashboard-permissions.js";
3
4
  import { sendPackSources } from "./web-office-pack-read.js";
@@ -32,14 +33,14 @@ import { scopedFleetConfigEnvironment } from "./web-experimental.js";
32
33
  import { readAgentFile } from "./agent-file-read.js";
33
34
  import { ASSET_UPLOAD_MAX_BYTES, receivePreview, sendAssetInParts, writePreview } from "./web-asset-parts.js";
34
35
  import { LOGIN_FLOWS } from "./login-flows.js";
35
- import { ProgressAccumulator, summarizeProgress } from "./tool-progress.js";
36
+ import { ProgressAccumulator, isSearchProgram, summarizeProgress } from "./tool-progress.js";
36
37
  import { assembleSystemPrompt, resolveWorkflowText } from "./prompt-file-ref.js";
37
38
  import { isCrossInstanceVisibility } from "./cross-instance-notice.js";
38
39
  import { ContextGuardian } from "./context-guardian.js";
39
40
  import { IpcServer } from "./channel/ipc-bridge.js";
40
41
  import { daemonBudgetMs } from "./channel/ipc-timeouts.js";
41
42
  import { MessageBus } from "./channel/message-bus.js";
42
- import { shellQuote, UnsupportedCliError, ResumeBlockedError } from "./backend/types.js";
43
+ import { shellQuote, KiroCompatibilityError, UnsupportedCliError, ResumeBlockedError } from "./backend/types.js";
43
44
  import { getTmuxSession } from "./config.js";
44
45
  import { routeToolCall } from "./channel/tool-router.js";
45
46
  import { parseReplyButtons, replyButtonsFallbackText } from "./reply-buttons.js";
@@ -59,7 +60,8 @@ import { MuseUsageRelay, clearMuseUsageSnapshot } from "./muse-usage-relay.js";
59
60
  import { deliveryContentDigest } from "./delivery-outbox.js";
60
61
  import { consumedWatches } from "./delivery-consumed-watch.js";
61
62
  import { queueResumePolicyForAttempt } from "./delivery-queue-evidence.js";
62
- import { scanTranscriptForDeliveryMarker } from "./delivery-reconciliation.js";
63
+ import { markerConsumed, scanTranscriptForDeliveryMarker } from "./delivery-reconciliation.js";
64
+ import { QUEUED_HANDOFF_MAX_REDELIVERIES, QueuedHandoffStore } from "./queued-handoffs.js";
63
65
  import { measureSyncWork, noteSyncWork } from "./sync-work-attribution.js";
64
66
  import { TranscriptDeltaReader } from "./transcript-delta-reader.js";
65
67
  import { buildResumeContinuation, clearInFlightTurnMarker, consumeInFlightTurnMarker, decideTurnResume, normalizeResumeBackend, readResumeFingerprint, RESUME_BLOCKING_DELIVERY_STATES, writeInFlightTurnMarker, } from "./turn-resume.js";
@@ -150,6 +152,8 @@ export const HANDOFF_RECHECK_MS = 1_000;
150
152
  export const WAIT_STOP_POLL_MS = 250;
151
153
  /** #2770: human messages a cancel dropped are told in one notice, this long after the last of them. */
152
154
  export const CANCELLED_HUMANS_NOTICE_MS = 1_000;
155
+ /** #3030: a queued hand-off's watch outlives a long turn (the record outlives the watch in any case). */
156
+ export const QUEUED_HANDOFF_WATCH_LIFETIME_MS = 12 * 60 * 60_000;
153
157
  /**
154
158
  * An instance publishing more than this many execution-state edges inside the
155
159
  * window is flapping; its edges stop being logged one by one (#1116). One
@@ -342,7 +346,8 @@ export function shellCommandLabel(command) {
342
346
  if (!program)
343
347
  return "";
344
348
  const isBareWord = (t) => !!t && /^[A-Za-z][A-Za-z0-9_-]*$/.test(t);
345
- const label = isBareWord(tokens[1]) ? `${program} ${tokens[1]}` : program;
349
+ // A search program's next word is what it searches for, not a subcommand: `grep <token>` keeps only `grep`.
350
+ const label = isBareWord(tokens[1]) && !isSearchProgram(program) ? `${program} ${tokens[1]}` : program;
346
351
  return label.length > 40 ? `${label.slice(0, 39)}…` : label;
347
352
  }
348
353
  export class PaneStateMachine {
@@ -531,6 +536,9 @@ function countOccurrences(haystack, needle) {
531
536
  }
532
537
  /** Bottom-ready re-poll cadence for Enter-dropping TUIs once the pane is quiet. */
533
538
  const BOTTOM_READY_POLL_MS = 250;
539
+ /** M-2267: how often a pane left in a tmux mode is looked at again, and how long the delivery Enter waits for it to end. */
540
+ const PANE_MODE_POLL_MS = 500;
541
+ const PANE_MODE_ENTER_WAIT_MS = 10 * 60_000;
534
542
  /** Consecutive unreadable pane probes tolerated by the delivery gate before the delivery is failed (≈10s at the poll cadence). */
535
543
  const DIALOG_PROBE_UNKNOWN_MAX = 40;
536
544
  /** Startup dialog scan: poll cadence and wall-clock budget (keys and waits included). */
@@ -1576,6 +1584,12 @@ export class Daemon extends EventEmitter {
1576
1584
  pendingWhatsNew = null;
1577
1585
  pasteQueueDepth = 0;
1578
1586
  /** #2770: a person's messages a cancel dropped before they reached the CLI, told together (noteCancelledHuman). */
1587
+ /** #3030: people's messages a CLI never took (or that a restart left unverifiable), told once per batch. */
1588
+ unsettledHandoffs = { "not-taken": [], unverifiable: [] };
1589
+ unsettledHandoffsTimer = null;
1590
+ /** #3030: since when (monotonic) the CLI has been idle without a break; null while it is not. */
1591
+ instanceIdleSinceMono = null;
1592
+ queuedHandoffStore = null;
1579
1593
  cancelledHumans = [];
1580
1594
  cancelledHumansTimer = null;
1581
1595
  firstDeliveryDelay = new FirstDeliveryDelay();
@@ -1987,6 +2001,9 @@ export class Daemon extends EventEmitter {
1987
2001
  this.publishWorkingState("turn_closed");
1988
2002
  };
1989
2003
  }
2004
+ /** Register each issued HTTP credential with the same-process lifecycle owner. */
2005
+ agentCredentialIssuer;
2006
+ setAgentCredentialIssuer(issue) { this.agentCredentialIssuer = issue; }
1990
2007
  /** FleetManager and this daemon share a process, so durable begin/ACK is a direct synchronous store call. */
1991
2008
  setDeliveryOutboxPort(port) {
1992
2009
  this.deliveryOutbox = port;
@@ -2123,6 +2140,243 @@ export class Daemon extends EventEmitter {
2123
2140
  logger: this.logger,
2124
2141
  });
2125
2142
  }
2143
+ handoffStore() {
2144
+ return this.queuedHandoffStore ??= new QueuedHandoffStore(this.instanceDir);
2145
+ }
2146
+ /** #3030: the CLI that holds a queued hand-off; a record is settled only by a later one. */
2147
+ handoffHolder() { return `${this.bootId}:${this.spawnGeneration}`; }
2148
+ /**
2149
+ * #3030: before the paste. A hand-off into a busy Claude Code / Codex queue gets its record — the transcript
2150
+ * checkpoint before the paste, the unique leading marker and what a redelivery needs — or, for a redelivery written
2151
+ * as an ordinary idle submission, its record moves its checkpoint here, so a crash right after this paste is still
2152
+ * judged from the transcript that will show it. A write that did not keep a record runs as before.
2153
+ */
2154
+ async recordQueuedHandoff(formatted, handingOff, verdict, durableAttempt, humanVerificationId) {
2155
+ const track = verdict.trackHandoff;
2156
+ if (!track)
2157
+ return;
2158
+ if (!handingOff && !track.id) {
2159
+ // An earlier attempt of this delivery was a hand-off; this one is an idle submission.
2160
+ if (verdict.handoff)
2161
+ this.dropQueuedHandoff(verdict);
2162
+ return;
2163
+ }
2164
+ const backend = this.config.backend ?? this.backend?.binaryName ?? "";
2165
+ if (!["claude-code", "codex"].includes(backend))
2166
+ return;
2167
+ const marker = durableAttempt ? `[agend-delivery-id:${durableAttempt.deliveryId}]`
2168
+ : humanVerificationId ? `[agend-human-id:${humanVerificationId}]` : null;
2169
+ if (!marker || !formatted.startsWith(marker))
2170
+ return;
2171
+ let checkpoint = verdict.transcriptCheckpoint ?? null;
2172
+ if (!checkpoint) {
2173
+ try {
2174
+ checkpoint = await this.transcriptMonitor?.reconciliationCheckpoint() ?? null;
2175
+ }
2176
+ catch (err) {
2177
+ this.logger.debug({ err }, "Could not checkpoint the transcript before a queued hand-off");
2178
+ }
2179
+ }
2180
+ if (!checkpoint)
2181
+ return; // nothing could judge it later: it runs as before (✅ at the hand-off)
2182
+ const entry = {
2183
+ id: track.id ?? randomUUID(), marker, backend, path: checkpoint.path, offset: checkpoint.offset,
2184
+ content: track.content, meta: track.meta, holder: this.handoffHolder(), createdAt: Date.now(),
2185
+ redeliveries: track.redeliveries ?? 0,
2186
+ };
2187
+ try {
2188
+ const dropped = this.handoffStore().put(entry);
2189
+ if (handingOff)
2190
+ verdict.handoff = entry;
2191
+ for (const old of dropped)
2192
+ this.logger.warn({ handoff: old.id, createdAt: old.createdAt }, "Queued hand-off record dropped: too many on record");
2193
+ }
2194
+ catch (err) {
2195
+ this.logger.warn({ err }, "Could not record a queued hand-off; it runs as before (✅ at the hand-off)");
2196
+ }
2197
+ }
2198
+ /**
2199
+ * #3030: after pushChannelMessage's write. A hand-off on record whose paste went out is watched (✅ when a turn
2200
+ * takes it); one that was never written goes. A redelivery that ended as an ordinary submission is done; one that a
2201
+ * cancel dropped goes too (a cancel is the person's); one stopped before its write stays for the next CLI.
2202
+ */
2203
+ afterTrackedWrite(meta, verdict, sent, deliveryEpoch) {
2204
+ try {
2205
+ // A stale epoch with writes still allowed is a cancel (the person's); a stop fences writes too, and keeps records.
2206
+ const cancelled = !this.isDeliveryEpochCurrent(deliveryEpoch) && !this.deliveryWritesStopping;
2207
+ if (verdict.handoff) {
2208
+ if (sent || verdict.paneWriteStarted)
2209
+ this.watchQueuedHandoff(verdict.handoff);
2210
+ else if (!verdict.trackHandoff?.id || cancelled)
2211
+ this.dropQueuedHandoff(verdict);
2212
+ return;
2213
+ }
2214
+ const id = meta.handoff_redelivery;
2215
+ if (!id)
2216
+ return;
2217
+ if (sent || verdict.paneWriteStarted || cancelled) {
2218
+ this.handoffStore().remove(id);
2219
+ this.logger.info({ handoff: id, sent, proof: verdict.proof ?? null }, "Queued hand-off redelivered as an ordinary submission");
2220
+ }
2221
+ }
2222
+ catch (err) {
2223
+ this.logger.warn({ err }, "Could not update the queued hand-off record after its write");
2224
+ }
2225
+ }
2226
+ redeliveryStillOurs(meta) {
2227
+ const holder = this.handoffHolder();
2228
+ if (meta.handoff_holder !== holder)
2229
+ return false;
2230
+ try {
2231
+ return this.handoffStore().get(meta.handoff_redelivery)?.holder === holder;
2232
+ }
2233
+ catch {
2234
+ return false;
2235
+ }
2236
+ }
2237
+ dropQueuedHandoff(verdict) {
2238
+ const entry = verdict.handoff;
2239
+ verdict.handoff = undefined;
2240
+ if (!entry)
2241
+ return;
2242
+ try {
2243
+ this.handoffStore().remove(entry.id);
2244
+ }
2245
+ catch (err) {
2246
+ this.logger.warn({ err, handoff: entry.id }, "Could not remove a queued hand-off record");
2247
+ }
2248
+ }
2249
+ /**
2250
+ * #3030: the hand-off's CLI is the current one: watch its transcript for the turn that takes it (✅ then), and for an
2251
+ * idle CLI that never did (reported). A stop ends the watch; the record stays for the next CLI to settle.
2252
+ */
2253
+ watchQueuedHandoff(entry) {
2254
+ const spawn = this.spawnGeneration, fence = this.launchFenceEpoch, epoch = this.deliveryEpoch;
2255
+ const started = consumedWatches.start({
2256
+ deliveryId: `handoff:${entry.id}`,
2257
+ attemptNo: entry.redeliveries,
2258
+ backend: entry.backend,
2259
+ path: entry.path,
2260
+ offset: entry.offset,
2261
+ marker: entry.marker,
2262
+ // A cancel or stop moves the epoch: the watch ends quietly (a cancel cleared the record, a stop leaves it).
2263
+ current: () => !this.deliveryWritesStopping && spawn === this.spawnGeneration && fence === this.launchFenceEpoch
2264
+ && epoch === this.deliveryEpoch,
2265
+ consumed: (via) => this.settleQueuedHandoff(entry, "taken", `transcript:${via}`),
2266
+ idleForMs: () => this.instanceIdleSinceMono === null ? null : performance.now() - this.instanceIdleSinceMono,
2267
+ notTaken: () => this.settleQueuedHandoff(entry, "not-taken", "transcript:absent-while-idle"),
2268
+ // A queued message waits for the running turn, however long that is.
2269
+ lifetimeMs: QUEUED_HANDOFF_WATCH_LIFETIME_MS,
2270
+ logger: this.logger,
2271
+ });
2272
+ if (!started) {
2273
+ // No watch (the fleet's cap): ✅ as before; the record still lets a restart judge it.
2274
+ const status = channelStatus(entry.meta);
2275
+ if (status)
2276
+ this.emit("message_confirmed", status);
2277
+ }
2278
+ }
2279
+ /** #3030: a record's end — taken (✅), or never taken / not judgeable (❌, told once per batch). */
2280
+ settleQueuedHandoff(entry, outcome, proof) {
2281
+ try {
2282
+ this.handoffStore().remove(entry.id);
2283
+ }
2284
+ catch (err) {
2285
+ this.logger.warn({ err, handoff: entry.id }, "Could not remove a settled queued hand-off record");
2286
+ }
2287
+ const meta = entry.meta;
2288
+ const status = channelStatus(meta);
2289
+ if (outcome === "taken") {
2290
+ this.logger.info({ handoff: entry.id, proof, redeliveries: entry.redeliveries }, "Queued hand-off taken by the CLI");
2291
+ if (status)
2292
+ this.emit("message_confirmed", status);
2293
+ return;
2294
+ }
2295
+ this.logger.warn({ handoff: entry.id, outcome, proof, redeliveries: entry.redeliveries }, "Queued hand-off was not taken by the CLI");
2296
+ if (meta.from_instance) {
2297
+ this.reportCrossInstanceDeliveryFailure(meta, { reached: true, phase: "handoff-not-consumed", proof: outcome === "not-taken" ? "not-taken" : "unverifiable" });
2298
+ return;
2299
+ }
2300
+ if (!status)
2301
+ return;
2302
+ this.unsettledHandoffs[outcome].push({ chatId: status.chatId, messageId: status.messageId, ...(status.threadId ? { threadId: status.threadId } : {}) });
2303
+ if (this.unsettledHandoffsTimer)
2304
+ clearTimeout(this.unsettledHandoffsTimer);
2305
+ this.unsettledHandoffsTimer = setTimeout(() => {
2306
+ this.unsettledHandoffsTimer = null;
2307
+ for (const reason of ["not-taken", "unverifiable"]) {
2308
+ const messages = this.unsettledHandoffs[reason].splice(0);
2309
+ if (messages.length)
2310
+ this.emit("human_handoffs_unsettled", { reason, messages });
2311
+ }
2312
+ }, CANCELLED_HUMANS_NOTICE_MS);
2313
+ this.unsettledHandoffsTimer.unref?.();
2314
+ }
2315
+ /**
2316
+ * #3030: right after a CLI came up — the records left by CLIs that are gone. Their queue went with them, so each is
2317
+ * judged by its own transcript from its checkpoint: taken there (✅, nothing pasted); proven not taken (the whole
2318
+ * delta read, the marker absent or only enqueued) → redelivered once, with the same marker, through the ordinary
2319
+ * queue; anything that cannot be judged → never pasted, reported. Ownership is evidence: never pasted twice.
2320
+ */
2321
+ async reconcileQueuedHandoffs() {
2322
+ let entries;
2323
+ try {
2324
+ entries = this.handoffStore().list().filter(e => e.holder !== this.handoffHolder());
2325
+ }
2326
+ catch (err) {
2327
+ this.logger.warn({ err }, "Could not read the queued hand-off records");
2328
+ return;
2329
+ }
2330
+ if (!entries.length)
2331
+ return;
2332
+ // Captured before any await: a reconciliation that outlives its CLI can never claim the next CLI's holder.
2333
+ const holder = this.handoffHolder();
2334
+ const spawn = this.spawnGeneration, fence = this.launchFenceEpoch, epoch = this.deliveryEpoch;
2335
+ const fresh = () => spawn === this.spawnGeneration && fence === this.launchFenceEpoch && epoch === this.deliveryEpoch
2336
+ && !this.deliveryWritesStopping;
2337
+ let redelivered = 0, taken = 0, unverifiable = 0;
2338
+ for (const entry of entries) {
2339
+ const kind = await scanTranscriptForDeliveryMarker(entry.path, entry.offset, entry.backend, { marker: entry.marker });
2340
+ // This CLI is gone already, or a cancel came: the next CLI settles them (a cancel cleared them).
2341
+ if (!fresh())
2342
+ return;
2343
+ // …and the record is still the one that was read (another reconciliation or a cancel did not change it meanwhile).
2344
+ let current;
2345
+ try {
2346
+ current = this.handoffStore().get(entry.id);
2347
+ }
2348
+ catch {
2349
+ current = undefined;
2350
+ }
2351
+ if (!current || current.holder !== entry.holder || current.redeliveries !== entry.redeliveries)
2352
+ continue;
2353
+ if (markerConsumed(kind)) {
2354
+ taken++;
2355
+ this.settleQueuedHandoff(entry, "taken", `restart-transcript:${kind}`);
2356
+ continue;
2357
+ }
2358
+ if (kind === "unavailable" || entry.redeliveries >= QUEUED_HANDOFF_MAX_REDELIVERIES) {
2359
+ unverifiable++;
2360
+ this.settleQueuedHandoff(entry, "unverifiable", kind === "unavailable" ? "restart-transcript:unavailable" : "redelivery-limit");
2361
+ continue;
2362
+ }
2363
+ // Proven not taken: the CLI that queued it is gone and its transcript, read to the end, never took it.
2364
+ const next = { ...entry, holder, redeliveries: entry.redeliveries + 1 };
2365
+ try {
2366
+ this.handoffStore().put(next);
2367
+ }
2368
+ catch (err) {
2369
+ this.logger.warn({ err, handoff: entry.id }, "Could not update a queued hand-off record; not redelivered now");
2370
+ continue;
2371
+ }
2372
+ redelivered++;
2373
+ // The person's receipt was settled by the first write: the redelivery carries its marker, not its receipt binding.
2374
+ const { human_receipt_id: _r, human_owner_key: _o, human_daemon_owner: _d, human_wire_sha256: _w, ...meta } = entry.meta;
2375
+ this.pushChannelMessage(entry.content, { ...meta, handoff_redelivery: entry.id,
2376
+ handoff_redeliveries: String(next.redeliveries), handoff_holder: next.holder }, undefined, epoch);
2377
+ }
2378
+ this.logger.info({ instance: this.name, redelivered, taken, unverifiable, proof: "transcript" }, "Queued hand-offs left by the previous CLI settled");
2379
+ }
2126
2380
  finishDurableSubmission(delivery, verdict) {
2127
2381
  // Legacy backends may accept a pane write without enough pane evidence to
2128
2382
  // claim submission. Keep that row inspectable instead of recording a false
@@ -2631,6 +2885,8 @@ export class Daemon extends EventEmitter {
2631
2885
  this.startErrorMonitor();
2632
2886
  }
2633
2887
  this.startInstanceStateMonitor();
2888
+ // #3030: messages a gone CLI still had queued — judged by its transcript, redelivered once or reported.
2889
+ void this.reconcileQueuedHandoffs().catch(err => this.logger.warn({ err }, "Queued hand-off reconciliation failed"));
2634
2890
  this.logger.info(`${this.name} ready`);
2635
2891
  }
2636
2892
  /**
@@ -3105,8 +3361,6 @@ export class Daemon extends EventEmitter {
3105
3361
  scheduleNext();
3106
3362
  return;
3107
3363
  }
3108
- // The CLI owns the MCP server process, so the daemon can only observe it.
3109
- measureSyncWork("daemon.healthCheck", () => this.checkMcpServerAlive());
3110
3364
  // Human-readable backend label for logs (e.g. "claude", "kiro-cli")
3111
3365
  const cliLabel = this.backend?.binaryName ?? "CLI";
3112
3366
  const healthTmux = this.tmux;
@@ -3127,6 +3381,9 @@ export class Daemon extends EventEmitter {
3127
3381
  if (discardStaleHealth())
3128
3382
  return;
3129
3383
  if (paneStatus?.alive) {
3384
+ // A CLI shutdown also closes its MCP children. Revive tools only for
3385
+ // a positively live pane, never while its exit is being classified.
3386
+ measureSyncWork("daemon.healthCheck", () => this.checkMcpServerAlive());
3130
3387
  // Capture only a known primary while it lives. No extra tmux call: PID comes
3131
3388
  // from this same status read. Fast/unobserved deaths remain ordinary crashes.
3132
3389
  const key = this.oomSessionKey();
@@ -3173,6 +3430,29 @@ export class Daemon extends EventEmitter {
3173
3430
  return;
3174
3431
  }
3175
3432
  }
3433
+ // tmux marks the PTY dead when its fd closes, before SIGCHLD can fill
3434
+ // pane_dead_status. Claude /exit (and any other CLI) can hit this gap.
3435
+ // Give the status four bounded re-probes; missing is never exit zero.
3436
+ if (paneStatus && !paneStatus.alive && paneStatus.exitCode === undefined) {
3437
+ for (let attempt = 0; attempt < 4; attempt++) {
3438
+ await new Promise(resolve => setTimeout(resolve, 250));
3439
+ if (discardStaleHealth())
3440
+ return;
3441
+ const observed = await healthTmux.getPaneStatus();
3442
+ if (discardStaleHealth())
3443
+ return;
3444
+ if (observed)
3445
+ paneStatus = observed;
3446
+ if (paneStatus.alive || paneStatus.exitCode !== undefined)
3447
+ break;
3448
+ }
3449
+ if (paneStatus.alive) {
3450
+ this.windowQueryFailureTicks = 0;
3451
+ this.stormWindow?.noteWindowAlive(this.name);
3452
+ scheduleNext();
3453
+ return;
3454
+ }
3455
+ }
3176
3456
  // paneStatus === null → window gone entirely (e.g. tmux server crash)
3177
3457
  // paneStatus.alive === false → pane dead, exit code available
3178
3458
  const exitCode = paneStatus?.exitCode;
@@ -3356,6 +3636,8 @@ export class Daemon extends EventEmitter {
3356
3636
  try {
3357
3637
  await this.spawnClaudeWindow();
3358
3638
  this.setProcessStatus("running");
3639
+ // #3030: messages a gone CLI still had queued — judged by its transcript, redelivered once or reported.
3640
+ void this.reconcileQueuedHandoffs().catch(err => this.logger.warn({ err }, "Queued hand-off reconciliation failed"));
3359
3641
  this.logger.info("Recovered from background session conflict");
3360
3642
  this.emit("crash_respawn", this.name);
3361
3643
  }
@@ -3515,6 +3797,8 @@ export class Daemon extends EventEmitter {
3515
3797
  catch { /* may not exist */ }
3516
3798
  }
3517
3799
  this.setProcessStatus("running");
3800
+ // #3030: messages a gone CLI still had queued — judged by its transcript, redelivered once or reported.
3801
+ void this.reconcileQueuedHandoffs().catch(err => this.logger.warn({ err }, "Queued hand-off reconciliation failed"));
3518
3802
  this.logger.info({ resumed }, `Respawned ${cliLabel} window after crash`);
3519
3803
  this.emit("crash_respawn", this.name);
3520
3804
  // Emit the per-instance audit event while the storm is still active
@@ -3706,7 +3990,9 @@ export class Daemon extends EventEmitter {
3706
3990
  const interactivePrompt = this.interactivePromptDetector.observe(pane, Date.now(), this.instanceStateLastOutputAt);
3707
3991
  if (interactivePrompt) {
3708
3992
  this.logger.warn(interactivePrompt, "Interactive terminal prompt is waiting for human input");
3709
- this.emit("interactive_prompt", { name: this.name, ...interactivePrompt });
3993
+ // M-2269: the prompt as it was on screen, from this capture, for the Log.
3994
+ const screen = dialogScreenText(pane);
3995
+ this.emit("interactive_prompt", { name: this.name, ...interactivePrompt, ...(screen ? { screen } : {}) });
3710
3996
  // A prompt is not an error and must not enter the PTY recovery gate.
3711
3997
  // Continue scanning real errors in this same snapshot.
3712
3998
  }
@@ -3901,7 +4187,7 @@ export class Daemon extends EventEmitter {
3901
4187
  else if (dialog.verifyAfterKeys && !resolved) {
3902
4188
  this.logger.warn({ dialog: dialog.description }, "Safety dialog choice was sent but follow-up verification or notice submission failed");
3903
4189
  }
3904
- this.trackDialogParked(blockingSeen);
4190
+ this.trackDialogParked(blockingSeen, pane);
3905
4191
  return; // Dialog handled (or deliberately deferred): skip error checks this cycle
3906
4192
  }
3907
4193
  // Reached only when no dialog was answered this poll (none on screen, a fenced one, or one waiting out its
@@ -3911,7 +4197,7 @@ export class Daemon extends EventEmitter {
3911
4197
  this.dialogAnswers = null;
3912
4198
  this.retireParkedNotice("answer");
3913
4199
  }
3914
- this.trackDialogParked(blockingSeen);
4200
+ this.trackDialogParked(blockingSeen, pane);
3915
4201
  this.trackRemoteCard(blockingSeen, pane, captureMono);
3916
4202
  if (blockingSeen)
3917
4203
  return; // held dialog: skip error checks this cycle
@@ -4209,6 +4495,14 @@ export class Daemon extends EventEmitter {
4209
4495
  // (Completion paths clear via the guard's onComplete; a cancel with no
4210
4496
  // later idle edge would otherwise leave the marker behind.)
4211
4497
  clearInFlightTurnMarker(this.instanceDir);
4498
+ // #3030: what the person cancelled is not kept for a redelivery (nor reported as not taken).
4499
+ try {
4500
+ for (const entry of this.handoffStore().list())
4501
+ this.handoffStore().remove(entry.id);
4502
+ }
4503
+ catch (err) {
4504
+ this.logger.warn({ err }, "Could not clear queued hand-off records on cancel");
4505
+ }
4212
4506
  this.deliveryEpoch = fleetEpoch === undefined
4213
4507
  ? this.deliveryEpoch + 1
4214
4508
  : Math.max(this.deliveryEpoch, fleetEpoch);
@@ -5505,6 +5799,10 @@ export class Daemon extends EventEmitter {
5505
5799
  if (snapshot.state === "working" && this.recoveryBootSettled
5506
5800
  && this.workingRecoveryInputOwner !== JSON.stringify(this.interactionOwner()))
5507
5801
  this.recoveryNativeWork = true;
5802
+ if (snapshot.state !== "idle")
5803
+ this.instanceIdleSinceMono = null;
5804
+ else if (this.instanceState !== "idle" || this.instanceIdleSinceMono === null)
5805
+ this.instanceIdleSinceMono = performance.now();
5508
5806
  this.instanceState = snapshot.state;
5509
5807
  this.acceptedInstanceSnapshot = { ...snapshot };
5510
5808
  // Reuse an already-captured pane and the backend's pure parser, never capture for the journal.
@@ -5616,7 +5914,7 @@ export class Daemon extends EventEmitter {
5616
5914
  * message can arrive mid-turn and sit queued, and marking at arrival would let
5617
5915
  * the PREVIOUS turn's idle edge consume (and reset) the new turn's state.
5618
5916
  */
5619
- markTurnStarted(meta, deliveredText, deliveryEpoch = this.deliveryEpoch) {
5917
+ markTurnStarted(meta, deliveredText, deliveryEpoch = this.deliveryEpoch, queuedHandoff = false) {
5620
5918
  // Cancel may have interrupted an in-flight paste before its promise settled.
5621
5919
  // Its late success must not re-arm the turn that the user just cancelled.
5622
5920
  if (!this.isDeliveryEpochCurrent(deliveryEpoch))
@@ -5664,7 +5962,9 @@ export class Daemon extends EventEmitter {
5664
5962
  // during the continuation boots clean instead of chaining resumes.
5665
5963
  // #2544: a reply that settled before this arm already answered the person —
5666
5964
  // writing a marker would re-inject a continuation for a finished turn.
5667
- if (!meta.resumedContinuationOf && !armed.completionDelivered) {
5965
+ // #3030: a message only queued in the busy CLI has not started a turn: the interrupted turn is still the running one,
5966
+ // and a queued message lost to a restart is the hand-off record's to redeliver, not a continuation's.
5967
+ if (!meta.resumedContinuationOf && !armed.completionDelivered && !queuedHandoff) {
5668
5968
  const resumeBackend = this.config.backend ?? this.backend?.binaryName ?? this.runtimeIdentity?.backend;
5669
5969
  const resumeCwd = this.config.working_directory ?? "";
5670
5970
  writeInFlightTurnMarker(this.instanceDir, {
@@ -7069,10 +7369,12 @@ export class Daemon extends EventEmitter {
7069
7369
  // Command name only — never the arguments. See shellCommandLabel.
7070
7370
  if (name === "Bash")
7071
7371
  return `$ ${shellCommandLabel(String(inp.command ?? ""))}`;
7372
+ // The tool name only — never the search pattern: it can be a literal secret someone is looking for, and this label
7373
+ // reaches the channel's progress line.
7072
7374
  if (name === "Glob")
7073
- return `Glob ${inp.pattern ?? ""}`;
7375
+ return "Glob";
7074
7376
  if (name === "Grep")
7075
- return `Grep ${inp.pattern ?? ""}`;
7377
+ return "Grep";
7076
7378
  if (name === "Agent")
7077
7379
  return "Agent (subagent)";
7078
7380
  if (name.startsWith("mcp__agend__"))
@@ -7443,6 +7745,13 @@ export class Daemon extends EventEmitter {
7443
7745
  if (!this.isDeliveryEpochCurrent(deliveryEpoch)) {
7444
7746
  this.logger.info("Pending channel delivery dropped by user cancel");
7445
7747
  this.deferDurableDelivery(meta, "delivery epoch changed while queued");
7748
+ // #3030: a cancel is the person's — a queued redelivery it dropped is not redelivered later.
7749
+ if (meta.handoff_redelivery && !this.deliveryWritesStopping) {
7750
+ try {
7751
+ this.handoffStore().remove(meta.handoff_redelivery);
7752
+ }
7753
+ catch { /* the next CLI judges it */ }
7754
+ }
7446
7755
  return;
7447
7756
  }
7448
7757
  if (this.config.pre_task_command) {
@@ -7453,13 +7762,26 @@ export class Daemon extends EventEmitter {
7453
7762
  // #2800: with this real message, in the same paste — never a paste or turn of its own: after its delivery marker
7454
7763
  // (transcript proof anchors on it), before the message. A person's message with a verified receipt is pasted
7455
7764
  // byte for byte (its proof is the wire's hash), so the notice waits for the next delivery without one.
7456
- const whatsNew = this.pendingWhatsNew && !meta.human_receipt_id ? this.currentWhatsNew(this.pendingWhatsNew) : null;
7765
+ const whatsNew = this.pendingWhatsNew && !meta.human_receipt_id && !meta.handoff_redelivery
7766
+ ? this.currentWhatsNew(this.pendingWhatsNew) : null;
7457
7767
  const pasted = whatsNew ? withWhatsNew(formatted, whatsNew.notice) : formatted;
7458
7768
  const status = channelStatus(meta);
7459
7769
  // A fresh delivery begins a fresh turn — its bubble must not inherit
7460
7770
  // the previous turn's tool list.
7461
7771
  this.resetToolProgress();
7462
7772
  verdict = { reached: false, humanReceipt: this.humanReceiptAttempt(meta, formatted) };
7773
+ // #3030: a hand-off into the CLI's own queue stays on record until a turn takes it (redeliveries included).
7774
+ if (!content.startsWith("/raw ") || meta.from_instance) {
7775
+ const { handoff_redelivery: redeliveryOf, handoff_redeliveries: redeliveries, handoff_holder: _holder, ...kept } = meta;
7776
+ verdict.trackHandoff = { content, meta: kept,
7777
+ ...(redeliveryOf ? { id: redeliveryOf, redeliveries: Number(redeliveries) || 0 } : {}) };
7778
+ }
7779
+ // #3030: a redelivery is written only by the CLI it was issued for, and only while its record still says so —
7780
+ // one queued for a CLI that was replaced meanwhile is that next CLI's reconciliation's to judge again.
7781
+ if (meta.handoff_redelivery && !this.redeliveryStillOurs(meta)) {
7782
+ this.logger.info({ handoff: meta.handoff_redelivery }, "Queued hand-off redelivery dropped: its CLI was replaced before the write");
7783
+ return;
7784
+ }
7463
7785
  const durableAttempt = this.durableDeliveryAttempt(meta, content);
7464
7786
  if (durableAttempt === false) {
7465
7787
  this.deferDurableDelivery(meta, "invalid durable delivery attempt metadata");
@@ -7482,10 +7804,11 @@ export class Daemon extends EventEmitter {
7482
7804
  if (whatsNew && (sent || verdict.paneWriteStarted))
7483
7805
  this.recordWhatsNewSent(whatsNew);
7484
7806
  }
7807
+ this.afterTrackedWrite(meta, verdict, !!sent, deliveryEpoch);
7485
7808
  if (sent) {
7486
7809
  delivered = true;
7487
7810
  this.finishDurableSubmission(durable, verdict);
7488
- this.markTurnStarted(meta, formatted, deliveryEpoch);
7811
+ this.markTurnStarted(meta, formatted, deliveryEpoch, !!verdict.handoff);
7489
7812
  }
7490
7813
  else if (durableAttempt && verdict.paneWriteStarted) {
7491
7814
  this.finishDurableDelivery(durable, "uncertain", `${verdict.phase ?? "post-submit"}:${verdict.proof ?? "unproven"}`, verdict);
@@ -7768,6 +8091,9 @@ export class Daemon extends EventEmitter {
7768
8091
  // The hand-off's last capture had no box: nothing was begun or written — wait for readiness, then try again.
7769
8092
  if (written === "handoff-box-unread")
7770
8093
  return "dialog";
8094
+ // M-2267: the pane is in a tmux mode: nothing written — wait for readiness (it holds while the mode lasts), retry.
8095
+ if (written === "pane-in-mode")
8096
+ return "dialog";
7771
8097
  // Fenced before its first write: not attempted — redo after a spawn, else drop without a ❌.
7772
8098
  return written === false && verdict.fenced ? stale() : written;
7773
8099
  });
@@ -8241,7 +8567,7 @@ export class Daemon extends EventEmitter {
8241
8567
  && this.hasAutoDialogClaim(dialog)
8242
8568
  && dialogs.some(other => other !== dialog && Daemon.dialogMatches(other, pane)))
8243
8569
  continue;
8244
- this.trackDialogParked(dialog);
8570
+ this.trackDialogParked(dialog, pane);
8245
8571
  this.trackRemoteCard(dialog, pane, probeMono);
8246
8572
  return { state: "dialog", dialog };
8247
8573
  }
@@ -8620,7 +8946,8 @@ export class Daemon extends EventEmitter {
8620
8946
  this.parkedNoticeOut = null;
8621
8947
  this.emit("dialog_unparked", { name: this.name });
8622
8948
  }
8623
- trackDialogParked(dialog) {
8949
+ /** `pane`: the capture the caller already holds — M-2269 keeps the dialog's text from it for the Log (no new capture). */
8950
+ trackDialogParked(dialog, pane = "") {
8624
8951
  if (!dialog) {
8625
8952
  if (this.dialogParkedSince !== 0)
8626
8953
  this.logger.info({ dialog: this.dialogParkedKey }, "Blocking dialog is gone from the pane");
@@ -8658,7 +8985,9 @@ export class Daemon extends EventEmitter {
8658
8985
  this.dialogParkedReported = true;
8659
8986
  this.parkedNoticeOut = "parked";
8660
8987
  this.logger.warn({ dialog: dialog.description, parkedForMs: Date.now() - this.dialogParkedSince }, "CLI dialog is still on screen — not auto-answering, reporting for a human");
8988
+ const screen = dialogScreenText(pane);
8661
8989
  this.emit("dialog_parked", { name: this.name, description: dialog.description, holdOnly: dialog.holdOnly === true,
8990
+ ...(screen ? { screen } : {}),
8662
8991
  ...(dialog.parkedAdvice ? { advice: dialog.parkedAdvice } : {}),
8663
8992
  ...(episode !== null && interaction.kind ? { kind: interaction.kind, episode, backend: this.config.backend ?? "claude-code" } : {}),
8664
8993
  ...(unrecognised ? { unrecognised: true, question: this.unrecognisedPrompt?.shape.question ?? null } : {}) });
@@ -9018,6 +9347,14 @@ export class Daemon extends EventEmitter {
9018
9347
  : await this.waitForPaneIdleForDelivery(windowId, remaining);
9019
9348
  if (idle !== true)
9020
9349
  return idle;
9350
+ // M-2267: a person left the pane in a tmux mode (copy-mode): its keys would go to the mode. Hold — never take the
9351
+ // pane out of it — and look again; the delivery goes once the mode ends (or this wait's own deadline).
9352
+ if (await this.paneInTmuxMode("readiness")) {
9353
+ if (stop && await stop())
9354
+ return "stopped";
9355
+ await new Promise(r => setTimeout(r, BOTTOM_READY_POLL_MS));
9356
+ continue;
9357
+ }
9021
9358
  // The idle wait just resolved, so re-reading the silence gate would only
9022
9359
  // add a stale-cache race; ask the pane the remaining questions directly.
9023
9360
  const transient = await this.probeInputTransient();
@@ -9563,17 +9900,119 @@ export class Daemon extends EventEmitter {
9563
9900
  * precisely so the lock's scope is visible at the call site rather than being
9564
9901
  * an invariant maintained by comments.
9565
9902
  */
9566
- async sendDeliveryEnter(phase, stillCurrent, durableAttempt) {
9903
+ async sendDeliveryEnter(phase, stillCurrent, durableAttempt, onInputHeld) {
9567
9904
  if (!(await this.waitForInputTransientToClear(phase)))
9568
9905
  return false;
9569
- return this.sendDeliveryEnterAfterAvailability(phase, stillCurrent, durableAttempt);
9906
+ return this.sendDeliveryEnterAfterAvailability(phase, stillCurrent, durableAttempt, onInputHeld);
9907
+ }
9908
+ /**
9909
+ * M-2267 (hmsg-b8a170c0): is the pane in a tmux mode (a person left it in copy-mode)? Logged once per stretch. Unknown
9910
+ * (unreadable) is not a mode: the delivery's own reads decide then, as before.
9911
+ */
9912
+ paneModeSince = 0;
9913
+ async paneInTmuxMode(phase) {
9914
+ const inMode = await this.tmux?.paneInMode?.();
9915
+ if (inMode !== true) {
9916
+ this.paneModeSince = 0;
9917
+ return false;
9918
+ }
9919
+ if (!this.paneModeSince) {
9920
+ this.paneModeSince = performance.now();
9921
+ this.logger.warn({ phase }, "The pane is in a tmux mode (copy-mode) — delivery waits for it to end; no key is sent to it");
9922
+ }
9923
+ return true;
9924
+ }
9925
+ /** M-2267: wait while the pane is in a tmux mode, until `deadline` (PANE_MODE_ENTER_WAIT_MS) — "clear", "stale" (fenced), "held". */
9926
+ async waitWhilePaneInMode(phase, current, deadline = performance.now() + PANE_MODE_ENTER_WAIT_MS) {
9927
+ for (;;) {
9928
+ const inMode = await this.paneInTmuxMode(phase);
9929
+ if (!current())
9930
+ return "stale";
9931
+ if (!inMode)
9932
+ return "clear";
9933
+ if (performance.now() >= deadline)
9934
+ return "held";
9935
+ await new Promise(r => setTimeout(r, PANE_MODE_POLL_MS));
9936
+ if (!current())
9937
+ return "stale";
9938
+ }
9570
9939
  }
9571
9940
  /** Write half only: callers must finish the availability wait before any final recovery ownership proof. */
9572
- async sendDeliveryEnterAfterAvailability(phase, stillCurrent, durableAttempt) {
9573
- // A recovery Enter may have waited for a transient while cancel or spawn
9574
- // replaced the delivery. Check at the last point before the tmux write.
9575
- if (this.deliveryWritesStopping || (stillCurrent && !stillCurrent()))
9941
+ async sendDeliveryEnterAfterAvailability(phase, stillCurrent, durableAttempt, onInputHeld) {
9942
+ // A recovery Enter may have waited for a transient while cancel or spawn replaced the delivery: check here, and
9943
+ // again after every wait below, at the last point before the tmux write.
9944
+ // M-2290 (#2985 review, Fable): the mode wait can last up to 10 minutes, and a respawn or relaunch reuses the same
9945
+ // tmux object and window — so the spawn generation and launch fence are part of the fence too: an Enter meant for
9946
+ // the CLI this paste went to is never sent to its successor.
9947
+ const tmux = this.tmux;
9948
+ const generation = this.spawnGeneration;
9949
+ const fence = this.launchFenceEpoch;
9950
+ const current = () => this.tmux === tmux && this.spawnGeneration === generation && this.launchFenceEpoch === fence
9951
+ && !this.deliveryWritesStopping && (!stillCurrent || stillCurrent());
9952
+ if (!current())
9576
9953
  return false;
9954
+ // The pasted text may still be in the box: the caller keeps it uncertain (no false failure, no second paste).
9955
+ const hold = () => {
9956
+ onInputHeld?.("pane-in-mode");
9957
+ this.logger.warn({ phase }, "The pane stayed in a tmux mode; Enter withheld");
9958
+ return false;
9959
+ };
9960
+ // M-2267 (#2971): a pane in a tmux mode (a person left it in copy-mode) would take this Enter. Wait for the mode to
9961
+ // end, then look once more within the same deadline — a pane flickering back into a mode is waited out again, never
9962
+ // past the bound. (beta.5: #2958's composer proof that used to sit between the two looks is reverted, decision
9963
+ // f57474aa: it rejected Codex's footer after a resume and while busy. #2957 is open again.)
9964
+ const modeDeadline = performance.now() + PANE_MODE_ENTER_WAIT_MS;
9965
+ for (;;) {
9966
+ const mode = await this.waitWhilePaneInMode(phase, current, modeDeadline);
9967
+ if (mode === "stale")
9968
+ return false;
9969
+ if (mode === "held")
9970
+ return hold();
9971
+ if (await this.paneInTmuxMode(phase)) {
9972
+ if (!current())
9973
+ return false;
9974
+ if (performance.now() >= modeDeadline)
9975
+ return hold();
9976
+ await new Promise(r => setTimeout(r, PANE_MODE_POLL_MS));
9977
+ if (!current())
9978
+ return false;
9979
+ continue;
9980
+ }
9981
+ if (!current())
9982
+ return false;
9983
+ break;
9984
+ }
9985
+ // M-2290 (the simple half of the #2957 redesign, leader xmsg-1793712561468): a dialog the backend RECOGNISES
9986
+ // (runtimeDialogs: a permission card, the dangerous-command card, …) that painted between the paste and this Enter
9987
+ // would take the key as its answer. Read the pane once more: such a dialog withholds the Enter, the caller keeps the
9988
+ // delivery uncertain (Needs-you), and the dialog scanner parks the dialog for a person. Nothing else is judged here
9989
+ // — an unknown or unreadable screen sends, exactly as before: #2958's composer proof (decision f57474aa) is NOT back.
9990
+ // Only a caller that can hold (onInputHeld: the delivery's initial submit, settled uncertain → Needs-you) asks: one
9991
+ // that cannot would read `false` as a failure and paste again. Only the backend's OWN dialogs count — not AgEnD's
9992
+ // unrecognised-prompt guess — so an unknown screen still sends.
9993
+ if (onInputHeld) {
9994
+ let finalPane = null;
9995
+ try {
9996
+ finalPane = await tmux.capturePane();
9997
+ }
9998
+ catch {
9999
+ finalPane = null;
10000
+ }
10001
+ if (!current())
10002
+ return false;
10003
+ let dialog;
10004
+ try {
10005
+ dialog = finalPane === null ? undefined : (this.backend?.getRuntimeDialogs?.() ?? []).find(d => Daemon.dialogMatches(d, finalPane))?.description;
10006
+ }
10007
+ catch {
10008
+ dialog = undefined;
10009
+ }
10010
+ if (dialog !== undefined) {
10011
+ onInputHeld?.("dialog");
10012
+ this.logger.warn({ phase, dialog }, "A dialog is on the pane; the delivery's Enter is withheld (it would answer it)");
10013
+ return false;
10014
+ }
10015
+ }
9577
10016
  // W1: persist the "Enter may have happened" boundary before tmux receives
9578
10017
  // the key. A crash after this commit is never auto-replayed without a
9579
10018
  // positive transcript marker, even if the stale pane still shows composer.
@@ -9581,7 +10020,7 @@ export class Daemon extends EventEmitter {
9581
10020
  this.logger.error({ phase, deliveryId: durableAttempt.deliveryId }, "Could not commit durable Enter write-ahead fence");
9582
10021
  return false;
9583
10022
  }
9584
- const sent = await this.tmux.sendSpecialKey("Enter");
10023
+ const sent = await tmux.sendSpecialKey("Enter");
9585
10024
  if (!sent) {
9586
10025
  this.logger.error({
9587
10026
  phase,
@@ -9641,6 +10080,10 @@ export class Daemon extends EventEmitter {
9641
10080
  verdict.fenced = !verdict.durableBeginCommitted;
9642
10081
  return false;
9643
10082
  }
10083
+ // M-2267: the pane went into a tmux mode after the readiness wait. Nothing begun or written: the caller waits for
10084
+ // readiness (which holds while it lasts) and tries again — as for a dialog that appeared before the write.
10085
+ if (!verdict.durableBeginCommitted && await this.paneInTmuxMode("paste"))
10086
+ return "pane-in-mode";
9644
10087
  // A hand-off pastes and presses Enter into a busy pane: the box must still be readable on this, the last capture
9645
10088
  // before the write (#1169 review) — a modal that appeared after the under-lock check would take that Enter. Before
9646
10089
  // the durable begin the caller waits for readiness outside the lock and tries again; after it (a paste retry),
@@ -9704,6 +10147,17 @@ export class Daemon extends EventEmitter {
9704
10147
  }
9705
10148
  }
9706
10149
  }
10150
+ // #3030: a hand-off into a busy CLI's own queue is on record before its paste (write-ahead): that queue lives only
10151
+ // in the CLI process, and a stop before a turn takes it would lose it without a trace.
10152
+ if (verdict.trackHandoff && !rawPaste) {
10153
+ await this.recordQueuedHandoff(formatted, handingOffToNativeQueue, verdict, durableAttempt, humanVerificationId);
10154
+ if (stillCurrent && !stillCurrent()) {
10155
+ if (!verdict.trackHandoff.id)
10156
+ this.dropQueuedHandoff(verdict);
10157
+ verdict.fenced = !verdict.durableBeginCommitted;
10158
+ return false;
10159
+ }
10160
+ }
9707
10161
  if (lookBeforePaste) {
9708
10162
  await lookBeforePaste();
9709
10163
  // The look awaited: this attempt is still this write's — the same fence, tmux and launch — or nothing is written.
@@ -9737,6 +10191,22 @@ export class Daemon extends EventEmitter {
9737
10191
  // failed/throwing write as uncertain rather than retrying bytes into the
9738
10192
  // same pane; the write-ahead Enter marker later separates safe pre-Enter
9739
10193
  // recovery from a possibly executed command.
10194
+ // M-2267 (#2971 review, Prism P2): EVERY paste attempt — a retry after a failed paste, a first paste whose durable
10195
+ // begin already went — asks too. Before the begin: nothing begun, the caller waits for readiness and tries again.
10196
+ // After it: this attempt waits here, keeping its durable attempt; a mode outlasting the wait fails it unwritten.
10197
+ if (!verdict.durableBeginCommitted) {
10198
+ if (await this.paneInTmuxMode("paste"))
10199
+ return "pane-in-mode";
10200
+ }
10201
+ else {
10202
+ const mode = await this.waitWhilePaneInMode("paste", () => (!stillCurrent || stillCurrent()) && !this.deliveryWritesStopping);
10203
+ if (mode === "stale") {
10204
+ verdict.fenced = false;
10205
+ return false;
10206
+ }
10207
+ if (mode === "held")
10208
+ return this.failDelivery(verdict, status, "paste", "pane-in-tmux-mode");
10209
+ }
9740
10210
  const pasteGeneration = this.spawnGeneration;
9741
10211
  beforePaste?.();
9742
10212
  const pasted = await this.tmux.pasteBuffer(formatted);
@@ -9825,7 +10295,15 @@ export class Daemon extends EventEmitter {
9825
10295
  let enterAt = Date.now();
9826
10296
  const enterMono = performance.now();
9827
10297
  const steerEnterCurrent = () => steerCurrent() && performance.now() <= steerEnterDeadline;
9828
- if (!(await this.sendDeliveryEnter("initial-submit", steerComposerProof ? steerEnterCurrent : undefined, durableAttempt))) {
10298
+ let inputHeld = null;
10299
+ if (!(await this.sendDeliveryEnter("initial-submit", steerComposerProof ? steerEnterCurrent : undefined, durableAttempt, (why) => { inputHeld = why; }))) {
10300
+ if (inputHeld) {
10301
+ // #2971: held in a tmux mode past the bound; M-2290: a recognised dialog on the pane. The paste may still be in
10302
+ // the CLI: keep it uncertain (Needs-you), never re-paste, never answer the dialog.
10303
+ verdict.phase = "submit-enter";
10304
+ verdict.proof = inputHeld === "dialog" ? "dialog-on-pane" : "pane-in-mode-held";
10305
+ return false;
10306
+ }
9829
10307
  if (steerComposerProof && !steerCurrent()) {
9830
10308
  verdict.phase = "submit-enter";
9831
10309
  verdict.proof = "fenced-before-enter";
@@ -9916,8 +10394,9 @@ export class Daemon extends EventEmitter {
9916
10394
  }
9917
10395
  const proof = await this.confirmSubmitted(signature, pasteBaseline);
9918
10396
  if (proof === "submitted") {
9919
- if (status)
9920
- this.emit("message_confirmed", status); // ✅ native queue accepted
10397
+ // ✅ native queue accepted — unless it is on record (#3030): then ✅ waits for the turn that takes it.
10398
+ if (status && !verdict.handoff)
10399
+ this.emit("message_confirmed", status);
9921
10400
  return true;
9922
10401
  }
9923
10402
  if (proof === "unverifiable") {
@@ -9949,8 +10428,8 @@ export class Daemon extends EventEmitter {
9949
10428
  this.logger.warn("Paste reached the pane but could not be verified as submitted — accepting without proof");
9950
10429
  verdict.phase = "best-effort-submission";
9951
10430
  verdict.proof = "unverified";
9952
- if (status)
9953
- this.emit("message_confirmed", status); // ✅ (best-effort)
10431
+ if (status && !verdict.handoff)
10432
+ this.emit("message_confirmed", status); // ✅ (best-effort; on record: when taken, #3030)
9954
10433
  return true;
9955
10434
  }
9956
10435
  // raw_paste has no unique marker and is not allowed to re-paste or
@@ -9965,8 +10444,14 @@ export class Daemon extends EventEmitter {
9965
10444
  // confirmation ladder. "stranded" is the case that used to be a silent
9966
10445
  // ✅ — the text was visible, so the old check called it delivered.
9967
10446
  this.logger.warn({ proof }, "Native-queue paste not confirmed as submitted — retrying via idle-gated delivery");
9968
- if (windowId && this.controlClient) {
9969
- await this.controlClient.waitUntilIdle(windowId);
10447
+ // #3030: what follows is an idle submission (or a failure the caller reports): no longer a queued hand-off.
10448
+ if (verdict.handoff && !verdict.trackHandoff?.id)
10449
+ this.dropQueuedHandoff(verdict);
10450
+ verdict.handoff = undefined;
10451
+ if (windowId && this.controlClient && !(await this.controlClient.waitUntilIdle(windowId))) {
10452
+ verdict.phase = "native-queue-idle-wait";
10453
+ verdict.proof = "idle-wait-failed";
10454
+ return false;
9970
10455
  }
9971
10456
  // Re-read before doing anything: the CLI had a whole turn to flush its
9972
10457
  // queue while we waited, and the right recovery differs per outcome.
@@ -12361,7 +12846,12 @@ export class Daemon extends EventEmitter {
12361
12846
  || this.startupAborted || this.deliveryWritesStopping || this.runtimeMonitorsFrozen || this.healthCheckPaused)) {
12362
12847
  throw new Error("OOM recovery cancelled: the instance was stopped, paused or replaced while memory was unavailable");
12363
12848
  }
12364
- return this.trySpawnInsideGate(reuseWindow, startupTimeoutMs);
12849
+ return this.trySpawnInsideGate(reuseWindow, startupTimeoutMs).catch(err => {
12850
+ // Includes an in-place binary replacement during asynchronous launch preparation.
12851
+ if (err instanceof KiroCompatibilityError)
12852
+ this.emit("kiro_compatibility_hold", err.message);
12853
+ throw err;
12854
+ });
12365
12855
  };
12366
12856
  const ready = !this.spawnGate ? await enter() : await this.spawnGate.run({
12367
12857
  instanceName: this.name,
@@ -12584,6 +13074,11 @@ export class Daemon extends EventEmitter {
12584
13074
  this.startupAdmission?.();
12585
13075
  const launchFence = this.launchFenceEpoch;
12586
13076
  const backendConfig = this.buildBackendConfig();
13077
+ // A compatibility refusal is not a failed resume: no configuration, pin, history or pane may change.
13078
+ if (this.backend?.assertLaunchSupported) {
13079
+ this.backend.assertLaunchSupported(backendConfig);
13080
+ this.emit("kiro_compatibility_hold", null);
13081
+ }
12587
13082
  // #1633: decided before prev-instructions is first written — its absence is what says "never launched before".
12588
13083
  this.prepareWhatsNew(!existsSync(join(this.instanceDir, "prev-instructions")));
12589
13084
  // Record what this launch writes. #2787: a change is never announced in the pane; the CLI takes the new
@@ -12620,6 +13115,7 @@ export class Daemon extends EventEmitter {
12620
13115
  const agentTokenPath = join(this.instanceDir, "agent.token");
12621
13116
  const agentToken = randomBytes(32).toString("hex");
12622
13117
  const tokenWrite = writeSecretFile(agentTokenPath, agentToken);
13118
+ this.agentCredentialIssuer?.(agentToken);
12623
13119
  if (!tokenWrite.ok) {
12624
13120
  // Do not fail the spawn over it — an instance that cannot start is worse
12625
13121
  // than one whose token is readable. But say so, loudly: the whole point of
@@ -12890,7 +13386,7 @@ export class Daemon extends EventEmitter {
12890
13386
  // ones included: if the auto-dismiss keeps failing here, the human
12891
13387
  // report should count from first sight, not from the runtime scan.
12892
13388
  if (!dialog.fatal && this.deliveryBlockingDialogs().some(d => Daemon.dialogKey(d) === Daemon.dialogKey(dialog))) {
12893
- this.trackDialogParked(dialog);
13389
+ this.trackDialogParked(dialog, pane);
12894
13390
  }
12895
13391
  // A fatal startup screen cannot be dismissed by keypresses (e.g. a
12896
13392
  // corrupt claude.json modal offering only "exit" / "reset config").
@@ -12920,7 +13416,7 @@ export class Daemon extends EventEmitter {
12920
13416
  this.heldReasonLoggedKey = Daemon.dialogKey(dialog);
12921
13417
  this.logger.warn({ description: dialog.description, ...reason }, "Startup dialog held — not auto-answering, deliveries stay blocked");
12922
13418
  }
12923
- this.trackDialogParked(dialog);
13419
+ this.trackDialogParked(dialog, pane);
12924
13420
  matched = true;
12925
13421
  break;
12926
13422
  }