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
@@ -2,7 +2,7 @@
2
2
  // Split from queue-advanced-typing.e2e.test.ts for parallelization.
3
3
  import { describe, test, expect } from 'vitest';
4
4
  import { setupQueueAdvancedSuite, TEST_USER_ID, } from './queue-advanced-e2e-setup.js';
5
- import { waitForBotMessageContaining, waitForFooterMessage, } from './test-utils.js';
5
+ import { getMessageVisibleText, isFooterMessage, waitForBotMessageContaining, waitForFooterMessage, } from './test-utils.js';
6
6
  const TEXT_CHANNEL_ID = '200000000000001008';
7
7
  const e2eTest = describe;
8
8
  e2eTest('queue advanced: typing interrupt', () => {
@@ -68,32 +68,30 @@ e2eTest('queue advanced: typing interrupt', () => {
68
68
  if (index <= finalUserIndex) {
69
69
  return false;
70
70
  }
71
- return message.author.id === ctx.discord.botUserId && message.content.includes('ok');
71
+ return message.author.id === ctx.discord.botUserId && getMessageVisibleText(message).includes('ok');
72
72
  });
73
73
  const finalFooterIndex = messages.findIndex((message, index) => {
74
74
  if (index <= finalReplyIndex) {
75
75
  return false;
76
76
  }
77
- return message.author.id === ctx.discord.botUserId
78
- && message.content.startsWith('*')
79
- && message.content.includes('⋅');
77
+ return isFooterMessage({ message, botUserId: ctx.discord.botUserId });
80
78
  });
81
79
  expect(await th.text()).toMatchInlineSnapshot(`
82
80
  "--- from: user (queue-advanced-tester)
83
81
  Reply with exactly: typing-stop-interrupt-setup
84
82
  --- from: assistant (TestBot)
85
- *using deterministic-provider/deterministic-v2*
86
- ⬥ ok
87
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
83
+ > *using deterministic-provider/deterministic-v2*
84
+ > ok
85
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>
88
86
  --- from: user (queue-advanced-tester)
89
87
  PLUGIN_TIMEOUT_SLEEP_MARKER
90
88
  --- from: assistant (TestBot)
91
- ⬥ starting sleep 100
89
+ > starting sleep 100
92
90
  --- from: user (queue-advanced-tester)
93
91
  Reply with exactly: typing-stop-interrupt-final
94
92
  --- from: assistant (TestBot)
95
- ⬥ ok
96
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
93
+ > ok
94
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>"
97
95
  `);
98
96
  const timeline = await th.text({ showTyping: true });
99
97
  expect(finalUserIndex).toBeGreaterThanOrEqual(0);
@@ -101,7 +99,7 @@ e2eTest('queue advanced: typing interrupt', () => {
101
99
  expect(finalFooterIndex).toBeGreaterThan(finalReplyIndex);
102
100
  expect(messages[finalFooterIndex]).toBeDefined();
103
101
  const finalPromptPosition = timeline.indexOf('Reply with exactly: typing-stop-interrupt-final');
104
- const finalReplyPosition = timeline.indexOf('--- from: assistant (TestBot)\n⬥ ok', finalPromptPosition);
102
+ const finalReplyPosition = timeline.indexOf('--- from: assistant (TestBot)\n> ok', finalPromptPosition);
105
103
  const lastFooterPosition = timeline.lastIndexOf('*project ⋅');
106
104
  expect(finalPromptPosition).toBeGreaterThanOrEqual(0);
107
105
  expect(finalReplyPosition).toBeGreaterThan(finalPromptPosition);
@@ -2,7 +2,7 @@
2
2
  // Split from thread-queue-advanced.e2e.test.ts for parallelization.
3
3
  import { describe, test, expect } from 'vitest';
4
4
  import { setupQueueAdvancedSuite, TEST_USER_ID, } from './queue-advanced-e2e-setup.js';
5
- import { waitForBotMessageContaining, waitForFooterMessage, } from './test-utils.js';
5
+ import { getMessageVisibleText, isFooterMessage, waitForBotMessageContaining, waitForFooterMessage, } from './test-utils.js';
6
6
  const TEXT_CHANNEL_ID = '200000000000001002';
7
7
  const e2eTest = describe;
8
8
  e2eTest('queue advanced: typing lifecycle', () => {
@@ -41,19 +41,17 @@ e2eTest('queue advanced: typing lifecycle', () => {
41
41
  afterAuthorId: ctx.discord.botUserId,
42
42
  });
43
43
  const replyIndex = messages.findIndex((message) => {
44
- return message.author.id === ctx.discord.botUserId && message.content.includes('ok');
44
+ return message.author.id === ctx.discord.botUserId && getMessageVisibleText(message).includes('ok');
45
45
  });
46
46
  const footerIndex = messages.findIndex((message, index) => {
47
47
  if (index <= replyIndex) {
48
48
  return false;
49
49
  }
50
- return message.author.id === ctx.discord.botUserId
51
- && message.content.startsWith('*')
52
- && message.content.includes('⋅');
50
+ return isFooterMessage({ message, botUserId: ctx.discord.botUserId });
53
51
  });
54
52
  const timeline = await th.text({ showTyping: true });
55
53
  expect(timeline).toContain('Reply with exactly: typing-stop-normal');
56
- expect(timeline).toContain('⬥ ok');
54
+ expect(timeline).toContain('ok');
57
55
  expect(timeline).toContain('*project ⋅ main ⋅');
58
56
  const typingCount = (timeline.match(/\[bot typing\]/g) || []).length;
59
57
  expect(typingCount).toBeGreaterThanOrEqual(1);
@@ -110,7 +108,7 @@ e2eTest('queue advanced: typing lifecycle', () => {
110
108
  return false;
111
109
  }
112
110
  return message.author.id === ctx.discord.botUserId
113
- && message.content.includes('repulse-first');
111
+ && getMessageVisibleText(message).includes('repulse-first');
114
112
  });
115
113
  if (!firstReply) {
116
114
  throw new Error('Expected first bot reply after TYPING_REPULSE_MARKER');
@@ -132,7 +130,7 @@ e2eTest('queue advanced: typing lifecycle', () => {
132
130
  });
133
131
  const timeline = await th.text({ showTyping: true });
134
132
  expect(timeline).toContain('TYPING_REPULSE_MARKER');
135
- expect(timeline).toContain('⬥ repulse-first');
133
+ expect(timeline).toContain('repulse-first');
136
134
  const typingCount = (timeline.match(/\[bot typing\]/g) || []).length;
137
135
  expect(typingCount).toBeGreaterThanOrEqual(2);
138
136
  const followupUserIndex = messages.findIndex((message) => {
@@ -144,7 +142,7 @@ e2eTest('queue advanced: typing lifecycle', () => {
144
142
  return false;
145
143
  }
146
144
  return message.author.id === ctx.discord.botUserId
147
- && message.content.includes('repulse-first');
145
+ && getMessageVisibleText(message).includes('repulse-first');
148
146
  });
149
147
  expect(followupUserIndex).toBeGreaterThanOrEqual(0);
150
148
  expect(followupReplyIndex).toBeGreaterThan(followupUserIndex);
@@ -63,11 +63,11 @@ e2eTest('queue delete message', () => {
63
63
  "--- from: user (queue-delete-tester)
64
64
  SLOW_BUSY_MARKER Reply with exactly: delete-queue-setup
65
65
  --- from: assistant (TestBot)
66
- *using deterministic-provider/deterministic-v2*
66
+ > *using deterministic-provider/deterministic-v2*
67
67
  Queued at position 1. Edit or delete your message to update the queue
68
68
  ⬦ **queue-delete-tester** removed message from queue
69
- ⬥ slow-busy-reply
70
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
69
+ > slow-busy-reply
70
+ > *project ⋅ main ⋅ 2s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>"
71
71
  `);
72
72
  const finalText = await th.text();
73
73
  expect(finalText).not.toContain('» **queue-delete-tester:** Reply with exactly: deleted-queue');
@@ -107,14 +107,14 @@ describe('queue drain with pending interactive UI', () => {
107
107
  "--- from: user (drain-ui-tester)
108
108
  Reply with exactly: drain-button-setup
109
109
  --- from: assistant (TestBot)
110
- *using deterministic-provider/deterministic-v2*
111
- ⬥ ok
112
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
110
+ > *using deterministic-provider/deterministic-v2*
111
+ > ok
112
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>
113
113
  **Action Required**
114
114
  [user interaction]
115
115
  » **drain-ui-tester:** Reply with exactly: post-button-drain
116
- ⬥ ok
117
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
116
+ > ok
117
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>"
118
118
  `);
119
119
  }, 20_000);
120
120
  });
@@ -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).
@@ -15,6 +15,9 @@
15
15
  import { describe, test, expect } from 'vitest';
16
16
  import { setupQueueAdvancedSuite, TEST_USER_ID, } from './queue-advanced-e2e-setup.js';
17
17
  import { waitForFooterMessage, waitForBotMessageContaining, waitForMessageById, } from './test-utils.js';
18
+ import { getThreadSession } from './database.js';
19
+ import { archiveThread } from './discord-utils.js';
20
+ import { getOpencodeClient } from './opencode.js';
18
21
  const TEXT_CHANNEL_ID = '200000000000001099';
19
22
  const e2eTest = describe;
20
23
  e2eTest('queue + interrupt drain ordering', () => {
@@ -24,6 +27,113 @@ e2eTest('queue + interrupt drain ordering', () => {
24
27
  dirName: 'qa-interrupt-drain-e2e',
25
28
  username: 'interrupt-tester',
26
29
  });
30
+ test('archiving keeps the active run alive so queued messages drain', async () => {
31
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
32
+ content: 'Reply with exactly: setup-archive-drain',
33
+ });
34
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
35
+ timeout: 4_000,
36
+ predicate: (candidate) => {
37
+ return candidate.name === 'Reply with exactly: setup-archive-drain';
38
+ },
39
+ });
40
+ const th = ctx.discord.thread(thread.id);
41
+ await th.waitForBotReply({ timeout: 4_000 });
42
+ await waitForFooterMessage({
43
+ discord: ctx.discord,
44
+ threadId: thread.id,
45
+ timeout: 4_000,
46
+ });
47
+ await th.user(TEST_USER_ID).sendMessage({
48
+ content: 'PLUGIN_TIMEOUT_SLEEP_MARKER archive queue drain',
49
+ });
50
+ await waitForBotMessageContaining({
51
+ discord: ctx.discord,
52
+ threadId: thread.id,
53
+ userId: TEST_USER_ID,
54
+ text: 'starting sleep',
55
+ afterUserMessageIncludes: 'PLUGIN_TIMEOUT_SLEEP_MARKER archive queue drain',
56
+ timeout: 4_000,
57
+ });
58
+ const { id: queueInteractionId } = await th.user(TEST_USER_ID)
59
+ .runSlashCommand({
60
+ name: 'queue',
61
+ options: [{
62
+ name: 'message',
63
+ type: 3,
64
+ value: 'Reply with exactly: archived-queue-survives',
65
+ }],
66
+ });
67
+ const queueAck = await th.waitForInteractionAck({
68
+ interactionId: queueInteractionId,
69
+ timeout: 4_000,
70
+ });
71
+ if (!queueAck.messageId) {
72
+ throw new Error('Expected /queue response message id');
73
+ }
74
+ const queueStatusMessage = await waitForMessageById({
75
+ discord: ctx.discord,
76
+ threadId: thread.id,
77
+ messageId: queueAck.messageId,
78
+ timeout: 4_000,
79
+ });
80
+ expect(queueStatusMessage.content).toContain('Queued message');
81
+ const sessionId = await getThreadSession(thread.id);
82
+ if (!sessionId)
83
+ throw new Error('Expected mapped OpenCode session');
84
+ const client = getOpencodeClient(ctx.directories.projectDirectory);
85
+ if (!client)
86
+ throw new Error('Expected initialized OpenCode client');
87
+ await archiveThread({
88
+ rest: ctx.botClient.rest,
89
+ threadId: thread.id,
90
+ parentChannelId: TEXT_CHANNEL_ID,
91
+ sessionId,
92
+ client,
93
+ });
94
+ const statusResponse = await client.session.status({
95
+ directory: ctx.directories.projectDirectory,
96
+ });
97
+ expect(statusResponse.data?.[sessionId]?.type).toBe('busy');
98
+ await th.user(TEST_USER_ID).sendMessage({
99
+ content: 'Reply with exactly: continue-after-archive',
100
+ });
101
+ await waitForFooterMessage({
102
+ discord: ctx.discord,
103
+ threadId: thread.id,
104
+ timeout: 12_000,
105
+ afterMessageIncludes: 'continue-after-archive',
106
+ afterAuthorId: TEST_USER_ID,
107
+ });
108
+ await waitForFooterMessage({
109
+ discord: ctx.discord,
110
+ threadId: thread.id,
111
+ timeout: 12_000,
112
+ afterMessageIncludes: 'archived-queue-survives',
113
+ afterAuthorId: ctx.discord.botUserId,
114
+ });
115
+ expect(await th.text()).toMatchInlineSnapshot(`
116
+ "--- from: user (interrupt-tester)
117
+ Reply with exactly: setup-archive-drain
118
+ --- from: assistant (TestBot)
119
+ > *using deterministic-provider/deterministic-v2*
120
+ > ok
121
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>
122
+ --- from: user (interrupt-tester)
123
+ PLUGIN_TIMEOUT_SLEEP_MARKER archive queue drain
124
+ --- from: assistant (TestBot)
125
+ > starting sleep 100
126
+ Queued message (position 1)
127
+ --- from: user (interrupt-tester)
128
+ Reply with exactly: continue-after-archive
129
+ --- from: assistant (TestBot)
130
+ > ok
131
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2*
132
+ » **interrupt-tester:** Reply with exactly: archived-queue-survives
133
+ > ok
134
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>"
135
+ `);
136
+ }, 20_000);
27
137
  test('queued message via /queue + normal interrupt: interrupt reply should appear, then queue drains', async () => {
28
138
  // 1. Establish session with a quick first message
29
139
  await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
@@ -84,7 +194,7 @@ e2eTest('queue + interrupt drain ordering', () => {
84
194
  content: 'Reply with exactly: interrupt-now',
85
195
  });
86
196
  // 5. Wait for the final state: the interrupt message should get its own
87
- // ⬥ ok reply, then the queued message should drain and get processed.
197
+ // ok reply, then the queued message should drain and get processed.
88
198
  // We wait for the queued message's footer as the final signal.
89
199
  await waitForFooterMessage({
90
200
  discord: ctx.discord,
@@ -98,24 +208,24 @@ e2eTest('queue + interrupt drain ordering', () => {
98
208
  "--- from: user (interrupt-tester)
99
209
  Reply with exactly: setup-interrupt-drain
100
210
  --- from: assistant (TestBot)
101
- *using deterministic-provider/deterministic-v2*
102
- ⬥ ok
103
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
211
+ > *using deterministic-provider/deterministic-v2*
212
+ > ok
213
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>
104
214
  --- from: user (interrupt-tester)
105
215
  PLUGIN_TIMEOUT_SLEEP_MARKER
106
216
  --- from: assistant (TestBot)
107
- ⬥ starting sleep 100
217
+ > starting sleep 100
108
218
  Queued message (position 1)
109
219
  --- from: user (interrupt-tester)
110
220
  Reply with exactly: interrupt-now
111
221
  --- from: assistant (TestBot)
112
- ⬥ ok
113
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
222
+ > ok
223
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2*
114
224
  » **interrupt-tester:** Reply with exactly: queued-behind-slow
115
- ⬥ ok
116
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
225
+ > ok
226
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>"
117
227
  `);
118
- // 7. Assert the interrupt message got its own ⬥ ok reply between the
228
+ // 7. Assert the interrupt message got its own ok reply between the
119
229
  // user's interrupt message and the queue dispatch indicator.
120
230
  const text = await th.text();
121
231
  const lines = text.split('\n');
@@ -129,7 +239,7 @@ e2eTest('queue + interrupt drain ordering', () => {
129
239
  expect(queueDispatchLine).toBeGreaterThan(-1);
130
240
  const linesBetween = lines.slice(interruptUserLine + 1, queueDispatchLine);
131
241
  const hasInterruptReply = linesBetween.some((line) => {
132
- return line.includes('⬥ ok');
242
+ return line.includes('ok');
133
243
  });
134
244
  expect(hasInterruptReply).toBe(true);
135
245
  }, 20_000);
@@ -1,7 +1,4 @@
1
- // E2e test: queued message must drain after the user answers a pending question
2
- // via the Discord dropdown select menu. Reproduces a bug where answering via
3
- // select (not text) leaves queued messages stuck because the session continues
4
- // processing after the answer and may enter another blocking state.
1
+ // E2e test for ordered queue drain after a pending question is answered via select.
5
2
  import { describe, test, expect } from 'vitest';
6
3
  import { setupQueueAdvancedSuite, TEST_USER_ID, } from './queue-advanced-e2e-setup.js';
7
4
  import { waitForBotMessageContaining, waitForFooterMessage, } from './test-utils.js';
@@ -27,8 +24,7 @@ async function expectNoBotMessageContaining({ discord, threadId, text, timeout,
27
24
  while (Date.now() - start < timeout) {
28
25
  const messages = await discord.thread(threadId).getMessages();
29
26
  const match = messages.find((message) => {
30
- return (message.author.id === discord.botUserId
31
- && message.content.includes(text));
27
+ return message.author.id === discord.botUserId && message.content.includes(text);
32
28
  });
33
29
  if (match) {
34
30
  throw new Error(`Unexpected bot message containing ${JSON.stringify(text)} while it should still be queued`);
@@ -45,110 +41,15 @@ describe('queue drain after question select answer', () => {
45
41
  dirName: 'qa-question-select-drain',
46
42
  username: 'question-select-tester',
47
43
  });
48
- test('queued message drains after answering question via dropdown select', async () => {
49
- // 1. Send a message that triggers the question tool
50
- await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
51
- content: 'QUESTION_SELECT_QUEUE_MARKER',
52
- });
53
- const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
54
- timeout: 8_000,
55
- predicate: (t) => {
56
- return t.name === 'QUESTION_SELECT_QUEUE_MARKER';
57
- },
58
- });
59
- const th = ctx.discord.thread(thread.id);
60
- // 2. Wait for the question dropdown message to appear in Discord.
61
- // Uses visible message wait instead of internal Map polling which
62
- // is too timing-sensitive on CI.
63
- const questionMessages = await waitForBotMessageContaining({
64
- discord: ctx.discord,
65
- threadId: thread.id,
66
- text: 'How to proceed?',
67
- timeout: 12_000,
68
- });
69
- // Get the pending question context hash from the internal map.
70
- // By this point the question message is visible so the context must exist.
71
- const pending = await waitForPendingQuestion({
72
- threadId: thread.id,
73
- timeoutMs: 8_000,
74
- });
75
- const questionMsg = questionMessages.find((m) => {
76
- return m.content.includes('How to proceed?');
77
- });
78
- expect(questionMsg).toBeTruthy();
79
- // 3. Queue a message while question is pending
80
- const { id: queueInteractionId } = await th.user(TEST_USER_ID)
81
- .runSlashCommand({
82
- name: 'queue',
83
- options: [{ name: 'message', type: 3, value: 'Reply with exactly: post-question-drain' }],
84
- });
85
- const queueAck = await th.waitForInteractionAck({
86
- interactionId: queueInteractionId,
87
- timeout: 8_000,
88
- });
89
- if (!queueAck.messageId) {
90
- throw new Error('Expected /queue response message id');
91
- }
92
- // 4. The first queued item should be handed off immediately even while
93
- // the question is still pending, so the visible dispatch indicator
94
- // appears before the user answers the dropdown.
95
- await waitForBotMessageContaining({
96
- discord: ctx.discord,
97
- threadId: thread.id,
98
- text: '» **question-select-tester:** Reply with exactly: post-question-drain',
99
- timeout: 8_000,
100
- });
101
- // 5. Answer the question via dropdown select (pick first option "Alpha")
102
- const interaction = await th.user(TEST_USER_ID).selectMenu({
103
- messageId: questionMsg.id,
104
- customId: `ask_question:${pending.contextHash}:0`,
105
- values: ['0'],
106
- });
107
- await th.waitForInteractionAck({
108
- interactionId: interaction.id,
109
- timeout: 8_000,
110
- });
111
- // 6. Wait for footer from the drained queued message
112
- await waitForFooterMessage({
113
- discord: ctx.discord,
114
- threadId: thread.id,
115
- timeout: 8_000,
116
- afterMessageIncludes: '» **question-select-tester:**',
117
- afterAuthorId: ctx.discord.botUserId,
118
- });
119
- const timeline = await th.text({ showInteractions: true });
120
- expect(timeline).toMatchInlineSnapshot(`
121
- "--- from: user (question-select-tester)
122
- QUESTION_SELECT_QUEUE_MARKER
123
- --- from: assistant (TestBot)
124
- *using deterministic-provider/deterministic-v2*
125
- **Select action**
126
- How to proceed?
127
- ✓ _Alpha_
128
- [user interaction]
129
- » **question-select-tester:** Reply with exactly: post-question-drain
130
- Queued message (position 1)
131
- [user selects dropdown: 0]
132
- » **question-select-tester:** Alpha
133
- ⬥ ok
134
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
135
- `);
136
- expect(timeline).toContain('QUESTION_SELECT_QUEUE_MARKER');
137
- expect(timeline).toContain('How to proceed?');
138
- expect(timeline).toContain('[user selects dropdown: 0]');
139
- expect(timeline).toContain('» **question-select-tester:** Reply with exactly: post-question-drain');
140
- expect(timeline).toContain('⬥ ok');
141
- expect(timeline).toContain('*project ⋅ main ⋅');
142
- }, 20_000);
143
- test('only the first queued message is handed off after dropdown answer', async () => {
144
- const marker = 'QUESTION_SELECT_QUEUE_MARKER second-test';
44
+ test('queued messages drain in order after answering via dropdown select', async () => {
45
+ const marker = 'QUESTION_SELECT_QUEUE_MARKER';
145
46
  await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
146
47
  content: marker,
147
48
  });
148
49
  const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
149
50
  timeout: 8_000,
150
- predicate: (t) => {
151
- return t.name === marker;
51
+ predicate: (candidate) => {
52
+ return candidate.name === marker;
152
53
  },
153
54
  });
154
55
  const th = ctx.discord.thread(thread.id);
@@ -162,14 +63,13 @@ describe('queue drain after question select answer', () => {
162
63
  threadId: thread.id,
163
64
  timeoutMs: 8_000,
164
65
  });
165
- const questionMsg = questionMessages.find((message) => {
66
+ const questionMessage = questionMessages.find((message) => {
166
67
  return message.content.includes('How to proceed?');
167
68
  });
168
- expect(questionMsg).toBeTruthy();
169
- if (!questionMsg) {
69
+ if (!questionMessage) {
170
70
  throw new Error('Expected question message');
171
71
  }
172
- const firstQueuedPrompt = 'SLOW_ABORT_MARKER run long response';
72
+ const firstQueuedPrompt = 'QUESTION_SELECT_DRAIN_FIRST_MARKER';
173
73
  const secondQueuedPrompt = 'Reply with exactly: post-question-second';
174
74
  const { id: firstQueueInteractionId } = await th.user(TEST_USER_ID)
175
75
  .runSlashCommand({
@@ -180,6 +80,12 @@ describe('queue drain after question select answer', () => {
180
80
  interactionId: firstQueueInteractionId,
181
81
  timeout: 8_000,
182
82
  });
83
+ await waitForBotMessageContaining({
84
+ discord: ctx.discord,
85
+ threadId: thread.id,
86
+ text: `» **question-select-tester:** ${firstQueuedPrompt}`,
87
+ timeout: 8_000,
88
+ });
183
89
  const { id: secondQueueInteractionId } = await th.user(TEST_USER_ID)
184
90
  .runSlashCommand({
185
91
  name: 'queue',
@@ -189,8 +95,14 @@ describe('queue drain after question select answer', () => {
189
95
  interactionId: secondQueueInteractionId,
190
96
  timeout: 8_000,
191
97
  });
98
+ await expectNoBotMessageContaining({
99
+ discord: ctx.discord,
100
+ threadId: thread.id,
101
+ text: `» **question-select-tester:** ${secondQueuedPrompt}`,
102
+ timeout: 200,
103
+ });
192
104
  const interaction = await th.user(TEST_USER_ID).selectMenu({
193
- messageId: questionMsg.id,
105
+ messageId: questionMessage.id,
194
106
  customId: `ask_question:${pending.contextHash}:0`,
195
107
  values: ['0'],
196
108
  });
@@ -198,18 +110,6 @@ describe('queue drain after question select answer', () => {
198
110
  interactionId: interaction.id,
199
111
  timeout: 8_000,
200
112
  });
201
- await waitForBotMessageContaining({
202
- discord: ctx.discord,
203
- threadId: thread.id,
204
- text: `» **question-select-tester:** ${firstQueuedPrompt}`,
205
- timeout: 8_000,
206
- });
207
- await expectNoBotMessageContaining({
208
- discord: ctx.discord,
209
- threadId: thread.id,
210
- text: `» **question-select-tester:** ${secondQueuedPrompt}`,
211
- timeout: 200,
212
- });
213
113
  await waitForFooterMessage({
214
114
  discord: ctx.discord,
215
115
  threadId: thread.id,
@@ -233,28 +133,30 @@ describe('queue drain after question select answer', () => {
233
133
  const timeline = await th.text({ showInteractions: true });
234
134
  expect(timeline).toMatchInlineSnapshot(`
235
135
  "--- from: user (question-select-tester)
236
- QUESTION_SELECT_QUEUE_MARKER second-test
136
+ QUESTION_SELECT_QUEUE_MARKER
237
137
  --- from: assistant (TestBot)
238
- *using deterministic-provider/deterministic-v2*
138
+ > *using deterministic-provider/deterministic-v2*
239
139
  **Select action**
240
140
  How to proceed?
241
141
  ✓ _Alpha_
242
142
  [user interaction]
243
- » **question-select-tester:** SLOW_ABORT_MARKER run long response
143
+ » **question-select-tester:** QUESTION_SELECT_DRAIN_FIRST_MARKER
244
144
  Queued message (position 1)
245
145
  [user interaction]
246
146
  Queued message (position 1)
247
147
  [user selects dropdown: 0]
248
148
  » **question-select-tester:** Alpha
249
- ⬥ slow-response-started
250
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
149
+ > question-drain-first
150
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2*
251
151
  » **question-select-tester:** Reply with exactly: post-question-second
252
- ⬥ ok
253
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
152
+ > ok
153
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>"
254
154
  `);
155
+ expect(timeline).toContain('How to proceed?');
156
+ expect(timeline).toContain('[user selects dropdown: 0]');
255
157
  expect(timeline).toContain(`» **question-select-tester:** ${firstQueuedPrompt}`);
256
- expect(timeline).toContain('⬥ slow-response-started');
158
+ expect(timeline).toContain('question-drain-first');
257
159
  expect(timeline).toContain(`» **question-select-tester:** ${secondQueuedPrompt}`);
258
- expect(timeline).toContain('⬥ ok');
259
- }, 20_000);
160
+ expect(timeline).toContain('ok');
161
+ }, 15_000);
260
162
  });