muonroi-cli 1.8.3 → 1.8.5
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.
- package/LICENSE +21 -21
- package/README.md +133 -122
- package/dist/packages/agent-harness-core/src/driver.d.ts +27 -1
- package/dist/packages/agent-harness-core/src/driver.js +46 -0
- package/dist/packages/agent-harness-core/src/event-tee.d.ts +48 -0
- package/dist/packages/agent-harness-core/src/event-tee.js +77 -0
- package/dist/packages/agent-harness-core/src/mcp-server.d.ts +11 -0
- package/dist/packages/agent-harness-core/src/mcp-server.js +87 -15
- package/dist/packages/agent-harness-core/src/protocol.d.ts +66 -2
- package/dist/packages/agent-harness-core/src/protocol.js +15 -0
- package/dist/packages/agent-harness-core/src/visual-quality.d.ts +58 -0
- package/dist/packages/agent-harness-core/src/visual-quality.js +141 -0
- package/dist/packages/agent-harness-opentui/src/agent-mode.d.ts +6 -0
- package/dist/packages/agent-harness-opentui/src/agent-mode.js +14 -1
- package/dist/packages/agent-harness-opentui/src/input-bridge.d.ts +2 -10
- package/dist/packages/agent-harness-opentui/src/input-bridge.js +103 -16
- package/dist/packages/agent-harness-opentui/src/install.d.ts +8 -0
- package/dist/packages/agent-harness-opentui/src/install.js +10 -0
- package/dist/packages/agent-harness-opentui/src/semantic.js +12 -10
- package/dist/packages/agent-harness-opentui/src/visual-capture.d.ts +56 -0
- package/dist/packages/agent-harness-opentui/src/visual-capture.js +103 -0
- package/dist/src/agent-harness/mock-model.d.ts +28 -0
- package/dist/src/agent-harness/mock-model.js +63 -1
- package/dist/src/agent-harness/test-spawn.js +31 -0
- package/dist/src/cli/config/screen-providers.js +1 -1
- package/dist/src/cli/cost-forensics.d.ts +10 -0
- package/dist/src/cli/cost-forensics.js +30 -15
- package/dist/src/cli/keys-bundle.d.ts +1 -1
- package/dist/src/cli/keys-bundle.js +1 -1
- package/dist/src/cli/keys.d.ts +2 -2
- package/dist/src/cli/keys.js +19 -81
- package/dist/src/council/clarifier.d.ts +28 -2
- package/dist/src/council/clarifier.js +81 -15
- package/dist/src/council/context.js +49 -15
- package/dist/src/council/debate-checkpoint.d.ts +129 -0
- package/dist/src/council/debate-checkpoint.js +176 -0
- package/dist/src/council/debate-planner.js +51 -3
- package/dist/src/council/debate-summary.d.ts +25 -0
- package/dist/src/council/debate-summary.js +85 -0
- package/dist/src/council/debate.d.ts +169 -2
- package/dist/src/council/debate.js +1210 -134
- package/dist/src/council/index.d.ts +85 -1
- package/dist/src/council/index.js +634 -196
- package/dist/src/council/leader.d.ts +26 -0
- package/dist/src/council/leader.js +150 -9
- package/dist/src/council/llm.d.ts +32 -0
- package/dist/src/council/llm.js +231 -38
- package/dist/src/council/panel-select.d.ts +30 -0
- package/dist/src/council/panel-select.js +72 -0
- package/dist/src/council/planner.js +23 -0
- package/dist/src/council/preflight.d.ts +7 -0
- package/dist/src/council/preflight.js +14 -2
- package/dist/src/council/prompts.d.ts +30 -3
- package/dist/src/council/prompts.js +254 -84
- package/dist/src/council/stance-recall.d.ts +42 -0
- package/dist/src/council/stance-recall.js +57 -0
- package/dist/src/council/strip-think.d.ts +17 -0
- package/dist/src/council/strip-think.js +33 -0
- package/dist/src/council/types.d.ts +128 -0
- package/dist/src/ee/artifact-cache.d.ts +16 -0
- package/dist/src/ee/artifact-cache.js +32 -0
- package/dist/src/ee/auth.d.ts +1 -0
- package/dist/src/ee/auth.js +15 -2
- package/dist/src/ee/bridge.d.ts +10 -0
- package/dist/src/ee/bridge.js +58 -0
- package/dist/src/ee/client.js +81 -18
- package/dist/src/ee/export-transcripts.d.ts +1 -0
- package/dist/src/ee/export-transcripts.js +8 -10
- package/dist/src/ee/extract-session.js +29 -0
- package/dist/src/ee/extract-style.d.ts +58 -0
- package/dist/src/ee/extract-style.js +270 -0
- package/dist/src/ee/recall-ledger.d.ts +9 -0
- package/dist/src/ee/recall-ledger.js +3 -0
- package/dist/src/ee/scope.d.ts +1 -0
- package/dist/src/ee/scope.js +26 -1
- package/dist/src/ee/search.d.ts +7 -0
- package/dist/src/ee/search.js +24 -0
- package/dist/src/ee/transcript-emit.js +2 -0
- package/dist/src/ee/types.d.ts +22 -0
- package/dist/src/ee/who-am-i-brain.d.ts +35 -0
- package/dist/src/ee/who-am-i-brain.js +220 -0
- package/dist/src/ee/who-am-i.d.ts +10 -3
- package/dist/src/ee/who-am-i.js +12 -0
- package/dist/src/ee/workflow-event.d.ts +48 -0
- package/dist/src/ee/workflow-event.js +81 -0
- package/dist/src/flow/compaction/compress.d.ts +3 -3
- package/dist/src/flow/compaction/compress.js +45 -8
- package/dist/src/flow/compaction/extract.d.ts +4 -7
- package/dist/src/flow/compaction/extract.js +50 -10
- package/dist/src/flow/compaction/index.d.ts +13 -1
- package/dist/src/flow/compaction/index.js +70 -3
- package/dist/src/flow/compaction/input-guard.d.ts +24 -0
- package/dist/src/flow/compaction/input-guard.js +43 -0
- package/dist/src/flow/fold-planning.d.ts +36 -0
- package/dist/src/flow/fold-planning.js +83 -0
- package/dist/src/flow/hierarchy.d.ts +146 -0
- package/dist/src/flow/hierarchy.js +427 -0
- package/dist/src/flow/index.d.ts +1 -0
- package/dist/src/flow/index.js +2 -0
- package/dist/src/flow/run-artifacts.d.ts +102 -0
- package/dist/src/flow/run-artifacts.js +208 -0
- package/dist/src/generated/version.d.ts +1 -1
- package/dist/src/generated/version.js +1 -1
- package/dist/src/gsd/assessment-schema.d.ts +44 -0
- package/dist/src/gsd/assessment-schema.js +134 -0
- package/dist/src/gsd/capability-registry.d.ts +45 -0
- package/dist/src/gsd/capability-registry.js +337 -0
- package/dist/src/gsd/complexity-assessor.d.ts +39 -0
- package/dist/src/gsd/complexity-assessor.js +152 -0
- package/dist/src/gsd/config-bridge.d.ts +7 -0
- package/dist/src/gsd/config-bridge.js +114 -0
- package/dist/src/gsd/config-loader.d.ts +27 -0
- package/dist/src/gsd/config-loader.js +50 -0
- package/dist/src/gsd/council-context.d.ts +44 -0
- package/dist/src/gsd/council-context.js +114 -0
- package/dist/src/gsd/ee-closure.d.ts +28 -0
- package/dist/src/gsd/ee-closure.js +49 -0
- package/dist/src/gsd/flags.d.ts +55 -0
- package/dist/src/gsd/flags.js +83 -0
- package/dist/src/gsd/gsd-dispatch.d.ts +58 -0
- package/dist/src/gsd/gsd-dispatch.js +131 -0
- package/dist/src/gsd/gsd-runtime.d.ts +22 -0
- package/dist/src/gsd/gsd-runtime.js +37 -0
- package/dist/src/gsd/host-adapter.d.ts +11 -0
- package/dist/src/gsd/host-adapter.js +29 -0
- package/dist/src/gsd/index.d.ts +24 -1
- package/dist/src/gsd/index.js +27 -0
- package/dist/src/gsd/loop-host-contract.d.ts +21 -0
- package/dist/src/gsd/loop-host-contract.js +39 -0
- package/dist/src/gsd/loop-host.d.ts +69 -0
- package/dist/src/gsd/loop-host.js +245 -0
- package/dist/src/gsd/loop-resolver.d.ts +36 -0
- package/dist/src/gsd/loop-resolver.js +79 -0
- package/dist/src/gsd/model-tier.d.ts +13 -0
- package/dist/src/gsd/model-tier.js +45 -0
- package/dist/src/gsd/mutation-gate.d.ts +16 -0
- package/dist/src/gsd/mutation-gate.js +41 -0
- package/dist/src/gsd/native-roadmap.d.ts +89 -0
- package/dist/src/gsd/native-roadmap.js +343 -0
- package/dist/src/gsd/native-state.d.ts +47 -0
- package/dist/src/gsd/native-state.js +220 -0
- package/dist/src/gsd/paths.d.ts +23 -0
- package/dist/src/gsd/paths.js +66 -0
- package/dist/src/gsd/phase-dag.d.ts +12 -0
- package/dist/src/gsd/phase-dag.js +94 -0
- package/dist/src/gsd/phase-sync.d.ts +42 -0
- package/dist/src/gsd/phase-sync.js +321 -0
- package/dist/src/gsd/pil-gate-context.d.ts +13 -0
- package/dist/src/gsd/pil-gate-context.js +64 -0
- package/dist/src/gsd/pil-gate-critic.d.ts +19 -0
- package/dist/src/gsd/pil-gate-critic.js +74 -0
- package/dist/src/gsd/plan-council-prompts.d.ts +25 -0
- package/dist/src/gsd/plan-council-prompts.js +79 -0
- package/dist/src/gsd/plan-council.d.ts +44 -0
- package/dist/src/gsd/plan-council.js +251 -0
- package/dist/src/gsd/plan-gate-vocabulary.d.ts +40 -0
- package/dist/src/gsd/plan-gate-vocabulary.js +64 -0
- package/dist/src/gsd/product-workspace.d.ts +13 -0
- package/dist/src/gsd/product-workspace.js +124 -0
- package/dist/src/gsd/ship-bridge.d.ts +25 -0
- package/dist/src/gsd/ship-bridge.js +65 -0
- package/dist/src/gsd/state-document.d.ts +40 -0
- package/dist/src/gsd/state-document.js +163 -0
- package/dist/src/gsd/verdict-schema.d.ts +39 -0
- package/dist/src/gsd/verdict-schema.js +144 -0
- package/dist/src/gsd/verify-context.d.ts +22 -0
- package/dist/src/gsd/verify-context.js +27 -0
- package/dist/src/gsd/verify-council-prompts.d.ts +19 -0
- package/dist/src/gsd/verify-council-prompts.js +85 -0
- package/dist/src/gsd/verify-council.d.ts +25 -0
- package/dist/src/gsd/verify-council.js +119 -0
- package/dist/src/gsd/verify-gate-vocabulary.d.ts +25 -0
- package/dist/src/gsd/verify-gate-vocabulary.js +46 -0
- package/dist/src/gsd/workflow-engine.d.ts +60 -0
- package/dist/src/gsd/workflow-engine.js +207 -0
- package/dist/src/gsd/workflow-tools.d.ts +13 -0
- package/dist/src/gsd/workflow-tools.js +277 -0
- package/dist/src/hooks/index.js +1 -1
- package/dist/src/index.js +44 -11
- package/dist/src/maintain/pr-builder.js +23 -13
- package/dist/src/mcp/auto-setup.js +57 -32
- package/dist/src/mcp/client-pool.js +1 -1
- package/dist/src/mcp/ee-tools.js +1 -0
- package/dist/src/mcp/oauth-callback.js +2 -2
- package/dist/src/mcp/research-onboarding.js +8 -7
- package/dist/src/mcp/runtime.js +34 -2
- package/dist/src/mcp/setup-guide-text.d.ts +1 -1
- package/dist/src/mcp/setup-guide-text.js +77 -76
- package/dist/src/models/catalog-client.d.ts +87 -0
- package/dist/src/models/catalog-client.js +105 -38
- package/dist/src/models/catalog.json +528 -265
- package/dist/src/models/registry.d.ts +22 -7
- package/dist/src/models/registry.js +73 -10
- package/dist/src/ops/doctor.js +8 -8
- package/dist/src/orchestrator/auto-commit.js +1 -1
- package/dist/src/orchestrator/batch-turn-runner.js +2 -2
- package/dist/src/orchestrator/cache-prefix.d.ts +67 -0
- package/dist/src/orchestrator/cache-prefix.js +83 -0
- package/dist/src/orchestrator/compact-request.d.ts +32 -0
- package/dist/src/orchestrator/compact-request.js +41 -0
- package/dist/src/orchestrator/compaction.d.ts +10 -0
- package/dist/src/orchestrator/compaction.js +27 -7
- package/dist/src/orchestrator/council-manager.d.ts +12 -3
- package/dist/src/orchestrator/council-manager.js +65 -24
- package/dist/src/orchestrator/cross-turn-dedup.d.ts +6 -0
- package/dist/src/orchestrator/cross-turn-dedup.js +22 -1
- package/dist/src/orchestrator/error-utils.d.ts +29 -0
- package/dist/src/orchestrator/error-utils.js +132 -24
- package/dist/src/orchestrator/grounding-check.js +39 -1
- package/dist/src/orchestrator/message-processor.js +242 -33
- package/dist/src/orchestrator/orchestrator.d.ts +39 -3
- package/dist/src/orchestrator/orchestrator.js +651 -102
- package/dist/src/orchestrator/preprocessor.js +1 -1
- package/dist/src/orchestrator/proactive-compact-detector.d.ts +26 -0
- package/dist/src/orchestrator/proactive-compact-detector.js +36 -0
- package/dist/src/orchestrator/prompts.js +159 -159
- package/dist/src/orchestrator/reactive-delegation.d.ts +39 -0
- package/dist/src/orchestrator/reactive-delegation.js +59 -0
- package/dist/src/orchestrator/retry-classifier.d.ts +2 -1
- package/dist/src/orchestrator/retry-classifier.js +46 -2
- package/dist/src/orchestrator/safety-intercept.d.ts +45 -0
- package/dist/src/orchestrator/safety-intercept.js +55 -0
- package/dist/src/orchestrator/scope-reminder.js +1 -1
- package/dist/src/orchestrator/session-experience.d.ts +2 -1
- package/dist/src/orchestrator/session-experience.js +2 -1
- package/dist/src/orchestrator/should-run-gate.d.ts +5 -0
- package/dist/src/orchestrator/should-run-gate.js +18 -0
- package/dist/src/orchestrator/stall-watchdog.d.ts +24 -3
- package/dist/src/orchestrator/stall-watchdog.js +47 -13
- package/dist/src/orchestrator/stream-runner.js +62 -29
- package/dist/src/orchestrator/sub-agent-cap.d.ts +13 -1
- package/dist/src/orchestrator/sub-agent-cap.js +16 -1
- package/dist/src/orchestrator/sub-agent-model-tier.d.ts +13 -1
- package/dist/src/orchestrator/sub-agent-model-tier.js +16 -2
- package/dist/src/orchestrator/subagent-compactor.d.ts +53 -1
- package/dist/src/orchestrator/subagent-compactor.js +126 -15
- package/dist/src/orchestrator/tool-engine.d.ts +26 -0
- package/dist/src/orchestrator/tool-engine.js +669 -56
- package/dist/src/orchestrator/tool-limit-auto-recover.d.ts +22 -0
- package/dist/src/orchestrator/tool-limit-auto-recover.js +30 -0
- package/dist/src/orchestrator/turn-runner-deps.d.ts +19 -0
- package/dist/src/orchestrator/turn-watchdog.d.ts +37 -0
- package/dist/src/orchestrator/turn-watchdog.js +55 -0
- package/dist/src/pil/agent-operating-contract.d.ts +1 -1
- package/dist/src/pil/agent-operating-contract.js +1 -1
- package/dist/src/pil/cheap-model-playbook.d.ts +1 -1
- package/dist/src/pil/cheap-model-playbook.js +5 -1
- package/dist/src/pil/cheap-model-workbooks.d.ts +1 -1
- package/dist/src/pil/cheap-model-workbooks.js +1 -1
- package/dist/src/pil/discovery-types.d.ts +1 -0
- package/dist/src/pil/discovery.js +16 -11
- package/dist/src/pil/layer1-intent.d.ts +18 -6
- package/dist/src/pil/layer1-intent.js +66 -757
- package/dist/src/pil/layer15-context-scan.js +15 -1
- package/dist/src/pil/layer2_5-ponytail.js +8 -8
- package/dist/src/pil/layer3-ee-injection.js +23 -8
- package/dist/src/pil/layer4-gsd.js +69 -16
- package/dist/src/pil/layer5-context.js +7 -3
- package/dist/src/pil/layer6-output.d.ts +23 -0
- package/dist/src/pil/layer6-output.js +5 -1
- package/dist/src/pil/llm-classify.d.ts +33 -2
- package/dist/src/pil/llm-classify.js +123 -131
- package/dist/src/pil/native-capabilities-workbook.d.ts +1 -1
- package/dist/src/pil/native-capabilities-workbook.js +1 -0
- package/dist/src/pil/pipeline.js +34 -2
- package/dist/src/pil/response-tools.js +5 -3
- package/dist/src/pil/schema.d.ts +1 -0
- package/dist/src/pil/schema.js +2 -0
- package/dist/src/pil/types.d.ts +18 -0
- package/dist/src/playbook/directives.d.ts +4 -0
- package/dist/src/playbook/directives.js +17 -5
- package/dist/src/product-loop/backlog-builder.d.ts +14 -1
- package/dist/src/product-loop/backlog-builder.js +30 -6
- package/dist/src/product-loop/discovery-context-format.js +3 -1
- package/dist/src/product-loop/discovery-ecosystem.js +4 -1
- package/dist/src/product-loop/discovery-interview.js +32 -3
- package/dist/src/product-loop/discovery-schema.js +5 -1
- package/dist/src/product-loop/done-gate.js +3 -3
- package/dist/src/product-loop/ideal-trace.d.ts +7 -0
- package/dist/src/product-loop/ideal-trace.js +64 -0
- package/dist/src/product-loop/index.d.ts +13 -1
- package/dist/src/product-loop/index.js +333 -52
- package/dist/src/product-loop/loop-driver.d.ts +7 -0
- package/dist/src/product-loop/loop-driver.js +327 -116
- package/dist/src/product-loop/phase-plan.d.ts +5 -0
- package/dist/src/product-loop/phase-plan.js +39 -2
- package/dist/src/product-loop/phase-runner.js +9 -1
- package/dist/src/product-loop/progress-snapshot.js +4 -4
- package/dist/src/product-loop/sprint-runner.d.ts +111 -0
- package/dist/src/product-loop/sprint-runner.js +559 -16
- package/dist/src/product-loop/types.d.ts +36 -5
- package/dist/src/providers/adapter.d.ts +1 -1
- package/dist/src/providers/adapter.js +3 -4
- package/dist/src/providers/auth/browser-flow.d.ts +1 -1
- package/dist/src/providers/auth/browser-flow.js +1 -1
- package/dist/src/providers/auth/openai-oauth.js +1 -1
- package/dist/src/providers/auth/registry.js +0 -34
- package/dist/src/providers/auth/token-store.js +4 -1
- package/dist/src/providers/auth/types.d.ts +1 -1
- package/dist/src/providers/auth/types.js +1 -1
- package/dist/src/providers/capabilities.d.ts +24 -5
- package/dist/src/providers/capabilities.js +42 -24
- package/dist/src/providers/endpoints.d.ts +2 -2
- package/dist/src/providers/endpoints.js +11 -10
- package/dist/src/providers/keychain.d.ts +1 -1
- package/dist/src/providers/keychain.js +7 -9
- package/dist/src/providers/mcp-vision-bridge.js +82 -172
- package/dist/src/providers/openai-compatible.js +8 -1
- package/dist/src/providers/pricing.d.ts +2 -2
- package/dist/src/providers/pricing.js +3 -13
- package/dist/src/providers/runtime.d.ts +27 -2
- package/dist/src/providers/runtime.js +78 -15
- package/dist/src/providers/strategies/base.strategy.d.ts +16 -0
- package/dist/src/providers/strategies/base.strategy.js +24 -1
- package/dist/src/providers/strategies/{siliconflow.strategy.d.ts → opencode-go.strategy.d.ts} +3 -3
- package/dist/src/providers/strategies/opencode-go.strategy.js +83 -0
- package/dist/src/providers/strategies/registry.js +4 -4
- package/dist/src/providers/strategies/thinking-mode.d.ts +109 -0
- package/dist/src/providers/strategies/thinking-mode.js +280 -1
- package/dist/src/providers/strategies/zai.strategy.d.ts +14 -0
- package/dist/src/providers/strategies/zai.strategy.js +44 -0
- package/dist/src/providers/types.d.ts +5 -6
- package/dist/src/providers/types.js +2 -2
- package/dist/src/providers/vision-backend.d.ts +47 -0
- package/dist/src/providers/vision-backend.js +258 -0
- package/dist/src/providers/vision-proxy.d.ts +22 -9
- package/dist/src/providers/vision-proxy.js +63 -132
- package/dist/src/providers/wire-debug.js +95 -0
- package/dist/src/reporter/index.js +1 -1
- package/dist/src/router/decide.d.ts +13 -0
- package/dist/src/router/decide.js +138 -36
- package/dist/src/router/peak-hour.d.ts +38 -0
- package/dist/src/router/peak-hour.js +107 -0
- package/dist/src/router/step-router.js +3 -2
- package/dist/src/router/warm.js +4 -5
- package/dist/src/scaffold/bb-ecosystem-apply.js +47 -47
- package/dist/src/scaffold/bb-quality-gate.js +5 -5
- package/dist/src/scaffold/continuation-prompt.d.ts +11 -0
- package/dist/src/scaffold/continuation-prompt.js +86 -60
- package/dist/src/scaffold/init-new.js +453 -453
- package/dist/src/scaffold/point-to-existing.d.ts +21 -0
- package/dist/src/scaffold/point-to-existing.js +25 -0
- package/dist/src/self-qa/agentic-loop.js +22 -22
- package/dist/src/{ui/state → state}/active-run.d.ts +19 -0
- package/dist/src/{ui/state → state}/active-run.js +21 -0
- package/dist/src/{ui/status-bar/store.d.ts → state/status-bar-store.d.ts} +4 -1
- package/dist/src/{ui/status-bar/store.js → state/status-bar-store.js} +12 -9
- package/dist/src/state/turn-trace.d.ts +43 -0
- package/dist/src/state/turn-trace.js +32 -0
- package/dist/src/storage/db.js +2 -1
- package/dist/src/storage/index.d.ts +1 -1
- package/dist/src/storage/index.js +1 -1
- package/dist/src/storage/interaction-log.d.ts +1 -1
- package/dist/src/storage/interaction-log.js +5 -5
- package/dist/src/storage/migrations.js +196 -126
- package/dist/src/storage/session-experience-store.js +4 -4
- package/dist/src/storage/sessions.d.ts +28 -10
- package/dist/src/storage/sessions.js +112 -55
- package/dist/src/storage/transcript-view.js +1 -1
- package/dist/src/storage/transcript.d.ts +51 -0
- package/dist/src/storage/transcript.js +383 -112
- package/dist/src/storage/usage.js +14 -14
- package/dist/src/storage/workspaces.js +12 -12
- package/dist/src/tools/file.d.ts +15 -0
- package/dist/src/tools/file.js +32 -0
- package/dist/src/tools/native-tools.js +5 -0
- package/dist/src/tools/registry.d.ts +3 -0
- package/dist/src/tools/registry.js +460 -22
- package/dist/src/tools/research.d.ts +29 -0
- package/dist/src/tools/research.js +233 -0
- package/dist/src/types/index.d.ts +118 -3
- package/dist/src/ui/app.js +0 -0
- package/dist/src/ui/cards/product-status-card.js +1 -1
- package/dist/src/ui/components/bubble-body-guard.d.ts +27 -0
- package/dist/src/ui/components/bubble-body-guard.js +50 -0
- package/dist/src/ui/components/context-rail.d.ts +26 -0
- package/dist/src/ui/components/context-rail.js +33 -0
- package/dist/src/ui/components/council-conclusion-card.d.ts +73 -0
- package/dist/src/ui/components/council-conclusion-card.js +420 -0
- package/dist/src/ui/components/council-debate-pill.d.ts +36 -0
- package/dist/src/ui/components/council-debate-pill.js +34 -0
- package/dist/src/ui/components/council-info-card.js +2 -2
- package/dist/src/ui/components/council-leader-bubble.d.ts +10 -3
- package/dist/src/ui/components/council-leader-bubble.js +21 -11
- package/dist/src/ui/components/council-message-bubble.d.ts +15 -2
- package/dist/src/ui/components/council-message-bubble.js +16 -15
- package/dist/src/ui/components/council-phase-timeline.d.ts +9 -1
- package/dist/src/ui/components/council-phase-timeline.js +49 -15
- package/dist/src/ui/components/council-placeholder-bubble.d.ts +16 -9
- package/dist/src/ui/components/council-placeholder-bubble.js +32 -29
- package/dist/src/ui/components/council-question-card.js +12 -12
- package/dist/src/ui/components/council-rail-rounds.d.ts +26 -0
- package/dist/src/ui/components/council-rail-rounds.js +57 -0
- package/dist/src/ui/components/council-round-group.d.ts +38 -0
- package/dist/src/ui/components/council-round-group.js +88 -0
- package/dist/src/ui/components/council-status-list.d.ts +3 -1
- package/dist/src/ui/components/council-status-list.js +36 -24
- package/dist/src/ui/components/council-synthesis-banner.d.ts +7 -2
- package/dist/src/ui/components/council-synthesis-banner.js +20 -5
- package/dist/src/ui/components/halt-recovery-card.js +9 -5
- package/dist/src/ui/components/jump-to-latest-pill.d.ts +11 -0
- package/dist/src/ui/components/jump-to-latest-pill.js +14 -0
- package/dist/src/ui/components/prompt-box.js +18 -16
- package/dist/src/ui/components/session-tree-card.d.ts +14 -0
- package/dist/src/ui/components/session-tree-card.js +46 -0
- package/dist/src/ui/components/slash-inline-menu.d.ts +12 -0
- package/dist/src/ui/components/slash-inline-menu.js +26 -5
- package/dist/src/ui/components/task-list-panel.d.ts +14 -1
- package/dist/src/ui/components/task-list-panel.js +22 -2
- package/dist/src/ui/containers/modals-layer.d.ts +2 -1
- package/dist/src/ui/hooks/use-session-picker.d.ts +3 -3
- package/dist/src/ui/mcp-modal.js +2 -4
- package/dist/src/ui/modals/api-key-modal.js +1 -1
- package/dist/src/ui/modals/connect-modal.js +4 -3
- package/dist/src/ui/modals/session-picker-modal.d.ts +2 -2
- package/dist/src/ui/modals/session-picker-modal.js +3 -5
- package/dist/src/ui/picker-providers.d.ts +1 -1
- package/dist/src/ui/picker-providers.js +1 -1
- package/dist/src/ui/primitives/index.d.ts +1 -0
- package/dist/src/ui/primitives/index.js +2 -0
- package/dist/src/ui/primitives/semantic-primitives.d.ts +76 -0
- package/dist/src/ui/primitives/semantic-primitives.js +81 -0
- package/dist/src/ui/slash/compact.js +5 -7
- package/dist/src/ui/slash/cost.js +1 -1
- package/dist/src/ui/slash/council-inspect.js +4 -4
- package/dist/src/ui/slash/council.js +19 -1
- package/dist/src/ui/slash/debug.d.ts +3 -31
- package/dist/src/ui/slash/debug.js +9 -20
- package/dist/src/ui/slash/ideal.d.ts +6 -2
- package/dist/src/ui/slash/ideal.js +97 -7
- package/dist/src/ui/slash/menu-items.d.ts +7 -0
- package/dist/src/ui/slash/menu-items.js +12 -18
- package/dist/src/ui/slash/registry.d.ts +2 -0
- package/dist/src/ui/slash/registry.js +4 -0
- package/dist/src/ui/status-bar/cache-hit.d.ts +6 -0
- package/dist/src/ui/status-bar/cache-hit.js +9 -0
- package/dist/src/ui/status-bar/index.d.ts +1 -1
- package/dist/src/ui/status-bar/index.js +7 -3
- package/dist/src/ui/status-bar/usd-meter.d.ts +5 -4
- package/dist/src/ui/status-bar/usd-meter.js +6 -4
- package/dist/src/ui/theme.d.ts +1 -0
- package/dist/src/ui/theme.js +2 -0
- package/dist/src/ui/types.d.ts +7 -0
- package/dist/src/ui/use-app-logic.js +0 -0
- package/dist/src/ui/utils/format.d.ts +14 -0
- package/dist/src/ui/utils/format.js +23 -3
- package/dist/src/usage/downgrade.js +2 -2
- package/dist/src/usage/product-ledger.js +2 -2
- package/dist/src/utils/clipboard-image.js +23 -23
- package/dist/src/utils/install-manager.js +14 -11
- package/dist/src/utils/logger.js +2 -2
- package/dist/src/utils/permission-mode.js +5 -3
- package/dist/src/utils/redactor.js +1 -1
- package/dist/src/utils/settings.d.ts +153 -5
- package/dist/src/utils/settings.js +233 -29
- package/dist/src/utils/side-question.js +2 -2
- package/dist/src/utils/skills.js +3 -3
- package/dist/src/utils/visible-retry.d.ts +11 -0
- package/dist/src/utils/visible-retry.js +10 -1
- package/dist/src/verify/entrypoint.d.ts +1 -1
- package/dist/src/verify/entrypoint.js +1 -1
- package/dist/src/verify/recipes.d.ts +13 -0
- package/dist/src/verify/recipes.js +15 -0
- package/package.json +135 -132
- package/dist/src/providers/auth/gcloud.d.ts +0 -28
- package/dist/src/providers/auth/gcloud.js +0 -102
- package/dist/src/providers/auth/gemini-oauth.d.ts +0 -82
- package/dist/src/providers/auth/gemini-oauth.js +0 -472
- package/dist/src/providers/gemini.d.ts +0 -11
- package/dist/src/providers/gemini.js +0 -45
- package/dist/src/providers/siliconflow-sse-repair.d.ts +0 -58
- package/dist/src/providers/siliconflow-sse-repair.js +0 -177
- package/dist/src/providers/strategies/google.strategy.d.ts +0 -22
- package/dist/src/providers/strategies/google.strategy.js +0 -174
- package/dist/src/providers/strategies/siliconflow.strategy.js +0 -29
- package/dist/src/ui/containers/chat-feed.d.ts +0 -40
- package/dist/src/ui/containers/chat-feed.js +0 -66
|
@@ -1,11 +1,34 @@
|
|
|
1
1
|
import { APICallError } from "@ai-sdk/provider";
|
|
2
|
+
import { isProviderThinkingDegraded, markProviderThinkingDegrade } from "../providers/strategies/thinking-mode.js";
|
|
2
3
|
import { STALL_ABORT_REASON } from "./stall-watchdog.js";
|
|
4
|
+
/**
|
|
5
|
+
* Detect the generic, spec-undocumented param rejection from the z.ai GLM
|
|
6
|
+
* coding endpoint (HTTP 400 code 1210 "Invalid API parameter") and the
|
|
7
|
+
* opencode-go Console Go proxy (HTTP 400 invalid_request "Upstream request
|
|
8
|
+
* failed"). z.ai does NOT publish the exact constraint (verified 2026-07-02 vs
|
|
9
|
+
* docs.z.ai/api-reference/api-code — 1210 is an intentionally generic bucket),
|
|
10
|
+
* so no client transform can guarantee prevention. These are matched narrowly
|
|
11
|
+
* (400 + specific phrasing) so ordinary 400s stay non-transient.
|
|
12
|
+
*/
|
|
13
|
+
function isProviderParamReject(err) {
|
|
14
|
+
const status = APICallError.isInstance(err)
|
|
15
|
+
? err.statusCode
|
|
16
|
+
: (err?.statusCode ??
|
|
17
|
+
err?.status);
|
|
18
|
+
if (status !== 400)
|
|
19
|
+
return false;
|
|
20
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
21
|
+
const body = APICallError.isInstance(err) && typeof err.responseBody === "string" ? err.responseBody : "";
|
|
22
|
+
const hay = `${message}\n${body}`;
|
|
23
|
+
return /invalid api parameter|upstream request failed|unexpected end of JSON input|code"?\s*:?\s*1210/i.test(hay);
|
|
24
|
+
}
|
|
3
25
|
/**
|
|
4
26
|
* Classifies a stream error as transient (safe to retry) or non-transient.
|
|
5
27
|
*
|
|
6
28
|
* Transient:
|
|
7
29
|
* - Network-level errors: ECONNREFUSED, ETIMEDOUT, ECONNRESET, EAI_AGAIN,
|
|
8
|
-
* "fetch failed", "Unable to connect", "socket hang up", "network"
|
|
30
|
+
* "fetch failed", "Unable to connect", "socket hang up", "network",
|
|
31
|
+
* "socket connection was closed unexpectedly" (Bun stream drop)
|
|
9
32
|
* - HTTP 408 (request timeout), 425 (too early), 429 (rate limit), 5xx
|
|
10
33
|
* - TypeError with "fetch failed" message (browser/bun/node fetch layer)
|
|
11
34
|
* - AbortSignal.timeout firing (err.name === "TimeoutError")
|
|
@@ -38,6 +61,19 @@ export function classifyStreamError(err, depth = 0) {
|
|
|
38
61
|
if (e.name === "TimeoutError") {
|
|
39
62
|
return { transient: true, reason: "timeout-error" };
|
|
40
63
|
}
|
|
64
|
+
// z.ai / opencode-go generic param reject (1210 / "Upstream request failed"):
|
|
65
|
+
// give EXACTLY ONE retry with a degraded-but-valid body. The first sighting
|
|
66
|
+
// latches thinking OFF (markProviderThinkingDegrade) so the rebuilt request
|
|
67
|
+
// (factory re-invoked by withStreamRetry) sends the validator-safe shape;
|
|
68
|
+
// parallel tool_calls are already split by the transform. If it STILL rejects
|
|
69
|
+
// after we've degraded, stop retrying — the cause is beyond our client fix.
|
|
70
|
+
if (isProviderParamReject(err)) {
|
|
71
|
+
if (isProviderThinkingDegraded()) {
|
|
72
|
+
return { transient: false, reason: "provider-param-reject-after-degrade" };
|
|
73
|
+
}
|
|
74
|
+
markProviderThinkingDegrade();
|
|
75
|
+
return { transient: true, reason: "provider-param-reject-degrade-retry" };
|
|
76
|
+
}
|
|
41
77
|
// AI SDK APICallError with statusCode
|
|
42
78
|
if (APICallError.isInstance(err)) {
|
|
43
79
|
const status = err.statusCode;
|
|
@@ -61,6 +97,10 @@ export function classifyStreamError(err, depth = 0) {
|
|
|
61
97
|
}
|
|
62
98
|
}
|
|
63
99
|
const message = typeof e.message === "string" ? e.message : "";
|
|
100
|
+
// Rate limits (including wrapped messages from proxies like "Console Go")
|
|
101
|
+
if (/rate limit|rate-limited|429|too many requests/i.test(message)) {
|
|
102
|
+
return { transient: true, reason: "rate-limit-message" };
|
|
103
|
+
}
|
|
64
104
|
// Malformed function/tool name errors — non-transient (own handler elsewhere)
|
|
65
105
|
if (/invalid.*function.*name|function.*name.*invalid|malformed.*tool|NoSuchTool/i.test(message)) {
|
|
66
106
|
return { transient: false, reason: "malformed-tool-name" };
|
|
@@ -86,7 +126,11 @@ function isTransientStatusCode(code) {
|
|
|
86
126
|
return code === 408 || code === 425 || code === 429 || (code >= 500 && code <= 599);
|
|
87
127
|
}
|
|
88
128
|
function isTransientMessage(message) {
|
|
89
|
-
|
|
129
|
+
// "socket connection was closed unexpectedly" is Bun's fetch phrasing when a
|
|
130
|
+
// provider drops a streaming response mid-flight — a transient network drop,
|
|
131
|
+
// not a client error. Left unclassified it escapes as an unhandledRejection,
|
|
132
|
+
// which OpenTUI's handleError turns into an un-dismissable console overlay.
|
|
133
|
+
return /ECONNREFUSED|ETIMEDOUT|ECONNRESET|EAI_AGAIN|fetch failed|Unable to connect|network|socket hang up|socket connection was closed|socket.*closed unexpectedly/i.test(message);
|
|
90
134
|
}
|
|
91
135
|
/**
|
|
92
136
|
* Parse Retry-After header value to milliseconds.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* safety-intercept.ts — pure decision helpers for the tool-engine safety-block
|
|
3
|
+
* interceptor (tool-engine.ts). Extracted so the block-parse and the
|
|
4
|
+
* permission-mode policy are unit-testable in isolation from the giant
|
|
5
|
+
* executeToolEngine generator.
|
|
6
|
+
*
|
|
7
|
+
* Flow context: bash.execute / registry precheck emit a tool result whose text
|
|
8
|
+
* starts with `BLOCKED (<kind>): <reason>`. The tool-engine joins output+error,
|
|
9
|
+
* parses it here, and decides whether to auto-block, auto-allow (yolo), or show
|
|
10
|
+
* the safety-override askcard via deps.askSafetyOverride.
|
|
11
|
+
*/
|
|
12
|
+
import type { PermissionMode } from "../utils/permission-mode.js";
|
|
13
|
+
import type { SafetyBlockKind } from "./safety-askcard.js";
|
|
14
|
+
export interface ParsedSafetyBlock {
|
|
15
|
+
kind: SafetyBlockKind;
|
|
16
|
+
reason: string;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Parse a joined tool-result text into a safety block, or null when the text
|
|
20
|
+
* is not a BLOCKED marker.
|
|
21
|
+
*
|
|
22
|
+
* Tolerant of leading whitespace: when bash.execute returns `{error: "BLOCKED
|
|
23
|
+
* (...)"}` with no `output`, the joined text is the raw marker — but a result
|
|
24
|
+
* shape that carries an empty-string `output` produces a leading "\n" before
|
|
25
|
+
* the marker, which an anchored `/^BLOCKED/` would miss and silently drop the
|
|
26
|
+
* askcard (hard-stop with no prompt). Stripping leading whitespace first makes
|
|
27
|
+
* the parse robust to both shapes while still requiring the marker at the head
|
|
28
|
+
* of the (trimmed) text — so a command whose normal output merely mentions
|
|
29
|
+
* "BLOCKED (...)" mid-stream is not mistaken for a real block.
|
|
30
|
+
*/
|
|
31
|
+
export declare function parseSafetyBlock(outputText: string): ParsedSafetyBlock | null;
|
|
32
|
+
/**
|
|
33
|
+
* Whether a parsed block should be auto-allowed (allow-once) without showing
|
|
34
|
+
* the askcard, given the active permission mode.
|
|
35
|
+
*
|
|
36
|
+
* Policy (confirmed with the user):
|
|
37
|
+
* - `yolo` auto-allows lower-severity blocks (`git-safety`, `dangerous`) so
|
|
38
|
+
* the "don't ask me" mode actually stops asking for routine guardrails.
|
|
39
|
+
* - `catastrophic` ALWAYS shows the askcard, even in yolo — an irreversible
|
|
40
|
+
* destroyer (rm -rf /dev, mkfs, sudo …) must never run unattended.
|
|
41
|
+
* - `empty-bash` is handled separately (auto-block, no card) and never
|
|
42
|
+
* reaches this policy.
|
|
43
|
+
* - `safe` / `auto-edit` always show the card for any real block.
|
|
44
|
+
*/
|
|
45
|
+
export declare function shouldAutoAllowYolo(kind: SafetyBlockKind, mode: PermissionMode): boolean;
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* safety-intercept.ts — pure decision helpers for the tool-engine safety-block
|
|
3
|
+
* interceptor (tool-engine.ts). Extracted so the block-parse and the
|
|
4
|
+
* permission-mode policy are unit-testable in isolation from the giant
|
|
5
|
+
* executeToolEngine generator.
|
|
6
|
+
*
|
|
7
|
+
* Flow context: bash.execute / registry precheck emit a tool result whose text
|
|
8
|
+
* starts with `BLOCKED (<kind>): <reason>`. The tool-engine joins output+error,
|
|
9
|
+
* parses it here, and decides whether to auto-block, auto-allow (yolo), or show
|
|
10
|
+
* the safety-override askcard via deps.askSafetyOverride.
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* Parse a joined tool-result text into a safety block, or null when the text
|
|
14
|
+
* is not a BLOCKED marker.
|
|
15
|
+
*
|
|
16
|
+
* Tolerant of leading whitespace: when bash.execute returns `{error: "BLOCKED
|
|
17
|
+
* (...)"}` with no `output`, the joined text is the raw marker — but a result
|
|
18
|
+
* shape that carries an empty-string `output` produces a leading "\n" before
|
|
19
|
+
* the marker, which an anchored `/^BLOCKED/` would miss and silently drop the
|
|
20
|
+
* askcard (hard-stop with no prompt). Stripping leading whitespace first makes
|
|
21
|
+
* the parse robust to both shapes while still requiring the marker at the head
|
|
22
|
+
* of the (trimmed) text — so a command whose normal output merely mentions
|
|
23
|
+
* "BLOCKED (...)" mid-stream is not mistaken for a real block.
|
|
24
|
+
*/
|
|
25
|
+
export function parseSafetyBlock(outputText) {
|
|
26
|
+
if (typeof outputText !== "string")
|
|
27
|
+
return null;
|
|
28
|
+
const match = outputText.replace(/^\s+/, "").match(/^BLOCKED \(([^)]+)\):\s*(.*)/);
|
|
29
|
+
if (!match)
|
|
30
|
+
return null;
|
|
31
|
+
return { kind: match[1], reason: match[2] ?? "" };
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Whether a parsed block should be auto-allowed (allow-once) without showing
|
|
35
|
+
* the askcard, given the active permission mode.
|
|
36
|
+
*
|
|
37
|
+
* Policy (confirmed with the user):
|
|
38
|
+
* - `yolo` auto-allows lower-severity blocks (`git-safety`, `dangerous`) so
|
|
39
|
+
* the "don't ask me" mode actually stops asking for routine guardrails.
|
|
40
|
+
* - `catastrophic` ALWAYS shows the askcard, even in yolo — an irreversible
|
|
41
|
+
* destroyer (rm -rf /dev, mkfs, sudo …) must never run unattended.
|
|
42
|
+
* - `empty-bash` is handled separately (auto-block, no card) and never
|
|
43
|
+
* reaches this policy.
|
|
44
|
+
* - `safe` / `auto-edit` always show the card for any real block.
|
|
45
|
+
*/
|
|
46
|
+
export function shouldAutoAllowYolo(kind, mode) {
|
|
47
|
+
if (mode !== "yolo")
|
|
48
|
+
return false;
|
|
49
|
+
if (kind === "catastrophic")
|
|
50
|
+
return false;
|
|
51
|
+
if (kind === "empty-bash")
|
|
52
|
+
return false;
|
|
53
|
+
return true;
|
|
54
|
+
}
|
|
55
|
+
//# sourceMappingURL=safety-intercept.js.map
|
|
@@ -229,7 +229,7 @@ export function buildCheckpointReminder(iteration, hasEECheckpoint) {
|
|
|
229
229
|
* compared `stripped.length` (a message count, ~tens) against a char-scaled
|
|
230
230
|
* threshold (~156000), so the warning could never fire — session 2b7a10219499.
|
|
231
231
|
*/
|
|
232
|
-
export function shouldPreWarnCompaction(promptChars, thresholdChars, ratio = 0.
|
|
232
|
+
export function shouldPreWarnCompaction(promptChars, thresholdChars, ratio = 0.6) {
|
|
233
233
|
if (thresholdChars <= 0 || promptChars <= 0)
|
|
234
234
|
return false;
|
|
235
235
|
return promptChars >= Math.floor(thresholdChars * ratio);
|
|
@@ -27,6 +27,7 @@ export interface ElisionRecord {
|
|
|
27
27
|
chars: number;
|
|
28
28
|
/** prepareStep step number at which it was elided. */
|
|
29
29
|
step: number;
|
|
30
|
+
summary?: string;
|
|
30
31
|
}
|
|
31
32
|
export interface SessionExperience {
|
|
32
33
|
compactions: number;
|
|
@@ -40,7 +41,7 @@ export interface SessionExperience {
|
|
|
40
41
|
/** Record that B3/B4 compaction actually elided something at `step`. */
|
|
41
42
|
export declare function recordCompaction(step: number): void;
|
|
42
43
|
/** Record a single tool output the compactor rewrote into a stub. */
|
|
43
|
-
export declare function recordElision(toolCallId: string, toolName: string, chars: number, step: number): void;
|
|
44
|
+
export declare function recordElision(toolCallId: string, toolName: string, chars: number, step: number, summary?: string): void;
|
|
44
45
|
/**
|
|
45
46
|
* Record an ee_query rehydrate of an elided artifact, tagged by where it came
|
|
46
47
|
* from. `unavailable` means the agent asked for an artifact that was neither in
|
|
@@ -38,7 +38,7 @@ export function recordCompaction(step) {
|
|
|
38
38
|
state.lastCompactionStep = Number.isFinite(step) ? step : state.lastCompactionStep;
|
|
39
39
|
}
|
|
40
40
|
/** Record a single tool output the compactor rewrote into a stub. */
|
|
41
|
-
export function recordElision(toolCallId, toolName, chars, step) {
|
|
41
|
+
export function recordElision(toolCallId, toolName, chars, step, summary) {
|
|
42
42
|
if (!toolCallId)
|
|
43
43
|
return;
|
|
44
44
|
state.elisions.push({
|
|
@@ -46,6 +46,7 @@ export function recordElision(toolCallId, toolName, chars, step) {
|
|
|
46
46
|
toolName: toolName || "",
|
|
47
47
|
chars: Number.isFinite(chars) && chars > 0 ? Math.floor(chars) : 0,
|
|
48
48
|
step: Number.isFinite(step) ? step : 0,
|
|
49
|
+
summary,
|
|
49
50
|
});
|
|
50
51
|
// FIFO trim — keep the most recent MAX_ELISIONS.
|
|
51
52
|
if (state.elisions.length > MAX_ELISIONS) {
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Keep the gate off pure chitchat, but ON for a resumed heavy task the classifier
|
|
2
|
+
// mislabels as chitchat (continuation phrases — preprocessor.ts:118-134). Reading
|
|
3
|
+
// STATE.md phase is the resume signal (execute phase = an active run).
|
|
4
|
+
export function shouldRunGate(pilCtx, readPhase) {
|
|
5
|
+
if (pilCtx.intentKind !== "chitchat")
|
|
6
|
+
return true;
|
|
7
|
+
if (pilCtx.resumeDigest || pilCtx.activeRunId)
|
|
8
|
+
return true;
|
|
9
|
+
try {
|
|
10
|
+
return readPhase() === "execute";
|
|
11
|
+
}
|
|
12
|
+
catch (err) {
|
|
13
|
+
// Missing/corrupt .planning is the normal "no active run" case, not an error — treat as no run.
|
|
14
|
+
console.error(`[pil-gate] shouldRunGate readPhase failed (treating as no active run): ${err.message}`);
|
|
15
|
+
return false;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=should-run-gate.js.map
|
|
@@ -21,13 +21,34 @@
|
|
|
21
21
|
export interface StallWatchdog {
|
|
22
22
|
/** Combine this into the streamText abortSignal. */
|
|
23
23
|
readonly signal: AbortSignal;
|
|
24
|
-
/** Call on every received stream chunk to reset the stall timer. */
|
|
24
|
+
/** Call on every received stream chunk to reset the any-activity stall timer. */
|
|
25
25
|
pet(): void;
|
|
26
|
-
/**
|
|
26
|
+
/**
|
|
27
|
+
* Call ONLY on real forward-progress chunks (a text-delta or a tool-call) to
|
|
28
|
+
* reset the no-forward-progress timer. No-op when the watchdog was created
|
|
29
|
+
* without a progressTimeoutMs. This is what makes the guard catch a reasoning
|
|
30
|
+
* model stuck in an endless chain-of-thought: `pet()` (called on EVERY chunk,
|
|
31
|
+
* including reasoning-delta) keeps the any-activity timer alive, but the
|
|
32
|
+
* progress timer only survives if actual output flows.
|
|
33
|
+
*/
|
|
34
|
+
petProgress(): void;
|
|
35
|
+
/** Stop the timers (call when the stream completes or errors). Idempotent. */
|
|
27
36
|
dispose(): void;
|
|
28
37
|
/** True iff the watchdog aborted the stream because of a stall. */
|
|
29
38
|
fired(): boolean;
|
|
30
39
|
}
|
|
40
|
+
/** Options for the second (no-forward-progress) timer of a stall watchdog. */
|
|
41
|
+
export interface StallWatchdogProgressOpts {
|
|
42
|
+
/**
|
|
43
|
+
* If > 0, arm a SECOND timer that is reset only by petProgress() (real
|
|
44
|
+
* output), not by pet() (any chunk). Aborts the same signal when no forward
|
|
45
|
+
* progress happens for this long — catching runaway reasoning that keeps the
|
|
46
|
+
* any-activity timer alive with reasoning-delta chunks. <= 0 disables it.
|
|
47
|
+
*/
|
|
48
|
+
progressTimeoutMs: number;
|
|
49
|
+
/** Called when the no-forward-progress timer fires (before abort). */
|
|
50
|
+
onProgressFire?: () => void;
|
|
51
|
+
}
|
|
31
52
|
export declare const STALL_ABORT_REASON = "provider-stall";
|
|
32
53
|
/** User-facing message surfaced when the stall watchdog fires. */
|
|
33
54
|
export declare const STALL_ERROR_MESSAGE: string;
|
|
@@ -109,4 +130,4 @@ export declare function shouldContinueAfterMidLoopStall(s: MidLoopStallState): b
|
|
|
109
130
|
* (1-based): 500 → 1000 → 2000 → 4000 → 4000.
|
|
110
131
|
*/
|
|
111
132
|
export declare function stallRepromptBackoffMs(attempt: number): number;
|
|
112
|
-
export declare function createStallWatchdog(timeoutMs: number, onFire?: () => void): StallWatchdog;
|
|
133
|
+
export declare function createStallWatchdog(timeoutMs: number, onFire?: () => void, progressOpts?: StallWatchdogProgressOpts): StallWatchdog;
|
|
@@ -70,36 +70,63 @@ export function shouldContinueAfterMidLoopStall(s) {
|
|
|
70
70
|
export function stallRepromptBackoffMs(attempt) {
|
|
71
71
|
return Math.min(500 * 2 ** (Math.max(1, attempt) - 1), 4_000);
|
|
72
72
|
}
|
|
73
|
-
export function createStallWatchdog(timeoutMs, onFire) {
|
|
73
|
+
export function createStallWatchdog(timeoutMs, onFire, progressOpts) {
|
|
74
74
|
const controller = new AbortController();
|
|
75
75
|
let firedFlag = false;
|
|
76
76
|
let timer = null;
|
|
77
77
|
const enabled = Number.isFinite(timeoutMs) && timeoutMs > 0;
|
|
78
|
+
const progressTimeoutMs = progressOpts?.progressTimeoutMs ?? 0;
|
|
79
|
+
const progressEnabled = Number.isFinite(progressTimeoutMs) && progressTimeoutMs > 0;
|
|
80
|
+
let progressTimer = null;
|
|
81
|
+
const fire = (onSpecificFire) => {
|
|
82
|
+
if (firedFlag)
|
|
83
|
+
return;
|
|
84
|
+
firedFlag = true;
|
|
85
|
+
// Stop the OTHER timer so it can't also fire after the abort (e.g. the
|
|
86
|
+
// any-activity stall timer that was armed just before the progress timer
|
|
87
|
+
// tripped — otherwise both onFire callbacks would run).
|
|
88
|
+
clearBoth();
|
|
89
|
+
// DOMException(TimeoutError) mirrors AbortSignal.timeout() semantics so
|
|
90
|
+
// downstream isAbortError-style checks treat it as an abort.
|
|
91
|
+
controller.abort(new DOMException(STALL_ABORT_REASON, "TimeoutError"));
|
|
92
|
+
try {
|
|
93
|
+
onSpecificFire?.();
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
/* callback must not break the watchdog */
|
|
97
|
+
}
|
|
98
|
+
};
|
|
78
99
|
const arm = () => {
|
|
79
100
|
if (!enabled)
|
|
80
101
|
return;
|
|
81
|
-
timer = setTimeout(() =>
|
|
82
|
-
firedFlag = true;
|
|
83
|
-
// DOMException(TimeoutError) mirrors AbortSignal.timeout() semantics so
|
|
84
|
-
// downstream isAbortError-style checks treat it as an abort.
|
|
85
|
-
controller.abort(new DOMException(STALL_ABORT_REASON, "TimeoutError"));
|
|
86
|
-
try {
|
|
87
|
-
onFire?.();
|
|
88
|
-
}
|
|
89
|
-
catch {
|
|
90
|
-
/* callback must not break the watchdog */
|
|
91
|
-
}
|
|
92
|
-
}, timeoutMs);
|
|
102
|
+
timer = setTimeout(() => fire(onFire), timeoutMs);
|
|
93
103
|
// Don't keep the event loop alive solely for the watchdog (Node).
|
|
94
104
|
timer.unref?.();
|
|
95
105
|
};
|
|
106
|
+
const armProgress = () => {
|
|
107
|
+
if (!progressEnabled)
|
|
108
|
+
return;
|
|
109
|
+
progressTimer = setTimeout(() => fire(progressOpts?.onProgressFire), progressTimeoutMs);
|
|
110
|
+
progressTimer.unref?.();
|
|
111
|
+
};
|
|
96
112
|
const clear = () => {
|
|
97
113
|
if (timer) {
|
|
98
114
|
clearTimeout(timer);
|
|
99
115
|
timer = null;
|
|
100
116
|
}
|
|
101
117
|
};
|
|
118
|
+
const clearProgress = () => {
|
|
119
|
+
if (progressTimer) {
|
|
120
|
+
clearTimeout(progressTimer);
|
|
121
|
+
progressTimer = null;
|
|
122
|
+
}
|
|
123
|
+
};
|
|
124
|
+
function clearBoth() {
|
|
125
|
+
clear();
|
|
126
|
+
clearProgress();
|
|
127
|
+
}
|
|
102
128
|
arm();
|
|
129
|
+
armProgress();
|
|
103
130
|
return {
|
|
104
131
|
signal: controller.signal,
|
|
105
132
|
pet() {
|
|
@@ -108,8 +135,15 @@ export function createStallWatchdog(timeoutMs, onFire) {
|
|
|
108
135
|
clear();
|
|
109
136
|
arm();
|
|
110
137
|
},
|
|
138
|
+
petProgress() {
|
|
139
|
+
if (!progressEnabled || firedFlag)
|
|
140
|
+
return;
|
|
141
|
+
clearProgress();
|
|
142
|
+
armProgress();
|
|
143
|
+
},
|
|
111
144
|
dispose() {
|
|
112
145
|
clear();
|
|
146
|
+
clearProgress();
|
|
113
147
|
},
|
|
114
148
|
fired() {
|
|
115
149
|
return firedFlag;
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
// - C1 (DeepSeek cache split read) — onFinish normalization
|
|
26
26
|
// - C3 (cross-turn dedup wrap) — wrapToolSetWithDedup
|
|
27
27
|
// - F1 (sub-agent cumulative cap) — wrapToolSetWithCap
|
|
28
|
-
// -
|
|
28
|
+
// - reasoning-strip (provider quirk) — taskCaps.sanitizeHistory
|
|
29
29
|
import { stepCountIs, streamText } from "ai";
|
|
30
30
|
import { recordArtifact } from "../ee/artifact-cache.js";
|
|
31
31
|
import { getDefaultEEClient } from "../ee/intercept.js";
|
|
@@ -36,14 +36,14 @@ import { injectCheapModelWorkbook, shouldInjectCheapModelWorkbook, subagentTaskT
|
|
|
36
36
|
import { getProviderCapabilities } from "../providers/capabilities.js";
|
|
37
37
|
import { getVisionGuidanceForTextOnly } from "../providers/mcp-vision-bridge.js";
|
|
38
38
|
import { captureToolSchemas } from "../providers/patch-zod-schema.js";
|
|
39
|
-
import { buildTurnProviderOptions, requireRuntimeProvider, resolveModelRuntime, shouldDropParam, } from "../providers/runtime.js";
|
|
39
|
+
import { buildTurnProviderOptions, requireRuntimeProvider, resolveModelRuntime, resolveTemperatureParam, shouldDropParam, } from "../providers/runtime.js";
|
|
40
40
|
import { needsVisionProxy } from "../providers/vision-proxy.js";
|
|
41
41
|
import { wireDebug } from "../providers/wire-debug.js";
|
|
42
|
+
import { statusBarStore } from "../state/status-bar-store.js";
|
|
42
43
|
import { BashTool } from "../tools/bash.js";
|
|
43
44
|
import { createBuiltinTools } from "../tools/registry.js";
|
|
44
|
-
import { statusBarStore } from "../ui/status-bar/store.js";
|
|
45
45
|
import { openUrl } from "../utils/open-url.js";
|
|
46
|
-
import { getCurrentShellSettings, getProviderStallTimeoutMs, getSubAgentBudgetChars, getSubAgentCompactKeepLast, getSubAgentCompactThresholdChars, loadMcpServers, loadValidSubAgents, } from "../utils/settings.js";
|
|
46
|
+
import { getCurrentShellSettings, getProviderProgressTimeoutMs, getProviderStallTimeoutMs, getSubAgentBudgetChars, getSubAgentCompactKeepLast, getSubAgentCompactThresholdChars, loadMcpServers, loadValidSubAgents, } from "../utils/settings.js";
|
|
47
47
|
import { resolveShell } from "../utils/shell.js";
|
|
48
48
|
import { prepareVerifySandbox } from "../verify/entrypoint.js";
|
|
49
49
|
import { asNumber } from "./batch-utils.js";
|
|
@@ -58,7 +58,7 @@ import { attachReminderToMessages, buildScopeReminder, cadenceForSize, shouldInj
|
|
|
58
58
|
import { recordCompaction, recordElision } from "./session-experience.js";
|
|
59
59
|
import { createStallWatchdog, STALL_ERROR_MESSAGE } from "./stall-watchdog.js";
|
|
60
60
|
import { wrapToolSetWithCap } from "./sub-agent-cap.js";
|
|
61
|
-
import {
|
|
61
|
+
import { applyAnthropicPromptCaching, compactSubAgentMessages } from "./subagent-compactor.js";
|
|
62
62
|
import { combineAbortSignals, firstLine, formatSubagentActivity } from "./tool-utils.js";
|
|
63
63
|
/**
|
|
64
64
|
* StreamRunner — extracted sub-agent stream lifecycle.
|
|
@@ -137,23 +137,36 @@ export class StreamRunner {
|
|
|
137
137
|
});
|
|
138
138
|
// Resolve child model early so we can pass modelId to createBuiltinTools
|
|
139
139
|
// (needed for vision-proxy tools: analyze_image / ask_vision_proxy).
|
|
140
|
-
const childModelId = normalizeModelId(
|
|
141
|
-
?
|
|
142
|
-
:
|
|
143
|
-
?
|
|
144
|
-
:
|
|
145
|
-
?
|
|
146
|
-
:
|
|
140
|
+
const childModelId = normalizeModelId(request.modelId
|
|
141
|
+
? request.modelId
|
|
142
|
+
: isVision
|
|
143
|
+
? VISION_MODEL
|
|
144
|
+
: isComputer
|
|
145
|
+
? COMPUTER_MODEL
|
|
146
|
+
: custom
|
|
147
|
+
? custom.model
|
|
148
|
+
: this.deps.resolveModelForTask(isExplore ? "explore" : isVerify || isVerifyDetect || isVerifyManifest ? "verify" : "general"));
|
|
147
149
|
// Mirror the file-local `createTools` wrapper from orchestrator.ts —
|
|
148
150
|
// pass modelId so registry can inject analyze_image/ask_vision_proxy for
|
|
149
151
|
// text-only child models (needsVisionProxy).
|
|
150
|
-
const childBaseToolsRaw = createBuiltinTools(childBash, childMode, {
|
|
152
|
+
const childBaseToolsRaw = createBuiltinTools(childBash, childMode, {
|
|
153
|
+
modelId: childModelId,
|
|
154
|
+
sessionId: this.deps.getSessionId(),
|
|
155
|
+
});
|
|
151
156
|
// Wrap with the cumulative cap so the sub-agent's tool loop cannot
|
|
152
157
|
// accumulate unbounded tool_result tokens. See sub-agent-cap.ts for the
|
|
153
158
|
// tiered compression schedule. The cap is per-invocation; each sub-agent
|
|
154
159
|
// gets a fresh budget.
|
|
155
160
|
const subAgentCapBudget = getSubAgentBudgetChars();
|
|
156
|
-
const subAgentCap = wrapToolSetWithCap(childBaseToolsRaw, {
|
|
161
|
+
const subAgentCap = wrapToolSetWithCap(childBaseToolsRaw, {
|
|
162
|
+
maxCumulativeChars: subAgentCapBudget,
|
|
163
|
+
// Opt-out toggle (default ON). The cap dedups identical tool outputs by
|
|
164
|
+
// content hash; set MUONROI_SUBAGENT_CAP_DEDUP=0 to disable. Used by the
|
|
165
|
+
// B3 compaction E2E, where the harness re-processes each tool result and
|
|
166
|
+
// the self-dedup would otherwise collapse distinct reads into pointer
|
|
167
|
+
// stubs before cumulative history can grow enough to trigger compaction.
|
|
168
|
+
dedupRepeatOutputs: process.env.MUONROI_SUBAGENT_CAP_DEDUP !== "0",
|
|
169
|
+
});
|
|
157
170
|
// Phase C3: layer cross-turn dedup ON TOP of the per-invocation cap. The
|
|
158
171
|
// cap sees raw output (for accurate cumulative accounting); dedup sees
|
|
159
172
|
// the already-trimmed output that will actually reach the model.
|
|
@@ -255,7 +268,7 @@ export class StreamRunner {
|
|
|
255
268
|
void signal;
|
|
256
269
|
const childMessages = [{ role: "user", content: childPrompt }];
|
|
257
270
|
// The main agent manages its sub-agents, so don't apply an arbitrary hard limit.
|
|
258
|
-
const maxSteps = request.maxToolRounds ??
|
|
271
|
+
const maxSteps = request.maxToolRounds ?? this.deps.getMaxToolRounds() * 2;
|
|
259
272
|
// F1 parity — derive per-turn providerOptions so the sub-agent OpenAI calls
|
|
260
273
|
// carry a stable session-derived promptCacheKey (every tool round routes to
|
|
261
274
|
// the same cache node, keeping the unchanging prefix cached). The top-level
|
|
@@ -328,7 +341,6 @@ export class StreamRunner {
|
|
|
328
341
|
// already strips it via `dropParam`, the sub-agent path was missing
|
|
329
342
|
// the same guard, causing G1: "Task failed: No output generated.").
|
|
330
343
|
const childDropMaxOutput = shouldDropParam(childRuntime, "maxOutputTokens");
|
|
331
|
-
const childDropTemperature = shouldDropParam(childRuntime, "temperature");
|
|
332
344
|
// Phase B3: compact older tool_results out of the running message history
|
|
333
345
|
// before each AI SDK step. First step (stepNumber === 0) has no history
|
|
334
346
|
// worth compacting; later steps are where cumulative input balloons.
|
|
@@ -346,8 +358,8 @@ export class StreamRunner {
|
|
|
346
358
|
providerOptions: childProviderOptions,
|
|
347
359
|
});
|
|
348
360
|
}
|
|
349
|
-
//
|
|
350
|
-
// `reasoning` parts (HTTP 400 code 20015).
|
|
361
|
+
// Some DeepSeek thinking-mode endpoints reject assistant history with
|
|
362
|
+
// `reasoning` parts (HTTP 400 code 20015). A provider capability
|
|
351
363
|
// override strips them; every other provider's capability is identity.
|
|
352
364
|
const subMessagesForCall = taskCaps.sanitizeHistory(childMessages);
|
|
353
365
|
// Phase 4 Plan 04 (4B) — mirror top-level scope-ceiling integration.
|
|
@@ -377,8 +389,26 @@ export class StreamRunner {
|
|
|
377
389
|
// and is re-armed by stall.pet() on every chunk. Cheap models (which run
|
|
378
390
|
// mostly as sub-agents via SAMR) hit this most.
|
|
379
391
|
let stallTriggered = false;
|
|
392
|
+
// Second timer (progressTimeoutMs) is the no-forward-progress guard: the
|
|
393
|
+
// stall timer re-arms on EVERY chunk — including a reasoning model's
|
|
394
|
+
// `reasoning-delta` chunks — so a sub-agent stuck in an endless
|
|
395
|
+
// chain-of-thought keeps petting it and it never fires (observed live
|
|
396
|
+
// 2026-07-10: a deepseek-v4-flash sprint sub-agent churned reasoning for
|
|
397
|
+
// 30+ min, 1.4M input tokens, ZERO text/tool output; the 2-min stall
|
|
398
|
+
// watchdog never tripped). The progress timer is reset ONLY by
|
|
399
|
+
// stall.petProgress() on real output (text-delta / tool-call), so a runaway
|
|
400
|
+
// reasoning loop is aborted while a legitimately long reasoning burst that
|
|
401
|
+
// DOES eventually emit output survives. Both timers abort the same signal
|
|
402
|
+
// and set stallTriggered, so the existing surface-and-return path handles it.
|
|
380
403
|
const stall = createStallWatchdog(getProviderStallTimeoutMs(), () => {
|
|
381
404
|
stallTriggered = true;
|
|
405
|
+
}, {
|
|
406
|
+
progressTimeoutMs: getProviderProgressTimeoutMs(),
|
|
407
|
+
onProgressFire: () => {
|
|
408
|
+
stallTriggered = true;
|
|
409
|
+
console.error(`[stream-runner] sub-agent aborted: no text/tool output for ${getProviderProgressTimeoutMs()}ms ` +
|
|
410
|
+
`(runaway reasoning / no forward progress) model=${childRuntime.modelId}`);
|
|
411
|
+
},
|
|
382
412
|
});
|
|
383
413
|
const result = streamText({
|
|
384
414
|
model: childRuntime.model,
|
|
@@ -396,11 +426,11 @@ export class StreamRunner {
|
|
|
396
426
|
prepareStep: ({ messages, stepNumber }) => {
|
|
397
427
|
if (stepNumber < 1)
|
|
398
428
|
return undefined;
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
//
|
|
402
|
-
//
|
|
403
|
-
//
|
|
429
|
+
// Internal multi-step loop: AI-SDK accumulates streamed reasoning
|
|
430
|
+
// parts into in-flight assistant history and re-POSTs them on the
|
|
431
|
+
// next step within the same streamText call — orchestrator-level
|
|
432
|
+
// strip at call setup never sees this. Strip per step too via the
|
|
433
|
+
// capability hook (identity for providers without the quirk).
|
|
404
434
|
const stripped = taskCaps.sanitizeHistory(messages);
|
|
405
435
|
// G1 + G2 — pass the sub-agent's model context window so the
|
|
406
436
|
// compactor can use a token-aware threshold and shrink the keep
|
|
@@ -428,17 +458,16 @@ export class StreamRunner {
|
|
|
428
458
|
break;
|
|
429
459
|
}
|
|
430
460
|
}
|
|
431
|
-
|
|
432
|
-
const persistSubArtifact = (toolCallId, toolName, fullContent, reason) => {
|
|
461
|
+
const persistSubArtifact = (toolCallId, toolName, fullContent, reason, summary) => {
|
|
433
462
|
// Local-first durable cache so ee_query rehydrates even when EE is down.
|
|
434
463
|
recordArtifact(toolCallId, toolName, fullContent);
|
|
435
|
-
recordElision(toolCallId, toolName, fullContent.length, stepNumber);
|
|
464
|
+
recordElision(toolCallId, toolName, fullContent.length, stepNumber, summary);
|
|
436
465
|
try {
|
|
437
466
|
getDefaultEEClient()
|
|
438
467
|
.extract({
|
|
439
468
|
transcript: fullContent.slice(0, 4000),
|
|
440
469
|
projectPath: process.cwd(),
|
|
441
|
-
meta: { source: "tool-artifact", toolCallId, toolName, reason },
|
|
470
|
+
meta: { source: "tool-artifact", toolCallId, toolName, reason, summary },
|
|
442
471
|
}, AbortSignal.timeout(600))
|
|
443
472
|
.catch(() => { });
|
|
444
473
|
}
|
|
@@ -494,7 +523,7 @@ export class StreamRunner {
|
|
|
494
523
|
return undefined;
|
|
495
524
|
return { messages: finalMessages };
|
|
496
525
|
},
|
|
497
|
-
...(
|
|
526
|
+
...resolveTemperatureParam(childRuntime, isExplore ? 0.2 : 0.5),
|
|
498
527
|
...(childDropMaxOutput ? {} : { maxOutputTokens: Math.min(this.deps.getMaxTokens(), 8_192) }),
|
|
499
528
|
...(childProviderOptions ? { providerOptions: childProviderOptions } : {}),
|
|
500
529
|
onFinish: ({ totalUsage, finishReason }) => {
|
|
@@ -557,6 +586,7 @@ export class StreamRunner {
|
|
|
557
586
|
});
|
|
558
587
|
}
|
|
559
588
|
if (part.type === "text-delta") {
|
|
589
|
+
stall.petProgress(); // real forward progress — reset the no-progress guard
|
|
560
590
|
textDeltaCount++;
|
|
561
591
|
assistantText += part.text;
|
|
562
592
|
// Task 2.6b — emit llm-token (agent-mode only; high-volume, default-off per Phase 4).
|
|
@@ -576,6 +606,7 @@ export class StreamRunner {
|
|
|
576
606
|
continue;
|
|
577
607
|
}
|
|
578
608
|
if (part.type === "tool-call") {
|
|
609
|
+
stall.petProgress(); // real forward progress — reset the no-progress guard
|
|
579
610
|
toolCallCount++;
|
|
580
611
|
lastActivity = formatSubagentActivity(part.toolName, part.input);
|
|
581
612
|
onActivity?.(lastActivity);
|
|
@@ -653,7 +684,9 @@ export class StreamRunner {
|
|
|
653
684
|
...priorMessages,
|
|
654
685
|
{
|
|
655
686
|
role: "user",
|
|
656
|
-
content: "You've reached your
|
|
687
|
+
content: "You've reached your tool execution budget (max steps) for this turn and have not written your final answer yet. Stop now — do NOT call any more tools.\n\n" +
|
|
688
|
+
"Option A: If you have enough findings, write your final synthesis FOR THE PARENT AGENT: lead with the answer to the delegated task, cite the concrete file:line behind each claim, then note any gaps or the recommended next step. Be concise; the parent only ingests this message.\n\n" +
|
|
689
|
+
"Option B: If you need to continue working but are blocked by this limit, you can request the system to compact the context and start a fresh turn. To do this, reply with EXACTLY this format and nothing else:\n/compact <instructions on what to focus on after compaction>",
|
|
657
690
|
},
|
|
658
691
|
],
|
|
659
692
|
tools: {},
|
|
@@ -18,7 +18,19 @@
|
|
|
18
18
|
* returns a fresh wrapper with its own counters. Don't share across
|
|
19
19
|
* sub-agent runs.
|
|
20
20
|
*
|
|
21
|
-
*
|
|
21
|
+
* BUDGET SOURCE — do not read the 120_000 below as "the sub-agent budget".
|
|
22
|
+
* DEFAULT_MAX_CUMULATIVE_CHARS (120_000) is only the fallback used when
|
|
23
|
+
* wrapToolSetWithCap() is called WITHOUT maxCumulativeChars. The wired
|
|
24
|
+
* budgets come from settings, not this constant:
|
|
25
|
+
* - sub-agent (`task`) turns: getSubAgentBudgetChars() → default 240_000
|
|
26
|
+
* (settings.ts), passed at stream-runner.ts.
|
|
27
|
+
* - top-level orchestrator turn: getTopLevelToolBudgetChars() → default
|
|
28
|
+
* 400_000, wired at tool-engine.ts with looser 0.5/0.8 tier ratios.
|
|
29
|
+
* See docs/agent-harness/CONTEXT-CONTROL-LAYERS.md for how this cap relates
|
|
30
|
+
* to the other four context-control layers (compactor, rotation, reactive
|
|
31
|
+
* sub-session, top-level cap).
|
|
32
|
+
*
|
|
33
|
+
* Tiers (percentages of whichever budget is in effect, NOT of 120_000):
|
|
22
34
|
* < 30% → pass through (A1's 32KB per-call cap already applied)
|
|
23
35
|
* 30-70% → truncate each new result to 8_000 chars head/tail
|
|
24
36
|
* 70-100% → truncate to 2_000 chars head + "[budget low, finalize work]" note
|