@otto-assistant/otto 0.1.2 → 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 (638) 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/README.md +0 -142
  533. package/dist/cli.d.ts +0 -3
  534. package/dist/cli.d.ts.map +0 -1
  535. package/dist/cli.js.map +0 -1
  536. package/dist/config.d.ts +0 -39
  537. package/dist/config.d.ts.map +0 -1
  538. package/dist/config.js.map +0 -1
  539. package/dist/config.test.d.ts +0 -2
  540. package/dist/config.test.d.ts.map +0 -1
  541. package/dist/config.test.js +0 -202
  542. package/dist/config.test.js.map +0 -1
  543. package/dist/detect.d.ts +0 -9
  544. package/dist/detect.d.ts.map +0 -1
  545. package/dist/detect.js +0 -40
  546. package/dist/detect.js.map +0 -1
  547. package/dist/detect.test.d.ts +0 -2
  548. package/dist/detect.test.d.ts.map +0 -1
  549. package/dist/detect.test.js +0 -26
  550. package/dist/detect.test.js.map +0 -1
  551. package/dist/docker.d.ts +0 -7
  552. package/dist/docker.d.ts.map +0 -1
  553. package/dist/docker.js +0 -17
  554. package/dist/docker.js.map +0 -1
  555. package/dist/docker.test.d.ts +0 -2
  556. package/dist/docker.test.d.ts.map +0 -1
  557. package/dist/docker.test.js +0 -12
  558. package/dist/docker.test.js.map +0 -1
  559. package/dist/health.d.ts +0 -31
  560. package/dist/health.d.ts.map +0 -1
  561. package/dist/health.js +0 -117
  562. package/dist/health.js.map +0 -1
  563. package/dist/health.test.d.ts +0 -2
  564. package/dist/health.test.d.ts.map +0 -1
  565. package/dist/health.test.js +0 -52
  566. package/dist/health.test.js.map +0 -1
  567. package/dist/index.d.ts +0 -20
  568. package/dist/index.d.ts.map +0 -1
  569. package/dist/index.js +0 -15
  570. package/dist/index.js.map +0 -1
  571. package/dist/index.test.d.ts +0 -2
  572. package/dist/index.test.d.ts.map +0 -1
  573. package/dist/index.test.js +0 -8
  574. package/dist/index.test.js.map +0 -1
  575. package/dist/installer.d.ts +0 -10
  576. package/dist/installer.d.ts.map +0 -1
  577. package/dist/installer.js +0 -50
  578. package/dist/installer.js.map +0 -1
  579. package/dist/installer.test.d.ts +0 -2
  580. package/dist/installer.test.d.ts.map +0 -1
  581. package/dist/installer.test.js +0 -43
  582. package/dist/installer.test.js.map +0 -1
  583. package/dist/lifecycle.d.ts +0 -10
  584. package/dist/lifecycle.d.ts.map +0 -1
  585. package/dist/lifecycle.js +0 -45
  586. package/dist/lifecycle.js.map +0 -1
  587. package/dist/lifecycle.test.d.ts +0 -2
  588. package/dist/lifecycle.test.d.ts.map +0 -1
  589. package/dist/lifecycle.test.js +0 -20
  590. package/dist/lifecycle.test.js.map +0 -1
  591. package/dist/manifest.d.ts +0 -18
  592. package/dist/manifest.d.ts.map +0 -1
  593. package/dist/manifest.js +0 -30
  594. package/dist/manifest.js.map +0 -1
  595. package/dist/skills-baseline.d.ts +0 -7
  596. package/dist/skills-baseline.d.ts.map +0 -1
  597. package/dist/skills-baseline.js +0 -9
  598. package/dist/skills-baseline.js.map +0 -1
  599. package/dist/skills.d.ts +0 -110
  600. package/dist/skills.d.ts.map +0 -1
  601. package/dist/skills.js +0 -429
  602. package/dist/skills.js.map +0 -1
  603. package/dist/skills.test.d.ts +0 -2
  604. package/dist/skills.test.d.ts.map +0 -1
  605. package/dist/skills.test.js +0 -416
  606. package/dist/skills.test.js.map +0 -1
  607. package/dist/sync.d.ts +0 -10
  608. package/dist/sync.d.ts.map +0 -1
  609. package/dist/sync.js +0 -39
  610. package/dist/sync.js.map +0 -1
  611. package/dist/tenant.d.ts +0 -13
  612. package/dist/tenant.d.ts.map +0 -1
  613. package/dist/tenant.js +0 -105
  614. package/dist/tenant.js.map +0 -1
  615. package/dist/tenant.test.d.ts +0 -2
  616. package/dist/tenant.test.d.ts.map +0 -1
  617. package/dist/tenant.test.js +0 -37
  618. package/dist/tenant.test.js.map +0 -1
  619. package/src/config.test.ts +0 -237
  620. package/src/detect.test.ts +0 -29
  621. package/src/detect.ts +0 -52
  622. package/src/docker.test.ts +0 -12
  623. package/src/docker.ts +0 -23
  624. package/src/health.test.ts +0 -61
  625. package/src/health.ts +0 -158
  626. package/src/index.test.ts +0 -8
  627. package/src/index.ts +0 -62
  628. package/src/installer.test.ts +0 -52
  629. package/src/installer.ts +0 -62
  630. package/src/lifecycle.test.ts +0 -23
  631. package/src/lifecycle.ts +0 -49
  632. package/src/manifest.ts +0 -42
  633. package/src/skills-baseline.ts +0 -14
  634. package/src/skills.test.ts +0 -503
  635. package/src/skills.ts +0 -512
  636. package/src/sync.ts +0 -53
  637. package/src/tenant.test.ts +0 -49
  638. package/src/tenant.ts +0 -120
@@ -0,0 +1,68 @@
1
+ import fs from 'node:fs';
2
+ import path from 'node:path';
3
+ import util from 'node:util';
4
+ import { sanitizeSensitiveText, sanitizeUnknownValue } from './privacy-sanitizer.js';
5
+ let pluginLogFilePath = null;
6
+ export function setPluginLogFilePath(dataDir) {
7
+ pluginLogFilePath = path.join(dataDir, 'otto.log');
8
+ }
9
+ function formatArg(arg) {
10
+ if (typeof arg === 'string') {
11
+ return sanitizeSensitiveText(arg, { redactPaths: false });
12
+ }
13
+ const safeArg = sanitizeUnknownValue(arg, { redactPaths: false });
14
+ return util.inspect(safeArg, { colors: false, depth: 4 });
15
+ }
16
+ export function formatPluginErrorWithStack(error) {
17
+ if (error instanceof Error) {
18
+ return sanitizeSensitiveText(error.stack ?? `${error.name}: ${error.message}`, { redactPaths: false });
19
+ }
20
+ if (typeof error === 'string') {
21
+ return sanitizeSensitiveText(error, { redactPaths: false });
22
+ }
23
+ const safeError = sanitizeUnknownValue(error, { redactPaths: false });
24
+ return sanitizeSensitiveText(util.inspect(safeError, { colors: false, depth: 4 }), {
25
+ redactPaths: false,
26
+ });
27
+ }
28
+ function writeToFile(level, prefix, args) {
29
+ if (!pluginLogFilePath) {
30
+ return;
31
+ }
32
+ const timestamp = new Date().toISOString();
33
+ const message = `[${timestamp}] [${level}] [${prefix}] ${args.map(formatArg).join(' ')}\n`;
34
+ try {
35
+ fs.appendFileSync(pluginLogFilePath, message);
36
+ }
37
+ catch {
38
+ // Plugin logging must never break the OpenCode plugin process.
39
+ }
40
+ }
41
+ export function createPluginLogger(prefix) {
42
+ return {
43
+ log: (...args) => {
44
+ writeToFile('LOG', prefix, args);
45
+ },
46
+ info: (...args) => {
47
+ writeToFile('INFO', prefix, args);
48
+ },
49
+ warn: (...args) => {
50
+ writeToFile('WARN', prefix, args);
51
+ },
52
+ error: (...args) => {
53
+ writeToFile('ERROR', prefix, args);
54
+ },
55
+ debug: (...args) => {
56
+ writeToFile('DEBUG', prefix, args);
57
+ },
58
+ };
59
+ }
60
+ // Append a session ID marker at the end of a toast message so the bot-side
61
+ // handleTuiToast can route the toast to the correct Discord thread.
62
+ // Without this marker the toast is silently dropped.
63
+ export function appendToastSessionMarker({ message, sessionId, }) {
64
+ if (!sessionId) {
65
+ return message;
66
+ }
67
+ return `${message} ${sessionId}`;
68
+ }
@@ -0,0 +1,105 @@
1
+ // Sensitive data redaction helpers for logs and telemetry payloads.
2
+ // Redacts common secrets, identifiers, emails, and can optionally redact paths.
3
+ const CORE_SENSITIVE_REPLACEMENTS = [
4
+ {
5
+ pattern: /\bBearer\s+[A-Za-z0-9._-]{10,}\b/gi,
6
+ replacement: 'Bearer [REDACTED]',
7
+ },
8
+ {
9
+ pattern: /\bsk-[A-Za-z0-9]{16,}\b/g,
10
+ replacement: '[REDACTED_OPENAI_KEY]',
11
+ },
12
+ {
13
+ pattern: /\bAIza[0-9A-Za-z_-]{20,}\b/g,
14
+ replacement: '[REDACTED_GOOGLE_KEY]',
15
+ },
16
+ {
17
+ pattern: /\bgh[pousr]_[A-Za-z0-9]{20,}\b/g,
18
+ replacement: '[REDACTED_GITHUB_TOKEN]',
19
+ },
20
+ {
21
+ pattern: /([?&](?:token|api[_-]?key|key|secret|password|authorization)=)[^&\s]+/gi,
22
+ replacement: '$1[REDACTED]',
23
+ },
24
+ {
25
+ pattern: /(\b(?:token|api[_-]?key|secret|password|authorization)\b\s*[:=]\s*")([^"]+)(")/gi,
26
+ replacement: '$1[REDACTED]$3',
27
+ },
28
+ {
29
+ pattern: /(\b(?:token|api[_-]?key|secret|password|authorization)\b\s*[:=]\s*)([^\s,;]+)/gi,
30
+ replacement: '$1[REDACTED]',
31
+ },
32
+ ];
33
+ const PATH_REPLACEMENTS = [
34
+ {
35
+ pattern: /\/(?:Users|home)\/[^/\s]+\/[^\s'"`)]*/g,
36
+ replacement: '[REDACTED_PATH]',
37
+ },
38
+ {
39
+ pattern: /[A-Za-z]:\\[^\s'"`)]*/g,
40
+ replacement: '[REDACTED_PATH]',
41
+ },
42
+ ];
43
+ export function sanitizeSensitiveText(value, { redactPaths = false } = {}) {
44
+ const replacements = redactPaths
45
+ ? [...CORE_SENSITIVE_REPLACEMENTS, ...PATH_REPLACEMENTS]
46
+ : CORE_SENSITIVE_REPLACEMENTS;
47
+ return replacements.reduce((current, entry) => {
48
+ return current.replace(entry.pattern, entry.replacement);
49
+ }, value);
50
+ }
51
+ export function sanitizeUnknownValue(value, { depth = 0, seen = new WeakSet(), redactPaths = false, } = {}) {
52
+ if (depth > 8) {
53
+ return '[REDACTED_DEPTH_LIMIT]';
54
+ }
55
+ if (typeof value === 'string') {
56
+ return sanitizeSensitiveText(value, { redactPaths });
57
+ }
58
+ if (typeof value === 'number' ||
59
+ typeof value === 'boolean' ||
60
+ value === null ||
61
+ value === undefined) {
62
+ return value;
63
+ }
64
+ if (value instanceof Date) {
65
+ return value.toISOString();
66
+ }
67
+ if (value instanceof Error) {
68
+ const sanitizedStack = value.stack
69
+ ? sanitizeSensitiveText(value.stack, { redactPaths })
70
+ : undefined;
71
+ return {
72
+ name: value.name,
73
+ message: sanitizeSensitiveText(value.message, { redactPaths }),
74
+ stack: sanitizedStack,
75
+ cause: sanitizeUnknownValue(value.cause, {
76
+ depth: depth + 1,
77
+ seen,
78
+ redactPaths,
79
+ }),
80
+ };
81
+ }
82
+ if (Array.isArray(value)) {
83
+ return value.map((item) => {
84
+ return sanitizeUnknownValue(item, { depth: depth + 1, seen, redactPaths });
85
+ });
86
+ }
87
+ if (typeof value === 'object') {
88
+ if (seen.has(value)) {
89
+ return '[REDACTED_CIRCULAR]';
90
+ }
91
+ seen.add(value);
92
+ const sanitizedEntries = Object.entries(value).map(([key, entryValue]) => {
93
+ return [
94
+ key,
95
+ sanitizeUnknownValue(entryValue, {
96
+ depth: depth + 1,
97
+ seen,
98
+ redactPaths,
99
+ }),
100
+ ];
101
+ });
102
+ return Object.fromEntries(sanitizedEntries);
103
+ }
104
+ return sanitizeSensitiveText(String(value), { redactPaths });
105
+ }
@@ -0,0 +1,293 @@
1
+ // E2e tests for abort, model-switch, and retry scenarios.
2
+ // Split from thread-queue-advanced.e2e.test.ts for parallelization.
3
+ import { describe, test, expect } from 'vitest';
4
+ import { setupQueueAdvancedSuite, TEST_USER_ID, } from './queue-advanced-e2e-setup.js';
5
+ import { getRuntime, } from './session-handler/thread-session-runtime.js';
6
+ import { getThreadState } from './session-handler/thread-runtime-state.js';
7
+ import { setSessionModel } from './database.js';
8
+ import { waitForFooterMessage, waitForBotMessageContaining, waitForBotReplyAfterUserMessage, } from './test-utils.js';
9
+ const TEXT_CHANNEL_ID = '200000000000001003';
10
+ const e2eTest = describe;
11
+ e2eTest('queue advanced: abort and retry', () => {
12
+ const ctx = setupQueueAdvancedSuite({
13
+ channelId: TEXT_CHANNEL_ID,
14
+ channelName: 'qa-abort-e2e',
15
+ dirName: 'qa-abort-e2e',
16
+ username: 'queue-advanced-tester',
17
+ });
18
+ test('slow tool call (sleep) gets aborted by explicit abort, then queue continues', async () => {
19
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
20
+ content: 'Reply with exactly: oscar',
21
+ });
22
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
23
+ timeout: 4_000,
24
+ predicate: (t) => {
25
+ return t.name === 'Reply with exactly: oscar';
26
+ },
27
+ });
28
+ const th = ctx.discord.thread(thread.id);
29
+ const firstReply = await th.waitForBotReply({ timeout: 4_000 });
30
+ expect(firstReply.content.trim().length).toBeGreaterThan(0);
31
+ // Wait for the first completion footer so it lands in a deterministic position
32
+ await waitForFooterMessage({
33
+ discord: ctx.discord,
34
+ threadId: thread.id,
35
+ timeout: 4_000,
36
+ });
37
+ const before = await th.getMessages();
38
+ const beforeBotCount = before.filter((m) => {
39
+ return m.author.id === ctx.discord.botUserId;
40
+ }).length;
41
+ await th.user(TEST_USER_ID).sendMessage({
42
+ content: 'PLUGIN_TIMEOUT_SLEEP_MARKER',
43
+ });
44
+ // The matcher emits "starting sleep 100" text before the long delay.
45
+ // Wait for it to land in Discord BEFORE aborting so the message is in a
46
+ // deterministic position and the abort produces no further stray messages.
47
+ await waitForBotMessageContaining({
48
+ discord: ctx.discord,
49
+ threadId: thread.id,
50
+ userId: TEST_USER_ID,
51
+ text: 'starting sleep',
52
+ afterUserMessageIncludes: 'PLUGIN_TIMEOUT_SLEEP_MARKER',
53
+ timeout: 4_000,
54
+ });
55
+ const runtime = getRuntime(thread.id);
56
+ expect(runtime).toBeDefined();
57
+ if (!runtime) {
58
+ throw new Error('Expected runtime to exist for explicit-abort test');
59
+ }
60
+ runtime.abortActiveRun('test-explicit-abort');
61
+ await th.user(TEST_USER_ID).sendMessage({
62
+ content: 'Reply with exactly: papa',
63
+ });
64
+ const after = await waitForBotReplyAfterUserMessage({
65
+ discord: ctx.discord,
66
+ threadId: thread.id,
67
+ userId: TEST_USER_ID,
68
+ userMessageIncludes: 'papa',
69
+ timeout: 8_000,
70
+ });
71
+ const afterBotMessages = after.filter((m) => {
72
+ return m.author.id === ctx.discord.botUserId;
73
+ });
74
+ await waitForFooterMessage({
75
+ discord: ctx.discord,
76
+ threadId: thread.id,
77
+ timeout: 8_000,
78
+ afterMessageIncludes: 'papa',
79
+ afterAuthorId: TEST_USER_ID,
80
+ });
81
+ // Assert ordering invariants instead of exact snapshot — the papa reply
82
+ // and footer can interleave non-deterministically.
83
+ const timeline = await th.text();
84
+ expect(timeline).toContain('Reply with exactly: oscar');
85
+ expect(timeline).toContain('PLUGIN_TIMEOUT_SLEEP_MARKER');
86
+ expect(timeline).toContain('⬥ starting sleep 100');
87
+ expect(timeline).toContain('Reply with exactly: papa');
88
+ expect(timeline).toContain('*project ⋅ main ⋅');
89
+ // oscar comes before the sleep marker, sleep before papa
90
+ const oscarIdx = timeline.indexOf('oscar');
91
+ const sleepIdx = timeline.indexOf('PLUGIN_TIMEOUT_SLEEP_MARKER');
92
+ const papaIdx = timeline.indexOf('papa');
93
+ expect(oscarIdx).toBeLessThan(sleepIdx);
94
+ expect(sleepIdx).toBeLessThan(papaIdx);
95
+ expect(afterBotMessages.length).toBeGreaterThanOrEqual(beforeBotCount + 1);
96
+ const sleepToolIndex = after.findIndex((m) => {
97
+ return (m.author.id === TEST_USER_ID &&
98
+ m.content.includes('PLUGIN_TIMEOUT_SLEEP_MARKER'));
99
+ });
100
+ expect(sleepToolIndex).toBeGreaterThan(-1);
101
+ const userPapaIndex = after.findIndex((m) => {
102
+ return m.author.id === TEST_USER_ID && m.content.includes('papa');
103
+ });
104
+ expect(userPapaIndex).toBeGreaterThan(-1);
105
+ expect(sleepToolIndex).toBeLessThan(userPapaIndex);
106
+ const lastBotIndex = after.findLastIndex((m) => {
107
+ return m.author.id === ctx.discord.botUserId;
108
+ });
109
+ expect(userPapaIndex).toBeLessThan(lastBotIndex);
110
+ }, 12_000);
111
+ test('explicit abort emits MessageAbortedError and does not emit footer', async () => {
112
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
113
+ content: 'Reply with exactly: abort-no-footer-setup',
114
+ });
115
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
116
+ timeout: 4_000,
117
+ predicate: (t) => {
118
+ return t.name === 'Reply with exactly: abort-no-footer-setup';
119
+ },
120
+ });
121
+ const th = ctx.discord.thread(thread.id);
122
+ await th.waitForBotReply({ timeout: 4_000 });
123
+ await waitForBotMessageContaining({
124
+ discord: ctx.discord,
125
+ threadId: thread.id,
126
+ userId: TEST_USER_ID,
127
+ text: '⋅',
128
+ timeout: 4_000,
129
+ });
130
+ await th.user(TEST_USER_ID).sendMessage({
131
+ content: 'SLOW_ABORT_MARKER run long response',
132
+ });
133
+ const runtime = getRuntime(thread.id);
134
+ expect(runtime).toBeDefined();
135
+ if (!runtime) {
136
+ throw new Error('Expected runtime to exist for abort no-footer test');
137
+ }
138
+ const beforeAbortMessages = await th.getMessages();
139
+ const baselineCount = beforeAbortMessages.length;
140
+ runtime.abortActiveRun('test-no-footer-on-abort');
141
+ for (let i = 0; i < 10; i++) {
142
+ await new Promise((resolve) => {
143
+ setTimeout(resolve, 20);
144
+ });
145
+ const msgs = await th.getMessages();
146
+ const newMsgs = msgs.slice(baselineCount);
147
+ const hasFooter = newMsgs.some((m) => {
148
+ return m.author.id === ctx.discord.botUserId
149
+ && m.content.startsWith('*')
150
+ && m.content.includes('⋅');
151
+ });
152
+ expect(hasFooter).toBe(false);
153
+ }
154
+ expect(await th.text()).toMatchInlineSnapshot(`
155
+ "--- from: user (queue-advanced-tester)
156
+ Reply with exactly: abort-no-footer-setup
157
+ --- from: assistant (TestBot)
158
+ ⬥ ok
159
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
160
+ --- from: user (queue-advanced-tester)
161
+ SLOW_ABORT_MARKER run long response"
162
+ `);
163
+ }, 10_000);
164
+ test.skip('explicit abort stale-idle window: follow-up prompt still gets assistant text', async () => {
165
+ const setupPrompt = 'Reply with exactly: race-setup-1';
166
+ const raceFinalPrompt = 'Reply with exactly: race-final-1';
167
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
168
+ content: setupPrompt,
169
+ });
170
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
171
+ timeout: 4_000,
172
+ predicate: (t) => {
173
+ return t.name === setupPrompt;
174
+ },
175
+ });
176
+ const th = ctx.discord.thread(thread.id);
177
+ const setupReply = await th.waitForBotReply({ timeout: 4_000 });
178
+ expect(setupReply.content.trim().length).toBeGreaterThan(0);
179
+ await th.user(TEST_USER_ID).sendMessage({
180
+ content: 'SLOW_ABORT_MARKER run long response',
181
+ });
182
+ const runtime = getRuntime(thread.id);
183
+ expect(runtime).toBeDefined();
184
+ if (!runtime) {
185
+ throw new Error('Expected runtime to exist for race abort scenario');
186
+ }
187
+ runtime.abortActiveRun('test-race-abort');
188
+ await th.user(TEST_USER_ID).sendMessage({
189
+ content: raceFinalPrompt,
190
+ });
191
+ await waitForBotReplyAfterUserMessage({
192
+ discord: ctx.discord,
193
+ threadId: thread.id,
194
+ userId: TEST_USER_ID,
195
+ userMessageIncludes: raceFinalPrompt,
196
+ timeout: 4_000,
197
+ });
198
+ }, 8_000);
199
+ test('model switch mid-session aborts and restarts from same session history', async () => {
200
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
201
+ content: 'Reply with exactly: retry-setup',
202
+ });
203
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
204
+ timeout: 4_000,
205
+ predicate: (t) => {
206
+ return t.name === 'Reply with exactly: retry-setup';
207
+ },
208
+ });
209
+ const th = ctx.discord.thread(thread.id);
210
+ const firstReply = await th.waitForBotReply({ timeout: 4_000 });
211
+ expect(firstReply.content.trim().length).toBeGreaterThan(0);
212
+ await th.user(TEST_USER_ID).sendMessage({
213
+ content: 'PLUGIN_TIMEOUT_SLEEP_MARKER',
214
+ });
215
+ await waitForBotMessageContaining({
216
+ discord: ctx.discord,
217
+ threadId: thread.id,
218
+ userId: TEST_USER_ID,
219
+ text: 'starting sleep',
220
+ afterUserMessageIncludes: 'PLUGIN_TIMEOUT_SLEEP_MARKER',
221
+ timeout: 4_000,
222
+ });
223
+ const sessionId = getThreadState(thread.id)?.sessionId;
224
+ expect(sessionId).toBeDefined();
225
+ if (!sessionId) {
226
+ throw new Error('Expected active session id for model switch test');
227
+ }
228
+ await setSessionModel({
229
+ sessionId,
230
+ modelId: 'deterministic-provider/deterministic-v3',
231
+ variant: null,
232
+ });
233
+ const runtime = getRuntime(thread.id);
234
+ expect(runtime).toBeDefined();
235
+ if (!runtime) {
236
+ throw new Error('Expected runtime to exist for model switch test');
237
+ }
238
+ const retried = await runtime.retryLastUserPrompt();
239
+ expect(retried).toBe(true);
240
+ await th.user(TEST_USER_ID).sendMessage({
241
+ content: 'Reply with exactly: model-switch-followup',
242
+ });
243
+ await waitForBotReplyAfterUserMessage({
244
+ discord: ctx.discord,
245
+ threadId: thread.id,
246
+ userId: TEST_USER_ID,
247
+ userMessageIncludes: 'model-switch-followup',
248
+ timeout: 4_000,
249
+ });
250
+ // Wait for potential footer to arrive (race between step-finish interrupt
251
+ // and model switch settling means footer may or may not appear).
252
+ await new Promise((resolve) => {
253
+ setTimeout(resolve, 200);
254
+ });
255
+ const text = await th.text();
256
+ // The follow-up reply ("ok") must be present with deterministic-v3
257
+ expect(text).toContain('Reply with exactly: model-switch-followup');
258
+ expect(text).toContain('⬥ ok');
259
+ // The old sleep text should be visible from the first turn
260
+ expect(text).toContain('starting sleep 100');
261
+ }, 10_000);
262
+ test('abortActiveRun settles correctly during long-running request', async () => {
263
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
264
+ content: 'Reply with exactly: force-abort-setup',
265
+ });
266
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
267
+ timeout: 4_000,
268
+ predicate: (t) => {
269
+ return t.name === 'Reply with exactly: force-abort-setup';
270
+ },
271
+ });
272
+ const th = ctx.discord.thread(thread.id);
273
+ const setupReply = await th.waitForBotReply({ timeout: 4_000 });
274
+ expect(setupReply.content.trim().length).toBeGreaterThan(0);
275
+ await th.user(TEST_USER_ID).sendMessage({
276
+ content: 'SLOW_ABORT_MARKER run long response',
277
+ });
278
+ const runtime = getRuntime(thread.id);
279
+ expect(runtime).toBeDefined();
280
+ if (!runtime) {
281
+ throw new Error('Expected runtime to exist for forced-abort test');
282
+ }
283
+ runtime.abortActiveRun('force-abort-test');
284
+ expect(await th.text()).toMatchInlineSnapshot(`
285
+ "--- from: user (queue-advanced-tester)
286
+ Reply with exactly: force-abort-setup
287
+ --- from: assistant (TestBot)
288
+ ⬥ ok
289
+ --- from: user (queue-advanced-tester)
290
+ SLOW_ABORT_MARKER run long response"
291
+ `);
292
+ }, 10_000);
293
+ });
@@ -0,0 +1,206 @@
1
+ // E2e regression test for action button click continuation in thread sessions.
2
+ // Reproduces the bug where button click interaction acks but the session does not continue.
3
+ import { describe, test, expect } from 'vitest';
4
+ import { setupQueueAdvancedSuite, TEST_USER_ID, } from './queue-advanced-e2e-setup.js';
5
+ import { waitForBotMessageContaining, waitForFooterMessage, } from './test-utils.js';
6
+ import { getThreadSession } from './database.js';
7
+ import { pendingActionButtonContexts, showActionButtons, } from './commands/action-buttons.js';
8
+ const TEXT_CHANNEL_ID = '200000000000001006';
9
+ async function waitForPendingActionButtons({ threadId, timeoutMs, }) {
10
+ const start = Date.now();
11
+ while (Date.now() - start < timeoutMs) {
12
+ const entry = [...pendingActionButtonContexts.entries()].find(([, context]) => {
13
+ return context.thread.id === threadId && Boolean(context.messageId);
14
+ });
15
+ if (entry) {
16
+ const [contextHash, context] = entry;
17
+ if (context.messageId) {
18
+ return { contextHash, messageId: context.messageId };
19
+ }
20
+ }
21
+ await new Promise((resolve) => {
22
+ setTimeout(resolve, 100);
23
+ });
24
+ }
25
+ throw new Error('Timed out waiting for pending action buttons context');
26
+ }
27
+ async function waitForNoPendingActionButtons({ threadId, timeoutMs, }) {
28
+ const start = Date.now();
29
+ while (Date.now() - start < timeoutMs) {
30
+ const stillPending = [...pendingActionButtonContexts.values()].some((context) => {
31
+ return context.thread.id === threadId;
32
+ });
33
+ if (!stillPending) {
34
+ return;
35
+ }
36
+ await new Promise((resolve) => {
37
+ setTimeout(resolve, 100);
38
+ });
39
+ }
40
+ throw new Error('Timed out waiting for action buttons cleanup');
41
+ }
42
+ describe('queue advanced: action buttons', () => {
43
+ const ctx = setupQueueAdvancedSuite({
44
+ channelId: TEXT_CHANNEL_ID,
45
+ channelName: 'qa-action-buttons-e2e',
46
+ dirName: 'qa-action-buttons-e2e',
47
+ username: 'queue-action-tester',
48
+ });
49
+ test('button click should continue the session with a follow-up assistant reply', async () => {
50
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
51
+ content: 'Reply with exactly: action-button-setup',
52
+ });
53
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
54
+ timeout: 4_000,
55
+ predicate: (t) => {
56
+ return t.name === 'Reply with exactly: action-button-setup';
57
+ },
58
+ });
59
+ const th = ctx.discord.thread(thread.id);
60
+ await waitForBotMessageContaining({
61
+ discord: ctx.discord,
62
+ threadId: thread.id,
63
+ userId: TEST_USER_ID,
64
+ text: 'ok',
65
+ timeout: 4_000,
66
+ });
67
+ await waitForFooterMessage({
68
+ discord: ctx.discord,
69
+ threadId: thread.id,
70
+ timeout: 4_000,
71
+ afterMessageIncludes: 'ok',
72
+ afterAuthorId: ctx.discord.botUserId,
73
+ });
74
+ const currentSessionId = await getThreadSession(thread.id);
75
+ if (!currentSessionId) {
76
+ throw new Error('Expected thread session id before showing action buttons');
77
+ }
78
+ const channel = await ctx.botClient.channels.fetch(thread.id);
79
+ if (!channel || !channel.isThread()) {
80
+ throw new Error('Expected Discord thread channel for action button test');
81
+ }
82
+ await showActionButtons({
83
+ thread: channel,
84
+ sessionId: currentSessionId,
85
+ directory: ctx.directories.projectDirectory,
86
+ buttons: [{ label: 'Continue action-buttons flow', color: 'green' }],
87
+ });
88
+ const action = await waitForPendingActionButtons({
89
+ threadId: thread.id,
90
+ timeoutMs: 12_000,
91
+ });
92
+ await waitForBotMessageContaining({
93
+ discord: ctx.discord,
94
+ threadId: thread.id,
95
+ userId: TEST_USER_ID,
96
+ text: 'Action Required',
97
+ timeout: 12_000,
98
+ });
99
+ const interaction = await th.user(TEST_USER_ID).clickButton({
100
+ messageId: action.messageId,
101
+ customId: `action_button:${action.contextHash}:0`,
102
+ });
103
+ await th.waitForInteractionAck({
104
+ interactionId: interaction.id,
105
+ timeout: 4_000,
106
+ });
107
+ await waitForBotMessageContaining({
108
+ discord: ctx.discord,
109
+ threadId: thread.id,
110
+ text: 'action-buttons-click-continued',
111
+ timeout: 12_000,
112
+ });
113
+ await waitForFooterMessage({
114
+ discord: ctx.discord,
115
+ threadId: thread.id,
116
+ timeout: 12_000,
117
+ afterMessageIncludes: 'action-buttons-click-continued',
118
+ afterAuthorId: ctx.discord.botUserId,
119
+ });
120
+ const timeline = await th.text({ showInteractions: true });
121
+ expect(timeline).toMatchInlineSnapshot(`
122
+ "--- from: user (queue-action-tester)
123
+ Reply with exactly: action-button-setup
124
+ --- from: assistant (TestBot)
125
+ ⬥ ok
126
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
127
+ **Action Required**
128
+ _Selected: Continue action-buttons flow_
129
+ [user clicks button]
130
+ ⬥ action-buttons-click-continued
131
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
132
+ `);
133
+ expect(timeline).toContain('action-buttons-click-continued');
134
+ }, 20_000);
135
+ test('manual thread message dismisses pending action buttons', async () => {
136
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
137
+ content: 'Reply with exactly: action-button-dismiss-setup',
138
+ });
139
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
140
+ timeout: 4_000,
141
+ predicate: (t) => {
142
+ return t.name === 'Reply with exactly: action-button-dismiss-setup';
143
+ },
144
+ });
145
+ const th = ctx.discord.thread(thread.id);
146
+ await waitForBotMessageContaining({
147
+ discord: ctx.discord,
148
+ threadId: thread.id,
149
+ userId: TEST_USER_ID,
150
+ text: 'ok',
151
+ timeout: 4_000,
152
+ });
153
+ await waitForFooterMessage({
154
+ discord: ctx.discord,
155
+ threadId: thread.id,
156
+ timeout: 4_000,
157
+ afterMessageIncludes: 'ok',
158
+ afterAuthorId: ctx.discord.botUserId,
159
+ });
160
+ const currentSessionId = await getThreadSession(thread.id);
161
+ if (!currentSessionId) {
162
+ throw new Error('Expected thread session id before showing action buttons');
163
+ }
164
+ const channel = await ctx.botClient.channels.fetch(thread.id);
165
+ if (!channel || !channel.isThread()) {
166
+ throw new Error('Expected Discord thread channel for action button test');
167
+ }
168
+ await showActionButtons({
169
+ thread: channel,
170
+ sessionId: currentSessionId,
171
+ directory: ctx.directories.projectDirectory,
172
+ buttons: [{ label: 'Dismiss me', color: 'white' }],
173
+ });
174
+ await waitForPendingActionButtons({
175
+ threadId: thread.id,
176
+ timeoutMs: 4_000,
177
+ });
178
+ await th.user(TEST_USER_ID).sendMessage({
179
+ content: 'Reply with exactly: post-dismiss-user-message',
180
+ });
181
+ await waitForBotMessageContaining({
182
+ discord: ctx.discord,
183
+ threadId: thread.id,
184
+ text: 'Buttons dismissed.',
185
+ timeout: 4_000,
186
+ });
187
+ await waitForNoPendingActionButtons({
188
+ threadId: thread.id,
189
+ timeoutMs: 4_000,
190
+ });
191
+ const timeline = await th.text({ showInteractions: true });
192
+ expect(timeline).toMatchInlineSnapshot(`
193
+ "--- from: user (queue-action-tester)
194
+ Reply with exactly: action-button-dismiss-setup
195
+ --- from: assistant (TestBot)
196
+ ⬥ ok
197
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
198
+ **Action Required**
199
+ _Buttons dismissed._
200
+ --- from: user (queue-action-tester)
201
+ Reply with exactly: post-dismiss-user-message"
202
+ `);
203
+ expect(timeline).toContain('_Buttons dismissed._');
204
+ expect(timeline).toContain('post-dismiss-user-message');
205
+ }, 20_000);
206
+ });