@cjhyy/code-shell-core 0.5.0-rc.2 → 0.6.0-rc.2
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/README.md +13 -10
- package/dist/agent/agent-definition-registry.d.ts +3 -0
- package/dist/agent/agent-definition-registry.js +3 -1
- package/dist/agent/agent-definition.d.ts +15 -0
- package/dist/agent/agent-definition.js +27 -3
- package/dist/arena/context/context-tools.js +47 -4
- package/dist/arena/ledger.js +9 -1
- package/dist/arena/strategies/utils.d.ts +1 -4
- package/dist/arena/strategies/utils.js +7 -84
- package/dist/automation/cron-expr.d.ts +10 -0
- package/dist/automation/cron-expr.js +23 -5
- package/dist/automation/runner.d.ts +21 -4
- package/dist/automation/runner.js +48 -12
- package/dist/automation/scheduler.d.ts +38 -0
- package/dist/automation/scheduler.js +89 -12
- package/dist/automation/store.js +5 -2
- package/dist/capability-control/disabled-lists.d.ts +25 -0
- package/dist/capability-control/disabled-lists.js +57 -0
- package/dist/capability-control/overlay.d.ts +15 -0
- package/dist/capability-control/overlay.js +27 -0
- package/dist/cc-orchestrator/agent-adapter.d.ts +50 -0
- package/dist/cc-orchestrator/agent-adapter.js +133 -0
- package/dist/cc-orchestrator/cc-capability.d.ts +19 -0
- package/dist/cc-orchestrator/cc-capability.js +53 -0
- package/dist/cc-orchestrator/codex-session-discovery.d.ts +24 -0
- package/dist/cc-orchestrator/codex-session-discovery.js +191 -0
- package/dist/cc-orchestrator/codex-session-history.d.ts +25 -0
- package/dist/cc-orchestrator/codex-session-history.js +187 -0
- package/dist/cc-orchestrator/external-agent-changes.d.ts +19 -0
- package/dist/cc-orchestrator/external-agent-changes.js +214 -0
- package/dist/cc-orchestrator/external-agent-driver.d.ts +18 -0
- package/dist/cc-orchestrator/external-agent-driver.js +69 -0
- package/dist/cc-orchestrator/index.d.ts +8 -0
- package/dist/cc-orchestrator/index.js +8 -0
- package/dist/cc-orchestrator/relevance-judge.d.ts +15 -0
- package/dist/cc-orchestrator/relevance-judge.js +29 -0
- package/dist/cc-orchestrator/session-discovery.d.ts +46 -0
- package/dist/cc-orchestrator/session-discovery.js +125 -0
- package/dist/cc-orchestrator/session-history.d.ts +19 -0
- package/dist/cc-orchestrator/session-history.js +67 -0
- package/dist/cli/agent-server-stdio.d.ts +15 -1
- package/dist/cli/agent-server-stdio.js +115 -17
- package/dist/cli/agent-server-tcp.js +40 -26
- package/dist/context/compaction.d.ts +56 -0
- package/dist/context/compaction.js +186 -0
- package/dist/context/manager.js +25 -4
- package/dist/cost-tracker.js +5 -61
- package/dist/credentials/cipher.d.ts +49 -0
- package/dist/credentials/cipher.js +45 -0
- package/dist/credentials/cookie-jar.d.ts +24 -0
- package/dist/credentials/cookie-jar.js +40 -0
- package/dist/credentials/index.d.ts +7 -0
- package/dist/credentials/index.js +5 -0
- package/dist/credentials/inject-credential-tool.d.ts +20 -0
- package/dist/credentials/inject-credential-tool.js +130 -0
- package/dist/credentials/store.d.ts +72 -0
- package/dist/credentials/store.js +184 -0
- package/dist/credentials/types.d.ts +56 -0
- package/dist/credentials/use-credential-tool.d.ts +29 -0
- package/dist/credentials/use-credential-tool.js +205 -0
- package/dist/credentials/use-gate.d.ts +56 -0
- package/dist/credentials/use-gate.js +52 -0
- package/dist/data/model-metadata.d.ts +77 -0
- package/dist/data/model-metadata.js +56 -0
- package/dist/data/model-metadata.json +216 -0
- package/dist/data/openrouter-models.d.ts +18 -7
- package/dist/data/openrouter-models.js +35 -8
- package/dist/engine/aux-key.d.ts +10 -0
- package/dist/engine/aux-key.js +11 -0
- package/dist/engine/dynamic-tool-defs.d.ts +19 -0
- package/dist/engine/dynamic-tool-defs.js +36 -0
- package/dist/engine/engine.d.ts +267 -138
- package/dist/engine/engine.js +957 -191
- package/dist/engine/friendly-error.d.ts +18 -0
- package/dist/engine/friendly-error.js +63 -0
- package/dist/engine/goal.d.ts +145 -0
- package/dist/engine/goal.js +144 -0
- package/dist/engine/image-policy.d.ts +13 -0
- package/dist/engine/image-policy.js +24 -0
- package/dist/engine/model-connections-pool.d.ts +17 -0
- package/dist/engine/model-connections-pool.js +66 -0
- package/dist/engine/model-facade.d.ts +10 -0
- package/dist/engine/model-facade.js +25 -0
- package/dist/engine/patch-orphaned-tools.js +3 -0
- package/dist/engine/resolve-llm-config.d.ts +16 -0
- package/dist/engine/resolve-llm-config.js +44 -0
- package/dist/engine/runtime.js +2 -1
- package/dist/engine/sandbox-cache-key.d.ts +10 -0
- package/dist/engine/sandbox-cache-key.js +9 -0
- package/dist/engine/sandbox-config.d.ts +31 -0
- package/dist/engine/sandbox-config.js +38 -0
- package/dist/engine/session-usage.d.ts +19 -0
- package/dist/engine/session-usage.js +25 -0
- package/dist/engine/steer-queue.d.ts +32 -0
- package/dist/engine/steer-queue.js +26 -0
- package/dist/engine/streaming-tool-queue.d.ts +12 -0
- package/dist/engine/streaming-tool-queue.js +50 -10
- package/dist/engine/turn-loop.d.ts +67 -3
- package/dist/engine/turn-loop.js +292 -41
- package/dist/engine/types.d.ts +175 -0
- package/dist/engine/types.js +13 -0
- package/dist/external-agents/config.d.ts +2 -0
- package/dist/external-agents/config.js +15 -0
- package/dist/external-agents/types.d.ts +31 -0
- package/dist/external-agents/types.js +1 -0
- package/dist/git/utils.js +11 -6
- package/dist/git/worktree.d.ts +48 -0
- package/dist/git/worktree.js +86 -10
- package/dist/hooks/goal-stop-hook.d.ts +28 -0
- package/dist/hooks/goal-stop-hook.js +186 -9
- package/dist/hooks/registry.d.ts +8 -0
- package/dist/hooks/registry.js +19 -0
- package/dist/hooks/shell-runner.js +10 -21
- package/dist/index.d.ts +49 -6
- package/dist/index.js +60 -4
- package/dist/llm/capabilities/param-specs.d.ts +14 -0
- package/dist/llm/capabilities/param-specs.js +62 -0
- package/dist/llm/capabilities/rules.js +4 -0
- package/dist/llm/capabilities/types.d.ts +10 -0
- package/dist/llm/client-base.d.ts +28 -1
- package/dist/llm/client-base.js +119 -13
- package/dist/llm/model-cache.js +4 -2
- package/dist/llm/model-pool.d.ts +14 -0
- package/dist/llm/model-pool.js +13 -0
- package/dist/llm/provider-auth.d.ts +41 -0
- package/dist/llm/provider-auth.js +76 -0
- package/dist/llm/provider-catalog.d.ts +4 -0
- package/dist/llm/providers/anthropic.js +64 -14
- package/dist/llm/providers/openai.d.ts +36 -0
- package/dist/llm/providers/openai.js +257 -51
- package/dist/llm/reasoning-setting.d.ts +3 -3
- package/dist/llm/reasoning-setting.js +9 -1
- package/dist/llm/stream-watchdog.js +5 -1
- package/dist/llm/token-counter.js +9 -2
- package/dist/llm/types.d.ts +4 -0
- package/dist/logging/sanitize-messages.js +22 -0
- package/dist/lsp/manager.d.ts +1 -1
- package/dist/lsp/manager.js +40 -10
- package/dist/model-catalog/builtin.d.ts +12 -0
- package/dist/model-catalog/builtin.js +407 -0
- package/dist/model-catalog/gen-connections.d.ts +20 -0
- package/dist/model-catalog/gen-connections.js +28 -0
- package/dist/model-catalog/index.d.ts +41 -0
- package/dist/model-catalog/index.js +90 -0
- package/dist/model-catalog/params.d.ts +20 -0
- package/dist/model-catalog/params.js +45 -0
- package/dist/model-catalog/resolve.d.ts +48 -0
- package/dist/model-catalog/resolve.js +33 -0
- package/dist/model-catalog/save-entry.d.ts +32 -0
- package/dist/model-catalog/save-entry.js +104 -0
- package/dist/model-catalog/types.d.ts +561 -0
- package/dist/model-catalog/types.js +93 -0
- package/dist/model-catalog/upsert.d.ts +9 -0
- package/dist/model-catalog/upsert.js +8 -0
- package/dist/onboarding.d.ts +12 -82
- package/dist/onboarding.js +61 -326
- package/dist/plugins/gitOps.d.ts +19 -0
- package/dist/plugins/gitOps.js +73 -4
- package/dist/plugins/installer/checkUpdate.d.ts +16 -0
- package/dist/plugins/installer/checkUpdate.js +32 -0
- package/dist/plugins/installer/codex/convertCommands.d.ts +19 -0
- package/dist/plugins/installer/codex/convertCommands.js +46 -0
- package/dist/plugins/installer/codex/convertMcp.d.ts +5 -2
- package/dist/plugins/installer/codex/convertMcp.js +45 -5
- package/dist/plugins/installer/install.js +25 -1
- package/dist/plugins/installer/installFromArchive.d.ts +43 -0
- package/dist/plugins/installer/installFromArchive.js +134 -0
- package/dist/plugins/installer/installFromSource.js +8 -2
- package/dist/plugins/installer/loadPluginAgents.js +6 -2
- package/dist/plugins/installer/loadPluginMcp.d.ts +9 -2
- package/dist/plugins/installer/loadPluginMcp.js +35 -2
- package/dist/plugins/installer/pruneDisabled.d.ts +24 -0
- package/dist/plugins/installer/pruneDisabled.js +73 -0
- package/dist/plugins/installer/types.d.ts +5 -2
- package/dist/plugins/installer/types.js +1 -0
- package/dist/plugins/installer/uninstall.js +4 -0
- package/dist/plugins/installer/unzip.d.ts +14 -0
- package/dist/plugins/installer/unzip.js +82 -0
- package/dist/plugins/installer/update.d.ts +14 -0
- package/dist/plugins/installer/update.js +53 -21
- package/dist/plugins/loadPluginHooks.d.ts +47 -1
- package/dist/plugins/loadPluginHooks.js +73 -1
- package/dist/plugins/marketplaceManager.d.ts +7 -0
- package/dist/plugins/marketplaceManager.js +20 -0
- package/dist/plugins/pluginCommandHook.js +4 -18
- package/dist/plugins/pluginContent.d.ts +30 -0
- package/dist/plugins/pluginContent.js +83 -0
- package/dist/plugins/pluginInstaller.d.ts +13 -0
- package/dist/plugins/pluginInstaller.js +42 -8
- package/dist/plugins/schemas.js +1 -0
- package/dist/plugins/types.d.ts +6 -0
- package/dist/preset/index.d.ts +7 -1
- package/dist/preset/index.js +96 -6
- package/dist/product/types.d.ts +1 -1
- package/dist/prompt/composer.d.ts +27 -0
- package/dist/prompt/composer.js +67 -21
- package/dist/prompt/instruction-scanner.js +5 -3
- package/dist/prompt/section-loader.js +1 -0
- package/dist/prompt/sections/base.md +1 -0
- package/dist/prompt/sections/browser.md +10 -0
- package/dist/prompt/sections/coding.md +4 -0
- package/dist/protocol/chat-session-manager.d.ts +8 -2
- package/dist/protocol/chat-session-manager.js +36 -0
- package/dist/protocol/chat-session.d.ts +65 -0
- package/dist/protocol/chat-session.js +94 -2
- package/dist/protocol/client.d.ts +27 -0
- package/dist/protocol/client.js +38 -0
- package/dist/protocol/server.d.ts +112 -0
- package/dist/protocol/server.js +586 -16
- package/dist/protocol/transport.js +3 -2
- package/dist/protocol/types.d.ts +46 -0
- package/dist/protocol/types.js +16 -0
- package/dist/quota/credentials.d.ts +3 -0
- package/dist/quota/credentials.js +80 -0
- package/dist/quota/index.d.ts +36 -0
- package/dist/quota/index.js +155 -0
- package/dist/quota/types.d.ts +48 -0
- package/dist/quota/types.js +13 -0
- package/dist/review/review-prompt.d.ts +28 -0
- package/dist/review/review-prompt.js +81 -0
- package/dist/run/FileRunStore.js +8 -3
- package/dist/run/RunApprovalBackend.js +25 -5
- package/dist/run/RunManager.d.ts +12 -0
- package/dist/run/RunManager.js +35 -0
- package/dist/run/factory.d.ts +1 -1
- package/dist/runtime/background-shell.d.ts +139 -0
- package/dist/runtime/background-shell.js +495 -0
- package/dist/runtime/output-clean.d.ts +24 -0
- package/dist/runtime/output-clean.js +41 -0
- package/dist/runtime/ring-file.d.ts +64 -0
- package/dist/runtime/ring-file.js +174 -0
- package/dist/runtime/safe-spawn.js +15 -34
- package/dist/runtime/spawn-common.d.ts +148 -0
- package/dist/runtime/spawn-common.js +303 -0
- package/dist/runtime/truncate-output.d.ts +22 -0
- package/dist/runtime/truncate-output.js +49 -0
- package/dist/runtime/utf8-cut.d.ts +11 -0
- package/dist/runtime/utf8-cut.js +23 -0
- package/dist/services/auto-dream.d.ts +4 -0
- package/dist/services/auto-dream.js +26 -26
- package/dist/services/diagnostics.d.ts +1 -2
- package/dist/services/diagnostics.js +12 -7
- package/dist/services/extract-memories.d.ts +14 -1
- package/dist/services/extract-memories.js +45 -6
- package/dist/services/memory-orchestrator.d.ts +21 -0
- package/dist/services/memory-orchestrator.js +125 -50
- package/dist/services/session-memory.js +24 -12
- package/dist/session/file-history.d.ts +124 -1
- package/dist/session/file-history.js +222 -6
- package/dist/session/memory.d.ts +116 -2
- package/dist/session/memory.js +250 -28
- package/dist/session/session-manager.d.ts +40 -0
- package/dist/session/session-manager.js +113 -3
- package/dist/session/simple-diff.d.ts +23 -0
- package/dist/session/simple-diff.js +84 -0
- package/dist/session/transcript.d.ts +25 -1
- package/dist/session/transcript.js +35 -2
- package/dist/session/undo-target.d.ts +67 -0
- package/dist/session/undo-target.js +144 -0
- package/dist/settings/disk-defaults.d.ts +9 -2
- package/dist/settings/disk-defaults.js +11 -2
- package/dist/settings/feature-flags.d.ts +64 -0
- package/dist/settings/feature-flags.js +61 -0
- package/dist/settings/manager.d.ts +68 -1
- package/dist/settings/manager.js +266 -29
- package/dist/settings/migrate-config.d.ts +45 -0
- package/dist/settings/migrate-config.js +125 -0
- package/dist/settings/schema-export.d.ts +25 -0
- package/dist/settings/schema-export.js +38 -0
- package/dist/settings/schema.d.ts +1186 -771
- package/dist/settings/schema.js +268 -97
- package/dist/skills/scanner.d.ts +9 -0
- package/dist/skills/scanner.js +30 -2
- package/dist/stt/resolve-transcribe.d.ts +31 -0
- package/dist/stt/resolve-transcribe.js +108 -0
- package/dist/stt/transcribe.d.ts +51 -0
- package/dist/stt/transcribe.js +65 -0
- package/dist/tool-system/browser-bridge.d.ts +226 -0
- package/dist/tool-system/browser-bridge.js +163 -0
- package/dist/tool-system/builtin/agent-heartbeat.d.ts +49 -0
- package/dist/tool-system/builtin/agent-heartbeat.js +89 -0
- package/dist/tool-system/builtin/agent-notifications.d.ts +12 -3
- package/dist/tool-system/builtin/agent-notifications.js +9 -3
- package/dist/tool-system/builtin/agent-output-file.d.ts +38 -0
- package/dist/tool-system/builtin/agent-output-file.js +72 -0
- package/dist/tool-system/builtin/agent-registry.d.ts +12 -0
- package/dist/tool-system/builtin/agent-registry.js +8 -0
- package/dist/tool-system/builtin/agent.d.ts +21 -1
- package/dist/tool-system/builtin/agent.js +489 -42
- package/dist/tool-system/builtin/apply-patch/applier.js +66 -8
- package/dist/tool-system/builtin/apply-patch/backup-targets.d.ts +10 -0
- package/dist/tool-system/builtin/apply-patch/backup-targets.js +30 -0
- package/dist/tool-system/builtin/apply-patch/index.js +0 -15
- package/dist/tool-system/builtin/background-jobs.d.ts +76 -0
- package/dist/tool-system/builtin/background-jobs.js +124 -0
- package/dist/tool-system/builtin/background-shell-tools.d.ts +20 -0
- package/dist/tool-system/builtin/background-shell-tools.js +108 -0
- package/dist/tool-system/builtin/background-work.d.ts +67 -0
- package/dist/tool-system/builtin/background-work.js +86 -0
- package/dist/tool-system/builtin/bash-output-style.d.ts +32 -0
- package/dist/tool-system/builtin/bash-output-style.js +40 -0
- package/dist/tool-system/builtin/bash.d.ts +5 -2
- package/dist/tool-system/builtin/bash.js +98 -62
- package/dist/tool-system/builtin/browser-tools.d.ts +33 -0
- package/dist/tool-system/builtin/browser-tools.js +312 -0
- package/dist/tool-system/builtin/cancel-goal.d.ts +31 -0
- package/dist/tool-system/builtin/cancel-goal.js +64 -0
- package/dist/tool-system/builtin/check-quota.d.ts +15 -0
- package/dist/tool-system/builtin/check-quota.js +34 -0
- package/dist/tool-system/builtin/config.js +7 -0
- package/dist/tool-system/builtin/cron.d.ts +7 -0
- package/dist/tool-system/builtin/cron.js +55 -3
- package/dist/tool-system/builtin/drive-claude-code.d.ts +30 -0
- package/dist/tool-system/builtin/drive-claude-code.js +157 -0
- package/dist/tool-system/builtin/edit-model-catalog.d.ts +3 -0
- package/dist/tool-system/builtin/edit-model-catalog.js +94 -0
- package/dist/tool-system/builtin/edit.js +20 -16
- package/dist/tool-system/builtin/eol.d.ts +29 -0
- package/dist/tool-system/builtin/eol.js +37 -0
- package/dist/tool-system/builtin/file-cache.d.ts +6 -0
- package/dist/tool-system/builtin/file-cache.js +8 -0
- package/dist/tool-system/builtin/generate-image.d.ts +35 -0
- package/dist/tool-system/builtin/generate-image.js +278 -50
- package/dist/tool-system/builtin/generate-video.d.ts +55 -0
- package/dist/tool-system/builtin/generate-video.js +361 -0
- package/dist/tool-system/builtin/glob.js +0 -7
- package/dist/tool-system/builtin/grep.js +6 -8
- package/dist/tool-system/builtin/image-providers.d.ts +86 -0
- package/dist/tool-system/builtin/image-providers.js +190 -0
- package/dist/tool-system/builtin/image-uploader.d.ts +33 -0
- package/dist/tool-system/builtin/image-uploader.js +74 -0
- package/dist/tool-system/builtin/index.d.ts +7 -1
- package/dist/tool-system/builtin/index.js +249 -25
- package/dist/tool-system/builtin/mcp-tools.js +23 -3
- package/dist/tool-system/builtin/memory.js +45 -7
- package/dist/tool-system/builtin/notebook-edit.js +0 -7
- package/dist/tool-system/builtin/powershell.js +4 -1
- package/dist/tool-system/builtin/read.js +10 -10
- package/dist/tool-system/builtin/repl.js +4 -1
- package/dist/tool-system/builtin/skill.js +9 -0
- package/dist/tool-system/builtin/sleep.js +3 -2
- package/dist/tool-system/builtin/tool-search.js +25 -7
- package/dist/tool-system/builtin/video-providers.d.ts +154 -0
- package/dist/tool-system/builtin/video-providers.js +235 -0
- package/dist/tool-system/builtin/web-fetch.js +12 -2
- package/dist/tool-system/builtin/web-search.js +21 -7
- package/dist/tool-system/builtin/worktree.d.ts +2 -1
- package/dist/tool-system/builtin/worktree.js +31 -4
- package/dist/tool-system/builtin/write.js +0 -6
- package/dist/tool-system/context.d.ts +154 -6
- package/dist/tool-system/executor.d.ts +3 -1
- package/dist/tool-system/executor.js +172 -88
- package/dist/tool-system/investigation-guard.js +1 -1
- package/dist/tool-system/mcp-manager.d.ts +42 -4
- package/dist/tool-system/mcp-manager.js +186 -20
- package/dist/tool-system/path-policy.d.ts +5 -0
- package/dist/tool-system/path-policy.js +307 -8
- package/dist/tool-system/permission.d.ts +36 -1
- package/dist/tool-system/permission.js +281 -69
- package/dist/tool-system/plan-mode-allowlist.d.ts +13 -2
- package/dist/tool-system/plan-mode-allowlist.js +24 -2
- package/dist/tool-system/registry.d.ts +1 -0
- package/dist/tool-system/registry.js +16 -2
- package/dist/tool-system/sandbox/index.d.ts +8 -0
- package/dist/tool-system/sandbox/index.js +7 -2
- package/dist/tool-system/validate-tool-metadata.d.ts +36 -0
- package/dist/tool-system/validate-tool-metadata.js +63 -0
- package/dist/types.d.ts +217 -6
- package/dist/updater.js +20 -9
- package/dist/utils/envUtils.d.ts +0 -9
- package/dist/utils/envUtils.js +3 -28
- package/dist/utils/exec.d.ts +40 -0
- package/dist/utils/exec.js +144 -0
- package/dist/utils/json.d.ts +12 -0
- package/dist/utils/json.js +92 -0
- package/dist/utils/theme.d.ts +1 -1
- package/dist/utils/theme.js +1 -1
- package/dist/utils/toolDisplay.js +0 -1
- package/package.json +13 -7
- package/dist/agent/coordinator.d.ts +0 -49
- package/dist/agent/coordinator.js +0 -77
- package/dist/settings/manager.test.js +0 -73
- package/dist/tool-system/builtin/remote-trigger.d.ts +0 -6
- package/dist/tool-system/builtin/remote-trigger.js +0 -54
- package/dist/tool-system/builtin/send-message.d.ts +0 -6
- package/dist/tool-system/builtin/send-message.js +0 -47
- /package/dist/{settings/manager.test.d.ts → credentials/types.js} +0 -0
package/dist/engine/engine.js
CHANGED
|
@@ -7,21 +7,29 @@ import { ToolExecutor } from "../tool-system/executor.js";
|
|
|
7
7
|
import { InvestigationGuard } from "../tool-system/investigation-guard.js";
|
|
8
8
|
import { TaskGuard } from "../tool-system/task-guard.js";
|
|
9
9
|
import { readLastTodoSnapshot } from "../tool-system/builtin/task.js";
|
|
10
|
-
import {
|
|
10
|
+
import { applyDynamicToolDef } from "./dynamic-tool-defs.js";
|
|
11
|
+
import { getMergedCatalog } from "../model-catalog/index.js";
|
|
12
|
+
import { modelEntriesFromConnections } from "./model-connections-pool.js";
|
|
13
|
+
import { resolveAuxKey } from "./aux-key.js";
|
|
14
|
+
import { foldRunUsage } from "./session-usage.js";
|
|
15
|
+
import { enqueueSteerItem, consumeSteerItems, removeSteerItem, } from "./steer-queue.js";
|
|
16
|
+
import { resolveSandboxConfig } from "./sandbox-config.js";
|
|
17
|
+
import { sandboxCacheKey } from "./sandbox-cache-key.js";
|
|
11
18
|
import { BUILTIN_TOOL_GUARDS } from "../tool-system/builtin/index.js";
|
|
12
19
|
import { asyncAgentRegistry } from "../tool-system/builtin/agent-registry.js";
|
|
20
|
+
import { backgroundShellManager } from "../runtime/background-shell.js";
|
|
13
21
|
import { notificationQueue, buildNotificationMessage, } from "../tool-system/builtin/agent-notifications.js";
|
|
14
22
|
import { PermissionClassifier, HeadlessApprovalBackend, AutoApprovalBackend, InteractiveApprovalBackend, getInteractiveApprovalBackend, } from "../tool-system/permission.js";
|
|
15
23
|
import { HookRegistry } from "../hooks/registry.js";
|
|
16
24
|
import { wrapHookMessages } from "../hooks/inject.js";
|
|
17
25
|
import { createGoalStopHook } from "../hooks/goal-stop-hook.js";
|
|
18
|
-
import { normalizeGoal } from "./goal.js";
|
|
26
|
+
import { normalizeGoal, resolveGoalSetAt, resolveMaxTurns, resolveMaxStopBlocks, } from "./goal.js";
|
|
19
27
|
import { loadPluginHooks } from "../plugins/loadPluginHooks.js";
|
|
20
28
|
import { pluginAgentDirs } from "../plugins/installer/loadPluginAgents.js";
|
|
21
29
|
import { patchOrphanedToolUses } from "./patch-orphaned-tools.js";
|
|
22
30
|
import { runShellHook, shellHookMatches } from "../hooks/shell-runner.js";
|
|
23
31
|
import { ContextManager } from "../context/manager.js";
|
|
24
|
-
import { estimateTokens } from "../context/compaction.js";
|
|
32
|
+
import { estimateTokens, clampContextRatios as clampContextRatiosImpl, } from "../context/compaction.js";
|
|
25
33
|
import { PLAN_MODE_ALLOWED_TOOLS } from "../tool-system/plan-mode-allowlist.js";
|
|
26
34
|
import { PromptComposer } from "../prompt/composer.js";
|
|
27
35
|
import { SessionManager } from "../session/session-manager.js";
|
|
@@ -32,26 +40,28 @@ import { sanitizeContent, sanitizeTaskString } from "../logging/sanitize-message
|
|
|
32
40
|
import { TurnLoop } from "./turn-loop.js";
|
|
33
41
|
import { MCPManager } from "../tool-system/mcp-manager.js";
|
|
34
42
|
import { SettingsManager, userHome } from "../settings/manager.js";
|
|
35
|
-
import {
|
|
43
|
+
import { CredentialStore } from "../credentials/store.js";
|
|
44
|
+
import { isFeatureEnabled, resolveFeatureFlags, } from "../settings/feature-flags.js";
|
|
45
|
+
import { effectiveDisabledList, effectiveBuiltinLists, } from "../capability-control/overlay.js";
|
|
46
|
+
import { computeEffectiveDisabledLists } from "../capability-control/disabled-lists.js";
|
|
36
47
|
import { FileHistory } from "../session/file-history.js";
|
|
37
|
-
import {
|
|
48
|
+
import { patchBackupTargets } from "../tool-system/builtin/apply-patch/backup-targets.js";
|
|
49
|
+
import { resolveSandboxBackend, } from "../tool-system/sandbox/index.js";
|
|
38
50
|
import { resolveAgentPreset, resolveBuiltinToolNames, } from "../preset/index.js";
|
|
39
51
|
import { ModelPool } from "../llm/model-pool.js";
|
|
40
52
|
import { AgentDefinitionRegistry } from "../agent/agent-definition-registry.js";
|
|
41
|
-
import { ProviderCatalog } from "../llm/provider-catalog.js";
|
|
42
53
|
import { defaultCacheDir } from "../llm/model-cache.js";
|
|
43
54
|
import { detectProviderFromApiKey, buildModelPool, } from "../onboarding.js";
|
|
44
55
|
import { detectPastedNoise } from "../utils/task-sanitizer.js";
|
|
45
56
|
import { parseTaskWithImages, } from "./parse-task.js";
|
|
46
|
-
import { enforceImagePolicy, byteLengthFromBase64, dropOversizedImages, } from "./image-policy.js";
|
|
57
|
+
import { enforceImagePolicy, byteLengthFromBase64, dropOversizedImages, collectAttachedImagePaths, } from "./image-policy.js";
|
|
47
58
|
import { tryCompressImages } from "./image-compression.js";
|
|
48
59
|
import { buildSessionTitle } from "./session-title.js";
|
|
49
60
|
import { capabilitiesFor } from "../llm/capabilities/index.js";
|
|
50
61
|
import { MemoryOrchestrator } from "../services/memory-orchestrator.js";
|
|
51
62
|
import { runDreamConsolidation } from "../services/dream-consolidation.js";
|
|
52
|
-
import { join } from "node:path";
|
|
53
|
-
import {
|
|
54
|
-
import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from "node:fs";
|
|
63
|
+
import { join, isAbsolute } from "node:path";
|
|
64
|
+
import { chmodSync, existsSync, mkdirSync, readFileSync, renameSync, writeFileSync, } from "node:fs";
|
|
55
65
|
/**
|
|
56
66
|
* Build ScanOptions.compatFileNames from the user's instruction compat toggles.
|
|
57
67
|
* Primary file name stays hard-wired to CODESHELL.md (not exposed). Turning a
|
|
@@ -118,8 +128,25 @@ export function resolveChildLlm(modelKey, pool, parentLlm) {
|
|
|
118
128
|
* 2. user-level ~/.code-shell/agents/*.md (user wins on name)
|
|
119
129
|
* Names in `disabledAgents` are filtered out so the LLM never sees them.
|
|
120
130
|
*/
|
|
131
|
+
/**
|
|
132
|
+
* Resolve the working directory for a run. Precedence:
|
|
133
|
+
* options.cwd > resumed session's state.cwd > config.cwd > process.cwd()
|
|
134
|
+
*
|
|
135
|
+
* The session-cwd tier is what stops a project-bound session from being
|
|
136
|
+
* resumed against the wrong directory: when a host omits options.cwd (e.g. its
|
|
137
|
+
* sidebar repo selection drifted to null), the session's own recorded cwd is
|
|
138
|
+
* recovered so the engine still loads THAT project's agents/settings/memory,
|
|
139
|
+
* not whatever process.cwd() happens to be. Pure so the precedence is testable
|
|
140
|
+
* without standing up an Engine.
|
|
141
|
+
*/
|
|
142
|
+
export function resolveRunCwd(args) {
|
|
143
|
+
return args.optionCwd ?? args.sessionCwd ?? args.configCwd ?? args.processCwd;
|
|
144
|
+
}
|
|
121
145
|
export function loadAgentDefinitionsForCwd(cwd, disabledAgents = [], disabledPlugins = []) {
|
|
122
|
-
|
|
146
|
+
// userHome() (not raw homedir(), which bun caches at process start and never
|
|
147
|
+
// re-reads) so the user-agents dir honors a test's process.env.HOME override
|
|
148
|
+
// and stays consistent with the rest of the codebase's home resolution.
|
|
149
|
+
const home = userHome();
|
|
123
150
|
// Increasing priority; loadFromDirs is last-dir-wins. ORDER ENCODES POLICY:
|
|
124
151
|
// user (cross-project personal default, lowest) → plugins (reusable baseline)
|
|
125
152
|
// → project (highest). A repo's in-tree agent therefore overrides a same-named
|
|
@@ -135,7 +162,7 @@ export function loadAgentDefinitionsForCwd(cwd, disabledAgents = [], disabledPlu
|
|
|
135
162
|
...(cwd ? [{ dir: `${cwd}/.code-shell/agents`, source: "project" }] : []),
|
|
136
163
|
], disabledAgents);
|
|
137
164
|
}
|
|
138
|
-
const NESTED_AGENT_TOOLS = ["Agent", "AgentStatus", "AgentCancel"];
|
|
165
|
+
const NESTED_AGENT_TOOLS = ["Agent", "AgentStatus", "AgentCancel", "AgentSendInput"];
|
|
139
166
|
/**
|
|
140
167
|
* #7: apply a project's per-turn builtin capability override to a tool list.
|
|
141
168
|
* A builtin marked `off` for the current cwd is HIDDEN from the turn's tool
|
|
@@ -238,7 +265,39 @@ export class Engine {
|
|
|
238
265
|
* LLM response arrives.
|
|
239
266
|
*/
|
|
240
267
|
ctxOverheadBySid = new Map();
|
|
268
|
+
/**
|
|
269
|
+
* Step-gap steering queue (per sessionId, in-memory). Host pushes user
|
|
270
|
+
* messages here via enqueueSteer while a run is in flight; the turn loop
|
|
271
|
+
* drains it at each step boundary and splices them into the next LLM request
|
|
272
|
+
* WITHOUT aborting (the 不打断 path, vs cancel+resend). Pure memory, forgotten
|
|
273
|
+
* on process exit — same model as the credential session-allow set, so
|
|
274
|
+
* multiple Engines don't interfere and it stays cleanly extractable.
|
|
275
|
+
*/
|
|
276
|
+
steerQueueBySid = new Map();
|
|
241
277
|
activePermission;
|
|
278
|
+
/**
|
|
279
|
+
* The TurnLoop of the in-flight run(), exposed so extendGoalRun() can bump a
|
|
280
|
+
* running goal's turn/budget ceilings mid-run (TODO 3.1). Null when idle.
|
|
281
|
+
*/
|
|
282
|
+
activeTurnLoop = null;
|
|
283
|
+
/**
|
|
284
|
+
* The goal-stop hook of the in-flight goal run, exposed so clearGoal() can
|
|
285
|
+
* unregister it mid-run (the closure holds the now-cleared goal and would
|
|
286
|
+
* otherwise keep re-blocking the stop). Null when no goal run is active.
|
|
287
|
+
*/
|
|
288
|
+
activeGoalHook = null;
|
|
289
|
+
/**
|
|
290
|
+
* The in-flight run's session bundle, held so clearGoal() can wipe the goal
|
|
291
|
+
* on the SAME instance the run loop is persisting each turn — not a fresh
|
|
292
|
+
* detached copy from resume(). Without this, a mid-run 清除 clears disk, but
|
|
293
|
+
* the still-running loop's next saveState(bundle.state) resurrects the goal
|
|
294
|
+
* (bundle.state.activeGoal was never dropped). A never-completing goal run
|
|
295
|
+
* (judge keeps returning not_met → continueSession) stays live for a long
|
|
296
|
+
* time, so this write-back race is the norm, not an edge case, for such runs.
|
|
297
|
+
* Single-valued like activeTurnLoop — one top-level run per engine at a time.
|
|
298
|
+
* Null when idle; set at run start, cleared in run's finally.
|
|
299
|
+
*/
|
|
300
|
+
activeRunSession = null;
|
|
242
301
|
/** Public accessor so UI/clients can read the resolved per-model window. */
|
|
243
302
|
get maxContextTokens() {
|
|
244
303
|
return this.resolveMaxContextTokens();
|
|
@@ -247,6 +306,27 @@ export class Engine {
|
|
|
247
306
|
const modelEntry = this.modelPool.get();
|
|
248
307
|
return modelEntry?.maxContextTokens ?? this.config.maxContextTokens ?? 200_000;
|
|
249
308
|
}
|
|
309
|
+
/**
|
|
310
|
+
* Compaction thresholds from settings.context, clamped so they keep the
|
|
311
|
+
* required ordering floor < compact < summarize even if the user configures
|
|
312
|
+
* conflicting values (e.g. summarize below compact). Falls back to the
|
|
313
|
+
* ContextManager defaults when a field is absent.
|
|
314
|
+
*/
|
|
315
|
+
resolveContextRatios() {
|
|
316
|
+
let ctx;
|
|
317
|
+
try {
|
|
318
|
+
// Read from SettingsManager (shared across all hosts) rather than
|
|
319
|
+
// EngineConfig, mirroring readMemoriesConfig — avoids per-host wiring
|
|
320
|
+
// drift (see memory: personalization host wiring).
|
|
321
|
+
ctx = this.getSettingsManager().get().context;
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
return {};
|
|
325
|
+
}
|
|
326
|
+
if (!ctx)
|
|
327
|
+
return {};
|
|
328
|
+
return clampContextRatiosImpl(ctx);
|
|
329
|
+
}
|
|
250
330
|
/**
|
|
251
331
|
* Emit a lifecycle hook with isSubAgent auto-merged into data so handlers
|
|
252
332
|
* can skip noisy injections for spawned children. All Engine-side hook
|
|
@@ -278,6 +358,10 @@ export class Engine {
|
|
|
278
358
|
}
|
|
279
359
|
const entries = settings.hooks ?? [];
|
|
280
360
|
for (const entry of entries) {
|
|
361
|
+
// Soft off-switch (settings hooks UI): the entry stays in the file but
|
|
362
|
+
// doesn't register. reloadHooks() re-runs this, so toggling is hot.
|
|
363
|
+
if (entry.disabled === true)
|
|
364
|
+
continue;
|
|
281
365
|
const event = entry.event;
|
|
282
366
|
const handler = async (ctx) => {
|
|
283
367
|
if (!shellHookMatches(entry, ctx))
|
|
@@ -310,6 +394,19 @@ export class Engine {
|
|
|
310
394
|
this.hooks.unregister(event, handler);
|
|
311
395
|
}
|
|
312
396
|
this.settingsHookHandles = [];
|
|
397
|
+
// Also drop & re-load plugin hooks. Plugin hooks are registered under
|
|
398
|
+
// `plugin:<name>:<event>` names; without this, disabling a plugin
|
|
399
|
+
// mid-session left its hooks firing until the next new session (asymmetric
|
|
400
|
+
// with settings-hook hot-reload). Re-reading readDisabledLists means a
|
|
401
|
+
// now-disabled plugin's hooks are simply not re-registered.
|
|
402
|
+
this.hooks.removeByNamePrefix("plugin:");
|
|
403
|
+
try {
|
|
404
|
+
const { disabledPlugins, disabledPluginHooks } = this.readDisabledLists();
|
|
405
|
+
loadPluginHooks(this.hooks, disabledPlugins, disabledPluginHooks);
|
|
406
|
+
}
|
|
407
|
+
catch {
|
|
408
|
+
// best-effort — a plugin-load failure must not break settings reload below
|
|
409
|
+
}
|
|
313
410
|
// Force the next get() to re-read disk so reloaded hooks reflect the
|
|
314
411
|
// newest settings.json, not a stale merged cache.
|
|
315
412
|
try {
|
|
@@ -362,7 +459,12 @@ export class Engine {
|
|
|
362
459
|
// disabledPlugins suppresses a plugin's hooks too (not just its
|
|
363
460
|
// Skill-tool entries) — see loadPluginHooks. readDisabledLists reads
|
|
364
461
|
// the same settings the prompt composer / tool context use.
|
|
365
|
-
|
|
462
|
+
// disabledPluginHooks is the per-hook overlay
|
|
463
|
+
// (capabilityOverrides.pluginHooks); applied at construction, so a
|
|
464
|
+
// toggle takes effect for NEW sessions (same semantics as
|
|
465
|
+
// disabledPlugins itself).
|
|
466
|
+
const { disabledPlugins, disabledPluginHooks } = this.readDisabledLists();
|
|
467
|
+
loadPluginHooks(this.hooks, disabledPlugins, disabledPluginHooks);
|
|
366
468
|
}
|
|
367
469
|
// settings.hooks → shell-command wrappers. Chain order:
|
|
368
470
|
// plugin (80) → shell (50) → code (default 0).
|
|
@@ -378,7 +480,7 @@ export class Engine {
|
|
|
378
480
|
}
|
|
379
481
|
}
|
|
380
482
|
/**
|
|
381
|
-
* Load
|
|
483
|
+
* Load modelConnections[] from settings into the active ModelPool and
|
|
382
484
|
* resync this.config.llm with the matching entry. Called from the ctor and
|
|
383
485
|
* from reloadModelPool() (e.g. after onboarding writes new entries to disk).
|
|
384
486
|
*/
|
|
@@ -387,54 +489,45 @@ export class Engine {
|
|
|
387
489
|
const sm = this.getSettingsManager();
|
|
388
490
|
sm.invalidate();
|
|
389
491
|
const settings = sm.get();
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
providerKey: m.providerKey,
|
|
402
|
-
});
|
|
403
|
-
}
|
|
404
|
-
// Build catalog from settings.providers[] and attach to the pool
|
|
405
|
-
// so model entries can resolve baseUrl/apiKey from their provider.
|
|
406
|
-
if (settings.providers?.length) {
|
|
407
|
-
this.modelPool.setProviderCatalog(new ProviderCatalog(settings.providers));
|
|
492
|
+
// Unified model catalog (统一模型接入方案 §6): register text
|
|
493
|
+
// connections from settings.modelConnections[] into the pool — the
|
|
494
|
+
// catalog-driven instance store is the sole source of model selection.
|
|
495
|
+
// A connection's instance id becomes its pool key. See
|
|
496
|
+
// docs/superpowers/specs/2026-06-15-unified-model-catalog-design.md.
|
|
497
|
+
const connections = settings.modelConnections;
|
|
498
|
+
if (Array.isArray(connections) && connections.length) {
|
|
499
|
+
const catalog = getMergedCatalog();
|
|
500
|
+
const credentials = settings.credentials;
|
|
501
|
+
for (const entry of modelEntriesFromConnections(connections, (Array.isArray(credentials) ? credentials : []), catalog)) {
|
|
502
|
+
this.modelPool.register(entry);
|
|
408
503
|
}
|
|
504
|
+
}
|
|
505
|
+
const hasConnections = Array.isArray(connections) && connections.length > 0;
|
|
506
|
+
if (hasConnections) {
|
|
409
507
|
this.modelPool.setCacheDir(defaultCacheDir());
|
|
410
508
|
this.modelPool.reloadCachedContextWindows();
|
|
411
|
-
// Resolve active entry.
|
|
412
|
-
//
|
|
413
|
-
//
|
|
414
|
-
//
|
|
415
|
-
//
|
|
416
|
-
//
|
|
417
|
-
//
|
|
418
|
-
//
|
|
419
|
-
//
|
|
420
|
-
// *user's* current UI model selection and must not clobber a child's
|
|
421
|
-
// routed model — without this guard a role's `model: flash` is silently
|
|
422
|
-
// overridden back to whatever the user has active in the foreground.
|
|
509
|
+
// Resolve the active entry from settings.defaults.text, then switch the
|
|
510
|
+
// pool and write the resolved entry's credentials into config.llm, so the
|
|
511
|
+
// first run() uses the right endpoint instead of whatever env-derived
|
|
512
|
+
// fallback repl.ts seeded earlier.
|
|
513
|
+
// Sub-agents skip this resync: their llm is chosen by the parent's
|
|
514
|
+
// resolveChildLlm (per-role model routing). defaults.text is the *user's*
|
|
515
|
+
// current UI model selection and must not clobber a child's routed model —
|
|
516
|
+
// without this guard a role's `model: flash` is silently overridden back
|
|
517
|
+
// to whatever the user has active in the foreground.
|
|
423
518
|
if (this.config.isSubAgent !== true) {
|
|
424
|
-
const
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
519
|
+
const defaultText = settings.defaults?.text;
|
|
520
|
+
// 统一 catalog only:defaults.text 命中则用;否则回退首个已注册连接,
|
|
521
|
+
// 避免选未配置模型时静默沿用空种子(旧 bug:抛误导性 OPENAI_API_KEY missing)。
|
|
522
|
+
let matchKey;
|
|
523
|
+
if (defaultText && this.modelPool.list().some((e) => e.key === defaultText)) {
|
|
524
|
+
matchKey = defaultText;
|
|
428
525
|
}
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
// OpenRouter stores entries as "provider/model-name"; the top-level
|
|
432
|
-
// settings.model.name is just "model-name". Match either form.
|
|
433
|
-
match = settings.models.find((m) => m.model === currentModel ||
|
|
434
|
-
(currentModel && m.model?.endsWith(`/${currentModel}`)));
|
|
526
|
+
else {
|
|
527
|
+
matchKey = this.modelPool.list()[0]?.key;
|
|
435
528
|
}
|
|
436
|
-
if (
|
|
437
|
-
const entry = this.modelPool.switch(
|
|
529
|
+
if (matchKey) {
|
|
530
|
+
const entry = this.modelPool.switch(matchKey);
|
|
438
531
|
this.config = {
|
|
439
532
|
...this.config,
|
|
440
533
|
llm: this.modelPool.toLLMConfig(entry),
|
|
@@ -443,9 +536,10 @@ export class Engine {
|
|
|
443
536
|
}
|
|
444
537
|
}
|
|
445
538
|
else if (this.config.llm.apiKey) {
|
|
446
|
-
// Auto-populate pool from the configured API key when
|
|
447
|
-
// This lets users who only
|
|
448
|
-
// use /model to switch between the provider's
|
|
539
|
+
// Auto-populate pool from the configured API key when no
|
|
540
|
+
// modelConnections[] are configured. This lets users who only have an
|
|
541
|
+
// env/seed API key still use /model to switch between the provider's
|
|
542
|
+
// available models.
|
|
449
543
|
this.autoPopulatePool(this.config.llm.apiKey, this.config.llm.baseUrl);
|
|
450
544
|
}
|
|
451
545
|
// Carry image-attachment settings + sampling temperature into
|
|
@@ -457,8 +551,10 @@ export class Engine {
|
|
|
457
551
|
const modelBlock = settings.model;
|
|
458
552
|
const nextDefaults = { ...(this.config.clientDefaults ?? {}) };
|
|
459
553
|
let defaultsChanged = false;
|
|
460
|
-
|
|
461
|
-
|
|
554
|
+
// Migrate legacy "original" → "high" (raw settings may bypass schema).
|
|
555
|
+
const detail = imageSettings?.detail === "original" ? "high" : imageSettings?.detail;
|
|
556
|
+
if (detail && nextDefaults.imageDetail !== detail) {
|
|
557
|
+
nextDefaults.imageDetail = detail;
|
|
462
558
|
defaultsChanged = true;
|
|
463
559
|
}
|
|
464
560
|
if (typeof modelBlock?.temperature === "number" &&
|
|
@@ -476,7 +572,7 @@ export class Engine {
|
|
|
476
572
|
}
|
|
477
573
|
/**
|
|
478
574
|
* Re-read settings and refresh the model pool. Used after onboarding /login
|
|
479
|
-
* writes new
|
|
575
|
+
* writes new modelConnections[] to disk so the running engine picks them
|
|
480
576
|
* up without a process restart. Existing pool entries are kept (re-registering
|
|
481
577
|
* the same key overwrites them), so callers don't need to clear first.
|
|
482
578
|
*/
|
|
@@ -490,7 +586,7 @@ export class Engine {
|
|
|
490
586
|
}
|
|
491
587
|
}
|
|
492
588
|
/**
|
|
493
|
-
* Auto-populate the model pool when
|
|
589
|
+
* Auto-populate the model pool when no modelConnections[] are configured but
|
|
494
590
|
* the user has configured an API key. Detects the provider from the
|
|
495
591
|
* key prefix / baseUrl and registers all its known models.
|
|
496
592
|
*/
|
|
@@ -528,6 +624,65 @@ export class Engine {
|
|
|
528
624
|
setAskUser(fn) {
|
|
529
625
|
this.config.askUser = fn;
|
|
530
626
|
}
|
|
627
|
+
/**
|
|
628
|
+
* Inject the browser automation bridge after construction (same chicken-and-egg
|
|
629
|
+
* as setAskUser: the desktop host builds the bridge — which drives a webview —
|
|
630
|
+
* after the Engine exists). Undefined → the browser_* tools degrade with a
|
|
631
|
+
* clear "no browser panel" error.
|
|
632
|
+
*/
|
|
633
|
+
setBrowserBridge(bridge) {
|
|
634
|
+
this.config.browserBridge = bridge;
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* Queue a user message to be spliced into the in-flight run for `sessionId`
|
|
638
|
+
* at the next turn-loop step boundary — the 不打断 steering path (vs cancel +
|
|
639
|
+
* resend). General-purpose: any host path (UI 引导, future agent coordination,
|
|
640
|
+
* external triggers) can call it. If no run is active for the session the
|
|
641
|
+
* message simply waits in the queue and is consumed when that session next
|
|
642
|
+
* runs (rare race; host normally only steers while busy). No-op on blank text.
|
|
643
|
+
*
|
|
644
|
+
* `id` is the host's stable queue-entry id. It rides through to the
|
|
645
|
+
* `steer_injected` event (so the host can match the injected bubble back to
|
|
646
|
+
* the queued draft) and is the handle `unsteer` uses to revoke a still-pending
|
|
647
|
+
* entry. A blank id is tolerated but means the entry can't be revoked.
|
|
648
|
+
*/
|
|
649
|
+
enqueueSteer(sessionId, text, id = "") {
|
|
650
|
+
if (!sessionId)
|
|
651
|
+
return;
|
|
652
|
+
const q = this.steerQueueBySid.get(sessionId) ?? [];
|
|
653
|
+
const next = enqueueSteerItem(q, id || `steer-${q.length}`, text);
|
|
654
|
+
if (next === q)
|
|
655
|
+
return; // blank text dropped
|
|
656
|
+
this.steerQueueBySid.set(sessionId, next);
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* Revoke a still-pending steer entry (the 撤回 path). Returns true if it was
|
|
660
|
+
* removed, false if it was already consumed by the turn loop (can't take it
|
|
661
|
+
* back — it has been spliced into the run).
|
|
662
|
+
*/
|
|
663
|
+
unsteer(sessionId, id) {
|
|
664
|
+
const q = this.steerQueueBySid.get(sessionId);
|
|
665
|
+
if (!q || q.length === 0)
|
|
666
|
+
return false;
|
|
667
|
+
const { list, removed } = removeSteerItem(q, id);
|
|
668
|
+
if (removed)
|
|
669
|
+
this.steerQueueBySid.set(sessionId, list);
|
|
670
|
+
return removed;
|
|
671
|
+
}
|
|
672
|
+
/** Drain + clear the steer queue for a session (turn loop consumes per step). */
|
|
673
|
+
consumeSteer(sessionId) {
|
|
674
|
+
const q = this.steerQueueBySid.get(sessionId);
|
|
675
|
+
if (!q || q.length === 0)
|
|
676
|
+
return [];
|
|
677
|
+
const { drained, rest } = consumeSteerItems(q);
|
|
678
|
+
this.steerQueueBySid.set(sessionId, rest);
|
|
679
|
+
return drained;
|
|
680
|
+
}
|
|
681
|
+
/** Wire the cookie→browser injection callback (InjectCredential tool). Same
|
|
682
|
+
* post-construction injection model as setBrowserBridge. */
|
|
683
|
+
setInjectCredential(fn) {
|
|
684
|
+
this.config.injectCredentialToBrowser = fn;
|
|
685
|
+
}
|
|
531
686
|
/**
|
|
532
687
|
* Whether this engine runs unattended (no interactive human). Used by the
|
|
533
688
|
* in-process AgentServer to decide whether to wire an interactive askUser.
|
|
@@ -535,11 +690,34 @@ export class Engine {
|
|
|
535
690
|
isHeadless() {
|
|
536
691
|
return this.config.headless === true;
|
|
537
692
|
}
|
|
693
|
+
/**
|
|
694
|
+
* Probe whether a session already exists on disk (its state/transcript dir is
|
|
695
|
+
* present). Used by the protocol server to distinguish "resume an existing
|
|
696
|
+
* session" from "silently create a fresh empty one" — e.g. a cron resume job
|
|
697
|
+
* whose target session the user deleted must fail loudly, not run its prompt
|
|
698
|
+
* against a blank session. A stat probe, not a load.
|
|
699
|
+
*/
|
|
700
|
+
sessionExistsOnDisk(sessionId) {
|
|
701
|
+
return this.sessionManager.exists(sessionId);
|
|
702
|
+
}
|
|
538
703
|
/**
|
|
539
704
|
* Run a task from start to finish.
|
|
540
705
|
*/
|
|
541
706
|
async run(task, options) {
|
|
542
|
-
|
|
707
|
+
// When the caller omits cwd but is resuming an existing session, recover
|
|
708
|
+
// that session's bound cwd from disk so a project-bound session keeps
|
|
709
|
+
// loading its own agents/settings/memory even if the host's UI repo
|
|
710
|
+
// selection has drifted to null. Only probe on omission — an explicit cwd
|
|
711
|
+
// always wins, and a fresh session has nothing to recover.
|
|
712
|
+
const sessionCwd = options?.cwd === undefined && options?.sessionId
|
|
713
|
+
? this.sessionManager.readCwd(options.sessionId)
|
|
714
|
+
: undefined;
|
|
715
|
+
const cwd = resolveRunCwd({
|
|
716
|
+
optionCwd: options?.cwd,
|
|
717
|
+
sessionCwd,
|
|
718
|
+
configCwd: this.config.cwd,
|
|
719
|
+
processCwd: process.cwd(),
|
|
720
|
+
});
|
|
543
721
|
// Wrap the caller's onStream so we can intercept `task_update`
|
|
544
722
|
// events emitted by TodoWrite and keep an in-engine snapshot.
|
|
545
723
|
// TaskGuard reads this snapshot at turn end to decide whether to
|
|
@@ -701,6 +879,22 @@ export class Engine {
|
|
|
701
879
|
permissionMode: this.config.permissionMode ?? "acceptEdits",
|
|
702
880
|
}),
|
|
703
881
|
spawn: async (req) => {
|
|
882
|
+
// Anchor this sub-agent in the PARENT transcript at spawn time — before
|
|
883
|
+
// it runs, so it's recorded whether it later completes, is interrupted,
|
|
884
|
+
// or still runs. Replay reads these anchors to rebuild sub-agent cards
|
|
885
|
+
// from sessions/<agentId>/ (agentId === childSid); without it a
|
|
886
|
+
// backgrounded sub-agent leaves no parent-transcript trace and vanishes
|
|
887
|
+
// on reopen. Only on a fresh spawn (not a resume/continuation, which
|
|
888
|
+
// already has its anchor). Guarded so a transcript hiccup never breaks
|
|
889
|
+
// the spawn.
|
|
890
|
+
if (!req.resumeSessionId) {
|
|
891
|
+
try {
|
|
892
|
+
session.transcript.appendSubagent(req.agentId, undefined, req.description);
|
|
893
|
+
}
|
|
894
|
+
catch {
|
|
895
|
+
/* anchor is best-effort; never block the spawn */
|
|
896
|
+
}
|
|
897
|
+
}
|
|
704
898
|
// No nested agents. Strip Agent / AgentStatus / AgentCancel from the
|
|
705
899
|
// child's tool pool so the LLM can't spawn grandchildren — matches
|
|
706
900
|
// Claude Code's ALL_AGENT_DISALLOWED_TOOLS approach. Without this
|
|
@@ -733,6 +927,7 @@ export class Engine {
|
|
|
733
927
|
sessionStorageDir: this.config.sessionStorageDir,
|
|
734
928
|
headless: this.config.headless,
|
|
735
929
|
readOnlySession: req.readOnlySession,
|
|
930
|
+
skillAllowlist: req.skillAllowlist,
|
|
736
931
|
sandbox: this.config.sandbox,
|
|
737
932
|
// Subagents inherit the parent's scope: a child runs in the same
|
|
738
933
|
// cwd/session, so it should see the same config layers the parent did.
|
|
@@ -770,12 +965,41 @@ export class Engine {
|
|
|
770
965
|
// child.run() establishes its own runWithSid scope internally, so
|
|
771
966
|
// child log lines route to the child's sid and parent's ALS
|
|
772
967
|
// binding is unaffected when control returns here.
|
|
773
|
-
|
|
774
|
-
|
|
968
|
+
//
|
|
969
|
+
// agent_id === childSid: cold-start the child UNDER its agentId as the
|
|
970
|
+
// session id (run() shape (2): a fresh sid the host wants materialized),
|
|
971
|
+
// so the session persists at sessions/<agentId>/ and AgentSendInput can
|
|
972
|
+
// later resume it by agentId with no extra id→sid mapping. When
|
|
973
|
+
// resumeSessionId is set we resume that existing session instead —
|
|
974
|
+
// run() detects the on-disk session and replays its full transcript
|
|
975
|
+
// (the CC continuation model; see AgentSendInput).
|
|
976
|
+
const childSessionId = req.resumeSessionId ?? req.agentId;
|
|
977
|
+
const result = await child.run(req.prompt, {
|
|
978
|
+
signal: req.signal,
|
|
979
|
+
onStream: childStream,
|
|
980
|
+
sessionId: childSessionId,
|
|
981
|
+
});
|
|
982
|
+
return { text: result.text, sessionId: result.sessionId };
|
|
775
983
|
},
|
|
984
|
+
sessionExists: (sessionId) => this.sessionManager.exists(sessionId),
|
|
776
985
|
};
|
|
777
|
-
|
|
778
|
-
|
|
986
|
+
// Priority: config.sandbox → project settings.sandbox → global → per-run
|
|
987
|
+
// default. Read UNMERGED per-scope (getForScope) so a project that wrote no
|
|
988
|
+
// sandbox genuinely follows global, rather than inheriting global's mode and
|
|
989
|
+
// looking like it set one. Fixes "项目级配了不生效" + the scope model.
|
|
990
|
+
let projectSandbox;
|
|
991
|
+
let globalSandbox;
|
|
992
|
+
try {
|
|
993
|
+
const sm = this.getSettingsManager();
|
|
994
|
+
if (this.config.isSubAgent !== true) {
|
|
995
|
+
projectSandbox = sm.getForScope("project", cwd).sandbox;
|
|
996
|
+
}
|
|
997
|
+
globalSandbox = sm.getForScope("user").sandbox;
|
|
998
|
+
}
|
|
999
|
+
catch {
|
|
1000
|
+
// settings unavailable → fall through to per-run default
|
|
1001
|
+
}
|
|
1002
|
+
const sandboxConfig = resolveSandboxConfig(this.config.sandbox, projectSandbox, globalSandbox, this.config.headless === true);
|
|
779
1003
|
// A2: explicit sandbox modes (seatbelt, bwrap) must fail closed
|
|
780
1004
|
// per standard §S4. resolveSandboxBackend throws when an explicit
|
|
781
1005
|
// mode is unavailable on this host; we let it propagate. The
|
|
@@ -789,6 +1013,17 @@ export class Engine {
|
|
|
789
1013
|
const sandboxBackend = this.runtime
|
|
790
1014
|
? await this.runtime.resolveSandbox(sandboxConfig, cwd)
|
|
791
1015
|
: await this.resolveSandboxWithoutRuntime(sandboxConfig, cwd);
|
|
1016
|
+
// Observability: surface what sandbox actually applied this run — the
|
|
1017
|
+
// configured mode vs the resolved backend (auto may downgrade to off when
|
|
1018
|
+
// no OS backend is available) + the network policy. Without this you can't
|
|
1019
|
+
// tell whether shell commands were isolated /网络放没放. One line per run.
|
|
1020
|
+
logger.info("sandbox.resolved", {
|
|
1021
|
+
mode: sandboxConfig.mode,
|
|
1022
|
+
backend: sandboxBackend.name,
|
|
1023
|
+
isolated: sandboxBackend.name !== "off",
|
|
1024
|
+
network: sandboxConfig.network,
|
|
1025
|
+
cwd,
|
|
1026
|
+
});
|
|
792
1027
|
// sessionId is filled in after the session bundle is resolved below
|
|
793
1028
|
// (the session may be cold-started or resumed). Until then this is
|
|
794
1029
|
// intentionally shaped as a mutable local; we treat it as immutable
|
|
@@ -797,7 +1032,13 @@ export class Engine {
|
|
|
797
1032
|
...this.buildToolContext(),
|
|
798
1033
|
subAgentSpawner,
|
|
799
1034
|
agentDefinitions: this.getAgentDefinitions(cwd),
|
|
800
|
-
|
|
1035
|
+
// Stamp the resolved network policy onto the backend the tools see so
|
|
1036
|
+
// Bash can surface "网络 deny" on its result. Shallow-copy (don't mutate
|
|
1037
|
+
// the cached backend) — `wrap`/`hintForBlockedOutput` are plain function
|
|
1038
|
+
// properties and survive the spread. Off keeps network undefined.
|
|
1039
|
+
sandbox: sandboxBackend.name === "off"
|
|
1040
|
+
? sandboxBackend
|
|
1041
|
+
: { ...sandboxBackend, network: sandboxConfig.network },
|
|
801
1042
|
cwd,
|
|
802
1043
|
// TodoWrite reads this to push task_update events independently
|
|
803
1044
|
// of its return value, so the UI's pinned task panel refreshes
|
|
@@ -818,10 +1059,24 @@ export class Engine {
|
|
|
818
1059
|
// text block (when prose is present) followed by one image block per
|
|
819
1060
|
// attachment — the provider-specific clients translate this to OpenAI
|
|
820
1061
|
// `image_url` or Anthropic `{type:image, source:base64}` downstream.
|
|
1062
|
+
// When an attached image came from a workspace FILE (the desktop composer's
|
|
1063
|
+
// path-attach flow sets ParsedImage.name = the absolute path), surface that
|
|
1064
|
+
// path to the model as text. The image bytes still ride along for vision,
|
|
1065
|
+
// but tools that operate on files — GenerateImage(referenceImages),
|
|
1066
|
+
// Read, etc. — need the on-disk path, not just the pixels. Without this the
|
|
1067
|
+
// path the composer already knew was silently dropped, and the model would
|
|
1068
|
+
// answer "图片没落到项目文件夹,找不到路径" (the seedance 图生图 dead-end).
|
|
1069
|
+
// Only names that resolve to an existing file qualify; a pasted screenshot
|
|
1070
|
+
// whose name is just "screenshot.png" is not a path and is left out.
|
|
1071
|
+
const attachedPaths = collectAttachedImagePaths(parsedTask.images, (name) => (isAbsolute(name) ? name : join(cwd, name)), existsSync);
|
|
1072
|
+
const pathHint = attachedPaths.length > 0
|
|
1073
|
+
? `\n\n<attached-image-paths>\n${attachedPaths.join("\n")}\n</attached-image-paths>\n` +
|
|
1074
|
+
`(上面附带的图片在工作区的真实路径,如需把它们作为工具输入(例如 GenerateImage 的 referenceImages、图生图参考图),直接使用这些路径。)`
|
|
1075
|
+
: "";
|
|
821
1076
|
const userMessageContent = parsedTask.hasImages
|
|
822
1077
|
? [
|
|
823
|
-
...(parsedTask.text
|
|
824
|
-
? [{ type: "text", text: parsedTask.text }]
|
|
1078
|
+
...(parsedTask.text || pathHint
|
|
1079
|
+
? [{ type: "text", text: `${parsedTask.text}${pathHint}` }]
|
|
825
1080
|
: []),
|
|
826
1081
|
...parsedTask.images.map((img) => ({
|
|
827
1082
|
type: "image",
|
|
@@ -872,7 +1127,7 @@ export class Engine {
|
|
|
872
1127
|
// Append new user message
|
|
873
1128
|
const userMsg = { role: "user", content: userMessageContent };
|
|
874
1129
|
messages.push(userMsg);
|
|
875
|
-
session.transcript.appendMessage("user", userMessageContent);
|
|
1130
|
+
session.transcript.appendMessage("user", userMessageContent, { injected: options?.injected === true });
|
|
876
1131
|
// Flush "active" status to disk immediately. resume() set it in memory
|
|
877
1132
|
// (session-manager.ts), but without this write the on-disk state.json
|
|
878
1133
|
// still shows the previous run's terminal reason — so any external
|
|
@@ -895,6 +1150,12 @@ export class Engine {
|
|
|
895
1150
|
session.state.summary = summarySrc.slice(0, 80).replace(/\n/g, " ");
|
|
896
1151
|
this.sessionManager.saveState(session.state);
|
|
897
1152
|
}
|
|
1153
|
+
// Bump the conversation-turn counter: this user message starts a new turn.
|
|
1154
|
+
// One user message = one turn, regardless of how many turn-loop iterations
|
|
1155
|
+
// or tool calls it spans. File-history snapshots taken below are tagged
|
|
1156
|
+
// with this value so `/undo` reverts exactly this turn's file changes.
|
|
1157
|
+
// (Both resume and cold-start paths converge here.)
|
|
1158
|
+
session.state.turnSeq = (session.state.turnSeq ?? 0) + 1;
|
|
898
1159
|
// Stamp the resolved session id for downstream logging.
|
|
899
1160
|
//
|
|
900
1161
|
// `setCurrentSid` updates the module-level fallback so any code path
|
|
@@ -1043,6 +1304,9 @@ export class Engine {
|
|
|
1043
1304
|
toolExecutor.setContext(toolCtx);
|
|
1044
1305
|
const contextManager = new ContextManager({
|
|
1045
1306
|
maxTokens: this.resolveMaxContextTokens(),
|
|
1307
|
+
// Drop undefined fields so they don't clobber ContextManager defaults
|
|
1308
|
+
// (spread of `{x: undefined}` would override the default with undefined).
|
|
1309
|
+
...Object.fromEntries(Object.entries(this.resolveContextRatios()).filter(([, v]) => v !== undefined)),
|
|
1046
1310
|
});
|
|
1047
1311
|
this.lastContextManager = contextManager;
|
|
1048
1312
|
const { disabledSkills, disabledPlugins } = this.readDisabledLists();
|
|
@@ -1057,6 +1321,8 @@ export class Engine {
|
|
|
1057
1321
|
instructionOptions: { compatFileNames: compatFileNamesFrom(this.config.instructions) },
|
|
1058
1322
|
disabledSkills,
|
|
1059
1323
|
disabledPlugins,
|
|
1324
|
+
skillAllowlist: this.config.skillAllowlist,
|
|
1325
|
+
memoriesMaxAgeDays: this.readMemoriesConfig()?.maxAge,
|
|
1060
1326
|
});
|
|
1061
1327
|
// Connect MCP servers (if configured and not already connected).
|
|
1062
1328
|
// B1: prefer the Runtime-owned MCPManager so all sessions in a
|
|
@@ -1071,7 +1337,7 @@ export class Engine {
|
|
|
1071
1337
|
else {
|
|
1072
1338
|
this.mcpManager = new MCPManager(this.toolRegistry);
|
|
1073
1339
|
}
|
|
1074
|
-
await this.mcpManager.connectAll(mcpServers);
|
|
1340
|
+
await this.mcpManager.connectAll(mcpServers, this);
|
|
1075
1341
|
}
|
|
1076
1342
|
// Parallelize slow initialization:
|
|
1077
1343
|
// 1. createLLMClient — network handshake (started earlier)
|
|
@@ -1114,14 +1380,45 @@ export class Engine {
|
|
|
1114
1380
|
const disabledBuiltins = new Set(Object.keys(builtinOverride).filter((name) => builtinOverride[name] === "off" && registryNames.has(name)));
|
|
1115
1381
|
toolCtx.disabledBuiltins = disabledBuiltins;
|
|
1116
1382
|
}
|
|
1383
|
+
// MCP tool exposure is per-SESSION even though the pool/registry are
|
|
1384
|
+
// worker-shared (B1): a server connected by another project's session
|
|
1385
|
+
// registers its tools into the SHARED registry, and without this filter
|
|
1386
|
+
// they leaked into every session (e.g. chrome-devtools tools showing up
|
|
1387
|
+
// in a project that never enabled the plugin). Keep an MCP tool only when
|
|
1388
|
+
// its server is in THIS session's merged config.mcpServers — which
|
|
1389
|
+
// already folds the project's capabilityOverrides. Gated on the config
|
|
1390
|
+
// being present: engines without one (sub-agents, bare tests) have no
|
|
1391
|
+
// MCP tools in their private registries anyway.
|
|
1392
|
+
const allowedMcpServers = new Set(Object.entries(this.config.mcpServers ?? {})
|
|
1393
|
+
.filter(([, c]) => c.enabled !== false)
|
|
1394
|
+
.map(([n]) => n));
|
|
1395
|
+
toolCtx.allowedMcpServers = allowedMcpServers;
|
|
1396
|
+
const mcpVisible = (toolName) => {
|
|
1397
|
+
const reg = this.toolRegistry.getTool(toolName);
|
|
1398
|
+
return reg?.source !== "mcp" || allowedMcpServers.has(reg?.serverName ?? "");
|
|
1399
|
+
};
|
|
1400
|
+
// Feature-flag visibility: a builtin mapped in TOOL_FEATURE_FLAGS is
|
|
1401
|
+
// hidden when its flag resolves to false (default-on flags only hide when
|
|
1402
|
+
// explicitly disabled, so zero regression out of the box). Read once per
|
|
1403
|
+
// turn so flipping a flag in settings takes effect on the NEXT message,
|
|
1404
|
+
// like the other capability kinds.
|
|
1405
|
+
const featureFlags = this.readFeatureFlags();
|
|
1117
1406
|
const allToolDefs = applyBuiltinOverrideVisibility(this.toolRegistry.getToolDefinitions(), builtinOverride)
|
|
1407
|
+
.filter((t) => mcpVisible(t.name))
|
|
1118
1408
|
.filter((t) => {
|
|
1119
1409
|
const guard = BUILTIN_TOOL_GUARDS.get(t.name);
|
|
1120
1410
|
return guard ? guard(guardCwd) : true;
|
|
1121
1411
|
})
|
|
1122
|
-
.
|
|
1123
|
-
|
|
1124
|
-
:
|
|
1412
|
+
.filter((t) => {
|
|
1413
|
+
const flag = TOOL_FEATURE_FLAGS.get(t.name);
|
|
1414
|
+
return flag ? isFeatureEnabled(featureFlags, flag) : true;
|
|
1415
|
+
})
|
|
1416
|
+
// Dynamic per-engine bits the static defs can't carry: the Agent tool's
|
|
1417
|
+
// agent_type enum + listing, and the image/video provider names. See
|
|
1418
|
+
// applyDynamicToolDef — forwarding only the Agent description (dropping
|
|
1419
|
+
// its rebuilt inputSchema) used to strip the agent_type enum, so the
|
|
1420
|
+
// model omitted agent_type and configured roles never applied.
|
|
1421
|
+
.map((t) => applyDynamicToolDef(t, toolCtx.agentDefinitions, guardCwd));
|
|
1125
1422
|
// In plan mode, only expose read-only/planning tools so the model won't
|
|
1126
1423
|
// attempt writes. Shared with executor.ts's execution gate via
|
|
1127
1424
|
// PLAN_MODE_ALLOWED_TOOLS so what the model SEES and what the executor
|
|
@@ -1130,12 +1427,13 @@ export class Engine {
|
|
|
1130
1427
|
const toolDefs = this.planMode
|
|
1131
1428
|
? allToolDefs.filter((t) => PLAN_MODE_ALLOWED_TOOLS.has(t.name))
|
|
1132
1429
|
: allToolDefs;
|
|
1133
|
-
const [llmClient,
|
|
1430
|
+
const [llmClient, fullSystemPrompt, dynamicContextMsg] = await Promise.all([
|
|
1134
1431
|
llmClientPromise,
|
|
1432
|
+
// System prompt is now the STABLE prefix only — skills + git status moved
|
|
1433
|
+
// out to a trailing per-turn message so they no longer bust the cache.
|
|
1135
1434
|
promptComposer.buildSystemPrompt(toolDefs),
|
|
1136
|
-
promptComposer.
|
|
1435
|
+
promptComposer.buildDynamicContextMessage(),
|
|
1137
1436
|
]);
|
|
1138
|
-
const fullSystemPrompt = [systemPrompt, systemContext].filter(Boolean).join("\n\n");
|
|
1139
1437
|
// Prepend userContext (CLAUDE.md) as first message (sync, fast)
|
|
1140
1438
|
const userContextMsg = promptComposer.buildUserContextMessage();
|
|
1141
1439
|
if (userContextMsg) {
|
|
@@ -1154,6 +1452,12 @@ export class Engine {
|
|
|
1154
1452
|
// reminder → user request.
|
|
1155
1453
|
messages.splice(messages.length - 1, 0, lifecycleReminder);
|
|
1156
1454
|
}
|
|
1455
|
+
// Volatile context (skills + git status) goes at the very END — after the
|
|
1456
|
+
// user task — so it sits past the conversation's cache breakpoint. A change
|
|
1457
|
+
// here (new skill, edited file) never invalidates the cached history prefix.
|
|
1458
|
+
if (dynamicContextMsg) {
|
|
1459
|
+
messages.push(dynamicContextMsg);
|
|
1460
|
+
}
|
|
1157
1461
|
this.lastSessionId = session.state.sessionId;
|
|
1158
1462
|
this.lastMessages = messages;
|
|
1159
1463
|
// Wire up LLM summarization for context compaction
|
|
@@ -1185,8 +1489,13 @@ export class Engine {
|
|
|
1185
1489
|
});
|
|
1186
1490
|
return summaryResponse.text;
|
|
1187
1491
|
});
|
|
1188
|
-
// Create components (requires resolved llmClient)
|
|
1492
|
+
// Create components (requires resolved llmClient).
|
|
1189
1493
|
const modelFacade = new ModelFacade(llmClient, session.transcript);
|
|
1494
|
+
// Session-cumulative usage baseline: the LLM client is recreated per run
|
|
1495
|
+
// (its getUsage() counts only THIS run), so to accumulate across runs we
|
|
1496
|
+
// capture the persisted total at run start and fold this run's usage onto
|
|
1497
|
+
// it (see foldRunUsage). Snapshot now, before any turn boundary fires.
|
|
1498
|
+
const usageBaseline = { ...session.state.tokenUsage };
|
|
1190
1499
|
// Wire getOutputTokens for token budget tracking
|
|
1191
1500
|
modelFacade.getOutputTokens = () => {
|
|
1192
1501
|
const usage = llmClient.getUsage();
|
|
@@ -1221,14 +1530,39 @@ export class Engine {
|
|
|
1221
1530
|
// File history: auto-backup before Write/Edit
|
|
1222
1531
|
const sessionDir = join(this.config.sessionStorageDir ?? join(userHome(), ".code-shell", "sessions"), session.state.sessionId);
|
|
1223
1532
|
const fileHistory = FileHistory.loadFromDir(sessionDir);
|
|
1224
|
-
|
|
1533
|
+
// Keep a reference so we can unregister in the finally below. Registering an
|
|
1534
|
+
// anonymous handler every run() leaks: unregister matches by handler
|
|
1535
|
+
// identity, so without a stored reference each run stacks another identical
|
|
1536
|
+
// on_tool_start handler that fires (and re-snapshots) on every tool forever.
|
|
1537
|
+
const fileHistoryHandler = async (context) => {
|
|
1225
1538
|
const toolName = context.data?.toolName;
|
|
1226
1539
|
const args = context.data?.args;
|
|
1540
|
+
// Tag snapshots with the current turn (stamped above before any tool
|
|
1541
|
+
// runs) so turn-level /undo can revert just this user message's edits.
|
|
1542
|
+
const turnSeq = session.state.turnSeq;
|
|
1227
1543
|
if ((toolName === "Write" || toolName === "Edit") && args?.file_path) {
|
|
1228
|
-
|
|
1544
|
+
const path = args.file_path;
|
|
1545
|
+
// saveSnapshot returns null when the file does not exist yet — this
|
|
1546
|
+
// hook runs BEFORE the tool, so a null here means the turn is CREATING
|
|
1547
|
+
// the file. Record it (idempotent per turn) so /undo can delete it and
|
|
1548
|
+
// /redo can recreate it.
|
|
1549
|
+
if (fileHistory.saveSnapshot(path, turnSeq) === null && turnSeq !== undefined) {
|
|
1550
|
+
fileHistory.recordCreated(path, turnSeq);
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
else if (toolName === "ApplyPatch" && typeof args?.patch === "string") {
|
|
1554
|
+
// ApplyPatch mutates files too, so /undo must see them. Snapshot every
|
|
1555
|
+
// existing file the patch updates or deletes (adds have no prior
|
|
1556
|
+
// content). Resolve relative patch paths against the engine cwd, the
|
|
1557
|
+
// same base ApplyPatch itself uses.
|
|
1558
|
+
const cwd = this.config.cwd ?? process.cwd();
|
|
1559
|
+
for (const target of patchBackupTargets(args.patch, cwd)) {
|
|
1560
|
+
fileHistory.saveSnapshot(target, turnSeq);
|
|
1561
|
+
}
|
|
1229
1562
|
}
|
|
1230
1563
|
return {};
|
|
1231
|
-
}
|
|
1564
|
+
};
|
|
1565
|
+
this.hooks.register("on_tool_start", fileHistoryHandler, 100, "file_history_backup");
|
|
1232
1566
|
// Hook: agent start
|
|
1233
1567
|
await this.emitHook("on_agent_start", {
|
|
1234
1568
|
sessionId: session.state.sessionId,
|
|
@@ -1238,20 +1572,70 @@ export class Engine {
|
|
|
1238
1572
|
// Goal mode: register a GoalStopHook for the lifetime of THIS run so the
|
|
1239
1573
|
// turn loop keeps going until the session model judges the goal met.
|
|
1240
1574
|
// Registered per-run (and cleared in `finally`) so a later goal-less
|
|
1241
|
-
// send doesn't inherit a stale goal. The judge
|
|
1242
|
-
// same model
|
|
1575
|
+
// send doesn't inherit a stale goal. The judge runs on `auxSummaryClient`
|
|
1576
|
+
// — the same cheap aux model used for summarize/compaction — not the
|
|
1577
|
+
// (potentially expensive) session model: "is this goal met?" is a classic
|
|
1578
|
+
// aux-tier task, and a goal run can invoke the judge up to maxStopBlocks
|
|
1579
|
+
// times.
|
|
1243
1580
|
// Normalize the raw goal (string | GoalConfig) once at the run boundary;
|
|
1244
1581
|
// everything inward uses the GoalConfig. normalizeGoal() returns undefined
|
|
1245
1582
|
// when there's effectively no goal (empty objective).
|
|
1246
|
-
|
|
1583
|
+
//
|
|
1584
|
+
// PERSISTENT GOAL (CC /goal style): a goal set on one send survives across
|
|
1585
|
+
// later sends and manual interrupts until met or cleared. Resolution:
|
|
1586
|
+
// 1. options.goal — this send explicitly sets/replaces the goal.
|
|
1587
|
+
// 2. session.state.activeGoal — a goal set on an earlier send.
|
|
1588
|
+
// 3. config.goal — engine-level default (rare; e.g. headless).
|
|
1589
|
+
// When (1) supplies a goal that differs from the stored one we REPLACE the
|
|
1590
|
+
// persisted active goal (one active goal per session) and announce it. A
|
|
1591
|
+
// bare send with no options.goal inherits the stored active goal so the
|
|
1592
|
+
// model keeps working toward it — that's what makes it persistent.
|
|
1593
|
+
const explicitGoal = normalizeGoal(options?.goal);
|
|
1594
|
+
const storedGoal = this.config.isSubAgent !== true ? session.state.activeGoal : undefined;
|
|
1595
|
+
if (explicitGoal && this.config.isSubAgent !== true) {
|
|
1596
|
+
const replaced = !!storedGoal && storedGoal.objective !== explicitGoal.objective;
|
|
1597
|
+
// Stamp WHEN this goal was set so the judge can anchor relative deadlines
|
|
1598
|
+
// ("做到3点") to the set time, not "now" — else once the clock passes the
|
|
1599
|
+
// deadline the judge could read "3点" as tomorrow's and never stop. A new
|
|
1600
|
+
// or changed objective gets a fresh stamp; re-sending the SAME objective
|
|
1601
|
+
// keeps the original anchor (the goal continues, the user didn't restate a
|
|
1602
|
+
// new deadline). User input never carries setAtMs, so we set it here.
|
|
1603
|
+
explicitGoal.setAtMs = resolveGoalSetAt(explicitGoal.objective, storedGoal, Date.now());
|
|
1604
|
+
session.state.activeGoal = explicitGoal;
|
|
1605
|
+
this.sessionManager.saveState(session.state);
|
|
1606
|
+
options?.onStream?.({
|
|
1607
|
+
type: "goal_set",
|
|
1608
|
+
objective: explicitGoal.objective,
|
|
1609
|
+
replaced,
|
|
1610
|
+
});
|
|
1611
|
+
}
|
|
1612
|
+
const normalizedGoal = explicitGoal ?? storedGoal ?? normalizeGoal(this.config.goal);
|
|
1247
1613
|
let goalHookHandler = null;
|
|
1248
1614
|
if (normalizedGoal && this.config.isSubAgent !== true) {
|
|
1249
1615
|
goalHookHandler = createGoalStopHook({
|
|
1250
1616
|
goal: normalizedGoal,
|
|
1251
|
-
llm:
|
|
1617
|
+
llm: auxSummaryClient,
|
|
1252
1618
|
log: logger,
|
|
1619
|
+
// Clear the persisted active goal the moment the judge says it's met,
|
|
1620
|
+
// so a later bare send doesn't re-inherit a satisfied goal. The hook
|
|
1621
|
+
// calls this from inside its met branch (single source of truth for
|
|
1622
|
+
// "goal achieved"); engine owns the persistence side-effect.
|
|
1623
|
+
onMet: () => {
|
|
1624
|
+
if (session.state.activeGoal) {
|
|
1625
|
+
session.state.activeGoal = undefined;
|
|
1626
|
+
this.sessionManager.saveState(session.state);
|
|
1627
|
+
}
|
|
1628
|
+
},
|
|
1629
|
+
// Re-read the persisted goal each turn so a mid-run 清除 (clearGoal
|
|
1630
|
+
// wrote state.json but this hook's frozen goal copy + the closure's
|
|
1631
|
+
// in-RAM session are untouched) actually stops the judge. Reads disk
|
|
1632
|
+
// via readActiveGoal — authoritative and independent of which session
|
|
1633
|
+
// instance the run closure holds.
|
|
1634
|
+
isGoalActive: (sid) => this.sessionManager.readActiveGoal(sid) !== undefined,
|
|
1253
1635
|
});
|
|
1254
1636
|
this.hooks.register("on_stop", goalHookHandler, 0, "goal-stop");
|
|
1637
|
+
// Expose for clearGoal() mid-run. Already guarded by isSubAgent above.
|
|
1638
|
+
this.activeGoalHook = goalHookHandler;
|
|
1255
1639
|
}
|
|
1256
1640
|
// Surface compaction events to the UI so the user knows when context was trimmed.
|
|
1257
1641
|
// Buffer the most recent event so TurnLoop can drain it and emit the
|
|
@@ -1278,6 +1662,22 @@ export class Engine {
|
|
|
1278
1662
|
pendingCompactInfo = null;
|
|
1279
1663
|
return info;
|
|
1280
1664
|
},
|
|
1665
|
+
consumeSteer: () => this.consumeSteer(sid),
|
|
1666
|
+
// Clear the persisted goal for a self-reported completion / confirmed
|
|
1667
|
+
// cancel. Clears the in-RAM session's activeGoal (so THIS run's later
|
|
1668
|
+
// turns don't re-arm) AND persists it, and drops the in-flight stop
|
|
1669
|
+
// hook so nothing re-blocks the stop we're about to return.
|
|
1670
|
+
clearPersistedGoal: () => {
|
|
1671
|
+
if (session.state.activeGoal !== undefined) {
|
|
1672
|
+
session.state.activeGoal = undefined;
|
|
1673
|
+
this.sessionManager.saveState(session.state);
|
|
1674
|
+
}
|
|
1675
|
+
if (goalHookHandler) {
|
|
1676
|
+
this.hooks.unregister("on_stop", goalHookHandler);
|
|
1677
|
+
if (this.activeGoalHook === goalHookHandler)
|
|
1678
|
+
this.activeGoalHook = null;
|
|
1679
|
+
}
|
|
1680
|
+
},
|
|
1281
1681
|
ctxOverheadStore: {
|
|
1282
1682
|
get: (s) => this.ctxOverheadBySid.get(s) ?? 0,
|
|
1283
1683
|
set: (s, n) => {
|
|
@@ -1285,8 +1685,20 @@ export class Engine {
|
|
|
1285
1685
|
},
|
|
1286
1686
|
},
|
|
1287
1687
|
}, {
|
|
1288
|
-
|
|
1289
|
-
|
|
1688
|
+
// Goal mode raises the turn ceiling: an unattended goal run keeps
|
|
1689
|
+
// getting re-blocked by the stop-hook until it's done, and the 100
|
|
1690
|
+
// interactive default would silently truncate a long objective. The
|
|
1691
|
+
// real backstops are the goal token/time budgets + maxStopBlocks.
|
|
1692
|
+
maxTurns: resolveMaxTurns(this.config.maxTurns, normalizedGoal),
|
|
1693
|
+
// Consecutive stop-block cap: config override > goal.maxStopBlocks >
|
|
1694
|
+
// GOAL_DEFAULT_MAX_STOP_BLOCKS(25). The old hardcoded 8 was too tight
|
|
1695
|
+
// for complex goals that legitimately get re-blocked while advancing.
|
|
1696
|
+
maxStopBlocks: resolveMaxStopBlocks(this.config.maxStopBlocks, normalizedGoal),
|
|
1697
|
+
// 25 (was 10): modern models routinely batch >10 parallel tool calls
|
|
1698
|
+
// (e.g. reading a dozen files at once). At 10 the excess was silently
|
|
1699
|
+
// dropped; the turn loop now also warns the model when it caps, but a
|
|
1700
|
+
// higher ceiling avoids the round-trip in the common case. (B-3)
|
|
1701
|
+
maxToolCallsPerTurn: this.config.maxToolCallsPerTurn ?? 25,
|
|
1290
1702
|
onStream: options?.onStream,
|
|
1291
1703
|
signal: options?.signal,
|
|
1292
1704
|
// Goal mode: the active goal is surfaced to the on_stop handler via
|
|
@@ -1298,62 +1710,99 @@ export class Engine {
|
|
|
1298
1710
|
// completed run.
|
|
1299
1711
|
onTurnBoundary: (turnCount) => {
|
|
1300
1712
|
session.state.turnCount = turnCount;
|
|
1301
|
-
|
|
1302
|
-
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1713
|
+
// baseline + this run's running total (idempotent per boundary,
|
|
1714
|
+
// accumulates across runs; carries cacheRead/cacheCreation too).
|
|
1715
|
+
session.state.tokenUsage = foldRunUsage(usageBaseline, modelFacade.getUsage());
|
|
1716
|
+
// Surface the session-cumulative cache counts to the UI (the "本会话
|
|
1717
|
+
// 累计命中率" tooltip). Separate from turn-loop's per-response
|
|
1718
|
+
// usage_update (which drives the live context reading).
|
|
1719
|
+
const cum = session.state.tokenUsage;
|
|
1720
|
+
options?.onStream?.({
|
|
1721
|
+
type: "usage_update",
|
|
1722
|
+
promptTokens: cum.promptTokens,
|
|
1723
|
+
sessionPromptTokens: cum.promptTokens,
|
|
1724
|
+
sessionCacheReadTokens: cum.cacheReadTokens ?? 0,
|
|
1725
|
+
sessionCacheCreationTokens: cum.cacheCreationTokens ?? 0,
|
|
1726
|
+
});
|
|
1307
1727
|
if (this.config.costStore) {
|
|
1308
1728
|
session.state.costState = this.config.costStore.serialize();
|
|
1309
1729
|
}
|
|
1310
1730
|
this.sessionManager.saveState(session.state);
|
|
1311
1731
|
},
|
|
1312
1732
|
});
|
|
1733
|
+
// Expose this run's loop for mid-run extension (TODO 3.1). Top-level only —
|
|
1734
|
+
// a sub-agent's loop is its own concern and isn't user-extendable.
|
|
1735
|
+
if (this.config.isSubAgent !== true)
|
|
1736
|
+
this.activeTurnLoop = turnLoop;
|
|
1737
|
+
// Expose this run's session bundle so a mid-run clearGoal() wipes the goal
|
|
1738
|
+
// on the very instance this loop keeps saving (see field doc). Top-level
|
|
1739
|
+
// only — sub-agents don't carry user-clearable persistent goals.
|
|
1740
|
+
if (this.config.isSubAgent !== true)
|
|
1741
|
+
this.activeRunSession = session;
|
|
1313
1742
|
let result;
|
|
1314
1743
|
try {
|
|
1315
1744
|
result = await turnLoop.run(messages);
|
|
1316
|
-
// ──
|
|
1317
|
-
//
|
|
1318
|
-
//
|
|
1319
|
-
//
|
|
1320
|
-
//
|
|
1321
|
-
//
|
|
1322
|
-
// session's background agents are running, then drain ALL their results
|
|
1323
|
-
// and feed them back as one more turn so the agent summarizes.
|
|
1745
|
+
// ── Headless: drain background sub-agents before resolving ───────
|
|
1746
|
+
// Unified background-work model (2026-06-17): the engine NO LONGER parks
|
|
1747
|
+
// every run waiting on background work. Background work (sub-agents,
|
|
1748
|
+
// video polls, shells) ends the turn, yields, and is picked up later by
|
|
1749
|
+
// the server's notification-wakeup path (maybeWakeIdleSession). The
|
|
1750
|
+
// INTERACTIVE path relies on that wakeup + a run-boundary re-check.
|
|
1324
1751
|
//
|
|
1325
|
-
//
|
|
1326
|
-
//
|
|
1752
|
+
// HEADLESS is the exception: a one-shot `engine.run` whose caller takes
|
|
1753
|
+
// `result.text` as THE answer (automation / SDK) has no later turn to
|
|
1754
|
+
// pick up a wakeup — so it must wait, before resolving, until its own
|
|
1755
|
+
// background SUB-AGENTS finish and summarize. Only sub-agents (their
|
|
1756
|
+
// summary IS part of this run's result), NOT shells (a dev server never
|
|
1757
|
+
// exits → would hang headless forever) and NOT video (a long render the
|
|
1758
|
+
// one-shot run shouldn't block on). This replaces the old for(;;) park
|
|
1759
|
+
// (s-mpvf4rsj-bb6e4639 invariant) for the headless case only.
|
|
1327
1760
|
const sid = session.state.sessionId;
|
|
1328
1761
|
const isTopLevel = this.config.isSubAgent !== true;
|
|
1329
|
-
if (isTopLevel) {
|
|
1762
|
+
if (isTopLevel && this.isHeadless()) {
|
|
1330
1763
|
let aborted = options?.signal?.aborted === true;
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1764
|
+
// Loop: a summarize turn can spawn a NEW background sub-agent; keep
|
|
1765
|
+
// draining + summarizing until none remain. turnCount accumulates, so
|
|
1766
|
+
// the turn-loop's maxTurns still bounds runaway re-summarization.
|
|
1767
|
+
for (;;) {
|
|
1768
|
+
while (!aborted && asyncAgentRegistry.hasRunningForSession(sid)) {
|
|
1769
|
+
aborted = await this.waitForBackgroundAgentChange(sid, options?.signal);
|
|
1770
|
+
}
|
|
1771
|
+
let pending = notificationQueue.drainAll(sid);
|
|
1772
|
+
if (aborted && pending.length === 0) {
|
|
1773
|
+
// Abort race: an agent calls markCompleted (registry notify) and only
|
|
1774
|
+
// THEN enqueue (queue notify) as two separate statements. If the abort
|
|
1775
|
+
// fired before that agent's completion `.then` ran, the while above
|
|
1776
|
+
// exited on `aborted`, this drainAll caught nothing, and a naive
|
|
1777
|
+
// `break` here would drop the agent's output. Give still-settling
|
|
1778
|
+
// agents a bounded window to finish enqueuing, then drain once more.
|
|
1779
|
+
// Each wait is timeout-bounded so a genuinely stuck (never-completing)
|
|
1780
|
+
// agent can't hang abort cleanup forever — we'd rather lose nothing in
|
|
1781
|
+
// the common case and not hang in the pathological one.
|
|
1782
|
+
for (let i = 0; i < 20 && asyncAgentRegistry.hasRunningForSession(sid); i++) {
|
|
1783
|
+
const changed = await this.waitForBackgroundAgentChangeOrTimeout(sid, 25);
|
|
1784
|
+
if (!changed)
|
|
1785
|
+
break; // timed out with no state change → stop waiting
|
|
1786
|
+
}
|
|
1787
|
+
pending = notificationQueue.drainAll(sid);
|
|
1788
|
+
if (pending.length === 0)
|
|
1789
|
+
break;
|
|
1790
|
+
}
|
|
1791
|
+
else if (pending.length === 0) {
|
|
1792
|
+
break;
|
|
1793
|
+
}
|
|
1339
1794
|
const injected = {
|
|
1340
1795
|
role: "user",
|
|
1341
1796
|
content: `<system-reminder>\n${buildNotificationMessage(pending)}\n</system-reminder>`,
|
|
1342
1797
|
};
|
|
1343
1798
|
if (aborted) {
|
|
1344
|
-
//
|
|
1345
|
-
//
|
|
1346
|
-
|
|
1347
|
-
// user message in this session will see these results in history.
|
|
1348
|
-
session.transcript.appendMessage(injected.role, injected.content);
|
|
1799
|
+
// Mark injected: a synthetic notification, not the user's own input —
|
|
1800
|
+
// the disk reader drops it on replay so no phantom user bubble.
|
|
1801
|
+
session.transcript.appendMessage(injected.role, injected.content, { injected: true });
|
|
1349
1802
|
result = { ...result, messages: [...result.messages, injected] };
|
|
1803
|
+
break;
|
|
1350
1804
|
}
|
|
1351
|
-
|
|
1352
|
-
// All background agents finished: one more turn so the agent reads
|
|
1353
|
-
// every result and summarizes. turnCount keeps accumulating, so
|
|
1354
|
-
// maxTurns still bounds runaway re-summarization.
|
|
1355
|
-
result = await turnLoop.run([...result.messages, injected]);
|
|
1356
|
-
}
|
|
1805
|
+
result = await turnLoop.run([...result.messages, injected]);
|
|
1357
1806
|
}
|
|
1358
1807
|
}
|
|
1359
1808
|
}
|
|
@@ -1362,6 +1811,15 @@ export class Engine {
|
|
|
1362
1811
|
// long-lived engine doesn't keep blocking stops.
|
|
1363
1812
|
if (goalHookHandler)
|
|
1364
1813
|
this.hooks.unregister("on_stop", goalHookHandler);
|
|
1814
|
+
if (this.activeGoalHook === goalHookHandler)
|
|
1815
|
+
this.activeGoalHook = null;
|
|
1816
|
+
if (this.activeTurnLoop === turnLoop)
|
|
1817
|
+
this.activeTurnLoop = null;
|
|
1818
|
+
if (this.activeRunSession === session)
|
|
1819
|
+
this.activeRunSession = null;
|
|
1820
|
+
// Run-scoped too: this handler is re-registered every run(), so it must be
|
|
1821
|
+
// dropped here or it stacks duplicates that re-snapshot on every tool.
|
|
1822
|
+
this.hooks.unregister("on_tool_start", fileHistoryHandler);
|
|
1365
1823
|
}
|
|
1366
1824
|
this.lastMessages = result.messages;
|
|
1367
1825
|
this.compactedMessagesBySession.set(session.state.sessionId, this.stripUserContextMessage(result.messages, userContextMsg));
|
|
@@ -1404,6 +1862,12 @@ export class Engine {
|
|
|
1404
1862
|
void buildSessionTitle(auxSummaryClient, firstUserText, result.text)
|
|
1405
1863
|
.then((title) => {
|
|
1406
1864
|
if (title) {
|
|
1865
|
+
// Persist the title so it survives a localStorage wipe / disk
|
|
1866
|
+
// rebuild — it used to live only in the renderer's localStorage
|
|
1867
|
+
// index. This .then resolves AFTER the saveState below (:1892), so
|
|
1868
|
+
// it must save again itself rather than rely on that write.
|
|
1869
|
+
session.state.title = title;
|
|
1870
|
+
this.sessionManager.saveState(session.state);
|
|
1407
1871
|
onStream({
|
|
1408
1872
|
type: "session_title",
|
|
1409
1873
|
sessionId: session.state.sessionId,
|
|
@@ -1421,12 +1885,9 @@ export class Engine {
|
|
|
1421
1885
|
// distinction and misled anyone reading state.json.
|
|
1422
1886
|
session.state.turnCount = turnLoop.currentTurn;
|
|
1423
1887
|
session.state.status = result.reason;
|
|
1888
|
+
// Session-cumulative (baseline + this run) for persistence...
|
|
1424
1889
|
const usage = modelFacade.getUsage();
|
|
1425
|
-
session.state.tokenUsage =
|
|
1426
|
-
promptTokens: usage.totalPromptTokens,
|
|
1427
|
-
completionTokens: usage.totalCompletionTokens,
|
|
1428
|
-
totalTokens: usage.totalTokens,
|
|
1429
|
-
};
|
|
1890
|
+
session.state.tokenUsage = foldRunUsage(usageBaseline, usage);
|
|
1430
1891
|
if (this.config.costStore) {
|
|
1431
1892
|
session.state.costState = this.config.costStore.serialize();
|
|
1432
1893
|
}
|
|
@@ -1459,8 +1920,8 @@ export class Engine {
|
|
|
1459
1920
|
*/
|
|
1460
1921
|
/**
|
|
1461
1922
|
* Resolve the LLM client for background/auxiliary work (memory extraction,
|
|
1462
|
-
* auto-dream). When settings.
|
|
1463
|
-
* cache) a dedicated client for it so per-turn book-keeping runs on a cheap
|
|
1923
|
+
* auto-dream). When settings.defaults.auxText names a valid pool model, build
|
|
1924
|
+
* (and cache) a dedicated client for it so per-turn book-keeping runs on a cheap
|
|
1464
1925
|
* fast model instead of the expensive primary. Falls back to `fallback` (the
|
|
1465
1926
|
* active run's client) when unset, unknown, or on any build failure — aux
|
|
1466
1927
|
* work is best-effort and must never break a run.
|
|
@@ -1473,7 +1934,9 @@ export class Engine {
|
|
|
1473
1934
|
// once per run on the post-run background path, so the cost is fine.
|
|
1474
1935
|
const sm = this.getSettingsManager();
|
|
1475
1936
|
sm.invalidate();
|
|
1476
|
-
|
|
1937
|
+
// Unified store's defaults.auxText (a connection id = pool key) selects
|
|
1938
|
+
// the aux model; resolveAuxKey returns it (or undefined).
|
|
1939
|
+
auxKey = resolveAuxKey(sm.get());
|
|
1477
1940
|
}
|
|
1478
1941
|
catch {
|
|
1479
1942
|
return fallback;
|
|
@@ -1519,7 +1982,9 @@ export class Engine {
|
|
|
1519
1982
|
try {
|
|
1520
1983
|
// Background calls run on the auxiliary model when configured, so memory
|
|
1521
1984
|
// book-keeping doesn't burn the expensive primary model every turn.
|
|
1522
|
-
|
|
1985
|
+
// settings.memories.extractionModel (if set + valid) overrides the aux
|
|
1986
|
+
// model specifically for memory extraction (TODO 8.1).
|
|
1987
|
+
const llmClient = await this.resolveExtractionClient(primaryClient);
|
|
1523
1988
|
// Only run memory extraction for substantive sessions. The previous
|
|
1524
1989
|
// threshold of 4 user+assistant messages was low enough that two-line
|
|
1525
1990
|
// exchanges ("what's the time?" / "noon") triggered a full LLM
|
|
@@ -1558,6 +2023,10 @@ export class Engine {
|
|
|
1558
2023
|
},
|
|
1559
2024
|
runDream: async ({ systemPrompt, userPrompt, projectDir }) => this.runDreamLoop({ systemPrompt, userPrompt, projectDir, llmClient, sessionId }),
|
|
1560
2025
|
projectDir: cwd,
|
|
2026
|
+
// settings.memories.maxCount caps memories accepted per extraction;
|
|
2027
|
+
// autoExtract=false turns the extractor off (summaries/dream stay).
|
|
2028
|
+
maxCount: this.readMemoriesConfig()?.maxCount,
|
|
2029
|
+
autoExtract: this.readMemoriesConfig()?.autoExtract,
|
|
1561
2030
|
});
|
|
1562
2031
|
await orchestrator.run(plainMessages, sessionId);
|
|
1563
2032
|
}
|
|
@@ -1608,10 +2077,10 @@ export class Engine {
|
|
|
1608
2077
|
* Switch the active model by pool key. Takes effect on the next run() call.
|
|
1609
2078
|
* Returns the new model entry.
|
|
1610
2079
|
*
|
|
1611
|
-
* Persists settings.
|
|
2080
|
+
* Persists settings.defaults.text (= the connection id / pool key) so the
|
|
1612
2081
|
* next process startup defaults to the same model — without this, switches
|
|
1613
2082
|
* only live in memory and every restart reverts to the previously persisted
|
|
1614
|
-
*
|
|
2083
|
+
* defaults.text.
|
|
1615
2084
|
*/
|
|
1616
2085
|
switchModel(key) {
|
|
1617
2086
|
const entry = this.modelPool.switch(key);
|
|
@@ -1620,19 +2089,44 @@ export class Engine {
|
|
|
1620
2089
|
// this.config.clientDefaults and survive the switch untouched.
|
|
1621
2090
|
const nextLlm = this.modelPool.toLLMConfig(entry);
|
|
1622
2091
|
this.config = { ...this.config, llm: nextLlm };
|
|
1623
|
-
this.persistActiveModel(entry
|
|
2092
|
+
this.persistActiveModel(entry);
|
|
1624
2093
|
return entry;
|
|
1625
2094
|
}
|
|
2095
|
+
/**
|
|
2096
|
+
* Zero a session's cumulative token/cache usage on disk. Called on a model
|
|
2097
|
+
* switch: a different model has its own prompt cache, so the accumulated
|
|
2098
|
+
* cache-hit stats from the prior model are no longer meaningful. The next
|
|
2099
|
+
* run's baseline (snapshotted from state.tokenUsage) then starts from zero.
|
|
2100
|
+
*/
|
|
2101
|
+
resetSessionUsage(sessionId) {
|
|
2102
|
+
const zero = { promptTokens: 0, completionTokens: 0, totalTokens: 0 };
|
|
2103
|
+
// If the session is mid-run right now, update its live state so the next
|
|
2104
|
+
// turn-boundary write doesn't re-fold a stale baseline.
|
|
2105
|
+
if (this.activeRunSession?.state.sessionId === sessionId) {
|
|
2106
|
+
this.activeRunSession.state.tokenUsage = { ...zero };
|
|
2107
|
+
}
|
|
2108
|
+
// Persist to disk so a reload / next run picks up the reset.
|
|
2109
|
+
if (this.sessionManager.exists(sessionId)) {
|
|
2110
|
+
try {
|
|
2111
|
+
const bundle = this.sessionManager.resume(sessionId);
|
|
2112
|
+
bundle.state.tokenUsage = { ...zero };
|
|
2113
|
+
this.sessionManager.saveState(bundle.state);
|
|
2114
|
+
}
|
|
2115
|
+
catch {
|
|
2116
|
+
// Session not resumable (never persisted yet) — the in-memory reset
|
|
2117
|
+
// above covers the live case; nothing else to do.
|
|
2118
|
+
}
|
|
2119
|
+
}
|
|
2120
|
+
}
|
|
1626
2121
|
/**
|
|
1627
2122
|
* Write the active model selection to ~/.code-shell/settings.json.
|
|
1628
2123
|
*
|
|
1629
|
-
*
|
|
1630
|
-
*
|
|
1631
|
-
*
|
|
1632
|
-
*
|
|
1633
|
-
* undefined when the entry resolves via providerCatalog.
|
|
2124
|
+
* Persists settings.defaults.text = entry.key (the connection id == pool
|
|
2125
|
+
* key). That is the single field the boot path reads to restore the active
|
|
2126
|
+
* text model on the next startup (see ctor: settings.defaults.text → pool
|
|
2127
|
+
* switch). No legacy activeKey/model.* mirror is written.
|
|
1634
2128
|
*/
|
|
1635
|
-
persistActiveModel(entry
|
|
2129
|
+
persistActiveModel(entry) {
|
|
1636
2130
|
try {
|
|
1637
2131
|
// userHome() (not raw homedir()) so a test that sets process.env.HOME to
|
|
1638
2132
|
// a tmpdir gets its writes isolated too — the SettingsManager reader
|
|
@@ -1651,29 +2145,30 @@ export class Engine {
|
|
|
1651
2145
|
return;
|
|
1652
2146
|
}
|
|
1653
2147
|
}
|
|
1654
|
-
const
|
|
1655
|
-
? existing.
|
|
2148
|
+
const prevDefaults = typeof existing.defaults === "object" && existing.defaults
|
|
2149
|
+
? existing.defaults
|
|
1656
2150
|
: {};
|
|
1657
2151
|
const updated = {
|
|
1658
2152
|
...existing,
|
|
1659
|
-
|
|
1660
|
-
model: {
|
|
1661
|
-
...prevModel,
|
|
1662
|
-
provider: llm.provider,
|
|
1663
|
-
name: entry.model,
|
|
1664
|
-
apiKey: llm.apiKey,
|
|
1665
|
-
baseUrl: llm.baseUrl,
|
|
1666
|
-
},
|
|
2153
|
+
defaults: { ...prevDefaults, text: entry.key },
|
|
1667
2154
|
};
|
|
2155
|
+
// mode 0o600: this writes model.apiKey (plaintext) into settings.json, so
|
|
2156
|
+
// it must be owner-only — same R-1 hardening as SettingsManager/onboarding.
|
|
2157
|
+
// (Third settings.json writer; the R-1 sweep initially missed this one.)
|
|
1668
2158
|
const tmp = `${file}.${process.pid}.tmp`;
|
|
1669
2159
|
const payload = JSON.stringify(updated, null, 2) + "\n";
|
|
1670
|
-
writeFileSync(tmp, payload, "utf-8");
|
|
2160
|
+
writeFileSync(tmp, payload, { encoding: "utf-8", mode: 0o600 });
|
|
1671
2161
|
try {
|
|
1672
2162
|
renameSync(tmp, file);
|
|
1673
2163
|
}
|
|
1674
2164
|
catch {
|
|
1675
|
-
writeFileSync(file, payload, "utf-8");
|
|
2165
|
+
writeFileSync(file, payload, { encoding: "utf-8", mode: 0o600 });
|
|
1676
2166
|
}
|
|
2167
|
+
// mode arg only applies on create; tighten an already-existing file too.
|
|
2168
|
+
try {
|
|
2169
|
+
chmodSync(file, 0o600);
|
|
2170
|
+
}
|
|
2171
|
+
catch { /* best-effort */ }
|
|
1677
2172
|
}
|
|
1678
2173
|
catch (err) {
|
|
1679
2174
|
logger.warn(`persistActiveModel failed: ${err.message}`);
|
|
@@ -1708,12 +2203,10 @@ export class Engine {
|
|
|
1708
2203
|
* payloads are dropped so out-of-order reload deliveries can't let an older
|
|
1709
2204
|
* config clobber a newer one (Q5).
|
|
1710
2205
|
*
|
|
1711
|
-
* MCP:
|
|
1712
|
-
*
|
|
1713
|
-
*
|
|
1714
|
-
*
|
|
1715
|
-
* new servers will be connected on the next run via the existing per-run
|
|
1716
|
-
* connectAll path — so we skip the connect here.
|
|
2206
|
+
* MCP: reconciles the shared MCP pool against the new disk-default server
|
|
2207
|
+
* set. Added servers connect idempotently; removed/disabled servers are
|
|
2208
|
+
* disconnected and their registered MCP tools are unregistered so plugin
|
|
2209
|
+
* disable takes effect without an Electron restart.
|
|
1717
2210
|
*
|
|
1718
2211
|
* Preset (#2): a preset hot-reload re-resolves `this.preset` so the next-turn
|
|
1719
2212
|
* PromptComposer picks up the new preset's system prompt / behavior — that's
|
|
@@ -1760,16 +2253,17 @@ export class Engine {
|
|
|
1760
2253
|
}
|
|
1761
2254
|
this.reloadHooks();
|
|
1762
2255
|
if (patch.mcpServers && this.mcpManager) {
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
2256
|
+
// Fire-and-forget reconcile (connect added / disconnect removed servers).
|
|
2257
|
+
// It must NOT surface as an unhandled rejection: a single flaky server
|
|
2258
|
+
// that fails to connect/disconnect during hot-reload would otherwise
|
|
2259
|
+
// crash the host process (or be silently swallowed). Catch + log so the
|
|
2260
|
+
// reconcile is best-effort and the next reload can retry.
|
|
2261
|
+
void this.mcpManager.reconcile(patch.mcpServers, this).catch((err) => {
|
|
2262
|
+
logger.error("engine.mcp_reconcile_failed", {
|
|
2263
|
+
error: err instanceof Error ? err.message : String(err),
|
|
2264
|
+
version,
|
|
2265
|
+
});
|
|
2266
|
+
});
|
|
1773
2267
|
}
|
|
1774
2268
|
this.lastAppliedConfigVersion = version;
|
|
1775
2269
|
}
|
|
@@ -1782,6 +2276,55 @@ export class Engine {
|
|
|
1782
2276
|
* engine.run() call for this session picks up the injected content
|
|
1783
2277
|
* instead of a stale snapshot from the previous run.
|
|
1784
2278
|
*/
|
|
2279
|
+
/**
|
|
2280
|
+
* Read a session's persisted active goal WITHOUT resuming it (cheap — reads
|
|
2281
|
+
* only state.json via SessionManager.readActiveGoal). The desktop host calls
|
|
2282
|
+
* this on session load to re-surface the goal block + its Cancel button: a
|
|
2283
|
+
* persistent goal lives only in state.activeGoal and is never replayed from
|
|
2284
|
+
* the transcript, so after a reload of an aborted goal run the UI would
|
|
2285
|
+
* otherwise show nothing (the "goal 还在但页面不显示、取消不了" bug). Returns
|
|
2286
|
+
* undefined when the session is unknown or has no active goal.
|
|
2287
|
+
*/
|
|
2288
|
+
getGoal(sessionId) {
|
|
2289
|
+
return this.sessionManager.readActiveGoal(sessionId);
|
|
2290
|
+
}
|
|
2291
|
+
/**
|
|
2292
|
+
* Clear a session's persisted active goal (CC `/goal clear`). Works whether
|
|
2293
|
+
* the session is idle or its goal run is in flight: it wipes
|
|
2294
|
+
* `state.activeGoal` (so the next bare send won't re-inherit it) and, if a
|
|
2295
|
+
* goal hook is currently registered for this engine, unregisters it so an
|
|
2296
|
+
* in-flight run can stop instead of being re-blocked by the now-cleared goal.
|
|
2297
|
+
* Returns true if a goal was actually cleared. Idempotent — clearing a
|
|
2298
|
+
* session with no active goal is a no-op returning false.
|
|
2299
|
+
*/
|
|
2300
|
+
clearGoal(sessionId) {
|
|
2301
|
+
if (!this.sessionManager.exists(sessionId))
|
|
2302
|
+
return false;
|
|
2303
|
+
// Prefer the LIVE run's bundle when it's this session: clearing its
|
|
2304
|
+
// in-RAM state.activeGoal is what stops the run loop from writing the goal
|
|
2305
|
+
// back on its next saveState. A fresh resume() copy would be cleared and
|
|
2306
|
+
// persisted, but the running loop's own detached bundle still holds the
|
|
2307
|
+
// goal and resurrects it — the stale-write-back race. Falls back to a
|
|
2308
|
+
// resumed copy when no run of this session is currently in flight.
|
|
2309
|
+
const live = this.activeRunSession && this.activeRunSession.state.sessionId === sessionId
|
|
2310
|
+
? this.activeRunSession
|
|
2311
|
+
: null;
|
|
2312
|
+
const session = live ?? this.sessionManager.resume(sessionId);
|
|
2313
|
+
const had = session.state.activeGoal !== undefined;
|
|
2314
|
+
if (had) {
|
|
2315
|
+
session.state.activeGoal = undefined;
|
|
2316
|
+
this.sessionManager.saveState(session.state);
|
|
2317
|
+
}
|
|
2318
|
+
// If THIS session's goal run is in flight, drop its stop hook so the
|
|
2319
|
+
// current run can terminate (the closure-held goal would otherwise keep
|
|
2320
|
+
// re-blocking). The run's own `finally` also unregisters; double-unregister
|
|
2321
|
+
// is safe (set delete is idempotent).
|
|
2322
|
+
if (this.activeGoalHook && this.lastSessionId === sessionId) {
|
|
2323
|
+
this.hooks.unregister("on_stop", this.activeGoalHook);
|
|
2324
|
+
this.activeGoalHook = null;
|
|
2325
|
+
}
|
|
2326
|
+
return had;
|
|
2327
|
+
}
|
|
1785
2328
|
injectContext(sessionId, content) {
|
|
1786
2329
|
const session = this.sessionManager.resume(sessionId);
|
|
1787
2330
|
session.transcript.appendMessage("assistant", content);
|
|
@@ -1823,7 +2366,7 @@ export class Engine {
|
|
|
1823
2366
|
}
|
|
1824
2367
|
getSettingsManager() {
|
|
1825
2368
|
if (!this.settingsManager) {
|
|
1826
|
-
this.settingsManager = new SettingsManager(this.config.cwd, this.config.settingsScope ?? "project");
|
|
2369
|
+
this.settingsManager = new SettingsManager(this.config.cwd, this.config.settingsScope ?? "project", this.config.projectTrusted !== false);
|
|
1827
2370
|
}
|
|
1828
2371
|
return this.settingsManager;
|
|
1829
2372
|
}
|
|
@@ -1875,7 +2418,7 @@ export class Engine {
|
|
|
1875
2418
|
rules.push({ tool: "Bash", decision: "allow" });
|
|
1876
2419
|
}
|
|
1877
2420
|
try {
|
|
1878
|
-
const settingsManager = new SettingsManager(cwd, this.config.settingsScope ?? "project");
|
|
2421
|
+
const settingsManager = new SettingsManager(cwd, this.config.settingsScope ?? "project", this.config.projectTrusted !== false);
|
|
1879
2422
|
const settings = settingsManager.get();
|
|
1880
2423
|
if (settings.permissions?.rules?.length) {
|
|
1881
2424
|
rules.unshift(...settings.permissions.rules);
|
|
@@ -1930,6 +2473,26 @@ export class Engine {
|
|
|
1930
2473
|
getPermissionMode() {
|
|
1931
2474
|
return this.config.permissionMode ?? "acceptEdits";
|
|
1932
2475
|
}
|
|
2476
|
+
/**
|
|
2477
|
+
* Extend the in-flight run's turn ceiling and/or goal budgets (TODO 3.1 —
|
|
2478
|
+
* 运行中续轮/加预算). No-op (returns null) when no run is active. Lets a user
|
|
2479
|
+
* keep an unattended goal going past its original cap instead of restarting.
|
|
2480
|
+
*/
|
|
2481
|
+
extendGoalRun(opts) {
|
|
2482
|
+
if (!this.activeTurnLoop)
|
|
2483
|
+
return null;
|
|
2484
|
+
return this.activeTurnLoop.extend(opts);
|
|
2485
|
+
}
|
|
2486
|
+
/**
|
|
2487
|
+
* The effective permission rules for the current mode + cwd (TODO 5.1) —
|
|
2488
|
+
* preset defaults + mode-derived + settings.permissions.rules, in the same
|
|
2489
|
+
* order the classifier evaluates them. Exposed read-only so `/permissions`
|
|
2490
|
+
* (and any UI) can list what's actually in force. Pure read; builds the same
|
|
2491
|
+
* rule set buildPermissionConfig does, without constructing a backend.
|
|
2492
|
+
*/
|
|
2493
|
+
getPermissionRules() {
|
|
2494
|
+
return this.buildPermissionConfig(this.getPermissionMode(), this.config.cwd ?? process.cwd()).rules;
|
|
2495
|
+
}
|
|
1933
2496
|
/**
|
|
1934
2497
|
* Toggle plan mode directly. Called by the Plan tool (Task 7) via ToolContext.engine.
|
|
1935
2498
|
* Also syncs permissionMode to keep both fields consistent.
|
|
@@ -1983,6 +2546,30 @@ export class Engine {
|
|
|
1983
2546
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
1984
2547
|
});
|
|
1985
2548
|
}
|
|
2549
|
+
/**
|
|
2550
|
+
* Like waitForBackgroundAgentChange but with no abort signal and a hard
|
|
2551
|
+
* timeout. Resolves `true` on a registry/queue change, `false` if `timeoutMs`
|
|
2552
|
+
* elapses first. Used only by the headless abort-drain cleanup, where we want
|
|
2553
|
+
* to catch a completing agent's just-about-to-enqueue notification without
|
|
2554
|
+
* risking a permanent hang on an agent that never completes.
|
|
2555
|
+
*/
|
|
2556
|
+
waitForBackgroundAgentChangeOrTimeout(_sessionId, timeoutMs) {
|
|
2557
|
+
return new Promise((resolve) => {
|
|
2558
|
+
let settled = false;
|
|
2559
|
+
const finish = (changed) => {
|
|
2560
|
+
if (settled)
|
|
2561
|
+
return;
|
|
2562
|
+
settled = true;
|
|
2563
|
+
clearTimeout(timer);
|
|
2564
|
+
unsubRegistry();
|
|
2565
|
+
unsubQueue();
|
|
2566
|
+
resolve(changed);
|
|
2567
|
+
};
|
|
2568
|
+
const timer = setTimeout(() => finish(false), timeoutMs);
|
|
2569
|
+
const unsubRegistry = asyncAgentRegistry.subscribe(() => finish(true));
|
|
2570
|
+
const unsubQueue = notificationQueue.subscribe(() => finish(true));
|
|
2571
|
+
});
|
|
2572
|
+
}
|
|
1986
2573
|
/**
|
|
1987
2574
|
* Sub-agent role registry for the given cwd, memoized per-cwd so the
|
|
1988
2575
|
* directory is read once rather than every turn. A new cwd (e.g. via
|
|
@@ -2053,7 +2640,7 @@ export class Engine {
|
|
|
2053
2640
|
* by tests that want a ToolContext without a full run() cycle.
|
|
2054
2641
|
*/
|
|
2055
2642
|
resolveSandboxWithoutRuntime(config, cwd) {
|
|
2056
|
-
const key =
|
|
2643
|
+
const key = sandboxCacheKey(config, cwd);
|
|
2057
2644
|
let cached = this.sandboxCache.get(key);
|
|
2058
2645
|
if (!cached) {
|
|
2059
2646
|
cached = resolveSandboxBackend(config, cwd);
|
|
@@ -2068,20 +2655,129 @@ export class Engine {
|
|
|
2068
2655
|
}
|
|
2069
2656
|
return cached;
|
|
2070
2657
|
}
|
|
2658
|
+
/**
|
|
2659
|
+
* Build the shell env layered onto the Bash tool / background shells (see
|
|
2660
|
+
* mergeShellEnv). Three user-configured sources, merged lowest → highest:
|
|
2661
|
+
*
|
|
2662
|
+
* 1. project `localEnvironment.env` — the per-project "local environment"
|
|
2663
|
+
* panel (DATABASE_URL etc.); the floor, so a project's own panel values
|
|
2664
|
+
* can be overridden by an explicit top-level `env`.
|
|
2665
|
+
* 2. global top-level `env` — ~/.code-shell/settings.json; the
|
|
2666
|
+
* canonical home for API keys (OPENAI_API_KEY) a skill script reads —
|
|
2667
|
+
* configure once, every project's skills get it.
|
|
2668
|
+
* 3. project top-level `env` — .code-shell/settings.json; a project
|
|
2669
|
+
* that wants to override a global key wins.
|
|
2670
|
+
*
|
|
2671
|
+
* Each scope is read UNMERGED so the layering here is the single source of
|
|
2672
|
+
* precedence (getForScope merges nothing). Returns undefined when no layer
|
|
2673
|
+
* contributes a key, so the caller passes it through unchanged for projects
|
|
2674
|
+
* that configure none.
|
|
2675
|
+
*
|
|
2676
|
+
* Sub-agents: a sub-agent is the user's OWN agent doing the user's work
|
|
2677
|
+
* (mirrors Claude Code, where sub-agents inherit the parent environment), so
|
|
2678
|
+
* it now reads the SAME env as the parent. The sub-agent branch is kept as an
|
|
2679
|
+
* explicit seam (`filterSubagentEnv`) rather than removed — a future policy
|
|
2680
|
+
* could narrow what a sub-agent sees (e.g. drop credential secrets) by
|
|
2681
|
+
* changing that one hook; today it passes everything through unchanged.
|
|
2682
|
+
* A no-cwd context still gets nothing (there is genuinely no project to read).
|
|
2683
|
+
*
|
|
2684
|
+
* None of these is filtered through the deny regex (mergeShellEnv): the user
|
|
2685
|
+
* put them there deliberately. The allowlist/deny machinery only guards the
|
|
2686
|
+
* host's process.env from a tainted model exfiltrating it via `env | curl`.
|
|
2687
|
+
*/
|
|
2688
|
+
readShellEnv(cwd) {
|
|
2689
|
+
if (!cwd)
|
|
2690
|
+
return undefined;
|
|
2691
|
+
const merged = {};
|
|
2692
|
+
const layer = (env) => {
|
|
2693
|
+
if (!env)
|
|
2694
|
+
return;
|
|
2695
|
+
for (const [k, v] of Object.entries(env)) {
|
|
2696
|
+
if (typeof v === "string")
|
|
2697
|
+
merged[k] = v;
|
|
2698
|
+
}
|
|
2699
|
+
};
|
|
2700
|
+
try {
|
|
2701
|
+
// The fully-merged settings already apply the scope guard (a 'project'
|
|
2702
|
+
// scope never reads the host ~/.code-shell) and the user < project <
|
|
2703
|
+
// local precedence — so the top-level `env` map read from here is global
|
|
2704
|
+
// values overridden by project values, exactly as specified. We layer
|
|
2705
|
+
// localEnvironment.env *under* it as the floor.
|
|
2706
|
+
const settings = this.getSettingsManager().get();
|
|
2707
|
+
layer(settings.localEnvironment?.env); // floor
|
|
2708
|
+
// Credentials flagged "expose as env var" (Credential.exposeAsEnv). This
|
|
2709
|
+
// is the wiring that was missing — the UI/store recorded the flag but no
|
|
2710
|
+
// code ever injected the secret, so `$FIGMA_TOKEN` was always empty.
|
|
2711
|
+
// Scope mirrors settingsScope so a project-scoped engine never surfaces
|
|
2712
|
+
// the host user's credentials (same isolation contract as top-level env).
|
|
2713
|
+
// Placed below settings.env so an explicit `env` entry can still override.
|
|
2714
|
+
const credScope = (this.config.settingsScope ?? "project") === "full" ? "full" : "project";
|
|
2715
|
+
layer(new CredentialStore(cwd).envExposures(credScope));
|
|
2716
|
+
layer(settings.env); // top-level env (global ⊕ project) wins
|
|
2717
|
+
}
|
|
2718
|
+
catch {
|
|
2719
|
+
return undefined;
|
|
2720
|
+
}
|
|
2721
|
+
const result = this.config.isSubAgent === true ? this.filterSubagentEnv(merged) : merged;
|
|
2722
|
+
return Object.keys(result).length > 0 ? result : undefined;
|
|
2723
|
+
}
|
|
2724
|
+
/**
|
|
2725
|
+
* Policy seam for what a sub-agent's shell sees. A sub-agent inherits the
|
|
2726
|
+
* parent environment by default (mirrors Claude Code), so this is the
|
|
2727
|
+
* identity function today. It exists so a future policy can narrow the set
|
|
2728
|
+
* (e.g. strip credential `exposeAsEnv` secrets, or allowlist by name) in ONE
|
|
2729
|
+
* place instead of scattering `isSubAgent` checks through readShellEnv.
|
|
2730
|
+
*/
|
|
2731
|
+
filterSubagentEnv(env) {
|
|
2732
|
+
return env;
|
|
2733
|
+
}
|
|
2734
|
+
/**
|
|
2735
|
+
* Read the project's `localEnvironment.setupScripts` for this cwd (the raw
|
|
2736
|
+
* per-platform map). Used by EnterWorktree to run setup once in a freshly
|
|
2737
|
+
* created worktree. Returns undefined for sub-agents / no cwd (same minimal
|
|
2738
|
+
* surface as readShellEnv). The platform selection + run live in
|
|
2739
|
+
* git/worktree.ts; this only fetches the configured scripts.
|
|
2740
|
+
*/
|
|
2741
|
+
readWorktreeSetupScripts(cwd) {
|
|
2742
|
+
if (this.config.isSubAgent === true || !cwd)
|
|
2743
|
+
return undefined;
|
|
2744
|
+
try {
|
|
2745
|
+
const scoped = this.getSettingsManager().getForScope("project", cwd);
|
|
2746
|
+
return scoped.localEnvironment?.setupScripts;
|
|
2747
|
+
}
|
|
2748
|
+
catch {
|
|
2749
|
+
return undefined;
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2071
2752
|
buildToolContext() {
|
|
2072
2753
|
const { disabledSkills, disabledPlugins } = this.readDisabledLists();
|
|
2073
2754
|
return {
|
|
2755
|
+
shellEnv: this.readShellEnv(this.config.cwd),
|
|
2074
2756
|
cwd: this.config.cwd ?? process.cwd(),
|
|
2075
2757
|
llmConfig: this.config.llm,
|
|
2076
2758
|
modelPool: this.modelPool,
|
|
2077
2759
|
toolRegistry: this.toolRegistry,
|
|
2078
2760
|
askUser: this.config.askUser,
|
|
2761
|
+
browser: this.config.browserBridge,
|
|
2762
|
+
injectCredentialToBrowser: this.config.injectCredentialToBrowser,
|
|
2079
2763
|
isSubAgent: this.config.isSubAgent === true,
|
|
2764
|
+
// Credential tools narrow their disk reads to this scope: a project/
|
|
2765
|
+
// isolated engine (SDK-embedded) must not surface the host user's
|
|
2766
|
+
// ~/.code-shell credentials or credentialUse.autoApprove. "full" (the
|
|
2767
|
+
// host-application default) merges user + project as before.
|
|
2768
|
+
settingsScope: this.config.settingsScope ?? "project",
|
|
2080
2769
|
hooks: this.hooks,
|
|
2081
2770
|
planMode: this.planMode,
|
|
2771
|
+
permissionMode: this.permissionMode,
|
|
2082
2772
|
engine: this,
|
|
2083
2773
|
disabledSkills,
|
|
2084
2774
|
disabledPlugins,
|
|
2775
|
+
skillAllowlist: this.config.skillAllowlist,
|
|
2776
|
+
backgroundShells: backgroundShellManager,
|
|
2777
|
+
// Sub-agents never start background shells (they're short-lived and
|
|
2778
|
+
// their lifecycle ends with the parent turn); unattended automation
|
|
2779
|
+
// opts out via config. Otherwise allowed.
|
|
2780
|
+
allowBackgroundShells: this.config.isSubAgent === true ? false : this.config.allowBackgroundShells !== false,
|
|
2085
2781
|
};
|
|
2086
2782
|
}
|
|
2087
2783
|
/**
|
|
@@ -2097,27 +2793,97 @@ export class Engine {
|
|
|
2097
2793
|
*/
|
|
2098
2794
|
readDisabledLists() {
|
|
2099
2795
|
if (this.config.isSubAgent === true) {
|
|
2100
|
-
return { disabledSkills: [], disabledPlugins: [] };
|
|
2796
|
+
return { disabledSkills: [], disabledPlugins: [], disabledPluginHooks: [] };
|
|
2101
2797
|
}
|
|
2798
|
+
// Shared folding (capability-control/disabled-lists.ts): project
|
|
2799
|
+
// capabilityOverrides over the global baseline + the no-repo whitelist
|
|
2800
|
+
// inversion. Extracted so the MCP merge consumers (engineFactory /
|
|
2801
|
+
// diskDefaultsFrom) fold identically — see that module's doc.
|
|
2802
|
+
return computeEffectiveDisabledLists(this.getSettingsManager(), this.config.cwd);
|
|
2803
|
+
}
|
|
2804
|
+
/**
|
|
2805
|
+
* Public view of the folded disabled lists, for hosts that need the
|
|
2806
|
+
* EFFECTIVE state (e.g. the protocol server's settings hot-reload rebuilds
|
|
2807
|
+
* the plugin-MCP merge per session — a project-level "on" must override the
|
|
2808
|
+
* global disabledPlugins there too).
|
|
2809
|
+
*/
|
|
2810
|
+
getEffectiveDisabledLists() {
|
|
2811
|
+
return this.readDisabledLists();
|
|
2812
|
+
}
|
|
2813
|
+
/**
|
|
2814
|
+
* Public: resolve every known feature flag to its effective boolean (the
|
|
2815
|
+
* settings overlay merged over the compiled-in defaults). Used by the
|
|
2816
|
+
* `config` protocol query so the `/features` command can list flag state.
|
|
2817
|
+
*/
|
|
2818
|
+
getFeatureFlags() {
|
|
2819
|
+
return resolveFeatureFlags(this.readFeatureFlags());
|
|
2820
|
+
}
|
|
2821
|
+
/**
|
|
2822
|
+
* Read the merged `settings.featureFlags` overlay for this cwd. Project
|
|
2823
|
+
* settings override user settings via the normal SettingsManager merge.
|
|
2824
|
+
* Returns undefined (→ all defaults) on any read error or for sub-agents,
|
|
2825
|
+
* so a flag check never throws and a child runs with default behavior.
|
|
2826
|
+
*/
|
|
2827
|
+
readFeatureFlags() {
|
|
2828
|
+
if (this.config.isSubAgent === true)
|
|
2829
|
+
return undefined;
|
|
2102
2830
|
try {
|
|
2103
|
-
const
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2831
|
+
const settings = this.getSettingsManager().get();
|
|
2832
|
+
return settings.featureFlags;
|
|
2833
|
+
}
|
|
2834
|
+
catch {
|
|
2835
|
+
return undefined;
|
|
2836
|
+
}
|
|
2837
|
+
}
|
|
2838
|
+
/**
|
|
2839
|
+
* Read settings.memories ({ maxCount, maxAge, extractionModel, autoExtract }).
|
|
2840
|
+
* Returns undefined on any error or when absent, so the memory pipeline
|
|
2841
|
+
* falls back to its built-in defaults.
|
|
2842
|
+
*/
|
|
2843
|
+
readMemoriesConfig() {
|
|
2844
|
+
try {
|
|
2845
|
+
const settings = this.getSettingsManager().get();
|
|
2846
|
+
return settings.memories;
|
|
2118
2847
|
}
|
|
2119
2848
|
catch {
|
|
2120
|
-
return
|
|
2849
|
+
return undefined;
|
|
2850
|
+
}
|
|
2851
|
+
}
|
|
2852
|
+
/**
|
|
2853
|
+
* LLM client for memory extraction (TODO 8.1). Prefers
|
|
2854
|
+
* settings.memories.extractionModel when it names a valid pool model;
|
|
2855
|
+
* otherwise falls back to the aux client (which itself falls back to the
|
|
2856
|
+
* passed primary). Build failures fall back too — extraction is best-effort.
|
|
2857
|
+
*/
|
|
2858
|
+
async resolveExtractionClient(primaryClient) {
|
|
2859
|
+
const key = this.readMemoriesConfig()?.extractionModel;
|
|
2860
|
+
if (key) {
|
|
2861
|
+
const entry = this.modelPool.get(key);
|
|
2862
|
+
if (entry) {
|
|
2863
|
+
try {
|
|
2864
|
+
return await createLLMClient(this.modelPool.toLLMConfig(entry), this.config.clientDefaults);
|
|
2865
|
+
}
|
|
2866
|
+
catch (err) {
|
|
2867
|
+
logger.warn("engine.extraction_model_build_failed", {
|
|
2868
|
+
extractionModel: key,
|
|
2869
|
+
error: err.message,
|
|
2870
|
+
});
|
|
2871
|
+
}
|
|
2872
|
+
}
|
|
2873
|
+
else {
|
|
2874
|
+
logger.warn("engine.extraction_model_missing", { extractionModel: key });
|
|
2875
|
+
}
|
|
2121
2876
|
}
|
|
2877
|
+
return this.resolveAuxClient(primaryClient);
|
|
2122
2878
|
}
|
|
2123
2879
|
}
|
|
2880
|
+
/**
|
|
2881
|
+
* Builtin tool name → the feature flag that gates its visibility. A tool here
|
|
2882
|
+
* is hidden from the LLM when its flag resolves to false. Tools not listed are
|
|
2883
|
+
* unaffected. Kept beside the engine (not in builtin/index) because the flag
|
|
2884
|
+
* read needs the engine's scoped SettingsManager.
|
|
2885
|
+
*/
|
|
2886
|
+
const TOOL_FEATURE_FLAGS = new Map([
|
|
2887
|
+
["WebSearch", "web_search"],
|
|
2888
|
+
["Bash", "shell_tool"],
|
|
2889
|
+
]);
|