@otto-assistant/bridge 0.4.92

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 (483) 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-auth-plugin.js +728 -0
  7. package/dist/anthropic-auth-plugin.test.js +125 -0
  8. package/dist/anthropic-auth-state.js +231 -0
  9. package/dist/bin.js +90 -0
  10. package/dist/channel-management.js +227 -0
  11. package/dist/cli-parsing.test.js +137 -0
  12. package/dist/cli-send-thread.e2e.test.js +356 -0
  13. package/dist/cli.js +3276 -0
  14. package/dist/commands/abort.js +65 -0
  15. package/dist/commands/action-buttons.js +245 -0
  16. package/dist/commands/add-project.js +113 -0
  17. package/dist/commands/agent.js +335 -0
  18. package/dist/commands/ask-question.js +274 -0
  19. package/dist/commands/btw.js +116 -0
  20. package/dist/commands/compact.js +120 -0
  21. package/dist/commands/context-usage.js +140 -0
  22. package/dist/commands/create-new-project.js +130 -0
  23. package/dist/commands/diff.js +63 -0
  24. package/dist/commands/file-upload.js +275 -0
  25. package/dist/commands/fork.js +220 -0
  26. package/dist/commands/gemini-apikey.js +70 -0
  27. package/dist/commands/login.js +885 -0
  28. package/dist/commands/mcp.js +239 -0
  29. package/dist/commands/memory-snapshot.js +24 -0
  30. package/dist/commands/mention-mode.js +44 -0
  31. package/dist/commands/merge-worktree.js +159 -0
  32. package/dist/commands/model-variant.js +364 -0
  33. package/dist/commands/model.js +776 -0
  34. package/dist/commands/new-worktree.js +366 -0
  35. package/dist/commands/paginated-select.js +57 -0
  36. package/dist/commands/permissions.js +274 -0
  37. package/dist/commands/queue.js +206 -0
  38. package/dist/commands/remove-project.js +115 -0
  39. package/dist/commands/restart-opencode-server.js +127 -0
  40. package/dist/commands/resume.js +149 -0
  41. package/dist/commands/run-command.js +79 -0
  42. package/dist/commands/screenshare.js +303 -0
  43. package/dist/commands/screenshare.test.js +20 -0
  44. package/dist/commands/session-id.js +78 -0
  45. package/dist/commands/session.js +176 -0
  46. package/dist/commands/share.js +80 -0
  47. package/dist/commands/tasks.js +205 -0
  48. package/dist/commands/types.js +2 -0
  49. package/dist/commands/undo-redo.js +305 -0
  50. package/dist/commands/unset-model.js +138 -0
  51. package/dist/commands/upgrade.js +42 -0
  52. package/dist/commands/user-command.js +155 -0
  53. package/dist/commands/verbosity.js +125 -0
  54. package/dist/commands/worktree-settings.js +43 -0
  55. package/dist/commands/worktrees.js +410 -0
  56. package/dist/condense-memory.js +33 -0
  57. package/dist/config.js +94 -0
  58. package/dist/context-awareness-plugin.js +363 -0
  59. package/dist/context-awareness-plugin.test.js +124 -0
  60. package/dist/critique-utils.js +95 -0
  61. package/dist/database.js +1310 -0
  62. package/dist/db.js +251 -0
  63. package/dist/db.test.js +138 -0
  64. package/dist/debounce-timeout.js +28 -0
  65. package/dist/debounced-process-flush.js +77 -0
  66. package/dist/discord-bot.js +1008 -0
  67. package/dist/discord-command-registration.js +524 -0
  68. package/dist/discord-urls.js +81 -0
  69. package/dist/discord-utils.js +591 -0
  70. package/dist/discord-utils.test.js +134 -0
  71. package/dist/errors.js +157 -0
  72. package/dist/escape-backticks.test.js +429 -0
  73. package/dist/event-stream-real-capture.e2e.test.js +533 -0
  74. package/dist/eventsource-parser.test.js +327 -0
  75. package/dist/exec-async.js +26 -0
  76. package/dist/external-opencode-sync.js +480 -0
  77. package/dist/format-tables.js +302 -0
  78. package/dist/format-tables.test.js +308 -0
  79. package/dist/forum-sync/config.js +79 -0
  80. package/dist/forum-sync/discord-operations.js +154 -0
  81. package/dist/forum-sync/index.js +5 -0
  82. package/dist/forum-sync/markdown.js +113 -0
  83. package/dist/forum-sync/sync-to-discord.js +417 -0
  84. package/dist/forum-sync/sync-to-files.js +190 -0
  85. package/dist/forum-sync/types.js +53 -0
  86. package/dist/forum-sync/watchers.js +307 -0
  87. package/dist/gateway-proxy-reconnect.e2e.test.js +394 -0
  88. package/dist/gateway-proxy.e2e.test.js +483 -0
  89. package/dist/genai-worker-wrapper.js +111 -0
  90. package/dist/genai-worker.js +311 -0
  91. package/dist/genai.js +232 -0
  92. package/dist/generated/browser.js +17 -0
  93. package/dist/generated/client.js +37 -0
  94. package/dist/generated/commonInputTypes.js +10 -0
  95. package/dist/generated/enums.js +52 -0
  96. package/dist/generated/internal/class.js +49 -0
  97. package/dist/generated/internal/prismaNamespace.js +253 -0
  98. package/dist/generated/internal/prismaNamespaceBrowser.js +223 -0
  99. package/dist/generated/models/bot_api_keys.js +1 -0
  100. package/dist/generated/models/bot_tokens.js +1 -0
  101. package/dist/generated/models/channel_agents.js +1 -0
  102. package/dist/generated/models/channel_directories.js +1 -0
  103. package/dist/generated/models/channel_mention_mode.js +1 -0
  104. package/dist/generated/models/channel_models.js +1 -0
  105. package/dist/generated/models/channel_verbosity.js +1 -0
  106. package/dist/generated/models/channel_worktrees.js +1 -0
  107. package/dist/generated/models/forum_sync_configs.js +1 -0
  108. package/dist/generated/models/global_models.js +1 -0
  109. package/dist/generated/models/ipc_requests.js +1 -0
  110. package/dist/generated/models/part_messages.js +1 -0
  111. package/dist/generated/models/scheduled_tasks.js +1 -0
  112. package/dist/generated/models/session_agents.js +1 -0
  113. package/dist/generated/models/session_events.js +1 -0
  114. package/dist/generated/models/session_models.js +1 -0
  115. package/dist/generated/models/session_start_sources.js +1 -0
  116. package/dist/generated/models/thread_sessions.js +1 -0
  117. package/dist/generated/models/thread_worktrees.js +1 -0
  118. package/dist/generated/models.js +1 -0
  119. package/dist/heap-monitor.js +122 -0
  120. package/dist/hrana-server.js +263 -0
  121. package/dist/hrana-server.test.js +370 -0
  122. package/dist/html-actions.js +123 -0
  123. package/dist/html-actions.test.js +70 -0
  124. package/dist/html-components.js +117 -0
  125. package/dist/html-components.test.js +34 -0
  126. package/dist/image-optimizer-plugin.js +153 -0
  127. package/dist/image-utils.js +112 -0
  128. package/dist/interaction-handler.js +397 -0
  129. package/dist/ipc-polling.js +252 -0
  130. package/dist/ipc-tools-plugin.js +193 -0
  131. package/dist/kimaki-digital-twin.e2e.test.js +161 -0
  132. package/dist/kimaki-opencode-plugin-loading.e2e.test.js +87 -0
  133. package/dist/kimaki-opencode-plugin.js +17 -0
  134. package/dist/kimaki-opencode-plugin.test.js +98 -0
  135. package/dist/limit-heading-depth.js +25 -0
  136. package/dist/limit-heading-depth.test.js +105 -0
  137. package/dist/logger.js +165 -0
  138. package/dist/markdown.js +342 -0
  139. package/dist/markdown.test.js +257 -0
  140. package/dist/message-finish-field.e2e.test.js +165 -0
  141. package/dist/message-formatting.js +413 -0
  142. package/dist/message-formatting.test.js +73 -0
  143. package/dist/message-preprocessing.js +330 -0
  144. package/dist/onboarding-tutorial.js +172 -0
  145. package/dist/onboarding-welcome.js +37 -0
  146. package/dist/openai-realtime.js +224 -0
  147. package/dist/opencode-command-detection.js +65 -0
  148. package/dist/opencode-command-detection.test.js +240 -0
  149. package/dist/opencode-command.js +129 -0
  150. package/dist/opencode-command.test.js +48 -0
  151. package/dist/opencode-interrupt-plugin.js +361 -0
  152. package/dist/opencode-interrupt-plugin.test.js +458 -0
  153. package/dist/opencode.js +861 -0
  154. package/dist/otto/branding.js +22 -0
  155. package/dist/otto/index.js +21 -0
  156. package/dist/parse-permission-rules.test.js +117 -0
  157. package/dist/patch-text-parser.js +97 -0
  158. package/dist/plugin-logger.js +59 -0
  159. package/dist/privacy-sanitizer.js +105 -0
  160. package/dist/queue-advanced-abort.e2e.test.js +293 -0
  161. package/dist/queue-advanced-action-buttons.e2e.test.js +206 -0
  162. package/dist/queue-advanced-e2e-setup.js +786 -0
  163. package/dist/queue-advanced-footer.e2e.test.js +472 -0
  164. package/dist/queue-advanced-model-switch.e2e.test.js +299 -0
  165. package/dist/queue-advanced-permissions-typing.e2e.test.js +180 -0
  166. package/dist/queue-advanced-question.e2e.test.js +261 -0
  167. package/dist/queue-advanced-typing-interrupt.e2e.test.js +114 -0
  168. package/dist/queue-advanced-typing.e2e.test.js +153 -0
  169. package/dist/queue-drain-after-interactive-ui.e2e.test.js +119 -0
  170. package/dist/queue-interrupt-drain.e2e.test.js +135 -0
  171. package/dist/queue-question-select-drain.e2e.test.js +120 -0
  172. package/dist/runtime-idle-sweeper.js +52 -0
  173. package/dist/runtime-lifecycle.e2e.test.js +508 -0
  174. package/dist/sentry.js +23 -0
  175. package/dist/session-handler/agent-utils.js +67 -0
  176. package/dist/session-handler/event-stream-state.js +420 -0
  177. package/dist/session-handler/event-stream-state.test.js +563 -0
  178. package/dist/session-handler/model-utils.js +124 -0
  179. package/dist/session-handler/opencode-session-event-log.js +94 -0
  180. package/dist/session-handler/thread-runtime-state.js +104 -0
  181. package/dist/session-handler/thread-session-runtime.js +3258 -0
  182. package/dist/session-handler.js +9 -0
  183. package/dist/session-search.js +100 -0
  184. package/dist/session-search.test.js +40 -0
  185. package/dist/session-title-rename.test.js +80 -0
  186. package/dist/startup-service.js +153 -0
  187. package/dist/startup-time.e2e.test.js +296 -0
  188. package/dist/store.js +17 -0
  189. package/dist/system-message.js +613 -0
  190. package/dist/system-message.test.js +602 -0
  191. package/dist/task-runner.js +295 -0
  192. package/dist/task-schedule.js +209 -0
  193. package/dist/task-schedule.test.js +71 -0
  194. package/dist/test-utils.js +299 -0
  195. package/dist/thinking-utils.js +35 -0
  196. package/dist/thread-message-queue.e2e.test.js +999 -0
  197. package/dist/tools.js +357 -0
  198. package/dist/undo-redo.e2e.test.js +161 -0
  199. package/dist/unnest-code-blocks.js +146 -0
  200. package/dist/unnest-code-blocks.test.js +673 -0
  201. package/dist/upgrade.js +114 -0
  202. package/dist/utils.js +144 -0
  203. package/dist/voice-attachment.js +34 -0
  204. package/dist/voice-handler.js +646 -0
  205. package/dist/voice-message.e2e.test.js +1021 -0
  206. package/dist/voice.js +447 -0
  207. package/dist/voice.test.js +235 -0
  208. package/dist/wait-session.js +94 -0
  209. package/dist/websockify.js +69 -0
  210. package/dist/worker-types.js +4 -0
  211. package/dist/worktree-lifecycle.e2e.test.js +308 -0
  212. package/dist/worktree-utils.js +3 -0
  213. package/dist/worktrees.js +929 -0
  214. package/dist/worktrees.test.js +189 -0
  215. package/dist/xml.js +92 -0
  216. package/dist/xml.test.js +32 -0
  217. package/package.json +98 -0
  218. package/schema.prisma +295 -0
  219. package/skills/batch/SKILL.md +87 -0
  220. package/skills/critique/SKILL.md +112 -0
  221. package/skills/egaki/SKILL.md +100 -0
  222. package/skills/errore/SKILL.md +647 -0
  223. package/skills/event-sourcing-state/SKILL.md +252 -0
  224. package/skills/gitchamber/SKILL.md +93 -0
  225. package/skills/goke/SKILL.md +644 -0
  226. package/skills/jitter/EDITOR.md +219 -0
  227. package/skills/jitter/EXPORT-INTERNALS.md +309 -0
  228. package/skills/jitter/SKILL.md +158 -0
  229. package/skills/jitter/jitter-clipboard.json +1042 -0
  230. package/skills/jitter/package.json +14 -0
  231. package/skills/jitter/tsconfig.json +15 -0
  232. package/skills/jitter/utils/actions.ts +212 -0
  233. package/skills/jitter/utils/export.ts +114 -0
  234. package/skills/jitter/utils/index.ts +141 -0
  235. package/skills/jitter/utils/snapshot.ts +154 -0
  236. package/skills/jitter/utils/traverse.ts +246 -0
  237. package/skills/jitter/utils/types.ts +279 -0
  238. package/skills/jitter/utils/wait.ts +133 -0
  239. package/skills/lintcn/SKILL.md +873 -0
  240. package/skills/new-skill/SKILL.md +211 -0
  241. package/skills/npm-package/SKILL.md +239 -0
  242. package/skills/playwriter/SKILL.md +35 -0
  243. package/skills/proxyman/SKILL.md +215 -0
  244. package/skills/security-review/SKILL.md +208 -0
  245. package/skills/simplify/SKILL.md +58 -0
  246. package/skills/spiceflow/SKILL.md +14 -0
  247. package/skills/termcast/SKILL.md +945 -0
  248. package/skills/tuistory/SKILL.md +250 -0
  249. package/skills/usecomputer/SKILL.md +264 -0
  250. package/skills/x-articles/SKILL.md +554 -0
  251. package/skills/zele/SKILL.md +112 -0
  252. package/skills/zustand-centralized-state/SKILL.md +1004 -0
  253. package/src/agent-model.e2e.test.ts +976 -0
  254. package/src/ai-tool-to-genai.test.ts +296 -0
  255. package/src/ai-tool-to-genai.ts +283 -0
  256. package/src/ai-tool.ts +39 -0
  257. package/src/anthropic-auth-plugin.test.ts +159 -0
  258. package/src/anthropic-auth-plugin.ts +861 -0
  259. package/src/anthropic-auth-state.ts +282 -0
  260. package/src/bin.ts +111 -0
  261. package/src/channel-management.ts +334 -0
  262. package/src/cli-parsing.test.ts +195 -0
  263. package/src/cli-send-thread.e2e.test.ts +464 -0
  264. package/src/cli.ts +4581 -0
  265. package/src/commands/abort.ts +89 -0
  266. package/src/commands/action-buttons.ts +364 -0
  267. package/src/commands/add-project.ts +149 -0
  268. package/src/commands/agent.ts +473 -0
  269. package/src/commands/ask-question.ts +390 -0
  270. package/src/commands/btw.ts +164 -0
  271. package/src/commands/compact.ts +157 -0
  272. package/src/commands/context-usage.ts +199 -0
  273. package/src/commands/create-new-project.ts +190 -0
  274. package/src/commands/diff.ts +91 -0
  275. package/src/commands/file-upload.ts +389 -0
  276. package/src/commands/fork.ts +321 -0
  277. package/src/commands/gemini-apikey.ts +104 -0
  278. package/src/commands/login.ts +1173 -0
  279. package/src/commands/mcp.ts +307 -0
  280. package/src/commands/memory-snapshot.ts +30 -0
  281. package/src/commands/mention-mode.ts +68 -0
  282. package/src/commands/merge-worktree.ts +223 -0
  283. package/src/commands/model-variant.ts +483 -0
  284. package/src/commands/model.ts +1053 -0
  285. package/src/commands/new-worktree.ts +510 -0
  286. package/src/commands/paginated-select.ts +81 -0
  287. package/src/commands/permissions.ts +397 -0
  288. package/src/commands/queue.ts +271 -0
  289. package/src/commands/remove-project.ts +155 -0
  290. package/src/commands/restart-opencode-server.ts +162 -0
  291. package/src/commands/resume.ts +230 -0
  292. package/src/commands/run-command.ts +123 -0
  293. package/src/commands/screenshare.test.ts +30 -0
  294. package/src/commands/screenshare.ts +366 -0
  295. package/src/commands/session-id.ts +109 -0
  296. package/src/commands/session.ts +227 -0
  297. package/src/commands/share.ts +106 -0
  298. package/src/commands/tasks.ts +293 -0
  299. package/src/commands/types.ts +25 -0
  300. package/src/commands/undo-redo.ts +386 -0
  301. package/src/commands/unset-model.ts +173 -0
  302. package/src/commands/upgrade.ts +52 -0
  303. package/src/commands/user-command.ts +198 -0
  304. package/src/commands/verbosity.ts +173 -0
  305. package/src/commands/worktree-settings.ts +70 -0
  306. package/src/commands/worktrees.ts +552 -0
  307. package/src/condense-memory.ts +36 -0
  308. package/src/config.ts +111 -0
  309. package/src/context-awareness-plugin.test.ts +142 -0
  310. package/src/context-awareness-plugin.ts +510 -0
  311. package/src/critique-utils.ts +139 -0
  312. package/src/database.ts +1876 -0
  313. package/src/db.test.ts +162 -0
  314. package/src/db.ts +286 -0
  315. package/src/debounce-timeout.ts +43 -0
  316. package/src/debounced-process-flush.ts +104 -0
  317. package/src/discord-bot.ts +1330 -0
  318. package/src/discord-command-registration.ts +693 -0
  319. package/src/discord-urls.ts +88 -0
  320. package/src/discord-utils.test.ts +153 -0
  321. package/src/discord-utils.ts +800 -0
  322. package/src/errors.ts +201 -0
  323. package/src/escape-backticks.test.ts +469 -0
  324. package/src/event-stream-real-capture.e2e.test.ts +692 -0
  325. package/src/eventsource-parser.test.ts +351 -0
  326. package/src/exec-async.ts +35 -0
  327. package/src/external-opencode-sync.ts +685 -0
  328. package/src/format-tables.test.ts +335 -0
  329. package/src/format-tables.ts +445 -0
  330. package/src/forum-sync/config.ts +92 -0
  331. package/src/forum-sync/discord-operations.ts +241 -0
  332. package/src/forum-sync/index.ts +9 -0
  333. package/src/forum-sync/markdown.ts +172 -0
  334. package/src/forum-sync/sync-to-discord.ts +595 -0
  335. package/src/forum-sync/sync-to-files.ts +294 -0
  336. package/src/forum-sync/types.ts +175 -0
  337. package/src/forum-sync/watchers.ts +454 -0
  338. package/src/gateway-proxy-reconnect.e2e.test.ts +523 -0
  339. package/src/gateway-proxy.e2e.test.ts +640 -0
  340. package/src/genai-worker-wrapper.ts +164 -0
  341. package/src/genai-worker.ts +386 -0
  342. package/src/genai.ts +321 -0
  343. package/src/generated/browser.ts +114 -0
  344. package/src/generated/client.ts +138 -0
  345. package/src/generated/commonInputTypes.ts +736 -0
  346. package/src/generated/enums.ts +88 -0
  347. package/src/generated/internal/class.ts +384 -0
  348. package/src/generated/internal/prismaNamespace.ts +2386 -0
  349. package/src/generated/internal/prismaNamespaceBrowser.ts +326 -0
  350. package/src/generated/models/bot_api_keys.ts +1288 -0
  351. package/src/generated/models/bot_tokens.ts +1656 -0
  352. package/src/generated/models/channel_agents.ts +1256 -0
  353. package/src/generated/models/channel_directories.ts +1859 -0
  354. package/src/generated/models/channel_mention_mode.ts +1300 -0
  355. package/src/generated/models/channel_models.ts +1288 -0
  356. package/src/generated/models/channel_verbosity.ts +1228 -0
  357. package/src/generated/models/channel_worktrees.ts +1300 -0
  358. package/src/generated/models/forum_sync_configs.ts +1452 -0
  359. package/src/generated/models/global_models.ts +1288 -0
  360. package/src/generated/models/ipc_requests.ts +1485 -0
  361. package/src/generated/models/part_messages.ts +1302 -0
  362. package/src/generated/models/scheduled_tasks.ts +2320 -0
  363. package/src/generated/models/session_agents.ts +1086 -0
  364. package/src/generated/models/session_events.ts +1439 -0
  365. package/src/generated/models/session_models.ts +1114 -0
  366. package/src/generated/models/session_start_sources.ts +1408 -0
  367. package/src/generated/models/thread_sessions.ts +1781 -0
  368. package/src/generated/models/thread_worktrees.ts +1356 -0
  369. package/src/generated/models.ts +30 -0
  370. package/src/heap-monitor.ts +152 -0
  371. package/src/hrana-server.test.ts +434 -0
  372. package/src/hrana-server.ts +314 -0
  373. package/src/html-actions.test.ts +87 -0
  374. package/src/html-actions.ts +174 -0
  375. package/src/html-components.test.ts +38 -0
  376. package/src/html-components.ts +181 -0
  377. package/src/image-optimizer-plugin.ts +194 -0
  378. package/src/image-utils.ts +149 -0
  379. package/src/interaction-handler.ts +576 -0
  380. package/src/ipc-polling.ts +326 -0
  381. package/src/ipc-tools-plugin.ts +236 -0
  382. package/src/kimaki-digital-twin.e2e.test.ts +199 -0
  383. package/src/kimaki-opencode-plugin-loading.e2e.test.ts +109 -0
  384. package/src/kimaki-opencode-plugin.test.ts +108 -0
  385. package/src/kimaki-opencode-plugin.ts +18 -0
  386. package/src/limit-heading-depth.test.ts +116 -0
  387. package/src/limit-heading-depth.ts +26 -0
  388. package/src/logger.ts +208 -0
  389. package/src/markdown.test.ts +308 -0
  390. package/src/markdown.ts +410 -0
  391. package/src/message-finish-field.e2e.test.ts +192 -0
  392. package/src/message-formatting.test.ts +81 -0
  393. package/src/message-formatting.ts +533 -0
  394. package/src/message-preprocessing.ts +455 -0
  395. package/src/onboarding-tutorial.ts +176 -0
  396. package/src/onboarding-welcome.ts +49 -0
  397. package/src/openai-realtime.ts +358 -0
  398. package/src/opencode-command-detection.test.ts +307 -0
  399. package/src/opencode-command-detection.ts +76 -0
  400. package/src/opencode-command.test.ts +70 -0
  401. package/src/opencode-command.ts +188 -0
  402. package/src/opencode-interrupt-plugin.test.ts +677 -0
  403. package/src/opencode-interrupt-plugin.ts +477 -0
  404. package/src/opencode.ts +1110 -0
  405. package/src/otto/branding.ts +23 -0
  406. package/src/otto/index.ts +22 -0
  407. package/src/parse-permission-rules.test.ts +127 -0
  408. package/src/patch-text-parser.ts +107 -0
  409. package/src/plugin-logger.ts +68 -0
  410. package/src/privacy-sanitizer.ts +142 -0
  411. package/src/queue-advanced-abort.e2e.test.ts +382 -0
  412. package/src/queue-advanced-action-buttons.e2e.test.ts +268 -0
  413. package/src/queue-advanced-e2e-setup.ts +873 -0
  414. package/src/queue-advanced-footer.e2e.test.ts +576 -0
  415. package/src/queue-advanced-model-switch.e2e.test.ts +383 -0
  416. package/src/queue-advanced-permissions-typing.e2e.test.ts +245 -0
  417. package/src/queue-advanced-question.e2e.test.ts +316 -0
  418. package/src/queue-advanced-typing-interrupt.e2e.test.ts +146 -0
  419. package/src/queue-advanced-typing.e2e.test.ts +199 -0
  420. package/src/queue-drain-after-interactive-ui.e2e.test.ts +151 -0
  421. package/src/queue-interrupt-drain.e2e.test.ts +166 -0
  422. package/src/queue-question-select-drain.e2e.test.ts +152 -0
  423. package/src/runtime-idle-sweeper.ts +76 -0
  424. package/src/runtime-lifecycle.e2e.test.ts +641 -0
  425. package/src/schema.sql +173 -0
  426. package/src/sentry.ts +26 -0
  427. package/src/session-handler/agent-utils.ts +97 -0
  428. package/src/session-handler/event-stream-fixtures/real-session-action-buttons.jsonl +45 -0
  429. package/src/session-handler/event-stream-fixtures/real-session-footer-suppressed-on-pre-idle-interrupt.jsonl +40 -0
  430. package/src/session-handler/event-stream-fixtures/real-session-permission-external-file.jsonl +23 -0
  431. package/src/session-handler/event-stream-fixtures/real-session-task-normal.jsonl +22 -0
  432. package/src/session-handler/event-stream-fixtures/real-session-task-three-parallel-sleeps.jsonl +277 -0
  433. package/src/session-handler/event-stream-fixtures/real-session-task-user-interruption.jsonl +46 -0
  434. package/src/session-handler/event-stream-fixtures/session-abort-after-idle-race.jsonl +21 -0
  435. package/src/session-handler/event-stream-fixtures/session-concurrent-messages-serialized.jsonl +56 -0
  436. package/src/session-handler/event-stream-fixtures/session-explicit-abort.jsonl +44 -0
  437. package/src/session-handler/event-stream-fixtures/session-normal-completion.jsonl +29 -0
  438. package/src/session-handler/event-stream-fixtures/session-tool-call-noisy-stream.jsonl +29 -0
  439. package/src/session-handler/event-stream-fixtures/session-two-completions-same-session.jsonl +50 -0
  440. package/src/session-handler/event-stream-fixtures/session-user-interruption.jsonl +59 -0
  441. package/src/session-handler/event-stream-fixtures/session-voice-queued-followup.jsonl +52 -0
  442. package/src/session-handler/event-stream-state.test.ts +645 -0
  443. package/src/session-handler/event-stream-state.ts +608 -0
  444. package/src/session-handler/model-utils.ts +183 -0
  445. package/src/session-handler/opencode-session-event-log.ts +130 -0
  446. package/src/session-handler/thread-runtime-state.ts +212 -0
  447. package/src/session-handler/thread-session-runtime.ts +4281 -0
  448. package/src/session-handler.ts +15 -0
  449. package/src/session-search.test.ts +50 -0
  450. package/src/session-search.ts +148 -0
  451. package/src/session-title-rename.test.ts +112 -0
  452. package/src/startup-service.ts +200 -0
  453. package/src/startup-time.e2e.test.ts +373 -0
  454. package/src/store.ts +122 -0
  455. package/src/system-message.test.ts +612 -0
  456. package/src/system-message.ts +723 -0
  457. package/src/task-runner.ts +421 -0
  458. package/src/task-schedule.test.ts +84 -0
  459. package/src/task-schedule.ts +311 -0
  460. package/src/test-utils.ts +435 -0
  461. package/src/thinking-utils.ts +61 -0
  462. package/src/thread-message-queue.e2e.test.ts +1219 -0
  463. package/src/tools.ts +430 -0
  464. package/src/undici.d.ts +12 -0
  465. package/src/undo-redo.e2e.test.ts +209 -0
  466. package/src/unnest-code-blocks.test.ts +713 -0
  467. package/src/unnest-code-blocks.ts +185 -0
  468. package/src/upgrade.ts +127 -0
  469. package/src/utils.ts +212 -0
  470. package/src/voice-attachment.ts +51 -0
  471. package/src/voice-handler.ts +908 -0
  472. package/src/voice-message.e2e.test.ts +1255 -0
  473. package/src/voice.test.ts +281 -0
  474. package/src/voice.ts +627 -0
  475. package/src/wait-session.ts +147 -0
  476. package/src/websockify.ts +101 -0
  477. package/src/worker-types.ts +64 -0
  478. package/src/worktree-lifecycle.e2e.test.ts +391 -0
  479. package/src/worktree-utils.ts +4 -0
  480. package/src/worktrees.test.ts +223 -0
  481. package/src/worktrees.ts +1294 -0
  482. package/src/xml.test.ts +38 -0
  483. package/src/xml.ts +121 -0
@@ -0,0 +1,151 @@
1
+ // E2e test: queued messages must drain immediately when the session is idle,
2
+ // even if action buttons are still pending. The isSessionBusy check is
3
+ // sufficient — hasPendingInteractiveUi() should NOT block queue drain.
4
+
5
+ import { describe, test, expect } from 'vitest'
6
+ import {
7
+ setupQueueAdvancedSuite,
8
+ TEST_USER_ID,
9
+ } from './queue-advanced-e2e-setup.js'
10
+ import {
11
+ waitForBotMessageContaining,
12
+ waitForFooterMessage,
13
+ } from './test-utils.js'
14
+ import { getThreadSession } from './database.js'
15
+ import {
16
+ pendingActionButtonContexts,
17
+ showActionButtons,
18
+ } from './commands/action-buttons.js'
19
+
20
+ const TEXT_CHANNEL_ID = '200000000000001020'
21
+
22
+ describe('queue drain with pending interactive UI', () => {
23
+ const ctx = setupQueueAdvancedSuite({
24
+ channelId: TEXT_CHANNEL_ID,
25
+ channelName: 'qa-drain-interactive-ui',
26
+ dirName: 'qa-drain-interactive-ui',
27
+ username: 'drain-ui-tester',
28
+ })
29
+
30
+ test(
31
+ 'queued message drains immediately while action buttons are still pending',
32
+ async () => {
33
+ // 1. Create a thread with a first completed reply
34
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
35
+ content: 'Reply with exactly: drain-button-setup',
36
+ })
37
+
38
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
39
+ timeout: 4_000,
40
+ predicate: (t) => {
41
+ return t.name === 'Reply with exactly: drain-button-setup'
42
+ },
43
+ })
44
+
45
+ const th = ctx.discord.thread(thread.id)
46
+
47
+ await waitForBotMessageContaining({
48
+ discord: ctx.discord,
49
+ threadId: thread.id,
50
+ userId: TEST_USER_ID,
51
+ text: 'ok',
52
+ timeout: 4_000,
53
+ })
54
+
55
+ await waitForFooterMessage({
56
+ discord: ctx.discord,
57
+ threadId: thread.id,
58
+ timeout: 4_000,
59
+ afterMessageIncludes: 'ok',
60
+ afterAuthorId: ctx.discord.botUserId,
61
+ })
62
+
63
+ // 2. Show action buttons (session is idle, buttons are pending)
64
+ const currentSessionId = await getThreadSession(thread.id)
65
+ if (!currentSessionId) {
66
+ throw new Error('Expected thread session id')
67
+ }
68
+
69
+ const channel = await ctx.botClient.channels.fetch(thread.id)
70
+ if (!channel || !channel.isThread()) {
71
+ throw new Error('Expected Discord thread channel')
72
+ }
73
+
74
+ await showActionButtons({
75
+ thread: channel,
76
+ sessionId: currentSessionId,
77
+ directory: ctx.directories.projectDirectory,
78
+ buttons: [{ label: 'Pending button', color: 'white' }],
79
+ })
80
+
81
+ // Verify buttons are pending
82
+ const start = Date.now()
83
+ while (Date.now() - start < 4_000) {
84
+ const entry = [...pendingActionButtonContexts.entries()].find(([, context]) => {
85
+ return context.thread.id === thread.id && Boolean(context.messageId)
86
+ })
87
+ if (entry) {
88
+ break
89
+ }
90
+ await new Promise<void>((resolve) => {
91
+ setTimeout(resolve, 100)
92
+ })
93
+ }
94
+ expect(
95
+ [...pendingActionButtonContexts.values()].some((c) => {
96
+ return c.thread.id === thread.id
97
+ }),
98
+ ).toBe(true)
99
+
100
+ // 3. Queue a message via /queue while buttons are still pending.
101
+ // The queue should drain immediately because session is idle.
102
+ // Currently FAILS: hasPendingInteractiveUi() blocks tryDrainQueue().
103
+ const { id: queueInteractionId } = await th.user(TEST_USER_ID)
104
+ .runSlashCommand({
105
+ name: 'queue',
106
+ options: [{ name: 'message', type: 3, value: 'Reply with exactly: post-button-drain' }],
107
+ })
108
+
109
+ const queueAck = await th.waitForInteractionAck({
110
+ interactionId: queueInteractionId,
111
+ timeout: 4_000,
112
+ })
113
+ if (!queueAck.messageId) {
114
+ throw new Error('Expected /queue response message id')
115
+ }
116
+
117
+ // 4. Queued message should dispatch immediately (not stay "Queued").
118
+ // The dispatch indicator should appear quickly.
119
+ await waitForBotMessageContaining({
120
+ discord: ctx.discord,
121
+ threadId: thread.id,
122
+ text: '» **drain-ui-tester:** Reply with exactly: post-button-drain',
123
+ timeout: 4_000,
124
+ })
125
+
126
+ // 5. Wait for the footer after the drained message completes
127
+ await waitForFooterMessage({
128
+ discord: ctx.discord,
129
+ threadId: thread.id,
130
+ timeout: 4_000,
131
+ afterMessageIncludes: '» **drain-ui-tester:**',
132
+ afterAuthorId: ctx.discord.botUserId,
133
+ })
134
+
135
+ const timeline = await th.text({ showInteractions: true })
136
+ expect(timeline).toMatchInlineSnapshot(`
137
+ "--- from: user (drain-ui-tester)
138
+ Reply with exactly: drain-button-setup
139
+ --- from: assistant (TestBot)
140
+ ⬥ ok
141
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
142
+ **Action Required**
143
+ [user interaction]
144
+ » **drain-ui-tester:** Reply with exactly: post-button-drain
145
+ ⬥ ok
146
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
147
+ `)
148
+ },
149
+ 20_000,
150
+ )
151
+ })
@@ -0,0 +1,166 @@
1
+ // E2e test for queue + interrupt interaction.
2
+ // Validates that a user can queue a command via /queue while a slow session
3
+ // is in progress, then send a normal (non-queued) message to interrupt.
4
+ //
5
+ // Expected behavior:
6
+ // 1. Slow session is running
7
+ // 2. User queues a message via /queue (enters kimaki local queue)
8
+ // 3. User sends a normal message (interrupt)
9
+ // 4. Session aborts the slow task, processes the interrupt message immediately
10
+ // 5. Interrupt response appears in Discord with a ⬥ ok reply
11
+ // 6. When interrupt response completes, the queued message drains and runs
12
+ //
13
+ // Uses opencode-deterministic-provider (no real LLM calls).
14
+ // Poll timeouts: 4s max, 100ms interval. Slow matcher uses 100s delay.
15
+
16
+ import { describe, test, expect } from 'vitest'
17
+ import {
18
+ setupQueueAdvancedSuite,
19
+ TEST_USER_ID,
20
+ } from './queue-advanced-e2e-setup.js'
21
+ import {
22
+ waitForFooterMessage,
23
+ waitForBotMessageContaining,
24
+ waitForMessageById,
25
+ } from './test-utils.js'
26
+
27
+ const TEXT_CHANNEL_ID = '200000000000001099'
28
+
29
+ const e2eTest = describe
30
+
31
+ e2eTest('queue + interrupt drain ordering', () => {
32
+ const ctx = setupQueueAdvancedSuite({
33
+ channelId: TEXT_CHANNEL_ID,
34
+ channelName: 'qa-interrupt-drain-e2e',
35
+ dirName: 'qa-interrupt-drain-e2e',
36
+ username: 'interrupt-tester',
37
+ })
38
+
39
+ test(
40
+ 'queued message via /queue + normal interrupt: interrupt reply should appear, then queue drains',
41
+ async () => {
42
+ // 1. Establish session with a quick first message
43
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
44
+ content: 'Reply with exactly: setup-interrupt-drain',
45
+ })
46
+
47
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
48
+ timeout: 4_000,
49
+ predicate: (t) => {
50
+ return t.name === 'Reply with exactly: setup-interrupt-drain'
51
+ },
52
+ })
53
+
54
+ const th = ctx.discord.thread(thread.id)
55
+ await th.waitForBotReply({ timeout: 4_000 })
56
+
57
+ // Wait for first run to fully complete (footer) so state is clean
58
+ await waitForFooterMessage({
59
+ discord: ctx.discord,
60
+ threadId: thread.id,
61
+ timeout: 4_000,
62
+ })
63
+
64
+ // 2. Start a slow session — PLUGIN_TIMEOUT_SLEEP_MARKER has a 100s delay
65
+ // before the finish event, guaranteeing the session stays busy.
66
+ await th.user(TEST_USER_ID).sendMessage({
67
+ content: 'PLUGIN_TIMEOUT_SLEEP_MARKER',
68
+ })
69
+
70
+ // Wait for the slow matcher to start streaming (text appears before delay)
71
+ await waitForBotMessageContaining({
72
+ discord: ctx.discord,
73
+ threadId: thread.id,
74
+ userId: TEST_USER_ID,
75
+ text: 'starting sleep',
76
+ afterUserMessageIncludes: 'PLUGIN_TIMEOUT_SLEEP_MARKER',
77
+ timeout: 4_000,
78
+ })
79
+
80
+ // 3. Queue a message via /queue while the slow session is running
81
+ const { id: queueInteractionId } = await th.user(TEST_USER_ID)
82
+ .runSlashCommand({
83
+ name: 'queue',
84
+ options: [{ name: 'message', type: 3, value: 'Reply with exactly: queued-behind-slow' }],
85
+ })
86
+
87
+ const queueAck = await th.waitForInteractionAck({
88
+ interactionId: queueInteractionId,
89
+ timeout: 4_000,
90
+ })
91
+ if (!queueAck.messageId) {
92
+ throw new Error('Expected /queue response message id')
93
+ }
94
+
95
+ const queueStatusMessage = await waitForMessageById({
96
+ discord: ctx.discord,
97
+ threadId: thread.id,
98
+ messageId: queueAck.messageId,
99
+ timeout: 4_000,
100
+ })
101
+ // The /queue message should be queued (session is busy with the 100s task)
102
+ expect(queueStatusMessage.content).toContain('Queued message')
103
+
104
+ // 4. Send a normal (non-queued) message — this should interrupt the slow
105
+ // session and be processed immediately
106
+ await th.user(TEST_USER_ID).sendMessage({
107
+ content: 'Reply with exactly: interrupt-now',
108
+ })
109
+
110
+ // 5. Wait for the final state: the interrupt message should get its own
111
+ // ⬥ ok reply, then the queued message should drain and get processed.
112
+ // We wait for the queued message's footer as the final signal.
113
+ await waitForFooterMessage({
114
+ discord: ctx.discord,
115
+ threadId: thread.id,
116
+ timeout: 12_000,
117
+ afterMessageIncludes: 'queued-behind-slow',
118
+ afterAuthorId: ctx.discord.botUserId,
119
+ })
120
+
121
+ // 6. Capture the full interaction in an inline snapshot.
122
+ expect(await th.text()).toMatchInlineSnapshot(`
123
+ "--- from: user (interrupt-tester)
124
+ Reply with exactly: setup-interrupt-drain
125
+ --- from: assistant (TestBot)
126
+ ⬥ ok
127
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
128
+ --- from: user (interrupt-tester)
129
+ PLUGIN_TIMEOUT_SLEEP_MARKER
130
+ --- from: assistant (TestBot)
131
+ ⬥ starting sleep 100
132
+ Queued message (position 1)
133
+ --- from: user (interrupt-tester)
134
+ Reply with exactly: interrupt-now
135
+ --- from: assistant (TestBot)
136
+ ⬥ ok
137
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*
138
+ » **interrupt-tester:** Reply with exactly: queued-behind-slow
139
+ ⬥ ok
140
+ *project ⋅ main ⋅ Ns ⋅ N% ⋅ deterministic-v2*"
141
+ `)
142
+
143
+ // 7. Assert the interrupt message got its own ⬥ ok reply between the
144
+ // user's interrupt message and the queue dispatch indicator.
145
+ const text = await th.text()
146
+ const lines = text.split('\n')
147
+
148
+ const interruptUserLine = lines.findIndex((line) => {
149
+ return line.includes('Reply with exactly: interrupt-now')
150
+ })
151
+ expect(interruptUserLine).toBeGreaterThan(-1)
152
+
153
+ const queueDispatchLine = lines.findIndex((line) => {
154
+ return line.includes('» **interrupt-tester:** Reply with exactly: queued-behind-slow')
155
+ })
156
+ expect(queueDispatchLine).toBeGreaterThan(-1)
157
+
158
+ const linesBetween = lines.slice(interruptUserLine + 1, queueDispatchLine)
159
+ const hasInterruptReply = linesBetween.some((line) => {
160
+ return line.includes('⬥ ok')
161
+ })
162
+ expect(hasInterruptReply).toBe(true)
163
+ },
164
+ 20_000,
165
+ )
166
+ })
@@ -0,0 +1,152 @@
1
+ // E2e test: queued message must drain after the user answers a pending question
2
+ // via the Discord dropdown select menu. Reproduces a bug where answering via
3
+ // select (not text) leaves queued messages stuck because the session continues
4
+ // processing after the answer and may enter another blocking state.
5
+
6
+ import { describe, test, expect } from 'vitest'
7
+ import {
8
+ setupQueueAdvancedSuite,
9
+ TEST_USER_ID,
10
+ } from './queue-advanced-e2e-setup.js'
11
+ import {
12
+ waitForBotMessageContaining,
13
+ waitForFooterMessage,
14
+ } from './test-utils.js'
15
+ import { pendingQuestionContexts } from './commands/ask-question.js'
16
+
17
+ const TEXT_CHANNEL_ID = '200000000000001030'
18
+
19
+ async function waitForPendingQuestion({
20
+ threadId,
21
+ timeoutMs,
22
+ }: {
23
+ threadId: string
24
+ timeoutMs: number
25
+ }): Promise<{ contextHash: string }> {
26
+ const start = Date.now()
27
+ while (Date.now() - start < timeoutMs) {
28
+ const entry = [...pendingQuestionContexts.entries()].find(([, context]) => {
29
+ return context.thread.id === threadId
30
+ })
31
+ if (entry) {
32
+ return { contextHash: entry[0] }
33
+ }
34
+ await new Promise<void>((resolve) => {
35
+ setTimeout(resolve, 100)
36
+ })
37
+ }
38
+ throw new Error('Timed out waiting for pending question context')
39
+ }
40
+
41
+ describe('queue drain after question select answer', () => {
42
+ const ctx = setupQueueAdvancedSuite({
43
+ channelId: TEXT_CHANNEL_ID,
44
+ channelName: 'qa-question-select-drain',
45
+ dirName: 'qa-question-select-drain',
46
+ username: 'question-select-tester',
47
+ })
48
+
49
+ test(
50
+ 'queued message drains after answering question via dropdown select',
51
+ async () => {
52
+ // 1. Send a message that triggers the question tool
53
+ await ctx.discord.channel(TEXT_CHANNEL_ID).user(TEST_USER_ID).sendMessage({
54
+ content: 'QUESTION_SELECT_QUEUE_MARKER',
55
+ })
56
+
57
+ const thread = await ctx.discord.channel(TEXT_CHANNEL_ID).waitForThread({
58
+ timeout: 8_000,
59
+ predicate: (t) => {
60
+ return t.name === 'QUESTION_SELECT_QUEUE_MARKER'
61
+ },
62
+ })
63
+
64
+ const th = ctx.discord.thread(thread.id)
65
+
66
+ // 2. Wait for the question dropdown message to appear in Discord.
67
+ // Uses visible message wait instead of internal Map polling which
68
+ // is too timing-sensitive on CI.
69
+ const questionMessages = await waitForBotMessageContaining({
70
+ discord: ctx.discord,
71
+ threadId: thread.id,
72
+ text: 'How to proceed?',
73
+ timeout: 12_000,
74
+ })
75
+
76
+ // Get the pending question context hash from the internal map.
77
+ // By this point the question message is visible so the context must exist.
78
+ const pending = (() => {
79
+ const entry = [...pendingQuestionContexts.entries()].find(([, context]) => {
80
+ return context.thread.id === thread.id
81
+ })
82
+ return entry ? { contextHash: entry[0] } : null
83
+ })()
84
+ expect(pending).toBeTruthy()
85
+ if (!pending) {
86
+ throw new Error('Expected pending question context')
87
+ }
88
+ const questionMsg = questionMessages.find((m) => {
89
+ return m.content.includes('How to proceed?')
90
+ })!
91
+ expect(questionMsg).toBeTruthy()
92
+
93
+ // 3. Queue a message while question is pending
94
+ const { id: queueInteractionId } = await th.user(TEST_USER_ID)
95
+ .runSlashCommand({
96
+ name: 'queue',
97
+ options: [{ name: 'message', type: 3, value: 'Reply with exactly: post-question-drain' }],
98
+ })
99
+
100
+ const queueAck = await th.waitForInteractionAck({
101
+ interactionId: queueInteractionId,
102
+ timeout: 8_000,
103
+ })
104
+ if (!queueAck.messageId) {
105
+ throw new Error('Expected /queue response message id')
106
+ }
107
+
108
+ // 4. Answer the question via dropdown select (pick first option "Alpha")
109
+ const interaction = await th.user(TEST_USER_ID).selectMenu({
110
+ messageId: questionMsg.id,
111
+ customId: `ask_question:${pending.contextHash}:0`,
112
+ values: ['0'],
113
+ })
114
+
115
+ await th.waitForInteractionAck({
116
+ interactionId: interaction.id,
117
+ timeout: 8_000,
118
+ })
119
+
120
+ // 5. Queued message should be handed off to OpenCode's own prompt queue
121
+ // after the question reply, so the dispatch indicator appears without
122
+ // waiting for a later natural idle.
123
+ await waitForBotMessageContaining({
124
+ discord: ctx.discord,
125
+ threadId: thread.id,
126
+ text: '» **question-select-tester:** Reply with exactly: post-question-drain',
127
+ timeout: 8_000,
128
+ })
129
+
130
+ // 6. Wait for footer from the drained queued message
131
+ await waitForFooterMessage({
132
+ discord: ctx.discord,
133
+ threadId: thread.id,
134
+ timeout: 8_000,
135
+ afterMessageIncludes: '» **question-select-tester:**',
136
+ afterAuthorId: ctx.discord.botUserId,
137
+ })
138
+
139
+ // Assert key invariants instead of exact snapshot — on CI the deterministic
140
+ // matcher can fire a second time after the drained message (rawPromptIncludes
141
+ // scans full history), adding an extra question to the timeline.
142
+ const timeline = await th.text({ showInteractions: true })
143
+ expect(timeline).toContain('QUESTION_SELECT_QUEUE_MARKER')
144
+ expect(timeline).toContain('How to proceed?')
145
+ expect(timeline).toContain('[user selects dropdown: 0]')
146
+ expect(timeline).toContain('» **question-select-tester:** Reply with exactly: post-question-drain')
147
+ expect(timeline).toContain('⬥ ok')
148
+ expect(timeline).toContain('*project ⋅ main ⋅')
149
+ },
150
+ 20_000,
151
+ )
152
+ })
@@ -0,0 +1,76 @@
1
+ // Runtime inactivity sweeper.
2
+ // Periodically disposes thread runtimes that stayed idle past a timeout.
3
+
4
+ import { createLogger, LogPrefix } from './logger.js'
5
+ import {
6
+ disposeInactiveRuntimes,
7
+ } from './session-handler/thread-session-runtime.js'
8
+
9
+ const logger = createLogger(LogPrefix.SESSION)
10
+
11
+ // 24 hours — users often return the next day to click buttons/selects,
12
+ // so runtimes (and their in-memory context maps) must stay alive that long.
13
+ export const DEFAULT_RUNTIME_IDLE_MS = 24 * 60 * 60 * 1000
14
+ export const DEFAULT_SWEEP_INTERVAL_MS = 60 * 1000
15
+
16
+ export function startRuntimeIdleSweeper({
17
+ runtimeIdleMs = DEFAULT_RUNTIME_IDLE_MS,
18
+ sweepIntervalMs = DEFAULT_SWEEP_INTERVAL_MS,
19
+ }: {
20
+ runtimeIdleMs?: number
21
+ sweepIntervalMs?: number
22
+ } = {}): () => Promise<void> {
23
+ let stopped = false
24
+ let sweeping = false
25
+ let sweepPromise: Promise<void> | null = null
26
+
27
+ const sweep = async (): Promise<void> => {
28
+ if (stopped || sweeping) {
29
+ return
30
+ }
31
+ sweeping = true
32
+
33
+ const currentSweepPromise = (async () => {
34
+ const nowMs = Date.now()
35
+ const disposeResult = disposeInactiveRuntimes({
36
+ idleMs: runtimeIdleMs,
37
+ nowMs,
38
+ })
39
+ if (disposeResult.disposedThreadIds.length > 0) {
40
+ logger.log(
41
+ `[IDLE SWEEP] Disposed ${disposeResult.disposedThreadIds.length} inactive runtime(s) after ${runtimeIdleMs}ms`,
42
+ )
43
+ }
44
+
45
+ })()
46
+
47
+ sweepPromise = currentSweepPromise
48
+ await currentSweepPromise.finally(() => {
49
+ sweeping = false
50
+ sweepPromise = null
51
+ })
52
+ }
53
+
54
+ const interval = setInterval(() => {
55
+ void sweep()
56
+ }, sweepIntervalMs)
57
+
58
+ void sweep()
59
+
60
+ logger.log(
61
+ `[IDLE SWEEP] Started (runtimeIdleMs=${runtimeIdleMs}, intervalMs=${sweepIntervalMs})`,
62
+ )
63
+
64
+ return async () => {
65
+ if (stopped) {
66
+ return
67
+ }
68
+ stopped = true
69
+ clearInterval(interval)
70
+ if (sweepPromise) {
71
+ await sweepPromise
72
+ sweepPromise = null
73
+ }
74
+ logger.log('[IDLE SWEEP] Stopped')
75
+ }
76
+ }