kimaki 0.2.0 → 0.2.1

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.
@@ -27,11 +27,8 @@ const dbLogger = createLogger('DB');
27
27
  const opencodeServers = new Map();
28
28
  // Map of session ID to current AbortController
29
29
  const abortControllers = new Map();
30
- // Map of guild ID to voice connection
30
+ // Map of guild ID to voice connection and GenAI worker
31
31
  const voiceConnections = new Map();
32
- // Map of channel ID to GenAI worker and session state
33
- // This allows sessions to persist across voice channel changes
34
- const genAiSessions = new Map();
35
32
  // Map of directory to retry count for server restarts
36
33
  const serverRetryCount = new Map();
37
34
  let db = null;
@@ -82,7 +79,7 @@ async function createUserAudioLogStream(guildId, channelId) {
82
79
  }
83
80
  }
84
81
  // Set up voice handling for a connection (called once per connection)
85
- async function setupVoiceHandling({ connection, guildId, channelId, appId, cleanupGenAiSession, }) {
82
+ async function setupVoiceHandling({ connection, guildId, channelId, appId, }) {
86
83
  voiceLogger.log(`Setting up voice handling for guild ${guildId}, channel ${channelId}`);
87
84
  // Check if this voice channel has an associated directory
88
85
  const channelDirRow = getDatabase()
@@ -102,28 +99,11 @@ async function setupVoiceHandling({ connection, guildId, channelId, appId, clean
102
99
  }
103
100
  // Create user audio stream for debugging
104
101
  voiceData.userAudioStream = await createUserAudioLogStream(guildId, channelId);
105
- // Check if we already have a GenAI session for this channel
106
- const existingSession = genAiSessions.get(channelId);
107
- if (existingSession) {
108
- voiceLogger.log(`Reusing existing GenAI session for channel ${channelId}`);
109
- // Cancel any pending cleanup since user has returned
110
- if (existingSession.pendingCleanup) {
111
- voiceLogger.log(`Cancelling pending cleanup for channel ${channelId} - user returned`);
112
- existingSession.pendingCleanup = false;
113
- if (existingSession.cleanupTimer) {
114
- clearTimeout(existingSession.cleanupTimer);
115
- existingSession.cleanupTimer = undefined;
116
- }
117
- }
118
- // Session already exists, just update the voice handling
119
- return;
120
- }
121
102
  // Get API keys from database
122
103
  const apiKeys = getDatabase()
123
104
  .prepare('SELECT gemini_api_key FROM bot_api_keys WHERE app_id = ?')
124
105
  .get(appId);
125
- // Track if sessions are active
126
- let hasActiveSessions = false;
106
+ // Create GenAI worker
127
107
  const genAiWorker = await createGenAIWorker({
128
108
  directory,
129
109
  guildId,
@@ -191,75 +171,28 @@ async function setupVoiceHandling({ connection, guildId, channelId, appId, clean
191
171
  genAiWorker.interrupt();
192
172
  connection.setSpeaking(false);
193
173
  },
194
- onAllSessionsCompleted() {
195
- // All OpenCode sessions have completed
196
- hasActiveSessions = false;
197
- voiceLogger.log('All OpenCode sessions completed for this GenAI session');
198
- // Update the stored session state
199
- const session = genAiSessions.get(channelId);
200
- if (session) {
201
- session.hasActiveSessions = false;
202
- // If cleanup is pending (user left channel), schedule cleanup with grace period
203
- if (session.pendingCleanup) {
204
- voiceLogger.log(`Scheduling cleanup for channel ${channelId} in 1 minute`);
205
- // Clear any existing timer
206
- if (session.cleanupTimer) {
207
- clearTimeout(session.cleanupTimer);
208
- }
209
- // Schedule cleanup after 1 minute grace period
210
- session.cleanupTimer = setTimeout(() => {
211
- // Double-check that cleanup is still needed
212
- const currentSession = genAiSessions.get(channelId);
213
- if (currentSession?.pendingCleanup &&
214
- !currentSession.hasActiveSessions) {
215
- voiceLogger.log(`Grace period expired, cleaning up GenAI session for channel ${channelId}`);
216
- // Use the main cleanup function - defined later in startDiscordBot
217
- cleanupGenAiSession(channelId);
218
- }
219
- }, 60000); // 1 minute
220
- }
221
- }
222
- },
223
174
  onToolCallCompleted(params) {
224
- // Note: We now track at the tools.ts level, but still handle completion messages
225
- voiceLogger.log(`OpenCode session ${params.sessionId} completed`);
226
175
  const text = params.error
227
176
  ? `<systemMessage>\nThe coding agent encountered an error while processing session ${params.sessionId}: ${params.error?.message || String(params.error)}\n</systemMessage>`
228
177
  : `<systemMessage>\nThe coding agent finished working on session ${params.sessionId}\n\nHere's what the assistant wrote:\n${params.markdown}\n</systemMessage>`;
229
178
  genAiWorker.sendTextInput(text);
230
- // Mark that we have active sessions (will be updated by onAllSessionsCompleted when done)
231
- hasActiveSessions = true;
232
- const session = genAiSessions.get(channelId);
233
- if (session) {
234
- session.hasActiveSessions = true;
235
- }
236
179
  },
237
180
  onError(error) {
238
181
  voiceLogger.error('GenAI worker error:', error);
239
182
  },
240
183
  });
184
+ // Stop any existing GenAI worker before storing new one
185
+ if (voiceData.genAiWorker) {
186
+ voiceLogger.log('Stopping existing GenAI worker before creating new one');
187
+ await voiceData.genAiWorker.stop();
188
+ }
241
189
  // Send initial greeting
242
190
  genAiWorker.sendTextInput(`<systemMessage>\nsay "Hello boss, how we doing today?"\n</systemMessage>`);
243
- // Store the GenAI session
244
- genAiSessions.set(channelId, {
245
- genAiWorker,
246
- hasActiveSessions,
247
- pendingCleanup: false,
248
- cleanupTimer: undefined,
249
- guildId,
250
- channelId,
251
- directory,
252
- });
191
+ voiceData.genAiWorker = genAiWorker;
253
192
  // Set up voice receiver for user input
254
193
  const receiver = connection.receiver;
255
194
  // Remove all existing listeners to prevent accumulation
256
195
  receiver.speaking.removeAllListeners('start');
257
- // Get the GenAI session for this channel
258
- const genAiSession = genAiSessions.get(channelId);
259
- if (!genAiSession) {
260
- voiceLogger.error(`GenAI session was just created but not found for channel ${channelId}`);
261
- return;
262
- }
263
196
  // Counter to track overlapping speaking sessions
264
197
  let speakingSessionCount = 0;
265
198
  receiver.speaking.on('start', (userId) => {
@@ -306,16 +239,15 @@ async function setupVoiceHandling({ connection, guildId, channelId, appId, clean
306
239
  // )
307
240
  return;
308
241
  }
309
- const genAiSession = genAiSessions.get(channelId);
310
- if (!genAiSession) {
311
- voiceLogger.warn(`[VOICE] Received audio frame but no GenAI session active for channel ${channelId}`);
242
+ if (!voiceData.genAiWorker) {
243
+ voiceLogger.warn(`[VOICE] Received audio frame but no GenAI worker active for guild ${guildId}`);
312
244
  return;
313
245
  }
314
246
  // voiceLogger.debug('User audio chunk length', frame.length)
315
247
  // Write to PCM file if stream exists
316
248
  voiceData.userAudioStream?.write(frame);
317
249
  // stream incrementally — low latency
318
- genAiSession.genAiWorker.sendRealtimeInput({
250
+ voiceData.genAiWorker.sendRealtimeInput({
319
251
  audio: {
320
252
  mimeType: 'audio/pcm;rate=16000',
321
253
  data: frame.toString('base64'),
@@ -326,8 +258,7 @@ async function setupVoiceHandling({ connection, guildId, channelId, appId, clean
326
258
  // Only send audioStreamEnd if this is still the current session
327
259
  if (currentSessionCount === speakingSessionCount) {
328
260
  voiceLogger.log(`User ${userId} stopped speaking (session ${currentSessionCount})`);
329
- const genAiSession = genAiSessions.get(channelId);
330
- genAiSession?.genAiWorker.sendRealtimeInput({
261
+ voiceData.genAiWorker?.sendRealtimeInput({
331
262
  audioStreamEnd: true,
332
263
  });
333
264
  }
@@ -718,17 +649,12 @@ export async function initializeOpencodeForDirectory(directory) {
718
649
  }
719
650
  });
720
651
  await waitForServer(port);
721
- // Create a custom fetch that disables Bun's default timeout
722
- const customFetch = (input, init) => {
723
- return fetch(input, {
724
- ...init,
725
- // @ts-ignore - Bun-specific option to disable timeout
726
- timeout: false,
727
- });
728
- };
729
652
  const client = createOpencodeClient({
730
653
  baseUrl: `http://localhost:${port}`,
731
- fetch: customFetch,
654
+ fetch: (request) => fetch(request, {
655
+ // @ts-ignore
656
+ timeout: false,
657
+ }),
732
658
  });
733
659
  opencodeServers.set(directory, {
734
660
  process: serverProcess,
@@ -1737,37 +1663,19 @@ export async function startDiscordBot({ token, appId, discordClient, }) {
1737
1663
  voiceLogger.error('[INTERACTION] Error handling interaction:', error);
1738
1664
  }
1739
1665
  });
1740
- // Helper function to clean up GenAI session
1741
- async function cleanupGenAiSession(channelId) {
1742
- const genAiSession = genAiSessions.get(channelId);
1743
- if (!genAiSession)
1744
- return;
1745
- try {
1746
- // Clear any cleanup timer
1747
- if (genAiSession.cleanupTimer) {
1748
- clearTimeout(genAiSession.cleanupTimer);
1749
- genAiSession.cleanupTimer = undefined;
1750
- }
1751
- voiceLogger.log(`Stopping GenAI worker for channel ${channelId}...`);
1752
- await genAiSession.genAiWorker.stop();
1753
- voiceLogger.log(`GenAI worker stopped for channel ${channelId}`);
1754
- // Remove from map
1755
- genAiSessions.delete(channelId);
1756
- voiceLogger.log(`GenAI session cleanup complete for channel ${channelId}`);
1757
- }
1758
- catch (error) {
1759
- voiceLogger.error(`Error during GenAI session cleanup for channel ${channelId}:`, error);
1760
- // Still remove from map even if there was an error
1761
- genAiSessions.delete(channelId);
1762
- }
1763
- }
1764
1666
  // Helper function to clean up voice connection and associated resources
1765
- async function cleanupVoiceConnection(guildId, channelId) {
1667
+ async function cleanupVoiceConnection(guildId) {
1766
1668
  const voiceData = voiceConnections.get(guildId);
1767
1669
  if (!voiceData)
1768
1670
  return;
1769
1671
  voiceLogger.log(`Starting cleanup for guild ${guildId}`);
1770
1672
  try {
1673
+ // Stop GenAI worker if exists (this is async!)
1674
+ if (voiceData.genAiWorker) {
1675
+ voiceLogger.log(`Stopping GenAI worker...`);
1676
+ await voiceData.genAiWorker.stop();
1677
+ voiceLogger.log(`GenAI worker stopped`);
1678
+ }
1771
1679
  // Close user audio stream if exists
1772
1680
  if (voiceData.userAudioStream) {
1773
1681
  voiceLogger.log(`Closing user audio stream...`);
@@ -1787,33 +1695,7 @@ export async function startDiscordBot({ token, appId, discordClient, }) {
1787
1695
  }
1788
1696
  // Remove from map
1789
1697
  voiceConnections.delete(guildId);
1790
- voiceLogger.log(`Voice connection cleanup complete for guild ${guildId}`);
1791
- // Mark the GenAI session for cleanup when all sessions complete
1792
- if (channelId) {
1793
- const genAiSession = genAiSessions.get(channelId);
1794
- if (genAiSession) {
1795
- voiceLogger.log(`Marking channel ${channelId} for cleanup when sessions complete`);
1796
- genAiSession.pendingCleanup = true;
1797
- // If no active sessions, trigger cleanup immediately (with grace period)
1798
- if (!genAiSession.hasActiveSessions) {
1799
- voiceLogger.log(`No active sessions, scheduling cleanup for channel ${channelId} in 1 minute`);
1800
- // Clear any existing timer
1801
- if (genAiSession.cleanupTimer) {
1802
- clearTimeout(genAiSession.cleanupTimer);
1803
- }
1804
- // Schedule cleanup after 1 minute grace period
1805
- genAiSession.cleanupTimer = setTimeout(() => {
1806
- // Double-check that cleanup is still needed
1807
- const currentSession = genAiSessions.get(channelId);
1808
- if (currentSession?.pendingCleanup &&
1809
- !currentSession.hasActiveSessions) {
1810
- voiceLogger.log(`Grace period expired, cleaning up GenAI session for channel ${channelId}`);
1811
- cleanupGenAiSession(channelId);
1812
- }
1813
- }, 60000); // 1 minute
1814
- }
1815
- }
1816
- }
1698
+ voiceLogger.log(`Cleanup complete for guild ${guildId}`);
1817
1699
  }
1818
1700
  catch (error) {
1819
1701
  voiceLogger.error(`Error during cleanup for guild ${guildId}:`, error);
@@ -1856,7 +1738,7 @@ export async function startDiscordBot({ token, appId, discordClient, }) {
1856
1738
  if (!hasOtherAdmins) {
1857
1739
  voiceLogger.log(`No other admins in channel, bot leaving voice channel in guild: ${guild.name}`);
1858
1740
  // Properly clean up all resources
1859
- await cleanupVoiceConnection(guildId, oldState.channelId);
1741
+ await cleanupVoiceConnection(guildId);
1860
1742
  }
1861
1743
  else {
1862
1744
  voiceLogger.log(`Other admins still in channel, bot staying in voice channel`);
@@ -1892,15 +1774,6 @@ export async function startDiscordBot({ token, appId, discordClient, }) {
1892
1774
  selfDeaf: false,
1893
1775
  selfMute: false,
1894
1776
  });
1895
- // Set up voice handling for the new channel
1896
- // This will reuse existing GenAI session if one exists
1897
- await setupVoiceHandling({
1898
- connection: voiceData.connection,
1899
- guildId,
1900
- channelId: voiceChannel.id,
1901
- appId: currentAppId,
1902
- cleanupGenAiSession,
1903
- });
1904
1777
  }
1905
1778
  }
1906
1779
  else {
@@ -1959,7 +1832,6 @@ export async function startDiscordBot({ token, appId, discordClient, }) {
1959
1832
  guildId: newState.guild.id,
1960
1833
  channelId: voiceChannel.id,
1961
1834
  appId: currentAppId,
1962
- cleanupGenAiSession,
1963
1835
  });
1964
1836
  // Handle connection state changes
1965
1837
  connection.on(VoiceConnectionStatus.Disconnected, async () => {
@@ -1982,7 +1854,7 @@ export async function startDiscordBot({ token, appId, discordClient, }) {
1982
1854
  connection.on(VoiceConnectionStatus.Destroyed, async () => {
1983
1855
  voiceLogger.log(`Connection destroyed for guild: ${newState.guild.name}`);
1984
1856
  // Use the cleanup function to ensure everything is properly closed
1985
- await cleanupVoiceConnection(newState.guild.id, voiceChannel.id);
1857
+ await cleanupVoiceConnection(newState.guild.id);
1986
1858
  });
1987
1859
  // Handle errors
1988
1860
  connection.on('error', (error) => {
@@ -1991,7 +1863,7 @@ export async function startDiscordBot({ token, appId, discordClient, }) {
1991
1863
  }
1992
1864
  catch (error) {
1993
1865
  voiceLogger.error(`Failed to join voice channel:`, error);
1994
- await cleanupVoiceConnection(newState.guild.id, voiceChannel.id);
1866
+ await cleanupVoiceConnection(newState.guild.id);
1995
1867
  }
1996
1868
  }
1997
1869
  catch (error) {
@@ -2009,31 +1881,17 @@ export async function startDiscordBot({ token, appId, discordClient, }) {
2009
1881
  ;
2010
1882
  global.shuttingDown = true;
2011
1883
  try {
2012
- // Clean up all voice connections
2013
- const voiceCleanupPromises = [];
2014
- for (const [guildId, voiceData] of voiceConnections) {
1884
+ // Clean up all voice connections (this includes GenAI workers and audio streams)
1885
+ const cleanupPromises = [];
1886
+ for (const [guildId] of voiceConnections) {
2015
1887
  voiceLogger.log(`[SHUTDOWN] Cleaning up voice connection for guild ${guildId}`);
2016
- // Find the channel ID for this connection
2017
- const channelId = voiceData.connection.joinConfig.channelId || undefined;
2018
- voiceCleanupPromises.push(cleanupVoiceConnection(guildId, channelId));
2019
- }
2020
- // Wait for all voice cleanups to complete
2021
- if (voiceCleanupPromises.length > 0) {
2022
- voiceLogger.log(`[SHUTDOWN] Waiting for ${voiceCleanupPromises.length} voice connection(s) to clean up...`);
2023
- await Promise.allSettled(voiceCleanupPromises);
2024
- discordLogger.log(`All voice connections cleaned up`);
1888
+ cleanupPromises.push(cleanupVoiceConnection(guildId));
2025
1889
  }
2026
- // Clean up all GenAI sessions (force cleanup regardless of active sessions)
2027
- const genAiCleanupPromises = [];
2028
- for (const [channelId, session] of genAiSessions) {
2029
- voiceLogger.log(`[SHUTDOWN] Cleaning up GenAI session for channel ${channelId} (active sessions: ${session.hasActiveSessions})`);
2030
- genAiCleanupPromises.push(cleanupGenAiSession(channelId));
2031
- }
2032
- // Wait for all GenAI cleanups to complete
2033
- if (genAiCleanupPromises.length > 0) {
2034
- voiceLogger.log(`[SHUTDOWN] Waiting for ${genAiCleanupPromises.length} GenAI session(s) to clean up...`);
2035
- await Promise.allSettled(genAiCleanupPromises);
2036
- discordLogger.log(`All GenAI sessions cleaned up`);
1890
+ // Wait for all cleanups to complete
1891
+ if (cleanupPromises.length > 0) {
1892
+ voiceLogger.log(`[SHUTDOWN] Waiting for ${cleanupPromises.length} voice connection(s) to clean up...`);
1893
+ await Promise.allSettled(cleanupPromises);
1894
+ discordLogger.log(`All voice connections cleaned up`);
2037
1895
  }
2038
1896
  // Kill all OpenCode servers
2039
1897
  for (const [dir, server] of opencodeServers) {
@@ -20,9 +20,6 @@ export function createGenAIWorker(options) {
20
20
  case 'assistantInterruptSpeaking':
21
21
  options.onAssistantInterruptSpeaking?.();
22
22
  break;
23
- case 'allSessionsCompleted':
24
- options.onAllSessionsCompleted?.();
25
- break;
26
23
  case 'toolCallCompleted':
27
24
  options.onToolCallCompleted?.(message);
28
25
  break;
@@ -205,11 +205,6 @@ parentPort.on('message', async (message) => {
205
205
  ...params,
206
206
  });
207
207
  },
208
- onAllSessionsCompleted: () => {
209
- parentPort.postMessage({
210
- type: 'allSessionsCompleted',
211
- });
212
- },
213
208
  });
214
209
  // Start GenAI session
215
210
  session = await startGenAiSession({
package/dist/tools.js CHANGED
@@ -9,14 +9,12 @@ import { formatDistanceToNow } from 'date-fns';
9
9
  import { ShareMarkdown } from './markdown.js';
10
10
  import pc from 'picocolors';
11
11
  import { initializeOpencodeForDirectory } from './discordBot.js';
12
- export async function getTools({ onMessageCompleted, onAllSessionsCompleted, directory, }) {
12
+ export async function getTools({ onMessageCompleted, directory, }) {
13
13
  const getClient = await initializeOpencodeForDirectory(directory);
14
14
  const client = getClient();
15
15
  const markdownRenderer = new ShareMarkdown(client);
16
16
  const providersResponse = await client.config.providers({});
17
17
  const providers = providersResponse.data?.providers || [];
18
- // Track all active OpenCode sessions
19
- const activeSessions = new Set();
20
18
  // Helper: get last assistant model for a session (non-summary)
21
19
  const getSessionModel = async (sessionId) => {
22
20
  const res = await getClient().session.messages({ path: { id: sessionId } });
@@ -43,9 +41,6 @@ export async function getTools({ onMessageCompleted, onAllSessionsCompleted, dir
43
41
  }),
44
42
  execute: async ({ sessionId, message }) => {
45
43
  const sessionModel = await getSessionModel(sessionId);
46
- // Track this session as active
47
- activeSessions.add(sessionId);
48
- toolsLogger.log(`Session ${sessionId} started, ${activeSessions.size} active sessions`);
49
44
  // do not await
50
45
  getClient()
51
46
  .session.prompt({
@@ -66,14 +61,6 @@ export async function getTools({ onMessageCompleted, onAllSessionsCompleted, dir
66
61
  data: response.data,
67
62
  markdown,
68
63
  });
69
- // Remove from active sessions
70
- activeSessions.delete(sessionId);
71
- toolsLogger.log(`Session ${sessionId} completed, ${activeSessions.size} active sessions remaining`);
72
- // Check if all sessions are complete
73
- if (activeSessions.size === 0) {
74
- toolsLogger.log('All sessions completed');
75
- onAllSessionsCompleted?.();
76
- }
77
64
  })
78
65
  .catch((error) => {
79
66
  onMessageCompleted?.({
@@ -81,14 +68,6 @@ export async function getTools({ onMessageCompleted, onAllSessionsCompleted, dir
81
68
  messageId: '',
82
69
  error,
83
70
  });
84
- // Remove from active sessions even on error
85
- activeSessions.delete(sessionId);
86
- toolsLogger.log(`Session ${sessionId} failed, ${activeSessions.size} active sessions remaining`);
87
- // Check if all sessions are complete
88
- if (activeSessions.size === 0) {
89
- toolsLogger.log('All sessions completed');
90
- onAllSessionsCompleted?.();
91
- }
92
71
  });
93
72
  return {
94
73
  success: true,
@@ -129,52 +108,32 @@ export async function getTools({ onMessageCompleted, onAllSessionsCompleted, dir
129
108
  if (!session.data) {
130
109
  throw new Error('Failed to create session');
131
110
  }
132
- const newSessionId = session.data.id;
133
- // Track this session as active
134
- activeSessions.add(newSessionId);
135
- toolsLogger.log(`New session ${newSessionId} created, ${activeSessions.size} active sessions`);
136
111
  // do not await
137
112
  getClient()
138
113
  .session.prompt({
139
- path: { id: newSessionId },
114
+ path: { id: session.data.id },
140
115
  body: {
141
116
  parts: [{ type: 'text', text: message }],
142
117
  },
143
118
  })
144
119
  .then(async (response) => {
145
120
  const markdown = await markdownRenderer.generate({
146
- sessionID: newSessionId,
121
+ sessionID: session.data.id,
147
122
  lastAssistantOnly: true,
148
123
  });
149
124
  onMessageCompleted?.({
150
- sessionId: newSessionId,
125
+ sessionId: session.data.id,
151
126
  messageId: '',
152
127
  data: response.data,
153
128
  markdown,
154
129
  });
155
- // Remove from active sessions
156
- activeSessions.delete(newSessionId);
157
- toolsLogger.log(`Session ${newSessionId} completed, ${activeSessions.size} active sessions remaining`);
158
- // Check if all sessions are complete
159
- if (activeSessions.size === 0) {
160
- toolsLogger.log('All sessions completed');
161
- onAllSessionsCompleted?.();
162
- }
163
130
  })
164
131
  .catch((error) => {
165
132
  onMessageCompleted?.({
166
- sessionId: newSessionId,
133
+ sessionId: session.data.id,
167
134
  messageId: '',
168
135
  error,
169
136
  });
170
- // Remove from active sessions even on error
171
- activeSessions.delete(newSessionId);
172
- toolsLogger.log(`Session ${newSessionId} failed, ${activeSessions.size} active sessions remaining`);
173
- // Check if all sessions are complete
174
- if (activeSessions.size === 0) {
175
- toolsLogger.log('All sessions completed');
176
- onAllSessionsCompleted?.();
177
- }
178
137
  });
179
138
  return {
180
139
  success: true,
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "kimaki",
3
3
  "module": "index.ts",
4
4
  "type": "module",
5
- "version": "0.2.0",
5
+ "version": "0.2.1",
6
6
  "repository": "https://github.com/remorses/kimaki",
7
7
  "bin": "bin.js",
8
8
  "files": [
package/src/discordBot.ts CHANGED
@@ -70,30 +70,16 @@ const opencodeServers = new Map<
70
70
  // Map of session ID to current AbortController
71
71
  const abortControllers = new Map<string, AbortController>()
72
72
 
73
- // Map of guild ID to voice connection
73
+ // Map of guild ID to voice connection and GenAI worker
74
74
  const voiceConnections = new Map<
75
75
  string,
76
76
  {
77
77
  connection: VoiceConnection
78
+ genAiWorker?: GenAIWorker
78
79
  userAudioStream?: fs.WriteStream
79
80
  }
80
81
  >()
81
82
 
82
- // Map of channel ID to GenAI worker and session state
83
- // This allows sessions to persist across voice channel changes
84
- const genAiSessions = new Map<
85
- string,
86
- {
87
- genAiWorker: GenAIWorker
88
- hasActiveSessions: boolean // Track if there are active OpenCode sessions
89
- pendingCleanup: boolean // Track if cleanup was requested (user left channel)
90
- cleanupTimer?: NodeJS.Timeout // Timer for delayed cleanup
91
- guildId: string
92
- channelId: string
93
- directory: string
94
- }
95
- >()
96
-
97
83
  // Map of directory to retry count for server restarts
98
84
  const serverRetryCount = new Map<string, number>()
99
85
 
@@ -168,13 +154,11 @@ async function setupVoiceHandling({
168
154
  guildId,
169
155
  channelId,
170
156
  appId,
171
- cleanupGenAiSession,
172
157
  }: {
173
158
  connection: VoiceConnection
174
159
  guildId: string
175
160
  channelId: string
176
161
  appId: string
177
- cleanupGenAiSession: (channelId: string) => Promise<void>
178
162
  }) {
179
163
  voiceLogger.log(
180
164
  `Setting up voice handling for guild ${guildId}, channel ${channelId}`,
@@ -207,36 +191,12 @@ async function setupVoiceHandling({
207
191
  // Create user audio stream for debugging
208
192
  voiceData.userAudioStream = await createUserAudioLogStream(guildId, channelId)
209
193
 
210
- // Check if we already have a GenAI session for this channel
211
- const existingSession = genAiSessions.get(channelId)
212
- if (existingSession) {
213
- voiceLogger.log(`Reusing existing GenAI session for channel ${channelId}`)
214
-
215
- // Cancel any pending cleanup since user has returned
216
- if (existingSession.pendingCleanup) {
217
- voiceLogger.log(
218
- `Cancelling pending cleanup for channel ${channelId} - user returned`,
219
- )
220
- existingSession.pendingCleanup = false
221
-
222
- if (existingSession.cleanupTimer) {
223
- clearTimeout(existingSession.cleanupTimer)
224
- existingSession.cleanupTimer = undefined
225
- }
226
- }
227
-
228
- // Session already exists, just update the voice handling
229
- return
230
- }
231
-
232
194
  // Get API keys from database
233
195
  const apiKeys = getDatabase()
234
196
  .prepare('SELECT gemini_api_key FROM bot_api_keys WHERE app_id = ?')
235
197
  .get(appId) as { gemini_api_key: string | null } | undefined
236
198
 
237
- // Track if sessions are active
238
- let hasActiveSessions = false
239
-
199
+ // Create GenAI worker
240
200
  const genAiWorker = await createGenAIWorker({
241
201
  directory,
242
202
  guildId,
@@ -304,82 +264,30 @@ async function setupVoiceHandling({
304
264
  genAiWorker.interrupt()
305
265
  connection.setSpeaking(false)
306
266
  },
307
- onAllSessionsCompleted() {
308
- // All OpenCode sessions have completed
309
- hasActiveSessions = false
310
- voiceLogger.log('All OpenCode sessions completed for this GenAI session')
311
-
312
- // Update the stored session state
313
- const session = genAiSessions.get(channelId)
314
- if (session) {
315
- session.hasActiveSessions = false
316
-
317
- // If cleanup is pending (user left channel), schedule cleanup with grace period
318
- if (session.pendingCleanup) {
319
- voiceLogger.log(
320
- `Scheduling cleanup for channel ${channelId} in 1 minute`,
321
- )
322
-
323
- // Clear any existing timer
324
- if (session.cleanupTimer) {
325
- clearTimeout(session.cleanupTimer)
326
- }
327
-
328
- // Schedule cleanup after 1 minute grace period
329
- session.cleanupTimer = setTimeout(() => {
330
- // Double-check that cleanup is still needed
331
- const currentSession = genAiSessions.get(channelId)
332
- if (
333
- currentSession?.pendingCleanup &&
334
- !currentSession.hasActiveSessions
335
- ) {
336
- voiceLogger.log(
337
- `Grace period expired, cleaning up GenAI session for channel ${channelId}`,
338
- )
339
- // Use the main cleanup function - defined later in startDiscordBot
340
- cleanupGenAiSession(channelId)
341
- }
342
- }, 60000) // 1 minute
343
- }
344
- }
345
- },
346
267
  onToolCallCompleted(params) {
347
- // Note: We now track at the tools.ts level, but still handle completion messages
348
- voiceLogger.log(`OpenCode session ${params.sessionId} completed`)
349
-
350
268
  const text = params.error
351
269
  ? `<systemMessage>\nThe coding agent encountered an error while processing session ${params.sessionId}: ${params.error?.message || String(params.error)}\n</systemMessage>`
352
270
  : `<systemMessage>\nThe coding agent finished working on session ${params.sessionId}\n\nHere's what the assistant wrote:\n${params.markdown}\n</systemMessage>`
353
271
 
354
272
  genAiWorker.sendTextInput(text)
355
-
356
- // Mark that we have active sessions (will be updated by onAllSessionsCompleted when done)
357
- hasActiveSessions = true
358
- const session = genAiSessions.get(channelId)
359
- if (session) {
360
- session.hasActiveSessions = true
361
- }
362
273
  },
363
274
  onError(error) {
364
275
  voiceLogger.error('GenAI worker error:', error)
365
276
  },
366
277
  })
367
278
 
279
+ // Stop any existing GenAI worker before storing new one
280
+ if (voiceData.genAiWorker) {
281
+ voiceLogger.log('Stopping existing GenAI worker before creating new one')
282
+ await voiceData.genAiWorker.stop()
283
+ }
284
+
368
285
  // Send initial greeting
369
286
  genAiWorker.sendTextInput(
370
287
  `<systemMessage>\nsay "Hello boss, how we doing today?"\n</systemMessage>`,
371
288
  )
372
289
 
373
- // Store the GenAI session
374
- genAiSessions.set(channelId, {
375
- genAiWorker,
376
- hasActiveSessions,
377
- pendingCleanup: false,
378
- cleanupTimer: undefined,
379
- guildId,
380
- channelId,
381
- directory,
382
- })
290
+ voiceData.genAiWorker = genAiWorker
383
291
 
384
292
  // Set up voice receiver for user input
385
293
  const receiver = connection.receiver
@@ -387,15 +295,6 @@ async function setupVoiceHandling({
387
295
  // Remove all existing listeners to prevent accumulation
388
296
  receiver.speaking.removeAllListeners('start')
389
297
 
390
- // Get the GenAI session for this channel
391
- const genAiSession = genAiSessions.get(channelId)
392
- if (!genAiSession) {
393
- voiceLogger.error(
394
- `GenAI session was just created but not found for channel ${channelId}`,
395
- )
396
- return
397
- }
398
-
399
298
  // Counter to track overlapping speaking sessions
400
299
  let speakingSessionCount = 0
401
300
 
@@ -451,10 +350,9 @@ async function setupVoiceHandling({
451
350
  return
452
351
  }
453
352
 
454
- const genAiSession = genAiSessions.get(channelId)
455
- if (!genAiSession) {
353
+ if (!voiceData.genAiWorker) {
456
354
  voiceLogger.warn(
457
- `[VOICE] Received audio frame but no GenAI session active for channel ${channelId}`,
355
+ `[VOICE] Received audio frame but no GenAI worker active for guild ${guildId}`,
458
356
  )
459
357
  return
460
358
  }
@@ -464,7 +362,7 @@ async function setupVoiceHandling({
464
362
  voiceData.userAudioStream?.write(frame)
465
363
 
466
364
  // stream incrementally — low latency
467
- genAiSession.genAiWorker.sendRealtimeInput({
365
+ voiceData.genAiWorker.sendRealtimeInput({
468
366
  audio: {
469
367
  mimeType: 'audio/pcm;rate=16000',
470
368
  data: frame.toString('base64'),
@@ -477,8 +375,7 @@ async function setupVoiceHandling({
477
375
  voiceLogger.log(
478
376
  `User ${userId} stopped speaking (session ${currentSessionCount})`,
479
377
  )
480
- const genAiSession = genAiSessions.get(channelId)
481
- genAiSession?.genAiWorker.sendRealtimeInput({
378
+ voiceData.genAiWorker?.sendRealtimeInput({
482
379
  audioStreamEnd: true,
483
380
  })
484
381
  } else {
@@ -982,21 +879,13 @@ export async function initializeOpencodeForDirectory(directory: string) {
982
879
 
983
880
  await waitForServer(port)
984
881
 
985
- // Create a custom fetch that disables Bun's default timeout
986
- const customFetch = (
987
- input: string | URL | Request,
988
- init?: RequestInit,
989
- ): Promise<Response> => {
990
- return fetch(input, {
991
- ...init,
992
- // @ts-ignore - Bun-specific option to disable timeout
993
- timeout: false,
994
- })
995
- }
996
-
997
882
  const client = createOpencodeClient({
998
883
  baseUrl: `http://localhost:${port}`,
999
- fetch: customFetch,
884
+ fetch: (request: Request) =>
885
+ fetch(request, {
886
+ // @ts-ignore
887
+ timeout: false,
888
+ }),
1000
889
  })
1001
890
 
1002
891
  opencodeServers.set(directory, {
@@ -2345,43 +2234,21 @@ export async function startDiscordBot({
2345
2234
  },
2346
2235
  )
2347
2236
 
2348
- // Helper function to clean up GenAI session
2349
- async function cleanupGenAiSession(channelId: string) {
2350
- const genAiSession = genAiSessions.get(channelId)
2351
- if (!genAiSession) return
2352
-
2353
- try {
2354
- // Clear any cleanup timer
2355
- if (genAiSession.cleanupTimer) {
2356
- clearTimeout(genAiSession.cleanupTimer)
2357
- genAiSession.cleanupTimer = undefined
2358
- }
2359
-
2360
- voiceLogger.log(`Stopping GenAI worker for channel ${channelId}...`)
2361
- await genAiSession.genAiWorker.stop()
2362
- voiceLogger.log(`GenAI worker stopped for channel ${channelId}`)
2363
-
2364
- // Remove from map
2365
- genAiSessions.delete(channelId)
2366
- voiceLogger.log(`GenAI session cleanup complete for channel ${channelId}`)
2367
- } catch (error) {
2368
- voiceLogger.error(
2369
- `Error during GenAI session cleanup for channel ${channelId}:`,
2370
- error,
2371
- )
2372
- // Still remove from map even if there was an error
2373
- genAiSessions.delete(channelId)
2374
- }
2375
- }
2376
-
2377
2237
  // Helper function to clean up voice connection and associated resources
2378
- async function cleanupVoiceConnection(guildId: string, channelId?: string) {
2238
+ async function cleanupVoiceConnection(guildId: string) {
2379
2239
  const voiceData = voiceConnections.get(guildId)
2380
2240
  if (!voiceData) return
2381
2241
 
2382
2242
  voiceLogger.log(`Starting cleanup for guild ${guildId}`)
2383
2243
 
2384
2244
  try {
2245
+ // Stop GenAI worker if exists (this is async!)
2246
+ if (voiceData.genAiWorker) {
2247
+ voiceLogger.log(`Stopping GenAI worker...`)
2248
+ await voiceData.genAiWorker.stop()
2249
+ voiceLogger.log(`GenAI worker stopped`)
2250
+ }
2251
+
2385
2252
  // Close user audio stream if exists
2386
2253
  if (voiceData.userAudioStream) {
2387
2254
  voiceLogger.log(`Closing user audio stream...`)
@@ -2405,45 +2272,7 @@ export async function startDiscordBot({
2405
2272
 
2406
2273
  // Remove from map
2407
2274
  voiceConnections.delete(guildId)
2408
- voiceLogger.log(`Voice connection cleanup complete for guild ${guildId}`)
2409
-
2410
- // Mark the GenAI session for cleanup when all sessions complete
2411
- if (channelId) {
2412
- const genAiSession = genAiSessions.get(channelId)
2413
- if (genAiSession) {
2414
- voiceLogger.log(
2415
- `Marking channel ${channelId} for cleanup when sessions complete`,
2416
- )
2417
- genAiSession.pendingCleanup = true
2418
-
2419
- // If no active sessions, trigger cleanup immediately (with grace period)
2420
- if (!genAiSession.hasActiveSessions) {
2421
- voiceLogger.log(
2422
- `No active sessions, scheduling cleanup for channel ${channelId} in 1 minute`,
2423
- )
2424
-
2425
- // Clear any existing timer
2426
- if (genAiSession.cleanupTimer) {
2427
- clearTimeout(genAiSession.cleanupTimer)
2428
- }
2429
-
2430
- // Schedule cleanup after 1 minute grace period
2431
- genAiSession.cleanupTimer = setTimeout(() => {
2432
- // Double-check that cleanup is still needed
2433
- const currentSession = genAiSessions.get(channelId)
2434
- if (
2435
- currentSession?.pendingCleanup &&
2436
- !currentSession.hasActiveSessions
2437
- ) {
2438
- voiceLogger.log(
2439
- `Grace period expired, cleaning up GenAI session for channel ${channelId}`,
2440
- )
2441
- cleanupGenAiSession(channelId)
2442
- }
2443
- }, 60000) // 1 minute
2444
- }
2445
- }
2446
- }
2275
+ voiceLogger.log(`Cleanup complete for guild ${guildId}`)
2447
2276
  } catch (error) {
2448
2277
  voiceLogger.error(`Error during cleanup for guild ${guildId}:`, error)
2449
2278
  // Still remove from map even if there was an error
@@ -2501,7 +2330,7 @@ export async function startDiscordBot({
2501
2330
  )
2502
2331
 
2503
2332
  // Properly clean up all resources
2504
- await cleanupVoiceConnection(guildId, oldState.channelId)
2333
+ await cleanupVoiceConnection(guildId)
2505
2334
  } else {
2506
2335
  voiceLogger.log(
2507
2336
  `Other admins still in channel, bot staying in voice channel`,
@@ -2551,16 +2380,6 @@ export async function startDiscordBot({
2551
2380
  selfDeaf: false,
2552
2381
  selfMute: false,
2553
2382
  })
2554
-
2555
- // Set up voice handling for the new channel
2556
- // This will reuse existing GenAI session if one exists
2557
- await setupVoiceHandling({
2558
- connection: voiceData.connection,
2559
- guildId,
2560
- channelId: voiceChannel.id,
2561
- appId: currentAppId!,
2562
- cleanupGenAiSession,
2563
- })
2564
2383
  }
2565
2384
  } else {
2566
2385
  voiceLogger.log(
@@ -2643,7 +2462,6 @@ export async function startDiscordBot({
2643
2462
  guildId: newState.guild.id,
2644
2463
  channelId: voiceChannel.id,
2645
2464
  appId: currentAppId!,
2646
- cleanupGenAiSession,
2647
2465
  })
2648
2466
 
2649
2467
  // Handle connection state changes
@@ -2671,7 +2489,7 @@ export async function startDiscordBot({
2671
2489
  `Connection destroyed for guild: ${newState.guild.name}`,
2672
2490
  )
2673
2491
  // Use the cleanup function to ensure everything is properly closed
2674
- await cleanupVoiceConnection(newState.guild.id, voiceChannel.id)
2492
+ await cleanupVoiceConnection(newState.guild.id)
2675
2493
  })
2676
2494
 
2677
2495
  // Handle errors
@@ -2683,7 +2501,7 @@ export async function startDiscordBot({
2683
2501
  })
2684
2502
  } catch (error) {
2685
2503
  voiceLogger.error(`Failed to join voice channel:`, error)
2686
- await cleanupVoiceConnection(newState.guild.id, voiceChannel.id)
2504
+ await cleanupVoiceConnection(newState.guild.id)
2687
2505
  }
2688
2506
  } catch (error) {
2689
2507
  voiceLogger.error('Error in voice state update handler:', error)
@@ -2703,44 +2521,24 @@ export async function startDiscordBot({
2703
2521
  ;(global as any).shuttingDown = true
2704
2522
 
2705
2523
  try {
2706
- // Clean up all voice connections
2707
- const voiceCleanupPromises: Promise<void>[] = []
2708
- for (const [guildId, voiceData] of voiceConnections) {
2524
+ // Clean up all voice connections (this includes GenAI workers and audio streams)
2525
+ const cleanupPromises: Promise<void>[] = []
2526
+ for (const [guildId] of voiceConnections) {
2709
2527
  voiceLogger.log(
2710
2528
  `[SHUTDOWN] Cleaning up voice connection for guild ${guildId}`,
2711
2529
  )
2712
- // Find the channel ID for this connection
2713
- const channelId = voiceData.connection.joinConfig.channelId || undefined
2714
- voiceCleanupPromises.push(cleanupVoiceConnection(guildId, channelId))
2530
+ cleanupPromises.push(cleanupVoiceConnection(guildId))
2715
2531
  }
2716
2532
 
2717
- // Wait for all voice cleanups to complete
2718
- if (voiceCleanupPromises.length > 0) {
2533
+ // Wait for all cleanups to complete
2534
+ if (cleanupPromises.length > 0) {
2719
2535
  voiceLogger.log(
2720
- `[SHUTDOWN] Waiting for ${voiceCleanupPromises.length} voice connection(s) to clean up...`,
2536
+ `[SHUTDOWN] Waiting for ${cleanupPromises.length} voice connection(s) to clean up...`,
2721
2537
  )
2722
- await Promise.allSettled(voiceCleanupPromises)
2538
+ await Promise.allSettled(cleanupPromises)
2723
2539
  discordLogger.log(`All voice connections cleaned up`)
2724
2540
  }
2725
2541
 
2726
- // Clean up all GenAI sessions (force cleanup regardless of active sessions)
2727
- const genAiCleanupPromises: Promise<void>[] = []
2728
- for (const [channelId, session] of genAiSessions) {
2729
- voiceLogger.log(
2730
- `[SHUTDOWN] Cleaning up GenAI session for channel ${channelId} (active sessions: ${session.hasActiveSessions})`,
2731
- )
2732
- genAiCleanupPromises.push(cleanupGenAiSession(channelId))
2733
- }
2734
-
2735
- // Wait for all GenAI cleanups to complete
2736
- if (genAiCleanupPromises.length > 0) {
2737
- voiceLogger.log(
2738
- `[SHUTDOWN] Waiting for ${genAiCleanupPromises.length} GenAI session(s) to clean up...`,
2739
- )
2740
- await Promise.allSettled(genAiCleanupPromises)
2741
- discordLogger.log(`All GenAI sessions cleaned up`)
2742
- }
2743
-
2744
2542
  // Kill all OpenCode servers
2745
2543
  for (const [dir, server] of opencodeServers) {
2746
2544
  if (!server.process.killed) {
@@ -17,7 +17,6 @@ export interface GenAIWorkerOptions {
17
17
  onAssistantStartSpeaking?: () => void
18
18
  onAssistantStopSpeaking?: () => void
19
19
  onAssistantInterruptSpeaking?: () => void
20
- onAllSessionsCompleted?: () => void
21
20
  onToolCallCompleted?: (params: {
22
21
  sessionId: string
23
22
  messageId: string
@@ -61,9 +60,6 @@ export function createGenAIWorker(
61
60
  case 'assistantInterruptSpeaking':
62
61
  options.onAssistantInterruptSpeaking?.()
63
62
  break
64
- case 'allSessionsCompleted':
65
- options.onAllSessionsCompleted?.()
66
- break
67
63
  case 'toolCallCompleted':
68
64
  options.onToolCallCompleted?.(message)
69
65
  break
@@ -265,11 +265,6 @@ parentPort.on('message', async (message: WorkerInMessage) => {
265
265
  ...params,
266
266
  } satisfies WorkerOutMessage)
267
267
  },
268
- onAllSessionsCompleted: () => {
269
- parentPort!.postMessage({
270
- type: 'allSessionsCompleted',
271
- } satisfies WorkerOutMessage)
272
- },
273
268
  })
274
269
 
275
270
  // Start GenAI session
package/src/tools.ts CHANGED
@@ -19,7 +19,6 @@ import { initializeOpencodeForDirectory } from './discordBot.js'
19
19
 
20
20
  export async function getTools({
21
21
  onMessageCompleted,
22
- onAllSessionsCompleted,
23
22
  directory,
24
23
  }: {
25
24
  directory: string
@@ -30,7 +29,6 @@ export async function getTools({
30
29
  error?: unknown
31
30
  markdown?: string
32
31
  }) => void
33
- onAllSessionsCompleted?: () => void
34
32
  }) {
35
33
  const getClient = await initializeOpencodeForDirectory(directory)
36
34
  const client = getClient()
@@ -39,9 +37,6 @@ export async function getTools({
39
37
 
40
38
  const providersResponse = await client.config.providers({})
41
39
  const providers: Provider[] = providersResponse.data?.providers || []
42
-
43
- // Track all active OpenCode sessions
44
- const activeSessions = new Set<string>()
45
40
 
46
41
  // Helper: get last assistant model for a session (non-summary)
47
42
  const getSessionModel = async (
@@ -73,10 +68,6 @@ export async function getTools({
73
68
  execute: async ({ sessionId, message }) => {
74
69
  const sessionModel = await getSessionModel(sessionId)
75
70
 
76
- // Track this session as active
77
- activeSessions.add(sessionId)
78
- toolsLogger.log(`Session ${sessionId} started, ${activeSessions.size} active sessions`)
79
-
80
71
  // do not await
81
72
  getClient()
82
73
  .session.prompt({
@@ -98,16 +89,6 @@ export async function getTools({
98
89
  data: response.data,
99
90
  markdown,
100
91
  })
101
-
102
- // Remove from active sessions
103
- activeSessions.delete(sessionId)
104
- toolsLogger.log(`Session ${sessionId} completed, ${activeSessions.size} active sessions remaining`)
105
-
106
- // Check if all sessions are complete
107
- if (activeSessions.size === 0) {
108
- toolsLogger.log('All sessions completed')
109
- onAllSessionsCompleted?.()
110
- }
111
92
  })
112
93
  .catch((error) => {
113
94
  onMessageCompleted?.({
@@ -115,16 +96,6 @@ export async function getTools({
115
96
  messageId: '',
116
97
  error,
117
98
  })
118
-
119
- // Remove from active sessions even on error
120
- activeSessions.delete(sessionId)
121
- toolsLogger.log(`Session ${sessionId} failed, ${activeSessions.size} active sessions remaining`)
122
-
123
- // Check if all sessions are complete
124
- if (activeSessions.size === 0) {
125
- toolsLogger.log('All sessions completed')
126
- onAllSessionsCompleted?.()
127
- }
128
99
  })
129
100
  return {
130
101
  success: true,
@@ -172,58 +143,32 @@ export async function getTools({
172
143
  throw new Error('Failed to create session')
173
144
  }
174
145
 
175
- const newSessionId = session.data.id
176
-
177
- // Track this session as active
178
- activeSessions.add(newSessionId)
179
- toolsLogger.log(`New session ${newSessionId} created, ${activeSessions.size} active sessions`)
180
-
181
146
  // do not await
182
147
  getClient()
183
148
  .session.prompt({
184
- path: { id: newSessionId },
149
+ path: { id: session.data.id },
185
150
  body: {
186
151
  parts: [{ type: 'text', text: message }],
187
152
  },
188
153
  })
189
154
  .then(async (response) => {
190
155
  const markdown = await markdownRenderer.generate({
191
- sessionID: newSessionId,
156
+ sessionID: session.data.id,
192
157
  lastAssistantOnly: true,
193
158
  })
194
159
  onMessageCompleted?.({
195
- sessionId: newSessionId,
160
+ sessionId: session.data.id,
196
161
  messageId: '',
197
162
  data: response.data,
198
163
  markdown,
199
164
  })
200
-
201
- // Remove from active sessions
202
- activeSessions.delete(newSessionId)
203
- toolsLogger.log(`Session ${newSessionId} completed, ${activeSessions.size} active sessions remaining`)
204
-
205
- // Check if all sessions are complete
206
- if (activeSessions.size === 0) {
207
- toolsLogger.log('All sessions completed')
208
- onAllSessionsCompleted?.()
209
- }
210
165
  })
211
166
  .catch((error) => {
212
167
  onMessageCompleted?.({
213
- sessionId: newSessionId,
168
+ sessionId: session.data.id,
214
169
  messageId: '',
215
170
  error,
216
171
  })
217
-
218
- // Remove from active sessions even on error
219
- activeSessions.delete(newSessionId)
220
- toolsLogger.log(`Session ${newSessionId} failed, ${activeSessions.size} active sessions remaining`)
221
-
222
- // Check if all sessions are complete
223
- if (activeSessions.size === 0) {
224
- toolsLogger.log('All sessions completed')
225
- onAllSessionsCompleted?.()
226
- }
227
172
  })
228
173
 
229
174
  return {
@@ -53,9 +53,6 @@ export type WorkerOutMessage =
53
53
  error?: any
54
54
  markdown?: string
55
55
  }
56
- | {
57
- type: 'allSessionsCompleted'
58
- }
59
56
  | {
60
57
  type: 'error'
61
58
  error: string