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
@@ -5,14 +5,17 @@
5
5
 
6
6
  import { describe, test, expect, afterEach } from 'vitest'
7
7
  import { setupQueueAdvancedSuite, TEST_USER_ID } from './queue-advanced-e2e-setup.js'
8
- import { waitForBotMessageContaining, waitForFooterMessage } from './test-utils.js'
8
+ import {
9
+ waitForBotMessageContaining,
10
+ waitForBotReplyAfterUserMessage,
11
+ waitForFooterMessage,
12
+ } from './test-utils.js'
9
13
  import { store, type DeterministicTranscriptionConfig } from './store.js'
10
14
  import { getOpencodeClient } from './opencode.js'
11
15
  import { getThreadSession } from './database.js'
12
16
  import type { Message, Part } from '@opencode-ai/sdk/v2'
13
17
 
14
18
  const TEXT_CHANNEL_ID = '200000000000001007'
15
- const VOICE_CHANNEL_ID = '200000000000001017'
16
19
 
17
20
  function setDeterministicTranscription(config: DeterministicTranscriptionConfig | null) {
18
21
  store.setState({
@@ -158,37 +161,44 @@ describe('queue advanced: question tool answer', () => {
158
161
  // 1. Dismiss the pending question (cleanup context)
159
162
  // 2. Abort the blocked session so OpenCode unblocks
160
163
  // 3. Enqueue the message as a normal user prompt (not consumed as answer)
164
+ const answer = 'Reply with exactly: question-text-answer-done'
161
165
  await th.user(TEST_USER_ID).sendMessage({
162
- content: 'my text answer',
166
+ content: answer,
163
167
  })
164
168
 
165
- // Give time for question cleanup to propagate
166
- await new Promise((r) => {
167
- setTimeout(r, 1_000)
169
+ await waitForBotReplyAfterUserMessage({
170
+ discord: ctx.discord,
171
+ threadId: thread.id,
172
+ userId: TEST_USER_ID,
173
+ userMessageIncludes: answer,
174
+ timeout: 8_000,
175
+ })
176
+ await waitForBotMessageContaining({
177
+ discord: ctx.discord,
178
+ threadId: thread.id,
179
+ userId: TEST_USER_ID,
180
+ text: 'ok',
181
+ afterUserMessageIncludes: answer,
182
+ timeout: 8_000,
183
+ })
184
+ await waitForFooterMessage({
185
+ discord: ctx.discord,
186
+ threadId: thread.id,
187
+ timeout: 8_000,
188
+ afterMessageIncludes: 'ok',
189
+ afterAuthorId: ctx.discord.botUserId,
168
190
  })
169
191
 
170
192
  const timeline = await th.text({ showInteractions: true })
171
193
 
172
194
  // The user's text answer must appear in Discord
173
- expect(timeline).toContain('my text answer')
195
+ expect(timeline).toContain(answer)
196
+ expect(timeline).toContain('ok')
174
197
  // The original question must have appeared
175
198
  expect(timeline).toContain('Which option do you prefer?')
176
199
  // The user's marker message triggered the question
177
200
  expect(timeline).toContain('QUESTION_TEXT_ANSWER_MARKER')
178
201
  }, 20_000)
179
- })
180
-
181
- describe('queue advanced: voice message during pending question', () => {
182
- const ctx = setupQueueAdvancedSuite({
183
- channelId: VOICE_CHANNEL_ID,
184
- channelName: 'qa-question-voice-e2e',
185
- dirName: 'qa-question-voice-e2e',
186
- username: 'queue-question-tester',
187
- })
188
-
189
- afterEach(() => {
190
- setDeterministicTranscription(null)
191
- })
192
202
 
193
203
  test('voice message during pending question dismisses question and transcribes normally', async () => {
194
204
  // This is the exact bug scenario: user sends a voice message while a
@@ -196,14 +206,15 @@ describe('queue advanced: voice message during pending question', () => {
196
206
  // (audio is in attachments, transcription happens later). The old code
197
207
  // passed "" as the question answer and consumed the message — the voice
198
208
  // content was completely lost.
199
- await ctx.discord.channel(VOICE_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
200
- content: 'QUESTION_TEXT_ANSWER_MARKER',
209
+ const marker = 'QUESTION_TEXT_ANSWER_MARKER voice'
210
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
211
+ content: marker,
201
212
  })
202
213
 
203
- const thread = await ctx.discord.channel(VOICE_CHANNEL_ID).waitForThread({
214
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
204
215
  timeout: 8_000,
205
216
  predicate: (t) => {
206
- return t.name === 'QUESTION_TEXT_ANSWER_MARKER'
217
+ return t.name === marker
207
218
  },
208
219
  })
209
220
 
@@ -230,11 +241,6 @@ describe('queue advanced: voice message during pending question', () => {
230
241
  content: 'VOICE_TEXT_CONTENT_SHOULD_NOT_REACH_MODEL',
231
242
  })
232
243
 
233
- // Give time for question cleanup to propagate
234
- await new Promise((r) => {
235
- setTimeout(r, 1_000)
236
- })
237
-
238
244
  // Voice content should be transcribed and appear as the next user message,
239
245
  // processed after the model responds to the empty question answer.
240
246
  await waitForBotMessageContaining({
@@ -308,7 +314,7 @@ describe('queue advanced: voice message during pending question', () => {
308
314
  expect(timeline).toContain('VOICE_TEXT_CONTENT_SHOULD_NOT_REACH_MODEL')
309
315
  expect(timeline).toContain('🎤 Transcribing voice message...')
310
316
  expect(timeline).toContain('📝 **Transcribed message:** I want option Alpha please')
311
- expect(timeline).toContain('⬥ ok')
317
+ expect(timeline).toContain('ok')
312
318
 
313
319
  // Voice content must be present as a real transcribed message, not lost
314
320
  expect(timeline).toContain('I want option Alpha please')
@@ -7,6 +7,8 @@ import {
7
7
  TEST_USER_ID,
8
8
  } from './queue-advanced-e2e-setup.js'
9
9
  import {
10
+ getMessageVisibleText,
11
+ isFooterMessage,
10
12
  waitForBotMessageContaining,
11
13
  waitForFooterMessage,
12
14
  } from './test-utils.js'
@@ -91,33 +93,31 @@ e2eTest('queue advanced: typing interrupt', () => {
91
93
  if (index <= finalUserIndex) {
92
94
  return false
93
95
  }
94
- return message.author.id === ctx.discord.botUserId && message.content.includes('ok')
96
+ return message.author.id === ctx.discord.botUserId && getMessageVisibleText(message).includes('ok')
95
97
  })
96
98
  const finalFooterIndex = messages.findIndex((message, index) => {
97
99
  if (index <= finalReplyIndex) {
98
100
  return false
99
101
  }
100
- return message.author.id === ctx.discord.botUserId
101
- && message.content.startsWith('*')
102
- && message.content.includes('⋅')
102
+ return isFooterMessage({ message, botUserId: ctx.discord.botUserId })
103
103
  })
104
104
 
105
105
  expect(await th.text()).toMatchInlineSnapshot(`
106
106
  "--- from: user (queue-advanced-tester)
107
107
  Reply with exactly: typing-stop-interrupt-setup
108
108
  --- from: assistant (TestBot)
109
- *using deterministic-provider/deterministic-v2*
110
- ⬥ ok
111
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
109
+ > *using deterministic-provider/deterministic-v2*
110
+ > ok
111
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>
112
112
  --- from: user (queue-advanced-tester)
113
113
  PLUGIN_TIMEOUT_SLEEP_MARKER
114
114
  --- from: assistant (TestBot)
115
- ⬥ starting sleep 100
115
+ > starting sleep 100
116
116
  --- from: user (queue-advanced-tester)
117
117
  Reply with exactly: typing-stop-interrupt-final
118
118
  --- from: assistant (TestBot)
119
- ⬥ ok
120
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
119
+ > ok
120
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>"
121
121
  `)
122
122
 
123
123
  const timeline = await th.text({ showTyping: true })
@@ -129,7 +129,7 @@ e2eTest('queue advanced: typing interrupt', () => {
129
129
  const finalPromptPosition = timeline.indexOf(
130
130
  'Reply with exactly: typing-stop-interrupt-final',
131
131
  )
132
- const finalReplyPosition = timeline.indexOf('--- from: assistant (TestBot)\n⬥ ok', finalPromptPosition)
132
+ const finalReplyPosition = timeline.indexOf('--- from: assistant (TestBot)\n> ok', finalPromptPosition)
133
133
  const lastFooterPosition = timeline.lastIndexOf('*project ⋅')
134
134
  expect(finalPromptPosition).toBeGreaterThanOrEqual(0)
135
135
  expect(finalReplyPosition).toBeGreaterThan(finalPromptPosition)
@@ -7,6 +7,8 @@ import {
7
7
  TEST_USER_ID,
8
8
  } from './queue-advanced-e2e-setup.js'
9
9
  import {
10
+ getMessageVisibleText,
11
+ isFooterMessage,
10
12
  waitForBotMessageContaining,
11
13
  waitForFooterMessage,
12
14
  } from './test-utils.js'
@@ -60,20 +62,18 @@ e2eTest('queue advanced: typing lifecycle', () => {
60
62
  })
61
63
 
62
64
  const replyIndex = messages.findIndex((message) => {
63
- return message.author.id === ctx.discord.botUserId && message.content.includes('ok')
65
+ return message.author.id === ctx.discord.botUserId && getMessageVisibleText(message).includes('ok')
64
66
  })
65
67
  const footerIndex = messages.findIndex((message, index) => {
66
68
  if (index <= replyIndex) {
67
69
  return false
68
70
  }
69
- return message.author.id === ctx.discord.botUserId
70
- && message.content.startsWith('*')
71
- && message.content.includes('⋅')
71
+ return isFooterMessage({ message, botUserId: ctx.discord.botUserId })
72
72
  })
73
73
 
74
74
  const timeline = await th.text({ showTyping: true })
75
75
  expect(timeline).toContain('Reply with exactly: typing-stop-normal')
76
- expect(timeline).toContain('⬥ ok')
76
+ expect(timeline).toContain('ok')
77
77
  expect(timeline).toContain('*project ⋅ main ⋅')
78
78
  const typingCount = (timeline.match(/\[bot typing\]/g) || []).length
79
79
  expect(typingCount).toBeGreaterThanOrEqual(1)
@@ -145,7 +145,7 @@ e2eTest('queue advanced: typing lifecycle', () => {
145
145
  return false
146
146
  }
147
147
  return message.author.id === ctx.discord.botUserId
148
- && message.content.includes('repulse-first')
148
+ && getMessageVisibleText(message).includes('repulse-first')
149
149
  })
150
150
  if (!firstReply) {
151
151
  throw new Error('Expected first bot reply after TYPING_REPULSE_MARKER')
@@ -173,7 +173,7 @@ e2eTest('queue advanced: typing lifecycle', () => {
173
173
 
174
174
  const timeline = await th.text({ showTyping: true })
175
175
  expect(timeline).toContain('TYPING_REPULSE_MARKER')
176
- expect(timeline).toContain('⬥ repulse-first')
176
+ expect(timeline).toContain('repulse-first')
177
177
  const typingCount = (timeline.match(/\[bot typing\]/g) || []).length
178
178
  expect(typingCount).toBeGreaterThanOrEqual(2)
179
179
 
@@ -186,7 +186,7 @@ e2eTest('queue advanced: typing lifecycle', () => {
186
186
  return false
187
187
  }
188
188
  return message.author.id === ctx.discord.botUserId
189
- && message.content.includes('repulse-first')
189
+ && getMessageVisibleText(message).includes('repulse-first')
190
190
  })
191
191
 
192
192
  expect(followupUserIndex).toBeGreaterThanOrEqual(0)
@@ -86,11 +86,11 @@ e2eTest('queue delete message', () => {
86
86
  "--- from: user (queue-delete-tester)
87
87
  SLOW_BUSY_MARKER Reply with exactly: delete-queue-setup
88
88
  --- from: assistant (TestBot)
89
- *using deterministic-provider/deterministic-v2*
89
+ > *using deterministic-provider/deterministic-v2*
90
90
  Queued at position 1. Edit or delete your message to update the queue
91
91
  ⬦ **queue-delete-tester** removed message from queue
92
- ⬥ slow-busy-reply
93
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
92
+ > slow-busy-reply
93
+ > *project ⋅ main ⋅ 2s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>"
94
94
  `)
95
95
  const finalText = await th.text()
96
96
  expect(finalText).not.toContain(
@@ -137,14 +137,14 @@ describe('queue drain with pending interactive UI', () => {
137
137
  "--- from: user (drain-ui-tester)
138
138
  Reply with exactly: drain-button-setup
139
139
  --- from: assistant (TestBot)
140
- *using deterministic-provider/deterministic-v2*
141
- ⬥ ok
142
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
140
+ > *using deterministic-provider/deterministic-v2*
141
+ > ok
142
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>
143
143
  **Action Required**
144
144
  [user interaction]
145
145
  » **drain-ui-tester:** Reply with exactly: post-button-drain
146
- ⬥ ok
147
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
146
+ > ok
147
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>"
148
148
  `)
149
149
  },
150
150
  20_000,
@@ -7,7 +7,7 @@
7
7
  // 2. User queues a message via /queue (enters kimaki local queue)
8
8
  // 3. User sends a normal message (interrupt)
9
9
  // 4. Session aborts the slow task, processes the interrupt message immediately
10
- // 5. Interrupt response appears in Discord with a ⬥ ok reply
10
+ // 5. Interrupt response appears in Discord with a ok reply
11
11
  // 6. When interrupt response completes, the queued message drains and runs
12
12
  //
13
13
  // Uses opencode-deterministic-provider (no real LLM calls).
@@ -23,6 +23,9 @@ import {
23
23
  waitForBotMessageContaining,
24
24
  waitForMessageById,
25
25
  } from './test-utils.js'
26
+ import { getThreadSession } from './database.js'
27
+ import { archiveThread } from './discord-utils.js'
28
+ import { getOpencodeClient } from './opencode.js'
26
29
 
27
30
  const TEXT_CHANNEL_ID = '200000000000001099'
28
31
 
@@ -36,6 +39,124 @@ e2eTest('queue + interrupt drain ordering', () => {
36
39
  username: 'interrupt-tester',
37
40
  })
38
41
 
42
+ test(
43
+ 'archiving keeps the active run alive so queued messages drain',
44
+ async () => {
45
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
46
+ content: 'Reply with exactly: setup-archive-drain',
47
+ })
48
+
49
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
50
+ timeout: 4_000,
51
+ predicate: (candidate) => {
52
+ return candidate.name === 'Reply with exactly: setup-archive-drain'
53
+ },
54
+ })
55
+ const th = ctx.discord.thread(thread.id)
56
+ await th.waitForBotReply({ timeout: 4_000 })
57
+ await waitForFooterMessage({
58
+ discord: ctx.discord,
59
+ threadId: thread.id,
60
+ timeout: 4_000,
61
+ })
62
+
63
+ await th.user(TEST_USER_ID).sendMessage({
64
+ content: 'PLUGIN_TIMEOUT_SLEEP_MARKER archive queue drain',
65
+ })
66
+ await waitForBotMessageContaining({
67
+ discord: ctx.discord,
68
+ threadId: thread.id,
69
+ userId: TEST_USER_ID,
70
+ text: 'starting sleep',
71
+ afterUserMessageIncludes: 'PLUGIN_TIMEOUT_SLEEP_MARKER archive queue drain',
72
+ timeout: 4_000,
73
+ })
74
+
75
+ const { id: queueInteractionId } = await th.user(TEST_USER_ID)
76
+ .runSlashCommand({
77
+ name: 'queue',
78
+ options: [{
79
+ name: 'message',
80
+ type: 3,
81
+ value: 'Reply with exactly: archived-queue-survives',
82
+ }],
83
+ })
84
+ const queueAck = await th.waitForInteractionAck({
85
+ interactionId: queueInteractionId,
86
+ timeout: 4_000,
87
+ })
88
+ if (!queueAck.messageId) {
89
+ throw new Error('Expected /queue response message id')
90
+ }
91
+ const queueStatusMessage = await waitForMessageById({
92
+ discord: ctx.discord,
93
+ threadId: thread.id,
94
+ messageId: queueAck.messageId,
95
+ timeout: 4_000,
96
+ })
97
+ expect(queueStatusMessage.content).toContain('Queued message')
98
+
99
+ const sessionId = await getThreadSession(thread.id)
100
+ if (!sessionId) throw new Error('Expected mapped OpenCode session')
101
+ const client = getOpencodeClient(ctx.directories.projectDirectory)
102
+ if (!client) throw new Error('Expected initialized OpenCode client')
103
+ await archiveThread({
104
+ rest: ctx.botClient.rest,
105
+ threadId: thread.id,
106
+ parentChannelId: TEXT_CHANNEL_ID,
107
+ sessionId,
108
+ client,
109
+ })
110
+
111
+ const statusResponse = await client.session.status({
112
+ directory: ctx.directories.projectDirectory,
113
+ })
114
+ expect(statusResponse.data?.[sessionId]?.type).toBe('busy')
115
+
116
+ await th.user(TEST_USER_ID).sendMessage({
117
+ content: 'Reply with exactly: continue-after-archive',
118
+ })
119
+ await waitForFooterMessage({
120
+ discord: ctx.discord,
121
+ threadId: thread.id,
122
+ timeout: 12_000,
123
+ afterMessageIncludes: 'continue-after-archive',
124
+ afterAuthorId: TEST_USER_ID,
125
+ })
126
+
127
+ await waitForFooterMessage({
128
+ discord: ctx.discord,
129
+ threadId: thread.id,
130
+ timeout: 12_000,
131
+ afterMessageIncludes: 'archived-queue-survives',
132
+ afterAuthorId: ctx.discord.botUserId,
133
+ })
134
+
135
+ expect(await th.text()).toMatchInlineSnapshot(`
136
+ "--- from: user (interrupt-tester)
137
+ Reply with exactly: setup-archive-drain
138
+ --- from: assistant (TestBot)
139
+ > *using deterministic-provider/deterministic-v2*
140
+ > ok
141
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>
142
+ --- from: user (interrupt-tester)
143
+ PLUGIN_TIMEOUT_SLEEP_MARKER archive queue drain
144
+ --- from: assistant (TestBot)
145
+ > starting sleep 100
146
+ Queued message (position 1)
147
+ --- from: user (interrupt-tester)
148
+ Reply with exactly: continue-after-archive
149
+ --- from: assistant (TestBot)
150
+ > ok
151
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2*
152
+ » **interrupt-tester:** Reply with exactly: archived-queue-survives
153
+ > ok
154
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>"
155
+ `)
156
+ },
157
+ 20_000,
158
+ )
159
+
39
160
  test(
40
161
  'queued message via /queue + normal interrupt: interrupt reply should appear, then queue drains',
41
162
  async () => {
@@ -108,7 +229,7 @@ e2eTest('queue + interrupt drain ordering', () => {
108
229
  })
109
230
 
110
231
  // 5. Wait for the final state: the interrupt message should get its own
111
- // ⬥ ok reply, then the queued message should drain and get processed.
232
+ // ok reply, then the queued message should drain and get processed.
112
233
  // We wait for the queued message's footer as the final signal.
113
234
  await waitForFooterMessage({
114
235
  discord: ctx.discord,
@@ -123,25 +244,25 @@ e2eTest('queue + interrupt drain ordering', () => {
123
244
  "--- from: user (interrupt-tester)
124
245
  Reply with exactly: setup-interrupt-drain
125
246
  --- from: assistant (TestBot)
126
- *using deterministic-provider/deterministic-v2*
127
- ⬥ ok
128
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
247
+ > *using deterministic-provider/deterministic-v2*
248
+ > ok
249
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>
129
250
  --- from: user (interrupt-tester)
130
251
  PLUGIN_TIMEOUT_SLEEP_MARKER
131
252
  --- from: assistant (TestBot)
132
- ⬥ starting sleep 100
253
+ > starting sleep 100
133
254
  Queued message (position 1)
134
255
  --- from: user (interrupt-tester)
135
256
  Reply with exactly: interrupt-now
136
257
  --- from: assistant (TestBot)
137
- ⬥ ok
138
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
258
+ > ok
259
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2*
139
260
  » **interrupt-tester:** Reply with exactly: queued-behind-slow
140
- ⬥ ok
141
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
261
+ > ok
262
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>"
142
263
  `)
143
264
 
144
- // 7. Assert the interrupt message got its own ⬥ ok reply between the
265
+ // 7. Assert the interrupt message got its own ok reply between the
145
266
  // user's interrupt message and the queue dispatch indicator.
146
267
  const text = await th.text()
147
268
  const lines = text.split('\n')
@@ -158,7 +279,7 @@ e2eTest('queue + interrupt drain ordering', () => {
158
279
 
159
280
  const linesBetween = lines.slice(interruptUserLine + 1, queueDispatchLine)
160
281
  const hasInterruptReply = linesBetween.some((line) => {
161
- return line.includes('⬥ ok')
282
+ return line.includes('ok')
162
283
  })
163
284
  expect(hasInterruptReply).toBe(true)
164
285
  },