muonroi-cli 1.7.2 → 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 (322) 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 +55 -60
  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.js +43 -43
  253. package/dist/src/storage/transcript-view.js +7 -1
  254. package/dist/src/storage/transcript.d.ts +1 -0
  255. package/dist/src/storage/transcript.js +55 -0
  256. package/dist/src/storage/ui-interaction-log.d.ts +17 -0
  257. package/dist/src/storage/usage.js +14 -14
  258. package/dist/src/storage/workspaces.js +12 -12
  259. package/dist/src/tools/__tests__/native-tools.test.js +6 -0
  260. package/dist/src/tools/bash.d.ts +36 -23
  261. package/dist/src/tools/bash.js +59 -301
  262. package/dist/src/tools/bash.test.js +2 -257
  263. package/dist/src/tools/file-tracker.d.ts +5 -0
  264. package/dist/src/tools/file-tracker.js +14 -0
  265. package/dist/src/tools/file.js +2 -0
  266. package/dist/src/tools/git-safety.d.ts +38 -1
  267. package/dist/src/tools/git-safety.js +94 -4
  268. package/dist/src/tools/git-safety.test.js +45 -1
  269. package/dist/src/tools/native-tools.d.ts +1 -1
  270. package/dist/src/tools/native-tools.js +56 -0
  271. package/dist/src/tools/registry-bash-empty-command.test.js +4 -3
  272. package/dist/src/tools/registry-git-safety.test.js +17 -6
  273. package/dist/src/tools/registry.d.ts +5 -0
  274. package/dist/src/tools/registry.js +185 -11
  275. package/dist/src/types/index.d.ts +7 -1
  276. package/dist/src/ui/app.d.ts +1 -0
  277. package/dist/src/ui/app.js +0 -0
  278. package/dist/src/ui/components/council-question-card.js +1 -0
  279. package/dist/src/ui/components/message-view.js +37 -3
  280. package/dist/src/ui/components/structured-response-view.js +7 -0
  281. package/dist/src/ui/components/task-list-panel.js +3 -12
  282. package/dist/src/ui/containers/chat-feed.d.ts +40 -0
  283. package/dist/src/ui/containers/chat-feed.js +66 -0
  284. package/dist/src/ui/containers/modals-layer.d.ts +87 -0
  285. package/dist/src/ui/containers/modals-layer.js +18 -0
  286. package/dist/src/ui/slash/__tests__/menu-parity.test.d.ts +1 -0
  287. package/dist/src/ui/slash/__tests__/menu-parity.test.js +1 -0
  288. package/dist/src/ui/slash/__tests__/ponytail.test.d.ts +1 -0
  289. package/dist/src/ui/slash/__tests__/ponytail.test.js +37 -0
  290. package/dist/src/ui/slash/council-inspect.js +4 -4
  291. package/dist/src/ui/slash/export.js +16 -1
  292. package/dist/src/ui/slash/menu-items.js +1 -0
  293. package/dist/src/ui/slash/ponytail.d.ts +2 -0
  294. package/dist/src/ui/slash/ponytail.js +19 -0
  295. package/dist/src/ui/status-bar/store.js +0 -1
  296. package/dist/src/ui/status-bar/store.test.js +2 -2
  297. package/dist/src/ui/use-app-logic.d.ts +26 -0
  298. package/dist/src/ui/use-app-logic.js +0 -0
  299. package/dist/src/ui/utils/relaunch.js +1 -1
  300. package/dist/src/ui/utils/text.d.ts +11 -0
  301. package/dist/src/ui/utils/text.js +21 -1
  302. package/dist/src/ui/utils/text.test.js +25 -1
  303. package/dist/src/ui/utils/tools.js +3 -1
  304. package/dist/src/usage/cost-log.js +2 -2
  305. package/dist/src/usage/estimator.d.ts +14 -0
  306. package/dist/src/usage/estimator.js +22 -0
  307. package/dist/src/utils/__tests__/compaction-caps.test.d.ts +21 -0
  308. package/dist/src/utils/__tests__/compaction-caps.test.js +187 -0
  309. package/dist/src/utils/__tests__/llm-deadline-abort.test.d.ts +1 -0
  310. package/dist/src/utils/__tests__/llm-deadline-abort.test.js +33 -0
  311. package/dist/src/utils/clipboard-image.js +23 -23
  312. package/dist/src/utils/llm-deadline.d.ts +9 -1
  313. package/dist/src/utils/llm-deadline.js +31 -2
  314. package/dist/src/utils/permission-mode.d.ts +33 -0
  315. package/dist/src/utils/permission-mode.js +164 -3
  316. package/dist/src/utils/permission-mode.test.js +92 -1
  317. package/dist/src/utils/settings.d.ts +27 -31
  318. package/dist/src/utils/settings.js +100 -109
  319. package/dist/src/utils/side-question.js +2 -2
  320. package/dist/src/utils/skills.js +3 -3
  321. package/dist/src/verify/__tests__/coverage-parsers.test.js +30 -30
  322. package/package.json +1 -1
@@ -7,17 +7,17 @@ import { loadCustomInstructions } from "../utils/instructions.js";
7
7
  import { loadValidSubAgents, } from "../utils/settings.js";
8
8
  import { resolveShell } from "../utils/shell.js";
9
9
  import { discoverSkills, formatSkillsForPrompt } from "../utils/skills.js";
10
- // F3 — hard cap on tool rounds per user turn. Default reduced 7550
10
+ // F3 — hard cap on tool rounds per user turn. Default reduced 400100
11
11
  // after session bca83bcbaad1 logged 178 tool calls in a single turn while
12
- // monotonically growing billed input. Env override allowed range 10..200.
12
+ // monotonically growing billed input. Env override allowed range 10..400.
13
13
  function readMaxToolRoundsFromEnv() {
14
14
  const raw = process.env.MUONROI_MAX_TOOL_ROUNDS;
15
15
  if (!raw)
16
- return 12;
16
+ return 100;
17
17
  const n = Number(raw);
18
18
  if (!Number.isFinite(n))
19
- return 50;
20
- return Math.max(10, Math.min(200, Math.floor(n)));
19
+ return 100;
20
+ return Math.max(10, Math.min(400, Math.floor(n)));
21
21
  }
22
22
  export const MAX_TOOL_ROUNDS = readMaxToolRoundsFromEnv();
23
23
  export const VISION_MODEL = "grok-4-1-fast-reasoning";
@@ -143,6 +143,7 @@ function buildEnvironmentBlock() {
143
143
  if (platform === "win32") {
144
144
  shellRules.push('- When a Windows-native command is genuinely needed, invoke it explicitly: `cmd.exe /c "command"` or `powershell -NoProfile -Command "command"`.');
145
145
  }
146
+ shellRules.push("- CRITICAL: Batch independent commands in ONE call with `&&` or `;` instead of N sequential calls — each extra call adds ~500 tokens of overhead and prevents cross-request cache reuse. Examples: `ls && cat file` or `a; b; c; d` instead of four separate bash calls.");
146
147
  }
147
148
  else if (shell.kind === "powershell") {
148
149
  shellRules.push("- The bash tool runs PowerShell. Use PowerShell cmdlets: Get-ChildItem, Select-String, Measure-Object, ConvertTo-Json, $env:VAR.", "- DO NOT use POSIX-only commands: grep, sed, awk, wc (use Select-String / Measure-Object / -split instead).", "- For pipe redirection, use PowerShell syntax: `cmd | Select-Object -First 10`, not `cmd | head -10`.");
@@ -272,11 +273,28 @@ IMPORTANT:
272
273
  - Use write_file only for new files or when most of the file is changing. For very large files (>500 lines), split into multiple edit_file calls or write smaller chunks.
273
274
  - Use read_file instead of cat/head/tail for reading files.
274
275
  - When the user asks for an automated recurring or one-time run, use the schedule tools instead of only describing the setup.
275
- - Commit incrementally (in any git repo, without being asked): the moment a cohesive, working chunk is finished and its checks pass, commit it before starting the next chunk. This is part of doing the work, not a separate request — prefer several small, logically-scoped commits over one large catch-all commit at the end. Group related file changes per commit; keep unrelated changes in separate commits.
276
- - Every commit message you create MUST end with this attribution line, verbatim, on its own final line: "Coding by - Muonroi-CLI".
276
+ - If you have worked for a long time or hit a tool execution limit, DO NOT tell the user to move to a new session. Instead, advise them to run the \`/compact\` command to free up memory before continuing.
277
+ - Use the experience brain actively (it is how you stop repeating mistakes across sessions): at the start of an unfamiliar or risky step call ee_query to recall past lessons, and after acting on a recalled \`[id col]\` rate it with ee_feedback. The MOMENT you hit a mistake / error / dead-end and find the working fix, call ee_write to save the lesson (the pitfall AND the fix, concise and generalizable) it is embedded immediately and recallable via ee_query in this and future sessions. Saving a hard-won fix is part of doing the work, not optional.
278
+ - Commit your own work as you go (in any git repo, without being asked): use the git_commit tool — YOU write the commit message — the moment a cohesive, working chunk passes its checks, and after EACH step of a multi-step plan. Prefer several small, logically-scoped commits with clear messages (describe WHAT changed) over one catch-all at the end. git_commit stages only the files you wrote, excludes secrets/artifacts, and appends the "Coding by - Muonroi-CLI" attribution for you. (Any commit you instead make by hand via bash must still end with that attribution line, verbatim, on its own final line.)
277
279
  - After creating a recurring schedule, check the daemon status and start it with \`schedule_daemon_start\` if needed.
278
280
 
279
- Be direct. Execute, don't just describe. Show results, not plans.`,
281
+
282
+ Be direct. Execute, don't just describe. Show results, not plans.
283
+
284
+ TOKEN BUDGET:
285
+ - Each tool round sends ~17K system prompt tokens + accumulated tool results to the model.
286
+ - Task(explore) / task(general) isolates context in a sub-agent — much cheaper than 5+ top-level rounds.
287
+ - Consider: 1-2 rounds → direct; 3-5 rounds → consider task(explore); >5 rounds → should use task(explore).
288
+ WORKFLOW RULES:
289
+ - RESEARCH FIRST: Always prioritize research before proposing edits. DeepSeek and other models have knowledge cutoffs; do not assume you know the exact codebase structure or latest external libraries. Use 'grep', 'lsp', and 'read_file' to search the local codebase. Use MCP tools (like web search or documentation readers) to research external knowledge, APIs, or libraries. Use 'delegate' for deep background research. Read before you write.
290
+ - CLARIFY GRAY AREAS: If the user's request is ambiguous or leaves critical design decisions unspecified, STOP and ask the user for clarification before writing code. Do not hallucinate requirements.
291
+
292
+ SELF-LIMIT:
293
+ - When you've read 5+ files and haven't concluded, summarize findings and propose next step instead of reading more.
294
+ - BATCH TOOL CALLS: You MUST combine and invoke independent tool calls in parallel (e.g. read multiple files, or run grep and read a file concurrently) in a SINGLE turn. Do not wait for the result of one tool call before invoking another if you already know both are needed. This dramatically reduces conversation turns, roundtrip latency, and input token accumulation.
295
+ - BATCH BASH COMMANDS: Combine independent commands into ONE bash call (a; b; c) rather than sequential single calls — each separate call adds ~500 tokens of overhead and prevents prompt-cache reuse across the session.
296
+ - Read only specific file sections (start_line/end_line) instead of whole files.
297
+ - When a clear direction emerges from the first 2-3 tool results, act on it — don't over-investigate.`,
280
298
  plan: `You are muonroi-cli in Plan mode — you analyze and plan but DO NOT execute changes.
281
299
 
282
300
  ${ENVIRONMENT}
@@ -386,40 +404,60 @@ export function buildMcpCapabilityBlock(toolNames) {
386
404
  "to bash or hand-write JSON-RPC to reach an MCP server:\n" +
387
405
  lines.join("\n"));
388
406
  }
389
- export function buildSystemPromptParts(cwd, mode, sandboxMode, planContext, subagents, sandboxSettings, providerId, resumeDigest, options) {
390
- const chitchat = options?.chitchat === true;
407
+ const _staticPrefixCache = new Map();
408
+ const STATIC_PREFIX_CACHE_TTL_MS = 300_000; // 5 min — ample; inputs are session-stable
409
+ function staticPrefixCacheKey(cwd, mode, providerId, isChitchat, subagentsHash, subAgent = false) {
410
+ return `${cwd}|${mode}|${providerId}|${isChitchat}|${subagentsHash}|${subAgent}`;
411
+ }
412
+ function computeStaticPrefix(cwd, mode, subagents, providerId, chitchat, subAgent = false) {
391
413
  const custom = loadCustomInstructions(cwd);
392
- const customSection = custom
393
- ? `\n\nCUSTOM INSTRUCTIONS:\n${custom}\n\nFollow the above alongside standard instructions.\n`
394
- : "";
395
- // Skip skills + subagents catalogs for chitchat these are pure tool/agent
396
- // routing context the model doesn't need to answer "Hi". On this codebase
397
- // they account for ~3K tokens per call.
398
- const skillsText = chitchat ? "" : formatSkillsForPrompt(discoverSkills(cwd));
414
+ const customSection = subAgent || !custom
415
+ ? ""
416
+ : `\n\nCUSTOM INSTRUCTIONS:\n${custom}\n\nFollow the above alongside standard instructions.\n`;
417
+ const skillsText = chitchat || subAgent ? "" : formatSkillsForPrompt(discoverSkills(cwd));
399
418
  const skillsSection = skillsText ? `\n\n${skillsText}\n` : "";
400
419
  const subagentsSection = chitchat ? "" : formatCustomSubagentsPromptSection(subagents ?? loadValidSubAgents());
401
- const sandboxSection = formatSandboxPromptSection(sandboxMode, sandboxSettings);
402
420
  let modePrompt = MODE_PROMPTS[mode];
403
- // Phase 12.2-G5: defer the "is this an anthropic-style prompt or not?"
404
- // decision to the provider capability. Default (no providerId) keeps the
405
- // pre-G5 behaviour by treating the prompt as anthropic-native.
406
421
  if (!providerId)
407
422
  throw new Error("providerId is required to build system prompt — cannot determine prompt style.");
408
423
  const promptStyle = getProviderCapabilities(providerId).systemPromptStyle();
409
424
  if (promptStyle !== "anthropic") {
410
425
  modePrompt = stripToolsSection(modePrompt) + NON_ANTHROPIC_TOOL_PREAMBLE;
411
426
  }
412
- // Front-load the Agent Operating Contract (all tiers) so the
413
- // grounding/evidence discipline registers the same rules buried in
414
- // CUSTOM INSTRUCTIONS get underweighted when not front-loaded. Skipped for
415
- // chitchat (no tools, no factual claims). See agent-operating-contract.ts.
427
+ // Agent mode: strip tool descriptions for tools rarely needed in coding tasks
428
+ // to reduce system-prompt bloat. The tools remain available via API.
429
+ if (mode === "agent") {
430
+ modePrompt = modePrompt.replace(/\n- (wallet_|paid_|fetch_payment|schedule_|generate_|computer_|search_x).*/g, "");
431
+ }
416
432
  const contractSection = buildContractSection({ chitchat });
417
- // Native capability manifest (agent mode only) — gives the in-CLI agent a
418
- // self-model of the tools, sub-agents, and CLI subsystems it has, so it
419
- // wields them instead of reconstructing its own runtime by grepping source.
420
- // See native-capabilities-workbook.ts (session d95113d3be09 motivation).
421
433
  const nativeCapabilitiesSection = buildNativeCapabilitiesSection({ mode, chitchat });
422
- const staticPrefix = `${contractSection}${nativeCapabilitiesSection}${modePrompt}${sandboxSection}${customSection}${skillsSection}${subagentsSection}`;
434
+ const prefix = `${contractSection}${nativeCapabilitiesSection}${modePrompt}${customSection}${skillsSection}${subagentsSection}`;
435
+ return { prefix };
436
+ }
437
+ export function buildSystemPromptParts(cwd, mode, sandboxMode, planContext, subagents, sandboxSettings, providerId, resumeDigest, options) {
438
+ const chitchat = options?.chitchat === true;
439
+ const subAgent = options?.subAgent ?? false;
440
+ const pid = providerId ?? "default";
441
+ // Subagents rarely change mid-session, but when they do we need a cache miss.
442
+ // JSON-stable stringify is fast for typical configs (< 10 entries, no circular refs).
443
+ const subagentsHash = subagents ? JSON.stringify(subagents) : "none";
444
+ // Try cache for the static prefix
445
+ const key = staticPrefixCacheKey(cwd, mode, pid, chitchat, subagentsHash, subAgent);
446
+ const now = Date.now();
447
+ const cached = _staticPrefixCache.get(key);
448
+ let staticPrefix;
449
+ if (cached && now - cached.cachedAt < STATIC_PREFIX_CACHE_TTL_MS) {
450
+ staticPrefix = cached.prefix;
451
+ }
452
+ else {
453
+ // Cache miss — compute and store
454
+ const result = computeStaticPrefix(cwd, mode, subagents, pid, chitchat, subAgent);
455
+ staticPrefix = result.prefix;
456
+ _staticPrefixCache.set(key, {
457
+ prefix: staticPrefix,
458
+ cachedAt: now,
459
+ });
460
+ }
423
461
  const planSection = planContext
424
462
  ? `\n\nAPPROVED PLAN:\nThe following plan has been approved by the user. Execute it now.\n${planContext}\n`
425
463
  : "";
@@ -429,6 +467,10 @@ export function buildSystemPromptParts(cwd, mode, sandboxMode, planContext, suba
429
467
  const dynamicSuffix = `${planSection}${resumeSection}\n\nCurrent working directory: ${cwd}`;
430
468
  return { staticPrefix, dynamicSuffix };
431
469
  }
470
+ /** Reset the static prefix cache (for tests). */
471
+ export function resetStaticPrefixCache() {
472
+ _staticPrefixCache.clear();
473
+ }
432
474
  export function buildSystemPrompt(cwd, mode, sandboxMode, planContext, subagents, sandboxSettings, providerId, resumeDigest, options) {
433
475
  const { staticPrefix, dynamicSuffix } = buildSystemPromptParts(cwd, mode, sandboxMode, planContext, subagents, sandboxSettings, providerId, resumeDigest, options);
434
476
  return `${staticPrefix}${dynamicSuffix}`;
@@ -556,39 +598,13 @@ export function buildSubagentPrompt(request, cwd, custom, sandboxMode, subagents
556
598
  "",
557
599
  `Delegated task: ${request.description}`,
558
600
  "",
559
- buildSystemPrompt(cwd, mode, sandboxMode, undefined, subagents, sandboxSettings, providerId),
601
+ buildSystemPrompt(cwd, mode, sandboxMode, undefined, subagents, sandboxSettings, providerId, undefined, {
602
+ subAgent: true,
603
+ }),
560
604
  ].join("\n");
561
605
  }
562
606
  export function formatSandboxPromptSection(sandboxMode, settings) {
563
- if (sandboxMode === "off")
564
- return "";
565
- const s = settings ?? {};
566
- let networkLine;
567
- if (s.allowNet) {
568
- networkLine = s.allowedHosts?.length
569
- ? `- Network access is restricted to: ${s.allowedHosts.join(", ")}.`
570
- : "- Network access is enabled.";
571
- }
572
- else {
573
- networkLine = "- Network is disabled.";
574
- }
575
- const lines = [
576
- "",
577
- "SANDBOX MODE:",
578
- "- Bash commands run inside a Shuru sandbox.",
579
- networkLine,
580
- "- The current workspace is mounted inside the sandbox at `/workspace`.",
581
- "- Shell-side workspace file changes do not persist back to the host in this version.",
582
- "- Use `read_file`, `edit_file`, and `write_file` for durable source edits.",
583
- "- If a task needs a host-persistent shell mutation, explain that sandbox mode blocks that workflow and ask whether to disable sandbox mode.",
584
- ];
585
- if (s.ports?.length) {
586
- lines.push(`- Port forwards: ${s.ports.join(", ")}.`);
587
- }
588
- if (s.from) {
589
- lines.push(`- Starting from checkpoint: ${s.from}.`);
590
- }
591
- return lines.join("\n");
607
+ return "";
592
608
  }
593
609
  export function applyModelConstraints(system, modelId) {
594
610
  const modelInfo = getModelInfo(modelId);
@@ -0,0 +1,66 @@
1
+ /**
2
+ * src/orchestrator/safety-askcard.ts
3
+ *
4
+ * Plans an askcard layout when a tool call is blocked by the safety filter
5
+ * (catastrophic, dangerous, git-safety, empty-bash).
6
+ *
7
+ * The askcard gives the user a choice: allow the block once, or confirm the block.
8
+ *
9
+ * Pure — no React, no side effects. Unit-testable in isolation.
10
+ */
11
+ export type SafetyBlockKind = "catastrophic" | "dangerous" | "git-safety" | "empty-bash";
12
+ export interface SafetyBlockInfo {
13
+ kind: SafetyBlockKind;
14
+ /** The blocked command or tool call description */
15
+ blockedItem: string;
16
+ /** Human-readable reason from the filter */
17
+ reason: string;
18
+ /** Suggested "safe" alternative, if any */
19
+ alternative?: string;
20
+ }
21
+ export interface SafetyAskcardLayout {
22
+ /** Question shown in the dialog title */
23
+ question: string;
24
+ /** Detail text shown below the question */
25
+ detail: string;
26
+ /** Options in render order. First = default (Enter applies). */
27
+ options: SafetyAskcardOption[];
28
+ /** Index of the default-selected option (0 = first). */
29
+ defaultIndex: number;
30
+ }
31
+ export interface SafetyAskcardOption {
32
+ label: string;
33
+ value: SafetyAskResult;
34
+ description?: string;
35
+ }
36
+ export type SafetyAskResult = "allow-once" | "allow-session" | "block";
37
+ /**
38
+ * Plan the askcard layout for a safety block.
39
+ * Pure function — easy to unit test and swap UI strategies.
40
+ */
41
+ export declare function planSafetyAskcard(info: SafetyBlockInfo): SafetyAskcardLayout;
42
+ /** Info passed to the safety-override handler when a tool block is detected. */
43
+ export interface SafetyOverrideAskInfo {
44
+ kind: SafetyBlockKind;
45
+ /** The blocked tool name (e.g. "bash", "edit_file"). */
46
+ toolName: string;
47
+ /** The command/args that were blocked. */
48
+ blockedItem: string;
49
+ /** Human-readable reason from the filter. */
50
+ reason: string;
51
+ /** Source block kind tag for the agent to decide retry. */
52
+ source: "bash.execute" | "registry.precheck";
53
+ }
54
+ /** Verdict the UI returns after the user answers the askcard. */
55
+ export type SafetyOverrideVerdict = {
56
+ action: "allow-once";
57
+ } | {
58
+ action: "allow-session";
59
+ } | {
60
+ action: "block";
61
+ };
62
+ /**
63
+ * Build a safe alternative suggestion for a blocked command.
64
+ * Returns null when no canned alternative exists.
65
+ */
66
+ export declare function suggestAlternative(kind: SafetyBlockKind, command: string): string | undefined;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * src/orchestrator/safety-askcard.ts
3
+ *
4
+ * Plans an askcard layout when a tool call is blocked by the safety filter
5
+ * (catastrophic, dangerous, git-safety, empty-bash).
6
+ *
7
+ * The askcard gives the user a choice: allow the block once, or confirm the block.
8
+ *
9
+ * Pure — no React, no side effects. Unit-testable in isolation.
10
+ */
11
+ const CATEGORY_LABELS = {
12
+ catastrophic: "Lỗi bảo mật nghiêm trọng",
13
+ dangerous: "Lệnh nguy hiểm",
14
+ "git-safety": "Git safety gate",
15
+ "empty-bash": "Bash call trống",
16
+ };
17
+ /**
18
+ * Plan the askcard layout for a safety block.
19
+ * Pure function — easy to unit test and swap UI strategies.
20
+ */
21
+ export function planSafetyAskcard(info) {
22
+ const kindLabel = CATEGORY_LABELS[info.kind];
23
+ const cmdPreview = info.blockedItem.length > 120 ? `${info.blockedItem.slice(0, 117)}...` : info.blockedItem;
24
+ const question = `⚠️ ${kindLabel}: Cho phép thực thi?`;
25
+ const detailLines = [`Lệnh bị chặn: \`${cmdPreview}\``, `Lý do: ${info.reason}`];
26
+ if (info.alternative) {
27
+ detailLines.push(`Gợi ý thay thế: ${info.alternative}`);
28
+ }
29
+ const options = [
30
+ {
31
+ label: "Cho phép 1 lần (Allow once)",
32
+ value: "allow-once",
33
+ description: "Chỉ chạy lệnh này một lần duy nhất",
34
+ },
35
+ {
36
+ label: "Chặn (Block)",
37
+ value: "block",
38
+ description: "Không chạy lệnh này, trả về lỗi cho agent",
39
+ },
40
+ ];
41
+ // Allow-session only for less severe blocks
42
+ if (info.kind !== "catastrophic") {
43
+ options.splice(1, 0, {
44
+ label: "Cho phép cả phiên (Allow session)",
45
+ value: "allow-session",
46
+ description: "Cho phép tất cả lệnh tương tự trong phiên này",
47
+ });
48
+ }
49
+ return {
50
+ question,
51
+ detail: detailLines.join("\n"),
52
+ options,
53
+ defaultIndex: options.findIndex((o) => o.value === "block"),
54
+ };
55
+ }
56
+ /**
57
+ * Build a safe alternative suggestion for a blocked command.
58
+ * Returns null when no canned alternative exists.
59
+ */
60
+ export function suggestAlternative(kind, command) {
61
+ if (kind === "empty-bash") {
62
+ return 'Provide a real command, e.g. {"command":"ls -la"}';
63
+ }
64
+ // sqlite3 / .db access on Windows
65
+ if (/sqlite3|\.db\b/i.test(command)) {
66
+ return "Use the filesystem MCP or write_file to read the DB file instead of shelling sqlite3";
67
+ }
68
+ // rm -rf patterns
69
+ if (/rm\s+-rf/i.test(command)) {
70
+ return "Explicitly list files to remove, or use move to trash";
71
+ }
72
+ return undefined;
73
+ }
74
+ //# sourceMappingURL=safety-askcard.js.map
@@ -39,21 +39,23 @@ afterEach(() => {
39
39
  vi.doUnmock("../storage/index.js");
40
40
  });
41
41
  describe("Agent sandbox mode", { timeout: 30_000 }, () => {
42
- it("can switch sandbox mode at runtime", async () => {
42
+ it("can switch sandbox mode at runtime — always 'off' (sandbox removed)", async () => {
43
43
  const { Agent } = await importAgentModule();
44
44
  const agent = new Agent(undefined, undefined, undefined, undefined, {
45
45
  persistSession: false,
46
46
  sandboxMode: "off",
47
47
  });
48
48
  expect(agent.getSandboxMode()).toBe("off");
49
+ // Sandbox has been removed: setSandboxMode is a no-op stub.
50
+ // The mode remains "off" regardless of what is passed.
49
51
  agent.setSandboxMode("shuru");
50
- expect(agent.getSandboxMode()).toBe("shuru");
52
+ expect(agent.getSandboxMode()).toBe("off");
51
53
  });
52
- it("passes sandbox mode into background delegations", async () => {
54
+ it("passes sandbox mode into background delegations — always 'off' (sandbox removed)", async () => {
53
55
  const { Agent } = await importAgentModule();
54
56
  const agent = new Agent(undefined, undefined, undefined, undefined, {
55
57
  persistSession: false,
56
- sandboxMode: "shuru",
58
+ sandboxMode: "shuru", // accepted for back-compat but ignored
57
59
  });
58
60
  const startMock = vi.fn(async () => ({ success: true, output: "ok" }));
59
61
  agent.delegations.start = startMock;
@@ -62,7 +64,8 @@ describe("Agent sandbox mode", { timeout: 30_000 }, () => {
62
64
  description: "Inspect",
63
65
  prompt: "Look around",
64
66
  });
65
- expect(startMock).toHaveBeenCalledWith(expect.objectContaining({ agent: "explore" }), expect.objectContaining({ sandboxMode: "shuru" }));
67
+ // Sandbox removed: delegations always receive sandboxMode: "off".
68
+ expect(startMock).toHaveBeenCalledWith(expect.objectContaining({ agent: "explore" }), expect.objectContaining({ sandboxMode: "off" }));
66
69
  });
67
70
  it("can get and set sandbox settings", async () => {
68
71
  const { Agent } = await importAgentModule();
@@ -80,7 +80,7 @@ export function parseBudgetOverride(raw) {
80
80
  return { override: undefined, cleanedPrompt: raw };
81
81
  // Strip the entire matched segment including its leading/trailing whitespace
82
82
  // and re-collapse the surrounding spaces.
83
- const cleaned = (raw.slice(0, match.index ?? 0) + " " + raw.slice((match.index ?? 0) + match[0].length))
83
+ const cleaned = `${raw.slice(0, match.index ?? 0)} ${raw.slice((match.index ?? 0) + match[0].length)}`
84
84
  .replace(/\s+/g, " ")
85
85
  .trim();
86
86
  return { override: n, cleanedPrompt: cleaned };
@@ -57,6 +57,53 @@ export interface StallRepromptState {
57
57
  * effects) so it is unit-testable in isolation from the orchestrator loop.
58
58
  */
59
59
  export declare function shouldRepromptStall(s: StallRepromptState): boolean;
60
+ /** Inputs to the mid-loop stall continuation decision — see {@link shouldContinueAfterMidLoopStall}. */
61
+ export interface MidLoopStallState {
62
+ /** The watchdog fired for this attempt. */
63
+ stallTriggered: boolean;
64
+ /**
65
+ * Real content parts received across the WHOLE streamText attempt (all steps).
66
+ * `> 0` proves earlier tool steps already ran — so this is NOT a time-to-first-
67
+ * byte stall (that case is handled by {@link shouldRepromptStall}).
68
+ */
69
+ chunksThisAttempt: number;
70
+ /**
71
+ * Real content parts received since the last step boundary (reset in
72
+ * `prepareStep`). `0` means the in-flight step's provider request produced no
73
+ * byte before the watchdog fired — a dead socket on a SINGLE step, with every
74
+ * prior step fully completed. Safe to continue: the completed steps'
75
+ * assistant+tool messages are appended to history before re-issuing, so no
76
+ * tool is re-run and no text is duplicated.
77
+ */
78
+ chunksThisStep: number;
79
+ /** How many mid-loop continuations have already happened this turn. */
80
+ retryCount: number;
81
+ /** Configured cap (getProviderStallRetries); 0 disables continuation. */
82
+ maxRetries: number;
83
+ /** True on genuine user cancel (never continue over a cancel). */
84
+ aborted: boolean;
85
+ }
86
+ /**
87
+ * Decide whether a fired stall watchdog should CONTINUE the turn (append the
88
+ * completed steps' messages, then re-issue streamText to resume from the
89
+ * stalled step) instead of falling through to the partial-answer rescue.
90
+ *
91
+ * This is the mid-loop counterpart to {@link shouldRepromptStall}. The TTFB
92
+ * re-prompt restarts the WHOLE request from the original prompt, so it is gated
93
+ * on `chunksThisAttempt === 0` to avoid re-running tools. Continuation instead
94
+ * preserves all completed steps in history (assistant tool-calls + their
95
+ * tool-results), so re-issuing cannot re-run a tool — making it safe even when
96
+ * earlier steps had side effects (writes, commits).
97
+ *
98
+ * Qualifies ONLY when: the watchdog fired, earlier steps ran
99
+ * (`chunksThisAttempt > 0`), the CURRENT step produced nothing
100
+ * (`chunksThisStep === 0` → a clean dead socket, no partial text to duplicate),
101
+ * under the retry cap, and not a user cancel. A step that emitted partial text
102
+ * then stalled (`chunksThisStep > 0`) falls through to rescue instead, since
103
+ * re-issuing would duplicate that partial output. Pure (no side effects) so it
104
+ * is unit-testable in isolation from the orchestrator loop.
105
+ */
106
+ export declare function shouldContinueAfterMidLoopStall(s: MidLoopStallState): boolean;
60
107
  /**
61
108
  * Exponential backoff (ms, capped at 4s) before the Nth stall re-prompt
62
109
  * (1-based): 500 → 1000 → 2000 → 4000 → 4000.
@@ -40,6 +40,29 @@ export function shouldRepromptStall(s) {
40
40
  s.assistantTextEmpty &&
41
41
  !s.aborted);
42
42
  }
43
+ /**
44
+ * Decide whether a fired stall watchdog should CONTINUE the turn (append the
45
+ * completed steps' messages, then re-issue streamText to resume from the
46
+ * stalled step) instead of falling through to the partial-answer rescue.
47
+ *
48
+ * This is the mid-loop counterpart to {@link shouldRepromptStall}. The TTFB
49
+ * re-prompt restarts the WHOLE request from the original prompt, so it is gated
50
+ * on `chunksThisAttempt === 0` to avoid re-running tools. Continuation instead
51
+ * preserves all completed steps in history (assistant tool-calls + their
52
+ * tool-results), so re-issuing cannot re-run a tool — making it safe even when
53
+ * earlier steps had side effects (writes, commits).
54
+ *
55
+ * Qualifies ONLY when: the watchdog fired, earlier steps ran
56
+ * (`chunksThisAttempt > 0`), the CURRENT step produced nothing
57
+ * (`chunksThisStep === 0` → a clean dead socket, no partial text to duplicate),
58
+ * under the retry cap, and not a user cancel. A step that emitted partial text
59
+ * then stalled (`chunksThisStep > 0`) falls through to rescue instead, since
60
+ * re-issuing would duplicate that partial output. Pure (no side effects) so it
61
+ * is unit-testable in isolation from the orchestrator loop.
62
+ */
63
+ export function shouldContinueAfterMidLoopStall(s) {
64
+ return (s.stallTriggered && s.chunksThisAttempt > 0 && s.chunksThisStep === 0 && s.retryCount < s.maxRetries && !s.aborted);
65
+ }
43
66
  /**
44
67
  * Exponential backoff (ms, capped at 4s) before the Nth stall re-prompt
45
68
  * (1-based): 500 → 1000 → 2000 → 4000 → 4000.
@@ -1,5 +1,5 @@
1
1
  import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
- import { createStallWatchdog, STALL_ABORT_REASON, shouldRepromptStall, stallRepromptBackoffMs, } from "./stall-watchdog.js";
2
+ import { createStallWatchdog, STALL_ABORT_REASON, shouldContinueAfterMidLoopStall, shouldRepromptStall, stallRepromptBackoffMs, } from "./stall-watchdog.js";
3
3
  describe("createStallWatchdog", () => {
4
4
  beforeEach(() => vi.useFakeTimers());
5
5
  afterEach(() => vi.useRealTimers());
@@ -103,6 +103,41 @@ describe("shouldRepromptStall", () => {
103
103
  expect(shouldRepromptStall(ttfb({ aborted: true }))).toBe(false);
104
104
  });
105
105
  });
106
+ describe("shouldContinueAfterMidLoopStall", () => {
107
+ // A clean mid-loop dead socket: watchdog fired AFTER earlier steps ran
108
+ // (chunksThisAttempt > 0), but the in-flight step produced nothing
109
+ // (chunksThisStep === 0). The ONLY shape that continues. Distinct from the
110
+ // TTFB re-prompt, which requires chunksThisAttempt === 0.
111
+ const midLoop = (over = {}) => ({
112
+ stallTriggered: true,
113
+ chunksThisAttempt: 132, // 66 prior steps' worth of chunks
114
+ chunksThisStep: 0,
115
+ retryCount: 0,
116
+ maxRetries: 1,
117
+ aborted: false,
118
+ ...over,
119
+ });
120
+ it("continues a mid-loop dead-socket stall under the cap", () => {
121
+ expect(shouldContinueAfterMidLoopStall(midLoop())).toBe(true);
122
+ });
123
+ it("does NOT continue when the watchdog never fired", () => {
124
+ expect(shouldContinueAfterMidLoopStall(midLoop({ stallTriggered: false }))).toBe(false);
125
+ });
126
+ it("does NOT continue a time-to-first-byte stall (chunksThisAttempt === 0 → TTFB re-prompt owns it)", () => {
127
+ expect(shouldContinueAfterMidLoopStall(midLoop({ chunksThisAttempt: 0 }))).toBe(false);
128
+ });
129
+ it("does NOT continue when the stalled step already emitted output (would duplicate)", () => {
130
+ expect(shouldContinueAfterMidLoopStall(midLoop({ chunksThisStep: 3 }))).toBe(false);
131
+ });
132
+ it("does NOT continue once the retry cap is reached", () => {
133
+ expect(shouldContinueAfterMidLoopStall(midLoop({ retryCount: 1, maxRetries: 1 }))).toBe(false);
134
+ // maxRetries=0 means the feature is disabled — never continue.
135
+ expect(shouldContinueAfterMidLoopStall(midLoop({ retryCount: 0, maxRetries: 0 }))).toBe(false);
136
+ });
137
+ it("does NOT continue over a genuine user cancel", () => {
138
+ expect(shouldContinueAfterMidLoopStall(midLoop({ aborted: true }))).toBe(false);
139
+ });
140
+ });
106
141
  describe("stallRepromptBackoffMs", () => {
107
142
  it("grows exponentially and caps at 4s", () => {
108
143
  expect(stallRepromptBackoffMs(1)).toBe(500);