mixdog 0.9.151 → 0.9.153

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 (307) hide show
  1. package/LICENSES/browser-import-NOTICE.txt +11 -0
  2. package/NOTICE.md +11 -0
  3. package/README.md +108 -68
  4. package/package.json +17 -4
  5. package/scripts/computer-task-cost-report.mjs +144 -0
  6. package/scripts/run-office-live-tests.mjs +32 -0
  7. package/scripts/tool-stress.mjs +0 -2
  8. package/src/agents/front-worker/AGENT.md +13 -0
  9. package/src/agents/front-worker/agent.json +6 -0
  10. package/src/agents/heavy-worker/AGENT.md +10 -11
  11. package/src/agents/heavy-worker/agent.json +1 -1
  12. package/src/agents/maintainer/AGENT.md +0 -2
  13. package/src/agents/reviewer/AGENT.md +9 -13
  14. package/src/agents/reviewer/agent.json +1 -1
  15. package/src/agents/security/AGENT.md +14 -0
  16. package/src/agents/security/agent.json +6 -0
  17. package/src/agents/worker/AGENT.md +8 -8
  18. package/src/agents/worker/agent.json +1 -1
  19. package/src/agents/writer/AGENT.md +14 -0
  20. package/src/agents/writer/agent.json +6 -0
  21. package/src/defaults/skills/setup/SKILL.md +18 -27
  22. package/src/headless-exec.mjs +3 -10
  23. package/src/headless-exec.test.mjs +2 -7
  24. package/src/rules/agent/00-common.md +4 -7
  25. package/src/rules/lead/01-general.md +0 -3
  26. package/src/rules/shared/10-tool-workflow.md +10 -2
  27. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +183 -26
  28. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.test.mjs +378 -0
  29. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +4 -0
  30. package/src/runtime/agent/orchestrator/cache-break-trace.mjs +109 -0
  31. package/src/runtime/agent/orchestrator/cache-break-trace.test.mjs +115 -0
  32. package/src/runtime/agent/orchestrator/config.mjs +13 -68
  33. package/src/runtime/agent/orchestrator/context/collect-skills.test.mjs +153 -0
  34. package/src/runtime/agent/orchestrator/context/collect.mjs +49 -45
  35. package/src/runtime/agent/orchestrator/mcp/client.mjs +242 -15
  36. package/src/runtime/agent/orchestrator/mcp/features.test.mjs +66 -0
  37. package/src/runtime/agent/orchestrator/mcp/security.test.mjs +36 -20
  38. package/src/runtime/agent/orchestrator/providers/admission-scheduler-metrics.test.mjs +47 -0
  39. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +16 -3
  40. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +54 -33
  41. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -0
  42. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
  43. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +7 -0
  44. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +3 -8
  45. package/src/runtime/agent/orchestrator/providers/gemini.mjs +7 -0
  46. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +19 -3
  47. package/src/runtime/agent/orchestrator/providers/lib/provider-replay.mjs +50 -0
  48. package/src/runtime/agent/orchestrator/providers/lib/sse-framing.mjs +93 -0
  49. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +30 -9
  50. package/src/runtime/agent/orchestrator/providers/media-parity.test.mjs +59 -0
  51. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +3 -3
  52. package/src/runtime/agent/orchestrator/providers/openai-codex-identity.test.mjs +71 -0
  53. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +18 -26
  54. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +8 -0
  55. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +7 -0
  56. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +25 -2
  57. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +35 -4
  58. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +47 -2
  59. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +103 -12
  60. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +13 -0
  61. package/src/runtime/agent/orchestrator/providers/openai-turn-state.mjs +68 -0
  62. package/src/runtime/agent/orchestrator/providers/openai-ws-delta-parity.test.mjs +122 -6
  63. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +36 -11
  64. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +144 -93
  65. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +18 -13
  66. package/src/runtime/agent/orchestrator/providers/provider-replay.test.mjs +288 -0
  67. package/src/runtime/agent/orchestrator/providers/sse-framing.test.mjs +719 -0
  68. package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +399 -39
  69. package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +26 -0
  70. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +86 -7
  71. package/src/runtime/agent/orchestrator/session/agent-loop.test.mjs +222 -0
  72. package/src/runtime/agent/orchestrator/session/browser-snapshot-supersession.mjs +61 -0
  73. package/src/runtime/agent/orchestrator/session/browser-snapshot-supersession.test.mjs +83 -0
  74. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +13 -0
  75. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +26 -2
  76. package/src/runtime/agent/orchestrator/session/compact-policy.test.mjs +112 -0
  77. package/src/runtime/agent/orchestrator/session/compaction-read-reset.test.mjs +192 -0
  78. package/src/runtime/agent/orchestrator/session/context-usage-restart.test.mjs +72 -0
  79. package/src/runtime/agent/orchestrator/session/context-utils.mjs +100 -15
  80. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +17 -1
  81. package/src/runtime/agent/orchestrator/session/eager-dispatch.test.mjs +74 -0
  82. package/src/runtime/agent/orchestrator/session/image-strip-recovery.mjs +12 -8
  83. package/src/runtime/agent/orchestrator/session/image-strip-recovery.test.mjs +20 -1
  84. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +110 -2
  85. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +3 -16
  86. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.test.mjs +28 -0
  87. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +2 -2
  88. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +6 -0
  89. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +15 -1
  90. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +6 -0
  91. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +111 -39
  92. package/src/runtime/agent/orchestrator/session/manager/ask-session.test.mjs +46 -0
  93. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +30 -6
  94. package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +3 -0
  95. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +27 -3
  96. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +26 -54
  97. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.test.mjs +63 -1
  98. package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +3 -0
  99. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +4 -110
  100. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +580 -0
  101. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.test.mjs +593 -0
  102. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +119 -190
  103. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +115 -0
  104. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +43 -8
  105. package/src/runtime/agent/orchestrator/session/provider-prefix-guard.mjs +129 -12
  106. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +9 -2
  107. package/src/runtime/agent/orchestrator/session/store/listing.mjs +8 -1
  108. package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +15 -1
  109. package/src/runtime/agent/orchestrator/session/store-live-publication.test.mjs +83 -0
  110. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +36 -2
  111. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +157 -30
  112. package/src/runtime/agent/orchestrator/session/store-summary-reader.test.mjs +64 -0
  113. package/src/runtime/agent/orchestrator/session/store-summary-visibility.mjs +57 -0
  114. package/src/runtime/agent/orchestrator/session/store-summary-visibility.test.mjs +219 -0
  115. package/src/runtime/agent/orchestrator/session/store.mjs +26 -2
  116. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +22 -0
  117. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool-cwd-env.test.mjs +14 -0
  118. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +19 -10
  119. package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +30 -14
  120. package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +15 -4
  121. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +73 -15
  122. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +76 -2
  123. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +24 -683
  124. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +64 -72
  125. package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +2 -5
  126. package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.mjs +75 -0
  127. package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.test.mjs +24 -0
  128. package/src/runtime/agent/orchestrator/tools/builtin/read-special-files.mjs +6 -6
  129. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +12 -0
  130. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +4 -1
  131. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +24 -5
  132. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.test.mjs +44 -0
  133. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +12 -2
  134. package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +35 -21
  135. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +28 -13
  136. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.test.mjs +54 -0
  137. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
  138. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  139. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +12 -0
  140. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
  141. package/src/runtime/attachments/pdf-extract.mjs +3 -0
  142. package/src/runtime/attachments/pdfjs-runtime.mjs +13 -0
  143. package/src/runtime/bridge-clients.test.mjs +827 -0
  144. package/src/runtime/browser-bridge/action-schema.mjs +251 -0
  145. package/src/runtime/browser-bridge/client.mjs +152 -0
  146. package/src/runtime/browser-bridge/tool-defs.mjs +159 -0
  147. package/src/runtime/computer-bridge/action-schema.mjs +653 -0
  148. package/src/runtime/computer-bridge/client.mjs +231 -0
  149. package/src/runtime/computer-bridge/tool-defs.mjs +33 -0
  150. package/src/runtime/media/store.mjs +18 -8
  151. package/src/runtime/media/store.test.mjs +14 -0
  152. package/src/runtime/memory/index.mjs +1 -1
  153. package/src/runtime/memory/lib/transcript-ingest.mjs +8 -6
  154. package/src/runtime/memory/lib/transcript-ingest.test.mjs +52 -0
  155. package/src/runtime/office/assurance-benchmark.mjs +358 -0
  156. package/src/runtime/office/assurance.mjs +833 -0
  157. package/src/runtime/office/benchmark.mjs +496 -0
  158. package/src/runtime/office/capabilities.mjs +551 -0
  159. package/src/runtime/office/com-adapter.mjs +283 -0
  160. package/src/runtime/office/content-model.mjs +183 -0
  161. package/src/runtime/office/contract-benchmark.mjs +105 -0
  162. package/src/runtime/office/design-library.mjs +1562 -0
  163. package/src/runtime/office/design-system.mjs +1966 -0
  164. package/src/runtime/office/index.mjs +2413 -0
  165. package/src/runtime/office/journal.mjs +82 -0
  166. package/src/runtime/office/office-assurance.test.mjs +371 -0
  167. package/src/runtime/office/office-com-host.ps1 +3781 -0
  168. package/src/runtime/office/office-com-session-host.ps1 +855 -0
  169. package/src/runtime/office/office-live-runtime.test.mjs +819 -0
  170. package/src/runtime/office/office-runtime.test.mjs +2070 -0
  171. package/src/runtime/office/ooxml-validator.mjs +271 -0
  172. package/src/runtime/office/pagination.mjs +132 -0
  173. package/src/runtime/office/pdf-adapter.mjs +591 -0
  174. package/src/runtime/office/pdf-analysis.mjs +372 -0
  175. package/src/runtime/office/pdf-render.mjs +167 -0
  176. package/src/runtime/office/pdf-security.mjs +63 -0
  177. package/src/runtime/office/polish-benchmark.mjs +190 -0
  178. package/src/runtime/office/portable-ooxml.mjs +1867 -0
  179. package/src/runtime/office/quality-live-benchmark.mjs +486 -0
  180. package/src/runtime/office/quality-pipeline.mjs +136 -0
  181. package/src/runtime/office/tabular.mjs +302 -0
  182. package/src/runtime/office/templates/mixdog-executive.pptx +0 -0
  183. package/src/runtime/office/templates/mixdog-executive.pptx.mixdog.json +190 -0
  184. package/src/runtime/office/tool-defs.mjs +113 -0
  185. package/src/runtime/office/visual-diff.mjs +89 -0
  186. package/src/runtime/office/xlsx-assertions.mjs +172 -0
  187. package/src/runtime/office/xlsx-contract.mjs +147 -0
  188. package/src/runtime/shared/agent-route-config.mjs +9 -0
  189. package/src/runtime/shared/child-spawn-gate.mjs +17 -0
  190. package/src/runtime/shared/child-spawn-remote.mjs +69 -7
  191. package/src/runtime/shared/child-spawn-remote.test.mjs +124 -0
  192. package/src/runtime/shared/pristine-execution-contract.json +2 -1
  193. package/src/runtime/shared/provider-api-key.mjs +1 -0
  194. package/src/runtime/shared/session-runtime-health.mjs +91 -0
  195. package/src/runtime/shared/session-runtime-health.test.mjs +53 -0
  196. package/src/runtime/shared/skill-document.mjs +86 -0
  197. package/src/runtime/shared/skill-document.test.mjs +81 -0
  198. package/src/runtime/shared/tool-execution-contract.mjs +16 -0
  199. package/src/runtime/shared/tool-surface.mjs +27 -0
  200. package/src/runtime/shared/turn-snapshot-store.mjs +1 -0
  201. package/src/runtime/shared/turn-snapshot.mjs +32 -9
  202. package/src/runtime/shared/user-cwd.mjs +33 -14
  203. package/src/runtime/shared/user-cwd.test.mjs +43 -0
  204. package/src/session-runtime/agent-disable.test.mjs +20 -1
  205. package/src/session-runtime/context-status.mjs +9 -3
  206. package/src/session-runtime/cwd-plugins.mjs +26 -26
  207. package/src/session-runtime/cwd-plugins.test.mjs +75 -0
  208. package/src/session-runtime/cwd-tool-routing.test.mjs +5 -0
  209. package/src/session-runtime/deferred-tool-delta.mjs +104 -0
  210. package/src/session-runtime/deferred-tool-delta.test.mjs +40 -0
  211. package/src/session-runtime/global-extensions.mjs +26 -0
  212. package/src/session-runtime/global-extensions.test.mjs +28 -0
  213. package/src/session-runtime/global-settings-session-identity.test.mjs +76 -0
  214. package/src/session-runtime/goal-runtime.mjs +1141 -0
  215. package/src/session-runtime/goal-runtime.test.mjs +645 -0
  216. package/src/session-runtime/lifecycle-api.mjs +58 -11
  217. package/src/session-runtime/lifecycle-api.test.mjs +183 -0
  218. package/src/session-runtime/mcp-glue.mjs +49 -47
  219. package/src/session-runtime/plugin-mcp-standard.test.mjs +177 -0
  220. package/src/session-runtime/plugin-mcp.mjs +96 -22
  221. package/src/session-runtime/prewarm.mjs +1 -10
  222. package/src/session-runtime/provider-request-snapshot.mjs +33 -9
  223. package/src/session-runtime/provider-request-snapshot.test.mjs +27 -5
  224. package/src/session-runtime/resource-api-global.test.mjs +109 -0
  225. package/src/session-runtime/resource-api.mjs +168 -116
  226. package/src/session-runtime/runtime-core.mjs +208 -29
  227. package/src/session-runtime/runtime-tunables.mjs +6 -8
  228. package/src/session-runtime/session-lifecycle.mjs +34 -12
  229. package/src/session-runtime/session-title.mjs +2 -2
  230. package/src/session-runtime/session-turn-api.mjs +37 -25
  231. package/src/session-runtime/skills-api.mjs +79 -25
  232. package/src/session-runtime/skills-api.test.mjs +99 -0
  233. package/src/session-runtime/tool-catalog-data.mjs +1 -1
  234. package/src/session-runtime/tool-catalog-schema.mjs +0 -5
  235. package/src/session-runtime/tool-catalog.mjs +42 -112
  236. package/src/session-runtime/tool-defs.mjs +6 -1
  237. package/src/session-runtime/tool-policy-surface.test.mjs +93 -17
  238. package/src/session-runtime/tool-surface.mjs +30 -8
  239. package/src/session-runtime/workflow-agents-api.mjs +8 -25
  240. package/src/session-runtime/workflow.mjs +8 -6
  241. package/src/standalone/agent-dispatch-broker.mjs +5 -9
  242. package/src/standalone/agent-tool/helpers.mjs +2 -2
  243. package/src/standalone/agent-tool/job-views.mjs +3 -1
  244. package/src/standalone/agent-tool/notify.mjs +11 -9
  245. package/src/standalone/agent-tool/notify.test.mjs +37 -0
  246. package/src/standalone/agent-tool/render.mjs +3 -0
  247. package/src/standalone/agent-tool/spawn-flow.mjs +61 -16
  248. package/src/standalone/agent-tool/spawn-flow.test.mjs +41 -0
  249. package/src/standalone/agent-tool/tool-def.mjs +1 -1
  250. package/src/standalone/agent-tool/worker-index.mjs +123 -0
  251. package/src/standalone/agent-tool/worker-index.test.mjs +132 -0
  252. package/src/standalone/agent-tool/worker-rows.mjs +2 -1
  253. package/src/standalone/agent-tool.mjs +57 -10
  254. package/src/standalone/daemon.mjs +113 -15
  255. package/src/standalone/hook-bus/config.mjs +1 -0
  256. package/src/standalone/plugin-admin.mjs +17 -0
  257. package/src/standalone/plugin-admin.test.mjs +7 -0
  258. package/src/standalone/provider-admin.mjs +1 -0
  259. package/src/standalone/session-protocol.mjs +5 -0
  260. package/src/standalone/session-runtime-agent-control-client.mjs +133 -0
  261. package/src/standalone/session-runtime-agent-control-client.test.mjs +71 -0
  262. package/src/standalone/session-runtime-dispatch-cancel.test.mjs +139 -0
  263. package/src/standalone/session-runtime-host-factory.mjs +17 -0
  264. package/src/standalone/session-runtime-host-factory.test.mjs +31 -0
  265. package/src/standalone/session-runtime-host-health.test.mjs +5 -0
  266. package/src/standalone/session-runtime-host.mjs +709 -58
  267. package/src/standalone/session-runtime-inline-host.mjs +258 -0
  268. package/src/standalone/session-runtime-inline-host.test.mjs +385 -0
  269. package/src/standalone/session-runtime-provider-cooldown.mjs +78 -0
  270. package/src/standalone/session-runtime-provider-cooldown.test.mjs +80 -0
  271. package/src/standalone/session-runtime-shard-host.test.mjs +642 -0
  272. package/src/standalone/session-runtime-shard-router.mjs +149 -0
  273. package/src/standalone/session-runtime-shard-router.test.mjs +101 -0
  274. package/src/standalone/session-runtime-worker.mjs +302 -26
  275. package/src/standalone/session-service-agent-tree.test.mjs +984 -0
  276. package/src/standalone/session-service-goal-recovery.test.mjs +177 -0
  277. package/src/standalone/session-service.mjs +711 -11
  278. package/src/tui/app/app-view.jsx +1 -0
  279. package/src/tui/app/core-memory-picker.mjs +2 -0
  280. package/src/tui/app/extension-pickers.mjs +1 -1
  281. package/src/tui/app/maintenance-pickers.mjs +3 -3
  282. package/src/tui/app/model-options.mjs +2 -0
  283. package/src/tui/app/model-picker.mjs +14 -4
  284. package/src/tui/app/panel-handoff.test.mjs +311 -0
  285. package/src/tui/app/project-picker.mjs +1 -0
  286. package/src/tui/app/route-pickers.mjs +21 -6
  287. package/src/tui/app/settings-picker.mjs +26 -3
  288. package/src/tui/app/slash-commands.mjs +1 -0
  289. package/src/tui/app/slash-dispatch.mjs +73 -22
  290. package/src/tui/app/theme-effort-pickers.mjs +7 -2
  291. package/src/tui/components/Picker.jsx +10 -1
  292. package/src/tui/dist/index.mjs +187 -45
  293. package/src/tui/lib/voice-setup.mjs +4 -1
  294. package/src/tui/session/agent-message-source.test.mjs +37 -0
  295. package/src/tui/session/completion-card-restore.test.mjs +50 -0
  296. package/src/tui/session/context-state.mjs +3 -13
  297. package/src/tui/session/goal-continuation.mjs +141 -0
  298. package/src/tui/session/goal-continuation.test.mjs +203 -0
  299. package/src/tui/session/queue-helpers.mjs +5 -2
  300. package/src/tui/session/session-action-surface.test.mjs +190 -0
  301. package/src/tui/session/session-api-ext.mjs +48 -8
  302. package/src/tui/session/session-api.mjs +73 -1
  303. package/src/tui/session/session-flow.mjs +31 -1
  304. package/src/tui/session/turn.mjs +88 -15
  305. package/src/tui/session-local.mjs +39 -3
  306. package/src/vendor/statusline/src/gateway/route-meta.mjs +31 -0
  307. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +0 -301
@@ -0,0 +1,378 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+ import {
4
+ bindAgentDispatchHostCallbacks,
5
+ buildAgentDispatchAskSessionArgs,
6
+ makeAgentDispatch,
7
+ resolveAgentDispatchLiveCallbacks,
8
+ } from './agent-dispatch.mjs';
9
+
10
+ const preparedSession = Object.freeze({
11
+ id: 'sess_host_adapt',
12
+ agent: 'worker',
13
+ owner: 'agent',
14
+ model: 'test-model',
15
+ provider: 'test',
16
+ });
17
+
18
+ function noopAdmission() {
19
+ return {
20
+ async acquire() {
21
+ return { release: async () => {} };
22
+ },
23
+ async runWithLease(_lease, task) {
24
+ return await task();
25
+ },
26
+ };
27
+ }
28
+
29
+ function lifecycleDispatch({ askSession, brief = true }) {
30
+ const events = [];
31
+ const session = {
32
+ id: 'sess_lifecycle',
33
+ agent: 'worker',
34
+ owner: 'agent',
35
+ model: 'test-model',
36
+ provider: 'test',
37
+ status: 'idle',
38
+ closed: false,
39
+ tools: [],
40
+ messages: [],
41
+ };
42
+ const dispatch = makeAgentDispatch({
43
+ agent: 'worker',
44
+ preset: { provider: 'test', model: 'test-model' },
45
+ config: { presets: [] },
46
+ brief,
47
+ resourceAdmission: noopAdmission(),
48
+ prepareAgentSession: () => ({ session }),
49
+ getSession: () => session,
50
+ updateSessionStatus: async (_id, status) => {
51
+ events.push(['status', status]);
52
+ if (session.closed) return false;
53
+ session.status = status;
54
+ return true;
55
+ },
56
+ closeSession: (_id, reason) => {
57
+ events.push(['close', reason, session.status]);
58
+ session.closed = true;
59
+ session.closeReason = reason;
60
+ return true;
61
+ },
62
+ askSession: async (...args) => {
63
+ events.push(['ask']);
64
+ return await askSession(session, ...args);
65
+ },
66
+ });
67
+ return { dispatch, events, session };
68
+ }
69
+
70
+ test('dispatch live callbacks stay suppressed without a live-text request', () => {
71
+ const onToolCall = () => {};
72
+ const onToolResult = () => {};
73
+ const onStageChange = () => {};
74
+ const onSessionStart = () => {};
75
+ const onAssistantMessageCommitted = () => {};
76
+ const onReasoningDelta = () => {};
77
+ const resolved = resolveAgentDispatchLiveCallbacks({
78
+ onToolCall,
79
+ onToolResult,
80
+ onStageChange,
81
+ onSessionStart,
82
+ onAssistantMessageCommitted,
83
+ onReasoningDelta,
84
+ }, {});
85
+ assert.equal(resolved.liveProjection, false);
86
+ assert.equal(resolved.callbacks.onToolCall, onToolCall);
87
+ assert.equal(resolved.callbacks.onStageChange, onStageChange);
88
+ assert.equal(resolved.callbacks.onSessionStart, onSessionStart);
89
+ assert.equal(resolved.callbacks.onAssistantMessageCommitted, onAssistantMessageCommitted);
90
+ assert.equal(resolved.callbacks.onReasoningDelta, onReasoningDelta);
91
+ assert.equal(resolved.callbacks.onTextDelta, undefined);
92
+ });
93
+
94
+ test('per-call live-text callbacks win and request liveProjection', () => {
95
+ const factoryDelta = () => {};
96
+ const callDelta = () => {};
97
+ const callReset = async () => true;
98
+ const resolved = resolveAgentDispatchLiveCallbacks({
99
+ onTextDelta: factoryDelta,
100
+ onToolCall: () => {},
101
+ }, {
102
+ onTextDelta: callDelta,
103
+ onTextReset: callReset,
104
+ });
105
+ assert.equal(resolved.liveProjection, true);
106
+ assert.equal(resolved.callbacks.onTextDelta, callDelta);
107
+ assert.equal(resolved.callbacks.onTextReset, callReset);
108
+ });
109
+
110
+ test('explicit liveProjection requests streaming even without text callbacks', () => {
111
+ assert.equal(resolveAgentDispatchLiveCallbacks({ liveProjection: true }, {}).liveProjection, true);
112
+ assert.equal(resolveAgentDispatchLiveCallbacks({}, { liveProjection: true }).liveProjection, true);
113
+ assert.equal(resolveAgentDispatchLiveCallbacks({
114
+ onAssistantText: () => {},
115
+ }, {}).liveProjection, true);
116
+ });
117
+
118
+ test('askSession args wrap host callbacks with the prepared session', async () => {
119
+ const events = [];
120
+ const compactEvents = [];
121
+ const host = {
122
+ onToolCall(session, iteration, calls) { events.push(['tool', session, iteration, calls]); },
123
+ onTextDelta(session, chunk) { events.push(['delta', session, chunk]); },
124
+ onTextReset(session, detail) { events.push(['reset', session, detail]); return detail?.chars === 3; },
125
+ onSessionStart(session) { events.push(['start', session]); },
126
+ onStageChange(session, stage, detail) { events.push(['stage', session, stage, detail]); },
127
+ onAssistantText(session, text) { events.push(['asst', session, text]); },
128
+ onAssistantMessageCommitted(session) { events.push(['commit', session]); },
129
+ onToolResult(session, message) { events.push(['result', session, message]); },
130
+ onReasoningDelta(session, chunk) { events.push(['reason', session, chunk]); },
131
+ };
132
+ const { onToolCall: positional, askOpts } = buildAgentDispatchAskSessionArgs({
133
+ interactiveSessionSurface: true,
134
+ liveProjection: false,
135
+ onToolCall: host.onToolCall,
136
+ }, {
137
+ onTextDelta: host.onTextDelta,
138
+ onTextReset: host.onTextReset,
139
+ onSessionStart: host.onSessionStart,
140
+ onStageChange: host.onStageChange,
141
+ onAssistantText: host.onAssistantText,
142
+ onAssistantMessageCommitted: host.onAssistantMessageCommitted,
143
+ onToolResult: host.onToolResult,
144
+ onReasoningDelta: host.onReasoningDelta,
145
+ interactiveSessionSurface: true,
146
+ }, {
147
+ onCompactEvent: (event) => compactEvents.push(event),
148
+ interactiveSessionSurface: true,
149
+ }, preparedSession);
150
+
151
+ assert.equal(askOpts.liveProjection, true);
152
+ assert.notEqual(askOpts.onTextDelta, host.onTextDelta);
153
+ assert.equal(Object.hasOwn(askOpts, 'interactiveSessionSurface'), false);
154
+ assert.equal(Object.hasOwn(askOpts, 'onToolCall'), false);
155
+
156
+ askOpts.onSessionStart({ sessionId: 'ignored' });
157
+ askOpts.onStageChange('streaming', { message: 'hi' });
158
+ askOpts.onReasoningDelta('think');
159
+ askOpts.onTextDelta('hello');
160
+ assert.equal(await askOpts.onTextReset({ chars: 3 }), true);
161
+ assert.equal(await askOpts.onTextReset({ chars: 99 }), false);
162
+ askOpts.onAssistantText('hello');
163
+ askOpts.onAssistantMessageCommitted({ role: 'assistant', content: 'hello' });
164
+ positional(1, [{ id: 'c1', name: 'read' }]);
165
+ askOpts.onToolResult({ role: 'tool', toolCallId: 'c1' });
166
+ askOpts.onCompactEvent({ status: 'ok' });
167
+
168
+ assert.deepEqual(events, [
169
+ ['start', preparedSession],
170
+ ['stage', preparedSession, 'streaming', { message: 'hi' }],
171
+ ['reason', preparedSession, 'think'],
172
+ ['delta', preparedSession, 'hello'],
173
+ ['reset', preparedSession, { chars: 3 }],
174
+ ['reset', preparedSession, { chars: 99 }],
175
+ ['asst', preparedSession, 'hello'],
176
+ ['commit', preparedSession],
177
+ ['tool', preparedSession, 1, [{ id: 'c1', name: 'read' }]],
178
+ ['result', preparedSession, { role: 'tool', toolCallId: 'c1' }],
179
+ ]);
180
+ assert.equal(compactEvents.length, 1);
181
+ });
182
+
183
+ test('silent dispatch askOpts keep liveProjection false', () => {
184
+ const { onToolCall, askOpts } = buildAgentDispatchAskSessionArgs({}, { prompt: 'hello' }, {}, preparedSession);
185
+ assert.equal(onToolCall, null);
186
+ assert.equal(askOpts.liveProjection, false);
187
+ assert.equal(askOpts.onTextDelta, undefined);
188
+ assert.equal(askOpts.onSessionStart, undefined);
189
+ assert.equal(Object.hasOwn(askOpts, 'interactiveSessionSurface'), false);
190
+ });
191
+
192
+ test('text-reset binding preserves the exact host acknowledgement', async () => {
193
+ const boundTrue = bindAgentDispatchHostCallbacks({
194
+ onTextReset: () => true,
195
+ }, preparedSession);
196
+ const boundFalse = bindAgentDispatchHostCallbacks({
197
+ onTextReset: () => false,
198
+ }, preparedSession);
199
+ const boundTruthy = bindAgentDispatchHostCallbacks({
200
+ onTextReset: () => 'yes',
201
+ }, preparedSession);
202
+ assert.equal(await boundTrue.onTextReset({ chars: 1 }), true);
203
+ assert.equal(await boundFalse.onTextReset({ chars: 1 }), false);
204
+ assert.equal(await boundTruthy.onTextReset({ chars: 1 }), 'yes');
205
+ });
206
+
207
+ test('makeAgentDispatch prepends the prepared session onto host-shaped callbacks', async () => {
208
+ const events = [];
209
+ const session = {
210
+ id: 'sess_make_dispatch_adapt',
211
+ agent: 'worker',
212
+ owner: 'agent',
213
+ model: 'test-model',
214
+ provider: 'test',
215
+ tools: [],
216
+ };
217
+ let capturedAskOpts = null;
218
+ let capturedOnToolCall = null;
219
+ const dispatch = makeAgentDispatch({
220
+ agent: 'worker',
221
+ preset: { provider: 'test', model: 'test-model' },
222
+ config: { presets: [] },
223
+ brief: false,
224
+ resourceAdmission: noopAdmission(),
225
+ prepareAgentSession: () => ({ session }),
226
+ updateSessionStatus: async () => {},
227
+ closeSession: () => {},
228
+ getSession: () => session,
229
+ askSession: async (_id, _prompt, _ctx, onToolCall, _cwd, _prefetch, askOpts) => {
230
+ capturedAskOpts = askOpts;
231
+ capturedOnToolCall = onToolCall;
232
+ askOpts.onSessionStart({ sessionId: session.id, agent: 'worker' });
233
+ askOpts.onStageChange('streaming', { transport: 'test' });
234
+ askOpts.onReasoningDelta('think');
235
+ askOpts.onTextDelta('PARTIAL');
236
+ const acked = await askOpts.onTextReset({ chars: 3 });
237
+ const rejected = await askOpts.onTextReset({ chars: 99 });
238
+ events.push(['ack', acked, rejected]);
239
+ askOpts.onAssistantText('PARTIAL');
240
+ askOpts.onAssistantMessageCommitted({ role: 'assistant', content: 'PARTIAL' });
241
+ await onToolCall(2, [{ id: 'c1', name: 'read' }]);
242
+ askOpts.onToolResult({ role: 'tool', toolCallId: 'c1' });
243
+ return { content: 'DONE' };
244
+ },
245
+ onSessionStart(received) { events.push(['start', received]); },
246
+ onStageChange(received, stage, detail) { events.push(['stage', received, stage, detail]); },
247
+ onReasoningDelta(received, chunk) { events.push(['reason', received, chunk]); },
248
+ onTextDelta(received, chunk) { events.push(['delta', received, chunk]); },
249
+ onTextReset(received, detail) {
250
+ events.push(['reset', received, detail]);
251
+ return detail?.chars === 3;
252
+ },
253
+ onAssistantText(received, text) { events.push(['asst', received, text]); },
254
+ onAssistantMessageCommitted(received) { events.push(['commit', received]); },
255
+ onToolCall(received, iteration, calls) { events.push(['tool', received, iteration, calls]); },
256
+ onToolResult(received, message) { events.push(['result', received, message]); },
257
+ });
258
+
259
+ const text = await dispatch({ prompt: 'Reply exactly DONE' });
260
+ assert.equal(text, 'DONE');
261
+ assert.equal(capturedAskOpts.liveProjection, true);
262
+ assert.equal(Object.hasOwn(session, 'interactiveSessionSurface'), false);
263
+ assert.equal(Object.hasOwn(session, 'liveProjection'), false);
264
+ assert.notEqual(capturedAskOpts.onTextDelta, dispatch);
265
+ assert.equal(typeof capturedOnToolCall, 'function');
266
+ assert.deepEqual(events, [
267
+ ['start', session],
268
+ ['stage', session, 'streaming', { transport: 'test' }],
269
+ ['reason', session, 'think'],
270
+ ['delta', session, 'PARTIAL'],
271
+ ['reset', session, { chars: 3 }],
272
+ ['reset', session, { chars: 99 }],
273
+ ['ack', true, false],
274
+ ['asst', session, 'PARTIAL'],
275
+ ['commit', session],
276
+ ['tool', session, 2, [{ id: 'c1', name: 'read' }]],
277
+ ['result', session, { role: 'tool', toolCallId: 'c1' }],
278
+ ]);
279
+ });
280
+
281
+ test('makeAgentDispatch silent path does not request live projection', async () => {
282
+ const session = { id: 'sess_silent', agent: 'worker', owner: 'agent', tools: [] };
283
+ let capturedAskOpts = null;
284
+ const dispatch = makeAgentDispatch({
285
+ agent: 'worker',
286
+ preset: { provider: 'test', model: 'test-model' },
287
+ config: { presets: [] },
288
+ brief: false,
289
+ resourceAdmission: noopAdmission(),
290
+ prepareAgentSession: () => ({ session }),
291
+ updateSessionStatus: async () => {},
292
+ closeSession: () => {},
293
+ getSession: () => session,
294
+ askSession: async (_id, _prompt, _ctx, onToolCall, _cwd, _prefetch, askOpts) => {
295
+ capturedAskOpts = askOpts;
296
+ assert.equal(onToolCall, null);
297
+ return { content: 'ok' };
298
+ },
299
+ });
300
+ assert.equal(await dispatch({ prompt: 'hello' }), 'ok');
301
+ assert.equal(capturedAskOpts.liveProjection, false);
302
+ assert.equal(capturedAskOpts.onTextDelta, undefined);
303
+ assert.equal(capturedAskOpts.onSessionStart, undefined);
304
+ assert.equal(Object.hasOwn(session, 'interactiveSessionSurface'), false);
305
+ });
306
+
307
+ test('successful dispatch publishes idle before done tombstoning and preserves brief trimming', async () => {
308
+ const raw = 'S'.repeat((12 * 1024) + 32);
309
+ const { dispatch, events, session } = lifecycleDispatch({
310
+ askSession: async () => ({ content: raw }),
311
+ });
312
+
313
+ const text = await dispatch({ prompt: 'hello' });
314
+
315
+ assert.notEqual(text, raw);
316
+ assert.equal(text.startsWith('S'.repeat(12 * 1024)), true);
317
+ assert.match(text, /\[TRUNCATED — full answer was/);
318
+ assert.deepEqual(events, [
319
+ ['status', 'running'],
320
+ ['ask'],
321
+ ['status', 'idle'],
322
+ ['close', 'ephemeral-done', 'idle'],
323
+ ]);
324
+ assert.equal(session.status, 'idle');
325
+ assert.equal(session.closed, true);
326
+ assert.equal(session.closeReason, 'ephemeral-done');
327
+ });
328
+
329
+ test('partial-salvage dispatch publishes idle before done tombstoning and preserves trimming', async () => {
330
+ const partial = 'P'.repeat((12 * 1024) + 32);
331
+ const { dispatch, events, session } = lifecycleDispatch({
332
+ askSession: async (activeSession) => {
333
+ activeSession.messages.push({ role: 'assistant', content: partial });
334
+ const error = new Error('deadline');
335
+ error.salvagePartial = true;
336
+ throw error;
337
+ },
338
+ });
339
+
340
+ const text = await dispatch({ prompt: 'hello' });
341
+
342
+ assert.notEqual(text, partial);
343
+ assert.equal(text.startsWith('P'.repeat(12 * 1024)), true);
344
+ assert.match(text, /\[TRUNCATED — full answer was/);
345
+ assert.deepEqual(events, [
346
+ ['status', 'running'],
347
+ ['ask'],
348
+ ['status', 'idle'],
349
+ ['close', 'ephemeral-done', 'idle'],
350
+ ]);
351
+ assert.equal(session.status, 'idle');
352
+ assert.equal(session.closed, true);
353
+ assert.equal(session.closeReason, 'ephemeral-done');
354
+ });
355
+
356
+ test('failed dispatch publishes error before error tombstoning and propagates the original error', async () => {
357
+ const failure = new Error('provider failed');
358
+ const { dispatch, events, session } = lifecycleDispatch({
359
+ askSession: async () => {
360
+ throw failure;
361
+ },
362
+ });
363
+
364
+ await assert.rejects(
365
+ dispatch({ prompt: 'hello' }),
366
+ (error) => error === failure,
367
+ );
368
+
369
+ assert.deepEqual(events, [
370
+ ['status', 'running'],
371
+ ['ask'],
372
+ ['status', 'error'],
373
+ ['close', 'ephemeral-error', 'error'],
374
+ ]);
375
+ assert.equal(session.status, 'error');
376
+ assert.equal(session.closed, true);
377
+ assert.equal(session.closeReason, 'ephemeral-error');
378
+ });
@@ -59,6 +59,7 @@ function normalizeAgentCompactionConfig(value = {}) {
59
59
  * @param {number} [opts.maxLoopIterations]
60
60
  * @param {string} [opts.parentSessionId]
61
61
  * @param {string|null} [opts.ownerSessionId] - owning Mixdog MCP instance id for statusline isolation
62
+ * @param {string|null} [opts.visibility] - catalog visibility scope
62
63
  * @param {string|null} [opts.mcpScopeId] - owning session-runtime MCP registry scope
63
64
  * @returns {{ session: object, effectiveCwd: string|null }}
64
65
  */
@@ -77,6 +78,7 @@ export function prepareAgentSession({
77
78
  maxLoopIterations,
78
79
  parentSessionId,
79
80
  ownerSessionId,
81
+ visibility,
80
82
  clientHostPid,
81
83
  agentTag,
82
84
  cacheKeyOverride,
@@ -115,7 +117,9 @@ export function prepareAgentSession({
115
117
  maxLoopIterations: Number.isFinite(effectiveMaxLoopIterations) ? effectiveMaxLoopIterations : undefined,
116
118
  sourceType: sourceType || undefined,
117
119
  sourceName: sourceName || undefined,
120
+ parentSessionId: parentSessionId || null,
118
121
  ownerSessionId: effectiveOwnerSessionId || null,
122
+ visibility: visibility || null,
119
123
  clientHostPid: clientHostPid || null,
120
124
  compaction: compaction || undefined,
121
125
  mcpScopeId: mcpScopeId || null,
@@ -0,0 +1,109 @@
1
+ import { appendAgentTrace } from './agent-trace.mjs';
2
+
3
+ const HASH_CHARS = 16;
4
+
5
+ function cleanText(value, max = 120) {
6
+ if (value == null || value === '') return null;
7
+ return String(value).replace(/[\r\n\t]+/g, ' ').slice(0, max);
8
+ }
9
+
10
+ function cleanHash(value) {
11
+ const text = cleanText(value, 128);
12
+ return text ? text.slice(0, HASH_CHARS) : null;
13
+ }
14
+
15
+ function finiteNumber(value) {
16
+ const number = Number(value);
17
+ return Number.isFinite(number) ? number : null;
18
+ }
19
+
20
+ export function buildCacheBreakPayload(record = {}) {
21
+ const payload = {
22
+ classification: cleanText(record.classification) || 'unknown',
23
+ reason: cleanText(record.reason) || 'unknown',
24
+ source: cleanText(record.source),
25
+ provider: cleanText(record.provider),
26
+ model: cleanText(record.model),
27
+ transport: cleanText(record.transport),
28
+ intentional_transition: cleanText(record.intentionalTransition),
29
+ index: finiteNumber(record.index),
30
+ previous_count: finiteNumber(record.previousCount),
31
+ next_count: finiteNumber(record.nextCount),
32
+ previous_hash: cleanHash(record.previousHash),
33
+ next_hash: cleanHash(record.nextHash),
34
+ previous_request_prefix_hash: cleanHash(record.previousRequestPrefixHash),
35
+ next_request_prefix_hash: cleanHash(record.nextRequestPrefixHash),
36
+ request_prefix_changed: record.requestPrefixChanged === true,
37
+ cached_tokens: finiteNumber(record.cachedTokens),
38
+ prompt_tokens: finiteNumber(record.promptTokens),
39
+ uncached_tokens: finiteNumber(record.uncachedTokens),
40
+ cache_ratio: finiteNumber(record.cacheRatio),
41
+ actual_cache_miss: record.actualCacheMiss === true,
42
+ input_prefix_mismatch_index: finiteNumber(record.inputPrefixMismatchIndex),
43
+ input_prefix_mismatch_previous_count: finiteNumber(record.inputPrefixMismatchPreviousCount),
44
+ input_prefix_mismatch_current_count: finiteNumber(record.inputPrefixMismatchCurrentCount),
45
+ input_prefix_mismatch_expected_type: cleanText(record.inputPrefixMismatchExpectedType),
46
+ input_prefix_mismatch_expected_hash: cleanHash(record.inputPrefixMismatchExpectedHash),
47
+ input_prefix_mismatch_actual_type: cleanText(record.inputPrefixMismatchActualType),
48
+ input_prefix_mismatch_actual_hash: cleanHash(record.inputPrefixMismatchActualHash),
49
+ };
50
+ return Object.fromEntries(Object.entries(payload).filter(([, value]) => value !== null));
51
+ }
52
+
53
+ export function formatCacheBreakLine(record = {}) {
54
+ const payload = buildCacheBreakPayload(record);
55
+ const fields = [
56
+ ['classification', payload.classification],
57
+ ['reason', payload.reason],
58
+ ['source', payload.source],
59
+ ['session', cleanText(record.sessionId, 160)],
60
+ ['provider', payload.provider],
61
+ ['model', payload.model],
62
+ ['iteration', finiteNumber(record.iteration)],
63
+ ['index', payload.index],
64
+ ['previousCount', payload.previous_count],
65
+ ['nextCount', payload.next_count],
66
+ ['previousHash', payload.previous_hash],
67
+ ['nextHash', payload.next_hash],
68
+ ['inputMismatchIndex', payload.input_prefix_mismatch_index],
69
+ ['intent', payload.intentional_transition],
70
+ ['cachedTokens', payload.cached_tokens],
71
+ ['promptTokens', payload.prompt_tokens],
72
+ ].filter(([, value]) => value !== null && value !== undefined);
73
+ return `[cache-break] ${fields.map(([key, value]) => (
74
+ typeof value === 'string' ? `${key}=${JSON.stringify(value)}` : `${key}=${value}`
75
+ )).join(' ')}`;
76
+ }
77
+
78
+ export function traceCacheBreak(record = {}, options = {}) {
79
+ const payload = buildCacheBreakPayload(record);
80
+ const traceFn = options.traceFn === null
81
+ ? null
82
+ : (typeof options.traceFn === 'function' ? options.traceFn : appendAgentTrace);
83
+ if (traceFn) {
84
+ try {
85
+ traceFn({
86
+ sessionId: record.sessionId || null,
87
+ iteration: finiteNumber(record.iteration),
88
+ kind: 'cache_break',
89
+ classification: payload.classification,
90
+ reason: payload.reason,
91
+ source: payload.source || null,
92
+ provider: payload.provider || null,
93
+ model: payload.model || null,
94
+ transport: payload.transport || null,
95
+ payload,
96
+ });
97
+ } catch { /* cache diagnostics never affect provider execution */ }
98
+ }
99
+ const explicitStderr = typeof options.stderrFn === 'function';
100
+ if (options.stderr !== false && (explicitStderr || !process.env.NODE_TEST_CONTEXT)) {
101
+ try {
102
+ const write = explicitStderr
103
+ ? options.stderrFn
104
+ : (line) => process.stderr.write(line);
105
+ write(`${formatCacheBreakLine(record)}\n`);
106
+ } catch { /* cache diagnostics never affect provider execution */ }
107
+ }
108
+ return payload;
109
+ }
@@ -0,0 +1,115 @@
1
+ import assert from 'node:assert/strict';
2
+ import test from 'node:test';
3
+
4
+ import {
5
+ buildCacheBreakPayload,
6
+ traceCacheBreak,
7
+ } from './cache-break-trace.mjs';
8
+ import {
9
+ prepareProviderPrefixGuard,
10
+ ProviderPrefixMutationError,
11
+ } from './session/provider-prefix-guard.mjs';
12
+
13
+ test('cache-break trace emits structured hashes without message content', () => {
14
+ const rows = [];
15
+ const lines = [];
16
+ const payload = traceCacheBreak({
17
+ sessionId: 'sess_test',
18
+ iteration: 2,
19
+ classification: 'unexpected',
20
+ reason: 'message_prefix',
21
+ source: 'browser_snapshot_supersession',
22
+ provider: 'openai-oauth',
23
+ model: 'gpt-test',
24
+ index: 7,
25
+ previousHash: 'a'.repeat(64),
26
+ nextHash: 'b'.repeat(64),
27
+ message: 'must never be logged',
28
+ }, {
29
+ traceFn: (row) => rows.push(row),
30
+ stderrFn: (line) => lines.push(line),
31
+ });
32
+ assert.equal(payload.previous_hash, 'a'.repeat(16));
33
+ assert.equal(payload.next_hash, 'b'.repeat(16));
34
+ assert.equal(rows[0].kind, 'cache_break');
35
+ assert.match(lines[0], /reason="message_prefix"/);
36
+ assert.doesNotMatch(`${JSON.stringify(rows)}${lines.join('')}`, /must never be logged/);
37
+ });
38
+
39
+ test('provider prefix guard explains unexpected rewrites and intentional compaction', () => {
40
+ const events = [];
41
+ const first = prepareProviderPrefixGuard(
42
+ null,
43
+ [{ role: 'user', content: 'one' }],
44
+ { tools: [{ name: 'read' }], nativeTools: [] },
45
+ { provider: 'openai-oauth', model: 'gpt-test' },
46
+ );
47
+ assert.throws(
48
+ () => prepareProviderPrefixGuard(
49
+ first,
50
+ [{ role: 'user', content: 'rewritten' }],
51
+ { tools: [{ name: 'read' }], nativeTools: [] },
52
+ {
53
+ provider: 'openai-oauth',
54
+ model: 'gpt-test',
55
+ mutationSource: 'test_projection',
56
+ onCacheBreak: (event) => events.push(event),
57
+ },
58
+ ),
59
+ (error) => error instanceof ProviderPrefixMutationError
60
+ && error.details.kind === 'message_prefix'
61
+ && error.details.index === 0,
62
+ );
63
+ assert.equal(events[0].classification, 'unexpected');
64
+ assert.equal(events[0].source, 'test_projection');
65
+
66
+ const compactEvents = [];
67
+ assert.doesNotThrow(() => prepareProviderPrefixGuard(
68
+ first,
69
+ [{ role: 'user', content: 'summary' }],
70
+ { tools: [{ name: 'read' }], nativeTools: [] },
71
+ {
72
+ provider: 'openai-oauth',
73
+ model: 'gpt-test',
74
+ cacheBreakIntent: 'automatic_compaction',
75
+ onCacheBreak: (event) => compactEvents.push(event),
76
+ },
77
+ ));
78
+ assert.equal(compactEvents[0].classification, 'intentional');
79
+ assert.equal(compactEvents[0].reason, 'automatic_compaction');
80
+ });
81
+
82
+ test('defer_loading tool changes are excluded from Anthropic cache-key diagnostics', () => {
83
+ const first = prepareProviderPrefixGuard(
84
+ null,
85
+ [{ role: 'user', content: 'one' }],
86
+ { tools: [{ name: 'read' }], nativeTools: [] },
87
+ { provider: 'anthropic-oauth', model: 'claude-test' },
88
+ );
89
+ const events = [];
90
+ const next = prepareProviderPrefixGuard(
91
+ first,
92
+ [{ role: 'user', content: 'one' }],
93
+ {
94
+ tools: [
95
+ { name: 'read' },
96
+ { name: 'mcp__demo__deferred', deferLoading: true },
97
+ ],
98
+ nativeTools: [],
99
+ },
100
+ {
101
+ provider: 'anthropic-oauth',
102
+ model: 'claude-test',
103
+ onCacheBreak: (event) => events.push(event),
104
+ },
105
+ );
106
+ assert.equal(next.requestPrefixHash, first.requestPrefixHash);
107
+ assert.deepEqual(events, []);
108
+
109
+ const payload = buildCacheBreakPayload({
110
+ classification: 'intentional',
111
+ reason: 'tool_schema_changed',
112
+ previousHash: 'c'.repeat(64),
113
+ });
114
+ assert.equal(payload.previous_hash.length, 16);
115
+ });