@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.
- package/dist/agent-cli.js +36 -4
- package/dist/agent-cli.js.map +1 -1
- package/dist/agent-endpoint.d.ts +3 -1
- package/dist/agent-endpoint.js +7 -1
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/backend/claude-code.d.ts +2 -0
- package/dist/backend/claude-code.js +2 -2
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/session-signals.d.ts +154 -0
- package/dist/backend/session-signals.js +430 -0
- package/dist/backend/session-signals.js.map +1 -0
- package/dist/channel/adapters/discord.d.ts +38 -1
- package/dist/channel/adapters/discord.js +131 -8
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +19 -1
- package/dist/channel/adapters/telegram.js +55 -0
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/mcp-tools.js +5 -1
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/channel/message-queue.d.ts +6 -0
- package/dist/channel/message-queue.js +21 -0
- package/dist/channel/message-queue.js.map +1 -1
- package/dist/channel/sticker-download.d.ts +14 -0
- package/dist/channel/sticker-download.js +42 -0
- package/dist/channel/sticker-download.js.map +1 -0
- package/dist/channel/tool-router.js +12 -5
- package/dist/channel/tool-router.js.map +1 -1
- package/dist/channel/types.d.ts +52 -0
- package/dist/cli-flags.d.ts +8 -0
- package/dist/cli-flags.js +22 -0
- package/dist/cli-flags.js.map +1 -0
- package/dist/config-validator.js +25 -0
- package/dist/config-validator.js.map +1 -1
- package/dist/daemon.d.ts +20 -0
- package/dist/daemon.js +155 -3
- package/dist/daemon.js.map +1 -1
- package/dist/delivery-outbox.d.ts +8 -0
- package/dist/delivery-outbox.js.map +1 -1
- package/dist/event-loop-watch.d.ts +25 -0
- package/dist/event-loop-watch.js +40 -0
- package/dist/event-loop-watch.js.map +1 -0
- package/dist/fleet-manager.d.ts +38 -3
- package/dist/fleet-manager.js +248 -12
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/persona-emoji/SKILL.md +4 -2
- package/dist/locale.js +4 -0
- package/dist/locale.js.map +1 -1
- package/dist/outbound-schemas.d.ts +15 -1
- package/dist/outbound-schemas.js +19 -1
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/tool-permissions.js +8 -1
- package/dist/tool-permissions.js.map +1 -1
- package/dist/turn-reply-guard.d.ts +7 -0
- package/dist/turn-reply-guard.js +11 -0
- package/dist/turn-reply-guard.js.map +1 -1
- package/dist/turn-resume.d.ts +80 -0
- package/dist/turn-resume.js +217 -0
- package/dist/turn-resume.js.map +1 -0
- package/dist/ui/view.html +8 -2
- package/dist/usage/format-rich.js +6 -1
- package/dist/usage/format-rich.js.map +1 -1
- package/dist/usage/i18n.d.ts +2 -0
- package/dist/usage/i18n.js +14 -1
- package/dist/usage/i18n.js.map +1 -1
- package/dist/usage/providers.d.ts +10 -0
- package/dist/usage/providers.js +2 -0
- package/dist/usage/providers.js.map +1 -1
- package/dist/usage/usage-api.d.ts +9 -0
- package/dist/usage/usage-api.js +31 -2
- package/dist/usage/usage-api.js.map +1 -1
- package/package.json +1 -1
package/dist/fleet-manager.js
CHANGED
|
@@ -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
|
-
|
|
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"
|
|
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.
|
|
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).
|