mixdog 0.8.1 → 0.9.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (299) hide show
  1. package/README.md +47 -23
  2. package/package.json +34 -28
  3. package/scripts/_test-folder-dialog.mjs +30 -0
  4. package/scripts/agent-parallel-smoke.mjs +388 -0
  5. package/scripts/agent-tag-reuse-smoke.mjs +183 -0
  6. package/scripts/background-task-meta-smoke.mjs +38 -0
  7. package/scripts/boot-smoke.mjs +52 -9
  8. package/scripts/build-runtime-linux.sh +348 -0
  9. package/scripts/build-runtime-macos.sh +217 -0
  10. package/scripts/build-runtime-windows.ps1 +242 -0
  11. package/scripts/compact-active-turn-test.mjs +68 -0
  12. package/scripts/compact-smoke.mjs +859 -129
  13. package/scripts/compact-trigger-migration-smoke.mjs +187 -0
  14. package/scripts/fix-brief-fn.mjs +35 -0
  15. package/scripts/fix-format-tool-surface.mjs +24 -0
  16. package/scripts/fix-tool-exec-visible.mjs +42 -0
  17. package/scripts/generate-runtime-manifest.mjs +166 -0
  18. package/scripts/hook-bus-test.mjs +330 -0
  19. package/scripts/lead-workflow-smoke.mjs +33 -39
  20. package/scripts/live-worker-smoke.mjs +43 -37
  21. package/scripts/llm-trace-summary.mjs +315 -0
  22. package/scripts/memory-meta-concurrency-test.mjs +20 -0
  23. package/scripts/output-style-smoke.mjs +56 -15
  24. package/scripts/parent-abort-link-test.mjs +44 -0
  25. package/scripts/patch-agent-brief.mjs +48 -0
  26. package/scripts/patch-app.mjs +21 -0
  27. package/scripts/patch-app2.mjs +18 -0
  28. package/scripts/patch-dist-brief.mjs +96 -0
  29. package/scripts/patch-tool-exec.mjs +70 -0
  30. package/scripts/pretool-ask-runtime-test.mjs +54 -0
  31. package/scripts/provider-toolcall-test.mjs +376 -0
  32. package/scripts/reactive-compact-persist-smoke.mjs +124 -0
  33. package/scripts/sanitize-tool-pairs-test.mjs +260 -0
  34. package/scripts/session-context-bench.mjs +205 -33
  35. package/scripts/session-ingest-smoke.mjs +177 -0
  36. package/scripts/set-effort-config-test.mjs +41 -0
  37. package/scripts/smoke-runtime-negative.ps1 +106 -0
  38. package/scripts/smoke-runtime-negative.sh +97 -0
  39. package/scripts/smoke.mjs +25 -0
  40. package/scripts/tool-result-hook-test.mjs +48 -0
  41. package/scripts/tool-smoke.mjs +1222 -90
  42. package/scripts/toolcall-args-test.mjs +150 -0
  43. package/scripts/tui-background-failure-smoke.mjs +73 -0
  44. package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
  45. package/src/agents/debugger/AGENT.md +8 -0
  46. package/src/agents/explore/AGENT.md +4 -0
  47. package/src/agents/heavy-worker/AGENT.md +9 -3
  48. package/src/agents/maintainer/AGENT.md +4 -0
  49. package/src/agents/reviewer/AGENT.md +8 -0
  50. package/src/agents/scheduler-task/AGENT.md +12 -0
  51. package/src/agents/scheduler-task/agent.json +6 -0
  52. package/src/agents/webhook-handler/AGENT.md +12 -0
  53. package/src/agents/webhook-handler/agent.json +6 -0
  54. package/src/agents/worker/AGENT.md +9 -3
  55. package/src/app.mjs +77 -3
  56. package/src/defaults/hidden-roles.json +17 -12
  57. package/src/headless-role.mjs +117 -0
  58. package/src/help.mjs +30 -0
  59. package/src/hooks/lib/permission-evaluator.cjs +11 -475
  60. package/src/lib/keychain-cjs.cjs +9 -1
  61. package/src/lib/mixdog-debug.cjs +0 -29
  62. package/src/lib/plugin-paths.cjs +1 -7
  63. package/src/lib/rules-builder.cjs +242 -98
  64. package/src/lib/text-utils.cjs +1 -1
  65. package/src/mixdog-session-runtime.mjs +2211 -446
  66. package/src/output-styles/default.md +12 -28
  67. package/src/output-styles/extreme-simple.md +9 -6
  68. package/src/output-styles/simple.md +22 -9
  69. package/src/repl.mjs +108 -59
  70. package/src/rules/agent/00-common.md +15 -0
  71. package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
  72. package/src/rules/agent/30-explorer.md +22 -0
  73. package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
  74. package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
  75. package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
  76. package/src/rules/lead/01-general.md +9 -5
  77. package/src/rules/lead/04-workflow.md +51 -12
  78. package/src/rules/lead/lead-tool.md +6 -0
  79. package/src/rules/shared/01-tool.md +12 -1
  80. package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
  81. package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
  82. package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
  83. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
  84. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
  85. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
  86. package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
  87. package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
  88. package/src/runtime/agent/orchestrator/config.mjs +174 -55
  89. package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
  90. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
  91. package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
  92. package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
  93. package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
  94. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
  95. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +380 -268
  96. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +131 -93
  97. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
  98. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
  99. package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
  100. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
  101. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
  102. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
  103. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
  104. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
  106. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
  107. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +203 -71
  108. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +512 -312
  109. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -518
  110. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
  111. package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
  112. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +289 -11
  113. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
  114. package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
  115. package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
  116. package/src/runtime/agent/orchestrator/session/loop.mjs +840 -612
  117. package/src/runtime/agent/orchestrator/session/manager.mjs +1574 -459
  118. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
  119. package/src/runtime/agent/orchestrator/session/store.mjs +67 -29
  120. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  121. package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
  122. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
  123. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +35 -8
  124. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -8
  125. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -53
  126. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
  127. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
  128. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
  129. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
  130. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
  131. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
  132. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
  133. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
  134. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
  135. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
  136. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
  137. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
  138. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
  139. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
  140. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
  141. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
  142. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
  143. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
  144. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
  145. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
  146. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
  147. package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
  148. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
  149. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
  150. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
  151. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
  152. package/src/runtime/channels/index.mjs +149 -51
  153. package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
  154. package/src/runtime/channels/lib/config.mjs +0 -1
  155. package/src/runtime/channels/lib/drop-trace.mjs +1 -1
  156. package/src/runtime/channels/lib/executor.mjs +0 -3
  157. package/src/runtime/channels/lib/memory-client.mjs +0 -38
  158. package/src/runtime/channels/lib/output-forwarder.mjs +1 -8
  159. package/src/runtime/channels/lib/runtime-paths.mjs +0 -6
  160. package/src/runtime/channels/lib/scheduler.mjs +18 -14
  161. package/src/runtime/channels/lib/session-discovery.mjs +3 -6
  162. package/src/runtime/channels/lib/tool-format.mjs +0 -1
  163. package/src/runtime/channels/lib/transcript-discovery.mjs +4 -12
  164. package/src/runtime/channels/lib/webhook.mjs +1 -1
  165. package/src/runtime/channels/tool-defs.mjs +29 -29
  166. package/src/runtime/lib/keychain-cjs.cjs +0 -1
  167. package/src/runtime/memory/data/runtime-manifest.json +6 -7
  168. package/src/runtime/memory/index.mjs +519 -118
  169. package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
  170. package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
  171. package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
  172. package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
  173. package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
  174. package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
  175. package/src/runtime/memory/lib/memory-cycle1.mjs +28 -7
  176. package/src/runtime/memory/lib/memory-cycle2.mjs +4 -5
  177. package/src/runtime/memory/lib/memory-cycle3.mjs +2 -2
  178. package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -2
  179. package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
  180. package/src/runtime/memory/lib/memory.mjs +88 -9
  181. package/src/runtime/memory/lib/model-profile.mjs +1 -1
  182. package/src/runtime/memory/lib/pg/adapter.mjs +1 -1
  183. package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
  184. package/src/runtime/memory/lib/runtime-fetcher.mjs +80 -21
  185. package/src/runtime/memory/lib/session-ingest.mjs +196 -0
  186. package/src/runtime/memory/lib/trace-store.mjs +96 -51
  187. package/src/runtime/memory/tool-defs.mjs +46 -37
  188. package/src/runtime/search/index.mjs +104 -473
  189. package/src/runtime/search/lib/config.mjs +0 -4
  190. package/src/runtime/search/lib/state.mjs +1 -15
  191. package/src/runtime/search/lib/web-tools.mjs +44 -25
  192. package/src/runtime/search/tool-defs.mjs +16 -23
  193. package/src/runtime/shared/abort-controller.mjs +1 -1
  194. package/src/runtime/shared/atomic-file.mjs +4 -3
  195. package/src/runtime/shared/background-tasks.mjs +122 -11
  196. package/src/runtime/shared/child-spawn-gate.mjs +139 -0
  197. package/src/runtime/shared/config.mjs +7 -4
  198. package/src/runtime/shared/err-text.mjs +131 -4
  199. package/src/runtime/shared/llm/cost.mjs +2 -2
  200. package/src/runtime/shared/llm/http-agent.mjs +23 -7
  201. package/src/runtime/shared/llm/index.mjs +34 -11
  202. package/src/runtime/shared/llm/usage-log.mjs +4 -4
  203. package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
  204. package/src/runtime/shared/singleton-owner.mjs +104 -0
  205. package/src/runtime/shared/tool-execution-contract.mjs +199 -20
  206. package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
  207. package/src/runtime/shared/tool-surface.mjs +624 -95
  208. package/src/runtime/shared/user-data-guard.mjs +0 -2
  209. package/src/standalone/agent-task-status.mjs +203 -0
  210. package/src/standalone/agent-task-status.test.mjs +76 -0
  211. package/src/standalone/agent-tool.mjs +1913 -0
  212. package/src/standalone/channel-worker.mjs +370 -14
  213. package/src/standalone/explore-tool.mjs +165 -70
  214. package/src/standalone/folder-dialog.mjs +314 -0
  215. package/src/standalone/hook-bus.mjs +898 -22
  216. package/src/standalone/memory-runtime-proxy.mjs +320 -0
  217. package/src/standalone/projects.mjs +226 -0
  218. package/src/standalone/provider-admin.mjs +41 -24
  219. package/src/standalone/seeds.mjs +1 -78
  220. package/src/standalone/usage-dashboard.mjs +96 -8
  221. package/src/tui/App.jsx +4800 -2140
  222. package/src/tui/components/AnsiText.jsx +39 -28
  223. package/src/tui/components/ContextPanel.jsx +14 -4
  224. package/src/tui/components/Markdown.jsx +43 -77
  225. package/src/tui/components/MarkdownTable.jsx +9 -184
  226. package/src/tui/components/Message.jsx +28 -11
  227. package/src/tui/components/Picker.jsx +95 -56
  228. package/src/tui/components/PromptInput.jsx +428 -239
  229. package/src/tui/components/QueuedCommands.jsx +1 -1
  230. package/src/tui/components/SlashCommandPalette.jsx +27 -21
  231. package/src/tui/components/Spinner.jsx +67 -38
  232. package/src/tui/components/StatusLine.jsx +606 -38
  233. package/src/tui/components/TextEntryPanel.jsx +128 -9
  234. package/src/tui/components/ToolExecution.jsx +597 -362
  235. package/src/tui/components/TurnDone.jsx +3 -3
  236. package/src/tui/components/UsagePanel.jsx +3 -5
  237. package/src/tui/components/tool-output-format.mjs +499 -0
  238. package/src/tui/components/tool-output-format.test.mjs +312 -0
  239. package/src/tui/dist/index.mjs +9192 -2399
  240. package/src/tui/engine-runtime-notification.test.mjs +115 -0
  241. package/src/tui/engine-tool-result-text.test.mjs +75 -0
  242. package/src/tui/engine.mjs +1455 -279
  243. package/src/tui/figures.mjs +21 -40
  244. package/src/tui/index.jsx +75 -31
  245. package/src/tui/input-editing.mjs +25 -0
  246. package/src/tui/markdown/format-token.mjs +671 -69
  247. package/src/tui/markdown/format-token.test.mjs +312 -0
  248. package/src/tui/markdown/render-ansi.mjs +94 -0
  249. package/src/tui/markdown/render-ansi.test.mjs +108 -0
  250. package/src/tui/markdown/stream-fence.mjs +34 -0
  251. package/src/tui/markdown/stream-fence.test.mjs +26 -0
  252. package/src/tui/markdown/table-layout.mjs +250 -0
  253. package/src/tui/paste-attachments.mjs +0 -7
  254. package/src/tui/spinner-verbs.mjs +1 -2
  255. package/src/tui/statusline-ansi-bridge.mjs +172 -0
  256. package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
  257. package/src/tui/theme.mjs +756 -24
  258. package/src/tui/time-format.mjs +1 -1
  259. package/src/tui/transcript-tool-failures.mjs +67 -0
  260. package/src/tui/transcript-tool-failures.test.mjs +111 -0
  261. package/src/ui/ansi.mjs +1 -2
  262. package/src/ui/markdown.mjs +85 -26
  263. package/src/ui/markdown.test.mjs +70 -0
  264. package/src/ui/model-display.mjs +121 -0
  265. package/src/ui/session-stats.mjs +44 -0
  266. package/src/ui/statusline-context-label.test.mjs +15 -0
  267. package/src/ui/statusline.mjs +386 -178
  268. package/src/ui/tool-card.mjs +2 -13
  269. package/src/vendor/statusline/bin/statusline-lib.mjs +1 -620
  270. package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
  271. package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
  272. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
  273. package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
  274. package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
  275. package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
  276. package/src/workflows/default/WORKFLOW.md +12 -5
  277. package/src/workflows/default/workflow.json +0 -1
  278. package/src/workflows/solo/WORKFLOW.md +15 -0
  279. package/src/workflows/solo/workflow.json +7 -0
  280. package/vendor/ink/build/ink.js +54 -8
  281. package/vendor/ink/build/output.js +6 -1
  282. package/src/agents/scheduler-task.md +0 -3
  283. package/src/agents/web-researcher/AGENT.md +0 -3
  284. package/src/agents/web-researcher/agent.json +0 -6
  285. package/src/agents/webhook-handler.md +0 -3
  286. package/src/hooks/lib/permission-rules.cjs +0 -170
  287. package/src/hooks/lib/settings-loader.cjs +0 -112
  288. package/src/lib/hook-pipe-path.cjs +0 -10
  289. package/src/rules/bridge/00-common.md +0 -5
  290. package/src/rules/bridge/30-explorer.md +0 -4
  291. package/src/rules/lead/00-tool-lead.md +0 -5
  292. package/src/rules/shared/00-language.md +0 -3
  293. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  294. package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
  295. package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
  296. package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
  297. package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
  298. package/src/standalone/bridge-tool.mjs +0 -1414
  299. package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
@@ -0,0 +1,376 @@
1
+ #!/usr/bin/env node
2
+ // Regression tests pinning the cross-provider "native tool_call extraction"
3
+ // contract: when a provider's native parser is fed a well-formed tool_call
4
+ // payload, it MUST surface the call in our canonical toolCalls shape
5
+ // ({ id, name, arguments }). Synthetic inputs fed directly to the exported parser, asserting the
6
+ // resulting outcome. No network, no model. Each provider also gets one
7
+ // negative case (no native tool_call → undefined / empty).
8
+ //
9
+ // Parser entry points (file:line at authoring time) and sharing notes are
10
+ // documented inline per provider block below.
11
+ import test from 'node:test';
12
+ import assert from 'node:assert/strict';
13
+
14
+ import {
15
+ _toResponsesToolsForTest,
16
+ _toXaiResponsesInputForTest,
17
+ parseToolCalls as compatParseToolCalls,
18
+ parseResponsesToolCalls as compatParseResponsesToolCalls,
19
+ } from '../src/runtime/agent/orchestrator/providers/openai-compat.mjs';
20
+ import {
21
+ consumeCompatResponsesStream,
22
+ isInvalidToolArgsMarker,
23
+ } from '../src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs';
24
+ import { parseToolCalls as geminiParseToolCalls } from '../src/runtime/agent/orchestrator/providers/gemini.mjs';
25
+ import { parseSSEStream as anthropicParseSSEStream } from '../src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs';
26
+ import { PATCH_TOOL_DEFS } from '../src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs';
27
+
28
+ // --- Helpers ---------------------------------------------------------------
29
+
30
+ // Wraps an array of Anthropic SSE event objects in a minimal Response-like
31
+ // shape exposing the single `body.getReader()` API that parseSSEStream uses.
32
+ // Each event becomes a `data: <json>` SSE frame, preceded by its `event:` line.
33
+ function anthropicSseResponse(events) {
34
+ const encoder = new TextEncoder();
35
+ const frames = events.map((e) => {
36
+ const type = e.type || 'message';
37
+ return `event: ${type}\ndata: ${JSON.stringify(e)}\n\n`;
38
+ });
39
+ const chunks = frames.map((f) => encoder.encode(f));
40
+ let i = 0;
41
+ return {
42
+ body: {
43
+ getReader() {
44
+ return {
45
+ read() {
46
+ if (i < chunks.length) return Promise.resolve({ done: false, value: chunks[i++] });
47
+ return Promise.resolve({ done: true, value: undefined });
48
+ },
49
+ cancel() { return Promise.resolve(); },
50
+ releaseLock() {},
51
+ };
52
+ },
53
+ },
54
+ };
55
+ }
56
+
57
+ function compatResponsesEventStream(events) {
58
+ return {
59
+ async *[Symbol.asyncIterator]() {
60
+ for (const event of events) yield event;
61
+ },
62
+ };
63
+ }
64
+
65
+ // === 1. openai-compat ======================================================
66
+ // Chat path: parseToolCalls(choice, label) openai-compat.mjs:957
67
+ // Responses path: parseResponsesToolCalls(response,...) openai-compat.mjs:972
68
+ // Both exported (added `export` keyword only).
69
+
70
+ test('openai-compat (chat): native tool_calls → canonical toolCalls', () => {
71
+ const choice = {
72
+ message: {
73
+ tool_calls: [{
74
+ id: 'call_1',
75
+ type: 'function',
76
+ function: { name: 'grep', arguments: '{"pattern":"x"}' },
77
+ }],
78
+ },
79
+ finish_reason: 'tool_calls',
80
+ };
81
+ const out = compatParseToolCalls(choice, 'test');
82
+ assert.deepEqual(out, [{ id: 'call_1', name: 'grep', arguments: { pattern: 'x' } }]);
83
+ });
84
+
85
+ test('openai-compat (chat): no tool_calls → undefined', () => {
86
+ assert.equal(compatParseToolCalls({ message: { content: 'hi' }, finish_reason: 'stop' }, 'test'), undefined);
87
+ assert.equal(compatParseToolCalls({ message: { tool_calls: [] } }, 'test'), undefined);
88
+ });
89
+
90
+ test('openai-compat (responses): native function_call → canonical toolCalls', () => {
91
+ const response = {
92
+ status: 'completed',
93
+ output: [{
94
+ type: 'function_call',
95
+ call_id: 'fc_1',
96
+ name: 'read',
97
+ arguments: '{"path":"a"}',
98
+ }],
99
+ };
100
+ const out = compatParseResponsesToolCalls(response, 'test');
101
+ assert.deepEqual(out, [{ id: 'fc_1', name: 'read', arguments: { path: 'a' } }]);
102
+ });
103
+
104
+ test('openai-compat (responses): no function_call → undefined', () => {
105
+ assert.equal(compatParseResponsesToolCalls({ status: 'completed', output: [] }, 'test'), undefined);
106
+ assert.equal(compatParseResponsesToolCalls({ output: [{ type: 'message', content: [] }] }, 'test'), undefined);
107
+ });
108
+
109
+ // Native convergence: a completed function tool_call whose arguments JSON is
110
+ // malformed must NOT make the provider layer throw or swallow to {}. The parse
111
+ // failure rides through on the call's `arguments` slot as an invalid-args
112
+ // marker so the dispatch loop can return an is_error tool_result and let the
113
+ // model self-correct in the same turn.
114
+ test('openai-compat (chat): malformed tool_calls args → invalid-args marker (provider does not throw)', () => {
115
+ const choice = {
116
+ message: {
117
+ tool_calls: [{
118
+ id: 'call_bad',
119
+ type: 'function',
120
+ function: { name: 'grep', arguments: '{"pattern": dispatchAiWrapped}' },
121
+ }],
122
+ },
123
+ finish_reason: 'tool_calls',
124
+ };
125
+ const out = compatParseToolCalls(choice, 'test');
126
+ assert.equal(Array.isArray(out), true);
127
+ assert.equal(out.length, 1);
128
+ assert.equal(out[0].id, 'call_bad');
129
+ assert.equal(out[0].name, 'grep');
130
+ assert.equal(isInvalidToolArgsMarker(out[0].arguments), true);
131
+ });
132
+
133
+ test('openai-compat (responses): malformed function_call args → invalid-args marker (no throw)', () => {
134
+ const response = {
135
+ status: 'completed',
136
+ output: [{
137
+ type: 'function_call',
138
+ call_id: 'fc_bad',
139
+ name: 'read',
140
+ arguments: '{path:}',
141
+ }],
142
+ };
143
+ const out = compatParseResponsesToolCalls(response, 'test');
144
+ assert.equal(Array.isArray(out), true);
145
+ assert.equal(out.length, 1);
146
+ assert.equal(out[0].id, 'fc_bad');
147
+ assert.equal(out[0].name, 'read');
148
+ assert.equal(isInvalidToolArgsMarker(out[0].arguments), true);
149
+ });
150
+
151
+ test('openai-compat/xai Responses stream: response.completed salvages deferred function_call id/name', async () => {
152
+ const captured = [];
153
+ const out = await consumeCompatResponsesStream(compatResponsesEventStream([
154
+ { type: 'response.created', response: { id: 'resp_1', model: 'grok' } },
155
+ {
156
+ type: 'response.function_call_arguments.done',
157
+ item_id: 'fc_item_1',
158
+ arguments: '{"path":"a"}',
159
+ },
160
+ {
161
+ type: 'response.completed',
162
+ response: {
163
+ id: 'resp_1',
164
+ model: 'grok',
165
+ status: 'completed',
166
+ output: [{
167
+ type: 'function_call',
168
+ id: 'fc_item_1',
169
+ call_id: 'fc_1',
170
+ name: 'read',
171
+ arguments: '{"path":"a"}',
172
+ }],
173
+ },
174
+ },
175
+ ]), {
176
+ label: 'test',
177
+ parseResponsesToolCalls: compatParseResponsesToolCalls,
178
+ responseOutputText: () => '',
179
+ onToolCall: (call) => captured.push(call),
180
+ });
181
+ assert.deepEqual(out.toolCalls, [{ id: 'fc_1', name: 'read', arguments: { path: 'a' } }]);
182
+ assert.deepEqual(captured, [{ id: 'fc_1', name: 'read', arguments: { path: 'a' } }]);
183
+ });
184
+
185
+ test('openai-compat/xai Responses: freeform apply_patch downgrades to function schema', () => {
186
+ const tools = _toResponsesToolsForTest(PATCH_TOOL_DEFS);
187
+ const patch = tools.find((tool) => tool.name === 'apply_patch');
188
+ assert.equal(patch.type, 'function');
189
+ assert.equal(patch.format, undefined);
190
+ assert.equal(patch.parameters?.properties?.patch?.type, 'string');
191
+ assert.deepEqual(patch.parameters?.required, ['patch']);
192
+ });
193
+
194
+ test('openai-compat/xai Responses: custom_tool_call history replays as function_call', () => {
195
+ const rawPatch = '*** Begin Patch\n*** Add File: xai-history.txt\n+ok\n*** End Patch\n';
196
+ const { input } = _toXaiResponsesInputForTest([
197
+ { role: 'user', content: 'patch please' },
198
+ {
199
+ role: 'assistant',
200
+ content: '',
201
+ toolCalls: [{ id: 'call_patch_1', name: 'apply_patch', arguments: { patch: rawPatch }, nativeType: 'custom_tool_call' }],
202
+ },
203
+ { role: 'tool', toolCallId: 'call_patch_1', content: 'OK' },
204
+ ], null, { model: 'grok-composer-2.5-fast' });
205
+ assert.equal(input.some((item) => item.type === 'custom_tool_call'), false);
206
+ assert.equal(input.some((item) => item.type === 'custom_tool_call_output'), false);
207
+ const call = input.find((item) => item.type === 'function_call' && item.name === 'apply_patch');
208
+ assert.equal(call.call_id, 'call_patch_1');
209
+ assert.deepEqual(JSON.parse(call.arguments), { patch: rawPatch });
210
+ const output = input.find((item) => item.type === 'function_call_output');
211
+ assert.equal(output.call_id, 'call_patch_1');
212
+ assert.equal(output.output, 'OK');
213
+ });
214
+
215
+ // === 2. gemini =============================================================
216
+ // parseToolCalls(parts) gemini.mjs:946 (exported — `export` keyword only).
217
+ // id is a content hash → assert the `gemini_` prefix, not the exact value.
218
+
219
+ test('gemini: native functionCall parts → canonical toolCalls (hashed id)', () => {
220
+ const parts = [{ functionCall: { name: 'read', args: { path: 'a' } } }];
221
+ const out = geminiParseToolCalls(parts);
222
+ assert.equal(Array.isArray(out), true);
223
+ assert.equal(out.length, 1);
224
+ assert.equal(out[0].name, 'read');
225
+ assert.deepEqual(out[0].arguments, { path: 'a' });
226
+ assert.match(out[0].id, /^gemini_/);
227
+ });
228
+
229
+ test('gemini: no functionCall parts → undefined', () => {
230
+ assert.equal(geminiParseToolCalls([{ text: 'hello' }]), undefined);
231
+ assert.equal(geminiParseToolCalls([]), undefined);
232
+ });
233
+
234
+ // === 3. anthropic / anthropic-oauth ========================================
235
+ // tool_use block parser lives in anthropic-oauth.mjs:936 parseSSEStream
236
+ // (content_block_start/delta/stop → toolCalls.push). anthropic.mjs has NO
237
+ // independent tool_use parser: it imports and reuses the SAME parseSSEStream
238
+ // from anthropic-oauth.mjs (anthropic.mjs:12). So a single test covers both
239
+ // providers — shared parser, no duplicate test needed.
240
+
241
+ test('anthropic(-oauth): streamed tool_use block → canonical toolCalls', async () => {
242
+ const events = [
243
+ { type: 'message_start', message: { model: 'claude', usage: { input_tokens: 1 } } },
244
+ { type: 'content_block_start', index: 0, content_block: { type: 'tool_use', id: 'toolu_1', name: 'shell' } },
245
+ { type: 'content_block_delta', index: 0, delta: { type: 'input_json_delta', partial_json: '{"command":"ls"}' } },
246
+ { type: 'content_block_stop', index: 0 },
247
+ { type: 'message_delta', delta: { stop_reason: 'tool_use' }, usage: { output_tokens: 1 } },
248
+ { type: 'message_stop' },
249
+ ];
250
+ const captured = [];
251
+ const result = await anthropicParseSSEStream(
252
+ anthropicSseResponse(events),
253
+ null, // signal
254
+ () => {}, // abortStream
255
+ () => {}, // onStreamDelta
256
+ (call) => captured.push(call), // onToolCall
257
+ {}, // state
258
+ null, // onTextDelta
259
+ );
260
+ assert.deepEqual(result.toolCalls, [{ id: 'toolu_1', name: 'shell', arguments: { command: 'ls' } }]);
261
+ // Eager dispatch fired the same call exactly once.
262
+ assert.deepEqual(captured, [{ id: 'toolu_1', name: 'shell', arguments: { command: 'ls' } }]);
263
+ });
264
+
265
+ test('anthropic(-oauth): malformed streamed tool_use args → invalid-args marker, not {} dispatch', async () => {
266
+ const events = [
267
+ { type: 'message_start', message: { model: 'claude', usage: { input_tokens: 1 } } },
268
+ { type: 'content_block_start', index: 0, content_block: { type: 'tool_use', id: 'toolu_bad', name: 'shell' } },
269
+ { type: 'content_block_delta', index: 0, delta: { type: 'input_json_delta', partial_json: '{"command": dispatchAiWrapped}' } },
270
+ { type: 'content_block_stop', index: 0 },
271
+ { type: 'message_delta', delta: { stop_reason: 'tool_use' }, usage: { output_tokens: 1 } },
272
+ { type: 'message_stop' },
273
+ ];
274
+ const captured = [];
275
+ const result = await anthropicParseSSEStream(
276
+ anthropicSseResponse(events),
277
+ null,
278
+ () => {},
279
+ () => {},
280
+ (call) => captured.push(call),
281
+ {},
282
+ null,
283
+ );
284
+ assert.equal(isInvalidToolArgsMarker(result.toolCalls[0].arguments), true);
285
+ assert.equal(result.toolCalls[0].arguments.__rawArguments, '{"command": dispatchAiWrapped}');
286
+ assert.equal(isInvalidToolArgsMarker(captured[0].arguments), true);
287
+ });
288
+
289
+ test('anthropic(-oauth): text-only stream → no toolCalls', async () => {
290
+ const events = [
291
+ { type: 'message_start', message: { model: 'claude', usage: { input_tokens: 1 } } },
292
+ { type: 'content_block_start', index: 0, content_block: { type: 'text' } },
293
+ { type: 'content_block_delta', index: 0, delta: { type: 'text_delta', text: 'hello' } },
294
+ { type: 'content_block_stop', index: 0 },
295
+ { type: 'message_delta', delta: { stop_reason: 'end_turn' }, usage: { output_tokens: 1 } },
296
+ { type: 'message_stop' },
297
+ ];
298
+ const result = await anthropicParseSSEStream(
299
+ anthropicSseResponse(events),
300
+ null, () => {}, () => {}, () => {}, {}, null,
301
+ );
302
+ assert.equal(result.toolCalls, undefined);
303
+ assert.equal(result.content, 'hello');
304
+ });
305
+
306
+ // === 4. openai-oauth / openai-oauth-ws =====================================
307
+ // openai-oauth (HTTP/SSE) and openai-oauth-ws (WebSocket) both consume the
308
+ // Responses event stream inside large stateful stream loops, NOT a standalone
309
+ // parser. The HTTP path's handleEvent is a private closure inside
310
+ // sendViaHttpSse (openai-oauth.mjs:1038) and cannot be exported without
311
+ // extracting it (forbidden: no logic change). The WS path's _streamResponse
312
+ // (openai-oauth-ws.mjs:1190) IS exported but requires a live `entry.socket`
313
+ // EventEmitter and resolves only on response.completed — driving it needs a
314
+ // full fake-socket test rig, well beyond "inject synthetic input to a parser".
315
+ //
316
+ // Their canonical Responses function_call shape (call_id/name/arguments) and
317
+ // custom_tool_call handling are the SAME wire contract already asserted via
318
+ // openai-compat's parseResponsesToolCalls above, and the shared
319
+ // customToolCallFromResponseItem helper (custom-tool-wire.mjs) is imported by
320
+ // all three. We add a focused unit test for that shared custom-tool helper so
321
+ // the OAuth custom_tool_call extraction path has explicit coverage; the
322
+ // function_call path is covered by the openai-compat Responses test.
323
+
324
+ import { customToolCallFromResponseItem } from '../src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs';
325
+ import { parseToolSearchArgs } from '../src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs';
326
+
327
+ test('openai-oauth (shared custom-tool-wire): custom_tool_call item → canonical call', () => {
328
+ const item = { type: 'custom_tool_call', call_id: 'ctc_1', name: 'apply_patch', input: '*** patch ***' };
329
+ const call = customToolCallFromResponseItem(item);
330
+ assert.equal(call.id, 'ctc_1');
331
+ assert.equal(call.name, 'apply_patch');
332
+ assert.deepEqual(call.arguments, { patch: '*** patch ***' });
333
+ assert.equal(call.nativeType, 'custom_tool_call');
334
+ });
335
+
336
+ test('openai-oauth (shared custom-tool-wire): non custom_tool_call → null', () => {
337
+ assert.equal(customToolCallFromResponseItem({ type: 'function_call' }), null);
338
+ assert.equal(customToolCallFromResponseItem(null), null);
339
+ });
340
+
341
+ // WS tool_search_call.arguments parse policy (parseToolSearchArgs, module-scope
342
+ // export of openai-oauth-ws.mjs). Native convergence: malformed non-empty JSON
343
+ // becomes an invalid-args marker so the dispatch loop blocks execution and
344
+ // returns an is_error tool_result — NOT a silent {} that would dispatch
345
+ // tool_search with empty arguments. Empty/whitespace/object inputs keep their
346
+ // prior, correct behavior.
347
+ test('openai-oauth-ws (tool_search): malformed args string → invalid-args marker (not {})', () => {
348
+ const out = parseToolSearchArgs('{"query": dispatchAiWrapped}');
349
+ assert.equal(isInvalidToolArgsMarker(out), true);
350
+ assert.equal(out.__invalidToolArgs, true);
351
+ assert.equal(out.__rawArguments, '{"query": dispatchAiWrapped}');
352
+ assert.equal(typeof out.__parseError, 'string');
353
+ assert.ok(out.__parseError.length > 0);
354
+ });
355
+
356
+ test('openai-oauth-ws (tool_search): valid args / object / empty preserved', () => {
357
+ // valid JSON string → parsed object
358
+ assert.deepEqual(parseToolSearchArgs('{"query":"x"}'), { query: 'x' });
359
+ // already an object → passthrough
360
+ const obj = { query: 'y' };
361
+ assert.equal(parseToolSearchArgs(obj), obj);
362
+ // empty / whitespace / null / non-string → {} (no args, not a marker)
363
+ assert.deepEqual(parseToolSearchArgs(''), {});
364
+ assert.deepEqual(parseToolSearchArgs(' '), {});
365
+ assert.deepEqual(parseToolSearchArgs(null), {});
366
+ assert.deepEqual(parseToolSearchArgs(undefined), {});
367
+ assert.equal(isInvalidToolArgsMarker(parseToolSearchArgs('')), false);
368
+ });
369
+
370
+ // === 5. grok-oauth =========================================================
371
+ // grok-oauth has NO independent tool_call parser. GrokOAuthProvider delegates
372
+ // all request shaping AND response parsing to an inner OpenAICompatProvider
373
+ // constructed as `new OpenAICompatProvider('xai', ...)` (grok-oauth.mjs:668).
374
+ // Its tool_call extraction therefore goes through the exact
375
+ // parseToolCalls / parseResponsesToolCalls already asserted in block 1 — no
376
+ // duplicate test. (Documented as shared in the report.)
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env node
2
+ import { mkdtempSync } from 'node:fs';
3
+ import { join } from 'node:path';
4
+ import { tmpdir } from 'node:os';
5
+ import { estimateMessagesTokens } from '../src/runtime/agent/orchestrator/session/context-utils.mjs';
6
+ import { SUMMARY_PREFIX } from '../src/runtime/agent/orchestrator/session/compact.mjs';
7
+
8
+ process.env.MIXDOG_AGENT_TRACE_DISABLE = '1';
9
+ process.env.MIXDOG_DATA_DIR = mkdtempSync(join(tmpdir(), 'mixdog-reactive-compact-persist-'));
10
+
11
+ function assert(condition, message) {
12
+ if (!condition) throw new Error(message);
13
+ }
14
+
15
+ const { initProviders, getProvider } = await import('../src/runtime/agent/orchestrator/providers/registry.mjs');
16
+ const { saveSessionAsync, loadSession } = await import('../src/runtime/agent/orchestrator/session/store.mjs');
17
+
18
+ await initProviders({ 'openai-oauth': { enabled: true } });
19
+ const provider = getProvider('openai-oauth');
20
+ if (!provider || typeof provider.send !== 'function') {
21
+ process.stdout.write('reactive-compact-persist smoke skipped (openai-oauth unavailable)\n');
22
+ process.exit(0);
23
+ }
24
+
25
+ let mainSendCount = 0;
26
+ let compactSendCount = 0;
27
+ provider.send = async (_sentMessages, _model, _tools, opts = {}) => {
28
+ if (String(opts?.sessionId || '').endsWith(':compact')) {
29
+ compactSendCount += 1;
30
+ return {
31
+ content: [
32
+ '## Goal',
33
+ '- continue reactive persist smoke',
34
+ '',
35
+ '## Constraints & Preferences',
36
+ '- (none)',
37
+ '',
38
+ '## Progress',
39
+ '### Done',
40
+ '- older turn summarized',
41
+ '',
42
+ '### In Progress',
43
+ '- (none)',
44
+ '',
45
+ '### Blocked',
46
+ '- (none)',
47
+ '',
48
+ '## Key Decisions',
49
+ '- (none)',
50
+ '',
51
+ '## Next Steps',
52
+ '- continue',
53
+ '',
54
+ '## Critical Context',
55
+ '- manager.mjs',
56
+ '',
57
+ '## Relevant Files',
58
+ '- src/runtime/agent/orchestrator/session/manager.mjs: askSession overflow persist',
59
+ ].join('\n'),
60
+ };
61
+ }
62
+ mainSendCount += 1;
63
+ throw new Error('input tokens exceeds the context window');
64
+ };
65
+
66
+ const messages = [{ role: 'system', content: 'system rules stay mandatory' }];
67
+ let overflowIndex = 0;
68
+ while (estimateMessagesTokens(messages) + 512 < 8_800) {
69
+ messages.push({ role: 'user', content: `older overflow request ${overflowIndex}: ${'important detail '.repeat(90)}` });
70
+ messages.push({ role: 'assistant', content: `older overflow answer ${overflowIndex}: ${'implementation note '.repeat(90)}` });
71
+ overflowIndex += 1;
72
+ }
73
+
74
+ const sessionId = `sess_reactive_compact_persist_${process.pid}_${Date.now()}`;
75
+ const session = {
76
+ id: sessionId,
77
+ provider: 'openai-oauth',
78
+ model: 'reactive-compact-persist-smoke',
79
+ messages,
80
+ tools: [],
81
+ generation: 0,
82
+ closed: false,
83
+ contextWindow: 12_000,
84
+ rawContextWindow: 12_000,
85
+ compactBoundaryTokens: 12_000,
86
+ compaction: { auto: true, semantic: true, type: 1, compactType: 1, lastStage: 'compacting' },
87
+ cwd: process.cwd(),
88
+ sessionStartMetaInjected: true,
89
+ providerState: { xaiResponses: { previousResponseId: 'stale-after-compact' } },
90
+ };
91
+ await saveSessionAsync(session, { expectedGeneration: 0 });
92
+ const beforeCount = messages.length;
93
+
94
+ const { askSession } = await import('../src/runtime/agent/orchestrator/session/manager.mjs');
95
+
96
+ let threw = false;
97
+ let thrownCode = null;
98
+ try {
99
+ await askSession(sessionId, 'current overflow task must stay verbatim', null, null, process.cwd());
100
+ } catch (err) {
101
+ threw = true;
102
+ thrownCode = err?.code || null;
103
+ }
104
+
105
+ assert(threw, 'askSession should surface overflow error');
106
+ assert(thrownCode === 'AGENT_CONTEXT_OVERFLOW', `expected AGENT_CONTEXT_OVERFLOW, got ${thrownCode}`);
107
+ assert(mainSendCount === 2, `expected 2 main sends after reactive retry, got ${mainSendCount}`);
108
+ assert(compactSendCount === 1, `expected 1 compact send, got ${compactSendCount}`);
109
+
110
+ const reloaded = loadSession(sessionId);
111
+ assert(reloaded, 'session should reload from store after overflow');
112
+ assert((reloaded.messages || []).length < beforeCount + 1, 'persisted transcript should reflect compaction shrink');
113
+ const summary = (reloaded.messages || []).find(
114
+ (m) => m?.role === 'user' && typeof m.content === 'string' && m.content.startsWith(SUMMARY_PREFIX),
115
+ );
116
+ assert(summary, 'compacted transcript with SUMMARY_PREFIX should persist after overflow error');
117
+ assert(
118
+ (reloaded.messages || []).some((m) => m?.role === 'user' && String(m.content).includes('current overflow task must stay verbatim')),
119
+ 'current user turn should remain in persisted transcript',
120
+ );
121
+ assert(reloaded.compaction?.lastStage !== 'compacting', 'compaction lastStage should not remain stuck on compacting');
122
+ assert(reloaded.providerState === undefined, 'providerState should clear after reactive compact persist');
123
+
124
+ process.stdout.write('reactive-compact-persist smoke passed ✓\n');