kimaki 0.30.1 → 1.0.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 (987) hide show
  1. package/dist/agent-ui.d.ts +33 -0
  2. package/dist/agent-ui.e2e.test.d.ts +1 -0
  3. package/dist/agent-ui.e2e.test.js +140 -0
  4. package/dist/agent-ui.e2e.test.js.map +1 -0
  5. package/dist/agent-ui.js +199 -0
  6. package/dist/agent-ui.js.map +1 -0
  7. package/dist/analytics.d.ts +42 -0
  8. package/dist/analytics.e2e.test.d.ts +1 -0
  9. package/dist/analytics.e2e.test.js +112 -0
  10. package/dist/analytics.e2e.test.js.map +1 -0
  11. package/dist/analytics.js +162 -221
  12. package/dist/analytics.js.map +1 -0
  13. package/dist/analytics.test.d.ts +1 -0
  14. package/dist/analytics.test.js +121 -157
  15. package/dist/analytics.test.js.map +1 -0
  16. package/dist/autostart.d.ts +7 -0
  17. package/dist/autostart.js +75 -0
  18. package/dist/autostart.js.map +1 -0
  19. package/dist/bot.d.ts +90 -0
  20. package/dist/bot.js +120 -0
  21. package/dist/bot.js.map +1 -0
  22. package/dist/btw.e2e.test.d.ts +1 -0
  23. package/dist/btw.e2e.test.js +102 -0
  24. package/dist/btw.e2e.test.js.map +1 -0
  25. package/dist/catalog-refresh.e2e.test.d.ts +1 -0
  26. package/dist/catalog-refresh.e2e.test.js +90 -0
  27. package/dist/catalog-refresh.e2e.test.js.map +1 -0
  28. package/dist/cli/bot.d.ts +5 -0
  29. package/dist/cli/bot.js +427 -0
  30. package/dist/cli/bot.js.map +1 -0
  31. package/dist/cli/discord.d.ts +3 -0
  32. package/dist/cli/discord.js +118 -0
  33. package/dist/cli/discord.js.map +1 -0
  34. package/dist/cli/project.d.ts +5 -0
  35. package/dist/cli/project.js +143 -0
  36. package/dist/cli/project.js.map +1 -0
  37. package/dist/cli/schedule.d.ts +3 -0
  38. package/dist/cli/schedule.js +156 -0
  39. package/dist/cli/schedule.js.map +1 -0
  40. package/dist/cli/session.d.ts +4 -0
  41. package/dist/cli/session.js +281 -0
  42. package/dist/cli/session.js.map +1 -0
  43. package/dist/cli/shared.d.ts +55 -0
  44. package/dist/cli/shared.js +138 -0
  45. package/dist/cli/shared.js.map +1 -0
  46. package/dist/cli-discord.e2e.test.d.ts +1 -0
  47. package/dist/cli-discord.e2e.test.js +64 -0
  48. package/dist/cli-discord.e2e.test.js.map +1 -0
  49. package/dist/cli-lock.e2e.test.d.ts +1 -0
  50. package/dist/cli-lock.e2e.test.js +520 -0
  51. package/dist/cli-lock.e2e.test.js.map +1 -0
  52. package/dist/cli-read.e2e.test.d.ts +1 -0
  53. package/dist/cli-read.e2e.test.js +81 -0
  54. package/dist/cli-read.e2e.test.js.map +1 -0
  55. package/dist/cli-sessions.e2e.test.d.ts +1 -0
  56. package/dist/cli-sessions.e2e.test.js +116 -0
  57. package/dist/cli-sessions.e2e.test.js.map +1 -0
  58. package/dist/cli.d.ts +2 -0
  59. package/dist/cli.js +34 -263
  60. package/dist/cli.js.map +1 -0
  61. package/dist/commands/login-commands.d.ts +59 -0
  62. package/dist/commands/login-commands.js +341 -0
  63. package/dist/commands/login-commands.js.map +1 -0
  64. package/dist/commands/preference-commands.d.ts +44 -0
  65. package/dist/commands/preference-commands.js +551 -0
  66. package/dist/commands/preference-commands.js.map +1 -0
  67. package/dist/commands/session-commands.d.ts +8 -0
  68. package/dist/commands/session-commands.js +267 -0
  69. package/dist/commands/session-commands.js.map +1 -0
  70. package/dist/commands/thread-commands.d.ts +16 -0
  71. package/dist/commands/thread-commands.js +285 -0
  72. package/dist/commands/thread-commands.js.map +1 -0
  73. package/dist/commands/worktree-commands.d.ts +41 -0
  74. package/dist/commands/worktree-commands.js +279 -0
  75. package/dist/commands/worktree-commands.js.map +1 -0
  76. package/dist/commands.e2e.test.d.ts +1 -0
  77. package/dist/commands.e2e.test.js +255 -0
  78. package/dist/commands.e2e.test.js.map +1 -0
  79. package/dist/credentials.d.ts +84 -0
  80. package/dist/credentials.js +320 -0
  81. package/dist/credentials.js.map +1 -0
  82. package/dist/db.d.ts +1532 -0
  83. package/dist/db.js +57 -245
  84. package/dist/db.js.map +1 -0
  85. package/dist/db.test.d.ts +1 -0
  86. package/dist/db.test.js +352 -673
  87. package/dist/db.test.js.map +1 -0
  88. package/dist/duration.d.ts +2 -0
  89. package/dist/duration.js +18 -0
  90. package/dist/duration.js.map +1 -0
  91. package/dist/effects.d.ts +15 -0
  92. package/dist/effects.js +291 -0
  93. package/dist/effects.js.map +1 -0
  94. package/dist/errors.d.ts +38 -0
  95. package/dist/errors.js +40 -170
  96. package/dist/errors.js.map +1 -0
  97. package/dist/event-loop.d.ts +40 -0
  98. package/dist/event-loop.js +414 -0
  99. package/dist/event-loop.js.map +1 -0
  100. package/dist/file-edit-log.d.ts +41 -0
  101. package/dist/file-edit-log.js +122 -164
  102. package/dist/file-edit-log.js.map +1 -0
  103. package/dist/file-edit-log.test.d.ts +1 -0
  104. package/dist/file-edit-log.test.js +68 -244
  105. package/dist/file-edit-log.test.js.map +1 -0
  106. package/dist/format-parts.d.ts +137 -0
  107. package/dist/format-parts.js +300 -0
  108. package/dist/format-parts.js.map +1 -0
  109. package/dist/format-parts.test.d.ts +1 -0
  110. package/dist/format-parts.test.js +144 -0
  111. package/dist/format-parts.test.js.map +1 -0
  112. package/dist/gateway-replay.e2e.test.d.ts +1 -0
  113. package/dist/gateway-replay.e2e.test.js +95 -0
  114. package/dist/gateway-replay.e2e.test.js.map +1 -0
  115. package/dist/harness.e2e.test.d.ts +1 -0
  116. package/dist/harness.e2e.test.js +107 -0
  117. package/dist/harness.e2e.test.js.map +1 -0
  118. package/dist/hello-thread.e2e.test.d.ts +1 -0
  119. package/dist/hello-thread.e2e.test.js +194 -0
  120. package/dist/hello-thread.e2e.test.js.map +1 -0
  121. package/dist/ingress.d.ts +7 -0
  122. package/dist/ingress.js +337 -0
  123. package/dist/ingress.js.map +1 -0
  124. package/dist/interaction-context.d.ts +45 -0
  125. package/dist/interaction-context.js +77 -0
  126. package/dist/interaction-context.js.map +1 -0
  127. package/dist/interrupt.e2e.test.d.ts +1 -0
  128. package/dist/interrupt.e2e.test.js +124 -0
  129. package/dist/interrupt.e2e.test.js.map +1 -0
  130. package/dist/legacy.e2e.test.d.ts +1 -0
  131. package/dist/legacy.e2e.test.js +85 -0
  132. package/dist/legacy.e2e.test.js.map +1 -0
  133. package/dist/live-call.d.ts +51 -0
  134. package/dist/live-call.js +207 -0
  135. package/dist/live-call.js.map +1 -0
  136. package/dist/lock-routes.d.ts +812 -0
  137. package/dist/lock-routes.js +635 -0
  138. package/dist/lock-routes.js.map +1 -0
  139. package/dist/lock-server.d.ts +36 -0
  140. package/dist/lock-server.js +223 -0
  141. package/dist/lock-server.js.map +1 -0
  142. package/dist/logger.d.ts +13 -0
  143. package/dist/logger.js +112 -162
  144. package/dist/logger.js.map +1 -0
  145. package/dist/login.e2e.test.d.ts +1 -0
  146. package/dist/login.e2e.test.js +83 -0
  147. package/dist/login.e2e.test.js.map +1 -0
  148. package/dist/main.d.ts +32 -0
  149. package/dist/main.js +190 -0
  150. package/dist/main.js.map +1 -0
  151. package/dist/markdown/components.d.ts +10 -0
  152. package/dist/markdown/components.js +77 -0
  153. package/dist/markdown/components.js.map +1 -0
  154. package/dist/markdown/markdown.test.d.ts +1 -0
  155. package/dist/markdown/markdown.test.js +393 -0
  156. package/dist/markdown/markdown.test.js.map +1 -0
  157. package/dist/markdown/render-markdown.d.ts +30 -0
  158. package/dist/markdown/render-markdown.js +265 -0
  159. package/dist/markdown/render-markdown.js.map +1 -0
  160. package/dist/message-tool.e2e.test.d.ts +1 -0
  161. package/dist/message-tool.e2e.test.js +39 -0
  162. package/dist/message-tool.e2e.test.js.map +1 -0
  163. package/dist/migrations.d.ts +8 -0
  164. package/dist/migrations.js +187 -0
  165. package/dist/migrations.js.map +1 -0
  166. package/dist/onboarding-cli.e2e.test.d.ts +1 -0
  167. package/dist/onboarding-cli.e2e.test.js +178 -0
  168. package/dist/onboarding-cli.e2e.test.js.map +1 -0
  169. package/dist/onboarding.d.ts +36 -0
  170. package/dist/onboarding.e2e.test.d.ts +1 -0
  171. package/dist/onboarding.e2e.test.js +148 -0
  172. package/dist/onboarding.e2e.test.js.map +1 -0
  173. package/dist/onboarding.js +261 -0
  174. package/dist/onboarding.js.map +1 -0
  175. package/dist/opencode-server.d.ts +54 -0
  176. package/dist/opencode-server.js +413 -0
  177. package/dist/opencode-server.js.map +1 -0
  178. package/dist/opencode-server.test.d.ts +1 -0
  179. package/dist/opencode-server.test.js +21 -0
  180. package/dist/opencode-server.test.js.map +1 -0
  181. package/dist/permissions.d.ts +31 -0
  182. package/dist/permissions.e2e.test.d.ts +1 -0
  183. package/dist/permissions.e2e.test.js +166 -0
  184. package/dist/permissions.e2e.test.js.map +1 -0
  185. package/dist/permissions.js +95 -0
  186. package/dist/permissions.js.map +1 -0
  187. package/dist/plugin/anthropic/index.d.ts +13 -0
  188. package/dist/plugin/anthropic/index.js +201 -0
  189. package/dist/plugin/anthropic/index.js.map +1 -0
  190. package/dist/plugin/anthropic/index.test.d.ts +1 -0
  191. package/dist/plugin/anthropic/index.test.js +39 -0
  192. package/dist/plugin/anthropic/index.test.js.map +1 -0
  193. package/dist/plugin/index.d.ts +3 -0
  194. package/dist/plugin/index.js +106 -0
  195. package/dist/plugin/index.js.map +1 -0
  196. package/dist/plugin/session-id/index.d.ts +3 -0
  197. package/dist/plugin/session-id/index.js +18 -0
  198. package/dist/plugin/session-id/index.js.map +1 -0
  199. package/dist/plugin.e2e.test.d.ts +1 -0
  200. package/dist/plugin.e2e.test.js +87 -0
  201. package/dist/plugin.e2e.test.js.map +1 -0
  202. package/dist/preference-commands.e2e.test.d.ts +1 -0
  203. package/dist/preference-commands.e2e.test.js +555 -0
  204. package/dist/preference-commands.e2e.test.js.map +1 -0
  205. package/dist/probe-live.tmp.d.ts +1 -0
  206. package/dist/probe-live.tmp.js +82 -0
  207. package/dist/probe-live.tmp.js.map +1 -0
  208. package/dist/project.d.ts +58 -0
  209. package/dist/project.js +169 -0
  210. package/dist/project.js.map +1 -0
  211. package/dist/prompt.d.ts +112 -0
  212. package/dist/prompt.js +578 -0
  213. package/dist/prompt.js.map +1 -0
  214. package/dist/questions.d.ts +59 -0
  215. package/dist/questions.e2e.test.d.ts +1 -0
  216. package/dist/questions.e2e.test.js +290 -0
  217. package/dist/questions.e2e.test.js.map +1 -0
  218. package/dist/questions.js +231 -0
  219. package/dist/questions.js.map +1 -0
  220. package/dist/queue.d.ts +45 -0
  221. package/dist/queue.e2e.test.d.ts +1 -0
  222. package/dist/queue.e2e.test.js +220 -0
  223. package/dist/queue.e2e.test.js.map +1 -0
  224. package/dist/queue.js +132 -0
  225. package/dist/queue.js.map +1 -0
  226. package/dist/remote-send.d.ts +32 -0
  227. package/dist/remote-send.e2e.test.d.ts +1 -0
  228. package/dist/remote-send.e2e.test.js +89 -0
  229. package/dist/remote-send.e2e.test.js.map +1 -0
  230. package/dist/remote-send.js +105 -0
  231. package/dist/remote-send.js.map +1 -0
  232. package/dist/renderer.e2e.test.d.ts +1 -0
  233. package/dist/renderer.e2e.test.js +357 -0
  234. package/dist/renderer.e2e.test.js.map +1 -0
  235. package/dist/restart.e2e.test.d.ts +1 -0
  236. package/dist/restart.e2e.test.js +68 -0
  237. package/dist/restart.e2e.test.js.map +1 -0
  238. package/dist/routes.d.ts +24 -0
  239. package/dist/routes.js +42 -0
  240. package/dist/routes.js.map +1 -0
  241. package/dist/routes.test.d.ts +1 -0
  242. package/dist/routes.test.js +79 -0
  243. package/dist/routes.test.js.map +1 -0
  244. package/dist/routines.e2e.test.d.ts +1 -0
  245. package/dist/routines.e2e.test.js +188 -0
  246. package/dist/routines.e2e.test.js.map +1 -0
  247. package/dist/scheduler.d.ts +116 -0
  248. package/dist/scheduler.e2e.test.d.ts +1 -0
  249. package/dist/scheduler.e2e.test.js +163 -0
  250. package/dist/scheduler.e2e.test.js.map +1 -0
  251. package/dist/scheduler.js +602 -0
  252. package/dist/scheduler.js.map +1 -0
  253. package/dist/scheduler.test.d.ts +1 -0
  254. package/dist/scheduler.test.js +180 -0
  255. package/dist/scheduler.test.js.map +1 -0
  256. package/dist/schema-sql.d.ts +1 -0
  257. package/dist/schema-sql.js +3 -0
  258. package/dist/schema-sql.js.map +1 -0
  259. package/dist/schema.d.ts +2968 -0
  260. package/dist/schema.js +95 -252
  261. package/dist/schema.js.map +1 -0
  262. package/dist/session-commands.e2e.test.d.ts +1 -0
  263. package/dist/session-commands.e2e.test.js +285 -0
  264. package/dist/session-commands.e2e.test.js.map +1 -0
  265. package/dist/session-events.d.ts +55 -0
  266. package/dist/session-events.js +190 -0
  267. package/dist/session-events.js.map +1 -0
  268. package/dist/session-ops.e2e.test.d.ts +1 -0
  269. package/dist/session-ops.e2e.test.js +114 -0
  270. package/dist/session-ops.e2e.test.js.map +1 -0
  271. package/dist/sessions.d.ts +121 -0
  272. package/dist/sessions.js +662 -0
  273. package/dist/sessions.js.map +1 -0
  274. package/dist/shell.e2e.test.d.ts +1 -0
  275. package/dist/shell.e2e.test.js +147 -0
  276. package/dist/shell.e2e.test.js.map +1 -0
  277. package/dist/slash-commands.d.ts +62 -0
  278. package/dist/slash-commands.js +475 -0
  279. package/dist/slash-commands.js.map +1 -0
  280. package/dist/slash-commands.test.d.ts +1 -0
  281. package/dist/slash-commands.test.js +87 -0
  282. package/dist/slash-commands.test.js.map +1 -0
  283. package/dist/sleep.e2e.test.d.ts +1 -0
  284. package/dist/sleep.e2e.test.js +115 -487
  285. package/dist/sleep.e2e.test.js.map +1 -0
  286. package/dist/sleeps.d.ts +37 -0
  287. package/dist/sleeps.js +220 -0
  288. package/dist/sleeps.js.map +1 -0
  289. package/dist/store.d.ts +10 -0
  290. package/dist/store.js +8 -28
  291. package/dist/store.js.map +1 -0
  292. package/dist/system-prompt.d.ts +65 -0
  293. package/dist/system-prompt.js +875 -0
  294. package/dist/system-prompt.js.map +1 -0
  295. package/dist/tasks.e2e.test.d.ts +1 -0
  296. package/dist/tasks.e2e.test.js +173 -0
  297. package/dist/tasks.e2e.test.js.map +1 -0
  298. package/dist/test/auth-plugin/index.d.ts +3 -0
  299. package/dist/test/auth-plugin/index.js +18 -0
  300. package/dist/test/auth-plugin/index.js.map +1 -0
  301. package/dist/test/fake-mcp-server.d.ts +1 -0
  302. package/dist/test/fake-mcp-server.js +45 -0
  303. package/dist/test/fake-mcp-server.js.map +1 -0
  304. package/dist/test/harness.d.ts +246 -0
  305. package/dist/test/harness.js +704 -0
  306. package/dist/test/harness.js.map +1 -0
  307. package/dist/test/replay.d.ts +14 -0
  308. package/dist/test/replay.js +75 -0
  309. package/dist/test/replay.js.map +1 -0
  310. package/dist/thread-reducer.d.ts +172 -0
  311. package/dist/thread-reducer.fixtures.test.d.ts +1 -0
  312. package/dist/thread-reducer.fixtures.test.js +360 -0
  313. package/dist/thread-reducer.fixtures.test.js.map +1 -0
  314. package/dist/thread-reducer.js +497 -0
  315. package/dist/thread-reducer.js.map +1 -0
  316. package/dist/thread-reducer.test.d.ts +1 -0
  317. package/dist/thread-reducer.test.js +369 -0
  318. package/dist/thread-reducer.test.js.map +1 -0
  319. package/dist/tts.test.d.ts +1 -0
  320. package/dist/tts.test.js +147 -0
  321. package/dist/tts.test.js.map +1 -0
  322. package/dist/voice-agent.d.ts +33 -0
  323. package/dist/voice-agent.js +159 -0
  324. package/dist/voice-agent.js.map +1 -0
  325. package/dist/voice-attachments.e2e.test.d.ts +1 -0
  326. package/dist/voice-attachments.e2e.test.js +253 -0
  327. package/dist/voice-attachments.e2e.test.js.map +1 -0
  328. package/dist/voice-call.d.ts +59 -0
  329. package/dist/voice-call.e2e.test.d.ts +1 -0
  330. package/dist/voice-call.e2e.test.js +259 -0
  331. package/dist/voice-call.e2e.test.js.map +1 -0
  332. package/dist/voice-call.js +760 -0
  333. package/dist/voice-call.js.map +1 -0
  334. package/dist/voice-openai.e2e.test.d.ts +1 -0
  335. package/dist/voice-openai.e2e.test.js +161 -0
  336. package/dist/voice-openai.e2e.test.js.map +1 -0
  337. package/dist/voice.d.ts +116 -0
  338. package/dist/voice.js +648 -646
  339. package/dist/voice.js.map +1 -0
  340. package/dist/working-directory.e2e.test.d.ts +1 -0
  341. package/dist/working-directory.e2e.test.js +231 -0
  342. package/dist/working-directory.e2e.test.js.map +1 -0
  343. package/dist/worktrees.d.ts +54 -0
  344. package/dist/worktrees.js +247 -709
  345. package/dist/worktrees.js.map +1 -0
  346. package/dist/worktrees.test.d.ts +1 -0
  347. package/dist/worktrees.test.js +97 -952
  348. package/dist/worktrees.test.js.map +1 -0
  349. package/package.json +40 -79
  350. package/src/agent-ui.e2e.test.ts +134 -0
  351. package/src/agent-ui.ts +216 -0
  352. package/src/analytics.e2e.test.ts +138 -0
  353. package/src/analytics.test.ts +122 -182
  354. package/src/analytics.ts +206 -256
  355. package/src/autostart.ts +78 -0
  356. package/src/bot.ts +194 -0
  357. package/src/btw.e2e.test.ts +130 -0
  358. package/src/catalog-refresh.e2e.test.ts +112 -0
  359. package/src/cli/bot.ts +419 -0
  360. package/src/cli/discord.ts +116 -0
  361. package/src/cli/project.ts +147 -0
  362. package/src/cli/schedule.ts +151 -0
  363. package/src/cli/session.ts +285 -0
  364. package/src/cli/shared.ts +139 -0
  365. package/src/cli-discord.e2e.test.ts +62 -0
  366. package/src/cli-lock.e2e.test.ts +530 -0
  367. package/src/cli-read.e2e.test.ts +78 -0
  368. package/src/cli-sessions.e2e.test.ts +122 -0
  369. package/src/cli.ts +35 -418
  370. package/src/commands/login-commands.ts +360 -0
  371. package/src/commands/preference-commands.ts +586 -0
  372. package/src/commands/session-commands.ts +289 -0
  373. package/src/commands/thread-commands.ts +324 -0
  374. package/src/commands/worktree-commands.ts +288 -0
  375. package/src/commands.e2e.test.ts +290 -0
  376. package/src/credentials.ts +425 -0
  377. package/src/db.test.ts +345 -748
  378. package/src/db.ts +66 -284
  379. package/src/duration.ts +18 -0
  380. package/src/effects.ts +303 -0
  381. package/src/errors.ts +39 -222
  382. package/src/event-loop.ts +462 -0
  383. package/src/file-edit-log.test.ts +68 -272
  384. package/src/file-edit-log.ts +108 -231
  385. package/src/fixtures/v1-schema.sql +241 -0
  386. package/src/fixtures/voice.ogg +0 -0
  387. package/src/format-parts.test.ts +150 -0
  388. package/src/format-parts.ts +405 -0
  389. package/src/gateway-replay.e2e.test.ts +116 -0
  390. package/src/harness.e2e.test.ts +124 -0
  391. package/src/hello-thread.e2e.test.ts +223 -0
  392. package/src/ingress.ts +334 -0
  393. package/src/interaction-context.ts +131 -0
  394. package/src/interrupt.e2e.test.ts +151 -0
  395. package/src/legacy.e2e.test.ts +105 -0
  396. package/src/live-call.ts +249 -0
  397. package/src/lock-routes.ts +625 -0
  398. package/src/lock-server.ts +233 -0
  399. package/src/logger.ts +120 -207
  400. package/src/login.e2e.test.ts +86 -0
  401. package/src/main.ts +230 -0
  402. package/src/markdown/components.ts +102 -0
  403. package/src/markdown/markdown.test.ts +414 -0
  404. package/src/markdown/render-markdown.ts +288 -0
  405. package/src/migrations.ts +198 -0
  406. package/src/onboarding-cli.e2e.test.ts +215 -0
  407. package/src/onboarding.e2e.test.ts +171 -0
  408. package/src/onboarding.ts +297 -0
  409. package/src/opencode-server.test.ts +24 -0
  410. package/src/opencode-server.ts +461 -0
  411. package/src/permissions.e2e.test.ts +191 -0
  412. package/src/permissions.ts +121 -0
  413. package/src/plugin/anthropic/index.test.ts +39 -0
  414. package/src/plugin/anthropic/index.ts +213 -0
  415. package/src/plugin/index.ts +93 -0
  416. package/src/plugin/session-id/index.ts +19 -0
  417. package/src/plugin.e2e.test.ts +112 -0
  418. package/src/preference-commands.e2e.test.ts +597 -0
  419. package/src/project.ts +216 -0
  420. package/src/prompt.ts +664 -0
  421. package/src/questions.e2e.test.ts +328 -0
  422. package/src/questions.ts +322 -0
  423. package/src/queue.e2e.test.ts +253 -0
  424. package/src/queue.ts +158 -0
  425. package/src/remote-send.e2e.test.ts +91 -0
  426. package/src/remote-send.ts +110 -0
  427. package/src/renderer.e2e.test.ts +390 -0
  428. package/src/restart.e2e.test.ts +90 -0
  429. package/src/routes.test.ts +81 -0
  430. package/src/routes.ts +48 -0
  431. package/src/routines.e2e.test.ts +210 -0
  432. package/src/scheduler.e2e.test.ts +182 -0
  433. package/src/scheduler.test.ts +183 -0
  434. package/src/scheduler.ts +648 -0
  435. package/src/schema-sql.ts +3 -0
  436. package/src/schema.ts +95 -271
  437. package/src/session-commands.e2e.test.ts +326 -0
  438. package/src/session-events.ts +218 -0
  439. package/src/session-ops.e2e.test.ts +141 -0
  440. package/src/sessions.ts +803 -0
  441. package/src/shell.e2e.test.ts +173 -0
  442. package/src/slash-commands.test.ts +89 -0
  443. package/src/slash-commands.ts +539 -0
  444. package/src/sleep.e2e.test.ts +135 -552
  445. package/src/sleeps.ts +230 -0
  446. package/src/store.ts +19 -217
  447. package/src/system-prompt.ts +944 -0
  448. package/src/test/auth-plugin/index.ts +17 -0
  449. package/src/test/fake-mcp-server.ts +49 -0
  450. package/src/test/harness.ts +957 -0
  451. package/src/test/replay.ts +90 -0
  452. package/src/thread-reducer.fixtures.test.ts +374 -0
  453. package/src/thread-reducer.test.ts +380 -0
  454. package/src/thread-reducer.ts +671 -0
  455. package/src/tts.test.ts +150 -0
  456. package/src/voice-agent.ts +196 -0
  457. package/src/voice-attachments.e2e.test.ts +298 -0
  458. package/src/voice-call.e2e.test.ts +293 -0
  459. package/src/voice-call.ts +764 -0
  460. package/src/voice-openai.e2e.test.ts +191 -0
  461. package/src/voice.ts +691 -890
  462. package/src/working-directory.e2e.test.ts +226 -0
  463. package/src/worktrees.test.ts +89 -1115
  464. package/src/worktrees.ts +195 -957
  465. package/bin.js +0 -2
  466. package/dist/agent-model.e2e.test.js +0 -1215
  467. package/dist/ai-tool-to-genai.js +0 -233
  468. package/dist/ai-tool-to-genai.test.js +0 -267
  469. package/dist/ai-tool.js +0 -6
  470. package/dist/anthropic-account-identity.js +0 -70
  471. package/dist/anthropic-account-identity.test.js +0 -38
  472. package/dist/anthropic-auth-plugin.js +0 -958
  473. package/dist/anthropic-auth-plugin.test.js +0 -115
  474. package/dist/anthropic-auth-state.js +0 -186
  475. package/dist/anthropic-auth-state.test.js +0 -203
  476. package/dist/bash-tool-schema-plugin.js +0 -50
  477. package/dist/bash-tool-schema-plugin.test.js +0 -68
  478. package/dist/bin.js +0 -151
  479. package/dist/bot-lifecycle.test.js +0 -206
  480. package/dist/btw-prefix-detection.js +0 -15
  481. package/dist/btw-prefix-detection.test.js +0 -93
  482. package/dist/cache-drift-plugin.js +0 -183
  483. package/dist/cache-rewrite.js +0 -39
  484. package/dist/cache-rewrite.test.js +0 -41
  485. package/dist/channel-management.js +0 -347
  486. package/dist/cli-commands/bot.js +0 -254
  487. package/dist/cli-commands/maintenance.js +0 -171
  488. package/dist/cli-commands/misc.js +0 -153
  489. package/dist/cli-commands/project.js +0 -510
  490. package/dist/cli-commands/send.js +0 -659
  491. package/dist/cli-commands/session.js +0 -889
  492. package/dist/cli-commands/task.js +0 -277
  493. package/dist/cli-commands/thread.js +0 -63
  494. package/dist/cli-commands/user.js +0 -145
  495. package/dist/cli-parsing.test.js +0 -226
  496. package/dist/cli-runner.js +0 -1579
  497. package/dist/cli-runner.test.js +0 -86
  498. package/dist/cli-send-thread.e2e.test.js +0 -470
  499. package/dist/cloudflare-transcription.js +0 -56
  500. package/dist/commands/abort.js +0 -84
  501. package/dist/commands/action-buttons.js +0 -248
  502. package/dist/commands/add-project.js +0 -111
  503. package/dist/commands/agent.js +0 -658
  504. package/dist/commands/ask-question.js +0 -371
  505. package/dist/commands/ask-question.test.js +0 -150
  506. package/dist/commands/btw.js +0 -167
  507. package/dist/commands/compact.js +0 -117
  508. package/dist/commands/context-usage.js +0 -147
  509. package/dist/commands/create-new-project.js +0 -138
  510. package/dist/commands/diff.js +0 -63
  511. package/dist/commands/file-upload.js +0 -280
  512. package/dist/commands/fork-subagent.js +0 -175
  513. package/dist/commands/fork.js +0 -260
  514. package/dist/commands/gemini-apikey.js +0 -123
  515. package/dist/commands/last-sessions.js +0 -129
  516. package/dist/commands/login.js +0 -893
  517. package/dist/commands/mcp.js +0 -239
  518. package/dist/commands/memory-snapshot.js +0 -24
  519. package/dist/commands/mention-mode.js +0 -42
  520. package/dist/commands/merge-worktree.js +0 -205
  521. package/dist/commands/model-variant.js +0 -390
  522. package/dist/commands/model.js +0 -957
  523. package/dist/commands/multioauth.js +0 -425
  524. package/dist/commands/new-worktree.js +0 -622
  525. package/dist/commands/paginated-select.js +0 -57
  526. package/dist/commands/permissions.js +0 -327
  527. package/dist/commands/queue.js +0 -340
  528. package/dist/commands/queue.test.js +0 -15
  529. package/dist/commands/remove-project.js +0 -111
  530. package/dist/commands/restart-opencode-server.js +0 -133
  531. package/dist/commands/resume.js +0 -148
  532. package/dist/commands/run-command.js +0 -79
  533. package/dist/commands/screenshare.js +0 -307
  534. package/dist/commands/screenshare.test.js +0 -20
  535. package/dist/commands/session-id.js +0 -78
  536. package/dist/commands/session.js +0 -243
  537. package/dist/commands/share.js +0 -80
  538. package/dist/commands/tasks.js +0 -296
  539. package/dist/commands/types.js +0 -2
  540. package/dist/commands/undo-redo.js +0 -313
  541. package/dist/commands/unset-model.js +0 -116
  542. package/dist/commands/upgrade.js +0 -41
  543. package/dist/commands/user-command.js +0 -196
  544. package/dist/commands/verbosity.js +0 -124
  545. package/dist/commands/vscode.js +0 -295
  546. package/dist/commands/worktrees.js +0 -548
  547. package/dist/condense-memory.js +0 -33
  548. package/dist/config.js +0 -126
  549. package/dist/context-awareness-plugin.js +0 -357
  550. package/dist/context-awareness-plugin.test.js +0 -126
  551. package/dist/cpu-profiler.js +0 -211
  552. package/dist/cpu-profiler.test.js +0 -160
  553. package/dist/critique-utils.js +0 -95
  554. package/dist/database.js +0 -1074
  555. package/dist/debounce-timeout.js +0 -28
  556. package/dist/debounced-process-flush.js +0 -77
  557. package/dist/default-channel-provisioning.test.js +0 -149
  558. package/dist/discord-bot.js +0 -1409
  559. package/dist/discord-command-registration.js +0 -519
  560. package/dist/discord-command-registration.test.js +0 -17
  561. package/dist/discord-urls.js +0 -81
  562. package/dist/discord-utils.js +0 -789
  563. package/dist/discord-utils.test.js +0 -400
  564. package/dist/escape-backticks.test.js +0 -429
  565. package/dist/event-stream-real-capture.e2e.test.js +0 -533
  566. package/dist/eventsource-parser.test.js +0 -327
  567. package/dist/exec-async.js +0 -38
  568. package/dist/external-directory-permissions.e2e.test.js +0 -86
  569. package/dist/external-directory-project-deny.e2e.test.js +0 -70
  570. package/dist/external-opencode-sync.js +0 -620
  571. package/dist/external-opencode-sync.test.js +0 -206
  572. package/dist/extract-sdk-error-message.test.js +0 -13
  573. package/dist/format-tables.js +0 -728
  574. package/dist/format-tables.test.js +0 -915
  575. package/dist/forum-sync/config.js +0 -79
  576. package/dist/forum-sync/discord-operations.js +0 -157
  577. package/dist/forum-sync/index.js +0 -5
  578. package/dist/forum-sync/markdown.js +0 -110
  579. package/dist/forum-sync/sync-to-discord.js +0 -417
  580. package/dist/forum-sync/sync-to-files.js +0 -190
  581. package/dist/forum-sync/types.js +0 -53
  582. package/dist/forum-sync/watchers.js +0 -322
  583. package/dist/gateway-proxy-reconnect.e2e.test.js +0 -414
  584. package/dist/gateway-proxy.e2e.test.js +0 -488
  585. package/dist/genai-worker-wrapper.js +0 -111
  586. package/dist/genai-worker.js +0 -309
  587. package/dist/genai.js +0 -232
  588. package/dist/git-worktree-core.js +0 -461
  589. package/dist/guild-categories.test.js +0 -225
  590. package/dist/heap-monitor.js +0 -122
  591. package/dist/hrana-server.js +0 -322
  592. package/dist/hrana-server.test.js +0 -99
  593. package/dist/html-actions.js +0 -123
  594. package/dist/html-actions.test.js +0 -70
  595. package/dist/html-components.js +0 -115
  596. package/dist/html-components.test.js +0 -34
  597. package/dist/image-optimizer-plugin.js +0 -153
  598. package/dist/image-utils.js +0 -112
  599. package/dist/interaction-handler.js +0 -578
  600. package/dist/ipc-polling.js +0 -245
  601. package/dist/ipc-tools-plugin.js +0 -251
  602. package/dist/kimaki-digital-twin.e2e.test.js +0 -161
  603. package/dist/kimaki-opencode-plugin-loading.e2e.test.js +0 -158
  604. package/dist/kimaki-opencode-plugin.js +0 -33
  605. package/dist/kimaki-opencode-plugin.test.js +0 -126
  606. package/dist/kimaki-workspace-adaptor.js +0 -118
  607. package/dist/limit-heading-depth.js +0 -25
  608. package/dist/limit-heading-depth.test.js +0 -105
  609. package/dist/list-channel-threads.js +0 -163
  610. package/dist/list-channel-threads.test.js +0 -80
  611. package/dist/markdown.js +0 -455
  612. package/dist/markdown.test.js +0 -411
  613. package/dist/memory-overview-plugin.js +0 -123
  614. package/dist/message-formatting.js +0 -763
  615. package/dist/message-formatting.test.js +0 -1132
  616. package/dist/message-preprocessing.js +0 -441
  617. package/dist/oauth-rotation-shared.js +0 -253
  618. package/dist/onboarding-tutorial.js +0 -163
  619. package/dist/onboarding-welcome.js +0 -37
  620. package/dist/openai-auth-plugin.js +0 -125
  621. package/dist/openai-auth-state.js +0 -237
  622. package/dist/openai-realtime.js +0 -224
  623. package/dist/opencode-command-detection.js +0 -65
  624. package/dist/opencode-command-detection.test.js +0 -240
  625. package/dist/opencode-command.js +0 -181
  626. package/dist/opencode-command.test.js +0 -218
  627. package/dist/opencode-interrupt-plugin.js +0 -253
  628. package/dist/opencode-interrupt-plugin.test.js +0 -293
  629. package/dist/opencode.js +0 -1217
  630. package/dist/parse-permission-rules.test.js +0 -117
  631. package/dist/patch-text-parser.js +0 -97
  632. package/dist/plugin-logger.js +0 -105
  633. package/dist/plugin-opencode-client.js +0 -54
  634. package/dist/privacy-sanitizer.js +0 -105
  635. package/dist/project-add.test.js +0 -62
  636. package/dist/question-abort-clears-dropdown.e2e.test.js +0 -89
  637. package/dist/question-abort-resume.e2e.test.js +0 -100
  638. package/dist/question-abort-retry-text-before-dropdown.e2e.test.js +0 -150
  639. package/dist/question-text-before-dropdown.e2e.test.js +0 -52
  640. package/dist/queue-advanced-abort.e2e.test.js +0 -348
  641. package/dist/queue-advanced-action-buttons.e2e.test.js +0 -209
  642. package/dist/queue-advanced-e2e-setup.js +0 -1043
  643. package/dist/queue-advanced-footer.e2e.test.js +0 -286
  644. package/dist/queue-advanced-model-switch.e2e.test.js +0 -297
  645. package/dist/queue-advanced-permissions-typing.e2e.test.js +0 -189
  646. package/dist/queue-advanced-question.e2e.test.js +0 -266
  647. package/dist/queue-advanced-typing-interrupt.e2e.test.js +0 -113
  648. package/dist/queue-advanced-typing.e2e.test.js +0 -151
  649. package/dist/queue-delete-message.e2e.test.js +0 -75
  650. package/dist/queue-drain-after-interactive-ui.e2e.test.js +0 -120
  651. package/dist/queue-interrupt-drain.e2e.test.js +0 -246
  652. package/dist/queue-question-select-drain.e2e.test.js +0 -162
  653. package/dist/runtime-idle-sweeper.js +0 -52
  654. package/dist/runtime-lifecycle.e2e.test.js +0 -570
  655. package/dist/sentry.js +0 -23
  656. package/dist/session-handler/agent-utils.js +0 -66
  657. package/dist/session-handler/event-stream-state.js +0 -1217
  658. package/dist/session-handler/event-stream-state.test.js +0 -3034
  659. package/dist/session-handler/global-event-listener.js +0 -212
  660. package/dist/session-handler/model-utils.js +0 -311
  661. package/dist/session-handler/model-utils.test.js +0 -445
  662. package/dist/session-handler/opencode-session-event-log.js +0 -121
  663. package/dist/session-handler/thread-ingress.test.js +0 -41
  664. package/dist/session-handler/thread-runtime-state.js +0 -209
  665. package/dist/session-handler/thread-session-runtime-summary.test.js +0 -121
  666. package/dist/session-handler/thread-session-runtime.js +0 -4252
  667. package/dist/session-handler.js +0 -9
  668. package/dist/session-search.js +0 -219
  669. package/dist/session-search.test.js +0 -287
  670. package/dist/session-sleep.test.js +0 -224
  671. package/dist/session-title-rename.test.js +0 -128
  672. package/dist/skill-filter.js +0 -43
  673. package/dist/skill-filter.test.js +0 -86
  674. package/dist/startup-service.js +0 -153
  675. package/dist/startup-time.e2e.test.js +0 -297
  676. package/dist/system-message.js +0 -984
  677. package/dist/system-message.test.js +0 -1093
  678. package/dist/task-id-plugin.js +0 -16
  679. package/dist/task-runner.js +0 -554
  680. package/dist/task-schedule.js +0 -298
  681. package/dist/task-schedule.test.js +0 -366
  682. package/dist/test-utils.js +0 -359
  683. package/dist/thinking-utils.js +0 -45
  684. package/dist/thinking-utils.test.js +0 -35
  685. package/dist/thread-message-queue.e2e.test.js +0 -1155
  686. package/dist/tools.js +0 -357
  687. package/dist/undo-redo.e2e.test.js +0 -171
  688. package/dist/unnest-code-blocks.js +0 -146
  689. package/dist/unnest-code-blocks.test.js +0 -673
  690. package/dist/upgrade.js +0 -114
  691. package/dist/utils.js +0 -148
  692. package/dist/voice-attachment.js +0 -56
  693. package/dist/voice-handler.js +0 -726
  694. package/dist/voice-message.e2e.test.js +0 -1263
  695. package/dist/voice-routing-input.e2e.test.js +0 -195
  696. package/dist/voice-routing-question.e2e.test.js +0 -125
  697. package/dist/voice.test.js +0 -352
  698. package/dist/wait-session.js +0 -193
  699. package/dist/websockify.js +0 -69
  700. package/dist/worker-types.js +0 -4
  701. package/dist/worktree-clone-isolation.e2e.test.js +0 -188
  702. package/dist/worktree-lifecycle.e2e.test.js +0 -735
  703. package/dist/worktree-utils.js +0 -3
  704. package/dist/xai-auth-plugin.js +0 -238
  705. package/dist/xai-auth-state.js +0 -234
  706. package/dist/xai-auth-state.test.js +0 -183
  707. package/dist/xml.js +0 -92
  708. package/dist/xml.test.js +0 -32
  709. package/skills/bump-holocron-consumers/SKILL.md +0 -137
  710. package/skills/critique/SKILL.md +0 -112
  711. package/skills/egaki/SKILL.md +0 -86
  712. package/skills/errore/SKILL.md +0 -501
  713. package/skills/goke/SKILL.md +0 -695
  714. package/skills/holocron/SKILL.md +0 -942
  715. package/skills/lintcn/SKILL.md +0 -873
  716. package/skills/new-skill/SKILL.md +0 -339
  717. package/skills/npm-package/SKILL.md +0 -692
  718. package/skills/opensrc/SKILL.md +0 -95
  719. package/skills/playwriter/SKILL.md +0 -35
  720. package/skills/profano/SKILL.md +0 -33
  721. package/skills/readme.md +0 -20
  722. package/skills/sigillo/SKILL.md +0 -370
  723. package/skills/spiceflow/SKILL.md +0 -80
  724. package/skills/strada/SKILL.md +0 -104
  725. package/skills/termcast/SKILL.md +0 -133
  726. package/skills/tuistory/SKILL.md +0 -237
  727. package/skills/usecomputer/SKILL.md +0 -264
  728. package/skills/zele/SKILL.md +0 -81
  729. package/src/agent-model.e2e.test.ts +0 -1548
  730. package/src/ai-tool-to-genai.test.ts +0 -296
  731. package/src/ai-tool-to-genai.ts +0 -283
  732. package/src/ai-tool.ts +0 -39
  733. package/src/anthropic-account-identity.test.ts +0 -52
  734. package/src/anthropic-account-identity.ts +0 -96
  735. package/src/anthropic-auth-plugin.test.ts +0 -135
  736. package/src/anthropic-auth-plugin.ts +0 -1180
  737. package/src/anthropic-auth-state.test.ts +0 -259
  738. package/src/anthropic-auth-state.ts +0 -272
  739. package/src/bash-tool-schema-plugin.test.ts +0 -84
  740. package/src/bash-tool-schema-plugin.ts +0 -77
  741. package/src/bin.ts +0 -173
  742. package/src/bot-lifecycle.test.ts +0 -252
  743. package/src/btw-prefix-detection.test.ts +0 -104
  744. package/src/btw-prefix-detection.ts +0 -19
  745. package/src/cache-drift-plugin.ts +0 -249
  746. package/src/cache-rewrite.test.ts +0 -49
  747. package/src/cache-rewrite.ts +0 -96
  748. package/src/channel-management.ts +0 -494
  749. package/src/cli-commands/bot.ts +0 -374
  750. package/src/cli-commands/maintenance.ts +0 -237
  751. package/src/cli-commands/misc.ts +0 -208
  752. package/src/cli-commands/project.ts +0 -680
  753. package/src/cli-commands/send.ts +0 -879
  754. package/src/cli-commands/session.ts +0 -1181
  755. package/src/cli-commands/task.ts +0 -358
  756. package/src/cli-commands/thread.ts +0 -86
  757. package/src/cli-commands/user.ts +0 -195
  758. package/src/cli-parsing.test.ts +0 -305
  759. package/src/cli-runner.test.ts +0 -129
  760. package/src/cli-runner.ts +0 -2141
  761. package/src/cli-send-thread.e2e.test.ts +0 -609
  762. package/src/cloudflare-transcription.ts +0 -76
  763. package/src/commands/abort.ts +0 -113
  764. package/src/commands/action-buttons.ts +0 -371
  765. package/src/commands/add-project.ts +0 -146
  766. package/src/commands/agent.ts +0 -911
  767. package/src/commands/ask-question.test.ts +0 -177
  768. package/src/commands/ask-question.ts +0 -532
  769. package/src/commands/btw.ts +0 -246
  770. package/src/commands/compact.ts +0 -154
  771. package/src/commands/context-usage.ts +0 -208
  772. package/src/commands/create-new-project.ts +0 -203
  773. package/src/commands/diff.ts +0 -91
  774. package/src/commands/file-upload.ts +0 -398
  775. package/src/commands/fork-subagent.ts +0 -262
  776. package/src/commands/fork.ts +0 -388
  777. package/src/commands/gemini-apikey.ts +0 -205
  778. package/src/commands/last-sessions.ts +0 -176
  779. package/src/commands/login.ts +0 -1182
  780. package/src/commands/mcp.ts +0 -307
  781. package/src/commands/memory-snapshot.ts +0 -30
  782. package/src/commands/mention-mode.ts +0 -66
  783. package/src/commands/merge-worktree.ts +0 -282
  784. package/src/commands/model-variant.ts +0 -522
  785. package/src/commands/model.ts +0 -1310
  786. package/src/commands/multioauth.ts +0 -508
  787. package/src/commands/new-worktree.ts +0 -832
  788. package/src/commands/paginated-select.ts +0 -81
  789. package/src/commands/permissions.ts +0 -458
  790. package/src/commands/queue.test.ts +0 -19
  791. package/src/commands/queue.ts +0 -455
  792. package/src/commands/remove-project.ts +0 -152
  793. package/src/commands/restart-opencode-server.ts +0 -173
  794. package/src/commands/resume.ts +0 -229
  795. package/src/commands/run-command.ts +0 -123
  796. package/src/commands/screenshare.test.ts +0 -30
  797. package/src/commands/screenshare.ts +0 -388
  798. package/src/commands/session-id.ts +0 -109
  799. package/src/commands/session.ts +0 -311
  800. package/src/commands/share.ts +0 -106
  801. package/src/commands/tasks.ts +0 -399
  802. package/src/commands/types.ts +0 -25
  803. package/src/commands/undo-redo.ts +0 -394
  804. package/src/commands/unset-model.ts +0 -152
  805. package/src/commands/upgrade.ts +0 -51
  806. package/src/commands/user-command.ts +0 -260
  807. package/src/commands/verbosity.ts +0 -172
  808. package/src/commands/vscode.ts +0 -378
  809. package/src/commands/worktrees.ts +0 -762
  810. package/src/condense-memory.ts +0 -36
  811. package/src/config.ts +0 -147
  812. package/src/context-awareness-plugin.test.ts +0 -144
  813. package/src/context-awareness-plugin.ts +0 -508
  814. package/src/cpu-profiler.test.ts +0 -183
  815. package/src/cpu-profiler.ts +0 -293
  816. package/src/critique-utils.ts +0 -139
  817. package/src/database.ts +0 -1484
  818. package/src/debounce-timeout.ts +0 -43
  819. package/src/debounced-process-flush.ts +0 -104
  820. package/src/default-channel-provisioning.test.ts +0 -156
  821. package/src/discord-bot.ts +0 -1836
  822. package/src/discord-command-registration.test.ts +0 -20
  823. package/src/discord-command-registration.ts +0 -704
  824. package/src/discord-urls.ts +0 -88
  825. package/src/discord-utils.test.ts +0 -470
  826. package/src/discord-utils.ts +0 -1079
  827. package/src/escape-backticks.test.ts +0 -469
  828. package/src/event-stream-real-capture.e2e.test.ts +0 -692
  829. package/src/eventsource-parser.test.ts +0 -351
  830. package/src/exec-async.ts +0 -44
  831. package/src/external-directory-permissions.e2e.test.ts +0 -103
  832. package/src/external-directory-project-deny.e2e.test.ts +0 -84
  833. package/src/external-opencode-sync.test.ts +0 -248
  834. package/src/external-opencode-sync.ts +0 -867
  835. package/src/extract-sdk-error-message.test.ts +0 -18
  836. package/src/format-tables.test.ts +0 -988
  837. package/src/format-tables.ts +0 -1031
  838. package/src/forum-sync/config.ts +0 -92
  839. package/src/forum-sync/discord-operations.ts +0 -245
  840. package/src/forum-sync/index.ts +0 -9
  841. package/src/forum-sync/markdown.ts +0 -172
  842. package/src/forum-sync/sync-to-discord.ts +0 -595
  843. package/src/forum-sync/sync-to-files.ts +0 -294
  844. package/src/forum-sync/types.ts +0 -175
  845. package/src/forum-sync/watchers.ts +0 -474
  846. package/src/gateway-proxy-reconnect.e2e.test.ts +0 -553
  847. package/src/gateway-proxy.e2e.test.ts +0 -648
  848. package/src/genai-worker-wrapper.ts +0 -164
  849. package/src/genai-worker.ts +0 -385
  850. package/src/genai.ts +0 -318
  851. package/src/git-worktree-core.ts +0 -616
  852. package/src/guild-categories.test.ts +0 -260
  853. package/src/heap-monitor.ts +0 -152
  854. package/src/hrana-server.test.ts +0 -130
  855. package/src/hrana-server.ts +0 -399
  856. package/src/html-actions.test.ts +0 -87
  857. package/src/html-actions.ts +0 -174
  858. package/src/html-components.test.ts +0 -38
  859. package/src/html-components.ts +0 -177
  860. package/src/image-optimizer-plugin.ts +0 -194
  861. package/src/image-utils.ts +0 -150
  862. package/src/interaction-handler.ts +0 -775
  863. package/src/ipc-polling.ts +0 -325
  864. package/src/ipc-tools-plugin.ts +0 -306
  865. package/src/kimaki-digital-twin.e2e.test.ts +0 -199
  866. package/src/kimaki-opencode-plugin-loading.e2e.test.ts +0 -208
  867. package/src/kimaki-opencode-plugin.test.ts +0 -142
  868. package/src/kimaki-opencode-plugin.ts +0 -34
  869. package/src/kimaki-workspace-adaptor.ts +0 -144
  870. package/src/limit-heading-depth.test.ts +0 -116
  871. package/src/limit-heading-depth.ts +0 -26
  872. package/src/list-channel-threads.test.ts +0 -90
  873. package/src/list-channel-threads.ts +0 -252
  874. package/src/markdown.test.ts +0 -498
  875. package/src/markdown.ts +0 -577
  876. package/src/memory-overview-plugin.ts +0 -159
  877. package/src/message-formatting.test.ts +0 -1401
  878. package/src/message-formatting.ts +0 -972
  879. package/src/message-preprocessing.ts +0 -579
  880. package/src/oauth-rotation-shared.ts +0 -332
  881. package/src/onboarding-tutorial.ts +0 -167
  882. package/src/onboarding-welcome.ts +0 -49
  883. package/src/openai-auth-plugin.ts +0 -158
  884. package/src/openai-auth-state.ts +0 -297
  885. package/src/openai-realtime.ts +0 -358
  886. package/src/opencode-command-detection.test.ts +0 -307
  887. package/src/opencode-command-detection.ts +0 -76
  888. package/src/opencode-command.test.ts +0 -290
  889. package/src/opencode-command.ts +0 -251
  890. package/src/opencode-interrupt-plugin.test.ts +0 -419
  891. package/src/opencode-interrupt-plugin.ts +0 -306
  892. package/src/opencode.ts +0 -1590
  893. package/src/parse-permission-rules.test.ts +0 -127
  894. package/src/patch-text-parser.ts +0 -107
  895. package/src/plugin-logger.ts +0 -139
  896. package/src/plugin-opencode-client.ts +0 -71
  897. package/src/privacy-sanitizer.ts +0 -142
  898. package/src/project-add.test.ts +0 -80
  899. package/src/question-abort-clears-dropdown.e2e.test.ts +0 -109
  900. package/src/question-abort-resume.e2e.test.ts +0 -121
  901. package/src/question-abort-retry-text-before-dropdown.e2e.test.ts +0 -172
  902. package/src/question-text-before-dropdown.e2e.test.ts +0 -67
  903. package/src/queue-advanced-abort.e2e.test.ts +0 -449
  904. package/src/queue-advanced-action-buttons.e2e.test.ts +0 -271
  905. package/src/queue-advanced-e2e-setup.ts +0 -1164
  906. package/src/queue-advanced-footer.e2e.test.ts +0 -357
  907. package/src/queue-advanced-model-switch.e2e.test.ts +0 -382
  908. package/src/queue-advanced-permissions-typing.e2e.test.ts +0 -256
  909. package/src/queue-advanced-question.e2e.test.ts +0 -322
  910. package/src/queue-advanced-typing-interrupt.e2e.test.ts +0 -147
  911. package/src/queue-advanced-typing.e2e.test.ts +0 -199
  912. package/src/queue-delete-message.e2e.test.ts +0 -102
  913. package/src/queue-drain-after-interactive-ui.e2e.test.ts +0 -152
  914. package/src/queue-interrupt-drain.e2e.test.ts +0 -288
  915. package/src/queue-question-select-drain.e2e.test.ts +0 -206
  916. package/src/runtime-idle-sweeper.ts +0 -76
  917. package/src/runtime-lifecycle.e2e.test.ts +0 -714
  918. package/src/schema.sql +0 -251
  919. package/src/sentry.ts +0 -26
  920. package/src/session-handler/agent-utils.ts +0 -99
  921. package/src/session-handler/event-stream-fixtures/real-session-action-buttons.jsonl +0 -45
  922. package/src/session-handler/event-stream-fixtures/real-session-footer-suppressed-on-pre-idle-interrupt.jsonl +0 -40
  923. package/src/session-handler/event-stream-fixtures/real-session-permission-external-file.jsonl +0 -23
  924. package/src/session-handler/event-stream-fixtures/real-session-task-normal.jsonl +0 -22
  925. package/src/session-handler/event-stream-fixtures/real-session-task-three-parallel-sleeps.jsonl +0 -277
  926. package/src/session-handler/event-stream-fixtures/real-session-task-user-interruption.jsonl +0 -46
  927. package/src/session-handler/event-stream-fixtures/session-abort-after-idle-race.jsonl +0 -21
  928. package/src/session-handler/event-stream-fixtures/session-concurrent-messages-serialized.jsonl +0 -56
  929. package/src/session-handler/event-stream-fixtures/session-explicit-abort.jsonl +0 -44
  930. package/src/session-handler/event-stream-fixtures/session-normal-completion.jsonl +0 -29
  931. package/src/session-handler/event-stream-fixtures/session-tool-call-noisy-stream.jsonl +0 -29
  932. package/src/session-handler/event-stream-fixtures/session-two-completions-same-session.jsonl +0 -50
  933. package/src/session-handler/event-stream-fixtures/session-user-interruption.jsonl +0 -59
  934. package/src/session-handler/event-stream-fixtures/session-voice-queued-followup.jsonl +0 -52
  935. package/src/session-handler/event-stream-state.test.ts +0 -3304
  936. package/src/session-handler/event-stream-state.ts +0 -1773
  937. package/src/session-handler/global-event-listener.ts +0 -258
  938. package/src/session-handler/model-utils.test.ts +0 -537
  939. package/src/session-handler/model-utils.ts +0 -489
  940. package/src/session-handler/opencode-session-event-log.ts +0 -159
  941. package/src/session-handler/thread-ingress.test.ts +0 -54
  942. package/src/session-handler/thread-runtime-state.ts +0 -351
  943. package/src/session-handler/thread-session-runtime-summary.test.ts +0 -169
  944. package/src/session-handler/thread-session-runtime.ts +0 -5608
  945. package/src/session-handler.ts +0 -15
  946. package/src/session-search.test.ts +0 -331
  947. package/src/session-search.ts +0 -382
  948. package/src/session-sleep.test.ts +0 -294
  949. package/src/session-title-rename.test.ts +0 -186
  950. package/src/skill-filter.test.ts +0 -110
  951. package/src/skill-filter.ts +0 -63
  952. package/src/startup-service.ts +0 -200
  953. package/src/startup-time.e2e.test.ts +0 -377
  954. package/src/system-message.test.ts +0 -1176
  955. package/src/system-message.ts +0 -1179
  956. package/src/task-id-plugin.ts +0 -17
  957. package/src/task-runner.ts +0 -768
  958. package/src/task-schedule.test.ts +0 -424
  959. package/src/task-schedule.ts +0 -470
  960. package/src/test-utils.ts +0 -506
  961. package/src/thinking-utils.test.ts +0 -46
  962. package/src/thinking-utils.ts +0 -82
  963. package/src/thread-message-queue.e2e.test.ts +0 -1395
  964. package/src/tools.ts +0 -430
  965. package/src/undici.d.ts +0 -12
  966. package/src/undo-redo.e2e.test.ts +0 -219
  967. package/src/unnest-code-blocks.test.ts +0 -713
  968. package/src/unnest-code-blocks.ts +0 -185
  969. package/src/upgrade.ts +0 -127
  970. package/src/utils.ts +0 -216
  971. package/src/voice-attachment.ts +0 -79
  972. package/src/voice-handler.ts +0 -999
  973. package/src/voice-message.e2e.test.ts +0 -1524
  974. package/src/voice-routing-input.e2e.test.ts +0 -203
  975. package/src/voice-routing-question.e2e.test.ts +0 -131
  976. package/src/voice.test.ts +0 -415
  977. package/src/wait-session.ts +0 -291
  978. package/src/websockify.ts +0 -101
  979. package/src/worker-types.ts +0 -64
  980. package/src/worktree-clone-isolation.e2e.test.ts +0 -213
  981. package/src/worktree-lifecycle.e2e.test.ts +0 -900
  982. package/src/worktree-utils.ts +0 -4
  983. package/src/xai-auth-plugin.ts +0 -303
  984. package/src/xai-auth-state.test.ts +0 -224
  985. package/src/xai-auth-state.ts +0 -294
  986. package/src/xml.test.ts +0 -38
  987. package/src/xml.ts +0 -121
@@ -1,1524 +0,0 @@
1
- // E2e tests for voice message handling (audio attachment transcription).
2
- // Uses deterministic transcription (store.test.deterministicTranscription) to
3
- // bypass real AI model calls and control transcription output, timing, and
4
- // queueMessage flag. Combined with opencode-deterministic-provider for session
5
- // responses. Tests validate the full flow: attachment detection → transcription
6
- // → session dispatch, including interrupt, queue, and race condition scenarios.
7
- //
8
- // Tests assert on both Discord messages (via digital twin) and session state
9
- // transitions (via getThreadState from the zustand store).
10
-
11
- import fs from 'node:fs'
12
- import http from 'node:http'
13
-
14
- import path from 'node:path'
15
- import url from 'node:url'
16
- import { describe, beforeAll, afterAll, beforeEach, test, expect } from 'vitest'
17
- import { ChannelType, Client, GatewayIntentBits, Partials } from 'discord.js'
18
- import { DigitalDiscord } from 'discord-digital-twin/src'
19
- import {
20
- buildDeterministicOpencodeConfig,
21
- type DeterministicMatcher,
22
- } from 'opencode-deterministic-provider'
23
- import { setDataDir } from './config.js'
24
- import { store, type DeterministicTranscriptionConfig } from './store.js'
25
- import { startDiscordBot } from './discord-bot.js'
26
- import {
27
- setBotToken,
28
- initDatabase,
29
- closeDatabase,
30
- setChannelDirectory,
31
- setChannelVerbosity,
32
- getThreadSession,
33
- createPendingWorkspace,
34
- setWorkspaceReady,
35
- getThreadWorktreeOrWorkspace,
36
- } from './database.js'
37
- import { startHranaServer, stopHranaServer } from './hrana-server.js'
38
- import { initializeOpencodeForDirectory, getOpencodeClient, stopOpencodeServer } from './opencode.js'
39
- import type { Part, Message } from '@opencode-ai/sdk/v2'
40
- import {
41
- chooseLockPort,
42
- cleanupTestSessions,
43
- initTestGitRepo,
44
- waitForFooterMessage,
45
- waitForBotMessageContaining,
46
- waitForThreadState,
47
- } from './test-utils.js'
48
-
49
- import { getThreadState } from './session-handler/thread-runtime-state.js'
50
-
51
- const e2eTest = describe
52
-
53
- // ── Helpers ──────────────────────────────────────────────────────
54
-
55
- function createRunDirectories() {
56
- const root = path.resolve(process.cwd(), 'tmp', 'voice-msg-e2e')
57
- fs.mkdirSync(root, { recursive: true })
58
-
59
- const dataDir = fs.mkdtempSync(path.join(root, 'data-'))
60
- const projectDirectory = path.join(root, 'project')
61
- fs.mkdirSync(projectDirectory, { recursive: true })
62
- initTestGitRepo(projectDirectory)
63
-
64
- return { root, dataDir, projectDirectory }
65
- }
66
-
67
- function createDiscordJsClient({ restUrl }: { restUrl: string }) {
68
- return new Client({
69
- intents: [
70
- GatewayIntentBits.Guilds,
71
- GatewayIntentBits.GuildMessages,
72
- GatewayIntentBits.MessageContent,
73
- GatewayIntentBits.GuildVoiceStates,
74
- ],
75
- partials: [
76
- Partials.Channel,
77
- Partials.Message,
78
- Partials.User,
79
- Partials.ThreadMember,
80
- ],
81
- rest: {
82
- api: restUrl,
83
- version: '10',
84
- },
85
- })
86
- }
87
-
88
- /** Set the deterministic transcription config in the store for the next voice message. */
89
- function setDeterministicTranscription(config: DeterministicTranscriptionConfig | null) {
90
- store.setState({
91
- test: { deterministicTranscription: config },
92
- })
93
- }
94
-
95
- // ── OpenCode session assertion helpers ───────────────────────────
96
- // These verify what actually happened in the OpenCode session (prompts
97
- // sent, aborts, responses) beyond just Discord messages and thread state.
98
-
99
- type SessionMessage = { info: Message; parts: Part[] }
100
-
101
- function getOpencodeClientForTest(projectDirectory: string) {
102
- const client = getOpencodeClient(projectDirectory)
103
- if (!client) {
104
- throw new Error('OpenCode client not found for project directory')
105
- }
106
- return client
107
- }
108
-
109
- /** Extract text content from an array of parts (filters to TextPart only). */
110
- function getTextFromParts(parts: Part[]): string[] {
111
- return parts.flatMap((part) => {
112
- if (part.type === 'text') {
113
- return [part.text]
114
- }
115
- return []
116
- })
117
- }
118
-
119
- /** Get all user-role messages' text parts joined. */
120
- function getUserTexts(messages: SessionMessage[]): string[] {
121
- return messages
122
- .filter((m) => m.info.role === 'user')
123
- .flatMap((m) => getTextFromParts(m.parts))
124
- }
125
-
126
- /** Get all assistant-role messages' text parts joined. */
127
- function getAssistantTexts(messages: SessionMessage[]): string[] {
128
- return messages
129
- .filter((m) => m.info.role === 'assistant')
130
- .flatMap((m) => getTextFromParts(m.parts))
131
- }
132
-
133
- /**
134
- * Poll session.messages() until predicate returns true.
135
- * Used to wait for async session updates (prompts dispatched, responses completed).
136
- */
137
- async function waitForSessionMessages({
138
- projectDirectory,
139
- sessionID,
140
- timeout,
141
- predicate,
142
- description,
143
- }: {
144
- projectDirectory: string
145
- sessionID: string
146
- timeout: number
147
- predicate: (messages: SessionMessage[]) => boolean
148
- description: string
149
- }): Promise<SessionMessage[]> {
150
- const client = getOpencodeClientForTest(projectDirectory)
151
- const start = Date.now()
152
- while (Date.now() - start < timeout) {
153
- const result = await client.session.messages({
154
- sessionID,
155
- directory: projectDirectory,
156
- })
157
- const messages = result.data ?? []
158
- if (predicate(messages)) {
159
- return messages
160
- }
161
- await new Promise((resolve) => {
162
- setTimeout(resolve, 100)
163
- })
164
- }
165
- // Final attempt for error reporting
166
- const finalResult = await client.session.messages({
167
- sessionID,
168
- directory: projectDirectory,
169
- })
170
- const finalMessages = finalResult.data ?? []
171
- const userTexts = getUserTexts(finalMessages)
172
- const assistantTexts = getAssistantTexts(finalMessages)
173
- throw new Error(
174
- `Timed out waiting for session messages (${description}). ` +
175
- `User texts: ${JSON.stringify(userTexts.map((t) => t.slice(0, 80)))}. ` +
176
- `Assistant texts: ${JSON.stringify(assistantTexts.map((t) => t.slice(0, 80)))}`,
177
- )
178
- }
179
-
180
- // ── Deterministic provider matchers ──────────────────────────────
181
- // The opencode session uses these to produce canned responses.
182
-
183
- function createDeterministicMatchers(): DeterministicMatcher[] {
184
- // Slow response: emits text-delta after 2s delay, giving voice messages
185
- // time to arrive while the session is still "running".
186
- // Uses latestUserTextIncludes (not rawPromptIncludes) so it only matches
187
- // the current user message, not previous messages in session history.
188
- const slowResponse: DeterministicMatcher = {
189
- id: 'slow-response',
190
- priority: 100,
191
- when: {
192
- latestUserTextIncludes: 'SLOW_RESPONSE_MARKER',
193
- },
194
- then: {
195
- parts: [
196
- { type: 'stream-start', warnings: [] },
197
- { type: 'text-start', id: 'slow' },
198
- { type: 'text-delta', id: 'slow', delta: 'slow-response-done' },
199
- { type: 'text-end', id: 'slow' },
200
- {
201
- type: 'finish',
202
- finishReason: 'stop',
203
- usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
204
- },
205
- ],
206
- // 2s delay on the first text delta — keeps the session in "running" state
207
- partDelaysMs: [0, 0, 2000, 0, 0],
208
- },
209
- }
210
-
211
- // Fast response: completes almost immediately (~100ms)
212
- const fastResponse: DeterministicMatcher = {
213
- id: 'fast-response',
214
- priority: 90,
215
- when: {
216
- latestUserTextIncludes: 'FAST_RESPONSE_MARKER',
217
- },
218
- then: {
219
- parts: [
220
- { type: 'stream-start', warnings: [] },
221
- { type: 'text-start', id: 'fast' },
222
- { type: 'text-delta', id: 'fast', delta: 'fast-response-done' },
223
- { type: 'text-end', id: 'fast' },
224
- {
225
- type: 'finish',
226
- finishReason: 'stop',
227
- usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
228
- },
229
- ],
230
- partDelaysMs: [0, 100, 0, 0, 0],
231
- },
232
- }
233
-
234
- // Default: matches any user message (fallback)
235
- const defaultReply: DeterministicMatcher = {
236
- id: 'default-reply',
237
- priority: 1,
238
- when: {
239
- lastMessageRole: 'user',
240
- },
241
- then: {
242
- parts: [
243
- { type: 'stream-start', warnings: [] },
244
- { type: 'text-start', id: 'default' },
245
- { type: 'text-delta', id: 'default', delta: 'session-reply' },
246
- { type: 'text-end', id: 'default' },
247
- {
248
- type: 'finish',
249
- finishReason: 'stop',
250
- usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
251
- },
252
- ],
253
- partDelaysMs: [0, 100, 0, 0, 0],
254
- },
255
- }
256
-
257
- // Tool followup: when the last message is a tool result
258
- const toolFollowup: DeterministicMatcher = {
259
- id: 'tool-followup',
260
- priority: 50,
261
- when: {
262
- lastMessageRole: 'tool',
263
- },
264
- then: {
265
- parts: [
266
- { type: 'stream-start', warnings: [] },
267
- { type: 'text-start', id: 'tool-followup' },
268
- { type: 'text-delta', id: 'tool-followup', delta: 'tool done' },
269
- { type: 'text-end', id: 'tool-followup' },
270
- {
271
- type: 'finish',
272
- finishReason: 'stop',
273
- usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
274
- },
275
- ],
276
- },
277
- }
278
-
279
- return [slowResponse, fastResponse, toolFollowup, defaultReply]
280
- }
281
-
282
- // ── Test constants ───────────────────────────────────────────────
283
-
284
- const TEST_USER_ID = '300000000000000777'
285
- const TEXT_CHANNEL_ID = '300000000000000778'
286
-
287
- // ── Test suite ───────────────────────────────────────────────────
288
-
289
- e2eTest('voice message handling', () => {
290
- let directories: ReturnType<typeof createRunDirectories>
291
- let discord: DigitalDiscord
292
- let botClient: Client
293
- let previousDefaultVerbosity = store.getState().defaultVerbosity
294
- let testStartTime = Date.now()
295
-
296
- beforeAll(async () => {
297
- testStartTime = Date.now()
298
- directories = createRunDirectories()
299
- const lockPort = chooseLockPort({ key: TEXT_CHANNEL_ID })
300
-
301
- process.env['KIMAKI_LOCK_PORT'] = String(lockPort)
302
- setDataDir(directories.dataDir)
303
- previousDefaultVerbosity = store.getState().defaultVerbosity
304
- store.setState({ defaultVerbosity: 'tools_and_text' })
305
-
306
- const digitalDiscordDbPath = path.join(
307
- directories.dataDir,
308
- 'digital-discord.db',
309
- )
310
-
311
- discord = new DigitalDiscord({
312
- guild: {
313
- name: 'Voice E2E Guild',
314
- ownerId: TEST_USER_ID,
315
- },
316
- channels: [
317
- {
318
- id: TEXT_CHANNEL_ID,
319
- name: 'voice-e2e',
320
- type: ChannelType.GuildText,
321
- },
322
- ],
323
- users: [
324
- {
325
- id: TEST_USER_ID,
326
- username: 'voice-tester',
327
- },
328
- ],
329
- dbUrl: `file:${digitalDiscordDbPath}`,
330
- })
331
-
332
- await discord.start()
333
-
334
- const providerNpm = url
335
- .pathToFileURL(
336
- path.resolve(
337
- process.cwd(),
338
- '..',
339
- 'opencode-deterministic-provider',
340
- 'src',
341
- 'index.ts',
342
- ),
343
- )
344
- .toString()
345
-
346
- const opencodeConfig = buildDeterministicOpencodeConfig({
347
- providerName: 'deterministic-provider',
348
- providerNpm,
349
- model: 'deterministic-v2',
350
- smallModel: 'deterministic-v2',
351
- settings: {
352
- strict: false,
353
- matchers: createDeterministicMatchers(),
354
- },
355
- })
356
- fs.writeFileSync(
357
- path.join(directories.projectDirectory, 'opencode.json'),
358
- JSON.stringify(opencodeConfig, null, 2),
359
- )
360
-
361
- const dbPath = path.join(directories.dataDir, 'discord-sessions.db')
362
- const hranaResult = await startHranaServer({ dbPath })
363
- if (hranaResult instanceof Error) {
364
- throw hranaResult
365
- }
366
- process.env['KIMAKI_DB_URL'] = hranaResult
367
- await initDatabase()
368
- await setBotToken(discord.botUserId, discord.botToken)
369
-
370
- await setChannelDirectory({
371
- channelId: TEXT_CHANNEL_ID,
372
- directory: directories.projectDirectory,
373
- channelType: 'text',
374
- })
375
- await setChannelVerbosity(TEXT_CHANNEL_ID, 'tools_and_text')
376
-
377
- botClient = createDiscordJsClient({ restUrl: discord.restUrl })
378
- await startDiscordBot({
379
- token: discord.botToken,
380
- appId: discord.botUserId,
381
- discordClient: botClient,
382
- })
383
-
384
- // Pre-warm the opencode server
385
- const warmup = await initializeOpencodeForDirectory(
386
- directories.projectDirectory,
387
- )
388
- if (warmup instanceof Error) {
389
- throw warmup
390
- }
391
- }, 20_000)
392
-
393
- afterAll(async () => {
394
- // Reset deterministic transcription
395
- setDeterministicTranscription(null)
396
-
397
- if (directories) {
398
- await cleanupTestSessions({
399
- projectDirectory: directories.projectDirectory,
400
- testStartTime,
401
- })
402
- }
403
-
404
- if (botClient) {
405
- void botClient.destroy()
406
- }
407
-
408
- await stopOpencodeServer()
409
- await Promise.all([
410
- closeDatabase().catch(() => {
411
- return
412
- }),
413
- stopHranaServer().catch(() => {
414
- return
415
- }),
416
- discord?.stop().catch(() => {
417
- return
418
- }),
419
- ])
420
-
421
- delete process.env['KIMAKI_LOCK_PORT']
422
- delete process.env['KIMAKI_DB_URL']
423
- store.setState({ defaultVerbosity: previousDefaultVerbosity })
424
- if (directories) {
425
- fs.rmSync(directories.dataDir, { recursive: true, force: true })
426
- }
427
- }, 5_000)
428
-
429
- beforeEach(() => {
430
- // Reset deterministic transcription before each test to prevent leakage
431
- // from a failed test that set it but didn't clean up
432
- setDeterministicTranscription(null)
433
- })
434
-
435
- // ── Test 1: Voice message in a channel creates thread + session ──
436
-
437
- // Vitest disallows inline snapshots in test.each; register separate tests instead.
438
- for (const sessionAction of ['btw', 'new-session'] as const) {
439
- test(
440
- `voice ${sessionAction} routing creates a separate thread with the correct history`,
441
- async () => {
442
- await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID)
443
- .sendMessage({ content: `Source history for voice ${sessionAction}` })
444
- const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
445
- timeout: 4_000,
446
- predicate: (candidate) => Boolean(candidate.name?.includes(`Source history for voice ${sessionAction}`)),
447
- })
448
- await waitForFooterMessage({ discord, threadId: thread.id, timeout: 4_000 })
449
- const sourceSessionId = await getThreadSession(thread.id)
450
- await createPendingWorkspace({
451
- threadId: thread.id,
452
- workspaceType: 'worktree',
453
- workspaceName: 'voice-workspace',
454
- projectDirectory: directories.projectDirectory,
455
- })
456
- await setWorkspaceReady({
457
- threadId: thread.id,
458
- workspaceId: 'voice-workspace-id',
459
- workspaceDirectory: directories.projectDirectory,
460
- })
461
- const prompt = `Explain voice routing ${sessionAction}`
462
- setDeterministicTranscription({ transcription: prompt, queueMessage: false, sessionAction })
463
- await discord.thread(thread.id).user(TEST_USER_ID).sendVoiceMessage()
464
- const confirmation = sessionAction === 'btw' ? 'Session forked!' : 'Created new session in'
465
- const routingMessages = await waitForBotMessageContaining({
466
- discord,
467
- threadId: thread.id,
468
- text: confirmation,
469
- timeout: 4_000,
470
- })
471
- // OpenCode can rename the destination before discovery; follow its confirmed ID.
472
- const targetId = routingMessages.find((message) => message.content.includes(confirmation))
473
- ?.content.match(/<#(\d+)>/)?.[1]
474
- if (!targetId) throw new Error('Expected destination thread link')
475
- const target = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
476
- timeout: 4_000,
477
- predicate: (candidate) => candidate.id === targetId,
478
- })
479
- await waitForFooterMessage({ discord, threadId: target.id, timeout: 4_000 })
480
- const th = discord.thread(target.id)
481
- const transcript = {
482
- source: (await discord.thread(thread.id).text()).replaceAll(target.id, 'TARGET_THREAD'),
483
- target: (await th.text()).replaceAll(thread.id, 'SOURCE_THREAD'),
484
- }
485
- if (sessionAction === 'btw') {
486
- expect(transcript).toMatchInlineSnapshot(`
487
- {
488
- "source": "--- from: user (voice-tester)
489
- Source history for voice btw
490
- --- from: assistant (TestBot)
491
- -# *using deterministic-provider/deterministic-v2*
492
- > session-reply
493
- -# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
494
- --- from: user (voice-tester)
495
- [attachment: voice-message.ogg]
496
- --- from: assistant (TestBot)
497
- 🎤 Transcribing voice message...
498
- 📝 **Transcribed message:** Explain voice routing btw
499
- Session forked! Continue in <#TARGET_THREAD>",
500
- "target": "--- from: assistant (TestBot)
501
- Reusing context from <#SOURCE_THREAD> to answer prompt...
502
- Explain voice routing btw
503
- > session-reply
504
- -# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*",
505
- }
506
- `)
507
- } else {
508
- expect(transcript).toMatchInlineSnapshot(`
509
- {
510
- "source": "--- from: user (voice-tester)
511
- Source history for voice new-session
512
- --- from: assistant (TestBot)
513
- -# *using deterministic-provider/deterministic-v2*
514
- > session-reply
515
- -# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
516
- --- from: user (voice-tester)
517
- [attachment: voice-message.ogg]
518
- --- from: assistant (TestBot)
519
- 🎤 Transcribing voice message...
520
- 📝 **Transcribed message:** Explain voice routing new-session
521
- Created new session in <#TARGET_THREAD>",
522
- "target": "--- from: assistant (TestBot)
523
- **Starting OpenCode session**
524
- Explain voice routing new-session
525
- -# *using deterministic-provider/deterministic-v2*
526
- > session-reply
527
- -# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*",
528
- }
529
- `)
530
- }
531
- const targetSessionId = await getThreadSession(target.id)
532
- expect(targetSessionId).toBeTruthy()
533
- expect(targetSessionId).not.toBe(sourceSessionId)
534
- expect(await getThreadWorktreeOrWorkspace(target.id)).toMatchObject({
535
- status: 'ready',
536
- workspace_type: 'worktree',
537
- workspace_name: 'voice-workspace',
538
- workspace_id: 'voice-workspace-id',
539
- workspace_directory: directories.projectDirectory,
540
- })
541
- const client = getOpencodeClientForTest(directories.projectDirectory)
542
- const targetMessages = await client.session.messages({ sessionID: targetSessionId! })
543
- const targetTexts = getUserTexts(targetMessages.data ?? []).join('\n')
544
- expect(targetTexts).toContain(prompt)
545
- expect(targetTexts).toContain(`Voice message transcription from Discord user:\n${prompt}`)
546
- expect(targetTexts.includes(`Source history for voice ${sessionAction}`)).toBe(sessionAction === 'btw')
547
- const sourceMessages = await client.session.messages({ sessionID: sourceSessionId! })
548
- expect(getUserTexts(sourceMessages.data ?? []).join('\n')).not.toContain(prompt)
549
- },
550
- )
551
- }
552
-
553
- test(
554
- 'voice message in channel creates thread and starts session',
555
- async () => {
556
- setDeterministicTranscription({
557
- transcription: 'Fix the login bug in auth.ts',
558
- queueMessage: false,
559
- })
560
-
561
- // Send voice message in the text channel
562
- await discord
563
- .channel(TEXT_CHANNEL_ID)
564
- .user(TEST_USER_ID)
565
- .sendVoiceMessage()
566
-
567
- // Thread should be created and renamed to the transcription text
568
- const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
569
- timeout: 4_000,
570
- predicate: (t) => {
571
- return t.name?.includes('Fix the login bug') ?? false
572
- },
573
- })
574
- expect(thread).toBeDefined()
575
-
576
- const th = discord.thread(thread.id)
577
-
578
- // Bot should post "Transcribing..." then "Transcribed message: ..."
579
- await waitForBotMessageContaining({
580
- discord,
581
- threadId: thread.id,
582
- userId: TEST_USER_ID,
583
- text: 'Transcribing voice message',
584
- timeout: 4_000,
585
- })
586
-
587
- await waitForBotMessageContaining({
588
- discord,
589
- threadId: thread.id,
590
- userId: TEST_USER_ID,
591
- text: 'Fix the login bug in auth.ts',
592
- timeout: 4_000,
593
- })
594
-
595
- // Session should get the transcribed prompt and respond
596
- const sessionReply = await th.waitForBotReply({ timeout: 4_000 })
597
- expect(sessionReply).toBeDefined()
598
-
599
- await waitForFooterMessage({
600
- discord,
601
- threadId: thread.id,
602
- timeout: 4_000,
603
- })
604
-
605
- // Assert thread state has a session and no queued messages after footer.
606
- const finalState = await waitForThreadState({
607
- threadId: thread.id,
608
- predicate: (state) => {
609
- return Boolean(state.sessionId) && state.queueItems.length === 0
610
- },
611
- timeout: 4_000,
612
- description: 'voice turn settled with empty queue',
613
- })
614
-
615
- await waitForFooterMessage({
616
- discord,
617
- threadId: thread.id,
618
- timeout: 4_000,
619
- })
620
-
621
- expect(await th.text()).toMatchInlineSnapshot(`
622
- "--- from: user (voice-tester)
623
- [attachment: voice-message.ogg]
624
- --- from: assistant (TestBot)
625
- 🎤 Transcribing voice message...
626
- 📝 **Transcribed message:** Fix the login bug in auth.ts
627
- -# *using deterministic-provider/deterministic-v2*
628
- > session-reply
629
- -# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
630
- `)
631
- expect(finalState.sessionId).toBeDefined()
632
-
633
- // Verify OpenCode session received the transcribed voice message as a prompt
634
- const messages = await waitForSessionMessages({
635
- projectDirectory: directories.projectDirectory,
636
- sessionID: finalState.sessionId!,
637
- timeout: 4_000,
638
- description: 'voice transcription prompt sent to session',
639
- predicate: (all) => {
640
- const userTexts = getUserTexts(all)
641
- return userTexts.some((text) => text.includes('Fix the login bug in auth.ts'))
642
- },
643
- })
644
- const userTexts = getUserTexts(messages)
645
- expect(userTexts.some((t) => t.includes('Fix the login bug in auth.ts'))).toBe(true)
646
- // Session should have at least one assistant response
647
- const assistantTexts = getAssistantTexts(messages)
648
- expect(assistantTexts.length).toBeGreaterThan(0)
649
- },
650
- 8_000,
651
- )
652
-
653
- test(
654
- 'first voice message resumes after the user saves an API key',
655
- async () => {
656
- setDeterministicTranscription({
657
- transcription: 'Resume the original voice note',
658
- queueMessage: false,
659
- requireApiKey: true,
660
- })
661
-
662
- await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendVoiceMessage()
663
-
664
- const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
665
- timeout: 4_000,
666
- predicate: (candidate) => candidate.name === 'Voice Message',
667
- })
668
- const th = discord.thread(thread.id)
669
- const messages = await waitForBotMessageContaining({
670
- discord,
671
- threadId: thread.id,
672
- userId: TEST_USER_ID,
673
- text: 'Voice transcription requires an API key',
674
- timeout: 4_000,
675
- })
676
- const keyMessage = messages.find((message) => {
677
- return message.content.includes('Voice transcription requires an API key')
678
- })
679
- if (!keyMessage) throw new Error('Expected API key prompt message')
680
-
681
- const buttonData = JSON.stringify(keyMessage.components)
682
- const buttonCustomId = buttonData.match(
683
- /"custom_id":"(transcription_apikey:[^"]+)"/,
684
- )?.[1]
685
- if (!buttonCustomId) throw new Error('Expected API key button custom id')
686
-
687
- const buttonInteraction = await th.user(TEST_USER_ID).clickButton({
688
- messageId: keyMessage.id,
689
- customId: buttonCustomId,
690
- })
691
- await th.waitForInteractionAck({
692
- interactionId: buttonInteraction.id,
693
- timeout: 4_000,
694
- })
695
- const modalResponse = await th.getInteractionResponse(buttonInteraction.id)
696
- const modalCustomId = modalResponse?.data?.match(
697
- /"custom_id":"(transcription_apikey_modal:[^"]+)"/,
698
- )?.[1]
699
- if (!modalCustomId) throw new Error('Expected API key modal custom id')
700
-
701
- const modalInteraction = await th.user(TEST_USER_ID).submitModal({
702
- customId: modalCustomId,
703
- fields: [{ customId: 'apikey', value: 'AIza-test-key' }],
704
- })
705
- await th.waitForInteractionAck({
706
- interactionId: modalInteraction.id,
707
- timeout: 4_000,
708
- })
709
-
710
- await waitForBotMessageContaining({
711
- discord,
712
- threadId: thread.id,
713
- userId: TEST_USER_ID,
714
- text: 'Resume the original voice note',
715
- timeout: 4_000,
716
- })
717
- await waitForFooterMessage({
718
- discord,
719
- threadId: thread.id,
720
- timeout: 4_000,
721
- })
722
-
723
- expect(await th.text()).toMatchInlineSnapshot(`
724
- "--- from: user (voice-tester)
725
- [attachment: voice-message.ogg]
726
- --- from: assistant (TestBot)
727
- 🎤 Transcribing voice message...
728
- Voice transcription requires an API key (OpenAI or Gemini). Set one to enable voice message transcription.
729
- Gemini API key saved. Retrying the original voice message.
730
- 📝 **Transcribed message:** Resume the original voice note
731
- -# *using deterministic-provider/deterministic-v2*
732
- > session-reply
733
- -# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
734
- `)
735
-
736
- const finalState = getThreadState(thread.id)
737
- expect(finalState?.sessionId).toBeDefined()
738
- },
739
- 10_000,
740
- )
741
-
742
- test(
743
- 'voice attachment without content type still transcribes and avoids empty prompt dispatch',
744
- async () => {
745
- setDeterministicTranscription({
746
- transcription: 'Investigate the missing content type path',
747
- queueMessage: false,
748
- })
749
-
750
- await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
751
- content: '',
752
- attachments: [
753
- {
754
- id: 'voice-no-content-type',
755
- filename: 'voice-message.ogg',
756
- size: 1024,
757
- url: 'https://fake-cdn.discord.test/voice-no-content-type.ogg',
758
- proxy_url: 'https://fake-cdn.discord.test/voice-no-content-type.ogg',
759
- },
760
- ],
761
- })
762
-
763
- const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
764
- timeout: 4_000,
765
- predicate: (t) => {
766
- return t.name?.includes('Investigate the missing content type path') ?? false
767
- },
768
- })
769
-
770
- const th = discord.thread(thread.id)
771
-
772
- await waitForBotMessageContaining({
773
- discord,
774
- threadId: thread.id,
775
- userId: TEST_USER_ID,
776
- text: 'Transcribing voice message',
777
- timeout: 4_000,
778
- })
779
-
780
- await waitForBotMessageContaining({
781
- discord,
782
- threadId: thread.id,
783
- userId: TEST_USER_ID,
784
- text: 'Investigate the missing content type path',
785
- timeout: 4_000,
786
- })
787
-
788
- await waitForFooterMessage({
789
- discord,
790
- threadId: thread.id,
791
- timeout: 4_000,
792
- })
793
-
794
- const finalState = await waitForThreadState({
795
- threadId: thread.id,
796
- predicate: (state) => {
797
- return Boolean(state.sessionId) && state.queueItems.length === 0
798
- },
799
- timeout: 4_000,
800
- description: 'voice attachment without content type settled',
801
- })
802
-
803
- expect(await th.text()).toMatchInlineSnapshot(`
804
- "--- from: user (voice-tester)
805
- [attachment: voice-message.ogg]
806
- --- from: assistant (TestBot)
807
- 🎤 Transcribing voice message...
808
- 📝 **Transcribed message:** Investigate the missing content type path
809
- -# *using deterministic-provider/deterministic-v2*
810
- > session-reply
811
- -# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
812
- `)
813
-
814
- const messages = await waitForSessionMessages({
815
- projectDirectory: directories.projectDirectory,
816
- sessionID: finalState.sessionId!,
817
- timeout: 4_000,
818
- description: 'voice attachment without content type dispatched once',
819
- predicate: (all) => {
820
- const userTexts = getUserTexts(all)
821
- return userTexts.some((text) => {
822
- return text.includes('Investigate the missing content type path')
823
- })
824
- },
825
- })
826
-
827
- const userTexts = getUserTexts(messages)
828
- expect(userTexts).not.toContain('')
829
- expect(
830
- userTexts.some((text) => {
831
- return text.includes('Investigate the missing content type path')
832
- }),
833
- ).toBe(true)
834
- },
835
- 8_000,
836
- )
837
-
838
- // ── Test 2: Voice message in thread with idle session ──
839
-
840
- test(
841
- 'voice message in thread with idle session starts new request',
842
- async () => {
843
- // 1. Create a session with a text message first
844
- setDeterministicTranscription(null) // text message, no transcription
845
- await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
846
- content: 'FAST_RESPONSE_MARKER initial setup',
847
- })
848
-
849
- const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
850
- timeout: 4_000,
851
- predicate: (t) => {
852
- return t.name?.includes('FAST_RESPONSE_MARKER') ?? false
853
- },
854
- })
855
-
856
- const th = discord.thread(thread.id)
857
-
858
- // Wait for the initial setup turn to fully complete before sending voice.
859
- await waitForBotMessageContaining({
860
- discord,
861
- threadId: thread.id,
862
- userId: TEST_USER_ID,
863
- text: 'fast-response-done',
864
- timeout: 4_000,
865
- })
866
- await waitForFooterMessage({
867
- discord,
868
- threadId: thread.id,
869
- timeout: 4_000,
870
- })
871
-
872
- // 2. Now send a voice message to the idle session
873
- setDeterministicTranscription({
874
- transcription: 'Add error handling to the parser',
875
- queueMessage: false,
876
- })
877
-
878
- await th.user(TEST_USER_ID).sendVoiceMessage()
879
-
880
- // Bot should post transcription messages
881
- await waitForBotMessageContaining({
882
- discord,
883
- threadId: thread.id,
884
- userId: TEST_USER_ID,
885
- text: 'Transcribing voice message',
886
- timeout: 4_000,
887
- })
888
-
889
- await waitForBotMessageContaining({
890
- discord,
891
- threadId: thread.id,
892
- userId: TEST_USER_ID,
893
- text: 'Add error handling to the parser',
894
- timeout: 4_000,
895
- })
896
-
897
- await waitForBotMessageContaining({
898
- discord,
899
- threadId: thread.id,
900
- userId: TEST_USER_ID,
901
- text: 'session-reply',
902
- timeout: 4_000,
903
- })
904
-
905
- await waitForFooterMessage({
906
- discord,
907
- threadId: thread.id,
908
- timeout: 4_000,
909
- afterMessageIncludes: 'session-reply',
910
- afterAuthorId: discord.botUserId,
911
- })
912
-
913
- const finalState = getThreadState(thread.id)
914
- expect(await th.text()).toMatchInlineSnapshot(`
915
- "--- from: user (voice-tester)
916
- FAST_RESPONSE_MARKER initial setup
917
- --- from: assistant (TestBot)
918
- -# *using deterministic-provider/deterministic-v2*
919
- > fast-response-done
920
- -# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
921
- --- from: user (voice-tester)
922
- [attachment: voice-message.ogg]
923
- --- from: assistant (TestBot)
924
- 🎤 Transcribing voice message...
925
- 📝 **Transcribed message:** Add error handling to the parser
926
- > session-reply
927
- -# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
928
- `)
929
- expect(finalState?.sessionId).toBeDefined()
930
- if (!finalState?.sessionId) {
931
- throw new Error('Expected final state with sessionId')
932
- }
933
- expect(finalState.queueItems.length).toBe(0)
934
-
935
- // Verify the same OpenCode session received both prompts:
936
- // the initial text message AND the voice transcription
937
- const messages = await waitForSessionMessages({
938
- projectDirectory: directories.projectDirectory,
939
- sessionID: finalState.sessionId,
940
- timeout: 4_000,
941
- description: 'idle session receives voice transcription prompt',
942
- predicate: (all) => {
943
- const userTexts = getUserTexts(all)
944
- return (
945
- userTexts.some((t) => t.includes('FAST_RESPONSE_MARKER initial setup')) &&
946
- userTexts.some((t) => t.includes('Add error handling to the parser'))
947
- )
948
- },
949
- })
950
- const userTexts = getUserTexts(messages)
951
- expect(userTexts.some((t) => t.includes('FAST_RESPONSE_MARKER initial setup'))).toBe(true)
952
- expect(userTexts.some((t) => t.includes('Add error handling to the parser'))).toBe(true)
953
- // Both prompts should have gotten assistant responses
954
- const assistantTexts = getAssistantTexts(messages)
955
- expect(assistantTexts.length).toBeGreaterThanOrEqual(2)
956
- },
957
- 8_000,
958
- )
959
-
960
- // ── Test 3: Voice message queues behind running session (default) ──
961
-
962
- test.skip(
963
- 'voice message with queueMessage=false queues behind running session',
964
- async () => {
965
- // 1. Start a session with a slow response
966
- setDeterministicTranscription(null)
967
- await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
968
- content: 'SLOW_RESPONSE_MARKER start slow task',
969
- })
970
-
971
- const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
972
- timeout: 4_000,
973
- predicate: (t) => {
974
- return t.name?.includes('SLOW_RESPONSE_MARKER') ?? false
975
- },
976
- })
977
-
978
- const th = discord.thread(thread.id)
979
-
980
- // 2. Send voice message while session is running (default: queue)
981
- setDeterministicTranscription({
982
- transcription: 'Stop and do this instead',
983
- queueMessage: false,
984
- })
985
-
986
- await th.user(TEST_USER_ID).sendVoiceMessage()
987
-
988
- // 3. Wait for transcription to appear first
989
- await waitForBotMessageContaining({
990
- discord,
991
- threadId: thread.id,
992
- userId: TEST_USER_ID,
993
- text: 'Stop and do this instead',
994
- timeout: 4_000,
995
- })
996
-
997
- // queueMessage=false no longer interrupts by default, so we should NOT
998
- // receive the queued-position ack that queueMessage=true sends.
999
- const afterTranscription = await th.getMessages()
1000
- const hasQueuedAck = afterTranscription.some((m) => {
1001
- return (
1002
- m.author.id === discord.botUserId &&
1003
- m.content.includes('Queued at position')
1004
- )
1005
- })
1006
- expect(hasQueuedAck).toBe(false)
1007
-
1008
- const midState = getThreadState(thread.id)
1009
- expect(midState).toBeDefined()
1010
-
1011
- // 4. Wait for both runs to finish (slow prompt + queued transcription)
1012
- const finalState = await waitForThreadState({
1013
- threadId: thread.id,
1014
- predicate: (s) => {
1015
- return s.queueItems.length === 0
1016
- },
1017
- timeout: 8_000,
1018
- description: 'queue empty (default queued voice behavior)',
1019
- })
1020
- expect(finalState.sessionId).toBeDefined()
1021
- expect(finalState.queueItems.length).toBe(0)
1022
-
1023
- // Verify the OpenCode session processed both prompts sequentially.
1024
- const messages = await waitForSessionMessages({
1025
- projectDirectory: directories.projectDirectory,
1026
- sessionID: finalState.sessionId!,
1027
- timeout: 4_000,
1028
- description: 'default queue: original prompt + voice prompt',
1029
- predicate: (all) => {
1030
- const userTexts = getUserTexts(all)
1031
- const assistantTexts = getAssistantTexts(all)
1032
- return (
1033
- userTexts.some((t) => t.includes('SLOW_RESPONSE_MARKER start slow task')) &&
1034
- userTexts.some((t) => t.includes('Stop and do this instead')) &&
1035
- assistantTexts.some((t) => t.includes('slow-response-done')) &&
1036
- assistantTexts.some((t) => t.includes('session-reply'))
1037
- )
1038
- },
1039
- })
1040
- const userTexts = getUserTexts(messages)
1041
- // Both prompts were sent to the same session
1042
- expect(userTexts.some((t) => t.includes('SLOW_RESPONSE_MARKER start slow task'))).toBe(true)
1043
- expect(userTexts.some((t) => t.includes('Stop and do this instead'))).toBe(true)
1044
-
1045
- const assistantTexts = getAssistantTexts(messages)
1046
- expect(assistantTexts.some((t) => t.includes('slow-response-done'))).toBe(true)
1047
- expect(assistantTexts.some((t) => t.includes('session-reply'))).toBe(true)
1048
- },
1049
- 10_000,
1050
- )
1051
-
1052
- // ── Test 4: Voice message with queueMessage=true queues instead of interrupting ──
1053
-
1054
- test(
1055
- 'voice message with queueMessage=true queues behind running session',
1056
- async () => {
1057
- // 1. Start a session with a slow response
1058
- setDeterministicTranscription(null)
1059
- await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
1060
- content: 'SLOW_RESPONSE_MARKER start queued task',
1061
- })
1062
-
1063
- const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
1064
- timeout: 4_000,
1065
- predicate: (t) => {
1066
- return t.name?.includes('start queued task') ?? false
1067
- },
1068
- })
1069
-
1070
- const th = discord.thread(thread.id)
1071
-
1072
- // 2. Send voice message with queueMessage=true (should NOT interrupt)
1073
- setDeterministicTranscription({
1074
- transcription: 'Queue this task for later',
1075
- queueMessage: true,
1076
- })
1077
-
1078
- await waitForBotMessageContaining({
1079
- discord,
1080
- threadId: thread.id,
1081
- userId: TEST_USER_ID,
1082
- text: 'using deterministic-provider/deterministic-v2',
1083
- timeout: 4_000,
1084
- })
1085
-
1086
- await th.user(TEST_USER_ID).sendVoiceMessage()
1087
-
1088
- // 3. Transcription should appear, followed by queue notification
1089
- await waitForBotMessageContaining({
1090
- discord,
1091
- threadId: thread.id,
1092
- userId: TEST_USER_ID,
1093
- text: 'Queue this task for later',
1094
- timeout: 4_000,
1095
- })
1096
-
1097
- const messagesWithQueueAck = await waitForBotMessageContaining({
1098
- discord,
1099
- threadId: thread.id,
1100
- userId: TEST_USER_ID,
1101
- text: 'Queued at position',
1102
- timeout: 4_000,
1103
- })
1104
- const queueAckMessage = messagesWithQueueAck.find((message) => {
1105
- return (
1106
- message.author.id === discord.botUserId
1107
- && message.content.includes('Queued at position')
1108
- )
1109
- })
1110
- expect(queueAckMessage).toBeDefined()
1111
-
1112
- // 4. queueMessage=true should not interrupt the in-flight response.
1113
- await waitForBotMessageContaining({
1114
- discord,
1115
- threadId: thread.id,
1116
- userId: TEST_USER_ID,
1117
- text: 'slow-response-done',
1118
- timeout: 4_000,
1119
- })
1120
-
1121
- if (!queueAckMessage) {
1122
- throw new Error('Expected queue ack message')
1123
- }
1124
-
1125
- const dispatchPrefix = '» **voice-tester:** Voice message transcription from Discord user:'
1126
- const messagesWithDispatch = await waitForBotMessageContaining({
1127
- discord,
1128
- threadId: thread.id,
1129
- text: dispatchPrefix,
1130
- afterMessageId: queueAckMessage.id,
1131
- timeout: 8_000,
1132
- })
1133
- const dispatchMessage = messagesWithDispatch.find((message) => {
1134
- return (
1135
- message.author.id === discord.botUserId
1136
- && message.content.includes(dispatchPrefix)
1137
- )
1138
- })
1139
- expect(dispatchMessage).toBeDefined()
1140
-
1141
- if (!dispatchMessage) {
1142
- throw new Error('Expected queued dispatch indicator message')
1143
- }
1144
-
1145
- await waitForBotMessageContaining({
1146
- discord,
1147
- threadId: thread.id,
1148
- text: 'session-reply',
1149
- afterMessageId: dispatchMessage.id,
1150
- timeout: 8_000,
1151
- })
1152
-
1153
- // 5. Wait for the slow session to finish AND the queue to drain.
1154
- // Using waitForThreadState with a compound predicate avoids matching
1155
- // the transient 'idle' state from run A before run B starts.
1156
- const finalState = await waitForThreadState({
1157
- threadId: thread.id,
1158
- predicate: (s) => {
1159
- return s.queueItems.length === 0
1160
- },
1161
- timeout: 8_000,
1162
- description: 'queue empty (both runs completed)',
1163
- })
1164
-
1165
- await waitForFooterMessage({
1166
- discord,
1167
- threadId: thread.id,
1168
- timeout: 4_000,
1169
- afterMessageIncludes: 'session-reply',
1170
- afterAuthorId: discord.botUserId,
1171
- })
1172
-
1173
- expect(await th.text()).toMatchInlineSnapshot(`
1174
- "--- from: user (voice-tester)
1175
- SLOW_RESPONSE_MARKER start queued task
1176
- --- from: assistant (TestBot)
1177
- -# *using deterministic-provider/deterministic-v2*
1178
- --- from: user (voice-tester)
1179
- [attachment: voice-message.ogg]
1180
- --- from: assistant (TestBot)
1181
- 🎤 Transcribing voice message...
1182
- 📝 **Transcribed message:** Queue this task for later
1183
- -# Queued at position 1. Edit or delete your message to update the queue
1184
- > slow-response-done
1185
- -# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
1186
- » **voice-tester:** Voice message transcription from Discord user:
1187
- Queue this task for later
1188
- > session-reply
1189
- -# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
1190
- `)
1191
- expect(finalState.queueItems.length).toBe(0)
1192
-
1193
- // Verify the OpenCode session processed BOTH prompts sequentially:
1194
- // the slow initial prompt completed, then the queued voice prompt ran
1195
- const messages = await waitForSessionMessages({
1196
- projectDirectory: directories.projectDirectory,
1197
- sessionID: finalState.sessionId!,
1198
- timeout: 4_000,
1199
- description: 'queue: both prompts processed with responses',
1200
- predicate: (all) => {
1201
- const userTexts = getUserTexts(all)
1202
- const assistantTexts = getAssistantTexts(all)
1203
- return (
1204
- userTexts.some((t) => t.includes('SLOW_RESPONSE_MARKER start queued task')) &&
1205
- userTexts.some((t) => t.includes('Queue this task for later')) &&
1206
- assistantTexts.some((t) => t.includes('slow-response-done')) &&
1207
- assistantTexts.some((t) => t.includes('session-reply'))
1208
- )
1209
- },
1210
- })
1211
- const userTexts = getUserTexts(messages)
1212
- const assistantTexts = getAssistantTexts(messages)
1213
- // Both prompts sent to the session
1214
- expect(userTexts.some((t) => t.includes('SLOW_RESPONSE_MARKER start queued task'))).toBe(true)
1215
- expect(userTexts.some((t) => t.includes('Queue this task for later'))).toBe(true)
1216
- // Both got responses (slow response + default reply for queued message)
1217
- expect(assistantTexts.some((t) => t.includes('slow-response-done'))).toBe(true)
1218
- expect(assistantTexts.some((t) => t.includes('session-reply'))).toBe(true)
1219
- // No abort errors — the queue preserved the first run
1220
- const abortedAssistant = messages.find((m) => {
1221
- return m.info.role === 'assistant' && m.info.error?.name === 'MessageAbortedError'
1222
- })
1223
- expect(abortedAssistant).toBeUndefined()
1224
- },
1225
- 12_000,
1226
- )
1227
-
1228
- // ── Test 5: Slow transcription finishes after session becomes idle (race condition) ──
1229
-
1230
- test(
1231
- 'slow transcription completing after session finishes is handled correctly',
1232
- async () => {
1233
- // This tests the race condition where:
1234
- // 1. Session starts with a fast response (~100ms)
1235
- // 2. Voice message is sent simultaneously with slow transcription (500ms)
1236
- // 3. The fast session finishes BEFORE transcription completes
1237
- // 4. When transcription completes, the session is idle → should start new request
1238
-
1239
- // 1. Start a session with a fast response
1240
- setDeterministicTranscription(null)
1241
- await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
1242
- content: 'FAST_RESPONSE_MARKER quick task',
1243
- })
1244
-
1245
- const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
1246
- timeout: 4_000,
1247
- predicate: (t) => {
1248
- return t.name?.includes('quick task') ?? false
1249
- },
1250
- })
1251
-
1252
- const th = discord.thread(thread.id)
1253
-
1254
- // Wait for the first run to complete before sending voice.
1255
- await th.waitForBotReply({ timeout: 4_000 })
1256
- await waitForFooterMessage({
1257
- discord,
1258
- threadId: thread.id,
1259
- timeout: 4_000,
1260
- })
1261
-
1262
- // 2. Now send voice message with slow transcription
1263
- // The fast response completes in ~100ms, but transcription takes 500ms.
1264
- // By the time transcription returns, the session is already idle.
1265
- setDeterministicTranscription({
1266
- transcription: 'Delayed transcription result',
1267
- queueMessage: false,
1268
- delayMs: 500,
1269
- })
1270
-
1271
- await th.user(TEST_USER_ID).sendVoiceMessage()
1272
-
1273
- // 3. The transcription should complete after the session finishes
1274
- // and the transcribed message should be processed as a new request
1275
- await waitForBotMessageContaining({
1276
- discord,
1277
- threadId: thread.id,
1278
- userId: TEST_USER_ID,
1279
- text: 'Delayed transcription result',
1280
- timeout: 4_000,
1281
- })
1282
-
1283
- await waitForFooterMessage({
1284
- discord,
1285
- threadId: thread.id,
1286
- timeout: 4_000,
1287
- afterMessageIncludes: 'Delayed transcription result',
1288
- afterAuthorId: discord.botUserId,
1289
- })
1290
-
1291
- // 4. Session should process the delayed transcription and settle.
1292
- const finalState = await waitForThreadState({
1293
- threadId: thread.id,
1294
- predicate: (state) => {
1295
- return Boolean(state.sessionId) && state.queueItems.length === 0
1296
- },
1297
- timeout: 4_000,
1298
- description: 'delayed transcription settled with empty queue',
1299
- })
1300
-
1301
- expect(await th.text()).toMatchInlineSnapshot(`
1302
- "--- from: user (voice-tester)
1303
- FAST_RESPONSE_MARKER quick task
1304
- --- from: assistant (TestBot)
1305
- -# *using deterministic-provider/deterministic-v2*
1306
- > fast-response-done
1307
- -# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
1308
- --- from: user (voice-tester)
1309
- [attachment: voice-message.ogg]
1310
- --- from: assistant (TestBot)
1311
- 🎤 Transcribing voice message...
1312
- 📝 **Transcribed message:** Delayed transcription result
1313
- > session-reply
1314
- -# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
1315
- `)
1316
- expect(finalState.sessionId).toBeDefined()
1317
- expect(finalState.queueItems.length).toBe(0)
1318
-
1319
- // 5. Verify the OpenCode session processed both prompts on the same session:
1320
- // the fast text message completed first, then the delayed voice transcription
1321
- const sessionMessages = await waitForSessionMessages({
1322
- projectDirectory: directories.projectDirectory,
1323
- sessionID: finalState.sessionId!,
1324
- timeout: 4_000,
1325
- description: 'race: both prompts processed with responses on same session',
1326
- predicate: (all) => {
1327
- const userTexts = getUserTexts(all)
1328
- const aTexts = getAssistantTexts(all)
1329
- return (
1330
- userTexts.some((t) => t.includes('FAST_RESPONSE_MARKER quick task')) &&
1331
- userTexts.some((t) => t.includes('Delayed transcription result')) &&
1332
- aTexts.length >= 2
1333
- )
1334
- },
1335
- })
1336
- const userTexts = getUserTexts(sessionMessages)
1337
- expect(userTexts.some((t) => t.includes('FAST_RESPONSE_MARKER quick task'))).toBe(true)
1338
- expect(userTexts.some((t) => t.includes('Delayed transcription result'))).toBe(true)
1339
- // Both prompts got assistant responses (no aborts — second arrived after first finished)
1340
- const assistantTexts = getAssistantTexts(sessionMessages)
1341
- expect(assistantTexts.length).toBeGreaterThanOrEqual(2)
1342
- const abortedAssistant = sessionMessages.find((m) => {
1343
- return m.info.role === 'assistant' && m.info.error?.name === 'MessageAbortedError'
1344
- })
1345
- expect(abortedAssistant).toBeUndefined()
1346
- },
1347
- 8_000,
1348
- )
1349
-
1350
- // ── Test 6: Slow transcription with queueMessage=true arriving after idle queue drain ──
1351
-
1352
- test(
1353
- 'slow queued transcription completing after session idle is dispatched',
1354
- async () => {
1355
- // Reproduces the bug where a voice message with queueMessage=true has
1356
- // slow transcription that completes after the session is already idle.
1357
- // The message gets inserted into the local queue but was never drained
1358
- // because handleSessionIdle() didn't call tryDrainQueue().
1359
- //
1360
- // 1. Send a fast text message → session starts and finishes quickly
1361
- // 2. Send a voice message with queueMessage=true and slow transcription
1362
- // 3. Fast session finishes → handleSessionIdle fires
1363
- // 4. Transcription completes → enqueueViaLocalQueue adds item
1364
- // 5. tryDrainQueue in handleSessionIdle should pick it up
1365
-
1366
- // 1. Start a session with a fast response
1367
- setDeterministicTranscription(null)
1368
- await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
1369
- content: 'FAST_RESPONSE_MARKER fast before queued voice',
1370
- })
1371
-
1372
- const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
1373
- timeout: 4_000,
1374
- predicate: (t) => {
1375
- return t.name?.includes('fast before queued voice') ?? false
1376
- },
1377
- })
1378
-
1379
- const th = discord.thread(thread.id)
1380
-
1381
- // Wait for the first run to fully complete before sending the queued voice message.
1382
- await th.waitForBotReply({ timeout: 4_000 })
1383
- await waitForFooterMessage({
1384
- discord,
1385
- threadId: thread.id,
1386
- timeout: 4_000,
1387
- })
1388
-
1389
- // 2. Send voice message with queueMessage=true AND slow transcription.
1390
- // Session is already idle when this arrives. The transcription delay
1391
- // means the message enters the local queue after idle has fired.
1392
- setDeterministicTranscription({
1393
- transcription: 'Queued voice after idle',
1394
- queueMessage: true,
1395
- delayMs: 500,
1396
- })
1397
-
1398
- await th.user(TEST_USER_ID).sendVoiceMessage()
1399
-
1400
- // 3. The transcription should complete, and even though queueMessage=true
1401
- // routes through the local queue, the item should be drained immediately
1402
- // because the session is idle. No dispatch indicator (» prefix) appears
1403
- // because the message is dispatched immediately by enqueueViaLocalQueue's
1404
- // tryDrainQueue (showIndicator=false for first drain).
1405
- await waitForBotMessageContaining({
1406
- discord,
1407
- threadId: thread.id,
1408
- userId: TEST_USER_ID,
1409
- text: 'Queued voice after idle',
1410
- timeout: 4_000,
1411
- })
1412
-
1413
- // Wait for the queued message response and footer
1414
- await waitForBotMessageContaining({
1415
- discord,
1416
- threadId: thread.id,
1417
- text: 'session-reply',
1418
- timeout: 4_000,
1419
- })
1420
-
1421
- await waitForFooterMessage({
1422
- discord,
1423
- threadId: thread.id,
1424
- timeout: 4_000,
1425
- afterMessageIncludes: 'session-reply',
1426
- afterAuthorId: discord.botUserId,
1427
- })
1428
-
1429
- // 4. Final state: queue should be empty
1430
- const finalState = await waitForThreadState({
1431
- threadId: thread.id,
1432
- predicate: (state) => {
1433
- return Boolean(state.sessionId) && state.queueItems.length === 0
1434
- },
1435
- timeout: 4_000,
1436
- description: 'queued voice after idle settled with empty queue',
1437
- })
1438
-
1439
- expect(await th.text()).toMatchInlineSnapshot(`
1440
- "--- from: user (voice-tester)
1441
- FAST_RESPONSE_MARKER fast before queued voice
1442
- --- from: assistant (TestBot)
1443
- -# *using deterministic-provider/deterministic-v2*
1444
- > fast-response-done
1445
- -# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
1446
- --- from: user (voice-tester)
1447
- [attachment: voice-message.ogg]
1448
- --- from: assistant (TestBot)
1449
- 🎤 Transcribing voice message...
1450
- 📝 **Transcribed message:** Queued voice after idle
1451
- > session-reply
1452
- -# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
1453
- `)
1454
- expect(finalState.sessionId).toBeDefined()
1455
- expect(finalState.queueItems.length).toBe(0)
1456
-
1457
- // 5. Verify the OpenCode session processed both prompts
1458
- const sessionMessages = await waitForSessionMessages({
1459
- projectDirectory: directories.projectDirectory,
1460
- sessionID: finalState.sessionId!,
1461
- timeout: 4_000,
1462
- description: 'queued-voice-idle: both prompts processed',
1463
- predicate: (all) => {
1464
- const userTexts = getUserTexts(all)
1465
- const aTexts = getAssistantTexts(all)
1466
- return (
1467
- userTexts.some((t) => t.includes('FAST_RESPONSE_MARKER fast before queued voice')) &&
1468
- userTexts.some((t) => t.includes('Queued voice after idle')) &&
1469
- aTexts.length >= 2
1470
- )
1471
- },
1472
- })
1473
- const userTexts = getUserTexts(sessionMessages)
1474
- expect(userTexts.some((t) => t.includes('FAST_RESPONSE_MARKER fast before queued voice'))).toBe(true)
1475
- expect(userTexts.some((t) => t.includes('Queued voice after idle'))).toBe(true)
1476
- const assistantTexts = getAssistantTexts(sessionMessages)
1477
- expect(assistantTexts.length).toBeGreaterThanOrEqual(2)
1478
- },
1479
- 10_000,
1480
- )
1481
-
1482
- test.skipIf(!process.env.OPENAI_API_KEY)(
1483
- 'transcribes a real ogg voice note and starts a session',
1484
- async () => {
1485
- const audioPath = path.resolve(process.cwd(), 'scripts', 'example-audio.ogg')
1486
- const audio = fs.readFileSync(audioPath)
1487
- const server = http.createServer((req, res) => {
1488
- if (req.url !== '/voice-message.ogg') {
1489
- res.writeHead(404).end()
1490
- return
1491
- }
1492
- res.writeHead(200, { 'content-type': 'audio/ogg' })
1493
- res.end(audio)
1494
- })
1495
- await new Promise<void>((resolve) => {
1496
- server.listen(0, '127.0.0.1', resolve)
1497
- })
1498
- const address = server.address()
1499
- if (!address || typeof address === 'string') throw new Error('Audio server did not bind')
1500
- const audioUrl = `http://127.0.0.1:${address.port}/voice-message.ogg`
1501
-
1502
- setDeterministicTranscription(null)
1503
- await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendVoiceMessage({ url: audioUrl })
1504
-
1505
- const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
1506
- timeout: 20_000,
1507
- predicate: (candidate) => Boolean(candidate.name?.toLowerCase().includes('hear me')),
1508
- })
1509
- await waitForFooterMessage({ discord, threadId: thread.id, timeout: 20_000 })
1510
- expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
1511
- "--- from: user (voice-tester)
1512
- [attachment: voice-message.ogg]
1513
- --- from: assistant (TestBot)
1514
- 🎤 Transcribing voice message...
1515
- 📝 **Transcribed message:** Yo, can you hear me loud and clear? What a shit model. Why are you so slow?
1516
- -# *using deterministic-provider/deterministic-v2*
1517
- > session-reply
1518
- -# *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2* <@300000000000000777>"
1519
- `)
1520
- server.close()
1521
- },
1522
- 30_000,
1523
- )
1524
- })