@songsid/agend 2.1.12-beta.3 โ†’ 2.1.12-beta.4

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 (107) hide show
  1. package/README.md +54 -8
  2. package/README.zh-TW.md +104 -11
  3. package/dist/agent-endpoint.d.ts +2 -0
  4. package/dist/agent-endpoint.js +38 -29
  5. package/dist/agent-endpoint.js.map +1 -1
  6. package/dist/backend/factory.js +5 -5
  7. package/dist/backend/factory.js.map +1 -1
  8. package/dist/backend/index.d.ts +0 -1
  9. package/dist/backend/index.js +0 -1
  10. package/dist/backend/index.js.map +1 -1
  11. package/dist/backend/kiro.d.ts +10 -1
  12. package/dist/backend/kiro.js +13 -0
  13. package/dist/backend/kiro.js.map +1 -1
  14. package/dist/backend/mock.d.ts +1 -1
  15. package/dist/backend/mock.js +1 -1
  16. package/dist/backend/muse.d.ts +14 -0
  17. package/dist/backend/muse.js +60 -11
  18. package/dist/backend/muse.js.map +1 -1
  19. package/dist/backend/removed.d.ts +12 -0
  20. package/dist/backend/removed.js +18 -0
  21. package/dist/backend/removed.js.map +1 -0
  22. package/dist/backend/session-signals.js +5 -10
  23. package/dist/backend/session-signals.js.map +1 -1
  24. package/dist/backend/types.d.ts +1 -1
  25. package/dist/backend/types.js +0 -1
  26. package/dist/backend/types.js.map +1 -1
  27. package/dist/channel/adapters/discord.js +9 -0
  28. package/dist/channel/adapters/discord.js.map +1 -1
  29. package/dist/channel/adapters/telegram.d.ts +14 -8
  30. package/dist/channel/adapters/telegram.js +80 -26
  31. package/dist/channel/adapters/telegram.js.map +1 -1
  32. package/dist/channel/mcp-tools.js +2 -2
  33. package/dist/channel/mcp-tools.js.map +1 -1
  34. package/dist/classic-channel-manager.js +2 -2
  35. package/dist/classic-channel-manager.js.map +1 -1
  36. package/dist/cli.js +27 -53
  37. package/dist/cli.js.map +1 -1
  38. package/dist/command-table.d.ts +1 -1
  39. package/dist/command-table.js +4 -2
  40. package/dist/command-table.js.map +1 -1
  41. package/dist/config-validator.js +48 -10
  42. package/dist/config-validator.js.map +1 -1
  43. package/dist/config.js +7 -5
  44. package/dist/config.js.map +1 -1
  45. package/dist/context-guardian.d.ts +1 -1
  46. package/dist/context-guardian.js +1 -1
  47. package/dist/cross-instance-notice.d.ts +39 -0
  48. package/dist/cross-instance-notice.js +84 -0
  49. package/dist/cross-instance-notice.js.map +1 -0
  50. package/dist/daemon.d.ts +2 -0
  51. package/dist/daemon.js +25 -11
  52. package/dist/daemon.js.map +1 -1
  53. package/dist/daily-summary.js +0 -3
  54. package/dist/daily-summary.js.map +1 -1
  55. package/dist/fleet-manager.d.ts +16 -3
  56. package/dist/fleet-manager.js +110 -53
  57. package/dist/fleet-manager.js.map +1 -1
  58. package/dist/general-knowledge/skills/fleet-config/SKILL.md +1 -1
  59. package/dist/instance-config-impact.js +3 -0
  60. package/dist/instance-config-impact.js.map +1 -1
  61. package/dist/instance-lifecycle.d.ts +9 -1
  62. package/dist/instance-lifecycle.js +62 -7
  63. package/dist/instance-lifecycle.js.map +1 -1
  64. package/dist/locale.js +26 -2
  65. package/dist/locale.js.map +1 -1
  66. package/dist/memory-pressure.d.ts +8 -2
  67. package/dist/memory-pressure.js +10 -4
  68. package/dist/memory-pressure.js.map +1 -1
  69. package/dist/outbound-handlers.js +36 -23
  70. package/dist/outbound-handlers.js.map +1 -1
  71. package/dist/outbound-schemas.d.ts +0 -1
  72. package/dist/outbound-schemas.js +3 -3
  73. package/dist/outbound-schemas.js.map +1 -1
  74. package/dist/reply-dedup.d.ts +5 -0
  75. package/dist/reply-dedup.js +9 -0
  76. package/dist/reply-dedup.js.map +1 -1
  77. package/dist/settings-api.js +5 -1
  78. package/dist/settings-api.js.map +1 -1
  79. package/dist/setup-wizard.js +0 -4
  80. package/dist/setup-wizard.js.map +1 -1
  81. package/dist/spawn-gate.js +4 -3
  82. package/dist/spawn-gate.js.map +1 -1
  83. package/dist/target-queue-worker.d.ts +1 -1
  84. package/dist/topic-commands.d.ts +64 -5
  85. package/dist/topic-commands.js +172 -14
  86. package/dist/topic-commands.js.map +1 -1
  87. package/dist/transcript-jsonl.d.ts +12 -0
  88. package/dist/transcript-jsonl.js +37 -0
  89. package/dist/transcript-jsonl.js.map +1 -1
  90. package/dist/transcript-monitor.js +10 -7
  91. package/dist/transcript-monitor.js.map +1 -1
  92. package/dist/transcript-sources.d.ts +1 -1
  93. package/dist/transcript-sources.js +29 -11
  94. package/dist/transcript-sources.js.map +1 -1
  95. package/dist/types.d.ts +13 -4
  96. package/dist/ui/dashboard.html +3 -3
  97. package/dist/ui/settings.html +58 -16
  98. package/dist/update-check.d.ts +15 -6
  99. package/dist/update-check.js +20 -7
  100. package/dist/update-check.js.map +1 -1
  101. package/dist/wake-coordinator.js +1 -1
  102. package/dist/web-api.js +1 -2
  103. package/dist/web-api.js.map +1 -1
  104. package/package.json +1 -1
  105. package/dist/backend/gemini-cli.d.ts +0 -20
  106. package/dist/backend/gemini-cli.js +0 -169
  107. package/dist/backend/gemini-cli.js.map +0 -1
@@ -36,13 +36,10 @@ export class DailySummary {
36
36
  const lines = [`๐Ÿ“Š Daily Report โ€” ${today}`, ""];
37
37
  for (const name of instances) {
38
38
  const instanceEvents = todayEvents.filter(e => e.instance_name === name);
39
- const restarts = instanceEvents.filter(e => e.event_type === "context_restart").length;
40
39
  const hangs = instanceEvents.filter(e => e.event_type === "hang_detected").length;
41
40
  const deferred = instanceEvents.filter(e => e.event_type === "schedule_deferred").length;
42
41
  const costCents = costCentsMap.get(name) ?? 0;
43
42
  let line = `${name}: ${formatCents(costCents)}`;
44
- if (restarts > 0)
45
- line += `, ${restarts} restart${restarts > 1 ? "s" : ""}`;
46
43
  if (deferred > 0)
47
44
  line += `, ${deferred} deferred`;
48
45
  const anomalies = [];
@@ -1 +1 @@
1
- {"version":3,"file":"daily-summary.js","sourceRoot":"","sources":["../src/daily-summary.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"daily-summary.js","sourceRoot":"","sources":["../src/daily-summary.ts"],"names":[],"mappings":"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"}
@@ -51,6 +51,14 @@ export declare function selectLruEvictions(warm: string[], cap: number, opts: {
51
51
  isIdle: (name: string) => boolean;
52
52
  lastInboundAt: (name: string) => number;
53
53
  }): string[];
54
+ /**
55
+ * Window-name shape this fleet recognises as an instance window: the
56
+ * `-t<digits>` form every allocator emits, plus legacy `classic-` windows.
57
+ * Startup cleanup reaps windows matching this that are no longer in
58
+ * fleet.yaml. Keep in sync with `uniqueInstanceName` โ€” every name it emits
59
+ * must match here, or a deleted instance's CLI is left running (#1305 P2-3).
60
+ */
61
+ export declare function isOrphanInstanceWindowName(name: string): boolean;
54
62
  /**
55
63
  * Answer shape for `list_models`. `scope` reports where the LIST came from โ€”
56
64
  * "instance" only when it was read through that instance's own backend config,
@@ -530,6 +538,8 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
530
538
  private onFullRestartSignal;
531
539
  private handleUpdateSlash;
532
540
  private handleRestartSlash;
541
+ /** #1302: `/visibility [mode]`. The dispatcher has already applied the command table's fleet-admin gate. */
542
+ private handleVisibilitySlash;
533
543
  private handleTipsSlash;
534
544
  /** Admin-only full conversation reset for fleet-topic and Classic instances. */
535
545
  private handleClearSlash;
@@ -1197,7 +1207,8 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
1197
1207
  private handleTaskCrud;
1198
1208
  /** Create a forum topic via the adapter. Returns the message_thread_id. */
1199
1209
  createForumTopic(topicName: string, adapterId?: string): Promise<number | string>;
1200
- deleteForumTopic(topicId: number | string): Promise<void>;
1210
+ deleteForumTopic(topicId: number | string, adapterId?: string): Promise<void>;
1211
+ getForumTopicDeleter(adapterId?: string): ((topicId: number | string) => Promise<void>) | null;
1201
1212
  private topicCleanupTimer;
1202
1213
  private sessionPruneTimer;
1203
1214
  private classicReloadTimer;
@@ -1412,6 +1423,8 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
1412
1423
  * own adapter rather than through reactMessageStatus (classic bots).
1413
1424
  */
1414
1425
  private receivedReactionFor;
1426
+ private reactClassicReceived;
1427
+ private reactClassicForwardedAttachment;
1415
1428
  /**
1416
1429
  * The stamp for an inbound photo / file a classic bot saved (#1080): the
1417
1430
  * instance's `status_emojis.photo|attachment`, then its connection's, then
@@ -1419,7 +1432,7 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
1419
1432
  * Anything else a saved attachment can be counts as a file.
1420
1433
  */
1421
1434
  private savedAttachmentReactionFor;
1422
- reactMessageStatus(instanceName: string, chatId: string, messageId: string, status: DeliveryStatus, threadId?: string): void;
1435
+ reactMessageStatus(instanceName: string, chatId: string, messageId: string, status: DeliveryStatus, threadId?: string, receivedAt?: number): void;
1423
1436
  /**
1424
1437
  * Apply a terminal delivery verdict. On Telegram the built-in delivered is
1425
1438
  * ๐Ÿ‘€ and failed is ๐Ÿ‘Ž; Discord shows โœ…/โŒ โ€” both configurable (#1005).
@@ -1968,7 +1981,7 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
1968
1981
  /**
1969
1982
  * Where each backend natively loads on-demand skills from, relative to the
1970
1983
  * workspace. Backends without a native skill mechanism (opencode, grok,
1971
- * antigravity, gemini-cli) are deliberately absent: dropping files a CLI
1984
+ * antigravity) are deliberately absent: dropping files a CLI
1972
1985
  * never reads is clutter, not capability. Unknown directories are ignored
1973
1986
  * by older CLI versions, so publishing is fail-open across upgrades.
1974
1987
  */
@@ -14,7 +14,7 @@ import { formatUpdateProgress } from "./update-progress.js";
14
14
  import { sdNotify, sdNotifyBlocking } from "./sd-notify.js";
15
15
  import { readFleetMemory } from "./process-memory.js";
16
16
  import { MemoryPressure } from "./memory-pressure.js";
17
- import { ReplyDeduper } from "./reply-dedup.js";
17
+ import { replyDedupText, ReplyDeduper } from "./reply-dedup.js";
18
18
  import { isMap, isScalar, parseDocument } from "yaml";
19
19
  const __filename = fileURLToPath(import.meta.url);
20
20
  const __dirname = dirname(__filename);
@@ -64,7 +64,8 @@ import { LoginSession } from "./login-manager.js";
64
64
  import { tightenInstanceDirs } from "./private-dir.js";
65
65
  import { decideSlash } from "./slash-authz.js";
66
66
  import { commandSpec, decideCommand } from "./command-table.js";
67
- import { UPDATE_COMMAND, isPrereleaseVersion, updateNoticeKey } from "./update-check.js";
67
+ import { runVisibilityCommand } from "./cross-instance-notice.js";
68
+ import { UPDATE_COMMAND, installedChannel, isPrereleaseVersion, updateNoticeKey } from "./update-check.js";
68
69
  import { LoginController, LOGIN_TOKEN_RESEND_PREFIX, POST_LOGIN_RECOVERY_DEADLINE_MS } from "./login-controller.js";
69
70
  import { runBeforeDeadline } from "./deadline.js";
70
71
  import { LoginWindowLock } from "./login-window-lock.js";
@@ -76,6 +77,7 @@ import { handleAgentRequest, ToolNotPermittedError } from "./agent-endpoint.js";
76
77
  import { ClassicChannelManager, getClassicBackendChoices, isSelectableClassicBackend, readClassicLastActivityAt } from "./classic-channel-manager.js";
77
78
  import { assertExplicitInstanceRemoval } from "./instance-removal.js";
78
79
  import { validateFleetConfig } from "./config-validator.js";
80
+ import { isRemovedBackend, removedBackendMessage } from "./backend/removed.js";
79
81
  import { presentationState, interactionSummary } from "./interaction-observation.js";
80
82
  import { readLastInboundAt } from "./daemon.js";
81
83
  import { clearPausedMarker, readPausedAt, readPauseReason, writePausedMarker } from "./pause-marker.js";
@@ -139,6 +141,16 @@ export function selectLruEvictions(warm, cap, opts) {
139
141
  candidates.sort((a, b) => opts.lastInboundAt(a) - opts.lastInboundAt(b));
140
142
  return candidates.slice(0, warm.length - cap);
141
143
  }
144
+ /**
145
+ * Window-name shape this fleet recognises as an instance window: the
146
+ * `-t<digits>` form every allocator emits, plus legacy `classic-` windows.
147
+ * Startup cleanup reaps windows matching this that are no longer in
148
+ * fleet.yaml. Keep in sync with `uniqueInstanceName` โ€” every name it emits
149
+ * must match here, or a deleted instance's CLI is left running (#1305 P2-3).
150
+ */
151
+ export function isOrphanInstanceWindowName(name) {
152
+ return /-t\d+$/.test(name) || /^classic-/.test(name);
153
+ }
142
154
  /** Retry cadence for retiring a cancel button whose delete failed (e.g. a DC
143
155
  * forum thread the bot momentarily can't reach). 3 retries ร— 5min = 15min. */
144
156
  const CANCEL_BTN_RETRY_INTERVAL_MS = 5 * 60_000;
@@ -1007,7 +1019,10 @@ export class FleetManager {
1007
1019
  this.logger.debug({ hostMemory: snapshot }, "Host memory sample");
1008
1020
  }
1009
1021
  const changed = snapshot.level !== this.memoryLogLevel;
1010
- if (this.memoryPressure.allowsUnknown(snapshot)) {
1022
+ if (this.memoryPressure.advisoryOnly()) {
1023
+ // macOS (#1256): the sample is kept in the log for calibration, but nothing is sent to a channel.
1024
+ if (changed)
1025
+ this.logger.info({ hostMemory: snapshot }, "Host memory sample (macOS: logged only โ€” no notice, no spawn throttling)");
1011
1026
  this.memoryLogLevel = snapshot.level;
1012
1027
  return;
1013
1028
  }
@@ -1551,6 +1566,13 @@ export class FleetManager {
1551
1566
  const chatId = String(this.getChannelConfig(adapterId)?.group_id ?? data.channelId);
1552
1567
  await this.requestFullRestart(adapter, chatId, data.channelId, messageId);
1553
1568
  }
1569
+ /** #1302: `/visibility [mode]`. The dispatcher has already applied the command table's fleet-admin gate. */
1570
+ async handleVisibilitySlash(data) {
1571
+ if (!this.fleetConfig)
1572
+ return;
1573
+ const mode = typeof data.options?.mode === "string" ? data.options.mode : "";
1574
+ await data.respond(runVisibilityCommand(this.fleetConfig, mode, () => this.saveFleetConfig()));
1575
+ }
1554
1576
  async handleTipsSlash(data, adapterId) {
1555
1577
  if (!this.fleetConfig)
1556
1578
  return;
@@ -2427,6 +2449,9 @@ export class FleetManager {
2427
2449
  else if (data.command === "doctor") {
2428
2450
  await data.respond(await this.runBackendDoctor());
2429
2451
  }
2452
+ else if (data.command === "visibility") {
2453
+ await this.handleVisibilitySlash(data);
2454
+ }
2430
2455
  else if (data.command === "usage") {
2431
2456
  // Same permission level as /ctx (none). The reply is still ephemeral โ€”
2432
2457
  // the adapter defers non-chat commands that way โ€” so it never spams the
@@ -3804,7 +3829,7 @@ export class FleetManager {
3804
3829
  // Also kill orphaned windows: any window with a topic ID suffix (name-tNNNNN)
3805
3830
  // that isn't in the current config โ€” these are leftovers from deleted instances
3806
3831
  const isKnownInstance = agendNames.has(w.name);
3807
- const isOrphanedInstance = !isKnownInstance && (/-t\d+$/.test(w.name) || /^classic-/.test(w.name));
3832
+ const isOrphanedInstance = !isKnownInstance && isOrphanInstanceWindowName(w.name);
3808
3833
  if (isKnownInstance || isOrphanedInstance) {
3809
3834
  if (isOrphanedInstance)
3810
3835
  this.logger.info({ window: w.name }, "Cleaning up orphaned tmux window");
@@ -4510,7 +4535,7 @@ export class FleetManager {
4510
4535
  }
4511
4536
  const problems = [];
4512
4537
  const hostMemory = this.memoryPressure.snapshot();
4513
- if (hostMemory.level === "critical" || hostMemory.level === "elevated") {
4538
+ if (!this.memoryPressure.advisoryOnly() && (hostMemory.level === "critical" || hostMemory.level === "elevated")) {
4514
4539
  problems.push(`host memory pressure is ${hostMemory.level}`);
4515
4540
  }
4516
4541
  if (this.adapterState.size > 0 && connected === 0)
@@ -5652,10 +5677,9 @@ export class FleetManager {
5652
5677
  this.bindInstanceAdapter(generalInstance, msg.adapterId, true);
5653
5678
  const inboundAdapter = this.worlds.get(msg.adapterId ?? "")?.adapter ?? this.adapter;
5654
5679
  // React immediately โ€” before any other API calls โ€” through the status
5655
- // path, so a later delivery status replaces (not stacks onto) this
5656
- // acknowledgement.
5680
+ // path, which tracks Telegram slot ownership and Discord status adds.
5657
5681
  if (msg.chatId && msg.messageId) {
5658
- this.reactMessageStatus(generalInstance, msg.chatId, msg.messageId, "received", msg.threadId || undefined);
5682
+ this.reactMessageStatus(generalInstance, msg.chatId, msg.messageId, "received", msg.threadId || undefined, msg.timestamp.getTime());
5659
5683
  }
5660
5684
  this.warnIfRateLimited(generalInstance, msg);
5661
5685
  const { text, extraMeta } = await processAttachments(msg, inboundAdapter, this.logger, generalInstance);
@@ -5671,7 +5695,7 @@ export class FleetManager {
5671
5695
  user: msg.username,
5672
5696
  user_id: msg.userId,
5673
5697
  ts: msg.timestamp.toISOString(),
5674
- thread_id: "",
5698
+ thread_id: msg.threadId ?? "",
5675
5699
  // Fleet instances have an authoritative adapter binding. Multiple
5676
5700
  // bots in one guild can observe the same inbound message, so the
5677
5701
  // adapter whose event wins dedup is not necessarily the bot that
@@ -5744,10 +5768,10 @@ export class FleetManager {
5744
5768
  this.bindInstanceAdapter(instanceName, msg.adapterId, true);
5745
5769
  const inboundAdapter = this.worlds.get(msg.adapterId ?? "")?.adapter ?? this.adapter;
5746
5770
  // React immediately โ€” before any other Discord API calls โ€” through the
5747
- // status path, so a later delivery status replaces (not stacks onto) this
5748
- // acknowledgement. Same bound-adapter routing as the status path itself.
5771
+ // status path, which tracks Telegram slot ownership and Discord status adds.
5772
+ // Same bound-adapter routing as the status path itself.
5749
5773
  if (msg.chatId && msg.messageId) {
5750
- this.reactMessageStatus(instanceName, msg.chatId, msg.messageId, "received", msg.threadId || undefined);
5774
+ this.reactMessageStatus(instanceName, msg.chatId, msg.messageId, "received", msg.threadId || undefined, msg.timestamp.getTime());
5751
5775
  }
5752
5776
  // These may hit Discord API (topic icon, archive) โ€” do after react
5753
5777
  if (this.topicArchiver.isArchived(threadId)) {
@@ -5976,10 +6000,7 @@ export class FleetManager {
5976
6000
  respond(null, stickerProblem);
5977
6001
  return;
5978
6002
  }
5979
- const stickers = Array.isArray(args.stickers) ? args.stickers : [];
5980
- const ticket = this.replyDeduper.begin(instanceName,
5981
- // A reply with stickers is not the same reply as its text alone.
5982
- stickers.length ? `${String(args.text ?? "")}\u0000stickers:${stickers.join(",")}` : String(args.text ?? ""), Array.isArray(args.files) ? args.files : []);
6003
+ const ticket = this.replyDeduper.begin(instanceName, replyDedupText(args), Array.isArray(args.files) ? args.files : []);
5983
6004
  if (ticket.duplicate) {
5984
6005
  this.logger.info({ instanceName }, "Concurrent duplicate reply joined โ€” awaiting the original send's outcome");
5985
6006
  ticket.subscribe(respond);
@@ -6788,16 +6809,23 @@ export class FleetManager {
6788
6809
  }
6789
6810
  return adapter.createTopic(topicName);
6790
6811
  }
6791
- async deleteForumTopic(topicId) {
6812
+ async deleteForumTopic(topicId, adapterId) {
6792
6813
  try {
6793
- if (!this.adapter?.deleteTopic)
6814
+ const adapter = (adapterId ? this.worlds.get(adapterId)?.adapter : undefined) ?? this.adapter;
6815
+ if (!adapter?.deleteTopic)
6794
6816
  return;
6795
- await this.adapter.deleteTopic(topicId);
6817
+ await adapter.deleteTopic(topicId);
6796
6818
  }
6797
6819
  catch (err) {
6798
6820
  this.logger.warn({ err, topicId }, "Failed to delete forum topic during rollback");
6799
6821
  }
6800
6822
  }
6823
+ getForumTopicDeleter(adapterId) {
6824
+ const adapter = (adapterId ? this.worlds.get(adapterId)?.adapter : undefined) ?? this.adapter;
6825
+ if (!adapter?.deleteTopic)
6826
+ return null;
6827
+ return (topicId) => adapter.deleteTopic(topicId);
6828
+ }
6801
6829
  topicCleanupTimer = null;
6802
6830
  sessionPruneTimer = null;
6803
6831
  classicReloadTimer = null;
@@ -7883,6 +7911,25 @@ export class FleetManager {
7883
7911
  const resolved = this.resolveStatusEmojisFor(instanceName, adapterId, adapter);
7884
7912
  return reactionForm(resolved.platform, resolved.received);
7885
7913
  }
7914
+ reactClassicReceived(instanceName, adapter, msg) {
7915
+ const emoji = this.receivedReactionFor(instanceName, adapter, msg.adapterId);
7916
+ // Classic's system receipt is status-owned too. An ordinary react call
7917
+ // would label it as an agent/other reaction and suppress later statuses.
7918
+ if (adapter instanceof TelegramAdapter) {
7919
+ return adapter.reactDeliveryStatus(msg.chatId, msg.messageId, emoji, msg.timestamp.getTime()).then(() => { });
7920
+ }
7921
+ return adapter.react(msg.threadId ?? msg.chatId, msg.messageId, emoji);
7922
+ }
7923
+ reactClassicForwardedAttachment(instanceName, adapter, msg, kind) {
7924
+ const emoji = this.savedAttachmentReactionFor(instanceName, adapter, msg.adapterId, kind);
7925
+ // A forwarded attachment's saved stamp is ours, so later delivery statuses
7926
+ // may replace it. The receipt queued first establishes ownership; this
7927
+ // stamp cannot bootstrap an unknown slot or replace an agent's reaction.
7928
+ if (adapter instanceof TelegramAdapter) {
7929
+ return adapter.reactDeliveryStatus(msg.chatId, msg.messageId, emoji).then(() => { });
7930
+ }
7931
+ return adapter.react(msg.threadId ?? msg.chatId, msg.messageId, emoji);
7932
+ }
7886
7933
  /**
7887
7934
  * The stamp for an inbound photo / file a classic bot saved (#1080): the
7888
7935
  * instance's `status_emojis.photo|attachment`, then its connection's, then
@@ -7893,7 +7940,7 @@ export class FleetManager {
7893
7940
  const resolved = this.resolveStatusEmojisFor(instanceName, adapterId, adapter);
7894
7941
  return reactionForm(resolved.platform, kind === "photo" ? resolved.photo : resolved.attachment);
7895
7942
  }
7896
- reactMessageStatus(instanceName, chatId, messageId, status, threadId) {
7943
+ reactMessageStatus(instanceName, chatId, messageId, status, threadId, receivedAt) {
7897
7944
  // React via the adapter BOUND to this instance โ€” NOT the first discord world.
7898
7945
  // Otherwise, in a same-channel/same-guild multi-bot setup, the inbound ๐Ÿ‘€
7899
7946
  // (bound bot) and the delivery/confirm reactions (some other bot) come from
@@ -7914,7 +7961,7 @@ export class FleetManager {
7914
7961
  const adapterId = typeof adapter.id === "string"
7915
7962
  ? adapter.id : "?";
7916
7963
  const key = `${adapterId}:${chatId}:${threadId ?? ""}:${messageId}`;
7917
- this.queueDeliveryStatusReaction(adapter, key, chatId, messageId, statusEmoji, threadId, status);
7964
+ this.queueDeliveryStatusReaction(adapter, key, chatId, messageId, statusEmoji, threadId, status, status === "received" ? receivedAt : undefined);
7918
7965
  }
7919
7966
  /**
7920
7967
  * Apply a terminal delivery verdict. On Telegram the built-in delivered is
@@ -7923,19 +7970,30 @@ export class FleetManager {
7923
7970
  finishDeliveryStatus(instanceName, chatId, messageId, status, threadId) {
7924
7971
  this.reactMessageStatus(instanceName, chatId, messageId, status, threadId);
7925
7972
  }
7926
- queueDeliveryStatusReaction(adapter, key, chatId, messageId, emoji, threadId, status) {
7973
+ queueDeliveryStatusReaction(adapter, key, chatId, messageId, emoji, threadId, status, receivedAt) {
7927
7974
  const prev = this.deliveryStatusChains.get(key) ?? Promise.resolve();
7928
- const run = prev.then(() => this.applyDeliveryStatusReaction(adapter, key, chatId, messageId, emoji, threadId, status));
7975
+ const run = prev.then(() => this.applyDeliveryStatusReaction(adapter, key, chatId, messageId, emoji, threadId, status, receivedAt));
7929
7976
  this.deliveryStatusChains.set(key, run);
7930
7977
  void run.then(() => { if (this.deliveryStatusChains.get(key) === run)
7931
7978
  this.deliveryStatusChains.delete(key); }, () => { if (this.deliveryStatusChains.get(key) === run)
7932
7979
  this.deliveryStatusChains.delete(key); });
7933
7980
  }
7934
- async applyDeliveryStatusReaction(adapter, key, chatId, messageId, emoji, threadId, status) {
7981
+ async applyDeliveryStatusReaction(adapter, key, chatId, messageId, emoji, threadId, status, receivedAt) {
7935
7982
  try {
7936
7983
  const target = adapter instanceof TelegramAdapter ? chatId : (threadId ?? chatId);
7937
7984
  const last = this.lastStatusEmoji.get(key);
7938
7985
  const prev = last?.emoji;
7986
+ if (adapter instanceof TelegramAdapter) {
7987
+ // The adapter owns the one-reaction slot and serializes this check
7988
+ // with agent react calls. The fleet's last emoji is not ownership.
7989
+ if (await adapter.reactDeliveryStatus(chatId, messageId, emoji, receivedAt)) {
7990
+ if (emoji == null)
7991
+ this.lastStatusEmoji.delete(key);
7992
+ else
7993
+ this.lastStatusEmoji.set(key, { emoji, status });
7994
+ }
7995
+ return;
7996
+ }
7939
7997
  if (emoji == null) {
7940
7998
  if (prev && adapter.unreact) {
7941
7999
  await adapter.unreact(target, messageId, prev, threadId);
@@ -7943,9 +8001,7 @@ export class FleetManager {
7943
8001
  }
7944
8002
  return;
7945
8003
  }
7946
- // A Telegram status may map multiple lifecycle states to the same
7947
- // reaction. Avoid resending it: an agent may have replaced the one
7948
- // reaction slot with its own ๐Ÿ‘ between delivered and confirmed.
8004
+ // Discord adds reactions rather than replacing a single slot.
7949
8005
  if (prev === emoji)
7950
8006
  return;
7951
8007
  // Thread-aware adapters (Discord) react where the thread is; Telegram
@@ -9847,9 +9903,8 @@ export class FleetManager {
9847
9903
  const installed = this.probeInstalledBackends();
9848
9904
  // One entry point for "get this CLI working" (#1131): a backend that is
9849
9905
  // not installed is offered too, and the click installs it first, then
9850
- // signs in (startLoginSession routes it). gemini-cli is not recommended,
9851
- // as before; `/login gemini-cli` still installs it.
9852
- const installable = new Set(Object.keys(BACKEND_INSTALLATION_INFO).filter(backend => backend !== "gemini-cli"));
9906
+ // signs in (startLoginSession routes it).
9907
+ const installable = new Set(Object.keys(BACKEND_INSTALLATION_INFO));
9853
9908
  const candidates = new Set([...installed, ...configured, ...installable]);
9854
9909
  const unsupported = [];
9855
9910
  const choices = [...candidates].sort().flatMap(backend => {
@@ -9873,11 +9928,13 @@ export class FleetManager {
9873
9928
  return [];
9874
9929
  });
9875
9930
  if (unsupported.length) {
9876
- const guidance = unsupported.map(({ backend, flow, status }) => `${backend} ยท ${status.join(" ยท ")} โ€” ${!installed.has(backend)
9877
- ? (BACKEND_INSTALLATION_INFO[backend] ? t("login.install_by_name", backend) : t("login.install_on_host", backend))
9878
- : flow?.remoteLogin === "unsupported"
9879
- ? t("login.remote_unsupported_agent_cli", backend, flow.command)
9880
- : backend === "opencode" ? t("login.unsupported", backend) : t("login.no_remote_flow", backend)}`).join("\n");
9931
+ const guidance = unsupported.map(({ backend, flow, status }) => `${backend} ยท ${status.join(" ยท ")} โ€” ${isRemovedBackend(backend)
9932
+ ? removedBackendMessage(backend)
9933
+ : !installed.has(backend)
9934
+ ? (BACKEND_INSTALLATION_INFO[backend] ? t("login.install_by_name", backend) : t("login.install_on_host", backend))
9935
+ : flow?.remoteLogin === "unsupported"
9936
+ ? t("login.remote_unsupported_agent_cli", backend, flow.command)
9937
+ : backend === "opencode" ? t("login.unsupported", backend) : t("login.no_remote_flow", backend)}`).join("\n");
9881
9938
  await chat.adapter.sendText(chat.chatId, guidance, { threadId: chat.threadId })
9882
9939
  .catch(err => this.logger.warn({ err }, "Failed to post unsupported login guidance"));
9883
9940
  }
@@ -10024,6 +10081,9 @@ export class FleetManager {
10024
10081
  if (opts.skipAuthCheck)
10025
10082
  return this.launchSignIn(backendArg, chat, opts);
10026
10083
  const wanted = LOGIN_BACKEND_ALIASES[backendArg.toLowerCase()] ?? backendArg.toLowerCase();
10084
+ // A removed backend (#1280) is neither installed nor signed into: say what replaces it.
10085
+ if (isRemovedBackend(wanted))
10086
+ return removedBackendMessage(wanted);
10027
10087
  if (BACKEND_INSTALLATION_INFO[wanted] && !this.isCliInstalled(wanted)) {
10028
10088
  const flow = LOGIN_FLOWS[wanted];
10029
10089
  this.recordLoginFlow(wanted, flow && flow.remoteLogin !== "unsupported" ? "install_then_login" : "install_only", chat.userId);
@@ -10538,7 +10598,6 @@ export class FleetManager {
10538
10598
  static INSTRUCTIONS_FILENAME = {
10539
10599
  "claude-code": "CLAUDE.md",
10540
10600
  "codex": "AGENTS.md",
10541
- "gemini-cli": "GEMINI.md",
10542
10601
  "opencode": "AGENTS.md",
10543
10602
  "kiro-cli": ".kiro/steering/project.md",
10544
10603
  // Grok reads AGENTS.md project docs; agy reads .agents/agents.md โ€” the
@@ -10609,7 +10668,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
10609
10668
  /**
10610
10669
  * Where each backend natively loads on-demand skills from, relative to the
10611
10670
  * workspace. Backends without a native skill mechanism (opencode, grok,
10612
- * antigravity, gemini-cli) are deliberately absent: dropping files a CLI
10671
+ * antigravity) are deliberately absent: dropping files a CLI
10613
10672
  * never reads is clutter, not capability. Unknown directories are ignored
10614
10673
  * by older CLI versions, so publishing is fail-open across upgrades.
10615
10674
  */
@@ -10946,7 +11005,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
10946
11005
  const classicAdapter = this.worlds.get(msg.adapterId ?? "")?.adapter ?? this.adapter;
10947
11006
  const collabReactChatId = msg.threadId ?? msg.chatId;
10948
11007
  if (classicAdapter && collabReactChatId && msg.messageId) {
10949
- classicAdapter.react(collabReactChatId, msg.messageId, this.receivedReactionFor(instanceName, classicAdapter, msg.adapterId))
11008
+ this.reactClassicReceived(instanceName, classicAdapter, msg)
10950
11009
  .catch(e => this.logger.debug({ err: e.message }, "Auto-react failed"));
10951
11010
  }
10952
11011
  // Block /raw bypass โ€” check the mention-stripped text so the self-marker
@@ -10955,8 +11014,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
10955
11014
  return;
10956
11015
  // Attachments already saved at the top of the collab block.
10957
11016
  if (saved && classicAdapter && collabReactChatId && msg.messageId) {
10958
- const emoji = this.savedAttachmentReactionFor(instanceName, classicAdapter, msg.adapterId, saved.kind);
10959
- classicAdapter.react(collabReactChatId, msg.messageId, emoji)
11017
+ this.reactClassicForwardedAttachment(instanceName, classicAdapter, msg, saved.kind)
10960
11018
  .catch(e => this.logger.debug({ err: e.message }, "Auto-react failed"));
10961
11019
  }
10962
11020
  // Strip saved attachment to avoid double download
@@ -11045,12 +11103,10 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
11045
11103
  }
11046
11104
  }
11047
11105
  if (msg.chatId && msg.messageId) {
11048
- const reactChatId = msg.threadId ?? msg.chatId;
11049
- classicMsgAdapter.react(reactChatId, msg.messageId, this.receivedReactionFor(instanceName, classicMsgAdapter, msg.adapterId))
11106
+ this.reactClassicReceived(instanceName, classicMsgAdapter, msg)
11050
11107
  .catch(e => this.logger.debug({ err: e.message }, "Auto-react failed"));
11051
11108
  if (saved) {
11052
- const savedEmoji = this.savedAttachmentReactionFor(instanceName, classicMsgAdapter, msg.adapterId, saved.kind);
11053
- classicMsgAdapter.react(reactChatId, msg.messageId, savedEmoji)
11109
+ this.reactClassicForwardedAttachment(instanceName, classicMsgAdapter, msg, saved.kind)
11054
11110
  .catch(e => this.logger.debug({ err: e.message }, "Auto-react failed"));
11055
11111
  }
11056
11112
  }
@@ -14289,17 +14345,18 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
14289
14345
  const currentVersion = JSON.parse(readFileSync(pkgPath, "utf-8")).version ?? "0.0.0";
14290
14346
  const latest = await npmVersion("@songsid/agend");
14291
14347
  let target = latest;
14292
- // The same channel rule as `agend update` (any SemVer prerelease is on @beta): rc and alpha too.
14293
- if (isPrereleaseVersion(currentVersion)) {
14294
- // Beta users track the @beta channel (never fall back to @latest, which is
14295
- // older), but should also hear when a newer STABLE ships โ€” pick whichever
14296
- // of beta/latest is the newest.
14297
- let beta = "";
14348
+ // The same channel rule as `agend update` (#1259): an alpha follows @alpha, any other prerelease @beta.
14349
+ const channel = installedChannel(currentVersion);
14350
+ if (channel !== "latest") {
14351
+ // Prerelease users track their own channel (never fall back to @latest,
14352
+ // which is older, and an alpha is never offered @beta), but should also
14353
+ // hear when a newer STABLE ships โ€” pick whichever is the newest.
14354
+ let pre = "";
14298
14355
  try {
14299
- beta = await npmVersion("@songsid/agend@beta");
14356
+ pre = await npmVersion(`@songsid/agend@${channel}`);
14300
14357
  }
14301
- catch { /* no beta tag */ }
14302
- target = beta || latest;
14358
+ catch { /* no such tag */ }
14359
+ target = pre || latest;
14303
14360
  if (latest && this.semverGt(latest, target))
14304
14361
  target = latest;
14305
14362
  }