@songsid/agend 2.0.12 → 2.1.0-beta.10
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/backend/antigravity.js +3 -1
- package/dist/backend/antigravity.js.map +1 -1
- package/dist/backend/codex.js +3 -1
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/kiro.js +3 -2
- package/dist/backend/kiro.js.map +1 -1
- package/dist/backend/types.d.ts +12 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/cli.js +43 -6
- package/dist/cli.js.map +1 -1
- package/dist/config.js +1 -0
- package/dist/config.js.map +1 -1
- package/dist/context-guardian.d.ts +2 -0
- package/dist/context-guardian.js +10 -2
- package/dist/context-guardian.js.map +1 -1
- package/dist/daemon-entry.js +2 -1
- package/dist/daemon-entry.js.map +1 -1
- package/dist/daemon.d.ts +78 -3
- package/dist/daemon.js +496 -27
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-context.d.ts +7 -1
- package/dist/fleet-manager.d.ts +23 -2
- package/dist/fleet-manager.js +207 -65
- package/dist/fleet-manager.js.map +1 -1
- package/dist/instance-lifecycle.d.ts +6 -1
- package/dist/instance-lifecycle.js +45 -3
- package/dist/instance-lifecycle.js.map +1 -1
- package/dist/logger.js +6 -3
- package/dist/logger.js.map +1 -1
- package/dist/outbound-handlers.d.ts +5 -0
- package/dist/outbound-handlers.js +68 -16
- package/dist/outbound-handlers.js.map +1 -1
- package/dist/statusline-watcher.d.ts +1 -1
- package/dist/statusline-watcher.js +3 -2
- package/dist/statusline-watcher.js.map +1 -1
- package/dist/tmux-manager.d.ts +2 -0
- package/dist/tmux-manager.js +9 -0
- package/dist/tmux-manager.js.map +1 -1
- package/dist/topic-archiver.d.ts +1 -1
- package/dist/topic-commands.d.ts +5 -1
- package/dist/topic-commands.js +72 -12
- package/dist/topic-commands.js.map +1 -1
- package/dist/transcript-monitor.js +2 -0
- package/dist/transcript-monitor.js.map +1 -1
- package/dist/types.d.ts +2 -0
- package/dist/view-api.d.ts +1 -1
- package/dist/web-api.d.ts +2 -1
- package/dist/web-api.js +21 -14
- package/dist/web-api.js.map +1 -1
- package/package.json +1 -1
package/dist/fleet-manager.js
CHANGED
|
@@ -93,6 +93,8 @@ export class FleetManager {
|
|
|
93
93
|
// Topic icon + auto-archive state
|
|
94
94
|
topicIcons = {};
|
|
95
95
|
lastActivity = new Map();
|
|
96
|
+
/** Latest pane-derived execution snapshot reported by each daemon. */
|
|
97
|
+
instanceStateCache = new Map();
|
|
96
98
|
lastInboundUser = new Map(); // instanceName → last username
|
|
97
99
|
// Active "🛑 Cancel" buttons, tracked per button (keyed by messageId) rather
|
|
98
100
|
// than one-per-instance. A button is retired (deleted, with bounded retry) on
|
|
@@ -110,6 +112,8 @@ export class FleetManager {
|
|
|
110
112
|
ipcStoppingInstances = new Set();
|
|
111
113
|
// Adapter restart: prevents re-entrant restart attempts
|
|
112
114
|
adapterRestarting = new Set();
|
|
115
|
+
// Adapter isolation: track state per adapter for retry + visibility
|
|
116
|
+
adapterState = new Map();
|
|
113
117
|
collabInstances = new Set();
|
|
114
118
|
// Health endpoint
|
|
115
119
|
healthServer = null;
|
|
@@ -158,6 +162,7 @@ export class FleetManager {
|
|
|
158
162
|
const instances = Object.keys(this.fleetConfig?.instances ?? {}).map(name => ({
|
|
159
163
|
name,
|
|
160
164
|
status: this.getInstanceStatus(name),
|
|
165
|
+
state: this.getInstanceExecutionState(name),
|
|
161
166
|
ipc: this.instanceIpcClients.has(name),
|
|
162
167
|
costCents: this.costGuard?.getDailyCostCents(name) ?? 0,
|
|
163
168
|
rateLimits: this.statuslineWatcher.getRateLimits(name) ?? null,
|
|
@@ -263,17 +268,24 @@ export class FleetManager {
|
|
|
263
268
|
*/
|
|
264
269
|
bindInstanceAdapter(name, adapterId, fromInbound = false) {
|
|
265
270
|
const cfg = this.fleetConfig?.instances[name];
|
|
266
|
-
if (fromInbound
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
271
|
+
if (fromInbound) {
|
|
272
|
+
// Skip inbound-derived binding for any instance that doesn't have an
|
|
273
|
+
// explicit channel_id — those default to primary adapter deterministically.
|
|
274
|
+
// This prevents a non-deterministic race where whichever adapter delivers
|
|
275
|
+
// first after restart wins the binding.
|
|
276
|
+
if (cfg?.general_topic || cfg?.channel_id)
|
|
277
|
+
return;
|
|
278
|
+
if (cfg && !cfg.channel_id)
|
|
279
|
+
return; // fleet instance without explicit binding → use primary
|
|
280
|
+
// Classic instance: don't override an existing binding (authoritative from /start)
|
|
281
|
+
if (this.classicChannels?.getChannelIdByInstance(name) !== undefined && this.instanceWorldBinding.has(name))
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
274
284
|
this.instanceWorldBinding.set(name, adapterId);
|
|
275
285
|
}
|
|
276
286
|
getInstanceStatus(name) {
|
|
287
|
+
if (this.lifecycle.isPaused(name))
|
|
288
|
+
return "paused";
|
|
277
289
|
const pidPath = join(this.getInstanceDir(name), "daemon.pid");
|
|
278
290
|
if (!existsSync(pidPath))
|
|
279
291
|
return "stopped";
|
|
@@ -286,6 +298,35 @@ export class FleetManager {
|
|
|
286
298
|
return "crashed";
|
|
287
299
|
}
|
|
288
300
|
}
|
|
301
|
+
getInstanceExecutionState(name) {
|
|
302
|
+
if (this.lifecycle.isPaused(name))
|
|
303
|
+
return null;
|
|
304
|
+
return this.instanceStateCache.get(name)?.state ?? null;
|
|
305
|
+
}
|
|
306
|
+
cacheInstanceExecutionState(name, msg) {
|
|
307
|
+
const state = msg.state;
|
|
308
|
+
if (state !== "idle" && state !== "working" && state !== "stuck")
|
|
309
|
+
return;
|
|
310
|
+
const previous = this.instanceStateCache.get(name);
|
|
311
|
+
const now = Date.now();
|
|
312
|
+
const numberOr = (value, fallback) => typeof value === "number" && Number.isFinite(value) ? value : fallback;
|
|
313
|
+
this.instanceStateCache.set(name, {
|
|
314
|
+
state,
|
|
315
|
+
unchangedForMs: numberOr(msg.unchangedForMs, previous?.unchangedForMs ?? 0),
|
|
316
|
+
observedAt: numberOr(msg.observedAt, now),
|
|
317
|
+
stateChangedAt: numberOr(msg.stateChangedAt, previous?.state === state ? previous.stateChangedAt : now),
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
/** Single delivery facade: wake an auto-paused CLI before sending to daemon IPC. */
|
|
321
|
+
async deliverToInstance(instanceName, payload) {
|
|
322
|
+
if (this.lifecycle.isPaused(instanceName)) {
|
|
323
|
+
await this.lifecycle.wake(instanceName, 30_000);
|
|
324
|
+
}
|
|
325
|
+
const ipc = this.instanceIpcClients.get(instanceName);
|
|
326
|
+
if (!ipc?.connected)
|
|
327
|
+
throw new Error(`Instance '${instanceName}' IPC is unavailable`);
|
|
328
|
+
ipc.send(payload);
|
|
329
|
+
}
|
|
289
330
|
async startInstance(name, config, topicMode) {
|
|
290
331
|
if (config.general_topic) {
|
|
291
332
|
this.ensureGeneralInstructions(config.working_directory, config.backend);
|
|
@@ -353,6 +394,7 @@ export class FleetManager {
|
|
|
353
394
|
}
|
|
354
395
|
async stopInstance(name) {
|
|
355
396
|
this.failoverActive.delete(name);
|
|
397
|
+
this.instanceStateCache.delete(name);
|
|
356
398
|
return this.lifecycle.stop(name);
|
|
357
399
|
}
|
|
358
400
|
/** Restart a single instance, reloading fleet.yaml first to pick up config changes. */
|
|
@@ -836,17 +878,81 @@ export class FleetManager {
|
|
|
836
878
|
const channelConfigs = fleet.channels ?? (fleet.channel ? [fleet.channel] : []);
|
|
837
879
|
if (channelConfigs.length === 0)
|
|
838
880
|
return;
|
|
839
|
-
// Start
|
|
840
|
-
await
|
|
841
|
-
|
|
842
|
-
|
|
843
|
-
//
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
881
|
+
// Start ALL adapters in parallel — any single failure doesn't block others.
|
|
882
|
+
const results = await Promise.allSettled(channelConfigs.map((cfg, i) => i === 0
|
|
883
|
+
? this.startSingleAdapter(fleet, cfg)
|
|
884
|
+
: this.startAdditionalAdapter(cfg)));
|
|
885
|
+
// Track state + schedule background retry for failures.
|
|
886
|
+
for (let i = 0; i < channelConfigs.length; i++) {
|
|
887
|
+
const adapterId = channelConfigs[i].id ?? channelConfigs[i].type;
|
|
888
|
+
if (results[i].status === "fulfilled") {
|
|
889
|
+
this.adapterState.set(adapterId, { status: "connected", retryCount: 0 });
|
|
890
|
+
}
|
|
891
|
+
else {
|
|
892
|
+
const err = results[i].reason;
|
|
893
|
+
this.logger.error({ adapterId, err: err?.message ?? err }, "Adapter startup failed — scheduling background retry");
|
|
894
|
+
this.adapterState.set(adapterId, { status: "retrying", retryCount: 0, lastError: err?.message ?? String(err) });
|
|
895
|
+
this.scheduleAdapterRetry(adapterId, channelConfigs[i], i === 0 ? fleet : undefined);
|
|
896
|
+
// Notify admin via whichever adapter is already up
|
|
897
|
+
this.notifyAdapterFailure(adapterId, err?.message ?? String(err));
|
|
898
|
+
}
|
|
848
899
|
}
|
|
849
900
|
}
|
|
901
|
+
/** Exponential backoff retry for a single failed adapter (background, non-blocking). */
|
|
902
|
+
scheduleAdapterRetry(adapterId, channelConfig, fleet) {
|
|
903
|
+
const MAX_RETRIES = 10;
|
|
904
|
+
const INITIAL_DELAY_MS = 5_000;
|
|
905
|
+
const MAX_DELAY_MS = 5 * 60_000;
|
|
906
|
+
const state = this.adapterState.get(adapterId);
|
|
907
|
+
if (!state || state.retryCount >= MAX_RETRIES) {
|
|
908
|
+
if (state) {
|
|
909
|
+
state.status = "failed";
|
|
910
|
+
this.logger.error({ adapterId, retries: state.retryCount }, "Adapter retry exhausted — giving up");
|
|
911
|
+
this.notifyAdapterFailure(adapterId, `Retry exhausted after ${state.retryCount} attempts. Check token/network and restart fleet.`);
|
|
912
|
+
}
|
|
913
|
+
return;
|
|
914
|
+
}
|
|
915
|
+
const delay = Math.min(INITIAL_DELAY_MS * Math.pow(2, state.retryCount), MAX_DELAY_MS);
|
|
916
|
+
this.logger.info({ adapterId, attempt: state.retryCount + 1, delay_ms: delay }, "Scheduling adapter retry");
|
|
917
|
+
state.retryTimer = setTimeout(async () => {
|
|
918
|
+
state.retryCount++;
|
|
919
|
+
try {
|
|
920
|
+
if (fleet) {
|
|
921
|
+
await this.startSingleAdapter(fleet, channelConfig);
|
|
922
|
+
}
|
|
923
|
+
else {
|
|
924
|
+
await this.startAdditionalAdapter(channelConfig);
|
|
925
|
+
}
|
|
926
|
+
state.status = "connected";
|
|
927
|
+
state.lastError = undefined;
|
|
928
|
+
this.logger.info({ adapterId, attempts: state.retryCount }, "Adapter reconnected on retry");
|
|
929
|
+
this.notifyAdapterRecovery(adapterId, state.retryCount);
|
|
930
|
+
}
|
|
931
|
+
catch (err) {
|
|
932
|
+
state.lastError = err?.message ?? String(err);
|
|
933
|
+
this.logger.warn({ adapterId, attempt: state.retryCount, err: state.lastError }, "Adapter retry failed");
|
|
934
|
+
this.scheduleAdapterRetry(adapterId, channelConfig, fleet);
|
|
935
|
+
}
|
|
936
|
+
}, delay);
|
|
937
|
+
}
|
|
938
|
+
/** Notify admin about adapter failure (uses any available adapter). */
|
|
939
|
+
notifyAdapterFailure(adapterId, error) {
|
|
940
|
+
const generalId = this.findGeneralInstance();
|
|
941
|
+
if (generalId) {
|
|
942
|
+
this.notifyInstanceTopic(generalId, `⚠️ Adapter "${adapterId}" failed to start: ${error}\nRetrying in background. Other adapters unaffected.`);
|
|
943
|
+
}
|
|
944
|
+
}
|
|
945
|
+
/** Notify admin that a retried adapter reconnected. */
|
|
946
|
+
notifyAdapterRecovery(adapterId, attempts) {
|
|
947
|
+
const generalId = this.findGeneralInstance();
|
|
948
|
+
if (generalId) {
|
|
949
|
+
this.notifyInstanceTopic(generalId, `✅ Adapter "${adapterId}" reconnected (after ${attempts} ${attempts === 1 ? "retry" : "retries"}).`);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
/** Get adapter states for /status visibility. */
|
|
953
|
+
getAdapterStates() {
|
|
954
|
+
return this.adapterState;
|
|
955
|
+
}
|
|
850
956
|
/** Start the primary adapter (backward-compatible, sets this.adapter) */
|
|
851
957
|
async startSingleAdapter(fleet, channelConfig) {
|
|
852
958
|
const botToken = process.env[channelConfig.bot_token_env];
|
|
@@ -1454,10 +1560,16 @@ export class FleetManager {
|
|
|
1454
1560
|
else if (msg.type === "fleet_set_description") {
|
|
1455
1561
|
this.handleSetDescription(name, msg);
|
|
1456
1562
|
}
|
|
1563
|
+
else if (msg.type === "instance_state" || msg.type === "instance_state_response") {
|
|
1564
|
+
this.cacheInstanceExecutionState(name, msg);
|
|
1565
|
+
}
|
|
1457
1566
|
}, this.logger, `ipc.message[${name}]`));
|
|
1458
1567
|
// Ask daemon for any sessions that registered before we connected
|
|
1459
1568
|
// (fixes race condition where mcp_ready was broadcast before fleet manager connected)
|
|
1460
1569
|
ipc.send({ type: "query_sessions" });
|
|
1570
|
+
// The initial state transition may have happened before FleetManager
|
|
1571
|
+
// connected, so seed the cache instead of waiting for another transition.
|
|
1572
|
+
ipc.send({ type: "query_instance_state", requestId: `fleet-state-${Date.now()}` });
|
|
1461
1573
|
this.logger.debug({ name }, "Connected to instance IPC");
|
|
1462
1574
|
if (!this.statuslineWatcher.has(name)) {
|
|
1463
1575
|
this.statuslineWatcher.watch(name);
|
|
@@ -1860,9 +1972,8 @@ export class FleetManager {
|
|
|
1860
1972
|
}
|
|
1861
1973
|
this.warnIfRateLimited(generalInstance, msg);
|
|
1862
1974
|
const { text, extraMeta } = await processAttachments(msg, inboundAdapter, this.logger, generalInstance);
|
|
1863
|
-
|
|
1864
|
-
|
|
1865
|
-
ipc.send({
|
|
1975
|
+
try {
|
|
1976
|
+
await this.deliverToInstance(generalInstance, {
|
|
1866
1977
|
type: "fleet_inbound",
|
|
1867
1978
|
content: text,
|
|
1868
1979
|
targetSession: generalInstance,
|
|
@@ -1888,6 +1999,9 @@ export class FleetManager {
|
|
|
1888
1999
|
this.trackInboundMsg(generalInstance, msg);
|
|
1889
2000
|
void this.sendCancelButton(generalInstance);
|
|
1890
2001
|
}
|
|
2002
|
+
catch (err) {
|
|
2003
|
+
this.logger.warn({ err: err.message, instanceName: generalInstance }, "General wake/delivery failed");
|
|
2004
|
+
}
|
|
1891
2005
|
}
|
|
1892
2006
|
return;
|
|
1893
2007
|
}
|
|
@@ -1950,27 +2064,32 @@ export class FleetManager {
|
|
|
1950
2064
|
this.setTopicIcon(instanceName, "blue");
|
|
1951
2065
|
this.warnIfRateLimited(instanceName, msg);
|
|
1952
2066
|
const { text, extraMeta } = await processAttachments(msg, inboundAdapter, this.logger, instanceName);
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
2067
|
+
try {
|
|
2068
|
+
await this.deliverToInstance(instanceName, {
|
|
2069
|
+
type: "fleet_inbound",
|
|
2070
|
+
content: text,
|
|
2071
|
+
targetSession: instanceName, // Channel messages → instance's own session
|
|
2072
|
+
meta: {
|
|
2073
|
+
chat_id: msg.chatId,
|
|
2074
|
+
message_id: msg.messageId,
|
|
2075
|
+
user: msg.username,
|
|
2076
|
+
user_id: msg.userId,
|
|
2077
|
+
ts: msg.timestamp.toISOString(),
|
|
2078
|
+
thread_id: msg.threadId ?? "",
|
|
2079
|
+
source: msg.source,
|
|
2080
|
+
...(msg.replyToText ? { reply_to_text: msg.replyToText } : {}),
|
|
2081
|
+
...extraMeta,
|
|
2082
|
+
},
|
|
2083
|
+
});
|
|
2084
|
+
}
|
|
2085
|
+
catch (err) {
|
|
2086
|
+
this.logger.warn({ err: err.message, instanceName }, "Wake/delivery failed");
|
|
2087
|
+
if (msg.chatId && msg.messageId) {
|
|
2088
|
+
const reactAdapter = this.getAdapterForInstance(instanceName) ?? inboundAdapter;
|
|
2089
|
+
reactAdapter.react(this.reactTarget(msg), msg.messageId, "❌").catch(() => { });
|
|
2090
|
+
}
|
|
1956
2091
|
return;
|
|
1957
2092
|
}
|
|
1958
|
-
ipc.send({
|
|
1959
|
-
type: "fleet_inbound",
|
|
1960
|
-
content: text,
|
|
1961
|
-
targetSession: instanceName, // Channel messages → instance's own session
|
|
1962
|
-
meta: {
|
|
1963
|
-
chat_id: msg.chatId,
|
|
1964
|
-
message_id: msg.messageId,
|
|
1965
|
-
user: msg.username,
|
|
1966
|
-
user_id: msg.userId,
|
|
1967
|
-
ts: msg.timestamp.toISOString(),
|
|
1968
|
-
thread_id: msg.threadId ?? "",
|
|
1969
|
-
source: msg.source,
|
|
1970
|
-
...(msg.replyToText ? { reply_to_text: msg.replyToText } : {}),
|
|
1971
|
-
...extraMeta,
|
|
1972
|
-
},
|
|
1973
|
-
});
|
|
1974
2093
|
this.lastInboundUser.set(instanceName, msg.username);
|
|
1975
2094
|
this.logger.info(`${msg.username} → ${instanceName}: ${(text ?? "").slice(0, 100)}`);
|
|
1976
2095
|
this.eventLog?.logActivity("message", msg.username, (text ?? "").slice(0, 200), instanceName);
|
|
@@ -2131,20 +2250,23 @@ export class FleetManager {
|
|
|
2131
2250
|
const schedulerDefaults = this.fleetConfig?.defaults.scheduler;
|
|
2132
2251
|
const retryCount = schedulerDefaults?.retry_count ?? 3;
|
|
2133
2252
|
const retryInterval = schedulerDefaults?.retry_interval_ms ?? 30_000;
|
|
2134
|
-
const deliver = () => {
|
|
2135
|
-
|
|
2136
|
-
|
|
2253
|
+
const deliver = async () => {
|
|
2254
|
+
try {
|
|
2255
|
+
await this.deliverToInstance(target, {
|
|
2256
|
+
type: "fleet_schedule_trigger",
|
|
2257
|
+
payload: { schedule_id: id, message: `[Scheduled] ${message}`, label },
|
|
2258
|
+
meta: { chat_id: reply_chat_id, thread_id: reply_thread_id, user: "scheduler" },
|
|
2259
|
+
});
|
|
2260
|
+
// A scheduled trigger also puts the instance to work — show a cancel button.
|
|
2261
|
+
void this.sendCancelButton(target);
|
|
2262
|
+
return true;
|
|
2263
|
+
}
|
|
2264
|
+
catch (err) {
|
|
2265
|
+
this.logger.warn({ err: err.message, target }, "Scheduled wake/delivery attempt failed");
|
|
2137
2266
|
return false;
|
|
2138
|
-
|
|
2139
|
-
type: "fleet_schedule_trigger",
|
|
2140
|
-
payload: { schedule_id: id, message: `[Scheduled] ${message}`, label },
|
|
2141
|
-
meta: { chat_id: reply_chat_id, thread_id: reply_thread_id, user: "scheduler" },
|
|
2142
|
-
});
|
|
2143
|
-
// A scheduled trigger also puts the instance to work — show a cancel button.
|
|
2144
|
-
void this.sendCancelButton(target);
|
|
2145
|
-
return true;
|
|
2267
|
+
}
|
|
2146
2268
|
};
|
|
2147
|
-
if (deliver()) {
|
|
2269
|
+
if (await deliver()) {
|
|
2148
2270
|
this.scheduler.recordRun(id, "delivered");
|
|
2149
2271
|
if (source !== target)
|
|
2150
2272
|
this.notifySourceTopic(schedule);
|
|
@@ -2152,7 +2274,7 @@ export class FleetManager {
|
|
|
2152
2274
|
}
|
|
2153
2275
|
for (let i = 0; i < retryCount; i++) {
|
|
2154
2276
|
await new Promise((r) => setTimeout(r, retryInterval));
|
|
2155
|
-
if (deliver()) {
|
|
2277
|
+
if (await deliver()) {
|
|
2156
2278
|
this.scheduler.recordRun(id, "delivered");
|
|
2157
2279
|
if (source !== target)
|
|
2158
2280
|
this.notifySourceTopic(schedule);
|
|
@@ -2685,6 +2807,10 @@ export class FleetManager {
|
|
|
2685
2807
|
startStatuslineWatcher(name) {
|
|
2686
2808
|
this.statuslineWatcher.watch(name);
|
|
2687
2809
|
}
|
|
2810
|
+
stopStatuslineWatcher(name) {
|
|
2811
|
+
// Pausing stops I/O but retains the last observed limits for status views.
|
|
2812
|
+
this.statuslineWatcher.unwatch(name, true);
|
|
2813
|
+
}
|
|
2688
2814
|
reactMessageStatus(instanceName, chatId, messageId, emoji) {
|
|
2689
2815
|
// React via the adapter BOUND to this instance — NOT the first discord world.
|
|
2690
2816
|
// Otherwise, in a same-channel/same-guild multi-bot setup, the inbound 👀
|
|
@@ -3035,7 +3161,7 @@ export class FleetManager {
|
|
|
3035
3161
|
return [];
|
|
3036
3162
|
}
|
|
3037
3163
|
}
|
|
3038
|
-
async sendHangNotification(instanceName) {
|
|
3164
|
+
async sendHangNotification(instanceName, unchangedForMs) {
|
|
3039
3165
|
const adapter = this.getAdapterForInstance(instanceName) ?? this.adapter;
|
|
3040
3166
|
if (!adapter)
|
|
3041
3167
|
return;
|
|
@@ -3044,11 +3170,18 @@ export class FleetManager {
|
|
|
3044
3170
|
if (!groupId)
|
|
3045
3171
|
return;
|
|
3046
3172
|
const threadId = this.fleetConfig?.instances[instanceName]?.topic_id;
|
|
3173
|
+
const instanceHangConfig = this.fleetConfig?.instances[instanceName]?.hang_detector;
|
|
3174
|
+
const configuredMinutes = instanceHangConfig?.timeout_minutes
|
|
3175
|
+
?? this.fleetConfig?.defaults?.hang_detector?.timeout_minutes
|
|
3176
|
+
?? 15;
|
|
3177
|
+
const unchangedMinutes = unchangedForMs == null
|
|
3178
|
+
? configuredMinutes
|
|
3179
|
+
: Math.max(1, Math.floor(unchangedForMs / 60_000));
|
|
3047
3180
|
this.setTopicIcon(instanceName, "red");
|
|
3048
3181
|
await adapter.notifyAlert(String(groupId), {
|
|
3049
3182
|
type: "hang",
|
|
3050
3183
|
instanceName,
|
|
3051
|
-
message: `⚠️ ${instanceName}
|
|
3184
|
+
message: `⚠️ ${instanceName} may be stuck — pane unchanged for ${unchangedMinutes}min, ready prompt not recognized`,
|
|
3052
3185
|
choices: [
|
|
3053
3186
|
{ id: `hang:restart:${instanceName}`, label: "🔄 Force restart" },
|
|
3054
3187
|
{ id: `hang:wait:${instanceName}`, label: "⏳ Keep waiting" },
|
|
@@ -3549,11 +3682,6 @@ When users create specialized instances, suggest these configurations:
|
|
|
3549
3682
|
const fullText = logContext
|
|
3550
3683
|
? `[Chat log for context]\n${logContext}\n\n[User message]\n${text}`
|
|
3551
3684
|
: text;
|
|
3552
|
-
const ipc = this.instanceIpcClients.get(instanceName);
|
|
3553
|
-
if (!ipc) {
|
|
3554
|
-
this.logger.warn({ instanceName }, "Classic channel instance IPC not connected");
|
|
3555
|
-
return;
|
|
3556
|
-
}
|
|
3557
3685
|
const meta = {
|
|
3558
3686
|
chat_id: msg.chatId,
|
|
3559
3687
|
message_id: msg.messageId,
|
|
@@ -3575,12 +3703,18 @@ When users create specialized instances, suggest these configurations:
|
|
|
3575
3703
|
meta.image_path = saves[saves.length - 1][1].split(",")[0].trim();
|
|
3576
3704
|
}
|
|
3577
3705
|
}
|
|
3578
|
-
|
|
3579
|
-
|
|
3580
|
-
|
|
3581
|
-
|
|
3582
|
-
|
|
3583
|
-
|
|
3706
|
+
try {
|
|
3707
|
+
await this.deliverToInstance(instanceName, {
|
|
3708
|
+
type: "fleet_inbound",
|
|
3709
|
+
content: fullText,
|
|
3710
|
+
targetSession: instanceName,
|
|
3711
|
+
meta,
|
|
3712
|
+
});
|
|
3713
|
+
}
|
|
3714
|
+
catch (err) {
|
|
3715
|
+
this.logger.warn({ err: err.message, instanceName }, "Classic wake/delivery failed");
|
|
3716
|
+
return;
|
|
3717
|
+
}
|
|
3584
3718
|
this.lastInboundUser.set(instanceName, msg.username);
|
|
3585
3719
|
this.logger.info(`${msg.username} → ${instanceName} (classic): ${text.slice(0, 100)}`);
|
|
3586
3720
|
this.trackInboundMsg(instanceName, msg);
|
|
@@ -3683,6 +3817,13 @@ When users create specialized instances, suggest these configurations:
|
|
|
3683
3817
|
clearInterval(this.watchdogTimer);
|
|
3684
3818
|
this.watchdogTimer = null;
|
|
3685
3819
|
}
|
|
3820
|
+
// Cancel adapter retry timers
|
|
3821
|
+
for (const state of this.adapterState.values()) {
|
|
3822
|
+
if (state.retryTimer) {
|
|
3823
|
+
clearTimeout(state.retryTimer);
|
|
3824
|
+
state.retryTimer = undefined;
|
|
3825
|
+
}
|
|
3826
|
+
}
|
|
3686
3827
|
this.clearStatuslineWatchers();
|
|
3687
3828
|
this.costGuard?.stop();
|
|
3688
3829
|
this.dailySummary?.stop();
|
|
@@ -4253,6 +4394,7 @@ When users create specialized instances, suggest these configurations:
|
|
|
4253
4394
|
lastActivity: this.lastActivityMs(inst.name) || null,
|
|
4254
4395
|
currentTask,
|
|
4255
4396
|
idle: this.getInstanceIdle(inst.name),
|
|
4397
|
+
state: this.getInstanceExecutionState(inst.name),
|
|
4256
4398
|
};
|
|
4257
4399
|
});
|
|
4258
4400
|
res.setHeader("Access-Control-Allow-Origin", "*");
|