muonroi-cli 1.7.2 → 1.8.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 (326) hide show
  1. package/dist/packages/agent-harness-core/src/event-filter.js +1 -0
  2. package/dist/packages/agent-harness-core/src/event-redact.js +8 -0
  3. package/dist/packages/agent-harness-core/src/mcp-server.d.ts +16 -0
  4. package/dist/packages/agent-harness-core/src/mcp-server.js +88 -10
  5. package/dist/packages/agent-harness-core/src/protocol.d.ts +15 -0
  6. package/dist/packages/agent-harness-core/src/selector.js +2 -2
  7. package/dist/packages/agent-harness-core/src/transports/sidechannel.js +1 -1
  8. package/dist/packages/agent-harness-opentui/src/agent-mode.js +1 -1
  9. package/dist/packages/agent-harness-opentui/src/install.js +1 -1
  10. package/dist/src/__test-stubs__/vitest-setup.js +4 -0
  11. package/dist/src/agent-harness/__tests__/mock-model.spec.js +54 -0
  12. package/dist/src/agent-harness/mock-model.d.ts +1 -1
  13. package/dist/src/agent-harness/mock-model.js +143 -6
  14. package/dist/src/agent-harness/test-spawn.js +3 -1
  15. package/dist/src/chat/chat-keychain.js +9 -1
  16. package/dist/src/cli/cost-forensics.js +7 -4
  17. package/dist/src/cli/keys.d.ts +2 -2
  18. package/dist/src/cli/keys.js +107 -28
  19. package/dist/src/cli/usage-report.js +2 -2
  20. package/dist/src/council/__tests__/abort-threading.test.d.ts +1 -0
  21. package/dist/src/council/__tests__/abort-threading.test.js +193 -0
  22. package/dist/src/council/__tests__/clarification-prompt.test.js +21 -1
  23. package/dist/src/council/__tests__/council-turn-length-emit.test.d.ts +1 -0
  24. package/dist/src/council/__tests__/council-turn-length-emit.test.js +130 -0
  25. package/dist/src/council/__tests__/debate-round-budget.test.d.ts +1 -0
  26. package/dist/src/council/__tests__/debate-round-budget.test.js +45 -0
  27. package/dist/src/council/__tests__/evaluator-metrics.test.js +3 -0
  28. package/dist/src/council/__tests__/post-debate-recommendation.test.d.ts +1 -0
  29. package/dist/src/council/__tests__/post-debate-recommendation.test.js +58 -0
  30. package/dist/src/council/__tests__/research-tools.test.js +4 -0
  31. package/dist/src/council/__tests__/round-tools.test.js +6 -0
  32. package/dist/src/council/clarifier-question-cap.test.d.ts +12 -0
  33. package/dist/src/council/clarifier-question-cap.test.js +73 -0
  34. package/dist/src/council/clarifier.d.ts +29 -1
  35. package/dist/src/council/clarifier.js +64 -1
  36. package/dist/src/council/debate-planner.d.ts +2 -1
  37. package/dist/src/council/debate-planner.js +5 -3
  38. package/dist/src/council/debate.d.ts +9 -0
  39. package/dist/src/council/debate.js +88 -8
  40. package/dist/src/council/index.d.ts +46 -1
  41. package/dist/src/council/index.js +125 -19
  42. package/dist/src/council/llm.js +41 -13
  43. package/dist/src/council/prompts.js +36 -1
  44. package/dist/src/council/types.d.ts +14 -1
  45. package/dist/src/ee/__tests__/export-transcripts.test.js +1 -1
  46. package/dist/src/ee/bridge.d.ts +3 -1
  47. package/dist/src/ee/bridge.js +9 -3
  48. package/dist/src/ee/bridge.test.js +1 -2
  49. package/dist/src/ee/client.js +15 -2
  50. package/dist/src/ee/recall-ledger.d.ts +20 -0
  51. package/dist/src/ee/recall-ledger.js +40 -1
  52. package/dist/src/ee/search.d.ts +25 -0
  53. package/dist/src/ee/search.js +73 -0
  54. package/dist/src/ee/types.d.ts +15 -1
  55. package/dist/src/ee/who-am-i.d.ts +41 -0
  56. package/dist/src/ee/who-am-i.js +143 -0
  57. package/dist/src/ee/who-am-i.test.d.ts +1 -0
  58. package/dist/src/ee/who-am-i.test.js +101 -0
  59. package/dist/src/flow/compaction/index.d.ts +1 -0
  60. package/dist/src/flow/compaction/index.js +4 -0
  61. package/dist/src/generated/version.d.ts +1 -1
  62. package/dist/src/generated/version.js +1 -1
  63. package/dist/src/headless/council-answers.js +1 -0
  64. package/dist/src/headless/output.d.ts +30 -1
  65. package/dist/src/headless/output.js +180 -0
  66. package/dist/src/headless/output.test.js +61 -1
  67. package/dist/src/hooks/index.js +48 -2
  68. package/dist/src/index.d.ts +2 -0
  69. package/dist/src/index.js +59 -62
  70. package/dist/src/lsp/client-normalize.test.d.ts +12 -0
  71. package/dist/src/lsp/client-normalize.test.js +34 -0
  72. package/dist/src/lsp/client.d.ts +1 -0
  73. package/dist/src/lsp/client.js +10 -2
  74. package/dist/src/lsp/manager.d.ts +1 -1
  75. package/dist/src/lsp/manager.js +13 -5
  76. package/dist/src/lsp/manager.test.js +41 -2
  77. package/dist/src/lsp/runtime.d.ts +1 -1
  78. package/dist/src/lsp/runtime.js +2 -2
  79. package/dist/src/maintain/__tests__/gh-create-pr.test.js +7 -3
  80. package/dist/src/maintain/__tests__/pr-builder.test.js +7 -3
  81. package/dist/src/mcp/__tests__/client-pool.spec.js +3 -3
  82. package/dist/src/mcp/__tests__/ee-tools.test.js +42 -14
  83. package/dist/src/mcp/__tests__/forensics-tools.test.js +3 -3
  84. package/dist/src/mcp/__tests__/lsp-tools.test.js +4 -4
  85. package/dist/src/mcp/auto-setup.js +0 -8
  86. package/dist/src/mcp/client-pool.d.ts +1 -1
  87. package/dist/src/mcp/client-pool.js +25 -9
  88. package/dist/src/mcp/ee-tools.d.ts +10 -0
  89. package/dist/src/mcp/ee-tools.js +34 -0
  90. package/dist/src/mcp/self-verify-jobs.d.ts +1 -0
  91. package/dist/src/mcp/self-verify-jobs.js +1 -3
  92. package/dist/src/mcp/self-verify-runner.js +1 -1
  93. package/dist/src/models/catalog-gemini.test.js +12 -10
  94. package/dist/src/models/catalog.json +106 -38
  95. package/dist/src/orchestrator/__tests__/auto-commit.test.d.ts +1 -0
  96. package/dist/src/orchestrator/__tests__/auto-commit.test.js +142 -0
  97. package/dist/src/orchestrator/__tests__/batch-turn-runner.test.js +3 -0
  98. package/dist/src/orchestrator/__tests__/delegations.test.d.ts +1 -0
  99. package/dist/src/orchestrator/__tests__/delegations.test.js +96 -0
  100. package/dist/src/orchestrator/__tests__/message-processor.test.js +2 -0
  101. package/dist/src/orchestrator/__tests__/prompts.test.d.ts +1 -0
  102. package/dist/src/orchestrator/__tests__/prompts.test.js +88 -0
  103. package/dist/src/orchestrator/__tests__/stream-runner.test.js +29 -0
  104. package/dist/src/orchestrator/__tests__/sub-session-delegation.test.d.ts +1 -0
  105. package/dist/src/orchestrator/__tests__/sub-session-delegation.test.js +507 -0
  106. package/dist/src/orchestrator/agent.test.js +5 -2
  107. package/dist/src/orchestrator/auto-commit.d.ts +110 -0
  108. package/dist/src/orchestrator/auto-commit.js +366 -0
  109. package/dist/src/orchestrator/batch-turn-runner.d.ts +3 -2
  110. package/dist/src/orchestrator/batch-turn-runner.js +2 -1
  111. package/dist/src/orchestrator/compaction-proposer-prompt.d.ts +8 -0
  112. package/dist/src/orchestrator/compaction-proposer-prompt.js +43 -0
  113. package/dist/src/orchestrator/compaction.d.ts +22 -0
  114. package/dist/src/orchestrator/compaction.js +42 -0
  115. package/dist/src/orchestrator/delegations.d.ts +1 -0
  116. package/dist/src/orchestrator/delegations.js +41 -0
  117. package/dist/src/orchestrator/error-utils.js +27 -2
  118. package/dist/src/orchestrator/message-processor.d.ts +23 -5
  119. package/dist/src/orchestrator/message-processor.js +226 -2573
  120. package/dist/src/orchestrator/orchestrator.d.ts +11 -2
  121. package/dist/src/orchestrator/orchestrator.js +530 -43
  122. package/dist/src/orchestrator/pending-calls.js +2 -1
  123. package/dist/src/orchestrator/preprocessor.d.ts +13 -0
  124. package/dist/src/orchestrator/preprocessor.js +146 -0
  125. package/dist/src/orchestrator/prompts.d.ts +16 -0
  126. package/dist/src/orchestrator/prompts.js +130 -67
  127. package/dist/src/orchestrator/safety-askcard.d.ts +66 -0
  128. package/dist/src/orchestrator/safety-askcard.js +74 -0
  129. package/dist/src/orchestrator/sandbox.test.js +8 -5
  130. package/dist/src/orchestrator/scope-ceiling.js +1 -1
  131. package/dist/src/orchestrator/stall-watchdog.d.ts +47 -0
  132. package/dist/src/orchestrator/stall-watchdog.js +23 -0
  133. package/dist/src/orchestrator/stall-watchdog.test.js +36 -1
  134. package/dist/src/orchestrator/stream-runner.js +52 -30
  135. package/dist/src/orchestrator/sub-agent-cap.d.ts +2 -0
  136. package/dist/src/orchestrator/sub-agent-cap.js +12 -4
  137. package/dist/src/orchestrator/sub-agent-model-tier.js +1 -1
  138. package/dist/src/orchestrator/subagent-compactor.d.ts +21 -0
  139. package/dist/src/orchestrator/subagent-compactor.js +148 -24
  140. package/dist/src/orchestrator/subagent-compactor.spec.js +249 -1
  141. package/dist/src/orchestrator/text-tool-call-detector.d.ts +9 -5
  142. package/dist/src/orchestrator/text-tool-call-detector.js +16 -12
  143. package/dist/src/orchestrator/text-tool-call-detector.test.js +66 -8
  144. package/dist/src/orchestrator/tool-args-repair.js +1 -1
  145. package/dist/src/orchestrator/tool-engine.d.ts +153 -0
  146. package/dist/src/orchestrator/tool-engine.js +3138 -0
  147. package/dist/src/orchestrator/turn-runner-deps.d.ts +3 -0
  148. package/dist/src/pil/__tests__/discovery-types.test.js +15 -4
  149. package/dist/src/pil/__tests__/discovery.test.js +89 -145
  150. package/dist/src/pil/__tests__/dual-run.test.js +2 -0
  151. package/dist/src/pil/__tests__/layer1-intent-trace.test.js +3 -0
  152. package/dist/src/pil/__tests__/layer1-intent.test.js +3 -0
  153. package/dist/src/pil/__tests__/layer16-clarity.test.js +35 -32
  154. package/dist/src/pil/__tests__/layer18-acceptance.test.js +1 -78
  155. package/dist/src/pil/__tests__/layer2_5-ponytail.test.d.ts +1 -0
  156. package/dist/src/pil/__tests__/layer2_5-ponytail.test.js +53 -0
  157. package/dist/src/pil/__tests__/layer3-ee-injection.test.js +67 -4
  158. package/dist/src/pil/__tests__/layer3-injected-chunk.test.js +14 -2
  159. package/dist/src/pil/__tests__/layer4-gsd.test.js +30 -0
  160. package/dist/src/pil/__tests__/layer5-context.test.js +39 -1
  161. package/dist/src/pil/__tests__/layer6-output.test.js +30 -2
  162. package/dist/src/pil/__tests__/llm-classify.test.js +79 -1
  163. package/dist/src/pil/__tests__/orchestrator-integration.test.js +3 -1
  164. package/dist/src/pil/__tests__/pipeline.test.js +11 -8
  165. package/dist/src/pil/__tests__/response-tools.test.js +52 -0
  166. package/dist/src/pil/agent-operating-contract.d.ts +1 -1
  167. package/dist/src/pil/agent-operating-contract.js +5 -3
  168. package/dist/src/pil/agent-operating-contract.test.js +2 -2
  169. package/dist/src/pil/cheap-model-playbook.d.ts +1 -1
  170. package/dist/src/pil/cheap-model-playbook.js +1 -1
  171. package/dist/src/pil/cheap-model-workbooks.d.ts +1 -1
  172. package/dist/src/pil/cheap-model-workbooks.js +1 -1
  173. package/dist/src/pil/config.d.ts +20 -0
  174. package/dist/src/pil/config.js +35 -0
  175. package/dist/src/pil/discovery-types.d.ts +32 -9
  176. package/dist/src/pil/discovery.d.ts +3 -3
  177. package/dist/src/pil/discovery.js +138 -244
  178. package/dist/src/pil/index.d.ts +1 -1
  179. package/dist/src/pil/index.js +1 -1
  180. package/dist/src/pil/layer1-intent.d.ts +9 -1
  181. package/dist/src/pil/layer1-intent.js +62 -7
  182. package/dist/src/pil/layer1-intent.test.js +112 -2
  183. package/dist/src/pil/layer15-context-scan.js +1 -1
  184. package/dist/src/pil/layer16-clarity.d.ts +21 -25
  185. package/dist/src/pil/layer16-clarity.js +52 -88
  186. package/dist/src/pil/layer18-acceptance.d.ts +13 -3
  187. package/dist/src/pil/layer18-acceptance.js +1 -75
  188. package/dist/src/pil/layer2-personality.js +7 -8
  189. package/dist/src/pil/layer2_5-ponytail.d.ts +2 -0
  190. package/dist/src/pil/layer2_5-ponytail.js +41 -0
  191. package/dist/src/pil/layer3-ee-injection.js +180 -14
  192. package/dist/src/pil/layer4-gsd.js +22 -8
  193. package/dist/src/pil/layer5-context.d.ts +1 -0
  194. package/dist/src/pil/layer5-context.js +26 -11
  195. package/dist/src/pil/layer6-output.js +23 -5
  196. package/dist/src/pil/llm-classify.d.ts +11 -0
  197. package/dist/src/pil/llm-classify.js +196 -3
  198. package/dist/src/pil/native-capabilities-workbook.d.ts +1 -1
  199. package/dist/src/pil/native-capabilities-workbook.js +4 -3
  200. package/dist/src/pil/pipeline.js +15 -1
  201. package/dist/src/pil/response-tools.d.ts +16 -0
  202. package/dist/src/pil/response-tools.js +41 -0
  203. package/dist/src/pil/schema.d.ts +8 -0
  204. package/dist/src/pil/schema.js +24 -3
  205. package/dist/src/pil/task-tier-map.js +7 -7
  206. package/dist/src/pil/types.d.ts +9 -3
  207. package/dist/src/product-loop/__tests__/discovery-interview.test.js +103 -1
  208. package/dist/src/product-loop/__tests__/gather-selectable-alts.test.js +34 -0
  209. package/dist/src/product-loop/__tests__/sprint-self-verify.test.js +12 -12
  210. package/dist/src/product-loop/discovery-interview.d.ts +20 -0
  211. package/dist/src/product-loop/discovery-interview.js +121 -7
  212. package/dist/src/product-loop/gather.js +19 -5
  213. package/dist/src/product-loop/loop-driver.js +2 -2
  214. package/dist/src/product-loop/sprint-self-verify.js +2 -2
  215. package/dist/src/providers/__tests__/reasoning-roundtrip.test.js +12 -0
  216. package/dist/src/providers/auth/__tests__/gemini-oauth.test.d.ts +1 -5
  217. package/dist/src/providers/auth/__tests__/gemini-oauth.test.js +1 -5
  218. package/dist/src/providers/auth/gcloud.d.ts +28 -0
  219. package/dist/src/providers/auth/gcloud.js +102 -0
  220. package/dist/src/providers/auth/gemini-oauth.d.ts +46 -19
  221. package/dist/src/providers/auth/gemini-oauth.js +292 -105
  222. package/dist/src/providers/auth/grok-oauth.d.ts +1 -1
  223. package/dist/src/providers/auth/grok-oauth.js +1 -1
  224. package/dist/src/providers/auth/registry.js +28 -6
  225. package/dist/src/providers/gemini.d.ts +1 -1
  226. package/dist/src/providers/gemini.js +10 -3
  227. package/dist/src/providers/keychain.d.ts +2 -2
  228. package/dist/src/providers/keychain.js +20 -2
  229. package/dist/src/providers/openai.js +5 -1
  230. package/dist/src/providers/runtime.d.ts +1 -1
  231. package/dist/src/providers/runtime.js +11 -2
  232. package/dist/src/providers/siliconflow-sse-repair.js +1 -1
  233. package/dist/src/providers/strategies/google.strategy.d.ts +9 -2
  234. package/dist/src/providers/strategies/google.strategy.js +159 -9
  235. package/dist/src/providers/strategies/thinking-mode.js +19 -6
  236. package/dist/src/providers/types.d.ts +1 -1
  237. package/dist/src/router/decide.test.js +40 -8
  238. package/dist/src/router/step-router.d.ts +20 -0
  239. package/dist/src/router/step-router.js +73 -0
  240. package/dist/src/self-qa/agentic-loop.js +30 -21
  241. package/dist/src/self-qa/orchestrator.js +5 -5
  242. package/dist/src/self-qa/scenario-planner.js +1 -1
  243. package/dist/src/self-qa/spec-emitter.js +4 -4
  244. package/dist/src/storage/__tests__/hierarchical-session.test.d.ts +1 -0
  245. package/dist/src/storage/__tests__/hierarchical-session.test.js +161 -0
  246. package/dist/src/storage/__tests__/sweep-stale-pending.test.js +2 -2
  247. package/dist/src/storage/index.d.ts +1 -1
  248. package/dist/src/storage/index.js +1 -1
  249. package/dist/src/storage/migrations.js +13 -1
  250. package/dist/src/storage/transcript-response-entry.test.js +18 -5
  251. package/dist/src/storage/transcript-view.js +7 -1
  252. package/dist/src/storage/transcript.d.ts +3 -0
  253. package/dist/src/storage/transcript.js +254 -73
  254. package/dist/src/storage/ui-interaction-log.d.ts +17 -0
  255. package/dist/src/storage/usage.js +22 -5
  256. package/dist/src/tools/__tests__/native-tools.test.js +6 -0
  257. package/dist/src/tools/bash.d.ts +36 -23
  258. package/dist/src/tools/bash.js +59 -301
  259. package/dist/src/tools/bash.test.js +2 -257
  260. package/dist/src/tools/file-tracker.d.ts +5 -0
  261. package/dist/src/tools/file-tracker.js +14 -0
  262. package/dist/src/tools/file.js +2 -0
  263. package/dist/src/tools/git-safety.d.ts +38 -1
  264. package/dist/src/tools/git-safety.js +94 -4
  265. package/dist/src/tools/git-safety.test.js +45 -1
  266. package/dist/src/tools/native-tools.d.ts +1 -1
  267. package/dist/src/tools/native-tools.js +66 -1
  268. package/dist/src/tools/registry-bash-empty-command.test.js +4 -3
  269. package/dist/src/tools/registry-git-safety.test.js +17 -6
  270. package/dist/src/tools/registry.d.ts +6 -0
  271. package/dist/src/tools/registry.js +215 -13
  272. package/dist/src/types/index.d.ts +8 -2
  273. package/dist/src/ui/app.d.ts +1 -0
  274. package/dist/src/ui/app.js +0 -0
  275. package/dist/src/ui/components/council-question-card.js +1 -0
  276. package/dist/src/ui/components/message-view.js +37 -3
  277. package/dist/src/ui/components/structured-response-view.js +7 -0
  278. package/dist/src/ui/components/task-list-panel.js +3 -12
  279. package/dist/src/ui/containers/chat-feed.d.ts +40 -0
  280. package/dist/src/ui/containers/chat-feed.js +66 -0
  281. package/dist/src/ui/containers/modals-layer.d.ts +87 -0
  282. package/dist/src/ui/containers/modals-layer.js +18 -0
  283. package/dist/src/ui/slash/__tests__/menu-parity.test.d.ts +1 -0
  284. package/dist/src/ui/slash/__tests__/menu-parity.test.js +1 -0
  285. package/dist/src/ui/slash/__tests__/ponytail.test.d.ts +1 -0
  286. package/dist/src/ui/slash/__tests__/ponytail.test.js +37 -0
  287. package/dist/src/ui/slash/expand.js +14 -1
  288. package/dist/src/ui/slash/export.js +16 -1
  289. package/dist/src/ui/slash/menu-items.js +1 -0
  290. package/dist/src/ui/slash/ponytail.d.ts +2 -0
  291. package/dist/src/ui/slash/ponytail.js +19 -0
  292. package/dist/src/ui/status-bar/store.js +0 -1
  293. package/dist/src/ui/status-bar/store.test.js +2 -2
  294. package/dist/src/ui/use-app-logic.d.ts +26 -0
  295. package/dist/src/ui/use-app-logic.js +0 -0
  296. package/dist/src/ui/utils/relaunch.js +1 -1
  297. package/dist/src/ui/utils/text.d.ts +11 -0
  298. package/dist/src/ui/utils/text.js +21 -1
  299. package/dist/src/ui/utils/text.test.js +25 -1
  300. package/dist/src/ui/utils/tools.js +3 -1
  301. package/dist/src/usage/cost-log.js +2 -2
  302. package/dist/src/usage/estimator.d.ts +14 -0
  303. package/dist/src/usage/estimator.js +22 -0
  304. package/dist/src/utils/__tests__/compaction-caps.test.d.ts +21 -0
  305. package/dist/src/utils/__tests__/compaction-caps.test.js +187 -0
  306. package/dist/src/utils/__tests__/llm-deadline-abort.test.d.ts +1 -0
  307. package/dist/src/utils/__tests__/llm-deadline-abort.test.js +33 -0
  308. package/dist/src/utils/__tests__/llm-deadline.test.js +16 -4
  309. package/dist/src/utils/__tests__/logger.test.d.ts +1 -0
  310. package/dist/src/utils/__tests__/logger.test.js +115 -0
  311. package/dist/src/utils/__tests__/settings-agent-first.test.d.ts +1 -0
  312. package/dist/src/utils/__tests__/settings-agent-first.test.js +48 -0
  313. package/dist/src/utils/install-manager.d.ts +1 -0
  314. package/dist/src/utils/install-manager.js +76 -7
  315. package/dist/src/utils/install-manager.test.js +43 -1
  316. package/dist/src/utils/llm-deadline.d.ts +9 -1
  317. package/dist/src/utils/llm-deadline.js +31 -2
  318. package/dist/src/utils/logger.d.ts +28 -0
  319. package/dist/src/utils/logger.js +171 -0
  320. package/dist/src/utils/permission-mode.d.ts +33 -0
  321. package/dist/src/utils/permission-mode.js +164 -3
  322. package/dist/src/utils/permission-mode.test.js +92 -1
  323. package/dist/src/utils/settings.d.ts +36 -32
  324. package/dist/src/utils/settings.js +151 -123
  325. package/dist/src/utils/update-checker.test.js +5 -1
  326. package/package.json +1 -1
@@ -104,11 +104,12 @@ export function isHighValueToolResult(toolName, preview, explicitKeepIds, toolCa
104
104
  const p = preview.toLowerCase();
105
105
  if (/error|fail|todo|plan|done|✔|blocked|critical/.test(p))
106
106
  return true;
107
- if (/\.(ts|tsx|js|md|json|test|spec)\b/.test(p) || p.includes("src/") || p.includes("PLAN"))
108
- return true;
109
- if (preview.length > 1500)
110
- return true;
111
- return true; // read_file/grep etc on source are presumptively high-value
107
+ if (/\.(ts|tsx|js|md|json|test|spec)\b/.test(p) || p.includes("src/") || p.includes("PLAN")) {
108
+ // For source code/plans, only keep verbatim if it is relatively small (under 12000 chars)
109
+ // to prevent large file reads from leaking memory/tokens indefinitely in long sessions.
110
+ return preview.length <= 12000;
111
+ }
112
+ return false;
112
113
  }
113
114
  if (toolCallId && explicitKeepIds?.has(toolCallId))
114
115
  return true;
@@ -126,6 +127,7 @@ function resolveOpts(o) {
126
127
  contextFillRatio: Math.min(0.95, Math.max(0.1, o?.contextFillRatio ?? 0.5)),
127
128
  keepToolIds: keepIds,
128
129
  persistArtifact: o?.persistArtifact,
130
+ stripOldReasoning: o?.stripOldReasoning ?? false,
129
131
  };
130
132
  }
131
133
  /**
@@ -324,22 +326,25 @@ function rewriteOlderToolMessage(msg, previewChars, label, keepToolIds, persistA
324
326
  export function compactSubAgentMessages(messages, opts = {}) {
325
327
  const resolved = resolveOpts(opts);
326
328
  const { outputPreviewChars, label, envelopeChars } = resolved;
327
- // F2 threshold check uses TRUE prompt size (messages + system + tools).
328
- // The envelope (system prompt + JSON-schema for every tool) is re-sent on
329
- // every step and was previously invisible to the compactor, so a session
330
- // with 20-50K of fixed overhead would never trip the messages-only check.
329
+ // Step 4: Hard-limit message history sent to the model to prevent token bloating
330
+ // When input (messages + envelope) exceeds 50K characters and messages array is > 30,
331
+ // we slice the history to keep at most 30 messages (preserving system and user start).
332
+ let processedMessages = messages;
331
333
  const messagesTotal = cumulativeMessageChars(messages);
332
334
  const total = messagesTotal + envelopeChars;
333
- // G1 + G2 derive effective threshold and keepLastTurns from context
334
- // window utilization. Falls back to static char threshold + keepLast
335
- // when no contextWindowTokens supplied (preserves old behaviour).
336
- const { effectiveThresholdChars, effectiveKeepLastTurns } = computeDynamicParams(total, resolved);
335
+ if (total > 50_000 && messages.length > 30) {
336
+ processedMessages = sliceMessageHistory(messages, 30);
337
+ }
338
+ // Calculate effective thresholds and keep last turns using the processed messages
339
+ const processedMessagesTotal = cumulativeMessageChars(processedMessages);
340
+ const processedTotal = processedMessagesTotal + envelopeChars;
341
+ const { effectiveThresholdChars, effectiveKeepLastTurns } = computeDynamicParams(processedTotal, resolved);
337
342
  // No-op: return the input BY REFERENCE (contract above) so `compacted === input`.
338
- if (total < effectiveThresholdChars)
339
- return messages;
340
- const keepFrom = findKeepFromIndex(messages, effectiveKeepLastTurns);
343
+ if (processedTotal < effectiveThresholdChars)
344
+ return processedMessages;
345
+ const keepFrom = findKeepFromIndex(processedMessages, effectiveKeepLastTurns);
341
346
  if (keepFrom <= 0)
342
- return messages;
347
+ return processedMessages;
343
348
  // Walk older messages; rewrite fresh tool results into stubs, super-shrink
344
349
  // already-stubbed results (F1), and strip args off older assistant
345
350
  // tool-call shells (F1). The 1:1 assistant↔tool pairing required by the AI
@@ -347,8 +352,8 @@ export function compactSubAgentMessages(messages, opts = {}) {
347
352
  // structure or count.
348
353
  let firstUserSeen = false;
349
354
  const out = [];
350
- for (let i = 0; i < messages.length; i++) {
351
- const msg = messages[i];
355
+ for (let i = 0; i < processedMessages.length; i++) {
356
+ const msg = processedMessages[i];
352
357
  if (i >= keepFrom) {
353
358
  out.push(msg);
354
359
  continue;
@@ -378,17 +383,43 @@ export function compactSubAgentMessages(messages, opts = {}) {
378
383
  continue;
379
384
  }
380
385
  if (msg.role === "assistant" && Array.isArray(msg.content)) {
381
- // F1 — strip args off older assistant tool-call shells. 50+ of these
382
- // accumulate ~10-30K chars of args (file paths, bash commands) that
383
- // the model does not need once the matching result has been elided.
384
- // We keep toolCallId + toolName so pairing with tool-result is intact.
385
- out.push(stripAssistantToolCallArgs(msg));
386
+ // T1.1 — strip reasoning parts from older assistant turns. DeepSeek V4
387
+ // Flash / R1 emit 2K-5K reasoning tokens per turn that accumulate
388
+ // across the multi-step loop. These are never re-read by the model
389
+ // but cost full input billing on every subsequent round. Strip them
390
+ // from turns older than keepLastTurns to cut ~30-50% of input tokens.
391
+ // Then F1 — strip args off older assistant tool-call shells.
392
+ let processed = resolved.stripOldReasoning ? stripAssistantReasoning(msg) : msg;
393
+ processed = stripAssistantToolCallArgs(processed);
394
+ out.push(processed);
386
395
  continue;
387
396
  }
388
397
  out.push(msg);
389
398
  }
390
399
  return out;
391
400
  }
401
+ /**
402
+ * T1.1 — strip reasoning parts from an assistant message. Reasoning tokens
403
+ * (CoT / `<think>`) from older turns have zero re-read value for the model
404
+ * on subsequent steps but they accumulate O(N) chars per turn and are billed
405
+ * as full input tokens. Removing them from turns older than keepLastTurns
406
+ * cuts ~30-50% of cumulative input in multi-step loops with reasoning models
407
+ * (DeepSeek V4 Flash, R1, etc.).
408
+ *
409
+ * Preserves text + tool-call parts (the structural skeleton the model needs
410
+ * to maintain coherent tool-call↔tool-result pairing).
411
+ */
412
+ function stripAssistantReasoning(msg) {
413
+ if (!Array.isArray(msg.content))
414
+ return msg;
415
+ const parts = msg.content;
416
+ const filtered = parts.filter((part) => part.type !== "reasoning");
417
+ if (filtered.length === parts.length)
418
+ return msg; // nothing stripped
419
+ // Edge case: if ALL parts were reasoning (no text/tool-call), keep the
420
+ // message with an empty content array to preserve message-count pairing.
421
+ return { ...msg, content: filtered };
422
+ }
392
423
  function stripAssistantToolCallArgs(msg) {
393
424
  if (!Array.isArray(msg.content))
394
425
  return msg;
@@ -424,4 +455,97 @@ function stripAssistantToolCallArgs(msg) {
424
455
  return msg;
425
456
  return { ...msg, content: next };
426
457
  }
458
+ /**
459
+ * Injects Anthropic prompt caching (cacheControl) into the last message's content
460
+ * block(s) if the model is Claude (starts with 'claude').
461
+ * Creates a copy of the messages array and the last message to avoid mutating in-place.
462
+ */
463
+ export function applyAnthropicPromptCaching(messages, modelId) {
464
+ if (!modelId.startsWith("claude")) {
465
+ return messages;
466
+ }
467
+ if (messages.length === 0) {
468
+ return messages;
469
+ }
470
+ const newMessages = [...messages];
471
+ const lastIndex = newMessages.length - 1;
472
+ const originalLastMsg = newMessages[lastIndex];
473
+ if (!originalLastMsg)
474
+ return messages;
475
+ const lastMsg = { ...originalLastMsg };
476
+ if (typeof lastMsg.content === "string") {
477
+ lastMsg.content = [
478
+ {
479
+ type: "text",
480
+ text: lastMsg.content,
481
+ providerOptions: {
482
+ anthropic: { cacheControl: { type: "ephemeral" } },
483
+ },
484
+ },
485
+ ];
486
+ }
487
+ else if (Array.isArray(lastMsg.content)) {
488
+ const content = [...lastMsg.content];
489
+ const lastBlockIndex = content.length - 1;
490
+ if (lastBlockIndex >= 0) {
491
+ const lastBlock = { ...content[lastBlockIndex] };
492
+ lastBlock.providerOptions = {
493
+ ...lastBlock.providerOptions,
494
+ anthropic: { cacheControl: { type: "ephemeral" } },
495
+ };
496
+ content[lastBlockIndex] = lastBlock;
497
+ lastMsg.content = content;
498
+ }
499
+ }
500
+ newMessages[lastIndex] = lastMsg;
501
+ return newMessages;
502
+ }
503
+ /**
504
+ * Safely slice message history to keep at most `maxMessages` messages.
505
+ * Preserves the system message(s) at the front and ensures that the slice
506
+ * starts with a "user" message and does not split assistant tool calls and
507
+ * corresponding tool results.
508
+ */
509
+ export function sliceMessageHistory(messages, maxMessages = 30) {
510
+ if (messages.length <= maxMessages)
511
+ return messages;
512
+ // Find all user message indices (excluding system messages)
513
+ const userIndices = [];
514
+ for (let idx = 0; idx < messages.length; idx++) {
515
+ if (messages[idx]?.role === "user") {
516
+ userIndices.push(idx);
517
+ }
518
+ }
519
+ if (userIndices.length === 0) {
520
+ return messages;
521
+ }
522
+ // Group messages into turns.
523
+ // Each turn is a range [start, end] inclusive.
524
+ const turns = [];
525
+ for (let idx = 0; idx < userIndices.length; idx++) {
526
+ const start = userIndices[idx];
527
+ const end = idx + 1 < userIndices.length ? userIndices[idx + 1] - 1 : messages.length - 1;
528
+ turns.push({ start, end });
529
+ }
530
+ // Accumulate turns from the end, up to maxMessages
531
+ let keptMessagesCount = 0;
532
+ let keepFromIndex = -1;
533
+ for (let idx = turns.length - 1; idx >= 0; idx--) {
534
+ const turn = turns[idx];
535
+ const turnLength = turn.end - turn.start + 1;
536
+ if (keptMessagesCount === 0 || keptMessagesCount + turnLength <= maxMessages) {
537
+ keptMessagesCount += turnLength;
538
+ keepFromIndex = turn.start;
539
+ }
540
+ else {
541
+ break;
542
+ }
543
+ }
544
+ if (keepFromIndex === -1) {
545
+ return messages;
546
+ }
547
+ const kept = messages.slice(keepFromIndex);
548
+ const systemMessages = messages.filter((m) => m.role === "system");
549
+ return [...systemMessages, ...kept];
550
+ }
427
551
  //# sourceMappingURL=subagent-compactor.js.map
@@ -1,5 +1,5 @@
1
1
  import { afterEach, describe, expect, it } from "vitest";
2
- import { compactSubAgentMessages, cumulativeMessageChars } from "./subagent-compactor.js";
2
+ import { compactSubAgentMessages, cumulativeMessageChars, sliceMessageHistory } from "./subagent-compactor.js";
3
3
  function bigText(label, kb) {
4
4
  const block = `${label}:${"x".repeat(kb * 1000)}`;
5
5
  return block;
@@ -384,5 +384,253 @@ describe("subagent-compactor: compactSubAgentMessages", () => {
384
384
  }
385
385
  expect(keptExplicit).toBe(true);
386
386
  });
387
+ it("elides low-value large read_file results while keeping high-value ones", () => {
388
+ // Turn 1: large low-value read_file output (15000 chars of 'x', no ts/src/PLAN/error keywords)
389
+ const lowValueOutput = "x".repeat(15000);
390
+ // Turn 2: large high-value read_file output containing 'error'
391
+ const highValueOutputWithError = "error: failed to load module\n" + "x".repeat(15000);
392
+ // Turn 3: small source file read (5000 chars of 'x' containing 'src/index.ts')
393
+ const highValueSourceOutput = "src/index.ts\n" + "x".repeat(5000);
394
+ const msgs = [
395
+ { role: "system", content: "You are the agent." },
396
+ { role: "user", content: "perform task" },
397
+ {
398
+ role: "assistant",
399
+ content: [{ type: "tool-call", toolCallId: "call_low", toolName: "read_file", input: {} }],
400
+ },
401
+ {
402
+ role: "tool",
403
+ content: [
404
+ {
405
+ type: "tool-result",
406
+ toolCallId: "call_low",
407
+ toolName: "read_file",
408
+ output: { type: "text", value: lowValueOutput },
409
+ },
410
+ ],
411
+ },
412
+ {
413
+ role: "assistant",
414
+ content: [{ type: "tool-call", toolCallId: "call_err", toolName: "read_file", input: {} }],
415
+ },
416
+ {
417
+ role: "tool",
418
+ content: [
419
+ {
420
+ type: "tool-result",
421
+ toolCallId: "call_err",
422
+ toolName: "read_file",
423
+ output: { type: "text", value: highValueOutputWithError },
424
+ },
425
+ ],
426
+ },
427
+ {
428
+ role: "assistant",
429
+ content: [{ type: "tool-call", toolCallId: "call_src", toolName: "read_file", input: {} }],
430
+ },
431
+ {
432
+ role: "tool",
433
+ content: [
434
+ {
435
+ type: "tool-result",
436
+ toolCallId: "call_src",
437
+ toolName: "read_file",
438
+ output: { type: "text", value: highValueSourceOutput },
439
+ },
440
+ ],
441
+ },
442
+ // Add a couple of low-value turns to push past threshold and keepLast turns (keepLast=1)
443
+ ...toolTurn(9, 10),
444
+ ];
445
+ // Force low value on the final kept turn
446
+ msgs[msgs.length - 1].content[0].toolName = "other_tool";
447
+ const out = compactSubAgentMessages(msgs, { thresholdChars: 10_000, keepLastTurns: 1 });
448
+ // low-value large read_file MUST be elided
449
+ const lowValMsg = out.find((m) => m.role === "tool" && Array.isArray(m.content) && m.content[0]?.toolCallId === "call_low");
450
+ expect((lowValMsg?.content)[0].output.value).toMatch(/elided by sub-agent compactor/);
451
+ // high-value large error read_file MUST NOT be elided
452
+ const errValMsg = out.find((m) => m.role === "tool" && Array.isArray(m.content) && m.content[0]?.toolCallId === "call_err");
453
+ expect((errValMsg?.content)[0].output.value).toBe(highValueOutputWithError);
454
+ // high-value small source read_file MUST NOT be elided
455
+ const srcValMsg = out.find((m) => m.role === "tool" && Array.isArray(m.content) && m.content[0]?.toolCallId === "call_src");
456
+ expect((srcValMsg?.content)[0].output.value).toBe(highValueSourceOutput);
457
+ });
458
+ it("T1.1 — strips reasoning parts from old assistant turns when stripOldReasoning is enabled", () => {
459
+ // Build history with reasoning parts in assistant messages
460
+ const msgs = [
461
+ { role: "system", content: "You are an agent." },
462
+ { role: "user", content: "fix the bug" },
463
+ ];
464
+ // Add 6 tool turns with reasoning in assistant messages
465
+ for (let i = 1; i <= 6; i++) {
466
+ msgs.push({
467
+ role: "assistant",
468
+ content: [
469
+ { type: "reasoning", text: `Thinking about step ${i}... ${"x".repeat(3000)}` },
470
+ { type: "text", text: `Step ${i} analysis` },
471
+ { type: "tool-call", toolCallId: `call_${i}`, toolName: "bash", input: `echo ${i}` },
472
+ ],
473
+ });
474
+ msgs.push({
475
+ role: "tool",
476
+ content: [
477
+ {
478
+ type: "tool-result",
479
+ toolCallId: `call_${i}`,
480
+ toolName: "bash",
481
+ output: { type: "text", value: bigText(`R${i}`, 2) },
482
+ },
483
+ ],
484
+ });
485
+ }
486
+ const out = compactSubAgentMessages(msgs, {
487
+ thresholdChars: 1000, // force compaction
488
+ keepLastTurns: 2,
489
+ stripOldReasoning: true,
490
+ });
491
+ // Old turns (1-4) should have reasoning stripped
492
+ const oldAssistants = out.filter((m, idx) => m.role === "assistant" && idx < out.length - 4);
493
+ for (const m of oldAssistants) {
494
+ const content = m.content;
495
+ if (!Array.isArray(content))
496
+ continue;
497
+ const hasReasoning = content.some((p) => p.type === "reasoning");
498
+ expect(hasReasoning).toBe(false);
499
+ }
500
+ // Recent turns (last 2) should still have reasoning
501
+ const recentAssistants = out.filter((m, idx) => m.role === "assistant" && idx >= out.length - 4);
502
+ for (const m of recentAssistants) {
503
+ const content = m.content;
504
+ if (!Array.isArray(content))
505
+ continue;
506
+ const hasReasoning = content.some((p) => p.type === "reasoning");
507
+ expect(hasReasoning).toBe(true);
508
+ }
509
+ });
510
+ it("T1.1 — does NOT strip reasoning when stripOldReasoning is false (default)", () => {
511
+ const msgs = [
512
+ { role: "system", content: "You are an agent." },
513
+ { role: "user", content: "fix the bug" },
514
+ ];
515
+ for (let i = 1; i <= 4; i++) {
516
+ msgs.push({
517
+ role: "assistant",
518
+ content: [
519
+ { type: "reasoning", text: `Thinking... ${"x".repeat(3000)}` },
520
+ { type: "tool-call", toolCallId: `call_${i}`, toolName: "bash", input: `echo ${i}` },
521
+ ],
522
+ });
523
+ msgs.push({
524
+ role: "tool",
525
+ content: [
526
+ {
527
+ type: "tool-result",
528
+ toolCallId: `call_${i}`,
529
+ toolName: "bash",
530
+ output: { type: "text", value: bigText(`R${i}`, 2) },
531
+ },
532
+ ],
533
+ });
534
+ }
535
+ const out = compactSubAgentMessages(msgs, {
536
+ thresholdChars: 1000,
537
+ keepLastTurns: 1,
538
+ stripOldReasoning: false, // explicitly off
539
+ });
540
+ // All assistant messages should still have reasoning parts
541
+ const assistants = out.filter((m) => m.role === "assistant");
542
+ for (const m of assistants) {
543
+ const content = m.content;
544
+ if (!Array.isArray(content))
545
+ continue;
546
+ const hasReasoning = content.some((p) => p.type === "reasoning");
547
+ expect(hasReasoning).toBe(true);
548
+ }
549
+ });
550
+ describe("sliceMessageHistory & Step 4 truncation", () => {
551
+ it("preserves system prompt and user start while slicing excess messages", () => {
552
+ const msgs = [
553
+ { role: "system", content: "system prompt" },
554
+ { role: "user", content: "first prompt" },
555
+ { role: "assistant", content: "first answer" },
556
+ { role: "user", content: "second prompt" },
557
+ {
558
+ role: "assistant",
559
+ content: [{ type: "tool-call", toolCallId: "call-1", toolName: "bash", input: "ls" }],
560
+ },
561
+ {
562
+ role: "tool",
563
+ content: [{ type: "tool-result", toolCallId: "call-1", toolName: "bash", output: "file1" }],
564
+ },
565
+ { role: "assistant", content: "second answer" },
566
+ { role: "user", content: "third prompt" },
567
+ {
568
+ role: "assistant",
569
+ content: [{ type: "tool-call", toolCallId: "call-2", toolName: "bash", input: "pwd" }],
570
+ },
571
+ {
572
+ role: "tool",
573
+ content: [{ type: "tool-result", toolCallId: "call-2", toolName: "bash", output: "/dir" }],
574
+ },
575
+ { role: "assistant", content: "third answer" },
576
+ ];
577
+ // Slicing to max 5 messages should start cleanly at a user message
578
+ const sliced = sliceMessageHistory(msgs, 5);
579
+ expect(sliced[0].role).toBe("system");
580
+ expect(sliced[1].role).toBe("user");
581
+ // Slicing walked backward to find a user message start that satisfies max 5.
582
+ // Last messages:
583
+ // assistant: third answer (idx 10) -> tool result 2 (idx 9) -> assistant call 2 (idx 8) -> user third prompt (idx 7).
584
+ // That is 4 messages. Since kept must start with user, it stops at idx 7.
585
+ // So kept messages are from idx 7 onwards: [user third prompt, assistant call 2, tool result 2, assistant third answer]
586
+ expect(sliced).toHaveLength(5); // 1 system + 4 kept messages
587
+ expect(sliced[1].content).toBe("third prompt");
588
+ });
589
+ it("does not orphaned tool-calls and tool-results", () => {
590
+ const msgs = [
591
+ { role: "system", content: "system prompt" },
592
+ { role: "user", content: "prompt" },
593
+ {
594
+ role: "assistant",
595
+ content: [{ type: "tool-call", toolCallId: "call-99", toolName: "bash", input: "whoami" }],
596
+ },
597
+ {
598
+ role: "tool",
599
+ content: [{ type: "tool-result", toolCallId: "call-99", toolName: "bash", output: "root" }],
600
+ },
601
+ { role: "assistant", content: "hello" },
602
+ ];
603
+ // Even if maxMessages is 2, it shouldn't split assistant call-99 and tool result-99.
604
+ // Walking backward:
605
+ // - assistant: hello (count 1)
606
+ // - tool: result-99 (count 2, pendingToolCalls = {call-99})
607
+ // - assistant: call-99 (count 3, pendingToolCalls = {})
608
+ // - user: prompt (count 4, clean user start)
609
+ // So it must keep 4 messages + 1 system = 5 messages.
610
+ const sliced = sliceMessageHistory(msgs, 2);
611
+ expect(sliced).toHaveLength(5);
612
+ expect(sliced[1].role).toBe("user");
613
+ });
614
+ it("triggers slicing in compactSubAgentMessages when total exceeds 50K and messages count is > 30", () => {
615
+ const msgs = [
616
+ { role: "system", content: "system" },
617
+ { role: "user", content: "start" },
618
+ ];
619
+ // Push 40 messages to exceed 30 messages limit
620
+ for (let i = 0; i < 20; i++) {
621
+ msgs.push({ role: "assistant", content: `helper ${i}` });
622
+ msgs.push({ role: "user", content: `next question ${i}` });
623
+ }
624
+ // Generate a massive envelope size or message content to exceed 50K total chars
625
+ const out = compactSubAgentMessages(msgs, {
626
+ envelopeChars: 60_000, // force exceeds 50K
627
+ thresholdChars: 200_000, // high threshold to prevent compaction from running on top
628
+ });
629
+ // The message history should have been sliced (will be much fewer than 42 messages)
630
+ expect(out.length).toBeLessThan(msgs.length);
631
+ expect(out[0].role).toBe("system");
632
+ expect(out[1].role).toBe("user");
633
+ });
634
+ });
387
635
  });
388
636
  //# sourceMappingURL=subagent-compactor.spec.js.map
@@ -42,12 +42,16 @@ export declare function detectTextEmittedToolCall(text: string): TextToolCallDet
42
42
  * Parse the DeepSeek-native DSML tool-call markup into a structured list so the
43
43
  * re-steer can restate the model's EXACT intent (much more effective than a
44
44
  * generic "use the tool interface" nudge). Pure — no execution. Recognizes:
45
- * <||DSML||invoke name="read_file">
46
- * <||DSML||parameter name="file_path" string="true">src/app/foo.ts</||DSML||parameter>
47
- * </||DSML||invoke>
45
+ * Old format: <│invoke name="read_file">
46
+ * <│parameter name="file_path" string="true">src/app/foo.ts</│parameter>
47
+ * </│invoke>
48
+ * New format: <│ DSML │invoke name="read_file">
49
+ * <│ DSML │parameter name="file_path" string="true">src/app/foo.ts</│ DSML │parameter>
50
+ * </│ DSML │invoke>
51
+ * Both U+2502 (box-drawing │) and U+FF5C (fullwidth |) bars are recognized.
48
52
  * Returns one entry per invoke block; args preserve insertion order. Tolerant of
49
- * missing close tags (cheap models truncate) captures whatever parameters are
50
- * present. Returns [] when no parseable invoke block exists.
53
+ * missing close tags (cheap models truncate). Returns [] when no parseable invoke
54
+ * block exists.
51
55
  */
52
56
  export interface ParsedDsmlCall {
53
57
  name: string;
@@ -90,16 +90,18 @@ const PARAM_ALTERNATION = PARAM_TAGS.join("|");
90
90
  // is the Anthropic XML style; `<tool_call>` / `<function_calls>` are Qwen/other.
91
91
  // These are matched directly (the wrapper itself is the signal).
92
92
  const GENERIC_WRAPPER_RE = /<\/?(?:tool_call|function_calls|tool_use)\b|<invoke\b[^>]*\bname\s*=|<function\b[^>]*\bname\s*=/i;
93
- // DeepSeek native tool-call markup leaking into text content. Signature is the
94
- // fullwidth-bar sentinel `||DSML||` (U+FF5C, NOT ASCII pipes) wrapping
95
- // invoke/tool_calls/parameter tokens, e.g. `<||DSML||invoke name="read_file">`.
93
+ // DeepSeek native tool-call markup leaking into text content. Signature is a
94
+ // vertical-bar sentinel either U+FF5C fullwidth `|` or U+2502 box-drawing `│`
95
+ // — wrapping invoke/tool_calls/parameter tokens, e.g.:
96
+ // Old format: <|invoke name="read_file"> (U+FF5C, single bar)
97
+ // New format: <│ DSML │invoke name="…"> (U+2502, with optional DSML label)
96
98
  // Live: storyflow_ui explore-A/B, deepseek T3 (session 799f0508e830) emitted a
97
- // full `<||DSML||invoke name="read_file">…` block as text and made no real tool
98
- // call → empty, silent turn. The generic `<invoke` matcher misses it because the
99
- // `<` is followed by the sentinel, not `invoke`. (BUG-A telemetry already flags
100
- // the `||DSML||` substring; this wires it into detect re-steer/surface.)
101
- const DSML_WRAPPER_RE = /||DSML||\s*(?:invoke|tool_calls?|parameter)\b/i;
102
- const DSML_INVOKE_NAME_RE = /||DSML||\s*invoke\s+name\s*=\s*"([^"]+)"/i;
99
+ // full DSML invoke block as text and made no real tool call → empty, silent turn.
100
+ // The generic `<invoke` matcher misses it because `<` is followed by the sentinel.
101
+ // Updated 2026-06-24 to cover both U+2502 and U+FF5C (tests use U+2502).
102
+ const DSML_BAR = "[\\s]*[│||]+[│||\\s]*"; // matches U+2502 (box-drawing), U+FF5C (fullwidth), ASCII pipe |, and spaces
103
+ const DSML_WRAPPER_RE = new RegExp(`${DSML_BAR}\\s*(?:DSML\\s*${DSML_BAR}\\s*)?(?:invoke|tool_calls?|parameter)\\b`, "i");
104
+ const DSML_INVOKE_NAME_RE = new RegExp(`${DSML_BAR}\\s*(?:DSML\\s*${DSML_BAR}\\s*)?invoke\\s+name\\s*=\\s*"([^"]+)"`, "i");
103
105
  /** Build a per-tool detector: `<tool>` then (within a small gap) a `<param>` or `</tool>`. */
104
106
  function buildToolRegexes() {
105
107
  return TOOL_TAGS.map((tag) =>
@@ -136,10 +138,12 @@ function normalizeWrapperName(raw) {
136
138
  const m = raw.match(/tool_call|function_calls|tool_use|invoke|function/i);
137
139
  return m ? m[0].toLowerCase() : "tool_call";
138
140
  }
139
- const DSML_INVOKE_BLOCK_RE = /||DSML||\s*invoke\s+name\s*=\s*"([^"]+)"([\s\S]*?)(?=||DSML||\s*invoke\s|$)/gi;
140
- const DSML_PARAM_RE = /||DSML||\s*parameter\s+name\s*=\s*"([^"]+)"[^>]*>([\s\S]*?)<\/?||DSML||\s*parameter/gi;
141
+ // Guard regex: at least one DSML-bar sentinel must exist before we bother scanning
142
+ const DSML_GUARD_RE = /[│||]/;
143
+ const DSML_INVOKE_BLOCK_RE = new RegExp(`${DSML_BAR}\\s*(?:DSML\\s*${DSML_BAR}\\s*)?invoke\\s+name\\s*=\\s*"([^"]+)"([\\s\\S]*?)(?=${DSML_BAR}\\s*(?:DSML\\s*${DSML_BAR}\\s*)?invoke\\s|$)`, "gi");
144
+ const DSML_PARAM_RE = new RegExp(`${DSML_BAR}\\s*(?:DSML\\s*${DSML_BAR}\\s*)?parameter\\s+name\\s*=\\s*"([^"]+)"[^>]*>([\\s\\S]*?)<\\/?${DSML_BAR}\\s*(?:DSML\\s*${DSML_BAR}\\s*)?parameter`, "gi");
141
145
  export function parseDsmlToolCalls(text) {
142
- if (!text || !text.includes("||DSML||"))
146
+ if (!text || !DSML_GUARD_RE.test(text))
143
147
  return [];
144
148
  const calls = [];
145
149
  DSML_INVOKE_BLOCK_RE.lastIndex = 0;