@chrisromp/copilot-bridge 0.11.0 → 0.12.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/core/bridge.d.ts +5 -1
- package/dist/core/bridge.d.ts.map +1 -1
- package/dist/core/bridge.js +7 -3
- package/dist/core/bridge.js.map +1 -1
- package/dist/core/command-handler.d.ts +1 -1
- package/dist/core/command-handler.d.ts.map +1 -1
- package/dist/core/command-handler.js +12 -2
- package/dist/core/command-handler.js.map +1 -1
- package/dist/core/quiet-mode.js +1 -1
- package/dist/core/session-manager.d.ts +37 -0
- package/dist/core/session-manager.d.ts.map +1 -1
- package/dist/core/session-manager.js +200 -25
- package/dist/core/session-manager.js.map +1 -1
- package/dist/index.js +162 -50
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/templates/admin/AGENTS.md +2 -38
- package/templates/agents/AGENTS.md +0 -49
package/dist/index.js
CHANGED
|
@@ -19,6 +19,11 @@ import path from 'node:path';
|
|
|
19
19
|
const log = createLogger('bridge');
|
|
20
20
|
// Active streaming responses, keyed by channelId
|
|
21
21
|
const activeStreams = new Map(); // channelId → streamKey
|
|
22
|
+
// Channels where the no_reply tool was called — used to suppress the SDK's
|
|
23
|
+
// second agentic turn (which always fires after a tool call).
|
|
24
|
+
const noReplyChannels = new Set();
|
|
25
|
+
// Tracks whether content was emitted after no_reply (second turn succeeded)
|
|
26
|
+
const noReplyHadContent = new Set();
|
|
22
27
|
// Preserve thread context across turn_end stream finalization so auto-started
|
|
23
28
|
// streams stay in the same thread.
|
|
24
29
|
const channelThreadRoots = new Map(); // channelId → threadRootId
|
|
@@ -32,7 +37,7 @@ const channelLocks = new Map();
|
|
|
32
37
|
// Per-channel promise chain to serialize SESSION EVENT handling (prevents race on auto-start)
|
|
33
38
|
const eventLocks = new Map();
|
|
34
39
|
// Channels in "quiet mode" — all streaming output suppressed until we determine
|
|
35
|
-
// whether the response is NO_REPLY. Used for
|
|
40
|
+
// whether the response is NO_REPLY. Used for scheduled tasks and silent cron jobs.
|
|
36
41
|
// State managed in src/core/quiet-mode.ts
|
|
37
42
|
// Bot adapters keyed by "platform:botName" for channel→adapter lookup
|
|
38
43
|
const botAdapters = new Map();
|
|
@@ -482,7 +487,7 @@ async function main() {
|
|
|
482
487
|
log.info(`Auto-registered DM channel ${dm.channelId.slice(0, 8)}... for bot "${botName}"`);
|
|
483
488
|
}
|
|
484
489
|
else {
|
|
485
|
-
// Mark pre-configured DM channels so
|
|
490
|
+
// Mark pre-configured DM channels so restart notice logic can identify them
|
|
486
491
|
markChannelAsDM(dm.channelId);
|
|
487
492
|
}
|
|
488
493
|
}
|
|
@@ -547,8 +552,8 @@ async function main() {
|
|
|
547
552
|
}
|
|
548
553
|
},
|
|
549
554
|
});
|
|
550
|
-
//
|
|
551
|
-
|
|
555
|
+
// Post restart notice to admin DM channels (no session creation needed)
|
|
556
|
+
postRestartNotices();
|
|
552
557
|
// Graceful shutdown
|
|
553
558
|
const shutdown = async () => {
|
|
554
559
|
log.info('Shutting down...');
|
|
@@ -833,6 +838,12 @@ async function handleInboundMessage(msg, sessionManager, platformName, botName)
|
|
|
833
838
|
if (key.startsWith(`${platformName}:`) && msg.userId === a.getBotUserId())
|
|
834
839
|
return;
|
|
835
840
|
}
|
|
841
|
+
// Clear stale no_reply flags from previous turn.
|
|
842
|
+
// Note: a late post-no_reply error could theoretically arrive after this
|
|
843
|
+
// clear if the user types very quickly after restart, but post-no_reply errors take
|
|
844
|
+
// ~30s and users rarely type during that window.
|
|
845
|
+
noReplyChannels.delete(msg.channelId);
|
|
846
|
+
noReplyHadContent.delete(msg.channelId);
|
|
836
847
|
// Check user-level access control (reads live config — hot-reloadable)
|
|
837
848
|
const botInfo = getPlatformBots(platformName).get(botName);
|
|
838
849
|
if (!checkUserAccess(msg.userId, msg.username, botInfo?.access, getPlatformAccess(platformName))) {
|
|
@@ -915,7 +926,7 @@ async function handleInboundMessage(msg, sessionManager, platformName, botName)
|
|
|
915
926
|
if (cmdResult.handled) {
|
|
916
927
|
const threadRoot = resolveThreadRoot(msg, threadRequested, channelConfig);
|
|
917
928
|
// Send response before action, except for actions that send their own ack after completing
|
|
918
|
-
const deferResponse = cmdResult.action === 'switch_model' || cmdResult.action === 'switch_agent' || cmdResult.action === 'set_reasoning';
|
|
929
|
+
const deferResponse = cmdResult.action === 'switch_model' || cmdResult.action === 'switch_agent' || cmdResult.action === 'set_reasoning' || cmdResult.action === 'reload_mcp' || cmdResult.action === 'reload_skills';
|
|
919
930
|
if (cmdResult.response && !deferResponse) {
|
|
920
931
|
await adapter.sendMessage(msg.channelId, cmdResult.response, { threadRootId: threadRoot });
|
|
921
932
|
}
|
|
@@ -994,6 +1005,30 @@ async function handleInboundMessage(msg, sessionManager, platformName, botName)
|
|
|
994
1005
|
await adapter.updateMessage(msg.channelId, ackId, reloadMsg);
|
|
995
1006
|
break;
|
|
996
1007
|
}
|
|
1008
|
+
case 'reload_mcp': {
|
|
1009
|
+
const mcpAck = await adapter.sendMessage(msg.channelId, '⏳ Reloading MCP servers...', { threadRootId: threadRoot });
|
|
1010
|
+
try {
|
|
1011
|
+
await sessionManager.reloadMcp(msg.channelId);
|
|
1012
|
+
await adapter.updateMessage(msg.channelId, mcpAck, '✅ MCP servers reloaded.');
|
|
1013
|
+
}
|
|
1014
|
+
catch (err) {
|
|
1015
|
+
log.warn(`MCP reload failed for ${msg.channelId}:`, err);
|
|
1016
|
+
await adapter.updateMessage(msg.channelId, mcpAck, `❌ MCP reload failed: ${err?.message ?? err}`);
|
|
1017
|
+
}
|
|
1018
|
+
break;
|
|
1019
|
+
}
|
|
1020
|
+
case 'reload_skills': {
|
|
1021
|
+
const skillsAck = await adapter.sendMessage(msg.channelId, '⏳ Reloading skills...', { threadRootId: threadRoot });
|
|
1022
|
+
try {
|
|
1023
|
+
await sessionManager.reloadSkills(msg.channelId);
|
|
1024
|
+
await adapter.updateMessage(msg.channelId, skillsAck, '✅ Skills reloaded.');
|
|
1025
|
+
}
|
|
1026
|
+
catch (err) {
|
|
1027
|
+
log.warn(`Skills reload failed for ${msg.channelId}:`, err);
|
|
1028
|
+
await adapter.updateMessage(msg.channelId, skillsAck, `❌ Skills reload failed: ${err?.message ?? err}`);
|
|
1029
|
+
}
|
|
1030
|
+
break;
|
|
1031
|
+
}
|
|
997
1032
|
case 'resume_session': {
|
|
998
1033
|
const oldResumeStream = activeStreams.get(msg.channelId);
|
|
999
1034
|
if (oldResumeStream) {
|
|
@@ -1107,14 +1142,12 @@ async function handleInboundMessage(msg, sessionManager, platformName, botName)
|
|
|
1107
1142
|
}
|
|
1108
1143
|
const ackId = await adapter.sendMessage(msg.channelId, `🧠 Setting reasoning effort to **${cmdResult.payload}**...`, { threadRootId: threadRoot });
|
|
1109
1144
|
try {
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
const suffix = wasNew ? ' (previous session expired — new session created)' : '';
|
|
1113
|
-
await adapter.updateMessage(msg.channelId, ackId, `🧠 Reasoning effort set to **${cmdResult.payload}**.${suffix}`);
|
|
1145
|
+
await sessionManager.setReasoningEffort(msg.channelId, cmdResult.payload);
|
|
1146
|
+
await adapter.updateMessage(msg.channelId, ackId, `🧠 Reasoning effort set to **${cmdResult.payload}**.`);
|
|
1114
1147
|
}
|
|
1115
1148
|
catch (err) {
|
|
1116
|
-
log.error(`Failed to
|
|
1117
|
-
await adapter.updateMessage(msg.channelId, ackId, `🧠 Reasoning effort saved as **${cmdResult.payload}** but
|
|
1149
|
+
log.error(`Failed to set reasoning effort on ${msg.channelId.slice(0, 8)}...:`, err);
|
|
1150
|
+
await adapter.updateMessage(msg.channelId, ackId, `🧠 Reasoning effort saved as **${cmdResult.payload}** but RPC failed. Use \`/reload\` to apply.`);
|
|
1118
1151
|
}
|
|
1119
1152
|
break;
|
|
1120
1153
|
}
|
|
@@ -1348,11 +1381,25 @@ async function handleInboundMessage(msg, sessionManager, platformName, botName)
|
|
|
1348
1381
|
if (toggleAction === 'disable') {
|
|
1349
1382
|
const allNames = [...new Set(skills.map(s => s.name))];
|
|
1350
1383
|
setChannelPrefs(msg.channelId, { disabledSkills: allNames });
|
|
1351
|
-
|
|
1384
|
+
// Apply via RPC for each skill
|
|
1385
|
+
for (const name of allNames) {
|
|
1386
|
+
try {
|
|
1387
|
+
await sessionManager.toggleSkillRpc(msg.channelId, name, 'disable');
|
|
1388
|
+
}
|
|
1389
|
+
catch { /* best-effort */ }
|
|
1390
|
+
}
|
|
1391
|
+
await adapter.sendMessage(msg.channelId, `🔴 Disabled all ${allNames.length} skills.`, { threadRootId: threadRoot });
|
|
1352
1392
|
}
|
|
1353
1393
|
else {
|
|
1394
|
+
const allNames = [...currentDisabled];
|
|
1354
1395
|
setChannelPrefs(msg.channelId, { disabledSkills: [] });
|
|
1355
|
-
|
|
1396
|
+
for (const name of allNames) {
|
|
1397
|
+
try {
|
|
1398
|
+
await sessionManager.toggleSkillRpc(msg.channelId, name, 'enable');
|
|
1399
|
+
}
|
|
1400
|
+
catch { /* best-effort */ }
|
|
1401
|
+
}
|
|
1402
|
+
await adapter.sendMessage(msg.channelId, `🟢 Enabled all skills.`, { threadRootId: threadRoot });
|
|
1356
1403
|
}
|
|
1357
1404
|
break;
|
|
1358
1405
|
}
|
|
@@ -1388,6 +1435,13 @@ async function handleInboundMessage(msg, sessionManager, platformName, botName)
|
|
|
1388
1435
|
}
|
|
1389
1436
|
if (matched.length > 0) {
|
|
1390
1437
|
setChannelPrefs(msg.channelId, { disabledSkills: [...currentDisabled] });
|
|
1438
|
+
// Apply each toggle via RPC (best-effort — pref is already persisted)
|
|
1439
|
+
for (const name of matched) {
|
|
1440
|
+
try {
|
|
1441
|
+
await sessionManager.toggleSkillRpc(msg.channelId, name, toggleAction);
|
|
1442
|
+
}
|
|
1443
|
+
catch { /* best-effort */ }
|
|
1444
|
+
}
|
|
1391
1445
|
}
|
|
1392
1446
|
const lines = [];
|
|
1393
1447
|
if (matched.length > 0) {
|
|
@@ -1401,9 +1455,6 @@ async function handleInboundMessage(msg, sessionManager, platformName, botName)
|
|
|
1401
1455
|
if (notFound.length > 0) {
|
|
1402
1456
|
lines.push(`❌ No match: ${notFound.map(n => `"${n}"`).join(', ')}`);
|
|
1403
1457
|
}
|
|
1404
|
-
if (matched.length > 0) {
|
|
1405
|
-
lines.push('Use `/reload` to apply.');
|
|
1406
|
-
}
|
|
1407
1458
|
await adapter.sendMessage(msg.channelId, lines.join(' '), { threadRootId: threadRoot });
|
|
1408
1459
|
break;
|
|
1409
1460
|
}
|
|
@@ -1918,6 +1969,14 @@ async function handleSessionEvent(sessionId, channelId, event, sessionManager) {
|
|
|
1918
1969
|
}
|
|
1919
1970
|
if (event.type === 'assistant.message') {
|
|
1920
1971
|
const content = formatted.content?.trim();
|
|
1972
|
+
// Check if this message includes a no_reply tool request — the SDK may
|
|
1973
|
+
// bundle tool requests with content and then skip tool execution entirely,
|
|
1974
|
+
// so we must detect no_reply here (not just in tool.execution_start)
|
|
1975
|
+
const toolRequests = event.data?.toolRequests;
|
|
1976
|
+
const hasNoReply = toolRequests?.some(t => t.name === 'no_reply');
|
|
1977
|
+
if (hasNoReply) {
|
|
1978
|
+
noReplyChannels.add(channelId);
|
|
1979
|
+
}
|
|
1921
1980
|
// Skip empty assistant.message events (tool-call signals)
|
|
1922
1981
|
if (!content)
|
|
1923
1982
|
return;
|
|
@@ -1944,6 +2003,15 @@ async function handleSessionEvent(sessionId, channelId, event, sessionManager) {
|
|
|
1944
2003
|
// Suppress verbose/tool/status events during quiet — but let session.idle
|
|
1945
2004
|
// and session.error pass through so channel idle tracking still works
|
|
1946
2005
|
if (formatted.type === 'tool_start' || formatted.type === 'tool_complete') {
|
|
2006
|
+
// Detect no_reply tool even during quiet mode so we can suppress the
|
|
2007
|
+
// second-turn error that the SDK fires after the tool completes
|
|
2008
|
+
if (formatted.type === 'tool_start' && event.type === 'tool.execution_start') {
|
|
2009
|
+
const toolName = event.data?.toolName ?? event.data?.name;
|
|
2010
|
+
if (toolName === 'no_reply') {
|
|
2011
|
+
log.info(`no_reply tool invoked (quiet) on channel ${channelId.slice(0, 8)}...`);
|
|
2012
|
+
noReplyChannels.add(channelId);
|
|
2013
|
+
}
|
|
2014
|
+
}
|
|
1947
2015
|
return;
|
|
1948
2016
|
}
|
|
1949
2017
|
if (formatted.type === 'status' && event.type !== 'session.idle') {
|
|
@@ -1952,7 +2020,20 @@ async function handleSessionEvent(sessionId, channelId, event, sessionManager) {
|
|
|
1952
2020
|
// Errors: exit quiet and fall through to normal error handling (surfaces to user)
|
|
1953
2021
|
if (formatted.type === 'error') {
|
|
1954
2022
|
exitQuietMode(channelId);
|
|
1955
|
-
// Fall through to normal error handling
|
|
2023
|
+
// Fall through to post-no_reply suppression or normal error handling below
|
|
2024
|
+
}
|
|
2025
|
+
}
|
|
2026
|
+
// Suppress errors from the second agentic turn after no_reply tool.
|
|
2027
|
+
// The SDK always starts another turn after a tool call; that turn's model
|
|
2028
|
+
// call may fail but the session remains functional.
|
|
2029
|
+
// Scoped to the specific SDK error to avoid masking unrelated failures.
|
|
2030
|
+
if (formatted.type === 'error' && noReplyChannels.has(channelId)) {
|
|
2031
|
+
const msg = event.data?.message ?? '';
|
|
2032
|
+
if (typeof msg === 'string' && msg.includes('Failed to get response from the AI model')) {
|
|
2033
|
+
log.info(`Suppressing post-no_reply model error on ${channelId.slice(0, 8)}...`);
|
|
2034
|
+
noReplyChannels.delete(channelId);
|
|
2035
|
+
noReplyHadContent.delete(channelId);
|
|
2036
|
+
return;
|
|
1956
2037
|
}
|
|
1957
2038
|
}
|
|
1958
2039
|
if (formatted.verbose && !verbose)
|
|
@@ -1964,6 +2045,31 @@ async function handleSessionEvent(sessionId, channelId, event, sessionManager) {
|
|
|
1964
2045
|
cancelIdleDebounce(channelId);
|
|
1965
2046
|
if (!isBusy(channelId))
|
|
1966
2047
|
markBusy(channelId);
|
|
2048
|
+
// Track if content arrives after no_reply (second turn succeeded)
|
|
2049
|
+
if (noReplyChannels.has(channelId) && formatted.content?.trim()) {
|
|
2050
|
+
noReplyHadContent.add(channelId);
|
|
2051
|
+
}
|
|
2052
|
+
// Suppress NO_REPLY responses — agent decided no response was needed.
|
|
2053
|
+
// This can happen outside quiet mode when the agent determines a user
|
|
2054
|
+
// message doesn't require a reply.
|
|
2055
|
+
if (event.type === 'assistant.message') {
|
|
2056
|
+
// Detect no_reply in tool requests bundled with the message — the SDK
|
|
2057
|
+
// may skip tool execution when content is present alongside tool calls
|
|
2058
|
+
const toolReqs = event.data?.toolRequests;
|
|
2059
|
+
if (toolReqs?.some(t => t.name === 'no_reply')) {
|
|
2060
|
+
noReplyChannels.add(channelId);
|
|
2061
|
+
}
|
|
2062
|
+
const trimmed = formatted.content?.trim();
|
|
2063
|
+
if (trimmed === 'NO_REPLY' || trimmed === '`NO_REPLY`') {
|
|
2064
|
+
log.info(`Filtered NO_REPLY on channel ${channelId.slice(0, 8)}...`);
|
|
2065
|
+
const sk = activeStreams.get(channelId);
|
|
2066
|
+
if (sk) {
|
|
2067
|
+
await streaming.deleteStream(sk);
|
|
2068
|
+
activeStreams.delete(channelId);
|
|
2069
|
+
}
|
|
2070
|
+
break;
|
|
2071
|
+
}
|
|
2072
|
+
}
|
|
1967
2073
|
// When response content starts, finalize the activity feed
|
|
1968
2074
|
if (activityFeeds.has(channelId)) {
|
|
1969
2075
|
await finalizeActivityFeed(channelId, adapter);
|
|
@@ -2017,6 +2123,12 @@ async function handleSessionEvent(sessionId, channelId, event, sessionManager) {
|
|
|
2017
2123
|
if (event.type === 'tool.execution_start') {
|
|
2018
2124
|
const toolName = event.data?.toolName ?? event.data?.name ?? 'unknown';
|
|
2019
2125
|
const args = event.data?.arguments ?? {};
|
|
2126
|
+
// Detect no_reply tool — mark channel for stream suppression
|
|
2127
|
+
if (toolName === 'no_reply') {
|
|
2128
|
+
log.info(`no_reply tool invoked on channel ${channelId.slice(0, 8)}...`);
|
|
2129
|
+
noReplyChannels.add(channelId);
|
|
2130
|
+
break;
|
|
2131
|
+
}
|
|
2020
2132
|
const loop = loopDetector.recordToolCall(channelId, toolName, args);
|
|
2021
2133
|
if (loop.isCritical) {
|
|
2022
2134
|
// Critical loop — warn and force a new session
|
|
@@ -2106,7 +2218,30 @@ async function handleSessionEvent(sessionId, channelId, event, sessionManager) {
|
|
|
2106
2218
|
await finalizeActivityFeed(channelId, adapter);
|
|
2107
2219
|
initialStreamPosted.delete(channelId);
|
|
2108
2220
|
channelThreadRoots.delete(channelId);
|
|
2109
|
-
|
|
2221
|
+
// If no_reply tool was called, handle stream based on whether
|
|
2222
|
+
// the SDK's second turn produced any content.
|
|
2223
|
+
if (noReplyChannels.has(channelId)) {
|
|
2224
|
+
if (noReplyHadContent.has(channelId)) {
|
|
2225
|
+
// Second turn succeeded and produced content — finalize normally
|
|
2226
|
+
// (content will already have been suppressed by quiet mode or
|
|
2227
|
+
// the text NO_REPLY fallback in most cases)
|
|
2228
|
+
log.info(`no_reply: second turn had content, finalizing stream for ${channelId.slice(0, 8)}...`);
|
|
2229
|
+
if (streamKey) {
|
|
2230
|
+
await streaming.finalizeStream(streamKey);
|
|
2231
|
+
activeStreams.delete(channelId);
|
|
2232
|
+
}
|
|
2233
|
+
}
|
|
2234
|
+
else if (streamKey) {
|
|
2235
|
+
// No content from second turn — delete the stream silently
|
|
2236
|
+
log.info(`no_reply: deleting stream for ${channelId.slice(0, 8)}...`);
|
|
2237
|
+
await streaming.deleteStream(streamKey);
|
|
2238
|
+
activeStreams.delete(channelId);
|
|
2239
|
+
}
|
|
2240
|
+
noReplyHadContent.delete(channelId);
|
|
2241
|
+
// Don't clear noReplyChannels here — the SDK may start a second
|
|
2242
|
+
// agentic turn that errors. We clear it on the error or next message.
|
|
2243
|
+
}
|
|
2244
|
+
else if (streamKey) {
|
|
2110
2245
|
log.info(`Session idle, finalizing stream for ${channelId.slice(0, 8)}...`);
|
|
2111
2246
|
await streaming.finalizeStream(streamKey);
|
|
2112
2247
|
activeStreams.delete(channelId);
|
|
@@ -2167,46 +2302,23 @@ async function finalizeActivityFeed(channelId, adapter) {
|
|
|
2167
2302
|
}
|
|
2168
2303
|
activityFeeds.delete(channelId);
|
|
2169
2304
|
}
|
|
2170
|
-
// ---
|
|
2171
|
-
|
|
2172
|
-
|
|
2305
|
+
// --- Startup Restart Notice ---
|
|
2306
|
+
/** Post a restart notice to admin DM channels (no session creation or LLM interaction). */
|
|
2307
|
+
function postRestartNotices() {
|
|
2173
2308
|
const allSessions = getAllChannelSessions();
|
|
2174
|
-
if (allSessions.length === 0)
|
|
2175
|
-
return;
|
|
2176
2309
|
for (const { channelId } of allSessions) {
|
|
2177
|
-
// Only nudge channels belonging to admin bots
|
|
2178
2310
|
if (!isConfiguredChannel(channelId))
|
|
2179
2311
|
continue;
|
|
2180
2312
|
const channelConfig = getChannelConfig(channelId);
|
|
2181
2313
|
const botName = getChannelBotName(channelId);
|
|
2182
2314
|
if (!isBotAdmin(channelConfig.platform, botName))
|
|
2183
2315
|
continue;
|
|
2184
|
-
|
|
2185
|
-
|
|
2186
|
-
|
|
2187
|
-
|
|
2188
|
-
|
|
2189
|
-
|
|
2190
|
-
}
|
|
2191
|
-
// Only post the visible restart notice in DM channels
|
|
2192
|
-
if (channelConfig.isDM) {
|
|
2193
|
-
resolved.adapter.sendMessage(channelId, '🔄 Bridge restarted.').catch(e => log.warn(`Failed to post restart notice on ${channelId.slice(0, 8)}...:`, e));
|
|
2194
|
-
}
|
|
2195
|
-
const clearQuiet = enterQuietMode(channelId);
|
|
2196
|
-
try {
|
|
2197
|
-
markBusy(channelId);
|
|
2198
|
-
await sessionManager.sendMessage(channelId, NUDGE_PROMPT);
|
|
2199
|
-
await waitForChannelIdle(channelId);
|
|
2200
|
-
}
|
|
2201
|
-
finally {
|
|
2202
|
-
clearQuiet();
|
|
2203
|
-
}
|
|
2204
|
-
}
|
|
2205
|
-
catch (err) {
|
|
2206
|
-
exitQuietMode(channelId);
|
|
2207
|
-
markIdleImmediate(channelId);
|
|
2208
|
-
log.warn(`Failed to nudge admin session on channel ${channelId.slice(0, 8)}...:`, err);
|
|
2209
|
-
}
|
|
2316
|
+
if (!channelConfig.isDM)
|
|
2317
|
+
continue;
|
|
2318
|
+
const resolved = getAdapterForChannel(channelId);
|
|
2319
|
+
if (!resolved)
|
|
2320
|
+
continue;
|
|
2321
|
+
resolved.adapter.sendMessage(channelId, '🔄 Bridge restarted.').catch(e => log.warn(`Failed to post restart notice on ${channelId.slice(0, 8)}...:`, e));
|
|
2210
2322
|
}
|
|
2211
2323
|
}
|
|
2212
2324
|
// Start the bridge
|