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
|
@@ -5,6 +5,7 @@ import { getTenantId } from "../ee/tenant.js";
|
|
|
5
5
|
import { emitTranscriptToDisk } from "../ee/transcript-emit.js";
|
|
6
6
|
import { createRun, getActiveRunId, setActiveRunId } from "../flow/run-manager.js";
|
|
7
7
|
import { ensureFlowDir } from "../flow/scaffold.js";
|
|
8
|
+
import { isContextRailEnabled } from "../gsd/flags.js";
|
|
8
9
|
import { executeEventHooks } from "../hooks/index.js";
|
|
9
10
|
import { shutdownWorkspaceLspManager } from "../lsp/runtime.js";
|
|
10
11
|
import { ensureDefaultMcpServers } from "../mcp/auto-setup.js";
|
|
@@ -12,27 +13,27 @@ import { getModelInfo, normalizeModelId } from "../models/registry.js";
|
|
|
12
13
|
import { getProviderCapabilities } from "../providers/capabilities.js";
|
|
13
14
|
import { apiBaseFor } from "../providers/endpoints.js";
|
|
14
15
|
import { loadKeyForProvider } from "../providers/keychain.js";
|
|
15
|
-
import { createProviderFactory, createProviderFactoryAsync, detectProviderForModel, requireRuntimeProvider, resolveModelRuntime as resolveRuntime, } from "../providers/runtime.js";
|
|
16
|
+
import { createProviderFactory, createProviderFactoryAsync, detectProviderForModel, requireRuntimeProvider, resolveModelRuntime as resolveRuntime, resolveTemperatureParam, } from "../providers/runtime.js";
|
|
16
17
|
import { ALL_PROVIDER_IDS } from "../providers/types.js";
|
|
17
|
-
import {
|
|
18
|
+
import { statusBarStore } from "../state/status-bar-store.js";
|
|
19
|
+
import { appendCompaction, appendMessages, appendSystemMessage, buildChatEntries, getLastTodoWriteArgs, getNextMessageSequence, getSessionChainInfos, getSessionTotalTokens, loadSessionChainTranscriptState, logInteraction, markMessageCompleted, persistApprovedPlan, recordUsageEvent, SessionStore, } from "../storage/index.js";
|
|
18
20
|
import { BashTool } from "../tools/bash.js";
|
|
19
21
|
import { createBuiltinTools } from "../tools/registry.js";
|
|
20
22
|
import { ScheduleManager } from "../tools/schedule.js";
|
|
21
23
|
import { snapshotFromTodoWriteArgs } from "../tools/todo-write-snapshot.js";
|
|
22
|
-
import { statusBarStore } from "../ui/status-bar/store.js";
|
|
23
24
|
import { appendCostLog } from "../usage/cost-log.js";
|
|
24
25
|
import { appendDecisionLog } from "../usage/decision-log.js";
|
|
25
26
|
import { projectCostUSD, sanitizeInputTokens } from "../usage/estimator.js";
|
|
26
27
|
import { logger } from "../utils/logger.js";
|
|
27
|
-
import { getAutoCompactThresholdPct, getCouncilRounds, getCurrentModel, getCurrentShellSettings, getModeSpecificModel, getRoleModel, getRoleModels, isAutoCompactAfterTurnEnabled, isCouncilMultiProviderPreferred, isProviderDisabled, loadUserSettings, } from "../utils/settings.js";
|
|
28
|
+
import { getAutoCompactMinNewTokens, getAutoCompactThresholdPct, getCouncilRounds, getCurrentModel, getCurrentShellSettings, getModeSpecificModel, getRoleModel, getRoleModels, getSubAgentCompactKeepLast, getSubAgentCompactThresholdChars, isAutoCompactAfterTurnEnabled, isCouncilMultiProviderPreferred, isProviderDisabled, loadUserSettings, } from "../utils/settings.js";
|
|
28
29
|
import { runSideQuestion } from "../utils/side-question.js";
|
|
29
|
-
import { buildVerifyDetectPrompt, normalizeVerifyRecipe } from "../verify/entrypoint.js";
|
|
30
|
+
import { buildVerifyDetectPrompt, inferVerifyProjectProfile, normalizeVerifyRecipe, shouldTrustDeterministicRecipe, } from "../verify/entrypoint.js";
|
|
30
31
|
import { runVerifyOrchestration } from "../verify/orchestrator.js";
|
|
31
32
|
import { COUNCIL_COLOR_BG, COUNCIL_COLOR_RESET, COUNCIL_ROLE_COLORS, } from "./agent-options.js";
|
|
32
33
|
import { AUTO_COMMIT_ATTRIBUTION, isAutoCommitEnabled, maybeAutoCommitTurn, snapshotDirtyPaths, } from "./auto-commit.js";
|
|
33
34
|
import { BatchTurnRunner } from "./batch-turn-runner.js";
|
|
34
35
|
import { accumulateUsage, buildAssistantBatchMessage, buildBatchChatCompletionRequest, buildBatchName, buildToolBatchMessage, extractJsonObject, getBatchUsage, hasUsage, parseToolArgumentsOrRaw, toLocalToolCall, } from "./batch-utils.js";
|
|
35
|
-
import { createCompactionSummaryMessage, DEFAULT_KEEP_RECENT_TOKENS, DEFAULT_RESERVE_TOKENS, estimateConversationTokens, extractUserContent, generateCompactionSummary, POST_TURN_MIN_TOKENS, prepareCompaction, proposeCompaction, shouldCompactContext, } from "./compaction.js";
|
|
36
|
+
import { createCompactionSummaryMessage, DEFAULT_KEEP_RECENT_TOKENS, DEFAULT_RESERVE_TOKENS, estimateConversationTokens, extractUserContent, generateCompactionSummary, isCompactionSummaryMessage, isCompactionThrash, POST_TURN_MIN_TOKENS, prepareCompaction, proposeCompaction, shouldCompactContext, } from "./compaction.js";
|
|
36
37
|
import { CouncilManager } from "./council-manager.js";
|
|
37
38
|
import { CrossTurnDedup, isCrossTurnDedupEnabled } from "./cross-turn-dedup.js";
|
|
38
39
|
import { DelegationManager } from "./delegations.js";
|
|
@@ -40,11 +41,14 @@ import { loadFlowResumeDigest } from "./flow-resume.js";
|
|
|
40
41
|
import { MessageProcessor } from "./message-processor.js";
|
|
41
42
|
import { lastPersistedSeq } from "./message-seq.js";
|
|
42
43
|
import { buildSystemPrompt, HARD_MAX_TOOL_ROUNDS, MAX_TOOL_ROUNDS } from "./prompts.js";
|
|
44
|
+
import { getReactiveDelegationThresholdChars, shouldReactivelyEscalate } from "./reactive-delegation.js";
|
|
43
45
|
import { getReadPathBudgetCap, ReadPathBudget } from "./read-path-budget.js";
|
|
44
46
|
import { withStreamRetry } from "./retry-stream.js";
|
|
45
47
|
import { StreamRunner } from "./stream-runner.js";
|
|
46
48
|
import { resolveModelForTask } from "./sub-agent-model-tier.js";
|
|
49
|
+
import { compactSubAgentMessages } from "./subagent-compactor.js";
|
|
47
50
|
import { setProviderHint } from "./token-counter.js";
|
|
51
|
+
import { getToolLimitAutoRecoverCap } from "./tool-limit-auto-recover.js";
|
|
48
52
|
import { firstLine, formatSubagentActivity, toToolResult } from "./tool-utils.js";
|
|
49
53
|
// ---------------------------------------------------------------------------
|
|
50
54
|
// Provider implementations
|
|
@@ -69,15 +73,65 @@ function isAnyProviderApiBase(url) {
|
|
|
69
73
|
}
|
|
70
74
|
return false;
|
|
71
75
|
}
|
|
76
|
+
const TITLE_SYSTEM_PROMPT = `You are a session-naming assistant. Given the first message a user sent to an AI coding assistant, produce a short session title.
|
|
77
|
+
|
|
78
|
+
Rules:
|
|
79
|
+
- 5-7 words that capture the essence of the request (the goal, not the phrasing).
|
|
80
|
+
- Same language as the user's message (English request → English title, Vietnamese → Vietnamese, etc.).
|
|
81
|
+
- Title Case for languages that use it; natural casing otherwise.
|
|
82
|
+
- No surrounding quotes, no trailing punctuation, no emoji, no markdown.
|
|
83
|
+
- Output ONLY the title text — nothing else.`;
|
|
84
|
+
/** Deterministic fallback title: truncated first user message, or "" when the
|
|
85
|
+
* message is empty or a JSON-ish payload (so the caller leaves title = NULL
|
|
86
|
+
* and the picker renders a clean "(untitled)" instead of a literal "{}"). */
|
|
87
|
+
export function fallbackTitle(userMessage) {
|
|
88
|
+
const trimmed = userMessage.trim();
|
|
89
|
+
if (!trimmed)
|
|
90
|
+
return "";
|
|
91
|
+
// JSON object/array payloads (programmatic first messages) make useless titles.
|
|
92
|
+
if (/^[{[]/.test(trimmed) && /[}\]]$/.test(trimmed))
|
|
93
|
+
return "";
|
|
94
|
+
return trimmed.slice(0, 60).trim();
|
|
95
|
+
}
|
|
96
|
+
/** Strip quotes/backticks, collapse whitespace, drop trailing punctuation, cap at ~60 chars. */
|
|
97
|
+
function sanitizeTitle(raw) {
|
|
98
|
+
let t = raw.trim().split("\n")[0]?.trim() ?? "";
|
|
99
|
+
t = t.replace(/^["'`“”‘’]+|["'`“”‘’]+$/g, "");
|
|
100
|
+
t = t
|
|
101
|
+
.replace(/\s+/g, " ")
|
|
102
|
+
.replace(/[.,:;!?…]+$/g, "")
|
|
103
|
+
.trim();
|
|
104
|
+
if (t.length > 60)
|
|
105
|
+
t = t.slice(0, 60).trim();
|
|
106
|
+
return t;
|
|
107
|
+
}
|
|
72
108
|
/**
|
|
73
|
-
* Generate a session title
|
|
74
|
-
*
|
|
109
|
+
* Generate a session title with a single lightweight LLM call on the current
|
|
110
|
+
* session model. Falls back to a truncated first-message title on any failure.
|
|
75
111
|
*/
|
|
76
|
-
function genTitle(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
112
|
+
async function genTitle(provider, userMessage, modelId) {
|
|
113
|
+
try {
|
|
114
|
+
const { generateText } = await import("ai");
|
|
115
|
+
const runtime = resolveModelRuntime(provider, modelId);
|
|
116
|
+
const snippet = userMessage.length > 1500 ? `${userMessage.slice(0, 1500)}…` : userMessage;
|
|
117
|
+
const { text, usage } = await generateText({
|
|
118
|
+
model: runtime.model,
|
|
119
|
+
system: TITLE_SYSTEM_PROMPT,
|
|
120
|
+
prompt: `User's first message:\n\n${snippet}\n\nTitle:`,
|
|
121
|
+
...resolveTemperatureParam(runtime, 0.3),
|
|
122
|
+
...(runtime.modelInfo?.supportsMaxOutputTokens === false ? {} : { maxOutputTokens: 64 }),
|
|
123
|
+
...(runtime.providerOptions ? { providerOptions: runtime.providerOptions } : {}),
|
|
124
|
+
});
|
|
125
|
+
const title = sanitizeTitle(text ?? "");
|
|
126
|
+
if (title) {
|
|
127
|
+
return { title, modelId, usage };
|
|
128
|
+
}
|
|
129
|
+
console.error(`[orchestrator/genTitle] title generation returned empty text (model=${modelId}); using fallback`);
|
|
130
|
+
}
|
|
131
|
+
catch (err) {
|
|
132
|
+
console.error(`[orchestrator/genTitle] title generation failed (model=${modelId}): ${err?.message}; using fallback`);
|
|
133
|
+
}
|
|
134
|
+
return { title: fallbackTitle(userMessage), modelId };
|
|
81
135
|
}
|
|
82
136
|
/**
|
|
83
137
|
* Resolve a model ID to a runnable AI SDK LanguageModel.
|
|
@@ -110,6 +164,7 @@ function createTools(_bash, _provider, _mode, _opts) {
|
|
|
110
164
|
listDelegations: _opts?.listDelegations,
|
|
111
165
|
killDelegation: _opts?.killDelegation,
|
|
112
166
|
modelId: _opts?.modelId,
|
|
167
|
+
sessionId: _opts?.sessionId,
|
|
113
168
|
});
|
|
114
169
|
}
|
|
115
170
|
// ---------------------------------------------------------------------------
|
|
@@ -138,6 +193,8 @@ export class Agent {
|
|
|
138
193
|
steerDrain = null;
|
|
139
194
|
maxToolRounds;
|
|
140
195
|
hardMaxToolRounds;
|
|
196
|
+
/** Original hard ceiling at construction — bounds auto-compaction extension. */
|
|
197
|
+
_initialHardMaxToolRounds = 0;
|
|
141
198
|
mode = "agent";
|
|
142
199
|
modelId;
|
|
143
200
|
maxTokens;
|
|
@@ -196,6 +253,9 @@ export class Agent {
|
|
|
196
253
|
_turnUserGoalExcerpt = "";
|
|
197
254
|
/** Compaction statistics tracking count and total tokens saved. */
|
|
198
255
|
_compactionStats = { count: 0, totalSaved: 0 };
|
|
256
|
+
/** Token count immediately after the last successful compaction (or null if none yet this
|
|
257
|
+
* session). Used by the cross-turn anti-thrash guard in postTurnCompact. */
|
|
258
|
+
_lastCompactionTokensAfter = null;
|
|
199
259
|
/**
|
|
200
260
|
* Pinned message sequences. A pinned user message is preserved verbatim across
|
|
201
261
|
* compaction — it is re-injected as a system note immediately after the
|
|
@@ -208,6 +268,15 @@ export class Agent {
|
|
|
208
268
|
// Phase C3: cross-turn tool-output dedup. One instance per session; bumped
|
|
209
269
|
// on each user turn. Lazily initialized so disabled-via-env path stays cheap.
|
|
210
270
|
_crossTurnDedup = isCrossTurnDedupEnabled() ? new CrossTurnDedup() : null;
|
|
271
|
+
// Reactive sub-session escalation: the PREVIOUS turn's cumulative tool-output
|
|
272
|
+
// chars (from the top-level cap), reported by the tool engine at turn end and
|
|
273
|
+
// read at the next turn's route decision. See reactive-delegation.ts.
|
|
274
|
+
_lastTurnToolChars = 0;
|
|
275
|
+
// Instrument (cold-first-turn measurement): ordinal of the top-level turn whose
|
|
276
|
+
// tool load was last reported. Lets telemetry distinguish a turn-1 blow-up
|
|
277
|
+
// (which reactive escalation CANNOT isolate) from a later one (which it can).
|
|
278
|
+
// Gates the decision to build an in-turn checkpoint. See CONTEXT-CONTROL-LAYERS.md.
|
|
279
|
+
_turnLoadOrdinal = 0;
|
|
211
280
|
// Phase C4 — input-keyed read-path budget. Complements C3 (output hash) by
|
|
212
281
|
// catching re-reads of files the agent edited between rounds. Disabled
|
|
213
282
|
// when MUONROI_MAX_READS_PER_PATH=0.
|
|
@@ -246,6 +315,10 @@ export class Agent {
|
|
|
246
315
|
this.maxToolRounds = maxToolRounds || settings.maxToolRounds || MAX_TOOL_ROUNDS;
|
|
247
316
|
const baseHardMax = settings.hardMaxToolRounds || HARD_MAX_TOOL_ROUNDS;
|
|
248
317
|
this.hardMaxToolRounds = Math.max(Math.floor(this.maxToolRounds * 1.5), baseHardMax);
|
|
318
|
+
// Baseline captured so auto-compaction ceiling extension (see
|
|
319
|
+
// extendHardCeilingForAutoCompaction) is bounded relative to the ORIGINAL
|
|
320
|
+
// hard cap and cannot grow without limit across a runaway turn.
|
|
321
|
+
this._initialHardMaxToolRounds = this.hardMaxToolRounds;
|
|
249
322
|
const envMax = Number(process.env.MUONROI_MAX_TOKENS);
|
|
250
323
|
this.maxTokens = Number.isFinite(envMax) && envMax > 0 ? envMax : 16_384;
|
|
251
324
|
this.batchApi = options.batchApi ?? false;
|
|
@@ -280,7 +353,7 @@ export class Agent {
|
|
|
280
353
|
this.workspace = this.sessionStore.getWorkspace();
|
|
281
354
|
this.session = this.sessionStore.openSession(options.session, this.modelId, this.mode, this.bash.getCwd());
|
|
282
355
|
this.mode = this.session.mode;
|
|
283
|
-
const transcript =
|
|
356
|
+
const transcript = loadSessionChainTranscriptState(this.session.id);
|
|
284
357
|
this.messages = transcript.messages;
|
|
285
358
|
this.messageSeqs = transcript.seqs;
|
|
286
359
|
this.sessionStore.setModel(this.session.id, this.modelId);
|
|
@@ -333,26 +406,36 @@ export class Agent {
|
|
|
333
406
|
setModel(model) {
|
|
334
407
|
this.modelId = normalizeModelId(model);
|
|
335
408
|
const newProviderId = detectProviderForModel(this.modelId);
|
|
336
|
-
if (newProviderId !== this.providerId
|
|
409
|
+
if (newProviderId !== this.providerId) {
|
|
337
410
|
this.providerId = newProviderId;
|
|
338
411
|
setProviderHint(this.providerId);
|
|
339
412
|
// Drop this.baseURL when it points at a DIFFERENT provider's default
|
|
340
413
|
// apiBase — otherwise the rebuilt factory binds the new provider's
|
|
341
414
|
// strategy to the OLD provider's URL, sending requests to the wrong
|
|
342
415
|
// host. Evidence: session 2492d6579b1d — user switched defaultProvider
|
|
343
|
-
//
|
|
344
|
-
// startup,
|
|
345
|
-
// baseURL, requests landed at
|
|
346
|
-
//
|
|
347
|
-
// model names are deepseek-v4-pro or deepseek-v4-flash".
|
|
416
|
+
// (via UI), this.baseURL was still the old provider's apiBase from
|
|
417
|
+
// startup, the rebuilt strategy factory was created with that stale
|
|
418
|
+
// baseURL, and requests landed at the wrong host which rejected the
|
|
419
|
+
// model id.
|
|
348
420
|
// A user-supplied custom baseURL is preserved only when it does NOT
|
|
349
421
|
// match any known provider's apiBase (i.e. it's a real override, not
|
|
350
422
|
// a stale default).
|
|
351
423
|
const staleBaseURL = isAnyProviderApiBase(this.baseURL) && this.baseURL !== apiBaseFor(this.providerId);
|
|
352
|
-
const effectiveBaseURL = staleBaseURL ? undefined : (this.baseURL ?? undefined);
|
|
353
424
|
if (staleBaseURL)
|
|
354
425
|
this.baseURL = null;
|
|
355
|
-
|
|
426
|
+
// Provider changed — DEFER factory construction. The current this.apiKey
|
|
427
|
+
// belongs to the PREVIOUS provider (or is the "oauth" OAuth sentinel) and
|
|
428
|
+
// is invalid for the new one. Rebuilding here with it sent provider A's
|
|
429
|
+
// key to provider B → 401 "Authentication Fails, your api key … is
|
|
430
|
+
// invalid" on the next chat, for EVERY TUI provider switch (evidence:
|
|
431
|
+
// routing log routed to "deepseek-v4-flash via deepseek" with a
|
|
432
|
+
// non-deepseek key; and the "oauth" leak in session ff932f8568e8). Clear
|
|
433
|
+
// the stale key, null the provider, and re-arm _initOAuthProvider, which
|
|
434
|
+
// resolves the NEW provider's own key (or OAuth tokens) before the next
|
|
435
|
+
// turn (message-processor calls initOAuthProvider at turn start).
|
|
436
|
+
this.apiKey = null;
|
|
437
|
+
this.provider = null;
|
|
438
|
+
this._oauthInitDone = false;
|
|
356
439
|
}
|
|
357
440
|
if (this.sessionStore && this.session) {
|
|
358
441
|
this.sessionStore.setModel(this.session.id, this.modelId);
|
|
@@ -392,6 +475,17 @@ export class Agent {
|
|
|
392
475
|
}
|
|
393
476
|
setPlanContext(ctx) {
|
|
394
477
|
this.planContext = ctx;
|
|
478
|
+
// Persist so that bare continuation phrases ("tiếp tục", "continue") after
|
|
479
|
+
// interrupt or cross-process resume can re-hydrate the APPROVED PLAN section
|
|
480
|
+
// instead of forgetting context and re-asking for details.
|
|
481
|
+
if (this.session && ctx) {
|
|
482
|
+
try {
|
|
483
|
+
persistApprovedPlan(this.session.id, ctx);
|
|
484
|
+
}
|
|
485
|
+
catch {
|
|
486
|
+
/* best-effort */
|
|
487
|
+
}
|
|
488
|
+
}
|
|
395
489
|
}
|
|
396
490
|
setSendTelegramFile(fn) {
|
|
397
491
|
this.sendTelegramFile = fn;
|
|
@@ -403,7 +497,7 @@ export class Agent {
|
|
|
403
497
|
this.apiKey = apiKey;
|
|
404
498
|
// Drop baseURL when it points at a DIFFERENT provider's default apiBase
|
|
405
499
|
// (e.g. caller passed the legacy anthropic URL while providerId is
|
|
406
|
-
//
|
|
500
|
+
// deepseek — without this we'd send deepseek requests to
|
|
407
501
|
// api.anthropic.com or similar). User-supplied custom URLs that don't
|
|
408
502
|
// match any known provider's apiBase are preserved as real overrides.
|
|
409
503
|
const stale = isAnyProviderApiBase(baseURL) && baseURL !== apiBaseFor(this.providerId);
|
|
@@ -419,6 +513,14 @@ export class Agent {
|
|
|
419
513
|
getProviderId() {
|
|
420
514
|
return this.providerId;
|
|
421
515
|
}
|
|
516
|
+
/**
|
|
517
|
+
* Expose the provider factory for external callers (e.g., /compact slash command,
|
|
518
|
+
* sub-agent spawning) that need to make LLM calls outside the normal processMessage flow.
|
|
519
|
+
* Throws if no provider is configured.
|
|
520
|
+
*/
|
|
521
|
+
getProvider() {
|
|
522
|
+
return this.requireProvider();
|
|
523
|
+
}
|
|
422
524
|
getCwd() {
|
|
423
525
|
return this.bash.getCwd();
|
|
424
526
|
}
|
|
@@ -432,6 +534,9 @@ export class Agent {
|
|
|
432
534
|
setMessages(messages) {
|
|
433
535
|
this.messages = messages;
|
|
434
536
|
this.messageSeqs = messages.map(() => null);
|
|
537
|
+
// Manual compaction (/compact) or /clear replaces the conversation; reset
|
|
538
|
+
// the per-turn auto-compact latch so future tool loops can compact again.
|
|
539
|
+
this._compactedThisTurn = false;
|
|
435
540
|
}
|
|
436
541
|
async listSchedules() {
|
|
437
542
|
return this.schedules.list();
|
|
@@ -460,7 +565,7 @@ export class Agent {
|
|
|
460
565
|
if (!provider) {
|
|
461
566
|
return "New session";
|
|
462
567
|
}
|
|
463
|
-
const generated = await genTitle(provider, userMessage);
|
|
568
|
+
const generated = await genTitle(provider, userMessage, this.modelId);
|
|
464
569
|
this.recordUsage(generated.usage, "title", generated.modelId);
|
|
465
570
|
if (this.sessionStore && this.session && !this.session.title && generated.title) {
|
|
466
571
|
this.sessionStore.setTitle(this.session.id, generated.title);
|
|
@@ -530,6 +635,22 @@ export class Agent {
|
|
|
530
635
|
setToolLoopCapHandler(fn) {
|
|
531
636
|
this._toolLoopCapHandler = fn;
|
|
532
637
|
}
|
|
638
|
+
/**
|
|
639
|
+
* Extend the absolute hard-cap ceiling when the turn is being sustained by
|
|
640
|
+
* AUTO-COMPACTION (a productive long task, not a runaway). Rationale: each
|
|
641
|
+
* auto-compacted round resets context to O(N) input, so it is cheap — the
|
|
642
|
+
* hard cap's cost-runaway purpose is already served by the compaction. We
|
|
643
|
+
* grant `maxToolRounds/2` more headroom per compaction, bounded to at most
|
|
644
|
+
* one extra auto-recover budget's worth above the ORIGINAL ceiling so a
|
|
645
|
+
* genuinely wedged turn (which trips the pattern guard, not this path) can
|
|
646
|
+
* never grow the cap without limit. Called from the tool-engine auto-recover
|
|
647
|
+
* branch after a successful compaction.
|
|
648
|
+
*/
|
|
649
|
+
extendHardCeilingForAutoCompaction() {
|
|
650
|
+
const bump = Math.max(25, Math.floor(this.maxToolRounds / 2));
|
|
651
|
+
const absoluteMax = this._initialHardMaxToolRounds + bump * getToolLimitAutoRecoverCap();
|
|
652
|
+
this.hardMaxToolRounds = Math.min(this.hardMaxToolRounds + bump, absoluteMax);
|
|
653
|
+
}
|
|
533
654
|
// Safety-override handler — set by the UI (app.tsx) at startup. Invoked
|
|
534
655
|
// from the message-processor tool-result pipeline when a safety block
|
|
535
656
|
// (catastrophic, dangerous, git-safety, empty-bash) is detected. The UI
|
|
@@ -585,6 +706,7 @@ export class Agent {
|
|
|
585
706
|
compaction_summary: undefined,
|
|
586
707
|
});
|
|
587
708
|
this._compactionStats = { count: 0, totalSaved: 0 };
|
|
709
|
+
this._lastCompactionTokensAfter = null;
|
|
588
710
|
this._pinnedSeqs.clear();
|
|
589
711
|
if (!this.sessionStore) {
|
|
590
712
|
this.messages = [];
|
|
@@ -663,9 +785,35 @@ export class Agent {
|
|
|
663
785
|
return [];
|
|
664
786
|
return buildChatEntries(this.session.id);
|
|
665
787
|
}
|
|
788
|
+
/**
|
|
789
|
+
* The session tree this TUI currently hosts: the root conversation plus every
|
|
790
|
+
* rotation / sub-agent descendant, with per-session metadata. Content from
|
|
791
|
+
* these sessions is already merged into the transcript on resume; this exposes
|
|
792
|
+
* WHICH sessions produced it so the rail can show the multi-session structure.
|
|
793
|
+
*/
|
|
794
|
+
getSessionTree() {
|
|
795
|
+
if (!this.session)
|
|
796
|
+
return [];
|
|
797
|
+
try {
|
|
798
|
+
return getSessionChainInfos(this.session.id);
|
|
799
|
+
}
|
|
800
|
+
catch (err) {
|
|
801
|
+
logger.error("orchestrator", "getSessionTree failed", { message: err?.message });
|
|
802
|
+
return [];
|
|
803
|
+
}
|
|
804
|
+
}
|
|
666
805
|
getLastTodoSnapshot() {
|
|
667
806
|
if (!this.session)
|
|
668
807
|
return null;
|
|
808
|
+
try {
|
|
809
|
+
const { getTaskListSnapshotFromGsd } = require("../gsd/phase-sync.js");
|
|
810
|
+
const gsdSnap = getTaskListSnapshotFromGsd(this.bash?.getCwd() ?? process.cwd());
|
|
811
|
+
if (gsdSnap)
|
|
812
|
+
return gsdSnap;
|
|
813
|
+
}
|
|
814
|
+
catch (err) {
|
|
815
|
+
// fail-open to legacy todo_write args
|
|
816
|
+
}
|
|
669
817
|
const argsJson = getLastTodoWriteArgs(this.session.id);
|
|
670
818
|
if (!argsJson)
|
|
671
819
|
return null;
|
|
@@ -677,7 +825,7 @@ export class Agent {
|
|
|
677
825
|
return {
|
|
678
826
|
workspace: this.workspace,
|
|
679
827
|
session: this.session,
|
|
680
|
-
messages:
|
|
828
|
+
messages: loadSessionChainTranscriptState(this.session.id).messages,
|
|
681
829
|
entries: buildChatEntries(this.session.id),
|
|
682
830
|
totalTokens: getSessionTotalTokens(this.session.id),
|
|
683
831
|
};
|
|
@@ -796,7 +944,7 @@ export class Agent {
|
|
|
796
944
|
// where orchestrator/task/title traffic is actually spending.
|
|
797
945
|
// Best-effort: failures inside appendCostLog are swallowed (see cost-log.ts).
|
|
798
946
|
const breakdown = source === "message" ? (this._lastPromptBreakdown ?? undefined) : undefined;
|
|
799
|
-
// Sanitize actualInputTokens for providers
|
|
947
|
+
// Sanitize actualInputTokens for providers that return
|
|
800
948
|
// implausibly low prompt_tokens (e.g. 10) regardless of prompt size.
|
|
801
949
|
const estIn = breakdown ? Math.ceil(((breakdown.systemChars ?? 0) + (breakdown.messagesChars ?? 0)) / 4) : 0;
|
|
802
950
|
const actualInput = sanitizeInputTokens(totalInput, estIn);
|
|
@@ -919,8 +1067,27 @@ export class Agent {
|
|
|
919
1067
|
const totalUsage = {};
|
|
920
1068
|
let assistantText = "";
|
|
921
1069
|
let lastActivity = initialDetail;
|
|
1070
|
+
// Phase B3 parity: the streamText sub-agent path compacts older tool
|
|
1071
|
+
// results in prepareStep, but this batch path historically concatenated
|
|
1072
|
+
// `[...childMessages, ...turnMessages]` unbounded every round — on a long
|
|
1073
|
+
// batch loop the re-sent history balloons exactly like the stream path did
|
|
1074
|
+
// before B3. Apply the same compactor here (round >= 1, mirroring the
|
|
1075
|
+
// stream path's `stepNumber >= 1` gate). High-value results stay verbatim.
|
|
1076
|
+
const batchCompactThreshold = getSubAgentCompactThresholdChars();
|
|
1077
|
+
const batchCompactKeepLast = getSubAgentCompactKeepLast();
|
|
1078
|
+
const batchChildCtxWindow = childRuntime.modelInfo?.contextWindow ?? 0;
|
|
1079
|
+
const batchIsReasoningModel = childRuntime.modelInfo?.reasoning === true;
|
|
922
1080
|
for (let round = 0; round < maxSteps; round++) {
|
|
923
1081
|
const batchRequestId = `task-${Date.now()}-${round + 1}`;
|
|
1082
|
+
const roundMessages = round < 1
|
|
1083
|
+
? [...childMessages, ...turnMessages]
|
|
1084
|
+
: compactSubAgentMessages([...childMessages, ...turnMessages], {
|
|
1085
|
+
thresholdChars: batchCompactThreshold,
|
|
1086
|
+
keepLastTurns: batchCompactKeepLast,
|
|
1087
|
+
contextWindowTokens: batchChildCtxWindow,
|
|
1088
|
+
contextFillRatio: batchIsReasoningModel ? 0.3 : undefined,
|
|
1089
|
+
stripOldReasoning: batchIsReasoningModel,
|
|
1090
|
+
});
|
|
924
1091
|
await addBatchRequests({
|
|
925
1092
|
...this.getBatchClientOptions(signal),
|
|
926
1093
|
batchId: batch.batch_id,
|
|
@@ -931,8 +1098,8 @@ export class Agent {
|
|
|
931
1098
|
chat_get_completion: buildBatchChatCompletionRequest({
|
|
932
1099
|
modelId: childRuntime.modelId,
|
|
933
1100
|
system: childSystem,
|
|
934
|
-
messages:
|
|
935
|
-
temperature: request.agent === "explore" ? 0.2 : 0.5,
|
|
1101
|
+
messages: roundMessages,
|
|
1102
|
+
temperature: childRuntime.modelInfo?.fixedTemperature ?? (request.agent === "explore" ? 0.2 : 0.5),
|
|
936
1103
|
maxOutputTokens: !childCaps.acceptsParam("maxOutputTokens", childRuntime.modelInfo)
|
|
937
1104
|
? undefined
|
|
938
1105
|
: Math.min(this.maxTokens, 8_192),
|
|
@@ -1244,7 +1411,10 @@ export class Agent {
|
|
|
1244
1411
|
return this._resolveModelForTask("compact");
|
|
1245
1412
|
}
|
|
1246
1413
|
_resolveModelForTask(task) {
|
|
1247
|
-
|
|
1414
|
+
const parentTier = getModelInfo(this.modelId)?.tier;
|
|
1415
|
+
return resolveModelForTask(task, this.providerId, this.modelId, undefined, {
|
|
1416
|
+
parentTier,
|
|
1417
|
+
});
|
|
1248
1418
|
}
|
|
1249
1419
|
async compactForContext(provider, system, contextWindow, signal, settings = this.getCompactionSettings(contextWindow), force = false) {
|
|
1250
1420
|
if (!this.session)
|
|
@@ -1387,6 +1557,7 @@ export class Agent {
|
|
|
1387
1557
|
// grossSaved tells the user how many context tokens were reclaimed — a real benefit that
|
|
1388
1558
|
// reduces subsequent turn costs.
|
|
1389
1559
|
const tokensAfter = estimateConversationTokens(system, this.messages);
|
|
1560
|
+
this._lastCompactionTokensAfter = tokensAfter;
|
|
1390
1561
|
const saved = Math.max(0, preparation.tokensBefore - tokensAfter);
|
|
1391
1562
|
const pct = preparation.tokensBefore > 0 ? ((saved / preparation.tokensBefore) * 100).toFixed(1) : "0.0";
|
|
1392
1563
|
this._compactionStats.count++;
|
|
@@ -1452,6 +1623,13 @@ export class Agent {
|
|
|
1452
1623
|
minMeaningfulTokens,
|
|
1453
1624
|
});
|
|
1454
1625
|
}
|
|
1626
|
+
const minNew = getAutoCompactMinNewTokens();
|
|
1627
|
+
if (isCompactionThrash(tokens, this._lastCompactionTokensAfter, minNew)) {
|
|
1628
|
+
return log(false, `anti-thrash (only ${tokens - (this._lastCompactionTokensAfter ?? 0)} new < ${minNew})`, {
|
|
1629
|
+
tokens,
|
|
1630
|
+
lastAfter: this._lastCompactionTokensAfter,
|
|
1631
|
+
});
|
|
1632
|
+
}
|
|
1455
1633
|
log(true, `over-threshold (${tokens} >= ${minMeaningfulTokens})`, { tokens, thresholdPct, minMeaningfulTokens });
|
|
1456
1634
|
await this.compactForContext(provider, system, contextWindow, signal, this.getCompactionSettings(contextWindow), true).catch((err) => logger.warn("orchestrator", "compaction failed", { error: err }));
|
|
1457
1635
|
}
|
|
@@ -1464,8 +1642,8 @@ export class Agent {
|
|
|
1464
1642
|
// (respondToCouncilQuestion/Preflight + the internal _create*Responder
|
|
1465
1643
|
// hooks used by orchestrator.agent.test.ts).
|
|
1466
1644
|
// ========================================================================
|
|
1467
|
-
respondToCouncilQuestion(questionId, answer) {
|
|
1468
|
-
this.councilManager.respondToQuestion(questionId, answer);
|
|
1645
|
+
respondToCouncilQuestion(questionId, answer, questionText) {
|
|
1646
|
+
this.councilManager.respondToQuestion(questionId, answer, questionText);
|
|
1469
1647
|
}
|
|
1470
1648
|
respondToCouncilPreflight(preflightId, approved) {
|
|
1471
1649
|
this.councilManager.respondToPreflight(preflightId, approved);
|
|
@@ -1474,7 +1652,7 @@ export class Agent {
|
|
|
1474
1652
|
// Council v2 — Clarify → Confirm → Debate → Plan → Execute
|
|
1475
1653
|
// ========================================================================
|
|
1476
1654
|
async *runCouncilV2(topic, options) {
|
|
1477
|
-
const { runCouncil } = await import("../council/index.js");
|
|
1655
|
+
const { runCouncil, postDebateContinuation } = await import("../council/index.js");
|
|
1478
1656
|
const { createCouncilLLM } = await import("../council/llm.js");
|
|
1479
1657
|
const councilStats = { calls: 0, startMs: Date.now(), phases: [] };
|
|
1480
1658
|
const llm = createCouncilLLM(this.bash, this.mode, this.session?.id, councilStats);
|
|
@@ -1514,6 +1692,9 @@ export class Agent {
|
|
|
1514
1692
|
catch (err) {
|
|
1515
1693
|
logger.error("router", "runDir resolution failed (decisions.lock will be skipped)", { error: err });
|
|
1516
1694
|
}
|
|
1695
|
+
// Captures the post-debate action so we can auto-continue on
|
|
1696
|
+
// "continue_session" (user chose to keep working with the debate result).
|
|
1697
|
+
let chosenAction;
|
|
1517
1698
|
try {
|
|
1518
1699
|
const gen = runCouncil(topic, this.modelId, this.messages, this.session?.id, llm, this.councilManager.createQuestionResponder(), this.councilManager.createPreflightResponder(), processMessageFn, {
|
|
1519
1700
|
skipClarification: options?.skipClarification,
|
|
@@ -1521,12 +1702,39 @@ export class Agent {
|
|
|
1521
1702
|
cwd: this.bash.getCwd(),
|
|
1522
1703
|
councilStats, // NEW — share orchestrator's stats object with runCouncil (Phase 14 CQ-01)
|
|
1523
1704
|
signal,
|
|
1705
|
+
// When the Context Rail is active it carries leader/panel/cost as
|
|
1706
|
+
// ambient sidebar rows, so suppress the duplicate inline summary.
|
|
1707
|
+
suppressInlineMeta: isContextRailEnabled(),
|
|
1524
1708
|
runDir, // B1 — persist decisions.lock.md for the /council slash path
|
|
1709
|
+
// #2 — isolated research bridge. Same StreamRunner sub-agent the /ideal
|
|
1710
|
+
// sprint-runner uses (runTaskRequest): a budget-capped, near-empty
|
|
1711
|
+
// explore child runs the debate's research phase so its multi-step tool
|
|
1712
|
+
// clutter never accretes into the council thread/context. Threads the
|
|
1713
|
+
// council abort signal so Esc cancels the research child too.
|
|
1714
|
+
runIsolatedTask: (request) => this.runTaskRequest(request, undefined, signal),
|
|
1715
|
+
onPostDebateAction: (action) => {
|
|
1716
|
+
chosenAction = action;
|
|
1717
|
+
// Relay to the auto-council caller (tool-engine) so nested runs honor
|
|
1718
|
+
// the user's choice instead of always continuing.
|
|
1719
|
+
this.councilManager.setLastPostDebateAction(action);
|
|
1720
|
+
},
|
|
1525
1721
|
});
|
|
1526
1722
|
let result;
|
|
1723
|
+
// Hold back runCouncil's terminal `done`. The /council slash consumer
|
|
1724
|
+
// (use-app-logic.tsx) does `for await (chunk of gen) { if (chunk.type ===
|
|
1725
|
+
// "done") break; }` — so forwarding runCouncil's end-of-debate `done`
|
|
1726
|
+
// strands THIS generator before the continue_session auto-continue below
|
|
1727
|
+
// ever runs (the generator is suspended at the yield and never resumes).
|
|
1728
|
+
// Swallow it here and re-emit our own `done` after the optional
|
|
1729
|
+
// continuation so the consumer terminates at the right boundary.
|
|
1730
|
+
let innerDoneSeen = false;
|
|
1527
1731
|
do {
|
|
1528
1732
|
result = await gen.next();
|
|
1529
1733
|
if (!result.done && result.value) {
|
|
1734
|
+
if (result.value.type === "done") {
|
|
1735
|
+
innerDoneSeen = true;
|
|
1736
|
+
continue;
|
|
1737
|
+
}
|
|
1530
1738
|
yield result.value;
|
|
1531
1739
|
}
|
|
1532
1740
|
} while (!result.done);
|
|
@@ -1535,6 +1743,124 @@ export class Agent {
|
|
|
1535
1743
|
if (options?.userModelMessage && synthesis) {
|
|
1536
1744
|
this.appendCompletedTurn(options.userModelMessage, [{ role: "assistant", content: synthesis }]);
|
|
1537
1745
|
}
|
|
1746
|
+
// Keep working in THIS session when the chosen action calls for it
|
|
1747
|
+
// (continue_session → carry the conclusion; generate_plan/implement →
|
|
1748
|
+
// execute action items). postDebateContinuation is the single source of
|
|
1749
|
+
// truth shared with the auto-council caller (tool-engine). Re-entering
|
|
1750
|
+
// processMessage also writes real message rows, which is what makes the
|
|
1751
|
+
// session resumable (the /council slash path otherwise leaves no messages
|
|
1752
|
+
// and is filtered from the resume picker). Guarded by ownsController so it
|
|
1753
|
+
// fires ONLY on the top-level slash path, never when nested inside
|
|
1754
|
+
// processMessage (auto-council) or drained by the runDebate tool — those
|
|
1755
|
+
// callers manage their own continuation.
|
|
1756
|
+
const continuationPrompt = ownsController && synthesis ? postDebateContinuation(chosenAction, synthesis) : null;
|
|
1757
|
+
const isBuildContinuation = chosenAction === "implement" || chosenAction === "generate_plan";
|
|
1758
|
+
if (continuationPrompt && isBuildContinuation && process.env.MUONROI_COUNCIL_ISOLATE_IMPL !== "0") {
|
|
1759
|
+
// #1 — build the council decision in an ISOLATED sub-agent instead of
|
|
1760
|
+
// re-entering the full processMessage turn. The flat turn inherited the
|
|
1761
|
+
// entire multi-round debate history and could overflow the context window
|
|
1762
|
+
// (the exact wedge 97bc9d12 fixed for the /ideal sprint-runner but that
|
|
1763
|
+
// /council never got). runTaskRequest starts near-empty and returns a
|
|
1764
|
+
// compact ToolResult. ANTI-MÙ: the child is NOT blind — it is seeded with
|
|
1765
|
+
// (a) the approved synthesis-as-spec (already in continuationPrompt) and
|
|
1766
|
+
// (b) councilManager.buildContext(), the same compaction-summary + Recent
|
|
1767
|
+
// Conversation + [Council Decision]/[Council Memory] "Key Decisions"
|
|
1768
|
+
// checkpoint the anti-mù layer surfaces — so it has the decision + its
|
|
1769
|
+
// rationale without carrying the raw transcript. Opt out with
|
|
1770
|
+
// MUONROI_COUNCIL_ISOLATE_IMPL=0 (falls back to the processMessage path).
|
|
1771
|
+
yield { type: "content", content: "\n[Implementing the council decision in an isolated context…]\n" };
|
|
1772
|
+
this.councilManager.setContinuation(true);
|
|
1773
|
+
try {
|
|
1774
|
+
let councilCheckpoint = "";
|
|
1775
|
+
try {
|
|
1776
|
+
councilCheckpoint = this.councilManager.buildContext();
|
|
1777
|
+
}
|
|
1778
|
+
catch {
|
|
1779
|
+
/* anti-mù bundle is best-effort — the synthesis alone still grounds the build */
|
|
1780
|
+
}
|
|
1781
|
+
const seededPrompt = councilCheckpoint
|
|
1782
|
+
? `${continuationPrompt}\n\n## Council context (decision + recent session — do NOT re-debate, just build)\n${councilCheckpoint}`
|
|
1783
|
+
: continuationPrompt;
|
|
1784
|
+
let result;
|
|
1785
|
+
try {
|
|
1786
|
+
result = await this.runTaskRequest({ agent: "general", description: "Council implementation", prompt: seededPrompt, modelId: this.modelId }, undefined, signal);
|
|
1787
|
+
}
|
|
1788
|
+
catch (err) {
|
|
1789
|
+
// A throw (vs a returned {success:false}) must not escape and take the
|
|
1790
|
+
// whole /council run down — surface it in-band like the processMessage
|
|
1791
|
+
// path's TurnStallError handling and end the turn cleanly.
|
|
1792
|
+
yield {
|
|
1793
|
+
type: "error",
|
|
1794
|
+
content: `Council implementation failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1795
|
+
};
|
|
1796
|
+
yield { type: "done" };
|
|
1797
|
+
return;
|
|
1798
|
+
}
|
|
1799
|
+
if (result.success && result.output?.trim()) {
|
|
1800
|
+
yield { type: "content", content: `\n${result.output.trim()}\n` };
|
|
1801
|
+
}
|
|
1802
|
+
else if (result.error) {
|
|
1803
|
+
yield { type: "error", content: `Council implementation failed: ${result.error}` };
|
|
1804
|
+
}
|
|
1805
|
+
// Persist a COMPACT record so the /council slash session stays resumable
|
|
1806
|
+
// (the processMessage path wrote rows for this) WITHOUT re-inheriting the
|
|
1807
|
+
// debate bloat the isolated child deliberately avoided.
|
|
1808
|
+
try {
|
|
1809
|
+
this.appendCompletedTurn({ role: "user", content: "[Council implementation of the debate decision]" }, [{ role: "assistant", content: (result.output ?? result.error ?? "(no output)").trim() }]);
|
|
1810
|
+
}
|
|
1811
|
+
catch {
|
|
1812
|
+
/* non-critical persistence */
|
|
1813
|
+
}
|
|
1814
|
+
yield { type: "done" };
|
|
1815
|
+
}
|
|
1816
|
+
finally {
|
|
1817
|
+
this.councilManager.setContinuation(false);
|
|
1818
|
+
}
|
|
1819
|
+
}
|
|
1820
|
+
else if (continuationPrompt) {
|
|
1821
|
+
yield { type: "content", content: "\n[Continuing with the debate conclusion…]\n" };
|
|
1822
|
+
this.councilManager.setContinuation(true);
|
|
1823
|
+
try {
|
|
1824
|
+
// processMessage emits its own terminal `done`, which becomes the
|
|
1825
|
+
// consumer's break boundary — so the UI stops AFTER the continuation
|
|
1826
|
+
// turn, not before it.
|
|
1827
|
+
//
|
|
1828
|
+
// Guard the continuation with an idle+total watchdog: the per-chunk
|
|
1829
|
+
// stall watchdog only covers streamText byte flow, NOT a turn that
|
|
1830
|
+
// wedges inside a tool call (a `task` sub-agent or long `bash`).
|
|
1831
|
+
// Session 578b2eae7099 hung exactly there — the UI froze at "Council
|
|
1832
|
+
// working… elapsed 0s" with no rescue. On fire we abort the turn and
|
|
1833
|
+
// surface a toast instead of hanging forever.
|
|
1834
|
+
const { withTurnWatchdog, TurnStallError } = await import("./turn-watchdog.js");
|
|
1835
|
+
const idleMs = Number(process.env.MUONROI_COUNCIL_CONTINUATION_IDLE_MS ?? 120_000);
|
|
1836
|
+
const totalMs = Number(process.env.MUONROI_COUNCIL_CONTINUATION_TOTAL_MS ?? 600_000);
|
|
1837
|
+
try {
|
|
1838
|
+
yield* withTurnWatchdog(this.processMessage(continuationPrompt, options?.observer), {
|
|
1839
|
+
idleMs,
|
|
1840
|
+
totalMs,
|
|
1841
|
+
label: "council continuation turn",
|
|
1842
|
+
});
|
|
1843
|
+
}
|
|
1844
|
+
catch (err) {
|
|
1845
|
+
if (err instanceof TurnStallError) {
|
|
1846
|
+
this.abortController?.abort(new DOMException(err.message, "TimeoutError"));
|
|
1847
|
+
yield { type: "error", content: `Council continuation stalled — ${err.message}` };
|
|
1848
|
+
yield { type: "done" };
|
|
1849
|
+
}
|
|
1850
|
+
else {
|
|
1851
|
+
throw err;
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
finally {
|
|
1856
|
+
this.councilManager.setContinuation(false);
|
|
1857
|
+
}
|
|
1858
|
+
}
|
|
1859
|
+
else if (innerDoneSeen) {
|
|
1860
|
+
// Non-continuation paths (save_exit, nested auto-council, sprint drain)
|
|
1861
|
+
// still need the terminal `done` the consumer expects.
|
|
1862
|
+
yield { type: "done" };
|
|
1863
|
+
}
|
|
1538
1864
|
}
|
|
1539
1865
|
finally {
|
|
1540
1866
|
if (ownsController && this.abortController?.signal === signal) {
|
|
@@ -1556,25 +1882,67 @@ export class Agent {
|
|
|
1556
1882
|
};
|
|
1557
1883
|
const llm = createCouncilLLM(this.bash, this.mode, this.session?.id, productStats);
|
|
1558
1884
|
const processMessageFn = (m) => this.processMessage(m, options?.observer);
|
|
1885
|
+
// Isolated bounded task-runner bridge for the sprint implement stage: a fresh
|
|
1886
|
+
// child context (getSubAgentBudgetChars cap, independent compaction) that does
|
|
1887
|
+
// NOT inherit this turn's council-debate history — the root fix for the live
|
|
1888
|
+
// ctx-overflow wedge. Returns a compact ToolResult (absorbed, no parent bloat).
|
|
1889
|
+
const runIsolatedTask = (request) => this.runTaskRequest(request, undefined, this.abortController?.signal);
|
|
1559
1890
|
const flowDir = nodePath.join(this.bash.getCwd(), ".muonroi-flow");
|
|
1560
|
-
// P2.7
|
|
1561
|
-
//
|
|
1891
|
+
// P2.7 (LLM-first — no-regex routing): the work-depth tier that decides
|
|
1892
|
+
// /ideal's route is judged by the MODEL (the same depthTier the PIL Layer-1
|
|
1893
|
+
// classifier emits for chat turns), NOT by a keyword scorer. The old regex
|
|
1894
|
+
// `scoreComplexity` mis-tiered plainly-phrased heavy work — a "merge / vendor
|
|
1895
|
+
// / rename" refactor scored LOW because it lacked the FORCE_HIGH keywords —
|
|
1896
|
+
// which hot-pathed genuinely heavy tasks with zero interview/council. Only
|
|
1897
|
+
// meaningful for "start"; other subcommands ignore it.
|
|
1562
1898
|
let complexity;
|
|
1899
|
+
let needsClarification;
|
|
1563
1900
|
let sufficiencyMissing;
|
|
1901
|
+
let routeForceCouncil = payload.flags.forceCouncil;
|
|
1564
1902
|
if (payload.subcommand === "start" && payload.idea) {
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1903
|
+
let depth = "standard";
|
|
1904
|
+
try {
|
|
1905
|
+
const { createLlmClassifier } = await import("../pil/llm-classify.js");
|
|
1906
|
+
const classify = createLlmClassifier(this.requireProvider(), this.modelId);
|
|
1907
|
+
const res = await classify(payload.idea);
|
|
1908
|
+
if (res?.depthTier) {
|
|
1909
|
+
depth = res.depthTier;
|
|
1910
|
+
}
|
|
1911
|
+
else {
|
|
1912
|
+
// Same convention as PIL Layer 1: a null/garbled depth defaults to the
|
|
1913
|
+
// safe middle. Logged so a persistently-null classifier is diagnosable.
|
|
1914
|
+
console.error(`[ideal/route] model depth classify returned no depthTier — defaulting to "standard". idea=${JSON.stringify(payload.idea.slice(0, 80))}`);
|
|
1915
|
+
}
|
|
1916
|
+
// LLM-first clarity signal — feeds the downstream interview as a hint.
|
|
1917
|
+
if (res?.needsClarification === true)
|
|
1918
|
+
needsClarification = true;
|
|
1919
|
+
}
|
|
1920
|
+
catch (err) {
|
|
1921
|
+
// Fail-open: never block /ideal on a classify hiccup (No-Silent-Catch).
|
|
1922
|
+
console.error(`[ideal/route] depth classify failed, defaulting to "standard": ${err?.message}`);
|
|
1923
|
+
}
|
|
1924
|
+
// The cheap 8-word classify above is only a HINT for the downstream GSD
|
|
1925
|
+
// depth pipeline — it is NOT a routing gate. It was too noisy to gate the
|
|
1926
|
+
// council decision: the SAME architectural prompt scored `heavy` on one run
|
|
1927
|
+
// and `standard` the next, so routing flip-flopped between full Council and
|
|
1928
|
+
// a straight-to-Edit maint-edit that skipped the debate. /ideal is the
|
|
1929
|
+
// DELIBERATE path — it must ALWAYS run the full Council/loop-driver
|
|
1930
|
+
// pipeline, in this order:
|
|
1931
|
+
// 1. scan the source (discovery/scoping) to understand what the user's
|
|
1932
|
+
// request actually touches,
|
|
1933
|
+
// 2. interview the user (AskCard — adaptive: only asks what the source
|
|
1934
|
+
// can't answer, so an existing repo isn't dragged through 6 questions),
|
|
1935
|
+
// 3. leader-tier complexity assessment + debate INSIDE that flow, AFTER
|
|
1936
|
+
// scope is understood — never a "no debate" verdict from a bare-prompt
|
|
1937
|
+
// guess.
|
|
1938
|
+
// So we force Council for every /ideal start; the fast classify only
|
|
1939
|
+
// seeds depth/clarity hints for that flow. Explicit `--maintain` still opts
|
|
1940
|
+
// out of Council (handled at the top of runProductLoop dispatch).
|
|
1941
|
+
complexity = depth === "heavy" ? "high" : depth === "quick" ? "low" : "medium";
|
|
1942
|
+
sufficiencyMissing = undefined;
|
|
1943
|
+
if (payload.flags.mode !== "maintain") {
|
|
1944
|
+
routeForceCouncil = true;
|
|
1945
|
+
}
|
|
1578
1946
|
}
|
|
1579
1947
|
const gen = runProductLoop({
|
|
1580
1948
|
subcommand: payload.subcommand,
|
|
@@ -1589,16 +1957,18 @@ export class Agent {
|
|
|
1589
1957
|
doneThreshold: payload.flags.doneThreshold,
|
|
1590
1958
|
budgetTokens: payload.flags.budgetTokens,
|
|
1591
1959
|
stack: payload.flags.stack,
|
|
1592
|
-
forceCouncil:
|
|
1960
|
+
forceCouncil: routeForceCouncil,
|
|
1593
1961
|
},
|
|
1594
1962
|
respondToQuestion: this.councilManager.createQuestionResponder(),
|
|
1595
1963
|
respondToPreflight: this.councilManager.createPreflightResponder(),
|
|
1596
1964
|
cwd: this.bash.getCwd(),
|
|
1597
1965
|
processMessageFn,
|
|
1966
|
+
runIsolatedTask,
|
|
1598
1967
|
// Mode C — wire verify-recipe detector so runProductLoop auto-detect can probe cwd.
|
|
1599
1968
|
detectVerifyRecipe: () => this.detectVerifyRecipe(),
|
|
1600
1969
|
skipPriorContext: payload.flags.noPriorContext === true,
|
|
1601
1970
|
complexity,
|
|
1971
|
+
needsClarification,
|
|
1602
1972
|
sufficiencyMissing,
|
|
1603
1973
|
// Mode C explicit override + gh pr create opt-in (see .planning/MAINTAIN-MODE.md).
|
|
1604
1974
|
mode: payload.flags.mode,
|
|
@@ -2064,9 +2434,11 @@ export class Agent {
|
|
|
2064
2434
|
setLastProviderOptionsShape: (shape) => {
|
|
2065
2435
|
self._lastProviderOptionsShape = shape;
|
|
2066
2436
|
},
|
|
2437
|
+
getCompactionStats: () => self.getCompactionStats(),
|
|
2067
2438
|
getBatchClientOptions: (signal) => self.getBatchClientOptions(signal),
|
|
2068
2439
|
getCompactionSettings: (cw) => self.getCompactionSettings(cw),
|
|
2069
2440
|
compactForContext: (provider, system, cw, signal, settings, overflow) => self.compactForContext(provider, system, cw, signal, settings, overflow),
|
|
2441
|
+
extendHardCeilingForAutoCompaction: () => self.extendHardCeilingForAutoCompaction(),
|
|
2070
2442
|
postTurnCompact: (provider, system, cw, signal) => self.postTurnCompact(provider, system, cw, signal),
|
|
2071
2443
|
createTools: (bash, provider, mode, opts) => createTools(bash, provider, mode, opts),
|
|
2072
2444
|
runTask: (request, signal) => self.runTask(request, signal),
|
|
@@ -2175,7 +2547,16 @@ export class Agent {
|
|
|
2175
2547
|
else {
|
|
2176
2548
|
try {
|
|
2177
2549
|
const { classifySubSessionAction } = await import("../pil/llm-classify.js");
|
|
2178
|
-
|
|
2550
|
+
// Feed the recent-conversation digest so the router can actually honour
|
|
2551
|
+
// its "decide based on the conversation history" contract — otherwise a
|
|
2552
|
+
// continuation ("ok làm phần đó đi") is judged in isolation and can be
|
|
2553
|
+
// mis-routed as a fresh/unrelated task. Passing contextInfo also turns
|
|
2554
|
+
// on the session-size metadata block the ROTATE_SESSION rule relies on.
|
|
2555
|
+
const routeResult = await classifySubSessionAction(this.requireProvider(), this.modelId, userMessage, {
|
|
2556
|
+
currentChars,
|
|
2557
|
+
threshold,
|
|
2558
|
+
recentTurns: this._buildRecentTurnsSummary(),
|
|
2559
|
+
});
|
|
2179
2560
|
if (routeResult) {
|
|
2180
2561
|
routeAction = routeResult.action;
|
|
2181
2562
|
logger.info("orchestrator", "Routing action selected for user message", {
|
|
@@ -2189,6 +2570,18 @@ export class Agent {
|
|
|
2189
2570
|
logger.error("orchestrator", "Routing classification failed, falling back to DIRECT_ANSWER", { error: err });
|
|
2190
2571
|
}
|
|
2191
2572
|
}
|
|
2573
|
+
// Reactive escalation — override a DIRECT_ANSWER route (the router's blind
|
|
2574
|
+
// spot on read-heavy analysis, and its silent-degrade to DIRECT on classify
|
|
2575
|
+
// failure) when the PREVIOUS turn's observed tool load proves the session is
|
|
2576
|
+
// doing heavy multi-tool work. Deterministic, based on real execution, not a
|
|
2577
|
+
// prompt guess. Only rescues DIRECT_ANSWER — never hijacks a deliberate
|
|
2578
|
+
// ROTATE_SESSION. See reactive-delegation.ts.
|
|
2579
|
+
if (routeAction === "DIRECT_ANSWER" && shouldReactivelyEscalate(this._lastTurnToolChars)) {
|
|
2580
|
+
logger.info("orchestrator", "Reactive escalation to sub-session (prior turn tool-heavy)", {
|
|
2581
|
+
prevTurnToolChars: this._lastTurnToolChars,
|
|
2582
|
+
});
|
|
2583
|
+
routeAction = "SPAWN_SUB_SESSION";
|
|
2584
|
+
}
|
|
2192
2585
|
const shouldRotate = currentChars > threshold || routeAction === "ROTATE_SESSION";
|
|
2193
2586
|
if (shouldRotate && this.session && this.sessionStore) {
|
|
2194
2587
|
logger.info("orchestrator", "Triggering silent session rotation", {
|
|
@@ -2206,7 +2599,7 @@ export class Agent {
|
|
|
2206
2599
|
const cr = await deliberateCompact(flowDir, this.messages, "", 4096, this.requireProvider(), this.modelId);
|
|
2207
2600
|
const newSession = this.sessionStore.createSession(this.modelId, this.mode, this.bash.getCwd());
|
|
2208
2601
|
const db = getDatabase();
|
|
2209
|
-
|
|
2602
|
+
this.sessionStore.linkChild(newSession.id, parentSessionId, "rotation");
|
|
2210
2603
|
const summaryMessage = createCompactionSummaryMessage(cr.summary);
|
|
2211
2604
|
const nextSeq = getNextMessageSequence(newSession.id);
|
|
2212
2605
|
appendCompaction(newSession.id, nextSeq, cr.summary, cr.tokensBeforeCompress);
|
|
@@ -2229,10 +2622,10 @@ export class Agent {
|
|
|
2229
2622
|
let parentSessionId = null;
|
|
2230
2623
|
let subSessionId = null;
|
|
2231
2624
|
if (routeAction === "SPAWN_SUB_SESSION" && this.session && this.sessionStore) {
|
|
2232
|
-
yield { type: "
|
|
2625
|
+
yield { type: "toast", toastLevel: "info", content: "Đang khởi tạo sub-session ngầm để xử lý tác vụ..." };
|
|
2233
2626
|
parentSessionId = this.session.id;
|
|
2234
2627
|
try {
|
|
2235
|
-
const { loadLatestCompaction, getNextMessageSequence, appendCompaction
|
|
2628
|
+
const { loadLatestCompaction, getNextMessageSequence, appendCompaction } = await import("../storage/transcript.js");
|
|
2236
2629
|
const { getDatabase } = await import("../storage/db.js");
|
|
2237
2630
|
const db = getDatabase();
|
|
2238
2631
|
// Check if there is already an active child session for this parent session
|
|
@@ -2259,9 +2652,13 @@ export class Agent {
|
|
|
2259
2652
|
}
|
|
2260
2653
|
}
|
|
2261
2654
|
if (shouldResume && subSessionId) {
|
|
2262
|
-
yield {
|
|
2655
|
+
yield {
|
|
2656
|
+
type: "toast",
|
|
2657
|
+
toastLevel: "info",
|
|
2658
|
+
content: "Phát hiện sub-session trước đó bị gián đoạn, đang khôi phục...",
|
|
2659
|
+
};
|
|
2263
2660
|
this.session = this.sessionStore.getRequiredSession(subSessionId);
|
|
2264
|
-
const childState =
|
|
2661
|
+
const childState = loadSessionChainTranscriptState(subSessionId);
|
|
2265
2662
|
this.messages = childState.messages;
|
|
2266
2663
|
this.messageSeqs = childState.seqs;
|
|
2267
2664
|
this.sessionStore.touchSession(subSessionId, this.bash.getCwd());
|
|
@@ -2274,21 +2671,26 @@ export class Agent {
|
|
|
2274
2671
|
else {
|
|
2275
2672
|
const latest = loadLatestCompaction(parentSessionId);
|
|
2276
2673
|
const newSession = this.sessionStore.createSession(this.modelId, this.mode, this.bash.getCwd());
|
|
2277
|
-
|
|
2674
|
+
this.sessionStore.linkChild(newSession.id, parentSessionId, "subagent");
|
|
2278
2675
|
subSessionId = newSession.id;
|
|
2279
|
-
|
|
2280
|
-
|
|
2676
|
+
// Seed the child with the parent's CURRENT working set — after a
|
|
2677
|
+
// compaction that is already [summary, ...kept raw tail] (see the
|
|
2678
|
+
// compactForContext assignment). Previously the compaction branch
|
|
2679
|
+
// reseeded ONLY [summary], discarding the kept raw tail — so a forked
|
|
2680
|
+
// council/debate saw a bare generic summary instead of the actual
|
|
2681
|
+
// recent discussion the user pointed at ("debate THESE parts"), and
|
|
2682
|
+
// drifted off-topic. Keep the tail; just guarantee the summary is
|
|
2683
|
+
// present and persist the compaction row for the child's continuity.
|
|
2684
|
+
const seedMessages = [...this.messages];
|
|
2685
|
+
const seedSeqs = [...this.messageSeqs];
|
|
2281
2686
|
if (latest?.summary) {
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2687
|
+
if (!isCompactionSummaryMessage(seedMessages[0])) {
|
|
2688
|
+
seedMessages.unshift(createCompactionSummaryMessage(latest.summary));
|
|
2689
|
+
seedSeqs.unshift(null);
|
|
2690
|
+
}
|
|
2285
2691
|
const nextSeq = getNextMessageSequence(subSessionId);
|
|
2286
2692
|
appendCompaction(subSessionId, nextSeq, latest.summary, latest.tokensBefore);
|
|
2287
2693
|
}
|
|
2288
|
-
else {
|
|
2289
|
-
seedMessages = [...this.messages];
|
|
2290
|
-
seedSeqs = [...this.messageSeqs];
|
|
2291
|
-
}
|
|
2292
2694
|
// Add sub-session overlay system message
|
|
2293
2695
|
const overlayMessage = {
|
|
2294
2696
|
role: "system",
|
|
@@ -2322,6 +2724,22 @@ export class Agent {
|
|
|
2322
2724
|
const autoCommitOn = isAutoCommitEnabled();
|
|
2323
2725
|
const cwd = this.bash.getCwd();
|
|
2324
2726
|
const dirtyBefore = autoCommitOn ? await snapshotDirtyPaths(cwd) : new Set();
|
|
2727
|
+
// Top-level turn watchdog. The per-chunk stall watchdog only covers
|
|
2728
|
+
// streamText byte flow; it does NOT cover a turn generator that keeps the
|
|
2729
|
+
// socket open but never RETURNS after the model is done. Observed live:
|
|
2730
|
+
// xai/grok-composer-2.5-fast fires onFinish/llm-done (finishReason=stop) but
|
|
2731
|
+
// its stream generator never terminates, so processor.run() hangs, the UI's
|
|
2732
|
+
// `for await` never ends, finalizeActiveTurn never runs, and the TUI stays
|
|
2733
|
+
// frozen in the "processing" state (partial/raw markdown) until the next
|
|
2734
|
+
// message forces a new run. Mirrors the council-continuation guard (idle +
|
|
2735
|
+
// hard total). On fire we abort the turn and emit a terminal `done` so the
|
|
2736
|
+
// consumer finalizes cleanly instead of hanging forever. idleMs resets on
|
|
2737
|
+
// every yielded chunk, so long legitimate tool calls are safe; totalMs is a
|
|
2738
|
+
// hard ceiling (0 = disabled by default, since a big multi-tool turn is not a
|
|
2739
|
+
// hang). Env-overridable.
|
|
2740
|
+
const { withTurnWatchdog, TurnStallError } = await import("./turn-watchdog.js");
|
|
2741
|
+
const turnIdleMs = Number(process.env.MUONROI_TURN_IDLE_MS ?? 120_000);
|
|
2742
|
+
const turnTotalMs = Number(process.env.MUONROI_TURN_TOTAL_MS ?? 0);
|
|
2325
2743
|
try {
|
|
2326
2744
|
let attempts = 0;
|
|
2327
2745
|
const maxAttempts = 3;
|
|
@@ -2330,7 +2748,29 @@ export class Agent {
|
|
|
2330
2748
|
while (attempts < maxAttempts) {
|
|
2331
2749
|
try {
|
|
2332
2750
|
attempts++;
|
|
2333
|
-
|
|
2751
|
+
try {
|
|
2752
|
+
yield* withTurnWatchdog(processor.run(userMessage, observer, images), {
|
|
2753
|
+
idleMs: turnIdleMs,
|
|
2754
|
+
totalMs: turnTotalMs,
|
|
2755
|
+
label: "assistant turn",
|
|
2756
|
+
});
|
|
2757
|
+
}
|
|
2758
|
+
catch (stallErr) {
|
|
2759
|
+
// A hung turn is NOT a transient error — retrying it (below) would
|
|
2760
|
+
// just hang again. Abort, surface a toast, and terminate the turn.
|
|
2761
|
+
if (stallErr instanceof TurnStallError) {
|
|
2762
|
+
logger.warn("orchestrator", "Top-level turn watchdog fired — finalizing turn", {
|
|
2763
|
+
kind: stallErr.kind,
|
|
2764
|
+
message: stallErr.message,
|
|
2765
|
+
});
|
|
2766
|
+
this.abortController?.abort(new DOMException(stallErr.message, "TimeoutError"));
|
|
2767
|
+
yield { type: "toast", toastLevel: "warn", content: `Turn ended by watchdog: ${stallErr.message}` };
|
|
2768
|
+
yield { type: "done" };
|
|
2769
|
+
}
|
|
2770
|
+
else {
|
|
2771
|
+
throw stallErr;
|
|
2772
|
+
}
|
|
2773
|
+
}
|
|
2334
2774
|
break;
|
|
2335
2775
|
}
|
|
2336
2776
|
catch (err) {
|
|
@@ -2516,6 +2956,42 @@ export class Agent {
|
|
|
2516
2956
|
self._compactedThisTurn = v;
|
|
2517
2957
|
},
|
|
2518
2958
|
getCompactedThisTurn: () => self._compactedThisTurn,
|
|
2959
|
+
getCompactionStats: () => self.getCompactionStats(),
|
|
2960
|
+
reportTurnToolLoad: (chars) => {
|
|
2961
|
+
self._lastTurnToolChars = Number.isFinite(chars) && chars > 0 ? chars : 0;
|
|
2962
|
+
self._turnLoadOrdinal += 1;
|
|
2963
|
+
// Cold-first-turn instrument: record every turn whose load crosses the
|
|
2964
|
+
// reactive threshold, tagged with its ordinal. A later query counts what
|
|
2965
|
+
// fraction of threshold-crossing turns are ordinal 1 (the un-isolatable
|
|
2966
|
+
// cold turn) — the evidence that gates building an in-turn checkpoint.
|
|
2967
|
+
try {
|
|
2968
|
+
if (self.session && self._lastTurnToolChars >= getReactiveDelegationThresholdChars()) {
|
|
2969
|
+
// isolated = the heavy turn ran in a forked child (router SPAWN,
|
|
2970
|
+
// reactive escalation, or rotation), so the parent never held it.
|
|
2971
|
+
// The cold-first-turn HOLE is specifically coldFirstTurn && !isolated
|
|
2972
|
+
// — a turn-1 blow-up that landed in the parent because neither the
|
|
2973
|
+
// router nor reactive escalation caught it. Recording `isolated`
|
|
2974
|
+
// stops the metric from over-counting router-isolated heavy turns.
|
|
2975
|
+
const kind = self.sessionStore?.getSessionKind(self.session.id) ?? "conversation";
|
|
2976
|
+
const isolated = kind !== "conversation";
|
|
2977
|
+
logInteraction(self.session.id, "turn_tool_load", {
|
|
2978
|
+
data: {
|
|
2979
|
+
chars: self._lastTurnToolChars,
|
|
2980
|
+
ordinal: self._turnLoadOrdinal,
|
|
2981
|
+
coldFirstTurn: self._turnLoadOrdinal === 1,
|
|
2982
|
+
isolated,
|
|
2983
|
+
kind,
|
|
2984
|
+
threshold: getReactiveDelegationThresholdChars(),
|
|
2985
|
+
},
|
|
2986
|
+
});
|
|
2987
|
+
}
|
|
2988
|
+
}
|
|
2989
|
+
catch (err) {
|
|
2990
|
+
logger.debug("orchestrator", "turn_tool_load telemetry failed (best-effort)", {
|
|
2991
|
+
error: err?.message,
|
|
2992
|
+
});
|
|
2993
|
+
}
|
|
2994
|
+
},
|
|
2519
2995
|
setTurnUserGoalExcerpt: (v) => {
|
|
2520
2996
|
self._turnUserGoalExcerpt = v;
|
|
2521
2997
|
},
|
|
@@ -2562,7 +3038,7 @@ export class Agent {
|
|
|
2562
3038
|
model: runtime.model,
|
|
2563
3039
|
system: systemPrompt,
|
|
2564
3040
|
prompt: `Child Sub-session is stuck. Question:\n${question}`,
|
|
2565
|
-
|
|
3041
|
+
...resolveTemperatureParam(runtime, 0.2),
|
|
2566
3042
|
...(runtime.providerOptions ? { providerOptions: runtime.providerOptions } : {}),
|
|
2567
3043
|
});
|
|
2568
3044
|
return result.text;
|
|
@@ -2575,6 +3051,7 @@ export class Agent {
|
|
|
2575
3051
|
countFilesTouched: () => self._countFilesTouched(),
|
|
2576
3052
|
getCompactionSettings: (cw) => self.getCompactionSettings(cw),
|
|
2577
3053
|
compactForContext: (provider, system, cw, signal, settings, overflow) => self.compactForContext(provider, system, cw, signal, settings, overflow),
|
|
3054
|
+
extendHardCeilingForAutoCompaction: () => self.extendHardCeilingForAutoCompaction(),
|
|
2578
3055
|
postTurnCompact: (provider, system, cw, signal) => self.postTurnCompact(provider, system, cw, signal),
|
|
2579
3056
|
runTask: (request, signal) => self.runTask(request, signal),
|
|
2580
3057
|
runDelegation: (request, signal) => self.runDelegation(request, signal),
|
|
@@ -2633,20 +3110,31 @@ export class Agent {
|
|
|
2633
3110
|
const recent = this.messages.slice(-6);
|
|
2634
3111
|
const parts = [];
|
|
2635
3112
|
for (const msg of recent) {
|
|
2636
|
-
if (msg.role !== "user" && msg.role !== "assistant")
|
|
3113
|
+
if (msg.role !== "user" && msg.role !== "assistant" && msg.role !== "tool")
|
|
2637
3114
|
continue;
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
? msg.content
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
.
|
|
2645
|
-
|
|
3115
|
+
let text = "";
|
|
3116
|
+
if (msg.role === "tool") {
|
|
3117
|
+
text = Array.isArray(msg.content)
|
|
3118
|
+
? msg.content.map((p) => (typeof p.result === "string" ? p.result : JSON.stringify(p.result))).join(" ")
|
|
3119
|
+
: typeof msg.content === "string"
|
|
3120
|
+
? msg.content
|
|
3121
|
+
: JSON.stringify(msg.content);
|
|
3122
|
+
}
|
|
3123
|
+
else {
|
|
3124
|
+
text =
|
|
3125
|
+
typeof msg.content === "string"
|
|
3126
|
+
? msg.content
|
|
3127
|
+
: Array.isArray(msg.content)
|
|
3128
|
+
? msg.content
|
|
3129
|
+
.filter((p) => p.type === "text")
|
|
3130
|
+
.map((p) => p.text ?? "")
|
|
3131
|
+
.join("")
|
|
3132
|
+
: "";
|
|
3133
|
+
}
|
|
2646
3134
|
if (!text)
|
|
2647
3135
|
continue;
|
|
2648
|
-
const snippet = text.length >
|
|
2649
|
-
parts.push(`[${msg.role}]: ${snippet}`);
|
|
3136
|
+
const snippet = text.length > 300 ? `${text.slice(0, 297)}...` : text;
|
|
3137
|
+
parts.push(`[${msg.role}]: ${snippet.replace(/\n/g, " ")}`);
|
|
2650
3138
|
}
|
|
2651
3139
|
return parts.length > 0 ? parts.join(" | ") : null;
|
|
2652
3140
|
}
|
|
@@ -2716,48 +3204,109 @@ export class Agent {
|
|
|
2716
3204
|
if (this._oauthInitDone)
|
|
2717
3205
|
return;
|
|
2718
3206
|
this._oauthInitDone = true;
|
|
2719
|
-
//
|
|
2720
|
-
//
|
|
2721
|
-
//
|
|
2722
|
-
//
|
|
2723
|
-
//
|
|
2724
|
-
|
|
3207
|
+
// Re-resolve auth for the CURRENT provider. Runs when either:
|
|
3208
|
+
// (a) no explicit API key / the "oauth" sentinel is held — OAuth may
|
|
3209
|
+
// apply (the boot wizard in src/index.ts uses the literal "oauth" to
|
|
3210
|
+
// mean "no key but OAuth tokens exist"); OR
|
|
3211
|
+
// (b) setModel deferred provider construction on a provider switch
|
|
3212
|
+
// (this.provider === null) — the PREVIOUS provider's key was cleared
|
|
3213
|
+
// and must be re-resolved from the NEW provider's own credentials.
|
|
3214
|
+
const providerDeferred = this.provider === null;
|
|
3215
|
+
const keyIsSentinelOrEmpty = !this.apiKey || this.apiKey === "oauth";
|
|
3216
|
+
if (!providerDeferred && !keyIsSentinelOrEmpty)
|
|
2725
3217
|
return;
|
|
2726
3218
|
try {
|
|
2727
3219
|
const { listOAuthProviderIds } = await import("../providers/auth/registry.js");
|
|
2728
3220
|
const ids = await listOAuthProviderIds();
|
|
2729
|
-
|
|
3221
|
+
// OAuth path: the provider supports OAuth AND we don't hold a real API
|
|
3222
|
+
// key, so inject subscription tokens as Bearer headers.
|
|
3223
|
+
if (ids.includes(this.providerId) && keyIsSentinelOrEmpty) {
|
|
3224
|
+
const effectiveBaseURL = this.baseURL &&
|
|
3225
|
+
this.baseURL !== (await import("../providers/endpoints.js").then((m) => m.apiBaseFor("anthropic")))
|
|
3226
|
+
? this.baseURL
|
|
3227
|
+
: undefined;
|
|
3228
|
+
const result = await createProviderFactoryAsync(this.providerId, {
|
|
3229
|
+
baseURL: effectiveBaseURL ?? undefined,
|
|
3230
|
+
});
|
|
3231
|
+
this.apiKey = "oauth";
|
|
3232
|
+
this.provider = result.factory;
|
|
2730
3233
|
return;
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
3234
|
+
}
|
|
3235
|
+
// API-key path: resolve THIS provider's own stored key. Reached when the
|
|
3236
|
+
// provider isn't OAuth-capable, or when a provider switch deferred
|
|
3237
|
+
// construction (we must swap in the NEW provider's key — never reuse the
|
|
3238
|
+
// previous provider's, which 401'd on every TUI provider switch). If no
|
|
3239
|
+
// key exists for a deferred switch, leave the provider null so
|
|
3240
|
+
// requireProvider() surfaces a clear "API key required".
|
|
3241
|
+
try {
|
|
3242
|
+
const key = await loadKeyForProvider(this.providerId);
|
|
3243
|
+
if (key) {
|
|
3244
|
+
this.apiKey = key;
|
|
3245
|
+
const staleBaseURL = isAnyProviderApiBase(this.baseURL) && this.baseURL !== apiBaseFor(this.providerId);
|
|
3246
|
+
if (staleBaseURL)
|
|
3247
|
+
this.baseURL = null;
|
|
3248
|
+
this.provider = createProvider(this.providerId, key, this.baseURL ?? undefined);
|
|
3249
|
+
}
|
|
3250
|
+
else if (providerDeferred) {
|
|
3251
|
+
this.provider = null;
|
|
3252
|
+
}
|
|
3253
|
+
}
|
|
3254
|
+
catch (err) {
|
|
3255
|
+
// ProviderKeyMissingError (or keychain failure) — no usable key. For a
|
|
3256
|
+
// deferred provider switch, null the provider so the next turn fails
|
|
3257
|
+
// loudly ("API key required") instead of 401-ing on a wrong/sentinel
|
|
3258
|
+
// key. For a non-deferred empty-key case, leave the existing provider
|
|
3259
|
+
// untouched (fail-open, as before).
|
|
3260
|
+
if (providerDeferred) {
|
|
3261
|
+
console.error(`[orchestrator] no API key for provider '${this.providerId}' after model switch: ${err instanceof Error ? err.message : String(err)}`);
|
|
3262
|
+
this.provider = null;
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
2739
3265
|
}
|
|
2740
3266
|
catch {
|
|
2741
|
-
//
|
|
3267
|
+
// Registry unavailable — fail-open; the existing provider (if any) is
|
|
3268
|
+
// left untouched and requireProvider() will surface any error.
|
|
2742
3269
|
}
|
|
2743
3270
|
}
|
|
2744
3271
|
async detectVerifyRecipe(settings, abortSignal) {
|
|
3272
|
+
const cwd = this.bash.getCwd();
|
|
3273
|
+
const effectiveSettings = settings ?? this.bash.getSandboxSettings();
|
|
3274
|
+
// Primary: LLM verify-detect turn — a codebase-aware, richer recipe.
|
|
3275
|
+
let llmRecipe = null;
|
|
2745
3276
|
try {
|
|
2746
3277
|
const result = await this.runTaskRequest({
|
|
2747
3278
|
agent: "verify-detect",
|
|
2748
3279
|
description: "Detect verification recipe",
|
|
2749
|
-
prompt: buildVerifyDetectPrompt(
|
|
3280
|
+
prompt: buildVerifyDetectPrompt(cwd, effectiveSettings),
|
|
2750
3281
|
}, undefined, abortSignal);
|
|
2751
|
-
if (
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
return normalizeVerifyRecipe(JSON.parse(maybeJson));
|
|
3282
|
+
if (result.success && result.output) {
|
|
3283
|
+
const maybeJson = extractJsonObject(result.output);
|
|
3284
|
+
if (maybeJson)
|
|
3285
|
+
llmRecipe = normalizeVerifyRecipe(JSON.parse(maybeJson));
|
|
3286
|
+
}
|
|
2757
3287
|
}
|
|
2758
|
-
catch {
|
|
2759
|
-
|
|
3288
|
+
catch (err) {
|
|
3289
|
+
console.error(`[orchestrator] verify-detect turn failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
3290
|
+
}
|
|
3291
|
+
if (llmRecipe)
|
|
3292
|
+
return llmRecipe;
|
|
3293
|
+
// Deterministic fallback: an existing repo with a recognizable manifest
|
|
3294
|
+
// (package.json test script, *.sln, pyproject, …) yields a reliable recipe
|
|
3295
|
+
// WITHOUT an LLM. This stops CB-3 from false-halting an in-place /ideal
|
|
3296
|
+
// migration with the "Recovery options" card purely because the flaky
|
|
3297
|
+
// verify-detect model returned no parseable JSON (root cause of the
|
|
3298
|
+
// existing-repo implementation-reachability halt). Only trusted when the
|
|
3299
|
+
// profiler recognizes the ecosystem AND found real test commands.
|
|
3300
|
+
try {
|
|
3301
|
+
const profile = inferVerifyProjectProfile(cwd, effectiveSettings);
|
|
3302
|
+
if (shouldTrustDeterministicRecipe(profile.recipe)) {
|
|
3303
|
+
return profile.recipe;
|
|
3304
|
+
}
|
|
3305
|
+
}
|
|
3306
|
+
catch (err) {
|
|
3307
|
+
console.error(`[orchestrator] deterministic verify-recipe fallback failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
2760
3308
|
}
|
|
3309
|
+
return null;
|
|
2761
3310
|
}
|
|
2762
3311
|
async runVerify(onProgress, abortSignal) {
|
|
2763
3312
|
this.abortController = new AbortController();
|