@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
|
@@ -0,0 +1,495 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* BackgroundShellManager — session-scoped registry + lifecycle for
|
|
3
|
+
* fire-and-forget background shells (design §4).
|
|
4
|
+
*
|
|
5
|
+
* A background shell is a long-lived process (typically `npm run dev`)
|
|
6
|
+
* started via `Bash(run_in_background=true)`. Unlike the foreground Bash
|
|
7
|
+
* path (which awaits the command to completion), this:
|
|
8
|
+
*
|
|
9
|
+
* - spawns the command **detached** so it owns its own process group
|
|
10
|
+
* (so KillShell can reap the whole `sh → npm → node vite` tree, §难点2);
|
|
11
|
+
* - returns a `shell_id` immediately, never blocking the turn;
|
|
12
|
+
* - streams output to an in-memory ring buffer + a wrap-around disk file,
|
|
13
|
+
* stripping ANSI/progress noise only when handed to the agent (§难点4);
|
|
14
|
+
* - detects a served port best-effort from the output (§难点3);
|
|
15
|
+
* - is **completely separate** from `asyncAgentRegistry` so Engine.run's
|
|
16
|
+
* wait-for-background loop never waits on a dev server (§难点5);
|
|
17
|
+
* - on exit, enqueues a one-line completion notification (no output body,
|
|
18
|
+
* §难点6 / D6) keyed by sessionId.
|
|
19
|
+
*
|
|
20
|
+
* Lifetime is process-local and session-scoped: `killSession(sid)` on
|
|
21
|
+
* explicit session deletion, `killAll()` on app/worker exit. Crashing the
|
|
22
|
+
* worker leaks the detached children as orphans — pidfiles let a restarted
|
|
23
|
+
* worker discover and reap them (§难点1, `reapOrphansFromPidfiles`).
|
|
24
|
+
*/
|
|
25
|
+
import { spawn } from "node:child_process";
|
|
26
|
+
import { homedir } from "node:os";
|
|
27
|
+
import { join } from "node:path";
|
|
28
|
+
import { mkdirSync, writeFileSync, rmSync, existsSync, readdirSync, readFileSync, } from "node:fs";
|
|
29
|
+
import { StringDecoder } from "node:string_decoder";
|
|
30
|
+
import { utf8SafeCutLength } from "./utf8-cut.js";
|
|
31
|
+
import { resolveSpawnTarget, buildSandboxEnv, mergeShellEnv, killProcessGroup, groupAlive, defaultShellBinary } from "./spawn-common.js";
|
|
32
|
+
import { RingFile } from "./ring-file.js";
|
|
33
|
+
import { cleanOutput } from "./output-clean.js";
|
|
34
|
+
import { notificationQueue } from "../tool-system/builtin/agent-notifications.js";
|
|
35
|
+
import { logger } from "../logging/logger.js";
|
|
36
|
+
/** Per-session soft cap on background shells (design §7 — fork-bomb guard). */
|
|
37
|
+
export const MAX_SHELLS_PER_SESSION = 16;
|
|
38
|
+
/** Disk ring-file cap: keep the most recent 8MB of raw output (design §6). */
|
|
39
|
+
const DISK_CAP_BYTES = 8 * 1024 * 1024;
|
|
40
|
+
/** BashOutput single-return cap (strip后尾部 ~16KB) so one read can't blow context. */
|
|
41
|
+
const READ_RETURN_CAP = 16 * 1024;
|
|
42
|
+
const KILL_GRACE_MS = 3000;
|
|
43
|
+
let shellCounter = 0;
|
|
44
|
+
function nextShellId() {
|
|
45
|
+
shellCounter += 1;
|
|
46
|
+
// Short, collision-resistant enough within a process: counter + base36 of
|
|
47
|
+
// a monotonic-ish value. Avoids Date.now()/Math.random() (banned in some
|
|
48
|
+
// contexts and unnecessary here).
|
|
49
|
+
return `bg_${shellCounter.toString(36)}${(shellCounter * 2654435761 % 0xffffff).toString(36)}`;
|
|
50
|
+
}
|
|
51
|
+
function bgShellsRoot() {
|
|
52
|
+
const base = process.env.CODE_SHELL_HOME ?? process.env.HOME ?? homedir();
|
|
53
|
+
return join(base, ".code-shell", "bg-shells");
|
|
54
|
+
}
|
|
55
|
+
const PORT_REGEXES = [
|
|
56
|
+
/localhost:(\d{2,5})/i,
|
|
57
|
+
/127\.0\.0\.1:(\d{2,5})/,
|
|
58
|
+
/0\.0\.0\.0:(\d{2,5})/,
|
|
59
|
+
/:(\d{4,5})\b/,
|
|
60
|
+
];
|
|
61
|
+
function detectPort(text) {
|
|
62
|
+
for (const re of PORT_REGEXES) {
|
|
63
|
+
const m = re.exec(text);
|
|
64
|
+
if (m) {
|
|
65
|
+
const p = Number(m[1]);
|
|
66
|
+
if (p >= 1 && p <= 65535)
|
|
67
|
+
return p;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
export class BackgroundShellManager {
|
|
73
|
+
shells = new Map();
|
|
74
|
+
spawnBackground(opts) {
|
|
75
|
+
// Defense-in-depth: sessionId becomes a directory under bgShellsRoot()
|
|
76
|
+
// (`join(root, sessionId)`, below). It's engine-generated/validated today,
|
|
77
|
+
// but a `..`/separator/empty id would write the shell log + record OUTSIDE
|
|
78
|
+
// the bg-shells root — refuse rather than trust the caller. Mirrors
|
|
79
|
+
// SessionManager.assertSafeSessionId without importing across the layer.
|
|
80
|
+
const sid = opts.sessionId;
|
|
81
|
+
if (typeof sid !== "string" || sid.length === 0 || sid.includes("..") || sid.includes("/") || sid.includes("\\")) {
|
|
82
|
+
return { ok: false, error: `invalid sessionId for background shell` };
|
|
83
|
+
}
|
|
84
|
+
// Per-session soft cap (fork-bomb guard, §7).
|
|
85
|
+
const live = [...this.shells.values()].filter((s) => s.sessionId === opts.sessionId && (s.status === "running" || s.status === "starting"));
|
|
86
|
+
if (live.length >= MAX_SHELLS_PER_SESSION) {
|
|
87
|
+
return {
|
|
88
|
+
ok: false,
|
|
89
|
+
error: `Too many background shells for this session (max ${MAX_SHELLS_PER_SESSION}). KillShell some first.`,
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
const shell = opts.shell ?? defaultShellBinary();
|
|
93
|
+
const { file, args } = resolveSpawnTarget(opts.command, {
|
|
94
|
+
cwd: opts.cwd,
|
|
95
|
+
shell,
|
|
96
|
+
sandbox: opts.sandbox,
|
|
97
|
+
});
|
|
98
|
+
const baseEnv = opts.sandbox && opts.sandbox.name !== "off" ? buildSandboxEnv() : { ...process.env };
|
|
99
|
+
const env = mergeShellEnv(baseEnv, opts.shellEnv);
|
|
100
|
+
let child;
|
|
101
|
+
try {
|
|
102
|
+
child = spawn(file, args, {
|
|
103
|
+
cwd: opts.cwd,
|
|
104
|
+
env,
|
|
105
|
+
// POSIX: detached → own process group so a negative-pid signal reaps
|
|
106
|
+
// the whole tree (npm → node → vite). Windows has no process groups;
|
|
107
|
+
// detached there spawns a separate console window instead, so we keep
|
|
108
|
+
// it attached and rely on `taskkill /T` (killProcessGroup's win32
|
|
109
|
+
// branch) to reap the tree. windowsHide stops a console flashing up.
|
|
110
|
+
detached: process.platform !== "win32",
|
|
111
|
+
windowsHide: true,
|
|
112
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
catch (err) {
|
|
116
|
+
return { ok: false, error: `Failed to spawn background shell: ${err.message}` };
|
|
117
|
+
}
|
|
118
|
+
if (child.pid === undefined) {
|
|
119
|
+
return { ok: false, error: "Failed to spawn background shell: no pid" };
|
|
120
|
+
}
|
|
121
|
+
const shellId = nextShellId();
|
|
122
|
+
const pgid = child.pid; // detached ⇒ pgid == pid
|
|
123
|
+
const dir = join(bgShellsRoot(), opts.sessionId);
|
|
124
|
+
const pidfilePath = join(dir, `${shellId}.json`);
|
|
125
|
+
const logPath = join(dir, `${shellId}.log`);
|
|
126
|
+
let ring;
|
|
127
|
+
try {
|
|
128
|
+
mkdirSync(dir, { recursive: true });
|
|
129
|
+
ring = new RingFile(logPath, DISK_CAP_BYTES);
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// Disk unavailable — degrade to a memory-only ring at a temp-less path.
|
|
133
|
+
ring = new RingFile(join(dir, `${shellId}.log`), DISK_CAP_BYTES);
|
|
134
|
+
}
|
|
135
|
+
const startedAt = Date.now();
|
|
136
|
+
const rec = {
|
|
137
|
+
shellId,
|
|
138
|
+
pgid,
|
|
139
|
+
command: opts.command,
|
|
140
|
+
startedAt,
|
|
141
|
+
sessionId: opts.sessionId,
|
|
142
|
+
};
|
|
143
|
+
try {
|
|
144
|
+
writeFileSync(pidfilePath, JSON.stringify(rec), "utf8");
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
/* pidfile is best-effort; orphan reaping just won't see this one */
|
|
148
|
+
}
|
|
149
|
+
const sh = {
|
|
150
|
+
shellId,
|
|
151
|
+
sessionId: opts.sessionId,
|
|
152
|
+
command: opts.command,
|
|
153
|
+
cwd: opts.cwd,
|
|
154
|
+
pgid,
|
|
155
|
+
status: "running",
|
|
156
|
+
startedAt,
|
|
157
|
+
exitCode: null,
|
|
158
|
+
signal: null,
|
|
159
|
+
agentReadOffset: 0,
|
|
160
|
+
didWrap: false,
|
|
161
|
+
diskAvailable: ring.diskAvailable(),
|
|
162
|
+
child,
|
|
163
|
+
ring,
|
|
164
|
+
pidfilePath,
|
|
165
|
+
stdoutDec: new StringDecoder("utf-8"),
|
|
166
|
+
stderrDec: new StringDecoder("utf-8"),
|
|
167
|
+
portScanBuf: "",
|
|
168
|
+
};
|
|
169
|
+
this.shells.set(shellId, sh);
|
|
170
|
+
const onData = (chunk) => {
|
|
171
|
+
sh.ring.write(chunk);
|
|
172
|
+
sh.didWrap = sh.ring.didWrap();
|
|
173
|
+
if (sh.detectedPort === undefined) {
|
|
174
|
+
// Scan a bounded recent window of decoded text for a port.
|
|
175
|
+
sh.portScanBuf = (sh.portScanBuf + chunk.toString("utf8")).slice(-4096);
|
|
176
|
+
const p = detectPort(sh.portScanBuf);
|
|
177
|
+
if (p !== undefined) {
|
|
178
|
+
sh.detectedPort = p;
|
|
179
|
+
this.updatePidfilePort(sh, p);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
child.stdout?.on("data", onData);
|
|
184
|
+
child.stderr?.on("data", onData);
|
|
185
|
+
child.on("error", (err) => {
|
|
186
|
+
logger.warn("bg_shell.child_error", { shellId, error: err.message });
|
|
187
|
+
});
|
|
188
|
+
child.on("exit", (code, sig) => {
|
|
189
|
+
// Only natural exits land here as "exited"; kill() sets "killed" first.
|
|
190
|
+
if (sh.status === "killed") {
|
|
191
|
+
this.finalize(sh, code, sig);
|
|
192
|
+
return;
|
|
193
|
+
}
|
|
194
|
+
sh.status = "exited";
|
|
195
|
+
this.finalize(sh, code, sig);
|
|
196
|
+
this.enqueueExitNotification(sh);
|
|
197
|
+
});
|
|
198
|
+
return { ok: true, shellId };
|
|
199
|
+
}
|
|
200
|
+
finalize(sh, code, sig) {
|
|
201
|
+
sh.exitCode = code;
|
|
202
|
+
sh.signal = sig;
|
|
203
|
+
sh.exitedAt = Date.now();
|
|
204
|
+
sh.child = null;
|
|
205
|
+
try {
|
|
206
|
+
sh.ring.close();
|
|
207
|
+
}
|
|
208
|
+
catch {
|
|
209
|
+
/* best-effort */
|
|
210
|
+
}
|
|
211
|
+
try {
|
|
212
|
+
rmSync(sh.pidfilePath, { force: true });
|
|
213
|
+
}
|
|
214
|
+
catch {
|
|
215
|
+
/* best-effort */
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
enqueueExitNotification(sh) {
|
|
219
|
+
const ok = sh.exitCode === 0;
|
|
220
|
+
const exitDesc = sh.signal
|
|
221
|
+
? `signal ${sh.signal}`
|
|
222
|
+
: `exit ${sh.exitCode ?? "?"}`;
|
|
223
|
+
notificationQueue.enqueue({
|
|
224
|
+
agentId: sh.shellId,
|
|
225
|
+
// English description is the agent-facing wakeup text. The UI ignores
|
|
226
|
+
// it for the toast and uses workKind + command to build a localized
|
|
227
|
+
// label (see bgCompletionText in desktop renderer types.ts). No `name`
|
|
228
|
+
// so legacy UIs fall back to their own default rather than showing the
|
|
229
|
+
// English "background shell" brand string.
|
|
230
|
+
description: `Background shell exited (${exitDesc}): ${sh.command}`,
|
|
231
|
+
status: ok ? "completed" : "failed",
|
|
232
|
+
workKind: "shell",
|
|
233
|
+
command: sh.command,
|
|
234
|
+
error: ok ? undefined : `Background shell ${sh.shellId} exited with ${exitDesc}. Use BashOutput("${sh.shellId}") to inspect.`,
|
|
235
|
+
enqueuedAt: Date.now(),
|
|
236
|
+
}, sh.sessionId);
|
|
237
|
+
}
|
|
238
|
+
updatePidfilePort(sh, port) {
|
|
239
|
+
try {
|
|
240
|
+
const rec = {
|
|
241
|
+
shellId: sh.shellId,
|
|
242
|
+
pgid: sh.pgid,
|
|
243
|
+
command: sh.command,
|
|
244
|
+
port,
|
|
245
|
+
startedAt: sh.startedAt,
|
|
246
|
+
sessionId: sh.sessionId,
|
|
247
|
+
};
|
|
248
|
+
writeFileSync(sh.pidfilePath, JSON.stringify(rec), "utf8");
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
/* best-effort */
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
get(shellId) {
|
|
255
|
+
const sh = this.shells.get(shellId);
|
|
256
|
+
return sh ? this.toPublic(sh) : undefined;
|
|
257
|
+
}
|
|
258
|
+
/** Raw retained output (no cleaning) — test/UI helper. */
|
|
259
|
+
readOutputRaw(shellId) {
|
|
260
|
+
return this.shells.get(shellId)?.ring.readAll();
|
|
261
|
+
}
|
|
262
|
+
listForSession(sessionId) {
|
|
263
|
+
return [...this.shells.values()]
|
|
264
|
+
.filter((s) => s.sessionId === sessionId)
|
|
265
|
+
.map((s) => this.toPublic(s));
|
|
266
|
+
}
|
|
267
|
+
/**
|
|
268
|
+
* Read a shell's output. `mode=incremental` (default) returns text since the
|
|
269
|
+
* agent's last read; `mode=all` returns the full retained buffer. Output is
|
|
270
|
+
* ANSI-stripped and progress-folded, capped at the last READ_RETURN_CAP
|
|
271
|
+
* chars. `expectSessionId`, when given, enforces session ownership.
|
|
272
|
+
*/
|
|
273
|
+
readOutput(shellId, mode = "incremental", expectSessionId) {
|
|
274
|
+
const sh = this.shells.get(shellId);
|
|
275
|
+
if (!sh)
|
|
276
|
+
return { ok: false, error: `Unknown shell_id: ${shellId}` };
|
|
277
|
+
if (expectSessionId !== undefined && sh.sessionId !== expectSessionId) {
|
|
278
|
+
return { ok: false, error: `Unknown shell_id: ${shellId}` };
|
|
279
|
+
}
|
|
280
|
+
let rawSlice;
|
|
281
|
+
let capped = false;
|
|
282
|
+
if (mode === "all") {
|
|
283
|
+
// Snapshot of the full retained buffer. This is explicitly a "show me the
|
|
284
|
+
// tail" view — when it exceeds the cap we keep the LAST 16KB (most recent
|
|
285
|
+
// output) and don't touch the incremental cursor.
|
|
286
|
+
rawSlice = sh.ring.readAll();
|
|
287
|
+
let text = cleanOutput(rawSlice);
|
|
288
|
+
if (text.length > READ_RETURN_CAP) {
|
|
289
|
+
text = text.slice(-READ_RETURN_CAP);
|
|
290
|
+
capped = true;
|
|
291
|
+
}
|
|
292
|
+
return { ok: true, text, header: this.buildHeader(sh, capped, "all") };
|
|
293
|
+
}
|
|
294
|
+
// Incremental: deliver bytes since the agent's last read WITHOUT dropping
|
|
295
|
+
// any. A single burst larger than the cap is paginated across successive
|
|
296
|
+
// reads — we consume only the leading READ_RETURN_CAP *raw* bytes this call
|
|
297
|
+
// and advance the absolute cursor by exactly that many, so the remainder is
|
|
298
|
+
// returned next read. (Truncating to the trailing 16KB while advancing the
|
|
299
|
+
// cursor to the end silently dropped the earliest bytes forever — #5.)
|
|
300
|
+
//
|
|
301
|
+
// Absolute stream cursor — survives ring wraparound (a window-relative
|
|
302
|
+
// offset would silently skip new bytes once the window slides).
|
|
303
|
+
const sliceBuf = sh.ring.sliceFromAbsolute(sh.agentReadOffset);
|
|
304
|
+
let consumedBytes = sliceBuf.length;
|
|
305
|
+
let rawBuf = sliceBuf;
|
|
306
|
+
if (rawBuf.length > READ_RETURN_CAP) {
|
|
307
|
+
// Cap on RAW bytes (not cleaned length): cleanOutput only ever shrinks,
|
|
308
|
+
// so the cleaned result of the first 16KB raw bytes is itself ≤ 16KB and
|
|
309
|
+
// won't need a second truncation. Advancing the cursor by exactly
|
|
310
|
+
// consumedBytes keeps the next read seamless.
|
|
311
|
+
// Back the cut off to the last complete UTF-8 char boundary so a
|
|
312
|
+
// multibyte char straddling the 16KB line isn't split into a `�` — and
|
|
313
|
+
// so consumedBytes lands on a boundary and the next read resumes cleanly.
|
|
314
|
+
// Guard against a pathological 0 (buffer starting mid-char) to guarantee
|
|
315
|
+
// forward progress.
|
|
316
|
+
const safeCut = utf8SafeCutLength(rawBuf, READ_RETURN_CAP) || READ_RETURN_CAP;
|
|
317
|
+
rawBuf = rawBuf.subarray(0, safeCut);
|
|
318
|
+
consumedBytes = safeCut;
|
|
319
|
+
capped = true;
|
|
320
|
+
}
|
|
321
|
+
rawSlice = rawBuf.toString("utf8");
|
|
322
|
+
sh.agentReadOffset += consumedBytes;
|
|
323
|
+
const text = cleanOutput(rawSlice);
|
|
324
|
+
return { ok: true, text, header: this.buildHeader(sh, capped, "incremental") };
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Build the status header line for a readOutput result. `mode` shapes the cap
|
|
328
|
+
* note: `all` keeps the trailing 16KB (older output omitted from THIS view),
|
|
329
|
+
* while incremental paginates (more output available on the next read).
|
|
330
|
+
*/
|
|
331
|
+
buildHeader(sh, capped, mode) {
|
|
332
|
+
const portPart = sh.detectedPort !== undefined ? ` port=${sh.detectedPort}` : "";
|
|
333
|
+
const exitPart = sh.status === "exited" || sh.status === "killed"
|
|
334
|
+
? sh.signal
|
|
335
|
+
? ` signal=${sh.signal}`
|
|
336
|
+
: ` exit=${sh.exitCode ?? "?"}`
|
|
337
|
+
: "";
|
|
338
|
+
const wrapNote = sh.didWrap ? " (older output discarded)" : "";
|
|
339
|
+
const capNote = capped
|
|
340
|
+
? mode === "all"
|
|
341
|
+
? " (showing last 16KB)"
|
|
342
|
+
: " (16KB this read; call again for more)"
|
|
343
|
+
: "";
|
|
344
|
+
const diskNote = sh.diskAvailable ? "" : " (disk buffer unavailable)";
|
|
345
|
+
return `[${sh.shellId} status=${sh.status}${portPart}${exitPart}]${wrapNote}${capNote}${diskNote}`;
|
|
346
|
+
}
|
|
347
|
+
/** Terminate a shell (its whole process group). Idempotent. */
|
|
348
|
+
async kill(shellId, expectSessionId) {
|
|
349
|
+
const sh = this.shells.get(shellId);
|
|
350
|
+
if (!sh)
|
|
351
|
+
return { ok: false, error: `Unknown shell_id: ${shellId}` };
|
|
352
|
+
if (expectSessionId !== undefined && sh.sessionId !== expectSessionId) {
|
|
353
|
+
return { ok: false, error: `Unknown shell_id: ${shellId}` };
|
|
354
|
+
}
|
|
355
|
+
if (sh.status === "exited" || sh.status === "killed") {
|
|
356
|
+
return { ok: true, alreadyExited: true, status: sh.status };
|
|
357
|
+
}
|
|
358
|
+
sh.status = "killed";
|
|
359
|
+
await killProcessGroup(sh.pgid, { graceMs: KILL_GRACE_MS });
|
|
360
|
+
// The 'exit' handler runs finalize(); if the child already detached/died
|
|
361
|
+
// without firing it, finalize defensively.
|
|
362
|
+
if (sh.exitedAt === undefined) {
|
|
363
|
+
this.finalize(sh, sh.exitCode, sh.signal);
|
|
364
|
+
}
|
|
365
|
+
return { ok: true, status: "killed" };
|
|
366
|
+
}
|
|
367
|
+
/** Kill every background shell belonging to `sessionId`. */
|
|
368
|
+
async killSession(sessionId) {
|
|
369
|
+
const ids = [...this.shells.values()]
|
|
370
|
+
.filter((s) => s.sessionId === sessionId && (s.status === "running" || s.status === "starting"))
|
|
371
|
+
.map((s) => s.shellId);
|
|
372
|
+
await Promise.all(ids.map((id) => this.kill(id)));
|
|
373
|
+
}
|
|
374
|
+
/** Kill every background shell (app/worker exit). */
|
|
375
|
+
async killAll() {
|
|
376
|
+
const ids = [...this.shells.values()]
|
|
377
|
+
.filter((s) => s.status === "running" || s.status === "starting")
|
|
378
|
+
.map((s) => s.shellId);
|
|
379
|
+
await Promise.all(ids.map((id) => this.kill(id)));
|
|
380
|
+
}
|
|
381
|
+
/**
|
|
382
|
+
* Scan pidfiles left by a previous (crashed) worker. For each: if the pgid
|
|
383
|
+
* is still alive it's an orphan (the dev server outlived its worker) — list
|
|
384
|
+
* it as `orphaned` so KillShell can clean it up; if dead, delete the stale
|
|
385
|
+
* pidfile. Returns the orphaned records found.
|
|
386
|
+
*/
|
|
387
|
+
reapOrphansFromPidfiles() {
|
|
388
|
+
const root = bgShellsRoot();
|
|
389
|
+
if (!existsSync(root))
|
|
390
|
+
return [];
|
|
391
|
+
const orphans = [];
|
|
392
|
+
let sessionDirs;
|
|
393
|
+
try {
|
|
394
|
+
sessionDirs = readdirSync(root);
|
|
395
|
+
}
|
|
396
|
+
catch {
|
|
397
|
+
return [];
|
|
398
|
+
}
|
|
399
|
+
for (const sid of sessionDirs) {
|
|
400
|
+
const dir = join(root, sid);
|
|
401
|
+
let files;
|
|
402
|
+
try {
|
|
403
|
+
files = readdirSync(dir);
|
|
404
|
+
}
|
|
405
|
+
catch {
|
|
406
|
+
continue;
|
|
407
|
+
}
|
|
408
|
+
for (const f of files) {
|
|
409
|
+
if (!f.endsWith(".json"))
|
|
410
|
+
continue;
|
|
411
|
+
const path = join(dir, f);
|
|
412
|
+
let rec;
|
|
413
|
+
try {
|
|
414
|
+
rec = JSON.parse(readFileSync(path, "utf8"));
|
|
415
|
+
}
|
|
416
|
+
catch {
|
|
417
|
+
try {
|
|
418
|
+
rmSync(path, { force: true });
|
|
419
|
+
}
|
|
420
|
+
catch { /* ignore */ }
|
|
421
|
+
continue;
|
|
422
|
+
}
|
|
423
|
+
// Skip ones we already track in-process (this worker's own).
|
|
424
|
+
if (this.shells.has(rec.shellId))
|
|
425
|
+
continue;
|
|
426
|
+
if (groupAlive(rec.pgid)) {
|
|
427
|
+
orphans.push(rec);
|
|
428
|
+
this.registerOrphan(rec, path);
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
try {
|
|
432
|
+
rmSync(path, { force: true });
|
|
433
|
+
}
|
|
434
|
+
catch { /* ignore */ }
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
return orphans;
|
|
439
|
+
}
|
|
440
|
+
registerOrphan(rec, pidfilePath) {
|
|
441
|
+
// A minimal entry: we have no live child/ring (different process), but
|
|
442
|
+
// ListShells should show it and KillShell should be able to reap it.
|
|
443
|
+
const sh = {
|
|
444
|
+
shellId: rec.shellId,
|
|
445
|
+
sessionId: rec.sessionId,
|
|
446
|
+
command: rec.command,
|
|
447
|
+
cwd: "",
|
|
448
|
+
pgid: rec.pgid,
|
|
449
|
+
status: "orphaned",
|
|
450
|
+
startedAt: rec.startedAt,
|
|
451
|
+
exitCode: null,
|
|
452
|
+
signal: null,
|
|
453
|
+
detectedPort: rec.port,
|
|
454
|
+
agentReadOffset: 0,
|
|
455
|
+
didWrap: false,
|
|
456
|
+
diskAvailable: false,
|
|
457
|
+
child: null,
|
|
458
|
+
// Read the orphan's ALREADY-captured output from its on-disk .log (the
|
|
459
|
+
// live process in the other worker owns/writes it). Opening it read-only
|
|
460
|
+
// surfaces the real tail instead of a fresh EMPTY .orphan ring that made
|
|
461
|
+
// a recovered shell always show "(无输出)". (#7)
|
|
462
|
+
ring: new RingFile(join(bgShellsRoot(), rec.sessionId, `${rec.shellId}.log`), DISK_CAP_BYTES, true),
|
|
463
|
+
pidfilePath,
|
|
464
|
+
stdoutDec: new StringDecoder("utf-8"),
|
|
465
|
+
stderrDec: new StringDecoder("utf-8"),
|
|
466
|
+
portScanBuf: "",
|
|
467
|
+
};
|
|
468
|
+
this.shells.set(rec.shellId, sh);
|
|
469
|
+
}
|
|
470
|
+
/** Test/reset helper — clears the registry without killing (used in tests). */
|
|
471
|
+
_clear() {
|
|
472
|
+
this.shells.clear();
|
|
473
|
+
}
|
|
474
|
+
toPublic(s) {
|
|
475
|
+
return {
|
|
476
|
+
shellId: s.shellId,
|
|
477
|
+
sessionId: s.sessionId,
|
|
478
|
+
command: s.command,
|
|
479
|
+
cwd: s.cwd,
|
|
480
|
+
pgid: s.pgid,
|
|
481
|
+
status: s.status,
|
|
482
|
+
startedAt: s.startedAt,
|
|
483
|
+
exitedAt: s.exitedAt,
|
|
484
|
+
exitCode: s.exitCode,
|
|
485
|
+
signal: s.signal,
|
|
486
|
+
detectedPort: s.detectedPort,
|
|
487
|
+
agentReadOffset: s.agentReadOffset,
|
|
488
|
+
totalBytes: s.ring.totalWritten(),
|
|
489
|
+
didWrap: s.didWrap,
|
|
490
|
+
diskAvailable: s.diskAvailable,
|
|
491
|
+
};
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
/** Process-local singleton, mirroring asyncAgentRegistry's lifetime contract. */
|
|
495
|
+
export const backgroundShellManager = new BackgroundShellManager();
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* output-clean — sanitize raw process output before it's handed to the LLM.
|
|
3
|
+
*
|
|
4
|
+
* Background dev servers (`npm run dev`, vite, webpack) emit two kinds of
|
|
5
|
+
* noise that are worthless — and token-expensive — in a model's context
|
|
6
|
+
* (design §难点4):
|
|
7
|
+
*
|
|
8
|
+
* - ANSI escape sequences (colors, cursor moves, line erases).
|
|
9
|
+
* - `\r`-driven progress redraws (`▕███░ 45%\r▕████ 100%`) where every
|
|
10
|
+
* frame but the last is stale.
|
|
11
|
+
*
|
|
12
|
+
* The *raw* bytes are still persisted to disk untouched (so a user `tail`ing
|
|
13
|
+
* the file keeps colors); this cleanup only applies to what `BashOutput`
|
|
14
|
+
* returns to the agent.
|
|
15
|
+
*/
|
|
16
|
+
export declare function stripAnsi(s: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Collapse carriage-return progress redraws. Within a single line (text
|
|
19
|
+
* between `\n`s), `\r` means "redraw from column 0" — only the segment after
|
|
20
|
+
* the last `\r` survives. Newlines are preserved as line separators.
|
|
21
|
+
*/
|
|
22
|
+
export declare function foldProgressLines(s: string): string;
|
|
23
|
+
/** Strip ANSI then fold progress frames — the full agent-facing cleanup. */
|
|
24
|
+
export declare function cleanOutput(s: string): string;
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* output-clean — sanitize raw process output before it's handed to the LLM.
|
|
3
|
+
*
|
|
4
|
+
* Background dev servers (`npm run dev`, vite, webpack) emit two kinds of
|
|
5
|
+
* noise that are worthless — and token-expensive — in a model's context
|
|
6
|
+
* (design §难点4):
|
|
7
|
+
*
|
|
8
|
+
* - ANSI escape sequences (colors, cursor moves, line erases).
|
|
9
|
+
* - `\r`-driven progress redraws (`▕███░ 45%\r▕████ 100%`) where every
|
|
10
|
+
* frame but the last is stale.
|
|
11
|
+
*
|
|
12
|
+
* The *raw* bytes are still persisted to disk untouched (so a user `tail`ing
|
|
13
|
+
* the file keeps colors); this cleanup only applies to what `BashOutput`
|
|
14
|
+
* returns to the agent.
|
|
15
|
+
*/
|
|
16
|
+
// Matches CSI sequences (ESC [ ... final-byte) plus a few common non-CSI
|
|
17
|
+
// escapes (ESC ] OSC, single-char escapes). Good enough for dev-server output;
|
|
18
|
+
// we deliberately don't pull in a full terminal parser.
|
|
19
|
+
// eslint-disable-next-line no-control-regex
|
|
20
|
+
const ANSI_REGEX = /[][[\]()#;?]*(?:[0-9]{1,4}(?:;[0-9]{0,4})*)?[0-9A-PR-TZcf-ntqry=><~]/g;
|
|
21
|
+
export function stripAnsi(s) {
|
|
22
|
+
return s.replace(ANSI_REGEX, "");
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Collapse carriage-return progress redraws. Within a single line (text
|
|
26
|
+
* between `\n`s), `\r` means "redraw from column 0" — only the segment after
|
|
27
|
+
* the last `\r` survives. Newlines are preserved as line separators.
|
|
28
|
+
*/
|
|
29
|
+
export function foldProgressLines(s) {
|
|
30
|
+
return s
|
|
31
|
+
.split("\n")
|
|
32
|
+
.map((line) => {
|
|
33
|
+
const lastCr = line.lastIndexOf("\r");
|
|
34
|
+
return lastCr === -1 ? line : line.slice(lastCr + 1);
|
|
35
|
+
})
|
|
36
|
+
.join("\n");
|
|
37
|
+
}
|
|
38
|
+
/** Strip ANSI then fold progress frames — the full agent-facing cleanup. */
|
|
39
|
+
export function cleanOutput(s) {
|
|
40
|
+
return foldProgressLines(stripAnsi(s));
|
|
41
|
+
}
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RingFile — a bounded, wrap-around output sink for a background shell's
|
|
3
|
+
* stdout/stderr (design §6 "落盘 8MB 环绕覆盖").
|
|
4
|
+
*
|
|
5
|
+
* Semantics: the logical stream is unbounded, but only the **last N bytes**
|
|
6
|
+
* are retained. When the stream exceeds the cap, the oldest bytes are
|
|
7
|
+
* dropped (`didWrap()` flips true). A dev server's startup banner is
|
|
8
|
+
* disposable; the recent tail is what the agent/user wants.
|
|
9
|
+
*
|
|
10
|
+
* Implementation: an in-memory tail Buffer is authoritative (so `readAll`
|
|
11
|
+
* and the manager's offset math never touch disk), mirrored to a file so a
|
|
12
|
+
* user can `tail`/open it with external tools. The disk mirror is
|
|
13
|
+
* best-effort — if the path is unwritable the in-memory buffer still works
|
|
14
|
+
* and the manager degrades to memory-only (design §7).
|
|
15
|
+
*
|
|
16
|
+
* The on-disk file holds the same retained tail as memory (rewritten on
|
|
17
|
+
* wrap, appended otherwise). We accept the cost of an occasional full
|
|
18
|
+
* rewrite on wrap; for line-oriented dev output at the 8MB cap this is rare
|
|
19
|
+
* and cheap relative to the process's own work.
|
|
20
|
+
*/
|
|
21
|
+
export declare class RingFile {
|
|
22
|
+
private readonly path;
|
|
23
|
+
private readonly capBytes;
|
|
24
|
+
private buf;
|
|
25
|
+
private wrapped;
|
|
26
|
+
private fd;
|
|
27
|
+
private diskOk;
|
|
28
|
+
private readonlyMode;
|
|
29
|
+
/** Total bytes ever written to the logical stream (monotonic, NOT capped).
|
|
30
|
+
* The retained window is the last `min(total, cap)` of these. Readers track
|
|
31
|
+
* an ABSOLUTE position in this stream so incremental reads survive
|
|
32
|
+
* wraparound (a window-relative offset would silently skip new data once
|
|
33
|
+
* the window starts sliding). */
|
|
34
|
+
private total;
|
|
35
|
+
constructor(path: string, capBytes: number,
|
|
36
|
+
/** Read-only: load the EXISTING file's tail into memory and never truncate
|
|
37
|
+
* or write. Used to surface a recovered orphan shell's already-captured
|
|
38
|
+
* output (the live process owns the .log in another worker). */
|
|
39
|
+
readonlyExisting?: boolean);
|
|
40
|
+
/** True if the disk mirror is usable (false → memory-only degraded mode). */
|
|
41
|
+
diskAvailable(): boolean;
|
|
42
|
+
write(chunk: Buffer): void;
|
|
43
|
+
/** Whether any bytes have been discarded by wraparound. */
|
|
44
|
+
didWrap(): boolean;
|
|
45
|
+
/** The retained tail, decoded as UTF-8. */
|
|
46
|
+
readAll(): string;
|
|
47
|
+
/** Current retained byte length (for the agent read offset math). */
|
|
48
|
+
byteLength(): number;
|
|
49
|
+
/** Bytes from `offset` (within the retained window) to the end. */
|
|
50
|
+
sliceFrom(offset: number): Buffer;
|
|
51
|
+
/** Total bytes ever written to the logical stream (monotonic). Readers use
|
|
52
|
+
* this as their next cursor after a read. */
|
|
53
|
+
totalWritten(): number;
|
|
54
|
+
/**
|
|
55
|
+
* Bytes from an ABSOLUTE stream position to the current end. `absOffset` is
|
|
56
|
+
* a value previously returned by {@link totalWritten}. If it points before
|
|
57
|
+
* the retained window (the bytes were discarded by wraparound), returns the
|
|
58
|
+
* whole window. If it's at/after the end, returns empty.
|
|
59
|
+
*/
|
|
60
|
+
sliceFromAbsolute(absOffset: number): Buffer;
|
|
61
|
+
close(): void;
|
|
62
|
+
private appendToDisk;
|
|
63
|
+
private rewriteDisk;
|
|
64
|
+
}
|