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
@@ -17,7 +17,7 @@ import { buildDeterministicOpencodeConfig, } from 'opencode-deterministic-provid
17
17
  import { setDataDir } from './config.js';
18
18
  import { store } from './store.js';
19
19
  import { startDiscordBot } from './discord-bot.js';
20
- import { setBotToken, initDatabase, closeDatabase, setChannelDirectory, setChannelVerbosity, } from './database.js';
20
+ import { setBotToken, initDatabase, closeDatabase, setChannelDirectory, setChannelVerbosity, getThreadSession, createPendingWorkspace, setWorkspaceReady, getThreadWorktreeOrWorkspace, } from './database.js';
21
21
  import { startHranaServer, stopHranaServer } from './hrana-server.js';
22
22
  import { initializeOpencodeForDirectory, getOpencodeClient, stopOpencodeServer } from './opencode.js';
23
23
  import { chooseLockPort, cleanupTestSessions, initTestGitRepo, waitForFooterMessage, waitForBotMessageContaining, waitForThreadState, } from './test-utils.js';
@@ -332,6 +332,120 @@ e2eTest('voice message handling', () => {
332
332
  setDeterministicTranscription(null);
333
333
  });
334
334
  // ── Test 1: Voice message in a channel creates thread + session ──
335
+ // Vitest disallows inline snapshots in test.each; register separate tests instead.
336
+ for (const sessionAction of ['btw', 'new-session']) {
337
+ test(`voice ${sessionAction} routing creates a separate thread with the correct history`, async () => {
338
+ await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID)
339
+ .sendMessage({ content: `Source history for voice ${sessionAction}` });
340
+ const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
341
+ timeout: 4_000,
342
+ predicate: (candidate) => Boolean(candidate.name?.includes(`Source history for voice ${sessionAction}`)),
343
+ });
344
+ await waitForFooterMessage({ discord, threadId: thread.id, timeout: 4_000 });
345
+ const sourceSessionId = await getThreadSession(thread.id);
346
+ await createPendingWorkspace({
347
+ threadId: thread.id,
348
+ workspaceType: 'worktree',
349
+ workspaceName: 'voice-workspace',
350
+ projectDirectory: directories.projectDirectory,
351
+ });
352
+ await setWorkspaceReady({
353
+ threadId: thread.id,
354
+ workspaceId: 'voice-workspace-id',
355
+ workspaceDirectory: directories.projectDirectory,
356
+ });
357
+ const prompt = `Explain voice routing ${sessionAction}`;
358
+ setDeterministicTranscription({ transcription: prompt, queueMessage: false, sessionAction });
359
+ await discord.thread(thread.id).user(TEST_USER_ID).sendVoiceMessage();
360
+ const confirmation = sessionAction === 'btw' ? 'Session forked!' : 'Created new session in';
361
+ const routingMessages = await waitForBotMessageContaining({
362
+ discord,
363
+ threadId: thread.id,
364
+ text: confirmation,
365
+ timeout: 4_000,
366
+ });
367
+ // OpenCode can rename the destination before discovery; follow its confirmed ID.
368
+ const targetId = routingMessages.find((message) => message.content.includes(confirmation))
369
+ ?.content.match(/<#(\d+)>/)?.[1];
370
+ if (!targetId)
371
+ throw new Error('Expected destination thread link');
372
+ const target = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
373
+ timeout: 4_000,
374
+ predicate: (candidate) => candidate.id === targetId,
375
+ });
376
+ await waitForFooterMessage({ discord, threadId: target.id, timeout: 4_000 });
377
+ const th = discord.thread(target.id);
378
+ const transcript = {
379
+ source: (await discord.thread(thread.id).text()).replaceAll(target.id, 'TARGET_THREAD'),
380
+ target: (await th.text()).replaceAll(thread.id, 'SOURCE_THREAD'),
381
+ };
382
+ if (sessionAction === 'btw') {
383
+ expect(transcript).toMatchInlineSnapshot(`
384
+ {
385
+ "source": "--- from: user (voice-tester)
386
+ Source history for voice btw
387
+ --- from: assistant (TestBot)
388
+ > *using deterministic-provider/deterministic-v2*
389
+ > session-reply
390
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@300000000000000777>
391
+ --- from: user (voice-tester)
392
+ [attachment: voice-message.ogg]
393
+ --- from: assistant (TestBot)
394
+ 🎤 Transcribing voice message...
395
+ 📝 **Transcribed message:** Explain voice routing btw
396
+ Session forked! Continue in <#TARGET_THREAD>",
397
+ "target": "--- from: assistant (TestBot)
398
+ Reusing context from <#SOURCE_THREAD> to answer prompt...
399
+ Explain voice routing btw
400
+ > session-reply
401
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@300000000000000777>",
402
+ }
403
+ `);
404
+ }
405
+ else {
406
+ expect(transcript).toMatchInlineSnapshot(`
407
+ {
408
+ "source": "--- from: user (voice-tester)
409
+ Source history for voice new-session
410
+ --- from: assistant (TestBot)
411
+ > *using deterministic-provider/deterministic-v2*
412
+ > session-reply
413
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@300000000000000777>
414
+ --- from: user (voice-tester)
415
+ [attachment: voice-message.ogg]
416
+ --- from: assistant (TestBot)
417
+ 🎤 Transcribing voice message...
418
+ 📝 **Transcribed message:** Explain voice routing new-session
419
+ Created new session in <#TARGET_THREAD>",
420
+ "target": "--- from: assistant (TestBot)
421
+ **Starting OpenCode session**
422
+ Explain voice routing new-session
423
+ > *using deterministic-provider/deterministic-v2*
424
+ > session-reply
425
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@300000000000000777>",
426
+ }
427
+ `);
428
+ }
429
+ const targetSessionId = await getThreadSession(target.id);
430
+ expect(targetSessionId).toBeTruthy();
431
+ expect(targetSessionId).not.toBe(sourceSessionId);
432
+ expect(await getThreadWorktreeOrWorkspace(target.id)).toMatchObject({
433
+ status: 'ready',
434
+ workspace_type: 'worktree',
435
+ workspace_name: 'voice-workspace',
436
+ workspace_id: 'voice-workspace-id',
437
+ workspace_directory: directories.projectDirectory,
438
+ });
439
+ const client = getOpencodeClientForTest(directories.projectDirectory);
440
+ const targetMessages = await client.session.messages({ sessionID: targetSessionId });
441
+ const targetTexts = getUserTexts(targetMessages.data ?? []).join('\n');
442
+ expect(targetTexts).toContain(prompt);
443
+ expect(targetTexts).toContain(`Voice message transcription from Discord user:\n${prompt}`);
444
+ expect(targetTexts.includes(`Source history for voice ${sessionAction}`)).toBe(sessionAction === 'btw');
445
+ const sourceMessages = await client.session.messages({ sessionID: sourceSessionId });
446
+ expect(getUserTexts(sourceMessages.data ?? []).join('\n')).not.toContain(prompt);
447
+ });
448
+ }
335
449
  test('voice message in channel creates thread and starts session', async () => {
336
450
  setDeterministicTranscription({
337
451
  transcription: 'Fix the login bug in auth.ts',
@@ -394,9 +508,9 @@ e2eTest('voice message handling', () => {
394
508
  --- from: assistant (TestBot)
395
509
  🎤 Transcribing voice message...
396
510
  📝 **Transcribed message:** Fix the login bug in auth.ts
397
- *using deterministic-provider/deterministic-v2*
398
- ⬥ session-reply
399
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
511
+ > *using deterministic-provider/deterministic-v2*
512
+ > session-reply
513
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@300000000000000777>"
400
514
  `);
401
515
  expect(finalState.sessionId).toBeDefined();
402
516
  // Verify OpenCode session received the transcribed voice message as a prompt
@@ -484,9 +598,9 @@ e2eTest('voice message handling', () => {
484
598
  Voice transcription requires an API key (OpenAI or Gemini). Set one to enable voice message transcription.
485
599
  Gemini API key saved. Retrying the original voice message.
486
600
  📝 **Transcribed message:** Resume the original voice note
487
- *using deterministic-provider/deterministic-v2*
488
- ⬥ session-reply
489
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
601
+ > *using deterministic-provider/deterministic-v2*
602
+ > session-reply
603
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@300000000000000777>"
490
604
  `);
491
605
  const finalState = getThreadState(thread.id);
492
606
  expect(finalState?.sessionId).toBeDefined();
@@ -548,9 +662,9 @@ e2eTest('voice message handling', () => {
548
662
  --- from: assistant (TestBot)
549
663
  🎤 Transcribing voice message...
550
664
  📝 **Transcribed message:** Investigate the missing content type path
551
- *using deterministic-provider/deterministic-v2*
552
- ⬥ session-reply
553
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
665
+ > *using deterministic-provider/deterministic-v2*
666
+ > session-reply
667
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@300000000000000777>"
554
668
  `);
555
669
  const messages = await waitForSessionMessages({
556
670
  projectDirectory: directories.projectDirectory,
@@ -637,16 +751,16 @@ e2eTest('voice message handling', () => {
637
751
  "--- from: user (voice-tester)
638
752
  FAST_RESPONSE_MARKER initial setup
639
753
  --- from: assistant (TestBot)
640
- *using deterministic-provider/deterministic-v2*
641
- ⬥ fast-response-done
642
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
754
+ > *using deterministic-provider/deterministic-v2*
755
+ > fast-response-done
756
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@300000000000000777>
643
757
  --- from: user (voice-tester)
644
758
  [attachment: voice-message.ogg]
645
759
  --- from: assistant (TestBot)
646
760
  🎤 Transcribing voice message...
647
761
  📝 **Transcribed message:** Add error handling to the parser
648
- ⬥ session-reply
649
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
762
+ > session-reply
763
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@300000000000000777>"
650
764
  `);
651
765
  expect(finalState?.sessionId).toBeDefined();
652
766
  if (!finalState?.sessionId) {
@@ -848,19 +962,19 @@ e2eTest('voice message handling', () => {
848
962
  "--- from: user (voice-tester)
849
963
  SLOW_RESPONSE_MARKER start queued task
850
964
  --- from: assistant (TestBot)
851
- *using deterministic-provider/deterministic-v2*
965
+ > *using deterministic-provider/deterministic-v2*
852
966
  --- from: user (voice-tester)
853
967
  [attachment: voice-message.ogg]
854
968
  --- from: assistant (TestBot)
855
969
  🎤 Transcribing voice message...
856
970
  📝 **Transcribed message:** Queue this task for later
857
971
  Queued at position 1. Edit or delete your message to update the queue
858
- ⬥ slow-response-done
859
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
972
+ > slow-response-done
973
+ > *project ⋅ main ⋅ 2s ⋅ 0% ⋅ deterministic-v2*
860
974
  » **voice-tester:** Voice message transcription from Discord user:
861
975
  Queue this task for later
862
- ⬥ session-reply
863
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
976
+ > session-reply
977
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@300000000000000777>"
864
978
  `);
865
979
  expect(finalState.queueItems.length).toBe(0);
866
980
  // Verify the OpenCode session processed BOTH prompts sequentially:
@@ -957,16 +1071,16 @@ e2eTest('voice message handling', () => {
957
1071
  "--- from: user (voice-tester)
958
1072
  FAST_RESPONSE_MARKER quick task
959
1073
  --- from: assistant (TestBot)
960
- *using deterministic-provider/deterministic-v2*
961
- ⬥ fast-response-done
962
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
1074
+ > *using deterministic-provider/deterministic-v2*
1075
+ > fast-response-done
1076
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@300000000000000777>
963
1077
  --- from: user (voice-tester)
964
1078
  [attachment: voice-message.ogg]
965
1079
  --- from: assistant (TestBot)
966
1080
  🎤 Transcribing voice message...
967
1081
  📝 **Transcribed message:** Delayed transcription result
968
- ⬥ session-reply
969
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
1082
+ > session-reply
1083
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@300000000000000777>"
970
1084
  `);
971
1085
  expect(finalState.sessionId).toBeDefined();
972
1086
  expect(finalState.queueItems.length).toBe(0);
@@ -1075,16 +1189,16 @@ e2eTest('voice message handling', () => {
1075
1189
  "--- from: user (voice-tester)
1076
1190
  FAST_RESPONSE_MARKER fast before queued voice
1077
1191
  --- from: assistant (TestBot)
1078
- *using deterministic-provider/deterministic-v2*
1079
- ⬥ fast-response-done
1080
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
1192
+ > *using deterministic-provider/deterministic-v2*
1193
+ > fast-response-done
1194
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@300000000000000777>
1081
1195
  --- from: user (voice-tester)
1082
1196
  [attachment: voice-message.ogg]
1083
1197
  --- from: assistant (TestBot)
1084
1198
  🎤 Transcribing voice message...
1085
1199
  📝 **Transcribed message:** Queued voice after idle
1086
- ⬥ session-reply
1087
- *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
1200
+ > session-reply
1201
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@300000000000000777>"
1088
1202
  `);
1089
1203
  expect(finalState.sessionId).toBeDefined();
1090
1204
  expect(finalState.queueItems.length).toBe(0);
@@ -0,0 +1,195 @@
1
+ // Cover long routed prompts and rejected voice routes without an existing session.
2
+ import { afterEach, describe, expect, test } from 'vitest';
3
+ import { ChannelType } from 'discord.js';
4
+ import { setupQueueAdvancedSuite, TEST_USER_ID } from './queue-advanced-e2e-setup.js';
5
+ import { waitForBotMessageContaining, waitForFooterMessage } from './test-utils.js';
6
+ import { getThreadSession } from './database.js';
7
+ import { getOpencodeClient } from './opencode.js';
8
+ import { store } from './store.js';
9
+ const TEXT_CHANNEL_ID = '200000000000001084';
10
+ describe('voice routing input boundaries', () => {
11
+ const ctx = setupQueueAdvancedSuite({
12
+ channelId: TEXT_CHANNEL_ID,
13
+ channelName: 'voice-routing-input',
14
+ dirName: 'voice-routing-input',
15
+ username: 'voice-input-tester',
16
+ });
17
+ afterEach(() => {
18
+ store.setState({ test: { deterministicTranscription: null } });
19
+ });
20
+ test('fresh voice route splits display and preserves the full model prompt', async () => {
21
+ const channel = await ctx.botClient.channels.fetch(TEXT_CHANNEL_ID);
22
+ if (channel?.type !== ChannelType.GuildText)
23
+ throw new Error('Expected text channel');
24
+ const source = await channel.threads.create({ name: 'Long voice source' });
25
+ const prompt = `Long voice request ${'Z'.repeat(2100)} END_OF_REQUEST`;
26
+ const caption = 'Keep the existing public API unchanged.';
27
+ store.setState({
28
+ test: {
29
+ deterministicTranscription: {
30
+ transcription: prompt,
31
+ queueMessage: false,
32
+ sessionAction: 'new-session',
33
+ },
34
+ },
35
+ });
36
+ await ctx.discord.thread(source.id).user(TEST_USER_ID).sendVoiceMessage({ content: caption });
37
+ const routingMessages = await waitForBotMessageContaining({
38
+ discord: ctx.discord,
39
+ threadId: source.id,
40
+ text: 'Created new session in',
41
+ timeout: 4_000,
42
+ });
43
+ const targetId = routingMessages.find((message) => message.content.includes('Created new session in'))
44
+ ?.content.match(/<#(\d+)>/)?.[1];
45
+ if (!targetId)
46
+ throw new Error('Expected destination thread link');
47
+ const target = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
48
+ timeout: 4_000,
49
+ predicate: (thread) => thread.id === targetId,
50
+ });
51
+ await waitForFooterMessage({ discord: ctx.discord, threadId: target.id, timeout: 4_000 });
52
+ expect((await ctx.discord.channel(TEXT_CHANNEL_ID).text()).replace(/Z+/g, '[DETAILS]'))
53
+ .toMatchInlineSnapshot(`
54
+ "--- from: assistant (TestBot)
55
+ **Starting OpenCode session**
56
+ Keep the existing public API unchanged.
57
+
58
+ "
59
+ `);
60
+ expect((await ctx.discord.thread(target.id).text()).replace(/Z+/g, '[DETAILS]'))
61
+ .toMatchInlineSnapshot(`
62
+ "--- from: assistant (TestBot)
63
+ **Starting OpenCode session**
64
+ Keep the existing public API unchanged.
65
+
66
+
67
+ Long voice request [DETAILS]
68
+ [DETAILS] END_OF_REQUEST
69
+ > *using deterministic-provider/deterministic-v2*
70
+ > ok
71
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>"
72
+ `);
73
+ const channelMessages = await ctx.discord.channel(TEXT_CHANNEL_ID).getMessages();
74
+ expect(channelMessages).toHaveLength(1);
75
+ expect(channelMessages.every((message) => message.content.length <= 2000)).toBe(true);
76
+ const threadText = await ctx.discord.thread(target.id).text();
77
+ expect(threadText).toContain(caption);
78
+ expect(threadText.replace(/\s/g, '')).toContain(prompt.replace(/\s/g, ''));
79
+ expect(threadText.match(/Z/g)).toHaveLength(2100);
80
+ const sessionId = await getThreadSession(target.id);
81
+ if (!sessionId)
82
+ throw new Error('Expected target session');
83
+ const client = getOpencodeClient(ctx.directories.projectDirectory);
84
+ if (!client)
85
+ throw new Error('Expected OpenCode client');
86
+ const messages = await client.session.messages({ sessionID: sessionId });
87
+ const userTexts = messages.data
88
+ ?.filter((message) => message.info.role === 'user')
89
+ .flatMap((message) => message.parts.flatMap((part) => (part.type === 'text' ? [part.text] : [])));
90
+ expect(userTexts?.join('\n')).toContain(`Voice message transcription from Discord user:\n${prompt}`);
91
+ expect(userTexts?.join('\n')).toContain(caption);
92
+ expect(await getThreadSession(source.id)).toBeFalsy();
93
+ });
94
+ test('unsupported contextual route keeps the transcript and never dispatches raw text', async () => {
95
+ const channel = await ctx.botClient.channels.fetch(TEXT_CHANNEL_ID);
96
+ if (channel?.type !== ChannelType.GuildText)
97
+ throw new Error('Expected text channel');
98
+ const source = await channel.threads.create({ name: 'Unsupported voice source' });
99
+ const th = ctx.discord.thread(source.id);
100
+ store.setState({
101
+ test: {
102
+ deterministicTranscription: {
103
+ transcription: 'Explain the parser in a contextual side chat',
104
+ queueMessage: false,
105
+ sessionAction: 'btw',
106
+ },
107
+ },
108
+ });
109
+ await th.user(TEST_USER_ID).sendVoiceMessage({ content: 'Do not dispatch this raw text' });
110
+ await waitForBotMessageContaining({
111
+ discord: ctx.discord,
112
+ threadId: source.id,
113
+ text: 'Nothing was sent to the agent',
114
+ timeout: 4_000,
115
+ });
116
+ expect(await th.text()).toMatchInlineSnapshot(`
117
+ "--- from: user (voice-input-tester)
118
+ Do not dispatch this raw text
119
+ [attachment: voice-message.ogg]
120
+ --- from: assistant (TestBot)
121
+ 🎤 Transcribing voice message...
122
+ 📝 **Transcribed message:** Explain the parser in a contextual side chat
123
+ 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."
124
+ `);
125
+ for (let index = 0; index < 10; index++) {
126
+ await new Promise((resolve) => setTimeout(resolve, 20));
127
+ expect(await getThreadSession(source.id)).toBeFalsy();
128
+ }
129
+ });
130
+ test('routing-only audio does not start a junk turn', async () => {
131
+ const channel = await ctx.botClient.channels.fetch(TEXT_CHANNEL_ID);
132
+ if (channel?.type !== ChannelType.GuildText)
133
+ throw new Error('Expected text channel');
134
+ const source = await channel.threads.create({ name: 'Empty voice source' });
135
+ const th = ctx.discord.thread(source.id);
136
+ store.setState({
137
+ test: {
138
+ deterministicTranscription: {
139
+ transcription: '',
140
+ queueMessage: false,
141
+ sessionAction: 'new-session',
142
+ },
143
+ },
144
+ });
145
+ await th.user(TEST_USER_ID).sendVoiceMessage({ content: 'Create a new chat' });
146
+ await waitForBotMessageContaining({
147
+ discord: ctx.discord,
148
+ threadId: source.id,
149
+ text: 'No request was transcribed',
150
+ timeout: 4_000,
151
+ });
152
+ expect(await th.text()).toMatchInlineSnapshot(`
153
+ "--- from: user (voice-input-tester)
154
+ Create a new chat
155
+ [attachment: voice-message.ogg]
156
+ --- from: assistant (TestBot)
157
+ 🎤 Transcribing voice message...
158
+ No request was transcribed. Record another voice message with the task to send."
159
+ `);
160
+ for (let index = 0; index < 10; index++) {
161
+ await new Promise((resolve) => setTimeout(resolve, 20));
162
+ expect(await getThreadSession(source.id)).toBeFalsy();
163
+ }
164
+ });
165
+ test('/new-session confirms the destination and sends the prompt', async () => {
166
+ const channel = await ctx.botClient.channels.fetch(TEXT_CHANNEL_ID);
167
+ if (channel?.type !== ChannelType.GuildText)
168
+ throw new Error('Expected text channel');
169
+ const source = await channel.threads.create({ name: 'Slash source' });
170
+ const th = ctx.discord.thread(source.id);
171
+ const interaction = await th.user(TEST_USER_ID).runSlashCommand({
172
+ name: 'new-session',
173
+ options: [{ name: 'prompt', type: 3, value: 'Slash fresh request' }],
174
+ });
175
+ await th.waitForInteractionAck({ interactionId: interaction.id, timeout: 4_000 });
176
+ const target = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
177
+ timeout: 4_000,
178
+ predicate: (thread) => thread.name === 'Slash fresh request',
179
+ });
180
+ await waitForFooterMessage({ discord: ctx.discord, threadId: target.id, timeout: 4_000 });
181
+ expect((await th.text({ showInteractions: true })).replaceAll(target.id, 'TARGET_THREAD')).toMatchInlineSnapshot(`
182
+ "[user interaction]
183
+ --- from: assistant (TestBot)
184
+ Created new session in <#TARGET_THREAD>"
185
+ `);
186
+ expect(await ctx.discord.thread(target.id).text()).toMatchInlineSnapshot(`
187
+ "--- from: assistant (TestBot)
188
+ **Starting OpenCode session**
189
+ Slash fresh request
190
+ > *using deterministic-provider/deterministic-v2*
191
+ > ok
192
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>"
193
+ `);
194
+ });
195
+ });
@@ -0,0 +1,125 @@
1
+ // Voice routes must leave the source question answerable without aborting its run.
2
+ import { afterEach, describe, expect, test } from 'vitest';
3
+ import { setupQueueAdvancedSuite, TEST_USER_ID } from './queue-advanced-e2e-setup.js';
4
+ import { waitForBotMessageContaining, waitForFooterMessage } from './test-utils.js';
5
+ import { store } from './store.js';
6
+ const TEXT_CHANNEL_ID = '200000000000001083';
7
+ describe('voice routing preserves source questions', () => {
8
+ const ctx = setupQueueAdvancedSuite({
9
+ channelId: TEXT_CHANNEL_ID,
10
+ channelName: 'voice-routing-question',
11
+ dirName: 'voice-routing-question',
12
+ username: 'voice-question-tester',
13
+ });
14
+ afterEach(() => {
15
+ store.setState({ test: { deterministicTranscription: null } });
16
+ });
17
+ // Separate registrations permit distinct inline snapshots with Vitest 3.
18
+ for (const sessionAction of ['btw', 'new-session']) {
19
+ test(`${sessionAction} preserves the source dropdown`, async () => {
20
+ const marker = `QUESTION_SELECT_QUEUE_MARKER voice ${sessionAction}`;
21
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({ content: marker });
22
+ const source = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
23
+ timeout: 4_000,
24
+ predicate: (thread) => thread.name === marker,
25
+ });
26
+ const th = ctx.discord.thread(source.id);
27
+ const messages = await waitForBotMessageContaining({
28
+ discord: ctx.discord,
29
+ threadId: source.id,
30
+ text: 'How to proceed?',
31
+ timeout: 4_000,
32
+ });
33
+ const question = messages.find((message) => message.content.includes('How to proceed?'));
34
+ if (!question)
35
+ throw new Error('Expected question message');
36
+ const customId = JSON.stringify(question.components).match(/"custom_id":"(ask_question:[^"]+)"/)?.[1];
37
+ if (!customId)
38
+ throw new Error('Expected question dropdown');
39
+ const prompt = `Explain routing without changing the source ${sessionAction}`;
40
+ store.setState({
41
+ test: {
42
+ deterministicTranscription: {
43
+ transcription: prompt,
44
+ queueMessage: false,
45
+ sessionAction,
46
+ },
47
+ },
48
+ });
49
+ await th.user(TEST_USER_ID).sendVoiceMessage();
50
+ const confirmation = sessionAction === 'btw' ? 'Session forked!' : 'Created new session in';
51
+ const routingMessages = await waitForBotMessageContaining({
52
+ discord: ctx.discord,
53
+ threadId: source.id,
54
+ text: confirmation,
55
+ timeout: 4_000,
56
+ });
57
+ const targetId = routingMessages.find((message) => message.content.includes(confirmation))
58
+ ?.content.match(/<#(\d+)>/)?.[1];
59
+ if (!targetId)
60
+ throw new Error('Expected destination thread link');
61
+ const target = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
62
+ timeout: 4_000,
63
+ predicate: (thread) => thread.id === targetId,
64
+ });
65
+ await waitForFooterMessage({ discord: ctx.discord, threadId: target.id, timeout: 4_000 });
66
+ const interaction = await th.user(TEST_USER_ID).selectMenu({
67
+ messageId: question.id,
68
+ customId,
69
+ values: ['0'],
70
+ });
71
+ await th.waitForInteractionAck({ interactionId: interaction.id, timeout: 4_000 });
72
+ await waitForBotMessageContaining({
73
+ discord: ctx.discord,
74
+ threadId: source.id,
75
+ text: '» **voice-question-tester:** Alpha',
76
+ timeout: 4_000,
77
+ });
78
+ await waitForFooterMessage({ discord: ctx.discord, threadId: source.id, timeout: 4_000 });
79
+ const transcript = (await th.text({ showInteractions: true })).replaceAll(target.id, 'TARGET_THREAD');
80
+ if (sessionAction === 'btw') {
81
+ expect(transcript).toMatchInlineSnapshot(`
82
+ "--- from: user (voice-question-tester)
83
+ QUESTION_SELECT_QUEUE_MARKER voice btw
84
+ --- from: assistant (TestBot)
85
+ > *using deterministic-provider/deterministic-v2*
86
+ **Select action**
87
+ How to proceed?
88
+ ✓ _Alpha_
89
+ --- from: user (voice-question-tester)
90
+ [attachment: voice-message.ogg]
91
+ --- from: assistant (TestBot)
92
+ 🎤 Transcribing voice message...
93
+ 📝 **Transcribed message:** Explain routing without changing the source btw
94
+ Session forked! Continue in <#TARGET_THREAD>
95
+ [user selects dropdown: 0]
96
+ » **voice-question-tester:** Alpha
97
+ > tool done
98
+ > *project ⋅ main ⋅ 1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>"
99
+ `);
100
+ }
101
+ else {
102
+ expect(transcript).toMatchInlineSnapshot(`
103
+ "--- from: user (voice-question-tester)
104
+ QUESTION_SELECT_QUEUE_MARKER voice new-session
105
+ --- from: assistant (TestBot)
106
+ > *using deterministic-provider/deterministic-v2*
107
+ **Select action**
108
+ How to proceed?
109
+ ✓ _Alpha_
110
+ --- from: user (voice-question-tester)
111
+ [attachment: voice-message.ogg]
112
+ --- from: assistant (TestBot)
113
+ 🎤 Transcribing voice message...
114
+ 📝 **Transcribed message:** Explain routing without changing the source new-session
115
+ Created new session in <#TARGET_THREAD>
116
+ [user selects dropdown: 0]
117
+ » **voice-question-tester:** Alpha
118
+ > tool done
119
+ > *project ⋅ main ⋅ <1s ⋅ 0% ⋅ deterministic-v2* <@200000000000000991>"
120
+ `);
121
+ }
122
+ expect(transcript).not.toContain('expired');
123
+ });
124
+ }
125
+ });