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.
Files changed (302) hide show
  1. package/dist/agent-model.e2e.test.js +250 -164
  2. package/dist/anthropic-account-identity.js +9 -1
  3. package/dist/anthropic-auth-plugin.js +10 -13
  4. package/dist/anthropic-auth-plugin.test.js +17 -0
  5. package/dist/anthropic-auth-state.js +1 -0
  6. package/dist/bash-tool-schema-plugin.js +32 -0
  7. package/dist/bash-tool-schema-plugin.test.js +49 -0
  8. package/dist/bin.js +73 -19
  9. package/dist/cli-commands/maintenance.js +46 -7
  10. package/dist/cli-commands/project.js +11 -27
  11. package/dist/cli-commands/send.js +30 -1
  12. package/dist/cli-commands/session.js +322 -90
  13. package/dist/cli-commands/task.js +34 -15
  14. package/dist/cli-parsing.test.js +83 -4
  15. package/dist/cli-runner.js +44 -8
  16. package/dist/cli-runner.test.js +27 -0
  17. package/dist/cli-send-thread.e2e.test.js +9 -9
  18. package/dist/cli.js +32 -0
  19. package/dist/commands/abort.js +5 -1
  20. package/dist/commands/action-buttons.js +2 -1
  21. package/dist/commands/add-project.js +4 -7
  22. package/dist/commands/agent.js +164 -32
  23. package/dist/commands/ask-question.js +23 -33
  24. package/dist/commands/ask-question.test.js +43 -1
  25. package/dist/commands/btw.js +28 -6
  26. package/dist/commands/file-upload.js +2 -1
  27. package/dist/commands/fork-subagent.js +2 -4
  28. package/dist/commands/fork.js +4 -4
  29. package/dist/commands/last-sessions.js +17 -10
  30. package/dist/commands/login.js +6 -0
  31. package/dist/commands/merge-worktree.js +47 -14
  32. package/dist/commands/model-variant.js +8 -1
  33. package/dist/commands/model.js +16 -9
  34. package/dist/commands/multioauth.js +1 -0
  35. package/dist/commands/new-worktree.js +170 -102
  36. package/dist/commands/queue.js +3 -2
  37. package/dist/commands/resume.js +4 -4
  38. package/dist/commands/session.js +78 -63
  39. package/dist/commands/user-command.js +6 -8
  40. package/dist/commands/worktrees.js +5 -13
  41. package/dist/config.js +16 -1
  42. package/dist/cpu-profiler.js +211 -0
  43. package/dist/cpu-profiler.test.js +160 -0
  44. package/dist/database.js +152 -3
  45. package/dist/db.js +33 -0
  46. package/dist/db.test.js +267 -1
  47. package/dist/default-channel-provisioning.test.js +148 -0
  48. package/dist/discord-bot.js +517 -488
  49. package/dist/discord-command-registration.js +25 -10
  50. package/dist/discord-command-registration.test.js +17 -0
  51. package/dist/discord-utils.js +97 -5
  52. package/dist/discord-utils.test.js +151 -1
  53. package/dist/errors.js +10 -0
  54. package/dist/exec-async.js +17 -5
  55. package/dist/external-directory-permissions.e2e.test.js +11 -7
  56. package/dist/external-directory-project-deny.e2e.test.js +5 -3
  57. package/dist/external-opencode-sync.js +94 -17
  58. package/dist/external-opencode-sync.test.js +206 -0
  59. package/dist/extract-sdk-error-message.test.js +13 -0
  60. package/dist/file-edit-log.js +197 -0
  61. package/dist/file-edit-log.test.js +258 -0
  62. package/dist/format-tables.js +52 -0
  63. package/dist/format-tables.test.js +93 -0
  64. package/dist/gateway-proxy-reconnect.e2e.test.js +28 -8
  65. package/dist/gateway-proxy.e2e.test.js +18 -18
  66. package/dist/git-worktree-core.js +182 -17
  67. package/dist/interaction-handler.js +416 -398
  68. package/dist/ipc-tools-plugin.js +68 -3
  69. package/dist/kimaki-opencode-plugin-loading.e2e.test.js +122 -58
  70. package/dist/kimaki-opencode-plugin.js +9 -3
  71. package/dist/kimaki-workspace-adaptor.js +33 -10
  72. package/dist/logger.js +11 -1
  73. package/dist/markdown.js +3 -1
  74. package/dist/markdown.test.js +11 -3
  75. package/dist/message-formatting.js +181 -51
  76. package/dist/message-formatting.test.js +529 -22
  77. package/dist/message-preprocessing.js +105 -1
  78. package/dist/oauth-rotation-shared.js +10 -0
  79. package/dist/openai-auth-plugin.js +1 -0
  80. package/dist/openai-auth-state.js +1 -0
  81. package/dist/opencode-command.js +28 -0
  82. package/dist/opencode-command.test.js +109 -1
  83. package/dist/opencode-interrupt-plugin.js +4 -0
  84. package/dist/opencode-interrupt-plugin.test.js +24 -0
  85. package/dist/opencode.js +82 -15
  86. package/dist/project-add.test.js +62 -0
  87. package/dist/question-abort-clears-dropdown.e2e.test.js +89 -0
  88. package/dist/question-abort-retry-text-before-dropdown.e2e.test.js +150 -0
  89. package/dist/question-text-before-dropdown.e2e.test.js +52 -0
  90. package/dist/queue-advanced-abort.e2e.test.js +8 -10
  91. package/dist/queue-advanced-action-buttons.e2e.test.js +8 -8
  92. package/dist/queue-advanced-e2e-setup.js +78 -5
  93. package/dist/queue-advanced-footer.e2e.test.js +60 -269
  94. package/dist/queue-advanced-model-switch.e2e.test.js +9 -11
  95. package/dist/queue-advanced-permissions-typing.e2e.test.js +15 -15
  96. package/dist/queue-advanced-question.e2e.test.js +32 -27
  97. package/dist/queue-advanced-typing-interrupt.e2e.test.js +10 -12
  98. package/dist/queue-advanced-typing.e2e.test.js +7 -9
  99. package/dist/queue-delete-message.e2e.test.js +3 -3
  100. package/dist/queue-drain-after-interactive-ui.e2e.test.js +5 -5
  101. package/dist/queue-interrupt-drain.e2e.test.js +122 -12
  102. package/dist/queue-question-select-drain.e2e.test.js +34 -132
  103. package/dist/runtime-lifecycle.e2e.test.js +21 -51
  104. package/dist/schema.js +35 -1
  105. package/dist/session-handler/event-stream-state.js +391 -50
  106. package/dist/session-handler/event-stream-state.test.js +736 -8
  107. package/dist/session-handler/model-utils.js +172 -5
  108. package/dist/session-handler/model-utils.test.js +445 -0
  109. package/dist/session-handler/opencode-session-event-log.js +30 -1
  110. package/dist/session-handler/thread-ingress.test.js +41 -0
  111. package/dist/session-handler/thread-runtime-state.js +2 -0
  112. package/dist/session-handler/thread-session-runtime-summary.test.js +121 -0
  113. package/dist/session-handler/thread-session-runtime.js +574 -299
  114. package/dist/session-search.js +123 -4
  115. package/dist/session-search.test.js +248 -1
  116. package/dist/session-sleep.test.js +224 -0
  117. package/dist/session-title-rename.test.js +37 -39
  118. package/dist/sleep.e2e.test.js +494 -0
  119. package/dist/startup-time.e2e.test.js +2 -1
  120. package/dist/store.js +3 -0
  121. package/dist/system-message.js +102 -16
  122. package/dist/system-message.test.js +192 -15
  123. package/dist/task-runner.js +96 -5
  124. package/dist/task-schedule.js +87 -1
  125. package/dist/task-schedule.test.js +149 -1
  126. package/dist/test-utils.js +17 -9
  127. package/dist/thinking-utils.js +10 -0
  128. package/dist/thinking-utils.test.js +35 -0
  129. package/dist/thread-message-queue.e2e.test.js +65 -213
  130. package/dist/undo-redo.e2e.test.js +17 -9
  131. package/dist/voice-attachment.js +23 -1
  132. package/dist/voice-handler.js +21 -4
  133. package/dist/voice-message.e2e.test.js +144 -30
  134. package/dist/voice-routing-input.e2e.test.js +195 -0
  135. package/dist/voice-routing-question.e2e.test.js +125 -0
  136. package/dist/voice.js +28 -8
  137. package/dist/voice.test.js +96 -30
  138. package/dist/worktree-clone-isolation.e2e.test.js +188 -0
  139. package/dist/worktree-lifecycle.e2e.test.js +55 -36
  140. package/dist/worktrees.js +226 -443
  141. package/dist/worktrees.test.js +435 -58
  142. package/dist/xai-auth-plugin.js +1 -0
  143. package/dist/xai-auth-state.js +1 -0
  144. package/package.json +8 -6
  145. package/skills/bump-holocron-consumers/SKILL.md +137 -0
  146. package/skills/critique/SKILL.md +4 -4
  147. package/skills/errore/SKILL.md +60 -218
  148. package/skills/goke/SKILL.md +150 -10
  149. package/skills/holocron/SKILL.md +154 -17
  150. package/skills/npm-package/SKILL.md +55 -35
  151. package/skills/profano/SKILL.md +17 -2
  152. package/skills/spiceflow/SKILL.md +39 -3
  153. package/skills/strada/SKILL.md +39 -0
  154. package/skills/zele/SKILL.md +19 -0
  155. package/src/agent-model.e2e.test.ts +287 -186
  156. package/src/anthropic-account-identity.ts +20 -1
  157. package/src/anthropic-auth-plugin.test.ts +19 -0
  158. package/src/anthropic-auth-plugin.ts +12 -16
  159. package/src/anthropic-auth-state.ts +1 -0
  160. package/src/bash-tool-schema-plugin.test.ts +55 -0
  161. package/src/bash-tool-schema-plugin.ts +46 -0
  162. package/src/bin.ts +78 -26
  163. package/src/cli-commands/maintenance.ts +59 -13
  164. package/src/cli-commands/project.ts +14 -35
  165. package/src/cli-commands/send.ts +31 -1
  166. package/src/cli-commands/session.ts +374 -114
  167. package/src/cli-commands/task.ts +34 -15
  168. package/src/cli-parsing.test.ts +103 -4
  169. package/src/cli-runner.test.ts +49 -0
  170. package/src/cli-runner.ts +44 -7
  171. package/src/cli-send-thread.e2e.test.ts +9 -11
  172. package/src/cli.ts +53 -0
  173. package/src/commands/abort.ts +6 -1
  174. package/src/commands/action-buttons.ts +2 -1
  175. package/src/commands/add-project.ts +4 -8
  176. package/src/commands/agent.ts +205 -29
  177. package/src/commands/ask-question.test.ts +48 -1
  178. package/src/commands/ask-question.ts +23 -33
  179. package/src/commands/btw.ts +40 -5
  180. package/src/commands/file-upload.ts +2 -1
  181. package/src/commands/fork-subagent.ts +2 -3
  182. package/src/commands/fork.ts +4 -3
  183. package/src/commands/last-sessions.ts +10 -4
  184. package/src/commands/login.ts +6 -0
  185. package/src/commands/merge-worktree.ts +54 -16
  186. package/src/commands/model-variant.ts +9 -1
  187. package/src/commands/model.ts +20 -9
  188. package/src/commands/multioauth.ts +1 -0
  189. package/src/commands/new-worktree.ts +138 -55
  190. package/src/commands/queue.ts +3 -2
  191. package/src/commands/resume.ts +4 -3
  192. package/src/commands/session.ts +94 -70
  193. package/src/commands/user-command.ts +6 -6
  194. package/src/commands/worktrees.ts +5 -12
  195. package/src/config.ts +18 -1
  196. package/src/cpu-profiler.test.ts +183 -0
  197. package/src/cpu-profiler.ts +293 -0
  198. package/src/database.ts +235 -3
  199. package/src/db.test.ts +293 -0
  200. package/src/db.ts +38 -0
  201. package/src/default-channel-provisioning.test.ts +155 -0
  202. package/src/discord-bot.ts +78 -36
  203. package/src/discord-command-registration.test.ts +20 -0
  204. package/src/discord-command-registration.ts +42 -12
  205. package/src/discord-utils.test.ts +176 -0
  206. package/src/discord-utils.ts +152 -7
  207. package/src/errors.ts +12 -0
  208. package/src/exec-async.ts +20 -11
  209. package/src/external-directory-permissions.e2e.test.ts +11 -7
  210. package/src/external-directory-project-deny.e2e.test.ts +5 -3
  211. package/src/external-opencode-sync.test.ts +248 -0
  212. package/src/external-opencode-sync.ts +122 -19
  213. package/src/extract-sdk-error-message.test.ts +18 -0
  214. package/src/file-edit-log.test.ts +287 -0
  215. package/src/file-edit-log.ts +283 -0
  216. package/src/format-tables.test.ts +101 -0
  217. package/src/format-tables.ts +75 -0
  218. package/src/gateway-proxy-reconnect.e2e.test.ts +38 -8
  219. package/src/gateway-proxy.e2e.test.ts +18 -17
  220. package/src/git-worktree-core.ts +247 -20
  221. package/src/interaction-handler.ts +36 -13
  222. package/src/ipc-tools-plugin.ts +82 -5
  223. package/src/kimaki-opencode-plugin-loading.e2e.test.ts +188 -97
  224. package/src/kimaki-opencode-plugin.ts +9 -3
  225. package/src/kimaki-workspace-adaptor.ts +40 -11
  226. package/src/logger.ts +14 -4
  227. package/src/markdown.test.ts +13 -3
  228. package/src/markdown.ts +4 -1
  229. package/src/message-formatting.test.ts +672 -25
  230. package/src/message-formatting.ts +242 -53
  231. package/src/message-preprocessing.ts +116 -1
  232. package/src/oauth-rotation-shared.ts +10 -0
  233. package/src/openai-auth-plugin.ts +1 -0
  234. package/src/openai-auth-state.ts +1 -0
  235. package/src/opencode-command.test.ts +140 -0
  236. package/src/opencode-command.ts +38 -0
  237. package/src/opencode-interrupt-plugin.test.ts +32 -0
  238. package/src/opencode-interrupt-plugin.ts +3 -0
  239. package/src/opencode.ts +115 -15
  240. package/src/project-add.test.ts +80 -0
  241. package/src/question-abort-clears-dropdown.e2e.test.ts +109 -0
  242. package/src/question-abort-retry-text-before-dropdown.e2e.test.ts +172 -0
  243. package/src/question-text-before-dropdown.e2e.test.ts +67 -0
  244. package/src/queue-advanced-abort.e2e.test.ts +8 -9
  245. package/src/queue-advanced-action-buttons.e2e.test.ts +8 -8
  246. package/src/queue-advanced-e2e-setup.ts +83 -4
  247. package/src/queue-advanced-footer.e2e.test.ts +67 -315
  248. package/src/queue-advanced-model-switch.e2e.test.ts +9 -10
  249. package/src/queue-advanced-permissions-typing.e2e.test.ts +15 -15
  250. package/src/queue-advanced-question.e2e.test.ts +36 -30
  251. package/src/queue-advanced-typing-interrupt.e2e.test.ts +11 -11
  252. package/src/queue-advanced-typing.e2e.test.ts +8 -8
  253. package/src/queue-delete-message.e2e.test.ts +3 -3
  254. package/src/queue-drain-after-interactive-ui.e2e.test.ts +5 -5
  255. package/src/queue-interrupt-drain.e2e.test.ts +133 -12
  256. package/src/queue-question-select-drain.e2e.test.ts +36 -161
  257. package/src/runtime-lifecycle.e2e.test.ts +21 -54
  258. package/src/schema.sql +15 -2
  259. package/src/schema.ts +37 -1
  260. package/src/session-handler/event-stream-state.test.ts +790 -7
  261. package/src/session-handler/event-stream-state.ts +568 -53
  262. package/src/session-handler/model-utils.test.ts +537 -0
  263. package/src/session-handler/model-utils.ts +281 -5
  264. package/src/session-handler/opencode-session-event-log.ts +31 -1
  265. package/src/session-handler/thread-ingress.test.ts +54 -0
  266. package/src/session-handler/thread-runtime-state.ts +3 -0
  267. package/src/session-handler/thread-session-runtime-summary.test.ts +169 -0
  268. package/src/session-handler/thread-session-runtime.ts +712 -358
  269. package/src/session-search.test.ts +281 -0
  270. package/src/session-search.ts +241 -7
  271. package/src/session-sleep.test.ts +294 -0
  272. package/src/session-title-rename.test.ts +54 -48
  273. package/src/sleep.e2e.test.ts +565 -0
  274. package/src/startup-time.e2e.test.ts +5 -1
  275. package/src/store.ts +22 -0
  276. package/src/system-message.test.ts +226 -15
  277. package/src/system-message.ts +130 -16
  278. package/src/task-runner.ts +138 -3
  279. package/src/task-schedule.test.ts +170 -0
  280. package/src/task-schedule.ts +141 -1
  281. package/src/test-utils.ts +21 -10
  282. package/src/thinking-utils.test.ts +46 -0
  283. package/src/thinking-utils.ts +21 -0
  284. package/src/thread-message-queue.e2e.test.ts +65 -245
  285. package/src/undo-redo.e2e.test.ts +17 -9
  286. package/src/voice-attachment.ts +29 -1
  287. package/src/voice-handler.ts +23 -4
  288. package/src/voice-message.e2e.test.ts +149 -29
  289. package/src/voice-routing-input.e2e.test.ts +203 -0
  290. package/src/voice-routing-question.e2e.test.ts +131 -0
  291. package/src/voice.test.ts +107 -29
  292. package/src/voice.ts +34 -6
  293. package/src/worktree-clone-isolation.e2e.test.ts +213 -0
  294. package/src/worktree-lifecycle.e2e.test.ts +55 -35
  295. package/src/worktrees.test.ts +505 -64
  296. package/src/worktrees.ts +257 -603
  297. package/src/xai-auth-plugin.ts +1 -0
  298. package/src/xai-auth-state.ts +1 -0
  299. package/dist/message-finish-field.e2e.test.js +0 -169
  300. package/dist/subagent-rate-limit-plugin.js +0 -184
  301. package/src/message-finish-field.e2e.test.ts +0 -196
  302. package/src/subagent-rate-limit-plugin.ts +0 -228
@@ -1,3 +1,4 @@
1
+ // LEGACY rotation, superseded by @subrouter/opencode. See oauth-rotation-shared.ts.
1
2
  /**
2
3
  * xAI OAuth rotation plugin for OpenCode.
3
4
  *
@@ -1,3 +1,4 @@
1
+ // LEGACY rotation, superseded by @subrouter/opencode. See oauth-rotation-shared.ts.
1
2
  /**
2
3
  * xAI OAuth account store and rotation.
3
4
  * Mirrors openai-auth-state.ts but for xAI/Grok OAuth accounts.
@@ -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)