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
package/src/xai-auth-plugin.ts
CHANGED
package/src/xai-auth-state.ts
CHANGED
|
@@ -1,169 +0,0 @@
|
|
|
1
|
-
// E2e test verifying that the opencode server populates the `finish` field
|
|
2
|
-
// on assistant messages. This field is critical for kimaki's footer logic:
|
|
3
|
-
// isAssistantMessageNaturalCompletion checks `message.finish !== 'tool-calls'`
|
|
4
|
-
// to suppress footers on intermediate tool-call steps.
|
|
5
|
-
// When `finish` is missing/null, every completed assistant message gets a
|
|
6
|
-
// spurious footer, breaking multi-step tool chains (16 test failures).
|
|
7
|
-
//
|
|
8
|
-
// Direct SDK test — no Discord layer needed since this is a server-level bug.
|
|
9
|
-
import fs from 'node:fs';
|
|
10
|
-
import path from 'node:path';
|
|
11
|
-
import url from 'node:url';
|
|
12
|
-
import { test, expect, beforeAll, afterAll } from 'vitest';
|
|
13
|
-
import { buildDeterministicOpencodeConfig, } from 'opencode-deterministic-provider';
|
|
14
|
-
import { setDataDir } from './config.js';
|
|
15
|
-
import { initializeOpencodeForDirectory, stopOpencodeServer } from './opencode.js';
|
|
16
|
-
import { cleanupTestSessions, initTestGitRepo } from './test-utils.js';
|
|
17
|
-
const ROOT = path.resolve(process.cwd(), 'tmp', 'finish-field-e2e');
|
|
18
|
-
function createRunDirectories() {
|
|
19
|
-
fs.mkdirSync(ROOT, { recursive: true });
|
|
20
|
-
const dataDir = fs.mkdtempSync(path.join(ROOT, 'data-'));
|
|
21
|
-
const projectDirectory = path.join(ROOT, 'project');
|
|
22
|
-
fs.mkdirSync(projectDirectory, { recursive: true });
|
|
23
|
-
initTestGitRepo(projectDirectory);
|
|
24
|
-
return { dataDir, projectDirectory };
|
|
25
|
-
}
|
|
26
|
-
function createMatchers() {
|
|
27
|
-
// Tool-call step: finish="tool-calls"
|
|
28
|
-
const toolCallMatcher = {
|
|
29
|
-
id: 'finish-tool-call',
|
|
30
|
-
priority: 20,
|
|
31
|
-
when: {
|
|
32
|
-
lastMessageRole: 'user',
|
|
33
|
-
latestUserTextIncludes: 'FINISH_FIELD_TOOLCALL',
|
|
34
|
-
},
|
|
35
|
-
then: {
|
|
36
|
-
parts: [
|
|
37
|
-
{ type: 'stream-start', warnings: [] },
|
|
38
|
-
{ type: 'text-start', id: 'ft' },
|
|
39
|
-
{ type: 'text-delta', id: 'ft', delta: 'calling tool' },
|
|
40
|
-
{ type: 'text-end', id: 'ft' },
|
|
41
|
-
{
|
|
42
|
-
type: 'tool-call',
|
|
43
|
-
toolCallId: 'finish-bash',
|
|
44
|
-
toolName: 'bash',
|
|
45
|
-
input: JSON.stringify({ command: 'echo ok', description: 'test' }),
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
type: 'finish',
|
|
49
|
-
finishReason: 'tool-calls',
|
|
50
|
-
usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
|
51
|
-
},
|
|
52
|
-
],
|
|
53
|
-
},
|
|
54
|
-
};
|
|
55
|
-
// Follow-up after tool result: finish="stop"
|
|
56
|
-
const followupMatcher = {
|
|
57
|
-
id: 'finish-followup',
|
|
58
|
-
priority: 21,
|
|
59
|
-
when: {
|
|
60
|
-
lastMessageRole: 'tool',
|
|
61
|
-
latestUserTextIncludes: 'FINISH_FIELD_TOOLCALL',
|
|
62
|
-
},
|
|
63
|
-
then: {
|
|
64
|
-
parts: [
|
|
65
|
-
{ type: 'stream-start', warnings: [] },
|
|
66
|
-
{ type: 'text-start', id: 'ff' },
|
|
67
|
-
{ type: 'text-delta', id: 'ff', delta: 'tool done' },
|
|
68
|
-
{ type: 'text-end', id: 'ff' },
|
|
69
|
-
{
|
|
70
|
-
type: 'finish',
|
|
71
|
-
finishReason: 'stop',
|
|
72
|
-
usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
|
73
|
-
},
|
|
74
|
-
],
|
|
75
|
-
},
|
|
76
|
-
};
|
|
77
|
-
return [toolCallMatcher, followupMatcher];
|
|
78
|
-
}
|
|
79
|
-
let client;
|
|
80
|
-
let directories;
|
|
81
|
-
let testStartTime;
|
|
82
|
-
beforeAll(async () => {
|
|
83
|
-
testStartTime = Date.now();
|
|
84
|
-
directories = createRunDirectories();
|
|
85
|
-
setDataDir(directories.dataDir);
|
|
86
|
-
const providerNpm = url
|
|
87
|
-
.pathToFileURL(path.resolve(process.cwd(), '..', 'opencode-deterministic-provider', 'src', 'index.ts'))
|
|
88
|
-
.toString();
|
|
89
|
-
const opencodeConfig = buildDeterministicOpencodeConfig({
|
|
90
|
-
providerName: 'deterministic-provider',
|
|
91
|
-
providerNpm,
|
|
92
|
-
model: 'deterministic-v2',
|
|
93
|
-
smallModel: 'deterministic-v2',
|
|
94
|
-
settings: { strict: false, matchers: createMatchers() },
|
|
95
|
-
});
|
|
96
|
-
fs.writeFileSync(path.join(directories.projectDirectory, 'opencode.json'), JSON.stringify(opencodeConfig, null, 2));
|
|
97
|
-
const getClient = await initializeOpencodeForDirectory(directories.projectDirectory);
|
|
98
|
-
if (getClient instanceof Error) {
|
|
99
|
-
throw getClient;
|
|
100
|
-
}
|
|
101
|
-
client = getClient();
|
|
102
|
-
}, 20_000);
|
|
103
|
-
afterAll(async () => {
|
|
104
|
-
await cleanupTestSessions({
|
|
105
|
-
projectDirectory: directories.projectDirectory,
|
|
106
|
-
testStartTime,
|
|
107
|
-
});
|
|
108
|
-
await stopOpencodeServer();
|
|
109
|
-
}, 5_000);
|
|
110
|
-
test('tool-call step has finish="tool-calls", follow-up has finish="stop"', async () => {
|
|
111
|
-
const session = await client.session.create({
|
|
112
|
-
directory: directories.projectDirectory,
|
|
113
|
-
title: 'finish-field-test',
|
|
114
|
-
});
|
|
115
|
-
const sessionID = session.data.id;
|
|
116
|
-
await client.session.promptAsync({
|
|
117
|
-
sessionID,
|
|
118
|
-
directory: directories.projectDirectory,
|
|
119
|
-
parts: [{ type: 'text', text: 'FINISH_FIELD_TOOLCALL' }],
|
|
120
|
-
});
|
|
121
|
-
// Poll until we have 2 completed assistant messages (tool-call + follow-up)
|
|
122
|
-
const maxWait = 8_000;
|
|
123
|
-
const pollStart = Date.now();
|
|
124
|
-
let completedAssistants = [];
|
|
125
|
-
while (Date.now() - pollStart < maxWait) {
|
|
126
|
-
const msgs = await client.session.messages({
|
|
127
|
-
sessionID,
|
|
128
|
-
directory: directories.projectDirectory,
|
|
129
|
-
});
|
|
130
|
-
completedAssistants = (msgs.data || [])
|
|
131
|
-
.filter((m) => {
|
|
132
|
-
return m.info.role === 'assistant' && m.info.time.completed;
|
|
133
|
-
})
|
|
134
|
-
.map((m) => {
|
|
135
|
-
return {
|
|
136
|
-
finish: m.info.finish ?? null,
|
|
137
|
-
partTypes: m.parts.map((p) => { return p.type; }),
|
|
138
|
-
};
|
|
139
|
-
});
|
|
140
|
-
if (completedAssistants.length >= 2) {
|
|
141
|
-
break;
|
|
142
|
-
}
|
|
143
|
-
await new Promise((resolve) => { setTimeout(resolve, 100); });
|
|
144
|
-
}
|
|
145
|
-
// Snapshot completed assistant messages — finish should NOT be null
|
|
146
|
-
expect(completedAssistants).toMatchInlineSnapshot(`
|
|
147
|
-
[
|
|
148
|
-
{
|
|
149
|
-
"finish": "tool-calls",
|
|
150
|
-
"partTypes": [
|
|
151
|
-
"step-start",
|
|
152
|
-
"text",
|
|
153
|
-
"tool",
|
|
154
|
-
"step-finish",
|
|
155
|
-
],
|
|
156
|
-
},
|
|
157
|
-
{
|
|
158
|
-
"finish": "stop",
|
|
159
|
-
"partTypes": [
|
|
160
|
-
"step-start",
|
|
161
|
-
"text",
|
|
162
|
-
"step-finish",
|
|
163
|
-
],
|
|
164
|
-
},
|
|
165
|
-
]
|
|
166
|
-
`);
|
|
167
|
-
const finishes = completedAssistants.map((m) => { return m.finish; });
|
|
168
|
-
expect(finishes).toEqual(['tool-calls', 'stop']);
|
|
169
|
-
}, 5_000);
|
|
@@ -1,184 +0,0 @@
|
|
|
1
|
-
// OpenCode plugin that aborts task-created subagent sessions after rate limits.
|
|
2
|
-
import { appendToastSessionMarker, createPluginLogger, formatPluginErrorWithStack, setPluginLogFilePath, } from './plugin-logger.js';
|
|
3
|
-
import { createPluginClient } from './plugin-opencode-client.js';
|
|
4
|
-
import { initSentry, notifyError } from './sentry.js';
|
|
5
|
-
const logger = createPluginLogger('SUBMODEL');
|
|
6
|
-
const RATE_LIMIT_TEXT_PATTERNS = [
|
|
7
|
-
'rate_limit',
|
|
8
|
-
'rate limit',
|
|
9
|
-
'resource exhausted',
|
|
10
|
-
'retry after',
|
|
11
|
-
'too many requests',
|
|
12
|
-
'quota exceeded',
|
|
13
|
-
'balance exhausted',
|
|
14
|
-
'spending-limit',
|
|
15
|
-
'run out of credits',
|
|
16
|
-
];
|
|
17
|
-
function isRateLimitText(text) {
|
|
18
|
-
if (!text) {
|
|
19
|
-
return false;
|
|
20
|
-
}
|
|
21
|
-
const haystack = text.toLowerCase();
|
|
22
|
-
return RATE_LIMIT_TEXT_PATTERNS.some((pattern) => {
|
|
23
|
-
return haystack.includes(pattern);
|
|
24
|
-
});
|
|
25
|
-
}
|
|
26
|
-
function getTaskChildSession(event) {
|
|
27
|
-
if (event.type !== 'message.part.updated') {
|
|
28
|
-
return undefined;
|
|
29
|
-
}
|
|
30
|
-
const part = event.properties.part;
|
|
31
|
-
if (part.type !== 'tool' || part.tool !== 'task' || part.state.status === 'pending') {
|
|
32
|
-
return undefined;
|
|
33
|
-
}
|
|
34
|
-
const childSessionId = part.state.metadata?.sessionId;
|
|
35
|
-
if (typeof childSessionId !== 'string' || childSessionId.length === 0) {
|
|
36
|
-
return undefined;
|
|
37
|
-
}
|
|
38
|
-
const subagentType = part.state.input?.subagent_type;
|
|
39
|
-
return {
|
|
40
|
-
childSessionId,
|
|
41
|
-
subagentType: typeof subagentType === 'string' ? subagentType : undefined,
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
function getEventSessionId(event) {
|
|
45
|
-
if (event.type === 'session.status' || event.type === 'session.idle') {
|
|
46
|
-
return event.properties.sessionID;
|
|
47
|
-
}
|
|
48
|
-
if (event.type === 'session.error') {
|
|
49
|
-
return event.properties.sessionID;
|
|
50
|
-
}
|
|
51
|
-
if (event.type === 'message.updated') {
|
|
52
|
-
return event.properties.info.sessionID;
|
|
53
|
-
}
|
|
54
|
-
if (event.type === 'message.part.updated') {
|
|
55
|
-
return event.properties.part.sessionID;
|
|
56
|
-
}
|
|
57
|
-
if (event.type === 'session.created'
|
|
58
|
-
|| event.type === 'session.updated'
|
|
59
|
-
|| event.type === 'session.deleted') {
|
|
60
|
-
return event.properties.info.id;
|
|
61
|
-
}
|
|
62
|
-
return undefined;
|
|
63
|
-
}
|
|
64
|
-
function extractRateLimitReason(event) {
|
|
65
|
-
if (event.type === 'session.status' && event.properties.status.type === 'retry') {
|
|
66
|
-
return isRateLimitText(event.properties.status.message)
|
|
67
|
-
? event.properties.status.message
|
|
68
|
-
: undefined;
|
|
69
|
-
}
|
|
70
|
-
if (event.type === 'message.part.updated' && event.properties.part.type === 'retry') {
|
|
71
|
-
const retryError = event.properties.part.error;
|
|
72
|
-
if (retryError.data.statusCode === 429 || retryError.data.statusCode === 402) {
|
|
73
|
-
return retryError.data.message;
|
|
74
|
-
}
|
|
75
|
-
if (isRateLimitText(retryError.data.responseBody)) {
|
|
76
|
-
return retryError.data.responseBody;
|
|
77
|
-
}
|
|
78
|
-
return isRateLimitText(retryError.data.message)
|
|
79
|
-
? retryError.data.message
|
|
80
|
-
: undefined;
|
|
81
|
-
}
|
|
82
|
-
const apiError = (() => {
|
|
83
|
-
if (event.type === 'session.error' && event.properties.error?.name === 'APIError') {
|
|
84
|
-
return event.properties.error.data;
|
|
85
|
-
}
|
|
86
|
-
if (event.type === 'message.updated'
|
|
87
|
-
&& event.properties.info.role === 'assistant'
|
|
88
|
-
&& event.properties.info.error?.name === 'APIError') {
|
|
89
|
-
return event.properties.info.error.data;
|
|
90
|
-
}
|
|
91
|
-
return undefined;
|
|
92
|
-
})();
|
|
93
|
-
if (!apiError) {
|
|
94
|
-
return undefined;
|
|
95
|
-
}
|
|
96
|
-
if (apiError.statusCode === 429 || apiError.statusCode === 402) {
|
|
97
|
-
return apiError.message;
|
|
98
|
-
}
|
|
99
|
-
// xAI returns 403 with spending-limit or run out of credits text.
|
|
100
|
-
// Only treat 403 as a usage limit if the body matches specific patterns,
|
|
101
|
-
// not all 403s (which can be auth errors handled elsewhere).
|
|
102
|
-
if (apiError.statusCode === 403) {
|
|
103
|
-
const errorText = [apiError.message, apiError.responseBody].filter(Boolean).join(' ');
|
|
104
|
-
if (isRateLimitText(errorText))
|
|
105
|
-
return apiError.message;
|
|
106
|
-
}
|
|
107
|
-
if (isRateLimitText(apiError.responseBody)) {
|
|
108
|
-
return apiError.responseBody;
|
|
109
|
-
}
|
|
110
|
-
return isRateLimitText(apiError.message) ? apiError.message : undefined;
|
|
111
|
-
}
|
|
112
|
-
export const subagentRateLimitPlugin = async ({ serverUrl, directory }) => {
|
|
113
|
-
initSentry();
|
|
114
|
-
const dataDir = process.env.KIMAKI_DATA_DIR;
|
|
115
|
-
if (dataDir) {
|
|
116
|
-
setPluginLogFilePath(dataDir);
|
|
117
|
-
}
|
|
118
|
-
// Build our own v2 client. The plugin-provided ctx.client (v1) does not
|
|
119
|
-
// reliably make REST calls (session.abort silently no-ops) from inside the
|
|
120
|
-
// plugin process. See plugin-opencode-client.ts.
|
|
121
|
-
const client = createPluginClient({ serverUrl, directory });
|
|
122
|
-
logger.bindClient(client);
|
|
123
|
-
const subagentSessions = new Map();
|
|
124
|
-
return {
|
|
125
|
-
event: async ({ event }) => {
|
|
126
|
-
const taskChild = getTaskChildSession(event);
|
|
127
|
-
if (taskChild) {
|
|
128
|
-
const existing = subagentSessions.get(taskChild.childSessionId);
|
|
129
|
-
if (existing) {
|
|
130
|
-
if (taskChild.subagentType) {
|
|
131
|
-
existing.subagentType = taskChild.subagentType;
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
else {
|
|
135
|
-
subagentSessions.set(taskChild.childSessionId, {
|
|
136
|
-
subagentType: taskChild.subagentType,
|
|
137
|
-
aborting: false,
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
const eventSessionId = getEventSessionId(event);
|
|
142
|
-
if (!eventSessionId) {
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
if (event.type === 'session.deleted' || event.type === 'session.idle') {
|
|
146
|
-
subagentSessions.delete(eventSessionId);
|
|
147
|
-
return;
|
|
148
|
-
}
|
|
149
|
-
const rateLimitReason = extractRateLimitReason(event);
|
|
150
|
-
if (!rateLimitReason) {
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
const subagent = subagentSessions.get(eventSessionId);
|
|
154
|
-
if (!subagent || subagent.aborting) {
|
|
155
|
-
return;
|
|
156
|
-
}
|
|
157
|
-
subagent.aborting = true;
|
|
158
|
-
const abortResult = await (async () => {
|
|
159
|
-
await client.session.abort({ sessionID: eventSessionId, directory });
|
|
160
|
-
await client.tui.showToast({
|
|
161
|
-
message: appendToastSessionMarker({
|
|
162
|
-
message: `Aborting ${subagent.subagentType || 'subagent'} after rate limit so the parent task can recover: ${rateLimitReason}`,
|
|
163
|
-
sessionId: eventSessionId,
|
|
164
|
-
}),
|
|
165
|
-
variant: 'info',
|
|
166
|
-
}).catch(() => {
|
|
167
|
-
return;
|
|
168
|
-
});
|
|
169
|
-
logger.info(`Aborted subagent ${eventSessionId} after rate limit`);
|
|
170
|
-
})()
|
|
171
|
-
.catch((error) => {
|
|
172
|
-
return new Error('Subagent rate-limit abort failed', {
|
|
173
|
-
cause: error,
|
|
174
|
-
});
|
|
175
|
-
});
|
|
176
|
-
subagentSessions.delete(eventSessionId);
|
|
177
|
-
if (!(abortResult instanceof Error)) {
|
|
178
|
-
return;
|
|
179
|
-
}
|
|
180
|
-
logger.warn(`[subagent-rate-limit-plugin] ${formatPluginErrorWithStack(abortResult)}`);
|
|
181
|
-
void notifyError(abortResult, 'subagent rate-limit plugin abort failed');
|
|
182
|
-
},
|
|
183
|
-
};
|
|
184
|
-
};
|
|
@@ -1,196 +0,0 @@
|
|
|
1
|
-
// E2e test verifying that the opencode server populates the `finish` field
|
|
2
|
-
// on assistant messages. This field is critical for kimaki's footer logic:
|
|
3
|
-
// isAssistantMessageNaturalCompletion checks `message.finish !== 'tool-calls'`
|
|
4
|
-
// to suppress footers on intermediate tool-call steps.
|
|
5
|
-
// When `finish` is missing/null, every completed assistant message gets a
|
|
6
|
-
// spurious footer, breaking multi-step tool chains (16 test failures).
|
|
7
|
-
//
|
|
8
|
-
// Direct SDK test — no Discord layer needed since this is a server-level bug.
|
|
9
|
-
|
|
10
|
-
import fs from 'node:fs'
|
|
11
|
-
import path from 'node:path'
|
|
12
|
-
import url from 'node:url'
|
|
13
|
-
import { test, expect, beforeAll, afterAll } from 'vitest'
|
|
14
|
-
import type { OpencodeClient } from '@opencode-ai/sdk/v2'
|
|
15
|
-
import {
|
|
16
|
-
buildDeterministicOpencodeConfig,
|
|
17
|
-
type DeterministicMatcher,
|
|
18
|
-
} from 'opencode-deterministic-provider'
|
|
19
|
-
import { setDataDir } from './config.js'
|
|
20
|
-
import { initializeOpencodeForDirectory, stopOpencodeServer } from './opencode.js'
|
|
21
|
-
import { cleanupTestSessions, initTestGitRepo } from './test-utils.js'
|
|
22
|
-
|
|
23
|
-
const ROOT = path.resolve(process.cwd(), 'tmp', 'finish-field-e2e')
|
|
24
|
-
|
|
25
|
-
function createRunDirectories() {
|
|
26
|
-
fs.mkdirSync(ROOT, { recursive: true })
|
|
27
|
-
const dataDir = fs.mkdtempSync(path.join(ROOT, 'data-'))
|
|
28
|
-
const projectDirectory = path.join(ROOT, 'project')
|
|
29
|
-
fs.mkdirSync(projectDirectory, { recursive: true })
|
|
30
|
-
initTestGitRepo(projectDirectory)
|
|
31
|
-
return { dataDir, projectDirectory }
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
function createMatchers(): DeterministicMatcher[] {
|
|
35
|
-
// Tool-call step: finish="tool-calls"
|
|
36
|
-
const toolCallMatcher: DeterministicMatcher = {
|
|
37
|
-
id: 'finish-tool-call',
|
|
38
|
-
priority: 20,
|
|
39
|
-
when: {
|
|
40
|
-
lastMessageRole: 'user',
|
|
41
|
-
latestUserTextIncludes: 'FINISH_FIELD_TOOLCALL',
|
|
42
|
-
},
|
|
43
|
-
then: {
|
|
44
|
-
parts: [
|
|
45
|
-
{ type: 'stream-start', warnings: [] },
|
|
46
|
-
{ type: 'text-start', id: 'ft' },
|
|
47
|
-
{ type: 'text-delta', id: 'ft', delta: 'calling tool' },
|
|
48
|
-
{ type: 'text-end', id: 'ft' },
|
|
49
|
-
{
|
|
50
|
-
type: 'tool-call',
|
|
51
|
-
toolCallId: 'finish-bash',
|
|
52
|
-
toolName: 'bash',
|
|
53
|
-
input: JSON.stringify({ command: 'echo ok', description: 'test' }),
|
|
54
|
-
},
|
|
55
|
-
{
|
|
56
|
-
type: 'finish',
|
|
57
|
-
finishReason: 'tool-calls',
|
|
58
|
-
usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
|
59
|
-
},
|
|
60
|
-
],
|
|
61
|
-
},
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Follow-up after tool result: finish="stop"
|
|
65
|
-
const followupMatcher: DeterministicMatcher = {
|
|
66
|
-
id: 'finish-followup',
|
|
67
|
-
priority: 21,
|
|
68
|
-
when: {
|
|
69
|
-
lastMessageRole: 'tool',
|
|
70
|
-
latestUserTextIncludes: 'FINISH_FIELD_TOOLCALL',
|
|
71
|
-
},
|
|
72
|
-
then: {
|
|
73
|
-
parts: [
|
|
74
|
-
{ type: 'stream-start', warnings: [] },
|
|
75
|
-
{ type: 'text-start', id: 'ff' },
|
|
76
|
-
{ type: 'text-delta', id: 'ff', delta: 'tool done' },
|
|
77
|
-
{ type: 'text-end', id: 'ff' },
|
|
78
|
-
{
|
|
79
|
-
type: 'finish',
|
|
80
|
-
finishReason: 'stop',
|
|
81
|
-
usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
|
|
82
|
-
},
|
|
83
|
-
],
|
|
84
|
-
},
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return [toolCallMatcher, followupMatcher]
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
let client: OpencodeClient
|
|
91
|
-
let directories: ReturnType<typeof createRunDirectories>
|
|
92
|
-
let testStartTime: number
|
|
93
|
-
|
|
94
|
-
beforeAll(async () => {
|
|
95
|
-
testStartTime = Date.now()
|
|
96
|
-
directories = createRunDirectories()
|
|
97
|
-
setDataDir(directories.dataDir)
|
|
98
|
-
|
|
99
|
-
const providerNpm = url
|
|
100
|
-
.pathToFileURL(
|
|
101
|
-
path.resolve(process.cwd(), '..', 'opencode-deterministic-provider', 'src', 'index.ts'),
|
|
102
|
-
)
|
|
103
|
-
.toString()
|
|
104
|
-
|
|
105
|
-
const opencodeConfig = buildDeterministicOpencodeConfig({
|
|
106
|
-
providerName: 'deterministic-provider',
|
|
107
|
-
providerNpm,
|
|
108
|
-
model: 'deterministic-v2',
|
|
109
|
-
smallModel: 'deterministic-v2',
|
|
110
|
-
settings: { strict: false, matchers: createMatchers() },
|
|
111
|
-
})
|
|
112
|
-
fs.writeFileSync(
|
|
113
|
-
path.join(directories.projectDirectory, 'opencode.json'),
|
|
114
|
-
JSON.stringify(opencodeConfig, null, 2),
|
|
115
|
-
)
|
|
116
|
-
|
|
117
|
-
const getClient = await initializeOpencodeForDirectory(directories.projectDirectory)
|
|
118
|
-
if (getClient instanceof Error) {
|
|
119
|
-
throw getClient
|
|
120
|
-
}
|
|
121
|
-
client = getClient()
|
|
122
|
-
}, 20_000)
|
|
123
|
-
|
|
124
|
-
afterAll(async () => {
|
|
125
|
-
await cleanupTestSessions({
|
|
126
|
-
projectDirectory: directories.projectDirectory,
|
|
127
|
-
testStartTime,
|
|
128
|
-
})
|
|
129
|
-
await stopOpencodeServer()
|
|
130
|
-
}, 5_000)
|
|
131
|
-
|
|
132
|
-
test('tool-call step has finish="tool-calls", follow-up has finish="stop"', async () => {
|
|
133
|
-
const session = await client.session.create({
|
|
134
|
-
directory: directories.projectDirectory,
|
|
135
|
-
title: 'finish-field-test',
|
|
136
|
-
})
|
|
137
|
-
const sessionID = session.data!.id
|
|
138
|
-
|
|
139
|
-
await client.session.promptAsync({
|
|
140
|
-
sessionID,
|
|
141
|
-
directory: directories.projectDirectory,
|
|
142
|
-
parts: [{ type: 'text', text: 'FINISH_FIELD_TOOLCALL' }],
|
|
143
|
-
})
|
|
144
|
-
|
|
145
|
-
// Poll until we have 2 completed assistant messages (tool-call + follow-up)
|
|
146
|
-
const maxWait = 8_000
|
|
147
|
-
const pollStart = Date.now()
|
|
148
|
-
let completedAssistants: Array<{ finish: string | null; partTypes: string[] }> = []
|
|
149
|
-
|
|
150
|
-
while (Date.now() - pollStart < maxWait) {
|
|
151
|
-
const msgs = await client.session.messages({
|
|
152
|
-
sessionID,
|
|
153
|
-
directory: directories.projectDirectory,
|
|
154
|
-
})
|
|
155
|
-
completedAssistants = (msgs.data || [])
|
|
156
|
-
.filter((m) => {
|
|
157
|
-
return m.info.role === 'assistant' && m.info.time.completed
|
|
158
|
-
})
|
|
159
|
-
.map((m) => {
|
|
160
|
-
return {
|
|
161
|
-
finish: (m.info as Record<string, unknown>).finish as string | null ?? null,
|
|
162
|
-
partTypes: m.parts.map((p) => { return p.type }),
|
|
163
|
-
}
|
|
164
|
-
})
|
|
165
|
-
if (completedAssistants.length >= 2) {
|
|
166
|
-
break
|
|
167
|
-
}
|
|
168
|
-
await new Promise((resolve) => { setTimeout(resolve, 100) })
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
// Snapshot completed assistant messages — finish should NOT be null
|
|
172
|
-
expect(completedAssistants).toMatchInlineSnapshot(`
|
|
173
|
-
[
|
|
174
|
-
{
|
|
175
|
-
"finish": "tool-calls",
|
|
176
|
-
"partTypes": [
|
|
177
|
-
"step-start",
|
|
178
|
-
"text",
|
|
179
|
-
"tool",
|
|
180
|
-
"step-finish",
|
|
181
|
-
],
|
|
182
|
-
},
|
|
183
|
-
{
|
|
184
|
-
"finish": "stop",
|
|
185
|
-
"partTypes": [
|
|
186
|
-
"step-start",
|
|
187
|
-
"text",
|
|
188
|
-
"step-finish",
|
|
189
|
-
],
|
|
190
|
-
},
|
|
191
|
-
]
|
|
192
|
-
`)
|
|
193
|
-
|
|
194
|
-
const finishes = completedAssistants.map((m) => { return m.finish })
|
|
195
|
-
expect(finishes).toEqual(['tool-calls', 'stop'])
|
|
196
|
-
}, 5_000)
|