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
@@ -0,0 +1,2269 @@
1
+ import { classifyResultKind } from './result-classification.mjs';
2
+ import { executeMcpTool, isMcpTool, mcpToolHasField } from '../mcp/client.mjs';
3
+ import { canonicalizeBuiltinToolName, executeBuiltinTool, formatUnknownBuiltinToolMessage, isBuiltinTool } from '../tools/builtin.mjs';
4
+ import { executeBashSessionTool } from '../tools/bash-session.mjs';
5
+ import { executePatchTool } from '../tools/patch.mjs';
6
+ import { executeCodeGraphTool, isCodeGraphTool } from '../tools/code-graph.mjs';
7
+ import { executeInternalTool, isInternalTool } from '../internal-tools.mjs';
8
+ import { collectSkillsCached, loadSkillContent } from '../context/collect.mjs';
9
+ import { traceBridgeLoop, traceBridgeTool, traceBridgeToolFailure, traceBridgeCompact, estimateProviderPayloadBytes, messagePrefixHash } from '../bridge-trace.mjs';
10
+ import { markSessionToolCall, updateSessionStage, SessionClosedError, getSessionAbortSignal, enqueuePendingMessage } from './manager.mjs';
11
+ import { estimateMessagesTokens, estimateRequestReserveTokens } from './context-utils.mjs';
12
+ import {
13
+ compactMessages,
14
+ pruneToolOutputs,
15
+ semanticCompactMessages,
16
+ compactActiveTurn,
17
+ SUMMARY_PREFIX,
18
+ DEFAULT_COMPACTION_BUFFER_TOKENS,
19
+ DEFAULT_COMPACTION_BUFFER_RATIO,
20
+ DEFAULT_COMPACTION_KEEP_TOKENS,
21
+ compactionBufferTokensForBoundary,
22
+ normalizeCompactionBufferRatio,
23
+ } from './compact.mjs';
24
+ import { isContextOverflowError } from '../providers/retry-classifier.mjs';
25
+ import { classifyBashFileLookupCommand, classifyBridgeWorkerGitMutationCommand, stripSoftWarns } from '../tool-loop-guard.mjs';
26
+ import { maybeOffloadToolResult } from './tool-result-offload.mjs';
27
+ import { tryReadCached, setReadCached, invalidatePathForSession, markPostEdit, consumePostEditMark, clearReadDedupSession, extractTouchedPathsFromPatch, tryScopedToolCached, setScopedToolCached, clearScopedToolsForSession, clearScopedToolsForSessionPaths, invalidatePrefetchCache } from './read-dedup.mjs';
28
+ import { createScopedCacheOutcome } from './cache/scoped-cache-outcome.mjs';
29
+ import { createHash } from 'crypto';
30
+
31
+ // Tool-name classification for cross-turn read dedup.
32
+ // Strips the MCP prefix so direct calls and MCP-wrapped calls share the
33
+ // same cache.
34
+ function _stripMcpPrefix(name) {
35
+ return typeof name === 'string' && name.startsWith(MCP_TOOL_PREFIX)
36
+ ? name.slice(MCP_TOOL_PREFIX.length) : name;
37
+ }
38
+ function _isReadTool(name) {
39
+ return _stripMcpPrefix(name) === 'read';
40
+ }
41
+ function _isMutationTool(name) {
42
+ const n = _stripMcpPrefix(name);
43
+ return n === 'apply_patch';
44
+ }
45
+ const SCOPED_CACHEABLE_TOOLS = new Set([
46
+ 'code_graph',
47
+ 'grep',
48
+ 'list',
49
+ 'glob',
50
+ ]);
51
+ function _isScopedCacheableTool(name) {
52
+ const n = _stripMcpPrefix(name);
53
+ return SCOPED_CACHEABLE_TOOLS.has(n);
54
+ }
55
+ function _isShellTool(name) {
56
+ const n = _stripMcpPrefix(name);
57
+ return n === 'shell' || n === 'bash_session';
58
+ }
59
+
60
+ // classifyResultKind is imported from result-classification.mjs at the top of
61
+ // this file; import it from there directly rather than via this module.
62
+
63
+ // Canonical signature for intra-turn duplicate detection. Sorting keys
64
+ // produces a stable hash regardless of arg-object key order. Anything
65
+ // non-serializable falls back to String(args) — still deterministic for
66
+ // the model's typical structured-arg shape.
67
+ function _canonicalArgs(args) {
68
+ if (args == null || typeof args !== 'object') {
69
+ try { return JSON.stringify(args); } catch { return String(args); }
70
+ }
71
+ try {
72
+ const keys = Object.keys(args).sort();
73
+ const sorted = {};
74
+ for (const k of keys) sorted[k] = args[k];
75
+ return JSON.stringify(sorted);
76
+ } catch { return String(args); }
77
+ }
78
+ function _intraTurnSig(name, args) {
79
+ return createHash('sha256').update(`${name}:${_canonicalArgs(args)}`).digest('hex').slice(0, 16);
80
+ }
81
+
82
+ // Shared pre-dispatch deny — single source of truth for role/scope/permission
83
+ // rejects. Called by BOTH the eager dispatch path (startEagerTool) and the
84
+ // serial dispatch path (executeTool body). Returns null when the call is
85
+ // allowed to proceed; otherwise returns the Error string the serial path
86
+ // would emit. The eager caller ignores the message body and just treats
87
+ // non-null as "do not start eager".
88
+ //
89
+ // Predicates are kept in the same order as the legacy serial branch so a
90
+ // bridge-owned control-plane tool fails on _bridgeOwned+_controlPlaneTool
91
+ // FIRST (not on permission/wrapper checks) — matches the prior wording.
92
+ // Bridge workers are sandboxed to code/research tools. They must never reach
93
+ // owner/host control surfaces: session management, the ENTIRE channels module
94
+ // (Discord messaging, schedules, webhook/config, channel-bridge toggle,
95
+ // command injection), or host input injection. Explicit name list (no imports)
96
+ // keeps this hot-path gate dependency-free; add new owner/channel tools here.
97
+ const WORKER_DENIED_TOOLS = new Set([
98
+ // session control-plane — unified into the single `bridge` tool
99
+ // (type=spawn|send|close|list). Denying the one name blocks all worker
100
+ // session control. Legacy names kept for defense-in-depth against any
101
+ // stale catalog entry that still advertises them.
102
+ 'bridge', 'close_session', 'list_sessions', 'create_session',
103
+ // channels module (owner/Discord-facing)
104
+ 'reply', 'react', 'edit_message', 'download_attachment', 'fetch',
105
+ 'schedule_status', 'trigger_schedule', 'schedule_control',
106
+ 'activate_channel_bridge', 'reload_config', 'inject_command',
107
+ // host input injection
108
+ 'inject_input',
109
+ ]);
110
+ function _preDispatchDeny(call, toolKind, sessionRef) {
111
+ const name = call?.name;
112
+ if (typeof name !== 'string' || !name) return null;
113
+ const _bridgeOwned = sessionRef?.scope?.startsWith?.('bridge:') || sessionRef?.owner === 'bridge';
114
+ const _controlPlaneTool = WORKER_DENIED_TOOLS.has(name);
115
+ if (_bridgeOwned && _controlPlaneTool) {
116
+ return `Error: control-plane tool "${name}" is Lead-only and not available to bridge agents.`;
117
+ }
118
+ const noToolRole = sessionRef?.role === 'cycle1-agent' || sessionRef?.role === 'cycle2-agent';
119
+ if (noToolRole) {
120
+ return `Error: tool "${name}" is not available in role "${sessionRef.role}". Re-emit the answer as pipe-separated text per the role's output format (first character a digit, NO tool_use blocks, NO JSON, NO prose, NO apology).`;
121
+ }
122
+ if (isBlockedHiddenWrapperCall(name, sessionRef)) {
123
+ return `Error: tool "${name}" is the wrapper your role (${sessionRef?.role || 'hidden'}) backs. Calling it would spawn another hidden agent of the same kind — use direct read/grep/glob/code_graph instead.`;
124
+ }
125
+ const effectivePermission = effectiveToolPermission(sessionRef);
126
+ const permissionBlocked = isBlockedByPermission(name, toolKind, effectivePermission);
127
+ if (permissionBlocked && effectivePermission === 'mcp') {
128
+ return `Error: tool "${name}" is not available on this session (permission=mcp). Use MCP/internal retrieval tools only.`;
129
+ }
130
+ if (permissionBlocked && effectivePermission === 'read') {
131
+ return `Error: tool "${name}" is not available on this session (permission=read). Use Mixdog MCP read/grep/glob/recall/search/explore instead.`;
132
+ }
133
+ if (permissionBlocked && effectivePermission && typeof effectivePermission === 'object') {
134
+ return `Error: tool "${name}" is not permitted on this session by the role's allow/deny permission policy.`;
135
+ }
136
+ return null;
137
+ }
138
+ /** Exported for smoke tests — same runtime deny as the agent loop. */
139
+ export function preDispatchDenyForSession(sessionRef, call, toolKind = 'builtin') {
140
+ return _preDispatchDeny(call, toolKind, sessionRef);
141
+ }
142
+ import { compressToolResult, recordToolBatch } from '../tools/result-compression.mjs';
143
+
144
+
145
+ import { isHiddenRole } from '../internal-roles.mjs';
146
+ import { createRequire } from 'module';
147
+ import { readFileSync as _readFileSync } from 'fs';
148
+ import { fileURLToPath } from 'url';
149
+ import { dirname, resolve as resolvePath, isAbsolute } from 'path';
150
+ // Load the CJS permission evaluator. The hooks/ directory lives above
151
+ // src/runtime/agent/orchestrator/session/, so we walk up from __dirname.
152
+ const _require = createRequire(import.meta.url);
153
+ const _hooksLib = resolvePath(dirname(fileURLToPath(import.meta.url)), '../../../../hooks/lib/permission-evaluator.cjs');
154
+ const { evaluatePermission: _evaluatePermission } = _require(_hooksLib);
155
+ const MCP_TOOL_PREFIX = 'mcp__plugin_mixdog_mixdog__';
156
+ const COMPACT_SAFETY_PERCENT = 1.00;
157
+ const COMPACT_BUFFER_MAX_WINDOW_FRACTION = 0.25;
158
+ // Stricter budget used for the one-shot retry after a provider rejects a send
159
+ // with a context-window-exceeded error. 0.60×contextWindow forces older
160
+ // non-system history into a tighter compact summary when the pre-send estimate
161
+ // under-counted provider-side bytes (tool schemas, framing,
162
+ // provider-internal token accounting). Used exactly once per send; see the
163
+ // retry block around provider.send below.
164
+ const OVERFLOW_RETRY_COMPACT_PERCENT = 0.60;
165
+
166
+ function estimateMessagesTokensSafe(messages) {
167
+ try { return estimateMessagesTokens(messages); }
168
+ catch { return null; }
169
+ }
170
+
171
+ function steeringContentText(content) {
172
+ if (typeof content === 'string') return content;
173
+ if (Array.isArray(content)) {
174
+ return content.map((part) => {
175
+ if (typeof part === 'string') return part;
176
+ if (part?.type === 'text') return part.text || '';
177
+ if (part?.type === 'image') return '[Image]';
178
+ return part?.text || '';
179
+ }).filter(Boolean).join('\n');
180
+ }
181
+ return String(content ?? '');
182
+ }
183
+
184
+ function normalizeSteeringEntry(entry) {
185
+ if (typeof entry === 'string') {
186
+ const text = entry.trim();
187
+ return text ? { content: text, text } : null;
188
+ }
189
+ if (!entry || typeof entry !== 'object') return null;
190
+ const content = Object.prototype.hasOwnProperty.call(entry, 'content') ? entry.content : entry;
191
+ const text = typeof entry.text === 'string' ? entry.text.trim() : steeringContentText(content).trim();
192
+ if (Array.isArray(content)) return content.length > 0 ? { content, text } : null;
193
+ if (typeof content === 'string') {
194
+ const value = content.trim();
195
+ return value ? { content: value, text: text || value } : null;
196
+ }
197
+ const fallback = steeringContentText(content).trim();
198
+ return fallback ? { content: fallback, text: text || fallback } : null;
199
+ }
200
+
201
+ function mergeSteeringEntries(entries) {
202
+ const normalized = (Array.isArray(entries) ? entries : [])
203
+ .map(normalizeSteeringEntry)
204
+ .filter(Boolean);
205
+ if (normalized.length === 0) return null;
206
+ const displayText = normalized.map((entry) => entry.text || steeringContentText(entry.content))
207
+ .filter((text) => String(text || '').trim())
208
+ .join('\n');
209
+ if (normalized.every((entry) => typeof entry.content === 'string')) {
210
+ return {
211
+ content: normalized.map((entry) => entry.content).filter(Boolean).join('\n'),
212
+ text: displayText,
213
+ count: normalized.length,
214
+ };
215
+ }
216
+ const parts = [];
217
+ for (const entry of normalized) {
218
+ if (typeof entry.content === 'string') {
219
+ if (entry.content.trim()) parts.push({ type: 'text', text: entry.content });
220
+ } else if (Array.isArray(entry.content)) {
221
+ parts.push(...entry.content);
222
+ } else {
223
+ const text = steeringContentText(entry.content);
224
+ if (text.trim()) parts.push({ type: 'text', text });
225
+ }
226
+ parts.push({ type: 'text', text: '\n' });
227
+ }
228
+ while (parts.length && parts[parts.length - 1]?.type === 'text' && parts[parts.length - 1]?.text === '\n') parts.pop();
229
+ return { content: parts, text: displayText || steeringContentText(parts), count: normalized.length };
230
+ }
231
+
232
+ class BridgeContextOverflowError extends Error {
233
+ constructor({ stage, sessionId, provider, model, contextWindow, budgetTokens, reserveTokens, messageTokensEst }, cause) {
234
+ const target = [provider, model].filter(Boolean).join('/') || 'target model';
235
+ const causeMsg = cause && cause.message ? `: ${cause.message}` : '';
236
+ super(
237
+ `bridge context overflow (${target}, stage=${stage || 'compact'}): ` +
238
+ `latest turn cannot fit target context budget=${budgetTokens ?? 'unknown'} ` +
239
+ `reserve=${reserveTokens ?? 'unknown'} contextWindow=${contextWindow ?? 'unknown'} ` +
240
+ `messageTokensEst=${messageTokensEst ?? 'unknown'}${causeMsg}`,
241
+ );
242
+ this.name = 'BridgeContextOverflowError';
243
+ this.code = 'BRIDGE_CONTEXT_OVERFLOW';
244
+ this.sessionId = sessionId || null;
245
+ this.provider = provider || null;
246
+ this.model = model || null;
247
+ this.contextWindow = contextWindow ?? null;
248
+ this.budgetTokens = budgetTokens ?? null;
249
+ this.reserveTokens = reserveTokens ?? null;
250
+ this.messageTokensEst = messageTokensEst ?? null;
251
+ if (cause) this.cause = cause;
252
+ }
253
+ }
254
+
255
+ function bridgeContextOverflowError({ stage, sessionId, sessionRef, model, budgetTokens, reserveTokens, messageTokensEst }, cause) {
256
+ return new BridgeContextOverflowError({
257
+ stage,
258
+ sessionId,
259
+ provider: sessionRef?.provider || null,
260
+ model: sessionRef?.model || model || null,
261
+ contextWindow: sessionRef?.contextWindow ?? null,
262
+ budgetTokens,
263
+ reserveTokens,
264
+ messageTokensEst,
265
+ }, cause);
266
+ }
267
+
268
+ // Cache-hit results always inline the cached body. The earlier size-gated
269
+ // `[cache-hit-ref]` branch confused bridge agents whose context did not
270
+ // contain the referenced prior tool_result, triggering shell-cat detours.
271
+ // Hard iteration ceiling for every agent loop. Reset to 0 whenever the
272
+ // transcript is compacted (see the trim block below): a long task that keeps
273
+ // compacting can proceed past this count, while a tight NON-compacting loop
274
+ // still stops here and returns the accumulated transcript.
275
+ const MAX_LOOP_ITERATIONS = 200;
276
+ // Consecutive identical-AND-failing tool calls (same name+args, error result)
277
+ // tolerated across iterations before the loop refuses to re-execute and steers
278
+ // the model to change approach. Distinct from the hard iteration cap above:
279
+ // this catches tight deterministic-failure loops (e.g. a command that errors
280
+ // the same way every time) far earlier than 100 iterations.
281
+ const REPEAT_FAIL_LIMIT = 3;
282
+ const _HIDDEN_ROLES_JSON = resolvePath(dirname(fileURLToPath(import.meta.url)), '../../../../defaults/hidden-roles.json');
283
+ let _hiddenRolesCache = null;
284
+ function _getHiddenRoles() {
285
+ if (_hiddenRolesCache) return _hiddenRolesCache;
286
+ try {
287
+ _hiddenRolesCache = JSON.parse(_readFileSync(_HIDDEN_ROLES_JSON, 'utf8'));
288
+ } catch { _hiddenRolesCache = { roles: [] }; }
289
+ return _hiddenRolesCache;
290
+ }
291
+ // Transcript pairing guard. Anthropic 400-rejects when an assistant message
292
+ // ends with tool_use blocks and the next message isn't tool results for
293
+ // those exact ids. abort/timeout/error race in the loop body can leave a
294
+ // dangling assistant tool_use at the tail (e.g. the structure_probe loop
295
+ // running 12 deep then aborting between push-assistant and push-tool).
296
+ // Strip any trailing assistant tool_use that has no matching tool result
297
+ // so provider.send sees a valid transcript instead of leaking the 400 to
298
+ // the user. Repair runs every iteration but is a no-op on healthy paths.
299
+ function _ensureTranscriptPairing(msgs, sessionId) {
300
+ // Walk backwards to find the last assistant message that emitted
301
+ // tool_use, then validate that every id has a matching tool result
302
+ // inside the CONTIGUOUS tool-message block immediately following it.
303
+ // Earlier guard splice'd the entire tail — which silently deleted any
304
+ // user prompt appended after the dangling assistant by manager.mjs:
305
+ // when the guard fired with shape
306
+ // [..., assistant{a,b}, tool{a}, user{new prompt}]
307
+ // the splice removed user{new prompt} along with the orphan suffix.
308
+ // Fix: remove only assistant + the contiguous tool block; preserve
309
+ // anything past it (user / system / next assistant) untouched.
310
+ let popped = 0;
311
+ while (msgs.length > 0) {
312
+ let lastAssistantIdx = -1;
313
+ for (let i = msgs.length - 1; i >= 0; i--) {
314
+ const m = msgs[i];
315
+ if (m?.role === 'assistant' && Array.isArray(m.toolCalls) && m.toolCalls.length > 0) {
316
+ lastAssistantIdx = i;
317
+ break;
318
+ }
319
+ }
320
+ if (lastAssistantIdx === -1) break;
321
+ // Collect the contiguous tool messages directly after this assistant.
322
+ // Anything past that block is unrelated (next user prompt, system
323
+ // marker, etc.) and must survive the repair.
324
+ let toolBlockEnd = lastAssistantIdx + 1;
325
+ while (toolBlockEnd < msgs.length && msgs[toolBlockEnd]?.role === 'tool') {
326
+ toolBlockEnd += 1;
327
+ }
328
+ const toolBlock = msgs.slice(lastAssistantIdx + 1, toolBlockEnd);
329
+ const ids = msgs[lastAssistantIdx].toolCalls.map(c => c.id);
330
+ const matched = ids.every(id => toolBlock.some(m => m.toolCallId === id));
331
+ if (matched) break;
332
+ const removed = toolBlockEnd - lastAssistantIdx;
333
+ msgs.splice(lastAssistantIdx, removed);
334
+ popped += removed;
335
+ }
336
+ // Second sweep — catch dangling tool results that survived the
337
+ // contiguous-block splice. Anthropic strict spec requires every
338
+ // tool result to sit in a contiguous block right after the
339
+ // assistant whose toolCalls produced it; a `[..., assistant{a,b},
340
+ // tool{a}, user, tool{b}]` shape leaves tool{b} orphaned even
341
+ // after assistant + tool{a} are repaired by the loop above.
342
+ // Walk back from each tool message to the nearest non-tool
343
+ // ancestor; if it is not an assistant whose toolCalls include
344
+ // this id, drop the orphan.
345
+ for (let i = msgs.length - 1; i >= 0; i--) {
346
+ const m = msgs[i];
347
+ if (m?.role !== 'tool') continue;
348
+ if (!m.toolCallId) {
349
+ msgs.splice(i, 1);
350
+ popped += 1;
351
+ continue;
352
+ }
353
+ let prevIdx = i - 1;
354
+ while (prevIdx >= 0 && msgs[prevIdx]?.role === 'tool') prevIdx--;
355
+ const anchor = prevIdx >= 0 ? msgs[prevIdx] : null;
356
+ const anchorOk = anchor?.role === 'assistant'
357
+ && Array.isArray(anchor.toolCalls)
358
+ && anchor.toolCalls.some(c => c.id === m.toolCallId);
359
+ if (!anchorOk) {
360
+ msgs.splice(i, 1);
361
+ popped += 1;
362
+ }
363
+ }
364
+ if (popped > 0 && sessionId) {
365
+ try { process.stderr.write(`[transcript-repair] sess=${sessionId} popped=${popped} dangling assistant tool_use\n`); } catch {}
366
+ }
367
+ }
368
+
369
+ // Write-class tools that a permission=read session must not execute. The
370
+ // schema still advertises them to keep one unified shard; this runtime set
371
+ // is the fail-safe reject at call time.
372
+ const READ_BLOCKED_TOOLS = new Set([
373
+ 'shell', 'bash_session',
374
+ 'apply_patch',
375
+ ]);
376
+ const MCP_ONLY_ALLOWED_KINDS = new Set(['mcp', 'internal', 'skill']);
377
+ // Wrappers that hidden retrieval roles back. Hidden roles MUST NOT call
378
+ // these or they spawn another hidden agent of the same kind — nested chain
379
+ // + token burn. Block at call time; the role's rule prompt also says so.
380
+ const RETRIEVAL_WRAPPERS = new Set(['recall', 'search', 'explore']);
381
+ // Hidden roles that may call specific retrieval wrappers. Default policy
382
+ // blocks all hidden→wrapper calls; roles listed here have a documented
383
+ // need:
384
+ // - scheduler-task / webhook-handler: state-changing agents whose
385
+ // tasks routinely require both reach-back into past context
386
+ // (`recall`) and fresh external info (`search`).
387
+ const HIDDEN_ROLE_WRAPPER_ALLOWLIST = {
388
+ 'scheduler-task': new Set(['recall', 'search']),
389
+ 'webhook-handler': new Set(['recall', 'search']),
390
+ };
391
+ // Eager-dispatch: tools with readOnlyHint:true in their declaration are safe
392
+ // to execute during SSE parsing so tool work overlaps with the rest of the
393
+ // stream. Writes, bash, MCP and skills stay serial after send() returns.
394
+ function isEagerDispatchable(name, tools) {
395
+ if (!Array.isArray(tools)) return false;
396
+ const def = tools.find(t => t?.name === name);
397
+ return def?.annotations?.readOnlyHint === true;
398
+ }
399
+ // ── Bridge-worker permission enforcement ──────────────────────────────────────
400
+ // Mirrors the PreToolUse hook evaluation for tool calls that originate inside a
401
+ // bridge worker session. Worker dispatch previously bypassed the hook pipeline
402
+ // entirely; this guard closes that gap by running the same evaluator inline.
403
+ //
404
+ // `ask` is treated as deny here — forwarding `ask` decisions to the channel
405
+ // UI approval flow needs bidirectional prompt plumbing that does not exist.
406
+ function _checkWorkerPermission(toolName, toolInput, sessionRef) {
407
+ const bareToolName = _stripMcpPrefix(toolName);
408
+ if (sessionRef?.owner === 'bridge' && bareToolName === 'shell') {
409
+ const cmdClass = classifyBashFileLookupCommand(toolInput?.command);
410
+ if (cmdClass) {
411
+ return `Error: bridge worker shell file lookup blocked (${cmdClass}). Use Mixdog MCP read/grep/glob/list directly; shell is only for build/test/run/git-style commands.`;
412
+ }
413
+ const gitClass = classifyBridgeWorkerGitMutationCommand(toolInput?.command);
414
+ if (gitClass) {
415
+ return `Error: bridge worker git operation blocked (${gitClass}). Git operations are deferred to Lead.`;
416
+ }
417
+ }
418
+ // Even when no explicit permissionMode is propagated to the worker, run
419
+ // the evaluator under the most restrictive baseline ('default') so the
420
+ // bypass-proof hard-deny patterns (UNC paths, /etc, C:/Windows, etc.)
421
+ // and the user's settings.json deny rules still apply. Previously a
422
+ // missing permissionMode short-circuited to null and the worker
423
+ // ran ungated — a model could dispatch a bridge to read or write
424
+ // protected paths even when the same call would have been denied for
425
+ // the parent. Callers that genuinely need bypassPermissions can still
426
+ // forward it explicitly via session-builder; this only closes the
427
+ // silent default-to-bypass path.
428
+ const permissionMode = sessionRef?.permissionMode || 'default';
429
+ // Prefix bare mixdog tool names so the evaluator path-logic handles them correctly.
430
+ const fullName = toolName.startsWith(MCP_TOOL_PREFIX) || toolName.startsWith('mcp__')
431
+ ? toolName
432
+ : `${MCP_TOOL_PREFIX}${toolName}`;
433
+ const projectDir = sessionRef?.cwd || undefined;
434
+ const userCwd = sessionRef?.cwd || undefined;
435
+ try {
436
+ const { decision, reason } = _evaluatePermission({
437
+ toolName: fullName,
438
+ toolInput: toolInput || {},
439
+ permissionMode,
440
+ projectDir,
441
+ userCwd,
442
+ });
443
+ if (decision === 'deny' || decision === 'ask') {
444
+ return `Error: tool "${toolName}" blocked by permission evaluator (decision=${decision}): ${reason}`;
445
+ }
446
+ } catch (err) {
447
+ // Evaluator errors must not crash the loop — log and allow.
448
+ try { process.stderr.write(`[permission-evaluator] error: ${err?.message}\n`); } catch {}
449
+ }
450
+ return null;
451
+ }
452
+ function effectiveToolPermission(sessionRef) {
453
+ return sessionRef?.toolPermission || sessionRef?.permission || null;
454
+ }
455
+ function isBlockedByPermission(toolName, toolKind, permission) {
456
+ if (permission === 'mcp') return !MCP_ONLY_ALLOWED_KINDS.has(toolKind);
457
+ if (permission === 'read') return READ_BLOCKED_TOOLS.has(toolName);
458
+ // Object-form {allow,deny} permission (role template / profile). The
459
+ // schema-level intersection in createSession only narrows the ADVERTISED
460
+ // tool list; it is not a runtime execution boundary. Enforce the same
461
+ // allow/deny here as the fail-safe so a tool call for a non-advertised
462
+ // (denied / out-of-allow) tool is rejected at dispatch time, matching
463
+ // the string-form ('read'/'mcp') guards. Names are compared bare +
464
+ // lowercased to mirror createSession's allow/deny set construction.
465
+ if (permission && typeof permission === 'object') {
466
+ const name = String(_stripMcpPrefix(toolName) || '').toLowerCase();
467
+ const deny = Array.isArray(permission.deny) && permission.deny.length > 0
468
+ ? permission.deny.map(n => String(n).toLowerCase())
469
+ : null;
470
+ if (deny && deny.includes(name)) return true;
471
+ const allow = Array.isArray(permission.allow) && permission.allow.length > 0
472
+ ? permission.allow.map(n => String(n).toLowerCase())
473
+ : null;
474
+ if (allow && !allow.includes(name)) return true;
475
+ return false;
476
+ }
477
+ return false;
478
+ }
479
+ function isBlockedHiddenWrapperCall(toolName, sessionRef) {
480
+ if (!RETRIEVAL_WRAPPERS.has(toolName)) return false;
481
+ if (sessionRef?.owner !== 'bridge') return false;
482
+ if (!isHiddenRole(sessionRef?.role)) return false;
483
+ const allow = HIDDEN_ROLE_WRAPPER_ALLOWLIST[sessionRef.role];
484
+ if (allow && allow.has(toolName)) return false;
485
+ return true;
486
+ }
487
+ function messagesArrayChanged(before, after) {
488
+ if (!Array.isArray(before) || !Array.isArray(after)) return before !== after;
489
+ if (before.length !== after.length) return true;
490
+ for (let i = 0; i < before.length; i += 1) {
491
+ if (before[i] !== after[i]) return true;
492
+ }
493
+ return false;
494
+ }
495
+ function normalizeUsage(usage) {
496
+ if (!usage) return null;
497
+ const costUsd = Number(usage.costUsd);
498
+ return {
499
+ inputTokens: usage.inputTokens || 0,
500
+ outputTokens: usage.outputTokens || 0,
501
+ cachedTokens: usage.cachedTokens || 0,
502
+ cacheWriteTokens: usage.cacheWriteTokens || 0,
503
+ promptTokens: usage.promptTokens || 0,
504
+ ...(Number.isFinite(costUsd) ? { costUsd } : {}),
505
+ raw: usage.raw,
506
+ };
507
+ }
508
+ function addUsage(total, usage) {
509
+ const delta = normalizeUsage(usage);
510
+ if (!delta) return total;
511
+ if (!total) return { ...delta };
512
+ const next = {
513
+ ...total,
514
+ inputTokens: (total.inputTokens || 0) + delta.inputTokens,
515
+ outputTokens: (total.outputTokens || 0) + delta.outputTokens,
516
+ cachedTokens: (total.cachedTokens || 0) + delta.cachedTokens,
517
+ cacheWriteTokens: (total.cacheWriteTokens || 0) + delta.cacheWriteTokens,
518
+ promptTokens: (total.promptTokens || 0) + delta.promptTokens,
519
+ };
520
+ if (delta.costUsd != null || total.costUsd != null) {
521
+ next.costUsd = (total.costUsd || 0) + (delta.costUsd || 0);
522
+ }
523
+ return next;
524
+ }
525
+ function splitMessagesForRemoteCompact(messages) {
526
+ if (!Array.isArray(messages)) return null;
527
+ const system = messages.filter(m => m?.role === 'system');
528
+ const nonSystem = messages.filter(m => m?.role !== 'system');
529
+ let turnStart = -1;
530
+ for (let i = nonSystem.length - 1; i >= 0; i -= 1) {
531
+ if (nonSystem[i]?.role === 'user') {
532
+ turnStart = i;
533
+ break;
534
+ }
535
+ }
536
+ if (turnStart <= 0) return null;
537
+ const oldHistory = nonSystem.slice(0, turnStart);
538
+ if (!oldHistory.length) return null;
539
+ return [...system, ...oldHistory];
540
+ }
541
+ function markRemoteCompactFallback(messages, providerName) {
542
+ if (!Array.isArray(messages)) return false;
543
+ for (const m of messages) {
544
+ if (m?.role !== 'user' || typeof m.content !== 'string') continue;
545
+ if (!m.content.startsWith(SUMMARY_PREFIX)) continue;
546
+ m._mixdogRemoteCompactFallback = 'openai-codex';
547
+ m._mixdogRemoteCompactProvider = providerName || 'openai-oauth';
548
+ return true;
549
+ }
550
+ return false;
551
+ }
552
+ function providerRemoteCompactEnabled(provider, opts) {
553
+ if (opts?.remoteCompact === false) return false;
554
+ if (process.env.MIXDOG_REMOTE_COMPACT === '0') return false;
555
+ return typeof provider?.remoteCompactMessages === 'function';
556
+ }
557
+ function positiveTokenInt(value) {
558
+ const n = Number(value);
559
+ return Number.isFinite(n) && n > 0 ? Math.floor(n) : null;
560
+ }
561
+ function envFlag(name, fallback = false) {
562
+ const v = process.env[name];
563
+ if (v === undefined) return fallback;
564
+ return !['0', 'false', 'off', 'no'].includes(String(v).trim().toLowerCase());
565
+ }
566
+ function envTokenInt(name) {
567
+ return positiveTokenInt(process.env[name]);
568
+ }
569
+ function resolveSemanticCompactSetting(sessionRef, cfg = {}) {
570
+ const env = process.env.MIXDOG_BRIDGE_COMPACT_SEMANTIC;
571
+ if (env !== undefined) return envFlag('MIXDOG_BRIDGE_COMPACT_SEMANTIC', true);
572
+ if (cfg.semantic === false || cfg.semantic === 'false' || cfg.semantic === 'off') return false;
573
+ if (cfg.semantic === true || cfg.semantic === 'true' || cfg.semantic === 'on') return true;
574
+ // OpenCode keeps compaction as a session concern. For Mixdog, default the
575
+ // semantic agent only on bridge-owned workers so direct user sessions and
576
+ // narrow tests do not pay an extra provider call unless they opt in.
577
+ return sessionRef?.owner === 'bridge';
578
+ }
579
+ function resolveCompactBufferRatio(cfg = {}) {
580
+ return normalizeCompactionBufferRatio(
581
+ cfg.bufferPercent
582
+ ?? cfg.bufferPct
583
+ ?? cfg.bufferRatio
584
+ ?? cfg.bufferFraction
585
+ ?? process.env.MIXDOG_BRIDGE_COMPACT_BUFFER_PERCENT
586
+ ?? process.env.MIXDOG_BRIDGE_COMPACT_BUFFER_RATIO,
587
+ DEFAULT_COMPACTION_BUFFER_RATIO,
588
+ );
589
+ }
590
+ function resolveCompactBufferTokens(boundaryTokens, cfg = {}) {
591
+ const configured = positiveTokenInt(cfg.bufferTokens ?? cfg.buffer)
592
+ || envTokenInt('MIXDOG_BRIDGE_COMPACT_BUFFER_TOKENS')
593
+ || 0;
594
+ const boundary = positiveTokenInt(boundaryTokens);
595
+ if (!boundary) return configured || DEFAULT_COMPACTION_BUFFER_TOKENS;
596
+ return compactionBufferTokensForBoundary(boundary, {
597
+ explicitTokens: configured,
598
+ ratio: resolveCompactBufferRatio(cfg),
599
+ maxRatio: COMPACT_BUFFER_MAX_WINDOW_FRACTION,
600
+ });
601
+ }
602
+ const COMPACT_TARGET_RATIO = 0.02;
603
+ const COMPACT_TARGET_MIN_TOKENS = 4_000;
604
+ const COMPACT_TARGET_MAX_TOKENS = 16_000;
605
+ function resolveCompactTargetRatio(cfg = {}) {
606
+ const raw = cfg.targetPercent
607
+ ?? cfg.targetPct
608
+ ?? cfg.targetRatio
609
+ ?? cfg.targetFraction
610
+ ?? process.env.MIXDOG_BRIDGE_COMPACT_TARGET_PERCENT
611
+ ?? process.env.MIXDOG_COMPACT_TARGET_PERCENT
612
+ ?? COMPACT_TARGET_RATIO;
613
+ const n = Number(raw);
614
+ if (!Number.isFinite(n) || n <= 0) return COMPACT_TARGET_RATIO;
615
+ return n > 1 ? n / 100 : n;
616
+ }
617
+ function resolveCompactTargetTokens(boundaryTokens, cfg = {}) {
618
+ const boundary = positiveTokenInt(boundaryTokens);
619
+ if (!boundary) return null;
620
+ const explicit = positiveTokenInt(cfg.targetTokens ?? cfg.target)
621
+ || envTokenInt('MIXDOG_BRIDGE_COMPACT_TARGET_TOKENS')
622
+ || envTokenInt('MIXDOG_COMPACT_TARGET_TOKENS');
623
+ if (explicit) return Math.max(1, Math.min(boundary, explicit));
624
+ const minTarget = Math.min(boundary, positiveTokenInt(cfg.targetMinTokens ?? cfg.minTargetTokens)
625
+ || envTokenInt('MIXDOG_BRIDGE_COMPACT_TARGET_MIN_TOKENS')
626
+ || envTokenInt('MIXDOG_COMPACT_TARGET_MIN_TOKENS')
627
+ || COMPACT_TARGET_MIN_TOKENS);
628
+ const maxTarget = Math.min(boundary, positiveTokenInt(cfg.targetMaxTokens ?? cfg.maxTargetTokens)
629
+ || envTokenInt('MIXDOG_BRIDGE_COMPACT_TARGET_MAX_TOKENS')
630
+ || envTokenInt('MIXDOG_COMPACT_TARGET_MAX_TOKENS')
631
+ || COMPACT_TARGET_MAX_TOKENS);
632
+ const byRatio = Math.max(1, Math.floor(boundary * resolveCompactTargetRatio(cfg)));
633
+ return Math.max(1, Math.min(boundary, maxTarget, Math.max(minTarget, byRatio)));
634
+ }
635
+ function resolveCompactKeepTokens(cfg = {}) {
636
+ return positiveTokenInt(cfg.keepTokens ?? cfg.keep?.tokens ?? cfg.preserveRecentTokens)
637
+ || envTokenInt('MIXDOG_BRIDGE_COMPACT_KEEP_TOKENS')
638
+ || DEFAULT_COMPACTION_KEEP_TOKENS;
639
+ }
640
+ function resolveWorkerCompactPolicy(sessionRef, tools) {
641
+ if (!sessionRef) return null;
642
+ const cfg = sessionRef.compaction || {};
643
+ const auto = cfg.auto !== false && envFlag('MIXDOG_BRIDGE_COMPACT_AUTO', true);
644
+ if (!auto) return { auto: false };
645
+ const contextWindow = positiveTokenInt(sessionRef.contextWindow ?? cfg.contextWindow);
646
+ const explicitBoundary = positiveTokenInt(sessionRef.compactBoundaryTokens ?? cfg.boundaryTokens);
647
+ const autoLimit = positiveTokenInt(sessionRef.autoCompactTokenLimit ?? cfg.autoCompactTokenLimit);
648
+ const boundaryTokens = explicitBoundary && contextWindow
649
+ ? Math.min(explicitBoundary, contextWindow)
650
+ : (explicitBoundary || contextWindow || autoLimit);
651
+ if (!boundaryTokens) return null;
652
+ const compactBoundaryTokens = Math.max(1, Math.floor(boundaryTokens * COMPACT_SAFETY_PERCENT));
653
+ const autoTriggerTokens = autoLimit && autoLimit <= compactBoundaryTokens ? Math.max(1, autoLimit) : null;
654
+ const bufferTokens = autoTriggerTokens
655
+ ? Math.max(0, compactBoundaryTokens - autoTriggerTokens)
656
+ : resolveCompactBufferTokens(compactBoundaryTokens, cfg);
657
+ const bufferRatio = compactBoundaryTokens ? (bufferTokens / compactBoundaryTokens) : resolveCompactBufferRatio(cfg);
658
+ const triggerTokens = autoTriggerTokens || Math.max(1, compactBoundaryTokens - bufferTokens);
659
+ const configuredReserve = positiveTokenInt(cfg.reservedTokens)
660
+ || envTokenInt('MIXDOG_BRIDGE_COMPACT_RESERVED_TOKENS')
661
+ || 0;
662
+ const requestReserve = estimateRequestReserveTokens(tools);
663
+ const keepTokens = resolveCompactKeepTokens(cfg);
664
+ return {
665
+ auto: true,
666
+ prune: cfg.prune === true || envFlag('MIXDOG_BRIDGE_COMPACT_PRUNE', false),
667
+ // Narrow active-turn fallback (bridge/worker only). When system +
668
+ // current turn alone overflow the budget, shrink older same-turn tool
669
+ // outputs / drop older same-turn groups before declaring overflow,
670
+ // preserving system + task user + the latest group(s) and tool
671
+ // pairing. Defaults on for workers; disable via env to restore the
672
+ // strict no-fallback behavior.
673
+ activeTurnFallback: cfg.activeTurnFallback !== false
674
+ && envFlag('MIXDOG_BRIDGE_COMPACT_ACTIVE_TURN_FALLBACK', true),
675
+ boundaryTokens: compactBoundaryTokens,
676
+ triggerTokens,
677
+ bufferTokens,
678
+ bufferRatio,
679
+ contextWindow,
680
+ rawContextWindow: positiveTokenInt(sessionRef.rawContextWindow ?? cfg.rawContextWindow) || contextWindow,
681
+ effectiveContextWindowPercent: Number.isFinite(Number(sessionRef.effectiveContextWindowPercent ?? cfg.effectiveContextWindowPercent))
682
+ ? Number(sessionRef.effectiveContextWindowPercent ?? cfg.effectiveContextWindowPercent)
683
+ : null,
684
+ autoCompactTokenLimit: positiveTokenInt(sessionRef.autoCompactTokenLimit ?? cfg.autoCompactTokenLimit),
685
+ semantic: resolveSemanticCompactSetting(sessionRef, cfg),
686
+ semanticTimeoutMs: positiveTokenInt(cfg.timeoutMs) || envTokenInt('MIXDOG_BRIDGE_COMPACT_TIMEOUT_MS') || 30_000,
687
+ tailTurns: positiveTokenInt(cfg.tailTurns) || envTokenInt('MIXDOG_BRIDGE_COMPACT_TAIL_TURNS') || 2,
688
+ keepTokens,
689
+ preserveRecentTokens: positiveTokenInt(cfg.preserveRecentTokens) || envTokenInt('MIXDOG_BRIDGE_COMPACT_PRESERVE_RECENT_TOKENS') || keepTokens,
690
+ reserveTokens: requestReserve + configuredReserve,
691
+ requestReserveTokens: requestReserve,
692
+ configuredReserveTokens: configuredReserve,
693
+ };
694
+ }
695
+ function latestProviderContextTokens(sessionRef) {
696
+ const tokens = positiveTokenInt(sessionRef?.lastContextTokens);
697
+ if (!tokens) return 0;
698
+ const compactAt = positiveTokenInt(sessionRef?.compaction?.lastChangedAt ?? sessionRef?.compaction?.lastCompactAt) || 0;
699
+ const usageAt = positiveTokenInt(sessionRef?.lastContextTokensUpdatedAt) || 0;
700
+ // Legacy sessions did not stamp usage. Treat that usage as usable until a
701
+ // new compaction boundary appears; after compaction, only post-compaction
702
+ // provider usage may pressure the next auto-compact decision.
703
+ if (compactAt > 0 && usageAt > 0 && usageAt <= compactAt) return 0;
704
+ if (compactAt > 0 && usageAt === 0) return 0;
705
+ return tokens;
706
+ }
707
+ function compactPressureTokens(messageTokensEst, policy, sessionRef) {
708
+ const estimated = messageTokensEst === null
709
+ ? null
710
+ : Math.max(0, messageTokensEst + (policy?.reserveTokens || 0));
711
+ const providerReported = latestProviderContextTokens(sessionRef);
712
+ return Math.max(estimated ?? 0, providerReported || 0);
713
+ }
714
+ function compactTargetBudget(policy) {
715
+ const boundary = positiveTokenInt(policy?.boundaryTokens);
716
+ if (!boundary) return null;
717
+ const reserve = Math.max(0, Number(policy?.reserveTokens) || 0);
718
+ const targetEffective = resolveCompactTargetTokens(boundary, policy) || boundary;
719
+ return Math.max(1, Math.min(boundary, targetEffective + reserve));
720
+ }
721
+ function shouldCompactForSession(messageTokensEst, policy, sessionRef) {
722
+ if (!policy?.auto || !policy.boundaryTokens) return false;
723
+ if (messageTokensEst === null) return true;
724
+ return compactPressureTokens(messageTokensEst, policy, sessionRef) >= (policy.triggerTokens || policy.boundaryTokens);
725
+ }
726
+ function countPrunedToolOutputs(before, after) {
727
+ if (!Array.isArray(before) || !Array.isArray(after)) return 0;
728
+ let count = 0;
729
+ const n = Math.min(before.length, after.length);
730
+ for (let i = 0; i < n; i += 1) {
731
+ if (before[i]?.role !== 'tool' || after[i]?.role !== 'tool') continue;
732
+ if (before[i]?.content !== after[i]?.content && after[i]?.compactedKind === 'tool_output_prune') count += 1;
733
+ }
734
+ return count;
735
+ }
736
+ function rememberCompactTelemetry(sessionRef, policy, meta = {}) {
737
+ if (!sessionRef || !policy) return;
738
+ const prev = sessionRef.compaction && typeof sessionRef.compaction === 'object'
739
+ ? sessionRef.compaction
740
+ : {};
741
+ const changed = meta.compactChanged === true || meta.pruneCount > 0;
742
+ sessionRef.compaction = {
743
+ ...prev,
744
+ auto: policy.auto !== false,
745
+ prune: policy.prune === true,
746
+ reservedTokens: policy.configuredReserveTokens || prev.reservedTokens || null,
747
+ requestReserveTokens: policy.requestReserveTokens || 0,
748
+ reserveTokens: policy.reserveTokens || 0,
749
+ boundaryTokens: policy.boundaryTokens || null,
750
+ triggerTokens: policy.triggerTokens || null,
751
+ bufferTokens: policy.bufferTokens || 0,
752
+ bufferRatio: policy.bufferRatio ?? prev.bufferRatio ?? null,
753
+ contextWindow: policy.contextWindow || null,
754
+ rawContextWindow: policy.rawContextWindow || null,
755
+ effectiveContextWindowPercent: policy.effectiveContextWindowPercent ?? null,
756
+ autoCompactTokenLimit: policy.autoCompactTokenLimit || null,
757
+ semantic: policy.semantic === true ? 'auto' : false,
758
+ semanticModel: policy.semanticModel || null,
759
+ semanticTimeoutMs: policy.semanticTimeoutMs || null,
760
+ tailTurns: policy.tailTurns || null,
761
+ keepTokens: policy.keepTokens || null,
762
+ preserveRecentTokens: policy.preserveRecentTokens || null,
763
+ lastCheckedAt: Date.now(),
764
+ lastBeforeTokens: meta.beforeTokens ?? null,
765
+ lastAfterTokens: meta.afterTokens ?? null,
766
+ lastPressureTokens: meta.pressureTokens ?? null,
767
+ lastStage: meta.stage || prev.lastStage || null,
768
+ lastChanged: changed,
769
+ lastRemote: meta.remoteCompact === true,
770
+ lastSemantic: meta.semanticCompact === true,
771
+ lastSemanticError: meta.semanticError || null,
772
+ lastPruneCount: meta.pruneCount || 0,
773
+ compactCount: (prev.compactCount || 0) + (changed ? 1 : 0),
774
+ };
775
+ if (changed) {
776
+ const changedAt = Date.now();
777
+ sessionRef.compaction.lastChangedAt = changedAt;
778
+ sessionRef.compaction.lastCompactAt = changedAt;
779
+ sessionRef.lastContextTokensStaleAfterCompact = true;
780
+ }
781
+ sessionRef.contextWindow = policy.contextWindow || sessionRef.contextWindow;
782
+ sessionRef.rawContextWindow = policy.rawContextWindow || sessionRef.rawContextWindow;
783
+ sessionRef.autoCompactTokenLimit = policy.autoCompactTokenLimit || sessionRef.autoCompactTokenLimit || null;
784
+ sessionRef.compactBoundaryTokens = policy.boundaryTokens || sessionRef.compactBoundaryTokens || null;
785
+ if (policy.effectiveContextWindowPercent !== null) {
786
+ sessionRef.effectiveContextWindowPercent = policy.effectiveContextWindowPercent;
787
+ }
788
+ }
789
+ const SKILL_TOOL_NAMES = new Set(['skills_list', 'skill_view', 'skill_execute']);
790
+ const SPECIAL_TOOL_NAMES = new Set(['bash_session', 'apply_patch', 'code_graph']);
791
+ const BASH_SESSION_HEADER_RE = /\[session: ([^\]\r\n]+)\]/;
792
+ const STORED_TOOL_ARG_BODY_KEY_RE = /^(?:content|old_string|new_string|patch|rewrite)$/i;
793
+ const STORED_TOOL_ARG_LONG_KEY_RE = /^(?:command|script)$/i;
794
+ const STORED_TOOL_ARG_BODY_LIMIT = 2_000;
795
+ const STORED_TOOL_ARG_LONG_LIMIT = 8_000;
796
+ const STORED_TOOL_ARG_PREVIEW_HEAD = 360;
797
+ const STORED_TOOL_ARG_PREVIEW_TAIL = 160;
798
+
799
+ function compactStoredToolArgString(value, key = '') {
800
+ if (typeof value !== 'string') return value;
801
+ const isBody = STORED_TOOL_ARG_BODY_KEY_RE.test(key);
802
+ const isLong = isBody || STORED_TOOL_ARG_LONG_KEY_RE.test(key);
803
+ const limit = isBody ? STORED_TOOL_ARG_BODY_LIMIT : (isLong ? STORED_TOOL_ARG_LONG_LIMIT : Infinity);
804
+ if (value.length <= limit) return value;
805
+ const hash = createHash('sha256').update(value).digest('hex').slice(0, 16);
806
+ const head = value.slice(0, STORED_TOOL_ARG_PREVIEW_HEAD).replace(/\r\n/g, '\n');
807
+ const tail = value.slice(-STORED_TOOL_ARG_PREVIEW_TAIL).replace(/\r\n/g, '\n');
808
+ return `[mixdog compacted ${key || 'string'}: ${value.length} chars, sha256:${hash}]\n${head}\n... [middle omitted from stored tool-call args] ...\n${tail}`;
809
+ }
810
+
811
+ function compactStoredToolArgValue(value, key = '', depth = 0) {
812
+ if (value === null || value === undefined) return value;
813
+ if (typeof value === 'string') return compactStoredToolArgString(value, key);
814
+ if (typeof value !== 'object') return value;
815
+ if (depth >= 6) return Array.isArray(value) ? `[${value.length} items]` : '{...}';
816
+ if (Array.isArray(value)) {
817
+ return value.map((item) => compactStoredToolArgValue(item, key, depth + 1));
818
+ }
819
+ const out = {};
820
+ for (const [k, v] of Object.entries(value)) {
821
+ out[k] = compactStoredToolArgValue(v, k, depth + 1);
822
+ }
823
+ return out;
824
+ }
825
+
826
+ function compactToolCallsForHistory(calls) {
827
+ if (!Array.isArray(calls)) return calls;
828
+ return calls.map((call) => {
829
+ if (!call || typeof call !== 'object') return call;
830
+ return {
831
+ ...call,
832
+ arguments: compactStoredToolArgValue(call.arguments),
833
+ };
834
+ });
835
+ }
836
+
837
+ // Restore the FULL body of ONE tool call inside a history assistant message
838
+ // whose toolCalls were compacted at push time. Used for a failed edit call so
839
+ // the model sees the original patch/old_string on retry instead of a
840
+ // `[mixdog compacted …]` placeholder it cannot act on. Must run BEFORE the
841
+ // message is first transmitted so it never mutates an already-cached prefix
842
+ // (the prompt cache is content-prefix matched).
843
+ //
844
+ // Only the compactable body/long keys (patch, old_string, new_string, content,
845
+ // rewrite, command, script) are restored, and at ANY depth — compaction is
846
+ // recursive (compactStoredToolArgValue), so batch shapes like edits[].old_string
847
+ // or writes[].content carry nested compacted bodies too. Every other field
848
+ // (e.g. `path`, which a tool may mutate in place during execution) is taken from
849
+ // the compacted snapshot captured at push time, before any mutation. The
850
+ // compacted args tree is built fresh by compactToolCallsForHistory and is not
851
+ // shared with originalCalls, so rebuilding it here is safe.
852
+ function restoreToolCallBodyForId(assistantMsg, originalCalls, callId) {
853
+ if (!assistantMsg || !Array.isArray(assistantMsg.toolCalls) || !callId) return;
854
+ if (!Array.isArray(originalCalls)) return;
855
+ const tc = assistantMsg.toolCalls.find((t) => t && t.id === callId);
856
+ const orig = originalCalls.find((c) => c && c.id === callId);
857
+ if (!tc || !orig) return;
858
+ if (!tc.arguments || typeof tc.arguments !== 'object'
859
+ || !orig.arguments || typeof orig.arguments !== 'object') return;
860
+ tc.arguments = _restoreCompactedBodies(tc.arguments, orig.arguments, '');
861
+ }
862
+
863
+ // Recursively rebuild a compacted args tree: replace ONLY compactable body/long
864
+ // string fields (matched by key at any depth) with their full originals, and
865
+ // keep every other field from the compacted snapshot. tcVal and origVal share
866
+ // the same structure (compaction only shortens body strings), so the walk
867
+ // descends them in parallel; a missing or non-object origVal falls back to the
868
+ // compacted value rather than throwing.
869
+ function _restoreCompactedBodies(tcVal, origVal, key) {
870
+ if ((STORED_TOOL_ARG_BODY_KEY_RE.test(key) || STORED_TOOL_ARG_LONG_KEY_RE.test(key))
871
+ && typeof origVal === 'string') {
872
+ return origVal;
873
+ }
874
+ if (Array.isArray(tcVal) && Array.isArray(origVal)) {
875
+ return tcVal.map((item, i) => _restoreCompactedBodies(item, origVal[i], key));
876
+ }
877
+ if (tcVal && typeof tcVal === 'object' && origVal && typeof origVal === 'object') {
878
+ const out = {};
879
+ for (const k of Object.keys(tcVal)) {
880
+ out[k] = (k in origVal) ? _restoreCompactedBodies(tcVal[k], origVal[k], k) : tcVal[k];
881
+ }
882
+ return out;
883
+ }
884
+ return tcVal;
885
+ }
886
+ /**
887
+ * Execute a single tool call — routes to MCP or builtin.
888
+ */
889
+ function getToolKind(name) {
890
+ if (SKILL_TOOL_NAMES.has(name)) return 'skill';
891
+ if (SPECIAL_TOOL_NAMES.has(name)) return 'builtin';
892
+ if (isMcpTool(name)) return 'mcp';
893
+ if (isInternalTool(name)) return 'internal';
894
+ if (isBuiltinTool(name)) return 'builtin';
895
+ return 'builtin';
896
+ }
897
+ function buildSkillsListResponse(cwd) {
898
+ const skills = collectSkillsCached(cwd);
899
+ const entries = skills.map(s => ({ name: s.name, description: s.description || '' }));
900
+ return JSON.stringify({ skills: entries });
901
+ }
902
+ function viewSkill(cwd, name) {
903
+ if (!name) return 'Error: skill name is required';
904
+ const content = loadSkillContent(name, cwd);
905
+ return content || `Error: skill "${name}" not found`;
906
+ }
907
+ function executeSkill(cwd, name, _args) {
908
+ if (!name) return 'Error: skill name is required';
909
+ const content = loadSkillContent(name, cwd);
910
+ return content || `Error: skill "${name}" not found`;
911
+ }
912
+ function extractBashSessionId(result) {
913
+ if (typeof result !== 'string') return null;
914
+ const match = BASH_SESSION_HEADER_RE.exec(result);
915
+ return match ? match[1] : null;
916
+ }
917
+
918
+ export function buildBridgeBashSessionArgs(args, sessionRef) {
919
+ if (sessionRef?.owner !== 'bridge') return null;
920
+ // run_in_background is a detached one-shot job, incompatible with the
921
+ // persistent bash session. Fall through to the background-job path
922
+ // (executeBuiltinTool -> startBackgroundShellJob) so the worker gets a
923
+ // task_id that task control can resolve — otherwise the persistent
924
+ // session returns a [session: ...] header and task control reports "task not found".
925
+ if (args?.run_in_background === true) return null;
926
+ const routedArgs = { ...(args || {}) };
927
+ const explicitSessionId = typeof routedArgs.session_id === 'string' && routedArgs.session_id.trim()
928
+ ? routedArgs.session_id.trim()
929
+ : null;
930
+ const wantsPersistent = routedArgs.persistent === true || !!explicitSessionId;
931
+ if (!wantsPersistent) return null;
932
+ if (!explicitSessionId && sessionRef?.implicitBashSessionId) {
933
+ routedArgs.session_id = sessionRef.implicitBashSessionId;
934
+ } else if (explicitSessionId) {
935
+ routedArgs.session_id = explicitSessionId;
936
+ }
937
+ delete routedArgs.persistent;
938
+ return routedArgs;
939
+ }
940
+
941
+ function _scopedCacheOutcomeForCall(sessionRef, toolCallId, toolName, callerSessionId, executeOpts = {}) {
942
+ if (executeOpts.scopedCacheOutcome) {
943
+ if (sessionRef && toolCallId) {
944
+ if (!sessionRef._scopedCacheOutcomeByCallId) sessionRef._scopedCacheOutcomeByCallId = new Map();
945
+ sessionRef._scopedCacheOutcomeByCallId.set(toolCallId, executeOpts.scopedCacheOutcome);
946
+ }
947
+ return executeOpts.scopedCacheOutcome;
948
+ }
949
+ if (!callerSessionId || !toolCallId || !_isScopedCacheableTool(toolName)) return null;
950
+ const outcome = createScopedCacheOutcome();
951
+ if (sessionRef) {
952
+ if (!sessionRef._scopedCacheOutcomeByCallId) sessionRef._scopedCacheOutcomeByCallId = new Map();
953
+ sessionRef._scopedCacheOutcomeByCallId.set(toolCallId, outcome);
954
+ }
955
+ return outcome;
956
+ }
957
+
958
+ async function executeTool(name, args, cwd, callerSessionId, sessionRef, executeOpts = {}) {
959
+ const scopedCacheOutcome = _scopedCacheOutcomeForCall(
960
+ sessionRef,
961
+ executeOpts.toolCallId,
962
+ name,
963
+ callerSessionId,
964
+ executeOpts,
965
+ );
966
+ const toolOpts = scopedCacheOutcome
967
+ ? { ...executeOpts, scopedCacheOutcome }
968
+ : executeOpts;
969
+ const notifyFn = (text) => {
970
+ if (!callerSessionId) return;
971
+ try { enqueuePendingMessage(callerSessionId, String(text || '')); } catch { /* best effort */ }
972
+ };
973
+ const completionToolOpts = {
974
+ ...toolOpts,
975
+ sessionId: callerSessionId,
976
+ callerSessionId,
977
+ routingSessionId: callerSessionId,
978
+ clientHostPid: sessionRef?.clientHostPid,
979
+ notifyFn,
980
+ };
981
+ const beforeToolHook = typeof executeOpts.beforeToolHook === 'function'
982
+ ? executeOpts.beforeToolHook
983
+ : sessionRef?.beforeToolHook;
984
+ if (beforeToolHook) {
985
+ try {
986
+ const decision = await beforeToolHook({
987
+ name,
988
+ args,
989
+ cwd,
990
+ sessionId: callerSessionId,
991
+ toolCallId: executeOpts.toolCallId || null,
992
+ });
993
+ const action = String(decision?.action || decision?.decision || '').toLowerCase();
994
+ if (action === 'deny' || action === 'block') {
995
+ const reason = decision?.reason ? `: ${decision.reason}` : '';
996
+ return `Error: tool "${name}" denied by hook${reason}`;
997
+ }
998
+ if ((action === 'modify' || action === 'rewrite') && decision?.args && typeof decision.args === 'object' && !Array.isArray(decision.args)) {
999
+ args = decision.args;
1000
+ }
1001
+ } catch {
1002
+ // Hooks are policy extensions. A broken hook must not wedge the agent loop.
1003
+ }
1004
+ }
1005
+ if (name === 'skills_list') {
1006
+ return buildSkillsListResponse(cwd);
1007
+ }
1008
+ if (name === 'skill_view') {
1009
+ return viewSkill(cwd, args?.name);
1010
+ }
1011
+ if (name === 'skill_execute') {
1012
+ return executeSkill(cwd, args?.name, args?.args);
1013
+ }
1014
+ if (isMcpTool(name)) {
1015
+ // 24h trace data shows ~24% of external MCP calls are cwd-sensitive
1016
+ // (bash / grep / read / list / glob etc.) but the worker session's
1017
+ // cwd was previously dropped here. Inject cwd only when the tool's
1018
+ // inputSchema declares the field — schemas without it would reject
1019
+ // an unknown argument.
1020
+ const needsCwdInjection = cwd
1021
+ && mcpToolHasField(name, 'cwd')
1022
+ && (args == null || args.cwd == null);
1023
+ const finalArgs = needsCwdInjection ? { ...(args || {}), cwd } : args;
1024
+ return executeMcpTool(name, finalArgs);
1025
+ }
1026
+ if (isCodeGraphTool(name)) {
1027
+ // cwd chain: args.cwd (caller-explicit) → session cwd → undefined (handler throws)
1028
+ const graphCwd = (typeof args?.cwd === 'string' && args.cwd.trim()) ? args.cwd.trim() : cwd;
1029
+ return executeCodeGraphTool(name, args, graphCwd, null, toolOpts);
1030
+ }
1031
+ if (isInternalTool(name)) {
1032
+ // callerSessionId propagates into server.mjs dispatchTool so that
1033
+ // dispatchAiWrapped can detect and reject recursive calls from a
1034
+ // hidden-role session (recall/search/explore → self).
1035
+ return executeInternalTool(name, args, {
1036
+ callerSessionId,
1037
+ callerCwd: cwd,
1038
+ clientHostPid: sessionRef?.clientHostPid,
1039
+ signal: executeOpts.signal,
1040
+ routingSessionId: callerSessionId,
1041
+ notifyFn,
1042
+ });
1043
+ }
1044
+ if (name === 'shell') {
1045
+ const routedArgs = buildBridgeBashSessionArgs(args, sessionRef);
1046
+ if (!routedArgs) {
1047
+ // clientHostPid scopes background shell-jobs to the dispatching
1048
+ // terminal's claude.exe pid (bridge sessions store it on sessionRef);
1049
+ // without it resolveJobOwnerHostPid falls back to the daemon-global env.
1050
+ return executeBuiltinTool(name, args, cwd, completionToolOpts);
1051
+ }
1052
+ // Thread the session's AbortSignal so bridge type=close can interrupt the
1053
+ // persistent child process. getSessionAbortSignal is imported at top of
1054
+ // loop.mjs from manager.mjs; callerSessionId identifies the controller.
1055
+ let _bashAbortSignal = null;
1056
+ try { _bashAbortSignal = getSessionAbortSignal(callerSessionId); } catch { /* ignore */ }
1057
+ const result = await executeBashSessionTool('bash_session', routedArgs, cwd, {
1058
+ sessionId: callerSessionId,
1059
+ abortSignal: _bashAbortSignal,
1060
+ });
1061
+ const bashSid = extractBashSessionId(result);
1062
+ if (bashSid) {
1063
+ sessionRef.implicitBashSessionId = bashSid;
1064
+ // Track all persistent bash sessions for bulk teardown on close.
1065
+ if (sessionRef.allBashSessionIds) {
1066
+ if (!sessionRef.allBashSessionIds.includes(bashSid)) {
1067
+ sessionRef.allBashSessionIds.push(bashSid);
1068
+ }
1069
+ } else {
1070
+ sessionRef.allBashSessionIds = [bashSid];
1071
+ }
1072
+ }
1073
+ return result;
1074
+ }
1075
+ if (name === 'apply_patch') {
1076
+ return executePatchTool(name, args, cwd, { sessionId: callerSessionId });
1077
+ }
1078
+ if (isBuiltinTool(name)) {
1079
+ // clientHostPid threaded for the same per-terminal job-scope reason as
1080
+ // the bash branch above (see resolveJobOwnerHostPid).
1081
+ return executeBuiltinTool(name, args, cwd, completionToolOpts);
1082
+ }
1083
+ return formatUnknownBuiltinToolMessage(name, args, 'tool');
1084
+ }
1085
+ /**
1086
+ * Agent loop: send → tool_call → execute → re-send → repeat until text.
1087
+ * sendOpts may include:
1088
+ * - `effort` (provider-specific)
1089
+ * - `fast` (boolean)
1090
+ * - `sessionId` — enables runtime liveness markers (optional)
1091
+ * - `signal` — AbortSignal; checked at each iteration boundary and after each
1092
+ * tool. When aborted, throws SessionClosedError so the ask
1093
+ * wrapper can propagate a clean cancellation.
1094
+ * - `onStageChange(stage)` / `onStreamDelta()` — forwarded to provider.send for heartbeats
1095
+ */
1096
+ // Source of truth: defaults/hidden-roles.json (loaded via _getHiddenRoles
1097
+ // above). Build the name Set eagerly at module load so HIDDEN_ROLE_NAMES
1098
+ // stays in sync with the declarative registry — no hardcoded duplicate.
1099
+ const HIDDEN_ROLE_NAMES = new Set(
1100
+ (_getHiddenRoles().roles || []).map((r) => r && r.name).filter((n) => typeof n === 'string' && n.length > 0)
1101
+ );
1102
+
1103
+ // Stop reasons that signal the turn was cut short mid-synthesis (token cap,
1104
+ // provider pause). Empty content + one of these reasons means the worker
1105
+ // was not done — re-prompt instead of accepting empty as final.
1106
+ // Covers Anthropic (pause_turn, max_tokens), OpenAI (length), Gemini
1107
+ // (MAX_TOKENS, OTHER), and case variants.
1108
+ const INCOMPLETE_STOP_REASONS = new Set([
1109
+ 'pause_turn', 'max_tokens', 'length', 'MAX_TOKENS', 'OTHER',
1110
+ ]);
1111
+
1112
+ export async function agentLoop(provider, messages, model, tools, onToolCall, cwd, sendOpts) {
1113
+ let iterations = 0;
1114
+ let toolCallsTotal = 0;
1115
+ let lastUsage;
1116
+ let firstTurnUsage;
1117
+ let response;
1118
+ let contractNudges = 0;
1119
+ const opts = sendOpts || {};
1120
+ const sessionId = opts.sessionId || null;
1121
+ const signal = opts.signal || null;
1122
+ const sessionRole = opts.session?.role;
1123
+ const forcedFirstTool = opts.forcedFirstTool ?? null;
1124
+ const forcedFirstToolDef = forcedFirstTool
1125
+ ? tools.find(tool => tool?.name === forcedFirstTool)
1126
+ : null;
1127
+ // Opaque providerState passthrough. The loop never inspects provider-native
1128
+ // payloads; the originating provider owns them. OpenAI Codex uses this for
1129
+ // native remote compaction prefixes, and stateful Responses providers may
1130
+ // use it for continuation anchors.
1131
+ let providerState = opts.providerState ?? undefined;
1132
+ const throwIfAborted = () => {
1133
+ if (signal?.aborted) {
1134
+ const reason = signal.reason instanceof Error ? signal.reason : null;
1135
+ // Preserve any structured abort reason (SessionClosedError,
1136
+ // StreamStalledAbortError, etc.). Fallback to SessionClosedError
1137
+ // when the reason is not an Error instance.
1138
+ if (reason) throw reason;
1139
+ throw new SessionClosedError(sessionId || 'unknown', 'agent loop aborted');
1140
+ }
1141
+ };
1142
+ const sessionRef = opts.session || null;
1143
+ const pushToolResultMessage = (message) => {
1144
+ messages.push(message);
1145
+ try { opts.onToolResult?.(message); } catch {}
1146
+ };
1147
+ const drainSteeringIntoMessages = (stage = 'mid-turn') => {
1148
+ if (typeof opts.drainSteering !== 'function') return false;
1149
+ let steerMsgs = [];
1150
+ try { steerMsgs = opts.drainSteering(sessionId) || []; }
1151
+ catch { steerMsgs = []; }
1152
+ const merged = mergeSteeringEntries(steerMsgs);
1153
+ if (!merged) return false;
1154
+ messages.push({ role: 'user', content: merged.content });
1155
+ try { opts.onSteerMessage?.(merged.text || steeringContentText(merged.content)); } catch {}
1156
+ if (sessionId) {
1157
+ try { process.stderr.write(`[steer] sess=${sessionId} injected ${stage} user message (merged=${merged.count} len=${String(merged.text || '').length})\n`); } catch {}
1158
+ }
1159
+ return true;
1160
+ };
1161
+ const maxLoopIterations = Number.isFinite(sessionRef?.maxLoopIterations)
1162
+ ? sessionRef.maxLoopIterations
1163
+ : MAX_LOOP_ITERATIONS;
1164
+ // Tool execution must use the session cwd even when the caller omitted the
1165
+ // legacy positional cwd argument. Bridge workers always carry their cwd on
1166
+ // sessionRef; falling through to pwd()/process.cwd() resolves relatives
1167
+ // against the host/plugin root instead of the worker workspace.
1168
+ cwd = cwd || sessionRef?.cwd || undefined;
1169
+ while (true) {
1170
+ throwIfAborted();
1171
+ if (iterations >= maxLoopIterations) {
1172
+ process.stderr.write(`[loop] hard iteration cap ${maxLoopIterations} reached (sess=${sessionId || 'unknown'}); stopping loop.\n`);
1173
+ break;
1174
+ }
1175
+ const compactPolicy = resolveWorkerCompactPolicy(sessionRef, tools);
1176
+ if (compactPolicy?.auto) {
1177
+ // Snapshot pre-compact shape so compact_meta can record the actual
1178
+ // mutation (or no-op) for prefix-mutation forensics. Bytes are
1179
+ // a best-effort JSON.stringify length — close enough to the
1180
+ // payload we hand the provider for prefix-cache analysis.
1181
+ const beforeCount = messages.length;
1182
+ let beforeBytes = null;
1183
+ try { beforeBytes = Buffer.byteLength(JSON.stringify(messages), 'utf8'); } catch { beforeBytes = null; }
1184
+ const messageTokensEst = estimateMessagesTokensSafe(messages);
1185
+ const pressureTokens = compactPressureTokens(messageTokensEst, compactPolicy, sessionRef);
1186
+ const shouldCompact = shouldCompactForSession(messageTokensEst, compactPolicy, sessionRef);
1187
+ const compactBudgetTokens = shouldCompact
1188
+ ? (compactTargetBudget({ ...compactPolicy, pressureTokens }) || compactPolicy.boundaryTokens)
1189
+ : compactPolicy.boundaryTokens;
1190
+ if (!shouldCompact) {
1191
+ rememberCompactTelemetry(sessionRef, compactPolicy, {
1192
+ stage: 'pre_send_check',
1193
+ beforeTokens: messageTokensEst,
1194
+ afterTokens: messageTokensEst,
1195
+ pressureTokens,
1196
+ });
1197
+ } else {
1198
+ try { opts.onStageChange?.('compacting'); } catch { /* best-effort */ }
1199
+ rememberCompactTelemetry(sessionRef, compactPolicy, {
1200
+ stage: 'compacting',
1201
+ beforeTokens: messageTokensEst,
1202
+ afterTokens: messageTokensEst,
1203
+ pressureTokens,
1204
+ });
1205
+ let compacted;
1206
+ let remoteCompactResult = null;
1207
+ let pruneCount = 0;
1208
+ let summaryChanged = false;
1209
+ let semanticCompactResult = null;
1210
+ let semanticCompactError = null;
1211
+ try {
1212
+ let compactInputMessages = messages;
1213
+ if (compactPolicy.prune) {
1214
+ const pruned = pruneToolOutputs(messages, compactPolicy.boundaryTokens, {
1215
+ reserveTokens: compactPolicy.reserveTokens,
1216
+ });
1217
+ pruneCount = countPrunedToolOutputs(messages, pruned);
1218
+ compactInputMessages = pruned;
1219
+ }
1220
+ // Pre-send compaction replaces destructive drop: older
1221
+ // non-system history is condensed into one summary message.
1222
+ // The current turn and tool pairing stay intact; if those
1223
+ // mandatory parts cannot fit, compactMessages throws instead
1224
+ // of silently discarding user-visible context.
1225
+ if (compactPolicy.semantic) {
1226
+ try {
1227
+ semanticCompactResult = await semanticCompactMessages(
1228
+ provider,
1229
+ compactInputMessages,
1230
+ model,
1231
+ compactBudgetTokens,
1232
+ {
1233
+ reserveTokens: compactPolicy.reserveTokens,
1234
+ providerName: sessionRef.provider || provider?.name || null,
1235
+ sessionId,
1236
+ signal,
1237
+ sendOpts: opts,
1238
+ promptCacheKey: opts.promptCacheKey || null,
1239
+ providerCacheKey: opts.providerCacheKey || null,
1240
+ timeoutMs: compactPolicy.semanticTimeoutMs,
1241
+ tailTurns: compactPolicy.tailTurns,
1242
+ keepTokens: compactPolicy.keepTokens,
1243
+ preserveRecentTokens: compactPolicy.preserveRecentTokens,
1244
+ force: true,
1245
+ },
1246
+ );
1247
+ const semanticMessages = Array.isArray(semanticCompactResult?.messages)
1248
+ ? semanticCompactResult.messages
1249
+ : null;
1250
+ if (semanticMessages && (semanticCompactResult?.semantic === true
1251
+ || messagesArrayChanged(compactInputMessages, semanticMessages))) {
1252
+ compacted = semanticMessages;
1253
+ }
1254
+ if (semanticCompactResult?.usage) {
1255
+ lastUsage = addUsage(lastUsage, semanticCompactResult.usage);
1256
+ if (!firstTurnUsage) firstTurnUsage = normalizeUsage(semanticCompactResult.usage);
1257
+ if (sessionId && opts.onUsageDelta) {
1258
+ try {
1259
+ opts.onUsageDelta({
1260
+ sessionId,
1261
+ iterationIndex: iterations + 1,
1262
+ deltaInput: semanticCompactResult.usage.inputTokens || 0,
1263
+ deltaOutput: semanticCompactResult.usage.outputTokens || 0,
1264
+ deltaCachedRead: semanticCompactResult.usage.cachedTokens || 0,
1265
+ deltaCacheWrite: semanticCompactResult.usage.cacheWriteTokens || 0,
1266
+ source: 'semantic_compact',
1267
+ ts: Date.now(),
1268
+ });
1269
+ } catch { /* best-effort */ }
1270
+ }
1271
+ }
1272
+ } catch (semanticErr) {
1273
+ semanticCompactError = semanticErr;
1274
+ try {
1275
+ process.stderr.write(
1276
+ `[loop] semantic compact failed (sess=${sessionId || 'unknown'}): ` +
1277
+ `${semanticErr?.message || semanticErr}; falling back to deterministic compact\n`,
1278
+ );
1279
+ } catch { /* best-effort */ }
1280
+ }
1281
+ }
1282
+ if (!compacted) {
1283
+ try {
1284
+ compacted = compactMessages(compactInputMessages, compactBudgetTokens, {
1285
+ reserveTokens: compactPolicy.reserveTokens,
1286
+ force: true,
1287
+ });
1288
+ } catch (deterministicErr) {
1289
+ // Deterministic (and any prior semantic) compaction
1290
+ // failed because system + the entire current turn is
1291
+ // mandatory and overflows the budget. For bridge
1292
+ // workers, fall back to the narrow active-turn
1293
+ // compactor, which shrinks older same-turn tool
1294
+ // outputs / drops older same-turn groups while
1295
+ // preserving system + task user + the latest
1296
+ // group(s) and tool pairing. It still throws (and we
1297
+ // surface overflow below) when even that floor cannot
1298
+ // fit, so overflow/cancellation behavior is preserved.
1299
+ if (!compactPolicy.activeTurnFallback) throw deterministicErr;
1300
+ compacted = compactActiveTurn(compactInputMessages, compactBudgetTokens, {
1301
+ reserveTokens: compactPolicy.reserveTokens,
1302
+ force: true,
1303
+ });
1304
+ try {
1305
+ process.stderr.write(
1306
+ `[loop] active-turn fallback compaction (sess=${sessionId || 'unknown'}): ` +
1307
+ `${deterministicErr?.message || deterministicErr}\n`,
1308
+ );
1309
+ } catch { /* best-effort */ }
1310
+ }
1311
+ }
1312
+ summaryChanged = messagesArrayChanged(compactInputMessages, compacted);
1313
+ if (summaryChanged && providerRemoteCompactEnabled(provider, opts)) {
1314
+ const compactInput = splitMessagesForRemoteCompact(messages);
1315
+ if (compactInput) {
1316
+ try {
1317
+ remoteCompactResult = await provider.remoteCompactMessages(
1318
+ compactInput,
1319
+ model,
1320
+ tools,
1321
+ {
1322
+ ...opts,
1323
+ thinkingBudgetTokens: undefined,
1324
+ xaiReasoningEffort: undefined,
1325
+ reasoningEffort: undefined,
1326
+ effort: 'low',
1327
+ providerState,
1328
+ iteration: iterations + 1,
1329
+ remoteCompact: true,
1330
+ onToolCall: undefined,
1331
+ onStreamDelta: undefined,
1332
+ },
1333
+ );
1334
+ if (remoteCompactResult?.providerState !== undefined) {
1335
+ markRemoteCompactFallback(compacted, provider?.name);
1336
+ }
1337
+ if (remoteCompactResult?.usage) {
1338
+ lastUsage = addUsage(lastUsage, remoteCompactResult.usage);
1339
+ if (!firstTurnUsage) firstTurnUsage = normalizeUsage(remoteCompactResult.usage);
1340
+ if (sessionId && opts.onUsageDelta) {
1341
+ try {
1342
+ opts.onUsageDelta({
1343
+ sessionId,
1344
+ iterationIndex: iterations + 1,
1345
+ deltaInput: remoteCompactResult.usage.inputTokens || 0,
1346
+ deltaOutput: remoteCompactResult.usage.outputTokens || 0,
1347
+ deltaCachedRead: remoteCompactResult.usage.cachedTokens || 0,
1348
+ deltaCacheWrite: remoteCompactResult.usage.cacheWriteTokens || 0,
1349
+ source: 'remote_compact',
1350
+ ts: Date.now(),
1351
+ });
1352
+ } catch { /* best-effort */ }
1353
+ }
1354
+ }
1355
+ } catch (remoteErr) {
1356
+ try {
1357
+ process.stderr.write(
1358
+ `[loop] remote compact failed (sess=${sessionId || 'unknown'}): ` +
1359
+ `${remoteErr?.message || remoteErr}; falling back to local summary\n`,
1360
+ );
1361
+ } catch { /* best-effort */ }
1362
+ traceBridgeCompact({
1363
+ sessionId,
1364
+ iteration: iterations + 1,
1365
+ stage: 'remote_compact',
1366
+ prune_count: pruneCount,
1367
+ compact_changed: false,
1368
+ input_prefix_hash: messagePrefixHash(messages),
1369
+ before_count: beforeCount,
1370
+ after_count: beforeCount,
1371
+ before_bytes: beforeBytes,
1372
+ after_bytes: beforeBytes,
1373
+ context_window: compactPolicy.contextWindow,
1374
+ budget_tokens: compactPolicy.boundaryTokens,
1375
+ target_budget_tokens: compactBudgetTokens,
1376
+ reserve_tokens: compactPolicy.reserveTokens,
1377
+ message_tokens_est: messageTokensEst,
1378
+ provider: sessionRef.provider,
1379
+ model: sessionRef.model || model,
1380
+ error: remoteErr && remoteErr.message ? remoteErr.message : String(remoteErr),
1381
+ error_code: 'REMOTE_COMPACT_FAILED',
1382
+ });
1383
+ }
1384
+ }
1385
+ }
1386
+ } catch (compactErr) {
1387
+ traceBridgeCompact({
1388
+ sessionId,
1389
+ iteration: iterations + 1,
1390
+ stage: 'pre_send',
1391
+ prune_count: pruneCount,
1392
+ compact_changed: false,
1393
+ input_prefix_hash: messagePrefixHash(messages),
1394
+ before_count: beforeCount,
1395
+ after_count: messages.length,
1396
+ before_bytes: beforeBytes,
1397
+ after_bytes: beforeBytes,
1398
+ context_window: compactPolicy.contextWindow,
1399
+ budget_tokens: compactPolicy.boundaryTokens,
1400
+ target_budget_tokens: compactBudgetTokens,
1401
+ reserve_tokens: compactPolicy.reserveTokens,
1402
+ message_tokens_est: messageTokensEst,
1403
+ provider: sessionRef.provider,
1404
+ model: sessionRef.model || model,
1405
+ error: compactErr && compactErr.message ? compactErr.message : String(compactErr),
1406
+ error_code: 'BRIDGE_CONTEXT_OVERFLOW',
1407
+ });
1408
+ throw bridgeContextOverflowError({
1409
+ stage: 'pre_send',
1410
+ sessionId,
1411
+ sessionRef,
1412
+ model,
1413
+ budgetTokens: compactBudgetTokens,
1414
+ reserveTokens: compactPolicy.reserveTokens,
1415
+ messageTokensEst,
1416
+ }, compactErr);
1417
+ }
1418
+ try { opts.onStageChange?.('requesting'); } catch { /* best-effort */ }
1419
+ const compactChanged = messagesArrayChanged(messages, compacted);
1420
+ if (compactChanged) {
1421
+ messages.length = 0;
1422
+ messages.push(...compacted);
1423
+ if (remoteCompactResult?.providerState !== undefined) {
1424
+ providerState = remoteCompactResult.providerState;
1425
+ } else {
1426
+ // Compacting/pruning the transcript invalidates the
1427
+ // server-side conversation anchor (xAI Responses /
1428
+ // Codex WS rely on previous_response_id which points
1429
+ // at a now-mutated prefix). Drop providerState so the
1430
+ // next send starts a fresh chain instead of triggering
1431
+ // silent cache miss or hard mismatch.
1432
+ providerState = undefined;
1433
+ }
1434
+ // Compaction shrank the transcript, so prior turns no
1435
+ // longer pressure the window — reset the iteration counter
1436
+ // so a steadily-compacting long task isn't killed by the
1437
+ // cap, while a non-compacting tight loop still hits it.
1438
+ iterations = 0;
1439
+ }
1440
+ const afterTokens = estimateMessagesTokensSafe(messages);
1441
+ rememberCompactTelemetry(sessionRef, compactPolicy, {
1442
+ stage: 'pre_send',
1443
+ beforeTokens: messageTokensEst,
1444
+ afterTokens,
1445
+ pressureTokens,
1446
+ compactChanged,
1447
+ remoteCompact: remoteCompactResult?.providerState !== undefined,
1448
+ semanticCompact: semanticCompactResult?.semantic === true,
1449
+ semanticError: semanticCompactError?.message || null,
1450
+ pruneCount,
1451
+ });
1452
+ let afterBytes = null;
1453
+ try { afterBytes = Buffer.byteLength(JSON.stringify(messages), 'utf8'); } catch { afterBytes = null; }
1454
+ traceBridgeCompact({
1455
+ sessionId,
1456
+ iteration: iterations + 1,
1457
+ stage: 'pre_send',
1458
+ prune_count: pruneCount,
1459
+ compact_changed: compactChanged || summaryChanged,
1460
+ input_prefix_hash: messagePrefixHash(messages),
1461
+ before_count: beforeCount,
1462
+ after_count: messages.length,
1463
+ before_bytes: beforeBytes,
1464
+ after_bytes: afterBytes,
1465
+ context_window: compactPolicy.contextWindow,
1466
+ budget_tokens: compactPolicy.boundaryTokens,
1467
+ target_budget_tokens: compactBudgetTokens,
1468
+ reserve_tokens: compactPolicy.reserveTokens,
1469
+ message_tokens_est: messageTokensEst,
1470
+ provider: sessionRef.provider,
1471
+ model: sessionRef.model || model,
1472
+ });
1473
+ }
1474
+ }
1475
+ // A pre-send compaction pass can take long enough for the user (or a
1476
+ // background completion notification) to enqueue more input. Drain once
1477
+ // immediately before provider.send so that input is included in the very
1478
+ // next request instead of sitting in the queue until after the model has
1479
+ // already answered the pre-compaction prompt.
1480
+ drainSteeringIntoMessages('pre-send');
1481
+ const nextIteration = iterations + 1;
1482
+ opts.iteration = nextIteration;
1483
+ opts.providerState = providerState;
1484
+ if (forcedFirstTool && toolCallsTotal === 0) {
1485
+ opts.toolChoice = 'required';
1486
+ } else {
1487
+ delete opts.toolChoice;
1488
+ }
1489
+ const sendTools = forcedFirstToolDef && toolCallsTotal === 0 ? [forcedFirstToolDef] : tools;
1490
+ // Eager-dispatch queue: when the provider streams a tool-call event,
1491
+ // start read-only tools immediately so execution overlaps with the
1492
+ // remaining SSE parse. Writes and unknown tools wait until send()
1493
+ // returns and run serially in the call-order loop below.
1494
+ const pending = new Map();
1495
+ // Streaming-time intra-turn dedup. When the LLM emits two
1496
+ // tool_use blocks with identical (name, args) signatures in
1497
+ // sequence, the provider's onToolCall fires for both BEFORE
1498
+ // the iter for-body runs, so the batch-level pre-pass would be
1499
+ // too late to prevent the eager dispatch of the second one.
1500
+ // Track signatures of in-flight eager calls and skip starting a
1501
+ // second one for the same sig. The duplicate's executeTool is
1502
+ // never invoked; the for-body's pre-pass marks it as a duplicate
1503
+ // and emits a stub tool_result. The sig is NOT cleared when the
1504
+ // eager promise settles (see finally below): a streaming onToolCall
1505
+ // can deliver a same-turn identical call AFTER the first promise
1506
+ // settles but BEFORE the deferred cache set (:1256), and the static
1507
+ // pre-pass (:909) only runs after send() returns — so clearing the
1508
+ // sig on settle would let that second streaming eager call
1509
+ // re-execute. A fresh Map() is created per turn, so the sig set
1510
+ // resets at the turn boundary without leaking across iterations.
1511
+ const _eagerInFlightSigs = new Map();
1512
+ let _mutationEpoch = 0;
1513
+ const startEagerTool = (call) => {
1514
+ if (!call?.id || pending.has(call.id) || !isEagerDispatchable(call.name, tools)) return null;
1515
+ const _sig = _intraTurnSig(call.name, call.arguments);
1516
+ if (_eagerInFlightSigs.has(_sig)) return null;
1517
+ // Repeat-failure guard also gates eager dispatch (reviewer-flagged):
1518
+ // streaming onToolCall / startEagerRun would otherwise re-run an
1519
+ // identical read-only call that already failed REPEAT_FAIL_LIMIT
1520
+ // times before the serial for-body guard runs. Returning null here
1521
+ // lets the serial body push the [repeat-failure-guard] stub.
1522
+ {
1523
+ const _rfg = sessionRef?._repeatFailGuard;
1524
+ if (_rfg && _rfg.sig === _sig && _rfg.count >= REPEAT_FAIL_LIMIT) return null;
1525
+ }
1526
+ const toolKind = getToolKind(call.name);
1527
+ // Shared pre-dispatch deny: identical predicate runs in the
1528
+ // serial path below. If any role/permission guard would reject
1529
+ // this call there, never start it eagerly here.
1530
+ if (_preDispatchDeny(call, toolKind, sessionRef) !== null) return null;
1531
+ const entry = { startedAt: Date.now(), endedAt: null, mutationEpoch: _mutationEpoch };
1532
+ _eagerInFlightSigs.set(_sig, call.id);
1533
+ entry.promise = (async () => {
1534
+ try {
1535
+ const permBlocked = _checkWorkerPermission(call.name, call.arguments, sessionRef);
1536
+ if (permBlocked !== null) return { ok: true, value: permBlocked };
1537
+ return { ok: true, value: await executeTool(call.name, call.arguments, cwd, sessionId, sessionRef, { toolCallId: call.id, signal }) };
1538
+ } catch (error) {
1539
+ return { ok: false, error };
1540
+ }
1541
+ })()
1542
+ .finally(() => {
1543
+ entry.endedAt = Date.now();
1544
+ // Intentionally do NOT delete _sig here — see the block
1545
+ // comment above. The sig must outlive promise settlement
1546
+ // so a later same-turn streaming duplicate stays blocked
1547
+ // at the _eagerInFlightSigs.has(_sig) guard until the turn
1548
+ // boundary recreates the Map.
1549
+ });
1550
+ pending.set(call.id, entry);
1551
+ return entry;
1552
+ };
1553
+ const startEagerRun = (calls, startIndex, dupSet) => {
1554
+ for (let j = startIndex; j < calls.length; j += 1) {
1555
+ const call = calls[j];
1556
+ if (!call?.id || !isEagerDispatchable(call.name, tools)) break;
1557
+ if (dupSet && dupSet.has(call.id)) continue;
1558
+ if (!startEagerTool(call) && !pending.has(call.id)) break;
1559
+ }
1560
+ };
1561
+ let _streamEagerBlocked = false;
1562
+ opts.onToolCall = (call) => {
1563
+ if (!isEagerDispatchable(call?.name, tools)) {
1564
+ _streamEagerBlocked = true;
1565
+ return;
1566
+ }
1567
+ if (_streamEagerBlocked) return;
1568
+ startEagerTool(call);
1569
+ };
1570
+ // Repair any dangling assistant tool_use left over from a prior
1571
+ // abort/error path before the provider sees the transcript. No-op
1572
+ // on the healthy iteration cycle (every assistant tool_use is
1573
+ // followed by tool results in the same loop body below).
1574
+ _ensureTranscriptPairing(messages, sessionId);
1575
+ // Strip soft-warn markers from prior tool results before the next
1576
+ // send. Marker bytes (Tool-budget(xN), Same-file reads(xN), etc.)
1577
+ // mutate every turn with dynamic counters, so leaving them in the
1578
+ // transcript breaks server-side prefix cache lookup on later turns.
1579
+ // The current turn's marker (if any) is appended AFTER this strip,
1580
+ // so the model still sees the self-correct hint on its own iteration.
1581
+ for (let _i = 0; _i < messages.length; _i++) {
1582
+ const _m = messages[_i];
1583
+ if (_m && _m.role === 'tool' && typeof _m.content === 'string' && _m.content.includes('⚠')) {
1584
+ const _stripped = stripSoftWarns(_m.content);
1585
+ if (_stripped !== _m.content) _m.content = _stripped;
1586
+ }
1587
+ }
1588
+ const sendStartedAt = Date.now();
1589
+ try {
1590
+ response = await provider.send(messages, model, sendTools.length ? sendTools : undefined, opts);
1591
+ } catch (sendErr) {
1592
+ // Context-window-exceeded is a deterministic refusal: the request is
1593
+ // simply too large. Retry ONCE with a stricter budget using the
1594
+ // same summary-based compaction path. It never falls back to
1595
+ // destructive trim/drop: if system + current turn + compact marker
1596
+ // cannot fit, surface overflow. Unrelated errors (network, stall,
1597
+ // auth, etc.) re-throw untouched — they are handled by the
1598
+ // provider/bridge retry layers.
1599
+ if (
1600
+ !isContextOverflowError(sendErr)
1601
+ || !(sessionRef && typeof sessionRef.contextWindow === 'number')
1602
+ ) {
1603
+ throw sendErr;
1604
+ }
1605
+ const overflowPolicy = resolveWorkerCompactPolicy(sessionRef, sendTools.length ? sendTools : tools);
1606
+ const overflowBase = overflowPolicy?.boundaryTokens || sessionRef.contextWindow;
1607
+ const overflowStrictBudget = Math.floor(overflowBase * OVERFLOW_RETRY_COMPACT_PERCENT);
1608
+ const overflowBudget = Math.max(1, Math.min(
1609
+ overflowBase,
1610
+ overflowPolicy?.triggerTokens || overflowBase,
1611
+ overflowStrictBudget || overflowBase,
1612
+ ));
1613
+ const overflowReserve = overflowPolicy?.reserveTokens || estimateRequestReserveTokens(sendTools.length ? sendTools : tools);
1614
+ const beforeCount = messages.length;
1615
+ let beforeBytes = null;
1616
+ try { beforeBytes = Buffer.byteLength(JSON.stringify(messages), 'utf8'); } catch { beforeBytes = null; }
1617
+ const messageTokensEst = estimateMessagesTokensSafe(messages);
1618
+ let recompacted;
1619
+ try {
1620
+ recompacted = compactMessages(messages, overflowBudget, { reserveTokens: overflowReserve, force: true });
1621
+ } catch (compactErr) {
1622
+ // Same narrow active-turn fallback as the pre-send path: when
1623
+ // system + the whole current turn overflow even the stricter
1624
+ // overflow-retry budget, shrink older same-turn tool outputs /
1625
+ // drop older same-turn groups before surfacing overflow. Throws
1626
+ // (caught below) when even the floor cannot fit, preserving the
1627
+ // overflow error behavior.
1628
+ if (overflowPolicy?.activeTurnFallback) {
1629
+ try {
1630
+ recompacted = compactActiveTurn(messages, overflowBudget, { reserveTokens: overflowReserve, force: true });
1631
+ try {
1632
+ process.stderr.write(
1633
+ `[loop] active-turn fallback compaction on overflow retry ` +
1634
+ `(sess=${sessionId || 'unknown'} iter=${nextIteration}): ` +
1635
+ `${compactErr?.message || compactErr}\n`,
1636
+ );
1637
+ } catch { /* best-effort */ }
1638
+ } catch { recompacted = undefined; }
1639
+ }
1640
+ if (!recompacted) {
1641
+ traceBridgeCompact({
1642
+ sessionId,
1643
+ iteration: nextIteration,
1644
+ stage: 'overflow_retry',
1645
+ prune_count: 0,
1646
+ compact_changed: false,
1647
+ input_prefix_hash: messagePrefixHash(messages),
1648
+ before_count: beforeCount,
1649
+ after_count: messages.length,
1650
+ before_bytes: beforeBytes,
1651
+ after_bytes: beforeBytes,
1652
+ context_window: overflowPolicy?.contextWindow || sessionRef.contextWindow,
1653
+ budget_tokens: overflowBudget,
1654
+ reserve_tokens: overflowReserve,
1655
+ message_tokens_est: messageTokensEst,
1656
+ provider: sessionRef.provider,
1657
+ model: sessionRef.model || model,
1658
+ error: compactErr && compactErr.message ? compactErr.message : String(compactErr),
1659
+ error_code: 'BRIDGE_CONTEXT_OVERFLOW',
1660
+ });
1661
+ throw bridgeContextOverflowError({
1662
+ stage: 'overflow_retry',
1663
+ sessionId,
1664
+ sessionRef,
1665
+ model,
1666
+ budgetTokens: overflowBudget,
1667
+ reserveTokens: overflowReserve,
1668
+ messageTokensEst,
1669
+ }, compactErr);
1670
+ }
1671
+ }
1672
+ const compactChanged = messagesArrayChanged(messages, recompacted);
1673
+ const pruneCount = Math.max(beforeCount - recompacted.length, 0);
1674
+ messages.length = 0;
1675
+ messages.push(...recompacted);
1676
+ let afterBytes = null;
1677
+ try { afterBytes = Buffer.byteLength(JSON.stringify(messages), 'utf8'); } catch { afterBytes = null; }
1678
+ traceBridgeCompact({
1679
+ sessionId,
1680
+ iteration: nextIteration,
1681
+ stage: 'overflow_retry',
1682
+ prune_count: pruneCount,
1683
+ compact_changed: compactChanged,
1684
+ input_prefix_hash: messagePrefixHash(messages),
1685
+ before_count: beforeCount,
1686
+ after_count: messages.length,
1687
+ before_bytes: beforeBytes,
1688
+ after_bytes: afterBytes,
1689
+ context_window: overflowPolicy?.contextWindow || sessionRef.contextWindow,
1690
+ budget_tokens: overflowBudget,
1691
+ reserve_tokens: overflowReserve,
1692
+ message_tokens_est: messageTokensEst,
1693
+ provider: sessionRef.provider,
1694
+ model: sessionRef.model || model,
1695
+ });
1696
+ rememberCompactTelemetry(sessionRef, overflowPolicy, {
1697
+ stage: 'overflow_retry',
1698
+ beforeTokens: messageTokensEst,
1699
+ afterTokens: estimateMessagesTokensSafe(messages),
1700
+ compactChanged,
1701
+ pruneCount,
1702
+ });
1703
+ // The transcript prefix changed; the server-side conversation anchor
1704
+ // (previous_response_id / WS continuation) is now invalid. Drop
1705
+ // providerState so the retry starts a fresh chain instead of
1706
+ // tripping a silent cache miss or hard mismatch.
1707
+ providerState = undefined;
1708
+ opts.providerState = undefined;
1709
+ // Drop eager-dispatch state before the retry send. A tool_use
1710
+ // streamed by the failed first send could otherwise orphan its
1711
+ // eager result or be double-dispatched; force the retry's tools
1712
+ // through the serial post-send path with a clean matching slate.
1713
+ opts.onToolCall = undefined;
1714
+ pending.clear();
1715
+ _eagerInFlightSigs.clear();
1716
+ try {
1717
+ process.stderr.write(
1718
+ `[loop] context overflow on send (sess=${sessionId || 'unknown'} iter=${nextIteration}); ` +
1719
+ `retrying once at budget=${overflowBudget} reserve=${overflowReserve} ` +
1720
+ `messages=${messages.length}\n`,
1721
+ );
1722
+ } catch { /* best-effort */ }
1723
+ response = await provider.send(messages, model, sendTools.length ? sendTools : undefined, opts);
1724
+ }
1725
+ opts.onToolCall = undefined;
1726
+ // Capture opaque state for the next turn (may be undefined — that's
1727
+ // the stateless contract for providers that don't use continuation).
1728
+ providerState = response?.providerState ?? undefined;
1729
+ iterations = nextIteration;
1730
+ traceBridgeLoop({
1731
+ sessionId,
1732
+ iteration: iterations,
1733
+ sendMs: Date.now() - sendStartedAt,
1734
+ messageCount: Array.isArray(messages) ? messages.length : 0,
1735
+ bodyBytesEst: estimateProviderPayloadBytes(messages, model, sendTools),
1736
+ });
1737
+ // Accumulate usage across iterations — every billable slot, not just
1738
+ // input/output. Anthropic cache_read/cache_write typically stay 0 on
1739
+ // the first iteration and surge on later ones (warm prefix reuse),
1740
+ // so aggregating only the head would silently drop most of the
1741
+ // cache-side tokens.
1742
+ if (response.usage) {
1743
+ const hadUsage = !!lastUsage;
1744
+ lastUsage = addUsage(lastUsage, response.usage);
1745
+ if (!hadUsage) {
1746
+ // Snapshot the first turn separately so callers can show
1747
+ // iter1 vs final cache-hit ratios — first iter is the
1748
+ // warm-prefix signal, final iter is the steady-state
1749
+ // efficiency signal after tool-result accumulation.
1750
+ firstTurnUsage = { ...lastUsage };
1751
+ }
1752
+ }
1753
+ // Provider may have returned despite an abort (SDKs that don't honour
1754
+ // signal) — bail before processing any of its output.
1755
+ throwIfAborted();
1756
+ // Incremental metric persistence (fix A): push per-iteration token delta
1757
+ // immediately so watchdog / bridge type=list sees live totals mid-turn.
1758
+ if (sessionId && opts.onUsageDelta && response.usage) {
1759
+ try {
1760
+ opts.onUsageDelta({
1761
+ sessionId,
1762
+ iterationIndex: iterations,
1763
+ deltaInput: response.usage.inputTokens || 0,
1764
+ deltaOutput: response.usage.outputTokens || 0,
1765
+ deltaPrompt: response.usage.promptTokens || 0,
1766
+ // Cache delta carried alongside input/output so live metrics
1767
+ // reflect the same token classes the terminal aggregate adds;
1768
+ // additive — callers that ignore these fields keep working.
1769
+ deltaCachedRead: response.usage.cachedTokens || 0,
1770
+ deltaCacheWrite: response.usage.cacheWriteTokens || 0,
1771
+ ts: Date.now(),
1772
+ });
1773
+ } catch { /* best-effort — never break the loop */ }
1774
+ }
1775
+ // No tool calls. For PUBLIC bridge agents, the bridge contract
1776
+ // (rules/bridge/00-common.md) requires either a tool call or a
1777
+ // `<final-answer>` wrapped reply.
1778
+ // A text-only turn without those tags violates the contract (e.g.
1779
+ // Opus 4.6 emits 'Now I'll polish…' preamble before its first tool
1780
+ // call) and used to leave the session idle until the idle sweep
1781
+ // collected it. Re-prompt the worker with a contract reminder; cap
1782
+ // at 2 nudges so a model that never complies still terminates the
1783
+ // loop. Hidden roles (cycle1-agent / cycle2-agent / explorer /
1784
+ // scheduler-task / webhook-handler) are exempt:
1785
+ // their own role rules define a different output contract (pipe-
1786
+ // separated chunker output, structured pipe-format, etc.) and a
1787
+ // text-only terminal turn is the correct shape — nudging them
1788
+ // produces a contradictory user message that traps the model in a
1789
+ // tool-call-blocked vs contract-required oscillation.
1790
+ if (!response.toolCalls?.length) {
1791
+ // No tool calls. Decide between final-answer accept vs nudge.
1792
+ // - has content + non-hidden role → valid final, break.
1793
+ // - empty content + hidden role → contract allows text-only
1794
+ // terminal turn, break.
1795
+ // - empty content + non-hidden role → one soft nudge. Repeated
1796
+ // reminders waste turns and fragment the working context, so
1797
+ // the second empty turn is accepted as terminal.
1798
+ const hasContent = typeof response.content === 'string' && response.content.trim().length > 0;
1799
+ const isHidden = HIDDEN_ROLE_NAMES.has(sessionRole);
1800
+ const stopReason = response.stopReason ?? response.stop_reason ?? null;
1801
+ const isIncompleteStop = stopReason && INCOMPLETE_STOP_REASONS.has(stopReason);
1802
+ if (!hasContent && !isHidden) {
1803
+ if (contractNudges >= 1) break;
1804
+ contractNudges += 1;
1805
+ let nudgeMsg;
1806
+ if (isIncompleteStop) {
1807
+ nudgeMsg = `[mixdog-runtime] Previous turn ended mid-synthesis (stopReason=${stopReason}) with empty content. Continue — emit <final-answer>...</final-answer> with your synthesis so far, or call more tools to finish.`;
1808
+ } else {
1809
+ nudgeMsg = '[mixdog-runtime] Your previous response was empty (no <final-answer> tag and no tool call). Either emit your final answer wrapped in <final-answer>...</final-answer> tags, or continue with tool calls. Do not return an empty turn.';
1810
+ }
1811
+ messages.push({ role: 'user', content: nudgeMsg });
1812
+ continue;
1813
+ }
1814
+ break;
1815
+ }
1816
+ const calls = response.toolCalls;
1817
+ toolCallsTotal += calls.length;
1818
+ // Per-turn batch shape — one row per assistant turn so trace
1819
+ // consumers can derive multi-tool adoption ratio without scanning
1820
+ // every assistant message body.
1821
+ recordToolBatch(sessionId, calls.length);
1822
+ await Promise.resolve(onToolCall?.(iterations, calls));
1823
+ // Append assistant message with tool calls. reasoningItems is the
1824
+ // OpenAI Responses API replay payload (encrypted_content blobs);
1825
+ // providers that ignore it just see an extra field and drop it,
1826
+ // openai-oauth.convertMessagesToResponsesInput emits matching
1827
+ // type:'reasoning' input items on the next turn to keep the Codex
1828
+ // server-side cache prefix stable.
1829
+ const _assistantTurnMsg = {
1830
+ role: 'assistant',
1831
+ content: response.content || '',
1832
+ toolCalls: compactToolCallsForHistory(calls),
1833
+ ...(Array.isArray(response.reasoningItems) && response.reasoningItems.length
1834
+ ? { reasoningItems: response.reasoningItems }
1835
+ : {}),
1836
+ ...(typeof response.reasoningContent === 'string' && response.reasoningContent
1837
+ ? { reasoningContent: response.reasoningContent }
1838
+ : {}),
1839
+ };
1840
+ messages.push(_assistantTurnMsg);
1841
+ // Execute each tool and append results.
1842
+ //
1843
+ // Intra-turn duplicate suppression: when an LLM emits two tool_use
1844
+ // blocks with identical (name, args) inside the SAME assistant turn,
1845
+ // re-executing wastes tokens. Restricted to tools with
1846
+ // `readOnlyHint:true` (= isEagerDispatchable) — bash/apply_patch
1847
+ // may be intentional repeats with distinct side effects.
1848
+ // Pre-pass identifies duplicates BEFORE startEagerRun so eager
1849
+ // dispatch also skips them, not just the for-body.
1850
+ const _duplicateCallIds = new Set();
1851
+ const _dupFirstId = new Map();
1852
+ {
1853
+ const _firstIdBySig = new Map();
1854
+ for (const c of calls) {
1855
+ if (!c?.id) continue;
1856
+ if (!isEagerDispatchable(c.name, tools)) {
1857
+ _firstIdBySig.clear();
1858
+ continue;
1859
+ }
1860
+ const sig = _intraTurnSig(c.name, c.arguments);
1861
+ const first = _firstIdBySig.get(sig);
1862
+ if (first === undefined) {
1863
+ _firstIdBySig.set(sig, c.id);
1864
+ } else {
1865
+ _duplicateCallIds.add(c.id);
1866
+ _dupFirstId.set(c.id, first);
1867
+ }
1868
+ }
1869
+ }
1870
+ // R15: per-turn scalar read-count Map. Lifetime = this turn's tool-call batch.
1871
+ // Declared between the duplicate-detection block and the for-loop so it resets
1872
+ for (let callIndex = 0; callIndex < calls.length; callIndex += 1) {
1873
+ const call = calls[callIndex];
1874
+ if (isBuiltinTool(call.name)) {
1875
+ call.name = canonicalizeBuiltinToolName(call.name);
1876
+ }
1877
+ if (_duplicateCallIds.has(call.id)) {
1878
+ const _firstId = _dupFirstId.get(call.id);
1879
+ const _stub = `[intra-turn-dedup] identical read-only \`${call.name}\` call was already executed in this same assistant turn as tool_use_id=${_firstId}. The first call's tool_result is in context immediately above; skipping re-execution to save tokens. If you needed a different slice of the file, narrow the next call (different path / offset / limit / pattern) so it has a distinct signature.`;
1880
+ pushToolResultMessage({
1881
+ role: 'tool',
1882
+ content: _stub,
1883
+ toolCallId: call.id,
1884
+ });
1885
+ continue;
1886
+ }
1887
+ // Cross-iteration repeat-failure guard. Distinct from the
1888
+ // intra-turn dedup above (which spans ONE assistant turn and
1889
+ // resets every turn): when the model re-issues an IDENTICAL
1890
+ // (name,args) call that has already failed REPEAT_FAIL_LIMIT times
1891
+ // in a row across iterations, stop re-executing — the result will
1892
+ // not change, and each retry burns a full (often slow) LLM
1893
+ // round-trip until the hard iteration cap. Steer it to change
1894
+ // approach instead.
1895
+ const _repeatFailSig = _intraTurnSig(call.name, call.arguments);
1896
+ {
1897
+ const _rfg = sessionRef?._repeatFailGuard;
1898
+ if (_rfg && _rfg.sig === _repeatFailSig && _rfg.count >= REPEAT_FAIL_LIMIT) {
1899
+ pushToolResultMessage({
1900
+ role: 'tool',
1901
+ content: `[repeat-failure-guard] This exact \`${call.name}\` call (identical arguments) has already failed ${_rfg.count} times in a row; not re-executing because the result will not change. Change approach: use different arguments, a different tool, or skip this step.`,
1902
+ toolCallId: call.id,
1903
+ });
1904
+ continue;
1905
+ }
1906
+ }
1907
+ if (sessionId) markSessionToolCall(sessionId, call.name);
1908
+ let result;
1909
+ let toolStartedAt;
1910
+ let toolEndedAt;
1911
+ const toolKind = getToolKind(call.name);
1912
+ // Cross-turn read dedup. Mirrors Anthropic Claude Code's
1913
+ // fileReadCache.ts: if the path's stat tuple (mtime/size/ino/dev)
1914
+ // is unchanged since a prior read in THIS session, return the cached
1915
+ // body instead of executing. Both scalar and array/object-array path
1916
+ // forms are cached — keyed by (abs, offset, limit, mode, n) per entry.
1917
+ //
1918
+ // Scoped-tool cache (grep/glob/list + graph lookups): same idea
1919
+ // but keyed by (toolName, canonical args) without per-file stat.
1920
+ // These tools scan many files so a single stat tuple cannot cover
1921
+ // them. The scoped cache registers dependency roots and write-class
1922
+ // tools evict entries whose root contains the touched path.
1923
+ let _readCacheHit = null;
1924
+ let _scopedCacheHit = null;
1925
+ let _executeOk = false;
1926
+ let _resultKind = 'normal';
1927
+ if (sessionId && _isReadTool(call.name)) {
1928
+ _readCacheHit = tryReadCached({ sessionId, args: call.arguments, cwd });
1929
+ } else if (sessionId && _isScopedCacheableTool(call.name)) {
1930
+ _scopedCacheHit = tryScopedToolCached({ sessionId, toolName: _stripMcpPrefix(call.name), args: call.arguments, cwd });
1931
+ }
1932
+ try {
1933
+ if (_readCacheHit !== null) {
1934
+ toolStartedAt = Date.now();
1935
+ toolEndedAt = toolStartedAt;
1936
+ const _body = _readCacheHit.content;
1937
+ // Return the cached body byte-for-byte instead of a
1938
+ // human-readable cache marker. The marker made public
1939
+ // bridge agents treat a successful cached read as a
1940
+ // meta instruction and repeat the same read loop.
1941
+ result = _body;
1942
+ _resultKind = 'cache-hit';
1943
+ _executeOk = true;
1944
+ } else if (_scopedCacheHit !== null) {
1945
+ toolStartedAt = Date.now();
1946
+ toolEndedAt = toolStartedAt;
1947
+ const _body = _scopedCacheHit.content;
1948
+ result = _body;
1949
+ _resultKind = 'scoped-cache-hit';
1950
+ _executeOk = true;
1951
+ } else {
1952
+ // Fallback for providers that don't stream tool calls early:
1953
+ // execute a contiguous read-only run in parallel, but never
1954
+ // cross a write/bash/MCP boundary that may change state.
1955
+ if (isEagerDispatchable(call.name, tools)) {
1956
+ startEagerRun(calls, callIndex, _duplicateCallIds);
1957
+ }
1958
+ let eager = pending.get(call.id);
1959
+ if (eager !== undefined && eager.mutationEpoch < _mutationEpoch) {
1960
+ pending.delete(call.id);
1961
+ eager = undefined;
1962
+ }
1963
+ if (eager !== undefined) {
1964
+ toolStartedAt = eager.startedAt;
1965
+ const settled = await eager.promise;
1966
+ if (!settled.ok) throw settled.error;
1967
+ result = settled.value;
1968
+ toolEndedAt = eager.endedAt ?? Date.now();
1969
+ const _eagerKind = classifyResultKind(result);
1970
+ if (_eagerKind === 'error') {
1971
+ _resultKind = 'error';
1972
+ _executeOk = false;
1973
+ } else {
1974
+ _executeOk = true;
1975
+ }
1976
+ } else {
1977
+ toolStartedAt = Date.now();
1978
+ // Runtime permission guard. Schema profiles may hide
1979
+ // tools for routing efficiency, but this remains the
1980
+ // safety boundary for any tool_use that still reaches
1981
+ // the loop. _preDispatchDeny is the SHARED helper used
1982
+ // by both the eager dispatch path (startEagerTool) and
1983
+ // this serial path — keeps the bridge-owned control-
1984
+ // plane reject, role guards, wrapper guards, and
1985
+ // permission guards consistent across both paths.
1986
+ const _denyMsg = _preDispatchDeny(call, toolKind, sessionRef);
1987
+ if (_denyMsg !== null) {
1988
+ result = _denyMsg;
1989
+ toolEndedAt = Date.now();
1990
+ _resultKind = 'error';
1991
+ } else {
1992
+ const permBlocked = _checkWorkerPermission(call.name, call.arguments, sessionRef);
1993
+ if (permBlocked !== null) {
1994
+ result = permBlocked;
1995
+ toolEndedAt = Date.now();
1996
+ _resultKind = 'error';
1997
+ } else {
1998
+ result = await executeTool(call.name, call.arguments, cwd, sessionId, sessionRef, { toolCallId: call.id, signal });
1999
+ toolEndedAt = Date.now();
2000
+ // Boundary: tool-return string convention → structural kind.
2001
+ // The only prefix check in this codebase; downstream layers
2002
+ // operate on _resultKind.
2003
+ if (classifyResultKind(result) === 'error') {
2004
+ _resultKind = 'error';
2005
+ _executeOk = false;
2006
+ } else {
2007
+ _executeOk = true;
2008
+ }
2009
+ // _resultKind stays 'normal' when tool returned a non-error string.
2010
+ }
2011
+ }
2012
+ }
2013
+ } // close: else branch of _readCacheHit check
2014
+ }
2015
+ catch (err) {
2016
+ if (toolStartedAt === undefined) toolStartedAt = Date.now();
2017
+ toolEndedAt = Date.now();
2018
+ result = `Error: ${err instanceof Error ? err.message : String(err)}`;
2019
+ _resultKind = 'error';
2020
+ }
2021
+ // Update the cross-iteration repeat-failure guard with this call's
2022
+ // outcome: bump the consecutive-failure count for an identical
2023
+ // signature, or clear it the moment the same call succeeds.
2024
+ if (sessionRef) {
2025
+ const _failed = !_executeOk || _resultKind === 'error';
2026
+ if (_failed) {
2027
+ sessionRef._repeatFailGuard = (sessionRef._repeatFailGuard?.sig === _repeatFailSig)
2028
+ ? { sig: _repeatFailSig, count: sessionRef._repeatFailGuard.count + 1 }
2029
+ : { sig: _repeatFailSig, count: 1 };
2030
+ } else if (sessionRef._repeatFailGuard?.sig === _repeatFailSig) {
2031
+ sessionRef._repeatFailGuard = null;
2032
+ }
2033
+ }
2034
+ // A failed executed call keeps its FULL argument body in history so the
2035
+ // model can retry against the original (a large apply_patch `patch`
2036
+ // would otherwise be hidden behind a
2037
+ // `[mixdog compacted …]` placeholder). Restored IMMEDIATELY — not at end
2038
+ // of loop — so an abort or post-processing throw after this point cannot
2039
+ // leave a failed patch compacted. Cache-safe: _assistantTurnMsg is not
2040
+ // transmitted until the next provider.send. Early-continue paths (dedup /
2041
+ // repeat-failure-guard) never reach here and stay compacted.
2042
+ if ((!_executeOk || _resultKind === 'error') && call?.id) {
2043
+ restoreToolCallBodyForId(_assistantTurnMsg, calls, call.id);
2044
+ }
2045
+ // Cross-turn cache maintenance — gate on both _executeOk and _resultKind==='normal'.
2046
+ // _executeOk=false catches permission-blocked / catch-path / partial-fail results.
2047
+ // _resultKind==='normal' ensures cache-hit refs are never re-stored (structural,
2048
+ // no prefix sniffing).
2049
+ // NOTE: setReadCached / setScopedToolCached are deferred below (after
2050
+ // compressToolResult) so the cache holds the same content as conversation
2051
+ // history. Cache-hit refs point to a tool_use_id whose message body matches
2052
+ // exactly what's stored — no phantom full body.
2053
+ if (sessionId && _executeOk && _resultKind === 'normal') {
2054
+ const _toolBare = _stripMcpPrefix(call.name);
2055
+ if (_readCacheHit === null && _isReadTool(call.name)) {
2056
+ // Post-patch advisory: handle BOTH scalar and array forms
2057
+ // of args.path. The array form (path:[a,b,c] or
2058
+ // path:[{path:a},{path:b}]) was a coverage gap in R1 —
2059
+ // an LLM that patches X then reads [X,Y] should still see
2060
+ // the advisory for X.
2061
+ const _argsPath = call.arguments?.path;
2062
+ const _pathList = [];
2063
+ if (typeof _argsPath === 'string') {
2064
+ _pathList.push(_argsPath);
2065
+ } else if (typeof call.arguments?.file_path === 'string') {
2066
+ _pathList.push(call.arguments.file_path);
2067
+ } else if (Array.isArray(_argsPath)) {
2068
+ for (const _item of _argsPath) {
2069
+ if (typeof _item === 'string') _pathList.push(_item);
2070
+ else if (_item && typeof _item === 'object') {
2071
+ const _itemPath = typeof _item.path === 'string' ? _item.path : _item.file_path;
2072
+ if (typeof _itemPath === 'string') _pathList.push(_itemPath);
2073
+ }
2074
+ }
2075
+ }
2076
+ const _marks = [];
2077
+ for (const _p of _pathList) {
2078
+ const _m = consumePostEditMark({ sessionId, path: _p, cwd });
2079
+ if (_m) _marks.push({ path: _p, mark: _m });
2080
+ }
2081
+ } else if (_toolBare === 'apply_patch') {
2082
+ // apply_patch's args are a unified-diff text in `patch`
2083
+ // (resolved against `base_path` or cwd). Parse the diff
2084
+ // headers (`--- a/path` / `+++ b/path`) to extract the
2085
+ // touched paths and invalidate / mark each one. Falls
2086
+ // back to a full session clear only when no paths could
2087
+ // be parsed (malformed diff or unknown format).
2088
+ const _argsBase = call.arguments?.base_path;
2089
+ const _patchBase = (typeof _argsBase === 'string' && _argsBase.length > 0)
2090
+ ? (isAbsolute(_argsBase) ? _argsBase : resolvePath(cwd || process.cwd(), _argsBase))
2091
+ : (cwd || process.cwd());
2092
+ const _touched = extractTouchedPathsFromPatch(call.arguments?.patch);
2093
+ if (_touched.length > 0) {
2094
+ for (const _p of _touched) {
2095
+ invalidatePathForSession(sessionId, _p, _patchBase);
2096
+ markPostEdit({ sessionId, path: _p, cwd: _patchBase, toolName: 'apply_patch' });
2097
+ // R20: cross-dispatch prefetch cache invalidation.
2098
+ invalidatePrefetchCache(_p, _patchBase);
2099
+ }
2100
+ } else {
2101
+ clearReadDedupSession(sessionId);
2102
+ // R20: path unknown — can't target; no-op on prefetch cache
2103
+ // (stat-validation at lookup time will naturally reject stale entries).
2104
+ }
2105
+ // Targeted scoped-cache invalidation: only evict entries whose
2106
+ // dep paths intersect the touched set. Full wipe is the fallback
2107
+ // when no paths were extracted (D).
2108
+ if (_touched.length > 0) {
2109
+ clearScopedToolsForSessionPaths(sessionId, _touched, _patchBase);
2110
+ } else {
2111
+ clearScopedToolsForSession(sessionId);
2112
+ }
2113
+ }
2114
+ } // end _executeOk+_resultKind gate (scoped tool cache set)
2115
+ // E: mutation tools (apply_patch) must invalidate caches
2116
+ // even on returned-error/partial-fail — the file state is unknown after
2117
+ // an error exit, and some tools report failure as an Error: result string
2118
+ // rather than throwing.
2119
+ // This block runs unconditionally (not gated on _executeOk or _resultKind).
2120
+ if (sessionId && (!_executeOk || _resultKind === 'error') && _stripMcpPrefix(call.name) === 'apply_patch') {
2121
+ clearReadDedupSession(sessionId);
2122
+ }
2123
+ if (_isMutationTool(call.name)) {
2124
+ _mutationEpoch += 1;
2125
+ }
2126
+ // Bash always clears scoped cache UNCONDITIONALLY — a mutating bash
2127
+ // that throws or fails partway can still leave stale find_symbol / grep entries.
2128
+ // Must not be gated on _executeOk or _resultKind.
2129
+ if (sessionId && _isShellTool(call.name)) {
2130
+ clearScopedToolsForSession(sessionId);
2131
+ }
2132
+ // R17 compression pipeline — correct ordering (compress → cache → push):
2133
+ // 1. compressToolResult: lossless ANSI/dedup/separator passes.
2134
+ // 2. setReadCached / setScopedToolCached: cache stores the SAME result that
2135
+ // goes into conversation history. Cache-hit refs point to the tool_use_id
2136
+ // whose message body matches — no phantom full body.
2137
+ // 3. offload → hint → message push.
2138
+ // Offload FIRST — before compress. Large RAW output goes to a disk sidecar
2139
+ // + ~2K preview before any in-place shrink (lossless compress) can reduce
2140
+ // it below the offload threshold and pre-empt the sidecar. When offload
2141
+ // fires it replaces `result` with a short preview stub (<2K) referencing
2142
+ // the on-disk path; the later compress is a no-op on that stub. compress
2143
+ // then only touches output that stayed inline (<= threshold).
2144
+ // Per-tool post-processing backstop. The executeTool try/catch
2145
+ // above terminates BEFORE offload/compress/trim/hint/cache writes/
2146
+ // trace/messages.push, so a maybeOffloadToolResult rejection (or
2147
+ // any downstream throw) would otherwise leave the assistant
2148
+ // tool_use message with no matching tool result. Wrap the whole
2149
+ // post-processing window through messages.push() in a catch; on
2150
+ // failure push a synthetic Error: tool result for this call.id
2151
+ // and skip the cache writes for it.
2152
+ let _postProcessOk = true;
2153
+ try {
2154
+ // Offload thresholds are keyed by BARE tool name
2155
+ // (INLINE_THRESHOLD_BY_TOOL: grep=20k, bash=30k, read=Infinity, ...),
2156
+ // so strip the MCP prefix exactly as the cache write below does.
2157
+ // Otherwise an mcp__..__grep name misses its 20k grep cap and
2158
+ // silently falls back to the 50k default — per-tool limits ignored.
2159
+ const _toolBare = _stripMcpPrefix(call.name);
2160
+ result = await maybeOffloadToolResult(sessionId, call.id, _toolBare, result);
2161
+ result = compressToolResult(call.name, call.arguments, result, { sessionId, toolKind });
2162
+ traceBridgeTool({
2163
+ sessionId,
2164
+ iteration: iterations,
2165
+ toolName: call.name,
2166
+ toolKind,
2167
+ toolMs: toolEndedAt - toolStartedAt,
2168
+ toolArgs: call.arguments,
2169
+ role: sessionRef?.role || null,
2170
+ model: sessionRef?.model || null,
2171
+ resultKind: _resultKind,
2172
+ resultText: result,
2173
+ cwd,
2174
+ });
2175
+ // Cache stores run AFTER compress+trim+offload+hint AND after all other
2176
+ // post-processing (trace) so stored content == history content. Placing
2177
+ // the cache writes immediately before messages.push ensures ANY throw
2178
+ // earlier in post-processing skips the cache entirely — no stale or
2179
+ // partial result is ever cached. Cache-hit refs pointing to an offloaded
2180
+ // tool_use will show the offload stub; LLM can still recover the full
2181
+ // body via the disk path in that stub.
2182
+ if (sessionId && _executeOk && _resultKind === 'normal') {
2183
+ if (_scopedCacheHit === null && _isScopedCacheableTool(call.name)) {
2184
+ const _outcome = sessionRef?._scopedCacheOutcomeByCallId?.get(call.id);
2185
+ setScopedToolCached({
2186
+ sessionId,
2187
+ toolName: _toolBare,
2188
+ args: call.arguments,
2189
+ cwd,
2190
+ content: result,
2191
+ toolUseId: call.id,
2192
+ complete: _outcome ? _outcome.complete : true,
2193
+ });
2194
+ sessionRef?._scopedCacheOutcomeByCallId?.delete(call.id);
2195
+ }
2196
+ if (_readCacheHit === null && _isReadTool(call.name)) {
2197
+ // Pass tool_use id so future cache-hits can reference the body's location in history.
2198
+ setReadCached({ sessionId, args: call.arguments, cwd, content: result, toolUseId: call.id });
2199
+ }
2200
+ }
2201
+ pushToolResultMessage({
2202
+ role: 'tool',
2203
+ content: result,
2204
+ toolCallId: call.id,
2205
+ toolKind: _resultKind,
2206
+ });
2207
+ } catch (postErr) {
2208
+ _postProcessOk = false;
2209
+ // Post-processing failed AFTER a successful exec: the result is
2210
+ // replaced with an error below, so preserve this call's full body
2211
+ // too for a clean retry (mirrors the failed-exec path above).
2212
+ if (call?.id) restoreToolCallBodyForId(_assistantTurnMsg, calls, call.id);
2213
+ const _postMsg = `Error: tool result post-processing failed for "${call.name}": ${postErr instanceof Error ? postErr.message : String(postErr)}`;
2214
+ traceBridgeToolFailure({
2215
+ sessionId,
2216
+ iteration: iterations,
2217
+ toolName: call.name,
2218
+ toolKind,
2219
+ toolMs: toolEndedAt && toolStartedAt ? toolEndedAt - toolStartedAt : null,
2220
+ toolArgs: call.arguments,
2221
+ role: sessionRef?.role || null,
2222
+ model: sessionRef?.model || null,
2223
+ cwd,
2224
+ resultText: _postMsg,
2225
+ resultKind: 'error',
2226
+ });
2227
+ // Always emit a matching tool result so the assistant
2228
+ // tool_use isn't orphaned. Cache writes are placed at the
2229
+ // end of the try block (immediately before messages.push),
2230
+ // so ANY throw in post-processing reaches this catch before
2231
+ // the cache is written — stale/partial results are never
2232
+ // cached. The next read on the same path/scope re-executes
2233
+ // naturally.
2234
+ pushToolResultMessage({
2235
+ role: 'tool',
2236
+ content: _postMsg,
2237
+ toolCallId: call.id,
2238
+ toolKind: 'error',
2239
+ });
2240
+ }
2241
+ // Soft-cancel after each tool: if close landed during execution,
2242
+ // discard the rest of the batch and skip the next provider.send.
2243
+ throwIfAborted();
2244
+ }
2245
+ // ── Mid-turn steering (Claude Code / pi agent-loop parity) ──
2246
+ // Before re-sending with the tool results, drain any user / `bridge
2247
+ // type=send` messages that arrived WHILE this tool batch was in
2248
+ // flight and inject them as user turns. This is the difference
2249
+ // between "steer" (interrupt now) and "followUp" (wait for the turn
2250
+ // to finish): previously every injected message was held until the
2251
+ // entire askSession turn completed (manager.mjs _pendingTail drain),
2252
+ // so a user typing mid-task only got picked up after the agent had
2253
+ // already run to completion. Mirrors pi/agent-loop.ts:253 +
2254
+ // 182-190 — steering messages land right before the next assistant
2255
+ // response so the model sees them on its very next iteration.
2256
+ drainSteeringIntoMessages('mid-turn');
2257
+ // About to re-send with tool results — transition back to connecting for the next turn.
2258
+ if (sessionId) updateSessionStage(sessionId, 'connecting');
2259
+ }
2260
+ return {
2261
+ ...response,
2262
+ usage: lastUsage || response.usage,
2263
+ lastTurnUsage: response.usage,
2264
+ firstTurnUsage: firstTurnUsage || response.usage,
2265
+ iterations,
2266
+ toolCallsTotal,
2267
+ providerState,
2268
+ };
2269
+ }