@songsid/agend 2.3.2 → 2.3.3-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 (136) hide show
  1. package/dist/agent-cli-instructions.md +2 -2
  2. package/dist/agent-whats-new.d.ts +21 -0
  3. package/dist/agent-whats-new.js +52 -23
  4. package/dist/agent-whats-new.js.map +1 -1
  5. package/dist/authority/authority-state.js +1 -1
  6. package/dist/authority/authority-state.js.map +1 -1
  7. package/dist/authority/web-authority.d.ts +9 -4
  8. package/dist/authority/web-authority.js +10 -5
  9. package/dist/authority/web-authority.js.map +1 -1
  10. package/dist/backend/claude-code.d.ts +13 -1
  11. package/dist/backend/claude-code.js +20 -1
  12. package/dist/backend/claude-code.js.map +1 -1
  13. package/dist/backend/codex-session-lookup.d.ts +16 -3
  14. package/dist/backend/codex-session-lookup.js +45 -11
  15. package/dist/backend/codex-session-lookup.js.map +1 -1
  16. package/dist/backend/kiro-reconcile-store.js +8 -1
  17. package/dist/backend/kiro-reconcile-store.js.map +1 -1
  18. package/dist/backend/kiro.d.ts +0 -5
  19. package/dist/backend/kiro.js +0 -10
  20. package/dist/backend/kiro.js.map +1 -1
  21. package/dist/backend/types.d.ts +7 -4
  22. package/dist/backend/types.js.map +1 -1
  23. package/dist/cli.js +18 -3
  24. package/dist/cli.js.map +1 -1
  25. package/dist/config-validator.js +4 -5
  26. package/dist/config-validator.js.map +1 -1
  27. package/dist/config.js +4 -2
  28. package/dist/config.js.map +1 -1
  29. package/dist/cross-instance-envelope.d.ts +10 -5
  30. package/dist/cross-instance-envelope.js +20 -17
  31. package/dist/cross-instance-envelope.js.map +1 -1
  32. package/dist/daemon.d.ts +90 -63
  33. package/dist/daemon.js +412 -317
  34. package/dist/daemon.js.map +1 -1
  35. package/dist/daily-summary.d.ts +8 -1
  36. package/dist/daily-summary.js +8 -5
  37. package/dist/daily-summary.js.map +1 -1
  38. package/dist/delivery-outbox.d.ts +31 -22
  39. package/dist/delivery-outbox.js +119 -161
  40. package/dist/delivery-outbox.js.map +1 -1
  41. package/dist/delivery-outbox.test.js +10 -25
  42. package/dist/delivery-outbox.test.js.map +1 -1
  43. package/dist/fleet-level-config.js +1 -1
  44. package/dist/fleet-level-config.js.map +1 -1
  45. package/dist/fleet-manager.d.ts +4 -21
  46. package/dist/fleet-manager.js +38 -159
  47. package/dist/fleet-manager.js.map +1 -1
  48. package/dist/fleet-service-owner.d.ts +11 -0
  49. package/dist/fleet-service-owner.js +3 -0
  50. package/dist/fleet-service-owner.js.map +1 -0
  51. package/dist/general-knowledge/skills/delegation-playbook/SKILL.md +1 -1
  52. package/dist/general-knowledge/skills/fleet-restart/SKILL.md +1 -1
  53. package/dist/general-knowledge/skills/session-management/SKILL.md +1 -1
  54. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +4 -5
  55. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +4 -5
  56. package/dist/instance-lifecycle.js +23 -5
  57. package/dist/instance-lifecycle.js.map +1 -1
  58. package/dist/instructions.js +6 -7
  59. package/dist/instructions.js.map +1 -1
  60. package/dist/kiro-reconcile.js +3 -3
  61. package/dist/kiro-reconcile.js.map +1 -1
  62. package/dist/locale.js +8 -52
  63. package/dist/locale.js.map +1 -1
  64. package/dist/log-service.js +1 -1
  65. package/dist/log-service.js.map +1 -1
  66. package/dist/needs-you-hub.js +4 -1
  67. package/dist/needs-you-hub.js.map +1 -1
  68. package/dist/needs-you-live.d.ts +3 -1
  69. package/dist/needs-you-live.js +24 -2
  70. package/dist/needs-you-live.js.map +1 -1
  71. package/dist/needs-you.d.ts +6 -1
  72. package/dist/needs-you.js +4 -0
  73. package/dist/needs-you.js.map +1 -1
  74. package/dist/outbound-handlers.js +1 -1
  75. package/dist/outbound-handlers.js.map +1 -1
  76. package/dist/outbound-schemas.js +1 -1
  77. package/dist/outbound-schemas.js.map +1 -1
  78. package/dist/scheduler/task-board.d.ts +17 -25
  79. package/dist/scheduler/task-board.js +29 -39
  80. package/dist/scheduler/task-board.js.map +1 -1
  81. package/dist/sd-notify.d.ts +13 -14
  82. package/dist/sd-notify.js +62 -28
  83. package/dist/sd-notify.js.map +1 -1
  84. package/dist/settings-api.d.ts +0 -1
  85. package/dist/settings-api.js +1 -14
  86. package/dist/settings-api.js.map +1 -1
  87. package/dist/settings-effect.js +0 -2
  88. package/dist/settings-effect.js.map +1 -1
  89. package/dist/setup-wizard.d.ts +3 -0
  90. package/dist/setup-wizard.js +4 -2
  91. package/dist/setup-wizard.js.map +1 -1
  92. package/dist/task-board-waker.d.ts +40 -1
  93. package/dist/task-board-waker.js +110 -9
  94. package/dist/task-board-waker.js.map +1 -1
  95. package/dist/tmux-control.d.ts +1 -1
  96. package/dist/tmux-control.js +3 -2
  97. package/dist/tmux-control.js.map +1 -1
  98. package/dist/tmux-manager.js +18 -16
  99. package/dist/tmux-manager.js.map +1 -1
  100. package/dist/tmux-read.d.ts +2 -0
  101. package/dist/tmux-read.js +7 -1
  102. package/dist/tmux-read.js.map +1 -1
  103. package/dist/topic-commands.d.ts +2 -0
  104. package/dist/topic-commands.js +2 -1
  105. package/dist/topic-commands.js.map +1 -1
  106. package/dist/transcript-monitor.js +17 -0
  107. package/dist/transcript-monitor.js.map +1 -1
  108. package/dist/types.d.ts +7 -10
  109. package/dist/ui/panel-log.js +2 -2
  110. package/dist/ui/settings-appearance.js +3 -35
  111. package/dist/ui/settings-registry.js +0 -1
  112. package/dist/ui/settings-strings.js +4 -4
  113. package/dist/ui/settings-web-authority.js +4 -3
  114. package/dist/ui/shared/app-i18n.js +2 -2
  115. package/dist/update-check.d.ts +13 -4
  116. package/dist/update-check.js +17 -4
  117. package/dist/update-check.js.map +1 -1
  118. package/dist/web-experimental.d.ts +6 -2
  119. package/dist/web-experimental.js +8 -4
  120. package/dist/web-experimental.js.map +1 -1
  121. package/dist/web-settings-patch.d.ts +0 -1
  122. package/dist/web-settings-patch.js +3 -4
  123. package/dist/web-settings-patch.js.map +1 -1
  124. package/dist/workflow-templates/default.md +2 -2
  125. package/dist/working-recovery.d.ts +0 -2
  126. package/dist/working-recovery.js +2 -23
  127. package/dist/working-recovery.js.map +1 -1
  128. package/launcher/agend-install.cjs +7 -1
  129. package/launcher/fleet-service-owner.cjs +138 -0
  130. package/package.json +1 -1
  131. package/dist/task-board-summary.d.ts +0 -101
  132. package/dist/task-board-summary.js +0 -177
  133. package/dist/task-board-summary.js.map +0 -1
  134. package/dist/upgrade-notices.d.ts +0 -17
  135. package/dist/upgrade-notices.js +0 -93
  136. package/dist/upgrade-notices.js.map +0 -1
@@ -362,8 +362,6 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
362
362
  costGuard: CostGuard | null;
363
363
  private statuslineWatcher;
364
364
  private dailySummary;
365
- /** #2246 step 3: the task board's daily summary for the person (task-board-summary.ts). */
366
- private taskBoardSummary;
367
365
  private dailyTipScheduler;
368
366
  /** The task board's wakes (step 2): an instance hears when someone else changes a card it owns or handed out. */
369
367
  private taskBoardWaker;
@@ -965,9 +963,8 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
965
963
  private listenForWorkingState;
966
964
  private observeWorkingState;
967
965
  /**
968
- * #926: the two reply-obligation safety nets. Idle owners with an unanswered
969
- * request get one reminder (after the grace); requesters get one overdue
970
- * notice when the owner is not working and nothing came back in time.
966
+ * #926: idle owners with an unanswered request get one reminder (after the grace). #2802: the only reply notice —
967
+ * requesters are no longer told about overdue or expired asks (they can check delivery_status / describe_instance).
971
968
  */
972
969
  sweepReplyObligations(now?: Date): void;
973
970
  private expireReplyObligations;
@@ -2289,13 +2286,6 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
2289
2286
  * their fleet is organised. Both stay theirs — this only makes sure they are
2290
2287
  * not discovered by an agent failing at three in the morning.
2291
2288
  */
2292
- /**
2293
- * #1366: the first time a fleet runs 2.2 or later, tell each chat platform's General — once — that its agents can
2294
- * now be talked to from a browser. Claimed in upgrade-notices.json before the send, released if the send fails.
2295
- * Platforms with nowhere to post fleet notices (no group, or a Discord fleet with no General channel) are skipped
2296
- * and not recorded, so they are told once they have one.
2297
- */
2298
- announceWebChatOnce(version: string): Promise<void>;
2299
2289
  private announceToolPermissionsChange;
2300
2290
  private checkToolPermission;
2301
2291
  private handleOutboundFromInstance;
@@ -2847,13 +2837,6 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
2847
2837
  * should say so rather than send into a guaranteed failure.
2848
2838
  */
2849
2839
  private fleetNoticeTarget;
2850
- /**
2851
- * #2246 step 3: the task board's daily summary of [since, until) (task-board-summary.ts). Each chat world's General
2852
- * gets its own instances' cards — a card that no world's instance holds (nobody's, a person's) goes to the primary
2853
- * one — and each of that world's Generals shows the same text in the web chat. A world with nothing to say gets
2854
- * nothing. A web-only fleet: everything, in the web chat.
2855
- */
2856
- private postTaskBoardSummary;
2857
2840
  /**
2858
2841
  * Post the daily summary where fleet-wide notices go.
2859
2842
  *
@@ -3673,8 +3656,8 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
3673
3656
  * The task board's live side: every committed change goes to the open pages (never to a guest) — the board's new
3674
3657
  * revision and the card numbers it touched; not who touched them (the page does not need it, and a public-link page
3675
3658
  * must not learn a hidden instance's name from it). Its news goes to the instances whose cards someone else changed:
3676
- * one "[Task board]" notice per instance, gathered for 10 s, held by the delivery outbox until it is taken
3677
- * (task-board-waker.ts).
3659
+ * one "[Task board]" notice per instance, gathered for 10 s, held by the delivery outbox until it is taken, or
3660
+ * withdrawn once its cards changed again (#2801; task-board-waker.ts).
3678
3661
  */
3679
3662
  private attachTaskBoard;
3680
3663
  /** A task board notice, as AgEnD's own (not a person's words): through the delivery outbox, held until delivered. */
@@ -52,7 +52,7 @@ import { applyKiroMigration, kiroMigrationPrompt, planKiroMigration } from "./ki
52
52
  import { fleetLabel } from "./fleet-label.js";
53
53
  import { beginFullRestartProgress as persistFullRestartProgress, beginUpdateProgress as persistUpdateProgress, clearUpdateMarker, isUpdateInProgress, readUpdateProgress, sameUpdateMarker, setUpdateProgressStage, updateMarkerIdentity, updateProgressOperation, updateReceiptOf, } from "./update-marker.js";
54
54
  import { formatUpdateProgress } from "./update-progress.js";
55
- import { sdNotify, sdNotifyBlocking, setSdNotifyWarn } from "./sd-notify.js";
55
+ import { sdNotify, sdNotifyBlocking, setSdNotifyWarn, startSdWatchdog } from "./sd-notify.js";
56
56
  import { StartupMemoryDeadline, fleetStopConcurrency } from "./startup-memory-deadline.js";
57
57
  import { readFleetMemory } from "./process-memory.js";
58
58
  import { MemoryPressure } from "./memory-pressure.js";
@@ -94,13 +94,12 @@ import { processAttachments } from "./channel/attachment-handler.js";
94
94
  import { routeToolCall } from "./channel/tool-router.js";
95
95
  import { Scheduler } from "./scheduler/index.js";
96
96
  import { boardActivity, boardCardLimit, boardToolAction } from "./task-board-tool.js";
97
- import { TaskBoardWaker } from "./task-board-waker.js";
97
+ import { TaskBoardWaker, namesCard } from "./task-board-waker.js";
98
98
  import { TaskBoardError, waitsForPerson, wrapUpGap } from "./scheduler/task-board.js";
99
99
  import { escapeTelegramHtml, scheduleClock, scheduleRetryLabel } from "./scheduler/retry-label.js";
100
100
  import { DEFAULT_SCHEDULER_CONFIG } from "./scheduler/index.js";
101
101
  import { TopicCommands, saveCommandForBackend, parseSaveFilename, parsePauseWakeCommand, parseCompactCommand, SAVE_FILENAME_RE, resolveInstanceContext, forgetInstanceContext, readStatuslineJson, statuslineModel } from "./topic-commands.js";
102
102
  import { DailySummary } from "./daily-summary.js";
103
- import { TaskBoardSummaryClock, buildSummary, isQuiet, renderSummary, summaryMarkFile, summaryTime } from "./task-board-summary.js";
104
103
  import { WebhookEmitter } from "./webhook-emitter.js";
105
104
  import { TmuxControlClient } from "./tmux-control.js";
106
105
  import { safeHandler } from "./safe-async.js";
@@ -254,7 +253,6 @@ import { isSetupComplete, markSetupComplete } from "./setup-marker.js";
254
253
  import { manualCleanupMessage, reapStaleTunnel } from "./tunnel/lease.js";
255
254
  import { buildToolPermissionsNotice } from "./tool-permissions-notice.js";
256
255
  import { NeedsYouHub } from "./needs-you-hub.js";
257
- import { WEB_CHAT_NOTICE, claimNotice, hasWebChat, releaseNotice, upgradeNoticesPath } from "./upgrade-notices.js";
258
256
  import { GENERAL_NAME, GENERAL_PAUSE_ERROR, adoptGeneralByName, freeGeneralName, isGeneralInstance } from "./general-instance.js";
259
257
  import { buildOrgChart } from "./web-org.js";
260
258
  import { topMemoryHogs } from "./memory-hogs.js";
@@ -612,7 +610,6 @@ export function deriveSpawnConcurrency(freeMemMB, cores) {
612
610
  /** #926: how often reply obligations are checked, the owner-reminder grace, and the default overdue window. */
613
611
  const REPLY_OBLIGATION_SWEEP_MS = 30_000;
614
612
  const REPLY_REMINDER_GRACE_MS = 60_000;
615
- const DEFAULT_REPLY_OVERDUE_MINUTES = 15;
616
613
  /**
617
614
  * The filter `list_emojis` and `list_stickers` take (#1226). Values may come from the agent CLI as strings, so a
618
615
  * limit is read as a number and the flags accept "true". No limit means all of them — never zero.
@@ -676,32 +673,6 @@ export class FleetManager {
676
673
  laneFree: name => !this.isInstanceRestarting(name) && !this.activeDurableTargets.has(name)
677
674
  && outbox.countForTarget(name, ["queued", "retry_wait", "delivering", "submission_started", "reconciliation_pending"]) === 0,
678
675
  admit: (episode, tasks, advisory) => outbox.admitWorkingRecovery(episode, buildWorkingRecoveryPrompt(tasks, this.workingRecoveryCause, advisory), advisory),
679
- summary: (group, rows) => {
680
- const general = this.findGeneralInstance();
681
- if (!general || !this.daemons.has(general))
682
- return;
683
- // Our own informational notice must not supersede General's recovery
684
- // as unrelated pending work before it crosses the input boundary.
685
- const generalEpisode = store.get(general);
686
- if (generalEpisode?.outcome === "admitted" && generalEpisode.deliveryId) {
687
- const state = outbox.get(generalEpisode.deliveryId)?.state;
688
- if (!state || !["delivered", "uncertain", "failed", "cancelled"].includes(state))
689
- return;
690
- }
691
- const deferredAdvisories = rows.filter(row => row.outcome === "deferred"
692
- && (this.daemons.get(row.instance)?.getWorkingRecoveryState?.().background ?? null) === null)
693
- .map(row => `${row.instance} (${row.reason})`);
694
- const posted = store.summary(group, () => outbox.admitSystemNotice(general, `working-recovery-summary:${group}`, `[system:working-recovery-summary] Observed cause: unknown interruption${this.workingRecoveryCause === "unknown_interruption" ? "" : `; prior requested action: ${this.workingRecoveryCause}`}. ${rows.length} interrupted instances; continuation queued ${rows.filter(r => r.outcome === "admitted").length}, excluded ${rows.filter(r => r.outcome === "excluded").length}, still deferred ${rows.filter(r => r.outcome === "deferred").length}. ${rows.filter(r => r.reason === "advisory_background_unknown").length ? `Advisory, background unknown: ${rows.filter(r => r.reason === "advisory_background_unknown").map(r => r.instance).join(", ")}. ` : ""}${deferredAdvisories.length ? `Deferred advisory candidates (background unknown; all other gates still apply): ${deferredAdvisories.join(", ")}. ` : ""}Details are in the Log. Deferred work remains eligible; no action or confirmation is requested.`));
695
- if (posted) {
696
- const counts = { interrupted: rows.length, queued: rows.filter(row => row.outcome === "admitted").length,
697
- excluded: rows.filter(row => row.outcome === "excluded").length, deferred: rows.filter(row => row.outcome === "deferred").length };
698
- const requestedAction = this.workingRecoveryCause === "unknown_interruption" ? null : this.workingRecoveryCause;
699
- this.logger.info({ group, ...counts, observedCause: "unknown_interruption", requestedAction }, "Interrupted work recovery summary queued for General");
700
- this.eventLog?.insert(general, "working_state", { reason: "recovery_summary", group, ...counts,
701
- observedCause: "unknown_interruption", requestedAction,
702
- message: `Interrupted work summary queued for General: ${counts.interrupted} interrupted; ${counts.queued} queued, ${counts.excluded} excluded, ${counts.deferred} deferred.` });
703
- }
704
- },
705
676
  log: (episode, outcome, reason) => {
706
677
  const requestedAction = this.workingRecoveryCause === "unknown_interruption" ? null : this.workingRecoveryCause;
707
678
  this.logger.info({ name: episode.instance, episode: episode.key, outcome, reason,
@@ -825,8 +796,6 @@ export class FleetManager {
825
796
  costGuard = null;
826
797
  statuslineWatcher;
827
798
  dailySummary = null;
828
- /** #2246 step 3: the task board's daily summary for the person (task-board-summary.ts). */
829
- taskBoardSummary = null;
830
799
  dailyTipScheduler = null;
831
800
  /** The task board's wakes (step 2): an instance hears when someone else changes a card it owns or handed out. */
832
801
  taskBoardWaker = null;
@@ -2436,12 +2405,10 @@ export class FleetManager {
2436
2405
  const notice = silent
2437
2406
  ? t(`delivery.fleet_${row.state}_silent_schedule`, row.targetInstance, row.operationId, row.deliveryId, row.lastError ?? "unknown")
2438
2407
  : t(`delivery.fleet_${row.state}`, row.targetInstance, row.operationId, row.deliveryId);
2408
+ // #2804: log-only. The person sees a failed or unconfirmed delivery as a Needs-you item (with Acknowledge),
2409
+ // not as a separate General post; the sending agent still gets its delivery-outcome notice.
2439
2410
  this.logger.error({ deliveryId: row.deliveryId, operationId: row.operationId, target: row.targetInstance, state: row.state,
2440
2411
  cause: row.lastError, ...(silent ? { silentSchedule: true } : {}) }, notice);
2441
- // A self-scheduled silent heartbeat stays log-only, including the
2442
- // operator alert; another source still receives the normal notice.
2443
- if (!silent || row.sourceInstance !== row.targetInstance)
2444
- this.notifyFleetError(notice);
2445
2412
  }
2446
2413
  }
2447
2414
  });
@@ -2473,8 +2440,9 @@ export class FleetManager {
2473
2440
  if (event.count)
2474
2441
  this.needsYou?.poke(); // #1386: rows that expired failed — they wait on someone now
2475
2442
  this.wakeCoordinator?.kick();
2443
+ // #2804: the expired rows are Needs-you items now; the count is logged, not posted to General.
2476
2444
  if (event.count)
2477
- this.notifyFleetError(t("delivery.expired", event.count, event.uncertain ?? 0));
2445
+ this.logger.warn({ count: event.count, uncertain: event.uncertain ?? 0 }, t("delivery.expired", event.count, event.uncertain ?? 0));
2478
2446
  });
2479
2447
  this.scheduleDeliveryOutboxPump();
2480
2448
  }
@@ -2604,27 +2572,8 @@ export class FleetManager {
2604
2572
  const daemon = this.daemons.get(name);
2605
2573
  if (daemon?.bootId === daemonBootId)
2606
2574
  this.listenForWorkingState(name, daemon);
2607
- const outbox = this.deliveryOutbox;
2608
- if (!outbox)
2609
- return;
2610
- for (const prior of outbox.getUnansweredAccepted(name, daemonBootId)) {
2611
- try {
2612
- outbox.admitPostRestartOutcomeNotice(prior, daemonBootId);
2613
- }
2614
- catch (err) {
2615
- this.logger.error({ err, source: name, operationId: prior.operationId }, "Could not persist post-restart delivery outcome notice");
2616
- }
2617
- }
2618
- // #926: a restart kills the turn that was working on a request, and a
2619
- // resumed CLI does not pick it back up. Remind the owner once per restart.
2620
- try {
2621
- const now = new Date();
2622
- this.expireReplyObligations(now);
2623
- outbox.remindReplyObligations(name, { reason: "restart", graceMs: 0, now, since: now, restartId: daemonBootId });
2624
- }
2625
- catch (err) {
2626
- this.logger.error({ err, owner: name }, "Could not persist the post-restart reply reminder");
2627
- }
2575
+ // #2802: no post-restart notices. The outbox still delivers or fails a row accepted before the restart, and an open
2576
+ // request is reminded by the ordinary turn-ended nudge (sweepReplyObligations) once the owner is idle.
2628
2577
  this.scheduleDeliveryOutboxPump();
2629
2578
  }
2630
2579
  workingStateListeners;
@@ -2708,9 +2657,8 @@ export class FleetManager {
2708
2657
  }
2709
2658
  }
2710
2659
  /**
2711
- * #926: the two reply-obligation safety nets. Idle owners with an unanswered
2712
- * request get one reminder (after the grace); requesters get one overdue
2713
- * notice when the owner is not working and nothing came back in time.
2660
+ * #926: idle owners with an unanswered request get one reminder (after the grace). #2802: the only reply notice —
2661
+ * requesters are no longer told about overdue or expired asks (they can check delivery_status / describe_instance).
2714
2662
  */
2715
2663
  sweepReplyObligations(now = new Date()) {
2716
2664
  const outbox = this.deliveryOutbox;
@@ -2721,10 +2669,8 @@ export class FleetManager {
2721
2669
  this.expireReplyObligations(now);
2722
2670
  for (const name of this.daemons.keys()) {
2723
2671
  if (stateOf(name) === "idle")
2724
- outbox.remindReplyObligations(name, { now, graceMs: REPLY_REMINDER_GRACE_MS, reason: "turn-ended" });
2672
+ outbox.remindReplyObligations(name, { now, graceMs: REPLY_REMINDER_GRACE_MS });
2725
2673
  }
2726
- const minutes = this.fleetConfig?.defaults?.reply_overdue_minutes ?? DEFAULT_REPLY_OVERDUE_MINUTES;
2727
- outbox.notifyOverdueReplyObligations({ now, overdueMs: minutes * 60_000, ownerIdle: name => stateOf(name) !== "working" });
2728
2674
  }
2729
2675
  catch (err) {
2730
2676
  this.logger.error({ err }, "Reply-obligation sweep failed");
@@ -2981,7 +2927,9 @@ export class FleetManager {
2981
2927
  if ((current?.state === "delivering" || current?.state === "submission_started")
2982
2928
  && current.targetDaemonBootId === claimed.targetDaemonBootId
2983
2929
  && current.managerBootId === claimed.managerBootId) {
2984
- this.notifyFleetError(t("delivery.lane_waiting", claimed.targetInstance, Math.round(DURABLE_DELIVERY_LANE_ALERT_MS / 60_000), deliveryId));
2930
+ // #2804: logged, not posted. A delivery still in flight needs nobody yet; if it ends failed or unconfirmed it
2931
+ // becomes a Needs-you item.
2932
+ this.logger.warn({ deliveryId, target: claimed.targetInstance }, t("delivery.lane_waiting", claimed.targetInstance, Math.round(DURABLE_DELIVERY_LANE_ALERT_MS / 60_000), deliveryId));
2985
2933
  }
2986
2934
  }, DURABLE_DELIVERY_LANE_ALERT_MS);
2987
2935
  alertTimer.unref?.();
@@ -8170,7 +8118,7 @@ export class FleetManager {
8170
8118
  setSdNotifyWarn((fields, message) => this.logger.warn(fields, message));
8171
8119
  if (this.watchdogTimer)
8172
8120
  clearInterval(this.watchdogTimer);
8173
- this.watchdogTimer = setInterval(() => sdNotify("WATCHDOG=1"), 30_000);
8121
+ this.watchdogTimer = startSdWatchdog();
8174
8122
  this.eventLoopWatch?.stop();
8175
8123
  this.eventLoopWatch = startEventLoopWatch({ logger: this.logger });
8176
8124
  try {
@@ -8346,7 +8294,8 @@ export class FleetManager {
8346
8294
  ...fleet.defaults.daily_summary,
8347
8295
  };
8348
8296
  this.dailySummary = new DailySummary(summaryConfig, costGuardConfig.timezone, (text) => {
8349
- this.postDailySummary(text);
8297
+ if (text !== null)
8298
+ this.postDailySummary(text);
8350
8299
  // Rotate classic channel chat logs daily
8351
8300
  this.classicChannels?.rotateLogs();
8352
8301
  this.rotateInboxes();
@@ -8364,17 +8313,6 @@ export class FleetManager {
8364
8313
  return DailySummary.generateText(this.eventLog, instances, costMap, this.costGuard?.getFleetTotalCents() ?? 0);
8365
8314
  });
8366
8315
  this.dailySummary.start();
8367
- // #2246 step 3: the task board's daily summary — its own time (defaults.task_board.summary_at, read at every look),
8368
- // in the fleet's zone; a summary missed while the fleet was down goes out when it is back.
8369
- this.taskBoardSummary = new TaskBoardSummaryClock({
8370
- at: () => summaryTime(this.fleetConfig?.defaults?.task_board?.summary_at) ?? null,
8371
- zone: () => this.usageZone(),
8372
- now: () => new Date(),
8373
- mark: summaryMarkFile(join(this.dataDir, "task-board-summary.json")),
8374
- send: (since, until) => this.postTaskBoardSummary(since, until),
8375
- failed: (err) => this.logger.warn({ err }, "The task board's daily summary failed; the next minute tries again"),
8376
- });
8377
- this.taskBoardSummary.start();
8378
8316
  // Rotate classic channel chat logs daily (piggyback on daily summary timer)
8379
8317
  this.classicChannels?.rotateLogs();
8380
8318
  this.rotateInboxes();
@@ -8734,8 +8672,6 @@ export class FleetManager {
8734
8672
  }
8735
8673
  await this.sendFleetStartCompletionFallback(String(fleet.channel.group_id), text, generalThreadId != null ? String(generalThreadId) : undefined);
8736
8674
  }
8737
- // After "fleet ready", so the notice is not the first thing people see of a restart.
8738
- void this.announceWebChatOnce(agendVersion);
8739
8675
  // #named-link-ttl: a named link with auto_open comes back now — after the web listener and the chat connections.
8740
8676
  this.startPublicLinkAutoOpen();
8741
8677
  }
@@ -10870,34 +10806,6 @@ export class FleetManager {
10870
10806
  * their fleet is organised. Both stay theirs — this only makes sure they are
10871
10807
  * not discovered by an agent failing at three in the morning.
10872
10808
  */
10873
- /**
10874
- * #1366: the first time a fleet runs 2.2 or later, tell each chat platform's General — once — that its agents can
10875
- * now be talked to from a browser. Claimed in upgrade-notices.json before the send, released if the send fails.
10876
- * Platforms with nowhere to post fleet notices (no group, or a Discord fleet with no General channel) are skipped
10877
- * and not recorded, so they are told once they have one.
10878
- */
10879
- async announceWebChatOnce(version) {
10880
- if (!hasWebChat(version))
10881
- return;
10882
- const path = upgradeNoticesPath(this.dataDir);
10883
- for (const [adapterId, world] of this.worlds) {
10884
- if (this.shuttingDown)
10885
- return;
10886
- const target = this.fleetNoticeTarget(adapterId);
10887
- if (!target)
10888
- continue;
10889
- if (!claimNotice(path, WEB_CHAT_NOTICE, adapterId))
10890
- continue;
10891
- try {
10892
- await world.adapter.sendText(target.chatId, t("upgrade.web_chat"), target.opts);
10893
- this.logger.info({ adapterId, notice: WEB_CHAT_NOTICE }, "Announced web chat in General");
10894
- }
10895
- catch (err) {
10896
- const released = releaseNotice(path, WEB_CHAT_NOTICE, adapterId);
10897
- this.logger.warn({ err, adapterId, released }, "Could not announce web chat in General — will try at the next start");
10898
- }
10899
- }
10900
- }
10901
10809
  announceToolPermissionsChange() {
10902
10810
  try {
10903
10811
  const thirtyDaysAgo = new Date(Date.now() - 30 * 864e5).toISOString().slice(0, 19).replace("T", " ");
@@ -14392,51 +14300,6 @@ export class FleetManager {
14392
14300
  }
14393
14301
  return { chatId, opts: {} };
14394
14302
  }
14395
- /**
14396
- * #2246 step 3: the task board's daily summary of [since, until) (task-board-summary.ts). Each chat world's General
14397
- * gets its own instances' cards — a card that no world's instance holds (nobody's, a person's) goes to the primary
14398
- * one — and each of that world's Generals shows the same text in the web chat. A world with nothing to say gets
14399
- * nothing. A web-only fleet: everything, in the web chat.
14400
- */
14401
- postTaskBoardSummary(since, until) {
14402
- const board = this.scheduler?.db.board;
14403
- if (!board)
14404
- return;
14405
- const read = board.summaryRead(since.toISOString(), until.toISOString(), this.dispatcherGone);
14406
- const projects = board.listProjects(true);
14407
- const stats = board.projectStats(this.dispatcherGone);
14408
- const names = new Map(projects.map((p) => [p.id, p.name]));
14409
- const configured = this.getPrimaryAdapterId();
14410
- const primary = configured !== undefined && this.worlds.has(configured) ? configured : this.worlds.keys().next().value;
14411
- const worldOf = (assignee) => {
14412
- const owner = assignee ? this.getInstanceAdapterId(assignee) : undefined;
14413
- return owner !== undefined && this.worlds.has(owner) ? owner : primary;
14414
- };
14415
- const zone = this.usageZone();
14416
- for (const world of this.worlds.size ? [...this.worlds.keys()] : [undefined]) {
14417
- const summary = buildSummary(read, since, until, { inChat: (assignee) => worldOf(assignee) === world, projects, stats });
14418
- if (isQuiet(summary))
14419
- continue;
14420
- const text = renderSummary(summary, { zone, t: (key, ...args) => t(key, ...args), projectOf: (c) => (c.projectId === null ? null : names.get(c.projectId) ?? null) });
14421
- const cards = [...new Set([...summary.review, ...summary.blocked, ...summary.done, ...summary.created].map((c) => c.code))];
14422
- if (world !== undefined) {
14423
- const adapter = this.worlds.get(world)?.adapter;
14424
- const target = this.fleetNoticeTarget(world);
14425
- if (adapter && target) {
14426
- adapter.sendText(target.chatId, text, target.opts).catch(e => this.logger.warn({ err: e, world }, "Failed to send the task board's daily summary"));
14427
- }
14428
- else {
14429
- this.logger.warn({ world }, "The task board's daily summary has no postable target — set a General topic or channel.options.general_channel_id");
14430
- }
14431
- }
14432
- for (const [name, cfg] of Object.entries(this.fleetConfig?.instances ?? {})) {
14433
- if (cfg.general_topic !== true || (world !== undefined && this.getInstanceAdapterId(name) !== world))
14434
- continue;
14435
- // #2341 review: with the cards it names, so the public link withholds it while one of them is hidden there.
14436
- this.emitSseEvent("message", { instance: name, sender: name, role: "status", text, ts: new Date().toISOString(), cards });
14437
- }
14438
- }
14439
- }
14440
14303
  /**
14441
14304
  * Post the daily summary where fleet-wide notices go.
14442
14305
  *
@@ -15146,6 +15009,7 @@ export class FleetManager {
15146
15009
  pauseReason, pausedAt,
15147
15010
  crashedAt: crashed ? (this.needsCrashedAt.get(name) ?? null) : null,
15148
15011
  signIn: pauseReason === "auth" ? this.signInFor(name) : null,
15012
+ usageHold: this.daemons.get(name)?.usageHold?.() ?? null,
15149
15013
  });
15150
15014
  }
15151
15015
  return out;
@@ -18321,8 +18185,8 @@ export class FleetManager {
18321
18185
  * The task board's live side: every committed change goes to the open pages (never to a guest) — the board's new
18322
18186
  * revision and the card numbers it touched; not who touched them (the page does not need it, and a public-link page
18323
18187
  * must not learn a hidden instance's name from it). Its news goes to the instances whose cards someone else changed:
18324
- * one "[Task board]" notice per instance, gathered for 10 s, held by the delivery outbox until it is taken
18325
- * (task-board-waker.ts).
18188
+ * one "[Task board]" notice per instance, gathered for 10 s, held by the delivery outbox until it is taken, or
18189
+ * withdrawn once its cards changed again (#2801; task-board-waker.ts).
18326
18190
  */
18327
18191
  attachTaskBoard(db) {
18328
18192
  this.taskBoardWaker?.stop();
@@ -18331,6 +18195,19 @@ export class FleetManager {
18331
18195
  const waker = this.taskBoardWaker = new TaskBoardWaker({
18332
18196
  deliver: (instance, message, key) => this.admitTaskBoardNotice(instance, message, key),
18333
18197
  isInstance: (name) => this.fleetConfig?.instances?.[name] != null,
18198
+ // #2785 (c), #2801: the maker's own message already told it; the board's line would repeat it.
18199
+ messagedAbout: (from, to, codes, sinceMs) => {
18200
+ const outbox = this.deliveryOutbox;
18201
+ if (!outbox?.isOpen)
18202
+ return false;
18203
+ return outbox.recentTextsFrom(to, from, new Date(sinceMs).toISOString()).some((text) => codes.some((code) => namesCard(text, code)));
18204
+ },
18205
+ // #2801: a notice still waiting whose cards have all changed again is not delivered.
18206
+ withdraw: (prefix, stale, reason) => {
18207
+ const outbox = this.deliveryOutbox;
18208
+ return outbox?.isOpen ? outbox.withdrawWaitingNotices(prefix, stale, reason).length : 0;
18209
+ },
18210
+ changedAt: (uuid) => db.board.lastChangeAt(uuid),
18334
18211
  holds: (wake) => db.board.wakeHolds(wake),
18335
18212
  card: (uuid) => db.board.findCard(uuid),
18336
18213
  // #2352: the card's Needs you buttons are in this instance's own channel (it is the card's lane).
@@ -18347,12 +18224,13 @@ export class FleetManager {
18347
18224
  },
18348
18225
  log: (level, message, extra) => this.logger[level](extra ?? {}, message),
18349
18226
  });
18350
- db.board.onChange(({ rev, ids, projects, wakes }) => {
18227
+ db.board.onChange(({ rev, ids, projects, wakes, changed }) => {
18351
18228
  // Refresh both old and new owners on reassignment; wakes are not an ownership index.
18352
18229
  if (ids.length)
18353
18230
  for (const name of this.daemons.keys())
18354
18231
  this.observeWorkingState(name, "tasks");
18355
18232
  this.emitSseEvent("tasks", { rev, ids, projects });
18233
+ waker.changed(changed);
18356
18234
  waker.note(wakes);
18357
18235
  // step 3: cards waiting for the person are Needs you items; #2355 review: a project's lead routes them too.
18358
18236
  if (ids.length || projects)
@@ -21399,7 +21277,6 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
21399
21277
  this.clearStatuslineWatchers();
21400
21278
  this.costGuard?.stop();
21401
21279
  this.dailySummary?.stop();
21402
- this.taskBoardSummary?.stop();
21403
21280
  this.dailyTipScheduler?.stop();
21404
21281
  this.dailyTipScheduler = null;
21405
21282
  if (this.updateCheckTimer) {
@@ -23506,7 +23383,9 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
23506
23383
  // From the install's own channel tag, this is what /update installs — even a stable the release pointed the
23507
23384
  // tag at (2.2.0 on @alpha / @beta): the plain /update text, never "/update stays on … use --stable" (#1928).
23508
23385
  const key = fromChannel ? "update.available_current" : updateNoticeKey(currentVersion, target);
23509
- this.notifyInstanceTopic(generalId, t(key, `v${target}`, `v${currentVersion}`));
23386
+ // #2775: null = a beta install and a stable target; beta users are never told to switch.
23387
+ if (key)
23388
+ this.notifyInstanceTopic(generalId, t(key, `v${target}`, `v${currentVersion}`));
23510
23389
  }
23511
23390
  }
23512
23391
  }