@songsid/agend 2.1.8-beta.1 → 2.1.8-beta.3
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 +9 -0
- 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/cli-env-probe-worker.d.ts +1 -0
- package/dist/backend/cli-env-probe-worker.js +25 -0
- package/dist/backend/cli-env-probe-worker.js.map +1 -0
- package/dist/backend/codex.js +15 -1
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/muse.js +10 -1
- package/dist/backend/muse.js.map +1 -1
- package/dist/backend/types.d.ts +12 -1
- package/dist/backend/types.js +14 -3
- package/dist/backend/types.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +16 -4
- package/dist/channel/adapters/discord.js +20 -5
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/mcp-tools.js +4 -0
- package/dist/channel/mcp-tools.js.map +1 -1
- package/dist/daemon.d.ts +31 -0
- package/dist/daemon.js +107 -25
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-context.d.ts +5 -0
- package/dist/fleet-context.js.map +1 -1
- package/dist/fleet-manager.d.ts +47 -11
- package/dist/fleet-manager.js +322 -61
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/persona-emoji/SKILL.md +45 -0
- package/dist/locale.js +18 -0
- package/dist/locale.js.map +1 -1
- package/dist/outbound-schemas.d.ts +18 -4
- package/dist/outbound-schemas.js +8 -0
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/settings-api.d.ts +6 -8
- package/dist/settings-api.js +9 -4
- package/dist/settings-api.js.map +1 -1
- package/dist/status-emojis.d.ts +19 -0
- package/dist/status-emojis.js.map +1 -1
- package/dist/tool-permissions.js +6 -2
- package/dist/tool-permissions.js.map +1 -1
- package/dist/topic-commands.d.ts +9 -0
- package/dist/topic-commands.js +41 -2
- package/dist/topic-commands.js.map +1 -1
- package/dist/ui/settings.html +23 -11
- package/package.json +1 -1
package/dist/fleet-context.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ import type { Logger } from "./logger.js";
|
|
|
6
6
|
import type { CostGuard } from "./cost-guard.js";
|
|
7
7
|
import type { ClassicChannelManager } from "./classic-channel-manager.js";
|
|
8
8
|
import type { ExplicitInstanceRemoval } from "./instance-removal.js";
|
|
9
|
+
import type { BackendCliVersionSnapshot } from "./backend/types.js";
|
|
9
10
|
export type RouteTarget = {
|
|
10
11
|
kind: "instance";
|
|
11
12
|
name: string;
|
|
@@ -68,6 +69,10 @@ export interface FleetContext {
|
|
|
68
69
|
/** Classic-bot channels (defined in classicBot.yaml, not fleet.yaml instances). */
|
|
69
70
|
readonly classicChannels: ClassicChannelManager | null;
|
|
70
71
|
getSysInfo(): SysInfo;
|
|
72
|
+
/** Read cached CLI versions without triggering a probe. */
|
|
73
|
+
getBackendCliVersionSnapshot?(): BackendCliVersionSnapshot;
|
|
74
|
+
/** Refresh stale/missing CLI versions after the sysinfo reply has been sent. */
|
|
75
|
+
refreshBackendCliVersions?(): void;
|
|
71
76
|
getInstanceStatus(name: string): "running" | "paused" | "stopped" | "crashed";
|
|
72
77
|
/** Current cancellation epoch stamped onto direct daemon IPC deliveries. */
|
|
73
78
|
getDeliveryEpoch?(name: string): number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"fleet-context.js","sourceRoot":"","sources":["../src/fleet-context.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"fleet-context.js","sourceRoot":"","sources":["../src/fleet-context.ts"],"names":[],"mappings":"AAkCA,MAAM,UAAU,sBAAsB,CAAC,MAAmB;IACxD,OAAO,MAAM,CAAC,IAAI,KAAK,UAAU,CAAC;AACpC,CAAC"}
|
package/dist/fleet-manager.d.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { type FleetMemory } from "./process-memory.js";
|
|
2
2
|
import { ReplyDeduper } from "./reply-dedup.js";
|
|
3
3
|
import type { FleetConfig, RawFleetConfig, InstanceConfig } from "./types.js";
|
|
4
|
-
import { type DeliveryStatus, type ResolvedStatusEmojis } from "./status-emojis.js";
|
|
4
|
+
import { type DeliveryStatus, type GuildEmoji, type GuildEmojiGroup, type ResolvedStatusEmojis } from "./status-emojis.js";
|
|
5
5
|
import { type RouteTarget } from "./fleet-context.js";
|
|
6
6
|
import { EventLog } from "./event-log.js";
|
|
7
7
|
import { AdapterWorld } from "./adapter-world.js";
|
|
8
8
|
import { CostGuard } from "./cost-guard.js";
|
|
9
9
|
import { IpcClient } from "./channel/ipc-bridge.js";
|
|
10
10
|
import type { AdapterHealthSnapshot, ChannelAdapter, InboundReaction } from "./channel/types.js";
|
|
11
|
+
import { type BackendCliVersionSnapshot } from "./backend/types.js";
|
|
11
12
|
import { type Logger } from "./logger.js";
|
|
12
13
|
import { Scheduler } from "./scheduler/index.js";
|
|
13
14
|
import type { FleetContext } from "./fleet-context.js";
|
|
@@ -960,6 +961,8 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
960
961
|
*/
|
|
961
962
|
private refuseTypedIpc;
|
|
962
963
|
private dispatchTypedIpc;
|
|
964
|
+
/** `list_emojis` / `set_persona_emoji` over IPC; the agent endpoint calls the same two methods. */
|
|
965
|
+
private handlePersonaEmoji;
|
|
963
966
|
private handleScheduleCrud;
|
|
964
967
|
private handleDecisionCrud;
|
|
965
968
|
/** Resolve display name for an instance, fallback to instance name. */
|
|
@@ -987,6 +990,8 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
987
990
|
handleDecisionCrudHttp(instance: string, op: string, args: Record<string, unknown>): Promise<unknown>;
|
|
988
991
|
handleTaskCrudHttp(instance: string, args: Record<string, unknown>): Promise<unknown>;
|
|
989
992
|
handleSetDisplayNameHttp(instance: string, name: string): Promise<unknown>;
|
|
993
|
+
handleListEmojisHttp(instance: string, refresh: boolean): Promise<unknown>;
|
|
994
|
+
handleSetPersonaEmojiHttp(instance: string, args: Record<string, unknown>): Promise<unknown>;
|
|
990
995
|
handleSetDescriptionHttp(instance: string, description: string): Promise<unknown>;
|
|
991
996
|
private summarizeToolCall;
|
|
992
997
|
private handleTaskCrud;
|
|
@@ -1077,6 +1082,8 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
1077
1082
|
private deliveryStatusChains;
|
|
1078
1083
|
/** #725: in-flight CLI env probes, keyed by backend name. Coalesces concurrent /model requests. */
|
|
1079
1084
|
private pendingCliEnvProbes;
|
|
1085
|
+
/** Forced vendor catalog refreshes are separate flights, but sysinfo joins either flight. */
|
|
1086
|
+
private pendingVendorCliEnvProbes;
|
|
1080
1087
|
/**
|
|
1081
1088
|
* The status emojis for an instance (#1005): its own `status_emojis`, then
|
|
1082
1089
|
* its channel's `options.status_emojis`, then the platform built-in. An
|
|
@@ -1088,27 +1095,49 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
1088
1095
|
};
|
|
1089
1096
|
private warnedStatusEmojis;
|
|
1090
1097
|
/**
|
|
1091
|
-
* A Discord connection's server emojis for the Settings picker (#1005)
|
|
1092
|
-
*
|
|
1093
|
-
*
|
|
1094
|
-
*
|
|
1098
|
+
* A Discord connection's server emojis for the Settings picker (#1005), from
|
|
1099
|
+
* every server its bot can draw on (#1021): the primary `group_id` server,
|
|
1100
|
+
* then each other server the bot is a member of that ClassicBot's
|
|
1101
|
+
* `allowed_guilds` admits (unset = all, as for classic routing). A bot can
|
|
1102
|
+
* react with a custom emoji from any server it is in. Cached per server — a
|
|
1103
|
+
* server's emoji set changes rarely — and one fetch per server at a time;
|
|
1104
|
+
* `refresh` forces new ones. One server refusing (e.g. Missing Access) is
|
|
1105
|
+
* reported on that server and does not hide the others. Uses the running
|
|
1106
|
+
* adapter, so the bot token never leaves the server.
|
|
1095
1107
|
*/
|
|
1096
1108
|
listGuildEmojis(channelId: string, refresh?: boolean): Promise<{
|
|
1097
1109
|
ok: true;
|
|
1098
1110
|
fetched_at: number;
|
|
1099
|
-
emojis:
|
|
1100
|
-
|
|
1101
|
-
name: string;
|
|
1102
|
-
animated: boolean;
|
|
1103
|
-
available: boolean;
|
|
1104
|
-
}>;
|
|
1111
|
+
emojis: GuildEmoji[];
|
|
1112
|
+
guilds: GuildEmojiGroup[];
|
|
1105
1113
|
} | {
|
|
1106
1114
|
ok: false;
|
|
1107
1115
|
error: string;
|
|
1108
1116
|
}>;
|
|
1109
1117
|
private static GUILD_EMOJI_TTL_MS;
|
|
1118
|
+
/** Keyed `<connection>\0<server id>`. */
|
|
1110
1119
|
private guildEmojiCache;
|
|
1111
1120
|
private guildEmojiFetches;
|
|
1121
|
+
/**
|
|
1122
|
+
* What `instanceName` may pick as its persona emoji (the `list_emojis`
|
|
1123
|
+
* tool): its current status set with where each value comes from, the
|
|
1124
|
+
* standard emojis its platform takes, and on Discord the server emojis its
|
|
1125
|
+
* bot can draw on — the same lists, and the same resolution, as the
|
|
1126
|
+
* Settings picker (#1005/#1021), so an agent and an operator see one truth.
|
|
1127
|
+
*/
|
|
1128
|
+
listEmojisFor(instanceName: string, refresh?: boolean): Promise<Record<string, unknown>>;
|
|
1129
|
+
/**
|
|
1130
|
+
* `set_persona_emoji`: `instanceName` sets one of its own status emojis —
|
|
1131
|
+
* `delivered` unless it names another — in its per-instance `status_emojis`
|
|
1132
|
+
* override (#1005 addendum 2), so its stamp is told apart from other bots'
|
|
1133
|
+
* on a shared message. The value is judged exactly as Settings and the
|
|
1134
|
+
* react path judge it (statusEmojiProblem on the instance's platform), and
|
|
1135
|
+
* a Discord server emoji must be one its bot can use: an emoji from a
|
|
1136
|
+
* server it is not in would be stored, preview fine, and fail every react.
|
|
1137
|
+
* An empty emoji removes that status's override.
|
|
1138
|
+
*/
|
|
1139
|
+
setPersonaEmoji(instanceName: string, args: Record<string, unknown>): Promise<Record<string, unknown>>;
|
|
1140
|
+
private personaEmojiMissing;
|
|
1112
1141
|
/** What the instructions tell `instanceName` not to react with (its own status set). */
|
|
1113
1142
|
statusEmojiAvoidList(instanceName: string): string[];
|
|
1114
1143
|
/** The progress bubble's leading emoji, in the form the channel renders. */
|
|
@@ -1711,6 +1740,10 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
1711
1740
|
private cliEnvPath;
|
|
1712
1741
|
/** Read the cached CLI env for a backend, or null if missing/stale/unparseable. */
|
|
1713
1742
|
private readCliEnv;
|
|
1743
|
+
/** Return cached CLI versions immediately. Refresh is started after /sysinfo is sent. */
|
|
1744
|
+
getBackendCliVersionSnapshot(): BackendCliVersionSnapshot;
|
|
1745
|
+
/** Called only after the user-visible sysinfo send/respond has settled. */
|
|
1746
|
+
refreshBackendCliVersions(): void;
|
|
1714
1747
|
/** True when a cached CLI env is old enough that `/model` should re-probe. */
|
|
1715
1748
|
private cliEnvNeedsRefresh;
|
|
1716
1749
|
/**
|
|
@@ -1719,6 +1752,7 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
1719
1752
|
* previous list, never stall the command that asked for it.
|
|
1720
1753
|
*/
|
|
1721
1754
|
private probeBackendBounded;
|
|
1755
|
+
private startCliEnvProbeWorker;
|
|
1722
1756
|
/**
|
|
1723
1757
|
* Resolve the effective model for a fleet or ClassicBot instance, plus where it
|
|
1724
1758
|
* came from. Single source of truth for `/model` and `/ctx` — precedence:
|
|
@@ -1745,6 +1779,8 @@ export declare class FleetManager implements FleetContext, LifecycleContext, Arc
|
|
|
1745
1779
|
* the old list off as fresh.
|
|
1746
1780
|
*/
|
|
1747
1781
|
private probeBackend;
|
|
1782
|
+
/** Preserve the existing cache merge rules for both in-process and worker probes. */
|
|
1783
|
+
private persistCliEnvProbeResult;
|
|
1748
1784
|
/** Background-probe every distinct backend in use at startup (non-blocking). */
|
|
1749
1785
|
private probeCliEnvs;
|
|
1750
1786
|
/** Best-effort model list for `/model`: cached CLI env first, else live probe. Never throws. */
|
package/dist/fleet-manager.js
CHANGED
|
@@ -3,6 +3,7 @@ import { randomBytes, randomUUID } from "node:crypto";
|
|
|
3
3
|
import { spawnSync } from "node:child_process";
|
|
4
4
|
import { freemem, totalmem, cpus } from "node:os";
|
|
5
5
|
import { createServer } from "node:http";
|
|
6
|
+
import { Worker } from "node:worker_threads";
|
|
6
7
|
import { join, dirname, basename } from "node:path";
|
|
7
8
|
import { fileURLToPath } from "node:url";
|
|
8
9
|
import { getAgendHome, ensureWorkspaceGit } from "./paths.js";
|
|
@@ -16,7 +17,7 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
16
17
|
const __dirname = dirname(__filename);
|
|
17
18
|
/** Fallback access policy for a channel with no `access:` block — open (no gate). */
|
|
18
19
|
const DEFAULT_OPEN_ACCESS = { mode: "open", allowed_users: [], max_pending_codes: 0, code_expiry_minutes: 0 };
|
|
19
|
-
import { STATUS_EMOJI_KEYS, builtinStatusEmojis, reactionForm, reactionMatchKey, resolveStatusEmojis, statusAvoidList, statusMatchKey, statusMatchKeys, textForm, } from "./status-emojis.js";
|
|
20
|
+
import { STATUS_EMOJI_CONFIG_KEYS, STATUS_EMOJI_KEYS, STATUS_EMOJI_SUGGESTIONS, TELEGRAM_REACTION_EMOJIS, builtinStatusEmojis, customEmojiValue, normalizeEmoji, previewStatusEmojis, reactionForm, reactionMatchKey, resolveStatusEmojis, statusAvoidList, statusEmojiProblem, statusMatchKey, statusMatchKeys, textForm, } from "./status-emojis.js";
|
|
20
21
|
import { isProbeableRouteTarget } from "./fleet-context.js";
|
|
21
22
|
import { loadFleetConfig, loadRawFleetConfig, DEFAULT_COST_GUARD, DEFAULT_DAILY_SUMMARY, DEFAULT_INSTANCE_CONFIG } from "./config.js";
|
|
22
23
|
import { EventLog } from "./event-log.js";
|
|
@@ -28,7 +29,7 @@ import { IpcClient } from "./channel/ipc-bridge.js";
|
|
|
28
29
|
import { createAdapter } from "./channel/factory.js";
|
|
29
30
|
import { TelegramAdapter } from "./channel/adapters/telegram.js";
|
|
30
31
|
import { createBackend } from "./backend/factory.js";
|
|
31
|
-
import { isModelCompatible } from "./backend/types.js";
|
|
32
|
+
import { isModelCompatible, SYSINFO_BACKEND_IDS } from "./backend/types.js";
|
|
32
33
|
import { createLogger, rotateLogIfNeeded } from "./logger.js";
|
|
33
34
|
import { processAttachments } from "./channel/attachment-handler.js";
|
|
34
35
|
import { routeToolCall } from "./channel/tool-router.js";
|
|
@@ -3915,7 +3916,7 @@ export class FleetManager {
|
|
|
3915
3916
|
}
|
|
3916
3917
|
else if (data.command === "sysinfo") {
|
|
3917
3918
|
// Slash commands are Discord-only; use plain lines (no markdown table)
|
|
3918
|
-
await
|
|
3919
|
+
await this.topicCommands.sendSysInfo(text => data.respond(text), { platform: "discord" });
|
|
3919
3920
|
}
|
|
3920
3921
|
else if (data.command === "dashboard") {
|
|
3921
3922
|
// Reply is ephemeral (adapter defers non-chat commands ephemerally), so
|
|
@@ -4275,7 +4276,7 @@ export class FleetManager {
|
|
|
4275
4276
|
}
|
|
4276
4277
|
else if (data.command === "sysinfo") {
|
|
4277
4278
|
// Slash commands are Discord-only; use plain lines (no markdown table)
|
|
4278
|
-
await
|
|
4279
|
+
await this.topicCommands.sendSysInfo(text => data.respond(text), { platform: "discord" });
|
|
4279
4280
|
}
|
|
4280
4281
|
else if (data.command === "dashboard") {
|
|
4281
4282
|
// Reply is ephemeral (adapter defers non-chat commands ephemerally), so
|
|
@@ -5850,7 +5851,8 @@ export class FleetManager {
|
|
|
5850
5851
|
: type.startsWith("fleet_decision_") ? "fleet_decision_response"
|
|
5851
5852
|
: type === "fleet_task" ? "fleet_task_response"
|
|
5852
5853
|
: type === "fleet_set_display_name" ? "fleet_display_name_response"
|
|
5853
|
-
: "
|
|
5854
|
+
: type === "fleet_list_emojis" || type === "fleet_set_persona_emoji" ? "fleet_persona_emoji_response"
|
|
5855
|
+
: "fleet_description_response";
|
|
5854
5856
|
ipc.send({ type: responseType, fleetRequestId, error: message });
|
|
5855
5857
|
}
|
|
5856
5858
|
dispatchTypedIpc(name, msg) {
|
|
@@ -5875,6 +5877,24 @@ export class FleetManager {
|
|
|
5875
5877
|
this.handleSetDescription(name, msg);
|
|
5876
5878
|
return;
|
|
5877
5879
|
}
|
|
5880
|
+
if (type === "fleet_list_emojis" || type === "fleet_set_persona_emoji") {
|
|
5881
|
+
this.handlePersonaEmoji(name, msg);
|
|
5882
|
+
return;
|
|
5883
|
+
}
|
|
5884
|
+
}
|
|
5885
|
+
/** `list_emojis` / `set_persona_emoji` over IPC; the agent endpoint calls the same two methods. */
|
|
5886
|
+
handlePersonaEmoji(instanceName, msg) {
|
|
5887
|
+
const fleetRequestId = msg.fleetRequestId;
|
|
5888
|
+
const payload = (msg.payload ?? {});
|
|
5889
|
+
const ipc = this.instanceIpcClients.get(instanceName);
|
|
5890
|
+
if (!ipc)
|
|
5891
|
+
return;
|
|
5892
|
+
const op = msg.type === "fleet_list_emojis"
|
|
5893
|
+
? this.listEmojisFor(instanceName, payload.refresh === true)
|
|
5894
|
+
: this.setPersonaEmoji(instanceName, payload);
|
|
5895
|
+
op.then((r) => typeof r.error === "string"
|
|
5896
|
+
? ipc.send({ type: "fleet_persona_emoji_response", fleetRequestId, error: r.error })
|
|
5897
|
+
: ipc.send({ type: "fleet_persona_emoji_response", fleetRequestId, result: r }), (err) => ipc.send({ type: "fleet_persona_emoji_response", fleetRequestId, error: err.message }));
|
|
5878
5898
|
}
|
|
5879
5899
|
handleScheduleCrud(instanceName, msg) {
|
|
5880
5900
|
const fleetRequestId = msg.fleetRequestId;
|
|
@@ -6217,6 +6237,12 @@ export class FleetManager {
|
|
|
6217
6237
|
return { error: `Instance '${instance}' not found` };
|
|
6218
6238
|
return { display_name: name };
|
|
6219
6239
|
}
|
|
6240
|
+
async handleListEmojisHttp(instance, refresh) {
|
|
6241
|
+
return this.listEmojisFor(instance, refresh);
|
|
6242
|
+
}
|
|
6243
|
+
async handleSetPersonaEmojiHttp(instance, args) {
|
|
6244
|
+
return this.setPersonaEmoji(instance, args);
|
|
6245
|
+
}
|
|
6220
6246
|
async handleSetDescriptionHttp(instance, description) {
|
|
6221
6247
|
if (!this.fleetConfig)
|
|
6222
6248
|
return { error: "Fleet config not available" };
|
|
@@ -6834,6 +6860,8 @@ export class FleetManager {
|
|
|
6834
6860
|
deliveryStatusChains = new Map();
|
|
6835
6861
|
/** #725: in-flight CLI env probes, keyed by backend name. Coalesces concurrent /model requests. */
|
|
6836
6862
|
pendingCliEnvProbes = new Map();
|
|
6863
|
+
/** Forced vendor catalog refreshes are separate flights, but sysinfo joins either flight. */
|
|
6864
|
+
pendingVendorCliEnvProbes = new Map();
|
|
6837
6865
|
/**
|
|
6838
6866
|
* The status emojis for an instance (#1005): its own `status_emojis`, then
|
|
6839
6867
|
* its channel's `options.status_emojis`, then the platform built-in. An
|
|
@@ -6864,10 +6892,15 @@ export class FleetManager {
|
|
|
6864
6892
|
}
|
|
6865
6893
|
warnedStatusEmojis = new Set();
|
|
6866
6894
|
/**
|
|
6867
|
-
* A Discord connection's server emojis for the Settings picker (#1005)
|
|
6868
|
-
*
|
|
6869
|
-
*
|
|
6870
|
-
*
|
|
6895
|
+
* A Discord connection's server emojis for the Settings picker (#1005), from
|
|
6896
|
+
* every server its bot can draw on (#1021): the primary `group_id` server,
|
|
6897
|
+
* then each other server the bot is a member of that ClassicBot's
|
|
6898
|
+
* `allowed_guilds` admits (unset = all, as for classic routing). A bot can
|
|
6899
|
+
* react with a custom emoji from any server it is in. Cached per server — a
|
|
6900
|
+
* server's emoji set changes rarely — and one fetch per server at a time;
|
|
6901
|
+
* `refresh` forces new ones. One server refusing (e.g. Missing Access) is
|
|
6902
|
+
* reported on that server and does not hide the others. Uses the running
|
|
6903
|
+
* adapter, so the bot token never leaves the server.
|
|
6871
6904
|
*/
|
|
6872
6905
|
async listGuildEmojis(channelId, refresh = false) {
|
|
6873
6906
|
const world = this.worlds.get(channelId);
|
|
@@ -6878,28 +6911,162 @@ export class FleetManager {
|
|
|
6878
6911
|
const adapter = world.adapter;
|
|
6879
6912
|
if (!adapter.listGuildEmojis)
|
|
6880
6913
|
return { ok: false, error: "this adapter cannot list server emojis" };
|
|
6881
|
-
const
|
|
6882
|
-
|
|
6883
|
-
return { ok: true, ...cached };
|
|
6884
|
-
let pending = this.guildEmojiFetches.get(channelId);
|
|
6885
|
-
if (!pending) {
|
|
6886
|
-
pending = adapter.listGuildEmojis().then(emojis => {
|
|
6887
|
-
const entry = { fetched_at: Date.now(), emojis };
|
|
6888
|
-
this.guildEmojiCache.set(channelId, entry);
|
|
6889
|
-
return entry;
|
|
6890
|
-
}).finally(() => this.guildEmojiFetches.delete(channelId));
|
|
6891
|
-
this.guildEmojiFetches.set(channelId, pending);
|
|
6892
|
-
}
|
|
6914
|
+
const listEmojis = adapter.listGuildEmojis.bind(adapter);
|
|
6915
|
+
let servers;
|
|
6893
6916
|
try {
|
|
6894
|
-
|
|
6917
|
+
servers = adapter.listMemberGuilds
|
|
6918
|
+
? (await adapter.listMemberGuilds()).filter(g => g.primary || (this.classicChannels?.isGuildAllowed(g.id) ?? false))
|
|
6919
|
+
: [{ id: "", name: "", primary: true }];
|
|
6895
6920
|
}
|
|
6896
6921
|
catch (e) {
|
|
6897
|
-
return { ok: false, error: `Discord refused the
|
|
6898
|
-
}
|
|
6922
|
+
return { ok: false, error: `Discord refused the server list: ${e.message}` };
|
|
6923
|
+
}
|
|
6924
|
+
const guilds = await Promise.all(servers.map(async (g) => {
|
|
6925
|
+
const key = `${channelId}\0${g.id}`;
|
|
6926
|
+
const cached = this.guildEmojiCache.get(key);
|
|
6927
|
+
if (!refresh && cached && Date.now() - cached.fetched_at < FleetManager.GUILD_EMOJI_TTL_MS)
|
|
6928
|
+
return { ...g, ...cached };
|
|
6929
|
+
let pending = this.guildEmojiFetches.get(key);
|
|
6930
|
+
if (!pending) {
|
|
6931
|
+
pending = (g.id ? listEmojis(g.id) : listEmojis()).then(emojis => {
|
|
6932
|
+
const entry = { fetched_at: Date.now(), emojis };
|
|
6933
|
+
this.guildEmojiCache.set(key, entry);
|
|
6934
|
+
return entry;
|
|
6935
|
+
}).finally(() => this.guildEmojiFetches.delete(key));
|
|
6936
|
+
this.guildEmojiFetches.set(key, pending);
|
|
6937
|
+
}
|
|
6938
|
+
try {
|
|
6939
|
+
return { ...g, ...(await pending) };
|
|
6940
|
+
}
|
|
6941
|
+
catch (e) {
|
|
6942
|
+
return { ...g, error: `Discord refused the emoji list: ${e.message}` };
|
|
6943
|
+
}
|
|
6944
|
+
}));
|
|
6945
|
+
const listed = guilds.filter((g) => !!g.emojis);
|
|
6946
|
+
if (!listed.length)
|
|
6947
|
+
return { ok: false, error: guilds[0]?.error ?? "no server to list emojis from" };
|
|
6948
|
+
return {
|
|
6949
|
+
ok: true,
|
|
6950
|
+
fetched_at: Math.min(...listed.map(g => g.fetched_at)),
|
|
6951
|
+
emojis: listed.flatMap(g => g.emojis),
|
|
6952
|
+
guilds,
|
|
6953
|
+
};
|
|
6899
6954
|
}
|
|
6900
6955
|
static GUILD_EMOJI_TTL_MS = 10 * 60_000;
|
|
6956
|
+
/** Keyed `<connection>\0<server id>`. */
|
|
6901
6957
|
guildEmojiCache = new Map();
|
|
6902
6958
|
guildEmojiFetches = new Map();
|
|
6959
|
+
/**
|
|
6960
|
+
* What `instanceName` may pick as its persona emoji (the `list_emojis`
|
|
6961
|
+
* tool): its current status set with where each value comes from, the
|
|
6962
|
+
* standard emojis its platform takes, and on Discord the server emojis its
|
|
6963
|
+
* bot can draw on — the same lists, and the same resolution, as the
|
|
6964
|
+
* Settings picker (#1005/#1021), so an agent and an operator see one truth.
|
|
6965
|
+
*/
|
|
6966
|
+
async listEmojisFor(instanceName, refresh = false) {
|
|
6967
|
+
const self = this.fleetConfig?.instances[instanceName];
|
|
6968
|
+
if (!self)
|
|
6969
|
+
return { error: this.personaEmojiMissing(instanceName) };
|
|
6970
|
+
const worldId = this.getInstanceAdapterId(instanceName);
|
|
6971
|
+
const { platform } = this.resolveStatusEmojisFor(instanceName);
|
|
6972
|
+
const channel = (worldId ? this.worlds.get(worldId)?.channelConfig : undefined) ?? this.getChannelConfig(worldId ?? undefined);
|
|
6973
|
+
const current = previewStatusEmojis({ platform, platformConfig: channel?.options?.status_emojis, instanceConfig: self.status_emojis });
|
|
6974
|
+
const out = {
|
|
6975
|
+
platform: platform ?? null,
|
|
6976
|
+
statuses: current.entries.map(e => ({ status: e.key, value: e.value, source: e.source })),
|
|
6977
|
+
standard: platform === "telegram"
|
|
6978
|
+
? { reactions: [...TELEGRAM_REACTION_EMOJIS], note: "Telegram reacts only with these; progress_prefix may be any single emoji" }
|
|
6979
|
+
: { suggestions: STATUS_EMOJI_SUGGESTIONS, note: "any single emoji works" },
|
|
6980
|
+
};
|
|
6981
|
+
if (platform === "discord" && worldId) {
|
|
6982
|
+
const listed = await this.listGuildEmojis(worldId, refresh);
|
|
6983
|
+
out.server_emojis = listed.ok
|
|
6984
|
+
? listed.guilds.map(g => g.emojis
|
|
6985
|
+
? { server: g.name || g.id, primary: g.primary, emojis: g.emojis.filter(e => e.available).map(e => customEmojiValue(e)) }
|
|
6986
|
+
: { server: g.name || g.id, primary: g.primary, error: g.error })
|
|
6987
|
+
: { error: listed.error };
|
|
6988
|
+
}
|
|
6989
|
+
return out;
|
|
6990
|
+
}
|
|
6991
|
+
/**
|
|
6992
|
+
* `set_persona_emoji`: `instanceName` sets one of its own status emojis —
|
|
6993
|
+
* `delivered` unless it names another — in its per-instance `status_emojis`
|
|
6994
|
+
* override (#1005 addendum 2), so its stamp is told apart from other bots'
|
|
6995
|
+
* on a shared message. The value is judged exactly as Settings and the
|
|
6996
|
+
* react path judge it (statusEmojiProblem on the instance's platform), and
|
|
6997
|
+
* a Discord server emoji must be one its bot can use: an emoji from a
|
|
6998
|
+
* server it is not in would be stored, preview fine, and fail every react.
|
|
6999
|
+
* An empty emoji removes that status's override.
|
|
7000
|
+
*/
|
|
7001
|
+
async setPersonaEmoji(instanceName, args) {
|
|
7002
|
+
const self = this.fleetConfig?.instances[instanceName];
|
|
7003
|
+
if (!self)
|
|
7004
|
+
return { error: this.personaEmojiMissing(instanceName) };
|
|
7005
|
+
const status = (args.status ?? "delivered");
|
|
7006
|
+
if (!STATUS_EMOJI_CONFIG_KEYS.includes(status)) {
|
|
7007
|
+
return { error: `status must be one of ${STATUS_EMOJI_CONFIG_KEYS.join(", ")}` };
|
|
7008
|
+
}
|
|
7009
|
+
// Only an explicit "" removes the override. The agent endpoint, typed IPC
|
|
7010
|
+
// and agent-cli do not run the MCP schema, so a call missing `emoji` must
|
|
7011
|
+
// be refused here, never read as "clear my delivered stamp".
|
|
7012
|
+
if (typeof args.emoji !== "string") {
|
|
7013
|
+
return { error: 'emoji is required: one emoji, a <:name:id> from list_emojis, or "" to remove your override' };
|
|
7014
|
+
}
|
|
7015
|
+
const raw = args.emoji.trim();
|
|
7016
|
+
// Whitespace is not an explicit "": a stray " " must not clear the stamp either.
|
|
7017
|
+
if (!raw && args.emoji !== "") {
|
|
7018
|
+
return { error: 'emoji is blank: pass one emoji, a <:name:id> from list_emojis, or exactly "" to remove your override' };
|
|
7019
|
+
}
|
|
7020
|
+
let value;
|
|
7021
|
+
if (raw) {
|
|
7022
|
+
const { platform } = this.resolveStatusEmojisFor(instanceName);
|
|
7023
|
+
const problem = statusEmojiProblem(platform, status, raw);
|
|
7024
|
+
if (problem)
|
|
7025
|
+
return { error: `${problem}. Call list_emojis for what ${platform ?? "this channel"} accepts.` };
|
|
7026
|
+
const e = normalizeEmoji(raw);
|
|
7027
|
+
if (e.kind === "custom") {
|
|
7028
|
+
const worldId = this.getInstanceAdapterId(instanceName);
|
|
7029
|
+
if (!worldId)
|
|
7030
|
+
return { error: "no connection to check that server emoji against" };
|
|
7031
|
+
const listed = await this.listGuildEmojis(worldId);
|
|
7032
|
+
if (!listed.ok)
|
|
7033
|
+
return { error: `cannot check that server emoji: ${listed.error}` };
|
|
7034
|
+
const found = listed.emojis.find(g => g.id === e.id);
|
|
7035
|
+
if (!found?.available) {
|
|
7036
|
+
return { error: `${raw} is not a server emoji this bot can use${found ? " (Discord marks it unavailable)" : ""}. Call list_emojis for the ones it can.` };
|
|
7037
|
+
}
|
|
7038
|
+
value = customEmojiValue(found);
|
|
7039
|
+
}
|
|
7040
|
+
else {
|
|
7041
|
+
value = e.value;
|
|
7042
|
+
}
|
|
7043
|
+
}
|
|
7044
|
+
// Re-read after the validation above, which may have waited on Discord:
|
|
7045
|
+
// another set_persona_emoji or a Settings save can have changed another
|
|
7046
|
+
// status, or replaced this instance's config object, meanwhile. Change
|
|
7047
|
+
// only this one key on what is there now.
|
|
7048
|
+
const current = this.fleetConfig?.instances[instanceName];
|
|
7049
|
+
if (!current)
|
|
7050
|
+
return { error: this.personaEmojiMissing(instanceName) };
|
|
7051
|
+
const map = { ...(current.status_emojis ?? {}) };
|
|
7052
|
+
if (value)
|
|
7053
|
+
map[status] = value;
|
|
7054
|
+
else
|
|
7055
|
+
delete map[status];
|
|
7056
|
+
if (Object.keys(map).length)
|
|
7057
|
+
current.status_emojis = map;
|
|
7058
|
+
else
|
|
7059
|
+
delete current.status_emojis;
|
|
7060
|
+
this.saveFleetConfig();
|
|
7061
|
+
this.logger.info({ instanceName, status, value: value ?? null }, value ? "Persona emoji set" : "Persona emoji cleared");
|
|
7062
|
+
const resolved = this.resolveStatusEmojisFor(instanceName);
|
|
7063
|
+
return { status, value: value ?? null, now: resolved[status], status_emojis: current.status_emojis ?? null };
|
|
7064
|
+
}
|
|
7065
|
+
personaEmojiMissing(instanceName) {
|
|
7066
|
+
return this.classicChannels?.getAll().some(c => c.instanceName === instanceName)
|
|
7067
|
+
? "ClassicBot instances have no per-instance status emojis; an operator sets the connection's in Settings"
|
|
7068
|
+
: `Instance '${instanceName}' not found`;
|
|
7069
|
+
}
|
|
6903
7070
|
/** What the instructions tell `instanceName` not to react with (its own status set). */
|
|
6904
7071
|
statusEmojiAvoidList(instanceName) {
|
|
6905
7072
|
return statusAvoidList(this.resolveStatusEmojisFor(instanceName));
|
|
@@ -10325,6 +10492,32 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
10325
10492
|
catch { /* missing / stale / corrupt */ }
|
|
10326
10493
|
return null;
|
|
10327
10494
|
}
|
|
10495
|
+
/** Return cached CLI versions immediately. Refresh is started after /sysinfo is sent. */
|
|
10496
|
+
getBackendCliVersionSnapshot() {
|
|
10497
|
+
const snapshot = {};
|
|
10498
|
+
for (const backend of SYSINFO_BACKEND_IDS) {
|
|
10499
|
+
const cached = this.readCliEnv(backend);
|
|
10500
|
+
const needsRefresh = this.cliEnvNeedsRefresh(cached);
|
|
10501
|
+
const version = typeof cached?.version === "string" && cached.version.trim()
|
|
10502
|
+
? cached.version.trim()
|
|
10503
|
+
: null;
|
|
10504
|
+
snapshot[backend] = {
|
|
10505
|
+
version,
|
|
10506
|
+
probing: needsRefresh && version === null,
|
|
10507
|
+
};
|
|
10508
|
+
}
|
|
10509
|
+
return snapshot;
|
|
10510
|
+
}
|
|
10511
|
+
/** Called only after the user-visible sysinfo send/respond has settled. */
|
|
10512
|
+
refreshBackendCliVersions() {
|
|
10513
|
+
for (const backend of SYSINFO_BACKEND_IDS) {
|
|
10514
|
+
if (!this.cliEnvNeedsRefresh(this.readCliEnv(backend)))
|
|
10515
|
+
continue;
|
|
10516
|
+
void this.probeBackendBounded(backend, { nonBlocking: true }).catch(err => {
|
|
10517
|
+
this.logger.debug({ err, backend }, "Background CLI version refresh failed");
|
|
10518
|
+
});
|
|
10519
|
+
}
|
|
10520
|
+
}
|
|
10328
10521
|
/** True when a cached CLI env is old enough that `/model` should re-probe. */
|
|
10329
10522
|
cliEnvNeedsRefresh(env) {
|
|
10330
10523
|
return !env || typeof env.probedAt !== "number" || Date.now() - env.probedAt >= CLI_ENV_FRESH_MS;
|
|
@@ -10335,33 +10528,97 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
10335
10528
|
* previous list, never stall the command that asked for it.
|
|
10336
10529
|
*/
|
|
10337
10530
|
async probeBackendBounded(backend, opts = {}) {
|
|
10338
|
-
// #725: single-flight per backend
|
|
10339
|
-
//
|
|
10340
|
-
|
|
10341
|
-
|
|
10342
|
-
if (
|
|
10343
|
-
|
|
10344
|
-
|
|
10345
|
-
|
|
10346
|
-
|
|
10347
|
-
const work = this.probeBackend(backend, opts);
|
|
10531
|
+
// #725: single-flight per backend. Explicit vendor refreshes remain a
|
|
10532
|
+
// separate flight from an ordinary probe, while sysinfo can join either.
|
|
10533
|
+
const regularFlight = this.pendingCliEnvProbes.get(backend);
|
|
10534
|
+
const vendorFlight = this.pendingVendorCliEnvProbes.get(backend);
|
|
10535
|
+
if (opts.refreshVendorCatalog && vendorFlight)
|
|
10536
|
+
return vendorFlight;
|
|
10537
|
+
if (!opts.refreshVendorCatalog && (vendorFlight || regularFlight))
|
|
10538
|
+
return vendorFlight ?? regularFlight;
|
|
10539
|
+
const worker = opts.nonBlocking ? this.startCliEnvProbeWorker(backend, opts) : undefined;
|
|
10540
|
+
const work = worker?.promise ?? this.probeBackend(backend, opts);
|
|
10348
10541
|
work.catch(() => { });
|
|
10349
10542
|
let timer;
|
|
10350
10543
|
const deadline = new Promise(resolve => {
|
|
10351
10544
|
timer = setTimeout(() => {
|
|
10545
|
+
worker?.terminate();
|
|
10352
10546
|
this.logger.warn({ backend, deadlineMs: CLI_ENV_PROBE_DEADLINE_MS }, "CLI env live probe exceeded its deadline — serving the cached model list");
|
|
10353
10547
|
resolve(null);
|
|
10354
10548
|
}, CLI_ENV_PROBE_DEADLINE_MS);
|
|
10355
10549
|
});
|
|
10356
10550
|
const bounded = Promise.race([work, deadline]).finally(() => {
|
|
10357
10551
|
clearTimeout(timer);
|
|
10358
|
-
|
|
10359
|
-
|
|
10552
|
+
const flights = opts.refreshVendorCatalog ? this.pendingVendorCliEnvProbes : this.pendingCliEnvProbes;
|
|
10553
|
+
if (flights.get(backend) === bounded)
|
|
10554
|
+
flights.delete(backend);
|
|
10360
10555
|
});
|
|
10361
|
-
|
|
10362
|
-
this.pendingCliEnvProbes.set(backend, bounded);
|
|
10556
|
+
(opts.refreshVendorCatalog ? this.pendingVendorCliEnvProbes : this.pendingCliEnvProbes).set(backend, bounded);
|
|
10363
10557
|
return bounded;
|
|
10364
10558
|
}
|
|
10559
|
+
startCliEnvProbeWorker(backend, opts) {
|
|
10560
|
+
const workerData = {
|
|
10561
|
+
backend,
|
|
10562
|
+
instanceDir: join(getAgendHome(), "cli-env"),
|
|
10563
|
+
config: {
|
|
10564
|
+
workingDirectory: "",
|
|
10565
|
+
instanceDir: join(getAgendHome(), "cli-env"),
|
|
10566
|
+
instanceName: `probe-${backend}`,
|
|
10567
|
+
mcpServers: {},
|
|
10568
|
+
},
|
|
10569
|
+
refreshVendorCatalog: opts.refreshVendorCatalog === true,
|
|
10570
|
+
};
|
|
10571
|
+
// `tsx src/cli.ts` needs its TS module resolver inside the separate isolate.
|
|
10572
|
+
// Published builds load the compiled worker and require no tsx dependency.
|
|
10573
|
+
const worker = import.meta.url.endsWith(".ts")
|
|
10574
|
+
? new Worker(`const { workerData } = require("node:worker_threads"); import("tsx/esm/api").then(({ tsImport }) => tsImport(workerData.entry, workerData.entry));`, { eval: true, execArgv: [], workerData: { ...workerData, entry: new URL("./backend/cli-env-probe-worker.ts", import.meta.url).href } })
|
|
10575
|
+
: new Worker(new URL("./backend/cli-env-probe-worker.js", import.meta.url), { execArgv: [], workerData });
|
|
10576
|
+
let settled = false;
|
|
10577
|
+
let resolveProbe;
|
|
10578
|
+
const promise = new Promise(resolve => { resolveProbe = resolve; });
|
|
10579
|
+
const finish = (value) => {
|
|
10580
|
+
if (settled)
|
|
10581
|
+
return;
|
|
10582
|
+
settled = true;
|
|
10583
|
+
resolveProbe(value);
|
|
10584
|
+
void worker.terminate().catch(err => {
|
|
10585
|
+
this.logger.debug({ backend, err }, "CLI env worker termination failed");
|
|
10586
|
+
});
|
|
10587
|
+
};
|
|
10588
|
+
worker.once("message", (message) => {
|
|
10589
|
+
if (settled)
|
|
10590
|
+
return;
|
|
10591
|
+
if (!message.ok) {
|
|
10592
|
+
this.logger.warn({ backend, error: message.error }, "CLI env worker failed");
|
|
10593
|
+
finish(null);
|
|
10594
|
+
}
|
|
10595
|
+
else if (!message.result) {
|
|
10596
|
+
finish(null);
|
|
10597
|
+
}
|
|
10598
|
+
else {
|
|
10599
|
+
try {
|
|
10600
|
+
finish(this.persistCliEnvProbeResult(backend, message.result));
|
|
10601
|
+
}
|
|
10602
|
+
catch (err) {
|
|
10603
|
+
this.logger.warn({ backend, err }, "CLI env cache write failed");
|
|
10604
|
+
finish(null);
|
|
10605
|
+
}
|
|
10606
|
+
}
|
|
10607
|
+
});
|
|
10608
|
+
worker.once("error", err => {
|
|
10609
|
+
if (settled)
|
|
10610
|
+
return;
|
|
10611
|
+
this.logger.warn({ backend, err }, "CLI env worker crashed");
|
|
10612
|
+
finish(null);
|
|
10613
|
+
});
|
|
10614
|
+
worker.once("exit", code => {
|
|
10615
|
+
if (settled)
|
|
10616
|
+
return;
|
|
10617
|
+
this.logger.warn({ backend, code }, "CLI env worker exited without a result");
|
|
10618
|
+
finish(null);
|
|
10619
|
+
});
|
|
10620
|
+
return { promise, terminate: () => { finish(null); } };
|
|
10621
|
+
}
|
|
10365
10622
|
/**
|
|
10366
10623
|
* Resolve the effective model for a fleet or ClassicBot instance, plus where it
|
|
10367
10624
|
* came from. Single source of truth for `/model` and `/ctx` — precedence:
|
|
@@ -10428,35 +10685,39 @@ Plus the operational skills (fleet-health, instance-lifecycle, scheduling, sessi
|
|
|
10428
10685
|
if (opts.refreshVendorCatalog && be.refreshModelCatalog)
|
|
10429
10686
|
await be.refreshModelCatalog();
|
|
10430
10687
|
const probed = await be.probeCLIEnv({ workingDirectory: "", instanceDir: join(getAgendHome(), "cli-env"), instanceName: `probe-${backend}`, mcpServers: {} });
|
|
10431
|
-
|
|
10432
|
-
// An empty result must never overwrite a catalog we already have. Some
|
|
10433
|
-
// probes hit the network (`agy models` fetches, 5s cap), so a slow moment
|
|
10434
|
-
// returns [] — and writing that would blank the list for the whole 24h
|
|
10435
|
-
// TTL, long after the CLI recovered. Observed live: a good 11-model
|
|
10436
|
-
// antigravity cache replaced by an empty one. Keep the known models and
|
|
10437
|
-
// let the fresher currentModel/version through.
|
|
10438
|
-
if (!env.models?.length) {
|
|
10439
|
-
const previous = this.readCliEnv(backend);
|
|
10440
|
-
if (previous?.models?.length)
|
|
10441
|
-
env.models = previous.models;
|
|
10442
|
-
}
|
|
10443
|
-
// Same protection for the extended catalog: one offline moment must not
|
|
10444
|
-
// blank a good account list for the whole cache TTL.
|
|
10445
|
-
if (!env.apiModels?.length) {
|
|
10446
|
-
const previous = this.readCliEnv(backend);
|
|
10447
|
-
if (previous?.apiModels?.length)
|
|
10448
|
-
env.apiModels = previous.apiModels;
|
|
10449
|
-
}
|
|
10450
|
-
const path = this.cliEnvPath(backend);
|
|
10451
|
-
mkdirSync(dirname(path), { recursive: true });
|
|
10452
|
-
writeFileSync(path, JSON.stringify(env, null, 2));
|
|
10453
|
-
return env;
|
|
10688
|
+
return this.persistCliEnvProbeResult(backend, probed);
|
|
10454
10689
|
}
|
|
10455
10690
|
catch (err) {
|
|
10456
10691
|
this.logger.warn({ err, backend }, "CLI env probe failed");
|
|
10457
10692
|
return null;
|
|
10458
10693
|
}
|
|
10459
10694
|
}
|
|
10695
|
+
/** Preserve the existing cache merge rules for both in-process and worker probes. */
|
|
10696
|
+
persistCliEnvProbeResult(backend, probed) {
|
|
10697
|
+
const env = { backend, probedAt: Date.now(), ...probed };
|
|
10698
|
+
// An empty result must never overwrite a catalog we already have. Some
|
|
10699
|
+
// probes hit the network (`agy models` fetches, 5s cap), so a slow moment
|
|
10700
|
+
// returns [] — and writing that would blank the list for the whole 24h
|
|
10701
|
+
// TTL, long after the CLI recovered. Observed live: a good 11-model
|
|
10702
|
+
// antigravity cache replaced by an empty one. Keep the known models and
|
|
10703
|
+
// let the fresher currentModel/version through.
|
|
10704
|
+
if (!env.models?.length) {
|
|
10705
|
+
const previous = this.readCliEnv(backend);
|
|
10706
|
+
if (previous?.models?.length)
|
|
10707
|
+
env.models = previous.models;
|
|
10708
|
+
}
|
|
10709
|
+
// Same protection for the extended catalog: one offline moment must not
|
|
10710
|
+
// blank a good account list for the whole cache TTL.
|
|
10711
|
+
if (!env.apiModels?.length) {
|
|
10712
|
+
const previous = this.readCliEnv(backend);
|
|
10713
|
+
if (previous?.apiModels?.length)
|
|
10714
|
+
env.apiModels = previous.apiModels;
|
|
10715
|
+
}
|
|
10716
|
+
const path = this.cliEnvPath(backend);
|
|
10717
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
10718
|
+
writeFileSync(path, JSON.stringify(env, null, 2));
|
|
10719
|
+
return env;
|
|
10720
|
+
}
|
|
10460
10721
|
/** Background-probe every distinct backend in use at startup (non-blocking). */
|
|
10461
10722
|
probeCliEnvs() {
|
|
10462
10723
|
const backends = new Set();
|