mixdog 0.8.1 → 0.9.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 (280) hide show
  1. package/README.md +47 -23
  2. package/package.json +33 -27
  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 -7
  62. package/src/lib/rules-builder.cjs +240 -96
  63. package/src/lib/text-utils.cjs +1 -1
  64. package/src/mixdog-session-runtime.mjs +2212 -446
  65. package/src/output-styles/default.md +12 -28
  66. package/src/output-styles/extreme-simple.md +9 -6
  67. package/src/output-styles/simple.md +22 -9
  68. package/src/repl.mjs +110 -59
  69. package/src/rules/agent/00-common.md +15 -0
  70. package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
  71. package/src/rules/agent/30-explorer.md +22 -0
  72. package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
  73. package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
  74. package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
  75. package/src/rules/lead/01-general.md +9 -5
  76. package/src/rules/lead/04-workflow.md +51 -12
  77. package/src/rules/lead/lead-tool.md +6 -0
  78. package/src/rules/shared/01-tool.md +12 -1
  79. package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
  80. package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
  81. package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
  82. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
  83. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
  84. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
  85. package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
  86. package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
  87. package/src/runtime/agent/orchestrator/config.mjs +174 -55
  88. package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
  89. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
  90. package/src/runtime/agent/orchestrator/internal-roles.mjs +76 -28
  91. package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
  92. package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
  93. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
  94. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +377 -243
  95. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +146 -93
  96. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
  97. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
  98. package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
  99. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
  100. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
  101. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
  102. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
  103. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
  104. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
  106. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +221 -70
  107. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +477 -147
  108. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -496
  109. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
  110. package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
  111. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +31 -11
  112. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
  113. package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
  114. package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
  115. package/src/runtime/agent/orchestrator/session/loop.mjs +840 -612
  116. package/src/runtime/agent/orchestrator/session/manager.mjs +1574 -459
  117. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
  118. package/src/runtime/agent/orchestrator/session/store.mjs +67 -29
  119. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  120. package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
  121. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
  122. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +34 -7
  123. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -0
  124. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -9
  125. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
  126. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
  127. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
  128. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
  129. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
  130. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
  131. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
  132. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
  133. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
  134. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
  135. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
  136. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
  137. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
  138. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
  139. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
  140. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
  141. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
  142. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
  143. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
  144. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
  145. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
  146. package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
  147. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
  148. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
  149. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
  150. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
  151. package/src/runtime/channels/index.mjs +152 -24
  152. package/src/runtime/channels/lib/scheduler.mjs +18 -14
  153. package/src/runtime/channels/lib/session-discovery.mjs +3 -2
  154. package/src/runtime/channels/lib/transcript-discovery.mjs +3 -2
  155. package/src/runtime/channels/lib/webhook.mjs +1 -1
  156. package/src/runtime/channels/tool-defs.mjs +29 -29
  157. package/src/runtime/memory/index.mjs +520 -98
  158. package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
  159. package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
  160. package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
  161. package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
  162. package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
  163. package/src/runtime/memory/lib/memory-cycle1.mjs +28 -7
  164. package/src/runtime/memory/lib/memory-cycle2.mjs +4 -5
  165. package/src/runtime/memory/lib/memory-cycle3.mjs +2 -2
  166. package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -1
  167. package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
  168. package/src/runtime/memory/lib/memory.mjs +88 -9
  169. package/src/runtime/memory/lib/model-profile.mjs +1 -1
  170. package/src/runtime/memory/lib/pg/adapter.mjs +1 -1
  171. package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
  172. package/src/runtime/memory/lib/runtime-fetcher.mjs +37 -3
  173. package/src/runtime/memory/lib/session-ingest.mjs +194 -0
  174. package/src/runtime/memory/lib/trace-store.mjs +96 -51
  175. package/src/runtime/memory/tool-defs.mjs +46 -37
  176. package/src/runtime/search/index.mjs +102 -466
  177. package/src/runtime/search/lib/web-tools.mjs +45 -25
  178. package/src/runtime/search/tool-defs.mjs +16 -23
  179. package/src/runtime/shared/abort-controller.mjs +1 -1
  180. package/src/runtime/shared/atomic-file.mjs +4 -3
  181. package/src/runtime/shared/background-tasks.mjs +122 -11
  182. package/src/runtime/shared/child-spawn-gate.mjs +145 -0
  183. package/src/runtime/shared/config.mjs +7 -4
  184. package/src/runtime/shared/err-text.mjs +131 -4
  185. package/src/runtime/shared/llm/cost.mjs +2 -2
  186. package/src/runtime/shared/llm/http-agent.mjs +23 -7
  187. package/src/runtime/shared/llm/index.mjs +34 -11
  188. package/src/runtime/shared/llm/usage-log.mjs +4 -4
  189. package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
  190. package/src/runtime/shared/singleton-owner.mjs +104 -0
  191. package/src/runtime/shared/tool-execution-contract.mjs +199 -20
  192. package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
  193. package/src/runtime/shared/tool-surface.mjs +624 -95
  194. package/src/runtime/shared/user-data-guard.mjs +0 -2
  195. package/src/standalone/agent-task-status.mjs +203 -0
  196. package/src/standalone/agent-task-status.test.mjs +76 -0
  197. package/src/standalone/agent-tool.mjs +1913 -0
  198. package/src/standalone/channel-worker.mjs +370 -14
  199. package/src/standalone/explore-tool.mjs +165 -70
  200. package/src/standalone/folder-dialog.mjs +314 -0
  201. package/src/standalone/hook-bus.mjs +898 -22
  202. package/src/standalone/memory-runtime-proxy.mjs +320 -0
  203. package/src/standalone/projects.mjs +226 -0
  204. package/src/standalone/provider-admin.mjs +41 -24
  205. package/src/standalone/seeds.mjs +2 -69
  206. package/src/standalone/usage-dashboard.mjs +96 -8
  207. package/src/tui/App.jsx +4788 -2151
  208. package/src/tui/components/AnsiText.jsx +39 -28
  209. package/src/tui/components/ContextPanel.jsx +14 -4
  210. package/src/tui/components/Markdown.jsx +43 -77
  211. package/src/tui/components/MarkdownTable.jsx +9 -184
  212. package/src/tui/components/Message.jsx +28 -11
  213. package/src/tui/components/Picker.jsx +95 -56
  214. package/src/tui/components/PromptInput.jsx +367 -239
  215. package/src/tui/components/QueuedCommands.jsx +1 -1
  216. package/src/tui/components/SlashCommandPalette.jsx +27 -21
  217. package/src/tui/components/Spinner.jsx +67 -38
  218. package/src/tui/components/StatusLine.jsx +606 -38
  219. package/src/tui/components/TextEntryPanel.jsx +128 -9
  220. package/src/tui/components/ToolExecution.jsx +592 -362
  221. package/src/tui/components/TurnDone.jsx +3 -3
  222. package/src/tui/components/UsagePanel.jsx +3 -5
  223. package/src/tui/components/tool-output-format.mjs +365 -0
  224. package/src/tui/components/tool-output-format.test.mjs +220 -0
  225. package/src/tui/dist/index.mjs +8826 -2390
  226. package/src/tui/engine-runtime-notification.test.mjs +115 -0
  227. package/src/tui/engine-tool-result-text.test.mjs +75 -0
  228. package/src/tui/engine.mjs +1455 -279
  229. package/src/tui/figures.mjs +21 -40
  230. package/src/tui/index.jsx +75 -31
  231. package/src/tui/input-editing.mjs +25 -0
  232. package/src/tui/markdown/format-token.mjs +511 -68
  233. package/src/tui/markdown/format-token.test.mjs +216 -0
  234. package/src/tui/markdown/render-ansi.mjs +94 -0
  235. package/src/tui/markdown/render-ansi.test.mjs +108 -0
  236. package/src/tui/markdown/stream-fence.mjs +34 -0
  237. package/src/tui/markdown/stream-fence.test.mjs +26 -0
  238. package/src/tui/markdown/table-layout.mjs +250 -0
  239. package/src/tui/paste-attachments.mjs +0 -7
  240. package/src/tui/spinner-verbs.mjs +1 -2
  241. package/src/tui/statusline-ansi-bridge.mjs +172 -0
  242. package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
  243. package/src/tui/theme.mjs +746 -24
  244. package/src/tui/time-format.mjs +1 -1
  245. package/src/tui/transcript-tool-failures.mjs +67 -0
  246. package/src/tui/transcript-tool-failures.test.mjs +111 -0
  247. package/src/ui/ansi.mjs +1 -2
  248. package/src/ui/markdown.mjs +85 -26
  249. package/src/ui/markdown.test.mjs +70 -0
  250. package/src/ui/model-display.mjs +121 -0
  251. package/src/ui/session-stats.mjs +44 -0
  252. package/src/ui/statusline-context-label.test.mjs +15 -0
  253. package/src/ui/statusline.mjs +386 -178
  254. package/src/ui/tool-card.mjs +2 -13
  255. package/src/vendor/statusline/bin/statusline-lib.mjs +8 -4
  256. package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
  257. package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
  258. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
  259. package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
  260. package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
  261. package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
  262. package/src/workflows/default/WORKFLOW.md +12 -5
  263. package/src/workflows/default/workflow.json +0 -1
  264. package/src/workflows/solo/WORKFLOW.md +15 -0
  265. package/src/workflows/solo/workflow.json +7 -0
  266. package/vendor/ink/build/output.js +6 -1
  267. package/src/agents/scheduler-task.md +0 -3
  268. package/src/agents/web-researcher/AGENT.md +0 -3
  269. package/src/agents/web-researcher/agent.json +0 -6
  270. package/src/agents/webhook-handler.md +0 -3
  271. package/src/rules/bridge/00-common.md +0 -5
  272. package/src/rules/bridge/30-explorer.md +0 -4
  273. package/src/rules/lead/00-tool-lead.md +0 -5
  274. package/src/rules/shared/00-language.md +0 -3
  275. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  276. package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
  277. package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
  278. package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
  279. package/src/standalone/bridge-tool.mjs +0 -1414
  280. 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');