@otto-assistant/otto 0.1.2 → 0.7.16

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 +655 -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 +893 -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 +369 -0
  47. package/dist/commands/model.js +798 -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 +179 -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 +1124 -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 +789 -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 +1181 -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 +488 -0
  324. package/src/commands/model.ts +1082 -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 +1507 -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 +232 -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 +1462 -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,725 @@
1
+ // Scheduled task runner for executing due `send --send-at` jobs in the bot process.
2
+
3
+ import { Client, type REST, Routes } from "discord.js";
4
+ import { createDiscordRest } from "./discord-urls.js";
5
+ import YAML from "yaml";
6
+ import {
7
+ claimScheduledTaskRunning,
8
+ createIpcRequest,
9
+ getIpcRequestById,
10
+ getBotTokenWithMode,
11
+ getDuePlannedScheduledTasks,
12
+ markScheduledTaskCronRescheduled,
13
+ markScheduledTaskCronRetry,
14
+ markScheduledTaskFailed,
15
+ markScheduledTaskOneShotCompleted,
16
+ recoverStaleRunningScheduledTasks,
17
+ setThreadSession,
18
+ getAllTextChannelDirectories,
19
+ type ScheduledTask,
20
+ } from "./database.js";
21
+ import { createLogger, formatErrorWithStack, LogPrefix } from "./logger.js";
22
+ import {
23
+ buildSessionPermissions,
24
+ initializeOpencodeForDirectory,
25
+ } from "./opencode.js";
26
+ import { notifyError } from "./sentry.js";
27
+ import type { ThreadStartMarker } from "./system-message.js";
28
+ import {
29
+ type ScheduledTaskPayload,
30
+ getNextCronRun,
31
+ getPromptPreview,
32
+ parseScheduledTaskPayload,
33
+ } from "./task-schedule.js";
34
+
35
+ const taskLogger = createLogger(LogPrefix.TASK);
36
+
37
+ async function waitForIpcRequestCompletion({
38
+ requestId,
39
+ timeoutMs = 4_000,
40
+ pollMs = 100,
41
+ }: {
42
+ requestId: string;
43
+ timeoutMs?: number;
44
+ pollMs?: number;
45
+ }): Promise<true | Error> {
46
+ const startedAt = Date.now();
47
+ while (Date.now() - startedAt < timeoutMs) {
48
+ const row = await getIpcRequestById({ id: requestId });
49
+ if (row?.status === "completed") {
50
+ return true;
51
+ }
52
+ await new Promise<void>((resolve) => {
53
+ setTimeout(() => {
54
+ resolve();
55
+ }, pollMs);
56
+ });
57
+ }
58
+ return new Error(`Timed out waiting for IPC request ${requestId} completion`);
59
+ }
60
+
61
+ type StartTaskRunnerOptions = {
62
+ token: string;
63
+ discordClient?: Client;
64
+ pollIntervalMs?: number;
65
+ staleRunningMs?: number;
66
+ dueBatchSize?: number;
67
+ };
68
+
69
+ function isRecord(value: unknown): value is Record<string, unknown> {
70
+ return typeof value === "object" && value !== null;
71
+ }
72
+
73
+ function parseMessageId(value: unknown): string | Error {
74
+ if (!isRecord(value)) {
75
+ return new Error("Discord response is not an object");
76
+ }
77
+ if (typeof value.id !== "string") {
78
+ return new Error("Discord response is missing message ID");
79
+ }
80
+ return value.id;
81
+ }
82
+
83
+ async function executeThreadScheduledTask({
84
+ rest,
85
+ task,
86
+ payload,
87
+ }: {
88
+ rest: REST;
89
+ task: ScheduledTask;
90
+ payload: Extract<ScheduledTaskPayload, { kind: "thread" }>;
91
+ }): Promise<void | Error> {
92
+ const marker: ThreadStartMarker = {
93
+ start: true,
94
+ scheduledKind: task.schedule_kind,
95
+ scheduledTaskId: task.id,
96
+ ...(payload.agent ? { agent: payload.agent } : {}),
97
+ ...(payload.model ? { model: payload.model } : {}),
98
+ ...(payload.username ? { username: payload.username } : {}),
99
+ ...(payload.userId ? { userId: payload.userId } : {}),
100
+ ...(payload.permissions?.length
101
+ ? { permissions: payload.permissions }
102
+ : {}),
103
+ ...(payload.injectionGuardPatterns?.length
104
+ ? { injectionGuardPatterns: payload.injectionGuardPatterns }
105
+ : {}),
106
+ };
107
+ const embed = [{ color: 0x2b2d31, footer: { text: YAML.stringify(marker) } }];
108
+ // Newline between prefix and prompt so leading /command detection can
109
+ // find the command on its own line.
110
+ const prefixedPrompt = `» **otto-cli:**\n${payload.prompt}`;
111
+
112
+ // Agent-first path for silent mode: initialize opencode directly and IPC the response
113
+ if (payload.silentPrompt) {
114
+ const botRow = await getBotTokenWithMode();
115
+ const appId = botRow?.appId;
116
+ if (!appId) {
117
+ return new Error(`Cannot get bot appId for task ${task.id}`);
118
+ }
119
+
120
+ const projectDirectory =
121
+ task.project_directory || `/home/ubuntu/.otto/projects/general`;
122
+
123
+ const prevCleanup = process.env.OTTO_SKIP_OPENCODE_PROCESS_CLEANUP;
124
+ process.env.OTTO_SKIP_OPENCODE_PROCESS_CLEANUP = "1";
125
+
126
+ try {
127
+ const getClient = await initializeOpencodeForDirectory(projectDirectory);
128
+ if (getClient instanceof Error) {
129
+ return new Error(`Failed to initialize opencode for task ${task.id}`, {
130
+ cause: getClient,
131
+ });
132
+ }
133
+
134
+ const registeredProjectDirs = await getAllTextChannelDirectories()
135
+ const created = await getClient().session.create({
136
+ directory: projectDirectory,
137
+ permission: buildSessionPermissions({ directory: projectDirectory, extraAllowedDirectories: registeredProjectDirs }),
138
+ });
139
+ const sessionId = created.data?.id;
140
+ if (!sessionId) {
141
+ return new Error(
142
+ `Failed to create opencode session for task ${task.id}`,
143
+ );
144
+ }
145
+
146
+ // Post invisible starter with marker embed, then delete it
147
+ const starterResult = await rest
148
+ .post(Routes.channelMessages(payload.threadId), {
149
+ body: { content: "", embeds: embed },
150
+ })
151
+ .catch((e) => {
152
+ return new Error(`Failed to post starter for task ${task.id}`, {
153
+ cause: e,
154
+ });
155
+ });
156
+ if (starterResult instanceof Error) return starterResult;
157
+
158
+ const starterId = parseMessageId(starterResult);
159
+ if (starterId instanceof Error) return starterId;
160
+
161
+ // Keep starter message to preserve valid thread-first message in Discord UI.
162
+
163
+ // Persist thread -> session and IPC so bot streams the response
164
+ await setThreadSession(payload.threadId, sessionId);
165
+ const ipcRow = await createIpcRequest({
166
+ type: "start_thread_listener",
167
+ sessionId,
168
+ threadId: payload.threadId,
169
+ payload: JSON.stringify({
170
+ channelId: payload.threadId,
171
+ appId,
172
+ projectDirectory,
173
+ }),
174
+ });
175
+
176
+ const ipcReady = await waitForIpcRequestCompletion({
177
+ requestId: ipcRow.id,
178
+ });
179
+ if (ipcReady instanceof Error) {
180
+ return ipcReady;
181
+ }
182
+
183
+ // Submit prompt AFTER listener IPC request is created so short/fast
184
+ // model responses are not missed before subscription is active.
185
+ await getClient().session.promptAsync({
186
+ sessionID: sessionId,
187
+ directory: projectDirectory,
188
+ parts: [{ type: "text" as const, text: payload.prompt }],
189
+ ...(payload.agent ? { agent: payload.agent } : {}),
190
+ });
191
+
192
+ taskLogger.log(
193
+ `[task ${task.id}] Agent-first thread session started (thread=${payload.threadId}, session=${sessionId})`,
194
+ );
195
+ } finally {
196
+ if (prevCleanup === undefined) {
197
+ delete process.env.OTTO_SKIP_OPENCODE_PROCESS_CLEANUP;
198
+ } else {
199
+ process.env.OTTO_SKIP_OPENCODE_PROCESS_CLEANUP = prevCleanup;
200
+ }
201
+ }
202
+
203
+ return;
204
+ }
205
+
206
+ // Non-silent path: post prompt visibly
207
+ const postResult = await rest
208
+ .post(Routes.channelMessages(payload.threadId), {
209
+ body: {
210
+ content: prefixedPrompt,
211
+ embeds: embed,
212
+ },
213
+ })
214
+ .catch((error) => {
215
+ return new Error(`Failed to post scheduled thread task ${task.id}`, {
216
+ cause: error,
217
+ });
218
+ });
219
+
220
+ if (postResult instanceof Error) {
221
+ return postResult;
222
+ }
223
+ }
224
+
225
+ async function executeChannelScheduledTask({
226
+ rest,
227
+ discordClient,
228
+ task,
229
+ payload,
230
+ }: {
231
+ rest: REST;
232
+ discordClient?: Client;
233
+ task: ScheduledTask;
234
+ payload: Extract<ScheduledTaskPayload, { kind: "channel" }>;
235
+ }): Promise<void | Error> {
236
+ const marker: ThreadStartMarker | undefined = payload.notifyOnly
237
+ ? undefined
238
+ : {
239
+ start: true,
240
+ scheduledKind: task.schedule_kind,
241
+ scheduledTaskId: task.id,
242
+ ...(payload.worktreeName ? { worktree: payload.worktreeName } : {}),
243
+ ...(payload.cwd ? { cwd: payload.cwd } : {}),
244
+ ...(payload.agent ? { agent: payload.agent } : {}),
245
+ ...(payload.model ? { model: payload.model } : {}),
246
+ ...(payload.username ? { username: payload.username } : {}),
247
+ ...(payload.userId ? { userId: payload.userId } : {}),
248
+ ...(payload.permissions?.length
249
+ ? { permissions: payload.permissions }
250
+ : {}),
251
+ ...(payload.injectionGuardPatterns?.length
252
+ ? { injectionGuardPatterns: payload.injectionGuardPatterns }
253
+ : {}),
254
+ };
255
+ const embeds = marker
256
+ ? [{ color: 0x2b2d31, footer: { text: YAML.stringify(marker) } }]
257
+ : undefined;
258
+
259
+ const threadName = (payload.name || getPromptPreview(payload.prompt)).slice(
260
+ 0,
261
+ 100,
262
+ );
263
+
264
+ /**
265
+ * Agent-first path (silent, non-notify): the opencode session is created
266
+ * here and the prompt is submitted via SDK. No Discord message shows the
267
+ * user's prompt — the bot's IPC listener streams the response directly.
268
+ */
269
+ if (payload.silentPrompt && !payload.notifyOnly) {
270
+ // 1. Get bot appId from stored credentials and project directory from task
271
+ const botRow = await getBotTokenWithMode();
272
+ const appId = botRow?.appId;
273
+ if (!appId) {
274
+ return new Error(`Cannot get bot appId for task ${task.id}`);
275
+ }
276
+
277
+ const projectDirectory =
278
+ task.project_directory || `/home/ubuntu/.otto/projects/general`;
279
+
280
+ // 2. Prevent CLI exit from killing the opencode server we are about to start
281
+ const prevCleanup = process.env.OTTO_SKIP_OPENCODE_PROCESS_CLEANUP;
282
+ process.env.OTTO_SKIP_OPENCODE_PROCESS_CLEANUP = "1";
283
+
284
+ try {
285
+ // 3. Initialize opencode (starts server if not already running)
286
+ taskLogger.log(
287
+ `[task ${task.id}] Initializing opencode for ${projectDirectory}`,
288
+ );
289
+ const getClient = await initializeOpencodeForDirectory(projectDirectory);
290
+ if (getClient instanceof Error) {
291
+ return new Error(`Failed to initialize opencode for task ${task.id}`, {
292
+ cause: getClient,
293
+ });
294
+ }
295
+
296
+ // 4. Create session and queue the prompt
297
+ taskLogger.log(`[task ${task.id}] Creating opencode session`);
298
+ const registeredProjectDirs2 = await getAllTextChannelDirectories()
299
+ const created = await getClient().session.create({
300
+ directory: projectDirectory,
301
+ permission: buildSessionPermissions({ directory: projectDirectory, extraAllowedDirectories: registeredProjectDirs2 }),
302
+ });
303
+ const sessionId = created.data?.id;
304
+ if (!sessionId) {
305
+ return new Error(
306
+ `Failed to create opencode session for task ${task.id}`,
307
+ );
308
+ }
309
+
310
+ // 5. Post an invisible starter message (marker embed only — no content, no attachment)
311
+ const starterResult = await rest
312
+ .post(Routes.channelMessages(payload.channelId), {
313
+ body: {
314
+ content: "",
315
+ embeds,
316
+ },
317
+ })
318
+ .catch((error) => {
319
+ return new Error(
320
+ `Failed to create starter message for task ${task.id}`,
321
+ {
322
+ cause: error,
323
+ },
324
+ );
325
+ });
326
+ if (starterResult instanceof Error) {
327
+ return starterResult;
328
+ }
329
+
330
+ const starterMessageId = parseMessageId(starterResult);
331
+ if (starterMessageId instanceof Error) {
332
+ return new Error(
333
+ `Invalid starter message response for task ${task.id}`,
334
+ {
335
+ cause: starterMessageId,
336
+ },
337
+ );
338
+ }
339
+
340
+ // 6. Create thread from the invisible starter message
341
+ taskLogger.log(`[task ${task.id}] Creating thread`);
342
+ const threadResult = await rest
343
+ .post(Routes.threads(payload.channelId, starterMessageId), {
344
+ body: {
345
+ name: threadName,
346
+ auto_archive_duration: 1440,
347
+ },
348
+ })
349
+ .catch((error) => {
350
+ return new Error(`Failed to create thread for task ${task.id}`, {
351
+ cause: error,
352
+ });
353
+ });
354
+ if (threadResult instanceof Error) {
355
+ return threadResult;
356
+ }
357
+
358
+ const threadIdResult = parseMessageId(threadResult);
359
+ if (threadIdResult instanceof Error) {
360
+ return new Error(`Invalid thread response for task ${task.id}`, {
361
+ cause: threadIdResult,
362
+ });
363
+ }
364
+
365
+ // 7. Persist thread -> session mapping so future messages route to this session
366
+ await setThreadSession(threadIdResult, sessionId);
367
+
368
+ // 8. Create IPC request so the bot's listener picks up this session
369
+ const ipcRow = await createIpcRequest({
370
+ type: "start_thread_listener",
371
+ sessionId,
372
+ threadId: threadIdResult,
373
+ payload: JSON.stringify({
374
+ channelId: payload.channelId,
375
+ appId,
376
+ projectDirectory,
377
+ }),
378
+ });
379
+
380
+ const ipcReady = await waitForIpcRequestCompletion({
381
+ requestId: ipcRow.id,
382
+ });
383
+ if (ipcReady instanceof Error) {
384
+ return ipcReady;
385
+ }
386
+
387
+ // Submit prompt AFTER listener IPC request is created so short/fast
388
+ // model responses are not missed before subscription is active.
389
+ await getClient().session.promptAsync({
390
+ sessionID: sessionId,
391
+ directory: projectDirectory,
392
+ parts: [{ type: "text" as const, text: payload.prompt }],
393
+ ...(payload.agent ? { agent: payload.agent } : {}),
394
+ });
395
+
396
+ // 9. Add user to thread if specified
397
+ if (payload.userId) {
398
+ await rest
399
+ .put(Routes.threadMembers(threadIdResult, payload.userId))
400
+ .catch(() => {}); // Best-effort
401
+ }
402
+
403
+ taskLogger.log(
404
+ `[task ${task.id}] Agent-first scheduled session started (thread=${threadIdResult}, session=${sessionId})`,
405
+ );
406
+ } finally {
407
+ // Restore the env var so other task executions are unaffected
408
+ if (prevCleanup === undefined) {
409
+ delete process.env.OTTO_SKIP_OPENCODE_PROCESS_CLEANUP;
410
+ } else {
411
+ process.env.OTTO_SKIP_OPENCODE_PROCESS_CLEANUP = prevCleanup;
412
+ }
413
+ }
414
+
415
+ return;
416
+ }
417
+
418
+ // Non-silent / notify-only path: post the prompt visibly and let the bot handle it
419
+ const starterResult = await rest
420
+ .post(Routes.channelMessages(payload.channelId), {
421
+ body: {
422
+ content: payload.notifyOnly ? "" : payload.prompt,
423
+ embeds,
424
+ },
425
+ })
426
+ .catch((error) => {
427
+ return new Error(`Failed to create starter message for task ${task.id}`, {
428
+ cause: error,
429
+ });
430
+ });
431
+
432
+ if (starterResult instanceof Error) {
433
+ return starterResult;
434
+ }
435
+
436
+ const starterMessageId = parseMessageId(starterResult);
437
+ if (starterMessageId instanceof Error) {
438
+ return new Error(`Invalid starter message response for task ${task.id}`, {
439
+ cause: starterMessageId,
440
+ });
441
+ }
442
+
443
+ const threadResult = await rest
444
+ .post(Routes.threads(payload.channelId, starterMessageId), {
445
+ body: {
446
+ name: threadName,
447
+ auto_archive_duration: 1440,
448
+ },
449
+ })
450
+ .catch((error) => {
451
+ return new Error(`Failed to create thread for task ${task.id}`, {
452
+ cause: error,
453
+ });
454
+ });
455
+
456
+ if (threadResult instanceof Error) {
457
+ return threadResult;
458
+ }
459
+
460
+ if (!payload.userId) {
461
+ return;
462
+ }
463
+
464
+ const threadIdResult = parseMessageId(threadResult);
465
+ if (threadIdResult instanceof Error) {
466
+ return new Error(`Invalid thread response for task ${task.id}`, {
467
+ cause: threadIdResult,
468
+ });
469
+ }
470
+
471
+ const addMemberResult = await rest
472
+ .put(Routes.threadMembers(threadIdResult, payload.userId))
473
+ .catch((error) => {
474
+ return new Error(
475
+ `Failed to add user to scheduled thread for task ${task.id}`,
476
+ { cause: error },
477
+ );
478
+ });
479
+ if (addMemberResult instanceof Error) {
480
+ return addMemberResult;
481
+ }
482
+ }
483
+
484
+ async function executeScheduledTask({
485
+ rest,
486
+ discordClient,
487
+ task,
488
+ }: {
489
+ rest: REST;
490
+ discordClient?: Client;
491
+ task: ScheduledTask;
492
+ }): Promise<void | Error> {
493
+ const payloadResult = parseScheduledTaskPayload(task.payload_json);
494
+ if (payloadResult instanceof Error) {
495
+ return new Error(`Task ${task.id} has invalid payload`, {
496
+ cause: payloadResult,
497
+ });
498
+ }
499
+
500
+ if (payloadResult.kind === "thread") {
501
+ return executeThreadScheduledTask({
502
+ rest,
503
+ task,
504
+ payload: payloadResult,
505
+ });
506
+ }
507
+
508
+ return executeChannelScheduledTask({
509
+ rest,
510
+ discordClient,
511
+ task,
512
+ payload: payloadResult,
513
+ });
514
+ }
515
+
516
+ async function finalizeSuccessfulTask({
517
+ task,
518
+ completedAt,
519
+ }: {
520
+ task: ScheduledTask;
521
+ completedAt: Date;
522
+ }): Promise<void> {
523
+ if (task.schedule_kind === "at") {
524
+ await markScheduledTaskOneShotCompleted({ taskId: task.id, completedAt });
525
+ return;
526
+ }
527
+
528
+ if (!task.cron_expr) {
529
+ await markScheduledTaskFailed({
530
+ taskId: task.id,
531
+ failedAt: completedAt,
532
+ errorMessage: "Missing cron expression on cron task",
533
+ });
534
+ return;
535
+ }
536
+
537
+ // Use stored timezone, falling back to UTC (not machine local) for consistency
538
+ const timezone = task.timezone || "UTC";
539
+ const nextRunResult = getNextCronRun({
540
+ cronExpr: task.cron_expr,
541
+ timezone,
542
+ from: completedAt,
543
+ });
544
+ if (nextRunResult instanceof Error) {
545
+ await markScheduledTaskFailed({
546
+ taskId: task.id,
547
+ failedAt: completedAt,
548
+ errorMessage: nextRunResult.message,
549
+ });
550
+ return;
551
+ }
552
+
553
+ await markScheduledTaskCronRescheduled({
554
+ taskId: task.id,
555
+ completedAt,
556
+ nextRunAt: nextRunResult,
557
+ });
558
+ }
559
+
560
+ async function finalizeFailedTask({
561
+ task,
562
+ failedAt,
563
+ error,
564
+ }: {
565
+ task: ScheduledTask;
566
+ failedAt: Date;
567
+ error: Error;
568
+ }): Promise<void> {
569
+ if (task.schedule_kind === "cron" && task.cron_expr) {
570
+ // Use stored timezone, falling back to UTC (not machine local) for consistency
571
+ const timezone = task.timezone || "UTC";
572
+ const nextRunResult = getNextCronRun({
573
+ cronExpr: task.cron_expr,
574
+ timezone,
575
+ from: failedAt,
576
+ });
577
+ if (!(nextRunResult instanceof Error)) {
578
+ await markScheduledTaskCronRetry({
579
+ taskId: task.id,
580
+ failedAt,
581
+ errorMessage: error.message,
582
+ nextRunAt: nextRunResult,
583
+ });
584
+ return;
585
+ }
586
+ }
587
+
588
+ await markScheduledTaskFailed({
589
+ taskId: task.id,
590
+ failedAt,
591
+ errorMessage: error.message,
592
+ });
593
+ }
594
+
595
+ async function processDueTask({
596
+ rest,
597
+ discordClient,
598
+ task,
599
+ }: {
600
+ rest: REST;
601
+ discordClient?: Client;
602
+ task: ScheduledTask;
603
+ }): Promise<void> {
604
+ const startedAt = new Date();
605
+ const claimed = await claimScheduledTaskRunning({
606
+ taskId: task.id,
607
+ startedAt,
608
+ });
609
+ if (!claimed) {
610
+ return;
611
+ }
612
+
613
+ const executeResult = await executeScheduledTask({
614
+ rest,
615
+ discordClient,
616
+ task,
617
+ });
618
+ const finishedAt = new Date();
619
+
620
+ if (executeResult instanceof Error) {
621
+ taskLogger.warn(
622
+ `[task-runner] task ${task.id} failed: ${formatErrorWithStack(executeResult)}`,
623
+ );
624
+ await finalizeFailedTask({
625
+ task,
626
+ failedAt: finishedAt,
627
+ error: executeResult,
628
+ });
629
+ return;
630
+ }
631
+
632
+ await finalizeSuccessfulTask({ task, completedAt: finishedAt });
633
+ }
634
+
635
+ async function runTaskRunnerTick({
636
+ rest,
637
+ discordClient,
638
+ staleRunningMs,
639
+ dueBatchSize,
640
+ }: {
641
+ rest: REST;
642
+ discordClient?: Client;
643
+ staleRunningMs: number;
644
+ dueBatchSize: number;
645
+ }): Promise<void> {
646
+ const staleBefore = new Date(Date.now() - staleRunningMs);
647
+ const recoveredCount = await recoverStaleRunningScheduledTasks({
648
+ staleBefore,
649
+ });
650
+ if (recoveredCount > 0) {
651
+ taskLogger.warn(
652
+ `[task-runner] Recovered ${recoveredCount} stale running task(s)`,
653
+ );
654
+ }
655
+
656
+ const dueTasks = await getDuePlannedScheduledTasks({
657
+ now: new Date(),
658
+ limit: dueBatchSize,
659
+ });
660
+
661
+ await dueTasks.reduce<Promise<void>>(async (previous, task) => {
662
+ await previous;
663
+ await processDueTask({ rest, discordClient, task });
664
+ }, Promise.resolve());
665
+ }
666
+
667
+ export function startTaskRunner({
668
+ token,
669
+ discordClient,
670
+ pollIntervalMs = 5_000,
671
+ staleRunningMs = 120_000,
672
+ dueBatchSize = 20,
673
+ }: StartTaskRunnerOptions): () => Promise<void> {
674
+ const rest = createDiscordRest(token);
675
+ let stopped = false;
676
+ let ticking = false;
677
+ let tickPromise: Promise<void> | null = null;
678
+
679
+ const tick = async () => {
680
+ if (stopped || ticking) {
681
+ return;
682
+ }
683
+
684
+ ticking = true;
685
+ const currentTickPromise = runTaskRunnerTick({
686
+ rest,
687
+ discordClient,
688
+ staleRunningMs,
689
+ dueBatchSize,
690
+ }).catch((error) => {
691
+ return new Error("Task runner tick failed", { cause: error });
692
+ });
693
+ tickPromise = currentTickPromise.then(() => {
694
+ return;
695
+ });
696
+ const runResult = await currentTickPromise;
697
+ if (runResult instanceof Error) {
698
+ taskLogger.error(`[task-runner] ${formatErrorWithStack(runResult)}`);
699
+ void notifyError(runResult, "Task runner tick failed");
700
+ }
701
+ ticking = false;
702
+ tickPromise = null;
703
+ };
704
+
705
+ const timer = setInterval(() => {
706
+ void tick();
707
+ }, pollIntervalMs);
708
+
709
+ void tick();
710
+
711
+ taskLogger.log(`[task-runner] started (interval=${pollIntervalMs}ms)`);
712
+
713
+ return async () => {
714
+ if (stopped) {
715
+ return;
716
+ }
717
+ stopped = true;
718
+ clearInterval(timer);
719
+ if (tickPromise) {
720
+ await tickPromise;
721
+ tickPromise = null;
722
+ }
723
+ taskLogger.log("[task-runner] stopped");
724
+ };
725
+ }