@songsid/agend 2.3.3-beta.1 → 2.3.3-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 (84) hide show
  1. package/dist/authority/authority-state.js +1 -1
  2. package/dist/authority/authority-state.js.map +1 -1
  3. package/dist/authority/web-authority.d.ts +9 -4
  4. package/dist/authority/web-authority.js +10 -5
  5. package/dist/authority/web-authority.js.map +1 -1
  6. package/dist/backend/claude-code.js +3 -25
  7. package/dist/backend/claude-code.js.map +1 -1
  8. package/dist/backend/claude-transcript-errors.d.ts +18 -0
  9. package/dist/backend/claude-transcript-errors.js +19 -0
  10. package/dist/backend/claude-transcript-errors.js.map +1 -0
  11. package/dist/backend/codex-session-lookup.d.ts +16 -3
  12. package/dist/backend/codex-session-lookup.js +45 -11
  13. package/dist/backend/codex-session-lookup.js.map +1 -1
  14. package/dist/classic-channel-manager.d.ts +4 -1
  15. package/dist/classic-channel-manager.js +21 -7
  16. package/dist/classic-channel-manager.js.map +1 -1
  17. package/dist/cli.js +18 -3
  18. package/dist/cli.js.map +1 -1
  19. package/dist/config-history.d.ts +50 -0
  20. package/dist/config-history.js +263 -0
  21. package/dist/config-history.js.map +1 -0
  22. package/dist/config-validator.js +1 -4
  23. package/dist/config-validator.js.map +1 -1
  24. package/dist/config.d.ts +3 -0
  25. package/dist/config.js +11 -4
  26. package/dist/config.js.map +1 -1
  27. package/dist/daemon.d.ts +37 -0
  28. package/dist/daemon.js +156 -4
  29. package/dist/daemon.js.map +1 -1
  30. package/dist/daily-summary.d.ts +8 -1
  31. package/dist/daily-summary.js +8 -5
  32. package/dist/daily-summary.js.map +1 -1
  33. package/dist/delivery-outbox.d.ts +4 -0
  34. package/dist/delivery-outbox.js +12 -2
  35. package/dist/delivery-outbox.js.map +1 -1
  36. package/dist/fleet-level-config.js +1 -1
  37. package/dist/fleet-level-config.js.map +1 -1
  38. package/dist/fleet-manager.d.ts +8 -10
  39. package/dist/fleet-manager.js +57 -83
  40. package/dist/fleet-manager.js.map +1 -1
  41. package/dist/fleet-process-handlers.d.ts +5 -0
  42. package/dist/fleet-process-handlers.js +21 -0
  43. package/dist/fleet-process-handlers.js.map +1 -1
  44. package/dist/fleet-service-owner.d.ts +11 -0
  45. package/dist/fleet-service-owner.js +3 -0
  46. package/dist/fleet-service-owner.js.map +1 -0
  47. package/dist/general-knowledge/skills/web-dashboard/SKILL.canvas-worker-off.md +3 -4
  48. package/dist/general-knowledge/skills/web-dashboard/SKILL.md +3 -4
  49. package/dist/instance-lifecycle.js +2 -0
  50. package/dist/instance-lifecycle.js.map +1 -1
  51. package/dist/locale.js +24 -36
  52. package/dist/locale.js.map +1 -1
  53. package/dist/needs-you-hub.d.ts +1 -0
  54. package/dist/needs-you-hub.js +6 -3
  55. package/dist/needs-you-hub.js.map +1 -1
  56. package/dist/needs-you-live.d.ts +3 -1
  57. package/dist/needs-you-live.js +24 -2
  58. package/dist/needs-you-live.js.map +1 -1
  59. package/dist/needs-you.d.ts +13 -2
  60. package/dist/needs-you.js +11 -1
  61. package/dist/needs-you.js.map +1 -1
  62. package/dist/scheduler/task-board.d.ts +0 -13
  63. package/dist/scheduler/task-board.js +0 -14
  64. package/dist/scheduler/task-board.js.map +1 -1
  65. package/dist/setup-wizard.d.ts +3 -0
  66. package/dist/setup-wizard.js +4 -2
  67. package/dist/setup-wizard.js.map +1 -1
  68. package/dist/transcript-monitor.d.ts +7 -0
  69. package/dist/transcript-monitor.js +48 -3
  70. package/dist/transcript-monitor.js.map +1 -1
  71. package/dist/types.d.ts +6 -5
  72. package/dist/ui/panel-needs.js +11 -9
  73. package/dist/ui/settings-strings.js +2 -2
  74. package/dist/ui/settings-web-authority.js +4 -3
  75. package/dist/ui/shared/app-i18n.js +2 -2
  76. package/dist/ui/shared/app-needs.js +5 -1
  77. package/dist/web-api.js +2 -0
  78. package/dist/web-api.js.map +1 -1
  79. package/launcher/agend-install.cjs +7 -1
  80. package/launcher/fleet-service-owner.cjs +138 -0
  81. package/package.json +1 -1
  82. package/dist/task-board-summary.d.ts +0 -101
  83. package/dist/task-board-summary.js +0 -177
  84. package/dist/task-board-summary.js.map +0 -1
@@ -67,7 +67,7 @@ const __dirname = dirname(__filename);
67
67
  const DEFAULT_OPEN_ACCESS = { mode: "open", allowed_users: [], max_pending_codes: 0, code_expiry_minutes: 0 };
68
68
  import { STATUS_EMOJI_CONFIG_KEYS, STATUS_EMOJI_KEYS, STATUS_EMOJI_SUGGESTIONS, TELEGRAM_REACTION_EMOJIS, builtinStatusEmojis, customEmojiValue, emojiImageUrl, normalizeEmoji, previewStatusEmojis, reactionForm, reactionMatchKey, resolveStatusEmojis, statusAvoidList, statusEmojiProblem, statusMatchKey, statusMatchKeys, textForm, } from "./status-emojis.js";
69
69
  import { isProbeableRouteTarget } from "./fleet-context.js";
70
- import { getEffectiveInstanceDefaults, loadFleetConfig, loadRawFleetConfig, DEFAULT_COST_GUARD, DEFAULT_DAILY_SUMMARY, DEFAULT_INSTANCE_CONFIG, autoGeneralInstanceConfig } from "./config.js";
70
+ import { getEffectiveInstanceDefaults, loadFleetConfig, loadRawFleetConfig, loadFleetConfigSource, loadRawFleetConfigSource, DEFAULT_COST_GUARD, DEFAULT_DAILY_SUMMARY, DEFAULT_INSTANCE_CONFIG, autoGeneralInstanceConfig } from "./config.js";
71
71
  import { EventLog } from "./event-log.js";
72
72
  import { binaryProbe } from "./binary-probe.js";
73
73
  import { AVAILABILITY_STALE_MS, BackendAvailabilityStore, hostAvailabilityProbes } from "./backend-availability.js";
@@ -100,7 +100,6 @@ import { escapeTelegramHtml, scheduleClock, scheduleRetryLabel } from "./schedul
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";
@@ -235,6 +234,7 @@ import { remoteCardAction, remoteCardChoiceLabel, remoteCardOutcomeText, remoteC
235
234
  import { recentChatContext } from "./log-tail.js";
236
235
  import { describeSignalSource, recordInternalRequest, withOrigin } from "./fleet-control-audit.js";
237
236
  import { handleAgentRequest, ToolNotPermittedError } from "./agent-endpoint.js";
237
+ import { ConfigHistory, ConfigSyntaxError, configIssueText } from "./config-history.js";
238
238
  import { ClassicChannelManager, classicWebEchoOn, getClassicBackendChoices, inferClassicChannelType, isSelectableClassicBackend, readClassicLastActivityAt, readClassicRoomNames } from "./classic-channel-manager.js";
239
239
  import { assertExplicitInstanceRemoval } from "./instance-removal.js";
240
240
  import { KNOWN_BACKENDS, validateFleetConfig } from "./config-validator.js";
@@ -797,8 +797,6 @@ export class FleetManager {
797
797
  costGuard = null;
798
798
  statuslineWatcher;
799
799
  dailySummary = null;
800
- /** #2246 step 3: the task board's daily summary for the person (task-board-summary.ts). */
801
- taskBoardSummary = null;
802
800
  dailyTipScheduler = null;
803
801
  /** The task board's wakes (step 2): an instance hears when someone else changes a card it owns or handed out. */
804
802
  taskBoardWaker = null;
@@ -909,6 +907,12 @@ export class FleetManager {
909
907
  kiroMigrationRuns = new Map();
910
908
  /** #1386 "Needs you": the list, its live chat messages and Acknowledge. Started with the fleet (finishStartup). */
911
909
  needsYou = null;
910
+ configHistory = null;
911
+ configHistoryTimer = null;
912
+ loadedFleetSource = null;
913
+ getConfigHistory() {
914
+ return this.configHistory ??= new ConfigHistory(this.dataDir, () => this.needsYou?.poke(), message => this.logger.warn(message));
915
+ }
912
916
  /** When the fleet first saw an instance crashed, for its "Needs you" age; cleared when it is not. */
913
917
  needsCrashedAt = new Map();
914
918
  /** Pause reason/time read from the marker, per paused instance; dropped on any attention change of it, and at most
@@ -2408,12 +2412,10 @@ export class FleetManager {
2408
2412
  const notice = silent
2409
2413
  ? t(`delivery.fleet_${row.state}_silent_schedule`, row.targetInstance, row.operationId, row.deliveryId, row.lastError ?? "unknown")
2410
2414
  : t(`delivery.fleet_${row.state}`, row.targetInstance, row.operationId, row.deliveryId);
2415
+ // #2804: log-only. The person sees a failed or unconfirmed delivery as a Needs-you item (with Acknowledge),
2416
+ // not as a separate General post; the sending agent still gets its delivery-outcome notice.
2411
2417
  this.logger.error({ deliveryId: row.deliveryId, operationId: row.operationId, target: row.targetInstance, state: row.state,
2412
2418
  cause: row.lastError, ...(silent ? { silentSchedule: true } : {}) }, notice);
2413
- // A self-scheduled silent heartbeat stays log-only, including the
2414
- // operator alert; another source still receives the normal notice.
2415
- if (!silent || row.sourceInstance !== row.targetInstance)
2416
- this.notifyFleetError(notice);
2417
2419
  }
2418
2420
  }
2419
2421
  });
@@ -2445,8 +2447,9 @@ export class FleetManager {
2445
2447
  if (event.count)
2446
2448
  this.needsYou?.poke(); // #1386: rows that expired failed — they wait on someone now
2447
2449
  this.wakeCoordinator?.kick();
2450
+ // #2804: the expired rows are Needs-you items now; the count is logged, not posted to General.
2448
2451
  if (event.count)
2449
- this.notifyFleetError(t("delivery.expired", event.count, event.uncertain ?? 0));
2452
+ this.logger.warn({ count: event.count, uncertain: event.uncertain ?? 0 }, t("delivery.expired", event.count, event.uncertain ?? 0));
2450
2453
  });
2451
2454
  this.scheduleDeliveryOutboxPump();
2452
2455
  }
@@ -2931,7 +2934,9 @@ export class FleetManager {
2931
2934
  if ((current?.state === "delivering" || current?.state === "submission_started")
2932
2935
  && current.targetDaemonBootId === claimed.targetDaemonBootId
2933
2936
  && current.managerBootId === claimed.managerBootId) {
2934
- this.notifyFleetError(t("delivery.lane_waiting", claimed.targetInstance, Math.round(DURABLE_DELIVERY_LANE_ALERT_MS / 60_000), deliveryId));
2937
+ // #2804: logged, not posted. A delivery still in flight needs nobody yet; if it ends failed or unconfirmed it
2938
+ // becomes a Needs-you item.
2939
+ this.logger.warn({ deliveryId, target: claimed.targetInstance }, t("delivery.lane_waiting", claimed.targetInstance, Math.round(DURABLE_DELIVERY_LANE_ALERT_MS / 60_000), deliveryId));
2935
2940
  }
2936
2941
  }, DURABLE_DELIVERY_LANE_ALERT_MS);
2937
2942
  alertTimer.unref?.();
@@ -3103,7 +3108,8 @@ export class FleetManager {
3103
3108
  // fleet.yaml, sent SIGHUP, and got no reaction and no explanation.
3104
3109
  this.logger.error({ err }, "SIGHUP config reload failed");
3105
3110
  const message = err instanceof Error ? err.message : String(err);
3106
- this.notifyFleetError(t("fleet.reload_failed", message));
3111
+ if (!(err instanceof ConfigSyntaxError))
3112
+ this.notifyFleetError(t("fleet.reload_failed", message));
3107
3113
  });
3108
3114
  }
3109
3115
  /**
@@ -3339,19 +3345,19 @@ export class FleetManager {
3339
3345
  };
3340
3346
  }
3341
3347
  /** Load fleet.yaml and build routing table */
3342
- loadConfig(configPath) {
3348
+ loadConfig(configPath, options = {}) {
3343
3349
  this.configPath = configPath;
3344
- const source = existsSync(configPath) ? readFileSync(configPath, "utf-8") : "{}\n";
3345
- this.rawFleetDocument = parseDocument(source, { keepSourceTokens: true });
3346
- if (this.rawFleetDocument.errors.length > 0) {
3347
- throw new Error(`Invalid fleet.yaml: ${this.rawFleetDocument.errors[0].message}`);
3348
- }
3349
- const raw = loadRawFleetConfig(configPath);
3350
- const loaded = loadFleetConfig(configPath);
3350
+ const history = this.getConfigHistory();
3351
+ history.track("fleet.yaml", configPath);
3352
+ const selected = existsSync(configPath) ? history.read("fleet.yaml", configPath, options.startup) : { source: "{}\n", fallback: false };
3353
+ this.rawFleetDocument = parseDocument(selected.source, { keepSourceTokens: true });
3354
+ const raw = loadRawFleetConfigSource(selected.source);
3355
+ const loaded = loadFleetConfigSource(selected.source);
3351
3356
  this.assertProviderSecretEnvKeys(loaded);
3352
3357
  this.rawFleetConfig = raw;
3353
3358
  this.settingsRuntimeRevision++;
3354
3359
  this.fleetConfig = loaded;
3360
+ this.loadedFleetSource = selected;
3355
3361
  this.savedFleetConfigSnapshot = structuredClone(this.fleetConfig);
3356
3362
  this.warnAboutRemovedLoginMode(loaded);
3357
3363
  this.enforceGuestSwitch();
@@ -3360,6 +3366,8 @@ export class FleetManager {
3360
3366
  // Scope the path to its owning data directory so another inherited home's override cannot enable CW.
3361
3367
  process.env.AGEND_FLEET_DATA_DIR = resolvePath(this.dataDir);
3362
3368
  process.env.AGEND_FLEET_CONFIG_PATH = resolvePath(configPath);
3369
+ if (options.remember !== false && existsSync(configPath))
3370
+ history.accepted("fleet.yaml", configPath, selected);
3363
3371
  return this.fleetConfig;
3364
3372
  }
3365
3373
  relayLoginModeWarned = false;
@@ -8169,7 +8177,7 @@ export class FleetManager {
8169
8177
  this.tmuxExecHelperTimer ??= setInterval(() => tmuxExecHelper.maintain(), 5_000);
8170
8178
  this.tmuxExecHelperTimer.unref?.();
8171
8179
  this.rotateFleetLogs();
8172
- const fleet = this.loadConfig(configPath);
8180
+ const fleet = this.loadConfig(configPath, { startup: true });
8173
8181
  this.slimFleetConfigAtStartup();
8174
8182
  this.logConfigWarningsAtStartup();
8175
8183
  setLocale(detectLocale(fleet)); // user-facing text language (fleet.yaml defaults.locale / timezone)
@@ -8247,7 +8255,7 @@ export class FleetManager {
8247
8255
  // migrates legacy single-bot entries and names without a suffix. Classic
8248
8256
  // routing does NOT go through the routing engine (single-key, can't hold two
8249
8257
  // bots in one channel) — it resolves per-bot via getInstanceByChannel.
8250
- this.classicChannels = new ClassicChannelManager(this.dataDir, this.logger);
8258
+ this.classicChannels = new ClassicChannelManager(this.dataDir, this.logger, this.getConfigHistory());
8251
8259
  const classicAdapters = fleet.channels?.length ? fleet.channels : (fleet.channel ? [fleet.channel] : []);
8252
8260
  this.classicChannels.configureAdapters(classicAdapters);
8253
8261
  // The unrecoverable-id report is deliberately NOT sent here: no adapter
@@ -8265,6 +8273,9 @@ export class FleetManager {
8265
8273
  // rooms), before the dashboard serves anything; from now on every change is written back (off the loop).
8266
8274
  this.restoreWebChat(fleet);
8267
8275
  this.startSearchIndex(fleet);
8276
+ // Parse-only source monitoring; it does not apply hand-edited Fleet settings.
8277
+ this.configHistoryTimer = setInterval(() => this.configHistory?.check(), 1_000);
8278
+ this.configHistoryTimer.unref?.();
8268
8279
  // Poll classicBot.yaml for external changes every 30s
8269
8280
  this.classicReloadTimer = setInterval(() => {
8270
8281
  // Attributes the synchronous part, up to the reload's first await (#1235).
@@ -8296,7 +8307,8 @@ export class FleetManager {
8296
8307
  ...fleet.defaults.daily_summary,
8297
8308
  };
8298
8309
  this.dailySummary = new DailySummary(summaryConfig, costGuardConfig.timezone, (text) => {
8299
- this.postDailySummary(text);
8310
+ if (text !== null)
8311
+ this.postDailySummary(text);
8300
8312
  // Rotate classic channel chat logs daily
8301
8313
  this.classicChannels?.rotateLogs();
8302
8314
  this.rotateInboxes();
@@ -8314,17 +8326,6 @@ export class FleetManager {
8314
8326
  return DailySummary.generateText(this.eventLog, instances, costMap, this.costGuard?.getFleetTotalCents() ?? 0);
8315
8327
  });
8316
8328
  this.dailySummary.start();
8317
- // #2246 step 3: the task board's daily summary — its own time (defaults.task_board.summary_at, read at every look),
8318
- // in the fleet's zone; a summary missed while the fleet was down goes out when it is back.
8319
- this.taskBoardSummary = new TaskBoardSummaryClock({
8320
- at: () => summaryTime(this.fleetConfig?.defaults?.task_board?.summary_at) ?? null,
8321
- zone: () => this.usageZone(),
8322
- now: () => new Date(),
8323
- mark: summaryMarkFile(join(this.dataDir, "task-board-summary.json")),
8324
- send: (since, until) => this.postTaskBoardSummary(since, until),
8325
- failed: (err) => this.logger.warn({ err }, "The task board's daily summary failed; the next minute tries again"),
8326
- });
8327
- this.taskBoardSummary.start();
8328
8329
  // Rotate classic channel chat logs daily (piggyback on daily summary timer)
8329
8330
  this.classicChannels?.rotateLogs();
8330
8331
  this.rotateInboxes();
@@ -8413,7 +8414,7 @@ export class FleetManager {
8413
8414
  fleet.instances[name] = autoGeneralInstanceConfig(fleet.defaults, generalDir, need.adapterId); // #2598: its kiro_ui is the fleet's
8414
8415
  generalsCreated = true;
8415
8416
  }
8416
- if (generalsCreated)
8417
+ if (generalsCreated && !this.configHistory?.isFallback(this.configPath))
8417
8418
  this.saveFleetConfig();
8418
8419
  if (topicMode && (fleet.channel || fleet.channels?.length)) {
8419
8420
  const schedulerConfig = {
@@ -8589,7 +8590,7 @@ export class FleetManager {
8589
8590
  fixedGeneral = true;
8590
8591
  }
8591
8592
  }
8592
- if (fixedGeneral)
8593
+ if (fixedGeneral && !this.configHistory?.isFallback(this.configPath))
8593
8594
  this.saveFleetConfig();
8594
8595
  // Auto-create topics AFTER adapter is ready (needs adapter.createTopic)
8595
8596
  await this.topicCommands.autoCreateTopics();
@@ -12616,6 +12617,7 @@ export class FleetManager {
12616
12617
  },
12617
12618
  });
12618
12619
  this.rawFleetConfig = loadRawFleetConfig(this.configPath);
12620
+ this.configHistory?.accepted("fleet.yaml", this.configPath, { source: output, fallback: false });
12619
12621
  // The instance map is defaults-expanded. A defaults edit changes that view even when it writes no instance
12620
12622
  // leaves. Re-resolve from the committed YAML so inherited values cannot become stale overrides on the next save.
12621
12623
  // Keep the FleetConfig object itself: Settings and other request contexts hold it.
@@ -12694,6 +12696,8 @@ export class FleetManager {
12694
12696
  }
12695
12697
  /** One-time upgrade migration; invalid YAML is never rewritten. */
12696
12698
  migrateNoticeDeliveryStorage() {
12699
+ if (this.configHistory?.isFallback(this.configPath))
12700
+ return;
12697
12701
  if (this.fleetConfig && this.configPath && (this.fleetConfig.web?.notify_login === "dm" || this.fleetConfig.defaults.tips === "dm"
12698
12702
  || this.fleetConfig.web?.notice_delivery !== undefined)
12699
12703
  && validateFleetConfig(this.rawFleetConfig).valid) {
@@ -12708,6 +12712,8 @@ export class FleetManager {
12708
12712
  }
12709
12713
  }
12710
12714
  migrateOfficeStorage() {
12715
+ if (this.configHistory?.isFallback(this.configPath))
12716
+ return;
12711
12717
  if (!this.fleetConfig || !this.configPath || !hasLegacyOffice(this.rawFleetConfig) || !validateFleetConfig(this.rawFleetConfig).valid)
12712
12718
  return;
12713
12719
  try {
@@ -12731,6 +12737,8 @@ export class FleetManager {
12731
12737
  }
12732
12738
  }
12733
12739
  slimFleetConfigAtStartup() {
12740
+ if (this.configHistory?.isFallback(this.configPath))
12741
+ return;
12734
12742
  this.migrateOfficeStorage();
12735
12743
  this.migrateNoticeDeliveryStorage();
12736
12744
  const redundantPaths = collectRedundantInstanceDefaultPaths(this.rawFleetConfig);
@@ -14312,51 +14320,6 @@ export class FleetManager {
14312
14320
  }
14313
14321
  return { chatId, opts: {} };
14314
14322
  }
14315
- /**
14316
- * #2246 step 3: the task board's daily summary of [since, until) (task-board-summary.ts). Each chat world's General
14317
- * gets its own instances' cards — a card that no world's instance holds (nobody's, a person's) goes to the primary
14318
- * one — and each of that world's Generals shows the same text in the web chat. A world with nothing to say gets
14319
- * nothing. A web-only fleet: everything, in the web chat.
14320
- */
14321
- postTaskBoardSummary(since, until) {
14322
- const board = this.scheduler?.db.board;
14323
- if (!board)
14324
- return;
14325
- const read = board.summaryRead(since.toISOString(), until.toISOString(), this.dispatcherGone);
14326
- const projects = board.listProjects(true);
14327
- const stats = board.projectStats(this.dispatcherGone);
14328
- const names = new Map(projects.map((p) => [p.id, p.name]));
14329
- const configured = this.getPrimaryAdapterId();
14330
- const primary = configured !== undefined && this.worlds.has(configured) ? configured : this.worlds.keys().next().value;
14331
- const worldOf = (assignee) => {
14332
- const owner = assignee ? this.getInstanceAdapterId(assignee) : undefined;
14333
- return owner !== undefined && this.worlds.has(owner) ? owner : primary;
14334
- };
14335
- const zone = this.usageZone();
14336
- for (const world of this.worlds.size ? [...this.worlds.keys()] : [undefined]) {
14337
- const summary = buildSummary(read, since, until, { inChat: (assignee) => worldOf(assignee) === world, projects, stats });
14338
- if (isQuiet(summary))
14339
- continue;
14340
- const text = renderSummary(summary, { zone, t: (key, ...args) => t(key, ...args), projectOf: (c) => (c.projectId === null ? null : names.get(c.projectId) ?? null) });
14341
- const cards = [...new Set([...summary.review, ...summary.blocked, ...summary.done, ...summary.created].map((c) => c.code))];
14342
- if (world !== undefined) {
14343
- const adapter = this.worlds.get(world)?.adapter;
14344
- const target = this.fleetNoticeTarget(world);
14345
- if (adapter && target) {
14346
- adapter.sendText(target.chatId, text, target.opts).catch(e => this.logger.warn({ err: e, world }, "Failed to send the task board's daily summary"));
14347
- }
14348
- else {
14349
- this.logger.warn({ world }, "The task board's daily summary has no postable target — set a General topic or channel.options.general_channel_id");
14350
- }
14351
- }
14352
- for (const [name, cfg] of Object.entries(this.fleetConfig?.instances ?? {})) {
14353
- if (cfg.general_topic !== true || (world !== undefined && this.getInstanceAdapterId(name) !== world))
14354
- continue;
14355
- // #2341 review: with the cards it names, so the public link withholds it while one of them is hidden there.
14356
- this.emitSseEvent("message", { instance: name, sender: name, role: "status", text, ts: new Date().toISOString(), cards });
14357
- }
14358
- }
14359
- }
14360
14323
  /**
14361
14324
  * Post the daily summary where fleet-wide notices go.
14362
14325
  *
@@ -15066,6 +15029,7 @@ export class FleetManager {
15066
15029
  pauseReason, pausedAt,
15067
15030
  crashedAt: crashed ? (this.needsCrashedAt.get(name) ?? null) : null,
15068
15031
  signIn: pauseReason === "auth" ? this.signInFor(name) : null,
15032
+ usageHold: this.daemons.get(name)?.usageHold?.() ?? null,
15069
15033
  });
15070
15034
  }
15071
15035
  return out;
@@ -15139,6 +15103,9 @@ export class FleetManager {
15139
15103
  },
15140
15104
  tasks: () => this.needsYouTaskItems(),
15141
15105
  backendSignIns: () => this.needsYouBackendSignIns(),
15106
+ configIssues: () => (this.configHistory?.currentIssues() ?? []).map(issue => ({
15107
+ ...issue, detail: configIssueText(issue, t), owner: this.worlds.keys().next().value,
15108
+ })),
15142
15109
  taskAction: action => this.taskActionFromChat(action),
15143
15110
  settings: () => ({
15144
15111
  liveMessage: this.fleetConfig?.needs_you?.live_message !== false,
@@ -21278,6 +21245,9 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
21278
21245
  const sharesStopped = this.officeSharesInstance?.close(); // #2636: every Office share ends, and its tunnel
21279
21246
  // #1386: every live "Needs you" message says the fleet stopped (capabilities revoked first) — while the
21280
21247
  // adapters can still edit. Bounded: a platform that does not answer must not hold the shutdown.
21248
+ if (this.configHistoryTimer)
21249
+ clearInterval(this.configHistoryTimer);
21250
+ this.configHistoryTimer = null;
21281
21251
  const needsStopped = this.stopNeedsYou();
21282
21252
  // #1490 P3: stop() now returns a Promise; await it so the final
21283
21253
  // ledger flush completes before process.exit(0) in doStopAll's caller.
@@ -21333,7 +21303,6 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
21333
21303
  this.clearStatuslineWatchers();
21334
21304
  this.costGuard?.stop();
21335
21305
  this.dailySummary?.stop();
21336
- this.taskBoardSummary?.stop();
21337
21306
  this.dailyTipScheduler?.stop();
21338
21307
  this.dailyTipScheduler = null;
21339
21308
  if (this.updateCheckTimer) {
@@ -21624,14 +21593,16 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
21624
21593
  const previousRawConfig = this.rawFleetConfig;
21625
21594
  const previousRawDocument = this.rawFleetDocument;
21626
21595
  const previousSavedSnapshot = this.savedFleetConfigSnapshot;
21596
+ const previousSource = this.loadedFleetSource;
21627
21597
  try {
21628
- this.loadConfig(this.configPath);
21598
+ this.loadConfig(this.configPath, { remember: false });
21629
21599
  }
21630
21600
  catch (err) {
21631
21601
  this.fleetConfig = oldConfig;
21632
21602
  this.rawFleetConfig = previousRawConfig;
21633
21603
  this.rawFleetDocument = previousRawDocument;
21634
21604
  this.savedFleetConfigSnapshot = previousSavedSnapshot;
21605
+ this.loadedFleetSource = previousSource;
21635
21606
  throw err;
21636
21607
  }
21637
21608
  const validation = validateFleetConfig(this.rawFleetConfig);
@@ -21647,6 +21618,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
21647
21618
  this.rawFleetConfig = previousRawConfig;
21648
21619
  this.rawFleetDocument = previousRawDocument;
21649
21620
  this.savedFleetConfigSnapshot = previousSavedSnapshot;
21621
+ this.loadedFleetSource = previousSource;
21650
21622
  this.observeWebAuthority(); // #2453 review: the configuration back in force is the one seen
21651
21623
  this.logger.error({
21652
21624
  oldCount,
@@ -21667,6 +21639,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
21667
21639
  // mark every row done and tell the user "changes applied".
21668
21640
  return { rejected: why };
21669
21641
  }
21642
+ if (this.loadedFleetSource)
21643
+ this.getConfigHistory().accepted("fleet.yaml", this.configPath, this.loadedFleetSource);
21670
21644
  this.migrateOfficeStorage();
21671
21645
  this.migrateNoticeDeliveryStorage();
21672
21646
  if (canvasWorkerEnabled(oldConfig?.web) !== this.canvasWorkerEnabled()) {
@@ -21881,7 +21855,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
21881
21855
  * the panel shows the mismatch instead of a button.
21882
21856
  */
21883
21857
  checkStartupSignatureConsistency() {
21884
- if (!this.configPath) {
21858
+ if (!this.configPath || this.configHistory?.isFallback(this.configPath)) {
21885
21859
  this.fleetSignatureMismatch = null;
21886
21860
  return;
21887
21861
  }