mixdog 0.8.0 → 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 (285) 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 +344 -0
  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 +1223 -95
  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 +2325 -450
  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 +118 -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 +77 -29
  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/cache/post-edit-marks.mjs +4 -4
  114. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +1 -1
  115. package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
  116. package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
  117. package/src/runtime/agent/orchestrator/session/loop.mjs +851 -674
  118. package/src/runtime/agent/orchestrator/session/manager.mjs +1593 -466
  119. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
  120. package/src/runtime/agent/orchestrator/session/store.mjs +291 -46
  121. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  122. package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
  123. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
  124. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +34 -7
  125. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +1 -1
  126. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -0
  127. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -9
  128. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
  129. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
  130. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
  131. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
  132. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
  133. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
  134. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
  135. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
  136. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
  137. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
  138. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
  139. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
  140. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
  141. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
  142. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
  143. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
  144. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +1 -1
  145. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
  146. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
  147. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
  148. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
  149. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
  150. package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
  151. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
  152. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
  153. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
  154. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
  155. package/src/runtime/channels/index.mjs +152 -24
  156. package/src/runtime/channels/lib/scheduler.mjs +18 -14
  157. package/src/runtime/channels/lib/session-discovery.mjs +3 -2
  158. package/src/runtime/channels/lib/tool-format.mjs +0 -2
  159. package/src/runtime/channels/lib/transcript-discovery.mjs +3 -2
  160. package/src/runtime/channels/lib/webhook.mjs +1 -1
  161. package/src/runtime/channels/tool-defs.mjs +29 -29
  162. package/src/runtime/memory/index.mjs +635 -107
  163. package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
  164. package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
  165. package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
  166. package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
  167. package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
  168. package/src/runtime/memory/lib/memory-cycle1.mjs +38 -17
  169. package/src/runtime/memory/lib/memory-cycle2.mjs +6 -7
  170. package/src/runtime/memory/lib/memory-cycle3.mjs +4 -4
  171. package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -1
  172. package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
  173. package/src/runtime/memory/lib/memory.mjs +88 -9
  174. package/src/runtime/memory/lib/model-profile.mjs +1 -1
  175. package/src/runtime/memory/lib/pg/adapter.mjs +15 -1
  176. package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
  177. package/src/runtime/memory/lib/runtime-fetcher.mjs +37 -3
  178. package/src/runtime/memory/lib/session-ingest.mjs +194 -0
  179. package/src/runtime/memory/lib/trace-store.mjs +96 -51
  180. package/src/runtime/memory/tool-defs.mjs +46 -37
  181. package/src/runtime/search/index.mjs +102 -466
  182. package/src/runtime/search/lib/web-tools.mjs +45 -25
  183. package/src/runtime/search/tool-defs.mjs +16 -23
  184. package/src/runtime/shared/abort-controller.mjs +1 -1
  185. package/src/runtime/shared/atomic-file.mjs +4 -3
  186. package/src/runtime/shared/background-tasks.mjs +122 -11
  187. package/src/runtime/shared/child-spawn-gate.mjs +145 -0
  188. package/src/runtime/shared/config.mjs +7 -4
  189. package/src/runtime/shared/err-text.mjs +131 -4
  190. package/src/runtime/shared/llm/cost.mjs +2 -2
  191. package/src/runtime/shared/llm/http-agent.mjs +23 -7
  192. package/src/runtime/shared/llm/index.mjs +34 -11
  193. package/src/runtime/shared/llm/usage-log.mjs +4 -4
  194. package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
  195. package/src/runtime/shared/singleton-owner.mjs +104 -0
  196. package/src/runtime/shared/tool-execution-contract.mjs +199 -20
  197. package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
  198. package/src/runtime/shared/tool-surface.mjs +624 -98
  199. package/src/runtime/shared/user-data-guard.mjs +0 -2
  200. package/src/standalone/agent-task-status.mjs +203 -0
  201. package/src/standalone/agent-task-status.test.mjs +76 -0
  202. package/src/standalone/agent-tool.mjs +1913 -0
  203. package/src/standalone/channel-worker.mjs +370 -14
  204. package/src/standalone/explore-tool.mjs +165 -70
  205. package/src/standalone/folder-dialog.mjs +314 -0
  206. package/src/standalone/hook-bus.mjs +898 -22
  207. package/src/standalone/memory-runtime-proxy.mjs +320 -0
  208. package/src/standalone/projects.mjs +226 -0
  209. package/src/standalone/provider-admin.mjs +41 -24
  210. package/src/standalone/seeds.mjs +2 -69
  211. package/src/standalone/usage-dashboard.mjs +96 -8
  212. package/src/tui/App.jsx +4798 -2153
  213. package/src/tui/components/AnsiText.jsx +39 -28
  214. package/src/tui/components/ContextPanel.jsx +87 -29
  215. package/src/tui/components/Markdown.jsx +43 -77
  216. package/src/tui/components/MarkdownTable.jsx +9 -184
  217. package/src/tui/components/Message.jsx +28 -11
  218. package/src/tui/components/Picker.jsx +95 -56
  219. package/src/tui/components/PromptInput.jsx +367 -239
  220. package/src/tui/components/QueuedCommands.jsx +1 -1
  221. package/src/tui/components/SlashCommandPalette.jsx +27 -21
  222. package/src/tui/components/Spinner.jsx +67 -38
  223. package/src/tui/components/StatusLine.jsx +606 -38
  224. package/src/tui/components/TextEntryPanel.jsx +128 -9
  225. package/src/tui/components/ToolExecution.jsx +617 -368
  226. package/src/tui/components/TurnDone.jsx +3 -3
  227. package/src/tui/components/UsagePanel.jsx +3 -5
  228. package/src/tui/components/tool-output-format.mjs +365 -0
  229. package/src/tui/components/tool-output-format.test.mjs +220 -0
  230. package/src/tui/dist/index.mjs +8915 -2418
  231. package/src/tui/engine-runtime-notification.test.mjs +115 -0
  232. package/src/tui/engine-tool-result-text.test.mjs +75 -0
  233. package/src/tui/engine.mjs +1455 -279
  234. package/src/tui/figures.mjs +21 -40
  235. package/src/tui/index.jsx +75 -31
  236. package/src/tui/input-editing.mjs +25 -0
  237. package/src/tui/markdown/format-token.mjs +511 -68
  238. package/src/tui/markdown/format-token.test.mjs +216 -0
  239. package/src/tui/markdown/render-ansi.mjs +94 -0
  240. package/src/tui/markdown/render-ansi.test.mjs +108 -0
  241. package/src/tui/markdown/stream-fence.mjs +34 -0
  242. package/src/tui/markdown/stream-fence.test.mjs +26 -0
  243. package/src/tui/markdown/table-layout.mjs +250 -0
  244. package/src/tui/paste-attachments.mjs +0 -7
  245. package/src/tui/spinner-verbs.mjs +1 -2
  246. package/src/tui/statusline-ansi-bridge.mjs +172 -0
  247. package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
  248. package/src/tui/theme.mjs +746 -24
  249. package/src/tui/time-format.mjs +1 -1
  250. package/src/tui/transcript-tool-failures.mjs +67 -0
  251. package/src/tui/transcript-tool-failures.test.mjs +111 -0
  252. package/src/ui/ansi.mjs +1 -2
  253. package/src/ui/markdown.mjs +85 -26
  254. package/src/ui/markdown.test.mjs +70 -0
  255. package/src/ui/model-display.mjs +121 -0
  256. package/src/ui/session-stats.mjs +44 -0
  257. package/src/ui/statusline-context-label.test.mjs +15 -0
  258. package/src/ui/statusline.mjs +386 -178
  259. package/src/ui/tool-card.mjs +3 -16
  260. package/src/vendor/statusline/bin/statusline-lib.mjs +8 -4
  261. package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
  262. package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
  263. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
  264. package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
  265. package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
  266. package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
  267. package/src/workflows/default/WORKFLOW.md +12 -5
  268. package/src/workflows/default/workflow.json +0 -1
  269. package/src/workflows/solo/WORKFLOW.md +15 -0
  270. package/src/workflows/solo/workflow.json +7 -0
  271. package/vendor/ink/build/output.js +6 -1
  272. package/src/agents/scheduler-task.md +0 -3
  273. package/src/agents/web-researcher/AGENT.md +0 -3
  274. package/src/agents/web-researcher/agent.json +0 -6
  275. package/src/agents/webhook-handler.md +0 -3
  276. package/src/rules/bridge/00-common.md +0 -5
  277. package/src/rules/bridge/30-explorer.md +0 -4
  278. package/src/rules/lead/00-tool-lead.md +0 -5
  279. package/src/rules/shared/00-language.md +0 -3
  280. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  281. package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
  282. package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
  283. package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
  284. package/src/standalone/bridge-tool.mjs +0 -1414
  285. package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
@@ -0,0 +1,260 @@
1
+ import test from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { sanitizeToolPairs, sanitizeAnthropicContentPairs } from '../src/runtime/agent/orchestrator/session/context-utils.mjs';
4
+ import { repairTranscriptBeforeProviderSend } from '../src/runtime/agent/orchestrator/session/loop.mjs';
5
+
6
+ const TOOL_MISSING_STUB = '[Older tool result unavailable after context compaction]';
7
+
8
+ const assistantA = {
9
+ role: 'assistant',
10
+ content: '',
11
+ toolCalls: [{ id: 'call_a', name: 'grep', arguments: {} }],
12
+ };
13
+
14
+ const toolA = {
15
+ role: 'tool',
16
+ toolCallId: 'call_a',
17
+ content: 'grep output',
18
+ toolKind: 'normal',
19
+ };
20
+
21
+ test('reattaches interleaved tool result immediately after assistant toolCalls', () => {
22
+ const user = { role: 'user', content: 'continue' };
23
+ const input = [assistantA, user, toolA];
24
+ const out = sanitizeToolPairs(input);
25
+
26
+ assert.deepEqual(out, [assistantA, toolA, user]);
27
+ assert.equal(out.filter((m) => m.role === 'tool').length, 1);
28
+ });
29
+
30
+ test('contiguous block for multiple toolCalls when results were separated', () => {
31
+ const assistant = {
32
+ role: 'assistant',
33
+ content: '',
34
+ toolCalls: [
35
+ { id: 'call_a', name: 'read', arguments: {} },
36
+ { id: 'call_b', name: 'grep', arguments: {} },
37
+ ],
38
+ };
39
+ const user = { role: 'user', content: 'next' };
40
+ const input = [
41
+ assistant,
42
+ { role: 'tool', toolCallId: 'call_a', content: 'a', toolKind: 'normal' },
43
+ user,
44
+ { role: 'tool', toolCallId: 'call_b', content: 'b', toolKind: 'normal' },
45
+ ];
46
+ const out = sanitizeToolPairs(input);
47
+
48
+ assert.equal(out.length, 4);
49
+ assert.equal(out[0], assistant);
50
+ assert.equal(out[1].toolCallId, 'call_a');
51
+ assert.equal(out[1].content, 'a');
52
+ assert.equal(out[2].toolCallId, 'call_b');
53
+ assert.equal(out[3], user);
54
+ });
55
+
56
+ test('inserts stub when assistant toolCall has no matching tool message', () => {
57
+ const out = sanitizeToolPairs([assistantA, { role: 'user', content: 'hi' }]);
58
+
59
+ assert.equal(out.length, 3);
60
+ assert.equal(out[1].role, 'tool');
61
+ assert.equal(out[1].toolCallId, 'call_a');
62
+ assert.equal(out[1].content, TOOL_MISSING_STUB);
63
+ });
64
+
65
+ test('drops tool messages with no surviving assistant tool_call', () => {
66
+ const orphan = { role: 'tool', toolCallId: 'orphan', content: 'gone' };
67
+ const out = sanitizeToolPairs([orphan, { role: 'user', content: 'hi' }]);
68
+
69
+ assert.deepEqual(out, [{ role: 'user', content: 'hi' }]);
70
+ });
71
+
72
+ test('drops malformed tool messages without toolCallId', () => {
73
+ const malformed = { role: 'tool', content: 'no id' };
74
+ const out = sanitizeToolPairs([malformed, assistantA, toolA]);
75
+
76
+ assert.equal(out.length, 2);
77
+ assert.equal(out[0], assistantA);
78
+ assert.equal(out[1].toolCallId, 'call_a');
79
+ });
80
+
81
+ test('duplicate same-id prefers valid result after assistant over stale before', () => {
82
+ const stale = { role: 'tool', toolCallId: 'call_a', content: 'stale', toolKind: 'normal' };
83
+ const valid = { role: 'tool', toolCallId: 'call_a', content: 'valid', toolKind: 'normal' };
84
+ const out = sanitizeToolPairs([stale, assistantA, { role: 'user', content: 'x' }, valid]);
85
+
86
+ assert.equal(out.length, 3);
87
+ assert.equal(out[1].content, 'valid');
88
+ });
89
+
90
+ test('duplicate same-id in contiguous block after assistant keeps first there', () => {
91
+ const first = { role: 'tool', toolCallId: 'call_a', content: 'first', toolKind: 'normal' };
92
+ const second = { role: 'tool', toolCallId: 'call_a', content: 'second', toolKind: 'normal' };
93
+ const out = sanitizeToolPairs([assistantA, first, { role: 'user', content: 'x' }, second]);
94
+
95
+ assert.equal(out.length, 3);
96
+ assert.equal(out[1].content, 'first');
97
+ });
98
+
99
+ test('debugger06: assistantBlocks + role tool keeps REAL_BODY after sanitizeToolPairs', () => {
100
+ const REAL_BODY = 'REAL_BODY from debugger06 fixture';
101
+ const assistant = {
102
+ role: 'assistant',
103
+ content: '',
104
+ assistantBlocks: [
105
+ { type: 'text', text: 'calling tool' },
106
+ { type: 'tool_use', id: 'call_blocks', name: 'read', input: { path: 'x' } },
107
+ ],
108
+ };
109
+ const toolMsg = {
110
+ role: 'tool',
111
+ toolCallId: 'call_blocks',
112
+ content: REAL_BODY,
113
+ toolKind: 'normal',
114
+ };
115
+ const user = { role: 'user', content: 'next' };
116
+ const out = sanitizeToolPairs([assistant, user, toolMsg]);
117
+
118
+ assert.equal(out.length, 3);
119
+ assert.equal(out[0], assistant);
120
+ assert.equal(out[1].role, 'tool');
121
+ assert.equal(out[1].toolCallId, 'call_blocks');
122
+ assert.equal(out[1].content, REAL_BODY);
123
+ assert.equal(out[2], user);
124
+ });
125
+
126
+ test('debugger06: assistant content tool_use + role tool keeps REAL_BODY after sanitizeToolPairs', () => {
127
+ const REAL_BODY = 'REAL_BODY content-array fixture';
128
+ const assistant = {
129
+ role: 'assistant',
130
+ content: [
131
+ { type: 'text', text: 'hi' },
132
+ { type: 'tool_use', id: 'call_content', name: 'grep', input: {} },
133
+ ],
134
+ };
135
+ const toolMsg = {
136
+ role: 'tool',
137
+ toolCallId: 'call_content',
138
+ content: REAL_BODY,
139
+ };
140
+ const out = sanitizeToolPairs([assistant, toolMsg]);
141
+
142
+ assert.equal(out.length, 2);
143
+ assert.equal(out[1].content, REAL_BODY);
144
+ });
145
+
146
+ test('sanitizeAnthropicContentPairs reorders tool_result before user text for multi-tool assistant', () => {
147
+ const assistant = {
148
+ role: 'assistant',
149
+ content: [
150
+ { type: 'tool_use', id: 'call_a', name: 'read', input: {} },
151
+ { type: 'tool_use', id: 'call_b', name: 'grep', input: {} },
152
+ ],
153
+ };
154
+ const user = {
155
+ role: 'user',
156
+ content: [
157
+ { type: 'text', text: 'continue after tools' },
158
+ { type: 'tool_result', tool_use_id: 'call_a', content: 'a out' },
159
+ { type: 'tool_result', tool_use_id: 'call_b', content: 'b out' },
160
+ ],
161
+ };
162
+ const out = sanitizeAnthropicContentPairs([assistant, user]);
163
+
164
+ assert.equal(out.length, 2);
165
+ assert.equal(out[1].content[0].type, 'tool_result');
166
+ assert.equal(out[1].content[1].type, 'tool_result');
167
+ assert.equal(out[1].content[2].type, 'text');
168
+ });
169
+
170
+ test('sanitizeAnthropicContentPairs inserts user tool_result when no following user message', () => {
171
+ const assistant = {
172
+ role: 'assistant',
173
+ content: [{ type: 'tool_use', id: 'call_x', name: 'shell', input: {} }],
174
+ };
175
+ const out = sanitizeAnthropicContentPairs([assistant]);
176
+
177
+ assert.equal(out.length, 2);
178
+ assert.equal(out[0], assistant);
179
+ assert.equal(out[1].role, 'user');
180
+ assert.equal(out[1].content.length, 1);
181
+ assert.equal(out[1].content[0].type, 'tool_result');
182
+ assert.equal(out[1].content[0].tool_use_id, 'call_x');
183
+ });
184
+
185
+ test('sanitizeAnthropicContentPairs stubs missing ids and leads with all tool_result blocks', () => {
186
+ const assistant = {
187
+ role: 'assistant',
188
+ content: [
189
+ { type: 'tool_use', id: 'call_a', name: 'read', input: {} },
190
+ { type: 'tool_use', id: 'call_b', name: 'grep', input: {} },
191
+ ],
192
+ };
193
+ const user = {
194
+ role: 'user',
195
+ content: [
196
+ { type: 'text', text: 'steer' },
197
+ { type: 'tool_result', tool_use_id: 'call_a', content: 'only a' },
198
+ ],
199
+ };
200
+ const out = sanitizeAnthropicContentPairs([assistant, user]);
201
+ const blocks = out[1].content;
202
+
203
+ assert.equal(blocks[0].type, 'tool_result');
204
+ assert.equal(blocks[0].tool_use_id, 'call_b');
205
+ assert.equal(blocks[1].type, 'tool_result');
206
+ assert.equal(blocks[1].tool_use_id, 'call_a');
207
+ assert.equal(blocks[2].type, 'text');
208
+ });
209
+
210
+ test('debugger06: sanitizeAnthropicContentPairs drops leading orphan tool_result', () => {
211
+ const leadingOrphan = {
212
+ role: 'user',
213
+ content: [{ type: 'tool_result', tool_use_id: 'stale_lead', content: 'orphan' }],
214
+ };
215
+ const assistant = {
216
+ role: 'assistant',
217
+ content: [{ type: 'text', text: 'hello' }],
218
+ };
219
+ const out = sanitizeAnthropicContentPairs([leadingOrphan, assistant]);
220
+
221
+ assert.equal(out.length, 1);
222
+ assert.equal(out[0].role, 'assistant');
223
+ });
224
+
225
+ test('debugger06: sanitizeAnthropicContentPairs drops stale tool_result and reorders valid', () => {
226
+ const assistant = {
227
+ role: 'assistant',
228
+ content: [{ type: 'tool_use', id: 'call_live', name: 'read', input: {} }],
229
+ };
230
+ const user = {
231
+ role: 'user',
232
+ content: [
233
+ { type: 'tool_result', tool_use_id: 'stale_other', content: 'drop me' },
234
+ { type: 'text', text: 'between' },
235
+ { type: 'tool_result', tool_use_id: 'call_live', content: 'VALID_KEEP' },
236
+ ],
237
+ };
238
+ const out = sanitizeAnthropicContentPairs([assistant, user]);
239
+ const blocks = out[1].content;
240
+
241
+ assert.equal(blocks[0].type, 'tool_result');
242
+ assert.equal(blocks[0].tool_use_id, 'call_live');
243
+ assert.equal(blocks[0].content, 'VALID_KEEP');
244
+ assert.equal(blocks[1].type, 'text');
245
+ assert.ok(!blocks.some((b) => b.tool_use_id === 'stale_other'));
246
+ });
247
+
248
+ test('loop pre-send repair reattaches separated assistant/tool instead of deleting', () => {
249
+ const user = { role: 'user', content: 'steer' };
250
+ const msgs = [assistantA, user, { ...toolA }];
251
+ repairTranscriptBeforeProviderSend(msgs, 'test-sess');
252
+
253
+ assert.equal(msgs.length, 3);
254
+ assert.equal(msgs[0].role, 'assistant');
255
+ assert.equal(msgs[1].toolCallId, 'call_a');
256
+ assert.equal(msgs[1].content, 'grep output');
257
+ assert.equal(msgs[2], user);
258
+ });
259
+
260
+
@@ -0,0 +1,344 @@
1
+ #!/usr/bin/env node
2
+ import { mkdirSync, writeFileSync } from 'node:fs';
3
+ import { join, resolve } from 'node:path';
4
+ import { fileURLToPath, pathToFileURL } from 'node:url';
5
+ import { loadConfig, getDefaultPreset, getPreset } from '../src/runtime/agent/orchestrator/config.mjs';
6
+ import { initProviders, getProvider } from '../src/runtime/agent/orchestrator/providers/registry.mjs';
7
+ import { loadSession } from '../src/runtime/agent/orchestrator/session/store.mjs';
8
+ import { semanticCompactMessages } from '../src/runtime/agent/orchestrator/session/compact.mjs';
9
+ import { estimateMessagesTokens } from '../src/runtime/agent/orchestrator/session/context-utils.mjs';
10
+
11
+ const ROOT = resolve(fileURLToPath(new URL('..', import.meta.url)));
12
+ let memoryModule = null;
13
+
14
+ function arg(name, fallback = null) {
15
+ const prefix = `--${name}=`;
16
+ const found = process.argv.find((item) => item.startsWith(prefix));
17
+ if (found) return found.slice(prefix.length);
18
+ const idx = process.argv.indexOf(`--${name}`);
19
+ if (idx >= 0 && idx + 1 < process.argv.length) return process.argv[idx + 1];
20
+ return fallback;
21
+ }
22
+
23
+ function flag(name) {
24
+ return process.argv.includes(`--${name}`);
25
+ }
26
+
27
+ function isMainScript() {
28
+ return import.meta.url === pathToFileURL(process.argv[1] || '').href;
29
+ }
30
+
31
+ async function withTimeout(promise, ms) {
32
+ const timeoutMs = Number(ms) || 0;
33
+ if (!(timeoutMs > 0)) return { timedOut: false, value: await promise };
34
+ let timer = null;
35
+ try {
36
+ return await Promise.race([
37
+ Promise.resolve(promise).then((value) => ({ timedOut: false, value })),
38
+ new Promise((resolve) => {
39
+ timer = setTimeout(() => resolve({ timedOut: true }), timeoutMs);
40
+ timer.unref?.();
41
+ }),
42
+ ]);
43
+ } finally {
44
+ if (timer) clearTimeout(timer);
45
+ }
46
+ }
47
+
48
+ function textOfResult(result) {
49
+ if (result && typeof result === 'object' && Array.isArray(result.content)) {
50
+ return result.content.map((part) => part?.type === 'text' ? part.text || '' : JSON.stringify(part)).join('\n');
51
+ }
52
+ if (result && typeof result === 'object' && typeof result.content === 'string') return result.content;
53
+ if (typeof result === 'string') return result;
54
+ return JSON.stringify(result ?? '', null, 2);
55
+ }
56
+
57
+ function directCycle1Llm(provider, model) {
58
+ return async (_opts, prompt) => {
59
+ const response = await provider.send([
60
+ { role: 'system', content: 'You chunk memory entries. Output only the requested line format; no prose, no markdown.' },
61
+ { role: 'user', content: String(prompt || '') },
62
+ ], model, undefined, {
63
+ effort: 'low',
64
+ fast: true,
65
+ maxOutputTokens: Number(arg('cycle1-max-output-tokens', 1200)) || 1200,
66
+ });
67
+ return textOfResult(response).trim();
68
+ };
69
+ }
70
+
71
+ function messageText(content) {
72
+ if (typeof content === 'string') return content;
73
+ if (Array.isArray(content)) {
74
+ return content.map((part) => {
75
+ if (typeof part === 'string') return part;
76
+ if (part?.type === 'text') return part.text || '';
77
+ if (part?.type === 'image') return '[image]';
78
+ return part?.text || '';
79
+ }).filter(Boolean).join('\n');
80
+ }
81
+ try { return JSON.stringify(content ?? ''); } catch { return String(content ?? ''); }
82
+ }
83
+
84
+ function renderMessages(messages) {
85
+ return (messages || [])
86
+ .map((m, i) => `# message ${i + 1} role=${m.role}\n${messageText(m.content)}`.trim())
87
+ .join('\n\n');
88
+ }
89
+
90
+ function coverageScore(sourceText, candidateText) {
91
+ const terms = [...new Set(String(sourceText || '').toLowerCase().match(/[\p{L}\p{N}_./:-]{4,}/gu) || [])]
92
+ .filter((term) => !/^(https?|that|this|with|from|have|there|would|could|should)$/i.test(term))
93
+ .slice(0, 5000);
94
+ if (!terms.length) return 0;
95
+ const hay = String(candidateText || '').toLowerCase();
96
+ let hit = 0;
97
+ for (const term of terms) if (hay.includes(term)) hit += 1;
98
+ return Number((hit / terms.length).toFixed(4));
99
+ }
100
+
101
+ function latestUserText(messages, max = 1600) {
102
+ for (let i = (messages || []).length - 1; i >= 0; i -= 1) {
103
+ if (messages[i]?.role !== 'user') continue;
104
+ const text = messageText(messages[i].content).trim();
105
+ if (text) return text.slice(-max);
106
+ }
107
+ return '';
108
+ }
109
+
110
+ function recallQueryForSession(messages) {
111
+ return [
112
+ latestUserText(messages),
113
+ 'current task decisions constraints file paths changed files verification failures next steps',
114
+ ].filter(Boolean).join('\n').slice(0, 2400);
115
+ }
116
+
117
+ function parseCycle1Text(text) {
118
+ const value = String(text || '');
119
+ const num = (name) => {
120
+ const match = new RegExp(`${name}=([0-9]+)`).exec(value);
121
+ return match ? Number(match[1]) : null;
122
+ };
123
+ return {
124
+ chunks: num('chunks'),
125
+ processed: num('processed'),
126
+ skippedChunks: num('skipped_chunks'),
127
+ omitted: num('omitted'),
128
+ failedRows: num('failed_rows'),
129
+ invalidChunks: num('invalid_chunks'),
130
+ pending: num('pending'),
131
+ inFlight: /inFlight=true/.test(value),
132
+ timedOut: /timedOut=true/.test(value),
133
+ };
134
+ }
135
+
136
+ function rawChunkCount(dumpResult) {
137
+ const chunks = Array.isArray(dumpResult?.chunks) ? dumpResult.chunks : [];
138
+ return chunks.filter((chunk) => chunk?.kind === 'raw').length;
139
+ }
140
+
141
+ async function dumpSessionRoots(memoryModule, sessionId, limit) {
142
+ return await memoryModule.handleToolCall('memory', {
143
+ action: 'dump_session_roots',
144
+ sessionId,
145
+ includeRaw: true,
146
+ limit,
147
+ });
148
+ }
149
+
150
+ async function drainCycle1ForSession(memoryModule, sessionId, options = {}) {
151
+ const maxPasses = Math.max(1, Number(options.maxPasses) || 4);
152
+ const windowSize = Math.max(1, Number(options.windowSize) || 50);
153
+ const concurrency = Math.max(1, Math.min(8, Number(options.concurrency) || 4));
154
+ const rowsPerSession = Math.max(windowSize, Number(options.rowsPerSession) || (windowSize * concurrency));
155
+ const callerDeadlineMs = Math.max(0, Number(options.callerDeadlineMs) || 0);
156
+ const callLlm = typeof options.callLlm === 'function' ? options.callLlm : null;
157
+ const limit = Math.max(1, Number(options.limit) || 1000);
158
+ const runs = [];
159
+ let rawRemaining = null;
160
+ for (let pass = 1; pass <= maxPasses; pass += 1) {
161
+ const beforeDump = await dumpSessionRoots(memoryModule, sessionId, limit);
162
+ const rawBefore = rawChunkCount(beforeDump);
163
+ rawRemaining = rawBefore;
164
+ if (rawBefore === 0) break;
165
+ const startedAt = Date.now();
166
+ const cycle = await memoryModule.handleToolCall('memory', {
167
+ action: 'cycle1',
168
+ sessionId,
169
+ min_batch: 1,
170
+ session_cap: 1,
171
+ batch_size: windowSize,
172
+ window_size: windowSize,
173
+ rows_per_session: rowsPerSession,
174
+ concurrency,
175
+ ...(callerDeadlineMs > 0 ? { _callerDeadlineMs: callerDeadlineMs } : {}),
176
+ ...(callLlm ? { _callLlm: callLlm } : {}),
177
+ });
178
+ const text = textOfResult(cycle);
179
+ const parsed = parseCycle1Text(text);
180
+ const afterDump = await dumpSessionRoots(memoryModule, sessionId, limit);
181
+ const rawAfter = rawChunkCount(afterDump);
182
+ rawRemaining = rawAfter;
183
+ runs.push({
184
+ pass,
185
+ ms: Date.now() - startedAt,
186
+ rawBefore,
187
+ rawAfter,
188
+ ...parsed,
189
+ quality: cycle?.quality || null,
190
+ invalidChunksDetail: Array.isArray(cycle?.invalid_chunks) ? cycle.invalid_chunks.slice(0, 5) : [],
191
+ failedRowIds: Array.isArray(cycle?.failed_row_ids) ? cycle.failed_row_ids.slice(0, 20) : [],
192
+ text,
193
+ });
194
+ if (rawAfter === 0) break;
195
+ if ((parsed.processed || 0) === 0 && (parsed.chunks || 0) === 0 && rawAfter >= rawBefore) break;
196
+ }
197
+ return { maxPasses, windowSize, rowsPerSession, concurrency, callerDeadlineMs, rawRemaining, runs };
198
+ }
199
+
200
+ async function timed(label, timings, fn) {
201
+ const startedAt = Date.now();
202
+ try {
203
+ return await fn();
204
+ } finally {
205
+ timings[label] = Date.now() - startedAt;
206
+ }
207
+ }
208
+
209
+ async function providerFromConfig(config, { providerName, modelName }) {
210
+ await initProviders(config.providers || {});
211
+ let provider = providerName;
212
+ let model = modelName;
213
+ if (!provider || !model) {
214
+ const presetName = arg('preset', null);
215
+ const preset = presetName ? getPreset(config, presetName) : getDefaultPreset(config);
216
+ provider ||= preset?.provider;
217
+ model ||= preset?.model;
218
+ }
219
+ if (!provider || !model) throw new Error('provider/model required; pass --provider and --model or configure a default preset');
220
+ const impl = getProvider(provider);
221
+ if (!impl) throw new Error(`provider not available: ${provider}`);
222
+ return { provider, model, impl };
223
+ }
224
+
225
+ async function judgePair(provider, model, sourceText, semanticText, candidateBText, candidateBLabel) {
226
+ const safeLabel = String(candidateBLabel || 'candidate_b').replace(/[^a-z0-9_]+/gi, '_').toLowerCase() || 'candidate_b';
227
+ const prompt = `You are judging two context-compression strategies for continuing a coding session.\n\nEvaluate which preserves actionable state better for the next assistant turn. Prefer factual coverage, exact constraints, file paths, decisions, and current task continuity.\n\nReturn strict JSON: {"winner":"semantic"|"${safeLabel}"|"tie","semantic_score":1-10,"${safeLabel}_score":1-10,"reason":"short"}.\n\n# Original session excerpt\n${sourceText.slice(0, 30000)}\n\n# Candidate A: semantic compact\n${semanticText.slice(0, 30000)}\n\n# Candidate B: ${safeLabel}\n${candidateBText.slice(0, 30000)}`;
228
+ const response = await provider.send([
229
+ { role: 'system', content: 'Judge compression quality. Output JSON only.' },
230
+ { role: 'user', content: prompt },
231
+ ], model, undefined, { effort: 'low', fast: true, maxOutputTokens: 800 });
232
+ const text = String(response?.content || '').trim();
233
+ try { return JSON.parse(text.replace(/^```json\s*/i, '').replace(/```$/i, '').trim()); }
234
+ catch { return { raw: text }; }
235
+ }
236
+
237
+ async function main() {
238
+ if (flag('help') || flag('h')) {
239
+ process.stdout.write('usage: node scripts/session-context-bench.mjs --session <sessionId> [--provider p --model m] [--preset name] [--budget n] [--judge] [--recall-query q]\n');
240
+ return;
241
+ }
242
+ const sessionId = arg('session');
243
+ if (!sessionId) throw new Error('usage: node scripts/session-context-bench.mjs --session <sessionId> [--provider p --model m] [--judge]');
244
+ const session = loadSession(sessionId);
245
+ if (!session) throw new Error(`session not found: ${sessionId}`);
246
+ const messages = Array.isArray(session.messages) ? session.messages : [];
247
+ if (!messages.length) throw new Error(`session has no messages: ${sessionId}`);
248
+
249
+ const config = loadConfig();
250
+ const { provider, model, impl } = await providerFromConfig(config, {
251
+ providerName: arg('provider', session.provider || null),
252
+ modelName: arg('model', session.model || null),
253
+ });
254
+
255
+ const timings = {};
256
+ const budget = Number(arg('budget', 16000)) || 16000;
257
+ const semantic = await timed('semantic_compact_ms', timings, () => semanticCompactMessages(impl, messages, model, budget, {
258
+ force: true,
259
+ sessionId,
260
+ providerName: provider,
261
+ tailTurns: Number(arg('tail-turns', 2)) || 2,
262
+ timeoutMs: Number(arg('timeout-ms', 60000)) || 60000,
263
+ }));
264
+ const semanticText = renderMessages(semantic.messages);
265
+ const sourceText = renderMessages(messages);
266
+ const candidateB = 'recall';
267
+ let candidateBText = '';
268
+ let candidateBMeta = null;
269
+ memoryModule = await import('../src/runtime/memory/index.mjs');
270
+ await timed('memory_init_ms', timings, () => memoryModule.init());
271
+ const ingest = await timed('recall_ingest_ms', timings, () => memoryModule.handleToolCall('memory', {
272
+ action: 'ingest_session',
273
+ sessionId,
274
+ cwd: session.cwd || ROOT,
275
+ messages,
276
+ limit: Number(arg('ingest-limit', 500)) || 500,
277
+ }));
278
+ const cycle1 = await timed('recall_cycle1_drain_ms', timings, () => drainCycle1ForSession(memoryModule, sessionId, {
279
+ maxPasses: Number(arg('cycle1-passes', 4)) || 4,
280
+ windowSize: Number(arg('window-size', arg('batch-size', 50))) || 50,
281
+ rowsPerSession: Number(arg('rows-per-session', 0)) || 0,
282
+ concurrency: Number(arg('concurrency', 4)) || 4,
283
+ callerDeadlineMs: Number(arg('cycle1-deadline-ms', 120000)) || 120000,
284
+ callLlm: directCycle1Llm(impl, model),
285
+ limit: Number(arg('limit', 1000)) || 1000,
286
+ }));
287
+ const recallQuery = String(arg('recall-query', recallQueryForSession(messages)) || '').trim();
288
+ const recallResult = await timed('recall_search_ms', timings, () => memoryModule.handleToolCall('memory', {
289
+ action: 'search',
290
+ sessionId,
291
+ query: recallQuery,
292
+ limit: Number(arg('recall-limit', 100)) || 100,
293
+ includeArchived: true,
294
+ includeMembers: true,
295
+ }));
296
+ candidateBText = textOfResult(recallResult);
297
+ candidateBMeta = {
298
+ chars: candidateBText.length,
299
+ lexicalCoverage: coverageScore(sourceText, candidateBText),
300
+ query: recallQuery,
301
+ ingest: textOfResult(ingest),
302
+ cycle1,
303
+ };
304
+
305
+ const report = {
306
+ sessionId,
307
+ provider,
308
+ model,
309
+ timings,
310
+ source: { messages: messages.length, tokens: estimateMessagesTokens(messages), chars: sourceText.length },
311
+ semantic: { messages: semantic.messages.length, tokens: estimateMessagesTokens(semantic.messages), chars: semanticText.length, lexicalCoverage: coverageScore(sourceText, semanticText), usage: semantic.usage || null },
312
+ candidateB: { label: candidateB, ...candidateBMeta },
313
+ judge: null,
314
+ };
315
+ if (flag('judge')) {
316
+ report.judge = await timed('judge_ms', timings, () => judgePair(impl, model, sourceText, semanticText, candidateBText, candidateB));
317
+ }
318
+
319
+ const outDir = resolve(arg('out', join(ROOT, '.mixdog-bench', `session-context-${sessionId}`)));
320
+ mkdirSync(outDir, { recursive: true });
321
+ writeFileSync(join(outDir, 'source.txt'), sourceText, 'utf8');
322
+ writeFileSync(join(outDir, 'semantic.txt'), semanticText, 'utf8');
323
+ writeFileSync(join(outDir, `${candidateB}.txt`), candidateBText, 'utf8');
324
+ writeFileSync(join(outDir, 'report.json'), JSON.stringify(report, null, 2), 'utf8');
325
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\noutputs: ${outDir}\n`);
326
+ }
327
+
328
+ main()
329
+ .catch((err) => {
330
+ process.stderr.write(`${err?.stack || err?.message || err}\n`);
331
+ process.exitCode = 1;
332
+ })
333
+ .finally(async () => {
334
+ try {
335
+ if (memoryModule?.stop) {
336
+ const stopped = await withTimeout(memoryModule.stop(), Number(arg('stop-timeout-ms', 5000)) || 5000);
337
+ if (stopped.timedOut) process.stderr.write('warning: memory stop timed out; forcing bench exit\n');
338
+ }
339
+ } catch {}
340
+ if (isMainScript() && !flag('no-force-exit')) {
341
+ await new Promise((resolve) => process.stdout.write('', resolve));
342
+ setImmediate(() => process.exit(process.exitCode || 0));
343
+ }
344
+ });