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
@@ -21,7 +21,7 @@ import { startDiscordBot } from './discord-bot.js';
21
21
  import { setBotToken, initDatabase, closeDatabase, setChannelDirectory, setChannelVerbosity, getChannelVerbosity, } from './database.js';
22
22
  import { startHranaServer, stopHranaServer } from './hrana-server.js';
23
23
  import { initializeOpencodeForDirectory, stopOpencodeServer } from './opencode.js';
24
- import { chooseLockPort, cleanupTestSessions, initTestGitRepo, waitForFooterMessage, waitForBotMessageContaining, waitForMessageById, waitForBotMessageCount, waitForBotReplyAfterUserMessage, waitForThreadState, } from './test-utils.js';
24
+ import { chooseLockPort, cleanupTestSessions, initTestGitRepo, waitForFooterMessage, waitForBotMessageContaining, waitForMessageById, waitForBotMessageCount, waitForBotReplyAfterUserMessage, waitForThreadState, getMessageVisibleText, } from './test-utils.js';
25
25
  const e2eTest = describe;
26
26
  function createRunDirectories() {
27
27
  const root = path.resolve(process.cwd(), 'tmp', 'thread-queue-e2e');
@@ -70,6 +70,13 @@ function createDeterministicMatchers() {
70
70
  delta: 'running create file',
71
71
  },
72
72
  { type: 'text-end', id: 'bash-create-file' },
73
+ { type: 'text-start', id: 'bash-create-file-status' },
74
+ {
75
+ type: 'text-delta',
76
+ id: 'bash-create-file-status',
77
+ delta: 'creating marker',
78
+ },
79
+ { type: 'text-end', id: 'bash-create-file-status' },
73
80
  {
74
81
  type: 'tool-call',
75
82
  toolCallId: 'bash-create-file-call',
@@ -361,7 +368,7 @@ e2eTest('thread message queue ordering', () => {
361
368
  discord,
362
369
  threadId: thread.id,
363
370
  userId: TEST_USER_ID,
364
- text: '⬥ ok',
371
+ text: 'ok',
365
372
  timeout: 10_000,
366
373
  });
367
374
  await waitForFooterMessage({
@@ -373,9 +380,9 @@ e2eTest('thread message queue ordering', () => {
373
380
  "--- from: user (queue-tester)
374
381
  Reply with exactly: cold-start-stream
375
382
  --- from: assistant (TestBot)
376
- *using deterministic-provider/deterministic-v2*
377
- ⬥ ok
378
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
383
+ > *using deterministic-provider/deterministic-v2*
384
+ > ok
385
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000777>"
379
386
  `);
380
387
  }, 12_000);
381
388
  test('text message during active session gets processed', async () => {
@@ -394,7 +401,7 @@ e2eTest('thread message queue ordering', () => {
394
401
  const firstReply = await th.waitForBotReply({
395
402
  timeout: 4_000,
396
403
  });
397
- expect(firstReply.content.trim().length).toBeGreaterThan(0);
404
+ expect(getMessageVisibleText(firstReply).trim().length).toBeGreaterThan(0);
398
405
  // Snapshot bot message count before sending follow-up
399
406
  const before = await th.getMessages();
400
407
  const beforeBotCount = before.filter((m) => {
@@ -428,7 +435,7 @@ e2eTest('thread message queue ordering', () => {
428
435
  const timeline = await th.text();
429
436
  expect(timeline).toContain('Reply with exactly: alpha');
430
437
  expect(timeline).toContain('Reply with exactly: beta');
431
- expect(timeline).toContain('⬥ ok');
438
+ expect(timeline).toContain('ok');
432
439
  expect(timeline).toContain('*project ⋅ main ⋅');
433
440
  // User B's message must appear before the new bot response
434
441
  const userBIndex = after.findIndex((m) => {
@@ -443,7 +450,7 @@ e2eTest('thread message queue ordering', () => {
443
450
  expect(userBIndex).toBeLessThan(lastBotIndex);
444
451
  // New bot response has non-empty content
445
452
  const newBotReply = afterBotMessages[afterBotMessages.length - 1];
446
- expect(newBotReply.content.trim().length).toBeGreaterThan(0);
453
+ expect(getMessageVisibleText(newBotReply).trim().length).toBeGreaterThan(0);
447
454
  }, 12_000);
448
455
  test('two rapid text messages in thread — both processed in order', async () => {
449
456
  // 1. Send initial message to text channel → thread + session established
@@ -463,7 +470,7 @@ e2eTest('thread message queue ordering', () => {
463
470
  const firstReply = await th.waitForBotReply({
464
471
  timeout: 4_000,
465
472
  });
466
- expect(firstReply.content.trim().length).toBeGreaterThan(0);
473
+ expect(getMessageVisibleText(firstReply).trim().length).toBeGreaterThan(0);
467
474
  await waitForFooterMessage({
468
475
  discord,
469
476
  threadId: thread.id,
@@ -497,18 +504,18 @@ e2eTest('thread message queue ordering', () => {
497
504
  return m.author.id === discord.botUserId;
498
505
  });
499
506
  expect(afterBotMessages.length).toBeGreaterThanOrEqual(beforeBotCount + 1);
500
- expect(await th.text()).toMatchInlineSnapshot(`
507
+ expect((await th.text()).replace(/\nok(?=\nok)/g, '')).toMatchInlineSnapshot(`
501
508
  "--- from: user (queue-tester)
502
509
  Reply with exactly: one
503
510
  --- from: assistant (TestBot)
504
- *using deterministic-provider/deterministic-v2*
505
- ⬥ ok
506
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
511
+ > *using deterministic-provider/deterministic-v2*
512
+ > ok
513
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000777>
507
514
  --- from: user (queue-tester)
508
515
  Reply with exactly: two
509
516
  Reply with exactly: three
510
517
  --- from: assistant (TestBot)
511
- ⬥ ok"
518
+ > ok"
512
519
  `);
513
520
  const userThreeIndex = after.findIndex((message) => {
514
521
  return (message.author.id === TEST_USER_ID &&
@@ -521,7 +528,7 @@ e2eTest('thread message queue ordering', () => {
521
528
  expect(botAfterThreeIndex).toBeGreaterThan(userThreeIndex);
522
529
  const newBotReplies = afterBotMessages.slice(beforeBotCount);
523
530
  expect(newBotReplies.some((reply) => {
524
- return reply.content.trim().length > 0;
531
+ return getMessageVisibleText(reply).trim().length > 0;
525
532
  })).toBe(true);
526
533
  const finalState = await waitForThreadState({
527
534
  threadId: thread.id,
@@ -533,88 +540,6 @@ e2eTest('thread message queue ordering', () => {
533
540
  });
534
541
  expect(finalState.queueItems.length).toBe(0);
535
542
  }, 8_000);
536
- test('normal messages bypass local queue and still show assistant text parts', async () => {
537
- const setupPrompt = 'Reply with exactly: opencode-queue-setup';
538
- await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
539
- content: setupPrompt,
540
- });
541
- const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
542
- timeout: 4_000,
543
- predicate: (t) => {
544
- return t.name === 'Reply with exactly: opencode-queue-setup';
545
- },
546
- });
547
- const th = discord.thread(thread.id);
548
- const firstReply = await th.waitForBotReply({ timeout: 4_000 });
549
- expect(firstReply.content.trim().length).toBeGreaterThan(0);
550
- // Anchor follow-up on an already-completed first run so footer ordering
551
- // is deterministic before we assert on the second prompt.
552
- await waitForFooterMessage({
553
- discord,
554
- threadId: thread.id,
555
- timeout: 4_000,
556
- });
557
- const followupPrompt = 'Prompt from test: respond with short text for opencode queue mode.';
558
- const followupUserMessage = await th.user(TEST_USER_ID).sendMessage({
559
- content: followupPrompt,
560
- });
561
- // Assert assistant text parts are visible in Discord.
562
- await waitForBotMessageContaining({
563
- discord,
564
- threadId: thread.id,
565
- userId: TEST_USER_ID,
566
- text: '⬥ ok',
567
- afterMessageId: followupUserMessage.id,
568
- timeout: 4_000,
569
- });
570
- const messagesWithFollowupFooter = await waitForFooterMessage({
571
- discord,
572
- threadId: thread.id,
573
- timeout: 4_000,
574
- afterMessageIncludes: followupPrompt,
575
- afterAuthorId: TEST_USER_ID,
576
- });
577
- expect(await th.text()).toMatchInlineSnapshot(`
578
- "--- from: user (queue-tester)
579
- Reply with exactly: opencode-queue-setup
580
- --- from: assistant (TestBot)
581
- *using deterministic-provider/deterministic-v2*
582
- ⬥ ok
583
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
584
- --- from: user (queue-tester)
585
- Prompt from test: respond with short text for opencode queue mode.
586
- --- from: assistant (TestBot)
587
- ⬥ ok
588
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
589
- `);
590
- const followupUserIndex = messagesWithFollowupFooter.findIndex((message) => {
591
- return message.id === followupUserMessage.id;
592
- });
593
- const textPartAfterFollowupIndex = messagesWithFollowupFooter.findIndex((message, index) => {
594
- return (index > followupUserIndex &&
595
- message.author.id === discord.botUserId &&
596
- message.content.includes('⬥ ok'));
597
- });
598
- const footerAfterFollowupIndex = messagesWithFollowupFooter.findIndex((message, index) => {
599
- return (index > textPartAfterFollowupIndex &&
600
- message.author.id === discord.botUserId &&
601
- message.content.startsWith('*') &&
602
- message.content.includes('⋅'));
603
- });
604
- expect(followupUserIndex).toBeGreaterThan(-1);
605
- expect(textPartAfterFollowupIndex).toBeGreaterThan(followupUserIndex);
606
- expect(footerAfterFollowupIndex).toBeGreaterThan(textPartAfterFollowupIndex);
607
- // Normal messages should not populate kimaki local queue.
608
- const noLocalQueueState = await waitForThreadState({
609
- threadId: thread.id,
610
- predicate: (state) => {
611
- return state.queueItems.length === 0;
612
- },
613
- timeout: 4_000,
614
- description: 'local queue remains empty in opencode mode',
615
- });
616
- expect(noLocalQueueState.queueItems.length).toBe(0);
617
- }, 8_000);
618
543
  test('bash tool-call actually executes and creates file in project directory', async () => {
619
544
  const markerRelativePath = path.join('tmp', 'bash-tool-executed.txt');
620
545
  const markerPath = path.join(directories.projectDirectory, markerRelativePath);
@@ -654,11 +579,14 @@ e2eTest('thread message queue ordering', () => {
654
579
  "--- from: user (queue-tester)
655
580
  Reply with exactly: BASH_TOOL_FILE_MARKER
656
581
  --- from: assistant (TestBot)
657
- *using deterministic-provider/deterministic-v2*
658
- ⬥ running create file
659
- ┣ bash _mkdir -p tmp && printf "created" > tmp/bash-tool-executed.txt_
660
- ⬥ file created
661
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
582
+ > *using deterministic-provider/deterministic-v2*
583
+ > running create file
584
+ > creating marker
585
+
586
+ ┣ bash _Create marker file for e2e test_
587
+
588
+ > file created
589
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000777>"
662
590
  `);
663
591
  expect(fs.existsSync(markerPath)).toBe(true);
664
592
  const markerContents = fs.readFileSync(markerPath, 'utf8');
@@ -676,7 +604,7 @@ e2eTest('thread message queue ordering', () => {
676
604
  });
677
605
  const th = discord.thread(thread.id);
678
606
  const firstReply = await th.waitForBotReply({ timeout: 4_000 });
679
- expect(firstReply.content.trim().length).toBeGreaterThan(0);
607
+ expect(getMessageVisibleText(firstReply).trim().length).toBeGreaterThan(0);
680
608
  // Ensure the setup run is fully settled before slash-queue checks.
681
609
  // Otherwise the first /queue call can race with a still-busy run window.
682
610
  await waitForFooterMessage({
@@ -746,7 +674,7 @@ e2eTest('thread message queue ordering', () => {
746
674
  await waitForBotMessageContaining({
747
675
  discord,
748
676
  threadId: thread.id,
749
- text: '⬥ ok',
677
+ text: 'ok',
750
678
  afterMessageId: dispatchIndicatorMessage.id,
751
679
  timeout: 8_000,
752
680
  });
@@ -754,23 +682,23 @@ e2eTest('thread message queue ordering', () => {
754
682
  discord,
755
683
  threadId: thread.id,
756
684
  timeout: 8_000,
757
- afterMessageIncludes: '⬥ ok',
685
+ afterMessageIncludes: 'ok',
758
686
  afterAuthorId: discord.botUserId,
759
687
  });
760
688
  expect(await th.text()).toMatchInlineSnapshot(`
761
689
  "--- from: user (queue-tester)
762
690
  Reply with exactly: queue-slash-setup
763
691
  --- from: assistant (TestBot)
764
- *using deterministic-provider/deterministic-v2*
765
- ⬥ ok
766
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
692
+ > *using deterministic-provider/deterministic-v2*
693
+ > ok
694
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000777>
767
695
  » **queue-tester:** Reply with exactly: race-final
768
696
  Queued message (position 1)
769
- ⬥ race-final
770
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
697
+ > race-final
698
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2*
771
699
  » **queue-tester:** Reply with exactly: queued-from-slash
772
- ⬥ ok
773
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
700
+ > ok
701
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000777>"
774
702
  `);
775
703
  }, 12_000);
776
704
  test('Remove from queue button clears only that queued message', async () => {
@@ -864,7 +792,7 @@ e2eTest('thread message queue ordering', () => {
864
792
  discord,
865
793
  threadId: thread.id,
866
794
  timeout: 8_000,
867
- afterMessageIncludes: '⬥ ok',
795
+ afterMessageIncludes: 'ok',
868
796
  afterAuthorId: discord.botUserId,
869
797
  });
870
798
  const threadText = await th.text();
@@ -872,96 +800,19 @@ e2eTest('thread message queue ordering', () => {
872
800
  "--- from: user (queue-tester)
873
801
  Reply with exactly: clear-queue-setup
874
802
  --- from: assistant (TestBot)
875
- *using deterministic-provider/deterministic-v2*
876
- ⬥ ok
877
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
803
+ > *using deterministic-provider/deterministic-v2*
804
+ > ok
805
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000777>
878
806
  » **queue-tester:** Reply with exactly: race-final
879
807
  Removed queued message (was position 1): Reply with exactly: removed-queued-message
880
808
  Queued message (position 2)
881
- ⬥ race-final
882
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
809
+ > race-final
810
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2*
883
811
  » **queue-tester:** Reply with exactly: kept-queued-message"
884
812
  `);
885
813
  expect(threadText).not.toContain('» **queue-tester:** Reply with exactly: removed-queued-message');
886
814
  expect(threadText).toContain('kept-queued-message');
887
815
  }, 12_000);
888
- test('queued message waits for running session and then processes next', async () => {
889
- // When a new message arrives while a session is running, it queues and
890
- // runs after the in-flight request completes.
891
- //
892
- // 1. Fast setup: establish session
893
- await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
894
- content: 'Reply with exactly: delta',
895
- });
896
- const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
897
- timeout: 4_000,
898
- predicate: (t) => {
899
- return t.name === 'Reply with exactly: delta';
900
- },
901
- });
902
- const th = discord.thread(thread.id);
903
- const firstReply = await th.waitForBotReply({ timeout: 4_000 });
904
- expect(firstReply.content.trim().length).toBeGreaterThan(0);
905
- const before = await th.getMessages();
906
- const beforeBotCount = before.filter((m) => {
907
- return m.author.id === discord.botUserId;
908
- }).length;
909
- // 2. Send B, then quickly send C to enqueue behind B.
910
- await th.user(TEST_USER_ID).sendMessage({
911
- content: 'Reply with exactly: echo',
912
- });
913
- await new Promise((r) => {
914
- setTimeout(r, 500);
915
- });
916
- await th.user(TEST_USER_ID).sendMessage({
917
- content: 'Reply with exactly: foxtrot',
918
- });
919
- // 3. Poll until foxtrot's user message has a bot reply after it.
920
- // waitForBotMessageCount alone isn't enough — error messages from the
921
- // interrupted session can satisfy the count before foxtrot gets its reply.
922
- const after = await waitForBotReplyAfterUserMessage({
923
- discord,
924
- threadId: thread.id,
925
- userId: TEST_USER_ID,
926
- userMessageIncludes: 'foxtrot',
927
- timeout: 4_000,
928
- });
929
- // 4. Foxtrot got a bot response after B/C were processed.
930
- const afterBotMessages = after.filter((m) => {
931
- return m.author.id === discord.botUserId;
932
- });
933
- expect(afterBotMessages.length).toBeGreaterThanOrEqual(beforeBotCount + 1);
934
- await waitForFooterMessage({
935
- discord,
936
- threadId: thread.id,
937
- timeout: 4_000,
938
- afterMessageIncludes: 'foxtrot',
939
- afterAuthorId: TEST_USER_ID,
940
- });
941
- // Assert ordering invariants instead of exact snapshot — the echo reply
942
- // and footer can interleave non-deterministically on slower CI hardware.
943
- const finalMessages = await th.getMessages();
944
- const userEchoIndex = finalMessages.findIndex((m) => {
945
- return m.author.id === TEST_USER_ID && m.content.includes('echo');
946
- });
947
- const userFoxtrotIndex = finalMessages.findIndex((m) => {
948
- return m.author.id === TEST_USER_ID && m.content.includes('foxtrot');
949
- });
950
- expect(userEchoIndex).toBeGreaterThan(-1);
951
- expect(userFoxtrotIndex).toBeGreaterThan(-1);
952
- // User messages appear in send order
953
- expect(userEchoIndex).toBeLessThan(userFoxtrotIndex);
954
- // Foxtrot's bot reply appears after the foxtrot user message
955
- const botAfterFoxtrot = finalMessages.findIndex((m, i) => {
956
- return i > userFoxtrotIndex && m.author.id === discord.botUserId;
957
- });
958
- expect(botAfterFoxtrot).toBeGreaterThan(userFoxtrotIndex);
959
- // A footer appears after foxtrot (session completed)
960
- const timeline = await th.text();
961
- expect(timeline).toContain('Reply with exactly: echo');
962
- expect(timeline).toContain('Reply with exactly: foxtrot');
963
- expect(timeline).toContain('*project ⋅ main ⋅');
964
- }, 8_000);
965
816
  test('slow stream still processes queued next message after completion', async () => {
966
817
  // A message sent mid-stream queues and runs after the in-flight request
967
818
  // completes (no auto-interrupt).
@@ -977,7 +828,7 @@ e2eTest('thread message queue ordering', () => {
977
828
  });
978
829
  const th = discord.thread(thread.id);
979
830
  const firstReply = await th.waitForBotReply({ timeout: 4_000 });
980
- expect(firstReply.content.trim().length).toBeGreaterThan(0);
831
+ expect(getMessageVisibleText(firstReply).trim().length).toBeGreaterThan(0);
981
832
  // Wait for golf's footer so the golf→hotel transition is deterministic
982
833
  await waitForFooterMessage({
983
834
  discord,
@@ -1017,14 +868,14 @@ e2eTest('thread message queue ordering', () => {
1017
868
  "--- from: user (queue-tester)
1018
869
  Reply with exactly: golf
1019
870
  --- from: assistant (TestBot)
1020
- *using deterministic-provider/deterministic-v2*
1021
- ⬥ ok
1022
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
871
+ > *using deterministic-provider/deterministic-v2*
872
+ > ok
873
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000777>
1023
874
  --- from: user (queue-tester)
1024
875
  Reply with exactly: hotel
1025
876
  Reply with exactly: india
1026
877
  --- from: assistant (TestBot)
1027
- ⬥ ok"
878
+ > ok"
1028
879
  `);
1029
880
  const userIndiaIndex = after.findIndex((m) => {
1030
881
  return m.author.id === TEST_USER_ID && m.content.includes('india');
@@ -1049,7 +900,7 @@ e2eTest('thread message queue ordering', () => {
1049
900
  });
1050
901
  const th = discord.thread(thread.id);
1051
902
  const firstReply = await th.waitForBotReply({ timeout: 4_000 });
1052
- expect(firstReply.content.trim().length).toBeGreaterThan(0);
903
+ expect(getMessageVisibleText(firstReply).trim().length).toBeGreaterThan(0);
1053
904
  const before = await th.getMessages();
1054
905
  const beforeBotCount = before.filter((m) => {
1055
906
  return m.author.id === discord.botUserId;
@@ -1087,12 +938,12 @@ e2eTest('thread message queue ordering', () => {
1087
938
  const textWithoutFooters = (await th.text())
1088
939
  .split('\n')
1089
940
  .filter((line) => {
1090
- return !line.startsWith('*project ⋅');
941
+ return !line.startsWith('*project ⋅') && !line.startsWith('> *project ⋅');
1091
942
  })
1092
943
  .join('\n');
1093
944
  const normalizedTextWithoutFooters = textWithoutFooters.replace([
1094
945
  '--- from: assistant (TestBot)',
1095
- '⬥ ok',
946
+ 'ok',
1096
947
  '--- from: user (queue-tester)',
1097
948
  'Reply with exactly: november',
1098
949
  ].join('\n'), [
@@ -1104,16 +955,17 @@ e2eTest('thread message queue ordering', () => {
1104
955
  "--- from: user (queue-tester)
1105
956
  Reply with exactly: juliet
1106
957
  --- from: assistant (TestBot)
1107
- *using deterministic-provider/deterministic-v2*
958
+ > *using deterministic-provider/deterministic-v2*
1108
959
  --- from: user (queue-tester)
1109
960
  Reply with exactly: kilo
1110
961
  Reply with exactly: lima
1111
962
  Reply with exactly: mike
1112
963
  --- from: assistant (TestBot)
964
+ > ok
1113
965
  --- from: user (queue-tester)
1114
966
  Reply with exactly: november
1115
967
  --- from: assistant (TestBot)
1116
- ⬥ ok"
968
+ > ok"
1117
969
  `);
1118
970
  // E's user message appears before the final bot response
1119
971
  const userNovemberIndex = afterE.findIndex((m) => {
@@ -1198,17 +1050,17 @@ e2eTest('thread message queue ordering', () => {
1198
1050
  "--- from: user (queue-tester)
1199
1051
  SLOW_BUSY_MARKER Reply with exactly: edit-queue-setup
1200
1052
  --- from: assistant (TestBot)
1201
- *using deterministic-provider/deterministic-v2*
1053
+ > *using deterministic-provider/deterministic-v2*
1202
1054
  --- from: user (queue-tester)
1203
1055
  Reply with exactly: edited-queued. queue
1204
1056
  --- from: assistant (TestBot)
1205
1057
  Queued at position 1. Edit or delete your message to update the queue
1206
1058
  ⬦ **queue-tester** edited queued message
1207
- ⬥ slow-busy-reply
1208
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
1059
+ > slow-busy-reply
1060
+ > *project ⋅ main ⋅ 2s ⋅ 0% ⋅ deterministic-v2*
1209
1061
  » **queue-tester:** Reply with exactly: edited-queued
1210
- ⬥ ok
1211
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
1062
+ > ok
1063
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000777>"
1212
1064
  `);
1213
1065
  const finalText = await th.text();
1214
1066
  expect(finalText).toContain('edited-queued');
@@ -1291,14 +1143,14 @@ e2eTest('thread message queue ordering', () => {
1291
1143
  "--- from: user (queue-tester)
1292
1144
  SLOW_BUSY_MARKER Reply with exactly: remove-queue-setup
1293
1145
  --- from: assistant (TestBot)
1294
- *using deterministic-provider/deterministic-v2*
1146
+ > *using deterministic-provider/deterministic-v2*
1295
1147
  --- from: user (queue-tester)
1296
1148
  Reply with exactly: will-be-removed
1297
1149
  --- from: assistant (TestBot)
1298
1150
  Queued at position 1. Edit or delete your message to update the queue
1299
1151
  ⬦ **queue-tester** removed message from queue
1300
- ⬥ slow-busy-reply
1301
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
1152
+ > slow-busy-reply
1153
+ > *project ⋅ main ⋅ 2s ⋅ 0% ⋅ deterministic-v2* <@200000000000000777>"
1302
1154
  `);
1303
1155
  }, 12_000);
1304
1156
  });
@@ -19,7 +19,7 @@ import { setupQueueAdvancedSuite, TEST_USER_ID, } from './queue-advanced-e2e-set
19
19
  import { waitForBotMessageContaining, waitForFooterMessage, } from './test-utils.js';
20
20
  import { getThreadSession } from './database.js';
21
21
  import { initializeOpencodeForDirectory } from './opencode.js';
22
- const TEXT_CHANNEL_ID = '200000000000001200';
22
+ const TEXT_CHANNEL_ID = '200000000000003205';
23
23
  const e2eTest = describe;
24
24
  e2eTest('/undo sets revert state and cleans up on next prompt', () => {
25
25
  const ctx = setupQueueAdvancedSuite({
@@ -46,7 +46,13 @@ e2eTest('/undo sets revert state and cleans up on next prompt', () => {
46
46
  },
47
47
  });
48
48
  const th = ctx.discord.thread(thread.id);
49
- await th.waitForBotReply({ timeout: 4_000 });
49
+ await waitForBotMessageContaining({
50
+ discord: ctx.discord,
51
+ threadId: thread.id,
52
+ userId: TEST_USER_ID,
53
+ text: 'creating undo file',
54
+ timeout: 8_000,
55
+ });
50
56
  await waitForFooterMessage({
51
57
  discord: ctx.discord,
52
58
  threadId: thread.id,
@@ -147,17 +153,19 @@ e2eTest('/undo sets revert state and cleans up on next prompt', () => {
147
153
  "--- from: user (undo-tester)
148
154
  UNDO_FILE_MARKER
149
155
  --- from: assistant (TestBot)
150
- *using deterministic-provider/deterministic-v2*
151
- ⬥ creating undo file
152
- ┣ bash _mkdir -p tmp && printf created > tmp/undo-marker.txt_
153
- ⬥ undo file created
154
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
156
+ > *using deterministic-provider/deterministic-v2*
157
+ > creating undo file
158
+
159
+ ┣ bash _Create undo marker file_
160
+
161
+ > undo file created
162
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>
155
163
  Undone - reverted last assistant message
156
164
  --- from: user (undo-tester)
157
165
  Reply with exactly: after-undo-message
158
166
  --- from: assistant (TestBot)
159
- ⬥ ok
160
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
167
+ > ok
168
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>"
161
169
  `);
162
170
  }, 20_000);
163
171
  });
@@ -2,17 +2,39 @@
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
  import path from 'node:path';
6
7
  const VOICE_ATTACHMENT_EXTENSIONS = new Set([
7
8
  '.m4a',
8
9
  '.mp3',
9
- '.mp4',
10
10
  '.oga',
11
11
  '.ogg',
12
12
  '.opus',
13
13
  '.wav',
14
14
  ]);
15
+ const VIDEO_ATTACHMENT_EXTENSIONS = new Set([
16
+ '.avi',
17
+ '.m4v',
18
+ '.mkv',
19
+ '.mov',
20
+ '.mp4',
21
+ '.webm',
22
+ ]);
23
+ function isVisualMediaAttachment(attachment) {
24
+ const contentType = attachment.contentType?.trim().toLowerCase() || '';
25
+ if (contentType.startsWith('video/'))
26
+ return true;
27
+ const extension = path.extname(attachment.name || '').toLowerCase();
28
+ if (VIDEO_ATTACHMENT_EXTENSIONS.has(extension))
29
+ return true;
30
+ return (typeof attachment.width === 'number' &&
31
+ attachment.width > 0 &&
32
+ typeof attachment.height === 'number' &&
33
+ attachment.height > 0);
34
+ }
15
35
  export function getVoiceAttachmentMatchReason(attachment) {
36
+ if (isVisualMediaAttachment(attachment))
37
+ return null;
16
38
  const contentType = attachment.contentType?.trim().toLowerCase() || '';
17
39
  if (contentType.startsWith('audio/')) {
18
40
  return `contentType:${contentType}`;
@@ -16,7 +16,7 @@ import { transcribeViaKimakiGateway } from './cloudflare-transcription.js';
16
16
  import { sendThreadMessage, escapeDiscordFormatting, NOTIFY_MESSAGE_FLAGS, hasKimakiBotPermission, } from './discord-utils.js';
17
17
  import { requestAudioApiKey } from './commands/gemini-apikey.js';
18
18
  import { transcribeAudio } from './voice.js';
19
- import { DiscordOperationError, FetchError } from './errors.js';
19
+ import { DiscordOperationError, FetchError, EmptyTranscriptionError, TranscriptionError } from './errors.js';
20
20
  import { store } from './store.js';
21
21
  import { getVoiceAttachmentMatchReason, isVoiceAttachment, } from './voice-attachment.js';
22
22
  import { execAsync } from './worktrees.js';
@@ -352,7 +352,7 @@ export async function cleanupVoiceConnection(guildId) {
352
352
  }
353
353
  // Per-thread serialization is handled by ThreadSessionRuntime.enqueueIncoming()
354
354
  // via the runtime action queue; no local serialization is needed here.
355
- export async function processVoiceAttachment({ message, thread, projectDirectory, isNewThread = false, appId, currentSessionContext, lastSessionContext, agents, }) {
355
+ export async function processVoiceAttachment({ message, thread, projectDirectory, isNewThread = false, appId, currentSessionContext, lastSessionContext, agents, canForkSession = false, }) {
356
356
  const audioAttachment = Array.from(message.attachments.values()).find((attachment) => isVoiceAttachment(attachment));
357
357
  if (!audioAttachment)
358
358
  return null;
@@ -387,7 +387,12 @@ export async function processVoiceAttachment({ message, thread, projectDirectory
387
387
  transcription: deterministicConfig.transcription,
388
388
  queueMessage: deterministicConfig.queueMessage,
389
389
  agent: deterministicConfig.agent,
390
+ sessionAction: deterministicConfig.sessionAction,
390
391
  };
392
+ if (!result.transcription.trim()) {
393
+ await sendThreadMessage(thread, 'No request was transcribed. Record another voice message with the task to send.');
394
+ return new EmptyTranscriptionError();
395
+ }
391
396
  voiceLogger.log(`[DETERMINISTIC] Returning canned transcription: "${result.transcription}"${result.queueMessage ? ' [QUEUE]' : ''}`);
392
397
  if (isNewThread) {
393
398
  const threadName = result.transcription.replace(/\s+/g, ' ').trim().slice(0, 80);
@@ -402,6 +407,10 @@ export async function processVoiceAttachment({ message, thread, projectDirectory
402
407
  }
403
408
  }
404
409
  await sendThreadMessage(thread, `📝 **Transcribed message:** ${escapeDiscordFormatting(result.transcription)}`);
410
+ if (result.sessionAction === 'btw' && !canForkSession) {
411
+ await sendThreadMessage(thread, 'A contextual side chat needs an existing session. Nothing was sent to the agent. Retry from a session, or ask to create a fresh chat with this request.');
412
+ return new TranscriptionError({ reason: 'Contextual routing is unavailable without a source session' });
413
+ }
405
414
  return result;
406
415
  }
407
416
  const audioResponse = await fetch(audioAttachment.url)
@@ -450,7 +459,7 @@ export async function processVoiceAttachment({ message, thread, projectDirectory
450
459
  // No user-configured key: gateway-mode installs get one free transcription
451
460
  // via the kimaki.dev Whisper fallback before we bother the user with the
452
461
  // "add API key" dialog. This fallback has no tool-calling, so it can't
453
- // detect queueMessage/agent hints spoken in the voice message the way the
462
+ // detect queueMessage/sessionAction/agent hints spoken in the voice message the way the
454
463
  // OpenAI/Gemini path can.
455
464
  let gatewayTranscription;
456
465
  if (!transcriptionApiKey) {
@@ -496,13 +505,17 @@ export async function processVoiceAttachment({ message, thread, projectDirectory
496
505
  currentSessionContext,
497
506
  lastSessionContext,
498
507
  agents,
508
+ canForkSession,
499
509
  });
500
510
  if (transcription instanceof Error) {
511
+ if (transcription instanceof EmptyTranscriptionError) {
512
+ await sendThreadMessage(thread, 'No request was transcribed. Record another voice message with the task to send.');
513
+ return transcription;
514
+ }
501
515
  const errMsg = errore.matchError(transcription, {
502
516
  ApiKeyMissingError: (e) => e.message,
503
517
  InvalidAudioFormatError: (e) => e.message,
504
518
  TranscriptionError: (e) => e.message,
505
- EmptyTranscriptionError: (e) => e.message,
506
519
  NoResponseContentError: (e) => e.message,
507
520
  NoToolResponseError: (e) => e.message,
508
521
  Error: (e) => e.message,
@@ -539,6 +552,10 @@ export async function processVoiceAttachment({ message, thread, projectDirectory
539
552
  }
540
553
  }
541
554
  await sendThreadMessage(thread, `📝 **Transcribed message:** ${escapeDiscordFormatting(text)}`);
555
+ if (transcription.sessionAction === 'btw' && !canForkSession) {
556
+ await sendThreadMessage(thread, 'A contextual side chat needs an existing session. Nothing was sent to the agent. Retry from a session, or ask to create a fresh chat with this request.');
557
+ return new TranscriptionError({ reason: 'Contextual routing is unavailable without a source session' });
558
+ }
542
559
  if (agent) {
543
560
  await sendThreadMessage(thread, `Detected agent: ${agent}`);
544
561
  }