kimaki 0.4.71 → 0.4.73
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 +452 -0
- package/dist/channel-management.js +1 -4
- package/dist/cli-parsing.test.js +25 -0
- package/dist/cli.js +666 -164
- package/dist/commands/abort.js +26 -32
- package/dist/commands/action-buttons.js +10 -22
- package/dist/commands/add-project.js +2 -3
- package/dist/commands/agent.js +63 -23
- package/dist/commands/create-new-project.js +12 -6
- package/dist/commands/diff.js +1 -1
- package/dist/commands/discord-install-url.js +36 -0
- package/dist/commands/login.js +51 -18
- package/dist/commands/mention-mode.js +1 -8
- package/dist/commands/merge-worktree.js +13 -26
- package/dist/commands/model.js +10 -22
- package/dist/commands/queue.js +61 -101
- package/dist/commands/restart-opencode-server.js +12 -56
- package/dist/commands/resume.js +2 -11
- package/dist/commands/run-command.js +1 -1
- package/dist/commands/session.js +14 -19
- package/dist/commands/stop-opencode-server.js +79 -0
- package/dist/commands/unset-model.js +7 -21
- package/dist/commands/user-command.js +27 -21
- package/dist/commands/verbosity.js +102 -37
- package/dist/commands/worktree-settings.js +1 -8
- package/dist/commands/worktree.js +9 -29
- package/dist/commands/worktrees.js +128 -0
- package/dist/condense-memory.js +33 -0
- package/dist/config.js +16 -47
- package/dist/database.js +197 -64
- package/dist/db.js +47 -0
- package/dist/db.test.js +113 -0
- package/dist/debounced-process-flush.js +77 -0
- package/dist/discord-bot.js +210 -210
- package/dist/discord-urls.js +70 -0
- package/dist/discord-utils.js +31 -15
- package/dist/discord-utils.test.js +45 -1
- package/dist/event-stream-real-capture.e2e.test.js +549 -0
- package/dist/gateway-proxy-reconnect.e2e.test.js +394 -0
- package/dist/gateway-proxy.e2e.test.js +481 -0
- package/dist/generated/client.js +3 -1
- package/dist/generated/enums.js +18 -0
- package/dist/generated/internal/class.js +12 -4
- package/dist/generated/internal/prismaNamespace.js +17 -5
- package/dist/generated/internal/prismaNamespaceBrowser.js +13 -1
- package/dist/generated/models/session_events.js +1 -0
- package/dist/hrana-server.js +5 -4
- package/dist/install-url.js +27 -0
- package/dist/interaction-handler.js +26 -7
- package/dist/kimaki-digital-twin.e2e.test.js +25 -13
- package/dist/logger.js +43 -20
- package/dist/markdown.test.js +215 -210
- package/dist/message-preprocessing.js +186 -0
- package/dist/opencode-interrupt-plugin.js +183 -0
- package/dist/opencode-interrupt-plugin.test.js +334 -0
- package/dist/opencode-plugin-interrupt.test.js +138 -0
- package/dist/opencode-plugin-loading.e2e.test.js +91 -0
- package/dist/opencode-plugin.js +12 -35
- package/dist/opencode-plugin.test.js +1 -1
- package/dist/opencode.js +161 -20
- package/dist/privacy-sanitizer.js +105 -0
- package/dist/queue-advanced-abort.e2e.test.js +299 -0
- package/dist/queue-advanced-action-buttons.e2e.test.js +206 -0
- package/dist/queue-advanced-e2e-setup.js +371 -0
- package/dist/queue-advanced-footer.e2e.test.js +299 -0
- package/dist/queue-advanced-permissions-typing.e2e.test.js +104 -0
- package/dist/queue-advanced-typing.e2e.test.js +163 -0
- package/dist/queue-interrupt-drain.e2e.test.js +135 -0
- package/dist/runtime-idle-sweeper.js +59 -0
- package/dist/runtime-lifecycle.e2e.test.js +467 -0
- package/dist/sentry.js +57 -1
- package/dist/session-handler/agent-utils.js +67 -0
- package/dist/session-handler/event-stream-state.js +381 -0
- package/dist/session-handler/event-stream-state.test.js +426 -0
- package/dist/session-handler/model-utils.js +124 -0
- package/dist/session-handler/opencode-session-event-log.js +94 -0
- package/dist/session-handler/runtime-types.js +3 -0
- package/dist/session-handler/state.js +43 -141
- package/dist/session-handler/state.test.js +52 -0
- package/dist/session-handler/thread-runtime-state.js +95 -0
- package/dist/session-handler/thread-runtime-state.test.js +287 -0
- package/dist/session-handler/thread-session-runtime.js +2763 -50
- package/dist/session-handler.js +9 -1812
- package/dist/startup-service.js +1 -1
- package/dist/store.js +17 -0
- package/dist/system-message.js +23 -3
- package/dist/task-runner.js +3 -2
- package/dist/test-utils.js +276 -0
- package/dist/thread-message-queue.e2e.test.js +542 -335
- package/dist/thread-queue-advanced.e2e.test.js +884 -0
- package/dist/token-usage.js +11 -0
- package/dist/tools.js +2 -4
- package/dist/upgrade.js +1 -1
- package/dist/utils.js +29 -2
- package/dist/voice-handler.js +38 -6
- package/dist/voice-message.e2e.test.js +850 -0
- package/dist/voice.js +18 -5
- package/dist/voice.test.js +52 -8
- package/dist/worktree-utils.js +3 -727
- package/dist/worktrees.js +845 -0
- package/dist/worktrees.test.js +189 -0
- package/package.json +13 -14
- package/schema.prisma +58 -16
- package/skills/batch/SKILL.md +87 -0
- package/skills/goke/src/__test__/index.test.ts +7 -0
- package/skills/npm-package/SKILL.md +114 -0
- package/skills/security-review/SKILL.md +208 -0
- package/skills/simplify/SKILL.md +58 -0
- package/skills/zustand-centralized-state/SKILL.md +426 -4
- package/src/agent-model.e2e.test.ts +585 -0
- package/src/channel-management.ts +0 -6
- package/src/cli-parsing.test.ts +31 -0
- package/src/cli.ts +876 -200
- package/src/commands/abort.ts +25 -38
- package/src/commands/action-buttons.ts +11 -28
- package/src/commands/add-project.ts +0 -3
- package/src/commands/agent.ts +89 -24
- package/src/commands/create-new-project.ts +12 -8
- package/src/commands/diff.ts +1 -1
- package/src/commands/login.ts +56 -21
- package/src/commands/mention-mode.ts +0 -9
- package/src/commands/merge-worktree.ts +15 -36
- package/src/commands/model.ts +10 -23
- package/src/commands/queue.ts +67 -136
- package/src/commands/restart-opencode-server.ts +11 -69
- package/src/commands/resume.ts +0 -12
- package/src/commands/run-command.ts +1 -1
- package/src/commands/session.ts +15 -21
- package/src/commands/stop-opencode-server.ts +113 -0
- package/src/commands/unset-model.ts +7 -23
- package/src/commands/user-command.ts +28 -23
- package/src/commands/verbosity.ts +125 -40
- package/src/commands/worktree-settings.ts +0 -9
- package/src/commands/worktree.ts +7 -35
- package/src/commands/worktrees.ts +173 -0
- package/src/condense-memory.ts +36 -0
- package/src/config.ts +17 -72
- package/src/database.ts +250 -86
- package/src/db.test.ts +131 -0
- package/src/db.ts +48 -0
- package/src/debounced-process-flush.ts +104 -0
- package/src/discord-bot.ts +257 -260
- package/src/discord-urls.ts +76 -0
- package/src/discord-utils.test.ts +54 -1
- package/src/discord-utils.ts +45 -25
- package/src/event-stream-real-capture.e2e.test.ts +706 -0
- package/src/gateway-proxy-reconnect.e2e.test.ts +523 -0
- package/src/gateway-proxy.e2e.test.ts +637 -0
- package/src/generated/browser.ts +5 -0
- package/src/generated/client.ts +8 -1
- package/src/generated/commonInputTypes.ts +234 -44
- package/src/generated/enums.ts +34 -0
- package/src/generated/internal/class.ts +29 -7
- package/src/generated/internal/prismaNamespace.ts +132 -6
- package/src/generated/internal/prismaNamespaceBrowser.ts +17 -1
- package/src/generated/models/bot_tokens.ts +165 -126
- package/src/generated/models/channel_directories.ts +44 -289
- package/src/generated/models/channel_verbosity.ts +23 -19
- package/src/generated/models/channel_worktrees.ts +0 -8
- package/src/generated/models/session_events.ts +1439 -0
- package/src/generated/models/thread_sessions.ts +130 -0
- package/src/generated/models/thread_worktrees.ts +25 -21
- package/src/generated/models.ts +1 -0
- package/src/hrana-server.ts +11 -4
- package/src/interaction-handler.ts +34 -16
- package/src/kimaki-digital-twin.e2e.test.ts +30 -15
- package/src/logger.ts +49 -22
- package/src/markdown.test.ts +236 -287
- package/src/message-preprocessing.ts +266 -0
- package/src/opencode-interrupt-plugin.test.ts +445 -0
- package/src/opencode-interrupt-plugin.ts +237 -0
- package/src/opencode-plugin-loading.e2e.test.ts +112 -0
- package/src/opencode-plugin.test.ts +1 -1
- package/src/opencode-plugin.ts +17 -34
- package/src/opencode.ts +197 -24
- package/src/privacy-sanitizer.ts +142 -0
- package/src/queue-advanced-abort.e2e.test.ts +387 -0
- package/src/queue-advanced-action-buttons.e2e.test.ts +268 -0
- package/src/queue-advanced-e2e-setup.ts +446 -0
- package/src/queue-advanced-footer.e2e.test.ts +365 -0
- package/src/queue-advanced-permissions-typing.e2e.test.ts +141 -0
- package/src/queue-advanced-typing.e2e.test.ts +206 -0
- package/src/queue-interrupt-drain.e2e.test.ts +166 -0
- package/src/runtime-idle-sweeper.ts +86 -0
- package/src/runtime-lifecycle.e2e.test.ts +578 -0
- package/src/schema.sql +19 -6
- package/src/sentry.ts +57 -1
- package/src/session-handler/agent-utils.ts +97 -0
- package/src/session-handler/event-stream-fixtures/real-session-action-buttons.jsonl +45 -0
- package/src/session-handler/event-stream-fixtures/real-session-footer-suppressed-on-pre-idle-interrupt.jsonl +40 -0
- package/src/session-handler/event-stream-fixtures/real-session-permission-external-file.jsonl +23 -0
- package/src/session-handler/event-stream-fixtures/real-session-task-normal.jsonl +22 -0
- package/src/session-handler/event-stream-fixtures/real-session-task-three-parallel-sleeps.jsonl +277 -0
- package/src/session-handler/event-stream-fixtures/real-session-task-user-interruption.jsonl +46 -0
- package/src/session-handler/event-stream-fixtures/session-concurrent-messages-serialized.jsonl +56 -0
- package/src/session-handler/event-stream-fixtures/session-explicit-abort.jsonl +44 -0
- package/src/session-handler/event-stream-fixtures/session-normal-completion.jsonl +29 -0
- package/src/session-handler/event-stream-fixtures/session-tool-call-noisy-stream.jsonl +29 -0
- package/src/session-handler/event-stream-fixtures/session-two-completions-same-session.jsonl +50 -0
- package/src/session-handler/event-stream-fixtures/session-user-interruption.jsonl +59 -0
- package/src/session-handler/event-stream-fixtures/session-voice-queued-followup.jsonl +52 -0
- package/src/session-handler/event-stream-state.test.ts +501 -0
- package/src/session-handler/event-stream-state.ts +539 -0
- package/src/session-handler/model-utils.ts +183 -0
- package/src/session-handler/opencode-session-event-log.ts +130 -0
- package/src/session-handler/thread-runtime-state.ts +183 -0
- package/src/session-handler/thread-session-runtime.ts +3669 -0
- package/src/session-handler.ts +15 -2562
- package/src/startup-service.ts +1 -1
- package/src/store.ts +118 -0
- package/src/system-message.ts +23 -3
- package/src/task-runner.ts +3 -2
- package/src/test-utils.ts +412 -0
- package/src/thread-message-queue.e2e.test.ts +626 -419
- package/src/tools.ts +2 -4
- package/src/upgrade.ts +1 -1
- package/src/utils.ts +54 -3
- package/src/voice-handler.ts +48 -8
- package/src/voice-message.e2e.test.ts +1046 -0
- package/src/voice.test.ts +54 -8
- package/src/voice.ts +29 -10
- package/src/worktree-utils.ts +3 -987
- package/src/worktrees.test.ts +223 -0
- package/src/worktrees.ts +1152 -0
- package/src/__snapshots__/compact-session-context-no-system.md +0 -35
- package/src/__snapshots__/compact-session-context.md +0 -41
- package/src/__snapshots__/first-session-no-info.md +0 -17
- package/src/__snapshots__/first-session-with-info.md +0 -23
- package/src/__snapshots__/session-1.md +0 -17
- package/src/__snapshots__/session-2.md +0 -5871
- package/src/__snapshots__/session-3.md +0 -17
- package/src/__snapshots__/session-with-tools.md +0 -5871
- package/src/session-handler/state.ts +0 -232
|
@@ -0,0 +1,452 @@
|
|
|
1
|
+
// E2e test for agent model resolution in new threads.
|
|
2
|
+
// Reproduces a bug where /agent channel preference is ignored by the
|
|
3
|
+
// promptAsync path: submitViaOpencodeQueue only passes input.agent/input.model
|
|
4
|
+
// (undefined for normal Discord messages) instead of resolving channel agent
|
|
5
|
+
// preferences from DB like dispatchPrompt does.
|
|
6
|
+
//
|
|
7
|
+
// The test sets a channel agent with a custom model, sends a message,
|
|
8
|
+
// and verifies the footer contains the agent's model — not the default.
|
|
9
|
+
//
|
|
10
|
+
// Uses opencode-deterministic-provider (no real LLM calls).
|
|
11
|
+
// Poll timeouts: 4s max, 100ms interval.
|
|
12
|
+
import fs from 'node:fs';
|
|
13
|
+
import net from 'node:net';
|
|
14
|
+
import path from 'node:path';
|
|
15
|
+
import url from 'node:url';
|
|
16
|
+
import { describe, beforeAll, afterAll, test, expect, } from 'vitest';
|
|
17
|
+
import { ChannelType, Client, GatewayIntentBits, Partials } from 'discord.js';
|
|
18
|
+
import { DigitalDiscord } from 'discord-digital-twin/src';
|
|
19
|
+
import { buildDeterministicOpencodeConfig, } from 'opencode-deterministic-provider';
|
|
20
|
+
import { setDataDir } from './config.js';
|
|
21
|
+
import { store } from './store.js';
|
|
22
|
+
import { startDiscordBot } from './discord-bot.js';
|
|
23
|
+
import { setBotToken, initDatabase, closeDatabase, setChannelDirectory, setChannelVerbosity, setChannelAgent, setChannelModel, } from './database.js';
|
|
24
|
+
import { getPrisma } from './db.js';
|
|
25
|
+
import { startHranaServer, stopHranaServer } from './hrana-server.js';
|
|
26
|
+
import { initializeOpencodeForDirectory } from './opencode.js';
|
|
27
|
+
import { cleanupOpencodeServers, cleanupTestSessions, waitForBotMessageContaining, waitForFooterMessage, } from './test-utils.js';
|
|
28
|
+
const TEST_USER_ID = '200000000000000920';
|
|
29
|
+
const TEXT_CHANNEL_ID = '200000000000000921';
|
|
30
|
+
const AGENT_MODEL = 'agent-model-v2';
|
|
31
|
+
const CHANNEL_MODEL = 'channel-model-v2';
|
|
32
|
+
const DEFAULT_MODEL = 'deterministic-v2';
|
|
33
|
+
const PROVIDER_NAME = 'deterministic-provider';
|
|
34
|
+
function createRunDirectories() {
|
|
35
|
+
const root = path.resolve(process.cwd(), 'tmp', 'agent-model-e2e');
|
|
36
|
+
fs.mkdirSync(root, { recursive: true });
|
|
37
|
+
const dataDir = fs.mkdtempSync(path.join(root, 'data-'));
|
|
38
|
+
const projectDirectory = path.join(root, 'project');
|
|
39
|
+
fs.mkdirSync(projectDirectory, { recursive: true });
|
|
40
|
+
return { root, dataDir, projectDirectory };
|
|
41
|
+
}
|
|
42
|
+
function chooseLockPort() {
|
|
43
|
+
return new Promise((resolve, reject) => {
|
|
44
|
+
const server = net.createServer();
|
|
45
|
+
server.listen(0, () => {
|
|
46
|
+
const address = server.address();
|
|
47
|
+
if (!address || typeof address === 'string') {
|
|
48
|
+
server.close();
|
|
49
|
+
reject(new Error('Failed to resolve lock port'));
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
const port = address.port;
|
|
53
|
+
server.close(() => {
|
|
54
|
+
resolve(port);
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
function createDiscordJsClient({ restUrl }) {
|
|
60
|
+
return new Client({
|
|
61
|
+
intents: [
|
|
62
|
+
GatewayIntentBits.Guilds,
|
|
63
|
+
GatewayIntentBits.GuildMessages,
|
|
64
|
+
GatewayIntentBits.MessageContent,
|
|
65
|
+
GatewayIntentBits.GuildVoiceStates,
|
|
66
|
+
],
|
|
67
|
+
partials: [
|
|
68
|
+
Partials.Channel,
|
|
69
|
+
Partials.Message,
|
|
70
|
+
Partials.User,
|
|
71
|
+
Partials.ThreadMember,
|
|
72
|
+
],
|
|
73
|
+
rest: {
|
|
74
|
+
api: restUrl,
|
|
75
|
+
version: '10',
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
function createDeterministicMatchers() {
|
|
80
|
+
const systemContextMatcher = {
|
|
81
|
+
id: 'system-context-check',
|
|
82
|
+
priority: 20,
|
|
83
|
+
when: {
|
|
84
|
+
lastMessageRole: 'user',
|
|
85
|
+
latestUserTextIncludes: 'Reply with exactly: system-context-check',
|
|
86
|
+
rawPromptIncludes: `Current Discord user ID is: ${TEST_USER_ID}`,
|
|
87
|
+
},
|
|
88
|
+
then: {
|
|
89
|
+
parts: [
|
|
90
|
+
{ type: 'stream-start', warnings: [] },
|
|
91
|
+
{ type: 'text-start', id: 'system-context-reply' },
|
|
92
|
+
{
|
|
93
|
+
type: 'text-delta',
|
|
94
|
+
id: 'system-context-reply',
|
|
95
|
+
delta: 'system-context-ok',
|
|
96
|
+
},
|
|
97
|
+
{ type: 'text-end', id: 'system-context-reply' },
|
|
98
|
+
{
|
|
99
|
+
type: 'finish',
|
|
100
|
+
finishReason: 'stop',
|
|
101
|
+
usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
|
102
|
+
},
|
|
103
|
+
],
|
|
104
|
+
partDelaysMs: [0, 100, 0, 0, 0],
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
const userReplyMatcher = {
|
|
108
|
+
id: 'user-reply',
|
|
109
|
+
priority: 10,
|
|
110
|
+
when: {
|
|
111
|
+
lastMessageRole: 'user',
|
|
112
|
+
latestUserTextIncludes: 'Reply with exactly:',
|
|
113
|
+
},
|
|
114
|
+
then: {
|
|
115
|
+
parts: [
|
|
116
|
+
{ type: 'stream-start', warnings: [] },
|
|
117
|
+
{ type: 'text-start', id: 'default-reply' },
|
|
118
|
+
{ type: 'text-delta', id: 'default-reply', delta: 'ok' },
|
|
119
|
+
{ type: 'text-end', id: 'default-reply' },
|
|
120
|
+
{
|
|
121
|
+
type: 'finish',
|
|
122
|
+
finishReason: 'stop',
|
|
123
|
+
usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
|
124
|
+
},
|
|
125
|
+
],
|
|
126
|
+
partDelaysMs: [0, 100, 0, 0, 0],
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
return [systemContextMatcher, userReplyMatcher];
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Create an opencode agent .md file that uses a specific model.
|
|
133
|
+
* OpenCode discovers agents from .opencode/agent/*.md files.
|
|
134
|
+
*/
|
|
135
|
+
function createAgentFile({ projectDirectory, agentName, model, }) {
|
|
136
|
+
const agentDir = path.join(projectDirectory, '.opencode', 'agent');
|
|
137
|
+
fs.mkdirSync(agentDir, { recursive: true });
|
|
138
|
+
const content = [
|
|
139
|
+
'---',
|
|
140
|
+
`model: ${model}`,
|
|
141
|
+
'mode: primary',
|
|
142
|
+
`description: Test agent with custom model`,
|
|
143
|
+
'---',
|
|
144
|
+
'',
|
|
145
|
+
'You are a test agent. Reply concisely.',
|
|
146
|
+
'',
|
|
147
|
+
].join('\n');
|
|
148
|
+
fs.writeFileSync(path.join(agentDir, `${agentName}.md`), content);
|
|
149
|
+
}
|
|
150
|
+
describe('agent model resolution', () => {
|
|
151
|
+
let directories;
|
|
152
|
+
let discord;
|
|
153
|
+
let botClient;
|
|
154
|
+
let previousDefaultVerbosity = null;
|
|
155
|
+
let testStartTime = Date.now();
|
|
156
|
+
beforeAll(async () => {
|
|
157
|
+
testStartTime = Date.now();
|
|
158
|
+
directories = createRunDirectories();
|
|
159
|
+
const lockPort = await chooseLockPort();
|
|
160
|
+
process.env['KIMAKI_LOCK_PORT'] = String(lockPort);
|
|
161
|
+
setDataDir(directories.dataDir);
|
|
162
|
+
previousDefaultVerbosity = store.getState().defaultVerbosity;
|
|
163
|
+
store.setState({ defaultVerbosity: 'tools_and_text' });
|
|
164
|
+
const digitalDiscordDbPath = path.join(directories.dataDir, 'digital-discord.db');
|
|
165
|
+
discord = new DigitalDiscord({
|
|
166
|
+
guild: {
|
|
167
|
+
name: 'Agent Model E2E Guild',
|
|
168
|
+
ownerId: TEST_USER_ID,
|
|
169
|
+
},
|
|
170
|
+
channels: [
|
|
171
|
+
{
|
|
172
|
+
id: TEXT_CHANNEL_ID,
|
|
173
|
+
name: 'agent-model-e2e',
|
|
174
|
+
type: ChannelType.GuildText,
|
|
175
|
+
},
|
|
176
|
+
],
|
|
177
|
+
users: [
|
|
178
|
+
{
|
|
179
|
+
id: TEST_USER_ID,
|
|
180
|
+
username: 'agent-model-tester',
|
|
181
|
+
},
|
|
182
|
+
],
|
|
183
|
+
dbUrl: `file:${digitalDiscordDbPath}`,
|
|
184
|
+
});
|
|
185
|
+
await discord.start();
|
|
186
|
+
const providerNpm = url
|
|
187
|
+
.pathToFileURL(path.resolve(process.cwd(), '..', 'opencode-deterministic-provider', 'src', 'index.ts'))
|
|
188
|
+
.toString();
|
|
189
|
+
// Build base config with default model
|
|
190
|
+
const opencodeConfig = buildDeterministicOpencodeConfig({
|
|
191
|
+
providerName: PROVIDER_NAME,
|
|
192
|
+
providerNpm,
|
|
193
|
+
model: DEFAULT_MODEL,
|
|
194
|
+
smallModel: DEFAULT_MODEL,
|
|
195
|
+
settings: {
|
|
196
|
+
strict: false,
|
|
197
|
+
matchers: createDeterministicMatchers(),
|
|
198
|
+
},
|
|
199
|
+
});
|
|
200
|
+
// Add extra models to the provider so opencode accepts them
|
|
201
|
+
const providerConfig = opencodeConfig.provider[PROVIDER_NAME];
|
|
202
|
+
providerConfig.models[AGENT_MODEL] = { name: AGENT_MODEL };
|
|
203
|
+
providerConfig.models[CHANNEL_MODEL] = { name: CHANNEL_MODEL };
|
|
204
|
+
fs.writeFileSync(path.join(directories.projectDirectory, 'opencode.json'), JSON.stringify(opencodeConfig, null, 2));
|
|
205
|
+
// Create the agent .md file with custom model
|
|
206
|
+
createAgentFile({
|
|
207
|
+
projectDirectory: directories.projectDirectory,
|
|
208
|
+
agentName: 'test-agent',
|
|
209
|
+
model: `${PROVIDER_NAME}/${AGENT_MODEL}`,
|
|
210
|
+
});
|
|
211
|
+
const dbPath = path.join(directories.dataDir, 'discord-sessions.db');
|
|
212
|
+
const hranaResult = await startHranaServer({ dbPath });
|
|
213
|
+
if (hranaResult instanceof Error) {
|
|
214
|
+
throw hranaResult;
|
|
215
|
+
}
|
|
216
|
+
process.env['KIMAKI_DB_URL'] = hranaResult;
|
|
217
|
+
await initDatabase();
|
|
218
|
+
await setBotToken(discord.botUserId, discord.botToken);
|
|
219
|
+
await setChannelDirectory({
|
|
220
|
+
channelId: TEXT_CHANNEL_ID,
|
|
221
|
+
directory: directories.projectDirectory,
|
|
222
|
+
channelType: 'text',
|
|
223
|
+
});
|
|
224
|
+
await setChannelVerbosity(TEXT_CHANNEL_ID, 'tools_and_text');
|
|
225
|
+
botClient = createDiscordJsClient({ restUrl: discord.restUrl });
|
|
226
|
+
await startDiscordBot({
|
|
227
|
+
token: discord.botToken,
|
|
228
|
+
appId: discord.botUserId,
|
|
229
|
+
discordClient: botClient,
|
|
230
|
+
});
|
|
231
|
+
// Pre-warm the opencode server so agent discovery happens
|
|
232
|
+
const warmup = await initializeOpencodeForDirectory(directories.projectDirectory);
|
|
233
|
+
if (warmup instanceof Error) {
|
|
234
|
+
throw warmup;
|
|
235
|
+
}
|
|
236
|
+
}, 60_000);
|
|
237
|
+
afterAll(async () => {
|
|
238
|
+
if (directories) {
|
|
239
|
+
await cleanupTestSessions({
|
|
240
|
+
projectDirectory: directories.projectDirectory,
|
|
241
|
+
testStartTime,
|
|
242
|
+
});
|
|
243
|
+
}
|
|
244
|
+
if (botClient) {
|
|
245
|
+
botClient.destroy();
|
|
246
|
+
}
|
|
247
|
+
await cleanupOpencodeServers();
|
|
248
|
+
await Promise.all([
|
|
249
|
+
closeDatabase().catch(() => {
|
|
250
|
+
return;
|
|
251
|
+
}),
|
|
252
|
+
stopHranaServer().catch(() => {
|
|
253
|
+
return;
|
|
254
|
+
}),
|
|
255
|
+
discord?.stop().catch(() => {
|
|
256
|
+
return;
|
|
257
|
+
}),
|
|
258
|
+
]);
|
|
259
|
+
delete process.env['KIMAKI_LOCK_PORT'];
|
|
260
|
+
delete process.env['KIMAKI_DB_URL'];
|
|
261
|
+
if (previousDefaultVerbosity) {
|
|
262
|
+
store.setState({ defaultVerbosity: previousDefaultVerbosity });
|
|
263
|
+
}
|
|
264
|
+
if (directories) {
|
|
265
|
+
fs.rmSync(directories.dataDir, { recursive: true, force: true });
|
|
266
|
+
}
|
|
267
|
+
}, 10_000);
|
|
268
|
+
test('new thread uses agent model when channel agent is set', async () => {
|
|
269
|
+
// Set channel agent preference — this simulates /agent selecting test-agent
|
|
270
|
+
await setChannelAgent(TEXT_CHANNEL_ID, 'test-agent');
|
|
271
|
+
// Send a message to create a new thread
|
|
272
|
+
await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
273
|
+
content: 'Reply with exactly: agent-model-check',
|
|
274
|
+
});
|
|
275
|
+
const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
276
|
+
timeout: 4_000,
|
|
277
|
+
predicate: (t) => {
|
|
278
|
+
return t.name === 'Reply with exactly: agent-model-check';
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
// Wait for the footer (starts with *project) — proves run completed.
|
|
282
|
+
// Then assert which model ID appears in it.
|
|
283
|
+
await waitForBotMessageContaining({
|
|
284
|
+
discord,
|
|
285
|
+
threadId: thread.id,
|
|
286
|
+
userId: TEST_USER_ID,
|
|
287
|
+
text: '*project',
|
|
288
|
+
timeout: 4_000,
|
|
289
|
+
});
|
|
290
|
+
const messages = await discord.thread(thread.id).getMessages();
|
|
291
|
+
// Find the footer message (starts with * italic)
|
|
292
|
+
const footerMessage = messages.find((message) => {
|
|
293
|
+
return (message.author.id === discord.botUserId &&
|
|
294
|
+
message.content.startsWith('*'));
|
|
295
|
+
});
|
|
296
|
+
expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
|
|
297
|
+
"--- from: user (agent-model-tester)
|
|
298
|
+
Reply with exactly: agent-model-check
|
|
299
|
+
--- from: assistant (TestBot)
|
|
300
|
+
⬥ ok
|
|
301
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***"
|
|
302
|
+
`);
|
|
303
|
+
expect(footerMessage).toBeDefined();
|
|
304
|
+
if (!footerMessage) {
|
|
305
|
+
throw new Error(`Expected footer message but none found. Bot messages: ${messages
|
|
306
|
+
.filter((m) => m.author.id === discord.botUserId)
|
|
307
|
+
.map((m) => m.content.slice(0, 150))
|
|
308
|
+
.join(' | ')}`);
|
|
309
|
+
}
|
|
310
|
+
// The footer should contain the agent's model, not the default
|
|
311
|
+
expect(footerMessage.content).toContain(AGENT_MODEL);
|
|
312
|
+
expect(footerMessage.content).not.toContain(DEFAULT_MODEL);
|
|
313
|
+
}, 15_000);
|
|
314
|
+
test('promptAsync path includes rich system context', async () => {
|
|
315
|
+
await setChannelAgent(TEXT_CHANNEL_ID, 'test-agent');
|
|
316
|
+
await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
317
|
+
content: 'Reply with exactly: system-context-check',
|
|
318
|
+
});
|
|
319
|
+
const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
320
|
+
timeout: 4_000,
|
|
321
|
+
predicate: (t) => {
|
|
322
|
+
return t.name === 'Reply with exactly: system-context-check';
|
|
323
|
+
},
|
|
324
|
+
});
|
|
325
|
+
await waitForBotMessageContaining({
|
|
326
|
+
discord,
|
|
327
|
+
threadId: thread.id,
|
|
328
|
+
userId: TEST_USER_ID,
|
|
329
|
+
text: 'system-context-ok',
|
|
330
|
+
timeout: 4_000,
|
|
331
|
+
});
|
|
332
|
+
await waitForFooterMessage({
|
|
333
|
+
discord,
|
|
334
|
+
threadId: thread.id,
|
|
335
|
+
timeout: 4_000,
|
|
336
|
+
afterMessageIncludes: 'system-context-ok',
|
|
337
|
+
afterAuthorId: discord.botUserId,
|
|
338
|
+
});
|
|
339
|
+
expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
|
|
340
|
+
"--- from: user (agent-model-tester)
|
|
341
|
+
Reply with exactly: system-context-check
|
|
342
|
+
--- from: assistant (TestBot)
|
|
343
|
+
⬥ system-context-ok
|
|
344
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ agent-model-v2 ⋅ **test-agent***"
|
|
345
|
+
`);
|
|
346
|
+
}, 15_000);
|
|
347
|
+
test('new thread uses channel model when channel model preference is set', async () => {
|
|
348
|
+
// Clear channel agent so model resolution falls through to channel model
|
|
349
|
+
const prisma = await getPrisma();
|
|
350
|
+
await prisma.channel_agents.deleteMany({
|
|
351
|
+
where: { channel_id: TEXT_CHANNEL_ID },
|
|
352
|
+
});
|
|
353
|
+
// Set channel model preference — simulates /model selecting a model at channel scope
|
|
354
|
+
await setChannelModel({
|
|
355
|
+
channelId: TEXT_CHANNEL_ID,
|
|
356
|
+
modelId: `${PROVIDER_NAME}/${CHANNEL_MODEL}`,
|
|
357
|
+
});
|
|
358
|
+
await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
359
|
+
content: 'Reply with exactly: channel-model-check',
|
|
360
|
+
});
|
|
361
|
+
const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
362
|
+
timeout: 4_000,
|
|
363
|
+
predicate: (t) => {
|
|
364
|
+
return t.name === 'Reply with exactly: channel-model-check';
|
|
365
|
+
},
|
|
366
|
+
});
|
|
367
|
+
await waitForBotMessageContaining({
|
|
368
|
+
discord,
|
|
369
|
+
threadId: thread.id,
|
|
370
|
+
userId: TEST_USER_ID,
|
|
371
|
+
text: '*project',
|
|
372
|
+
timeout: 4_000,
|
|
373
|
+
});
|
|
374
|
+
const messages = await discord.thread(thread.id).getMessages();
|
|
375
|
+
const footerMessage = messages.find((message) => {
|
|
376
|
+
return (message.author.id === discord.botUserId &&
|
|
377
|
+
message.content.startsWith('*'));
|
|
378
|
+
});
|
|
379
|
+
expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
|
|
380
|
+
"--- from: user (agent-model-tester)
|
|
381
|
+
Reply with exactly: channel-model-check
|
|
382
|
+
--- from: assistant (TestBot)
|
|
383
|
+
⬥ ok
|
|
384
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ channel-model-v2*"
|
|
385
|
+
`);
|
|
386
|
+
expect(footerMessage).toBeDefined();
|
|
387
|
+
if (!footerMessage) {
|
|
388
|
+
throw new Error(`Expected footer message but none found. Bot messages: ${messages
|
|
389
|
+
.filter((m) => m.author.id === discord.botUserId)
|
|
390
|
+
.map((m) => m.content.slice(0, 150))
|
|
391
|
+
.join(' | ')}`);
|
|
392
|
+
}
|
|
393
|
+
// Footer should contain the channel model, not the default
|
|
394
|
+
expect(footerMessage.content).toContain(CHANNEL_MODEL);
|
|
395
|
+
expect(footerMessage.content).not.toContain(DEFAULT_MODEL);
|
|
396
|
+
}, 15_000);
|
|
397
|
+
test('channel model with variant preference completes without error', async () => {
|
|
398
|
+
// Clear channel agent so model resolution falls through to channel model
|
|
399
|
+
const prisma = await getPrisma();
|
|
400
|
+
await prisma.channel_agents.deleteMany({
|
|
401
|
+
where: { channel_id: TEXT_CHANNEL_ID },
|
|
402
|
+
});
|
|
403
|
+
// Set channel model with a variant (thinking level)
|
|
404
|
+
// The deterministic provider doesn't support thinking, so the variant
|
|
405
|
+
// is resolved but silently dropped (no matching thinking values).
|
|
406
|
+
// This test verifies the variant cascade code path runs without crashing
|
|
407
|
+
// and the correct model still appears in the footer.
|
|
408
|
+
await setChannelModel({
|
|
409
|
+
channelId: TEXT_CHANNEL_ID,
|
|
410
|
+
modelId: `${PROVIDER_NAME}/${CHANNEL_MODEL}`,
|
|
411
|
+
variant: 'high',
|
|
412
|
+
});
|
|
413
|
+
await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
|
|
414
|
+
content: 'Reply with exactly: variant-check',
|
|
415
|
+
});
|
|
416
|
+
const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
|
|
417
|
+
timeout: 4_000,
|
|
418
|
+
predicate: (t) => {
|
|
419
|
+
return t.name === 'Reply with exactly: variant-check';
|
|
420
|
+
},
|
|
421
|
+
});
|
|
422
|
+
await waitForBotMessageContaining({
|
|
423
|
+
discord,
|
|
424
|
+
threadId: thread.id,
|
|
425
|
+
userId: TEST_USER_ID,
|
|
426
|
+
text: '*project',
|
|
427
|
+
timeout: 4_000,
|
|
428
|
+
});
|
|
429
|
+
const messages = await discord.thread(thread.id).getMessages();
|
|
430
|
+
const footerMessage = messages.find((message) => {
|
|
431
|
+
return (message.author.id === discord.botUserId &&
|
|
432
|
+
message.content.startsWith('*'));
|
|
433
|
+
});
|
|
434
|
+
expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
|
|
435
|
+
"--- from: user (agent-model-tester)
|
|
436
|
+
Reply with exactly: variant-check
|
|
437
|
+
--- from: assistant (TestBot)
|
|
438
|
+
⬥ ok
|
|
439
|
+
*project ⋅ main ⋅ Ns ⋅ N% ⋅ channel-model-v2*"
|
|
440
|
+
`);
|
|
441
|
+
expect(footerMessage).toBeDefined();
|
|
442
|
+
if (!footerMessage) {
|
|
443
|
+
throw new Error(`Expected footer message but none found. Bot messages: ${messages
|
|
444
|
+
.filter((m) => m.author.id === discord.botUserId)
|
|
445
|
+
.map((m) => m.content.slice(0, 150))
|
|
446
|
+
.join(' | ')}`);
|
|
447
|
+
}
|
|
448
|
+
// Footer should still contain the channel model (variant doesn't crash)
|
|
449
|
+
expect(footerMessage.content).toContain(CHANNEL_MODEL);
|
|
450
|
+
expect(footerMessage.content).not.toContain(DEFAULT_MODEL);
|
|
451
|
+
}, 15_000);
|
|
452
|
+
});
|
|
@@ -40,7 +40,7 @@ export async function ensureKimakiAudioCategory(guild, botName) {
|
|
|
40
40
|
type: ChannelType.GuildCategory,
|
|
41
41
|
});
|
|
42
42
|
}
|
|
43
|
-
export async function createProjectChannels({ guild, projectDirectory,
|
|
43
|
+
export async function createProjectChannels({ guild, projectDirectory, botName, enableVoiceChannels = false, }) {
|
|
44
44
|
const baseName = path.basename(projectDirectory);
|
|
45
45
|
const channelName = `${baseName}`
|
|
46
46
|
.toLowerCase()
|
|
@@ -57,7 +57,6 @@ export async function createProjectChannels({ guild, projectDirectory, appId, bo
|
|
|
57
57
|
channelId: textChannel.id,
|
|
58
58
|
directory: projectDirectory,
|
|
59
59
|
channelType: 'text',
|
|
60
|
-
appId,
|
|
61
60
|
});
|
|
62
61
|
let voiceChannelId = null;
|
|
63
62
|
if (enableVoiceChannels) {
|
|
@@ -71,7 +70,6 @@ export async function createProjectChannels({ guild, projectDirectory, appId, bo
|
|
|
71
70
|
channelId: voiceChannel.id,
|
|
72
71
|
directory: projectDirectory,
|
|
73
72
|
channelType: 'voice',
|
|
74
|
-
appId,
|
|
75
73
|
});
|
|
76
74
|
voiceChannelId = voiceChannel.id;
|
|
77
75
|
}
|
|
@@ -94,7 +92,6 @@ export async function getChannelsWithDescriptions(guild) {
|
|
|
94
92
|
name: textChannel.name,
|
|
95
93
|
description,
|
|
96
94
|
kimakiDirectory: channelConfig?.directory,
|
|
97
|
-
kimakiApp: channelConfig?.appId || undefined,
|
|
98
95
|
});
|
|
99
96
|
}
|
|
100
97
|
return channels;
|
package/dist/cli-parsing.test.js
CHANGED
|
@@ -14,6 +14,10 @@ function createCliForIdParsing() {
|
|
|
14
14
|
.command('session search <query>', 'Search sessions')
|
|
15
15
|
.option('--channel <channelId>', 'Discord channel ID')
|
|
16
16
|
.option('--project <path>', 'Project path');
|
|
17
|
+
cli
|
|
18
|
+
.command('session export-events-jsonl', 'Export in-memory events to JSONL')
|
|
19
|
+
.option('--session <sessionId>', 'Session ID')
|
|
20
|
+
.option('--out <file>', 'Output path');
|
|
17
21
|
cli
|
|
18
22
|
.command('add-project', 'Add a project')
|
|
19
23
|
.option('-g, --guild <guildId>', 'Discord guild/server ID');
|
|
@@ -68,6 +72,27 @@ describe('goke CLI ID parsing', () => {
|
|
|
68
72
|
expect(result.options.channel).toBe(channelId);
|
|
69
73
|
expect(typeof result.options.channel).toBe('string');
|
|
70
74
|
});
|
|
75
|
+
test('keeps session export options as strings', () => {
|
|
76
|
+
const cli = createCliForIdParsing();
|
|
77
|
+
const sessionId = '001111222233334444';
|
|
78
|
+
const outPath = './tmp/session-events.jsonl';
|
|
79
|
+
const result = cli.parse([
|
|
80
|
+
'node',
|
|
81
|
+
'kimaki',
|
|
82
|
+
'session',
|
|
83
|
+
'export-events-jsonl',
|
|
84
|
+
'--session',
|
|
85
|
+
sessionId,
|
|
86
|
+
'--out',
|
|
87
|
+
outPath,
|
|
88
|
+
], {
|
|
89
|
+
run: false,
|
|
90
|
+
});
|
|
91
|
+
expect(result.options.session).toBe(sessionId);
|
|
92
|
+
expect(typeof result.options.session).toBe('string');
|
|
93
|
+
expect(result.options.out).toBe(outPath);
|
|
94
|
+
expect(typeof result.options.out).toBe('string');
|
|
95
|
+
});
|
|
71
96
|
test('keeps --send-at cron string intact', () => {
|
|
72
97
|
const cli = createCliForIdParsing();
|
|
73
98
|
const cron = '0 9 * * 1';
|