@songsid/agend 2.1.12-beta.1 → 2.1.12-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 (71) hide show
  1. package/dist/agent-cli.js +36 -4
  2. package/dist/agent-cli.js.map +1 -1
  3. package/dist/agent-endpoint.d.ts +3 -1
  4. package/dist/agent-endpoint.js +7 -1
  5. package/dist/agent-endpoint.js.map +1 -1
  6. package/dist/backend/claude-code.d.ts +2 -0
  7. package/dist/backend/claude-code.js +2 -2
  8. package/dist/backend/claude-code.js.map +1 -1
  9. package/dist/backend/session-signals.d.ts +154 -0
  10. package/dist/backend/session-signals.js +430 -0
  11. package/dist/backend/session-signals.js.map +1 -0
  12. package/dist/channel/adapters/discord.d.ts +38 -1
  13. package/dist/channel/adapters/discord.js +131 -8
  14. package/dist/channel/adapters/discord.js.map +1 -1
  15. package/dist/channel/adapters/telegram.d.ts +19 -1
  16. package/dist/channel/adapters/telegram.js +55 -0
  17. package/dist/channel/adapters/telegram.js.map +1 -1
  18. package/dist/channel/mcp-tools.js +5 -1
  19. package/dist/channel/mcp-tools.js.map +1 -1
  20. package/dist/channel/message-queue.d.ts +6 -0
  21. package/dist/channel/message-queue.js +21 -0
  22. package/dist/channel/message-queue.js.map +1 -1
  23. package/dist/channel/sticker-download.d.ts +14 -0
  24. package/dist/channel/sticker-download.js +42 -0
  25. package/dist/channel/sticker-download.js.map +1 -0
  26. package/dist/channel/tool-router.js +12 -5
  27. package/dist/channel/tool-router.js.map +1 -1
  28. package/dist/channel/types.d.ts +52 -0
  29. package/dist/cli-flags.d.ts +8 -0
  30. package/dist/cli-flags.js +22 -0
  31. package/dist/cli-flags.js.map +1 -0
  32. package/dist/config-validator.js +25 -0
  33. package/dist/config-validator.js.map +1 -1
  34. package/dist/daemon.d.ts +20 -0
  35. package/dist/daemon.js +155 -3
  36. package/dist/daemon.js.map +1 -1
  37. package/dist/delivery-outbox.d.ts +8 -0
  38. package/dist/delivery-outbox.js.map +1 -1
  39. package/dist/event-loop-watch.d.ts +25 -0
  40. package/dist/event-loop-watch.js +40 -0
  41. package/dist/event-loop-watch.js.map +1 -0
  42. package/dist/fleet-manager.d.ts +38 -3
  43. package/dist/fleet-manager.js +248 -12
  44. package/dist/fleet-manager.js.map +1 -1
  45. package/dist/general-knowledge/skills/persona-emoji/SKILL.md +4 -2
  46. package/dist/locale.js +4 -0
  47. package/dist/locale.js.map +1 -1
  48. package/dist/outbound-schemas.d.ts +15 -1
  49. package/dist/outbound-schemas.js +19 -1
  50. package/dist/outbound-schemas.js.map +1 -1
  51. package/dist/tool-permissions.js +8 -1
  52. package/dist/tool-permissions.js.map +1 -1
  53. package/dist/turn-reply-guard.d.ts +7 -0
  54. package/dist/turn-reply-guard.js +11 -0
  55. package/dist/turn-reply-guard.js.map +1 -1
  56. package/dist/turn-resume.d.ts +80 -0
  57. package/dist/turn-resume.js +217 -0
  58. package/dist/turn-resume.js.map +1 -0
  59. package/dist/ui/view.html +8 -2
  60. package/dist/usage/format-rich.js +6 -1
  61. package/dist/usage/format-rich.js.map +1 -1
  62. package/dist/usage/i18n.d.ts +2 -0
  63. package/dist/usage/i18n.js +14 -1
  64. package/dist/usage/i18n.js.map +1 -1
  65. package/dist/usage/providers.d.ts +10 -0
  66. package/dist/usage/providers.js +2 -0
  67. package/dist/usage/providers.js.map +1 -1
  68. package/dist/usage/usage-api.d.ts +9 -0
  69. package/dist/usage/usage-api.js +31 -2
  70. package/dist/usage/usage-api.js.map +1 -1
  71. package/package.json +1 -1
@@ -26,6 +26,7 @@ import { loadFleetConfig, loadRawFleetConfig, DEFAULT_COST_GUARD, DEFAULT_DAILY_
26
26
  import { EventLog } from "./event-log.js";
27
27
  import { AdapterWorld } from "./adapter-world.js";
28
28
  import { CostGuard, formatCents } from "./cost-guard.js";
29
+ import { startEventLoopWatch } from "./event-loop-watch.js";
29
30
  import { TmuxManager } from "./tmux-manager.js";
30
31
  import { AccessManager } from "./channel/access-manager.js";
31
32
  import { IpcClient } from "./channel/ipc-bridge.js";
@@ -299,6 +300,21 @@ export function deriveSpawnConcurrency(freeMemMB, cores) {
299
300
  const REPLY_OBLIGATION_SWEEP_MS = 30_000;
300
301
  const REPLY_REMINDER_GRACE_MS = 60_000;
301
302
  const DEFAULT_REPLY_OVERDUE_MINUTES = 15;
303
+ /**
304
+ * The filter `list_emojis` and `list_stickers` take (#1226). Values may come from the agent CLI as strings, so a
305
+ * limit is read as a number and the flags accept "true". No limit means all of them — never zero.
306
+ */
307
+ function emojiListFilter(opts) {
308
+ const name = typeof opts.name === "string" ? opts.name.trim().toLowerCase() : "";
309
+ const n = Number(opts.limit);
310
+ const flag = (v) => v === true || v === "true";
311
+ return {
312
+ matches: (...texts) => !name || texts.some(t => t.toLowerCase().includes(name)),
313
+ limit: Number.isInteger(n) && n >= 1 ? n : Infinity,
314
+ primaryOnly: flag(opts.primary_only),
315
+ withImageUrls: flag(opts.with_image_urls),
316
+ };
317
+ }
302
318
  export class FleetManager {
303
319
  dataDir;
304
320
  static signalTarget = null;
@@ -552,6 +568,7 @@ export class FleetManager {
552
568
  /** Days of event/activity history to keep. */
553
569
  static EVENT_LOG_RETENTION_DAYS = 30;
554
570
  watchdogTimer = null;
571
+ eventLoopWatch = null;
555
572
  startedAt = 0;
556
573
  stormOpenNotifyTimer = null;
557
574
  memoryLogAt = null;
@@ -4064,6 +4081,10 @@ export class FleetManager {
4064
4081
  // instance would get a restart loop. Those conditions surface through /health
4065
4082
  // (which now returns 503) and through the General-topic notifications instead.
4066
4083
  this.watchdogTimer = setInterval(() => sdNotify("WATCHDOG=1"), 30_000);
4084
+ // #1231: a stalled loop is what turns a slash command into "did not respond";
4085
+ // put each long stall in the log instead of leaving it to be guessed at.
4086
+ this.eventLoopWatch?.stop();
4087
+ this.eventLoopWatch = startEventLoopWatch({ logger: this.logger });
4067
4088
  // EventLog.prune() existed but was never called, so `events` and `activity`
4068
4089
  // grew without bound for the life of the install. Prune once at startup and
4069
4090
  // daily after that; the timer is unref'd so it never holds the loop open.
@@ -5949,7 +5970,16 @@ export class FleetManager {
5949
5970
  // still in flight and about to succeed. One real send, everyone gets its
5950
5971
  // outcome; a genuinely failed send clears the entry so a retry passes.
5951
5972
  if (tool === "reply") {
5952
- const ticket = this.replyDeduper.begin(instanceName, String(args.text ?? ""), Array.isArray(args.files) ? args.files : []);
5973
+ // Stickers (#1226) are checked before anything is sent: a refused one is the reply's error, not a gap.
5974
+ const stickerProblem = await this.replyStickerProblem(outAdapter, args, threadId, contextAdapterId ?? this.getInstanceAdapterId(senderInstanceName ?? instanceName));
5975
+ if (stickerProblem) {
5976
+ respond(null, stickerProblem);
5977
+ return;
5978
+ }
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 : []);
5953
5983
  if (ticket.duplicate) {
5954
5984
  this.logger.info({ instanceName }, "Concurrent duplicate reply joined — awaiting the original send's outcome");
5955
5985
  ticket.subscribe(respond);
@@ -6240,7 +6270,8 @@ export class FleetManager {
6240
6270
  : type.startsWith("fleet_decision_") ? "fleet_decision_response"
6241
6271
  : type === "fleet_task" ? "fleet_task_response"
6242
6272
  : type === "fleet_set_display_name" ? "fleet_display_name_response"
6243
- : type === "fleet_list_emojis" || type === "fleet_set_persona_emoji" || type === "fleet_preview_emojis" ? "fleet_persona_emoji_response"
6273
+ : type === "fleet_list_emojis" || type === "fleet_set_persona_emoji" || type === "fleet_preview_emojis"
6274
+ || type === "fleet_list_stickers" || type === "fleet_preview_stickers" ? "fleet_persona_emoji_response"
6244
6275
  : "fleet_description_response";
6245
6276
  ipc.send({ type: responseType, fleetRequestId, error: message });
6246
6277
  }
@@ -6266,21 +6297,24 @@ export class FleetManager {
6266
6297
  this.handleSetDescription(name, msg);
6267
6298
  return;
6268
6299
  }
6269
- if (type === "fleet_list_emojis" || type === "fleet_set_persona_emoji" || type === "fleet_preview_emojis") {
6300
+ if (type === "fleet_list_emojis" || type === "fleet_set_persona_emoji" || type === "fleet_preview_emojis"
6301
+ || type === "fleet_list_stickers" || type === "fleet_preview_stickers") {
6270
6302
  this.handlePersonaEmoji(name, msg);
6271
6303
  return;
6272
6304
  }
6273
6305
  }
6274
- /** `list_emojis` / `set_persona_emoji` / `preview_emojis` over IPC; the agent endpoint calls the same methods. */
6306
+ /** `list_emojis` / `set_persona_emoji` / `preview_emojis` / `list_stickers` / `preview_stickers` over IPC; the agent endpoint calls the same methods. */
6275
6307
  handlePersonaEmoji(instanceName, msg) {
6276
6308
  const fleetRequestId = msg.fleetRequestId;
6277
6309
  const payload = (msg.payload ?? {});
6278
6310
  const ipc = this.instanceIpcClients.get(instanceName);
6279
6311
  if (!ipc)
6280
6312
  return;
6281
- const op = msg.type === "fleet_list_emojis" ? this.listEmojisFor(instanceName, payload.refresh === true)
6313
+ const op = msg.type === "fleet_list_emojis" ? this.listEmojisFor(instanceName, payload.refresh === true, payload)
6282
6314
  : msg.type === "fleet_preview_emojis" ? this.previewEmojis(instanceName, payload)
6283
- : this.setPersonaEmoji(instanceName, payload);
6315
+ : msg.type === "fleet_list_stickers" ? this.listStickersFor(instanceName, payload)
6316
+ : msg.type === "fleet_preview_stickers" ? this.previewStickers(instanceName, payload)
6317
+ : this.setPersonaEmoji(instanceName, payload);
6284
6318
  op.then((r) => typeof r.error === "string"
6285
6319
  ? ipc.send({ type: "fleet_persona_emoji_response", fleetRequestId, error: r.error })
6286
6320
  : ipc.send({ type: "fleet_persona_emoji_response", fleetRequestId, result: r }), (err) => ipc.send({ type: "fleet_persona_emoji_response", fleetRequestId, error: err.message }));
@@ -6631,8 +6665,14 @@ export class FleetManager {
6631
6665
  return { error: `Instance '${instance}' not found` };
6632
6666
  return { display_name: name };
6633
6667
  }
6634
- async handleListEmojisHttp(instance, refresh) {
6635
- return this.listEmojisFor(instance, refresh);
6668
+ async handleListEmojisHttp(instance, refresh, args = {}) {
6669
+ return this.listEmojisFor(instance, refresh, args);
6670
+ }
6671
+ async handleListStickersHttp(instance, args) {
6672
+ return this.listStickersFor(instance, args);
6673
+ }
6674
+ async handlePreviewStickersHttp(instance, args) {
6675
+ return this.previewStickers(instance, args);
6636
6676
  }
6637
6677
  async handleSetPersonaEmojiHttp(instance, args) {
6638
6678
  return this.setPersonaEmoji(instance, args);
@@ -7363,7 +7403,7 @@ export class FleetManager {
7363
7403
  * bot can draw on — the same lists, and the same resolution, as the
7364
7404
  * Settings picker (#1005/#1021), so an agent and an operator see one truth.
7365
7405
  */
7366
- async listEmojisFor(instanceName, refresh = false) {
7406
+ async listEmojisFor(instanceName, refresh = false, opts = {}) {
7367
7407
  // Seeing is not setting: a ClassicBot instance has no per-instance stamp,
7368
7408
  // but it reacts in its own channel like any bot and must be able to see
7369
7409
  // the emojis it can use (only set_persona_emoji refuses it).
@@ -7386,18 +7426,25 @@ export class FleetManager {
7386
7426
  ...(classic ? { note: "ClassicBot instances have no per-instance status emojis: the statuses above are the connection's, which an operator sets in Settings. You can still use any emoji listed here in your own reactions." } : {}),
7387
7427
  };
7388
7428
  if (platform === "discord" && worldId) {
7429
+ // #1226: a busy server's list was ~10k characters for an agent that wanted one emoji. Filter by name, cap
7430
+ // the count, keep to the primary server if asked, and leave the image URLs out unless asked
7431
+ // (preview_emojis is how an agent looks at one).
7432
+ const filter = emojiListFilter(opts);
7389
7433
  const listed = await this.listGuildEmojis(worldId, refresh);
7434
+ let left = filter.limit;
7390
7435
  out.server_emojis = listed.ok
7391
- ? listed.guilds.map(g => g.emojis
7436
+ ? listed.guilds.filter(g => !filter.primaryOnly || g.primary).map(g => g.emojis
7392
7437
  ? {
7393
7438
  server: g.name || g.id, primary: g.primary,
7394
- emojis: g.emojis.filter(e => e.available).map(e => {
7439
+ emojis: g.emojis.filter(e => e.available && filter.matches(e.name)).filter(() => left-- > 0).map(e => {
7395
7440
  const value = customEmojiValue(e);
7396
- return { value, image_url: emojiImageUrl(value) };
7441
+ return filter.withImageUrls ? { value, image_url: emojiImageUrl(value) } : { value };
7397
7442
  }),
7398
7443
  }
7399
7444
  : { server: g.name || g.id, primary: g.primary, error: g.error })
7400
7445
  : { error: listed.error };
7446
+ if (!filter.withImageUrls)
7447
+ out.server_emojis_note = "Image URLs are left out; preview_emojis downloads the ones you want to look at (or pass with_image_urls).";
7401
7448
  }
7402
7449
  return out;
7403
7450
  }
@@ -7455,6 +7502,193 @@ export class FleetManager {
7455
7502
  }
7456
7503
  return { previews, errors, note: "Read each path to see the emoji (a static PNG)." };
7457
7504
  }
7505
+ // ── Stickers (#1226) ─────────────────────────────────────────────────────
7506
+ /** Where an instance talks: its connection, and its chat (and topic). The same address its notices go to. */
7507
+ instanceChatTarget(instanceName) {
7508
+ const worldId = this.getInstanceAdapterId(instanceName);
7509
+ const adapter = (worldId ? this.worlds.get(worldId)?.adapter : undefined) ?? this.getAdapterForInstance(instanceName) ?? this.adapter ?? undefined;
7510
+ const topic = this.fleetConfig?.instances[instanceName]?.topic_id;
7511
+ const groupId = this.getChannelConfig(worldId)?.group_id;
7512
+ if (topic != null && groupId != null)
7513
+ return { worldId, adapter, chatId: String(groupId), threadId: String(topic) };
7514
+ const classicChat = this.classicChannels?.getChannelIdByInstance(instanceName);
7515
+ return { worldId, adapter, chatId: classicChat ?? (groupId != null ? String(groupId) : undefined) };
7516
+ }
7517
+ static STICKER_LIST_TTL_MS = 10 * 60_000;
7518
+ /** Keyed `<connection>\0<scope key>` (a Discord channel, or "set:<name>"). */
7519
+ stickerCache = new Map();
7520
+ /** One sticker list through the adapter, cached; `refresh` forces a fetch. Errors are the platform's, reworded. */
7521
+ async stickerListFor(worldId, adapter, target, refresh = false) {
7522
+ const key = `${worldId}\0${target.set ? `set:${target.set}` : `chat:${target.threadId ?? target.chatId ?? ""}`}`;
7523
+ const cached = this.stickerCache.get(key);
7524
+ if (!refresh && cached && Date.now() - cached.fetched_at < FleetManager.STICKER_LIST_TTL_MS)
7525
+ return cached.list;
7526
+ const list = await adapter.listStickers(target);
7527
+ this.stickerCache.set(key, { fetched_at: Date.now(), list });
7528
+ return list;
7529
+ }
7530
+ /** The Telegram sticker sets an instance lists by default: its connection's `options.sticker_sets`. */
7531
+ defaultStickerSets(worldId) {
7532
+ const sets = this.getChannelConfig(worldId)?.options?.sticker_sets;
7533
+ return Array.isArray(sets) ? sets.filter((x) => typeof x === "string" && x.trim() !== "").slice(0, 10) : [];
7534
+ }
7535
+ /**
7536
+ * `list_stickers` (#1226): the stickers this instance can send where it talks, one shape on both platforms —
7537
+ * `{ id, name, emoji_or_tags, format }` and no image URLs (preview_stickers shows them). Discord: its channel's
7538
+ * server only — a bot cannot send another server's stickers, so listing them would only cost context.
7539
+ * Telegram: the set named, or the connection's `options.sticker_sets`.
7540
+ */
7541
+ async listStickersFor(instanceName, args) {
7542
+ if (!this.ownInstanceConfig(instanceName) && !this.isClassicPersonaTarget(instanceName)) {
7543
+ return { error: this.personaEmojiMissing(instanceName) };
7544
+ }
7545
+ const { worldId, adapter, chatId, threadId } = this.instanceChatTarget(instanceName);
7546
+ if (!worldId || !adapter?.listStickers)
7547
+ return { error: "this channel has no stickers to list" };
7548
+ const filter = emojiListFilter(args);
7549
+ const refresh = args.refresh === true || args.refresh === "true";
7550
+ const telegram = adapter.type === "telegram";
7551
+ const requested = typeof args.set === "string" && args.set.trim() ? [args.set.trim()] : [];
7552
+ const sets = telegram ? (requested.length ? requested : this.defaultStickerSets(worldId)) : [];
7553
+ if (telegram && !sets.length) {
7554
+ return { error: "name a sticker set: set=<name> (the <name> in t.me/addstickers/<name>), or have an operator add options.sticker_sets to this connection" };
7555
+ }
7556
+ const targets = telegram ? sets.map(set => ({ set })) : [{ chatId, threadId }];
7557
+ const lists = [];
7558
+ let left = filter.limit;
7559
+ for (const target of targets) {
7560
+ try {
7561
+ const list = await this.stickerListFor(worldId, adapter, target, refresh);
7562
+ const stickers = list.stickers
7563
+ .filter(st => st.available && filter.matches(st.name, st.emoji_or_tags))
7564
+ .filter(() => left-- > 0)
7565
+ .map(({ id, name, emoji_or_tags, format }) => ({ id, name, emoji_or_tags, format }));
7566
+ lists.push({ scope: list.scope, stickers });
7567
+ }
7568
+ catch (err) {
7569
+ lists.push({ scope: target.set ? `set ${target.set}` : "this server", error: `cannot list stickers: ${err.message}` });
7570
+ }
7571
+ }
7572
+ return {
7573
+ platform: adapter.type,
7574
+ lists,
7575
+ note: "Send one with reply({ stickers: [id] }) — up to 3, never in the text. preview_stickers shows what they look like.",
7576
+ };
7577
+ }
7578
+ /**
7579
+ * `preview_stickers` (#1226), the sticker twin of preview_emojis: the fleet downloads a still picture of each named
7580
+ * sticker and returns a local path to Read. Only ids this instance's list_stickers returned (from the cache it
7581
+ * filled) — nothing the caller writes reaches a URL. A Lottie sticker, or an animated one with no thumbnail, has no
7582
+ * still picture: it is listed as preview_unavailable.
7583
+ */
7584
+ async previewStickers(instanceName, args) {
7585
+ if (!this.ownInstanceConfig(instanceName) && !this.isClassicPersonaTarget(instanceName)) {
7586
+ return { error: this.personaEmojiMissing(instanceName) };
7587
+ }
7588
+ const wanted = args.stickers;
7589
+ if (!Array.isArray(wanted) || wanted.length === 0 || wanted.some(x => typeof x !== "string" || !x)) {
7590
+ return { error: "stickers is required: a list of ids from list_stickers" };
7591
+ }
7592
+ if (wanted.length > FleetManager.EMOJI_PREVIEW_MAX)
7593
+ return { error: `at most ${FleetManager.EMOJI_PREVIEW_MAX} at a time: narrow them down by name first` };
7594
+ const { worldId, adapter } = this.instanceChatTarget(instanceName);
7595
+ if (!worldId || !adapter?.fetchStickerPreview)
7596
+ return { error: "this channel has no stickers to preview" };
7597
+ const known = new Map();
7598
+ for (const [key, entry] of this.stickerCache) {
7599
+ if (key.startsWith(`${worldId}\0`))
7600
+ for (const st of entry.list.stickers)
7601
+ known.set(st.id, st);
7602
+ }
7603
+ const previews = [];
7604
+ const unavailable = [];
7605
+ const errors = [];
7606
+ // In parallel: eight sequential downloads could outlast the tool's budget.
7607
+ const outcomes = await Promise.all(wanted.map(async (id) => {
7608
+ const st = known.get(id);
7609
+ if (!st)
7610
+ return { id, error: "not a sticker list_stickers returned here; list them first" };
7611
+ try {
7612
+ return { id, st, picture: await adapter.fetchStickerPreview(st) };
7613
+ }
7614
+ catch (err) {
7615
+ return { id, error: `download failed: ${err.message}` };
7616
+ }
7617
+ }));
7618
+ for (const o of outcomes) {
7619
+ if ("error" in o) {
7620
+ errors.push({ sticker: o.id, error: o.error });
7621
+ continue;
7622
+ }
7623
+ if (!o.picture) {
7624
+ unavailable.push({ sticker: o.id, name: o.st.name, reason: `preview_unavailable: a ${o.st.format} sticker has no still picture` });
7625
+ continue;
7626
+ }
7627
+ previews.push({ sticker: o.id, name: o.st.name, path: this.storeStickerPreview(o.id, o.picture) });
7628
+ }
7629
+ return { previews, unavailable, errors, note: "Read each path to see the sticker (a still picture)." };
7630
+ }
7631
+ /** A preview under the inbox, named by a hash of the id (a Telegram file_id is long), pruned after a day. */
7632
+ storeStickerPreview(id, picture) {
7633
+ const dir = join(this.dataDir, "inbox", "sticker-previews");
7634
+ mkdirSync(dir, { recursive: true });
7635
+ const now = Date.now();
7636
+ for (const name of readdirSync(dir)) {
7637
+ try {
7638
+ if (now - statSync(join(dir, name)).mtimeMs > FleetManager.EMOJI_PREVIEW_TTL_MS)
7639
+ unlinkSync(join(dir, name));
7640
+ }
7641
+ catch { /* raced */ }
7642
+ }
7643
+ const path = join(dir, `${createHash("sha256").update(id).digest("hex").slice(0, 24)}.${picture.ext}`);
7644
+ const tmp = `${path}.${process.pid}.tmp`;
7645
+ writeFileSync(tmp, picture.bytes);
7646
+ renameSync(tmp, path);
7647
+ return path;
7648
+ }
7649
+ /**
7650
+ * `reply.stickers` (#1226), checked before anything is sent — a refused sticker is an error the agent sees, never
7651
+ * a reply that silently arrives without it. At most 3, text or stickers required. Discord: each must be an available
7652
+ * sticker of the server the reply goes to (another server's cannot be sent). Telegram: any sticker's file_id can be
7653
+ * sent anywhere; only its shape is checked here, and Telegram's own refusal still comes back as the reply's error.
7654
+ */
7655
+ async replyStickerProblem(adapter, args, threadId, worldId) {
7656
+ const raw = args.stickers;
7657
+ const hasText = typeof args.text === "string" && args.text.length > 0;
7658
+ if (raw === undefined || (Array.isArray(raw) && raw.length === 0))
7659
+ return hasText ? null : "reply: text is required (or stickers)";
7660
+ if (!Array.isArray(raw) || raw.some(x => typeof x !== "string" || !x.trim()))
7661
+ return "reply: stickers must be a list of sticker ids from list_stickers";
7662
+ const ids = raw;
7663
+ if (ids.length > 3)
7664
+ return "reply: at most 3 stickers per message";
7665
+ if (new Set(ids).size !== ids.length)
7666
+ return "reply: the same sticker twice";
7667
+ if (!adapter.sendStickers || !adapter.listStickers)
7668
+ return "reply: this channel cannot send stickers";
7669
+ if (adapter.type === "telegram") {
7670
+ const bad = ids.find(id => !/^[A-Za-z0-9_-]{10,255}$/.test(id));
7671
+ return bad ? `reply: ${bad} is not a Telegram sticker id (use an id from list_stickers)` : null;
7672
+ }
7673
+ const target = { chatId: typeof args.chat_id === "string" ? args.chat_id : undefined, threadId };
7674
+ let list;
7675
+ try {
7676
+ list = await this.stickerListFor(worldId ?? adapter.id ?? adapter.type, adapter, target);
7677
+ if (ids.some(id => !list.stickers.some(st => st.id === id)))
7678
+ list = await this.stickerListFor(worldId ?? adapter.id ?? adapter.type, adapter, target, true);
7679
+ }
7680
+ catch (err) {
7681
+ return `reply: cannot check the stickers for this channel: ${err.message}`;
7682
+ }
7683
+ for (const id of ids) {
7684
+ const st = list.stickers.find(x => x.id === id);
7685
+ if (!st)
7686
+ return `reply: sticker ${id} cannot be sent here — only stickers of this channel's server can (${list.scope}); call list_stickers`;
7687
+ if (!st.available)
7688
+ return `reply: sticker ${st.name} (${id}) is unavailable on this server (it lost the boost level it needs)`;
7689
+ }
7690
+ return null;
7691
+ }
7458
7692
  static EMOJI_PREVIEW_MAX = 8;
7459
7693
  static EMOJI_PREVIEW_MAX_BYTES = 256 * 1024;
7460
7694
  static EMOJI_PREVIEW_TTL_MS = 24 * 60 * 60_000;
@@ -12272,6 +12506,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
12272
12506
  clearInterval(this.watchdogTimer);
12273
12507
  this.watchdogTimer = null;
12274
12508
  }
12509
+ this.eventLoopWatch?.stop();
12510
+ this.eventLoopWatch = null;
12275
12511
  // A login/install window is a dedicated tmux server with its own TTL
12276
12512
  // timer and HTTP listener living in THIS process: without an explicit
12277
12513
  // shutdown it would outlive us as an owner-less login CLI (sol B3).