@songsid/agend 2.1.0-beta.1 → 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/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 +50 -3
- package/dist/daemon.js +356 -31
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-context.d.ts +2 -1
- package/dist/fleet-manager.d.ts +10 -2
- package/dist/fleet-manager.js +125 -56
- 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 +60 -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
|
|
@@ -160,6 +162,7 @@ export class FleetManager {
|
|
|
160
162
|
const instances = Object.keys(this.fleetConfig?.instances ?? {}).map(name => ({
|
|
161
163
|
name,
|
|
162
164
|
status: this.getInstanceStatus(name),
|
|
165
|
+
state: this.getInstanceExecutionState(name),
|
|
163
166
|
ipc: this.instanceIpcClients.has(name),
|
|
164
167
|
costCents: this.costGuard?.getDailyCostCents(name) ?? 0,
|
|
165
168
|
rateLimits: this.statuslineWatcher.getRateLimits(name) ?? null,
|
|
@@ -265,17 +268,24 @@ export class FleetManager {
|
|
|
265
268
|
*/
|
|
266
269
|
bindInstanceAdapter(name, adapterId, fromInbound = false) {
|
|
267
270
|
const cfg = this.fleetConfig?.instances[name];
|
|
268
|
-
if (fromInbound
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
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
|
+
}
|
|
276
284
|
this.instanceWorldBinding.set(name, adapterId);
|
|
277
285
|
}
|
|
278
286
|
getInstanceStatus(name) {
|
|
287
|
+
if (this.lifecycle.isPaused(name))
|
|
288
|
+
return "paused";
|
|
279
289
|
const pidPath = join(this.getInstanceDir(name), "daemon.pid");
|
|
280
290
|
if (!existsSync(pidPath))
|
|
281
291
|
return "stopped";
|
|
@@ -288,6 +298,35 @@ export class FleetManager {
|
|
|
288
298
|
return "crashed";
|
|
289
299
|
}
|
|
290
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
|
+
}
|
|
291
330
|
async startInstance(name, config, topicMode) {
|
|
292
331
|
if (config.general_topic) {
|
|
293
332
|
this.ensureGeneralInstructions(config.working_directory, config.backend);
|
|
@@ -355,6 +394,7 @@ export class FleetManager {
|
|
|
355
394
|
}
|
|
356
395
|
async stopInstance(name) {
|
|
357
396
|
this.failoverActive.delete(name);
|
|
397
|
+
this.instanceStateCache.delete(name);
|
|
358
398
|
return this.lifecycle.stop(name);
|
|
359
399
|
}
|
|
360
400
|
/** Restart a single instance, reloading fleet.yaml first to pick up config changes. */
|
|
@@ -1520,10 +1560,16 @@ export class FleetManager {
|
|
|
1520
1560
|
else if (msg.type === "fleet_set_description") {
|
|
1521
1561
|
this.handleSetDescription(name, msg);
|
|
1522
1562
|
}
|
|
1563
|
+
else if (msg.type === "instance_state" || msg.type === "instance_state_response") {
|
|
1564
|
+
this.cacheInstanceExecutionState(name, msg);
|
|
1565
|
+
}
|
|
1523
1566
|
}, this.logger, `ipc.message[${name}]`));
|
|
1524
1567
|
// Ask daemon for any sessions that registered before we connected
|
|
1525
1568
|
// (fixes race condition where mcp_ready was broadcast before fleet manager connected)
|
|
1526
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()}` });
|
|
1527
1573
|
this.logger.debug({ name }, "Connected to instance IPC");
|
|
1528
1574
|
if (!this.statuslineWatcher.has(name)) {
|
|
1529
1575
|
this.statuslineWatcher.watch(name);
|
|
@@ -1926,9 +1972,8 @@ export class FleetManager {
|
|
|
1926
1972
|
}
|
|
1927
1973
|
this.warnIfRateLimited(generalInstance, msg);
|
|
1928
1974
|
const { text, extraMeta } = await processAttachments(msg, inboundAdapter, this.logger, generalInstance);
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
ipc.send({
|
|
1975
|
+
try {
|
|
1976
|
+
await this.deliverToInstance(generalInstance, {
|
|
1932
1977
|
type: "fleet_inbound",
|
|
1933
1978
|
content: text,
|
|
1934
1979
|
targetSession: generalInstance,
|
|
@@ -1954,6 +1999,9 @@ export class FleetManager {
|
|
|
1954
1999
|
this.trackInboundMsg(generalInstance, msg);
|
|
1955
2000
|
void this.sendCancelButton(generalInstance);
|
|
1956
2001
|
}
|
|
2002
|
+
catch (err) {
|
|
2003
|
+
this.logger.warn({ err: err.message, instanceName: generalInstance }, "General wake/delivery failed");
|
|
2004
|
+
}
|
|
1957
2005
|
}
|
|
1958
2006
|
return;
|
|
1959
2007
|
}
|
|
@@ -2016,27 +2064,32 @@ export class FleetManager {
|
|
|
2016
2064
|
this.setTopicIcon(instanceName, "blue");
|
|
2017
2065
|
this.warnIfRateLimited(instanceName, msg);
|
|
2018
2066
|
const { text, extraMeta } = await processAttachments(msg, inboundAdapter, this.logger, instanceName);
|
|
2019
|
-
|
|
2020
|
-
|
|
2021
|
-
|
|
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
|
+
}
|
|
2022
2091
|
return;
|
|
2023
2092
|
}
|
|
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
|
-
});
|
|
2040
2093
|
this.lastInboundUser.set(instanceName, msg.username);
|
|
2041
2094
|
this.logger.info(`${msg.username} → ${instanceName}: ${(text ?? "").slice(0, 100)}`);
|
|
2042
2095
|
this.eventLog?.logActivity("message", msg.username, (text ?? "").slice(0, 200), instanceName);
|
|
@@ -2197,20 +2250,23 @@ export class FleetManager {
|
|
|
2197
2250
|
const schedulerDefaults = this.fleetConfig?.defaults.scheduler;
|
|
2198
2251
|
const retryCount = schedulerDefaults?.retry_count ?? 3;
|
|
2199
2252
|
const retryInterval = schedulerDefaults?.retry_interval_ms ?? 30_000;
|
|
2200
|
-
const deliver = () => {
|
|
2201
|
-
|
|
2202
|
-
|
|
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");
|
|
2203
2266
|
return false;
|
|
2204
|
-
|
|
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;
|
|
2267
|
+
}
|
|
2212
2268
|
};
|
|
2213
|
-
if (deliver()) {
|
|
2269
|
+
if (await deliver()) {
|
|
2214
2270
|
this.scheduler.recordRun(id, "delivered");
|
|
2215
2271
|
if (source !== target)
|
|
2216
2272
|
this.notifySourceTopic(schedule);
|
|
@@ -2218,7 +2274,7 @@ export class FleetManager {
|
|
|
2218
2274
|
}
|
|
2219
2275
|
for (let i = 0; i < retryCount; i++) {
|
|
2220
2276
|
await new Promise((r) => setTimeout(r, retryInterval));
|
|
2221
|
-
if (deliver()) {
|
|
2277
|
+
if (await deliver()) {
|
|
2222
2278
|
this.scheduler.recordRun(id, "delivered");
|
|
2223
2279
|
if (source !== target)
|
|
2224
2280
|
this.notifySourceTopic(schedule);
|
|
@@ -2751,6 +2807,10 @@ export class FleetManager {
|
|
|
2751
2807
|
startStatuslineWatcher(name) {
|
|
2752
2808
|
this.statuslineWatcher.watch(name);
|
|
2753
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
|
+
}
|
|
2754
2814
|
reactMessageStatus(instanceName, chatId, messageId, emoji) {
|
|
2755
2815
|
// React via the adapter BOUND to this instance — NOT the first discord world.
|
|
2756
2816
|
// Otherwise, in a same-channel/same-guild multi-bot setup, the inbound 👀
|
|
@@ -3101,7 +3161,7 @@ export class FleetManager {
|
|
|
3101
3161
|
return [];
|
|
3102
3162
|
}
|
|
3103
3163
|
}
|
|
3104
|
-
async sendHangNotification(instanceName) {
|
|
3164
|
+
async sendHangNotification(instanceName, unchangedForMs) {
|
|
3105
3165
|
const adapter = this.getAdapterForInstance(instanceName) ?? this.adapter;
|
|
3106
3166
|
if (!adapter)
|
|
3107
3167
|
return;
|
|
@@ -3110,11 +3170,18 @@ export class FleetManager {
|
|
|
3110
3170
|
if (!groupId)
|
|
3111
3171
|
return;
|
|
3112
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));
|
|
3113
3180
|
this.setTopicIcon(instanceName, "red");
|
|
3114
3181
|
await adapter.notifyAlert(String(groupId), {
|
|
3115
3182
|
type: "hang",
|
|
3116
3183
|
instanceName,
|
|
3117
|
-
message: `⚠️ ${instanceName}
|
|
3184
|
+
message: `⚠️ ${instanceName} may be stuck — pane unchanged for ${unchangedMinutes}min, ready prompt not recognized`,
|
|
3118
3185
|
choices: [
|
|
3119
3186
|
{ id: `hang:restart:${instanceName}`, label: "🔄 Force restart" },
|
|
3120
3187
|
{ id: `hang:wait:${instanceName}`, label: "⏳ Keep waiting" },
|
|
@@ -3615,11 +3682,6 @@ When users create specialized instances, suggest these configurations:
|
|
|
3615
3682
|
const fullText = logContext
|
|
3616
3683
|
? `[Chat log for context]\n${logContext}\n\n[User message]\n${text}`
|
|
3617
3684
|
: 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
|
-
}
|
|
3623
3685
|
const meta = {
|
|
3624
3686
|
chat_id: msg.chatId,
|
|
3625
3687
|
message_id: msg.messageId,
|
|
@@ -3641,12 +3703,18 @@ When users create specialized instances, suggest these configurations:
|
|
|
3641
3703
|
meta.image_path = saves[saves.length - 1][1].split(",")[0].trim();
|
|
3642
3704
|
}
|
|
3643
3705
|
}
|
|
3644
|
-
|
|
3645
|
-
|
|
3646
|
-
|
|
3647
|
-
|
|
3648
|
-
|
|
3649
|
-
|
|
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
|
+
}
|
|
3650
3718
|
this.lastInboundUser.set(instanceName, msg.username);
|
|
3651
3719
|
this.logger.info(`${msg.username} → ${instanceName} (classic): ${text.slice(0, 100)}`);
|
|
3652
3720
|
this.trackInboundMsg(instanceName, msg);
|
|
@@ -4326,6 +4394,7 @@ When users create specialized instances, suggest these configurations:
|
|
|
4326
4394
|
lastActivity: this.lastActivityMs(inst.name) || null,
|
|
4327
4395
|
currentTask,
|
|
4328
4396
|
idle: this.getInstanceIdle(inst.name),
|
|
4397
|
+
state: this.getInstanceExecutionState(inst.name),
|
|
4329
4398
|
};
|
|
4330
4399
|
});
|
|
4331
4400
|
res.setHeader("Access-Control-Allow-Origin", "*");
|