@songsid/agend 2.2.0-beta.12 → 2.2.0-beta.14

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 (43) hide show
  1. package/dist/cli.js +12 -6
  2. package/dist/cli.js.map +1 -1
  3. package/dist/config.d.ts +1 -0
  4. package/dist/config.js +2 -1
  5. package/dist/config.js.map +1 -1
  6. package/dist/daemon.d.ts +1 -0
  7. package/dist/daemon.js +31 -4
  8. package/dist/daemon.js.map +1 -1
  9. package/dist/fleet-manager.d.ts +14 -5
  10. package/dist/fleet-manager.js +54 -52
  11. package/dist/fleet-manager.js.map +1 -1
  12. package/dist/instance-avatar.d.ts +1 -1
  13. package/dist/instance-avatar.js +7 -4
  14. package/dist/instance-avatar.js.map +1 -1
  15. package/dist/locale.js +48 -22
  16. package/dist/locale.js.map +1 -1
  17. package/dist/restart-progress.d.ts +0 -2
  18. package/dist/restart-progress.js +0 -2
  19. package/dist/restart-progress.js.map +1 -1
  20. package/dist/topic-commands.d.ts +4 -0
  21. package/dist/topic-commands.js +9 -2
  22. package/dist/topic-commands.js.map +1 -1
  23. package/dist/transcript-sources.js +6 -1
  24. package/dist/transcript-sources.js.map +1 -1
  25. package/dist/ui/chat-strings.js +10 -10
  26. package/dist/ui/chat-tour.js +17 -14
  27. package/dist/ui/panel-chat.js +9 -9
  28. package/dist/ui/panel-fleet.js +15 -2
  29. package/dist/ui/panel-log.js +2 -2
  30. package/dist/ui/panel-needs.js +10 -7
  31. package/dist/ui/panel-settings.js +9 -5
  32. package/dist/ui/settings-dialogs.js +2 -2
  33. package/dist/ui/settings-model.js +3 -1
  34. package/dist/ui/settings-strings.js +8 -8
  35. package/dist/ui/shared/app.css +3 -2
  36. package/dist/ui/shared/header-tools.js +22 -10
  37. package/dist/ui/shared/panel-view.js +7 -4
  38. package/dist/ui/shared/signin.js +15 -9
  39. package/dist/ui/shared/ui-icons.js +1 -0
  40. package/dist/ui/signin.html +8 -3
  41. package/dist/update-launch.js +12 -6
  42. package/dist/update-launch.js.map +1 -1
  43. package/package.json +1 -1
@@ -439,7 +439,6 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
439
439
  private updateProgressTimer;
440
440
  private updateProgressEditRunning;
441
441
  private lastUpdateProgressText;
442
- private updateCompletionTipText;
443
442
  /**
444
443
  * #1660: an adopted update whose retire outcome is still owed. The
445
444
  * completion summary was (or will be) posted; this watch appends the
@@ -1904,9 +1903,13 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
1904
1903
  private webImageCaches;
1905
1904
  private webImageCache;
1906
1905
  /**
1907
- * instance → the connection that is its own bot: configured now (a connection removed from fleet.yaml is nobody's,
1908
- * whatever an instance still names — #1635 review), not the primary, and answering for it alone.
1906
+ * The bots instances and ClassicBot rooms speak through, by connections configured now (a connection removed from
1907
+ * fleet.yaml is nobody's, whatever an instance still names — #1635 review). `own`: instance → its own bot (not the
1908
+ * primary, answering for it alone). `speaking` (#1867): instance → the bot it speaks through — its own, a shared one,
1909
+ * or, with none configured for it, the primary.
1909
1910
  */
1911
+ private webBots;
1912
+ /** instance → the connection that is its own bot (see webBots). */
1910
1913
  private ownBots;
1911
1914
  /**
1912
1915
  * The bot a connection is now: its running adapter and that bot's own user id. Everything cached for an own bot is
@@ -1922,9 +1925,15 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
1922
1925
  private webAccent;
1923
1926
  /** The cache key of an own bot's avatar, and its loader (which refuses an answer from a bot no longer there). */
1924
1927
  private botAvatarSource;
1925
- /** One instance's avatar and ring for the status frame. Synchronous: what is cached now; fetches run behind. */
1928
+ /**
1929
+ * One instance's avatar and ring for the status frame. Synchronous: what is cached now; fetches run behind. The bot
1930
+ * is its own, else the one it speaks through (#1867) — one cache entry per bot, so a shared bot is fetched once.
1931
+ */
1926
1932
  private webIdentityImage;
1927
- /** GET /ui/img/bot|avatar/<key>: what is cached, never a fetch. Only an own-bot connection, or a known instance's upload. */
1933
+ /**
1934
+ * GET /ui/img/bot|avatar/<key>: what is cached, never a fetch. Only a connection some instance or room speaks
1935
+ * through (#1867), or a known instance's upload.
1936
+ */
1928
1937
  webImage(kind: "bot" | "avatar", key: string): CachedImage | null;
1929
1938
  /**
1930
1939
  * #1682 GET /ui/mentions: names for the mention ids a page found in what it rendered — never a Discord request; in order:
@@ -178,7 +178,7 @@ import { assertExplicitInstanceRemoval } from "./instance-removal.js";
178
178
  import { KNOWN_BACKENDS, validateFleetConfig } from "./config-validator.js";
179
179
  import { isRemovedBackend, removedBackendMessage } from "./backend/removed.js";
180
180
  import { presentationState, interactionSummary, sameInteractionOwner } from "./interaction-observation.js";
181
- import { readLastInboundAt } from "./daemon.js";
181
+ import { DEFAULT_STUCK_TIMEOUT_MS, readLastInboundAt } from "./daemon.js";
182
182
  import { clearPausedMarker, readPausedAt, readPauseReason, writePausedMarker } from "./pause-marker.js";
183
183
  import { readBackgroundWorkEndedMarker } from "./background-work-ended.js";
184
184
  import { DEFAULT_WARM_OVERFLOW, WakeCoordinator } from "./wake-coordinator.js";
@@ -189,7 +189,7 @@ import { isSetupComplete, markSetupComplete } from "./setup-marker.js";
189
189
  import { manualCleanupMessage, reapStaleTunnel } from "./tunnel/lease.js";
190
190
  import { buildToolPermissionsNotice } from "./tool-permissions-notice.js";
191
191
  import { NeedsYouHub } from "./needs-you-hub.js";
192
- import { WEB_CHAT_NOTICE, WEB_REMOTE_DOCS_URL, claimNotice, hasWebChat, releaseNotice, upgradeNoticesPath } from "./upgrade-notices.js";
192
+ import { WEB_CHAT_NOTICE, claimNotice, hasWebChat, releaseNotice, upgradeNoticesPath } from "./upgrade-notices.js";
193
193
  import { GENERAL_PAUSE_ERROR, isGeneralInstance } from "./general-instance.js";
194
194
  import { buildOrgChart } from "./web-org.js";
195
195
  import { CacheService, WINDOWS } from "./cache-service.js";
@@ -762,7 +762,6 @@ export class FleetManager {
762
762
  updateProgressTimer = null;
763
763
  updateProgressEditRunning = false;
764
764
  lastUpdateProgressText = null;
765
- updateCompletionTipText = null;
766
765
  /**
767
766
  * #1660: an adopted update whose retire outcome is still owed. The
768
767
  * completion summary was (or will be) posted; this watch appends the
@@ -1662,6 +1661,7 @@ export class FleetManager {
1662
1661
  content: payload.content,
1663
1662
  delivery_id: claimed.deliveryId,
1664
1663
  delivery_attempt: String(claimed.attemptNo),
1664
+ ...(isSilentScheduleDelivery(claimed) ? { silent_schedule: true } : {}),
1665
1665
  }, { waitForIdle: false, noInlineWake: this.deliveryWorkerMode(target) !== "off" });
1666
1666
  }
1667
1667
  else {
@@ -2316,14 +2316,14 @@ export class FleetManager {
2316
2316
  // "not a beta" and sent a beta install to @latest. #1450 C5: the INSTALLED agend, verified, by absolute path.
2317
2317
  const installed = await resolveInstalledAgend();
2318
2318
  if (!installed.ok) {
2319
- this.failUpdateProgress(`/update cannot verify the installed AgEnD (${installed.reason}). Run \`agend update\` from a shell.`);
2319
+ this.failUpdateProgress(`${t("update.failure.shell")} ${t("update.failure.installed", installed.reason)}`);
2320
2320
  return;
2321
2321
  }
2322
2322
  const origin = `slash /update by ${adapterId}:${data.userId}`;
2323
2323
  recordInternalRequest(this.dataDir, "update", origin);
2324
2324
  const launch = await resolveUpdateLaunch(installed.agend);
2325
2325
  if (!launch.ok) {
2326
- this.failUpdateProgress(`/update cannot prepare an independent updater (${launch.reason}). Run ` + "`agend update` from a host shell.");
2326
+ this.failUpdateProgress(`${t("update.failure.shell")} ${t("update.failure.independent", launch.reason)}`);
2327
2327
  return;
2328
2328
  }
2329
2329
  if (this.fleetAdminGate(data.userId, adapterId) !== "ok" || this.shuttingDown) {
@@ -5398,13 +5398,12 @@ export class FleetManager {
5398
5398
  const adapter = owner.binding;
5399
5399
  const status = this.getPublicWebStatus();
5400
5400
  const publicAllowed = publicLinkSettings(this.fleetConfig?.web).allowed;
5401
- const tgHint = adapter.type === "telegram" && dmChatId === undefined ? this.tgStartWebUrl(owner.adapterId) : null;
5402
- const menuText = this.topicCommands.getDashboardText(false, !publicAllowed)
5403
- + (publicAllowed ? "\n\n" + t("dashboard.public_risk") : "")
5404
- + "\n" + t("dashboard.public_status", status.state, status.remainingSeconds ?? 0)
5405
- + (status.expiresAt ? t("dashboard.public_until", formatFleetTime(status.expiresAt)) : "")
5406
- + (tgHint ? "\n" + t("dashboard.menu_tg_hint", tgHint) : "");
5407
- const choices = [{ action: "local", label: t("dashboard.local") },
5401
+ // Show the action before any link exists. TG's Start instructions belong to confirmed DM refusal below.
5402
+ const menuText = this.topicCommands.getDashboardMenuText()
5403
+ + (publicAllowed ? "\n" + t("dashboard.public_risk") : "")
5404
+ + (status.state !== "closed" ? "\n" + t("dashboard.public_status", status.state, status.remainingSeconds ?? 0)
5405
+ + (status.expiresAt ? t("dashboard.public_until", formatFleetTime(status.expiresAt)) : "") : "");
5406
+ const choices = [{ action: "local", label: t("dashboard.local_signin") },
5408
5407
  ...(publicLinkSettings(this.fleetConfig?.web).allowed ? [{ action: "public", label: t("dashboard.public_open") }] : []),
5409
5408
  ...(status.state !== "closed" ? [{ action: "close", label: t("dashboard.public_close") }] : [])];
5410
5409
  const posted = await this.postNonceButtonPrompt({
@@ -5480,9 +5479,11 @@ export class FleetManager {
5480
5479
  return false;
5481
5480
  this.initializeWebSessions();
5482
5481
  const issued = this.webLoginCodes.issue({ epoch: tokenEpoch(token), audience: exposureId, owner });
5483
- // #1586: the expiry in the fleet's zone with its offset, never a bare UTC ISO string.
5484
- const text = t("dashboard.private_link", url, issued.display, Math.round(LOGIN_CODE_TTL_MS / 60_000), expiresAt ? formatFleetTime(expiresAt) : t("dashboard.local"))
5485
- + "\n" + t("dashboard.code_next");
5482
+ // Only a public link has a link expiry; the code's five-minute lifetime applies to either packet.
5483
+ const minutes = Math.round(LOGIN_CODE_TTL_MS / 60_000);
5484
+ const text = expiresAt !== undefined
5485
+ ? t("dashboard.private_public", url, issued.display, minutes, formatFleetTime(expiresAt))
5486
+ : t("dashboard.private_local", url, issued.display, minutes);
5486
5487
  const deadline = performance.now() + 10_000;
5487
5488
  const direct = owner.binding;
5488
5489
  // The route the sign-in message took: the code-only follow-up takes the same one (#1586).
@@ -5671,7 +5672,6 @@ export class FleetManager {
5671
5672
  messageId,
5672
5673
  });
5673
5674
  this.lastUpdateProgressText = null;
5674
- this.updateCompletionTipText = null;
5675
5675
  this.startUpdateProgressMonitor(adapter);
5676
5676
  }
5677
5677
  /** Persist the public response, wait for idle, then start the canonical service restart. */
@@ -5702,7 +5702,6 @@ export class FleetManager {
5702
5702
  return false;
5703
5703
  }
5704
5704
  this.lastUpdateProgressText = null;
5705
- this.updateCompletionTipText = null;
5706
5705
  this.startUpdateProgressMonitor(adapter);
5707
5706
  await this.waitForFullRestartIdleGrace();
5708
5707
  // An update can begin while the idle wait yields. Never launch a second
@@ -5942,17 +5941,8 @@ export class FleetManager {
5942
5941
  const adapter = this.adapters.get(target.adapterId) ?? (initialAdapter?.id === target.adapterId ? initialAdapter : undefined);
5943
5942
  if (!adapter)
5944
5943
  return;
5945
- let text = formatUpdateProgress(marker);
5946
- // No-restart updates complete in the old fleet process, so they never
5947
- // reach RestartProgress.finish(). Append the same optional tip here.
5948
- if (marker.progress.stage === "complete" && this.tipsEnabled()) {
5949
- if (this.updateCompletionTipText === null) {
5950
- const tip = this.pickAvailableTip();
5951
- this.updateCompletionTipText = tip ? this.formatTip(tip) : "";
5952
- }
5953
- if (this.updateCompletionTipText)
5954
- text += `\n\n${this.updateCompletionTipText}`;
5955
- }
5944
+ // #1854: no tip on an update's completion (either path); tips come on their own schedule and from /tips.
5945
+ const text = formatUpdateProgress(marker);
5956
5946
  if (text === this.lastUpdateProgressText)
5957
5947
  return;
5958
5948
  this.updateProgressEditRunning = true;
@@ -6456,12 +6446,6 @@ export class FleetManager {
6456
6446
  pausedNames,
6457
6447
  failedNames,
6458
6448
  backgroundWorkEndedNames,
6459
- tipText: pendingProgressOperation === "update" && this.tipsEnabled()
6460
- ? (() => {
6461
- const tip = this.pickAvailableTip();
6462
- return tip ? this.formatTip(tip) : undefined;
6463
- })()
6464
- : undefined,
6465
6449
  ...(cleanupOutcome && "render" in cleanupOutcome
6466
6450
  ? { warningText: cleanupOutcome.render.warning, warningCommand: cleanupOutcome.render.cleanupCommand }
6467
6451
  : {}),
@@ -8462,7 +8446,7 @@ export class FleetManager {
8462
8446
  if (!claimNotice(path, WEB_CHAT_NOTICE, adapterId))
8463
8447
  continue;
8464
8448
  try {
8465
- await world.adapter.sendText(target.chatId, t("upgrade.web_chat", WEB_REMOTE_DOCS_URL), target.opts);
8449
+ await world.adapter.sendText(target.chatId, t("upgrade.web_chat"), target.opts);
8466
8450
  this.logger.info({ adapterId, notice: WEB_CHAT_NOTICE }, "Announced web chat in General");
8467
8451
  }
8468
8452
  catch (err) {
@@ -10448,10 +10432,12 @@ export class FleetManager {
10448
10432
  return this.webImageCaches[kind];
10449
10433
  }
10450
10434
  /**
10451
- * instance → the connection that is its own bot: configured now (a connection removed from fleet.yaml is nobody's,
10452
- * whatever an instance still names — #1635 review), not the primary, and answering for it alone.
10435
+ * The bots instances and ClassicBot rooms speak through, by connections configured now (a connection removed from
10436
+ * fleet.yaml is nobody's, whatever an instance still names — #1635 review). `own`: instance → its own bot (not the
10437
+ * primary, answering for it alone). `speaking` (#1867): instance → the bot it speaks through — its own, a shared one,
10438
+ * or, with none configured for it, the primary.
10453
10439
  */
10454
- ownBots() {
10440
+ webBots() {
10455
10441
  const instances = this.fleetConfig?.instances ?? {};
10456
10442
  const channels = this.fleetConfig?.channels ?? (this.fleetConfig?.channel ? [this.fleetConfig.channel] : []);
10457
10443
  const configured = new Set(channels.map(ch => ch.id ?? ch.type).filter((id) => typeof id === "string"));
@@ -10461,8 +10447,17 @@ export class FleetManager {
10461
10447
  if (!Object.hasOwn(instances, room.instanceName))
10462
10448
  assignments.push([room.instanceName, assigned(room.instanceName)]);
10463
10449
  }
10464
- return ownBotConnections(this.getPrimaryAdapterId(), assignments);
10450
+ const primary = this.getPrimaryAdapterId(), fallback = primary && configured.has(primary) ? primary : undefined;
10451
+ const speaking = new Map();
10452
+ for (const [instance, connection] of assignments) {
10453
+ const c = connection ?? fallback;
10454
+ if (c)
10455
+ speaking.set(instance, c);
10456
+ }
10457
+ return { own: ownBotConnections(primary, assignments), speaking };
10465
10458
  }
10459
+ /** instance → the connection that is its own bot (see webBots). */
10460
+ ownBots() { return this.webBots().own; }
10466
10461
  /**
10467
10462
  * The bot a connection is now: its running adapter and that bot's own user id. Everything cached for an own bot is
10468
10463
  * keyed by both, so a bot put in its place (a token replaced under the same connection id) never shows the old one's
@@ -10505,8 +10500,11 @@ export class FleetManager {
10505
10500
  },
10506
10501
  };
10507
10502
  }
10508
- /** One instance's avatar and ring for the status frame. Synchronous: what is cached now; fetches run behind. */
10509
- webIdentityImage(name, displayName, profileAvatar, own) {
10503
+ /**
10504
+ * One instance's avatar and ring for the status frame. Synchronous: what is cached now; fetches run behind. The bot
10505
+ * is its own, else the one it speaks through (#1867) — one cache entry per bot, so a shared bot is fetched once.
10506
+ */
10507
+ webIdentityImage(name, displayName, profileAvatar, bots) {
10510
10508
  let profileVersion = null;
10511
10509
  if (profileAvatar && imageMimeForFile(profileAvatar)) {
10512
10510
  try {
@@ -10514,22 +10512,26 @@ export class FleetManager {
10514
10512
  }
10515
10513
  catch { /* removed */ }
10516
10514
  }
10517
- const connection = own.get(name), owner = connection ? this.botOwner(connection) : null;
10515
+ const ownConnection = bots.own.get(name), connection = ownConnection ?? bots.speaking.get(name);
10516
+ const owner = connection ? this.botOwner(connection) : null;
10518
10517
  const source = connection && owner?.adapter.fetchBotAvatar ? this.botAvatarSource(connection, owner) : null;
10519
10518
  const bot = source ? this.webImageCache("avatars").peek(source.key, source.load) : null;
10520
10519
  const cfg = this.ownInstanceConfig(name);
10521
10520
  const avatar = avatarFor({ name, displayName, profileVersion, bot: bot && connection ? { connection, version: bot.version } : null,
10522
10521
  personaEmoji: cfg?.status_emojis?.delivered });
10523
- // The ring belongs to the bot's own face: only when that is what is shown.
10524
- const accent = !profileVersion && bot && connection && owner
10525
- ? this.webAccent(connection, owner, cfg ? (cfg.topic_id == null ? undefined : String(cfg.topic_id)) : this.classicChannels?.getChannelIdByInstance(name))
10522
+ // The ring belongs to an own bot's face: only when that is what is shown (a shared bot's would ring everyone alike).
10523
+ const accent = !profileVersion && bot && ownConnection && owner
10524
+ ? this.webAccent(ownConnection, owner, cfg ? (cfg.topic_id == null ? undefined : String(cfg.topic_id)) : this.classicChannels?.getChannelIdByInstance(name))
10526
10525
  : null;
10527
10526
  return accent ? { avatar, accent } : { avatar };
10528
10527
  }
10529
- /** GET /ui/img/bot|avatar/<key>: what is cached, never a fetch. Only an own-bot connection, or a known instance's upload. */
10528
+ /**
10529
+ * GET /ui/img/bot|avatar/<key>: what is cached, never a fetch. Only a connection some instance or room speaks
10530
+ * through (#1867), or a known instance's upload.
10531
+ */
10530
10532
  webImage(kind, key) {
10531
10533
  if (kind === "bot") {
10532
- if (![...this.ownBots().values()].includes(key))
10534
+ if (![...this.webBots().speaking.values()].includes(key))
10533
10535
  return null;
10534
10536
  const owner = this.botOwner(key);
10535
10537
  return owner ? this.webImageCache("avatars").read(this.botAvatarSource(key, owner).key) : null;
@@ -14579,7 +14581,7 @@ export class FleetManager {
14579
14581
  const instanceHangConfig = this.fleetConfig?.instances[instanceName]?.hang_detector;
14580
14582
  const configuredMinutes = instanceHangConfig?.timeout_minutes
14581
14583
  ?? this.fleetConfig?.defaults?.hang_detector?.timeout_minutes
14582
- ?? 15;
14584
+ ?? DEFAULT_STUCK_TIMEOUT_MS / 60_000; // #1854: the detector's own default (10 min), not a stale 15
14583
14585
  const unchangedMinutes = unchangedForMs == null
14584
14586
  ? configuredMinutes
14585
14587
  : Math.max(1, Math.floor(unchangedForMs / 60_000));
@@ -19889,12 +19891,12 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
19889
19891
  const classicRooms = new Map((this.classicChannels?.getAll() ?? []).map(ch => [ch.instanceName, ch]));
19890
19892
  const profiles = profileIdentities(this.dataDir);
19891
19893
  // #1630: decorative — whatever goes wrong deciding an avatar never costs the frame (every instance gets its first character).
19892
- let ownBots = new Map();
19894
+ let bots = { own: new Map(), speaking: new Map() };
19893
19895
  try {
19894
- ownBots = this.ownBots();
19896
+ bots = this.webBots();
19895
19897
  }
19896
19898
  catch (err) {
19897
- this.logger.debug({ err }, "own bots unreadable (getUiStatus)");
19899
+ this.logger.debug({ err }, "bots unreadable (getUiStatus)");
19898
19900
  }
19899
19901
  const instances = names.map(name => {
19900
19902
  const statusFile = join(this.getInstanceDir(name), "statusline.json");
@@ -19935,7 +19937,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
19935
19937
  const identity = resolveInstanceIdentity({ cfg: this.fleetConfig?.instances[name], classic: classicRooms.get(name), profile: profiles.get(name) });
19936
19938
  let image;
19937
19939
  try {
19938
- image = this.webIdentityImage(name, identity.display_name, profiles.get(name)?.avatar_path, ownBots);
19940
+ image = this.webIdentityImage(name, identity.display_name, profiles.get(name)?.avatar_path, bots);
19939
19941
  }
19940
19942
  catch (err) {
19941
19943
  this.logger.debug({ err, name }, "avatar unreadable (getUiStatus)");