@suzuke/agend 1.1.1 → 1.2.0
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/access-path.d.ts +1 -1
- package/dist/access-path.js +2 -2
- package/dist/access-path.js.map +1 -1
- package/dist/backend/claude-code.d.ts +1 -0
- package/dist/backend/claude-code.js +6 -1
- package/dist/backend/claude-code.js.map +1 -1
- package/dist/backend/codex.d.ts +1 -0
- package/dist/backend/codex.js +16 -6
- package/dist/backend/codex.js.map +1 -1
- package/dist/backend/gemini-cli.d.ts +1 -0
- package/dist/backend/gemini-cli.js +13 -1
- package/dist/backend/gemini-cli.js.map +1 -1
- package/dist/backend/opencode.d.ts +1 -0
- package/dist/backend/opencode.js +12 -4
- package/dist/backend/opencode.js.map +1 -1
- package/dist/backend/types.d.ts +2 -0
- package/dist/backend/types.js.map +1 -1
- package/dist/channel/access-manager.d.ts +5 -5
- package/dist/channel/access-manager.js.map +1 -1
- package/dist/channel/adapters/discord.d.ts +3 -2
- package/dist/channel/adapters/discord.js +11 -4
- package/dist/channel/adapters/discord.js.map +1 -1
- package/dist/channel/adapters/telegram.d.ts +4 -3
- package/dist/channel/adapters/telegram.js +11 -5
- package/dist/channel/adapters/telegram.js.map +1 -1
- package/dist/channel/ipc-bridge.js +30 -3
- package/dist/channel/ipc-bridge.js.map +1 -1
- package/dist/channel/types.d.ts +8 -7
- package/dist/cli.js +3 -3
- package/dist/cli.js.map +1 -1
- package/dist/daemon.js +15 -8
- package/dist/daemon.js.map +1 -1
- package/dist/fleet-context.d.ts +2 -2
- package/dist/fleet-manager.d.ts +3 -3
- package/dist/fleet-manager.js +59 -78
- package/dist/fleet-manager.js.map +1 -1
- package/dist/safe-async.d.ts +6 -0
- package/dist/safe-async.js +20 -0
- package/dist/safe-async.js.map +1 -0
- package/dist/setup-wizard.js +3 -3
- package/dist/setup-wizard.js.map +1 -1
- package/dist/tmux-manager.js +1 -1
- package/dist/tmux-manager.js.map +1 -1
- package/dist/topic-commands.d.ts +2 -2
- package/dist/topic-commands.js +1 -1
- package/dist/topic-commands.js.map +1 -1
- package/dist/types.d.ts +2 -2
- package/package.json +1 -1
package/dist/fleet-manager.d.ts
CHANGED
|
@@ -14,7 +14,7 @@ export declare class FleetManager implements FleetContext {
|
|
|
14
14
|
private daemons;
|
|
15
15
|
fleetConfig: FleetConfig | null;
|
|
16
16
|
adapter: ChannelAdapter | null;
|
|
17
|
-
routingTable: Map<
|
|
17
|
+
routingTable: Map<string, RouteTarget>;
|
|
18
18
|
instanceIpcClients: Map<string, IpcClient>;
|
|
19
19
|
scheduler: Scheduler | null;
|
|
20
20
|
private configPath;
|
|
@@ -40,7 +40,7 @@ export declare class FleetManager implements FleetContext {
|
|
|
40
40
|
/** Load fleet.yaml and build routing table */
|
|
41
41
|
loadConfig(configPath: string): FleetConfig;
|
|
42
42
|
/** Build topic routing table: { topicId -> RouteTarget } */
|
|
43
|
-
buildRoutingTable(): Map<
|
|
43
|
+
buildRoutingTable(): Map<string, RouteTarget>;
|
|
44
44
|
getInstanceDir(name: string): string;
|
|
45
45
|
getInstanceStatus(name: string): "running" | "stopped" | "crashed";
|
|
46
46
|
startInstance(name: string, config: InstanceConfig, topicMode: boolean): Promise<void>;
|
|
@@ -69,7 +69,7 @@ export declare class FleetManager implements FleetContext {
|
|
|
69
69
|
private notifyScheduleFailure;
|
|
70
70
|
private handleScheduleCrud;
|
|
71
71
|
/** Create a forum topic via the adapter. Returns the message_thread_id. */
|
|
72
|
-
createForumTopic(topicName: string): Promise<number>;
|
|
72
|
+
createForumTopic(topicName: string): Promise<number | string>;
|
|
73
73
|
private deleteForumTopic;
|
|
74
74
|
private topicCleanupTimer;
|
|
75
75
|
private sessionPruneTimer;
|
package/dist/fleet-manager.js
CHANGED
|
@@ -24,6 +24,7 @@ import { TopicCommands, sanitizeInstanceName } from "./topic-commands.js";
|
|
|
24
24
|
import { DailySummary } from "./daily-summary.js";
|
|
25
25
|
import { WebhookEmitter } from "./webhook-emitter.js";
|
|
26
26
|
import { TmuxControlClient } from "./tmux-control.js";
|
|
27
|
+
import { safeHandler } from "./safe-async.js";
|
|
27
28
|
const TMUX_SESSION = "agend";
|
|
28
29
|
export function resolveReplyThreadId(argsThreadId, instanceConfig) {
|
|
29
30
|
if (typeof argsThreadId === "string" && argsThreadId.length > 0) {
|
|
@@ -82,7 +83,7 @@ export class FleetManager {
|
|
|
82
83
|
return table;
|
|
83
84
|
for (const [name, inst] of Object.entries(this.fleetConfig.instances)) {
|
|
84
85
|
if (inst.topic_id != null) {
|
|
85
|
-
table.set(inst.topic_id, {
|
|
86
|
+
table.set(String(inst.topic_id), {
|
|
86
87
|
kind: inst.general_topic ? "general" : "instance",
|
|
87
88
|
name,
|
|
88
89
|
});
|
|
@@ -124,25 +125,25 @@ export class FleetManager {
|
|
|
124
125
|
const daemon = new Daemon(name, config, instanceDir, topicMode, backend, this.controlClient ?? undefined);
|
|
125
126
|
await daemon.start();
|
|
126
127
|
this.daemons.set(name, daemon);
|
|
127
|
-
daemon.on("restart_complete", (data) => {
|
|
128
|
+
daemon.on("restart_complete", safeHandler((data) => {
|
|
128
129
|
this.eventLog?.insert(name, "context_rotation", data);
|
|
129
130
|
this.logger.info({ name, ...data }, "Context restart completed");
|
|
130
|
-
});
|
|
131
|
+
}, this.logger, `daemon.restart_complete[${name}]`));
|
|
131
132
|
const hangDetector = daemon.getHangDetector();
|
|
132
133
|
if (hangDetector) {
|
|
133
|
-
hangDetector.on("hang", () => {
|
|
134
|
+
hangDetector.on("hang", safeHandler(async () => {
|
|
134
135
|
this.eventLog?.insert(name, "hang_detected", {});
|
|
135
136
|
this.logger.warn({ name }, "Instance appears hung");
|
|
136
|
-
this.sendHangNotification(name);
|
|
137
|
+
await this.sendHangNotification(name);
|
|
137
138
|
this.webhookEmitter?.emit("hang", name);
|
|
138
|
-
});
|
|
139
|
+
}, this.logger, `hangDetector[${name}]`));
|
|
139
140
|
}
|
|
140
|
-
daemon.on("crash_loop", () => {
|
|
141
|
+
daemon.on("crash_loop", safeHandler(() => {
|
|
141
142
|
this.eventLog?.insert(name, "crash_loop", {});
|
|
142
143
|
this.logger.error({ name }, "Instance in crash loop — respawn paused");
|
|
143
144
|
this.notifyInstanceTopic(name, `🔴 ${name} keeps crashing shortly after launch — respawn paused. Check rate limits or run \`agend fleet restart\`.`);
|
|
144
145
|
this.setTopicIcon(name, "red");
|
|
145
|
-
});
|
|
146
|
+
}, this.logger, `daemon.crash_loop[${name}]`));
|
|
146
147
|
this.setTopicIcon(name, "green");
|
|
147
148
|
this.touchActivity(name);
|
|
148
149
|
}
|
|
@@ -226,16 +227,16 @@ export class FleetManager {
|
|
|
226
227
|
this.webhookEmitter = new WebhookEmitter(webhookConfigs, this.logger);
|
|
227
228
|
this.logger.info({ count: webhookConfigs.length }, "Webhook emitter initialized");
|
|
228
229
|
}
|
|
229
|
-
this.costGuard.on("warn", (instance, totalCents, limitCents) => {
|
|
230
|
+
this.costGuard.on("warn", safeHandler((instance, totalCents, limitCents) => {
|
|
230
231
|
this.notifyInstanceTopic(instance, `⚠️ ${instance} cost: ${formatCents(totalCents)} / ${formatCents(limitCents)} (${Math.round(totalCents / limitCents * 100)}%)`);
|
|
231
232
|
this.webhookEmitter?.emit("cost_warning", instance, { cost_cents: totalCents, limit_cents: limitCents });
|
|
232
|
-
});
|
|
233
|
-
this.costGuard.on("limit", (instance, totalCents, limitCents) => {
|
|
233
|
+
}, this.logger, "costGuard.warn"));
|
|
234
|
+
this.costGuard.on("limit", safeHandler(async (instance, totalCents, limitCents) => {
|
|
234
235
|
this.notifyInstanceTopic(instance, `🛑 ${instance} daily limit ${formatCents(limitCents)} reached — pausing instance.`);
|
|
235
236
|
this.eventLog?.insert(instance, "instance_paused", { reason: "cost_limit", cost_cents: totalCents });
|
|
236
237
|
this.webhookEmitter?.emit("cost_limit", instance, { cost_cents: totalCents, limit_cents: limitCents });
|
|
237
|
-
this.stopInstance(instance)
|
|
238
|
-
});
|
|
238
|
+
await this.stopInstance(instance);
|
|
239
|
+
}, this.logger, "costGuard.limit"));
|
|
239
240
|
const summaryConfig = {
|
|
240
241
|
...DEFAULT_DAILY_SUMMARY,
|
|
241
242
|
...fleet.defaults?.daily_summary ?? {},
|
|
@@ -371,10 +372,10 @@ export class FleetManager {
|
|
|
371
372
|
accessManager,
|
|
372
373
|
inboxDir,
|
|
373
374
|
});
|
|
374
|
-
this.adapter.on("message", (msg) => {
|
|
375
|
-
this.handleInboundMessage(msg);
|
|
376
|
-
});
|
|
377
|
-
this.adapter.on("callback_query", async (data) => {
|
|
375
|
+
this.adapter.on("message", safeHandler(async (msg) => {
|
|
376
|
+
await this.handleInboundMessage(msg);
|
|
377
|
+
}, this.logger, "adapter.message"));
|
|
378
|
+
this.adapter.on("callback_query", safeHandler(async (data) => {
|
|
378
379
|
if (data.callbackData.startsWith("hang:")) {
|
|
379
380
|
const parts = data.callbackData.split(":");
|
|
380
381
|
const action = parts[1];
|
|
@@ -395,28 +396,27 @@ export class FleetManager {
|
|
|
395
396
|
}
|
|
396
397
|
return;
|
|
397
398
|
}
|
|
398
|
-
});
|
|
399
|
-
this.adapter.on("topic_closed", (data) => {
|
|
400
|
-
const tid = parseInt(data.threadId, 10);
|
|
399
|
+
}, this.logger, "adapter.callback_query"));
|
|
400
|
+
this.adapter.on("topic_closed", safeHandler(async (data) => {
|
|
401
401
|
// Skip unbind if we archived this topic ourselves
|
|
402
|
-
if (this.archivedTopics.has(
|
|
402
|
+
if (this.archivedTopics.has(data.threadId))
|
|
403
403
|
return;
|
|
404
|
-
this.topicCommands.handleTopicDeleted(
|
|
405
|
-
});
|
|
404
|
+
await this.topicCommands.handleTopicDeleted(data.threadId);
|
|
405
|
+
}, this.logger, "adapter.topic_closed"));
|
|
406
406
|
await this.topicCommands.registerBotCommands();
|
|
407
407
|
await this.adapter.start();
|
|
408
408
|
if (fleet.channel?.group_id) {
|
|
409
409
|
this.adapter.setChatId(String(fleet.channel.group_id));
|
|
410
410
|
}
|
|
411
|
-
this.adapter.on("started", (username) => {
|
|
412
|
-
this.logger.info(`
|
|
413
|
-
});
|
|
414
|
-
this.adapter.on("polling_conflict", ({ attempt, delay }) => {
|
|
411
|
+
this.adapter.on("started", safeHandler((username) => {
|
|
412
|
+
this.logger.info(`Bot @${username} polling`);
|
|
413
|
+
}, this.logger, "adapter.started"));
|
|
414
|
+
this.adapter.on("polling_conflict", safeHandler(({ attempt, delay }) => {
|
|
415
415
|
this.logger.warn(`409 Conflict (attempt ${attempt}), retry in ${delay / 1000}s`);
|
|
416
|
-
});
|
|
417
|
-
this.adapter.on("handler_error", (err) => {
|
|
418
|
-
this.logger.warn({ err: err instanceof Error ? err.message : String(err) }, "
|
|
419
|
-
});
|
|
416
|
+
}, this.logger, "adapter.polling_conflict"));
|
|
417
|
+
this.adapter.on("handler_error", safeHandler((err) => {
|
|
418
|
+
this.logger.warn({ err: err instanceof Error ? err.message : String(err) }, "Adapter handler error");
|
|
419
|
+
}, this.logger, "adapter.handler_error"));
|
|
420
420
|
this.startTopicCleanupPoller();
|
|
421
421
|
// Prune stale external sessions every 5 minutes
|
|
422
422
|
this.sessionPruneTimer = setInterval(() => {
|
|
@@ -438,7 +438,7 @@ export class FleetManager {
|
|
|
438
438
|
try {
|
|
439
439
|
await ipc.connect();
|
|
440
440
|
this.instanceIpcClients.set(name, ipc);
|
|
441
|
-
ipc.on("message", (msg) => {
|
|
441
|
+
ipc.on("message", safeHandler(async (msg) => {
|
|
442
442
|
if (msg.type === "mcp_ready") {
|
|
443
443
|
// Register external sessions (sessionName differs from instance name)
|
|
444
444
|
const sessionName = msg.sessionName;
|
|
@@ -463,7 +463,7 @@ export class FleetManager {
|
|
|
463
463
|
this.sessionRegistry.set(sender, name);
|
|
464
464
|
this.logger.info({ sessionName: sender, instanceName: name }, "Registered external session");
|
|
465
465
|
}
|
|
466
|
-
this.handleOutboundFromInstance(name, msg)
|
|
466
|
+
await this.handleOutboundFromInstance(name, msg);
|
|
467
467
|
}
|
|
468
468
|
else if (msg.type === "fleet_tool_status") {
|
|
469
469
|
this.handleToolStatusFromInstance(name, msg);
|
|
@@ -472,7 +472,7 @@ export class FleetManager {
|
|
|
472
472
|
msg.type === "fleet_schedule_update" || msg.type === "fleet_schedule_delete") {
|
|
473
473
|
this.handleScheduleCrud(name, msg);
|
|
474
474
|
}
|
|
475
|
-
});
|
|
475
|
+
}, this.logger, `ipc.message[${name}]`));
|
|
476
476
|
// Ask daemon for any sessions that registered before we connected
|
|
477
477
|
// (fixes race condition where mcp_ready was broadcast before fleet manager connected)
|
|
478
478
|
ipc.send({ type: "query_sessions" });
|
|
@@ -497,7 +497,7 @@ export class FleetManager {
|
|
|
497
497
|
return undefined;
|
|
498
498
|
}
|
|
499
499
|
async handleInboundMessage(msg) {
|
|
500
|
-
const threadId = msg.threadId
|
|
500
|
+
const threadId = msg.threadId || undefined;
|
|
501
501
|
if (threadId == null) {
|
|
502
502
|
// General topic: check for /status command
|
|
503
503
|
if (await this.topicCommands.handleGeneralCommand(msg))
|
|
@@ -583,7 +583,7 @@ export class FleetManager {
|
|
|
583
583
|
if (this.adapter && msg.chatId) {
|
|
584
584
|
const threadId = msg.threadId ?? undefined;
|
|
585
585
|
this.adapter.sendText(msg.chatId, `⏸ ${instanceName} has hit the weekly usage limit. Your message was not delivered. Limit resets automatically — check /status for details.`, { threadId })
|
|
586
|
-
.catch(e => this.logger.
|
|
586
|
+
.catch(e => this.logger.warn({ err: e }, "Failed to send rate limit notice"));
|
|
587
587
|
}
|
|
588
588
|
this.logger.info({ instanceName }, "Blocked inbound message — weekly rate limit at 100%");
|
|
589
589
|
return true;
|
|
@@ -695,13 +695,13 @@ export class FleetManager {
|
|
|
695
695
|
if (senderTopicId && !isExternalSender) {
|
|
696
696
|
this.adapter.sendText(String(groupId), `→ ${targetName}:\n${message}`, {
|
|
697
697
|
threadId: String(senderTopicId),
|
|
698
|
-
}).catch(e => this.logger.
|
|
698
|
+
}).catch(e => this.logger.warn({ err: e }, "Failed to post cross-instance notification"));
|
|
699
699
|
}
|
|
700
700
|
// Only post to target topic if target is an instance (not external session)
|
|
701
701
|
if (targetTopicId && !this.sessionRegistry.has(targetName)) {
|
|
702
702
|
this.adapter.sendText(String(groupId), `← ${senderLabel}:\n${message}`, {
|
|
703
703
|
threadId: String(targetTopicId),
|
|
704
|
-
}).catch(e => this.logger.
|
|
704
|
+
}).catch(e => this.logger.warn({ err: e }, "Failed to post cross-instance notification"));
|
|
705
705
|
}
|
|
706
706
|
}
|
|
707
707
|
this.logger.info(`✉ ${senderLabel} → ${targetName}: ${(message ?? "").slice(0, 100)}`);
|
|
@@ -737,14 +737,9 @@ export class FleetManager {
|
|
|
737
737
|
const question = args.question;
|
|
738
738
|
const context = args.context;
|
|
739
739
|
const body = context ? `${question}\n\nContext: ${context}` : question;
|
|
740
|
-
// Re-dispatch as send_to_instance with structured metadata
|
|
741
|
-
args
|
|
742
|
-
|
|
743
|
-
args.request_kind = "query";
|
|
744
|
-
args.requires_reply = true;
|
|
745
|
-
args.task_summary = question.slice(0, 120);
|
|
746
|
-
// Recursively handle via the same switch (will hit send_to_instance case above)
|
|
747
|
-
return this.handleOutboundFromInstance(instanceName, { tool: "send_to_instance", args, requestId, fleetRequestId, senderSessionName });
|
|
740
|
+
// Re-dispatch as send_to_instance with structured metadata (new object to avoid mutating input)
|
|
741
|
+
const queryArgs = { ...args, instance_name: targetName, message: body, request_kind: "query", requires_reply: true, task_summary: question.slice(0, 120) };
|
|
742
|
+
return this.handleOutboundFromInstance(instanceName, { tool: "send_to_instance", args: queryArgs, requestId, fleetRequestId, senderSessionName });
|
|
748
743
|
}
|
|
749
744
|
case "delegate_task": {
|
|
750
745
|
const targetName = args.target_instance;
|
|
@@ -756,12 +751,8 @@ export class FleetManager {
|
|
|
756
751
|
body += `\n\nSuccess criteria: ${criteria}`;
|
|
757
752
|
if (context)
|
|
758
753
|
body += `\n\nContext: ${context}`;
|
|
759
|
-
args
|
|
760
|
-
|
|
761
|
-
args.request_kind = "task";
|
|
762
|
-
args.requires_reply = true;
|
|
763
|
-
args.task_summary = task.slice(0, 120);
|
|
764
|
-
return this.handleOutboundFromInstance(instanceName, { tool: "send_to_instance", args, requestId, fleetRequestId, senderSessionName });
|
|
754
|
+
const taskArgs = { ...args, instance_name: targetName, message: body, request_kind: "task", requires_reply: true, task_summary: task.slice(0, 120) };
|
|
755
|
+
return this.handleOutboundFromInstance(instanceName, { tool: "send_to_instance", args: taskArgs, requestId, fleetRequestId, senderSessionName });
|
|
765
756
|
}
|
|
766
757
|
case "report_result": {
|
|
767
758
|
const targetName = args.target_instance;
|
|
@@ -773,12 +764,8 @@ export class FleetManager {
|
|
|
773
764
|
let body = summary;
|
|
774
765
|
if (artifacts)
|
|
775
766
|
body += `\n\nArtifacts: ${artifacts}`;
|
|
776
|
-
args
|
|
777
|
-
|
|
778
|
-
args.request_kind = "report";
|
|
779
|
-
args.requires_reply = false;
|
|
780
|
-
args.task_summary = summary.slice(0, 120);
|
|
781
|
-
return this.handleOutboundFromInstance(instanceName, { tool: "send_to_instance", args, requestId, fleetRequestId, senderSessionName });
|
|
767
|
+
const reportArgs = { ...args, instance_name: targetName, message: body, request_kind: "report", requires_reply: false, task_summary: summary.slice(0, 120) };
|
|
768
|
+
return this.handleOutboundFromInstance(instanceName, { tool: "send_to_instance", args: reportArgs, requestId, fleetRequestId, senderSessionName });
|
|
782
769
|
}
|
|
783
770
|
// Phase 4: Capability discovery
|
|
784
771
|
case "describe_instance": {
|
|
@@ -939,7 +926,7 @@ export class FleetManager {
|
|
|
939
926
|
...(worktreePath ? { worktree_source: directory } : {}),
|
|
940
927
|
};
|
|
941
928
|
this.fleetConfig.instances[newInstanceName] = instanceConfig;
|
|
942
|
-
this.routingTable.set(createdTopicId, { kind: "instance", name: newInstanceName });
|
|
929
|
+
this.routingTable.set(String(createdTopicId), { kind: "instance", name: newInstanceName });
|
|
943
930
|
this.saveFleetConfig();
|
|
944
931
|
// Step c: Start instance
|
|
945
932
|
await this.startInstance(newInstanceName, instanceConfig, true);
|
|
@@ -954,12 +941,12 @@ export class FleetManager {
|
|
|
954
941
|
catch (err) {
|
|
955
942
|
// Rollback in reverse order
|
|
956
943
|
if (newInstanceName && this.daemons.has(newInstanceName)) {
|
|
957
|
-
await this.stopInstance(newInstanceName).catch(
|
|
944
|
+
await this.stopInstance(newInstanceName).catch(e => this.logger.error({ err: e, name: newInstanceName }, "Failed to stop instance during rollback"));
|
|
958
945
|
}
|
|
959
946
|
if (newInstanceName && this.fleetConfig?.instances[newInstanceName]) {
|
|
960
947
|
delete this.fleetConfig.instances[newInstanceName];
|
|
961
948
|
if (createdTopicId)
|
|
962
|
-
this.routingTable.delete(createdTopicId);
|
|
949
|
+
this.routingTable.delete(String(createdTopicId));
|
|
963
950
|
this.saveFleetConfig();
|
|
964
951
|
}
|
|
965
952
|
if (createdTopicId) {
|
|
@@ -1021,7 +1008,7 @@ export class FleetManager {
|
|
|
1021
1008
|
this.adapter.sendText(chatId, text, { threadId }).then((sent) => {
|
|
1022
1009
|
const ipc = this.instanceIpcClients.get(instanceName);
|
|
1023
1010
|
ipc?.send({ type: "fleet_tool_status_ack", messageId: sent.messageId });
|
|
1024
|
-
}).catch(e => this.logger.
|
|
1011
|
+
}).catch(e => this.logger.warn({ err: e }, "Failed to send tool status message"));
|
|
1025
1012
|
}
|
|
1026
1013
|
}
|
|
1027
1014
|
// ===================== Scheduler =====================
|
|
@@ -1141,18 +1128,9 @@ export class FleetManager {
|
|
|
1141
1128
|
}
|
|
1142
1129
|
async deleteForumTopic(topicId) {
|
|
1143
1130
|
try {
|
|
1144
|
-
|
|
1145
|
-
const botTokenEnv = this.fleetConfig?.channel?.bot_token_env;
|
|
1146
|
-
if (!groupId || !botTokenEnv)
|
|
1131
|
+
if (!this.adapter?.deleteTopic)
|
|
1147
1132
|
return;
|
|
1148
|
-
|
|
1149
|
-
if (!botToken)
|
|
1150
|
-
return;
|
|
1151
|
-
await fetch(`https://api.telegram.org/bot${botToken}/deleteForumTopic`, {
|
|
1152
|
-
method: "POST",
|
|
1153
|
-
headers: { "Content-Type": "application/json" },
|
|
1154
|
-
body: JSON.stringify({ chat_id: groupId, message_thread_id: topicId }),
|
|
1155
|
-
});
|
|
1133
|
+
await this.adapter.deleteTopic(topicId);
|
|
1156
1134
|
}
|
|
1157
1135
|
catch (err) {
|
|
1158
1136
|
this.logger.warn({ err, topicId }, "Failed to delete forum topic during rollback");
|
|
@@ -1276,8 +1254,8 @@ export class FleetManager {
|
|
|
1276
1254
|
this.instanceIpcClients.delete(name);
|
|
1277
1255
|
}
|
|
1278
1256
|
// Remove from routing table
|
|
1279
|
-
if (config.topic_id) {
|
|
1280
|
-
this.routingTable.delete(config.topic_id);
|
|
1257
|
+
if (config.topic_id != null) {
|
|
1258
|
+
this.routingTable.delete(String(config.topic_id));
|
|
1281
1259
|
}
|
|
1282
1260
|
// Remove from fleet config and save
|
|
1283
1261
|
delete this.fleetConfig.instances[name];
|
|
@@ -1289,7 +1267,9 @@ export class FleetManager {
|
|
|
1289
1267
|
const timer = setInterval(() => {
|
|
1290
1268
|
try {
|
|
1291
1269
|
const data = JSON.parse(readFileSync(statusFile, "utf-8"));
|
|
1292
|
-
|
|
1270
|
+
if (data.cost?.total_cost_usd != null) {
|
|
1271
|
+
this.costGuard?.updateCost(name, data.cost.total_cost_usd);
|
|
1272
|
+
}
|
|
1293
1273
|
const rl = data.rate_limits;
|
|
1294
1274
|
if (rl) {
|
|
1295
1275
|
const prev = this.instanceRateLimits.get(name);
|
|
@@ -1435,7 +1415,8 @@ export class FleetManager {
|
|
|
1435
1415
|
const topicId = config.topic_id;
|
|
1436
1416
|
if (topicId == null || config.general_topic)
|
|
1437
1417
|
continue;
|
|
1438
|
-
|
|
1418
|
+
const topicIdStr = String(topicId);
|
|
1419
|
+
if (this.archivedTopics.has(topicIdStr))
|
|
1439
1420
|
continue;
|
|
1440
1421
|
const status = this.getInstanceStatus(name);
|
|
1441
1422
|
if (status !== "running")
|
|
@@ -1446,7 +1427,7 @@ export class FleetManager {
|
|
|
1446
1427
|
if (now - last < FleetManager.ARCHIVE_IDLE_MS)
|
|
1447
1428
|
continue;
|
|
1448
1429
|
this.logger.info({ name, topicId, idleHours: Math.round((now - last) / 3600000) }, "Archiving idle topic");
|
|
1449
|
-
this.archivedTopics.add(
|
|
1430
|
+
this.archivedTopics.add(topicIdStr);
|
|
1450
1431
|
this.setTopicIcon(name, "remove");
|
|
1451
1432
|
await this.adapter.closeForumTopic(topicId);
|
|
1452
1433
|
}
|