@songsid/agend 2.0.11-beta.13 → 2.0.11-beta.14
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/classic-channel-manager.d.ts +55 -12
- package/dist/classic-channel-manager.js +116 -30
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/fleet-manager.d.ts +9 -2
- package/dist/fleet-manager.js +171 -136
- package/dist/fleet-manager.js.map +1 -1
- package/dist/quickstart.js +23 -8
- package/dist/quickstart.js.map +1 -1
- package/package.json +1 -1
package/dist/fleet-manager.js
CHANGED
|
@@ -22,7 +22,7 @@ import { processAttachments } from "./channel/attachment-handler.js";
|
|
|
22
22
|
import { routeToolCall } from "./channel/tool-router.js";
|
|
23
23
|
import { Scheduler } from "./scheduler/index.js";
|
|
24
24
|
import { DEFAULT_SCHEDULER_CONFIG } from "./scheduler/index.js";
|
|
25
|
-
import { TopicCommands,
|
|
25
|
+
import { TopicCommands, saveCommandForBackend, parseSaveFilename, SAVE_FILENAME_RE, SAVE_UNSUPPORTED_MSG } from "./topic-commands.js";
|
|
26
26
|
import { DailySummary } from "./daily-summary.js";
|
|
27
27
|
import { WebhookEmitter } from "./webhook-emitter.js";
|
|
28
28
|
import { TmuxControlClient } from "./tmux-control.js";
|
|
@@ -35,7 +35,7 @@ import { outboundHandlers } from "./outbound-handlers.js";
|
|
|
35
35
|
import { handleWebRequest, broadcastSseEvent } from "./web-api.js";
|
|
36
36
|
import { handleViewRequest, isViewPath } from "./view-api.js";
|
|
37
37
|
import { handleAgentRequest } from "./agent-endpoint.js";
|
|
38
|
-
import { ClassicChannelManager
|
|
38
|
+
import { ClassicChannelManager } from "./classic-channel-manager.js";
|
|
39
39
|
import { getTmuxSession } from "./config.js";
|
|
40
40
|
export function resolveReplyThreadId(argsThreadId, instanceConfig) {
|
|
41
41
|
if (typeof argsThreadId === "string" && argsThreadId.length > 0) {
|
|
@@ -179,22 +179,27 @@ export class FleetManager {
|
|
|
179
179
|
}
|
|
180
180
|
return this.routing.map;
|
|
181
181
|
}
|
|
182
|
-
/**
|
|
182
|
+
/**
|
|
183
|
+
* Refresh each adapter's open-channel whitelist after a classic change.
|
|
184
|
+
* Classic channels are NOT registered in the routing engine (it's single-key
|
|
185
|
+
* per channel — can't represent two bots in one channel); routing resolves
|
|
186
|
+
* per-bot via ClassicChannelManager.getInstanceByChannel. Each adapter only
|
|
187
|
+
* opens the channels IT owns so a sibling bot doesn't process another's cross-
|
|
188
|
+
* guild channel.
|
|
189
|
+
*/
|
|
183
190
|
reregisterClassicChannels() {
|
|
184
191
|
if (!this.classicChannels)
|
|
185
192
|
return;
|
|
186
193
|
const channels = this.classicChannels.getAll();
|
|
187
|
-
for (const ch of channels) {
|
|
188
|
-
this.routing.register(ch.channelId, { kind: "classic", name: ch.instanceName });
|
|
189
|
-
}
|
|
190
194
|
// Always update adapter openChannels (including empty — clears stale entries on /stop)
|
|
191
|
-
for (const [, w] of this.worlds) {
|
|
195
|
+
for (const [adapterId, w] of this.worlds) {
|
|
192
196
|
if (typeof w.adapter?.setOpenChannels === "function") {
|
|
193
|
-
|
|
197
|
+
const owned = channels.filter(ch => ch.adapterId === adapterId).map(ch => ch.channelId);
|
|
198
|
+
w.adapter.setOpenChannels(owned);
|
|
194
199
|
}
|
|
195
200
|
}
|
|
196
201
|
if (channels.length > 0) {
|
|
197
|
-
this.logger.info({ count: channels.length }, "
|
|
202
|
+
this.logger.info({ count: channels.length }, "Refreshed classic channel open-lists");
|
|
198
203
|
}
|
|
199
204
|
}
|
|
200
205
|
getInstanceDir(name) {
|
|
@@ -411,10 +416,19 @@ export class FleetManager {
|
|
|
411
416
|
const pidPath = join(this.dataDir, "fleet.pid");
|
|
412
417
|
writeFileSync(pidPath, String(process.pid), "utf-8");
|
|
413
418
|
this.eventLog = new EventLog(join(this.dataDir, "events.db"));
|
|
414
|
-
// Initialize classic channel manager
|
|
419
|
+
// Initialize classic channel manager. The primary adapter (channels[0])
|
|
420
|
+
// migrates legacy single-bot entries and names without a suffix. Classic
|
|
421
|
+
// routing does NOT go through the routing engine (single-key, can't hold two
|
|
422
|
+
// bots in one channel) — it resolves per-bot via getInstanceByChannel.
|
|
415
423
|
this.classicChannels = new ClassicChannelManager(this.dataDir, this.logger);
|
|
424
|
+
const primaryCh = fleet.channels?.[0] ?? fleet.channel;
|
|
425
|
+
if (primaryCh)
|
|
426
|
+
this.classicChannels.setPrimaryAdapterId(primaryCh.id ?? primaryCh.type);
|
|
427
|
+
// Restore the persisted bot binding so replies/cancel go through the right
|
|
428
|
+
// bot after a restart (before this, inbound would re-bind lazily).
|
|
416
429
|
for (const ch of this.classicChannels.getAll()) {
|
|
417
|
-
|
|
430
|
+
if (ch.adapterId)
|
|
431
|
+
this.instanceWorldBinding.set(ch.instanceName, ch.adapterId);
|
|
418
432
|
}
|
|
419
433
|
// Poll classicBot.yaml for external changes every 30s
|
|
420
434
|
this.classicReloadTimer = setInterval(async () => {
|
|
@@ -846,7 +860,7 @@ export class FleetManager {
|
|
|
846
860
|
await data.respond(reply);
|
|
847
861
|
}
|
|
848
862
|
else if (data.command === "stop") {
|
|
849
|
-
const reply = await this.handleClassicStop(data.channelId);
|
|
863
|
+
const reply = await this.handleClassicStop(data.channelId, adapterId);
|
|
850
864
|
await data.respond(reply);
|
|
851
865
|
}
|
|
852
866
|
else if (data.command === "chat") {
|
|
@@ -855,15 +869,15 @@ export class FleetManager {
|
|
|
855
869
|
await data.respond("Usage: `/chat <message>`");
|
|
856
870
|
return;
|
|
857
871
|
}
|
|
858
|
-
const
|
|
859
|
-
if (!
|
|
872
|
+
const name = this.classicChannels?.getInstanceByChannel(data.channelId, adapterId);
|
|
873
|
+
if (!name) {
|
|
860
874
|
await data.respond("No active agent in this channel. Use `/start` first.");
|
|
861
875
|
return;
|
|
862
876
|
}
|
|
863
877
|
const replyMsgId = await data.respond("👀");
|
|
864
878
|
const username = data.username ?? data.userId;
|
|
865
|
-
ClassicChannelManager.logMessage(
|
|
866
|
-
await this.forwardToClassicInstance(
|
|
879
|
+
ClassicChannelManager.logMessage(name, username, `/chat ${text}`, new Date());
|
|
880
|
+
await this.forwardToClassicInstance(name, text, {
|
|
867
881
|
chatId: data.channelId,
|
|
868
882
|
threadId: data.channelId,
|
|
869
883
|
messageId: replyMsgId ?? "",
|
|
@@ -874,7 +888,7 @@ export class FleetManager {
|
|
|
874
888
|
});
|
|
875
889
|
}
|
|
876
890
|
else if (data.command === "save") {
|
|
877
|
-
await this.handleSlashSave(data);
|
|
891
|
+
await this.handleSlashSave(data, adapterId);
|
|
878
892
|
}
|
|
879
893
|
else if (data.command === "load") {
|
|
880
894
|
// load is kiro-cli/classic only — no claude-code equivalent.
|
|
@@ -882,8 +896,8 @@ export class FleetManager {
|
|
|
882
896
|
await data.respond("⛔ This command requires admin access.");
|
|
883
897
|
return;
|
|
884
898
|
}
|
|
885
|
-
const
|
|
886
|
-
if (!
|
|
899
|
+
const name = this.classicChannels?.getInstanceByChannel(data.channelId, adapterId);
|
|
900
|
+
if (!name) {
|
|
887
901
|
await data.respond("No active agent in this channel. Use `/start` first.");
|
|
888
902
|
return;
|
|
889
903
|
}
|
|
@@ -892,35 +906,35 @@ export class FleetManager {
|
|
|
892
906
|
await data.respond("⛔ Invalid filename — only letters, numbers, dots, hyphens, underscores allowed.");
|
|
893
907
|
return;
|
|
894
908
|
}
|
|
895
|
-
this.pasteRawToClassicInstance(
|
|
896
|
-
await data.respond(`✅ Sent \`/chat load ${filename}\` to ${
|
|
909
|
+
this.pasteRawToClassicInstance(name, `/chat load ${filename}`);
|
|
910
|
+
await data.respond(`✅ Sent \`/chat load ${filename}\` to ${name}`);
|
|
897
911
|
}
|
|
898
912
|
else if (data.command === "compact") {
|
|
899
|
-
const
|
|
900
|
-
if (!
|
|
913
|
+
const name = this.classicChannels?.getInstanceByChannel(data.channelId, adapterId);
|
|
914
|
+
if (!name) {
|
|
901
915
|
await data.respond("No active agent in this channel.");
|
|
902
916
|
return;
|
|
903
917
|
}
|
|
904
|
-
const result = await this.topicCommands.sendCompact(
|
|
918
|
+
const result = await this.topicCommands.sendCompact(name);
|
|
905
919
|
await data.respond(result);
|
|
906
920
|
}
|
|
907
921
|
else if (data.command === "cancel") {
|
|
908
|
-
const
|
|
909
|
-
if (!
|
|
922
|
+
const name = this.classicChannels?.getInstanceByChannel(data.channelId, adapterId);
|
|
923
|
+
if (!name) {
|
|
910
924
|
await data.respond("No active agent in this channel.");
|
|
911
925
|
return;
|
|
912
926
|
}
|
|
913
|
-
const ok = this.cancelInstance(
|
|
914
|
-
await data.respond(ok ? `🛑 Sent cancel to ${
|
|
927
|
+
const ok = this.cancelInstance(name);
|
|
928
|
+
await data.respond(ok ? `🛑 Sent cancel to ${name}.` : `❌ ${name} not running.`);
|
|
915
929
|
}
|
|
916
930
|
else if (data.command === "ctx") {
|
|
917
|
-
const
|
|
918
|
-
if (!
|
|
931
|
+
const name = this.classicChannels?.getInstanceByChannel(data.channelId, adapterId);
|
|
932
|
+
if (!name) {
|
|
919
933
|
await data.respond("No active agent in this channel.");
|
|
920
934
|
return;
|
|
921
935
|
}
|
|
922
936
|
// Single source of truth (statusline.json + robust tmux pane fallback).
|
|
923
|
-
await data.respond(await this.topicCommands.getCtxText(
|
|
937
|
+
await data.respond(await this.topicCommands.getCtxText(name));
|
|
924
938
|
}
|
|
925
939
|
else if (data.command === "collab") {
|
|
926
940
|
const collabTarget = this.routing.resolve(data.channelId);
|
|
@@ -938,11 +952,11 @@ export class FleetManager {
|
|
|
938
952
|
await data.respond("⛔ This command requires admin access.");
|
|
939
953
|
return;
|
|
940
954
|
}
|
|
941
|
-
if (!this.classicChannels.isClassicChannel(data.channelId)) {
|
|
955
|
+
if (!this.classicChannels.isClassicChannel(data.channelId, adapterId)) {
|
|
942
956
|
await data.respond("No active agent in this channel. Use `/start` first.");
|
|
943
957
|
return;
|
|
944
958
|
}
|
|
945
|
-
const newState = this.classicChannels.toggleCollab(data.channelId);
|
|
959
|
+
const newState = this.classicChannels.toggleCollab(data.channelId, adapterId);
|
|
946
960
|
await data.respond(newState
|
|
947
961
|
? "🤝 Collaboration mode **ON** — @mention this bot to trigger the agent. Other bot messages are visible."
|
|
948
962
|
: "💬 Collaboration mode **OFF** — use `/chat` to talk to the agent.");
|
|
@@ -995,12 +1009,12 @@ export class FleetManager {
|
|
|
995
1009
|
process.kill(process.pid, "SIGUSR2");
|
|
996
1010
|
}
|
|
997
1011
|
else if (data.command === "compact") {
|
|
998
|
-
const
|
|
999
|
-
if (!
|
|
1012
|
+
const name = this.classicChannels?.getInstanceByChannel(data.channelId, adapterId);
|
|
1013
|
+
if (!name) {
|
|
1000
1014
|
await data.respond("No active agent in this channel.");
|
|
1001
1015
|
return;
|
|
1002
1016
|
}
|
|
1003
|
-
const result = await this.topicCommands.sendCompact(
|
|
1017
|
+
const result = await this.topicCommands.sendCompact(name);
|
|
1004
1018
|
await data.respond(result);
|
|
1005
1019
|
}
|
|
1006
1020
|
}, this.logger, "adapter.slash_command"));
|
|
@@ -1109,7 +1123,7 @@ export class FleetManager {
|
|
|
1109
1123
|
await data.respond(reply);
|
|
1110
1124
|
}
|
|
1111
1125
|
else if (data.command === "stop") {
|
|
1112
|
-
const reply = await this.handleClassicStop(data.channelId);
|
|
1126
|
+
const reply = await this.handleClassicStop(data.channelId, adapterId);
|
|
1113
1127
|
await data.respond(reply);
|
|
1114
1128
|
}
|
|
1115
1129
|
else if (data.command === "chat") {
|
|
@@ -1118,15 +1132,15 @@ export class FleetManager {
|
|
|
1118
1132
|
await data.respond("Usage: `/chat <message>`");
|
|
1119
1133
|
return;
|
|
1120
1134
|
}
|
|
1121
|
-
const
|
|
1122
|
-
if (!
|
|
1135
|
+
const name = this.classicChannels?.getInstanceByChannel(data.channelId, adapterId);
|
|
1136
|
+
if (!name) {
|
|
1123
1137
|
await data.respond("No active agent in this channel. Use `/start` first.");
|
|
1124
1138
|
return;
|
|
1125
1139
|
}
|
|
1126
1140
|
const replyMsgId = await data.respond("👀");
|
|
1127
1141
|
const username = data.username ?? data.userId;
|
|
1128
|
-
ClassicChannelManager.logMessage(
|
|
1129
|
-
await this.forwardToClassicInstance(
|
|
1142
|
+
ClassicChannelManager.logMessage(name, username, `/chat ${text}`, new Date());
|
|
1143
|
+
await this.forwardToClassicInstance(name, text, {
|
|
1130
1144
|
chatId: data.channelId,
|
|
1131
1145
|
threadId: data.channelId,
|
|
1132
1146
|
messageId: replyMsgId ?? "",
|
|
@@ -1137,7 +1151,7 @@ export class FleetManager {
|
|
|
1137
1151
|
});
|
|
1138
1152
|
}
|
|
1139
1153
|
else if (data.command === "save") {
|
|
1140
|
-
await this.handleSlashSave(data);
|
|
1154
|
+
await this.handleSlashSave(data, adapterId);
|
|
1141
1155
|
}
|
|
1142
1156
|
else if (data.command === "load") {
|
|
1143
1157
|
// load is kiro-cli/classic only — no claude-code equivalent.
|
|
@@ -1145,8 +1159,8 @@ export class FleetManager {
|
|
|
1145
1159
|
await data.respond("⛔ This command requires admin access.");
|
|
1146
1160
|
return;
|
|
1147
1161
|
}
|
|
1148
|
-
const
|
|
1149
|
-
if (!
|
|
1162
|
+
const name = this.classicChannels?.getInstanceByChannel(data.channelId, adapterId);
|
|
1163
|
+
if (!name) {
|
|
1150
1164
|
await data.respond("No active agent in this channel. Use `/start` first.");
|
|
1151
1165
|
return;
|
|
1152
1166
|
}
|
|
@@ -1155,35 +1169,26 @@ export class FleetManager {
|
|
|
1155
1169
|
await data.respond("⛔ Invalid filename — only letters, numbers, dots, hyphens, underscores allowed.");
|
|
1156
1170
|
return;
|
|
1157
1171
|
}
|
|
1158
|
-
this.pasteRawToClassicInstance(
|
|
1159
|
-
await data.respond(`✅ Sent \`/chat load ${filename}\` to ${
|
|
1160
|
-
}
|
|
1161
|
-
else if (data.command === "compact") {
|
|
1162
|
-
const target = this.routing.resolve(data.channelId);
|
|
1163
|
-
if (!target) {
|
|
1164
|
-
await data.respond("No active agent in this channel.");
|
|
1165
|
-
return;
|
|
1166
|
-
}
|
|
1167
|
-
const result = await this.topicCommands.sendCompact(target.name);
|
|
1168
|
-
await data.respond(result);
|
|
1172
|
+
this.pasteRawToClassicInstance(name, `/chat load ${filename}`);
|
|
1173
|
+
await data.respond(`✅ Sent \`/chat load ${filename}\` to ${name}`);
|
|
1169
1174
|
}
|
|
1170
1175
|
else if (data.command === "cancel") {
|
|
1171
|
-
const
|
|
1172
|
-
if (!
|
|
1176
|
+
const name = this.classicChannels?.getInstanceByChannel(data.channelId, adapterId);
|
|
1177
|
+
if (!name) {
|
|
1173
1178
|
await data.respond("No active agent in this channel.");
|
|
1174
1179
|
return;
|
|
1175
1180
|
}
|
|
1176
|
-
const ok = this.cancelInstance(
|
|
1177
|
-
await data.respond(ok ? `🛑 Sent cancel to ${
|
|
1181
|
+
const ok = this.cancelInstance(name);
|
|
1182
|
+
await data.respond(ok ? `🛑 Sent cancel to ${name}.` : `❌ ${name} not running.`);
|
|
1178
1183
|
}
|
|
1179
1184
|
else if (data.command === "ctx") {
|
|
1180
|
-
const
|
|
1181
|
-
if (!
|
|
1185
|
+
const name = this.classicChannels?.getInstanceByChannel(data.channelId, adapterId);
|
|
1186
|
+
if (!name) {
|
|
1182
1187
|
await data.respond("No active agent in this channel.");
|
|
1183
1188
|
return;
|
|
1184
1189
|
}
|
|
1185
1190
|
// Single source of truth (statusline.json + robust tmux pane fallback).
|
|
1186
|
-
await data.respond(await this.topicCommands.getCtxText(
|
|
1191
|
+
await data.respond(await this.topicCommands.getCtxText(name));
|
|
1187
1192
|
}
|
|
1188
1193
|
else if (data.command === "collab") {
|
|
1189
1194
|
const collabTarget2 = this.routing.resolve(data.channelId);
|
|
@@ -1201,11 +1206,11 @@ export class FleetManager {
|
|
|
1201
1206
|
await data.respond("⛔ This command requires admin access.");
|
|
1202
1207
|
return;
|
|
1203
1208
|
}
|
|
1204
|
-
if (!this.classicChannels.isClassicChannel(data.channelId)) {
|
|
1209
|
+
if (!this.classicChannels.isClassicChannel(data.channelId, adapterId)) {
|
|
1205
1210
|
await data.respond("No active agent in this channel. Use `/start` first.");
|
|
1206
1211
|
return;
|
|
1207
1212
|
}
|
|
1208
|
-
const newState = this.classicChannels.toggleCollab(data.channelId);
|
|
1213
|
+
const newState = this.classicChannels.toggleCollab(data.channelId, adapterId);
|
|
1209
1214
|
await data.respond(newState
|
|
1210
1215
|
? "🤝 Collaboration mode **ON** — @mention this bot to trigger the agent. Other bot messages are visible."
|
|
1211
1216
|
: "💬 Collaboration mode **OFF** — use `/chat` to talk to the agent.");
|
|
@@ -1258,12 +1263,12 @@ export class FleetManager {
|
|
|
1258
1263
|
process.kill(process.pid, "SIGUSR2");
|
|
1259
1264
|
}
|
|
1260
1265
|
else if (data.command === "compact") {
|
|
1261
|
-
const
|
|
1262
|
-
if (!
|
|
1266
|
+
const name = this.classicChannels?.getInstanceByChannel(data.channelId, adapterId);
|
|
1267
|
+
if (!name) {
|
|
1263
1268
|
await data.respond("No active agent in this channel.");
|
|
1264
1269
|
return;
|
|
1265
1270
|
}
|
|
1266
|
-
const result = await this.topicCommands.sendCompact(
|
|
1271
|
+
const result = await this.topicCommands.sendCompact(name);
|
|
1267
1272
|
await data.respond(result);
|
|
1268
1273
|
}
|
|
1269
1274
|
}, this.logger, `adapter[${adapterId}].slash_command`));
|
|
@@ -1486,8 +1491,17 @@ export class FleetManager {
|
|
|
1486
1491
|
// Routing (by topic/channel) and reply-adapter selection (by channel_id
|
|
1487
1492
|
// binding) are adapter-independent, so it's safe to let whichever adapter
|
|
1488
1493
|
// arrives first handle it.
|
|
1494
|
+
//
|
|
1495
|
+
// EXCEPTION — classic channels with same-channel multi-bot: two bots may own
|
|
1496
|
+
// separate agents in one channel, so each bot must process its OWN copy of
|
|
1497
|
+
// the message (the @mention filter downstream decides who actually forwards).
|
|
1498
|
+
// Key the dedup per-adapter there so a sibling bot's copy isn't dropped.
|
|
1489
1499
|
if (msg.messageId) {
|
|
1490
|
-
const
|
|
1500
|
+
const classicCid = msg.threadId || msg.chatId;
|
|
1501
|
+
const isClassicMsg = this.classicChannels?.hasChannel(classicCid) ?? false;
|
|
1502
|
+
const dedupKey = isClassicMsg
|
|
1503
|
+
? `${msg.source}:${msg.chatId}:${msg.messageId}:${msg.adapterId ?? ""}`
|
|
1504
|
+
: `${msg.source}:${msg.chatId}:${msg.messageId}`;
|
|
1491
1505
|
if (this.recentMessageIds.has(dedupKey)) {
|
|
1492
1506
|
this.logger.debug({ dedupKey, adapterId: msg.adapterId }, "Duplicate inbound across adapters — skipping");
|
|
1493
1507
|
return;
|
|
@@ -1514,21 +1528,25 @@ export class FleetManager {
|
|
|
1514
1528
|
return;
|
|
1515
1529
|
// Fall through to TG classic handling below
|
|
1516
1530
|
}
|
|
1531
|
+
else if (this.classicChannels?.hasChannel(threadId)) {
|
|
1532
|
+
// Classic channel (per-bot): bot messages only when THIS bot owns an
|
|
1533
|
+
// agent here and collab is on for it.
|
|
1534
|
+
const classicName = this.classicChannels.getInstanceByChannel(threadId, msg.adapterId);
|
|
1535
|
+
if (!classicName)
|
|
1536
|
+
return;
|
|
1537
|
+
if (!this.classicChannels.isCollab(threadId, msg.adapterId))
|
|
1538
|
+
return;
|
|
1539
|
+
// Fall through to channel handling
|
|
1540
|
+
}
|
|
1517
1541
|
else {
|
|
1518
1542
|
const target = this.routing.resolve(threadId);
|
|
1519
1543
|
if (!target)
|
|
1520
1544
|
return;
|
|
1521
|
-
if
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
// Fleet topic: allow if collab enabled OR access mode is open
|
|
1527
|
-
const channelCfg = this.getChannelConfig(msg.adapterId);
|
|
1528
|
-
const isOpen = channelCfg?.access?.mode === "open";
|
|
1529
|
-
if (!isOpen && !this.collabInstances.has(target.name))
|
|
1530
|
-
return;
|
|
1531
|
-
}
|
|
1545
|
+
// Fleet topic: allow if collab enabled OR access mode is open
|
|
1546
|
+
const channelCfg = this.getChannelConfig(msg.adapterId);
|
|
1547
|
+
const isOpen = channelCfg?.access?.mode === "open";
|
|
1548
|
+
if (!isOpen && !this.collabInstances.has(target.name))
|
|
1549
|
+
return;
|
|
1532
1550
|
// Fall through to channel handling
|
|
1533
1551
|
}
|
|
1534
1552
|
}
|
|
@@ -1538,9 +1556,10 @@ export class FleetManager {
|
|
|
1538
1556
|
const adapterGroupId = String(this.getChannelConfig(msg.adapterId)?.group_id ?? "");
|
|
1539
1557
|
const isTelegramClassicCandidate = msg.source === "telegram" && msg.chatId !== adapterGroupId && !threadId;
|
|
1540
1558
|
if (!isTelegramClassicCandidate) {
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1559
|
+
// Classic channels are open to all; check per-bot ownership (or fleet topic).
|
|
1560
|
+
const isClassic = !!(threadId && this.classicChannels?.hasChannel(threadId));
|
|
1561
|
+
this.logger.info({ userId: msg.userId, threadId, isClassic }, "Access DENIED for non-allowed user");
|
|
1562
|
+
if (!isClassic)
|
|
1544
1563
|
return;
|
|
1545
1564
|
}
|
|
1546
1565
|
}
|
|
@@ -1628,7 +1647,7 @@ export class FleetManager {
|
|
|
1628
1647
|
}
|
|
1629
1648
|
return;
|
|
1630
1649
|
}
|
|
1631
|
-
const reply = await this.handleClassicStop(chatId);
|
|
1650
|
+
const reply = await this.handleClassicStop(chatId, msg.adapterId);
|
|
1632
1651
|
await msgAdapter?.sendText(chatId, reply);
|
|
1633
1652
|
return;
|
|
1634
1653
|
}
|
|
@@ -1638,34 +1657,34 @@ export class FleetManager {
|
|
|
1638
1657
|
await msgAdapter?.sendText(chatId, "⛔ /compact requires admin access.");
|
|
1639
1658
|
return;
|
|
1640
1659
|
}
|
|
1641
|
-
const
|
|
1642
|
-
if (!
|
|
1660
|
+
const compactName = this.classicChannels.getInstanceByChannel(chatId, msg.adapterId);
|
|
1661
|
+
if (!compactName) {
|
|
1643
1662
|
await msgAdapter?.sendText(chatId, "No active agent. Use /start first.");
|
|
1644
1663
|
return;
|
|
1645
1664
|
}
|
|
1646
|
-
const result = await this.topicCommands.sendCompact(
|
|
1665
|
+
const result = await this.topicCommands.sendCompact(compactName);
|
|
1647
1666
|
await msgAdapter?.sendText(chatId, result);
|
|
1648
1667
|
return;
|
|
1649
1668
|
}
|
|
1650
1669
|
// Handle /cancel command
|
|
1651
1670
|
if (text === "/cancel" || text.startsWith("/cancel@")) {
|
|
1652
|
-
const
|
|
1653
|
-
if (!
|
|
1671
|
+
const cancelName = this.classicChannels.getInstanceByChannel(chatId, msg.adapterId);
|
|
1672
|
+
if (!cancelName) {
|
|
1654
1673
|
await msgAdapter?.sendText(chatId, "No active agent. Use /start first.");
|
|
1655
1674
|
return;
|
|
1656
1675
|
}
|
|
1657
|
-
const ok = this.cancelInstance(
|
|
1658
|
-
await msgAdapter?.sendText(chatId, ok ? `🛑 已送出取消給 ${
|
|
1676
|
+
const ok = this.cancelInstance(cancelName);
|
|
1677
|
+
await msgAdapter?.sendText(chatId, ok ? `🛑 已送出取消給 ${cancelName}。` : `❌ ${cancelName} 未在執行。`);
|
|
1659
1678
|
return;
|
|
1660
1679
|
}
|
|
1661
1680
|
// Handle /ctx command
|
|
1662
1681
|
if (text === "/ctx" || text.startsWith("/ctx@")) {
|
|
1663
|
-
const
|
|
1664
|
-
if (!
|
|
1682
|
+
const ctxName = this.classicChannels.getInstanceByChannel(chatId, msg.adapterId);
|
|
1683
|
+
if (!ctxName) {
|
|
1665
1684
|
await msgAdapter?.sendText(chatId, "No active agent. Use /start first.");
|
|
1666
1685
|
return;
|
|
1667
1686
|
}
|
|
1668
|
-
const reply = await this.topicCommands.getCtxText(
|
|
1687
|
+
const reply = await this.topicCommands.getCtxText(ctxName);
|
|
1669
1688
|
await msgAdapter?.sendText(chatId, reply);
|
|
1670
1689
|
return;
|
|
1671
1690
|
}
|
|
@@ -1675,8 +1694,8 @@ export class FleetManager {
|
|
|
1675
1694
|
await msgAdapter?.sendText(chatId, "⛔ /save requires admin access.");
|
|
1676
1695
|
return;
|
|
1677
1696
|
}
|
|
1678
|
-
const
|
|
1679
|
-
if (!
|
|
1697
|
+
const saveName = this.classicChannels.getInstanceByChannel(chatId, msg.adapterId);
|
|
1698
|
+
if (!saveName) {
|
|
1680
1699
|
await msgAdapter?.sendText(chatId, "No active agent. Use /start first.");
|
|
1681
1700
|
return;
|
|
1682
1701
|
}
|
|
@@ -1689,26 +1708,26 @@ export class FleetManager {
|
|
|
1689
1708
|
await msgAdapter?.sendText(chatId, "⛔ Invalid filename — only letters, numbers, dots, hyphens, underscores allowed.");
|
|
1690
1709
|
return;
|
|
1691
1710
|
}
|
|
1692
|
-
const backend = this.classicChannels.getBackendByInstance(
|
|
1711
|
+
const backend = this.classicChannels.getBackendByInstance(saveName, this.fleetConfig?.defaults?.backend);
|
|
1693
1712
|
const cmd = saveCommandForBackend(backend, filename);
|
|
1694
1713
|
if (!cmd) {
|
|
1695
1714
|
await msgAdapter?.sendText(chatId, SAVE_UNSUPPORTED_MSG);
|
|
1696
1715
|
return;
|
|
1697
1716
|
}
|
|
1698
|
-
this.pasteRawToClassicInstance(
|
|
1699
|
-
await msgAdapter?.sendText(chatId, `✅ Sent \`${cmd}\` to ${
|
|
1717
|
+
this.pasteRawToClassicInstance(saveName, cmd);
|
|
1718
|
+
await msgAdapter?.sendText(chatId, `✅ Sent \`${cmd}\` to ${saveName}`);
|
|
1700
1719
|
return;
|
|
1701
1720
|
}
|
|
1702
|
-
// Route to classic channel if
|
|
1703
|
-
const
|
|
1704
|
-
if (
|
|
1721
|
+
// Route to classic channel if this bot has an agent here (per-bot).
|
|
1722
|
+
const classicName = this.classicChannels.getInstanceByChannel(chatId, msg.adapterId);
|
|
1723
|
+
if (classicName) {
|
|
1705
1724
|
if (msg.adapterId)
|
|
1706
|
-
this.bindInstanceAdapter(
|
|
1725
|
+
this.bindInstanceAdapter(classicName, msg.adapterId, true);
|
|
1707
1726
|
// TG ClassicBot: group requires @mention, private chat forwards directly.
|
|
1708
1727
|
if (!isPrivateChat && !isBotMentioned) {
|
|
1709
1728
|
// No trigger: save attachments + react, log, but don't forward to agent
|
|
1710
1729
|
const syntheticMsg = { ...msg, threadId: chatId, text: rawText.startsWith("/") ? "" : rawText };
|
|
1711
|
-
await this.handleClassicChannelMessage(
|
|
1730
|
+
await this.handleClassicChannelMessage(classicName, syntheticMsg);
|
|
1712
1731
|
return;
|
|
1713
1732
|
}
|
|
1714
1733
|
// Strip @bot from text and forward as /chat
|
|
@@ -1718,7 +1737,7 @@ export class FleetManager {
|
|
|
1718
1737
|
return;
|
|
1719
1738
|
}
|
|
1720
1739
|
const syntheticMsg = { ...msg, threadId: chatId, text: `/chat ${cleanText}` };
|
|
1721
|
-
await this.handleClassicChannelMessage(
|
|
1740
|
+
await this.handleClassicChannelMessage(classicName, syntheticMsg);
|
|
1722
1741
|
return;
|
|
1723
1742
|
}
|
|
1724
1743
|
// Handle @bot without active agent
|
|
@@ -1781,6 +1800,18 @@ export class FleetManager {
|
|
|
1781
1800
|
}
|
|
1782
1801
|
return;
|
|
1783
1802
|
}
|
|
1803
|
+
// Classic channels resolve per-bot (same-channel multi-bot) — a channel can
|
|
1804
|
+
// host two bots' agents. If this channel is classic but THIS bot has no
|
|
1805
|
+
// agent here, a sibling bot owns it; skip rather than misroute to it.
|
|
1806
|
+
if (this.classicChannels?.hasChannel(threadId)) {
|
|
1807
|
+
const classicName = this.classicChannels.getInstanceByChannel(threadId, msg.adapterId);
|
|
1808
|
+
if (!classicName)
|
|
1809
|
+
return;
|
|
1810
|
+
if (msg.adapterId)
|
|
1811
|
+
this.bindInstanceAdapter(classicName, msg.adapterId, true);
|
|
1812
|
+
await this.handleClassicChannelMessage(classicName, msg);
|
|
1813
|
+
return;
|
|
1814
|
+
}
|
|
1784
1815
|
const target = this.routing.resolve(threadId);
|
|
1785
1816
|
if (!target) {
|
|
1786
1817
|
// Only show unbound message for actual forum topics (same group, has threadId)
|
|
@@ -1939,7 +1970,7 @@ export class FleetManager {
|
|
|
1939
1970
|
ts: new Date().toISOString(),
|
|
1940
1971
|
});
|
|
1941
1972
|
// Log bot reply to classic instance chat-log
|
|
1942
|
-
const isClassic =
|
|
1973
|
+
const isClassic = this.classicChannels?.getChannelIdByInstance(instanceName) !== undefined;
|
|
1943
1974
|
if (isClassic) {
|
|
1944
1975
|
ClassicChannelManager.logMessage(instanceName, "bot", args.text ?? "", new Date());
|
|
1945
1976
|
}
|
|
@@ -2628,13 +2659,12 @@ export class FleetManager {
|
|
|
2628
2659
|
.catch(e => this.logger.warn({ err: e, instanceName }, "Failed to send instance topic notification"));
|
|
2629
2660
|
return;
|
|
2630
2661
|
}
|
|
2631
|
-
// Classic instance: find
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
}
|
|
2662
|
+
// Classic instance: find its channelId from the classic manager
|
|
2663
|
+
const classicChatId = this.classicChannels?.getChannelIdByInstance(instanceName);
|
|
2664
|
+
if (classicChatId) {
|
|
2665
|
+
adapter.sendText(classicChatId, text)
|
|
2666
|
+
.catch(e => this.logger.warn({ err: e, instanceName }, "Failed to send classic notification"));
|
|
2667
|
+
return;
|
|
2638
2668
|
}
|
|
2639
2669
|
// Fallback: send to group without threadId
|
|
2640
2670
|
if (groupId) {
|
|
@@ -2651,12 +2681,16 @@ export class FleetManager {
|
|
|
2651
2681
|
* Picks the backend-appropriate command (kiro → /chat save, claude → /export);
|
|
2652
2682
|
* unsupported backends get a clear error. Routes via classic paste or fleet IPC.
|
|
2653
2683
|
*/
|
|
2654
|
-
async handleSlashSave(data) {
|
|
2684
|
+
async handleSlashSave(data, adapterId) {
|
|
2655
2685
|
if (!this.classicChannels?.isAdmin(data.userId)) {
|
|
2656
2686
|
await data.respond("⛔ This command requires admin access.");
|
|
2657
2687
|
return;
|
|
2658
2688
|
}
|
|
2659
|
-
|
|
2689
|
+
// Classic resolves per-bot (same-channel multi-bot); otherwise a fleet topic.
|
|
2690
|
+
const classicName = this.classicChannels.getInstanceByChannel(data.channelId, adapterId);
|
|
2691
|
+
const target = classicName
|
|
2692
|
+
? { kind: "classic", name: classicName }
|
|
2693
|
+
: this.routing.resolve(data.channelId);
|
|
2660
2694
|
if (!target) {
|
|
2661
2695
|
await data.respond("No active agent in this channel. Use `/start` first.");
|
|
2662
2696
|
return;
|
|
@@ -2711,13 +2745,8 @@ export class FleetManager {
|
|
|
2711
2745
|
threadId = String(topicId);
|
|
2712
2746
|
}
|
|
2713
2747
|
else {
|
|
2714
|
-
// Classic instance:
|
|
2715
|
-
|
|
2716
|
-
if (target.kind === "classic" && target.name === instanceName) {
|
|
2717
|
-
chatId = cid;
|
|
2718
|
-
break;
|
|
2719
|
-
}
|
|
2720
|
-
}
|
|
2748
|
+
// Classic instance: channelId from the classic manager.
|
|
2749
|
+
chatId = this.classicChannels?.getChannelIdByInstance(instanceName);
|
|
2721
2750
|
// General / flat fallback: post to the group (no thread).
|
|
2722
2751
|
if (!chatId && groupId)
|
|
2723
2752
|
chatId = String(groupId);
|
|
@@ -3200,7 +3229,7 @@ When users create specialized instances, suggest these configurations:
|
|
|
3200
3229
|
async handleClassicChannelMessage(instanceName, msg) {
|
|
3201
3230
|
const text = msg.text ?? "";
|
|
3202
3231
|
const channelId = msg.threadId ?? msg.chatId;
|
|
3203
|
-
const isCollabMode = this.classicChannels?.isCollab(channelId) ?? false;
|
|
3232
|
+
const isCollabMode = this.classicChannels?.isCollab(channelId, msg.adapterId) ?? false;
|
|
3204
3233
|
// Handle /ctx in classic mode — always, regardless of collab mode
|
|
3205
3234
|
if (text === "/ctx" || text.startsWith("/ctx@")) {
|
|
3206
3235
|
const reply = await this.topicCommands.getCtxText(instanceName);
|
|
@@ -3406,7 +3435,11 @@ When users create specialized instances, suggest these configurations:
|
|
|
3406
3435
|
}
|
|
3407
3436
|
/** Forward a message to a classic channel instance with chat log context */
|
|
3408
3437
|
async forwardToClassicInstance(instanceName, text, msg, extraMeta) {
|
|
3409
|
-
|
|
3438
|
+
// Resolve the channel/adapter from the instance itself so per-channel context
|
|
3439
|
+
// config is correct even for a same-channel second bot.
|
|
3440
|
+
const ctxAdapterId = this.classicChannels?.getAdapterIdByInstance(instanceName);
|
|
3441
|
+
const ctxChannelId = this.classicChannels?.getChannelIdByInstance(instanceName) ?? msg.chatId;
|
|
3442
|
+
const contextLines = this.classicChannels?.getContextLines(ctxChannelId, ctxAdapterId) ?? 5;
|
|
3410
3443
|
const logContext = this.getRecentChatLog(instanceName, contextLines);
|
|
3411
3444
|
const fullText = logContext
|
|
3412
3445
|
? `[Chat log for context]\n${logContext}\n\n[User message]\n${text}`
|
|
@@ -3502,38 +3535,40 @@ When users create specialized instances, suggest these configurations:
|
|
|
3502
3535
|
}
|
|
3503
3536
|
return "⛔ This server is not in the allowed guilds list.";
|
|
3504
3537
|
}
|
|
3505
|
-
|
|
3538
|
+
// Per-bot check: a second bot may /start in the same channel (own agent).
|
|
3539
|
+
if (this.classicChannels.isClassicChannel(channelId, adapterId))
|
|
3506
3540
|
return "This channel already has an active agent. Use /chat to talk.";
|
|
3541
|
+
// Classic no longer lives in the routing engine, so this only guards against
|
|
3542
|
+
// a fleet topic-mode instance colliding with the channel.
|
|
3507
3543
|
if (this.routing.resolve(channelId))
|
|
3508
3544
|
return "This channel is already bound to a topic-mode instance.";
|
|
3509
|
-
const instanceName =
|
|
3510
|
-
this.classicChannels.register(channelId, instanceName, channelName || channelId, userId);
|
|
3511
|
-
this.routing.register(channelId, { kind: "classic", name: instanceName });
|
|
3545
|
+
const instanceName = this.classicChannels.deriveInstanceName(channelName || channelId, channelId, adapterId);
|
|
3546
|
+
this.classicChannels.register(channelId, adapterId, instanceName, channelName || channelId, userId);
|
|
3512
3547
|
// Bind this classic instance to the bot that started it (authoritative), so
|
|
3513
3548
|
// replies/cancel go out through that bot even though every same-guild bot
|
|
3514
|
-
// also sees the channel's messages.
|
|
3549
|
+
// also sees the channel's messages.
|
|
3515
3550
|
if (adapterId)
|
|
3516
3551
|
this.bindInstanceAdapter(instanceName, adapterId);
|
|
3517
|
-
await this.startClassicInstance(instanceName, this.classicChannels.getBackend(channelId, this.fleetConfig?.defaults?.backend), this.classicChannels.getPreTaskCommand(channelId), this.classicChannels.getModel(channelId, this.fleetConfig?.defaults?.model));
|
|
3552
|
+
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));
|
|
3518
3553
|
this.reregisterClassicChannels();
|
|
3519
3554
|
// Auto-enable collab for Discord classic channels (TG uses @mention directly without collab mode)
|
|
3520
|
-
if (guildId && !this.classicChannels.isCollab(channelId)) {
|
|
3521
|
-
this.classicChannels.toggleCollab(channelId);
|
|
3555
|
+
if (guildId && !this.classicChannels.isCollab(channelId, adapterId)) {
|
|
3556
|
+
this.classicChannels.toggleCollab(channelId, adapterId);
|
|
3522
3557
|
}
|
|
3523
|
-
this.logger.info({ channelId, instanceName, userId }, "Classic channel started");
|
|
3558
|
+
this.logger.info({ channelId, adapterId, instanceName, userId }, "Classic channel started");
|
|
3524
3559
|
return `✅ Agent started in this channel. Use \`/chat <message>\` or @mention to talk.`;
|
|
3525
3560
|
}
|
|
3526
3561
|
/** Handle /stop slash command — unregister classic channel */
|
|
3527
|
-
async handleClassicStop(channelId) {
|
|
3562
|
+
async handleClassicStop(channelId, adapterId) {
|
|
3528
3563
|
if (!this.classicChannels)
|
|
3529
3564
|
return "Classic channel manager not initialized.";
|
|
3530
|
-
const ch = this.classicChannels.unregister(channelId);
|
|
3565
|
+
const ch = this.classicChannels.unregister(channelId, adapterId);
|
|
3531
3566
|
if (!ch)
|
|
3532
3567
|
return "No active agent in this channel.";
|
|
3533
|
-
this.
|
|
3568
|
+
this.instanceWorldBinding.delete(ch.instanceName);
|
|
3534
3569
|
await this.stopInstance(ch.instanceName).catch(err => this.logger.warn({ err, instanceName: ch.instanceName }, "Failed to stop classic instance"));
|
|
3535
3570
|
this.reregisterClassicChannels();
|
|
3536
|
-
this.logger.info({ channelId, instanceName: ch.instanceName }, "Classic channel stopped");
|
|
3571
|
+
this.logger.info({ channelId, adapterId, instanceName: ch.instanceName }, "Classic channel stopped");
|
|
3537
3572
|
return `🛑 Agent stopped in this channel.`;
|
|
3538
3573
|
}
|
|
3539
3574
|
async stopAll() {
|