@otto-assistant/bridge 0.4.92

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 (483) hide show
  1. package/bin.js +2 -0
  2. package/dist/agent-model.e2e.test.js +755 -0
  3. package/dist/ai-tool-to-genai.js +233 -0
  4. package/dist/ai-tool-to-genai.test.js +267 -0
  5. package/dist/ai-tool.js +6 -0
  6. package/dist/anthropic-auth-plugin.js +728 -0
  7. package/dist/anthropic-auth-plugin.test.js +125 -0
  8. package/dist/anthropic-auth-state.js +231 -0
  9. package/dist/bin.js +90 -0
  10. package/dist/channel-management.js +227 -0
  11. package/dist/cli-parsing.test.js +137 -0
  12. package/dist/cli-send-thread.e2e.test.js +356 -0
  13. package/dist/cli.js +3276 -0
  14. package/dist/commands/abort.js +65 -0
  15. package/dist/commands/action-buttons.js +245 -0
  16. package/dist/commands/add-project.js +113 -0
  17. package/dist/commands/agent.js +335 -0
  18. package/dist/commands/ask-question.js +274 -0
  19. package/dist/commands/btw.js +116 -0
  20. package/dist/commands/compact.js +120 -0
  21. package/dist/commands/context-usage.js +140 -0
  22. package/dist/commands/create-new-project.js +130 -0
  23. package/dist/commands/diff.js +63 -0
  24. package/dist/commands/file-upload.js +275 -0
  25. package/dist/commands/fork.js +220 -0
  26. package/dist/commands/gemini-apikey.js +70 -0
  27. package/dist/commands/login.js +885 -0
  28. package/dist/commands/mcp.js +239 -0
  29. package/dist/commands/memory-snapshot.js +24 -0
  30. package/dist/commands/mention-mode.js +44 -0
  31. package/dist/commands/merge-worktree.js +159 -0
  32. package/dist/commands/model-variant.js +364 -0
  33. package/dist/commands/model.js +776 -0
  34. package/dist/commands/new-worktree.js +366 -0
  35. package/dist/commands/paginated-select.js +57 -0
  36. package/dist/commands/permissions.js +274 -0
  37. package/dist/commands/queue.js +206 -0
  38. package/dist/commands/remove-project.js +115 -0
  39. package/dist/commands/restart-opencode-server.js +127 -0
  40. package/dist/commands/resume.js +149 -0
  41. package/dist/commands/run-command.js +79 -0
  42. package/dist/commands/screenshare.js +303 -0
  43. package/dist/commands/screenshare.test.js +20 -0
  44. package/dist/commands/session-id.js +78 -0
  45. package/dist/commands/session.js +176 -0
  46. package/dist/commands/share.js +80 -0
  47. package/dist/commands/tasks.js +205 -0
  48. package/dist/commands/types.js +2 -0
  49. package/dist/commands/undo-redo.js +305 -0
  50. package/dist/commands/unset-model.js +138 -0
  51. package/dist/commands/upgrade.js +42 -0
  52. package/dist/commands/user-command.js +155 -0
  53. package/dist/commands/verbosity.js +125 -0
  54. package/dist/commands/worktree-settings.js +43 -0
  55. package/dist/commands/worktrees.js +410 -0
  56. package/dist/condense-memory.js +33 -0
  57. package/dist/config.js +94 -0
  58. package/dist/context-awareness-plugin.js +363 -0
  59. package/dist/context-awareness-plugin.test.js +124 -0
  60. package/dist/critique-utils.js +95 -0
  61. package/dist/database.js +1310 -0
  62. package/dist/db.js +251 -0
  63. package/dist/db.test.js +138 -0
  64. package/dist/debounce-timeout.js +28 -0
  65. package/dist/debounced-process-flush.js +77 -0
  66. package/dist/discord-bot.js +1008 -0
  67. package/dist/discord-command-registration.js +524 -0
  68. package/dist/discord-urls.js +81 -0
  69. package/dist/discord-utils.js +591 -0
  70. package/dist/discord-utils.test.js +134 -0
  71. package/dist/errors.js +157 -0
  72. package/dist/escape-backticks.test.js +429 -0
  73. package/dist/event-stream-real-capture.e2e.test.js +533 -0
  74. package/dist/eventsource-parser.test.js +327 -0
  75. package/dist/exec-async.js +26 -0
  76. package/dist/external-opencode-sync.js +480 -0
  77. package/dist/format-tables.js +302 -0
  78. package/dist/format-tables.test.js +308 -0
  79. package/dist/forum-sync/config.js +79 -0
  80. package/dist/forum-sync/discord-operations.js +154 -0
  81. package/dist/forum-sync/index.js +5 -0
  82. package/dist/forum-sync/markdown.js +113 -0
  83. package/dist/forum-sync/sync-to-discord.js +417 -0
  84. package/dist/forum-sync/sync-to-files.js +190 -0
  85. package/dist/forum-sync/types.js +53 -0
  86. package/dist/forum-sync/watchers.js +307 -0
  87. package/dist/gateway-proxy-reconnect.e2e.test.js +394 -0
  88. package/dist/gateway-proxy.e2e.test.js +483 -0
  89. package/dist/genai-worker-wrapper.js +111 -0
  90. package/dist/genai-worker.js +311 -0
  91. package/dist/genai.js +232 -0
  92. package/dist/generated/browser.js +17 -0
  93. package/dist/generated/client.js +37 -0
  94. package/dist/generated/commonInputTypes.js +10 -0
  95. package/dist/generated/enums.js +52 -0
  96. package/dist/generated/internal/class.js +49 -0
  97. package/dist/generated/internal/prismaNamespace.js +253 -0
  98. package/dist/generated/internal/prismaNamespaceBrowser.js +223 -0
  99. package/dist/generated/models/bot_api_keys.js +1 -0
  100. package/dist/generated/models/bot_tokens.js +1 -0
  101. package/dist/generated/models/channel_agents.js +1 -0
  102. package/dist/generated/models/channel_directories.js +1 -0
  103. package/dist/generated/models/channel_mention_mode.js +1 -0
  104. package/dist/generated/models/channel_models.js +1 -0
  105. package/dist/generated/models/channel_verbosity.js +1 -0
  106. package/dist/generated/models/channel_worktrees.js +1 -0
  107. package/dist/generated/models/forum_sync_configs.js +1 -0
  108. package/dist/generated/models/global_models.js +1 -0
  109. package/dist/generated/models/ipc_requests.js +1 -0
  110. package/dist/generated/models/part_messages.js +1 -0
  111. package/dist/generated/models/scheduled_tasks.js +1 -0
  112. package/dist/generated/models/session_agents.js +1 -0
  113. package/dist/generated/models/session_events.js +1 -0
  114. package/dist/generated/models/session_models.js +1 -0
  115. package/dist/generated/models/session_start_sources.js +1 -0
  116. package/dist/generated/models/thread_sessions.js +1 -0
  117. package/dist/generated/models/thread_worktrees.js +1 -0
  118. package/dist/generated/models.js +1 -0
  119. package/dist/heap-monitor.js +122 -0
  120. package/dist/hrana-server.js +263 -0
  121. package/dist/hrana-server.test.js +370 -0
  122. package/dist/html-actions.js +123 -0
  123. package/dist/html-actions.test.js +70 -0
  124. package/dist/html-components.js +117 -0
  125. package/dist/html-components.test.js +34 -0
  126. package/dist/image-optimizer-plugin.js +153 -0
  127. package/dist/image-utils.js +112 -0
  128. package/dist/interaction-handler.js +397 -0
  129. package/dist/ipc-polling.js +252 -0
  130. package/dist/ipc-tools-plugin.js +193 -0
  131. package/dist/kimaki-digital-twin.e2e.test.js +161 -0
  132. package/dist/kimaki-opencode-plugin-loading.e2e.test.js +87 -0
  133. package/dist/kimaki-opencode-plugin.js +17 -0
  134. package/dist/kimaki-opencode-plugin.test.js +98 -0
  135. package/dist/limit-heading-depth.js +25 -0
  136. package/dist/limit-heading-depth.test.js +105 -0
  137. package/dist/logger.js +165 -0
  138. package/dist/markdown.js +342 -0
  139. package/dist/markdown.test.js +257 -0
  140. package/dist/message-finish-field.e2e.test.js +165 -0
  141. package/dist/message-formatting.js +413 -0
  142. package/dist/message-formatting.test.js +73 -0
  143. package/dist/message-preprocessing.js +330 -0
  144. package/dist/onboarding-tutorial.js +172 -0
  145. package/dist/onboarding-welcome.js +37 -0
  146. package/dist/openai-realtime.js +224 -0
  147. package/dist/opencode-command-detection.js +65 -0
  148. package/dist/opencode-command-detection.test.js +240 -0
  149. package/dist/opencode-command.js +129 -0
  150. package/dist/opencode-command.test.js +48 -0
  151. package/dist/opencode-interrupt-plugin.js +361 -0
  152. package/dist/opencode-interrupt-plugin.test.js +458 -0
  153. package/dist/opencode.js +861 -0
  154. package/dist/otto/branding.js +22 -0
  155. package/dist/otto/index.js +21 -0
  156. package/dist/parse-permission-rules.test.js +117 -0
  157. package/dist/patch-text-parser.js +97 -0
  158. package/dist/plugin-logger.js +59 -0
  159. package/dist/privacy-sanitizer.js +105 -0
  160. package/dist/queue-advanced-abort.e2e.test.js +293 -0
  161. package/dist/queue-advanced-action-buttons.e2e.test.js +206 -0
  162. package/dist/queue-advanced-e2e-setup.js +786 -0
  163. package/dist/queue-advanced-footer.e2e.test.js +472 -0
  164. package/dist/queue-advanced-model-switch.e2e.test.js +299 -0
  165. package/dist/queue-advanced-permissions-typing.e2e.test.js +180 -0
  166. package/dist/queue-advanced-question.e2e.test.js +261 -0
  167. package/dist/queue-advanced-typing-interrupt.e2e.test.js +114 -0
  168. package/dist/queue-advanced-typing.e2e.test.js +153 -0
  169. package/dist/queue-drain-after-interactive-ui.e2e.test.js +119 -0
  170. package/dist/queue-interrupt-drain.e2e.test.js +135 -0
  171. package/dist/queue-question-select-drain.e2e.test.js +120 -0
  172. package/dist/runtime-idle-sweeper.js +52 -0
  173. package/dist/runtime-lifecycle.e2e.test.js +508 -0
  174. package/dist/sentry.js +23 -0
  175. package/dist/session-handler/agent-utils.js +67 -0
  176. package/dist/session-handler/event-stream-state.js +420 -0
  177. package/dist/session-handler/event-stream-state.test.js +563 -0
  178. package/dist/session-handler/model-utils.js +124 -0
  179. package/dist/session-handler/opencode-session-event-log.js +94 -0
  180. package/dist/session-handler/thread-runtime-state.js +104 -0
  181. package/dist/session-handler/thread-session-runtime.js +3258 -0
  182. package/dist/session-handler.js +9 -0
  183. package/dist/session-search.js +100 -0
  184. package/dist/session-search.test.js +40 -0
  185. package/dist/session-title-rename.test.js +80 -0
  186. package/dist/startup-service.js +153 -0
  187. package/dist/startup-time.e2e.test.js +296 -0
  188. package/dist/store.js +17 -0
  189. package/dist/system-message.js +613 -0
  190. package/dist/system-message.test.js +602 -0
  191. package/dist/task-runner.js +295 -0
  192. package/dist/task-schedule.js +209 -0
  193. package/dist/task-schedule.test.js +71 -0
  194. package/dist/test-utils.js +299 -0
  195. package/dist/thinking-utils.js +35 -0
  196. package/dist/thread-message-queue.e2e.test.js +999 -0
  197. package/dist/tools.js +357 -0
  198. package/dist/undo-redo.e2e.test.js +161 -0
  199. package/dist/unnest-code-blocks.js +146 -0
  200. package/dist/unnest-code-blocks.test.js +673 -0
  201. package/dist/upgrade.js +114 -0
  202. package/dist/utils.js +144 -0
  203. package/dist/voice-attachment.js +34 -0
  204. package/dist/voice-handler.js +646 -0
  205. package/dist/voice-message.e2e.test.js +1021 -0
  206. package/dist/voice.js +447 -0
  207. package/dist/voice.test.js +235 -0
  208. package/dist/wait-session.js +94 -0
  209. package/dist/websockify.js +69 -0
  210. package/dist/worker-types.js +4 -0
  211. package/dist/worktree-lifecycle.e2e.test.js +308 -0
  212. package/dist/worktree-utils.js +3 -0
  213. package/dist/worktrees.js +929 -0
  214. package/dist/worktrees.test.js +189 -0
  215. package/dist/xml.js +92 -0
  216. package/dist/xml.test.js +32 -0
  217. package/package.json +98 -0
  218. package/schema.prisma +295 -0
  219. package/skills/batch/SKILL.md +87 -0
  220. package/skills/critique/SKILL.md +112 -0
  221. package/skills/egaki/SKILL.md +100 -0
  222. package/skills/errore/SKILL.md +647 -0
  223. package/skills/event-sourcing-state/SKILL.md +252 -0
  224. package/skills/gitchamber/SKILL.md +93 -0
  225. package/skills/goke/SKILL.md +644 -0
  226. package/skills/jitter/EDITOR.md +219 -0
  227. package/skills/jitter/EXPORT-INTERNALS.md +309 -0
  228. package/skills/jitter/SKILL.md +158 -0
  229. package/skills/jitter/jitter-clipboard.json +1042 -0
  230. package/skills/jitter/package.json +14 -0
  231. package/skills/jitter/tsconfig.json +15 -0
  232. package/skills/jitter/utils/actions.ts +212 -0
  233. package/skills/jitter/utils/export.ts +114 -0
  234. package/skills/jitter/utils/index.ts +141 -0
  235. package/skills/jitter/utils/snapshot.ts +154 -0
  236. package/skills/jitter/utils/traverse.ts +246 -0
  237. package/skills/jitter/utils/types.ts +279 -0
  238. package/skills/jitter/utils/wait.ts +133 -0
  239. package/skills/lintcn/SKILL.md +873 -0
  240. package/skills/new-skill/SKILL.md +211 -0
  241. package/skills/npm-package/SKILL.md +239 -0
  242. package/skills/playwriter/SKILL.md +35 -0
  243. package/skills/proxyman/SKILL.md +215 -0
  244. package/skills/security-review/SKILL.md +208 -0
  245. package/skills/simplify/SKILL.md +58 -0
  246. package/skills/spiceflow/SKILL.md +14 -0
  247. package/skills/termcast/SKILL.md +945 -0
  248. package/skills/tuistory/SKILL.md +250 -0
  249. package/skills/usecomputer/SKILL.md +264 -0
  250. package/skills/x-articles/SKILL.md +554 -0
  251. package/skills/zele/SKILL.md +112 -0
  252. package/skills/zustand-centralized-state/SKILL.md +1004 -0
  253. package/src/agent-model.e2e.test.ts +976 -0
  254. package/src/ai-tool-to-genai.test.ts +296 -0
  255. package/src/ai-tool-to-genai.ts +283 -0
  256. package/src/ai-tool.ts +39 -0
  257. package/src/anthropic-auth-plugin.test.ts +159 -0
  258. package/src/anthropic-auth-plugin.ts +861 -0
  259. package/src/anthropic-auth-state.ts +282 -0
  260. package/src/bin.ts +111 -0
  261. package/src/channel-management.ts +334 -0
  262. package/src/cli-parsing.test.ts +195 -0
  263. package/src/cli-send-thread.e2e.test.ts +464 -0
  264. package/src/cli.ts +4581 -0
  265. package/src/commands/abort.ts +89 -0
  266. package/src/commands/action-buttons.ts +364 -0
  267. package/src/commands/add-project.ts +149 -0
  268. package/src/commands/agent.ts +473 -0
  269. package/src/commands/ask-question.ts +390 -0
  270. package/src/commands/btw.ts +164 -0
  271. package/src/commands/compact.ts +157 -0
  272. package/src/commands/context-usage.ts +199 -0
  273. package/src/commands/create-new-project.ts +190 -0
  274. package/src/commands/diff.ts +91 -0
  275. package/src/commands/file-upload.ts +389 -0
  276. package/src/commands/fork.ts +321 -0
  277. package/src/commands/gemini-apikey.ts +104 -0
  278. package/src/commands/login.ts +1173 -0
  279. package/src/commands/mcp.ts +307 -0
  280. package/src/commands/memory-snapshot.ts +30 -0
  281. package/src/commands/mention-mode.ts +68 -0
  282. package/src/commands/merge-worktree.ts +223 -0
  283. package/src/commands/model-variant.ts +483 -0
  284. package/src/commands/model.ts +1053 -0
  285. package/src/commands/new-worktree.ts +510 -0
  286. package/src/commands/paginated-select.ts +81 -0
  287. package/src/commands/permissions.ts +397 -0
  288. package/src/commands/queue.ts +271 -0
  289. package/src/commands/remove-project.ts +155 -0
  290. package/src/commands/restart-opencode-server.ts +162 -0
  291. package/src/commands/resume.ts +230 -0
  292. package/src/commands/run-command.ts +123 -0
  293. package/src/commands/screenshare.test.ts +30 -0
  294. package/src/commands/screenshare.ts +366 -0
  295. package/src/commands/session-id.ts +109 -0
  296. package/src/commands/session.ts +227 -0
  297. package/src/commands/share.ts +106 -0
  298. package/src/commands/tasks.ts +293 -0
  299. package/src/commands/types.ts +25 -0
  300. package/src/commands/undo-redo.ts +386 -0
  301. package/src/commands/unset-model.ts +173 -0
  302. package/src/commands/upgrade.ts +52 -0
  303. package/src/commands/user-command.ts +198 -0
  304. package/src/commands/verbosity.ts +173 -0
  305. package/src/commands/worktree-settings.ts +70 -0
  306. package/src/commands/worktrees.ts +552 -0
  307. package/src/condense-memory.ts +36 -0
  308. package/src/config.ts +111 -0
  309. package/src/context-awareness-plugin.test.ts +142 -0
  310. package/src/context-awareness-plugin.ts +510 -0
  311. package/src/critique-utils.ts +139 -0
  312. package/src/database.ts +1876 -0
  313. package/src/db.test.ts +162 -0
  314. package/src/db.ts +286 -0
  315. package/src/debounce-timeout.ts +43 -0
  316. package/src/debounced-process-flush.ts +104 -0
  317. package/src/discord-bot.ts +1330 -0
  318. package/src/discord-command-registration.ts +693 -0
  319. package/src/discord-urls.ts +88 -0
  320. package/src/discord-utils.test.ts +153 -0
  321. package/src/discord-utils.ts +800 -0
  322. package/src/errors.ts +201 -0
  323. package/src/escape-backticks.test.ts +469 -0
  324. package/src/event-stream-real-capture.e2e.test.ts +692 -0
  325. package/src/eventsource-parser.test.ts +351 -0
  326. package/src/exec-async.ts +35 -0
  327. package/src/external-opencode-sync.ts +685 -0
  328. package/src/format-tables.test.ts +335 -0
  329. package/src/format-tables.ts +445 -0
  330. package/src/forum-sync/config.ts +92 -0
  331. package/src/forum-sync/discord-operations.ts +241 -0
  332. package/src/forum-sync/index.ts +9 -0
  333. package/src/forum-sync/markdown.ts +172 -0
  334. package/src/forum-sync/sync-to-discord.ts +595 -0
  335. package/src/forum-sync/sync-to-files.ts +294 -0
  336. package/src/forum-sync/types.ts +175 -0
  337. package/src/forum-sync/watchers.ts +454 -0
  338. package/src/gateway-proxy-reconnect.e2e.test.ts +523 -0
  339. package/src/gateway-proxy.e2e.test.ts +640 -0
  340. package/src/genai-worker-wrapper.ts +164 -0
  341. package/src/genai-worker.ts +386 -0
  342. package/src/genai.ts +321 -0
  343. package/src/generated/browser.ts +114 -0
  344. package/src/generated/client.ts +138 -0
  345. package/src/generated/commonInputTypes.ts +736 -0
  346. package/src/generated/enums.ts +88 -0
  347. package/src/generated/internal/class.ts +384 -0
  348. package/src/generated/internal/prismaNamespace.ts +2386 -0
  349. package/src/generated/internal/prismaNamespaceBrowser.ts +326 -0
  350. package/src/generated/models/bot_api_keys.ts +1288 -0
  351. package/src/generated/models/bot_tokens.ts +1656 -0
  352. package/src/generated/models/channel_agents.ts +1256 -0
  353. package/src/generated/models/channel_directories.ts +1859 -0
  354. package/src/generated/models/channel_mention_mode.ts +1300 -0
  355. package/src/generated/models/channel_models.ts +1288 -0
  356. package/src/generated/models/channel_verbosity.ts +1228 -0
  357. package/src/generated/models/channel_worktrees.ts +1300 -0
  358. package/src/generated/models/forum_sync_configs.ts +1452 -0
  359. package/src/generated/models/global_models.ts +1288 -0
  360. package/src/generated/models/ipc_requests.ts +1485 -0
  361. package/src/generated/models/part_messages.ts +1302 -0
  362. package/src/generated/models/scheduled_tasks.ts +2320 -0
  363. package/src/generated/models/session_agents.ts +1086 -0
  364. package/src/generated/models/session_events.ts +1439 -0
  365. package/src/generated/models/session_models.ts +1114 -0
  366. package/src/generated/models/session_start_sources.ts +1408 -0
  367. package/src/generated/models/thread_sessions.ts +1781 -0
  368. package/src/generated/models/thread_worktrees.ts +1356 -0
  369. package/src/generated/models.ts +30 -0
  370. package/src/heap-monitor.ts +152 -0
  371. package/src/hrana-server.test.ts +434 -0
  372. package/src/hrana-server.ts +314 -0
  373. package/src/html-actions.test.ts +87 -0
  374. package/src/html-actions.ts +174 -0
  375. package/src/html-components.test.ts +38 -0
  376. package/src/html-components.ts +181 -0
  377. package/src/image-optimizer-plugin.ts +194 -0
  378. package/src/image-utils.ts +149 -0
  379. package/src/interaction-handler.ts +576 -0
  380. package/src/ipc-polling.ts +326 -0
  381. package/src/ipc-tools-plugin.ts +236 -0
  382. package/src/kimaki-digital-twin.e2e.test.ts +199 -0
  383. package/src/kimaki-opencode-plugin-loading.e2e.test.ts +109 -0
  384. package/src/kimaki-opencode-plugin.test.ts +108 -0
  385. package/src/kimaki-opencode-plugin.ts +18 -0
  386. package/src/limit-heading-depth.test.ts +116 -0
  387. package/src/limit-heading-depth.ts +26 -0
  388. package/src/logger.ts +208 -0
  389. package/src/markdown.test.ts +308 -0
  390. package/src/markdown.ts +410 -0
  391. package/src/message-finish-field.e2e.test.ts +192 -0
  392. package/src/message-formatting.test.ts +81 -0
  393. package/src/message-formatting.ts +533 -0
  394. package/src/message-preprocessing.ts +455 -0
  395. package/src/onboarding-tutorial.ts +176 -0
  396. package/src/onboarding-welcome.ts +49 -0
  397. package/src/openai-realtime.ts +358 -0
  398. package/src/opencode-command-detection.test.ts +307 -0
  399. package/src/opencode-command-detection.ts +76 -0
  400. package/src/opencode-command.test.ts +70 -0
  401. package/src/opencode-command.ts +188 -0
  402. package/src/opencode-interrupt-plugin.test.ts +677 -0
  403. package/src/opencode-interrupt-plugin.ts +477 -0
  404. package/src/opencode.ts +1110 -0
  405. package/src/otto/branding.ts +23 -0
  406. package/src/otto/index.ts +22 -0
  407. package/src/parse-permission-rules.test.ts +127 -0
  408. package/src/patch-text-parser.ts +107 -0
  409. package/src/plugin-logger.ts +68 -0
  410. package/src/privacy-sanitizer.ts +142 -0
  411. package/src/queue-advanced-abort.e2e.test.ts +382 -0
  412. package/src/queue-advanced-action-buttons.e2e.test.ts +268 -0
  413. package/src/queue-advanced-e2e-setup.ts +873 -0
  414. package/src/queue-advanced-footer.e2e.test.ts +576 -0
  415. package/src/queue-advanced-model-switch.e2e.test.ts +383 -0
  416. package/src/queue-advanced-permissions-typing.e2e.test.ts +245 -0
  417. package/src/queue-advanced-question.e2e.test.ts +316 -0
  418. package/src/queue-advanced-typing-interrupt.e2e.test.ts +146 -0
  419. package/src/queue-advanced-typing.e2e.test.ts +199 -0
  420. package/src/queue-drain-after-interactive-ui.e2e.test.ts +151 -0
  421. package/src/queue-interrupt-drain.e2e.test.ts +166 -0
  422. package/src/queue-question-select-drain.e2e.test.ts +152 -0
  423. package/src/runtime-idle-sweeper.ts +76 -0
  424. package/src/runtime-lifecycle.e2e.test.ts +641 -0
  425. package/src/schema.sql +173 -0
  426. package/src/sentry.ts +26 -0
  427. package/src/session-handler/agent-utils.ts +97 -0
  428. package/src/session-handler/event-stream-fixtures/real-session-action-buttons.jsonl +45 -0
  429. package/src/session-handler/event-stream-fixtures/real-session-footer-suppressed-on-pre-idle-interrupt.jsonl +40 -0
  430. package/src/session-handler/event-stream-fixtures/real-session-permission-external-file.jsonl +23 -0
  431. package/src/session-handler/event-stream-fixtures/real-session-task-normal.jsonl +22 -0
  432. package/src/session-handler/event-stream-fixtures/real-session-task-three-parallel-sleeps.jsonl +277 -0
  433. package/src/session-handler/event-stream-fixtures/real-session-task-user-interruption.jsonl +46 -0
  434. package/src/session-handler/event-stream-fixtures/session-abort-after-idle-race.jsonl +21 -0
  435. package/src/session-handler/event-stream-fixtures/session-concurrent-messages-serialized.jsonl +56 -0
  436. package/src/session-handler/event-stream-fixtures/session-explicit-abort.jsonl +44 -0
  437. package/src/session-handler/event-stream-fixtures/session-normal-completion.jsonl +29 -0
  438. package/src/session-handler/event-stream-fixtures/session-tool-call-noisy-stream.jsonl +29 -0
  439. package/src/session-handler/event-stream-fixtures/session-two-completions-same-session.jsonl +50 -0
  440. package/src/session-handler/event-stream-fixtures/session-user-interruption.jsonl +59 -0
  441. package/src/session-handler/event-stream-fixtures/session-voice-queued-followup.jsonl +52 -0
  442. package/src/session-handler/event-stream-state.test.ts +645 -0
  443. package/src/session-handler/event-stream-state.ts +608 -0
  444. package/src/session-handler/model-utils.ts +183 -0
  445. package/src/session-handler/opencode-session-event-log.ts +130 -0
  446. package/src/session-handler/thread-runtime-state.ts +212 -0
  447. package/src/session-handler/thread-session-runtime.ts +4281 -0
  448. package/src/session-handler.ts +15 -0
  449. package/src/session-search.test.ts +50 -0
  450. package/src/session-search.ts +148 -0
  451. package/src/session-title-rename.test.ts +112 -0
  452. package/src/startup-service.ts +200 -0
  453. package/src/startup-time.e2e.test.ts +373 -0
  454. package/src/store.ts +122 -0
  455. package/src/system-message.test.ts +612 -0
  456. package/src/system-message.ts +723 -0
  457. package/src/task-runner.ts +421 -0
  458. package/src/task-schedule.test.ts +84 -0
  459. package/src/task-schedule.ts +311 -0
  460. package/src/test-utils.ts +435 -0
  461. package/src/thinking-utils.ts +61 -0
  462. package/src/thread-message-queue.e2e.test.ts +1219 -0
  463. package/src/tools.ts +430 -0
  464. package/src/undici.d.ts +12 -0
  465. package/src/undo-redo.e2e.test.ts +209 -0
  466. package/src/unnest-code-blocks.test.ts +713 -0
  467. package/src/unnest-code-blocks.ts +185 -0
  468. package/src/upgrade.ts +127 -0
  469. package/src/utils.ts +212 -0
  470. package/src/voice-attachment.ts +51 -0
  471. package/src/voice-handler.ts +908 -0
  472. package/src/voice-message.e2e.test.ts +1255 -0
  473. package/src/voice.test.ts +281 -0
  474. package/src/voice.ts +627 -0
  475. package/src/wait-session.ts +147 -0
  476. package/src/websockify.ts +101 -0
  477. package/src/worker-types.ts +64 -0
  478. package/src/worktree-lifecycle.e2e.test.ts +391 -0
  479. package/src/worktree-utils.ts +4 -0
  480. package/src/worktrees.test.ts +223 -0
  481. package/src/worktrees.ts +1294 -0
  482. package/src/xml.test.ts +38 -0
  483. package/src/xml.ts +121 -0
@@ -0,0 +1,641 @@
1
+ // E2e tests for ThreadSessionRuntime lifecycle behaviors.
2
+ // Tests scenarios not covered by the queue/interrupt tests:
3
+ // 1. Sequential completions: listener stays alive across multiple full run cycles
4
+ // 2. Concurrent first messages: runtime serialization without threadMessageQueue
5
+ //
6
+ // Uses opencode-deterministic-provider (no real LLM calls).
7
+ // Poll timeouts: 4s max, 100ms interval.
8
+
9
+ import fs from 'node:fs'
10
+
11
+ import path from 'node:path'
12
+ import url from 'node:url'
13
+ import { describe, beforeAll, afterAll, test, expect } from 'vitest'
14
+ import { ChannelType, Client, GatewayIntentBits, Partials } from 'discord.js'
15
+ import { DigitalDiscord } from 'discord-digital-twin/src'
16
+ import {
17
+ buildDeterministicOpencodeConfig,
18
+ type DeterministicMatcher,
19
+ } from 'opencode-deterministic-provider'
20
+ import { setDataDir } from './config.js'
21
+ import { store } from './store.js'
22
+ import { startDiscordBot } from './discord-bot.js'
23
+ import { getRuntime } from './session-handler/thread-session-runtime.js'
24
+ import {
25
+ setBotToken,
26
+ initDatabase,
27
+ closeDatabase,
28
+ setChannelDirectory,
29
+ setChannelVerbosity,
30
+ type VerbosityLevel,
31
+ } from './database.js'
32
+ import { startHranaServer, stopHranaServer } from './hrana-server.js'
33
+ import {
34
+ initializeOpencodeForDirectory,
35
+ restartOpencodeServer,
36
+ stopOpencodeServer,
37
+ } from './opencode.js'
38
+ import {
39
+ chooseLockPort,
40
+ cleanupTestSessions,
41
+ initTestGitRepo,
42
+ waitForBotMessageContaining,
43
+ waitForBotReplyAfterUserMessage,
44
+ } from './test-utils.js'
45
+
46
+
47
+ const TEST_USER_ID = '200000000000000888'
48
+ const TEXT_CHANNEL_ID = '200000000000000889'
49
+
50
+ function createRunDirectories() {
51
+ const root = path.resolve(process.cwd(), 'tmp', 'runtime-lifecycle-e2e')
52
+ fs.mkdirSync(root, { recursive: true })
53
+ const dataDir = fs.mkdtempSync(path.join(root, 'data-'))
54
+ const projectDirectory = path.join(root, 'project')
55
+ fs.mkdirSync(projectDirectory, { recursive: true })
56
+ initTestGitRepo(projectDirectory)
57
+ return { root, dataDir, projectDirectory }
58
+ }
59
+
60
+
61
+
62
+ function createDiscordJsClient({ restUrl }: { restUrl: string }) {
63
+ return new Client({
64
+ intents: [
65
+ GatewayIntentBits.Guilds,
66
+ GatewayIntentBits.GuildMessages,
67
+ GatewayIntentBits.MessageContent,
68
+ GatewayIntentBits.GuildVoiceStates,
69
+ ],
70
+ partials: [
71
+ Partials.Channel,
72
+ Partials.Message,
73
+ Partials.User,
74
+ Partials.ThreadMember,
75
+ ],
76
+ rest: {
77
+ api: restUrl,
78
+ version: '10',
79
+ },
80
+ })
81
+ }
82
+
83
+ function createDeterministicMatchers(): DeterministicMatcher[] {
84
+ const highUsageReplyMatcher: DeterministicMatcher = {
85
+ id: 'high-usage-reply',
86
+ priority: 20,
87
+ when: {
88
+ lastMessageRole: 'user',
89
+ rawPromptIncludes: 'Reply with exactly: footer-high-usage',
90
+ },
91
+ then: {
92
+ parts: [
93
+ { type: 'stream-start', warnings: [] },
94
+ { type: 'text-start', id: 'high-usage-reply' },
95
+ { type: 'text-delta', id: 'high-usage-reply', delta: 'ok' },
96
+ { type: 'text-end', id: 'high-usage-reply' },
97
+ {
98
+ type: 'finish',
99
+ finishReason: 'stop',
100
+ usage: { inputTokens: 15_000, outputTokens: 1, totalTokens: 15_001 },
101
+ },
102
+ ],
103
+ partDelaysMs: [0, 100, 0, 0, 0],
104
+ },
105
+ }
106
+
107
+ // Simple reply matcher: model echoes back the requested text.
108
+ // Uses 100ms delay on first text delta to keep streams async without adding
109
+ // unnecessary latency. Tests verify ordering/serialization, not latency handling.
110
+ const userReplyMatcher: DeterministicMatcher = {
111
+ id: 'user-reply',
112
+ priority: 10,
113
+ when: {
114
+ lastMessageRole: 'user',
115
+ rawPromptIncludes: 'Reply with exactly:',
116
+ },
117
+ then: {
118
+ parts: [
119
+ { type: 'stream-start', warnings: [] },
120
+ { type: 'text-start', id: 'default-reply' },
121
+ { type: 'text-delta', id: 'default-reply', delta: 'ok' },
122
+ { type: 'text-end', id: 'default-reply' },
123
+ {
124
+ type: 'finish',
125
+ finishReason: 'stop',
126
+ usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
127
+ },
128
+ ],
129
+ partDelaysMs: [0, 100, 0, 0, 0],
130
+ },
131
+ }
132
+
133
+ return [highUsageReplyMatcher, userReplyMatcher]
134
+ }
135
+
136
+ describe('runtime lifecycle', () => {
137
+ let directories: ReturnType<typeof createRunDirectories>
138
+ let discord: DigitalDiscord
139
+ let botClient: Client
140
+ let previousDefaultVerbosity: VerbosityLevel | null = null
141
+ let testStartTime = Date.now()
142
+
143
+ beforeAll(async () => {
144
+ testStartTime = Date.now()
145
+ directories = createRunDirectories()
146
+ const lockPort = chooseLockPort({ key: TEXT_CHANNEL_ID })
147
+
148
+ process.env['KIMAKI_LOCK_PORT'] = String(lockPort)
149
+ setDataDir(directories.dataDir)
150
+ previousDefaultVerbosity = store.getState().defaultVerbosity
151
+ store.setState({ defaultVerbosity: 'tools_and_text' })
152
+
153
+ const digitalDiscordDbPath = path.join(
154
+ directories.dataDir,
155
+ 'digital-discord.db',
156
+ )
157
+
158
+ discord = new DigitalDiscord({
159
+ guild: {
160
+ name: 'Lifecycle E2E Guild',
161
+ ownerId: TEST_USER_ID,
162
+ },
163
+ channels: [
164
+ {
165
+ id: TEXT_CHANNEL_ID,
166
+ name: 'lifecycle-e2e',
167
+ type: ChannelType.GuildText,
168
+ },
169
+ ],
170
+ users: [
171
+ {
172
+ id: TEST_USER_ID,
173
+ username: 'lifecycle-tester',
174
+ },
175
+ ],
176
+ dbUrl: `file:${digitalDiscordDbPath}`,
177
+ })
178
+
179
+ await discord.start()
180
+
181
+ const providerNpm = url
182
+ .pathToFileURL(
183
+ path.resolve(
184
+ process.cwd(),
185
+ '..',
186
+ 'opencode-deterministic-provider',
187
+ 'src',
188
+ 'index.ts',
189
+ ),
190
+ )
191
+ .toString()
192
+
193
+ const opencodeConfig = buildDeterministicOpencodeConfig({
194
+ providerName: 'deterministic-provider',
195
+ providerNpm,
196
+ model: 'deterministic-v2',
197
+ smallModel: 'deterministic-v2',
198
+ settings: {
199
+ strict: false,
200
+ matchers: createDeterministicMatchers(),
201
+ },
202
+ })
203
+ fs.writeFileSync(
204
+ path.join(directories.projectDirectory, 'opencode.json'),
205
+ JSON.stringify(opencodeConfig, null, 2),
206
+ )
207
+
208
+ const dbPath = path.join(directories.dataDir, 'discord-sessions.db')
209
+ const hranaResult = await startHranaServer({ dbPath })
210
+ if (hranaResult instanceof Error) {
211
+ throw hranaResult
212
+ }
213
+ process.env['KIMAKI_DB_URL'] = hranaResult
214
+ await initDatabase()
215
+ await setBotToken(discord.botUserId, discord.botToken)
216
+
217
+ await setChannelDirectory({
218
+ channelId: TEXT_CHANNEL_ID,
219
+ directory: directories.projectDirectory,
220
+ channelType: 'text',
221
+ })
222
+ await setChannelVerbosity(TEXT_CHANNEL_ID, 'tools_and_text')
223
+
224
+ botClient = createDiscordJsClient({ restUrl: discord.restUrl })
225
+ await startDiscordBot({
226
+ token: discord.botToken,
227
+ appId: discord.botUserId,
228
+ discordClient: botClient,
229
+ })
230
+
231
+ // Pre-warm the opencode server
232
+ const warmup = await initializeOpencodeForDirectory(
233
+ directories.projectDirectory,
234
+ )
235
+ if (warmup instanceof Error) {
236
+ throw warmup
237
+ }
238
+ }, 60_000)
239
+
240
+ afterAll(async () => {
241
+ if (directories) {
242
+ await cleanupTestSessions({
243
+ projectDirectory: directories.projectDirectory,
244
+ testStartTime,
245
+ })
246
+ }
247
+ if (botClient) {
248
+ botClient.destroy()
249
+ }
250
+ await stopOpencodeServer()
251
+ await Promise.all([
252
+ closeDatabase().catch(() => { return }),
253
+ stopHranaServer().catch(() => { return }),
254
+ discord?.stop().catch(() => { return }),
255
+ ])
256
+ delete process.env['KIMAKI_LOCK_PORT']
257
+ delete process.env['KIMAKI_DB_URL']
258
+ if (previousDefaultVerbosity) {
259
+ store.setState({ defaultVerbosity: previousDefaultVerbosity })
260
+ }
261
+ if (directories) {
262
+ fs.rmSync(directories.dataDir, { recursive: true, force: true })
263
+ }
264
+ }, 10_000)
265
+
266
+ test(
267
+ 'three sequential completions reuse same runtime and listener',
268
+ async () => {
269
+ // Sends A, waits for full completion (footer), sends B, waits for
270
+ // footer, sends C, waits for footer. Proves the listener stays alive
271
+ // across full run cycles without any interrupt/queue involvement.
272
+ // This is the "calm" path — no abort, no queue, just sequential use.
273
+
274
+ // 1. Send first message → thread created, session established
275
+ await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
276
+ content: 'Reply with exactly: seq-alpha',
277
+ })
278
+
279
+ const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
280
+ timeout: 4_000,
281
+ predicate: (t) => {
282
+ return t.name === 'Reply with exactly: seq-alpha'
283
+ },
284
+ })
285
+
286
+ const th = discord.thread(thread.id)
287
+
288
+ // Wait for footer (italic project info line) — proves run A completed
289
+ await waitForBotMessageContaining({
290
+ discord,
291
+ threadId: thread.id,
292
+ userId: TEST_USER_ID,
293
+ text: '*project',
294
+ timeout: 4_000,
295
+ })
296
+
297
+ // Capture runtime identity — should not change across runs
298
+ const runtimeAfterA = getRuntime(thread.id)
299
+ expect(runtimeAfterA).toBeDefined()
300
+
301
+ // 2. Send B after A fully completed
302
+ await th.user(TEST_USER_ID).sendMessage({
303
+ content: 'Reply with exactly: seq-beta',
304
+ })
305
+
306
+ await waitForBotReplyAfterUserMessage({
307
+ discord,
308
+ threadId: thread.id,
309
+ userId: TEST_USER_ID,
310
+ userMessageIncludes: 'seq-beta',
311
+ timeout: 4_000,
312
+ })
313
+
314
+ // Wait for B's footer
315
+ await waitForBotMessageContaining({
316
+ discord,
317
+ threadId: thread.id,
318
+ userId: TEST_USER_ID,
319
+ text: '*project',
320
+ afterUserMessageIncludes: 'seq-beta',
321
+ timeout: 4_000,
322
+ })
323
+
324
+ // Same runtime instance — listener was not recreated
325
+ const runtimeAfterB = getRuntime(thread.id)
326
+ expect(runtimeAfterB).toBe(runtimeAfterA)
327
+
328
+ // 3. Send C after B fully completed
329
+ await th.user(TEST_USER_ID).sendMessage({
330
+ content: 'Reply with exactly: seq-gamma',
331
+ })
332
+
333
+ await waitForBotReplyAfterUserMessage({
334
+ discord,
335
+ threadId: thread.id,
336
+ userId: TEST_USER_ID,
337
+ userMessageIncludes: 'seq-gamma',
338
+ timeout: 4_000,
339
+ })
340
+
341
+ await waitForBotMessageContaining({
342
+ discord,
343
+ threadId: thread.id,
344
+ userId: TEST_USER_ID,
345
+ text: '*project',
346
+ afterUserMessageIncludes: 'seq-gamma',
347
+ timeout: 4_000,
348
+ })
349
+
350
+ // Still the same runtime — three full cycles, one runtime, one listener
351
+ const runtimeAfterC = getRuntime(thread.id)
352
+ expect(await th.text()).toMatchInlineSnapshot(`
353
+ "--- from: user (lifecycle-tester)
354
+ Reply with exactly: seq-alpha
355
+ --- from: assistant (TestBot)
356
+ ⬥ ok
357
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
358
+ --- from: user (lifecycle-tester)
359
+ Reply with exactly: seq-beta
360
+ --- from: assistant (TestBot)
361
+ ⬥ ok
362
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
363
+ --- from: user (lifecycle-tester)
364
+ Reply with exactly: seq-gamma
365
+ --- from: assistant (TestBot)
366
+ ⬥ ok
367
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
368
+ `)
369
+ expect(runtimeAfterC).toBe(runtimeAfterA)
370
+ },
371
+ 15_000,
372
+ )
373
+
374
+ test(
375
+ 'footer includes context percentage and model id',
376
+ async () => {
377
+ const prompt = 'Reply with exactly: footer-check'
378
+ await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
379
+ content: prompt,
380
+ })
381
+
382
+ const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
383
+ timeout: 4_000,
384
+ predicate: (t) => {
385
+ return t.name === prompt
386
+ },
387
+ })
388
+
389
+ await waitForBotMessageContaining({
390
+ discord,
391
+ threadId: thread.id,
392
+ userId: TEST_USER_ID,
393
+ text: 'deterministic-v2',
394
+ timeout: 4_000,
395
+ })
396
+
397
+ const messages = await discord.thread(thread.id).getMessages()
398
+
399
+ const footerMessage = messages.find((message) => {
400
+ if (message.author.id !== discord.botUserId) {
401
+ return false
402
+ }
403
+ if (!message.content.startsWith('*')) {
404
+ return false
405
+ }
406
+ return message.content.includes('deterministic-v2')
407
+ })
408
+
409
+ expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
410
+ "--- from: user (lifecycle-tester)
411
+ Reply with exactly: footer-check
412
+ --- from: assistant (TestBot)
413
+ ⬥ ok
414
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
415
+ `)
416
+ expect(footerMessage).toBeDefined()
417
+ if (!footerMessage) {
418
+ throw new Error('Expected footer message to be present')
419
+ }
420
+ expect(footerMessage.content).toContain('deterministic-v2')
421
+ expect(footerMessage.content).toMatch(/\d+%/)
422
+ },
423
+ 10_000,
424
+ )
425
+
426
+ test(
427
+ 'existing runtime reconnects after shared opencode server restart',
428
+ async () => {
429
+ const prompt = 'Reply with exactly: reconnect-alpha'
430
+ await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
431
+ content: prompt,
432
+ })
433
+
434
+ const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
435
+ timeout: 4_000,
436
+ predicate: (t) => {
437
+ return t.name === prompt
438
+ },
439
+ })
440
+
441
+ const th = discord.thread(thread.id)
442
+
443
+ await waitForBotMessageContaining({
444
+ discord,
445
+ threadId: thread.id,
446
+ userId: TEST_USER_ID,
447
+ text: '*project',
448
+ timeout: 4_000,
449
+ })
450
+
451
+ const runtimeBeforeRestart = getRuntime(thread.id)
452
+ expect(runtimeBeforeRestart).toBeDefined()
453
+
454
+ const restartResult = await restartOpencodeServer()
455
+ if (restartResult instanceof Error) {
456
+ throw restartResult
457
+ }
458
+
459
+ await th.user(TEST_USER_ID).sendMessage({
460
+ content: 'Reply with exactly: reconnect-beta',
461
+ })
462
+
463
+ await waitForBotReplyAfterUserMessage({
464
+ discord,
465
+ threadId: thread.id,
466
+ userId: TEST_USER_ID,
467
+ userMessageIncludes: 'reconnect-beta',
468
+ timeout: 4_000,
469
+ })
470
+
471
+ await waitForBotMessageContaining({
472
+ discord,
473
+ threadId: thread.id,
474
+ userId: TEST_USER_ID,
475
+ text: '*project',
476
+ afterUserMessageIncludes: 'reconnect-beta',
477
+ timeout: 4_000,
478
+ })
479
+
480
+ expect(await th.text()).toMatchInlineSnapshot(`
481
+ "--- from: user (lifecycle-tester)
482
+ Reply with exactly: reconnect-alpha
483
+ --- from: assistant (TestBot)
484
+ ⬥ ok
485
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
486
+ --- from: user (lifecycle-tester)
487
+ Reply with exactly: reconnect-beta
488
+ --- from: assistant (TestBot)
489
+ ⬥ ok
490
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
491
+ `)
492
+
493
+ const runtimeAfterRestart = getRuntime(thread.id)
494
+ expect(runtimeAfterRestart).toBe(runtimeBeforeRestart)
495
+ },
496
+ 15_000,
497
+ )
498
+
499
+ test(
500
+ 'does not print a context-usage notice for the final text part right before the footer',
501
+ async () => {
502
+ const prompt = 'Reply with exactly: footer-high-usage'
503
+ await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
504
+ content: prompt,
505
+ })
506
+
507
+ const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
508
+ timeout: 4_000,
509
+ predicate: (t) => {
510
+ return t.name === prompt
511
+ },
512
+ })
513
+
514
+ await waitForBotMessageContaining({
515
+ discord,
516
+ threadId: thread.id,
517
+ userId: TEST_USER_ID,
518
+ text: 'deterministic-v2',
519
+ timeout: 4_000,
520
+ })
521
+
522
+ expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
523
+ "--- from: user (lifecycle-tester)
524
+ Reply with exactly: footer-high-usage
525
+ --- from: assistant (TestBot)
526
+ ⬥ ok
527
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
528
+ `)
529
+
530
+ const threadText = await discord.thread(thread.id).text()
531
+ expect(threadText).not.toContain('⬦ context usage')
532
+ },
533
+ 10_000,
534
+ )
535
+
536
+ test(
537
+ 'two near-simultaneous messages to same thread serialize correctly',
538
+ async () => {
539
+ // Sends A to create a thread, then fires B and C simultaneously into
540
+ // the thread (no await between them). Without the old threadMessageQueue,
541
+ // the runtime's dispatchAction must serialize these. Both should get
542
+ // responses and the thread should not deadlock or create duplicate sessions.
543
+
544
+ // 1. Establish thread + session
545
+ await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
546
+ content: 'Reply with exactly: concurrent-setup',
547
+ })
548
+
549
+ const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
550
+ timeout: 4_000,
551
+ predicate: (t) => {
552
+ return t.name === 'Reply with exactly: concurrent-setup'
553
+ },
554
+ })
555
+
556
+ const th = discord.thread(thread.id)
557
+ const setupReply = await th.waitForBotReply({ timeout: 4_000 })
558
+ expect(setupReply.content.trim().length).toBeGreaterThan(0)
559
+
560
+ // Wait for setup footer so the run is fully idle
561
+ await waitForBotMessageContaining({
562
+ discord,
563
+ threadId: thread.id,
564
+ userId: TEST_USER_ID,
565
+ text: '*project',
566
+ timeout: 4_000,
567
+ })
568
+
569
+ // Snapshot bot message count before sending concurrent messages
570
+ const beforeMessages = await th.getMessages()
571
+ const beforeBotCount = beforeMessages.filter((m) => {
572
+ return m.author.id === discord.botUserId
573
+ }).length
574
+
575
+ // 2. Fire B and C simultaneously — no await between sends
576
+ const sendB = th.user(TEST_USER_ID).sendMessage({
577
+ content: 'Reply with exactly: concurrent-bravo',
578
+ })
579
+ const sendC = th.user(TEST_USER_ID).sendMessage({
580
+ content: 'Reply with exactly: concurrent-charlie',
581
+ })
582
+ await Promise.all([sendB, sendC])
583
+
584
+ // 3. Both should eventually get bot replies — the runtime serializes them
585
+ await waitForBotReplyAfterUserMessage({
586
+ discord,
587
+ threadId: thread.id,
588
+ userId: TEST_USER_ID,
589
+ userMessageIncludes: 'concurrent-bravo',
590
+ timeout: 4_000,
591
+ })
592
+
593
+ await waitForBotReplyAfterUserMessage({
594
+ discord,
595
+ threadId: thread.id,
596
+ userId: TEST_USER_ID,
597
+ userMessageIncludes: 'concurrent-charlie',
598
+ timeout: 4_000,
599
+ })
600
+
601
+ // 4. Verify both user messages arrived and the thread didn't deadlock.
602
+ // With explicit abort flows, bravo can be aborted by charlie before
603
+ // producing a reply, so we can't assert +2 bot messages. What we
604
+ // CAN verify: both user messages exist, charlie (the last one) has
605
+ // a bot reply after it, and the replies are distinct messages.
606
+ // No inline snapshot here — the concurrent abort race makes message
607
+ // ordering nondeterministic (bravo may or may not get a reply).
608
+ const messages = await th.getMessages()
609
+
610
+ const bravoIndex = messages.findIndex((m) => {
611
+ return (
612
+ m.author.id === TEST_USER_ID &&
613
+ m.content.includes('concurrent-bravo')
614
+ )
615
+ })
616
+ const charlieIndex = messages.findIndex((m) => {
617
+ return (
618
+ m.author.id === TEST_USER_ID &&
619
+ m.content.includes('concurrent-charlie')
620
+ )
621
+ })
622
+ expect(bravoIndex).toBeGreaterThan(-1)
623
+ expect(charlieIndex).toBeGreaterThan(-1)
624
+ expect(bravoIndex).toBeLessThan(charlieIndex)
625
+
626
+ // Charlie (the last queued message) must have a bot reply after it.
627
+ const charlieReplyIndex = messages.findIndex((m, i) => {
628
+ return i > charlieIndex && m.author.id === discord.botUserId
629
+ })
630
+ expect(charlieReplyIndex).toBeGreaterThan(-1)
631
+
632
+ // At least 1 new bot message appeared (charlie's reply). If bravo
633
+ // wasn't aborted, there will be 2. Either way, no deadlock.
634
+ const afterBotCount = messages.filter((m) => {
635
+ return m.author.id === discord.botUserId
636
+ }).length
637
+ expect(afterBotCount).toBeGreaterThanOrEqual(beforeBotCount + 1)
638
+ },
639
+ 15_000,
640
+ )
641
+ })