kimaki 0.4.37 → 0.4.39

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.
Files changed (53) hide show
  1. package/dist/channel-management.js +6 -2
  2. package/dist/cli.js +41 -15
  3. package/dist/commands/abort.js +15 -6
  4. package/dist/commands/add-project.js +9 -0
  5. package/dist/commands/agent.js +114 -20
  6. package/dist/commands/fork.js +13 -2
  7. package/dist/commands/model.js +12 -0
  8. package/dist/commands/remove-project.js +26 -16
  9. package/dist/commands/resume.js +9 -0
  10. package/dist/commands/session.js +13 -0
  11. package/dist/commands/share.js +10 -1
  12. package/dist/commands/undo-redo.js +13 -4
  13. package/dist/database.js +24 -5
  14. package/dist/discord-bot.js +38 -31
  15. package/dist/errors.js +110 -0
  16. package/dist/genai-worker.js +18 -16
  17. package/dist/interaction-handler.js +6 -1
  18. package/dist/markdown.js +96 -85
  19. package/dist/markdown.test.js +10 -3
  20. package/dist/message-formatting.js +50 -37
  21. package/dist/opencode.js +43 -46
  22. package/dist/session-handler.js +136 -8
  23. package/dist/system-message.js +2 -0
  24. package/dist/tools.js +18 -8
  25. package/dist/voice-handler.js +48 -25
  26. package/dist/voice.js +159 -131
  27. package/package.json +2 -1
  28. package/src/channel-management.ts +6 -2
  29. package/src/cli.ts +67 -19
  30. package/src/commands/abort.ts +17 -7
  31. package/src/commands/add-project.ts +9 -0
  32. package/src/commands/agent.ts +160 -25
  33. package/src/commands/fork.ts +18 -7
  34. package/src/commands/model.ts +12 -0
  35. package/src/commands/remove-project.ts +28 -16
  36. package/src/commands/resume.ts +9 -0
  37. package/src/commands/session.ts +13 -0
  38. package/src/commands/share.ts +11 -1
  39. package/src/commands/undo-redo.ts +15 -6
  40. package/src/database.ts +26 -4
  41. package/src/discord-bot.ts +42 -34
  42. package/src/errors.ts +208 -0
  43. package/src/genai-worker.ts +20 -17
  44. package/src/interaction-handler.ts +7 -1
  45. package/src/markdown.test.ts +13 -3
  46. package/src/markdown.ts +111 -95
  47. package/src/message-formatting.ts +55 -38
  48. package/src/opencode.ts +52 -49
  49. package/src/session-handler.ts +164 -11
  50. package/src/system-message.ts +2 -0
  51. package/src/tools.ts +18 -8
  52. package/src/voice-handler.ts +48 -23
  53. package/src/voice.ts +195 -148
@@ -11,6 +11,7 @@ import { createLogger } from './logger.js';
11
11
  import { isAbortError } from './utils.js';
12
12
  import { showAskUserQuestionDropdowns, cancelPendingQuestion, pendingQuestionContexts, } from './commands/ask-question.js';
13
13
  import { showPermissionDropdown, cleanupPermissionContext } from './commands/permissions.js';
14
+ import * as errore from 'errore';
14
15
  const sessionLogger = createLogger('SESSION');
15
16
  const voiceLogger = createLogger('VOICE');
16
17
  const discordLogger = createLogger('DISCORD');
@@ -48,6 +49,10 @@ export async function abortAndRetrySession({ sessionId, thread, projectDirectory
48
49
  controller.abort('model-change');
49
50
  // Also call the API abort endpoint
50
51
  const getClient = await initializeOpencodeForDirectory(projectDirectory);
52
+ if (errore.isError(getClient)) {
53
+ sessionLogger.error(`[ABORT+RETRY] Failed to initialize OpenCode client:`, getClient.message);
54
+ return false;
55
+ }
51
56
  try {
52
57
  await getClient().session.abort({ path: { id: sessionId } });
53
58
  }
@@ -93,6 +98,10 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
93
98
  const directory = projectDirectory || process.cwd();
94
99
  sessionLogger.log(`Using directory: ${directory}`);
95
100
  const getClient = await initializeOpencodeForDirectory(directory);
101
+ if (errore.isError(getClient)) {
102
+ await sendThreadMessage(thread, `✗ ${getClient.message}`);
103
+ return;
104
+ }
96
105
  const serverEntry = getOpencodeServers().get(directory);
97
106
  const port = serverEntry?.port;
98
107
  const row = getDatabase()
@@ -258,11 +267,20 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
258
267
  }
259
268
  };
260
269
  const eventHandler = async () => {
270
+ // Subtask tracking: child sessionId → { label, assistantMessageId }
271
+ const subtaskSessions = new Map();
272
+ // Counts spawned tasks per agent type: "explore" → 2
273
+ const agentSpawnCounts = {};
261
274
  try {
262
275
  let assistantMessageId;
263
276
  for await (const event of events) {
264
277
  if (event.type === 'message.updated') {
265
278
  const msg = event.properties.info;
279
+ // Track assistant message IDs for subtask sessions
280
+ const subtaskInfo = subtaskSessions.get(msg.sessionID);
281
+ if (subtaskInfo && msg.role === 'assistant') {
282
+ subtaskInfo.assistantMessageId = msg.id;
283
+ }
266
284
  if (msg.sessionID !== session.id) {
267
285
  continue;
268
286
  }
@@ -309,9 +327,48 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
309
327
  }
310
328
  else if (event.type === 'message.part.updated') {
311
329
  const part = event.properties.part;
312
- if (part.sessionID !== session.id) {
330
+ // Check if this is a subtask event (child session we're tracking)
331
+ const subtaskInfo = subtaskSessions.get(part.sessionID);
332
+ const isSubtaskEvent = Boolean(subtaskInfo);
333
+ // Accept events from main session OR tracked subtask sessions
334
+ if (part.sessionID !== session.id && !isSubtaskEvent) {
313
335
  continue;
314
336
  }
337
+ // For subtask events, send them immediately with prefix (don't buffer in currentParts)
338
+ if (isSubtaskEvent && subtaskInfo) {
339
+ // Skip parts that aren't useful to show (step-start, step-finish, pending tools)
340
+ if (part.type === 'step-start' || part.type === 'step-finish') {
341
+ continue;
342
+ }
343
+ if (part.type === 'tool' && part.state.status === 'pending') {
344
+ continue;
345
+ }
346
+ // Skip text parts - the outer agent will report the task result anyway
347
+ if (part.type === 'text') {
348
+ continue;
349
+ }
350
+ // Only show parts from assistant messages (not user prompts sent to subtask)
351
+ // Skip if we haven't seen an assistant message yet, or if this part is from a different message
352
+ if (!subtaskInfo.assistantMessageId ||
353
+ part.messageID !== subtaskInfo.assistantMessageId) {
354
+ continue;
355
+ }
356
+ const content = formatPart(part, subtaskInfo.label);
357
+ if (content.trim() && !sentPartIds.has(part.id)) {
358
+ try {
359
+ const msg = await sendThreadMessage(thread, content + '\n\n');
360
+ sentPartIds.add(part.id);
361
+ getDatabase()
362
+ .prepare('INSERT OR REPLACE INTO part_messages (part_id, message_id, thread_id) VALUES (?, ?, ?)')
363
+ .run(part.id, msg.id, thread.id);
364
+ }
365
+ catch (error) {
366
+ discordLogger.error(`ERROR: Failed to send subtask part ${part.id}:`, error);
367
+ }
368
+ }
369
+ continue;
370
+ }
371
+ // Main session events: require matching assistantMessageId
315
372
  if (part.messageID !== assistantMessageId) {
316
373
  continue;
317
374
  }
@@ -339,6 +396,20 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
339
396
  }
340
397
  }
341
398
  await sendPartMessage(part);
399
+ // Track task tool and register child session when sessionId is available
400
+ if (part.tool === 'task' && !sentPartIds.has(part.id)) {
401
+ const description = part.state.input?.description || '';
402
+ const agent = part.state.input?.subagent_type || 'task';
403
+ const childSessionId = part.state.metadata?.sessionId || '';
404
+ if (description && childSessionId) {
405
+ agentSpawnCounts[agent] = (agentSpawnCounts[agent] || 0) + 1;
406
+ const label = `${agent}-${agentSpawnCounts[agent]}`;
407
+ subtaskSessions.set(childSessionId, { label, assistantMessageId: undefined });
408
+ const taskDisplay = `┣ task **${label}** _${description}_`;
409
+ await sendThreadMessage(thread, taskDisplay + '\n\n');
410
+ sentPartIds.add(part.id);
411
+ }
412
+ }
342
413
  }
343
414
  // Show token usage for completed tools with large output (>5k tokens)
344
415
  if (part.type === 'tool' && part.state.status === 'completed') {
@@ -473,13 +544,44 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
473
544
  requestId: questionRequest.id,
474
545
  input: { questions: questionRequest.questions },
475
546
  });
547
+ // Process queued messages if any - queued message will cancel the pending question
548
+ const queue = messageQueue.get(thread.id);
549
+ if (queue && queue.length > 0) {
550
+ const nextMessage = queue.shift();
551
+ if (queue.length === 0) {
552
+ messageQueue.delete(thread.id);
553
+ }
554
+ sessionLogger.log(`[QUEUE] Question shown but queue has messages, processing from ${nextMessage.username}`);
555
+ await sendThreadMessage(thread, `» **${nextMessage.username}:** ${nextMessage.prompt.slice(0, 150)}${nextMessage.prompt.length > 150 ? '...' : ''}`);
556
+ // handleOpencodeSession will call cancelPendingQuestion, which cancels the dropdown
557
+ setImmediate(() => {
558
+ handleOpencodeSession({
559
+ prompt: nextMessage.prompt,
560
+ thread,
561
+ projectDirectory: directory,
562
+ images: nextMessage.images,
563
+ channelId,
564
+ }).catch(async (e) => {
565
+ sessionLogger.error(`[QUEUE] Failed to process queued message:`, e);
566
+ const errorMsg = e instanceof Error ? e.message : String(e);
567
+ await sendThreadMessage(thread, `✗ Queued message failed: ${errorMsg.slice(0, 200)}`);
568
+ });
569
+ });
570
+ }
476
571
  }
477
572
  else if (event.type === 'session.idle') {
573
+ const idleSessionId = event.properties.sessionID;
478
574
  // Session is done processing - abort to signal completion
479
- if (event.properties.sessionID === session.id) {
575
+ if (idleSessionId === session.id) {
480
576
  sessionLogger.log(`[SESSION IDLE] Session ${session.id} is idle, aborting`);
481
577
  abortController.abort('finished');
482
578
  }
579
+ else if (subtaskSessions.has(idleSessionId)) {
580
+ // Child session completed - clean up tracking
581
+ const subtask = subtaskSessions.get(idleSessionId);
582
+ sessionLogger.log(`[SUBTASK IDLE] Subtask "${subtask?.label}" completed`);
583
+ subtaskSessions.delete(idleSessionId);
584
+ }
483
585
  }
484
586
  }
485
587
  }
@@ -513,6 +615,26 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
513
615
  const agentInfo = usedAgent && usedAgent.toLowerCase() !== 'build' ? ` ⋅ **${usedAgent}**` : '';
514
616
  let contextInfo = '';
515
617
  try {
618
+ // Fetch final token count from API since message.updated events can arrive
619
+ // after session.idle due to race conditions in event ordering
620
+ if (tokensUsedInSession === 0) {
621
+ const messagesResponse = await getClient().session.messages({
622
+ path: { id: session.id },
623
+ });
624
+ const messages = messagesResponse.data || [];
625
+ const lastAssistant = [...messages]
626
+ .reverse()
627
+ .find((m) => m.info.role === 'assistant');
628
+ if (lastAssistant && 'tokens' in lastAssistant.info) {
629
+ const tokens = lastAssistant.info.tokens;
630
+ tokensUsedInSession =
631
+ tokens.input +
632
+ tokens.output +
633
+ tokens.reasoning +
634
+ tokens.cache.read +
635
+ tokens.cache.write;
636
+ }
637
+ }
516
638
  const providersResponse = await getClient().provider.list({ query: { directory } });
517
639
  const provider = providersResponse.data?.all?.find((p) => p.id === usedProviderID);
518
640
  const model = provider?.models?.[usedModel || ''];
@@ -581,9 +703,20 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
581
703
  })();
582
704
  const parts = [{ type: 'text', text: promptWithImagePaths }, ...images];
583
705
  sessionLogger.log(`[PROMPT] Parts to send:`, parts.length);
706
+ // Get agent preference: session-level overrides channel-level
707
+ const agentPreference = getSessionAgent(session.id) || (channelId ? getChannelAgent(channelId) : undefined);
708
+ if (agentPreference) {
709
+ sessionLogger.log(`[AGENT] Using agent preference: ${agentPreference}`);
710
+ }
584
711
  // Get model preference: session-level overrides channel-level
712
+ // BUT: if an agent is set, don't pass model param so the agent's model takes effect
585
713
  const modelPreference = getSessionModel(session.id) || (channelId ? getChannelModel(channelId) : undefined);
586
714
  const modelParam = (() => {
715
+ // When an agent is set, let the agent's model config take effect
716
+ if (agentPreference) {
717
+ sessionLogger.log(`[MODEL] Skipping model param, agent "${agentPreference}" controls model`);
718
+ return undefined;
719
+ }
587
720
  if (!modelPreference) {
588
721
  return undefined;
589
722
  }
@@ -595,11 +728,6 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
595
728
  sessionLogger.log(`[MODEL] Using model preference: ${modelPreference}`);
596
729
  return { providerID, modelID };
597
730
  })();
598
- // Get agent preference: session-level overrides channel-level
599
- const agentPreference = getSessionAgent(session.id) || (channelId ? getChannelAgent(channelId) : undefined);
600
- if (agentPreference) {
601
- sessionLogger.log(`[AGENT] Using agent preference: ${agentPreference}`);
602
- }
603
731
  // Use session.command API for slash commands, session.prompt for regular messages
604
732
  const response = command
605
733
  ? await getClient().session.command({
@@ -650,8 +778,8 @@ export async function handleOpencodeSession({ prompt, thread, projectDirectory,
650
778
  return { sessionID: session.id, result: response.data, port };
651
779
  }
652
780
  catch (error) {
653
- sessionLogger.error(`ERROR: Failed to send prompt:`, error);
654
781
  if (!isAbortError(error, abortController.signal)) {
782
+ sessionLogger.error(`ERROR: Failed to send prompt:`, error);
655
783
  abortController.abort('error');
656
784
  if (originalMessage) {
657
785
  try {
@@ -76,6 +76,8 @@ you can create diagrams wrapping them in code blocks.
76
76
 
77
77
  IMPORTANT: At the end of each response, especially after completing a task or presenting a plan, use the question tool to offer the user clear options for what to do next.
78
78
 
79
+ IMPORTANT: The question tool must be called last, after all text parts. If it is called before your final text response, the user will not see the text.
80
+
79
81
  Examples:
80
82
  - After showing a plan: offer "Start implementing?" with Yes/No options
81
83
  - After completing edits: offer "Commit changes?" with Yes/No options
package/dist/tools.js CHANGED
@@ -7,6 +7,7 @@ import { spawn } from 'node:child_process';
7
7
  import net from 'node:net';
8
8
  import { createOpencodeClient, } from '@opencode-ai/sdk';
9
9
  import { createLogger } from './logger.js';
10
+ import * as errore from 'errore';
10
11
  const toolsLogger = createLogger('TOOLS');
11
12
  import { ShareMarkdown } from './markdown.js';
12
13
  import { formatDistanceToNow } from './utils.js';
@@ -14,6 +15,9 @@ import pc from 'picocolors';
14
15
  import { initializeOpencodeForDirectory, getOpencodeSystemMessage } from './discord-bot.js';
15
16
  export async function getTools({ onMessageCompleted, directory, }) {
16
17
  const getClient = await initializeOpencodeForDirectory(directory);
18
+ if (errore.isError(getClient)) {
19
+ throw new Error(getClient.message);
20
+ }
17
21
  const client = getClient();
18
22
  const markdownRenderer = new ShareMarkdown(client);
19
23
  const providersResponse = await client.config.providers({});
@@ -55,7 +59,7 @@ export async function getTools({ onMessageCompleted, directory, }) {
55
59
  },
56
60
  })
57
61
  .then(async (response) => {
58
- const markdown = await markdownRenderer.generate({
62
+ const markdownResult = await markdownRenderer.generate({
59
63
  sessionID: sessionId,
60
64
  lastAssistantOnly: true,
61
65
  });
@@ -63,7 +67,7 @@ export async function getTools({ onMessageCompleted, directory, }) {
63
67
  sessionId,
64
68
  messageId: '',
65
69
  data: response.data,
66
- markdown,
70
+ markdown: errore.unwrapOr(markdownResult, ''),
67
71
  });
68
72
  })
69
73
  .catch((error) => {
@@ -116,7 +120,7 @@ export async function getTools({ onMessageCompleted, directory, }) {
116
120
  },
117
121
  })
118
122
  .then(async (response) => {
119
- const markdown = await markdownRenderer.generate({
123
+ const markdownResult = await markdownRenderer.generate({
120
124
  sessionID: session.data.id,
121
125
  lastAssistantOnly: true,
122
126
  });
@@ -124,7 +128,7 @@ export async function getTools({ onMessageCompleted, directory, }) {
124
128
  sessionId: session.data.id,
125
129
  messageId: '',
126
130
  data: response.data,
127
- markdown,
131
+ markdown: errore.unwrapOr(markdownResult, ''),
128
132
  });
129
133
  })
130
134
  .catch((error) => {
@@ -240,20 +244,26 @@ export async function getTools({ onMessageCompleted, directory, }) {
240
244
  const status = 'completed' in lastMessage.info.time && lastMessage.info.time.completed
241
245
  ? 'completed'
242
246
  : 'in_progress';
243
- const markdown = await markdownRenderer.generate({
247
+ const markdownResult = await markdownRenderer.generate({
244
248
  sessionID: sessionId,
245
249
  lastAssistantOnly: true,
246
250
  });
251
+ if (errore.isError(markdownResult)) {
252
+ throw new Error(markdownResult.message);
253
+ }
247
254
  return {
248
255
  success: true,
249
- markdown,
256
+ markdown: markdownResult,
250
257
  status,
251
258
  };
252
259
  }
253
260
  else {
254
- const markdown = await markdownRenderer.generate({
261
+ const markdownResult = await markdownRenderer.generate({
255
262
  sessionID: sessionId,
256
263
  });
264
+ if (errore.isError(markdownResult)) {
265
+ throw new Error(markdownResult.message);
266
+ }
257
267
  const messages = await getClient().session.messages({
258
268
  path: { id: sessionId },
259
269
  });
@@ -266,7 +276,7 @@ export async function getTools({ onMessageCompleted, directory, }) {
266
276
  : 'completed';
267
277
  return {
268
278
  success: true,
269
- markdown,
279
+ markdown: markdownResult,
270
280
  status,
271
281
  };
272
282
  }
@@ -1,6 +1,7 @@
1
1
  // Discord voice channel connection and audio stream handler.
2
2
  // Manages joining/leaving voice channels, captures user audio, resamples to 16kHz,
3
3
  // and routes audio to the GenAI worker for real-time voice assistant interactions.
4
+ import * as errore from 'errore';
4
5
  import { VoiceConnectionStatus, EndBehaviorType, joinVoiceChannel, entersState, } from '@discordjs/voice';
5
6
  import { exec } from 'node:child_process';
6
7
  import fs, { createWriteStream } from 'node:fs';
@@ -15,6 +16,7 @@ import { createGenAIWorker } from './genai-worker-wrapper.js';
15
16
  import { getDatabase } from './database.js';
16
17
  import { sendThreadMessage, escapeDiscordFormatting, SILENT_MESSAGE_FLAGS, } from './discord-utils.js';
17
18
  import { transcribeAudio } from './voice.js';
19
+ import { FetchError } from './errors.js';
18
20
  import { createLogger } from './logger.js';
19
21
  const voiceLogger = createLogger('VOICE');
20
22
  export const voiceConnections = new Map();
@@ -320,7 +322,15 @@ export async function processVoiceAttachment({ message, thread, projectDirectory
320
322
  return null;
321
323
  voiceLogger.log(`Detected audio attachment: ${audioAttachment.name} (${audioAttachment.contentType})`);
322
324
  await sendThreadMessage(thread, '🎤 Transcribing voice message...');
323
- const audioResponse = await fetch(audioAttachment.url);
325
+ const audioResponse = await errore.tryAsync({
326
+ try: () => fetch(audioAttachment.url),
327
+ catch: (e) => new FetchError({ url: audioAttachment.url, cause: e }),
328
+ });
329
+ if (errore.isError(audioResponse)) {
330
+ voiceLogger.error(`Failed to download audio attachment:`, audioResponse.message);
331
+ await sendThreadMessage(thread, `⚠️ Failed to download audio: ${audioResponse.message}`);
332
+ return null;
333
+ }
324
334
  const audioBuffer = Buffer.from(await audioResponse.arrayBuffer());
325
335
  voiceLogger.log(`Downloaded ${audioBuffer.length} bytes, transcribing...`);
326
336
  let transcriptionPrompt = 'Discord voice message transcription';
@@ -331,9 +341,8 @@ export async function processVoiceAttachment({ message, thread, projectDirectory
331
341
  const { stdout } = await execAsync('git ls-files | tree --fromfile -a', {
332
342
  cwd: projectDirectory,
333
343
  });
334
- const result = stdout;
335
- if (result) {
336
- transcriptionPrompt = `Discord voice message transcription. Project file structure:\n${result}\n\nPlease transcribe file names and paths accurately based on this context.`;
344
+ if (stdout) {
345
+ transcriptionPrompt = `Discord voice message transcription. Project file structure:\n${stdout}\n\nPlease transcribe file names and paths accurately based on this context.`;
337
346
  voiceLogger.log(`Added project context to transcription prompt`);
338
347
  }
339
348
  }
@@ -350,20 +359,24 @@ export async function processVoiceAttachment({ message, thread, projectDirectory
350
359
  geminiApiKey = apiKeys.gemini_api_key;
351
360
  }
352
361
  }
353
- let transcription;
354
- try {
355
- transcription = await transcribeAudio({
356
- audio: audioBuffer,
357
- prompt: transcriptionPrompt,
358
- geminiApiKey,
359
- directory: projectDirectory,
360
- currentSessionContext,
361
- lastSessionContext,
362
+ const transcription = await transcribeAudio({
363
+ audio: audioBuffer,
364
+ prompt: transcriptionPrompt,
365
+ geminiApiKey,
366
+ directory: projectDirectory,
367
+ currentSessionContext,
368
+ lastSessionContext,
369
+ });
370
+ if (errore.isError(transcription)) {
371
+ const errMsg = errore.matchError(transcription, {
372
+ ApiKeyMissingError: (e) => e.message,
373
+ InvalidAudioFormatError: (e) => e.message,
374
+ TranscriptionError: (e) => e.message,
375
+ EmptyTranscriptionError: (e) => e.message,
376
+ NoResponseContentError: (e) => e.message,
377
+ NoToolResponseError: (e) => e.message,
362
378
  });
363
- }
364
- catch (error) {
365
- const errMsg = error instanceof Error ? error.message : String(error);
366
- voiceLogger.error(`Transcription failed:`, error);
379
+ voiceLogger.error(`Transcription failed:`, transcription);
367
380
  await sendThreadMessage(thread, `⚠️ Transcription failed: ${errMsg}`);
368
381
  return null;
369
382
  }
@@ -371,15 +384,25 @@ export async function processVoiceAttachment({ message, thread, projectDirectory
371
384
  if (isNewThread) {
372
385
  const threadName = transcription.replace(/\s+/g, ' ').trim().slice(0, 80);
373
386
  if (threadName) {
374
- try {
375
- await Promise.race([
376
- thread.setName(threadName),
377
- new Promise((resolve) => setTimeout(resolve, 2000)),
378
- ]);
379
- voiceLogger.log(`Updated thread name to: "${threadName}"`);
387
+ const renamed = await Promise.race([
388
+ errore.tryAsync({
389
+ try: () => thread.setName(threadName),
390
+ catch: (e) => e,
391
+ }),
392
+ new Promise((resolve) => {
393
+ setTimeout(() => {
394
+ resolve(null);
395
+ }, 2000);
396
+ }),
397
+ ]);
398
+ if (renamed === null) {
399
+ voiceLogger.log(`Thread name update timed out`);
380
400
  }
381
- catch (e) {
382
- voiceLogger.log(`Could not update thread name:`, e);
401
+ else if (errore.isError(renamed)) {
402
+ voiceLogger.log(`Could not update thread name:`, renamed.message);
403
+ }
404
+ else {
405
+ voiceLogger.log(`Updated thread name to: "${threadName}"`);
383
406
  }
384
407
  }
385
408
  }