@cjhyy/code-shell-core 0.5.0-rc.2 → 0.6.0-rc.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +13 -10
- package/dist/agent/agent-definition-registry.d.ts +3 -0
- package/dist/agent/agent-definition-registry.js +3 -1
- package/dist/agent/agent-definition.d.ts +15 -0
- package/dist/agent/agent-definition.js +27 -3
- package/dist/arena/context/context-tools.js +47 -4
- package/dist/arena/ledger.js +9 -1
- package/dist/arena/strategies/utils.d.ts +1 -4
- package/dist/arena/strategies/utils.js +7 -84
- package/dist/automation/cron-expr.d.ts +10 -0
- package/dist/automation/cron-expr.js +23 -5
- package/dist/automation/runner.d.ts +21 -4
- package/dist/automation/runner.js +48 -12
- package/dist/automation/scheduler.d.ts +38 -0
- package/dist/automation/scheduler.js +89 -12
- package/dist/automation/store.js +5 -2
- package/dist/capability-control/disabled-lists.d.ts +25 -0
- package/dist/capability-control/disabled-lists.js +57 -0
- package/dist/capability-control/overlay.d.ts +15 -0
- package/dist/capability-control/overlay.js +27 -0
- package/dist/cc-orchestrator/agent-adapter.d.ts +50 -0
- package/dist/cc-orchestrator/agent-adapter.js +133 -0
- package/dist/cc-orchestrator/cc-capability.d.ts +19 -0
- package/dist/cc-orchestrator/cc-capability.js +53 -0
- package/dist/cc-orchestrator/codex-session-discovery.d.ts +24 -0
- package/dist/cc-orchestrator/codex-session-discovery.js +191 -0
- package/dist/cc-orchestrator/codex-session-history.d.ts +25 -0
- package/dist/cc-orchestrator/codex-session-history.js +187 -0
- package/dist/cc-orchestrator/external-agent-changes.d.ts +19 -0
- package/dist/cc-orchestrator/external-agent-changes.js +214 -0
- package/dist/cc-orchestrator/external-agent-driver.d.ts +18 -0
- package/dist/cc-orchestrator/external-agent-driver.js +69 -0
- package/dist/cc-orchestrator/index.d.ts +8 -0
- package/dist/cc-orchestrator/index.js +8 -0
- package/dist/cc-orchestrator/relevance-judge.d.ts +15 -0
- package/dist/cc-orchestrator/relevance-judge.js +29 -0
- package/dist/cc-orchestrator/session-discovery.d.ts +46 -0
- package/dist/cc-orchestrator/session-discovery.js +125 -0
- package/dist/cc-orchestrator/session-history.d.ts +19 -0
- package/dist/cc-orchestrator/session-history.js +67 -0
- package/dist/cli/agent-server-stdio.d.ts +15 -1
- package/dist/cli/agent-server-stdio.js +115 -17
- package/dist/cli/agent-server-tcp.js +40 -26
- package/dist/context/compaction.d.ts +56 -0
- package/dist/context/compaction.js +186 -0
- package/dist/context/manager.js +25 -4
- package/dist/cost-tracker.js +5 -61
- package/dist/credentials/cipher.d.ts +49 -0
- package/dist/credentials/cipher.js +45 -0
- package/dist/credentials/cookie-jar.d.ts +24 -0
- package/dist/credentials/cookie-jar.js +40 -0
- package/dist/credentials/index.d.ts +7 -0
- package/dist/credentials/index.js +5 -0
- package/dist/credentials/inject-credential-tool.d.ts +20 -0
- package/dist/credentials/inject-credential-tool.js +130 -0
- package/dist/credentials/store.d.ts +72 -0
- package/dist/credentials/store.js +184 -0
- package/dist/credentials/types.d.ts +56 -0
- package/dist/credentials/use-credential-tool.d.ts +29 -0
- package/dist/credentials/use-credential-tool.js +205 -0
- package/dist/credentials/use-gate.d.ts +56 -0
- package/dist/credentials/use-gate.js +52 -0
- package/dist/data/model-metadata.d.ts +77 -0
- package/dist/data/model-metadata.js +56 -0
- package/dist/data/model-metadata.json +216 -0
- package/dist/data/openrouter-models.d.ts +18 -7
- package/dist/data/openrouter-models.js +35 -8
- package/dist/engine/aux-key.d.ts +10 -0
- package/dist/engine/aux-key.js +11 -0
- package/dist/engine/dynamic-tool-defs.d.ts +19 -0
- package/dist/engine/dynamic-tool-defs.js +36 -0
- package/dist/engine/engine.d.ts +267 -138
- package/dist/engine/engine.js +957 -191
- package/dist/engine/friendly-error.d.ts +18 -0
- package/dist/engine/friendly-error.js +63 -0
- package/dist/engine/goal.d.ts +145 -0
- package/dist/engine/goal.js +144 -0
- package/dist/engine/image-policy.d.ts +13 -0
- package/dist/engine/image-policy.js +24 -0
- package/dist/engine/model-connections-pool.d.ts +17 -0
- package/dist/engine/model-connections-pool.js +66 -0
- package/dist/engine/model-facade.d.ts +10 -0
- package/dist/engine/model-facade.js +25 -0
- package/dist/engine/patch-orphaned-tools.js +3 -0
- package/dist/engine/resolve-llm-config.d.ts +16 -0
- package/dist/engine/resolve-llm-config.js +44 -0
- package/dist/engine/runtime.js +2 -1
- package/dist/engine/sandbox-cache-key.d.ts +10 -0
- package/dist/engine/sandbox-cache-key.js +9 -0
- package/dist/engine/sandbox-config.d.ts +31 -0
- package/dist/engine/sandbox-config.js +38 -0
- package/dist/engine/session-usage.d.ts +19 -0
- package/dist/engine/session-usage.js +25 -0
- package/dist/engine/steer-queue.d.ts +32 -0
- package/dist/engine/steer-queue.js +26 -0
- package/dist/engine/streaming-tool-queue.d.ts +12 -0
- package/dist/engine/streaming-tool-queue.js +50 -10
- package/dist/engine/turn-loop.d.ts +67 -3
- package/dist/engine/turn-loop.js +292 -41
- package/dist/engine/types.d.ts +175 -0
- package/dist/engine/types.js +13 -0
- package/dist/external-agents/config.d.ts +2 -0
- package/dist/external-agents/config.js +15 -0
- package/dist/external-agents/types.d.ts +31 -0
- package/dist/external-agents/types.js +1 -0
- package/dist/git/utils.js +11 -6
- package/dist/git/worktree.d.ts +48 -0
- package/dist/git/worktree.js +86 -10
- package/dist/hooks/goal-stop-hook.d.ts +28 -0
- package/dist/hooks/goal-stop-hook.js +186 -9
- package/dist/hooks/registry.d.ts +8 -0
- package/dist/hooks/registry.js +19 -0
- package/dist/hooks/shell-runner.js +10 -21
- package/dist/index.d.ts +49 -6
- package/dist/index.js +60 -4
- package/dist/llm/capabilities/param-specs.d.ts +14 -0
- package/dist/llm/capabilities/param-specs.js +62 -0
- package/dist/llm/capabilities/rules.js +4 -0
- package/dist/llm/capabilities/types.d.ts +10 -0
- package/dist/llm/client-base.d.ts +28 -1
- package/dist/llm/client-base.js +119 -13
- package/dist/llm/model-cache.js +4 -2
- package/dist/llm/model-pool.d.ts +14 -0
- package/dist/llm/model-pool.js +13 -0
- package/dist/llm/provider-auth.d.ts +41 -0
- package/dist/llm/provider-auth.js +76 -0
- package/dist/llm/provider-catalog.d.ts +4 -0
- package/dist/llm/providers/anthropic.js +64 -14
- package/dist/llm/providers/openai.d.ts +36 -0
- package/dist/llm/providers/openai.js +257 -51
- package/dist/llm/reasoning-setting.d.ts +3 -3
- package/dist/llm/reasoning-setting.js +9 -1
- package/dist/llm/stream-watchdog.js +5 -1
- package/dist/llm/token-counter.js +9 -2
- package/dist/llm/types.d.ts +4 -0
- package/dist/logging/sanitize-messages.js +22 -0
- package/dist/lsp/manager.d.ts +1 -1
- package/dist/lsp/manager.js +40 -10
- package/dist/model-catalog/builtin.d.ts +12 -0
- package/dist/model-catalog/builtin.js +407 -0
- package/dist/model-catalog/gen-connections.d.ts +20 -0
- package/dist/model-catalog/gen-connections.js +28 -0
- package/dist/model-catalog/index.d.ts +41 -0
- package/dist/model-catalog/index.js +90 -0
- package/dist/model-catalog/params.d.ts +20 -0
- package/dist/model-catalog/params.js +45 -0
- package/dist/model-catalog/resolve.d.ts +48 -0
- package/dist/model-catalog/resolve.js +33 -0
- package/dist/model-catalog/save-entry.d.ts +32 -0
- package/dist/model-catalog/save-entry.js +104 -0
- package/dist/model-catalog/types.d.ts +561 -0
- package/dist/model-catalog/types.js +93 -0
- package/dist/model-catalog/upsert.d.ts +9 -0
- package/dist/model-catalog/upsert.js +8 -0
- package/dist/onboarding.d.ts +12 -82
- package/dist/onboarding.js +61 -326
- package/dist/plugins/gitOps.d.ts +19 -0
- package/dist/plugins/gitOps.js +73 -4
- package/dist/plugins/installer/checkUpdate.d.ts +16 -0
- package/dist/plugins/installer/checkUpdate.js +32 -0
- package/dist/plugins/installer/codex/convertCommands.d.ts +19 -0
- package/dist/plugins/installer/codex/convertCommands.js +46 -0
- package/dist/plugins/installer/codex/convertMcp.d.ts +5 -2
- package/dist/plugins/installer/codex/convertMcp.js +45 -5
- package/dist/plugins/installer/install.js +25 -1
- package/dist/plugins/installer/installFromArchive.d.ts +43 -0
- package/dist/plugins/installer/installFromArchive.js +134 -0
- package/dist/plugins/installer/installFromSource.js +8 -2
- package/dist/plugins/installer/loadPluginAgents.js +6 -2
- package/dist/plugins/installer/loadPluginMcp.d.ts +9 -2
- package/dist/plugins/installer/loadPluginMcp.js +35 -2
- package/dist/plugins/installer/pruneDisabled.d.ts +24 -0
- package/dist/plugins/installer/pruneDisabled.js +73 -0
- package/dist/plugins/installer/types.d.ts +5 -2
- package/dist/plugins/installer/types.js +1 -0
- package/dist/plugins/installer/uninstall.js +4 -0
- package/dist/plugins/installer/unzip.d.ts +14 -0
- package/dist/plugins/installer/unzip.js +82 -0
- package/dist/plugins/installer/update.d.ts +14 -0
- package/dist/plugins/installer/update.js +53 -21
- package/dist/plugins/loadPluginHooks.d.ts +47 -1
- package/dist/plugins/loadPluginHooks.js +73 -1
- package/dist/plugins/marketplaceManager.d.ts +7 -0
- package/dist/plugins/marketplaceManager.js +20 -0
- package/dist/plugins/pluginCommandHook.js +4 -18
- package/dist/plugins/pluginContent.d.ts +30 -0
- package/dist/plugins/pluginContent.js +83 -0
- package/dist/plugins/pluginInstaller.d.ts +13 -0
- package/dist/plugins/pluginInstaller.js +42 -8
- package/dist/plugins/schemas.js +1 -0
- package/dist/plugins/types.d.ts +6 -0
- package/dist/preset/index.d.ts +7 -1
- package/dist/preset/index.js +96 -6
- package/dist/product/types.d.ts +1 -1
- package/dist/prompt/composer.d.ts +27 -0
- package/dist/prompt/composer.js +67 -21
- package/dist/prompt/instruction-scanner.js +5 -3
- package/dist/prompt/section-loader.js +1 -0
- package/dist/prompt/sections/base.md +1 -0
- package/dist/prompt/sections/browser.md +10 -0
- package/dist/prompt/sections/coding.md +4 -0
- package/dist/protocol/chat-session-manager.d.ts +8 -2
- package/dist/protocol/chat-session-manager.js +36 -0
- package/dist/protocol/chat-session.d.ts +65 -0
- package/dist/protocol/chat-session.js +94 -2
- package/dist/protocol/client.d.ts +27 -0
- package/dist/protocol/client.js +38 -0
- package/dist/protocol/server.d.ts +112 -0
- package/dist/protocol/server.js +586 -16
- package/dist/protocol/transport.js +3 -2
- package/dist/protocol/types.d.ts +46 -0
- package/dist/protocol/types.js +16 -0
- package/dist/quota/credentials.d.ts +3 -0
- package/dist/quota/credentials.js +80 -0
- package/dist/quota/index.d.ts +36 -0
- package/dist/quota/index.js +155 -0
- package/dist/quota/types.d.ts +48 -0
- package/dist/quota/types.js +13 -0
- package/dist/review/review-prompt.d.ts +28 -0
- package/dist/review/review-prompt.js +81 -0
- package/dist/run/FileRunStore.js +8 -3
- package/dist/run/RunApprovalBackend.js +25 -5
- package/dist/run/RunManager.d.ts +12 -0
- package/dist/run/RunManager.js +35 -0
- package/dist/run/factory.d.ts +1 -1
- package/dist/runtime/background-shell.d.ts +139 -0
- package/dist/runtime/background-shell.js +495 -0
- package/dist/runtime/output-clean.d.ts +24 -0
- package/dist/runtime/output-clean.js +41 -0
- package/dist/runtime/ring-file.d.ts +64 -0
- package/dist/runtime/ring-file.js +174 -0
- package/dist/runtime/safe-spawn.js +15 -34
- package/dist/runtime/spawn-common.d.ts +148 -0
- package/dist/runtime/spawn-common.js +303 -0
- package/dist/runtime/truncate-output.d.ts +22 -0
- package/dist/runtime/truncate-output.js +49 -0
- package/dist/runtime/utf8-cut.d.ts +11 -0
- package/dist/runtime/utf8-cut.js +23 -0
- package/dist/services/auto-dream.d.ts +4 -0
- package/dist/services/auto-dream.js +26 -26
- package/dist/services/diagnostics.d.ts +1 -2
- package/dist/services/diagnostics.js +12 -7
- package/dist/services/extract-memories.d.ts +14 -1
- package/dist/services/extract-memories.js +45 -6
- package/dist/services/memory-orchestrator.d.ts +21 -0
- package/dist/services/memory-orchestrator.js +125 -50
- package/dist/services/session-memory.js +24 -12
- package/dist/session/file-history.d.ts +124 -1
- package/dist/session/file-history.js +222 -6
- package/dist/session/memory.d.ts +116 -2
- package/dist/session/memory.js +250 -28
- package/dist/session/session-manager.d.ts +40 -0
- package/dist/session/session-manager.js +113 -3
- package/dist/session/simple-diff.d.ts +23 -0
- package/dist/session/simple-diff.js +84 -0
- package/dist/session/transcript.d.ts +25 -1
- package/dist/session/transcript.js +35 -2
- package/dist/session/undo-target.d.ts +67 -0
- package/dist/session/undo-target.js +144 -0
- package/dist/settings/disk-defaults.d.ts +9 -2
- package/dist/settings/disk-defaults.js +11 -2
- package/dist/settings/feature-flags.d.ts +64 -0
- package/dist/settings/feature-flags.js +61 -0
- package/dist/settings/manager.d.ts +68 -1
- package/dist/settings/manager.js +266 -29
- package/dist/settings/migrate-config.d.ts +45 -0
- package/dist/settings/migrate-config.js +125 -0
- package/dist/settings/schema-export.d.ts +25 -0
- package/dist/settings/schema-export.js +38 -0
- package/dist/settings/schema.d.ts +1186 -771
- package/dist/settings/schema.js +268 -97
- package/dist/skills/scanner.d.ts +9 -0
- package/dist/skills/scanner.js +30 -2
- package/dist/stt/resolve-transcribe.d.ts +31 -0
- package/dist/stt/resolve-transcribe.js +108 -0
- package/dist/stt/transcribe.d.ts +51 -0
- package/dist/stt/transcribe.js +65 -0
- package/dist/tool-system/browser-bridge.d.ts +226 -0
- package/dist/tool-system/browser-bridge.js +163 -0
- package/dist/tool-system/builtin/agent-heartbeat.d.ts +49 -0
- package/dist/tool-system/builtin/agent-heartbeat.js +89 -0
- package/dist/tool-system/builtin/agent-notifications.d.ts +12 -3
- package/dist/tool-system/builtin/agent-notifications.js +9 -3
- package/dist/tool-system/builtin/agent-output-file.d.ts +38 -0
- package/dist/tool-system/builtin/agent-output-file.js +72 -0
- package/dist/tool-system/builtin/agent-registry.d.ts +12 -0
- package/dist/tool-system/builtin/agent-registry.js +8 -0
- package/dist/tool-system/builtin/agent.d.ts +21 -1
- package/dist/tool-system/builtin/agent.js +489 -42
- package/dist/tool-system/builtin/apply-patch/applier.js +66 -8
- package/dist/tool-system/builtin/apply-patch/backup-targets.d.ts +10 -0
- package/dist/tool-system/builtin/apply-patch/backup-targets.js +30 -0
- package/dist/tool-system/builtin/apply-patch/index.js +0 -15
- package/dist/tool-system/builtin/background-jobs.d.ts +76 -0
- package/dist/tool-system/builtin/background-jobs.js +124 -0
- package/dist/tool-system/builtin/background-shell-tools.d.ts +20 -0
- package/dist/tool-system/builtin/background-shell-tools.js +108 -0
- package/dist/tool-system/builtin/background-work.d.ts +67 -0
- package/dist/tool-system/builtin/background-work.js +86 -0
- package/dist/tool-system/builtin/bash-output-style.d.ts +32 -0
- package/dist/tool-system/builtin/bash-output-style.js +40 -0
- package/dist/tool-system/builtin/bash.d.ts +5 -2
- package/dist/tool-system/builtin/bash.js +98 -62
- package/dist/tool-system/builtin/browser-tools.d.ts +33 -0
- package/dist/tool-system/builtin/browser-tools.js +312 -0
- package/dist/tool-system/builtin/cancel-goal.d.ts +31 -0
- package/dist/tool-system/builtin/cancel-goal.js +64 -0
- package/dist/tool-system/builtin/check-quota.d.ts +15 -0
- package/dist/tool-system/builtin/check-quota.js +34 -0
- package/dist/tool-system/builtin/config.js +7 -0
- package/dist/tool-system/builtin/cron.d.ts +7 -0
- package/dist/tool-system/builtin/cron.js +55 -3
- package/dist/tool-system/builtin/drive-claude-code.d.ts +30 -0
- package/dist/tool-system/builtin/drive-claude-code.js +157 -0
- package/dist/tool-system/builtin/edit-model-catalog.d.ts +3 -0
- package/dist/tool-system/builtin/edit-model-catalog.js +94 -0
- package/dist/tool-system/builtin/edit.js +20 -16
- package/dist/tool-system/builtin/eol.d.ts +29 -0
- package/dist/tool-system/builtin/eol.js +37 -0
- package/dist/tool-system/builtin/file-cache.d.ts +6 -0
- package/dist/tool-system/builtin/file-cache.js +8 -0
- package/dist/tool-system/builtin/generate-image.d.ts +35 -0
- package/dist/tool-system/builtin/generate-image.js +278 -50
- package/dist/tool-system/builtin/generate-video.d.ts +55 -0
- package/dist/tool-system/builtin/generate-video.js +361 -0
- package/dist/tool-system/builtin/glob.js +0 -7
- package/dist/tool-system/builtin/grep.js +6 -8
- package/dist/tool-system/builtin/image-providers.d.ts +86 -0
- package/dist/tool-system/builtin/image-providers.js +190 -0
- package/dist/tool-system/builtin/image-uploader.d.ts +33 -0
- package/dist/tool-system/builtin/image-uploader.js +74 -0
- package/dist/tool-system/builtin/index.d.ts +7 -1
- package/dist/tool-system/builtin/index.js +249 -25
- package/dist/tool-system/builtin/mcp-tools.js +23 -3
- package/dist/tool-system/builtin/memory.js +45 -7
- package/dist/tool-system/builtin/notebook-edit.js +0 -7
- package/dist/tool-system/builtin/powershell.js +4 -1
- package/dist/tool-system/builtin/read.js +10 -10
- package/dist/tool-system/builtin/repl.js +4 -1
- package/dist/tool-system/builtin/skill.js +9 -0
- package/dist/tool-system/builtin/sleep.js +3 -2
- package/dist/tool-system/builtin/tool-search.js +25 -7
- package/dist/tool-system/builtin/video-providers.d.ts +154 -0
- package/dist/tool-system/builtin/video-providers.js +235 -0
- package/dist/tool-system/builtin/web-fetch.js +12 -2
- package/dist/tool-system/builtin/web-search.js +21 -7
- package/dist/tool-system/builtin/worktree.d.ts +2 -1
- package/dist/tool-system/builtin/worktree.js +31 -4
- package/dist/tool-system/builtin/write.js +0 -6
- package/dist/tool-system/context.d.ts +154 -6
- package/dist/tool-system/executor.d.ts +3 -1
- package/dist/tool-system/executor.js +172 -88
- package/dist/tool-system/investigation-guard.js +1 -1
- package/dist/tool-system/mcp-manager.d.ts +42 -4
- package/dist/tool-system/mcp-manager.js +186 -20
- package/dist/tool-system/path-policy.d.ts +5 -0
- package/dist/tool-system/path-policy.js +307 -8
- package/dist/tool-system/permission.d.ts +36 -1
- package/dist/tool-system/permission.js +281 -69
- package/dist/tool-system/plan-mode-allowlist.d.ts +13 -2
- package/dist/tool-system/plan-mode-allowlist.js +24 -2
- package/dist/tool-system/registry.d.ts +1 -0
- package/dist/tool-system/registry.js +16 -2
- package/dist/tool-system/sandbox/index.d.ts +8 -0
- package/dist/tool-system/sandbox/index.js +7 -2
- package/dist/tool-system/validate-tool-metadata.d.ts +36 -0
- package/dist/tool-system/validate-tool-metadata.js +63 -0
- package/dist/types.d.ts +217 -6
- package/dist/updater.js +20 -9
- package/dist/utils/envUtils.d.ts +0 -9
- package/dist/utils/envUtils.js +3 -28
- package/dist/utils/exec.d.ts +40 -0
- package/dist/utils/exec.js +144 -0
- package/dist/utils/json.d.ts +12 -0
- package/dist/utils/json.js +92 -0
- package/dist/utils/theme.d.ts +1 -1
- package/dist/utils/theme.js +1 -1
- package/dist/utils/toolDisplay.js +0 -1
- package/package.json +13 -7
- package/dist/agent/coordinator.d.ts +0 -49
- package/dist/agent/coordinator.js +0 -77
- package/dist/settings/manager.test.js +0 -73
- package/dist/tool-system/builtin/remote-trigger.d.ts +0 -6
- package/dist/tool-system/builtin/remote-trigger.js +0 -54
- package/dist/tool-system/builtin/send-message.d.ts +0 -6
- package/dist/tool-system/builtin/send-message.js +0 -47
- /package/dist/{settings/manager.test.d.ts → credentials/types.js} +0 -0
package/dist/protocol/server.js
CHANGED
|
@@ -15,9 +15,14 @@
|
|
|
15
15
|
*/
|
|
16
16
|
import { Methods, ErrorCodes, createResponse, createErrorResponse, createNotification, isRequest, } from "./types.js";
|
|
17
17
|
import { diskDefaultsFrom } from "../engine/engine.js";
|
|
18
|
+
import { isProtectedSettingKey } from "../settings/manager.js";
|
|
18
19
|
import { setInteractiveApprovalFn } from "../tool-system/permission.js";
|
|
19
20
|
import { getArenaStatus } from "../tool-system/builtin/arena.js";
|
|
20
|
-
import { agentNotificationBus } from "../tool-system/builtin/agent-notifications.js";
|
|
21
|
+
import { agentNotificationBus, notificationQueue, buildNotificationMessage, } from "../tool-system/builtin/agent-notifications.js";
|
|
22
|
+
import { backgroundShellManager } from "../runtime/background-shell.js";
|
|
23
|
+
import { backgroundJobRegistry } from "../tool-system/builtin/background-jobs.js";
|
|
24
|
+
import { listBackgroundWorkForUI } from "../tool-system/builtin/background-work.js";
|
|
25
|
+
import { logger } from "../logging/logger.js";
|
|
21
26
|
import { nanoid } from "nanoid";
|
|
22
27
|
import { redactLlmConfig, maskSecretValue } from "./redact.js";
|
|
23
28
|
import { redactSecrets } from "../logging/sanitize-messages.js";
|
|
@@ -28,6 +33,8 @@ export class AgentServer {
|
|
|
28
33
|
transport;
|
|
29
34
|
/** Fresh-disk settings reader for config hot-reload; null when not wired. */
|
|
30
35
|
settingsReader;
|
|
36
|
+
/** Disk-only active-goal reader for agent/goalGet on a non-live session. */
|
|
37
|
+
readActiveGoalFromDisk;
|
|
31
38
|
/**
|
|
32
39
|
* Monotonic config-reload version, bumped per reloadSettings request so each
|
|
33
40
|
* Engine.refreshRuntimeConfig can drop out-of-order (stale) deliveries (Q5).
|
|
@@ -64,6 +71,7 @@ export class AgentServer {
|
|
|
64
71
|
this.chatManager = options.chatManager ?? null;
|
|
65
72
|
this.legacyEngine = options.engine ?? null;
|
|
66
73
|
this.settingsReader = options.settingsReader ?? null;
|
|
74
|
+
this.readActiveGoalFromDisk = options.readActiveGoalFromDisk ?? null;
|
|
67
75
|
if (!this.chatManager && !this.legacyEngine) {
|
|
68
76
|
throw new Error("AgentServer: either chatManager or engine must be supplied");
|
|
69
77
|
}
|
|
@@ -93,15 +101,121 @@ export class AgentServer {
|
|
|
93
101
|
// B2.2 — forward background sub-agent completion events through the
|
|
94
102
|
// protocol so Desktop / SDK / remote AgentClients see them too. The
|
|
95
103
|
// bus is fed from `NotificationQueue.enqueue`, so this single
|
|
96
|
-
// subscription covers every enqueue site
|
|
97
|
-
//
|
|
98
|
-
//
|
|
104
|
+
// subscription covers every enqueue site — background sub-agents
|
|
105
|
+
// (agent.ts), background shells (background-shell.ts), and video/job
|
|
106
|
+
// polls (background-jobs.ts). The bus now guarantees a real sessionId
|
|
107
|
+
// (no more legacy-bucket coercion to ""), so we forward whatever sid it
|
|
108
|
+
// hands us.
|
|
99
109
|
this.bgAgentBusUnsubscribe = agentNotificationBus.subscribe((sessionId, event) => {
|
|
100
110
|
this.notify(Methods.StreamEvent, { sessionId, event });
|
|
111
|
+
// Background work that finishes while the session is IDLE (a
|
|
112
|
+
// run_in_background Bash like a download, a background sub-agent, or a
|
|
113
|
+
// video poll — the engine no longer parks on any of them) would otherwise
|
|
114
|
+
// leave its completion sitting in the queue until the user manually sends.
|
|
115
|
+
// Wake the session with one run carrying the notification so the model
|
|
116
|
+
// reads "download complete" and continues on its own (the persisted goal
|
|
117
|
+
// is judged that turn). If the work finishes while a run is still in
|
|
118
|
+
// flight, the idle guard below skips it and the run-boundary re-check
|
|
119
|
+
// (trigger B) drains it at end-of-turn instead. A never-exiting dev server
|
|
120
|
+
// emits no completion, so it never wakes anything (no task/service
|
|
121
|
+
// classification needed).
|
|
122
|
+
this.maybeWakeIdleSession(sessionId);
|
|
101
123
|
});
|
|
102
124
|
// Notify client we're ready
|
|
103
125
|
this.notify(Methods.Status, { status: "ready" });
|
|
104
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* Wake an IDLE chatManager session that has pending background-completion
|
|
129
|
+
* notifications, by enqueueing one turn whose task is the drained
|
|
130
|
+
* notification(s). The model then sees the completion and continues; the
|
|
131
|
+
* session's persisted goal (if any) is judged on that turn.
|
|
132
|
+
*
|
|
133
|
+
* Guards:
|
|
134
|
+
* - chatManager path only (the legacy single-engine / headless path drives
|
|
135
|
+
* its own loop and has no idle-session-resume concept).
|
|
136
|
+
* - Session must exist AND be idle. If it's busy, we do nothing: the in-flight
|
|
137
|
+
* run's end-of-turn `drainAll` already collects every pending notification,
|
|
138
|
+
* so a second run would be redundant — and `enqueueTurn` while busy would
|
|
139
|
+
* queue a spurious extra turn.
|
|
140
|
+
* - We `drainAll` exactly here and feed the items into the woken turn. This
|
|
141
|
+
* also merges a burst of near-simultaneous completions into one wakeup:
|
|
142
|
+
* the first drains all currently-pending items; subsequent bus events for
|
|
143
|
+
* the same session find it busy (or find an empty queue) and no-op.
|
|
144
|
+
*
|
|
145
|
+
* INVARIANT (the burst-merge correctness depends on it): the merge only
|
|
146
|
+
* holds because (a) the bus fans out to subscribers SYNCHRONOUSLY, and (b)
|
|
147
|
+
* `enqueueTurn` sets the session's `active` (→ isBusy()===true) SYNCHRONOUSLY
|
|
148
|
+
* before its first await. So when bus events #2..N arrive — still on the same
|
|
149
|
+
* synchronous fan-out as #1 — they already see the session as busy and no-op.
|
|
150
|
+
* If either path is ever made async (e.g. an await is added inside
|
|
151
|
+
* enqueueTurn before `active` is set, or the bus starts deferring delivery),
|
|
152
|
+
* this degrades into N concurrent wakeups for N completions. Keep both
|
|
153
|
+
* synchronous, or replace this comment's assumption with an explicit
|
|
154
|
+
* "wakeup in flight" guard flag.
|
|
155
|
+
*/
|
|
156
|
+
maybeWakeIdleSession(sessionId) {
|
|
157
|
+
if (!this.chatManager)
|
|
158
|
+
return;
|
|
159
|
+
const session = this.chatManager.get(sessionId);
|
|
160
|
+
if (!session || session.isBusy())
|
|
161
|
+
return;
|
|
162
|
+
// Headless / automation runs are one-shot: the caller takes result.text and
|
|
163
|
+
// is gone, so there's no consumer for a woken continuation turn. Headless
|
|
164
|
+
// already drained its background sub-agents inside engine.run before
|
|
165
|
+
// returning; any remaining queued notification (video/shell) must NOT spin
|
|
166
|
+
// an orphan turn. Only the interactive path auto-continues.
|
|
167
|
+
if (session.engine.isHeadless())
|
|
168
|
+
return;
|
|
169
|
+
// Don't resurrect a session the user just Stopped: cancel() leaves it idle
|
|
170
|
+
// (active=null) so isBusy() reads false, but auto-running a fresh turn here
|
|
171
|
+
// would defeat the Stop. The flag clears the moment the user sends again.
|
|
172
|
+
if (session.wasCancelledSinceLastTurn())
|
|
173
|
+
return;
|
|
174
|
+
const pending = notificationQueue.drainAll(sessionId);
|
|
175
|
+
if (pending.length === 0)
|
|
176
|
+
return;
|
|
177
|
+
const task = `<system-reminder>\n${buildNotificationMessage(pending)}\n</system-reminder>`;
|
|
178
|
+
void session
|
|
179
|
+
.enqueueTurn(task, {
|
|
180
|
+
// Synthetic notification, not the user's own input: persisted with an
|
|
181
|
+
// `injected` flag so a disk rebuild doesn't render it as a phantom user
|
|
182
|
+
// bubble (the live UI shows only the woken assistant's reply).
|
|
183
|
+
injected: true,
|
|
184
|
+
onStream: (event) => this.notify(Methods.StreamEvent, { sessionId, event }),
|
|
185
|
+
})
|
|
186
|
+
.catch((err) => {
|
|
187
|
+
// A wakeup turn failing must not crash the bus fan-out. The drained
|
|
188
|
+
// notifications are already in the transcript via the run's messages;
|
|
189
|
+
// log and move on.
|
|
190
|
+
logger.warn("bg_wakeup.turn_failed", {
|
|
191
|
+
sessionId,
|
|
192
|
+
error: err.message,
|
|
193
|
+
});
|
|
194
|
+
// Belt-and-braces, mirroring the send() path's run().then(clear-busy):
|
|
195
|
+
// the renderer set the composer "working" spinner on this run's
|
|
196
|
+
// session_started, and clears it on turn_complete/error. A failure
|
|
197
|
+
// BEFORE the turn-loop runs (e.g. a setup error) emits neither, which
|
|
198
|
+
// would leave the spinner stuck. Emit a terminal `error` (NOT a
|
|
199
|
+
// turn_complete) so the renderer clears busy AND a woken automation
|
|
200
|
+
// session's runStatus flips to "failed" rather than being mislabeled
|
|
201
|
+
// "completed". If the turn-loop already emitted its own `error`, this is
|
|
202
|
+
// a harmless duplicate (busy already cleared, status already failed).
|
|
203
|
+
this.notify(Methods.StreamEvent, {
|
|
204
|
+
sessionId,
|
|
205
|
+
event: { type: "error", error: err?.message ?? "background wakeup failed" },
|
|
206
|
+
});
|
|
207
|
+
})
|
|
208
|
+
.finally(() => {
|
|
209
|
+
// Run-boundary re-check (trigger B): the woken summarize turn may have
|
|
210
|
+
// spawned NEW background work (e.g. goal "generate 2 videos" → after #1
|
|
211
|
+
// completes, this turn submits #2). When #2 finishes its notification
|
|
212
|
+
// arrives while we're idle again — but if it arrived DURING this turn
|
|
213
|
+
// (busy), trigger A skipped it. Re-checking at the run boundary drains
|
|
214
|
+
// anything that landed while busy, chaining wakeups until the queue is
|
|
215
|
+
// truly empty. Replaces the old engine for(;;) outer loop.
|
|
216
|
+
this.maybeWakeIdleSession(sessionId);
|
|
217
|
+
});
|
|
218
|
+
}
|
|
105
219
|
// ─── Request Dispatch ───────────────────────────────────────────
|
|
106
220
|
async handleRequest(req) {
|
|
107
221
|
switch (req.method) {
|
|
@@ -123,9 +237,30 @@ export class AgentServer {
|
|
|
123
237
|
case Methods.Inject:
|
|
124
238
|
this.handleInject(req);
|
|
125
239
|
break;
|
|
240
|
+
case Methods.Steer:
|
|
241
|
+
this.handleSteer(req);
|
|
242
|
+
break;
|
|
243
|
+
case Methods.Unsteer:
|
|
244
|
+
this.handleUnsteer(req);
|
|
245
|
+
break;
|
|
126
246
|
case Methods.CloseSession:
|
|
127
247
|
this.handleCloseSession(req);
|
|
128
248
|
break;
|
|
249
|
+
case Methods.GoalExtend:
|
|
250
|
+
this.handleGoalExtend(req);
|
|
251
|
+
break;
|
|
252
|
+
case Methods.GoalClear:
|
|
253
|
+
this.handleGoalClear(req);
|
|
254
|
+
break;
|
|
255
|
+
case Methods.GoalGet:
|
|
256
|
+
this.handleGoalGet(req);
|
|
257
|
+
break;
|
|
258
|
+
case Methods.BackgroundShells:
|
|
259
|
+
this.handleBackgroundShells(req);
|
|
260
|
+
break;
|
|
261
|
+
case Methods.BackgroundWork:
|
|
262
|
+
this.handleBackgroundWork(req);
|
|
263
|
+
break;
|
|
129
264
|
default:
|
|
130
265
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.MethodNotFound, `Unknown method: ${req.method}`));
|
|
131
266
|
}
|
|
@@ -155,6 +290,7 @@ export class AgentServer {
|
|
|
155
290
|
session = cm.getOrCreate(params.sessionId, {
|
|
156
291
|
permissionMode: params.permissionMode,
|
|
157
292
|
cwd: params.cwd,
|
|
293
|
+
projectTrusted: params.projectTrusted,
|
|
158
294
|
});
|
|
159
295
|
}
|
|
160
296
|
catch (err) {
|
|
@@ -162,6 +298,15 @@ export class AgentServer {
|
|
|
162
298
|
this.transport.send(createErrorResponse(req.id, code, err.message));
|
|
163
299
|
return;
|
|
164
300
|
}
|
|
301
|
+
// `requireExisting`: reject if the target session isn't on disk rather than
|
|
302
|
+
// running the prompt against a freshly-created blank session. A cron
|
|
303
|
+
// "continue this conversation" job whose session the user deleted must fail
|
|
304
|
+
// loudly here (SessionNotFound) so the scheduler can auto-disable it,
|
|
305
|
+
// instead of silently executing with no transcript/goal/context.
|
|
306
|
+
if (params.requireExisting === true && !session.engine.sessionExistsOnDisk(params.sessionId)) {
|
|
307
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionNotFound, `session ${params.sessionId} does not exist`));
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
165
310
|
if (typeof params.planMode === "boolean") {
|
|
166
311
|
session.engine.setPlanMode(params.planMode);
|
|
167
312
|
}
|
|
@@ -176,6 +321,15 @@ export class AgentServer {
|
|
|
176
321
|
// headless engines (no human to answer).
|
|
177
322
|
if (!session.engine.isHeadless()) {
|
|
178
323
|
session.engine.setAskUser((question, opts) => this.requestAskUserForSession(session, sid, question, opts));
|
|
324
|
+
// Browser automation bridge: each method routes a browser action to the
|
|
325
|
+
// client (Electron main drives the webview via CDP) over the SAME
|
|
326
|
+
// request/response channel as askUser (pendingApprovals + requestId),
|
|
327
|
+
// reusing its proven resolve/timeout/cleanup. The main-side handler
|
|
328
|
+
// replies with the action's JSON result.
|
|
329
|
+
session.engine.setBrowserBridge(this.makeBrowserBridge(session, sid));
|
|
330
|
+
// Cookie→browser injection (InjectCredential tool): same cross-process
|
|
331
|
+
// channel; main restores the cookie jar into the built-in browser.
|
|
332
|
+
session.engine.setInjectCredential((credentialId) => this.requestCredentialInjectForSession(session, sid, credentialId));
|
|
179
333
|
}
|
|
180
334
|
try {
|
|
181
335
|
const result = await session.enqueueTurn(params.task, {
|
|
@@ -194,9 +348,18 @@ export class AgentServer {
|
|
|
194
348
|
usage: result.usage,
|
|
195
349
|
};
|
|
196
350
|
this.transport.send(createResponse(req.id, runResult));
|
|
351
|
+
// Run-boundary re-check (trigger B): the session is idle now. If a
|
|
352
|
+
// background task (shell/video) completed DURING this run, its bus event
|
|
353
|
+
// fired while we were busy and trigger A skipped it — drain it now and
|
|
354
|
+
// wake a continuation turn. Interactive path only; headless already
|
|
355
|
+
// drained its sub-agents inside engine.run before returning.
|
|
356
|
+
this.maybeWakeIdleSession(sid);
|
|
197
357
|
}
|
|
198
358
|
catch (err) {
|
|
199
359
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
|
|
360
|
+
// Even on a failed run the session goes idle — re-check so a background
|
|
361
|
+
// completion that landed during the failed run isn't orphaned.
|
|
362
|
+
this.maybeWakeIdleSession(sid);
|
|
200
363
|
}
|
|
201
364
|
}
|
|
202
365
|
async handleRunLegacy(req) {
|
|
@@ -302,6 +465,11 @@ export class AgentServer {
|
|
|
302
465
|
return;
|
|
303
466
|
}
|
|
304
467
|
s.pendingApprovals.delete(params.requestId);
|
|
468
|
+
// Cancel the pending timeout for THIS request (the ask path armed one).
|
|
469
|
+
// The legacy single-engine branch below already does this; without it
|
|
470
|
+
// here a decided request leaves a live timer that fires at
|
|
471
|
+
// APPROVAL_TIMEOUT_MS and churns a dead map entry.
|
|
472
|
+
this.clearApprovalTimer(params.requestId);
|
|
305
473
|
resolve(params.decision);
|
|
306
474
|
this.transport.send(createResponse(req.id, { ok: true }));
|
|
307
475
|
return;
|
|
@@ -332,6 +500,14 @@ export class AgentServer {
|
|
|
332
500
|
return;
|
|
333
501
|
}
|
|
334
502
|
s.cancel();
|
|
503
|
+
// s.cancel() only aborts the engine controller + drains queued turns. The
|
|
504
|
+
// session's pendingApprovals (askUser / browser_action / tool approvals)
|
|
505
|
+
// are NOT driven by the abort signal — they only settle on a client reply
|
|
506
|
+
// or the 5-minute APPROVAL_TIMEOUT_MS. Left alone, the awaiting tool call
|
|
507
|
+
// hangs until that timeout (delaying run wind-down) and the server-side
|
|
508
|
+
// approvalTimers entry leaks. Resolve them as cancelled now and clear the
|
|
509
|
+
// matching timers, mirroring the legacy path below.
|
|
510
|
+
this.cancelSessionApprovals(s);
|
|
335
511
|
this.transport.send(createResponse(req.id, { ok: true }));
|
|
336
512
|
return;
|
|
337
513
|
}
|
|
@@ -348,6 +524,163 @@ export class AgentServer {
|
|
|
348
524
|
this.clearAllApprovalTimers();
|
|
349
525
|
this.transport.send(createResponse(req.id, { ok: true }));
|
|
350
526
|
}
|
|
527
|
+
/**
|
|
528
|
+
* Extend a running goal's turn/budget ceilings mid-run (TODO 3.1). Routed to
|
|
529
|
+
* a specific session (multi-session path). Returns the resulting effective
|
|
530
|
+
* limits, or an error if no session / no active run.
|
|
531
|
+
*/
|
|
532
|
+
handleGoalExtend(req) {
|
|
533
|
+
const params = (req.params ?? {});
|
|
534
|
+
const ext = {
|
|
535
|
+
addTurns: params.addTurns,
|
|
536
|
+
addTokenBudget: params.addTokenBudget,
|
|
537
|
+
addTimeBudgetMs: params.addTimeBudgetMs,
|
|
538
|
+
addStopBlocks: params.addStopBlocks,
|
|
539
|
+
};
|
|
540
|
+
// Multi-session path: route to the named session.
|
|
541
|
+
const session = this.chatManager && typeof params.sessionId === "string"
|
|
542
|
+
? this.chatManager.get(params.sessionId)
|
|
543
|
+
: undefined;
|
|
544
|
+
// Legacy single-engine path (no chatManager): extend the one engine's run,
|
|
545
|
+
// mirroring handleCancel which also supports both paths. Without this a
|
|
546
|
+
// legacy host can cancel but never extend a goal.
|
|
547
|
+
const result = session
|
|
548
|
+
? session.extendGoalRun(ext)
|
|
549
|
+
: this.legacyEngine
|
|
550
|
+
? this.legacyEngine.extendGoalRun(ext)
|
|
551
|
+
: undefined;
|
|
552
|
+
if (result === undefined) {
|
|
553
|
+
// Couldn't even resolve a target (no session and no legacy engine).
|
|
554
|
+
if (!session && !this.legacyEngine) {
|
|
555
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionClosed, params.sessionId ? `No such session: ${params.sessionId}` : "sessionId is required"));
|
|
556
|
+
return;
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
if (!result) {
|
|
560
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "No active run to extend"));
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
this.transport.send(createResponse(req.id, { ok: true, limits: result }));
|
|
564
|
+
}
|
|
565
|
+
/**
|
|
566
|
+
* Clear a session's persisted active goal (CC /goal clear). Routes to the
|
|
567
|
+
* named session, falling back to the legacy single engine (mirrors
|
|
568
|
+
* handleGoalExtend / handleCancel's dual path). Returns { ok, cleared }.
|
|
569
|
+
*/
|
|
570
|
+
handleGoalClear(req) {
|
|
571
|
+
const params = (req.params ?? {});
|
|
572
|
+
const session = this.chatManager && typeof params.sessionId === "string"
|
|
573
|
+
? this.chatManager.get(params.sessionId)
|
|
574
|
+
: undefined;
|
|
575
|
+
if (!session && !this.legacyEngine) {
|
|
576
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionClosed, params.sessionId ? `No such session: ${params.sessionId}` : "sessionId is required"));
|
|
577
|
+
return;
|
|
578
|
+
}
|
|
579
|
+
const cleared = session
|
|
580
|
+
? session.clearGoal()
|
|
581
|
+
: params.sessionId
|
|
582
|
+
? (this.legacyEngine.clearGoal(params.sessionId) ?? false)
|
|
583
|
+
: false;
|
|
584
|
+
this.transport.send(createResponse(req.id, { ok: true, cleared }));
|
|
585
|
+
}
|
|
586
|
+
/**
|
|
587
|
+
* Read a session's persisted active goal so the host can re-surface the goal
|
|
588
|
+
* block + Cancel button on session load. A persistent goal lives only in
|
|
589
|
+
* state.activeGoal and is never replayed from the transcript, so a reloaded
|
|
590
|
+
* (or disk-rebuilt) session has no other way to learn it. Prefers a live
|
|
591
|
+
* chatManager session, else falls through to a disk read (readActiveGoalFromDisk
|
|
592
|
+
* in worker mode, or the legacy engine's disk read in single-engine mode) — the
|
|
593
|
+
* bug case is an aborted/reloaded session that is NOT live. Returns { ok, goal } where goal
|
|
594
|
+
* is the objective string, or null when there's no goal / unknown session.
|
|
595
|
+
* Never errors on "no goal" — null is the normal "nothing to show" answer.
|
|
596
|
+
*/
|
|
597
|
+
handleGoalGet(req) {
|
|
598
|
+
const params = (req.params ?? {});
|
|
599
|
+
if (typeof params.sessionId !== "string" || !params.sessionId) {
|
|
600
|
+
// Missing/empty param is InvalidParams, not SessionClosed (no session was
|
|
601
|
+
// ever named) — matches handleSteer/handleUnsteer/handleBackgroundShells.
|
|
602
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "sessionId is required"));
|
|
603
|
+
return;
|
|
604
|
+
}
|
|
605
|
+
// Prefer the live session; otherwise read straight off disk. The goal we
|
|
606
|
+
// want to recover belongs to a session that is, by definition of this bug,
|
|
607
|
+
// usually NOT currently live: reopening after restart doesn't create a
|
|
608
|
+
// ChatSession (that only happens on a send), so chatManager.get() misses.
|
|
609
|
+
// In worker (chatManager) mode there is no legacyEngine, so the disk read
|
|
610
|
+
// MUST come from readActiveGoalFromDisk — relying on legacyEngine there
|
|
611
|
+
// silently returned null and the UI goal block never re-surfaced.
|
|
612
|
+
const live = this.chatManager ? this.chatManager.get(params.sessionId) : undefined;
|
|
613
|
+
const goal = live
|
|
614
|
+
? live.getGoal()
|
|
615
|
+
: (this.readActiveGoalFromDisk?.(params.sessionId) ??
|
|
616
|
+
this.legacyEngine?.getGoal(params.sessionId) ??
|
|
617
|
+
undefined);
|
|
618
|
+
this.transport.send(createResponse(req.id, { ok: true, goal: goal ? goal.objective : null }));
|
|
619
|
+
}
|
|
620
|
+
/**
|
|
621
|
+
* Query/control a session's background shells for the desktop UI panel (TODO
|
|
622
|
+
* 3.2). Reads the singleton BackgroundShellManager directly (shells outlive
|
|
623
|
+
* the run that spawned them, so we don't need a live ChatSession). Actions:
|
|
624
|
+
* - (default) list → the session's shells
|
|
625
|
+
* - output → full retained output of one shell
|
|
626
|
+
* - kill → terminate one shell's process group
|
|
627
|
+
* All scoped by sessionId (manager enforces ownership on output/kill).
|
|
628
|
+
*/
|
|
629
|
+
handleBackgroundShells(req) {
|
|
630
|
+
const params = (req.params ?? {});
|
|
631
|
+
const sessionId = params.sessionId;
|
|
632
|
+
if (typeof sessionId !== "string" || !sessionId) {
|
|
633
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "sessionId is required"));
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
const action = params.action ?? "list";
|
|
637
|
+
if (action === "list") {
|
|
638
|
+
const shells = backgroundShellManager.listForSession(sessionId);
|
|
639
|
+
this.transport.send(createResponse(req.id, { shells }));
|
|
640
|
+
return;
|
|
641
|
+
}
|
|
642
|
+
if (!params.shellId) {
|
|
643
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "shellId is required"));
|
|
644
|
+
return;
|
|
645
|
+
}
|
|
646
|
+
if (action === "output") {
|
|
647
|
+
const res = backgroundShellManager.readOutput(params.shellId, "all", sessionId);
|
|
648
|
+
if (!res.ok) {
|
|
649
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, res.error));
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
this.transport.send(createResponse(req.id, { header: res.header, text: res.text }));
|
|
653
|
+
return;
|
|
654
|
+
}
|
|
655
|
+
if (action === "kill") {
|
|
656
|
+
void backgroundShellManager.kill(params.shellId, sessionId).then((res) => {
|
|
657
|
+
if (!res.ok) {
|
|
658
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, res.error));
|
|
659
|
+
}
|
|
660
|
+
else {
|
|
661
|
+
this.transport.send(createResponse(req.id, { ok: true }));
|
|
662
|
+
}
|
|
663
|
+
});
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, `unknown action: ${action}`));
|
|
667
|
+
}
|
|
668
|
+
/**
|
|
669
|
+
* BackgroundWork — unified, list-only view of a session's background work
|
|
670
|
+
* across all three registries (shells + sub-agents + jobs) for the desktop
|
|
671
|
+
* background panel. Per-shell output/kill still flow through BackgroundShells
|
|
672
|
+
* (by shellId); this just answers "what's running in the background right now".
|
|
673
|
+
*/
|
|
674
|
+
handleBackgroundWork(req) {
|
|
675
|
+
const params = (req.params ?? {});
|
|
676
|
+
const sessionId = params.sessionId;
|
|
677
|
+
if (typeof sessionId !== "string" || !sessionId) {
|
|
678
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "sessionId is required"));
|
|
679
|
+
return;
|
|
680
|
+
}
|
|
681
|
+
const items = listBackgroundWorkForUI(sessionId);
|
|
682
|
+
this.transport.send(createResponse(req.id, { items }));
|
|
683
|
+
}
|
|
351
684
|
// ─── CloseSession ───────────────────────────────────────────────
|
|
352
685
|
handleCloseSession(req) {
|
|
353
686
|
const params = (req.params ?? {});
|
|
@@ -358,6 +691,14 @@ export class AgentServer {
|
|
|
358
691
|
if (this.chatManager) {
|
|
359
692
|
this.chatManager.close(params.sessionId);
|
|
360
693
|
}
|
|
694
|
+
// Explicit session teardown — reap that session's background shells
|
|
695
|
+
// (design §6 "session 被显式删除 → killSession"). This is the RPC path
|
|
696
|
+
// (agent/closeSession from the host on delete), distinct from the idle
|
|
697
|
+
// sweeper's chatManager.close() which must NOT kill (§6). Fire-and-forget.
|
|
698
|
+
void backgroundShellManager.killSession(params.sessionId);
|
|
699
|
+
// Drop this session's retained background jobs too (#2/#5): finished jobs
|
|
700
|
+
// are kept for the panel, so explicit teardown is where they're released.
|
|
701
|
+
backgroundJobRegistry.dropForSession(params.sessionId);
|
|
361
702
|
this.transport.send(createResponse(req.id, { ok: true }));
|
|
362
703
|
}
|
|
363
704
|
// ─── Configure ──────────────────────────────────────────────────
|
|
@@ -397,7 +738,7 @@ export class AgentServer {
|
|
|
397
738
|
}
|
|
398
739
|
const settings = this.settingsReader();
|
|
399
740
|
const version = ++this.configVersion;
|
|
400
|
-
s.engine.refreshRuntimeConfig(diskDefaultsFrom(settings), version);
|
|
741
|
+
s.engine.refreshRuntimeConfig(diskDefaultsFrom(settings, s.engine.getEffectiveDisabledLists().disabledPlugins), version);
|
|
401
742
|
}
|
|
402
743
|
this.transport.send(createResponse(req.id, { ok: true }));
|
|
403
744
|
return;
|
|
@@ -405,9 +746,22 @@ export class AgentServer {
|
|
|
405
746
|
// Global configure — delegate to legacyEngine if available,
|
|
406
747
|
// or to any session's engine from chatManager for settings ops
|
|
407
748
|
const engine = this.legacyEngine ?? this.anyEngine();
|
|
408
|
-
if (params.reloadModels
|
|
749
|
+
if (params.reloadModels) {
|
|
409
750
|
try {
|
|
410
|
-
|
|
751
|
+
const seen = new Set();
|
|
752
|
+
const reload = (target) => {
|
|
753
|
+
if (!target || seen.has(target))
|
|
754
|
+
return;
|
|
755
|
+
seen.add(target);
|
|
756
|
+
target.reloadModelPool();
|
|
757
|
+
};
|
|
758
|
+
reload(this.legacyEngine);
|
|
759
|
+
reload(this.globalQueryEngine);
|
|
760
|
+
if (this.chatManager) {
|
|
761
|
+
this.chatManager.forEachSession((s) => reload(s.engine));
|
|
762
|
+
}
|
|
763
|
+
if (seen.size === 0)
|
|
764
|
+
reload(engine);
|
|
411
765
|
}
|
|
412
766
|
catch (err) {
|
|
413
767
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
|
|
@@ -445,23 +799,29 @@ export class AgentServer {
|
|
|
445
799
|
// Mirrors how reloadModels/model/planMode above act on `engine`.
|
|
446
800
|
if (engine) {
|
|
447
801
|
const settings = this.settingsReader();
|
|
448
|
-
engine.refreshRuntimeConfig(diskDefaultsFrom(settings), ++this.configVersion);
|
|
802
|
+
engine.refreshRuntimeConfig(diskDefaultsFrom(settings, engine.getEffectiveDisabledLists().disabledPlugins), ++this.configVersion);
|
|
449
803
|
}
|
|
450
804
|
this.transport.send(createResponse(req.id, { ok: true }));
|
|
451
805
|
return;
|
|
452
806
|
}
|
|
453
807
|
const settings = this.settingsReader();
|
|
454
|
-
|
|
455
|
-
//
|
|
456
|
-
//
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
808
|
+
// The MCP merge folds each session's PROJECT capabilityOverrides (a
|
|
809
|
+
// project-level "on" must override the global disabledPlugins), so the
|
|
810
|
+
// patch is cwd-dependent — compute it per session, not once.
|
|
811
|
+
const perSession = [];
|
|
812
|
+
this.chatManager.forEachSession((s) => {
|
|
813
|
+
const patch = diskDefaultsFrom(settings, s.engine.getEffectiveDisabledLists().disabledPlugins);
|
|
814
|
+
perSession.push({ apply: (v) => s.engine.refreshRuntimeConfig(patch, v), patch });
|
|
815
|
+
});
|
|
816
|
+
// #6: content short-circuit — now keyed on ALL per-session patches, so a
|
|
817
|
+
// change visible to only one session (e.g. its project's overrides)
|
|
818
|
+
// still propagates while a true no-op skips the reloadHooks churn.
|
|
819
|
+
const patchJson = JSON.stringify(perSession.map((p) => p.patch));
|
|
461
820
|
if (patchJson !== this.lastBroadcastPatch) {
|
|
462
821
|
this.lastBroadcastPatch = patchJson;
|
|
463
822
|
const version = ++this.configVersion;
|
|
464
|
-
|
|
823
|
+
for (const p of perSession)
|
|
824
|
+
p.apply(version);
|
|
465
825
|
}
|
|
466
826
|
}
|
|
467
827
|
this.transport.send(createResponse(req.id, { ok: true }));
|
|
@@ -528,6 +888,11 @@ export class AgentServer {
|
|
|
528
888
|
// apiKey is redacted at this boundary: clients only get
|
|
529
889
|
// hasApiKey + optional apiKeyPreview. See protocol/redact.ts.
|
|
530
890
|
llm: redactLlmConfig(config.llm),
|
|
891
|
+
// Resolved feature flags (defaults merged with settings overlay)
|
|
892
|
+
// so the /features command can list current state.
|
|
893
|
+
featureFlags: engine.getFeatureFlags(),
|
|
894
|
+
// Effective permission rules so /permissions can list them (TODO 5.1).
|
|
895
|
+
permissionRules: engine.getPermissionRules(),
|
|
531
896
|
},
|
|
532
897
|
}));
|
|
533
898
|
break;
|
|
@@ -654,6 +1019,15 @@ export class AgentServer {
|
|
|
654
1019
|
const { key, value } = params;
|
|
655
1020
|
if (!key)
|
|
656
1021
|
throw new Error("key is required for config_set");
|
|
1022
|
+
// Refuse writes to trust-root fields via the generic config path. A
|
|
1023
|
+
// protocol peer (external driver, paired phone, compromised renderer)
|
|
1024
|
+
// could otherwise self-authorize tools or inject env/hooks/MCP servers,
|
|
1025
|
+
// which is equivalent to remotely bypassing workspace trust. Legit
|
|
1026
|
+
// writes to these go through the dedicated settings UI, not config_set;
|
|
1027
|
+
// provider/model edits use provider_add / models handlers below.
|
|
1028
|
+
if (isProtectedSettingKey(key)) {
|
|
1029
|
+
throw new Error(`config_set refused: "${key}" is a protected trust/permission field and cannot be written through this path.`);
|
|
1030
|
+
}
|
|
657
1031
|
engine.updateConfig(key, value);
|
|
658
1032
|
// Echo back through the same secret-aware masker as config_get so
|
|
659
1033
|
// a `config_set("llm.apiKey", "...")` confirmation doesn't ship
|
|
@@ -861,6 +1235,55 @@ export class AgentServer {
|
|
|
861
1235
|
this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
|
|
862
1236
|
}
|
|
863
1237
|
}
|
|
1238
|
+
// ─── Steer ──────────────────────────────────────────────────────
|
|
1239
|
+
// Queue a user message into the in-flight run's turn loop (不打断). Unlike
|
|
1240
|
+
// Inject (appends an assistant context msg for the NEXT run), Steer feeds a
|
|
1241
|
+
// user message to the CURRENT run's next step. No abort, no LLM trigger by
|
|
1242
|
+
// itself — the running loop picks it up at its next step boundary.
|
|
1243
|
+
handleSteer(req) {
|
|
1244
|
+
const params = (req.params ?? {});
|
|
1245
|
+
if (!params.text || !params.sessionId) {
|
|
1246
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "text and sessionId required"));
|
|
1247
|
+
return;
|
|
1248
|
+
}
|
|
1249
|
+
const engine = this.chatManager
|
|
1250
|
+
? this.chatManager.get(params.sessionId)?.engine
|
|
1251
|
+
: this.legacyEngine;
|
|
1252
|
+
if (!engine) {
|
|
1253
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionClosed, `No such session: ${params.sessionId}`));
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
try {
|
|
1257
|
+
engine.enqueueSteer(params.sessionId, params.text, params.id);
|
|
1258
|
+
this.transport.send(createResponse(req.id, { ok: true }));
|
|
1259
|
+
}
|
|
1260
|
+
catch (err) {
|
|
1261
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
|
|
1262
|
+
}
|
|
1263
|
+
}
|
|
1264
|
+
handleUnsteer(req) {
|
|
1265
|
+
const params = (req.params ?? {});
|
|
1266
|
+
if (!params.id || !params.sessionId) {
|
|
1267
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InvalidParams, "id and sessionId required"));
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
const engine = this.chatManager
|
|
1271
|
+
? this.chatManager.get(params.sessionId)?.engine
|
|
1272
|
+
: this.legacyEngine;
|
|
1273
|
+
if (!engine) {
|
|
1274
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.SessionClosed, `No such session: ${params.sessionId}`));
|
|
1275
|
+
return;
|
|
1276
|
+
}
|
|
1277
|
+
try {
|
|
1278
|
+
// removed=false means the loop already consumed it — not an error, the
|
|
1279
|
+
// host just learns it couldn't be revoked (it will arrive as a bubble).
|
|
1280
|
+
const removed = engine.unsteer(params.sessionId, params.id);
|
|
1281
|
+
this.transport.send(createResponse(req.id, { ok: true, removed }));
|
|
1282
|
+
}
|
|
1283
|
+
catch (err) {
|
|
1284
|
+
this.transport.send(createErrorResponse(req.id, ErrorCodes.InternalError, err.message));
|
|
1285
|
+
}
|
|
1286
|
+
}
|
|
864
1287
|
// ─── Client Communication ───────────────────────────────────────
|
|
865
1288
|
/**
|
|
866
1289
|
* Ask the client to approve a tool operation (legacy single-engine path).
|
|
@@ -919,6 +1342,8 @@ export class AgentServer {
|
|
|
919
1342
|
args.options = opts.options;
|
|
920
1343
|
if (opts?.multiSelect !== undefined)
|
|
921
1344
|
args.multiSelect = opts.multiSelect;
|
|
1345
|
+
if (opts?.optionsOnly !== undefined)
|
|
1346
|
+
args.optionsOnly = opts.optionsOnly;
|
|
922
1347
|
this.notify(Methods.ApprovalRequest, {
|
|
923
1348
|
sessionId,
|
|
924
1349
|
requestId,
|
|
@@ -931,6 +1356,129 @@ export class AgentServer {
|
|
|
931
1356
|
});
|
|
932
1357
|
});
|
|
933
1358
|
}
|
|
1359
|
+
/**
|
|
1360
|
+
* Build a BrowserBridge whose every method routes a browser action to the
|
|
1361
|
+
* client over the session's pendingApprovals channel (same as askUser). The
|
|
1362
|
+
* client (Electron main) drives the webview via CDP and replies with a JSON
|
|
1363
|
+
* result string we parse back into the typed result. On timeout / malformed
|
|
1364
|
+
* reply we degrade to a safe error result rather than throwing.
|
|
1365
|
+
*/
|
|
1366
|
+
makeBrowserBridge(session, sessionId) {
|
|
1367
|
+
const call = (action, payload) => this.requestBrowserActionForSession(session, sessionId, action, payload);
|
|
1368
|
+
return {
|
|
1369
|
+
snapshot: () => call("snapshot", {}),
|
|
1370
|
+
click: (ref) => call("click", { ref }),
|
|
1371
|
+
type: (ref, text) => call("type", { ref, text }),
|
|
1372
|
+
navigate: (url) => call("navigate", { url }),
|
|
1373
|
+
scroll: (dir, amount) => call("scroll", { dir, amount }),
|
|
1374
|
+
readContent: () => call("readContent", {}),
|
|
1375
|
+
extractLinks: () => call("extractLinks", {}),
|
|
1376
|
+
waitForLoad: (timeoutMs) => call("waitForLoad", { timeoutMs }),
|
|
1377
|
+
hover: (ref) => call("hover", { ref }),
|
|
1378
|
+
selectOption: (ref, value) => call("selectOption", { ref, value }),
|
|
1379
|
+
pressKey: (key, ref) => call("pressKey", { key, ref }),
|
|
1380
|
+
fetchImages: (refs) => call("fetchImages", { refs }),
|
|
1381
|
+
screenshot: (ref) => call("screenshot", { ref }),
|
|
1382
|
+
listTabs: () => call("listTabs", {}),
|
|
1383
|
+
switchTab: (tabId) => call("switchTab", { tabId }),
|
|
1384
|
+
};
|
|
1385
|
+
}
|
|
1386
|
+
requestBrowserActionForSession(session, sessionId, action, payload) {
|
|
1387
|
+
return new Promise((resolve) => {
|
|
1388
|
+
const requestId = nanoid(12);
|
|
1389
|
+
session.pendingApprovals.set(requestId, (decision) => {
|
|
1390
|
+
this.clearApprovalTimer(requestId);
|
|
1391
|
+
// Main replies with { approved:true, answer:<json string> } (reusing the
|
|
1392
|
+
// ApprovalResult shape) or a raw json string. Parse → typed result.
|
|
1393
|
+
let raw;
|
|
1394
|
+
if (decision && typeof decision === "object" && "approved" in decision) {
|
|
1395
|
+
const r = decision;
|
|
1396
|
+
raw = r.approved ? r.answer : undefined;
|
|
1397
|
+
}
|
|
1398
|
+
else if (typeof decision === "string") {
|
|
1399
|
+
raw = decision;
|
|
1400
|
+
}
|
|
1401
|
+
if (raw === undefined) {
|
|
1402
|
+
resolve({ ok: false, detail: "browser action declined or unavailable" });
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
try {
|
|
1406
|
+
resolve(JSON.parse(raw));
|
|
1407
|
+
}
|
|
1408
|
+
catch {
|
|
1409
|
+
resolve({ ok: false, detail: "malformed browser action result" });
|
|
1410
|
+
}
|
|
1411
|
+
});
|
|
1412
|
+
const timer = setTimeout(() => {
|
|
1413
|
+
if (session.pendingApprovals.has(requestId)) {
|
|
1414
|
+
session.pendingApprovals.delete(requestId);
|
|
1415
|
+
this.approvalTimers.delete(requestId);
|
|
1416
|
+
resolve({ ok: false, detail: "browser action timed out" });
|
|
1417
|
+
}
|
|
1418
|
+
}, AgentServer.APPROVAL_TIMEOUT_MS);
|
|
1419
|
+
this.approvalTimers.set(requestId, timer);
|
|
1420
|
+
this.notify(Methods.ApprovalRequest, {
|
|
1421
|
+
sessionId,
|
|
1422
|
+
requestId,
|
|
1423
|
+
request: {
|
|
1424
|
+
toolName: "__browser_action__",
|
|
1425
|
+
args: { action, ...payload },
|
|
1426
|
+
description: `browser:${action}`,
|
|
1427
|
+
riskLevel: "low",
|
|
1428
|
+
},
|
|
1429
|
+
});
|
|
1430
|
+
});
|
|
1431
|
+
}
|
|
1432
|
+
/**
|
|
1433
|
+
* Ask the client (Electron main) to inject a cookie credential into the
|
|
1434
|
+
* built-in browser. Same pendingApprovals channel as browser actions; main
|
|
1435
|
+
* calls restoreCookiesToBrowser and replies with a JSON result string we
|
|
1436
|
+
* parse into { ok, count?, error? }. Degrades to ok:false on timeout/malformed.
|
|
1437
|
+
*/
|
|
1438
|
+
requestCredentialInjectForSession(session, sessionId, credentialId) {
|
|
1439
|
+
return new Promise((resolve) => {
|
|
1440
|
+
const requestId = nanoid(12);
|
|
1441
|
+
session.pendingApprovals.set(requestId, (decision) => {
|
|
1442
|
+
this.clearApprovalTimer(requestId);
|
|
1443
|
+
let raw;
|
|
1444
|
+
if (decision && typeof decision === "object" && "approved" in decision) {
|
|
1445
|
+
const r = decision;
|
|
1446
|
+
raw = r.approved ? r.answer : undefined;
|
|
1447
|
+
}
|
|
1448
|
+
else if (typeof decision === "string") {
|
|
1449
|
+
raw = decision;
|
|
1450
|
+
}
|
|
1451
|
+
if (raw === undefined) {
|
|
1452
|
+
resolve({ ok: false, error: "credential inject declined or unavailable" });
|
|
1453
|
+
return;
|
|
1454
|
+
}
|
|
1455
|
+
try {
|
|
1456
|
+
resolve(JSON.parse(raw));
|
|
1457
|
+
}
|
|
1458
|
+
catch {
|
|
1459
|
+
resolve({ ok: false, error: "malformed credential inject result" });
|
|
1460
|
+
}
|
|
1461
|
+
});
|
|
1462
|
+
const timer = setTimeout(() => {
|
|
1463
|
+
if (session.pendingApprovals.has(requestId)) {
|
|
1464
|
+
session.pendingApprovals.delete(requestId);
|
|
1465
|
+
this.approvalTimers.delete(requestId);
|
|
1466
|
+
resolve({ ok: false, error: "credential inject timed out" });
|
|
1467
|
+
}
|
|
1468
|
+
}, AgentServer.APPROVAL_TIMEOUT_MS);
|
|
1469
|
+
this.approvalTimers.set(requestId, timer);
|
|
1470
|
+
this.notify(Methods.ApprovalRequest, {
|
|
1471
|
+
sessionId,
|
|
1472
|
+
requestId,
|
|
1473
|
+
request: {
|
|
1474
|
+
toolName: "__credential_action__",
|
|
1475
|
+
args: { action: "injectCookie", credentialId },
|
|
1476
|
+
description: `credential:inject:${credentialId}`,
|
|
1477
|
+
riskLevel: "low",
|
|
1478
|
+
},
|
|
1479
|
+
});
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
934
1482
|
requestAskUserFromClient(question, opts) {
|
|
935
1483
|
return new Promise((resolve) => {
|
|
936
1484
|
const requestId = nanoid(12);
|
|
@@ -958,6 +1506,8 @@ export class AgentServer {
|
|
|
958
1506
|
args.options = opts.options;
|
|
959
1507
|
if (opts?.multiSelect !== undefined)
|
|
960
1508
|
args.multiSelect = opts.multiSelect;
|
|
1509
|
+
if (opts?.optionsOnly !== undefined)
|
|
1510
|
+
args.optionsOnly = opts.optionsOnly;
|
|
961
1511
|
this.notify(Methods.ApprovalRequest, {
|
|
962
1512
|
requestId,
|
|
963
1513
|
request: {
|
|
@@ -1036,6 +1586,26 @@ export class AgentServer {
|
|
|
1036
1586
|
this.approvalTimers.delete(requestId);
|
|
1037
1587
|
}
|
|
1038
1588
|
}
|
|
1589
|
+
/**
|
|
1590
|
+
* Resolve all of a chat session's pending approvals as cancelled and clear
|
|
1591
|
+
* the matching server-side approval timers. Used by handleCancel's
|
|
1592
|
+
* per-session path so a Stop while a tool is awaiting approval doesn't leave
|
|
1593
|
+
* the tool hanging until APPROVAL_TIMEOUT_MS (and leak the timer). Each
|
|
1594
|
+
* requestId in the session map has a same-keyed entry in this.approvalTimers
|
|
1595
|
+
* (see requestAskUserForSession / makeBrowserBridge).
|
|
1596
|
+
*/
|
|
1597
|
+
cancelSessionApprovals(session) {
|
|
1598
|
+
for (const [requestId, resolve] of session.pendingApprovals) {
|
|
1599
|
+
this.clearApprovalTimer(requestId);
|
|
1600
|
+
try {
|
|
1601
|
+
resolve({ approved: false, reason: "cancelled" });
|
|
1602
|
+
}
|
|
1603
|
+
catch {
|
|
1604
|
+
/* a resolver must never break cancel cleanup */
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
session.pendingApprovals.clear();
|
|
1608
|
+
}
|
|
1039
1609
|
clearAllApprovalTimers() {
|
|
1040
1610
|
for (const timer of this.approvalTimers.values()) {
|
|
1041
1611
|
clearTimeout(timer);
|