@songsid/agend 2.1.11 → 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/README.md +23 -15
- package/README.zh-TW.md +50 -11
- 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 +4 -0
- package/dist/backend/claude-code.js +65 -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/backend/types.d.ts +25 -0
- package/dist/backend/types.js.map +1 -1
- 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/classic-bindings.d.ts +48 -0
- package/dist/classic-bindings.js +128 -0
- package/dist/classic-bindings.js.map +1 -0
- package/dist/classic-channel-manager.d.ts +21 -0
- package/dist/classic-channel-manager.js +31 -0
- package/dist/classic-channel-manager.js.map +1 -1
- 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 +61 -0
- package/dist/daemon.js +454 -19
- 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/delivery-reconciliation.d.ts +13 -0
- package/dist/delivery-reconciliation.js +71 -7
- package/dist/delivery-reconciliation.js.map +1 -1
- package/dist/delivery-reconciliation.test.js +115 -2
- package/dist/delivery-reconciliation.test.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 +52 -4
- package/dist/fleet-manager.js +323 -20
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/persona-emoji/SKILL.md +4 -2
- package/dist/instance-lifecycle.js +4 -2
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/interaction-observation.d.ts +32 -0
- package/dist/interaction-observation.js +118 -0
- package/dist/interaction-observation.js.map +1 -0
- package/dist/kiro-engine-status.d.ts +2 -0
- package/dist/kiro-engine-status.js +3 -1
- package/dist/kiro-engine-status.js.map +1 -1
- package/dist/locale.js +26 -0
- package/dist/locale.js.map +1 -1
- package/dist/outbound-handlers.d.ts +2 -0
- package/dist/outbound-handlers.js +28 -11
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +15 -1
- package/dist/outbound-schemas.js +20 -2
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/output-log-tail.d.ts +21 -0
- package/dist/output-log-tail.js +89 -0
- package/dist/output-log-tail.js.map +1 -0
- 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 +21 -0
- package/dist/usage/providers.js +36 -7
- 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";
|
|
@@ -75,6 +76,7 @@ import { handleAgentRequest, ToolNotPermittedError } from "./agent-endpoint.js";
|
|
|
75
76
|
import { ClassicChannelManager, getClassicBackendChoices, isSelectableClassicBackend, readClassicLastActivityAt } from "./classic-channel-manager.js";
|
|
76
77
|
import { assertExplicitInstanceRemoval } from "./instance-removal.js";
|
|
77
78
|
import { validateFleetConfig } from "./config-validator.js";
|
|
79
|
+
import { presentationState, interactionSummary } from "./interaction-observation.js";
|
|
78
80
|
import { readLastInboundAt } from "./daemon.js";
|
|
79
81
|
import { clearPausedMarker, readPausedAt, readPauseReason, writePausedMarker } from "./pause-marker.js";
|
|
80
82
|
import { DEFAULT_WARM_OVERFLOW, WakeCoordinator } from "./wake-coordinator.js";
|
|
@@ -97,7 +99,8 @@ import { PROVIDER_SECRET_SPECS, providerSecretSpec, providerRegistryEnvKeys, isR
|
|
|
97
99
|
/** A self-restart is a whole service restart; 120s is the apply budget, not this. */
|
|
98
100
|
const SELF_RESTART_DEADLINE_MS = 300_000;
|
|
99
101
|
import { APPLY_FLEET_TARGET, ApplyJobStore, viewOf, } from "./apply-job.js";
|
|
100
|
-
import { instanceCredentialProfile } from "./backend/credential-profile.js";
|
|
102
|
+
import { credentialSwitchStartsFresh, instanceCredentialProfile } from "./backend/credential-profile.js";
|
|
103
|
+
import { mergeBackendOptions, profileKey, profileName, profileOf } from "./classic-bindings.js";
|
|
101
104
|
import { classifyInstanceChange, CLASSIC_HOT_CONFIG_KEYS, HOT_INSTANCE_CONFIG_KEYS, hotConfigUpdate, } from "./instance-config-impact.js";
|
|
102
105
|
import { formatRestartProgressCompletion, RESTART_PROGRESS_TERMINAL_TIMEOUT_MS, RestartProgress, } from "./restart-progress.js";
|
|
103
106
|
import { launchFullRestartHelper } from "./full-restart.js";
|
|
@@ -297,6 +300,21 @@ export function deriveSpawnConcurrency(freeMemMB, cores) {
|
|
|
297
300
|
const REPLY_OBLIGATION_SWEEP_MS = 30_000;
|
|
298
301
|
const REPLY_REMINDER_GRACE_MS = 60_000;
|
|
299
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
|
+
}
|
|
300
318
|
export class FleetManager {
|
|
301
319
|
dataDir;
|
|
302
320
|
static signalTarget = null;
|
|
@@ -550,6 +568,7 @@ export class FleetManager {
|
|
|
550
568
|
/** Days of event/activity history to keep. */
|
|
551
569
|
static EVENT_LOG_RETENTION_DAYS = 30;
|
|
552
570
|
watchdogTimer = null;
|
|
571
|
+
eventLoopWatch = null;
|
|
553
572
|
startedAt = 0;
|
|
554
573
|
stormOpenNotifyTimer = null;
|
|
555
574
|
memoryLogAt = null;
|
|
@@ -1774,6 +1793,21 @@ export class FleetManager {
|
|
|
1774
1793
|
return null;
|
|
1775
1794
|
return this.instanceStateCache.get(name)?.state ?? null;
|
|
1776
1795
|
}
|
|
1796
|
+
/** Pure read of the single daemon-owned observation; no probe/capture or second tracker. */
|
|
1797
|
+
getInstanceInteraction(name) {
|
|
1798
|
+
if (this.lifecycle.isPaused(name) || this.instanceProcessStatus.has(name))
|
|
1799
|
+
return null;
|
|
1800
|
+
const daemon = this.lifecycle.daemons.get(name);
|
|
1801
|
+
if (daemon?.getProcessStatus?.() !== "running")
|
|
1802
|
+
return null;
|
|
1803
|
+
return daemon.getInteractionSnapshot?.() ?? null;
|
|
1804
|
+
}
|
|
1805
|
+
instancePresentation(name) {
|
|
1806
|
+
const execution_state = this.getInstanceExecutionState(name);
|
|
1807
|
+
const interaction = this.getInstanceInteraction(name);
|
|
1808
|
+
return { state: presentationState(execution_state, interaction), execution_state,
|
|
1809
|
+
interaction, interaction_summary: interactionSummary(interaction) };
|
|
1810
|
+
}
|
|
1777
1811
|
/**
|
|
1778
1812
|
* Subscription providers currently relevant to this fleet. Stopped/crashed
|
|
1779
1813
|
* rows do not keep a usage card alive, while persisted paused instances do.
|
|
@@ -1805,6 +1839,34 @@ export class FleetManager {
|
|
|
1805
1839
|
}
|
|
1806
1840
|
return providers;
|
|
1807
1841
|
}
|
|
1842
|
+
/** #1220: the login a running classic instance was launched on, from its own config; null when not running. */
|
|
1843
|
+
classicLaunchedProfile(name) {
|
|
1844
|
+
const launched = this.daemons.get(name)?.getConfigSnapshot?.();
|
|
1845
|
+
return launched ? profileOf(launched.backend_options, launched.backend ?? "claude-code") : null;
|
|
1846
|
+
}
|
|
1847
|
+
/**
|
|
1848
|
+
* #1220: whether a running classic instance's login differs from what its
|
|
1849
|
+
* configuration now says — compared with what it was launched on, not with an
|
|
1850
|
+
* earlier reading of the config (a reload consumes that), and with an invalid
|
|
1851
|
+
* setting as a value of its own rather than the shared login it would fall to.
|
|
1852
|
+
* A switch on the same backend that loses the conversation (kiro) starts fresh.
|
|
1853
|
+
*/
|
|
1854
|
+
classicProfileChange(name, backendChanged) {
|
|
1855
|
+
const launched = this.classicLaunchedProfile(name);
|
|
1856
|
+
if (!launched || !this.classicChannels)
|
|
1857
|
+
return { changed: false, startsFresh: false };
|
|
1858
|
+
const next = this.classicChannels.getCredentialProfileByInstance(name, this.fleetConfig?.defaults);
|
|
1859
|
+
if (profileKey(launched) === profileKey(next))
|
|
1860
|
+
return { changed: false, startsFresh: false };
|
|
1861
|
+
const backend = this.classicChannels.getBackendByInstance(name, this.fleetConfig?.defaults?.backend);
|
|
1862
|
+
return {
|
|
1863
|
+
changed: true,
|
|
1864
|
+
// A backend change keeps its own (existing) restart semantics.
|
|
1865
|
+
startsFresh: !backendChanged && credentialSwitchStartsFresh(backend),
|
|
1866
|
+
from: profileKey(launched),
|
|
1867
|
+
to: profileKey(next),
|
|
1868
|
+
};
|
|
1869
|
+
}
|
|
1808
1870
|
/** `[instance, effective backend, credential profile]` for everything that is
|
|
1809
1871
|
* running or paused — the one place both usage views agree on who is live. */
|
|
1810
1872
|
activeBackendBindings() {
|
|
@@ -1823,8 +1885,9 @@ export class FleetManager {
|
|
|
1823
1885
|
}
|
|
1824
1886
|
for (const channel of this.classicChannels?.getAll() ?? []) {
|
|
1825
1887
|
const backend = this.classicChannels?.getBackendByInstance(channel.instanceName, this.fleetConfig?.defaults?.backend);
|
|
1826
|
-
//
|
|
1827
|
-
add(channel.instanceName, backend,
|
|
1888
|
+
// The login it was launched on; before a launch, the one it would get (#1220).
|
|
1889
|
+
add(channel.instanceName, backend, profileName(this.classicLaunchedProfile(channel.instanceName)
|
|
1890
|
+
?? this.classicChannels.getCredentialProfileByInstance(channel.instanceName, this.fleetConfig?.defaults)));
|
|
1828
1891
|
}
|
|
1829
1892
|
return bindings;
|
|
1830
1893
|
}
|
|
@@ -2693,9 +2756,15 @@ export class FleetManager {
|
|
|
2693
2756
|
const backendChanged = oldBackends.get(ch.instanceName) !== newBackend;
|
|
2694
2757
|
const modelChanged = oldModels.get(ch.instanceName) !== newModel;
|
|
2695
2758
|
const autoPauseChanged = oldAutoPause.get(ch.instanceName) !== newAutoPause;
|
|
2696
|
-
|
|
2697
|
-
|
|
2759
|
+
// #1220: a running CLI keeps the login it was launched with, so a new
|
|
2760
|
+
// credential_profile only takes effect through a restart.
|
|
2761
|
+
const { changed: profileChanged, startsFresh: profileSwitchStartsFresh, from: profileFrom, to: profileTo } = this.classicProfileChange(ch.instanceName, backendChanged);
|
|
2762
|
+
if (this.daemons.has(ch.instanceName) && (backendChanged || modelChanged || autoPauseChanged || profileChanged)) {
|
|
2763
|
+
this.logger.info({ instanceName: ch.instanceName, backendFrom: oldBackends.get(ch.instanceName), backendTo: newBackend, modelFrom: oldModels.get(ch.instanceName), modelTo: newModel,
|
|
2764
|
+
...(profileChanged ? { profileFrom, profileTo } : {}) }, "Backend/model/profile changed — restarting");
|
|
2698
2765
|
await this.stopInstance(ch.instanceName).catch(() => { });
|
|
2766
|
+
if (profileSwitchStartsFresh)
|
|
2767
|
+
this.writeFreshStartMarker(ch.instanceName);
|
|
2699
2768
|
// Small delay to let tmux window clean up
|
|
2700
2769
|
await new Promise(r => setTimeout(r, 2000));
|
|
2701
2770
|
// The manager already holds the new backend/model/auto-pause; the
|
|
@@ -4012,6 +4081,10 @@ export class FleetManager {
|
|
|
4012
4081
|
// instance would get a restart loop. Those conditions surface through /health
|
|
4013
4082
|
// (which now returns 503) and through the General-topic notifications instead.
|
|
4014
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 });
|
|
4015
4088
|
// EventLog.prune() existed but was never called, so `events` and `activity`
|
|
4016
4089
|
// grew without bound for the life of the install. Prune once at startup and
|
|
4017
4090
|
// daily after that; the timer is unref'd so it never holds the loop open.
|
|
@@ -5897,7 +5970,16 @@ export class FleetManager {
|
|
|
5897
5970
|
// still in flight and about to succeed. One real send, everyone gets its
|
|
5898
5971
|
// outcome; a genuinely failed send clears the entry so a retry passes.
|
|
5899
5972
|
if (tool === "reply") {
|
|
5900
|
-
|
|
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 : []);
|
|
5901
5983
|
if (ticket.duplicate) {
|
|
5902
5984
|
this.logger.info({ instanceName }, "Concurrent duplicate reply joined — awaiting the original send's outcome");
|
|
5903
5985
|
ticket.subscribe(respond);
|
|
@@ -6188,7 +6270,8 @@ export class FleetManager {
|
|
|
6188
6270
|
: type.startsWith("fleet_decision_") ? "fleet_decision_response"
|
|
6189
6271
|
: type === "fleet_task" ? "fleet_task_response"
|
|
6190
6272
|
: type === "fleet_set_display_name" ? "fleet_display_name_response"
|
|
6191
|
-
: 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"
|
|
6192
6275
|
: "fleet_description_response";
|
|
6193
6276
|
ipc.send({ type: responseType, fleetRequestId, error: message });
|
|
6194
6277
|
}
|
|
@@ -6214,21 +6297,24 @@ export class FleetManager {
|
|
|
6214
6297
|
this.handleSetDescription(name, msg);
|
|
6215
6298
|
return;
|
|
6216
6299
|
}
|
|
6217
|
-
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") {
|
|
6218
6302
|
this.handlePersonaEmoji(name, msg);
|
|
6219
6303
|
return;
|
|
6220
6304
|
}
|
|
6221
6305
|
}
|
|
6222
|
-
/** `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. */
|
|
6223
6307
|
handlePersonaEmoji(instanceName, msg) {
|
|
6224
6308
|
const fleetRequestId = msg.fleetRequestId;
|
|
6225
6309
|
const payload = (msg.payload ?? {});
|
|
6226
6310
|
const ipc = this.instanceIpcClients.get(instanceName);
|
|
6227
6311
|
if (!ipc)
|
|
6228
6312
|
return;
|
|
6229
|
-
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)
|
|
6230
6314
|
: msg.type === "fleet_preview_emojis" ? this.previewEmojis(instanceName, payload)
|
|
6231
|
-
: 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);
|
|
6232
6318
|
op.then((r) => typeof r.error === "string"
|
|
6233
6319
|
? ipc.send({ type: "fleet_persona_emoji_response", fleetRequestId, error: r.error })
|
|
6234
6320
|
: ipc.send({ type: "fleet_persona_emoji_response", fleetRequestId, result: r }), (err) => ipc.send({ type: "fleet_persona_emoji_response", fleetRequestId, error: err.message }));
|
|
@@ -6579,8 +6665,14 @@ export class FleetManager {
|
|
|
6579
6665
|
return { error: `Instance '${instance}' not found` };
|
|
6580
6666
|
return { display_name: name };
|
|
6581
6667
|
}
|
|
6582
|
-
async handleListEmojisHttp(instance, refresh) {
|
|
6583
|
-
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);
|
|
6584
6676
|
}
|
|
6585
6677
|
async handleSetPersonaEmojiHttp(instance, args) {
|
|
6586
6678
|
return this.setPersonaEmoji(instance, args);
|
|
@@ -7311,7 +7403,7 @@ export class FleetManager {
|
|
|
7311
7403
|
* bot can draw on — the same lists, and the same resolution, as the
|
|
7312
7404
|
* Settings picker (#1005/#1021), so an agent and an operator see one truth.
|
|
7313
7405
|
*/
|
|
7314
|
-
async listEmojisFor(instanceName, refresh = false) {
|
|
7406
|
+
async listEmojisFor(instanceName, refresh = false, opts = {}) {
|
|
7315
7407
|
// Seeing is not setting: a ClassicBot instance has no per-instance stamp,
|
|
7316
7408
|
// but it reacts in its own channel like any bot and must be able to see
|
|
7317
7409
|
// the emojis it can use (only set_persona_emoji refuses it).
|
|
@@ -7334,18 +7426,25 @@ export class FleetManager {
|
|
|
7334
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." } : {}),
|
|
7335
7427
|
};
|
|
7336
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);
|
|
7337
7433
|
const listed = await this.listGuildEmojis(worldId, refresh);
|
|
7434
|
+
let left = filter.limit;
|
|
7338
7435
|
out.server_emojis = listed.ok
|
|
7339
|
-
? listed.guilds.map(g => g.emojis
|
|
7436
|
+
? listed.guilds.filter(g => !filter.primaryOnly || g.primary).map(g => g.emojis
|
|
7340
7437
|
? {
|
|
7341
7438
|
server: g.name || g.id, primary: g.primary,
|
|
7342
|
-
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 => {
|
|
7343
7440
|
const value = customEmojiValue(e);
|
|
7344
|
-
return { value, image_url: emojiImageUrl(value) };
|
|
7441
|
+
return filter.withImageUrls ? { value, image_url: emojiImageUrl(value) } : { value };
|
|
7345
7442
|
}),
|
|
7346
7443
|
}
|
|
7347
7444
|
: { server: g.name || g.id, primary: g.primary, error: g.error })
|
|
7348
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).";
|
|
7349
7448
|
}
|
|
7350
7449
|
return out;
|
|
7351
7450
|
}
|
|
@@ -7403,6 +7502,193 @@ export class FleetManager {
|
|
|
7403
7502
|
}
|
|
7404
7503
|
return { previews, errors, note: "Read each path to see the emoji (a static PNG)." };
|
|
7405
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
|
+
}
|
|
7406
7692
|
static EMOJI_PREVIEW_MAX = 8;
|
|
7407
7693
|
static EMOJI_PREVIEW_MAX_BYTES = 256 * 1024;
|
|
7408
7694
|
static EMOJI_PREVIEW_TTL_MS = 24 * 60 * 60_000;
|
|
@@ -12123,6 +12409,17 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
12123
12409
|
...(autoPauseAfter !== undefined ? { auto_pause_after: autoPauseAfter } : {}),
|
|
12124
12410
|
...(preTaskCommand ? { pre_task_command: preTaskCommand } : {}),
|
|
12125
12411
|
};
|
|
12412
|
+
// #1220: a channel's own backend_options (credential_profile) override the
|
|
12413
|
+
// fleet defaults' per backend, as a fleet.yaml instance's do.
|
|
12414
|
+
config.backend_options = mergeBackendOptions(config.backend_options, classicIdentity?.backendOptions);
|
|
12415
|
+
// ClassicBot has no validator in front of its launch, and a backend that
|
|
12416
|
+
// cannot read a profile name falls back to the shared login. Refuse here,
|
|
12417
|
+
// so a bad name never runs the agent on the wrong subscription.
|
|
12418
|
+
const profile = profileOf(config.backend_options, config.backend ?? "claude-code");
|
|
12419
|
+
if (profile.state === "invalid") {
|
|
12420
|
+
throw new Error(`Classic instance '${instanceName}' has an invalid credential_profile (${profile.reason}) — `
|
|
12421
|
+
+ "fix it in classicBot.yaml; it will not start on another login");
|
|
12422
|
+
}
|
|
12126
12423
|
const topicMode = this.fleetConfig?.channel?.mode === "topic";
|
|
12127
12424
|
await this.startInstance(instanceName, config, topicMode, "classic", false, transition);
|
|
12128
12425
|
}
|
|
@@ -12209,6 +12506,8 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
12209
12506
|
clearInterval(this.watchdogTimer);
|
|
12210
12507
|
this.watchdogTimer = null;
|
|
12211
12508
|
}
|
|
12509
|
+
this.eventLoopWatch?.stop();
|
|
12510
|
+
this.eventLoopWatch = null;
|
|
12212
12511
|
// A login/install window is a dedicated tmux server with its own TTL
|
|
12213
12512
|
// timer and HTTP listener living in THIS process: without an explicit
|
|
12214
12513
|
// shutdown it would outlive us as an owner-less login CLI (sol B3).
|
|
@@ -12631,10 +12930,13 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
12631
12930
|
const backend = this.classicChannels.getBackend(ch.channelId, ch.adapterId, this.fleetConfig?.defaults?.backend);
|
|
12632
12931
|
const model = this.classicChannels.getModel(ch.channelId, ch.adapterId, this.fleetConfig?.defaults?.model);
|
|
12633
12932
|
const autoPauseAfter = this.classicChannels.getAutoPauseAfter(ch.channelId, ch.adapterId, this.fleetConfig?.defaults?.auto_pause_after);
|
|
12634
|
-
|
|
12635
|
-
|
|
12933
|
+
const profile = this.classicProfileChange(ch.instanceName, old.backend !== backend);
|
|
12934
|
+
if (old.backend !== backend || old.model !== model || old.autoPauseAfter !== autoPauseAfter || profile.changed) {
|
|
12935
|
+
this.logger.info({ instanceName: ch.instanceName, ...(profile.changed ? { profileFrom: profile.from, profileTo: profile.to } : {}) }, "Classic cold config changed — restarting");
|
|
12636
12936
|
observe?.(ch.instanceName, "restart", "running");
|
|
12637
12937
|
await this.stopInstance(ch.instanceName).catch(() => { });
|
|
12938
|
+
if (profile.startsFresh)
|
|
12939
|
+
this.writeFreshStartMarker(ch.instanceName);
|
|
12638
12940
|
await this.startClassicInstanceUnattended(ch, "classic instance after fleet reload");
|
|
12639
12941
|
observe?.(ch.instanceName, "restart", "done");
|
|
12640
12942
|
continue;
|
|
@@ -14247,7 +14549,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
14247
14549
|
lastActivity,
|
|
14248
14550
|
currentTask,
|
|
14249
14551
|
idle: this.getInstanceIdle(inst.name),
|
|
14250
|
-
|
|
14552
|
+
...this.instancePresentation(inst.name),
|
|
14251
14553
|
};
|
|
14252
14554
|
});
|
|
14253
14555
|
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
@@ -14504,6 +14806,7 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
14504
14806
|
backend,
|
|
14505
14807
|
effort,
|
|
14506
14808
|
effort_source,
|
|
14809
|
+
...this.instancePresentation(name),
|
|
14507
14810
|
};
|
|
14508
14811
|
});
|
|
14509
14812
|
return {
|