muonroi-cli 1.8.4 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +17 -5
- 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-filter.js +11 -0
- package/dist/packages/agent-harness-core/src/event-redact.js +7 -0
- package/dist/packages/agent-harness-core/src/event-tee.d.ts +64 -0
- package/dist/packages/agent-harness-core/src/event-tee.js +104 -0
- package/dist/packages/agent-harness-core/src/mcp-server.d.ts +25 -0
- package/dist/packages/agent-harness-core/src/mcp-server.js +169 -21
- package/dist/packages/agent-harness-core/src/predicate.d.ts +1 -1
- package/dist/packages/agent-harness-core/src/protocol.d.ts +90 -4
- 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 +38 -0
- package/dist/src/agent-harness/mock-model.js +69 -3
- package/dist/src/agent-harness/test-spawn.js +31 -0
- package/dist/src/chat/chat-keychain.d.ts +7 -12
- package/dist/src/chat/chat-keychain.js +19 -86
- 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 +18 -3
- 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 +10 -47
- package/dist/src/cli/keys.js +31 -399
- package/dist/src/council/clarifier.d.ts +31 -3
- package/dist/src/council/clarifier.js +220 -32
- 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 +54 -5
- 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 +1265 -135
- package/dist/src/council/index.d.ts +108 -1
- package/dist/src/council/index.js +670 -197
- package/dist/src/council/leader.d.ts +26 -0
- package/dist/src/council/leader.js +150 -9
- package/dist/src/council/llm.d.ts +94 -0
- package/dist/src/council/llm.js +348 -55
- package/dist/src/council/panel-select.d.ts +30 -0
- package/dist/src/council/panel-select.js +82 -0
- package/dist/src/council/planner.js +40 -0
- package/dist/src/council/preflight.d.ts +17 -0
- package/dist/src/council/preflight.js +50 -2
- package/dist/src/council/prompts.d.ts +39 -4
- package/dist/src/council/prompts.js +256 -69
- 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 +138 -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 +20 -0
- package/dist/src/ee/auth.js +54 -2
- package/dist/src/ee/bridge.d.ts +10 -0
- package/dist/src/ee/bridge.js +58 -0
- package/dist/src/ee/client.js +109 -21
- package/dist/src/ee/ee-onboarding.js +6 -26
- 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 +58 -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 +14 -1
- package/dist/src/flow/compaction/index.js +96 -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/compaction/progress.d.ts +35 -0
- package/dist/src/flow/compaction/progress.js +35 -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 +66 -0
- package/dist/src/gsd/flags.js +102 -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 +283 -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/headless/council-answers.js +4 -0
- package/dist/src/hooks/index.js +1 -1
- package/dist/src/index.js +172 -270
- package/dist/src/lsp/builtins.js +3 -1
- package/dist/src/lsp/manager.d.ts +5 -1
- package/dist/src/lsp/manager.js +249 -3
- package/dist/src/lsp/npm-cache.d.ts +11 -1
- package/dist/src/lsp/npm-cache.js +17 -1
- package/dist/src/lsp/runtime.d.ts +6 -1
- package/dist/src/lsp/runtime.js +17 -1
- package/dist/src/lsp/types.d.ts +83 -1
- package/dist/src/lsp/types.js +10 -0
- 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 +44 -16
- package/dist/src/mcp/lsp-tools.d.ts +5 -1
- package/dist/src/mcp/lsp-tools.js +93 -2
- package/dist/src/mcp/mcp-keychain.d.ts +3 -5
- package/dist/src/mcp/mcp-keychain.js +9 -49
- 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 +22 -2
- package/dist/src/mcp/tools-server.d.ts +10 -0
- package/dist/src/mcp/tools-server.js +10 -2
- 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 +1 -1
- package/dist/src/orchestrator/ask-user.d.ts +61 -0
- package/dist/src/orchestrator/ask-user.js +65 -0
- 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 +12 -3
- package/dist/src/orchestrator/compaction.js +35 -15
- package/dist/src/orchestrator/council-manager.d.ts +12 -3
- package/dist/src/orchestrator/council-manager.js +74 -32
- package/dist/src/orchestrator/council-request.d.ts +49 -0
- package/dist/src/orchestrator/council-request.js +62 -0
- 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/interactive-pause.d.ts +26 -0
- package/dist/src/orchestrator/interactive-pause.js +36 -0
- package/dist/src/orchestrator/message-processor.d.ts +4 -0
- package/dist/src/orchestrator/message-processor.js +268 -41
- package/dist/src/orchestrator/orchestrator.d.ts +64 -3
- package/dist/src/orchestrator/orchestrator.js +823 -120
- package/dist/src/orchestrator/preprocessor.js +3 -3
- 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 +17 -17
- 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-askcard.d.ts +1 -1
- package/dist/src/orchestrator/safety-askcard.js +5 -2
- package/dist/src/orchestrator/safety-intercept.d.ts +50 -0
- package/dist/src/orchestrator/safety-intercept.js +62 -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 +31 -3
- package/dist/src/orchestrator/stall-watchdog.js +65 -10
- package/dist/src/orchestrator/stream-runner.d.ts +13 -3
- package/dist/src/orchestrator/stream-runner.js +115 -49
- 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 +41 -0
- package/dist/src/orchestrator/tool-engine.js +846 -66
- 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 +44 -0
- package/dist/src/orchestrator/turn-watchdog.js +84 -0
- package/dist/src/pil/agent-operating-contract.d.ts +1 -1
- package/dist/src/pil/agent-operating-contract.js +6 -4
- 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.d.ts +1 -1
- package/dist/src/pil/discovery.js +18 -13
- 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/layer1_5-complexity-size.d.ts +7 -0
- package/dist/src/pil/layer1_5-complexity-size.js +31 -5
- 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 +111 -5
- package/dist/src/pil/llm-classify.js +421 -189
- package/dist/src/pil/native-capabilities-workbook.d.ts +1 -1
- package/dist/src/pil/native-capabilities-workbook.js +8 -0
- package/dist/src/pil/pipeline.js +36 -2
- package/dist/src/pil/repo-grounding-probe.d.ts +15 -0
- package/dist/src/pil/repo-grounding-probe.js +136 -0
- package/dist/src/pil/repo-structure-hints.d.ts +7 -0
- package/dist/src/pil/repo-structure-hints.js +45 -0
- 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/artifact-io.js +4 -0
- 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/criteria-seed.d.ts +51 -0
- package/dist/src/product-loop/criteria-seed.js +200 -0
- 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.d.ts +9 -0
- package/dist/src/product-loop/discovery-interview.js +60 -12
- package/dist/src/product-loop/discovery-recommender.js +2 -1
- package/dist/src/product-loop/discovery-schema.js +19 -2
- package/dist/src/product-loop/discovery-triage.d.ts +23 -0
- package/dist/src/product-loop/discovery-triage.js +109 -0
- package/dist/src/product-loop/gather.js +150 -2
- 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 +340 -52
- package/dist/src/product-loop/loop-driver.d.ts +7 -0
- package/dist/src/product-loop/loop-driver.js +330 -106
- package/dist/src/product-loop/phase-plan.d.ts +21 -0
- package/dist/src/product-loop/phase-plan.js +81 -6
- package/dist/src/product-loop/phase-rituals.d.ts +3 -0
- package/dist/src/product-loop/phase-rituals.js +8 -3
- package/dist/src/product-loop/phase-runner.js +39 -12
- package/dist/src/product-loop/plan-adherence-review.d.ts +26 -0
- package/dist/src/product-loop/plan-adherence-review.js +144 -0
- package/dist/src/product-loop/sprint-runner.d.ts +173 -0
- package/dist/src/product-loop/sprint-runner.js +863 -19
- package/dist/src/product-loop/types.d.ts +61 -5
- package/dist/src/providers/adapter.d.ts +1 -1
- package/dist/src/providers/adapter.js +3 -4
- package/dist/src/providers/anthropic.d.ts +9 -8
- package/dist/src/providers/anthropic.js +13 -47
- 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/grok-oauth.d.ts +1 -0
- package/dist/src/providers/auth/grok-oauth.js +30 -5
- package/dist/src/providers/auth/openai-oauth.d.ts +1 -0
- package/dist/src/providers/auth/openai-oauth.js +15 -1
- package/dist/src/providers/auth/registry.js +0 -34
- package/dist/src/providers/auth/token-store.d.ts +9 -9
- package/dist/src/providers/auth/token-store.js +8 -67
- package/dist/src/providers/auth/types.d.ts +9 -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/env-store.d.ts +17 -0
- package/dist/src/providers/env-store.js +228 -0
- package/dist/src/providers/keychain.d.ts +22 -18
- package/dist/src/providers/keychain.js +127 -140
- package/dist/src/providers/mcp-vision-bridge.js +56 -146
- 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 +43 -3
- package/dist/src/providers/runtime.js +88 -14
- 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 +288 -1
- package/dist/src/providers/strategies/xai.strategy.js +27 -0
- 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/warm.d.ts +65 -0
- package/dist/src/providers/warm.js +145 -0
- package/dist/src/providers/wire-debug.js +95 -0
- 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/continuation-prompt.d.ts +11 -0
- package/dist/src/scaffold/continuation-prompt.js +26 -0
- 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 +6 -5
- 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/migrations.js +71 -1
- package/dist/src/storage/sessions.d.ts +28 -10
- package/dist/src/storage/sessions.js +78 -21
- package/dist/src/storage/transcript-view.js +1 -1
- package/dist/src/storage/transcript.d.ts +51 -0
- package/dist/src/storage/transcript.js +340 -15
- package/dist/src/tools/file.d.ts +15 -0
- package/dist/src/tools/file.js +32 -0
- package/dist/src/tools/git-safety.d.ts +19 -0
- package/dist/src/tools/git-safety.js +168 -0
- package/dist/src/tools/native-tools.d.ts +1 -1
- package/dist/src/tools/native-tools.js +81 -1
- package/dist/src/tools/registry.d.ts +20 -0
- package/dist/src/tools/registry.js +576 -23
- package/dist/src/tools/research.d.ts +29 -0
- package/dist/src/tools/research.js +233 -0
- package/dist/src/types/index.d.ts +147 -4
- package/dist/src/ui/app.js +0 -0
- package/dist/src/ui/cards/product-status-card.js +1 -1
- package/dist/src/ui/components/agent-rail-activities.d.ts +26 -0
- package/dist/src/ui/components/agent-rail-activities.js +47 -0
- 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/compact-progress-card.d.ts +24 -0
- package/dist/src/ui/components/compact-progress-card.js +42 -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 +66 -17
- 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 +13 -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/message-view.d.ts +15 -0
- package/dist/src/ui/components/message-view.js +50 -1
- 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/components/tool-group.d.ts +15 -3
- package/dist/src/ui/components/tool-group.js +69 -11
- package/dist/src/ui/containers/modals-layer.d.ts +4 -2
- package/dist/src/ui/containers/modals-layer.js +2 -2
- package/dist/src/ui/council-harness-event.d.ts +57 -0
- package/dist/src/ui/council-harness-event.js +46 -0
- package/dist/src/ui/heartbeat-debug.d.ts +29 -0
- package/dist/src/ui/heartbeat-debug.js +45 -0
- 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/model-picker-modal.d.ts +8 -18
- package/dist/src/ui/modals/model-picker-modal.js +8 -10
- 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.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/ee.js +81 -0
- 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 +23 -20
- 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/agent-activities.d.ts +39 -0
- package/dist/src/ui/utils/agent-activities.js +96 -0
- package/dist/src/ui/utils/format.d.ts +14 -0
- package/dist/src/ui/utils/format.js +23 -3
- package/dist/src/ui/utils/group-tool-entries.d.ts +26 -0
- package/dist/src/ui/utils/group-tool-entries.js +111 -0
- package/dist/src/ui/utils/tool-summary.d.ts +21 -0
- package/dist/src/ui/utils/tool-summary.js +91 -0
- package/dist/src/usage/downgrade.js +2 -2
- package/dist/src/usage/product-ledger.js +2 -2
- package/dist/src/utils/event-loop-monitor.d.ts +85 -0
- package/dist/src/utils/event-loop-monitor.js +107 -0
- package/dist/src/utils/install-manager.js +2 -1
- package/dist/src/utils/llm-deadline.d.ts +14 -0
- package/dist/src/utils/llm-deadline.js +19 -0
- package/dist/src/utils/logger.js +2 -2
- package/dist/src/utils/loop-profiler.d.ts +102 -0
- package/dist/src/utils/loop-profiler.js +202 -0
- package/dist/src/utils/permission-mode.js +5 -3
- package/dist/src/utils/redactor.js +1 -1
- package/dist/src/utils/settings.d.ts +180 -5
- package/dist/src/utils/settings.js +271 -31
- package/dist/src/utils/side-question.d.ts +1 -2
- package/dist/src/utils/side-question.js +2 -2
- 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 +52 -17
- package/dist/src/verify/orchestrator.d.ts +1 -1
- package/dist/src/verify/orchestrator.js +20 -3
- package/dist/src/verify/recipes.d.ts +13 -0
- package/dist/src/verify/recipes.js +15 -0
- package/package.json +134 -132
- package/dist/src/cli/bw-vault.d.ts +0 -55
- package/dist/src/cli/bw-vault.js +0 -133
- package/dist/src/mcp/ee-tools.d.ts +0 -46
- package/dist/src/mcp/ee-tools.js +0 -193
- 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,39 +13,44 @@ 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
|
|
16
|
+
import { createProviderFactory, createProviderFactoryAsync, detectProviderForModel, requireRuntimeProvider, resolveModelRuntime, 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";
|
|
33
|
+
import { ASK_USER_DISMISSED } from "./ask-user.js";
|
|
32
34
|
import { AUTO_COMMIT_ATTRIBUTION, isAutoCommitEnabled, maybeAutoCommitTurn, snapshotDirtyPaths, } from "./auto-commit.js";
|
|
33
35
|
import { BatchTurnRunner } from "./batch-turn-runner.js";
|
|
34
36
|
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";
|
|
37
|
+
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
38
|
import { CouncilManager } from "./council-manager.js";
|
|
37
39
|
import { CrossTurnDedup, isCrossTurnDedupEnabled } from "./cross-turn-dedup.js";
|
|
38
40
|
import { DelegationManager } from "./delegations.js";
|
|
39
41
|
import { loadFlowResumeDigest } from "./flow-resume.js";
|
|
42
|
+
import { beginInteractivePause, endInteractivePause, isInteractivePaused } from "./interactive-pause.js";
|
|
40
43
|
import { MessageProcessor } from "./message-processor.js";
|
|
41
44
|
import { lastPersistedSeq } from "./message-seq.js";
|
|
42
45
|
import { buildSystemPrompt, HARD_MAX_TOOL_ROUNDS, MAX_TOOL_ROUNDS } from "./prompts.js";
|
|
46
|
+
import { getReactiveDelegationThresholdChars, shouldReactivelyEscalate } from "./reactive-delegation.js";
|
|
43
47
|
import { getReadPathBudgetCap, ReadPathBudget } from "./read-path-budget.js";
|
|
44
48
|
import { withStreamRetry } from "./retry-stream.js";
|
|
45
49
|
import { StreamRunner } from "./stream-runner.js";
|
|
46
50
|
import { resolveModelForTask } from "./sub-agent-model-tier.js";
|
|
51
|
+
import { compactSubAgentMessages } from "./subagent-compactor.js";
|
|
47
52
|
import { setProviderHint } from "./token-counter.js";
|
|
53
|
+
import { getToolLimitAutoRecoverCap } from "./tool-limit-auto-recover.js";
|
|
48
54
|
import { firstLine, formatSubagentActivity, toToolResult } from "./tool-utils.js";
|
|
49
55
|
// ---------------------------------------------------------------------------
|
|
50
56
|
// Provider implementations
|
|
@@ -69,22 +75,65 @@ function isAnyProviderApiBase(url) {
|
|
|
69
75
|
}
|
|
70
76
|
return false;
|
|
71
77
|
}
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
78
|
+
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.
|
|
79
|
+
|
|
80
|
+
Rules:
|
|
81
|
+
- 5-7 words that capture the essence of the request (the goal, not the phrasing).
|
|
82
|
+
- Same language as the user's message (English request → English title, Vietnamese → Vietnamese, etc.).
|
|
83
|
+
- Title Case for languages that use it; natural casing otherwise.
|
|
84
|
+
- No surrounding quotes, no trailing punctuation, no emoji, no markdown.
|
|
85
|
+
- Output ONLY the title text — nothing else.`;
|
|
86
|
+
/** Deterministic fallback title: truncated first user message, or "" when the
|
|
87
|
+
* message is empty or a JSON-ish payload (so the caller leaves title = NULL
|
|
88
|
+
* and the picker renders a clean "(untitled)" instead of a literal "{}"). */
|
|
89
|
+
export function fallbackTitle(userMessage) {
|
|
90
|
+
const trimmed = userMessage.trim();
|
|
91
|
+
if (!trimmed)
|
|
92
|
+
return "";
|
|
93
|
+
// JSON object/array payloads (programmatic first messages) make useless titles.
|
|
94
|
+
if (/^[{[]/.test(trimmed) && /[}\]]$/.test(trimmed))
|
|
95
|
+
return "";
|
|
96
|
+
return trimmed.slice(0, 60).trim();
|
|
97
|
+
}
|
|
98
|
+
/** Strip quotes/backticks, collapse whitespace, drop trailing punctuation, cap at ~60 chars. */
|
|
99
|
+
function sanitizeTitle(raw) {
|
|
100
|
+
let t = raw.trim().split("\n")[0]?.trim() ?? "";
|
|
101
|
+
t = t.replace(/^["'`“”‘’]+|["'`“”‘’]+$/g, "");
|
|
102
|
+
t = t
|
|
103
|
+
.replace(/\s+/g, " ")
|
|
104
|
+
.replace(/[.,:;!?…]+$/g, "")
|
|
105
|
+
.trim();
|
|
106
|
+
if (t.length > 60)
|
|
107
|
+
t = t.slice(0, 60).trim();
|
|
108
|
+
return t;
|
|
81
109
|
}
|
|
82
110
|
/**
|
|
83
|
-
*
|
|
84
|
-
*
|
|
111
|
+
* Generate a session title with a single lightweight LLM call on the current
|
|
112
|
+
* session model. Falls back to a truncated first-message title on any failure.
|
|
85
113
|
*/
|
|
86
|
-
function
|
|
87
|
-
|
|
114
|
+
async function genTitle(userMessage, modelId) {
|
|
115
|
+
try {
|
|
116
|
+
const { generateText } = await import("ai");
|
|
117
|
+
const runtime = resolveModelRuntime(modelId);
|
|
118
|
+
const snippet = userMessage.length > 1500 ? `${userMessage.slice(0, 1500)}…` : userMessage;
|
|
119
|
+
const { text, usage } = await generateText({
|
|
120
|
+
model: runtime.model,
|
|
121
|
+
system: TITLE_SYSTEM_PROMPT,
|
|
122
|
+
prompt: `User's first message:\n\n${snippet}\n\nTitle:`,
|
|
123
|
+
...resolveTemperatureParam(runtime, 0.3),
|
|
124
|
+
...(runtime.modelInfo?.supportsMaxOutputTokens === false ? {} : { maxOutputTokens: 64 }),
|
|
125
|
+
...(runtime.providerOptions ? { providerOptions: runtime.providerOptions } : {}),
|
|
126
|
+
});
|
|
127
|
+
const title = sanitizeTitle(text ?? "");
|
|
128
|
+
if (title) {
|
|
129
|
+
return { title, modelId, usage };
|
|
130
|
+
}
|
|
131
|
+
console.error(`[orchestrator/genTitle] title generation returned empty text (model=${modelId}); using fallback`);
|
|
132
|
+
}
|
|
133
|
+
catch (err) {
|
|
134
|
+
console.error(`[orchestrator/genTitle] title generation failed (model=${modelId}): ${err?.message}; using fallback`);
|
|
135
|
+
}
|
|
136
|
+
return { title: fallbackTitle(userMessage), modelId };
|
|
88
137
|
}
|
|
89
138
|
async function toolSetToBatchTools(_tools) {
|
|
90
139
|
// Batch API not supported with Anthropic in Phase 0. Phase 1 may add this.
|
|
@@ -110,6 +159,7 @@ function createTools(_bash, _provider, _mode, _opts) {
|
|
|
110
159
|
listDelegations: _opts?.listDelegations,
|
|
111
160
|
killDelegation: _opts?.killDelegation,
|
|
112
161
|
modelId: _opts?.modelId,
|
|
162
|
+
sessionId: _opts?.sessionId,
|
|
113
163
|
});
|
|
114
164
|
}
|
|
115
165
|
// ---------------------------------------------------------------------------
|
|
@@ -138,6 +188,8 @@ export class Agent {
|
|
|
138
188
|
steerDrain = null;
|
|
139
189
|
maxToolRounds;
|
|
140
190
|
hardMaxToolRounds;
|
|
191
|
+
/** Original hard ceiling at construction — bounds auto-compaction extension. */
|
|
192
|
+
_initialHardMaxToolRounds = 0;
|
|
141
193
|
mode = "agent";
|
|
142
194
|
modelId;
|
|
143
195
|
maxTokens;
|
|
@@ -196,6 +248,9 @@ export class Agent {
|
|
|
196
248
|
_turnUserGoalExcerpt = "";
|
|
197
249
|
/** Compaction statistics tracking count and total tokens saved. */
|
|
198
250
|
_compactionStats = { count: 0, totalSaved: 0 };
|
|
251
|
+
/** Token count immediately after the last successful compaction (or null if none yet this
|
|
252
|
+
* session). Used by the cross-turn anti-thrash guard in postTurnCompact. */
|
|
253
|
+
_lastCompactionTokensAfter = null;
|
|
199
254
|
/**
|
|
200
255
|
* Pinned message sequences. A pinned user message is preserved verbatim across
|
|
201
256
|
* compaction — it is re-injected as a system note immediately after the
|
|
@@ -208,6 +263,15 @@ export class Agent {
|
|
|
208
263
|
// Phase C3: cross-turn tool-output dedup. One instance per session; bumped
|
|
209
264
|
// on each user turn. Lazily initialized so disabled-via-env path stays cheap.
|
|
210
265
|
_crossTurnDedup = isCrossTurnDedupEnabled() ? new CrossTurnDedup() : null;
|
|
266
|
+
// Reactive sub-session escalation: the PREVIOUS turn's cumulative tool-output
|
|
267
|
+
// chars (from the top-level cap), reported by the tool engine at turn end and
|
|
268
|
+
// read at the next turn's route decision. See reactive-delegation.ts.
|
|
269
|
+
_lastTurnToolChars = 0;
|
|
270
|
+
// Instrument (cold-first-turn measurement): ordinal of the top-level turn whose
|
|
271
|
+
// tool load was last reported. Lets telemetry distinguish a turn-1 blow-up
|
|
272
|
+
// (which reactive escalation CANNOT isolate) from a later one (which it can).
|
|
273
|
+
// Gates the decision to build an in-turn checkpoint. See CONTEXT-CONTROL-LAYERS.md.
|
|
274
|
+
_turnLoadOrdinal = 0;
|
|
211
275
|
// Phase C4 — input-keyed read-path budget. Complements C3 (output hash) by
|
|
212
276
|
// catching re-reads of files the agent edited between rounds. Disabled
|
|
213
277
|
// when MUONROI_MAX_READS_PER_PATH=0.
|
|
@@ -246,6 +310,10 @@ export class Agent {
|
|
|
246
310
|
this.maxToolRounds = maxToolRounds || settings.maxToolRounds || MAX_TOOL_ROUNDS;
|
|
247
311
|
const baseHardMax = settings.hardMaxToolRounds || HARD_MAX_TOOL_ROUNDS;
|
|
248
312
|
this.hardMaxToolRounds = Math.max(Math.floor(this.maxToolRounds * 1.5), baseHardMax);
|
|
313
|
+
// Baseline captured so auto-compaction ceiling extension (see
|
|
314
|
+
// extendHardCeilingForAutoCompaction) is bounded relative to the ORIGINAL
|
|
315
|
+
// hard cap and cannot grow without limit across a runaway turn.
|
|
316
|
+
this._initialHardMaxToolRounds = this.hardMaxToolRounds;
|
|
249
317
|
const envMax = Number(process.env.MUONROI_MAX_TOKENS);
|
|
250
318
|
this.maxTokens = Number.isFinite(envMax) && envMax > 0 ? envMax : 16_384;
|
|
251
319
|
this.batchApi = options.batchApi ?? false;
|
|
@@ -280,7 +348,7 @@ export class Agent {
|
|
|
280
348
|
this.workspace = this.sessionStore.getWorkspace();
|
|
281
349
|
this.session = this.sessionStore.openSession(options.session, this.modelId, this.mode, this.bash.getCwd());
|
|
282
350
|
this.mode = this.session.mode;
|
|
283
|
-
const transcript =
|
|
351
|
+
const transcript = loadSessionChainTranscriptState(this.session.id);
|
|
284
352
|
this.messages = transcript.messages;
|
|
285
353
|
this.messageSeqs = transcript.seqs;
|
|
286
354
|
this.sessionStore.setModel(this.session.id, this.modelId);
|
|
@@ -330,30 +398,58 @@ export class Agent {
|
|
|
330
398
|
getActiveRunId() {
|
|
331
399
|
return this._activeRunId;
|
|
332
400
|
}
|
|
333
|
-
|
|
401
|
+
/**
|
|
402
|
+
* Point the agent at `model`, re-resolving the provider when the new model
|
|
403
|
+
* belongs to a different one.
|
|
404
|
+
*
|
|
405
|
+
* EVERY path that changes `this.modelId` must go through here. Changing the
|
|
406
|
+
* id alone leaves `providerId` / `provider` / `baseURL` / `apiKey` describing
|
|
407
|
+
* the PREVIOUS provider, so the next turn POSTs the new model to the old
|
|
408
|
+
* provider's endpoint — which rejects it as unknown (session 0c6728ba1a25:
|
|
409
|
+
* `gpt-5.4` sent to api.x.ai → 404 "The model gpt-5.4 does not exist", a
|
|
410
|
+
* wiring bug that reads like a bad model name). `setMode` used to do exactly
|
|
411
|
+
* that; it now calls this too.
|
|
412
|
+
*
|
|
413
|
+
* Session persistence is NOT done here — callers own that, because setMode
|
|
414
|
+
* writes mode+model together.
|
|
415
|
+
*/
|
|
416
|
+
_applyModelId(model) {
|
|
334
417
|
this.modelId = normalizeModelId(model);
|
|
335
418
|
const newProviderId = detectProviderForModel(this.modelId);
|
|
336
|
-
if (newProviderId !== this.providerId
|
|
419
|
+
if (newProviderId !== this.providerId) {
|
|
337
420
|
this.providerId = newProviderId;
|
|
338
421
|
setProviderHint(this.providerId);
|
|
339
422
|
// Drop this.baseURL when it points at a DIFFERENT provider's default
|
|
340
423
|
// apiBase — otherwise the rebuilt factory binds the new provider's
|
|
341
424
|
// strategy to the OLD provider's URL, sending requests to the wrong
|
|
342
425
|
// 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".
|
|
426
|
+
// (via UI), this.baseURL was still the old provider's apiBase from
|
|
427
|
+
// startup, the rebuilt strategy factory was created with that stale
|
|
428
|
+
// baseURL, and requests landed at the wrong host which rejected the
|
|
429
|
+
// model id.
|
|
348
430
|
// A user-supplied custom baseURL is preserved only when it does NOT
|
|
349
431
|
// match any known provider's apiBase (i.e. it's a real override, not
|
|
350
432
|
// a stale default).
|
|
351
433
|
const staleBaseURL = isAnyProviderApiBase(this.baseURL) && this.baseURL !== apiBaseFor(this.providerId);
|
|
352
|
-
const effectiveBaseURL = staleBaseURL ? undefined : (this.baseURL ?? undefined);
|
|
353
434
|
if (staleBaseURL)
|
|
354
435
|
this.baseURL = null;
|
|
355
|
-
|
|
436
|
+
// Provider changed — DEFER factory construction. The current this.apiKey
|
|
437
|
+
// belongs to the PREVIOUS provider (or is the "oauth" OAuth sentinel) and
|
|
438
|
+
// is invalid for the new one. Rebuilding here with it sent provider A's
|
|
439
|
+
// key to provider B → 401 "Authentication Fails, your api key … is
|
|
440
|
+
// invalid" on the next chat, for EVERY TUI provider switch (evidence:
|
|
441
|
+
// routing log routed to "deepseek-v4-flash via deepseek" with a
|
|
442
|
+
// non-deepseek key; and the "oauth" leak in session ff932f8568e8). Clear
|
|
443
|
+
// the stale key, null the provider, and re-arm _initOAuthProvider, which
|
|
444
|
+
// resolves the NEW provider's own key (or OAuth tokens) before the next
|
|
445
|
+
// turn (message-processor calls initOAuthProvider at turn start).
|
|
446
|
+
this.apiKey = null;
|
|
447
|
+
this.provider = null;
|
|
448
|
+
this._oauthInitDone = false;
|
|
356
449
|
}
|
|
450
|
+
}
|
|
451
|
+
setModel(model) {
|
|
452
|
+
this._applyModelId(model);
|
|
357
453
|
if (this.sessionStore && this.session) {
|
|
358
454
|
this.sessionStore.setModel(this.session.id, this.modelId);
|
|
359
455
|
this.session = this.sessionStore.getRequiredSession(this.session.id);
|
|
@@ -381,7 +477,11 @@ export class Agent {
|
|
|
381
477
|
this.mode = mode;
|
|
382
478
|
const modeModel = getModeSpecificModel(mode);
|
|
383
479
|
if (modeModel) {
|
|
384
|
-
this.modelId =
|
|
480
|
+
// Via _applyModelId, NOT a bare `this.modelId = ...`: a mode-specific
|
|
481
|
+
// model can belong to a different provider than the session's, and
|
|
482
|
+
// assigning the id alone would leave provider/baseURL/apiKey pointing at
|
|
483
|
+
// the old one — the cross-wire class in _applyModelId's docstring.
|
|
484
|
+
this._applyModelId(modeModel);
|
|
385
485
|
}
|
|
386
486
|
if (this.sessionStore && this.session) {
|
|
387
487
|
this.sessionStore.setMode(this.session.id, mode);
|
|
@@ -392,6 +492,17 @@ export class Agent {
|
|
|
392
492
|
}
|
|
393
493
|
setPlanContext(ctx) {
|
|
394
494
|
this.planContext = ctx;
|
|
495
|
+
// Persist so that bare continuation phrases ("tiếp tục", "continue") after
|
|
496
|
+
// interrupt or cross-process resume can re-hydrate the APPROVED PLAN section
|
|
497
|
+
// instead of forgetting context and re-asking for details.
|
|
498
|
+
if (this.session && ctx) {
|
|
499
|
+
try {
|
|
500
|
+
persistApprovedPlan(this.session.id, ctx);
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
/* best-effort */
|
|
504
|
+
}
|
|
505
|
+
}
|
|
395
506
|
}
|
|
396
507
|
setSendTelegramFile(fn) {
|
|
397
508
|
this.sendTelegramFile = fn;
|
|
@@ -403,7 +514,7 @@ export class Agent {
|
|
|
403
514
|
this.apiKey = apiKey;
|
|
404
515
|
// Drop baseURL when it points at a DIFFERENT provider's default apiBase
|
|
405
516
|
// (e.g. caller passed the legacy anthropic URL while providerId is
|
|
406
|
-
//
|
|
517
|
+
// deepseek — without this we'd send deepseek requests to
|
|
407
518
|
// api.anthropic.com or similar). User-supplied custom URLs that don't
|
|
408
519
|
// match any known provider's apiBase are preserved as real overrides.
|
|
409
520
|
const stale = isAnyProviderApiBase(baseURL) && baseURL !== apiBaseFor(this.providerId);
|
|
@@ -419,6 +530,14 @@ export class Agent {
|
|
|
419
530
|
getProviderId() {
|
|
420
531
|
return this.providerId;
|
|
421
532
|
}
|
|
533
|
+
/**
|
|
534
|
+
* Expose the provider factory for external callers (e.g., /compact slash command,
|
|
535
|
+
* sub-agent spawning) that need to make LLM calls outside the normal processMessage flow.
|
|
536
|
+
* Throws if no provider is configured.
|
|
537
|
+
*/
|
|
538
|
+
getProvider() {
|
|
539
|
+
return this.requireProvider();
|
|
540
|
+
}
|
|
422
541
|
getCwd() {
|
|
423
542
|
return this.bash.getCwd();
|
|
424
543
|
}
|
|
@@ -432,6 +551,9 @@ export class Agent {
|
|
|
432
551
|
setMessages(messages) {
|
|
433
552
|
this.messages = messages;
|
|
434
553
|
this.messageSeqs = messages.map(() => null);
|
|
554
|
+
// Manual compaction (/compact) or /clear replaces the conversation; reset
|
|
555
|
+
// the per-turn auto-compact latch so future tool loops can compact again.
|
|
556
|
+
this._compactedThisTurn = false;
|
|
435
557
|
}
|
|
436
558
|
async listSchedules() {
|
|
437
559
|
return this.schedules.list();
|
|
@@ -460,7 +582,7 @@ export class Agent {
|
|
|
460
582
|
if (!provider) {
|
|
461
583
|
return "New session";
|
|
462
584
|
}
|
|
463
|
-
const generated = await genTitle(
|
|
585
|
+
const generated = await genTitle(userMessage, this.modelId);
|
|
464
586
|
this.recordUsage(generated.usage, "title", generated.modelId);
|
|
465
587
|
if (this.sessionStore && this.session && !this.session.title && generated.title) {
|
|
466
588
|
this.sessionStore.setTitle(this.session.id, generated.title);
|
|
@@ -493,7 +615,7 @@ export class Agent {
|
|
|
493
615
|
charBudget -= snippet.length;
|
|
494
616
|
}
|
|
495
617
|
const conversationContext = contextParts.join("\n\n");
|
|
496
|
-
const result = await runSideQuestion(question, this.
|
|
618
|
+
const result = await runSideQuestion(question, this.modelId, conversationContext, signal);
|
|
497
619
|
this.recordUsage(result.usage, "other");
|
|
498
620
|
return result;
|
|
499
621
|
}
|
|
@@ -530,6 +652,22 @@ export class Agent {
|
|
|
530
652
|
setToolLoopCapHandler(fn) {
|
|
531
653
|
this._toolLoopCapHandler = fn;
|
|
532
654
|
}
|
|
655
|
+
/**
|
|
656
|
+
* Extend the absolute hard-cap ceiling when the turn is being sustained by
|
|
657
|
+
* AUTO-COMPACTION (a productive long task, not a runaway). Rationale: each
|
|
658
|
+
* auto-compacted round resets context to O(N) input, so it is cheap — the
|
|
659
|
+
* hard cap's cost-runaway purpose is already served by the compaction. We
|
|
660
|
+
* grant `maxToolRounds/2` more headroom per compaction, bounded to at most
|
|
661
|
+
* one extra auto-recover budget's worth above the ORIGINAL ceiling so a
|
|
662
|
+
* genuinely wedged turn (which trips the pattern guard, not this path) can
|
|
663
|
+
* never grow the cap without limit. Called from the tool-engine auto-recover
|
|
664
|
+
* branch after a successful compaction.
|
|
665
|
+
*/
|
|
666
|
+
extendHardCeilingForAutoCompaction() {
|
|
667
|
+
const bump = Math.max(25, Math.floor(this.maxToolRounds / 2));
|
|
668
|
+
const absoluteMax = this._initialHardMaxToolRounds + bump * getToolLimitAutoRecoverCap();
|
|
669
|
+
this.hardMaxToolRounds = Math.min(this.hardMaxToolRounds + bump, absoluteMax);
|
|
670
|
+
}
|
|
533
671
|
// Safety-override handler — set by the UI (app.tsx) at startup. Invoked
|
|
534
672
|
// from the message-processor tool-result pipeline when a safety block
|
|
535
673
|
// (catastrophic, dangerous, git-safety, empty-bash) is detected. The UI
|
|
@@ -539,6 +677,15 @@ export class Agent {
|
|
|
539
677
|
setSafetyOverrideHandler(fn) {
|
|
540
678
|
this._safetyOverrideHandler = fn;
|
|
541
679
|
}
|
|
680
|
+
// ask_user handler — set by the UI (app.tsx). Invoked when the model calls the
|
|
681
|
+
// `ask_user` tool: the UI surfaces an agent-authored card and resolves with the
|
|
682
|
+
// human's answer string, which becomes the tool result. When unset (headless
|
|
683
|
+
// without an answerer), the tool returns the dismissed sentinel so the agent
|
|
684
|
+
// still continues from its own judgment.
|
|
685
|
+
_askUserHandler = null;
|
|
686
|
+
setAskUserHandler(fn) {
|
|
687
|
+
this._askUserHandler = fn;
|
|
688
|
+
}
|
|
542
689
|
respondToToolApproval(approvalId, approved) {
|
|
543
690
|
const toolApprovalResponse = {
|
|
544
691
|
role: "tool",
|
|
@@ -585,6 +732,7 @@ export class Agent {
|
|
|
585
732
|
compaction_summary: undefined,
|
|
586
733
|
});
|
|
587
734
|
this._compactionStats = { count: 0, totalSaved: 0 };
|
|
735
|
+
this._lastCompactionTokensAfter = null;
|
|
588
736
|
this._pinnedSeqs.clear();
|
|
589
737
|
if (!this.sessionStore) {
|
|
590
738
|
this.messages = [];
|
|
@@ -663,9 +811,35 @@ export class Agent {
|
|
|
663
811
|
return [];
|
|
664
812
|
return buildChatEntries(this.session.id);
|
|
665
813
|
}
|
|
814
|
+
/**
|
|
815
|
+
* The session tree this TUI currently hosts: the root conversation plus every
|
|
816
|
+
* rotation / sub-agent descendant, with per-session metadata. Content from
|
|
817
|
+
* these sessions is already merged into the transcript on resume; this exposes
|
|
818
|
+
* WHICH sessions produced it so the rail can show the multi-session structure.
|
|
819
|
+
*/
|
|
820
|
+
getSessionTree() {
|
|
821
|
+
if (!this.session)
|
|
822
|
+
return [];
|
|
823
|
+
try {
|
|
824
|
+
return getSessionChainInfos(this.session.id);
|
|
825
|
+
}
|
|
826
|
+
catch (err) {
|
|
827
|
+
logger.error("orchestrator", "getSessionTree failed", { message: err?.message });
|
|
828
|
+
return [];
|
|
829
|
+
}
|
|
830
|
+
}
|
|
666
831
|
getLastTodoSnapshot() {
|
|
667
832
|
if (!this.session)
|
|
668
833
|
return null;
|
|
834
|
+
try {
|
|
835
|
+
const { getTaskListSnapshotFromGsd } = require("../gsd/phase-sync.js");
|
|
836
|
+
const gsdSnap = getTaskListSnapshotFromGsd(this.bash?.getCwd() ?? process.cwd());
|
|
837
|
+
if (gsdSnap)
|
|
838
|
+
return gsdSnap;
|
|
839
|
+
}
|
|
840
|
+
catch (err) {
|
|
841
|
+
// fail-open to legacy todo_write args
|
|
842
|
+
}
|
|
669
843
|
const argsJson = getLastTodoWriteArgs(this.session.id);
|
|
670
844
|
if (!argsJson)
|
|
671
845
|
return null;
|
|
@@ -677,7 +851,7 @@ export class Agent {
|
|
|
677
851
|
return {
|
|
678
852
|
workspace: this.workspace,
|
|
679
853
|
session: this.session,
|
|
680
|
-
messages:
|
|
854
|
+
messages: loadSessionChainTranscriptState(this.session.id).messages,
|
|
681
855
|
entries: buildChatEntries(this.session.id),
|
|
682
856
|
totalTokens: getSessionTotalTokens(this.session.id),
|
|
683
857
|
};
|
|
@@ -796,7 +970,7 @@ export class Agent {
|
|
|
796
970
|
// where orchestrator/task/title traffic is actually spending.
|
|
797
971
|
// Best-effort: failures inside appendCostLog are swallowed (see cost-log.ts).
|
|
798
972
|
const breakdown = source === "message" ? (this._lastPromptBreakdown ?? undefined) : undefined;
|
|
799
|
-
// Sanitize actualInputTokens for providers
|
|
973
|
+
// Sanitize actualInputTokens for providers that return
|
|
800
974
|
// implausibly low prompt_tokens (e.g. 10) regardless of prompt size.
|
|
801
975
|
const estIn = breakdown ? Math.ceil(((breakdown.systemChars ?? 0) + (breakdown.messagesChars ?? 0)) / 4) : 0;
|
|
802
976
|
const actualInput = sanitizeInputTokens(totalInput, estIn);
|
|
@@ -919,8 +1093,27 @@ export class Agent {
|
|
|
919
1093
|
const totalUsage = {};
|
|
920
1094
|
let assistantText = "";
|
|
921
1095
|
let lastActivity = initialDetail;
|
|
1096
|
+
// Phase B3 parity: the streamText sub-agent path compacts older tool
|
|
1097
|
+
// results in prepareStep, but this batch path historically concatenated
|
|
1098
|
+
// `[...childMessages, ...turnMessages]` unbounded every round — on a long
|
|
1099
|
+
// batch loop the re-sent history balloons exactly like the stream path did
|
|
1100
|
+
// before B3. Apply the same compactor here (round >= 1, mirroring the
|
|
1101
|
+
// stream path's `stepNumber >= 1` gate). High-value results stay verbatim.
|
|
1102
|
+
const batchCompactThreshold = getSubAgentCompactThresholdChars();
|
|
1103
|
+
const batchCompactKeepLast = getSubAgentCompactKeepLast();
|
|
1104
|
+
const batchChildCtxWindow = childRuntime.modelInfo?.contextWindow ?? 0;
|
|
1105
|
+
const batchIsReasoningModel = childRuntime.modelInfo?.reasoning === true;
|
|
922
1106
|
for (let round = 0; round < maxSteps; round++) {
|
|
923
1107
|
const batchRequestId = `task-${Date.now()}-${round + 1}`;
|
|
1108
|
+
const roundMessages = round < 1
|
|
1109
|
+
? [...childMessages, ...turnMessages]
|
|
1110
|
+
: compactSubAgentMessages([...childMessages, ...turnMessages], {
|
|
1111
|
+
thresholdChars: batchCompactThreshold,
|
|
1112
|
+
keepLastTurns: batchCompactKeepLast,
|
|
1113
|
+
contextWindowTokens: batchChildCtxWindow,
|
|
1114
|
+
contextFillRatio: batchIsReasoningModel ? 0.3 : undefined,
|
|
1115
|
+
stripOldReasoning: batchIsReasoningModel,
|
|
1116
|
+
});
|
|
924
1117
|
await addBatchRequests({
|
|
925
1118
|
...this.getBatchClientOptions(signal),
|
|
926
1119
|
batchId: batch.batch_id,
|
|
@@ -931,8 +1124,8 @@ export class Agent {
|
|
|
931
1124
|
chat_get_completion: buildBatchChatCompletionRequest({
|
|
932
1125
|
modelId: childRuntime.modelId,
|
|
933
1126
|
system: childSystem,
|
|
934
|
-
messages:
|
|
935
|
-
temperature: request.agent === "explore" ? 0.2 : 0.5,
|
|
1127
|
+
messages: roundMessages,
|
|
1128
|
+
temperature: childRuntime.modelInfo?.fixedTemperature ?? (request.agent === "explore" ? 0.2 : 0.5),
|
|
936
1129
|
maxOutputTokens: !childCaps.acceptsParam("maxOutputTokens", childRuntime.modelInfo)
|
|
937
1130
|
? undefined
|
|
938
1131
|
: Math.min(this.maxTokens, 8_192),
|
|
@@ -1023,7 +1216,6 @@ export class Agent {
|
|
|
1023
1216
|
async runTaskRequest(request, onActivity, abortSignal) {
|
|
1024
1217
|
const provider = this.requireProvider();
|
|
1025
1218
|
const deps = {
|
|
1026
|
-
getProvider: () => provider,
|
|
1027
1219
|
resolveModelForTask: (task) => this._resolveModelForTask(task),
|
|
1028
1220
|
getModelId: () => this.modelId,
|
|
1029
1221
|
getProviderId: () => this.providerId,
|
|
@@ -1244,7 +1436,10 @@ export class Agent {
|
|
|
1244
1436
|
return this._resolveModelForTask("compact");
|
|
1245
1437
|
}
|
|
1246
1438
|
_resolveModelForTask(task) {
|
|
1247
|
-
|
|
1439
|
+
const parentTier = getModelInfo(this.modelId)?.tier;
|
|
1440
|
+
return resolveModelForTask(task, this.providerId, this.modelId, undefined, {
|
|
1441
|
+
parentTier,
|
|
1442
|
+
});
|
|
1248
1443
|
}
|
|
1249
1444
|
async compactForContext(provider, system, contextWindow, signal, settings = this.getCompactionSettings(contextWindow), force = false) {
|
|
1250
1445
|
if (!this.session)
|
|
@@ -1252,7 +1447,7 @@ export class Agent {
|
|
|
1252
1447
|
// Phase 1: ask the compaction proposer model whether to compact and what to keep/drop.
|
|
1253
1448
|
// Only compact if the model says yes. On error/skip, fall back to heuristic.
|
|
1254
1449
|
const compactModelId = this._resolveCompactModel();
|
|
1255
|
-
const proposal = await proposeCompaction(
|
|
1450
|
+
const proposal = await proposeCompaction(compactModelId, this.messages, signal);
|
|
1256
1451
|
if (proposal !== null) {
|
|
1257
1452
|
// Model decided — compact only if model says shouldCompact
|
|
1258
1453
|
if (force) {
|
|
@@ -1315,7 +1510,7 @@ export class Agent {
|
|
|
1315
1510
|
const customInstructions = isSubSession
|
|
1316
1511
|
? "This is a temporary sub-session. Under sub-sessions, it is CRITICAL to preserve active files being worked on, compiler/linter error states, and exact line coordinates in the summary. Do not omit details of files edited, tests run, or compiler diagnostics, as the model needs this specific context to continue working without re-reading the files."
|
|
1317
1512
|
: undefined;
|
|
1318
|
-
const { summary, usage: compactUsage } = await generateCompactionSummary(
|
|
1513
|
+
const { summary, usage: compactUsage } = await generateCompactionSummary(compactModelId, preparation, customInstructions, signal);
|
|
1319
1514
|
// Record compaction call in cost-log — bypasses recordUsage because
|
|
1320
1515
|
// compaction returns usage separately and isn't routed through the
|
|
1321
1516
|
// status-bar / usage event pipeline (intentional: it's overhead, not user spend).
|
|
@@ -1387,6 +1582,7 @@ export class Agent {
|
|
|
1387
1582
|
// grossSaved tells the user how many context tokens were reclaimed — a real benefit that
|
|
1388
1583
|
// reduces subsequent turn costs.
|
|
1389
1584
|
const tokensAfter = estimateConversationTokens(system, this.messages);
|
|
1585
|
+
this._lastCompactionTokensAfter = tokensAfter;
|
|
1390
1586
|
const saved = Math.max(0, preparation.tokensBefore - tokensAfter);
|
|
1391
1587
|
const pct = preparation.tokensBefore > 0 ? ((saved / preparation.tokensBefore) * 100).toFixed(1) : "0.0";
|
|
1392
1588
|
this._compactionStats.count++;
|
|
@@ -1452,6 +1648,13 @@ export class Agent {
|
|
|
1452
1648
|
minMeaningfulTokens,
|
|
1453
1649
|
});
|
|
1454
1650
|
}
|
|
1651
|
+
const minNew = getAutoCompactMinNewTokens();
|
|
1652
|
+
if (isCompactionThrash(tokens, this._lastCompactionTokensAfter, minNew)) {
|
|
1653
|
+
return log(false, `anti-thrash (only ${tokens - (this._lastCompactionTokensAfter ?? 0)} new < ${minNew})`, {
|
|
1654
|
+
tokens,
|
|
1655
|
+
lastAfter: this._lastCompactionTokensAfter,
|
|
1656
|
+
});
|
|
1657
|
+
}
|
|
1455
1658
|
log(true, `over-threshold (${tokens} >= ${minMeaningfulTokens})`, { tokens, thresholdPct, minMeaningfulTokens });
|
|
1456
1659
|
await this.compactForContext(provider, system, contextWindow, signal, this.getCompactionSettings(contextWindow), true).catch((err) => logger.warn("orchestrator", "compaction failed", { error: err }));
|
|
1457
1660
|
}
|
|
@@ -1464,8 +1667,8 @@ export class Agent {
|
|
|
1464
1667
|
// (respondToCouncilQuestion/Preflight + the internal _create*Responder
|
|
1465
1668
|
// hooks used by orchestrator.agent.test.ts).
|
|
1466
1669
|
// ========================================================================
|
|
1467
|
-
respondToCouncilQuestion(questionId, answer) {
|
|
1468
|
-
this.councilManager.respondToQuestion(questionId, answer);
|
|
1670
|
+
respondToCouncilQuestion(questionId, answer, questionText) {
|
|
1671
|
+
this.councilManager.respondToQuestion(questionId, answer, questionText);
|
|
1469
1672
|
}
|
|
1470
1673
|
respondToCouncilPreflight(preflightId, approved) {
|
|
1471
1674
|
this.councilManager.respondToPreflight(preflightId, approved);
|
|
@@ -1474,7 +1677,7 @@ export class Agent {
|
|
|
1474
1677
|
// Council v2 — Clarify → Confirm → Debate → Plan → Execute
|
|
1475
1678
|
// ========================================================================
|
|
1476
1679
|
async *runCouncilV2(topic, options) {
|
|
1477
|
-
const { runCouncil } = await import("../council/index.js");
|
|
1680
|
+
const { runCouncil, buildNeutralPostCouncilContinuation } = await import("../council/index.js");
|
|
1478
1681
|
const { createCouncilLLM } = await import("../council/llm.js");
|
|
1479
1682
|
const councilStats = { calls: 0, startMs: Date.now(), phases: [] };
|
|
1480
1683
|
const llm = createCouncilLLM(this.bash, this.mode, this.session?.id, councilStats);
|
|
@@ -1514,6 +1717,9 @@ export class Agent {
|
|
|
1514
1717
|
catch (err) {
|
|
1515
1718
|
logger.error("router", "runDir resolution failed (decisions.lock will be skipped)", { error: err });
|
|
1516
1719
|
}
|
|
1720
|
+
// Captures the post-debate action so we can auto-continue on
|
|
1721
|
+
// "continue_session" (user chose to keep working with the debate result).
|
|
1722
|
+
let chosenAction;
|
|
1517
1723
|
try {
|
|
1518
1724
|
const gen = runCouncil(topic, this.modelId, this.messages, this.session?.id, llm, this.councilManager.createQuestionResponder(), this.councilManager.createPreflightResponder(), processMessageFn, {
|
|
1519
1725
|
skipClarification: options?.skipClarification,
|
|
@@ -1521,12 +1727,42 @@ export class Agent {
|
|
|
1521
1727
|
cwd: this.bash.getCwd(),
|
|
1522
1728
|
councilStats, // NEW — share orchestrator's stats object with runCouncil (Phase 14 CQ-01)
|
|
1523
1729
|
signal,
|
|
1730
|
+
// convene_council path — suppress ALL hardcoded post-debate decision
|
|
1731
|
+
// surface; the agent decides what happens after the synthesis.
|
|
1732
|
+
convenePath: options?.convenePath,
|
|
1733
|
+
// When the Context Rail is active it carries leader/panel/cost as
|
|
1734
|
+
// ambient sidebar rows, so suppress the duplicate inline summary.
|
|
1735
|
+
suppressInlineMeta: isContextRailEnabled(),
|
|
1524
1736
|
runDir, // B1 — persist decisions.lock.md for the /council slash path
|
|
1737
|
+
// #2 — isolated research bridge. Same StreamRunner sub-agent the /ideal
|
|
1738
|
+
// sprint-runner uses (runTaskRequest): a budget-capped, near-empty
|
|
1739
|
+
// explore child runs the debate's research phase so its multi-step tool
|
|
1740
|
+
// clutter never accretes into the council thread/context. Threads the
|
|
1741
|
+
// council abort signal so Esc cancels the research child too.
|
|
1742
|
+
runIsolatedTask: (request) => this.runTaskRequest(request, undefined, signal),
|
|
1743
|
+
onPostDebateAction: (action) => {
|
|
1744
|
+
chosenAction = action;
|
|
1745
|
+
// Relay to the auto-council caller (tool-engine) so nested runs honor
|
|
1746
|
+
// the user's choice instead of always continuing.
|
|
1747
|
+
this.councilManager.setLastPostDebateAction(action);
|
|
1748
|
+
},
|
|
1525
1749
|
});
|
|
1526
1750
|
let result;
|
|
1751
|
+
// Hold back runCouncil's terminal `done`. The /council slash consumer
|
|
1752
|
+
// (use-app-logic.tsx) does `for await (chunk of gen) { if (chunk.type ===
|
|
1753
|
+
// "done") break; }` — so forwarding runCouncil's end-of-debate `done`
|
|
1754
|
+
// strands THIS generator before the continue_session auto-continue below
|
|
1755
|
+
// ever runs (the generator is suspended at the yield and never resumes).
|
|
1756
|
+
// Swallow it here and re-emit our own `done` after the optional
|
|
1757
|
+
// continuation so the consumer terminates at the right boundary.
|
|
1758
|
+
let innerDoneSeen = false;
|
|
1527
1759
|
do {
|
|
1528
1760
|
result = await gen.next();
|
|
1529
1761
|
if (!result.done && result.value) {
|
|
1762
|
+
if (result.value.type === "done") {
|
|
1763
|
+
innerDoneSeen = true;
|
|
1764
|
+
continue;
|
|
1765
|
+
}
|
|
1530
1766
|
yield result.value;
|
|
1531
1767
|
}
|
|
1532
1768
|
} while (!result.done);
|
|
@@ -1535,6 +1771,130 @@ export class Agent {
|
|
|
1535
1771
|
if (options?.userModelMessage && synthesis) {
|
|
1536
1772
|
this.appendCompletedTurn(options.userModelMessage, [{ role: "assistant", content: synthesis }]);
|
|
1537
1773
|
}
|
|
1774
|
+
// Keep working in THIS session when the chosen action calls for it
|
|
1775
|
+
// (continue_session → carry the conclusion; generate_plan/implement →
|
|
1776
|
+
// execute action items). postDebateContinuation is the single source of
|
|
1777
|
+
// truth shared with the auto-council caller (tool-engine). Re-entering
|
|
1778
|
+
// processMessage also writes real message rows, which is what makes the
|
|
1779
|
+
// session resumable (the /council slash path otherwise leaves no messages
|
|
1780
|
+
// and is filtered from the resume picker). Guarded by ownsController so it
|
|
1781
|
+
// fires ONLY on the top-level slash path, never when nested inside
|
|
1782
|
+
// processMessage (auto-council) or drained by the runDebate tool — those
|
|
1783
|
+
// callers manage their own continuation.
|
|
1784
|
+
// convenePath suppresses the hardcoded card (chosenAction stays undefined),
|
|
1785
|
+
// so always hand the synthesis to a normal agent turn via the neutral
|
|
1786
|
+
// continuation and let the agent decide. ownsController scopes this to the
|
|
1787
|
+
// top-level /council slash path (auto-council nests with ownsController
|
|
1788
|
+
// false and continues in tool-engine instead).
|
|
1789
|
+
const continuationPrompt = ownsController && synthesis ? buildNeutralPostCouncilContinuation(synthesis) || null : null;
|
|
1790
|
+
const isBuildContinuation = chosenAction === "implement" || chosenAction === "generate_plan";
|
|
1791
|
+
if (continuationPrompt && isBuildContinuation && process.env.MUONROI_COUNCIL_ISOLATE_IMPL !== "0") {
|
|
1792
|
+
// #1 — build the council decision in an ISOLATED sub-agent instead of
|
|
1793
|
+
// re-entering the full processMessage turn. The flat turn inherited the
|
|
1794
|
+
// entire multi-round debate history and could overflow the context window
|
|
1795
|
+
// (the exact wedge 97bc9d12 fixed for the /ideal sprint-runner but that
|
|
1796
|
+
// /council never got). runTaskRequest starts near-empty and returns a
|
|
1797
|
+
// compact ToolResult. ANTI-MÙ: the child is NOT blind — it is seeded with
|
|
1798
|
+
// (a) the approved synthesis-as-spec (already in continuationPrompt) and
|
|
1799
|
+
// (b) councilManager.buildContext(), the same compaction-summary + Recent
|
|
1800
|
+
// Conversation + [Council Decision]/[Council Memory] "Key Decisions"
|
|
1801
|
+
// checkpoint the anti-mù layer surfaces — so it has the decision + its
|
|
1802
|
+
// rationale without carrying the raw transcript. Opt out with
|
|
1803
|
+
// MUONROI_COUNCIL_ISOLATE_IMPL=0 (falls back to the processMessage path).
|
|
1804
|
+
yield { type: "content", content: "\n[Implementing the council decision in an isolated context…]\n" };
|
|
1805
|
+
this.councilManager.setContinuation(true);
|
|
1806
|
+
try {
|
|
1807
|
+
let councilCheckpoint = "";
|
|
1808
|
+
try {
|
|
1809
|
+
councilCheckpoint = this.councilManager.buildContext();
|
|
1810
|
+
}
|
|
1811
|
+
catch {
|
|
1812
|
+
/* anti-mù bundle is best-effort — the synthesis alone still grounds the build */
|
|
1813
|
+
}
|
|
1814
|
+
const seededPrompt = councilCheckpoint
|
|
1815
|
+
? `${continuationPrompt}\n\n## Council context (decision + recent session — do NOT re-debate, just build)\n${councilCheckpoint}`
|
|
1816
|
+
: continuationPrompt;
|
|
1817
|
+
let result;
|
|
1818
|
+
try {
|
|
1819
|
+
result = await this.runTaskRequest({ agent: "general", description: "Council implementation", prompt: seededPrompt, modelId: this.modelId }, undefined, signal);
|
|
1820
|
+
}
|
|
1821
|
+
catch (err) {
|
|
1822
|
+
// A throw (vs a returned {success:false}) must not escape and take the
|
|
1823
|
+
// whole /council run down — surface it in-band like the processMessage
|
|
1824
|
+
// path's TurnStallError handling and end the turn cleanly.
|
|
1825
|
+
yield {
|
|
1826
|
+
type: "error",
|
|
1827
|
+
content: `Council implementation failed: ${err instanceof Error ? err.message : String(err)}`,
|
|
1828
|
+
};
|
|
1829
|
+
yield { type: "done" };
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
if (result.success && result.output?.trim()) {
|
|
1833
|
+
yield { type: "content", content: `\n${result.output.trim()}\n` };
|
|
1834
|
+
}
|
|
1835
|
+
else if (result.error) {
|
|
1836
|
+
yield { type: "error", content: `Council implementation failed: ${result.error}` };
|
|
1837
|
+
}
|
|
1838
|
+
// Persist a COMPACT record so the /council slash session stays resumable
|
|
1839
|
+
// (the processMessage path wrote rows for this) WITHOUT re-inheriting the
|
|
1840
|
+
// debate bloat the isolated child deliberately avoided.
|
|
1841
|
+
try {
|
|
1842
|
+
this.appendCompletedTurn({ role: "user", content: "[Council implementation of the debate decision]" }, [{ role: "assistant", content: (result.output ?? result.error ?? "(no output)").trim() }]);
|
|
1843
|
+
}
|
|
1844
|
+
catch {
|
|
1845
|
+
/* non-critical persistence */
|
|
1846
|
+
}
|
|
1847
|
+
yield { type: "done" };
|
|
1848
|
+
}
|
|
1849
|
+
finally {
|
|
1850
|
+
this.councilManager.setContinuation(false);
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
else if (continuationPrompt) {
|
|
1854
|
+
yield { type: "content", content: "\n[Continuing with the debate conclusion…]\n" };
|
|
1855
|
+
this.councilManager.setContinuation(true);
|
|
1856
|
+
try {
|
|
1857
|
+
// processMessage emits its own terminal `done`, which becomes the
|
|
1858
|
+
// consumer's break boundary — so the UI stops AFTER the continuation
|
|
1859
|
+
// turn, not before it.
|
|
1860
|
+
//
|
|
1861
|
+
// Guard the continuation with an idle+total watchdog: the per-chunk
|
|
1862
|
+
// stall watchdog only covers streamText byte flow, NOT a turn that
|
|
1863
|
+
// wedges inside a tool call (a `task` sub-agent or long `bash`).
|
|
1864
|
+
// Session 578b2eae7099 hung exactly there — the UI froze at "Council
|
|
1865
|
+
// working… elapsed 0s" with no rescue. On fire we abort the turn and
|
|
1866
|
+
// surface a toast instead of hanging forever.
|
|
1867
|
+
const { withTurnWatchdog, TurnStallError } = await import("./turn-watchdog.js");
|
|
1868
|
+
const idleMs = Number(process.env.MUONROI_COUNCIL_CONTINUATION_IDLE_MS ?? 120_000);
|
|
1869
|
+
const totalMs = Number(process.env.MUONROI_COUNCIL_CONTINUATION_TOTAL_MS ?? 600_000);
|
|
1870
|
+
try {
|
|
1871
|
+
yield* withTurnWatchdog(this.processMessage(continuationPrompt, options?.observer), {
|
|
1872
|
+
idleMs,
|
|
1873
|
+
totalMs,
|
|
1874
|
+
label: "council continuation turn",
|
|
1875
|
+
shouldSuppressFire: isInteractivePaused,
|
|
1876
|
+
});
|
|
1877
|
+
}
|
|
1878
|
+
catch (err) {
|
|
1879
|
+
if (err instanceof TurnStallError) {
|
|
1880
|
+
this.abortController?.abort(new DOMException(err.message, "TimeoutError"));
|
|
1881
|
+
yield { type: "error", content: `Council continuation stalled — ${err.message}` };
|
|
1882
|
+
yield { type: "done" };
|
|
1883
|
+
}
|
|
1884
|
+
else {
|
|
1885
|
+
throw err;
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
}
|
|
1889
|
+
finally {
|
|
1890
|
+
this.councilManager.setContinuation(false);
|
|
1891
|
+
}
|
|
1892
|
+
}
|
|
1893
|
+
else if (innerDoneSeen) {
|
|
1894
|
+
// Non-continuation paths (save_exit, nested auto-council, sprint drain)
|
|
1895
|
+
// still need the terminal `done` the consumer expects.
|
|
1896
|
+
yield { type: "done" };
|
|
1897
|
+
}
|
|
1538
1898
|
}
|
|
1539
1899
|
finally {
|
|
1540
1900
|
if (ownsController && this.abortController?.signal === signal) {
|
|
@@ -1555,26 +1915,149 @@ export class Agent {
|
|
|
1555
1915
|
phases: [],
|
|
1556
1916
|
};
|
|
1557
1917
|
const llm = createCouncilLLM(this.bash, this.mode, this.session?.id, productStats);
|
|
1558
|
-
|
|
1918
|
+
// Autonomous-execution permission for the product loop. /ideal's consent
|
|
1919
|
+
// boundary is the preflight plan-approval askcard; once the PO approves the
|
|
1920
|
+
// plan, the sprint IMPLEMENT turn must apply its own file-op mutations without
|
|
1921
|
+
// a per-tool approval prompt. In `safe` mode a Write/Edit surfaces a
|
|
1922
|
+
// tool_approval_request and awaits respondToToolApproval — but in the driven
|
|
1923
|
+
// product-loop context nothing answers it and no approval askcard renders, so
|
|
1924
|
+
// the impl turn wedges forever right after finishReason:tool-calls (observed
|
|
1925
|
+
// live 2026-07-14: 0 files written across grok/opencode/deepseek + isolated &
|
|
1926
|
+
// streamed paths — tool-engine.ts:2997). Elevating safe→auto-edit for the turn
|
|
1927
|
+
// auto-approves file ops (yolo stays yolo); catastrophic bash stays hard-blocked
|
|
1928
|
+
// by permission-mode's CATASTROPHIC_PATTERNS regardless of mode.
|
|
1929
|
+
const self = this;
|
|
1930
|
+
const processMessageFn = (m) => (async function* () {
|
|
1931
|
+
const prev = self.permissionMode;
|
|
1932
|
+
if (self.permissionMode === "safe")
|
|
1933
|
+
self.permissionMode = "auto-edit";
|
|
1934
|
+
try {
|
|
1935
|
+
yield* self.processMessage(m, options?.observer);
|
|
1936
|
+
}
|
|
1937
|
+
finally {
|
|
1938
|
+
self.permissionMode = prev;
|
|
1939
|
+
}
|
|
1940
|
+
})();
|
|
1941
|
+
// Isolated bounded task-runner bridge for the sprint implement stage: a fresh
|
|
1942
|
+
// child context (getSubAgentBudgetChars cap, independent compaction) that does
|
|
1943
|
+
// NOT inherit this turn's council-debate history — the root fix for the live
|
|
1944
|
+
// ctx-overflow wedge. Returns a compact ToolResult (absorbed, no parent bloat).
|
|
1945
|
+
// Same autonomous-permission elevation as the streamed path above.
|
|
1946
|
+
const runIsolatedTask = async (request) => {
|
|
1947
|
+
const prev = self.permissionMode;
|
|
1948
|
+
if (self.permissionMode === "safe")
|
|
1949
|
+
self.permissionMode = "auto-edit";
|
|
1950
|
+
try {
|
|
1951
|
+
return await this.runTaskRequest(request, undefined, this.abortController?.signal);
|
|
1952
|
+
}
|
|
1953
|
+
finally {
|
|
1954
|
+
self.permissionMode = prev;
|
|
1955
|
+
}
|
|
1956
|
+
};
|
|
1559
1957
|
const flowDir = nodePath.join(this.bash.getCwd(), ".muonroi-flow");
|
|
1560
|
-
// P2.7
|
|
1561
|
-
//
|
|
1958
|
+
// P2.7 (LLM-first — no-regex routing): the work-depth tier that decides
|
|
1959
|
+
// /ideal's route is judged by the MODEL (the same depthTier the PIL Layer-1
|
|
1960
|
+
// classifier emits for chat turns), NOT by a keyword scorer. The old regex
|
|
1961
|
+
// `scoreComplexity` mis-tiered plainly-phrased heavy work — a "merge / vendor
|
|
1962
|
+
// / rename" refactor scored LOW because it lacked the FORCE_HIGH keywords —
|
|
1963
|
+
// which hot-pathed genuinely heavy tasks with zero interview/council. Only
|
|
1964
|
+
// meaningful for "start"; other subcommands ignore it.
|
|
1562
1965
|
let complexity;
|
|
1966
|
+
let needsClarification;
|
|
1563
1967
|
let sufficiencyMissing;
|
|
1968
|
+
let routeForceCouncil = payload.flags.forceCouncil;
|
|
1564
1969
|
if (payload.subcommand === "start" && payload.idea) {
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1970
|
+
let depth = "standard";
|
|
1971
|
+
try {
|
|
1972
|
+
const { createLlmClassifier } = await import("../pil/llm-classify.js");
|
|
1973
|
+
const { probeRepoGrounding } = await import("../pil/repo-grounding-probe.js");
|
|
1974
|
+
const { getRepoStructureHints } = await import("../pil/repo-structure-hints.js");
|
|
1975
|
+
const classify = createLlmClassifier(this.modelId, {
|
|
1976
|
+
routeFastTier: true,
|
|
1977
|
+
// /ideal routing needs a reliable depth verdict; allow cross-provider
|
|
1978
|
+
// fallback so an agentic/no-fast-tier session model (e.g. xai) doesn't
|
|
1979
|
+
// strand the classify at fail-open. Bounded by CLASSIFY_TOTAL_BUDGET_MS.
|
|
1980
|
+
crossProviderFallback: true,
|
|
1981
|
+
});
|
|
1982
|
+
const res = await classify(payload.idea);
|
|
1983
|
+
if (res?.depthTier) {
|
|
1984
|
+
depth = res.depthTier;
|
|
1985
|
+
}
|
|
1986
|
+
else {
|
|
1987
|
+
// Same convention as PIL Layer 1: a null/garbled depth defaults to the
|
|
1988
|
+
// safe middle. Logged so a persistently-null classifier is diagnosable.
|
|
1989
|
+
console.error(`[ideal/route] model depth classify returned no depthTier — defaulting to "standard". idea=${JSON.stringify(payload.idea.slice(0, 80))}`);
|
|
1990
|
+
}
|
|
1991
|
+
// LLM-first clarity signal — feeds the downstream interview as a hint.
|
|
1992
|
+
if (res?.needsClarification === true)
|
|
1993
|
+
needsClarification = true;
|
|
1994
|
+
// Design B — deterministic repo-grounding probe. Trigger on Layer-1
|
|
1995
|
+
// OUTPUT (ambiguity), NOT a prompt regex: run only when the verdict is
|
|
1996
|
+
// non-trivial (depth !== "quick") OR low-confidence, so a well-specified
|
|
1997
|
+
// trivial task still hot-paths on one cheap call. The probe itself is
|
|
1998
|
+
// deterministic (REPO_DEEP_MAP index + bounded on-disk reads, no LLM).
|
|
1999
|
+
// Once it runs its MEASURED facts drive routing — confidence is
|
|
2000
|
+
// trigger-only and can no longer override grounding.
|
|
2001
|
+
const lowConfidence = (res?.confidence ?? 1) < 0.7;
|
|
2002
|
+
if (depth !== "quick" || lowConfidence) {
|
|
2003
|
+
try {
|
|
2004
|
+
const hints = getRepoStructureHints(process.cwd());
|
|
2005
|
+
const probe = probeRepoGrounding(payload.idea, hints, { cwd: process.cwd() });
|
|
2006
|
+
if (probe.ran) {
|
|
2007
|
+
// Invariant: probe ran AND zero matched files, or a symbol
|
|
2008
|
+
// collision → route INTO Council (the arbiter). Never direct-
|
|
2009
|
+
// dispatch from weak grounding.
|
|
2010
|
+
if (probe.groundingUncertainty) {
|
|
2011
|
+
needsClarification = true;
|
|
2012
|
+
depth = depth === "quick" ? "standard" : depth;
|
|
2013
|
+
}
|
|
2014
|
+
// Measured size escalation: a large grounded surface routes heavy
|
|
2015
|
+
// regardless of the sentence-level classify verdict.
|
|
2016
|
+
if (probe.bucket === "large")
|
|
2017
|
+
depth = "heavy";
|
|
2018
|
+
console.error(`[ideal/route] grounding probe: files=${probe.matchedFiles} loc=${probe.totalLoc} ` +
|
|
2019
|
+
`dirs=${probe.matchedDirs} collision=${probe.collision} bucket=${probe.bucket} ` +
|
|
2020
|
+
`uncertainty=${probe.groundingUncertainty} → depth=${depth}`);
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
catch (err) {
|
|
2024
|
+
// Fail-open: a probe hiccup must never block /ideal (No-Silent-Catch).
|
|
2025
|
+
console.error(`[ideal/route] grounding probe failed, ignoring: ${err?.message}`);
|
|
2026
|
+
}
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
catch (err) {
|
|
2030
|
+
// Fail-open: never block /ideal on a classify hiccup (No-Silent-Catch).
|
|
2031
|
+
console.error(`[ideal/route] depth classify failed, defaulting to "standard": ${err?.message}`);
|
|
2032
|
+
}
|
|
2033
|
+
// Council routing gate (model-signal, no hardcoded depth→council table).
|
|
2034
|
+
//
|
|
2035
|
+
// History: /ideal used to force Council UNCONDITIONALLY because the cheap
|
|
2036
|
+
// classify was too noisy to gate on — the SAME architectural prompt scored
|
|
2037
|
+
// `heavy` on one run and `standard` the next, so routing flip-flopped. That
|
|
2038
|
+
// noise was root-caused (2026-07-15, harness-measured): the session model
|
|
2039
|
+
// could be an agentic model (e.g. grok-composer) that ignores the terse
|
|
2040
|
+
// 8-word classify contract and emits task-planning prose → null → fail-open
|
|
2041
|
+
// "standard"; and stream errors were swallowed into the same null. Both are
|
|
2042
|
+
// now fixed in llm-classify.ts (cross-provider route to a keyed instruction-
|
|
2043
|
+
// follower + No-Silent-Catch error surfacing), so the depth/clarity signal
|
|
2044
|
+
// is reliable enough to gate on.
|
|
2045
|
+
//
|
|
2046
|
+
// Gate: only a genuinely TRIVIAL /ideal — the model says depth=quick AND
|
|
2047
|
+
// not-underspecified — skips Council and takes the product-loop hot-path
|
|
2048
|
+
// (single sprint, no debate; index.ts complexity==="low" branch). Anything
|
|
2049
|
+
// else (standard, heavy, or any underspecified request) still runs the full
|
|
2050
|
+
// Council/loop-driver pipeline: scan source → adaptive interview → leader-
|
|
2051
|
+
// tier assessment + debate AFTER scope is understood. When the model is
|
|
2052
|
+
// uncertain (null depth → "medium") it routes INTO Council, the arbiter —
|
|
2053
|
+
// never a hardcoded default. Explicit `--maintain` still opts out entirely.
|
|
2054
|
+
complexity = depth === "heavy" ? "high" : depth === "quick" ? "low" : "medium";
|
|
2055
|
+
sufficiencyMissing = undefined;
|
|
2056
|
+
if (payload.flags.mode !== "maintain") {
|
|
2057
|
+
const trivial = depth === "quick" && needsClarification !== true;
|
|
2058
|
+
if (!trivial)
|
|
2059
|
+
routeForceCouncil = true;
|
|
2060
|
+
}
|
|
1578
2061
|
}
|
|
1579
2062
|
const gen = runProductLoop({
|
|
1580
2063
|
subcommand: payload.subcommand,
|
|
@@ -1589,16 +2072,18 @@ export class Agent {
|
|
|
1589
2072
|
doneThreshold: payload.flags.doneThreshold,
|
|
1590
2073
|
budgetTokens: payload.flags.budgetTokens,
|
|
1591
2074
|
stack: payload.flags.stack,
|
|
1592
|
-
forceCouncil:
|
|
2075
|
+
forceCouncil: routeForceCouncil,
|
|
1593
2076
|
},
|
|
1594
2077
|
respondToQuestion: this.councilManager.createQuestionResponder(),
|
|
1595
2078
|
respondToPreflight: this.councilManager.createPreflightResponder(),
|
|
1596
2079
|
cwd: this.bash.getCwd(),
|
|
1597
2080
|
processMessageFn,
|
|
2081
|
+
runIsolatedTask,
|
|
1598
2082
|
// Mode C — wire verify-recipe detector so runProductLoop auto-detect can probe cwd.
|
|
1599
2083
|
detectVerifyRecipe: () => this.detectVerifyRecipe(),
|
|
1600
2084
|
skipPriorContext: payload.flags.noPriorContext === true,
|
|
1601
2085
|
complexity,
|
|
2086
|
+
needsClarification,
|
|
1602
2087
|
sufficiencyMissing,
|
|
1603
2088
|
// Mode C explicit override + gh pr create opt-in (see .planning/MAINTAIN-MODE.md).
|
|
1604
2089
|
mode: payload.flags.mode,
|
|
@@ -2064,9 +2549,11 @@ export class Agent {
|
|
|
2064
2549
|
setLastProviderOptionsShape: (shape) => {
|
|
2065
2550
|
self._lastProviderOptionsShape = shape;
|
|
2066
2551
|
},
|
|
2552
|
+
getCompactionStats: () => self.getCompactionStats(),
|
|
2067
2553
|
getBatchClientOptions: (signal) => self.getBatchClientOptions(signal),
|
|
2068
2554
|
getCompactionSettings: (cw) => self.getCompactionSettings(cw),
|
|
2069
2555
|
compactForContext: (provider, system, cw, signal, settings, overflow) => self.compactForContext(provider, system, cw, signal, settings, overflow),
|
|
2556
|
+
extendHardCeilingForAutoCompaction: () => self.extendHardCeilingForAutoCompaction(),
|
|
2070
2557
|
postTurnCompact: (provider, system, cw, signal) => self.postTurnCompact(provider, system, cw, signal),
|
|
2071
2558
|
createTools: (bash, provider, mode, opts) => createTools(bash, provider, mode, opts),
|
|
2072
2559
|
runTask: (request, signal) => self.runTask(request, signal),
|
|
@@ -2175,7 +2662,16 @@ export class Agent {
|
|
|
2175
2662
|
else {
|
|
2176
2663
|
try {
|
|
2177
2664
|
const { classifySubSessionAction } = await import("../pil/llm-classify.js");
|
|
2178
|
-
|
|
2665
|
+
// Feed the recent-conversation digest so the router can actually honour
|
|
2666
|
+
// its "decide based on the conversation history" contract — otherwise a
|
|
2667
|
+
// continuation ("ok làm phần đó đi") is judged in isolation and can be
|
|
2668
|
+
// mis-routed as a fresh/unrelated task. Passing contextInfo also turns
|
|
2669
|
+
// on the session-size metadata block the ROTATE_SESSION rule relies on.
|
|
2670
|
+
const routeResult = await classifySubSessionAction(this.modelId, userMessage, {
|
|
2671
|
+
currentChars,
|
|
2672
|
+
threshold,
|
|
2673
|
+
recentTurns: this._buildRecentTurnsSummary(),
|
|
2674
|
+
});
|
|
2179
2675
|
if (routeResult) {
|
|
2180
2676
|
routeAction = routeResult.action;
|
|
2181
2677
|
logger.info("orchestrator", "Routing action selected for user message", {
|
|
@@ -2189,6 +2685,18 @@ export class Agent {
|
|
|
2189
2685
|
logger.error("orchestrator", "Routing classification failed, falling back to DIRECT_ANSWER", { error: err });
|
|
2190
2686
|
}
|
|
2191
2687
|
}
|
|
2688
|
+
// Reactive escalation — override a DIRECT_ANSWER route (the router's blind
|
|
2689
|
+
// spot on read-heavy analysis, and its silent-degrade to DIRECT on classify
|
|
2690
|
+
// failure) when the PREVIOUS turn's observed tool load proves the session is
|
|
2691
|
+
// doing heavy multi-tool work. Deterministic, based on real execution, not a
|
|
2692
|
+
// prompt guess. Only rescues DIRECT_ANSWER — never hijacks a deliberate
|
|
2693
|
+
// ROTATE_SESSION. See reactive-delegation.ts.
|
|
2694
|
+
if (routeAction === "DIRECT_ANSWER" && shouldReactivelyEscalate(this._lastTurnToolChars)) {
|
|
2695
|
+
logger.info("orchestrator", "Reactive escalation to sub-session (prior turn tool-heavy)", {
|
|
2696
|
+
prevTurnToolChars: this._lastTurnToolChars,
|
|
2697
|
+
});
|
|
2698
|
+
routeAction = "SPAWN_SUB_SESSION";
|
|
2699
|
+
}
|
|
2192
2700
|
const shouldRotate = currentChars > threshold || routeAction === "ROTATE_SESSION";
|
|
2193
2701
|
if (shouldRotate && this.session && this.sessionStore) {
|
|
2194
2702
|
logger.info("orchestrator", "Triggering silent session rotation", {
|
|
@@ -2203,10 +2711,10 @@ export class Agent {
|
|
|
2203
2711
|
const { deliberateCompact } = await import("../flow/compaction/index.js");
|
|
2204
2712
|
const { getDatabase } = await import("../storage/db.js");
|
|
2205
2713
|
const { appendCompaction, getNextMessageSequence } = await import("../storage/transcript.js");
|
|
2206
|
-
const cr = await deliberateCompact(flowDir, this.messages, "", 4096, this.
|
|
2714
|
+
const cr = await deliberateCompact(flowDir, this.messages, "", 4096, this.modelId);
|
|
2207
2715
|
const newSession = this.sessionStore.createSession(this.modelId, this.mode, this.bash.getCwd());
|
|
2208
2716
|
const db = getDatabase();
|
|
2209
|
-
|
|
2717
|
+
this.sessionStore.linkChild(newSession.id, parentSessionId, "rotation");
|
|
2210
2718
|
const summaryMessage = createCompactionSummaryMessage(cr.summary);
|
|
2211
2719
|
const nextSeq = getNextMessageSequence(newSession.id);
|
|
2212
2720
|
appendCompaction(newSession.id, nextSeq, cr.summary, cr.tokensBeforeCompress);
|
|
@@ -2229,10 +2737,10 @@ export class Agent {
|
|
|
2229
2737
|
let parentSessionId = null;
|
|
2230
2738
|
let subSessionId = null;
|
|
2231
2739
|
if (routeAction === "SPAWN_SUB_SESSION" && this.session && this.sessionStore) {
|
|
2232
|
-
yield { type: "
|
|
2740
|
+
yield { type: "toast", toastLevel: "info", content: "Đang khởi tạo sub-session ngầm để xử lý tác vụ..." };
|
|
2233
2741
|
parentSessionId = this.session.id;
|
|
2234
2742
|
try {
|
|
2235
|
-
const { loadLatestCompaction, getNextMessageSequence, appendCompaction
|
|
2743
|
+
const { loadLatestCompaction, getNextMessageSequence, appendCompaction } = await import("../storage/transcript.js");
|
|
2236
2744
|
const { getDatabase } = await import("../storage/db.js");
|
|
2237
2745
|
const db = getDatabase();
|
|
2238
2746
|
// Check if there is already an active child session for this parent session
|
|
@@ -2259,9 +2767,13 @@ export class Agent {
|
|
|
2259
2767
|
}
|
|
2260
2768
|
}
|
|
2261
2769
|
if (shouldResume && subSessionId) {
|
|
2262
|
-
yield {
|
|
2770
|
+
yield {
|
|
2771
|
+
type: "toast",
|
|
2772
|
+
toastLevel: "info",
|
|
2773
|
+
content: "Phát hiện sub-session trước đó bị gián đoạn, đang khôi phục...",
|
|
2774
|
+
};
|
|
2263
2775
|
this.session = this.sessionStore.getRequiredSession(subSessionId);
|
|
2264
|
-
const childState =
|
|
2776
|
+
const childState = loadSessionChainTranscriptState(subSessionId);
|
|
2265
2777
|
this.messages = childState.messages;
|
|
2266
2778
|
this.messageSeqs = childState.seqs;
|
|
2267
2779
|
this.sessionStore.touchSession(subSessionId, this.bash.getCwd());
|
|
@@ -2274,21 +2786,26 @@ export class Agent {
|
|
|
2274
2786
|
else {
|
|
2275
2787
|
const latest = loadLatestCompaction(parentSessionId);
|
|
2276
2788
|
const newSession = this.sessionStore.createSession(this.modelId, this.mode, this.bash.getCwd());
|
|
2277
|
-
|
|
2789
|
+
this.sessionStore.linkChild(newSession.id, parentSessionId, "subagent");
|
|
2278
2790
|
subSessionId = newSession.id;
|
|
2279
|
-
|
|
2280
|
-
|
|
2791
|
+
// Seed the child with the parent's CURRENT working set — after a
|
|
2792
|
+
// compaction that is already [summary, ...kept raw tail] (see the
|
|
2793
|
+
// compactForContext assignment). Previously the compaction branch
|
|
2794
|
+
// reseeded ONLY [summary], discarding the kept raw tail — so a forked
|
|
2795
|
+
// council/debate saw a bare generic summary instead of the actual
|
|
2796
|
+
// recent discussion the user pointed at ("debate THESE parts"), and
|
|
2797
|
+
// drifted off-topic. Keep the tail; just guarantee the summary is
|
|
2798
|
+
// present and persist the compaction row for the child's continuity.
|
|
2799
|
+
const seedMessages = [...this.messages];
|
|
2800
|
+
const seedSeqs = [...this.messageSeqs];
|
|
2281
2801
|
if (latest?.summary) {
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2802
|
+
if (!isCompactionSummaryMessage(seedMessages[0])) {
|
|
2803
|
+
seedMessages.unshift(createCompactionSummaryMessage(latest.summary));
|
|
2804
|
+
seedSeqs.unshift(null);
|
|
2805
|
+
}
|
|
2285
2806
|
const nextSeq = getNextMessageSequence(subSessionId);
|
|
2286
2807
|
appendCompaction(subSessionId, nextSeq, latest.summary, latest.tokensBefore);
|
|
2287
2808
|
}
|
|
2288
|
-
else {
|
|
2289
|
-
seedMessages = [...this.messages];
|
|
2290
|
-
seedSeqs = [...this.messageSeqs];
|
|
2291
|
-
}
|
|
2292
2809
|
// Add sub-session overlay system message
|
|
2293
2810
|
const overlayMessage = {
|
|
2294
2811
|
role: "system",
|
|
@@ -2322,6 +2839,22 @@ export class Agent {
|
|
|
2322
2839
|
const autoCommitOn = isAutoCommitEnabled();
|
|
2323
2840
|
const cwd = this.bash.getCwd();
|
|
2324
2841
|
const dirtyBefore = autoCommitOn ? await snapshotDirtyPaths(cwd) : new Set();
|
|
2842
|
+
// Top-level turn watchdog. The per-chunk stall watchdog only covers
|
|
2843
|
+
// streamText byte flow; it does NOT cover a turn generator that keeps the
|
|
2844
|
+
// socket open but never RETURNS after the model is done. Observed live:
|
|
2845
|
+
// xai/grok-composer-2.5-fast fires onFinish/llm-done (finishReason=stop) but
|
|
2846
|
+
// its stream generator never terminates, so processor.run() hangs, the UI's
|
|
2847
|
+
// `for await` never ends, finalizeActiveTurn never runs, and the TUI stays
|
|
2848
|
+
// frozen in the "processing" state (partial/raw markdown) until the next
|
|
2849
|
+
// message forces a new run. Mirrors the council-continuation guard (idle +
|
|
2850
|
+
// hard total). On fire we abort the turn and emit a terminal `done` so the
|
|
2851
|
+
// consumer finalizes cleanly instead of hanging forever. idleMs resets on
|
|
2852
|
+
// every yielded chunk, so long legitimate tool calls are safe; totalMs is a
|
|
2853
|
+
// hard ceiling (0 = disabled by default, since a big multi-tool turn is not a
|
|
2854
|
+
// hang). Env-overridable.
|
|
2855
|
+
const { withTurnWatchdog, TurnStallError } = await import("./turn-watchdog.js");
|
|
2856
|
+
const turnIdleMs = Number(process.env.MUONROI_TURN_IDLE_MS ?? 120_000);
|
|
2857
|
+
const turnTotalMs = Number(process.env.MUONROI_TURN_TOTAL_MS ?? 0);
|
|
2325
2858
|
try {
|
|
2326
2859
|
let attempts = 0;
|
|
2327
2860
|
const maxAttempts = 3;
|
|
@@ -2330,7 +2863,30 @@ export class Agent {
|
|
|
2330
2863
|
while (attempts < maxAttempts) {
|
|
2331
2864
|
try {
|
|
2332
2865
|
attempts++;
|
|
2333
|
-
|
|
2866
|
+
try {
|
|
2867
|
+
yield* withTurnWatchdog(processor.run(userMessage, observer, images), {
|
|
2868
|
+
idleMs: turnIdleMs,
|
|
2869
|
+
totalMs: turnTotalMs,
|
|
2870
|
+
label: "assistant turn",
|
|
2871
|
+
shouldSuppressFire: isInteractivePaused,
|
|
2872
|
+
});
|
|
2873
|
+
}
|
|
2874
|
+
catch (stallErr) {
|
|
2875
|
+
// A hung turn is NOT a transient error — retrying it (below) would
|
|
2876
|
+
// just hang again. Abort, surface a toast, and terminate the turn.
|
|
2877
|
+
if (stallErr instanceof TurnStallError) {
|
|
2878
|
+
logger.warn("orchestrator", "Top-level turn watchdog fired — finalizing turn", {
|
|
2879
|
+
kind: stallErr.kind,
|
|
2880
|
+
message: stallErr.message,
|
|
2881
|
+
});
|
|
2882
|
+
this.abortController?.abort(new DOMException(stallErr.message, "TimeoutError"));
|
|
2883
|
+
yield { type: "toast", toastLevel: "warn", content: `Turn ended by watchdog: ${stallErr.message}` };
|
|
2884
|
+
yield { type: "done" };
|
|
2885
|
+
}
|
|
2886
|
+
else {
|
|
2887
|
+
throw stallErr;
|
|
2888
|
+
}
|
|
2889
|
+
}
|
|
2334
2890
|
break;
|
|
2335
2891
|
}
|
|
2336
2892
|
catch (err) {
|
|
@@ -2516,6 +3072,42 @@ export class Agent {
|
|
|
2516
3072
|
self._compactedThisTurn = v;
|
|
2517
3073
|
},
|
|
2518
3074
|
getCompactedThisTurn: () => self._compactedThisTurn,
|
|
3075
|
+
getCompactionStats: () => self.getCompactionStats(),
|
|
3076
|
+
reportTurnToolLoad: (chars) => {
|
|
3077
|
+
self._lastTurnToolChars = Number.isFinite(chars) && chars > 0 ? chars : 0;
|
|
3078
|
+
self._turnLoadOrdinal += 1;
|
|
3079
|
+
// Cold-first-turn instrument: record every turn whose load crosses the
|
|
3080
|
+
// reactive threshold, tagged with its ordinal. A later query counts what
|
|
3081
|
+
// fraction of threshold-crossing turns are ordinal 1 (the un-isolatable
|
|
3082
|
+
// cold turn) — the evidence that gates building an in-turn checkpoint.
|
|
3083
|
+
try {
|
|
3084
|
+
if (self.session && self._lastTurnToolChars >= getReactiveDelegationThresholdChars()) {
|
|
3085
|
+
// isolated = the heavy turn ran in a forked child (router SPAWN,
|
|
3086
|
+
// reactive escalation, or rotation), so the parent never held it.
|
|
3087
|
+
// The cold-first-turn HOLE is specifically coldFirstTurn && !isolated
|
|
3088
|
+
// — a turn-1 blow-up that landed in the parent because neither the
|
|
3089
|
+
// router nor reactive escalation caught it. Recording `isolated`
|
|
3090
|
+
// stops the metric from over-counting router-isolated heavy turns.
|
|
3091
|
+
const kind = self.sessionStore?.getSessionKind(self.session.id) ?? "conversation";
|
|
3092
|
+
const isolated = kind !== "conversation";
|
|
3093
|
+
logInteraction(self.session.id, "turn_tool_load", {
|
|
3094
|
+
data: {
|
|
3095
|
+
chars: self._lastTurnToolChars,
|
|
3096
|
+
ordinal: self._turnLoadOrdinal,
|
|
3097
|
+
coldFirstTurn: self._turnLoadOrdinal === 1,
|
|
3098
|
+
isolated,
|
|
3099
|
+
kind,
|
|
3100
|
+
threshold: getReactiveDelegationThresholdChars(),
|
|
3101
|
+
},
|
|
3102
|
+
});
|
|
3103
|
+
}
|
|
3104
|
+
}
|
|
3105
|
+
catch (err) {
|
|
3106
|
+
logger.debug("orchestrator", "turn_tool_load telemetry failed (best-effort)", {
|
|
3107
|
+
error: err?.message,
|
|
3108
|
+
});
|
|
3109
|
+
}
|
|
3110
|
+
},
|
|
2519
3111
|
setTurnUserGoalExcerpt: (v) => {
|
|
2520
3112
|
self._turnUserGoalExcerpt = v;
|
|
2521
3113
|
},
|
|
@@ -2555,14 +3147,13 @@ export class Agent {
|
|
|
2555
3147
|
`${serializedParent}\n\n` +
|
|
2556
3148
|
`Provide clear, actionable guidance to resolve the child's query.`;
|
|
2557
3149
|
const { generateText } = await import("ai");
|
|
2558
|
-
const provider = self.requireProvider();
|
|
2559
3150
|
const modelId = self.modelId;
|
|
2560
|
-
const runtime = resolveModelRuntime(
|
|
3151
|
+
const runtime = resolveModelRuntime(modelId);
|
|
2561
3152
|
const result = await generateText({
|
|
2562
3153
|
model: runtime.model,
|
|
2563
3154
|
system: systemPrompt,
|
|
2564
3155
|
prompt: `Child Sub-session is stuck. Question:\n${question}`,
|
|
2565
|
-
|
|
3156
|
+
...resolveTemperatureParam(runtime, 0.2),
|
|
2566
3157
|
...(runtime.providerOptions ? { providerOptions: runtime.providerOptions } : {}),
|
|
2567
3158
|
});
|
|
2568
3159
|
return result.text;
|
|
@@ -2575,6 +3166,7 @@ export class Agent {
|
|
|
2575
3166
|
countFilesTouched: () => self._countFilesTouched(),
|
|
2576
3167
|
getCompactionSettings: (cw) => self.getCompactionSettings(cw),
|
|
2577
3168
|
compactForContext: (provider, system, cw, signal, settings, overflow) => self.compactForContext(provider, system, cw, signal, settings, overflow),
|
|
3169
|
+
extendHardCeilingForAutoCompaction: () => self.extendHardCeilingForAutoCompaction(),
|
|
2578
3170
|
postTurnCompact: (provider, system, cw, signal) => self.postTurnCompact(provider, system, cw, signal),
|
|
2579
3171
|
runTask: (request, signal) => self.runTask(request, signal),
|
|
2580
3172
|
runDelegation: (request, signal) => self.runDelegation(request, signal),
|
|
@@ -2613,6 +3205,25 @@ export class Agent {
|
|
|
2613
3205
|
return { action: "block" };
|
|
2614
3206
|
}
|
|
2615
3207
|
},
|
|
3208
|
+
askUser: async (info) => {
|
|
3209
|
+
const h = self._askUserHandler;
|
|
3210
|
+
if (!h) {
|
|
3211
|
+
logger.warn("orchestrator", "askUser called but no handler registered — returning dismissed sentinel");
|
|
3212
|
+
return ASK_USER_DISMISSED;
|
|
3213
|
+
}
|
|
3214
|
+
// Hold both watchdogs open while the human answers (see interactive-pause.ts).
|
|
3215
|
+
beginInteractivePause();
|
|
3216
|
+
try {
|
|
3217
|
+
return await h(info);
|
|
3218
|
+
}
|
|
3219
|
+
catch (err) {
|
|
3220
|
+
logger.error("orchestrator", "askUser crashed", { error: err });
|
|
3221
|
+
return ASK_USER_DISMISSED;
|
|
3222
|
+
}
|
|
3223
|
+
finally {
|
|
3224
|
+
endInteractivePause();
|
|
3225
|
+
}
|
|
3226
|
+
},
|
|
2616
3227
|
runCouncilV2: (msg, opts) => self.runCouncilV2(msg, opts),
|
|
2617
3228
|
processMessage: (msg, obs, imgs) => self.processMessage(msg, obs, imgs),
|
|
2618
3229
|
processMessageBatchTurn: (args) => self.processMessageBatchTurn(args),
|
|
@@ -2633,20 +3244,31 @@ export class Agent {
|
|
|
2633
3244
|
const recent = this.messages.slice(-6);
|
|
2634
3245
|
const parts = [];
|
|
2635
3246
|
for (const msg of recent) {
|
|
2636
|
-
if (msg.role !== "user" && msg.role !== "assistant")
|
|
3247
|
+
if (msg.role !== "user" && msg.role !== "assistant" && msg.role !== "tool")
|
|
2637
3248
|
continue;
|
|
2638
|
-
|
|
2639
|
-
|
|
2640
|
-
|
|
2641
|
-
? msg.content
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
.
|
|
2645
|
-
|
|
3249
|
+
let text = "";
|
|
3250
|
+
if (msg.role === "tool") {
|
|
3251
|
+
text = Array.isArray(msg.content)
|
|
3252
|
+
? msg.content.map((p) => (typeof p.result === "string" ? p.result : JSON.stringify(p.result))).join(" ")
|
|
3253
|
+
: typeof msg.content === "string"
|
|
3254
|
+
? msg.content
|
|
3255
|
+
: JSON.stringify(msg.content);
|
|
3256
|
+
}
|
|
3257
|
+
else {
|
|
3258
|
+
text =
|
|
3259
|
+
typeof msg.content === "string"
|
|
3260
|
+
? msg.content
|
|
3261
|
+
: Array.isArray(msg.content)
|
|
3262
|
+
? msg.content
|
|
3263
|
+
.filter((p) => p.type === "text")
|
|
3264
|
+
.map((p) => p.text ?? "")
|
|
3265
|
+
.join("")
|
|
3266
|
+
: "";
|
|
3267
|
+
}
|
|
2646
3268
|
if (!text)
|
|
2647
3269
|
continue;
|
|
2648
|
-
const snippet = text.length >
|
|
2649
|
-
parts.push(`[${msg.role}]: ${snippet}`);
|
|
3270
|
+
const snippet = text.length > 300 ? `${text.slice(0, 297)}...` : text;
|
|
3271
|
+
parts.push(`[${msg.role}]: ${snippet.replace(/\n/g, " ")}`);
|
|
2650
3272
|
}
|
|
2651
3273
|
return parts.length > 0 ? parts.join(" | ") : null;
|
|
2652
3274
|
}
|
|
@@ -2716,48 +3338,129 @@ export class Agent {
|
|
|
2716
3338
|
if (this._oauthInitDone)
|
|
2717
3339
|
return;
|
|
2718
3340
|
this._oauthInitDone = true;
|
|
2719
|
-
//
|
|
2720
|
-
//
|
|
2721
|
-
//
|
|
2722
|
-
//
|
|
2723
|
-
//
|
|
2724
|
-
|
|
2725
|
-
|
|
3341
|
+
// Re-resolve auth for the CURRENT provider. Runs when either:
|
|
3342
|
+
// (a) no explicit API key / the "oauth" sentinel is held — OAuth may
|
|
3343
|
+
// apply (the boot wizard in src/index.ts uses the literal "oauth" to
|
|
3344
|
+
// mean "no key but OAuth tokens exist"); OR
|
|
3345
|
+
// (b) setModel deferred provider construction on a provider switch
|
|
3346
|
+
// (this.provider === null) — the PREVIOUS provider's key was cleared
|
|
3347
|
+
// and must be re-resolved from the NEW provider's own credentials.
|
|
3348
|
+
const providerDeferred = this.provider === null;
|
|
3349
|
+
const keyIsSentinelOrEmpty = !this.apiKey || this.apiKey === "oauth";
|
|
2726
3350
|
try {
|
|
2727
|
-
const { listOAuthProviderIds } = await import("../providers/auth/registry.js");
|
|
3351
|
+
const { listOAuthProviderIds, getOAuthProviderConfig } = await import("../providers/auth/registry.js");
|
|
2728
3352
|
const ids = await listOAuthProviderIds();
|
|
2729
|
-
|
|
3353
|
+
const oauthCapable = ids.includes(this.providerId);
|
|
3354
|
+
// One auth mode per provider (OAuth XOR API key): if this OAuth-capable
|
|
3355
|
+
// provider has a live token, OAuth wins OUTRIGHT — even over a real held
|
|
3356
|
+
// API key. This is the shadowing-bug fix: a stale sk-proj key must not
|
|
3357
|
+
// beat a valid ChatGPT OAuth login. Check token presence BEFORE the
|
|
3358
|
+
// early-out so a held key can no longer suppress OAuth.
|
|
3359
|
+
let hasOAuthTokens = false;
|
|
3360
|
+
if (oauthCapable) {
|
|
3361
|
+
try {
|
|
3362
|
+
const cfg = await getOAuthProviderConfig(this.providerId);
|
|
3363
|
+
const tokens = cfg ? await cfg.loadTokensWithRefresh().catch(() => null) : null;
|
|
3364
|
+
hasOAuthTokens = !!tokens?.accessToken;
|
|
3365
|
+
}
|
|
3366
|
+
catch {
|
|
3367
|
+
hasOAuthTokens = false;
|
|
3368
|
+
}
|
|
3369
|
+
}
|
|
3370
|
+
// Nothing to do only when: no OAuth token, a real key is held, and the
|
|
3371
|
+
// provider was not deferred by a model switch.
|
|
3372
|
+
if (!hasOAuthTokens && !providerDeferred && !keyIsSentinelOrEmpty)
|
|
2730
3373
|
return;
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
3374
|
+
// OAuth path: inject subscription tokens as Bearer headers. Taken when a
|
|
3375
|
+
// token exists (override a held key), OR when no real key is held for an
|
|
3376
|
+
// OAuth-capable provider.
|
|
3377
|
+
if (oauthCapable && (hasOAuthTokens || keyIsSentinelOrEmpty)) {
|
|
3378
|
+
const effectiveBaseURL = this.baseURL &&
|
|
3379
|
+
this.baseURL !== (await import("../providers/endpoints.js").then((m) => m.apiBaseFor("anthropic")))
|
|
3380
|
+
? this.baseURL
|
|
3381
|
+
: undefined;
|
|
3382
|
+
const result = await createProviderFactoryAsync(this.providerId, {
|
|
3383
|
+
baseURL: effectiveBaseURL ?? undefined,
|
|
3384
|
+
});
|
|
3385
|
+
this.apiKey = "oauth";
|
|
3386
|
+
this.provider = result.factory;
|
|
3387
|
+
return;
|
|
3388
|
+
}
|
|
3389
|
+
// API-key path: resolve THIS provider's own stored key. Reached when the
|
|
3390
|
+
// provider isn't OAuth-capable, or when a provider switch deferred
|
|
3391
|
+
// construction (we must swap in the NEW provider's key — never reuse the
|
|
3392
|
+
// previous provider's, which 401'd on every TUI provider switch). If no
|
|
3393
|
+
// key exists for a deferred switch, leave the provider null so
|
|
3394
|
+
// requireProvider() surfaces a clear "API key required".
|
|
3395
|
+
try {
|
|
3396
|
+
const key = await loadKeyForProvider(this.providerId);
|
|
3397
|
+
if (key) {
|
|
3398
|
+
this.apiKey = key;
|
|
3399
|
+
const staleBaseURL = isAnyProviderApiBase(this.baseURL) && this.baseURL !== apiBaseFor(this.providerId);
|
|
3400
|
+
if (staleBaseURL)
|
|
3401
|
+
this.baseURL = null;
|
|
3402
|
+
this.provider = createProvider(this.providerId, key, this.baseURL ?? undefined);
|
|
3403
|
+
}
|
|
3404
|
+
else if (providerDeferred) {
|
|
3405
|
+
this.provider = null;
|
|
3406
|
+
}
|
|
3407
|
+
}
|
|
3408
|
+
catch (err) {
|
|
3409
|
+
// ProviderKeyMissingError (or keychain failure) — no usable key. For a
|
|
3410
|
+
// deferred provider switch, null the provider so the next turn fails
|
|
3411
|
+
// loudly ("API key required") instead of 401-ing on a wrong/sentinel
|
|
3412
|
+
// key. For a non-deferred empty-key case, leave the existing provider
|
|
3413
|
+
// untouched (fail-open, as before).
|
|
3414
|
+
if (providerDeferred) {
|
|
3415
|
+
console.error(`[orchestrator] no API key for provider '${this.providerId}' after model switch: ${err instanceof Error ? err.message : String(err)}`);
|
|
3416
|
+
this.provider = null;
|
|
3417
|
+
}
|
|
3418
|
+
}
|
|
2739
3419
|
}
|
|
2740
3420
|
catch {
|
|
2741
|
-
//
|
|
3421
|
+
// Registry unavailable — fail-open; the existing provider (if any) is
|
|
3422
|
+
// left untouched and requireProvider() will surface any error.
|
|
2742
3423
|
}
|
|
2743
3424
|
}
|
|
2744
3425
|
async detectVerifyRecipe(settings, abortSignal) {
|
|
3426
|
+
const cwd = this.bash.getCwd();
|
|
3427
|
+
const effectiveSettings = settings ?? this.bash.getSandboxSettings();
|
|
3428
|
+
// Primary: LLM verify-detect turn — a codebase-aware, richer recipe.
|
|
3429
|
+
let llmRecipe = null;
|
|
2745
3430
|
try {
|
|
2746
3431
|
const result = await this.runTaskRequest({
|
|
2747
3432
|
agent: "verify-detect",
|
|
2748
3433
|
description: "Detect verification recipe",
|
|
2749
|
-
prompt: buildVerifyDetectPrompt(
|
|
3434
|
+
prompt: buildVerifyDetectPrompt(cwd, effectiveSettings),
|
|
2750
3435
|
}, undefined, abortSignal);
|
|
2751
|
-
if (
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
return normalizeVerifyRecipe(JSON.parse(maybeJson));
|
|
3436
|
+
if (result.success && result.output) {
|
|
3437
|
+
const maybeJson = extractJsonObject(result.output);
|
|
3438
|
+
if (maybeJson)
|
|
3439
|
+
llmRecipe = normalizeVerifyRecipe(JSON.parse(maybeJson));
|
|
3440
|
+
}
|
|
2757
3441
|
}
|
|
2758
|
-
catch {
|
|
2759
|
-
|
|
3442
|
+
catch (err) {
|
|
3443
|
+
console.error(`[orchestrator] verify-detect turn failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
3444
|
+
}
|
|
3445
|
+
if (llmRecipe)
|
|
3446
|
+
return llmRecipe;
|
|
3447
|
+
// Deterministic fallback: an existing repo with a recognizable manifest
|
|
3448
|
+
// (package.json test script, *.sln, pyproject, …) yields a reliable recipe
|
|
3449
|
+
// WITHOUT an LLM. This stops CB-3 from false-halting an in-place /ideal
|
|
3450
|
+
// migration with the "Recovery options" card purely because the flaky
|
|
3451
|
+
// verify-detect model returned no parseable JSON (root cause of the
|
|
3452
|
+
// existing-repo implementation-reachability halt). Only trusted when the
|
|
3453
|
+
// profiler recognizes the ecosystem AND found real test commands.
|
|
3454
|
+
try {
|
|
3455
|
+
const profile = inferVerifyProjectProfile(cwd, effectiveSettings);
|
|
3456
|
+
if (shouldTrustDeterministicRecipe(profile.recipe)) {
|
|
3457
|
+
return profile.recipe;
|
|
3458
|
+
}
|
|
2760
3459
|
}
|
|
3460
|
+
catch (err) {
|
|
3461
|
+
console.error(`[orchestrator] deterministic verify-recipe fallback failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
3462
|
+
}
|
|
3463
|
+
return null;
|
|
2761
3464
|
}
|
|
2762
3465
|
async runVerify(onProgress, abortSignal) {
|
|
2763
3466
|
this.abortController = new AbortController();
|