mixdog 0.7.18 → 0.8.0

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