mixdog 0.9.3 → 0.9.5

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 (382) hide show
  1. package/README.md +112 -38
  2. package/package.json +10 -3
  3. package/scripts/bench/lead-review-tasks-r3.json +20 -0
  4. package/scripts/bench/lead-review-tasks.json +20 -0
  5. package/scripts/bench/r4-mixed-tasks.json +20 -0
  6. package/scripts/bench/r5-orchestrated-task.json +7 -0
  7. package/scripts/bench/review-tasks.json +20 -0
  8. package/scripts/bench/round-codex.json +114 -0
  9. package/scripts/bench/round-mixdog-lead-r3.json +269 -0
  10. package/scripts/bench/round-mixdog-lead.json +269 -0
  11. package/scripts/bench/round-mixdog.json +126 -0
  12. package/scripts/bench/round-r10-bigsample.json +679 -0
  13. package/scripts/bench/round-r11-codexalign.json +257 -0
  14. package/scripts/bench/round-r13-clientmeta.json +464 -0
  15. package/scripts/bench/round-r14-betafeatures.json +466 -0
  16. package/scripts/bench/round-r15-fulldefault.json +462 -0
  17. package/scripts/bench/round-r16-sessionid.json +466 -0
  18. package/scripts/bench/round-r17-wirebytes.json +456 -0
  19. package/scripts/bench/round-r18-prewarm.json +468 -0
  20. package/scripts/bench/round-r19-clean.json +472 -0
  21. package/scripts/bench/round-r20-prewarm-clean.json +475 -0
  22. package/scripts/bench/round-r21-delta-retry.json +473 -0
  23. package/scripts/bench/round-r22-full-probe.json +693 -0
  24. package/scripts/bench/round-r23-itemprobe.json +701 -0
  25. package/scripts/bench/round-r24-shapefix.json +677 -0
  26. package/scripts/bench/round-r25-serial.json +464 -0
  27. package/scripts/bench/round-r26-parallel3.json +671 -0
  28. package/scripts/bench/round-r27-parallel10.json +894 -0
  29. package/scripts/bench/round-r28-parallel10-stagger.json +882 -0
  30. package/scripts/bench/round-r29-parallel10-stagger166.json +886 -0
  31. package/scripts/bench/round-r30-instid.json +253 -0
  32. package/scripts/bench/round-r31-upgradeprobe.json +256 -0
  33. package/scripts/bench/round-r32-vs-codex-lead.json +254 -0
  34. package/scripts/bench/round-r33-vs-codex-codex.json +115 -0
  35. package/scripts/bench/round-r34-orchestrated.json +120 -0
  36. package/scripts/bench/round-r35-orchestrated-codex.json +61 -0
  37. package/scripts/bench/round-r36-orchestrated-capped.json +128 -0
  38. package/scripts/bench/round-r4-codex.json +114 -0
  39. package/scripts/bench/round-r4-mixed.json +225 -0
  40. package/scripts/bench/round-r5-gpt-lead.json +259 -0
  41. package/scripts/bench/round-r6-codex.json +114 -0
  42. package/scripts/bench/round-r6-solo.json +257 -0
  43. package/scripts/bench/round-r7-full.json +254 -0
  44. package/scripts/bench/round-r8-fulldefault.json +255 -0
  45. package/scripts/bench-run.mjs +251 -32
  46. package/scripts/freevar-smoke.mjs +95 -0
  47. package/scripts/internal-comms-bench.mjs +3 -4
  48. package/scripts/internal-comms-smoke.mjs +10 -9
  49. package/scripts/model-catalog-audit.mjs +209 -0
  50. package/scripts/model-list-sanitize-test.mjs +37 -0
  51. package/scripts/mouse-probe.mjs +45 -0
  52. package/scripts/output-style-bench.mjs +13 -6
  53. package/scripts/output-style-smoke.mjs +4 -4
  54. package/scripts/provider-toolcall-test.mjs +7 -3
  55. package/scripts/recall-bench.mjs +76 -13
  56. package/scripts/recall-quality-cases.json +12 -0
  57. package/scripts/recall-usecase-cases.json +18 -0
  58. package/scripts/session-bench.mjs +152 -6
  59. package/scripts/tool-smoke.mjs +25 -65
  60. package/scripts/tui-render-smoke.mjs +90 -0
  61. package/scripts/webhook-smoke.mjs +208 -0
  62. package/src/agents/debugger/AGENT.md +4 -1
  63. package/src/agents/heavy-worker/AGENT.md +9 -8
  64. package/src/agents/maintainer/AGENT.md +4 -0
  65. package/src/agents/reviewer/AGENT.md +2 -1
  66. package/src/agents/scheduler-task/AGENT.md +2 -3
  67. package/src/agents/webhook-handler/AGENT.md +2 -3
  68. package/src/agents/worker/AGENT.md +10 -7
  69. package/src/app.mjs +12 -1
  70. package/src/headless-role.mjs +7 -1
  71. package/src/lib/rules-builder.cjs +4 -0
  72. package/src/mixdog-session-runtime.mjs +647 -2056
  73. package/src/output-styles/default.md +30 -9
  74. package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
  75. package/src/output-styles/minimal.md +8 -6
  76. package/src/output-styles/simple.md +21 -7
  77. package/src/rules/agent/00-common.md +6 -3
  78. package/src/rules/agent/30-explorer.md +16 -5
  79. package/src/rules/lead/01-general.md +5 -5
  80. package/src/rules/lead/lead-brief.md +15 -0
  81. package/src/rules/lead/lead-tool.md +6 -15
  82. package/src/rules/shared/01-tool.md +17 -21
  83. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +8 -3
  84. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +25 -0
  85. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +100 -23
  86. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +6 -15
  87. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +362 -0
  88. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +410 -0
  89. package/src/runtime/agent/orchestrator/agent-trace.mjs +16 -735
  90. package/src/runtime/agent/orchestrator/config.mjs +69 -2
  91. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +62 -20
  92. package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +209 -0
  93. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +489 -0
  94. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +81 -1281
  95. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +607 -0
  96. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +32 -3
  97. package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +81 -0
  98. package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +248 -0
  99. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +303 -0
  100. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +505 -0
  101. package/src/runtime/agent/orchestrator/providers/gemini.mjs +43 -1013
  102. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +17 -3
  103. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +105 -11
  104. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +356 -0
  105. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +108 -0
  106. package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +58 -0
  107. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +368 -0
  108. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +760 -0
  109. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +40 -1143
  110. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +740 -0
  111. package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
  112. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +349 -2131
  113. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +143 -1002
  114. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +229 -0
  115. package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
  116. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +465 -0
  117. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1105 -0
  118. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -1
  119. package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +80 -0
  120. package/src/runtime/agent/orchestrator/session/compact/budget.mjs +288 -0
  121. package/src/runtime/agent/orchestrator/session/compact/constants.mjs +85 -0
  122. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +749 -0
  123. package/src/runtime/agent/orchestrator/session/compact/messages.mjs +82 -0
  124. package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +315 -0
  125. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +643 -0
  126. package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +326 -0
  127. package/src/runtime/agent/orchestrator/session/compact.mjs +40 -2282
  128. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +14 -2
  129. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +61 -0
  130. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -3
  131. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +275 -0
  132. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +173 -0
  133. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +58 -0
  134. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +239 -0
  135. package/src/runtime/agent/orchestrator/session/loop.mjs +278 -402
  136. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +471 -0
  137. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +7 -4
  138. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +12 -0
  139. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +406 -0
  140. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +80 -0
  141. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +210 -0
  142. package/src/runtime/agent/orchestrator/session/manager.mjs +166 -1087
  143. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +189 -0
  144. package/src/runtime/agent/orchestrator/session/store.mjs +74 -179
  145. package/src/runtime/agent/orchestrator/stall-policy.mjs +20 -1
  146. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +70 -20
  147. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +22 -2
  148. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +35 -44
  149. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +40 -0
  150. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +29 -0
  151. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +8 -0
  152. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +126 -0
  153. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +81 -92
  154. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +161 -0
  155. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +108 -0
  156. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +28 -265
  157. package/src/runtime/agent/orchestrator/tools/builtin.mjs +0 -6
  158. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +57 -3
  159. package/src/runtime/agent/orchestrator/tools/code-graph/keyword-match.mjs +82 -0
  160. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +10 -122
  161. package/src/runtime/agent/orchestrator/tools/code-graph/text-columns.mjs +45 -0
  162. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
  163. package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +6 -3
  164. package/src/runtime/agent/orchestrator/tools/patch/constants.mjs +9 -0
  165. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +171 -0
  166. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +471 -0
  167. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +436 -0
  168. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +342 -0
  169. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +359 -0
  170. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +340 -0
  171. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +643 -0
  172. package/src/runtime/agent/orchestrator/tools/patch.mjs +36 -2959
  173. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +0 -21
  174. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -72
  175. package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +77 -0
  176. package/src/runtime/agent/orchestrator/tools/shell-state.mjs +154 -0
  177. package/src/runtime/channels/backends/discord-access.mjs +32 -0
  178. package/src/runtime/channels/backends/discord-attachments.mjs +65 -0
  179. package/src/runtime/channels/backends/discord-gateway.mjs +233 -0
  180. package/src/runtime/channels/backends/discord.mjs +27 -318
  181. package/src/runtime/channels/backends/telegram.mjs +8 -12
  182. package/src/runtime/channels/index.mjs +247 -701
  183. package/src/runtime/channels/lib/backend-dispatch.mjs +46 -0
  184. package/src/runtime/channels/lib/config.mjs +37 -149
  185. package/src/runtime/channels/lib/event-pipeline.mjs +22 -5
  186. package/src/runtime/channels/lib/event-queue.mjs +78 -13
  187. package/src/runtime/channels/lib/inbound-routing.mjs +74 -0
  188. package/src/runtime/channels/lib/interaction-workflows.mjs +5 -113
  189. package/src/runtime/channels/lib/output-forwarder.mjs +1 -1
  190. package/src/runtime/channels/lib/owner-heartbeat.mjs +75 -0
  191. package/src/runtime/channels/lib/parent-bridge.mjs +88 -0
  192. package/src/runtime/channels/lib/runtime-paths.mjs +14 -4
  193. package/src/runtime/channels/lib/scheduler.mjs +27 -113
  194. package/src/runtime/channels/lib/session-discovery.mjs +56 -4
  195. package/src/runtime/channels/lib/tool-dispatch.mjs +158 -0
  196. package/src/runtime/channels/lib/tool-format.mjs +1 -1
  197. package/src/runtime/channels/lib/transcript-discovery.mjs +4 -4
  198. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -3
  199. package/src/runtime/channels/lib/voice-transcription.mjs +179 -0
  200. package/src/runtime/channels/lib/webhook/deliveries.mjs +313 -0
  201. package/src/runtime/channels/lib/webhook/log.mjs +42 -0
  202. package/src/runtime/channels/lib/webhook/ngrok.mjs +181 -0
  203. package/src/runtime/channels/lib/webhook/signature.mjs +60 -0
  204. package/src/runtime/channels/lib/webhook.mjs +43 -616
  205. package/src/runtime/channels/tool-defs.mjs +11 -130
  206. package/src/runtime/memory/index.mjs +210 -1948
  207. package/src/runtime/memory/lib/core-memory-store.mjs +5 -1
  208. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +58 -0
  209. package/src/runtime/memory/lib/cycle-scheduler.mjs +497 -0
  210. package/src/runtime/memory/lib/embedding-warmup.mjs +58 -0
  211. package/src/runtime/memory/lib/ko-morph.mjs +195 -0
  212. package/src/runtime/memory/lib/memory-config-flags.mjs +91 -0
  213. package/src/runtime/memory/lib/memory-cycle.mjs +1 -1
  214. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +515 -0
  215. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +324 -0
  216. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +18 -0
  217. package/src/runtime/memory/lib/memory-cycle2.mjs +24 -842
  218. package/src/runtime/memory/lib/memory-embed.mjs +149 -0
  219. package/src/runtime/memory/lib/memory-process-lock.mjs +162 -0
  220. package/src/runtime/memory/lib/memory-recall-store.mjs +69 -12
  221. package/src/runtime/memory/lib/memory-text-utils.mjs +46 -0
  222. package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
  223. package/src/runtime/memory/lib/query-handlers.mjs +802 -0
  224. package/src/runtime/memory/lib/recall-format.mjs +55 -0
  225. package/src/runtime/memory/lib/runtime-fetcher.mjs +8 -3
  226. package/src/runtime/memory/lib/transcript-ingest.mjs +425 -0
  227. package/src/runtime/memory/tool-defs.mjs +5 -13
  228. package/src/runtime/search/lib/http-fetch.mjs +274 -0
  229. package/src/runtime/search/lib/ssrf-guard.mjs +333 -0
  230. package/src/runtime/search/lib/web-tools.mjs +24 -602
  231. package/src/runtime/shared/atomic-file.mjs +26 -1
  232. package/src/runtime/shared/config.mjs +14 -4
  233. package/src/runtime/shared/launcher-control.mjs +2 -2
  234. package/src/runtime/shared/markdown-frontmatter.mjs +19 -0
  235. package/src/runtime/shared/schedules-store.mjs +13 -3
  236. package/src/runtime/shared/tool-execution-contract.mjs +2 -2
  237. package/src/runtime/shared/tool-primitives.mjs +308 -0
  238. package/src/runtime/shared/tool-result-summary.mjs +515 -0
  239. package/src/runtime/shared/tool-surface.mjs +80 -898
  240. package/src/runtime/shared/transcript-writer.mjs +23 -0
  241. package/src/runtime/shared/update-checker.mjs +7 -4
  242. package/src/session-runtime/config-helpers.mjs +119 -2
  243. package/src/session-runtime/config-lifecycle.mjs +232 -0
  244. package/src/session-runtime/cwd-plugins.mjs +226 -0
  245. package/src/session-runtime/mcp-glue.mjs +177 -0
  246. package/src/session-runtime/model-recency.mjs +111 -0
  247. package/src/session-runtime/native-search.mjs +247 -0
  248. package/src/session-runtime/output-styles.mjs +11 -9
  249. package/src/session-runtime/prewarm.mjs +142 -0
  250. package/src/session-runtime/provider-models.mjs +278 -0
  251. package/src/session-runtime/provider-usage.mjs +120 -0
  252. package/src/session-runtime/quick-model-rows.mjs +205 -0
  253. package/src/session-runtime/quick-search-models.mjs +47 -0
  254. package/src/session-runtime/session-hooks.mjs +93 -0
  255. package/src/session-runtime/settings-api.mjs +352 -0
  256. package/src/session-runtime/tool-catalog.mjs +29 -29
  257. package/src/session-runtime/tool-defs.mjs +84 -0
  258. package/src/session-runtime/warmup-schedulers.mjs +201 -0
  259. package/src/session-runtime/workflow.mjs +1 -1
  260. package/src/standalone/agent-tool/helpers.mjs +237 -0
  261. package/src/standalone/agent-tool/notify.mjs +107 -0
  262. package/src/standalone/agent-tool/provider-init.mjs +143 -0
  263. package/src/standalone/agent-tool/render.mjs +152 -0
  264. package/src/standalone/agent-tool/tool-def.mjs +55 -0
  265. package/src/standalone/agent-tool.mjs +138 -669
  266. package/src/standalone/channel-admin.mjs +102 -90
  267. package/src/standalone/channel-worker.mjs +4 -7
  268. package/src/standalone/explore-tool.mjs +64 -14
  269. package/src/standalone/hook-bus/config.mjs +207 -0
  270. package/src/standalone/hook-bus/constants.mjs +90 -0
  271. package/src/standalone/hook-bus/handlers.mjs +481 -0
  272. package/src/standalone/hook-bus/payload.mjs +31 -0
  273. package/src/standalone/hook-bus/rules.mjs +77 -0
  274. package/src/standalone/hook-bus.mjs +77 -870
  275. package/src/standalone/memory-runtime-proxy.mjs +7 -0
  276. package/src/standalone/opencode-go-login.mjs +5 -1
  277. package/src/standalone/provider-admin.mjs +1 -16
  278. package/src/standalone/usage-dashboard.mjs +3 -1
  279. package/src/tui/App.jsx +1059 -8110
  280. package/src/tui/app/app-format.mjs +213 -0
  281. package/src/tui/app/channel-pickers.mjs +508 -0
  282. package/src/tui/app/clipboard.mjs +67 -0
  283. package/src/tui/app/core-memory-picker.mjs +210 -0
  284. package/src/tui/app/extension-pickers.mjs +506 -0
  285. package/src/tui/app/input-parsers.mjs +193 -0
  286. package/src/tui/app/maintenance-pickers.mjs +356 -0
  287. package/src/tui/app/model-options.mjs +334 -0
  288. package/src/tui/app/model-picker.mjs +365 -0
  289. package/src/tui/app/onboarding-steps.mjs +400 -0
  290. package/src/tui/app/project-picker.mjs +247 -0
  291. package/src/tui/app/provider-setup-picker.mjs +580 -0
  292. package/src/tui/app/resume-picker.mjs +55 -0
  293. package/src/tui/app/route-pickers.mjs +419 -0
  294. package/src/tui/app/settings-picker.mjs +489 -0
  295. package/src/tui/app/slash-commands.mjs +101 -0
  296. package/src/tui/app/slash-dispatch.mjs +427 -0
  297. package/src/tui/app/text-layout.mjs +46 -0
  298. package/src/tui/app/theme-effort-pickers.mjs +154 -0
  299. package/src/tui/app/transcript-window.mjs +677 -0
  300. package/src/tui/app/use-mouse-input.mjs +460 -0
  301. package/src/tui/app/use-prompt-handlers.mjs +310 -0
  302. package/src/tui/app/use-transcript-scroll.mjs +512 -0
  303. package/src/tui/app/use-transcript-window.mjs +607 -0
  304. package/src/tui/components/ConfirmBar.jsx +10 -7
  305. package/src/tui/components/Picker.jsx +64 -15
  306. package/src/tui/components/PromptInput.jsx +33 -102
  307. package/src/tui/components/SlashCommandPalette.jsx +8 -1
  308. package/src/tui/components/StatusLine.jsx +69 -15
  309. package/src/tui/components/TextEntryPanel.jsx +11 -0
  310. package/src/tui/components/ToolExecution.jsx +52 -594
  311. package/src/tui/components/TranscriptItem.jsx +105 -0
  312. package/src/tui/components/UsagePanel.jsx +18 -4
  313. package/src/tui/components/prompt-input/edit-helpers.mjs +72 -0
  314. package/src/tui/components/prompt-input/voice-indicator.mjs +39 -0
  315. package/src/tui/components/tool-execution/ResultBody.jsx +56 -0
  316. package/src/tui/components/tool-execution/surface-detail.mjs +405 -0
  317. package/src/tui/components/tool-execution/text-format.mjs +161 -0
  318. package/src/tui/display-width.mjs +20 -3
  319. package/src/tui/dist/index.mjs +13553 -12384
  320. package/src/tui/engine/agent-job-feed.mjs +133 -0
  321. package/src/tui/engine/notification-plan.mjs +76 -0
  322. package/src/tui/engine/render-timing.mjs +17 -0
  323. package/src/tui/engine/tool-approval.mjs +94 -0
  324. package/src/tui/engine/tool-card-results.mjs +234 -0
  325. package/src/tui/engine/tool-result-status.mjs +135 -0
  326. package/src/tui/engine.mjs +170 -574
  327. package/src/tui/figures.mjs +5 -0
  328. package/src/tui/index.jsx +65 -1
  329. package/src/tui/input-editing.mjs +2 -2
  330. package/src/tui/markdown/format-token.mjs +4 -1
  331. package/src/tui/statusline-ansi-bridge.mjs +11 -3
  332. package/src/tui/theme.mjs +6 -0
  333. package/src/ui/statusline-agents.mjs +213 -0
  334. package/src/ui/statusline-format.mjs +146 -0
  335. package/src/ui/statusline-segments.mjs +148 -0
  336. package/src/ui/statusline.mjs +77 -501
  337. package/src/ui/tool-card.mjs +0 -1
  338. package/src/vendor/statusline/bin/statusline-route.mjs +15 -2
  339. package/src/workflows/default/WORKFLOW.md +16 -18
  340. package/src/workflows/sequential/WORKFLOW.md +16 -18
  341. package/vendor/ink/build/display-width.js +19 -3
  342. package/vendor/ink/build/ink.js +112 -7
  343. package/vendor/ink/build/log-update.js +17 -3
  344. package/vendor/ink/build/wrap-text.js +125 -0
  345. package/scripts/_test-folder-dialog.mjs +0 -30
  346. package/scripts/fix-brief-fn.mjs +0 -35
  347. package/scripts/fix-format-tool-surface.mjs +0 -24
  348. package/scripts/fix-tool-exec-visible.mjs +0 -42
  349. package/scripts/patch-agent-brief.mjs +0 -48
  350. package/scripts/patch-app.mjs +0 -21
  351. package/scripts/patch-app2.mjs +0 -18
  352. package/scripts/patch-dist-brief.mjs +0 -96
  353. package/scripts/patch-tool-exec.mjs +0 -70
  354. package/src/examples/schedules/SCHEDULE.example.md +0 -32
  355. package/src/examples/webhooks/WEBHOOK.example.md +0 -40
  356. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +0 -107
  357. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +0 -143
  358. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -285
  359. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +0 -162
  360. package/src/runtime/agent/orchestrator/tools/builtin/open-config-tool.mjs +0 -26
  361. package/src/runtime/channels/lib/holidays.mjs +0 -138
  362. package/src/runtime/shared/channel-notification-routing.test.mjs +0 -45
  363. package/src/runtime/shared/task-notification-envelope.test.mjs +0 -107
  364. package/src/runtime/shared/tool-execution-contract.test.mjs +0 -183
  365. package/src/standalone/agent-task-status.test.mjs +0 -76
  366. package/src/tui/components/tool-output-format.test.mjs +0 -399
  367. package/src/tui/display-width.test.mjs +0 -35
  368. package/src/tui/engine-runtime-notification.test.mjs +0 -115
  369. package/src/tui/engine-tool-result-text.test.mjs +0 -75
  370. package/src/tui/input-editing.selection.test.mjs +0 -75
  371. package/src/tui/markdown/format-token.test.mjs +0 -354
  372. package/src/tui/markdown/render-ansi.test.mjs +0 -108
  373. package/src/tui/markdown/stream-fence.test.mjs +0 -26
  374. package/src/tui/markdown/streaming-markdown.test.mjs +0 -70
  375. package/src/tui/paste-fix.test.mjs +0 -119
  376. package/src/tui/prompt-history-store.test.mjs +0 -52
  377. package/src/tui/statusline-ansi-bridge.test.mjs +0 -159
  378. package/src/tui/transcript-tool-failures.test.mjs +0 -111
  379. package/src/ui/markdown.test.mjs +0 -70
  380. package/src/ui/statusline-context-label.test.mjs +0 -15
  381. package/src/vendor/statusline/bin/statusline-lib.mjs +0 -186
  382. package/src/vendor/statusline/bin/statusline-route.test.mjs +0 -80
@@ -0,0 +1,233 @@
1
+ // Gateway connect / login / event-listener wiring extracted from discord.mjs.
2
+ // Implemented as instance-method installers that operate on a DiscordBackend
3
+ // `self`. Behavior is byte-identical to the originals; only the host object is
4
+ // passed explicitly instead of via `this`.
5
+
6
+ async function connect(self) {
7
+ // Re-entry guard: if a connect() is already in-flight or completed, return
8
+ // the same promise / no-op so concurrent ownership-timer fires cannot
9
+ // overwrite self.client, duplicate listeners, or trigger duplicate logins.
10
+ if (self._connectPromise) return self._connectPromise;
11
+ self._connectPromise = connectInner(self).catch((err) => {
12
+ self._connectPromise = null;
13
+ throw err;
14
+ });
15
+ return self._connectPromise;
16
+ }
17
+
18
+ async function connectInner(self) {
19
+ await buildClient(self);
20
+ applyStaticAccessOverride(self);
21
+ registerEventListeners(self);
22
+ registerSlashCommands(self);
23
+ registerShardListeners(self);
24
+ try {
25
+ await awaitLogin(self);
26
+ } catch (err) {
27
+ // Destroy the partial Client to free the listeners/handles it already
28
+ // attached. Without this, a ready-timeout retry leaks every listener
29
+ // set by registerEventListeners/registerSlashCommands/registerShardListeners.
30
+ try { self.client?.destroy?.(); } catch {}
31
+ self.client = null;
32
+ throw err;
33
+ }
34
+ self.persistAccessFromMainChannel();
35
+ }
36
+
37
+ async function buildClient(self) {
38
+ const { Client, GatewayIntentBits, Partials } = await self._ensureDiscord();
39
+ self.client = new Client({
40
+ intents: [
41
+ GatewayIntentBits.DirectMessages,
42
+ GatewayIntentBits.Guilds,
43
+ GatewayIntentBits.GuildMessages,
44
+ GatewayIntentBits.MessageContent
45
+ ],
46
+ partials: [Partials.Channel]
47
+ });
48
+ }
49
+
50
+ function applyStaticAccessOverride(self) {
51
+ if (self.isStatic) {
52
+ self.bootAccess = self.loadAccess();
53
+ }
54
+ }
55
+
56
+ function registerEventListeners(self) {
57
+ self.client.on("error", (err) => {
58
+ process.stderr.write(`mixdog discord: client error: ${err}
59
+ `);
60
+ });
61
+ self.client.on("messageCreate", (msg) => {
62
+ if (msg.author.id === self.client.user?.id) {
63
+ return;
64
+ }
65
+ if (msg.author.bot) return;
66
+ self.handleInbound(msg, Date.now()).catch(
67
+ (e) => process.stderr.write(`mixdog discord: handleInbound failed: ${e}
68
+ `)
69
+ );
70
+ });
71
+ self.client.on("interactionCreate", async (interaction) => {
72
+ try {
73
+ // Trust gate for interactions. Buttons / selects / modal submits used to
74
+ // reach onInteraction / onModalRequest without passing through the
75
+ // message gate(), so a configured allowFrom never applied to them
76
+ // (schedule/quiet/profile modals + perm approvals were openable by any
77
+ // user in the channel). Apply the same allowFrom decision here; an empty
78
+ // allowFrom stays open so current configs are unaffected.
79
+ if (!self._interactionAllowed(interaction.channelId ?? "", interaction.user?.id, interaction)) {
80
+ try {
81
+ if (typeof interaction.reply === "function" && !interaction.replied && !interaction.deferred) {
82
+ await interaction.reply({ content: "⛔ Not authorized for this action.", ephemeral: true });
83
+ } else if (typeof interaction.deferUpdate === "function") {
84
+ await interaction.deferUpdate();
85
+ }
86
+ } catch {}
87
+ return;
88
+ }
89
+ if (interaction.isChatInputCommand() && interaction.commandName === "stop") {
90
+ await interaction.reply({ content: "\u23F9 Stopping...", ephemeral: true });
91
+ if (self.onInteraction) {
92
+ self.onInteraction({
93
+ type: "button",
94
+ customId: "stop_task",
95
+ userId: interaction.user.id,
96
+ channelId: interaction.channelId ?? ""
97
+ });
98
+ }
99
+ return;
100
+ }
101
+ if (interaction.isModalSubmit()) {
102
+ if (self.onInteraction) {
103
+ const fields = {};
104
+ for (const row of interaction.components) {
105
+ for (const comp of row.components ?? []) {
106
+ if (comp.customId && comp.value != null) fields[comp.customId] = String(comp.value);
107
+ }
108
+ }
109
+ self.onInteraction({
110
+ type: "modal",
111
+ customId: interaction.customId,
112
+ userId: interaction.user.id,
113
+ channelId: interaction.channelId ?? "",
114
+ fields,
115
+ message: interaction.message ? { id: interaction.message.id } : void 0
116
+ });
117
+ }
118
+ await interaction.deferUpdate().catch(() => {
119
+ });
120
+ return;
121
+ }
122
+ if (interaction.isButton() || interaction.isStringSelectMenu() || interaction.isRoleSelectMenu() || interaction.isUserSelectMenu() || interaction.isChannelSelectMenu()) {
123
+ const needsModal = interaction.isButton() && (interaction.customId === "sched_add_next" || interaction.customId === "sched_edit_next" || interaction.customId === "profile_edit");
124
+ if (needsModal) {
125
+ if (self.onModalRequest) {
126
+ await Promise.resolve(self.onModalRequest(interaction)).catch((err) => {
127
+ process.stderr.write(`mixdog discord: onModalRequest failed: ${err}\n`);
128
+ });
129
+ }
130
+ return;
131
+ }
132
+ await interaction.deferUpdate().catch(() => {
133
+ });
134
+ if (self.onInteraction) {
135
+ self.onInteraction({
136
+ type: interaction.isButton() ? "button" : "select",
137
+ customId: interaction.customId,
138
+ userId: interaction.user.id,
139
+ channelId: interaction.channelId,
140
+ values: interaction.isStringSelectMenu() ? interaction.values : void 0,
141
+ message: interaction.message ? { id: interaction.message.id } : void 0
142
+ });
143
+ }
144
+ }
145
+ } catch (err) {
146
+ process.stderr.write(`mixdog discord: interaction error: ${err}
147
+ `);
148
+ }
149
+ });
150
+ }
151
+
152
+ function registerSlashCommands(self) {
153
+ self.client.on(self._discordEvents().ClientReady, async (c) => {
154
+ process.stderr.write(`mixdog discord: gateway connected as ${c.user.tag}
155
+ `);
156
+ try {
157
+ // Plugin registers no global commands; clear the global slot so any
158
+ // pre-existing entry from prior installs is not surfaced to users.
159
+ await c.application?.commands.set([]);
160
+ process.stderr.write(`mixdog discord: global application commands cleared
161
+ `);
162
+
163
+ // Replace each guild's command set with just /stop. set() overwrites,
164
+ // so the desired set is the only one that survives.
165
+ const desiredCommands = [
166
+ { name: "stop", description: "Stop the current Mixdog response" },
167
+ ];
168
+ for (const [guildId] of c.guilds.cache) {
169
+ await c.application?.commands.set(desiredCommands, guildId);
170
+ }
171
+ // Register /stop globally so it is available in DM bridge contexts
172
+ // where there is no guild scope.
173
+ try {
174
+ await c.application?.commands.set(desiredCommands);
175
+ } catch (e) {
176
+ process.stderr.write(`mixdog discord: global /stop register failed: ${e?.message}\n`);
177
+ }
178
+ process.stderr.write(`mixdog discord: /stop command registered (${c.guilds.cache.size} guild(s))
179
+ `);
180
+ } catch (err) {
181
+ process.stderr.write(`mixdog discord: slash command registration failed: ${err}
182
+ `);
183
+ }
184
+ });
185
+ }
186
+
187
+ function registerShardListeners(self) {
188
+ self.client.on("shardDisconnect", (ev, id) => {
189
+ process.stderr.write(`mixdog discord: shard ${id} disconnected (code ${ev.code}). Will auto-reconnect.
190
+ `);
191
+ });
192
+ self.client.on("shardReconnecting", (id) => {
193
+ process.stderr.write(`mixdog discord: shard ${id} reconnecting...
194
+ `);
195
+ });
196
+ self.client.on("shardResume", (id, replayedEvents) => {
197
+ process.stderr.write(`mixdog discord: shard ${id} resumed (replayed ${replayedEvents} events)
198
+ `);
199
+ });
200
+ self.client.on("warn", (msg) => {
201
+ process.stderr.write(`mixdog discord: warn: ${msg}
202
+ `);
203
+ });
204
+ }
205
+
206
+ async function awaitLogin(self) {
207
+ let readyTimeout;
208
+ const readyPromise = new Promise((resolve, reject) => {
209
+ readyTimeout = setTimeout(() => reject(new Error("discord ready timeout (30s)")), 3e4);
210
+ self.client.once(self._discordEvents().ClientReady, () => {
211
+ clearTimeout(readyTimeout);
212
+ resolve();
213
+ });
214
+ });
215
+ try {
216
+ await self.client.login(self.token);
217
+ } catch (err) {
218
+ clearTimeout(readyTimeout);
219
+ throw err;
220
+ }
221
+ await readyPromise;
222
+ }
223
+
224
+ export {
225
+ connect,
226
+ connectInner,
227
+ buildClient,
228
+ applyStaticAccessOverride,
229
+ registerEventListeners,
230
+ registerSlashCommands,
231
+ registerShardListeners,
232
+ awaitLogin
233
+ };
@@ -10,7 +10,6 @@ async function ensureDiscord() {
10
10
  return _discord;
11
11
  }
12
12
  import {
13
- writeFileSync,
14
13
  mkdirSync,
15
14
  statSync,
16
15
  realpathSync
@@ -20,37 +19,11 @@ import { createHash } from "crypto";
20
19
  import { chunk, formatForDiscord, MAX_DISCORD_MESSAGE } from "../lib/format.mjs";
21
20
  import { withConfigLock } from "../lib/config-lock.mjs";
22
21
  import { readSection, updateSection } from "../../shared/config.mjs";
22
+ import { normalizeAccess, safeAttName } from "./discord-access.mjs";
23
+ import { MAX_ATTACHMENT_BYTES, downloadSingleAttachment } from "./discord-attachments.mjs";
24
+ import * as gateway from "./discord-gateway.mjs";
23
25
  const MAX_CHUNK_LIMIT = MAX_DISCORD_MESSAGE;
24
- const MAX_ATTACHMENT_BYTES = 25 * 1024 * 1024;
25
26
  const RECENT_SENT_CAP = 200;
26
- function defaultAccess() {
27
- return {
28
- dmPolicy: "allowlist",
29
- allowFrom: [],
30
- channels: {}
31
- };
32
- }
33
- function normalizeAccess(parsed) {
34
- const defaults = defaultAccess();
35
- return {
36
- // Legacy "pairing" policy was removed (its approval flow was never
37
- // completable); normalize it to "allowlist" on load.
38
- dmPolicy: parsed?.dmPolicy === "pairing" ? "allowlist" : (parsed?.dmPolicy ?? defaults.dmPolicy),
39
- allowFrom: parsed?.allowFrom ?? defaults.allowFrom,
40
- channels: parsed?.channels ?? defaults.channels,
41
- mentionPatterns: parsed?.mentionPatterns,
42
- // Setup UI historically saved a boolean toggle; runtime needs an emoji
43
- // string for msg.react(). true → default emoji, non-string → off.
44
- ackReaction: parsed?.ackReaction === true
45
- ? "✅"
46
- : (typeof parsed?.ackReaction === "string" && parsed.ackReaction ? parsed.ackReaction : undefined),
47
- replyToMode: parsed?.replyToMode,
48
- textChunkLimit: parsed?.textChunkLimit,
49
- };
50
- }
51
- function safeAttName(att) {
52
- return (att.name ?? att.id).replace(/[\[\]\r\n;]/g, "_");
53
- }
54
27
  class DiscordBackend {
55
28
  name = "discord";
56
29
  MAX_MESSAGE_LENGTH = MAX_DISCORD_MESSAGE;
@@ -82,216 +55,16 @@ class DiscordBackend {
82
55
  formatOutgoing(text) {
83
56
  return formatForDiscord(text);
84
57
  }
85
- // ── Lifecycle ──────────────────────────────────────────────────────
86
- async connect() {
87
- // Re-entry guard: if a connect() is already in-flight or completed, return
88
- // the same promise / no-op so concurrent ownership-timer fires cannot
89
- // overwrite this.client, duplicate listeners, or trigger duplicate logins.
90
- if (this._connectPromise) return this._connectPromise;
91
- this._connectPromise = this._connectInner().catch((err) => {
92
- this._connectPromise = null;
93
- throw err;
94
- });
95
- return this._connectPromise;
96
- }
97
- async _connectInner() {
98
- await this._buildClient();
99
- this._applyStaticAccessOverride();
100
- this._registerEventListeners();
101
- this._registerSlashCommands();
102
- this._registerShardListeners();
103
- try {
104
- await this._awaitLogin();
105
- } catch (err) {
106
- // Destroy the partial Client to free the listeners/handles it already
107
- // attached. Without this, a ready-timeout retry leaks every listener
108
- // set by _registerEventListeners/_registerSlashCommands/_registerShardListeners.
109
- try { this.client?.destroy?.(); } catch {}
110
- this.client = null;
111
- throw err;
112
- }
113
- this.persistAccessFromChannelsConfig();
114
- }
115
- async _buildClient() {
116
- const { Client, GatewayIntentBits, Partials } = await ensureDiscord();
117
- this.client = new Client({
118
- intents: [
119
- GatewayIntentBits.DirectMessages,
120
- GatewayIntentBits.Guilds,
121
- GatewayIntentBits.GuildMessages,
122
- GatewayIntentBits.MessageContent
123
- ],
124
- partials: [Partials.Channel]
125
- });
126
- }
127
- _applyStaticAccessOverride() {
128
- if (this.isStatic) {
129
- this.bootAccess = this.loadAccess();
130
- }
131
- }
132
- _registerEventListeners() {
133
- this.client.on("error", (err) => {
134
- process.stderr.write(`mixdog discord: client error: ${err}
135
- `);
136
- });
137
- this.client.on("messageCreate", (msg) => {
138
- if (msg.author.id === this.client.user?.id) {
139
- return;
140
- }
141
- if (msg.author.bot) return;
142
- this.handleInbound(msg, Date.now()).catch(
143
- (e) => process.stderr.write(`mixdog discord: handleInbound failed: ${e}
144
- `)
145
- );
146
- });
147
- this.client.on("interactionCreate", async (interaction) => {
148
- try {
149
- // Trust gate for interactions. Buttons / selects / modal submits used to
150
- // reach onInteraction / onModalRequest without passing through the
151
- // message gate(), so a configured allowFrom never applied to them
152
- // (schedule/quiet/profile modals + perm approvals were openable by any
153
- // user in the channel). Apply the same allowFrom decision here; an empty
154
- // allowFrom stays open so current configs are unaffected.
155
- if (!this._interactionAllowed(interaction.channelId ?? "", interaction.user?.id, interaction)) {
156
- try {
157
- if (typeof interaction.reply === "function" && !interaction.replied && !interaction.deferred) {
158
- await interaction.reply({ content: "⛔ Not authorized for this action.", ephemeral: true });
159
- } else if (typeof interaction.deferUpdate === "function") {
160
- await interaction.deferUpdate();
161
- }
162
- } catch {}
163
- return;
164
- }
165
- if (interaction.isChatInputCommand() && interaction.commandName === "stop") {
166
- await interaction.reply({ content: "\u23F9 Stopping...", ephemeral: true });
167
- if (this.onInteraction) {
168
- this.onInteraction({
169
- type: "button",
170
- customId: "stop_task",
171
- userId: interaction.user.id,
172
- channelId: interaction.channelId ?? ""
173
- });
174
- }
175
- return;
176
- }
177
- if (interaction.isModalSubmit()) {
178
- if (this.onInteraction) {
179
- const fields = {};
180
- for (const row of interaction.components) {
181
- for (const comp of row.components ?? []) {
182
- if (comp.customId && comp.value != null) fields[comp.customId] = String(comp.value);
183
- }
184
- }
185
- this.onInteraction({
186
- type: "modal",
187
- customId: interaction.customId,
188
- userId: interaction.user.id,
189
- channelId: interaction.channelId ?? "",
190
- fields,
191
- message: interaction.message ? { id: interaction.message.id } : void 0
192
- });
193
- }
194
- await interaction.deferUpdate().catch(() => {
195
- });
196
- return;
197
- }
198
- if (interaction.isButton() || interaction.isStringSelectMenu() || interaction.isRoleSelectMenu() || interaction.isUserSelectMenu() || interaction.isChannelSelectMenu()) {
199
- const needsModal = interaction.isButton() && (interaction.customId === "sched_add_next" || interaction.customId === "sched_edit_next" || interaction.customId === "quiet_set_next" || interaction.customId === "activity_add_next" || interaction.customId === "profile_edit");
200
- if (needsModal) {
201
- if (this.onModalRequest) {
202
- await Promise.resolve(this.onModalRequest(interaction)).catch((err) => {
203
- process.stderr.write(`mixdog discord: onModalRequest failed: ${err}\n`);
204
- });
205
- }
206
- return;
207
- }
208
- await interaction.deferUpdate().catch(() => {
209
- });
210
- if (this.onInteraction) {
211
- this.onInteraction({
212
- type: interaction.isButton() ? "button" : "select",
213
- customId: interaction.customId,
214
- userId: interaction.user.id,
215
- channelId: interaction.channelId,
216
- values: interaction.isStringSelectMenu() ? interaction.values : void 0,
217
- message: interaction.message ? { id: interaction.message.id } : void 0
218
- });
219
- }
220
- }
221
- } catch (err) {
222
- process.stderr.write(`mixdog discord: interaction error: ${err}
223
- `);
224
- }
225
- });
226
- }
227
- _registerSlashCommands() {
228
- this.client.on(_discord.Events.ClientReady, async (c) => {
229
- process.stderr.write(`mixdog discord: gateway connected as ${c.user.tag}
230
- `);
231
- try {
232
- // Plugin registers no global commands; clear the global slot so any
233
- // pre-existing entry from prior installs is not surfaced to users.
234
- await c.application?.commands.set([]);
235
- process.stderr.write(`mixdog discord: global application commands cleared
236
- `);
237
-
238
- // Replace each guild's command set with just /stop. set() overwrites,
239
- // so the desired set is the only one that survives.
240
- const desiredCommands = [
241
- { name: "stop", description: "Stop the current Mixdog response" },
242
- ];
243
- for (const [guildId] of c.guilds.cache) {
244
- await c.application?.commands.set(desiredCommands, guildId);
245
- }
246
- // Register /stop globally so it is available in DM bridge contexts
247
- // where there is no guild scope.
248
- try {
249
- await c.application?.commands.set(desiredCommands);
250
- } catch (e) {
251
- process.stderr.write(`mixdog discord: global /stop register failed: ${e?.message}\n`);
252
- }
253
- process.stderr.write(`mixdog discord: /stop command registered (${c.guilds.cache.size} guild(s))
254
- `);
255
- } catch (err) {
256
- process.stderr.write(`mixdog discord: slash command registration failed: ${err}
257
- `);
258
- }
259
- });
58
+ // ── discord.js access bridges for extracted gateway module ─────────
59
+ _ensureDiscord() {
60
+ return ensureDiscord();
260
61
  }
261
- _registerShardListeners() {
262
- this.client.on("shardDisconnect", (ev, id) => {
263
- process.stderr.write(`mixdog discord: shard ${id} disconnected (code ${ev.code}). Will auto-reconnect.
264
- `);
265
- });
266
- this.client.on("shardReconnecting", (id) => {
267
- process.stderr.write(`mixdog discord: shard ${id} reconnecting...
268
- `);
269
- });
270
- this.client.on("shardResume", (id, replayedEvents) => {
271
- process.stderr.write(`mixdog discord: shard ${id} resumed (replayed ${replayedEvents} events)
272
- `);
273
- });
274
- this.client.on("warn", (msg) => {
275
- process.stderr.write(`mixdog discord: warn: ${msg}
276
- `);
277
- });
62
+ _discordEvents() {
63
+ return _discord.Events;
278
64
  }
279
- async _awaitLogin() {
280
- let readyTimeout;
281
- const readyPromise = new Promise((resolve, reject) => {
282
- readyTimeout = setTimeout(() => reject(new Error("discord ready timeout (30s)")), 3e4);
283
- this.client.once(_discord.Events.ClientReady, () => {
284
- clearTimeout(readyTimeout);
285
- resolve();
286
- });
287
- });
288
- try {
289
- await this.client.login(this.token);
290
- } catch (err) {
291
- clearTimeout(readyTimeout);
292
- throw err;
293
- }
294
- await readyPromise;
65
+ // ── Lifecycle ──────────────────────────────────────────────────────
66
+ async connect() {
67
+ return gateway.connect(this);
295
68
  }
296
69
  async disconnect() {
297
70
  for (const interval of this.typingIntervals.values()) {
@@ -553,47 +326,37 @@ class DiscordBackend {
553
326
  try {
554
327
  const parsed = readSection("channels");
555
328
  const access = normalizeAccess(parsed.access ?? this.initialAccess);
556
- if (parsed.channelsConfig) {
557
- for (const entry of Object.values(parsed.channelsConfig)) {
558
- if (typeof entry === "object" && entry !== null) {
559
- const id = entry.channelId;
560
- if (id && !(id in access.channels)) {
561
- access.channels[id] = { requireMention: false, allowFrom: [] };
562
- }
563
- }
564
- }
329
+ // Single main channel: auto-allow the configured channel id so channel
330
+ // adds apply without a restart (mirrors loadAccess()).
331
+ const id = this.mainChannelId;
332
+ if (id && !(id in access.channels)) {
333
+ access.channels[id] = { requireMention: false, allowFrom: [] };
565
334
  }
566
335
  return access;
567
336
  } catch {
568
337
  return this.initialAccess;
569
338
  }
570
339
  }
571
- persistAccessFromChannelsConfig() {
340
+ persistAccessFromMainChannel() {
572
341
  if (this.isStatic || !this.configFile) return;
573
342
  try {
343
+ const id = this.mainChannelId;
344
+ if (!id) return;
574
345
  const parsed = readSection("channels");
575
- if (!parsed.channelsConfig) return;
576
346
  const access = normalizeAccess(parsed.access);
577
- let changed = false;
578
- for (const entry of Object.values(parsed.channelsConfig)) {
579
- if (typeof entry === "object" && entry !== null) {
580
- const id = entry.channelId;
581
- if (id && !(id in access.channels)) {
582
- access.channels[id] = { requireMention: false, allowFrom: [] };
583
- changed = true;
584
- }
585
- }
347
+ if (!(id in access.channels)) {
348
+ access.channels[id] = { requireMention: false, allowFrom: [] };
349
+ this.saveAccess(access);
586
350
  }
587
- if (changed) this.saveAccess(access);
588
351
  } catch (err) {
589
- process.stderr.write(`mixdog discord: persistAccessFromChannelsConfig failed: ${err}\n`);
352
+ process.stderr.write(`mixdog discord: persistAccessFromMainChannel failed: ${err}\n`);
590
353
  }
591
354
  }
592
355
  loadAccess() {
593
356
  const a = this.bootAccess ?? this.readConfigAccess();
594
357
  // Single-source channel setup: auto-allow the configured main channel so
595
358
  // both inbound (gate) and outbound (fetchAllowedChannel) accept it from a
596
- // single channelsConfig.main.channelId — no separate access.channels entry.
359
+ // single channel.channelId — no separate access.channels entry.
597
360
  // An explicit entry for the same channel is preserved (not overwritten).
598
361
  if (this.mainChannelId && a && !(this.mainChannelId in (a.channels ?? {}))) {
599
362
  return { ...a, channels: { ...(a.channels ?? {}), [this.mainChannelId]: { allowFrom: [], requireMention: false } } };
@@ -743,9 +506,8 @@ class DiscordBackend {
743
506
  }
744
507
  if (this.onMessage) {
745
508
  // Thread messages were gated against the parent channel's policy (see
746
- // gate() above), but routing also needs the parent id to find labels/
747
- // modes in channelsConfig. Surface parentChatId so resolveInboundRoute
748
- // can fall back to the parent when the thread id has no entry.
509
+ // gate() above). Surface parentChatId so downstream routing can fall
510
+ // back to the parent when the thread id needs it.
749
511
  const isThread = (() => { try { return !!msg.channel?.isThread?.(); } catch { return false; } })();
750
512
  const parentChatId = isThread ? (msg.channel.parentId ?? null) : null;
751
513
  this.onMessage({
@@ -810,60 +572,7 @@ class DiscordBackend {
810
572
  }
811
573
  }
812
574
  async downloadSingleAttachment(att) {
813
- if (att.size > MAX_ATTACHMENT_BYTES) {
814
- throw new Error(
815
- `attachment too large: ${(att.size / 1024 / 1024).toFixed(1)}MB, max ${MAX_ATTACHMENT_BYTES / 1024 / 1024}MB`
816
- );
817
- }
818
- const res = await fetch(att.url, { signal: AbortSignal.timeout(180_000) });
819
- if (!res.ok) {
820
- throw new Error(`attachment download failed: HTTP ${res.status}`);
821
- }
822
- if (!res.body) {
823
- throw new Error(`attachment download returned empty body: ${att.name ?? att.id}`);
824
- }
825
- // Stream the response so an oversized payload is aborted before the
826
- // full body lands in memory. Buffering via arrayBuffer() first would
827
- // already exceed MAX_ATTACHMENT_BYTES by the time we checked length.
828
- const reader = res.body.getReader();
829
- const chunks = [];
830
- let received = 0;
831
- try {
832
- while (true) {
833
- const { done, value } = await reader.read();
834
- if (done) break;
835
- if (!value) continue;
836
- received += value.byteLength;
837
- if (received > MAX_ATTACHMENT_BYTES) {
838
- try { await reader.cancel(); } catch {}
839
- throw new Error(
840
- `attachment payload too large: exceeded ${MAX_ATTACHMENT_BYTES / 1024 / 1024}MB while streaming (${att.name ?? att.id})`
841
- );
842
- }
843
- chunks.push(value);
844
- }
845
- } finally {
846
- try { reader.releaseLock(); } catch {}
847
- }
848
- if (received === 0) {
849
- throw new Error(`attachment download returned empty buffer: ${att.name ?? att.id}`);
850
- }
851
- const buf = Buffer.concat(chunks.map((c) => Buffer.from(c.buffer, c.byteOffset, c.byteLength)), received);
852
- if (att.size > 0 && buf.length !== att.size) {
853
- process.stderr.write(`mixdog discord: attachment size mismatch: expected ${att.size} got ${buf.length} (${att.name ?? att.id})\n`);
854
- }
855
- const name = att.name ?? `${att.id}`;
856
- const rawExt = name.includes(".") ? name.slice(name.lastIndexOf(".") + 1) : "bin";
857
- const ext = rawExt.replace(/[^a-zA-Z0-9]/g, "") || "bin";
858
- const safeId = String(att.id).replace(/[^a-zA-Z0-9_-]/g, "_");
859
- mkdirSync(this.inboxDir, { recursive: true });
860
- const candidate = join(this.inboxDir, `${Date.now()}-${safeId}.${ext}`);
861
- const resolvedInbox = realpathSync(this.inboxDir);
862
- if (!candidate.startsWith(resolvedInbox + sep)) {
863
- throw new Error(`attachment path traversal rejected: ${candidate}`);
864
- }
865
- writeFileSync(candidate, buf);
866
- return candidate;
575
+ return downloadSingleAttachment(att, this.inboxDir);
867
576
  }
868
577
  }
869
578
  export {
@@ -112,22 +112,18 @@ class TelegramBackend {
112
112
  formatOutgoing(text) {
113
113
  return text;
114
114
  }
115
- // Fold every channelsConfig entry's channelId into access.channels, mirroring
116
- // DiscordBackend.readConfigAccess(). Read live so same-backend channel adds
117
- // apply without a restart. Falls back to initialAccess if the read throws.
115
+ // Fold the single configured main channel id into access.channels, mirroring
116
+ // DiscordBackend.readConfigAccess(). Read live so a channel change applies
117
+ // without a restart. Falls back to initialAccess if the read throws.
118
118
  readConfigAccess() {
119
119
  try {
120
120
  const parsed = readSection("channels");
121
121
  const access = normalizeAccess(parsed.access ?? this.initialAccess);
122
- if (parsed.channelsConfig) {
123
- for (const entry of Object.values(parsed.channelsConfig)) {
124
- if (typeof entry === "object" && entry !== null) {
125
- const id = entry.channelId;
126
- if (id && !(id in access.channels)) {
127
- access.channels[id] = { requireMention: false, allowFrom: [] };
128
- }
129
- }
130
- }
122
+ // Single main channel: auto-allow the configured channel id so a channel
123
+ // change applies without a restart (mirrors loadAccess()).
124
+ const id = this.mainChannelId;
125
+ if (id && !(id in access.channels)) {
126
+ access.channels[id] = { requireMention: false, allowFrom: [] };
131
127
  }
132
128
  return access;
133
129
  } catch {