@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,481 @@
1
+ // E2e tests for footer emission in advanced queue 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 { waitForFooterMessage, waitForBotMessageContaining, waitForBotReplyAfterUserMessage, } from './test-utils.js';
6
+ const TEXT_CHANNEL_ID = '200000000000001001';
7
+ const e2eTest = describe;
8
+ e2eTest('queue advanced: footer emission', () => {
9
+ const ctx = setupQueueAdvancedSuite({
10
+ channelId: TEXT_CHANNEL_ID,
11
+ channelName: 'qa-footer-e2e',
12
+ dirName: 'qa-footer-e2e',
13
+ username: 'queue-advanced-tester',
14
+ });
15
+ test('normal completion emits footer after bot reply', async () => {
16
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
17
+ content: 'Reply with exactly: footer-check',
18
+ });
19
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
20
+ timeout: 4_000,
21
+ predicate: (t) => {
22
+ return t.name === 'Reply with exactly: footer-check';
23
+ },
24
+ });
25
+ const th = ctx.discord.thread(thread.id);
26
+ await th.waitForBotReply({ timeout: 4_000 });
27
+ const footerMessages = await waitForFooterMessage({
28
+ discord: ctx.discord,
29
+ threadId: thread.id,
30
+ timeout: 4_000,
31
+ });
32
+ expect(await th.text()).toMatchInlineSnapshot(`
33
+ "--- from: user (queue-advanced-tester)
34
+ Reply with exactly: footer-check
35
+ --- from: assistant (TestBot)
36
+ ⬥ ok
37
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
38
+ `);
39
+ const foundFooter = footerMessages.some((m) => {
40
+ return m.author.id === ctx.discord.botUserId
41
+ && m.content.startsWith('*')
42
+ && m.content.includes('⋅');
43
+ });
44
+ expect(foundFooter).toBe(true);
45
+ }, 8_000);
46
+ test('footer appears after second message in same session', async () => {
47
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
48
+ content: 'Reply with exactly: footer-multi-setup',
49
+ });
50
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
51
+ timeout: 4_000,
52
+ predicate: (t) => {
53
+ return t.name === 'Reply with exactly: footer-multi-setup';
54
+ },
55
+ });
56
+ const th = ctx.discord.thread(thread.id);
57
+ await th.waitForBotReply({ timeout: 4_000 });
58
+ await waitForBotMessageContaining({
59
+ discord: ctx.discord,
60
+ threadId: thread.id,
61
+ userId: TEST_USER_ID,
62
+ text: '⋅',
63
+ timeout: 4_000,
64
+ });
65
+ await th.user(TEST_USER_ID).sendMessage({
66
+ content: 'Reply with exactly: footer-multi-second',
67
+ });
68
+ await waitForBotReplyAfterUserMessage({
69
+ discord: ctx.discord,
70
+ threadId: thread.id,
71
+ userId: TEST_USER_ID,
72
+ userMessageIncludes: 'footer-multi-second',
73
+ timeout: 4_000,
74
+ });
75
+ await waitForFooterMessage({
76
+ discord: ctx.discord,
77
+ threadId: thread.id,
78
+ timeout: 4_000,
79
+ afterMessageIncludes: 'footer-multi-second',
80
+ afterAuthorId: TEST_USER_ID,
81
+ });
82
+ const msgs = await th.getMessages();
83
+ const footerCount = msgs.filter((m) => {
84
+ return m.author.id === ctx.discord.botUserId
85
+ && m.content.startsWith('*')
86
+ && m.content.includes('⋅');
87
+ }).length;
88
+ expect(await th.text()).toMatchInlineSnapshot(`
89
+ "--- from: user (queue-advanced-tester)
90
+ Reply with exactly: footer-multi-setup
91
+ --- from: assistant (TestBot)
92
+ ⬥ ok
93
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
94
+ --- from: user (queue-advanced-tester)
95
+ Reply with exactly: footer-multi-second
96
+ --- from: assistant (TestBot)
97
+ ⬥ ok
98
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
99
+ `);
100
+ if (footerCount >= 2) {
101
+ expect(footerCount).toBeGreaterThanOrEqual(2);
102
+ return;
103
+ }
104
+ const pollDeadline = Date.now() + 4_000;
105
+ let found = false;
106
+ while (Date.now() < pollDeadline) {
107
+ await new Promise((resolve) => {
108
+ setTimeout(resolve, 100);
109
+ });
110
+ const latestMsgs = await th.getMessages();
111
+ const count = latestMsgs.filter((m) => {
112
+ return m.author.id === ctx.discord.botUserId
113
+ && m.content.startsWith('*')
114
+ && m.content.includes('⋅');
115
+ }).length;
116
+ if (count >= 2) {
117
+ found = true;
118
+ break;
119
+ }
120
+ }
121
+ expect(found).toBe(true);
122
+ }, 12_000);
123
+ test('interrupted run has no footer, completed follow-up has footer', async () => {
124
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
125
+ content: 'Reply with exactly: interrupt-footer-setup',
126
+ });
127
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
128
+ timeout: 4_000,
129
+ predicate: (t) => {
130
+ return t.name === 'Reply with exactly: interrupt-footer-setup';
131
+ },
132
+ });
133
+ const th = ctx.discord.thread(thread.id);
134
+ await th.waitForBotReply({ timeout: 4_000 });
135
+ await waitForBotMessageContaining({
136
+ discord: ctx.discord,
137
+ threadId: thread.id,
138
+ userId: TEST_USER_ID,
139
+ text: '⋅',
140
+ timeout: 4_000,
141
+ });
142
+ const beforeInterruptMsgs = await th.getMessages();
143
+ const baselineCount = beforeInterruptMsgs.length;
144
+ await th.user(TEST_USER_ID).sendMessage({
145
+ content: 'PLUGIN_TIMEOUT_SLEEP_MARKER',
146
+ });
147
+ await waitForBotMessageContaining({
148
+ discord: ctx.discord,
149
+ threadId: thread.id,
150
+ userId: TEST_USER_ID,
151
+ text: 'starting sleep 100',
152
+ afterUserMessageIncludes: 'PLUGIN_TIMEOUT_SLEEP_MARKER',
153
+ timeout: 4_000,
154
+ });
155
+ await th.user(TEST_USER_ID).sendMessage({
156
+ content: 'Reply with exactly: interrupt-footer-followup',
157
+ });
158
+ const messages = await waitForBotMessageContaining({
159
+ discord: ctx.discord,
160
+ threadId: thread.id,
161
+ userId: TEST_USER_ID,
162
+ text: 'ok',
163
+ afterUserMessageIncludes: 'interrupt-footer-followup',
164
+ timeout: 12_000,
165
+ });
166
+ const followupUserIdx = messages.findIndex((m, idx) => {
167
+ return idx >= baselineCount
168
+ && m.author.id === TEST_USER_ID
169
+ && m.content.includes('interrupt-footer-followup');
170
+ });
171
+ const okReplyIdx = messages.findIndex((m, idx) => {
172
+ if (idx <= followupUserIdx) {
173
+ return false;
174
+ }
175
+ return m.author.id === ctx.discord.botUserId && m.content.includes('ok');
176
+ });
177
+ await waitForFooterMessage({
178
+ discord: ctx.discord,
179
+ threadId: thread.id,
180
+ timeout: 12_000,
181
+ afterMessageIncludes: 'interrupt-footer-followup',
182
+ afterAuthorId: TEST_USER_ID,
183
+ });
184
+ expect(await th.text()).toMatchInlineSnapshot(`
185
+ "--- from: user (queue-advanced-tester)
186
+ Reply with exactly: interrupt-footer-setup
187
+ --- from: assistant (TestBot)
188
+ ⬥ ok
189
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
190
+ --- from: user (queue-advanced-tester)
191
+ PLUGIN_TIMEOUT_SLEEP_MARKER
192
+ --- from: assistant (TestBot)
193
+ ⬥ starting sleep 100
194
+ --- from: user (queue-advanced-tester)
195
+ Reply with exactly: interrupt-footer-followup
196
+ --- from: assistant (TestBot)
197
+ ⬥ ok
198
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
199
+ `);
200
+ expect(followupUserIdx).toBeGreaterThanOrEqual(0);
201
+ expect(okReplyIdx).toBeGreaterThan(followupUserIdx);
202
+ const footerBetween = messages.some((m, idx) => {
203
+ if (idx < baselineCount || idx >= okReplyIdx) {
204
+ return false;
205
+ }
206
+ return m.author.id === ctx.discord.botUserId
207
+ && m.content.startsWith('*')
208
+ && m.content.includes('⋅');
209
+ });
210
+ expect(footerBetween).toBe(false);
211
+ }, 15_000);
212
+ test('plugin timeout interrupt aborts slow sleep and avoids intermediate footer', async () => {
213
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
214
+ content: 'Reply with exactly: plugin-timeout-setup',
215
+ });
216
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
217
+ timeout: 4_000,
218
+ predicate: (t) => {
219
+ return t.name === 'Reply with exactly: plugin-timeout-setup';
220
+ },
221
+ });
222
+ const th = ctx.discord.thread(thread.id);
223
+ await th.waitForBotReply({ timeout: 4_000 });
224
+ await waitForBotMessageContaining({
225
+ discord: ctx.discord,
226
+ threadId: thread.id,
227
+ userId: TEST_USER_ID,
228
+ text: '*project',
229
+ timeout: 4_000,
230
+ });
231
+ await th.user(TEST_USER_ID).sendMessage({
232
+ content: 'PLUGIN_TIMEOUT_SLEEP_MARKER',
233
+ });
234
+ await waitForBotMessageContaining({
235
+ discord: ctx.discord,
236
+ threadId: thread.id,
237
+ userId: TEST_USER_ID,
238
+ text: 'starting sleep 100',
239
+ afterUserMessageIncludes: 'PLUGIN_TIMEOUT_SLEEP_MARKER',
240
+ timeout: 4_000,
241
+ });
242
+ await th.user(TEST_USER_ID).sendMessage({
243
+ content: 'Reply with exactly: plugin-timeout-after',
244
+ });
245
+ const messages = await waitForBotMessageContaining({
246
+ discord: ctx.discord,
247
+ threadId: thread.id,
248
+ userId: TEST_USER_ID,
249
+ text: 'ok',
250
+ afterUserMessageIncludes: 'plugin-timeout-after',
251
+ timeout: 12_000,
252
+ });
253
+ const messagesWithFooter = await waitForFooterMessage({
254
+ discord: ctx.discord,
255
+ threadId: thread.id,
256
+ timeout: 12_000,
257
+ afterMessageIncludes: 'plugin-timeout-after',
258
+ afterAuthorId: TEST_USER_ID,
259
+ });
260
+ const afterIndex = messagesWithFooter.findIndex((message) => {
261
+ return (message.author.id === TEST_USER_ID
262
+ && message.content.includes('plugin-timeout-after'));
263
+ });
264
+ expect(await th.text()).toMatchInlineSnapshot(`
265
+ "--- from: user (queue-advanced-tester)
266
+ Reply with exactly: plugin-timeout-setup
267
+ --- from: assistant (TestBot)
268
+ ⬥ ok
269
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
270
+ --- from: user (queue-advanced-tester)
271
+ PLUGIN_TIMEOUT_SLEEP_MARKER
272
+ --- from: assistant (TestBot)
273
+ ⬥ starting sleep 100
274
+ --- from: user (queue-advanced-tester)
275
+ Reply with exactly: plugin-timeout-after
276
+ --- from: assistant (TestBot)
277
+ ⬥ ok
278
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
279
+ `);
280
+ expect(afterIndex).toBeGreaterThanOrEqual(0);
281
+ const okReplyIndex = messagesWithFooter.findIndex((message, index) => {
282
+ if (index <= afterIndex) {
283
+ return false;
284
+ }
285
+ return message.author.id === ctx.discord.botUserId && message.content.includes('ok');
286
+ });
287
+ expect(okReplyIndex).toBeGreaterThan(afterIndex);
288
+ const footerBeforeReply = messagesWithFooter.some((message, index) => {
289
+ if (index <= afterIndex || index >= okReplyIndex) {
290
+ return false;
291
+ }
292
+ if (message.author.id !== ctx.discord.botUserId) {
293
+ return false;
294
+ }
295
+ return message.content.startsWith('*') && message.content.includes('⋅');
296
+ });
297
+ expect(footerBeforeReply).toBe(false);
298
+ }, 15_000);
299
+ test('tool-call assistant message gets footer when it completes normally', async () => {
300
+ // Reproduces the bug: model responds with text + tool call,
301
+ // finish="tool-calls", message gets completed timestamp. Then the tool
302
+ // result triggers a follow-up text response in a second assistant message.
303
+ // The second message gets a footer, but the first (tool-call) message
304
+ // should ALSO get a footer since it completed normally.
305
+ // This matches the real-world scenario where an agent calls a bash tool
306
+ // (e.g. `otto send`) and then follows up with a summary text.
307
+ const existingThreadIds = new Set((await ctx.discord.channel(TEXT_CHANNEL_ID).getThreads()).map((thread) => {
308
+ return thread.id;
309
+ }));
310
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
311
+ content: 'TOOL_CALL_FOOTER_MARKER',
312
+ });
313
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
314
+ timeout: 6_000,
315
+ predicate: (t) => {
316
+ return !existingThreadIds.has(t.id);
317
+ },
318
+ });
319
+ const th = ctx.discord.thread(thread.id);
320
+ // Wait for the follow-up text response after tool completion.
321
+ // The tool call completes and the model follows up with a second
322
+ // assistant message containing text.
323
+ await waitForBotReplyAfterUserMessage({
324
+ discord: ctx.discord,
325
+ threadId: thread.id,
326
+ userId: TEST_USER_ID,
327
+ userMessageIncludes: 'TOOL_CALL_FOOTER_MARKER',
328
+ timeout: 6_000,
329
+ });
330
+ // Wait for at least one footer to appear
331
+ await waitForFooterMessage({
332
+ discord: ctx.discord,
333
+ threadId: thread.id,
334
+ timeout: 4_000,
335
+ });
336
+ // Poll until both footers have arrived — the first footer (after the
337
+ // tool-call step) and the second (after the text follow-up) are emitted
338
+ // by sequential handleNaturalAssistantCompletion calls but the second
339
+ // may not have hit the Discord thread by the time we first check.
340
+ const deadline = Date.now() + 4_000;
341
+ let footerCount = 0;
342
+ while (Date.now() < deadline) {
343
+ const msgs = await th.getMessages();
344
+ footerCount = msgs.filter((m) => {
345
+ return m.author.id === ctx.discord.botUserId
346
+ && m.content.startsWith('*')
347
+ && m.content.includes('⋅');
348
+ }).length;
349
+ if (footerCount >= 2) {
350
+ break;
351
+ }
352
+ await new Promise((resolve) => {
353
+ setTimeout(resolve, 100);
354
+ });
355
+ }
356
+ expect(await th.text()).toMatchInlineSnapshot(`
357
+ "--- from: user (queue-advanced-tester)
358
+ TOOL_CALL_FOOTER_MARKER
359
+ --- from: assistant (TestBot)
360
+ ⬥ running tool
361
+ ⬥ ok
362
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
363
+ `);
364
+ // Only ONE footer at the end — the tool-call step's footer is NOT
365
+ // emitted mid-turn. The final text follow-up gets the footer.
366
+ expect(footerCount).toBe(1);
367
+ }, 10_000);
368
+ test('multi-step tool chain should only have one footer at the end', async () => {
369
+ // Model does 3 sequential tool calls (each a separate assistant message
370
+ // with finish="tool-calls") then a final text response. Only the final
371
+ // text response should get a footer — intermediate tool-call steps
372
+ // should NOT get footers since they're mid-turn work.
373
+ const existingThreadIds = new Set((await ctx.discord.channel(TEXT_CHANNEL_ID).getThreads()).map((thread) => {
374
+ return thread.id;
375
+ }));
376
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
377
+ content: 'MULTI_TOOL_FOOTER_MARKER',
378
+ });
379
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
380
+ timeout: 6_000,
381
+ predicate: (t) => {
382
+ return !existingThreadIds.has(t.id);
383
+ },
384
+ });
385
+ const th = ctx.discord.thread(thread.id);
386
+ // Wait for the final text response after all 3 tool steps
387
+ await waitForBotMessageContaining({
388
+ discord: ctx.discord,
389
+ threadId: thread.id,
390
+ userId: TEST_USER_ID,
391
+ text: 'all done, fixed 3 files',
392
+ timeout: 6_000,
393
+ });
394
+ // Wait for the footer after the final response
395
+ await waitForFooterMessage({
396
+ discord: ctx.discord,
397
+ threadId: thread.id,
398
+ timeout: 6_000,
399
+ });
400
+ // Give any spurious extra footers time to arrive
401
+ await new Promise((resolve) => {
402
+ setTimeout(resolve, 500);
403
+ });
404
+ const messages = await th.getMessages();
405
+ const footerCount = messages.filter((m) => {
406
+ return m.author.id === ctx.discord.botUserId
407
+ && m.content.startsWith('*')
408
+ && m.content.includes('⋅');
409
+ }).length;
410
+ expect(await th.text()).toMatchInlineSnapshot(`
411
+ "--- from: user (queue-advanced-tester)
412
+ MULTI_TOOL_FOOTER_MARKER
413
+ --- from: assistant (TestBot)
414
+ ⬥ investigating the issue
415
+ ⬥ all done, fixed 3 files
416
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
417
+ `);
418
+ // Only ONE footer should appear — after the final text response.
419
+ // Intermediate tool-call steps should NOT get footers.
420
+ expect(footerCount).toBe(1);
421
+ }, 10_000);
422
+ test('3 sequential tool-call steps produce exactly 1 footer, not 3', async () => {
423
+ // This is the most obvious reproduction of the multi-footer bug:
424
+ // the model runs 3 sequential tool-call steps (each a SEPARATE
425
+ // assistant message with finish="tool-calls"), then a final text.
426
+ // With a naive fix that treats tool-calls as natural completions,
427
+ // you'd see 4 footers (one per assistant message). Only the final
428
+ // text response should produce a footer.
429
+ const existingThreadIds = new Set((await ctx.discord.channel(TEXT_CHANNEL_ID).getThreads()).map((thread) => {
430
+ return thread.id;
431
+ }));
432
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
433
+ content: 'MULTI_STEP_CHAIN_MARKER',
434
+ });
435
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
436
+ timeout: 6_000,
437
+ predicate: (t) => {
438
+ return !existingThreadIds.has(t.id);
439
+ },
440
+ });
441
+ const th = ctx.discord.thread(thread.id);
442
+ // Wait for the final text after all 3 sequential tool steps
443
+ await waitForBotMessageContaining({
444
+ discord: ctx.discord,
445
+ threadId: thread.id,
446
+ userId: TEST_USER_ID,
447
+ text: 'chain complete: all 3 steps done',
448
+ timeout: 10_000,
449
+ });
450
+ // Wait for footer
451
+ await waitForFooterMessage({
452
+ discord: ctx.discord,
453
+ threadId: thread.id,
454
+ timeout: 6_000,
455
+ });
456
+ // Give any spurious extra footers time to arrive
457
+ await new Promise((resolve) => {
458
+ setTimeout(resolve, 500);
459
+ });
460
+ const messages = await th.getMessages();
461
+ const footerCount = messages.filter((m) => {
462
+ return m.author.id === ctx.discord.botUserId
463
+ && m.content.startsWith('*')
464
+ && m.content.includes('⋅');
465
+ }).length;
466
+ expect(await th.text()).toMatchInlineSnapshot(`
467
+ "--- from: user (queue-advanced-tester)
468
+ MULTI_STEP_CHAIN_MARKER
469
+ --- from: assistant (TestBot)
470
+ ⬥ chain step 1: reading config
471
+ ⬥ chain step 2: analyzing results
472
+ ⬥ chain step 3: applying fix
473
+ ⬥ chain complete: all 3 steps done
474
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
475
+ `);
476
+ // The critical assertion: only 1 footer at the very end.
477
+ // With the naive "allow tool-calls as natural completion" fix,
478
+ // this would be 4 (one per assistant message). We want 1.
479
+ expect(footerCount).toBe(1);
480
+ }, 15_000);
481
+ });