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,621 @@
|
|
|
1
|
+
import { buildFtsQuery } from './memory-text-utils.mjs'
|
|
2
|
+
import { VALID_CATEGORY, embeddingToSql } from './memory.mjs'
|
|
3
|
+
import { freshnessFactor } from './memory-score.mjs'
|
|
4
|
+
import { buildRecallScopeFilter } from './memory-recall-scope-filter.mjs'
|
|
5
|
+
import { recallReadQuery } from './memory-recall-read-query.mjs'
|
|
6
|
+
|
|
7
|
+
// Per-db cache of mv_hot_active populated state. The main recall path currently
|
|
8
|
+
// uses entries directly; this guard remains for explicit useHotActive callers.
|
|
9
|
+
const _MV_HOT_ACTIVE_TTL_MS = 60_000
|
|
10
|
+
const _mvHotActiveCache = new WeakMap() // db → { populated: boolean, ts: number }
|
|
11
|
+
const SEMANTIC_ONLY_MIN_SIM = 0.72
|
|
12
|
+
const SHORT_QUERY_TOKEN_MAX = 2
|
|
13
|
+
|
|
14
|
+
function buildExactTerms(query) {
|
|
15
|
+
const clean = String(query ?? '').replace(/\s+/g, ' ').trim()
|
|
16
|
+
if (!clean) return []
|
|
17
|
+
const terms = []
|
|
18
|
+
const add = (value) => {
|
|
19
|
+
const term = String(value ?? '').trim()
|
|
20
|
+
.replace(/^[^\p{L}\p{N}_./:-]+|[^\p{L}\p{N}_./:-]+$/gu, '')
|
|
21
|
+
if (!term) return
|
|
22
|
+
const hasIdentifierShape = /[_./:-]/.test(term)
|
|
23
|
+
if (!hasIdentifierShape && /^[\d\s]+$/.test(term)) return
|
|
24
|
+
const symbolCount = Array.from(term).length
|
|
25
|
+
if (!hasIdentifierShape && symbolCount < 2) return
|
|
26
|
+
terms.push(term.slice(0, 80))
|
|
27
|
+
}
|
|
28
|
+
if (clean.length <= 80) add(clean)
|
|
29
|
+
const tokens = clean.match(/[\p{L}\p{N}_./:-]+/gu) || []
|
|
30
|
+
for (const token of tokens) add(token)
|
|
31
|
+
for (let i = 0; i < tokens.length - 1; i++) {
|
|
32
|
+
add(`${tokens[i]} ${tokens[i + 1]}`)
|
|
33
|
+
}
|
|
34
|
+
return [...new Set(terms.map(t => t.toLowerCase()))].slice(0, 12)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function countQueryTokens(query) {
|
|
38
|
+
const clean = String(query ?? '').replace(/\s+/g, ' ').trim()
|
|
39
|
+
if (!clean) return 0
|
|
40
|
+
return (clean.match(/[\p{L}\p{N}_./:-]+/gu) || [])
|
|
41
|
+
.filter(token => String(token ?? '').trim().length > 0)
|
|
42
|
+
.length
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function hasFullQueryTextMatch(query, row) {
|
|
46
|
+
const clean = String(query ?? '').replace(/\s+/g, ' ').trim().toLowerCase()
|
|
47
|
+
if (!clean || clean.length > 160) return false
|
|
48
|
+
const element = String(row?.element ?? '').toLowerCase()
|
|
49
|
+
const summary = String(row?.summary ?? '').toLowerCase()
|
|
50
|
+
const content = String(row?.content ?? '').toLowerCase()
|
|
51
|
+
return (element && element.includes(clean))
|
|
52
|
+
|| (summary && summary.includes(clean))
|
|
53
|
+
|| (content && content.includes(clean))
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function hasQueryTokenCoverage(row, queryTokenCount) {
|
|
57
|
+
if (queryTokenCount <= SHORT_QUERY_TOKEN_MAX) return true
|
|
58
|
+
const hits = Number(row?.exact_hits)
|
|
59
|
+
return Number.isFinite(hits) && hits >= queryTokenCount
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function exactTextBoost(query, row, exactHits) {
|
|
63
|
+
const clean = String(query ?? '').trim().toLowerCase()
|
|
64
|
+
if (!clean) return 0
|
|
65
|
+
const element = String(row?.element ?? '').toLowerCase()
|
|
66
|
+
const summary = String(row?.summary ?? '').toLowerCase()
|
|
67
|
+
const content = String(row?.content ?? '').toLowerCase()
|
|
68
|
+
let boost = 0
|
|
69
|
+
if (element && element.includes(clean)) boost += 0.035
|
|
70
|
+
if (summary && summary.includes(clean)) boost += 0.025
|
|
71
|
+
if (content && content.includes(clean)) boost += 0.015
|
|
72
|
+
const hits = Number(exactHits)
|
|
73
|
+
if (Number.isFinite(hits) && hits > 0) boost += Math.min(0.03, hits * 0.008)
|
|
74
|
+
return boost
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function _checkMvHotActivePopulated(db) {
|
|
78
|
+
const cached = _mvHotActiveCache.get(db)
|
|
79
|
+
const now = Date.now()
|
|
80
|
+
if (cached && now - cached.ts < _MV_HOT_ACTIVE_TTL_MS) return cached.populated
|
|
81
|
+
const r = await recallReadQuery(
|
|
82
|
+
db,
|
|
83
|
+
`SELECT relispopulated FROM pg_class WHERE relname = 'mv_hot_active' LIMIT 1`,
|
|
84
|
+
)
|
|
85
|
+
if (!r.rows?.length) throw new Error('mv_hot_active not found in pg_class')
|
|
86
|
+
const populated = Boolean(r.rows[0].relispopulated)
|
|
87
|
+
_mvHotActiveCache.set(db, { populated, ts: now })
|
|
88
|
+
return populated
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function searchRelevantHybrid(db, query, options = {}) {
|
|
92
|
+
const clean = String(query ?? '').trim()
|
|
93
|
+
if (!clean) return []
|
|
94
|
+
// Numeric-only lookup is too broad for text recall ("1" matches nearly
|
|
95
|
+
// everything through the short ILIKE path). Callers that know an entry id
|
|
96
|
+
// should use recall's `id` mode instead of query search.
|
|
97
|
+
if (/^\d+$/.test(clean)) return []
|
|
98
|
+
|
|
99
|
+
const limit = Math.max(1, Math.floor(Number(options?.limit ?? 8)))
|
|
100
|
+
const candidateWindow = Math.max(40, limit * 8)
|
|
101
|
+
const includeMembers = Boolean(options.includeMembers)
|
|
102
|
+
const writeBackMemberHits = options.writeBackMemberHits !== false
|
|
103
|
+
// Pre-filter knobs. Without them, FTS/vec rank the whole tree and a
|
|
104
|
+
// post-filter time window can wipe the result set.
|
|
105
|
+
const tsFrom = Number.isFinite(Number(options.ts_from)) ? Number(options.ts_from) : null
|
|
106
|
+
const tsTo = Number.isFinite(Number(options.ts_to)) ? Number(options.ts_to) : null
|
|
107
|
+
// Default = empty exclusion. The archive bucket holds the bulk of historical
|
|
108
|
+
// work (active is reserved for permanent invariants in this design; the
|
|
109
|
+
// last-week / last-month / "what did I work on previously" recall pattern
|
|
110
|
+
// depends on archived rows being in the pool). Cycle2 internal sweeps
|
|
111
|
+
// that genuinely want active-only data must pass excludeStatuses
|
|
112
|
+
// explicitly.
|
|
113
|
+
const excludeStatuses = Array.isArray(options.excludeStatuses)
|
|
114
|
+
? options.excludeStatuses.filter(s => typeof s === 'string' && s.trim()).map(s => s.trim().toLowerCase())
|
|
115
|
+
: []
|
|
116
|
+
// Project scope pre-filter applied to the candidate fetch SQL.
|
|
117
|
+
// 'common' → project_id IS NULL; specific slug → project_id IS NULL OR = slug;
|
|
118
|
+
// 'all' or undefined → no filter.
|
|
119
|
+
const projectScope = typeof options.projectScope === 'string' ? options.projectScope : null
|
|
120
|
+
const categories = (Array.isArray(options.category) ? options.category : [options.category])
|
|
121
|
+
.map(c => String(c ?? '').trim().toLowerCase())
|
|
122
|
+
.filter(c => VALID_CATEGORY.has(c))
|
|
123
|
+
// Caller can disable freshness decay when the period is calendar-bounded
|
|
124
|
+
// (yesterday/today/this_week/last_week/specific date). Inside a fixed
|
|
125
|
+
// window, absolute-age decay misranks early-week vs late-week entries.
|
|
126
|
+
const applyFreshness = options.applyFreshness !== false
|
|
127
|
+
|
|
128
|
+
// ── mv_hot_active fast-path opt-in ──────────────────────────────────────
|
|
129
|
+
// When useHotActive:true, the dense and sparse CTE legs query mv_hot_active
|
|
130
|
+
// instead of the full entries table.
|
|
131
|
+
//
|
|
132
|
+
// WHEN TO USE:
|
|
133
|
+
// - Explicit active-only recall (no archived inclusion, no ts_from/ts_to
|
|
134
|
+
// window). The history-first default recall path should keep useHotActive
|
|
135
|
+
// false so archived roots and fresh pending work remain eligible.
|
|
136
|
+
// - mv_hot_active holds only active roots with embeddings. Its dedicated
|
|
137
|
+
// HNSW (mv_hot_active_hnsw) and GIN (mv_hot_active_tsv) indexes are smaller
|
|
138
|
+
// than the partial indexes on entries, so ANN and FTS scans are faster.
|
|
139
|
+
// - Caller must ensure cycle2 has run at least once. The MV is created WITH NO
|
|
140
|
+
// DATA; a never-refreshed MV silently returns 0 rows — primary risk on fresh
|
|
141
|
+
// deployments.
|
|
142
|
+
//
|
|
143
|
+
// WHEN NOT TO USE:
|
|
144
|
+
// - ts_from / ts_to active: MV lacks the ts column; the filter clause would
|
|
145
|
+
// reference a non-existent column and the query would error.
|
|
146
|
+
// - Archived entries must be included: MV only holds active rows.
|
|
147
|
+
// - trgm is the primary signal: MV lacks content and ts, so the trgm leg
|
|
148
|
+
// always routes to entries regardless of useHotActive.
|
|
149
|
+
//
|
|
150
|
+
// COLUMN GAPS (resolved per CTE leg):
|
|
151
|
+
// ts : missing → trgm short-query ORDER BY ts DESC impossible on MV;
|
|
152
|
+
// also makes ts_from/ts_to filter clauses invalid.
|
|
153
|
+
// content : missing → trgm similarity/ILIKE impossible on MV.
|
|
154
|
+
// Both gaps are intentional; trgm is unconditionally routed to entries.
|
|
155
|
+
//
|
|
156
|
+
// The combined/JOIN fetch after the CTE always queries entries by id, so the
|
|
157
|
+
// final row shape is identical regardless of which path was taken.
|
|
158
|
+
const hasTsFilter = tsFrom != null || tsTo != null
|
|
159
|
+
const hasArchivedInclusion = !excludeStatuses.includes('archived')
|
|
160
|
+
let useHotActive = Boolean(options.useHotActive)
|
|
161
|
+
&& !hasTsFilter
|
|
162
|
+
&& !hasArchivedInclusion
|
|
163
|
+
// Guard against unrefreshed mv_hot_active (created WITH NO DATA → SQLSTATE
|
|
164
|
+
// 55000 on read). Cheap pg_class check, cached 60 s per db handle to avoid
|
|
165
|
+
// per-recall round-trip cost.
|
|
166
|
+
if (useHotActive) {
|
|
167
|
+
const populated = await _checkMvHotActivePopulated(db)
|
|
168
|
+
if (!populated) useHotActive = false
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// buildFilterClause: pushes ts/status/scope filters INTO candidate SELECTs.
|
|
172
|
+
// offset = 1-based index of the first bind param it may consume.
|
|
173
|
+
// Returns { clause: string, params: any[] }; clause begins with AND or is ''.
|
|
174
|
+
function buildFilterClause(offset) {
|
|
175
|
+
return buildRecallScopeFilter(offset, {
|
|
176
|
+
ts_from: tsFrom,
|
|
177
|
+
ts_to: tsTo,
|
|
178
|
+
excludeStatuses,
|
|
179
|
+
category: categories,
|
|
180
|
+
projectScope,
|
|
181
|
+
})
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
// Kept for the non-candidate root-lookup inside the member-hit resolution path.
|
|
185
|
+
function buildScopeClause(offset) {
|
|
186
|
+
if (projectScope === 'common') {
|
|
187
|
+
return { clause: 'AND project_id IS NULL', params: [] }
|
|
188
|
+
} else if (projectScope && projectScope !== 'all') {
|
|
189
|
+
return { clause: `AND (project_id IS NULL OR project_id = $${offset})`, params: [projectScope] }
|
|
190
|
+
}
|
|
191
|
+
return { clause: '', params: [] }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ── Single-round-trip hybrid CTE ─────────────────────────────────────────
|
|
195
|
+
// Param layout (fixed prefix):
|
|
196
|
+
// $1 = halfvec literal (NULL when no queryVector)
|
|
197
|
+
// $2 = tsQuery text (NULL when short query)
|
|
198
|
+
// $3 = cleanText (trigram term)
|
|
199
|
+
// $4 = candidateWindow (LIMIT for each CTE leg)
|
|
200
|
+
// $5+ = filter params (ts_from, ts_to, excludeStatuses..., category..., projectScope slug)
|
|
201
|
+
//
|
|
202
|
+
// When a leg is inapplicable its CTE returns no rows; the UNION + LEFT JOINs
|
|
203
|
+
// handle that cleanly. dense/sparse/trgm legs each re-use the same filter
|
|
204
|
+
// params starting at $5 since they live in independent CTE scopes.
|
|
205
|
+
|
|
206
|
+
const vecSql = (Array.isArray(options.queryVector) && options.queryVector.length > 0)
|
|
207
|
+
? embeddingToSql(options.queryVector)
|
|
208
|
+
: null
|
|
209
|
+
|
|
210
|
+
const ftsQuery = clean.length >= 3 ? (buildFtsQuery(clean) ?? null) : null
|
|
211
|
+
const exactTerms = buildExactTerms(clean)
|
|
212
|
+
const queryTokenCount = countQueryTokens(clean)
|
|
213
|
+
const minExactHits = exactTerms.length >= 8 ? 3 : exactTerms.length >= 4 ? 2 : 1
|
|
214
|
+
|
|
215
|
+
// For very short queries (< 3 chars) the trigram operator still works but
|
|
216
|
+
// we relax the server-side threshold via set_limit() — however that requires
|
|
217
|
+
// a separate round-trip. Instead we fall back to a plain ILIKE scan for
|
|
218
|
+
// short text (rare edge case; sequential scan is acceptable for < 3 chars).
|
|
219
|
+
const isShortQuery = clean.length < 3
|
|
220
|
+
|
|
221
|
+
// $5 onward are the filter params for the entries legs (non-MV path).
|
|
222
|
+
// Each CTE leg duplicates the same positional params because they live in
|
|
223
|
+
// independent SELECT scopes. When useHotActive=true, the trgm leg still uses
|
|
224
|
+
// these params but at adjusted offsets (see activeBindParams below).
|
|
225
|
+
const { clause: filterClause, params: filterParams } = buildFilterClause(5)
|
|
226
|
+
|
|
227
|
+
// MV-specific filter: only category/projectScope matter (status='active' and
|
|
228
|
+
// embedding IS NOT NULL are baked into mv_hot_active; ts_from/ts_to are
|
|
229
|
+
// unavailable since MV lacks the ts column).
|
|
230
|
+
function buildMvFilterClause(offset) {
|
|
231
|
+
const clauses = []
|
|
232
|
+
const params = []
|
|
233
|
+
let next = offset
|
|
234
|
+
if (categories.length > 0) {
|
|
235
|
+
const placeholders = categories.map(() => `$${next++}`).join(', ')
|
|
236
|
+
clauses.push(`category IN (${placeholders})`)
|
|
237
|
+
params.push(...categories)
|
|
238
|
+
}
|
|
239
|
+
if (projectScope === 'common') {
|
|
240
|
+
clauses.push('project_id IS NULL')
|
|
241
|
+
} else if (projectScope && projectScope !== 'all') {
|
|
242
|
+
clauses.push(`(project_id IS NULL OR project_id = $${next++})`)
|
|
243
|
+
params.push(projectScope)
|
|
244
|
+
}
|
|
245
|
+
return { clause: clauses.length > 0 ? `AND ${clauses.join(' AND ')}` : '', params }
|
|
246
|
+
}
|
|
247
|
+
// mvBindParams layout when useHotActive=true:
|
|
248
|
+
// $1–$4 : same prefix (vec, fts, clean, window)
|
|
249
|
+
// $5+ : mvFilterParams (category filters + optional projectScope slug)
|
|
250
|
+
// $5+N+ : trgmFilterParams (ts/status/scope for the entries-only trgm leg)
|
|
251
|
+
//
|
|
252
|
+
// The trgm CTE always targets entries and needs the full filter (excludeStatuses,
|
|
253
|
+
// ts_from, ts_to, category, projectScope). When useHotActive=true, trgm filter params
|
|
254
|
+
// start AFTER mvFilterParams so positional params align correctly in the
|
|
255
|
+
// combined bind array.
|
|
256
|
+
const { clause: mvFilterClause, params: mvFilterParams } = buildMvFilterClause(5)
|
|
257
|
+
// trgm filter: when useHotActive, build starting at offset 5 + mvFilterParams.length.
|
|
258
|
+
const trgmFilterOffset = useHotActive ? 5 + mvFilterParams.length : 5
|
|
259
|
+
const { clause: trgmFilterClause, params: trgmFilterParams } = buildFilterClause(trgmFilterOffset)
|
|
260
|
+
// activeBindParams is the single array passed to db.query for the full hybrid SQL.
|
|
261
|
+
// Non-MV path: [vec,fts,clean,window, ...filterParams] (filterClause == trgmFilterClause).
|
|
262
|
+
// MV path: [vec,fts,clean,window, ...mvFilterParams, ...trgmFilterParams].
|
|
263
|
+
const recallScopeOpts = {
|
|
264
|
+
ts_from: tsFrom,
|
|
265
|
+
ts_to: tsTo,
|
|
266
|
+
excludeStatuses,
|
|
267
|
+
category: categories,
|
|
268
|
+
projectScope,
|
|
269
|
+
}
|
|
270
|
+
const exactTermsParam = useHotActive
|
|
271
|
+
? 5 + mvFilterParams.length + trgmFilterParams.length
|
|
272
|
+
: 5 + filterParams.length
|
|
273
|
+
const exactFilterClause = buildRecallScopeFilter(
|
|
274
|
+
useHotActive ? trgmFilterOffset : 5,
|
|
275
|
+
recallScopeOpts,
|
|
276
|
+
'ee',
|
|
277
|
+
).clause
|
|
278
|
+
const activeBindParams = useHotActive
|
|
279
|
+
? [vecSql, ftsQuery, clean, candidateWindow, ...mvFilterParams, ...trgmFilterParams, ...(exactTerms.length > 0 ? [exactTerms] : [])]
|
|
280
|
+
: [vecSql, ftsQuery, clean, candidateWindow, ...filterParams, ...(exactTerms.length > 0 ? [exactTerms] : [])]
|
|
281
|
+
|
|
282
|
+
// dense CTE: active only when a query vector is supplied.
|
|
283
|
+
// useHotActive → queries mv_hot_active (smaller HNSW, no ts/content needed).
|
|
284
|
+
const denseCte = vecSql ? (useHotActive ? `
|
|
285
|
+
dense AS (
|
|
286
|
+
SELECT id,
|
|
287
|
+
1 - (embedding <=> $1::halfvec) AS sim,
|
|
288
|
+
ROW_NUMBER() OVER (ORDER BY embedding <=> $1::halfvec) AS dense_rank
|
|
289
|
+
FROM mv_hot_active
|
|
290
|
+
WHERE true
|
|
291
|
+
${mvFilterClause}
|
|
292
|
+
ORDER BY embedding <=> $1::halfvec
|
|
293
|
+
LIMIT $4
|
|
294
|
+
),` : `
|
|
295
|
+
dense AS (
|
|
296
|
+
SELECT id,
|
|
297
|
+
1 - (embedding <=> $1::halfvec) AS sim,
|
|
298
|
+
ROW_NUMBER() OVER (ORDER BY embedding <=> $1::halfvec) AS dense_rank
|
|
299
|
+
FROM entries
|
|
300
|
+
WHERE embedding IS NOT NULL
|
|
301
|
+
${filterClause}
|
|
302
|
+
ORDER BY embedding <=> $1::halfvec
|
|
303
|
+
LIMIT $4
|
|
304
|
+
),`) : `
|
|
305
|
+
dense AS (SELECT NULL::bigint AS id, NULL::float8 AS sim, NULL::bigint AS dense_rank WHERE $1::halfvec IS NOT NULL AND false),`
|
|
306
|
+
|
|
307
|
+
// sparse CTE: active only when ftsQuery is non-null.
|
|
308
|
+
// useHotActive → queries mv_hot_active GIN index (mv_hot_active_tsv).
|
|
309
|
+
const sparseCte = ftsQuery ? (useHotActive ? `
|
|
310
|
+
sparse AS (
|
|
311
|
+
SELECT id,
|
|
312
|
+
ts_rank_cd(search_tsv, websearch_to_tsquery('simple', $2)) AS lex,
|
|
313
|
+
ROW_NUMBER() OVER (ORDER BY ts_rank_cd(search_tsv, websearch_to_tsquery('simple', $2)) DESC) AS sparse_rank
|
|
314
|
+
FROM mv_hot_active
|
|
315
|
+
WHERE search_tsv @@ websearch_to_tsquery('simple', $2)
|
|
316
|
+
${mvFilterClause}
|
|
317
|
+
ORDER BY lex DESC
|
|
318
|
+
LIMIT $4
|
|
319
|
+
),` : `
|
|
320
|
+
sparse AS (
|
|
321
|
+
SELECT id,
|
|
322
|
+
ts_rank_cd(search_tsv, websearch_to_tsquery('simple', $2)) AS lex,
|
|
323
|
+
ROW_NUMBER() OVER (ORDER BY ts_rank_cd(search_tsv, websearch_to_tsquery('simple', $2)) DESC) AS sparse_rank
|
|
324
|
+
FROM entries
|
|
325
|
+
WHERE search_tsv @@ websearch_to_tsquery('simple', $2)
|
|
326
|
+
${filterClause}
|
|
327
|
+
ORDER BY lex DESC
|
|
328
|
+
LIMIT $4
|
|
329
|
+
),`) : `
|
|
330
|
+
sparse AS (SELECT NULL::bigint AS id, NULL::float8 AS lex, NULL::bigint AS sparse_rank WHERE $2::text IS NOT NULL AND false),`
|
|
331
|
+
|
|
332
|
+
// trgm CTE: pg_trgm similarity path. For short queries (< 3 chars) the %
|
|
333
|
+
// operator is unreliable (trigrams need at least 3 chars); use ILIKE instead.
|
|
334
|
+
// NOTE: trgm always queries entries regardless of useHotActive — mv_hot_active
|
|
335
|
+
// lacks the content column (trgm/ILIKE) and ts column (short-query ORDER BY).
|
|
336
|
+
// Uses trgmFilterClause whose $N offsets are aligned to activeBindParams.
|
|
337
|
+
const trgmCte = isShortQuery ? `
|
|
338
|
+
trgm AS (
|
|
339
|
+
SELECT id,
|
|
340
|
+
0.5::float8 AS trg_sim,
|
|
341
|
+
ROW_NUMBER() OVER (ORDER BY ts DESC) AS trgm_rank
|
|
342
|
+
FROM entries
|
|
343
|
+
WHERE (content ILIKE '%' || $3 || '%' OR element ILIKE '%' || $3 || '%')
|
|
344
|
+
${trgmFilterClause}
|
|
345
|
+
ORDER BY ts DESC
|
|
346
|
+
LIMIT $4
|
|
347
|
+
),` : `
|
|
348
|
+
trgm AS (
|
|
349
|
+
SELECT id,
|
|
350
|
+
GREATEST(
|
|
351
|
+
CASE WHEN content ILIKE '%' || $3 || '%' THEN 1.0 ELSE similarity(content, $3) END,
|
|
352
|
+
CASE WHEN coalesce(element, '') ILIKE '%' || $3 || '%' THEN 1.0 ELSE similarity(coalesce(element, ''), $3) END,
|
|
353
|
+
CASE WHEN coalesce(summary, '') ILIKE '%' || $3 || '%' THEN 1.0 ELSE similarity(coalesce(summary, ''), $3) END
|
|
354
|
+
) AS trg_sim,
|
|
355
|
+
ROW_NUMBER() OVER (ORDER BY GREATEST(
|
|
356
|
+
CASE WHEN content ILIKE '%' || $3 || '%' THEN 1.0 ELSE similarity(content, $3) END,
|
|
357
|
+
CASE WHEN coalesce(element, '') ILIKE '%' || $3 || '%' THEN 1.0 ELSE similarity(coalesce(element, ''), $3) END,
|
|
358
|
+
CASE WHEN coalesce(summary, '') ILIKE '%' || $3 || '%' THEN 1.0 ELSE similarity(coalesce(summary, ''), $3) END
|
|
359
|
+
) DESC) AS trgm_rank
|
|
360
|
+
FROM entries
|
|
361
|
+
WHERE (
|
|
362
|
+
content % $3 OR element % $3 OR summary % $3
|
|
363
|
+
OR content ILIKE '%' || $3 || '%'
|
|
364
|
+
OR coalesce(element, '') ILIKE '%' || $3 || '%'
|
|
365
|
+
OR coalesce(summary, '') ILIKE '%' || $3 || '%'
|
|
366
|
+
)
|
|
367
|
+
AND GREATEST(
|
|
368
|
+
CASE WHEN content ILIKE '%' || $3 || '%' THEN 1.0 ELSE similarity(content, $3) END,
|
|
369
|
+
CASE WHEN coalesce(element, '') ILIKE '%' || $3 || '%' THEN 1.0 ELSE similarity(coalesce(element, ''), $3) END,
|
|
370
|
+
CASE WHEN coalesce(summary, '') ILIKE '%' || $3 || '%' THEN 1.0 ELSE similarity(coalesce(summary, ''), $3) END
|
|
371
|
+
) >= 0.10
|
|
372
|
+
${trgmFilterClause}
|
|
373
|
+
ORDER BY trg_sim DESC
|
|
374
|
+
LIMIT $4
|
|
375
|
+
),`
|
|
376
|
+
|
|
377
|
+
const exactCte = exactTerms.length > 0 ? `
|
|
378
|
+
exact AS (
|
|
379
|
+
SELECT ee.id,
|
|
380
|
+
COUNT(*)::float8 AS exact_hits,
|
|
381
|
+
ROW_NUMBER() OVER (ORDER BY COUNT(*) DESC, ee.ts DESC) AS exact_rank
|
|
382
|
+
FROM entries ee
|
|
383
|
+
JOIN LATERAL unnest($${exactTermsParam}::text[]) AS q(term) ON (
|
|
384
|
+
ee.content ILIKE '%' || q.term || '%'
|
|
385
|
+
OR coalesce(ee.element, '') ILIKE '%' || q.term || '%'
|
|
386
|
+
OR coalesce(ee.summary, '') ILIKE '%' || q.term || '%'
|
|
387
|
+
)
|
|
388
|
+
WHERE true
|
|
389
|
+
${exactFilterClause}
|
|
390
|
+
GROUP BY ee.id, ee.ts
|
|
391
|
+
HAVING COUNT(*) >= ${minExactHits}
|
|
392
|
+
ORDER BY exact_hits DESC, ee.ts DESC
|
|
393
|
+
LIMIT $4
|
|
394
|
+
),` : `
|
|
395
|
+
exact AS (SELECT NULL::bigint AS id, NULL::float8 AS exact_hits, NULL::bigint AS exact_rank WHERE false),`
|
|
396
|
+
|
|
397
|
+
const hybridSql = `
|
|
398
|
+
WITH
|
|
399
|
+
${denseCte}
|
|
400
|
+
${sparseCte}
|
|
401
|
+
${trgmCte}
|
|
402
|
+
${exactCte}
|
|
403
|
+
combined AS (
|
|
404
|
+
SELECT id FROM dense WHERE id IS NOT NULL UNION
|
|
405
|
+
SELECT id FROM sparse WHERE id IS NOT NULL UNION
|
|
406
|
+
SELECT id FROM trgm WHERE id IS NOT NULL UNION
|
|
407
|
+
SELECT id FROM exact WHERE id IS NOT NULL
|
|
408
|
+
)
|
|
409
|
+
SELECT
|
|
410
|
+
e.id, e.element, e.summary, e.category, e.status, e.score,
|
|
411
|
+
e.last_seen_at, e.ts, e.project_id, e.session_id, e.source_ref,
|
|
412
|
+
e.source_turn, e.content, e.chunk_root, e.is_root,
|
|
413
|
+
e.role,
|
|
414
|
+
d.sim AS dense_sim,
|
|
415
|
+
d.dense_rank,
|
|
416
|
+
s.lex AS sparse_lex,
|
|
417
|
+
s.sparse_rank,
|
|
418
|
+
t.trg_sim,
|
|
419
|
+
t.trgm_rank,
|
|
420
|
+
x.exact_hits,
|
|
421
|
+
x.exact_rank
|
|
422
|
+
FROM combined c
|
|
423
|
+
JOIN entries e ON e.id = c.id
|
|
424
|
+
LEFT JOIN dense d ON d.id = c.id
|
|
425
|
+
LEFT JOIN sparse s ON s.id = c.id
|
|
426
|
+
LEFT JOIN trgm t ON t.id = c.id
|
|
427
|
+
LEFT JOIN exact x ON x.id = c.id`
|
|
428
|
+
|
|
429
|
+
let rawRows = []
|
|
430
|
+
let denseCount = 0
|
|
431
|
+
let sparseCount = 0
|
|
432
|
+
let trgmCount = 0
|
|
433
|
+
let exactCount = 0
|
|
434
|
+
|
|
435
|
+
try {
|
|
436
|
+
const { rows } = await recallReadQuery(db, hybridSql, activeBindParams)
|
|
437
|
+
rawRows = rows
|
|
438
|
+
// Count how many rows each leg contributed (a row may appear in multiple legs).
|
|
439
|
+
for (const r of rawRows) {
|
|
440
|
+
if (r.dense_rank != null) denseCount++
|
|
441
|
+
if (r.sparse_rank != null) sparseCount++
|
|
442
|
+
if (r.trgm_rank != null) trgmCount++
|
|
443
|
+
if (r.exact_rank != null) exactCount++
|
|
444
|
+
}
|
|
445
|
+
} catch (err) {
|
|
446
|
+
process.stderr.write(`[recall] hybrid CTE failed: ${err.message}\n`)
|
|
447
|
+
return []
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
if (rawRows.length === 0) return []
|
|
451
|
+
|
|
452
|
+
// ── JS-side RRF merge (unchanged logic) ──────────────────────────────────
|
|
453
|
+
// K=60 is the standard RRF constant from Cormack et al. (SIGIR 2009).
|
|
454
|
+
const K = 60
|
|
455
|
+
const nowMs = Date.now()
|
|
456
|
+
|
|
457
|
+
const scoredAll = rawRows.map(row => {
|
|
458
|
+
const id = Number(row.id)
|
|
459
|
+
const denseRank = row.dense_rank != null ? Number(row.dense_rank) : null
|
|
460
|
+
const sparseRank = row.sparse_rank != null ? Number(row.sparse_rank) : null
|
|
461
|
+
const trgmRank = row.trgm_rank != null ? Number(row.trgm_rank) : null
|
|
462
|
+
const exactRank = row.exact_rank != null ? Number(row.exact_rank) : null
|
|
463
|
+
const rrf = (denseRank ? 1 / (K + denseRank) : 0)
|
|
464
|
+
+ (sparseRank ? 1 / (K + sparseRank) : 0)
|
|
465
|
+
+ (trgmRank ? 1 / (K + trgmRank) : 0)
|
|
466
|
+
+ (exactRank ? 1 / (K + exactRank) : 0)
|
|
467
|
+
const freshness = applyFreshness ? freshnessFactor(row.ts, nowMs) : 1.0
|
|
468
|
+
const boost = exactTextBoost(clean, row, row.exact_hits)
|
|
469
|
+
return { id, row, rrf, freshness, retrievalScore: (rrf * freshness) + boost }
|
|
470
|
+
})
|
|
471
|
+
let semanticOnlyDropped = 0
|
|
472
|
+
let weakTextDropped = 0
|
|
473
|
+
const scored = scoredAll.filter(({ row }) => {
|
|
474
|
+
const hasTextSupport = row.sparse_rank != null || row.trgm_rank != null || row.exact_rank != null
|
|
475
|
+
const sim = Number(row.dense_sim)
|
|
476
|
+
const hasSemanticSupport = Number.isFinite(sim) && sim >= SEMANTIC_ONLY_MIN_SIM
|
|
477
|
+
if (!hasTextSupport) {
|
|
478
|
+
if (hasSemanticSupport) return true
|
|
479
|
+
semanticOnlyDropped += 1
|
|
480
|
+
return false
|
|
481
|
+
}
|
|
482
|
+
// A long query that only shares a weak trigram/exact tail with old rows
|
|
483
|
+
// should not fill the page with accidental matches. Accept lexical-only
|
|
484
|
+
// rows when the query is a short keyword/identifier lookup, a full phrase
|
|
485
|
+
// match, or an FTS hit. Otherwise require semantic support as a second
|
|
486
|
+
// independent signal.
|
|
487
|
+
if (categories.length > 0) return true
|
|
488
|
+
const hasFullPhrase = hasFullQueryTextMatch(clean, row)
|
|
489
|
+
if (hasFullPhrase) return true
|
|
490
|
+
if (queryTokenCount <= SHORT_QUERY_TOKEN_MAX) return true
|
|
491
|
+
if (row.sparse_rank != null) return true
|
|
492
|
+
if (hasSemanticSupport && hasQueryTokenCoverage(row, queryTokenCount)) return true
|
|
493
|
+
weakTextDropped += 1
|
|
494
|
+
return false
|
|
495
|
+
})
|
|
496
|
+
if (scored.length === 0) return []
|
|
497
|
+
scored.sort((a, b) => b.retrievalScore - a.retrievalScore || b.rrf - a.rrf)
|
|
498
|
+
|
|
499
|
+
const filtered = scored
|
|
500
|
+
|
|
501
|
+
// ── Root resolution + member-hit write-back ───────────────────────────────
|
|
502
|
+
const byId = new Map(rawRows.map(r => [Number(r.id), r]))
|
|
503
|
+
const memberHitRootIds = new Set()
|
|
504
|
+
const rootIdsForReturn = []
|
|
505
|
+
const seen = new Set()
|
|
506
|
+
|
|
507
|
+
for (const { id, rrf, retrievalScore } of filtered) {
|
|
508
|
+
const row = byId.get(id)
|
|
509
|
+
if (!row) continue
|
|
510
|
+
let targetRow = null
|
|
511
|
+
if (row.is_root === 1) {
|
|
512
|
+
targetRow = row
|
|
513
|
+
} else if (row.chunk_root != null && row.chunk_root !== row.id) {
|
|
514
|
+
// $1 = chunk_root id, scope param (if any) = $2
|
|
515
|
+
const { clause: rootScopeClause, params: rootScopeParams } = buildScopeClause(2)
|
|
516
|
+
const { rows: rootRows } = await recallReadQuery(
|
|
517
|
+
db,
|
|
518
|
+
`SELECT id, ts, role, content, session_id, source_turn, chunk_root, is_root,
|
|
519
|
+
element, category, summary, project_id, status, score, last_seen_at
|
|
520
|
+
FROM entries WHERE id = $1 AND is_root = 1 ${rootScopeClause}`,
|
|
521
|
+
[row.chunk_root, ...rootScopeParams],
|
|
522
|
+
)
|
|
523
|
+
const r = rootRows[0]
|
|
524
|
+
if (!r) continue
|
|
525
|
+
memberHitRootIds.add(r.id)
|
|
526
|
+
targetRow = r
|
|
527
|
+
} else {
|
|
528
|
+
targetRow = row
|
|
529
|
+
}
|
|
530
|
+
if (seen.has(targetRow.id)) continue
|
|
531
|
+
seen.add(targetRow.id)
|
|
532
|
+
rootIdsForReturn.push({
|
|
533
|
+
root: targetRow,
|
|
534
|
+
rrf,
|
|
535
|
+
retrievalScore,
|
|
536
|
+
retrievalRank: rootIdsForReturn.length + 1,
|
|
537
|
+
})
|
|
538
|
+
if (rootIdsForReturn.length >= limit) break
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
let writeBackCount = 0
|
|
542
|
+
if (writeBackMemberHits && memberHitRootIds.size > 0) {
|
|
543
|
+
// Batch UPDATE — single round-trip instead of N (one per member-hit root).
|
|
544
|
+
const validRootIds = []
|
|
545
|
+
for (const rootId of memberHitRootIds) {
|
|
546
|
+
const r = rootIdsForReturn.find(x => x.root.id === rootId)?.root ?? byId.get(rootId)
|
|
547
|
+
if (r) validRootIds.push(Number(rootId))
|
|
548
|
+
}
|
|
549
|
+
if (validRootIds.length > 0) {
|
|
550
|
+
try {
|
|
551
|
+
const { rowCount } = await db.query(
|
|
552
|
+
`UPDATE entries SET last_seen_at = $1::bigint WHERE id = ANY($2::bigint[]) AND is_root = 1
|
|
553
|
+
AND (last_seen_at IS NULL OR last_seen_at < $1::bigint - 3600000)`,
|
|
554
|
+
[String(nowMs), validRootIds],
|
|
555
|
+
)
|
|
556
|
+
writeBackCount = rowCount ?? validRootIds.length
|
|
557
|
+
} catch (err) {
|
|
558
|
+
process.stderr.write(`[recall] writeback batch failed (count=${validRootIds.length}): ${err.message}\n`)
|
|
559
|
+
}
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
// ── Final fetch: full row for each root by id = ANY(bigint[]) ────────────
|
|
564
|
+
const topIds = rootIdsForReturn.map(x => Number(x.root.id))
|
|
565
|
+
const { clause: finalFilter, params: finalFilterParams } = buildFilterClause(2)
|
|
566
|
+
const { rows: finalRows } = await recallReadQuery(
|
|
567
|
+
db,
|
|
568
|
+
`SELECT id, ts, role, content, session_id, source_turn, chunk_root, is_root,
|
|
569
|
+
element, category, summary, project_id, status, score, last_seen_at
|
|
570
|
+
FROM entries
|
|
571
|
+
WHERE id = ANY($1::bigint[])
|
|
572
|
+
${finalFilter}`,
|
|
573
|
+
[topIds, ...finalFilterParams],
|
|
574
|
+
)
|
|
575
|
+
const finalById = new Map(finalRows.map(r => [Number(r.id), r]))
|
|
576
|
+
|
|
577
|
+
// Members: single batch fetch keyed by chunk_root = ANY($1) — one
|
|
578
|
+
// round-trip vs N. Map to per-root arrays preserving (ts ASC, id ASC).
|
|
579
|
+
let membersByRoot = new Map()
|
|
580
|
+
if (includeMembers) {
|
|
581
|
+
const rootIds = rootIdsForReturn
|
|
582
|
+
.map(x => Number(finalById.get(Number(x.root.id))?.id ?? x.root.id))
|
|
583
|
+
.filter(id => {
|
|
584
|
+
const fr = finalById.get(id) ?? rootIdsForReturn.find(x => Number(x.root.id) === id)?.root
|
|
585
|
+
return fr && fr.is_root === 1
|
|
586
|
+
})
|
|
587
|
+
if (rootIds.length > 0) {
|
|
588
|
+
const { rows: memberRows } = await recallReadQuery(
|
|
589
|
+
db,
|
|
590
|
+
`SELECT id, ts, role, content, session_id, source_turn, project_id, chunk_root
|
|
591
|
+
FROM entries WHERE chunk_root = ANY($1::bigint[]) AND is_root = 0
|
|
592
|
+
ORDER BY ts ASC, id ASC`,
|
|
593
|
+
[rootIds],
|
|
594
|
+
)
|
|
595
|
+
for (const m of memberRows) {
|
|
596
|
+
const k = Number(m.chunk_root)
|
|
597
|
+
if (!membersByRoot.has(k)) membersByRoot.set(k, [])
|
|
598
|
+
membersByRoot.get(k).push(m)
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
const results = []
|
|
603
|
+
for (const { root, rrf, retrievalScore, retrievalRank } of rootIdsForReturn) {
|
|
604
|
+
// Roots absent from finalById were excluded by the status/time filter on
|
|
605
|
+
// the final fetch; falling back to the unfiltered `root` would leak
|
|
606
|
+
// archived / out-of-window rows via member-hit resolution.
|
|
607
|
+
const finalRoot = finalById.get(Number(root.id))
|
|
608
|
+
if (!finalRoot) continue
|
|
609
|
+
const out = { ...finalRoot, rrf, retrievalScore, retrievalRank }
|
|
610
|
+
if (includeMembers && finalRoot.is_root === 1) {
|
|
611
|
+
out.members = membersByRoot.get(Number(finalRoot.id)) ?? []
|
|
612
|
+
}
|
|
613
|
+
results.push(out)
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
process.stderr.write(
|
|
617
|
+
`[recall] dense=${denseCount} sparse=${sparseCount} trgm=${trgmCount} exact=${exactCount} semantic_only_dropped=${semanticOnlyDropped} weak_text_dropped=${weakTextDropped} merged=${results.length} write_back=${writeBackCount}\n`,
|
|
618
|
+
)
|
|
619
|
+
|
|
620
|
+
return results
|
|
621
|
+
}
|