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.
- package/dist/cli.js +69 -21
- package/dist/commands/abort.js +4 -2
- package/dist/commands/add-project.js +2 -2
- package/dist/commands/agent.js +4 -4
- package/dist/commands/ask-question.js +9 -8
- package/dist/commands/compact.js +126 -0
- package/dist/commands/create-new-project.js +60 -30
- package/dist/commands/fork.js +3 -3
- package/dist/commands/merge-worktree.js +23 -10
- package/dist/commands/model.js +5 -5
- package/dist/commands/permissions.js +5 -3
- package/dist/commands/queue.js +2 -2
- package/dist/commands/remove-project.js +2 -2
- package/dist/commands/resume.js +2 -2
- package/dist/commands/session.js +6 -3
- package/dist/commands/share.js +2 -2
- package/dist/commands/undo-redo.js +2 -2
- package/dist/commands/user-command.js +2 -2
- package/dist/commands/verbosity.js +5 -5
- package/dist/commands/worktree-settings.js +2 -2
- package/dist/commands/worktree.js +18 -8
- package/dist/config.js +7 -0
- package/dist/database.js +10 -7
- package/dist/discord-bot.js +30 -12
- package/dist/discord-utils.js +2 -2
- package/dist/genai-worker-wrapper.js +3 -3
- package/dist/genai-worker.js +2 -2
- package/dist/genai.js +2 -2
- package/dist/interaction-handler.js +6 -2
- package/dist/logger.js +57 -9
- package/dist/markdown.js +2 -2
- package/dist/message-formatting.js +91 -6
- package/dist/openai-realtime.js +2 -2
- package/dist/opencode.js +19 -25
- package/dist/session-handler.js +89 -29
- package/dist/system-message.js +11 -9
- package/dist/tools.js +3 -2
- package/dist/utils.js +1 -0
- package/dist/voice-handler.js +2 -2
- package/dist/voice.js +2 -2
- package/dist/worktree-utils.js +91 -7
- package/dist/xml.js +2 -2
- package/package.json +3 -3
- package/src/cli.ts +108 -21
- package/src/commands/abort.ts +4 -2
- package/src/commands/add-project.ts +2 -2
- package/src/commands/agent.ts +4 -4
- package/src/commands/ask-question.ts +9 -8
- package/src/commands/compact.ts +148 -0
- package/src/commands/create-new-project.ts +87 -36
- package/src/commands/fork.ts +3 -3
- package/src/commands/merge-worktree.ts +47 -10
- package/src/commands/model.ts +5 -5
- package/src/commands/permissions.ts +6 -2
- package/src/commands/queue.ts +2 -2
- package/src/commands/remove-project.ts +2 -2
- package/src/commands/resume.ts +2 -2
- package/src/commands/session.ts +6 -3
- package/src/commands/share.ts +2 -2
- package/src/commands/undo-redo.ts +2 -2
- package/src/commands/user-command.ts +2 -2
- package/src/commands/verbosity.ts +5 -5
- package/src/commands/worktree-settings.ts +2 -2
- package/src/commands/worktree.ts +20 -7
- package/src/config.ts +14 -0
- package/src/database.ts +13 -7
- package/src/discord-bot.ts +45 -12
- package/src/discord-utils.ts +2 -2
- package/src/genai-worker-wrapper.ts +3 -3
- package/src/genai-worker.ts +2 -2
- package/src/genai.ts +2 -2
- package/src/interaction-handler.ts +7 -2
- package/src/logger.ts +64 -10
- package/src/markdown.ts +2 -2
- package/src/message-formatting.ts +100 -6
- package/src/openai-realtime.ts +2 -2
- package/src/opencode.ts +19 -26
- package/src/session-handler.ts +102 -29
- package/src/system-message.ts +11 -9
- package/src/tools.ts +3 -2
- package/src/utils.ts +1 -0
- package/src/voice-handler.ts +2 -2
- package/src/voice.ts +2 -2
- package/src/worktree-utils.ts +111 -7
- package/src/xml.ts +2 -2
package/src/openai-realtime.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
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
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
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
|
}
|
package/src/session-handler.ts
CHANGED
|
@@ -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(
|
|
42
|
-
const voiceLogger = createLogger(
|
|
43
|
-
const discordLogger = createLogger(
|
|
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
|
-
|
|
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
|
|
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
|
-
//
|
|
354
|
-
const
|
|
355
|
-
if (
|
|
356
|
-
sessionLogger.log(`[QUESTION]
|
|
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
|
-
//
|
|
480
|
+
// Read verbosity dynamically so mid-session /verbosity changes take effect immediately
|
|
473
481
|
const verbosityChannelId = channelId || thread.parentId || thread.id
|
|
474
|
-
const
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
1017
|
-
|
|
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
|
-
|
|
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:
|
|
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: ${
|
|
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
|
-
|
|
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
|
-
|
|
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 () => {
|
package/src/system-message.ts
CHANGED
|
@@ -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
|
-
##
|
|
136
|
+
## proactivity
|
|
137
137
|
|
|
138
|
-
|
|
138
|
+
Be proactive. When the user asks you to do something, do it. Do NOT stop to ask for confirmation.
|
|
139
139
|
|
|
140
|
-
|
|
140
|
+
Only ask questions when the request is genuinely ambiguous with multiple valid approaches, or the action is destructive and irreversible.
|
|
141
141
|
|
|
142
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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')
|
package/src/voice-handler.ts
CHANGED
|
@@ -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(
|
|
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(
|
|
22
|
+
const voiceLogger = createLogger(LogPrefix.VOICE)
|
|
23
23
|
|
|
24
24
|
export type TranscriptionToolRunner = ({
|
|
25
25
|
name,
|