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,491 @@
|
|
|
1
|
+
import { isOffloadedToolResultText } from './tool-result-offload.mjs';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
|
|
4
|
+
// Rough token estimate: ~4 chars per token
|
|
5
|
+
function estimateTokens(text) {
|
|
6
|
+
return Math.ceil(String(text ?? '').length / 4);
|
|
7
|
+
}
|
|
8
|
+
function messageEstimateText(m) {
|
|
9
|
+
if (!m || typeof m !== 'object') return '';
|
|
10
|
+
let text = typeof m.content === 'string' ? m.content : JSON.stringify(m.content ?? '');
|
|
11
|
+
if (m.role === 'assistant' && Array.isArray(m.toolCalls) && m.toolCalls.length) {
|
|
12
|
+
try { text += `\n${JSON.stringify(m.toolCalls)}`; }
|
|
13
|
+
catch { text += `\n[${m.toolCalls.length} tool calls]`; }
|
|
14
|
+
}
|
|
15
|
+
if (m.role === 'tool' && m.toolCallId) text += `\n${m.toolCallId}`;
|
|
16
|
+
return text;
|
|
17
|
+
}
|
|
18
|
+
function estimateMessageTokens(m) {
|
|
19
|
+
return estimateTokens(messageEstimateText(m)) + 4;
|
|
20
|
+
}
|
|
21
|
+
function estimateMessagesTokens(messages) {
|
|
22
|
+
return messages.reduce((sum, m) => sum + estimateMessageTokens(m), 0);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Per-request overhead the provider injects that never appears in the
|
|
26
|
+
// `messages` array: function-calling preamble + system-prompt framing the
|
|
27
|
+
// provider wraps around the request. The chars/4 message estimate misses all
|
|
28
|
+
// of it, so a "fits" verdict computed from messages alone is optimistic.
|
|
29
|
+
const REQUEST_OVERHEAD_TOKENS = 512;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Estimate the token cost of the tool/function schemas a provider appends to
|
|
33
|
+
* the request body. These are NOT part of `messages` (they're a separate
|
|
34
|
+
* argument to provider.send), so estimateMessagesTokens() ignores them
|
|
35
|
+
* entirely — a transcript that "fits" by message tokens can still overflow
|
|
36
|
+
* once N tool schemas are serialized into the same request. Best-effort
|
|
37
|
+
* chars/4 over the JSON-serialized definitions.
|
|
38
|
+
*/
|
|
39
|
+
export function estimateToolSchemaTokens(tools) {
|
|
40
|
+
if (!Array.isArray(tools) || tools.length === 0) return 0;
|
|
41
|
+
let text = '';
|
|
42
|
+
try { text = JSON.stringify(tools); }
|
|
43
|
+
catch { text = tools.map(t => String(t?.name ?? '')).join(''); }
|
|
44
|
+
return estimateTokens(text);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Total headroom the caller should reserve out of the context window before
|
|
49
|
+
* trimming: tool-schema bytes + fixed request framing overhead. Pass the
|
|
50
|
+
* result as `opts.reserveTokens` to trimMessages so the working budget
|
|
51
|
+
* accounts for request-side bytes the message estimate cannot see.
|
|
52
|
+
*/
|
|
53
|
+
export function estimateRequestReserveTokens(tools) {
|
|
54
|
+
return estimateToolSchemaTokens(tools) + REQUEST_OVERHEAD_TOKENS;
|
|
55
|
+
}
|
|
56
|
+
const TOOL_MISSING_STUB = '[Older tool result dropped to fit the context window]';
|
|
57
|
+
/**
|
|
58
|
+
* Walk backward from `idx` past consecutive tool messages to the parent
|
|
59
|
+
* assistant message that emitted the tool_calls. Returns an index that points
|
|
60
|
+
* at (or before) that assistant so a byte-budget cut drops the group as a
|
|
61
|
+
* unit rather than leaving orphan tool results. Returns `idx` unchanged if
|
|
62
|
+
* we didn't land inside a group.
|
|
63
|
+
*/
|
|
64
|
+
export function alignBoundaryBackward(messages, idx) {
|
|
65
|
+
if (!Array.isArray(messages) || idx <= 0 || idx >= messages.length) return idx;
|
|
66
|
+
let i = idx;
|
|
67
|
+
while (i > 0 && messages[i]?.role === 'tool') i--;
|
|
68
|
+
if (i === idx) return idx;
|
|
69
|
+
const anchor = messages[i];
|
|
70
|
+
if (anchor?.role === 'assistant' && Array.isArray(anchor.toolCalls) && anchor.toolCalls.length) {
|
|
71
|
+
return i;
|
|
72
|
+
}
|
|
73
|
+
return idx;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Post-trim sanitization (Hermes `_sanitize_tool_pairs`):
|
|
77
|
+
* - Drop `tool` messages whose toolCallId has no surviving assistant tool_call.
|
|
78
|
+
* - For surviving assistant tool_calls whose results got trimmed, insert a
|
|
79
|
+
* stub tool message so the provider doesn't reject the request for
|
|
80
|
+
* unmatched tool_use_id.
|
|
81
|
+
* Messages ordering is preserved; stubs are inserted immediately after the
|
|
82
|
+
* assistant message so the tool pair sits adjacent.
|
|
83
|
+
*/
|
|
84
|
+
export function sanitizeToolPairs(messages) {
|
|
85
|
+
if (!Array.isArray(messages) || messages.length === 0) return messages;
|
|
86
|
+
const assistantCallIds = new Set();
|
|
87
|
+
for (const m of messages) {
|
|
88
|
+
if (m.role === 'assistant' && Array.isArray(m.toolCalls)) {
|
|
89
|
+
for (const tc of m.toolCalls) {
|
|
90
|
+
if (tc && tc.id) assistantCallIds.add(tc.id);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
const toolById = new Map();
|
|
95
|
+
for (const m of messages) {
|
|
96
|
+
if (m.role === 'tool' && m.toolCallId) toolById.set(m.toolCallId, m);
|
|
97
|
+
}
|
|
98
|
+
const filtered = messages.filter(m => {
|
|
99
|
+
if (m.role !== 'tool') return true;
|
|
100
|
+
if (!m.toolCallId) return true;
|
|
101
|
+
return assistantCallIds.has(m.toolCallId);
|
|
102
|
+
});
|
|
103
|
+
const result = [];
|
|
104
|
+
for (const m of filtered) {
|
|
105
|
+
result.push(m);
|
|
106
|
+
if (m.role !== 'assistant' || !Array.isArray(m.toolCalls)) continue;
|
|
107
|
+
for (const tc of m.toolCalls) {
|
|
108
|
+
if (!tc?.id) continue;
|
|
109
|
+
const existing = toolById.get(tc.id);
|
|
110
|
+
if (existing && filtered.includes(existing)) continue;
|
|
111
|
+
const preserved = existing?.content;
|
|
112
|
+
result.push({
|
|
113
|
+
role: 'tool',
|
|
114
|
+
content: isOffloadedToolResultText(preserved) ? preserved : TOOL_MISSING_STUB,
|
|
115
|
+
toolCallId: tc.id,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
return result;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Minimum body size to consider for hash-based dedup. Small results are
|
|
123
|
+
// cheap to re-deliver and short strings often collide on trivial content
|
|
124
|
+
// like "ok" or "done", so deduplicate only non-trivial bodies.
|
|
125
|
+
const DEDUP_MIN_BYTES = 512;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* Replace duplicate tool-result bodies (2nd+ occurrence of the same content
|
|
129
|
+
* hash) with a compact reference stub. Hash-based dedup avoids re-delivering
|
|
130
|
+
* large identical results (e.g. the same grep output called twice) while
|
|
131
|
+
* keeping the first occurrence intact so the model still has the body.
|
|
132
|
+
*
|
|
133
|
+
* Skip conditions (structural — not heuristic prefix sniffing):
|
|
134
|
+
* - m.toolKind !== 'normal' (and defined): cache-hit / error / ref messages
|
|
135
|
+
* carry a structured kind annotation set by loop.mjs; skip them.
|
|
136
|
+
* - No toolKind (undefined): legacy or intra-turn-dedup stubs — apply dedup
|
|
137
|
+
* (backward compatible; the dedup body IS the meaningful result).
|
|
138
|
+
* - content.length < DEDUP_MIN_BYTES: structural cost optimization.
|
|
139
|
+
* - isOffloadedToolResultText(content): body is on disk, not inline.
|
|
140
|
+
*/
|
|
141
|
+
export function dedupToolResultBodies(messages) {
|
|
142
|
+
if (!Array.isArray(messages) || messages.length === 0) return messages;
|
|
143
|
+
const seenHash = new Map(); // hash -> first toolCallId
|
|
144
|
+
return messages.map((m) => {
|
|
145
|
+
if (m?.role !== 'tool' || typeof m.content !== 'string') return m;
|
|
146
|
+
const content = m.content;
|
|
147
|
+
if (content.length < DEDUP_MIN_BYTES) return m;
|
|
148
|
+
if (isOffloadedToolResultText(content)) return m;
|
|
149
|
+
// Structural kind-based skip: non-normal kinds are already stubs/refs —
|
|
150
|
+
// deduping them would nest stubs inside stubs and confuse the model.
|
|
151
|
+
if (m.toolKind !== undefined && m.toolKind !== 'normal') return m;
|
|
152
|
+
const hash = createHash('sha256').update(content).digest('hex').slice(0, 16);
|
|
153
|
+
const first = seenHash.get(hash);
|
|
154
|
+
if (!first) {
|
|
155
|
+
seenHash.set(hash, m.toolCallId || '?');
|
|
156
|
+
return m;
|
|
157
|
+
}
|
|
158
|
+
const stub = `[duplicate-of tool_use_id=${first}] body identical to result of ${first} (sha256 prefix matches; ${content.length} bytes elided).`;
|
|
159
|
+
return { ...m, content: stub };
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// Match the head of dedupToolResultBodies' stub body so we can detect whether
|
|
164
|
+
// the referenced first-occurrence tool_use_id is still present after later
|
|
165
|
+
// drop passes (safety loop, sanitize). Any stub pointing at an id no longer
|
|
166
|
+
// in the message stream is reconciled back to TOOL_MISSING_STUB so the model
|
|
167
|
+
// never sees `[duplicate-of call_X]` with no call_X.
|
|
168
|
+
const DEDUP_STUB_HEAD_RE = /^\[duplicate-of tool_use_id=([^\]]+)\]/;
|
|
169
|
+
export function reconcileDedupStubs(messages) {
|
|
170
|
+
if (!Array.isArray(messages) || messages.length === 0) return messages;
|
|
171
|
+
const presentIds = new Set();
|
|
172
|
+
for (const m of messages) {
|
|
173
|
+
if (m?.role === 'tool' && m.toolCallId) presentIds.add(m.toolCallId);
|
|
174
|
+
}
|
|
175
|
+
return messages.map((m) => {
|
|
176
|
+
if (m?.role !== 'tool' || typeof m.content !== 'string') return m;
|
|
177
|
+
const match = DEDUP_STUB_HEAD_RE.exec(m.content);
|
|
178
|
+
if (!match) return m;
|
|
179
|
+
if (presentIds.has(match[1])) return m;
|
|
180
|
+
return { ...m, content: TOOL_MISSING_STUB };
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Final-mile pairing for Anthropic API content arrays. Operates on the
|
|
186
|
+
* already-converted format (role: assistant|user|system, content: block[])
|
|
187
|
+
* — the mixdog-internal sanitizeToolPairs only sees toolCalls/toolCallId
|
|
188
|
+
* fields and misses cases where tool_use blocks were pushed directly into
|
|
189
|
+
* content (streaming chunk inserts, salvage paths, etc.). Without this
|
|
190
|
+
* pass, an unmatched tool_use can reach the provider and trigger
|
|
191
|
+
* `messages.N: tool_use ids were found without tool_result blocks
|
|
192
|
+
* immediately after`.
|
|
193
|
+
*/
|
|
194
|
+
export function sanitizeAnthropicContentPairs(messages) {
|
|
195
|
+
if (!Array.isArray(messages)) return messages;
|
|
196
|
+
const work = messages.slice();
|
|
197
|
+
const out = [];
|
|
198
|
+
for (let i = 0; i < work.length; i++) {
|
|
199
|
+
let m = work[i];
|
|
200
|
+
// Drop tool_use blocks without an id from assistant messages — these
|
|
201
|
+
// come from partial streaming chunks that never finalised, and the
|
|
202
|
+
// provider rejects them as `tool_use ids were found without
|
|
203
|
+
// tool_result blocks` even though no id was actually emitted.
|
|
204
|
+
if (m?.role === 'assistant' && Array.isArray(m.content)) {
|
|
205
|
+
const cleaned = m.content.filter(
|
|
206
|
+
(b) => !(b?.type === 'tool_use' && !b.id),
|
|
207
|
+
);
|
|
208
|
+
if (cleaned.length !== m.content.length) {
|
|
209
|
+
m = { ...m, content: cleaned };
|
|
210
|
+
work[i] = m;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
out.push(m);
|
|
214
|
+
if (m?.role !== 'assistant' || !Array.isArray(m.content)) continue;
|
|
215
|
+
const toolUseIds = m.content
|
|
216
|
+
.filter((b) => b?.type === 'tool_use' && b.id)
|
|
217
|
+
.map((b) => b.id);
|
|
218
|
+
if (toolUseIds.length === 0) continue;
|
|
219
|
+
const next = work[i + 1];
|
|
220
|
+
const nextResultIds = (next?.role === 'user' && Array.isArray(next.content))
|
|
221
|
+
? new Set(
|
|
222
|
+
next.content
|
|
223
|
+
.filter((b) => b?.type === 'tool_result' && b.tool_use_id)
|
|
224
|
+
.map((b) => b.tool_use_id),
|
|
225
|
+
)
|
|
226
|
+
: new Set();
|
|
227
|
+
const missing = toolUseIds.filter((id) => !nextResultIds.has(id));
|
|
228
|
+
const stubs = missing.map((id) => ({
|
|
229
|
+
type: 'tool_result',
|
|
230
|
+
tool_use_id: id,
|
|
231
|
+
content: '[tool_result missing — recovered by sanitizeAnthropicContentPairs]',
|
|
232
|
+
is_error: true,
|
|
233
|
+
}));
|
|
234
|
+
if (next?.role === 'user' && Array.isArray(next.content)) {
|
|
235
|
+
// Anthropic requires tool_result blocks to lead the user message
|
|
236
|
+
// when responding to a prior tool_use. Reorder even when no stub
|
|
237
|
+
// was needed; a matching tool_result after text still triggers the
|
|
238
|
+
// same `tool_use ids ... without tool_result blocks immediately
|
|
239
|
+
// after` rejection.
|
|
240
|
+
const existingResults = next.content.filter((b) => b?.type === 'tool_result');
|
|
241
|
+
const nonResults = next.content.filter((b) => b?.type !== 'tool_result');
|
|
242
|
+
const reordered = [...stubs, ...existingResults, ...nonResults];
|
|
243
|
+
const changed = missing.length > 0 || reordered.some((b, idx) => b !== next.content[idx]);
|
|
244
|
+
if (changed) work[i + 1] = { ...next, content: reordered };
|
|
245
|
+
} else {
|
|
246
|
+
if (missing.length === 0) continue;
|
|
247
|
+
out.push({ role: 'user', content: stubs });
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/**
|
|
254
|
+
* Trim messages to fit within a token budget.
|
|
255
|
+
*
|
|
256
|
+
* Single linear path — no fallback chain:
|
|
257
|
+
* 1. Sanitize tool pairs on entry.
|
|
258
|
+
* 2. Dedup repeated large tool-result bodies (hash-based; first occurrence kept).
|
|
259
|
+
* 3. Return deduped messages if already within budget.
|
|
260
|
+
* 4. Drop tool result messages oldest-first; also drop paired assistant
|
|
261
|
+
* once all its tool calls are gone.
|
|
262
|
+
* 5. If still over budget, drop oldest non-system messages respecting
|
|
263
|
+
* tool-call group boundaries.
|
|
264
|
+
* 6. Final sanitize + safety loop absorbs stub-insertion overshoot.
|
|
265
|
+
*
|
|
266
|
+
* budgetTokens MUST be derived from the model's context window by the
|
|
267
|
+
* caller (session.contextWindow * safetyFactor). Passing 0 or a negative
|
|
268
|
+
* value is a caller error and throws immediately.
|
|
269
|
+
*
|
|
270
|
+
* opts.reserveTokens (optional) subtracts request-side bytes the message
|
|
271
|
+
* estimate cannot see (tool schemas + provider framing — see
|
|
272
|
+
* estimateRequestReserveTokens). The reserve is clamped so it can never
|
|
273
|
+
* drive the effective budget to <= 0; at most it leaves 1 token of working
|
|
274
|
+
* room so callers that over-estimate the reserve still get a usable budget
|
|
275
|
+
* rather than an immediate throw.
|
|
276
|
+
*/
|
|
277
|
+
export function trimMessages(messages, budgetTokens, opts = {}) {
|
|
278
|
+
if (!(budgetTokens > 0)) throw new Error();
|
|
279
|
+
const reserve = Number(opts?.reserveTokens) || 0;
|
|
280
|
+
if (reserve > 0) {
|
|
281
|
+
// Cap the reserve so it can never consume more than half the budget.
|
|
282
|
+
// An over-estimated reserve (large tool schemas relative to a small
|
|
283
|
+
// context window) would otherwise collapse the effective budget toward
|
|
284
|
+
// 1, making system+latest baseCost exceed it and forcing trimMessages
|
|
285
|
+
// to throw — which defeats the overflow-resilience goal. Half-budget
|
|
286
|
+
// is the floor: the reserve still accounts for tool schemas but can
|
|
287
|
+
// never starve the mandatory system+latest content.
|
|
288
|
+
const effectiveReserve = Math.min(reserve, Math.floor(budgetTokens * 0.5));
|
|
289
|
+
budgetTokens = Math.max(1, budgetTokens - effectiveReserve);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const sanitized = sanitizeToolPairs(messages);
|
|
293
|
+
// Cache-anchor invariant: when sanitized messages already fit the budget,
|
|
294
|
+
// return them WITHOUT running dedupToolResultBodies. Dedup creates new
|
|
295
|
+
// object refs on every duplicate hit (sha256 match on ≥512B tool result
|
|
296
|
+
// bodies); messagesArrayChanged in loop.mjs detects those new refs as
|
|
297
|
+
// mutation and drops providerState, which kills the server-side
|
|
298
|
+
// previous_response_id chain that xAI Responses / Codex WS rely on for
|
|
299
|
+
// prefix cache reuse. Skip the payload optimization when it would cost
|
|
300
|
+
// a fresh cache cold-start for no token-budget benefit.
|
|
301
|
+
if (estimateMessagesTokens(sanitized) <= budgetTokens) return reconcileDedupStubs(sanitized);
|
|
302
|
+
// Over budget: dedup-then-fit before drop. If post-dedup body fits the
|
|
303
|
+
// budget, return the deduped result without dropping. Both stub and
|
|
304
|
+
// original are still in the message stream so the stub reference is safe.
|
|
305
|
+
// Survivors-only dedup (Pass 1/2 below) still applies when drop is required.
|
|
306
|
+
const deduped = dedupToolResultBodies(sanitized);
|
|
307
|
+
if (estimateMessagesTokens(deduped) <= budgetTokens) return reconcileDedupStubs(deduped);
|
|
308
|
+
|
|
309
|
+
const system = sanitized.filter(m => m.role === 'system');
|
|
310
|
+
const rest = sanitized.filter(m => m.role !== 'system');
|
|
311
|
+
if (rest.length === 0) return reconcileDedupStubs(system);
|
|
312
|
+
|
|
313
|
+
// The most-recent turn must survive intact. When the last message is a tool
|
|
314
|
+
// result, its owning assistant (and that assistant's sibling tool results)
|
|
315
|
+
// form one atomic "last group" — we keep the whole group, never a bare
|
|
316
|
+
// orphan tool result. The boundary is determined strictly by toolCallId
|
|
317
|
+
// ownership (invariant), not by position.
|
|
318
|
+
const lastMsg = rest[rest.length - 1];
|
|
319
|
+
let lastGroupStart = rest.length - 1;
|
|
320
|
+
if (lastMsg && lastMsg.role === 'tool' && lastMsg.toolCallId) {
|
|
321
|
+
const ownerIdx = rest.findIndex(m =>
|
|
322
|
+
m.role === 'assistant' && Array.isArray(m.toolCalls) &&
|
|
323
|
+
m.toolCalls.some(tc => tc.id === lastMsg.toolCallId)
|
|
324
|
+
);
|
|
325
|
+
if (ownerIdx !== -1) lastGroupStart = ownerIdx;
|
|
326
|
+
}
|
|
327
|
+
const lastGroup = rest.slice(lastGroupStart);
|
|
328
|
+
let middle = rest.slice(0, lastGroupStart);
|
|
329
|
+
const baseCost = estimateMessagesTokens(system) + estimateMessagesTokens(lastGroup);
|
|
330
|
+
|
|
331
|
+
// Exact fit (baseCost === budget) must be allowed — only a true overshoot throws.
|
|
332
|
+
if (baseCost > budgetTokens) throw new Error(`trimMessages: cannot fit even system+last group within budget=${budgetTokens} (base=${baseCost})`);
|
|
333
|
+
|
|
334
|
+
// Pass 1: drop tool results oldest-first; drop paired assistant when all its calls are gone.
|
|
335
|
+
let total = estimateMessagesTokens(middle);
|
|
336
|
+
while (total + baseCost > budgetTokens) {
|
|
337
|
+
const toolIdx = middle.findIndex(m => m.role === 'tool');
|
|
338
|
+
if (toolIdx === -1) break;
|
|
339
|
+
const toolCallId = middle[toolIdx].toolCallId;
|
|
340
|
+
total -= estimateMessageTokens(middle[toolIdx]);
|
|
341
|
+
middle.splice(toolIdx, 1);
|
|
342
|
+
if (toolCallId) {
|
|
343
|
+
const assistantIdx = middle.findIndex(m =>
|
|
344
|
+
m.role === 'assistant' && Array.isArray(m.toolCalls) &&
|
|
345
|
+
m.toolCalls.some(tc => tc.id === toolCallId)
|
|
346
|
+
);
|
|
347
|
+
if (assistantIdx !== -1) {
|
|
348
|
+
const assistantMsg = middle[assistantIdx];
|
|
349
|
+
const remainingCalls = assistantMsg.toolCalls.filter(tc =>
|
|
350
|
+
middle.some(m => m.role === 'tool' && m.toolCallId === tc.id)
|
|
351
|
+
);
|
|
352
|
+
if (remainingCalls.length === 0) {
|
|
353
|
+
total -= estimateMessageTokens(assistantMsg);
|
|
354
|
+
middle.splice(assistantIdx, 1);
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
if (total + baseCost <= budgetTokens) {
|
|
361
|
+
const s = sanitizeToolPairs([...system, ...middle, ...lastGroup]);
|
|
362
|
+
if (estimateMessagesTokens(s) <= budgetTokens) return reconcileDedupStubs(dedupToolResultBodies(s));
|
|
363
|
+
// sanitizeToolPairs stub inserts pushed back over budget — fall through to Pass 2.
|
|
364
|
+
// Recompute the last-group span on the POST-sanitize sequence: stubs may have
|
|
365
|
+
// been inserted adjacent to the last group, so using the original lastGroup.length
|
|
366
|
+
// as a tail offset is off-by-N. Locate by matching the lastGroup subsequence
|
|
367
|
+
// from the END of s — indexOf(lastGroup[0]) is wrong when the same object
|
|
368
|
+
// reference repeats earlier in middle (a duplicate would promote part of
|
|
369
|
+
// middle into the locked last group).
|
|
370
|
+
let anchorIdx = -1;
|
|
371
|
+
let lgMatchIdx = lastGroup.length - 1;
|
|
372
|
+
for (let si = s.length - 1; si >= 0 && lgMatchIdx >= 0; si--) {
|
|
373
|
+
if (s[si] === lastGroup[lgMatchIdx]) {
|
|
374
|
+
if (lgMatchIdx === 0) { anchorIdx = si; break; }
|
|
375
|
+
lgMatchIdx--;
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
const postSanitizeLastGroup = anchorIdx === -1 ? lastGroup : s.slice(anchorIdx);
|
|
379
|
+
const nonSystem = s.slice(0, anchorIdx === -1 ? s.length : anchorIdx)
|
|
380
|
+
.filter(m => m.role !== 'system');
|
|
381
|
+
middle = nonSystem;
|
|
382
|
+
// Replace lastGroup binding so downstream baseCost/safety bounds reflect
|
|
383
|
+
// the sanitize-grown tail (stubs inserted around the last group).
|
|
384
|
+
lastGroup.length = 0;
|
|
385
|
+
for (const m of postSanitizeLastGroup) lastGroup.push(m);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Pass 2: drop oldest non-system messages, respecting tool-call group boundaries.
|
|
389
|
+
// Recompute baseCost against the (possibly grown) lastGroup so we don't underflow
|
|
390
|
+
// remaining by the original tail-cost estimate.
|
|
391
|
+
const baseCostP2 = estimateMessagesTokens(system) + estimateMessagesTokens(lastGroup);
|
|
392
|
+
let remaining = budgetTokens - baseCostP2;
|
|
393
|
+
const kept = [];
|
|
394
|
+
// Track which middle messages have already had their cost subtracted from
|
|
395
|
+
// `remaining`. Without this, a paired-assistant charged via the tool→owner
|
|
396
|
+
// lookup gets charged again when the outer loop reaches its own index,
|
|
397
|
+
// double-counting its cost and underflowing `remaining` -> premature break
|
|
398
|
+
// that drops content which still fits.
|
|
399
|
+
const charged = new Set();
|
|
400
|
+
const startIdx = alignBoundaryBackward(middle, middle.length - 1);
|
|
401
|
+
for (let i = startIdx; i >= 0; i--) {
|
|
402
|
+
const m = middle[i];
|
|
403
|
+
const cost = charged.has(m) ? 0 : estimateMessageTokens(m);
|
|
404
|
+
if (remaining - cost < 0) break;
|
|
405
|
+
if (m.role === 'tool' && m.toolCallId) {
|
|
406
|
+
const pairedIdx = middle.findIndex((a, idx) =>
|
|
407
|
+
idx < i && a.role === 'assistant' && Array.isArray(a.toolCalls) &&
|
|
408
|
+
a.toolCalls.some(tc => tc.id === m.toolCallId)
|
|
409
|
+
);
|
|
410
|
+
if (pairedIdx !== -1 && !kept.includes(middle[pairedIdx])) {
|
|
411
|
+
const paired = middle[pairedIdx];
|
|
412
|
+
const pairedCost = charged.has(paired) ? 0 : estimateMessageTokens(paired);
|
|
413
|
+
if (remaining - cost - pairedCost < 0) break;
|
|
414
|
+
remaining -= pairedCost;
|
|
415
|
+
charged.add(paired);
|
|
416
|
+
kept.unshift(paired);
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
if (m.role === 'assistant' && Array.isArray(m.toolCalls)) {
|
|
420
|
+
const toolResultCosts = m.toolCalls.reduce((sum, tc) => {
|
|
421
|
+
const toolMsg = middle.find(t =>
|
|
422
|
+
t.role === 'tool' && t.toolCallId === tc.id && !kept.includes(t)
|
|
423
|
+
);
|
|
424
|
+
return sum + (toolMsg && !charged.has(toolMsg) ? estimateMessageTokens(toolMsg) : 0);
|
|
425
|
+
}, 0);
|
|
426
|
+
if (remaining - cost - toolResultCosts < 0) break;
|
|
427
|
+
for (const tc of m.toolCalls) {
|
|
428
|
+
const toolMsg = middle.find(t =>
|
|
429
|
+
t.role === 'tool' && t.toolCallId === tc.id && !kept.includes(t)
|
|
430
|
+
);
|
|
431
|
+
if (toolMsg) {
|
|
432
|
+
if (!charged.has(toolMsg)) {
|
|
433
|
+
remaining -= estimateMessageTokens(toolMsg);
|
|
434
|
+
charged.add(toolMsg);
|
|
435
|
+
}
|
|
436
|
+
kept.push(toolMsg);
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
if (!charged.has(m)) {
|
|
441
|
+
remaining -= cost;
|
|
442
|
+
charged.add(m);
|
|
443
|
+
}
|
|
444
|
+
if (!kept.includes(m)) kept.unshift(m);
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
const middleOrder = new Map(middle.map((m, idx) => [m, idx]));
|
|
448
|
+
kept.sort((a, b) => (middleOrder.get(a) ?? 0) - (middleOrder.get(b) ?? 0));
|
|
449
|
+
|
|
450
|
+
// Run sanitizeToolPairs before the safety loop; defer dedupToolResultBodies until after
|
|
451
|
+
// so stubs are never left referencing content that the safety loop subsequently drops.
|
|
452
|
+
let result = sanitizeToolPairs([...system, ...kept, ...lastGroup]);
|
|
453
|
+
let safety = 16;
|
|
454
|
+
while (
|
|
455
|
+
safety-- > 0
|
|
456
|
+
&& result.length > system.length + lastGroup.length
|
|
457
|
+
&& estimateMessagesTokens(result) > budgetTokens
|
|
458
|
+
) {
|
|
459
|
+
// Drop a whole aligned tool-group from the head of the non-system region,
|
|
460
|
+
// not a single arbitrary line. Splicing one line at a time can sever an
|
|
461
|
+
// assistant from its tool results (orphaned pair) — sanitizeToolPairs
|
|
462
|
+
// then re-inserts a stub for the dangling tool_use, the byte count drops
|
|
463
|
+
// by less than expected, and the loop oscillates without progress.
|
|
464
|
+
const head = system.length;
|
|
465
|
+
if (head >= result.length) break;
|
|
466
|
+
let dropEnd = head + 1;
|
|
467
|
+
const first = result[head];
|
|
468
|
+
if (first?.role === 'assistant' && Array.isArray(first.toolCalls) && first.toolCalls.length) {
|
|
469
|
+
const callIds = new Set(first.toolCalls.map(tc => tc.id).filter(Boolean));
|
|
470
|
+
while (
|
|
471
|
+
dropEnd < result.length
|
|
472
|
+
&& result[dropEnd]?.role === 'tool'
|
|
473
|
+
&& callIds.has(result[dropEnd].toolCallId)
|
|
474
|
+
) dropEnd++;
|
|
475
|
+
}
|
|
476
|
+
// Refuse to cross into the locked last group.
|
|
477
|
+
const lastGroupStartIdx = result.length - lastGroup.length;
|
|
478
|
+
if (dropEnd > lastGroupStartIdx) break;
|
|
479
|
+
result.splice(head, dropEnd - head);
|
|
480
|
+
result = sanitizeToolPairs(result);
|
|
481
|
+
}
|
|
482
|
+
result = dedupToolResultBodies(result);
|
|
483
|
+
// Final stub-vs-survivor reconciliation: any [duplicate-of tool_use_id=X]
|
|
484
|
+
// pointing at an X no longer present in `result` (carried over from a
|
|
485
|
+
// prior trim and orphaned by this trim's drops) is rewritten to the
|
|
486
|
+
// generic missing-stub so the model never sees a reference to an absent id.
|
|
487
|
+
result = reconcileDedupStubs(result);
|
|
488
|
+
const finalTokens = estimateMessagesTokens(result);
|
|
489
|
+
if (finalTokens > budgetTokens) throw new Error(`trimMessages: exhausted drop strategy, result=${finalTokens} > budget=${budgetTokens}`);
|
|
490
|
+
return result;
|
|
491
|
+
}
|
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
# Bridge Cache-Shard Policy
|
|
2
|
+
|
|
3
|
+
Authoritative policy for prefix-cache shard construction across all bridge sessions. The implementation must satisfy every rule below; reviewers should reject changes that turn stable shared layers into per-call fragments.
|
|
4
|
+
|
|
5
|
+
## Design philosophy
|
|
6
|
+
|
|
7
|
+
Maximize cross-role / cross-call cache reuse by packing every role's policy into a SHARED monolithic prefix. Bridge sessions across all roles see bit-identical BP1 + BP2. User-defined customizations (roles / schedules / webhooks) are baked into BP1 as a fixed-value block — a user edit invalidates BP1 once and the new prefix re-warms across every role together.
|
|
8
|
+
|
|
9
|
+
The only per-call variance lives in BP4 (5m tail). Lead-only fields (e.g. memory recap) never enter bridge sessions.
|
|
10
|
+
|
|
11
|
+
## 4-Block Layout
|
|
12
|
+
|
|
13
|
+
| Block | Role | Cache TTL | Hashed by registry | Content | Variability |
|
|
14
|
+
|---|---|---|---|---|---|
|
|
15
|
+
| BP1 baseRules | system | 1h | YES | bridge common rules + tool/memory/search/explore guidance + DATA_DIR roles/schedules/webhooks (monolithic) | Stable across all bridge calls. Invalidated only when a user edits roles/schedules/webhooks or plugin upgrades. |
|
|
16
|
+
| BP2 roleCatalog | system | 1h | YES | scoped role catalog + project context | Stable for a given provider/project. Project context changes re-hash this block. |
|
|
17
|
+
| BP3 sessionMarker | user (`<system-reminder>` with `<!-- bp3-sentinel -->` marker) | 1h | NO | role-specific task instructions, when present | Stable for that bridge session/task. Empty when no role-specific body is present. |
|
|
18
|
+
| BP4 volatileTail | user (`<system-reminder>`, no sentinel) | 5m | NO | role / permission / task-brief | May vary per call. |
|
|
19
|
+
|
|
20
|
+
**Note on tool schemas (Anthropic):** Anthropic's `cache_control` caches every block from the marked block back through the prefix (order: tools → system → messages). The system block's `cache_control` therefore covers the tool-schema array implicitly, so a separate dedicated tools breakpoint slot is not needed. This frees one of the 4 slots for the messages tail. See `anthropic.mjs:211-214` and `anthropic-oauth.mjs` for the layout decision.
|
|
21
|
+
|
|
22
|
+
**Provider cache classes:** Anthropic uses explicit breakpoints and can be marked warm. OpenAI uses a provider cache key/session prefix and can be tracked as a reusable shard. Gemini uses implicit prompt caching only; hits are observed via `cachedContentTokenCount`, but the registry must not treat that as a guaranteed warm shard.
|
|
23
|
+
|
|
24
|
+
## Hash inputs
|
|
25
|
+
|
|
26
|
+
`registry.markWarm` / `checkWarm` hash exactly two things:
|
|
27
|
+
|
|
28
|
+
1. `JSON.stringify(systemMessages)` — array of leading system-role messages from `session.messages` (BP1 + BP2 only)
|
|
29
|
+
2. `tools.map(t => ({ name, description, inputSchema }))` — the bridge tool array (sorted alphabetically by name for bridge sessions)
|
|
30
|
+
|
|
31
|
+
Anything outside these two inputs MUST NOT influence the registry hash. cwd, role, permission, task-brief, memory recap, files, prompt text, tool results, provider/model/effort/fast — all excluded. Project context is part of BP2 when present, so it is included via the leading system-message hash.
|
|
32
|
+
|
|
33
|
+
## cwd policy
|
|
34
|
+
|
|
35
|
+
- `cwd: null` is a fixed sentinel meaning "no caller workspace context". Internal callers (memory-cycle, scheduler, webhook) pass null deliberately to share one shard.
|
|
36
|
+
- Never upgrade `null` to `process.cwd()` — that defeats fork suppression. The public bridge entry at `src/agent/index.mjs` MUST honor null.
|
|
37
|
+
- Tilde (`~`) and relative paths must be normalized at the entry point. Once inside `prepareBridgeSession`, cwd is either an absolute path or null.
|
|
38
|
+
- cwd does NOT enter the registry hash. cwd-aware tools receive cwd via tool args at call time, not via prompt injection.
|
|
39
|
+
|
|
40
|
+
## What goes where (must / must-not)
|
|
41
|
+
|
|
42
|
+
**baseRules (BP1) MUST contain (monolithic, fixed value):**
|
|
43
|
+
- `lib/rules-builder.cjs` static bridge injection (tool/memory/search/explore guidance)
|
|
44
|
+
- `rules/bridge/00-common.md`
|
|
45
|
+
- `DATA_DIR/roles/*.md` — every role definition aggregated
|
|
46
|
+
- `DATA_DIR/schedules/*/instructions.md` — every schedule aggregated
|
|
47
|
+
- `DATA_DIR/webhooks/*/instructions.md` — every webhook aggregated
|
|
48
|
+
|
|
49
|
+
User edits to roles/schedules/webhooks invalidate BP1 once. This is acceptable because: (a) edits are infrequent, (b) every role re-warms together to the new prefix, (c) keeping all role policies in BP1 maximizes cross-role hit rate compared to per-call branching.
|
|
50
|
+
|
|
51
|
+
**baseRules (BP1) MUST NOT contain:**
|
|
52
|
+
- per-call values (role identity, permission, task brief, memory recap)
|
|
53
|
+
- cwd, project context, file references
|
|
54
|
+
|
|
55
|
+
**roleCatalog (BP2) MUST contain only:**
|
|
56
|
+
- the scoped role catalog selected by `loadScopedRoleCatalog`
|
|
57
|
+
- `# project-context` when project context is present
|
|
58
|
+
|
|
59
|
+
**roleCatalog (BP2) MUST NOT contain:**
|
|
60
|
+
- role / permission / task-brief markers
|
|
61
|
+
- volatile tool results or user task text
|
|
62
|
+
|
|
63
|
+
**sessionMarker (BP3) MUST contain only:**
|
|
64
|
+
- `<!-- bp3-sentinel -->\n<roleSpecific>` when role-specific instructions are present
|
|
65
|
+
- nothing when role-specific instructions are absent — emit no `<system-reminder>` at all in that case
|
|
66
|
+
|
|
67
|
+
**volatileTail (BP4) MUST contain (any subset):**
|
|
68
|
+
- `# role`, `# permission`, `# task-brief`
|
|
69
|
+
|
|
70
|
+
**Lead-only fields — MUST NOT enter bridge sessions:**
|
|
71
|
+
- `# memory-context` — recap / history context. Reserved for Lead session prompt only. Bridge `composeSystemPrompt` callers must not pass `opts.memoryContext` for `opts.owner === 'bridge'`.
|
|
72
|
+
|
|
73
|
+
## Provider Tier3 selection
|
|
74
|
+
|
|
75
|
+
Anthropic provider wrapper auto-marks the first user `<system-reminder>` as BP3 (1h). To prevent volatileTail from being mistaken for BP3:
|
|
76
|
+
|
|
77
|
+
- `sessionMarker` MUST carry the explicit BP3 sentinel `<!-- bp3-sentinel -->` at the head.
|
|
78
|
+
- The provider wrapper MUST mark only sentinel-bearing reminders as 1h. Non-sentinel reminders ride 5m default.
|
|
79
|
+
- When sessionMarker is empty, no BP3 mark is emitted; volatileTail stays at 5m.
|
|
80
|
+
|
|
81
|
+
## Tool list canonicalization
|
|
82
|
+
|
|
83
|
+
Bridge sessions (`opts.owner === 'bridge'`) receive a tool array sorted alphabetically by `tool.name` after the deny-list filter. This eliminates incidental fragmentation from MCP/internal registration order changes.
|
|
84
|
+
|
|
85
|
+
## Entry-point checklist (cwd handling)
|
|
86
|
+
|
|
87
|
+
Every external entry point that constructs a bridge session must satisfy:
|
|
88
|
+
|
|
89
|
+
1. `args.cwd` provided → `normalizeInputPath(args.cwd)` (expand `~`, resolve relative)
|
|
90
|
+
2. `args.cwd` absent and `callerCwd` available → use `callerCwd`
|
|
91
|
+
3. Both absent → `cwd: null`. NEVER fall back to `process.cwd()`.
|
|
92
|
+
|
|
93
|
+
Current entry points (must remain compliant):
|
|
94
|
+
- `src/agent/index.mjs` `case 'bridge'` (Lead-originated MCP dispatch; the unified `bridge` handler routes type=spawn/send/close/list)
|
|
95
|
+
- `src/agent/orchestrator/smart-bridge/bridge-llm.mjs` `makeBridgeLlm` (internal callers)
|
|
96
|
+
|
|
97
|
+
## Trigger map (when prefix transitions)
|
|
98
|
+
|
|
99
|
+
Single transition cause → effect:
|
|
100
|
+
|
|
101
|
+
- Edit `lib/rules-builder.cjs` static block, `agents/*.md`, `rules/bridge/*.md`, or any `DATA_DIR/{roles,schedules,webhooks}/*.md` → BP1 or BP2 hash changes once → all roles/presets re-warm together (acceptable, infrequent)
|
|
102
|
+
- Add/remove an MCP or internal tool → tool_schema_hash changes → all bridge sessions re-warm
|
|
103
|
+
- Project switch → project context may change BP2 → provider shard re-warm/observe under a new prefix hash
|
|
104
|
+
- Per-call (role / permission / task-brief change) → only BP4 5m tail differs; BP1/BP2/BP3 untouched
|
|
105
|
+
|
|
106
|
+
If a single per-call change invalidates more than BP4, the policy has been violated.
|
|
107
|
+
|
|
108
|
+
## Forbidden patterns
|
|
109
|
+
|
|
110
|
+
- per-role tool narrowing in bridge sessions (`opts.allowedTools` whitelist) — fragments BP2 by role group
|
|
111
|
+
- `process.cwd()` fallback in any bridge entry point — breaks the null sentinel
|
|
112
|
+
- emitting cwd or other variant data inside `<system-reminder>` blocks intended for BP3
|
|
113
|
+
- mid-pipeline cwd normalization (do it once at entry)
|
|
114
|
+
- routing memory-context into bridge sessions — Lead-only field
|
|
115
|
+
- changing the registry hash inputs (additions or removals) without updating this document in the same change
|