muonroi-cli 1.7.1 → 1.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (328) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +122 -122
  3. package/dist/packages/agent-harness-core/src/event-filter.js +1 -0
  4. package/dist/packages/agent-harness-core/src/event-redact.js +8 -0
  5. package/dist/packages/agent-harness-core/src/mcp-server.d.ts +16 -0
  6. package/dist/packages/agent-harness-core/src/mcp-server.js +88 -10
  7. package/dist/packages/agent-harness-core/src/protocol.d.ts +15 -0
  8. package/dist/packages/agent-harness-core/src/selector.js +2 -2
  9. package/dist/packages/agent-harness-core/src/transports/sidechannel.js +1 -1
  10. package/dist/packages/agent-harness-opentui/src/agent-mode.js +1 -1
  11. package/dist/packages/agent-harness-opentui/src/install.js +1 -1
  12. package/dist/src/__test-stubs__/vitest-setup.js +4 -0
  13. package/dist/src/agent-harness/__tests__/mock-model.spec.js +54 -0
  14. package/dist/src/agent-harness/mock-model.d.ts +1 -1
  15. package/dist/src/agent-harness/mock-model.js +32 -3
  16. package/dist/src/agent-harness/test-spawn.js +3 -1
  17. package/dist/src/chat/chat-keychain.js +9 -1
  18. package/dist/src/cli/cost-forensics.js +12 -12
  19. package/dist/src/cli/keys.d.ts +2 -2
  20. package/dist/src/cli/keys.js +107 -28
  21. package/dist/src/cli/usage-report.js +2 -2
  22. package/dist/src/council/__tests__/abort-threading.test.d.ts +1 -0
  23. package/dist/src/council/__tests__/abort-threading.test.js +193 -0
  24. package/dist/src/council/__tests__/clarification-prompt.test.js +21 -1
  25. package/dist/src/council/__tests__/council-turn-length-emit.test.d.ts +1 -0
  26. package/dist/src/council/__tests__/council-turn-length-emit.test.js +130 -0
  27. package/dist/src/council/__tests__/debate-round-budget.test.d.ts +1 -0
  28. package/dist/src/council/__tests__/debate-round-budget.test.js +45 -0
  29. package/dist/src/council/__tests__/evaluator-metrics.test.js +3 -0
  30. package/dist/src/council/__tests__/post-debate-recommendation.test.d.ts +1 -0
  31. package/dist/src/council/__tests__/post-debate-recommendation.test.js +58 -0
  32. package/dist/src/council/__tests__/research-tools.test.js +4 -0
  33. package/dist/src/council/__tests__/round-tools.test.js +6 -0
  34. package/dist/src/council/clarifier-question-cap.test.d.ts +12 -0
  35. package/dist/src/council/clarifier-question-cap.test.js +73 -0
  36. package/dist/src/council/clarifier.d.ts +29 -1
  37. package/dist/src/council/clarifier.js +64 -1
  38. package/dist/src/council/debate-planner.d.ts +2 -1
  39. package/dist/src/council/debate-planner.js +5 -3
  40. package/dist/src/council/debate.d.ts +9 -0
  41. package/dist/src/council/debate.js +88 -8
  42. package/dist/src/council/index.d.ts +46 -1
  43. package/dist/src/council/index.js +125 -19
  44. package/dist/src/council/llm.js +41 -13
  45. package/dist/src/council/prompts.js +56 -21
  46. package/dist/src/council/types.d.ts +14 -1
  47. package/dist/src/ee/__tests__/export-transcripts.test.js +1 -1
  48. package/dist/src/ee/bridge.d.ts +3 -1
  49. package/dist/src/ee/bridge.js +9 -3
  50. package/dist/src/ee/bridge.test.js +1 -2
  51. package/dist/src/ee/client.js +15 -2
  52. package/dist/src/ee/recall-ledger.d.ts +20 -0
  53. package/dist/src/ee/recall-ledger.js +40 -1
  54. package/dist/src/ee/search.d.ts +25 -0
  55. package/dist/src/ee/search.js +73 -0
  56. package/dist/src/ee/types.d.ts +15 -1
  57. package/dist/src/ee/who-am-i.d.ts +41 -0
  58. package/dist/src/ee/who-am-i.js +143 -0
  59. package/dist/src/ee/who-am-i.test.d.ts +1 -0
  60. package/dist/src/ee/who-am-i.test.js +101 -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 +143 -104
  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 +2 -2
  76. package/dist/src/lsp/manager.test.js +2 -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/client-pool.d.ts +1 -1
  86. package/dist/src/mcp/client-pool.js +25 -9
  87. package/dist/src/mcp/ee-tools.d.ts +10 -0
  88. package/dist/src/mcp/ee-tools.js +34 -0
  89. package/dist/src/mcp/oauth-callback.js +2 -2
  90. package/dist/src/mcp/parse-headers.test.js +14 -14
  91. package/dist/src/mcp/self-verify-jobs.js +1 -3
  92. package/dist/src/mcp/setup-guide-text.js +74 -74
  93. package/dist/src/mcp/smoke.test.js +43 -43
  94. package/dist/src/models/catalog-gemini.test.js +12 -10
  95. package/dist/src/models/catalog.json +106 -38
  96. package/dist/src/ops/doctor.js +7 -7
  97. package/dist/src/orchestrator/__tests__/auto-commit.test.d.ts +1 -0
  98. package/dist/src/orchestrator/__tests__/auto-commit.test.js +142 -0
  99. package/dist/src/orchestrator/__tests__/batch-turn-runner.test.js +2 -0
  100. package/dist/src/orchestrator/__tests__/delegations.test.d.ts +1 -0
  101. package/dist/src/orchestrator/__tests__/delegations.test.js +96 -0
  102. package/dist/src/orchestrator/__tests__/message-processor.test.js +1 -0
  103. package/dist/src/orchestrator/__tests__/stream-runner.test.js +29 -0
  104. package/dist/src/orchestrator/agent.test.js +5 -2
  105. package/dist/src/orchestrator/auto-commit.d.ts +110 -0
  106. package/dist/src/orchestrator/auto-commit.js +364 -0
  107. package/dist/src/orchestrator/batch-turn-runner.d.ts +3 -2
  108. package/dist/src/orchestrator/batch-turn-runner.js +2 -1
  109. package/dist/src/orchestrator/compaction-proposer-prompt.d.ts +8 -0
  110. package/dist/src/orchestrator/compaction-proposer-prompt.js +43 -0
  111. package/dist/src/orchestrator/compaction.d.ts +22 -0
  112. package/dist/src/orchestrator/compaction.js +42 -0
  113. package/dist/src/orchestrator/delegations.d.ts +1 -0
  114. package/dist/src/orchestrator/delegations.js +41 -0
  115. package/dist/src/orchestrator/error-utils.js +27 -2
  116. package/dist/src/orchestrator/message-processor.d.ts +21 -5
  117. package/dist/src/orchestrator/message-processor.js +219 -2573
  118. package/dist/src/orchestrator/orchestrator.d.ts +8 -2
  119. package/dist/src/orchestrator/orchestrator.js +187 -34
  120. package/dist/src/orchestrator/preprocessor.d.ts +13 -0
  121. package/dist/src/orchestrator/preprocessor.js +145 -0
  122. package/dist/src/orchestrator/prompts.d.ts +8 -0
  123. package/dist/src/orchestrator/prompts.js +76 -60
  124. package/dist/src/orchestrator/safety-askcard.d.ts +66 -0
  125. package/dist/src/orchestrator/safety-askcard.js +74 -0
  126. package/dist/src/orchestrator/sandbox.test.js +8 -5
  127. package/dist/src/orchestrator/scope-ceiling.js +1 -1
  128. package/dist/src/orchestrator/stall-watchdog.d.ts +47 -0
  129. package/dist/src/orchestrator/stall-watchdog.js +23 -0
  130. package/dist/src/orchestrator/stall-watchdog.test.js +36 -1
  131. package/dist/src/orchestrator/stream-runner.js +52 -30
  132. package/dist/src/orchestrator/sub-agent-model-tier.js +1 -1
  133. package/dist/src/orchestrator/subagent-compactor.d.ts +14 -0
  134. package/dist/src/orchestrator/subagent-compactor.js +83 -10
  135. package/dist/src/orchestrator/subagent-compactor.spec.js +132 -0
  136. package/dist/src/orchestrator/text-tool-call-detector.d.ts +9 -5
  137. package/dist/src/orchestrator/text-tool-call-detector.js +16 -12
  138. package/dist/src/orchestrator/text-tool-call-detector.test.js +66 -8
  139. package/dist/src/orchestrator/tool-args-repair.js +1 -1
  140. package/dist/src/orchestrator/tool-engine.d.ts +151 -0
  141. package/dist/src/orchestrator/tool-engine.js +3079 -0
  142. package/dist/src/orchestrator/turn-runner-deps.d.ts +1 -0
  143. package/dist/src/pil/__tests__/discovery-types.test.js +15 -4
  144. package/dist/src/pil/__tests__/discovery.test.js +89 -145
  145. package/dist/src/pil/__tests__/dual-run.test.js +2 -0
  146. package/dist/src/pil/__tests__/layer1-intent-trace.test.js +3 -0
  147. package/dist/src/pil/__tests__/layer1-intent.test.js +3 -0
  148. package/dist/src/pil/__tests__/layer16-clarity.test.js +35 -32
  149. package/dist/src/pil/__tests__/layer18-acceptance.test.js +1 -78
  150. package/dist/src/pil/__tests__/layer2_5-ponytail.test.d.ts +1 -0
  151. package/dist/src/pil/__tests__/layer2_5-ponytail.test.js +53 -0
  152. package/dist/src/pil/__tests__/layer3-ee-injection.test.js +67 -4
  153. package/dist/src/pil/__tests__/layer3-injected-chunk.test.js +14 -2
  154. package/dist/src/pil/__tests__/layer4-gsd.test.js +30 -0
  155. package/dist/src/pil/__tests__/layer5-context.test.js +39 -1
  156. package/dist/src/pil/__tests__/layer6-output.test.js +30 -2
  157. package/dist/src/pil/__tests__/orchestrator-integration.test.js +3 -1
  158. package/dist/src/pil/__tests__/pipeline.test.js +11 -8
  159. package/dist/src/pil/__tests__/response-tools.test.js +52 -0
  160. package/dist/src/pil/agent-operating-contract.d.ts +1 -1
  161. package/dist/src/pil/agent-operating-contract.js +5 -3
  162. package/dist/src/pil/agent-operating-contract.test.js +2 -2
  163. package/dist/src/pil/cheap-model-playbook.d.ts +1 -1
  164. package/dist/src/pil/cheap-model-playbook.js +1 -1
  165. package/dist/src/pil/cheap-model-workbooks.d.ts +1 -1
  166. package/dist/src/pil/cheap-model-workbooks.js +1 -1
  167. package/dist/src/pil/config.d.ts +20 -0
  168. package/dist/src/pil/config.js +35 -0
  169. package/dist/src/pil/discovery-types.d.ts +32 -9
  170. package/dist/src/pil/discovery.d.ts +3 -3
  171. package/dist/src/pil/discovery.js +138 -244
  172. package/dist/src/pil/index.d.ts +1 -1
  173. package/dist/src/pil/index.js +1 -1
  174. package/dist/src/pil/layer1-intent.d.ts +9 -1
  175. package/dist/src/pil/layer1-intent.js +99 -44
  176. package/dist/src/pil/layer1-intent.test.js +112 -2
  177. package/dist/src/pil/layer15-context-scan.js +1 -1
  178. package/dist/src/pil/layer16-clarity.d.ts +21 -25
  179. package/dist/src/pil/layer16-clarity.js +52 -88
  180. package/dist/src/pil/layer18-acceptance.d.ts +13 -3
  181. package/dist/src/pil/layer18-acceptance.js +1 -75
  182. package/dist/src/pil/layer2-personality.js +7 -8
  183. package/dist/src/pil/layer2_5-ponytail.d.ts +2 -0
  184. package/dist/src/pil/layer2_5-ponytail.js +41 -0
  185. package/dist/src/pil/layer3-ee-injection.js +180 -14
  186. package/dist/src/pil/layer4-gsd.js +22 -8
  187. package/dist/src/pil/layer5-context.d.ts +1 -0
  188. package/dist/src/pil/layer5-context.js +26 -11
  189. package/dist/src/pil/layer6-output.js +23 -5
  190. package/dist/src/pil/llm-classify.js +3 -3
  191. package/dist/src/pil/native-capabilities-workbook.d.ts +1 -1
  192. package/dist/src/pil/native-capabilities-workbook.js +4 -3
  193. package/dist/src/pil/pipeline.js +15 -1
  194. package/dist/src/pil/response-tools.d.ts +16 -0
  195. package/dist/src/pil/response-tools.js +41 -0
  196. package/dist/src/pil/schema.d.ts +8 -0
  197. package/dist/src/pil/schema.js +24 -3
  198. package/dist/src/pil/task-tier-map.js +7 -7
  199. package/dist/src/pil/types.d.ts +9 -3
  200. package/dist/src/product-loop/__tests__/discovery-interview.test.js +103 -1
  201. package/dist/src/product-loop/__tests__/gather-selectable-alts.test.js +34 -0
  202. package/dist/src/product-loop/__tests__/sprint-self-verify.test.js +12 -12
  203. package/dist/src/product-loop/discovery-interview.d.ts +20 -0
  204. package/dist/src/product-loop/discovery-interview.js +121 -7
  205. package/dist/src/product-loop/done-gate.js +3 -3
  206. package/dist/src/product-loop/gather.js +19 -5
  207. package/dist/src/product-loop/loop-driver.js +20 -20
  208. package/dist/src/product-loop/progress-snapshot.js +4 -4
  209. package/dist/src/product-loop/sprint-self-verify.js +2 -2
  210. package/dist/src/providers/__tests__/reasoning-roundtrip.test.js +12 -0
  211. package/dist/src/providers/auth/__tests__/gemini-oauth.test.d.ts +1 -5
  212. package/dist/src/providers/auth/__tests__/gemini-oauth.test.js +1 -5
  213. package/dist/src/providers/auth/gcloud.d.ts +28 -0
  214. package/dist/src/providers/auth/gcloud.js +102 -0
  215. package/dist/src/providers/auth/gemini-oauth.d.ts +46 -19
  216. package/dist/src/providers/auth/gemini-oauth.js +292 -105
  217. package/dist/src/providers/auth/grok-oauth.d.ts +1 -1
  218. package/dist/src/providers/auth/grok-oauth.js +1 -1
  219. package/dist/src/providers/auth/registry.js +28 -6
  220. package/dist/src/providers/gemini.d.ts +1 -1
  221. package/dist/src/providers/gemini.js +10 -3
  222. package/dist/src/providers/keychain.d.ts +2 -2
  223. package/dist/src/providers/keychain.js +20 -2
  224. package/dist/src/providers/mcp-vision-bridge.js +48 -48
  225. package/dist/src/providers/openai.js +5 -1
  226. package/dist/src/providers/runtime.d.ts +1 -1
  227. package/dist/src/providers/runtime.js +11 -2
  228. package/dist/src/providers/siliconflow-sse-repair.js +1 -1
  229. package/dist/src/providers/strategies/google.strategy.d.ts +9 -2
  230. package/dist/src/providers/strategies/google.strategy.js +159 -9
  231. package/dist/src/providers/strategies/thinking-mode.js +19 -6
  232. package/dist/src/providers/types.d.ts +1 -1
  233. package/dist/src/reporter/index.js +1 -1
  234. package/dist/src/router/decide.test.js +40 -8
  235. package/dist/src/router/step-router.d.ts +20 -0
  236. package/dist/src/router/step-router.js +73 -0
  237. package/dist/src/scaffold/bb-ecosystem-apply.js +47 -47
  238. package/dist/src/scaffold/bb-quality-gate.js +5 -5
  239. package/dist/src/scaffold/continuation-prompt.js +60 -60
  240. package/dist/src/scaffold/init-new.js +453 -453
  241. package/dist/src/self-qa/__tests__/scenario-planner.test.js +3 -3
  242. package/dist/src/self-qa/agentic-loop.js +19 -19
  243. package/dist/src/self-qa/orchestrator.js +5 -5
  244. package/dist/src/self-qa/scenario-planner.js +1 -1
  245. package/dist/src/self-qa/spec-emitter.js +4 -4
  246. package/dist/src/storage/__tests__/migrations.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/interaction-log.js +5 -5
  250. package/dist/src/storage/migrations.js +122 -122
  251. package/dist/src/storage/session-experience-store.js +4 -4
  252. package/dist/src/storage/sessions.d.ts +6 -0
  253. package/dist/src/storage/sessions.js +49 -42
  254. package/dist/src/storage/transcript-view.js +7 -1
  255. package/dist/src/storage/transcript.d.ts +1 -0
  256. package/dist/src/storage/transcript.js +55 -0
  257. package/dist/src/storage/ui-interaction-log.d.ts +17 -0
  258. package/dist/src/storage/usage.js +14 -14
  259. package/dist/src/storage/workspaces.js +12 -12
  260. package/dist/src/tools/__tests__/native-tools.test.js +6 -0
  261. package/dist/src/tools/bash.d.ts +36 -23
  262. package/dist/src/tools/bash.js +59 -301
  263. package/dist/src/tools/bash.test.js +2 -257
  264. package/dist/src/tools/file-tracker.d.ts +5 -0
  265. package/dist/src/tools/file-tracker.js +14 -0
  266. package/dist/src/tools/file.js +2 -0
  267. package/dist/src/tools/git-safety.d.ts +38 -1
  268. package/dist/src/tools/git-safety.js +94 -4
  269. package/dist/src/tools/git-safety.test.js +45 -1
  270. package/dist/src/tools/native-tools.d.ts +1 -1
  271. package/dist/src/tools/native-tools.js +56 -0
  272. package/dist/src/tools/registry-bash-empty-command.test.js +4 -3
  273. package/dist/src/tools/registry-git-safety.test.js +17 -6
  274. package/dist/src/tools/registry.d.ts +5 -0
  275. package/dist/src/tools/registry.js +185 -11
  276. package/dist/src/types/index.d.ts +7 -1
  277. package/dist/src/ui/app.d.ts +2 -1
  278. package/dist/src/ui/app.js +0 -0
  279. package/dist/src/ui/components/council-question-card.js +1 -0
  280. package/dist/src/ui/components/message-view.js +37 -3
  281. package/dist/src/ui/components/structured-response-view.js +7 -0
  282. package/dist/src/ui/components/task-list-panel.js +3 -12
  283. package/dist/src/ui/containers/chat-feed.d.ts +40 -0
  284. package/dist/src/ui/containers/chat-feed.js +66 -0
  285. package/dist/src/ui/containers/modals-layer.d.ts +87 -0
  286. package/dist/src/ui/containers/modals-layer.js +18 -0
  287. package/dist/src/ui/modals/session-picker-modal.js +14 -9
  288. package/dist/src/ui/modals/update-modal.js +2 -1
  289. package/dist/src/ui/slash/__tests__/menu-parity.test.d.ts +1 -0
  290. package/dist/src/ui/slash/__tests__/menu-parity.test.js +1 -0
  291. package/dist/src/ui/slash/__tests__/ponytail.test.d.ts +1 -0
  292. package/dist/src/ui/slash/__tests__/ponytail.test.js +37 -0
  293. package/dist/src/ui/slash/council-inspect.js +4 -4
  294. package/dist/src/ui/slash/export.js +16 -1
  295. package/dist/src/ui/slash/menu-items.js +1 -0
  296. package/dist/src/ui/slash/ponytail.d.ts +2 -0
  297. package/dist/src/ui/slash/ponytail.js +19 -0
  298. package/dist/src/ui/status-bar/store.js +0 -1
  299. package/dist/src/ui/status-bar/store.test.js +2 -2
  300. package/dist/src/ui/types.d.ts +7 -0
  301. package/dist/src/ui/use-app-logic.d.ts +26 -0
  302. package/dist/src/ui/use-app-logic.js +0 -0
  303. package/dist/src/ui/utils/relaunch.d.ts +17 -0
  304. package/dist/src/ui/utils/relaunch.js +28 -4
  305. package/dist/src/ui/utils/relaunch.test.js +50 -0
  306. package/dist/src/ui/utils/text.d.ts +11 -0
  307. package/dist/src/ui/utils/text.js +21 -1
  308. package/dist/src/ui/utils/text.test.js +25 -1
  309. package/dist/src/ui/utils/tools.js +3 -1
  310. package/dist/src/usage/cost-log.js +2 -2
  311. package/dist/src/usage/estimator.d.ts +14 -0
  312. package/dist/src/usage/estimator.js +22 -0
  313. package/dist/src/utils/__tests__/compaction-caps.test.d.ts +21 -0
  314. package/dist/src/utils/__tests__/compaction-caps.test.js +187 -0
  315. package/dist/src/utils/__tests__/llm-deadline-abort.test.d.ts +1 -0
  316. package/dist/src/utils/__tests__/llm-deadline-abort.test.js +33 -0
  317. package/dist/src/utils/clipboard-image.js +23 -23
  318. package/dist/src/utils/llm-deadline.d.ts +9 -1
  319. package/dist/src/utils/llm-deadline.js +31 -2
  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 +27 -31
  324. package/dist/src/utils/settings.js +100 -109
  325. package/dist/src/utils/side-question.js +2 -2
  326. package/dist/src/utils/skills.js +3 -3
  327. package/dist/src/verify/__tests__/coverage-parsers.test.js +30 -30
  328. package/package.json +1 -1
@@ -20,22 +20,28 @@ import { spawn } from "node:child_process";
20
20
  * `process.argv[0]` itself. We sanitize the WHOLE args portion in one pass.
21
21
  */
22
22
  export function sanitizeArgvForResume(args, sessionId) {
23
- if (!sessionId || !sessionId.trim()) {
23
+ if (!sessionId?.trim()) {
24
24
  throw new Error("sanitizeArgvForResume: sessionId is required");
25
25
  }
26
26
  const out = [];
27
27
  for (let i = 0; i < args.length; i++) {
28
28
  const a = args[i];
29
- if (a === "-s" || a === "--session") {
29
+ if (a === "-s" || a === "--session" || a === "--mock-llm") {
30
30
  // skip the flag AND its value (if present and not another flag)
31
31
  const next = args[i + 1];
32
32
  if (next !== undefined && !next.startsWith("-"))
33
33
  i++;
34
34
  continue;
35
35
  }
36
- if (a.startsWith("--session=")) {
36
+ if (a.startsWith("--session=") || a.startsWith("--mock-llm=")) {
37
37
  continue; // skip the combined form
38
38
  }
39
+ // Transient launch-mode flags must NOT survive a resume — re-entering
40
+ // agent-mode (named-pipe transport) on a user-driven resume strands the
41
+ // child with no harness server. Drop them.
42
+ if (a === "--agent-mode") {
43
+ continue;
44
+ }
39
45
  out.push(a);
40
46
  }
41
47
  out.push("--session", sessionId);
@@ -60,12 +66,30 @@ export function relaunchWithSession(sessionId, opts = {}) {
60
66
  const exit = opts.onExit ?? ((code) => process.exit(code));
61
67
  const spawnImpl = opts.spawnFn ?? spawn;
62
68
  const args = sanitizeArgvForResume(argv.slice(1), sessionId);
69
+ // Tear down the current TUI and restore the terminal BEFORE spawning so the
70
+ // child inherits a clean TTY (no raw mode / mouse tracking / alt-screen).
71
+ if (opts.beforeSpawn) {
72
+ try {
73
+ opts.beforeSpawn();
74
+ }
75
+ catch (err) {
76
+ console.error(`[relaunch] beforeSpawn teardown failed: ${err?.message ?? err}`);
77
+ }
78
+ }
63
79
  const child = spawnImpl(exec, args, { stdio: "inherit", detached: false });
64
80
  child.once("error", (err) => {
65
81
  console.error(`[relaunch] spawn failed: ${err?.message ?? err}`);
66
82
  exit(1);
67
83
  });
68
- // Hand the TTY to the child and exit cleanly. The child takes over rendering.
84
+ if (opts.supervise) {
85
+ // Stay alive until the child exits so any intermediate launcher (bun run
86
+ // wrapper / bin shim) keeps the terminal foreground for the child instead
87
+ // of letting the shell reclaim it mid-restart.
88
+ child.once("exit", (code) => exit(code ?? 0));
89
+ return;
90
+ }
91
+ // Legacy fast-handoff: exit the instant the child spawns. Correct only when
92
+ // there is no intervening launcher waiting on this process.
69
93
  child.once("spawn", () => exit(0));
70
94
  }
71
95
  //# sourceMappingURL=relaunch.js.map
@@ -40,6 +40,24 @@ describe("sanitizeArgvForResume", () => {
40
40
  expect(() => sanitizeArgvForResume([], "")).toThrow(/sessionId is required/);
41
41
  expect(() => sanitizeArgvForResume([], " ")).toThrow(/sessionId is required/);
42
42
  });
43
+ it("strips transient launch-mode flags (--agent-mode) so resume does not re-enter agent-mode", () => {
44
+ expect(sanitizeArgvForResume(["--agent-mode", "-m", "grok-build-0.1"], "new-id")).toEqual([
45
+ "-m",
46
+ "grok-build-0.1",
47
+ "--session",
48
+ "new-id",
49
+ ]);
50
+ });
51
+ it("strips `--mock-llm <dir>` and the combined form", () => {
52
+ expect(sanitizeArgvForResume(["--mock-llm", "/fix/dir", "-y"], "new-id")).toEqual(["-y", "--session", "new-id"]);
53
+ expect(sanitizeArgvForResume(["--mock-llm=/fix/dir", "-y"], "new-id")).toEqual(["-y", "--session", "new-id"]);
54
+ });
55
+ it("strips agent-mode + mock-llm together (harness-launched resume → clean user CLI)", () => {
56
+ expect(sanitizeArgvForResume(["--agent-mode", "--mock-llm", "/fx", "--session", "old"], "new")).toEqual([
57
+ "--session",
58
+ "new",
59
+ ]);
60
+ });
43
61
  });
44
62
  describe("relaunchWithSession", () => {
45
63
  it("spawns the same executable with the sanitized argv + session id, then exits 0", () => {
@@ -72,6 +90,38 @@ describe("relaunchWithSession", () => {
72
90
  expect(errMock).toHaveBeenCalled();
73
91
  errMock.mockRestore();
74
92
  });
93
+ it("runs beforeSpawn teardown before spawning", () => {
94
+ const calls = [];
95
+ const child = new EventEmitter();
96
+ const spawnMock = vi.fn(() => {
97
+ calls.push("spawn");
98
+ return child;
99
+ });
100
+ relaunchWithSession("sess", {
101
+ argv: ["/bin/bun", "src/index.ts"],
102
+ onExit: () => { },
103
+ spawnFn: spawnMock,
104
+ beforeSpawn: () => calls.push("teardown"),
105
+ });
106
+ expect(calls).toEqual(["teardown", "spawn"]);
107
+ });
108
+ it("supervise: stays alive until the child exits, then propagates the exit code", () => {
109
+ const exitMock = vi.fn();
110
+ const child = new EventEmitter();
111
+ const spawnMock = vi.fn(() => child);
112
+ relaunchWithSession("sess", {
113
+ argv: ["/bin/bun", "src/index.ts"],
114
+ onExit: exitMock,
115
+ spawnFn: spawnMock,
116
+ supervise: true,
117
+ });
118
+ // Does NOT exit on spawn when supervising.
119
+ child.emit("spawn");
120
+ expect(exitMock).not.toHaveBeenCalled();
121
+ // Exits with the child's code when the child finally exits.
122
+ child.emit("exit", 3);
123
+ expect(exitMock).toHaveBeenCalledWith(3);
124
+ });
75
125
  it("throws when argv[0] is missing (cannot relaunch without an executable)", () => {
76
126
  expect(() => relaunchWithSession("sess", {
77
127
  argv: [],
@@ -1,4 +1,15 @@
1
1
  export declare function formatTokenCount(tokens: number): string;
2
+ /**
3
+ * Strip invisible/control Unicode characters that leak into terminal output.
4
+ * Keeps \t \n \r (needed for rendering); removes everything else that most
5
+ * terminals cannot display safely: zero-width spaces, soft hyphens, BiDi
6
+ * overrides, C0/C1 controls, BOM, and other default-ignorable code points.
7
+ * Fast-pathed: returns the input unchanged when no match is found.
8
+ *
9
+ * Uses `new RegExp()` to avoid Biome's `noControlCharactersInRegex` lint
10
+ * (control-char escapes in a regex literal are blocked).
11
+ */
12
+ export declare function stripInvisibleChars(text: string): string;
2
13
  export declare function trunc(s: string, n: number): string;
3
14
  export declare function truncateLine(s: string, n: number): string;
4
15
  export declare function truncateBlock(text: string, maxLines: number): string;
@@ -5,6 +5,26 @@ export function formatTokenCount(tokens) {
5
5
  return `${Math.round(tokens / 1_000)}K`;
6
6
  return String(tokens);
7
7
  }
8
+ /**
9
+ * Strip invisible/control Unicode characters that leak into terminal output.
10
+ * Keeps \t \n \r (needed for rendering); removes everything else that most
11
+ * terminals cannot display safely: zero-width spaces, soft hyphens, BiDi
12
+ * overrides, C0/C1 controls, BOM, and other default-ignorable code points.
13
+ * Fast-pathed: returns the input unchanged when no match is found.
14
+ *
15
+ * Uses `new RegExp()` to avoid Biome's `noControlCharactersInRegex` lint
16
+ * (control-char escapes in a regex literal are blocked).
17
+ */
18
+ export function stripInvisibleChars(text) {
19
+ if (!text)
20
+ return text;
21
+ // Constructed via new RegExp to bypass Biome lint for control-char patterns.
22
+ // U+034F (Combining Grapheme Joiner) is separated via alternation per
23
+ // Biome's noMisleadingCharacterClass rule.
24
+ const re = new RegExp("[\u0000-\u0008\u000B\u000C\u000E-\u001F\u007F-\u009F\u00AD\u061C\u070F\u200B-\u200F\u2028-\u202F\u2060-\u2069\uFEFF\uFFF9-\uFFFB]|" +
25
+ "\u034F|\u{1BCA0}-\u{1BCA3}|\u{E0001}|\u{E0020}-\u{E007F}", "gu");
26
+ return re.test(text) ? text.replace(re, "") : text;
27
+ }
8
28
  export function trunc(s, n) {
9
29
  const str = String(s ?? "");
10
30
  return str.length <= n ? str : `${str.slice(0, n)}…`;
@@ -37,7 +57,7 @@ export function sanitizeContent(raw) {
37
57
  * answer (e.g. `\frac{a}{b}`) is untouched. Fast-pathed: no work when absent.
38
58
  */
39
59
  export function stripStrayModelMacros(text) {
40
- if (!text || !text.includes("\\confidence"))
60
+ if (!text?.includes("\\confidence"))
41
61
  return text;
42
62
  // Trailing form (most common) — also swallow the whitespace/newline before it.
43
63
  let out = text.replace(/\s*\\confidence\s*\{[^}]*\}\s*$/i, "");
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from "vitest";
2
- import { stripStrayModelMacros } from "./text.js";
2
+ import { stripInvisibleChars, stripStrayModelMacros } from "./text.js";
3
3
  describe("stripStrayModelMacros", () => {
4
4
  it("strips a trailing \\confidence{NN} macro and the blank line before it", () => {
5
5
  const input = "Root cause: parseInt radix.\n\nFix: use radix 10.\n\n\\confidence{85}";
@@ -20,4 +20,28 @@ describe("stripStrayModelMacros", () => {
20
20
  expect(stripStrayModelMacros("")).toBe("");
21
21
  });
22
22
  });
23
+ describe("stripInvisibleChars", () => {
24
+ it("strips zero-width spaces (U+200B)", () => {
25
+ expect(stripInvisibleChars("hello\u200Bworld")).toBe("helloworld");
26
+ });
27
+ it("strips soft hyphen (U+00AD)", () => {
28
+ expect(stripInvisibleChars("hy\u00ADphen")).toBe("hyphen");
29
+ });
30
+ it("strips BOM (U+FEFF)", () => {
31
+ expect(stripInvisibleChars("\uFEFFHello")).toBe("Hello");
32
+ });
33
+ it("strips BiDi overrides (U+202E)", () => {
34
+ expect(stripInvisibleChars("abc\u202Edef")).toBe("abcdef");
35
+ });
36
+ it("strips C0 controls except \\t \\n \\r", () => {
37
+ expect(stripInvisibleChars("a\u0001b\rc\td")).toBe("ab\rc\td");
38
+ });
39
+ it("keeps normal text unchanged (fast path)", () => {
40
+ const normal = "Hello world, how are you?";
41
+ expect(stripInvisibleChars(normal)).toBe(normal);
42
+ });
43
+ it("handles empty string", () => {
44
+ expect(stripInvisibleChars("")).toBe("");
45
+ });
46
+ });
23
47
  //# sourceMappingURL=text.test.js.map
@@ -27,7 +27,9 @@ export function toolArgs(tc) {
27
27
  try {
28
28
  const a = JSON.parse(tc.function.arguments);
29
29
  if (tc.function.name === "bash")
30
- return String(a.command || "").replace(/\n/g, " ").trim();
30
+ return String(a.command || "")
31
+ .replace(/\n/g, " ")
32
+ .trim();
31
33
  if (tc.function.name === "read_file" || tc.function.name === "write_file" || tc.function.name === "edit_file")
32
34
  return String(a.file_path || a.path || "");
33
35
  if (describeMcpFsTool(tc.function.name)) {
@@ -14,7 +14,7 @@
14
14
  import { promises as fs } from "node:fs";
15
15
  import * as os from "node:os";
16
16
  import * as path from "node:path";
17
- import { projectCostUSD } from "./estimator.js";
17
+ import { projectCostUSD, sanitizeInputTokens } from "./estimator.js";
18
18
  function muonroiHome(homeOverride) {
19
19
  return homeOverride ?? process.env.MUONROI_CLI_HOME ?? path.join(os.homedir(), ".muonroi-cli");
20
20
  }
@@ -57,7 +57,7 @@ export async function meteredGenerate(args, fn) {
57
57
  const startedAt = Date.now();
58
58
  try {
59
59
  const { result, actualInputTokens, actualOutputTokens, cachedInputTokens, stepCount } = await fn();
60
- const inTok = actualInputTokens ?? estIn;
60
+ const inTok = sanitizeInputTokens(actualInputTokens, estIn);
61
61
  const outTok = actualOutputTokens ?? Math.max(1, Math.ceil(estOut / 4));
62
62
  const usd = projectCostUSD(args.provider, args.model, inTok, outTok);
63
63
  await appendCostLog({
@@ -10,6 +10,20 @@
10
10
  * Acceptable for cap projection only -- not for billing reconciliation.
11
11
  */
12
12
  export declare function estimateTokensFromChars(chars: number): number;
13
+ /**
14
+ * Sanitize an actual input-token count by comparing it to the estimated
15
+ * (chars/4) count. Some providers (SiliconFlow, possibly others) return
16
+ * implausibly low `prompt_tokens` (e.g. 10) regardless of actual prompt size,
17
+ * which under-reports cost and inflates apparent cache-hit ratios.
18
+ *
19
+ * Rules:
20
+ * - When `actual === undefined`: return `estimated` (no data = use estimate).
21
+ * - When `actual === 0`: return 0 (preserve mock / failed-call semantics).
22
+ * - When `estimated > 0` and `actual < estimated * 0.1`: the value is likely
23
+ * bogus — return `estimated` so cost projections stay accurate.
24
+ * - Otherwise: return `actual` as-is.
25
+ */
26
+ export declare function sanitizeInputTokens(actual: number | undefined, estimated: number): number;
13
27
  /**
14
28
  * Project the USD cost of a request given token estimates and the static pricing table.
15
29
  * Returns 0 for unknown provider/model -- caller decides how to handle (ledger treats as 0 risk).
@@ -13,6 +13,28 @@ import { lookupPricing } from "../providers/pricing.js";
13
13
  export function estimateTokensFromChars(chars) {
14
14
  return Math.ceil(chars / 4);
15
15
  }
16
+ /**
17
+ * Sanitize an actual input-token count by comparing it to the estimated
18
+ * (chars/4) count. Some providers (SiliconFlow, possibly others) return
19
+ * implausibly low `prompt_tokens` (e.g. 10) regardless of actual prompt size,
20
+ * which under-reports cost and inflates apparent cache-hit ratios.
21
+ *
22
+ * Rules:
23
+ * - When `actual === undefined`: return `estimated` (no data = use estimate).
24
+ * - When `actual === 0`: return 0 (preserve mock / failed-call semantics).
25
+ * - When `estimated > 0` and `actual < estimated * 0.1`: the value is likely
26
+ * bogus — return `estimated` so cost projections stay accurate.
27
+ * - Otherwise: return `actual` as-is.
28
+ */
29
+ export function sanitizeInputTokens(actual, estimated) {
30
+ if (actual === undefined)
31
+ return estimated;
32
+ if (actual === 0)
33
+ return 0;
34
+ if (estimated > 0 && actual < estimated * 0.1)
35
+ return estimated;
36
+ return actual;
37
+ }
16
38
  /**
17
39
  * Project the USD cost of a request given token estimates and the static pricing table.
18
40
  * Returns 0 for unknown provider/model -- caller decides how to handle (ledger treats as 0 risk).
@@ -0,0 +1,21 @@
1
+ /**
2
+ * src/utils/__tests__/compaction-caps.test.ts
3
+ *
4
+ * G4 regression guard for the token-thrift compaction caps.
5
+ *
6
+ * The compaction caps themselves are wired correctly (cost-leak-b3/b4 specs
7
+ * cover the algorithm; the *-tui specs cover the live wiring). The hole this
8
+ * file closes is DRIFT: nothing pinned the PRODUCTION DEFAULT knob values, and
9
+ * the fast cost-leak specs drive `compactSubAgentMessages` with HARDCODED
10
+ * options (thresholdChars: 60_000, keepLastTurns: 1) that are decoupled from
11
+ * the real getters. So a regression that widens a default (e.g. reverting the
12
+ * Phase C5 40_000 back to 200_000, or fat-fingering a clamp range) would sail
13
+ * past the entire fast suite and only be caught by the slow ~30-120s TUI specs.
14
+ *
15
+ * Part 1 pins each getter's documented contract (default / clamp / honor).
16
+ * Part 2 feeds the REAL getter outputs into the compactor over a synthetic
17
+ * load sized BETWEEN the default threshold and a plausible widened drift, so a
18
+ * default-value regression fails behaviourally too — not just at the value
19
+ * assertion.
20
+ */
21
+ export {};
@@ -0,0 +1,187 @@
1
+ /**
2
+ * src/utils/__tests__/compaction-caps.test.ts
3
+ *
4
+ * G4 regression guard for the token-thrift compaction caps.
5
+ *
6
+ * The compaction caps themselves are wired correctly (cost-leak-b3/b4 specs
7
+ * cover the algorithm; the *-tui specs cover the live wiring). The hole this
8
+ * file closes is DRIFT: nothing pinned the PRODUCTION DEFAULT knob values, and
9
+ * the fast cost-leak specs drive `compactSubAgentMessages` with HARDCODED
10
+ * options (thresholdChars: 60_000, keepLastTurns: 1) that are decoupled from
11
+ * the real getters. So a regression that widens a default (e.g. reverting the
12
+ * Phase C5 40_000 back to 200_000, or fat-fingering a clamp range) would sail
13
+ * past the entire fast suite and only be caught by the slow ~30-120s TUI specs.
14
+ *
15
+ * Part 1 pins each getter's documented contract (default / clamp / honor).
16
+ * Part 2 feeds the REAL getter outputs into the compactor over a synthetic
17
+ * load sized BETWEEN the default threshold and a plausible widened drift, so a
18
+ * default-value regression fails behaviourally too — not just at the value
19
+ * assertion.
20
+ */
21
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
22
+ import { compactSubAgentMessages, cumulativeMessageChars } from "../../orchestrator/subagent-compactor.js";
23
+ import { getSubAgentBudgetChars, getSubAgentCompactKeepLast, getSubAgentCompactThresholdChars, getTopLevelCompactKeepLast, getTopLevelCompactThresholdChars, getTopLevelToolBudgetChars, loadUserSettings, } from "../settings.js";
24
+ const ENV_KEYS = [
25
+ "MUONROI_SUB_AGENT_BUDGET_CHARS",
26
+ "MUONROI_SUBAGENT_COMPACT_THRESHOLD_CHARS",
27
+ "MUONROI_SUBAGENT_COMPACT_KEEP_LAST",
28
+ "MUONROI_TOP_LEVEL_COMPACT_THRESHOLD_CHARS",
29
+ "MUONROI_TOP_LEVEL_COMPACT_KEEP_LAST",
30
+ "MUONROI_TOP_LEVEL_TOOL_BUDGET_CHARS",
31
+ ];
32
+ const saved = {};
33
+ beforeEach(() => {
34
+ for (const k of ENV_KEYS) {
35
+ saved[k] = process.env[k];
36
+ delete process.env[k];
37
+ }
38
+ });
39
+ afterEach(() => {
40
+ for (const k of ENV_KEYS) {
41
+ if (saved[k] === undefined)
42
+ delete process.env[k];
43
+ else
44
+ process.env[k] = saved[k];
45
+ }
46
+ });
47
+ describe("compaction cap getters — production contract (G4 drift guard)", () => {
48
+ describe("compact thresholds & keepLast (Phase C5 defaults — no user-settings layer)", () => {
49
+ it("return the documented defaults when env is unset", () => {
50
+ expect(getSubAgentCompactThresholdChars()).toBe(40_000);
51
+ expect(getSubAgentCompactKeepLast()).toBe(3);
52
+ expect(getTopLevelCompactThresholdChars()).toBe(100_000);
53
+ expect(getTopLevelCompactKeepLast()).toBe(5);
54
+ });
55
+ it("clamp out-of-range env back to the default", () => {
56
+ process.env.MUONROI_SUBAGENT_COMPACT_THRESHOLD_CHARS = "999999999"; // > 500_000
57
+ expect(getSubAgentCompactThresholdChars()).toBe(40_000);
58
+ process.env.MUONROI_SUBAGENT_COMPACT_THRESHOLD_CHARS = "1000"; // < 20_000
59
+ expect(getSubAgentCompactThresholdChars()).toBe(40_000);
60
+ process.env.MUONROI_SUBAGENT_COMPACT_KEEP_LAST = "0"; // < 1
61
+ expect(getSubAgentCompactKeepLast()).toBe(3);
62
+ process.env.MUONROI_SUBAGENT_COMPACT_KEEP_LAST = "99"; // > 20
63
+ expect(getSubAgentCompactKeepLast()).toBe(3);
64
+ process.env.MUONROI_TOP_LEVEL_COMPACT_THRESHOLD_CHARS = "10000"; // < 50_000
65
+ expect(getTopLevelCompactThresholdChars()).toBe(100_000);
66
+ process.env.MUONROI_TOP_LEVEL_COMPACT_KEEP_LAST = "99"; // > 30
67
+ expect(getTopLevelCompactKeepLast()).toBe(5);
68
+ });
69
+ it("honor valid in-range env overrides", () => {
70
+ process.env.MUONROI_SUBAGENT_COMPACT_THRESHOLD_CHARS = "60000";
71
+ expect(getSubAgentCompactThresholdChars()).toBe(60_000);
72
+ process.env.MUONROI_SUBAGENT_COMPACT_KEEP_LAST = "5";
73
+ expect(getSubAgentCompactKeepLast()).toBe(5);
74
+ process.env.MUONROI_TOP_LEVEL_COMPACT_THRESHOLD_CHARS = "250000";
75
+ expect(getTopLevelCompactThresholdChars()).toBe(250_000);
76
+ process.env.MUONROI_TOP_LEVEL_COMPACT_KEEP_LAST = "8";
77
+ expect(getTopLevelCompactKeepLast()).toBe(8);
78
+ });
79
+ });
80
+ describe("budget getters (env override → user-settings → default)", () => {
81
+ it("getSubAgentBudgetChars: default 120_000 when env unset and no user override", () => {
82
+ // Deterministic on a clean checkout / CI (no user-settings.json). When a
83
+ // dev machine HAS an explicit override the getter must honor it instead —
84
+ // the default-drift case is what CI guards.
85
+ const override = loadUserSettings().subAgentBudgetChars;
86
+ if (typeof override === "number" && override >= 20_000 && override <= 600_000) {
87
+ expect(getSubAgentBudgetChars()).toBe(Math.floor(override));
88
+ }
89
+ else {
90
+ expect(getSubAgentBudgetChars()).toBe(120_000);
91
+ }
92
+ });
93
+ it("getTopLevelToolBudgetChars: default 200_000 when env unset and no user override", () => {
94
+ const override = loadUserSettings().topLevelToolBudgetChars;
95
+ if (typeof override === "number" && override >= 50_000 && override <= 1_500_000) {
96
+ expect(getTopLevelToolBudgetChars()).toBe(Math.floor(override));
97
+ }
98
+ else {
99
+ expect(getTopLevelToolBudgetChars()).toBe(200_000);
100
+ }
101
+ });
102
+ it("honor valid in-range env overrides (env short-circuits user-settings)", () => {
103
+ process.env.MUONROI_SUB_AGENT_BUDGET_CHARS = "300000";
104
+ expect(getSubAgentBudgetChars()).toBe(300_000);
105
+ process.env.MUONROI_TOP_LEVEL_TOOL_BUDGET_CHARS = "800000";
106
+ expect(getTopLevelToolBudgetChars()).toBe(800_000);
107
+ });
108
+ it("reject out-of-range env (result stays within the documented range)", () => {
109
+ process.env.MUONROI_SUB_AGENT_BUDGET_CHARS = "999999999"; // > 600_000
110
+ const sub = getSubAgentBudgetChars();
111
+ expect(sub).not.toBe(999_999_999);
112
+ expect(sub).toBeGreaterThanOrEqual(20_000);
113
+ expect(sub).toBeLessThanOrEqual(600_000);
114
+ process.env.MUONROI_TOP_LEVEL_TOOL_BUDGET_CHARS = "1000"; // < 50_000
115
+ const top = getTopLevelToolBudgetChars();
116
+ expect(top).not.toBe(1000);
117
+ expect(top).toBeGreaterThanOrEqual(50_000);
118
+ expect(top).toBeLessThanOrEqual(1_500_000);
119
+ });
120
+ });
121
+ });
122
+ /**
123
+ * Build N tool turns (assistant tool-call + tool result), each result
124
+ * `resultChars` long, using a tool name that is NOT high-value (so the
125
+ * compactor is allowed to elide it). Mirrors the cost-leak-b3 message shape.
126
+ */
127
+ function buildToolTurns(n, resultChars) {
128
+ const msgs = [
129
+ { role: "system", content: "You are the Explore sub-agent." },
130
+ { role: "user", content: "trace auth wiring" },
131
+ ];
132
+ for (let i = 0; i < n; i++) {
133
+ const id = `c${i}`;
134
+ msgs.push({
135
+ role: "assistant",
136
+ content: [{ type: "tool-call", toolCallId: id, toolName: "fake_read", input: { path: `/tmp/${id}.txt` } }],
137
+ });
138
+ msgs.push({
139
+ role: "tool",
140
+ content: [
141
+ {
142
+ type: "tool-result",
143
+ toolCallId: id,
144
+ toolName: "fake_read",
145
+ output: { type: "text", value: "x".repeat(resultChars) },
146
+ },
147
+ ],
148
+ });
149
+ }
150
+ return msgs;
151
+ }
152
+ describe("compaction fires with the REAL default getter values (G4 behavioural guard)", () => {
153
+ // Load sized BETWEEN the default thresholds (40K sub / 100K top) and a
154
+ // plausible widened drift (200K). So: with the real defaults it compacts;
155
+ // if a default were reverted to 200_000 the load would fall below threshold
156
+ // and these assertions would fail — catching drift behaviourally.
157
+ it("sub-agent: default threshold (40K) elides older tool results on a ~120K load", () => {
158
+ const messages = buildToolTurns(6, 20_000); // ~120K cumulative, 6 tool turns
159
+ const cumulative = cumulativeMessageChars(messages);
160
+ expect(cumulative).toBeGreaterThan(40_000);
161
+ expect(cumulative).toBeLessThan(200_000);
162
+ const compacted = compactSubAgentMessages(messages, {
163
+ thresholdChars: getSubAgentCompactThresholdChars(),
164
+ keepLastTurns: getSubAgentCompactKeepLast(),
165
+ });
166
+ expect(compacted).not.toBe(messages); // identity differs → compaction happened
167
+ const before = JSON.stringify(messages).length;
168
+ const after = JSON.stringify(compacted).length;
169
+ expect(after).toBeLessThan(before);
170
+ expect(JSON.stringify(compacted)).toContain("elided by sub-agent compactor");
171
+ });
172
+ it("top-level: default threshold (100K) elides older tool results on a ~160K load", () => {
173
+ const messages = buildToolTurns(8, 20_000); // ~160K cumulative, 8 tool turns
174
+ const cumulative = cumulativeMessageChars(messages);
175
+ expect(cumulative).toBeGreaterThan(100_000);
176
+ expect(cumulative).toBeLessThan(200_000);
177
+ const compacted = compactSubAgentMessages(messages, {
178
+ thresholdChars: getTopLevelCompactThresholdChars(),
179
+ keepLastTurns: getTopLevelCompactKeepLast(),
180
+ label: "top-level",
181
+ });
182
+ expect(compacted).not.toBe(messages);
183
+ expect(JSON.stringify(compacted).length).toBeLessThan(JSON.stringify(messages).length);
184
+ expect(JSON.stringify(compacted)).toContain("elided by top-level compactor");
185
+ });
186
+ });
187
+ //# sourceMappingURL=compaction-caps.test.js.map
@@ -0,0 +1,33 @@
1
+ /**
2
+ * withDeadlineRace abort-grace: a provider that ignores its abortSignal mid-call
3
+ * must not keep the caller blocked until the (minutes-long) wall-clock deadline.
4
+ * When the user-abort signal fires, the race rejects within a short grace window.
5
+ */
6
+ import { describe, expect, it } from "vitest";
7
+ import { withDeadlineRace } from "../llm-deadline.js";
8
+ describe("withDeadlineRace — abort grace", () => {
9
+ it("rejects shortly after abort even when fn() never settles", async () => {
10
+ const ac = new AbortController();
11
+ const neverSettles = () => new Promise(() => { }); // simulates a stalled provider
12
+ const start = Date.now();
13
+ setTimeout(() => ac.abort(new Error("user pressed Esc")), 40);
14
+ await expect(withDeadlineRace(neverSettles, 60_000, "test", ac.signal, 80)).rejects.toThrow(/aborted by user/);
15
+ // Must unblock far before the 60s deadline.
16
+ expect(Date.now() - start).toBeLessThan(2_000);
17
+ });
18
+ it("rejects immediately when the signal is already aborted", async () => {
19
+ const ac = new AbortController();
20
+ ac.abort(new Error("already cancelled"));
21
+ const neverSettles = () => new Promise(() => { });
22
+ await expect(withDeadlineRace(neverSettles, 60_000, "test", ac.signal, 50)).rejects.toThrow(/aborted by user/);
23
+ });
24
+ it("returns the fn() result when it settles before any abort", async () => {
25
+ const ac = new AbortController();
26
+ await expect(withDeadlineRace(() => Promise.resolve("ok"), 60_000, "test", ac.signal, 80)).resolves.toBe("ok");
27
+ });
28
+ it("still enforces the wall-clock deadline when no signal is passed (back-compat)", async () => {
29
+ const neverSettles = () => new Promise(() => { });
30
+ await expect(withDeadlineRace(neverSettles, 60, "test")).rejects.toThrow(/deadline/);
31
+ });
32
+ });
33
+ //# sourceMappingURL=llm-deadline-abort.test.js.map
@@ -25,24 +25,24 @@ function readWin32() {
25
25
  try {
26
26
  const escaped = tmpFile.replace(/\\/g, "\\\\");
27
27
  // Run in STA thread with retry — clipboard can be locked by other processes
28
- const ps = `
29
- Add-Type -AssemblyName System.Windows.Forms
30
- Add-Type -AssemblyName System.Drawing
31
- $maxRetries = 3
32
- $img = $null
33
- for ($i = 0; $i -lt $maxRetries; $i++) {
34
- try {
35
- $img = [System.Windows.Forms.Clipboard]::GetImage()
36
- if ($img -ne $null) { break }
37
- } catch {
38
- # clipboard locked — wait and retry
39
- }
40
- Start-Sleep -Milliseconds 100
41
- }
42
- if ($img -ne $null) {
43
- $img.Save('${escaped}', [System.Drawing.Imaging.ImageFormat]::Png)
44
- $img.Dispose()
45
- }
28
+ const ps = `
29
+ Add-Type -AssemblyName System.Windows.Forms
30
+ Add-Type -AssemblyName System.Drawing
31
+ $maxRetries = 3
32
+ $img = $null
33
+ for ($i = 0; $i -lt $maxRetries; $i++) {
34
+ try {
35
+ $img = [System.Windows.Forms.Clipboard]::GetImage()
36
+ if ($img -ne $null) { break }
37
+ } catch {
38
+ # clipboard locked — wait and retry
39
+ }
40
+ Start-Sleep -Milliseconds 100
41
+ }
42
+ if ($img -ne $null) {
43
+ $img.Save('${escaped}', [System.Drawing.Imaging.ImageFormat]::Png)
44
+ $img.Dispose()
45
+ }
46
46
  `;
47
47
  const result = spawnSync("powershell", ["-NoProfile", "-STA", "-Command", ps], { timeout: 8000 });
48
48
  if (result.error || result.status !== 0)
@@ -81,11 +81,11 @@ function readDarwin() {
81
81
  const pngpaste = spawnSync("pngpaste", [tmpFile], { timeout: 5000 });
82
82
  if (pngpaste.status !== 0) {
83
83
  spawnSync("screencapture", ["-c", "-x"]);
84
- const osascript = `
85
- set imgData to the clipboard as «class PNGf»
86
- set f to open for access POSIX file "${tmpFile}" with write permission
87
- write imgData to f
88
- close access f
84
+ const osascript = `
85
+ set imgData to the clipboard as «class PNGf»
86
+ set f to open for access POSIX file "${tmpFile}" with write permission
87
+ write imgData to f
88
+ close access f
89
89
  `;
90
90
  spawnSync("osascript", ["-e", osascript], { timeout: 5000 });
91
91
  }
@@ -41,4 +41,12 @@ export declare function withTimeoutSignal(parent: AbortSignal | undefined, timeo
41
41
  * still be aborted via `withTimeoutSignal` for cleanup; this layer just ensures
42
42
  * the caller is never blocked past the deadline.
43
43
  */
44
- export declare function withDeadlineRace<T>(fn: () => Promise<T>, deadlineMs: number, label: string): Promise<T>;
44
+ export declare function withDeadlineRace<T>(fn: () => Promise<T>, deadlineMs: number, label: string,
45
+ /**
46
+ * User-abort signal. When it fires, the race rejects within `abortGraceMs`
47
+ * even if `fn()` hasn't settled — so a provider that ignores its abortSignal
48
+ * mid-call (observed with DeepSeek/grok socket stalls) can't keep the caller
49
+ * (and, for council, the locked composer) blocked until the full `deadlineMs`.
50
+ * The short grace lets the normal fetch-level abort win first when it's quick.
51
+ */
52
+ abortSignal?: AbortSignal, abortGraceMs?: number): Promise<T>;