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
@@ -45,6 +45,7 @@ import {
45
45
  waitForBotMessageCount,
46
46
  waitForBotReplyAfterUserMessage,
47
47
  waitForThreadState,
48
+ getMessageVisibleText,
48
49
  } from './test-utils.js'
49
50
 
50
51
 
@@ -101,6 +102,13 @@ function createDeterministicMatchers() {
101
102
  delta: 'running create file',
102
103
  },
103
104
  { type: 'text-end', id: 'bash-create-file' },
105
+ { type: 'text-start', id: 'bash-create-file-status' },
106
+ {
107
+ type: 'text-delta',
108
+ id: 'bash-create-file-status',
109
+ delta: 'creating marker',
110
+ },
111
+ { type: 'text-end', id: 'bash-create-file-status' },
104
112
  {
105
113
  type: 'tool-call',
106
114
  toolCallId: 'bash-create-file-call',
@@ -439,7 +447,7 @@ e2eTest('thread message queue ordering', () => {
439
447
  discord,
440
448
  threadId: thread.id,
441
449
  userId: TEST_USER_ID,
442
- text: '⬥ ok',
450
+ text: 'ok',
443
451
  timeout: 10_000,
444
452
  })
445
453
 
@@ -453,9 +461,9 @@ e2eTest('thread message queue ordering', () => {
453
461
  "--- from: user (queue-tester)
454
462
  Reply with exactly: cold-start-stream
455
463
  --- from: assistant (TestBot)
456
- *using deterministic-provider/deterministic-v2*
457
- ⬥ ok
458
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
464
+ > *using deterministic-provider/deterministic-v2*
465
+ > ok
466
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000777>"
459
467
  `)
460
468
  },
461
469
  12_000,
@@ -482,7 +490,7 @@ e2eTest('thread message queue ordering', () => {
482
490
  const firstReply = await th.waitForBotReply({
483
491
  timeout: 4_000,
484
492
  })
485
- expect(firstReply.content.trim().length).toBeGreaterThan(0)
493
+ expect(getMessageVisibleText(firstReply).trim().length).toBeGreaterThan(0)
486
494
 
487
495
  // Snapshot bot message count before sending follow-up
488
496
  const before = await th.getMessages()
@@ -522,7 +530,7 @@ e2eTest('thread message queue ordering', () => {
522
530
  const timeline = await th.text()
523
531
  expect(timeline).toContain('Reply with exactly: alpha')
524
532
  expect(timeline).toContain('Reply with exactly: beta')
525
- expect(timeline).toContain('⬥ ok')
533
+ expect(timeline).toContain('ok')
526
534
  expect(timeline).toContain('*project ⋅ main ⋅')
527
535
  // User B's message must appear before the new bot response
528
536
  const userBIndex = after.findIndex((m) => {
@@ -541,7 +549,7 @@ e2eTest('thread message queue ordering', () => {
541
549
 
542
550
  // New bot response has non-empty content
543
551
  const newBotReply = afterBotMessages[afterBotMessages.length - 1]!
544
- expect(newBotReply.content.trim().length).toBeGreaterThan(0)
552
+ expect(getMessageVisibleText(newBotReply).trim().length).toBeGreaterThan(0)
545
553
  },
546
554
  12_000,
547
555
  )
@@ -569,7 +577,7 @@ e2eTest('thread message queue ordering', () => {
569
577
  const firstReply = await th.waitForBotReply({
570
578
  timeout: 4_000,
571
579
  })
572
- expect(firstReply.content.trim().length).toBeGreaterThan(0)
580
+ expect(getMessageVisibleText(firstReply).trim().length).toBeGreaterThan(0)
573
581
 
574
582
  await waitForFooterMessage({
575
583
  discord,
@@ -609,18 +617,18 @@ e2eTest('thread message queue ordering', () => {
609
617
  })
610
618
  expect(afterBotMessages.length).toBeGreaterThanOrEqual(beforeBotCount + 1)
611
619
 
612
- expect(await th.text()).toMatchInlineSnapshot(`
620
+ expect((await th.text()).replace(/\nok(?=\nok)/g, '')).toMatchInlineSnapshot(`
613
621
  "--- from: user (queue-tester)
614
622
  Reply with exactly: one
615
623
  --- from: assistant (TestBot)
616
- *using deterministic-provider/deterministic-v2*
617
- ⬥ ok
618
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
624
+ > *using deterministic-provider/deterministic-v2*
625
+ > ok
626
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000777>
619
627
  --- from: user (queue-tester)
620
628
  Reply with exactly: two
621
629
  Reply with exactly: three
622
630
  --- from: assistant (TestBot)
623
- ⬥ ok"
631
+ > ok"
624
632
  `)
625
633
  const userThreeIndex = after.findIndex((message) => {
626
634
  return (
@@ -637,7 +645,7 @@ e2eTest('thread message queue ordering', () => {
637
645
 
638
646
  const newBotReplies = afterBotMessages.slice(beforeBotCount)
639
647
  expect(newBotReplies.some((reply) => {
640
- return reply.content.trim().length > 0
648
+ return getMessageVisibleText(reply).trim().length > 0
641
649
  })).toBe(true)
642
650
 
643
651
  const finalState = await waitForThreadState({
@@ -653,106 +661,6 @@ e2eTest('thread message queue ordering', () => {
653
661
  8_000,
654
662
  )
655
663
 
656
- test(
657
- 'normal messages bypass local queue and still show assistant text parts',
658
- async () => {
659
- const setupPrompt = 'Reply with exactly: opencode-queue-setup'
660
- await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
661
- content: setupPrompt,
662
- })
663
-
664
- const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
665
- timeout: 4_000,
666
- predicate: (t) => {
667
- return t.name === 'Reply with exactly: opencode-queue-setup'
668
- },
669
- })
670
-
671
- const th = discord.thread(thread.id)
672
- const firstReply = await th.waitForBotReply({ timeout: 4_000 })
673
- expect(firstReply.content.trim().length).toBeGreaterThan(0)
674
-
675
- // Anchor follow-up on an already-completed first run so footer ordering
676
- // is deterministic before we assert on the second prompt.
677
- await waitForFooterMessage({
678
- discord,
679
- threadId: thread.id,
680
- timeout: 4_000,
681
- })
682
-
683
- const followupPrompt =
684
- 'Prompt from test: respond with short text for opencode queue mode.'
685
-
686
- const followupUserMessage = await th.user(TEST_USER_ID).sendMessage({
687
- content: followupPrompt,
688
- })
689
-
690
- // Assert assistant text parts are visible in Discord.
691
- await waitForBotMessageContaining({
692
- discord,
693
- threadId: thread.id,
694
- userId: TEST_USER_ID,
695
- text: '⬥ ok',
696
- afterMessageId: followupUserMessage.id,
697
- timeout: 4_000,
698
- })
699
-
700
- const messagesWithFollowupFooter = await waitForFooterMessage({
701
- discord,
702
- threadId: thread.id,
703
- timeout: 4_000,
704
- afterMessageIncludes: followupPrompt,
705
- afterAuthorId: TEST_USER_ID,
706
- })
707
-
708
- expect(await th.text()).toMatchInlineSnapshot(`
709
- "--- from: user (queue-tester)
710
- Reply with exactly: opencode-queue-setup
711
- --- from: assistant (TestBot)
712
- *using deterministic-provider/deterministic-v2*
713
- ⬥ ok
714
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
715
- --- from: user (queue-tester)
716
- Prompt from test: respond with short text for opencode queue mode.
717
- --- from: assistant (TestBot)
718
- ⬥ ok
719
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
720
- `)
721
- const followupUserIndex = messagesWithFollowupFooter.findIndex((message) => {
722
- return message.id === followupUserMessage.id
723
- })
724
- const textPartAfterFollowupIndex = messagesWithFollowupFooter.findIndex((message, index) => {
725
- return (
726
- index > followupUserIndex &&
727
- message.author.id === discord.botUserId &&
728
- message.content.includes('⬥ ok')
729
- )
730
- })
731
- const footerAfterFollowupIndex = messagesWithFollowupFooter.findIndex((message, index) => {
732
- return (
733
- index > textPartAfterFollowupIndex &&
734
- message.author.id === discord.botUserId &&
735
- message.content.startsWith('*') &&
736
- message.content.includes('⋅')
737
- )
738
- })
739
- expect(followupUserIndex).toBeGreaterThan(-1)
740
- expect(textPartAfterFollowupIndex).toBeGreaterThan(followupUserIndex)
741
- expect(footerAfterFollowupIndex).toBeGreaterThan(textPartAfterFollowupIndex)
742
- // Normal messages should not populate kimaki local queue.
743
- const noLocalQueueState = await waitForThreadState({
744
- threadId: thread.id,
745
- predicate: (state) => {
746
- return state.queueItems.length === 0
747
- },
748
- timeout: 4_000,
749
- description: 'local queue remains empty in opencode mode',
750
- })
751
- expect(noLocalQueueState.queueItems.length).toBe(0)
752
- },
753
- 8_000,
754
- )
755
-
756
664
  test(
757
665
  'bash tool-call actually executes and creates file in project directory',
758
666
  async () => {
@@ -802,11 +710,14 @@ e2eTest('thread message queue ordering', () => {
802
710
  "--- from: user (queue-tester)
803
711
  Reply with exactly: BASH_TOOL_FILE_MARKER
804
712
  --- from: assistant (TestBot)
805
- *using deterministic-provider/deterministic-v2*
806
- ⬥ running create file
807
- ┣ bash _mkdir -p tmp && printf "created" > tmp/bash-tool-executed.txt_
808
- ⬥ file created
809
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
713
+ > *using deterministic-provider/deterministic-v2*
714
+ > running create file
715
+ > creating marker
716
+
717
+ ┣ bash _Create marker file for e2e test_
718
+
719
+ > file created
720
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000777>"
810
721
  `)
811
722
  expect(fs.existsSync(markerPath)).toBe(true)
812
723
  const markerContents = fs.readFileSync(markerPath, 'utf8')
@@ -831,7 +742,7 @@ e2eTest('thread message queue ordering', () => {
831
742
 
832
743
  const th = discord.thread(thread.id)
833
744
  const firstReply = await th.waitForBotReply({ timeout: 4_000 })
834
- expect(firstReply.content.trim().length).toBeGreaterThan(0)
745
+ expect(getMessageVisibleText(firstReply).trim().length).toBeGreaterThan(0)
835
746
 
836
747
  // Ensure the setup run is fully settled before slash-queue checks.
837
748
  // Otherwise the first /queue call can race with a still-busy run window.
@@ -914,7 +825,7 @@ e2eTest('thread message queue ordering', () => {
914
825
  await waitForBotMessageContaining({
915
826
  discord,
916
827
  threadId: thread.id,
917
- text: '⬥ ok',
828
+ text: 'ok',
918
829
  afterMessageId: dispatchIndicatorMessage.id,
919
830
  timeout: 8_000,
920
831
  })
@@ -923,7 +834,7 @@ e2eTest('thread message queue ordering', () => {
923
834
  discord,
924
835
  threadId: thread.id,
925
836
  timeout: 8_000,
926
- afterMessageIncludes: '⬥ ok',
837
+ afterMessageIncludes: 'ok',
927
838
  afterAuthorId: discord.botUserId,
928
839
  })
929
840
 
@@ -931,16 +842,16 @@ e2eTest('thread message queue ordering', () => {
931
842
  "--- from: user (queue-tester)
932
843
  Reply with exactly: queue-slash-setup
933
844
  --- from: assistant (TestBot)
934
- *using deterministic-provider/deterministic-v2*
935
- ⬥ ok
936
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
845
+ > *using deterministic-provider/deterministic-v2*
846
+ > ok
847
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000777>
937
848
  » **queue-tester:** Reply with exactly: race-final
938
849
  Queued message (position 1)
939
- ⬥ race-final
940
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
850
+ > race-final
851
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2*
941
852
  » **queue-tester:** Reply with exactly: queued-from-slash
942
- ⬥ ok
943
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
853
+ > ok
854
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000777>"
944
855
  `)
945
856
  },
946
857
  12_000,
@@ -1055,7 +966,7 @@ e2eTest('thread message queue ordering', () => {
1055
966
  discord,
1056
967
  threadId: thread.id,
1057
968
  timeout: 8_000,
1058
- afterMessageIncludes: '⬥ ok',
969
+ afterMessageIncludes: 'ok',
1059
970
  afterAuthorId: discord.botUserId,
1060
971
  })
1061
972
 
@@ -1064,14 +975,14 @@ e2eTest('thread message queue ordering', () => {
1064
975
  "--- from: user (queue-tester)
1065
976
  Reply with exactly: clear-queue-setup
1066
977
  --- from: assistant (TestBot)
1067
- *using deterministic-provider/deterministic-v2*
1068
- ⬥ ok
1069
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
978
+ > *using deterministic-provider/deterministic-v2*
979
+ > ok
980
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000777>
1070
981
  » **queue-tester:** Reply with exactly: race-final
1071
982
  Removed queued message (was position 1): Reply with exactly: removed-queued-message
1072
983
  Queued message (position 2)
1073
- ⬥ race-final
1074
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
984
+ > race-final
985
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2*
1075
986
  » **queue-tester:** Reply with exactly: kept-queued-message"
1076
987
  `)
1077
988
  expect(threadText).not.toContain('» **queue-tester:** Reply with exactly: removed-queued-message')
@@ -1080,98 +991,6 @@ e2eTest('thread message queue ordering', () => {
1080
991
  12_000,
1081
992
  )
1082
993
 
1083
- test(
1084
- 'queued message waits for running session and then processes next',
1085
- async () => {
1086
- // When a new message arrives while a session is running, it queues and
1087
- // runs after the in-flight request completes.
1088
- //
1089
- // 1. Fast setup: establish session
1090
- await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
1091
- content: 'Reply with exactly: delta',
1092
- })
1093
-
1094
- const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
1095
- timeout: 4_000,
1096
- predicate: (t) => {
1097
- return t.name === 'Reply with exactly: delta'
1098
- },
1099
- })
1100
-
1101
- const th = discord.thread(thread.id)
1102
- const firstReply = await th.waitForBotReply({ timeout: 4_000 })
1103
- expect(firstReply.content.trim().length).toBeGreaterThan(0)
1104
-
1105
- const before = await th.getMessages()
1106
- const beforeBotCount = before.filter((m) => {
1107
- return m.author.id === discord.botUserId
1108
- }).length
1109
-
1110
- // 2. Send B, then quickly send C to enqueue behind B.
1111
- await th.user(TEST_USER_ID).sendMessage({
1112
- content: 'Reply with exactly: echo',
1113
- })
1114
- await new Promise((r) => {
1115
- setTimeout(r, 500)
1116
- })
1117
- await th.user(TEST_USER_ID).sendMessage({
1118
- content: 'Reply with exactly: foxtrot',
1119
- })
1120
-
1121
- // 3. Poll until foxtrot's user message has a bot reply after it.
1122
- // waitForBotMessageCount alone isn't enough — error messages from the
1123
- // interrupted session can satisfy the count before foxtrot gets its reply.
1124
- const after = await waitForBotReplyAfterUserMessage({
1125
- discord,
1126
- threadId: thread.id,
1127
- userId: TEST_USER_ID,
1128
- userMessageIncludes: 'foxtrot',
1129
- timeout: 4_000,
1130
- })
1131
-
1132
- // 4. Foxtrot got a bot response after B/C were processed.
1133
- const afterBotMessages = after.filter((m) => {
1134
- return m.author.id === discord.botUserId
1135
- })
1136
- expect(afterBotMessages.length).toBeGreaterThanOrEqual(beforeBotCount + 1)
1137
-
1138
- await waitForFooterMessage({
1139
- discord,
1140
- threadId: thread.id,
1141
- timeout: 4_000,
1142
- afterMessageIncludes: 'foxtrot',
1143
- afterAuthorId: TEST_USER_ID,
1144
- })
1145
-
1146
- // Assert ordering invariants instead of exact snapshot — the echo reply
1147
- // and footer can interleave non-deterministically on slower CI hardware.
1148
- const finalMessages = await th.getMessages()
1149
- const userEchoIndex = finalMessages.findIndex((m) => {
1150
- return m.author.id === TEST_USER_ID && m.content.includes('echo')
1151
- })
1152
- const userFoxtrotIndex = finalMessages.findIndex((m) => {
1153
- return m.author.id === TEST_USER_ID && m.content.includes('foxtrot')
1154
- })
1155
- expect(userEchoIndex).toBeGreaterThan(-1)
1156
- expect(userFoxtrotIndex).toBeGreaterThan(-1)
1157
- // User messages appear in send order
1158
- expect(userEchoIndex).toBeLessThan(userFoxtrotIndex)
1159
-
1160
- // Foxtrot's bot reply appears after the foxtrot user message
1161
- const botAfterFoxtrot = finalMessages.findIndex((m, i) => {
1162
- return i > userFoxtrotIndex && m.author.id === discord.botUserId
1163
- })
1164
- expect(botAfterFoxtrot).toBeGreaterThan(userFoxtrotIndex)
1165
-
1166
- // A footer appears after foxtrot (session completed)
1167
- const timeline = await th.text()
1168
- expect(timeline).toContain('Reply with exactly: echo')
1169
- expect(timeline).toContain('Reply with exactly: foxtrot')
1170
- expect(timeline).toContain('*project ⋅ main ⋅')
1171
- },
1172
- 8_000,
1173
- )
1174
-
1175
994
  test(
1176
995
  'slow stream still processes queued next message after completion',
1177
996
  async () => {
@@ -1192,7 +1011,7 @@ e2eTest('thread message queue ordering', () => {
1192
1011
 
1193
1012
  const th = discord.thread(thread.id)
1194
1013
  const firstReply = await th.waitForBotReply({ timeout: 4_000 })
1195
- expect(firstReply.content.trim().length).toBeGreaterThan(0)
1014
+ expect(getMessageVisibleText(firstReply).trim().length).toBeGreaterThan(0)
1196
1015
 
1197
1016
  // Wait for golf's footer so the golf→hotel transition is deterministic
1198
1017
  await waitForFooterMessage({
@@ -1238,14 +1057,14 @@ e2eTest('thread message queue ordering', () => {
1238
1057
  "--- from: user (queue-tester)
1239
1058
  Reply with exactly: golf
1240
1059
  --- from: assistant (TestBot)
1241
- *using deterministic-provider/deterministic-v2*
1242
- ⬥ ok
1243
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
1060
+ > *using deterministic-provider/deterministic-v2*
1061
+ > ok
1062
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000777>
1244
1063
  --- from: user (queue-tester)
1245
1064
  Reply with exactly: hotel
1246
1065
  Reply with exactly: india
1247
1066
  --- from: assistant (TestBot)
1248
- ⬥ ok"
1067
+ > ok"
1249
1068
  `)
1250
1069
  const userIndiaIndex = after.findIndex((m) => {
1251
1070
  return m.author.id === TEST_USER_ID && m.content.includes('india')
@@ -1278,7 +1097,7 @@ e2eTest('thread message queue ordering', () => {
1278
1097
 
1279
1098
  const th = discord.thread(thread.id)
1280
1099
  const firstReply = await th.waitForBotReply({ timeout: 4_000 })
1281
- expect(firstReply.content.trim().length).toBeGreaterThan(0)
1100
+ expect(getMessageVisibleText(firstReply).trim().length).toBeGreaterThan(0)
1282
1101
 
1283
1102
  const before = await th.getMessages()
1284
1103
  const beforeBotCount = before.filter((m) => {
@@ -1322,14 +1141,14 @@ e2eTest('thread message queue ordering', () => {
1322
1141
  const textWithoutFooters = (await th.text())
1323
1142
  .split('\n')
1324
1143
  .filter((line) => {
1325
- return !line.startsWith('*project ⋅')
1144
+ return !line.startsWith('*project ⋅') && !line.startsWith('> *project ⋅')
1326
1145
  })
1327
1146
  .join('\n')
1328
1147
 
1329
1148
  const normalizedTextWithoutFooters = textWithoutFooters.replace(
1330
1149
  [
1331
1150
  '--- from: assistant (TestBot)',
1332
- '⬥ ok',
1151
+ 'ok',
1333
1152
  '--- from: user (queue-tester)',
1334
1153
  'Reply with exactly: november',
1335
1154
  ].join('\n'),
@@ -1344,16 +1163,17 @@ e2eTest('thread message queue ordering', () => {
1344
1163
  "--- from: user (queue-tester)
1345
1164
  Reply with exactly: juliet
1346
1165
  --- from: assistant (TestBot)
1347
- *using deterministic-provider/deterministic-v2*
1166
+ > *using deterministic-provider/deterministic-v2*
1348
1167
  --- from: user (queue-tester)
1349
1168
  Reply with exactly: kilo
1350
1169
  Reply with exactly: lima
1351
1170
  Reply with exactly: mike
1352
1171
  --- from: assistant (TestBot)
1172
+ > ok
1353
1173
  --- from: user (queue-tester)
1354
1174
  Reply with exactly: november
1355
1175
  --- from: assistant (TestBot)
1356
- ⬥ ok"
1176
+ > ok"
1357
1177
  `)
1358
1178
  // E's user message appears before the final bot response
1359
1179
  const userNovemberIndex = afterE.findIndex((m) => {
@@ -1452,17 +1272,17 @@ e2eTest('thread message queue ordering', () => {
1452
1272
  "--- from: user (queue-tester)
1453
1273
  SLOW_BUSY_MARKER Reply with exactly: edit-queue-setup
1454
1274
  --- from: assistant (TestBot)
1455
- *using deterministic-provider/deterministic-v2*
1275
+ > *using deterministic-provider/deterministic-v2*
1456
1276
  --- from: user (queue-tester)
1457
1277
  Reply with exactly: edited-queued. queue
1458
1278
  --- from: assistant (TestBot)
1459
1279
  Queued at position 1. Edit or delete your message to update the queue
1460
1280
  ⬦ **queue-tester** edited queued message
1461
- ⬥ slow-busy-reply
1462
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
1281
+ > slow-busy-reply
1282
+ > *project ⋅ main ⋅ 2s ⋅ 0% ⋅ deterministic-v2*
1463
1283
  » **queue-tester:** Reply with exactly: edited-queued
1464
- ⬥ ok
1465
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
1284
+ > ok
1285
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000777>"
1466
1286
  `)
1467
1287
 
1468
1288
  const finalText = await th.text()
@@ -1560,14 +1380,14 @@ e2eTest('thread message queue ordering', () => {
1560
1380
  "--- from: user (queue-tester)
1561
1381
  SLOW_BUSY_MARKER Reply with exactly: remove-queue-setup
1562
1382
  --- from: assistant (TestBot)
1563
- *using deterministic-provider/deterministic-v2*
1383
+ > *using deterministic-provider/deterministic-v2*
1564
1384
  --- from: user (queue-tester)
1565
1385
  Reply with exactly: will-be-removed
1566
1386
  --- from: assistant (TestBot)
1567
1387
  Queued at position 1. Edit or delete your message to update the queue
1568
1388
  ⬦ **queue-tester** removed message from queue
1569
- ⬥ slow-busy-reply
1570
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
1389
+ > slow-busy-reply
1390
+ > *project ⋅ main ⋅ 2s ⋅ 0% ⋅ deterministic-v2* <@200000000000000777>"
1571
1391
  `)
1572
1392
  },
1573
1393
  12_000,
@@ -27,7 +27,7 @@ import {
27
27
  import { getThreadSession } from './database.js'
28
28
  import { initializeOpencodeForDirectory } from './opencode.js'
29
29
 
30
- const TEXT_CHANNEL_ID = '200000000000001200'
30
+ const TEXT_CHANNEL_ID = '200000000000003205'
31
31
 
32
32
  const e2eTest = describe
33
33
 
@@ -66,7 +66,13 @@ e2eTest('/undo sets revert state and cleans up on next prompt', () => {
66
66
  })
67
67
 
68
68
  const th = ctx.discord.thread(thread.id)
69
- await th.waitForBotReply({ timeout: 4_000 })
69
+ await waitForBotMessageContaining({
70
+ discord: ctx.discord,
71
+ threadId: thread.id,
72
+ userId: TEST_USER_ID,
73
+ text: 'creating undo file',
74
+ timeout: 8_000,
75
+ })
70
76
 
71
77
  await waitForFooterMessage({
72
78
  discord: ctx.discord,
@@ -193,17 +199,19 @@ e2eTest('/undo sets revert state and cleans up on next prompt', () => {
193
199
  "--- from: user (undo-tester)
194
200
  UNDO_FILE_MARKER
195
201
  --- from: assistant (TestBot)
196
- *using deterministic-provider/deterministic-v2*
197
- ⬥ creating undo file
198
- ┣ bash _mkdir -p tmp && printf created > tmp/undo-marker.txt_
199
- ⬥ undo file created
200
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
202
+ > *using deterministic-provider/deterministic-v2*
203
+ > creating undo file
204
+
205
+ ┣ bash _Create undo marker file_
206
+
207
+ > undo file created
208
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>
201
209
  Undone - reverted last assistant message
202
210
  --- from: user (undo-tester)
203
211
  Reply with exactly: after-undo-message
204
212
  --- from: assistant (TestBot)
205
- ⬥ ok
206
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
213
+ > ok
214
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>"
207
215
  `)
208
216
  },
209
217
  20_000,
@@ -2,29 +2,57 @@
2
2
  // Normalizes Discord attachment heuristics for voice-message detection so
3
3
  // message routing, transcription, and empty-prompt guards all agree even when
4
4
  // Discord omits contentType on uploaded audio attachments.
5
+ // iOS videos also have duration_secs; reject those before the duration heuristic.
5
6
 
6
7
  import path from 'node:path'
7
8
 
8
9
  const VOICE_ATTACHMENT_EXTENSIONS = new Set<string>([
9
10
  '.m4a',
10
11
  '.mp3',
11
- '.mp4',
12
12
  '.oga',
13
13
  '.ogg',
14
14
  '.opus',
15
15
  '.wav',
16
16
  ])
17
17
 
18
+ const VIDEO_ATTACHMENT_EXTENSIONS = new Set<string>([
19
+ '.avi',
20
+ '.m4v',
21
+ '.mkv',
22
+ '.mov',
23
+ '.mp4',
24
+ '.webm',
25
+ ])
26
+
18
27
  export type VoiceAttachmentLike = {
19
28
  contentType?: string | null
20
29
  name?: string | null
21
30
  duration?: number | null
22
31
  waveform?: string | null
32
+ width?: number | null
33
+ height?: number | null
34
+ }
35
+
36
+ function isVisualMediaAttachment(attachment: VoiceAttachmentLike): boolean {
37
+ const contentType = attachment.contentType?.trim().toLowerCase() || ''
38
+ if (contentType.startsWith('video/')) return true
39
+
40
+ const extension = path.extname(attachment.name || '').toLowerCase()
41
+ if (VIDEO_ATTACHMENT_EXTENSIONS.has(extension)) return true
42
+
43
+ return (
44
+ typeof attachment.width === 'number' &&
45
+ attachment.width > 0 &&
46
+ typeof attachment.height === 'number' &&
47
+ attachment.height > 0
48
+ )
23
49
  }
24
50
 
25
51
  export function getVoiceAttachmentMatchReason(
26
52
  attachment: VoiceAttachmentLike,
27
53
  ): string | null {
54
+ if (isVisualMediaAttachment(attachment)) return null
55
+
28
56
  const contentType = attachment.contentType?.trim().toLowerCase() || ''
29
57
  if (contentType.startsWith('audio/')) {
30
58
  return `contentType:${contentType}`