@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,296 @@
1
+ // Measures time-to-ready for the otto Discord bot startup.
2
+ // Used as a baseline to track startup performance and guide optimizations
3
+ // for scale-to-zero deployments where cold start time is critical.
4
+ //
5
+ // Measures each phase independently:
6
+ // 1. Hrana server start (DB + lock port)
7
+ // 2. Database init (Prisma connect via HTTP)
8
+ // 3. Discord.js client creation + login (Gateway READY)
9
+ // 4. startDiscordBot (event handlers + markDiscordGatewayReady)
10
+ // 5. OpenCode server startup (spawn + health poll)
11
+ // 6. Total wall-clock time from zero to "bot ready"
12
+ //
13
+ // Uses discord-digital-twin so Gateway READY is instant (no real Discord).
14
+ // OpenCode startup uses deterministic provider (no real LLM).
15
+ import fs from 'node:fs';
16
+ import path from 'node:path';
17
+ import url from 'node:url';
18
+ import { describe, test, expect, afterAll } from 'vitest';
19
+ import { ChannelType, Client, GatewayIntentBits, Partials } from 'discord.js';
20
+ import { DigitalDiscord } from 'discord-digital-twin/src';
21
+ import { buildDeterministicOpencodeConfig, } from 'opencode-deterministic-provider';
22
+ import { setDataDir } from './config.js';
23
+ import { startDiscordBot } from './discord-bot.js';
24
+ import { setBotToken, initDatabase, closeDatabase, setChannelDirectory, } from './database.js';
25
+ import { startHranaServer, stopHranaServer } from './hrana-server.js';
26
+ import { initializeOpencodeForDirectory, stopOpencodeServer } from './opencode.js';
27
+ import { chooseLockPort, cleanupTestSessions, initTestGitRepo } from './test-utils.js';
28
+ function createRunDirectories() {
29
+ const root = path.resolve(process.cwd(), 'tmp', 'startup-time-e2e');
30
+ fs.mkdirSync(root, { recursive: true });
31
+ const dataDir = fs.mkdtempSync(path.join(root, 'data-'));
32
+ const projectDirectory = path.join(root, 'project');
33
+ fs.mkdirSync(projectDirectory, { recursive: true });
34
+ initTestGitRepo(projectDirectory);
35
+ return { root, dataDir, projectDirectory };
36
+ }
37
+ function createDiscordJsClient({ restUrl }) {
38
+ return new Client({
39
+ intents: [
40
+ GatewayIntentBits.Guilds,
41
+ GatewayIntentBits.GuildMessages,
42
+ GatewayIntentBits.MessageContent,
43
+ GatewayIntentBits.GuildVoiceStates,
44
+ ],
45
+ partials: [
46
+ Partials.Channel,
47
+ Partials.Message,
48
+ Partials.User,
49
+ Partials.ThreadMember,
50
+ ],
51
+ rest: {
52
+ api: restUrl,
53
+ version: '10',
54
+ },
55
+ });
56
+ }
57
+ function createMinimalMatchers() {
58
+ return [
59
+ {
60
+ id: 'startup-test-reply',
61
+ priority: 10,
62
+ when: {
63
+ lastMessageRole: 'user',
64
+ rawPromptIncludes: 'startup-test',
65
+ },
66
+ then: {
67
+ parts: [
68
+ { type: 'stream-start', warnings: [] },
69
+ { type: 'text-start', id: 'startup-reply' },
70
+ { type: 'text-delta', id: 'startup-reply', delta: 'ok' },
71
+ { type: 'text-end', id: 'startup-reply' },
72
+ {
73
+ type: 'finish',
74
+ finishReason: 'stop',
75
+ usage: { inputTokens: 1, outputTokens: 1, totalTokens: 2 },
76
+ },
77
+ ],
78
+ },
79
+ },
80
+ ];
81
+ }
82
+ const TEST_USER_ID = '900000000000000777';
83
+ const TEXT_CHANNEL_ID = '900000000000000778';
84
+ describe('startup time measurement', () => {
85
+ let directories;
86
+ let discord;
87
+ let botClient = null;
88
+ const testStartTime = Date.now();
89
+ afterAll(async () => {
90
+ if (directories) {
91
+ await cleanupTestSessions({
92
+ projectDirectory: directories.projectDirectory,
93
+ testStartTime,
94
+ });
95
+ }
96
+ if (botClient) {
97
+ botClient.destroy();
98
+ }
99
+ await Promise.all([
100
+ stopOpencodeServer().catch(() => { }),
101
+ closeDatabase().catch(() => { }),
102
+ stopHranaServer().catch(() => { }),
103
+ discord?.stop().catch(() => { }),
104
+ ]);
105
+ delete process.env['OTTO_LOCK_PORT'];
106
+ delete process.env['OTTO_DB_URL'];
107
+ if (directories) {
108
+ fs.rmSync(directories.dataDir, { recursive: true, force: true });
109
+ }
110
+ }, 5_000);
111
+ test('measures per-phase startup timings', async () => {
112
+ directories = createRunDirectories();
113
+ const lockPort = chooseLockPort({ key: 'startup-time-e2e' });
114
+ process.env['OTTO_LOCK_PORT'] = String(lockPort);
115
+ setDataDir(directories.dataDir);
116
+ const digitalDiscordDbPath = path.join(directories.dataDir, 'digital-discord.db');
117
+ discord = new DigitalDiscord({
118
+ guild: {
119
+ name: 'Startup Time Guild',
120
+ ownerId: TEST_USER_ID,
121
+ },
122
+ channels: [
123
+ {
124
+ id: TEXT_CHANNEL_ID,
125
+ name: 'startup-time',
126
+ type: ChannelType.GuildText,
127
+ },
128
+ ],
129
+ users: [
130
+ {
131
+ id: TEST_USER_ID,
132
+ username: 'startup-tester',
133
+ },
134
+ ],
135
+ dbUrl: `file:${digitalDiscordDbPath}`,
136
+ });
137
+ await discord.start();
138
+ // Write deterministic opencode config
139
+ const providerNpm = url
140
+ .pathToFileURL(path.resolve(process.cwd(), '..', 'opencode-deterministic-provider', 'src', 'index.ts'))
141
+ .toString();
142
+ const opencodeConfig = buildDeterministicOpencodeConfig({
143
+ providerName: 'deterministic-provider',
144
+ providerNpm,
145
+ model: 'deterministic-v2',
146
+ smallModel: 'deterministic-v2',
147
+ settings: {
148
+ strict: false,
149
+ matchers: createMinimalMatchers(),
150
+ },
151
+ });
152
+ fs.writeFileSync(path.join(directories.projectDirectory, 'opencode.json'), JSON.stringify(opencodeConfig, null, 2));
153
+ // ── Phase timings ──
154
+ const totalStart = performance.now();
155
+ // Phase 1: Hrana server
156
+ const hranaStart = performance.now();
157
+ const dbPath = path.join(directories.dataDir, 'discord-sessions.db');
158
+ const hranaResult = await startHranaServer({ dbPath });
159
+ if (hranaResult instanceof Error) {
160
+ throw hranaResult;
161
+ }
162
+ process.env['OTTO_DB_URL'] = hranaResult;
163
+ const hranaMs = performance.now() - hranaStart;
164
+ // Phase 2: Database init
165
+ const dbStart = performance.now();
166
+ await initDatabase();
167
+ await setBotToken(discord.botUserId, discord.botToken);
168
+ await setChannelDirectory({
169
+ channelId: TEXT_CHANNEL_ID,
170
+ directory: directories.projectDirectory,
171
+ channelType: 'text',
172
+ });
173
+ const dbMs = performance.now() - dbStart;
174
+ // Phase 3+4: Discord.js login + startDiscordBot
175
+ // In the real cli.ts flow, login happens first (line 2077), then
176
+ // startDiscordBot is called with the already-logged-in client (line 2130).
177
+ // startDiscordBot calls login() again internally (line 1069) which is
178
+ // a no-op on already-connected clients. We measure them together since
179
+ // that's the real critical path.
180
+ const loginStart = performance.now();
181
+ botClient = createDiscordJsClient({ restUrl: discord.restUrl });
182
+ // Don't pre-login — let startDiscordBot handle login internally.
183
+ // This avoids the double-login overhead that inflates measurements.
184
+ const loginMs = Math.round(performance.now() - loginStart);
185
+ const botStart = performance.now();
186
+ await startDiscordBot({
187
+ token: discord.botToken,
188
+ appId: discord.botUserId,
189
+ discordClient: botClient,
190
+ });
191
+ const botMs = performance.now() - botStart;
192
+ // Phase 5: OpenCode server startup (biggest bottleneck)
193
+ const opencodeStart = performance.now();
194
+ const opencodeResult = await initializeOpencodeForDirectory(directories.projectDirectory);
195
+ if (opencodeResult instanceof Error) {
196
+ throw opencodeResult;
197
+ }
198
+ const opencodeMs = performance.now() - opencodeStart;
199
+ const totalMs = performance.now() - totalStart;
200
+ const timings = {
201
+ hranaServerMs: Math.round(hranaMs),
202
+ databaseInitMs: Math.round(dbMs),
203
+ discordLoginMs: Math.round(loginMs),
204
+ startDiscordBotMs: Math.round(botMs),
205
+ opencodeServerMs: Math.round(opencodeMs),
206
+ totalMs: Math.round(totalMs),
207
+ };
208
+ // Print timings for CI/local visibility
209
+ console.log('\n┌─────────────────────────────────────────────┐');
210
+ console.log('│ Otto Startup Time Breakdown │');
211
+ console.log('├─────────────────────────────────────────────┤');
212
+ console.log(`│ Hrana server: ${String(timings.hranaServerMs).padStart(6)} ms │`);
213
+ console.log(`│ Database init: ${String(timings.databaseInitMs).padStart(6)} ms │`);
214
+ console.log(`│ Discord.js login: ${String(timings.discordLoginMs).padStart(6)} ms │`);
215
+ console.log(`│ startDiscordBot: ${String(timings.startDiscordBotMs).padStart(6)} ms │`);
216
+ console.log(`│ OpenCode server: ${String(timings.opencodeServerMs).padStart(6)} ms │`);
217
+ console.log('├─────────────────────────────────────────────┤');
218
+ console.log(`│ TOTAL: ${String(timings.totalMs).padStart(6)} ms │`);
219
+ console.log('└─────────────────────────────────────────────┘\n');
220
+ // Sanity assertions — these are baselines, not targets yet.
221
+ // Each phase should complete (no infinite hang).
222
+ expect(timings.hranaServerMs).toBeLessThan(5_000);
223
+ expect(timings.databaseInitMs).toBeLessThan(5_000);
224
+ expect(timings.discordLoginMs).toBeLessThan(10_000);
225
+ expect(timings.startDiscordBotMs).toBeLessThan(5_000);
226
+ expect(timings.opencodeServerMs).toBeLessThan(30_000);
227
+ expect(timings.totalMs).toBeLessThan(60_000);
228
+ // Verify the bot is actually functional by sending a message
229
+ // and getting a response (validates the full pipeline works)
230
+ await discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
231
+ content: 'startup-test ping',
232
+ });
233
+ const thread = await discord.channel(TEXT_CHANNEL_ID).waitForThread({
234
+ timeout: 10_000,
235
+ });
236
+ const reply = await discord.thread(thread.id).waitForBotReply({
237
+ timeout: 30_000,
238
+ });
239
+ expect(reply.content.length).toBeGreaterThan(0);
240
+ expect(thread.id.length).toBeGreaterThan(0);
241
+ }, 120_000);
242
+ test('measures parallel startup (discord + opencode simultaneously)', async () => {
243
+ // This test reuses the infrastructure from test 1 (hrana, db already up)
244
+ // to measure what happens when we run Discord login + OpenCode in parallel.
245
+ // In a fresh cold start, hrana+db init would add ~50ms on top.
246
+ // Stop opencode server from test 1 so we get a fresh measurement
247
+ await stopOpencodeServer().catch(() => { });
248
+ // Destroy and recreate bot client for a clean login measurement
249
+ if (botClient) {
250
+ botClient.destroy();
251
+ botClient = null;
252
+ }
253
+ // ── Parallel phase: Discord login + OpenCode server simultaneously ──
254
+ const parallelStart = performance.now();
255
+ const [discordResult, opencodeResult] = await Promise.all([
256
+ // Discord path: create client, login, start bot
257
+ (async () => {
258
+ const loginStart = performance.now();
259
+ const client = createDiscordJsClient({ restUrl: discord.restUrl });
260
+ await startDiscordBot({
261
+ token: discord.botToken,
262
+ appId: discord.botUserId,
263
+ discordClient: client,
264
+ });
265
+ return {
266
+ client,
267
+ totalMs: Math.round(performance.now() - loginStart),
268
+ };
269
+ })(),
270
+ // OpenCode path: spawn server + wait for health
271
+ (async () => {
272
+ const start = performance.now();
273
+ const result = await initializeOpencodeForDirectory(directories.projectDirectory);
274
+ if (result instanceof Error) {
275
+ throw result;
276
+ }
277
+ return { ms: Math.round(performance.now() - start) };
278
+ })(),
279
+ ]);
280
+ const parallelMs = Math.round(performance.now() - parallelStart);
281
+ botClient = discordResult.client;
282
+ console.log('\n┌─────────────────────────────────────────────┐');
283
+ console.log('│ Parallel Startup Time Breakdown │');
284
+ console.log('├─────────────────────────────────────────────┤');
285
+ console.log(`│ Discord login+bot: ${String(discordResult.totalMs).padStart(6)} ms │`);
286
+ console.log(`│ OpenCode server: ${String(opencodeResult.ms).padStart(6)} ms │`);
287
+ console.log('├─────────────────────────────────────────────┤');
288
+ console.log(`│ PARALLEL TOTAL: ${String(parallelMs).padStart(6)} ms │`);
289
+ console.log(`│ (vs sequential: ${String(discordResult.totalMs + opencodeResult.ms).padStart(6)} ms) │`);
290
+ console.log('└─────────────────────────────────────────────┘\n');
291
+ // Parallel total should be dominated by the slower path,
292
+ // not the sum of both.
293
+ const maxSingle = Math.max(discordResult.totalMs, opencodeResult.ms);
294
+ expect(parallelMs).toBeLessThan(maxSingle + 500);
295
+ }, 120_000);
296
+ });
package/dist/store.js ADDED
@@ -0,0 +1,19 @@
1
+ // Centralized zustand/vanilla store for global bot state.
2
+ // Replaces scattered module-level `let` variables, process.env mutations,
3
+ // and mutable arrays with a single immutable state atom.
4
+ // See skills/zustand-centralized-state/SKILL.md for the pattern.
5
+ import { createStore } from 'zustand/vanilla';
6
+ export const store = createStore(() => ({
7
+ dataDir: null,
8
+ projectsDir: null,
9
+ defaultVerbosity: 'text_and_essential_tools',
10
+ defaultMentionMode: false,
11
+ critiqueEnabled: true,
12
+ enabledSkills: [],
13
+ disabledSkills: [],
14
+ discordBaseUrl: 'https://discord.com',
15
+ gatewayToken: null,
16
+ registeredUserCommands: [],
17
+ threads: new Map(),
18
+ test: { deterministicTranscription: null },
19
+ }));
@@ -0,0 +1,175 @@
1
+ // OpenCode plugin that aborts task-created subagent sessions after rate limits.
2
+ import * as errore from 'errore';
3
+ import { appendToastSessionMarker, createPluginLogger, formatPluginErrorWithStack, setPluginLogFilePath, } from './plugin-logger.js';
4
+ import { initSentry, notifyError } from './sentry.js';
5
+ const logger = createPluginLogger('SUBMODEL');
6
+ const RATE_LIMIT_TEXT_PATTERNS = [
7
+ 'rate_limit',
8
+ 'rate limit',
9
+ 'resource exhausted',
10
+ 'retry after',
11
+ 'too many requests',
12
+ 'quota exceeded',
13
+ ];
14
+ function isRateLimitText(text) {
15
+ if (!text) {
16
+ return false;
17
+ }
18
+ const haystack = text.toLowerCase();
19
+ return RATE_LIMIT_TEXT_PATTERNS.some((pattern) => {
20
+ return haystack.includes(pattern);
21
+ });
22
+ }
23
+ function getTaskChildSession(event) {
24
+ if (event.type !== 'message.part.updated') {
25
+ return undefined;
26
+ }
27
+ const part = event.properties.part;
28
+ if (part.type !== 'tool' || part.tool !== 'task' || part.state.status === 'pending') {
29
+ return undefined;
30
+ }
31
+ const childSessionId = part.state.metadata?.sessionId;
32
+ if (typeof childSessionId !== 'string' || childSessionId.length === 0) {
33
+ return undefined;
34
+ }
35
+ const subagentType = part.state.input?.subagent_type;
36
+ return {
37
+ childSessionId,
38
+ subagentType: typeof subagentType === 'string' ? subagentType : undefined,
39
+ };
40
+ }
41
+ function getEventSessionId(event) {
42
+ if (event.type === 'session.status' || event.type === 'session.idle') {
43
+ return event.properties.sessionID;
44
+ }
45
+ if (event.type === 'session.error') {
46
+ return event.properties.sessionID;
47
+ }
48
+ if (event.type === 'message.updated') {
49
+ return event.properties.info.sessionID;
50
+ }
51
+ if (event.type === 'message.part.updated') {
52
+ return event.properties.part.sessionID;
53
+ }
54
+ if (event.type === 'session.created'
55
+ || event.type === 'session.updated'
56
+ || event.type === 'session.deleted') {
57
+ return event.properties.info.id;
58
+ }
59
+ return undefined;
60
+ }
61
+ function extractRateLimitReason(event) {
62
+ if (event.type === 'session.status' && event.properties.status.type === 'retry') {
63
+ return isRateLimitText(event.properties.status.message)
64
+ ? event.properties.status.message
65
+ : undefined;
66
+ }
67
+ if (event.type === 'message.part.updated' && event.properties.part.type === 'retry') {
68
+ const retryError = event.properties.part.error;
69
+ if (retryError.data.statusCode === 429) {
70
+ return retryError.data.message;
71
+ }
72
+ if (isRateLimitText(retryError.data.responseBody)) {
73
+ return retryError.data.responseBody;
74
+ }
75
+ return isRateLimitText(retryError.data.message)
76
+ ? retryError.data.message
77
+ : undefined;
78
+ }
79
+ const apiError = (() => {
80
+ if (event.type === 'session.error' && event.properties.error?.name === 'APIError') {
81
+ return event.properties.error.data;
82
+ }
83
+ if (event.type === 'message.updated'
84
+ && event.properties.info.role === 'assistant'
85
+ && event.properties.info.error?.name === 'APIError') {
86
+ return event.properties.info.error.data;
87
+ }
88
+ return undefined;
89
+ })();
90
+ if (!apiError) {
91
+ return undefined;
92
+ }
93
+ if (apiError.statusCode === 429) {
94
+ return apiError.message;
95
+ }
96
+ if (isRateLimitText(apiError.responseBody)) {
97
+ return apiError.responseBody;
98
+ }
99
+ return isRateLimitText(apiError.message) ? apiError.message : undefined;
100
+ }
101
+ export const subagentRateLimitPlugin = async ({ client, directory }) => {
102
+ initSentry();
103
+ const dataDir = process.env.OTTO_DATA_DIR;
104
+ if (dataDir) {
105
+ setPluginLogFilePath(dataDir);
106
+ }
107
+ const subagentSessions = new Map();
108
+ return {
109
+ event: async ({ event }) => {
110
+ const taskChild = getTaskChildSession(event);
111
+ if (taskChild) {
112
+ const existing = subagentSessions.get(taskChild.childSessionId);
113
+ if (existing) {
114
+ if (taskChild.subagentType) {
115
+ existing.subagentType = taskChild.subagentType;
116
+ }
117
+ }
118
+ else {
119
+ subagentSessions.set(taskChild.childSessionId, {
120
+ subagentType: taskChild.subagentType,
121
+ aborting: false,
122
+ });
123
+ }
124
+ }
125
+ const eventSessionId = getEventSessionId(event);
126
+ if (!eventSessionId) {
127
+ return;
128
+ }
129
+ if (event.type === 'session.deleted' || event.type === 'session.idle') {
130
+ subagentSessions.delete(eventSessionId);
131
+ return;
132
+ }
133
+ const rateLimitReason = extractRateLimitReason(event);
134
+ if (!rateLimitReason) {
135
+ return;
136
+ }
137
+ const subagent = subagentSessions.get(eventSessionId);
138
+ if (!subagent || subagent.aborting) {
139
+ return;
140
+ }
141
+ subagent.aborting = true;
142
+ const abortResult = await errore.tryAsync({
143
+ try: async () => {
144
+ await client.session.abort({
145
+ path: { id: eventSessionId },
146
+ query: { directory },
147
+ });
148
+ await client.tui.showToast({
149
+ body: {
150
+ message: appendToastSessionMarker({
151
+ message: `Aborting ${subagent.subagentType || 'subagent'} after rate limit so the parent task can recover: ${rateLimitReason}`,
152
+ sessionId: eventSessionId,
153
+ }),
154
+ variant: 'info',
155
+ },
156
+ }).catch(() => {
157
+ return;
158
+ });
159
+ logger.info(`Aborted subagent ${eventSessionId} after rate limit`);
160
+ },
161
+ catch: (error) => {
162
+ return new Error('Subagent rate-limit abort failed', {
163
+ cause: error,
164
+ });
165
+ },
166
+ });
167
+ subagentSessions.delete(eventSessionId);
168
+ if (!(abortResult instanceof Error)) {
169
+ return;
170
+ }
171
+ logger.warn(`[subagent-rate-limit-plugin] ${formatPluginErrorWithStack(abortResult)}`);
172
+ void notifyError(abortResult, 'subagent rate-limit plugin abort failed');
173
+ },
174
+ };
175
+ };