@songsid/agend 2.1.0-beta.23 → 2.1.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -1
- package/README.zh-TW.md +1 -2
- package/dist/access-path.js +3 -3
- package/dist/access-path.js.map +1 -1
- package/dist/agent-endpoint.d.ts +0 -8
- package/dist/agent-endpoint.js +8 -36
- package/dist/agent-endpoint.js.map +1 -1
- package/dist/backend/antigravity.js +2 -3
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/factory.js +1 -5
- package/dist/backend/factory.js.map +1 -1
- package/dist/backend/kiro.js +2 -3
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/types.d.ts +2 -12
- package/dist/backend/types.js +0 -1
- package/dist/backend/types.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +0 -20
- package/dist/channel/adapters/discord.js +11 -69
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.js +1 -4
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/classic-channel-manager.d.ts +1 -6
- package/dist/classic-channel-manager.js +3 -26
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli.js +4 -48
- package/dist/cli.js.map +1 -1
- package/dist/config-validator.js +1 -50
- package/dist/config-validator.js.map +1 -1
- package/dist/config.d.ts +1 -3
- package/dist/config.js +1 -13
- package/dist/config.js.map +1 -1
- package/dist/context-guardian.d.ts +0 -2
- package/dist/context-guardian.js +2 -10
- package/dist/context-guardian.js.map +1 -1
- package/dist/daemon-entry.js +1 -2
- package/dist/daemon-entry.js.map +1 -1
- package/dist/daemon.d.ts +1 -25
- package/dist/daemon.js +30 -156
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-context.d.ts +0 -1
- package/dist/fleet-manager.d.ts +5 -30
- package/dist/fleet-manager.js +106 -312
- package/dist/fleet-manager.js.map +1 -1
- package/dist/general-knowledge/skills/fleet-config/SKILL.md +1 -1
- package/dist/general-knowledge/skills/model-discovery/SKILL.md +0 -1
- package/dist/instance-lifecycle.d.ts +1 -2
- package/dist/instance-lifecycle.js +4 -15
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/locale.js +0 -2
- package/dist/locale.js.map +1 -1
- package/dist/logger.d.ts +0 -6
- package/dist/logger.js +9 -28
- package/dist/logger.js.map +1 -1
- package/dist/outbound-handlers.d.ts +0 -3
- package/dist/outbound-handlers.js +0 -25
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/outbound-schemas.d.ts +1 -2
- package/dist/outbound-schemas.js +1 -1
- package/dist/outbound-schemas.js.map +1 -1
- package/dist/settings-api.d.ts +2 -17
- package/dist/settings-api.js +8 -105
- package/dist/settings-api.js.map +1 -1
- package/dist/setup-wizard.js +0 -8
- package/dist/setup-wizard.js.map +1 -1
- package/dist/statusline-watcher.d.ts +1 -1
- package/dist/statusline-watcher.js +2 -3
- package/dist/statusline-watcher.js.map +1 -1
- package/dist/tmux-manager.d.ts +1 -1
- package/dist/tmux-manager.js.map +1 -1
- package/dist/topic-commands.d.ts +1 -14
- package/dist/topic-commands.js +23 -89
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-monitor.js +0 -2
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/types.d.ts +1 -16
- package/dist/ui/dashboard.html +2 -2
- package/dist/ui/settings.html +40 -251
- package/dist/web-api.d.ts +0 -1
- package/dist/web-api.js +15 -24
- package/dist/web-api.js.map +1 -1
- package/package.json +1 -2
- package/dist/backend/grok.d.ts +0 -51
- package/dist/backend/grok.js +0 -280
- package/dist/backend/grok.js.map +0 -1
package/dist/fleet-manager.js
CHANGED
|
@@ -5,13 +5,13 @@ import { join, dirname, basename } from "node:path";
|
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { getAgendHome, ensureWorkspaceGit } from "./paths.js";
|
|
7
7
|
import { sdNotify } from "./sd-notify.js";
|
|
8
|
-
import
|
|
8
|
+
import yaml from "js-yaml";
|
|
9
9
|
const __filename = fileURLToPath(import.meta.url);
|
|
10
10
|
const __dirname = dirname(__filename);
|
|
11
11
|
/** Fallback access policy for a channel with no `access:` block — open (no gate). */
|
|
12
12
|
const DEFAULT_OPEN_ACCESS = { mode: "open", allowed_users: [], max_pending_codes: 0, code_expiry_minutes: 0 };
|
|
13
13
|
import { isProbeableRouteTarget } from "./fleet-context.js";
|
|
14
|
-
import { loadFleetConfig,
|
|
14
|
+
import { loadFleetConfig, DEFAULT_COST_GUARD, DEFAULT_DAILY_SUMMARY, DEFAULT_INSTANCE_CONFIG } from "./config.js";
|
|
15
15
|
import { EventLog } from "./event-log.js";
|
|
16
16
|
import { AdapterWorld } from "./adapter-world.js";
|
|
17
17
|
import { CostGuard, formatCents } from "./cost-guard.js";
|
|
@@ -39,7 +39,7 @@ import { handleViewRequest, isViewPath } from "./view-api.js";
|
|
|
39
39
|
import { handleSettingsRequest } from "./settings-api.js";
|
|
40
40
|
import { setLocale, detectLocale, t } from "./locale.js";
|
|
41
41
|
import { handleAgentRequest } from "./agent-endpoint.js";
|
|
42
|
-
import { ClassicChannelManager
|
|
42
|
+
import { ClassicChannelManager } from "./classic-channel-manager.js";
|
|
43
43
|
import { getTmuxSession } from "./config.js";
|
|
44
44
|
export function resolveReplyThreadId(argsThreadId, instanceConfig) {
|
|
45
45
|
if (typeof argsThreadId === "string" && argsThreadId.length > 0) {
|
|
@@ -58,8 +58,6 @@ const CANCEL_BTN_MAX_RETRIES = 3;
|
|
|
58
58
|
* buttons no clear trigger reached (e.g. a scheduled/HTTP turn that never called
|
|
59
59
|
* reply). 5min (not the old 2s idle-watch) so Thinking isn't misread as idle. */
|
|
60
60
|
const CANCEL_BTN_IDLE_CHECK_INTERVAL_MS = 5 * 60_000;
|
|
61
|
-
const CLASSIC_BACKEND_SELECTION_TIMEOUT_MS = 60_000;
|
|
62
|
-
const CLASSIC_BACKEND_CALLBACK_PREFIX = "classic-backend:";
|
|
63
61
|
export class FleetManager {
|
|
64
62
|
dataDir;
|
|
65
63
|
children = new Map();
|
|
@@ -67,9 +65,6 @@ export class FleetManager {
|
|
|
67
65
|
/** @deprecated Use lifecycle.daemons — kept for backward compat */
|
|
68
66
|
get daemons() { return this.lifecycle.daemons; }
|
|
69
67
|
fleetConfig = null;
|
|
70
|
-
rawFleetConfig = {};
|
|
71
|
-
rawFleetDocument = null;
|
|
72
|
-
savedFleetConfigSnapshot = null;
|
|
73
68
|
adapter = null;
|
|
74
69
|
worlds = new Map();
|
|
75
70
|
adapters = new Map(); // derived view for backward compat
|
|
@@ -98,8 +93,6 @@ export class FleetManager {
|
|
|
98
93
|
// Topic icon + auto-archive state
|
|
99
94
|
topicIcons = {};
|
|
100
95
|
lastActivity = new Map();
|
|
101
|
-
/** Latest pane-derived execution snapshot reported by each daemon. */
|
|
102
|
-
instanceStateCache = new Map();
|
|
103
96
|
lastInboundUser = new Map(); // instanceName → last username
|
|
104
97
|
// Active "🛑 Cancel" buttons, tracked per button (keyed by messageId) rather
|
|
105
98
|
// than one-per-instance. A button is retired (deleted, with bounded retry) on
|
|
@@ -111,7 +104,6 @@ export class FleetManager {
|
|
|
111
104
|
topicArchiver;
|
|
112
105
|
controlClient = null;
|
|
113
106
|
classicChannels = null;
|
|
114
|
-
pendingClassicStarts = new Map();
|
|
115
107
|
// Model failover state
|
|
116
108
|
failoverActive = new Map(); // instance → current failover model
|
|
117
109
|
// IPC reconnect: tracks instances being intentionally stopped (skip reconnect)
|
|
@@ -168,7 +160,6 @@ export class FleetManager {
|
|
|
168
160
|
const instances = Object.keys(this.fleetConfig?.instances ?? {}).map(name => ({
|
|
169
161
|
name,
|
|
170
162
|
status: this.getInstanceStatus(name),
|
|
171
|
-
state: this.getInstanceExecutionState(name),
|
|
172
163
|
ipc: this.instanceIpcClients.has(name),
|
|
173
164
|
costCents: this.costGuard?.getDailyCostCents(name) ?? 0,
|
|
174
165
|
rateLimits: this.statuslineWatcher.getRateLimits(name) ?? null,
|
|
@@ -183,21 +174,9 @@ export class FleetManager {
|
|
|
183
174
|
}
|
|
184
175
|
/** Load fleet.yaml and build routing table */
|
|
185
176
|
loadConfig(configPath) {
|
|
186
|
-
this.configPath = configPath;
|
|
187
|
-
const source = existsSync(configPath) ? readFileSync(configPath, "utf-8") : "{}\n";
|
|
188
|
-
this.rawFleetDocument = parseDocument(source, { keepSourceTokens: true });
|
|
189
|
-
if (this.rawFleetDocument.errors.length > 0) {
|
|
190
|
-
throw new Error(`Invalid fleet.yaml: ${this.rawFleetDocument.errors[0].message}`);
|
|
191
|
-
}
|
|
192
|
-
this.rawFleetConfig = loadRawFleetConfig(configPath);
|
|
193
177
|
this.fleetConfig = loadFleetConfig(configPath);
|
|
194
|
-
this.savedFleetConfigSnapshot = structuredClone(this.fleetConfig);
|
|
195
178
|
return this.fleetConfig;
|
|
196
179
|
}
|
|
197
|
-
/** User-authored fleet.yaml, before defaults are merged into instances. */
|
|
198
|
-
getRawFleetConfig() {
|
|
199
|
-
return structuredClone(this.rawFleetConfig);
|
|
200
|
-
}
|
|
201
180
|
/** Build topic routing table: { topicId -> RouteTarget } */
|
|
202
181
|
buildRoutingTable() {
|
|
203
182
|
if (this.fleetConfig) {
|
|
@@ -286,19 +265,14 @@ export class FleetManager {
|
|
|
286
265
|
*/
|
|
287
266
|
bindInstanceAdapter(name, adapterId, fromInbound = false) {
|
|
288
267
|
const cfg = this.fleetConfig?.instances[name];
|
|
289
|
-
if (fromInbound)
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
return; // fleet instance without explicit binding → use primary
|
|
298
|
-
// Classic instance: don't override an existing binding (authoritative from /start)
|
|
299
|
-
if (this.classicChannels?.getChannelIdByInstance(name) !== undefined && this.instanceWorldBinding.has(name))
|
|
300
|
-
return;
|
|
301
|
-
}
|
|
268
|
+
if (fromInbound && (cfg?.general_topic || cfg?.channel_id))
|
|
269
|
+
return;
|
|
270
|
+
// A classic instance is bound authoritatively at /start. Don't let an inbound
|
|
271
|
+
// (seen by every same-guild bot) override an existing binding — but if there
|
|
272
|
+
// is none yet (e.g. after a restart, before v2.1 persistence), allow inbound
|
|
273
|
+
// to re-establish it so replies don't fall back to the primary bot forever.
|
|
274
|
+
if (fromInbound && this.classicChannels?.getChannelIdByInstance(name) !== undefined && this.instanceWorldBinding.has(name))
|
|
275
|
+
return;
|
|
302
276
|
this.instanceWorldBinding.set(name, adapterId);
|
|
303
277
|
}
|
|
304
278
|
getInstanceStatus(name) {
|
|
@@ -316,25 +290,6 @@ export class FleetManager {
|
|
|
316
290
|
return "crashed";
|
|
317
291
|
}
|
|
318
292
|
}
|
|
319
|
-
getInstanceExecutionState(name) {
|
|
320
|
-
if (this.lifecycle.isPaused(name))
|
|
321
|
-
return null;
|
|
322
|
-
return this.instanceStateCache.get(name)?.state ?? null;
|
|
323
|
-
}
|
|
324
|
-
cacheInstanceExecutionState(name, msg) {
|
|
325
|
-
const state = msg.state;
|
|
326
|
-
if (state !== "idle" && state !== "working" && state !== "stuck")
|
|
327
|
-
return;
|
|
328
|
-
const previous = this.instanceStateCache.get(name);
|
|
329
|
-
const now = Date.now();
|
|
330
|
-
const numberOr = (value, fallback) => typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
331
|
-
this.instanceStateCache.set(name, {
|
|
332
|
-
state,
|
|
333
|
-
unchangedForMs: numberOr(msg.unchangedForMs, previous?.unchangedForMs ?? 0),
|
|
334
|
-
observedAt: numberOr(msg.observedAt, now),
|
|
335
|
-
stateChangedAt: numberOr(msg.stateChangedAt, previous?.state === state ? previous.stateChangedAt : now),
|
|
336
|
-
});
|
|
337
|
-
}
|
|
338
293
|
/** Single delivery facade: wake an auto-paused CLI before sending to daemon IPC. */
|
|
339
294
|
async deliverToInstance(instanceName, payload) {
|
|
340
295
|
if (this.lifecycle.isPaused(instanceName)) {
|
|
@@ -412,7 +367,6 @@ export class FleetManager {
|
|
|
412
367
|
}
|
|
413
368
|
async stopInstance(name) {
|
|
414
369
|
this.failoverActive.delete(name);
|
|
415
|
-
this.instanceStateCache.delete(name);
|
|
416
370
|
return this.lifecycle.stop(name);
|
|
417
371
|
}
|
|
418
372
|
/** Restart a single instance, reloading fleet.yaml first to pick up config changes. */
|
|
@@ -998,8 +952,6 @@ export class FleetManager {
|
|
|
998
952
|
await this.handleInboundMessage(msg);
|
|
999
953
|
}, this.logger, "adapter.message"));
|
|
1000
954
|
this.adapter.on("callback_query", safeHandler(async (data) => {
|
|
1001
|
-
if (await this.handleClassicBackendSelection(data))
|
|
1002
|
-
return;
|
|
1003
955
|
if (data.callbackData.startsWith("hang:")) {
|
|
1004
956
|
const parts = data.callbackData.split(":");
|
|
1005
957
|
const action = parts[1];
|
|
@@ -1038,14 +990,8 @@ export class FleetManager {
|
|
|
1038
990
|
// Handle classic bot slash commands (/start, /stop, /chat, /compact, /save, /load)
|
|
1039
991
|
this.adapter.on("slash_command", safeHandler(async (data) => {
|
|
1040
992
|
if (data.command === "start") {
|
|
1041
|
-
const
|
|
1042
|
-
|
|
1043
|
-
const reply = await this.handleClassicStart(data.channelId, data.channelName, data.userId, data.guildId, adapterId, requestedBackend);
|
|
1044
|
-
await data.respond(reply);
|
|
1045
|
-
}
|
|
1046
|
-
else {
|
|
1047
|
-
await this.beginClassicBackendSelection(data, this.adapter);
|
|
1048
|
-
}
|
|
993
|
+
const reply = await this.handleClassicStart(data.channelId, data.channelName, data.userId, data.guildId, adapterId);
|
|
994
|
+
await data.respond(reply);
|
|
1049
995
|
}
|
|
1050
996
|
else if (data.command === "stop") {
|
|
1051
997
|
const reply = await this.handleClassicStop(data.channelId, adapterId);
|
|
@@ -1289,8 +1235,6 @@ export class FleetManager {
|
|
|
1289
1235
|
await this.handleInboundMessage(msg);
|
|
1290
1236
|
}, this.logger, `adapter[${adapterId}].message`));
|
|
1291
1237
|
adapter.on("callback_query", safeHandler(async (data) => {
|
|
1292
|
-
if (await this.handleClassicBackendSelection(data))
|
|
1293
|
-
return;
|
|
1294
1238
|
if (data.callbackData.startsWith("hang:")) {
|
|
1295
1239
|
const parts = data.callbackData.split(":");
|
|
1296
1240
|
const action = parts[1];
|
|
@@ -1325,14 +1269,8 @@ export class FleetManager {
|
|
|
1325
1269
|
// Slash commands: classic bot + admin commands
|
|
1326
1270
|
adapter.on("slash_command", safeHandler(async (data) => {
|
|
1327
1271
|
if (data.command === "start") {
|
|
1328
|
-
const
|
|
1329
|
-
|
|
1330
|
-
const reply = await this.handleClassicStart(data.channelId, data.channelName, data.userId, data.guildId, adapterId, requestedBackend);
|
|
1331
|
-
await data.respond(reply);
|
|
1332
|
-
}
|
|
1333
|
-
else {
|
|
1334
|
-
await this.beginClassicBackendSelection(data, adapter);
|
|
1335
|
-
}
|
|
1272
|
+
const reply = await this.handleClassicStart(data.channelId, data.channelName, data.userId, data.guildId, adapterId);
|
|
1273
|
+
await data.respond(reply);
|
|
1336
1274
|
}
|
|
1337
1275
|
else if (data.command === "stop") {
|
|
1338
1276
|
const reply = await this.handleClassicStop(data.channelId, adapterId);
|
|
@@ -1594,16 +1532,10 @@ export class FleetManager {
|
|
|
1594
1532
|
else if (msg.type === "fleet_set_description") {
|
|
1595
1533
|
this.handleSetDescription(name, msg);
|
|
1596
1534
|
}
|
|
1597
|
-
else if (msg.type === "instance_state" || msg.type === "instance_state_response") {
|
|
1598
|
-
this.cacheInstanceExecutionState(name, msg);
|
|
1599
|
-
}
|
|
1600
1535
|
}, this.logger, `ipc.message[${name}]`));
|
|
1601
1536
|
// Ask daemon for any sessions that registered before we connected
|
|
1602
1537
|
// (fixes race condition where mcp_ready was broadcast before fleet manager connected)
|
|
1603
1538
|
ipc.send({ type: "query_sessions" });
|
|
1604
|
-
// The initial state transition may have happened before FleetManager
|
|
1605
|
-
// connected, so seed the cache instead of waiting for another transition.
|
|
1606
|
-
ipc.send({ type: "query_instance_state", requestId: `fleet-state-${Date.now()}` });
|
|
1607
1539
|
this.logger.debug({ name }, "Connected to instance IPC");
|
|
1608
1540
|
if (!this.statuslineWatcher.has(name)) {
|
|
1609
1541
|
this.statuslineWatcher.watch(name);
|
|
@@ -1866,21 +1798,9 @@ export class FleetManager {
|
|
|
1866
1798
|
}
|
|
1867
1799
|
}
|
|
1868
1800
|
const channelName = msg.username || chatId;
|
|
1869
|
-
|
|
1870
|
-
|
|
1871
|
-
|
|
1872
|
-
const reply = await this.handleClassicStart(chatId, channelName, msg.userId, undefined, msg.adapterId, requestedBackend);
|
|
1873
|
-
await msgAdapter?.sendText(chatId, reply);
|
|
1874
|
-
}
|
|
1875
|
-
else if (msgAdapter) {
|
|
1876
|
-
await this.beginClassicBackendSelection({
|
|
1877
|
-
command: "start",
|
|
1878
|
-
channelId: chatId,
|
|
1879
|
-
channelName,
|
|
1880
|
-
userId: msg.userId,
|
|
1881
|
-
respond: async (reply) => (await msgAdapter.sendText(chatId, reply)).messageId,
|
|
1882
|
-
}, msgAdapter);
|
|
1883
|
-
}
|
|
1801
|
+
// handleClassicStart binds the instance to this adapter authoritatively.
|
|
1802
|
+
const reply = await this.handleClassicStart(chatId, channelName, msg.userId, undefined, msg.adapterId);
|
|
1803
|
+
await msgAdapter?.sendText(chatId, reply);
|
|
1884
1804
|
return;
|
|
1885
1805
|
}
|
|
1886
1806
|
// Handle /stop command
|
|
@@ -2030,7 +1950,6 @@ export class FleetManager {
|
|
|
2030
1950
|
user_id: msg.userId,
|
|
2031
1951
|
ts: msg.timestamp.toISOString(),
|
|
2032
1952
|
thread_id: "",
|
|
2033
|
-
adapter_id: msg.adapterId,
|
|
2034
1953
|
source: msg.source,
|
|
2035
1954
|
...(msg.replyToText ? { reply_to_text: msg.replyToText } : {}),
|
|
2036
1955
|
...extraMeta,
|
|
@@ -2123,7 +2042,6 @@ export class FleetManager {
|
|
|
2123
2042
|
user_id: msg.userId,
|
|
2124
2043
|
ts: msg.timestamp.toISOString(),
|
|
2125
2044
|
thread_id: msg.threadId ?? "",
|
|
2126
|
-
adapter_id: msg.adapterId,
|
|
2127
2045
|
source: msg.source,
|
|
2128
2046
|
...(msg.replyToText ? { reply_to_text: msg.replyToText } : {}),
|
|
2129
2047
|
...extraMeta,
|
|
@@ -2746,92 +2664,73 @@ export class FleetManager {
|
|
|
2746
2664
|
}
|
|
2747
2665
|
}, 5 * 60_000);
|
|
2748
2666
|
}
|
|
2749
|
-
/**
|
|
2750
|
-
|
|
2751
|
-
* document. Unknown keys, explicit overrides and comments remain untouched.
|
|
2752
|
-
*/
|
|
2753
|
-
saveFleetConfig(explicitPatches = []) {
|
|
2667
|
+
/** Save fleet config back to fleet.yaml */
|
|
2668
|
+
saveFleetConfig() {
|
|
2754
2669
|
if (!this.fleetConfig || !this.configPath)
|
|
2755
2670
|
return;
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
if (this.
|
|
2763
|
-
|
|
2764
|
-
}
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
}
|
|
2779
|
-
const
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
|
|
2790
|
-
|
|
2791
|
-
|
|
2792
|
-
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
|
|
2802
|
-
|
|
2803
|
-
|
|
2804
|
-
|
|
2805
|
-
|
|
2806
|
-
|
|
2807
|
-
|
|
2808
|
-
|
|
2809
|
-
|
|
2810
|
-
|
|
2811
|
-
|
|
2812
|
-
|
|
2813
|
-
|
|
2814
|
-
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
}
|
|
2820
|
-
if (after === undefined) {
|
|
2821
|
-
document.deleteIn(path);
|
|
2822
|
-
}
|
|
2823
|
-
else if (path.length === 0) {
|
|
2824
|
-
document.contents = document.createNode(after);
|
|
2825
|
-
}
|
|
2826
|
-
else {
|
|
2827
|
-
const currentNode = document.getIn(path, true);
|
|
2828
|
-
if (isScalar(currentNode) && (after === null || typeof after !== "object")) {
|
|
2829
|
-
currentNode.value = after;
|
|
2830
|
-
}
|
|
2831
|
-
else {
|
|
2832
|
-
document.setIn(path, after);
|
|
2833
|
-
}
|
|
2834
|
-
}
|
|
2671
|
+
const toSave = {};
|
|
2672
|
+
if (this.fleetConfig.project_roots)
|
|
2673
|
+
toSave.project_roots = this.fleetConfig.project_roots;
|
|
2674
|
+
if (this.fleetConfig.channels && this.fleetConfig.channels.length > 0) {
|
|
2675
|
+
toSave.channels = this.fleetConfig.channels;
|
|
2676
|
+
}
|
|
2677
|
+
else if (this.fleetConfig.channel) {
|
|
2678
|
+
toSave.channel = this.fleetConfig.channel;
|
|
2679
|
+
}
|
|
2680
|
+
if (this.fleetConfig.health_port)
|
|
2681
|
+
toSave.health_port = this.fleetConfig.health_port;
|
|
2682
|
+
if (Object.keys(this.fleetConfig.defaults).length > 0)
|
|
2683
|
+
toSave.defaults = this.fleetConfig.defaults;
|
|
2684
|
+
if (this.fleetConfig.teams && Object.keys(this.fleetConfig.teams).length > 0) {
|
|
2685
|
+
toSave.teams = this.fleetConfig.teams;
|
|
2686
|
+
}
|
|
2687
|
+
if (this.fleetConfig.templates && Object.keys(this.fleetConfig.templates).length > 0) {
|
|
2688
|
+
toSave.templates = this.fleetConfig.templates;
|
|
2689
|
+
}
|
|
2690
|
+
if (this.fleetConfig.profiles && Object.keys(this.fleetConfig.profiles).length > 0) {
|
|
2691
|
+
toSave.profiles = this.fleetConfig.profiles;
|
|
2692
|
+
}
|
|
2693
|
+
toSave.instances = {};
|
|
2694
|
+
for (const [name, inst] of Object.entries(this.fleetConfig.instances)) {
|
|
2695
|
+
const serialized = {
|
|
2696
|
+
working_directory: inst.working_directory,
|
|
2697
|
+
topic_id: inst.topic_id,
|
|
2698
|
+
};
|
|
2699
|
+
// Preserve all optional user-configured fields so saveFleetConfig() never silently drops them
|
|
2700
|
+
if (inst.general_topic)
|
|
2701
|
+
serialized.general_topic = true;
|
|
2702
|
+
if (inst.display_name)
|
|
2703
|
+
serialized.display_name = inst.display_name;
|
|
2704
|
+
if (inst.channel_id)
|
|
2705
|
+
serialized.channel_id = inst.channel_id;
|
|
2706
|
+
if (inst.description)
|
|
2707
|
+
serialized.description = inst.description;
|
|
2708
|
+
if (inst.tags?.length)
|
|
2709
|
+
serialized.tags = inst.tags;
|
|
2710
|
+
if (inst.model)
|
|
2711
|
+
serialized.model = inst.model;
|
|
2712
|
+
if (inst.model_failover?.length)
|
|
2713
|
+
serialized.model_failover = inst.model_failover;
|
|
2714
|
+
if (inst.worktree_source)
|
|
2715
|
+
serialized.worktree_source = inst.worktree_source;
|
|
2716
|
+
if (inst.backend)
|
|
2717
|
+
serialized.backend = inst.backend;
|
|
2718
|
+
if (inst.systemPrompt)
|
|
2719
|
+
serialized.systemPrompt = inst.systemPrompt;
|
|
2720
|
+
if (inst.skipPermissions)
|
|
2721
|
+
serialized.skipPermissions = inst.skipPermissions;
|
|
2722
|
+
if (inst.lightweight)
|
|
2723
|
+
serialized.lightweight = inst.lightweight;
|
|
2724
|
+
if (inst.cost_guard)
|
|
2725
|
+
serialized.cost_guard = inst.cost_guard;
|
|
2726
|
+
if (inst.workflow !== undefined)
|
|
2727
|
+
serialized.workflow = inst.workflow;
|
|
2728
|
+
if (inst.agent_mode)
|
|
2729
|
+
serialized.agent_mode = inst.agent_mode;
|
|
2730
|
+
toSave.instances[name] = serialized;
|
|
2731
|
+
}
|
|
2732
|
+
writeFileSync(this.configPath, yaml.dump(toSave, { lineWidth: 120 }));
|
|
2733
|
+
this.logger.info({ path: this.configPath }, "Saved fleet config");
|
|
2835
2734
|
}
|
|
2836
2735
|
async removeInstance(name) {
|
|
2837
2736
|
// Clean up schedules (scheduler is fleet-level, not lifecycle-level)
|
|
@@ -2874,10 +2773,6 @@ export class FleetManager {
|
|
|
2874
2773
|
startStatuslineWatcher(name) {
|
|
2875
2774
|
this.statuslineWatcher.watch(name);
|
|
2876
2775
|
}
|
|
2877
|
-
stopStatuslineWatcher(name) {
|
|
2878
|
-
// Pausing stops I/O but retains the last observed limits for status views.
|
|
2879
|
-
this.statuslineWatcher.unwatch(name, true);
|
|
2880
|
-
}
|
|
2881
2776
|
reactMessageStatus(instanceName, chatId, messageId, emoji) {
|
|
2882
2777
|
// React via the adapter BOUND to this instance — NOT the first discord world.
|
|
2883
2778
|
// Otherwise, in a same-channel/same-guild multi-bot setup, the inbound 👀
|
|
@@ -3228,7 +3123,7 @@ export class FleetManager {
|
|
|
3228
3123
|
return [];
|
|
3229
3124
|
}
|
|
3230
3125
|
}
|
|
3231
|
-
async sendHangNotification(instanceName
|
|
3126
|
+
async sendHangNotification(instanceName) {
|
|
3232
3127
|
const adapter = this.getAdapterForInstance(instanceName) ?? this.adapter;
|
|
3233
3128
|
if (!adapter)
|
|
3234
3129
|
return;
|
|
@@ -3237,18 +3132,11 @@ export class FleetManager {
|
|
|
3237
3132
|
if (!groupId)
|
|
3238
3133
|
return;
|
|
3239
3134
|
const threadId = this.fleetConfig?.instances[instanceName]?.topic_id;
|
|
3240
|
-
const instanceHangConfig = this.fleetConfig?.instances[instanceName]?.hang_detector;
|
|
3241
|
-
const configuredMinutes = instanceHangConfig?.timeout_minutes
|
|
3242
|
-
?? this.fleetConfig?.defaults?.hang_detector?.timeout_minutes
|
|
3243
|
-
?? 15;
|
|
3244
|
-
const unchangedMinutes = unchangedForMs == null
|
|
3245
|
-
? configuredMinutes
|
|
3246
|
-
: Math.max(1, Math.floor(unchangedForMs / 60_000));
|
|
3247
3135
|
this.setTopicIcon(instanceName, "red");
|
|
3248
3136
|
await adapter.notifyAlert(String(groupId), {
|
|
3249
3137
|
type: "hang",
|
|
3250
3138
|
instanceName,
|
|
3251
|
-
message: `⚠️ ${instanceName}
|
|
3139
|
+
message: `⚠️ ${instanceName} appears hung (no activity for 15+ minutes)`,
|
|
3252
3140
|
choices: [
|
|
3253
3141
|
{ id: `hang:restart:${instanceName}`, label: "🔄 Force restart" },
|
|
3254
3142
|
{ id: `hang:wait:${instanceName}`, label: "⏳ Keep waiting" },
|
|
@@ -3756,7 +3644,6 @@ When users create specialized instances, suggest these configurations:
|
|
|
3756
3644
|
user_id: msg.userId,
|
|
3757
3645
|
ts: msg.timestamp.toISOString(),
|
|
3758
3646
|
thread_id: msg.threadId ?? "",
|
|
3759
|
-
...(msg.adapterId ? { adapter_id: msg.adapterId } : {}),
|
|
3760
3647
|
source: msg.source,
|
|
3761
3648
|
...extraMeta,
|
|
3762
3649
|
...(msg.replyToText ? { reply_to_text: msg.replyToText } : {}),
|
|
@@ -3813,104 +3700,6 @@ When users create specialized instances, suggest these configurations:
|
|
|
3813
3700
|
return undefined;
|
|
3814
3701
|
}
|
|
3815
3702
|
}
|
|
3816
|
-
/** Return a user-facing blocker without mutating ClassicBot state. */
|
|
3817
|
-
validateClassicStart(channelId, userId, guildId, adapterId) {
|
|
3818
|
-
if (!this.classicChannels)
|
|
3819
|
-
return "Classic channel manager not initialized.";
|
|
3820
|
-
if (guildId && !this.classicChannels.isGuildAllowed(guildId)) {
|
|
3821
|
-
const generalId = this.findGeneralInstance(adapterId);
|
|
3822
|
-
if (generalId)
|
|
3823
|
-
this.notifyInstanceTopic(generalId, t("alert.unauth_guild", guildId, userId));
|
|
3824
|
-
return t("classic.not_authorized_guild");
|
|
3825
|
-
}
|
|
3826
|
-
if (this.classicChannels.isClassicChannel(channelId, adapterId))
|
|
3827
|
-
return t("classic.already_active");
|
|
3828
|
-
if (this.routing.resolve(channelId))
|
|
3829
|
-
return t("classic.topic_bound");
|
|
3830
|
-
return undefined;
|
|
3831
|
-
}
|
|
3832
|
-
/** Present platform-native backend choices, then start on selection or timeout. */
|
|
3833
|
-
async beginClassicBackendSelection(data, adapter) {
|
|
3834
|
-
const adapterId = adapter.id;
|
|
3835
|
-
const blocker = this.validateClassicStart(data.channelId, data.userId, data.guildId, adapterId);
|
|
3836
|
-
if (blocker) {
|
|
3837
|
-
await data.respond(blocker);
|
|
3838
|
-
return;
|
|
3839
|
-
}
|
|
3840
|
-
const nonce = randomBytes(6).toString("hex");
|
|
3841
|
-
const choices = getClassicBackendChoices().map(choice => ({
|
|
3842
|
-
id: `${CLASSIC_BACKEND_CALLBACK_PREFIX}${nonce}:${choice.id}`,
|
|
3843
|
-
label: choice.label,
|
|
3844
|
-
}));
|
|
3845
|
-
const complete = data.respondChoices
|
|
3846
|
-
? async (text) => { await data.respond(text); }
|
|
3847
|
-
: async (text, messageId) => {
|
|
3848
|
-
if (messageId && adapter.editMessageRemoveButtons) {
|
|
3849
|
-
try {
|
|
3850
|
-
await adapter.editMessageRemoveButtons(data.channelId, messageId, text);
|
|
3851
|
-
return;
|
|
3852
|
-
}
|
|
3853
|
-
catch { /* fall back to a new message */ }
|
|
3854
|
-
}
|
|
3855
|
-
await data.respond(text);
|
|
3856
|
-
};
|
|
3857
|
-
const timer = setTimeout(() => {
|
|
3858
|
-
void this.finishClassicBackendSelection(nonce).catch(err => this.logger.warn({ err, channelId: data.channelId, adapterId }, "Classic backend selection timeout fallback failed"));
|
|
3859
|
-
}, CLASSIC_BACKEND_SELECTION_TIMEOUT_MS);
|
|
3860
|
-
timer.unref?.();
|
|
3861
|
-
const pending = {
|
|
3862
|
-
channelId: data.channelId,
|
|
3863
|
-
channelName: data.channelName,
|
|
3864
|
-
userId: data.userId,
|
|
3865
|
-
guildId: data.guildId,
|
|
3866
|
-
adapterId,
|
|
3867
|
-
timer,
|
|
3868
|
-
complete,
|
|
3869
|
-
};
|
|
3870
|
-
this.pendingClassicStarts.set(nonce, pending);
|
|
3871
|
-
try {
|
|
3872
|
-
pending.messageId = data.respondChoices
|
|
3873
|
-
? await data.respondChoices(t("classic.choose_backend"), choices)
|
|
3874
|
-
: await adapter.promptUser(data.channelId, t("classic.choose_backend"), choices);
|
|
3875
|
-
}
|
|
3876
|
-
catch (err) {
|
|
3877
|
-
// A menu transport failure should not make /start unusable: consume the
|
|
3878
|
-
// pending request and immediately use the configured default.
|
|
3879
|
-
this.logger.warn({ err, channelId: data.channelId, adapterId }, "Classic backend menu failed; using default");
|
|
3880
|
-
await this.finishClassicBackendSelection(nonce);
|
|
3881
|
-
}
|
|
3882
|
-
}
|
|
3883
|
-
/** Consume a selection callback. Returns true for all ClassicBot callback IDs, including stale ones. */
|
|
3884
|
-
async handleClassicBackendSelection(data) {
|
|
3885
|
-
if (!data.callbackData.startsWith(CLASSIC_BACKEND_CALLBACK_PREFIX))
|
|
3886
|
-
return false;
|
|
3887
|
-
const match = data.callbackData.match(/^classic-backend:([0-9a-f]+):(.+)$/);
|
|
3888
|
-
if (!match)
|
|
3889
|
-
return true;
|
|
3890
|
-
const pending = this.pendingClassicStarts.get(match[1]);
|
|
3891
|
-
if (!pending)
|
|
3892
|
-
return true;
|
|
3893
|
-
// Telegram keyboards are visible to everyone in a group. Only the user who
|
|
3894
|
-
// issued /start may consume the pending selection.
|
|
3895
|
-
if (data.userId && data.userId !== pending.userId)
|
|
3896
|
-
return true;
|
|
3897
|
-
const callbackChannelId = data.threadId ?? data.chatId;
|
|
3898
|
-
if (callbackChannelId !== pending.channelId && data.chatId !== pending.channelId)
|
|
3899
|
-
return true;
|
|
3900
|
-
await this.finishClassicBackendSelection(match[1], match[2]);
|
|
3901
|
-
return true;
|
|
3902
|
-
}
|
|
3903
|
-
/** Atomically claim one pending request so timeout/click races create at most one instance. */
|
|
3904
|
-
async finishClassicBackendSelection(nonce, backend) {
|
|
3905
|
-
const pending = this.pendingClassicStarts.get(nonce);
|
|
3906
|
-
if (!pending)
|
|
3907
|
-
return;
|
|
3908
|
-
this.pendingClassicStarts.delete(nonce);
|
|
3909
|
-
clearTimeout(pending.timer);
|
|
3910
|
-
const selectedBackend = isSelectableClassicBackend(backend) ? backend : undefined;
|
|
3911
|
-
const reply = await this.handleClassicStart(pending.channelId, pending.channelName, pending.userId, pending.guildId, pending.adapterId, selectedBackend);
|
|
3912
|
-
await pending.complete(reply, pending.messageId);
|
|
3913
|
-
}
|
|
3914
3703
|
/** Start a classic channel instance with lightweight config */
|
|
3915
3704
|
async startClassicInstance(instanceName, backend, preTaskCommand, model) {
|
|
3916
3705
|
if (this.daemons.has(instanceName))
|
|
@@ -3930,26 +3719,35 @@ When users create specialized instances, suggest these configurations:
|
|
|
3930
3719
|
await this.startInstance(instanceName, config, topicMode);
|
|
3931
3720
|
}
|
|
3932
3721
|
/** Handle /start slash command — register classic channel */
|
|
3933
|
-
async handleClassicStart(channelId, channelName, userId, guildId, adapterId
|
|
3934
|
-
|
|
3935
|
-
if (blocker)
|
|
3936
|
-
return blocker;
|
|
3937
|
-
const classicChannels = this.classicChannels;
|
|
3938
|
-
if (!classicChannels)
|
|
3722
|
+
async handleClassicStart(channelId, channelName, userId, guildId, adapterId) {
|
|
3723
|
+
if (!this.classicChannels)
|
|
3939
3724
|
return "Classic channel manager not initialized.";
|
|
3940
|
-
|
|
3941
|
-
|
|
3942
|
-
|
|
3725
|
+
if (guildId && !this.classicChannels.isGuildAllowed(guildId)) {
|
|
3726
|
+
const generalId = this.findGeneralInstance();
|
|
3727
|
+
if (generalId) {
|
|
3728
|
+
this.notifyInstanceTopic(generalId, t("alert.unauth_guild", guildId, userId));
|
|
3729
|
+
}
|
|
3730
|
+
return t("classic.not_authorized_guild");
|
|
3731
|
+
}
|
|
3732
|
+
// Per-bot check: a second bot may /start in the same channel (own agent).
|
|
3733
|
+
if (this.classicChannels.isClassicChannel(channelId, adapterId))
|
|
3734
|
+
return t("classic.already_active");
|
|
3735
|
+
// Classic no longer lives in the routing engine, so this only guards against
|
|
3736
|
+
// a fleet topic-mode instance colliding with the channel.
|
|
3737
|
+
if (this.routing.resolve(channelId))
|
|
3738
|
+
return t("classic.topic_bound");
|
|
3739
|
+
const instanceName = this.classicChannels.deriveInstanceName(channelName || channelId, channelId, adapterId);
|
|
3740
|
+
this.classicChannels.register(channelId, adapterId, instanceName, channelName || channelId, userId);
|
|
3943
3741
|
// Bind this classic instance to the bot that started it (authoritative), so
|
|
3944
3742
|
// replies/cancel go out through that bot even though every same-guild bot
|
|
3945
3743
|
// also sees the channel's messages.
|
|
3946
3744
|
if (adapterId)
|
|
3947
3745
|
this.bindInstanceAdapter(instanceName, adapterId);
|
|
3948
|
-
await this.startClassicInstance(instanceName, classicChannels.getBackend(channelId, adapterId, this.fleetConfig?.defaults?.backend), classicChannels.getPreTaskCommand(channelId, adapterId), classicChannels.getModel(channelId, adapterId, this.fleetConfig?.defaults?.model));
|
|
3746
|
+
await this.startClassicInstance(instanceName, this.classicChannels.getBackend(channelId, adapterId, this.fleetConfig?.defaults?.backend), this.classicChannels.getPreTaskCommand(channelId, adapterId), this.classicChannels.getModel(channelId, adapterId, this.fleetConfig?.defaults?.model));
|
|
3949
3747
|
this.reregisterClassicChannels();
|
|
3950
3748
|
// Auto-enable collab for Discord classic channels (TG uses @mention directly without collab mode)
|
|
3951
|
-
if (guildId && !classicChannels.isCollab(channelId, adapterId)) {
|
|
3952
|
-
classicChannels.toggleCollab(channelId, adapterId);
|
|
3749
|
+
if (guildId && !this.classicChannels.isCollab(channelId, adapterId)) {
|
|
3750
|
+
this.classicChannels.toggleCollab(channelId, adapterId);
|
|
3953
3751
|
}
|
|
3954
3752
|
this.logger.info({ channelId, adapterId, instanceName, userId }, "Classic channel started");
|
|
3955
3753
|
return t("classic.started");
|
|
@@ -4006,9 +3804,6 @@ When users create specialized instances, suggest these configurations:
|
|
|
4006
3804
|
clearInterval(this.classicReloadTimer);
|
|
4007
3805
|
this.classicReloadTimer = null;
|
|
4008
3806
|
}
|
|
4009
|
-
for (const pending of this.pendingClassicStarts.values())
|
|
4010
|
-
clearTimeout(pending.timer);
|
|
4011
|
-
this.pendingClassicStarts.clear();
|
|
4012
3807
|
this.topicArchiver.stop();
|
|
4013
3808
|
this.scheduler?.shutdown();
|
|
4014
3809
|
// Stop instances in parallel batches to avoid long sequential waits.
|
|
@@ -4554,7 +4349,6 @@ When users create specialized instances, suggest these configurations:
|
|
|
4554
4349
|
lastActivity: this.lastActivityMs(inst.name) || null,
|
|
4555
4350
|
currentTask,
|
|
4556
4351
|
idle: this.getInstanceIdle(inst.name),
|
|
4557
|
-
state: this.getInstanceExecutionState(inst.name),
|
|
4558
4352
|
};
|
|
4559
4353
|
});
|
|
4560
4354
|
res.setHeader("Access-Control-Allow-Origin", "*");
|