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.
- package/dist/discordBot.js +37 -179
- package/dist/genai-worker-wrapper.js +0 -3
- package/dist/genai-worker.js +0 -5
- package/dist/tools.js +5 -46
- package/package.json +1 -1
- package/src/discordBot.ts +39 -241
- package/src/genai-worker-wrapper.ts +0 -4
- package/src/genai-worker.ts +0 -5
- package/src/tools.ts +4 -59
- package/src/worker-types.ts +0 -3
package/dist/discordBot.js
CHANGED
|
@@ -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,
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
310
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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(`
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2014
|
-
for (const [guildId
|
|
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
|
-
|
|
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
|
-
//
|
|
2027
|
-
|
|
2028
|
-
|
|
2029
|
-
|
|
2030
|
-
|
|
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;
|
package/dist/genai-worker.js
CHANGED
|
@@ -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,
|
|
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:
|
|
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:
|
|
121
|
+
sessionID: session.data.id,
|
|
147
122
|
lastAssistantOnly: true,
|
|
148
123
|
});
|
|
149
124
|
onMessageCompleted?.({
|
|
150
|
-
sessionId:
|
|
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:
|
|
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
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
455
|
-
if (!genAiSession) {
|
|
353
|
+
if (!voiceData.genAiWorker) {
|
|
456
354
|
voiceLogger.warn(
|
|
457
|
-
`[VOICE] Received audio frame but no GenAI
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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(`
|
|
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
|
|
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
|
|
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
|
|
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
|
|
2708
|
-
for (const [guildId
|
|
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
|
-
|
|
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
|
|
2718
|
-
if (
|
|
2533
|
+
// Wait for all cleanups to complete
|
|
2534
|
+
if (cleanupPromises.length > 0) {
|
|
2719
2535
|
voiceLogger.log(
|
|
2720
|
-
`[SHUTDOWN] Waiting for ${
|
|
2536
|
+
`[SHUTDOWN] Waiting for ${cleanupPromises.length} voice connection(s) to clean up...`,
|
|
2721
2537
|
)
|
|
2722
|
-
await Promise.allSettled(
|
|
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
|
package/src/genai-worker.ts
CHANGED
|
@@ -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:
|
|
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:
|
|
156
|
+
sessionID: session.data.id,
|
|
192
157
|
lastAssistantOnly: true,
|
|
193
158
|
})
|
|
194
159
|
onMessageCompleted?.({
|
|
195
|
-
sessionId:
|
|
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:
|
|
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 {
|