kimaki 0.26.0 → 0.28.0
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/agent-model.e2e.test.js +250 -164
- package/dist/anthropic-account-identity.js +9 -1
- package/dist/anthropic-auth-plugin.js +10 -13
- package/dist/anthropic-auth-plugin.test.js +17 -0
- package/dist/anthropic-auth-state.js +1 -0
- package/dist/bash-tool-schema-plugin.js +32 -0
- package/dist/bash-tool-schema-plugin.test.js +49 -0
- package/dist/bin.js +73 -19
- package/dist/cli-commands/maintenance.js +46 -7
- package/dist/cli-commands/project.js +11 -27
- package/dist/cli-commands/send.js +30 -1
- package/dist/cli-commands/session.js +322 -90
- package/dist/cli-commands/task.js +34 -15
- package/dist/cli-parsing.test.js +83 -4
- package/dist/cli-runner.js +44 -8
- package/dist/cli-runner.test.js +27 -0
- package/dist/cli-send-thread.e2e.test.js +9 -9
- package/dist/cli.js +32 -0
- package/dist/commands/abort.js +5 -1
- package/dist/commands/action-buttons.js +2 -1
- package/dist/commands/add-project.js +4 -7
- package/dist/commands/agent.js +164 -32
- package/dist/commands/ask-question.js +23 -33
- package/dist/commands/ask-question.test.js +43 -1
- package/dist/commands/btw.js +28 -6
- package/dist/commands/file-upload.js +2 -1
- package/dist/commands/fork-subagent.js +2 -4
- package/dist/commands/fork.js +4 -4
- package/dist/commands/last-sessions.js +17 -10
- package/dist/commands/login.js +6 -0
- package/dist/commands/merge-worktree.js +47 -14
- package/dist/commands/model-variant.js +8 -1
- package/dist/commands/model.js +16 -9
- package/dist/commands/multioauth.js +1 -0
- package/dist/commands/new-worktree.js +170 -102
- package/dist/commands/queue.js +3 -2
- package/dist/commands/resume.js +4 -4
- package/dist/commands/session.js +78 -63
- package/dist/commands/user-command.js +6 -8
- package/dist/commands/worktrees.js +5 -13
- package/dist/config.js +16 -1
- package/dist/cpu-profiler.js +211 -0
- package/dist/cpu-profiler.test.js +160 -0
- package/dist/database.js +152 -3
- package/dist/db.js +33 -0
- package/dist/db.test.js +267 -1
- package/dist/default-channel-provisioning.test.js +148 -0
- package/dist/discord-bot.js +517 -488
- package/dist/discord-command-registration.js +25 -10
- package/dist/discord-command-registration.test.js +17 -0
- package/dist/discord-utils.js +97 -5
- package/dist/discord-utils.test.js +151 -1
- package/dist/errors.js +10 -0
- package/dist/exec-async.js +17 -5
- package/dist/external-directory-permissions.e2e.test.js +11 -7
- package/dist/external-directory-project-deny.e2e.test.js +5 -3
- package/dist/external-opencode-sync.js +94 -17
- package/dist/external-opencode-sync.test.js +206 -0
- package/dist/extract-sdk-error-message.test.js +13 -0
- package/dist/file-edit-log.js +197 -0
- package/dist/file-edit-log.test.js +258 -0
- package/dist/format-tables.js +52 -0
- package/dist/format-tables.test.js +93 -0
- package/dist/gateway-proxy-reconnect.e2e.test.js +28 -8
- package/dist/gateway-proxy.e2e.test.js +18 -18
- package/dist/git-worktree-core.js +182 -17
- package/dist/interaction-handler.js +416 -398
- package/dist/ipc-tools-plugin.js +68 -3
- package/dist/kimaki-opencode-plugin-loading.e2e.test.js +122 -58
- package/dist/kimaki-opencode-plugin.js +9 -3
- package/dist/kimaki-workspace-adaptor.js +33 -10
- package/dist/logger.js +11 -1
- package/dist/markdown.js +3 -1
- package/dist/markdown.test.js +11 -3
- package/dist/message-formatting.js +181 -51
- package/dist/message-formatting.test.js +529 -22
- package/dist/message-preprocessing.js +105 -1
- package/dist/oauth-rotation-shared.js +10 -0
- package/dist/openai-auth-plugin.js +1 -0
- package/dist/openai-auth-state.js +1 -0
- package/dist/opencode-command.js +28 -0
- package/dist/opencode-command.test.js +109 -1
- package/dist/opencode-interrupt-plugin.js +4 -0
- package/dist/opencode-interrupt-plugin.test.js +24 -0
- package/dist/opencode.js +82 -15
- package/dist/project-add.test.js +62 -0
- package/dist/question-abort-clears-dropdown.e2e.test.js +89 -0
- package/dist/question-abort-retry-text-before-dropdown.e2e.test.js +150 -0
- package/dist/question-text-before-dropdown.e2e.test.js +52 -0
- package/dist/queue-advanced-abort.e2e.test.js +8 -10
- package/dist/queue-advanced-action-buttons.e2e.test.js +8 -8
- package/dist/queue-advanced-e2e-setup.js +78 -5
- package/dist/queue-advanced-footer.e2e.test.js +60 -269
- package/dist/queue-advanced-model-switch.e2e.test.js +9 -11
- package/dist/queue-advanced-permissions-typing.e2e.test.js +15 -15
- package/dist/queue-advanced-question.e2e.test.js +32 -27
- package/dist/queue-advanced-typing-interrupt.e2e.test.js +10 -12
- package/dist/queue-advanced-typing.e2e.test.js +7 -9
- package/dist/queue-delete-message.e2e.test.js +3 -3
- package/dist/queue-drain-after-interactive-ui.e2e.test.js +5 -5
- package/dist/queue-interrupt-drain.e2e.test.js +122 -12
- package/dist/queue-question-select-drain.e2e.test.js +34 -132
- package/dist/runtime-lifecycle.e2e.test.js +21 -51
- package/dist/schema.js +35 -1
- package/dist/session-handler/event-stream-state.js +391 -50
- package/dist/session-handler/event-stream-state.test.js +736 -8
- package/dist/session-handler/model-utils.js +172 -5
- package/dist/session-handler/model-utils.test.js +445 -0
- package/dist/session-handler/opencode-session-event-log.js +30 -1
- package/dist/session-handler/thread-ingress.test.js +41 -0
- package/dist/session-handler/thread-runtime-state.js +2 -0
- package/dist/session-handler/thread-session-runtime-summary.test.js +121 -0
- package/dist/session-handler/thread-session-runtime.js +574 -299
- package/dist/session-search.js +123 -4
- package/dist/session-search.test.js +248 -1
- package/dist/session-sleep.test.js +224 -0
- package/dist/session-title-rename.test.js +37 -39
- package/dist/sleep.e2e.test.js +494 -0
- package/dist/startup-time.e2e.test.js +2 -1
- package/dist/store.js +3 -0
- package/dist/system-message.js +102 -16
- package/dist/system-message.test.js +192 -15
- package/dist/task-runner.js +96 -5
- package/dist/task-schedule.js +87 -1
- package/dist/task-schedule.test.js +149 -1
- package/dist/test-utils.js +17 -9
- package/dist/thinking-utils.js +10 -0
- package/dist/thinking-utils.test.js +35 -0
- package/dist/thread-message-queue.e2e.test.js +65 -213
- package/dist/undo-redo.e2e.test.js +17 -9
- package/dist/voice-attachment.js +23 -1
- package/dist/voice-handler.js +21 -4
- package/dist/voice-message.e2e.test.js +144 -30
- package/dist/voice-routing-input.e2e.test.js +195 -0
- package/dist/voice-routing-question.e2e.test.js +125 -0
- package/dist/voice.js +28 -8
- package/dist/voice.test.js +96 -30
- package/dist/worktree-clone-isolation.e2e.test.js +188 -0
- package/dist/worktree-lifecycle.e2e.test.js +55 -36
- package/dist/worktrees.js +226 -443
- package/dist/worktrees.test.js +435 -58
- package/dist/xai-auth-plugin.js +1 -0
- package/dist/xai-auth-state.js +1 -0
- package/package.json +8 -6
- package/skills/bump-holocron-consumers/SKILL.md +137 -0
- package/skills/critique/SKILL.md +4 -4
- package/skills/errore/SKILL.md +60 -218
- package/skills/goke/SKILL.md +150 -10
- package/skills/holocron/SKILL.md +154 -17
- package/skills/npm-package/SKILL.md +55 -35
- package/skills/profano/SKILL.md +17 -2
- package/skills/spiceflow/SKILL.md +39 -3
- package/skills/strada/SKILL.md +39 -0
- package/skills/zele/SKILL.md +19 -0
- package/src/agent-model.e2e.test.ts +287 -186
- package/src/anthropic-account-identity.ts +20 -1
- package/src/anthropic-auth-plugin.test.ts +19 -0
- package/src/anthropic-auth-plugin.ts +12 -16
- package/src/anthropic-auth-state.ts +1 -0
- package/src/bash-tool-schema-plugin.test.ts +55 -0
- package/src/bash-tool-schema-plugin.ts +46 -0
- package/src/bin.ts +78 -26
- package/src/cli-commands/maintenance.ts +59 -13
- package/src/cli-commands/project.ts +14 -35
- package/src/cli-commands/send.ts +31 -1
- package/src/cli-commands/session.ts +374 -114
- package/src/cli-commands/task.ts +34 -15
- package/src/cli-parsing.test.ts +103 -4
- package/src/cli-runner.test.ts +49 -0
- package/src/cli-runner.ts +44 -7
- package/src/cli-send-thread.e2e.test.ts +9 -11
- package/src/cli.ts +53 -0
- package/src/commands/abort.ts +6 -1
- package/src/commands/action-buttons.ts +2 -1
- package/src/commands/add-project.ts +4 -8
- package/src/commands/agent.ts +205 -29
- package/src/commands/ask-question.test.ts +48 -1
- package/src/commands/ask-question.ts +23 -33
- package/src/commands/btw.ts +40 -5
- package/src/commands/file-upload.ts +2 -1
- package/src/commands/fork-subagent.ts +2 -3
- package/src/commands/fork.ts +4 -3
- package/src/commands/last-sessions.ts +10 -4
- package/src/commands/login.ts +6 -0
- package/src/commands/merge-worktree.ts +54 -16
- package/src/commands/model-variant.ts +9 -1
- package/src/commands/model.ts +20 -9
- package/src/commands/multioauth.ts +1 -0
- package/src/commands/new-worktree.ts +138 -55
- package/src/commands/queue.ts +3 -2
- package/src/commands/resume.ts +4 -3
- package/src/commands/session.ts +94 -70
- package/src/commands/user-command.ts +6 -6
- package/src/commands/worktrees.ts +5 -12
- package/src/config.ts +18 -1
- package/src/cpu-profiler.test.ts +183 -0
- package/src/cpu-profiler.ts +293 -0
- package/src/database.ts +235 -3
- package/src/db.test.ts +293 -0
- package/src/db.ts +38 -0
- package/src/default-channel-provisioning.test.ts +155 -0
- package/src/discord-bot.ts +78 -36
- package/src/discord-command-registration.test.ts +20 -0
- package/src/discord-command-registration.ts +42 -12
- package/src/discord-utils.test.ts +176 -0
- package/src/discord-utils.ts +152 -7
- package/src/errors.ts +12 -0
- package/src/exec-async.ts +20 -11
- package/src/external-directory-permissions.e2e.test.ts +11 -7
- package/src/external-directory-project-deny.e2e.test.ts +5 -3
- package/src/external-opencode-sync.test.ts +248 -0
- package/src/external-opencode-sync.ts +122 -19
- package/src/extract-sdk-error-message.test.ts +18 -0
- package/src/file-edit-log.test.ts +287 -0
- package/src/file-edit-log.ts +283 -0
- package/src/format-tables.test.ts +101 -0
- package/src/format-tables.ts +75 -0
- package/src/gateway-proxy-reconnect.e2e.test.ts +38 -8
- package/src/gateway-proxy.e2e.test.ts +18 -17
- package/src/git-worktree-core.ts +247 -20
- package/src/interaction-handler.ts +36 -13
- package/src/ipc-tools-plugin.ts +82 -5
- package/src/kimaki-opencode-plugin-loading.e2e.test.ts +188 -97
- package/src/kimaki-opencode-plugin.ts +9 -3
- package/src/kimaki-workspace-adaptor.ts +40 -11
- package/src/logger.ts +14 -4
- package/src/markdown.test.ts +13 -3
- package/src/markdown.ts +4 -1
- package/src/message-formatting.test.ts +672 -25
- package/src/message-formatting.ts +242 -53
- package/src/message-preprocessing.ts +116 -1
- package/src/oauth-rotation-shared.ts +10 -0
- package/src/openai-auth-plugin.ts +1 -0
- package/src/openai-auth-state.ts +1 -0
- package/src/opencode-command.test.ts +140 -0
- package/src/opencode-command.ts +38 -0
- package/src/opencode-interrupt-plugin.test.ts +32 -0
- package/src/opencode-interrupt-plugin.ts +3 -0
- package/src/opencode.ts +115 -15
- package/src/project-add.test.ts +80 -0
- package/src/question-abort-clears-dropdown.e2e.test.ts +109 -0
- package/src/question-abort-retry-text-before-dropdown.e2e.test.ts +172 -0
- package/src/question-text-before-dropdown.e2e.test.ts +67 -0
- package/src/queue-advanced-abort.e2e.test.ts +8 -9
- package/src/queue-advanced-action-buttons.e2e.test.ts +8 -8
- package/src/queue-advanced-e2e-setup.ts +83 -4
- package/src/queue-advanced-footer.e2e.test.ts +67 -315
- package/src/queue-advanced-model-switch.e2e.test.ts +9 -10
- package/src/queue-advanced-permissions-typing.e2e.test.ts +15 -15
- package/src/queue-advanced-question.e2e.test.ts +36 -30
- package/src/queue-advanced-typing-interrupt.e2e.test.ts +11 -11
- package/src/queue-advanced-typing.e2e.test.ts +8 -8
- package/src/queue-delete-message.e2e.test.ts +3 -3
- package/src/queue-drain-after-interactive-ui.e2e.test.ts +5 -5
- package/src/queue-interrupt-drain.e2e.test.ts +133 -12
- package/src/queue-question-select-drain.e2e.test.ts +36 -161
- package/src/runtime-lifecycle.e2e.test.ts +21 -54
- package/src/schema.sql +15 -2
- package/src/schema.ts +37 -1
- package/src/session-handler/event-stream-state.test.ts +790 -7
- package/src/session-handler/event-stream-state.ts +568 -53
- package/src/session-handler/model-utils.test.ts +537 -0
- package/src/session-handler/model-utils.ts +281 -5
- package/src/session-handler/opencode-session-event-log.ts +31 -1
- package/src/session-handler/thread-ingress.test.ts +54 -0
- package/src/session-handler/thread-runtime-state.ts +3 -0
- package/src/session-handler/thread-session-runtime-summary.test.ts +169 -0
- package/src/session-handler/thread-session-runtime.ts +712 -358
- package/src/session-search.test.ts +281 -0
- package/src/session-search.ts +241 -7
- package/src/session-sleep.test.ts +294 -0
- package/src/session-title-rename.test.ts +54 -48
- package/src/sleep.e2e.test.ts +565 -0
- package/src/startup-time.e2e.test.ts +5 -1
- package/src/store.ts +22 -0
- package/src/system-message.test.ts +226 -15
- package/src/system-message.ts +130 -16
- package/src/task-runner.ts +138 -3
- package/src/task-schedule.test.ts +170 -0
- package/src/task-schedule.ts +141 -1
- package/src/test-utils.ts +21 -10
- package/src/thinking-utils.test.ts +46 -0
- package/src/thinking-utils.ts +21 -0
- package/src/thread-message-queue.e2e.test.ts +65 -245
- package/src/undo-redo.e2e.test.ts +17 -9
- package/src/voice-attachment.ts +29 -1
- package/src/voice-handler.ts +23 -4
- package/src/voice-message.e2e.test.ts +149 -29
- package/src/voice-routing-input.e2e.test.ts +203 -0
- package/src/voice-routing-question.e2e.test.ts +131 -0
- package/src/voice.test.ts +107 -29
- package/src/voice.ts +34 -6
- package/src/worktree-clone-isolation.e2e.test.ts +213 -0
- package/src/worktree-lifecycle.e2e.test.ts +55 -35
- package/src/worktrees.test.ts +505 -64
- package/src/worktrees.ts +257 -603
- package/src/xai-auth-plugin.ts +1 -0
- package/src/xai-auth-state.ts +1 -0
- package/dist/message-finish-field.e2e.test.js +0 -169
- package/dist/subagent-rate-limit-plugin.js +0 -184
- package/src/message-finish-field.e2e.test.ts +0 -196
- package/src/subagent-rate-limit-plugin.ts +0 -228
|
@@ -1,12 +1,22 @@
|
|
|
1
1
|
// Model resolution utilities.
|
|
2
2
|
// getDefaultModel resolves the default model from OpenCode when no user preference is set.
|
|
3
|
+
// listModels wraps provider.list() with a per-directory memoized cache.
|
|
3
4
|
|
|
4
5
|
import fs from 'node:fs'
|
|
5
6
|
import path from 'node:path'
|
|
6
7
|
import { xdgState } from 'xdg-basedir'
|
|
7
8
|
import * as errore from 'errore'
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
9
|
+
import type { OpencodeClient, Provider } from '@opencode-ai/sdk/v2'
|
|
10
|
+
import {
|
|
11
|
+
formatCandidateRef,
|
|
12
|
+
PROVIDER_ID as SUBROUTER_PROVIDER_ID,
|
|
13
|
+
resolveLiveModel,
|
|
14
|
+
} from '@subrouter/cli'
|
|
15
|
+
import { InvalidModelError, OpenCodeSdkError } from '../errors.js'
|
|
16
|
+
import {
|
|
17
|
+
initializeOpencodeForDirectory,
|
|
18
|
+
subscribeOpencodeServerLifecycle,
|
|
19
|
+
} from '../opencode.js'
|
|
10
20
|
import { createLogger, LogPrefix } from '../logger.js'
|
|
11
21
|
import type { ScheduledTaskScheduleKind } from '../database.js'
|
|
12
22
|
|
|
@@ -58,7 +68,7 @@ function getRecentModelsFromTuiState(): Array<{
|
|
|
58
68
|
/**
|
|
59
69
|
* Parse a model string in format "provider/model" into providerID and modelID.
|
|
60
70
|
*/
|
|
61
|
-
function
|
|
71
|
+
export function parseModelId(
|
|
62
72
|
model: string,
|
|
63
73
|
): { providerID: string; modelID: string } | undefined {
|
|
64
74
|
const [providerID, ...modelParts] = model.split('/')
|
|
@@ -85,7 +95,7 @@ function getModelFromProjectConfig({
|
|
|
85
95
|
if (!parsed.model) {
|
|
86
96
|
return undefined
|
|
87
97
|
}
|
|
88
|
-
return
|
|
98
|
+
return parseModelId(parsed.model)
|
|
89
99
|
})
|
|
90
100
|
if (result instanceof Error) return undefined
|
|
91
101
|
return result
|
|
@@ -107,6 +117,272 @@ function isModelValid(
|
|
|
107
117
|
return isConnected && !!modelExists
|
|
108
118
|
}
|
|
109
119
|
|
|
120
|
+
export type ListedModel = {
|
|
121
|
+
providerID: string
|
|
122
|
+
modelID: string
|
|
123
|
+
name: string
|
|
124
|
+
connected: boolean
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
type ProviderModelLookup = {
|
|
128
|
+
id: string
|
|
129
|
+
models?: Record<string, { name?: string } | undefined>
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** SDK display name from provider.list. */
|
|
133
|
+
export function getProviderModelName({
|
|
134
|
+
providers,
|
|
135
|
+
providerID,
|
|
136
|
+
modelID,
|
|
137
|
+
}: {
|
|
138
|
+
providers: ProviderModelLookup[]
|
|
139
|
+
providerID?: string
|
|
140
|
+
modelID?: string
|
|
141
|
+
}): string | undefined {
|
|
142
|
+
if (!providerID || !modelID) return undefined
|
|
143
|
+
const provider = providers.find((p) => p.id === providerID)
|
|
144
|
+
const model = provider?.models?.[modelID]
|
|
145
|
+
if (!model) return undefined
|
|
146
|
+
if (typeof model.name === 'string' && model.name.trim()) return model.name
|
|
147
|
+
return modelID
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Use the SDK name only when it is the model id, or the model id plus `(live-model)`. */
|
|
151
|
+
export function displayedModelLabel({
|
|
152
|
+
modelID,
|
|
153
|
+
name,
|
|
154
|
+
}: {
|
|
155
|
+
modelID: string
|
|
156
|
+
name?: string
|
|
157
|
+
}): string {
|
|
158
|
+
if (name && (name === modelID || name.startsWith(`${modelID} (`))) return name
|
|
159
|
+
return modelID
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function formatDisplayedModelId({
|
|
163
|
+
providerID,
|
|
164
|
+
modelID,
|
|
165
|
+
name,
|
|
166
|
+
}: {
|
|
167
|
+
providerID?: string
|
|
168
|
+
modelID?: string
|
|
169
|
+
name?: string
|
|
170
|
+
}): string | undefined {
|
|
171
|
+
if (!providerID || !modelID) return undefined
|
|
172
|
+
return `${providerID}/${displayedModelLabel({ modelID, name })}`
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
/** Subrouter presets resolve to the live cooldown-aware candidate. */
|
|
176
|
+
export async function resolveDisplayedModelName({
|
|
177
|
+
providers,
|
|
178
|
+
providerID,
|
|
179
|
+
modelID,
|
|
180
|
+
sessionID,
|
|
181
|
+
}: {
|
|
182
|
+
providers: ProviderModelLookup[]
|
|
183
|
+
providerID?: string
|
|
184
|
+
modelID?: string
|
|
185
|
+
sessionID?: string
|
|
186
|
+
}): Promise<string | undefined> {
|
|
187
|
+
if (providerID === SUBROUTER_PROVIDER_ID && modelID) {
|
|
188
|
+
const candidate = await resolveLiveModel({ preset: modelID, sessionID }).catch((e) => {
|
|
189
|
+
sessionLogger.warn(
|
|
190
|
+
`[MODEL] Failed to resolve subrouter candidate for ${modelID}:`,
|
|
191
|
+
e instanceof Error ? e.message : e,
|
|
192
|
+
)
|
|
193
|
+
return null
|
|
194
|
+
})
|
|
195
|
+
if (candidate) return `${modelID} (${formatCandidateRef(candidate)})`
|
|
196
|
+
}
|
|
197
|
+
return getProviderModelName({ providers, providerID, modelID })
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export async function resolveDisplayedModelId({
|
|
201
|
+
providers,
|
|
202
|
+
providerID,
|
|
203
|
+
modelID,
|
|
204
|
+
sessionID,
|
|
205
|
+
}: {
|
|
206
|
+
providers: ProviderModelLookup[]
|
|
207
|
+
providerID?: string
|
|
208
|
+
modelID?: string
|
|
209
|
+
sessionID?: string
|
|
210
|
+
}): Promise<string | undefined> {
|
|
211
|
+
return formatDisplayedModelId({
|
|
212
|
+
providerID,
|
|
213
|
+
modelID,
|
|
214
|
+
name: await resolveDisplayedModelName({ providers, providerID, modelID, sessionID }),
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
type ModelListCacheEntry =
|
|
219
|
+
| { status: 'pending'; promise: Promise<ListedModel[] | OpenCodeSdkError> }
|
|
220
|
+
| { status: 'ready'; value: ListedModel[] }
|
|
221
|
+
|
|
222
|
+
const modelListCache = new Map<string, ModelListCacheEntry>()
|
|
223
|
+
|
|
224
|
+
export function clearModelListCache() {
|
|
225
|
+
modelListCache.clear()
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
subscribeOpencodeServerLifecycle(() => {
|
|
229
|
+
clearModelListCache()
|
|
230
|
+
})
|
|
231
|
+
|
|
232
|
+
function flattenProviderModels({
|
|
233
|
+
providers,
|
|
234
|
+
connected,
|
|
235
|
+
}: {
|
|
236
|
+
providers: Provider[]
|
|
237
|
+
connected: string[]
|
|
238
|
+
}): ListedModel[] {
|
|
239
|
+
const connectedSet = new Set(connected)
|
|
240
|
+
const models: ListedModel[] = []
|
|
241
|
+
for (const provider of providers) {
|
|
242
|
+
const isConnected = connectedSet.has(provider.id)
|
|
243
|
+
for (const [modelID, model] of Object.entries(provider.models)) {
|
|
244
|
+
models.push({
|
|
245
|
+
providerID: provider.id,
|
|
246
|
+
modelID,
|
|
247
|
+
name: model.name || modelID,
|
|
248
|
+
connected: isConnected,
|
|
249
|
+
})
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
return models
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
export async function listModels({
|
|
256
|
+
getClient,
|
|
257
|
+
directory,
|
|
258
|
+
}: {
|
|
259
|
+
getClient: () => OpencodeClient
|
|
260
|
+
directory?: string
|
|
261
|
+
}): Promise<ListedModel[] | OpenCodeSdkError> {
|
|
262
|
+
const cacheKey = directory ?? ''
|
|
263
|
+
const cached = modelListCache.get(cacheKey)
|
|
264
|
+
if (cached?.status === 'ready') return cached.value
|
|
265
|
+
if (cached?.status === 'pending') return cached.promise
|
|
266
|
+
|
|
267
|
+
const promise = (async () => {
|
|
268
|
+
const providersResponse = await getClient()
|
|
269
|
+
.provider.list({ directory })
|
|
270
|
+
.catch((e) => new OpenCodeSdkError({ operation: 'provider.list', cause: e }))
|
|
271
|
+
if (providersResponse instanceof Error) return providersResponse
|
|
272
|
+
if (!providersResponse.data) {
|
|
273
|
+
return new OpenCodeSdkError({ operation: 'provider.list' })
|
|
274
|
+
}
|
|
275
|
+
return flattenProviderModels({
|
|
276
|
+
providers: providersResponse.data.all,
|
|
277
|
+
connected: providersResponse.data.connected,
|
|
278
|
+
})
|
|
279
|
+
})()
|
|
280
|
+
|
|
281
|
+
modelListCache.set(cacheKey, { status: 'pending', promise })
|
|
282
|
+
const result = await promise
|
|
283
|
+
const current = modelListCache.get(cacheKey)
|
|
284
|
+
const ownsCacheEntry = current?.status === 'pending' && current.promise === promise
|
|
285
|
+
if (result instanceof Error) {
|
|
286
|
+
if (ownsCacheEntry) modelListCache.delete(cacheKey)
|
|
287
|
+
return result
|
|
288
|
+
}
|
|
289
|
+
if (ownsCacheEntry) modelListCache.set(cacheKey, { status: 'ready', value: result })
|
|
290
|
+
return result
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
export function validateModelIdAgainstList({
|
|
294
|
+
model,
|
|
295
|
+
models,
|
|
296
|
+
}: {
|
|
297
|
+
model: string
|
|
298
|
+
models: ListedModel[]
|
|
299
|
+
}): { providerID: string; modelID: string } | InvalidModelError {
|
|
300
|
+
const parsed = parseModelId(model)
|
|
301
|
+
if (!parsed) {
|
|
302
|
+
return new InvalidModelError({
|
|
303
|
+
model,
|
|
304
|
+
reason: 'expected provider/model, for example anthropic/claude-opus-4-6',
|
|
305
|
+
})
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
const match = models.find((candidate) => {
|
|
309
|
+
return (
|
|
310
|
+
candidate.providerID === parsed.providerID &&
|
|
311
|
+
candidate.modelID === parsed.modelID
|
|
312
|
+
)
|
|
313
|
+
})
|
|
314
|
+
if (!match) {
|
|
315
|
+
const siblings = models
|
|
316
|
+
.filter((candidate) => candidate.providerID === parsed.providerID)
|
|
317
|
+
.map((candidate) => `${candidate.providerID}/${candidate.modelID}`)
|
|
318
|
+
if (siblings.length > 0) {
|
|
319
|
+
return new InvalidModelError({
|
|
320
|
+
model,
|
|
321
|
+
reason: `unknown model. Similar: ${siblings.slice(0, 8).join(', ')}`,
|
|
322
|
+
})
|
|
323
|
+
}
|
|
324
|
+
const connectedProviders = [
|
|
325
|
+
...new Set(
|
|
326
|
+
models.filter((candidate) => candidate.connected).map((candidate) => {
|
|
327
|
+
return candidate.providerID
|
|
328
|
+
}),
|
|
329
|
+
),
|
|
330
|
+
]
|
|
331
|
+
const connectedHint = connectedProviders.length
|
|
332
|
+
? ` Connected providers: ${connectedProviders.join(', ')}`
|
|
333
|
+
: ''
|
|
334
|
+
return new InvalidModelError({
|
|
335
|
+
model,
|
|
336
|
+
reason: `unknown provider ${parsed.providerID}.${connectedHint}`,
|
|
337
|
+
})
|
|
338
|
+
}
|
|
339
|
+
if (!match.connected) {
|
|
340
|
+
return new InvalidModelError({
|
|
341
|
+
model,
|
|
342
|
+
reason: `provider ${parsed.providerID} is not connected`,
|
|
343
|
+
})
|
|
344
|
+
}
|
|
345
|
+
return parsed
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export async function validateModelId({
|
|
349
|
+
model,
|
|
350
|
+
getClient,
|
|
351
|
+
directory,
|
|
352
|
+
}: {
|
|
353
|
+
model: string
|
|
354
|
+
getClient: () => OpencodeClient
|
|
355
|
+
directory?: string
|
|
356
|
+
}): Promise<
|
|
357
|
+
{ providerID: string; modelID: string } | InvalidModelError | OpenCodeSdkError
|
|
358
|
+
> {
|
|
359
|
+
const models = await listModels({ getClient, directory })
|
|
360
|
+
if (models instanceof Error) return models
|
|
361
|
+
return validateModelIdAgainstList({ model, models })
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
export async function validateCliModelOption({
|
|
365
|
+
model,
|
|
366
|
+
directory,
|
|
367
|
+
}: {
|
|
368
|
+
model: string | undefined
|
|
369
|
+
directory?: string
|
|
370
|
+
}) {
|
|
371
|
+
if (!model) return
|
|
372
|
+
const parsed = parseModelId(model)
|
|
373
|
+
if (!parsed) {
|
|
374
|
+
return new InvalidModelError({
|
|
375
|
+
model,
|
|
376
|
+
reason: 'expected provider/model, for example anthropic/claude-opus-4-6',
|
|
377
|
+
})
|
|
378
|
+
}
|
|
379
|
+
if (!directory) return parsed
|
|
380
|
+
|
|
381
|
+
const getClient = await initializeOpencodeForDirectory(directory)
|
|
382
|
+
if (getClient instanceof Error) return getClient
|
|
383
|
+
return validateModelId({ model, getClient, directory })
|
|
384
|
+
}
|
|
385
|
+
|
|
110
386
|
/**
|
|
111
387
|
* Get the default model from OpenCode when no user preference is set.
|
|
112
388
|
* Priority (matches OpenCode TUI behavior):
|
|
@@ -163,7 +439,7 @@ export async function getDefaultModel({
|
|
|
163
439
|
const configResponse = await getClient().config.get({ directory })
|
|
164
440
|
.catch((e) => new OpenCodeSdkError({ operation: 'config.get', cause: e }))
|
|
165
441
|
if (!(configResponse instanceof Error) && configResponse.data?.model) {
|
|
166
|
-
const configModel =
|
|
442
|
+
const configModel = parseModelId(configResponse.data.model)
|
|
167
443
|
if (configModel && isModelValid(configModel, connected, providers)) {
|
|
168
444
|
sessionLogger.log(
|
|
169
445
|
`[MODEL] Using config model: ${configModel.providerID}/${configModel.modelID}`,
|
|
@@ -16,6 +16,35 @@ export function isOpencodeSessionEventLogEnabled(): boolean {
|
|
|
16
16
|
return process.env['KIMAKI_LOG_OPENCODE_SESSION_EVENTS'] === '1'
|
|
17
17
|
}
|
|
18
18
|
|
|
19
|
+
function sessionIdFromEventProperties(properties: OpenCodeEvent['properties']): string | undefined {
|
|
20
|
+
if (!properties || typeof properties !== 'object') {
|
|
21
|
+
return undefined
|
|
22
|
+
}
|
|
23
|
+
const sessionID = Reflect.get(properties, 'sessionID')
|
|
24
|
+
if (typeof sessionID === 'string') {
|
|
25
|
+
return sessionID
|
|
26
|
+
}
|
|
27
|
+
const info = Reflect.get(properties, 'info')
|
|
28
|
+
if (info && typeof info === 'object') {
|
|
29
|
+
const id = Reflect.get(info, 'id')
|
|
30
|
+
if (typeof id === 'string') {
|
|
31
|
+
return id
|
|
32
|
+
}
|
|
33
|
+
const infoSessionId = Reflect.get(info, 'sessionID')
|
|
34
|
+
if (typeof infoSessionId === 'string') {
|
|
35
|
+
return infoSessionId
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
const part = Reflect.get(properties, 'part')
|
|
39
|
+
if (part && typeof part === 'object') {
|
|
40
|
+
const partSessionId = Reflect.get(part, 'sessionID')
|
|
41
|
+
if (typeof partSessionId === 'string') {
|
|
42
|
+
return partSessionId
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return undefined
|
|
46
|
+
}
|
|
47
|
+
|
|
19
48
|
export function getOpencodeEventSessionId(event: OpenCodeEvent): string | undefined {
|
|
20
49
|
switch (event.type) {
|
|
21
50
|
case 'message.updated':
|
|
@@ -40,7 +69,8 @@ export function getOpencodeEventSessionId(event: OpenCodeEvent): string | undefi
|
|
|
40
69
|
case 'session.deleted':
|
|
41
70
|
return event.properties.info.id
|
|
42
71
|
default:
|
|
43
|
-
|
|
72
|
+
// session.next.* and later SDK events put sessionID on properties.
|
|
73
|
+
return sessionIdFromEventProperties(event.properties)
|
|
44
74
|
}
|
|
45
75
|
}
|
|
46
76
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// Unit tests for the per-thread Discord arrival FIFO.
|
|
2
|
+
// /plan-agent and a follow-up message must keep Discord order.
|
|
3
|
+
|
|
4
|
+
import { describe, expect, test } from 'vitest'
|
|
5
|
+
import {
|
|
6
|
+
reserveThreadIngress,
|
|
7
|
+
runInThreadIngressSlot,
|
|
8
|
+
waitForCurrentThreadIngress,
|
|
9
|
+
} from './thread-session-runtime.js'
|
|
10
|
+
|
|
11
|
+
describe('thread ingress FIFO', () => {
|
|
12
|
+
test('later waiters stay behind an unreleased earlier slot', async () => {
|
|
13
|
+
const first = reserveThreadIngress('thread-fifo')
|
|
14
|
+
const second = reserveThreadIngress('thread-fifo')
|
|
15
|
+
const order: string[] = []
|
|
16
|
+
|
|
17
|
+
const firstWork = runInThreadIngressSlot(first, async () => {
|
|
18
|
+
await waitForCurrentThreadIngress()
|
|
19
|
+
order.push('first-write')
|
|
20
|
+
first.release()
|
|
21
|
+
await new Promise((resolve) => setTimeout(resolve, 20))
|
|
22
|
+
order.push('first-work')
|
|
23
|
+
})
|
|
24
|
+
|
|
25
|
+
const secondWork = runInThreadIngressSlot(second, async () => {
|
|
26
|
+
await waitForCurrentThreadIngress()
|
|
27
|
+
order.push('second')
|
|
28
|
+
second.release()
|
|
29
|
+
})
|
|
30
|
+
|
|
31
|
+
await Promise.all([firstWork, secondWork])
|
|
32
|
+
expect(order).toEqual(['first-write', 'second', 'first-work'])
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
test('runInThreadIngressSlot releases after a thrown callback', async () => {
|
|
36
|
+
const first = reserveThreadIngress('thread-fifo-throw')
|
|
37
|
+
const second = reserveThreadIngress('thread-fifo-throw')
|
|
38
|
+
const order: string[] = []
|
|
39
|
+
|
|
40
|
+
const firstWork = runInThreadIngressSlot(first, async () => {
|
|
41
|
+
await waitForCurrentThreadIngress()
|
|
42
|
+
order.push('first')
|
|
43
|
+
throw new Error('boom')
|
|
44
|
+
}).catch(() => {})
|
|
45
|
+
|
|
46
|
+
const secondWork = runInThreadIngressSlot(second, async () => {
|
|
47
|
+
await waitForCurrentThreadIngress()
|
|
48
|
+
order.push('second')
|
|
49
|
+
})
|
|
50
|
+
|
|
51
|
+
await Promise.all([firstWork, secondWork])
|
|
52
|
+
expect(order).toEqual(['first', 'second'])
|
|
53
|
+
})
|
|
54
|
+
})
|
|
@@ -43,6 +43,8 @@ export type QueuedMessage = {
|
|
|
43
43
|
// Set by --agent/--model/--permission flags on kimaki send or slash commands.
|
|
44
44
|
agent?: string
|
|
45
45
|
model?: string
|
|
46
|
+
// Thinking-level variant from `/xxx-agent variant:`.
|
|
47
|
+
variant?: string
|
|
46
48
|
// Raw permission rule strings ("tool:action" or "tool:pattern:action").
|
|
47
49
|
// Parsed and merged into session permissions on creation.
|
|
48
50
|
permissions?: string[]
|
|
@@ -183,6 +185,7 @@ export function setSessionUsername(threadId: string, username: string): void {
|
|
|
183
185
|
}
|
|
184
186
|
|
|
185
187
|
export function setSessionUserId(threadId: string, userId: string): void {
|
|
188
|
+
if (!userId) return
|
|
186
189
|
updateThread(threadId, (t) => {
|
|
187
190
|
if (t.sessionUserId) {
|
|
188
191
|
return t
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
Event as OpenCodeEvent,
|
|
3
|
+
Message as OpenCodeMessage,
|
|
4
|
+
Part,
|
|
5
|
+
} from '@opencode-ai/sdk/v2'
|
|
6
|
+
import { describe, expect, test } from 'vitest'
|
|
7
|
+
import type { EventBufferEntry, EventBufferEvent } from './event-stream-state.js'
|
|
8
|
+
import { ThreadSessionRuntime } from './thread-session-runtime.js'
|
|
9
|
+
|
|
10
|
+
type RuntimeInternals = {
|
|
11
|
+
state?: { sessionId: string }
|
|
12
|
+
eventBuffer: EventBufferEntry[]
|
|
13
|
+
partBuffer: Map<string, Map<string, Part>>
|
|
14
|
+
compactEventForEventBuffer: (event: EventBufferEvent) => EventBufferEvent | undefined
|
|
15
|
+
flushBufferedParts: () => Promise<void>
|
|
16
|
+
handleMessageUpdated: (message: OpenCodeMessage) => Promise<void>
|
|
17
|
+
handlePartUpdated: (part: Part) => Promise<void>
|
|
18
|
+
handleNaturalAssistantCompletion: () => Promise<void>
|
|
19
|
+
handleMainPart: (part: Part) => Promise<void>
|
|
20
|
+
handleSubtaskPart: (part: Part) => Promise<void>
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function summaryMessageEvent({
|
|
24
|
+
sessionId,
|
|
25
|
+
messageId,
|
|
26
|
+
}: {
|
|
27
|
+
sessionId: string
|
|
28
|
+
messageId: string
|
|
29
|
+
}): OpenCodeEvent {
|
|
30
|
+
return {
|
|
31
|
+
id: `evt_${messageId}`,
|
|
32
|
+
type: 'message.updated',
|
|
33
|
+
properties: {
|
|
34
|
+
sessionID: sessionId,
|
|
35
|
+
info: {
|
|
36
|
+
id: messageId,
|
|
37
|
+
sessionID: sessionId,
|
|
38
|
+
role: 'assistant',
|
|
39
|
+
parentID: 'msg_user',
|
|
40
|
+
time: { created: 2, completed: 3 },
|
|
41
|
+
modelID: 'compaction-model',
|
|
42
|
+
providerID: 'test-provider',
|
|
43
|
+
mode: 'compaction',
|
|
44
|
+
agent: 'compaction',
|
|
45
|
+
path: { cwd: '/test', root: '/test' },
|
|
46
|
+
cost: 1,
|
|
47
|
+
tokens: {
|
|
48
|
+
input: 10,
|
|
49
|
+
output: 1,
|
|
50
|
+
reasoning: 0,
|
|
51
|
+
cache: { read: 0, write: 0 },
|
|
52
|
+
},
|
|
53
|
+
finish: 'stop',
|
|
54
|
+
summary: true,
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function summaryTextPart({
|
|
61
|
+
sessionId,
|
|
62
|
+
messageId,
|
|
63
|
+
}: {
|
|
64
|
+
sessionId: string
|
|
65
|
+
messageId: string
|
|
66
|
+
}): Part {
|
|
67
|
+
return {
|
|
68
|
+
id: `prt_${messageId}`,
|
|
69
|
+
sessionID: sessionId,
|
|
70
|
+
messageID: messageId,
|
|
71
|
+
type: 'text',
|
|
72
|
+
text: 'internal compaction summary must not reach Discord',
|
|
73
|
+
time: { start: 2, end: 3 },
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
describe('ThreadSessionRuntime compaction summary routing', () => {
|
|
78
|
+
test('drops summary message parts before fallback buffering or completion', async () => {
|
|
79
|
+
const sessionId = 'ses_main'
|
|
80
|
+
const messageId = 'msg_summary_embedded_parts'
|
|
81
|
+
const event = summaryMessageEvent({ sessionId, messageId })
|
|
82
|
+
if (event.type !== 'message.updated') {
|
|
83
|
+
throw new Error('Expected summary message event')
|
|
84
|
+
}
|
|
85
|
+
const message = {
|
|
86
|
+
...event.properties.info,
|
|
87
|
+
parts: [summaryTextPart({ sessionId, messageId })],
|
|
88
|
+
} as unknown as OpenCodeMessage
|
|
89
|
+
const runtime = Object.create(ThreadSessionRuntime.prototype) as RuntimeInternals
|
|
90
|
+
Object.defineProperty(runtime, 'state', {
|
|
91
|
+
value: { sessionId },
|
|
92
|
+
configurable: true,
|
|
93
|
+
})
|
|
94
|
+
runtime.eventBuffer = [{ event, timestamp: 1 }]
|
|
95
|
+
runtime.partBuffer = new Map()
|
|
96
|
+
runtime.flushBufferedParts = async () => {
|
|
97
|
+
throw new Error('Summary message must not flush parts')
|
|
98
|
+
}
|
|
99
|
+
runtime.handleNaturalAssistantCompletion = async () => {
|
|
100
|
+
throw new Error('Summary message must not complete the Discord turn')
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
await runtime.handleMessageUpdated(message)
|
|
104
|
+
|
|
105
|
+
expect(runtime.partBuffer.size).toBe(0)
|
|
106
|
+
})
|
|
107
|
+
|
|
108
|
+
test.each([
|
|
109
|
+
['main session', 'ses_main'],
|
|
110
|
+
['subagent session', 'ses_child'],
|
|
111
|
+
])('preserves summary identity and drops %s parts before buffering', async (_, sessionId) => {
|
|
112
|
+
const messageId = `msg_summary_${sessionId}`
|
|
113
|
+
const runtime = Object.create(ThreadSessionRuntime.prototype) as RuntimeInternals
|
|
114
|
+
const compacted = runtime.compactEventForEventBuffer(
|
|
115
|
+
summaryMessageEvent({ sessionId, messageId }),
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
expect(compacted?.type).toBe('message.updated')
|
|
119
|
+
if (!compacted || compacted.type !== 'message.updated') {
|
|
120
|
+
throw new Error('Expected compacted summary message event')
|
|
121
|
+
}
|
|
122
|
+
expect(compacted.properties.info.summary).toBe(true)
|
|
123
|
+
|
|
124
|
+
runtime.eventBuffer = [{ event: compacted, timestamp: 1 }]
|
|
125
|
+
runtime.partBuffer = new Map()
|
|
126
|
+
|
|
127
|
+
await runtime.handlePartUpdated(summaryTextPart({ sessionId, messageId }))
|
|
128
|
+
|
|
129
|
+
expect(runtime.partBuffer.size).toBe(0)
|
|
130
|
+
})
|
|
131
|
+
|
|
132
|
+
test.each([
|
|
133
|
+
['main session', 'ses_main'],
|
|
134
|
+
['subagent session', 'ses_child'],
|
|
135
|
+
])('holds %s parts until summary identity arrives, then drops them', async (_, sessionId) => {
|
|
136
|
+
const messageId = `msg_summary_unknown_${sessionId}`
|
|
137
|
+
const runtime = Object.create(ThreadSessionRuntime.prototype) as RuntimeInternals
|
|
138
|
+
Object.defineProperty(runtime, 'state', {
|
|
139
|
+
value: { sessionId: 'ses_main' },
|
|
140
|
+
configurable: true,
|
|
141
|
+
})
|
|
142
|
+
runtime.eventBuffer = []
|
|
143
|
+
runtime.partBuffer = new Map()
|
|
144
|
+
runtime.handleMainPart = async () => {
|
|
145
|
+
throw new Error('Unknown summary parts must not render')
|
|
146
|
+
}
|
|
147
|
+
runtime.handleSubtaskPart = async () => {
|
|
148
|
+
throw new Error('Unknown summary parts must not render')
|
|
149
|
+
}
|
|
150
|
+
runtime.flushBufferedParts = async () => {
|
|
151
|
+
throw new Error('Summary message must not flush parts')
|
|
152
|
+
}
|
|
153
|
+
runtime.handleNaturalAssistantCompletion = async () => {
|
|
154
|
+
throw new Error('Summary message must not complete the Discord turn')
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
await runtime.handlePartUpdated(summaryTextPart({ sessionId, messageId }))
|
|
158
|
+
|
|
159
|
+
expect(runtime.partBuffer.get(messageId)?.size).toBe(1)
|
|
160
|
+
|
|
161
|
+
const event = summaryMessageEvent({ sessionId, messageId })
|
|
162
|
+
if (event.type !== 'message.updated') {
|
|
163
|
+
throw new Error('Expected summary message event')
|
|
164
|
+
}
|
|
165
|
+
await runtime.handleMessageUpdated(event.properties.info)
|
|
166
|
+
|
|
167
|
+
expect(runtime.partBuffer.size).toBe(0)
|
|
168
|
+
})
|
|
169
|
+
})
|