kimaki 0.4.46 → 0.4.48

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 (85) hide show
  1. package/dist/cli.js +69 -21
  2. package/dist/commands/abort.js +4 -2
  3. package/dist/commands/add-project.js +2 -2
  4. package/dist/commands/agent.js +4 -4
  5. package/dist/commands/ask-question.js +9 -8
  6. package/dist/commands/compact.js +126 -0
  7. package/dist/commands/create-new-project.js +60 -30
  8. package/dist/commands/fork.js +3 -3
  9. package/dist/commands/merge-worktree.js +23 -10
  10. package/dist/commands/model.js +5 -5
  11. package/dist/commands/permissions.js +5 -3
  12. package/dist/commands/queue.js +2 -2
  13. package/dist/commands/remove-project.js +2 -2
  14. package/dist/commands/resume.js +2 -2
  15. package/dist/commands/session.js +6 -3
  16. package/dist/commands/share.js +2 -2
  17. package/dist/commands/undo-redo.js +2 -2
  18. package/dist/commands/user-command.js +2 -2
  19. package/dist/commands/verbosity.js +5 -5
  20. package/dist/commands/worktree-settings.js +2 -2
  21. package/dist/commands/worktree.js +18 -8
  22. package/dist/config.js +7 -0
  23. package/dist/database.js +10 -7
  24. package/dist/discord-bot.js +30 -12
  25. package/dist/discord-utils.js +2 -2
  26. package/dist/genai-worker-wrapper.js +3 -3
  27. package/dist/genai-worker.js +2 -2
  28. package/dist/genai.js +2 -2
  29. package/dist/interaction-handler.js +6 -2
  30. package/dist/logger.js +57 -9
  31. package/dist/markdown.js +2 -2
  32. package/dist/message-formatting.js +91 -6
  33. package/dist/openai-realtime.js +2 -2
  34. package/dist/opencode.js +19 -25
  35. package/dist/session-handler.js +89 -29
  36. package/dist/system-message.js +11 -9
  37. package/dist/tools.js +3 -2
  38. package/dist/utils.js +1 -0
  39. package/dist/voice-handler.js +2 -2
  40. package/dist/voice.js +2 -2
  41. package/dist/worktree-utils.js +91 -7
  42. package/dist/xml.js +2 -2
  43. package/package.json +3 -3
  44. package/src/cli.ts +108 -21
  45. package/src/commands/abort.ts +4 -2
  46. package/src/commands/add-project.ts +2 -2
  47. package/src/commands/agent.ts +4 -4
  48. package/src/commands/ask-question.ts +9 -8
  49. package/src/commands/compact.ts +148 -0
  50. package/src/commands/create-new-project.ts +87 -36
  51. package/src/commands/fork.ts +3 -3
  52. package/src/commands/merge-worktree.ts +47 -10
  53. package/src/commands/model.ts +5 -5
  54. package/src/commands/permissions.ts +6 -2
  55. package/src/commands/queue.ts +2 -2
  56. package/src/commands/remove-project.ts +2 -2
  57. package/src/commands/resume.ts +2 -2
  58. package/src/commands/session.ts +6 -3
  59. package/src/commands/share.ts +2 -2
  60. package/src/commands/undo-redo.ts +2 -2
  61. package/src/commands/user-command.ts +2 -2
  62. package/src/commands/verbosity.ts +5 -5
  63. package/src/commands/worktree-settings.ts +2 -2
  64. package/src/commands/worktree.ts +20 -7
  65. package/src/config.ts +14 -0
  66. package/src/database.ts +13 -7
  67. package/src/discord-bot.ts +45 -12
  68. package/src/discord-utils.ts +2 -2
  69. package/src/genai-worker-wrapper.ts +3 -3
  70. package/src/genai-worker.ts +2 -2
  71. package/src/genai.ts +2 -2
  72. package/src/interaction-handler.ts +7 -2
  73. package/src/logger.ts +64 -10
  74. package/src/markdown.ts +2 -2
  75. package/src/message-formatting.ts +100 -6
  76. package/src/openai-realtime.ts +2 -2
  77. package/src/opencode.ts +19 -26
  78. package/src/session-handler.ts +102 -29
  79. package/src/system-message.ts +11 -9
  80. package/src/tools.ts +3 -2
  81. package/src/utils.ts +1 -0
  82. package/src/voice-handler.ts +2 -2
  83. package/src/voice.ts +2 -2
  84. package/src/worktree-utils.ts +111 -7
  85. package/src/xml.ts +2 -2
@@ -5,9 +5,9 @@
5
5
  import { RealtimeClient } from '@openai/realtime-api-beta'
6
6
  import { writeFile } from 'fs'
7
7
  import type { Tool } from 'ai'
8
- import { createLogger } from './logger.js'
8
+ import { createLogger, LogPrefix } from './logger.js'
9
9
 
10
- const openaiLogger = createLogger('OPENAI')
10
+ const openaiLogger = createLogger(LogPrefix.OPENAI)
11
11
 
12
12
  // Export the session type for reuse
13
13
  export interface OpenAIRealtimeSession {
package/src/opencode.ts CHANGED
@@ -12,7 +12,7 @@ import {
12
12
  type OpencodeClient as OpencodeClientV2,
13
13
  } from '@opencode-ai/sdk/v2'
14
14
  import * as errore from 'errore'
15
- import { createLogger } from './logger.js'
15
+ import { createLogger, LogPrefix } from './logger.js'
16
16
  import {
17
17
  DirectoryNotAccessibleError,
18
18
  ServerStartError,
@@ -21,7 +21,7 @@ import {
21
21
  type OpenCodeErrors,
22
22
  } from './errors.js'
23
23
 
24
- const opencodeLogger = createLogger('OPENCODE')
24
+ const opencodeLogger = createLogger(LogPrefix.OPENCODE)
25
25
 
26
26
  const opencodeServers = new Map<
27
27
  string,
@@ -54,31 +54,24 @@ async function getOpenPort(): Promise<number> {
54
54
  }
55
55
 
56
56
  async function waitForServer(port: number, maxAttempts = 30): Promise<ServerStartError | true> {
57
+ const endpoint = `http://127.0.0.1:${port}/api/health`
57
58
  for (let i = 0; i < maxAttempts; i++) {
58
- const endpoints = [
59
- `http://127.0.0.1:${port}/api/health`,
60
- `http://127.0.0.1:${port}/`,
61
- `http://127.0.0.1:${port}/api`,
62
- ]
63
-
64
- for (const endpoint of endpoints) {
65
- const response = await errore.tryAsync({
66
- try: () => fetch(endpoint),
67
- catch: (e) => new FetchError({ url: endpoint, cause: e }),
68
- })
69
- if (response instanceof Error) {
70
- // Connection refused or other transient errors - continue polling
71
- opencodeLogger.debug(`Server polling attempt failed: ${response.message}`)
72
- continue
73
- }
74
- if (response.status < 500) {
75
- return true
76
- }
77
- const body = await response.text()
78
- // Fatal errors that won't resolve with retrying
79
- if (body.includes('BunInstallFailedError')) {
80
- return new ServerStartError({ port, reason: body.slice(0, 200) })
81
- }
59
+ const response = await errore.tryAsync({
60
+ try: () => fetch(endpoint),
61
+ catch: (e) => new FetchError({ url: endpoint, cause: e }),
62
+ })
63
+ if (response instanceof Error) {
64
+ // Connection refused or other transient errors - continue polling
65
+ await new Promise((resolve) => setTimeout(resolve, 1000))
66
+ continue
67
+ }
68
+ if (response.status < 500) {
69
+ return true
70
+ }
71
+ const body = await response.text()
72
+ // Fatal errors that won't resolve with retrying
73
+ if (body.includes('BunInstallFailedError')) {
74
+ return new ServerStartError({ port, reason: body.slice(0, 200) })
82
75
  }
83
76
  await new Promise((resolve) => setTimeout(resolve, 1000))
84
77
  }
@@ -24,7 +24,7 @@ import {
24
24
  import { sendThreadMessage, NOTIFY_MESSAGE_FLAGS, SILENT_MESSAGE_FLAGS } from './discord-utils.js'
25
25
  import { formatPart } from './message-formatting.js'
26
26
  import { getOpencodeSystemMessage, type WorktreeInfo } from './system-message.js'
27
- import { createLogger } from './logger.js'
27
+ import { createLogger, LogPrefix } from './logger.js'
28
28
  import { isAbortError } from './utils.js'
29
29
  import {
30
30
  showAskUserQuestionDropdowns,
@@ -38,9 +38,9 @@ import {
38
38
  } from './commands/permissions.js'
39
39
  import * as errore from 'errore'
40
40
 
41
- const sessionLogger = createLogger('SESSION')
42
- const voiceLogger = createLogger('VOICE')
43
- const discordLogger = createLogger('DISCORD')
41
+ const sessionLogger = createLogger(LogPrefix.SESSION)
42
+ const voiceLogger = createLogger(LogPrefix.VOICE)
43
+ const discordLogger = createLogger(LogPrefix.DISCORD)
44
44
 
45
45
  export const abortControllers = new Map<string, AbortController>()
46
46
 
@@ -134,7 +134,8 @@ export async function abortAndRetrySession({
134
134
  sessionLogger.log(`[ABORT+RETRY] Aborting session ${sessionId} for model change`)
135
135
 
136
136
  // Abort with special reason so we don't show "completed" message
137
- controller.abort('model-change')
137
+ sessionLogger.log(`[ABORT] reason=model-change sessionId=${sessionId} - user changed model mid-request, will retry with new model`)
138
+ controller.abort(new Error('model-change'))
138
139
 
139
140
  // Also call the API abort endpoint
140
141
  const getClient = await initializeOpencodeForDirectory(projectDirectory)
@@ -142,11 +143,12 @@ export async function abortAndRetrySession({
142
143
  sessionLogger.error(`[ABORT+RETRY] Failed to initialize OpenCode client:`, getClient.message)
143
144
  return false
144
145
  }
146
+ sessionLogger.log(`[ABORT-API] reason=model-change sessionId=${sessionId} - sending API abort for model change retry`)
145
147
  const abortResult = await errore.tryAsync(() => {
146
148
  return getClient().session.abort({ path: { id: sessionId } })
147
149
  })
148
150
  if (abortResult instanceof Error) {
149
- sessionLogger.log(`[ABORT+RETRY] API abort call failed (may already be done):`, abortResult)
151
+ sessionLogger.log(`[ABORT-API] API abort call failed (may already be done):`, abortResult)
150
152
  }
151
153
 
152
154
  // Small delay to let the abort propagate
@@ -302,7 +304,9 @@ export async function handleOpencodeSession({
302
304
  const existingController = abortControllers.get(session.id)
303
305
  if (existingController) {
304
306
  voiceLogger.log(`[ABORT] Cancelling existing request for session: ${session.id}`)
307
+ sessionLogger.log(`[ABORT] reason=new-request sessionId=${session.id} threadId=${thread.id} - new user message arrived while previous request was still running`)
305
308
  existingController.abort(new Error('New request started'))
309
+ sessionLogger.log(`[ABORT-API] reason=new-request sessionId=${session.id} - sending API abort because new message arrived`)
306
310
  const abortResult = await errore.tryAsync(() => {
307
311
  return getClient().session.abort({
308
312
  path: { id: session.id },
@@ -310,7 +314,7 @@ export async function handleOpencodeSession({
310
314
  })
311
315
  })
312
316
  if (abortResult instanceof Error) {
313
- sessionLogger.log(`[ABORT] Server abort failed (may be already done):`, abortResult)
317
+ sessionLogger.log(`[ABORT-API] Server abort failed (may be already done):`, abortResult)
314
318
  }
315
319
  }
316
320
 
@@ -350,10 +354,10 @@ export async function handleOpencodeSession({
350
354
  }
351
355
  }
352
356
 
353
- // Cancel any pending question tool if user sends a new message (silently, no thread message)
354
- const questionCancelled = await cancelPendingQuestion(thread.id)
355
- if (questionCancelled) {
356
- sessionLogger.log(`[QUESTION] Cancelled pending question due to new message`)
357
+ // Answer any pending question tool with the user's message (silently, no thread message)
358
+ const questionAnswered = await cancelPendingQuestion(thread.id, prompt)
359
+ if (questionAnswered) {
360
+ sessionLogger.log(`[QUESTION] Answered pending question with user message`)
357
361
  }
358
362
 
359
363
  const abortController = new AbortController()
@@ -418,11 +422,15 @@ export async function handleOpencodeSession({
418
422
  let usedAgent: string | undefined
419
423
  let tokensUsedInSession = 0
420
424
  let lastDisplayedContextPercentage = 0
425
+ let lastRateLimitDisplayTime = 0
421
426
  let modelContextLimit: number | undefined
422
427
  let assistantMessageId: string | undefined
423
428
  let handlerPromise: Promise<void> | null = null
424
429
 
425
430
  let typingInterval: NodeJS.Timeout | null = null
431
+ let hasSentParts = false
432
+ let promptResolved = false
433
+ let hasReceivedEvent = false
426
434
 
427
435
  function startTyping(): () => void {
428
436
  if (abortController.signal.aborted) {
@@ -469,13 +477,15 @@ export async function handleOpencodeSession({
469
477
  }
470
478
  }
471
479
 
472
- // Get verbosity setting for this channel (use parent channel for threads)
480
+ // Read verbosity dynamically so mid-session /verbosity changes take effect immediately
473
481
  const verbosityChannelId = channelId || thread.parentId || thread.id
474
- const verbosity = getChannelVerbosity(verbosityChannelId)
482
+ const getVerbosity = () => {
483
+ return getChannelVerbosity(verbosityChannelId)
484
+ }
475
485
 
476
486
  const sendPartMessage = async (part: Part) => {
477
487
  // In text-only mode, only send text parts (the ⬥ diamond messages)
478
- if (verbosity === 'text-only' && part.type !== 'text') {
488
+ if (getVerbosity() === 'text-only' && part.type !== 'text') {
479
489
  return
480
490
  }
481
491
 
@@ -496,6 +506,7 @@ export async function handleOpencodeSession({
496
506
  discordLogger.error(`ERROR: Failed to send part ${part.id}:`, sendResult)
497
507
  return
498
508
  }
509
+ hasSentParts = true
499
510
  sentPartIds.add(part.id)
500
511
 
501
512
  getDatabase()
@@ -587,6 +598,7 @@ export async function handleOpencodeSession({
587
598
  if (msg.sessionID !== session.id) {
588
599
  return
589
600
  }
601
+ hasReceivedEvent = true
590
602
 
591
603
  if (msg.role !== 'assistant') {
592
604
  return
@@ -686,7 +698,7 @@ export async function handleOpencodeSession({
686
698
  const label = `${agent}-${agentSpawnCounts[agent]}`
687
699
  subtaskSessions.set(childSessionId, { label, assistantMessageId: undefined })
688
700
  // Skip task messages in text-only mode
689
- if (verbosity !== 'text-only') {
701
+ if (getVerbosity() !== 'text-only') {
690
702
  const taskDisplay = `┣ task **${label}** _${description}_`
691
703
  await sendThreadMessage(thread, taskDisplay + '\n\n')
692
704
  }
@@ -696,7 +708,7 @@ export async function handleOpencodeSession({
696
708
  return
697
709
  }
698
710
 
699
- if (part.type === 'tool' && part.state.status === 'completed') {
711
+ if (part.type === 'tool' && part.state.status === 'completed' && getVerbosity() !== 'text-only') {
700
712
  const output = part.state.output || ''
701
713
  const outputTokens = Math.ceil(output.length / 4)
702
714
  const largeOutputThreshold = 3000
@@ -750,7 +762,7 @@ export async function handleOpencodeSession({
750
762
  subtaskInfo: { label: string; assistantMessageId?: string },
751
763
  ) => {
752
764
  // In text-only mode, skip all subtask output (they're tool-related)
753
- if (verbosity === 'text-only') {
765
+ if (getVerbosity() === 'text-only') {
754
766
  return
755
767
  }
756
768
  if (part.type === 'step-start' || part.type === 'step-finish') {
@@ -836,13 +848,20 @@ export async function handleOpencodeSession({
836
848
  }
837
849
 
838
850
  const handlePermissionAsked = async (permission: PermissionRequest) => {
839
- if (permission.sessionID !== session.id) {
851
+ const isMainSession = permission.sessionID === session.id
852
+ const isSubtaskSession = subtaskSessions.has(permission.sessionID)
853
+
854
+ if (!isMainSession && !isSubtaskSession) {
840
855
  voiceLogger.log(
841
- `[PERMISSION IGNORED] Permission for different session (expected: ${session.id}, got: ${permission.sessionID})`,
856
+ `[PERMISSION IGNORED] Permission for unknown session (expected: ${session.id} or subtask, got: ${permission.sessionID})`,
842
857
  )
843
858
  return
844
859
  }
845
860
 
861
+ const subtaskLabel = isSubtaskSession
862
+ ? subtaskSessions.get(permission.sessionID)?.label
863
+ : undefined
864
+
846
865
  const dedupeKey = buildPermissionDedupeKey({ permission, directory })
847
866
  const threadPermissions = pendingPermissions.get(thread.id)
848
867
  const existingPending = threadPermissions
@@ -882,7 +901,7 @@ export async function handleOpencodeSession({
882
901
  }
883
902
 
884
903
  sessionLogger.log(
885
- `Permission requested: permission=${permission.permission}, patterns=${permission.patterns.join(', ')}`,
904
+ `Permission requested: permission=${permission.permission}, patterns=${permission.patterns.join(', ')}${subtaskLabel ? `, subtask=${subtaskLabel}` : ''}`,
886
905
  )
887
906
 
888
907
  if (stopTyping) {
@@ -894,6 +913,7 @@ export async function handleOpencodeSession({
894
913
  thread,
895
914
  permission,
896
915
  directory,
916
+ subtaskLabel,
897
917
  })
898
918
 
899
919
  if (!pendingPermissions.has(thread.id)) {
@@ -917,7 +937,10 @@ export async function handleOpencodeSession({
917
937
  reply: string
918
938
  sessionID: string
919
939
  }) => {
920
- if (sessionID !== session.id) {
940
+ const isMainSession = sessionID === session.id
941
+ const isSubtaskSession = subtaskSessions.has(sessionID)
942
+
943
+ if (!isMainSession && !isSubtaskSession) {
921
944
  return
922
945
  }
923
946
 
@@ -988,10 +1011,11 @@ export async function handleOpencodeSession({
988
1011
  )
989
1012
 
990
1013
  setImmediate(() => {
1014
+ const prefixedPrompt = `<discord-user name="${nextMessage.username}" />\n${nextMessage.prompt}`
991
1015
  void errore
992
1016
  .tryAsync(async () => {
993
1017
  return handleOpencodeSession({
994
- prompt: nextMessage.prompt,
1018
+ prompt: prefixedPrompt,
995
1019
  thread,
996
1020
  projectDirectory: directory,
997
1021
  images: nextMessage.images,
@@ -1011,10 +1035,51 @@ export async function handleOpencodeSession({
1011
1035
  })
1012
1036
  }
1013
1037
 
1038
+ const handleSessionStatus = async (properties: {
1039
+ sessionID: string
1040
+ status: { type: 'idle' } | { type: 'retry'; attempt: number; message: string; next: number } | { type: 'busy' }
1041
+ }) => {
1042
+ if (properties.sessionID !== session.id) {
1043
+ return
1044
+ }
1045
+ if (properties.status.type !== 'retry') {
1046
+ return
1047
+ }
1048
+ // Throttle to once per 10 seconds
1049
+ const now = Date.now()
1050
+ if (now - lastRateLimitDisplayTime < 10_000) {
1051
+ return
1052
+ }
1053
+ lastRateLimitDisplayTime = now
1054
+
1055
+ const { attempt, message, next } = properties.status
1056
+ const remainingMs = Math.max(0, next - now)
1057
+ const remainingSec = Math.ceil(remainingMs / 1000)
1058
+
1059
+ const duration = (() => {
1060
+ if (remainingSec < 60) {
1061
+ return `${remainingSec}s`
1062
+ }
1063
+ const mins = Math.floor(remainingSec / 60)
1064
+ const secs = remainingSec % 60
1065
+ return secs > 0 ? `${mins}m ${secs}s` : `${mins}m`
1066
+ })()
1067
+
1068
+ const chunk = `⬦ ${message} - retrying in ${duration} (attempt #${attempt})`
1069
+ await thread.send({ content: chunk, flags: SILENT_MESSAGE_FLAGS })
1070
+ }
1071
+
1014
1072
  const handleSessionIdle = (idleSessionId: string) => {
1015
1073
  if (idleSessionId === session.id) {
1016
- sessionLogger.log(`[SESSION IDLE] Session ${session.id} is idle, aborting`)
1017
- abortController.abort('finished')
1074
+ if (!promptResolved || !hasReceivedEvent) {
1075
+ sessionLogger.log(
1076
+ `[SESSION IDLE] Ignoring idle event for ${session.id} (prompt not resolved or no events yet)`,
1077
+ )
1078
+ return
1079
+ }
1080
+ sessionLogger.log(`[SESSION IDLE] Session ${session.id} is idle, ending stream`)
1081
+ sessionLogger.log(`[ABORT] reason=finished sessionId=${session.id} threadId=${thread.id} - session completed normally, received idle event after prompt resolved`)
1082
+ abortController.abort(new Error('finished'))
1018
1083
  return
1019
1084
  }
1020
1085
 
@@ -1051,6 +1116,9 @@ export async function handleOpencodeSession({
1051
1116
  case 'session.idle':
1052
1117
  handleSessionIdle(event.properties.sessionID)
1053
1118
  break
1119
+ case 'session.status':
1120
+ await handleSessionStatus(event.properties)
1121
+ break
1054
1122
  default:
1055
1123
  break
1056
1124
  }
@@ -1079,7 +1147,8 @@ export async function handleOpencodeSession({
1079
1147
  stopTyping = null
1080
1148
  }
1081
1149
 
1082
- if (!abortController.signal.aborted || abortController.signal.reason === 'finished') {
1150
+ const abortReason = (abortController.signal.reason as Error)?.message
1151
+ if (!abortController.signal.aborted || abortReason === 'finished') {
1083
1152
  const sessionDuration = prettyMilliseconds(Date.now() - sessionStartTime)
1084
1153
  const attachCommand = port ? ` ⋅ ${session.id}` : ''
1085
1154
  const modelInfo = usedModel ? ` ⋅ ${usedModel}` : ''
@@ -1155,8 +1224,9 @@ export async function handleOpencodeSession({
1155
1224
  // Send the queued message as a new prompt (recursive call)
1156
1225
  // Use setImmediate to avoid blocking and allow this finally to complete
1157
1226
  setImmediate(() => {
1227
+ const prefixedPrompt = `<discord-user name="${nextMessage.username}" />\n${nextMessage.prompt}`
1158
1228
  handleOpencodeSession({
1159
- prompt: nextMessage.prompt,
1229
+ prompt: prefixedPrompt,
1160
1230
  thread,
1161
1231
  projectDirectory,
1162
1232
  images: nextMessage.images,
@@ -1170,7 +1240,7 @@ export async function handleOpencodeSession({
1170
1240
  }
1171
1241
  } else {
1172
1242
  sessionLogger.log(
1173
- `Session was aborted (reason: ${abortController.signal.reason}), skipping duration message`,
1243
+ `Session was aborted (reason: ${abortReason}), skipping duration message`,
1174
1244
  )
1175
1245
  }
1176
1246
  }
@@ -1250,6 +1320,8 @@ export async function handleOpencodeSession({
1250
1320
  }
1251
1321
  : undefined
1252
1322
 
1323
+ hasSentParts = false
1324
+
1253
1325
  const response = command
1254
1326
  ? await getClient().session.command({
1255
1327
  path: { id: session.id },
@@ -1289,7 +1361,7 @@ export async function handleOpencodeSession({
1289
1361
  throw new Error(`OpenCode API error (${response.response.status}): ${errorMessage}`)
1290
1362
  }
1291
1363
 
1292
- abortController.abort('finished')
1364
+ promptResolved = true
1293
1365
 
1294
1366
  sessionLogger.log(`Successfully sent prompt, got response`)
1295
1367
 
@@ -1325,7 +1397,8 @@ export async function handleOpencodeSession({
1325
1397
  }
1326
1398
 
1327
1399
  sessionLogger.error(`ERROR: Failed to send prompt:`, promptError)
1328
- abortController.abort('error')
1400
+ sessionLogger.log(`[ABORT] reason=error sessionId=${session.id} threadId=${thread.id} - prompt failed with error: ${(promptError as Error).message}`)
1401
+ abortController.abort(new Error('error'))
1329
1402
 
1330
1403
  if (originalMessage) {
1331
1404
  const reactionResult = await errore.tryAsync(async () => {
@@ -133,19 +133,21 @@ headings are discouraged anyway. instead try to use bold text for titles which r
133
133
 
134
134
  you can create diagrams wrapping them in code blocks.
135
135
 
136
- ## ending conversations with options
136
+ ## proactivity
137
137
 
138
- 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.
138
+ Be proactive. When the user asks you to do something, do it. Do NOT stop to ask for confirmation.
139
139
 
140
- 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.
140
+ Only ask questions when the request is genuinely ambiguous with multiple valid approaches, or the action is destructive and irreversible.
141
141
 
142
- Examples:
143
- - After showing a plan: offer "Start implementing?" with Yes/No options
144
- - After completing edits: offer "Commit changes?" with Yes/No options
145
- - After debugging: offer "How to proceed?" with options like "Apply fix", "Investigate further", "Try different approach"
142
+ ## ending conversations with options
146
143
 
147
- The user can always select "Other" to type a custom response if the provided options don't fit their needs, or if the plan needs updating.
144
+ After **completing** a task, use the question tool to offer follow-up options. The question tool must be called last, after all text parts.
148
145
 
149
- This makes the interaction more guided and reduces friction for the user.
146
+ IMPORTANT: Do NOT use the question tool to ask permission before doing work. Do the work first, then offer follow-ups.
147
+
148
+ Examples:
149
+ - After completing edits: offer "Commit changes?" or "Run tests?"
150
+ - After debugging: offer "Apply fix", "Investigate further", "Try different approach"
151
+ - After a genuinely ambiguous request where you cannot infer intent: offer the different approaches
150
152
  `
151
153
  }
package/src/tools.ts CHANGED
@@ -12,10 +12,10 @@ import {
12
12
  type AssistantMessage,
13
13
  type Provider,
14
14
  } from '@opencode-ai/sdk'
15
- import { createLogger } from './logger.js'
15
+ import { createLogger, LogPrefix } from './logger.js'
16
16
  import * as errore from 'errore'
17
17
 
18
- const toolsLogger = createLogger('TOOLS')
18
+ const toolsLogger = createLogger(LogPrefix.TOOLS)
19
19
 
20
20
  import { ShareMarkdown } from './markdown.js'
21
21
  import { formatDistanceToNow } from './utils.js'
@@ -343,6 +343,7 @@ export async function getTools({
343
343
  }),
344
344
  execute: async ({ sessionId }) => {
345
345
  try {
346
+ toolsLogger.log(`[ABORT] reason=voice-tool sessionId=${sessionId} - user requested abort via voice assistant tool`)
346
347
  const result = await getClient().session.abort({
347
348
  path: { id: sessionId },
348
349
  })
package/src/utils.ts CHANGED
@@ -73,6 +73,7 @@ export function isAbortError(error: unknown, signal?: AbortSignal): error is Err
73
73
  error.name === 'Aborterror' ||
74
74
  error.name === 'aborterror' ||
75
75
  error.name.toLowerCase() === 'aborterror' ||
76
+ error.name === 'MessageAbortedError' ||
76
77
  error.message?.includes('aborted') ||
77
78
  (signal?.aborted ?? false))) ||
78
79
  (error instanceof DOMException && error.name === 'AbortError')
@@ -37,9 +37,9 @@ import {
37
37
  import { transcribeAudio } from './voice.js'
38
38
  import { FetchError } from './errors.js'
39
39
 
40
- import { createLogger } from './logger.js'
40
+ import { createLogger, LogPrefix } from './logger.js'
41
41
 
42
- const voiceLogger = createLogger('VOICE')
42
+ const voiceLogger = createLogger(LogPrefix.VOICE)
43
43
 
44
44
  export type VoiceConnectionData = {
45
45
  connection: VoiceConnection
package/src/voice.ts CHANGED
@@ -5,7 +5,7 @@
5
5
 
6
6
  import { GoogleGenAI, Type, type Content, type Part, type Tool } from '@google/genai'
7
7
  import * as errore from 'errore'
8
- import { createLogger } from './logger.js'
8
+ import { createLogger, LogPrefix } from './logger.js'
9
9
  import { glob } from 'glob'
10
10
  import { ripGrep } from 'ripgrep-js'
11
11
  import {
@@ -19,7 +19,7 @@ import {
19
19
  GlobSearchError,
20
20
  } from './errors.js'
21
21
 
22
- const voiceLogger = createLogger('VOICE')
22
+ const voiceLogger = createLogger(LogPrefix.VOICE)
23
23
 
24
24
  export type TranscriptionToolRunner = ({
25
25
  name,