mixdog 0.7.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +31 -0
- package/.claude-plugin/plugin.json +20 -0
- package/.gitattributes +34 -0
- package/.mcp.json +14 -0
- package/ARCHITECTURE.md +77 -0
- package/CHANGELOG.md +7 -0
- package/CONTRIBUTING.md +45 -0
- package/DATA-FLOW.md +79 -0
- package/LICENSE +21 -0
- package/README.md +389 -0
- package/SECURITY.md +138 -0
- package/UNINSTALL.md +112 -0
- package/agents/maintenance.md +5 -0
- package/agents/memory-classification.md +30 -0
- package/agents/scheduler-task.md +18 -0
- package/agents/webhook-handler.md +27 -0
- package/agents/worker.md +24 -0
- package/bin/bridge +133 -0
- package/bin/statusline-launcher.mjs +78 -0
- package/bin/statusline-lib.mjs +550 -0
- package/bin/statusline.mjs +607 -0
- package/bun.lock +802 -0
- package/commands/config.md +16 -0
- package/commands/doctor.md +13 -0
- package/commands/setup.md +17 -0
- package/defaults/cycle3-review-prompt.md +90 -0
- package/defaults/hidden-roles.json +65 -0
- package/defaults/memory-chunk-prompt.md +63 -0
- package/defaults/memory-promote-prompt.md +135 -0
- package/defaults/mixdog-config.template.json +27 -0
- package/defaults/user-workflow.json +8 -0
- package/defaults/user-workflow.md +12 -0
- package/hooks/hooks.json +73 -0
- package/hooks/lib/active-instance.cjs +77 -0
- package/hooks/lib/permission-evaluator.cjs +411 -0
- package/hooks/lib/permission-route.cjs +63 -0
- package/hooks/lib/permission-rules.cjs +170 -0
- package/hooks/lib/settings-loader.cjs +116 -0
- package/hooks/post-tool-use.cjs +84 -0
- package/hooks/pre-mcp-sandbox.cjs +158 -0
- package/hooks/pre-tool-subagent.cjs +253 -0
- package/hooks/session-start.cjs +1372 -0
- package/hooks/turn-timer.cjs +82 -0
- package/lib/claude-md-writer.cjs +386 -0
- package/lib/config-cjs.cjs +61 -0
- package/lib/hook-pipe-path.cjs +10 -0
- package/lib/keychain-cjs.cjs +263 -0
- package/lib/plugin-paths.cjs +61 -0
- package/lib/rules-builder.cjs +241 -0
- package/lib/text-utils.cjs +61 -0
- package/native/README.md +117 -0
- package/native/prebuilt/linux-aarch64/mixdog-shim +0 -0
- package/native/prebuilt/linux-x86_64/mixdog-shim +0 -0
- package/native/prebuilt/macos-aarch64/mixdog-shim +0 -0
- package/native/prebuilt/macos-x86_64/mixdog-shim +0 -0
- package/native/prebuilt/windows-x86_64/mixdog-shim.exe +0 -0
- package/package.json +107 -0
- package/prompts/code-review.txt +16 -0
- package/prompts/security-audit.txt +17 -0
- package/rules/bridge/00-common.md +39 -0
- package/rules/bridge/20-skip-protocol.md +18 -0
- package/rules/bridge/30-explorer.md +33 -0
- package/rules/bridge/40-cycle1-agent.md +52 -0
- package/rules/bridge/41-cycle2-agent.md +62 -0
- package/rules/bridge/42-cycle3-agent.md +44 -0
- package/rules/lead/00-tool-lead.md +61 -0
- package/rules/lead/01-general.md +23 -0
- package/rules/lead/02-channels.md +49 -0
- package/rules/lead/03-team.md +27 -0
- package/rules/lead/04-workflow.md +20 -0
- package/rules/shared/00-language.md +14 -0
- package/rules/shared/01-tool.md +138 -0
- package/scripts/bootstrap.mjs +184 -0
- package/scripts/bridge-unify-smoke.mjs +308 -0
- package/scripts/build-runtime-linux.sh +348 -0
- package/scripts/build-runtime-macos.sh +217 -0
- package/scripts/build-runtime-windows.ps1 +242 -0
- package/scripts/builtin-utils-smoke.mjs +392 -0
- package/scripts/check-json.mjs +45 -0
- package/scripts/check-syntax-changed.mjs +102 -0
- package/scripts/check-syntax.mjs +58 -0
- package/scripts/code-graph-batch.test.mjs +33 -0
- package/scripts/config-preserve-smoke.mjs +180 -0
- package/scripts/doctor.mjs +484 -0
- package/scripts/edit-normalize-fuzz.mjs +130 -0
- package/scripts/edit-normalize-smoke.mjs +401 -0
- package/scripts/edit-operation-smoke.mjs +369 -0
- package/scripts/edit2-smoke.mjs +63 -0
- package/scripts/fuzzy-e2e.mjs +28 -0
- package/scripts/fuzzy-smoke.mjs +26 -0
- package/scripts/generate-runtime-manifest.mjs +166 -0
- package/scripts/guard-smoke.mjs +66 -0
- package/scripts/hidden-role-schema-smoke.mjs +162 -0
- package/scripts/hook-routing-smoke.mjs +29 -0
- package/scripts/inject-input.ps1 +204 -0
- package/scripts/io-complex-smoke.mjs +667 -0
- package/scripts/io-explore-bench.mjs +424 -0
- package/scripts/io-guardrails-smoke.mjs +205 -0
- package/scripts/io-mini-bench-baseline.json +11 -0
- package/scripts/io-mini-bench.mjs +216 -0
- package/scripts/io-route-harness.mjs +933 -0
- package/scripts/io-telemetry-report.mjs +691 -0
- package/scripts/mutation-bench.mjs +564 -0
- package/scripts/mutation-io-smoke.mjs +1081 -0
- package/scripts/native-patch-bridge-smoke.mjs +288 -0
- package/scripts/native-patch-smoke.mjs +304 -0
- package/scripts/patch-interior-context-smoke.mjs +49 -0
- package/scripts/patch-newline-utf8-smoke.mjs +157 -0
- package/scripts/perf-hook-smoke.mjs +71 -0
- package/scripts/permission-eval-smoke.mjs +426 -0
- package/scripts/prep-patch.mjs +53 -0
- package/scripts/prep-shim.mjs +96 -0
- package/scripts/provider-cache-smoke.mjs +687 -0
- package/scripts/report-runtime-health.mjs +132 -0
- package/scripts/run-mcp.mjs +1547 -0
- package/scripts/salvage-v4a-shatter.test.mjs +58 -0
- package/scripts/scoped-cache-io-smoke.mjs +103 -0
- package/scripts/shell-policy-round3-smoke.mjs +46 -0
- package/scripts/smoke-runtime-negative.ps1 +100 -0
- package/scripts/smoke-runtime-negative.sh +95 -0
- package/scripts/stall-policy-smoke.mjs +50 -0
- package/scripts/start-memory-worker.mjs +23 -0
- package/scripts/statusline-launcher-smoke.mjs +82 -0
- package/scripts/stress-atomic-write.mjs +1028 -0
- package/scripts/test-config-rmw-restore.mjs +122 -0
- package/scripts/test-fault-inject.mjs +164 -0
- package/scripts/test-large-file.mjs +174 -0
- package/scripts/tool-edge-smoke.mjs +209 -0
- package/scripts/uninstall.mjs +201 -0
- package/scripts/webhook-selfheal-smoke.mjs +29 -0
- package/scripts/write-overwrite-guard-smoke.mjs +56 -0
- package/server-main.mjs +3055 -0
- package/server.mjs +468 -0
- package/setup/config-merge.mjs +254 -0
- package/setup/install.mjs +120 -0
- package/setup/launch-core.mjs +507 -0
- package/setup/launch.mjs +101 -0
- package/setup/setup-server.mjs +3206 -0
- package/setup/setup.html +3693 -0
- package/skills/retro-skill-proposer/SKILL.md +92 -0
- package/skills/schedule-add/SKILL.md +77 -0
- package/skills/setup/SKILL.md +346 -0
- package/skills/webhook-add/SKILL.md +81 -0
- package/src/agent/bridge-stall-watchdog.mjs +337 -0
- package/src/agent/index.mjs +2138 -0
- package/src/agent/orchestrator/activity-bus.mjs +38 -0
- package/src/agent/orchestrator/ai-wrapped-dispatch.mjs +1010 -0
- package/src/agent/orchestrator/bridge-retry.mjs +220 -0
- package/src/agent/orchestrator/bridge-trace.mjs +583 -0
- package/src/agent/orchestrator/cache-mtime.mjs +58 -0
- package/src/agent/orchestrator/config.mjs +358 -0
- package/src/agent/orchestrator/context/collect.mjs +651 -0
- package/src/agent/orchestrator/dispatch-persist.mjs +549 -0
- package/src/agent/orchestrator/drain-registry.mjs +50 -0
- package/src/agent/orchestrator/explore-validator.mjs +8 -0
- package/src/agent/orchestrator/internal-roles.mjs +118 -0
- package/src/agent/orchestrator/internal-tools.mjs +88 -0
- package/src/agent/orchestrator/jobs.mjs +116 -0
- package/src/agent/orchestrator/mcp/client.mjs +364 -0
- package/src/agent/orchestrator/providers/anthropic-betas.mjs +21 -0
- package/src/agent/orchestrator/providers/anthropic-oauth.mjs +1745 -0
- package/src/agent/orchestrator/providers/anthropic.mjs +437 -0
- package/src/agent/orchestrator/providers/gemini.mjs +1175 -0
- package/src/agent/orchestrator/providers/grok-oauth.mjs +782 -0
- package/src/agent/orchestrator/providers/model-catalog.mjs +241 -0
- package/src/agent/orchestrator/providers/openai-compat.mjs +1467 -0
- package/src/agent/orchestrator/providers/openai-oauth-ws.mjs +1890 -0
- package/src/agent/orchestrator/providers/openai-oauth.mjs +1307 -0
- package/src/agent/orchestrator/providers/openai-ws.mjs +104 -0
- package/src/agent/orchestrator/providers/registry.mjs +192 -0
- package/src/agent/orchestrator/providers/retry-classifier.mjs +325 -0
- package/src/agent/orchestrator/session/abort-lookup.mjs +13 -0
- package/src/agent/orchestrator/session/cache/post-edit-marks.mjs +42 -0
- package/src/agent/orchestrator/session/cache/prefetch-cache.mjs +142 -0
- package/src/agent/orchestrator/session/cache/read-cache.mjs +319 -0
- package/src/agent/orchestrator/session/cache/scoped-cache-outcome.mjs +11 -0
- package/src/agent/orchestrator/session/cache/scoped-cache.mjs +361 -0
- package/src/agent/orchestrator/session/cache/util.mjs +49 -0
- package/src/agent/orchestrator/session/loop.mjs +1478 -0
- package/src/agent/orchestrator/session/manager.mjs +1975 -0
- package/src/agent/orchestrator/session/read-dedup.mjs +6 -0
- package/src/agent/orchestrator/session/result-classification.mjs +65 -0
- package/src/agent/orchestrator/session/save-session-worker.mjs +18 -0
- package/src/agent/orchestrator/session/store.mjs +624 -0
- package/src/agent/orchestrator/session/stream-watchdog.mjs +130 -0
- package/src/agent/orchestrator/session/tool-result-offload.mjs +166 -0
- package/src/agent/orchestrator/session/trim.mjs +491 -0
- package/src/agent/orchestrator/smart-bridge/CACHE-SHARD.md +115 -0
- package/src/agent/orchestrator/smart-bridge/bridge-llm.mjs +327 -0
- package/src/agent/orchestrator/smart-bridge/cache-obs.mjs +150 -0
- package/src/agent/orchestrator/smart-bridge/cache-strategy.mjs +228 -0
- package/src/agent/orchestrator/smart-bridge/index.mjs +215 -0
- package/src/agent/orchestrator/smart-bridge/profiles.mjs +37 -0
- package/src/agent/orchestrator/smart-bridge/registry.mjs +348 -0
- package/src/agent/orchestrator/smart-bridge/session-builder.mjs +116 -0
- package/src/agent/orchestrator/stall-policy.mjs +195 -0
- package/src/agent/orchestrator/tool-loop-guard.mjs +75 -0
- package/src/agent/orchestrator/tools/bash-policy-scan.mjs +77 -0
- package/src/agent/orchestrator/tools/bash-session.mjs +721 -0
- package/src/agent/orchestrator/tools/builtin/advisory-lock.mjs +171 -0
- package/src/agent/orchestrator/tools/builtin/arg-guard.mjs +455 -0
- package/src/agent/orchestrator/tools/builtin/atomic-write.mjs +236 -0
- package/src/agent/orchestrator/tools/builtin/bash-tool.mjs +480 -0
- package/src/agent/orchestrator/tools/builtin/binary-file.mjs +76 -0
- package/src/agent/orchestrator/tools/builtin/builtin-tools.mjs +256 -0
- package/src/agent/orchestrator/tools/builtin/cache-layers.mjs +386 -0
- package/src/agent/orchestrator/tools/builtin/cwd-utils.mjs +37 -0
- package/src/agent/orchestrator/tools/builtin/device-paths.mjs +154 -0
- package/src/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +292 -0
- package/src/agent/orchestrator/tools/builtin/diff-utils.mjs +109 -0
- package/src/agent/orchestrator/tools/builtin/edit-base-guard.mjs +58 -0
- package/src/agent/orchestrator/tools/builtin/edit-byte-plan.mjs +240 -0
- package/src/agent/orchestrator/tools/builtin/edit-byte-utils.mjs +113 -0
- package/src/agent/orchestrator/tools/builtin/edit-commit.mjs +74 -0
- package/src/agent/orchestrator/tools/builtin/edit-context-utils.mjs +242 -0
- package/src/agent/orchestrator/tools/builtin/edit-diagnostics.mjs +211 -0
- package/src/agent/orchestrator/tools/builtin/edit-engine.mjs +1364 -0
- package/src/agent/orchestrator/tools/builtin/edit-failure-context.mjs +126 -0
- package/src/agent/orchestrator/tools/builtin/edit-hint.mjs +141 -0
- package/src/agent/orchestrator/tools/builtin/edit-match-utils.mjs +194 -0
- package/src/agent/orchestrator/tools/builtin/edit-partial-write.mjs +60 -0
- package/src/agent/orchestrator/tools/builtin/edit-stale-refresh.mjs +168 -0
- package/src/agent/orchestrator/tools/builtin/edit-tool.mjs +173 -0
- package/src/agent/orchestrator/tools/builtin/edit-utf8-guard.mjs +48 -0
- package/src/agent/orchestrator/tools/builtin/fs-reachability.mjs +48 -0
- package/src/agent/orchestrator/tools/builtin/fuzzy-match.mjs +99 -0
- package/src/agent/orchestrator/tools/builtin/glob-walk.mjs +170 -0
- package/src/agent/orchestrator/tools/builtin/grep-formatting.mjs +113 -0
- package/src/agent/orchestrator/tools/builtin/hash-utils.mjs +6 -0
- package/src/agent/orchestrator/tools/builtin/list-formatting.mjs +7 -0
- package/src/agent/orchestrator/tools/builtin/list-tool.mjs +593 -0
- package/src/agent/orchestrator/tools/builtin/native-edit-runner.mjs +89 -0
- package/src/agent/orchestrator/tools/builtin/notebook-edit-tool.mjs +300 -0
- package/src/agent/orchestrator/tools/builtin/open-config-tool.mjs +26 -0
- package/src/agent/orchestrator/tools/builtin/path-diagnostics.mjs +152 -0
- package/src/agent/orchestrator/tools/builtin/path-locks.mjs +35 -0
- package/src/agent/orchestrator/tools/builtin/path-utils.mjs +201 -0
- package/src/agent/orchestrator/tools/builtin/read-args.mjs +103 -0
- package/src/agent/orchestrator/tools/builtin/read-batch.mjs +172 -0
- package/src/agent/orchestrator/tools/builtin/read-constants.mjs +40 -0
- package/src/agent/orchestrator/tools/builtin/read-formatting.mjs +118 -0
- package/src/agent/orchestrator/tools/builtin/read-image-resize.mjs +189 -0
- package/src/agent/orchestrator/tools/builtin/read-image.mjs +88 -0
- package/src/agent/orchestrator/tools/builtin/read-lines.mjs +12 -0
- package/src/agent/orchestrator/tools/builtin/read-mode-tool.mjs +455 -0
- package/src/agent/orchestrator/tools/builtin/read-open.mjs +190 -0
- package/src/agent/orchestrator/tools/builtin/read-range-index.mjs +271 -0
- package/src/agent/orchestrator/tools/builtin/read-ranges.mjs +26 -0
- package/src/agent/orchestrator/tools/builtin/read-single-tool.mjs +728 -0
- package/src/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +173 -0
- package/src/agent/orchestrator/tools/builtin/read-special-files.mjs +268 -0
- package/src/agent/orchestrator/tools/builtin/read-streaming.mjs +602 -0
- package/src/agent/orchestrator/tools/builtin/read-tool.mjs +530 -0
- package/src/agent/orchestrator/tools/builtin/read-windows.mjs +107 -0
- package/src/agent/orchestrator/tools/builtin/rename-tool.mjs +196 -0
- package/src/agent/orchestrator/tools/builtin/rg-runner.mjs +422 -0
- package/src/agent/orchestrator/tools/builtin/search-builders.mjs +158 -0
- package/src/agent/orchestrator/tools/builtin/search-tool.mjs +869 -0
- package/src/agent/orchestrator/tools/builtin/shell-analysis.mjs +653 -0
- package/src/agent/orchestrator/tools/builtin/shell-jobs.mjs +936 -0
- package/src/agent/orchestrator/tools/builtin/shell-output.mjs +36 -0
- package/src/agent/orchestrator/tools/builtin/shell-runtime.mjs +214 -0
- package/src/agent/orchestrator/tools/builtin/snapshot-helpers.mjs +143 -0
- package/src/agent/orchestrator/tools/builtin/snapshot-store.mjs +206 -0
- package/src/agent/orchestrator/tools/builtin/snapshot-validation.mjs +98 -0
- package/src/agent/orchestrator/tools/builtin/text-stats.mjs +69 -0
- package/src/agent/orchestrator/tools/builtin/windows-roots.mjs +23 -0
- package/src/agent/orchestrator/tools/builtin/write-tool.mjs +401 -0
- package/src/agent/orchestrator/tools/builtin.mjs +500 -0
- package/src/agent/orchestrator/tools/code-graph-prewarm-worker.mjs +39 -0
- package/src/agent/orchestrator/tools/code-graph-tool-defs.mjs +24 -0
- package/src/agent/orchestrator/tools/code-graph.mjs +4095 -0
- package/src/agent/orchestrator/tools/cwd-tool.mjs +298 -0
- package/src/agent/orchestrator/tools/destructive-warning.mjs +323 -0
- package/src/agent/orchestrator/tools/edit-normalize.mjs +603 -0
- package/src/agent/orchestrator/tools/env-scrub.mjs +100 -0
- package/src/agent/orchestrator/tools/graph-binary-fetcher.mjs +144 -0
- package/src/agent/orchestrator/tools/graph-manifest.json +26 -0
- package/src/agent/orchestrator/tools/host-input.mjs +204 -0
- package/src/agent/orchestrator/tools/mutation-content-cache.mjs +67 -0
- package/src/agent/orchestrator/tools/mutation-planner.mjs +75 -0
- package/src/agent/orchestrator/tools/next-call-utils.mjs +48 -0
- package/src/agent/orchestrator/tools/patch-binary-fetcher.mjs +133 -0
- package/src/agent/orchestrator/tools/patch-manifest.json +26 -0
- package/src/agent/orchestrator/tools/patch-tool-defs.mjs +20 -0
- package/src/agent/orchestrator/tools/patch.mjs +2754 -0
- package/src/agent/orchestrator/tools/progress-message.mjs +118 -0
- package/src/agent/orchestrator/tools/result-compression.mjs +279 -0
- package/src/agent/orchestrator/tools/shell-command.mjs +865 -0
- package/src/agent/orchestrator/tools/shell-exec-policy.mjs +89 -0
- package/src/agent/orchestrator/tools/shell-policy-danger-target.mjs +27 -0
- package/src/agent/orchestrator/tools/shell-policy-imports.mjs +7 -0
- package/src/agent/orchestrator/tools/shell-policy.mjs +345 -0
- package/src/agent/orchestrator/tools/shell-snapshot.mjs +313 -0
- package/src/agent/orchestrator/workflow-store.mjs +93 -0
- package/src/agent/tool-defs.mjs +103 -0
- package/src/channels/backends/discord.mjs +784 -0
- package/src/channels/data/voice-runtime-manifest.json +138 -0
- package/src/channels/index.mjs +3229 -0
- package/src/channels/lib/cli-worker-host.mjs +12 -0
- package/src/channels/lib/config-lock.mjs +13 -0
- package/src/channels/lib/config.mjs +292 -0
- package/src/channels/lib/drop-trace.mjs +71 -0
- package/src/channels/lib/event-pipeline.mjs +81 -0
- package/src/channels/lib/event-queue.mjs +345 -0
- package/src/channels/lib/executor.mjs +168 -0
- package/src/channels/lib/format.mjs +188 -0
- package/src/channels/lib/holidays.mjs +138 -0
- package/src/channels/lib/hook-pipe-server.mjs +802 -0
- package/src/channels/lib/interaction-workflows.mjs +184 -0
- package/src/channels/lib/memory-client.mjs +149 -0
- package/src/channels/lib/output-forwarder.mjs +765 -0
- package/src/channels/lib/runtime-paths.mjs +479 -0
- package/src/channels/lib/scheduler.mjs +723 -0
- package/src/channels/lib/session-control.mjs +36 -0
- package/src/channels/lib/session-discovery.mjs +103 -0
- package/src/channels/lib/settings.mjs +11 -0
- package/src/channels/lib/state-file.mjs +68 -0
- package/src/channels/lib/status-snapshot.mjs +219 -0
- package/src/channels/lib/tool-format.mjs +140 -0
- package/src/channels/lib/transcript-discovery.mjs +195 -0
- package/src/channels/lib/voice-runtime-fetcher.mjs +734 -0
- package/src/channels/lib/webhook.mjs +1179 -0
- package/src/channels/lib/whisper-server.mjs +477 -0
- package/src/channels/tool-defs.mjs +170 -0
- package/src/daemon/host.mjs +118 -0
- package/src/daemon/mcp-transport.mjs +47 -0
- package/src/daemon/session.mjs +100 -0
- package/src/daemon/thin-client.mjs +71 -0
- package/src/daemon/transport.mjs +163 -0
- package/src/memory/data/runtime-manifest.json +40 -0
- package/src/memory/index.mjs +3305 -0
- package/src/memory/lib/agent-ipc.mjs +93 -0
- package/src/memory/lib/bridge-trace-queries.mjs +120 -0
- package/src/memory/lib/core-memory-store.mjs +330 -0
- package/src/memory/lib/embedding-provider.mjs +269 -0
- package/src/memory/lib/embedding-worker.mjs +323 -0
- package/src/memory/lib/llm-worker-host.mjs +17 -0
- package/src/memory/lib/memory-cycle.mjs +11 -0
- package/src/memory/lib/memory-cycle1.mjs +641 -0
- package/src/memory/lib/memory-cycle2.mjs +1284 -0
- package/src/memory/lib/memory-cycle3.mjs +540 -0
- package/src/memory/lib/memory-embed.mjs +299 -0
- package/src/memory/lib/memory-extraction.mjs +5 -0
- package/src/memory/lib/memory-maintenance-store.mjs +32 -0
- package/src/memory/lib/memory-ops-policy.mjs +190 -0
- package/src/memory/lib/memory-recall-id-patch.mjs +15 -0
- package/src/memory/lib/memory-recall-read-query.mjs +7 -0
- package/src/memory/lib/memory-recall-scope-filter.mjs +63 -0
- package/src/memory/lib/memory-recall-store.mjs +621 -0
- package/src/memory/lib/memory-retrievers.mjs +112 -0
- package/src/memory/lib/memory-score.mjs +71 -0
- package/src/memory/lib/memory-text-utils.mjs +58 -0
- package/src/memory/lib/memory.mjs +412 -0
- package/src/memory/lib/model-profile.mjs +85 -0
- package/src/memory/lib/pg/adapter.mjs +308 -0
- package/src/memory/lib/pg/process.mjs +360 -0
- package/src/memory/lib/pg/supervisor.mjs +396 -0
- package/src/memory/lib/project-id-resolver.mjs +86 -0
- package/src/memory/lib/runtime-fetcher.mjs +442 -0
- package/src/memory/lib/trace-store.mjs +728 -0
- package/src/memory/tool-defs.mjs +79 -0
- package/src/search/index.mjs +1173 -0
- package/src/search/lib/backends/anthropic-oauth.mjs +98 -0
- package/src/search/lib/backends/exa.mjs +50 -0
- package/src/search/lib/backends/firecrawl.mjs +61 -0
- package/src/search/lib/backends/gemini-api.mjs +83 -0
- package/src/search/lib/backends/grok-oauth.mjs +86 -0
- package/src/search/lib/backends/index.mjs +150 -0
- package/src/search/lib/backends/openai-api.mjs +144 -0
- package/src/search/lib/backends/openai-oauth.mjs +98 -0
- package/src/search/lib/backends/openai-web-search.mjs +76 -0
- package/src/search/lib/backends/tavily.mjs +55 -0
- package/src/search/lib/backends/xai-api.mjs +113 -0
- package/src/search/lib/cache.mjs +131 -0
- package/src/search/lib/config.mjs +192 -0
- package/src/search/lib/formatter.mjs +115 -0
- package/src/search/lib/provider-usage.mjs +67 -0
- package/src/search/lib/providers.mjs +47 -0
- package/src/search/lib/search-intent.mjs +109 -0
- package/src/search/lib/setup-handler.mjs +261 -0
- package/src/search/lib/state.mjs +201 -0
- package/src/search/lib/web-tools.mjs +1207 -0
- package/src/search/tool-defs.mjs +83 -0
- package/src/setup/defender-exclusion.mjs +183 -0
- package/src/shared/abort-controller.mjs +15 -0
- package/src/shared/atomic-file.mjs +420 -0
- package/src/shared/config.mjs +350 -0
- package/src/shared/daemon-recycle.mjs +108 -0
- package/src/shared/disable-claude-builtins.mjs +88 -0
- package/src/shared/err-text.mjs +12 -0
- package/src/shared/llm/cost.mjs +66 -0
- package/src/shared/llm/http-agent.mjs +123 -0
- package/src/shared/llm/index.mjs +41 -0
- package/src/shared/llm/pid-cleanup.mjs +27 -0
- package/src/shared/llm/usage-log.mjs +47 -0
- package/src/shared/plugin-paths.mjs +58 -0
- package/src/shared/schedules-store.mjs +70 -0
- package/src/shared/seed.mjs +119 -0
- package/src/shared/user-cwd.mjs +213 -0
- package/src/shared/user-data-guard.mjs +238 -0
- package/src/status/aggregator.mjs +584 -0
- package/src/status/server.mjs +413 -0
- package/tools.json +1653 -0
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
// bash_session — persistent shell with state preserved across calls.
|
|
2
|
+
//
|
|
3
|
+
// Companion to the stateless `bash` tool. The default `bash` spawns a fresh
|
|
4
|
+
// subshell every call: cwd, exports, `source`d virtualenvs, shell functions
|
|
5
|
+
// all vanish between invocations. That's the safe default but it's clumsy
|
|
6
|
+
// for the common "cd into a project → activate venv → run three commands"
|
|
7
|
+
// workflow; each step has to reconstruct the prior shell context by hand.
|
|
8
|
+
//
|
|
9
|
+
// bash_session keeps a long-lived `bash` child process per session_id. The
|
|
10
|
+
// caller writes commands to stdin; we frame each command with a sentinel
|
|
11
|
+
// so we know when the command has finished and what its exit code was.
|
|
12
|
+
// State carried automatically: $PWD, exports, shell vars, readline history
|
|
13
|
+
// (not that we use it), aliases, function defs, `source`d files.
|
|
14
|
+
//
|
|
15
|
+
// Session lifecycle:
|
|
16
|
+
// - session_id omitted → mint a fresh id, spawn child, run command
|
|
17
|
+
// - session_id matches pool → reuse existing child
|
|
18
|
+
// - session_id misses pool → spawn child for that id (pool empty after
|
|
19
|
+
// orchestrator restart or idle eviction)
|
|
20
|
+
// - close:true → terminate child after command returns
|
|
21
|
+
// - idle > IDLE_TIMEOUT_MS → reaper removes & kills the child
|
|
22
|
+
// - pool > MAX_SESSIONS → oldest-idle evicted at spawn time
|
|
23
|
+
//
|
|
24
|
+
// Output protocol:
|
|
25
|
+
// write: <command>\necho "__MIXDOG_END__:$?"\n
|
|
26
|
+
// read: everything on stdout up to (not including) the marker line
|
|
27
|
+
// exit: the N in __MIXDOG_END__:N
|
|
28
|
+
// stderr: captured in parallel; sentinel not echoed there, so we flush
|
|
29
|
+
// whatever arrived up to the command's completion. Small
|
|
30
|
+
// quiescence window (STDERR_DRAIN_MS) after the stdout marker
|
|
31
|
+
// so trailing writes on stderr don't get cut off.
|
|
32
|
+
//
|
|
33
|
+
// Safety: same BLOCKED_PATTERNS as the `bash` tool. The session holds state
|
|
34
|
+
// so a dangerous command can't hide in an earlier turn (we scan per call).
|
|
35
|
+
// Same ANSI strip + smart middle-truncate applied to stdout/stderr.
|
|
36
|
+
//
|
|
37
|
+
// This tool takes a command, not a file path — no path-safety check applies.
|
|
38
|
+
|
|
39
|
+
import { spawn } from 'node:child_process';
|
|
40
|
+
import * as nodeUtil from 'node:util';
|
|
41
|
+
import { existsSync } from 'node:fs';
|
|
42
|
+
import { randomUUID } from 'node:crypto';
|
|
43
|
+
import { invalidateBuiltinResultCache, analyzeShellCommandEffects, preflightShellLargeFileProbe } from './builtin.mjs';
|
|
44
|
+
import { markCodeGraphDirtyPaths, drainCodeGraphCache } from './code-graph.mjs';
|
|
45
|
+
import { isBlockedCommand, maybeRewriteWmicProcessCommand } from './shell-policy.mjs';
|
|
46
|
+
import { stripQuotedAndHeredoc, extractShellCInner } from './destructive-warning.mjs';
|
|
47
|
+
import { _maybeEncodePowerShellCommand } from './shell-command.mjs';
|
|
48
|
+
import { _captureTrackedMtimes, _trackedDriftNoteAfter, _injectionBlockTargets, getDedupedDestructiveWarnings } from './builtin/bash-tool.mjs';
|
|
49
|
+
import { scrubLoaderVars, scrubProviderSecrets } from './env-scrub.mjs';
|
|
50
|
+
|
|
51
|
+
// Default 600 s (10 min), max 1800 s. Aligned with the one-shot bash tool's
|
|
52
|
+
// 600 s default (builtin/bash-tool.mjs); the persistent shell carries
|
|
53
|
+
// longer-running pipelines (build/test/install) than one-shot calls, so a
|
|
54
|
+
// 120 s default left those workflows timing out at 2 min. Per-call `timeout`
|
|
55
|
+
// still overrides, capped at MAX_TIMEOUT_MS.
|
|
56
|
+
const DEFAULT_TIMEOUT_MS = 600_000;
|
|
57
|
+
const MAX_TIMEOUT_MS = 1_800_000;
|
|
58
|
+
const IDLE_TIMEOUT_MS = 5 * 60_000;
|
|
59
|
+
const MAX_SESSIONS = 10;
|
|
60
|
+
const STDERR_DRAIN_MS = 25;
|
|
61
|
+
const STDERR_DRAIN_MAX_MS = 250;
|
|
62
|
+
const STDERR_QUIESCENT_MS = 25;
|
|
63
|
+
// SHELL_OUTPUT_MAX_CHARS — output preview cap, matches the `bash` tool.
|
|
64
|
+
// Duplicated here so bash-session stays decoupled from builtin.mjs private constants.
|
|
65
|
+
const SHELL_OUTPUT_MAX_CHARS = 30_000;
|
|
66
|
+
// STREAM_BUF_BYTE_CAP — hard byte cap per in-memory stream buffer. Past the
|
|
67
|
+
// cap data is dropped and a truncation marker injected so a runaway command
|
|
68
|
+
// (e.g. `cat /dev/urandom`) can't OOM the orchestrator.
|
|
69
|
+
const STREAM_BUF_BYTE_CAP = 4 * 1024 * 1024; // 4 MB per stream
|
|
70
|
+
const STREAM_TRUNC_MARKER = '\n... [TRUNCATED — stream cap reached] ...\n';
|
|
71
|
+
// Output truncation runtime envelope: 400 lines / 30 KB total;
|
|
72
|
+
// head=80 + tail=80 lines preserved on middle-truncation.
|
|
73
|
+
const SMART_BASH_MAX_LINES = 400;
|
|
74
|
+
const SMART_BASH_MAX_BYTES = 30 * 1024;
|
|
75
|
+
const SMART_BASH_HEAD_LINES = 80;
|
|
76
|
+
const SMART_BASH_TAIL_LINES = 80;
|
|
77
|
+
|
|
78
|
+
// Marker prefix for per-command sentinels. A random suffix is added on each
|
|
79
|
+
// command so user output that happens to contain the static prefix does not
|
|
80
|
+
// terminate the command early.
|
|
81
|
+
const MARKER_PREFIX = '__MIXDOG_END__';
|
|
82
|
+
|
|
83
|
+
// --- ANSI strip (self-contained; mirrors builtin.mjs's implementation) ---
|
|
84
|
+
const _ANSI_REGEX = /\u001B(?:\[[0-?]*[ -/]*[@-~]|\][\s\S]*?(?:\u0007|\u001B\\|\u009C))/g;
|
|
85
|
+
const _stripAnsi = typeof nodeUtil.stripVTControlCharacters === 'function'
|
|
86
|
+
? (s) => nodeUtil.stripVTControlCharacters(s)
|
|
87
|
+
: (s) => s.replace(_ANSI_REGEX, () => '');
|
|
88
|
+
function stripAnsi(s) {
|
|
89
|
+
if (typeof s !== 'string' || s.length === 0) return s;
|
|
90
|
+
return _stripAnsi(s);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function escapeRegex(s) {
|
|
94
|
+
return String(s).replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// --- Smart middle-truncate (shared with bash tool) ---
|
|
98
|
+
function smartMiddleTruncate(content) {
|
|
99
|
+
const s = typeof content === 'string' ? content : String(content ?? '');
|
|
100
|
+
if (s.length <= SMART_BASH_MAX_BYTES) {
|
|
101
|
+
const fastLines = s.split('\n');
|
|
102
|
+
if (fastLines.length <= SMART_BASH_MAX_LINES) return s;
|
|
103
|
+
const head = fastLines.slice(0, SMART_BASH_HEAD_LINES).join('\n');
|
|
104
|
+
const tail = fastLines.slice(-SMART_BASH_TAIL_LINES).join('\n');
|
|
105
|
+
const middle = fastLines.length - SMART_BASH_HEAD_LINES - SMART_BASH_TAIL_LINES;
|
|
106
|
+
return `${head}\n\n... [TRUNCATED — ${middle} lines middle elided; total ${fastLines.length} lines. Rerun with tighter filters for more] ...\n\n${tail}`;
|
|
107
|
+
}
|
|
108
|
+
const lines = s.split('\n');
|
|
109
|
+
if (lines.length <= SMART_BASH_MAX_LINES) {
|
|
110
|
+
const head = s.slice(0, SMART_BASH_MAX_BYTES);
|
|
111
|
+
return `${head}\n\n... [TRUNCATED — output exceeded ${Math.round(SMART_BASH_MAX_BYTES / 1024)} KB on a single line] ...`;
|
|
112
|
+
}
|
|
113
|
+
const head = lines.slice(0, SMART_BASH_HEAD_LINES).join('\n');
|
|
114
|
+
const tail = lines.slice(-SMART_BASH_TAIL_LINES).join('\n');
|
|
115
|
+
const middle = lines.length - SMART_BASH_HEAD_LINES - SMART_BASH_TAIL_LINES;
|
|
116
|
+
const totalKb = Math.round(s.length / 1024);
|
|
117
|
+
return `${head}\n\n... [TRUNCATED — ${middle} lines middle elided; total ${lines.length} lines / ${totalKb} KB. Rerun with tighter filters for more] ...\n\n${tail}`;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function _prependDestructiveWarning(command, text) {
|
|
121
|
+
const warnings = getDedupedDestructiveWarnings(command);
|
|
122
|
+
if (!warnings.length) return text;
|
|
123
|
+
return `${warnings.map((w) => `⚠️ ${w}`).join('\n')}\n${text}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// Blocked command check delegated to shell-policy.mjs (shared with
|
|
127
|
+
// builtin.mjs). See that module for the full pattern set and rationale.
|
|
128
|
+
// Locate a usable bash binary on POSIX. Windows intentionally does not
|
|
129
|
+
// support persistent shell sessions; one-shot commands use PowerShell
|
|
130
|
+
// through builtin/bash-tool.mjs. We deliberately pin bash (not sh) since
|
|
131
|
+
// the feature set depended on by the sentinel echo and `$?` is bash-shaped.
|
|
132
|
+
function resolveBash() {
|
|
133
|
+
if (process.platform === 'win32') {
|
|
134
|
+
throw new Error('persistent shell sessions are not supported on Windows; use one-shot PowerShell commands');
|
|
135
|
+
}
|
|
136
|
+
if (existsSync('/bin/bash')) return '/bin/bash';
|
|
137
|
+
if (existsSync('/usr/bin/bash')) return '/usr/bin/bash';
|
|
138
|
+
return '/bin/sh'; // fallback; `$?` + echo still work
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// --- Session pool ---
|
|
142
|
+
// Map<id, { proc, lastUsed, stdoutBuf, stderrBuf, busy }>
|
|
143
|
+
const _sessions = new Map();
|
|
144
|
+
let _reaperTimer = null;
|
|
145
|
+
// R17 parent-exit hook installed exactly once at first _spawnSession. Without
|
|
146
|
+
// it, persistent bash shells orphan on server-main death (the async
|
|
147
|
+
// 'process-exit' path at the bottom of this module never gets to run its
|
|
148
|
+
// awaits when the host dies abruptly). Sync iteration of _sessions + direct
|
|
149
|
+
// _killProcessTree (sync taskkill on win, sync process.kill on posix).
|
|
150
|
+
let _parentExitInstalled = false;
|
|
151
|
+
function _installParentExitHook() {
|
|
152
|
+
if (_parentExitInstalled) return;
|
|
153
|
+
_parentExitInstalled = true;
|
|
154
|
+
const sweep = () => {
|
|
155
|
+
for (const [, s] of _sessions) {
|
|
156
|
+
try { _killProcessTree(s.proc); } catch { /* ignore */ }
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
try { process.on('exit', sweep); } catch { /* ignore */ }
|
|
160
|
+
try { process.on('SIGTERM', sweep); } catch { /* ignore */ }
|
|
161
|
+
try { process.on('SIGINT', sweep); } catch { /* ignore */ }
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
function _startReaper() {
|
|
165
|
+
if (_reaperTimer) return;
|
|
166
|
+
_reaperTimer = setInterval(() => {
|
|
167
|
+
const now = Date.now();
|
|
168
|
+
for (const [id, s] of _sessions) {
|
|
169
|
+
if (!s.busy && now - s.lastUsed > IDLE_TIMEOUT_MS) {
|
|
170
|
+
_killSession(id, 'idle-timeout');
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
if (_sessions.size === 0) {
|
|
174
|
+
clearInterval(_reaperTimer);
|
|
175
|
+
_reaperTimer = null;
|
|
176
|
+
}
|
|
177
|
+
}, 30_000);
|
|
178
|
+
// Don't keep the event loop alive just for the reaper.
|
|
179
|
+
if (typeof _reaperTimer.unref === 'function') _reaperTimer.unref();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Kill a spawned shell along with any child processes it forked. Posix path
|
|
183
|
+
// signals the process group (pgid == pid because we spawn with detached:true),
|
|
184
|
+
// so `sleep 1000 &` or a node server started inside the session is reaped
|
|
185
|
+
// instead of being left orphaned holding pipes open. Windows uses taskkill
|
|
186
|
+
// /T /F to walk the process tree. SIGTERM is sent first so well-behaved
|
|
187
|
+
// children can shut down cleanly; a SIGKILL escalation timer (3 s) forces the
|
|
188
|
+
// issue if they don't. Safe to call multiple times — all errors swallowed.
|
|
189
|
+
function _killProcessTree(proc) {
|
|
190
|
+
// proc.killed flips to true the moment a signal is *sent*, NOT when the
|
|
191
|
+
// child actually exits — escalation off `proc.killed` was a no-op. We
|
|
192
|
+
// instead track the real exit/close state via a flag and only escalate
|
|
193
|
+
// when neither has fired by the timer deadline.
|
|
194
|
+
if (!proc) return;
|
|
195
|
+
const pid = proc.pid;
|
|
196
|
+
if (!pid) return;
|
|
197
|
+
let exited = false;
|
|
198
|
+
const onDone = () => { exited = true; };
|
|
199
|
+
try { proc.once('exit', onDone); } catch {}
|
|
200
|
+
try { proc.once('close', onDone); } catch {}
|
|
201
|
+
// Already dead from a prior call? Skip the SIGTERM but still let the
|
|
202
|
+
// listener wiring above clean up if a future exit/close arrives.
|
|
203
|
+
if (proc.exitCode !== null && proc.exitCode !== undefined) {
|
|
204
|
+
exited = true;
|
|
205
|
+
} else if (proc.signalCode) {
|
|
206
|
+
exited = true;
|
|
207
|
+
}
|
|
208
|
+
try {
|
|
209
|
+
if (process.platform === 'win32') {
|
|
210
|
+
// /T walks the tree, /F forces — no graceful SIGTERM on win.
|
|
211
|
+
spawn('taskkill', ['/pid', String(pid), '/t', '/f'], { windowsHide: true, stdio: 'ignore' });
|
|
212
|
+
} else if (!exited) {
|
|
213
|
+
try { process.kill(-pid, 'SIGTERM'); }
|
|
214
|
+
catch { try { proc.kill('SIGTERM'); } catch { /* ignore */ } }
|
|
215
|
+
}
|
|
216
|
+
} catch { /* ignore */ }
|
|
217
|
+
// Escalate to SIGKILL if the child hasn't actually exited by the
|
|
218
|
+
// deadline. Windows taskkill /F is already forceful, so only posix
|
|
219
|
+
// needs the escalation timer.
|
|
220
|
+
if (process.platform !== 'win32') {
|
|
221
|
+
const esc = setTimeout(() => {
|
|
222
|
+
if (exited) return;
|
|
223
|
+
try { process.kill(-pid, 'SIGKILL'); }
|
|
224
|
+
catch { try { proc.kill('SIGKILL'); } catch { /* ignore */ } }
|
|
225
|
+
}, 3000);
|
|
226
|
+
if (typeof esc.unref === 'function') esc.unref();
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
async function _killSession(id, _reason) {
|
|
231
|
+
const s = _sessions.get(id);
|
|
232
|
+
if (!s) return;
|
|
233
|
+
_sessions.delete(id);
|
|
234
|
+
const exited = new Promise((resolve) => {
|
|
235
|
+
s.proc.once('exit', resolve);
|
|
236
|
+
s.proc.once('close', resolve);
|
|
237
|
+
});
|
|
238
|
+
try { s.proc.stdin?.end(); } catch { /* ignore */ }
|
|
239
|
+
_killProcessTree(s.proc);
|
|
240
|
+
await Promise.race([exited, new Promise((r) => setTimeout(r, 3000))]);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function shellQuoteSingle(s) {
|
|
244
|
+
return `'${String(s).replace(/'/g, `'\"'\"'`)}'`;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
function _hostPathToShellPath(p) {
|
|
248
|
+
let s = String(p || '').replace(/\\/g, '/');
|
|
249
|
+
return s;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
function _shellPwdToHostPath(p) {
|
|
253
|
+
return String(p || '').trim();
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
function _cwdKey(p) {
|
|
257
|
+
let s = _hostPathToShellPath(p).replace(/\/+$/, '');
|
|
258
|
+
if (process.platform === 'win32') s = s.toLowerCase();
|
|
259
|
+
return s || '/';
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
async function _ensureSessionCwd(entry, targetCwd, timeoutMs) {
|
|
263
|
+
if (!targetCwd || _cwdKey(entry.cwd) === _cwdKey(targetCwd)) return null;
|
|
264
|
+
const cdTarget = _hostPathToShellPath(targetCwd);
|
|
265
|
+
const result = await _runCommand(entry, `cd ${shellQuoteSingle(cdTarget)}`, Math.min(timeoutMs, 5000));
|
|
266
|
+
if (result?.exit_code !== 0) {
|
|
267
|
+
const stderr = stripAnsi(result?.stderr || '').trim();
|
|
268
|
+
return `Error: failed to set persistent cwd: ${targetCwd}${stderr ? `\n\n[stderr]\n${stderr}` : ''}`;
|
|
269
|
+
}
|
|
270
|
+
entry.cwd = targetCwd;
|
|
271
|
+
return null;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function _evictOldestIfFull() {
|
|
275
|
+
if (_sessions.size < MAX_SESSIONS) return;
|
|
276
|
+
// Prefer an idle session. If all are busy we can't evict safely; throw.
|
|
277
|
+
let oldestId = null;
|
|
278
|
+
let oldestTs = Infinity;
|
|
279
|
+
for (const [id, s] of _sessions) {
|
|
280
|
+
if (s.busy) continue;
|
|
281
|
+
if (s.lastUsed < oldestTs) {
|
|
282
|
+
oldestTs = s.lastUsed;
|
|
283
|
+
oldestId = id;
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
if (oldestId) {
|
|
287
|
+
_killSession(oldestId, 'pool-full');
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
throw new Error(`bash_session pool full (${MAX_SESSIONS} concurrent sessions, all busy)`);
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Build the env handed to the child bash. This path is POSIX-only; Windows
|
|
294
|
+
// one-shot commands use PowerShell and persistent shell sessions are disabled.
|
|
295
|
+
function buildBashEnv() {
|
|
296
|
+
const env = { ...process.env, LANG: 'C.UTF-8', LC_ALL: 'C.UTF-8' };
|
|
297
|
+
// R5 secret scrub — strip provider/cloud tokens before handing env to
|
|
298
|
+
// the persistent shell. The stateless `bash` tool applies this same
|
|
299
|
+
// sweep in builtin/bash-tool.mjs; the persistent shell's env is
|
|
300
|
+
// constructed here and returns before that site runs, so it must be
|
|
301
|
+
// done independently. Shared with shell-jobs and shell-snapshot via
|
|
302
|
+
// env-scrub.mjs so the prefix/suffix lists never drift.
|
|
303
|
+
scrubProviderSecrets(env);
|
|
304
|
+
// R11 loader/execution scrub (NODE_OPTIONS, LD_PRELOAD, DYLD_*, …).
|
|
305
|
+
scrubLoaderVars(env);
|
|
306
|
+
return env;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function _spawnSession(id, initialCwd = process.cwd()) {
|
|
310
|
+
_installParentExitHook();
|
|
311
|
+
_evictOldestIfFull();
|
|
312
|
+
const shell = resolveBash();
|
|
313
|
+
const proc = spawn(shell, [], {
|
|
314
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
315
|
+
env: buildBashEnv(),
|
|
316
|
+
cwd: initialCwd,
|
|
317
|
+
windowsHide: true,
|
|
318
|
+
// detached:true on posix gives the child its own process group so
|
|
319
|
+
// _killProcessTree can signal the whole group (catches `sleep 1000 &`
|
|
320
|
+
// and similar backgrounded children). Skipped on win32 where
|
|
321
|
+
// detached has different semantics (no console attached, used for
|
|
322
|
+
// daemonization — unwanted here).
|
|
323
|
+
detached: process.platform !== 'win32',
|
|
324
|
+
});
|
|
325
|
+
proc.stdout.setEncoding('utf-8');
|
|
326
|
+
proc.stderr.setEncoding('utf-8');
|
|
327
|
+
const entry = {
|
|
328
|
+
proc,
|
|
329
|
+
lastUsed: Date.now(),
|
|
330
|
+
cwd: initialCwd,
|
|
331
|
+
stdoutBuf: '',
|
|
332
|
+
stderrBuf: '',
|
|
333
|
+
busy: false,
|
|
334
|
+
dead: false,
|
|
335
|
+
exitInfo: null,
|
|
336
|
+
};
|
|
337
|
+
// Hard-capped concat: past STREAM_BUF_BYTE_CAP we drop further
|
|
338
|
+
// chunks and stamp a truncation marker once. Without this a runaway
|
|
339
|
+
// command (e.g. `cat /dev/urandom`) blocks until OOM long before any
|
|
340
|
+
// smartMiddleTruncate downstream gets a chance to trim.
|
|
341
|
+
entry.stdoutCapped = false;
|
|
342
|
+
entry.stderrCapped = false;
|
|
343
|
+
proc.stdout.on('data', (chunk) => {
|
|
344
|
+
if (entry.stdoutCapped) return;
|
|
345
|
+
entry.stdoutBuf += chunk;
|
|
346
|
+
if (entry.stdoutBuf.length >= STREAM_BUF_BYTE_CAP) {
|
|
347
|
+
entry.stdoutBuf = entry.stdoutBuf.slice(0, STREAM_BUF_BYTE_CAP) + STREAM_TRUNC_MARKER;
|
|
348
|
+
entry.stdoutCapped = true;
|
|
349
|
+
}
|
|
350
|
+
});
|
|
351
|
+
proc.stderr.on('data', (chunk) => {
|
|
352
|
+
if (entry.stderrCapped) return;
|
|
353
|
+
entry.stderrBuf += chunk;
|
|
354
|
+
if (entry.stderrBuf.length >= STREAM_BUF_BYTE_CAP) {
|
|
355
|
+
entry.stderrBuf = entry.stderrBuf.slice(0, STREAM_BUF_BYTE_CAP) + STREAM_TRUNC_MARKER;
|
|
356
|
+
entry.stderrCapped = true;
|
|
357
|
+
}
|
|
358
|
+
});
|
|
359
|
+
proc.on('error', (err) => {
|
|
360
|
+
entry.dead = true;
|
|
361
|
+
entry.exitInfo = { error: err?.message || String(err) };
|
|
362
|
+
});
|
|
363
|
+
proc.on('exit', (code, signal) => {
|
|
364
|
+
entry.dead = true;
|
|
365
|
+
entry.exitInfo = { code, signal };
|
|
366
|
+
_sessions.delete(id);
|
|
367
|
+
});
|
|
368
|
+
_sessions.set(id, entry);
|
|
369
|
+
_startReaper();
|
|
370
|
+
return entry;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
function _getOrCreate(sessionId, initialCwd = process.cwd(), opts = {}) {
|
|
374
|
+
const explicit = typeof sessionId === 'string' && sessionId.length > 0;
|
|
375
|
+
const id = explicit ? sessionId : `sess_${randomUUID()}`;
|
|
376
|
+
let entry = _sessions.get(id);
|
|
377
|
+
if (entry && entry.dead) {
|
|
378
|
+
_sessions.delete(id);
|
|
379
|
+
entry = null;
|
|
380
|
+
}
|
|
381
|
+
if (!entry) {
|
|
382
|
+
if (explicit && opts.create !== true) {
|
|
383
|
+
return { error: `Error: unknown session_id "${id}" (pass create:true to start a new persistent session)` };
|
|
384
|
+
}
|
|
385
|
+
entry = _spawnSession(id, initialCwd);
|
|
386
|
+
}
|
|
387
|
+
return { id, entry };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Core command-run: frame with sentinel, wait for marker on stdout, flush
|
|
391
|
+
// stderr with a small drain window, return { stdout, stderr, exit_code }.
|
|
392
|
+
function _runCommand(entry, command, timeoutMs, abortSignal = null) {
|
|
393
|
+
return new Promise((resolve, reject) => {
|
|
394
|
+
entry.busy = true;
|
|
395
|
+
// Reset buffers for this command. Anything left from a prior run is
|
|
396
|
+
// unexpected (we only return after the marker), but be defensive.
|
|
397
|
+
entry.stdoutBuf = '';
|
|
398
|
+
entry.stderrBuf = '';
|
|
399
|
+
|
|
400
|
+
let finished = false;
|
|
401
|
+
let timeoutHandle = null;
|
|
402
|
+
let abortHandler = null;
|
|
403
|
+
const marker = `${MARKER_PREFIX}:${randomUUID()}`;
|
|
404
|
+
const markerRe = new RegExp(`^${escapeRegex(marker)}:(-?\\d+)\\s*$`, 'm');
|
|
405
|
+
|
|
406
|
+
const onExit = () => {
|
|
407
|
+
if (finished) return;
|
|
408
|
+
fail(new Error('bash_session: shell exited before command completed'));
|
|
409
|
+
};
|
|
410
|
+
|
|
411
|
+
const cleanup = () => {
|
|
412
|
+
finished = true;
|
|
413
|
+
entry.busy = false;
|
|
414
|
+
entry.lastUsed = Date.now();
|
|
415
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
416
|
+
if (abortSignal && abortHandler) {
|
|
417
|
+
try { abortSignal.removeEventListener('abort', abortHandler); } catch {}
|
|
418
|
+
}
|
|
419
|
+
entry.proc.stdout.removeListener('data', onStdout);
|
|
420
|
+
entry.proc.removeListener('exit', onExit);
|
|
421
|
+
};
|
|
422
|
+
|
|
423
|
+
const settle = (result) => {
|
|
424
|
+
if (finished) return;
|
|
425
|
+
cleanup();
|
|
426
|
+
resolve(result);
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
const fail = (err) => {
|
|
430
|
+
if (finished) return;
|
|
431
|
+
cleanup();
|
|
432
|
+
reject(err);
|
|
433
|
+
};
|
|
434
|
+
|
|
435
|
+
const onStdout = () => {
|
|
436
|
+
const m = markerRe.exec(entry.stdoutBuf);
|
|
437
|
+
if (!m) return;
|
|
438
|
+
const exitCode = Number(m[1]);
|
|
439
|
+
// Everything before the marker line is the real stdout.
|
|
440
|
+
const before = entry.stdoutBuf.slice(0, m.index);
|
|
441
|
+
// Drain pending stderr writes adaptively. The fixed 25 ms
|
|
442
|
+
// window dropped late stderr from forked children that
|
|
443
|
+
// flushed slightly after the parent shell exited. Loop
|
|
444
|
+
// instead: poll the stderr buffer length and finish only
|
|
445
|
+
// once it's been quiescent for STDERR_QUIESCENT_MS, the
|
|
446
|
+
// child closed stderr, or we hit the absolute ceiling.
|
|
447
|
+
const drainStart = Date.now();
|
|
448
|
+
let lastLen = entry.stderrBuf.length;
|
|
449
|
+
let lastChange = drainStart;
|
|
450
|
+
let stderrClosed = false;
|
|
451
|
+
const onStderrEnd = () => { stderrClosed = true; };
|
|
452
|
+
try { entry.proc.stderr.once('end', onStderrEnd); } catch {}
|
|
453
|
+
const finish = () => {
|
|
454
|
+
try { entry.proc.stderr.removeListener('end', onStderrEnd); } catch {}
|
|
455
|
+
const stderr = entry.stderrBuf;
|
|
456
|
+
entry.stdoutBuf = '';
|
|
457
|
+
entry.stderrBuf = '';
|
|
458
|
+
settle({ stdout: before, stderr, exit_code: exitCode });
|
|
459
|
+
};
|
|
460
|
+
const tick = () => {
|
|
461
|
+
if (finished) { try { entry.proc.stderr.removeListener('end', onStderrEnd); } catch {} return; }
|
|
462
|
+
const now = Date.now();
|
|
463
|
+
const curLen = entry.stderrBuf.length;
|
|
464
|
+
if (curLen !== lastLen) {
|
|
465
|
+
lastLen = curLen;
|
|
466
|
+
lastChange = now;
|
|
467
|
+
}
|
|
468
|
+
if (stderrClosed) return finish();
|
|
469
|
+
if (now - drainStart >= STDERR_DRAIN_MAX_MS) return finish();
|
|
470
|
+
if (now - lastChange >= STDERR_QUIESCENT_MS) return finish();
|
|
471
|
+
setTimeout(tick, 10);
|
|
472
|
+
};
|
|
473
|
+
setTimeout(tick, STDERR_DRAIN_MS);
|
|
474
|
+
};
|
|
475
|
+
|
|
476
|
+
// Caller-driven abort (ESC / new prompt / session close). Kill the
|
|
477
|
+
// shell immediately and resolve with an aborted marker so the agent
|
|
478
|
+
// sees the cancellation rather than waiting for timeoutMs.
|
|
479
|
+
if (abortSignal) {
|
|
480
|
+
const fireAbort = () => {
|
|
481
|
+
if (finished) return;
|
|
482
|
+
const partialOut = entry.stdoutBuf || '';
|
|
483
|
+
const partialErr = entry.stderrBuf || '';
|
|
484
|
+
entry.stdoutBuf = '';
|
|
485
|
+
entry.stderrBuf = '';
|
|
486
|
+
_killProcessTree(entry.proc);
|
|
487
|
+
// Mark dead and remove from pool immediately so the next call
|
|
488
|
+
// doesn't pick up a killed shell entry.
|
|
489
|
+
entry.dead = true;
|
|
490
|
+
for (const [sid, s] of _sessions) { if (s === entry) { _sessions.delete(sid); break; } }
|
|
491
|
+
cleanup();
|
|
492
|
+
resolve({
|
|
493
|
+
stdout: partialOut,
|
|
494
|
+
stderr: partialErr,
|
|
495
|
+
exit_code: null,
|
|
496
|
+
signal: 'SIGTERM',
|
|
497
|
+
aborted: true,
|
|
498
|
+
});
|
|
499
|
+
};
|
|
500
|
+
if (abortSignal.aborted) { fireAbort(); return; }
|
|
501
|
+
abortHandler = fireAbort;
|
|
502
|
+
abortSignal.addEventListener('abort', abortHandler, { once: true });
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
entry.proc.stdout.on('data', onStdout);
|
|
506
|
+
// Check the buffer in case the marker already arrived (tiny commands).
|
|
507
|
+
onStdout();
|
|
508
|
+
|
|
509
|
+
entry.proc.on('exit', onExit);
|
|
510
|
+
|
|
511
|
+
timeoutHandle = setTimeout(() => {
|
|
512
|
+
// Timeout: surface what we have but don't leave the shell in a
|
|
513
|
+
// half-run state. Killing the process is the only reliable way
|
|
514
|
+
// to interrupt a stuck command; the caller can mint a new session.
|
|
515
|
+
const partialOut = entry.stdoutBuf;
|
|
516
|
+
const partialErr = entry.stderrBuf;
|
|
517
|
+
entry.stdoutBuf = '';
|
|
518
|
+
entry.stderrBuf = '';
|
|
519
|
+
_killProcessTree(entry.proc);
|
|
520
|
+
cleanup();
|
|
521
|
+
// Return a structured result (not a reject) so the caller
|
|
522
|
+
// renders a proper exit/stderr block instead of a bare Error.
|
|
523
|
+
resolve({
|
|
524
|
+
stdout: partialOut,
|
|
525
|
+
stderr: partialErr,
|
|
526
|
+
exit_code: null,
|
|
527
|
+
signal: 'SIGTERM',
|
|
528
|
+
timed_out: true,
|
|
529
|
+
timeout_ms: timeoutMs,
|
|
530
|
+
});
|
|
531
|
+
}, timeoutMs);
|
|
532
|
+
|
|
533
|
+
// Write the command + sentinel. Newline before `echo` in case the
|
|
534
|
+
// command didn't end with one. `$?` captures the final pipeline's
|
|
535
|
+
// exit status as of bash semantics.
|
|
536
|
+
const _preEncodePolicy = checkExecPolicyMessage(command);
|
|
537
|
+
if (_preEncodePolicy) {
|
|
538
|
+
fail(new Error(_preEncodePolicy.replace(/^Error: /, '')));
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
const encoded = _maybeEncodePowerShellCommand(command);
|
|
542
|
+
const payload = `${encoded}\necho "${marker}:$?"\n`;
|
|
543
|
+
try {
|
|
544
|
+
entry.proc.stdin.write(payload, 'utf-8');
|
|
545
|
+
} catch (err) {
|
|
546
|
+
fail(err);
|
|
547
|
+
}
|
|
548
|
+
});
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
async function _syncSessionCwd(entry, timeoutMs) {
|
|
552
|
+
try {
|
|
553
|
+
const result = await _runCommand(entry, 'pwd', Math.min(timeoutMs, 2000));
|
|
554
|
+
if (result?.exit_code !== 0) return;
|
|
555
|
+
const stdout = stripAnsi(result.stdout || '').trim();
|
|
556
|
+
if (!stdout) return;
|
|
557
|
+
const lines = stdout.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
558
|
+
if (lines.length > 0) entry.cwd = _shellPwdToHostPath(lines[lines.length - 1]);
|
|
559
|
+
} catch (err) {
|
|
560
|
+
entry.syncError = `[bash-session] persistent-session cwd sync failed: ${err.message}`;
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
async function bash_session(args, cwd = process.cwd(), opts = {}) {
|
|
565
|
+
if (process.platform === 'win32') {
|
|
566
|
+
return 'Error: persistent shell sessions are not supported on Windows; use one-shot PowerShell commands without persistent/session_id.';
|
|
567
|
+
}
|
|
568
|
+
const abortSignal = opts && opts.abortSignal ? opts.abortSignal : null;
|
|
569
|
+
let command = typeof args?.command === 'string' ? args.command : '';
|
|
570
|
+
const close = args?.close === true;
|
|
571
|
+
const implicitSessionId = args?.persistent === true && typeof opts?.sessionId === 'string'
|
|
572
|
+
? `__default__${opts.sessionId}`
|
|
573
|
+
: '';
|
|
574
|
+
const requestedSessionId = typeof args?.session_id === 'string' ? args.session_id : implicitSessionId;
|
|
575
|
+
if (!command && close) {
|
|
576
|
+
if (!requestedSessionId) return 'Error: command is required';
|
|
577
|
+
const existing = _sessions.get(requestedSessionId);
|
|
578
|
+
if (existing?.busy) return `Error: session "${requestedSessionId}" is busy executing a prior command`;
|
|
579
|
+
if (existing) await _killSession(requestedSessionId, 'close-requested');
|
|
580
|
+
return `[session: ${requestedSessionId}]\n[closed]\n\n${existing ? '(no output)' : '(no active session)'}`;
|
|
581
|
+
}
|
|
582
|
+
if (!command) return 'Error: command is required';
|
|
583
|
+
const wmicRewrite = maybeRewriteWmicProcessCommand(command);
|
|
584
|
+
if (wmicRewrite?.error) return `Error: ${wmicRewrite.error}`;
|
|
585
|
+
if (wmicRewrite?.command) command = wmicRewrite.command;
|
|
586
|
+
// R5-③: match the stateless one-shot path's full sweep so callers that
|
|
587
|
+
// reach bash_session directly (via session_id without going through
|
|
588
|
+
// executeBashTool) still get stripQuotedAndHeredoc + extractShellCInner
|
|
589
|
+
// + unquote-span coverage. Persistent:true callers funnel through
|
|
590
|
+
// executeBashTool which now pre-sweeps, but bash_session is also reached
|
|
591
|
+
// by close:true session reuse and by direct tool dispatch.
|
|
592
|
+
const _bsPolicy = checkExecPolicyMessage(command);
|
|
593
|
+
if (_bsPolicy) return _bsPolicy;
|
|
594
|
+
const explicitCwd = typeof args?.cwd === 'string' && args.cwd.trim().length > 0;
|
|
595
|
+
const requestedCwd = cwd || process.cwd();
|
|
596
|
+
const baseCwd = (() => {
|
|
597
|
+
if (explicitCwd) return requestedCwd;
|
|
598
|
+
if (requestedSessionId) {
|
|
599
|
+
const existing = _sessions.get(requestedSessionId);
|
|
600
|
+
if (existing?.cwd) return existing.cwd;
|
|
601
|
+
}
|
|
602
|
+
return requestedCwd;
|
|
603
|
+
})();
|
|
604
|
+
const largeProbe = await preflightShellLargeFileProbe(command, baseCwd);
|
|
605
|
+
if (largeProbe) return `Error: ${largeProbe.message}`;
|
|
606
|
+
const rawTimeout = typeof args?.timeout === 'number' ? args.timeout : DEFAULT_TIMEOUT_MS;
|
|
607
|
+
// Accept seconds OR milliseconds for ergonomics: values ≤ 600 are
|
|
608
|
+
// treated as seconds (matches the spec's "max 600s"); larger values
|
|
609
|
+
// are taken as ms. Cap either way.
|
|
610
|
+
const timeoutMs = rawTimeout <= 600 ? rawTimeout * 1000 : rawTimeout;
|
|
611
|
+
const effectiveTimeout = Math.min(Math.max(timeoutMs, 1000), wmicRewrite?.timeoutMs || MAX_TIMEOUT_MS);
|
|
612
|
+
const resolved = _getOrCreate(requestedSessionId || args?.session_id, baseCwd, { create: args?.create === true });
|
|
613
|
+
if (resolved.error) return resolved.error;
|
|
614
|
+
const { id, entry } = resolved;
|
|
615
|
+
if (entry.syncError) {
|
|
616
|
+
const msg = entry.syncError;
|
|
617
|
+
delete entry.syncError;
|
|
618
|
+
throw new Error(msg);
|
|
619
|
+
}
|
|
620
|
+
if (entry.busy) {
|
|
621
|
+
return `Error: session "${id}" is busy executing a prior command`;
|
|
622
|
+
}
|
|
623
|
+
if (explicitCwd) {
|
|
624
|
+
const cwdErr = await _ensureSessionCwd(entry, requestedCwd, effectiveTimeout);
|
|
625
|
+
if (cwdErr) return cwdErr;
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
let shellEffects;
|
|
629
|
+
try {
|
|
630
|
+
shellEffects = await analyzeShellCommandEffects(command, entry.cwd || baseCwd);
|
|
631
|
+
} catch (err) {
|
|
632
|
+
return `Error: ${err?.message || String(err)}`;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
const _bsScope = opts?.readStateScope ?? opts?.sessionId ?? null;
|
|
636
|
+
const _bsPreDrift = _captureTrackedMtimes(_bsScope);
|
|
637
|
+
let result;
|
|
638
|
+
try {
|
|
639
|
+
result = await _runCommand(entry, command, effectiveTimeout, abortSignal);
|
|
640
|
+
} catch (err) {
|
|
641
|
+
return `Error: ${err?.message || String(err)}`;
|
|
642
|
+
}
|
|
643
|
+
if (result.exit_code === 0 && shellEffects.finalCwd) {
|
|
644
|
+
entry.cwd = shellEffects.finalCwd;
|
|
645
|
+
}
|
|
646
|
+
// Skip cwd sync on abort: the shell was already killed, so issuing
|
|
647
|
+
// another `pwd` would either hang on a dead pipe or spawn a fresh
|
|
648
|
+
// session against caller intent. Mark dead so the next call mints a
|
|
649
|
+
// new shell rather than reusing this one. Same logic for timeouts.
|
|
650
|
+
if (result.aborted) {
|
|
651
|
+
entry.dead = true;
|
|
652
|
+
} else if (!close && !result.timed_out) {
|
|
653
|
+
await _syncSessionCwd(entry, effectiveTimeout);
|
|
654
|
+
}
|
|
655
|
+
if (shellEffects.mutationMode === 'paths') {
|
|
656
|
+
invalidateBuiltinResultCache(shellEffects.paths);
|
|
657
|
+
markCodeGraphDirtyPaths(shellEffects.paths);
|
|
658
|
+
} else if (shellEffects.mutationMode === 'global') {
|
|
659
|
+
invalidateBuiltinResultCache();
|
|
660
|
+
drainCodeGraphCache();
|
|
661
|
+
}
|
|
662
|
+
const _bsDriftNote = _trackedDriftNoteAfter(_bsScope, _bsPreDrift);
|
|
663
|
+
|
|
664
|
+
if (close) {
|
|
665
|
+
await _killSession(id, 'close-requested');
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
const stdoutClean = stripAnsi(result.stdout || '');
|
|
669
|
+
const stderrClean = stripAnsi(result.stderr || '');
|
|
670
|
+
const stdoutT = smartMiddleTruncate(stdoutClean);
|
|
671
|
+
const stderrT = stderrClean ? smartMiddleTruncate(stderrClean) : '';
|
|
672
|
+
|
|
673
|
+
// Structured header so the agent can parse session_id + exit_code out
|
|
674
|
+
// of the text response without bespoke JSON. Keeps parity with the
|
|
675
|
+
// `bash` tool's free-form `[exit code: N]` marker but additive.
|
|
676
|
+
const headerLines = [`[session: ${id}]`];
|
|
677
|
+
if (wmicRewrite?.note) headerLines.push(wmicRewrite.note);
|
|
678
|
+
if (result.aborted) {
|
|
679
|
+
headerLines.push(`[aborted: caller cancelled — session killed]`);
|
|
680
|
+
} else if (result.timed_out) {
|
|
681
|
+
headerLines.push(`[timeout: ${result.timeout_ms} ms — session killed]`);
|
|
682
|
+
} else if (result.exit_code !== 0 && result.exit_code !== null) {
|
|
683
|
+
headerLines.push(`[exit code: ${result.exit_code}]`);
|
|
684
|
+
}
|
|
685
|
+
if (close) headerLines.push(`[closed]`);
|
|
686
|
+
const header = headerLines.join('\n');
|
|
687
|
+
|
|
688
|
+
const body = stdoutT || (stderrT ? '' : '(no output)');
|
|
689
|
+
const stderrBlock = stderrT ? `\n\n[stderr]\n${stderrT}` : '';
|
|
690
|
+
return _prependDestructiveWarning(command, `${header}\n\n${body}${stderrBlock}${_bsDriftNote}`);
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
// BASH_SESSION_TOOL_DEFS removed in 0.1.126: the `bash` tool's
|
|
694
|
+
// `persistent:true` option absorbed every use case; the dedicated
|
|
695
|
+
// `bash_session` schema only added prompt bytes and triggered LLM
|
|
696
|
+
// hallucinations of the legacy name. Implementation (executeBashSessionTool,
|
|
697
|
+
// closeBashSession) stays — `bash` with persistent:true routes here.
|
|
698
|
+
export async function executeBashSessionTool(name, args, _cwd, opts = {}) {
|
|
699
|
+
switch (name) {
|
|
700
|
+
case 'bash_session':
|
|
701
|
+
return bash_session(args || {}, _cwd || process.cwd(), opts);
|
|
702
|
+
default:
|
|
703
|
+
throw new Error(`Unknown bash-session tool: ${name}`);
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
export function closeBashSession(sessionId, reason = 'external-close') {
|
|
708
|
+
if (!sessionId || !_sessions.has(sessionId)) return false;
|
|
709
|
+
_killSession(sessionId, reason);
|
|
710
|
+
return true;
|
|
711
|
+
}
|
|
712
|
+
|
|
713
|
+
// Best-effort cleanup on process exit so orphan bash children don't linger
|
|
714
|
+
// when the plugin host shuts down. drain-registry runs this on signal exit
|
|
715
|
+
// paths; the bare 'exit' hook stays as an idempotent backup.
|
|
716
|
+
export function drainBashSessions() {
|
|
717
|
+
for (const id of [..._sessions.keys()]) _killSession(id, 'process-exit');
|
|
718
|
+
}
|
|
719
|
+
if (typeof process?.on === 'function') {
|
|
720
|
+
process.on('exit', drainBashSessions);
|
|
721
|
+
}
|