@songsid/agend 2.0.11-beta.13 → 2.0.11-beta.15
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 +59 -12
- package/dist/classic-channel-manager.js +121 -30
- package/dist/classic-channel-manager.js.map +1 -1
- package/dist/cli.js +19 -21
- package/dist/cli.js.map +1 -1
- package/dist/fleet-manager.d.ts +16 -2
- package/dist/fleet-manager.js +206 -144
- package/dist/fleet-manager.js.map +1 -1
- package/dist/quickstart.js +23 -8
- package/dist/quickstart.js.map +1 -1
- package/dist/ui/view.html +21 -14
- package/dist/view-api.js +4 -28
- package/dist/view-api.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,27 +179,42 @@ 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) {
|
|
201
206
|
return join(this.dataDir, "instances", name);
|
|
202
207
|
}
|
|
208
|
+
/**
|
|
209
|
+
* Resolve a slash-command target in a channel. Classic channels are looked up
|
|
210
|
+
* per-bot (same-channel multi-bot); a fleet-topic instance is found via the
|
|
211
|
+
* routing engine. Used by commands that work in BOTH contexts (/ctx, /compact,
|
|
212
|
+
* /cancel). Classic-only commands (/chat, /load) must NOT use this.
|
|
213
|
+
*/
|
|
214
|
+
resolveSlashTarget(channelId, adapterId) {
|
|
215
|
+
return this.classicChannels?.getInstanceByChannel(channelId, adapterId)
|
|
216
|
+
?? this.routing.resolve(channelId)?.name;
|
|
217
|
+
}
|
|
203
218
|
/** Get the adapter bound to an instance, falling back to primary adapter */
|
|
204
219
|
getAdapterForInstance(name) {
|
|
205
220
|
const worldId = this.instanceWorldBinding.get(name);
|
|
@@ -411,10 +426,19 @@ export class FleetManager {
|
|
|
411
426
|
const pidPath = join(this.dataDir, "fleet.pid");
|
|
412
427
|
writeFileSync(pidPath, String(process.pid), "utf-8");
|
|
413
428
|
this.eventLog = new EventLog(join(this.dataDir, "events.db"));
|
|
414
|
-
// Initialize classic channel manager
|
|
429
|
+
// Initialize classic channel manager. The primary adapter (channels[0])
|
|
430
|
+
// migrates legacy single-bot entries and names without a suffix. Classic
|
|
431
|
+
// routing does NOT go through the routing engine (single-key, can't hold two
|
|
432
|
+
// bots in one channel) — it resolves per-bot via getInstanceByChannel.
|
|
415
433
|
this.classicChannels = new ClassicChannelManager(this.dataDir, this.logger);
|
|
434
|
+
const primaryCh = fleet.channels?.[0] ?? fleet.channel;
|
|
435
|
+
if (primaryCh)
|
|
436
|
+
this.classicChannels.setPrimaryAdapterId(primaryCh.id ?? primaryCh.type);
|
|
437
|
+
// Restore the persisted bot binding so replies/cancel go through the right
|
|
438
|
+
// bot after a restart (before this, inbound would re-bind lazily).
|
|
416
439
|
for (const ch of this.classicChannels.getAll()) {
|
|
417
|
-
|
|
440
|
+
if (ch.adapterId)
|
|
441
|
+
this.instanceWorldBinding.set(ch.instanceName, ch.adapterId);
|
|
418
442
|
}
|
|
419
443
|
// Poll classicBot.yaml for external changes every 30s
|
|
420
444
|
this.classicReloadTimer = setInterval(async () => {
|
|
@@ -846,7 +870,7 @@ export class FleetManager {
|
|
|
846
870
|
await data.respond(reply);
|
|
847
871
|
}
|
|
848
872
|
else if (data.command === "stop") {
|
|
849
|
-
const reply = await this.handleClassicStop(data.channelId);
|
|
873
|
+
const reply = await this.handleClassicStop(data.channelId, adapterId);
|
|
850
874
|
await data.respond(reply);
|
|
851
875
|
}
|
|
852
876
|
else if (data.command === "chat") {
|
|
@@ -855,15 +879,15 @@ export class FleetManager {
|
|
|
855
879
|
await data.respond("Usage: `/chat <message>`");
|
|
856
880
|
return;
|
|
857
881
|
}
|
|
858
|
-
const
|
|
859
|
-
if (!
|
|
882
|
+
const name = this.classicChannels?.getInstanceByChannel(data.channelId, adapterId);
|
|
883
|
+
if (!name) {
|
|
860
884
|
await data.respond("No active agent in this channel. Use `/start` first.");
|
|
861
885
|
return;
|
|
862
886
|
}
|
|
863
887
|
const replyMsgId = await data.respond("👀");
|
|
864
888
|
const username = data.username ?? data.userId;
|
|
865
|
-
ClassicChannelManager.logMessage(
|
|
866
|
-
await this.forwardToClassicInstance(
|
|
889
|
+
ClassicChannelManager.logMessage(name, username, `/chat ${text}`, new Date());
|
|
890
|
+
await this.forwardToClassicInstance(name, text, {
|
|
867
891
|
chatId: data.channelId,
|
|
868
892
|
threadId: data.channelId,
|
|
869
893
|
messageId: replyMsgId ?? "",
|
|
@@ -874,7 +898,7 @@ export class FleetManager {
|
|
|
874
898
|
});
|
|
875
899
|
}
|
|
876
900
|
else if (data.command === "save") {
|
|
877
|
-
await this.handleSlashSave(data);
|
|
901
|
+
await this.handleSlashSave(data, adapterId);
|
|
878
902
|
}
|
|
879
903
|
else if (data.command === "load") {
|
|
880
904
|
// load is kiro-cli/classic only — no claude-code equivalent.
|
|
@@ -882,8 +906,8 @@ export class FleetManager {
|
|
|
882
906
|
await data.respond("⛔ This command requires admin access.");
|
|
883
907
|
return;
|
|
884
908
|
}
|
|
885
|
-
const
|
|
886
|
-
if (!
|
|
909
|
+
const name = this.classicChannels?.getInstanceByChannel(data.channelId, adapterId);
|
|
910
|
+
if (!name) {
|
|
887
911
|
await data.respond("No active agent in this channel. Use `/start` first.");
|
|
888
912
|
return;
|
|
889
913
|
}
|
|
@@ -892,39 +916,41 @@ export class FleetManager {
|
|
|
892
916
|
await data.respond("⛔ Invalid filename — only letters, numbers, dots, hyphens, underscores allowed.");
|
|
893
917
|
return;
|
|
894
918
|
}
|
|
895
|
-
this.pasteRawToClassicInstance(
|
|
896
|
-
await data.respond(`✅ Sent \`/chat load ${filename}\` to ${
|
|
919
|
+
this.pasteRawToClassicInstance(name, `/chat load ${filename}`);
|
|
920
|
+
await data.respond(`✅ Sent \`/chat load ${filename}\` to ${name}`);
|
|
897
921
|
}
|
|
898
922
|
else if (data.command === "compact") {
|
|
899
|
-
const
|
|
900
|
-
if (!
|
|
923
|
+
const name = this.resolveSlashTarget(data.channelId, adapterId);
|
|
924
|
+
if (!name) {
|
|
901
925
|
await data.respond("No active agent in this channel.");
|
|
902
926
|
return;
|
|
903
927
|
}
|
|
904
|
-
const result = await this.topicCommands.sendCompact(
|
|
928
|
+
const result = await this.topicCommands.sendCompact(name);
|
|
905
929
|
await data.respond(result);
|
|
906
930
|
}
|
|
907
931
|
else if (data.command === "cancel") {
|
|
908
|
-
const
|
|
909
|
-
if (!
|
|
932
|
+
const name = this.resolveSlashTarget(data.channelId, adapterId);
|
|
933
|
+
if (!name) {
|
|
910
934
|
await data.respond("No active agent in this channel.");
|
|
911
935
|
return;
|
|
912
936
|
}
|
|
913
|
-
const ok = this.cancelInstance(
|
|
914
|
-
await data.respond(ok ? `🛑 Sent cancel to ${
|
|
937
|
+
const ok = this.cancelInstance(name);
|
|
938
|
+
await data.respond(ok ? `🛑 Sent cancel to ${name}.` : `❌ ${name} not running.`);
|
|
915
939
|
}
|
|
916
940
|
else if (data.command === "ctx") {
|
|
917
|
-
const
|
|
918
|
-
if (!
|
|
941
|
+
const name = this.resolveSlashTarget(data.channelId, adapterId);
|
|
942
|
+
if (!name) {
|
|
919
943
|
await data.respond("No active agent in this channel.");
|
|
920
944
|
return;
|
|
921
945
|
}
|
|
922
946
|
// Single source of truth (statusline.json + robust tmux pane fallback).
|
|
923
|
-
await data.respond(await this.topicCommands.getCtxText(
|
|
947
|
+
await data.respond(await this.topicCommands.getCtxText(name));
|
|
924
948
|
}
|
|
925
949
|
else if (data.command === "collab") {
|
|
950
|
+
// Classic no longer lives in the routing engine, so a routing hit here is
|
|
951
|
+
// always a fleet-topic instance.
|
|
926
952
|
const collabTarget = this.routing.resolve(data.channelId);
|
|
927
|
-
if (collabTarget
|
|
953
|
+
if (collabTarget) {
|
|
928
954
|
const allowed = this.fleetConfig?.channel?.access?.allowed_users ?? [];
|
|
929
955
|
if (allowed.length > 0 && !allowed.some(u => String(u) === String(data.userId))) {
|
|
930
956
|
await data.respond("⛔ Not authorized");
|
|
@@ -938,11 +964,11 @@ export class FleetManager {
|
|
|
938
964
|
await data.respond("⛔ This command requires admin access.");
|
|
939
965
|
return;
|
|
940
966
|
}
|
|
941
|
-
if (!this.classicChannels.isClassicChannel(data.channelId)) {
|
|
967
|
+
if (!this.classicChannels.isClassicChannel(data.channelId, adapterId)) {
|
|
942
968
|
await data.respond("No active agent in this channel. Use `/start` first.");
|
|
943
969
|
return;
|
|
944
970
|
}
|
|
945
|
-
const newState = this.classicChannels.toggleCollab(data.channelId);
|
|
971
|
+
const newState = this.classicChannels.toggleCollab(data.channelId, adapterId);
|
|
946
972
|
await data.respond(newState
|
|
947
973
|
? "🤝 Collaboration mode **ON** — @mention this bot to trigger the agent. Other bot messages are visible."
|
|
948
974
|
: "💬 Collaboration mode **OFF** — use `/chat` to talk to the agent.");
|
|
@@ -995,12 +1021,12 @@ export class FleetManager {
|
|
|
995
1021
|
process.kill(process.pid, "SIGUSR2");
|
|
996
1022
|
}
|
|
997
1023
|
else if (data.command === "compact") {
|
|
998
|
-
const
|
|
999
|
-
if (!
|
|
1024
|
+
const name = this.resolveSlashTarget(data.channelId, adapterId);
|
|
1025
|
+
if (!name) {
|
|
1000
1026
|
await data.respond("No active agent in this channel.");
|
|
1001
1027
|
return;
|
|
1002
1028
|
}
|
|
1003
|
-
const result = await this.topicCommands.sendCompact(
|
|
1029
|
+
const result = await this.topicCommands.sendCompact(name);
|
|
1004
1030
|
await data.respond(result);
|
|
1005
1031
|
}
|
|
1006
1032
|
}, this.logger, "adapter.slash_command"));
|
|
@@ -1109,7 +1135,7 @@ export class FleetManager {
|
|
|
1109
1135
|
await data.respond(reply);
|
|
1110
1136
|
}
|
|
1111
1137
|
else if (data.command === "stop") {
|
|
1112
|
-
const reply = await this.handleClassicStop(data.channelId);
|
|
1138
|
+
const reply = await this.handleClassicStop(data.channelId, adapterId);
|
|
1113
1139
|
await data.respond(reply);
|
|
1114
1140
|
}
|
|
1115
1141
|
else if (data.command === "chat") {
|
|
@@ -1118,15 +1144,15 @@ export class FleetManager {
|
|
|
1118
1144
|
await data.respond("Usage: `/chat <message>`");
|
|
1119
1145
|
return;
|
|
1120
1146
|
}
|
|
1121
|
-
const
|
|
1122
|
-
if (!
|
|
1147
|
+
const name = this.classicChannels?.getInstanceByChannel(data.channelId, adapterId);
|
|
1148
|
+
if (!name) {
|
|
1123
1149
|
await data.respond("No active agent in this channel. Use `/start` first.");
|
|
1124
1150
|
return;
|
|
1125
1151
|
}
|
|
1126
1152
|
const replyMsgId = await data.respond("👀");
|
|
1127
1153
|
const username = data.username ?? data.userId;
|
|
1128
|
-
ClassicChannelManager.logMessage(
|
|
1129
|
-
await this.forwardToClassicInstance(
|
|
1154
|
+
ClassicChannelManager.logMessage(name, username, `/chat ${text}`, new Date());
|
|
1155
|
+
await this.forwardToClassicInstance(name, text, {
|
|
1130
1156
|
chatId: data.channelId,
|
|
1131
1157
|
threadId: data.channelId,
|
|
1132
1158
|
messageId: replyMsgId ?? "",
|
|
@@ -1137,7 +1163,7 @@ export class FleetManager {
|
|
|
1137
1163
|
});
|
|
1138
1164
|
}
|
|
1139
1165
|
else if (data.command === "save") {
|
|
1140
|
-
await this.handleSlashSave(data);
|
|
1166
|
+
await this.handleSlashSave(data, adapterId);
|
|
1141
1167
|
}
|
|
1142
1168
|
else if (data.command === "load") {
|
|
1143
1169
|
// load is kiro-cli/classic only — no claude-code equivalent.
|
|
@@ -1145,8 +1171,8 @@ export class FleetManager {
|
|
|
1145
1171
|
await data.respond("⛔ This command requires admin access.");
|
|
1146
1172
|
return;
|
|
1147
1173
|
}
|
|
1148
|
-
const
|
|
1149
|
-
if (!
|
|
1174
|
+
const name = this.classicChannels?.getInstanceByChannel(data.channelId, adapterId);
|
|
1175
|
+
if (!name) {
|
|
1150
1176
|
await data.respond("No active agent in this channel. Use `/start` first.");
|
|
1151
1177
|
return;
|
|
1152
1178
|
}
|
|
@@ -1155,39 +1181,32 @@ export class FleetManager {
|
|
|
1155
1181
|
await data.respond("⛔ Invalid filename — only letters, numbers, dots, hyphens, underscores allowed.");
|
|
1156
1182
|
return;
|
|
1157
1183
|
}
|
|
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);
|
|
1184
|
+
this.pasteRawToClassicInstance(name, `/chat load ${filename}`);
|
|
1185
|
+
await data.respond(`✅ Sent \`/chat load ${filename}\` to ${name}`);
|
|
1169
1186
|
}
|
|
1170
1187
|
else if (data.command === "cancel") {
|
|
1171
|
-
const
|
|
1172
|
-
if (!
|
|
1188
|
+
const name = this.resolveSlashTarget(data.channelId, adapterId);
|
|
1189
|
+
if (!name) {
|
|
1173
1190
|
await data.respond("No active agent in this channel.");
|
|
1174
1191
|
return;
|
|
1175
1192
|
}
|
|
1176
|
-
const ok = this.cancelInstance(
|
|
1177
|
-
await data.respond(ok ? `🛑 Sent cancel to ${
|
|
1193
|
+
const ok = this.cancelInstance(name);
|
|
1194
|
+
await data.respond(ok ? `🛑 Sent cancel to ${name}.` : `❌ ${name} not running.`);
|
|
1178
1195
|
}
|
|
1179
1196
|
else if (data.command === "ctx") {
|
|
1180
|
-
const
|
|
1181
|
-
if (!
|
|
1197
|
+
const name = this.resolveSlashTarget(data.channelId, adapterId);
|
|
1198
|
+
if (!name) {
|
|
1182
1199
|
await data.respond("No active agent in this channel.");
|
|
1183
1200
|
return;
|
|
1184
1201
|
}
|
|
1185
1202
|
// Single source of truth (statusline.json + robust tmux pane fallback).
|
|
1186
|
-
await data.respond(await this.topicCommands.getCtxText(
|
|
1203
|
+
await data.respond(await this.topicCommands.getCtxText(name));
|
|
1187
1204
|
}
|
|
1188
1205
|
else if (data.command === "collab") {
|
|
1206
|
+
// Classic no longer lives in the routing engine, so a routing hit here is
|
|
1207
|
+
// always a fleet-topic instance.
|
|
1189
1208
|
const collabTarget2 = this.routing.resolve(data.channelId);
|
|
1190
|
-
if (collabTarget2
|
|
1209
|
+
if (collabTarget2) {
|
|
1191
1210
|
const allowed = this.fleetConfig?.channel?.access?.allowed_users ?? [];
|
|
1192
1211
|
if (allowed.length > 0 && !allowed.some(u => String(u) === String(data.userId))) {
|
|
1193
1212
|
await data.respond("⛔ Not authorized");
|
|
@@ -1201,11 +1220,11 @@ export class FleetManager {
|
|
|
1201
1220
|
await data.respond("⛔ This command requires admin access.");
|
|
1202
1221
|
return;
|
|
1203
1222
|
}
|
|
1204
|
-
if (!this.classicChannels.isClassicChannel(data.channelId)) {
|
|
1223
|
+
if (!this.classicChannels.isClassicChannel(data.channelId, adapterId)) {
|
|
1205
1224
|
await data.respond("No active agent in this channel. Use `/start` first.");
|
|
1206
1225
|
return;
|
|
1207
1226
|
}
|
|
1208
|
-
const newState = this.classicChannels.toggleCollab(data.channelId);
|
|
1227
|
+
const newState = this.classicChannels.toggleCollab(data.channelId, adapterId);
|
|
1209
1228
|
await data.respond(newState
|
|
1210
1229
|
? "🤝 Collaboration mode **ON** — @mention this bot to trigger the agent. Other bot messages are visible."
|
|
1211
1230
|
: "💬 Collaboration mode **OFF** — use `/chat` to talk to the agent.");
|
|
@@ -1258,12 +1277,12 @@ export class FleetManager {
|
|
|
1258
1277
|
process.kill(process.pid, "SIGUSR2");
|
|
1259
1278
|
}
|
|
1260
1279
|
else if (data.command === "compact") {
|
|
1261
|
-
const
|
|
1262
|
-
if (!
|
|
1280
|
+
const name = this.resolveSlashTarget(data.channelId, adapterId);
|
|
1281
|
+
if (!name) {
|
|
1263
1282
|
await data.respond("No active agent in this channel.");
|
|
1264
1283
|
return;
|
|
1265
1284
|
}
|
|
1266
|
-
const result = await this.topicCommands.sendCompact(
|
|
1285
|
+
const result = await this.topicCommands.sendCompact(name);
|
|
1267
1286
|
await data.respond(result);
|
|
1268
1287
|
}
|
|
1269
1288
|
}, this.logger, `adapter[${adapterId}].slash_command`));
|
|
@@ -1486,8 +1505,17 @@ export class FleetManager {
|
|
|
1486
1505
|
// Routing (by topic/channel) and reply-adapter selection (by channel_id
|
|
1487
1506
|
// binding) are adapter-independent, so it's safe to let whichever adapter
|
|
1488
1507
|
// arrives first handle it.
|
|
1508
|
+
//
|
|
1509
|
+
// EXCEPTION — classic channels with same-channel multi-bot: two bots may own
|
|
1510
|
+
// separate agents in one channel, so each bot must process its OWN copy of
|
|
1511
|
+
// the message (the @mention filter downstream decides who actually forwards).
|
|
1512
|
+
// Key the dedup per-adapter there so a sibling bot's copy isn't dropped.
|
|
1489
1513
|
if (msg.messageId) {
|
|
1490
|
-
const
|
|
1514
|
+
const classicCid = msg.threadId || msg.chatId;
|
|
1515
|
+
const isClassicMsg = this.classicChannels?.hasChannel(classicCid) ?? false;
|
|
1516
|
+
const dedupKey = isClassicMsg
|
|
1517
|
+
? `${msg.source}:${msg.chatId}:${msg.messageId}:${msg.adapterId ?? ""}`
|
|
1518
|
+
: `${msg.source}:${msg.chatId}:${msg.messageId}`;
|
|
1491
1519
|
if (this.recentMessageIds.has(dedupKey)) {
|
|
1492
1520
|
this.logger.debug({ dedupKey, adapterId: msg.adapterId }, "Duplicate inbound across adapters — skipping");
|
|
1493
1521
|
return;
|
|
@@ -1514,21 +1542,25 @@ export class FleetManager {
|
|
|
1514
1542
|
return;
|
|
1515
1543
|
// Fall through to TG classic handling below
|
|
1516
1544
|
}
|
|
1545
|
+
else if (this.classicChannels?.hasChannel(threadId)) {
|
|
1546
|
+
// Classic channel (per-bot): bot messages only when THIS bot owns an
|
|
1547
|
+
// agent here and collab is on for it.
|
|
1548
|
+
const classicName = this.classicChannels.getInstanceByChannel(threadId, msg.adapterId);
|
|
1549
|
+
if (!classicName)
|
|
1550
|
+
return;
|
|
1551
|
+
if (!this.classicChannels.isCollab(threadId, msg.adapterId))
|
|
1552
|
+
return;
|
|
1553
|
+
// Fall through to channel handling
|
|
1554
|
+
}
|
|
1517
1555
|
else {
|
|
1518
1556
|
const target = this.routing.resolve(threadId);
|
|
1519
1557
|
if (!target)
|
|
1520
1558
|
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
|
-
}
|
|
1559
|
+
// Fleet topic: allow if collab enabled OR access mode is open
|
|
1560
|
+
const channelCfg = this.getChannelConfig(msg.adapterId);
|
|
1561
|
+
const isOpen = channelCfg?.access?.mode === "open";
|
|
1562
|
+
if (!isOpen && !this.collabInstances.has(target.name))
|
|
1563
|
+
return;
|
|
1532
1564
|
// Fall through to channel handling
|
|
1533
1565
|
}
|
|
1534
1566
|
}
|
|
@@ -1538,9 +1570,10 @@ export class FleetManager {
|
|
|
1538
1570
|
const adapterGroupId = String(this.getChannelConfig(msg.adapterId)?.group_id ?? "");
|
|
1539
1571
|
const isTelegramClassicCandidate = msg.source === "telegram" && msg.chatId !== adapterGroupId && !threadId;
|
|
1540
1572
|
if (!isTelegramClassicCandidate) {
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1573
|
+
// Classic channels are open to all; check per-bot ownership (or fleet topic).
|
|
1574
|
+
const isClassic = !!(threadId && this.classicChannels?.hasChannel(threadId));
|
|
1575
|
+
this.logger.info({ userId: msg.userId, threadId, isClassic }, "Access DENIED for non-allowed user");
|
|
1576
|
+
if (!isClassic)
|
|
1544
1577
|
return;
|
|
1545
1578
|
}
|
|
1546
1579
|
}
|
|
@@ -1628,7 +1661,7 @@ export class FleetManager {
|
|
|
1628
1661
|
}
|
|
1629
1662
|
return;
|
|
1630
1663
|
}
|
|
1631
|
-
const reply = await this.handleClassicStop(chatId);
|
|
1664
|
+
const reply = await this.handleClassicStop(chatId, msg.adapterId);
|
|
1632
1665
|
await msgAdapter?.sendText(chatId, reply);
|
|
1633
1666
|
return;
|
|
1634
1667
|
}
|
|
@@ -1638,34 +1671,34 @@ export class FleetManager {
|
|
|
1638
1671
|
await msgAdapter?.sendText(chatId, "⛔ /compact requires admin access.");
|
|
1639
1672
|
return;
|
|
1640
1673
|
}
|
|
1641
|
-
const
|
|
1642
|
-
if (!
|
|
1674
|
+
const compactName = this.classicChannels.getInstanceByChannel(chatId, msg.adapterId);
|
|
1675
|
+
if (!compactName) {
|
|
1643
1676
|
await msgAdapter?.sendText(chatId, "No active agent. Use /start first.");
|
|
1644
1677
|
return;
|
|
1645
1678
|
}
|
|
1646
|
-
const result = await this.topicCommands.sendCompact(
|
|
1679
|
+
const result = await this.topicCommands.sendCompact(compactName);
|
|
1647
1680
|
await msgAdapter?.sendText(chatId, result);
|
|
1648
1681
|
return;
|
|
1649
1682
|
}
|
|
1650
1683
|
// Handle /cancel command
|
|
1651
1684
|
if (text === "/cancel" || text.startsWith("/cancel@")) {
|
|
1652
|
-
const
|
|
1653
|
-
if (!
|
|
1685
|
+
const cancelName = this.classicChannels.getInstanceByChannel(chatId, msg.adapterId);
|
|
1686
|
+
if (!cancelName) {
|
|
1654
1687
|
await msgAdapter?.sendText(chatId, "No active agent. Use /start first.");
|
|
1655
1688
|
return;
|
|
1656
1689
|
}
|
|
1657
|
-
const ok = this.cancelInstance(
|
|
1658
|
-
await msgAdapter?.sendText(chatId, ok ? `🛑 已送出取消給 ${
|
|
1690
|
+
const ok = this.cancelInstance(cancelName);
|
|
1691
|
+
await msgAdapter?.sendText(chatId, ok ? `🛑 已送出取消給 ${cancelName}。` : `❌ ${cancelName} 未在執行。`);
|
|
1659
1692
|
return;
|
|
1660
1693
|
}
|
|
1661
1694
|
// Handle /ctx command
|
|
1662
1695
|
if (text === "/ctx" || text.startsWith("/ctx@")) {
|
|
1663
|
-
const
|
|
1664
|
-
if (!
|
|
1696
|
+
const ctxName = this.classicChannels.getInstanceByChannel(chatId, msg.adapterId);
|
|
1697
|
+
if (!ctxName) {
|
|
1665
1698
|
await msgAdapter?.sendText(chatId, "No active agent. Use /start first.");
|
|
1666
1699
|
return;
|
|
1667
1700
|
}
|
|
1668
|
-
const reply = await this.topicCommands.getCtxText(
|
|
1701
|
+
const reply = await this.topicCommands.getCtxText(ctxName);
|
|
1669
1702
|
await msgAdapter?.sendText(chatId, reply);
|
|
1670
1703
|
return;
|
|
1671
1704
|
}
|
|
@@ -1675,8 +1708,8 @@ export class FleetManager {
|
|
|
1675
1708
|
await msgAdapter?.sendText(chatId, "⛔ /save requires admin access.");
|
|
1676
1709
|
return;
|
|
1677
1710
|
}
|
|
1678
|
-
const
|
|
1679
|
-
if (!
|
|
1711
|
+
const saveName = this.classicChannels.getInstanceByChannel(chatId, msg.adapterId);
|
|
1712
|
+
if (!saveName) {
|
|
1680
1713
|
await msgAdapter?.sendText(chatId, "No active agent. Use /start first.");
|
|
1681
1714
|
return;
|
|
1682
1715
|
}
|
|
@@ -1689,26 +1722,26 @@ export class FleetManager {
|
|
|
1689
1722
|
await msgAdapter?.sendText(chatId, "⛔ Invalid filename — only letters, numbers, dots, hyphens, underscores allowed.");
|
|
1690
1723
|
return;
|
|
1691
1724
|
}
|
|
1692
|
-
const backend = this.classicChannels.getBackendByInstance(
|
|
1725
|
+
const backend = this.classicChannels.getBackendByInstance(saveName, this.fleetConfig?.defaults?.backend);
|
|
1693
1726
|
const cmd = saveCommandForBackend(backend, filename);
|
|
1694
1727
|
if (!cmd) {
|
|
1695
1728
|
await msgAdapter?.sendText(chatId, SAVE_UNSUPPORTED_MSG);
|
|
1696
1729
|
return;
|
|
1697
1730
|
}
|
|
1698
|
-
this.pasteRawToClassicInstance(
|
|
1699
|
-
await msgAdapter?.sendText(chatId, `✅ Sent \`${cmd}\` to ${
|
|
1731
|
+
this.pasteRawToClassicInstance(saveName, cmd);
|
|
1732
|
+
await msgAdapter?.sendText(chatId, `✅ Sent \`${cmd}\` to ${saveName}`);
|
|
1700
1733
|
return;
|
|
1701
1734
|
}
|
|
1702
|
-
// Route to classic channel if
|
|
1703
|
-
const
|
|
1704
|
-
if (
|
|
1735
|
+
// Route to classic channel if this bot has an agent here (per-bot).
|
|
1736
|
+
const classicName = this.classicChannels.getInstanceByChannel(chatId, msg.adapterId);
|
|
1737
|
+
if (classicName) {
|
|
1705
1738
|
if (msg.adapterId)
|
|
1706
|
-
this.bindInstanceAdapter(
|
|
1739
|
+
this.bindInstanceAdapter(classicName, msg.adapterId, true);
|
|
1707
1740
|
// TG ClassicBot: group requires @mention, private chat forwards directly.
|
|
1708
1741
|
if (!isPrivateChat && !isBotMentioned) {
|
|
1709
1742
|
// No trigger: save attachments + react, log, but don't forward to agent
|
|
1710
1743
|
const syntheticMsg = { ...msg, threadId: chatId, text: rawText.startsWith("/") ? "" : rawText };
|
|
1711
|
-
await this.handleClassicChannelMessage(
|
|
1744
|
+
await this.handleClassicChannelMessage(classicName, syntheticMsg);
|
|
1712
1745
|
return;
|
|
1713
1746
|
}
|
|
1714
1747
|
// Strip @bot from text and forward as /chat
|
|
@@ -1718,7 +1751,7 @@ export class FleetManager {
|
|
|
1718
1751
|
return;
|
|
1719
1752
|
}
|
|
1720
1753
|
const syntheticMsg = { ...msg, threadId: chatId, text: `/chat ${cleanText}` };
|
|
1721
|
-
await this.handleClassicChannelMessage(
|
|
1754
|
+
await this.handleClassicChannelMessage(classicName, syntheticMsg);
|
|
1722
1755
|
return;
|
|
1723
1756
|
}
|
|
1724
1757
|
// Handle @bot without active agent
|
|
@@ -1743,9 +1776,12 @@ export class FleetManager {
|
|
|
1743
1776
|
if (msg.adapterId)
|
|
1744
1777
|
this.bindInstanceAdapter(generalInstance, msg.adapterId, true);
|
|
1745
1778
|
const inboundAdapter = this.worlds.get(msg.adapterId ?? "")?.adapter ?? this.adapter;
|
|
1746
|
-
// React immediately — before any other API calls
|
|
1779
|
+
// React immediately — before any other API calls. Use the adapter BOUND to
|
|
1780
|
+
// the instance (not whichever same-guild bot received the event first) so
|
|
1781
|
+
// exactly the owning bot reacts — no duplicate 👀 from a sibling bot.
|
|
1747
1782
|
if (msg.chatId && msg.messageId) {
|
|
1748
|
-
|
|
1783
|
+
const reactAdapter = this.getAdapterForInstance(generalInstance) ?? inboundAdapter;
|
|
1784
|
+
reactAdapter.react(msg.threadId ?? msg.chatId, msg.messageId, "👀")
|
|
1749
1785
|
.catch(e => this.logger.debug({ err: e.message }, "Auto-react failed"));
|
|
1750
1786
|
}
|
|
1751
1787
|
this.warnIfRateLimited(generalInstance, msg);
|
|
@@ -1781,6 +1817,18 @@ export class FleetManager {
|
|
|
1781
1817
|
}
|
|
1782
1818
|
return;
|
|
1783
1819
|
}
|
|
1820
|
+
// Classic channels resolve per-bot (same-channel multi-bot) — a channel can
|
|
1821
|
+
// host two bots' agents. If this channel is classic but THIS bot has no
|
|
1822
|
+
// agent here, a sibling bot owns it; skip rather than misroute to it.
|
|
1823
|
+
if (this.classicChannels?.hasChannel(threadId)) {
|
|
1824
|
+
const classicName = this.classicChannels.getInstanceByChannel(threadId, msg.adapterId);
|
|
1825
|
+
if (!classicName)
|
|
1826
|
+
return;
|
|
1827
|
+
if (msg.adapterId)
|
|
1828
|
+
this.bindInstanceAdapter(classicName, msg.adapterId, true);
|
|
1829
|
+
await this.handleClassicChannelMessage(classicName, msg);
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1784
1832
|
const target = this.routing.resolve(threadId);
|
|
1785
1833
|
if (!target) {
|
|
1786
1834
|
// Only show unbound message for actual forum topics (same group, has threadId)
|
|
@@ -1812,9 +1860,12 @@ export class FleetManager {
|
|
|
1812
1860
|
if (msg.adapterId)
|
|
1813
1861
|
this.bindInstanceAdapter(instanceName, msg.adapterId, true);
|
|
1814
1862
|
const inboundAdapter = this.worlds.get(msg.adapterId ?? "")?.adapter ?? this.adapter;
|
|
1815
|
-
// React immediately — before any other Discord API calls
|
|
1863
|
+
// React immediately — before any other Discord API calls. Use the adapter
|
|
1864
|
+
// BOUND to the instance (not whichever same-guild bot received the event
|
|
1865
|
+
// first) so exactly the owning bot reacts — no duplicate 👀 from a sibling.
|
|
1816
1866
|
if (msg.chatId && msg.messageId) {
|
|
1817
|
-
|
|
1867
|
+
const reactAdapter = this.getAdapterForInstance(instanceName) ?? inboundAdapter;
|
|
1868
|
+
reactAdapter.react(this.reactTarget(msg), msg.messageId, "👀")
|
|
1818
1869
|
.catch(e => this.logger.debug({ err: e.message }, "Auto-react failed"));
|
|
1819
1870
|
}
|
|
1820
1871
|
// These may hit Discord API (topic icon, archive) — do after react
|
|
@@ -1939,7 +1990,7 @@ export class FleetManager {
|
|
|
1939
1990
|
ts: new Date().toISOString(),
|
|
1940
1991
|
});
|
|
1941
1992
|
// Log bot reply to classic instance chat-log
|
|
1942
|
-
const isClassic =
|
|
1993
|
+
const isClassic = this.classicChannels?.getChannelIdByInstance(instanceName) !== undefined;
|
|
1943
1994
|
if (isClassic) {
|
|
1944
1995
|
ClassicChannelManager.logMessage(instanceName, "bot", args.text ?? "", new Date());
|
|
1945
1996
|
}
|
|
@@ -2628,13 +2679,12 @@ export class FleetManager {
|
|
|
2628
2679
|
.catch(e => this.logger.warn({ err: e, instanceName }, "Failed to send instance topic notification"));
|
|
2629
2680
|
return;
|
|
2630
2681
|
}
|
|
2631
|
-
// Classic instance: find
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
|
|
2636
|
-
|
|
2637
|
-
}
|
|
2682
|
+
// Classic instance: find its channelId from the classic manager
|
|
2683
|
+
const classicChatId = this.classicChannels?.getChannelIdByInstance(instanceName);
|
|
2684
|
+
if (classicChatId) {
|
|
2685
|
+
adapter.sendText(classicChatId, text)
|
|
2686
|
+
.catch(e => this.logger.warn({ err: e, instanceName }, "Failed to send classic notification"));
|
|
2687
|
+
return;
|
|
2638
2688
|
}
|
|
2639
2689
|
// Fallback: send to group without threadId
|
|
2640
2690
|
if (groupId) {
|
|
@@ -2651,12 +2701,16 @@ export class FleetManager {
|
|
|
2651
2701
|
* Picks the backend-appropriate command (kiro → /chat save, claude → /export);
|
|
2652
2702
|
* unsupported backends get a clear error. Routes via classic paste or fleet IPC.
|
|
2653
2703
|
*/
|
|
2654
|
-
async handleSlashSave(data) {
|
|
2704
|
+
async handleSlashSave(data, adapterId) {
|
|
2655
2705
|
if (!this.classicChannels?.isAdmin(data.userId)) {
|
|
2656
2706
|
await data.respond("⛔ This command requires admin access.");
|
|
2657
2707
|
return;
|
|
2658
2708
|
}
|
|
2659
|
-
|
|
2709
|
+
// Classic resolves per-bot (same-channel multi-bot); otherwise a fleet topic.
|
|
2710
|
+
const classicName = this.classicChannels.getInstanceByChannel(data.channelId, adapterId);
|
|
2711
|
+
const target = classicName
|
|
2712
|
+
? { kind: "classic", name: classicName }
|
|
2713
|
+
: this.routing.resolve(data.channelId);
|
|
2660
2714
|
if (!target) {
|
|
2661
2715
|
await data.respond("No active agent in this channel. Use `/start` first.");
|
|
2662
2716
|
return;
|
|
@@ -2711,13 +2765,8 @@ export class FleetManager {
|
|
|
2711
2765
|
threadId = String(topicId);
|
|
2712
2766
|
}
|
|
2713
2767
|
else {
|
|
2714
|
-
// Classic instance:
|
|
2715
|
-
|
|
2716
|
-
if (target.kind === "classic" && target.name === instanceName) {
|
|
2717
|
-
chatId = cid;
|
|
2718
|
-
break;
|
|
2719
|
-
}
|
|
2720
|
-
}
|
|
2768
|
+
// Classic instance: channelId from the classic manager.
|
|
2769
|
+
chatId = this.classicChannels?.getChannelIdByInstance(instanceName);
|
|
2721
2770
|
// General / flat fallback: post to the group (no thread).
|
|
2722
2771
|
if (!chatId && groupId)
|
|
2723
2772
|
chatId = String(groupId);
|
|
@@ -3200,7 +3249,7 @@ When users create specialized instances, suggest these configurations:
|
|
|
3200
3249
|
async handleClassicChannelMessage(instanceName, msg) {
|
|
3201
3250
|
const text = msg.text ?? "";
|
|
3202
3251
|
const channelId = msg.threadId ?? msg.chatId;
|
|
3203
|
-
const isCollabMode = this.classicChannels?.isCollab(channelId) ?? false;
|
|
3252
|
+
const isCollabMode = this.classicChannels?.isCollab(channelId, msg.adapterId) ?? false;
|
|
3204
3253
|
// Handle /ctx in classic mode — always, regardless of collab mode
|
|
3205
3254
|
if (text === "/ctx" || text.startsWith("/ctx@")) {
|
|
3206
3255
|
const reply = await this.topicCommands.getCtxText(instanceName);
|
|
@@ -3227,8 +3276,15 @@ When users create specialized instances, suggest these configurations:
|
|
|
3227
3276
|
: "");
|
|
3228
3277
|
ClassicChannelManager.logMessage(instanceName, msg.username, text + collabAttachTag, msg.timestamp, msg.replyToText);
|
|
3229
3278
|
this.logger.info({ instanceName, user: msg.username, textLen: text.length, attachments: msg.attachments?.length ?? 0, source: msg.source }, "Collab mode message");
|
|
3230
|
-
// Check for @mention trigger: must be exact <@BOT_USER_ID>, not @everyone/@here
|
|
3231
|
-
|
|
3279
|
+
// Check for @mention trigger: must be exact <@BOT_USER_ID>, not @everyone/@here.
|
|
3280
|
+
// Each bot matches ONLY its own id. A secondary bot must NOT fall back to the
|
|
3281
|
+
// process-wide botUserId (the primary's) — otherwise, in a same-channel
|
|
3282
|
+
// multi-bot setup, an @mention of the primary would also match the secondary
|
|
3283
|
+
// and BOTH bots would react 👀 and forward. Only the primary adapter may use
|
|
3284
|
+
// the fallback.
|
|
3285
|
+
const mentionWorld = this.worlds.get(msg.adapterId ?? "");
|
|
3286
|
+
const isPrimaryAdapter = !mentionWorld || mentionWorld.adapter === this.adapter;
|
|
3287
|
+
const adapterBotUserId = mentionWorld?.botUserId ?? (isPrimaryAdapter ? this.botUserId : undefined);
|
|
3232
3288
|
const mentionTag = adapterBotUserId ? `<@${adapterBotUserId}>` : null;
|
|
3233
3289
|
const isMentioned = mentionTag && text.includes(mentionTag);
|
|
3234
3290
|
if (!isMentioned) {
|
|
@@ -3406,7 +3462,11 @@ When users create specialized instances, suggest these configurations:
|
|
|
3406
3462
|
}
|
|
3407
3463
|
/** Forward a message to a classic channel instance with chat log context */
|
|
3408
3464
|
async forwardToClassicInstance(instanceName, text, msg, extraMeta) {
|
|
3409
|
-
|
|
3465
|
+
// Resolve the channel/adapter from the instance itself so per-channel context
|
|
3466
|
+
// config is correct even for a same-channel second bot.
|
|
3467
|
+
const ctxAdapterId = this.classicChannels?.getAdapterIdByInstance(instanceName);
|
|
3468
|
+
const ctxChannelId = this.classicChannels?.getChannelIdByInstance(instanceName) ?? msg.chatId;
|
|
3469
|
+
const contextLines = this.classicChannels?.getContextLines(ctxChannelId, ctxAdapterId) ?? 5;
|
|
3410
3470
|
const logContext = this.getRecentChatLog(instanceName, contextLines);
|
|
3411
3471
|
const fullText = logContext
|
|
3412
3472
|
? `[Chat log for context]\n${logContext}\n\n[User message]\n${text}`
|
|
@@ -3502,38 +3562,40 @@ When users create specialized instances, suggest these configurations:
|
|
|
3502
3562
|
}
|
|
3503
3563
|
return "⛔ This server is not in the allowed guilds list.";
|
|
3504
3564
|
}
|
|
3505
|
-
|
|
3565
|
+
// Per-bot check: a second bot may /start in the same channel (own agent).
|
|
3566
|
+
if (this.classicChannels.isClassicChannel(channelId, adapterId))
|
|
3506
3567
|
return "This channel already has an active agent. Use /chat to talk.";
|
|
3568
|
+
// Classic no longer lives in the routing engine, so this only guards against
|
|
3569
|
+
// a fleet topic-mode instance colliding with the channel.
|
|
3507
3570
|
if (this.routing.resolve(channelId))
|
|
3508
3571
|
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 });
|
|
3572
|
+
const instanceName = this.classicChannels.deriveInstanceName(channelName || channelId, channelId, adapterId);
|
|
3573
|
+
this.classicChannels.register(channelId, adapterId, instanceName, channelName || channelId, userId);
|
|
3512
3574
|
// Bind this classic instance to the bot that started it (authoritative), so
|
|
3513
3575
|
// replies/cancel go out through that bot even though every same-guild bot
|
|
3514
|
-
// also sees the channel's messages.
|
|
3576
|
+
// also sees the channel's messages.
|
|
3515
3577
|
if (adapterId)
|
|
3516
3578
|
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));
|
|
3579
|
+
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
3580
|
this.reregisterClassicChannels();
|
|
3519
3581
|
// 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);
|
|
3582
|
+
if (guildId && !this.classicChannels.isCollab(channelId, adapterId)) {
|
|
3583
|
+
this.classicChannels.toggleCollab(channelId, adapterId);
|
|
3522
3584
|
}
|
|
3523
|
-
this.logger.info({ channelId, instanceName, userId }, "Classic channel started");
|
|
3585
|
+
this.logger.info({ channelId, adapterId, instanceName, userId }, "Classic channel started");
|
|
3524
3586
|
return `✅ Agent started in this channel. Use \`/chat <message>\` or @mention to talk.`;
|
|
3525
3587
|
}
|
|
3526
3588
|
/** Handle /stop slash command — unregister classic channel */
|
|
3527
|
-
async handleClassicStop(channelId) {
|
|
3589
|
+
async handleClassicStop(channelId, adapterId) {
|
|
3528
3590
|
if (!this.classicChannels)
|
|
3529
3591
|
return "Classic channel manager not initialized.";
|
|
3530
|
-
const ch = this.classicChannels.unregister(channelId);
|
|
3592
|
+
const ch = this.classicChannels.unregister(channelId, adapterId);
|
|
3531
3593
|
if (!ch)
|
|
3532
3594
|
return "No active agent in this channel.";
|
|
3533
|
-
this.
|
|
3595
|
+
this.instanceWorldBinding.delete(ch.instanceName);
|
|
3534
3596
|
await this.stopInstance(ch.instanceName).catch(err => this.logger.warn({ err, instanceName: ch.instanceName }, "Failed to stop classic instance"));
|
|
3535
3597
|
this.reregisterClassicChannels();
|
|
3536
|
-
this.logger.info({ channelId, instanceName: ch.instanceName }, "Classic channel stopped");
|
|
3598
|
+
this.logger.info({ channelId, adapterId, instanceName: ch.instanceName }, "Classic channel stopped");
|
|
3537
3599
|
return `🛑 Agent stopped in this channel.`;
|
|
3538
3600
|
}
|
|
3539
3601
|
async stopAll() {
|