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,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
+
@@ -1,15 +1,15 @@
1
1
  #!/usr/bin/env node
2
2
  import { mkdirSync, writeFileSync } from 'node:fs';
3
3
  import { join, resolve } from 'node:path';
4
- import { fileURLToPath } from 'node:url';
4
+ import { fileURLToPath, pathToFileURL } from 'node:url';
5
5
  import { loadConfig, getDefaultPreset, getPreset } from '../src/runtime/agent/orchestrator/config.mjs';
6
6
  import { initProviders, getProvider } from '../src/runtime/agent/orchestrator/providers/registry.mjs';
7
7
  import { loadSession } from '../src/runtime/agent/orchestrator/session/store.mjs';
8
8
  import { semanticCompactMessages } from '../src/runtime/agent/orchestrator/session/compact.mjs';
9
9
  import { estimateMessagesTokens } from '../src/runtime/agent/orchestrator/session/context-utils.mjs';
10
- import * as memory from '../src/runtime/memory/index.mjs';
11
10
 
12
11
  const ROOT = resolve(fileURLToPath(new URL('..', import.meta.url)));
12
+ let memoryModule = null;
13
13
 
14
14
  function arg(name, fallback = null) {
15
15
  const prefix = `--${name}=`;
@@ -24,14 +24,50 @@ function flag(name) {
24
24
  return process.argv.includes(`--${name}`);
25
25
  }
26
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
+
27
48
  function textOfResult(result) {
28
49
  if (result && typeof result === 'object' && Array.isArray(result.content)) {
29
50
  return result.content.map((part) => part?.type === 'text' ? part.text || '' : JSON.stringify(part)).join('\n');
30
51
  }
52
+ if (result && typeof result === 'object' && typeof result.content === 'string') return result.content;
31
53
  if (typeof result === 'string') return result;
32
54
  return JSON.stringify(result ?? '', null, 2);
33
55
  }
34
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
+
35
71
  function messageText(content) {
36
72
  if (typeof content === 'string') return content;
37
73
  if (Array.isArray(content)) {
@@ -62,6 +98,114 @@ function coverageScore(sourceText, candidateText) {
62
98
  return Number((hit / terms.length).toFixed(4));
63
99
  }
64
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
+
65
209
  async function providerFromConfig(config, { providerName, modelName }) {
66
210
  await initProviders(config.providers || {});
67
211
  let provider = providerName;
@@ -78,8 +222,9 @@ async function providerFromConfig(config, { providerName, modelName }) {
78
222
  return { provider, model, impl };
79
223
  }
80
224
 
81
- async function judgePair(provider, model, sourceText, semanticText, recallText) {
82
- 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"|"recall_roots"|"tie","semantic_score":1-10,"recall_roots_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: recall root/raw chunks\n${recallText.slice(0, 30000)}`;
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)}`;
83
228
  const response = await provider.send([
84
229
  { role: 'system', content: 'Judge compression quality. Output JSON only.' },
85
230
  { role: 'user', content: prompt },
@@ -91,7 +236,7 @@ async function judgePair(provider, model, sourceText, semanticText, recallText)
91
236
 
92
237
  async function main() {
93
238
  if (flag('help') || flag('h')) {
94
- process.stdout.write('usage: node scripts/session-context-bench.mjs --session <sessionId> [--provider p --model m] [--preset name] [--budget n] [--judge]\n');
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');
95
240
  return;
96
241
  }
97
242
  const sessionId = arg('session');
@@ -107,57 +252,75 @@ async function main() {
107
252
  modelName: arg('model', session.model || null),
108
253
  });
109
254
 
110
- await memory.init();
111
- await memory.handleToolCall('memory', {
112
- action: 'ingest_session',
113
- sessionId,
114
- cwd: session.cwd || ROOT,
115
- messages,
116
- limit: Number(arg('ingest-limit', 500)) || 500,
117
- });
118
- await memory.handleToolCall('memory', {
119
- action: 'cycle1',
120
- min_batch: 1,
121
- session_cap: 1,
122
- batch_size: Math.max(1, Math.min(200, Number(arg('batch-size', messages.length)) || messages.length)),
123
- });
124
- const recallDumpResult = await memory.handleToolCall('memory', {
125
- action: 'dump_session_roots',
126
- sessionId,
127
- includeRaw: true,
128
- limit: Number(arg('limit', 1000)) || 1000,
129
- });
130
- const recallText = textOfResult(recallDumpResult);
131
-
255
+ const timings = {};
132
256
  const budget = Number(arg('budget', 16000)) || 16000;
133
- const semantic = await semanticCompactMessages(impl, messages, model, budget, {
257
+ const semantic = await timed('semantic_compact_ms', timings, () => semanticCompactMessages(impl, messages, model, budget, {
134
258
  force: true,
135
259
  sessionId,
136
260
  providerName: provider,
137
261
  tailTurns: Number(arg('tail-turns', 2)) || 2,
138
262
  timeoutMs: Number(arg('timeout-ms', 60000)) || 60000,
139
- });
263
+ }));
140
264
  const semanticText = renderMessages(semantic.messages);
141
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
+ };
142
304
 
143
305
  const report = {
144
306
  sessionId,
145
307
  provider,
146
308
  model,
309
+ timings,
147
310
  source: { messages: messages.length, tokens: estimateMessagesTokens(messages), chars: sourceText.length },
148
311
  semantic: { messages: semantic.messages.length, tokens: estimateMessagesTokens(semantic.messages), chars: semanticText.length, lexicalCoverage: coverageScore(sourceText, semanticText), usage: semantic.usage || null },
149
- recallRoots: { chars: recallText.length, lexicalCoverage: coverageScore(sourceText, recallText) },
312
+ candidateB: { label: candidateB, ...candidateBMeta },
150
313
  judge: null,
151
314
  };
152
315
  if (flag('judge')) {
153
- report.judge = await judgePair(impl, model, sourceText, semanticText, recallText);
316
+ report.judge = await timed('judge_ms', timings, () => judgePair(impl, model, sourceText, semanticText, candidateBText, candidateB));
154
317
  }
155
318
 
156
319
  const outDir = resolve(arg('out', join(ROOT, '.mixdog-bench', `session-context-${sessionId}`)));
157
320
  mkdirSync(outDir, { recursive: true });
158
321
  writeFileSync(join(outDir, 'source.txt'), sourceText, 'utf8');
159
322
  writeFileSync(join(outDir, 'semantic.txt'), semanticText, 'utf8');
160
- writeFileSync(join(outDir, 'recall-roots.txt'), recallText, 'utf8');
323
+ writeFileSync(join(outDir, `${candidateB}.txt`), candidateBText, 'utf8');
161
324
  writeFileSync(join(outDir, 'report.json'), JSON.stringify(report, null, 2), 'utf8');
162
325
  process.stdout.write(`${JSON.stringify(report, null, 2)}\noutputs: ${outDir}\n`);
163
326
  }
@@ -168,5 +331,14 @@ main()
168
331
  process.exitCode = 1;
169
332
  })
170
333
  .finally(async () => {
171
- try { await memory.stop?.(); } catch {}
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
+ }
172
344
  });