@chrisromp/copilot-bridge 0.9.2 → 0.10.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.
@@ -10,9 +10,10 @@ import { canCall, createContext, extendContext, getBotWorkspaceMap, buildWorkspa
10
10
  import { createLogger } from '../logger.js';
11
11
  import { tryWithFallback, isModelError, buildFallbackChain } from './model-fallback.js';
12
12
  import { loadHooks, getHooksInfo } from './hooks-loader.js';
13
+ import { getBridgeDocs } from './bridge-docs.js';
13
14
  const log = createLogger('session');
14
- /** Custom tools auto-approved without interactive prompt (they enforce workspace boundaries internally). */
15
- export const BRIDGE_CUSTOM_TOOLS = ['send_file', 'show_file_in_chat', 'ask_agent', 'schedule'];
15
+ /** Custom tools auto-approved without interactive prompt (read-only or enforce workspace boundaries internally). */
16
+ export const BRIDGE_CUSTOM_TOOLS = ['send_file', 'show_file_in_chat', 'ask_agent', 'schedule', 'fetch_copilot_bridge_documentation'];
16
17
  /** Simple mutex for serializing env-sensitive session creation. */
17
18
  let envLock = Promise.resolve();
18
19
  /**
@@ -210,6 +211,35 @@ function loadWorkspaceMcpServers(workspacePath) {
210
211
  return { servers: {}, env: workspaceEnv };
211
212
  }
212
213
  }
214
+ /**
215
+ * Merge global and workspace MCP server configs, injecting cwd and env into local servers.
216
+ * Exported for testing — used internally by SessionManager.resolveMcpServers().
217
+ */
218
+ export function mergeMcpServers(globalServers, workspaceServers, workspaceEnv, workingDirectory) {
219
+ const merged = {};
220
+ for (const [name, config] of Object.entries(globalServers)) {
221
+ const serverConfig = { ...config };
222
+ const isLocal = !serverConfig.type || serverConfig.type === 'local' || serverConfig.type === 'stdio';
223
+ if (isLocal) {
224
+ if (!serverConfig.cwd) {
225
+ serverConfig.cwd = workingDirectory;
226
+ }
227
+ if (Object.keys(workspaceEnv).length > 0) {
228
+ serverConfig.env = { ...workspaceEnv, ...(serverConfig.env || {}) };
229
+ }
230
+ }
231
+ merged[name] = serverConfig;
232
+ }
233
+ for (const [name, config] of Object.entries(workspaceServers)) {
234
+ const serverConfig = { ...config };
235
+ const isLocal = !serverConfig.type || serverConfig.type === 'local' || serverConfig.type === 'stdio';
236
+ if (isLocal && !serverConfig.cwd) {
237
+ serverConfig.cwd = workingDirectory;
238
+ }
239
+ merged[name] = serverConfig;
240
+ }
241
+ return merged;
242
+ }
213
243
  /**
214
244
  * Extract individual command names from a shell command string.
215
245
  * Handles chained commands: "ls -la && grep -r foo . | head" → ["ls", "grep", "head"]
@@ -294,6 +324,36 @@ function discoverSkillDirectories(workingDirectory) {
294
324
  }
295
325
  return dirs;
296
326
  }
327
+ /** Extract a succinct summary from plan content (first heading + first body line, ~150 chars max). */
328
+ export function extractPlanSummary(content) {
329
+ if (!content?.trim())
330
+ return '(empty plan)';
331
+ const lines = content.split('\n');
332
+ let heading = '';
333
+ let body = '';
334
+ for (const line of lines) {
335
+ const trimmed = line.trim();
336
+ if (!trimmed)
337
+ continue;
338
+ if (!heading && trimmed.startsWith('#')) {
339
+ heading = trimmed.replace(/^#+\s*/, '');
340
+ continue;
341
+ }
342
+ if (!trimmed.startsWith('#') && !body) {
343
+ body = trimmed;
344
+ break;
345
+ }
346
+ }
347
+ if (heading && body) {
348
+ const full = `${heading}: ${body}`;
349
+ return full.length > 150 ? full.slice(0, 147) + '...' : full;
350
+ }
351
+ if (heading)
352
+ return heading.length > 150 ? heading.slice(0, 147) + '...' : heading;
353
+ if (body)
354
+ return body.length > 150 ? body.slice(0, 147) + '...' : body;
355
+ return '(empty plan)';
356
+ }
297
357
  export class SessionManager {
298
358
  bridge;
299
359
  channelSessions = new Map(); // channelId → sessionId
@@ -307,6 +367,8 @@ export class SessionManager {
307
367
  pendingUserInput = new Map();
308
368
  // Cached context usage from session.usage_info events
309
369
  contextUsage = new Map();
370
+ // Cached max_context_window_tokens from listModels() (separate from usage_info to avoid ordering issues)
371
+ contextWindowTokens = new Map();
310
372
  lastMessageUserIds = new Map(); // channelId → userId of last message sender
311
373
  // MCP server names that were passed to the session at creation/resume time
312
374
  sessionMcpServers = new Map(); // channelId → server names
@@ -314,6 +376,12 @@ export class SessionManager {
314
376
  sessionSkillDirs = new Map(); // channelId → skill dir paths
315
377
  // Loaded session hooks per workspace (cached after first load)
316
378
  workspaceHooks = new Map();
379
+ // Pending plan exit requests (one per channel)
380
+ pendingPlanExit = new Map();
381
+ // Debounce timers for plan_changed events
382
+ planChangedDebounce = new Map();
383
+ // Previous permissionMode before yolo was auto-enabled by plan exit
384
+ yoloPreviousState = new Map();
317
385
  // Handler for send_file tool (set by index.ts, calls adapter.sendFile)
318
386
  sendFileHandler = null;
319
387
  getAdapterForChannel = null;
@@ -408,19 +476,7 @@ export class SessionManager {
408
476
  */
409
477
  resolveMcpServers(workingDirectory) {
410
478
  const { servers: workspaceServers, env: workspaceEnv } = loadWorkspaceMcpServers(workingDirectory);
411
- // Clone global servers and inject workspace .env into local ones
412
- const merged = {};
413
- for (const [name, config] of Object.entries(this.mcpServers)) {
414
- const serverConfig = { ...config };
415
- const isLocal = !serverConfig.type || serverConfig.type === 'local' || serverConfig.type === 'stdio';
416
- if (isLocal && Object.keys(workspaceEnv).length > 0) {
417
- serverConfig.env = { ...workspaceEnv, ...(serverConfig.env || {}) };
418
- }
419
- merged[name] = serverConfig;
420
- }
421
- if (Object.keys(workspaceServers).length === 0)
422
- return merged;
423
- return { ...merged, ...workspaceServers };
479
+ return mergeMcpServers(this.mcpServers, workspaceServers, workspaceEnv, workingDirectory);
424
480
  }
425
481
  /** Get annotated MCP server info for a channel, showing which layer each server came from. */
426
482
  getMcpServerInfo(channelId) {
@@ -546,9 +602,17 @@ export class SessionManager {
546
602
  this.channelSessions.delete(channelId);
547
603
  this.sessionChannels.delete(existingId);
548
604
  this.contextUsage.delete(channelId);
605
+ this.contextWindowTokens.delete(channelId);
549
606
  this.lastMessageUserIds.delete(channelId);
550
607
  this.sessionMcpServers.delete(channelId);
551
608
  this.sessionSkillDirs.delete(channelId);
609
+ this.pendingPlanExit.delete(channelId);
610
+ this.revertYoloIfNeeded(channelId);
611
+ const debounceTimer = this.planChangedDebounce.get(channelId);
612
+ if (debounceTimer) {
613
+ clearTimeout(debounceTimer);
614
+ this.planChangedDebounce.delete(channelId);
615
+ }
552
616
  }
553
617
  clearChannelSession(channelId);
554
618
  return this.createNewSession(channelId);
@@ -575,9 +639,19 @@ export class SessionManager {
575
639
  this.mcpServers = loadMcpServers();
576
640
  // Re-attach the same session (re-reads workspace config, AGENTS.md, MCP, etc.)
577
641
  this.contextUsage.delete(channelId);
642
+ this.contextWindowTokens.delete(channelId);
578
643
  this.lastMessageUserIds.delete(channelId);
579
644
  this.sessionMcpServers.delete(channelId);
580
645
  this.sessionSkillDirs.delete(channelId);
646
+ this.pendingPlanExit.delete(channelId);
647
+ this.revertYoloIfNeeded(channelId);
648
+ {
649
+ const dt = this.planChangedDebounce.get(channelId);
650
+ if (dt) {
651
+ clearTimeout(dt);
652
+ this.planChangedDebounce.delete(channelId);
653
+ }
654
+ }
581
655
  try {
582
656
  await this.attachSession(channelId, existingId);
583
657
  log.info(`Reloaded session ${existingId} for channel ${channelId}`);
@@ -589,9 +663,12 @@ export class SessionManager {
589
663
  this.channelSessions.delete(channelId);
590
664
  this.sessionChannels.delete(existingId);
591
665
  this.contextUsage.delete(channelId);
666
+ this.contextWindowTokens.delete(channelId);
592
667
  this.lastMessageUserIds.delete(channelId);
593
668
  this.sessionMcpServers.delete(channelId);
594
669
  this.sessionSkillDirs.delete(channelId);
670
+ this.pendingPlanExit.delete(channelId);
671
+ // yoloPreviousState already reverted above
595
672
  clearChannelSession(channelId);
596
673
  return this.createNewSession(channelId);
597
674
  }
@@ -617,9 +694,19 @@ export class SessionManager {
617
694
  this.channelSessions.delete(channelId);
618
695
  this.sessionChannels.delete(existingId);
619
696
  this.contextUsage.delete(channelId);
697
+ this.contextWindowTokens.delete(channelId);
620
698
  this.lastMessageUserIds.delete(channelId);
621
699
  this.sessionMcpServers.delete(channelId);
622
700
  this.sessionSkillDirs.delete(channelId);
701
+ this.pendingPlanExit.delete(channelId);
702
+ this.revertYoloIfNeeded(channelId);
703
+ {
704
+ const dt = this.planChangedDebounce.get(channelId);
705
+ if (dt) {
706
+ clearTimeout(dt);
707
+ this.planChangedDebounce.delete(channelId);
708
+ }
709
+ }
623
710
  }
624
711
  // If target session is active on another channel, disconnect it first
625
712
  const otherChannel = this.sessionChannels.get(targetSessionId);
@@ -636,9 +723,19 @@ export class SessionManager {
636
723
  this.channelSessions.delete(otherChannel);
637
724
  this.sessionChannels.delete(targetSessionId);
638
725
  this.contextUsage.delete(otherChannel);
726
+ this.contextWindowTokens.delete(otherChannel);
639
727
  this.lastMessageUserIds.delete(otherChannel);
640
728
  this.sessionMcpServers.delete(otherChannel);
641
729
  this.sessionSkillDirs.delete(otherChannel);
730
+ this.pendingPlanExit.delete(otherChannel);
731
+ this.revertYoloIfNeeded(otherChannel);
732
+ {
733
+ const dt = this.planChangedDebounce.get(otherChannel);
734
+ if (dt) {
735
+ clearTimeout(dt);
736
+ this.planChangedDebounce.delete(otherChannel);
737
+ }
738
+ }
642
739
  clearChannelSession(otherChannel);
643
740
  }
644
741
  // Attach to the target session — fail hard if it doesn't exist
@@ -785,6 +882,16 @@ export class SessionManager {
785
882
  }
786
883
  }
787
884
  setChannelPrefs(channelId, { model });
885
+ // Clear stale context window tokens so /context falls back to tokenLimit during transition
886
+ this.contextWindowTokens.delete(channelId);
887
+ // Update cached context window tokens for the new model (best-effort)
888
+ this.bridge.listModels().then(models => {
889
+ // Guard against rapid switches: only cache if the channel is still on this model
890
+ const currentPrefs = this.getEffectivePrefs(channelId);
891
+ if (currentPrefs.model === model) {
892
+ this.cacheContextWindowTokens(channelId, model, models);
893
+ }
894
+ }).catch(() => { });
788
895
  }
789
896
  /** Switch the agent for a channel's session. */
790
897
  async switchAgent(channelId, agent) {
@@ -877,6 +984,94 @@ export class SessionManager {
877
984
  return false;
878
985
  }
879
986
  }
987
+ /**
988
+ * Extract a succinct summary from plan content.
989
+ * Returns the first heading + first 1-2 sentences, ~100-150 chars.
990
+ */
991
+ extractPlanSummary(content) {
992
+ return extractPlanSummary(content);
993
+ }
994
+ // Preferred models for plan summarization — balanced for quality and cost.
995
+ // Summarization needs accurate extraction without hallucination, so we
996
+ // prefer capable mid-tier models over the cheapest options.
997
+ // Matched against available models via exact-then-prefix so partial matches
998
+ // work even if the catalog adds suffixes or version bumps.
999
+ static SUMMARIZER_MODELS = [
1000
+ 'claude-sonnet-4.6', // strong comprehension, likely to stay available
1001
+ 'claude-sonnet-4.5', // strong comprehension
1002
+ 'gpt-4.1', // fast, capable
1003
+ 'gpt-5-mini', // smaller but still capable
1004
+ 'claude-haiku-4.5', // fastest Claude, adequate for short summaries
1005
+ 'gpt-5.1', // full-size fallback
1006
+ 'gpt-5.2', // full-size fallback
1007
+ ];
1008
+ /**
1009
+ * Summarize a plan using a lightweight ephemeral session.
1010
+ * Creates a throwaway session with a cheap model, sends the plan for summarization,
1011
+ * collects the streamed response, and destroys the session.
1012
+ */
1013
+ async summarizePlan(channelId) {
1014
+ const plan = await this.readPlan(channelId);
1015
+ if (!plan.exists || !plan.content)
1016
+ return null;
1017
+ // Pick the best available cheap model
1018
+ let availableIds = [];
1019
+ try {
1020
+ const models = await this.bridge.listModels();
1021
+ availableIds = models.map(m => m.id);
1022
+ }
1023
+ catch { /* best-effort */ }
1024
+ let model;
1025
+ for (const candidate of SessionManager.SUMMARIZER_MODELS) {
1026
+ const match = availableIds.find(id => id === candidate || id.startsWith(candidate));
1027
+ if (match) {
1028
+ model = match;
1029
+ break;
1030
+ }
1031
+ }
1032
+ // If none matched, leave model undefined — SDK picks its default
1033
+ log.info(`Summarizing plan for ${channelId.slice(0, 8)}... using model ${model ?? '(sdk default)'}`);
1034
+ let session;
1035
+ try {
1036
+ session = await this.bridge.createSession({
1037
+ ...(model ? { model } : {}),
1038
+ onPermissionRequest: async () => ({ kind: 'denied-interactively-by-user' }),
1039
+ systemMessage: { content: 'You are a concise summarizer. Respond with only the summary, no preamble.' },
1040
+ });
1041
+ const prompt = `Condense this plan into a short summary that preserves the main section headings (## level). Under each heading, write one sentence capturing the key point. Include any unresolved questions or TBDs. Omit code blocks, type definitions, and resolved questions. Keep the total under 500 characters.\n\n${plan.content}`;
1042
+ const response = await this.sendAndWaitForIdle(session, prompt, 30_000);
1043
+ return response.trim() || null;
1044
+ }
1045
+ catch (err) {
1046
+ log.warn(`Plan summarization failed: ${err?.message ?? err}`);
1047
+ return null;
1048
+ }
1049
+ finally {
1050
+ if (session) {
1051
+ try {
1052
+ await this.bridge.destroySession(session.sessionId);
1053
+ }
1054
+ catch { /* best-effort */ }
1055
+ }
1056
+ }
1057
+ }
1058
+ /**
1059
+ * Check for an existing plan and return summary if found.
1060
+ * Called after session resume to notify the user.
1061
+ */
1062
+ async surfacePlanIfExists(channelId) {
1063
+ const plan = await this.readPlan(channelId);
1064
+ if (!plan.exists || !plan.content)
1065
+ return null;
1066
+ const summary = this.extractPlanSummary(plan.content);
1067
+ let inPlanMode = false;
1068
+ try {
1069
+ const mode = await this.getSessionMode(channelId);
1070
+ inPlanMode = mode === 'plan';
1071
+ }
1072
+ catch { /* best-effort */ }
1073
+ return { exists: true, summary, inPlanMode };
1074
+ }
880
1075
  /** Check if the Copilot CLI is authenticated. */
881
1076
  async getAuthStatus() {
882
1077
  try {
@@ -965,6 +1160,49 @@ export class SessionManager {
965
1160
  const queue = this.pendingPermissions.get(channelId);
966
1161
  return !!queue && queue.length > 0 && !!queue[0].fromHook;
967
1162
  }
1163
+ /** Store a pending plan exit request. */
1164
+ setPendingPlanExit(channelId, pending) {
1165
+ this.pendingPlanExit.set(channelId, pending);
1166
+ }
1167
+ /** Check if channel has a pending plan exit request. */
1168
+ hasPendingPlanExit(channelId) {
1169
+ return this.pendingPlanExit.has(channelId);
1170
+ }
1171
+ /** Get and clear the pending plan exit request. */
1172
+ consumePendingPlanExit(channelId) {
1173
+ const pending = this.pendingPlanExit.get(channelId);
1174
+ if (pending)
1175
+ this.pendingPlanExit.delete(channelId);
1176
+ return pending;
1177
+ }
1178
+ /** Save previous permission mode before auto-enabling yolo. */
1179
+ saveYoloPreviousState(channelId) {
1180
+ const prefs = getChannelPrefs(channelId);
1181
+ const channelConfig = getChannelConfig(channelId);
1182
+ const current = prefs?.permissionMode ?? channelConfig.permissionMode;
1183
+ this.yoloPreviousState.set(channelId, current);
1184
+ }
1185
+ /** Revert yolo to previous state (called on session.idle after plan implementation). */
1186
+ revertYoloIfNeeded(channelId) {
1187
+ const previous = this.yoloPreviousState.get(channelId);
1188
+ if (previous === undefined)
1189
+ return false;
1190
+ this.yoloPreviousState.delete(channelId);
1191
+ setChannelPrefs(channelId, { permissionMode: previous });
1192
+ return true;
1193
+ }
1194
+ /** Debounced plan change handler — calls callback after debounce window. */
1195
+ debouncePlanChanged(channelId, callback, delayMs = 3000) {
1196
+ const existing = this.planChangedDebounce.get(channelId);
1197
+ if (existing)
1198
+ clearTimeout(existing);
1199
+ this.planChangedDebounce.set(channelId, setTimeout(() => {
1200
+ this.planChangedDebounce.delete(channelId);
1201
+ void Promise.resolve(callback()).catch((err) => {
1202
+ log.warn(`Debounced plan callback failed for ${channelId.slice(0, 8)}...: ${err}`);
1203
+ });
1204
+ }, delayMs));
1205
+ }
968
1206
  /** Get the current session ID for a channel (if any). */
969
1207
  getSessionId(channelId) {
970
1208
  return this.channelSessions.get(channelId) ?? getChannelSession(channelId) ?? undefined;
@@ -991,7 +1229,19 @@ export class SessionManager {
991
1229
  }
992
1230
  /** Get cached context window usage for a channel. */
993
1231
  getContextUsage(channelId) {
994
- return this.contextUsage.get(channelId) ?? null;
1232
+ const usage = this.contextUsage.get(channelId);
1233
+ if (!usage)
1234
+ return null;
1235
+ const ctxWindow = this.contextWindowTokens.get(channelId);
1236
+ return ctxWindow ? { ...usage, contextWindowTokens: ctxWindow } : usage;
1237
+ }
1238
+ /** Cache the model's max_context_window_tokens for accurate /context display. */
1239
+ cacheContextWindowTokens(channelId, modelId, modelList) {
1240
+ const model = modelList.find((m) => m.id === modelId);
1241
+ const ctxTokens = model?.capabilities?.limits?.max_context_window_tokens;
1242
+ if (typeof ctxTokens === 'number' && ctxTokens > 0) {
1243
+ this.contextWindowTokens.set(channelId, ctxTokens);
1244
+ }
995
1245
  }
996
1246
  /**
997
1247
  * Resolve a session ID prefix to matching full session IDs.
@@ -1042,9 +1292,10 @@ export class SessionManager {
1042
1292
  const configFallbacks = configChannel.fallbackModels ?? getConfig().defaults.fallbackModels;
1043
1293
  // Fetch available models for fallback chain (best-effort — don't block on failure)
1044
1294
  let availableModels = [];
1295
+ let modelList = [];
1045
1296
  try {
1046
- const models = await this.bridge.listModels();
1047
- availableModels = models.map(m => m.id);
1297
+ modelList = await this.bridge.listModels();
1298
+ availableModels = modelList.map(m => m.id);
1048
1299
  }
1049
1300
  catch {
1050
1301
  log.warn('Failed to fetch model list for fallback resolution');
@@ -1074,6 +1325,7 @@ export class SessionManager {
1074
1325
  const { result: session, usedModel, didFallback } = await tryWithFallback(prefs.model, availableModels, configFallbacks, createWithModel);
1075
1326
  this.sessionMcpServers.set(channelId, new Set(Object.keys(resolvedMcpServers)));
1076
1327
  this.sessionSkillDirs.set(channelId, new Set(skillDirectories));
1328
+ this.cacheContextWindowTokens(channelId, usedModel, modelList);
1077
1329
  if (didFallback) {
1078
1330
  log.info(`Model fallback: "${prefs.model}" → "${usedModel}" for channel ${channelId}`);
1079
1331
  setChannelPrefs(channelId, { model: usedModel });
@@ -1125,6 +1377,15 @@ export class SessionManager {
1125
1377
  this.channelSessions.set(channelId, sessionId);
1126
1378
  this.sessionChannels.set(sessionId, channelId);
1127
1379
  this.attachSessionEvents(session, channelId);
1380
+ // Cache context window tokens for /context display (best-effort, non-blocking)
1381
+ const resumeModel = prefs.model;
1382
+ this.bridge.listModels().then(models => {
1383
+ // Guard against model changes before this resolves
1384
+ const currentPrefs = this.getEffectivePrefs(channelId);
1385
+ if (currentPrefs.model === resumeModel) {
1386
+ this.cacheContextWindowTokens(channelId, resumeModel, models);
1387
+ }
1388
+ }).catch(() => { });
1128
1389
  }
1129
1390
  /**
1130
1391
  * Execute an ephemeral inter-agent call: create a fresh session for the target bot,
@@ -1761,6 +2022,8 @@ export class SessionManager {
1761
2022
  }
1762
2023
  // Scheduler tool: create/list/cancel/pause/resume scheduled tasks
1763
2024
  tools.push(this.buildScheduleToolDef(channelId));
2025
+ // Bridge documentation tool
2026
+ tools.push(this.buildBridgeDocsTool(channelId));
1764
2027
  if (tools.length > 0) {
1765
2028
  log.info(`Built ${tools.length} custom tool(s) for channel ${channelId.slice(0, 8)}...`);
1766
2029
  }
@@ -1877,6 +2140,38 @@ export class SessionManager {
1877
2140
  },
1878
2141
  };
1879
2142
  }
2143
+ buildBridgeDocsTool(channelId) {
2144
+ const channelConfig = getChannelConfig(channelId);
2145
+ const botName = getChannelBotName(channelId);
2146
+ const isAdmin = isBotAdmin(channelConfig.platform, botName);
2147
+ return {
2148
+ name: 'fetch_copilot_bridge_documentation',
2149
+ description: 'Fetch documentation about copilot-bridge capabilities, commands, configuration, and system status. Use this when you need to understand bridge features, help users with commands, troubleshoot issues, or check system status. Call with a specific topic to get focused information. If the documentation doesn\'t fully answer your question, each topic includes source code pointers for deeper investigation.',
2150
+ parameters: {
2151
+ type: 'object',
2152
+ properties: {
2153
+ topic: {
2154
+ type: 'string',
2155
+ enum: ['overview', 'commands', 'config', 'mcp', 'permissions', 'workspaces', 'hooks', 'skills', 'inter-agent', 'scheduling', 'troubleshooting', 'status'],
2156
+ description: "Topic to query. 'overview' = what the bridge is and key features. 'commands' = common slash commands. 'config' = configuration options. 'mcp' = MCP server setup. 'permissions' = permission system. 'workspaces' = workspace structure. 'hooks' = tool hooks. 'skills' = skill discovery. 'inter-agent' = bot-to-bot communication. 'scheduling' = task scheduling. 'troubleshooting' = common issues. 'status' = live system state.",
2157
+ },
2158
+ },
2159
+ required: [],
2160
+ },
2161
+ handler: async (args) => {
2162
+ const sessionInfo = this.getSessionInfo(channelId);
2163
+ return {
2164
+ content: getBridgeDocs({
2165
+ topic: args.topic,
2166
+ isAdmin,
2167
+ channelId,
2168
+ model: sessionInfo?.model,
2169
+ sessionId: sessionInfo?.sessionId,
2170
+ }),
2171
+ };
2172
+ },
2173
+ };
2174
+ }
1880
2175
  attachSessionEvents(session, channelId) {
1881
2176
  const unsub = session.on((event) => {
1882
2177
  if (event.type === 'session.usage_info' && event.data) {