@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
package/src/tools.ts ADDED
@@ -0,0 +1,430 @@
1
+ // Voice assistant tool definitions for the GenAI worker.
2
+ // Provides tools for managing OpenCode sessions (create, submit, abort),
3
+ // listing chats, searching files, and reading session messages.
4
+
5
+ import { tool } from './ai-tool.js'
6
+ import { z } from 'zod'
7
+ import { spawn, type ChildProcess } from 'node:child_process'
8
+ import net from 'node:net'
9
+ import {
10
+ type OpencodeClient,
11
+ type AssistantMessage,
12
+ type Provider,
13
+ } from '@opencode-ai/sdk/v2'
14
+ import { createLogger, LogPrefix } from './logger.js'
15
+ import * as errore from 'errore'
16
+
17
+ const toolsLogger = createLogger(LogPrefix.TOOLS)
18
+
19
+ import { ShareMarkdown } from './markdown.js'
20
+ import { formatDistanceToNow } from './utils.js'
21
+ import pc from 'picocolors'
22
+ import {
23
+ initializeOpencodeForDirectory,
24
+ getOpencodeSystemMessage,
25
+ } from './discord-bot.js'
26
+
27
+ export async function getTools({
28
+ onMessageCompleted,
29
+ directory,
30
+ }: {
31
+ directory: string
32
+ onMessageCompleted?: (params: {
33
+ sessionId: string
34
+ messageId: string
35
+ data?: { info: AssistantMessage }
36
+ error?: unknown
37
+ markdown?: string
38
+ }) => void
39
+ }) {
40
+ const getClient = await initializeOpencodeForDirectory(directory)
41
+ if (getClient instanceof Error) {
42
+ throw new Error(getClient.message)
43
+ }
44
+ const client = getClient()
45
+
46
+ const markdownRenderer = new ShareMarkdown(client)
47
+
48
+ const providersResponse = await client.config.providers()
49
+ const providers: Provider[] = providersResponse.data?.providers || []
50
+
51
+ // Helper: get last assistant model for a session (non-summary)
52
+ const getSessionModel = async (
53
+ sessionId: string,
54
+ ): Promise<{ providerID: string; modelID: string } | undefined> => {
55
+ const res = await getClient().session.messages({ sessionID: sessionId })
56
+ const data = res.data
57
+ if (!data || data.length === 0) return undefined
58
+ for (let i = data.length - 1; i >= 0; i--) {
59
+ const info = data?.[i]?.info
60
+ if (info?.role === 'assistant') {
61
+ const ai = info as AssistantMessage
62
+ if (!ai.summary && ai.providerID && ai.modelID) {
63
+ return { providerID: ai.providerID, modelID: ai.modelID }
64
+ }
65
+ }
66
+ }
67
+ return undefined
68
+ }
69
+
70
+ const tools = {
71
+ submitMessage: tool({
72
+ description:
73
+ 'Submit a message to an existing chat session. Does not wait for the message to complete',
74
+ inputSchema: z.object({
75
+ sessionId: z.string().describe('The session ID to send message to'),
76
+ message: z.string().describe('The message text to send'),
77
+ }),
78
+ execute: async ({ sessionId, message }) => {
79
+ const sessionModel = await getSessionModel(sessionId)
80
+
81
+ // do not await
82
+ getClient()
83
+ .session.promptAsync({
84
+ sessionID: sessionId,
85
+ parts: [{ type: 'text', text: message }],
86
+ model: sessionModel,
87
+ system: getOpencodeSystemMessage({ sessionId }),
88
+ })
89
+ .then(async (response) => {
90
+ const markdownResult = await markdownRenderer.generate({
91
+ sessionID: sessionId,
92
+ lastAssistantOnly: true,
93
+ })
94
+ onMessageCompleted?.({
95
+ sessionId,
96
+ messageId: '',
97
+ markdown: errore.unwrapOr(markdownResult, ''),
98
+ })
99
+ })
100
+ .catch((error) => {
101
+ onMessageCompleted?.({
102
+ sessionId,
103
+ messageId: '',
104
+ error,
105
+ })
106
+ })
107
+ return {
108
+ success: true,
109
+ sessionId,
110
+ directive: 'Tell user that message has been sent successfully',
111
+ }
112
+ },
113
+ }),
114
+
115
+ createNewChat: tool({
116
+ description:
117
+ 'Start a new chat session with an initial message. Does not wait for the message to complete',
118
+ inputSchema: z.object({
119
+ message: z
120
+ .string()
121
+ .describe('The initial message to start the chat with'),
122
+ title: z.string().optional().describe('Optional title for the session'),
123
+ model: z
124
+ .object({
125
+ providerId: z
126
+ .string()
127
+ .describe('The provider ID (e.g., "anthropic", "openai")'),
128
+ modelId: z
129
+ .string()
130
+ .describe(
131
+ 'The model ID (e.g., "claude-opus-4-20250514", "gpt-5")',
132
+ ),
133
+ })
134
+ .optional()
135
+ .describe('Optional model to use for this session'),
136
+ }),
137
+ execute: async ({ message, title }) => {
138
+ if (!message.trim()) {
139
+ throw new Error(`message must be a non empty string`)
140
+ }
141
+
142
+ try {
143
+ const session = await getClient().session.create({
144
+ ...(title ? { title } : {}),
145
+ })
146
+
147
+ if (!session.data) {
148
+ throw new Error('Failed to create session')
149
+ }
150
+
151
+ // do not await
152
+ getClient()
153
+ .session.promptAsync({
154
+ sessionID: session.data.id,
155
+ parts: [{ type: 'text', text: message }],
156
+ system: getOpencodeSystemMessage({ sessionId: session.data.id }),
157
+ })
158
+ .then(async (response) => {
159
+ const markdownResult = await markdownRenderer.generate({
160
+ sessionID: session.data.id,
161
+ lastAssistantOnly: true,
162
+ })
163
+ onMessageCompleted?.({
164
+ sessionId: session.data.id,
165
+ messageId: '',
166
+ markdown: errore.unwrapOr(markdownResult, ''),
167
+ })
168
+ })
169
+ .catch((error) => {
170
+ onMessageCompleted?.({
171
+ sessionId: session.data.id,
172
+ messageId: '',
173
+ error,
174
+ })
175
+ })
176
+
177
+ return {
178
+ success: true,
179
+ sessionId: session.data.id,
180
+ title: session.data.title,
181
+ }
182
+ } catch (error) {
183
+ return {
184
+ success: false,
185
+ error:
186
+ error instanceof Error
187
+ ? error.message
188
+ : 'Failed to create chat session',
189
+ }
190
+ }
191
+ },
192
+ }),
193
+
194
+ listChats: tool({
195
+ description:
196
+ 'Get a list of available chat sessions sorted by most recent',
197
+ inputSchema: z.object({}),
198
+ execute: async () => {
199
+ toolsLogger.log(`Listing opencode sessions`)
200
+ const sessions = await getClient().session.list()
201
+
202
+ if (!sessions.data) {
203
+ return { success: false, error: 'No sessions found' }
204
+ }
205
+
206
+ const sortedSessions = [...sessions.data]
207
+ .sort((a, b) => {
208
+ return b.time.updated - a.time.updated
209
+ })
210
+ .slice(0, 20)
211
+
212
+ const sessionList = sortedSessions.map(async (session) => {
213
+ const finishedAt = session.time.updated
214
+ const status = await (async () => {
215
+ if (session.revert) return 'error'
216
+ const messagesResponse = await getClient().session.messages({
217
+ sessionID: session.id,
218
+ })
219
+ const messages = messagesResponse.data || []
220
+ const lastMessage = messages[messages.length - 1]
221
+ if (
222
+ lastMessage?.info.role === 'assistant' &&
223
+ !lastMessage.info.time.completed
224
+ ) {
225
+ return 'in_progress'
226
+ }
227
+ return 'finished'
228
+ })()
229
+
230
+ return {
231
+ id: session.id,
232
+ folder: session.directory,
233
+ status,
234
+ finishedAt: formatDistanceToNow(new Date(finishedAt)),
235
+ title: session.title,
236
+ prompt: session.title,
237
+ }
238
+ })
239
+
240
+ const resolvedList = await Promise.all(sessionList)
241
+
242
+ return {
243
+ success: true,
244
+ sessions: resolvedList,
245
+ }
246
+ },
247
+ }),
248
+
249
+ searchFiles: tool({
250
+ description: 'Search for files in a folder',
251
+ inputSchema: z.object({
252
+ folder: z
253
+ .string()
254
+ .optional()
255
+ .describe(
256
+ 'The folder path to search in, optional. only use if user specifically asks for it',
257
+ ),
258
+ query: z.string().describe('The search query for files'),
259
+ }),
260
+ execute: async ({ folder, query }) => {
261
+ const results = await getClient().find.files({
262
+ query,
263
+ directory: folder,
264
+ })
265
+
266
+ return {
267
+ success: true,
268
+ files: results.data || [],
269
+ }
270
+ },
271
+ }),
272
+
273
+ readSessionMessages: tool({
274
+ description: 'Read messages from a chat session',
275
+ inputSchema: z.object({
276
+ sessionId: z.string().describe('The session ID to read messages from'),
277
+ lastAssistantOnly: z
278
+ .boolean()
279
+ .optional()
280
+ .describe('Only read the last assistant message'),
281
+ }),
282
+ execute: async ({ sessionId, lastAssistantOnly = false }) => {
283
+ if (lastAssistantOnly) {
284
+ const messages = await getClient().session.messages({
285
+ sessionID: sessionId,
286
+ })
287
+
288
+ if (!messages.data) {
289
+ return { success: false, error: 'No messages found' }
290
+ }
291
+
292
+ const assistantMessages = messages.data.filter(
293
+ (m) => m.info.role === 'assistant',
294
+ )
295
+
296
+ if (assistantMessages.length === 0) {
297
+ return {
298
+ success: false,
299
+ error: 'No assistant messages found',
300
+ }
301
+ }
302
+
303
+ const lastMessage = assistantMessages[assistantMessages.length - 1]
304
+ const status =
305
+ 'completed' in lastMessage!.info.time &&
306
+ lastMessage!.info.time.completed
307
+ ? 'completed'
308
+ : 'in_progress'
309
+
310
+ const markdownResult = await markdownRenderer.generate({
311
+ sessionID: sessionId,
312
+ lastAssistantOnly: true,
313
+ })
314
+ if (markdownResult instanceof Error) {
315
+ throw new Error(markdownResult.message)
316
+ }
317
+
318
+ return {
319
+ success: true,
320
+ markdown: markdownResult,
321
+ status,
322
+ }
323
+ } else {
324
+ const markdownResult = await markdownRenderer.generate({
325
+ sessionID: sessionId,
326
+ })
327
+ if (markdownResult instanceof Error) {
328
+ throw new Error(markdownResult.message)
329
+ }
330
+
331
+ const messages = await getClient().session.messages({
332
+ sessionID: sessionId,
333
+ })
334
+ const lastMessage = messages.data?.[messages.data.length - 1]
335
+ const status =
336
+ lastMessage?.info.role === 'assistant' &&
337
+ lastMessage?.info.time &&
338
+ 'completed' in lastMessage.info.time &&
339
+ !lastMessage.info.time.completed
340
+ ? 'in_progress'
341
+ : 'completed'
342
+
343
+ return {
344
+ success: true,
345
+ markdown: markdownResult,
346
+ status,
347
+ }
348
+ }
349
+ },
350
+ }),
351
+
352
+ abortChat: tool({
353
+ description: 'Abort/stop an in-progress chat session',
354
+ inputSchema: z.object({
355
+ sessionId: z.string().describe('The session ID to abort'),
356
+ }),
357
+ execute: async ({ sessionId }) => {
358
+ try {
359
+ toolsLogger.log(
360
+ `[ABORT] reason=voice-tool sessionId=${sessionId} - user requested abort via voice assistant tool`,
361
+ )
362
+ const result = await getClient().session.abort({
363
+ sessionID: sessionId,
364
+ })
365
+
366
+ if (!result.data) {
367
+ return {
368
+ success: false,
369
+ error: 'Failed to abort session',
370
+ }
371
+ }
372
+
373
+ return {
374
+ success: true,
375
+ sessionId,
376
+ message: 'Session aborted successfully',
377
+ }
378
+ } catch (error) {
379
+ return {
380
+ success: false,
381
+ error:
382
+ error instanceof Error ? error.message : 'Unknown error occurred',
383
+ }
384
+ }
385
+ },
386
+ }),
387
+
388
+ getModels: tool({
389
+ description: 'Get all available AI models from all providers',
390
+ inputSchema: z.object({}),
391
+ execute: async () => {
392
+ try {
393
+ const providersResponse = await getClient().config.providers()
394
+ const providers: Provider[] = providersResponse.data?.providers || []
395
+
396
+ const models: Array<{ providerId: string; modelId: string }> = []
397
+
398
+ providers.forEach((provider) => {
399
+ if (provider.models && typeof provider.models === 'object') {
400
+ Object.entries(provider.models).forEach(([modelId, model]) => {
401
+ models.push({
402
+ providerId: provider.id,
403
+ modelId: modelId,
404
+ })
405
+ })
406
+ }
407
+ })
408
+
409
+ return {
410
+ success: true,
411
+ models,
412
+ totalCount: models.length,
413
+ }
414
+ } catch (error) {
415
+ return {
416
+ success: false,
417
+ error:
418
+ error instanceof Error ? error.message : 'Failed to fetch models',
419
+ models: [],
420
+ }
421
+ }
422
+ },
423
+ }),
424
+ }
425
+
426
+ return {
427
+ tools,
428
+ providers,
429
+ }
430
+ }
@@ -0,0 +1,12 @@
1
+ // Minimal type declarations for undici (transitive dep from discord.js).
2
+ // We don't list undici in package.json — discord.js bundles it.
3
+ declare module 'undici' {
4
+ export class Agent {
5
+ constructor(opts?: {
6
+ headersTimeout?: number
7
+ bodyTimeout?: number
8
+ connections?: number
9
+ })
10
+ }
11
+ export function setGlobalDispatcher(dispatcher: Agent): void
12
+ }
@@ -0,0 +1,209 @@
1
+ // E2e test for /undo command.
2
+ // Validates that:
3
+ // 1. After /undo, session.revert state is set (files reverted, revert boundary marked)
4
+ // 2. Messages are NOT deleted yet (they stay until next prompt cleans them up)
5
+ // 3. On the next user message, reverted messages are cleaned up by OpenCode's
6
+ // SessionRevert.cleanup() and the model only sees pre-revert messages
7
+ //
8
+ // This matches the OpenCode TUI behavior (use-session-commands.tsx):
9
+ // - Pass the user message ID (not assistant ID)
10
+ // - Don't delete messages — just mark session as reverted
11
+ // - Cleanup happens automatically on next promptAsync()
12
+ //
13
+ // Uses opencode-deterministic-provider (no real LLM calls).
14
+ // Poll timeouts: 4s max, 100ms interval.
15
+
16
+ import { describe, test, expect } from 'vitest'
17
+ import fs from 'node:fs'
18
+ import path from 'node:path'
19
+ import {
20
+ setupQueueAdvancedSuite,
21
+ TEST_USER_ID,
22
+ } from './queue-advanced-e2e-setup.js'
23
+ import {
24
+ waitForBotMessageContaining,
25
+ waitForFooterMessage,
26
+ } from './test-utils.js'
27
+ import { getThreadSession } from './database.js'
28
+ import { initializeOpencodeForDirectory } from './opencode.js'
29
+
30
+ const TEXT_CHANNEL_ID = '200000000000001200'
31
+
32
+ const e2eTest = describe
33
+
34
+ e2eTest('/undo sets revert state and cleans up on next prompt', () => {
35
+ const ctx = setupQueueAdvancedSuite({
36
+ channelId: TEXT_CHANNEL_ID,
37
+ channelName: 'qa-undo-e2e',
38
+ dirName: 'qa-undo-e2e',
39
+ username: 'undo-tester',
40
+ })
41
+
42
+ test(
43
+ 'undo sets revert state, next message cleans up reverted messages',
44
+ async () => {
45
+ const markerPath = path.join(
46
+ ctx.directories.projectDirectory,
47
+ 'tmp',
48
+ 'undo-marker.txt',
49
+ )
50
+
51
+ // 1. Send a message and wait for complete session (footer)
52
+ await ctx.discord
53
+ .channel(TEXT_CHANNEL_ID)
54
+ .user(TEST_USER_ID)
55
+ .sendMessage({
56
+ content: 'UNDO_FILE_MARKER',
57
+ })
58
+
59
+ const thread = await ctx.discord
60
+ .channel(TEXT_CHANNEL_ID)
61
+ .waitForThread({
62
+ timeout: 8_000,
63
+ predicate: (t) => {
64
+ return t.name === 'UNDO_FILE_MARKER'
65
+ },
66
+ })
67
+
68
+ const th = ctx.discord.thread(thread.id)
69
+ await th.waitForBotReply({ timeout: 8_000 })
70
+
71
+ await waitForFooterMessage({
72
+ discord: ctx.discord,
73
+ threadId: thread.id,
74
+ timeout: 8_000,
75
+ })
76
+
77
+ // 2. Get session ID and verify it has messages
78
+ const sessionId = await getThreadSession(thread.id)
79
+ expect(sessionId).toBeTruthy()
80
+
81
+ const getClient = await initializeOpencodeForDirectory(
82
+ ctx.directories.projectDirectory,
83
+ )
84
+ if (getClient instanceof Error) {
85
+ throw getClient
86
+ }
87
+
88
+ const beforeMessages = await getClient().session.messages({
89
+ sessionID: sessionId!,
90
+ directory: ctx.directories.projectDirectory,
91
+ })
92
+ const beforeCount = (beforeMessages.data || []).length
93
+ expect(beforeCount).toBeGreaterThan(0)
94
+
95
+ const beforeUserMessages = (beforeMessages.data || []).filter((m) => {
96
+ return m.info.role === 'user'
97
+ })
98
+ const beforeAssistantMessages = (beforeMessages.data || []).filter(
99
+ (m) => {
100
+ return m.info.role === 'assistant'
101
+ },
102
+ )
103
+ expect(beforeUserMessages.length).toBeGreaterThan(0)
104
+ expect(beforeAssistantMessages.length).toBeGreaterThan(0)
105
+ expect(fs.existsSync(markerPath)).toBe(true)
106
+
107
+ // Verify no revert state yet
108
+ const beforeSession = await getClient().session.get({
109
+ sessionID: sessionId!,
110
+ })
111
+ expect(beforeSession.data?.revert).toBeFalsy()
112
+
113
+ // 3. Run /undo command
114
+ const { id: undoInteractionId } = await th
115
+ .user(TEST_USER_ID)
116
+ .runSlashCommand({ name: 'undo' })
117
+
118
+ const undoAck = await th.waitForInteractionAck({
119
+ interactionId: undoInteractionId,
120
+ timeout: 4_000,
121
+ })
122
+ expect(undoAck).toBeDefined()
123
+
124
+ await waitForBotMessageContaining({
125
+ discord: ctx.discord,
126
+ threadId: thread.id,
127
+ text: 'Undone - reverted last assistant message',
128
+ timeout: 8_000,
129
+ })
130
+ // 4. Verify session now has revert state set
131
+ const afterSession = await getClient().session.get({
132
+ sessionID: sessionId!,
133
+ })
134
+ expect(afterSession.data?.revert).toBeTruthy()
135
+ expect(afterSession.data?.revert?.messageID).toBeTruthy()
136
+
137
+ // Messages should still exist (not deleted — cleanup happens on next prompt)
138
+ const afterMessages = await getClient().session.messages({
139
+ sessionID: sessionId!,
140
+ directory: ctx.directories.projectDirectory,
141
+ })
142
+ expect((afterMessages.data || []).length).toBe(beforeCount)
143
+
144
+ // 5. Send a new message — this triggers SessionRevert.cleanup()
145
+ // which removes reverted messages before processing the new prompt
146
+ await th.user(TEST_USER_ID).sendMessage({
147
+ content: 'Reply with exactly: after-undo-message',
148
+ })
149
+
150
+ await waitForFooterMessage({
151
+ discord: ctx.discord,
152
+ threadId: thread.id,
153
+ timeout: 8_000,
154
+ afterMessageIncludes: 'after-undo-message',
155
+ })
156
+
157
+ // 6. Verify reverted messages were cleaned up
158
+ const finalMessages = await getClient().session.messages({
159
+ sessionID: sessionId!,
160
+ directory: ctx.directories.projectDirectory,
161
+ })
162
+ const finalAssistantMessages = (finalMessages.data || []).filter(
163
+ (m) => {
164
+ return m.info.role === 'assistant'
165
+ },
166
+ )
167
+
168
+ // The original assistant message should have been cleaned up,
169
+ // only the new one (from after-undo-message) should remain
170
+ const originalAssistantStillExists = finalAssistantMessages.some(
171
+ (m) => {
172
+ return m.parts.some((p) => {
173
+ return p.type === 'text' && 'text' in p && p.text === 'ok'
174
+ })
175
+ },
176
+ )
177
+ // The first "ok" response was reverted and should be cleaned up.
178
+ // The new response for "after-undo-message" should produce a fresh "ok".
179
+ // We verify the total count dropped: the original user+assistant pair
180
+ // was removed, and replaced by just the new user+assistant pair.
181
+ expect(finalAssistantMessages.length).toBeLessThanOrEqual(
182
+ beforeAssistantMessages.length,
183
+ )
184
+
185
+ // Revert state should be cleared after cleanup
186
+ const finalSession = await getClient().session.get({
187
+ sessionID: sessionId!,
188
+ })
189
+ expect(finalSession.data?.revert).toBeFalsy()
190
+
191
+ // 7. Snapshot the Discord thread
192
+ expect(await th.text()).toMatchInlineSnapshot(`
193
+ "--- from: user (undo-tester)
194
+ UNDO_FILE_MARKER
195
+ --- from: assistant (TestBot)
196
+ ⬥ creating undo file
197
+ ⬥ undo file created
198
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
199
+ Undone - reverted last assistant message
200
+ --- from: user (undo-tester)
201
+ Reply with exactly: after-undo-message
202
+ --- from: assistant (TestBot)
203
+ ⬥ ok
204
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
205
+ `)
206
+ },
207
+ 20_000,
208
+ )
209
+ })