@otto-assistant/otto 0.1.1 → 0.7.15

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 (637) 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-account-identity.js +62 -0
  7. package/dist/anthropic-account-identity.test.js +38 -0
  8. package/dist/anthropic-auth-plugin.js +917 -0
  9. package/dist/anthropic-auth-state.js +303 -0
  10. package/dist/anthropic-auth-state.test.js +150 -0
  11. package/dist/bin.js +152 -0
  12. package/dist/btw-prefix-detection.js +17 -0
  13. package/dist/btw-prefix-detection.test.js +63 -0
  14. package/dist/channel-management.js +259 -0
  15. package/dist/cli-parsing.test.js +142 -0
  16. package/dist/cli-send-thread.e2e.test.js +353 -0
  17. package/dist/cli-telegram-options.test.js +99 -0
  18. package/dist/cli.js +4210 -568
  19. package/dist/commands/abort.js +65 -0
  20. package/dist/commands/action-buttons.js +245 -0
  21. package/dist/commands/add-dir.js +124 -0
  22. package/dist/commands/add-dir.test.js +126 -0
  23. package/dist/commands/add-project.js +113 -0
  24. package/dist/commands/agent.js +355 -0
  25. package/dist/commands/ask-question.js +320 -0
  26. package/dist/commands/ask-question.test.js +92 -0
  27. package/dist/commands/btw.js +121 -0
  28. package/dist/commands/cli-commands-group-a.test.js +728 -0
  29. package/dist/commands/cli-commands-group-b.test.js +695 -0
  30. package/dist/commands/compact.js +120 -0
  31. package/dist/commands/context-usage.js +140 -0
  32. package/dist/commands/create-new-project.js +130 -0
  33. package/dist/commands/diff.js +63 -0
  34. package/dist/commands/discord-commands-group-a.test.js +621 -0
  35. package/dist/commands/discord-commands-group-b.test.js +595 -0
  36. package/dist/commands/discord-commands-group-c.test.js +739 -0
  37. package/dist/commands/file-upload.js +275 -0
  38. package/dist/commands/fork-subagent.js +177 -0
  39. package/dist/commands/fork.js +262 -0
  40. package/dist/commands/gemini-apikey.js +70 -0
  41. package/dist/commands/login.js +887 -0
  42. package/dist/commands/mcp.js +239 -0
  43. package/dist/commands/memory-snapshot.js +24 -0
  44. package/dist/commands/mention-mode.js +44 -0
  45. package/dist/commands/merge-worktree.js +162 -0
  46. package/dist/commands/model-variant.js +366 -0
  47. package/dist/commands/model.js +794 -0
  48. package/dist/commands/new-worktree.js +465 -0
  49. package/dist/commands/paginated-select.js +57 -0
  50. package/dist/commands/permissions.js +274 -0
  51. package/dist/commands/queue.js +223 -0
  52. package/dist/commands/remove-project.js +115 -0
  53. package/dist/commands/restart-opencode-server.js +127 -0
  54. package/dist/commands/resume.js +149 -0
  55. package/dist/commands/run-command.js +79 -0
  56. package/dist/commands/screenshare.js +303 -0
  57. package/dist/commands/screenshare.test.js +20 -0
  58. package/dist/commands/session-id.js +78 -0
  59. package/dist/commands/session.js +176 -0
  60. package/dist/commands/share.js +80 -0
  61. package/dist/commands/tasks.js +205 -0
  62. package/dist/commands/thread-deletion-sync.js +50 -0
  63. package/dist/commands/types.js +2 -0
  64. package/dist/commands/undo-redo.js +305 -0
  65. package/dist/commands/unset-model.js +139 -0
  66. package/dist/commands/upgrade.js +48 -0
  67. package/dist/commands/user-command.js +155 -0
  68. package/dist/commands/verbosity.js +125 -0
  69. package/dist/commands/vscode.js +269 -0
  70. package/dist/commands/worktree-settings.js +43 -0
  71. package/dist/commands/worktrees.js +468 -0
  72. package/dist/condense-memory.js +33 -0
  73. package/dist/config.js +100 -255
  74. package/dist/context-awareness-plugin.js +340 -0
  75. package/dist/context-awareness-plugin.test.js +126 -0
  76. package/dist/critique-utils.js +95 -0
  77. package/dist/database.js +1355 -0
  78. package/dist/db.js +260 -0
  79. package/dist/db.test.js +138 -0
  80. package/dist/debounce-timeout.js +28 -0
  81. package/dist/debounced-process-flush.js +77 -0
  82. package/dist/discord-bot.js +1124 -0
  83. package/dist/discord-command-registration.js +567 -0
  84. package/dist/discord-urls.js +82 -0
  85. package/dist/discord-utils.js +616 -0
  86. package/dist/discord-utils.test.js +134 -0
  87. package/dist/errors.js +157 -0
  88. package/dist/escape-backticks.test.js +429 -0
  89. package/dist/event-stream-real-capture.e2e.test.js +533 -0
  90. package/dist/eventsource-parser.test.js +327 -0
  91. package/dist/exec-async.js +26 -0
  92. package/dist/external-opencode-sync.js +480 -0
  93. package/dist/format-tables.js +491 -0
  94. package/dist/format-tables.test.js +478 -0
  95. package/dist/forum-sync/config.js +79 -0
  96. package/dist/forum-sync/discord-operations.js +154 -0
  97. package/dist/forum-sync/index.js +5 -0
  98. package/dist/forum-sync/markdown.js +113 -0
  99. package/dist/forum-sync/sync-to-discord.js +417 -0
  100. package/dist/forum-sync/sync-to-files.js +190 -0
  101. package/dist/forum-sync/types.js +53 -0
  102. package/dist/forum-sync/watchers.js +307 -0
  103. package/dist/gateway-proxy-reconnect.e2e.test.js +394 -0
  104. package/dist/gateway-proxy.e2e.test.js +485 -0
  105. package/dist/genai-worker-wrapper.js +111 -0
  106. package/dist/genai-worker.js +311 -0
  107. package/dist/genai.js +232 -0
  108. package/dist/generated/browser.js +17 -0
  109. package/dist/generated/client.js +37 -0
  110. package/dist/generated/commonInputTypes.js +10 -0
  111. package/dist/generated/enums.js +58 -0
  112. package/dist/generated/internal/class.js +49 -0
  113. package/dist/generated/internal/prismaNamespace.js +254 -0
  114. package/dist/generated/internal/prismaNamespaceBrowser.js +224 -0
  115. package/dist/generated/models/bot_api_keys.js +1 -0
  116. package/dist/generated/models/bot_tokens.js +1 -0
  117. package/dist/generated/models/channel_agents.js +1 -0
  118. package/dist/generated/models/channel_directories.js +1 -0
  119. package/dist/generated/models/channel_mention_mode.js +1 -0
  120. package/dist/generated/models/channel_models.js +1 -0
  121. package/dist/generated/models/channel_verbosity.js +1 -0
  122. package/dist/generated/models/channel_worktrees.js +1 -0
  123. package/dist/generated/models/forum_sync_configs.js +1 -0
  124. package/dist/generated/models/global_models.js +1 -0
  125. package/dist/generated/models/ipc_requests.js +1 -0
  126. package/dist/generated/models/part_messages.js +1 -0
  127. package/dist/generated/models/scheduled_tasks.js +1 -0
  128. package/dist/generated/models/session_agents.js +1 -0
  129. package/dist/generated/models/session_events.js +1 -0
  130. package/dist/generated/models/session_models.js +1 -0
  131. package/dist/generated/models/session_start_sources.js +1 -0
  132. package/dist/generated/models/thread_sessions.js +1 -0
  133. package/dist/generated/models/thread_worktrees.js +1 -0
  134. package/dist/generated/models.js +1 -0
  135. package/dist/heap-monitor.js +122 -0
  136. package/dist/hrana-server.js +251 -0
  137. package/dist/hrana-server.test.js +370 -0
  138. package/dist/html-actions.js +123 -0
  139. package/dist/html-actions.test.js +70 -0
  140. package/dist/html-components.js +117 -0
  141. package/dist/html-components.test.js +34 -0
  142. package/dist/image-optimizer-plugin.js +153 -0
  143. package/dist/image-utils.js +112 -0
  144. package/dist/interaction-handler.js +420 -0
  145. package/dist/ipc-polling.js +327 -0
  146. package/dist/ipc-tools-plugin.js +193 -0
  147. package/dist/ipc-utils.js +18 -0
  148. package/dist/limit-heading-depth.js +25 -0
  149. package/dist/limit-heading-depth.test.js +105 -0
  150. package/dist/logger.js +171 -0
  151. package/dist/markdown.js +342 -0
  152. package/dist/markdown.test.js +264 -0
  153. package/dist/memory-overview-plugin.js +128 -0
  154. package/dist/message-finish-field.e2e.test.js +168 -0
  155. package/dist/message-formatting.js +415 -0
  156. package/dist/message-formatting.test.js +115 -0
  157. package/dist/message-preprocessing.js +359 -0
  158. package/dist/onboarding-tutorial.js +163 -0
  159. package/dist/onboarding-welcome.js +37 -0
  160. package/dist/openai-realtime.js +224 -0
  161. package/dist/opencode-command-detection.js +65 -0
  162. package/dist/opencode-command-detection.test.js +240 -0
  163. package/dist/opencode-command.js +131 -0
  164. package/dist/opencode-command.test.js +48 -0
  165. package/dist/opencode-interrupt-plugin.js +388 -0
  166. package/dist/opencode-interrupt-plugin.test.js +463 -0
  167. package/dist/opencode.js +1117 -0
  168. package/dist/otto/branding.js +22 -0
  169. package/dist/otto/index.js +21 -0
  170. package/dist/otto-digital-twin.e2e.test.js +161 -0
  171. package/dist/otto-opencode-plugin-loading.e2e.test.js +94 -0
  172. package/dist/otto-opencode-plugin.js +21 -0
  173. package/dist/otto-opencode-plugin.test.js +98 -0
  174. package/dist/parse-permission-rules.test.js +117 -0
  175. package/dist/patch-text-parser.js +97 -0
  176. package/dist/plugin-logger.js +68 -0
  177. package/dist/privacy-sanitizer.js +105 -0
  178. package/dist/queue-advanced-abort.e2e.test.js +293 -0
  179. package/dist/queue-advanced-action-buttons.e2e.test.js +206 -0
  180. package/dist/queue-advanced-e2e-setup.js +790 -0
  181. package/dist/queue-advanced-footer.e2e.test.js +481 -0
  182. package/dist/queue-advanced-model-switch.e2e.test.js +299 -0
  183. package/dist/queue-advanced-permissions-typing.e2e.test.js +179 -0
  184. package/dist/queue-advanced-question.e2e.test.js +261 -0
  185. package/dist/queue-advanced-typing-interrupt.e2e.test.js +114 -0
  186. package/dist/queue-advanced-typing.e2e.test.js +153 -0
  187. package/dist/queue-drain-after-interactive-ui.e2e.test.js +119 -0
  188. package/dist/queue-interrupt-drain.e2e.test.js +135 -0
  189. package/dist/queue-question-select-drain.e2e.test.js +256 -0
  190. package/dist/runtime-idle-sweeper.js +52 -0
  191. package/dist/runtime-lifecycle.e2e.test.js +514 -0
  192. package/dist/sentry.js +23 -0
  193. package/dist/session-handler/agent-utils.js +67 -0
  194. package/dist/session-handler/event-stream-state.js +475 -0
  195. package/dist/session-handler/event-stream-state.test.js +632 -0
  196. package/dist/session-handler/model-utils.js +147 -0
  197. package/dist/session-handler/opencode-session-event-log.js +94 -0
  198. package/dist/session-handler/thread-runtime-state.js +131 -0
  199. package/dist/session-handler/thread-session-runtime.js +3390 -0
  200. package/dist/session-handler.js +9 -0
  201. package/dist/session-search.js +100 -0
  202. package/dist/session-search.test.js +40 -0
  203. package/dist/session-title-rename.test.js +92 -0
  204. package/dist/skill-filter.js +31 -0
  205. package/dist/skill-filter.test.js +65 -0
  206. package/dist/startup-service.js +153 -0
  207. package/dist/startup-time.e2e.test.js +296 -0
  208. package/dist/store.js +19 -0
  209. package/dist/subagent-rate-limit-plugin.js +175 -0
  210. package/dist/system-message.js +702 -0
  211. package/dist/system-message.test.js +697 -0
  212. package/dist/task-runner.js +530 -0
  213. package/dist/task-schedule.js +213 -0
  214. package/dist/task-schedule.test.js +71 -0
  215. package/dist/test-utils.js +313 -0
  216. package/dist/thinking-utils.js +35 -0
  217. package/dist/thread-message-queue.e2e.test.js +1111 -0
  218. package/dist/tools.js +357 -0
  219. package/dist/undo-redo.e2e.test.js +161 -0
  220. package/dist/unnest-code-blocks.js +146 -0
  221. package/dist/unnest-code-blocks.test.js +673 -0
  222. package/dist/upgrade.js +156 -0
  223. package/dist/utils.js +172 -0
  224. package/dist/utils.test.js +130 -0
  225. package/dist/voice-attachment.js +34 -0
  226. package/dist/voice-handler.js +646 -0
  227. package/dist/voice-message.e2e.test.js +1021 -0
  228. package/dist/voice.js +456 -0
  229. package/dist/voice.test.js +235 -0
  230. package/dist/wait-session.js +171 -0
  231. package/dist/websockify.js +69 -0
  232. package/dist/worker-types.js +4 -0
  233. package/dist/worktree-lifecycle.e2e.test.js +311 -0
  234. package/dist/worktree-utils.js +3 -0
  235. package/dist/worktrees.js +991 -0
  236. package/dist/worktrees.test.js +415 -0
  237. package/dist/xml.js +92 -0
  238. package/dist/xml.test.js +32 -0
  239. package/package.json +90 -38
  240. package/schema.prisma +303 -0
  241. package/skills/batch/SKILL.md +87 -0
  242. package/skills/critique/SKILL.md +112 -0
  243. package/skills/egaki/SKILL.md +100 -0
  244. package/skills/errore/SKILL.md +647 -0
  245. package/skills/event-sourcing-state/SKILL.md +252 -0
  246. package/skills/goke/SKILL.md +38 -0
  247. package/skills/jitter/EDITOR.md +219 -0
  248. package/skills/jitter/EXPORT-INTERNALS.md +309 -0
  249. package/skills/jitter/SKILL.md +158 -0
  250. package/skills/jitter/jitter-clipboard.json +1042 -0
  251. package/skills/jitter/package.json +14 -0
  252. package/skills/jitter/tsconfig.json +15 -0
  253. package/skills/jitter/utils/actions.ts +212 -0
  254. package/skills/jitter/utils/export.ts +114 -0
  255. package/skills/jitter/utils/index.ts +141 -0
  256. package/skills/jitter/utils/snapshot.ts +154 -0
  257. package/skills/jitter/utils/traverse.ts +246 -0
  258. package/skills/jitter/utils/types.ts +279 -0
  259. package/skills/jitter/utils/wait.ts +133 -0
  260. package/skills/lintcn/SKILL.md +873 -0
  261. package/skills/manual-kimaki-upstream-adapt/SKILL.md +114 -0
  262. package/skills/new-skill/SKILL.md +237 -0
  263. package/skills/npm-package/SKILL.md +617 -0
  264. package/skills/opensrc/SKILL.md +78 -0
  265. package/skills/otto-publish/SKILL.md +61 -0
  266. package/skills/playwriter/SKILL.md +35 -0
  267. package/skills/profano/SKILL.md +16 -0
  268. package/skills/proxyman/SKILL.md +215 -0
  269. package/skills/security-review/SKILL.md +208 -0
  270. package/skills/sigillo/SKILL.md +101 -0
  271. package/skills/simplify/SKILL.md +58 -0
  272. package/skills/spiceflow/SKILL.md +28 -0
  273. package/skills/termcast/SKILL.md +945 -0
  274. package/skills/tuistory/SKILL.md +98 -0
  275. package/skills/usecomputer/SKILL.md +264 -0
  276. package/skills/x-articles/SKILL.md +554 -0
  277. package/skills/zele/SKILL.md +49 -0
  278. package/skills/zustand-centralized-state/SKILL.md +1004 -0
  279. package/src/agent-model.e2e.test.ts +979 -0
  280. package/src/ai-tool-to-genai.test.ts +296 -0
  281. package/src/ai-tool-to-genai.ts +283 -0
  282. package/src/ai-tool.ts +39 -0
  283. package/src/anthropic-account-identity.test.ts +52 -0
  284. package/src/anthropic-account-identity.ts +77 -0
  285. package/src/anthropic-auth-plugin.ts +1139 -0
  286. package/src/anthropic-auth-state.test.ts +187 -0
  287. package/src/anthropic-auth-state.ts +386 -0
  288. package/src/bin.ts +182 -0
  289. package/src/btw-prefix-detection.test.ts +73 -0
  290. package/src/btw-prefix-detection.ts +23 -0
  291. package/src/channel-management.ts +376 -0
  292. package/src/cli-parsing.test.ts +197 -0
  293. package/src/cli-send-thread.e2e.test.ts +463 -0
  294. package/src/cli-telegram-options.test.ts +114 -0
  295. package/src/cli.ts +5718 -580
  296. package/src/commands/abort.ts +89 -0
  297. package/src/commands/action-buttons.ts +364 -0
  298. package/src/commands/add-dir.test.ts +154 -0
  299. package/src/commands/add-dir.ts +175 -0
  300. package/src/commands/add-project.ts +149 -0
  301. package/src/commands/agent.ts +496 -0
  302. package/src/commands/ask-question.test.ts +111 -0
  303. package/src/commands/ask-question.ts +455 -0
  304. package/src/commands/btw.ts +184 -0
  305. package/src/commands/cli-commands-group-a.test.ts +837 -0
  306. package/src/commands/cli-commands-group-b.test.ts +800 -0
  307. package/src/commands/compact.ts +157 -0
  308. package/src/commands/context-usage.ts +199 -0
  309. package/src/commands/create-new-project.ts +190 -0
  310. package/src/commands/diff.ts +91 -0
  311. package/src/commands/discord-commands-group-a.test.ts +751 -0
  312. package/src/commands/discord-commands-group-b.test.ts +648 -0
  313. package/src/commands/discord-commands-group-c.test.ts +882 -0
  314. package/src/commands/file-upload.ts +389 -0
  315. package/src/commands/fork-subagent.ts +263 -0
  316. package/src/commands/fork.ts +386 -0
  317. package/src/commands/gemini-apikey.ts +104 -0
  318. package/src/commands/login.ts +1175 -0
  319. package/src/commands/mcp.ts +307 -0
  320. package/src/commands/memory-snapshot.ts +30 -0
  321. package/src/commands/mention-mode.ts +68 -0
  322. package/src/commands/merge-worktree.ts +226 -0
  323. package/src/commands/model-variant.ts +485 -0
  324. package/src/commands/model.ts +1078 -0
  325. package/src/commands/new-worktree.ts +645 -0
  326. package/src/commands/paginated-select.ts +81 -0
  327. package/src/commands/permissions.ts +397 -0
  328. package/src/commands/queue.ts +293 -0
  329. package/src/commands/remove-project.ts +155 -0
  330. package/src/commands/restart-opencode-server.ts +162 -0
  331. package/src/commands/resume.ts +230 -0
  332. package/src/commands/run-command.ts +123 -0
  333. package/src/commands/screenshare.test.ts +30 -0
  334. package/src/commands/screenshare.ts +366 -0
  335. package/src/commands/session-id.ts +109 -0
  336. package/src/commands/session.ts +227 -0
  337. package/src/commands/share.ts +106 -0
  338. package/src/commands/tasks.ts +293 -0
  339. package/src/commands/thread-deletion-sync.ts +80 -0
  340. package/src/commands/types.ts +25 -0
  341. package/src/commands/undo-redo.ts +386 -0
  342. package/src/commands/unset-model.ts +174 -0
  343. package/src/commands/upgrade.ts +59 -0
  344. package/src/commands/user-command.ts +198 -0
  345. package/src/commands/verbosity.ts +173 -0
  346. package/src/commands/vscode.ts +342 -0
  347. package/src/commands/worktree-settings.ts +70 -0
  348. package/src/commands/worktrees.ts +645 -0
  349. package/src/condense-memory.ts +36 -0
  350. package/src/config.ts +103 -339
  351. package/src/context-awareness-plugin.test.ts +144 -0
  352. package/src/context-awareness-plugin.ts +469 -0
  353. package/src/critique-utils.ts +139 -0
  354. package/src/database.ts +1949 -0
  355. package/src/db.test.ts +162 -0
  356. package/src/db.ts +295 -0
  357. package/src/debounce-timeout.ts +43 -0
  358. package/src/debounced-process-flush.ts +104 -0
  359. package/src/discord-bot.ts +1505 -0
  360. package/src/discord-command-registration.ts +752 -0
  361. package/src/discord-urls.ts +89 -0
  362. package/src/discord-utils.test.ts +153 -0
  363. package/src/discord-utils.ts +846 -0
  364. package/src/errors.ts +201 -0
  365. package/src/escape-backticks.test.ts +469 -0
  366. package/src/event-stream-real-capture.e2e.test.ts +692 -0
  367. package/src/eventsource-parser.test.ts +351 -0
  368. package/src/exec-async.ts +35 -0
  369. package/src/external-opencode-sync.ts +685 -0
  370. package/src/format-tables.test.ts +515 -0
  371. package/src/format-tables.ts +718 -0
  372. package/src/forum-sync/config.ts +92 -0
  373. package/src/forum-sync/discord-operations.ts +241 -0
  374. package/src/forum-sync/index.ts +9 -0
  375. package/src/forum-sync/markdown.ts +172 -0
  376. package/src/forum-sync/sync-to-discord.ts +595 -0
  377. package/src/forum-sync/sync-to-files.ts +294 -0
  378. package/src/forum-sync/types.ts +175 -0
  379. package/src/forum-sync/watchers.ts +454 -0
  380. package/src/gateway-proxy-reconnect.e2e.test.ts +523 -0
  381. package/src/gateway-proxy.e2e.test.ts +644 -0
  382. package/src/genai-worker-wrapper.ts +164 -0
  383. package/src/genai-worker.ts +386 -0
  384. package/src/genai.ts +321 -0
  385. package/src/generated/browser.ts +114 -0
  386. package/src/generated/client.ts +138 -0
  387. package/src/generated/commonInputTypes.ts +770 -0
  388. package/src/generated/enums.ts +98 -0
  389. package/src/generated/internal/class.ts +384 -0
  390. package/src/generated/internal/prismaNamespace.ts +2394 -0
  391. package/src/generated/internal/prismaNamespaceBrowser.ts +327 -0
  392. package/src/generated/models/bot_api_keys.ts +1288 -0
  393. package/src/generated/models/bot_tokens.ts +1700 -0
  394. package/src/generated/models/channel_agents.ts +1256 -0
  395. package/src/generated/models/channel_directories.ts +1859 -0
  396. package/src/generated/models/channel_mention_mode.ts +1300 -0
  397. package/src/generated/models/channel_models.ts +1288 -0
  398. package/src/generated/models/channel_verbosity.ts +1228 -0
  399. package/src/generated/models/channel_worktrees.ts +1300 -0
  400. package/src/generated/models/forum_sync_configs.ts +1452 -0
  401. package/src/generated/models/global_models.ts +1288 -0
  402. package/src/generated/models/ipc_requests.ts +1485 -0
  403. package/src/generated/models/part_messages.ts +1302 -0
  404. package/src/generated/models/scheduled_tasks.ts +2320 -0
  405. package/src/generated/models/session_agents.ts +1086 -0
  406. package/src/generated/models/session_events.ts +1439 -0
  407. package/src/generated/models/session_models.ts +1114 -0
  408. package/src/generated/models/session_start_sources.ts +1408 -0
  409. package/src/generated/models/thread_sessions.ts +1781 -0
  410. package/src/generated/models/thread_worktrees.ts +1356 -0
  411. package/src/generated/models.ts +30 -0
  412. package/src/heap-monitor.ts +152 -0
  413. package/src/hrana-server.test.ts +434 -0
  414. package/src/hrana-server.ts +299 -0
  415. package/src/html-actions.test.ts +87 -0
  416. package/src/html-actions.ts +174 -0
  417. package/src/html-components.test.ts +38 -0
  418. package/src/html-components.ts +181 -0
  419. package/src/image-optimizer-plugin.ts +194 -0
  420. package/src/image-utils.ts +149 -0
  421. package/src/interaction-handler.ts +610 -0
  422. package/src/ipc-polling.ts +427 -0
  423. package/src/ipc-tools-plugin.ts +236 -0
  424. package/src/ipc-utils.ts +29 -0
  425. package/src/limit-heading-depth.test.ts +116 -0
  426. package/src/limit-heading-depth.ts +26 -0
  427. package/src/logger.ts +215 -0
  428. package/src/markdown.test.ts +315 -0
  429. package/src/markdown.ts +410 -0
  430. package/src/memory-overview-plugin.ts +163 -0
  431. package/src/message-finish-field.e2e.test.ts +195 -0
  432. package/src/message-formatting.test.ts +126 -0
  433. package/src/message-formatting.ts +535 -0
  434. package/src/message-preprocessing.ts +488 -0
  435. package/src/onboarding-tutorial.ts +167 -0
  436. package/src/onboarding-welcome.ts +49 -0
  437. package/src/openai-realtime.ts +358 -0
  438. package/src/opencode-command-detection.test.ts +307 -0
  439. package/src/opencode-command-detection.ts +76 -0
  440. package/src/opencode-command.test.ts +70 -0
  441. package/src/opencode-command.ts +191 -0
  442. package/src/opencode-interrupt-plugin.test.ts +682 -0
  443. package/src/opencode-interrupt-plugin.ts +507 -0
  444. package/src/opencode.ts +1453 -0
  445. package/src/otto/branding.ts +23 -0
  446. package/src/otto/index.ts +22 -0
  447. package/src/otto-digital-twin.e2e.test.ts +199 -0
  448. package/src/otto-opencode-plugin-loading.e2e.test.ts +117 -0
  449. package/src/otto-opencode-plugin.test.ts +108 -0
  450. package/src/otto-opencode-plugin.ts +22 -0
  451. package/src/parse-permission-rules.test.ts +127 -0
  452. package/src/patch-text-parser.ts +107 -0
  453. package/src/plugin-logger.ts +84 -0
  454. package/src/privacy-sanitizer.ts +142 -0
  455. package/src/queue-advanced-abort.e2e.test.ts +382 -0
  456. package/src/queue-advanced-action-buttons.e2e.test.ts +268 -0
  457. package/src/queue-advanced-e2e-setup.ts +877 -0
  458. package/src/queue-advanced-footer.e2e.test.ts +591 -0
  459. package/src/queue-advanced-model-switch.e2e.test.ts +383 -0
  460. package/src/queue-advanced-permissions-typing.e2e.test.ts +246 -0
  461. package/src/queue-advanced-question.e2e.test.ts +316 -0
  462. package/src/queue-advanced-typing-interrupt.e2e.test.ts +146 -0
  463. package/src/queue-advanced-typing.e2e.test.ts +199 -0
  464. package/src/queue-drain-after-interactive-ui.e2e.test.ts +151 -0
  465. package/src/queue-interrupt-drain.e2e.test.ts +166 -0
  466. package/src/queue-question-select-drain.e2e.test.ts +327 -0
  467. package/src/runtime-idle-sweeper.ts +76 -0
  468. package/src/runtime-lifecycle.e2e.test.ts +651 -0
  469. package/src/schema.sql +174 -0
  470. package/src/sentry.ts +26 -0
  471. package/src/session-handler/agent-utils.ts +99 -0
  472. package/src/session-handler/event-stream-fixtures/real-session-action-buttons.jsonl +45 -0
  473. package/src/session-handler/event-stream-fixtures/real-session-footer-suppressed-on-pre-idle-interrupt.jsonl +40 -0
  474. package/src/session-handler/event-stream-fixtures/real-session-permission-external-file.jsonl +23 -0
  475. package/src/session-handler/event-stream-fixtures/real-session-task-normal.jsonl +22 -0
  476. package/src/session-handler/event-stream-fixtures/real-session-task-three-parallel-sleeps.jsonl +277 -0
  477. package/src/session-handler/event-stream-fixtures/real-session-task-user-interruption.jsonl +46 -0
  478. package/src/session-handler/event-stream-fixtures/session-abort-after-idle-race.jsonl +21 -0
  479. package/src/session-handler/event-stream-fixtures/session-concurrent-messages-serialized.jsonl +56 -0
  480. package/src/session-handler/event-stream-fixtures/session-explicit-abort.jsonl +44 -0
  481. package/src/session-handler/event-stream-fixtures/session-normal-completion.jsonl +29 -0
  482. package/src/session-handler/event-stream-fixtures/session-tool-call-noisy-stream.jsonl +29 -0
  483. package/src/session-handler/event-stream-fixtures/session-two-completions-same-session.jsonl +50 -0
  484. package/src/session-handler/event-stream-fixtures/session-user-interruption.jsonl +59 -0
  485. package/src/session-handler/event-stream-fixtures/session-voice-queued-followup.jsonl +52 -0
  486. package/src/session-handler/event-stream-state.test.ts +717 -0
  487. package/src/session-handler/event-stream-state.ts +706 -0
  488. package/src/session-handler/model-utils.ts +217 -0
  489. package/src/session-handler/opencode-session-event-log.ts +130 -0
  490. package/src/session-handler/thread-runtime-state.ts +247 -0
  491. package/src/session-handler/thread-session-runtime.ts +4440 -0
  492. package/src/session-handler.ts +15 -0
  493. package/src/session-search.test.ts +50 -0
  494. package/src/session-search.ts +148 -0
  495. package/src/session-title-rename.test.ts +130 -0
  496. package/src/skill-filter.test.ts +83 -0
  497. package/src/skill-filter.ts +42 -0
  498. package/src/startup-service.ts +200 -0
  499. package/src/startup-time.e2e.test.ts +373 -0
  500. package/src/store.ts +139 -0
  501. package/src/subagent-rate-limit-plugin.ts +218 -0
  502. package/src/system-message.test.ts +710 -0
  503. package/src/system-message.ts +814 -0
  504. package/src/task-runner.ts +725 -0
  505. package/src/task-schedule.test.ts +84 -0
  506. package/src/task-schedule.ts +317 -0
  507. package/src/test-utils.ts +451 -0
  508. package/src/thinking-utils.ts +61 -0
  509. package/src/thread-message-queue.e2e.test.ts +1350 -0
  510. package/src/tools.ts +430 -0
  511. package/src/undici.d.ts +12 -0
  512. package/src/undo-redo.e2e.test.ts +209 -0
  513. package/src/unnest-code-blocks.test.ts +713 -0
  514. package/src/unnest-code-blocks.ts +185 -0
  515. package/src/upgrade.ts +185 -0
  516. package/src/utils.test.ts +155 -0
  517. package/src/utils.ts +265 -0
  518. package/src/voice-attachment.ts +51 -0
  519. package/src/voice-handler.ts +908 -0
  520. package/src/voice-message.e2e.test.ts +1255 -0
  521. package/src/voice.test.ts +281 -0
  522. package/src/voice.ts +638 -0
  523. package/src/wait-session.ts +273 -0
  524. package/src/websockify.ts +101 -0
  525. package/src/worker-types.ts +64 -0
  526. package/src/worktree-lifecycle.e2e.test.ts +396 -0
  527. package/src/worktree-utils.ts +4 -0
  528. package/src/worktrees.test.ts +489 -0
  529. package/src/worktrees.ts +1370 -0
  530. package/src/xml.test.ts +38 -0
  531. package/src/xml.ts +121 -0
  532. package/dist/cli.d.ts +0 -3
  533. package/dist/cli.d.ts.map +0 -1
  534. package/dist/cli.js.map +0 -1
  535. package/dist/config.d.ts +0 -39
  536. package/dist/config.d.ts.map +0 -1
  537. package/dist/config.js.map +0 -1
  538. package/dist/config.test.d.ts +0 -2
  539. package/dist/config.test.d.ts.map +0 -1
  540. package/dist/config.test.js +0 -202
  541. package/dist/config.test.js.map +0 -1
  542. package/dist/detect.d.ts +0 -9
  543. package/dist/detect.d.ts.map +0 -1
  544. package/dist/detect.js +0 -40
  545. package/dist/detect.js.map +0 -1
  546. package/dist/detect.test.d.ts +0 -2
  547. package/dist/detect.test.d.ts.map +0 -1
  548. package/dist/detect.test.js +0 -26
  549. package/dist/detect.test.js.map +0 -1
  550. package/dist/docker.d.ts +0 -7
  551. package/dist/docker.d.ts.map +0 -1
  552. package/dist/docker.js +0 -17
  553. package/dist/docker.js.map +0 -1
  554. package/dist/docker.test.d.ts +0 -2
  555. package/dist/docker.test.d.ts.map +0 -1
  556. package/dist/docker.test.js +0 -12
  557. package/dist/docker.test.js.map +0 -1
  558. package/dist/health.d.ts +0 -31
  559. package/dist/health.d.ts.map +0 -1
  560. package/dist/health.js +0 -117
  561. package/dist/health.js.map +0 -1
  562. package/dist/health.test.d.ts +0 -2
  563. package/dist/health.test.d.ts.map +0 -1
  564. package/dist/health.test.js +0 -52
  565. package/dist/health.test.js.map +0 -1
  566. package/dist/index.d.ts +0 -20
  567. package/dist/index.d.ts.map +0 -1
  568. package/dist/index.js +0 -15
  569. package/dist/index.js.map +0 -1
  570. package/dist/index.test.d.ts +0 -2
  571. package/dist/index.test.d.ts.map +0 -1
  572. package/dist/index.test.js +0 -8
  573. package/dist/index.test.js.map +0 -1
  574. package/dist/installer.d.ts +0 -10
  575. package/dist/installer.d.ts.map +0 -1
  576. package/dist/installer.js +0 -50
  577. package/dist/installer.js.map +0 -1
  578. package/dist/installer.test.d.ts +0 -2
  579. package/dist/installer.test.d.ts.map +0 -1
  580. package/dist/installer.test.js +0 -43
  581. package/dist/installer.test.js.map +0 -1
  582. package/dist/lifecycle.d.ts +0 -10
  583. package/dist/lifecycle.d.ts.map +0 -1
  584. package/dist/lifecycle.js +0 -45
  585. package/dist/lifecycle.js.map +0 -1
  586. package/dist/lifecycle.test.d.ts +0 -2
  587. package/dist/lifecycle.test.d.ts.map +0 -1
  588. package/dist/lifecycle.test.js +0 -20
  589. package/dist/lifecycle.test.js.map +0 -1
  590. package/dist/manifest.d.ts +0 -18
  591. package/dist/manifest.d.ts.map +0 -1
  592. package/dist/manifest.js +0 -30
  593. package/dist/manifest.js.map +0 -1
  594. package/dist/skills-baseline.d.ts +0 -7
  595. package/dist/skills-baseline.d.ts.map +0 -1
  596. package/dist/skills-baseline.js +0 -9
  597. package/dist/skills-baseline.js.map +0 -1
  598. package/dist/skills.d.ts +0 -110
  599. package/dist/skills.d.ts.map +0 -1
  600. package/dist/skills.js +0 -429
  601. package/dist/skills.js.map +0 -1
  602. package/dist/skills.test.d.ts +0 -2
  603. package/dist/skills.test.d.ts.map +0 -1
  604. package/dist/skills.test.js +0 -416
  605. package/dist/skills.test.js.map +0 -1
  606. package/dist/sync.d.ts +0 -10
  607. package/dist/sync.d.ts.map +0 -1
  608. package/dist/sync.js +0 -39
  609. package/dist/sync.js.map +0 -1
  610. package/dist/tenant.d.ts +0 -13
  611. package/dist/tenant.d.ts.map +0 -1
  612. package/dist/tenant.js +0 -105
  613. package/dist/tenant.js.map +0 -1
  614. package/dist/tenant.test.d.ts +0 -2
  615. package/dist/tenant.test.d.ts.map +0 -1
  616. package/dist/tenant.test.js +0 -37
  617. package/dist/tenant.test.js.map +0 -1
  618. package/src/config.test.ts +0 -237
  619. package/src/detect.test.ts +0 -29
  620. package/src/detect.ts +0 -52
  621. package/src/docker.test.ts +0 -12
  622. package/src/docker.ts +0 -23
  623. package/src/health.test.ts +0 -61
  624. package/src/health.ts +0 -158
  625. package/src/index.test.ts +0 -8
  626. package/src/index.ts +0 -62
  627. package/src/installer.test.ts +0 -52
  628. package/src/installer.ts +0 -62
  629. package/src/lifecycle.test.ts +0 -23
  630. package/src/lifecycle.ts +0 -49
  631. package/src/manifest.ts +0 -42
  632. package/src/skills-baseline.ts +0 -14
  633. package/src/skills.test.ts +0 -503
  634. package/src/skills.ts +0 -512
  635. package/src/sync.ts +0 -53
  636. package/src/tenant.test.ts +0 -49
  637. package/src/tenant.ts +0 -120
@@ -0,0 +1,135 @@
1
+ // E2e test for queue + interrupt interaction.
2
+ // Validates that a user can queue a command via /queue while a slow session
3
+ // is in progress, then send a normal (non-queued) message to interrupt.
4
+ //
5
+ // Expected behavior:
6
+ // 1. Slow session is running
7
+ // 2. User queues a message via /queue (enters otto local queue)
8
+ // 3. User sends a normal message (interrupt)
9
+ // 4. Session aborts the slow task, processes the interrupt message immediately
10
+ // 5. Interrupt response appears in Discord with a ⬥ ok reply
11
+ // 6. When interrupt response completes, the queued message drains and runs
12
+ //
13
+ // Uses opencode-deterministic-provider (no real LLM calls).
14
+ // Poll timeouts: 4s max, 100ms interval. Slow matcher uses 100s delay.
15
+ import { describe, test, expect } from 'vitest';
16
+ import { setupQueueAdvancedSuite, TEST_USER_ID, } from './queue-advanced-e2e-setup.js';
17
+ import { waitForFooterMessage, waitForBotMessageContaining, waitForMessageById, } from './test-utils.js';
18
+ const TEXT_CHANNEL_ID = '200000000000001099';
19
+ const e2eTest = describe;
20
+ e2eTest('queue + interrupt drain ordering', () => {
21
+ const ctx = setupQueueAdvancedSuite({
22
+ channelId: TEXT_CHANNEL_ID,
23
+ channelName: 'qa-interrupt-drain-e2e',
24
+ dirName: 'qa-interrupt-drain-e2e',
25
+ username: 'interrupt-tester',
26
+ });
27
+ test('queued message via /queue + normal interrupt: interrupt reply should appear, then queue drains', async () => {
28
+ // 1. Establish session with a quick first message
29
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
30
+ content: 'Reply with exactly: setup-interrupt-drain',
31
+ });
32
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
33
+ timeout: 4_000,
34
+ predicate: (t) => {
35
+ return t.name === 'Reply with exactly: setup-interrupt-drain';
36
+ },
37
+ });
38
+ const th = ctx.discord.thread(thread.id);
39
+ await th.waitForBotReply({ timeout: 4_000 });
40
+ // Wait for first run to fully complete (footer) so state is clean
41
+ await waitForFooterMessage({
42
+ discord: ctx.discord,
43
+ threadId: thread.id,
44
+ timeout: 4_000,
45
+ });
46
+ // 2. Start a slow session — PLUGIN_TIMEOUT_SLEEP_MARKER has a 100s delay
47
+ // before the finish event, guaranteeing the session stays busy.
48
+ await th.user(TEST_USER_ID).sendMessage({
49
+ content: 'PLUGIN_TIMEOUT_SLEEP_MARKER',
50
+ });
51
+ // Wait for the slow matcher to start streaming (text appears before delay)
52
+ await waitForBotMessageContaining({
53
+ discord: ctx.discord,
54
+ threadId: thread.id,
55
+ userId: TEST_USER_ID,
56
+ text: 'starting sleep',
57
+ afterUserMessageIncludes: 'PLUGIN_TIMEOUT_SLEEP_MARKER',
58
+ timeout: 4_000,
59
+ });
60
+ // 3. Queue a message via /queue while the slow session is running
61
+ const { id: queueInteractionId } = await th.user(TEST_USER_ID)
62
+ .runSlashCommand({
63
+ name: 'queue',
64
+ options: [{ name: 'message', type: 3, value: 'Reply with exactly: queued-behind-slow' }],
65
+ });
66
+ const queueAck = await th.waitForInteractionAck({
67
+ interactionId: queueInteractionId,
68
+ timeout: 4_000,
69
+ });
70
+ if (!queueAck.messageId) {
71
+ throw new Error('Expected /queue response message id');
72
+ }
73
+ const queueStatusMessage = await waitForMessageById({
74
+ discord: ctx.discord,
75
+ threadId: thread.id,
76
+ messageId: queueAck.messageId,
77
+ timeout: 4_000,
78
+ });
79
+ // The /queue message should be queued (session is busy with the 100s task)
80
+ expect(queueStatusMessage.content).toContain('Queued message');
81
+ // 4. Send a normal (non-queued) message — this should interrupt the slow
82
+ // session and be processed immediately
83
+ await th.user(TEST_USER_ID).sendMessage({
84
+ content: 'Reply with exactly: interrupt-now',
85
+ });
86
+ // 5. Wait for the final state: the interrupt message should get its own
87
+ // ⬥ ok reply, then the queued message should drain and get processed.
88
+ // We wait for the queued message's footer as the final signal.
89
+ await waitForFooterMessage({
90
+ discord: ctx.discord,
91
+ threadId: thread.id,
92
+ timeout: 12_000,
93
+ afterMessageIncludes: 'queued-behind-slow',
94
+ afterAuthorId: ctx.discord.botUserId,
95
+ });
96
+ // 6. Capture the full interaction in an inline snapshot.
97
+ expect(await th.text()).toMatchInlineSnapshot(`
98
+ "--- from: user (interrupt-tester)
99
+ Reply with exactly: setup-interrupt-drain
100
+ --- from: assistant (TestBot)
101
+ ⬥ ok
102
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
103
+ --- from: user (interrupt-tester)
104
+ PLUGIN_TIMEOUT_SLEEP_MARKER
105
+ --- from: assistant (TestBot)
106
+ ⬥ starting sleep 100
107
+ Queued message (position 1)
108
+ --- from: user (interrupt-tester)
109
+ Reply with exactly: interrupt-now
110
+ --- from: assistant (TestBot)
111
+ ⬥ ok
112
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
113
+ » **interrupt-tester:** Reply with exactly: queued-behind-slow
114
+ ⬥ ok
115
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
116
+ `);
117
+ // 7. Assert the interrupt message got its own ⬥ ok reply between the
118
+ // user's interrupt message and the queue dispatch indicator.
119
+ const text = await th.text();
120
+ const lines = text.split('\n');
121
+ const interruptUserLine = lines.findIndex((line) => {
122
+ return line.includes('Reply with exactly: interrupt-now');
123
+ });
124
+ expect(interruptUserLine).toBeGreaterThan(-1);
125
+ const queueDispatchLine = lines.findIndex((line) => {
126
+ return line.includes('» **interrupt-tester:** Reply with exactly: queued-behind-slow');
127
+ });
128
+ expect(queueDispatchLine).toBeGreaterThan(-1);
129
+ const linesBetween = lines.slice(interruptUserLine + 1, queueDispatchLine);
130
+ const hasInterruptReply = linesBetween.some((line) => {
131
+ return line.includes('⬥ ok');
132
+ });
133
+ expect(hasInterruptReply).toBe(true);
134
+ }, 20_000);
135
+ });
@@ -0,0 +1,256 @@
1
+ // E2e test: queued message must drain after the user answers a pending question
2
+ // via the Discord dropdown select menu. Reproduces a bug where answering via
3
+ // select (not text) leaves queued messages stuck because the session continues
4
+ // processing after the answer and may enter another blocking state.
5
+ import { describe, test, expect } from 'vitest';
6
+ import { setupQueueAdvancedSuite, TEST_USER_ID, } from './queue-advanced-e2e-setup.js';
7
+ import { waitForBotMessageContaining, waitForFooterMessage, } from './test-utils.js';
8
+ import { pendingQuestionContexts } from './commands/ask-question.js';
9
+ const TEXT_CHANNEL_ID = '200000000000001030';
10
+ async function waitForPendingQuestion({ threadId, timeoutMs, }) {
11
+ const start = Date.now();
12
+ while (Date.now() - start < timeoutMs) {
13
+ const entry = [...pendingQuestionContexts.entries()].find(([, context]) => {
14
+ return context.thread.id === threadId;
15
+ });
16
+ if (entry) {
17
+ return { contextHash: entry[0] };
18
+ }
19
+ await new Promise((resolve) => {
20
+ setTimeout(resolve, 100);
21
+ });
22
+ }
23
+ throw new Error('Timed out waiting for pending question context');
24
+ }
25
+ async function expectNoBotMessageContaining({ discord, threadId, text, timeout, }) {
26
+ const start = Date.now();
27
+ while (Date.now() - start < timeout) {
28
+ const messages = await discord.thread(threadId).getMessages();
29
+ const match = messages.find((message) => {
30
+ return (message.author.id === discord.botUserId
31
+ && message.content.includes(text));
32
+ });
33
+ if (match) {
34
+ throw new Error(`Unexpected bot message containing ${JSON.stringify(text)} while it should still be queued`);
35
+ }
36
+ await new Promise((resolve) => {
37
+ setTimeout(resolve, 20);
38
+ });
39
+ }
40
+ }
41
+ describe('queue drain after question select answer', () => {
42
+ const ctx = setupQueueAdvancedSuite({
43
+ channelId: TEXT_CHANNEL_ID,
44
+ channelName: 'qa-question-select-drain',
45
+ dirName: 'qa-question-select-drain',
46
+ username: 'question-select-tester',
47
+ });
48
+ test('queued message drains after answering question via dropdown select', async () => {
49
+ // 1. Send a message that triggers the question tool
50
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
51
+ content: 'QUESTION_SELECT_QUEUE_MARKER',
52
+ });
53
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
54
+ timeout: 8_000,
55
+ predicate: (t) => {
56
+ return t.name === 'QUESTION_SELECT_QUEUE_MARKER';
57
+ },
58
+ });
59
+ const th = ctx.discord.thread(thread.id);
60
+ // 2. Wait for the question dropdown message to appear in Discord.
61
+ // Uses visible message wait instead of internal Map polling which
62
+ // is too timing-sensitive on CI.
63
+ const questionMessages = await waitForBotMessageContaining({
64
+ discord: ctx.discord,
65
+ threadId: thread.id,
66
+ text: 'How to proceed?',
67
+ timeout: 12_000,
68
+ });
69
+ // Get the pending question context hash from the internal map.
70
+ // By this point the question message is visible so the context must exist.
71
+ const pending = await waitForPendingQuestion({
72
+ threadId: thread.id,
73
+ timeoutMs: 8_000,
74
+ });
75
+ const questionMsg = questionMessages.find((m) => {
76
+ return m.content.includes('How to proceed?');
77
+ });
78
+ expect(questionMsg).toBeTruthy();
79
+ // 3. Queue a message while question is pending
80
+ const { id: queueInteractionId } = await th.user(TEST_USER_ID)
81
+ .runSlashCommand({
82
+ name: 'queue',
83
+ options: [{ name: 'message', type: 3, value: 'Reply with exactly: post-question-drain' }],
84
+ });
85
+ const queueAck = await th.waitForInteractionAck({
86
+ interactionId: queueInteractionId,
87
+ timeout: 8_000,
88
+ });
89
+ if (!queueAck.messageId) {
90
+ throw new Error('Expected /queue response message id');
91
+ }
92
+ // 4. The first queued item should be handed off immediately even while
93
+ // the question is still pending, so the visible dispatch indicator
94
+ // appears before the user answers the dropdown.
95
+ await waitForBotMessageContaining({
96
+ discord: ctx.discord,
97
+ threadId: thread.id,
98
+ text: '» **question-select-tester:** Reply with exactly: post-question-drain',
99
+ timeout: 8_000,
100
+ });
101
+ // 5. Answer the question via dropdown select (pick first option "Alpha")
102
+ const interaction = await th.user(TEST_USER_ID).selectMenu({
103
+ messageId: questionMsg.id,
104
+ customId: `ask_question:${pending.contextHash}:0`,
105
+ values: ['0'],
106
+ });
107
+ await th.waitForInteractionAck({
108
+ interactionId: interaction.id,
109
+ timeout: 8_000,
110
+ });
111
+ // 6. Wait for footer from the drained queued message
112
+ await waitForFooterMessage({
113
+ discord: ctx.discord,
114
+ threadId: thread.id,
115
+ timeout: 8_000,
116
+ afterMessageIncludes: '» **question-select-tester:**',
117
+ afterAuthorId: ctx.discord.botUserId,
118
+ });
119
+ const timeline = await th.text({ showInteractions: true });
120
+ expect(timeline).toMatchInlineSnapshot(`
121
+ "--- from: user (question-select-tester)
122
+ QUESTION_SELECT_QUEUE_MARKER
123
+ --- from: assistant (TestBot)
124
+ **Select action**
125
+ How to proceed?
126
+ ✓ _Alpha_
127
+ [user interaction]
128
+ » **question-select-tester:** Reply with exactly: post-question-drain
129
+ Queued message (position 1)
130
+ [user selects dropdown: 0]
131
+ ⬥ ok
132
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
133
+ `);
134
+ expect(timeline).toContain('QUESTION_SELECT_QUEUE_MARKER');
135
+ expect(timeline).toContain('How to proceed?');
136
+ expect(timeline).toContain('[user selects dropdown: 0]');
137
+ expect(timeline).toContain('» **question-select-tester:** Reply with exactly: post-question-drain');
138
+ expect(timeline).toContain('⬥ ok');
139
+ expect(timeline).toContain('*project ⋅ main ⋅');
140
+ }, 20_000);
141
+ test('only the first queued message is handed off after dropdown answer', async () => {
142
+ const marker = 'QUESTION_SELECT_QUEUE_MARKER second-test';
143
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
144
+ content: marker,
145
+ });
146
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
147
+ timeout: 8_000,
148
+ predicate: (t) => {
149
+ return t.name === marker;
150
+ },
151
+ });
152
+ const th = ctx.discord.thread(thread.id);
153
+ const questionMessages = await waitForBotMessageContaining({
154
+ discord: ctx.discord,
155
+ threadId: thread.id,
156
+ text: 'How to proceed?',
157
+ timeout: 12_000,
158
+ });
159
+ const pending = await waitForPendingQuestion({
160
+ threadId: thread.id,
161
+ timeoutMs: 8_000,
162
+ });
163
+ const questionMsg = questionMessages.find((message) => {
164
+ return message.content.includes('How to proceed?');
165
+ });
166
+ expect(questionMsg).toBeTruthy();
167
+ if (!questionMsg) {
168
+ throw new Error('Expected question message');
169
+ }
170
+ const firstQueuedPrompt = 'SLOW_ABORT_MARKER run long response';
171
+ const secondQueuedPrompt = 'Reply with exactly: post-question-second';
172
+ const { id: firstQueueInteractionId } = await th.user(TEST_USER_ID)
173
+ .runSlashCommand({
174
+ name: 'queue',
175
+ options: [{ name: 'message', type: 3, value: firstQueuedPrompt }],
176
+ });
177
+ await th.waitForInteractionAck({
178
+ interactionId: firstQueueInteractionId,
179
+ timeout: 8_000,
180
+ });
181
+ const { id: secondQueueInteractionId } = await th.user(TEST_USER_ID)
182
+ .runSlashCommand({
183
+ name: 'queue',
184
+ options: [{ name: 'message', type: 3, value: secondQueuedPrompt }],
185
+ });
186
+ await th.waitForInteractionAck({
187
+ interactionId: secondQueueInteractionId,
188
+ timeout: 8_000,
189
+ });
190
+ const interaction = await th.user(TEST_USER_ID).selectMenu({
191
+ messageId: questionMsg.id,
192
+ customId: `ask_question:${pending.contextHash}:0`,
193
+ values: ['0'],
194
+ });
195
+ await th.waitForInteractionAck({
196
+ interactionId: interaction.id,
197
+ timeout: 8_000,
198
+ });
199
+ await waitForBotMessageContaining({
200
+ discord: ctx.discord,
201
+ threadId: thread.id,
202
+ text: `» **question-select-tester:** ${firstQueuedPrompt}`,
203
+ timeout: 8_000,
204
+ });
205
+ await expectNoBotMessageContaining({
206
+ discord: ctx.discord,
207
+ threadId: thread.id,
208
+ text: `» **question-select-tester:** ${secondQueuedPrompt}`,
209
+ timeout: 200,
210
+ });
211
+ await waitForFooterMessage({
212
+ discord: ctx.discord,
213
+ threadId: thread.id,
214
+ timeout: 8_000,
215
+ afterMessageIncludes: `» **question-select-tester:** ${firstQueuedPrompt}`,
216
+ afterAuthorId: ctx.discord.botUserId,
217
+ });
218
+ await waitForBotMessageContaining({
219
+ discord: ctx.discord,
220
+ threadId: thread.id,
221
+ text: `» **question-select-tester:** ${secondQueuedPrompt}`,
222
+ timeout: 8_000,
223
+ });
224
+ await waitForFooterMessage({
225
+ discord: ctx.discord,
226
+ threadId: thread.id,
227
+ timeout: 8_000,
228
+ afterMessageIncludes: `» **question-select-tester:** ${secondQueuedPrompt}`,
229
+ afterAuthorId: ctx.discord.botUserId,
230
+ });
231
+ const timeline = await th.text({ showInteractions: true });
232
+ expect(timeline).toMatchInlineSnapshot(`
233
+ "--- from: user (question-select-tester)
234
+ QUESTION_SELECT_QUEUE_MARKER second-test
235
+ --- from: assistant (TestBot)
236
+ **Select action**
237
+ How to proceed?
238
+ ✓ _Alpha_
239
+ [user interaction]
240
+ » **question-select-tester:** SLOW_ABORT_MARKER run long response
241
+ Queued message (position 1)
242
+ [user interaction]
243
+ Queued message (position 1)
244
+ [user selects dropdown: 0]
245
+ ⬥ slow-response-started
246
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
247
+ » **question-select-tester:** Reply with exactly: post-question-second
248
+ ⬥ ok
249
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
250
+ `);
251
+ expect(timeline).toContain(`» **question-select-tester:** ${firstQueuedPrompt}`);
252
+ expect(timeline).toContain('⬥ slow-response-started');
253
+ expect(timeline).toContain(`» **question-select-tester:** ${secondQueuedPrompt}`);
254
+ expect(timeline).toContain('⬥ ok');
255
+ }, 20_000);
256
+ });
@@ -0,0 +1,52 @@
1
+ // Runtime inactivity sweeper.
2
+ // Periodically disposes thread runtimes that stayed idle past a timeout.
3
+ import { createLogger, LogPrefix } from './logger.js';
4
+ import { disposeInactiveRuntimes, } from './session-handler/thread-session-runtime.js';
5
+ const logger = createLogger(LogPrefix.SESSION);
6
+ // 24 hours — users often return the next day to click buttons/selects,
7
+ // so runtimes (and their in-memory context maps) must stay alive that long.
8
+ export const DEFAULT_RUNTIME_IDLE_MS = 24 * 60 * 60 * 1000;
9
+ export const DEFAULT_SWEEP_INTERVAL_MS = 60 * 1000;
10
+ export function startRuntimeIdleSweeper({ runtimeIdleMs = DEFAULT_RUNTIME_IDLE_MS, sweepIntervalMs = DEFAULT_SWEEP_INTERVAL_MS, } = {}) {
11
+ let stopped = false;
12
+ let sweeping = false;
13
+ let sweepPromise = null;
14
+ const sweep = async () => {
15
+ if (stopped || sweeping) {
16
+ return;
17
+ }
18
+ sweeping = true;
19
+ const currentSweepPromise = (async () => {
20
+ const nowMs = Date.now();
21
+ const disposeResult = disposeInactiveRuntimes({
22
+ idleMs: runtimeIdleMs,
23
+ nowMs,
24
+ });
25
+ if (disposeResult.disposedThreadIds.length > 0) {
26
+ logger.log(`[IDLE SWEEP] Disposed ${disposeResult.disposedThreadIds.length} inactive runtime(s) after ${runtimeIdleMs}ms`);
27
+ }
28
+ })();
29
+ sweepPromise = currentSweepPromise;
30
+ await currentSweepPromise.finally(() => {
31
+ sweeping = false;
32
+ sweepPromise = null;
33
+ });
34
+ };
35
+ const interval = setInterval(() => {
36
+ void sweep();
37
+ }, sweepIntervalMs);
38
+ void sweep();
39
+ logger.log(`[IDLE SWEEP] Started (runtimeIdleMs=${runtimeIdleMs}, intervalMs=${sweepIntervalMs})`);
40
+ return async () => {
41
+ if (stopped) {
42
+ return;
43
+ }
44
+ stopped = true;
45
+ clearInterval(interval);
46
+ if (sweepPromise) {
47
+ await sweepPromise;
48
+ sweepPromise = null;
49
+ }
50
+ logger.log('[IDLE SWEEP] Stopped');
51
+ };
52
+ }