mixdog 0.9.1 → 0.9.3

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 (301) hide show
  1. package/package.json +9 -1
  2. package/scripts/_bench-cwc.json +20 -0
  3. package/scripts/agent-loop-policy-test.mjs +37 -0
  4. package/scripts/agent-parallel-smoke.mjs +54 -10
  5. package/scripts/anthropic-maxtokens-test.mjs +119 -0
  6. package/scripts/background-task-meta-smoke.mjs +1 -1
  7. package/scripts/bench-run.mjs +262 -0
  8. package/scripts/build-tui.mjs +13 -1
  9. package/scripts/compact-smoke.mjs +12 -0
  10. package/scripts/compact-trigger-migration-smoke.mjs +67 -1
  11. package/scripts/explore-bench.mjs +124 -0
  12. package/scripts/hook-bus-test.mjs +191 -0
  13. package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
  14. package/scripts/internal-comms-bench.mjs +727 -0
  15. package/scripts/internal-comms-smoke.mjs +75 -0
  16. package/scripts/lead-workflow-smoke.mjs +4 -4
  17. package/scripts/live-worker-smoke.mjs +9 -9
  18. package/scripts/output-style-bench.mjs +285 -0
  19. package/scripts/output-style-smoke.mjs +13 -10
  20. package/scripts/patch-replay.mjs +90 -0
  21. package/scripts/path-suffix-test.mjs +57 -0
  22. package/scripts/provider-stream-stall-test.mjs +276 -0
  23. package/scripts/provider-toolcall-test.mjs +599 -1
  24. package/scripts/recall-bench.mjs +207 -0
  25. package/scripts/routing-corpus.mjs +281 -0
  26. package/scripts/session-bench.mjs +1526 -0
  27. package/scripts/session-diag.mjs +595 -0
  28. package/scripts/task-bench.mjs +207 -0
  29. package/scripts/tool-failures.mjs +6 -6
  30. package/scripts/tool-smoke.mjs +310 -67
  31. package/scripts/toolcall-args-test.mjs +81 -0
  32. package/src/agents/debugger/AGENT.md +4 -4
  33. package/src/agents/heavy-worker/AGENT.md +20 -9
  34. package/src/agents/reviewer/AGENT.md +4 -4
  35. package/src/agents/worker/AGENT.md +17 -9
  36. package/src/app.mjs +10 -6
  37. package/src/defaults/{hidden-roles.json → agents.json} +7 -7
  38. package/src/examples/schedules/SCHEDULE.example.md +32 -0
  39. package/src/examples/webhooks/WEBHOOK.example.md +40 -0
  40. package/src/headless-role.mjs +14 -14
  41. package/src/help.mjs +1 -0
  42. package/src/lib/rules-builder.cjs +32 -54
  43. package/src/mixdog-session-runtime.mjs +1040 -2036
  44. package/src/output-styles/default.md +12 -7
  45. package/src/output-styles/minimal.md +25 -0
  46. package/src/output-styles/oneline.md +21 -0
  47. package/src/output-styles/simple.md +10 -9
  48. package/src/repl.mjs +17 -7
  49. package/src/rules/agent/00-common.md +7 -5
  50. package/src/rules/agent/30-explorer.md +8 -12
  51. package/src/rules/lead/01-general.md +3 -1
  52. package/src/rules/lead/lead-tool.md +13 -2
  53. package/src/rules/shared/01-tool.md +23 -12
  54. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
  55. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
  56. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
  57. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
  58. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
  59. package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
  60. package/src/runtime/agent/orchestrator/config.mjs +3 -0
  61. package/src/runtime/agent/orchestrator/context/collect.mjs +182 -67
  62. package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
  63. package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
  64. package/src/runtime/agent/orchestrator/mcp/client.mjs +100 -18
  65. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
  66. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
  67. package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
  68. package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
  69. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +313 -84
  70. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +104 -38
  71. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +28 -33
  72. package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
  73. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
  74. package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
  75. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
  76. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
  77. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +227 -31
  78. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +83 -6
  79. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +229 -115
  80. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +213 -33
  81. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
  82. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +49 -17
  83. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
  84. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +22 -17
  85. package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
  86. package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
  87. package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +28 -0
  88. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +262 -0
  89. package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +38 -0
  90. package/src/runtime/agent/orchestrator/session/loop/env.mjs +14 -0
  91. package/src/runtime/agent/orchestrator/session/loop/hidden-agents.mjs +21 -0
  92. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +49 -0
  93. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +63 -0
  94. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +100 -0
  95. package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
  96. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +218 -0
  97. package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +101 -0
  98. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +35 -0
  99. package/src/runtime/agent/orchestrator/session/loop.mjs +513 -1000
  100. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +227 -0
  101. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
  102. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +137 -0
  103. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
  104. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
  105. package/src/runtime/agent/orchestrator/session/manager.mjs +232 -1152
  106. package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
  107. package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
  108. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
  109. package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
  110. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -24
  111. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
  112. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
  113. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
  114. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +162 -0
  115. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
  116. package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
  117. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
  118. package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +42 -2
  119. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
  120. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
  121. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
  122. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +14 -5
  123. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
  124. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
  125. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +23 -2
  126. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +50 -39
  127. package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
  128. package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
  129. package/src/runtime/agent/orchestrator/tools/builtin.mjs +70 -1
  130. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +303 -0
  131. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +43 -0
  132. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +382 -0
  133. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +497 -0
  134. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +295 -0
  135. package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +158 -0
  136. package/src/runtime/agent/orchestrator/tools/code-graph/lang-predicates.mjs +128 -0
  137. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +66 -0
  138. package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +44 -0
  139. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1192 -0
  140. package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +81 -0
  141. package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +19 -0
  142. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +280 -0
  143. package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +347 -0
  144. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
  145. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +39 -4189
  146. package/src/runtime/agent/orchestrator/tools/patch.mjs +119 -5
  147. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -2
  148. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -2
  149. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
  150. package/src/runtime/channels/backends/discord.mjs +99 -9
  151. package/src/runtime/channels/backends/telegram.mjs +501 -0
  152. package/src/runtime/channels/index.mjs +437 -1439
  153. package/src/runtime/channels/lib/boot-profile.mjs +23 -0
  154. package/src/runtime/channels/lib/config.mjs +54 -2
  155. package/src/runtime/channels/lib/crash-log.mjs +106 -0
  156. package/src/runtime/channels/lib/format.mjs +4 -2
  157. package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
  158. package/src/runtime/channels/lib/output-forwarder.mjs +88 -67
  159. package/src/runtime/channels/lib/runtime-paths.mjs +29 -0
  160. package/src/runtime/channels/lib/scheduler.mjs +1 -1
  161. package/src/runtime/channels/lib/telegram-format.mjs +280 -0
  162. package/src/runtime/channels/lib/tool-format.mjs +1 -1
  163. package/src/runtime/channels/lib/transcript-discovery.mjs +19 -1
  164. package/src/runtime/channels/lib/webhook.mjs +59 -31
  165. package/src/runtime/channels/lib/whisper-language.mjs +42 -0
  166. package/src/runtime/channels/tool-defs.mjs +1 -1
  167. package/src/runtime/memory/index.mjs +465 -345
  168. package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
  169. package/src/runtime/memory/lib/core-memory-store.mjs +352 -2
  170. package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
  171. package/src/runtime/memory/lib/http-wire.mjs +57 -0
  172. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
  173. package/src/runtime/memory/lib/memory-cycle2.mjs +65 -9
  174. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
  175. package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
  176. package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
  177. package/src/runtime/memory/lib/memory.mjs +121 -4
  178. package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
  179. package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
  180. package/src/runtime/memory/lib/recall-format.mjs +183 -0
  181. package/src/runtime/memory/lib/session-ingest.mjs +107 -0
  182. package/src/runtime/memory/lib/trace-store.mjs +69 -22
  183. package/src/runtime/memory/tool-defs.mjs +10 -7
  184. package/src/runtime/shared/abort-controller.mjs +1 -1
  185. package/src/runtime/shared/background-tasks.mjs +2 -3
  186. package/src/runtime/shared/buffered-appender.mjs +149 -0
  187. package/src/runtime/shared/channel-notification-routing.mjs +12 -0
  188. package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
  189. package/src/runtime/shared/config.mjs +9 -0
  190. package/src/runtime/shared/llm/http-agent.mjs +12 -5
  191. package/src/runtime/shared/schedules-store.mjs +21 -19
  192. package/src/runtime/shared/task-notification-envelope.mjs +98 -0
  193. package/src/runtime/shared/task-notification-envelope.test.mjs +107 -0
  194. package/src/runtime/shared/tool-execution-contract.mjs +2 -2
  195. package/src/runtime/shared/tool-surface.mjs +98 -13
  196. package/src/runtime/shared/transcript-writer.mjs +156 -0
  197. package/src/runtime/shared/update-checker.mjs +214 -0
  198. package/src/session-runtime/config-helpers.mjs +209 -0
  199. package/src/session-runtime/effort.mjs +128 -0
  200. package/src/session-runtime/fs-utils.mjs +10 -0
  201. package/src/session-runtime/model-capabilities.mjs +130 -0
  202. package/src/session-runtime/output-styles.mjs +124 -0
  203. package/src/session-runtime/plugin-mcp.mjs +114 -0
  204. package/src/session-runtime/session-text.mjs +100 -0
  205. package/src/session-runtime/statusline-route.mjs +35 -0
  206. package/src/session-runtime/tool-catalog.mjs +720 -0
  207. package/src/session-runtime/workflow.mjs +358 -0
  208. package/src/standalone/agent-tool.mjs +302 -117
  209. package/src/standalone/channel-admin.mjs +133 -40
  210. package/src/standalone/channel-worker.mjs +10 -292
  211. package/src/standalone/explore-tool.mjs +12 -5
  212. package/src/standalone/hook-bus.mjs +165 -8
  213. package/src/standalone/memory-runtime-proxy.mjs +3 -1
  214. package/src/standalone/opencode-go-login.mjs +121 -0
  215. package/src/standalone/provider-admin.mjs +25 -3
  216. package/src/standalone/usage-dashboard.mjs +1 -1
  217. package/src/tui/App.jsx +2883 -778
  218. package/src/tui/components/ConfirmBar.jsx +47 -0
  219. package/src/tui/components/ContextPanel.jsx +5 -3
  220. package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
  221. package/src/tui/components/Markdown.jsx +22 -98
  222. package/src/tui/components/Message.jsx +14 -35
  223. package/src/tui/components/Picker.jsx +87 -12
  224. package/src/tui/components/PromptInput.jsx +355 -22
  225. package/src/tui/components/QueuedCommands.jsx +1 -1
  226. package/src/tui/components/SlashCommandPalette.jsx +8 -5
  227. package/src/tui/components/Spinner.jsx +7 -7
  228. package/src/tui/components/StatusLine.jsx +40 -21
  229. package/src/tui/components/TextEntryPanel.jsx +51 -7
  230. package/src/tui/components/ToolExecution.jsx +183 -101
  231. package/src/tui/components/TurnDone.jsx +4 -4
  232. package/src/tui/components/UsagePanel.jsx +1 -1
  233. package/src/tui/components/tool-output-format.mjs +161 -23
  234. package/src/tui/components/tool-output-format.test.mjs +87 -0
  235. package/src/tui/display-width.mjs +69 -0
  236. package/src/tui/display-width.test.mjs +35 -0
  237. package/src/tui/dist/index.mjs +6731 -2333
  238. package/src/tui/engine/agent-envelope.mjs +296 -0
  239. package/src/tui/engine/boot-profile.mjs +21 -0
  240. package/src/tui/engine/labels.mjs +67 -0
  241. package/src/tui/engine/notice-text.mjs +112 -0
  242. package/src/tui/engine/queue-helpers.mjs +161 -0
  243. package/src/tui/engine/session-stats.mjs +46 -0
  244. package/src/tui/engine/tool-call-fields.mjs +23 -0
  245. package/src/tui/engine/tool-result-text.mjs +126 -0
  246. package/src/tui/engine.mjs +569 -948
  247. package/src/tui/index.jsx +117 -7
  248. package/src/tui/input-editing.mjs +58 -8
  249. package/src/tui/input-editing.selection.test.mjs +75 -0
  250. package/src/tui/keyboard-protocol.mjs +42 -0
  251. package/src/tui/lib/voice-recorder.mjs +469 -0
  252. package/src/tui/markdown/format-token.mjs +128 -76
  253. package/src/tui/markdown/format-token.test.mjs +61 -19
  254. package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
  255. package/src/tui/markdown/render-ansi.test.mjs +1 -1
  256. package/src/tui/markdown/streaming-markdown.mjs +167 -0
  257. package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
  258. package/src/tui/markdown/table-layout.mjs +9 -9
  259. package/src/tui/paste-attachments.mjs +36 -9
  260. package/src/tui/paste-fix.test.mjs +119 -0
  261. package/src/tui/prompt-history-store.mjs +129 -0
  262. package/src/tui/prompt-history-store.test.mjs +52 -0
  263. package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
  264. package/src/tui/theme.mjs +41 -657
  265. package/src/tui/themes/base.mjs +86 -0
  266. package/src/tui/themes/basic.mjs +85 -0
  267. package/src/tui/themes/catppuccin.mjs +72 -0
  268. package/src/tui/themes/dracula.mjs +70 -0
  269. package/src/tui/themes/everforest.mjs +71 -0
  270. package/src/tui/themes/gruvbox.mjs +71 -0
  271. package/src/tui/themes/index.mjs +71 -0
  272. package/src/tui/themes/indigo.mjs +78 -0
  273. package/src/tui/themes/kanagawa.mjs +80 -0
  274. package/src/tui/themes/light.mjs +81 -0
  275. package/src/tui/themes/nord.mjs +72 -0
  276. package/src/tui/themes/onedark.mjs +16 -0
  277. package/src/tui/themes/rosepine.mjs +70 -0
  278. package/src/tui/themes/teal.mjs +80 -0
  279. package/src/tui/themes/tokyonight.mjs +79 -0
  280. package/src/tui/themes/utils.mjs +106 -0
  281. package/src/tui/themes/warm.mjs +79 -0
  282. package/src/tui/transcript-tool-failures.mjs +13 -2
  283. package/src/ui/markdown.mjs +1 -1
  284. package/src/ui/model-display.mjs +2 -2
  285. package/src/ui/statusline.mjs +75 -27
  286. package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
  287. package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
  288. package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
  289. package/src/workflows/default/WORKFLOW.md +46 -12
  290. package/src/workflows/sequential/WORKFLOW.md +51 -0
  291. package/src/workflows/solo/WORKFLOW.md +12 -1
  292. package/vendor/ink/build/display-width.js +62 -0
  293. package/vendor/ink/build/ink.js +100 -12
  294. package/vendor/ink/build/measure-text.js +4 -1
  295. package/vendor/ink/build/output.js +115 -9
  296. package/vendor/ink/build/render-node-to-output.js +4 -1
  297. package/vendor/ink/build/render.js +4 -0
  298. package/src/output-styles/extreme-simple.md +0 -20
  299. package/src/rules/lead/04-workflow.md +0 -51
  300. package/src/workflows/default/workflow.json +0 -13
  301. package/src/workflows/solo/workflow.json +0 -7
@@ -0,0 +1,49 @@
1
+ // Shared pre-dispatch deny — single source of truth for the remaining
2
+ // control-plane / role scoping rejects. Called by BOTH the eager dispatch
3
+ // path (startEagerTool) and the serial dispatch path (executeTool body).
4
+ // Returns null when the call is allowed to proceed; otherwise returns the
5
+ // Error string the serial path would emit. The eager caller ignores the
6
+ // message body and just treats non-null as "do not start eager".
7
+ //
8
+ // This is NOT a permission gate — runtime permission enforcement was removed
9
+ // (every tool call is trusted). What remains is architectural scoping:
10
+ // agent workers are sandboxed to code/research tools. They must never reach
11
+ // owner/host control surfaces: session management, the ENTIRE channels module
12
+ // (Discord messaging, schedules, webhook/config, channel-bridge toggle,
13
+ // command injection), or host input injection. Explicit name list (no imports)
14
+ // keeps this hot-path gate dependency-free; add new owner/channel tools here.
15
+ import { isAgentOwner } from '../../agent-owner.mjs';
16
+
17
+ const WORKER_DENIED_TOOLS = new Set([
18
+ // session control-plane — unified into the single `agent` tool
19
+ // (type=spawn|send|close|list). Denying the one name blocks all worker
20
+ // session control.
21
+ 'agent',
22
+ // channels module (owner/Discord-facing)
23
+ 'reply', 'react', 'edit_message', 'download_attachment', 'fetch',
24
+ 'schedule_status', 'trigger_schedule', 'schedule_control',
25
+ 'activate_channel_bridge', 'reload_config', 'inject_command',
26
+ // host input injection
27
+ 'inject_input',
28
+ ]);
29
+
30
+ function _preDispatchDeny(call, toolKind, sessionRef) {
31
+ const name = call?.name;
32
+ if (typeof name !== 'string' || !name) return null;
33
+ const _agentOwned = sessionRef?.scope?.startsWith?.('agent:')
34
+ || isAgentOwner(sessionRef);
35
+ const _controlPlaneTool = WORKER_DENIED_TOOLS.has(name);
36
+ if (_agentOwned && _controlPlaneTool) {
37
+ return `Error: control-plane tool "${name}" is Lead-only and not available to agent workers.`;
38
+ }
39
+ const noToolAgent = sessionRef?.agent === 'cycle1-agent' || sessionRef?.agent === 'cycle2-agent';
40
+ if (noToolAgent) {
41
+ return `Error: tool "${name}" is not available in agent "${sessionRef.agent}". Re-emit the answer as pipe-separated text per the agent's output format (first character a digit, NO tool_use blocks, NO JSON, NO prose, NO apology).`;
42
+ }
43
+ return null;
44
+ }
45
+
46
+ /** Exported for smoke tests — same runtime deny as the agent loop. */
47
+ export function preDispatchDenyForSession(sessionRef, call, toolKind = 'builtin') {
48
+ return _preDispatchDeny(call, toolKind, sessionRef);
49
+ }
@@ -0,0 +1,63 @@
1
+ // Steering-message normalization/merge helpers extracted from loop.mjs.
2
+ // Merges queued steering entries into a single content payload + display text.
3
+
4
+ export function steeringContentText(content) {
5
+ if (typeof content === 'string') return content;
6
+ if (Array.isArray(content)) {
7
+ return content.map((part) => {
8
+ if (typeof part === 'string') return part;
9
+ if (part?.type === 'text') return part.text || '';
10
+ if (part?.type === 'image') return '[Image]';
11
+ return part?.text || '';
12
+ }).filter(Boolean).join('\n');
13
+ }
14
+ return String(content ?? '');
15
+ }
16
+
17
+ export function normalizeSteeringEntry(entry) {
18
+ if (typeof entry === 'string') {
19
+ const text = entry.trim();
20
+ return text ? { content: text, text } : null;
21
+ }
22
+ if (!entry || typeof entry !== 'object') return null;
23
+ const content = Object.prototype.hasOwnProperty.call(entry, 'content') ? entry.content : entry;
24
+ const text = typeof entry.text === 'string' ? entry.text.trim() : steeringContentText(content).trim();
25
+ if (Array.isArray(content)) return content.length > 0 ? { content, text } : null;
26
+ if (typeof content === 'string') {
27
+ const value = content.trim();
28
+ return value ? { content: value, text: text || value } : null;
29
+ }
30
+ const fallback = steeringContentText(content).trim();
31
+ return fallback ? { content: fallback, text: text || fallback } : null;
32
+ }
33
+
34
+ export function mergeSteeringEntries(entries) {
35
+ const normalized = (Array.isArray(entries) ? entries : [])
36
+ .map(normalizeSteeringEntry)
37
+ .filter(Boolean);
38
+ if (normalized.length === 0) return null;
39
+ const displayText = normalized.map((entry) => entry.text || steeringContentText(entry.content))
40
+ .filter((text) => String(text || '').trim())
41
+ .join('\n');
42
+ if (normalized.every((entry) => typeof entry.content === 'string')) {
43
+ return {
44
+ content: normalized.map((entry) => entry.content).filter(Boolean).join('\n'),
45
+ text: displayText,
46
+ count: normalized.length,
47
+ };
48
+ }
49
+ const parts = [];
50
+ for (const entry of normalized) {
51
+ if (typeof entry.content === 'string') {
52
+ if (entry.content.trim()) parts.push({ type: 'text', text: entry.content });
53
+ } else if (Array.isArray(entry.content)) {
54
+ parts.push(...entry.content);
55
+ } else {
56
+ const text = steeringContentText(entry.content);
57
+ if (text.trim()) parts.push({ type: 'text', text });
58
+ }
59
+ parts.push({ type: 'text', text: '\n' });
60
+ }
61
+ while (parts.length && parts[parts.length - 1]?.type === 'text' && parts[parts.length - 1]?.text === '\n') parts.pop();
62
+ return { content: parts, text: displayText || steeringContentText(parts), count: normalized.length };
63
+ }
@@ -0,0 +1,100 @@
1
+ // Stored tool-call argument compaction/restoration, extracted from loop.mjs.
2
+ // Long body/command args are truncated with a sha256-tagged head/tail preview
3
+ // when persisted into assistant history; the FULL body of a single call can be
4
+ // restored on retry (e.g. a failed edit) so the model sees the original patch.
5
+ import { createHash } from 'crypto';
6
+
7
+ const STORED_TOOL_ARG_BODY_KEY_RE = /^(?:content|old_string|new_string|patch|rewrite)$/i;
8
+ const STORED_TOOL_ARG_LONG_KEY_RE = /^(?:command|script)$/i;
9
+ const STORED_TOOL_ARG_BODY_LIMIT = 2_000;
10
+ const STORED_TOOL_ARG_LONG_LIMIT = 8_000;
11
+ const STORED_TOOL_ARG_PREVIEW_HEAD = 360;
12
+ const STORED_TOOL_ARG_PREVIEW_TAIL = 160;
13
+
14
+ function compactStoredToolArgString(value, key = '') {
15
+ if (typeof value !== 'string') return value;
16
+ const isBody = STORED_TOOL_ARG_BODY_KEY_RE.test(key);
17
+ const isLong = isBody || STORED_TOOL_ARG_LONG_KEY_RE.test(key);
18
+ const limit = isBody ? STORED_TOOL_ARG_BODY_LIMIT : (isLong ? STORED_TOOL_ARG_LONG_LIMIT : Infinity);
19
+ if (value.length <= limit) return value;
20
+ const hash = createHash('sha256').update(value).digest('hex').slice(0, 16);
21
+ const head = value.slice(0, STORED_TOOL_ARG_PREVIEW_HEAD).replace(/\r\n/g, '\n');
22
+ const tail = value.slice(-STORED_TOOL_ARG_PREVIEW_TAIL).replace(/\r\n/g, '\n');
23
+ return `[mixdog compacted ${key || 'string'}: ${value.length} chars, sha256:${hash}]\n${head}\n... [middle omitted from stored tool-call args] ...\n${tail}`;
24
+ }
25
+
26
+ function compactStoredToolArgValue(value, key = '', depth = 0) {
27
+ if (value === null || value === undefined) return value;
28
+ if (typeof value === 'string') return compactStoredToolArgString(value, key);
29
+ if (typeof value !== 'object') return value;
30
+ if (depth >= 6) return Array.isArray(value) ? `[${value.length} items]` : '{...}';
31
+ if (Array.isArray(value)) {
32
+ return value.map((item) => compactStoredToolArgValue(item, key, depth + 1));
33
+ }
34
+ const out = {};
35
+ for (const [k, v] of Object.entries(value)) {
36
+ out[k] = compactStoredToolArgValue(v, k, depth + 1);
37
+ }
38
+ return out;
39
+ }
40
+
41
+ export function compactToolCallsForHistory(calls) {
42
+ if (!Array.isArray(calls)) return calls;
43
+ return calls.map((call) => {
44
+ if (!call || typeof call !== 'object') return call;
45
+ return {
46
+ ...call,
47
+ arguments: compactStoredToolArgValue(call.arguments),
48
+ };
49
+ });
50
+ }
51
+
52
+ // Restore the FULL body of ONE tool call inside a history assistant message
53
+ // whose toolCalls were compacted at push time. Used for a failed edit call so
54
+ // the model sees the original patch/old_string on retry instead of a
55
+ // `[mixdog compacted …]` placeholder it cannot act on. Must run BEFORE the
56
+ // message is first transmitted so it never mutates an already-cached prefix
57
+ // (the prompt cache is content-prefix matched).
58
+ //
59
+ // Only the compactable body/long keys (patch, old_string, new_string, content,
60
+ // rewrite, command, script) are restored, and at ANY depth — compaction is
61
+ // recursive (compactStoredToolArgValue), so batch shapes like edits[].old_string
62
+ // or writes[].content carry nested compacted bodies too. Every other field
63
+ // (e.g. `path`, which a tool may mutate in place during execution) is taken from
64
+ // the compacted snapshot captured at push time, before any mutation. The
65
+ // compacted args tree is built fresh by compactToolCallsForHistory and is not
66
+ // shared with originalCalls, so rebuilding it here is safe.
67
+ export function restoreToolCallBodyForId(assistantMsg, originalCalls, callId) {
68
+ if (!assistantMsg || !Array.isArray(assistantMsg.toolCalls) || !callId) return;
69
+ if (!Array.isArray(originalCalls)) return;
70
+ const tc = assistantMsg.toolCalls.find((t) => t && t.id === callId);
71
+ const orig = originalCalls.find((c) => c && c.id === callId);
72
+ if (!tc || !orig) return;
73
+ if (!tc.arguments || typeof tc.arguments !== 'object'
74
+ || !orig.arguments || typeof orig.arguments !== 'object') return;
75
+ tc.arguments = _restoreCompactedBodies(tc.arguments, orig.arguments, '');
76
+ }
77
+
78
+ // Recursively rebuild a compacted args tree: replace ONLY compactable body/long
79
+ // string fields (matched by key at any depth) with their full originals, and
80
+ // keep every other field from the compacted snapshot. tcVal and origVal share
81
+ // the same structure (compaction only shortens body strings), so the walk
82
+ // descends them in parallel; a missing or non-object origVal falls back to the
83
+ // compacted value rather than throwing.
84
+ function _restoreCompactedBodies(tcVal, origVal, key) {
85
+ if ((STORED_TOOL_ARG_BODY_KEY_RE.test(key) || STORED_TOOL_ARG_LONG_KEY_RE.test(key))
86
+ && typeof origVal === 'string') {
87
+ return origVal;
88
+ }
89
+ if (Array.isArray(tcVal) && Array.isArray(origVal)) {
90
+ return tcVal.map((item, i) => _restoreCompactedBodies(item, origVal[i], key));
91
+ }
92
+ if (tcVal && typeof tcVal === 'object' && origVal && typeof origVal === 'object') {
93
+ const out = {};
94
+ for (const k of Object.keys(tcVal)) {
95
+ out[k] = (k in origVal) ? _restoreCompactedBodies(tcVal[k], origVal[k], k) : tcVal[k];
96
+ }
97
+ return out;
98
+ }
99
+ return tcVal;
100
+ }
@@ -0,0 +1,52 @@
1
+ // Tool-name classification + intra-turn signature helpers, extracted from
2
+ // loop.mjs. These drive cross-turn read dedup, scoped caching, shell routing,
3
+ // and duplicate-call detection. Strips the MCP prefix so direct calls and
4
+ // MCP-wrapped calls share the same cache.
5
+ import { createHash } from 'crypto';
6
+
7
+ export const MCP_TOOL_PREFIX = 'mcp__plugin_mixdog_mixdog__';
8
+
9
+ export function _stripMcpPrefix(name) {
10
+ return typeof name === 'string' && name.startsWith(MCP_TOOL_PREFIX)
11
+ ? name.slice(MCP_TOOL_PREFIX.length) : name;
12
+ }
13
+ export function _isReadTool(name) {
14
+ return _stripMcpPrefix(name) === 'read';
15
+ }
16
+ export function _isMutationTool(name) {
17
+ const n = _stripMcpPrefix(name);
18
+ return n === 'apply_patch';
19
+ }
20
+ export const SCOPED_CACHEABLE_TOOLS = new Set([
21
+ 'code_graph',
22
+ 'grep',
23
+ 'list',
24
+ 'glob',
25
+ ]);
26
+ export function _isScopedCacheableTool(name) {
27
+ const n = _stripMcpPrefix(name);
28
+ return SCOPED_CACHEABLE_TOOLS.has(n);
29
+ }
30
+ export function _isShellTool(name) {
31
+ const n = _stripMcpPrefix(name);
32
+ return n === 'shell' || n === 'bash_session';
33
+ }
34
+
35
+ // Canonical signature for intra-turn duplicate detection. Sorting keys
36
+ // produces a stable hash regardless of arg-object key order. Anything
37
+ // non-serializable falls back to String(args) — still deterministic for
38
+ // the model's typical structured-arg shape.
39
+ export function _canonicalArgs(args) {
40
+ if (args == null || typeof args !== 'object') {
41
+ try { return JSON.stringify(args); } catch { return String(args); }
42
+ }
43
+ try {
44
+ const keys = Object.keys(args).sort();
45
+ const sorted = {};
46
+ for (const k of keys) sorted[k] = args[k];
47
+ return JSON.stringify(sorted);
48
+ } catch { return String(args); }
49
+ }
50
+ export function _intraTurnSig(name, args) {
51
+ return createHash('sha256').update(`${name}:${_canonicalArgs(args)}`).digest('hex').slice(0, 16);
52
+ }
@@ -0,0 +1,218 @@
1
+ // Miscellaneous tool helpers extracted from loop.mjs: eager-dispatch check,
2
+ // message-array change detect, tool-kind classification, skills, hook overrides,
3
+ // native tool-search payload parsing, bash-session routing, and approval.
4
+ import { isMcpTool } from '../../mcp/client.mjs';
5
+ import { isBuiltinTool } from '../../tools/builtin.mjs';
6
+ import { isInternalTool } from '../../internal-tools.mjs';
7
+ import { collectSkillsCached, loadSkillResource, buildSkillToolEnvelope } from '../../context/collect.mjs';
8
+ import { isAgentOwner } from '../../agent-owner.mjs';
9
+
10
+ // Eager-dispatch: tools with readOnlyHint:true in their declaration are safe
11
+ // to execute during SSE parsing so tool work overlaps with the rest of the
12
+ // stream. Writes, bash, MCP and skills stay serial after send() returns.
13
+ export function isEagerDispatchable(name, tools) {
14
+ if (!Array.isArray(tools)) return false;
15
+ const def = tools.find(t => t?.name === name);
16
+ return def?.annotations?.readOnlyHint === true;
17
+ }
18
+ export function messagesArrayChanged(before, after) {
19
+ if (!Array.isArray(before) || !Array.isArray(after)) return before !== after;
20
+ if (before.length !== after.length) return true;
21
+ for (let i = 0; i < before.length; i += 1) {
22
+ if (before[i] !== after[i]) return true;
23
+ }
24
+ return false;
25
+ }
26
+
27
+ const SKILL_TOOL_NAMES = new Set(['Skill', 'skills_list', 'skill_view']);
28
+ const SPECIAL_TOOL_NAMES = new Set(['bash_session', 'apply_patch', 'code_graph']);
29
+ const BASH_SESSION_HEADER_RE = /\[session: ([^\]\r\n]+)\]/;
30
+
31
+ export function getToolKind(name) {
32
+ if (SKILL_TOOL_NAMES.has(name)) return 'skill';
33
+ if (SPECIAL_TOOL_NAMES.has(name)) return 'builtin';
34
+ if (isMcpTool(name)) return 'mcp';
35
+ if (isInternalTool(name)) return 'internal';
36
+ if (isBuiltinTool(name)) return 'builtin';
37
+ return 'builtin';
38
+ }
39
+ export function buildSkillsListResponse(cwd) {
40
+ const skills = collectSkillsCached(cwd);
41
+ const entries = skills.map(s => ({ name: s.name, description: s.description || '' }));
42
+ return JSON.stringify({ skills: entries });
43
+ }
44
+ export function viewSkill(cwd, name) {
45
+ if (!name) return 'Error: skill name is required';
46
+ const res = loadSkillResource(name, cwd);
47
+ if (!res) return `Error: skill "${name}" not found`;
48
+ // Return the general tool envelope: the model-visible tool_result is the
49
+ // short stub (`Loaded skill: <name>`) and the full SKILL.md body is
50
+ // delivered ONCE as a separate injected role:'user' message (newMessages).
51
+ return buildSkillToolEnvelope(name, res.content, res.dir);
52
+ }
53
+
54
+ /** Normalize PostToolUse hook override values (legacy MCP text envelopes only). */
55
+ export function normalizeHookUpdatedToolOutput(value) {
56
+ if (typeof value === 'string') return value;
57
+ if (value == null) return '';
58
+ if (typeof value === 'object' && Array.isArray(value.content)) {
59
+ const hasNonText = value.content.some((c) => c && typeof c === 'object' && c.type && c.type !== 'text');
60
+ if (hasNonText) return value;
61
+ return value.content
62
+ .map((c) => (c?.type === 'text' ? c.text || '' : JSON.stringify(c)))
63
+ .join('\n');
64
+ }
65
+ return value;
66
+ }
67
+
68
+ export function resolveToolResultAfterHook(originalResult, hookResult) {
69
+ if (!hookResult || typeof hookResult !== 'object' || hookResult.updatedToolOutput === undefined) {
70
+ return originalResult;
71
+ }
72
+ const updated = normalizeHookUpdatedToolOutput(hookResult.updatedToolOutput);
73
+ return updated === undefined ? originalResult : updated;
74
+ }
75
+
76
+ export function parseNativeToolSearchPayload(toolName, result) {
77
+ if (toolName !== 'tool_search' || typeof result !== 'string') return null;
78
+ try {
79
+ const parsed = JSON.parse(result);
80
+ const native = parsed?.nativeToolSearch;
81
+ if (!native || typeof native !== 'object') return null;
82
+ const rawToolReferences = Array.isArray(native.toolReferences) ? native.toolReferences : [];
83
+ const toolReferences = rawToolReferences
84
+ .filter((name) => typeof name === 'string')
85
+ .map((name) => name.trim())
86
+ .filter(Boolean);
87
+ const rawOpenaiTools = Array.isArray(native.openaiTools) ? native.openaiTools : [];
88
+ const openaiTools = rawOpenaiTools.filter((tool) => (
89
+ tool
90
+ && typeof tool === 'object'
91
+ && !Array.isArray(tool)
92
+ && typeof tool.name === 'string'
93
+ && tool.name.trim()
94
+ && (tool.type === undefined || typeof tool.type === 'string')
95
+ ));
96
+ if (!toolReferences.length && !openaiTools.length) return null;
97
+ const baseSummary = typeof native.summary === 'string' && native.summary
98
+ ? native.summary
99
+ : `Loaded deferred tools: ${toolReferences.join(', ') || openaiTools.map((tool) => tool.name).filter(Boolean).join(', ')}`;
100
+ const selectedTools = parsed?.selected?.tools;
101
+ const missing = Array.isArray(selectedTools?.missing)
102
+ ? selectedTools.missing.map((name) => String(name || '').trim()).filter(Boolean)
103
+ : [];
104
+ const blocked = Array.isArray(selectedTools?.blocked)
105
+ ? selectedTools.blocked
106
+ .map((entry) => {
107
+ if (entry && typeof entry === 'object') {
108
+ const name = String(entry.name || '').trim();
109
+ if (!name) return '';
110
+ const reason = String(entry.reason || '').trim();
111
+ return reason ? `${name} (${reason})` : name;
112
+ }
113
+ return String(entry || '').trim();
114
+ })
115
+ .filter(Boolean)
116
+ : [];
117
+ const extraLines = [];
118
+ if (missing.length) extraLines.push(`missing: ${missing.join(', ')}`);
119
+ if (blocked.length) extraLines.push(`blocked: ${blocked.join(', ')}`);
120
+ const summary = extraLines.length ? `${baseSummary}\n${extraLines.join('; ')}` : baseSummary;
121
+ return {
122
+ toolReferences,
123
+ openaiTools,
124
+ summary,
125
+ };
126
+ } catch {
127
+ return null;
128
+ }
129
+ }
130
+ export function extractBashSessionId(result) {
131
+ if (typeof result !== 'string') return null;
132
+ const match = BASH_SESSION_HEADER_RE.exec(result);
133
+ return match ? match[1] : null;
134
+ }
135
+
136
+ export function buildAgentBashSessionArgs(args, sessionRef) {
137
+ if (!isAgentOwner(sessionRef)) return null;
138
+ // run_in_background is a detached one-shot job, incompatible with the
139
+ // persistent bash session. Fall through to the background-job path
140
+ // (executeBuiltinTool -> startBackgroundShellJob) so the worker gets a
141
+ // task_id that task control can resolve — otherwise the persistent
142
+ // session returns a [session: ...] header and task control reports "task not found".
143
+ if (args?.run_in_background === true) return null;
144
+ const routedArgs = { ...(args || {}) };
145
+ const explicitSessionId = typeof routedArgs.session_id === 'string' && routedArgs.session_id.trim()
146
+ ? routedArgs.session_id.trim()
147
+ : null;
148
+ const wantsPersistent = routedArgs.persistent === true || !!explicitSessionId;
149
+ if (!wantsPersistent) return null;
150
+ if (!explicitSessionId && sessionRef?.implicitBashSessionId) {
151
+ routedArgs.session_id = sessionRef.implicitBashSessionId;
152
+ } else if (explicitSessionId) {
153
+ routedArgs.session_id = explicitSessionId;
154
+ }
155
+ delete routedArgs.persistent;
156
+ return routedArgs;
157
+ }
158
+
159
+ export function formatMissingToolApprovalUiDenial(toolName, askReason) {
160
+ const reason = String(askReason || 'approval requested by hook').trim();
161
+ const name = String(toolName || 'tool');
162
+ return `Error: tool "${name}" denied by hook: approval required but no approval UI is available${reason ? ` (${reason})` : ''}`;
163
+ }
164
+
165
+ /**
166
+ * Resolve PreToolUse `{ action: 'ask' }` against an optional approval callback.
167
+ * Returns `{ denial }` when the tool must not run; otherwise `{ approval }`.
168
+ */
169
+ export async function resolvePreToolAskApproval({
170
+ toolName,
171
+ args,
172
+ cwd,
173
+ sessionId,
174
+ toolCallId,
175
+ askReason,
176
+ toolApprovalHook,
177
+ }) {
178
+ const name = String(toolName || 'tool');
179
+ const reason = String(askReason || 'approval requested by hook').trim();
180
+ if (typeof toolApprovalHook !== 'function') {
181
+ return { denial: formatMissingToolApprovalUiDenial(name, reason) };
182
+ }
183
+ let approval;
184
+ try {
185
+ approval = await toolApprovalHook({
186
+ name,
187
+ args,
188
+ cwd,
189
+ sessionId,
190
+ toolCallId: toolCallId || null,
191
+ reason,
192
+ });
193
+ } catch (error) {
194
+ const detail = error?.message || String(error || 'approval failed');
195
+ return { denial: `Error: tool "${name}" denied by hook: ${detail}` };
196
+ }
197
+ if (!approvalGranted(approval)) {
198
+ const detail = approvalReason(approval, reason || 'not approved');
199
+ return { denial: `Error: tool "${name}" denied by hook: ${detail}` };
200
+ }
201
+ return { approval };
202
+ }
203
+
204
+ export function approvalGranted(value) {
205
+ if (value === true) return true;
206
+ if (!value || typeof value !== 'object') return false;
207
+ if (value.approved === true || value.allow === true || value.allowed === true) return true;
208
+ const decision = String(value.decision || value.action || value.result || '').trim().toLowerCase();
209
+ return decision === 'approve' || decision === 'approved' || decision === 'allow' || decision === 'yes';
210
+ }
211
+
212
+ export function approvalReason(value, fallback = '') {
213
+ if (value && typeof value === 'object') {
214
+ const reason = String(value.reason || value.message || '').trim();
215
+ if (reason) return reason;
216
+ }
217
+ return fallback;
218
+ }
@@ -0,0 +1,101 @@
1
+ // Pre-provider transcript pairing repair, extracted from loop.mjs.
2
+ // Anthropic 400-rejects when an assistant message ends with tool_use blocks and
3
+ // the next message isn't tool results for those exact ids. abort/timeout/error
4
+ // races in the loop body can leave a dangling assistant tool_use at the tail;
5
+ // strip any trailing/orphaned tool_use|tool_result so provider.send sees a
6
+ // valid transcript instead of leaking the 400 to the user.
7
+ import { sanitizeToolPairs } from '../context-utils.mjs';
8
+
9
+ // Transcript pairing guard. Anthropic 400-rejects when an assistant message
10
+ // ends with tool_use blocks and the next message isn't tool results for
11
+ // those exact ids. abort/timeout/error race in the loop body can leave a
12
+ // dangling assistant tool_use at the tail (e.g. the structure_probe loop
13
+ // running 12 deep then aborting between push-assistant and push-tool).
14
+ // Strip any trailing assistant tool_use that has no matching tool result
15
+ // so provider.send sees a valid transcript instead of leaking the 400 to
16
+ // the user. Repair runs every iteration but is a no-op on healthy paths.
17
+ function _ensureTranscriptPairing(msgs, sessionId) {
18
+ // Walk backwards to find the last assistant message that emitted
19
+ // tool_use, then validate that every id has a matching tool result
20
+ // inside the CONTIGUOUS tool-message block immediately following it.
21
+ // Earlier guard splice'd the entire tail — which silently deleted any
22
+ // user prompt appended after the dangling assistant by manager.mjs:
23
+ // when the guard fired with shape
24
+ // [..., assistant{a,b}, tool{a}, user{new prompt}]
25
+ // the splice removed user{new prompt} along with the orphan suffix.
26
+ // Fix: remove only assistant + the contiguous tool block; preserve
27
+ // anything past it (user / system / next assistant) untouched.
28
+ let popped = 0;
29
+ while (msgs.length > 0) {
30
+ let lastAssistantIdx = -1;
31
+ for (let i = msgs.length - 1; i >= 0; i--) {
32
+ const m = msgs[i];
33
+ if (m?.role === 'assistant' && Array.isArray(m.toolCalls) && m.toolCalls.length > 0) {
34
+ lastAssistantIdx = i;
35
+ break;
36
+ }
37
+ }
38
+ if (lastAssistantIdx === -1) break;
39
+ // Collect the contiguous tool messages directly after this assistant.
40
+ // Anything past that block is unrelated (next user prompt, system
41
+ // marker, etc.) and must survive the repair.
42
+ let toolBlockEnd = lastAssistantIdx + 1;
43
+ while (toolBlockEnd < msgs.length && msgs[toolBlockEnd]?.role === 'tool') {
44
+ toolBlockEnd += 1;
45
+ }
46
+ const toolBlock = msgs.slice(lastAssistantIdx + 1, toolBlockEnd);
47
+ const ids = msgs[lastAssistantIdx].toolCalls.map(c => c.id);
48
+ const matched = ids.every(id => toolBlock.some(m => m.toolCallId === id));
49
+ if (matched) break;
50
+ const removed = toolBlockEnd - lastAssistantIdx;
51
+ msgs.splice(lastAssistantIdx, removed);
52
+ popped += removed;
53
+ }
54
+ // Second sweep — catch dangling tool results that survived the
55
+ // contiguous-block splice. Anthropic strict spec requires every
56
+ // tool result to sit in a contiguous block right after the
57
+ // assistant whose toolCalls produced it; a `[..., assistant{a,b},
58
+ // tool{a}, user, tool{b}]` shape leaves tool{b} orphaned even
59
+ // after assistant + tool{a} are repaired by the loop above.
60
+ // Walk back from each tool message to the nearest non-tool
61
+ // ancestor; if it is not an assistant whose toolCalls include
62
+ // this id, drop the orphan.
63
+ for (let i = msgs.length - 1; i >= 0; i--) {
64
+ const m = msgs[i];
65
+ if (m?.role !== 'tool') continue;
66
+ if (!m.toolCallId) {
67
+ msgs.splice(i, 1);
68
+ popped += 1;
69
+ continue;
70
+ }
71
+ let prevIdx = i - 1;
72
+ while (prevIdx >= 0 && msgs[prevIdx]?.role === 'tool') prevIdx--;
73
+ const anchor = prevIdx >= 0 ? msgs[prevIdx] : null;
74
+ const anchorOk = anchor?.role === 'assistant'
75
+ && Array.isArray(anchor.toolCalls)
76
+ && anchor.toolCalls.some(c => c.id === m.toolCallId);
77
+ if (!anchorOk) {
78
+ msgs.splice(i, 1);
79
+ popped += 1;
80
+ }
81
+ }
82
+ if (popped > 0 && sessionId) {
83
+ try { process.stderr.write(`[transcript-repair] sess=${sessionId} popped=${popped} dangling assistant tool_use\n`); } catch {}
84
+ }
85
+ }
86
+
87
+ /**
88
+ * Pre-provider transcript repair for the agent loop. Reattach valid tool
89
+ * results (non-destructive) before any destructive orphan pairing cleanup.
90
+ * Mutates `messages` in place to preserve the session array reference.
91
+ */
92
+ export function repairTranscriptBeforeProviderSend(messages, sessionId = null) {
93
+ if (!Array.isArray(messages)) return messages;
94
+ const sanitized = sanitizeToolPairs(messages);
95
+ if (sanitized !== messages) {
96
+ messages.length = 0;
97
+ messages.push(...sanitized);
98
+ }
99
+ _ensureTranscriptPairing(messages, sessionId);
100
+ return messages;
101
+ }
@@ -0,0 +1,35 @@
1
+ // Usage accumulation helpers extracted from loop.mjs.
2
+ // Normalize a provider usage payload into the canonical token/cost shape and
3
+ // fold successive deltas into a running total across loop iterations.
4
+
5
+ export function normalizeUsage(usage) {
6
+ if (!usage) return null;
7
+ const costUsd = Number(usage.costUsd);
8
+ return {
9
+ inputTokens: usage.inputTokens || 0,
10
+ outputTokens: usage.outputTokens || 0,
11
+ cachedTokens: usage.cachedTokens || 0,
12
+ cacheWriteTokens: usage.cacheWriteTokens || 0,
13
+ promptTokens: usage.promptTokens || 0,
14
+ ...(Number.isFinite(costUsd) ? { costUsd } : {}),
15
+ raw: usage.raw,
16
+ };
17
+ }
18
+
19
+ export function addUsage(total, usage) {
20
+ const delta = normalizeUsage(usage);
21
+ if (!delta) return total;
22
+ if (!total) return { ...delta };
23
+ const next = {
24
+ ...total,
25
+ inputTokens: (total.inputTokens || 0) + delta.inputTokens,
26
+ outputTokens: (total.outputTokens || 0) + delta.outputTokens,
27
+ cachedTokens: (total.cachedTokens || 0) + delta.cachedTokens,
28
+ cacheWriteTokens: (total.cacheWriteTokens || 0) + delta.cacheWriteTokens,
29
+ promptTokens: (total.promptTokens || 0) + delta.promptTokens,
30
+ };
31
+ if (delta.costUsd != null || total.costUsd != null) {
32
+ next.costUsd = (total.costUsd || 0) + (delta.costUsd || 0);
33
+ }
34
+ return next;
35
+ }