mixdog 0.7.18 → 0.8.1

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 (847) hide show
  1. package/README.md +37 -331
  2. package/package.json +67 -99
  3. package/scripts/boot-smoke.mjs +94 -0
  4. package/scripts/build-tui.mjs +52 -0
  5. package/scripts/compact-smoke.mjs +199 -0
  6. package/scripts/lead-workflow-smoke.mjs +598 -0
  7. package/scripts/live-worker-smoke.mjs +239 -0
  8. package/scripts/output-style-smoke.mjs +101 -0
  9. package/scripts/session-context-bench.mjs +172 -0
  10. package/scripts/smoke-loop-report.mjs +221 -0
  11. package/scripts/smoke-loop.mjs +201 -0
  12. package/scripts/smoke.mjs +113 -0
  13. package/scripts/tool-failures.mjs +143 -0
  14. package/scripts/tool-smoke.mjs +452 -0
  15. package/src/agents/debugger/AGENT.md +3 -0
  16. package/src/agents/debugger/agent.json +6 -0
  17. package/src/agents/explore/AGENT.md +4 -0
  18. package/src/agents/explore/agent.json +6 -0
  19. package/src/agents/heavy-worker/AGENT.md +3 -0
  20. package/src/agents/heavy-worker/agent.json +6 -0
  21. package/src/agents/maintainer/AGENT.md +3 -0
  22. package/src/agents/maintainer/agent.json +6 -0
  23. package/src/agents/reviewer/AGENT.md +3 -0
  24. package/src/agents/reviewer/agent.json +6 -0
  25. package/src/agents/scheduler-task.md +3 -0
  26. package/src/agents/web-researcher/AGENT.md +3 -0
  27. package/src/agents/web-researcher/agent.json +6 -0
  28. package/src/agents/webhook-handler.md +3 -0
  29. package/src/agents/worker/AGENT.md +3 -0
  30. package/src/agents/worker/agent.json +6 -0
  31. package/src/app.mjs +90 -0
  32. package/src/cli.mjs +11 -0
  33. package/src/defaults/hidden-roles.json +72 -0
  34. package/src/defaults/mixdog-config.template.json +15 -0
  35. package/src/hooks/lib/permission-evaluator.cjs +488 -0
  36. package/src/hooks/lib/settings-loader.cjs +112 -0
  37. package/src/lib/keychain-cjs.cjs +332 -0
  38. package/src/lib/plugin-paths.cjs +28 -0
  39. package/src/lib/rules-builder.cjs +315 -0
  40. package/src/mixdog-session-runtime.mjs +3813 -0
  41. package/src/output-styles/default.md +38 -0
  42. package/src/output-styles/extreme-simple.md +17 -0
  43. package/src/output-styles/simple.md +17 -0
  44. package/src/repl.mjs +330 -0
  45. package/src/rules/bridge/00-common.md +5 -0
  46. package/src/rules/bridge/20-skip-protocol.md +11 -0
  47. package/src/rules/bridge/30-explorer.md +4 -0
  48. package/src/rules/bridge/40-cycle1-agent.md +28 -0
  49. package/src/rules/bridge/41-cycle2-agent.md +59 -0
  50. package/src/rules/lead/00-tool-lead.md +5 -0
  51. package/src/rules/lead/01-general.md +5 -0
  52. package/src/rules/lead/02-channels.md +3 -0
  53. package/src/rules/lead/04-workflow.md +12 -0
  54. package/src/rules/shared/00-language.md +3 -0
  55. package/src/rules/shared/01-tool.md +3 -0
  56. package/src/runtime/agent/orchestrator/bridge-trace.mjs +814 -0
  57. package/src/runtime/agent/orchestrator/cache-mtime.mjs +60 -0
  58. package/src/runtime/agent/orchestrator/config.mjs +446 -0
  59. package/src/runtime/agent/orchestrator/context/collect.mjs +796 -0
  60. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +417 -0
  61. package/src/runtime/agent/orchestrator/internal-roles.mjs +188 -0
  62. package/src/runtime/agent/orchestrator/internal-tools.mjs +88 -0
  63. package/src/runtime/agent/orchestrator/mcp/client.mjs +345 -0
  64. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +2104 -0
  65. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +784 -0
  66. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +341 -0
  67. package/src/runtime/agent/orchestrator/providers/gemini.mjs +1679 -0
  68. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +959 -0
  69. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +213 -0
  70. package/src/runtime/agent/orchestrator/providers/model-cache.mjs +38 -0
  71. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +471 -0
  72. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +615 -0
  73. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +808 -0
  74. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +1719 -0
  75. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +2587 -0
  76. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +1953 -0
  77. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +136 -0
  78. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +317 -0
  79. package/src/runtime/agent/orchestrator/providers/opencode-go.mjs +109 -0
  80. package/src/runtime/agent/orchestrator/providers/registry.mjs +247 -0
  81. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +332 -0
  82. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +11 -0
  83. package/src/runtime/agent/orchestrator/providers/trace-utils.mjs +50 -0
  84. package/src/runtime/agent/orchestrator/session/cache/post-edit-marks.mjs +42 -0
  85. package/src/runtime/agent/orchestrator/session/cache/prefetch-cache.mjs +142 -0
  86. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +318 -0
  87. package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +367 -0
  88. package/src/runtime/agent/orchestrator/session/compact.mjs +882 -0
  89. package/src/runtime/agent/orchestrator/session/context-utils.mjs +233 -0
  90. package/src/runtime/agent/orchestrator/session/loop.mjs +2269 -0
  91. package/src/runtime/agent/orchestrator/session/manager.mjs +2972 -0
  92. package/src/runtime/agent/orchestrator/session/result-classification.mjs +65 -0
  93. package/src/runtime/agent/orchestrator/session/store.mjs +870 -0
  94. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +166 -0
  95. package/src/runtime/agent/orchestrator/smart-bridge/bridge-llm.mjs +339 -0
  96. package/src/runtime/agent/orchestrator/smart-bridge/cache-strategy.mjs +419 -0
  97. package/src/runtime/agent/orchestrator/stall-policy.mjs +227 -0
  98. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +235 -0
  99. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +723 -0
  100. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +171 -0
  101. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +389 -0
  102. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +637 -0
  103. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +165 -0
  104. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +104 -0
  105. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +194 -0
  106. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +596 -0
  107. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +110 -0
  108. package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +153 -0
  109. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +118 -0
  110. package/src/runtime/agent/orchestrator/tools/builtin/read-open.mjs +189 -0
  111. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +731 -0
  112. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +168 -0
  113. package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +602 -0
  114. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +465 -0
  115. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +160 -0
  116. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +982 -0
  117. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +1087 -0
  118. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +231 -0
  119. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +223 -0
  120. package/src/runtime/agent/orchestrator/tools/builtin.mjs +478 -0
  121. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +24 -0
  122. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4102 -0
  123. package/src/runtime/agent/orchestrator/tools/destructive-warning.mjs +323 -0
  124. package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +154 -0
  125. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +26 -0
  126. package/src/runtime/agent/orchestrator/tools/patch-binary-fetcher.mjs +143 -0
  127. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +18 -0
  128. package/src/runtime/agent/orchestrator/tools/patch.mjs +2772 -0
  129. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +114 -0
  130. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +880 -0
  131. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +312 -0
  132. package/src/runtime/channels/backends/discord.mjs +781 -0
  133. package/src/runtime/channels/data/voice-runtime-manifest.json +138 -0
  134. package/src/runtime/channels/index.mjs +3309 -0
  135. package/src/runtime/channels/lib/config.mjs +285 -0
  136. package/src/runtime/channels/lib/drop-trace.mjs +71 -0
  137. package/src/runtime/channels/lib/event-pipeline.mjs +81 -0
  138. package/src/runtime/channels/lib/holidays.mjs +138 -0
  139. package/src/runtime/channels/lib/hook-pipe-server.mjs +671 -0
  140. package/src/runtime/channels/lib/output-forwarder.mjs +765 -0
  141. package/src/runtime/channels/lib/runtime-paths.mjs +497 -0
  142. package/src/runtime/channels/lib/scheduler.mjs +710 -0
  143. package/src/runtime/channels/lib/session-discovery.mjs +102 -0
  144. package/src/runtime/channels/lib/state-file.mjs +68 -0
  145. package/src/runtime/channels/lib/status-snapshot.mjs +224 -0
  146. package/src/runtime/channels/lib/tool-format.mjs +122 -0
  147. package/src/runtime/channels/lib/transcript-discovery.mjs +195 -0
  148. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +734 -0
  149. package/src/runtime/channels/lib/webhook.mjs +1288 -0
  150. package/src/runtime/channels/tool-defs.mjs +177 -0
  151. package/src/runtime/lib/keychain-cjs.cjs +289 -0
  152. package/src/runtime/memory/data/runtime-manifest.json +40 -0
  153. package/src/runtime/memory/index.mjs +3706 -0
  154. package/src/runtime/memory/lib/core-memory-store.mjs +336 -0
  155. package/src/runtime/memory/lib/embedding-provider.mjs +275 -0
  156. package/src/runtime/memory/lib/embedding-worker.mjs +331 -0
  157. package/src/runtime/memory/lib/memory-cycle-requests.mjs +276 -0
  158. package/src/runtime/memory/lib/memory-cycle1.mjs +783 -0
  159. package/src/runtime/memory/lib/memory-cycle2.mjs +1389 -0
  160. package/src/runtime/memory/lib/memory-cycle3.mjs +646 -0
  161. package/src/runtime/memory/lib/memory-embed.mjs +300 -0
  162. package/src/runtime/memory/lib/memory-ops-policy.mjs +149 -0
  163. package/src/runtime/memory/lib/memory-recall-store.mjs +644 -0
  164. package/src/runtime/memory/lib/memory.mjs +418 -0
  165. package/src/runtime/memory/lib/pg/adapter.mjs +328 -0
  166. package/src/runtime/memory/lib/pg/process.mjs +366 -0
  167. package/src/runtime/memory/lib/pg/supervisor.mjs +495 -0
  168. package/src/runtime/memory/lib/runtime-fetcher.mjs +464 -0
  169. package/src/runtime/memory/lib/trace-store.mjs +734 -0
  170. package/src/runtime/memory/tool-defs.mjs +79 -0
  171. package/src/runtime/search/index.mjs +925 -0
  172. package/src/runtime/search/lib/config.mjs +61 -0
  173. package/src/runtime/search/lib/web-tools.mjs +1278 -0
  174. package/src/runtime/search/tool-defs.mjs +64 -0
  175. package/src/runtime/shared/atomic-file.mjs +435 -0
  176. package/src/runtime/shared/background-tasks.mjs +376 -0
  177. package/src/runtime/shared/child-guardian.mjs +98 -0
  178. package/src/runtime/shared/config.mjs +393 -0
  179. package/src/runtime/shared/err-text.mjs +121 -0
  180. package/src/runtime/shared/launcher-control.mjs +259 -0
  181. package/src/runtime/shared/llm/http-agent.mjs +129 -0
  182. package/src/runtime/shared/open-url.mjs +37 -0
  183. package/src/runtime/shared/plugin-paths.mjs +25 -0
  184. package/src/runtime/shared/process-shutdown.mjs +147 -0
  185. package/src/runtime/shared/schedules-store.mjs +70 -0
  186. package/src/runtime/shared/tool-execution-contract.mjs +104 -0
  187. package/src/runtime/shared/tool-surface.mjs +947 -0
  188. package/src/runtime/shared/user-cwd.mjs +221 -0
  189. package/src/runtime/shared/user-data-guard.mjs +232 -0
  190. package/src/runtime/shared/workspace-router.mjs +259 -0
  191. package/src/standalone/bridge-tool.mjs +1414 -0
  192. package/src/standalone/channel-admin.mjs +366 -0
  193. package/src/standalone/channel-worker-preload.cjs +3 -0
  194. package/src/standalone/channel-worker.mjs +353 -0
  195. package/src/standalone/explore-tool.mjs +233 -0
  196. package/src/standalone/hook-bus.mjs +246 -0
  197. package/src/standalone/plugin-admin.mjs +247 -0
  198. package/src/standalone/provider-admin.mjs +338 -0
  199. package/src/standalone/seeds.mjs +94 -0
  200. package/src/standalone/usage-dashboard.mjs +510 -0
  201. package/src/tui/App.jsx +5446 -0
  202. package/src/tui/components/AnsiText.jsx +199 -0
  203. package/src/tui/components/ContextPanel.jsx +265 -0
  204. package/src/tui/components/Markdown.jsx +205 -0
  205. package/src/tui/components/MarkdownTable.jsx +204 -0
  206. package/src/tui/components/Message.jsx +103 -0
  207. package/src/tui/components/Picker.jsx +317 -0
  208. package/src/tui/components/PromptInput.jsx +584 -0
  209. package/src/tui/components/QueuedCommands.jsx +47 -0
  210. package/src/tui/components/SlashCommandPalette.jsx +114 -0
  211. package/src/tui/components/Spinner.jsx +317 -0
  212. package/src/tui/components/StatusLine.jsx +87 -0
  213. package/src/tui/components/TextEntryPanel.jsx +323 -0
  214. package/src/tui/components/ToolExecution.jsx +791 -0
  215. package/src/tui/components/TurnDone.jsx +78 -0
  216. package/src/tui/components/UsagePanel.jsx +331 -0
  217. package/src/tui/dist/index.mjs +12420 -0
  218. package/src/tui/engine.mjs +2410 -0
  219. package/src/tui/figures.mjs +50 -0
  220. package/src/tui/hooks/useEngine.mjs +16 -0
  221. package/src/tui/index.jsx +254 -0
  222. package/src/tui/input-editing.mjs +242 -0
  223. package/src/tui/markdown/format-token.mjs +194 -0
  224. package/src/tui/paste-attachments.mjs +198 -0
  225. package/src/tui/runtime/shared/process-shutdown.mjs +1 -0
  226. package/src/tui/spinner-verbs.mjs +45 -0
  227. package/src/tui/theme.mjs +67 -0
  228. package/src/tui/time-format.mjs +53 -0
  229. package/src/ui/ansi.mjs +115 -0
  230. package/src/ui/markdown.mjs +195 -0
  231. package/src/ui/statusline.mjs +730 -0
  232. package/src/ui/tool-card.mjs +99 -0
  233. package/src/vendor/statusline/bin/statusline-lib.mjs +805 -0
  234. package/src/vendor/statusline/bin/statusline-route.mjs +596 -0
  235. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +285 -0
  236. package/src/vendor/statusline/src/gateway/claude-current.mjs +320 -0
  237. package/src/vendor/statusline/src/gateway/route-meta.mjs +753 -0
  238. package/src/vendor/statusline/src/gateway/session-routes.mjs +244 -0
  239. package/src/workflows/default/WORKFLOW.md +7 -0
  240. package/src/workflows/default/workflow.json +14 -0
  241. package/vendor/ink/build/ansi-tokenizer.d.ts +38 -0
  242. package/vendor/ink/build/ansi-tokenizer.js +316 -0
  243. package/vendor/ink/build/ansi-tokenizer.js.map +1 -0
  244. package/vendor/ink/build/colorize.d.ts +3 -0
  245. package/vendor/ink/build/colorize.js +48 -0
  246. package/vendor/ink/build/colorize.js.map +1 -0
  247. package/vendor/ink/build/components/AccessibilityContext.d.ts +3 -0
  248. package/vendor/ink/build/components/AccessibilityContext.js +5 -0
  249. package/vendor/ink/build/components/AccessibilityContext.js.map +1 -0
  250. package/vendor/ink/build/components/AnimationContext.d.ts +9 -0
  251. package/vendor/ink/build/components/AnimationContext.js +13 -0
  252. package/vendor/ink/build/components/AnimationContext.js.map +1 -0
  253. package/vendor/ink/build/components/App.d.ts +24 -0
  254. package/vendor/ink/build/components/App.js +554 -0
  255. package/vendor/ink/build/components/App.js.map +1 -0
  256. package/vendor/ink/build/components/AppContext.d.ts +80 -0
  257. package/vendor/ink/build/components/AppContext.js +25 -0
  258. package/vendor/ink/build/components/AppContext.js.map +1 -0
  259. package/vendor/ink/build/components/BackgroundContext.d.ts +4 -0
  260. package/vendor/ink/build/components/BackgroundContext.js +3 -0
  261. package/vendor/ink/build/components/BackgroundContext.js.map +1 -0
  262. package/vendor/ink/build/components/Box.d.ts +130 -0
  263. package/vendor/ink/build/components/Box.js +34 -0
  264. package/vendor/ink/build/components/Box.js.map +1 -0
  265. package/vendor/ink/build/components/CursorContext.d.ts +11 -0
  266. package/vendor/ink/build/components/CursorContext.js +8 -0
  267. package/vendor/ink/build/components/CursorContext.js.map +1 -0
  268. package/vendor/ink/build/components/ErrorBoundary.d.ts +18 -0
  269. package/vendor/ink/build/components/ErrorBoundary.js +23 -0
  270. package/vendor/ink/build/components/ErrorBoundary.js.map +1 -0
  271. package/vendor/ink/build/components/ErrorOverview.d.ts +6 -0
  272. package/vendor/ink/build/components/ErrorOverview.js +90 -0
  273. package/vendor/ink/build/components/ErrorOverview.js.map +1 -0
  274. package/vendor/ink/build/components/FocusContext.d.ts +16 -0
  275. package/vendor/ink/build/components/FocusContext.js +17 -0
  276. package/vendor/ink/build/components/FocusContext.js.map +1 -0
  277. package/vendor/ink/build/components/Newline.d.ts +13 -0
  278. package/vendor/ink/build/components/Newline.js +8 -0
  279. package/vendor/ink/build/components/Newline.js.map +1 -0
  280. package/vendor/ink/build/components/Spacer.d.ts +7 -0
  281. package/vendor/ink/build/components/Spacer.js +11 -0
  282. package/vendor/ink/build/components/Spacer.js.map +1 -0
  283. package/vendor/ink/build/components/Static.d.ts +24 -0
  284. package/vendor/ink/build/components/Static.js +28 -0
  285. package/vendor/ink/build/components/Static.js.map +1 -0
  286. package/vendor/ink/build/components/StderrContext.d.ts +15 -0
  287. package/vendor/ink/build/components/StderrContext.js +13 -0
  288. package/vendor/ink/build/components/StderrContext.js.map +1 -0
  289. package/vendor/ink/build/components/StdinContext.d.ts +28 -0
  290. package/vendor/ink/build/components/StdinContext.js +20 -0
  291. package/vendor/ink/build/components/StdinContext.js.map +1 -0
  292. package/vendor/ink/build/components/StdoutContext.d.ts +15 -0
  293. package/vendor/ink/build/components/StdoutContext.js +13 -0
  294. package/vendor/ink/build/components/StdoutContext.js.map +1 -0
  295. package/vendor/ink/build/components/Text.d.ts +55 -0
  296. package/vendor/ink/build/components/Text.js +50 -0
  297. package/vendor/ink/build/components/Text.js.map +1 -0
  298. package/vendor/ink/build/components/Transform.d.ts +16 -0
  299. package/vendor/ink/build/components/Transform.js +15 -0
  300. package/vendor/ink/build/components/Transform.js.map +1 -0
  301. package/vendor/ink/build/cursor-helpers.d.ts +39 -0
  302. package/vendor/ink/build/cursor-helpers.js +62 -0
  303. package/vendor/ink/build/cursor-helpers.js.map +1 -0
  304. package/vendor/ink/build/devtools-window-polyfill.d.ts +1 -0
  305. package/vendor/ink/build/devtools-window-polyfill.js +68 -0
  306. package/vendor/ink/build/devtools-window-polyfill.js.map +1 -0
  307. package/vendor/ink/build/devtools.d.ts +1 -0
  308. package/vendor/ink/build/devtools.js +36 -0
  309. package/vendor/ink/build/devtools.js.map +1 -0
  310. package/vendor/ink/build/dom.d.ts +62 -0
  311. package/vendor/ink/build/dom.js +143 -0
  312. package/vendor/ink/build/dom.js.map +1 -0
  313. package/vendor/ink/build/get-max-width.d.ts +3 -0
  314. package/vendor/ink/build/get-max-width.js +10 -0
  315. package/vendor/ink/build/get-max-width.js.map +1 -0
  316. package/vendor/ink/build/hooks/use-animation.d.ts +49 -0
  317. package/vendor/ink/build/hooks/use-animation.js +87 -0
  318. package/vendor/ink/build/hooks/use-animation.js.map +1 -0
  319. package/vendor/ink/build/hooks/use-app.d.ts +5 -0
  320. package/vendor/ink/build/hooks/use-app.js +8 -0
  321. package/vendor/ink/build/hooks/use-app.js.map +1 -0
  322. package/vendor/ink/build/hooks/use-box-metrics.d.ts +59 -0
  323. package/vendor/ink/build/hooks/use-box-metrics.js +81 -0
  324. package/vendor/ink/build/hooks/use-box-metrics.js.map +1 -0
  325. package/vendor/ink/build/hooks/use-cursor.d.ts +12 -0
  326. package/vendor/ink/build/hooks/use-cursor.js +29 -0
  327. package/vendor/ink/build/hooks/use-cursor.js.map +1 -0
  328. package/vendor/ink/build/hooks/use-focus-manager.d.ts +43 -0
  329. package/vendor/ink/build/hooks/use-focus-manager.js +18 -0
  330. package/vendor/ink/build/hooks/use-focus-manager.js.map +1 -0
  331. package/vendor/ink/build/hooks/use-focus.d.ts +30 -0
  332. package/vendor/ink/build/hooks/use-focus.js +43 -0
  333. package/vendor/ink/build/hooks/use-focus.js.map +1 -0
  334. package/vendor/ink/build/hooks/use-input.d.ts +132 -0
  335. package/vendor/ink/build/hooks/use-input.js +126 -0
  336. package/vendor/ink/build/hooks/use-input.js.map +1 -0
  337. package/vendor/ink/build/hooks/use-is-screen-reader-enabled.d.ts +6 -0
  338. package/vendor/ink/build/hooks/use-is-screen-reader-enabled.js +12 -0
  339. package/vendor/ink/build/hooks/use-is-screen-reader-enabled.js.map +1 -0
  340. package/vendor/ink/build/hooks/use-paste.d.ts +35 -0
  341. package/vendor/ink/build/hooks/use-paste.js +62 -0
  342. package/vendor/ink/build/hooks/use-paste.js.map +1 -0
  343. package/vendor/ink/build/hooks/use-stderr.d.ts +5 -0
  344. package/vendor/ink/build/hooks/use-stderr.js +8 -0
  345. package/vendor/ink/build/hooks/use-stderr.js.map +1 -0
  346. package/vendor/ink/build/hooks/use-stdin.d.ts +7 -0
  347. package/vendor/ink/build/hooks/use-stdin.js +9 -0
  348. package/vendor/ink/build/hooks/use-stdin.js.map +1 -0
  349. package/vendor/ink/build/hooks/use-stdout.d.ts +5 -0
  350. package/vendor/ink/build/hooks/use-stdout.js +8 -0
  351. package/vendor/ink/build/hooks/use-stdout.js.map +1 -0
  352. package/vendor/ink/build/hooks/use-window-size.d.ts +18 -0
  353. package/vendor/ink/build/hooks/use-window-size.js +22 -0
  354. package/vendor/ink/build/hooks/use-window-size.js.map +1 -0
  355. package/vendor/ink/build/index.d.ts +42 -0
  356. package/vendor/ink/build/index.js +24 -0
  357. package/vendor/ink/build/index.js.map +1 -0
  358. package/vendor/ink/build/ink.d.ts +146 -0
  359. package/vendor/ink/build/ink.js +1022 -0
  360. package/vendor/ink/build/ink.js.map +1 -0
  361. package/vendor/ink/build/input-parser.d.ts +10 -0
  362. package/vendor/ink/build/input-parser.js +194 -0
  363. package/vendor/ink/build/input-parser.js.map +1 -0
  364. package/vendor/ink/build/instances.d.ts +3 -0
  365. package/vendor/ink/build/instances.js +8 -0
  366. package/vendor/ink/build/instances.js.map +1 -0
  367. package/vendor/ink/build/kitty-keyboard.d.ts +23 -0
  368. package/vendor/ink/build/kitty-keyboard.js +32 -0
  369. package/vendor/ink/build/kitty-keyboard.js.map +1 -0
  370. package/vendor/ink/build/log-update.d.ts +20 -0
  371. package/vendor/ink/build/log-update.js +261 -0
  372. package/vendor/ink/build/log-update.js.map +1 -0
  373. package/vendor/ink/build/measure-element.d.ts +20 -0
  374. package/vendor/ink/build/measure-element.js +13 -0
  375. package/vendor/ink/build/measure-element.js.map +1 -0
  376. package/vendor/ink/build/measure-text.d.ts +6 -0
  377. package/vendor/ink/build/measure-text.js +21 -0
  378. package/vendor/ink/build/measure-text.js.map +1 -0
  379. package/vendor/ink/build/output.d.ts +35 -0
  380. package/vendor/ink/build/output.js +328 -0
  381. package/vendor/ink/build/output.js.map +1 -0
  382. package/vendor/ink/build/parse-keypress.d.ts +20 -0
  383. package/vendor/ink/build/parse-keypress.js +495 -0
  384. package/vendor/ink/build/parse-keypress.js.map +1 -0
  385. package/vendor/ink/build/reconciler.d.ts +4 -0
  386. package/vendor/ink/build/reconciler.js +306 -0
  387. package/vendor/ink/build/reconciler.js.map +1 -0
  388. package/vendor/ink/build/render-background.d.ts +4 -0
  389. package/vendor/ink/build/render-background.js +25 -0
  390. package/vendor/ink/build/render-background.js.map +1 -0
  391. package/vendor/ink/build/render-border.d.ts +4 -0
  392. package/vendor/ink/build/render-border.js +84 -0
  393. package/vendor/ink/build/render-border.js.map +1 -0
  394. package/vendor/ink/build/render-node-to-output.d.ts +14 -0
  395. package/vendor/ink/build/render-node-to-output.js +162 -0
  396. package/vendor/ink/build/render-node-to-output.js.map +1 -0
  397. package/vendor/ink/build/render-to-string.d.ts +38 -0
  398. package/vendor/ink/build/render-to-string.js +116 -0
  399. package/vendor/ink/build/render-to-string.js.map +1 -0
  400. package/vendor/ink/build/render.d.ts +176 -0
  401. package/vendor/ink/build/render.js +71 -0
  402. package/vendor/ink/build/render.js.map +1 -0
  403. package/vendor/ink/build/renderer.d.ts +8 -0
  404. package/vendor/ink/build/renderer.js +64 -0
  405. package/vendor/ink/build/renderer.js.map +1 -0
  406. package/vendor/ink/build/sanitize-ansi.d.ts +2 -0
  407. package/vendor/ink/build/sanitize-ansi.js +27 -0
  408. package/vendor/ink/build/sanitize-ansi.js.map +1 -0
  409. package/vendor/ink/build/squash-text-nodes.d.ts +3 -0
  410. package/vendor/ink/build/squash-text-nodes.js +36 -0
  411. package/vendor/ink/build/squash-text-nodes.js.map +1 -0
  412. package/vendor/ink/build/styles.d.ts +302 -0
  413. package/vendor/ink/build/styles.js +303 -0
  414. package/vendor/ink/build/styles.js.map +1 -0
  415. package/vendor/ink/build/utils.d.ts +9 -0
  416. package/vendor/ink/build/utils.js +19 -0
  417. package/vendor/ink/build/utils.js.map +1 -0
  418. package/vendor/ink/build/wrap-text.d.ts +3 -0
  419. package/vendor/ink/build/wrap-text.js +38 -0
  420. package/vendor/ink/build/wrap-text.js.map +1 -0
  421. package/vendor/ink/build/write-synchronized.d.ts +4 -0
  422. package/vendor/ink/build/write-synchronized.js +9 -0
  423. package/vendor/ink/build/write-synchronized.js.map +1 -0
  424. package/vendor/ink/license +10 -0
  425. package/vendor/ink/package.json +137 -0
  426. package/.claude-plugin/marketplace.json +0 -34
  427. package/.claude-plugin/plugin.json +0 -20
  428. package/.gitattributes +0 -34
  429. package/.mcp.json +0 -14
  430. package/ARCHITECTURE.md +0 -77
  431. package/CHANGELOG.md +0 -30
  432. package/CONTRIBUTING.md +0 -45
  433. package/DATA-FLOW.md +0 -79
  434. package/LICENSE +0 -21
  435. package/SECURITY.md +0 -138
  436. package/UNINSTALL.md +0 -115
  437. package/agents/maintenance.md +0 -5
  438. package/agents/memory-classification.md +0 -30
  439. package/agents/scheduler-task.md +0 -18
  440. package/agents/webhook-handler.md +0 -27
  441. package/agents/worker.md +0 -24
  442. package/bin/bridge +0 -133
  443. package/bin/statusline-launcher.mjs +0 -82
  444. package/bin/statusline-lib.mjs +0 -581
  445. package/bin/statusline-route.mjs +0 -273
  446. package/bin/statusline.mjs +0 -638
  447. package/bun.lock +0 -927
  448. package/commands/config.md +0 -16
  449. package/commands/doctor.md +0 -13
  450. package/commands/model.md +0 -61
  451. package/commands/setup.md +0 -17
  452. package/defaults/hidden-roles.json +0 -68
  453. package/defaults/memory-chunk-prompt.md +0 -63
  454. package/defaults/mixdog-config.template.json +0 -27
  455. package/defaults/user-workflow.json +0 -8
  456. package/defaults/user-workflow.md +0 -17
  457. package/hooks/hooks.json +0 -73
  458. package/hooks/lib/active-instance.cjs +0 -77
  459. package/hooks/lib/permission-evaluator.cjs +0 -411
  460. package/hooks/lib/permission-route.cjs +0 -63
  461. package/hooks/lib/settings-loader.cjs +0 -117
  462. package/hooks/post-tool-use.cjs +0 -84
  463. package/hooks/pre-mcp-sandbox.cjs +0 -158
  464. package/hooks/pre-tool-subagent.cjs +0 -258
  465. package/hooks/session-start.cjs +0 -1493
  466. package/hooks/shim-launcher.cjs +0 -65
  467. package/hooks/turn-timer.cjs +0 -82
  468. package/lib/claude-md-writer.cjs +0 -386
  469. package/lib/keychain-cjs.cjs +0 -290
  470. package/lib/plugin-paths.cjs +0 -69
  471. package/lib/rules-builder.cjs +0 -241
  472. package/native/README.md +0 -117
  473. package/native/prebuilt/linux-aarch64/mixdog-shim +0 -0
  474. package/native/prebuilt/linux-x86_64/mixdog-shim +0 -0
  475. package/native/prebuilt/macos-aarch64/mixdog-shim +0 -0
  476. package/native/prebuilt/macos-x86_64/mixdog-shim +0 -0
  477. package/native/prebuilt/windows-x86_64/mixdog-shim.exe +0 -0
  478. package/prompts/code-review.txt +0 -16
  479. package/prompts/security-audit.txt +0 -17
  480. package/rules/bridge/00-common.md +0 -39
  481. package/rules/bridge/20-skip-protocol.md +0 -18
  482. package/rules/bridge/30-explorer.md +0 -33
  483. package/rules/bridge/40-cycle1-agent.md +0 -52
  484. package/rules/bridge/41-cycle2-agent.md +0 -62
  485. package/rules/lead/00-tool-lead.md +0 -61
  486. package/rules/lead/01-general.md +0 -26
  487. package/rules/lead/02-channels.md +0 -49
  488. package/rules/lead/03-team.md +0 -27
  489. package/rules/lead/04-workflow.md +0 -20
  490. package/rules/shared/00-language.md +0 -14
  491. package/rules/shared/01-tool.md +0 -138
  492. package/scripts/bootstrap.mjs +0 -130
  493. package/scripts/bridge-unify-smoke.mjs +0 -308
  494. package/scripts/build-runtime-linux.sh +0 -348
  495. package/scripts/build-runtime-macos.sh +0 -217
  496. package/scripts/build-runtime-windows.ps1 +0 -242
  497. package/scripts/builtin-utils-smoke.mjs +0 -398
  498. package/scripts/bump.mjs +0 -80
  499. package/scripts/check-json.mjs +0 -45
  500. package/scripts/check-syntax-changed.mjs +0 -102
  501. package/scripts/check-syntax.mjs +0 -58
  502. package/scripts/code-graph-batch.test.mjs +0 -33
  503. package/scripts/config-preserve-smoke.mjs +0 -180
  504. package/scripts/doctor.mjs +0 -489
  505. package/scripts/edit-normalize-fuzz.mjs +0 -130
  506. package/scripts/edit-normalize-smoke.mjs +0 -401
  507. package/scripts/edit-operation-smoke.mjs +0 -369
  508. package/scripts/edit2-smoke.mjs +0 -63
  509. package/scripts/ensure-deps.mjs +0 -259
  510. package/scripts/fuzzy-e2e.mjs +0 -28
  511. package/scripts/fuzzy-smoke.mjs +0 -26
  512. package/scripts/gateway-model.mjs +0 -596
  513. package/scripts/generate-runtime-manifest.mjs +0 -166
  514. package/scripts/guard-smoke.mjs +0 -66
  515. package/scripts/hidden-role-schema-smoke.mjs +0 -162
  516. package/scripts/hook-routing-smoke.mjs +0 -29
  517. package/scripts/inject-input.ps1 +0 -204
  518. package/scripts/io-complex-smoke.mjs +0 -667
  519. package/scripts/io-explore-bench.mjs +0 -424
  520. package/scripts/io-guardrails-smoke.mjs +0 -205
  521. package/scripts/io-mini-bench-baseline.json +0 -11
  522. package/scripts/io-mini-bench.mjs +0 -216
  523. package/scripts/io-route-harness.mjs +0 -933
  524. package/scripts/io-telemetry-report.mjs +0 -691
  525. package/scripts/lib/gateway-inventory.mjs +0 -178
  526. package/scripts/lib/gateway-settings.mjs +0 -78
  527. package/scripts/mutation-bench.mjs +0 -564
  528. package/scripts/mutation-io-smoke.mjs +0 -1097
  529. package/scripts/native-patch-bridge-smoke.mjs +0 -288
  530. package/scripts/native-patch-smoke.mjs +0 -304
  531. package/scripts/openai-oauth-catalog-smoke.mjs +0 -53
  532. package/scripts/patch-interior-context-smoke.mjs +0 -49
  533. package/scripts/patch-newline-utf8-smoke.mjs +0 -157
  534. package/scripts/perf-hook-smoke.mjs +0 -71
  535. package/scripts/permission-eval-smoke.mjs +0 -443
  536. package/scripts/prep-patch.mjs +0 -53
  537. package/scripts/prep-shim.mjs +0 -96
  538. package/scripts/provider-cache-smoke.mjs +0 -687
  539. package/scripts/report-runtime-health.mjs +0 -132
  540. package/scripts/resolve-bun.mjs +0 -60
  541. package/scripts/run-mcp.mjs +0 -1473
  542. package/scripts/salvage-v4a-shatter.test.mjs +0 -58
  543. package/scripts/scoped-cache-io-smoke.mjs +0 -103
  544. package/scripts/shell-policy-round3-smoke.mjs +0 -46
  545. package/scripts/smoke-runtime-negative.ps1 +0 -100
  546. package/scripts/smoke-runtime-negative.sh +0 -95
  547. package/scripts/stall-policy-smoke.mjs +0 -50
  548. package/scripts/start-memory-worker.mjs +0 -23
  549. package/scripts/statusline-launcher-smoke.mjs +0 -235
  550. package/scripts/stress-atomic-write.mjs +0 -1028
  551. package/scripts/test-fault-inject.mjs +0 -164
  552. package/scripts/test-large-file.mjs +0 -174
  553. package/scripts/tool-edge-smoke.mjs +0 -209
  554. package/scripts/uninstall.mjs +0 -238
  555. package/scripts/webhook-selfheal-smoke.mjs +0 -27
  556. package/scripts/write-overwrite-guard-smoke.mjs +0 -56
  557. package/server-main.mjs +0 -3350
  558. package/server.mjs +0 -468
  559. package/setup/config-merge.mjs +0 -246
  560. package/setup/install.mjs +0 -574
  561. package/setup/launch-core.mjs +0 -617
  562. package/setup/launch.mjs +0 -101
  563. package/setup/locate-claude.mjs +0 -56
  564. package/setup/mixdog-cli.mjs +0 -122
  565. package/setup/setup-server.mjs +0 -3305
  566. package/setup/setup.html +0 -3740
  567. package/setup/tui.mjs +0 -325
  568. package/skills/retro-skill-proposer/SKILL.md +0 -92
  569. package/skills/schedule-add/SKILL.md +0 -77
  570. package/skills/setup/SKILL.md +0 -356
  571. package/skills/webhook-add/SKILL.md +0 -81
  572. package/src/agent/bridge-stall-watchdog.mjs +0 -337
  573. package/src/agent/index.mjs +0 -2229
  574. package/src/agent/orchestrator/ai-wrapped-dispatch.mjs +0 -1010
  575. package/src/agent/orchestrator/bridge-retry.mjs +0 -220
  576. package/src/agent/orchestrator/bridge-trace.mjs +0 -601
  577. package/src/agent/orchestrator/cache-mtime.mjs +0 -58
  578. package/src/agent/orchestrator/config.mjs +0 -405
  579. package/src/agent/orchestrator/context/collect.mjs +0 -651
  580. package/src/agent/orchestrator/dispatch-persist.mjs +0 -549
  581. package/src/agent/orchestrator/drain-registry.mjs +0 -50
  582. package/src/agent/orchestrator/explore-validator.mjs +0 -8
  583. package/src/agent/orchestrator/internal-roles.mjs +0 -118
  584. package/src/agent/orchestrator/internal-tools.mjs +0 -88
  585. package/src/agent/orchestrator/jobs.mjs +0 -116
  586. package/src/agent/orchestrator/mcp/client.mjs +0 -364
  587. package/src/agent/orchestrator/providers/anthropic-oauth.mjs +0 -1884
  588. package/src/agent/orchestrator/providers/anthropic.mjs +0 -598
  589. package/src/agent/orchestrator/providers/gemini.mjs +0 -1530
  590. package/src/agent/orchestrator/providers/grok-oauth.mjs +0 -779
  591. package/src/agent/orchestrator/providers/model-catalog.mjs +0 -374
  592. package/src/agent/orchestrator/providers/openai-compat-stream.mjs +0 -366
  593. package/src/agent/orchestrator/providers/openai-compat.mjs +0 -1511
  594. package/src/agent/orchestrator/providers/openai-oauth-ws.mjs +0 -1891
  595. package/src/agent/orchestrator/providers/openai-oauth.mjs +0 -1456
  596. package/src/agent/orchestrator/providers/openai-ws.mjs +0 -127
  597. package/src/agent/orchestrator/providers/registry.mjs +0 -192
  598. package/src/agent/orchestrator/providers/retry-classifier.mjs +0 -325
  599. package/src/agent/orchestrator/session/cache/post-edit-marks.mjs +0 -42
  600. package/src/agent/orchestrator/session/cache/prefetch-cache.mjs +0 -142
  601. package/src/agent/orchestrator/session/cache/read-cache.mjs +0 -319
  602. package/src/agent/orchestrator/session/cache/scoped-cache.mjs +0 -361
  603. package/src/agent/orchestrator/session/loop.mjs +0 -1619
  604. package/src/agent/orchestrator/session/manager.mjs +0 -1991
  605. package/src/agent/orchestrator/session/result-classification.mjs +0 -65
  606. package/src/agent/orchestrator/session/store.mjs +0 -632
  607. package/src/agent/orchestrator/session/stream-watchdog.mjs +0 -130
  608. package/src/agent/orchestrator/session/tool-result-offload.mjs +0 -166
  609. package/src/agent/orchestrator/session/trim.mjs +0 -491
  610. package/src/agent/orchestrator/smart-bridge/CACHE-SHARD.md +0 -115
  611. package/src/agent/orchestrator/smart-bridge/bridge-llm.mjs +0 -331
  612. package/src/agent/orchestrator/smart-bridge/cache-obs.mjs +0 -150
  613. package/src/agent/orchestrator/smart-bridge/cache-strategy.mjs +0 -228
  614. package/src/agent/orchestrator/smart-bridge/index.mjs +0 -215
  615. package/src/agent/orchestrator/smart-bridge/profiles.mjs +0 -37
  616. package/src/agent/orchestrator/smart-bridge/registry.mjs +0 -348
  617. package/src/agent/orchestrator/stall-policy.mjs +0 -201
  618. package/src/agent/orchestrator/tool-loop-guard.mjs +0 -75
  619. package/src/agent/orchestrator/tools/bash-session.mjs +0 -722
  620. package/src/agent/orchestrator/tools/builtin/advisory-lock.mjs +0 -171
  621. package/src/agent/orchestrator/tools/builtin/arg-guard.mjs +0 -511
  622. package/src/agent/orchestrator/tools/builtin/bash-tool.mjs +0 -480
  623. package/src/agent/orchestrator/tools/builtin/builtin-tools.mjs +0 -256
  624. package/src/agent/orchestrator/tools/builtin/edit-base-guard.mjs +0 -58
  625. package/src/agent/orchestrator/tools/builtin/edit-byte-plan.mjs +0 -240
  626. package/src/agent/orchestrator/tools/builtin/edit-byte-utils.mjs +0 -113
  627. package/src/agent/orchestrator/tools/builtin/edit-commit.mjs +0 -74
  628. package/src/agent/orchestrator/tools/builtin/edit-context-utils.mjs +0 -242
  629. package/src/agent/orchestrator/tools/builtin/edit-diagnostics.mjs +0 -211
  630. package/src/agent/orchestrator/tools/builtin/edit-engine.mjs +0 -1364
  631. package/src/agent/orchestrator/tools/builtin/edit-failure-context.mjs +0 -126
  632. package/src/agent/orchestrator/tools/builtin/edit-hint.mjs +0 -141
  633. package/src/agent/orchestrator/tools/builtin/edit-match-utils.mjs +0 -194
  634. package/src/agent/orchestrator/tools/builtin/edit-partial-write.mjs +0 -60
  635. package/src/agent/orchestrator/tools/builtin/edit-stale-refresh.mjs +0 -168
  636. package/src/agent/orchestrator/tools/builtin/edit-tool.mjs +0 -173
  637. package/src/agent/orchestrator/tools/builtin/edit-utf8-guard.mjs +0 -48
  638. package/src/agent/orchestrator/tools/builtin/fuzzy-match.mjs +0 -99
  639. package/src/agent/orchestrator/tools/builtin/glob-walk.mjs +0 -193
  640. package/src/agent/orchestrator/tools/builtin/list-tool.mjs +0 -597
  641. package/src/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  642. package/src/agent/orchestrator/tools/builtin/notebook-edit-tool.mjs +0 -300
  643. package/src/agent/orchestrator/tools/builtin/path-diagnostics.mjs +0 -152
  644. package/src/agent/orchestrator/tools/builtin/read-formatting.mjs +0 -118
  645. package/src/agent/orchestrator/tools/builtin/read-open.mjs +0 -190
  646. package/src/agent/orchestrator/tools/builtin/read-single-tool.mjs +0 -728
  647. package/src/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +0 -173
  648. package/src/agent/orchestrator/tools/builtin/read-streaming.mjs +0 -602
  649. package/src/agent/orchestrator/tools/builtin/rename-tool.mjs +0 -196
  650. package/src/agent/orchestrator/tools/builtin/rg-runner.mjs +0 -422
  651. package/src/agent/orchestrator/tools/builtin/search-builders.mjs +0 -158
  652. package/src/agent/orchestrator/tools/builtin/search-tool.mjs +0 -869
  653. package/src/agent/orchestrator/tools/builtin/shell-jobs.mjs +0 -962
  654. package/src/agent/orchestrator/tools/builtin/shell-runtime.mjs +0 -223
  655. package/src/agent/orchestrator/tools/builtin/snapshot-store.mjs +0 -206
  656. package/src/agent/orchestrator/tools/builtin/write-tool.mjs +0 -401
  657. package/src/agent/orchestrator/tools/builtin.mjs +0 -503
  658. package/src/agent/orchestrator/tools/code-graph-tool-defs.mjs +0 -24
  659. package/src/agent/orchestrator/tools/code-graph.mjs +0 -4095
  660. package/src/agent/orchestrator/tools/cwd-tool.mjs +0 -298
  661. package/src/agent/orchestrator/tools/destructive-warning.mjs +0 -323
  662. package/src/agent/orchestrator/tools/edit-normalize.mjs +0 -603
  663. package/src/agent/orchestrator/tools/graph-binary-fetcher.mjs +0 -154
  664. package/src/agent/orchestrator/tools/graph-manifest.json +0 -26
  665. package/src/agent/orchestrator/tools/host-input.mjs +0 -204
  666. package/src/agent/orchestrator/tools/patch-binary-fetcher.mjs +0 -143
  667. package/src/agent/orchestrator/tools/patch-manifest.json +0 -26
  668. package/src/agent/orchestrator/tools/patch-tool-defs.mjs +0 -20
  669. package/src/agent/orchestrator/tools/patch.mjs +0 -2754
  670. package/src/agent/orchestrator/tools/progress-message.mjs +0 -118
  671. package/src/agent/orchestrator/tools/shell-command.mjs +0 -865
  672. package/src/agent/orchestrator/tools/shell-policy-imports.mjs +0 -7
  673. package/src/agent/orchestrator/tools/shell-snapshot.mjs +0 -313
  674. package/src/agent/orchestrator/workflow-store.mjs +0 -93
  675. package/src/agent/tool-defs.mjs +0 -110
  676. package/src/channels/backends/discord.mjs +0 -784
  677. package/src/channels/data/voice-runtime-manifest.json +0 -138
  678. package/src/channels/index.mjs +0 -3268
  679. package/src/channels/lib/config.mjs +0 -292
  680. package/src/channels/lib/drop-trace.mjs +0 -71
  681. package/src/channels/lib/event-pipeline.mjs +0 -81
  682. package/src/channels/lib/holidays.mjs +0 -138
  683. package/src/channels/lib/hook-pipe-server.mjs +0 -822
  684. package/src/channels/lib/output-forwarder.mjs +0 -765
  685. package/src/channels/lib/runtime-paths.mjs +0 -552
  686. package/src/channels/lib/scheduler.mjs +0 -723
  687. package/src/channels/lib/session-discovery.mjs +0 -103
  688. package/src/channels/lib/state-file.mjs +0 -68
  689. package/src/channels/lib/status-snapshot.mjs +0 -219
  690. package/src/channels/lib/tool-format.mjs +0 -140
  691. package/src/channels/lib/transcript-discovery.mjs +0 -195
  692. package/src/channels/lib/voice-runtime-fetcher.mjs +0 -734
  693. package/src/channels/lib/webhook.mjs +0 -1318
  694. package/src/channels/tool-defs.mjs +0 -170
  695. package/src/daemon/host.mjs +0 -118
  696. package/src/daemon/mcp-transport.mjs +0 -47
  697. package/src/daemon/session.mjs +0 -100
  698. package/src/daemon/thin-client.mjs +0 -71
  699. package/src/daemon/transport.mjs +0 -163
  700. package/src/gateway/claude-current.mjs +0 -255
  701. package/src/gateway/oauth-usage.mjs +0 -598
  702. package/src/gateway/route-meta.mjs +0 -629
  703. package/src/gateway/server.mjs +0 -713
  704. package/src/memory/data/runtime-manifest.json +0 -40
  705. package/src/memory/index.mjs +0 -3332
  706. package/src/memory/lib/core-memory-store.mjs +0 -330
  707. package/src/memory/lib/embedding-provider.mjs +0 -269
  708. package/src/memory/lib/embedding-worker.mjs +0 -323
  709. package/src/memory/lib/memory-cycle1.mjs +0 -645
  710. package/src/memory/lib/memory-cycle2.mjs +0 -1284
  711. package/src/memory/lib/memory-cycle3.mjs +0 -540
  712. package/src/memory/lib/memory-embed.mjs +0 -299
  713. package/src/memory/lib/memory-ops-policy.mjs +0 -190
  714. package/src/memory/lib/memory-recall-store.mjs +0 -638
  715. package/src/memory/lib/memory.mjs +0 -412
  716. package/src/memory/lib/pg/adapter.mjs +0 -308
  717. package/src/memory/lib/pg/process.mjs +0 -360
  718. package/src/memory/lib/pg/supervisor.mjs +0 -396
  719. package/src/memory/lib/runtime-fetcher.mjs +0 -458
  720. package/src/memory/lib/trace-store.mjs +0 -728
  721. package/src/memory/tool-defs.mjs +0 -79
  722. package/src/search/index.mjs +0 -1173
  723. package/src/search/lib/backends/anthropic-oauth.mjs +0 -98
  724. package/src/search/lib/backends/exa.mjs +0 -50
  725. package/src/search/lib/backends/firecrawl.mjs +0 -61
  726. package/src/search/lib/backends/gemini-api.mjs +0 -83
  727. package/src/search/lib/backends/grok-oauth.mjs +0 -86
  728. package/src/search/lib/backends/index.mjs +0 -150
  729. package/src/search/lib/backends/openai-api.mjs +0 -144
  730. package/src/search/lib/backends/openai-oauth.mjs +0 -102
  731. package/src/search/lib/backends/openai-web-search.mjs +0 -76
  732. package/src/search/lib/backends/tavily.mjs +0 -55
  733. package/src/search/lib/backends/xai-api.mjs +0 -113
  734. package/src/search/lib/config.mjs +0 -192
  735. package/src/search/lib/provider-usage.mjs +0 -67
  736. package/src/search/lib/providers.mjs +0 -47
  737. package/src/search/lib/search-intent.mjs +0 -109
  738. package/src/search/lib/setup-handler.mjs +0 -261
  739. package/src/search/lib/web-tools.mjs +0 -1219
  740. package/src/search/tool-defs.mjs +0 -83
  741. package/src/setup/defender-exclusion.mjs +0 -183
  742. package/src/shared/atomic-file.mjs +0 -436
  743. package/src/shared/config.mjs +0 -372
  744. package/src/shared/daemon-recycle.mjs +0 -108
  745. package/src/shared/disable-claude-builtins.mjs +0 -91
  746. package/src/shared/err-text.mjs +0 -12
  747. package/src/shared/llm/http-agent.mjs +0 -123
  748. package/src/shared/open-url.mjs +0 -62
  749. package/src/shared/plugin-paths.mjs +0 -58
  750. package/src/shared/schedules-store.mjs +0 -70
  751. package/src/shared/seed.mjs +0 -161
  752. package/src/shared/user-cwd.mjs +0 -225
  753. package/src/shared/user-data-guard.mjs +0 -244
  754. package/src/status/aggregator.mjs +0 -584
  755. package/src/status/server.mjs +0 -413
  756. package/tools.json +0 -1671
  757. /package/{defaults → src/defaults}/cycle3-review-prompt.md +0 -0
  758. /package/{defaults → src/defaults}/memory-promote-prompt.md +0 -0
  759. /package/{hooks → src/hooks}/lib/permission-rules.cjs +0 -0
  760. /package/{lib → src/lib}/config-cjs.cjs +0 -0
  761. /package/{lib → src/lib}/hook-pipe-path.cjs +0 -0
  762. /package/{lib → src/lib}/mixdog-debug.cjs +0 -0
  763. /package/{lib → src/lib}/text-utils.cjs +0 -0
  764. /package/{rules → src/rules}/bridge/42-cycle3-agent.md +0 -0
  765. /package/src/{agent → runtime/agent}/orchestrator/activity-bus.mjs +0 -0
  766. /package/src/{agent → runtime/agent}/orchestrator/providers/anthropic-betas.mjs +0 -0
  767. /package/src/{agent → runtime/agent}/orchestrator/session/abort-lookup.mjs +0 -0
  768. /package/src/{agent → runtime/agent}/orchestrator/session/cache/scoped-cache-outcome.mjs +0 -0
  769. /package/src/{agent → runtime/agent}/orchestrator/session/cache/util.mjs +0 -0
  770. /package/src/{agent → runtime/agent}/orchestrator/session/read-dedup.mjs +0 -0
  771. /package/src/{agent → runtime/agent}/orchestrator/session/save-session-worker.mjs +0 -0
  772. /package/src/{agent → runtime/agent}/orchestrator/smart-bridge/session-builder.mjs +0 -0
  773. /package/src/{agent → runtime/agent}/orchestrator/tools/bash-policy-scan.mjs +0 -0
  774. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/atomic-write.mjs +0 -0
  775. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/binary-file.mjs +0 -0
  776. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/cache-layers.mjs +0 -0
  777. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/cwd-utils.mjs +0 -0
  778. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/device-paths.mjs +0 -0
  779. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -0
  780. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/diff-utils.mjs +0 -0
  781. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/fs-reachability.mjs +0 -0
  782. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/grep-formatting.mjs +0 -0
  783. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/hash-utils.mjs +0 -0
  784. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/list-formatting.mjs +0 -0
  785. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/open-config-tool.mjs +0 -0
  786. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/path-locks.mjs +0 -0
  787. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/path-utils.mjs +0 -0
  788. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-args.mjs +0 -0
  789. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-batch.mjs +0 -0
  790. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-constants.mjs +0 -0
  791. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-image-resize.mjs +0 -0
  792. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-image.mjs +0 -0
  793. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-lines.mjs +0 -0
  794. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-mode-tool.mjs +0 -0
  795. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-range-index.mjs +0 -0
  796. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-ranges.mjs +0 -0
  797. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-special-files.mjs +0 -0
  798. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-tool.mjs +0 -0
  799. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/read-windows.mjs +0 -0
  800. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/shell-analysis.mjs +0 -0
  801. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/shell-output.mjs +0 -0
  802. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/snapshot-helpers.mjs +0 -0
  803. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/snapshot-validation.mjs +0 -0
  804. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/text-stats.mjs +0 -0
  805. /package/src/{agent → runtime/agent}/orchestrator/tools/builtin/windows-roots.mjs +0 -0
  806. /package/src/{agent → runtime/agent}/orchestrator/tools/code-graph-prewarm-worker.mjs +0 -0
  807. /package/src/{agent → runtime/agent}/orchestrator/tools/env-scrub.mjs +0 -0
  808. /package/src/{agent → runtime/agent}/orchestrator/tools/mutation-content-cache.mjs +0 -0
  809. /package/src/{agent → runtime/agent}/orchestrator/tools/mutation-planner.mjs +0 -0
  810. /package/src/{agent → runtime/agent}/orchestrator/tools/next-call-utils.mjs +0 -0
  811. /package/src/{agent → runtime/agent}/orchestrator/tools/result-compression.mjs +0 -0
  812. /package/src/{agent → runtime/agent}/orchestrator/tools/shell-exec-policy.mjs +0 -0
  813. /package/src/{agent → runtime/agent}/orchestrator/tools/shell-policy-danger-target.mjs +0 -0
  814. /package/src/{agent → runtime/agent}/orchestrator/tools/shell-policy.mjs +0 -0
  815. /package/src/{channels → runtime/channels}/lib/cli-worker-host.mjs +0 -0
  816. /package/src/{channels → runtime/channels}/lib/config-lock.mjs +0 -0
  817. /package/src/{channels → runtime/channels}/lib/event-queue.mjs +0 -0
  818. /package/src/{channels → runtime/channels}/lib/executor.mjs +0 -0
  819. /package/src/{channels → runtime/channels}/lib/format.mjs +0 -0
  820. /package/src/{channels → runtime/channels}/lib/interaction-workflows.mjs +0 -0
  821. /package/src/{channels → runtime/channels}/lib/memory-client.mjs +0 -0
  822. /package/src/{channels → runtime/channels}/lib/session-control.mjs +0 -0
  823. /package/src/{channels → runtime/channels}/lib/settings.mjs +0 -0
  824. /package/src/{channels → runtime/channels}/lib/whisper-server.mjs +0 -0
  825. /package/src/{memory → runtime/memory}/lib/agent-ipc.mjs +0 -0
  826. /package/src/{memory → runtime/memory}/lib/bridge-trace-queries.mjs +0 -0
  827. /package/src/{memory → runtime/memory}/lib/llm-worker-host.mjs +0 -0
  828. /package/src/{memory → runtime/memory}/lib/memory-cycle.mjs +0 -0
  829. /package/src/{memory → runtime/memory}/lib/memory-extraction.mjs +0 -0
  830. /package/src/{memory → runtime/memory}/lib/memory-maintenance-store.mjs +0 -0
  831. /package/src/{memory → runtime/memory}/lib/memory-recall-id-patch.mjs +0 -0
  832. /package/src/{memory → runtime/memory}/lib/memory-recall-read-query.mjs +0 -0
  833. /package/src/{memory → runtime/memory}/lib/memory-recall-scope-filter.mjs +0 -0
  834. /package/src/{memory → runtime/memory}/lib/memory-retrievers.mjs +0 -0
  835. /package/src/{memory → runtime/memory}/lib/memory-score.mjs +0 -0
  836. /package/src/{memory → runtime/memory}/lib/memory-text-utils.mjs +0 -0
  837. /package/src/{memory → runtime/memory}/lib/model-profile.mjs +0 -0
  838. /package/src/{memory → runtime/memory}/lib/project-id-resolver.mjs +0 -0
  839. /package/src/{search → runtime/search}/lib/cache.mjs +0 -0
  840. /package/src/{search → runtime/search}/lib/formatter.mjs +0 -0
  841. /package/src/{search → runtime/search}/lib/state.mjs +0 -0
  842. /package/src/{shared → runtime/shared}/abort-controller.mjs +0 -0
  843. /package/src/{shared → runtime/shared}/llm/cost.mjs +0 -0
  844. /package/src/{shared → runtime/shared}/llm/index.mjs +0 -0
  845. /package/src/{shared → runtime/shared}/llm/pid-cleanup.mjs +0 -0
  846. /package/src/{shared → runtime/shared}/llm/usage-log.mjs +0 -0
  847. /package/src/{shared → runtime/shared}/wsl.mjs +0 -0
@@ -1,2229 +0,0 @@
1
- import { initProviders, refreshProviderCatalogsOnStartup, refreshCatalogs } from './orchestrator/providers/registry.mjs';
2
- import { runWithCwdOverride, pwd } from '../shared/user-cwd.mjs';
3
- import { askSession, listSessions, closeSession, updateSessionStatus, updateSessionStage, getSessionRuntime, SessionClosedError, setSmartBridge, forEachSessionRuntime, hideSessionFromList, getSession, enqueuePendingMessage } from './orchestrator/session/manager.mjs';
4
- import { publishHeartbeat as publishSessionHeartbeat } from './orchestrator/session/store.mjs';
5
- import { StreamStalledAbortError, startWatchdog as startStreamWatchdog } from './orchestrator/session/stream-watchdog.mjs';
6
- import { startBridgeStallWatchdog } from './bridge-stall-watchdog.mjs';
7
- import { loadConfig, getPluginData, listPresets, getDefaultPreset, resolveRuntimeSpec } from './orchestrator/config.mjs';
8
- import { updateSection } from '../shared/config.mjs';
9
- import { readGatewayRouteInfo } from '../gateway/route-meta.mjs';
10
- import { CLAUDE_CURRENT_MODE } from '../gateway/claude-current.mjs';
11
- // Shared gateway inventory/target helpers — SAME module scripts/gateway-model.mjs
12
- // uses, so the interactive gateway_select_model tool and the CLI build the model
13
- // list and persist the routing target through one code path.
14
- import { buildInventory as buildGatewayInventory, inventoryChoices as gatewayInventoryChoices, parseChoiceId as parseGatewayChoiceId, resolveTargetChoice as resolveGatewayTargetChoice, writeGatewayTarget } from '../../scripts/lib/gateway-inventory.mjs';
15
- import { connectMcpServers, disconnectAll } from './orchestrator/mcp/client.mjs';
16
- import { setInternalToolsProvider, awaitBootReady } from './orchestrator/internal-tools.mjs';
17
- import { listWorkflows, getWorkflow, seedDefaults } from './orchestrator/workflow-store.mjs';
18
- import { prepareBridgeSession } from './orchestrator/smart-bridge/session-builder.mjs';
19
- import { normalizeInputPath } from './orchestrator/tools/builtin.mjs';
20
- import { prewarmCodeGraph, prewarmCodeGraphSymbols } from './orchestrator/tools/code-graph.mjs';
21
- import { runWithDispatchRetry } from './orchestrator/bridge-retry.mjs';
22
- import { ensureDataSeeds } from '../shared/seed.mjs';
23
- import { errText } from '../shared/err-text.mjs';
24
- import { isCurrentDaemonDoomed } from '../shared/daemon-recycle.mjs';
25
- import { writeFileSync, readFileSync, existsSync, watch, watchFile, unwatchFile } from 'fs';
26
- import { readFile } from 'fs/promises';
27
- import { execFileSync } from 'child_process';
28
- import { addPending, removePending, setPendingResult } from './orchestrator/dispatch-persist.mjs';
29
- import { join, resolve, isAbsolute } from 'path';
30
-
31
- // --- user-workflow.json loader ---
32
- // The plugin already persists user role -> preset mapping in
33
- // <plugin-data>/user-workflow.json
34
- // Smart Bridge consumes this directly instead of introducing a duplicate
35
- // config key. fs.watch keeps Smart Bridge in sync when the user edits roles.
36
-
37
- /**
38
- * @typedef {Object} RoleConfig
39
- * @property {string} name - unique role identifier
40
- * @property {string} preset - preset name from agent-config presets
41
- * @property {'read'|'read-write'|'mcp'|'full'} permission - tool permission category
42
- * @property {string|null} desc_path - relative to CLAUDE_PLUGIN_ROOT
43
- */
44
-
45
- const VALID_PERMISSIONS = new Set(['read', 'read-write', 'mcp', 'full']);
46
- const SILENT_BRIDGE_CANCEL_REASONS = new Set([
47
- 'manual',
48
- 'request-abort',
49
- 'request-aborted',
50
- 'retry-replaced',
51
- 'idle-sweep',
52
- ]);
53
-
54
- function shouldEmitBridgeCancellation(reason) {
55
- if (!reason) return true;
56
- return !SILENT_BRIDGE_CANCEL_REASONS.has(String(reason));
57
- }
58
-
59
- // Sanitize cwd before passing it to a child-process spawn. On Windows,
60
- // node:child_process spawn() with a non-ASCII cwd often fails with ENOENT
61
- // because of code-page / UTF-8 mismatches in the inherited environment
62
- // (worker invocations with non-ASCII OneDrive paths such as a localized
63
- // Desktop directory saw zero tool activity
64
- // before this guard). Falls back to process.cwd() when the cwd contains
65
- // non-ASCII; the caller (case 'bridge') is expected to surface the
66
- // originally-requested path inside the prompt body so absolute paths in
67
- // tool calls still hit the right tree.
68
- function _safeCwdForSpawn(rawCwd) {
69
- if (!rawCwd) return rawCwd;
70
- if (process.platform === 'win32' && /[^\x20-\x7e]/.test(String(rawCwd))) {
71
- try { return process.cwd(); } catch { return rawCwd; }
72
- }
73
- return rawCwd;
74
- }
75
-
76
- function applyRoleDefaults(raw) {
77
- const permission = raw.permission ?? 'full';
78
- const desc_path = typeof raw.desc_path === 'string' ? raw.desc_path : null;
79
-
80
- return {
81
- name: raw.name,
82
- preset: raw.preset,
83
- permission,
84
- desc_path,
85
- };
86
- }
87
-
88
- function validateRoleConfig(role) {
89
- if (!role.name || typeof role.name !== 'string')
90
- throw new Error(`[user-workflow] role entry missing "name"`);
91
- if (!role.preset || typeof role.preset !== 'string')
92
- throw new Error(`[user-workflow] role "${role.name}" missing "preset"`);
93
- if (!VALID_PERMISSIONS.has(role.permission))
94
- throw new Error(`[user-workflow] role "${role.name}": invalid permission "${role.permission}" (expected: ${[...VALID_PERMISSIONS].join(", ")})`);
95
- }
96
-
97
- /** @type {Map<string, RoleConfig>} */
98
- let _roleConfigCache = new Map();
99
-
100
- function loadResolvedRoles() {
101
- const path = join(getPluginData(), 'user-workflow.json');
102
- const map = new Map();
103
- if (!existsSync(path)) return map;
104
- try {
105
- const data = JSON.parse(readFileSync(path, 'utf8'));
106
- if (Array.isArray(data?.roles)) {
107
- for (const raw of data.roles) {
108
- if (!raw?.name || !raw?.preset) continue;
109
- const resolved = applyRoleDefaults(raw);
110
- validateRoleConfig(resolved);
111
- map.set(resolved.name, resolved);
112
- }
113
- }
114
- } catch (e) {
115
- process.stderr.write(`[user-workflow] load error: ${e.message}\n`);
116
- }
117
- return map;
118
- }
119
-
120
- function loadUserWorkflowRoles() {
121
- _roleConfigCache = loadResolvedRoles();
122
- const out = {};
123
- for (const [name, cfg] of _roleConfigCache) out[name] = cfg.preset;
124
- return out;
125
- }
126
-
127
- /**
128
- * Get the fully-resolved RoleConfig for a given role name.
129
- * @param {string} roleName
130
- * @returns {RoleConfig|null}
131
- */
132
- export function getRoleConfig(roleName) {
133
- return _roleConfigCache.get(roleName) ?? null;
134
- }
135
-
136
- let _userWorkflowWatcher = null;
137
- function watchUserWorkflow(onChange) {
138
- if (_userWorkflowWatcher) return;
139
- const dir = getPluginData();
140
- if (process.platform === 'linux') {
141
- // linux/WSL: fs.watch on a directory is unreliable/unsupported.
142
- // Use fs.watchFile polling on the specific file instead.
143
- try {
144
- const fp = join(dir, 'user-workflow.json');
145
- watchFile(fp, { persistent: false, interval: 2000 }, () => {
146
- try { onChange(loadUserWorkflowRoles()); } catch {}
147
- });
148
- _userWorkflowWatcher = { close: () => unwatchFile(fp) };
149
- } catch (e) {
150
- process.stderr.write(`[user-workflow-watch] watchFile failed on ${dir}: ${e?.message}\n`);
151
- }
152
- } else {
153
- // win32: reliable; darwin: flat watch on single dir is fine.
154
- try {
155
- _userWorkflowWatcher = watch(dir, { persistent: false }, (_event, filename) => {
156
- if (filename === 'user-workflow.json') {
157
- try { onChange(loadUserWorkflowRoles()); } catch {}
158
- }
159
- });
160
- } catch (e) {
161
- // fs.watch can fail on some platforms — log and continue; watcher is non-critical.
162
- process.stderr.write(`[user-workflow-watch] fs.watch failed on ${dir}: ${e?.message}\n`);
163
- }
164
- }
165
- }
166
-
167
- let _agentConfigWatcher = null;
168
- let _agentConfigReloadTimer = null;
169
- let _agentConfigReloadRunning = false;
170
- let _agentConfigReloadQueued = false;
171
- let _lastExternalServersKey = '';
172
-
173
- function stableJson(value) {
174
- if (!value || typeof value !== 'object') return JSON.stringify(value);
175
- if (Array.isArray(value)) return `[${value.map(stableJson).join(',')}]`;
176
- const entries = Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJson(value[key])}`);
177
- return `{${entries.join(',')}}`;
178
- }
179
-
180
- function externalMcpServersFromConfig(config) {
181
- const rawServers = (config?.mcpServers && typeof config.mcpServers === 'object') ? config.mcpServers : {};
182
- const externalServers = {};
183
- for (const [name, cfg] of Object.entries(rawServers)) {
184
- if (name === 'mixdog' || name === 'trib-plugin') continue;
185
- externalServers[name] = cfg;
186
- }
187
- return externalServers;
188
- }
189
-
190
- function scheduleAgentConfigReload(reason = 'change') {
191
- clearTimeout(_agentConfigReloadTimer);
192
- _agentConfigReloadTimer = setTimeout(() => {
193
- reloadAgentConfig(reason).catch((err) => {
194
- process.stderr.write(`[agent-config-watch] reload failed: ${err?.message || String(err)}\n`);
195
- });
196
- }, 250);
197
- _agentConfigReloadTimer.unref?.();
198
- }
199
-
200
- export async function reloadAgentConfig(reason = 'change') {
201
- if (_agentConfigReloadRunning) {
202
- _agentConfigReloadQueued = true;
203
- return;
204
- }
205
- _agentConfigReloadRunning = true;
206
- try {
207
- const config = loadConfig();
208
- await initProviders(config.providers);
209
-
210
- try {
211
- const { getSmartBridge } = await import('./orchestrator/smart-bridge/index.mjs');
212
- getSmartBridge().updatePresets(config.presets || []);
213
- } catch {}
214
-
215
- const externalServers = externalMcpServersFromConfig(config);
216
- const externalKey = stableJson(externalServers);
217
- if (_lastExternalServersKey !== externalKey) {
218
- try {
219
- await disconnectAll();
220
- if (Object.keys(externalServers).length > 0) await connectMcpServers(externalServers);
221
- process.stderr.write(`[agent-config-watch] external MCP servers refreshed (${Object.keys(externalServers).length})\n`);
222
- } catch (err) {
223
- process.stderr.write(`[agent-config-watch] external MCP refresh failed: ${err?.message || String(err)}\n`);
224
- } finally {
225
- _lastExternalServersKey = externalKey;
226
- }
227
- }
228
-
229
- refreshCatalogs();
230
- process.stderr.write(`[agent-config-watch] runtime refreshed (${reason})\n`);
231
- } finally {
232
- _agentConfigReloadRunning = false;
233
- if (_agentConfigReloadQueued) {
234
- _agentConfigReloadQueued = false;
235
- scheduleAgentConfigReload('queued');
236
- }
237
- }
238
- }
239
-
240
- function watchAgentConfig() {
241
- if (_agentConfigWatcher) return;
242
- const dir = getPluginData();
243
- const file = join(dir, 'mixdog-config.json');
244
- if (process.platform === 'linux') {
245
- try {
246
- watchFile(file, { persistent: false, interval: 2000 }, () => scheduleAgentConfigReload('mixdog-config.json'));
247
- _agentConfigWatcher = { close: () => unwatchFile(file) };
248
- } catch (e) {
249
- process.stderr.write(`[agent-config-watch] watchFile failed on ${file}: ${e?.message}\n`);
250
- }
251
- } else {
252
- try {
253
- _agentConfigWatcher = watch(dir, { persistent: false }, (_event, filename) => {
254
- if (filename === 'mixdog-config.json') scheduleAgentConfigReload('mixdog-config.json');
255
- });
256
- } catch (e) {
257
- process.stderr.write(`[agent-config-watch] fs.watch failed on ${dir}: ${e?.message}\n`);
258
- }
259
- }
260
- }
261
-
262
- function buildInstructions() {
263
- const lines = [];
264
-
265
- try {
266
- const workflows = listWorkflows();
267
- lines.push('');
268
- if (workflows.length > 0) {
269
- lines.push('Available workflows:');
270
- for (const w of workflows) {
271
- lines.push(`- ${w.name}: ${w.description}`);
272
- }
273
- } else {
274
- lines.push('No custom workflows configured.');
275
- }
276
- } catch {
277
- lines.push('');
278
- lines.push('No custom workflows configured.');
279
- }
280
-
281
- return lines.join('\n');
282
- }
283
-
284
- // Seed default workflows into user data dir if none exist yet.
285
- seedDefaults();
286
-
287
- // jobId → current activeSession.id registry. Populated by detached bridge
288
- // workers so bridge type=close can reach the *latest* session after a retry
289
- // replaced the original. Also maintains a reverse map (sessionId → jobId)
290
- // so bridge type=close can resolve any session id — original or replacement —
291
- // back to the job and close its current active session. Both maps are
292
- // cleared when the job finishes (finally block).
293
- // Map<jobId, sessionId> + Map<sessionId, jobId>
294
- const _jobSessionRegistry = new Map();
295
- const _sessionJobRegistry = new Map(); // reverse: sessionId → jobId
296
-
297
- // tag → sessionId registry for the unified `bridge` tool. A `tag` is a stable,
298
- // human-named handle for a detached worker session so the Lead can `bridge
299
- // type=send|close tag=<tag>` without tracking the raw sess_ id. Unlike the
300
- // job registries (which are torn down in the worker finally block to free
301
- // memory), tag entries persist until the session is explicitly closed OR a
302
- // completed/errored session is reaped from listSessions — this is what makes
303
- // detached workers RESUMABLE: askSession replays session.messages from the
304
- // {id}.json transcript on disk, so a `send` to a still-live (idle) session
305
- // continues the conversation. On retry the worker swaps activeSession.id; the
306
- // spawn handler updates this map in lockstep with _jobSessionRegistry so the
307
- // tag keeps pointing at the live replacement (history of the prior attempt is
308
- // not carried — see retry-swap note in the bridge spawn branch).
309
- // Map<tag, sessionId>
310
- const _tagSessionRegistry = new Map();
311
- // Map<tag, role> — in-memory, EXPLICIT caller tags only (omitted tag → auto
312
- // ${role}${n} is NOT recorded). Set at bind-time after session creation succeeds;
313
- // survives terminal reap (tag→session binding is dropped on reap; role mapping
314
- // stays so a cold `bridge type=send` can respawn without passing role). Cleared
315
- // on `bridge type=close` (live session or cold tag with only a role mapping).
316
- const _tagRoleRegistry = new Map();
317
- // Map<tag, cwd> — the resolved workerCwd captured at spawn-bind time, kept
318
- // alongside the tag→role map so a COLD `bridge type=send` respawn restores the
319
- // original spawn's working directory. Without it a cold respawn falls back to
320
- // the daemon's launch dir (the plugin cache), and the worker's relative-path
321
- // reads resolve to the deployed copy instead of the live working tree.
322
- const _tagCwdRegistry = new Map();
323
- // Per-role auto-tag counter for spawns that omit an explicit tag.
324
- // Map<role, number>
325
- const _roleTagCounters = new Map();
326
-
327
- // Per-session terminal-reap timer handle. A completed/errored bridge worker
328
- // schedules a 1h reap (hide-from-list + close/tombstone + tag reclaim) in its finally block;
329
- // the handle is stored here keyed by sessionId so a later `bridge type=send`
330
- // resume can CANCEL or RESCHEDULE it (see _cancelBridgeReap /
331
- // _scheduleBridgeReap). Without this, a send to a just-completed session could
332
- // be reaped mid-resume — hidden and its tag deleted — leaving the resumed
333
- // session unaddressable by tag.
334
- // Map<sessionId, ReturnType<typeof setTimeout>>
335
- const _sessionReapTimers = new Map();
336
-
337
- // Resolve a tag (or a raw sess_ id) to a live sessionId. Returns null when the
338
- // tag is unknown OR maps to a session that no longer exists / was closed.
339
- function _resolveBridgeTag(tagOrId) {
340
- if (typeof tagOrId !== 'string' || !tagOrId) return null;
341
- // Raw session id passes through (still validated against the store below).
342
- let sessionId = _tagSessionRegistry.get(tagOrId) || (tagOrId.startsWith('sess_') ? tagOrId : null);
343
- if (!sessionId) return null;
344
- let session = null;
345
- try { session = getSession(sessionId); } catch { session = null; }
346
- if (!session || session.closed === true) return null;
347
- return sessionId;
348
- }
349
-
350
- // Allocate a unique tag for a spawn. Explicit tag must not already map to a
351
- // LIVE session (no silent overwrite); a stale tag (closed/missing session) is
352
- // reclaimed. Returns { tag } or { error }.
353
- function _allocateBridgeTag(requestedTag, role) {
354
- if (typeof requestedTag === 'string' && requestedTag.trim()) {
355
- const tag = requestedTag.trim();
356
- if (_resolveBridgeTag(tag)) {
357
- return { error: `tag "${tag}" already maps to a live session — close it first or use a different tag` };
358
- }
359
- return { tag };
360
- }
361
- // Auto-tag: ${role}${n} per-role counter, skipping any live collisions.
362
- let n = (_roleTagCounters.get(role) || 0) + 1;
363
- let tag = `${role}${n}`;
364
- while (_resolveBridgeTag(tag)) {
365
- n += 1;
366
- tag = `${role}${n}`;
367
- }
368
- _roleTagCounters.set(role, n);
369
- return { tag };
370
- }
371
-
372
- // Reverse-lookup the tag bound to a sessionId (for list output). Linear scan;
373
- // the registry is small (one live worker per entry).
374
- function _tagForSessionId(sessionId) {
375
- for (const [tag, sid] of _tagSessionRegistry.entries()) {
376
- if (sid === sessionId) return tag;
377
- }
378
- return null;
379
- }
380
-
381
- // Validate that a raw `sess_...` id maps to a LIVE (on-disk, non-tombstoned)
382
- // session. A reaped/closed session leaves a tombstone (closed===true) or is
383
- // gone entirely; treating such an id as resolved would launch an async
384
- // askSession against a dead session (#5). Used to gate the raw-sess_ fallback
385
- // in both the cold-respawn precheck and the send branch.
386
- function _isLiveSession(sessionId) {
387
- if (!sessionId) return false;
388
- try {
389
- const s = getSession(sessionId);
390
- return !!(s && s.closed !== true);
391
- } catch { return false; }
392
- }
393
-
394
- // Schedule the 1h (3600s) terminal reap for a completed/errored session: hide
395
- // it from listSessions() and reclaim its tag. The 1h window keeps a finished
396
- // bridge worker resumable for same-task reuse — a follow-up `bridge type=send`
397
- // to the same tag continues the SAME session (transcript preserved, no
398
- // from-scratch re-discovery) and lands within the 1h prompt-cache window of
399
- // every provider we use, so the resume is cheap. The timer handle is recorded in
400
- // _sessionReapTimers so a resume (`bridge type=send`) can cancel it. Any
401
- // previously-pending reap for the same session is cancelled first so the
402
- // window is not double-scheduled. `.unref()` keeps the timer from holding the
403
- // process open (mirrors a detached lifecycle hook).
404
- function _scheduleBridgeReap(sessionId) {
405
- if (!sessionId) return;
406
- _cancelBridgeReap(sessionId);
407
- const handle = setTimeout(() => {
408
- _sessionReapTimers.delete(sessionId);
409
- try { hideSessionFromList(sessionId); } catch { /* ignore */ }
410
- // #3: also CLOSE/tombstone the persisted session JSON. hideSessionFromList
411
- // only flips an in-memory listHidden flag (lost on restart) and the
412
- // statusline aggregator reads the on-disk JSON directly — it treats any
413
- // non-closed bridge worker as an idle worker and keeps rendering the
414
- // reaped session until the 24h store sweep. Closing plants closed===true
415
- // (status='closed') so the aggregator's `s.closed === true` filter drops
416
- // it immediately. This is the terminal reap, so the worker is no longer
417
- // resumable — closing is the correct lifecycle end.
418
- try { closeSession(sessionId, 'terminal-reap'); } catch { /* ignore */ }
419
- // Reclaim the tag once the terminal session is reaped from the list — the
420
- // worker is no longer resumable, so free the name.
421
- try {
422
- const _t = _tagForSessionId(sessionId);
423
- if (_t) _tagSessionRegistry.delete(_t);
424
- } catch { /* ignore */ }
425
- }, 3_600_000);
426
- try { handle.unref?.(); } catch { /* ignore */ }
427
- _sessionReapTimers.set(sessionId, handle);
428
- }
429
-
430
- // Cancel a pending terminal reap for a session (called on resume so an
431
- // in-flight/just-resumed worker is not hidden + tag-reclaimed mid-use).
432
- // Returns true if a timer was actually cleared.
433
- function _cancelBridgeReap(sessionId) {
434
- if (!sessionId) return false;
435
- const handle = _sessionReapTimers.get(sessionId);
436
- if (!handle) return false;
437
- try { clearTimeout(handle); } catch { /* ignore */ }
438
- _sessionReapTimers.delete(sessionId);
439
- return true;
440
- }
441
-
442
- // --- Shared session-control helpers (folded from the removed standalone
443
- // list / close session handlers (now bridge type=list / bridge type=close);
444
- // the unified `bridge` handler calls these so the runtime stage/staleness
445
- // derivation stays single-sourced).
446
-
447
- // Build the list-sessions view with runtime stage + staleness. Optional
448
- // role/status filters and a brief flag mirror the legacy tool's shape, plus a
449
- // `tag` field resolved from the tag registry.
450
- function _bridgeListSessions(opts = {}) {
451
- const includeClosed = opts.includeClosed === true;
452
- const sessions = listSessions({ includeClosed });
453
- if (sessions.length === 0) return 'No active sessions.';
454
- const now = Date.now();
455
- const brief = opts.brief !== false;
456
- const roleFilter = typeof opts.role === 'string' && opts.role ? opts.role : null;
457
- const statusFilter = typeof opts.status === 'string' && opts.status ? opts.status : null;
458
- const filtered = sessions;
459
- if (filtered.length === 0) return 'No active sessions.';
460
- const rows = filtered.map((s) => {
461
- const runtime = getSessionRuntime(s.id);
462
- // No runtime entry → session has no in-flight work; stage derives from
463
- // persisted status ('running' is only set by long-running callers; idle
464
- // otherwise). Single derivation, no legacy fallback path.
465
- const persistedStatus = s.status || 'idle';
466
- // Tombstones override status/stage. The persisted `status` field may still
467
- // be 'running' because close aborts rather than touches it; `closed: true`
468
- // is the authoritative signal.
469
- const isClosed = s.closed === true;
470
- const status = isClosed ? 'closed' : persistedStatus;
471
- // Persisted status is the authoritative terminal signal — runtime stage may
472
- // linger as 'streaming'/'tool_running' if the runtime entry missed the
473
- // terminal transition. Trust persistedStatus when terminal; only use
474
- // runtime stage while status is still 'running'.
475
- const persistedTerminal = persistedStatus === 'idle' || persistedStatus === 'error';
476
- const stage = isClosed
477
- ? 'closed'
478
- : (persistedTerminal ? persistedStatus : (runtime?.stage || 'connecting'));
479
- const lastStreamDeltaAt = runtime?.lastStreamDeltaAt
480
- ? new Date(runtime.lastStreamDeltaAt).toISOString()
481
- : null;
482
- const staleSeconds = runtime?.lastStreamDeltaAt
483
- ? Math.floor((now - runtime.lastStreamDeltaAt) / 1000)
484
- : null;
485
- // windowTokens: provider-normalized context footprint at the most-recent
486
- // call — the prompt tokens the model actually saw last turn, comparable
487
- // across providers. Anthropic's input_tokens excludes cache, so
488
- // lastContextTokens folds cache_read back in; openai/grok/gemini already
489
- // include it. Far more honest than the lifetime-cumulative totalInputTokens
490
- // (which mixes per-provider cache conventions). Falls back to
491
- // lastInputTokens for sessions persisted before lastContextTokens existed.
492
- const windowTokens = Number(s.lastContextTokens ?? s.lastInputTokens) || 0;
493
- const base = {
494
- id: s.id,
495
- tag: _tagForSessionId(s.id),
496
- role: s.role || null,
497
- provider: s.provider,
498
- model: s.model,
499
- messages: s.messages.length,
500
- tools: s.tools.length,
501
- windowTokens,
502
- windowCap: Number(s.contextWindow) || null,
503
- cumulativeInputTokens: s.totalInputTokens,
504
- cumulativeOutputTokens: s.totalOutputTokens,
505
- scope: s.scopeKey || null,
506
- status,
507
- lastStatus: persistedStatus,
508
- createdAt: new Date(s.createdAt).toISOString(),
509
- updatedAt: s.updatedAt ? new Date(s.updatedAt).toISOString() : null,
510
- stage,
511
- lastStreamDeltaAt,
512
- staleSeconds,
513
- lastToolCall: runtime?.lastToolCall || null,
514
- };
515
- if (!brief) {
516
- base.toolNames = Array.isArray(s.tools) ? s.tools.map((t) => t?.name).filter(Boolean) : [];
517
- }
518
- return base;
519
- });
520
- const out = rows.filter((r) =>
521
- (!roleFilter || r.role === roleFilter) &&
522
- (!statusFilter || r.status === statusFilter));
523
- if (out.length === 0) return 'No active sessions.';
524
- if (brief) {
525
- // Brief (default): one compact line per session for token efficiency.
526
- // Null/empty fields are omitted; callers needing full detail pass
527
- // brief:false to get the unchanged JSON object array below.
528
- return out.map((r) => {
529
- const label = r.tag || String(r.id).split('_').pop();
530
- const parts = [label];
531
- if (r.role) parts.push(`role=${r.role}`);
532
- if (r.model) parts.push(`model=${r.model}`);
533
- if (r.status) parts.push(`status=${r.status}`);
534
- if (r.stage) parts.push(`stage=${r.stage}`);
535
- if (r.staleSeconds) parts.push(`stale=${r.staleSeconds}s`);
536
- parts.push(`msgs=${r.messages}`);
537
- const windowK = Math.round(r.windowTokens / 1000);
538
- if (windowK) parts.push(`window=${windowK}k`);
539
- if (r.lastToolCall) parts.push(`lastTool=${r.lastToolCall}`);
540
- return parts.join(' ');
541
- }).join('\n');
542
- }
543
- return out;
544
- }
545
-
546
- // Close a session by raw id (fire-and-forget). Resolves any retry-replacement
547
- // session via the job registries and plants a cancellation tombstone. Returns
548
- // the close ack shape. Folded verbatim from the removed standalone close
549
- // handler (now bridge type=close).
550
- function _bridgeCloseSession(sessionId) {
551
- // Fire-and-forget: plant tombstone, abort in-flight controller, defer
552
- // cleanup. We don't wait for the abort to unwind — callers get an immediate
553
- // ack and unknown IDs return the same shape for simplicity.
554
- //
555
- // Retry-session forwarding: if the supplied sessionId was the initial session
556
- // for a detached bridge job since replaced by a retry, the registry maps
557
- // jobId→currentSessionId. We close the current (possibly newer) replacement
558
- // too so the worker is actually stopped. If no retry replacement exists the
559
- // current==requested and both closeSession calls are idempotent no-ops.
560
- closeSession(sessionId, 'manual');
561
- const owningJobId = _sessionJobRegistry.get(sessionId);
562
- if (owningJobId != null) {
563
- // Plant a job-level cancellation tombstone so the dispatch-retry loop stops
564
- // between attempts. closeSession() only kills the currently-active session;
565
- // without this flag, a manual close landing during STALL_RETRY_BACKOFF_MS
566
- // would still let the retry path create a fresh activeSession after backoff.
567
- _jobCancelledTombstones.add(owningJobId);
568
- const currentId = _jobSessionRegistry.get(owningJobId);
569
- if (currentId && currentId !== sessionId) {
570
- try { closeSession(currentId, 'manual'); } catch {}
571
- }
572
- }
573
- return { ok: true, sessionId };
574
- }
575
-
576
- // Job-level cancellation tombstones. bridge type=close plants the owning jobId so
577
- // the dispatch-retry path stops between attempts even when the manual close
578
- // races the stall-retry backoff (the OLD session is closed, but runWithDispatchRetry
579
- // is still asleep on STALL_RETRY_BACKOFF_MS and would otherwise unconditionally
580
- // spin a fresh activeSession on the next attempt). Cleared by the worker's
581
- // finally / .catch block when the job tears down.
582
- const _jobCancelledTombstones = new Set();
583
-
584
- // Seed plugin-owned scaffolding files (memory-config.json, etc.) so
585
- // first-time installs land with the Pool B surface populated and the Config
586
- // UI has real paths to edit.
587
- ensureDataSeeds(getPluginData());
588
-
589
- const INSTRUCTIONS = buildInstructions();
590
-
591
- // --- Prompt store (file-backed, shared with bin/bridge CLI) ---
592
- const _promptStorePath = join(getPluginData(), 'prompt-store.json');
593
- let _promptSeq = 0;
594
-
595
- function _psLoad() {
596
- try {
597
- return JSON.parse(readFileSync(_promptStorePath, 'utf-8'));
598
- } catch {
599
- return {};
600
- }
601
- }
602
-
603
- function _psSave(store) {
604
- writeFileSync(_promptStorePath, JSON.stringify(store) + '\n', 'utf-8');
605
- }
606
-
607
- const _promptStore = {
608
- get(key) {
609
- return _psLoad()[key] ?? null;
610
- },
611
- set(key, val) {
612
- const store = _psLoad();
613
- store[key] = val;
614
- _psSave(store);
615
- },
616
- delete(key) {
617
- const store = _psLoad();
618
- delete store[key];
619
- _psSave(store);
620
- },
621
- };
622
-
623
- // --- Helpers ---
624
-
625
- // Worker reply protocol: every bridge response is wrapped in
626
- // <final-answer>...</final-answer> by instruction (rules/bridge/00-common.md).
627
- // Extract the wrapped content; if the tag is missing return raw text.
628
- function extractFinalAnswer(text) {
629
- if (typeof text !== 'string' || !text) return text;
630
- const m = text.match(/<final-answer>([\s\S]*?)<\/final-answer>/);
631
- if (m) return m[1].trim();
632
- return text;
633
- }
634
-
635
- function ok(data) {
636
- return { content: [{ type: 'text', text: typeof data === 'string' ? data : JSON.stringify(data, null, 2) }] };
637
- }
638
-
639
- function fail(err) {
640
- const msg = errText(err);
641
- return { content: [{ type: 'text', text: `Error: ${msg}` }], isError: true };
642
- }
643
-
644
- // Format token counts in Claude Code style: <1000 as-is, >=1000 as "9.9k".
645
- function fmtTokens(n) {
646
- if (typeof n !== 'number') return String(n ?? '?');
647
- if (n < 1000) return String(n);
648
- return `${(n / 1000).toFixed(1)}k`;
649
- }
650
-
651
- // --- bridge case helpers ---
652
- // These are ordered helpers extracted from the `case 'bridge'` handler.
653
- // They are side-effecting (emit notifications, mutate registries, wire
654
- // abort listeners, call into the session manager) but take all collaborators
655
- // as arguments — so call-site behavior is byte-identical to the inlined form.
656
-
657
- // Input resolution: prompt | file | ref → { prompt } or { error }.
658
- // `bridgeCwd` is the bridge worker's resolved working directory
659
- // (args.cwd > callerCwd). When supplied, a relative `args.file` is
660
- // resolved against it rather than the agent process cwd — previously
661
- // `readFile(args.file)` would silently read from the agent's process
662
- // cwd, producing the wrong contents (or ENOENT) for bridge callers
663
- // whose effective workspace differs from the host process.
664
- async function _resolveBridgePrompt(args, bridgeCwd = null) {
665
- let prompt = args.prompt;
666
- // Invariant: each input slot counts only when it carries
667
- // non-whitespace content. filter(Boolean) historically accepted
668
- // trim-empty strings ("\n", " "), producing a downstream
669
- // "# Task\n<whitespace>" block and silent empty-<final-answer>
670
- // responses from the worker.
671
- const _isMeaningful = (x) => typeof x === 'string' ? x.trim().length > 0 : Boolean(x);
672
- const _inputCount = [args.prompt, args.file, args.ref].filter(_isMeaningful).length;
673
- if (_inputCount > 1) return { error: 'bridge: provide exactly one of prompt|ref|file' };
674
- if (!_isMeaningful(prompt) && _isMeaningful(args.file)) {
675
- try {
676
- const _filePath = (bridgeCwd && !isAbsolute(args.file))
677
- ? resolve(bridgeCwd, args.file)
678
- : args.file;
679
- prompt = await readFile(_filePath, 'utf-8');
680
- } catch (e) {
681
- return { error: `Cannot read file: ${e.message}` };
682
- }
683
- }
684
- if (!_isMeaningful(prompt) && _isMeaningful(args.ref)) {
685
- prompt = _promptStore.get(args.ref);
686
- if (!prompt) return { error: `ref "${args.ref}" not found in prompt store` };
687
- _promptStore.delete(args.ref);
688
- }
689
- if (!_isMeaningful(prompt)) return { error: 'prompt must contain non-whitespace content (or provide a non-empty file/ref)' };
690
- return { prompt };
691
- }
692
-
693
- // Role/preset lookup: bench escape hatch (provider+model) OR
694
- // user-workflow.json role→preset mapping.
695
- function _resolveBridgePreset(args, config) {
696
- if (args.provider && args.model) {
697
- const preset = {
698
- id: '__bench__',
699
- name: '__BENCH__',
700
- type: 'bridge',
701
- provider: String(args.provider),
702
- model: String(args.model),
703
- effort: args.effort ? String(args.effort) : undefined,
704
- fast: args.fast === true,
705
- tools: args.tools ? String(args.tools) : 'full',
706
- };
707
- const promptPrefix = args.systemPrompt ? String(args.systemPrompt) + '\n\n' : null;
708
- return { preset, presetName: preset.name, _roleConfig: null, promptPrefix };
709
- }
710
- // Bench escape hatch, model-only form: a model override with no provider.
711
- // Resolve the provider from the loaded config presets by exact model match
712
- // so the override is honored instead of silently falling through to the
713
- // role default. Errors out (rather than guessing) when no preset matches.
714
- if (args.model && !args.provider) {
715
- const _matches = config.presets?.filter((x) => x.model === String(args.model)) ?? [];
716
- if (_matches.length === 0) {
717
- return { error: `model "${args.model}" did not match any preset in mixdog-config.json — pass provider explicitly or use a preset` };
718
- }
719
- const _providers = [...new Set(_matches.map((x) => String(x.provider)))];
720
- if (_providers.length > 1) {
721
- return { error: `model "${args.model}" is ambiguous — it maps to multiple providers (${_providers.join(', ')}) in mixdog-config.json; pass provider explicitly` };
722
- }
723
- const _matched = _matches[0];
724
- const preset = {
725
- id: '__bench__',
726
- name: '__BENCH__',
727
- type: 'bridge',
728
- provider: String(_matched.provider),
729
- model: String(args.model),
730
- effort: args.effort ? String(args.effort) : undefined,
731
- fast: args.fast === true,
732
- tools: args.tools ? String(args.tools) : 'full',
733
- };
734
- const promptPrefix = args.systemPrompt ? String(args.systemPrompt) + '\n\n' : null;
735
- return { preset, presetName: preset.name, _roleConfig: null, promptPrefix };
736
- }
737
- // Load role→preset mapping from user-workflow.json. Role primitives only —
738
- // no suffix variants, exact match required.
739
- // Route through loadUserWorkflowRoles() so validateRoleConfig runs on
740
- // every entry and _roleConfigCache is populated with full RoleConfig objects
741
- // (including permission). getRoleConfig() then returns the validated record.
742
- loadUserWorkflowRoles();
743
- const _roleConfig = getRoleConfig(args.role);
744
- const presetName = args.preset || _roleConfig?.preset;
745
- if (!presetName) return { error: `role "${args.role}" not found in user-workflow.json (and no preset override given)` };
746
- const preset = config.presets?.find((x) => x.id === presetName || x.name === presetName);
747
- if (!preset) return { error: `preset "${presetName}" (mapped from role "${args.role}") not found in mixdog-config.json` };
748
- return { preset, presetName, _roleConfig, promptPrefix: null };
749
- }
750
-
751
- // cwd resolution: explicit args.cwd > callerCwd > null; then spawn-safe variant.
752
- function _buildBridgeCwds(args, callerCwd) {
753
- const _rawBridgeCwd = args.cwd ? normalizeInputPath(args.cwd) : (callerCwd || null);
754
- const _safeBridgeCwd = _safeCwdForSpawn(_rawBridgeCwd);
755
- return { _rawBridgeCwd, _safeBridgeCwd };
756
- }
757
-
758
- // cwd rewrite: prepend authoritative-cwd note (when silent-swapped) and the
759
- // invariant [effective-cwd] header. Idempotent against retry paths.
760
- function _applyBridgeCwdHeaders(prompt, rawCwd, safeCwd) {
761
- if (rawCwd && safeCwd !== rawCwd) {
762
- prompt = `# Working directory note\nThe authoritative working directory is \`${rawCwd}\`. Use absolute paths starting with this prefix in every tool call. The host process is running from \`${safeCwd}\` because the original cwd contains characters that node's child_process spawn cannot reliably handle on this platform.\n\n${prompt}`;
763
- }
764
- // Invariant header: tell the worker its effective cwd as data so that
765
- // bare `~` in brief text cannot be re-expanded against a container HOME.
766
- // Omit entirely when cwd is null (no workspace context).
767
- // Idempotent: skip if the brief already carries the header (retry paths).
768
- if (rawCwd && !String(prompt).startsWith('[effective-cwd]')) {
769
- prompt = `[effective-cwd] ${rawCwd}\n\n${prompt}`;
770
- }
771
- return prompt;
772
- }
773
-
774
- // R3 Gap 3: clamp forwarded permissionMode against the parent Lead session
775
- // so a bridge worker can never become MORE permissive than its caller.
776
- // Rank: plan(0) < default(1) < acceptEdits(2) < bypassPermissions(3).
777
- // Unknown/missing parent ⇒ treat as 'default'. Unknown requested ⇒ pass
778
- // through unranked (validator already enums the accepted set upstream).
779
- function _clampBridgePermissionMode(requested, parentPermissionMode, role) {
780
- const _permRank = { plan: 0, default: 1, acceptEdits: 2, bypassPermissions: 3 };
781
- const _parentRanked = Object.prototype.hasOwnProperty.call(_permRank, parentPermissionMode);
782
- const _parentRank = _parentRanked ? _permRank[parentPermissionMode] : _permRank.default;
783
- let _permissionMode = requested;
784
- if (requested && Object.prototype.hasOwnProperty.call(_permRank, requested)) {
785
- const _reqRank = _permRank[requested];
786
- if (_reqRank > _parentRank) {
787
- const _clampedTo = _parentRanked ? parentPermissionMode : 'default';
788
- process.stderr.write(
789
- `[bridge-perm-clamp] role=${role} requested=${requested} parent=${parentPermissionMode || 'default'} clamped=${_clampedTo}\n`
790
- );
791
- _permissionMode = _clampedTo;
792
- }
793
- }
794
- return _permissionMode;
795
- }
796
-
797
- // Fire-and-forget code-graph prewarm so the first find_symbol in this
798
- // bridge dispatch hits a warm cache (cold _buildCodeGraph is the 90s
799
- // outlier in PG bridge_calls telemetry). Same pattern as warmupCatalogs.
800
- // Prewarm only when prefetch signals symbol-graph use. files-only
801
- // prefetch embeds file bodies in the prompt and never fires
802
- // find_symbol — prewarming the graph for that path is wasted CPU.
803
- // callers / references are the true graph-touch signals.
804
- function _runBridgePrewarm(workerCwd, prefetch) {
805
- const _prewarmCallers = Array.isArray(prefetch?.callers) ? prefetch.callers : [];
806
- const _prewarmRefs = Array.isArray(prefetch?.references) ? prefetch.references : [];
807
- const _prewarmSymbols = [..._prewarmCallers, ..._prewarmRefs].filter(Boolean);
808
- if (_prewarmSymbols.length > 0) {
809
- // Symbol-aware prewarm populates lazy per-symbol candidate cache
810
- // so the worker's first find_symbol on these names skips the
811
- // O(N) node scan in addition to skipping the graph cold build.
812
- try { prewarmCodeGraphSymbols(workerCwd, _prewarmSymbols); }
813
- catch (e) { process.stderr.write(`[bridge] prewarmCodeGraphSymbols failed: ${e?.message}\n`); }
814
- } else if (prefetch && (prefetch.callers || prefetch.references)) {
815
- // Only fall back to a full graph prewarm when prefetch signals
816
- // symbol-graph use (callers/references fields present). Files-only
817
- // or otherwise empty prefetch never fires find_symbol, so skip.
818
- try { prewarmCodeGraph(workerCwd); }
819
- catch (e) { process.stderr.write(`[bridge] prewarmCodeGraph failed: ${e?.message}\n`); }
820
- }
821
- }
822
-
823
- // --- opt-in git-worktree isolation for bridge workers ---
824
- // When a spawn passes isolation:'worktree' and the resolved workerCwd lives
825
- // inside a git repo, the worker runs in a dedicated `git worktree` + branch so
826
- // parallel workers can edit the same files without stepping on each other's
827
- // working tree. On any git failure we fall back to the plain workerCwd (a warn
828
- // to stderr) so isolation is strictly best-effort and never blocks a spawn.
829
-
830
- // Sanitize a tag/jobId fragment into a shell-safe, git-ref-safe token. Only
831
- // [A-Za-z0-9_-] survive; everything else is dropped. Empty result → null so
832
- // callers fall back to the jobId. Never interpolated into a shell (execFile),
833
- // but sanitized anyway to keep the branch ref well-formed.
834
- function _sanitizeWorktreeName(raw) {
835
- if (raw == null) return null;
836
- const cleaned = String(raw).replace(/[^A-Za-z0-9_-]/g, '');
837
- return cleaned.length > 0 ? cleaned : null;
838
- }
839
-
840
- // Resolve the git repo root for a cwd (null when not a git repo / git absent).
841
- function _gitRepoRoot(cwd) {
842
- try {
843
- const out = execFileSync('git', ['-C', cwd, 'rev-parse', '--show-toplevel'], {
844
- encoding: 'utf8',
845
- stdio: ['ignore', 'pipe', 'ignore'],
846
- // Bound a hung git so it can't block the daemon event loop for ALL
847
- // sessions; on timeout execFileSync throws and the catch falls back.
848
- timeout: 10000,
849
- maxBuffer: 8 * 1024 * 1024,
850
- windowsHide: true,
851
- });
852
- const root = String(out).trim();
853
- return root.length > 0 ? root : null;
854
- } catch {
855
- return null;
856
- }
857
- }
858
-
859
- // Attempt to create an isolated worktree for a worker. Returns the new worktree
860
- // path on success, or null to signal "use plain workerCwd". Best-effort: any
861
- // failure warns to stderr and returns null.
862
- // Returns { worktreePath, branch } on success; null on fallback.
863
- function _setupWorkerWorktree({ workerCwd, jobId, tag }) {
864
- const repoRoot = _gitRepoRoot(workerCwd);
865
- if (!repoRoot) {
866
- try { process.stderr.write(`[bridge] isolation=worktree requested but ${workerCwd} is not in a git repo — using plain cwd\n`); } catch {}
867
- return null;
868
- }
869
- const safeName = _sanitizeWorktreeName(tag) || _sanitizeWorktreeName(jobId) || _sanitizeWorktreeName(`job_${Date.now()}`);
870
- const pluginData = process.env.CLAUDE_PLUGIN_DATA || getPluginData();
871
- const worktreePath = join(pluginData, 'worktrees', _sanitizeWorktreeName(jobId) || safeName);
872
- const branch = `bridge/${safeName}`;
873
- try {
874
- execFileSync('git', ['-C', repoRoot, 'worktree', 'add', worktreePath, '-b', branch], {
875
- encoding: 'utf8',
876
- stdio: ['ignore', 'pipe', 'pipe'],
877
- // Bound a hung git so it can't block the daemon event loop for ALL
878
- // sessions; on timeout execFileSync throws and the catch falls back.
879
- timeout: 10000,
880
- maxBuffer: 8 * 1024 * 1024,
881
- windowsHide: true,
882
- });
883
- return { worktreePath, branch, repoRoot };
884
- } catch (e) {
885
- try { process.stderr.write(`[bridge] git worktree add failed (job=${jobId}) — falling back to plain cwd: ${e?.message ?? e}\n`); } catch {}
886
- return null;
887
- }
888
- }
889
-
890
- // Tear down a worker's isolated worktree in the IIFE finally block. If the
891
- // worktree is clean it is removed and its branch deleted. If dirty, it is kept
892
- // (no auto-merge) and { kept:true, path, changedFiles } is returned so the
893
- // completion emit can surface the leftover path + changed-file count.
894
- function _teardownWorkerWorktree({ worktreePath, branch, repoRoot }) {
895
- let dirty = false;
896
- let changedFiles = 0;
897
- try {
898
- const out = execFileSync('git', ['-C', worktreePath, 'status', '--porcelain'], {
899
- encoding: 'utf8',
900
- stdio: ['ignore', 'pipe', 'ignore'],
901
- // Bound a hung git so it can't block the daemon event loop for ALL
902
- // sessions; on timeout execFileSync throws and the catch falls back.
903
- timeout: 10000,
904
- maxBuffer: 8 * 1024 * 1024,
905
- windowsHide: true,
906
- });
907
- const lines = String(out).split('\n').filter((l) => l.trim().length > 0);
908
- changedFiles = lines.length;
909
- dirty = changedFiles > 0;
910
- } catch (e) {
911
- // If status can't be read, treat as dirty (keep) — never destroy unknown state.
912
- try { process.stderr.write(`[bridge] worktree status failed (${worktreePath}) — keeping worktree: ${e?.message ?? e}\n`); } catch {}
913
- return { kept: true, path: worktreePath, changedFiles: 0 };
914
- }
915
- if (dirty) {
916
- return { kept: true, path: worktreePath, changedFiles };
917
- }
918
- // Clean — remove the worktree and delete its branch.
919
- try {
920
- execFileSync('git', ['-C', repoRoot, 'worktree', 'remove', '--force', worktreePath], {
921
- encoding: 'utf8',
922
- stdio: ['ignore', 'pipe', 'pipe'],
923
- // Bound a hung git so it can't block the daemon event loop for ALL
924
- // sessions; on timeout execFileSync throws and the catch falls back.
925
- timeout: 10000,
926
- maxBuffer: 8 * 1024 * 1024,
927
- windowsHide: true,
928
- });
929
- } catch (e) {
930
- try { process.stderr.write(`[bridge] worktree remove failed (${worktreePath}): ${e?.message ?? e}\n`); } catch {}
931
- }
932
- try {
933
- execFileSync('git', ['-C', repoRoot, 'branch', '-D', branch], {
934
- encoding: 'utf8',
935
- stdio: ['ignore', 'pipe', 'pipe'],
936
- // Bound a hung git so it can't block the daemon event loop for ALL
937
- // sessions; on timeout execFileSync throws and the catch falls back.
938
- timeout: 10000,
939
- maxBuffer: 8 * 1024 * 1024,
940
- windowsHide: true,
941
- });
942
- } catch (e) {
943
- try { process.stderr.write(`[bridge] branch delete failed (${branch}): ${e?.message ?? e}\n`); } catch {}
944
- }
945
- return { kept: false, path: worktreePath, changedFiles: 0 };
946
- }
947
-
948
- // Short model tag for bridge worker lifecycle notifications.
949
- // Strip the redundant `claude-` vendor prefix; other providers
950
- // (gpt-*, etc.) pass through unchanged. Falls back to empty on
951
- // missing model so callers never throw.
952
- function _bridgeModelTag(preset) {
953
- try {
954
- const raw = preset.model;
955
- if (!raw || typeof raw !== 'string') return '';
956
- const stripped = raw.startsWith('claude-') ? raw.slice('claude-'.length) : raw;
957
- return stripped ? `[${stripped}] ` : '';
958
- } catch { return ''; }
959
- }
960
-
961
- // Brief size soft-warn helper: cap is ≤150 words per rules/lead/00-tool-lead.md.
962
- function _bridgeBriefWordCount(prompt) {
963
- return prompt ? String(prompt).trim().split(/\s+/).filter(Boolean).length : 0;
964
- }
965
-
966
- // Logging-only listener so operators can correlate the MCP request
967
- // abort with a still-running detached worker. Returns the detach fn so the
968
- // IIFE finally block can remove it on normal completion (previously
969
- // leaked until GC). Detached bridge workers do NOT close the session on
970
- // MCP request abort — by contract they outlive the originating request.
971
- function _wireRequestAbortLog(requestSignal, { sessionId, role, jobId }) {
972
- if (!requestSignal) return () => {};
973
- const onRequestAbort = () => {
974
- try {
975
- process.stderr.write(
976
- `[bridge] MCP request aborted — detached worker continues: session=${sessionId} role=${role} job=${jobId}\n`,
977
- );
978
- // Intentionally NOT calling closeSession() here — detached bridge
979
- // workers survive the MCP request lifecycle.
980
- } catch (e) { process.stderr.write(`[bridge] onRequestAbort write failed: ${e?.message}\n`); }
981
- };
982
- if (requestSignal.aborted) {
983
- queueMicrotask(onRequestAbort);
984
- return () => {};
985
- }
986
- try {
987
- requestSignal.addEventListener('abort', onRequestAbort, { once: true });
988
- return () => {
989
- try { requestSignal.removeEventListener('abort', onRequestAbort); } catch { /* ignore */ }
990
- };
991
- } catch (e) {
992
- process.stderr.write(`[bridge] addEventListener abort failed: ${e?.message}\n`);
993
- return () => {};
994
- }
995
- }
996
-
997
- // Render: empty-content diagnostic. Builds the telemetry shape, emits the
998
- // stderr breadcrumb, and returns the user-facing diagnostic string. Pure
999
- // w.r.t. the dispatch loop: only stderr side-effect, no state mutation.
1000
- function _renderEmptyContentDiagnostic(result, activeSession) {
1001
- // Telemetry: why did the bridge result lack content?
1002
- const shape = {
1003
- resultType: typeof result,
1004
- contentType: typeof result?.content,
1005
- contentLen: typeof result?.content === 'string' ? result.content.length : null,
1006
- hasToolCalls: Array.isArray(result?.toolCalls) ? result.toolCalls.length : null,
1007
- stopReason: result?.stopReason ?? result?.stop_reason ?? null,
1008
- midstreamRetries: result?.__midstreamRetries ?? null,
1009
- toolCallsTotal: typeof result?.toolCallsTotal === 'number' ? result.toolCallsTotal : null,
1010
- outputTokens: typeof result?.lastUsage?.outputTokens === 'number'
1011
- ? result.lastUsage.outputTokens
1012
- : (typeof result?.usage?.outputTokens === 'number' ? result.usage.outputTokens : null),
1013
- model: activeSession?.model ?? null,
1014
- // Provider content-block classification (anthropic*.mjs):
1015
- // distinguishes thinking-only stalls (reasoning emitted, no
1016
- // text/tool_use) from true silent empty turns. null when
1017
- // the provider doesn't expose these fields.
1018
- hasThinkingContent: typeof result?.hasThinkingContent === 'boolean'
1019
- ? result.hasThinkingContent
1020
- : null,
1021
- contentBlockTypes: Array.isArray(result?.contentBlockTypes)
1022
- ? result.contentBlockTypes.slice()
1023
- : null,
1024
- keys: result && typeof result === 'object' ? Object.keys(result) : null,
1025
- };
1026
- try { process.stderr.write(`[bridge] empty-content fallback for sessionId=${activeSession?.id ?? 'unknown'} shape=${JSON.stringify(shape)}\n`); } catch {}
1027
- // Empty-content protocol guard: emit diagnostic instead of
1028
- // silent empty so Lead always sees actionable signal.
1029
- // Reports iterations + cumulative toolCallsTotal (not just
1030
- // final-turn toolCalls which is 0 when synthesis stalls) +
1031
- // stopReason. Work landed via tool calls survives even when
1032
- // the final synthesis turn returned empty content; this
1033
- // message helps Lead distinguish "work landed, synthesis
1034
- // missing" from "nothing happened".
1035
- const _emptyIterations = result?.iterations ?? 0;
1036
- const _emptyToolCallsTotal = result?.toolCallsTotal ?? shape.hasToolCalls ?? 0;
1037
- const _emptyFinalToolCalls = shape.hasToolCalls ?? 0;
1038
- const _emptyStopReason = shape.stopReason ?? 'unknown';
1039
- const _emptyOutTokens = shape.outputTokens;
1040
- const _emptyModel = shape.model ?? 'unknown';
1041
- const _outTokPart = typeof _emptyOutTokens === 'number'
1042
- ? `${_emptyOutTokens} output token(s)`
1043
- : 'output tokens unknown';
1044
- // Thinking-only stall vs. true empty disambiguator: when the
1045
- // provider classifies content blocks (anthropic*.mjs) we can
1046
- // tell Lead whether the model spent the turn on reasoning
1047
- // (hasThinkingContent=true, blockTypes contains 'thinking')
1048
- // versus producing nothing at all. Older providers leave
1049
- // these null — fall back to "unknown" so Lead still knows
1050
- // the classification wasn't available.
1051
- const _emptyHasThinking = shape.hasThinkingContent;
1052
- const _emptyBlockTypes = shape.contentBlockTypes;
1053
- const _thinkingPart = _emptyHasThinking === null
1054
- ? 'hasThinkingContent=unknown'
1055
- : `hasThinkingContent=${_emptyHasThinking}`;
1056
- const _blockTypesPart = _emptyBlockTypes === null
1057
- ? 'contentBlockTypes=unknown'
1058
- : `contentBlockTypes=[${_emptyBlockTypes.join(',') || 'none'}]`;
1059
- const _classifyHint = _emptyHasThinking === true
1060
- ? ' — thinking-only stall (model emitted reasoning but no text/tool_use); likely max_tokens or budget cutoff mid-synthesis'
1061
- : (_emptyHasThinking === false && Array.isArray(_emptyBlockTypes) && _emptyBlockTypes.length === 0
1062
- ? ' — true empty turn (no content blocks emitted at all)'
1063
- : '');
1064
- return `(empty final synthesis: bridge worker [model=${_emptyModel}] completed ${_emptyIterations} iteration(s) with ${_emptyToolCallsTotal} cumulative tool call(s); final turn had ${_emptyFinalToolCalls} tool call(s), 0 content, ${_outTokPart}. stopReason=${_emptyStopReason}. ${_thinkingPart} ${_blockTypesPart}${_classifyHint}. Tool-side work (read/edit/write/apply_patch) may have landed — if toolCallsTotal>0 check git diff or trace store; if toolCallsTotal=0 and outputTokens=0 nothing happened. Possible causes: provider returned text-empty terminal turn (thinking-only block, pause_turn, max_tokens), worker skipped <final-answer> tag emission, or contract-nudge cap reached. Re-dispatch or inspect landed work.)`;
1065
- }
1066
-
1067
- // Empty <final-answer> guard: extractFinalAnswer returns '' when the
1068
- // model emitted `<final-answer></final-answer>` (or wrapped only
1069
- // whitespace). Lead would otherwise receive a bare `[role]` header
1070
- // and have no signal about what the worker produced. Surface the
1071
- // raw content (truncated) with an explicit warning so the Lead can
1072
- // see the actual model output and decide next steps.
1073
- function _resolveFinalAnswer(content) {
1074
- let _extractedAnswer = extractFinalAnswer(content);
1075
- if (typeof _extractedAnswer === 'string' && _extractedAnswer.trim().length === 0) {
1076
- const _rawSample = typeof content === 'string' ? content.slice(0, 500) : '';
1077
- _extractedAnswer = _rawSample
1078
- ? `(warning: empty <final-answer> tag; raw response: ${_rawSample})`
1079
- : '(warning: worker produced no content)';
1080
- }
1081
- return _extractedAnswer;
1082
- }
1083
-
1084
- // Unified emit-with-delivery-flag helper for the bridge dispatch IIFE.
1085
- // Matches the original four-site pattern: `emit → set delivered → catch
1086
- // emit error → log to stderr (best-effort)`. Returns true on emit success,
1087
- // false on failure; callers OR-in the result so an earlier success cannot
1088
- // be flipped to false by a later branch's emit failure (the original code
1089
- // only ever set `_delivered = true`, never back to false).
1090
- function _bridgeDispatchMeta(jobId, { error = false, instruction } = {}) {
1091
- return {
1092
- type: 'dispatch_result',
1093
- dispatch_id: jobId,
1094
- tool: 'bridge',
1095
- error: String(!!error),
1096
- instruction: instruction || `The bridge dispatch you started earlier (${jobId}) has returned — use this answer in your next step.`,
1097
- };
1098
- }
1099
-
1100
- async function _bridgeEmitDelivered(emit, message, { pathLabel, jobId, sessionId, role, meta }) {
1101
- try {
1102
- await Promise.resolve(emit(message, meta || _bridgeDispatchMeta(jobId)));
1103
- return true;
1104
- } catch (emitErr) {
1105
- try {
1106
- process.stderr.write(`[bridge] emit failed (${pathLabel}): job=${jobId} session=${sessionId} role=${role} ${emitErr instanceof Error ? emitErr.message : String(emitErr)}\n`);
1107
- } catch {}
1108
- return false;
1109
- }
1110
- }
1111
-
1112
- // --- Tool definitions ---
1113
- //
1114
- // Tool array lives in ./tool-defs.mjs (pure data, zero side effects) so
1115
- // it can be imported without dragging the full agent module graph.
1116
- // Re-aliased here as `TOOLS` to keep existing references and the
1117
- // `TOOL_DEFS` export below resolving unchanged.
1118
- import { TOOL_DEFS as TOOLS } from './tool-defs.mjs';
1119
-
1120
- // ── Module exports (for unified server) ──────────────────────────────
1121
-
1122
- export { TOOLS as TOOL_DEFS };
1123
- export { INSTRUCTIONS as instructions };
1124
-
1125
- export async function init() {
1126
- const config = loadConfig();
1127
- // External MCP servers only. Plugin's own tools are injected in-process
1128
- // via the context from server.mjs; a self-ref entry would self-spawn or
1129
- // partially loop back through HTTP, so strip on ingress.
1130
- const externalServers = externalMcpServersFromConfig(config);
1131
- // Run independent init steps in parallel: provider registry + external
1132
- // MCP server connections. Both are network-bound and have no shared
1133
- // dependency, so serialising them was wasted boot latency.
1134
- await Promise.all([
1135
- initProviders(config.providers),
1136
- Object.keys(externalServers).length > 0 ? connectMcpServers(externalServers) : Promise.resolve(),
1137
- ]);
1138
- _lastExternalServersKey = stableJson(externalServers);
1139
- // Force-refresh each provider's /models catalog in the background so models
1140
- // released since the last run are picked up on every MCP start (the old
1141
- // warmupCatalogs respected the 24h provider TTL and no-op'd on a fresh
1142
- // cache). LiteLLM pricing/context metadata is intentionally left on its own
1143
- // 24h TTL — not force-refreshed here.
1144
- setImmediate(() => refreshProviderCatalogsOnStartup());
1145
- seedDefaults();
1146
- startStreamWatchdog(forEachSessionRuntime);
1147
- // Smart Bridge — unified router + cache strategy + profile system.
1148
- // User-role preset mapping comes from user-workflow.json (existing source
1149
- // of truth). Preset catalog (provider/model/effort) comes from config.presets.
1150
- try {
1151
- const { initSmartBridge, getSmartBridge, setRoleResolver } = await import('./orchestrator/smart-bridge/index.mjs');
1152
- const userRoles = loadUserWorkflowRoles();
1153
- const presets = config.presets || [];
1154
- // Inject the role resolver so SmartBridge.resolveSync() can read role
1155
- // configs without lazy-importing this module (avoids the circular
1156
- // require that the old router.mjs had).
1157
- setRoleResolver(getRoleConfig);
1158
- const sb = initSmartBridge({ userRoles, presets });
1159
- // Inject into session manager so createSession() can resolve profiles
1160
- // synchronously (no lazy-import race).
1161
- setSmartBridge(sb);
1162
- // Keep Smart Bridge in sync with user-workflow.json edits.
1163
- watchUserWorkflow((nextRoles) => {
1164
- try { getSmartBridge().updateUserRoles(nextRoles); } catch {}
1165
- });
1166
- } catch (e) {
1167
- process.stderr.write(`[smart-bridge] init skipped: ${e.message}\n`);
1168
- }
1169
- watchAgentConfig();
1170
- }
1171
-
1172
- /**
1173
- * Handle a tool call from the unified server.
1174
- * @param {string} name - tool name
1175
- * @param {object} args - tool arguments
1176
- * @param {{ notifyFn?: (text: string) => void, elicitFn?: (opts: object) => Promise<object> }} [opts]
1177
- */
1178
- export async function handleToolCall(name, args, opts = {}) {
1179
- const _baseNotifyFn = typeof opts.notifyFn === 'function' ? opts.notifyFn : null;
1180
- // Daemon routing: tag every bridge notification with the dispatching MCP
1181
- // session id so the daemon router delivers detached-worker results back to
1182
- // THIS terminal instead of the Lead connection. No-op outside the daemon
1183
- // (routingSessionId undefined → meta unchanged → router falls back to Lead).
1184
- const _routingSessionId = typeof opts.routingSessionId === 'string' && opts.routingSessionId ? opts.routingSessionId : null;
1185
- const _clientHostPid = Number(opts.clientHostPid) || null;
1186
- const notifyFn = _baseNotifyFn
1187
- ? (text, extraMeta) => {
1188
- const patch = {};
1189
- if (_routingSessionId) patch.caller_session_id = _routingSessionId;
1190
- if (_clientHostPid > 0) patch.client_host_pid = String(_clientHostPid);
1191
- const merged = { ...(extraMeta || {}), ...patch };
1192
- // CC channel schema requires meta to be Record<string,string> (channelNotification.ts);
1193
- // coerce every value so a non-string (boolean/number) can't fail zod and silently drop the notify.
1194
- const meta = {};
1195
- for (const [k, v] of Object.entries(merged)) {
1196
- if (v === undefined || v === null) continue;
1197
- // silent_to_agent is an internal routing flag the daemon router and
1198
- // agentNotify consume (=== true) BEFORE the CC zod boundary; keep it
1199
- // boolean so those checks fire. It never reaches CC — silent notifies
1200
- // are dropped or Discord-forwarded pre-CC.
1201
- meta[k] = k === 'silent_to_agent' ? (v === true || v === 'true') : String(v);
1202
- }
1203
- return _baseNotifyFn(text, Object.keys(meta).length ? meta : undefined);
1204
- }
1205
- : null;
1206
- const requestSignal = opts.requestSignal instanceof AbortSignal ? opts.requestSignal : null;
1207
- const callerSessionId = typeof opts.callerSessionId === 'string' && opts.callerSessionId ? opts.callerSessionId : null;
1208
- const callerCwd = typeof opts.callerCwd === 'string' && opts.callerCwd ? opts.callerCwd : null;
1209
- await awaitBootReady(2000);
1210
- // Idempotent fallback — server.mjs populates the registry at boot via
1211
- // loadModule('agent').then(...), but if eager init failed (missing deps,
1212
- // file error), the first tool call still restores it here. Re-registration
1213
- // is safe: setInternalToolsProvider replaces the executor/tools refs.
1214
- if (typeof opts.toolExecutor === 'function' && Array.isArray(opts.internalTools)) {
1215
- setInternalToolsProvider({
1216
- executor: opts.toolExecutor,
1217
- tools: opts.internalTools,
1218
- });
1219
- }
1220
-
1221
- try {
1222
- switch (name) {
1223
- case 'list_models': {
1224
- const cfg = loadConfig();
1225
- const presets = listPresets(cfg);
1226
- const current = getDefaultPreset(cfg);
1227
- if (presets.length === 0) return ok('No presets configured.');
1228
- const currentLabel = current ? `${current.model}${current.effort ? ' · ' + current.effort : ''}${current.fast ? ' · fast' : ''}` : 'none';
1229
- const routeInfo = readGatewayRouteInfo();
1230
- const gatewayRoute = routeInfo?.provider && routeInfo?.model
1231
- ? `${routeInfo.provider} / ${routeInfo.model}${routeInfo.effort ? ' · ' + routeInfo.effort : ''}${routeInfo.fast ? ' · fast' : ''}`
1232
- : 'none';
1233
- // list_models is read-only; default-preset changes go through
1234
- // mixdog-config.json or the config UI, not interactive elicit.
1235
- const lines = presets.map((p, i) => {
1236
- const parts = [p.name, p.model];
1237
- if (p.effort) parts.push(p.effort);
1238
- if (p.fast) parts.push('fast');
1239
- const mark = current && p.name === current.name ? ' ← active' : '';
1240
- return `[${i}] ${parts.join(' · ')}${mark}`;
1241
- });
1242
- return ok({ current: currentLabel, defaultPreset: currentLabel, gatewayRoute, presets: lines });
1243
- }
1244
-
1245
- case 'gateway_select_model': {
1246
- // Interactive gateway routing-target picker via MCP elicitation.
1247
- // Opt-in (only on explicit invocation). Builds the model inventory from
1248
- // the SAME shared helper the CLI uses, then asks the client to render a
1249
- // dropdown. Degrades gracefully when elicitation is unavailable.
1250
- const cfg = loadConfig();
1251
- const inventory = buildGatewayInventory(cfg);
1252
- const choices = gatewayInventoryChoices(inventory);
1253
- if (choices.length === 0) {
1254
- return fail('No enabled agent providers found. Enable a provider in /mixdog:config first.');
1255
- }
1256
- const elicitFn = typeof opts.elicitFn === 'function' ? opts.elicitFn : null;
1257
- // Graceful degradation: no elicitation support → list + point at the
1258
- // slash command instead of opening a dropdown.
1259
- if (!elicitFn) {
1260
- const listing = choices.map((c) => ` • ${c.label}`).join('\n');
1261
- return ok(
1262
- 'Interactive selection is unavailable (this client does not support MCP elicitation).\n'
1263
- + `Available gateway targets:\n${listing}\n\n`
1264
- + 'Run /mixdog:model (or: bun scripts/gateway-model.mjs --set <provider> [model]) to choose one.',
1265
- );
1266
- }
1267
- const ids = choices.map((c) => c.id);
1268
- const labels = choices.map((c) => c.label);
1269
- const listing = choices.map((c, i) => ` ${i + 1}. ${c.label}`).join('\n');
1270
- let res;
1271
- try {
1272
- res = await elicitFn({
1273
- message:
1274
- 'Select how the mixdog gateway should route Claude Code\'s main model:\n\n'
1275
- + `${listing}\n\n`
1276
- + 'The first option follows Claude Code\'s native /model setting on future requests.',
1277
- requestedSchema: {
1278
- type: 'object',
1279
- properties: {
1280
- model: { type: 'string', title: 'Gateway model', enum: ids, enumNames: labels },
1281
- },
1282
- required: ['model'],
1283
- },
1284
- });
1285
- } catch (e) {
1286
- // elicitInput throws if the client capability is missing despite the
1287
- // hook being present — fall back to the listing rather than erroring.
1288
- const listing = choices.map((c) => ` • ${c.label}`).join('\n');
1289
- return ok(
1290
- `Interactive selection failed (${errText(e)}).\n`
1291
- + `Available gateway targets:\n${listing}\n\n`
1292
- + 'Run /mixdog:model to choose one.',
1293
- );
1294
- }
1295
- if (!res || res.action !== 'accept') {
1296
- return ok(`Selection ${res?.action || 'cancelled'} — gateway routing unchanged.`);
1297
- }
1298
- const chosenId = res.content && typeof res.content.model === 'string' ? res.content.model : null;
1299
- const choice = choices.find((c) => c.id === chosenId) || null;
1300
- const parsed = choice || parseGatewayChoiceId(chosenId);
1301
- const provider = parsed?.provider || null;
1302
- let model = parsed?.model || null;
1303
- if (!provider) return fail(`Invalid selection: ${chosenId}`);
1304
- // A provider-only choice (no concrete model) resolves to its first
1305
- // candidate model from the inventory.
1306
- if (!model) {
1307
- const inv = inventory.find((e) => e.provider === provider);
1308
- model = inv && inv.models.length ? inv.models[0] : null;
1309
- }
1310
- if (!model) return fail(`No model available for "${provider}".`);
1311
- const meta = choice || resolveGatewayTargetChoice(inventory, provider, model, false);
1312
- writeGatewayTarget(updateSection, provider, model, meta);
1313
- const label = meta?.mode === CLAUDE_CURRENT_MODE
1314
- ? `Claude Code current (${meta.modelDisplay || model}) — follows Claude Code /model`
1315
- : `${provider} / ${model}${meta?.effort ? ` · ${String(meta.effort).toUpperCase()}` : ''}${meta?.fast ? ' · fast' : ''}`;
1316
- return ok(
1317
- `Gateway routing set: ${label}\n`
1318
- + 'modules.gateway.enabled = true — live on the next gateway request (no restart needed if the gateway is running).',
1319
- );
1320
- }
1321
-
1322
- case 'get_workflows': {
1323
- const workflows = listWorkflows();
1324
- return ok({ workflows });
1325
- }
1326
-
1327
- case 'get_workflow': {
1328
- if (!args.name) return fail('name is required');
1329
- const workflow = getWorkflow(args.name);
1330
- if (!workflow) return fail('workflow not found');
1331
- return ok(workflow);
1332
- }
1333
-
1334
- case 'set_prompt': {
1335
- let content = args.content;
1336
- if (!content && args.file) {
1337
- try {
1338
- content = await readFile(args.file, 'utf-8');
1339
- } catch (e) {
1340
- return fail(`Cannot read file: ${e.message}`);
1341
- }
1342
- }
1343
- if (!content) return fail('content or file is required');
1344
- const key = args.key || `p${++_promptSeq}`;
1345
- _promptStore.set(key, content);
1346
- return ok(`Stored as '${key}' (${content.length} chars)`);
1347
- }
1348
-
1349
- case 'bridge': {
1350
- // Unified worker session control. `type` selects the action; default
1351
- // 'spawn' preserves the legacy detached-dispatch behavior so callers
1352
- // that pass only role+prompt (channels/index.mjs, webhook.mjs) keep
1353
- // working unchanged.
1354
- let bridgeType = typeof args.type === 'string' && args.type ? args.type : 'spawn';
1355
- // True when a cold `send` was flipped to `spawn` (fresh session, no
1356
- // transcript carry-over). Surfaced as `respawned: true` on the spawn
1357
- // immediate-return payload; live `send` resumes use `respawned: false`.
1358
- let _bridgeColdRespawn = false;
1359
-
1360
- // Cold-tag respawn: a `type=send` whose tag no longer resolves to a
1361
- // live session (reaped after the 1h window, or never spawned) but
1362
- // whose role is known (args.role or the session-scoped tag→role map)
1363
- // should respawn FRESH rather than error — the prompt cache is cold
1364
- // anyway, so a new session is the correct outcome. Flip to the spawn
1365
- // path (which re-allocates args.tag + args.role and folds args.message
1366
- // into the prompt) and carry the requested tag forward so the new
1367
- // session registers under the same name. Without a recoverable role we
1368
- // leave bridgeType as 'send' and let the send handler emit its clear
1369
- // "include a role to respawn" error.
1370
- if (bridgeType === 'send') {
1371
- const _sendTarget = args.tag || args.sessionId;
1372
- const _sendTagKey = (typeof args.tag === 'string' && args.tag.trim())
1373
- ? args.tag.trim()
1374
- : (typeof _sendTarget === 'string' && !_sendTarget.startsWith('sess_') ? _sendTarget.trim() : null);
1375
- const _recoveredRole = args.role
1376
- || (_sendTagKey ? _tagRoleRegistry.get(_sendTagKey) : null)
1377
- || null;
1378
- // A raw `sess_...` id only counts as resolved when it maps to a LIVE
1379
- // session — a reaped/closed id must fall through to the respawn
1380
- // branch (role recoverable) rather than being launched against a dead
1381
- // session (#5).
1382
- const _resolved = _sendTarget
1383
- ? (_resolveBridgeTag(_sendTarget) || (typeof _sendTarget === 'string' && _sendTarget.startsWith('sess_') && _isLiveSession(_sendTarget) ? _sendTarget : null))
1384
- : null;
1385
- if (_sendTarget && !_resolved && _recoveredRole) {
1386
- if (args.tag == null && typeof _sendTarget === 'string' && !_sendTarget.startsWith('sess_')) {
1387
- args = { ...args, tag: _sendTarget };
1388
- }
1389
- if (!args.role) args = { ...args, role: _recoveredRole };
1390
- // Restore the original spawn's cwd so a cold respawn does NOT fall
1391
- // back to the daemon launch dir (plugin cache). Explicit args.cwd
1392
- // still wins; this only fills the gap a reaped session left behind.
1393
- if (args.cwd == null && _sendTagKey) {
1394
- const _recoveredCwd = _tagCwdRegistry.get(_sendTagKey);
1395
- if (_recoveredCwd) args = { ...args, cwd: _recoveredCwd };
1396
- }
1397
- bridgeType = 'spawn';
1398
- _bridgeColdRespawn = true;
1399
- }
1400
- }
1401
-
1402
- if (bridgeType === 'list') {
1403
- return ok(_bridgeListSessions({
1404
- role: args.role,
1405
- status: args.status,
1406
- brief: args.brief,
1407
- includeClosed: args.includeClosed,
1408
- }));
1409
- }
1410
-
1411
- if (bridgeType === 'close') {
1412
- const target = args.tag || args.sessionId;
1413
- if (!target) return fail('bridge close: tag (or sess_ id) is required');
1414
- const sessionId = _resolveBridgeTag(target) || (target.startsWith('sess_') ? target : null);
1415
- if (!sessionId) {
1416
- const _coldTag = (typeof args.tag === 'string' && args.tag.trim())
1417
- ? args.tag.trim()
1418
- : (typeof target === 'string' && !target.startsWith('sess_') ? String(target).trim() : null);
1419
- if (_coldTag && _tagRoleRegistry.has(_coldTag)) {
1420
- try { _tagSessionRegistry.delete(_coldTag); } catch {}
1421
- try { _tagRoleRegistry.delete(_coldTag); } catch {}
1422
- try { _tagCwdRegistry.delete(_coldTag); } catch {}
1423
- return ok({ closed: true, forgotten: true, tag: _coldTag, sessionId: null });
1424
- }
1425
- return fail(`bridge close: tag "${target}" does not map to a live session`);
1426
- }
1427
- const res = _bridgeCloseSession(sessionId);
1428
- // Cancel any pending terminal reap — the session is being closed
1429
- // explicitly now, so the deferred hide/tag-reclaim timer is moot.
1430
- _cancelBridgeReap(sessionId);
1431
- // Drop the tag binding so the name can be reused for a fresh spawn.
1432
- const tag = _tagForSessionId(sessionId) || (args.tag && _tagSessionRegistry.get(args.tag) === sessionId ? args.tag : null);
1433
- if (tag) {
1434
- try { _tagSessionRegistry.delete(tag); } catch {}
1435
- try { _tagRoleRegistry.delete(tag); } catch {}
1436
- try { _tagCwdRegistry.delete(tag); } catch {}
1437
- }
1438
- return ok({ ...res, tag: tag || null });
1439
- }
1440
-
1441
- if (bridgeType === 'send') {
1442
- const target = args.tag || args.sessionId;
1443
- if (!target) return fail('bridge send: tag (or sess_ id) is required');
1444
- const message = args.message || args.prompt;
1445
- if (!message) return fail('bridge send: message is required');
1446
- // A raw `sess_...` id resolves only if it points at a LIVE session;
1447
- // a reaped/closed id is treated as unresolved so it cannot launch an
1448
- // async askSession against a dead session (#5). With no role to
1449
- // respawn (the role case was already flipped to spawn above), this
1450
- // falls into the clear "include a role to respawn" error below.
1451
- const sessionId = _resolveBridgeTag(target) || (target.startsWith('sess_') && _isLiveSession(target) ? target : null);
1452
- // A cold tag carrying a role was already flipped to the spawn path
1453
- // above, so reaching here means no role was supplied — guide the
1454
- // caller to respawn fresh.
1455
- if (!sessionId) return fail(`bridge send: tag "${target}" not found (may have been reaped after 1h idle) — include a role to respawn fresh`);
1456
- // Busy worker → QUEUE, don't reject (Claude Code pendingMessages
1457
- // pattern). The tag is published BEFORE the worker enters askSession
1458
- // (spawn) and re-published on each retry rebind, so a `send` can land
1459
- // while the startup / retry turn is still in flight. Rather than
1460
- // making it the first user message (jumping ahead of the original
1461
- // prompt) or rejecting it, enqueue it onto the per-session pending
1462
- // queue; askSession drains the queue after the in-flight turn and
1463
- // runs queued messages — in order — as follow-up turns. This both
1464
- // removes the startup race (queued send runs AFTER the original
1465
- // prompt) and lets a mid-turn send land without a retry. We treat a
1466
- // live, un-aborted controller OR a non-terminal status as "busy".
1467
- {
1468
- const _rt = getSessionRuntime(sessionId);
1469
- const _inFlight = !!(_rt && _rt.controller && _rt.controller.signal && !_rt.controller.signal.aborted);
1470
- let _sess = null;
1471
- try { _sess = getSession(sessionId); } catch { _sess = null; }
1472
- const _activeStates = new Set(['connecting', 'requesting', 'streaming', 'tool_running', 'running', 'cancelling']);
1473
- // #2 race close: prefer the RUNTIME terminal stage over a stale
1474
- // persisted 'running'. Spawn keeps persisted status 'running' until
1475
- // AFTER the completion emit (updateSessionStatus(idle) runs only
1476
- // after the await _bridgeEmitDelivered, OUTSIDE askSession). A send
1477
- // landing in that post-askSession / pre-idle gap would see persisted
1478
- // 'running' and wrongly ENQUEUE onto a queue nothing drains (the
1479
- // turn already returned past askSession's drain point) — stranded.
1480
- // The runtime stage flips to a terminal value (done/idle/error) the
1481
- // instant the turn completes (markSessionDone), so when a runtime
1482
- // entry exists it is authoritative for in-flight detection: enqueue
1483
- // ONLY when the runtime stage is genuinely active. Fall back to the
1484
- // persisted status only when there is no runtime entry at all (a
1485
- // truly live ask always creates one via markSessionAskStart).
1486
- const _runtimeStage = _rt?.stage || null;
1487
- let _busy;
1488
- if (_inFlight) {
1489
- _busy = true;
1490
- } else if (_runtimeStage) {
1491
- _busy = _activeStates.has(_runtimeStage);
1492
- } else {
1493
- const _status = _sess?.status || null;
1494
- _busy = !!(_status && _activeStates.has(_status));
1495
- }
1496
- if (_busy) {
1497
- const _depth = enqueuePendingMessage(sessionId, message);
1498
- return ok({
1499
- queued: true,
1500
- tag: _tagForSessionId(sessionId) || (typeof args.tag === 'string' ? args.tag : null) || null,
1501
- sessionId,
1502
- queueDepth: _depth,
1503
- respawned: false,
1504
- });
1505
- }
1506
- }
1507
- // Resume the conversation — askSession replays session.messages from
1508
- // the {id}.json transcript, so a follow-up continues where the
1509
- // detached worker left off. Like spawn, a detached resume does NOT
1510
- // bail on an already-aborted request signal — it runs to completion and
1511
- // delivers via notifyFn (abort is logging-only, wired below).
1512
- // Cancel the pending 1h terminal reap (scheduled when the spawn's
1513
- // initial turn completed) so this just-resumed / in-flight session is
1514
- // not hidden + tag-reclaimed mid-resume. Re-armed after the resumed
1515
- // turn settles below.
1516
- _cancelBridgeReap(sessionId);
1517
- // Keep the tag bound to the live session across the resume — if the
1518
- // reap had already raced ahead and dropped the binding, restore it so
1519
- // the session stays addressable by tag afterward.
1520
- const _liveTag = (typeof args.tag === 'string' && args.tag.trim()) ? args.tag.trim() : _tagForSessionId(sessionId);
1521
- if (_liveTag) { try { _tagSessionRegistry.set(_liveTag, sessionId); } catch {} }
1522
- // Detached resume — mirror the spawn dispatch path. The guard, the
1523
- // reap-cancel and the tag rebind above all run synchronously BEFORE
1524
- // we hand off; from here we generate a jobId and return IMMEDIATELY
1525
- // ({ jobId, sessionId, tag, detached:true }) without blocking on
1526
- // askSession. The resume runs inside the async IIFE below and its
1527
- // reply is delivered via notifyFn using the SAME shape spawn's
1528
- // completion emit uses (`${modelTag}[${role}] ${answer}`), so the
1529
- // Lead + channel see it like a SPAWN-OK dispatch result instead of a
1530
- // synchronous return value.
1531
- const sendJobId = `bridge_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
1532
- const _sendSession = (() => { try { return getSession(sessionId); } catch { return null; } })();
1533
- const sendRole = _sendSession?.role || 'worker';
1534
- // Prefer the live bridge tag for completion labels; fall back through
1535
- // the persisted session tag, then role only if the tag is missing.
1536
- const sendTag = _liveTag || _tagForSessionId(sessionId) || _sendSession?.bridgeTag || sendRole;
1537
- const sendModelTag = _bridgeModelTag({ model: _sendSession?.model });
1538
- const emit = notifyFn || (() => {});
1539
- addPending(process.env.CLAUDE_PLUGIN_DATA, sendJobId, 'bridge', [sendTag || sendRole], _routingSessionId, _clientHostPid);
1540
- // Detached resume MUST outlive the originating MCP request — mirror
1541
- // the spawn dispatch contract: do NOT close/kill the session on
1542
- // request abort (#1). A request-abort that tore down the resumed
1543
- // session here used to flip a long resume to silently-dropped /
1544
- // cancelled the moment the MCP request lifecycle ended. Only a
1545
- // logging listener is installed; operators stop the work explicitly
1546
- // via bridge type=close.
1547
- const _detachSendAbortLog = _wireRequestAbortLog(requestSignal, {
1548
- sessionId,
1549
- role: sendRole,
1550
- jobId: sendJobId,
1551
- });
1552
- (async () => {
1553
- let _delivered = false;
1554
- try {
1555
- const result = await askSession(sessionId, message, args.context || null);
1556
- let content;
1557
- if (result && typeof result.content === 'string' && result.content.length > 0) {
1558
- content = result.content;
1559
- } else {
1560
- content = _renderEmptyContentDiagnostic(result, _sendSession);
1561
- }
1562
- const _extractedAnswer = _resolveFinalAnswer(content);
1563
- try {
1564
- const _tail = setPendingResult(process.env.CLAUDE_PLUGIN_DATA, sendJobId, 'bridge', [sendTag || sendRole], content, false, _routingSessionId, _clientHostPid);
1565
- if (_tail && typeof _tail.then === 'function') await _tail;
1566
- } catch (e) { try { process.stderr.write(`[bridge] setPendingResult (send success) failed: job=${sendJobId} role=${sendRole} ${e?.message ?? e}\n`); } catch {} }
1567
- _delivered = (await _bridgeEmitDelivered(
1568
- emit,
1569
- `${sendModelTag}[${sendTag}] ${_extractedAnswer}`,
1570
- { pathLabel: 'send success path', jobId: sendJobId, sessionId, role: sendRole },
1571
- )) || _delivered;
1572
- } catch (err) {
1573
- if (err instanceof SessionClosedError) {
1574
- // Preserve SessionClosedError handling — emit a cancellation
1575
- // notice only for reasons that warrant one (mirrors spawn).
1576
- // Label by tag (bridgeTag||role), matching the spawn cancel
1577
- // path (#4).
1578
- let reason = err.reason || null;
1579
- if (!reason && typeof err.message === 'string') {
1580
- const m = err.message.match(/reason=([\w-]+)/);
1581
- if (m) reason = m[1];
1582
- }
1583
- if (shouldEmitBridgeCancellation(reason)) {
1584
- _delivered = (await _bridgeEmitDelivered(
1585
- emit,
1586
- reason ? `${sendTag} cancelled (reason=${reason})` : `${sendTag} cancelled`,
1587
- { pathLabel: 'send cancel path', jobId: sendJobId, sessionId, role: sendRole, meta: _bridgeDispatchMeta(sendJobId, { error: true }) },
1588
- )) || _delivered;
1589
- } else {
1590
- _delivered = true;
1591
- }
1592
- } else {
1593
- const _errBody = `${sendTag} error: ${errText(err)}`;
1594
- try {
1595
- const _tail = setPendingResult(process.env.CLAUDE_PLUGIN_DATA, sendJobId, 'bridge', [sendTag || sendRole], _errBody, true, _routingSessionId, _clientHostPid);
1596
- if (_tail && typeof _tail.then === 'function') await _tail;
1597
- } catch (e) { try { process.stderr.write(`[bridge] setPendingResult (send error) failed: job=${sendJobId} role=${sendRole} ${e?.message ?? e}\n`); } catch {} }
1598
- // Error label by tag (bridgeTag||role) to match the spawn error
1599
- // path (#4).
1600
- _delivered = (await _bridgeEmitDelivered(
1601
- emit,
1602
- _errBody,
1603
- { pathLabel: 'send error path', jobId: sendJobId, sessionId, role: sendRole, meta: _bridgeDispatchMeta(sendJobId, { error: true }) },
1604
- )) || _delivered;
1605
- }
1606
- } finally {
1607
- try { _detachSendAbortLog(); } catch { /* ignore */ }
1608
- if (_delivered) {
1609
- try { removePending(process.env.CLAUDE_PLUGIN_DATA, sendJobId); } catch {}
1610
- } else {
1611
- try { process.stderr.write(`[bridge] keeping pending record (send emit not delivered): job=${sendJobId} session=${sessionId} role=${sendRole}\n`); } catch {}
1612
- }
1613
- // Re-arm the deferred reap on BOTH success and failure (#2 fix):
1614
- // send() cancelled the pending reap before resuming, so a failed
1615
- // resumed turn would otherwise leave the idle/error session never
1616
- // terminally reaped. _scheduleBridgeReap cancels any prior timer
1617
- // so this is idempotent. Keep the tag bound across the resume.
1618
- // The detached resume outlives the request (no close-on-abort),
1619
- // so always re-arm.
1620
- {
1621
- if (_liveTag) { try { _tagSessionRegistry.set(_liveTag, sessionId); } catch {} }
1622
- try { _scheduleBridgeReap(sessionId); } catch { /* ignore */ }
1623
- }
1624
- }
1625
- })().catch(async (err) => {
1626
- try { process.stderr.write(`[bridge] detached send runner unhandled: session=${sessionId} role=${sendRole} job=${sendJobId} ${err instanceof Error ? (err.stack || err.message) : String(err)}\n`); } catch {}
1627
- const _crashDelivered = await _bridgeEmitDelivered(
1628
- emit,
1629
- `${sendTag} crash: ${errText(err)}`,
1630
- { pathLabel: 'send crash path', jobId: sendJobId, sessionId, role: sendRole, meta: _bridgeDispatchMeta(sendJobId, { error: true }) },
1631
- );
1632
- if (_crashDelivered) {
1633
- try { removePending(process.env.CLAUDE_PLUGIN_DATA, sendJobId); } catch {}
1634
- } else {
1635
- try { process.stderr.write(`[bridge] keeping pending record (send crash emit not delivered): job=${sendJobId} session=${sessionId} role=${sendRole}\n`); } catch {}
1636
- }
1637
- });
1638
-
1639
- return ok({
1640
- jobId: sendJobId,
1641
- sessionId,
1642
- tag: _tagForSessionId(sessionId) || _liveTag || null,
1643
- detached: true,
1644
- respawned: false,
1645
- });
1646
- }
1647
-
1648
- if (bridgeType !== 'spawn') {
1649
- return fail(`bridge: unknown type "${bridgeType}" (expected spawn|send|close|list)`);
1650
- }
1651
-
1652
- // --- spawn (default): dispatch a detached worker ---
1653
- // Enforce exactly-one-of: prompt | file | ref. Schema is the first
1654
- // gate; _resolveBridgePrompt is the second defence for clients that
1655
- // bypass JSON Schema validation. Resolve the bridge
1656
- // worker's cwd BEFORE reading args.file so a relative path is resolved
1657
- // against the worker's effective workspace (args.cwd > callerCwd), not
1658
- // the agent host process cwd.
1659
- if (!args.role) return fail('role is required');
1660
- // dev-sync recycle barrier: if dev-sync has flagged THIS daemon
1661
- // (server_pid) for a forced restart, the in-band kill is delayed and
1662
- // the daemon keeps serving for the kill-delay + respawn window.
1663
- // Spawning here would bind the worker to about-to-die, stale
1664
- // code/schema daemon (the stale-schema-after-restart incident).
1665
- // Fail fast with a retryable message so the caller's next call
1666
- // reconnects to the fresh daemon instead.
1667
- if (isCurrentDaemonDoomed()) {
1668
- return fail('bridge spawn deferred: this daemon (server_pid) is being recycled by dev-sync — retry; the next call reconnects to the fresh daemon');
1669
- }
1670
- // Allocate the tag up front so a duplicate-live-tag request fails fast
1671
- // before any session is created.
1672
- const _tagAlloc = _allocateBridgeTag(args.tag, args.role);
1673
- if (_tagAlloc.error) return fail(_tagAlloc.error);
1674
- const bridgeTag = _tagAlloc.tag;
1675
- const { _rawBridgeCwd, _safeBridgeCwd } = _buildBridgeCwds(args, callerCwd);
1676
- const _promptResolution = await _resolveBridgePrompt(args, _rawBridgeCwd);
1677
- if (_promptResolution.error) {
1678
- const _messageOnly = args.message != null && args.prompt == null && args.file == null && args.ref == null;
1679
- return fail(_messageOnly
1680
- ? 'bridge spawn requires prompt (or file/ref). message is only for send.'
1681
- : _promptResolution.error);
1682
- }
1683
- let prompt = _promptResolution.prompt;
1684
-
1685
- // Bench escape hatch — when both provider and model are supplied,
1686
- // build an ad-hoc preset on the fly and bypass mixdog-config.json.
1687
- // Role still drives BP2 catalog scoping (unknown role names fall back
1688
- // to the legacy all-in-one catalog inside loadScopedRoleCatalog).
1689
- // systemPrompt is appended as a prefix to prompt so the same dispatch
1690
- // path can carry caller-injected guidance without changing the
1691
- // session-builder schema.
1692
- const config = loadConfig();
1693
- const _presetResolution = _resolveBridgePreset(args, config);
1694
- if (_presetResolution.error) return fail(_presetResolution.error);
1695
- const preset = _presetResolution.preset;
1696
- const presetName = _presetResolution.presetName;
1697
- const _roleConfig = _presetResolution._roleConfig;
1698
- if (_presetResolution.promptPrefix) {
1699
- prompt = _presetResolution.promptPrefix + prompt;
1700
- }
1701
-
1702
- const role = args.role;
1703
- const effectiveLane = 'bridge';
1704
- const runtimeSpec = resolveRuntimeSpec(preset, {
1705
- lane: effectiveLane,
1706
- agentId: role,
1707
- });
1708
-
1709
- // Stateless ephemeral session — created fresh per call (v0.6.97+).
1710
- // No pool, no resume, no reset. Provider-level prefix cache still
1711
- // hits because cache is content-keyed, not session-keyed. Shared
1712
- // with the Smart Bridge path via session-builder so role/preset
1713
- // telemetry stays bit-identical in bridge-trace.jsonl.
1714
- // P1 fix: cwd silent-swap surfacing. When _safeCwdForSpawn substitutes
1715
- // process.cwd() for a non-ASCII Windows cwd, the agent has no way to
1716
- // know the authoritative working directory and may resolve relative
1717
- // paths into the wrong tree. Inject a header into the prompt so the
1718
- // worker uses absolute paths under the originally-requested cwd.
1719
- // R3 Gap 3: clamp forwarded permissionMode against the parent Lead session
1720
- // so a bridge worker can never become MORE permissive than its caller.
1721
- const _requestedPermissionMode = args.permission_mode || args.permissionMode || undefined;
1722
- let _parentPermissionMode = null;
1723
- if (callerSessionId) {
1724
- try { _parentPermissionMode = getSession(callerSessionId)?.permissionMode || null; }
1725
- catch (e) {
1726
- // Fail closed: an unreadable parent must clamp the worker to the most
1727
- // restrictive mode ('plan'), not silently pass as a default parent.
1728
- try { process.stderr.write(`[bridge] parent permission lookup failed for ${callerSessionId}: ${e?.message ?? e}\n`); } catch {}
1729
- _parentPermissionMode = 'plan';
1730
- }
1731
- }
1732
- const _permissionMode = _clampBridgePermissionMode(_requestedPermissionMode, _parentPermissionMode, args.role);
1733
- prompt = _applyBridgeCwdHeaders(prompt, _rawBridgeCwd, _safeBridgeCwd);
1734
- const { session, effectiveCwd } = prepareBridgeSession({
1735
- role,
1736
- presetName,
1737
- preset,
1738
- runtimeSpec,
1739
- cwd: _safeBridgeCwd,
1740
- sourceType: 'lead',
1741
- sourceName: role,
1742
- parentSessionId: callerSessionId,
1743
- permissionMode: _permissionMode,
1744
- cacheKeyOverride: args.cacheKey || undefined,
1745
- permission: _roleConfig?.permission || undefined,
1746
- // Persist the bridge tag on the session JSON (sync save inside
1747
- // createSession lands it BEFORE the heartbeat publish below) so the
1748
- // forked statusline process + aggregator can read s.bridgeTag.
1749
- bridgeTag,
1750
- clientHostPid: _clientHostPid || undefined,
1751
- });
1752
-
1753
- // workerCwd precedence: explicit Lead intent (effectiveCwd) > the
1754
- // dispatching session's cwd (callerCwd) > the ambient override/user cwd
1755
- // (pwd()). callerCwd sits ahead of pwd() so a worker never silently
1756
- // inherits the daemon launch dir (plugin cache) when no explicit cwd was
1757
- // given — pwd() in the daemon resolves to process.cwd() == plugin root.
1758
- let workerCwd = effectiveCwd || callerCwd || pwd();
1759
-
1760
- const jobId = `bridge_${Date.now()}_${Math.random().toString(36).slice(2, 6)}`;
1761
- // Opt-in git-worktree isolation (args.isolation === 'worktree'): run the
1762
- // worker in a dedicated worktree + branch so parallel workers can edit
1763
- // the same files without clobbering each other's working tree. Resolved
1764
- // BEFORE prewarm so the code-graph cache warms against the worktree path.
1765
- // Best-effort — any git failure falls back to the plain workerCwd. The
1766
- // handle is captured for teardown in the IIFE finally block.
1767
- // Pre-isolation cwd: the real workerCwd before any worktree swap. The
1768
- // worktree path is per-jobId ephemeral and deleted on clean teardown,
1769
- // so a cold tag-respawn must restore THIS (the real repo cwd), not the
1770
- // swapped worktree dir which would no longer exist.
1771
- const _preIsolationWorkerCwd = workerCwd;
1772
- let _workerWorktree = null;
1773
- if (args.isolation === 'worktree') {
1774
- _workerWorktree = _setupWorkerWorktree({ workerCwd, jobId, tag: bridgeTag });
1775
- if (_workerWorktree) workerCwd = _workerWorktree.worktreePath;
1776
- }
1777
- _runBridgePrewarm(workerCwd, args.prefetch);
1778
- // Brief size soft-warn: cap is ≤150 words per subsystem per rules/lead/00-tool-lead.md.
1779
- const _briefWordCount = _bridgeBriefWordCount(prompt);
1780
- if (_briefWordCount > 150) {
1781
- process.stderr.write(
1782
- `[brief-size-warn] brief is ${_briefWordCount} words (cap=150) — split into multiple roles\n`
1783
- );
1784
- }
1785
- const modelLabel = preset.model || preset.name;
1786
- const emit = notifyFn || (() => {});
1787
- // Persist the in-flight bridge job so a child crash/restart can surface
1788
- // a loss notification via recoverPending() on next bootstrap instead of
1789
- // leaving the session permanently stuck at 'running'.
1790
- addPending(process.env.CLAUDE_PLUGIN_DATA, jobId, 'bridge', [role], _routingSessionId, _clientHostPid);
1791
- // Synchronous .hb write at dispatch entry (BEFORE the async IIFE) so
1792
- // the status aggregator surfaces this session immediately. Without
1793
- // this, the first heartbeat had to wait for the IIFE to spawn and the
1794
- // first stream delta to land — short / cached bridge-worker calls
1795
- // (recall / search / explore via dispatchAiWrapped) often completed
1796
- // before any .hb existed, making them invisible on the statusline.
1797
- // publishHeartbeat is atomic (tmp + rename) and ≤5s self-throttled,
1798
- // so the IIFE's own first markSessionStreamDelta naturally collapses
1799
- // into the same throttle window — no double-write.
1800
- try { publishSessionHeartbeat(session.id); } catch (e) { process.stderr.write(`[bridge] publishSessionHeartbeat failed: ${e?.message}\n`); }
1801
- // Public `bridge` is intentionally detached: we return immediately and
1802
- // keep the session alive until it completes (or is explicitly closed).
1803
- // Tying requestSignal to session lifetime caused long reviewer runs to
1804
- // flip to `role cancelled` when the MCP request lifecycle ended before
1805
- // the detached worker finished. Detached mode therefore does NOT wire
1806
- // request abort into closeSession(); operators can still stop the work
1807
- // explicitly through bridge type=close.
1808
- //
1809
- // Logging-only listener so operators can correlate the MCP request
1810
- // abort with a still-running detached worker. Tracked locally so the
1811
- // IIFE finally block can detach it on normal completion (previously
1812
- // leaked until GC).
1813
- const _detachRequestAbortLog = _wireRequestAbortLog(requestSignal, {
1814
- sessionId: session.id,
1815
- role,
1816
- jobId,
1817
- });
1818
- const modelTag = _bridgeModelTag(preset);
1819
-
1820
- // Detached bridge workers do NOT close the session on MCP request
1821
- // abort — by contract they outlive the originating request. The
1822
- // logging listener installed above is the only request-lifecycle
1823
- // tie-in; no closeSession-on-abort wire-up is installed here.
1824
-
1825
- // Track the active session id across retry attempts (may be replaced
1826
- // on each retry by a fresh session — the outer `session` binding is
1827
- // the first attempt; activeSession is updated per attempt and hoisted
1828
- // outside the IIFE so the .catch() handler can reference it).
1829
- let activeSession = session;
1830
- // Track all session IDs created across retry attempts so the finally
1831
- // block can clean up every _sessionJobRegistry entry, not just the last.
1832
- const _allRetrySessionIds = new Set([session.id]);
1833
- // Register initial mapping so bridge type=close can resolve the job's current
1834
- // session before the IIFE has had a chance to update it.
1835
- _jobSessionRegistry.set(jobId, activeSession.id);
1836
- _sessionJobRegistry.set(activeSession.id, jobId);
1837
- // Bind the tag → current sessionId so `bridge type=send|close tag=<tag>`
1838
- // can reach this worker. Kept in lockstep with _jobSessionRegistry on
1839
- // retry-swap (below) so the tag stays stable across retry attempts.
1840
- _tagSessionRegistry.set(bridgeTag, activeSession.id);
1841
- if (typeof args.tag === 'string' && args.tag.trim()) {
1842
- try { _tagRoleRegistry.set(bridgeTag, role); } catch { /* ignore */ }
1843
- // Capture the resolved cwd so a later cold respawn restores it. Use
1844
- // the PRE-isolation cwd: a worktree path is per-jobId ephemeral and
1845
- // removed on clean teardown, so a tag-respawn must start from the
1846
- // real repo cwd, not a dir that may no longer exist.
1847
- try { if (_preIsolationWorkerCwd) _tagCwdRegistry.set(bridgeTag, _preIsolationWorkerCwd); } catch { /* ignore */ }
1848
- }
1849
- // Close the send busy-guard's startup race: the tag is now resolvable
1850
- // but the session status is not written 'running' until inside the async
1851
- // IIFE below (and the runtime entry isn't created until askSession). A
1852
- // `send` landing in that gap would otherwise see no controller + no
1853
- // active status and wrongly become the first askSession turn. Mark the
1854
- // runtime stage 'connecting' synchronously so the guard's active-status
1855
- // set already catches this window. (in-memory + immediate, unlike the
1856
- // worker-thread-deferred updateSessionStatus disk write).
1857
- try { updateSessionStage(activeSession.id, 'connecting'); } catch { /* ignore */ }
1858
- // Wrap the detached async IIFE in runWithCwdOverride so all builtin tool
1859
- // calls inside this worker's async context resolve paths against workerCwd
1860
- // via pwd() — no manual callerCwd propagation through nested calls needed.
1861
- (async () => runWithCwdOverride(workerCwd, async () => {
1862
- const t0 = Date.now();
1863
- let errorMessage = null;
1864
- let result = null;
1865
- let lastIteration = 0;
1866
- let stallWatch = { stop() {}, fired() { return false; } };
1867
- let _delivered = false;
1868
- // Worktree-isolation teardown result, computed once on the success
1869
- // path (so a dirty leftover can be appended to the completion emit)
1870
- // and otherwise in the finally block. Guarded so it only runs once.
1871
- let _worktreeTornDown = false;
1872
- let _worktreeTeardown = null;
1873
- const _runWorktreeTeardown = () => {
1874
- if (_worktreeTornDown || !_workerWorktree) return _worktreeTeardown;
1875
- _worktreeTornDown = true;
1876
- try { _worktreeTeardown = _teardownWorkerWorktree(_workerWorktree); }
1877
- catch (e) { try { process.stderr.write(`[bridge] worktree teardown failed: job=${jobId} ${e?.message ?? e}\n`); } catch {} }
1878
- return _worktreeTeardown;
1879
- };
1880
- try {
1881
- await updateSessionStatus(activeSession.id, 'running');
1882
- // Bridge Start — silent_to_agent so the "started" lifecycle ping
1883
- // surfaces on Discord / user terminal but does NOT land in the Lead
1884
- // agent's context (redundant since Lead already knows it just
1885
- // dispatched). Done / Error emissions below stay non-silent so the
1886
- // Lead receives the result / failure.
1887
- // Best-effort: a synchronous notifyFn throw on this non-critical
1888
- // lifecycle ping must not abort the bridge work (matches the
1889
- // completion / error emit pattern wrapped via _bridgeEmitDelivered).
1890
- try { emit(`${modelTag}${role} started`, { silent_to_agent: true }); } catch (emitErr) {
1891
- try { process.stderr.write(`[bridge] emit failed (started path): job=${jobId} session=${activeSession.id} role=${role} ${emitErr instanceof Error ? emitErr.message : String(emitErr)}\n`); } catch {}
1892
- }
1893
- // Per-session stall watchdog — complements the orchestrator's
1894
- // stream-watchdog (which fires at 300s/600s on raw stream silence).
1895
- // This one catches the bridge-specific case where the lead is
1896
- // waiting on a `worker finished` notification that never arrives:
1897
- // if the SSE stream is quiet beyond STALL_TIMEOUT_S (default 600s)
1898
- // and the session isn't in `tool_running`, emit via notifyFn and
1899
- // abort so the outer catch renders a normal error footer.
1900
- //
1901
- // The watchdog is one-shot (self-stops after firing). For
1902
- // stall-class auto-retry we re-arm a fresh watchdog on each
1903
- // attempt below, and expose the *current* watchdog to the
1904
- // retry wrapper via `isStallAbort` so the race-rejector path
1905
- // (SessionClosedError 'aborted during call' with no closeReason)
1906
- // is still classified as a stall.
1907
- const _startStallWatch = () => startBridgeStallWatchdog({
1908
- sessionId: activeSession.id,
1909
- // Retry replaces activeSession; the watchdog's flush/hide path
1910
- // must target the *current* session id, not the original one
1911
- // captured at construction.
1912
- getSessionId: () => activeSession.id,
1913
- getRuntime: () => getSessionRuntime(activeSession.id),
1914
- getIteration: () => lastIteration,
1915
- abort: (reason) => {
1916
- const rt = getSessionRuntime(activeSession.id);
1917
- rt?.controller?.abort?.(reason);
1918
- try { closeSession(activeSession.id, String(reason?.message || reason || 'stall-watchdog')); } catch {}
1919
- },
1920
- notify: emit,
1921
- modelTag,
1922
- role,
1923
- });
1924
- stallWatch = _startStallWatch();
1925
- ({ result } = await runWithDispatchRetry({
1926
- role,
1927
- jobId,
1928
- startAttempt: 0,
1929
- // Detached bridge worker survives the MCP request lifecycle by
1930
- // contract; passing requestSignal would let the retry wrapper
1931
- // abort at attempt boundaries when the originating request ends.
1932
- externalSignal: null,
1933
- // Race-rejector path in manager.mjs:1232 surfaces a generic
1934
- // SessionClosedError('aborted during call', reason=null) — the
1935
- // structured BridgeStallAbortError marker is lost. Expose the
1936
- // current watchdog's fired() so runWithDispatchRetry can still
1937
- // classify that surface as a retryable stall.
1938
- isStallAbort: () => { try { return stallWatch.fired(); } catch { return false; } },
1939
- runFn: async (attempt) => {
1940
- if (attempt > 0) {
1941
- // Job-level cancellation tombstone check — bridge type=close
1942
- // may have planted this while runWithDispatchRetry was
1943
- // sleeping on STALL_RETRY_BACKOFF_MS. If set, surface a
1944
- // SessionClosedError(reason=manual) so the outer catch
1945
- // takes the silent-cancel path instead of spinning a
1946
- // fresh retry session against a job the user already
1947
- // killed.
1948
- if (_jobCancelledTombstones.has(jobId)) {
1949
- throw new SessionClosedError(activeSession.id, 'cancelled before retry (reason=manual)', 'manual');
1950
- }
1951
- // Fresh session for retry — no message-history carry-over.
1952
- try { closeSession(activeSession.id, 'retry-replaced'); } catch {}
1953
- const retryBuilt = prepareBridgeSession({
1954
- role,
1955
- presetName,
1956
- preset,
1957
- runtimeSpec,
1958
- cwd: _safeBridgeCwd,
1959
- sourceType: 'lead',
1960
- sourceName: role,
1961
- parentSessionId: callerSessionId,
1962
- permissionMode: _permissionMode,
1963
- cacheKeyOverride: args.cacheKey || undefined,
1964
- permission: _roleConfig?.permission || undefined,
1965
- // Carry the stable bridge tag onto the replacement session
1966
- // JSON so statusline/aggregator keep reading it post-swap.
1967
- bridgeTag,
1968
- clientHostPid: _clientHostPid || undefined,
1969
- });
1970
- activeSession = retryBuilt.session;
1971
- _allRetrySessionIds.add(activeSession.id);
1972
- // Keep jobId→sessionId registry current so bridge type=close
1973
- // (called by the Lead with the original sessionId, which
1974
- // bridge type=close resolves via _sessionJobRegistry) can
1975
- // forward the close to the replacement worker.
1976
- _jobSessionRegistry.set(jobId, activeSession.id);
1977
- _sessionJobRegistry.set(activeSession.id, jobId);
1978
- // Keep the tag pointing at the live replacement so a
1979
- // concurrent `bridge type=send tag=<tag>` resumes the current
1980
- // retry session, not the discarded one. The tag is stable
1981
- // across the swap; only the underlying sessionId changes.
1982
- _tagSessionRegistry.set(bridgeTag, activeSession.id);
1983
- // Same startup-race guard as the initial bind: the rebound
1984
- // tag now resolves to the fresh retry session, but its status
1985
- // isn't 'running' until the updateSessionStatus below. Mark
1986
- // the runtime stage 'connecting' synchronously so a `send`
1987
- // arriving in this rebind→running gap is rejected by the
1988
- // busy-guard instead of becoming the first askSession turn.
1989
- try { updateSessionStage(activeSession.id, 'connecting'); } catch { /* ignore */ }
1990
- await updateSessionStatus(activeSession.id, 'running');
1991
- // Best-effort: a synchronous notifyFn throw on this non-critical
1992
- // retry-attempt ping must not abort the bridge work (matches the
1993
- // completion / error emit pattern wrapped via _bridgeEmitDelivered).
1994
- try { emit(`${modelTag}${role} retry attempt=${attempt}`, { silent_to_agent: true }); } catch (emitErr) {
1995
- try { process.stderr.write(`[bridge] emit failed (retry path): job=${jobId} session=${activeSession.id} role=${role} attempt=${attempt} ${emitErr instanceof Error ? emitErr.message : String(emitErr)}\n`); } catch {}
1996
- }
1997
- // Re-arm the stall watchdog for the retry attempt. The
1998
- // previous watchdog is one-shot — it cleared its interval
1999
- // when it fired (or stopped via _api_call_with_interrupt's
2000
- // abort), and stays in the fired() state forever. Without
2001
- // re-arming, a second stall on the retry attempt would go
2002
- // undetected and the retry would hang against the bare
2003
- // provider timeout instead of being aborted again.
2004
- try { stallWatch.stop(); } catch { /* ignore */ }
2005
- stallWatch = _startStallWatch();
2006
- // Reset the iteration tracker so stall messages on this
2007
- // retry attempt report the current attempt's progress.
2008
- // Otherwise the askSession callback (iteration >
2009
- // lastIteration) would silently skip early iterations
2010
- // whose number is <= the previous attempt's final value.
2011
- lastIteration = 0;
2012
- }
2013
- return askSession(activeSession.id, prompt, args.context || null, (iteration, _calls) => {
2014
- if (typeof iteration === 'number' && iteration > lastIteration) lastIteration = iteration;
2015
- }, workerCwd, (args.prefetch && typeof args.prefetch === 'object') ? args.prefetch : undefined);
2016
- },
2017
- }));
2018
- // Footer (model/ctx/cache/out/loops/elapsed) dropped per user spec
2019
- // — caller (Lead) wants core content only. Per-turn usage stats are
2020
- // still available via session telemetry / bridge type=list.
2021
- let content;
2022
- if (result && typeof result.content === 'string' && result.content.length > 0) {
2023
- content = result.content;
2024
- } else {
2025
- content = _renderEmptyContentDiagnostic(result, activeSession);
2026
- }
2027
- const _extractedAnswer = _resolveFinalAnswer(content);
2028
- // Persist the full result BODY before emit so a torn-down
2029
- // transport / exit-255 between emit and disk-flush can't drop the
2030
- // answer — recoverPending replays this content instead of the
2031
- // generic Aborted boilerplate. Await the disk-flush tail; a
2032
- // persist failure is best-effort and must NOT block the emit.
2033
- try {
2034
- const _tail = setPendingResult(process.env.CLAUDE_PLUGIN_DATA, jobId, 'bridge', [role], content, false, _routingSessionId, _clientHostPid);
2035
- if (_tail && typeof _tail.then === 'function') await _tail;
2036
- } catch (e) { try { process.stderr.write(`[bridge] setPendingResult (success) failed: job=${jobId} role=${role} ${e?.message ?? e}\n`); } catch {} }
2037
- // Tear down the isolation worktree before the completion emit so a
2038
- // dirty (un-merged) leftover can be surfaced to the Lead. No
2039
- // auto-merge: a dirty worktree is kept and its path + changed-file
2040
- // count appended to the emit.
2041
- const _wtResult = _runWorktreeTeardown();
2042
- const _wtNote = (_wtResult && _wtResult.kept)
2043
- ? `\n\n⚠ isolation worktree kept (dirty, ${_wtResult.changedFiles} changed file${_wtResult.changedFiles === 1 ? '' : 's'}): ${_wtResult.path} — no auto-merge`
2044
- : '';
2045
- _delivered = (await _bridgeEmitDelivered(
2046
- emit,
2047
- `${modelTag}[${bridgeTag || role}] ${_extractedAnswer}${_wtNote}`,
2048
- { pathLabel: 'success path', jobId, sessionId: activeSession.id, role },
2049
- )) || _delivered;
2050
- await updateSessionStatus(activeSession.id, 'idle');
2051
- } catch (err) {
2052
- errorMessage = errText(err);
2053
- if (stallWatch.fired()) {
2054
- // The stall watchdog already attempted a user-facing message
2055
- // before aborting; whatever error bubbled up here (likely a
2056
- // SessionClosedError or provider-side abort surface) is just
2057
- // the unwind. Mark the session as errored and fall through.
2058
- // Only treat the pending record as delivered when the
2059
- // watchdog's notify actually landed (delivered()) — an async
2060
- // notify rejection routes to the fallback addPending and the
2061
- // pending record must stay replayable on next boot. Older
2062
- // builds shipping a watchdog without delivered() degrade
2063
- // safely: treat absence as "delivered" so behaviour matches
2064
- // the prior contract.
2065
- const _watchdogDelivered = typeof stallWatch.delivered === 'function'
2066
- ? (() => { try { return !!stallWatch.delivered(); } catch { return true; } })()
2067
- : true;
2068
- if (_watchdogDelivered) _delivered = true;
2069
- await updateSessionStatus(activeSession.id, 'error');
2070
- } else if (err instanceof SessionClosedError) {
2071
- // Prefer the structured enum on the error; fall back to
2072
- // regex-parsing the message for older call paths that might
2073
- // have constructed the error without the third arg.
2074
- let reason = err.reason || null;
2075
- if (!reason && typeof err.message === 'string') {
2076
- const m = err.message.match(/reason=([\w-]+)/);
2077
- if (m) reason = m[1];
2078
- }
2079
- if (shouldEmitBridgeCancellation(reason)) {
2080
- _delivered = (await _bridgeEmitDelivered(
2081
- emit,
2082
- reason ? `${bridgeTag || role} cancelled (reason=${reason})` : `${bridgeTag || role} cancelled`,
2083
- { pathLabel: 'cancel path', jobId, sessionId: activeSession.id, role, meta: _bridgeDispatchMeta(jobId, { error: true }) },
2084
- )) || _delivered;
2085
- } else {
2086
- // Intentionally-silent cancel (manual / request-abort /
2087
- // retry-replaced / idle-sweep): no notification by design.
2088
- // Mark delivered so the finally block clears the pending
2089
- // record — otherwise the next bootstrap would resurface a
2090
- // bogus "loss notification" for a user-requested cancel.
2091
- _delivered = true;
2092
- }
2093
- // Stop stall watchdog synchronously on close so the timer can't
2094
- // fire a spurious notify after the session is already closed.
2095
- try { stallWatch.stop(); } catch { /* ignore */ }
2096
- // Cancellation isn't an error; flip to idle so the next sweep
2097
- // pass can reclaim the file instead of leaving a 'running'
2098
- // zombie until the 24h tombstone window expires.
2099
- await updateSessionStatus(activeSession.id, 'idle');
2100
- } else if (err instanceof StreamStalledAbortError) {
2101
- const info = err.info || {};
2102
- const header = `⚠ stream stalled — ${info.staleSeconds}s no delta (stage: ${info.stage || 'unknown'})`;
2103
- // Persist the rendered error BODY before emit so a crash after
2104
- // the error is computed replays the real error, not boilerplate.
2105
- try {
2106
- const _tail = setPendingResult(process.env.CLAUDE_PLUGIN_DATA, jobId, 'bridge', [role], `${role} error: ${header}`, true, _routingSessionId, _clientHostPid);
2107
- if (_tail && typeof _tail.then === 'function') await _tail;
2108
- } catch (e) { try { process.stderr.write(`[bridge] setPendingResult (stall) failed: job=${jobId} role=${role} ${e?.message ?? e}\n`); } catch {} }
2109
- _delivered = (await _bridgeEmitDelivered(
2110
- emit,
2111
- `${bridgeTag || role} error: ${header}`,
2112
- { pathLabel: 'stall path', jobId, sessionId: activeSession.id, role, meta: _bridgeDispatchMeta(jobId, { error: true }) },
2113
- )) || _delivered;
2114
- await updateSessionStatus(activeSession.id, 'error');
2115
- } else {
2116
- // Persist the error BODY before emit so a crash after the error
2117
- // is computed replays the real error, not Aborted boilerplate.
2118
- try {
2119
- const _tail = setPendingResult(process.env.CLAUDE_PLUGIN_DATA, jobId, 'bridge', [role], `${role} error: ${errorMessage}`, true, _routingSessionId, _clientHostPid);
2120
- if (_tail && typeof _tail.then === 'function') await _tail;
2121
- } catch (e) { try { process.stderr.write(`[bridge] setPendingResult (error) failed: job=${jobId} role=${role} ${e?.message ?? e}\n`); } catch {} }
2122
- _delivered = (await _bridgeEmitDelivered(
2123
- emit,
2124
- `${bridgeTag || role} error: ${errorMessage}`,
2125
- { pathLabel: 'error path', jobId, sessionId: activeSession.id, role, meta: _bridgeDispatchMeta(jobId, { error: true }) },
2126
- )) || _delivered;
2127
- await updateSessionStatus(activeSession.id, 'error');
2128
- }
2129
- } finally {
2130
- // Ensure the isolation worktree is torn down on every exit path
2131
- // (error / cancel / stall). Idempotent: a no-op when the success
2132
- // path already ran it. A dirty worktree is kept (logged in the
2133
- // teardown helper); only a clean one is removed + branch deleted.
2134
- try { _runWorktreeTeardown(); } catch { /* ignore */ }
2135
- // Do NOT stop stallWatch here unconditionally — stopping it before
2136
- // the 1h terminal-reap window prevents terminal-stale sessions
2137
- // from ever being hidden. Instead, schedule a deferred hide so
2138
- // listSessions() stops returning the completed bridge session after
2139
- // 1h. stallWatch.stop() is kept for the explicit close path only
2140
- // (handled inside the watchdog itself on abort).
2141
- // Schedule via _scheduleBridgeReap so the timer handle is recorded
2142
- // per-session in _sessionReapTimers — a later `bridge type=send`
2143
- // resume can then cancel/reschedule it (see send branch) instead of
2144
- // being reaped (hidden + tag-deleted) mid-resume.
2145
- try { _scheduleBridgeReap(activeSession.id); } catch { /* ignore */ }
2146
- // Detach request-abort logging listener — IIFE has settled, the
2147
- // MCP request has nothing left to log against. Harmless if abort
2148
- // already fired (listener was registered { once: true }).
2149
- try { _detachRequestAbortLog(); } catch { /* ignore */ }
2150
- // Remove persist record only after emit confirmed delivered.
2151
- // If emit failed (channel down, owner restart), leave the pending
2152
- // record intact so the supervisor can resurface or retry the job.
2153
- if (_delivered) {
2154
- try { removePending(process.env.CLAUDE_PLUGIN_DATA, jobId); } catch {}
2155
- } else {
2156
- try { process.stderr.write(`[bridge] keeping pending record (emit not delivered): job=${jobId} session=${activeSession.id} role=${role}\n`); } catch {}
2157
- }
2158
- try { _jobSessionRegistry.delete(jobId); } catch {}
2159
- // Clean up all session IDs registered across retry attempts.
2160
- try { for (const sid of _allRetrySessionIds) _sessionJobRegistry.delete(sid); } catch {}
2161
- try { _jobCancelledTombstones.delete(jobId); } catch {}
2162
- }
2163
- }))().catch(async (err) => {
2164
- // Order: emit FIRST, then remove pending only if emit succeeded.
2165
- // Previously this handler removed the pending record before
2166
- // attempting the crash emit — if the emit then failed (channel
2167
- // down, owner restart) the loss notification was silently
2168
- // dropped and the next bootstrap had no record to resurface.
2169
- // Keep genuine emit-failure replayable by keeping the pending
2170
- // record intact when emit throws.
2171
- const msg = err instanceof Error ? (err.stack || err.message) : String(err);
2172
- const crashSessionId = activeSession?.id ?? session.id;
2173
- try {
2174
- process.stderr.write(`[bridge] detached runner unhandled: session=${crashSessionId} role=${role} job=${jobId} ${msg}\n`);
2175
- } catch {}
2176
- // Best-effort isolation-worktree teardown for the crash path (the
2177
- // inner finally may not have run if the override wrapper threw).
2178
- // Idempotent at the git level: removing an already-removed worktree
2179
- // fails harmlessly and is logged by the helper.
2180
- try { if (_workerWorktree) _teardownWorkerWorktree(_workerWorktree); } catch { /* ignore */ }
2181
- // Notify via emit so the crash surfaces to the Lead / channel instead
2182
- // of silently disappearing. Uses the same channel as normal error
2183
- // emissions so the Lead can act on it.
2184
- const _crashDelivered = await _bridgeEmitDelivered(
2185
- emit,
2186
- `${bridgeTag || role} crash: ${errText(err)}`,
2187
- { pathLabel: 'crash path', jobId, sessionId: crashSessionId, role, meta: _bridgeDispatchMeta(jobId, { error: true }) },
2188
- );
2189
- if (_crashDelivered) {
2190
- try { removePending(process.env.CLAUDE_PLUGIN_DATA, jobId); } catch {}
2191
- } else {
2192
- try { process.stderr.write(`[bridge] keeping pending record (crash emit not delivered): job=${jobId} session=${crashSessionId} role=${role}\n`); } catch {}
2193
- }
2194
- try { _jobSessionRegistry.delete(jobId); } catch {}
2195
- try { for (const sid of _allRetrySessionIds) _sessionJobRegistry.delete(sid); } catch {}
2196
- try { _tagSessionRegistry.delete(bridgeTag); } catch {}
2197
- try { for (const sid of _allRetrySessionIds) _cancelBridgeReap(sid); } catch {}
2198
- try { _jobCancelledTombstones.delete(jobId); } catch {}
2199
- try { await updateSessionStatus(crashSessionId, 'error'); } catch {}
2200
- try { closeSession(crashSessionId, 'runner-crash'); } catch {}
2201
- });
2202
-
2203
- return ok({
2204
- jobId,
2205
- sessionId: activeSession.id,
2206
- tag: bridgeTag,
2207
- role,
2208
- model: modelLabel,
2209
- detached: true,
2210
- respawned: !!_bridgeColdRespawn,
2211
- // Surface brief size to dispatcher (Lead) so over-cap briefs are
2212
- // visible without needing to inspect bridge stderr.
2213
- ...(_briefWordCount > 0 ? { briefWords: _briefWordCount } : {}),
2214
- ...(_briefWordCount > 150
2215
- ? { briefWarning: `brief is ${_briefWordCount} words (cap=150)` }
2216
- : {}),
2217
- });
2218
- }
2219
-
2220
- default:
2221
- return fail(`Unknown tool: ${name}`);
2222
- }
2223
- } catch (err) {
2224
- return fail(err);
2225
- }
2226
- }
2227
-
2228
- export async function start() { /* noop — standalone mode uses main() */ }
2229
- export async function stop() { await disconnectAll(); }