@cjhyy/code-shell-core 0.5.0-rc.2 → 0.6.0-rc.10
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 +86 -0
- package/dist/context/compaction.js +279 -0
- package/dist/context/manager.d.ts +18 -0
- package/dist/context/manager.js +181 -48
- package/dist/context/token-counter.js +13 -0
- 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 +280 -141
- package/dist/engine/engine.js +1193 -245
- 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 +67 -0
- package/dist/engine/model-facade.d.ts +10 -0
- package/dist/engine/model-facade.js +15 -0
- package/dist/engine/patch-orphaned-tools.js +3 -0
- package/dist/engine/query.js +2 -0
- package/dist/engine/resolve-llm-config.d.ts +16 -0
- package/dist/engine/resolve-llm-config.js +44 -0
- package/dist/engine/runtime.d.ts +2 -0
- package/dist/engine/runtime.js +27 -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 +31 -0
- package/dist/engine/session-usage.js +81 -0
- package/dist/engine/steer-queue.d.ts +33 -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 +94 -4
- package/dist/engine/turn-loop.js +432 -54
- 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.d.ts +12 -0
- package/dist/git/utils.js +38 -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 +51 -7
- package/dist/index.js +62 -5
- 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 +5 -1
- 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 +21 -0
- package/dist/llm/model-pool.js +21 -1
- 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 +412 -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 +11 -1
- package/dist/preset/index.js +115 -6
- package/dist/product/types.d.ts +1 -1
- package/dist/prompt/composer.d.ts +32 -0
- package/dist/prompt/composer.js +75 -21
- package/dist/prompt/instruction-scanner.js +5 -3
- package/dist/prompt/section-loader.js +1 -0
- package/dist/prompt/sections/base.md +2 -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 +9 -2
- package/dist/protocol/chat-session-manager.js +38 -0
- package/dist/protocol/chat-session.d.ts +69 -1
- package/dist/protocol/chat-session.js +102 -4
- package/dist/protocol/client.d.ts +31 -0
- package/dist/protocol/client.js +44 -0
- package/dist/protocol/server.d.ts +120 -7
- package/dist/protocol/server.js +753 -51
- package/dist/protocol/transport.js +3 -2
- package/dist/protocol/types.d.ts +60 -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 +509 -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 +98 -39
- package/dist/runtime/spawn-common.d.ts +159 -0
- package/dist/runtime/spawn-common.js +404 -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 +119 -3
- package/dist/session/simple-diff.d.ts +23 -0
- package/dist/session/simple-diff.js +84 -0
- package/dist/session/transcript.d.ts +29 -1
- package/dist/session/transcript.js +56 -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 +101 -64
- 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 +64 -4
- 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 +104 -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 +364 -0
- package/dist/tool-system/builtin/glob.js +0 -7
- package/dist/tool-system/builtin/grep.d.ts +9 -0
- package/dist/tool-system/builtin/grep.js +106 -11
- 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 +10 -2
- package/dist/tool-system/builtin/index.js +251 -27
- 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 +8 -2
- 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 +8 -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 +164 -6
- package/dist/tool-system/executor.d.ts +3 -1
- package/dist/tool-system/executor.js +197 -90
- 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 +203 -20
- package/dist/tool-system/mcp-stdio-diagnostics.d.ts +9 -0
- package/dist/tool-system/mcp-stdio-diagnostics.js +93 -0
- 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 +39 -2
- package/dist/tool-system/permission.js +283 -70
- 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/sandbox/off.js +7 -1
- 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 +252 -7
- 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 +48 -0
- package/dist/utils/exec.js +154 -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/protocol/server.js
CHANGED
|
@@ -15,12 +15,30 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { Methods, ErrorCodes, createResponse, createErrorResponse, createNotification, isRequest, } from "./types.js";
|
|
17
17
|
import { diskDefaultsFrom } from "../engine/engine.js";
|
|
18
|
+
import { isProtectedSettingKey } from "../settings/manager.js";
|
|
18
19
|
import { setInteractiveApprovalFn } from "../tool-system/permission.js";
|
|
19
20
|
import { getArenaStatus } from "../tool-system/builtin/arena.js";
|
|
20
|
-
import { agentNotificationBus } from "../tool-system/builtin/agent-notifications.js";
|
|
21
|
+
import { agentNotificationBus, notificationQueue, buildNotificationMessage, } from "../tool-system/builtin/agent-notifications.js";
|
|
22
|
+
import { backgroundShellManager } from "../runtime/background-shell.js";
|
|
23
|
+
import { backgroundJobRegistry } from "../tool-system/builtin/background-jobs.js";
|
|
24
|
+
import { listBackgroundWorkForUI } from "../tool-system/builtin/background-work.js";
|
|
25
|
+
import { logger } from "../logging/logger.js";
|
|
21
26
|
import { nanoid } from "nanoid";
|
|
22
27
|
import { redactLlmConfig, maskSecretValue } from "./redact.js";
|
|
23
28
|
import { redactSecrets } from "../logging/sanitize-messages.js";
|
|
29
|
+
const COMPACT_STREAM_STRATEGIES = new Set([
|
|
30
|
+
"micro",
|
|
31
|
+
"summary",
|
|
32
|
+
"window",
|
|
33
|
+
"snip",
|
|
34
|
+
"emergency",
|
|
35
|
+
"compacted",
|
|
36
|
+
]);
|
|
37
|
+
function toCompactStreamStrategy(strategy) {
|
|
38
|
+
return COMPACT_STREAM_STRATEGIES.has(strategy)
|
|
39
|
+
? strategy
|
|
40
|
+
: "compacted";
|
|
41
|
+
}
|
|
24
42
|
export class AgentServer {
|
|
25
43
|
chatManager;
|
|
26
44
|
legacyEngine;
|
|
@@ -28,6 +46,8 @@ export class AgentServer {
|
|
|
28
46
|
transport;
|
|
29
47
|
/** Fresh-disk settings reader for config hot-reload; null when not wired. */
|
|
30
48
|
settingsReader;
|
|
49
|
+
/** Disk-only active-goal reader for agent/goalGet on a non-live session. */
|
|
50
|
+
readActiveGoalFromDisk;
|
|
31
51
|
/**
|
|
32
52
|
* Monotonic config-reload version, bumped per reloadSettings request so each
|
|
33
53
|
* Engine.refreshRuntimeConfig can drop out-of-order (stale) deliveries (Q5).
|
|
@@ -64,6 +84,7 @@ export class AgentServer {
|
|
|
64
84
|
this.chatManager = options.chatManager ?? null;
|
|
65
85
|
this.legacyEngine = options.engine ?? null;
|
|
66
86
|
this.settingsReader = options.settingsReader ?? null;
|
|
87
|
+
this.readActiveGoalFromDisk = options.readActiveGoalFromDisk ?? null;
|
|
67
88
|
if (!this.chatManager && !this.legacyEngine) {
|
|
68
89
|
throw new Error("AgentServer: either chatManager or engine must be supplied");
|
|
69
90
|
}
|
|
@@ -93,15 +114,121 @@ export class AgentServer {
|
|
|
93
114
|
// B2.2 — forward background sub-agent completion events through the
|
|
94
115
|
// protocol so Desktop / SDK / remote AgentClients see them too. The
|
|
95
116
|
// bus is fed from `NotificationQueue.enqueue`, so this single
|
|
96
|
-
// subscription covers every enqueue site
|
|
97
|
-
//
|
|
98
|
-
//
|
|
117
|
+
// subscription covers every enqueue site — background sub-agents
|
|
118
|
+
// (agent.ts), background shells (background-shell.ts), and video/job
|
|
119
|
+
// polls (background-jobs.ts). The bus now guarantees a real sessionId
|
|
120
|
+
// (no more legacy-bucket coercion to ""), so we forward whatever sid it
|
|
121
|
+
// hands us.
|
|
99
122
|
this.bgAgentBusUnsubscribe = agentNotificationBus.subscribe((sessionId, event) => {
|
|
100
123
|
this.notify(Methods.StreamEvent, { sessionId, event });
|
|
124
|
+
// Background work that finishes while the session is IDLE (a
|
|
125
|
+
// run_in_background Bash like a download, a background sub-agent, or a
|
|
126
|
+
// video poll — the engine no longer parks on any of them) would otherwise
|
|
127
|
+
// leave its completion sitting in the queue until the user manually sends.
|
|
128
|
+
// Wake the session with one run carrying the notification so the model
|
|
129
|
+
// reads "download complete" and continues on its own (the persisted goal
|
|
130
|
+
// is judged that turn). If the work finishes while a run is still in
|
|
131
|
+
// flight, the idle guard below skips it and the run-boundary re-check
|
|
132
|
+
// (trigger B) drains it at end-of-turn instead. A never-exiting dev server
|
|
133
|
+
// emits no completion, so it never wakes anything (no task/service
|
|
134
|
+
// classification needed).
|
|
135
|
+
this.maybeWakeIdleSession(sessionId);
|
|
101
136
|
});
|
|
102
137
|
// Notify client we're ready
|
|
103
138
|
this.notify(Methods.Status, { status: "ready" });
|
|
104
139
|
}
|
|
140
|
+
/**
|
|
141
|
+
* Wake an IDLE chatManager session that has pending background-completion
|
|
142
|
+
* notifications, by enqueueing one turn whose task is the drained
|
|
143
|
+
* notification(s). The model then sees the completion and continues; the
|
|
144
|
+
* session's persisted goal (if any) is judged on that turn.
|
|
145
|
+
*
|
|
146
|
+
* Guards:
|
|
147
|
+
* - chatManager path only (the legacy single-engine / headless path drives
|
|
148
|
+
* its own loop and has no idle-session-resume concept).
|
|
149
|
+
* - Session must exist AND be idle. If it's busy, we do nothing: the in-flight
|
|
150
|
+
* run's end-of-turn `drainAll` already collects every pending notification,
|
|
151
|
+
* so a second run would be redundant — and `enqueueTurn` while busy would
|
|
152
|
+
* queue a spurious extra turn.
|
|
153
|
+
* - We `drainAll` exactly here and feed the items into the woken turn. This
|
|
154
|
+
* also merges a burst of near-simultaneous completions into one wakeup:
|
|
155
|
+
* the first drains all currently-pending items; subsequent bus events for
|
|
156
|
+
* the same session find it busy (or find an empty queue) and no-op.
|
|
157
|
+
*
|
|
158
|
+
* INVARIANT (the burst-merge correctness depends on it): the merge only
|
|
159
|
+
* holds because (a) the bus fans out to subscribers SYNCHRONOUSLY, and (b)
|
|
160
|
+
* `enqueueTurn` sets the session's `active` (→ isBusy()===true) SYNCHRONOUSLY
|
|
161
|
+
* before its first await. So when bus events #2..N arrive — still on the same
|
|
162
|
+
* synchronous fan-out as #1 — they already see the session as busy and no-op.
|
|
163
|
+
* If either path is ever made async (e.g. an await is added inside
|
|
164
|
+
* enqueueTurn before `active` is set, or the bus starts deferring delivery),
|
|
165
|
+
* this degrades into N concurrent wakeups for N completions. Keep both
|
|
166
|
+
* synchronous, or replace this comment's assumption with an explicit
|
|
167
|
+
* "wakeup in flight" guard flag.
|
|
168
|
+
*/
|
|
169
|
+
maybeWakeIdleSession(sessionId) {
|
|
170
|
+
if (!this.chatManager)
|
|
171
|
+
return;
|
|
172
|
+
const session = this.chatManager.get(sessionId);
|
|
173
|
+
if (!session || session.isBusy())
|
|
174
|
+
return;
|
|
175
|
+
// Headless / automation runs are one-shot: the caller takes result.text and
|
|
176
|
+
// is gone, so there's no consumer for a woken continuation turn. Headless
|
|
177
|
+
// already drained its background sub-agents inside engine.run before
|
|
178
|
+
// returning; any remaining queued notification (video/shell) must NOT spin
|
|
179
|
+
// an orphan turn. Only the interactive path auto-continues.
|
|
180
|
+
if (session.engine.isHeadless())
|
|
181
|
+
return;
|
|
182
|
+
// Don't resurrect a session the user just Stopped: cancel() leaves it idle
|
|
183
|
+
// (active=null) so isBusy() reads false, but auto-running a fresh turn here
|
|
184
|
+
// would defeat the Stop. The flag clears the moment the user sends again.
|
|
185
|
+
if (session.wasCancelledSinceLastTurn())
|
|
186
|
+
return;
|
|
187
|
+
const pending = notificationQueue.drainAll(sessionId);
|
|
188
|
+
if (pending.length === 0)
|
|
189
|
+
return;
|
|
190
|
+
const task = `<system-reminder>\n${buildNotificationMessage(pending)}\n</system-reminder>`;
|
|
191
|
+
void session
|
|
192
|
+
.enqueueTurn(task, {
|
|
193
|
+
// Synthetic notification, not the user's own input: persisted with an
|
|
194
|
+
// `injected` flag so a disk rebuild doesn't render it as a phantom user
|
|
195
|
+
// bubble (the live UI shows only the woken assistant's reply).
|
|
196
|
+
injected: true,
|
|
197
|
+
onStream: (event) => this.notify(Methods.StreamEvent, { sessionId, event }),
|
|
198
|
+
})
|
|
199
|
+
.catch((err) => {
|
|
200
|
+
// A wakeup turn failing must not crash the bus fan-out. The drained
|
|
201
|
+
// notifications are already in the transcript via the run's messages;
|
|
202
|
+
// log and move on.
|
|
203
|
+
logger.warn("bg_wakeup.turn_failed", {
|
|
204
|
+
sessionId,
|
|
205
|
+
error: err.message,
|
|
206
|
+
});
|
|
207
|
+
// Belt-and-braces, mirroring the send() path's run().then(clear-busy):
|
|
208
|
+
// the renderer set the composer "working" spinner on this run's
|
|
209
|
+
// session_started, and clears it on turn_complete/error. A failure
|
|
210
|
+
// BEFORE the turn-loop runs (e.g. a setup error) emits neither, which
|
|
211
|
+
// would leave the spinner stuck. Emit a terminal `error` (NOT a
|
|
212
|
+
// turn_complete) so the renderer clears busy AND a woken automation
|
|
213
|
+
// session's runStatus flips to "failed" rather than being mislabeled
|
|
214
|
+
// "completed". If the turn-loop already emitted its own `error`, this is
|
|
215
|
+
// a harmless duplicate (busy already cleared, status already failed).
|
|
216
|
+
this.notify(Methods.StreamEvent, {
|
|
217
|
+
sessionId,
|
|
218
|
+
event: { type: "error", error: err?.message ?? "background wakeup failed" },
|
|
219
|
+
});
|
|
220
|
+
})
|
|
221
|
+
.finally(() => {
|
|
222
|
+
// Run-boundary re-check (trigger B): the woken summarize turn may have
|
|
223
|
+
// spawned NEW background work (e.g. goal "generate 2 videos" → after #1
|
|
224
|
+
// completes, this turn submits #2). When #2 finishes its notification
|
|
225
|
+
// arrives while we're idle again — but if it arrived DURING this turn
|
|
226
|
+
// (busy), trigger A skipped it. Re-checking at the run boundary drains
|
|
227
|
+
// anything that landed while busy, chaining wakeups until the queue is
|
|
228
|
+
// truly empty. Replaces the old engine for(;;) outer loop.
|
|
229
|
+
this.maybeWakeIdleSession(sessionId);
|
|
230
|
+
});
|
|
231
|
+
}
|
|
105
232
|
// ─── Request Dispatch ───────────────────────────────────────────
|
|
106
233
|
async handleRequest(req) {
|
|
107
234
|
switch (req.method) {
|
|
@@ -123,9 +250,30 @@ export class AgentServer {
|
|
|
123
250
|
case Methods.Inject:
|
|
124
251
|
this.handleInject(req);
|
|
125
252
|
break;
|
|
253
|
+
case Methods.Steer:
|
|
254
|
+
this.handleSteer(req);
|
|
255
|
+
break;
|
|
256
|
+
case Methods.Unsteer:
|
|
257
|
+
this.handleUnsteer(req);
|
|
258
|
+
break;
|
|
126
259
|
case Methods.CloseSession:
|
|
127
260
|
this.handleCloseSession(req);
|
|
128
261
|
break;
|
|
262
|
+
case Methods.GoalExtend:
|
|
263
|
+
this.handleGoalExtend(req);
|
|
264
|
+
break;
|
|
265
|
+
case Methods.GoalClear:
|
|
266
|
+
this.handleGoalClear(req);
|
|
267
|
+
break;
|
|
268
|
+
case Methods.GoalGet:
|
|
269
|
+
this.handleGoalGet(req);
|
|
270
|
+
break;
|
|
271
|
+
case Methods.BackgroundShells:
|
|
272
|
+
this.handleBackgroundShells(req);
|
|
273
|
+
break;
|
|
274
|
+
case Methods.BackgroundWork:
|
|
275
|
+
this.handleBackgroundWork(req);
|
|
276
|
+
break;
|
|
129
277
|
default:
|
|
130
278
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.MethodNotFound, `Unknown method: ${req.method}`));
|
|
131
279
|
}
|
|
@@ -155,6 +303,7 @@ export class AgentServer {
|
|
|
155
303
|
session = cm.getOrCreate(params.sessionId, {
|
|
156
304
|
permissionMode: params.permissionMode,
|
|
157
305
|
cwd: params.cwd,
|
|
306
|
+
projectTrusted: params.projectTrusted,
|
|
158
307
|
});
|
|
159
308
|
}
|
|
160
309
|
catch (err) {
|
|
@@ -162,6 +311,28 @@ export class AgentServer {
|
|
|
162
311
|
this.transport.send(createErrorResponse(req.id, code, err.message));
|
|
163
312
|
return;
|
|
164
313
|
}
|
|
314
|
+
// `requireExisting`: reject if the target session isn't on disk rather than
|
|
315
|
+
// running the prompt against a freshly-created blank session. A cron
|
|
316
|
+
// "continue this conversation" job whose session the user deleted must fail
|
|
317
|
+
// loudly here (SessionNotFound) so the scheduler can auto-disable it,
|
|
318
|
+
// instead of silently executing with no transcript/goal/context.
|
|
319
|
+
if (params.requireExisting === true && !session.engine.sessionExistsOnDisk(params.sessionId)) {
|
|
320
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionNotFound, `session ${params.sessionId} does not exist`));
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
if (params.model !== undefined) {
|
|
324
|
+
if (typeof params.model !== "string" || params.model.length === 0) {
|
|
325
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "model must be a non-empty string"));
|
|
326
|
+
return;
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
session.requestModelSwitch(params.model);
|
|
330
|
+
}
|
|
331
|
+
catch (err) {
|
|
332
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, err.message));
|
|
333
|
+
return;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
165
336
|
if (typeof params.planMode === "boolean") {
|
|
166
337
|
session.engine.setPlanMode(params.planMode);
|
|
167
338
|
}
|
|
@@ -176,6 +347,15 @@ export class AgentServer {
|
|
|
176
347
|
// headless engines (no human to answer).
|
|
177
348
|
if (!session.engine.isHeadless()) {
|
|
178
349
|
session.engine.setAskUser((question, opts) => this.requestAskUserForSession(session, sid, question, opts));
|
|
350
|
+
// Browser automation bridge: each method routes a browser action to the
|
|
351
|
+
// client (Electron main drives the webview via CDP) over the SAME
|
|
352
|
+
// request/response channel as askUser (pendingApprovals + requestId).
|
|
353
|
+
// Browser actions still keep their own bounded timeout; AskUserQuestion
|
|
354
|
+
// waits for a real answer or Stop/cancel.
|
|
355
|
+
session.engine.setBrowserBridge(this.makeBrowserBridge(session, sid));
|
|
356
|
+
// Cookie→browser injection (InjectCredential tool): same cross-process
|
|
357
|
+
// channel; main restores the cookie jar into the built-in browser.
|
|
358
|
+
session.engine.setInjectCredential((credentialId) => this.requestCredentialInjectForSession(session, sid, credentialId));
|
|
179
359
|
}
|
|
180
360
|
try {
|
|
181
361
|
const result = await session.enqueueTurn(params.task, {
|
|
@@ -185,6 +365,7 @@ export class AgentServer {
|
|
|
185
365
|
? params.goal
|
|
186
366
|
: undefined,
|
|
187
367
|
onStream: (event) => this.notify(Methods.StreamEvent, { sessionId: sid, event }),
|
|
368
|
+
clientMessageId: typeof params.clientMessageId === "string" ? params.clientMessageId : undefined,
|
|
188
369
|
});
|
|
189
370
|
const runResult = {
|
|
190
371
|
text: result.text,
|
|
@@ -194,9 +375,18 @@ export class AgentServer {
|
|
|
194
375
|
usage: result.usage,
|
|
195
376
|
};
|
|
196
377
|
this.transport.send(createResponse(req.id, runResult));
|
|
378
|
+
// Run-boundary re-check (trigger B): the session is idle now. If a
|
|
379
|
+
// background task (shell/video) completed DURING this run, its bus event
|
|
380
|
+
// fired while we were busy and trigger A skipped it — drain it now and
|
|
381
|
+
// wake a continuation turn. Interactive path only; headless already
|
|
382
|
+
// drained its sub-agents inside engine.run before returning.
|
|
383
|
+
this.maybeWakeIdleSession(sid);
|
|
197
384
|
}
|
|
198
385
|
catch (err) {
|
|
199
386
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
|
|
387
|
+
// Even on a failed run the session goes idle — re-check so a background
|
|
388
|
+
// completion that landed during the failed run isn't orphaned.
|
|
389
|
+
this.maybeWakeIdleSession(sid);
|
|
200
390
|
}
|
|
201
391
|
}
|
|
202
392
|
async handleRunLegacy(req) {
|
|
@@ -224,6 +414,19 @@ export class AgentServer {
|
|
|
224
414
|
// Engine.setPermissionMode now keeps this.permissionMode + this.planMode
|
|
225
415
|
// in sync and tools read them via ToolContext.permissionMode/planMode.
|
|
226
416
|
}
|
|
417
|
+
if (params.model !== undefined) {
|
|
418
|
+
if (typeof params.model !== "string" || params.model.length === 0) {
|
|
419
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "model must be a non-empty string"));
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
try {
|
|
423
|
+
this.legacyEngine.switchModel(params.model);
|
|
424
|
+
}
|
|
425
|
+
catch (err) {
|
|
426
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, err.message));
|
|
427
|
+
return;
|
|
428
|
+
}
|
|
429
|
+
}
|
|
227
430
|
this.running = true;
|
|
228
431
|
this.abortController = new AbortController();
|
|
229
432
|
this.notify(Methods.Status, { status: "running" });
|
|
@@ -242,6 +445,7 @@ export class AgentServer {
|
|
|
242
445
|
sessionId: params.sessionId,
|
|
243
446
|
signal: runController.signal,
|
|
244
447
|
onStream: streamToClient,
|
|
448
|
+
clientMessageId: typeof params.clientMessageId === "string" ? params.clientMessageId : undefined,
|
|
245
449
|
goal: typeof params.goal === "string" ||
|
|
246
450
|
(params.goal != null && typeof params.goal === "object")
|
|
247
451
|
? params.goal
|
|
@@ -289,7 +493,10 @@ export class AgentServer {
|
|
|
289
493
|
// ─── Approve ────────────────────────────────────────────────────
|
|
290
494
|
handleApprove(req) {
|
|
291
495
|
const params = (req.params ?? {});
|
|
292
|
-
// ChatSessionManager path:
|
|
496
|
+
// ChatSessionManager path: approvals are scoped by (sessionId, requestId).
|
|
497
|
+
// Never fall back from a session-tagged response to the legacy global map:
|
|
498
|
+
// a stale/misrouted UI response must fail closed instead of resolving a
|
|
499
|
+
// pending prompt from another session.
|
|
293
500
|
if (this.chatManager && typeof params.sessionId === "string") {
|
|
294
501
|
const s = this.chatManager.get(params.sessionId);
|
|
295
502
|
if (!s) {
|
|
@@ -298,10 +505,14 @@ export class AgentServer {
|
|
|
298
505
|
}
|
|
299
506
|
const resolve = s.pendingApprovals.get(params.requestId);
|
|
300
507
|
if (!resolve) {
|
|
301
|
-
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, `No pending approval: ${params.requestId}`));
|
|
508
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, `No pending approval for session ${params.sessionId}: ${params.requestId}`));
|
|
302
509
|
return;
|
|
303
510
|
}
|
|
304
511
|
s.pendingApprovals.delete(params.requestId);
|
|
512
|
+
// Cancel the pending timeout for requests that arm one (browser actions,
|
|
513
|
+
// credential injection, and tool approvals). AskUserQuestion does not use
|
|
514
|
+
// a timeout; this is harmless for those request ids.
|
|
515
|
+
this.clearApprovalTimer(params.requestId);
|
|
305
516
|
resolve(params.decision);
|
|
306
517
|
this.transport.send(createResponse(req.id, { ok: true }));
|
|
307
518
|
return;
|
|
@@ -332,6 +543,13 @@ export class AgentServer {
|
|
|
332
543
|
return;
|
|
333
544
|
}
|
|
334
545
|
s.cancel();
|
|
546
|
+
// s.cancel() only aborts the engine controller + drains queued turns. The
|
|
547
|
+
// session's pendingApprovals (askUser / browser_action / tool approvals)
|
|
548
|
+
// are NOT driven directly by the abort signal. Left alone, an awaiting
|
|
549
|
+
// AskUserQuestion would now wait forever, while bounded request types
|
|
550
|
+
// would wait until APPROVAL_TIMEOUT_MS. Resolve them as cancelled now and
|
|
551
|
+
// clear any matching timers, mirroring the legacy path below.
|
|
552
|
+
this.cancelSessionApprovals(s);
|
|
335
553
|
this.transport.send(createResponse(req.id, { ok: true }));
|
|
336
554
|
return;
|
|
337
555
|
}
|
|
@@ -348,6 +566,163 @@ export class AgentServer {
|
|
|
348
566
|
this.clearAllApprovalTimers();
|
|
349
567
|
this.transport.send(createResponse(req.id, { ok: true }));
|
|
350
568
|
}
|
|
569
|
+
/**
|
|
570
|
+
* Extend a running goal's turn/budget ceilings mid-run (TODO 3.1). Routed to
|
|
571
|
+
* a specific session (multi-session path). Returns the resulting effective
|
|
572
|
+
* limits, or an error if no session / no active run.
|
|
573
|
+
*/
|
|
574
|
+
handleGoalExtend(req) {
|
|
575
|
+
const params = (req.params ?? {});
|
|
576
|
+
const ext = {
|
|
577
|
+
addTurns: params.addTurns,
|
|
578
|
+
addTokenBudget: params.addTokenBudget,
|
|
579
|
+
addTimeBudgetMs: params.addTimeBudgetMs,
|
|
580
|
+
addStopBlocks: params.addStopBlocks,
|
|
581
|
+
};
|
|
582
|
+
// Multi-session path: route to the named session.
|
|
583
|
+
const session = this.chatManager && typeof params.sessionId === "string"
|
|
584
|
+
? this.chatManager.get(params.sessionId)
|
|
585
|
+
: undefined;
|
|
586
|
+
// Legacy single-engine path (no chatManager): extend the one engine's run,
|
|
587
|
+
// mirroring handleCancel which also supports both paths. Without this a
|
|
588
|
+
// legacy host can cancel but never extend a goal.
|
|
589
|
+
const result = session
|
|
590
|
+
? session.extendGoalRun(ext)
|
|
591
|
+
: this.legacyEngine
|
|
592
|
+
? this.legacyEngine.extendGoalRun(ext)
|
|
593
|
+
: undefined;
|
|
594
|
+
if (result === undefined) {
|
|
595
|
+
// Couldn't even resolve a target (no session and no legacy engine).
|
|
596
|
+
if (!session && !this.legacyEngine) {
|
|
597
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionClosed, params.sessionId ? `No such session: ${params.sessionId}` : "sessionId is required"));
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
if (!result) {
|
|
602
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "No active run to extend"));
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
this.transport.send(createResponse(req.id, { ok: true, limits: result }));
|
|
606
|
+
}
|
|
607
|
+
/**
|
|
608
|
+
* Clear a session's persisted active goal (CC /goal clear). Routes to the
|
|
609
|
+
* named session, falling back to the legacy single engine (mirrors
|
|
610
|
+
* handleGoalExtend / handleCancel's dual path). Returns { ok, cleared }.
|
|
611
|
+
*/
|
|
612
|
+
handleGoalClear(req) {
|
|
613
|
+
const params = (req.params ?? {});
|
|
614
|
+
const session = this.chatManager && typeof params.sessionId === "string"
|
|
615
|
+
? this.chatManager.get(params.sessionId)
|
|
616
|
+
: undefined;
|
|
617
|
+
if (!session && !this.legacyEngine) {
|
|
618
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionClosed, params.sessionId ? `No such session: ${params.sessionId}` : "sessionId is required"));
|
|
619
|
+
return;
|
|
620
|
+
}
|
|
621
|
+
const cleared = session
|
|
622
|
+
? session.clearGoal()
|
|
623
|
+
: params.sessionId
|
|
624
|
+
? (this.legacyEngine.clearGoal(params.sessionId) ?? false)
|
|
625
|
+
: false;
|
|
626
|
+
this.transport.send(createResponse(req.id, { ok: true, cleared }));
|
|
627
|
+
}
|
|
628
|
+
/**
|
|
629
|
+
* Read a session's persisted active goal so the host can re-surface the goal
|
|
630
|
+
* block + Cancel button on session load. A persistent goal lives only in
|
|
631
|
+
* state.activeGoal and is never replayed from the transcript, so a reloaded
|
|
632
|
+
* (or disk-rebuilt) session has no other way to learn it. Prefers a live
|
|
633
|
+
* chatManager session, else falls through to a disk read (readActiveGoalFromDisk
|
|
634
|
+
* in worker mode, or the legacy engine's disk read in single-engine mode) — the
|
|
635
|
+
* bug case is an aborted/reloaded session that is NOT live. Returns { ok, goal } where goal
|
|
636
|
+
* is the objective string, or null when there's no goal / unknown session.
|
|
637
|
+
* Never errors on "no goal" — null is the normal "nothing to show" answer.
|
|
638
|
+
*/
|
|
639
|
+
handleGoalGet(req) {
|
|
640
|
+
const params = (req.params ?? {});
|
|
641
|
+
if (typeof params.sessionId !== "string" || !params.sessionId) {
|
|
642
|
+
// Missing/empty param is InvalidParams, not SessionClosed (no session was
|
|
643
|
+
// ever named) — matches handleSteer/handleUnsteer/handleBackgroundShells.
|
|
644
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "sessionId is required"));
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
// Prefer the live session; otherwise read straight off disk. The goal we
|
|
648
|
+
// want to recover belongs to a session that is, by definition of this bug,
|
|
649
|
+
// usually NOT currently live: reopening after restart doesn't create a
|
|
650
|
+
// ChatSession (that only happens on a send), so chatManager.get() misses.
|
|
651
|
+
// In worker (chatManager) mode there is no legacyEngine, so the disk read
|
|
652
|
+
// MUST come from readActiveGoalFromDisk — relying on legacyEngine there
|
|
653
|
+
// silently returned null and the UI goal block never re-surfaced.
|
|
654
|
+
const live = this.chatManager ? this.chatManager.get(params.sessionId) : undefined;
|
|
655
|
+
const goal = live
|
|
656
|
+
? live.getGoal()
|
|
657
|
+
: (this.readActiveGoalFromDisk?.(params.sessionId) ??
|
|
658
|
+
this.legacyEngine?.getGoal(params.sessionId) ??
|
|
659
|
+
undefined);
|
|
660
|
+
this.transport.send(createResponse(req.id, { ok: true, goal: goal ? goal.objective : null }));
|
|
661
|
+
}
|
|
662
|
+
/**
|
|
663
|
+
* Query/control a session's background shells for the desktop UI panel (TODO
|
|
664
|
+
* 3.2). Reads the singleton BackgroundShellManager directly (shells outlive
|
|
665
|
+
* the run that spawned them, so we don't need a live ChatSession). Actions:
|
|
666
|
+
* - (default) list → the session's shells
|
|
667
|
+
* - output → full retained output of one shell
|
|
668
|
+
* - kill → terminate one shell's process group
|
|
669
|
+
* All scoped by sessionId (manager enforces ownership on output/kill).
|
|
670
|
+
*/
|
|
671
|
+
handleBackgroundShells(req) {
|
|
672
|
+
const params = (req.params ?? {});
|
|
673
|
+
const sessionId = params.sessionId;
|
|
674
|
+
if (typeof sessionId !== "string" || !sessionId) {
|
|
675
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "sessionId is required"));
|
|
676
|
+
return;
|
|
677
|
+
}
|
|
678
|
+
const action = params.action ?? "list";
|
|
679
|
+
if (action === "list") {
|
|
680
|
+
const shells = backgroundShellManager.listForSession(sessionId);
|
|
681
|
+
this.transport.send(createResponse(req.id, { shells }));
|
|
682
|
+
return;
|
|
683
|
+
}
|
|
684
|
+
if (!params.shellId) {
|
|
685
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "shellId is required"));
|
|
686
|
+
return;
|
|
687
|
+
}
|
|
688
|
+
if (action === "output") {
|
|
689
|
+
const res = backgroundShellManager.readOutput(params.shellId, "all", sessionId);
|
|
690
|
+
if (!res.ok) {
|
|
691
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, res.error));
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
this.transport.send(createResponse(req.id, { header: res.header, text: res.text }));
|
|
695
|
+
return;
|
|
696
|
+
}
|
|
697
|
+
if (action === "kill") {
|
|
698
|
+
void backgroundShellManager.kill(params.shellId, sessionId).then((res) => {
|
|
699
|
+
if (!res.ok) {
|
|
700
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, res.error));
|
|
701
|
+
}
|
|
702
|
+
else {
|
|
703
|
+
this.transport.send(createResponse(req.id, { ok: true }));
|
|
704
|
+
}
|
|
705
|
+
});
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, `unknown action: ${action}`));
|
|
709
|
+
}
|
|
710
|
+
/**
|
|
711
|
+
* BackgroundWork — unified, list-only view of a session's background work
|
|
712
|
+
* across all three registries (shells + sub-agents + jobs) for the desktop
|
|
713
|
+
* background panel. Per-shell output/kill still flow through BackgroundShells
|
|
714
|
+
* (by shellId); this just answers "what's running in the background right now".
|
|
715
|
+
*/
|
|
716
|
+
handleBackgroundWork(req) {
|
|
717
|
+
const params = (req.params ?? {});
|
|
718
|
+
const sessionId = params.sessionId;
|
|
719
|
+
if (typeof sessionId !== "string" || !sessionId) {
|
|
720
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "sessionId is required"));
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
const items = listBackgroundWorkForUI(sessionId);
|
|
724
|
+
this.transport.send(createResponse(req.id, { items }));
|
|
725
|
+
}
|
|
351
726
|
// ─── CloseSession ───────────────────────────────────────────────
|
|
352
727
|
handleCloseSession(req) {
|
|
353
728
|
const params = (req.params ?? {});
|
|
@@ -356,8 +731,20 @@ export class AgentServer {
|
|
|
356
731
|
return;
|
|
357
732
|
}
|
|
358
733
|
if (this.chatManager) {
|
|
734
|
+
const session = this.chatManager.get(params.sessionId);
|
|
735
|
+
if (session) {
|
|
736
|
+
this.cancelSessionApprovals(session, "session closed");
|
|
737
|
+
}
|
|
359
738
|
this.chatManager.close(params.sessionId);
|
|
360
739
|
}
|
|
740
|
+
// Explicit session teardown — reap that session's background shells
|
|
741
|
+
// (design §6 "session 被显式删除 → killSession"). This is the RPC path
|
|
742
|
+
// (agent/closeSession from the host on delete), distinct from the idle
|
|
743
|
+
// sweeper's chatManager.close() which must NOT kill (§6). Fire-and-forget.
|
|
744
|
+
void backgroundShellManager.killSession(params.sessionId);
|
|
745
|
+
// Drop this session's retained background jobs too (#2/#5): finished jobs
|
|
746
|
+
// are kept for the panel, so explicit teardown is where they're released.
|
|
747
|
+
backgroundJobRegistry.dropForSession(params.sessionId);
|
|
361
748
|
this.transport.send(createResponse(req.id, { ok: true }));
|
|
362
749
|
}
|
|
363
750
|
// ─── Configure ──────────────────────────────────────────────────
|
|
@@ -368,7 +755,11 @@ export class AgentServer {
|
|
|
368
755
|
const sid = params.sessionId;
|
|
369
756
|
const s = this.chatManager.get(sid);
|
|
370
757
|
if (!s) {
|
|
371
|
-
|
|
758
|
+
// Session not found (already cleaned by idle sweeper, or never created).
|
|
759
|
+
// Don't create one just for configure — let the subsequent run() do it
|
|
760
|
+
// with proper per-session config. Return OK since there's nothing to
|
|
761
|
+
// configure on a non-existent session.
|
|
762
|
+
this.transport.send(createResponse(req.id, { ok: true }));
|
|
372
763
|
return;
|
|
373
764
|
}
|
|
374
765
|
if (typeof params.planMode === "boolean")
|
|
@@ -376,13 +767,32 @@ export class AgentServer {
|
|
|
376
767
|
if (typeof params.permissionMode === "string") {
|
|
377
768
|
s.engine.setPermissionMode(params.permissionMode);
|
|
378
769
|
}
|
|
770
|
+
if (params.clearModels) {
|
|
771
|
+
this.chatManager.runtime.clearModels();
|
|
772
|
+
}
|
|
773
|
+
if (params.reloadModels) {
|
|
774
|
+
try {
|
|
775
|
+
this.chatManager.runtime.reloadModelsFromSettings();
|
|
776
|
+
}
|
|
777
|
+
catch (err) {
|
|
778
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
|
|
779
|
+
return;
|
|
780
|
+
}
|
|
781
|
+
}
|
|
379
782
|
// Per-session model switch — the missing piece that made model changes
|
|
380
783
|
// worker-global (session-isolation research §3). requestModelSwitch
|
|
381
784
|
// applies immediately when idle, defers to the run boundary when busy
|
|
382
785
|
// so it never swaps the model under a running LLM client.
|
|
383
786
|
if (typeof params.model === "string") {
|
|
384
787
|
try {
|
|
385
|
-
s.requestModelSwitch(params.model);
|
|
788
|
+
const entry = s.requestModelSwitch(params.model);
|
|
789
|
+
this.transport.send(createResponse(req.id, {
|
|
790
|
+
ok: true,
|
|
791
|
+
model: entry.model,
|
|
792
|
+
key: entry.key,
|
|
793
|
+
maxContextTokens: entry.maxContextTokens,
|
|
794
|
+
}));
|
|
795
|
+
return;
|
|
386
796
|
}
|
|
387
797
|
catch (err) {
|
|
388
798
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, err.message));
|
|
@@ -397,7 +807,7 @@ export class AgentServer {
|
|
|
397
807
|
}
|
|
398
808
|
const settings = this.settingsReader();
|
|
399
809
|
const version = ++this.configVersion;
|
|
400
|
-
s.engine.refreshRuntimeConfig(diskDefaultsFrom(settings), version);
|
|
810
|
+
s.engine.refreshRuntimeConfig(diskDefaultsFrom(settings, s.engine.getEffectiveDisabledLists().disabledPlugins), version);
|
|
401
811
|
}
|
|
402
812
|
this.transport.send(createResponse(req.id, { ok: true }));
|
|
403
813
|
return;
|
|
@@ -405,9 +815,33 @@ export class AgentServer {
|
|
|
405
815
|
// Global configure — delegate to legacyEngine if available,
|
|
406
816
|
// or to any session's engine from chatManager for settings ops
|
|
407
817
|
const engine = this.legacyEngine ?? this.anyEngine();
|
|
408
|
-
if (params.
|
|
818
|
+
if (params.clearModels) {
|
|
819
|
+
if (this.chatManager) {
|
|
820
|
+
this.chatManager.runtime.clearModels();
|
|
821
|
+
}
|
|
822
|
+
else {
|
|
823
|
+
this.legacyEngine?.getModelPool().clear();
|
|
824
|
+
this.globalQueryEngine?.getModelPool().clear();
|
|
825
|
+
}
|
|
826
|
+
}
|
|
827
|
+
if (params.reloadModels) {
|
|
409
828
|
try {
|
|
410
|
-
|
|
829
|
+
if (this.chatManager) {
|
|
830
|
+
this.chatManager.runtime.reloadModelsFromSettings();
|
|
831
|
+
}
|
|
832
|
+
else {
|
|
833
|
+
const seen = new Set();
|
|
834
|
+
const reload = (target) => {
|
|
835
|
+
if (!target || seen.has(target))
|
|
836
|
+
return;
|
|
837
|
+
seen.add(target);
|
|
838
|
+
target.reloadModelPool();
|
|
839
|
+
};
|
|
840
|
+
reload(this.legacyEngine);
|
|
841
|
+
reload(this.globalQueryEngine);
|
|
842
|
+
if (seen.size === 0)
|
|
843
|
+
reload(engine);
|
|
844
|
+
}
|
|
411
845
|
}
|
|
412
846
|
catch (err) {
|
|
413
847
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
|
|
@@ -417,7 +851,12 @@ export class AgentServer {
|
|
|
417
851
|
if (params.model !== undefined && engine) {
|
|
418
852
|
try {
|
|
419
853
|
const entry = engine.switchModel(params.model);
|
|
420
|
-
this.transport.send(createResponse(req.id, {
|
|
854
|
+
this.transport.send(createResponse(req.id, {
|
|
855
|
+
ok: true,
|
|
856
|
+
model: entry.model,
|
|
857
|
+
key: entry.key,
|
|
858
|
+
maxContextTokens: entry.maxContextTokens,
|
|
859
|
+
}));
|
|
421
860
|
return;
|
|
422
861
|
}
|
|
423
862
|
catch (err) {
|
|
@@ -445,23 +884,29 @@ export class AgentServer {
|
|
|
445
884
|
// Mirrors how reloadModels/model/planMode above act on `engine`.
|
|
446
885
|
if (engine) {
|
|
447
886
|
const settings = this.settingsReader();
|
|
448
|
-
engine.refreshRuntimeConfig(diskDefaultsFrom(settings), ++this.configVersion);
|
|
887
|
+
engine.refreshRuntimeConfig(diskDefaultsFrom(settings, engine.getEffectiveDisabledLists().disabledPlugins), ++this.configVersion);
|
|
449
888
|
}
|
|
450
889
|
this.transport.send(createResponse(req.id, { ok: true }));
|
|
451
890
|
return;
|
|
452
891
|
}
|
|
453
892
|
const settings = this.settingsReader();
|
|
454
|
-
|
|
455
|
-
//
|
|
456
|
-
//
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
893
|
+
// The MCP merge folds each session's PROJECT capabilityOverrides (a
|
|
894
|
+
// project-level "on" must override the global disabledPlugins), so the
|
|
895
|
+
// patch is cwd-dependent — compute it per session, not once.
|
|
896
|
+
const perSession = [];
|
|
897
|
+
this.chatManager.forEachSession((s) => {
|
|
898
|
+
const patch = diskDefaultsFrom(settings, s.engine.getEffectiveDisabledLists().disabledPlugins);
|
|
899
|
+
perSession.push({ apply: (v) => s.engine.refreshRuntimeConfig(patch, v), patch });
|
|
900
|
+
});
|
|
901
|
+
// #6: content short-circuit — now keyed on ALL per-session patches, so a
|
|
902
|
+
// change visible to only one session (e.g. its project's overrides)
|
|
903
|
+
// still propagates while a true no-op skips the reloadHooks churn.
|
|
904
|
+
const patchJson = JSON.stringify(perSession.map((p) => p.patch));
|
|
461
905
|
if (patchJson !== this.lastBroadcastPatch) {
|
|
462
906
|
this.lastBroadcastPatch = patchJson;
|
|
463
907
|
const version = ++this.configVersion;
|
|
464
|
-
|
|
908
|
+
for (const p of perSession)
|
|
909
|
+
p.apply(version);
|
|
465
910
|
}
|
|
466
911
|
}
|
|
467
912
|
this.transport.send(createResponse(req.id, { ok: true }));
|
|
@@ -528,6 +973,11 @@ export class AgentServer {
|
|
|
528
973
|
// apiKey is redacted at this boundary: clients only get
|
|
529
974
|
// hasApiKey + optional apiKeyPreview. See protocol/redact.ts.
|
|
530
975
|
llm: redactLlmConfig(config.llm),
|
|
976
|
+
// Resolved feature flags (defaults merged with settings overlay)
|
|
977
|
+
// so the /features command can list current state.
|
|
978
|
+
featureFlags: engine.getFeatureFlags(),
|
|
979
|
+
// Effective permission rules so /permissions can list them (TODO 5.1).
|
|
980
|
+
permissionRules: engine.getPermissionRules(),
|
|
531
981
|
},
|
|
532
982
|
}));
|
|
533
983
|
break;
|
|
@@ -556,12 +1006,53 @@ export class AgentServer {
|
|
|
556
1006
|
break;
|
|
557
1007
|
}
|
|
558
1008
|
case "compact": {
|
|
559
|
-
|
|
1009
|
+
const compactSessionId = typeof params.sessionId === "string" && params.sessionId.length > 0
|
|
1010
|
+
? params.sessionId
|
|
1011
|
+
: undefined;
|
|
1012
|
+
let compactEngine = engine;
|
|
1013
|
+
if (this.chatManager) {
|
|
1014
|
+
if (compactSessionId) {
|
|
1015
|
+
let session = this.chatManager.get(compactSessionId);
|
|
1016
|
+
if (!session) {
|
|
1017
|
+
const probeEngine = this.anyEngine();
|
|
1018
|
+
if (!probeEngine?.sessionExistsOnDisk(compactSessionId)) {
|
|
1019
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionNotFound, `Session not found: ${compactSessionId}`));
|
|
1020
|
+
return;
|
|
1021
|
+
}
|
|
1022
|
+
try {
|
|
1023
|
+
session = this.chatManager.getOrCreate(compactSessionId, {
|
|
1024
|
+
cwd: probeEngine.getSessionManager().readCwd(compactSessionId),
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
catch (err) {
|
|
1028
|
+
this.transport.send(createErrorResponse(req.id, err.code ?? ErrorCodes.InternalError, err.message));
|
|
1029
|
+
return;
|
|
1030
|
+
}
|
|
1031
|
+
}
|
|
1032
|
+
compactEngine = session.engine;
|
|
1033
|
+
}
|
|
1034
|
+
else {
|
|
1035
|
+
compactEngine = this.anyEngine();
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
if (!compactEngine) {
|
|
560
1039
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, "No engine available for compact query"));
|
|
561
1040
|
return;
|
|
562
1041
|
}
|
|
563
1042
|
try {
|
|
564
|
-
const result =
|
|
1043
|
+
const result = await compactEngine.forceCompact(compactSessionId);
|
|
1044
|
+
if (result.before > result.after) {
|
|
1045
|
+
const event = {
|
|
1046
|
+
type: "context_compact",
|
|
1047
|
+
strategy: toCompactStreamStrategy(result.strategy),
|
|
1048
|
+
before: result.before,
|
|
1049
|
+
after: result.after,
|
|
1050
|
+
};
|
|
1051
|
+
this.notify(Methods.StreamEvent, {
|
|
1052
|
+
sessionId: compactSessionId ?? "",
|
|
1053
|
+
event,
|
|
1054
|
+
});
|
|
1055
|
+
}
|
|
565
1056
|
this.transport.send(createResponse(req.id, {
|
|
566
1057
|
type: "compact",
|
|
567
1058
|
data: result,
|
|
@@ -654,6 +1145,15 @@ export class AgentServer {
|
|
|
654
1145
|
const { key, value } = params;
|
|
655
1146
|
if (!key)
|
|
656
1147
|
throw new Error("key is required for config_set");
|
|
1148
|
+
// Refuse writes to trust-root fields via the generic config path. A
|
|
1149
|
+
// protocol peer (external driver, paired phone, compromised renderer)
|
|
1150
|
+
// could otherwise self-authorize tools or inject env/hooks/MCP servers,
|
|
1151
|
+
// which is equivalent to remotely bypassing workspace trust. Legit
|
|
1152
|
+
// writes to these go through the dedicated settings UI, not config_set;
|
|
1153
|
+
// provider/model edits use provider_add / models handlers below.
|
|
1154
|
+
if (isProtectedSettingKey(key)) {
|
|
1155
|
+
throw new Error(`config_set refused: "${key}" is a protected trust/permission field and cannot be written through this path.`);
|
|
1156
|
+
}
|
|
657
1157
|
engine.updateConfig(key, value);
|
|
658
1158
|
// Echo back through the same secret-aware masker as config_get so
|
|
659
1159
|
// a `config_set("llm.apiKey", "...")` confirmation doesn't ship
|
|
@@ -861,6 +1361,55 @@ export class AgentServer {
|
|
|
861
1361
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
|
|
862
1362
|
}
|
|
863
1363
|
}
|
|
1364
|
+
// ─── Steer ──────────────────────────────────────────────────────
|
|
1365
|
+
// Queue a user message into the in-flight run's turn loop (不打断). Unlike
|
|
1366
|
+
// Inject (appends an assistant context msg for the NEXT run), Steer feeds a
|
|
1367
|
+
// user message to the CURRENT run's next step. No abort, no LLM trigger by
|
|
1368
|
+
// itself — the running loop picks it up at its next step boundary.
|
|
1369
|
+
handleSteer(req) {
|
|
1370
|
+
const params = (req.params ?? {});
|
|
1371
|
+
if (!params.text || !params.sessionId) {
|
|
1372
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "text and sessionId required"));
|
|
1373
|
+
return;
|
|
1374
|
+
}
|
|
1375
|
+
const engine = this.chatManager
|
|
1376
|
+
? this.chatManager.get(params.sessionId)?.engine
|
|
1377
|
+
: this.legacyEngine;
|
|
1378
|
+
if (!engine) {
|
|
1379
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionClosed, `No such session: ${params.sessionId}`));
|
|
1380
|
+
return;
|
|
1381
|
+
}
|
|
1382
|
+
try {
|
|
1383
|
+
const result = engine.enqueueSteer(params.sessionId, params.text, params.id, params.clientMessageId);
|
|
1384
|
+
this.transport.send(createResponse(req.id, { ok: true, ...result }));
|
|
1385
|
+
}
|
|
1386
|
+
catch (err) {
|
|
1387
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
handleUnsteer(req) {
|
|
1391
|
+
const params = (req.params ?? {});
|
|
1392
|
+
if (!params.id || !params.sessionId) {
|
|
1393
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "id and sessionId required"));
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
const engine = this.chatManager
|
|
1397
|
+
? this.chatManager.get(params.sessionId)?.engine
|
|
1398
|
+
: this.legacyEngine;
|
|
1399
|
+
if (!engine) {
|
|
1400
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionClosed, `No such session: ${params.sessionId}`));
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
try {
|
|
1404
|
+
// removed=false means the loop already consumed it — not an error, the
|
|
1405
|
+
// host just learns it couldn't be revoked (it will arrive as a bubble).
|
|
1406
|
+
const removed = engine.unsteer(params.sessionId, params.id);
|
|
1407
|
+
this.transport.send(createResponse(req.id, { ok: true, removed }));
|
|
1408
|
+
}
|
|
1409
|
+
catch (err) {
|
|
1410
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
|
|
1411
|
+
}
|
|
1412
|
+
}
|
|
864
1413
|
// ─── Client Communication ───────────────────────────────────────
|
|
865
1414
|
/**
|
|
866
1415
|
* Ask the client to approve a tool operation (legacy single-engine path).
|
|
@@ -868,27 +1417,42 @@ export class AgentServer {
|
|
|
868
1417
|
requestApprovalFromClient(request) {
|
|
869
1418
|
return new Promise((resolve) => {
|
|
870
1419
|
const requestId = nanoid(12);
|
|
871
|
-
|
|
1420
|
+
const sessionId = typeof request.sessionId === "string" ? request.sessionId : undefined;
|
|
1421
|
+
const session = this.chatManager && sessionId ? this.chatManager.get(sessionId) : undefined;
|
|
1422
|
+
if (this.chatManager && sessionId && !session) {
|
|
1423
|
+
resolve({ approved: false, reason: `session closed: ${sessionId}` });
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
if (session) {
|
|
1427
|
+
session.pendingApprovals.set(requestId, (decision) => {
|
|
1428
|
+
resolve(decision);
|
|
1429
|
+
});
|
|
1430
|
+
}
|
|
1431
|
+
else {
|
|
1432
|
+
this.pendingApprovals.set(requestId, resolve);
|
|
1433
|
+
}
|
|
872
1434
|
const timer = setTimeout(() => {
|
|
873
|
-
|
|
874
|
-
|
|
1435
|
+
const pending = session?.pendingApprovals ?? this.pendingApprovals;
|
|
1436
|
+
if (pending.has(requestId)) {
|
|
1437
|
+
pending.delete(requestId);
|
|
875
1438
|
this.approvalTimers.delete(requestId);
|
|
876
1439
|
resolve({ approved: false, reason: "approval timed out" });
|
|
877
1440
|
}
|
|
878
1441
|
}, AgentServer.APPROVAL_TIMEOUT_MS);
|
|
879
1442
|
this.approvalTimers.set(requestId, timer);
|
|
880
|
-
this.notify(Methods.ApprovalRequest, {
|
|
1443
|
+
this.notify(Methods.ApprovalRequest, {
|
|
1444
|
+
...(sessionId ? { sessionId } : {}),
|
|
1445
|
+
requestId,
|
|
1446
|
+
request,
|
|
1447
|
+
});
|
|
881
1448
|
});
|
|
882
1449
|
}
|
|
883
1450
|
/**
|
|
884
|
-
*
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
*
|
|
888
|
-
*
|
|
889
|
-
* (the chatManager approve handler looks there, keyed by sessionId+requestId)
|
|
890
|
-
* and tags the notify with sessionId so the renderer routes the question to
|
|
891
|
-
* the right chat tab.
|
|
1451
|
+
* Per-session AskUserQuestion for the chatManager path. Resolves via the
|
|
1452
|
+
* SESSION's pendingApprovals (the chatManager approve handler looks there,
|
|
1453
|
+
* keyed by sessionId+requestId) and tags the notify with sessionId so the
|
|
1454
|
+
* renderer routes the question to the right chat tab. This intentionally has
|
|
1455
|
+
* no wall-clock timeout; Stop/cancel drains the pending ask.
|
|
892
1456
|
*/
|
|
893
1457
|
requestAskUserForSession(session, sessionId, question, opts) {
|
|
894
1458
|
return new Promise((resolve) => {
|
|
@@ -904,14 +1468,6 @@ export class AgentServer {
|
|
|
904
1468
|
resolve(typeof decision === "string" ? decision : "");
|
|
905
1469
|
}
|
|
906
1470
|
});
|
|
907
|
-
const timer = setTimeout(() => {
|
|
908
|
-
if (session.pendingApprovals.has(requestId)) {
|
|
909
|
-
session.pendingApprovals.delete(requestId);
|
|
910
|
-
this.approvalTimers.delete(requestId);
|
|
911
|
-
resolve("(approval timed out)");
|
|
912
|
-
}
|
|
913
|
-
}, AgentServer.APPROVAL_TIMEOUT_MS);
|
|
914
|
-
this.approvalTimers.set(requestId, timer);
|
|
915
1471
|
const args = { question };
|
|
916
1472
|
if (opts?.header !== undefined)
|
|
917
1473
|
args.header = opts.header;
|
|
@@ -919,6 +1475,8 @@ export class AgentServer {
|
|
|
919
1475
|
args.options = opts.options;
|
|
920
1476
|
if (opts?.multiSelect !== undefined)
|
|
921
1477
|
args.multiSelect = opts.multiSelect;
|
|
1478
|
+
if (opts?.optionsOnly !== undefined)
|
|
1479
|
+
args.optionsOnly = opts.optionsOnly;
|
|
922
1480
|
this.notify(Methods.ApprovalRequest, {
|
|
923
1481
|
sessionId,
|
|
924
1482
|
requestId,
|
|
@@ -931,6 +1489,134 @@ export class AgentServer {
|
|
|
931
1489
|
});
|
|
932
1490
|
});
|
|
933
1491
|
}
|
|
1492
|
+
/**
|
|
1493
|
+
* Build a BrowserBridge whose every method routes a browser action to the
|
|
1494
|
+
* client over the session's pendingApprovals channel (same as askUser). The
|
|
1495
|
+
* client (Electron main) drives the webview via CDP and replies with a JSON
|
|
1496
|
+
* result string we parse back into the typed result. On timeout / malformed
|
|
1497
|
+
* reply we degrade to a safe error result rather than throwing.
|
|
1498
|
+
*/
|
|
1499
|
+
makeBrowserBridge(session, sessionId) {
|
|
1500
|
+
const call = (action, payload) => this.requestBrowserActionForSession(session, sessionId, action, payload);
|
|
1501
|
+
return {
|
|
1502
|
+
snapshot: () => call("snapshot", {}),
|
|
1503
|
+
click: (ref) => call("click", { ref }),
|
|
1504
|
+
type: (ref, text) => call("type", { ref, text }),
|
|
1505
|
+
navigate: (url) => call("navigate", { url }),
|
|
1506
|
+
scroll: (dir, amount) => call("scroll", { dir, amount }),
|
|
1507
|
+
readContent: () => call("readContent", {}),
|
|
1508
|
+
extractLinks: () => call("extractLinks", {}),
|
|
1509
|
+
waitForLoad: (timeoutMs) => call("waitForLoad", { timeoutMs }),
|
|
1510
|
+
hover: (ref) => call("hover", { ref }),
|
|
1511
|
+
selectOption: (ref, value) => call("selectOption", { ref, value }),
|
|
1512
|
+
pressKey: (key, ref) => call("pressKey", { key, ref }),
|
|
1513
|
+
fetchImages: (refs) => call("fetchImages", { refs }),
|
|
1514
|
+
screenshot: (ref) => call("screenshot", { ref }),
|
|
1515
|
+
listTabs: () => call("listTabs", {}),
|
|
1516
|
+
switchTab: (tabId) => call("switchTab", { tabId }),
|
|
1517
|
+
};
|
|
1518
|
+
}
|
|
1519
|
+
requestBrowserActionForSession(session, sessionId, action, payload) {
|
|
1520
|
+
return new Promise((resolve) => {
|
|
1521
|
+
const requestId = nanoid(12);
|
|
1522
|
+
session.pendingApprovals.set(requestId, (decision) => {
|
|
1523
|
+
this.clearApprovalTimer(requestId);
|
|
1524
|
+
// Main replies with { approved:true, answer:<json string> } (reusing the
|
|
1525
|
+
// ApprovalResult shape) or a raw json string. Parse → typed result.
|
|
1526
|
+
let raw;
|
|
1527
|
+
if (decision && typeof decision === "object" && "approved" in decision) {
|
|
1528
|
+
const r = decision;
|
|
1529
|
+
raw = r.approved ? r.answer : undefined;
|
|
1530
|
+
}
|
|
1531
|
+
else if (typeof decision === "string") {
|
|
1532
|
+
raw = decision;
|
|
1533
|
+
}
|
|
1534
|
+
if (raw === undefined) {
|
|
1535
|
+
resolve({ ok: false, detail: "browser action declined or unavailable" });
|
|
1536
|
+
return;
|
|
1537
|
+
}
|
|
1538
|
+
try {
|
|
1539
|
+
resolve(JSON.parse(raw));
|
|
1540
|
+
}
|
|
1541
|
+
catch {
|
|
1542
|
+
resolve({ ok: false, detail: "malformed browser action result" });
|
|
1543
|
+
}
|
|
1544
|
+
});
|
|
1545
|
+
const timer = setTimeout(() => {
|
|
1546
|
+
if (session.pendingApprovals.has(requestId)) {
|
|
1547
|
+
session.pendingApprovals.delete(requestId);
|
|
1548
|
+
this.approvalTimers.delete(requestId);
|
|
1549
|
+
resolve({ ok: false, detail: "browser action timed out" });
|
|
1550
|
+
}
|
|
1551
|
+
}, AgentServer.APPROVAL_TIMEOUT_MS);
|
|
1552
|
+
this.approvalTimers.set(requestId, timer);
|
|
1553
|
+
this.notify(Methods.ApprovalRequest, {
|
|
1554
|
+
sessionId,
|
|
1555
|
+
requestId,
|
|
1556
|
+
request: {
|
|
1557
|
+
toolName: "__browser_action__",
|
|
1558
|
+
args: { action, ...payload },
|
|
1559
|
+
description: `browser:${action}`,
|
|
1560
|
+
riskLevel: "low",
|
|
1561
|
+
},
|
|
1562
|
+
});
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
/**
|
|
1566
|
+
* Ask the client (Electron main) to inject a cookie credential into the
|
|
1567
|
+
* built-in browser. Same pendingApprovals channel as browser actions; main
|
|
1568
|
+
* calls restoreCookiesToBrowser and replies with a JSON result string we
|
|
1569
|
+
* parse into { ok, count?, error? }. Degrades to ok:false on timeout/malformed.
|
|
1570
|
+
*/
|
|
1571
|
+
requestCredentialInjectForSession(session, sessionId, credentialId) {
|
|
1572
|
+
return new Promise((resolve) => {
|
|
1573
|
+
const requestId = nanoid(12);
|
|
1574
|
+
session.pendingApprovals.set(requestId, (decision) => {
|
|
1575
|
+
this.clearApprovalTimer(requestId);
|
|
1576
|
+
let raw;
|
|
1577
|
+
if (decision && typeof decision === "object" && "approved" in decision) {
|
|
1578
|
+
const r = decision;
|
|
1579
|
+
raw = r.approved ? r.answer : undefined;
|
|
1580
|
+
}
|
|
1581
|
+
else if (typeof decision === "string") {
|
|
1582
|
+
raw = decision;
|
|
1583
|
+
}
|
|
1584
|
+
if (raw === undefined) {
|
|
1585
|
+
resolve({ ok: false, error: "credential inject declined or unavailable" });
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
try {
|
|
1589
|
+
resolve(JSON.parse(raw));
|
|
1590
|
+
}
|
|
1591
|
+
catch {
|
|
1592
|
+
resolve({ ok: false, error: "malformed credential inject result" });
|
|
1593
|
+
}
|
|
1594
|
+
});
|
|
1595
|
+
const timer = setTimeout(() => {
|
|
1596
|
+
if (session.pendingApprovals.has(requestId)) {
|
|
1597
|
+
session.pendingApprovals.delete(requestId);
|
|
1598
|
+
this.approvalTimers.delete(requestId);
|
|
1599
|
+
resolve({ ok: false, error: "credential inject timed out" });
|
|
1600
|
+
}
|
|
1601
|
+
}, AgentServer.APPROVAL_TIMEOUT_MS);
|
|
1602
|
+
this.approvalTimers.set(requestId, timer);
|
|
1603
|
+
this.notify(Methods.ApprovalRequest, {
|
|
1604
|
+
sessionId,
|
|
1605
|
+
requestId,
|
|
1606
|
+
request: {
|
|
1607
|
+
toolName: "__credential_action__",
|
|
1608
|
+
args: { action: "injectCookie", credentialId },
|
|
1609
|
+
description: `credential:inject:${credentialId}`,
|
|
1610
|
+
riskLevel: "low",
|
|
1611
|
+
},
|
|
1612
|
+
});
|
|
1613
|
+
});
|
|
1614
|
+
}
|
|
1615
|
+
/**
|
|
1616
|
+
* Ask the client to answer a question from the agent (legacy single-engine
|
|
1617
|
+
* path). This intentionally has no wall-clock timeout; Stop/cancel drains
|
|
1618
|
+
* the pending ask.
|
|
1619
|
+
*/
|
|
934
1620
|
requestAskUserFromClient(question, opts) {
|
|
935
1621
|
return new Promise((resolve) => {
|
|
936
1622
|
const requestId = nanoid(12);
|
|
@@ -943,14 +1629,6 @@ export class AgentServer {
|
|
|
943
1629
|
resolve(result.reason ?? "(user declined to answer)");
|
|
944
1630
|
}
|
|
945
1631
|
});
|
|
946
|
-
const timer = setTimeout(() => {
|
|
947
|
-
if (this.pendingApprovals.has(requestId)) {
|
|
948
|
-
this.pendingApprovals.delete(requestId);
|
|
949
|
-
this.approvalTimers.delete(requestId);
|
|
950
|
-
resolve("(approval timed out)");
|
|
951
|
-
}
|
|
952
|
-
}, AgentServer.APPROVAL_TIMEOUT_MS);
|
|
953
|
-
this.approvalTimers.set(requestId, timer);
|
|
954
1632
|
const args = { question };
|
|
955
1633
|
if (opts?.header !== undefined)
|
|
956
1634
|
args.header = opts.header;
|
|
@@ -958,6 +1636,8 @@ export class AgentServer {
|
|
|
958
1636
|
args.options = opts.options;
|
|
959
1637
|
if (opts?.multiSelect !== undefined)
|
|
960
1638
|
args.multiSelect = opts.multiSelect;
|
|
1639
|
+
if (opts?.optionsOnly !== undefined)
|
|
1640
|
+
args.optionsOnly = opts.optionsOnly;
|
|
961
1641
|
this.notify(Methods.ApprovalRequest, {
|
|
962
1642
|
requestId,
|
|
963
1643
|
request: {
|
|
@@ -1010,6 +1690,9 @@ export class AgentServer {
|
|
|
1010
1690
|
this.bgAgentBusUnsubscribe = null;
|
|
1011
1691
|
}
|
|
1012
1692
|
if (this.chatManager) {
|
|
1693
|
+
this.chatManager.forEachSession((session) => {
|
|
1694
|
+
this.cancelSessionApprovals(session, "server closing");
|
|
1695
|
+
});
|
|
1013
1696
|
this.chatManager.closeAll();
|
|
1014
1697
|
}
|
|
1015
1698
|
// Legacy path cleanup
|
|
@@ -1036,6 +1719,25 @@ export class AgentServer {
|
|
|
1036
1719
|
this.approvalTimers.delete(requestId);
|
|
1037
1720
|
}
|
|
1038
1721
|
}
|
|
1722
|
+
/**
|
|
1723
|
+
* Resolve all of a chat session's pending approvals as cancelled and clear
|
|
1724
|
+
* any matching server-side approval timers. Used by handleCancel's
|
|
1725
|
+
* per-session path so a Stop while a tool is awaiting approval doesn't leave
|
|
1726
|
+
* the tool hanging. Bounded request types have same-keyed timer entries;
|
|
1727
|
+
* AskUserQuestion does not.
|
|
1728
|
+
*/
|
|
1729
|
+
cancelSessionApprovals(session, reason = "cancelled") {
|
|
1730
|
+
for (const [requestId, resolve] of session.pendingApprovals) {
|
|
1731
|
+
this.clearApprovalTimer(requestId);
|
|
1732
|
+
try {
|
|
1733
|
+
resolve({ approved: false, reason });
|
|
1734
|
+
}
|
|
1735
|
+
catch {
|
|
1736
|
+
/* a resolver must never break cancel cleanup */
|
|
1737
|
+
}
|
|
1738
|
+
}
|
|
1739
|
+
session.pendingApprovals.clear();
|
|
1740
|
+
}
|
|
1039
1741
|
clearAllApprovalTimers() {
|
|
1040
1742
|
for (const timer of this.approvalTimers.values()) {
|
|
1041
1743
|
clearTimeout(timer);
|