@cjhyy/code-shell-core 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -23
- package/dist/agent/agent-definition-registry.d.ts +2 -2
- package/dist/agent/agent-definition-registry.js +47 -10
- package/dist/agent/agent-definition.d.ts +5 -0
- package/dist/agent/agent-definition.js +14 -0
- package/dist/automation/index.d.ts +7 -4
- package/dist/automation/index.js +4 -2
- package/dist/automation/runner.js +3 -1
- package/dist/automation/scheduler.d.ts +57 -1
- package/dist/automation/scheduler.js +123 -23
- package/dist/automation/store.d.ts +6 -2
- package/dist/automation/store.js +7 -3
- package/dist/capabilities/index.d.ts +109 -0
- package/dist/capabilities/index.js +80 -0
- package/dist/capability-control/disabled-lists.d.ts +2 -1
- package/dist/capability-control/disabled-lists.js +3 -5
- package/dist/capability-control/overlay.d.ts +14 -1
- package/dist/capability-control/overlay.js +40 -0
- package/dist/capability-control/service.js +6 -4
- package/dist/cli/agent-server-stdio.d.ts +1 -1
- package/dist/cli/agent-server-stdio.js +66 -6
- package/dist/cli/agent-server-tcp.js +20 -5
- package/dist/context/compaction.d.ts +39 -3
- package/dist/context/compaction.js +151 -39
- package/dist/context/manager.d.ts +20 -4
- package/dist/context/manager.js +42 -7
- package/dist/context/token-counter.js +5 -3
- package/dist/context/tool-result-storage.d.ts +6 -0
- package/dist/context/tool-result-storage.js +25 -4
- package/dist/credentials/access.d.ts +13 -2
- package/dist/credentials/access.js +99 -4
- package/dist/credentials/index.d.ts +3 -2
- package/dist/credentials/index.js +2 -1
- package/dist/credentials/oauth.d.ts +9 -1
- package/dist/credentials/oauth.js +80 -1
- package/dist/credentials/store.d.ts +16 -2
- package/dist/credentials/store.js +33 -11
- package/dist/credentials/types.d.ts +62 -1
- package/dist/credentials/types.js +16 -1
- package/dist/credentials/use-credential-tool.js +15 -1
- package/dist/engine/auxiliary-pipeline.d.ts +40 -0
- package/dist/engine/auxiliary-pipeline.js +181 -0
- package/dist/{settings → engine}/disk-defaults.d.ts +2 -2
- package/dist/{settings → engine}/disk-defaults.js +5 -2
- package/dist/engine/dynamic-tool-defs.d.ts +4 -0
- package/dist/engine/dynamic-tool-defs.js +4 -0
- package/dist/engine/engine.d.ts +272 -184
- package/dist/engine/engine.js +1869 -1864
- package/dist/engine/file-history-hook.d.ts +11 -0
- package/dist/engine/file-history-hook.js +31 -0
- package/dist/engine/goal-judge-context.d.ts +9 -0
- package/dist/engine/goal-judge-context.js +234 -0
- package/dist/engine/injected-context-cache.d.ts +6 -0
- package/dist/engine/injected-context-cache.js +13 -0
- package/dist/engine/input-attachments.d.ts +1 -1
- package/dist/engine/model-facade.d.ts +3 -3
- package/dist/engine/model-facade.js +15 -14
- package/dist/engine/permission-controller.d.ts +55 -0
- package/dist/engine/permission-controller.js +148 -0
- package/dist/engine/prompt-cache-diagnostics.d.ts +58 -0
- package/dist/engine/prompt-cache-diagnostics.js +178 -0
- package/dist/engine/run-accounting.d.ts +41 -0
- package/dist/engine/run-accounting.js +126 -0
- package/dist/engine/run-context.d.ts +44 -0
- package/dist/engine/run-context.js +88 -0
- package/dist/engine/run-environment.d.ts +30 -0
- package/dist/engine/run-environment.js +77 -0
- package/dist/engine/run-finalize.d.ts +58 -0
- package/dist/engine/run-finalize.js +245 -0
- package/dist/engine/run-goal.d.ts +58 -0
- package/dist/engine/run-goal.js +188 -0
- package/dist/engine/run-image-input.d.ts +1 -1
- package/dist/engine/run-image-input.js +5 -3
- package/dist/engine/run-session-open.d.ts +41 -0
- package/dist/engine/run-session-open.js +186 -0
- package/dist/engine/run-setup.d.ts +42 -0
- package/dist/engine/run-setup.js +58 -0
- package/dist/engine/run-tooling.d.ts +125 -0
- package/dist/engine/run-tooling.js +283 -0
- package/dist/engine/run-types.d.ts +139 -0
- package/dist/engine/run-types.js +12 -0
- package/dist/engine/run-workspace.d.ts +50 -0
- package/dist/engine/run-workspace.js +114 -0
- package/dist/engine/session-title.d.ts +2 -1
- package/dist/engine/session-title.js +4 -1
- package/dist/engine/steer-queue.d.ts +1 -1
- package/dist/engine/subagent-spawner.d.ts +32 -0
- package/dist/engine/subagent-spawner.js +287 -0
- package/dist/engine/tool-summary.d.ts +2 -2
- package/dist/engine/tool-summary.js +2 -2
- package/dist/engine/turn-loop.d.ts +67 -10
- package/dist/engine/turn-loop.js +538 -97
- package/dist/engine/types.d.ts +35 -4
- package/dist/engine/types.js +1 -1
- package/dist/{engine/goal.d.ts → goal/lifecycle.d.ts} +80 -6
- package/dist/goal/lifecycle.js +482 -0
- package/dist/hooks/events.d.ts +26 -9
- package/dist/hooks/events.js +0 -3
- package/dist/hooks/goal-stop-hook.d.ts +28 -29
- package/dist/hooks/goal-stop-hook.js +344 -144
- package/dist/hooks/registry.js +4 -1
- package/dist/hooks/shell-runner.d.ts +12 -1
- package/dist/hooks/shell-runner.js +160 -9
- package/dist/index.d.ts +51 -110
- package/dist/index.extension.d.ts +51 -0
- package/dist/index.extension.js +39 -0
- package/dist/index.internal.d.ts +86 -0
- package/dist/index.internal.js +92 -0
- package/dist/index.js +37 -125
- package/dist/links/cli.d.ts +48 -0
- package/dist/links/cli.js +617 -0
- package/dist/links/http.d.ts +40 -0
- package/dist/links/http.js +166 -0
- package/dist/links/index.d.ts +4 -0
- package/dist/links/index.js +3 -0
- package/dist/links/link-action-tool.d.ts +5 -0
- package/dist/links/link-action-tool.js +241 -0
- package/dist/links/providers.d.ts +8 -0
- package/dist/links/providers.js +665 -0
- package/dist/links/types.d.ts +44 -0
- package/dist/llm/client-base.d.ts +7 -0
- package/dist/llm/client-base.js +72 -10
- package/dist/llm/model-pool.d.ts +1 -1
- package/dist/llm/model-pool.js +1 -1
- package/dist/llm/providers/anthropic.d.ts +1 -0
- package/dist/llm/providers/anthropic.js +9 -0
- package/dist/llm/providers/openai.d.ts +1 -0
- package/dist/llm/providers/openai.js +17 -0
- package/dist/llm/types.d.ts +12 -5
- package/dist/model-catalog/index.js +15 -2
- package/dist/model-catalog/resolve.d.ts +13 -1
- package/dist/model-catalog/resolve.js +37 -2
- package/dist/model-catalog/types.d.ts +74 -48
- package/dist/model-catalog/types.js +16 -5
- package/dist/model-catalog/upsert.d.ts +3 -1
- package/dist/model-catalog/upsert.js +9 -0
- package/dist/onboarding.d.ts +0 -1
- package/dist/onboarding.js +1 -22
- package/dist/panel-apps/bindings.d.ts +24 -0
- package/dist/panel-apps/bindings.js +69 -0
- package/dist/panel-apps/github-archive.d.ts +21 -0
- package/dist/panel-apps/github-archive.js +98 -0
- package/dist/panel-apps/index.d.ts +4 -0
- package/dist/panel-apps/index.js +4 -0
- package/dist/panel-apps/installer.d.ts +68 -0
- package/dist/panel-apps/installer.js +622 -0
- package/dist/panel-apps/manifest.d.ts +364 -0
- package/dist/panel-apps/manifest.js +189 -0
- package/dist/panel-apps/paths.d.ts +13 -0
- package/dist/panel-apps/paths.js +38 -0
- package/dist/panel-apps/registry.d.ts +51 -0
- package/dist/panel-apps/registry.js +99 -0
- package/dist/panel-apps/runtime.d.ts +80 -0
- package/dist/panel-apps/runtime.js +154 -0
- package/dist/plugins/gitOps.js +8 -1
- package/dist/plugins/installedPlugins.d.ts +2 -1
- package/dist/plugins/installedPlugins.js +146 -8
- package/dist/plugins/installer/codex/convertAgents.d.ts +4 -1
- package/dist/plugins/installer/codex/convertAgents.js +62 -8
- package/dist/plugins/installer/codex/convertCommands.d.ts +5 -5
- package/dist/plugins/installer/codex/convertCommands.js +35 -12
- package/dist/plugins/installer/codex/convertHooks.d.ts +10 -0
- package/dist/plugins/installer/codex/convertHooks.js +82 -0
- package/dist/plugins/installer/codex/convertSkills.js +41 -6
- package/dist/plugins/installer/install.d.ts +7 -1
- package/dist/plugins/installer/install.js +21 -87
- package/dist/plugins/installer/installFromArchive.d.ts +26 -4
- package/dist/plugins/installer/installFromArchive.js +44 -27
- package/dist/plugins/installer/installFromNpm.d.ts +31 -0
- package/dist/plugins/installer/installFromNpm.js +273 -0
- package/dist/plugins/installer/loadPluginAgents.js +17 -4
- package/dist/plugins/installer/loadPluginMcp.d.ts +14 -0
- package/dist/plugins/installer/loadPluginMcp.js +245 -35
- package/dist/plugins/installer/normalizeManifest.d.ts +13 -0
- package/dist/plugins/installer/normalizeManifest.js +306 -0
- package/dist/plugins/installer/npmTar.d.ts +13 -0
- package/dist/plugins/installer/npmTar.js +321 -0
- package/dist/plugins/installer/parseSource.d.ts +11 -0
- package/dist/plugins/installer/parseSource.js +48 -0
- package/dist/plugins/installer/preview.d.ts +101 -0
- package/dist/plugins/installer/preview.js +336 -0
- package/dist/plugins/installer/projectPluginSource.d.ts +14 -0
- package/dist/plugins/installer/projectPluginSource.js +140 -0
- package/dist/plugins/installer/types.d.ts +775 -0
- package/dist/plugins/installer/types.js +100 -0
- package/dist/plugins/installer/unzip.d.ts +11 -0
- package/dist/plugins/installer/unzip.js +123 -9
- package/dist/plugins/installer/update.d.ts +2 -2
- package/dist/plugins/installer/update.js +43 -4
- package/dist/plugins/loadPluginHooks.d.ts +19 -2
- package/dist/plugins/loadPluginHooks.js +97 -86
- package/dist/plugins/pluginAutomationTemplates.d.ts +18 -0
- package/dist/plugins/pluginAutomationTemplates.js +50 -0
- package/dist/plugins/pluginCatalog.d.ts +45 -0
- package/dist/plugins/pluginCatalog.js +87 -0
- package/dist/plugins/pluginCommandHook.d.ts +10 -4
- package/dist/plugins/pluginCommandHook.js +133 -18
- package/dist/plugins/pluginCommandsLoader.d.ts +19 -0
- package/dist/plugins/pluginCommandsLoader.js +181 -7
- package/dist/plugins/pluginContent.d.ts +10 -1
- package/dist/plugins/pluginContent.js +87 -30
- package/dist/plugins/pluginHookApproval.d.ts +30 -0
- package/dist/plugins/pluginHookApproval.js +160 -0
- package/dist/plugins/pluginHookIntegrity.d.ts +69 -0
- package/dist/plugins/pluginHookIntegrity.js +361 -0
- package/dist/plugins/pluginInstaller.js +71 -12
- package/dist/plugins/pluginMcpApproval.d.ts +19 -0
- package/dist/plugins/pluginMcpApproval.js +110 -0
- package/dist/plugins/pluginMcpIntegrity.d.ts +21 -0
- package/dist/plugins/pluginMcpIntegrity.js +74 -0
- package/dist/plugins/types.d.ts +30 -0
- package/dist/plugins/varRewrite.d.ts +9 -5
- package/dist/plugins/varRewrite.js +26 -17
- package/dist/preset/index.d.ts +11 -8
- package/dist/preset/index.js +62 -203
- package/dist/product/define.js +9 -3
- package/dist/product/types.d.ts +0 -2
- package/dist/profile/activation.d.ts +36 -0
- package/dist/profile/activation.js +57 -0
- package/dist/profile/catalog-store.d.ts +77 -0
- package/dist/profile/catalog-store.js +346 -0
- package/dist/profile/catalog.d.ts +54 -0
- package/dist/profile/catalog.js +152 -0
- package/dist/profile/index.d.ts +7 -0
- package/dist/profile/index.js +7 -0
- package/dist/profile/requirements.d.ts +76 -0
- package/dist/profile/requirements.js +117 -0
- package/dist/profile/resolve.d.ts +11 -0
- package/dist/profile/resolve.js +41 -0
- package/dist/profile/store.d.ts +17 -0
- package/dist/profile/store.js +167 -0
- package/dist/profile/types.d.ts +321 -0
- package/dist/profile/types.js +104 -0
- package/dist/prompt/composer.d.ts +46 -7
- package/dist/prompt/composer.js +78 -48
- package/dist/prompt/instruction-scanner.d.ts +6 -5
- package/dist/prompt/instruction-scanner.js +8 -23
- package/dist/prompt/section-loader.d.ts +2 -2
- package/dist/prompt/section-loader.js +7 -4
- package/dist/prompt/sections/browser.md +10 -9
- package/dist/prompt/sections/harness-base.md +9 -0
- package/dist/protocol/chat-session-manager.d.ts +77 -3
- package/dist/protocol/chat-session-manager.js +194 -18
- package/dist/protocol/chat-session.d.ts +72 -9
- package/dist/protocol/chat-session.js +151 -16
- package/dist/protocol/client.d.ts +37 -4
- package/dist/protocol/client.js +118 -6
- package/dist/protocol/index.d.ts +2 -2
- package/dist/protocol/index.js +2 -2
- package/dist/protocol/mobile-remote-types.d.ts +406 -0
- package/dist/protocol/mobile-remote-types.js +1 -0
- package/dist/protocol/server.d.ts +152 -14
- package/dist/protocol/server.js +1574 -229
- package/dist/protocol/types.d.ts +182 -32
- package/dist/protocol/types.js +18 -0
- package/dist/run/ArtifactTracker.d.ts +6 -1
- package/dist/run/ArtifactTracker.js +29 -21
- package/dist/run/EngineRunner.d.ts +2 -0
- package/dist/run/EngineRunner.js +1 -0
- package/dist/run/RunManager.d.ts +26 -0
- package/dist/run/RunManager.js +82 -11
- package/dist/run/factory.d.ts +3 -0
- package/dist/run/factory.js +2 -0
- package/dist/runtime/safe-spawn.d.ts +6 -0
- package/dist/runtime/safe-spawn.js +8 -13
- package/dist/services/auto-dream.d.ts +13 -1
- package/dist/services/auto-dream.js +56 -8
- package/dist/services/dream-consolidation.d.ts +3 -0
- package/dist/services/dream-consolidation.js +14 -2
- package/dist/services/index.d.ts +1 -1
- package/dist/services/index.js +1 -1
- package/dist/services/memory-orchestrator.js +6 -2
- package/dist/services/oauth.d.ts +34 -10
- package/dist/services/oauth.js +236 -98
- package/dist/services/session-memory.d.ts +4 -4
- package/dist/services/session-memory.js +13 -10
- package/dist/session/memory.d.ts +26 -7
- package/dist/session/memory.js +79 -21
- package/dist/session/session-manager.d.ts +208 -25
- package/dist/session/session-manager.js +1232 -116
- package/dist/session/session-message.d.ts +22 -0
- package/dist/session/session-message.js +1 -0
- package/dist/session/transcript.d.ts +76 -6
- package/dist/session/transcript.js +278 -7
- package/dist/settings/feature-flags.d.ts +40 -0
- package/dist/settings/feature-flags.js +40 -0
- package/dist/settings/manager.d.ts +39 -1
- package/dist/settings/manager.js +102 -41
- package/dist/settings/migrate-config.js +4 -2
- package/dist/settings/schema.d.ts +511 -177
- package/dist/settings/schema.js +98 -25
- package/dist/skills/scanner.d.ts +6 -2
- package/dist/skills/scanner.js +160 -9
- package/dist/sources/adapter.d.ts +14 -0
- package/dist/sources/adapter.js +7 -0
- package/dist/sources/adapters/local-files.d.ts +10 -0
- package/dist/sources/adapters/local-files.js +127 -0
- package/dist/sources/adapters/mcp-resource.d.ts +17 -0
- package/dist/sources/adapters/mcp-resource.js +41 -0
- package/dist/sources/adapters/mock.d.ts +3 -0
- package/dist/sources/adapters/mock.js +29 -0
- package/dist/sources/binding.d.ts +6 -0
- package/dist/sources/binding.js +24 -0
- package/dist/sources/catalog.d.ts +6 -0
- package/dist/sources/catalog.js +64 -0
- package/dist/sources/context-summary.d.ts +8 -0
- package/dist/sources/context-summary.js +13 -0
- package/dist/sources/credential-status.d.ts +2 -0
- package/dist/sources/credential-status.js +13 -0
- package/dist/sources/index.d.ts +10 -0
- package/dist/sources/index.js +10 -0
- package/dist/sources/resolve.d.ts +28 -0
- package/dist/sources/resolve.js +47 -0
- package/dist/sources/truncate-utf8.d.ts +7 -0
- package/dist/sources/truncate-utf8.js +18 -0
- package/dist/sources/types.d.ts +71 -0
- package/dist/sources/types.js +26 -0
- package/dist/testing/fetch-stub.d.ts +32 -0
- package/dist/testing/fetch-stub.js +24 -0
- package/dist/themes/image.d.ts +18 -0
- package/dist/themes/image.js +117 -0
- package/dist/themes/index.d.ts +4 -0
- package/dist/themes/index.js +4 -0
- package/dist/themes/installer.d.ts +50 -0
- package/dist/themes/installer.js +290 -0
- package/dist/themes/manifest.d.ts +102 -0
- package/dist/themes/manifest.js +86 -0
- package/dist/themes/paths.d.ts +14 -0
- package/dist/themes/paths.js +33 -0
- package/dist/tool-system/browser-bridge.d.ts +53 -11
- package/dist/tool-system/browser-bridge.js +8 -9
- package/dist/tool-system/builtin/agent-heartbeat.d.ts +2 -2
- package/dist/tool-system/builtin/agent-heartbeat.js +20 -6
- package/dist/tool-system/builtin/agent-notifications.d.ts +127 -72
- package/dist/tool-system/builtin/agent-notifications.js +291 -101
- package/dist/tool-system/builtin/agent-output-file.js +1 -3
- package/dist/tool-system/builtin/agent-progress.d.ts +6 -0
- package/dist/tool-system/builtin/agent-progress.js +83 -0
- package/dist/tool-system/builtin/agent-registry.d.ts +38 -2
- package/dist/tool-system/builtin/agent-registry.js +178 -9
- package/dist/tool-system/builtin/agent-transcript-translator.js +1 -1
- package/dist/tool-system/builtin/agent.d.ts +2 -0
- package/dist/tool-system/builtin/agent.js +279 -34
- package/dist/tool-system/builtin/background-jobs.d.ts +63 -7
- package/dist/tool-system/builtin/background-jobs.js +111 -15
- package/dist/tool-system/builtin/background-work.d.ts +12 -1
- package/dist/tool-system/builtin/background-work.js +12 -1
- package/dist/tool-system/builtin/bash.d.ts +3 -5
- package/dist/tool-system/builtin/bash.js +14 -6
- package/dist/tool-system/builtin/browser-tools.d.ts +8 -5
- package/dist/tool-system/builtin/browser-tools.js +89 -23
- package/dist/tool-system/builtin/edit-model-catalog.d.ts +6 -0
- package/dist/tool-system/builtin/edit-model-catalog.js +172 -12
- package/dist/tool-system/builtin/edit.d.ts +2 -1
- package/dist/tool-system/builtin/edit.js +14 -10
- package/dist/tool-system/builtin/file-cache.d.ts +2 -0
- package/dist/tool-system/builtin/file-cache.js +4 -0
- package/dist/tool-system/builtin/generate-image.js +17 -10
- package/dist/tool-system/builtin/generate-video.d.ts +4 -0
- package/dist/tool-system/builtin/generate-video.js +164 -28
- package/dist/tool-system/builtin/glob.d.ts +2 -1
- package/dist/tool-system/builtin/glob.js +28 -3
- package/dist/tool-system/builtin/grep.d.ts +1 -0
- package/dist/tool-system/builtin/grep.js +82 -17
- package/dist/tool-system/builtin/image-uploader.js +7 -1
- package/dist/tool-system/builtin/index.d.ts +57 -18
- package/dist/tool-system/builtin/index.js +280 -167
- package/dist/tool-system/builtin/memory.d.ts +6 -0
- package/dist/tool-system/builtin/memory.js +36 -15
- package/dist/tool-system/builtin/panel.d.ts +9 -0
- package/dist/tool-system/builtin/panel.js +314 -0
- package/dist/tool-system/builtin/read.js +5 -1
- package/dist/tool-system/builtin/send-message-to-session.d.ts +6 -0
- package/dist/tool-system/builtin/send-message-to-session.js +67 -0
- package/dist/tool-system/builtin/sources.d.ts +8 -0
- package/dist/tool-system/builtin/sources.js +137 -0
- package/dist/tool-system/builtin/tool-search.js +7 -2
- package/dist/tool-system/builtin/update-automation-memory.js +2 -0
- package/dist/tool-system/builtin/video-providers.d.ts +12 -15
- package/dist/tool-system/builtin/video-providers.js +1 -0
- package/dist/tool-system/builtin/view-image.d.ts +2 -2
- package/dist/tool-system/builtin/web-fetch.js +60 -15
- package/dist/tool-system/builtin/web-search.js +1 -3
- package/dist/tool-system/builtin/write.d.ts +2 -1
- package/dist/tool-system/builtin/write.js +14 -4
- package/dist/tool-system/capability-module.d.ts +93 -0
- package/dist/tool-system/capability-module.js +53 -0
- package/dist/tool-system/context.d.ts +94 -24
- package/dist/tool-system/executor.js +99 -58
- package/dist/tool-system/external-tool-exposure.d.ts +60 -0
- package/dist/tool-system/external-tool-exposure.js +304 -0
- package/dist/tool-system/mcp-manager.d.ts +17 -1
- package/dist/tool-system/mcp-manager.js +72 -7
- package/dist/tool-system/mcp-tool-policy.d.ts +9 -0
- package/dist/tool-system/mcp-tool-policy.js +34 -0
- package/dist/tool-system/panel-bridge.d.ts +53 -0
- package/dist/tool-system/panel-bridge.js +1 -0
- package/dist/tool-system/path-policy.d.ts +19 -0
- package/dist/tool-system/path-policy.js +62 -1
- package/dist/tool-system/permission.d.ts +69 -3
- package/dist/tool-system/permission.js +429 -30
- package/dist/tool-system/registry.d.ts +15 -3
- package/dist/tool-system/registry.js +82 -40
- package/dist/tool-system/sandbox/index.js +3 -8
- package/dist/tool-system/sandbox/seatbelt.js +2 -6
- package/dist/tool-system/session-tool-host.d.ts +139 -0
- package/dist/tool-system/session-tool-host.js +167 -0
- package/dist/tool-system/testing/tool-registry-harness.d.ts +31 -0
- package/dist/tool-system/testing/tool-registry-harness.js +51 -0
- package/dist/tool-system/validation.d.ts +14 -7
- package/dist/tool-system/validation.js +701 -7
- package/dist/types.d.ts +201 -20
- package/dist/{cc-orchestrator → utils}/cwd-normalize.d.ts +1 -0
- package/dist/{cc-orchestrator → utils}/cwd-normalize.js +1 -0
- package/dist/utils/file-mutex.d.ts +65 -0
- package/dist/utils/file-mutex.js +145 -0
- package/dist/utils/json.d.ts +2 -4
- package/dist/utils/json.js +2 -4
- package/dist/utils/lockfile.d.ts +30 -1
- package/dist/utils/lockfile.js +2 -2
- package/dist/utils/secret-scrubber.d.ts +8 -0
- package/dist/utils/secret-scrubber.js +227 -0
- package/dist/utils/toolDisplay.js +1 -1
- package/package.json +20 -5
- package/THIRD_PARTY_NOTICES.md +0 -206
- package/dist/arena/arena.d.ts +0 -43
- package/dist/arena/arena.js +0 -333
- package/dist/arena/context/context-tools.d.ts +0 -16
- package/dist/arena/context/context-tools.js +0 -272
- package/dist/arena/context/within-root.d.ts +0 -7
- package/dist/arena/context/within-root.js +0 -15
- package/dist/arena/detect-mode.d.ts +0 -20
- package/dist/arena/detect-mode.js +0 -78
- package/dist/arena/digest-builder.d.ts +0 -25
- package/dist/arena/digest-builder.js +0 -120
- package/dist/arena/index.d.ts +0 -29
- package/dist/arena/index.js +0 -29
- package/dist/arena/iterate/convergence.d.ts +0 -25
- package/dist/arena/iterate/convergence.js +0 -103
- package/dist/arena/iterate/formats/index.d.ts +0 -22
- package/dist/arena/iterate/formats/index.js +0 -283
- package/dist/arena/iterate/index.d.ts +0 -11
- package/dist/arena/iterate/index.js +0 -9
- package/dist/arena/iterate/iterative-arena.d.ts +0 -23
- package/dist/arena/iterate/iterative-arena.js +0 -237
- package/dist/arena/iterate/parse.d.ts +0 -42
- package/dist/arena/iterate/parse.js +0 -123
- package/dist/arena/iterate/phases/argue.d.ts +0 -22
- package/dist/arena/iterate/phases/argue.js +0 -165
- package/dist/arena/iterate/phases/revise.d.ts +0 -16
- package/dist/arena/iterate/phases/revise.js +0 -62
- package/dist/arena/iterate/phases/tournament.d.ts +0 -34
- package/dist/arena/iterate/phases/tournament.js +0 -113
- package/dist/arena/iterate/tools/web-tools.d.ts +0 -13
- package/dist/arena/iterate/tools/web-tools.js +0 -54
- package/dist/arena/iterate/types.d.ts +0 -152
- package/dist/arena/iterate/types.js +0 -8
- package/dist/arena/ledger.d.ts +0 -47
- package/dist/arena/ledger.js +0 -159
- package/dist/arena/lenses/architecture.d.ts +0 -5
- package/dist/arena/lenses/architecture.js +0 -22
- package/dist/arena/lenses/engineering.d.ts +0 -5
- package/dist/arena/lenses/engineering.js +0 -22
- package/dist/arena/lenses/general.d.ts +0 -5
- package/dist/arena/lenses/general.js +0 -20
- package/dist/arena/lenses/index.d.ts +0 -16
- package/dist/arena/lenses/index.js +0 -47
- package/dist/arena/lenses/product.d.ts +0 -5
- package/dist/arena/lenses/product.js +0 -22
- package/dist/arena/model-presets.d.ts +0 -23
- package/dist/arena/model-presets.js +0 -44
- package/dist/arena/phases/adjudication.d.ts +0 -24
- package/dist/arena/phases/adjudication.js +0 -141
- package/dist/arena/phases/build-consensus.d.ts +0 -29
- package/dist/arena/phases/build-consensus.js +0 -83
- package/dist/arena/phases/claim-registry.d.ts +0 -26
- package/dist/arena/phases/claim-registry.js +0 -60
- package/dist/arena/phases/cross-review.d.ts +0 -45
- package/dist/arena/phases/cross-review.js +0 -218
- package/dist/arena/phases/debate-rounds.d.ts +0 -27
- package/dist/arena/phases/debate-rounds.js +0 -159
- package/dist/arena/phases/participant-research.d.ts +0 -38
- package/dist/arena/phases/participant-research.js +0 -314
- package/dist/arena/phases/planning-detail-expansion.d.ts +0 -38
- package/dist/arena/phases/planning-detail-expansion.js +0 -119
- package/dist/arena/planner.d.ts +0 -27
- package/dist/arena/planner.js +0 -311
- package/dist/arena/providers/docs.d.ts +0 -7
- package/dist/arena/providers/docs.js +0 -111
- package/dist/arena/providers/git.d.ts +0 -8
- package/dist/arena/providers/git.js +0 -174
- package/dist/arena/providers/index.d.ts +0 -32
- package/dist/arena/providers/index.js +0 -132
- package/dist/arena/providers/none.d.ts +0 -7
- package/dist/arena/providers/none.js +0 -11
- package/dist/arena/providers/repo.d.ts +0 -7
- package/dist/arena/providers/repo.js +0 -258
- package/dist/arena/render/session.d.ts +0 -17
- package/dist/arena/render/session.js +0 -190
- package/dist/arena/render/terminal.d.ts +0 -34
- package/dist/arena/render/terminal.js +0 -286
- package/dist/arena/strategies/discussion.d.ts +0 -25
- package/dist/arena/strategies/discussion.js +0 -143
- package/dist/arena/strategies/index.d.ts +0 -15
- package/dist/arena/strategies/index.js +0 -28
- package/dist/arena/strategies/language-wrapper.d.ts +0 -17
- package/dist/arena/strategies/language-wrapper.js +0 -102
- package/dist/arena/strategies/lens-wrapper.d.ts +0 -16
- package/dist/arena/strategies/lens-wrapper.js +0 -236
- package/dist/arena/strategies/planning.d.ts +0 -30
- package/dist/arena/strategies/planning.js +0 -225
- package/dist/arena/strategies/review.d.ts +0 -26
- package/dist/arena/strategies/review.js +0 -168
- package/dist/arena/strategies/utils.d.ts +0 -36
- package/dist/arena/strategies/utils.js +0 -603
- package/dist/arena/tools/selector.d.ts +0 -17
- package/dist/arena/tools/selector.js +0 -61
- package/dist/arena/transitions.d.ts +0 -53
- package/dist/arena/transitions.js +0 -97
- package/dist/arena/types.d.ts +0 -514
- package/dist/arena/types.js +0 -27
- package/dist/cc-orchestrator/agent-adapter.d.ts +0 -54
- package/dist/cc-orchestrator/agent-adapter.js +0 -143
- package/dist/cc-orchestrator/cc-capability.d.ts +0 -19
- package/dist/cc-orchestrator/cc-capability.js +0 -53
- package/dist/cc-orchestrator/codex-session-discovery.d.ts +0 -24
- package/dist/cc-orchestrator/codex-session-discovery.js +0 -191
- package/dist/cc-orchestrator/codex-session-history.d.ts +0 -38
- package/dist/cc-orchestrator/codex-session-history.js +0 -247
- package/dist/cc-orchestrator/external-agent-bindings.d.ts +0 -27
- package/dist/cc-orchestrator/external-agent-bindings.js +0 -150
- package/dist/cc-orchestrator/external-agent-changes.d.ts +0 -19
- package/dist/cc-orchestrator/external-agent-changes.js +0 -231
- package/dist/cc-orchestrator/external-agent-driver.d.ts +0 -19
- package/dist/cc-orchestrator/external-agent-driver.js +0 -284
- package/dist/cc-orchestrator/external-agent-session-store.d.ts +0 -23
- package/dist/cc-orchestrator/external-agent-session-store.js +0 -146
- package/dist/cc-orchestrator/index.d.ts +0 -8
- package/dist/cc-orchestrator/index.js +0 -8
- package/dist/cc-orchestrator/relevance-judge.d.ts +0 -15
- package/dist/cc-orchestrator/relevance-judge.js +0 -29
- package/dist/cc-orchestrator/session-discovery.d.ts +0 -46
- package/dist/cc-orchestrator/session-discovery.js +0 -125
- package/dist/cc-orchestrator/session-history.d.ts +0 -54
- package/dist/cc-orchestrator/session-history.js +0 -150
- package/dist/cron/cron-runtime.d.ts +0 -2
- package/dist/cron/cron-runtime.js +0 -2
- package/dist/cron/cron-store.d.ts +0 -2
- package/dist/cron/cron-store.js +0 -2
- package/dist/cron/scheduler.d.ts +0 -7
- package/dist/cron/scheduler.js +0 -7
- package/dist/engine/goal.js +0 -206
- package/dist/external-agents/config.d.ts +0 -2
- package/dist/external-agents/config.js +0 -15
- package/dist/external-agents/types.d.ts +0 -31
- package/dist/git/parse-log.d.ts +0 -13
- package/dist/git/parse-log.js +0 -21
- package/dist/git/utils.d.ts +0 -49
- package/dist/git/utils.js +0 -161
- package/dist/git/worktree/crud.d.ts +0 -69
- package/dist/git/worktree/crud.js +0 -206
- package/dist/git/worktree/diff.d.ts +0 -14
- package/dist/git/worktree/diff.js +0 -82
- package/dist/git/worktree/git-exec.d.ts +0 -7
- package/dist/git/worktree/git-exec.js +0 -51
- package/dist/git/worktree/index.d.ts +0 -5
- package/dist/git/worktree/index.js +0 -5
- package/dist/git/worktree/query.d.ts +0 -42
- package/dist/git/worktree/query.js +0 -121
- package/dist/git/worktree/slug.d.ts +0 -11
- package/dist/git/worktree/slug.js +0 -58
- package/dist/git/worktree.d.ts +0 -1
- package/dist/git/worktree.js +0 -5
- package/dist/lsp/client.d.ts +0 -41
- package/dist/lsp/client.js +0 -186
- package/dist/lsp/manager.d.ts +0 -40
- package/dist/lsp/manager.js +0 -155
- package/dist/lsp/root-path.d.ts +0 -9
- package/dist/lsp/root-path.js +0 -12
- package/dist/lsp/servers.d.ts +0 -16
- package/dist/lsp/servers.js +0 -60
- package/dist/prompt/sections/coding.md +0 -35
- package/dist/quota/credentials.d.ts +0 -3
- package/dist/quota/credentials.js +0 -80
- package/dist/quota/index.d.ts +0 -36
- package/dist/quota/index.js +0 -155
- package/dist/quota/types.d.ts +0 -48
- package/dist/quota/types.js +0 -13
- package/dist/review/review-prompt.d.ts +0 -28
- package/dist/review/review-prompt.js +0 -81
- package/dist/state.d.ts +0 -160
- package/dist/state.js +0 -267
- package/dist/tool-system/builtin/apply-patch/applier.d.ts +0 -26
- package/dist/tool-system/builtin/apply-patch/applier.js +0 -308
- package/dist/tool-system/builtin/apply-patch/backup-targets.d.ts +0 -10
- package/dist/tool-system/builtin/apply-patch/backup-targets.js +0 -30
- package/dist/tool-system/builtin/apply-patch/index.d.ts +0 -20
- package/dist/tool-system/builtin/apply-patch/index.js +0 -105
- package/dist/tool-system/builtin/apply-patch/parser.d.ts +0 -17
- package/dist/tool-system/builtin/apply-patch/parser.js +0 -212
- package/dist/tool-system/builtin/apply-patch/seek-sequence.d.ts +0 -18
- package/dist/tool-system/builtin/apply-patch/seek-sequence.js +0 -123
- package/dist/tool-system/builtin/apply-patch/types.d.ts +0 -49
- package/dist/tool-system/builtin/apply-patch/types.js +0 -13
- package/dist/tool-system/builtin/arena.d.ts +0 -31
- package/dist/tool-system/builtin/arena.js +0 -415
- package/dist/tool-system/builtin/brief.d.ts +0 -6
- package/dist/tool-system/builtin/brief.js +0 -41
- package/dist/tool-system/builtin/check-quota.d.ts +0 -15
- package/dist/tool-system/builtin/check-quota.js +0 -34
- package/dist/tool-system/builtin/drive-claude-code.d.ts +0 -51
- package/dist/tool-system/builtin/drive-claude-code.js +0 -698
- package/dist/tool-system/builtin/lsp.d.ts +0 -7
- package/dist/tool-system/builtin/lsp.js +0 -144
- package/dist/tool-system/builtin/notebook-edit.d.ts +0 -7
- package/dist/tool-system/builtin/notebook-edit.js +0 -127
- package/dist/tool-system/builtin/worktree.d.ts +0 -11
- package/dist/tool-system/builtin/worktree.js +0 -345
- /package/dist/{external-agents → links}/types.js +0 -0
- /package/dist/{engine/session-usage.d.ts → session/usage.d.ts} +0 -0
- /package/dist/{engine/session-usage.js → session/usage.js} +0 -0
package/dist/protocol/server.js
CHANGED
|
@@ -15,18 +15,66 @@
|
|
|
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";
|
|
19
|
-
import {
|
|
20
|
-
import {
|
|
21
|
-
import { agentNotificationBus, notificationQueue, buildNotificationMessage, } from "../tool-system/builtin/agent-notifications.js";
|
|
18
|
+
import { isProtectedSettingKey, SettingsManager } from "../settings/manager.js";
|
|
19
|
+
import { getApprovalRouter, getInteractiveApprovalBackend, } from "../tool-system/permission.js";
|
|
20
|
+
import { agentNotificationBus, notificationQueue, buildNotificationMessage, notificationEnvelopeToLegacyStreamEvent, } from "../tool-system/builtin/agent-notifications.js";
|
|
22
21
|
import { backgroundShellManager } from "../runtime/background-shell.js";
|
|
23
22
|
import { backgroundJobRegistry } from "../tool-system/builtin/background-jobs.js";
|
|
24
23
|
import { listBackgroundWorkForUI } from "../tool-system/builtin/background-work.js";
|
|
25
24
|
import { logger } from "../logging/logger.js";
|
|
26
25
|
import { nanoid } from "nanoid";
|
|
27
|
-
import { SessionManager } from "../session/session-manager.js";
|
|
26
|
+
import { assertSafeSessionId, SessionManager } from "../session/session-manager.js";
|
|
28
27
|
import { redactLlmConfig, maskSecretValue } from "./redact.js";
|
|
29
28
|
import { redactSecrets } from "../logging/sanitize-messages.js";
|
|
29
|
+
import { computeEffectiveDisabledLists } from "../capability-control/disabled-lists.js";
|
|
30
|
+
import { describePluginCommands, expandPluginCommandBody, scanPluginCommands, MAX_PLUGIN_COMMAND_ARGUMENT_CHARS, } from "../plugins/pluginCommandsLoader.js";
|
|
31
|
+
function isValidRunAttachment(value) {
|
|
32
|
+
if (!value || typeof value !== "object")
|
|
33
|
+
return false;
|
|
34
|
+
const attachment = value;
|
|
35
|
+
const hasPath = [attachment.path, attachment.relPath, attachment.absPath].some((path) => typeof path === "string" && path.trim().length > 0);
|
|
36
|
+
return (typeof attachment.id === "string" &&
|
|
37
|
+
attachment.id.length > 0 &&
|
|
38
|
+
typeof attachment.sessionId === "string" &&
|
|
39
|
+
attachment.sessionId.length > 0 &&
|
|
40
|
+
(attachment.kind === "image" ||
|
|
41
|
+
attachment.kind === "file" ||
|
|
42
|
+
attachment.kind === "directory") &&
|
|
43
|
+
hasPath);
|
|
44
|
+
}
|
|
45
|
+
function runInputError(params) {
|
|
46
|
+
if (typeof params.task !== "string")
|
|
47
|
+
return "task must be a string";
|
|
48
|
+
const hasAttachment = Array.isArray(params.attachments) && params.attachments.some(isValidRunAttachment);
|
|
49
|
+
if (params.task.trim().length === 0 && !hasAttachment) {
|
|
50
|
+
return "task or a valid attachment is required";
|
|
51
|
+
}
|
|
52
|
+
if (params.displayText !== undefined &&
|
|
53
|
+
(typeof params.displayText !== "string" ||
|
|
54
|
+
params.displayText.trim().length === 0 ||
|
|
55
|
+
params.displayText.length > 20_000)) {
|
|
56
|
+
return "displayText must be a non-empty string up to 20000 characters";
|
|
57
|
+
}
|
|
58
|
+
if (params.injected !== undefined && typeof params.injected !== "boolean") {
|
|
59
|
+
return "injected must be a boolean";
|
|
60
|
+
}
|
|
61
|
+
if (params.behaviorMode !== undefined &&
|
|
62
|
+
(typeof params.behaviorMode !== "string" || params.behaviorMode.length === 0)) {
|
|
63
|
+
return `invalid behavior mode: ${String(params.behaviorMode)}`;
|
|
64
|
+
}
|
|
65
|
+
if (params.kind !== undefined && (typeof params.kind !== "string" || params.kind.length === 0)) {
|
|
66
|
+
return `invalid session kind: ${String(params.kind)}`;
|
|
67
|
+
}
|
|
68
|
+
if (params.profileParams !== undefined &&
|
|
69
|
+
(!params.profileParams ||
|
|
70
|
+
typeof params.profileParams !== "object" ||
|
|
71
|
+
Array.isArray(params.profileParams))) {
|
|
72
|
+
return "profileParams must be an object";
|
|
73
|
+
}
|
|
74
|
+
// Domain-specific run-param validation (e.g. behavior-mode vocabularies and
|
|
75
|
+
// extension-owned param shapes) lives in ExtensionModule.validateRunParams.
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
30
78
|
const COMPACT_STREAM_STRATEGIES = new Set([
|
|
31
79
|
"micro",
|
|
32
80
|
"summary",
|
|
@@ -34,14 +82,146 @@ const COMPACT_STREAM_STRATEGIES = new Set([
|
|
|
34
82
|
"snip",
|
|
35
83
|
"emergency",
|
|
36
84
|
"compacted",
|
|
85
|
+
// Range archival (archive_range query) emits its boundary through this map;
|
|
86
|
+
// without "range" here it would be silently downgraded to "compacted".
|
|
87
|
+
"range",
|
|
37
88
|
]);
|
|
38
89
|
function toCompactStreamStrategy(strategy) {
|
|
39
90
|
return COMPACT_STREAM_STRATEGIES.has(strategy)
|
|
40
91
|
? strategy
|
|
41
92
|
: "compacted";
|
|
42
93
|
}
|
|
94
|
+
const INTERNAL_PENDING_TOOLS = new Set([
|
|
95
|
+
"__browser_action__",
|
|
96
|
+
"__credential_action__",
|
|
97
|
+
"__workspace_action__",
|
|
98
|
+
]);
|
|
99
|
+
function internalCancellation(failure, reason) {
|
|
100
|
+
return { __internalCancelled: true, failure, reason };
|
|
101
|
+
}
|
|
102
|
+
function asInternalCancellation(value) {
|
|
103
|
+
return value &&
|
|
104
|
+
typeof value === "object" &&
|
|
105
|
+
value.__internalCancelled === true
|
|
106
|
+
? value
|
|
107
|
+
: undefined;
|
|
108
|
+
}
|
|
109
|
+
/** Human-facing tail appended to a host-loopback failure detail. */
|
|
110
|
+
const HOST_LOOPBACK_FAILURE_DETAIL = {
|
|
111
|
+
denied: "declined by the user",
|
|
112
|
+
cancelled: "cancelled because the turn was stopped",
|
|
113
|
+
session_closed: "cancelled because the session closed",
|
|
114
|
+
owner_lost: "unavailable because the approving client disconnected",
|
|
115
|
+
timed_out: "timed out waiting for the host",
|
|
116
|
+
malformed: "returned a malformed result",
|
|
117
|
+
};
|
|
118
|
+
/**
|
|
119
|
+
* Cap on host-supplied failure text forwarded to the model.
|
|
120
|
+
*
|
|
121
|
+
* The rest of the Panel path is carefully bounded (512KB JSON results, 500-char
|
|
122
|
+
* tool descriptions, ≤16 tools); the failure path must be too. Host text reaches
|
|
123
|
+
* the model verbatim, so an unbounded echo is both a context-budget hazard and an
|
|
124
|
+
* injection surface. Collapsed to one line so a multi-line payload can't fake
|
|
125
|
+
* structure in the tool result.
|
|
126
|
+
*/
|
|
127
|
+
const MAX_HOST_LOOPBACK_DETAIL_CHARS = 500;
|
|
128
|
+
function boundedHostDetail(text) {
|
|
129
|
+
const oneLine = text.replace(/\s+/gu, " ").trim();
|
|
130
|
+
return oneLine.length <= MAX_HOST_LOOPBACK_DETAIL_CHARS
|
|
131
|
+
? oneLine
|
|
132
|
+
: `${oneLine.slice(0, MAX_HOST_LOOPBACK_DETAIL_CHARS)}…`;
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* Recover the classified failure detail a host-loopback request settled with, if
|
|
136
|
+
* any. Bridges call this before falling back to "malformed result": a cancelled /
|
|
137
|
+
* denied / timed-out request is a KNOWN terminal state, and mislabelling it as
|
|
138
|
+
* malformed blames the host for something the user (or a closing session) did.
|
|
139
|
+
*/
|
|
140
|
+
function hostLoopbackDetail(result) {
|
|
141
|
+
if (!result || typeof result !== "object")
|
|
142
|
+
return undefined;
|
|
143
|
+
const candidate = result;
|
|
144
|
+
// Take the human-readable text FIRST, and do NOT require `failure` to be
|
|
145
|
+
// present. Genuine Desktop replies are `{ok:false, panelId?, detail}` and carry
|
|
146
|
+
// no `failure` key at all (see AgentPanelHostResult) — gating on `failure`
|
|
147
|
+
// discarded every real host error and relabelled it "malformed result", which
|
|
148
|
+
// is exactly the mislabelling this whole change exists to prevent.
|
|
149
|
+
// The bridges also disagree on the key: panel uses `error`, browser `detail`.
|
|
150
|
+
if (typeof candidate.error === "string" && candidate.error) {
|
|
151
|
+
return boundedHostDetail(candidate.error);
|
|
152
|
+
}
|
|
153
|
+
if (typeof candidate.detail === "string" && candidate.detail) {
|
|
154
|
+
return boundedHostDetail(candidate.detail);
|
|
155
|
+
}
|
|
156
|
+
// Classification only — never the raw string. `failure` arrives from
|
|
157
|
+
// host-parsed JSON, so echoing an unrecognized value verbatim would let the
|
|
158
|
+
// host inject arbitrary unbounded text into a tool result.
|
|
159
|
+
if (typeof candidate.failure === "string") {
|
|
160
|
+
return HOST_LOOPBACK_FAILURE_DETAIL[candidate.failure];
|
|
161
|
+
}
|
|
162
|
+
return undefined;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Normalize what a host-loopback resolver received into either the parsed host
|
|
166
|
+
* payload or a classified failure. Shared by all four bridges so their terminal
|
|
167
|
+
* semantics can't drift apart.
|
|
168
|
+
*
|
|
169
|
+
* Accepts `{approved:true, answer:<json>}` (the ApprovalResult shape main replies
|
|
170
|
+
* with), a bare JSON string, or an InternalPendingCancellation sentinel.
|
|
171
|
+
*/
|
|
172
|
+
function parseHostLoopbackDecision(decision, label) {
|
|
173
|
+
const cancelled = asInternalCancellation(decision);
|
|
174
|
+
if (cancelled) {
|
|
175
|
+
return {
|
|
176
|
+
ok: false,
|
|
177
|
+
failure: cancelled.failure,
|
|
178
|
+
detail: `${label} ${HOST_LOOPBACK_FAILURE_DETAIL[cancelled.failure]}`,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
let raw;
|
|
182
|
+
if (decision && typeof decision === "object" && "approved" in decision) {
|
|
183
|
+
const result = decision;
|
|
184
|
+
raw = result.approved ? result.answer : undefined;
|
|
185
|
+
}
|
|
186
|
+
else if (typeof decision === "string") {
|
|
187
|
+
raw = decision;
|
|
188
|
+
}
|
|
189
|
+
if (raw === undefined) {
|
|
190
|
+
return {
|
|
191
|
+
ok: false,
|
|
192
|
+
failure: "denied",
|
|
193
|
+
detail: `${label} ${HOST_LOOPBACK_FAILURE_DETAIL.denied}`,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
try {
|
|
197
|
+
return { ok: true, value: JSON.parse(raw) };
|
|
198
|
+
}
|
|
199
|
+
catch {
|
|
200
|
+
// The host answered but garbled it. That is NOT a denial — blaming the user
|
|
201
|
+
// for a host serialization bug sends the model looking in the wrong place.
|
|
202
|
+
return {
|
|
203
|
+
ok: false,
|
|
204
|
+
failure: "malformed",
|
|
205
|
+
detail: `${label} ${HOST_LOOPBACK_FAILURE_DETAIL.malformed}`,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
function safeApprovalToolName(toolName) {
|
|
210
|
+
const firstLine = toolName.split(/\r?\n/, 1)[0]?.trim() ?? "";
|
|
211
|
+
if (/(?:sk|api|token|secret)[-_][a-z0-9_-]{6,}/i.test(firstLine))
|
|
212
|
+
return "工具";
|
|
213
|
+
return (firstLine
|
|
214
|
+
.replace(/[^\p{L}\p{N}_.:@/-]+/gu, " ")
|
|
215
|
+
.trim()
|
|
216
|
+
.slice(0, 40) || "工具");
|
|
217
|
+
}
|
|
43
218
|
export class AgentServer {
|
|
44
|
-
chatManager
|
|
219
|
+
/** Host-supplied manager; requests route through the `chatManager` getter. */
|
|
220
|
+
baseChatManager;
|
|
221
|
+
/** Identity resolution hook; null → single-manager behavior (today's path). */
|
|
222
|
+
resolveIdentity;
|
|
223
|
+
/** Lazily-derived managers keyed by identity (non-base identities only). */
|
|
224
|
+
identityManagers = new Map();
|
|
45
225
|
legacyEngine;
|
|
46
226
|
globalQueryEngine = null;
|
|
47
227
|
transport;
|
|
@@ -49,15 +229,35 @@ export class AgentServer {
|
|
|
49
229
|
settingsReader;
|
|
50
230
|
/** Disk-only active-goal reader for agent/goalGet on a non-live session. */
|
|
51
231
|
readActiveGoalFromDisk;
|
|
232
|
+
updateActiveGoalOnDisk;
|
|
233
|
+
clearActiveGoalOnDisk;
|
|
52
234
|
workspaceBridgeEnabled;
|
|
235
|
+
panelBridgeEnabled;
|
|
236
|
+
connectionId;
|
|
237
|
+
strictApprovalRouting;
|
|
238
|
+
approvalRouter;
|
|
239
|
+
approvalConnectionUnregister = null;
|
|
240
|
+
disconnected = false;
|
|
241
|
+
pendingApprovalTargets = new Map();
|
|
242
|
+
/** Extension modules this server consults for protocol-level hooks. */
|
|
243
|
+
extensionModules;
|
|
244
|
+
/** Protocol lifecycle observers created by extension modules (registration order). */
|
|
245
|
+
protocolObservers = [];
|
|
246
|
+
/** Extension-registered protocol method/query aliases (compat channel). */
|
|
247
|
+
protocolQueryHandlers = new Map();
|
|
248
|
+
/** Union of extension hiddenSessionKinds, excluded from generic session lists. */
|
|
249
|
+
hiddenSessionKinds;
|
|
53
250
|
/**
|
|
54
251
|
* Last per-session EngineConfigSlice supplied by agent/run. Kept even after
|
|
55
252
|
* idle eviction so background-completion wakeups can rebuild the ChatSession
|
|
56
|
-
* with the same cwd/
|
|
253
|
+
* with the same cwd/trust inputs before draining the queue. Per-turn
|
|
254
|
+
* permission/plan overrides deliberately do not survive into wakeup turns.
|
|
57
255
|
*/
|
|
58
256
|
lastSliceBySession = new Map();
|
|
59
257
|
/** Lazy disk reader for cold wakeup rehydrate when this process lacks a slice. */
|
|
60
258
|
diskSessionReader = null;
|
|
259
|
+
/** Sessions dir for diskSessionReader; undefined → default sessions root. */
|
|
260
|
+
sessionDiskRoot;
|
|
61
261
|
/**
|
|
62
262
|
* Monotonic config-reload version, bumped per reloadSettings request so each
|
|
63
263
|
* Engine.refreshRuntimeConfig can drop out-of-order (stale) deliveries (Q5).
|
|
@@ -98,16 +298,87 @@ export class AgentServer {
|
|
|
98
298
|
* itself outlives the server (it's a process-local singleton).
|
|
99
299
|
*/
|
|
100
300
|
bgAgentBusUnsubscribe = null;
|
|
301
|
+
wakeupsInFlight = new Set();
|
|
302
|
+
/**
|
|
303
|
+
* Effective session manager for the connection this server serves. Without
|
|
304
|
+
* a resolveIdentity hook this is exactly the host-supplied manager —
|
|
305
|
+
* today's single-manager behavior, byte-for-byte. With the hook, requests
|
|
306
|
+
* route through a lazily-created per-identity manager, so the same
|
|
307
|
+
* sessionId under two identities resolves to two isolated ChatSessions and
|
|
308
|
+
* a connection can only list/stream sessions of its own identity.
|
|
309
|
+
*/
|
|
310
|
+
get chatManager() {
|
|
311
|
+
if (!this.baseChatManager || !this.resolveIdentity)
|
|
312
|
+
return this.baseChatManager;
|
|
313
|
+
const identity = this.resolveIdentity({ connectionId: this.connectionId });
|
|
314
|
+
if (identity === this.baseChatManager.identity)
|
|
315
|
+
return this.baseChatManager;
|
|
316
|
+
let manager = this.identityManagers.get(identity);
|
|
317
|
+
if (!manager) {
|
|
318
|
+
// forIdentity validates the identity as a safe path segment and throws
|
|
319
|
+
// on garbage — the request that triggered routing then fails closed.
|
|
320
|
+
manager = this.baseChatManager.forIdentity(identity);
|
|
321
|
+
this.identityManagers.set(identity, manager);
|
|
322
|
+
}
|
|
323
|
+
return manager;
|
|
324
|
+
}
|
|
101
325
|
constructor(options) {
|
|
102
|
-
this.
|
|
326
|
+
this.baseChatManager = options.chatManager ?? null;
|
|
327
|
+
this.resolveIdentity = options.resolveIdentity ?? null;
|
|
328
|
+
this.sessionDiskRoot = options.sessionDiskRoot;
|
|
103
329
|
this.legacyEngine = options.engine ?? null;
|
|
104
330
|
this.settingsReader = options.settingsReader ?? null;
|
|
105
331
|
this.readActiveGoalFromDisk = options.readActiveGoalFromDisk ?? null;
|
|
332
|
+
this.updateActiveGoalOnDisk = options.updateActiveGoalOnDisk ?? null;
|
|
333
|
+
this.clearActiveGoalOnDisk = options.clearActiveGoalOnDisk ?? null;
|
|
106
334
|
this.workspaceBridgeEnabled = options.workspaceBridge === true;
|
|
107
|
-
|
|
335
|
+
this.panelBridgeEnabled = options.panelBridge === true;
|
|
336
|
+
this.connectionId = options.connectionId ?? "legacy-agent-server";
|
|
337
|
+
this.strictApprovalRouting = options.connectionId !== undefined;
|
|
338
|
+
this.approvalRouter = options.approvalRouter ?? getApprovalRouter();
|
|
339
|
+
if (!this.strictApprovalRouting) {
|
|
340
|
+
this.approvalRouter.deregister(this.connectionId, "legacy approval host replaced");
|
|
341
|
+
}
|
|
342
|
+
getInteractiveApprovalBackend().clearPromptFn();
|
|
343
|
+
if (!this.baseChatManager && !this.legacyEngine) {
|
|
108
344
|
throw new Error("AgentServer: either chatManager or engine must be supplied");
|
|
109
345
|
}
|
|
110
346
|
this.transport = options.transport;
|
|
347
|
+
// Protocol lifecycle observers. Each configured extension module may
|
|
348
|
+
// attach one; the server calls them at every lifecycle hook point and
|
|
349
|
+
// isolates per-observer failures. Core default-loads the pet extension
|
|
350
|
+
// until the pet domain moves out of core (see AgentServerOptions).
|
|
351
|
+
this.extensionModules = options.extensionModules ?? [];
|
|
352
|
+
this.hiddenSessionKinds = [
|
|
353
|
+
...new Set(this.extensionModules.flatMap((module) => module.hiddenSessionKinds ?? [])),
|
|
354
|
+
];
|
|
355
|
+
const observerHost = {
|
|
356
|
+
getLiveSessionSnapshot: () => this.chatManager?.getLiveSessionSnapshot().sessions ?? [],
|
|
357
|
+
projectionGeneration: () => this.workerGeneration(),
|
|
358
|
+
getSessionKind: (sessionId) => {
|
|
359
|
+
const session = this.chatManager?.get(sessionId);
|
|
360
|
+
if (!session)
|
|
361
|
+
return undefined;
|
|
362
|
+
return session.engine
|
|
363
|
+
.getSessionManager?.()
|
|
364
|
+
.readSessionKind?.(sessionId);
|
|
365
|
+
},
|
|
366
|
+
isTransportDisconnected: () => this.disconnected,
|
|
367
|
+
notify: (method, params) => this.notify(method, params),
|
|
368
|
+
registerQuery: (type, handler) => {
|
|
369
|
+
this.protocolQueryHandlers.set(type, handler);
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
for (const module of this.extensionModules) {
|
|
373
|
+
if (!module.createProtocolObserver)
|
|
374
|
+
continue;
|
|
375
|
+
try {
|
|
376
|
+
this.protocolObservers.push(module.createProtocolObserver(observerHost));
|
|
377
|
+
}
|
|
378
|
+
catch (err) {
|
|
379
|
+
logger.warn(`protocol observer init failed for extension ${module.id}: ${err.message}`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
111
382
|
// Wire up incoming messages
|
|
112
383
|
this.transport.onMessage((msg) => {
|
|
113
384
|
if (isRequest(msg)) {
|
|
@@ -116,9 +387,10 @@ export class AgentServer {
|
|
|
116
387
|
});
|
|
117
388
|
}
|
|
118
389
|
});
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
390
|
+
this.approvalConnectionUnregister = this.approvalRouter.addConnection(this.connectionId, {
|
|
391
|
+
requestApproval: (request, target) => this.requestApprovalFromClient(request, target),
|
|
392
|
+
ownershipLost: (targets, reason) => this.failClosedApprovalTargets(targets, reason),
|
|
393
|
+
}, { claimUnownedSessions: !this.strictApprovalRouting });
|
|
122
394
|
if (this.legacyEngine) {
|
|
123
395
|
// Only wire an interactive askUser when a human is present. For
|
|
124
396
|
// unattended (headless) runs we leave askUser undefined so
|
|
@@ -138,8 +410,11 @@ export class AgentServer {
|
|
|
138
410
|
// polls (background-jobs.ts). The bus now guarantees a real sessionId
|
|
139
411
|
// (no more legacy-bucket coercion to ""), so we forward whatever sid it
|
|
140
412
|
// hands us.
|
|
141
|
-
this.bgAgentBusUnsubscribe = agentNotificationBus.subscribe((
|
|
142
|
-
|
|
413
|
+
this.bgAgentBusUnsubscribe = agentNotificationBus.subscribe((envelope) => {
|
|
414
|
+
const sessionId = envelope.to.sessionId;
|
|
415
|
+
const event = notificationEnvelopeToLegacyStreamEvent(envelope);
|
|
416
|
+
if (event)
|
|
417
|
+
this.notify(Methods.StreamEvent, { sessionId, event });
|
|
143
418
|
// Background work that finishes while the session is IDLE (a
|
|
144
419
|
// run_in_background Bash like a download, a background sub-agent, or a
|
|
145
420
|
// video poll — the engine no longer parks on any of them) would otherwise
|
|
@@ -151,7 +426,8 @@ export class AgentServer {
|
|
|
151
426
|
// (trigger B) drains it at end-of-turn instead. A never-exiting dev server
|
|
152
427
|
// emits no completion, so it never wakes anything (no task/service
|
|
153
428
|
// classification needed).
|
|
154
|
-
|
|
429
|
+
if (envelope.kind === "result")
|
|
430
|
+
this.maybeWakeIdleSession(sessionId);
|
|
155
431
|
});
|
|
156
432
|
// Notify client we're ready
|
|
157
433
|
this.notify(Methods.Status, { status: "ready" });
|
|
@@ -174,48 +450,60 @@ export class AgentServer {
|
|
|
174
450
|
* the first drains all currently-pending items; subsequent bus events for
|
|
175
451
|
* the same session find it busy (or find an empty queue) and no-op.
|
|
176
452
|
*
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
*
|
|
180
|
-
* before its first await. So when bus events #2..N arrive — still on the same
|
|
181
|
-
* synchronous fan-out as #1 — they already see the session as busy and no-op.
|
|
182
|
-
* If either path is ever made async (e.g. an await is added inside
|
|
183
|
-
* enqueueTurn before `active` is set, or the bus starts deferring delivery),
|
|
184
|
-
* this degrades into N concurrent wakeups for N completions. Keep both
|
|
185
|
-
* synchronous, or replace this comment's assumption with an explicit
|
|
186
|
-
* "wakeup in flight" guard flag.
|
|
453
|
+
* `wakeupsInFlight` serializes the now-async rehydrate path, so a burst of
|
|
454
|
+
* completion events cannot create duplicate sessions or enqueue duplicate
|
|
455
|
+
* wakeup turns while getOrCreate waits for a closing generation to settle.
|
|
187
456
|
*/
|
|
188
457
|
maybeWakeIdleSession(sessionId) {
|
|
189
|
-
if (
|
|
458
|
+
if (this.wakeupsInFlight.has(sessionId))
|
|
190
459
|
return;
|
|
191
|
-
|
|
460
|
+
this.wakeupsInFlight.add(sessionId);
|
|
461
|
+
void this.wakeIdleSession(sessionId)
|
|
462
|
+
.then((ranTurn) => {
|
|
463
|
+
this.wakeupsInFlight.delete(sessionId);
|
|
464
|
+
if (ranTurn && notificationQueue.getSnapshot(sessionId).length > 0) {
|
|
465
|
+
this.maybeWakeIdleSession(sessionId);
|
|
466
|
+
}
|
|
467
|
+
})
|
|
468
|
+
.catch(() => {
|
|
469
|
+
this.wakeupsInFlight.delete(sessionId);
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
async wakeIdleSession(sessionId) {
|
|
473
|
+
if (!this.chatManager)
|
|
474
|
+
return false;
|
|
475
|
+
if (this.chatManager.isUnavailable(sessionId))
|
|
476
|
+
return false;
|
|
477
|
+
const session = this.chatManager.get(sessionId) ?? (await this.rehydrateSessionForWake(sessionId));
|
|
192
478
|
if (!session || session.isBusy())
|
|
193
|
-
return;
|
|
479
|
+
return false;
|
|
194
480
|
// Headless / automation runs are one-shot: the caller takes result.text and
|
|
195
481
|
// is gone, so there's no consumer for a woken continuation turn. Headless
|
|
196
482
|
// already drained its background sub-agents inside engine.run before
|
|
197
483
|
// returning; any remaining queued notification (video/shell) must NOT spin
|
|
198
484
|
// an orphan turn. Only the interactive path auto-continues.
|
|
199
485
|
if (session.engine.isHeadless())
|
|
200
|
-
return;
|
|
486
|
+
return false;
|
|
201
487
|
// Don't resurrect a session the user just Stopped: cancel() leaves it idle
|
|
202
488
|
// (active=null) so isBusy() reads false, but auto-running a fresh turn here
|
|
203
489
|
// would defeat the Stop. The flag clears the moment the user sends again.
|
|
204
490
|
if (session.wasCancelledSinceLastTurn())
|
|
205
|
-
return;
|
|
491
|
+
return false;
|
|
206
492
|
const pending = notificationQueue.drainAll(sessionId);
|
|
207
493
|
if (pending.length === 0)
|
|
208
|
-
return;
|
|
494
|
+
return false;
|
|
209
495
|
const task = `<system-reminder>\n${buildNotificationMessage(pending)}\n</system-reminder>`;
|
|
210
|
-
|
|
211
|
-
.enqueueTurn(task, {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
496
|
+
try {
|
|
497
|
+
await session.enqueueTurn(task, {
|
|
498
|
+
// Synthetic notification, not the user's own input: persisted with an
|
|
499
|
+
// `injected` flag so a disk rebuild doesn't render it as a phantom user
|
|
500
|
+
// bubble (the live UI shows only the woken assistant's reply).
|
|
501
|
+
injected: true,
|
|
502
|
+
onStream: (event) => this.notify(Methods.StreamEvent, { sessionId, event }),
|
|
503
|
+
approvalRouter: this.approvalRouter,
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
catch (err) {
|
|
219
507
|
// A wakeup turn failing must not crash the bus fan-out. The drained
|
|
220
508
|
// notifications are already in the transcript via the run's messages;
|
|
221
509
|
// log and move on.
|
|
@@ -236,21 +524,14 @@ export class AgentServer {
|
|
|
236
524
|
sessionId,
|
|
237
525
|
event: { type: "error", error: err?.message ?? "background wakeup failed" },
|
|
238
526
|
});
|
|
239
|
-
}
|
|
240
|
-
|
|
241
|
-
// Run-boundary re-check (trigger B): the woken summarize turn may have
|
|
242
|
-
// spawned NEW background work (e.g. goal "generate 2 videos" → after #1
|
|
243
|
-
// completes, this turn submits #2). When #2 finishes its notification
|
|
244
|
-
// arrives while we're idle again — but if it arrived DURING this turn
|
|
245
|
-
// (busy), trigger A skipped it. Re-checking at the run boundary drains
|
|
246
|
-
// anything that landed while busy, chaining wakeups until the queue is
|
|
247
|
-
// truly empty. Replaces the old engine for(;;) outer loop.
|
|
248
|
-
this.maybeWakeIdleSession(sessionId);
|
|
249
|
-
});
|
|
527
|
+
}
|
|
528
|
+
return true;
|
|
250
529
|
}
|
|
251
|
-
rehydrateSessionForWake(sessionId) {
|
|
530
|
+
async rehydrateSessionForWake(sessionId) {
|
|
252
531
|
if (!this.chatManager)
|
|
253
532
|
return null;
|
|
533
|
+
if (this.chatManager.isUnavailable(sessionId))
|
|
534
|
+
return null;
|
|
254
535
|
try {
|
|
255
536
|
const pending = notificationQueue.getSnapshot(sessionId);
|
|
256
537
|
if (pending.length === 0) {
|
|
@@ -273,7 +554,7 @@ export class AgentServer {
|
|
|
273
554
|
});
|
|
274
555
|
return null;
|
|
275
556
|
}
|
|
276
|
-
const session = this.chatManager.getOrCreate(sessionId, slice);
|
|
557
|
+
const session = await this.chatManager.getOrCreate(sessionId, slice);
|
|
277
558
|
this.wireInteractiveSession(session, sessionId);
|
|
278
559
|
logger.debug("bg_wakeup.rehydrated_session", {
|
|
279
560
|
sessionId,
|
|
@@ -295,12 +576,11 @@ export class AgentServer {
|
|
|
295
576
|
if (cached)
|
|
296
577
|
return { ...cached };
|
|
297
578
|
if (!this.diskSessionReader)
|
|
298
|
-
this.diskSessionReader = new SessionManager();
|
|
299
|
-
const cwd = this.diskSessionReader.
|
|
579
|
+
this.diskSessionReader = new SessionManager(this.sessionDiskRoot);
|
|
580
|
+
const cwd = this.diskSessionReader.readSessionMainRoot(sessionId);
|
|
300
581
|
if (!cwd)
|
|
301
582
|
return null;
|
|
302
583
|
return {
|
|
303
|
-
permissionMode: "default",
|
|
304
584
|
projectTrusted: false,
|
|
305
585
|
cwd,
|
|
306
586
|
};
|
|
@@ -314,9 +594,88 @@ export class AgentServer {
|
|
|
314
594
|
session.engine.setAskUser((question, opts) => this.requestAskUserForSession(session, sid, question, opts));
|
|
315
595
|
session.engine.setBrowserBridge(this.makeBrowserBridge(session, sid));
|
|
316
596
|
session.engine.setInjectCredential((credentialId, credentialScope) => this.requestCredentialInjectForSession(session, sid, credentialId, credentialScope));
|
|
597
|
+
session.engine.setSessionMessageRouter((input) => this.routeSessionMessage(input));
|
|
317
598
|
if (this.workspaceBridgeEnabled && typeof session.engine.setWorkspaceBridge === "function") {
|
|
318
599
|
session.engine.setWorkspaceBridge(this.makeWorkspaceBridge(session, sid));
|
|
319
600
|
}
|
|
601
|
+
if (this.panelBridgeEnabled && typeof session.engine.setPanelBridge === "function") {
|
|
602
|
+
session.engine.setPanelBridge(this.makePanelBridge(session, sid));
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
/** Queue a model-sent message as an ordinary user turn in another Session. */
|
|
606
|
+
async routeSessionMessage(input) {
|
|
607
|
+
const manager = this.chatManager;
|
|
608
|
+
if (!manager)
|
|
609
|
+
throw new Error("cross-Session messaging requires a multi-session host");
|
|
610
|
+
const targetId = input.target.sessionId;
|
|
611
|
+
if (manager.isUnavailable(targetId)) {
|
|
612
|
+
throw new Error(`target Session is closing or closed: ${targetId}`);
|
|
613
|
+
}
|
|
614
|
+
const sourceSlice = this.lastSliceBySession.get(input.sourceSessionId);
|
|
615
|
+
const targetSlice = {
|
|
616
|
+
cwd: input.target.workspaceRoot,
|
|
617
|
+
projectTrusted: sourceSlice?.projectTrusted ?? false,
|
|
618
|
+
};
|
|
619
|
+
const targetAlreadyExists = manager.sessionExistsOnDisk(targetId, targetSlice);
|
|
620
|
+
const targetSession = await manager.getOrCreate(targetId, targetSlice);
|
|
621
|
+
const approvalRegistration = this.approvalRouter.register(targetId, this.connectionId);
|
|
622
|
+
if (!approvalRegistration.ok) {
|
|
623
|
+
throw new Error(`target Session ${targetId} is owned by another connection`);
|
|
624
|
+
}
|
|
625
|
+
this.rememberSessionSlice(targetId, targetSlice);
|
|
626
|
+
this.observeSessionAttached(targetId, targetSession.lastActivityAt);
|
|
627
|
+
this.wireInteractiveSession(targetSession, targetId);
|
|
628
|
+
const userMessageEvent = {
|
|
629
|
+
type: "session_user_message",
|
|
630
|
+
text: input.message,
|
|
631
|
+
};
|
|
632
|
+
this.observeSessionStream(targetId, userMessageEvent);
|
|
633
|
+
this.notify(Methods.StreamEvent, { sessionId: targetId, event: userMessageEvent });
|
|
634
|
+
const run = targetSession.enqueueTurn(input.message, {
|
|
635
|
+
cwd: input.target.workspaceRoot,
|
|
636
|
+
// A planned Session needs its renderer-selected initial profile. Once a
|
|
637
|
+
// Session exists, omitting this field makes Engine use the target's own
|
|
638
|
+
// persisted binding, so a stale source catalog cannot switch it back.
|
|
639
|
+
workspaceProfile: targetAlreadyExists ? undefined : input.target.workspaceProfile,
|
|
640
|
+
sessionMessageTargets: input.catalog,
|
|
641
|
+
onStream: (event) => {
|
|
642
|
+
this.observeSessionStream(targetId, event);
|
|
643
|
+
this.notify(Methods.StreamEvent, { sessionId: targetId, event });
|
|
644
|
+
},
|
|
645
|
+
approvalRouter: this.approvalRouter,
|
|
646
|
+
});
|
|
647
|
+
// The turn itself must NOT block the sender — a member's work can take
|
|
648
|
+
// minutes and the lead has more to dispatch. But a turn that fails to *start*
|
|
649
|
+
// (most often: the target's digital human is not installed, so run-setup
|
|
650
|
+
// throws "Workspace profile ... is unavailable") used to be reported only to
|
|
651
|
+
// the log while SendMessageToSession still answered "has queued the turn".
|
|
652
|
+
// A real lead then waited two hours and re-sent, never learning the reason.
|
|
653
|
+
// So: surface an immediate failure to the caller, keep a slow turn detached.
|
|
654
|
+
let startupError;
|
|
655
|
+
const tracked = run
|
|
656
|
+
.then(() => this.maybeWakeIdleSession(targetId))
|
|
657
|
+
.catch((error) => {
|
|
658
|
+
startupError = error;
|
|
659
|
+
logger.warn("session_message.turn_failed", {
|
|
660
|
+
sourceSessionId: input.sourceSessionId,
|
|
661
|
+
targetSessionId: targetId,
|
|
662
|
+
error: error instanceof Error ? error.message : String(error),
|
|
663
|
+
});
|
|
664
|
+
this.notify(Methods.StreamEvent, {
|
|
665
|
+
sessionId: targetId,
|
|
666
|
+
event: {
|
|
667
|
+
type: "error",
|
|
668
|
+
error: error instanceof Error ? error.message : "cross-Session message failed",
|
|
669
|
+
},
|
|
670
|
+
});
|
|
671
|
+
});
|
|
672
|
+
// One macrotask is enough for a synchronous-throw path to settle; anything
|
|
673
|
+
// still running by then is genuine work and stays detached.
|
|
674
|
+
await Promise.race([tracked, new Promise((resolve) => setTimeout(resolve, 0))]);
|
|
675
|
+
if (startupError) {
|
|
676
|
+
throw startupError instanceof Error ? startupError : new Error(String(startupError));
|
|
677
|
+
}
|
|
678
|
+
void tracked;
|
|
320
679
|
}
|
|
321
680
|
// ─── Request Dispatch ───────────────────────────────────────────
|
|
322
681
|
async handleRequest(req) {
|
|
@@ -324,6 +683,9 @@ export class AgentServer {
|
|
|
324
683
|
case Methods.Run:
|
|
325
684
|
await this.handleRun(req);
|
|
326
685
|
break;
|
|
686
|
+
case Methods.ForkSession:
|
|
687
|
+
await this.handleForkSession(req);
|
|
688
|
+
break;
|
|
327
689
|
case Methods.Approve:
|
|
328
690
|
this.handleApprove(req);
|
|
329
691
|
break;
|
|
@@ -346,14 +708,23 @@ export class AgentServer {
|
|
|
346
708
|
this.handleUnsteer(req);
|
|
347
709
|
break;
|
|
348
710
|
case Methods.CloseSession:
|
|
349
|
-
this.handleCloseSession(req);
|
|
711
|
+
await this.handleCloseSession(req);
|
|
350
712
|
break;
|
|
351
713
|
case Methods.ReleaseWorkspace:
|
|
352
714
|
this.handleReleaseWorkspace(req);
|
|
353
715
|
break;
|
|
716
|
+
case Methods.SetWorkspace:
|
|
717
|
+
this.handleSetWorkspace(req);
|
|
718
|
+
break;
|
|
354
719
|
case Methods.GoalExtend:
|
|
355
720
|
this.handleGoalExtend(req);
|
|
356
721
|
break;
|
|
722
|
+
case Methods.GoalUpdate:
|
|
723
|
+
await this.handleGoalUpdate(req);
|
|
724
|
+
break;
|
|
725
|
+
case Methods.GoalDelete:
|
|
726
|
+
this.handleGoalClear(req, true);
|
|
727
|
+
break;
|
|
357
728
|
case Methods.GoalClear:
|
|
358
729
|
this.handleGoalClear(req);
|
|
359
730
|
break;
|
|
@@ -366,11 +737,326 @@ export class AgentServer {
|
|
|
366
737
|
case Methods.BackgroundWork:
|
|
367
738
|
this.handleBackgroundWork(req);
|
|
368
739
|
break;
|
|
740
|
+
case Methods.PluginCommandsList:
|
|
741
|
+
this.handlePluginCommandsList(req);
|
|
742
|
+
break;
|
|
743
|
+
case Methods.PluginCommandExpand:
|
|
744
|
+
this.handlePluginCommandExpand(req);
|
|
745
|
+
break;
|
|
746
|
+
// Compat channel: the wire method name is retained, but the handler is
|
|
747
|
+
// whatever extension registered it (the pet extension by default).
|
|
748
|
+
case Methods.GetPetProjectionSnapshot:
|
|
749
|
+
await this.handleExtensionProtocolMethod(req);
|
|
750
|
+
break;
|
|
369
751
|
default:
|
|
370
752
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.MethodNotFound, `Unknown method: ${req.method}`));
|
|
371
753
|
}
|
|
372
754
|
}
|
|
755
|
+
pluginCommandContext(cwd) {
|
|
756
|
+
if (typeof cwd !== "string" ||
|
|
757
|
+
cwd.trim().length === 0 ||
|
|
758
|
+
cwd.length > 4_096 ||
|
|
759
|
+
cwd.includes("\0")) {
|
|
760
|
+
return { error: "cwd must be a non-empty string of at most 4096 characters" };
|
|
761
|
+
}
|
|
762
|
+
const disabledPluginNames = new Set(computeEffectiveDisabledLists(new SettingsManager(cwd, "full"), cwd).disabledPlugins);
|
|
763
|
+
return { cwd, disabledPluginNames };
|
|
764
|
+
}
|
|
765
|
+
handlePluginCommandsList(req) {
|
|
766
|
+
const params = (req.params ?? {});
|
|
767
|
+
const context = this.pluginCommandContext(params.cwd);
|
|
768
|
+
if ("error" in context) {
|
|
769
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, context.error));
|
|
770
|
+
return;
|
|
771
|
+
}
|
|
772
|
+
try {
|
|
773
|
+
this.transport.send(createResponse(req.id, {
|
|
774
|
+
commands: describePluginCommands(scanPluginCommands(), context.disabledPluginNames),
|
|
775
|
+
}));
|
|
776
|
+
}
|
|
777
|
+
catch (error) {
|
|
778
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, error.message));
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
handlePluginCommandExpand(req) {
|
|
782
|
+
const params = (req.params ?? {});
|
|
783
|
+
const context = this.pluginCommandContext(params.cwd);
|
|
784
|
+
if ("error" in context) {
|
|
785
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, context.error));
|
|
786
|
+
return;
|
|
787
|
+
}
|
|
788
|
+
if (typeof params.name !== "string" ||
|
|
789
|
+
params.name.length === 0 ||
|
|
790
|
+
params.name.length > 512 ||
|
|
791
|
+
params.name.includes("\0")) {
|
|
792
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "name must be a non-empty string of at most 512 characters"));
|
|
793
|
+
return;
|
|
794
|
+
}
|
|
795
|
+
const rawArguments = params.rawArguments ?? "";
|
|
796
|
+
if (typeof rawArguments !== "string" ||
|
|
797
|
+
rawArguments.length > MAX_PLUGIN_COMMAND_ARGUMENT_CHARS) {
|
|
798
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, `rawArguments must be a string of at most ${MAX_PLUGIN_COMMAND_ARGUMENT_CHARS} characters`));
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
const command = scanPluginCommands().find((candidate) => candidate.name === params.name && !context.disabledPluginNames.has(candidate.pluginName));
|
|
802
|
+
if (!command) {
|
|
803
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, `plugin command is unavailable: ${params.name}`));
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
try {
|
|
807
|
+
this.transport.send(createResponse(req.id, {
|
|
808
|
+
prompt: expandPluginCommandBody(command.body, rawArguments),
|
|
809
|
+
}));
|
|
810
|
+
}
|
|
811
|
+
catch (error) {
|
|
812
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, error.message));
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
/** Route a protocol method to its extension-registered handler, if any. */
|
|
816
|
+
async handleExtensionProtocolMethod(req) {
|
|
817
|
+
const handler = this.protocolQueryHandlers.get(req.method);
|
|
818
|
+
if (!handler) {
|
|
819
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.MethodNotFound, `Unknown method: ${req.method}`));
|
|
820
|
+
return;
|
|
821
|
+
}
|
|
822
|
+
try {
|
|
823
|
+
const data = await handler((req.params ?? {}));
|
|
824
|
+
this.transport.send(createResponse(req.id, data));
|
|
825
|
+
}
|
|
826
|
+
catch (err) {
|
|
827
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
// ─── Protocol observers ─────────────────────────────────────────
|
|
831
|
+
// Domain-agnostic lifecycle dispatch. Observers run in registration order
|
|
832
|
+
// and a throwing observer never breaks the protocol path or its peers.
|
|
833
|
+
forEachObserver(hook, fn) {
|
|
834
|
+
for (const observer of this.protocolObservers) {
|
|
835
|
+
try {
|
|
836
|
+
fn(observer);
|
|
837
|
+
}
|
|
838
|
+
catch (err) {
|
|
839
|
+
logger.warn(`protocol observer ${hook} hook failed: ${err.message}`);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
}
|
|
843
|
+
observeSessionAttached(sessionId, lastActivityAt) {
|
|
844
|
+
this.forEachObserver("onSessionAttached", (observer) => observer.onSessionAttached?.(sessionId, lastActivityAt));
|
|
845
|
+
}
|
|
846
|
+
observeSessionStream(sessionId, event) {
|
|
847
|
+
this.forEachObserver("onSessionStream", (observer) => observer.onSessionStream?.(sessionId, event));
|
|
848
|
+
}
|
|
849
|
+
observeRunBoundary(sessionId, phase) {
|
|
850
|
+
this.forEachObserver("onRunBoundary", (observer) => observer.onRunBoundary?.(sessionId, phase));
|
|
851
|
+
}
|
|
852
|
+
/** Observers may replace approval metadata (e.g. surfaceable overrides). */
|
|
853
|
+
observeApprovalCreated(metadata) {
|
|
854
|
+
for (const observer of this.protocolObservers) {
|
|
855
|
+
try {
|
|
856
|
+
const next = observer.onApprovalCreated?.(metadata);
|
|
857
|
+
if (next)
|
|
858
|
+
metadata = next;
|
|
859
|
+
}
|
|
860
|
+
catch (err) {
|
|
861
|
+
logger.warn(`protocol observer onApprovalCreated hook failed: ${err.message}`);
|
|
862
|
+
}
|
|
863
|
+
}
|
|
864
|
+
return metadata;
|
|
865
|
+
}
|
|
866
|
+
observeApprovalTransition(metadata, status) {
|
|
867
|
+
this.forEachObserver("onApprovalTransition", (observer) => observer.onApprovalTransition?.(metadata, status));
|
|
868
|
+
}
|
|
869
|
+
observeSessionClosed(sessionId) {
|
|
870
|
+
this.forEachObserver("onSessionClosed", (observer) => observer.onSessionClosed?.(sessionId));
|
|
871
|
+
}
|
|
872
|
+
observeServerClose() {
|
|
873
|
+
this.forEachObserver("onServerClose", (observer) => observer.onServerClose?.());
|
|
874
|
+
}
|
|
875
|
+
/** Generic run-param shape checks plus each extension's domain validation. */
|
|
876
|
+
validateRunInput(params) {
|
|
877
|
+
const generic = runInputError(params);
|
|
878
|
+
if (generic)
|
|
879
|
+
return generic;
|
|
880
|
+
for (const module of this.extensionModules) {
|
|
881
|
+
if (!module.validateRunParams)
|
|
882
|
+
continue;
|
|
883
|
+
const error = module.validateRunParams(params);
|
|
884
|
+
if (error)
|
|
885
|
+
return error;
|
|
886
|
+
}
|
|
887
|
+
return null;
|
|
888
|
+
}
|
|
373
889
|
// ─── Run ────────────────────────────────────────────────────────
|
|
890
|
+
async handleForkSession(req) {
|
|
891
|
+
const params = req.params ?? {};
|
|
892
|
+
const sourceSessionId = typeof params.sourceSessionId === "string" ? params.sourceSessionId : "";
|
|
893
|
+
const targetSessionId = typeof params.targetSessionId === "string" ? params.targetSessionId : undefined;
|
|
894
|
+
const throughEventId = typeof params.throughEventId === "string" ? params.throughEventId : undefined;
|
|
895
|
+
const fromEventId = typeof params.fromEventId === "string" ? params.fromEventId : undefined;
|
|
896
|
+
const toEventId = typeof params.toEventId === "string" ? params.toEventId : undefined;
|
|
897
|
+
const mode = params.mode;
|
|
898
|
+
const isSideFork = params.forkKind === "side";
|
|
899
|
+
if (!sourceSessionId || (mode !== "full" && mode !== "summary")) {
|
|
900
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "forkSession requires sourceSessionId and mode=full|summary"));
|
|
901
|
+
return;
|
|
902
|
+
}
|
|
903
|
+
if (params.targetSessionId !== undefined &&
|
|
904
|
+
(typeof params.targetSessionId !== "string" || params.targetSessionId.length === 0)) {
|
|
905
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "targetSessionId must be a non-empty string"));
|
|
906
|
+
return;
|
|
907
|
+
}
|
|
908
|
+
if (params.throughEventId !== undefined && typeof params.throughEventId !== "string") {
|
|
909
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "throughEventId must be a string"));
|
|
910
|
+
return;
|
|
911
|
+
}
|
|
912
|
+
if (mode === "summary" && (!fromEventId || !toEventId)) {
|
|
913
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "summary fork requires fromEventId and toEventId"));
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
if (mode === "summary" &&
|
|
917
|
+
(params.throughEventId !== undefined ||
|
|
918
|
+
params.forkKind !== undefined ||
|
|
919
|
+
params.quickChatClaimId !== undefined)) {
|
|
920
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "summary fork only accepts fromEventId/toEventId range fields"));
|
|
921
|
+
return;
|
|
922
|
+
}
|
|
923
|
+
if (mode === "full" && (params.fromEventId !== undefined || params.toEventId !== undefined)) {
|
|
924
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "full fork does not accept summary range fields"));
|
|
925
|
+
return;
|
|
926
|
+
}
|
|
927
|
+
if (mode === "full" &&
|
|
928
|
+
params.quickChatClaimId !== undefined &&
|
|
929
|
+
typeof params.quickChatClaimId !== "string") {
|
|
930
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "quickChatClaimId must be a string"));
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
933
|
+
if (params.forkKind !== undefined && !isSideFork) {
|
|
934
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "forkKind must be side when present"));
|
|
935
|
+
return;
|
|
936
|
+
}
|
|
937
|
+
if (isSideFork && throughEventId !== undefined) {
|
|
938
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "side fork uses the last completed turn and cannot specify throughEventId"));
|
|
939
|
+
return;
|
|
940
|
+
}
|
|
941
|
+
try {
|
|
942
|
+
assertSafeSessionId(sourceSessionId);
|
|
943
|
+
if (targetSessionId !== undefined)
|
|
944
|
+
assertSafeSessionId(targetSessionId);
|
|
945
|
+
}
|
|
946
|
+
catch (err) {
|
|
947
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, err instanceof Error ? err.message : String(err)));
|
|
948
|
+
return;
|
|
949
|
+
}
|
|
950
|
+
let source;
|
|
951
|
+
try {
|
|
952
|
+
source = this.chatManager?.get(sourceSessionId);
|
|
953
|
+
if (!source && this.chatManager) {
|
|
954
|
+
if (this.chatManager.isUnavailable(sourceSessionId)) {
|
|
955
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionClosed, `Session is closing or closed: ${sourceSessionId}`));
|
|
956
|
+
return;
|
|
957
|
+
}
|
|
958
|
+
source =
|
|
959
|
+
(await this.chatManager.getOrCreatePersisted(sourceSessionId, this.lastSliceBySession.get(sourceSessionId), mode === "summary")) ?? undefined;
|
|
960
|
+
if (source) {
|
|
961
|
+
this.rememberSessionSlice(sourceSessionId, {
|
|
962
|
+
...this.lastSliceBySession.get(sourceSessionId),
|
|
963
|
+
cwd: source.engine.getConfig().cwd,
|
|
964
|
+
});
|
|
965
|
+
}
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
catch (err) {
|
|
969
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.Overloaded, err instanceof Error ? err.message : String(err)));
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
if (!isSideFork && source && (source.isBusy() || source.queueDepth() > 0)) {
|
|
973
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.Overloaded, "source session is still producing or has queued turns"));
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
const engine = source?.engine ?? this.legacyEngine;
|
|
977
|
+
if (!engine || !engine.sessionExistsOnDisk(sourceSessionId)) {
|
|
978
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionNotFound, `Session not found: ${sourceSessionId}`));
|
|
979
|
+
return;
|
|
980
|
+
}
|
|
981
|
+
if (targetSessionId && engine.sessionExistsOnDisk(targetSessionId)) {
|
|
982
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, `Session already exists: ${targetSessionId}`));
|
|
983
|
+
return;
|
|
984
|
+
}
|
|
985
|
+
try {
|
|
986
|
+
if (mode === "summary") {
|
|
987
|
+
const sourceRange = { fromEventId: fromEventId, toEventId: toEventId };
|
|
988
|
+
const packageAndPublish = async (signal) => {
|
|
989
|
+
const selected = engine.selectContextPackage(sourceSessionId, sourceRange);
|
|
990
|
+
const packaged = await engine.summarizeContextPackage(selected.messages, signal, sourceSessionId);
|
|
991
|
+
const result = engine.createSummaryFork(sourceSessionId, {
|
|
992
|
+
targetSessionId,
|
|
993
|
+
...sourceRange,
|
|
994
|
+
summary: packaged.summary,
|
|
995
|
+
sourceEventCount: selected.sourceEventCount,
|
|
996
|
+
estimatedTokens: packaged.estimatedTokens,
|
|
997
|
+
});
|
|
998
|
+
return { selected, packaged, result };
|
|
999
|
+
};
|
|
1000
|
+
let packagedResult;
|
|
1001
|
+
if (source) {
|
|
1002
|
+
packagedResult = await source.runExclusive(packageAndPublish);
|
|
1003
|
+
}
|
|
1004
|
+
else {
|
|
1005
|
+
if (this.running)
|
|
1006
|
+
throw new Error("source session is still producing or has queued turns");
|
|
1007
|
+
this.running = true;
|
|
1008
|
+
this.abortController = new AbortController();
|
|
1009
|
+
try {
|
|
1010
|
+
packagedResult = await packageAndPublish(this.abortController.signal);
|
|
1011
|
+
}
|
|
1012
|
+
finally {
|
|
1013
|
+
this.abortController = null;
|
|
1014
|
+
this.running = false;
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
const { packaged, result } = packagedResult;
|
|
1018
|
+
const workspace = result.bundle.state.workspace ?? {
|
|
1019
|
+
root: result.bundle.state.cwd,
|
|
1020
|
+
kind: "main",
|
|
1021
|
+
};
|
|
1022
|
+
this.transport.send(createResponse(req.id, {
|
|
1023
|
+
sessionId: result.bundle.state.sessionId,
|
|
1024
|
+
mode: "summary",
|
|
1025
|
+
summary: packaged.summary,
|
|
1026
|
+
sourceRange,
|
|
1027
|
+
estimatedTokens: packaged.estimatedTokens,
|
|
1028
|
+
forkedFrom: result.lineage,
|
|
1029
|
+
workspace,
|
|
1030
|
+
}));
|
|
1031
|
+
return;
|
|
1032
|
+
}
|
|
1033
|
+
const result = engine.forkSession(sourceSessionId, isSideFork
|
|
1034
|
+
? { targetSessionId, snapshotMode: "completed", ephemeral: true }
|
|
1035
|
+
: { targetSessionId, throughEventId });
|
|
1036
|
+
const workspace = result.bundle.state.workspace ?? {
|
|
1037
|
+
root: result.bundle.state.cwd,
|
|
1038
|
+
kind: "main",
|
|
1039
|
+
};
|
|
1040
|
+
this.transport.send(createResponse(req.id, {
|
|
1041
|
+
sessionId: result.bundle.state.sessionId,
|
|
1042
|
+
mode: "full",
|
|
1043
|
+
forkedFrom: result.lineage,
|
|
1044
|
+
workspace,
|
|
1045
|
+
copiedEventCount: result.copiedEventCount,
|
|
1046
|
+
}));
|
|
1047
|
+
}
|
|
1048
|
+
catch (err) {
|
|
1049
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
1050
|
+
const code = /not found/i.test(message)
|
|
1051
|
+
? ErrorCodes.SessionNotFound
|
|
1052
|
+
: /still producing|queued turns|busy/i.test(message)
|
|
1053
|
+
? ErrorCodes.Overloaded
|
|
1054
|
+
: /already exists|invalid|cursor|metadata|unfinished|orphaned|unsupported|malformed/i.test(message)
|
|
1055
|
+
? ErrorCodes.InvalidParams
|
|
1056
|
+
: ErrorCodes.InternalError;
|
|
1057
|
+
this.transport.send(createErrorResponse(req.id, code, message));
|
|
1058
|
+
}
|
|
1059
|
+
}
|
|
374
1060
|
async handleRun(req) {
|
|
375
1061
|
// ── ChatSessionManager path (multi-session) ──────────────────
|
|
376
1062
|
if (this.chatManager) {
|
|
@@ -386,12 +1072,12 @@ export class AgentServer {
|
|
|
386
1072
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "sessionId is required"));
|
|
387
1073
|
return;
|
|
388
1074
|
}
|
|
389
|
-
|
|
390
|
-
|
|
1075
|
+
const inputError = this.validateRunInput(params);
|
|
1076
|
+
if (inputError) {
|
|
1077
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, inputError));
|
|
391
1078
|
return;
|
|
392
1079
|
}
|
|
393
1080
|
const sessionConfig = {
|
|
394
|
-
permissionMode: params.permissionMode,
|
|
395
1081
|
cwd: params.cwd,
|
|
396
1082
|
projectTrusted: params.projectTrusted,
|
|
397
1083
|
goal: typeof params.goal === "string" || (params.goal != null && typeof params.goal === "object")
|
|
@@ -399,7 +1085,7 @@ export class AgentServer {
|
|
|
399
1085
|
: undefined,
|
|
400
1086
|
};
|
|
401
1087
|
if (params.requireExisting === true && !cm.get(params.sessionId)) {
|
|
402
|
-
let existsOnDisk
|
|
1088
|
+
let existsOnDisk;
|
|
403
1089
|
try {
|
|
404
1090
|
existsOnDisk = cm.sessionExistsOnDisk(params.sessionId, sessionConfig);
|
|
405
1091
|
}
|
|
@@ -415,13 +1101,20 @@ export class AgentServer {
|
|
|
415
1101
|
}
|
|
416
1102
|
let session;
|
|
417
1103
|
try {
|
|
418
|
-
session = cm.getOrCreate(params.sessionId, sessionConfig);
|
|
1104
|
+
session = await cm.getOrCreate(params.sessionId, sessionConfig);
|
|
419
1105
|
}
|
|
420
1106
|
catch (err) {
|
|
421
1107
|
const code = err.code ?? ErrorCodes.InternalError;
|
|
422
1108
|
this.transport.send(createErrorResponse(req.id, code, err.message));
|
|
423
1109
|
return;
|
|
424
1110
|
}
|
|
1111
|
+
const sid = params.sessionId;
|
|
1112
|
+
this.observeSessionAttached(sid, session.lastActivityAt);
|
|
1113
|
+
const approvalRegistration = this.approvalRouter.register(sid, this.connectionId);
|
|
1114
|
+
if (!approvalRegistration.ok) {
|
|
1115
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.Overloaded, `Session ${sid} is owned by another connection`));
|
|
1116
|
+
return;
|
|
1117
|
+
}
|
|
425
1118
|
this.rememberSessionSlice(params.sessionId, sessionConfig);
|
|
426
1119
|
if (params.model !== undefined) {
|
|
427
1120
|
if (typeof params.model !== "string" || params.model.length === 0) {
|
|
@@ -436,31 +1129,61 @@ export class AgentServer {
|
|
|
436
1129
|
return;
|
|
437
1130
|
}
|
|
438
1131
|
}
|
|
439
|
-
if (typeof params.planMode === "boolean") {
|
|
440
|
-
session.engine.setPlanMode(params.planMode);
|
|
441
|
-
}
|
|
442
|
-
const sid = params.sessionId;
|
|
443
1132
|
// Wire AskUserQuestion and host bridges for this interactive session. The
|
|
444
1133
|
// chatManager path builds a fresh per-session Engine via engineFactory, so
|
|
445
1134
|
// these host callbacks must be installed after every create/recreate.
|
|
446
1135
|
this.wireInteractiveSession(session, sid);
|
|
447
1136
|
try {
|
|
448
|
-
const
|
|
1137
|
+
const displayText = typeof params.displayText === "string" ? params.displayText.trim() : "";
|
|
1138
|
+
if (displayText) {
|
|
1139
|
+
const userMessageEvent = {
|
|
1140
|
+
type: "session_user_message",
|
|
1141
|
+
text: displayText,
|
|
1142
|
+
...(typeof params.clientMessageId === "string"
|
|
1143
|
+
? { clientMessageId: params.clientMessageId }
|
|
1144
|
+
: {}),
|
|
1145
|
+
};
|
|
1146
|
+
this.observeSessionStream(sid, userMessageEvent);
|
|
1147
|
+
this.notify(Methods.StreamEvent, { sessionId: sid, event: userMessageEvent });
|
|
1148
|
+
}
|
|
1149
|
+
const run = session.enqueueTurn(params.task, {
|
|
449
1150
|
cwd: params.cwd,
|
|
1151
|
+
displayText: displayText || undefined,
|
|
1152
|
+
injected: params.injected === true,
|
|
450
1153
|
attachments: Array.isArray(params.attachments) ? params.attachments : undefined,
|
|
451
1154
|
goal: typeof params.goal === "string" ||
|
|
452
1155
|
(params.goal != null && typeof params.goal === "object")
|
|
453
1156
|
? params.goal
|
|
454
1157
|
: undefined,
|
|
455
|
-
onStream: (event) =>
|
|
1158
|
+
onStream: (event) => {
|
|
1159
|
+
this.observeSessionStream(sid, event);
|
|
1160
|
+
this.notify(Methods.StreamEvent, { sessionId: sid, event });
|
|
1161
|
+
},
|
|
456
1162
|
clientMessageId: typeof params.clientMessageId === "string" ? params.clientMessageId : undefined,
|
|
1163
|
+
permissionMode: params.permissionMode,
|
|
1164
|
+
planMode: params.planMode,
|
|
1165
|
+
behaviorMode: params.behaviorMode,
|
|
1166
|
+
profileParams: params.profileParams,
|
|
1167
|
+
workspaceProfile: params.workspaceProfile,
|
|
1168
|
+
sessionBrief: params.sessionBrief,
|
|
1169
|
+
sessionMessageTargets: params.sessionMessageTargets,
|
|
1170
|
+
petRuntimeContext: params.petRuntimeContext,
|
|
1171
|
+
petWorkspaces: params.petWorkspaces,
|
|
1172
|
+
kind: params.kind,
|
|
1173
|
+
approvalRouter: this.approvalRouter,
|
|
457
1174
|
});
|
|
1175
|
+
this.notify(Methods.RunAccepted, { requestId: req.id, sessionId: sid });
|
|
1176
|
+
this.observeRunBoundary(sid, "start");
|
|
1177
|
+
const result = await run;
|
|
1178
|
+
this.observeRunBoundary(sid, "end");
|
|
458
1179
|
const runResult = {
|
|
459
1180
|
text: result.text,
|
|
460
1181
|
reason: result.reason,
|
|
461
1182
|
sessionId: result.sessionId ?? sid,
|
|
462
1183
|
turnCount: result.turnCount,
|
|
463
1184
|
usage: result.usage,
|
|
1185
|
+
...(result.extensions ? { extensions: result.extensions } : {}),
|
|
1186
|
+
petWorkDelegation: result.petWorkDelegation,
|
|
464
1187
|
};
|
|
465
1188
|
this.transport.send(createResponse(req.id, runResult));
|
|
466
1189
|
// Run-boundary re-check (trigger B): the session is idle now. If a
|
|
@@ -471,6 +1194,7 @@ export class AgentServer {
|
|
|
471
1194
|
this.maybeWakeIdleSession(sid);
|
|
472
1195
|
}
|
|
473
1196
|
catch (err) {
|
|
1197
|
+
this.observeRunBoundary(sid, "error");
|
|
474
1198
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
|
|
475
1199
|
// Even on a failed run the session goes idle — re-check so a background
|
|
476
1200
|
// completion that landed during the failed run isn't orphaned.
|
|
@@ -483,10 +1207,18 @@ export class AgentServer {
|
|
|
483
1207
|
return;
|
|
484
1208
|
}
|
|
485
1209
|
const params = (req.params ?? {});
|
|
486
|
-
|
|
487
|
-
|
|
1210
|
+
const inputError = this.validateRunInput(params);
|
|
1211
|
+
if (inputError) {
|
|
1212
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, inputError));
|
|
488
1213
|
return;
|
|
489
1214
|
}
|
|
1215
|
+
if (typeof params.sessionId === "string" && params.sessionId.length > 0) {
|
|
1216
|
+
const approvalRegistration = this.approvalRouter.register(params.sessionId, this.connectionId);
|
|
1217
|
+
if (!approvalRegistration.ok) {
|
|
1218
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.Overloaded, `Session ${params.sessionId} is owned by another connection`));
|
|
1219
|
+
return;
|
|
1220
|
+
}
|
|
1221
|
+
}
|
|
490
1222
|
if (params.cwd !== undefined && (typeof params.cwd !== "string" || params.cwd.length === 0)) {
|
|
491
1223
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "cwd must be a non-empty string"));
|
|
492
1224
|
return;
|
|
@@ -528,16 +1260,43 @@ export class AgentServer {
|
|
|
528
1260
|
// `signal.aborted` below it's already gone.
|
|
529
1261
|
const runController = this.abortController;
|
|
530
1262
|
try {
|
|
1263
|
+
const displayText = typeof params.displayText === "string" ? params.displayText.trim() : "";
|
|
1264
|
+
if (displayText) {
|
|
1265
|
+
streamToClient({
|
|
1266
|
+
type: "session_user_message",
|
|
1267
|
+
text: displayText,
|
|
1268
|
+
...(typeof params.clientMessageId === "string"
|
|
1269
|
+
? { clientMessageId: params.clientMessageId }
|
|
1270
|
+
: {}),
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
this.notify(Methods.RunAccepted, {
|
|
1274
|
+
requestId: req.id,
|
|
1275
|
+
sessionId: params.sessionId ?? "",
|
|
1276
|
+
});
|
|
531
1277
|
const result = await this.legacyEngine.run(params.task, {
|
|
532
1278
|
cwd: params.cwd,
|
|
533
1279
|
sessionId: params.sessionId,
|
|
1280
|
+
displayText: displayText || undefined,
|
|
1281
|
+
injected: params.injected === true,
|
|
534
1282
|
signal: runController.signal,
|
|
535
1283
|
onStream: streamToClient,
|
|
536
1284
|
clientMessageId: typeof params.clientMessageId === "string" ? params.clientMessageId : undefined,
|
|
1285
|
+
attachments: Array.isArray(params.attachments) ? params.attachments : undefined,
|
|
537
1286
|
goal: typeof params.goal === "string" ||
|
|
538
1287
|
(params.goal != null && typeof params.goal === "object")
|
|
539
1288
|
? params.goal
|
|
540
1289
|
: undefined,
|
|
1290
|
+
permissionMode: params.permissionMode,
|
|
1291
|
+
planMode: params.planMode,
|
|
1292
|
+
behaviorMode: params.behaviorMode,
|
|
1293
|
+
profileParams: params.profileParams,
|
|
1294
|
+
workspaceProfile: params.workspaceProfile,
|
|
1295
|
+
sessionBrief: params.sessionBrief,
|
|
1296
|
+
sessionMessageTargets: params.sessionMessageTargets,
|
|
1297
|
+
petRuntimeContext: params.petRuntimeContext,
|
|
1298
|
+
petWorkspaces: params.petWorkspaces,
|
|
1299
|
+
kind: params.kind,
|
|
541
1300
|
});
|
|
542
1301
|
const runResult = {
|
|
543
1302
|
text: result.text,
|
|
@@ -545,6 +1304,8 @@ export class AgentServer {
|
|
|
545
1304
|
sessionId: result.sessionId,
|
|
546
1305
|
turnCount: result.turnCount,
|
|
547
1306
|
usage: result.usage,
|
|
1307
|
+
...(result.extensions ? { extensions: result.extensions } : {}),
|
|
1308
|
+
petWorkDelegation: result.petWorkDelegation,
|
|
548
1309
|
};
|
|
549
1310
|
this.transport.send(createResponse(req.id, runResult));
|
|
550
1311
|
}
|
|
@@ -579,6 +1340,19 @@ export class AgentServer {
|
|
|
579
1340
|
// ─── Approve ────────────────────────────────────────────────────
|
|
580
1341
|
handleApprove(req) {
|
|
581
1342
|
const params = (req.params ?? {});
|
|
1343
|
+
if (this.strictApprovalRouting) {
|
|
1344
|
+
const pending = this.pendingApprovalTargets.get(params.requestId);
|
|
1345
|
+
const matchesPending = pending !== undefined &&
|
|
1346
|
+
params.connectionId === this.connectionId &&
|
|
1347
|
+
params.sessionId === pending.sessionId &&
|
|
1348
|
+
params.connectionId === pending.connectionId &&
|
|
1349
|
+
params.generation === pending.generation &&
|
|
1350
|
+
this.approvalRouter.matches(pending);
|
|
1351
|
+
if (!matchesPending) {
|
|
1352
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "Approval response does not match connectionId, sessionId, generation, and requestId"));
|
|
1353
|
+
return;
|
|
1354
|
+
}
|
|
1355
|
+
}
|
|
582
1356
|
// ChatSessionManager path: approvals are scoped by (sessionId, requestId).
|
|
583
1357
|
// Never fall back from a session-tagged response to the legacy global map:
|
|
584
1358
|
// a stale/misrouted UI response must fail closed instead of resolving a
|
|
@@ -589,17 +1363,19 @@ export class AgentServer {
|
|
|
589
1363
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionClosed, `No such session: ${params.sessionId}`));
|
|
590
1364
|
return;
|
|
591
1365
|
}
|
|
592
|
-
const
|
|
593
|
-
if (!
|
|
1366
|
+
const entry = s.pendingApprovals.get(params.requestId);
|
|
1367
|
+
if (!entry) {
|
|
594
1368
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, `No pending approval for session ${params.sessionId}: ${params.requestId}`));
|
|
595
1369
|
return;
|
|
596
1370
|
}
|
|
597
1371
|
s.pendingApprovals.delete(params.requestId);
|
|
1372
|
+
this.observeApprovalTransition(entry.metadata, "resolved");
|
|
1373
|
+
this.pendingApprovalTargets.delete(params.requestId);
|
|
598
1374
|
// Cancel the pending timeout for requests that arm one (browser actions,
|
|
599
1375
|
// credential injection, and tool approvals). AskUserQuestion does not use
|
|
600
1376
|
// a timeout; this is harmless for those request ids.
|
|
601
1377
|
this.clearApprovalTimer(params.requestId);
|
|
602
|
-
resolve(params.decision);
|
|
1378
|
+
entry.resolve(params.decision);
|
|
603
1379
|
this.transport.send(createResponse(req.id, { ok: true }));
|
|
604
1380
|
return;
|
|
605
1381
|
}
|
|
@@ -610,6 +1386,7 @@ export class AgentServer {
|
|
|
610
1386
|
return;
|
|
611
1387
|
}
|
|
612
1388
|
this.pendingApprovals.delete(params.requestId);
|
|
1389
|
+
this.pendingApprovalTargets.delete(params.requestId);
|
|
613
1390
|
this.clearApprovalTimer(params.requestId);
|
|
614
1391
|
resolve(params.decision);
|
|
615
1392
|
this.transport.send(createResponse(req.id, { ok: true }));
|
|
@@ -690,37 +1467,260 @@ export class AgentServer {
|
|
|
690
1467
|
}
|
|
691
1468
|
this.transport.send(createResponse(req.id, { ok: true, limits: result }));
|
|
692
1469
|
}
|
|
1470
|
+
/** Reject destructive Goal controls owned by a different TCP connection. */
|
|
1471
|
+
authorizeGoalControl(req, sessionId) {
|
|
1472
|
+
if (!this.strictApprovalRouting)
|
|
1473
|
+
return true;
|
|
1474
|
+
const owner = this.approvalRouter.current(sessionId);
|
|
1475
|
+
if (!owner || owner.connectionId === this.connectionId)
|
|
1476
|
+
return true;
|
|
1477
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.Overloaded, `Session ${sessionId} is owned by another connection`));
|
|
1478
|
+
return false;
|
|
1479
|
+
}
|
|
1480
|
+
/** Start a fully rebuilt Goal run for an idle or goal-less in-flight session. */
|
|
1481
|
+
async queueGoalResumeTurn(sessionId, goal, existing) {
|
|
1482
|
+
if (!this.chatManager)
|
|
1483
|
+
return null;
|
|
1484
|
+
let session = existing;
|
|
1485
|
+
if (!session) {
|
|
1486
|
+
// Let ChatSessionManager probe through its configured Engine factory so
|
|
1487
|
+
// custom sessionStorageDir roots stay aligned with the host. The generic
|
|
1488
|
+
// background-wakeup helper's default SessionManager probe is not safe for
|
|
1489
|
+
// this cold-resume path.
|
|
1490
|
+
session =
|
|
1491
|
+
(await this.chatManager.getOrCreatePersisted(sessionId, {
|
|
1492
|
+
projectTrusted: false,
|
|
1493
|
+
})) ?? undefined;
|
|
1494
|
+
if (!session)
|
|
1495
|
+
return null;
|
|
1496
|
+
this.wireInteractiveSession(session, sessionId);
|
|
1497
|
+
}
|
|
1498
|
+
if (session.engine.isHeadless())
|
|
1499
|
+
return null;
|
|
1500
|
+
return {
|
|
1501
|
+
completion: session.enqueueGoalResumeTurn(goal, {
|
|
1502
|
+
onStream: (event) => this.notify(Methods.StreamEvent, { sessionId, event }),
|
|
1503
|
+
approvalRouter: this.approvalRouter,
|
|
1504
|
+
}),
|
|
1505
|
+
};
|
|
1506
|
+
}
|
|
1507
|
+
reconcileFailedGoalResume(sessionId, goal, claimedOwner, error) {
|
|
1508
|
+
const patch = {
|
|
1509
|
+
paused: true,
|
|
1510
|
+
expectedGoalId: goal.goalId,
|
|
1511
|
+
expectedRevision: goal.revision,
|
|
1512
|
+
};
|
|
1513
|
+
const live = this.chatManager?.get(sessionId);
|
|
1514
|
+
const rolledBack = live
|
|
1515
|
+
? live.updateGoal(patch)
|
|
1516
|
+
: this.updateActiveGoalOnDisk?.(sessionId, patch);
|
|
1517
|
+
const authoritative = rolledBack ?? live?.getGoal() ?? this.readActiveGoalFromDisk?.(sessionId);
|
|
1518
|
+
if (authoritative) {
|
|
1519
|
+
this.notify(Methods.StreamEvent, {
|
|
1520
|
+
sessionId,
|
|
1521
|
+
event: {
|
|
1522
|
+
type: "goal_updated",
|
|
1523
|
+
goalId: authoritative.goalId,
|
|
1524
|
+
revision: authoritative.revision,
|
|
1525
|
+
objective: authoritative.objective,
|
|
1526
|
+
paused: authoritative.paused === true,
|
|
1527
|
+
},
|
|
1528
|
+
});
|
|
1529
|
+
}
|
|
1530
|
+
if (rolledBack && claimedOwner && this.approvalRouter.matches(claimedOwner)) {
|
|
1531
|
+
this.approvalRouter.release(sessionId, this.connectionId, "goal resume turn was not queued");
|
|
1532
|
+
}
|
|
1533
|
+
logger.warn("goal.resume_turn_not_queued", {
|
|
1534
|
+
sessionId,
|
|
1535
|
+
goalId: goal.goalId,
|
|
1536
|
+
revision: goal.revision,
|
|
1537
|
+
error: error instanceof Error ? error.message : String(error),
|
|
1538
|
+
});
|
|
1539
|
+
this.notify(Methods.StreamEvent, {
|
|
1540
|
+
sessionId,
|
|
1541
|
+
event: {
|
|
1542
|
+
type: "error",
|
|
1543
|
+
error: rolledBack
|
|
1544
|
+
? "Goal resume could not start; the Goal was paused again"
|
|
1545
|
+
: "Goal resume could not start; Goal state changed before rollback",
|
|
1546
|
+
},
|
|
1547
|
+
});
|
|
1548
|
+
}
|
|
1549
|
+
/** Edit or pause/resume a persisted goal without replacing its identity. */
|
|
1550
|
+
async handleGoalUpdate(req) {
|
|
1551
|
+
const params = (req.params ?? {});
|
|
1552
|
+
if (typeof params.sessionId !== "string" || !params.sessionId) {
|
|
1553
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "sessionId is required"));
|
|
1554
|
+
return;
|
|
1555
|
+
}
|
|
1556
|
+
if (params.objective === undefined && params.paused === undefined) {
|
|
1557
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "objective or paused is required"));
|
|
1558
|
+
return;
|
|
1559
|
+
}
|
|
1560
|
+
if (params.objective !== undefined && typeof params.objective !== "string") {
|
|
1561
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "objective must be a string"));
|
|
1562
|
+
return;
|
|
1563
|
+
}
|
|
1564
|
+
if (params.objective !== undefined && !params.objective.trim()) {
|
|
1565
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "objective must not be blank"));
|
|
1566
|
+
return;
|
|
1567
|
+
}
|
|
1568
|
+
if (params.paused !== undefined && typeof params.paused !== "boolean") {
|
|
1569
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "paused must be boolean"));
|
|
1570
|
+
return;
|
|
1571
|
+
}
|
|
1572
|
+
if (typeof params.expectedGoalId !== "string" || !params.expectedGoalId) {
|
|
1573
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "expectedGoalId is required and must be a non-empty string"));
|
|
1574
|
+
return;
|
|
1575
|
+
}
|
|
1576
|
+
if (typeof params.expectedRevision !== "number" ||
|
|
1577
|
+
!Number.isInteger(params.expectedRevision) ||
|
|
1578
|
+
params.expectedRevision < 1) {
|
|
1579
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "expectedRevision is required and must be a positive integer"));
|
|
1580
|
+
return;
|
|
1581
|
+
}
|
|
1582
|
+
const patch = {
|
|
1583
|
+
objective: params.objective,
|
|
1584
|
+
paused: params.paused,
|
|
1585
|
+
expectedGoalId: params.expectedGoalId,
|
|
1586
|
+
expectedRevision: params.expectedRevision,
|
|
1587
|
+
};
|
|
1588
|
+
const session = this.chatManager?.get(params.sessionId);
|
|
1589
|
+
const beforeGoal = session?.getGoal?.() ??
|
|
1590
|
+
this.legacyEngine?.getGoal(params.sessionId) ??
|
|
1591
|
+
this.readActiveGoalFromDisk?.(params.sessionId);
|
|
1592
|
+
const resumeInPlace = session?.canResumeGoalInPlace?.() === true;
|
|
1593
|
+
// `paused:false` is also an explicit kick for an unpaused-but-idle Goal
|
|
1594
|
+
// left behind by ESC, a model failure, or worker restart. A live Goal run
|
|
1595
|
+
// consumes the update in place and must not enqueue a duplicate turn.
|
|
1596
|
+
const driveRequested = !!beforeGoal && params.paused === false && !resumeInPlace;
|
|
1597
|
+
if (!this.authorizeGoalControl(req, params.sessionId))
|
|
1598
|
+
return;
|
|
1599
|
+
if (driveRequested && !this.chatManager) {
|
|
1600
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "Goal resume requires a multi-session host that can schedule a continuation turn"));
|
|
1601
|
+
return;
|
|
1602
|
+
}
|
|
1603
|
+
let claimedResumeOwner = null;
|
|
1604
|
+
if (this.strictApprovalRouting &&
|
|
1605
|
+
driveRequested &&
|
|
1606
|
+
!this.approvalRouter.current(params.sessionId)) {
|
|
1607
|
+
const registration = this.approvalRouter.register(params.sessionId, this.connectionId);
|
|
1608
|
+
if (!registration.ok) {
|
|
1609
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.Overloaded, `Session ${params.sessionId} is owned by another connection`));
|
|
1610
|
+
return;
|
|
1611
|
+
}
|
|
1612
|
+
claimedResumeOwner = registration.target;
|
|
1613
|
+
}
|
|
1614
|
+
const goal = session
|
|
1615
|
+
? session.updateGoal(patch)
|
|
1616
|
+
: this.legacyEngine
|
|
1617
|
+
? this.legacyEngine.updateGoal(params.sessionId, patch)
|
|
1618
|
+
: this.updateActiveGoalOnDisk?.(params.sessionId, patch);
|
|
1619
|
+
if (!goal) {
|
|
1620
|
+
if (claimedResumeOwner && this.approvalRouter.matches(claimedResumeOwner)) {
|
|
1621
|
+
this.approvalRouter.release(params.sessionId, this.connectionId, "goal resume CAS did not apply");
|
|
1622
|
+
}
|
|
1623
|
+
// A missing/stale goal is an ordinary optimistic-concurrency miss, not a
|
|
1624
|
+
// malformed request. Keep it in the result channel so Desktop, TUI and
|
|
1625
|
+
// the no-worker fallback share one contract.
|
|
1626
|
+
this.transport.send(createResponse(req.id, { ok: true, updated: false }));
|
|
1627
|
+
return;
|
|
1628
|
+
}
|
|
1629
|
+
this.notify(Methods.StreamEvent, {
|
|
1630
|
+
sessionId: params.sessionId,
|
|
1631
|
+
event: {
|
|
1632
|
+
type: "goal_updated",
|
|
1633
|
+
...(goal.goalId ? { goalId: goal.goalId } : {}),
|
|
1634
|
+
...(goal.revision ? { revision: goal.revision } : {}),
|
|
1635
|
+
objective: goal.objective,
|
|
1636
|
+
paused: goal.paused === true,
|
|
1637
|
+
},
|
|
1638
|
+
});
|
|
1639
|
+
this.transport.send(createResponse(req.id, {
|
|
1640
|
+
ok: true,
|
|
1641
|
+
updated: true,
|
|
1642
|
+
goal: goal.objective,
|
|
1643
|
+
...(goal.goalId ? { goalId: goal.goalId } : {}),
|
|
1644
|
+
...(goal.revision ? { revision: goal.revision } : {}),
|
|
1645
|
+
paused: goal.paused === true,
|
|
1646
|
+
}));
|
|
1647
|
+
if (driveRequested) {
|
|
1648
|
+
void this.queueGoalResumeTurn(params.sessionId, goal, session)
|
|
1649
|
+
.then((ticket) => {
|
|
1650
|
+
if (!ticket) {
|
|
1651
|
+
this.reconcileFailedGoalResume(params.sessionId, goal, claimedResumeOwner, "session is unavailable or headless");
|
|
1652
|
+
return;
|
|
1653
|
+
}
|
|
1654
|
+
void ticket.completion.catch((err) => {
|
|
1655
|
+
this.reconcileFailedGoalResume(params.sessionId, goal, claimedResumeOwner, err);
|
|
1656
|
+
});
|
|
1657
|
+
})
|
|
1658
|
+
.catch((err) => {
|
|
1659
|
+
this.reconcileFailedGoalResume(params.sessionId, goal, claimedResumeOwner, err);
|
|
1660
|
+
});
|
|
1661
|
+
}
|
|
1662
|
+
}
|
|
693
1663
|
/**
|
|
694
1664
|
* Clear a session's persisted active goal (CC /goal clear). Routes to the
|
|
695
1665
|
* named session, falling back to the legacy single engine (mirrors
|
|
696
1666
|
* handleGoalExtend / handleCancel's dual path). Returns { ok, cleared }.
|
|
697
1667
|
*/
|
|
698
|
-
handleGoalClear(req) {
|
|
1668
|
+
handleGoalClear(req, deleteAlias = false) {
|
|
699
1669
|
const params = (req.params ?? {});
|
|
1670
|
+
if (typeof params.sessionId !== "string" || !params.sessionId) {
|
|
1671
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "sessionId is required"));
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1674
|
+
if (deleteAlias &&
|
|
1675
|
+
(typeof params.expectedGoalId !== "string" ||
|
|
1676
|
+
!params.expectedGoalId ||
|
|
1677
|
+
typeof params.expectedRevision !== "number" ||
|
|
1678
|
+
!Number.isInteger(params.expectedRevision) ||
|
|
1679
|
+
params.expectedRevision < 1)) {
|
|
1680
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "goalDelete requires expectedGoalId and a positive expectedRevision"));
|
|
1681
|
+
return;
|
|
1682
|
+
}
|
|
1683
|
+
if (!this.authorizeGoalControl(req, params.sessionId))
|
|
1684
|
+
return;
|
|
1685
|
+
const expected = deleteAlias
|
|
1686
|
+
? { goalId: params.expectedGoalId, revision: params.expectedRevision }
|
|
1687
|
+
: undefined;
|
|
700
1688
|
const session = this.chatManager && typeof params.sessionId === "string"
|
|
701
1689
|
? this.chatManager.get(params.sessionId)
|
|
702
1690
|
: undefined;
|
|
703
|
-
if (!session && !this.legacyEngine) {
|
|
1691
|
+
if (!session && !this.legacyEngine && !this.clearActiveGoalOnDisk) {
|
|
704
1692
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionClosed, params.sessionId ? `No such session: ${params.sessionId}` : "sessionId is required"));
|
|
705
1693
|
return;
|
|
706
1694
|
}
|
|
1695
|
+
const clearingGoal = session
|
|
1696
|
+
? session.getGoal?.()
|
|
1697
|
+
: params.sessionId
|
|
1698
|
+
? (this.legacyEngine?.getGoal(params.sessionId) ??
|
|
1699
|
+
this.readActiveGoalFromDisk?.(params.sessionId))
|
|
1700
|
+
: undefined;
|
|
707
1701
|
const cleared = session
|
|
708
|
-
? session.clearGoal()
|
|
1702
|
+
? session.clearGoal(expected)
|
|
709
1703
|
: params.sessionId
|
|
710
|
-
? (this.legacyEngine
|
|
1704
|
+
? (this.legacyEngine?.clearGoal(params.sessionId, expected) ??
|
|
1705
|
+
this.clearActiveGoalOnDisk?.(params.sessionId, expected) ??
|
|
1706
|
+
false)
|
|
711
1707
|
: false;
|
|
712
1708
|
if (cleared && typeof params.sessionId === "string" && params.sessionId.length > 0) {
|
|
713
1709
|
this.notify(Methods.StreamEvent, {
|
|
714
1710
|
sessionId: params.sessionId,
|
|
715
|
-
event: {
|
|
1711
|
+
event: {
|
|
1712
|
+
type: "goal_cleared",
|
|
1713
|
+
...(clearingGoal?.goalId ? { goalId: clearingGoal.goalId } : {}),
|
|
1714
|
+
...(clearingGoal?.revision ? { revision: clearingGoal.revision } : {}),
|
|
1715
|
+
},
|
|
716
1716
|
});
|
|
717
1717
|
}
|
|
718
|
-
this.transport.send(createResponse(req.id, { ok: true, cleared }));
|
|
1718
|
+
this.transport.send(createResponse(req.id, deleteAlias ? { ok: true, deleted: cleared } : { ok: true, cleared }));
|
|
719
1719
|
}
|
|
720
1720
|
/**
|
|
721
1721
|
* Read a session's persisted active goal so the host can re-surface the goal
|
|
722
1722
|
* block + Cancel button on session load. A persistent goal lives only in
|
|
723
|
-
* state.
|
|
1723
|
+
* state.goalLifecycle and is never replayed from the transcript, so a reloaded
|
|
724
1724
|
* (or disk-rebuilt) session has no other way to learn it. Prefers a live
|
|
725
1725
|
* chatManager session, else falls through to a disk read (readActiveGoalFromDisk
|
|
726
1726
|
* in worker mode, or the legacy engine's disk read in single-engine mode) — the
|
|
@@ -749,7 +1749,13 @@ export class AgentServer {
|
|
|
749
1749
|
: (this.readActiveGoalFromDisk?.(params.sessionId) ??
|
|
750
1750
|
this.legacyEngine?.getGoal(params.sessionId) ??
|
|
751
1751
|
undefined);
|
|
752
|
-
this.transport.send(createResponse(req.id, {
|
|
1752
|
+
this.transport.send(createResponse(req.id, {
|
|
1753
|
+
ok: true,
|
|
1754
|
+
goal: goal ? goal.objective : null,
|
|
1755
|
+
...(goal?.goalId ? { goalId: goal.goalId } : {}),
|
|
1756
|
+
...(goal?.revision ? { revision: goal.revision } : {}),
|
|
1757
|
+
paused: goal?.paused === true,
|
|
1758
|
+
}));
|
|
753
1759
|
}
|
|
754
1760
|
/**
|
|
755
1761
|
* Query/control a session's background shells for the desktop UI panel (TODO
|
|
@@ -817,7 +1823,7 @@ export class AgentServer {
|
|
|
817
1823
|
this.transport.send(createResponse(req.id, { items }));
|
|
818
1824
|
}
|
|
819
1825
|
// ─── CloseSession ───────────────────────────────────────────────
|
|
820
|
-
handleCloseSession(req) {
|
|
1826
|
+
async handleCloseSession(req) {
|
|
821
1827
|
const params = (req.params ?? {});
|
|
822
1828
|
if (typeof params.sessionId !== "string" || params.sessionId.length === 0) {
|
|
823
1829
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "sessionId is required"));
|
|
@@ -826,15 +1832,18 @@ export class AgentServer {
|
|
|
826
1832
|
if (this.chatManager) {
|
|
827
1833
|
const session = this.chatManager.get(params.sessionId);
|
|
828
1834
|
if (session) {
|
|
829
|
-
this.cancelSessionApprovals(session, "session closed");
|
|
1835
|
+
this.cancelSessionApprovals(session, "session closed", "cancelled", "session_closed");
|
|
830
1836
|
}
|
|
831
|
-
this.
|
|
1837
|
+
this.approvalRouter.release(params.sessionId, this.connectionId, "session closed");
|
|
1838
|
+
await this.chatManager.close(params.sessionId);
|
|
1839
|
+
this.observeSessionClosed(params.sessionId);
|
|
832
1840
|
}
|
|
833
1841
|
// Explicit session teardown — reap that session's background shells
|
|
834
1842
|
// (design §6 "session 被显式删除 → killSession"). This is the RPC path
|
|
835
1843
|
// (agent/closeSession from the host on delete), distinct from the idle
|
|
836
|
-
// sweeper's
|
|
837
|
-
|
|
1844
|
+
// sweeper's idle eviction which must NOT kill (§6). Await teardown so the
|
|
1845
|
+
// close RPC is a real deletion barrier for Desktop.
|
|
1846
|
+
await backgroundShellManager.killSession(params.sessionId);
|
|
838
1847
|
// Drop this session's retained background jobs too (#2/#5): finished jobs
|
|
839
1848
|
// are kept for the panel, so explicit teardown is where they're released.
|
|
840
1849
|
backgroundJobRegistry.dropForSession(params.sessionId);
|
|
@@ -862,6 +1871,33 @@ export class AgentServer {
|
|
|
862
1871
|
const workspace = engine?.releaseSessionWorkspace?.(params.sessionId) ?? null;
|
|
863
1872
|
this.transport.send(createResponse(req.id, { ok: true, workspace }));
|
|
864
1873
|
}
|
|
1874
|
+
handleSetWorkspace(req) {
|
|
1875
|
+
const params = (req.params ?? {});
|
|
1876
|
+
if (typeof params.sessionId !== "string" || params.sessionId.length === 0) {
|
|
1877
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "sessionId is required"));
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1880
|
+
if (!params.workspace ||
|
|
1881
|
+
typeof params.workspace !== "object" ||
|
|
1882
|
+
typeof params.workspace.root !== "string" ||
|
|
1883
|
+
params.workspace.root.length === 0 ||
|
|
1884
|
+
(params.workspace.kind !== "main" && params.workspace.kind !== "worktree")) {
|
|
1885
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "valid workspace is required"));
|
|
1886
|
+
return;
|
|
1887
|
+
}
|
|
1888
|
+
const engine = this.chatManager
|
|
1889
|
+
? this.chatManager.get(params.sessionId)?.engine
|
|
1890
|
+
: this.legacyEngine;
|
|
1891
|
+
if (!engine) {
|
|
1892
|
+
this.transport.send(createResponse(req.id, { ok: true, workspace: null }));
|
|
1893
|
+
return;
|
|
1894
|
+
}
|
|
1895
|
+
const workspace = engine.setSessionWorkspace?.(params.sessionId, params.workspace);
|
|
1896
|
+
this.transport.send(createResponse(req.id, {
|
|
1897
|
+
ok: workspace !== undefined && workspace !== null,
|
|
1898
|
+
workspace: workspace ?? null,
|
|
1899
|
+
}));
|
|
1900
|
+
}
|
|
865
1901
|
// ─── Configure ──────────────────────────────────────────────────
|
|
866
1902
|
handleConfigure(req) {
|
|
867
1903
|
const params = (req.params ?? {});
|
|
@@ -1027,6 +2063,51 @@ export class AgentServer {
|
|
|
1027
2063
|
this.transport.send(createResponse(req.id, { ok: true }));
|
|
1028
2064
|
}
|
|
1029
2065
|
// ─── Query ──────────────────────────────────────────────────────
|
|
2066
|
+
/**
|
|
2067
|
+
* Resolve the Engine that owns a session-scoped query (compact, archive_range,
|
|
2068
|
+
* …). With a chatManager: materialize a persisted-but-not-live session on
|
|
2069
|
+
* demand, or borrow any live engine when no sessionId is given. Sends the
|
|
2070
|
+
* appropriate error response and returns null when the session is
|
|
2071
|
+
* closing/closed, unknown, or no engine is available; `label` names the query
|
|
2072
|
+
* in the "no engine" message. Domain-neutral — no query-specific literals.
|
|
2073
|
+
*/
|
|
2074
|
+
async resolveEngineForSessionQuery(req, sessionId, fallbackEngine, label) {
|
|
2075
|
+
let resolved = fallbackEngine;
|
|
2076
|
+
if (this.chatManager) {
|
|
2077
|
+
if (sessionId) {
|
|
2078
|
+
if (this.chatManager.isUnavailable(sessionId)) {
|
|
2079
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionClosed, `Session is closing or closed: ${sessionId}`));
|
|
2080
|
+
return null;
|
|
2081
|
+
}
|
|
2082
|
+
let session = this.chatManager.get(sessionId);
|
|
2083
|
+
if (!session) {
|
|
2084
|
+
const probeEngine = this.anyEngine();
|
|
2085
|
+
if (!probeEngine?.sessionExistsOnDisk(sessionId)) {
|
|
2086
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionNotFound, `Session not found: ${sessionId}`));
|
|
2087
|
+
return null;
|
|
2088
|
+
}
|
|
2089
|
+
try {
|
|
2090
|
+
session = await this.chatManager.getOrCreate(sessionId, {
|
|
2091
|
+
cwd: probeEngine.getSessionManager().readSessionMainRoot(sessionId),
|
|
2092
|
+
});
|
|
2093
|
+
}
|
|
2094
|
+
catch (err) {
|
|
2095
|
+
this.transport.send(createErrorResponse(req.id, err.code ?? ErrorCodes.InternalError, err.message));
|
|
2096
|
+
return null;
|
|
2097
|
+
}
|
|
2098
|
+
}
|
|
2099
|
+
resolved = session.engine;
|
|
2100
|
+
}
|
|
2101
|
+
else {
|
|
2102
|
+
resolved = this.anyEngine();
|
|
2103
|
+
}
|
|
2104
|
+
}
|
|
2105
|
+
if (!resolved) {
|
|
2106
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, `No engine available for ${label} query`));
|
|
2107
|
+
return null;
|
|
2108
|
+
}
|
|
2109
|
+
return resolved;
|
|
2110
|
+
}
|
|
1030
2111
|
async handleQuery(req) {
|
|
1031
2112
|
const params = (req.params ?? {});
|
|
1032
2113
|
// For query operations we prefer the legacyEngine; if absent borrow any
|
|
@@ -1049,20 +2130,14 @@ export class AgentServer {
|
|
|
1049
2130
|
}
|
|
1050
2131
|
case "sessions": {
|
|
1051
2132
|
if (this.chatManager) {
|
|
1052
|
-
//
|
|
1053
|
-
const sessions =
|
|
1054
|
-
this.chatManager.forEachSession((s) => {
|
|
1055
|
-
sessions.push({
|
|
1056
|
-
sessionId: s.id,
|
|
1057
|
-
busy: s.isBusy(),
|
|
1058
|
-
queueDepth: s.queueDepth(),
|
|
1059
|
-
lastActivityAt: s.lastActivityAt,
|
|
1060
|
-
});
|
|
1061
|
-
});
|
|
2133
|
+
// Pet projection and the legacy query share this one live-state builder.
|
|
2134
|
+
const { sessions } = this.chatManager.getLiveSessionSnapshot();
|
|
1062
2135
|
this.transport.send(createResponse(req.id, { type: "sessions", data: sessions }));
|
|
1063
2136
|
}
|
|
1064
2137
|
else if (engine) {
|
|
1065
|
-
const sessions = engine
|
|
2138
|
+
const sessions = engine
|
|
2139
|
+
.getSessionManager()
|
|
2140
|
+
.list(undefined, { excludeKinds: this.hiddenSessionKinds });
|
|
1066
2141
|
this.transport.send(createResponse(req.id, { type: "sessions", data: sessions }));
|
|
1067
2142
|
}
|
|
1068
2143
|
else {
|
|
@@ -1124,36 +2199,9 @@ export class AgentServer {
|
|
|
1124
2199
|
const compactSessionId = typeof params.sessionId === "string" && params.sessionId.length > 0
|
|
1125
2200
|
? params.sessionId
|
|
1126
2201
|
: undefined;
|
|
1127
|
-
|
|
1128
|
-
if (
|
|
1129
|
-
if (compactSessionId) {
|
|
1130
|
-
let session = this.chatManager.get(compactSessionId);
|
|
1131
|
-
if (!session) {
|
|
1132
|
-
const probeEngine = this.anyEngine();
|
|
1133
|
-
if (!probeEngine?.sessionExistsOnDisk(compactSessionId)) {
|
|
1134
|
-
this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionNotFound, `Session not found: ${compactSessionId}`));
|
|
1135
|
-
return;
|
|
1136
|
-
}
|
|
1137
|
-
try {
|
|
1138
|
-
session = this.chatManager.getOrCreate(compactSessionId, {
|
|
1139
|
-
cwd: probeEngine.getSessionManager().readCwd(compactSessionId),
|
|
1140
|
-
});
|
|
1141
|
-
}
|
|
1142
|
-
catch (err) {
|
|
1143
|
-
this.transport.send(createErrorResponse(req.id, err.code ?? ErrorCodes.InternalError, err.message));
|
|
1144
|
-
return;
|
|
1145
|
-
}
|
|
1146
|
-
}
|
|
1147
|
-
compactEngine = session.engine;
|
|
1148
|
-
}
|
|
1149
|
-
else {
|
|
1150
|
-
compactEngine = this.anyEngine();
|
|
1151
|
-
}
|
|
1152
|
-
}
|
|
1153
|
-
if (!compactEngine) {
|
|
1154
|
-
this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, "No engine available for compact query"));
|
|
2202
|
+
const compactEngine = await this.resolveEngineForSessionQuery(req, compactSessionId, engine, "compact");
|
|
2203
|
+
if (!compactEngine)
|
|
1155
2204
|
return;
|
|
1156
|
-
}
|
|
1157
2205
|
try {
|
|
1158
2206
|
const result = await compactEngine.forceCompact(compactSessionId);
|
|
1159
2207
|
if (result.before > result.after) {
|
|
@@ -1178,6 +2226,45 @@ export class AgentServer {
|
|
|
1178
2226
|
}
|
|
1179
2227
|
break;
|
|
1180
2228
|
}
|
|
2229
|
+
case "archive_range": {
|
|
2230
|
+
const archiveSessionId = typeof params.sessionId === "string" && params.sessionId.length > 0
|
|
2231
|
+
? params.sessionId
|
|
2232
|
+
: undefined;
|
|
2233
|
+
const start = Number(params.start);
|
|
2234
|
+
const end = Number(params.end);
|
|
2235
|
+
if (!archiveSessionId || !Number.isFinite(start) || !Number.isFinite(end)) {
|
|
2236
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "archive_range requires sessionId, start and end"));
|
|
2237
|
+
return;
|
|
2238
|
+
}
|
|
2239
|
+
const archiveEngine = await this.resolveEngineForSessionQuery(req, archiveSessionId, engine, "archive_range");
|
|
2240
|
+
if (!archiveEngine)
|
|
2241
|
+
return;
|
|
2242
|
+
try {
|
|
2243
|
+
const result = await archiveEngine.archiveTurnRange(archiveSessionId, { start, end });
|
|
2244
|
+
if (result.before > result.after) {
|
|
2245
|
+
const event = {
|
|
2246
|
+
type: "context_compact",
|
|
2247
|
+
// Range archival has its own tier; "range" must be in
|
|
2248
|
+
// COMPACT_STREAM_STRATEGIES or it degrades to "compacted".
|
|
2249
|
+
strategy: toCompactStreamStrategy("range"),
|
|
2250
|
+
before: result.before,
|
|
2251
|
+
after: result.after,
|
|
2252
|
+
};
|
|
2253
|
+
this.notify(Methods.StreamEvent, {
|
|
2254
|
+
sessionId: archiveSessionId,
|
|
2255
|
+
event,
|
|
2256
|
+
});
|
|
2257
|
+
}
|
|
2258
|
+
this.transport.send(createResponse(req.id, {
|
|
2259
|
+
type: "archive_range",
|
|
2260
|
+
data: result,
|
|
2261
|
+
}));
|
|
2262
|
+
}
|
|
2263
|
+
catch (err) {
|
|
2264
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
|
|
2265
|
+
}
|
|
2266
|
+
break;
|
|
2267
|
+
}
|
|
1181
2268
|
case "models": {
|
|
1182
2269
|
if (!engine) {
|
|
1183
2270
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, "No engine available for models query"));
|
|
@@ -1246,11 +2333,6 @@ export class AgentServer {
|
|
|
1246
2333
|
}
|
|
1247
2334
|
break;
|
|
1248
2335
|
}
|
|
1249
|
-
case "arena_status": {
|
|
1250
|
-
const status = getArenaStatus();
|
|
1251
|
-
this.transport.send(createResponse(req.id, { type: "arena_status", data: status }));
|
|
1252
|
-
break;
|
|
1253
|
-
}
|
|
1254
2336
|
case "config_set": {
|
|
1255
2337
|
if (!engine) {
|
|
1256
2338
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, "No engine available for config_set"));
|
|
@@ -1439,8 +2521,16 @@ export class AgentServer {
|
|
|
1439
2521
|
}
|
|
1440
2522
|
break;
|
|
1441
2523
|
}
|
|
1442
|
-
default:
|
|
2524
|
+
default: {
|
|
2525
|
+
const capability = engine
|
|
2526
|
+
? await engine.queryCapability(params.type, params)
|
|
2527
|
+
: { handled: false };
|
|
2528
|
+
if (capability.handled) {
|
|
2529
|
+
this.transport.send(createResponse(req.id, { type: params.type, data: capability.data }));
|
|
2530
|
+
break;
|
|
2531
|
+
}
|
|
1443
2532
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, `Unknown query type: ${params.type}`));
|
|
2533
|
+
}
|
|
1444
2534
|
}
|
|
1445
2535
|
}
|
|
1446
2536
|
// ─── Inject ─────────────────────────────────────────────────────
|
|
@@ -1452,6 +2542,10 @@ export class AgentServer {
|
|
|
1452
2542
|
}
|
|
1453
2543
|
// In the chatManager path, inject into the session's engine
|
|
1454
2544
|
if (this.chatManager) {
|
|
2545
|
+
if (this.chatManager.isUnavailable(params.sessionId)) {
|
|
2546
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionClosed, `Session is closing or closed: ${params.sessionId}`));
|
|
2547
|
+
return;
|
|
2548
|
+
}
|
|
1455
2549
|
const s = this.chatManager.get(params.sessionId);
|
|
1456
2550
|
if (!s) {
|
|
1457
2551
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionClosed, `No such session: ${params.sessionId}`));
|
|
@@ -1485,6 +2579,10 @@ export class AgentServer {
|
|
|
1485
2579
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "text and sessionId required"));
|
|
1486
2580
|
return;
|
|
1487
2581
|
}
|
|
2582
|
+
if (this.chatManager?.isUnavailable(params.sessionId)) {
|
|
2583
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionClosed, `Session is closing or closed: ${params.sessionId}`));
|
|
2584
|
+
return;
|
|
2585
|
+
}
|
|
1488
2586
|
const engine = this.chatManager
|
|
1489
2587
|
? this.chatManager.get(params.sessionId)?.engine
|
|
1490
2588
|
: this.legacyEngine;
|
|
@@ -1506,6 +2604,10 @@ export class AgentServer {
|
|
|
1506
2604
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "id and sessionId required"));
|
|
1507
2605
|
return;
|
|
1508
2606
|
}
|
|
2607
|
+
if (this.chatManager?.isUnavailable(params.sessionId)) {
|
|
2608
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionClosed, `Session is closing or closed: ${params.sessionId}`));
|
|
2609
|
+
return;
|
|
2610
|
+
}
|
|
1509
2611
|
const engine = this.chatManager
|
|
1510
2612
|
? this.chatManager.get(params.sessionId)?.engine
|
|
1511
2613
|
: this.legacyEngine;
|
|
@@ -1527,39 +2629,73 @@ export class AgentServer {
|
|
|
1527
2629
|
/**
|
|
1528
2630
|
* Ask the client to approve a tool operation (legacy single-engine path).
|
|
1529
2631
|
*/
|
|
1530
|
-
requestApprovalFromClient(request) {
|
|
2632
|
+
requestApprovalFromClient(request, route) {
|
|
1531
2633
|
return new Promise((resolve) => {
|
|
1532
2634
|
const requestId = nanoid(12);
|
|
1533
|
-
const
|
|
2635
|
+
const effectiveRoute = route ?? {
|
|
2636
|
+
connectionId: this.connectionId,
|
|
2637
|
+
sessionId: request.sessionId ?? "",
|
|
2638
|
+
generation: 0,
|
|
2639
|
+
};
|
|
2640
|
+
const sessionId = effectiveRoute.sessionId;
|
|
1534
2641
|
const session = this.chatManager && sessionId ? this.chatManager.get(sessionId) : undefined;
|
|
1535
2642
|
if (this.chatManager && sessionId && !session) {
|
|
1536
2643
|
resolve({ approved: false, reason: `session closed: ${sessionId}` });
|
|
1537
2644
|
return;
|
|
1538
2645
|
}
|
|
1539
2646
|
if (session) {
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
2647
|
+
const internal = INTERNAL_PENDING_TOOLS.has(request.toolName);
|
|
2648
|
+
const toolName = safeApprovalToolName(request.toolName);
|
|
2649
|
+
this.registerSessionApproval(session, {
|
|
2650
|
+
sessionId,
|
|
2651
|
+
requestId,
|
|
2652
|
+
routeGeneration: effectiveRoute.generation,
|
|
2653
|
+
workerGeneration: this.workerGeneration(),
|
|
2654
|
+
kind: internal ? "internal" : "tool_approval",
|
|
2655
|
+
title: internal ? "内部操作等待" : `等待批准 ${toolName}`,
|
|
2656
|
+
toolName,
|
|
2657
|
+
riskLevel: request.riskLevel,
|
|
2658
|
+
createdAt: Date.now(),
|
|
2659
|
+
expiresAt: Date.now() + AgentServer.APPROVAL_TIMEOUT_MS,
|
|
2660
|
+
surfaceable: !internal,
|
|
2661
|
+
}, (decision) => resolve(decision));
|
|
1543
2662
|
}
|
|
1544
2663
|
else {
|
|
1545
2664
|
this.pendingApprovals.set(requestId, resolve);
|
|
1546
2665
|
}
|
|
2666
|
+
this.pendingApprovalTargets.set(requestId, { ...effectiveRoute, requestId });
|
|
1547
2667
|
const timer = setTimeout(() => {
|
|
1548
2668
|
const pending = session?.pendingApprovals ?? this.pendingApprovals;
|
|
1549
2669
|
if (pending.has(requestId)) {
|
|
1550
|
-
|
|
2670
|
+
if (session) {
|
|
2671
|
+
this.takeSessionApproval(session, requestId, "expired");
|
|
2672
|
+
}
|
|
2673
|
+
else {
|
|
2674
|
+
pending.delete(requestId);
|
|
2675
|
+
}
|
|
2676
|
+
this.pendingApprovalTargets.delete(requestId);
|
|
1551
2677
|
this.approvalTimers.delete(requestId);
|
|
1552
2678
|
resolve({ approved: false, reason: "approval timed out" });
|
|
1553
2679
|
}
|
|
1554
2680
|
}, AgentServer.APPROVAL_TIMEOUT_MS);
|
|
1555
2681
|
this.approvalTimers.set(requestId, timer);
|
|
1556
2682
|
this.notify(Methods.ApprovalRequest, {
|
|
2683
|
+
connectionId: effectiveRoute.connectionId,
|
|
1557
2684
|
...(sessionId ? { sessionId } : {}),
|
|
2685
|
+
generation: effectiveRoute.generation,
|
|
1558
2686
|
requestId,
|
|
1559
2687
|
request,
|
|
1560
2688
|
});
|
|
1561
2689
|
});
|
|
1562
2690
|
}
|
|
2691
|
+
approvalRouteEnvelope(sessionId, requestId) {
|
|
2692
|
+
const route = this.approvalRouter.current(sessionId);
|
|
2693
|
+
if (!route || route.connectionId !== this.connectionId)
|
|
2694
|
+
return { sessionId, requestId };
|
|
2695
|
+
const target = { ...route, requestId };
|
|
2696
|
+
this.pendingApprovalTargets.set(requestId, target);
|
|
2697
|
+
return target;
|
|
2698
|
+
}
|
|
1563
2699
|
/**
|
|
1564
2700
|
* Per-session AskUserQuestion for the chatManager path. Resolves via the
|
|
1565
2701
|
* SESSION's pendingApprovals (the chatManager approve handler looks there,
|
|
@@ -1570,7 +2706,17 @@ export class AgentServer {
|
|
|
1570
2706
|
requestAskUserForSession(session, sessionId, question, opts) {
|
|
1571
2707
|
return new Promise((resolve) => {
|
|
1572
2708
|
const requestId = nanoid(12);
|
|
1573
|
-
|
|
2709
|
+
const routeEnvelope = this.approvalRouteEnvelope(sessionId, requestId);
|
|
2710
|
+
this.registerSessionApproval(session, {
|
|
2711
|
+
sessionId,
|
|
2712
|
+
requestId,
|
|
2713
|
+
routeGeneration: "generation" in routeEnvelope ? routeEnvelope.generation : undefined,
|
|
2714
|
+
workerGeneration: this.workerGeneration(),
|
|
2715
|
+
kind: "ask_user",
|
|
2716
|
+
title: "需要用户回答",
|
|
2717
|
+
createdAt: Date.now(),
|
|
2718
|
+
surfaceable: true,
|
|
2719
|
+
}, (decision) => {
|
|
1574
2720
|
this.clearApprovalTimer(requestId);
|
|
1575
2721
|
const result = decision;
|
|
1576
2722
|
if (result && typeof result === "object" && "approved" in result) {
|
|
@@ -1593,8 +2739,7 @@ export class AgentServer {
|
|
|
1593
2739
|
if (opts?.optionsOnly !== undefined)
|
|
1594
2740
|
args.optionsOnly = opts.optionsOnly;
|
|
1595
2741
|
this.notify(Methods.ApprovalRequest, {
|
|
1596
|
-
|
|
1597
|
-
requestId,
|
|
2742
|
+
...routeEnvelope,
|
|
1598
2743
|
request: {
|
|
1599
2744
|
toolName: "__ask_user__",
|
|
1600
2745
|
args,
|
|
@@ -1619,11 +2764,11 @@ export class AgentServer {
|
|
|
1619
2764
|
}
|
|
1620
2765
|
if (goalActive) {
|
|
1621
2766
|
const timer = setTimeout(() => {
|
|
1622
|
-
const
|
|
1623
|
-
if (
|
|
1624
|
-
|
|
2767
|
+
const entry = this.takeSessionApproval(session, requestId, "expired");
|
|
2768
|
+
if (entry) {
|
|
2769
|
+
this.pendingApprovalTargets.delete(requestId);
|
|
1625
2770
|
this.approvalTimers.delete(requestId);
|
|
1626
|
-
|
|
2771
|
+
entry.resolve("(用户未在限定时间内回答;目标运行中请自行做出合理假设并继续推进。)");
|
|
1627
2772
|
// Tell every client the ask is resolved (nobody answered) so the
|
|
1628
2773
|
// stale AskUserQuestion card is dismissed instead of lingering as
|
|
1629
2774
|
// if still waiting. Reuses the same envelope shape the desktop main
|
|
@@ -1650,7 +2795,7 @@ export class AgentServer {
|
|
|
1650
2795
|
type: (ref, text) => call("type", { ref, text }),
|
|
1651
2796
|
navigate: (url) => call("navigate", { url }),
|
|
1652
2797
|
scroll: (dir, amount) => call("scroll", { dir, amount }),
|
|
1653
|
-
readContent: () => call("readContent", {}),
|
|
2798
|
+
readContent: (options) => call("readContent", { ...(options ?? {}) }),
|
|
1654
2799
|
extractLinks: () => call("extractLinks", {}),
|
|
1655
2800
|
waitForLoad: (timeoutMs) => call("waitForLoad", { timeoutMs }),
|
|
1656
2801
|
hover: (ref) => call("hover", { ref }),
|
|
@@ -1665,40 +2810,31 @@ export class AgentServer {
|
|
|
1665
2810
|
requestBrowserActionForSession(session, sessionId, action, payload) {
|
|
1666
2811
|
return new Promise((resolve) => {
|
|
1667
2812
|
const requestId = nanoid(12);
|
|
1668
|
-
|
|
2813
|
+
const routeEnvelope = this.approvalRouteEnvelope(sessionId, requestId);
|
|
2814
|
+
this.registerSessionApproval(session, this.internalPendingMetadata(sessionId, requestId, routeEnvelope, "__browser_action__"), (decision) => {
|
|
1669
2815
|
this.clearApprovalTimer(requestId);
|
|
1670
2816
|
// Main replies with { approved:true, answer:<json string> } (reusing the
|
|
1671
2817
|
// ApprovalResult shape) or a raw json string. Parse → typed result.
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
}
|
|
1677
|
-
else if (typeof decision === "string") {
|
|
1678
|
-
raw = decision;
|
|
1679
|
-
}
|
|
1680
|
-
if (raw === undefined) {
|
|
1681
|
-
resolve({ ok: false, detail: "browser action declined or unavailable" });
|
|
1682
|
-
return;
|
|
1683
|
-
}
|
|
1684
|
-
try {
|
|
1685
|
-
resolve(JSON.parse(raw));
|
|
1686
|
-
}
|
|
1687
|
-
catch {
|
|
1688
|
-
resolve({ ok: false, detail: "malformed browser action result" });
|
|
1689
|
-
}
|
|
2818
|
+
const parsed = parseHostLoopbackDecision(decision, "browser action");
|
|
2819
|
+
resolve(parsed.ok
|
|
2820
|
+
? parsed.value
|
|
2821
|
+
: { ok: false, failure: parsed.failure, detail: parsed.detail });
|
|
1690
2822
|
});
|
|
1691
2823
|
const timer = setTimeout(() => {
|
|
1692
2824
|
if (session.pendingApprovals.has(requestId)) {
|
|
1693
|
-
|
|
2825
|
+
this.takeSessionApproval(session, requestId, "expired");
|
|
2826
|
+
this.pendingApprovalTargets.delete(requestId);
|
|
1694
2827
|
this.approvalTimers.delete(requestId);
|
|
1695
|
-
resolve({
|
|
2828
|
+
resolve({
|
|
2829
|
+
ok: false,
|
|
2830
|
+
failure: "timed_out",
|
|
2831
|
+
detail: `browser action ${HOST_LOOPBACK_FAILURE_DETAIL.timed_out}`,
|
|
2832
|
+
});
|
|
1696
2833
|
}
|
|
1697
2834
|
}, AgentServer.APPROVAL_TIMEOUT_MS);
|
|
1698
2835
|
this.approvalTimers.set(requestId, timer);
|
|
1699
2836
|
this.notify(Methods.ApprovalRequest, {
|
|
1700
|
-
|
|
1701
|
-
requestId,
|
|
2837
|
+
...routeEnvelope,
|
|
1702
2838
|
request: {
|
|
1703
2839
|
toolName: "__browser_action__",
|
|
1704
2840
|
args: { action, ...payload },
|
|
@@ -1717,38 +2853,28 @@ export class AgentServer {
|
|
|
1717
2853
|
requestCredentialInjectForSession(session, sessionId, credentialId, credentialScope = "full") {
|
|
1718
2854
|
return new Promise((resolve) => {
|
|
1719
2855
|
const requestId = nanoid(12);
|
|
1720
|
-
|
|
2856
|
+
const routeEnvelope = this.approvalRouteEnvelope(sessionId, requestId);
|
|
2857
|
+
this.registerSessionApproval(session, this.internalPendingMetadata(sessionId, requestId, routeEnvelope, "__credential_action__"), (decision) => {
|
|
1721
2858
|
this.clearApprovalTimer(requestId);
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
}
|
|
1727
|
-
else if (typeof decision === "string") {
|
|
1728
|
-
raw = decision;
|
|
1729
|
-
}
|
|
1730
|
-
if (raw === undefined) {
|
|
1731
|
-
resolve({ ok: false, error: "credential inject declined or unavailable" });
|
|
1732
|
-
return;
|
|
1733
|
-
}
|
|
1734
|
-
try {
|
|
1735
|
-
resolve(JSON.parse(raw));
|
|
1736
|
-
}
|
|
1737
|
-
catch {
|
|
1738
|
-
resolve({ ok: false, error: "malformed credential inject result" });
|
|
1739
|
-
}
|
|
2859
|
+
const parsed = parseHostLoopbackDecision(decision, "credential inject");
|
|
2860
|
+
resolve(parsed.ok
|
|
2861
|
+
? parsed.value
|
|
2862
|
+
: { ok: false, error: parsed.detail });
|
|
1740
2863
|
});
|
|
1741
2864
|
const timer = setTimeout(() => {
|
|
1742
2865
|
if (session.pendingApprovals.has(requestId)) {
|
|
1743
|
-
|
|
2866
|
+
this.takeSessionApproval(session, requestId, "expired");
|
|
2867
|
+
this.pendingApprovalTargets.delete(requestId);
|
|
1744
2868
|
this.approvalTimers.delete(requestId);
|
|
1745
|
-
resolve({
|
|
2869
|
+
resolve({
|
|
2870
|
+
ok: false,
|
|
2871
|
+
error: `credential inject ${HOST_LOOPBACK_FAILURE_DETAIL.timed_out}`,
|
|
2872
|
+
});
|
|
1746
2873
|
}
|
|
1747
2874
|
}, AgentServer.APPROVAL_TIMEOUT_MS);
|
|
1748
2875
|
this.approvalTimers.set(requestId, timer);
|
|
1749
2876
|
this.notify(Methods.ApprovalRequest, {
|
|
1750
|
-
|
|
1751
|
-
requestId,
|
|
2877
|
+
...routeEnvelope,
|
|
1752
2878
|
request: {
|
|
1753
2879
|
toolName: "__credential_action__",
|
|
1754
2880
|
args: { action: "injectCookie", credentialId, credentialScope },
|
|
@@ -1763,46 +2889,145 @@ export class AgentServer {
|
|
|
1763
2889
|
switch: (target) => this.requestWorkspaceSwitchForSession(session, sessionId, target),
|
|
1764
2890
|
};
|
|
1765
2891
|
}
|
|
2892
|
+
makePanelBridge(session, sessionId) {
|
|
2893
|
+
return {
|
|
2894
|
+
list: async () => {
|
|
2895
|
+
const result = (await this.requestPanelActionForSession(session, sessionId, "list", {}));
|
|
2896
|
+
if (result?.ok === true && Array.isArray(result.panels)) {
|
|
2897
|
+
return {
|
|
2898
|
+
items: result.panels,
|
|
2899
|
+
};
|
|
2900
|
+
}
|
|
2901
|
+
// Never report a failed discovery as an empty host. "(no panels
|
|
2902
|
+
// available)" is an affirmative claim that makes the model give up.
|
|
2903
|
+
return { items: [], failed: hostLoopbackDetail(result) ?? "panel list failed" };
|
|
2904
|
+
},
|
|
2905
|
+
open: async (panelId) => {
|
|
2906
|
+
const result = (await this.requestPanelActionForSession(session, sessionId, "open", {
|
|
2907
|
+
panelId,
|
|
2908
|
+
}));
|
|
2909
|
+
// Gate on SUCCESS, not merely on shape. A real failure reply is
|
|
2910
|
+
// `{ok:false, panelId, detail}` (AgentPanelHost) — it satisfies a
|
|
2911
|
+
// shape-only `result?.panelId` check and would return verbatim, skipping
|
|
2912
|
+
// both the classification recovery and the length bound below. Every
|
|
2913
|
+
// failure must fall through here.
|
|
2914
|
+
if (result?.ok === true && result?.panelId)
|
|
2915
|
+
return result;
|
|
2916
|
+
// A classified terminal failure (cancelled / denied / timed out) must
|
|
2917
|
+
// survive this normalization — reporting it as "malformed" would tell the
|
|
2918
|
+
// model the HOST misbehaved when in fact the user stopped the turn.
|
|
2919
|
+
return {
|
|
2920
|
+
ok: false,
|
|
2921
|
+
panelId: result?.panelId ?? panelId,
|
|
2922
|
+
detail: hostLoopbackDetail(result) ?? "panel host returned a malformed result",
|
|
2923
|
+
};
|
|
2924
|
+
},
|
|
2925
|
+
tools: async (panelId) => {
|
|
2926
|
+
const result = (await this.requestPanelActionForSession(session, sessionId, "tools", {
|
|
2927
|
+
panelId,
|
|
2928
|
+
}));
|
|
2929
|
+
if (result?.ok === true && Array.isArray(result.tools)) {
|
|
2930
|
+
return {
|
|
2931
|
+
items: result.tools,
|
|
2932
|
+
};
|
|
2933
|
+
}
|
|
2934
|
+
return { items: [], failed: hostLoopbackDetail(result) ?? "panel tools query failed" };
|
|
2935
|
+
},
|
|
2936
|
+
invoke: async (panelId, toolName, args) => {
|
|
2937
|
+
const result = (await this.requestPanelActionForSession(session, sessionId, "invoke", {
|
|
2938
|
+
panelId,
|
|
2939
|
+
toolName,
|
|
2940
|
+
arguments: args,
|
|
2941
|
+
}));
|
|
2942
|
+
// Success-gated for the same reason as `open` above. Note a real failure
|
|
2943
|
+
// reply carries `panelId` but usually NOT `toolName` (AgentPanelHost emits
|
|
2944
|
+
// `{ok:false, panelId, detail}`), so the shape check alone was less
|
|
2945
|
+
// frequently bypassed here than for `open` — but `ok === true` is the
|
|
2946
|
+
// property that actually matters, and it also keeps a Panel App's raw
|
|
2947
|
+
// `error.message` from reaching the model unbounded.
|
|
2948
|
+
if (result?.ok === true && result?.panelId && result?.toolName)
|
|
2949
|
+
return result;
|
|
2950
|
+
return {
|
|
2951
|
+
ok: false,
|
|
2952
|
+
panelId: result?.panelId ?? panelId,
|
|
2953
|
+
toolName: result?.toolName ?? toolName,
|
|
2954
|
+
detail: hostLoopbackDetail(result) ?? "panel host returned a malformed result",
|
|
2955
|
+
};
|
|
2956
|
+
},
|
|
2957
|
+
};
|
|
2958
|
+
}
|
|
2959
|
+
requestPanelActionForSession(session, sessionId, action, payload) {
|
|
2960
|
+
return new Promise((resolve) => {
|
|
2961
|
+
const requestId = nanoid(12);
|
|
2962
|
+
const routeEnvelope = this.approvalRouteEnvelope(sessionId, requestId);
|
|
2963
|
+
this.registerSessionApproval(session, this.internalPendingMetadata(sessionId, requestId, routeEnvelope, "__panel_action__"), (decision) => {
|
|
2964
|
+
this.clearApprovalTimer(requestId);
|
|
2965
|
+
const parsed = parseHostLoopbackDecision(decision, "panel action");
|
|
2966
|
+
resolve(parsed.ok ? parsed.value : { ok: false, failure: parsed.failure, error: parsed.detail });
|
|
2967
|
+
});
|
|
2968
|
+
const timer = setTimeout(() => {
|
|
2969
|
+
if (session.pendingApprovals.has(requestId)) {
|
|
2970
|
+
this.takeSessionApproval(session, requestId, "expired");
|
|
2971
|
+
this.pendingApprovalTargets.delete(requestId);
|
|
2972
|
+
this.approvalTimers.delete(requestId);
|
|
2973
|
+
resolve({
|
|
2974
|
+
ok: false,
|
|
2975
|
+
failure: "timed_out",
|
|
2976
|
+
error: `panel action ${HOST_LOOPBACK_FAILURE_DETAIL.timed_out}`,
|
|
2977
|
+
});
|
|
2978
|
+
}
|
|
2979
|
+
}, AgentServer.APPROVAL_TIMEOUT_MS);
|
|
2980
|
+
this.approvalTimers.set(requestId, timer);
|
|
2981
|
+
this.notify(Methods.ApprovalRequest, {
|
|
2982
|
+
...routeEnvelope,
|
|
2983
|
+
request: {
|
|
2984
|
+
toolName: "__panel_action__",
|
|
2985
|
+
args: { action, ...payload },
|
|
2986
|
+
description: `panel:${action}`,
|
|
2987
|
+
riskLevel: "low",
|
|
2988
|
+
},
|
|
2989
|
+
});
|
|
2990
|
+
});
|
|
2991
|
+
}
|
|
1766
2992
|
requestWorkspaceSwitchForSession(session, sessionId, target) {
|
|
1767
2993
|
return new Promise((resolve, reject) => {
|
|
1768
2994
|
const requestId = nanoid(12);
|
|
1769
|
-
|
|
2995
|
+
const routeEnvelope = this.approvalRouteEnvelope(sessionId, requestId);
|
|
2996
|
+
this.registerSessionApproval(session, this.internalPendingMetadata(sessionId, requestId, routeEnvelope, "__workspace_action__"), (decision) => {
|
|
1770
2997
|
this.clearApprovalTimer(requestId);
|
|
1771
|
-
|
|
1772
|
-
if (
|
|
1773
|
-
|
|
1774
|
-
raw = r.approved ? r.answer : undefined;
|
|
1775
|
-
}
|
|
1776
|
-
else if (typeof decision === "string") {
|
|
1777
|
-
raw = decision;
|
|
1778
|
-
}
|
|
1779
|
-
if (raw === undefined) {
|
|
1780
|
-
reject(new Error("workspace switch declined or unavailable"));
|
|
2998
|
+
const outcome = parseHostLoopbackDecision(decision, "workspace switch");
|
|
2999
|
+
if (!outcome.ok) {
|
|
3000
|
+
reject(new Error(outcome.detail));
|
|
1781
3001
|
return;
|
|
1782
3002
|
}
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
3003
|
+
const parsed = outcome.value;
|
|
3004
|
+
// A workspace is always an object. Reject any other JSON shape rather
|
|
3005
|
+
// than resolving it: `null` / a bare number / a string would otherwise
|
|
3006
|
+
// be handed to setSessionWorkspace as if the switch had succeeded,
|
|
3007
|
+
// rebasing the session onto a non-workspace. (Previously a `"ok" in
|
|
3008
|
+
// parsed` TypeError happened to be caught and turned into a rejection;
|
|
3009
|
+
// this states the requirement instead of relying on that.)
|
|
3010
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
|
|
3011
|
+
reject(new Error(`workspace switch ${HOST_LOOPBACK_FAILURE_DETAIL.malformed}`));
|
|
3012
|
+
return;
|
|
1790
3013
|
}
|
|
1791
|
-
|
|
1792
|
-
reject(new Error("
|
|
3014
|
+
if ("ok" in parsed && parsed.ok === false) {
|
|
3015
|
+
reject(new Error(parsed.error ?? "workspace switch failed"));
|
|
3016
|
+
return;
|
|
1793
3017
|
}
|
|
3018
|
+
resolve(parsed);
|
|
1794
3019
|
});
|
|
1795
3020
|
const timer = setTimeout(() => {
|
|
1796
3021
|
if (session.pendingApprovals.has(requestId)) {
|
|
1797
|
-
|
|
3022
|
+
this.takeSessionApproval(session, requestId, "expired");
|
|
3023
|
+
this.pendingApprovalTargets.delete(requestId);
|
|
1798
3024
|
this.approvalTimers.delete(requestId);
|
|
1799
|
-
reject(new Error(
|
|
3025
|
+
reject(new Error(`workspace switch ${HOST_LOOPBACK_FAILURE_DETAIL.timed_out}`));
|
|
1800
3026
|
}
|
|
1801
3027
|
}, AgentServer.APPROVAL_TIMEOUT_MS);
|
|
1802
3028
|
this.approvalTimers.set(requestId, timer);
|
|
1803
3029
|
this.notify(Methods.ApprovalRequest, {
|
|
1804
|
-
|
|
1805
|
-
requestId,
|
|
3030
|
+
...routeEnvelope,
|
|
1806
3031
|
request: {
|
|
1807
3032
|
toolName: "__workspace_action__",
|
|
1808
3033
|
args: { action: "switch", target },
|
|
@@ -1876,6 +3101,8 @@ export class AgentServer {
|
|
|
1876
3101
|
}
|
|
1877
3102
|
// ─── Lifecycle ──────────────────────────────────────────────────
|
|
1878
3103
|
close() {
|
|
3104
|
+
this.observeServerClose();
|
|
3105
|
+
this.disconnect("server closing");
|
|
1879
3106
|
// Detach the bg-agent bus subscription first so a final flurry of
|
|
1880
3107
|
// completions during shutdown can't race the `shutdown` status
|
|
1881
3108
|
// notify below. Safe to call repeatedly — the unsubscribe is a
|
|
@@ -1889,11 +3116,17 @@ export class AgentServer {
|
|
|
1889
3116
|
}
|
|
1890
3117
|
this.bgAgentBusUnsubscribe = null;
|
|
1891
3118
|
}
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
this.
|
|
3119
|
+
// Close the host-supplied manager plus every identity-derived manager
|
|
3120
|
+
// this server lazily created (the map is empty without a resolveIdentity
|
|
3121
|
+
// hook, so the default path closes exactly the base manager as before).
|
|
3122
|
+
if (this.baseChatManager) {
|
|
3123
|
+
const managers = [this.baseChatManager, ...this.identityManagers.values()];
|
|
3124
|
+
for (const manager of managers) {
|
|
3125
|
+
manager.forEachSession((session) => {
|
|
3126
|
+
this.cancelSessionApprovals(session, "server closing", "cancelled", "session_closed");
|
|
3127
|
+
});
|
|
3128
|
+
manager.closeAll();
|
|
3129
|
+
}
|
|
1897
3130
|
}
|
|
1898
3131
|
// Legacy path cleanup
|
|
1899
3132
|
if (this.abortController) {
|
|
@@ -1912,6 +3145,59 @@ export class AgentServer {
|
|
|
1912
3145
|
this.notify(Methods.Status, { status: "shutdown" });
|
|
1913
3146
|
this.transport.close();
|
|
1914
3147
|
}
|
|
3148
|
+
/** Release only this transport's approval ownership. Safe on socket close
|
|
3149
|
+
* even when the ChatSessionManager is shared by other TCP connections. */
|
|
3150
|
+
disconnect(reason = "approval connection disconnected") {
|
|
3151
|
+
if (this.disconnected)
|
|
3152
|
+
return;
|
|
3153
|
+
this.disconnected = true;
|
|
3154
|
+
const unregister = this.approvalConnectionUnregister;
|
|
3155
|
+
this.approvalConnectionUnregister = null;
|
|
3156
|
+
if (unregister) {
|
|
3157
|
+
this.approvalRouter.deregister(this.connectionId, reason);
|
|
3158
|
+
}
|
|
3159
|
+
}
|
|
3160
|
+
/** Resolver-free metadata view for extension projections and protocol snapshots. */
|
|
3161
|
+
getPendingDecisionSnapshot() {
|
|
3162
|
+
const entries = [];
|
|
3163
|
+
this.forEachObserver("snapshotPendingDecisions", (observer) => {
|
|
3164
|
+
const snapshot = observer.snapshotPendingDecisions?.();
|
|
3165
|
+
if (snapshot)
|
|
3166
|
+
entries.push(...snapshot);
|
|
3167
|
+
});
|
|
3168
|
+
return entries;
|
|
3169
|
+
}
|
|
3170
|
+
/** Host lifecycle generation stamped onto pending-approval metadata. */
|
|
3171
|
+
workerGeneration() {
|
|
3172
|
+
return this.chatManager?.getLiveSessionSnapshot().generation ?? 0;
|
|
3173
|
+
}
|
|
3174
|
+
registerSessionApproval(session, metadata, resolve) {
|
|
3175
|
+
metadata = this.observeApprovalCreated(metadata);
|
|
3176
|
+
session.pendingApprovals.set(metadata.requestId, { resolve, metadata });
|
|
3177
|
+
}
|
|
3178
|
+
takeSessionApproval(session, requestId, status) {
|
|
3179
|
+
const entry = session.pendingApprovals.get(requestId);
|
|
3180
|
+
if (!entry)
|
|
3181
|
+
return undefined;
|
|
3182
|
+
session.pendingApprovals.delete(requestId);
|
|
3183
|
+
this.observeApprovalTransition(entry.metadata, status);
|
|
3184
|
+
return entry;
|
|
3185
|
+
}
|
|
3186
|
+
internalPendingMetadata(sessionId, requestId, route, toolName) {
|
|
3187
|
+
const createdAt = Date.now();
|
|
3188
|
+
return {
|
|
3189
|
+
sessionId,
|
|
3190
|
+
requestId,
|
|
3191
|
+
routeGeneration: "generation" in route ? route.generation : undefined,
|
|
3192
|
+
workerGeneration: this.workerGeneration(),
|
|
3193
|
+
kind: "internal",
|
|
3194
|
+
title: "内部操作等待",
|
|
3195
|
+
toolName,
|
|
3196
|
+
createdAt,
|
|
3197
|
+
expiresAt: createdAt + AgentServer.APPROVAL_TIMEOUT_MS,
|
|
3198
|
+
surfaceable: false,
|
|
3199
|
+
};
|
|
3200
|
+
}
|
|
1915
3201
|
clearApprovalTimer(requestId) {
|
|
1916
3202
|
const timer = this.approvalTimers.get(requestId);
|
|
1917
3203
|
if (timer) {
|
|
@@ -1926,18 +3212,77 @@ export class AgentServer {
|
|
|
1926
3212
|
* the tool hanging. Bounded request types have same-keyed timer entries;
|
|
1927
3213
|
* AskUserQuestion does not.
|
|
1928
3214
|
*/
|
|
1929
|
-
cancelSessionApprovals(session, reason = "cancelled") {
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
3215
|
+
cancelSessionApprovals(session, reason = "cancelled", status = "cancelled", failure = "cancelled") {
|
|
3216
|
+
// Two kinds of entry share this map and they must NOT settle the same way.
|
|
3217
|
+
//
|
|
3218
|
+
// - kind "tool_approval": a real permission prompt. `{approved:false}` is
|
|
3219
|
+
// the correct terminal value — the tool call does not proceed.
|
|
3220
|
+
// - kind "internal": an in-flight host-loopback request (Panel / Browser /
|
|
3221
|
+
// workspace / credential). `{approved:false}` here is a lie: it makes a
|
|
3222
|
+
// Stop or a session close indistinguishable from the user pressing Deny,
|
|
3223
|
+
// and the model is told its Panel operation was refused. These get a
|
|
3224
|
+
// cancellation sentinel carrying the real cause instead.
|
|
3225
|
+
//
|
|
3226
|
+
// Internal entries settle FIRST: they need no user interaction and have a
|
|
3227
|
+
// determinate outcome, and draining them before the map is emptied keeps
|
|
3228
|
+
// every resolver reachable.
|
|
3229
|
+
//
|
|
3230
|
+
// Drain in a loop rather than over one snapshot. A resolver — or the
|
|
3231
|
+
// observeApprovalTransition hook called synchronously below — may register a
|
|
3232
|
+
// NEW pending entry while we are cancelling. Iterating a snapshot and then
|
|
3233
|
+
// clear()ing would delete that newcomer without ever settling it, leaving its
|
|
3234
|
+
// awaiting tool hung until an outer timeout (its own timer is already gone).
|
|
3235
|
+
// The `settled` guard keeps this terminating even if a resolver re-registers
|
|
3236
|
+
// the same requestId, and the iteration cap stops a pathological resolver
|
|
3237
|
+
// that registers a fresh id every time from spinning forever.
|
|
3238
|
+
const settled = new Set();
|
|
3239
|
+
for (let pass = 0; session.pendingApprovals.size > 0 && pass < 1000; pass += 1) {
|
|
3240
|
+
const entries = [...session.pendingApprovals].filter(([id]) => !settled.has(id));
|
|
3241
|
+
if (entries.length === 0)
|
|
3242
|
+
break;
|
|
3243
|
+
const ordered = [
|
|
3244
|
+
...entries.filter(([, entry]) => entry.metadata.kind === "internal"),
|
|
3245
|
+
...entries.filter(([, entry]) => entry.metadata.kind !== "internal"),
|
|
3246
|
+
];
|
|
3247
|
+
for (const [requestId, entry] of ordered) {
|
|
3248
|
+
settled.add(requestId);
|
|
3249
|
+
session.pendingApprovals.delete(requestId);
|
|
3250
|
+
this.clearApprovalTimer(requestId);
|
|
3251
|
+
this.pendingApprovalTargets.delete(requestId);
|
|
3252
|
+
this.observeApprovalTransition(entry.metadata, status);
|
|
3253
|
+
try {
|
|
3254
|
+
entry.resolve(entry.metadata.kind === "internal"
|
|
3255
|
+
? internalCancellation(failure, reason)
|
|
3256
|
+
: { approved: false, reason });
|
|
3257
|
+
}
|
|
3258
|
+
catch {
|
|
3259
|
+
/* a resolver must never break cancel cleanup */
|
|
3260
|
+
}
|
|
1937
3261
|
}
|
|
1938
3262
|
}
|
|
1939
3263
|
session.pendingApprovals.clear();
|
|
1940
3264
|
}
|
|
3265
|
+
failClosedApprovalTargets(targets, reason) {
|
|
3266
|
+
for (const target of targets) {
|
|
3267
|
+
const session = this.chatManager?.get(target.sessionId);
|
|
3268
|
+
if (session) {
|
|
3269
|
+
this.cancelSessionApprovals(session, reason, "owner-lost", "owner_lost");
|
|
3270
|
+
continue;
|
|
3271
|
+
}
|
|
3272
|
+
for (const [requestId, pending] of this.pendingApprovalTargets) {
|
|
3273
|
+
if (pending.connectionId !== target.connectionId ||
|
|
3274
|
+
pending.sessionId !== target.sessionId ||
|
|
3275
|
+
pending.generation !== target.generation) {
|
|
3276
|
+
continue;
|
|
3277
|
+
}
|
|
3278
|
+
const resolve = this.pendingApprovals.get(requestId);
|
|
3279
|
+
this.pendingApprovals.delete(requestId);
|
|
3280
|
+
this.pendingApprovalTargets.delete(requestId);
|
|
3281
|
+
this.clearApprovalTimer(requestId);
|
|
3282
|
+
resolve?.({ approved: false, reason });
|
|
3283
|
+
}
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
1941
3286
|
clearAllApprovalTimers() {
|
|
1942
3287
|
for (const timer of this.approvalTimers.values()) {
|
|
1943
3288
|
clearTimeout(timer);
|