@otto-assistant/otto 0.1.1 → 0.7.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (637) hide show
  1. package/bin.js +2 -0
  2. package/dist/agent-model.e2e.test.js +755 -0
  3. package/dist/ai-tool-to-genai.js +233 -0
  4. package/dist/ai-tool-to-genai.test.js +267 -0
  5. package/dist/ai-tool.js +6 -0
  6. package/dist/anthropic-account-identity.js +62 -0
  7. package/dist/anthropic-account-identity.test.js +38 -0
  8. package/dist/anthropic-auth-plugin.js +917 -0
  9. package/dist/anthropic-auth-state.js +303 -0
  10. package/dist/anthropic-auth-state.test.js +150 -0
  11. package/dist/bin.js +152 -0
  12. package/dist/btw-prefix-detection.js +17 -0
  13. package/dist/btw-prefix-detection.test.js +63 -0
  14. package/dist/channel-management.js +259 -0
  15. package/dist/cli-parsing.test.js +142 -0
  16. package/dist/cli-send-thread.e2e.test.js +353 -0
  17. package/dist/cli-telegram-options.test.js +99 -0
  18. package/dist/cli.js +4210 -568
  19. package/dist/commands/abort.js +65 -0
  20. package/dist/commands/action-buttons.js +245 -0
  21. package/dist/commands/add-dir.js +124 -0
  22. package/dist/commands/add-dir.test.js +126 -0
  23. package/dist/commands/add-project.js +113 -0
  24. package/dist/commands/agent.js +355 -0
  25. package/dist/commands/ask-question.js +320 -0
  26. package/dist/commands/ask-question.test.js +92 -0
  27. package/dist/commands/btw.js +121 -0
  28. package/dist/commands/cli-commands-group-a.test.js +728 -0
  29. package/dist/commands/cli-commands-group-b.test.js +695 -0
  30. package/dist/commands/compact.js +120 -0
  31. package/dist/commands/context-usage.js +140 -0
  32. package/dist/commands/create-new-project.js +130 -0
  33. package/dist/commands/diff.js +63 -0
  34. package/dist/commands/discord-commands-group-a.test.js +621 -0
  35. package/dist/commands/discord-commands-group-b.test.js +595 -0
  36. package/dist/commands/discord-commands-group-c.test.js +739 -0
  37. package/dist/commands/file-upload.js +275 -0
  38. package/dist/commands/fork-subagent.js +177 -0
  39. package/dist/commands/fork.js +262 -0
  40. package/dist/commands/gemini-apikey.js +70 -0
  41. package/dist/commands/login.js +887 -0
  42. package/dist/commands/mcp.js +239 -0
  43. package/dist/commands/memory-snapshot.js +24 -0
  44. package/dist/commands/mention-mode.js +44 -0
  45. package/dist/commands/merge-worktree.js +162 -0
  46. package/dist/commands/model-variant.js +366 -0
  47. package/dist/commands/model.js +794 -0
  48. package/dist/commands/new-worktree.js +465 -0
  49. package/dist/commands/paginated-select.js +57 -0
  50. package/dist/commands/permissions.js +274 -0
  51. package/dist/commands/queue.js +223 -0
  52. package/dist/commands/remove-project.js +115 -0
  53. package/dist/commands/restart-opencode-server.js +127 -0
  54. package/dist/commands/resume.js +149 -0
  55. package/dist/commands/run-command.js +79 -0
  56. package/dist/commands/screenshare.js +303 -0
  57. package/dist/commands/screenshare.test.js +20 -0
  58. package/dist/commands/session-id.js +78 -0
  59. package/dist/commands/session.js +176 -0
  60. package/dist/commands/share.js +80 -0
  61. package/dist/commands/tasks.js +205 -0
  62. package/dist/commands/thread-deletion-sync.js +50 -0
  63. package/dist/commands/types.js +2 -0
  64. package/dist/commands/undo-redo.js +305 -0
  65. package/dist/commands/unset-model.js +139 -0
  66. package/dist/commands/upgrade.js +48 -0
  67. package/dist/commands/user-command.js +155 -0
  68. package/dist/commands/verbosity.js +125 -0
  69. package/dist/commands/vscode.js +269 -0
  70. package/dist/commands/worktree-settings.js +43 -0
  71. package/dist/commands/worktrees.js +468 -0
  72. package/dist/condense-memory.js +33 -0
  73. package/dist/config.js +100 -255
  74. package/dist/context-awareness-plugin.js +340 -0
  75. package/dist/context-awareness-plugin.test.js +126 -0
  76. package/dist/critique-utils.js +95 -0
  77. package/dist/database.js +1355 -0
  78. package/dist/db.js +260 -0
  79. package/dist/db.test.js +138 -0
  80. package/dist/debounce-timeout.js +28 -0
  81. package/dist/debounced-process-flush.js +77 -0
  82. package/dist/discord-bot.js +1124 -0
  83. package/dist/discord-command-registration.js +567 -0
  84. package/dist/discord-urls.js +82 -0
  85. package/dist/discord-utils.js +616 -0
  86. package/dist/discord-utils.test.js +134 -0
  87. package/dist/errors.js +157 -0
  88. package/dist/escape-backticks.test.js +429 -0
  89. package/dist/event-stream-real-capture.e2e.test.js +533 -0
  90. package/dist/eventsource-parser.test.js +327 -0
  91. package/dist/exec-async.js +26 -0
  92. package/dist/external-opencode-sync.js +480 -0
  93. package/dist/format-tables.js +491 -0
  94. package/dist/format-tables.test.js +478 -0
  95. package/dist/forum-sync/config.js +79 -0
  96. package/dist/forum-sync/discord-operations.js +154 -0
  97. package/dist/forum-sync/index.js +5 -0
  98. package/dist/forum-sync/markdown.js +113 -0
  99. package/dist/forum-sync/sync-to-discord.js +417 -0
  100. package/dist/forum-sync/sync-to-files.js +190 -0
  101. package/dist/forum-sync/types.js +53 -0
  102. package/dist/forum-sync/watchers.js +307 -0
  103. package/dist/gateway-proxy-reconnect.e2e.test.js +394 -0
  104. package/dist/gateway-proxy.e2e.test.js +485 -0
  105. package/dist/genai-worker-wrapper.js +111 -0
  106. package/dist/genai-worker.js +311 -0
  107. package/dist/genai.js +232 -0
  108. package/dist/generated/browser.js +17 -0
  109. package/dist/generated/client.js +37 -0
  110. package/dist/generated/commonInputTypes.js +10 -0
  111. package/dist/generated/enums.js +58 -0
  112. package/dist/generated/internal/class.js +49 -0
  113. package/dist/generated/internal/prismaNamespace.js +254 -0
  114. package/dist/generated/internal/prismaNamespaceBrowser.js +224 -0
  115. package/dist/generated/models/bot_api_keys.js +1 -0
  116. package/dist/generated/models/bot_tokens.js +1 -0
  117. package/dist/generated/models/channel_agents.js +1 -0
  118. package/dist/generated/models/channel_directories.js +1 -0
  119. package/dist/generated/models/channel_mention_mode.js +1 -0
  120. package/dist/generated/models/channel_models.js +1 -0
  121. package/dist/generated/models/channel_verbosity.js +1 -0
  122. package/dist/generated/models/channel_worktrees.js +1 -0
  123. package/dist/generated/models/forum_sync_configs.js +1 -0
  124. package/dist/generated/models/global_models.js +1 -0
  125. package/dist/generated/models/ipc_requests.js +1 -0
  126. package/dist/generated/models/part_messages.js +1 -0
  127. package/dist/generated/models/scheduled_tasks.js +1 -0
  128. package/dist/generated/models/session_agents.js +1 -0
  129. package/dist/generated/models/session_events.js +1 -0
  130. package/dist/generated/models/session_models.js +1 -0
  131. package/dist/generated/models/session_start_sources.js +1 -0
  132. package/dist/generated/models/thread_sessions.js +1 -0
  133. package/dist/generated/models/thread_worktrees.js +1 -0
  134. package/dist/generated/models.js +1 -0
  135. package/dist/heap-monitor.js +122 -0
  136. package/dist/hrana-server.js +251 -0
  137. package/dist/hrana-server.test.js +370 -0
  138. package/dist/html-actions.js +123 -0
  139. package/dist/html-actions.test.js +70 -0
  140. package/dist/html-components.js +117 -0
  141. package/dist/html-components.test.js +34 -0
  142. package/dist/image-optimizer-plugin.js +153 -0
  143. package/dist/image-utils.js +112 -0
  144. package/dist/interaction-handler.js +420 -0
  145. package/dist/ipc-polling.js +327 -0
  146. package/dist/ipc-tools-plugin.js +193 -0
  147. package/dist/ipc-utils.js +18 -0
  148. package/dist/limit-heading-depth.js +25 -0
  149. package/dist/limit-heading-depth.test.js +105 -0
  150. package/dist/logger.js +171 -0
  151. package/dist/markdown.js +342 -0
  152. package/dist/markdown.test.js +264 -0
  153. package/dist/memory-overview-plugin.js +128 -0
  154. package/dist/message-finish-field.e2e.test.js +168 -0
  155. package/dist/message-formatting.js +415 -0
  156. package/dist/message-formatting.test.js +115 -0
  157. package/dist/message-preprocessing.js +359 -0
  158. package/dist/onboarding-tutorial.js +163 -0
  159. package/dist/onboarding-welcome.js +37 -0
  160. package/dist/openai-realtime.js +224 -0
  161. package/dist/opencode-command-detection.js +65 -0
  162. package/dist/opencode-command-detection.test.js +240 -0
  163. package/dist/opencode-command.js +131 -0
  164. package/dist/opencode-command.test.js +48 -0
  165. package/dist/opencode-interrupt-plugin.js +388 -0
  166. package/dist/opencode-interrupt-plugin.test.js +463 -0
  167. package/dist/opencode.js +1117 -0
  168. package/dist/otto/branding.js +22 -0
  169. package/dist/otto/index.js +21 -0
  170. package/dist/otto-digital-twin.e2e.test.js +161 -0
  171. package/dist/otto-opencode-plugin-loading.e2e.test.js +94 -0
  172. package/dist/otto-opencode-plugin.js +21 -0
  173. package/dist/otto-opencode-plugin.test.js +98 -0
  174. package/dist/parse-permission-rules.test.js +117 -0
  175. package/dist/patch-text-parser.js +97 -0
  176. package/dist/plugin-logger.js +68 -0
  177. package/dist/privacy-sanitizer.js +105 -0
  178. package/dist/queue-advanced-abort.e2e.test.js +293 -0
  179. package/dist/queue-advanced-action-buttons.e2e.test.js +206 -0
  180. package/dist/queue-advanced-e2e-setup.js +790 -0
  181. package/dist/queue-advanced-footer.e2e.test.js +481 -0
  182. package/dist/queue-advanced-model-switch.e2e.test.js +299 -0
  183. package/dist/queue-advanced-permissions-typing.e2e.test.js +179 -0
  184. package/dist/queue-advanced-question.e2e.test.js +261 -0
  185. package/dist/queue-advanced-typing-interrupt.e2e.test.js +114 -0
  186. package/dist/queue-advanced-typing.e2e.test.js +153 -0
  187. package/dist/queue-drain-after-interactive-ui.e2e.test.js +119 -0
  188. package/dist/queue-interrupt-drain.e2e.test.js +135 -0
  189. package/dist/queue-question-select-drain.e2e.test.js +256 -0
  190. package/dist/runtime-idle-sweeper.js +52 -0
  191. package/dist/runtime-lifecycle.e2e.test.js +514 -0
  192. package/dist/sentry.js +23 -0
  193. package/dist/session-handler/agent-utils.js +67 -0
  194. package/dist/session-handler/event-stream-state.js +475 -0
  195. package/dist/session-handler/event-stream-state.test.js +632 -0
  196. package/dist/session-handler/model-utils.js +147 -0
  197. package/dist/session-handler/opencode-session-event-log.js +94 -0
  198. package/dist/session-handler/thread-runtime-state.js +131 -0
  199. package/dist/session-handler/thread-session-runtime.js +3390 -0
  200. package/dist/session-handler.js +9 -0
  201. package/dist/session-search.js +100 -0
  202. package/dist/session-search.test.js +40 -0
  203. package/dist/session-title-rename.test.js +92 -0
  204. package/dist/skill-filter.js +31 -0
  205. package/dist/skill-filter.test.js +65 -0
  206. package/dist/startup-service.js +153 -0
  207. package/dist/startup-time.e2e.test.js +296 -0
  208. package/dist/store.js +19 -0
  209. package/dist/subagent-rate-limit-plugin.js +175 -0
  210. package/dist/system-message.js +702 -0
  211. package/dist/system-message.test.js +697 -0
  212. package/dist/task-runner.js +530 -0
  213. package/dist/task-schedule.js +213 -0
  214. package/dist/task-schedule.test.js +71 -0
  215. package/dist/test-utils.js +313 -0
  216. package/dist/thinking-utils.js +35 -0
  217. package/dist/thread-message-queue.e2e.test.js +1111 -0
  218. package/dist/tools.js +357 -0
  219. package/dist/undo-redo.e2e.test.js +161 -0
  220. package/dist/unnest-code-blocks.js +146 -0
  221. package/dist/unnest-code-blocks.test.js +673 -0
  222. package/dist/upgrade.js +156 -0
  223. package/dist/utils.js +172 -0
  224. package/dist/utils.test.js +130 -0
  225. package/dist/voice-attachment.js +34 -0
  226. package/dist/voice-handler.js +646 -0
  227. package/dist/voice-message.e2e.test.js +1021 -0
  228. package/dist/voice.js +456 -0
  229. package/dist/voice.test.js +235 -0
  230. package/dist/wait-session.js +171 -0
  231. package/dist/websockify.js +69 -0
  232. package/dist/worker-types.js +4 -0
  233. package/dist/worktree-lifecycle.e2e.test.js +311 -0
  234. package/dist/worktree-utils.js +3 -0
  235. package/dist/worktrees.js +991 -0
  236. package/dist/worktrees.test.js +415 -0
  237. package/dist/xml.js +92 -0
  238. package/dist/xml.test.js +32 -0
  239. package/package.json +90 -38
  240. package/schema.prisma +303 -0
  241. package/skills/batch/SKILL.md +87 -0
  242. package/skills/critique/SKILL.md +112 -0
  243. package/skills/egaki/SKILL.md +100 -0
  244. package/skills/errore/SKILL.md +647 -0
  245. package/skills/event-sourcing-state/SKILL.md +252 -0
  246. package/skills/goke/SKILL.md +38 -0
  247. package/skills/jitter/EDITOR.md +219 -0
  248. package/skills/jitter/EXPORT-INTERNALS.md +309 -0
  249. package/skills/jitter/SKILL.md +158 -0
  250. package/skills/jitter/jitter-clipboard.json +1042 -0
  251. package/skills/jitter/package.json +14 -0
  252. package/skills/jitter/tsconfig.json +15 -0
  253. package/skills/jitter/utils/actions.ts +212 -0
  254. package/skills/jitter/utils/export.ts +114 -0
  255. package/skills/jitter/utils/index.ts +141 -0
  256. package/skills/jitter/utils/snapshot.ts +154 -0
  257. package/skills/jitter/utils/traverse.ts +246 -0
  258. package/skills/jitter/utils/types.ts +279 -0
  259. package/skills/jitter/utils/wait.ts +133 -0
  260. package/skills/lintcn/SKILL.md +873 -0
  261. package/skills/manual-kimaki-upstream-adapt/SKILL.md +114 -0
  262. package/skills/new-skill/SKILL.md +237 -0
  263. package/skills/npm-package/SKILL.md +617 -0
  264. package/skills/opensrc/SKILL.md +78 -0
  265. package/skills/otto-publish/SKILL.md +61 -0
  266. package/skills/playwriter/SKILL.md +35 -0
  267. package/skills/profano/SKILL.md +16 -0
  268. package/skills/proxyman/SKILL.md +215 -0
  269. package/skills/security-review/SKILL.md +208 -0
  270. package/skills/sigillo/SKILL.md +101 -0
  271. package/skills/simplify/SKILL.md +58 -0
  272. package/skills/spiceflow/SKILL.md +28 -0
  273. package/skills/termcast/SKILL.md +945 -0
  274. package/skills/tuistory/SKILL.md +98 -0
  275. package/skills/usecomputer/SKILL.md +264 -0
  276. package/skills/x-articles/SKILL.md +554 -0
  277. package/skills/zele/SKILL.md +49 -0
  278. package/skills/zustand-centralized-state/SKILL.md +1004 -0
  279. package/src/agent-model.e2e.test.ts +979 -0
  280. package/src/ai-tool-to-genai.test.ts +296 -0
  281. package/src/ai-tool-to-genai.ts +283 -0
  282. package/src/ai-tool.ts +39 -0
  283. package/src/anthropic-account-identity.test.ts +52 -0
  284. package/src/anthropic-account-identity.ts +77 -0
  285. package/src/anthropic-auth-plugin.ts +1139 -0
  286. package/src/anthropic-auth-state.test.ts +187 -0
  287. package/src/anthropic-auth-state.ts +386 -0
  288. package/src/bin.ts +182 -0
  289. package/src/btw-prefix-detection.test.ts +73 -0
  290. package/src/btw-prefix-detection.ts +23 -0
  291. package/src/channel-management.ts +376 -0
  292. package/src/cli-parsing.test.ts +197 -0
  293. package/src/cli-send-thread.e2e.test.ts +463 -0
  294. package/src/cli-telegram-options.test.ts +114 -0
  295. package/src/cli.ts +5718 -580
  296. package/src/commands/abort.ts +89 -0
  297. package/src/commands/action-buttons.ts +364 -0
  298. package/src/commands/add-dir.test.ts +154 -0
  299. package/src/commands/add-dir.ts +175 -0
  300. package/src/commands/add-project.ts +149 -0
  301. package/src/commands/agent.ts +496 -0
  302. package/src/commands/ask-question.test.ts +111 -0
  303. package/src/commands/ask-question.ts +455 -0
  304. package/src/commands/btw.ts +184 -0
  305. package/src/commands/cli-commands-group-a.test.ts +837 -0
  306. package/src/commands/cli-commands-group-b.test.ts +800 -0
  307. package/src/commands/compact.ts +157 -0
  308. package/src/commands/context-usage.ts +199 -0
  309. package/src/commands/create-new-project.ts +190 -0
  310. package/src/commands/diff.ts +91 -0
  311. package/src/commands/discord-commands-group-a.test.ts +751 -0
  312. package/src/commands/discord-commands-group-b.test.ts +648 -0
  313. package/src/commands/discord-commands-group-c.test.ts +882 -0
  314. package/src/commands/file-upload.ts +389 -0
  315. package/src/commands/fork-subagent.ts +263 -0
  316. package/src/commands/fork.ts +386 -0
  317. package/src/commands/gemini-apikey.ts +104 -0
  318. package/src/commands/login.ts +1175 -0
  319. package/src/commands/mcp.ts +307 -0
  320. package/src/commands/memory-snapshot.ts +30 -0
  321. package/src/commands/mention-mode.ts +68 -0
  322. package/src/commands/merge-worktree.ts +226 -0
  323. package/src/commands/model-variant.ts +485 -0
  324. package/src/commands/model.ts +1078 -0
  325. package/src/commands/new-worktree.ts +645 -0
  326. package/src/commands/paginated-select.ts +81 -0
  327. package/src/commands/permissions.ts +397 -0
  328. package/src/commands/queue.ts +293 -0
  329. package/src/commands/remove-project.ts +155 -0
  330. package/src/commands/restart-opencode-server.ts +162 -0
  331. package/src/commands/resume.ts +230 -0
  332. package/src/commands/run-command.ts +123 -0
  333. package/src/commands/screenshare.test.ts +30 -0
  334. package/src/commands/screenshare.ts +366 -0
  335. package/src/commands/session-id.ts +109 -0
  336. package/src/commands/session.ts +227 -0
  337. package/src/commands/share.ts +106 -0
  338. package/src/commands/tasks.ts +293 -0
  339. package/src/commands/thread-deletion-sync.ts +80 -0
  340. package/src/commands/types.ts +25 -0
  341. package/src/commands/undo-redo.ts +386 -0
  342. package/src/commands/unset-model.ts +174 -0
  343. package/src/commands/upgrade.ts +59 -0
  344. package/src/commands/user-command.ts +198 -0
  345. package/src/commands/verbosity.ts +173 -0
  346. package/src/commands/vscode.ts +342 -0
  347. package/src/commands/worktree-settings.ts +70 -0
  348. package/src/commands/worktrees.ts +645 -0
  349. package/src/condense-memory.ts +36 -0
  350. package/src/config.ts +103 -339
  351. package/src/context-awareness-plugin.test.ts +144 -0
  352. package/src/context-awareness-plugin.ts +469 -0
  353. package/src/critique-utils.ts +139 -0
  354. package/src/database.ts +1949 -0
  355. package/src/db.test.ts +162 -0
  356. package/src/db.ts +295 -0
  357. package/src/debounce-timeout.ts +43 -0
  358. package/src/debounced-process-flush.ts +104 -0
  359. package/src/discord-bot.ts +1505 -0
  360. package/src/discord-command-registration.ts +752 -0
  361. package/src/discord-urls.ts +89 -0
  362. package/src/discord-utils.test.ts +153 -0
  363. package/src/discord-utils.ts +846 -0
  364. package/src/errors.ts +201 -0
  365. package/src/escape-backticks.test.ts +469 -0
  366. package/src/event-stream-real-capture.e2e.test.ts +692 -0
  367. package/src/eventsource-parser.test.ts +351 -0
  368. package/src/exec-async.ts +35 -0
  369. package/src/external-opencode-sync.ts +685 -0
  370. package/src/format-tables.test.ts +515 -0
  371. package/src/format-tables.ts +718 -0
  372. package/src/forum-sync/config.ts +92 -0
  373. package/src/forum-sync/discord-operations.ts +241 -0
  374. package/src/forum-sync/index.ts +9 -0
  375. package/src/forum-sync/markdown.ts +172 -0
  376. package/src/forum-sync/sync-to-discord.ts +595 -0
  377. package/src/forum-sync/sync-to-files.ts +294 -0
  378. package/src/forum-sync/types.ts +175 -0
  379. package/src/forum-sync/watchers.ts +454 -0
  380. package/src/gateway-proxy-reconnect.e2e.test.ts +523 -0
  381. package/src/gateway-proxy.e2e.test.ts +644 -0
  382. package/src/genai-worker-wrapper.ts +164 -0
  383. package/src/genai-worker.ts +386 -0
  384. package/src/genai.ts +321 -0
  385. package/src/generated/browser.ts +114 -0
  386. package/src/generated/client.ts +138 -0
  387. package/src/generated/commonInputTypes.ts +770 -0
  388. package/src/generated/enums.ts +98 -0
  389. package/src/generated/internal/class.ts +384 -0
  390. package/src/generated/internal/prismaNamespace.ts +2394 -0
  391. package/src/generated/internal/prismaNamespaceBrowser.ts +327 -0
  392. package/src/generated/models/bot_api_keys.ts +1288 -0
  393. package/src/generated/models/bot_tokens.ts +1700 -0
  394. package/src/generated/models/channel_agents.ts +1256 -0
  395. package/src/generated/models/channel_directories.ts +1859 -0
  396. package/src/generated/models/channel_mention_mode.ts +1300 -0
  397. package/src/generated/models/channel_models.ts +1288 -0
  398. package/src/generated/models/channel_verbosity.ts +1228 -0
  399. package/src/generated/models/channel_worktrees.ts +1300 -0
  400. package/src/generated/models/forum_sync_configs.ts +1452 -0
  401. package/src/generated/models/global_models.ts +1288 -0
  402. package/src/generated/models/ipc_requests.ts +1485 -0
  403. package/src/generated/models/part_messages.ts +1302 -0
  404. package/src/generated/models/scheduled_tasks.ts +2320 -0
  405. package/src/generated/models/session_agents.ts +1086 -0
  406. package/src/generated/models/session_events.ts +1439 -0
  407. package/src/generated/models/session_models.ts +1114 -0
  408. package/src/generated/models/session_start_sources.ts +1408 -0
  409. package/src/generated/models/thread_sessions.ts +1781 -0
  410. package/src/generated/models/thread_worktrees.ts +1356 -0
  411. package/src/generated/models.ts +30 -0
  412. package/src/heap-monitor.ts +152 -0
  413. package/src/hrana-server.test.ts +434 -0
  414. package/src/hrana-server.ts +299 -0
  415. package/src/html-actions.test.ts +87 -0
  416. package/src/html-actions.ts +174 -0
  417. package/src/html-components.test.ts +38 -0
  418. package/src/html-components.ts +181 -0
  419. package/src/image-optimizer-plugin.ts +194 -0
  420. package/src/image-utils.ts +149 -0
  421. package/src/interaction-handler.ts +610 -0
  422. package/src/ipc-polling.ts +427 -0
  423. package/src/ipc-tools-plugin.ts +236 -0
  424. package/src/ipc-utils.ts +29 -0
  425. package/src/limit-heading-depth.test.ts +116 -0
  426. package/src/limit-heading-depth.ts +26 -0
  427. package/src/logger.ts +215 -0
  428. package/src/markdown.test.ts +315 -0
  429. package/src/markdown.ts +410 -0
  430. package/src/memory-overview-plugin.ts +163 -0
  431. package/src/message-finish-field.e2e.test.ts +195 -0
  432. package/src/message-formatting.test.ts +126 -0
  433. package/src/message-formatting.ts +535 -0
  434. package/src/message-preprocessing.ts +488 -0
  435. package/src/onboarding-tutorial.ts +167 -0
  436. package/src/onboarding-welcome.ts +49 -0
  437. package/src/openai-realtime.ts +358 -0
  438. package/src/opencode-command-detection.test.ts +307 -0
  439. package/src/opencode-command-detection.ts +76 -0
  440. package/src/opencode-command.test.ts +70 -0
  441. package/src/opencode-command.ts +191 -0
  442. package/src/opencode-interrupt-plugin.test.ts +682 -0
  443. package/src/opencode-interrupt-plugin.ts +507 -0
  444. package/src/opencode.ts +1453 -0
  445. package/src/otto/branding.ts +23 -0
  446. package/src/otto/index.ts +22 -0
  447. package/src/otto-digital-twin.e2e.test.ts +199 -0
  448. package/src/otto-opencode-plugin-loading.e2e.test.ts +117 -0
  449. package/src/otto-opencode-plugin.test.ts +108 -0
  450. package/src/otto-opencode-plugin.ts +22 -0
  451. package/src/parse-permission-rules.test.ts +127 -0
  452. package/src/patch-text-parser.ts +107 -0
  453. package/src/plugin-logger.ts +84 -0
  454. package/src/privacy-sanitizer.ts +142 -0
  455. package/src/queue-advanced-abort.e2e.test.ts +382 -0
  456. package/src/queue-advanced-action-buttons.e2e.test.ts +268 -0
  457. package/src/queue-advanced-e2e-setup.ts +877 -0
  458. package/src/queue-advanced-footer.e2e.test.ts +591 -0
  459. package/src/queue-advanced-model-switch.e2e.test.ts +383 -0
  460. package/src/queue-advanced-permissions-typing.e2e.test.ts +246 -0
  461. package/src/queue-advanced-question.e2e.test.ts +316 -0
  462. package/src/queue-advanced-typing-interrupt.e2e.test.ts +146 -0
  463. package/src/queue-advanced-typing.e2e.test.ts +199 -0
  464. package/src/queue-drain-after-interactive-ui.e2e.test.ts +151 -0
  465. package/src/queue-interrupt-drain.e2e.test.ts +166 -0
  466. package/src/queue-question-select-drain.e2e.test.ts +327 -0
  467. package/src/runtime-idle-sweeper.ts +76 -0
  468. package/src/runtime-lifecycle.e2e.test.ts +651 -0
  469. package/src/schema.sql +174 -0
  470. package/src/sentry.ts +26 -0
  471. package/src/session-handler/agent-utils.ts +99 -0
  472. package/src/session-handler/event-stream-fixtures/real-session-action-buttons.jsonl +45 -0
  473. package/src/session-handler/event-stream-fixtures/real-session-footer-suppressed-on-pre-idle-interrupt.jsonl +40 -0
  474. package/src/session-handler/event-stream-fixtures/real-session-permission-external-file.jsonl +23 -0
  475. package/src/session-handler/event-stream-fixtures/real-session-task-normal.jsonl +22 -0
  476. package/src/session-handler/event-stream-fixtures/real-session-task-three-parallel-sleeps.jsonl +277 -0
  477. package/src/session-handler/event-stream-fixtures/real-session-task-user-interruption.jsonl +46 -0
  478. package/src/session-handler/event-stream-fixtures/session-abort-after-idle-race.jsonl +21 -0
  479. package/src/session-handler/event-stream-fixtures/session-concurrent-messages-serialized.jsonl +56 -0
  480. package/src/session-handler/event-stream-fixtures/session-explicit-abort.jsonl +44 -0
  481. package/src/session-handler/event-stream-fixtures/session-normal-completion.jsonl +29 -0
  482. package/src/session-handler/event-stream-fixtures/session-tool-call-noisy-stream.jsonl +29 -0
  483. package/src/session-handler/event-stream-fixtures/session-two-completions-same-session.jsonl +50 -0
  484. package/src/session-handler/event-stream-fixtures/session-user-interruption.jsonl +59 -0
  485. package/src/session-handler/event-stream-fixtures/session-voice-queued-followup.jsonl +52 -0
  486. package/src/session-handler/event-stream-state.test.ts +717 -0
  487. package/src/session-handler/event-stream-state.ts +706 -0
  488. package/src/session-handler/model-utils.ts +217 -0
  489. package/src/session-handler/opencode-session-event-log.ts +130 -0
  490. package/src/session-handler/thread-runtime-state.ts +247 -0
  491. package/src/session-handler/thread-session-runtime.ts +4440 -0
  492. package/src/session-handler.ts +15 -0
  493. package/src/session-search.test.ts +50 -0
  494. package/src/session-search.ts +148 -0
  495. package/src/session-title-rename.test.ts +130 -0
  496. package/src/skill-filter.test.ts +83 -0
  497. package/src/skill-filter.ts +42 -0
  498. package/src/startup-service.ts +200 -0
  499. package/src/startup-time.e2e.test.ts +373 -0
  500. package/src/store.ts +139 -0
  501. package/src/subagent-rate-limit-plugin.ts +218 -0
  502. package/src/system-message.test.ts +710 -0
  503. package/src/system-message.ts +814 -0
  504. package/src/task-runner.ts +725 -0
  505. package/src/task-schedule.test.ts +84 -0
  506. package/src/task-schedule.ts +317 -0
  507. package/src/test-utils.ts +451 -0
  508. package/src/thinking-utils.ts +61 -0
  509. package/src/thread-message-queue.e2e.test.ts +1350 -0
  510. package/src/tools.ts +430 -0
  511. package/src/undici.d.ts +12 -0
  512. package/src/undo-redo.e2e.test.ts +209 -0
  513. package/src/unnest-code-blocks.test.ts +713 -0
  514. package/src/unnest-code-blocks.ts +185 -0
  515. package/src/upgrade.ts +185 -0
  516. package/src/utils.test.ts +155 -0
  517. package/src/utils.ts +265 -0
  518. package/src/voice-attachment.ts +51 -0
  519. package/src/voice-handler.ts +908 -0
  520. package/src/voice-message.e2e.test.ts +1255 -0
  521. package/src/voice.test.ts +281 -0
  522. package/src/voice.ts +638 -0
  523. package/src/wait-session.ts +273 -0
  524. package/src/websockify.ts +101 -0
  525. package/src/worker-types.ts +64 -0
  526. package/src/worktree-lifecycle.e2e.test.ts +396 -0
  527. package/src/worktree-utils.ts +4 -0
  528. package/src/worktrees.test.ts +489 -0
  529. package/src/worktrees.ts +1370 -0
  530. package/src/xml.test.ts +38 -0
  531. package/src/xml.ts +121 -0
  532. package/dist/cli.d.ts +0 -3
  533. package/dist/cli.d.ts.map +0 -1
  534. package/dist/cli.js.map +0 -1
  535. package/dist/config.d.ts +0 -39
  536. package/dist/config.d.ts.map +0 -1
  537. package/dist/config.js.map +0 -1
  538. package/dist/config.test.d.ts +0 -2
  539. package/dist/config.test.d.ts.map +0 -1
  540. package/dist/config.test.js +0 -202
  541. package/dist/config.test.js.map +0 -1
  542. package/dist/detect.d.ts +0 -9
  543. package/dist/detect.d.ts.map +0 -1
  544. package/dist/detect.js +0 -40
  545. package/dist/detect.js.map +0 -1
  546. package/dist/detect.test.d.ts +0 -2
  547. package/dist/detect.test.d.ts.map +0 -1
  548. package/dist/detect.test.js +0 -26
  549. package/dist/detect.test.js.map +0 -1
  550. package/dist/docker.d.ts +0 -7
  551. package/dist/docker.d.ts.map +0 -1
  552. package/dist/docker.js +0 -17
  553. package/dist/docker.js.map +0 -1
  554. package/dist/docker.test.d.ts +0 -2
  555. package/dist/docker.test.d.ts.map +0 -1
  556. package/dist/docker.test.js +0 -12
  557. package/dist/docker.test.js.map +0 -1
  558. package/dist/health.d.ts +0 -31
  559. package/dist/health.d.ts.map +0 -1
  560. package/dist/health.js +0 -117
  561. package/dist/health.js.map +0 -1
  562. package/dist/health.test.d.ts +0 -2
  563. package/dist/health.test.d.ts.map +0 -1
  564. package/dist/health.test.js +0 -52
  565. package/dist/health.test.js.map +0 -1
  566. package/dist/index.d.ts +0 -20
  567. package/dist/index.d.ts.map +0 -1
  568. package/dist/index.js +0 -15
  569. package/dist/index.js.map +0 -1
  570. package/dist/index.test.d.ts +0 -2
  571. package/dist/index.test.d.ts.map +0 -1
  572. package/dist/index.test.js +0 -8
  573. package/dist/index.test.js.map +0 -1
  574. package/dist/installer.d.ts +0 -10
  575. package/dist/installer.d.ts.map +0 -1
  576. package/dist/installer.js +0 -50
  577. package/dist/installer.js.map +0 -1
  578. package/dist/installer.test.d.ts +0 -2
  579. package/dist/installer.test.d.ts.map +0 -1
  580. package/dist/installer.test.js +0 -43
  581. package/dist/installer.test.js.map +0 -1
  582. package/dist/lifecycle.d.ts +0 -10
  583. package/dist/lifecycle.d.ts.map +0 -1
  584. package/dist/lifecycle.js +0 -45
  585. package/dist/lifecycle.js.map +0 -1
  586. package/dist/lifecycle.test.d.ts +0 -2
  587. package/dist/lifecycle.test.d.ts.map +0 -1
  588. package/dist/lifecycle.test.js +0 -20
  589. package/dist/lifecycle.test.js.map +0 -1
  590. package/dist/manifest.d.ts +0 -18
  591. package/dist/manifest.d.ts.map +0 -1
  592. package/dist/manifest.js +0 -30
  593. package/dist/manifest.js.map +0 -1
  594. package/dist/skills-baseline.d.ts +0 -7
  595. package/dist/skills-baseline.d.ts.map +0 -1
  596. package/dist/skills-baseline.js +0 -9
  597. package/dist/skills-baseline.js.map +0 -1
  598. package/dist/skills.d.ts +0 -110
  599. package/dist/skills.d.ts.map +0 -1
  600. package/dist/skills.js +0 -429
  601. package/dist/skills.js.map +0 -1
  602. package/dist/skills.test.d.ts +0 -2
  603. package/dist/skills.test.d.ts.map +0 -1
  604. package/dist/skills.test.js +0 -416
  605. package/dist/skills.test.js.map +0 -1
  606. package/dist/sync.d.ts +0 -10
  607. package/dist/sync.d.ts.map +0 -1
  608. package/dist/sync.js +0 -39
  609. package/dist/sync.js.map +0 -1
  610. package/dist/tenant.d.ts +0 -13
  611. package/dist/tenant.d.ts.map +0 -1
  612. package/dist/tenant.js +0 -105
  613. package/dist/tenant.js.map +0 -1
  614. package/dist/tenant.test.d.ts +0 -2
  615. package/dist/tenant.test.d.ts.map +0 -1
  616. package/dist/tenant.test.js +0 -37
  617. package/dist/tenant.test.js.map +0 -1
  618. package/src/config.test.ts +0 -237
  619. package/src/detect.test.ts +0 -29
  620. package/src/detect.ts +0 -52
  621. package/src/docker.test.ts +0 -12
  622. package/src/docker.ts +0 -23
  623. package/src/health.test.ts +0 -61
  624. package/src/health.ts +0 -158
  625. package/src/index.test.ts +0 -8
  626. package/src/index.ts +0 -62
  627. package/src/installer.test.ts +0 -52
  628. package/src/installer.ts +0 -62
  629. package/src/lifecycle.test.ts +0 -23
  630. package/src/lifecycle.ts +0 -49
  631. package/src/manifest.ts +0 -42
  632. package/src/skills-baseline.ts +0 -14
  633. package/src/skills.test.ts +0 -503
  634. package/src/skills.ts +0 -512
  635. package/src/sync.ts +0 -53
  636. package/src/tenant.test.ts +0 -49
  637. package/src/tenant.ts +0 -120
@@ -0,0 +1,107 @@
1
+ // Shared apply_patch text parsing utilities.
2
+ // Used by diff-patch-plugin.ts (file path extraction for snapshots) and
3
+ // message-formatting.ts (per-file addition/deletion counts for Discord display).
4
+ //
5
+ // The apply_patch tool uses three path header formats:
6
+ // *** Add File: path — new file
7
+ // *** Update File: path — existing file edit
8
+ // *** Delete File: path — file removal
9
+ // *** Move to: path — rename destination
10
+ // --- a/path / +++ b/path — unified diff headers (fallback)
11
+
12
+ /**
13
+ * Extract all file paths referenced in a patchText string.
14
+ * Handles custom apply_patch headers, move targets, and unified diff headers.
15
+ * Returns deduplicated paths.
16
+ */
17
+ export function extractPatchFilePaths(patchText: string): string[] {
18
+ const custom = [
19
+ ...patchText.matchAll(
20
+ /^\*\*\* (?:Add|Update|Delete) File:\s+(.+)$/gm,
21
+ ),
22
+ ].map((m) => {
23
+ return (m[1] ?? '').trim()
24
+ })
25
+ const moved = [
26
+ ...patchText.matchAll(/^\*\*\* Move to:\s+(.+)$/gm),
27
+ ].map((m) => {
28
+ return (m[1] ?? '').trim()
29
+ })
30
+ const unified = [
31
+ ...patchText.matchAll(/^(?:---|\+\+\+) [ab]\/(.+)$/gm),
32
+ ].map((m) => {
33
+ return (m[1] ?? '').trim()
34
+ })
35
+ const all = [...custom, ...moved, ...unified].filter(Boolean)
36
+ return all.filter((v, i, a) => {
37
+ return a.indexOf(v) === i
38
+ })
39
+ }
40
+
41
+ /**
42
+ * Parse a patchText string and count additions/deletions per file.
43
+ * Patch format uses `*** Add File:`, `*** Update File:`, `*** Delete File:` headers,
44
+ * with diff lines prefixed by `+` (addition) or `-` (deletion) inside `@@` hunks.
45
+ */
46
+ export function parsePatchFileCounts(
47
+ patchText: string,
48
+ ): Map<string, { additions: number; deletions: number }> {
49
+ const counts = new Map<string, { additions: number; deletions: number }>()
50
+ const lines = patchText.split('\n')
51
+ let currentFile = ''
52
+ let currentType = ''
53
+ let inHunk = false
54
+
55
+ for (const line of lines) {
56
+ const addMatch = line.match(/^\*\*\* Add File:\s*(.+)/)
57
+ const updateMatch = line.match(/^\*\*\* Update File:\s*(.+)/)
58
+ const deleteMatch = line.match(/^\*\*\* Delete File:\s*(.+)/)
59
+
60
+ if (addMatch || updateMatch || deleteMatch) {
61
+ const match = addMatch || updateMatch || deleteMatch
62
+ currentFile = (match?.[1] ?? '').trim()
63
+ currentType = addMatch ? 'add' : updateMatch ? 'update' : 'delete'
64
+ counts.set(currentFile, { additions: 0, deletions: 0 })
65
+ inHunk = false
66
+ continue
67
+ }
68
+
69
+ if (line.startsWith('@@')) {
70
+ inHunk = true
71
+ continue
72
+ }
73
+
74
+ if (line.startsWith('*** ')) {
75
+ inHunk = false
76
+ continue
77
+ }
78
+
79
+ if (!currentFile) {
80
+ continue
81
+ }
82
+
83
+ const entry = counts.get(currentFile)
84
+ if (!entry) {
85
+ continue
86
+ }
87
+
88
+ if (currentType === 'add') {
89
+ // all content lines in Add File are additions
90
+ if (line.length > 0 && !line.startsWith('*** ')) {
91
+ entry.additions++
92
+ }
93
+ } else if (currentType === 'delete') {
94
+ // all content lines in Delete File are deletions
95
+ if (line.length > 0 && !line.startsWith('*** ')) {
96
+ entry.deletions++
97
+ }
98
+ } else if (inHunk) {
99
+ if (line.startsWith('+')) {
100
+ entry.additions++
101
+ } else if (line.startsWith('-')) {
102
+ entry.deletions++
103
+ }
104
+ }
105
+ }
106
+ return counts
107
+ }
@@ -0,0 +1,84 @@
1
+ import fs from 'node:fs'
2
+ import path from 'node:path'
3
+ import util from 'node:util'
4
+ import { sanitizeSensitiveText, sanitizeUnknownValue } from './privacy-sanitizer.js'
5
+
6
+ let pluginLogFilePath: string | null = null
7
+
8
+ export function setPluginLogFilePath(dataDir: string): void {
9
+ pluginLogFilePath = path.join(dataDir, 'otto.log')
10
+ }
11
+
12
+ function formatArg(arg: unknown): string {
13
+ if (typeof arg === 'string') {
14
+ return sanitizeSensitiveText(arg, { redactPaths: false })
15
+ }
16
+ const safeArg = sanitizeUnknownValue(arg, { redactPaths: false })
17
+ return util.inspect(safeArg, { colors: false, depth: 4 })
18
+ }
19
+
20
+ export function formatPluginErrorWithStack(error: unknown): string {
21
+ if (error instanceof Error) {
22
+ return sanitizeSensitiveText(
23
+ error.stack ?? `${error.name}: ${error.message}`,
24
+ { redactPaths: false },
25
+ )
26
+ }
27
+ if (typeof error === 'string') {
28
+ return sanitizeSensitiveText(error, { redactPaths: false })
29
+ }
30
+
31
+ const safeError = sanitizeUnknownValue(error, { redactPaths: false })
32
+ return sanitizeSensitiveText(util.inspect(safeError, { colors: false, depth: 4 }), {
33
+ redactPaths: false,
34
+ })
35
+ }
36
+
37
+ function writeToFile(level: string, prefix: string, args: unknown[]) {
38
+ if (!pluginLogFilePath) {
39
+ return
40
+ }
41
+ const timestamp = new Date().toISOString()
42
+ const message = `[${timestamp}] [${level}] [${prefix}] ${args.map(formatArg).join(' ')}\n`
43
+ try {
44
+ fs.appendFileSync(pluginLogFilePath, message)
45
+ } catch {
46
+ // Plugin logging must never break the OpenCode plugin process.
47
+ }
48
+ }
49
+
50
+ export function createPluginLogger(prefix: string) {
51
+ return {
52
+ log: (...args: unknown[]) => {
53
+ writeToFile('LOG', prefix, args)
54
+ },
55
+ info: (...args: unknown[]) => {
56
+ writeToFile('INFO', prefix, args)
57
+ },
58
+ warn: (...args: unknown[]) => {
59
+ writeToFile('WARN', prefix, args)
60
+ },
61
+ error: (...args: unknown[]) => {
62
+ writeToFile('ERROR', prefix, args)
63
+ },
64
+ debug: (...args: unknown[]) => {
65
+ writeToFile('DEBUG', prefix, args)
66
+ },
67
+ }
68
+ }
69
+
70
+ // Append a session ID marker at the end of a toast message so the bot-side
71
+ // handleTuiToast can route the toast to the correct Discord thread.
72
+ // Without this marker the toast is silently dropped.
73
+ export function appendToastSessionMarker({
74
+ message,
75
+ sessionId,
76
+ }: {
77
+ message: string
78
+ sessionId: string | undefined
79
+ }): string {
80
+ if (!sessionId) {
81
+ return message
82
+ }
83
+ return `${message} ${sessionId}`
84
+ }
@@ -0,0 +1,142 @@
1
+ // Sensitive data redaction helpers for logs and telemetry payloads.
2
+ // Redacts common secrets, identifiers, emails, and can optionally redact paths.
3
+
4
+ const CORE_SENSITIVE_REPLACEMENTS: Array<{
5
+ pattern: RegExp
6
+ replacement: string
7
+ }> = [
8
+ {
9
+ pattern: /\bBearer\s+[A-Za-z0-9._-]{10,}\b/gi,
10
+ replacement: 'Bearer [REDACTED]',
11
+ },
12
+ {
13
+ pattern: /\bsk-[A-Za-z0-9]{16,}\b/g,
14
+ replacement: '[REDACTED_OPENAI_KEY]',
15
+ },
16
+ {
17
+ pattern: /\bAIza[0-9A-Za-z_-]{20,}\b/g,
18
+ replacement: '[REDACTED_GOOGLE_KEY]',
19
+ },
20
+ {
21
+ pattern: /\bgh[pousr]_[A-Za-z0-9]{20,}\b/g,
22
+ replacement: '[REDACTED_GITHUB_TOKEN]',
23
+ },
24
+ {
25
+ pattern:
26
+ /([?&](?:token|api[_-]?key|key|secret|password|authorization)=)[^&\s]+/gi,
27
+ replacement: '$1[REDACTED]',
28
+ },
29
+ {
30
+ pattern:
31
+ /(\b(?:token|api[_-]?key|secret|password|authorization)\b\s*[:=]\s*")([^"]+)(")/gi,
32
+ replacement: '$1[REDACTED]$3',
33
+ },
34
+ {
35
+ pattern:
36
+ /(\b(?:token|api[_-]?key|secret|password|authorization)\b\s*[:=]\s*)([^\s,;]+)/gi,
37
+ replacement: '$1[REDACTED]',
38
+ },
39
+ ]
40
+
41
+ const PATH_REPLACEMENTS: Array<{
42
+ pattern: RegExp
43
+ replacement: string
44
+ }> = [
45
+ {
46
+ pattern: /\/(?:Users|home)\/[^/\s]+\/[^\s'"`)]*/g,
47
+ replacement: '[REDACTED_PATH]',
48
+ },
49
+ {
50
+ pattern: /[A-Za-z]:\\[^\s'"`)]*/g,
51
+ replacement: '[REDACTED_PATH]',
52
+ },
53
+ ]
54
+
55
+ export function sanitizeSensitiveText(
56
+ value: string,
57
+ { redactPaths = false }: { redactPaths?: boolean } = {},
58
+ ): string {
59
+ const replacements = redactPaths
60
+ ? [...CORE_SENSITIVE_REPLACEMENTS, ...PATH_REPLACEMENTS]
61
+ : CORE_SENSITIVE_REPLACEMENTS
62
+ return replacements.reduce((current, entry) => {
63
+ return current.replace(entry.pattern, entry.replacement)
64
+ }, value)
65
+ }
66
+
67
+ export function sanitizeUnknownValue(
68
+ value: unknown,
69
+ {
70
+ depth = 0,
71
+ seen = new WeakSet<object>(),
72
+ redactPaths = false,
73
+ }: {
74
+ depth?: number
75
+ seen?: WeakSet<object>
76
+ redactPaths?: boolean
77
+ } = {},
78
+ ): unknown {
79
+ if (depth > 8) {
80
+ return '[REDACTED_DEPTH_LIMIT]'
81
+ }
82
+
83
+ if (typeof value === 'string') {
84
+ return sanitizeSensitiveText(value, { redactPaths })
85
+ }
86
+
87
+ if (
88
+ typeof value === 'number' ||
89
+ typeof value === 'boolean' ||
90
+ value === null ||
91
+ value === undefined
92
+ ) {
93
+ return value
94
+ }
95
+
96
+ if (value instanceof Date) {
97
+ return value.toISOString()
98
+ }
99
+
100
+ if (value instanceof Error) {
101
+ const sanitizedStack = value.stack
102
+ ? sanitizeSensitiveText(value.stack, { redactPaths })
103
+ : undefined
104
+ return {
105
+ name: value.name,
106
+ message: sanitizeSensitiveText(value.message, { redactPaths }),
107
+ stack: sanitizedStack,
108
+ cause: sanitizeUnknownValue(value.cause, {
109
+ depth: depth + 1,
110
+ seen,
111
+ redactPaths,
112
+ }),
113
+ }
114
+ }
115
+
116
+ if (Array.isArray(value)) {
117
+ return value.map((item) => {
118
+ return sanitizeUnknownValue(item, { depth: depth + 1, seen, redactPaths })
119
+ })
120
+ }
121
+
122
+ if (typeof value === 'object') {
123
+ if (seen.has(value)) {
124
+ return '[REDACTED_CIRCULAR]'
125
+ }
126
+ seen.add(value)
127
+
128
+ const sanitizedEntries = Object.entries(value).map(([key, entryValue]) => {
129
+ return [
130
+ key,
131
+ sanitizeUnknownValue(entryValue, {
132
+ depth: depth + 1,
133
+ seen,
134
+ redactPaths,
135
+ }),
136
+ ]
137
+ })
138
+ return Object.fromEntries(sanitizedEntries)
139
+ }
140
+
141
+ return sanitizeSensitiveText(String(value), { redactPaths })
142
+ }
@@ -0,0 +1,382 @@
1
+ // E2e tests for abort, model-switch, and retry scenarios.
2
+ // Split from thread-queue-advanced.e2e.test.ts for parallelization.
3
+
4
+ import { describe, test, expect } from 'vitest'
5
+ import {
6
+ setupQueueAdvancedSuite,
7
+ TEST_USER_ID,
8
+ } from './queue-advanced-e2e-setup.js'
9
+ import {
10
+ getRuntime,
11
+ } from './session-handler/thread-session-runtime.js'
12
+ import { getThreadState } from './session-handler/thread-runtime-state.js'
13
+ import { setSessionModel } from './database.js'
14
+ import {
15
+ waitForFooterMessage,
16
+ waitForBotMessageContaining,
17
+ waitForBotReplyAfterUserMessage,
18
+ } from './test-utils.js'
19
+
20
+ const TEXT_CHANNEL_ID = '200000000000001003'
21
+
22
+ const e2eTest = describe
23
+
24
+ e2eTest('queue advanced: abort and retry', () => {
25
+ const ctx = setupQueueAdvancedSuite({
26
+ channelId: TEXT_CHANNEL_ID,
27
+ channelName: 'qa-abort-e2e',
28
+ dirName: 'qa-abort-e2e',
29
+ username: 'queue-advanced-tester',
30
+ })
31
+
32
+ test(
33
+ 'slow tool call (sleep) gets aborted by explicit abort, then queue continues',
34
+ async () => {
35
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
36
+ content: 'Reply with exactly: oscar',
37
+ })
38
+
39
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
40
+ timeout: 4_000,
41
+ predicate: (t) => {
42
+ return t.name === 'Reply with exactly: oscar'
43
+ },
44
+ })
45
+
46
+ const th = ctx.discord.thread(thread.id)
47
+ const firstReply = await th.waitForBotReply({ timeout: 4_000 })
48
+ expect(firstReply.content.trim().length).toBeGreaterThan(0)
49
+
50
+ // Wait for the first completion footer so it lands in a deterministic position
51
+ await waitForFooterMessage({
52
+ discord: ctx.discord,
53
+ threadId: thread.id,
54
+ timeout: 4_000,
55
+ })
56
+
57
+ const before = await th.getMessages()
58
+ const beforeBotCount = before.filter((m) => {
59
+ return m.author.id === ctx.discord.botUserId
60
+ }).length
61
+
62
+ await th.user(TEST_USER_ID).sendMessage({
63
+ content: 'PLUGIN_TIMEOUT_SLEEP_MARKER',
64
+ })
65
+
66
+ // The matcher emits "starting sleep 100" text before the long delay.
67
+ // Wait for it to land in Discord BEFORE aborting so the message is in a
68
+ // deterministic position and the abort produces no further stray messages.
69
+ await waitForBotMessageContaining({
70
+ discord: ctx.discord,
71
+ threadId: thread.id,
72
+ userId: TEST_USER_ID,
73
+ text: 'starting sleep',
74
+ afterUserMessageIncludes: 'PLUGIN_TIMEOUT_SLEEP_MARKER',
75
+ timeout: 4_000,
76
+ })
77
+
78
+ const runtime = getRuntime(thread.id)
79
+ expect(runtime).toBeDefined()
80
+ if (!runtime) {
81
+ throw new Error('Expected runtime to exist for explicit-abort test')
82
+ }
83
+
84
+ runtime.abortActiveRun('test-explicit-abort')
85
+
86
+ await th.user(TEST_USER_ID).sendMessage({
87
+ content: 'Reply with exactly: papa',
88
+ })
89
+
90
+ const after = await waitForBotReplyAfterUserMessage({
91
+ discord: ctx.discord,
92
+ threadId: thread.id,
93
+ userId: TEST_USER_ID,
94
+ userMessageIncludes: 'papa',
95
+ timeout: 8_000,
96
+ })
97
+
98
+ const afterBotMessages = after.filter((m) => {
99
+ return m.author.id === ctx.discord.botUserId
100
+ })
101
+
102
+ await waitForFooterMessage({
103
+ discord: ctx.discord,
104
+ threadId: thread.id,
105
+ timeout: 8_000,
106
+ afterMessageIncludes: 'papa',
107
+ afterAuthorId: TEST_USER_ID,
108
+ })
109
+
110
+ // Assert ordering invariants instead of exact snapshot — the papa reply
111
+ // and footer can interleave non-deterministically.
112
+ const timeline = await th.text()
113
+ expect(timeline).toContain('Reply with exactly: oscar')
114
+ expect(timeline).toContain('PLUGIN_TIMEOUT_SLEEP_MARKER')
115
+ expect(timeline).toContain('⬥ starting sleep 100')
116
+ expect(timeline).toContain('Reply with exactly: papa')
117
+ expect(timeline).toContain('*project ⋅ main ⋅')
118
+ // oscar comes before the sleep marker, sleep before papa
119
+ const oscarIdx = timeline.indexOf('oscar')
120
+ const sleepIdx = timeline.indexOf('PLUGIN_TIMEOUT_SLEEP_MARKER')
121
+ const papaIdx = timeline.indexOf('papa')
122
+ expect(oscarIdx).toBeLessThan(sleepIdx)
123
+ expect(sleepIdx).toBeLessThan(papaIdx)
124
+ expect(afterBotMessages.length).toBeGreaterThanOrEqual(beforeBotCount + 1)
125
+
126
+ const sleepToolIndex = after.findIndex((m) => {
127
+ return (
128
+ m.author.id === TEST_USER_ID &&
129
+ m.content.includes('PLUGIN_TIMEOUT_SLEEP_MARKER')
130
+ )
131
+ })
132
+ expect(sleepToolIndex).toBeGreaterThan(-1)
133
+
134
+ const userPapaIndex = after.findIndex((m) => {
135
+ return m.author.id === TEST_USER_ID && m.content.includes('papa')
136
+ })
137
+ expect(userPapaIndex).toBeGreaterThan(-1)
138
+ expect(sleepToolIndex).toBeLessThan(userPapaIndex)
139
+ const lastBotIndex = after.findLastIndex((m) => {
140
+ return m.author.id === ctx.discord.botUserId
141
+ })
142
+ expect(userPapaIndex).toBeLessThan(lastBotIndex)
143
+ },
144
+ 12_000,
145
+ )
146
+
147
+ test(
148
+ 'explicit abort emits MessageAbortedError and does not emit footer',
149
+ async () => {
150
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
151
+ content: 'Reply with exactly: abort-no-footer-setup',
152
+ })
153
+
154
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
155
+ timeout: 4_000,
156
+ predicate: (t) => {
157
+ return t.name === 'Reply with exactly: abort-no-footer-setup'
158
+ },
159
+ })
160
+
161
+ const th = ctx.discord.thread(thread.id)
162
+ await th.waitForBotReply({ timeout: 4_000 })
163
+
164
+ await waitForBotMessageContaining({
165
+ discord: ctx.discord,
166
+ threadId: thread.id,
167
+ userId: TEST_USER_ID,
168
+ text: '⋅',
169
+ timeout: 4_000,
170
+ })
171
+
172
+ await th.user(TEST_USER_ID).sendMessage({
173
+ content: 'SLOW_ABORT_MARKER run long response',
174
+ })
175
+
176
+ const runtime = getRuntime(thread.id)
177
+ expect(runtime).toBeDefined()
178
+ if (!runtime) {
179
+ throw new Error('Expected runtime to exist for abort no-footer test')
180
+ }
181
+
182
+ const beforeAbortMessages = await th.getMessages()
183
+ const baselineCount = beforeAbortMessages.length
184
+
185
+ runtime.abortActiveRun('test-no-footer-on-abort')
186
+
187
+ for (let i = 0; i < 10; i++) {
188
+ await new Promise((resolve) => {
189
+ setTimeout(resolve, 20)
190
+ })
191
+ const msgs = await th.getMessages()
192
+ const newMsgs = msgs.slice(baselineCount)
193
+ const hasFooter = newMsgs.some((m) => {
194
+ return m.author.id === ctx.discord.botUserId
195
+ && m.content.startsWith('*')
196
+ && m.content.includes('⋅')
197
+ })
198
+ expect(hasFooter).toBe(false)
199
+ }
200
+
201
+ expect(await th.text()).toMatchInlineSnapshot(`
202
+ "--- from: user (queue-advanced-tester)
203
+ Reply with exactly: abort-no-footer-setup
204
+ --- from: assistant (TestBot)
205
+ ⬥ ok
206
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
207
+ --- from: user (queue-advanced-tester)
208
+ SLOW_ABORT_MARKER run long response"
209
+ `)
210
+ },
211
+ 10_000,
212
+ )
213
+
214
+ test.skip(
215
+ 'explicit abort stale-idle window: follow-up prompt still gets assistant text',
216
+ async () => {
217
+ const setupPrompt = 'Reply with exactly: race-setup-1'
218
+ const raceFinalPrompt = 'Reply with exactly: race-final-1'
219
+
220
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
221
+ content: setupPrompt,
222
+ })
223
+
224
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
225
+ timeout: 4_000,
226
+ predicate: (t) => {
227
+ return t.name === setupPrompt
228
+ },
229
+ })
230
+
231
+ const th = ctx.discord.thread(thread.id)
232
+ const setupReply = await th.waitForBotReply({ timeout: 4_000 })
233
+ expect(setupReply.content.trim().length).toBeGreaterThan(0)
234
+
235
+ await th.user(TEST_USER_ID).sendMessage({
236
+ content: 'SLOW_ABORT_MARKER run long response',
237
+ })
238
+
239
+ const runtime = getRuntime(thread.id)
240
+ expect(runtime).toBeDefined()
241
+ if (!runtime) {
242
+ throw new Error('Expected runtime to exist for race abort scenario')
243
+ }
244
+
245
+ runtime.abortActiveRun('test-race-abort')
246
+
247
+ await th.user(TEST_USER_ID).sendMessage({
248
+ content: raceFinalPrompt,
249
+ })
250
+
251
+ await waitForBotReplyAfterUserMessage({
252
+ discord: ctx.discord,
253
+ threadId: thread.id,
254
+ userId: TEST_USER_ID,
255
+ userMessageIncludes: raceFinalPrompt,
256
+ timeout: 4_000,
257
+ })
258
+ },
259
+ 8_000,
260
+ )
261
+
262
+ test(
263
+ 'model switch mid-session aborts and restarts from same session history',
264
+ async () => {
265
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
266
+ content: 'Reply with exactly: retry-setup',
267
+ })
268
+
269
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
270
+ timeout: 4_000,
271
+ predicate: (t) => {
272
+ return t.name === 'Reply with exactly: retry-setup'
273
+ },
274
+ })
275
+
276
+ const th = ctx.discord.thread(thread.id)
277
+ const firstReply = await th.waitForBotReply({ timeout: 4_000 })
278
+ expect(firstReply.content.trim().length).toBeGreaterThan(0)
279
+
280
+ await th.user(TEST_USER_ID).sendMessage({
281
+ content: 'PLUGIN_TIMEOUT_SLEEP_MARKER',
282
+ })
283
+
284
+ await waitForBotMessageContaining({
285
+ discord: ctx.discord,
286
+ threadId: thread.id,
287
+ userId: TEST_USER_ID,
288
+ text: 'starting sleep',
289
+ afterUserMessageIncludes: 'PLUGIN_TIMEOUT_SLEEP_MARKER',
290
+ timeout: 4_000,
291
+ })
292
+
293
+ const sessionId = getThreadState(thread.id)?.sessionId
294
+ expect(sessionId).toBeDefined()
295
+ if (!sessionId) {
296
+ throw new Error('Expected active session id for model switch test')
297
+ }
298
+
299
+ await setSessionModel({
300
+ sessionId,
301
+ modelId: 'deterministic-provider/deterministic-v3',
302
+ variant: null,
303
+ })
304
+
305
+ const runtime = getRuntime(thread.id)
306
+ expect(runtime).toBeDefined()
307
+ if (!runtime) {
308
+ throw new Error('Expected runtime to exist for model switch test')
309
+ }
310
+ const retried = await runtime.retryLastUserPrompt()
311
+ expect(retried).toBe(true)
312
+
313
+ await th.user(TEST_USER_ID).sendMessage({
314
+ content: 'Reply with exactly: model-switch-followup',
315
+ })
316
+
317
+ await waitForBotReplyAfterUserMessage({
318
+ discord: ctx.discord,
319
+ threadId: thread.id,
320
+ userId: TEST_USER_ID,
321
+ userMessageIncludes: 'model-switch-followup',
322
+ timeout: 4_000,
323
+ })
324
+
325
+ // Wait for potential footer to arrive (race between step-finish interrupt
326
+ // and model switch settling means footer may or may not appear).
327
+ await new Promise((resolve) => {
328
+ setTimeout(resolve, 200)
329
+ })
330
+
331
+ const text = await th.text()
332
+ // The follow-up reply ("ok") must be present with deterministic-v3
333
+ expect(text).toContain('Reply with exactly: model-switch-followup')
334
+ expect(text).toContain('⬥ ok')
335
+ // The old sleep text should be visible from the first turn
336
+ expect(text).toContain('starting sleep 100')
337
+ },
338
+ 10_000,
339
+ )
340
+
341
+ test(
342
+ 'abortActiveRun settles correctly during long-running request',
343
+ async () => {
344
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
345
+ content: 'Reply with exactly: force-abort-setup',
346
+ })
347
+
348
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
349
+ timeout: 4_000,
350
+ predicate: (t) => {
351
+ return t.name === 'Reply with exactly: force-abort-setup'
352
+ },
353
+ })
354
+
355
+ const th = ctx.discord.thread(thread.id)
356
+ const setupReply = await th.waitForBotReply({ timeout: 4_000 })
357
+ expect(setupReply.content.trim().length).toBeGreaterThan(0)
358
+
359
+ await th.user(TEST_USER_ID).sendMessage({
360
+ content: 'SLOW_ABORT_MARKER run long response',
361
+ })
362
+
363
+ const runtime = getRuntime(thread.id)
364
+ expect(runtime).toBeDefined()
365
+ if (!runtime) {
366
+ throw new Error('Expected runtime to exist for forced-abort test')
367
+ }
368
+
369
+ runtime.abortActiveRun('force-abort-test')
370
+
371
+ expect(await th.text()).toMatchInlineSnapshot(`
372
+ "--- from: user (queue-advanced-tester)
373
+ Reply with exactly: force-abort-setup
374
+ --- from: assistant (TestBot)
375
+ ⬥ ok
376
+ --- from: user (queue-advanced-tester)
377
+ SLOW_ABORT_MARKER run long response"
378
+ `)
379
+ },
380
+ 10_000,
381
+ )
382
+ })