@songsid/agend 2.2.0-alpha.9 → 2.2.0-beta.2

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 (173) 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 +2 -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 +13 -1
  24. package/dist/channel/adapters/discord.js +34 -2
  25. package/dist/channel/adapters/discord.js.map +1 -1
  26. package/dist/channel/adapters/telegram.d.ts +11 -1
  27. package/dist/channel/adapters/telegram.js +28 -2
  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/tool-router.js +7 -2
  35. package/dist/channel/tool-router.js.map +1 -1
  36. package/dist/channel/types.d.ts +13 -1
  37. package/dist/cli.js +55 -16
  38. package/dist/cli.js.map +1 -1
  39. package/dist/command-table.js +1 -1
  40. package/dist/command-table.js.map +1 -1
  41. package/dist/config-validator.js +48 -0
  42. package/dist/config-validator.js.map +1 -1
  43. package/dist/cross-instance-envelope.d.ts +15 -2
  44. package/dist/cross-instance-envelope.js +27 -5
  45. package/dist/cross-instance-envelope.js.map +1 -1
  46. package/dist/daemon.d.ts +36 -0
  47. package/dist/daemon.js +157 -6
  48. package/dist/daemon.js.map +1 -1
  49. package/dist/delivery-outbox.d.ts +6 -0
  50. package/dist/delivery-outbox.js.map +1 -1
  51. package/dist/fleet-manager.d.ts +48 -0
  52. package/dist/fleet-manager.js +275 -6
  53. package/dist/fleet-manager.js.map +1 -1
  54. package/dist/general-knowledge/skills/backend-providers/SKILL.md +1 -1
  55. package/dist/general-knowledge/skills/credential-profiles/SKILL.md +9 -6
  56. package/dist/general-knowledge/skills/cross-instance-messaging/SKILL.md +11 -2
  57. package/dist/general-knowledge/skills/delegation-playbook/SKILL.md +8 -8
  58. package/dist/general-knowledge/skills/fleet-config/SKILL.md +5 -5
  59. package/dist/general-knowledge/skills/fleet-health/SKILL.md +4 -4
  60. package/dist/general-knowledge/skills/fleet-restart/SKILL.md +13 -13
  61. package/dist/general-knowledge/skills/instance-lifecycle/SKILL.md +3 -2
  62. package/dist/general-knowledge/skills/kiro-engine-migration/SKILL.md +6 -0
  63. package/dist/general-knowledge/skills/model-discovery/SKILL.md +9 -9
  64. package/dist/general-knowledge/skills/multi-channel/SKILL.md +8 -2
  65. package/dist/general-knowledge/skills/scheduling/SKILL.md +5 -3
  66. package/dist/general-knowledge/skills/session-management/SKILL.md +6 -5
  67. package/dist/general-knowledge/skills/tui-effort/SKILL.md +2 -2
  68. package/dist/general-knowledge/skills/worker-collaboration/SKILL.md +4 -3
  69. package/dist/general-knowledge/steering/core-rules.md +8 -7
  70. package/dist/instance-avatar.d.ts +36 -0
  71. package/dist/instance-avatar.js +57 -0
  72. package/dist/instance-avatar.js.map +1 -0
  73. package/dist/instance-config-impact.js +5 -0
  74. package/dist/instance-config-impact.js.map +1 -1
  75. package/dist/instance-lifecycle.d.ts +4 -0
  76. package/dist/instance-lifecycle.js +3 -1
  77. package/dist/instance-lifecycle.js.map +1 -1
  78. package/dist/instructions.d.ts +2 -0
  79. package/dist/instructions.js +10 -7
  80. package/dist/instructions.js.map +1 -1
  81. package/dist/locale.js +36 -26
  82. package/dist/locale.js.map +1 -1
  83. package/dist/next-steps.js +1 -1
  84. package/dist/next-steps.js.map +1 -1
  85. package/dist/outbound-handlers.js +7 -4
  86. package/dist/outbound-handlers.js.map +1 -1
  87. package/dist/outbound-schemas.d.ts +8 -0
  88. package/dist/outbound-schemas.js +15 -5
  89. package/dist/outbound-schemas.js.map +1 -1
  90. package/dist/public-web-gateway.js +3 -0
  91. package/dist/public-web-gateway.js.map +1 -1
  92. package/dist/service-activation.js +4 -3
  93. package/dist/service-activation.js.map +1 -1
  94. package/dist/settings-api.js +71 -21
  95. package/dist/settings-api.js.map +1 -1
  96. package/dist/settings-change.js +70 -1
  97. package/dist/settings-change.js.map +1 -1
  98. package/dist/settings-effect.js +26 -4
  99. package/dist/settings-effect.js.map +1 -1
  100. package/dist/settings-http-confirmation.js +7 -0
  101. package/dist/settings-http-confirmation.js.map +1 -1
  102. package/dist/setup-form.d.ts +1 -1
  103. package/dist/setup-form.js +1 -1
  104. package/dist/status-emojis.d.ts +10 -3
  105. package/dist/status-emojis.js +11 -2
  106. package/dist/status-emojis.js.map +1 -1
  107. package/dist/tips.js +4 -4
  108. package/dist/tips.js.map +1 -1
  109. package/dist/transcript-monitor.d.ts +8 -0
  110. package/dist/transcript-monitor.js +26 -2
  111. package/dist/transcript-monitor.js.map +1 -1
  112. package/dist/transcript-sources.d.ts +54 -0
  113. package/dist/transcript-sources.js +188 -27
  114. package/dist/transcript-sources.js.map +1 -1
  115. package/dist/transcript-turns.d.ts +15 -2
  116. package/dist/transcript-turns.js +15 -2
  117. package/dist/transcript-turns.js.map +1 -1
  118. package/dist/types.d.ts +11 -0
  119. package/dist/ui/chat-emojis.js +33 -0
  120. package/dist/ui/chat-render.js +12 -0
  121. package/dist/ui/chat-thread.js +1 -1
  122. package/dist/ui/panel-chat.js +13 -3
  123. package/dist/ui/panel-needs.js +5 -2
  124. package/dist/ui/panel-settings.js +117 -26
  125. package/dist/ui/settings-dialogs.js +7 -2
  126. package/dist/ui/settings-model.js +2 -1
  127. package/dist/ui/settings-strings.js +38 -2
  128. package/dist/ui/shared/app-i18n.js +2 -2
  129. package/dist/ui/shared/app-route.js +1 -1
  130. package/dist/ui/shared/app.css +29 -1
  131. package/dist/ui/shared/instance-nav.js +77 -8
  132. package/dist/ui/shared/signin.js +7 -7
  133. package/dist/ui/shared/tokens.css +9 -0
  134. package/dist/ui/shared/view-strings.js +2 -2
  135. package/dist/ui/signin.html +1 -1
  136. package/dist/update-install.d.ts +15 -1
  137. package/dist/update-install.js +6 -0
  138. package/dist/update-install.js.map +1 -1
  139. package/dist/update-preflight.d.ts +100 -0
  140. package/dist/update-preflight.js +264 -0
  141. package/dist/update-preflight.js.map +1 -0
  142. package/dist/updater-caller.d.ts +31 -0
  143. package/dist/updater-caller.js +109 -0
  144. package/dist/updater-caller.js.map +1 -0
  145. package/dist/view-api.d.ts +1 -1
  146. package/dist/view-api.js +33 -8
  147. package/dist/view-api.js.map +1 -1
  148. package/dist/wake-coordinator.d.ts +11 -0
  149. package/dist/wake-coordinator.js +38 -4
  150. package/dist/wake-coordinator.js.map +1 -1
  151. package/dist/web-api.d.ts +11 -0
  152. package/dist/web-api.js +54 -0
  153. package/dist/web-api.js.map +1 -1
  154. package/dist/web-auth.d.ts +2 -2
  155. package/dist/web-auth.js +5 -3
  156. package/dist/web-auth.js.map +1 -1
  157. package/dist/web-host-guard.d.ts +3 -11
  158. package/dist/web-host-guard.js +3 -12
  159. package/dist/web-host-guard.js.map +1 -1
  160. package/dist/web-images.d.ts +47 -0
  161. package/dist/web-images.js +169 -0
  162. package/dist/web-images.js.map +1 -0
  163. package/dist/web-session.d.ts +36 -1
  164. package/dist/web-session.js +52 -5
  165. package/dist/web-session.js.map +1 -1
  166. package/dist/web-settings-patch.d.ts +21 -0
  167. package/dist/web-settings-patch.js +74 -0
  168. package/dist/web-settings-patch.js.map +1 -0
  169. package/dist/web-shell-routes.d.ts +1 -1
  170. package/dist/web-shell-routes.js +1 -1
  171. package/dist/web-shell-routes.js.map +1 -1
  172. package/dist/workflow-templates/default.md +1 -1
  173. 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,6 +78,12 @@ 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
89
  /** The outbox evidence for a delivery the CLI's transcript shows taken (#758; #1201 absorbed). */
@@ -1237,6 +1244,11 @@ export class Daemon extends EventEmitter {
1237
1244
  turnReplyGuard = new TurnReplyGuard();
1238
1245
  /** #1510: turn boundaries from the CLI's transcript, for a backend whose guard reads its turn end there. */
1239
1246
  transcriptTurns = new TranscriptTurnLedger();
1247
+ /** #1641: when this launch's transcript monitor last read anything (a turn boundary, a tool, assistant text); performance.now(). */
1248
+ transcriptActivityAt = 0;
1249
+ /** #1641: the launch and composer a stuck pane's idle evidence is about, and since when (performance.now()); see recoverStuck. */
1250
+ stuckRecoveryKey = null;
1251
+ stuckRecoverySince = 0;
1240
1252
  /** #1510: since when (monotonic) this generation has been held as running in the transcript. */
1241
1253
  transcriptRunningSince = null;
1242
1254
  /** Prevent a visible stale XML fragment from being recovered on later turns. */
@@ -1388,6 +1400,8 @@ export class Daemon extends EventEmitter {
1388
1400
  recentPastes = [];
1389
1401
  pendingInstructionsUpdate;
1390
1402
  pendingInstructionsNotice = false;
1403
+ /** #1633: the one-time "what's new for you" notice, pasted before the next real delivery; recorded once pasted. */
1404
+ pendingWhatsNew = null;
1391
1405
  // Whether the warmup steering-reload notice should be injected after spawn.
1392
1406
  // Set in trySpawn by comparing the freshly-built instructions against the
1393
1407
  // last value the agent was told about (prev-instructions). Skipped when
@@ -5041,9 +5055,68 @@ export class Daemon extends EventEmitter {
5041
5055
  buildTranscriptMonitor(source) {
5042
5056
  const monitor = new TranscriptMonitor(this.instanceDir, this.logger, source);
5043
5057
  this.transcriptTurns.reset();
5044
- monitor.on("turns", (turns) => this.transcriptTurns.observe(turns));
5058
+ this.transcriptActivityAt = 0;
5059
+ // Monotonic: a wall-clock correction must neither end nor extend the open-turn hold (#1641 R1).
5060
+ const touch = () => { this.transcriptActivityAt = performance.now(); };
5061
+ monitor.on("turns", (turns) => { this.transcriptTurns.observe(turns, performance.now()); touch(); });
5062
+ // #1641 R2: a re-baselined source starts over, and so does what it said (an open turn read before is withdrawn).
5063
+ monitor.on("reset", () => { if (this.transcriptMonitor === monitor) {
5064
+ this.transcriptTurns.reset();
5065
+ this.transcriptActivityAt = 0;
5066
+ } });
5067
+ for (const event of ["tool_use", "tool_result", "assistant_text"])
5068
+ monitor.on(event, touch);
5045
5069
  return monitor;
5046
5070
  }
5071
+ /**
5072
+ * #1641: what the CLI's own transcript (codex's rollout) says about its turn. `open`: a turn started and has not ended,
5073
+ * and the transcript moved within TRANSCRIPT_OPEN_TURN_QUIET_MS. `closed`: every turn read since this launch's monitor
5074
+ * began has ended. `unknown`: not a transcript backend, nothing followed, or an open turn gone quiet (a missed end).
5075
+ */
5076
+ transcriptTurnState(now = performance.now()) {
5077
+ if (this.backend?.turnEndFromTranscript !== true || this.transcriptMonitor?.following() !== true)
5078
+ return "unknown";
5079
+ // #1641 R2: an empty ledger after a baseline that could not see the current turn's start is not "closed".
5080
+ if (this.transcriptMonitor.turnStateKnown?.() !== true)
5081
+ return "unknown";
5082
+ const since = this.transcriptTurns.openSince();
5083
+ if (since === null)
5084
+ return "closed";
5085
+ return now - Math.max(since, this.transcriptActivityAt) < TRANSCRIPT_OPEN_TURN_QUIET_MS ? "open" : "unknown";
5086
+ }
5087
+ /**
5088
+ * #1641: a stuck pane that is in fact an idle CLI (a footer this build cannot read) must not stay stuck for hours with
5089
+ * every delivery parked behind it. It recovers when the backend sees its own live, EMPTY composer with no busy, queued
5090
+ * or dialog marker (isStableUnknownLayoutIdlePane) AND the transcript shows no open turn — or, with no transcript to
5091
+ * ask, that same composer held unchanged for UNKNOWN_LAYOUT_STABLE_MS (#978's rule). An open turn never recovers.
5092
+ */
5093
+ recoverStuck(pane, snapshot, changeAt, turn) {
5094
+ const machine = this.instanceStateMachine;
5095
+ const idleComposer = this.backend?.isStableUnknownLayoutIdlePane?.(pane) === true;
5096
+ // The resume load (#1031: `Resuming session…` over the same composer) is input that is not open yet (#1641 R1).
5097
+ if (!machine || !idleComposer || turn === "open" || this.inputTransientInPane(pane)) {
5098
+ this.stuckRecoveryKey = null;
5099
+ return snapshot;
5100
+ }
5101
+ // The evidence is about this launch and this screen. Once it holds, it keeps holding — a still pane reads stuck
5102
+ // again at the next sweep (its footer is still unreadable) and must not start the window over (#1641 R1).
5103
+ const mono = performance.now();
5104
+ const key = `${this.spawnGeneration}:${this.launchAttempt}\n${pane.replace(/⋆/gu, "").replace(/[ \t]+$/gmu, "")}`;
5105
+ if (key !== this.stuckRecoveryKey) {
5106
+ this.stuckRecoveryKey = key;
5107
+ this.stuckRecoverySince = mono;
5108
+ }
5109
+ if (turn === "unknown" && mono - this.stuckRecoverySince < UNKNOWN_LAYOUT_STABLE_MS)
5110
+ return snapshot;
5111
+ // A footer this build cannot read stays unreadable: the same still pane reads stuck again at every sweep and is
5112
+ // recovered again. Only the edge into idle is news.
5113
+ const fields = { evidence: turn === "closed" ? "transcript_no_open_turn" : "stable_empty_composer", unchangedForMs: snapshot.unchangedForMs };
5114
+ if (this.instanceState !== "idle")
5115
+ this.logger.info(fields, "Stuck instance recovered — the CLI shows its idle composer and is not running a turn (#1641)");
5116
+ else
5117
+ this.logger.debug(fields, "Still idle on its unreadable footer — the stuck reading was recovered again (#1641)");
5118
+ return machine.observe(pane, Date.now(), { settled: true, changeAt, forceReady: true });
5119
+ }
5047
5120
  replyCompletionGuardEnabled() {
5048
5121
  return this.config.reply_completion_guard !== false
5049
5122
  && this.backend?.replyCompletionGuard === true;
@@ -5425,7 +5498,9 @@ export class Daemon extends EventEmitter {
5425
5498
  // a pair every ~1.6s for as long as the repaint lasted (#1116). Real
5426
5499
  // work cannot hide behind this: it changes the layout, the next
5427
5500
  // output arms another probe, and the debounce capture still fires.
5428
- if (this.instanceState === "idle" && this.backend?.isPeriodicRedrawIdlePane?.(pane) === true)
5501
+ // #1641: never while the transcript shows a turn running — that idle-looking screen is mid-turn.
5502
+ if (this.instanceState === "idle" && this.backend?.isPeriodicRedrawIdlePane?.(pane) === true
5503
+ && this.transcriptTurnState() !== "open")
5429
5504
  return;
5430
5505
  // A second burst overtook the probe. Do not bless its stale capture as
5431
5506
  // idle: continuous output is the safer working signal, while the next
@@ -5458,6 +5533,10 @@ export class Daemon extends EventEmitter {
5458
5533
  // decisions about *now*. The old double-observe expressed that by calling
5459
5534
  // observe twice, which silently disabled the "content moved" branch.
5460
5535
  const structuredPeriodicIdle = this.backend?.isPeriodicRedrawIdlePane;
5536
+ // #1641: the transcript outranks the pane: while it shows the turn running, no reading of this pane is idle (codex
5537
+ // hides its status row while streaming, leaving the idle composer and footer in view mid-turn).
5538
+ const transcriptTurn = this.transcriptTurnState();
5539
+ const transcriptBusy = transcriptTurn === "open";
5461
5540
  let snapshot;
5462
5541
  if (deliveryFallback !== null) {
5463
5542
  // A missing footer cannot fall through to a broad ready match. All
@@ -5466,7 +5545,7 @@ export class Daemon extends EventEmitter {
5466
5545
  settled: true,
5467
5546
  changeAt: observedChangeAt,
5468
5547
  forceReady: deliveryFallback,
5469
- forceBusy: !deliveryFallback || paneActivity !== null,
5548
+ forceBusy: !deliveryFallback || paneActivity !== null || transcriptBusy,
5470
5549
  });
5471
5550
  }
5472
5551
  else if (structuredPeriodicIdle && reason === "output_probe") {
@@ -5488,6 +5567,7 @@ export class Daemon extends EventEmitter {
5488
5567
  snapshot = this.instanceStateMachine.observe(pane, Date.now(), {
5489
5568
  settled: true,
5490
5569
  changeAt: observedChangeAt,
5570
+ forceBusy: transcriptBusy,
5491
5571
  });
5492
5572
  }
5493
5573
  }
@@ -5521,6 +5601,7 @@ export class Daemon extends EventEmitter {
5521
5601
  settled: true,
5522
5602
  changeAt: observedChangeAt,
5523
5603
  forceReady: true,
5604
+ forceBusy: transcriptBusy,
5524
5605
  });
5525
5606
  }
5526
5607
  else {
@@ -5569,6 +5650,7 @@ export class Daemon extends EventEmitter {
5569
5650
  snapshot = this.instanceStateMachine.observe(pane, Date.now(), {
5570
5651
  settled: true,
5571
5652
  changeAt: observedChangeAt,
5653
+ forceBusy: transcriptBusy,
5572
5654
  });
5573
5655
  }
5574
5656
  else {
@@ -5586,9 +5668,11 @@ export class Daemon extends EventEmitter {
5586
5668
  snapshot = this.instanceStateMachine.observe(pane, Date.now(), {
5587
5669
  settled,
5588
5670
  changeAt: observedChangeAt,
5589
- forceBusy: paneActivity !== null,
5671
+ forceBusy: paneActivity !== null || transcriptBusy,
5590
5672
  });
5591
5673
  }
5674
+ if (snapshot.state === "stuck")
5675
+ snapshot = this.recoverStuck(pane, snapshot, observedChangeAt, transcriptTurn);
5592
5676
  this.applyInstanceStateSnapshot(snapshot, pane);
5593
5677
  // Backends without a transcript feed can still say what they are doing, if
5594
5678
  // their TUI names it. Free-riding on a capture that already happened: no
@@ -6011,12 +6095,14 @@ export class Daemon extends EventEmitter {
6011
6095
  * here — a steered message must read EXACTLY like a queued one to the
6012
6096
  * agent, or the two drift apart in what the agent is told about replying.
6013
6097
  */
6098
+ /** #1650: how this instance talks to the fleet — MCP tools, or agend-agent commands (agent_mode: cli). */
6099
+ agentMode() { return this.config.agent_mode === "cli" ? "cli" : "mcp"; }
6014
6100
  formatInboundMessage(content, meta) {
6015
6101
  const user = meta.user || "unknown";
6016
6102
  const fromInstance = meta.from_instance;
6017
6103
  let formatted;
6018
6104
  if (fromInstance) {
6019
- formatted = formatCrossInstanceInboundMessage(content, meta);
6105
+ formatted = formatCrossInstanceInboundMessage(content, meta, this.agentMode());
6020
6106
  }
6021
6107
  else {
6022
6108
  const via = meta.source ? ` via ${meta.source}` : "";
@@ -6029,7 +6115,10 @@ export class Daemon extends EventEmitter {
6029
6115
  formatted = `[Recent reactions: ${meta.pending_reactions}]\n${formatted}`;
6030
6116
  }
6031
6117
  formatted += renderHandoffMetadata(meta);
6032
- formatted += "\n(Reply using the reply tool — do NOT respond with direct text)";
6118
+ // #1650: a CLI-mode agent has no reply tool — it answers with the agend-agent command.
6119
+ formatted += this.agentMode() === "cli"
6120
+ ? '\n(Reply with agend-agent reply "…" — do NOT respond with direct text)'
6121
+ : "\n(Reply using the reply tool — do NOT respond with direct text)";
6033
6122
  }
6034
6123
  if (meta.reply_to_text) {
6035
6124
  formatted += `\n(reply_to: "${meta.reply_to_text}")`;
@@ -6291,6 +6380,23 @@ export class Daemon extends EventEmitter {
6291
6380
  if (!this.isDeliveryEpochCurrent(deliveryEpoch))
6292
6381
  return;
6293
6382
  }
6383
+ const whatsNew = this.pendingWhatsNew ? this.currentWhatsNew(this.pendingWhatsNew) : null;
6384
+ if (whatsNew) {
6385
+ // #1633: here, inside the delivery lock and ahead of a real delivery — never into a busy turn, never to an
6386
+ // instance nobody writes to. Recorded only once pasted: a failed or cancelled paste stays pending.
6387
+ const pasted = await this.deliverMessage(whatsNew.notice, undefined, { deliveryEpoch });
6388
+ if (pasted && this.pendingWhatsNew === whatsNew) {
6389
+ this.pendingWhatsNew = null;
6390
+ try {
6391
+ recordWhatsNew(this.instanceDir, whatsNew.ids, whatsNew.version);
6392
+ }
6393
+ catch (err) {
6394
+ this.logger.warn({ err }, "what's-new record not written — the notice may repeat once");
6395
+ }
6396
+ }
6397
+ if (!this.isDeliveryEpochCurrent(deliveryEpoch))
6398
+ return;
6399
+ }
6294
6400
  const status = channelStatus(meta);
6295
6401
  // A fresh delivery begins a fresh turn — its bubble must not inherit
6296
6402
  // the previous turn's tool list.
@@ -10426,6 +10532,49 @@ export class Daemon extends EventEmitter {
10426
10532
  }
10427
10533
  catch { /* non-fatal */ }
10428
10534
  }
10535
+ /**
10536
+ * #1633 (#1639 review): the pending notice as the record stands NOW, just before the paste — it was decided at launch,
10537
+ * and the record may have changed while the delivery waited. Unreadable → nothing is sent (and nothing pending);
10538
+ * entries recorded meanwhile are dropped from it; none left → nothing.
10539
+ */
10540
+ currentWhatsNew(pending) {
10541
+ const record = readWhatsNewRecord(this.instanceDir);
10542
+ if (record === "unreadable") {
10543
+ this.pendingWhatsNew = null;
10544
+ return null;
10545
+ }
10546
+ const known = new Set(record?.announced ?? []);
10547
+ const ids = pending.ids.filter(id => !known.has(id));
10548
+ if (!ids.length) {
10549
+ this.pendingWhatsNew = null;
10550
+ return null;
10551
+ }
10552
+ if (ids.length === pending.ids.length)
10553
+ return pending;
10554
+ const narrowed = { notice: whatsNewNotice(pending.version, AGENT_WHATS_NEW.filter(e => ids.includes(e.id))), ids, version: pending.version };
10555
+ this.pendingWhatsNew = narrowed;
10556
+ return narrowed;
10557
+ }
10558
+ /** #1633: what this instance has not been told about its AgEnD version yet (see agent-whats-new.ts). */
10559
+ prepareWhatsNew(fresh, version = agendVersion()) {
10560
+ try {
10561
+ const plan = planWhatsNew({ record: readWhatsNewRecord(this.instanceDir), fresh, version });
10562
+ if (!plan) {
10563
+ this.pendingWhatsNew = null;
10564
+ return;
10565
+ }
10566
+ if ("silent" in plan) {
10567
+ recordWhatsNew(this.instanceDir, plan.silent, plan.version);
10568
+ this.pendingWhatsNew = null;
10569
+ return;
10570
+ }
10571
+ this.pendingWhatsNew = plan;
10572
+ this.logger.info({ entries: plan.ids }, "what's-new notice pending for the next delivery");
10573
+ }
10574
+ catch (err) {
10575
+ this.logger.debug({ err }, "what's-new not prepared");
10576
+ }
10577
+ }
10429
10578
  async trySpawnInsideGate(reuseWindow = false, startupTimeoutMs) {
10430
10579
  this.startupAdmission?.();
10431
10580
  const launchFence = this.launchFenceEpoch;
@@ -10435,6 +10584,8 @@ export class Daemon extends EventEmitter {
10435
10584
  // instructionsReloadedOnResume) so the warmup steering-reload can be
10436
10585
  // skipped when nothing changed.
10437
10586
  this.lastBuiltInstructions = backendConfig.instructions ?? "";
10587
+ // #1633: decided before prev-instructions is first written — its absence is what says "never launched before".
10588
+ this.prepareWhatsNew(!existsSync(join(this.instanceDir, "prev-instructions")));
10438
10589
  {
10439
10590
  const prevFile = join(this.instanceDir, "prev-instructions");
10440
10591
  let prev = "";