@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,508 @@
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
+ import fs from 'node:fs';
9
+ import path from 'node:path';
10
+ import url from 'node:url';
11
+ import { describe, beforeAll, afterAll, test, expect } from 'vitest';
12
+ import { ChannelType, Client, GatewayIntentBits, Partials } from 'discord.js';
13
+ import { DigitalDiscord } from 'discord-digital-twin/src';
14
+ import { buildDeterministicOpencodeConfig, } from 'opencode-deterministic-provider';
15
+ import { setDataDir } from './config.js';
16
+ import { store } from './store.js';
17
+ import { startDiscordBot } from './discord-bot.js';
18
+ import { getRuntime } from './session-handler/thread-session-runtime.js';
19
+ import { setBotToken, initDatabase, closeDatabase, setChannelDirectory, setChannelVerbosity, } from './database.js';
20
+ import { startHranaServer, stopHranaServer } from './hrana-server.js';
21
+ import { initializeOpencodeForDirectory, restartOpencodeServer, stopOpencodeServer, } from './opencode.js';
22
+ import { chooseLockPort, cleanupTestSessions, initTestGitRepo, waitForBotMessageContaining, waitForBotReplyAfterUserMessage, } from './test-utils.js';
23
+ const TEST_USER_ID = '200000000000000888';
24
+ const TEXT_CHANNEL_ID = '200000000000000889';
25
+ function createRunDirectories() {
26
+ const root = path.resolve(process.cwd(), 'tmp', 'runtime-lifecycle-e2e');
27
+ fs.mkdirSync(root, { recursive: true });
28
+ const dataDir = fs.mkdtempSync(path.join(root, 'data-'));
29
+ const projectDirectory = path.join(root, 'project');
30
+ fs.mkdirSync(projectDirectory, { recursive: true });
31
+ initTestGitRepo(projectDirectory);
32
+ return { root, dataDir, projectDirectory };
33
+ }
34
+ function createDiscordJsClient({ restUrl }) {
35
+ return new Client({
36
+ intents: [
37
+ GatewayIntentBits.Guilds,
38
+ GatewayIntentBits.GuildMessages,
39
+ GatewayIntentBits.MessageContent,
40
+ GatewayIntentBits.GuildVoiceStates,
41
+ ],
42
+ partials: [
43
+ Partials.Channel,
44
+ Partials.Message,
45
+ Partials.User,
46
+ Partials.ThreadMember,
47
+ ],
48
+ rest: {
49
+ api: restUrl,
50
+ version: '10',
51
+ },
52
+ });
53
+ }
54
+ function createDeterministicMatchers() {
55
+ const highUsageReplyMatcher = {
56
+ id: 'high-usage-reply',
57
+ priority: 20,
58
+ when: {
59
+ lastMessageRole: 'user',
60
+ rawPromptIncludes: 'Reply with exactly: footer-high-usage',
61
+ },
62
+ then: {
63
+ parts: [
64
+ { type: 'stream-start', warnings: [] },
65
+ { type: 'text-start', id: 'high-usage-reply' },
66
+ { type: 'text-delta', id: 'high-usage-reply', delta: 'ok' },
67
+ { type: 'text-end', id: 'high-usage-reply' },
68
+ {
69
+ type: 'finish',
70
+ finishReason: 'stop',
71
+ usage: { inputTokens: 15_000, outputTokens: 1, totalTokens: 15_001 },
72
+ },
73
+ ],
74
+ partDelaysMs: [0, 100, 0, 0, 0],
75
+ },
76
+ };
77
+ // Simple reply matcher: model echoes back the requested text.
78
+ // Uses 100ms delay on first text delta to keep streams async without adding
79
+ // unnecessary latency. Tests verify ordering/serialization, not latency handling.
80
+ const userReplyMatcher = {
81
+ id: 'user-reply',
82
+ priority: 10,
83
+ when: {
84
+ lastMessageRole: 'user',
85
+ rawPromptIncludes: 'Reply with exactly:',
86
+ },
87
+ then: {
88
+ parts: [
89
+ { type: 'stream-start', warnings: [] },
90
+ { type: 'text-start', id: 'default-reply' },
91
+ { type: 'text-delta', id: 'default-reply', delta: 'ok' },
92
+ { type: 'text-end', id: 'default-reply' },
93
+ {
94
+ type: 'finish',
95
+ finishReason: 'stop',
96
+ usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
97
+ },
98
+ ],
99
+ partDelaysMs: [0, 100, 0, 0, 0],
100
+ },
101
+ };
102
+ return [highUsageReplyMatcher, userReplyMatcher];
103
+ }
104
+ describe('runtime lifecycle', () => {
105
+ let directories;
106
+ let discord;
107
+ let botClient;
108
+ let previousDefaultVerbosity = null;
109
+ let testStartTime = Date.now();
110
+ beforeAll(async () => {
111
+ testStartTime = Date.now();
112
+ directories = createRunDirectories();
113
+ const lockPort = chooseLockPort({ key: TEXT_CHANNEL_ID });
114
+ process.env['KIMAKI_LOCK_PORT'] = String(lockPort);
115
+ setDataDir(directories.dataDir);
116
+ previousDefaultVerbosity = store.getState().defaultVerbosity;
117
+ store.setState({ defaultVerbosity: 'tools_and_text' });
118
+ const digitalDiscordDbPath = path.join(directories.dataDir, 'digital-discord.db');
119
+ discord = new DigitalDiscord({
120
+ guild: {
121
+ name: 'Lifecycle E2E Guild',
122
+ ownerId: TEST_USER_ID,
123
+ },
124
+ channels: [
125
+ {
126
+ id: TEXT_CHANNEL_ID,
127
+ name: 'lifecycle-e2e',
128
+ type: ChannelType.GuildText,
129
+ },
130
+ ],
131
+ users: [
132
+ {
133
+ id: TEST_USER_ID,
134
+ username: 'lifecycle-tester',
135
+ },
136
+ ],
137
+ dbUrl: `file:${digitalDiscordDbPath}`,
138
+ });
139
+ await discord.start();
140
+ const providerNpm = url
141
+ .pathToFileURL(path.resolve(process.cwd(), '..', 'opencode-deterministic-provider', 'src', 'index.ts'))
142
+ .toString();
143
+ const opencodeConfig = buildDeterministicOpencodeConfig({
144
+ providerName: 'deterministic-provider',
145
+ providerNpm,
146
+ model: 'deterministic-v2',
147
+ smallModel: 'deterministic-v2',
148
+ settings: {
149
+ strict: false,
150
+ matchers: createDeterministicMatchers(),
151
+ },
152
+ });
153
+ fs.writeFileSync(path.join(directories.projectDirectory, 'opencode.json'), JSON.stringify(opencodeConfig, null, 2));
154
+ const dbPath = path.join(directories.dataDir, 'discord-sessions.db');
155
+ const hranaResult = await startHranaServer({ dbPath });
156
+ if (hranaResult instanceof Error) {
157
+ throw hranaResult;
158
+ }
159
+ process.env['KIMAKI_DB_URL'] = hranaResult;
160
+ await initDatabase();
161
+ await setBotToken(discord.botUserId, discord.botToken);
162
+ await setChannelDirectory({
163
+ channelId: TEXT_CHANNEL_ID,
164
+ directory: directories.projectDirectory,
165
+ channelType: 'text',
166
+ });
167
+ await setChannelVerbosity(TEXT_CHANNEL_ID, 'tools_and_text');
168
+ botClient = createDiscordJsClient({ restUrl: discord.restUrl });
169
+ await startDiscordBot({
170
+ token: discord.botToken,
171
+ appId: discord.botUserId,
172
+ discordClient: botClient,
173
+ });
174
+ // Pre-warm the opencode server
175
+ const warmup = await initializeOpencodeForDirectory(directories.projectDirectory);
176
+ if (warmup instanceof Error) {
177
+ throw warmup;
178
+ }
179
+ }, 60_000);
180
+ afterAll(async () => {
181
+ if (directories) {
182
+ await cleanupTestSessions({
183
+ projectDirectory: directories.projectDirectory,
184
+ testStartTime,
185
+ });
186
+ }
187
+ if (botClient) {
188
+ botClient.destroy();
189
+ }
190
+ await stopOpencodeServer();
191
+ await Promise.all([
192
+ closeDatabase().catch(() => { return; }),
193
+ stopHranaServer().catch(() => { return; }),
194
+ discord?.stop().catch(() => { return; }),
195
+ ]);
196
+ delete process.env['KIMAKI_LOCK_PORT'];
197
+ delete process.env['KIMAKI_DB_URL'];
198
+ if (previousDefaultVerbosity) {
199
+ store.setState({ defaultVerbosity: previousDefaultVerbosity });
200
+ }
201
+ if (directories) {
202
+ fs.rmSync(directories.dataDir, { recursive: true, force: true });
203
+ }
204
+ }, 10_000);
205
+ test('three sequential completions reuse same runtime and listener', async () => {
206
+ // Sends A, waits for full completion (footer), sends B, waits for
207
+ // footer, sends C, waits for footer. Proves the listener stays alive
208
+ // across full run cycles without any interrupt/queue involvement.
209
+ // This is the "calm" path — no abort, no queue, just sequential use.
210
+ // 1. Send first message → thread created, session established
211
+ await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
212
+ content: 'Reply with exactly: seq-alpha',
213
+ });
214
+ const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
215
+ timeout: 4_000,
216
+ predicate: (t) => {
217
+ return t.name === 'Reply with exactly: seq-alpha';
218
+ },
219
+ });
220
+ const th = discord.thread(thread.id);
221
+ // Wait for footer (italic project info line) — proves run A completed
222
+ await waitForBotMessageContaining({
223
+ discord,
224
+ threadId: thread.id,
225
+ userId: TEST_USER_ID,
226
+ text: '*project',
227
+ timeout: 4_000,
228
+ });
229
+ // Capture runtime identity — should not change across runs
230
+ const runtimeAfterA = getRuntime(thread.id);
231
+ expect(runtimeAfterA).toBeDefined();
232
+ // 2. Send B after A fully completed
233
+ await th.user(TEST_USER_ID).sendMessage({
234
+ content: 'Reply with exactly: seq-beta',
235
+ });
236
+ await waitForBotReplyAfterUserMessage({
237
+ discord,
238
+ threadId: thread.id,
239
+ userId: TEST_USER_ID,
240
+ userMessageIncludes: 'seq-beta',
241
+ timeout: 4_000,
242
+ });
243
+ // Wait for B's footer
244
+ await waitForBotMessageContaining({
245
+ discord,
246
+ threadId: thread.id,
247
+ userId: TEST_USER_ID,
248
+ text: '*project',
249
+ afterUserMessageIncludes: 'seq-beta',
250
+ timeout: 4_000,
251
+ });
252
+ // Same runtime instance — listener was not recreated
253
+ const runtimeAfterB = getRuntime(thread.id);
254
+ expect(runtimeAfterB).toBe(runtimeAfterA);
255
+ // 3. Send C after B fully completed
256
+ await th.user(TEST_USER_ID).sendMessage({
257
+ content: 'Reply with exactly: seq-gamma',
258
+ });
259
+ await waitForBotReplyAfterUserMessage({
260
+ discord,
261
+ threadId: thread.id,
262
+ userId: TEST_USER_ID,
263
+ userMessageIncludes: 'seq-gamma',
264
+ timeout: 4_000,
265
+ });
266
+ await waitForBotMessageContaining({
267
+ discord,
268
+ threadId: thread.id,
269
+ userId: TEST_USER_ID,
270
+ text: '*project',
271
+ afterUserMessageIncludes: 'seq-gamma',
272
+ timeout: 4_000,
273
+ });
274
+ // Still the same runtime — three full cycles, one runtime, one listener
275
+ const runtimeAfterC = getRuntime(thread.id);
276
+ expect(await th.text()).toMatchInlineSnapshot(`
277
+ "--- from: user (lifecycle-tester)
278
+ Reply with exactly: seq-alpha
279
+ --- from: assistant (TestBot)
280
+ ⬥ ok
281
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
282
+ --- from: user (lifecycle-tester)
283
+ Reply with exactly: seq-beta
284
+ --- from: assistant (TestBot)
285
+ ⬥ ok
286
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
287
+ --- from: user (lifecycle-tester)
288
+ Reply with exactly: seq-gamma
289
+ --- from: assistant (TestBot)
290
+ ⬥ ok
291
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
292
+ `);
293
+ expect(runtimeAfterC).toBe(runtimeAfterA);
294
+ }, 15_000);
295
+ test('footer includes context percentage and model id', async () => {
296
+ const prompt = 'Reply with exactly: footer-check';
297
+ await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
298
+ content: prompt,
299
+ });
300
+ const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
301
+ timeout: 4_000,
302
+ predicate: (t) => {
303
+ return t.name === prompt;
304
+ },
305
+ });
306
+ await waitForBotMessageContaining({
307
+ discord,
308
+ threadId: thread.id,
309
+ userId: TEST_USER_ID,
310
+ text: 'deterministic-v2',
311
+ timeout: 4_000,
312
+ });
313
+ const messages = await discord.thread(thread.id).getMessages();
314
+ const footerMessage = messages.find((message) => {
315
+ if (message.author.id !== discord.botUserId) {
316
+ return false;
317
+ }
318
+ if (!message.content.startsWith('*')) {
319
+ return false;
320
+ }
321
+ return message.content.includes('deterministic-v2');
322
+ });
323
+ expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
324
+ "--- from: user (lifecycle-tester)
325
+ Reply with exactly: footer-check
326
+ --- from: assistant (TestBot)
327
+ ⬥ ok
328
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
329
+ `);
330
+ expect(footerMessage).toBeDefined();
331
+ if (!footerMessage) {
332
+ throw new Error('Expected footer message to be present');
333
+ }
334
+ expect(footerMessage.content).toContain('deterministic-v2');
335
+ expect(footerMessage.content).toMatch(/\d+%/);
336
+ }, 10_000);
337
+ test('existing runtime reconnects after shared opencode server restart', async () => {
338
+ const prompt = 'Reply with exactly: reconnect-alpha';
339
+ await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
340
+ content: prompt,
341
+ });
342
+ const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
343
+ timeout: 4_000,
344
+ predicate: (t) => {
345
+ return t.name === prompt;
346
+ },
347
+ });
348
+ const th = discord.thread(thread.id);
349
+ await waitForBotMessageContaining({
350
+ discord,
351
+ threadId: thread.id,
352
+ userId: TEST_USER_ID,
353
+ text: '*project',
354
+ timeout: 4_000,
355
+ });
356
+ const runtimeBeforeRestart = getRuntime(thread.id);
357
+ expect(runtimeBeforeRestart).toBeDefined();
358
+ const restartResult = await restartOpencodeServer();
359
+ if (restartResult instanceof Error) {
360
+ throw restartResult;
361
+ }
362
+ await th.user(TEST_USER_ID).sendMessage({
363
+ content: 'Reply with exactly: reconnect-beta',
364
+ });
365
+ await waitForBotReplyAfterUserMessage({
366
+ discord,
367
+ threadId: thread.id,
368
+ userId: TEST_USER_ID,
369
+ userMessageIncludes: 'reconnect-beta',
370
+ timeout: 4_000,
371
+ });
372
+ await waitForBotMessageContaining({
373
+ discord,
374
+ threadId: thread.id,
375
+ userId: TEST_USER_ID,
376
+ text: '*project',
377
+ afterUserMessageIncludes: 'reconnect-beta',
378
+ timeout: 4_000,
379
+ });
380
+ expect(await th.text()).toMatchInlineSnapshot(`
381
+ "--- from: user (lifecycle-tester)
382
+ Reply with exactly: reconnect-alpha
383
+ --- from: assistant (TestBot)
384
+ ⬥ ok
385
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
386
+ --- from: user (lifecycle-tester)
387
+ Reply with exactly: reconnect-beta
388
+ --- from: assistant (TestBot)
389
+ ⬥ ok
390
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
391
+ `);
392
+ const runtimeAfterRestart = getRuntime(thread.id);
393
+ expect(runtimeAfterRestart).toBe(runtimeBeforeRestart);
394
+ }, 15_000);
395
+ test('does not print a context-usage notice for the final text part right before the footer', async () => {
396
+ const prompt = 'Reply with exactly: footer-high-usage';
397
+ await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
398
+ content: prompt,
399
+ });
400
+ const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
401
+ timeout: 4_000,
402
+ predicate: (t) => {
403
+ return t.name === prompt;
404
+ },
405
+ });
406
+ await waitForBotMessageContaining({
407
+ discord,
408
+ threadId: thread.id,
409
+ userId: TEST_USER_ID,
410
+ text: 'deterministic-v2',
411
+ timeout: 4_000,
412
+ });
413
+ expect(await discord.thread(thread.id).text()).toMatchInlineSnapshot(`
414
+ "--- from: user (lifecycle-tester)
415
+ Reply with exactly: footer-high-usage
416
+ --- from: assistant (TestBot)
417
+ ⬥ ok
418
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
419
+ `);
420
+ const threadText = await discord.thread(thread.id).text();
421
+ expect(threadText).not.toContain('⬦ context usage');
422
+ }, 10_000);
423
+ test('two near-simultaneous messages to same thread serialize correctly', async () => {
424
+ // Sends A to create a thread, then fires B and C simultaneously into
425
+ // the thread (no await between them). Without the old threadMessageQueue,
426
+ // the runtime's dispatchAction must serialize these. Both should get
427
+ // responses and the thread should not deadlock or create duplicate sessions.
428
+ // 1. Establish thread + session
429
+ await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
430
+ content: 'Reply with exactly: concurrent-setup',
431
+ });
432
+ const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
433
+ timeout: 4_000,
434
+ predicate: (t) => {
435
+ return t.name === 'Reply with exactly: concurrent-setup';
436
+ },
437
+ });
438
+ const th = discord.thread(thread.id);
439
+ const setupReply = await th.waitForBotReply({ timeout: 4_000 });
440
+ expect(setupReply.content.trim().length).toBeGreaterThan(0);
441
+ // Wait for setup footer so the run is fully idle
442
+ await waitForBotMessageContaining({
443
+ discord,
444
+ threadId: thread.id,
445
+ userId: TEST_USER_ID,
446
+ text: '*project',
447
+ timeout: 4_000,
448
+ });
449
+ // Snapshot bot message count before sending concurrent messages
450
+ const beforeMessages = await th.getMessages();
451
+ const beforeBotCount = beforeMessages.filter((m) => {
452
+ return m.author.id === discord.botUserId;
453
+ }).length;
454
+ // 2. Fire B and C simultaneously — no await between sends
455
+ const sendB = th.user(TEST_USER_ID).sendMessage({
456
+ content: 'Reply with exactly: concurrent-bravo',
457
+ });
458
+ const sendC = th.user(TEST_USER_ID).sendMessage({
459
+ content: 'Reply with exactly: concurrent-charlie',
460
+ });
461
+ await Promise.all([sendB, sendC]);
462
+ // 3. Both should eventually get bot replies — the runtime serializes them
463
+ await waitForBotReplyAfterUserMessage({
464
+ discord,
465
+ threadId: thread.id,
466
+ userId: TEST_USER_ID,
467
+ userMessageIncludes: 'concurrent-bravo',
468
+ timeout: 4_000,
469
+ });
470
+ await waitForBotReplyAfterUserMessage({
471
+ discord,
472
+ threadId: thread.id,
473
+ userId: TEST_USER_ID,
474
+ userMessageIncludes: 'concurrent-charlie',
475
+ timeout: 4_000,
476
+ });
477
+ // 4. Verify both user messages arrived and the thread didn't deadlock.
478
+ // With explicit abort flows, bravo can be aborted by charlie before
479
+ // producing a reply, so we can't assert +2 bot messages. What we
480
+ // CAN verify: both user messages exist, charlie (the last one) has
481
+ // a bot reply after it, and the replies are distinct messages.
482
+ // No inline snapshot here — the concurrent abort race makes message
483
+ // ordering nondeterministic (bravo may or may not get a reply).
484
+ const messages = await th.getMessages();
485
+ const bravoIndex = messages.findIndex((m) => {
486
+ return (m.author.id === TEST_USER_ID &&
487
+ m.content.includes('concurrent-bravo'));
488
+ });
489
+ const charlieIndex = messages.findIndex((m) => {
490
+ return (m.author.id === TEST_USER_ID &&
491
+ m.content.includes('concurrent-charlie'));
492
+ });
493
+ expect(bravoIndex).toBeGreaterThan(-1);
494
+ expect(charlieIndex).toBeGreaterThan(-1);
495
+ expect(bravoIndex).toBeLessThan(charlieIndex);
496
+ // Charlie (the last queued message) must have a bot reply after it.
497
+ const charlieReplyIndex = messages.findIndex((m, i) => {
498
+ return i > charlieIndex && m.author.id === discord.botUserId;
499
+ });
500
+ expect(charlieReplyIndex).toBeGreaterThan(-1);
501
+ // At least 1 new bot message appeared (charlie's reply). If bravo
502
+ // wasn't aborted, there will be 2. Either way, no deadlock.
503
+ const afterBotCount = messages.filter((m) => {
504
+ return m.author.id === discord.botUserId;
505
+ }).length;
506
+ expect(afterBotCount).toBeGreaterThanOrEqual(beforeBotCount + 1);
507
+ }, 15_000);
508
+ });
package/dist/sentry.js ADDED
@@ -0,0 +1,23 @@
1
+ // Sentry stubs. @sentry/node was removed — these are no-op placeholders
2
+ // so the 20+ files importing notifyError/initSentry don't need changing.
3
+ // If Sentry is re-enabled in the future, replace these stubs with real calls.
4
+ /**
5
+ * Initialize Sentry. Currently a no-op.
6
+ */
7
+ export function initSentry(_opts) { }
8
+ /**
9
+ * Report an unexpected error. Currently a no-op.
10
+ * Safe to call even if Sentry is not initialized.
11
+ * Fire-and-forget only: use `void notifyError(error, msg)` and never await it.
12
+ */
13
+ export function notifyError(_error, _msg) { }
14
+ /**
15
+ * User-readable error class. Messages from AppError instances
16
+ * are forwarded to the user as-is; regular Error messages may be obfuscated.
17
+ */
18
+ export class AppError extends Error {
19
+ constructor(message) {
20
+ super(message);
21
+ this.name = 'AppError';
22
+ }
23
+ }
@@ -0,0 +1,67 @@
1
+ // Agent preference resolution utility.
2
+ // Validates agent preferences against the OpenCode API.
3
+ import * as errore from 'errore';
4
+ import { getSessionAgent, getSessionModel, getChannelAgent, } from '../database.js';
5
+ import {} from '../opencode.js';
6
+ import {} from '../system-message.js';
7
+ export async function resolveValidatedAgentPreference({ agent, sessionId, channelId, getClient, }) {
8
+ const agentPreference = await (async () => {
9
+ if (agent) {
10
+ return agent;
11
+ }
12
+ const sessionAgent = await getSessionAgent(sessionId);
13
+ if (sessionAgent) {
14
+ return sessionAgent;
15
+ }
16
+ const sessionModel = await getSessionModel(sessionId);
17
+ if (sessionModel) {
18
+ return undefined;
19
+ }
20
+ if (!channelId) {
21
+ return undefined;
22
+ }
23
+ return getChannelAgent(channelId);
24
+ })();
25
+ if (getClient instanceof Error) {
26
+ return { agentPreference: agentPreference || undefined, agents: [] };
27
+ }
28
+ const agentsResponse = await errore.tryAsync(() => {
29
+ return getClient().app.agents({});
30
+ });
31
+ if (agentsResponse instanceof Error) {
32
+ if (agentPreference) {
33
+ throw new Error(`Failed to validate agent "${agentPreference}"`, {
34
+ cause: agentsResponse,
35
+ });
36
+ }
37
+ return { agentPreference: undefined, agents: [] };
38
+ }
39
+ const availableAgents = agentsResponse.data || [];
40
+ // Non-hidden primary/all agents for system message context
41
+ const agents = availableAgents
42
+ .filter((a) => {
43
+ return ((a.mode === 'primary' || a.mode === 'all') &&
44
+ !a.hidden);
45
+ })
46
+ .map((a) => {
47
+ return { name: a.name, description: a.description };
48
+ });
49
+ if (!agentPreference) {
50
+ return { agentPreference: undefined, agents };
51
+ }
52
+ const hasAgent = availableAgents.some((availableAgent) => {
53
+ return availableAgent.name === agentPreference;
54
+ });
55
+ if (hasAgent) {
56
+ return { agentPreference, agents };
57
+ }
58
+ const availableAgentNames = availableAgents
59
+ .map((availableAgent) => {
60
+ return availableAgent.name;
61
+ })
62
+ .slice(0, 20);
63
+ const availableAgentsMessage = availableAgentNames.length > 0
64
+ ? `Available agents: ${availableAgentNames.join(', ')}`
65
+ : 'No agents are available in this project.';
66
+ throw new Error(`Agent "${agentPreference}" not found. ${availableAgentsMessage} Use /agent to choose a valid one.`);
67
+ }