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,549 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* dispatch-persist — crash / restart recovery for async dispatch handles.
|
|
3
|
+
*
|
|
4
|
+
* Plugin MCP server can be restarted by Claude Code at any time (idle timeout,
|
|
5
|
+
* user reload, etc.). Any in-flight dispatch whose merge callback had not yet
|
|
6
|
+
* run would otherwise be orphaned silently — handle issued, no result, no
|
|
7
|
+
* abort notification.
|
|
8
|
+
*
|
|
9
|
+
* This module persists the minimum needed to recover:
|
|
10
|
+
* - handle (`dispatch_<tool>_...`)
|
|
11
|
+
* - tool (`recall` / `search` / `explore`)
|
|
12
|
+
* - queries (for the abort message)
|
|
13
|
+
* - createdAt
|
|
14
|
+
*
|
|
15
|
+
* On add: write through to disk. On complete/error: remove entry.
|
|
16
|
+
* On bootstrap: read file, emit one abort Noti per surviving entry, clear.
|
|
17
|
+
*
|
|
18
|
+
* Best-effort everywhere — never let persist IO break the caller.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import fs from 'fs';
|
|
22
|
+
import path, { join } from 'path';
|
|
23
|
+
import { writeJsonAtomicSync } from '../../shared/atomic-file.mjs';
|
|
24
|
+
|
|
25
|
+
const TTL_MS = 30 * 60_000;
|
|
26
|
+
const FILE_NAME = 'pending-dispatches.json';
|
|
27
|
+
// Per-entry persisted result body cap. pushDispatchResult already
|
|
28
|
+
// smart-truncates to ~30KB; this is a defense-in-depth ceiling so the
|
|
29
|
+
// pending file cannot balloon if a future caller skips truncation.
|
|
30
|
+
const PERSIST_RESULT_MAX_BYTES = 64 * 1024;
|
|
31
|
+
|
|
32
|
+
// File mode for the on-disk pending-dispatches.json. Matches config/snapshot
|
|
33
|
+
// data-at-rest posture: owner-only read/write. The replayed body may contain
|
|
34
|
+
// tool output bytes (recall/search/explore) that, despite redaction below,
|
|
35
|
+
// should not be world-readable.
|
|
36
|
+
const PERSIST_FILE_MODE = 0o600;
|
|
37
|
+
|
|
38
|
+
// High-confidence secret patterns redacted before the result body is written
|
|
39
|
+
// to pending-dispatches.json. recoverPending replays entry.content verbatim
|
|
40
|
+
// into model context, so an async explore/search/recall result that happened
|
|
41
|
+
// to surface a credential would otherwise duplicate it into this JSON.
|
|
42
|
+
//
|
|
43
|
+
// Conservative on purpose — false negatives are acceptable (already-known
|
|
44
|
+
// truncation defense remains), false positives just redact a token-shaped
|
|
45
|
+
// substring in a transcript. Patterns:
|
|
46
|
+
// - `sk-...` bearer-style API keys (OpenAI / Anthropic family prefix)
|
|
47
|
+
// - `Bearer <token>` Authorization header values
|
|
48
|
+
// - `*_API_KEY` / `*_TOKEN` / `*_SECRET` assignments (env / json / yaml)
|
|
49
|
+
// - JWTs (`eyJ` header + two more base64url segments)
|
|
50
|
+
// - PEM private-key blocks (GCP service-account / RSA)
|
|
51
|
+
// - `"private_key_id": "<40 hex>"` JSON fields (GCP service-account)
|
|
52
|
+
const _SECRET_PATTERNS = [
|
|
53
|
+
// sk-<16+ token chars>
|
|
54
|
+
/\bsk-[A-Za-z0-9_\-]{16,}\b/g,
|
|
55
|
+
// Bearer <token>
|
|
56
|
+
/\bBearer\s+[A-Za-z0-9._\-]{16,}\b/gi,
|
|
57
|
+
// FOO_API_KEY = "..." | FOO_TOKEN: '...' | FOO_SECRET=...
|
|
58
|
+
// Captures the assignment prefix in group 1 so the key name survives.
|
|
59
|
+
/\b([A-Z][A-Z0-9_]*_(?:API_KEY|TOKEN|SECRET|PASSWORD))\s*[:=]\s*["']?([A-Za-z0-9_./+\-]{8,})["']?/g,
|
|
60
|
+
// JWT: three dot-separated base64url segments; header starts `eyJ`
|
|
61
|
+
// (base64 of `{"`). High-precision — the `eyJ` anchor + two segments
|
|
62
|
+
// avoids matching generic base64.
|
|
63
|
+
/\beyJ[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\b/g,
|
|
64
|
+
// PEM private-key block — any common label (PKCS#8 plain, RSA, EC, DSA,
|
|
65
|
+
// OPENSSH, ENCRYPTED). Non-greedy so a body with multiple keys redacts
|
|
66
|
+
// each block independently.
|
|
67
|
+
/-----BEGIN (?:RSA |EC |DSA |OPENSSH |ENCRYPTED )?PRIVATE KEY-----[\s\S]*?-----END (?:RSA |EC |DSA |OPENSSH |ENCRYPTED )?PRIVATE KEY-----/g,
|
|
68
|
+
// GCP service-account `"private_key_id": "<40 lowercase hex>"`.
|
|
69
|
+
// Captures the field prefix (g1) and closing quote (g2) so the JSON key
|
|
70
|
+
// and structure survive.
|
|
71
|
+
/("private_key_id"\s*:\s*")[a-f0-9]{40}(")/g,
|
|
72
|
+
];
|
|
73
|
+
|
|
74
|
+
function redactSecrets(body) {
|
|
75
|
+
if (typeof body !== 'string' || body.length === 0) return body;
|
|
76
|
+
try {
|
|
77
|
+
let out = body;
|
|
78
|
+
out = out.replace(_SECRET_PATTERNS[0], '[REDACTED:sk-key]');
|
|
79
|
+
out = out.replace(_SECRET_PATTERNS[1], 'Bearer [REDACTED]');
|
|
80
|
+
out = out.replace(_SECRET_PATTERNS[2], (_m, k) => `${k}=[REDACTED]`);
|
|
81
|
+
out = out.replace(_SECRET_PATTERNS[3], '[REDACTED:jwt]');
|
|
82
|
+
out = out.replace(_SECRET_PATTERNS[4], '[REDACTED:private-key]');
|
|
83
|
+
out = out.replace(_SECRET_PATTERNS[5], (_m, k, q) => `${k}[REDACTED]${q}`);
|
|
84
|
+
return out;
|
|
85
|
+
} catch {
|
|
86
|
+
// Fail closed: a redaction failure must NEVER leak the raw/partial body.
|
|
87
|
+
return '[REDACTED: secret-redaction failed]';
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Per-dataDir Promise tails — different dataDirs run in parallel.
|
|
92
|
+
// Keyed by normalized absolute dataDir path (path.resolve); value is the
|
|
93
|
+
// current tail Promise. Normalization ensures '/data/x/' and '/data/x' route
|
|
94
|
+
// to the same tail entry.
|
|
95
|
+
const _writeTails = new Map();
|
|
96
|
+
|
|
97
|
+
// Last successfully written payload per dataDir for exit-drain sync flush.
|
|
98
|
+
const _lastPayload = new Map();
|
|
99
|
+
|
|
100
|
+
// In-progress desired state captured at writeAll entry (before the async write
|
|
101
|
+
// completes). exitDrain prefers this over _lastPayload because it is newer —
|
|
102
|
+
// it reflects mutations that queued after the last completed writeAll but
|
|
103
|
+
// before process exit. Cleared once writeAll succeeds.
|
|
104
|
+
const _pendingPayload = new Map();
|
|
105
|
+
|
|
106
|
+
function getTail(dataDir) {
|
|
107
|
+
return _writeTails.get(path.resolve(dataDir)) ?? Promise.resolve();
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function setTail(dataDir, p) {
|
|
111
|
+
_writeTails.set(path.resolve(dataDir), p);
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// ── Exit drain: sync-flush in-flight tails on process exit ─────────────────
|
|
115
|
+
// Cannot await on exit; use sync writeFileSync to flush the last known payload.
|
|
116
|
+
//
|
|
117
|
+
// Risk (KEEP): this sync flush bypasses the async cross-process file lock.
|
|
118
|
+
// A concurrent writer from another process may race on the same file during
|
|
119
|
+
// the drain window. The window is bounded (process is exiting) and eliminating
|
|
120
|
+
// it requires a fundamentally different design (e.g. a dedicated lock-owner
|
|
121
|
+
// process). Best-effort is the correct trade-off here.
|
|
122
|
+
export function drainDispatchPersist() {
|
|
123
|
+
// Prefer _pendingPayload (desired state captured at writeAll entry) over
|
|
124
|
+
// _lastPayload (last successfully written state). Pending is strictly
|
|
125
|
+
// newer when a writeAll is still in-flight or queued at process exit.
|
|
126
|
+
const dirs = new Set([..._pendingPayload.keys(), ..._lastPayload.keys()]);
|
|
127
|
+
for (const dataDir of dirs) {
|
|
128
|
+
const payload = _pendingPayload.get(dataDir) ?? _lastPayload.get(dataDir);
|
|
129
|
+
if (!payload) continue;
|
|
130
|
+
try {
|
|
131
|
+
const p = pathFor(dataDir);
|
|
132
|
+
// fsync:false — see writeAll. This file is a best-effort restart-recovery
|
|
133
|
+
// spool; the page cache survives a plugin process restart (the only
|
|
134
|
+
// failure it guards), so we skip the synchronous disk-flush stall. KEEP
|
|
135
|
+
// lock:true: the exit-drain window can still race other processes.
|
|
136
|
+
writeJsonAtomicSync(p, payload, { compact: true, lock: true, mode: PERSIST_FILE_MODE, fsync: false });
|
|
137
|
+
} catch { /* best-effort */ }
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// SIGTERM/SIGINT drain runs through drain-registry.mjs (single signal
|
|
142
|
+
// handler for the whole orchestrator); the bare 'exit' hook stays as an
|
|
143
|
+
// idempotent backup for cases where the registry already ran.
|
|
144
|
+
process.once('exit', drainDispatchPersist);
|
|
145
|
+
|
|
146
|
+
// ── Cross-process file lock ─────────────────────────────────────────────────
|
|
147
|
+
// Uses O_EXCL (wx flag) on a sibling .lock file so concurrent writers from
|
|
148
|
+
// different processes serialize around the same R/M/W on pending-dispatches.json.
|
|
149
|
+
// Wait briefly with jittered polling; stale lock files are cleared so a crashed
|
|
150
|
+
// writer cannot make every later dispatch persist best-effort-only.
|
|
151
|
+
const LOCK_FILE_NAME = 'pending-dispatches.json.lock';
|
|
152
|
+
const LOCK_WAIT_MS = 8_000;
|
|
153
|
+
const LOCK_POLL_MS = 50;
|
|
154
|
+
const LOCK_STALE_MS = 30_000;
|
|
155
|
+
const LOCK_WAIT_CODES = new Set(['EEXIST', 'EPERM', 'EACCES', 'EBUSY']);
|
|
156
|
+
|
|
157
|
+
function lockPath(dataDir) {
|
|
158
|
+
return join(dataDir, LOCK_FILE_NAME);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Acquire a cross-process file lock. Returns the lock-file path on success
|
|
163
|
+
* so the caller can pass it to releaseFileLock. Returns null if the lock
|
|
164
|
+
* could not be acquired within the timeout; callers then skip this
|
|
165
|
+
* best-effort persist rather than writing unlocked over another process.
|
|
166
|
+
*/
|
|
167
|
+
async function acquireFileLock(dataDir) {
|
|
168
|
+
const lp = lockPath(dataDir);
|
|
169
|
+
const deadline = Date.now() + LOCK_WAIT_MS;
|
|
170
|
+
while (true) {
|
|
171
|
+
try {
|
|
172
|
+
// O_EXCL guarantees atomic create; fails with EEXIST if lock is held.
|
|
173
|
+
const fd = fs.openSync(lp, 'wx');
|
|
174
|
+
try { fs.writeSync(fd, `${process.pid} ${Date.now()}\n`, 0, 'utf8'); } catch { /* best-effort */ }
|
|
175
|
+
fs.closeSync(fd);
|
|
176
|
+
return lp;
|
|
177
|
+
} catch (err) {
|
|
178
|
+
if (!LOCK_WAIT_CODES.has(err?.code)) {
|
|
179
|
+
process.stderr.write(`[dispatch-persist] lock open error: ${err?.code || err?.message}\n`);
|
|
180
|
+
return null;
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
const st = fs.statSync(lp);
|
|
184
|
+
if (Date.now() - st.mtimeMs > LOCK_STALE_MS) {
|
|
185
|
+
try { fs.unlinkSync(lp); } catch { /* another process won */ }
|
|
186
|
+
continue;
|
|
187
|
+
}
|
|
188
|
+
} catch { /* stat race; retry */ }
|
|
189
|
+
if (Date.now() >= deadline) {
|
|
190
|
+
process.stderr.write(`[dispatch-persist] lock timeout after ${LOCK_WAIT_MS}ms — skipping this best-effort persist\n`);
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
await new Promise(r => setTimeout(r, LOCK_POLL_MS + Math.floor(Math.random() * LOCK_POLL_MS)));
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function releaseFileLock(lp) {
|
|
199
|
+
if (!lp) return;
|
|
200
|
+
try { fs.unlinkSync(lp); } catch { /* best-effort */ }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// ───────────────────────────────────────────────────────────────────────────
|
|
204
|
+
|
|
205
|
+
function pathFor(dataDir) {
|
|
206
|
+
return join(dataDir, FILE_NAME);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function readAll(dataDir) {
|
|
210
|
+
try {
|
|
211
|
+
const p = pathFor(dataDir);
|
|
212
|
+
try {
|
|
213
|
+
await fs.promises.access(p);
|
|
214
|
+
} catch {
|
|
215
|
+
return {};
|
|
216
|
+
}
|
|
217
|
+
const raw = await fs.promises.readFile(p, 'utf8');
|
|
218
|
+
if (!raw.trim()) return {};
|
|
219
|
+
const parsed = JSON.parse(raw);
|
|
220
|
+
return (parsed && typeof parsed === 'object') ? parsed : {};
|
|
221
|
+
} catch {
|
|
222
|
+
return {};
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function readAllSync(dataDir) {
|
|
227
|
+
try {
|
|
228
|
+
const p = pathFor(dataDir);
|
|
229
|
+
try {
|
|
230
|
+
fs.accessSync(p);
|
|
231
|
+
} catch {
|
|
232
|
+
return {};
|
|
233
|
+
}
|
|
234
|
+
const raw = fs.readFileSync(p, 'utf8');
|
|
235
|
+
if (!raw.trim()) return {};
|
|
236
|
+
const parsed = JSON.parse(raw);
|
|
237
|
+
return (parsed && typeof parsed === 'object') ? parsed : {};
|
|
238
|
+
} catch {
|
|
239
|
+
return {};
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
async function writeAll(dataDir, map) {
|
|
244
|
+
try {
|
|
245
|
+
const p = pathFor(dataDir);
|
|
246
|
+
// Capture desired state BEFORE the async write so exitDrain can sync-flush
|
|
247
|
+
// it even if this writeAll is still in-flight at process exit.
|
|
248
|
+
_pendingPayload.set(dataDir, map);
|
|
249
|
+
// fsync:false — pending-dispatches.json is a BEST-EFFORT restart-recovery
|
|
250
|
+
// spool, not durable data. The only event it must survive is a plugin MCP
|
|
251
|
+
// server restart, and the OS page cache already survives that (the bytes
|
|
252
|
+
// are visible to the next process without an fsync). The fsync only buys
|
|
253
|
+
// durability across an OS crash / power loss, which recovery does not rely
|
|
254
|
+
// on — so we skip the synchronous fsyncSync stall on the dispatch hot path.
|
|
255
|
+
// Atomic write-temp + rename ordering is unchanged; only the durability
|
|
256
|
+
// barrier is dropped. Default fsync behaviour is untouched for every other
|
|
257
|
+
// writeJsonAtomicSync caller (session saves, secrets, snapshots).
|
|
258
|
+
writeJsonAtomicSync(p, map, { compact: true, mode: PERSIST_FILE_MODE, fsync: false });
|
|
259
|
+
// Write completed — promote to last-written and clear pending (redundant now).
|
|
260
|
+
_lastPayload.set(dataDir, map);
|
|
261
|
+
_pendingPayload.delete(dataDir);
|
|
262
|
+
} catch { /* best-effort */ }
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/**
|
|
266
|
+
* Prune expired entries. Returns `{ map, changed }` so callers can decide
|
|
267
|
+
* whether to write the pruned state back to disk. `changed === true` iff
|
|
268
|
+
* at least one entry was deleted (or was present but falsy). addPending
|
|
269
|
+
* always writes regardless, so it does not need the flag; hasPending /
|
|
270
|
+
* recoverPending / removePending use it to persist the pruned map instead
|
|
271
|
+
* of letting expired entries accumulate in pending-dispatches.json across
|
|
272
|
+
* restarts.
|
|
273
|
+
*/
|
|
274
|
+
function gc(map) {
|
|
275
|
+
const now = Date.now();
|
|
276
|
+
let changed = false;
|
|
277
|
+
for (const [k, v] of Object.entries(map)) {
|
|
278
|
+
if (!v || (now - (v.createdAt || 0)) > TTL_MS) {
|
|
279
|
+
delete map[k];
|
|
280
|
+
changed = true;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return { map, changed };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
function normalizeClientHostPid(v) {
|
|
287
|
+
const n = Number(v);
|
|
288
|
+
return Number.isFinite(n) && n > 0 ? n : null;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export function addPending(dataDir, handle, tool, queries, callerSessionId, clientHostPid) {
|
|
292
|
+
if (!dataDir || !handle) return;
|
|
293
|
+
const tail = getTail(dataDir).then(async () => {
|
|
294
|
+
try {
|
|
295
|
+
const lp = await acquireFileLock(dataDir);
|
|
296
|
+
if (!lp) return;
|
|
297
|
+
try {
|
|
298
|
+
const { map } = gc(await readAll(dataDir));
|
|
299
|
+
// Preserve any prior fields (e.g. content from setPendingResult) so a
|
|
300
|
+
// re-add by pushDispatchResult does not erase the persisted body.
|
|
301
|
+
const prior = map[handle] && typeof map[handle] === 'object' ? map[handle] : {};
|
|
302
|
+
const sid = callerSessionId != null && String(callerSessionId)
|
|
303
|
+
? String(callerSessionId)
|
|
304
|
+
: prior.callerSessionId;
|
|
305
|
+
const hostPid = normalizeClientHostPid(clientHostPid) ?? normalizeClientHostPid(prior.clientHostPid);
|
|
306
|
+
map[handle] = {
|
|
307
|
+
...prior,
|
|
308
|
+
tool,
|
|
309
|
+
queries: Array.isArray(queries) ? queries : [String(queries)],
|
|
310
|
+
createdAt: prior.createdAt || Date.now(),
|
|
311
|
+
...(sid ? { callerSessionId: sid } : {}),
|
|
312
|
+
...(hostPid ? { clientHostPid: hostPid } : {}),
|
|
313
|
+
};
|
|
314
|
+
await writeAll(dataDir, map);
|
|
315
|
+
try {
|
|
316
|
+
process.stderr.write(`[dispatch-persist] persist handle=${handle} tool=${tool} entries=${Object.keys(map).length}\n`);
|
|
317
|
+
} catch { /* best-effort */ }
|
|
318
|
+
} finally {
|
|
319
|
+
releaseFileLock(lp);
|
|
320
|
+
}
|
|
321
|
+
} catch { /* best-effort */ }
|
|
322
|
+
});
|
|
323
|
+
setTail(dataDir, tail);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Attach the merged dispatch result body to an existing pending entry. Called
|
|
328
|
+
* BEFORE notify so a torn-down transport / mid-push restart can replay the
|
|
329
|
+
* actual answer via recoverPending instead of the generic Aborted boilerplate.
|
|
330
|
+
*
|
|
331
|
+
* Body is truncated to PERSIST_RESULT_MAX_BYTES so a future skipped-truncation
|
|
332
|
+
* caller cannot balloon the pending file. Truncation marker is appended verbatim
|
|
333
|
+
* — the body is already a finished user-facing answer when this runs.
|
|
334
|
+
*/
|
|
335
|
+
export function setPendingResult(dataDir, handle, tool, queries, content, isError, callerSessionId, clientHostPid) {
|
|
336
|
+
if (!dataDir || !handle) return Promise.resolve();
|
|
337
|
+
const tail = getTail(dataDir).then(async () => {
|
|
338
|
+
try {
|
|
339
|
+
const lp = await acquireFileLock(dataDir);
|
|
340
|
+
if (!lp) return;
|
|
341
|
+
try {
|
|
342
|
+
const { map } = gc(await readAll(dataDir));
|
|
343
|
+
const prior = map[handle] && typeof map[handle] === 'object' ? map[handle] : {};
|
|
344
|
+
const sid = callerSessionId != null && String(callerSessionId)
|
|
345
|
+
? String(callerSessionId)
|
|
346
|
+
: prior.callerSessionId;
|
|
347
|
+
const hostPid = normalizeClientHostPid(clientHostPid) ?? normalizeClientHostPid(prior.clientHostPid);
|
|
348
|
+
let body = typeof content === 'string' ? content : String(content ?? '');
|
|
349
|
+
// Redact high-confidence secret patterns BEFORE truncation so a key
|
|
350
|
+
// that lands near the truncation boundary cannot be sliced into the
|
|
351
|
+
// persisted prefix unredacted.
|
|
352
|
+
body = redactSecrets(body);
|
|
353
|
+
if (Buffer.byteLength(body, 'utf8') > PERSIST_RESULT_MAX_BYTES) {
|
|
354
|
+
body = body.slice(0, PERSIST_RESULT_MAX_BYTES) + '\n…[persist-truncated]';
|
|
355
|
+
}
|
|
356
|
+
map[handle] = {
|
|
357
|
+
...prior,
|
|
358
|
+
tool: tool || prior.tool,
|
|
359
|
+
queries: Array.isArray(queries) ? queries : (prior.queries || []),
|
|
360
|
+
createdAt: prior.createdAt || Date.now(),
|
|
361
|
+
content: body,
|
|
362
|
+
isError: !!isError,
|
|
363
|
+
...(sid ? { callerSessionId: sid } : {}),
|
|
364
|
+
...(hostPid ? { clientHostPid: hostPid } : {}),
|
|
365
|
+
};
|
|
366
|
+
await writeAll(dataDir, map);
|
|
367
|
+
try {
|
|
368
|
+
process.stderr.write(`[dispatch-persist] persist-result handle=${handle} tool=${tool} bytes=${Buffer.byteLength(body, 'utf8')}\n`);
|
|
369
|
+
} catch { /* best-effort */ }
|
|
370
|
+
} finally {
|
|
371
|
+
releaseFileLock(lp);
|
|
372
|
+
}
|
|
373
|
+
} catch { /* best-effort */ }
|
|
374
|
+
});
|
|
375
|
+
setTail(dataDir, tail);
|
|
376
|
+
// Return the tail Promise so callers (pushDispatchResult) can actually
|
|
377
|
+
// await the disk flush before notifying. Previously this returned void,
|
|
378
|
+
// letting notify race the debounced write — a crash between the two
|
|
379
|
+
// would lose the persisted body recoverPending depends on.
|
|
380
|
+
return tail;
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/**
|
|
384
|
+
* Best-effort check: is there at least one non-expired in-flight dispatch
|
|
385
|
+
* recorded for this dataDir? Used by the scheduler's idle-state probe so
|
|
386
|
+
* background tasks stay suppressed while a bridge dispatch is still
|
|
387
|
+
* running. Never throws.
|
|
388
|
+
*/
|
|
389
|
+
export function hasPending(dataDir) {
|
|
390
|
+
if (!dataDir) return false;
|
|
391
|
+
try {
|
|
392
|
+
// hasPending is a synchronous probe on the hot path; read without lock is
|
|
393
|
+
// acceptable (observation only). If gc pruned entries, flush asynchronously
|
|
394
|
+
// via per-dataDir tail so the write is still cross-process serialized.
|
|
395
|
+
const p = pathFor(dataDir);
|
|
396
|
+
let raw = '';
|
|
397
|
+
try { raw = fs.readFileSync(p, 'utf8'); } catch { /* missing = empty */ }
|
|
398
|
+
let parsed = {};
|
|
399
|
+
try { if (raw.trim()) parsed = JSON.parse(raw); } catch { /* best-effort */ }
|
|
400
|
+
if (!parsed || typeof parsed !== 'object') parsed = {};
|
|
401
|
+
const { map, changed } = gc(parsed);
|
|
402
|
+
if (changed) {
|
|
403
|
+
const tail = getTail(dataDir).then(async () => {
|
|
404
|
+
const lp = await acquireFileLock(dataDir);
|
|
405
|
+
if (!lp) return;
|
|
406
|
+
try { await writeAll(dataDir, map); } finally { releaseFileLock(lp); }
|
|
407
|
+
});
|
|
408
|
+
setTail(dataDir, tail);
|
|
409
|
+
}
|
|
410
|
+
return Object.keys(map).length > 0;
|
|
411
|
+
} catch {
|
|
412
|
+
return false;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
export function removePending(dataDir, handle) {
|
|
417
|
+
if (!dataDir || !handle) return;
|
|
418
|
+
const tail = getTail(dataDir).then(async () => {
|
|
419
|
+
try {
|
|
420
|
+
const lp = await acquireFileLock(dataDir);
|
|
421
|
+
if (!lp) return;
|
|
422
|
+
try {
|
|
423
|
+
const { map, changed } = gc(await readAll(dataDir));
|
|
424
|
+
let mutated = changed;
|
|
425
|
+
if (handle in map) {
|
|
426
|
+
delete map[handle];
|
|
427
|
+
mutated = true;
|
|
428
|
+
try {
|
|
429
|
+
process.stderr.write(`[dispatch-persist] ack-pop handle=${handle} entries=${Object.keys(map).length}\n`);
|
|
430
|
+
} catch { /* best-effort */ }
|
|
431
|
+
}
|
|
432
|
+
if (mutated) await writeAll(dataDir, map);
|
|
433
|
+
} finally {
|
|
434
|
+
releaseFileLock(lp);
|
|
435
|
+
}
|
|
436
|
+
} catch { /* best-effort */ }
|
|
437
|
+
});
|
|
438
|
+
setTail(dataDir, tail);
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Called once at plugin bootstrap after the MCP transport is connected.
|
|
443
|
+
* For every pending entry remaining from the previous process lifetime,
|
|
444
|
+
* emit a single Aborted notification with `type: 'dispatch_result'` so the
|
|
445
|
+
* Lead can close the loop on its next turn. Then clear the file.
|
|
446
|
+
*
|
|
447
|
+
* Recovery is chained onto the per-dataDir tail so it serializes with any
|
|
448
|
+
* in-flight addPending / removePending mutations for the same dataDir.
|
|
449
|
+
* Notifications fire asynchronously; the return value is the number of
|
|
450
|
+
* handles queued for recovery (callers use it as bootstrap telemetry).
|
|
451
|
+
*/
|
|
452
|
+
export function recoverPending(dataDir, notifyFn, { sessionId, priorSessionId, clientHostPid } = {}) {
|
|
453
|
+
if (!dataDir || typeof notifyFn !== 'function') return 0;
|
|
454
|
+
const { map: snapshot } = gc(readAllSync(dataDir));
|
|
455
|
+
const filterSid = sessionId != null && String(sessionId) ? String(sessionId) : null;
|
|
456
|
+
const priorSid = priorSessionId != null && String(priorSessionId) ? String(priorSessionId) : null;
|
|
457
|
+
const filterHostPid = normalizeClientHostPid(clientHostPid);
|
|
458
|
+
const matchesScope = (entry) => {
|
|
459
|
+
if (!filterSid && !filterHostPid) return true;
|
|
460
|
+
const callerSessionId = entry?.callerSessionId;
|
|
461
|
+
const cid = callerSessionId != null && String(callerSessionId) ? String(callerSessionId) : null;
|
|
462
|
+
if (cid && (cid === filterSid || (priorSid != null && cid === priorSid))) return true;
|
|
463
|
+
const entryHostPid = normalizeClientHostPid(entry?.clientHostPid);
|
|
464
|
+
return filterHostPid != null && entryHostPid === filterHostPid;
|
|
465
|
+
};
|
|
466
|
+
const scoped = filterSid || filterHostPid;
|
|
467
|
+
const queued = scoped
|
|
468
|
+
? Object.keys(snapshot).filter((h) => matchesScope(snapshot[h])).length
|
|
469
|
+
: Object.keys(snapshot).length;
|
|
470
|
+
const tail = getTail(dataDir).then(async () => {
|
|
471
|
+
const lp = await acquireFileLock(dataDir);
|
|
472
|
+
if (!lp) return;
|
|
473
|
+
try {
|
|
474
|
+
const { map, changed } = gc(await readAll(dataDir));
|
|
475
|
+
const handles = Object.keys(map).filter((handle) => {
|
|
476
|
+
return matchesScope(map[handle]);
|
|
477
|
+
});
|
|
478
|
+
if (handles.length === 0) {
|
|
479
|
+
// No handles to recover for this scope. A gc() pass may still have
|
|
480
|
+
// pruned expired entries — persist the pruned `map` (NOT `{}`): under a
|
|
481
|
+
// session-scoped recovery `handles` is only the reconnecting session's
|
|
482
|
+
// subset, so other sessions' still-live pending entries remain in `map`
|
|
483
|
+
// and must survive. (Unscoped recovery reaches here only when `map` is
|
|
484
|
+
// already empty, so writing `map` is equivalent to writing `{}` there.)
|
|
485
|
+
if (changed) await writeAll(dataDir, map);
|
|
486
|
+
return;
|
|
487
|
+
}
|
|
488
|
+
for (const handle of handles) {
|
|
489
|
+
const entry = map[handle] || {};
|
|
490
|
+
const tool = entry.tool || 'dispatch';
|
|
491
|
+
const queries = Array.isArray(entry.queries) ? entry.queries : [];
|
|
492
|
+
// Two recovery modes:
|
|
493
|
+
// 1. Persisted result body present → re-deliver the actual answer.
|
|
494
|
+
// The result was computed but the channel push didn't ack before
|
|
495
|
+
// the plugin died. setPendingResult wrote it pre-notify.
|
|
496
|
+
// 2. No body → the worker hadn't returned yet at restart; emit the
|
|
497
|
+
// Aborted boilerplate so the Lead can retry.
|
|
498
|
+
let content;
|
|
499
|
+
let isError;
|
|
500
|
+
let kind;
|
|
501
|
+
if (typeof entry.content === 'string' && entry.content.length > 0) {
|
|
502
|
+
content = entry.content;
|
|
503
|
+
isError = !!entry.isError;
|
|
504
|
+
kind = 'replay';
|
|
505
|
+
} else {
|
|
506
|
+
const qSuffix = queries.length === 1 ? '1 query' : `${queries.length} queries`;
|
|
507
|
+
content = `[${tool}] Aborted — plugin restart interrupted dispatch (${qSuffix}). Retry if still needed.`;
|
|
508
|
+
isError = true;
|
|
509
|
+
kind = 'abort';
|
|
510
|
+
}
|
|
511
|
+
const meta = {
|
|
512
|
+
type: 'dispatch_result',
|
|
513
|
+
dispatch_id: handle,
|
|
514
|
+
tool,
|
|
515
|
+
error: String(isError),
|
|
516
|
+
...(filterSid
|
|
517
|
+
? { caller_session_id: filterSid }
|
|
518
|
+
: (entry.callerSessionId ? { caller_session_id: entry.callerSessionId } : {})),
|
|
519
|
+
...(filterHostPid > 0
|
|
520
|
+
? { client_host_pid: String(filterHostPid) }
|
|
521
|
+
: (entry.clientHostPid > 0 ? { client_host_pid: String(entry.clientHostPid) } : {})),
|
|
522
|
+
instruction: kind === 'replay'
|
|
523
|
+
? `Earlier ${tool} dispatch (${handle}) result was queued before plugin restart — here it is.`
|
|
524
|
+
: `Earlier ${tool} dispatch (${handle}) was aborted by a plugin restart. Retry if the answer is still needed.`,
|
|
525
|
+
};
|
|
526
|
+
try { process.stderr.write(`[dispatch-persist] recover handle=${handle} tool=${tool} kind=${kind}\n`); } catch { /* best-effort */ }
|
|
527
|
+
// Entry remains on disk until notifyFn resolves. A crash between
|
|
528
|
+
// fire and ack is safe: the entry survives and recoverPending re-fires
|
|
529
|
+
// it on the next restart. Only clear AFTER ack to prevent silent loss.
|
|
530
|
+
try {
|
|
531
|
+
Promise.resolve(notifyFn(content, meta)).then(() => {
|
|
532
|
+
removePending(dataDir, handle);
|
|
533
|
+
}).catch(() => { /* best-effort — entry stays for next recoverPending */ });
|
|
534
|
+
} catch { /* best-effort */ }
|
|
535
|
+
}
|
|
536
|
+
// Do NOT bulk-clear here. Each handle is removed individually above,
|
|
537
|
+
// only after its notifyFn acks. If gc() pruned expired entries, write
|
|
538
|
+
// back the pruned map (without expired keys) — live handles remain on
|
|
539
|
+
// disk until their per-handle removePending calls land.
|
|
540
|
+
if (changed) await writeAll(dataDir, map);
|
|
541
|
+
try {
|
|
542
|
+
process.stderr.write(`[dispatch-persist] recoverPending recovered=${handles.length} entries queued\n`);
|
|
543
|
+
} catch { /* best-effort */ }
|
|
544
|
+
} catch { /* best-effort */ }
|
|
545
|
+
finally { releaseFileLock(lp); }
|
|
546
|
+
});
|
|
547
|
+
setTail(dataDir, tail);
|
|
548
|
+
return queued;
|
|
549
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Exit-time drain registry for the cli.mjs process. Single SIGTERM/SIGINT
|
|
3
|
+
* owner — modules must NOT self-install signal handlers (avoids import-order
|
|
4
|
+
* race where the first handler's process.exit suppressed the rest).
|
|
5
|
+
* runAllDrains walks _drainBuckets in priority order; drains are best-effort
|
|
6
|
+
* by contract (never throw). Bucket order/membership pinned by tests.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { drainSessionStore } from './session/store.mjs';
|
|
10
|
+
import { drainCodeGraphCache } from './tools/code-graph.mjs';
|
|
11
|
+
import { drainCacheStats } from './session/cache/scoped-cache.mjs';
|
|
12
|
+
import { drainBridgeTrace } from './bridge-trace.mjs';
|
|
13
|
+
import { drainDispatchPersist } from './dispatch-persist.mjs';
|
|
14
|
+
import { drainJobs } from './jobs.mjs';
|
|
15
|
+
import { drainBashSessions } from './tools/bash-session.mjs';
|
|
16
|
+
import { drainShellSnapshots } from './tools/shell-snapshot.mjs';
|
|
17
|
+
import { drainMcpClients } from './mcp/client.mjs';
|
|
18
|
+
import { drainOpenaiWsPool } from './providers/openai-oauth-ws.mjs';
|
|
19
|
+
|
|
20
|
+
const _drainBuckets = [
|
|
21
|
+
{ name: 'data-integrity', drains: [drainSessionStore, drainDispatchPersist, drainJobs] },
|
|
22
|
+
{ name: 'external-resource', drains: [drainBashSessions, drainShellSnapshots, drainMcpClients, drainOpenaiWsPool] },
|
|
23
|
+
{ name: 'recoverable-cache', drains: [drainCodeGraphCache, drainCacheStats] },
|
|
24
|
+
{ name: 'telemetry', drains: [drainBridgeTrace] },
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
// Atomic single-execution: concurrent callers share the same Promise so
|
|
28
|
+
// each drain fires exactly once across process lifetime.
|
|
29
|
+
let _runningDrain = null;
|
|
30
|
+
export function runAllDrains() {
|
|
31
|
+
if (_runningDrain) return _runningDrain;
|
|
32
|
+
_runningDrain = (async () => {
|
|
33
|
+
for (const bucket of _drainBuckets) {
|
|
34
|
+
for (const fn of bucket.drains) await fn();
|
|
35
|
+
}
|
|
36
|
+
})();
|
|
37
|
+
return _runningDrain;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Test-facing snapshot — tests pin priority order and bucket membership.
|
|
41
|
+
export const _internals = { _drainBuckets };
|
|
42
|
+
|
|
43
|
+
// Side-effect signal handlers — drain on SIGINT/SIGTERM same as normal exit.
|
|
44
|
+
// Exit code 128 + signal (SIGINT=2 → 130, SIGTERM=15 → 143).
|
|
45
|
+
async function _signalDrainExit(code) {
|
|
46
|
+
await runAllDrains();
|
|
47
|
+
process.exit(code);
|
|
48
|
+
}
|
|
49
|
+
process.on('SIGINT', () => { _signalDrainExit(130); });
|
|
50
|
+
process.on('SIGTERM', () => { _signalDrainExit(143); });
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Runtime envelope limits for explore output. These are not classifier
|
|
2
|
+
// heuristics — they cap raw string allocation to stay clear of V8's
|
|
3
|
+
// max-string-length (~512 MB) when concatenating subagent responses across
|
|
4
|
+
// a broad cwd (e.g. the whole ~/.claude tree). Lowering these would silently
|
|
5
|
+
// truncate legitimate output; raising them risks OOM crashes in the MCP server.
|
|
6
|
+
export const EXPLORE_OUTPUT_CHAR_CAP = 50_000_000
|
|
7
|
+
export const EXPLORE_PER_PIECE_CHAR_CAP = 5_000_000
|
|
8
|
+
export const EXPLORE_TRUNCATION_MARKER = '\n\n[explore: output truncated at 50MB cap; narrow cwd or split queries to see more]'
|