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

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 (197) hide show
  1. package/README.md +1 -1
  2. package/README.zh-TW.md +1 -1
  3. package/dist/agent-cli-instructions.md +10 -7
  4. package/dist/agent-endpoint.js +21 -9
  5. package/dist/agent-endpoint.js.map +1 -1
  6. package/dist/agent-whats-new.d.ts +47 -0
  7. package/dist/agent-whats-new.js +141 -0
  8. package/dist/agent-whats-new.js.map +1 -0
  9. package/dist/auth-api.d.ts +2 -2
  10. package/dist/auth-api.js +4 -2
  11. package/dist/auth-api.js.map +1 -1
  12. package/dist/backend/kiro-engine-ledger.d.ts +5 -0
  13. package/dist/backend/kiro-engine-ledger.js +20 -0
  14. package/dist/backend/kiro-engine-ledger.js.map +1 -1
  15. package/dist/backend/kiro-identity.d.ts +6 -0
  16. package/dist/backend/kiro-identity.js +19 -5
  17. package/dist/backend/kiro-identity.js.map +1 -1
  18. package/dist/backend/kiro.d.ts +14 -2
  19. package/dist/backend/kiro.js +84 -9
  20. package/dist/backend/kiro.js.map +1 -1
  21. package/dist/backend/types.d.ts +4 -2
  22. package/dist/backend/types.js.map +1 -1
  23. package/dist/channel/adapters/discord.d.ts +11 -1
  24. package/dist/channel/adapters/discord.js +23 -1
  25. package/dist/channel/adapters/discord.js.map +1 -1
  26. package/dist/channel/adapters/telegram.d.ts +19 -1
  27. package/dist/channel/adapters/telegram.js +60 -9
  28. package/dist/channel/adapters/telegram.js.map +1 -1
  29. package/dist/channel/mcp-server.js +9 -1
  30. package/dist/channel/mcp-server.js.map +1 -1
  31. package/dist/channel/mcp-tools.d.ts +11 -0
  32. package/dist/channel/mcp-tools.js +23 -12
  33. package/dist/channel/mcp-tools.js.map +1 -1
  34. package/dist/channel/telegram-markdown.d.ts +6 -0
  35. package/dist/channel/telegram-markdown.js +104 -0
  36. package/dist/channel/telegram-markdown.js.map +1 -0
  37. package/dist/channel/tool-router.js +9 -3
  38. package/dist/channel/tool-router.js.map +1 -1
  39. package/dist/channel/types.d.ts +14 -2
  40. package/dist/cli.js +55 -16
  41. package/dist/cli.js.map +1 -1
  42. package/dist/command-table.js +1 -1
  43. package/dist/command-table.js.map +1 -1
  44. package/dist/config-validator.js +49 -0
  45. package/dist/config-validator.js.map +1 -1
  46. package/dist/cross-instance-envelope.d.ts +15 -2
  47. package/dist/cross-instance-envelope.js +27 -5
  48. package/dist/cross-instance-envelope.js.map +1 -1
  49. package/dist/daemon.d.ts +48 -0
  50. package/dist/daemon.js +250 -22
  51. package/dist/daemon.js.map +1 -1
  52. package/dist/delivery-outbox.d.ts +17 -2
  53. package/dist/delivery-outbox.js +13 -2
  54. package/dist/delivery-outbox.js.map +1 -1
  55. package/dist/delivery-reconciliation.d.ts +18 -1
  56. package/dist/delivery-reconciliation.js +67 -2
  57. package/dist/delivery-reconciliation.js.map +1 -1
  58. package/dist/fleet-manager.d.ts +63 -1
  59. package/dist/fleet-manager.js +314 -20
  60. package/dist/fleet-manager.js.map +1 -1
  61. package/dist/general-knowledge/skills/backend-providers/SKILL.md +1 -1
  62. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +9 -6
  63. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +11 -2
  64. package/dist/general-knowledge/skills/delegation-playbook/SKILL.md +8 -8
  65. package/dist/general-knowledge/skills/fleet-config/SKILL.md +5 -5
  66. package/dist/general-knowledge/skills/fleet-health/SKILL.md +4 -4
  67. package/dist/general-knowledge/skills/fleet-restart/SKILL.md +13 -13
  68. package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +3 -2
  69. package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +6 -0
  70. package/dist/general-knowledge/skills/model-discovery/SKILL.md +9 -9
  71. package/dist/general-knowledge/skills/multi-channel/SKILL.md +8 -2
  72. package/dist/general-knowledge/skills/scheduling/SKILL.md +5 -3
  73. package/dist/general-knowledge/skills/session-management/SKILL.md +6 -5
  74. package/dist/general-knowledge/skills/tui-effort/SKILL.md +2 -2
  75. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +4 -3
  76. package/dist/general-knowledge/steering/core-rules.md +8 -7
  77. package/dist/instance-avatar.d.ts +36 -0
  78. package/dist/instance-avatar.js +57 -0
  79. package/dist/instance-avatar.js.map +1 -0
  80. package/dist/instance-config-impact.js +5 -0
  81. package/dist/instance-config-impact.js.map +1 -1
  82. package/dist/instance-lifecycle.d.ts +4 -0
  83. package/dist/instance-lifecycle.js +3 -1
  84. package/dist/instance-lifecycle.js.map +1 -1
  85. package/dist/instructions.d.ts +2 -0
  86. package/dist/instructions.js +10 -7
  87. package/dist/instructions.js.map +1 -1
  88. package/dist/locale.js +36 -26
  89. package/dist/locale.js.map +1 -1
  90. package/dist/next-steps.js +1 -1
  91. package/dist/next-steps.js.map +1 -1
  92. package/dist/outbound-handlers.d.ts +2 -0
  93. package/dist/outbound-handlers.js +28 -15
  94. package/dist/outbound-handlers.js.map +1 -1
  95. package/dist/outbound-schemas.d.ts +10 -2
  96. package/dist/outbound-schemas.js +15 -5
  97. package/dist/outbound-schemas.js.map +1 -1
  98. package/dist/public-web-gateway.js +7 -2
  99. package/dist/public-web-gateway.js.map +1 -1
  100. package/dist/service-activation.js +4 -3
  101. package/dist/service-activation.js.map +1 -1
  102. package/dist/settings-api.js +71 -21
  103. package/dist/settings-api.js.map +1 -1
  104. package/dist/settings-change.js +70 -1
  105. package/dist/settings-change.js.map +1 -1
  106. package/dist/settings-effect.js +26 -4
  107. package/dist/settings-effect.js.map +1 -1
  108. package/dist/settings-http-confirmation.js +7 -0
  109. package/dist/settings-http-confirmation.js.map +1 -1
  110. package/dist/setup-form.d.ts +1 -1
  111. package/dist/setup-form.js +1 -1
  112. package/dist/status-emojis.d.ts +10 -3
  113. package/dist/status-emojis.js +11 -2
  114. package/dist/status-emojis.js.map +1 -1
  115. package/dist/tips.js +4 -4
  116. package/dist/tips.js.map +1 -1
  117. package/dist/transcript-delta-reader.d.ts +3 -3
  118. package/dist/transcript-delta-reader.js +8 -8
  119. package/dist/transcript-delta-reader.js.map +1 -1
  120. package/dist/transcript-monitor.d.ts +8 -0
  121. package/dist/transcript-monitor.js +26 -2
  122. package/dist/transcript-monitor.js.map +1 -1
  123. package/dist/transcript-sources.d.ts +54 -0
  124. package/dist/transcript-sources.js +188 -27
  125. package/dist/transcript-sources.js.map +1 -1
  126. package/dist/transcript-turns.d.ts +15 -2
  127. package/dist/transcript-turns.js +15 -2
  128. package/dist/transcript-turns.js.map +1 -1
  129. package/dist/types.d.ts +11 -0
  130. package/dist/ui/chat-emojis.js +33 -0
  131. package/dist/ui/chat-render.js +12 -0
  132. package/dist/ui/chat-store.js +1 -1
  133. package/dist/ui/chat-strings.js +12 -12
  134. package/dist/ui/chat-thread.js +23 -4
  135. package/dist/ui/panel-chat.js +16 -3
  136. package/dist/ui/panel-needs.js +5 -2
  137. package/dist/ui/panel-settings.js +129 -31
  138. package/dist/ui/settings-dialogs.js +7 -2
  139. package/dist/ui/settings-model.js +2 -1
  140. package/dist/ui/settings-strings.js +44 -4
  141. package/dist/ui/shared/app-i18n.js +37 -15
  142. package/dist/ui/shared/app-prefs.js +249 -0
  143. package/dist/ui/shared/app-route.js +1 -1
  144. package/dist/ui/shared/app-shell.js +17 -4
  145. package/dist/ui/shared/app-store.js +3 -1
  146. package/dist/ui/shared/app.css +55 -0
  147. package/dist/ui/shared/app.js +6 -1
  148. package/dist/ui/shared/header-tools.js +55 -5
  149. package/dist/ui/shared/instance-nav.js +169 -32
  150. package/dist/ui/shared/panel-view.js +6 -0
  151. package/dist/ui/shared/signin.js +7 -7
  152. package/dist/ui/shared/theme.js +39 -7
  153. package/dist/ui/shared/ui-icons.js +1 -0
  154. package/dist/ui/shared/usage-dialog.js +13 -4
  155. package/dist/ui/shared/view-strings.js +22 -4
  156. package/dist/ui/signin.html +1 -1
  157. package/dist/update-install.d.ts +15 -1
  158. package/dist/update-install.js +6 -0
  159. package/dist/update-install.js.map +1 -1
  160. package/dist/update-preflight.d.ts +100 -0
  161. package/dist/update-preflight.js +264 -0
  162. package/dist/update-preflight.js.map +1 -0
  163. package/dist/updater-caller.d.ts +31 -0
  164. package/dist/updater-caller.js +109 -0
  165. package/dist/updater-caller.js.map +1 -0
  166. package/dist/view-api.d.ts +1 -1
  167. package/dist/view-api.js +38 -8
  168. package/dist/view-api.js.map +1 -1
  169. package/dist/wake-coordinator.d.ts +11 -0
  170. package/dist/wake-coordinator.js +38 -4
  171. package/dist/wake-coordinator.js.map +1 -1
  172. package/dist/web-api.d.ts +21 -1
  173. package/dist/web-api.js +157 -1
  174. package/dist/web-api.js.map +1 -1
  175. package/dist/web-auth.d.ts +2 -2
  176. package/dist/web-auth.js +7 -4
  177. package/dist/web-auth.js.map +1 -1
  178. package/dist/web-host-guard.d.ts +3 -11
  179. package/dist/web-host-guard.js +3 -12
  180. package/dist/web-host-guard.js.map +1 -1
  181. package/dist/web-images.d.ts +47 -0
  182. package/dist/web-images.js +169 -0
  183. package/dist/web-images.js.map +1 -0
  184. package/dist/web-prefs.d.ts +63 -0
  185. package/dist/web-prefs.js +263 -0
  186. package/dist/web-prefs.js.map +1 -0
  187. package/dist/web-session.d.ts +36 -1
  188. package/dist/web-session.js +53 -6
  189. package/dist/web-session.js.map +1 -1
  190. package/dist/web-settings-patch.d.ts +21 -0
  191. package/dist/web-settings-patch.js +74 -0
  192. package/dist/web-settings-patch.js.map +1 -0
  193. package/dist/web-shell-routes.d.ts +1 -1
  194. package/dist/web-shell-routes.js +1 -1
  195. package/dist/web-shell-routes.js.map +1 -1
  196. package/dist/workflow-templates/default.md +1 -1
  197. package/package.json +1 -1
package/dist/daemon.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { AGENT_WHATS_NEW, agendVersion, planWhatsNew, readWhatsNewRecord, recordWhatsNew, whatsNewNotice } from "./agent-whats-new.js";
1
2
  import { join, dirname, basename, resolve } from "node:path";
2
3
  import { mkdirSync, writeFileSync, readFileSync, readdirSync, existsSync, unlinkSync, rmSync, appendFileSync, statSync, renameSync } from "node:fs";
3
4
  import { fileURLToPath } from "node:url";
@@ -77,10 +78,18 @@ const REPLY_GUARD_IDLE_CONFIRM_MS = 60_000;
77
78
  * down like a turn the transcript cannot place — never a recovery.
78
79
  */
79
80
  const TRANSCRIPT_RUNNING_HOLD_MAX_MS = 30 * 60_000;
81
+ /**
82
+ * #1641: an open turn in the CLI's transcript holds the instance busy (no paste, whatever the pane shows) only while the
83
+ * transcript is still moving: a turn whose end was missed must not pin the instance busy forever. A live turn writes
84
+ * often; a long silent tool run shows its own busy row on the pane.
85
+ */
86
+ export const TRANSCRIPT_OPEN_TURN_QUIET_MS = 15 * 60_000;
80
87
  const REPLY_RECOVERY_PROMPT = "[system:reply-required] The previous human-facing turn ended without a successfully delivered reply. Do not redo the work. React with an emoji or use the reply tool exactly once now to send the user a concise conclusion. If no substantive answer is needed, a brief react is sufficient. If you already replied to this message, do nothing. Do not reply to this system instruction except through the react or reply tool.";
81
88
  /** Point a resumed CLI at its one backend-native instruction source. */
82
- /** The outbox evidence for a delivery the CLI's transcript shows taken (#758; #1201 absorbed). */
83
- function transcriptMarkerEvidence(kind) {
89
+ /** The outbox evidence for a delivery the CLI's transcript shows taken (#758; #1201 absorbed; #1647 a raw paste's text). */
90
+ function transcriptMarkerEvidence(kind, rawPaste = false) {
91
+ if (rawPaste)
92
+ return "transcript-turn-user-entry-raw-paste-text";
84
93
  return kind === "user" ? "transcript-marker" : kind === "absorbed" ? "transcript-marker-absorbed" : "transcript-marker-queued";
85
94
  }
86
95
  export function buildInstructionReloadNotice(binaryName, instanceName, instanceDir, backendSource) {
@@ -472,6 +481,16 @@ const POST_ENTER_PROOF_WINDOW_MS = 3_000;
472
481
  /** A final bounded observation before an ambiguous Codex submit can be classified. */
473
482
  const CODEX_LATE_PROOF_MS = 5_000;
474
483
  const POST_ENTER_PROOF_POLL_MS = 250;
484
+ /** #1647: a raw paste a pane with no transcript showed taken — its text left the input row (or the CLI's own busy marker
485
+ * replaced it), with output after the Enter. */
486
+ const RAW_PASTE_LEFT_INPUT = "raw-paste-left-input-after-output";
487
+ /**
488
+ * #1647/#1663 P1-2: backends whose transcript AgEnD holds for this instance alone, so a raw paste's exact text in it is
489
+ * this paste's turn. claude-code: the path is its own statusline.json's transcript_path (written by this instance's
490
+ * CLI). Not codex: its rollout is chosen by working directory, which another instance can share, and AgEnD holds no
491
+ * session id to bind it — so a codex raw paste the pane could not prove stays uncertain.
492
+ */
493
+ const RAW_TEXT_TRANSCRIPT_BACKENDS = ["claude-code"];
475
494
  /**
476
495
  * A readerless CLI's delivery is proven by its own transcript, which lags the pane by a flush. Only a delta that was
477
496
  * read and carried no marker for this long is held against the delivery — a late flush must not raise a false ⚠️.
@@ -1237,6 +1256,11 @@ export class Daemon extends EventEmitter {
1237
1256
  turnReplyGuard = new TurnReplyGuard();
1238
1257
  /** #1510: turn boundaries from the CLI's transcript, for a backend whose guard reads its turn end there. */
1239
1258
  transcriptTurns = new TranscriptTurnLedger();
1259
+ /** #1641: when this launch's transcript monitor last read anything (a turn boundary, a tool, assistant text); performance.now(). */
1260
+ transcriptActivityAt = 0;
1261
+ /** #1641: the launch and composer a stuck pane's idle evidence is about, and since when (performance.now()); see recoverStuck. */
1262
+ stuckRecoveryKey = null;
1263
+ stuckRecoverySince = 0;
1240
1264
  /** #1510: since when (monotonic) this generation has been held as running in the transcript. */
1241
1265
  transcriptRunningSince = null;
1242
1266
  /** Prevent a visible stale XML fragment from being recovered on later turns. */
@@ -1388,6 +1412,8 @@ export class Daemon extends EventEmitter {
1388
1412
  recentPastes = [];
1389
1413
  pendingInstructionsUpdate;
1390
1414
  pendingInstructionsNotice = false;
1415
+ /** #1633: the one-time "what's new for you" notice, pasted before the next real delivery; recorded once pasted. */
1416
+ pendingWhatsNew = null;
1391
1417
  // Whether the warmup steering-reload notice should be injected after spawn.
1392
1418
  // Set in trySpawn by comparing the freshly-built instructions against the
1393
1419
  // last value the agent was told about (prev-instructions). Skipped when
@@ -1856,8 +1882,14 @@ export class Daemon extends EventEmitter {
1856
1882
  this.finishDurableDelivery(delivery, "delivered", OUTPUT_EDGE_ONLY_EVIDENCE);
1857
1883
  return;
1858
1884
  }
1885
+ // #1647: a raw paste on a pane with no transcript (kiro), taken by the CLI as its own input row shows.
1886
+ if (verdict.proof === RAW_PASTE_LEFT_INPUT) {
1887
+ this.finishDurableDelivery(delivery, "delivered", verdict.proof, verdict);
1888
+ return;
1889
+ }
1859
1890
  this.finishDurableDelivery(delivery, "delivered", verdict.transcriptProof
1860
- ? `positive submission proof; ${transcriptMarkerEvidence(verdict.transcriptProof)}` : "positive submission proof", verdict);
1891
+ ? `positive submission proof; ${transcriptMarkerEvidence(verdict.transcriptProof, verdict.submissionMode === "raw_paste")}`
1892
+ : "positive submission proof", verdict);
1861
1893
  }
1862
1894
  /**
1863
1895
  * Finish a delivery that only the output edge vouched for, by the delivery's own marker in the CLI's transcript (#758).
@@ -1873,23 +1905,28 @@ export class Daemon extends EventEmitter {
1873
1905
  * TRANSCRIPT_PROOF_WINDOW_MS). It never finishes a row that stop has handed to reconciliation; a pause or respawn
1874
1906
  * only ends the wait, falling back to today's outcome.
1875
1907
  */
1876
- async proveDeliveryFromTranscript(delivery, checkpoint) {
1908
+ async proveDeliveryFromTranscript(delivery, checkpoint, rawPaste) {
1877
1909
  const spawn = this.spawnGeneration;
1878
1910
  const fence = this.launchFenceEpoch;
1879
1911
  let last = "unavailable";
1880
1912
  let outcome = "delivered";
1881
1913
  let evidence = OUTPUT_EDGE_ONLY_EVIDENCE;
1914
+ // #1647: a raw paste has no marker; its text is the needle, and its pane proof already failed — so without the turn
1915
+ // in the transcript it stays uncertain (with the pane's reason), never the output-edge outcome.
1916
+ const needle = rawPaste ? { rawText: rawPaste.text } : delivery.deliveryId;
1917
+ let found = false;
1882
1918
  try {
1883
1919
  for (let polls = 0; polls <= TRANSCRIPT_PROOF_WINDOW_MS / TRANSCRIPT_PROOF_POLL_MS; polls++) {
1884
1920
  if (this.deliveryWritesStopping)
1885
1921
  return; // reconciliation owns the row now
1886
- const found = await scanTranscriptForDeliveryMarker(checkpoint.path, checkpoint.offset, checkpoint.backend, delivery.deliveryId);
1887
- if (found === "user" || found === "absorbed" || found === "queued") {
1922
+ const look = await scanTranscriptForDeliveryMarker(checkpoint.path, checkpoint.offset, checkpoint.backend, needle);
1923
+ if (look === "user" || look === "absorbed" || look === "queued") {
1924
+ found = true;
1888
1925
  outcome = "delivered";
1889
- evidence = transcriptMarkerEvidence(found);
1926
+ evidence = transcriptMarkerEvidence(look, !!rawPaste);
1890
1927
  break;
1891
1928
  }
1892
- last = found;
1929
+ last = look;
1893
1930
  if (spawn !== this.spawnGeneration || fence !== this.launchFenceEpoch) {
1894
1931
  last = "unavailable";
1895
1932
  break;
@@ -1909,6 +1946,11 @@ export class Daemon extends EventEmitter {
1909
1946
  catch (err) {
1910
1947
  this.logger.debug({ err }, "Transcript proof of a delivery failed — keeping the output-edge outcome");
1911
1948
  }
1949
+ if (rawPaste && !found) {
1950
+ outcome = "uncertain";
1951
+ evidence = `${rawPaste.unproven}; no-transcript-turn (${last})`;
1952
+ this.logger.warn({ deliveryId: delivery.deliveryId, cause: evidence }, "Raw paste outcome uncertain — not re-pasting");
1953
+ }
1912
1954
  // The settlement itself can throw (e.g. the outbox store is unhealthy), and this runs on a detached `void`
1913
1955
  // promise: an uncaught throw is an unhandled rejection, which the fleet restart path turns into a global fault.
1914
1956
  // Settle observably and leave the row for reconciliation — never a false completion, never another paste (#1207 P2).
@@ -3719,7 +3761,18 @@ export class Daemon extends EventEmitter {
3719
3761
  if (delivered)
3720
3762
  this.finishDurableSubmission(durableAttempt ?? null, verdict);
3721
3763
  else if (durableAttempt && verdict.paneWriteStarted) {
3722
- this.finishDurableDelivery(durableAttempt, "uncertain", `${verdict.phase ?? "raw-paste"}:${verdict.proof ?? "unverified"}`);
3764
+ const unproven = `${verdict.phase ?? "raw-paste"}:${verdict.proof ?? "unverified"}`;
3765
+ const checkpoint = verdict.transcriptCheckpoint;
3766
+ if (checkpoint && RAW_TEXT_TRANSCRIPT_BACKENDS.includes(checkpoint.backend)) {
3767
+ // #1647: the pane could not show it, but the CLI's transcript records the turn the paste started — later
3768
+ // than the pane window sometimes. Wait for it there (bounded), off the paste lock.
3769
+ this.logger.info({ deliveryId: durableAttempt.deliveryId, unproven }, "Raw paste not proven on the pane — asking the CLI's transcript for the turn it started");
3770
+ void this.proveDeliveryFromTranscript(durableAttempt, checkpoint, { text: rawText, unproven });
3771
+ }
3772
+ else {
3773
+ this.logger.warn({ deliveryId: durableAttempt.deliveryId, cause: unproven }, "Raw paste outcome uncertain — not re-pasting");
3774
+ this.finishDurableDelivery(durableAttempt, "uncertain", unproven);
3775
+ }
3723
3776
  }
3724
3777
  else if (durableAttempt && verdict.durableBeginCommitted) {
3725
3778
  this.abortDurableDelivery(durableAttempt, `${verdict.phase ?? "raw-paste"}:${verdict.proof ?? "no-pane-write"}`);
@@ -5041,9 +5094,68 @@ export class Daemon extends EventEmitter {
5041
5094
  buildTranscriptMonitor(source) {
5042
5095
  const monitor = new TranscriptMonitor(this.instanceDir, this.logger, source);
5043
5096
  this.transcriptTurns.reset();
5044
- monitor.on("turns", (turns) => this.transcriptTurns.observe(turns));
5097
+ this.transcriptActivityAt = 0;
5098
+ // Monotonic: a wall-clock correction must neither end nor extend the open-turn hold (#1641 R1).
5099
+ const touch = () => { this.transcriptActivityAt = performance.now(); };
5100
+ monitor.on("turns", (turns) => { this.transcriptTurns.observe(turns, performance.now()); touch(); });
5101
+ // #1641 R2: a re-baselined source starts over, and so does what it said (an open turn read before is withdrawn).
5102
+ monitor.on("reset", () => { if (this.transcriptMonitor === monitor) {
5103
+ this.transcriptTurns.reset();
5104
+ this.transcriptActivityAt = 0;
5105
+ } });
5106
+ for (const event of ["tool_use", "tool_result", "assistant_text"])
5107
+ monitor.on(event, touch);
5045
5108
  return monitor;
5046
5109
  }
5110
+ /**
5111
+ * #1641: what the CLI's own transcript (codex's rollout) says about its turn. `open`: a turn started and has not ended,
5112
+ * and the transcript moved within TRANSCRIPT_OPEN_TURN_QUIET_MS. `closed`: every turn read since this launch's monitor
5113
+ * began has ended. `unknown`: not a transcript backend, nothing followed, or an open turn gone quiet (a missed end).
5114
+ */
5115
+ transcriptTurnState(now = performance.now()) {
5116
+ if (this.backend?.turnEndFromTranscript !== true || this.transcriptMonitor?.following() !== true)
5117
+ return "unknown";
5118
+ // #1641 R2: an empty ledger after a baseline that could not see the current turn's start is not "closed".
5119
+ if (this.transcriptMonitor.turnStateKnown?.() !== true)
5120
+ return "unknown";
5121
+ const since = this.transcriptTurns.openSince();
5122
+ if (since === null)
5123
+ return "closed";
5124
+ return now - Math.max(since, this.transcriptActivityAt) < TRANSCRIPT_OPEN_TURN_QUIET_MS ? "open" : "unknown";
5125
+ }
5126
+ /**
5127
+ * #1641: a stuck pane that is in fact an idle CLI (a footer this build cannot read) must not stay stuck for hours with
5128
+ * every delivery parked behind it. It recovers when the backend sees its own live, EMPTY composer with no busy, queued
5129
+ * or dialog marker (isStableUnknownLayoutIdlePane) AND the transcript shows no open turn — or, with no transcript to
5130
+ * ask, that same composer held unchanged for UNKNOWN_LAYOUT_STABLE_MS (#978's rule). An open turn never recovers.
5131
+ */
5132
+ recoverStuck(pane, snapshot, changeAt, turn) {
5133
+ const machine = this.instanceStateMachine;
5134
+ const idleComposer = this.backend?.isStableUnknownLayoutIdlePane?.(pane) === true;
5135
+ // The resume load (#1031: `Resuming session…` over the same composer) is input that is not open yet (#1641 R1).
5136
+ if (!machine || !idleComposer || turn === "open" || this.inputTransientInPane(pane)) {
5137
+ this.stuckRecoveryKey = null;
5138
+ return snapshot;
5139
+ }
5140
+ // The evidence is about this launch and this screen. Once it holds, it keeps holding — a still pane reads stuck
5141
+ // again at the next sweep (its footer is still unreadable) and must not start the window over (#1641 R1).
5142
+ const mono = performance.now();
5143
+ const key = `${this.spawnGeneration}:${this.launchAttempt}\n${pane.replace(/⋆/gu, "").replace(/[ \t]+$/gmu, "")}`;
5144
+ if (key !== this.stuckRecoveryKey) {
5145
+ this.stuckRecoveryKey = key;
5146
+ this.stuckRecoverySince = mono;
5147
+ }
5148
+ if (turn === "unknown" && mono - this.stuckRecoverySince < UNKNOWN_LAYOUT_STABLE_MS)
5149
+ return snapshot;
5150
+ // A footer this build cannot read stays unreadable: the same still pane reads stuck again at every sweep and is
5151
+ // recovered again. Only the edge into idle is news.
5152
+ const fields = { evidence: turn === "closed" ? "transcript_no_open_turn" : "stable_empty_composer", unchangedForMs: snapshot.unchangedForMs };
5153
+ if (this.instanceState !== "idle")
5154
+ this.logger.info(fields, "Stuck instance recovered — the CLI shows its idle composer and is not running a turn (#1641)");
5155
+ else
5156
+ this.logger.debug(fields, "Still idle on its unreadable footer — the stuck reading was recovered again (#1641)");
5157
+ return machine.observe(pane, Date.now(), { settled: true, changeAt, forceReady: true });
5158
+ }
5047
5159
  replyCompletionGuardEnabled() {
5048
5160
  return this.config.reply_completion_guard !== false
5049
5161
  && this.backend?.replyCompletionGuard === true;
@@ -5425,7 +5537,9 @@ export class Daemon extends EventEmitter {
5425
5537
  // a pair every ~1.6s for as long as the repaint lasted (#1116). Real
5426
5538
  // work cannot hide behind this: it changes the layout, the next
5427
5539
  // output arms another probe, and the debounce capture still fires.
5428
- if (this.instanceState === "idle" && this.backend?.isPeriodicRedrawIdlePane?.(pane) === true)
5540
+ // #1641: never while the transcript shows a turn running — that idle-looking screen is mid-turn.
5541
+ if (this.instanceState === "idle" && this.backend?.isPeriodicRedrawIdlePane?.(pane) === true
5542
+ && this.transcriptTurnState() !== "open")
5429
5543
  return;
5430
5544
  // A second burst overtook the probe. Do not bless its stale capture as
5431
5545
  // idle: continuous output is the safer working signal, while the next
@@ -5458,6 +5572,10 @@ export class Daemon extends EventEmitter {
5458
5572
  // decisions about *now*. The old double-observe expressed that by calling
5459
5573
  // observe twice, which silently disabled the "content moved" branch.
5460
5574
  const structuredPeriodicIdle = this.backend?.isPeriodicRedrawIdlePane;
5575
+ // #1641: the transcript outranks the pane: while it shows the turn running, no reading of this pane is idle (codex
5576
+ // hides its status row while streaming, leaving the idle composer and footer in view mid-turn).
5577
+ const transcriptTurn = this.transcriptTurnState();
5578
+ const transcriptBusy = transcriptTurn === "open";
5461
5579
  let snapshot;
5462
5580
  if (deliveryFallback !== null) {
5463
5581
  // A missing footer cannot fall through to a broad ready match. All
@@ -5466,7 +5584,7 @@ export class Daemon extends EventEmitter {
5466
5584
  settled: true,
5467
5585
  changeAt: observedChangeAt,
5468
5586
  forceReady: deliveryFallback,
5469
- forceBusy: !deliveryFallback || paneActivity !== null,
5587
+ forceBusy: !deliveryFallback || paneActivity !== null || transcriptBusy,
5470
5588
  });
5471
5589
  }
5472
5590
  else if (structuredPeriodicIdle && reason === "output_probe") {
@@ -5488,6 +5606,7 @@ export class Daemon extends EventEmitter {
5488
5606
  snapshot = this.instanceStateMachine.observe(pane, Date.now(), {
5489
5607
  settled: true,
5490
5608
  changeAt: observedChangeAt,
5609
+ forceBusy: transcriptBusy,
5491
5610
  });
5492
5611
  }
5493
5612
  }
@@ -5521,6 +5640,7 @@ export class Daemon extends EventEmitter {
5521
5640
  settled: true,
5522
5641
  changeAt: observedChangeAt,
5523
5642
  forceReady: true,
5643
+ forceBusy: transcriptBusy,
5524
5644
  });
5525
5645
  }
5526
5646
  else {
@@ -5569,6 +5689,7 @@ export class Daemon extends EventEmitter {
5569
5689
  snapshot = this.instanceStateMachine.observe(pane, Date.now(), {
5570
5690
  settled: true,
5571
5691
  changeAt: observedChangeAt,
5692
+ forceBusy: transcriptBusy,
5572
5693
  });
5573
5694
  }
5574
5695
  else {
@@ -5586,9 +5707,11 @@ export class Daemon extends EventEmitter {
5586
5707
  snapshot = this.instanceStateMachine.observe(pane, Date.now(), {
5587
5708
  settled,
5588
5709
  changeAt: observedChangeAt,
5589
- forceBusy: paneActivity !== null,
5710
+ forceBusy: paneActivity !== null || transcriptBusy,
5590
5711
  });
5591
5712
  }
5713
+ if (snapshot.state === "stuck")
5714
+ snapshot = this.recoverStuck(pane, snapshot, observedChangeAt, transcriptTurn);
5592
5715
  this.applyInstanceStateSnapshot(snapshot, pane);
5593
5716
  // Backends without a transcript feed can still say what they are doing, if
5594
5717
  // their TUI names it. Free-riding on a capture that already happened: no
@@ -6011,12 +6134,14 @@ export class Daemon extends EventEmitter {
6011
6134
  * here — a steered message must read EXACTLY like a queued one to the
6012
6135
  * agent, or the two drift apart in what the agent is told about replying.
6013
6136
  */
6137
+ /** #1650: how this instance talks to the fleet — MCP tools, or agend-agent commands (agent_mode: cli). */
6138
+ agentMode() { return this.config.agent_mode === "cli" ? "cli" : "mcp"; }
6014
6139
  formatInboundMessage(content, meta) {
6015
6140
  const user = meta.user || "unknown";
6016
6141
  const fromInstance = meta.from_instance;
6017
6142
  let formatted;
6018
6143
  if (fromInstance) {
6019
- formatted = formatCrossInstanceInboundMessage(content, meta);
6144
+ formatted = formatCrossInstanceInboundMessage(content, meta, this.agentMode());
6020
6145
  }
6021
6146
  else {
6022
6147
  const via = meta.source ? ` via ${meta.source}` : "";
@@ -6029,7 +6154,10 @@ export class Daemon extends EventEmitter {
6029
6154
  formatted = `[Recent reactions: ${meta.pending_reactions}]\n${formatted}`;
6030
6155
  }
6031
6156
  formatted += renderHandoffMetadata(meta);
6032
- formatted += "\n(Reply using the reply tool — do NOT respond with direct text)";
6157
+ // #1650: a CLI-mode agent has no reply tool — it answers with the agend-agent command.
6158
+ formatted += this.agentMode() === "cli"
6159
+ ? '\n(Reply with agend-agent reply "…" — do NOT respond with direct text)'
6160
+ : "\n(Reply using the reply tool — do NOT respond with direct text)";
6033
6161
  }
6034
6162
  if (meta.reply_to_text) {
6035
6163
  formatted += `\n(reply_to: "${meta.reply_to_text}")`;
@@ -6291,6 +6419,23 @@ export class Daemon extends EventEmitter {
6291
6419
  if (!this.isDeliveryEpochCurrent(deliveryEpoch))
6292
6420
  return;
6293
6421
  }
6422
+ const whatsNew = this.pendingWhatsNew ? this.currentWhatsNew(this.pendingWhatsNew) : null;
6423
+ if (whatsNew) {
6424
+ // #1633: here, inside the delivery lock and ahead of a real delivery — never into a busy turn, never to an
6425
+ // instance nobody writes to. Recorded only once pasted: a failed or cancelled paste stays pending.
6426
+ const pasted = await this.deliverMessage(whatsNew.notice, undefined, { deliveryEpoch });
6427
+ if (pasted && this.pendingWhatsNew === whatsNew) {
6428
+ this.pendingWhatsNew = null;
6429
+ try {
6430
+ recordWhatsNew(this.instanceDir, whatsNew.ids, whatsNew.version);
6431
+ }
6432
+ catch (err) {
6433
+ this.logger.warn({ err }, "what's-new record not written — the notice may repeat once");
6434
+ }
6435
+ }
6436
+ if (!this.isDeliveryEpochCurrent(deliveryEpoch))
6437
+ return;
6438
+ }
6294
6439
  const status = channelStatus(meta);
6295
6440
  // A fresh delivery begins a fresh turn — its bubble must not inherit
6296
6441
  // the previous turn's tool list.
@@ -7663,6 +7808,33 @@ export class Daemon extends EventEmitter {
7663
7808
  this.logger.warn("Could not read the pane after Enter — not confirming the submission");
7664
7809
  return sawOutput && residue === "absent";
7665
7810
  }
7811
+ /**
7812
+ * #1647: whether a raw paste on a pane with no transcript (kiro) was taken: its text gone from the input row (or the
7813
+ * CLI's own busy marker shown instead — pasteResidue's rule) with output after the Enter. That is the confirmation's
7814
+ * own rule, looked at more than once: kiro repaints its composer late, so the single look right after Enter misread
7815
+ * pastes it took. A generic `working` reading is not evidence (#1663 P1-3: a repaint looks like work).
7816
+ *
7817
+ * Bounded by the budget that confirmation already has, measured from the Enter (#1663 P2-1): the busy wait plus the
7818
+ * post-Enter proof window. Every reading is asked again after its await — still this write's owner (`current`: the
7819
+ * caller's fence, no stop, the same tmux and spawn, #1663 P2-2) and still inside the budget — before it counts; a late
7820
+ * or fenced answer proves nothing. Null: not shown — the caller settles uncertain, never a second Enter.
7821
+ */
7822
+ async rawPasteTakenOnPane(windowId, enterAt, enterMono, formatted, current) {
7823
+ const deadline = enterMono + CONFIRM_BUSY_POLLS * CONFIRM_BUSY_POLL_MS + POST_ENTER_PROOF_WINDOW_MS;
7824
+ const live = () => current() && performance.now() <= deadline;
7825
+ for (;;) {
7826
+ if (!live())
7827
+ return null;
7828
+ if (this.controlClient?.hasOutputSince?.(windowId, enterAt)) {
7829
+ const residue = await this.pasteResidue(formatted);
7830
+ if (!live())
7831
+ return null;
7832
+ if (residue === "absent")
7833
+ return RAW_PASTE_LEFT_INPUT;
7834
+ }
7835
+ await new Promise(r => setTimeout(r, POST_ENTER_PROOF_POLL_MS));
7836
+ }
7837
+ }
7666
7838
  /** Once Kiro returns to its prompt, its unique envelope id in history proves submission. */
7667
7839
  async kiroSubmissionEvidence(windowId, formatted, signature) {
7668
7840
  if (!this.tmux)
@@ -7975,9 +8147,11 @@ export class Daemon extends EventEmitter {
7975
8147
  offset: attemptEvidence.transcriptOffset,
7976
8148
  };
7977
8149
  // From here every proof of this write may also ask the CLI's transcript (the pane can lose the echo to the
7978
- // reply, or still be painting the paste) — this delivery's exact marker only.
7979
- if (["claude-code", "codex"].includes(attemptEvidence.backend)) {
7980
- signature.transcript = this.transcriptProofFor(verdict.transcriptCheckpoint, durableAttempt.deliveryId, spawnAtWrite, stillCurrent, kind => { verdict.transcriptProof = kind; });
8150
+ // reply, or still be painting the paste) — this delivery's exact marker only, or a raw paste's exact text (#1647).
8151
+ // A raw paste's text only where the transcript is this instance's alone (RAW_TEXT_TRANSCRIPT_BACKENDS).
8152
+ if (["claude-code", "codex"].includes(attemptEvidence.backend)
8153
+ && (!rawPaste || RAW_TEXT_TRANSCRIPT_BACKENDS.includes(attemptEvidence.backend))) {
8154
+ signature.transcript = this.transcriptProofFor(verdict.transcriptCheckpoint, rawPaste ? { rawText: formatted } : durableAttempt.deliveryId, spawnAtWrite, stillCurrent, kind => { verdict.transcriptProof = kind; });
7981
8155
  }
7982
8156
  }
7983
8157
  }
@@ -8068,6 +8242,7 @@ export class Daemon extends EventEmitter {
8068
8242
  steerEnterDeadline = deadline;
8069
8243
  }
8070
8244
  let enterAt = Date.now();
8245
+ const enterMono = performance.now();
8071
8246
  const steerEnterCurrent = () => steerCurrent() && performance.now() <= steerEnterDeadline;
8072
8247
  if (!(await this.sendDeliveryEnter("initial-submit", steerComposerProof ? steerEnterCurrent : undefined, durableAttempt))) {
8073
8248
  if (steerComposerProof && !steerCurrent()) {
@@ -8313,12 +8488,20 @@ export class Daemon extends EventEmitter {
8313
8488
  return { kind: "kiro-pending", windowId, spawnGeneration: this.spawnGeneration, signature };
8314
8489
  }
8315
8490
  }
8316
- if (!submitted) {
8317
- if (rawPaste) {
8491
+ if (!submitted && rawPaste) {
8492
+ // #1647: kiro paints output at once but its composer late, so one residue look right after Enter misreads a
8493
+ // paste it took. Keep looking within the confirmation's own budget from the Enter, under this write's fence;
8494
+ // never a second Enter (a raw command must not run twice).
8495
+ const taken = await this.rawPasteTakenOnPane(windowId, enterAt, enterMono, formatted, steerCurrent);
8496
+ if (!taken) {
8318
8497
  verdict.phase = "raw-paste-submit-proof";
8319
8498
  verdict.proof = "no-positive-acceptance-proof";
8320
8499
  return false;
8321
8500
  }
8501
+ verdict.proof = taken;
8502
+ submitted = true;
8503
+ }
8504
+ if (!submitted) {
8322
8505
  // Legacy system pastes have no unique envelope id. Preserve their
8323
8506
  // existing bounded retry until they can be tied to a specific turn.
8324
8507
  this.logger.warn("Message not submitted after Enter — waiting for the prompt, then re-sending Enter once");
@@ -8631,10 +8814,10 @@ export class Daemon extends EventEmitter {
8631
8814
  * A write's transcript proof: bounded looks from its checkpoint, fenced to the write — its spawn and launch, a stop,
8632
8815
  * and the caller's own fence — so a look that lands after any of them is never used (#1380 review).
8633
8816
  */
8634
- transcriptProofFor(checkpoint, deliveryId, spawnAtWrite, stillCurrent, onProof) {
8817
+ transcriptProofFor(checkpoint, needle, spawnAtWrite, stillCurrent, onProof) {
8635
8818
  const launchAtWrite = this.launchFenceEpoch;
8636
8819
  return {
8637
- reader: new TranscriptDeltaReader(checkpoint.path, checkpoint.offset, checkpoint.backend, deliveryId),
8820
+ reader: new TranscriptDeltaReader(checkpoint.path, checkpoint.offset, checkpoint.backend, needle),
8638
8821
  current: () => !this.deliveryWritesStopping && this.spawnGeneration === spawnAtWrite
8639
8822
  && this.launchFenceEpoch === launchAtWrite && (stillCurrent?.() ?? true),
8640
8823
  onProof,
@@ -10426,6 +10609,49 @@ export class Daemon extends EventEmitter {
10426
10609
  }
10427
10610
  catch { /* non-fatal */ }
10428
10611
  }
10612
+ /**
10613
+ * #1633 (#1639 review): the pending notice as the record stands NOW, just before the paste — it was decided at launch,
10614
+ * and the record may have changed while the delivery waited. Unreadable → nothing is sent (and nothing pending);
10615
+ * entries recorded meanwhile are dropped from it; none left → nothing.
10616
+ */
10617
+ currentWhatsNew(pending) {
10618
+ const record = readWhatsNewRecord(this.instanceDir);
10619
+ if (record === "unreadable") {
10620
+ this.pendingWhatsNew = null;
10621
+ return null;
10622
+ }
10623
+ const known = new Set(record?.announced ?? []);
10624
+ const ids = pending.ids.filter(id => !known.has(id));
10625
+ if (!ids.length) {
10626
+ this.pendingWhatsNew = null;
10627
+ return null;
10628
+ }
10629
+ if (ids.length === pending.ids.length)
10630
+ return pending;
10631
+ const narrowed = { notice: whatsNewNotice(pending.version, AGENT_WHATS_NEW.filter(e => ids.includes(e.id))), ids, version: pending.version };
10632
+ this.pendingWhatsNew = narrowed;
10633
+ return narrowed;
10634
+ }
10635
+ /** #1633: what this instance has not been told about its AgEnD version yet (see agent-whats-new.ts). */
10636
+ prepareWhatsNew(fresh, version = agendVersion()) {
10637
+ try {
10638
+ const plan = planWhatsNew({ record: readWhatsNewRecord(this.instanceDir), fresh, version });
10639
+ if (!plan) {
10640
+ this.pendingWhatsNew = null;
10641
+ return;
10642
+ }
10643
+ if ("silent" in plan) {
10644
+ recordWhatsNew(this.instanceDir, plan.silent, plan.version);
10645
+ this.pendingWhatsNew = null;
10646
+ return;
10647
+ }
10648
+ this.pendingWhatsNew = plan;
10649
+ this.logger.info({ entries: plan.ids }, "what's-new notice pending for the next delivery");
10650
+ }
10651
+ catch (err) {
10652
+ this.logger.debug({ err }, "what's-new not prepared");
10653
+ }
10654
+ }
10429
10655
  async trySpawnInsideGate(reuseWindow = false, startupTimeoutMs) {
10430
10656
  this.startupAdmission?.();
10431
10657
  const launchFence = this.launchFenceEpoch;
@@ -10435,6 +10661,8 @@ export class Daemon extends EventEmitter {
10435
10661
  // instructionsReloadedOnResume) so the warmup steering-reload can be
10436
10662
  // skipped when nothing changed.
10437
10663
  this.lastBuiltInstructions = backendConfig.instructions ?? "";
10664
+ // #1633: decided before prev-instructions is first written — its absence is what says "never launched before".
10665
+ this.prepareWhatsNew(!existsSync(join(this.instanceDir, "prev-instructions")));
10438
10666
  {
10439
10667
  const prevFile = join(this.instanceDir, "prev-instructions");
10440
10668
  let prev = "";