@songsid/agend 2.1.0-beta.19 → 2.1.0-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 +0 -1
- 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/cli.js +6 -56
- 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 +0 -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 +3 -51
- package/dist/daemon.js +36 -380
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-context.d.ts +1 -2
- package/dist/fleet-manager.d.ts +5 -23
- package/dist/fleet-manager.js +123 -229
- package/dist/fleet-manager.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +1 -6
- package/dist/instance-lifecycle.js +3 -45
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/logger.d.ts +0 -6
- package/dist/logger.js +10 -29
- package/dist/logger.js.map +1 -1
- package/dist/outbound-handlers.d.ts +0 -5
- package/dist/outbound-handlers.js +16 -68
- package/dist/outbound-handlers.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 -3
- package/dist/tmux-manager.js +0 -9
- package/dist/tmux-manager.js.map +1 -1
- package/dist/topic-archiver.d.ts +1 -1
- package/dist/topic-commands.d.ts +1 -14
- package/dist/topic-commands.js +25 -96
- 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 -18
- package/dist/ui/dashboard.html +2 -2
- package/dist/ui/settings.html +40 -251
- package/dist/view-api.d.ts +1 -1
- package/dist/web-api.d.ts +1 -2
- package/dist/web-api.js +14 -23
- package/dist/web-api.js.map +1 -1
- package/package.json +1 -2
- package/dist/backend/grok.d.ts +0 -41
- package/dist/backend/grok.js +0 -243
- 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";
|
|
@@ -65,9 +65,6 @@ export class FleetManager {
|
|
|
65
65
|
/** @deprecated Use lifecycle.daemons — kept for backward compat */
|
|
66
66
|
get daemons() { return this.lifecycle.daemons; }
|
|
67
67
|
fleetConfig = null;
|
|
68
|
-
rawFleetConfig = {};
|
|
69
|
-
rawFleetDocument = null;
|
|
70
|
-
savedFleetConfigSnapshot = null;
|
|
71
68
|
adapter = null;
|
|
72
69
|
worlds = new Map();
|
|
73
70
|
adapters = new Map(); // derived view for backward compat
|
|
@@ -96,8 +93,6 @@ export class FleetManager {
|
|
|
96
93
|
// Topic icon + auto-archive state
|
|
97
94
|
topicIcons = {};
|
|
98
95
|
lastActivity = new Map();
|
|
99
|
-
/** Latest pane-derived execution snapshot reported by each daemon. */
|
|
100
|
-
instanceStateCache = new Map();
|
|
101
96
|
lastInboundUser = new Map(); // instanceName → last username
|
|
102
97
|
// Active "🛑 Cancel" buttons, tracked per button (keyed by messageId) rather
|
|
103
98
|
// than one-per-instance. A button is retired (deleted, with bounded retry) on
|
|
@@ -165,7 +160,6 @@ export class FleetManager {
|
|
|
165
160
|
const instances = Object.keys(this.fleetConfig?.instances ?? {}).map(name => ({
|
|
166
161
|
name,
|
|
167
162
|
status: this.getInstanceStatus(name),
|
|
168
|
-
state: this.getInstanceExecutionState(name),
|
|
169
163
|
ipc: this.instanceIpcClients.has(name),
|
|
170
164
|
costCents: this.costGuard?.getDailyCostCents(name) ?? 0,
|
|
171
165
|
rateLimits: this.statuslineWatcher.getRateLimits(name) ?? null,
|
|
@@ -180,21 +174,9 @@ export class FleetManager {
|
|
|
180
174
|
}
|
|
181
175
|
/** Load fleet.yaml and build routing table */
|
|
182
176
|
loadConfig(configPath) {
|
|
183
|
-
this.configPath = configPath;
|
|
184
|
-
const source = existsSync(configPath) ? readFileSync(configPath, "utf-8") : "{}\n";
|
|
185
|
-
this.rawFleetDocument = parseDocument(source, { keepSourceTokens: true });
|
|
186
|
-
if (this.rawFleetDocument.errors.length > 0) {
|
|
187
|
-
throw new Error(`Invalid fleet.yaml: ${this.rawFleetDocument.errors[0].message}`);
|
|
188
|
-
}
|
|
189
|
-
this.rawFleetConfig = loadRawFleetConfig(configPath);
|
|
190
177
|
this.fleetConfig = loadFleetConfig(configPath);
|
|
191
|
-
this.savedFleetConfigSnapshot = structuredClone(this.fleetConfig);
|
|
192
178
|
return this.fleetConfig;
|
|
193
179
|
}
|
|
194
|
-
/** User-authored fleet.yaml, before defaults are merged into instances. */
|
|
195
|
-
getRawFleetConfig() {
|
|
196
|
-
return structuredClone(this.rawFleetConfig);
|
|
197
|
-
}
|
|
198
180
|
/** Build topic routing table: { topicId -> RouteTarget } */
|
|
199
181
|
buildRoutingTable() {
|
|
200
182
|
if (this.fleetConfig) {
|
|
@@ -283,24 +265,17 @@ export class FleetManager {
|
|
|
283
265
|
*/
|
|
284
266
|
bindInstanceAdapter(name, adapterId, fromInbound = false) {
|
|
285
267
|
const cfg = this.fleetConfig?.instances[name];
|
|
286
|
-
if (fromInbound)
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
return; // fleet instance without explicit binding → use primary
|
|
295
|
-
// Classic instance: don't override an existing binding (authoritative from /start)
|
|
296
|
-
if (this.classicChannels?.getChannelIdByInstance(name) !== undefined && this.instanceWorldBinding.has(name))
|
|
297
|
-
return;
|
|
298
|
-
}
|
|
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;
|
|
299
276
|
this.instanceWorldBinding.set(name, adapterId);
|
|
300
277
|
}
|
|
301
278
|
getInstanceStatus(name) {
|
|
302
|
-
if (this.lifecycle.isPaused(name))
|
|
303
|
-
return "paused";
|
|
304
279
|
const pidPath = join(this.getInstanceDir(name), "daemon.pid");
|
|
305
280
|
if (!existsSync(pidPath))
|
|
306
281
|
return "stopped";
|
|
@@ -313,35 +288,6 @@ export class FleetManager {
|
|
|
313
288
|
return "crashed";
|
|
314
289
|
}
|
|
315
290
|
}
|
|
316
|
-
getInstanceExecutionState(name) {
|
|
317
|
-
if (this.lifecycle.isPaused(name))
|
|
318
|
-
return null;
|
|
319
|
-
return this.instanceStateCache.get(name)?.state ?? null;
|
|
320
|
-
}
|
|
321
|
-
cacheInstanceExecutionState(name, msg) {
|
|
322
|
-
const state = msg.state;
|
|
323
|
-
if (state !== "idle" && state !== "working" && state !== "stuck")
|
|
324
|
-
return;
|
|
325
|
-
const previous = this.instanceStateCache.get(name);
|
|
326
|
-
const now = Date.now();
|
|
327
|
-
const numberOr = (value, fallback) => typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
328
|
-
this.instanceStateCache.set(name, {
|
|
329
|
-
state,
|
|
330
|
-
unchangedForMs: numberOr(msg.unchangedForMs, previous?.unchangedForMs ?? 0),
|
|
331
|
-
observedAt: numberOr(msg.observedAt, now),
|
|
332
|
-
stateChangedAt: numberOr(msg.stateChangedAt, previous?.state === state ? previous.stateChangedAt : now),
|
|
333
|
-
});
|
|
334
|
-
}
|
|
335
|
-
/** Single delivery facade: wake an auto-paused CLI before sending to daemon IPC. */
|
|
336
|
-
async deliverToInstance(instanceName, payload) {
|
|
337
|
-
if (this.lifecycle.isPaused(instanceName)) {
|
|
338
|
-
await this.lifecycle.wake(instanceName, 30_000);
|
|
339
|
-
}
|
|
340
|
-
const ipc = this.instanceIpcClients.get(instanceName);
|
|
341
|
-
if (!ipc?.connected)
|
|
342
|
-
throw new Error(`Instance '${instanceName}' IPC is unavailable`);
|
|
343
|
-
ipc.send(payload);
|
|
344
|
-
}
|
|
345
291
|
async startInstance(name, config, topicMode) {
|
|
346
292
|
if (config.general_topic) {
|
|
347
293
|
this.ensureGeneralInstructions(config.working_directory, config.backend);
|
|
@@ -409,7 +355,6 @@ export class FleetManager {
|
|
|
409
355
|
}
|
|
410
356
|
async stopInstance(name) {
|
|
411
357
|
this.failoverActive.delete(name);
|
|
412
|
-
this.instanceStateCache.delete(name);
|
|
413
358
|
return this.lifecycle.stop(name);
|
|
414
359
|
}
|
|
415
360
|
/** Restart a single instance, reloading fleet.yaml first to pick up config changes. */
|
|
@@ -1575,16 +1520,10 @@ export class FleetManager {
|
|
|
1575
1520
|
else if (msg.type === "fleet_set_description") {
|
|
1576
1521
|
this.handleSetDescription(name, msg);
|
|
1577
1522
|
}
|
|
1578
|
-
else if (msg.type === "instance_state" || msg.type === "instance_state_response") {
|
|
1579
|
-
this.cacheInstanceExecutionState(name, msg);
|
|
1580
|
-
}
|
|
1581
1523
|
}, this.logger, `ipc.message[${name}]`));
|
|
1582
1524
|
// Ask daemon for any sessions that registered before we connected
|
|
1583
1525
|
// (fixes race condition where mcp_ready was broadcast before fleet manager connected)
|
|
1584
1526
|
ipc.send({ type: "query_sessions" });
|
|
1585
|
-
// The initial state transition may have happened before FleetManager
|
|
1586
|
-
// connected, so seed the cache instead of waiting for another transition.
|
|
1587
|
-
ipc.send({ type: "query_instance_state", requestId: `fleet-state-${Date.now()}` });
|
|
1588
1527
|
this.logger.debug({ name }, "Connected to instance IPC");
|
|
1589
1528
|
if (!this.statuslineWatcher.has(name)) {
|
|
1590
1529
|
this.statuslineWatcher.watch(name);
|
|
@@ -1987,8 +1926,9 @@ export class FleetManager {
|
|
|
1987
1926
|
}
|
|
1988
1927
|
this.warnIfRateLimited(generalInstance, msg);
|
|
1989
1928
|
const { text, extraMeta } = await processAttachments(msg, inboundAdapter, this.logger, generalInstance);
|
|
1990
|
-
|
|
1991
|
-
|
|
1929
|
+
const ipc = this.instanceIpcClients.get(generalInstance);
|
|
1930
|
+
if (ipc) {
|
|
1931
|
+
ipc.send({
|
|
1992
1932
|
type: "fleet_inbound",
|
|
1993
1933
|
content: text,
|
|
1994
1934
|
targetSession: generalInstance,
|
|
@@ -1999,7 +1939,6 @@ export class FleetManager {
|
|
|
1999
1939
|
user_id: msg.userId,
|
|
2000
1940
|
ts: msg.timestamp.toISOString(),
|
|
2001
1941
|
thread_id: "",
|
|
2002
|
-
adapter_id: msg.adapterId,
|
|
2003
1942
|
source: msg.source,
|
|
2004
1943
|
...(msg.replyToText ? { reply_to_text: msg.replyToText } : {}),
|
|
2005
1944
|
...extraMeta,
|
|
@@ -2015,9 +1954,6 @@ export class FleetManager {
|
|
|
2015
1954
|
this.trackInboundMsg(generalInstance, msg);
|
|
2016
1955
|
void this.sendCancelButton(generalInstance);
|
|
2017
1956
|
}
|
|
2018
|
-
catch (err) {
|
|
2019
|
-
this.logger.warn({ err: err.message, instanceName: generalInstance }, "General wake/delivery failed");
|
|
2020
|
-
}
|
|
2021
1957
|
}
|
|
2022
1958
|
return;
|
|
2023
1959
|
}
|
|
@@ -2080,33 +2016,27 @@ export class FleetManager {
|
|
|
2080
2016
|
this.setTopicIcon(instanceName, "blue");
|
|
2081
2017
|
this.warnIfRateLimited(instanceName, msg);
|
|
2082
2018
|
const { text, extraMeta } = await processAttachments(msg, inboundAdapter, this.logger, instanceName);
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
content: text,
|
|
2087
|
-
targetSession: instanceName, // Channel messages → instance's own session
|
|
2088
|
-
meta: {
|
|
2089
|
-
chat_id: msg.chatId,
|
|
2090
|
-
message_id: msg.messageId,
|
|
2091
|
-
user: msg.username,
|
|
2092
|
-
user_id: msg.userId,
|
|
2093
|
-
ts: msg.timestamp.toISOString(),
|
|
2094
|
-
thread_id: msg.threadId ?? "",
|
|
2095
|
-
adapter_id: msg.adapterId,
|
|
2096
|
-
source: msg.source,
|
|
2097
|
-
...(msg.replyToText ? { reply_to_text: msg.replyToText } : {}),
|
|
2098
|
-
...extraMeta,
|
|
2099
|
-
},
|
|
2100
|
-
});
|
|
2101
|
-
}
|
|
2102
|
-
catch (err) {
|
|
2103
|
-
this.logger.warn({ err: err.message, instanceName }, "Wake/delivery failed");
|
|
2104
|
-
if (msg.chatId && msg.messageId) {
|
|
2105
|
-
const reactAdapter = this.getAdapterForInstance(instanceName) ?? inboundAdapter;
|
|
2106
|
-
reactAdapter.react(this.reactTarget(msg), msg.messageId, "❌").catch(() => { });
|
|
2107
|
-
}
|
|
2019
|
+
const ipc = this.instanceIpcClients.get(instanceName);
|
|
2020
|
+
if (!ipc) {
|
|
2021
|
+
this.logger.warn({ instanceName }, "No IPC connection to instance");
|
|
2108
2022
|
return;
|
|
2109
2023
|
}
|
|
2024
|
+
ipc.send({
|
|
2025
|
+
type: "fleet_inbound",
|
|
2026
|
+
content: text,
|
|
2027
|
+
targetSession: instanceName, // Channel messages → instance's own session
|
|
2028
|
+
meta: {
|
|
2029
|
+
chat_id: msg.chatId,
|
|
2030
|
+
message_id: msg.messageId,
|
|
2031
|
+
user: msg.username,
|
|
2032
|
+
user_id: msg.userId,
|
|
2033
|
+
ts: msg.timestamp.toISOString(),
|
|
2034
|
+
thread_id: msg.threadId ?? "",
|
|
2035
|
+
source: msg.source,
|
|
2036
|
+
...(msg.replyToText ? { reply_to_text: msg.replyToText } : {}),
|
|
2037
|
+
...extraMeta,
|
|
2038
|
+
},
|
|
2039
|
+
});
|
|
2110
2040
|
this.lastInboundUser.set(instanceName, msg.username);
|
|
2111
2041
|
this.logger.info(`${msg.username} → ${instanceName}: ${(text ?? "").slice(0, 100)}`);
|
|
2112
2042
|
this.eventLog?.logActivity("message", msg.username, (text ?? "").slice(0, 200), instanceName);
|
|
@@ -2267,23 +2197,20 @@ export class FleetManager {
|
|
|
2267
2197
|
const schedulerDefaults = this.fleetConfig?.defaults.scheduler;
|
|
2268
2198
|
const retryCount = schedulerDefaults?.retry_count ?? 3;
|
|
2269
2199
|
const retryInterval = schedulerDefaults?.retry_interval_ms ?? 30_000;
|
|
2270
|
-
const deliver =
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
type: "fleet_schedule_trigger",
|
|
2274
|
-
payload: { schedule_id: id, message: `[Scheduled] ${message}`, label },
|
|
2275
|
-
meta: { chat_id: reply_chat_id, thread_id: reply_thread_id, user: "scheduler" },
|
|
2276
|
-
});
|
|
2277
|
-
// A scheduled trigger also puts the instance to work — show a cancel button.
|
|
2278
|
-
void this.sendCancelButton(target);
|
|
2279
|
-
return true;
|
|
2280
|
-
}
|
|
2281
|
-
catch (err) {
|
|
2282
|
-
this.logger.warn({ err: err.message, target }, "Scheduled wake/delivery attempt failed");
|
|
2200
|
+
const deliver = () => {
|
|
2201
|
+
const ipc = this.instanceIpcClients.get(target);
|
|
2202
|
+
if (!ipc?.connected)
|
|
2283
2203
|
return false;
|
|
2284
|
-
|
|
2204
|
+
ipc.send({
|
|
2205
|
+
type: "fleet_schedule_trigger",
|
|
2206
|
+
payload: { schedule_id: id, message: `[Scheduled] ${message}`, label },
|
|
2207
|
+
meta: { chat_id: reply_chat_id, thread_id: reply_thread_id, user: "scheduler" },
|
|
2208
|
+
});
|
|
2209
|
+
// A scheduled trigger also puts the instance to work — show a cancel button.
|
|
2210
|
+
void this.sendCancelButton(target);
|
|
2211
|
+
return true;
|
|
2285
2212
|
};
|
|
2286
|
-
if (
|
|
2213
|
+
if (deliver()) {
|
|
2287
2214
|
this.scheduler.recordRun(id, "delivered");
|
|
2288
2215
|
if (source !== target)
|
|
2289
2216
|
this.notifySourceTopic(schedule);
|
|
@@ -2291,7 +2218,7 @@ export class FleetManager {
|
|
|
2291
2218
|
}
|
|
2292
2219
|
for (let i = 0; i < retryCount; i++) {
|
|
2293
2220
|
await new Promise((r) => setTimeout(r, retryInterval));
|
|
2294
|
-
if (
|
|
2221
|
+
if (deliver()) {
|
|
2295
2222
|
this.scheduler.recordRun(id, "delivered");
|
|
2296
2223
|
if (source !== target)
|
|
2297
2224
|
this.notifySourceTopic(schedule);
|
|
@@ -2715,92 +2642,73 @@ export class FleetManager {
|
|
|
2715
2642
|
}
|
|
2716
2643
|
}, 5 * 60_000);
|
|
2717
2644
|
}
|
|
2718
|
-
/**
|
|
2719
|
-
|
|
2720
|
-
* document. Unknown keys, explicit overrides and comments remain untouched.
|
|
2721
|
-
*/
|
|
2722
|
-
saveFleetConfig(explicitPatches = []) {
|
|
2645
|
+
/** Save fleet config back to fleet.yaml */
|
|
2646
|
+
saveFleetConfig() {
|
|
2723
2647
|
if (!this.fleetConfig || !this.configPath)
|
|
2724
2648
|
return;
|
|
2725
|
-
|
|
2726
|
-
|
|
2727
|
-
|
|
2728
|
-
|
|
2729
|
-
|
|
2730
|
-
|
|
2731
|
-
if (this.
|
|
2732
|
-
|
|
2733
|
-
}
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
}
|
|
2748
|
-
const
|
|
2749
|
-
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
|
|
2761
|
-
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
|
|
2767
|
-
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2779
|
-
|
|
2780
|
-
|
|
2781
|
-
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
2786
|
-
|
|
2787
|
-
|
|
2788
|
-
}
|
|
2789
|
-
if (after === undefined) {
|
|
2790
|
-
document.deleteIn(path);
|
|
2791
|
-
}
|
|
2792
|
-
else if (path.length === 0) {
|
|
2793
|
-
document.contents = document.createNode(after);
|
|
2794
|
-
}
|
|
2795
|
-
else {
|
|
2796
|
-
const currentNode = document.getIn(path, true);
|
|
2797
|
-
if (isScalar(currentNode) && (after === null || typeof after !== "object")) {
|
|
2798
|
-
currentNode.value = after;
|
|
2799
|
-
}
|
|
2800
|
-
else {
|
|
2801
|
-
document.setIn(path, after);
|
|
2802
|
-
}
|
|
2803
|
-
}
|
|
2649
|
+
const toSave = {};
|
|
2650
|
+
if (this.fleetConfig.project_roots)
|
|
2651
|
+
toSave.project_roots = this.fleetConfig.project_roots;
|
|
2652
|
+
if (this.fleetConfig.channels && this.fleetConfig.channels.length > 0) {
|
|
2653
|
+
toSave.channels = this.fleetConfig.channels;
|
|
2654
|
+
}
|
|
2655
|
+
else if (this.fleetConfig.channel) {
|
|
2656
|
+
toSave.channel = this.fleetConfig.channel;
|
|
2657
|
+
}
|
|
2658
|
+
if (this.fleetConfig.health_port)
|
|
2659
|
+
toSave.health_port = this.fleetConfig.health_port;
|
|
2660
|
+
if (Object.keys(this.fleetConfig.defaults).length > 0)
|
|
2661
|
+
toSave.defaults = this.fleetConfig.defaults;
|
|
2662
|
+
if (this.fleetConfig.teams && Object.keys(this.fleetConfig.teams).length > 0) {
|
|
2663
|
+
toSave.teams = this.fleetConfig.teams;
|
|
2664
|
+
}
|
|
2665
|
+
if (this.fleetConfig.templates && Object.keys(this.fleetConfig.templates).length > 0) {
|
|
2666
|
+
toSave.templates = this.fleetConfig.templates;
|
|
2667
|
+
}
|
|
2668
|
+
if (this.fleetConfig.profiles && Object.keys(this.fleetConfig.profiles).length > 0) {
|
|
2669
|
+
toSave.profiles = this.fleetConfig.profiles;
|
|
2670
|
+
}
|
|
2671
|
+
toSave.instances = {};
|
|
2672
|
+
for (const [name, inst] of Object.entries(this.fleetConfig.instances)) {
|
|
2673
|
+
const serialized = {
|
|
2674
|
+
working_directory: inst.working_directory,
|
|
2675
|
+
topic_id: inst.topic_id,
|
|
2676
|
+
};
|
|
2677
|
+
// Preserve all optional user-configured fields so saveFleetConfig() never silently drops them
|
|
2678
|
+
if (inst.general_topic)
|
|
2679
|
+
serialized.general_topic = true;
|
|
2680
|
+
if (inst.display_name)
|
|
2681
|
+
serialized.display_name = inst.display_name;
|
|
2682
|
+
if (inst.channel_id)
|
|
2683
|
+
serialized.channel_id = inst.channel_id;
|
|
2684
|
+
if (inst.description)
|
|
2685
|
+
serialized.description = inst.description;
|
|
2686
|
+
if (inst.tags?.length)
|
|
2687
|
+
serialized.tags = inst.tags;
|
|
2688
|
+
if (inst.model)
|
|
2689
|
+
serialized.model = inst.model;
|
|
2690
|
+
if (inst.model_failover?.length)
|
|
2691
|
+
serialized.model_failover = inst.model_failover;
|
|
2692
|
+
if (inst.worktree_source)
|
|
2693
|
+
serialized.worktree_source = inst.worktree_source;
|
|
2694
|
+
if (inst.backend)
|
|
2695
|
+
serialized.backend = inst.backend;
|
|
2696
|
+
if (inst.systemPrompt)
|
|
2697
|
+
serialized.systemPrompt = inst.systemPrompt;
|
|
2698
|
+
if (inst.skipPermissions)
|
|
2699
|
+
serialized.skipPermissions = inst.skipPermissions;
|
|
2700
|
+
if (inst.lightweight)
|
|
2701
|
+
serialized.lightweight = inst.lightweight;
|
|
2702
|
+
if (inst.cost_guard)
|
|
2703
|
+
serialized.cost_guard = inst.cost_guard;
|
|
2704
|
+
if (inst.workflow !== undefined)
|
|
2705
|
+
serialized.workflow = inst.workflow;
|
|
2706
|
+
if (inst.agent_mode)
|
|
2707
|
+
serialized.agent_mode = inst.agent_mode;
|
|
2708
|
+
toSave.instances[name] = serialized;
|
|
2709
|
+
}
|
|
2710
|
+
writeFileSync(this.configPath, yaml.dump(toSave, { lineWidth: 120 }));
|
|
2711
|
+
this.logger.info({ path: this.configPath }, "Saved fleet config");
|
|
2804
2712
|
}
|
|
2805
2713
|
async removeInstance(name) {
|
|
2806
2714
|
// Clean up schedules (scheduler is fleet-level, not lifecycle-level)
|
|
@@ -2843,10 +2751,6 @@ export class FleetManager {
|
|
|
2843
2751
|
startStatuslineWatcher(name) {
|
|
2844
2752
|
this.statuslineWatcher.watch(name);
|
|
2845
2753
|
}
|
|
2846
|
-
stopStatuslineWatcher(name) {
|
|
2847
|
-
// Pausing stops I/O but retains the last observed limits for status views.
|
|
2848
|
-
this.statuslineWatcher.unwatch(name, true);
|
|
2849
|
-
}
|
|
2850
2754
|
reactMessageStatus(instanceName, chatId, messageId, emoji) {
|
|
2851
2755
|
// React via the adapter BOUND to this instance — NOT the first discord world.
|
|
2852
2756
|
// Otherwise, in a same-channel/same-guild multi-bot setup, the inbound 👀
|
|
@@ -3197,7 +3101,7 @@ export class FleetManager {
|
|
|
3197
3101
|
return [];
|
|
3198
3102
|
}
|
|
3199
3103
|
}
|
|
3200
|
-
async sendHangNotification(instanceName
|
|
3104
|
+
async sendHangNotification(instanceName) {
|
|
3201
3105
|
const adapter = this.getAdapterForInstance(instanceName) ?? this.adapter;
|
|
3202
3106
|
if (!adapter)
|
|
3203
3107
|
return;
|
|
@@ -3206,18 +3110,11 @@ export class FleetManager {
|
|
|
3206
3110
|
if (!groupId)
|
|
3207
3111
|
return;
|
|
3208
3112
|
const threadId = this.fleetConfig?.instances[instanceName]?.topic_id;
|
|
3209
|
-
const instanceHangConfig = this.fleetConfig?.instances[instanceName]?.hang_detector;
|
|
3210
|
-
const configuredMinutes = instanceHangConfig?.timeout_minutes
|
|
3211
|
-
?? this.fleetConfig?.defaults?.hang_detector?.timeout_minutes
|
|
3212
|
-
?? 15;
|
|
3213
|
-
const unchangedMinutes = unchangedForMs == null
|
|
3214
|
-
? configuredMinutes
|
|
3215
|
-
: Math.max(1, Math.floor(unchangedForMs / 60_000));
|
|
3216
3113
|
this.setTopicIcon(instanceName, "red");
|
|
3217
3114
|
await adapter.notifyAlert(String(groupId), {
|
|
3218
3115
|
type: "hang",
|
|
3219
3116
|
instanceName,
|
|
3220
|
-
message: `⚠️ ${instanceName}
|
|
3117
|
+
message: `⚠️ ${instanceName} appears hung (no activity for 15+ minutes)`,
|
|
3221
3118
|
choices: [
|
|
3222
3119
|
{ id: `hang:restart:${instanceName}`, label: "🔄 Force restart" },
|
|
3223
3120
|
{ id: `hang:wait:${instanceName}`, label: "⏳ Keep waiting" },
|
|
@@ -3718,6 +3615,11 @@ When users create specialized instances, suggest these configurations:
|
|
|
3718
3615
|
const fullText = logContext
|
|
3719
3616
|
? `[Chat log for context]\n${logContext}\n\n[User message]\n${text}`
|
|
3720
3617
|
: text;
|
|
3618
|
+
const ipc = this.instanceIpcClients.get(instanceName);
|
|
3619
|
+
if (!ipc) {
|
|
3620
|
+
this.logger.warn({ instanceName }, "Classic channel instance IPC not connected");
|
|
3621
|
+
return;
|
|
3622
|
+
}
|
|
3721
3623
|
const meta = {
|
|
3722
3624
|
chat_id: msg.chatId,
|
|
3723
3625
|
message_id: msg.messageId,
|
|
@@ -3725,7 +3627,6 @@ When users create specialized instances, suggest these configurations:
|
|
|
3725
3627
|
user_id: msg.userId,
|
|
3726
3628
|
ts: msg.timestamp.toISOString(),
|
|
3727
3629
|
thread_id: msg.threadId ?? "",
|
|
3728
|
-
...(msg.adapterId ? { adapter_id: msg.adapterId } : {}),
|
|
3729
3630
|
source: msg.source,
|
|
3730
3631
|
...extraMeta,
|
|
3731
3632
|
...(msg.replyToText ? { reply_to_text: msg.replyToText } : {}),
|
|
@@ -3740,18 +3641,12 @@ When users create specialized instances, suggest these configurations:
|
|
|
3740
3641
|
meta.image_path = saves[saves.length - 1][1].split(",")[0].trim();
|
|
3741
3642
|
}
|
|
3742
3643
|
}
|
|
3743
|
-
|
|
3744
|
-
|
|
3745
|
-
|
|
3746
|
-
|
|
3747
|
-
|
|
3748
|
-
|
|
3749
|
-
});
|
|
3750
|
-
}
|
|
3751
|
-
catch (err) {
|
|
3752
|
-
this.logger.warn({ err: err.message, instanceName }, "Classic wake/delivery failed");
|
|
3753
|
-
return;
|
|
3754
|
-
}
|
|
3644
|
+
ipc.send({
|
|
3645
|
+
type: "fleet_inbound",
|
|
3646
|
+
content: fullText,
|
|
3647
|
+
targetSession: instanceName,
|
|
3648
|
+
meta,
|
|
3649
|
+
});
|
|
3755
3650
|
this.lastInboundUser.set(instanceName, msg.username);
|
|
3756
3651
|
this.logger.info(`${msg.username} → ${instanceName} (classic): ${text.slice(0, 100)}`);
|
|
3757
3652
|
this.trackInboundMsg(instanceName, msg);
|
|
@@ -4431,7 +4326,6 @@ When users create specialized instances, suggest these configurations:
|
|
|
4431
4326
|
lastActivity: this.lastActivityMs(inst.name) || null,
|
|
4432
4327
|
currentTask,
|
|
4433
4328
|
idle: this.getInstanceIdle(inst.name),
|
|
4434
|
-
state: this.getInstanceExecutionState(inst.name),
|
|
4435
4329
|
};
|
|
4436
4330
|
});
|
|
4437
4331
|
res.setHeader("Access-Control-Allow-Origin", "*");
|