@cjhyy/code-shell-core 0.7.1 → 0.8.1
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 +7 -3
- package/dist/context/compaction.d.ts +39 -3
- package/dist/context/compaction.js +151 -39
- package/dist/context/manager.d.ts +17 -1
- package/dist/context/manager.js +36 -1
- package/dist/credentials/access.d.ts +3 -1
- package/dist/credentials/access.js +24 -3
- package/dist/credentials/index.d.ts +1 -1
- package/dist/credentials/index.js +1 -1
- package/dist/credentials/oauth.d.ts +4 -1
- package/dist/credentials/oauth.js +14 -0
- package/dist/credentials/store.d.ts +15 -1
- package/dist/credentials/store.js +17 -4
- package/dist/credentials/types.d.ts +24 -0
- 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 +254 -195
- package/dist/engine/engine.js +1812 -2041
- 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/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 +49 -10
- package/dist/engine/turn-loop.js +370 -104
- package/dist/engine/types.d.ts +30 -3
- package/dist/engine/types.js +1 -1
- package/dist/{engine/goal.d.ts → goal/lifecycle.d.ts} +76 -4
- package/dist/goal/lifecycle.js +482 -0
- package/dist/hooks/events.d.ts +24 -10
- package/dist/hooks/goal-stop-hook.d.ts +17 -28
- package/dist/hooks/goal-stop-hook.js +110 -465
- package/dist/hooks/registry.js +1 -1
- package/dist/index.d.ts +48 -108
- 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 +34 -123
- 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 +60 -0
- 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/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 +6 -0
- package/dist/plugins/pluginCommandHook.js +25 -8
- 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 +64 -1
- package/dist/protocol/chat-session-manager.js +104 -0
- package/dist/protocol/chat-session.d.ts +61 -10
- package/dist/protocol/chat-session.js +135 -25
- package/dist/protocol/client.d.ts +33 -3
- package/dist/protocol/client.js +96 -5
- 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 +127 -3
- package/dist/protocol/server.js +1309 -182
- package/dist/protocol/types.d.ts +152 -34
- package/dist/protocol/types.js +16 -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.js +10 -1
- package/dist/services/memory-orchestrator.js +6 -2
- package/dist/services/oauth.js +4 -1
- 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 +182 -28
- package/dist/session/session-manager.js +910 -130
- package/dist/session/session-message.d.ts +22 -0
- package/dist/session/session-message.js +1 -0
- package/dist/session/transcript.d.ts +46 -5
- package/dist/session/transcript.js +159 -3
- 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 +274 -33
- package/dist/tool-system/builtin/background-jobs.d.ts +55 -2
- package/dist/tool-system/builtin/background-jobs.js +96 -4
- package/dist/tool-system/builtin/background-work.d.ts +6 -0
- package/dist/tool-system/builtin/background-work.js +8 -1
- package/dist/tool-system/builtin/bash.js +5 -2
- package/dist/tool-system/builtin/browser-tools.d.ts +6 -3
- 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.js +2 -6
- 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.js +26 -7
- package/dist/tool-system/builtin/grep.js +2 -2
- package/dist/tool-system/builtin/image-uploader.js +7 -1
- package/dist/tool-system/builtin/index.d.ts +32 -7
- package/dist/tool-system/builtin/index.js +237 -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/web-fetch.js +17 -13
- package/dist/tool-system/builtin/web-search.js +1 -3
- 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 +77 -19
- package/dist/tool-system/executor.js +75 -50
- 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 +11 -1
- package/dist/tool-system/mcp-manager.js +19 -4
- 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.js +99 -2
- package/dist/tool-system/permission.d.ts +26 -0
- package/dist/tool-system/permission.js +46 -0
- package/dist/tool-system/registry.d.ts +12 -1
- package/dist/tool-system/registry.js +31 -7
- 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 +186 -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 -45
- package/dist/arena/arena.js +0 -362
- 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 -25
- package/dist/arena/phases/adjudication.js +0 -142
- package/dist/arena/phases/build-consensus.d.ts +0 -30
- package/dist/arena/phases/build-consensus.js +0 -85
- 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 -47
- package/dist/arena/phases/cross-review.js +0 -224
- package/dist/arena/phases/debate-rounds.d.ts +0 -28
- package/dist/arena/phases/debate-rounds.js +0 -161
- package/dist/arena/phases/participant-research.d.ts +0 -39
- package/dist/arena/phases/participant-research.js +0 -316
- package/dist/arena/phases/planning-detail-expansion.d.ts +0 -39
- package/dist/arena/phases/planning-detail-expansion.js +0 -120
- package/dist/arena/planner.d.ts +0 -28
- package/dist/arena/planner.js +0 -312
- 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 -518
- 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 -72
- package/dist/git/worktree/crud.js +0 -235
- 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 -87
- 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 -43
- package/dist/git/worktree/query.js +0 -128
- 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 -416
- 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 -363
- /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,10 +15,9 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { Methods, ErrorCodes, createResponse, createErrorResponse, createNotification, isRequest, } from "./types.js";
|
|
17
17
|
import { diskDefaultsFrom } from "../engine/engine.js";
|
|
18
|
-
import { isProtectedSettingKey } from "../settings/manager.js";
|
|
18
|
+
import { isProtectedSettingKey, SettingsManager } from "../settings/manager.js";
|
|
19
19
|
import { getApprovalRouter, getInteractiveApprovalBackend, } from "../tool-system/permission.js";
|
|
20
|
-
import {
|
|
21
|
-
import { agentNotificationBus, notificationQueue, buildNotificationMessage, } from "../tool-system/builtin/agent-notifications.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";
|
|
@@ -27,6 +26,8 @@ import { nanoid } from "nanoid";
|
|
|
27
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";
|
|
30
31
|
function isValidRunAttachment(value) {
|
|
31
32
|
if (!value || typeof value !== "object")
|
|
32
33
|
return false;
|
|
@@ -48,6 +49,30 @@ function runInputError(params) {
|
|
|
48
49
|
if (params.task.trim().length === 0 && !hasAttachment) {
|
|
49
50
|
return "task or a valid attachment is required";
|
|
50
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.
|
|
51
76
|
return null;
|
|
52
77
|
}
|
|
53
78
|
const COMPACT_STREAM_STRATEGIES = new Set([
|
|
@@ -57,14 +82,146 @@ const COMPACT_STREAM_STRATEGIES = new Set([
|
|
|
57
82
|
"snip",
|
|
58
83
|
"emergency",
|
|
59
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",
|
|
60
88
|
]);
|
|
61
89
|
function toCompactStreamStrategy(strategy) {
|
|
62
90
|
return COMPACT_STREAM_STRATEGIES.has(strategy)
|
|
63
91
|
? strategy
|
|
64
92
|
: "compacted";
|
|
65
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
|
+
}
|
|
66
218
|
export class AgentServer {
|
|
67
|
-
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();
|
|
68
225
|
legacyEngine;
|
|
69
226
|
globalQueryEngine = null;
|
|
70
227
|
transport;
|
|
@@ -72,13 +229,24 @@ export class AgentServer {
|
|
|
72
229
|
settingsReader;
|
|
73
230
|
/** Disk-only active-goal reader for agent/goalGet on a non-live session. */
|
|
74
231
|
readActiveGoalFromDisk;
|
|
232
|
+
updateActiveGoalOnDisk;
|
|
233
|
+
clearActiveGoalOnDisk;
|
|
75
234
|
workspaceBridgeEnabled;
|
|
235
|
+
panelBridgeEnabled;
|
|
76
236
|
connectionId;
|
|
77
237
|
strictApprovalRouting;
|
|
78
238
|
approvalRouter;
|
|
79
239
|
approvalConnectionUnregister = null;
|
|
80
240
|
disconnected = false;
|
|
81
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;
|
|
82
250
|
/**
|
|
83
251
|
* Last per-session EngineConfigSlice supplied by agent/run. Kept even after
|
|
84
252
|
* idle eviction so background-completion wakeups can rebuild the ChatSession
|
|
@@ -88,6 +256,8 @@ export class AgentServer {
|
|
|
88
256
|
lastSliceBySession = new Map();
|
|
89
257
|
/** Lazy disk reader for cold wakeup rehydrate when this process lacks a slice. */
|
|
90
258
|
diskSessionReader = null;
|
|
259
|
+
/** Sessions dir for diskSessionReader; undefined → default sessions root. */
|
|
260
|
+
sessionDiskRoot;
|
|
91
261
|
/**
|
|
92
262
|
* Monotonic config-reload version, bumped per reloadSettings request so each
|
|
93
263
|
* Engine.refreshRuntimeConfig can drop out-of-order (stale) deliveries (Q5).
|
|
@@ -129,12 +299,40 @@ export class AgentServer {
|
|
|
129
299
|
*/
|
|
130
300
|
bgAgentBusUnsubscribe = null;
|
|
131
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
|
+
}
|
|
132
325
|
constructor(options) {
|
|
133
|
-
this.
|
|
326
|
+
this.baseChatManager = options.chatManager ?? null;
|
|
327
|
+
this.resolveIdentity = options.resolveIdentity ?? null;
|
|
328
|
+
this.sessionDiskRoot = options.sessionDiskRoot;
|
|
134
329
|
this.legacyEngine = options.engine ?? null;
|
|
135
330
|
this.settingsReader = options.settingsReader ?? null;
|
|
136
331
|
this.readActiveGoalFromDisk = options.readActiveGoalFromDisk ?? null;
|
|
332
|
+
this.updateActiveGoalOnDisk = options.updateActiveGoalOnDisk ?? null;
|
|
333
|
+
this.clearActiveGoalOnDisk = options.clearActiveGoalOnDisk ?? null;
|
|
137
334
|
this.workspaceBridgeEnabled = options.workspaceBridge === true;
|
|
335
|
+
this.panelBridgeEnabled = options.panelBridge === true;
|
|
138
336
|
this.connectionId = options.connectionId ?? "legacy-agent-server";
|
|
139
337
|
this.strictApprovalRouting = options.connectionId !== undefined;
|
|
140
338
|
this.approvalRouter = options.approvalRouter ?? getApprovalRouter();
|
|
@@ -142,10 +340,45 @@ export class AgentServer {
|
|
|
142
340
|
this.approvalRouter.deregister(this.connectionId, "legacy approval host replaced");
|
|
143
341
|
}
|
|
144
342
|
getInteractiveApprovalBackend().clearPromptFn();
|
|
145
|
-
if (!this.
|
|
343
|
+
if (!this.baseChatManager && !this.legacyEngine) {
|
|
146
344
|
throw new Error("AgentServer: either chatManager or engine must be supplied");
|
|
147
345
|
}
|
|
148
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
|
+
}
|
|
149
382
|
// Wire up incoming messages
|
|
150
383
|
this.transport.onMessage((msg) => {
|
|
151
384
|
if (isRequest(msg)) {
|
|
@@ -177,8 +410,11 @@ export class AgentServer {
|
|
|
177
410
|
// polls (background-jobs.ts). The bus now guarantees a real sessionId
|
|
178
411
|
// (no more legacy-bucket coercion to ""), so we forward whatever sid it
|
|
179
412
|
// hands us.
|
|
180
|
-
this.bgAgentBusUnsubscribe = agentNotificationBus.subscribe((
|
|
181
|
-
|
|
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 });
|
|
182
418
|
// Background work that finishes while the session is IDLE (a
|
|
183
419
|
// run_in_background Bash like a download, a background sub-agent, or a
|
|
184
420
|
// video poll — the engine no longer parks on any of them) would otherwise
|
|
@@ -190,7 +426,8 @@ export class AgentServer {
|
|
|
190
426
|
// (trigger B) drains it at end-of-turn instead. A never-exiting dev server
|
|
191
427
|
// emits no completion, so it never wakes anything (no task/service
|
|
192
428
|
// classification needed).
|
|
193
|
-
|
|
429
|
+
if (envelope.kind === "result")
|
|
430
|
+
this.maybeWakeIdleSession(sessionId);
|
|
194
431
|
});
|
|
195
432
|
// Notify client we're ready
|
|
196
433
|
this.notify(Methods.Status, { status: "ready" });
|
|
@@ -339,8 +576,8 @@ export class AgentServer {
|
|
|
339
576
|
if (cached)
|
|
340
577
|
return { ...cached };
|
|
341
578
|
if (!this.diskSessionReader)
|
|
342
|
-
this.diskSessionReader = new SessionManager();
|
|
343
|
-
const cwd = this.diskSessionReader.
|
|
579
|
+
this.diskSessionReader = new SessionManager(this.sessionDiskRoot);
|
|
580
|
+
const cwd = this.diskSessionReader.readSessionMainRoot(sessionId);
|
|
344
581
|
if (!cwd)
|
|
345
582
|
return null;
|
|
346
583
|
return {
|
|
@@ -357,9 +594,88 @@ export class AgentServer {
|
|
|
357
594
|
session.engine.setAskUser((question, opts) => this.requestAskUserForSession(session, sid, question, opts));
|
|
358
595
|
session.engine.setBrowserBridge(this.makeBrowserBridge(session, sid));
|
|
359
596
|
session.engine.setInjectCredential((credentialId, credentialScope) => this.requestCredentialInjectForSession(session, sid, credentialId, credentialScope));
|
|
597
|
+
session.engine.setSessionMessageRouter((input) => this.routeSessionMessage(input));
|
|
360
598
|
if (this.workspaceBridgeEnabled && typeof session.engine.setWorkspaceBridge === "function") {
|
|
361
599
|
session.engine.setWorkspaceBridge(this.makeWorkspaceBridge(session, sid));
|
|
362
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;
|
|
363
679
|
}
|
|
364
680
|
// ─── Request Dispatch ───────────────────────────────────────────
|
|
365
681
|
async handleRequest(req) {
|
|
@@ -368,7 +684,7 @@ export class AgentServer {
|
|
|
368
684
|
await this.handleRun(req);
|
|
369
685
|
break;
|
|
370
686
|
case Methods.ForkSession:
|
|
371
|
-
this.handleForkSession(req);
|
|
687
|
+
await this.handleForkSession(req);
|
|
372
688
|
break;
|
|
373
689
|
case Methods.Approve:
|
|
374
690
|
this.handleApprove(req);
|
|
@@ -397,9 +713,18 @@ export class AgentServer {
|
|
|
397
713
|
case Methods.ReleaseWorkspace:
|
|
398
714
|
this.handleReleaseWorkspace(req);
|
|
399
715
|
break;
|
|
716
|
+
case Methods.SetWorkspace:
|
|
717
|
+
this.handleSetWorkspace(req);
|
|
718
|
+
break;
|
|
400
719
|
case Methods.GoalExtend:
|
|
401
720
|
this.handleGoalExtend(req);
|
|
402
721
|
break;
|
|
722
|
+
case Methods.GoalUpdate:
|
|
723
|
+
await this.handleGoalUpdate(req);
|
|
724
|
+
break;
|
|
725
|
+
case Methods.GoalDelete:
|
|
726
|
+
this.handleGoalClear(req, true);
|
|
727
|
+
break;
|
|
403
728
|
case Methods.GoalClear:
|
|
404
729
|
this.handleGoalClear(req);
|
|
405
730
|
break;
|
|
@@ -412,31 +737,210 @@ export class AgentServer {
|
|
|
412
737
|
case Methods.BackgroundWork:
|
|
413
738
|
this.handleBackgroundWork(req);
|
|
414
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;
|
|
415
751
|
default:
|
|
416
752
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.MethodNotFound, `Unknown method: ${req.method}`));
|
|
417
753
|
}
|
|
418
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
|
+
}
|
|
419
889
|
// ─── Run ────────────────────────────────────────────────────────
|
|
420
|
-
handleForkSession(req) {
|
|
890
|
+
async handleForkSession(req) {
|
|
421
891
|
const params = req.params ?? {};
|
|
422
892
|
const sourceSessionId = typeof params.sourceSessionId === "string" ? params.sourceSessionId : "";
|
|
423
893
|
const targetSessionId = typeof params.targetSessionId === "string" ? params.targetSessionId : undefined;
|
|
424
894
|
const throughEventId = typeof params.throughEventId === "string" ? params.throughEventId : undefined;
|
|
425
|
-
|
|
426
|
-
|
|
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"));
|
|
427
901
|
return;
|
|
428
902
|
}
|
|
429
|
-
if (params.targetSessionId !== undefined &&
|
|
430
|
-
|
|
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"));
|
|
431
906
|
return;
|
|
432
907
|
}
|
|
433
908
|
if (params.throughEventId !== undefined && typeof params.throughEventId !== "string") {
|
|
434
909
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "throughEventId must be a string"));
|
|
435
910
|
return;
|
|
436
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
|
+
}
|
|
437
941
|
try {
|
|
438
942
|
assertSafeSessionId(sourceSessionId);
|
|
439
|
-
if (targetSessionId)
|
|
943
|
+
if (targetSessionId !== undefined)
|
|
440
944
|
assertSafeSessionId(targetSessionId);
|
|
441
945
|
}
|
|
442
946
|
catch (err) {
|
|
@@ -446,12 +950,30 @@ export class AgentServer {
|
|
|
446
950
|
let source;
|
|
447
951
|
try {
|
|
448
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
|
+
}
|
|
449
967
|
}
|
|
450
968
|
catch (err) {
|
|
451
969
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.Overloaded, err instanceof Error ? err.message : String(err)));
|
|
452
970
|
return;
|
|
453
971
|
}
|
|
454
|
-
|
|
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;
|
|
455
977
|
if (!engine || !engine.sessionExistsOnDisk(sourceSessionId)) {
|
|
456
978
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionNotFound, `Session not found: ${sourceSessionId}`));
|
|
457
979
|
return;
|
|
@@ -461,7 +983,56 @@ export class AgentServer {
|
|
|
461
983
|
return;
|
|
462
984
|
}
|
|
463
985
|
try {
|
|
464
|
-
|
|
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 });
|
|
465
1036
|
const workspace = result.bundle.state.workspace ?? {
|
|
466
1037
|
root: result.bundle.state.cwd,
|
|
467
1038
|
kind: "main",
|
|
@@ -478,9 +1049,11 @@ export class AgentServer {
|
|
|
478
1049
|
const message = err instanceof Error ? err.message : String(err);
|
|
479
1050
|
const code = /not found/i.test(message)
|
|
480
1051
|
? ErrorCodes.SessionNotFound
|
|
481
|
-
: /
|
|
482
|
-
? ErrorCodes.
|
|
483
|
-
:
|
|
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;
|
|
484
1057
|
this.transport.send(createErrorResponse(req.id, code, message));
|
|
485
1058
|
}
|
|
486
1059
|
}
|
|
@@ -499,7 +1072,7 @@ export class AgentServer {
|
|
|
499
1072
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "sessionId is required"));
|
|
500
1073
|
return;
|
|
501
1074
|
}
|
|
502
|
-
const inputError =
|
|
1075
|
+
const inputError = this.validateRunInput(params);
|
|
503
1076
|
if (inputError) {
|
|
504
1077
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, inputError));
|
|
505
1078
|
return;
|
|
@@ -512,7 +1085,7 @@ export class AgentServer {
|
|
|
512
1085
|
: undefined,
|
|
513
1086
|
};
|
|
514
1087
|
if (params.requireExisting === true && !cm.get(params.sessionId)) {
|
|
515
|
-
let existsOnDisk
|
|
1088
|
+
let existsOnDisk;
|
|
516
1089
|
try {
|
|
517
1090
|
existsOnDisk = cm.sessionExistsOnDisk(params.sessionId, sessionConfig);
|
|
518
1091
|
}
|
|
@@ -536,6 +1109,7 @@ export class AgentServer {
|
|
|
536
1109
|
return;
|
|
537
1110
|
}
|
|
538
1111
|
const sid = params.sessionId;
|
|
1112
|
+
this.observeSessionAttached(sid, session.lastActivityAt);
|
|
539
1113
|
const approvalRegistration = this.approvalRouter.register(sid, this.connectionId);
|
|
540
1114
|
if (!approvalRegistration.ok) {
|
|
541
1115
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.Overloaded, `Session ${sid} is owned by another connection`));
|
|
@@ -560,27 +1134,56 @@ export class AgentServer {
|
|
|
560
1134
|
// these host callbacks must be installed after every create/recreate.
|
|
561
1135
|
this.wireInteractiveSession(session, sid);
|
|
562
1136
|
try {
|
|
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
|
+
}
|
|
563
1149
|
const run = session.enqueueTurn(params.task, {
|
|
564
1150
|
cwd: params.cwd,
|
|
1151
|
+
displayText: displayText || undefined,
|
|
1152
|
+
injected: params.injected === true,
|
|
565
1153
|
attachments: Array.isArray(params.attachments) ? params.attachments : undefined,
|
|
566
1154
|
goal: typeof params.goal === "string" ||
|
|
567
1155
|
(params.goal != null && typeof params.goal === "object")
|
|
568
1156
|
? params.goal
|
|
569
1157
|
: undefined,
|
|
570
|
-
onStream: (event) =>
|
|
1158
|
+
onStream: (event) => {
|
|
1159
|
+
this.observeSessionStream(sid, event);
|
|
1160
|
+
this.notify(Methods.StreamEvent, { sessionId: sid, event });
|
|
1161
|
+
},
|
|
571
1162
|
clientMessageId: typeof params.clientMessageId === "string" ? params.clientMessageId : undefined,
|
|
572
1163
|
permissionMode: params.permissionMode,
|
|
573
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,
|
|
574
1173
|
approvalRouter: this.approvalRouter,
|
|
575
1174
|
});
|
|
576
1175
|
this.notify(Methods.RunAccepted, { requestId: req.id, sessionId: sid });
|
|
1176
|
+
this.observeRunBoundary(sid, "start");
|
|
577
1177
|
const result = await run;
|
|
1178
|
+
this.observeRunBoundary(sid, "end");
|
|
578
1179
|
const runResult = {
|
|
579
1180
|
text: result.text,
|
|
580
1181
|
reason: result.reason,
|
|
581
1182
|
sessionId: result.sessionId ?? sid,
|
|
582
1183
|
turnCount: result.turnCount,
|
|
583
1184
|
usage: result.usage,
|
|
1185
|
+
...(result.extensions ? { extensions: result.extensions } : {}),
|
|
1186
|
+
petWorkDelegation: result.petWorkDelegation,
|
|
584
1187
|
};
|
|
585
1188
|
this.transport.send(createResponse(req.id, runResult));
|
|
586
1189
|
// Run-boundary re-check (trigger B): the session is idle now. If a
|
|
@@ -591,6 +1194,7 @@ export class AgentServer {
|
|
|
591
1194
|
this.maybeWakeIdleSession(sid);
|
|
592
1195
|
}
|
|
593
1196
|
catch (err) {
|
|
1197
|
+
this.observeRunBoundary(sid, "error");
|
|
594
1198
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
|
|
595
1199
|
// Even on a failed run the session goes idle — re-check so a background
|
|
596
1200
|
// completion that landed during the failed run isn't orphaned.
|
|
@@ -603,7 +1207,7 @@ export class AgentServer {
|
|
|
603
1207
|
return;
|
|
604
1208
|
}
|
|
605
1209
|
const params = (req.params ?? {});
|
|
606
|
-
const inputError =
|
|
1210
|
+
const inputError = this.validateRunInput(params);
|
|
607
1211
|
if (inputError) {
|
|
608
1212
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, inputError));
|
|
609
1213
|
return;
|
|
@@ -656,6 +1260,16 @@ export class AgentServer {
|
|
|
656
1260
|
// `signal.aborted` below it's already gone.
|
|
657
1261
|
const runController = this.abortController;
|
|
658
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
|
+
}
|
|
659
1273
|
this.notify(Methods.RunAccepted, {
|
|
660
1274
|
requestId: req.id,
|
|
661
1275
|
sessionId: params.sessionId ?? "",
|
|
@@ -663,6 +1277,8 @@ export class AgentServer {
|
|
|
663
1277
|
const result = await this.legacyEngine.run(params.task, {
|
|
664
1278
|
cwd: params.cwd,
|
|
665
1279
|
sessionId: params.sessionId,
|
|
1280
|
+
displayText: displayText || undefined,
|
|
1281
|
+
injected: params.injected === true,
|
|
666
1282
|
signal: runController.signal,
|
|
667
1283
|
onStream: streamToClient,
|
|
668
1284
|
clientMessageId: typeof params.clientMessageId === "string" ? params.clientMessageId : undefined,
|
|
@@ -671,6 +1287,16 @@ export class AgentServer {
|
|
|
671
1287
|
(params.goal != null && typeof params.goal === "object")
|
|
672
1288
|
? params.goal
|
|
673
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,
|
|
674
1300
|
});
|
|
675
1301
|
const runResult = {
|
|
676
1302
|
text: result.text,
|
|
@@ -678,6 +1304,8 @@ export class AgentServer {
|
|
|
678
1304
|
sessionId: result.sessionId,
|
|
679
1305
|
turnCount: result.turnCount,
|
|
680
1306
|
usage: result.usage,
|
|
1307
|
+
...(result.extensions ? { extensions: result.extensions } : {}),
|
|
1308
|
+
petWorkDelegation: result.petWorkDelegation,
|
|
681
1309
|
};
|
|
682
1310
|
this.transport.send(createResponse(req.id, runResult));
|
|
683
1311
|
}
|
|
@@ -735,18 +1363,19 @@ export class AgentServer {
|
|
|
735
1363
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionClosed, `No such session: ${params.sessionId}`));
|
|
736
1364
|
return;
|
|
737
1365
|
}
|
|
738
|
-
const
|
|
739
|
-
if (!
|
|
1366
|
+
const entry = s.pendingApprovals.get(params.requestId);
|
|
1367
|
+
if (!entry) {
|
|
740
1368
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, `No pending approval for session ${params.sessionId}: ${params.requestId}`));
|
|
741
1369
|
return;
|
|
742
1370
|
}
|
|
743
1371
|
s.pendingApprovals.delete(params.requestId);
|
|
1372
|
+
this.observeApprovalTransition(entry.metadata, "resolved");
|
|
744
1373
|
this.pendingApprovalTargets.delete(params.requestId);
|
|
745
1374
|
// Cancel the pending timeout for requests that arm one (browser actions,
|
|
746
1375
|
// credential injection, and tool approvals). AskUserQuestion does not use
|
|
747
1376
|
// a timeout; this is harmless for those request ids.
|
|
748
1377
|
this.clearApprovalTimer(params.requestId);
|
|
749
|
-
resolve(params.decision);
|
|
1378
|
+
entry.resolve(params.decision);
|
|
750
1379
|
this.transport.send(createResponse(req.id, { ok: true }));
|
|
751
1380
|
return;
|
|
752
1381
|
}
|
|
@@ -838,37 +1467,260 @@ export class AgentServer {
|
|
|
838
1467
|
}
|
|
839
1468
|
this.transport.send(createResponse(req.id, { ok: true, limits: result }));
|
|
840
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
|
+
}
|
|
841
1663
|
/**
|
|
842
1664
|
* Clear a session's persisted active goal (CC /goal clear). Routes to the
|
|
843
1665
|
* named session, falling back to the legacy single engine (mirrors
|
|
844
1666
|
* handleGoalExtend / handleCancel's dual path). Returns { ok, cleared }.
|
|
845
1667
|
*/
|
|
846
|
-
handleGoalClear(req) {
|
|
1668
|
+
handleGoalClear(req, deleteAlias = false) {
|
|
847
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;
|
|
848
1688
|
const session = this.chatManager && typeof params.sessionId === "string"
|
|
849
1689
|
? this.chatManager.get(params.sessionId)
|
|
850
1690
|
: undefined;
|
|
851
|
-
if (!session && !this.legacyEngine) {
|
|
1691
|
+
if (!session && !this.legacyEngine && !this.clearActiveGoalOnDisk) {
|
|
852
1692
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionClosed, params.sessionId ? `No such session: ${params.sessionId}` : "sessionId is required"));
|
|
853
1693
|
return;
|
|
854
1694
|
}
|
|
1695
|
+
const clearingGoal = session
|
|
1696
|
+
? session.getGoal?.()
|
|
1697
|
+
: params.sessionId
|
|
1698
|
+
? (this.legacyEngine?.getGoal(params.sessionId) ??
|
|
1699
|
+
this.readActiveGoalFromDisk?.(params.sessionId))
|
|
1700
|
+
: undefined;
|
|
855
1701
|
const cleared = session
|
|
856
|
-
? session.clearGoal()
|
|
1702
|
+
? session.clearGoal(expected)
|
|
857
1703
|
: params.sessionId
|
|
858
|
-
? (this.legacyEngine
|
|
1704
|
+
? (this.legacyEngine?.clearGoal(params.sessionId, expected) ??
|
|
1705
|
+
this.clearActiveGoalOnDisk?.(params.sessionId, expected) ??
|
|
1706
|
+
false)
|
|
859
1707
|
: false;
|
|
860
1708
|
if (cleared && typeof params.sessionId === "string" && params.sessionId.length > 0) {
|
|
861
1709
|
this.notify(Methods.StreamEvent, {
|
|
862
1710
|
sessionId: params.sessionId,
|
|
863
|
-
event: {
|
|
1711
|
+
event: {
|
|
1712
|
+
type: "goal_cleared",
|
|
1713
|
+
...(clearingGoal?.goalId ? { goalId: clearingGoal.goalId } : {}),
|
|
1714
|
+
...(clearingGoal?.revision ? { revision: clearingGoal.revision } : {}),
|
|
1715
|
+
},
|
|
864
1716
|
});
|
|
865
1717
|
}
|
|
866
|
-
this.transport.send(createResponse(req.id, { ok: true, cleared }));
|
|
1718
|
+
this.transport.send(createResponse(req.id, deleteAlias ? { ok: true, deleted: cleared } : { ok: true, cleared }));
|
|
867
1719
|
}
|
|
868
1720
|
/**
|
|
869
1721
|
* Read a session's persisted active goal so the host can re-surface the goal
|
|
870
1722
|
* block + Cancel button on session load. A persistent goal lives only in
|
|
871
|
-
* state.
|
|
1723
|
+
* state.goalLifecycle and is never replayed from the transcript, so a reloaded
|
|
872
1724
|
* (or disk-rebuilt) session has no other way to learn it. Prefers a live
|
|
873
1725
|
* chatManager session, else falls through to a disk read (readActiveGoalFromDisk
|
|
874
1726
|
* in worker mode, or the legacy engine's disk read in single-engine mode) — the
|
|
@@ -897,7 +1749,13 @@ export class AgentServer {
|
|
|
897
1749
|
: (this.readActiveGoalFromDisk?.(params.sessionId) ??
|
|
898
1750
|
this.legacyEngine?.getGoal(params.sessionId) ??
|
|
899
1751
|
undefined);
|
|
900
|
-
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
|
+
}));
|
|
901
1759
|
}
|
|
902
1760
|
/**
|
|
903
1761
|
* Query/control a session's background shells for the desktop UI panel (TODO
|
|
@@ -974,10 +1832,11 @@ export class AgentServer {
|
|
|
974
1832
|
if (this.chatManager) {
|
|
975
1833
|
const session = this.chatManager.get(params.sessionId);
|
|
976
1834
|
if (session) {
|
|
977
|
-
this.cancelSessionApprovals(session, "session closed");
|
|
1835
|
+
this.cancelSessionApprovals(session, "session closed", "cancelled", "session_closed");
|
|
978
1836
|
}
|
|
979
1837
|
this.approvalRouter.release(params.sessionId, this.connectionId, "session closed");
|
|
980
1838
|
await this.chatManager.close(params.sessionId);
|
|
1839
|
+
this.observeSessionClosed(params.sessionId);
|
|
981
1840
|
}
|
|
982
1841
|
// Explicit session teardown — reap that session's background shells
|
|
983
1842
|
// (design §6 "session 被显式删除 → killSession"). This is the RPC path
|
|
@@ -1012,6 +1871,33 @@ export class AgentServer {
|
|
|
1012
1871
|
const workspace = engine?.releaseSessionWorkspace?.(params.sessionId) ?? null;
|
|
1013
1872
|
this.transport.send(createResponse(req.id, { ok: true, workspace }));
|
|
1014
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
|
+
}
|
|
1015
1901
|
// ─── Configure ──────────────────────────────────────────────────
|
|
1016
1902
|
handleConfigure(req) {
|
|
1017
1903
|
const params = (req.params ?? {});
|
|
@@ -1177,6 +2063,51 @@ export class AgentServer {
|
|
|
1177
2063
|
this.transport.send(createResponse(req.id, { ok: true }));
|
|
1178
2064
|
}
|
|
1179
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
|
+
}
|
|
1180
2111
|
async handleQuery(req) {
|
|
1181
2112
|
const params = (req.params ?? {});
|
|
1182
2113
|
// For query operations we prefer the legacyEngine; if absent borrow any
|
|
@@ -1199,20 +2130,14 @@ export class AgentServer {
|
|
|
1199
2130
|
}
|
|
1200
2131
|
case "sessions": {
|
|
1201
2132
|
if (this.chatManager) {
|
|
1202
|
-
//
|
|
1203
|
-
const sessions =
|
|
1204
|
-
this.chatManager.forEachSession((s) => {
|
|
1205
|
-
sessions.push({
|
|
1206
|
-
sessionId: s.id,
|
|
1207
|
-
busy: s.isBusy(),
|
|
1208
|
-
queueDepth: s.queueDepth(),
|
|
1209
|
-
lastActivityAt: s.lastActivityAt,
|
|
1210
|
-
});
|
|
1211
|
-
});
|
|
2133
|
+
// Pet projection and the legacy query share this one live-state builder.
|
|
2134
|
+
const { sessions } = this.chatManager.getLiveSessionSnapshot();
|
|
1212
2135
|
this.transport.send(createResponse(req.id, { type: "sessions", data: sessions }));
|
|
1213
2136
|
}
|
|
1214
2137
|
else if (engine) {
|
|
1215
|
-
const sessions = engine
|
|
2138
|
+
const sessions = engine
|
|
2139
|
+
.getSessionManager()
|
|
2140
|
+
.list(undefined, { excludeKinds: this.hiddenSessionKinds });
|
|
1216
2141
|
this.transport.send(createResponse(req.id, { type: "sessions", data: sessions }));
|
|
1217
2142
|
}
|
|
1218
2143
|
else {
|
|
@@ -1274,40 +2199,9 @@ export class AgentServer {
|
|
|
1274
2199
|
const compactSessionId = typeof params.sessionId === "string" && params.sessionId.length > 0
|
|
1275
2200
|
? params.sessionId
|
|
1276
2201
|
: undefined;
|
|
1277
|
-
|
|
1278
|
-
if (
|
|
1279
|
-
if (compactSessionId) {
|
|
1280
|
-
if (this.chatManager.isUnavailable(compactSessionId)) {
|
|
1281
|
-
this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionClosed, `Session is closing or closed: ${compactSessionId}`));
|
|
1282
|
-
return;
|
|
1283
|
-
}
|
|
1284
|
-
let session = this.chatManager.get(compactSessionId);
|
|
1285
|
-
if (!session) {
|
|
1286
|
-
const probeEngine = this.anyEngine();
|
|
1287
|
-
if (!probeEngine?.sessionExistsOnDisk(compactSessionId)) {
|
|
1288
|
-
this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionNotFound, `Session not found: ${compactSessionId}`));
|
|
1289
|
-
return;
|
|
1290
|
-
}
|
|
1291
|
-
try {
|
|
1292
|
-
session = await this.chatManager.getOrCreate(compactSessionId, {
|
|
1293
|
-
cwd: probeEngine.getSessionManager().readCwd(compactSessionId),
|
|
1294
|
-
});
|
|
1295
|
-
}
|
|
1296
|
-
catch (err) {
|
|
1297
|
-
this.transport.send(createErrorResponse(req.id, err.code ?? ErrorCodes.InternalError, err.message));
|
|
1298
|
-
return;
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1301
|
-
compactEngine = session.engine;
|
|
1302
|
-
}
|
|
1303
|
-
else {
|
|
1304
|
-
compactEngine = this.anyEngine();
|
|
1305
|
-
}
|
|
1306
|
-
}
|
|
1307
|
-
if (!compactEngine) {
|
|
1308
|
-
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)
|
|
1309
2204
|
return;
|
|
1310
|
-
}
|
|
1311
2205
|
try {
|
|
1312
2206
|
const result = await compactEngine.forceCompact(compactSessionId);
|
|
1313
2207
|
if (result.before > result.after) {
|
|
@@ -1332,6 +2226,45 @@ export class AgentServer {
|
|
|
1332
2226
|
}
|
|
1333
2227
|
break;
|
|
1334
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
|
+
}
|
|
1335
2268
|
case "models": {
|
|
1336
2269
|
if (!engine) {
|
|
1337
2270
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, "No engine available for models query"));
|
|
@@ -1400,11 +2333,6 @@ export class AgentServer {
|
|
|
1400
2333
|
}
|
|
1401
2334
|
break;
|
|
1402
2335
|
}
|
|
1403
|
-
case "arena_status": {
|
|
1404
|
-
const status = getArenaStatus();
|
|
1405
|
-
this.transport.send(createResponse(req.id, { type: "arena_status", data: status }));
|
|
1406
|
-
break;
|
|
1407
|
-
}
|
|
1408
2336
|
case "config_set": {
|
|
1409
2337
|
if (!engine) {
|
|
1410
2338
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, "No engine available for config_set"));
|
|
@@ -1593,8 +2521,16 @@ export class AgentServer {
|
|
|
1593
2521
|
}
|
|
1594
2522
|
break;
|
|
1595
2523
|
}
|
|
1596
|
-
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
|
+
}
|
|
1597
2532
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, `Unknown query type: ${params.type}`));
|
|
2533
|
+
}
|
|
1598
2534
|
}
|
|
1599
2535
|
}
|
|
1600
2536
|
// ─── Inject ─────────────────────────────────────────────────────
|
|
@@ -1708,9 +2644,21 @@ export class AgentServer {
|
|
|
1708
2644
|
return;
|
|
1709
2645
|
}
|
|
1710
2646
|
if (session) {
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
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));
|
|
1714
2662
|
}
|
|
1715
2663
|
else {
|
|
1716
2664
|
this.pendingApprovals.set(requestId, resolve);
|
|
@@ -1719,7 +2667,12 @@ export class AgentServer {
|
|
|
1719
2667
|
const timer = setTimeout(() => {
|
|
1720
2668
|
const pending = session?.pendingApprovals ?? this.pendingApprovals;
|
|
1721
2669
|
if (pending.has(requestId)) {
|
|
1722
|
-
|
|
2670
|
+
if (session) {
|
|
2671
|
+
this.takeSessionApproval(session, requestId, "expired");
|
|
2672
|
+
}
|
|
2673
|
+
else {
|
|
2674
|
+
pending.delete(requestId);
|
|
2675
|
+
}
|
|
1723
2676
|
this.pendingApprovalTargets.delete(requestId);
|
|
1724
2677
|
this.approvalTimers.delete(requestId);
|
|
1725
2678
|
resolve({ approved: false, reason: "approval timed out" });
|
|
@@ -1753,7 +2706,17 @@ export class AgentServer {
|
|
|
1753
2706
|
requestAskUserForSession(session, sessionId, question, opts) {
|
|
1754
2707
|
return new Promise((resolve) => {
|
|
1755
2708
|
const requestId = nanoid(12);
|
|
1756
|
-
|
|
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) => {
|
|
1757
2720
|
this.clearApprovalTimer(requestId);
|
|
1758
2721
|
const result = decision;
|
|
1759
2722
|
if (result && typeof result === "object" && "approved" in result) {
|
|
@@ -1776,7 +2739,7 @@ export class AgentServer {
|
|
|
1776
2739
|
if (opts?.optionsOnly !== undefined)
|
|
1777
2740
|
args.optionsOnly = opts.optionsOnly;
|
|
1778
2741
|
this.notify(Methods.ApprovalRequest, {
|
|
1779
|
-
...
|
|
2742
|
+
...routeEnvelope,
|
|
1780
2743
|
request: {
|
|
1781
2744
|
toolName: "__ask_user__",
|
|
1782
2745
|
args,
|
|
@@ -1801,12 +2764,11 @@ export class AgentServer {
|
|
|
1801
2764
|
}
|
|
1802
2765
|
if (goalActive) {
|
|
1803
2766
|
const timer = setTimeout(() => {
|
|
1804
|
-
const
|
|
1805
|
-
if (
|
|
1806
|
-
session.pendingApprovals.delete(requestId);
|
|
2767
|
+
const entry = this.takeSessionApproval(session, requestId, "expired");
|
|
2768
|
+
if (entry) {
|
|
1807
2769
|
this.pendingApprovalTargets.delete(requestId);
|
|
1808
2770
|
this.approvalTimers.delete(requestId);
|
|
1809
|
-
|
|
2771
|
+
entry.resolve("(用户未在限定时间内回答;目标运行中请自行做出合理假设并继续推进。)");
|
|
1810
2772
|
// Tell every client the ask is resolved (nobody answered) so the
|
|
1811
2773
|
// stale AskUserQuestion card is dismissed instead of lingering as
|
|
1812
2774
|
// if still waiting. Reuses the same envelope shape the desktop main
|
|
@@ -1833,7 +2795,7 @@ export class AgentServer {
|
|
|
1833
2795
|
type: (ref, text) => call("type", { ref, text }),
|
|
1834
2796
|
navigate: (url) => call("navigate", { url }),
|
|
1835
2797
|
scroll: (dir, amount) => call("scroll", { dir, amount }),
|
|
1836
|
-
readContent: () => call("readContent", {}),
|
|
2798
|
+
readContent: (options) => call("readContent", { ...(options ?? {}) }),
|
|
1837
2799
|
extractLinks: () => call("extractLinks", {}),
|
|
1838
2800
|
waitForLoad: (timeoutMs) => call("waitForLoad", { timeoutMs }),
|
|
1839
2801
|
hover: (ref) => call("hover", { ref }),
|
|
@@ -1848,40 +2810,31 @@ export class AgentServer {
|
|
|
1848
2810
|
requestBrowserActionForSession(session, sessionId, action, payload) {
|
|
1849
2811
|
return new Promise((resolve) => {
|
|
1850
2812
|
const requestId = nanoid(12);
|
|
1851
|
-
|
|
2813
|
+
const routeEnvelope = this.approvalRouteEnvelope(sessionId, requestId);
|
|
2814
|
+
this.registerSessionApproval(session, this.internalPendingMetadata(sessionId, requestId, routeEnvelope, "__browser_action__"), (decision) => {
|
|
1852
2815
|
this.clearApprovalTimer(requestId);
|
|
1853
2816
|
// Main replies with { approved:true, answer:<json string> } (reusing the
|
|
1854
2817
|
// ApprovalResult shape) or a raw json string. Parse → typed result.
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
}
|
|
1860
|
-
else if (typeof decision === "string") {
|
|
1861
|
-
raw = decision;
|
|
1862
|
-
}
|
|
1863
|
-
if (raw === undefined) {
|
|
1864
|
-
resolve({ ok: false, detail: "browser action declined or unavailable" });
|
|
1865
|
-
return;
|
|
1866
|
-
}
|
|
1867
|
-
try {
|
|
1868
|
-
resolve(JSON.parse(raw));
|
|
1869
|
-
}
|
|
1870
|
-
catch {
|
|
1871
|
-
resolve({ ok: false, detail: "malformed browser action result" });
|
|
1872
|
-
}
|
|
2818
|
+
const parsed = parseHostLoopbackDecision(decision, "browser action");
|
|
2819
|
+
resolve(parsed.ok
|
|
2820
|
+
? parsed.value
|
|
2821
|
+
: { ok: false, failure: parsed.failure, detail: parsed.detail });
|
|
1873
2822
|
});
|
|
1874
2823
|
const timer = setTimeout(() => {
|
|
1875
2824
|
if (session.pendingApprovals.has(requestId)) {
|
|
1876
|
-
|
|
2825
|
+
this.takeSessionApproval(session, requestId, "expired");
|
|
1877
2826
|
this.pendingApprovalTargets.delete(requestId);
|
|
1878
2827
|
this.approvalTimers.delete(requestId);
|
|
1879
|
-
resolve({
|
|
2828
|
+
resolve({
|
|
2829
|
+
ok: false,
|
|
2830
|
+
failure: "timed_out",
|
|
2831
|
+
detail: `browser action ${HOST_LOOPBACK_FAILURE_DETAIL.timed_out}`,
|
|
2832
|
+
});
|
|
1880
2833
|
}
|
|
1881
2834
|
}, AgentServer.APPROVAL_TIMEOUT_MS);
|
|
1882
2835
|
this.approvalTimers.set(requestId, timer);
|
|
1883
2836
|
this.notify(Methods.ApprovalRequest, {
|
|
1884
|
-
...
|
|
2837
|
+
...routeEnvelope,
|
|
1885
2838
|
request: {
|
|
1886
2839
|
toolName: "__browser_action__",
|
|
1887
2840
|
args: { action, ...payload },
|
|
@@ -1900,38 +2853,28 @@ export class AgentServer {
|
|
|
1900
2853
|
requestCredentialInjectForSession(session, sessionId, credentialId, credentialScope = "full") {
|
|
1901
2854
|
return new Promise((resolve) => {
|
|
1902
2855
|
const requestId = nanoid(12);
|
|
1903
|
-
|
|
2856
|
+
const routeEnvelope = this.approvalRouteEnvelope(sessionId, requestId);
|
|
2857
|
+
this.registerSessionApproval(session, this.internalPendingMetadata(sessionId, requestId, routeEnvelope, "__credential_action__"), (decision) => {
|
|
1904
2858
|
this.clearApprovalTimer(requestId);
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
}
|
|
1910
|
-
else if (typeof decision === "string") {
|
|
1911
|
-
raw = decision;
|
|
1912
|
-
}
|
|
1913
|
-
if (raw === undefined) {
|
|
1914
|
-
resolve({ ok: false, error: "credential inject declined or unavailable" });
|
|
1915
|
-
return;
|
|
1916
|
-
}
|
|
1917
|
-
try {
|
|
1918
|
-
resolve(JSON.parse(raw));
|
|
1919
|
-
}
|
|
1920
|
-
catch {
|
|
1921
|
-
resolve({ ok: false, error: "malformed credential inject result" });
|
|
1922
|
-
}
|
|
2859
|
+
const parsed = parseHostLoopbackDecision(decision, "credential inject");
|
|
2860
|
+
resolve(parsed.ok
|
|
2861
|
+
? parsed.value
|
|
2862
|
+
: { ok: false, error: parsed.detail });
|
|
1923
2863
|
});
|
|
1924
2864
|
const timer = setTimeout(() => {
|
|
1925
2865
|
if (session.pendingApprovals.has(requestId)) {
|
|
1926
|
-
|
|
2866
|
+
this.takeSessionApproval(session, requestId, "expired");
|
|
1927
2867
|
this.pendingApprovalTargets.delete(requestId);
|
|
1928
2868
|
this.approvalTimers.delete(requestId);
|
|
1929
|
-
resolve({
|
|
2869
|
+
resolve({
|
|
2870
|
+
ok: false,
|
|
2871
|
+
error: `credential inject ${HOST_LOOPBACK_FAILURE_DETAIL.timed_out}`,
|
|
2872
|
+
});
|
|
1930
2873
|
}
|
|
1931
2874
|
}, AgentServer.APPROVAL_TIMEOUT_MS);
|
|
1932
2875
|
this.approvalTimers.set(requestId, timer);
|
|
1933
2876
|
this.notify(Methods.ApprovalRequest, {
|
|
1934
|
-
...
|
|
2877
|
+
...routeEnvelope,
|
|
1935
2878
|
request: {
|
|
1936
2879
|
toolName: "__credential_action__",
|
|
1937
2880
|
args: { action: "injectCookie", credentialId, credentialScope },
|
|
@@ -1946,46 +2889,145 @@ export class AgentServer {
|
|
|
1946
2889
|
switch: (target) => this.requestWorkspaceSwitchForSession(session, sessionId, target),
|
|
1947
2890
|
};
|
|
1948
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
|
+
}
|
|
1949
2992
|
requestWorkspaceSwitchForSession(session, sessionId, target) {
|
|
1950
2993
|
return new Promise((resolve, reject) => {
|
|
1951
2994
|
const requestId = nanoid(12);
|
|
1952
|
-
|
|
2995
|
+
const routeEnvelope = this.approvalRouteEnvelope(sessionId, requestId);
|
|
2996
|
+
this.registerSessionApproval(session, this.internalPendingMetadata(sessionId, requestId, routeEnvelope, "__workspace_action__"), (decision) => {
|
|
1953
2997
|
this.clearApprovalTimer(requestId);
|
|
1954
|
-
|
|
1955
|
-
if (
|
|
1956
|
-
|
|
1957
|
-
raw = r.approved ? r.answer : undefined;
|
|
1958
|
-
}
|
|
1959
|
-
else if (typeof decision === "string") {
|
|
1960
|
-
raw = decision;
|
|
1961
|
-
}
|
|
1962
|
-
if (raw === undefined) {
|
|
1963
|
-
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));
|
|
1964
3001
|
return;
|
|
1965
3002
|
}
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
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;
|
|
1973
3013
|
}
|
|
1974
|
-
|
|
1975
|
-
reject(new Error("
|
|
3014
|
+
if ("ok" in parsed && parsed.ok === false) {
|
|
3015
|
+
reject(new Error(parsed.error ?? "workspace switch failed"));
|
|
3016
|
+
return;
|
|
1976
3017
|
}
|
|
3018
|
+
resolve(parsed);
|
|
1977
3019
|
});
|
|
1978
3020
|
const timer = setTimeout(() => {
|
|
1979
3021
|
if (session.pendingApprovals.has(requestId)) {
|
|
1980
|
-
|
|
3022
|
+
this.takeSessionApproval(session, requestId, "expired");
|
|
1981
3023
|
this.pendingApprovalTargets.delete(requestId);
|
|
1982
3024
|
this.approvalTimers.delete(requestId);
|
|
1983
|
-
reject(new Error(
|
|
3025
|
+
reject(new Error(`workspace switch ${HOST_LOOPBACK_FAILURE_DETAIL.timed_out}`));
|
|
1984
3026
|
}
|
|
1985
3027
|
}, AgentServer.APPROVAL_TIMEOUT_MS);
|
|
1986
3028
|
this.approvalTimers.set(requestId, timer);
|
|
1987
3029
|
this.notify(Methods.ApprovalRequest, {
|
|
1988
|
-
...
|
|
3030
|
+
...routeEnvelope,
|
|
1989
3031
|
request: {
|
|
1990
3032
|
toolName: "__workspace_action__",
|
|
1991
3033
|
args: { action: "switch", target },
|
|
@@ -2059,6 +3101,7 @@ export class AgentServer {
|
|
|
2059
3101
|
}
|
|
2060
3102
|
// ─── Lifecycle ──────────────────────────────────────────────────
|
|
2061
3103
|
close() {
|
|
3104
|
+
this.observeServerClose();
|
|
2062
3105
|
this.disconnect("server closing");
|
|
2063
3106
|
// Detach the bg-agent bus subscription first so a final flurry of
|
|
2064
3107
|
// completions during shutdown can't race the `shutdown` status
|
|
@@ -2073,11 +3116,17 @@ export class AgentServer {
|
|
|
2073
3116
|
}
|
|
2074
3117
|
this.bgAgentBusUnsubscribe = null;
|
|
2075
3118
|
}
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
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
|
+
}
|
|
2081
3130
|
}
|
|
2082
3131
|
// Legacy path cleanup
|
|
2083
3132
|
if (this.abortController) {
|
|
@@ -2108,6 +3157,47 @@ export class AgentServer {
|
|
|
2108
3157
|
this.approvalRouter.deregister(this.connectionId, reason);
|
|
2109
3158
|
}
|
|
2110
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
|
+
}
|
|
2111
3201
|
clearApprovalTimer(requestId) {
|
|
2112
3202
|
const timer = this.approvalTimers.get(requestId);
|
|
2113
3203
|
if (timer) {
|
|
@@ -2122,15 +3212,52 @@ export class AgentServer {
|
|
|
2122
3212
|
* the tool hanging. Bounded request types have same-keyed timer entries;
|
|
2123
3213
|
* AskUserQuestion does not.
|
|
2124
3214
|
*/
|
|
2125
|
-
cancelSessionApprovals(session, reason = "cancelled") {
|
|
2126
|
-
|
|
2127
|
-
|
|
2128
|
-
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
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
|
+
}
|
|
2134
3261
|
}
|
|
2135
3262
|
}
|
|
2136
3263
|
session.pendingApprovals.clear();
|
|
@@ -2139,7 +3266,7 @@ export class AgentServer {
|
|
|
2139
3266
|
for (const target of targets) {
|
|
2140
3267
|
const session = this.chatManager?.get(target.sessionId);
|
|
2141
3268
|
if (session) {
|
|
2142
|
-
this.cancelSessionApprovals(session, reason);
|
|
3269
|
+
this.cancelSessionApprovals(session, reason, "owner-lost", "owner_lost");
|
|
2143
3270
|
continue;
|
|
2144
3271
|
}
|
|
2145
3272
|
for (const [requestId, pending] of this.pendingApprovalTargets) {
|