mixdog 0.9.3 → 0.9.4
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/package.json +7 -3
- package/scripts/bench/lead-review-tasks-r3.json +20 -0
- package/scripts/bench/lead-review-tasks.json +20 -0
- package/scripts/bench/r4-mixed-tasks.json +20 -0
- package/scripts/bench/review-tasks.json +20 -0
- package/scripts/bench/round-codex.json +114 -0
- package/scripts/bench/round-mixdog-lead-r3.json +269 -0
- package/scripts/bench/round-mixdog-lead.json +269 -0
- package/scripts/bench/round-mixdog.json +126 -0
- package/scripts/bench/round-r10-bigsample.json +679 -0
- package/scripts/bench/round-r11-codexalign.json +257 -0
- package/scripts/bench/round-r4-codex.json +114 -0
- package/scripts/bench/round-r4-mixed.json +225 -0
- package/scripts/bench/round-r5-gpt-lead.json +259 -0
- package/scripts/bench/round-r6-codex.json +114 -0
- package/scripts/bench/round-r6-solo.json +257 -0
- package/scripts/bench/round-r7-full.json +254 -0
- package/scripts/bench/round-r8-fulldefault.json +255 -0
- package/scripts/bench-run.mjs +215 -29
- package/scripts/freevar-smoke.mjs +95 -0
- package/scripts/internal-comms-bench.mjs +1 -0
- package/scripts/internal-comms-smoke.mjs +10 -9
- package/scripts/mouse-probe.mjs +45 -0
- package/scripts/output-style-bench.mjs +13 -6
- package/scripts/output-style-smoke.mjs +4 -4
- package/scripts/provider-toolcall-test.mjs +7 -3
- package/scripts/recall-usecase-cases.json +18 -0
- package/scripts/recall-usecase-probe.json +6 -0
- package/scripts/session-bench.mjs +152 -6
- package/scripts/tool-smoke.mjs +23 -63
- package/scripts/tui-render-smoke.mjs +90 -0
- package/scripts/webhook-smoke.mjs +208 -0
- package/src/agents/debugger/AGENT.md +4 -1
- package/src/agents/heavy-worker/AGENT.md +6 -5
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +2 -1
- package/src/agents/worker/AGENT.md +8 -4
- package/src/lib/rules-builder.cjs +4 -0
- package/src/mixdog-session-runtime.mjs +632 -2042
- package/src/output-styles/default.md +34 -9
- package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
- package/src/output-styles/minimal.md +4 -1
- package/src/output-styles/simple.md +22 -7
- package/src/rules/agent/00-common.md +2 -0
- package/src/rules/lead/lead-brief.md +12 -0
- package/src/rules/lead/lead-tool.md +0 -11
- package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +25 -0
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +100 -23
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +6 -15
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +362 -0
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +410 -0
- package/src/runtime/agent/orchestrator/agent-trace.mjs +16 -735
- package/src/runtime/agent/orchestrator/config.mjs +69 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +62 -20
- package/src/runtime/agent/orchestrator/providers/anthropic-model-resolve.mjs +209 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +489 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +81 -1281
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +607 -0
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +32 -3
- package/src/runtime/agent/orchestrator/providers/codex-client-meta.mjs +81 -0
- package/src/runtime/agent/orchestrator/providers/gemini-cache.mjs +248 -0
- package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +303 -0
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +505 -0
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +43 -1013
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +17 -3
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +86 -11
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +348 -0
- package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +108 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-trace.mjs +58 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +368 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +760 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +40 -1143
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +732 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +297 -2123
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +130 -1002
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +227 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +436 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1105 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +2 -1
- package/src/runtime/agent/orchestrator/session/compact/budget.mjs +288 -0
- package/src/runtime/agent/orchestrator/session/compact/constants.mjs +85 -0
- package/src/runtime/agent/orchestrator/session/compact/engine.mjs +749 -0
- package/src/runtime/agent/orchestrator/session/compact/messages.mjs +82 -0
- package/src/runtime/agent/orchestrator/session/compact/summary-schema.mjs +315 -0
- package/src/runtime/agent/orchestrator/session/compact/summary.mjs +643 -0
- package/src/runtime/agent/orchestrator/session/compact/text-utils.mjs +326 -0
- package/src/runtime/agent/orchestrator/session/compact.mjs +40 -2282
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +14 -2
- package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +61 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +1 -3
- package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +182 -0
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +173 -0
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +58 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +239 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +251 -397
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +471 -0
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +7 -4
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +12 -0
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +406 -0
- package/src/runtime/agent/orchestrator/session/manager/status-telemetry.mjs +80 -0
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +210 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +166 -1087
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +189 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +74 -179
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +70 -20
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +22 -2
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +32 -41
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +40 -0
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +29 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +8 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +126 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +81 -92
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +161 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-process.mjs +108 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +28 -265
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +0 -6
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +57 -3
- package/src/runtime/agent/orchestrator/tools/code-graph/keyword-match.mjs +82 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +10 -122
- package/src/runtime/agent/orchestrator/tools/code-graph/text-columns.mjs +45 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
- package/src/runtime/agent/orchestrator/tools/graph-binary-fetcher.mjs +6 -3
- package/src/runtime/agent/orchestrator/tools/patch/constants.mjs +9 -0
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +171 -0
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +471 -0
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +436 -0
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +342 -0
- package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +359 -0
- package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +340 -0
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +643 -0
- package/src/runtime/agent/orchestrator/tools/patch.mjs +36 -2959
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +0 -21
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -72
- package/src/runtime/agent/orchestrator/tools/shell-powershell.mjs +77 -0
- package/src/runtime/agent/orchestrator/tools/shell-state.mjs +154 -0
- package/src/runtime/channels/backends/discord-access.mjs +32 -0
- package/src/runtime/channels/backends/discord-attachments.mjs +65 -0
- package/src/runtime/channels/backends/discord-gateway.mjs +233 -0
- package/src/runtime/channels/backends/discord.mjs +12 -292
- package/src/runtime/channels/index.mjs +229 -663
- package/src/runtime/channels/lib/backend-dispatch.mjs +44 -0
- package/src/runtime/channels/lib/event-pipeline.mjs +18 -1
- package/src/runtime/channels/lib/event-queue.mjs +63 -4
- package/src/runtime/channels/lib/inbound-routing.mjs +111 -0
- package/src/runtime/channels/lib/output-forwarder.mjs +1 -1
- package/src/runtime/channels/lib/owner-heartbeat.mjs +75 -0
- package/src/runtime/channels/lib/parent-bridge.mjs +88 -0
- package/src/runtime/channels/lib/runtime-paths.mjs +14 -4
- package/src/runtime/channels/lib/session-discovery.mjs +56 -4
- package/src/runtime/channels/lib/tool-dispatch.mjs +158 -0
- package/src/runtime/channels/lib/tool-format.mjs +1 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +4 -4
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -3
- package/src/runtime/channels/lib/voice-transcription.mjs +179 -0
- package/src/runtime/channels/lib/webhook/deliveries.mjs +312 -0
- package/src/runtime/channels/lib/webhook/log.mjs +42 -0
- package/src/runtime/channels/lib/webhook/ngrok.mjs +181 -0
- package/src/runtime/channels/lib/webhook/signature.mjs +60 -0
- package/src/runtime/channels/lib/webhook.mjs +36 -570
- package/src/runtime/channels/tool-defs.mjs +11 -130
- package/src/runtime/memory/index.mjs +201 -1948
- package/src/runtime/memory/lib/cycle-llm-adapters.mjs +58 -0
- package/src/runtime/memory/lib/cycle-scheduler.mjs +497 -0
- package/src/runtime/memory/lib/embedding-warmup.mjs +58 -0
- package/src/runtime/memory/lib/memory-config-flags.mjs +91 -0
- package/src/runtime/memory/lib/memory-cycle.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +515 -0
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +324 -0
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +18 -0
- package/src/runtime/memory/lib/memory-cycle2.mjs +24 -842
- package/src/runtime/memory/lib/memory-embed.mjs +149 -0
- package/src/runtime/memory/lib/memory-process-lock.mjs +162 -0
- package/src/runtime/memory/lib/memory-recall-store.mjs +22 -2
- package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
- package/src/runtime/memory/lib/query-handlers.mjs +780 -0
- package/src/runtime/memory/lib/recall-format.mjs +55 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +8 -3
- package/src/runtime/memory/lib/transcript-ingest.mjs +425 -0
- package/src/runtime/memory/tool-defs.mjs +5 -13
- package/src/runtime/search/lib/http-fetch.mjs +274 -0
- package/src/runtime/search/lib/ssrf-guard.mjs +333 -0
- package/src/runtime/search/lib/web-tools.mjs +24 -602
- package/src/runtime/shared/atomic-file.mjs +26 -1
- package/src/runtime/shared/launcher-control.mjs +2 -2
- package/src/runtime/shared/tool-primitives.mjs +308 -0
- package/src/runtime/shared/tool-result-summary.mjs +515 -0
- package/src/runtime/shared/tool-surface.mjs +80 -898
- package/src/runtime/shared/transcript-writer.mjs +23 -0
- package/src/runtime/shared/update-checker.mjs +7 -4
- package/src/session-runtime/config-helpers.mjs +84 -2
- package/src/session-runtime/config-lifecycle.mjs +232 -0
- package/src/session-runtime/cwd-plugins.mjs +226 -0
- package/src/session-runtime/mcp-glue.mjs +177 -0
- package/src/session-runtime/model-recency.mjs +111 -0
- package/src/session-runtime/native-search.mjs +247 -0
- package/src/session-runtime/output-styles.mjs +11 -9
- package/src/session-runtime/prewarm.mjs +142 -0
- package/src/session-runtime/provider-models.mjs +278 -0
- package/src/session-runtime/provider-usage.mjs +120 -0
- package/src/session-runtime/quick-model-rows.mjs +170 -0
- package/src/session-runtime/quick-search-models.mjs +46 -0
- package/src/session-runtime/session-hooks.mjs +93 -0
- package/src/session-runtime/settings-api.mjs +319 -0
- package/src/session-runtime/tool-catalog.mjs +29 -29
- package/src/session-runtime/tool-defs.mjs +84 -0
- package/src/session-runtime/warmup-schedulers.mjs +201 -0
- package/src/standalone/agent-tool/helpers.mjs +237 -0
- package/src/standalone/agent-tool/notify.mjs +107 -0
- package/src/standalone/agent-tool/provider-init.mjs +143 -0
- package/src/standalone/agent-tool/render.mjs +152 -0
- package/src/standalone/agent-tool/tool-def.mjs +55 -0
- package/src/standalone/agent-tool.mjs +110 -671
- package/src/standalone/channel-worker.mjs +4 -7
- package/src/standalone/explore-tool.mjs +30 -9
- package/src/standalone/hook-bus/config.mjs +207 -0
- package/src/standalone/hook-bus/constants.mjs +90 -0
- package/src/standalone/hook-bus/handlers.mjs +481 -0
- package/src/standalone/hook-bus/payload.mjs +31 -0
- package/src/standalone/hook-bus/rules.mjs +77 -0
- package/src/standalone/hook-bus.mjs +77 -870
- package/src/standalone/memory-runtime-proxy.mjs +7 -0
- package/src/standalone/opencode-go-login.mjs +5 -1
- package/src/standalone/provider-admin.mjs +1 -16
- package/src/standalone/usage-dashboard.mjs +3 -1
- package/src/tui/App.jsx +945 -8094
- package/src/tui/app/app-format.mjs +206 -0
- package/src/tui/app/channel-pickers.mjs +510 -0
- package/src/tui/app/clipboard.mjs +67 -0
- package/src/tui/app/core-memory-picker.mjs +210 -0
- package/src/tui/app/extension-pickers.mjs +506 -0
- package/src/tui/app/input-parsers.mjs +193 -0
- package/src/tui/app/maintenance-pickers.mjs +324 -0
- package/src/tui/app/model-options.mjs +330 -0
- package/src/tui/app/model-picker.mjs +365 -0
- package/src/tui/app/onboarding-steps.mjs +400 -0
- package/src/tui/app/project-picker.mjs +247 -0
- package/src/tui/app/provider-setup-picker.mjs +580 -0
- package/src/tui/app/resume-picker.mjs +55 -0
- package/src/tui/app/route-pickers.mjs +419 -0
- package/src/tui/app/settings-picker.mjs +490 -0
- package/src/tui/app/slash-commands.mjs +101 -0
- package/src/tui/app/slash-dispatch.mjs +427 -0
- package/src/tui/app/text-layout.mjs +46 -0
- package/src/tui/app/theme-effort-pickers.mjs +154 -0
- package/src/tui/app/transcript-window.mjs +671 -0
- package/src/tui/app/use-mouse-input.mjs +460 -0
- package/src/tui/app/use-prompt-handlers.mjs +310 -0
- package/src/tui/app/use-transcript-scroll.mjs +510 -0
- package/src/tui/app/use-transcript-window.mjs +589 -0
- package/src/tui/components/ConfirmBar.jsx +1 -1
- package/src/tui/components/Picker.jsx +32 -4
- package/src/tui/components/PromptInput.jsx +23 -101
- package/src/tui/components/SlashCommandPalette.jsx +8 -1
- package/src/tui/components/StatusLine.jsx +63 -12
- package/src/tui/components/TextEntryPanel.jsx +11 -0
- package/src/tui/components/ToolExecution.jsx +52 -594
- package/src/tui/components/TranscriptItem.jsx +105 -0
- package/src/tui/components/UsagePanel.jsx +18 -4
- package/src/tui/components/prompt-input/edit-helpers.mjs +72 -0
- package/src/tui/components/prompt-input/voice-indicator.mjs +39 -0
- package/src/tui/components/tool-execution/ResultBody.jsx +56 -0
- package/src/tui/components/tool-execution/surface-detail.mjs +405 -0
- package/src/tui/components/tool-execution/text-format.mjs +161 -0
- package/src/tui/display-width.mjs +20 -3
- package/src/tui/dist/index.mjs +19652 -18630
- package/src/tui/engine/agent-job-feed.mjs +133 -0
- package/src/tui/engine/notification-plan.mjs +76 -0
- package/src/tui/engine/render-timing.mjs +17 -0
- package/src/tui/engine/tool-approval.mjs +94 -0
- package/src/tui/engine/tool-card-results.mjs +234 -0
- package/src/tui/engine/tool-result-status.mjs +135 -0
- package/src/tui/engine.mjs +122 -562
- package/src/tui/figures.mjs +5 -0
- package/src/tui/index.jsx +105 -0
- package/src/tui/input-editing.mjs +2 -2
- package/src/tui/markdown/format-token.mjs +4 -1
- package/src/tui/statusline-ansi-bridge.mjs +11 -3
- package/src/tui/theme.mjs +6 -0
- package/src/ui/statusline-agents.mjs +213 -0
- package/src/ui/statusline-format.mjs +146 -0
- package/src/ui/statusline-segments.mjs +148 -0
- package/src/ui/statusline.mjs +67 -501
- package/src/ui/tool-card.mjs +0 -1
- package/src/vendor/statusline/bin/statusline-route.mjs +15 -2
- package/src/workflows/default/WORKFLOW.md +1 -1
- package/src/workflows/sequential/WORKFLOW.md +1 -1
- package/vendor/ink/build/display-width.js +19 -3
- package/vendor/ink/build/ink.js +103 -6
- package/vendor/ink/build/log-update.js +17 -3
- package/vendor/ink/build/wrap-text.js +125 -0
- package/scripts/_test-folder-dialog.mjs +0 -30
- package/scripts/fix-brief-fn.mjs +0 -35
- package/scripts/fix-format-tool-surface.mjs +0 -24
- package/scripts/fix-tool-exec-visible.mjs +0 -42
- package/scripts/patch-agent-brief.mjs +0 -48
- package/scripts/patch-app.mjs +0 -21
- package/scripts/patch-app2.mjs +0 -18
- package/scripts/patch-dist-brief.mjs +0 -96
- package/scripts/patch-tool-exec.mjs +0 -70
- package/src/examples/schedules/SCHEDULE.example.md +0 -32
- package/src/examples/webhooks/WEBHOOK.example.md +0 -40
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +0 -107
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +0 -143
- package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -285
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +0 -162
- package/src/runtime/agent/orchestrator/tools/builtin/open-config-tool.mjs +0 -26
- package/src/runtime/shared/channel-notification-routing.test.mjs +0 -45
- package/src/runtime/shared/task-notification-envelope.test.mjs +0 -107
- package/src/runtime/shared/tool-execution-contract.test.mjs +0 -183
- package/src/standalone/agent-task-status.test.mjs +0 -76
- package/src/tui/components/tool-output-format.test.mjs +0 -399
- package/src/tui/display-width.test.mjs +0 -35
- package/src/tui/engine-runtime-notification.test.mjs +0 -115
- package/src/tui/engine-tool-result-text.test.mjs +0 -75
- package/src/tui/input-editing.selection.test.mjs +0 -75
- package/src/tui/markdown/format-token.test.mjs +0 -354
- package/src/tui/markdown/render-ansi.test.mjs +0 -108
- package/src/tui/markdown/stream-fence.test.mjs +0 -26
- package/src/tui/markdown/streaming-markdown.test.mjs +0 -70
- package/src/tui/paste-fix.test.mjs +0 -119
- package/src/tui/prompt-history-store.test.mjs +0 -52
- package/src/tui/statusline-ansi-bridge.test.mjs +0 -159
- package/src/tui/transcript-tool-failures.test.mjs +0 -111
- package/src/ui/markdown.test.mjs +0 -70
- package/src/ui/statusline-context-label.test.mjs +0 -15
- package/src/vendor/statusline/bin/statusline-lib.mjs +0 -186
- package/src/vendor/statusline/bin/statusline-route.test.mjs +0 -80
|
@@ -0,0 +1,780 @@
|
|
|
1
|
+
// Recall / search / dump / stats query handlers extracted from index.mjs.
|
|
2
|
+
//
|
|
3
|
+
// These six functions form the read-side query cluster: they share the same
|
|
4
|
+
// live DB handle plus a small set of recall-format / retriever helpers, and a
|
|
5
|
+
// few facade-local pieces of state (the cold-recall log throttle and the
|
|
6
|
+
// optional trace DB used for recall
|
|
7
|
+
// telemetry). All of that is injected through createQueryHandlers({...}) so the
|
|
8
|
+
// module holds no state of its own and the facade keeps ownership of `db`,
|
|
9
|
+
// `_traceDb`, etc. (getBootTimestamp stays injected for facade compatibility
|
|
10
|
+
// but period='last' no longer reads it — 'last' is an unbounded newest-first
|
|
11
|
+
// browse.)
|
|
12
|
+
|
|
13
|
+
import {
|
|
14
|
+
parsePeriod,
|
|
15
|
+
coreRecallTerms,
|
|
16
|
+
normalizeRecallProjectScope,
|
|
17
|
+
sessionRecallTerms,
|
|
18
|
+
interleaveRawRows,
|
|
19
|
+
renderEntryLines,
|
|
20
|
+
renderSessionGroupedLines,
|
|
21
|
+
} from './recall-format.mjs'
|
|
22
|
+
import { searchRelevantHybrid } from './memory-recall-store.mjs'
|
|
23
|
+
import { fetchEntriesByIdsScoped } from './memory-recall-id-patch.mjs'
|
|
24
|
+
import { retrieveEntries } from './memory-retrievers.mjs'
|
|
25
|
+
import { cleanMemoryText } from './memory.mjs'
|
|
26
|
+
import { insertTraceEvents } from './trace-store.mjs'
|
|
27
|
+
import {
|
|
28
|
+
embedText,
|
|
29
|
+
embedTexts,
|
|
30
|
+
isEmbeddingModelReady,
|
|
31
|
+
warmupEmbeddingProvider,
|
|
32
|
+
} from './embedding-provider.mjs'
|
|
33
|
+
|
|
34
|
+
export function createQueryHandlers({
|
|
35
|
+
getDb,
|
|
36
|
+
log,
|
|
37
|
+
resolveProjectScope,
|
|
38
|
+
embeddingWarmupCanStart,
|
|
39
|
+
getBootTimestamp,
|
|
40
|
+
getTraceDb,
|
|
41
|
+
}) {
|
|
42
|
+
// Facade-owned cold-recall log throttle. Kept module-local to this factory
|
|
43
|
+
// instance (one memory runtime = one factory) so the 10s de-dup window
|
|
44
|
+
// behaves exactly as it did when it was a top-level `let` in index.mjs.
|
|
45
|
+
let _embeddingColdRecallLogAt = 0
|
|
46
|
+
|
|
47
|
+
// Raw-row priority lookup for narrow-window queries. Raw rows (is_root=0,
|
|
48
|
+
// chunk_root IS NULL) are inserted immediately by ingestTranscriptFile before
|
|
49
|
+
// cycle1 runs, so they always carry the freshest turns in the DB.
|
|
50
|
+
async function readRawRowsInWindow(db, tsFromMs, tsToMs, hardLimit = 10, { projectScope, sessionId, terms } = {}) {
|
|
51
|
+
try {
|
|
52
|
+
// Composable WHERE assembly (mirrors retrieveEntries' filter semantics so
|
|
53
|
+
// raw and chunked legs stay in filter parity: projectScope AND sessionId
|
|
54
|
+
// apply identically to both pools).
|
|
55
|
+
const where = ['chunk_root IS NULL', 'is_root = 0', 'ts >= $1', 'ts <= $2']
|
|
56
|
+
const params = [tsFromMs ?? 0, tsToMs ?? Date.now()]
|
|
57
|
+
if (projectScope === 'common') {
|
|
58
|
+
where.push('project_id IS NULL')
|
|
59
|
+
} else if (projectScope && projectScope !== 'all') {
|
|
60
|
+
params.push(projectScope)
|
|
61
|
+
where.push(`(project_id IS NULL OR project_id = $${params.length})`)
|
|
62
|
+
}
|
|
63
|
+
const sid = String(sessionId || '').trim()
|
|
64
|
+
if (sid) {
|
|
65
|
+
params.push(sid)
|
|
66
|
+
where.push(`session_id = $${params.length}`)
|
|
67
|
+
}
|
|
68
|
+
if (Array.isArray(terms) && terms.length > 0) {
|
|
69
|
+
const textExpr = `lower(coalesce(content, '') || ' ' || coalesce(element, '') || ' ' || coalesce(summary, ''))`
|
|
70
|
+
const clauses = terms.map((term) => {
|
|
71
|
+
params.push(`%${term}%`)
|
|
72
|
+
return `(CASE WHEN ${textExpr} LIKE $${params.length} THEN 1 ELSE 0 END)`
|
|
73
|
+
})
|
|
74
|
+
// Multi-token queries: from 3+ terms require at least 2 matching terms
|
|
75
|
+
// so one common token ("대화", "recall") can't drag unrelated raw rows
|
|
76
|
+
// into the page. 1-2 term queries keep single-hit contains semantics —
|
|
77
|
+
// short Korean queries are often exactly two meaningful tokens and a
|
|
78
|
+
// 2-of-2 requirement silently emptied the raw leg for them.
|
|
79
|
+
const minHits = terms.length >= 3 ? 2 : 1
|
|
80
|
+
where.push(`(${clauses.join(' + ')}) >= ${minHits}`)
|
|
81
|
+
}
|
|
82
|
+
params.push(hardLimit)
|
|
83
|
+
const sql = `SELECT id, ts, role, content, session_id, source_turn, chunk_root, is_root,
|
|
84
|
+
element, category, summary, status, score, last_seen_at, project_id
|
|
85
|
+
FROM entries
|
|
86
|
+
WHERE ${where.join(' AND ')}
|
|
87
|
+
ORDER BY ts DESC
|
|
88
|
+
LIMIT $${params.length}`
|
|
89
|
+
const rows = (await db.query(sql, params)).rows
|
|
90
|
+
return rows.map(r => ({ ...r, retrievalScore: 0, rrf: 0 }))
|
|
91
|
+
} catch { return [] }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async function recallSessionRows(args = {}) {
|
|
95
|
+
const db = getDb()
|
|
96
|
+
const sessionId = String(args.sessionId || args.session_id || '').trim()
|
|
97
|
+
if (!sessionId) return { text: '(no current session)' }
|
|
98
|
+
const limit = Math.max(1, Math.min(100, Number(args.limit) || 20))
|
|
99
|
+
const terms = sessionRecallTerms(args.query)
|
|
100
|
+
const params = [sessionId]
|
|
101
|
+
// Roots + not-yet-chunked leaves only. Once cycle1 turns raw leaves into
|
|
102
|
+
// (root, members) pairs, selecting every row unfiltered emitted the root's
|
|
103
|
+
// summary AND its own member rows in the same browse — duplicate content.
|
|
104
|
+
// A committed member (is_root=0 with a chunk_root) is always reachable via
|
|
105
|
+
// its root's `members` expansion below, so it never needs to be selected
|
|
106
|
+
// directly here.
|
|
107
|
+
const where = ['session_id = $1', '(is_root = 1 OR chunk_root IS NULL OR chunk_root = id)']
|
|
108
|
+
// Current-turn cutoff: the newest unchunked row is very often the calling
|
|
109
|
+
// turn's OWN recall request/tool-args, still being written when this query
|
|
110
|
+
// runs. Exclude it from a bare (no-query) browse so the in-flight turn
|
|
111
|
+
// doesn't self-echo; a query browse (explicit search intent) keeps it.
|
|
112
|
+
// Only treat the newest unchunked turn as "in-flight" when its latest row
|
|
113
|
+
// is fresh (within FRESHNESS_MS of now) — an older newest-unchunked-turn
|
|
114
|
+
// is completed history (cycle1 just hasn't gotten to it, or drain timed
|
|
115
|
+
// out) and must stay visible, not be silently hidden every browse.
|
|
116
|
+
const IN_FLIGHT_TURN_FRESHNESS_MS = 5 * 60 * 1000
|
|
117
|
+
let excludeSourceTurnId = null
|
|
118
|
+
if (terms.length === 0) {
|
|
119
|
+
try {
|
|
120
|
+
const r = await db.query(
|
|
121
|
+
`SELECT source_turn t, MAX(ts) last_ts FROM entries
|
|
122
|
+
WHERE session_id = $1 AND chunk_root IS NULL
|
|
123
|
+
GROUP BY source_turn ORDER BY source_turn DESC LIMIT 1`,
|
|
124
|
+
[sessionId],
|
|
125
|
+
)
|
|
126
|
+
const t = r.rows?.[0]?.t
|
|
127
|
+
const lastTs = Number(r.rows?.[0]?.last_ts)
|
|
128
|
+
if (t != null && Number.isFinite(lastTs) && (Date.now() - lastTs) <= IN_FLIGHT_TURN_FRESHNESS_MS) {
|
|
129
|
+
excludeSourceTurnId = Number(t)
|
|
130
|
+
}
|
|
131
|
+
} catch {}
|
|
132
|
+
}
|
|
133
|
+
if (Number.isFinite(excludeSourceTurnId)) {
|
|
134
|
+
params.push(excludeSourceTurnId)
|
|
135
|
+
where.push(`NOT (chunk_root IS NULL AND source_turn = $${params.length})`)
|
|
136
|
+
}
|
|
137
|
+
if (terms.length > 0) {
|
|
138
|
+
const textExpr = `lower(coalesce(content, '') || ' ' || coalesce(element, '') || ' ' || coalesce(summary, ''))`
|
|
139
|
+
const clauses = terms.map((term) => {
|
|
140
|
+
params.push(`%${term}%`)
|
|
141
|
+
return `${textExpr} LIKE $${params.length}`
|
|
142
|
+
})
|
|
143
|
+
where.push(`(${clauses.join(' OR ')})`)
|
|
144
|
+
}
|
|
145
|
+
params.push(limit)
|
|
146
|
+
let rows = (await db.query(`
|
|
147
|
+
SELECT id, ts, role, content, session_id, source_turn, chunk_root, is_root,
|
|
148
|
+
element, category, summary, status, score, last_seen_at, project_id
|
|
149
|
+
FROM entries
|
|
150
|
+
WHERE ${where.join(' AND ')}
|
|
151
|
+
ORDER BY ts DESC, id DESC
|
|
152
|
+
LIMIT $${params.length}
|
|
153
|
+
`, params)).rows
|
|
154
|
+
if (rows.length < limit) {
|
|
155
|
+
const seen = new Set(rows.map((row) => Number(row.id)).filter((id) => Number.isFinite(id)))
|
|
156
|
+
const fillLimit = Math.max(0, limit - rows.length)
|
|
157
|
+
const fillWhere = ['session_id = $1', 'id <> ALL($2::bigint[])', '(is_root = 1 OR chunk_root IS NULL OR chunk_root = id)']
|
|
158
|
+
const fillParams = [sessionId, [...seen]]
|
|
159
|
+
if (Number.isFinite(excludeSourceTurnId)) {
|
|
160
|
+
fillParams.push(excludeSourceTurnId)
|
|
161
|
+
fillWhere.push(`NOT (chunk_root IS NULL AND source_turn = $${fillParams.length})`)
|
|
162
|
+
}
|
|
163
|
+
fillParams.push(fillLimit)
|
|
164
|
+
const fillRows = fillLimit > 0
|
|
165
|
+
? (await db.query(`
|
|
166
|
+
SELECT id, ts, role, content, session_id, source_turn, chunk_root, is_root,
|
|
167
|
+
element, category, summary, status, score, last_seen_at, project_id
|
|
168
|
+
FROM entries
|
|
169
|
+
WHERE ${fillWhere.join(' AND ')}
|
|
170
|
+
ORDER BY ts DESC, id DESC
|
|
171
|
+
LIMIT $${fillParams.length}
|
|
172
|
+
`, fillParams)).rows
|
|
173
|
+
: []
|
|
174
|
+
if (fillRows.length > 0) rows = [...rows, ...fillRows]
|
|
175
|
+
}
|
|
176
|
+
if (args.includeMembers === true) {
|
|
177
|
+
const rootIds = rows
|
|
178
|
+
.filter((row) => Number(row.is_root) === 1)
|
|
179
|
+
.map((row) => Number(row.id))
|
|
180
|
+
.filter((id) => Number.isFinite(id))
|
|
181
|
+
if (rootIds.length > 0) {
|
|
182
|
+
const members = (await db.query(`
|
|
183
|
+
SELECT id, ts, role, content, session_id, source_turn, project_id, chunk_root
|
|
184
|
+
FROM entries
|
|
185
|
+
WHERE chunk_root = ANY($1::bigint[]) AND is_root = 0
|
|
186
|
+
ORDER BY chunk_root ASC, COALESCE(source_turn, 2147483647) ASC, ts ASC, id ASC
|
|
187
|
+
`, [rootIds])).rows
|
|
188
|
+
const byRoot = new Map(rootIds.map((id) => [id, []]))
|
|
189
|
+
for (const member of members) {
|
|
190
|
+
const root = Number(member.chunk_root)
|
|
191
|
+
if (byRoot.has(root)) byRoot.get(root).push(member)
|
|
192
|
+
}
|
|
193
|
+
for (const row of rows) {
|
|
194
|
+
const id = Number(row.id)
|
|
195
|
+
if (byRoot.has(id)) row.members = byRoot.get(id)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
return { text: renderEntryLines(rows) }
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function recallCoreRows(query, { projectScope, category, limit } = {}) {
|
|
203
|
+
const db = getDb()
|
|
204
|
+
const terms = coreRecallTerms(query)
|
|
205
|
+
if (terms.length === 0) return []
|
|
206
|
+
|
|
207
|
+
const params = []
|
|
208
|
+
const where = []
|
|
209
|
+
const scope = normalizeRecallProjectScope(projectScope)
|
|
210
|
+
if (scope === null) {
|
|
211
|
+
where.push('project_id IS NULL')
|
|
212
|
+
} else if (scope !== '*') {
|
|
213
|
+
params.push(scope)
|
|
214
|
+
where.push(`(project_id IS NULL OR project_id = $${params.length})`)
|
|
215
|
+
}
|
|
216
|
+
if (category != null) {
|
|
217
|
+
const cats = (Array.isArray(category) ? category : [category])
|
|
218
|
+
.map((value) => String(value || '').trim().toLowerCase())
|
|
219
|
+
.filter(Boolean)
|
|
220
|
+
if (cats.length > 0) {
|
|
221
|
+
const placeholders = cats.map((cat) => {
|
|
222
|
+
params.push(cat)
|
|
223
|
+
return `$${params.length}`
|
|
224
|
+
})
|
|
225
|
+
where.push(`category IN (${placeholders.join(', ')})`)
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
const textExpr = `lower(coalesce(element, '') || ' ' || coalesce(summary, ''))`
|
|
230
|
+
const termClauses = terms.map((term) => {
|
|
231
|
+
params.push(`%${term}%`)
|
|
232
|
+
return `${textExpr} LIKE $${params.length}`
|
|
233
|
+
})
|
|
234
|
+
where.push(`(${termClauses.join(' OR ')})`)
|
|
235
|
+
const hitExpr = termClauses.map((clause) => `CASE WHEN ${clause} THEN 1 ELSE 0 END`).join(' + ')
|
|
236
|
+
const rowLimit = Math.max(1, Math.min(10, Number(limit) || 5))
|
|
237
|
+
params.push(rowLimit)
|
|
238
|
+
|
|
239
|
+
const rows = (await db.query(`
|
|
240
|
+
SELECT id, element, summary, category, project_id, created_at, updated_at,
|
|
241
|
+
(${hitExpr}) AS hit_count
|
|
242
|
+
FROM core_entries
|
|
243
|
+
WHERE ${where.join(' AND ')}
|
|
244
|
+
ORDER BY hit_count DESC, updated_at DESC, id ASC
|
|
245
|
+
LIMIT $${params.length}
|
|
246
|
+
`, params)).rows
|
|
247
|
+
|
|
248
|
+
return rows.map((row) => ({
|
|
249
|
+
...row,
|
|
250
|
+
id: `core:${row.id}`,
|
|
251
|
+
ts: row.updated_at || row.created_at || Date.now(),
|
|
252
|
+
is_root: 1,
|
|
253
|
+
}))
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
async function handleSearch(args, signal) {
|
|
257
|
+
const db = getDb()
|
|
258
|
+
const _traceDb = getTraceDb()
|
|
259
|
+
// Cooperative abort check: throw early if the caller already aborted
|
|
260
|
+
// (IPC cancel handler signals the AbortController before re-entry).
|
|
261
|
+
if (signal?.aborted) throw signal.reason ?? new Error('aborted')
|
|
262
|
+
// No pre-search drain: recall NEVER runs LLM chunking inline. Unchunked
|
|
263
|
+
// rows are served directly by the raw leg (readRawRowsInWindow on the
|
|
264
|
+
// query path, the chunk_root IS NULL selection in recallSessionRows) and
|
|
265
|
+
// are dense-searchable via the always-on raw-embedding flush (post-ingest
|
|
266
|
+
// + checkCycles tick). Chunked/scored upgrades arrive from the background
|
|
267
|
+
// cycle1 sweep on its own schedule.
|
|
268
|
+
// #id lookup normalization: search_memories and memory action:'search'
|
|
269
|
+
// callers pass a single `id` (or an id array under that same key), not
|
|
270
|
+
// the `ids` array below. Normalize once here so every dispatch path gets
|
|
271
|
+
// exact-id lookup, not just callers who already knew to use `ids`.
|
|
272
|
+
if (!Array.isArray(args.ids) && args.id != null) {
|
|
273
|
+
args = { ...args, ids: Array.isArray(args.id) ? args.id : [args.id] }
|
|
274
|
+
}
|
|
275
|
+
if (args?.currentSession === true || args?.sessionId || args?.session_id) {
|
|
276
|
+
return await recallSessionRows(args)
|
|
277
|
+
}
|
|
278
|
+
// id mode (follow-up lookup): caller passed `#N` markers from a prior
|
|
279
|
+
// recall result. Fetch those rows directly + their chunk members,
|
|
280
|
+
// bypassing hybrid search entirely. Output reuses renderEntryLines so
|
|
281
|
+
// the shape stays identical to the search path (chunk members first,
|
|
282
|
+
// root summary fallback).
|
|
283
|
+
if (Array.isArray(args.ids) && args.ids.length > 0) {
|
|
284
|
+
const ids = args.ids
|
|
285
|
+
.map(v => Number(v))
|
|
286
|
+
.filter(v => Number.isInteger(v) && v > 0)
|
|
287
|
+
if (ids.length === 0) return { text: '(no valid ids)' }
|
|
288
|
+
const includeArchived = args.includeArchived !== false
|
|
289
|
+
const category = args.category
|
|
290
|
+
const period = String(args.period ?? '').trim() || undefined
|
|
291
|
+
const temporal = parsePeriod(period, false)
|
|
292
|
+
let projectScope
|
|
293
|
+
if (typeof args.projectScope === 'string' && args.projectScope) {
|
|
294
|
+
projectScope = args.projectScope
|
|
295
|
+
} else {
|
|
296
|
+
const projectId = resolveProjectScope(typeof args.cwd === 'string' && args.cwd ? args.cwd : null)
|
|
297
|
+
projectScope = projectId !== null ? projectId : 'common'
|
|
298
|
+
}
|
|
299
|
+
const excludeStatuses = includeArchived ? [] : ['archived']
|
|
300
|
+
const rows = await fetchEntriesByIdsScoped(db, ids, {
|
|
301
|
+
ts_from: temporal?.startMs,
|
|
302
|
+
ts_to: temporal?.endMs,
|
|
303
|
+
excludeStatuses,
|
|
304
|
+
category,
|
|
305
|
+
projectScope,
|
|
306
|
+
})
|
|
307
|
+
if (rows.length === 0) return { text: '(no results)' }
|
|
308
|
+
// Members for any root rows in the result set.
|
|
309
|
+
const rootIds = rows.filter(r => r.is_root === 1).map(r => Number(r.id))
|
|
310
|
+
const memberLeafIds = new Set()
|
|
311
|
+
if (rootIds.length > 0) {
|
|
312
|
+
const { rows: memberRows } = await db.query(
|
|
313
|
+
`SELECT id, ts, role, content, chunk_root
|
|
314
|
+
FROM entries WHERE chunk_root = ANY($1::bigint[]) AND is_root = 0
|
|
315
|
+
ORDER BY ts ASC, id ASC`,
|
|
316
|
+
[rootIds],
|
|
317
|
+
)
|
|
318
|
+
const membersByRoot = new Map()
|
|
319
|
+
for (const m of memberRows) {
|
|
320
|
+
const k = Number(m.chunk_root)
|
|
321
|
+
if (!membersByRoot.has(k)) membersByRoot.set(k, [])
|
|
322
|
+
membersByRoot.get(k).push(m)
|
|
323
|
+
memberLeafIds.add(Number(m.id))
|
|
324
|
+
}
|
|
325
|
+
for (const r of rows) {
|
|
326
|
+
if (r.is_root === 1) r.members = membersByRoot.get(Number(r.id)) ?? []
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
// Preserve caller-supplied id order; drop leaves already inlined as a
|
|
330
|
+
// root's chunk member to prevent double emission when the caller names
|
|
331
|
+
// a root and one of its leaves in the same batch.
|
|
332
|
+
const byId = new Map(rows.map(r => [Number(r.id), r]))
|
|
333
|
+
const ordered = ids
|
|
334
|
+
.map(id => byId.get(id))
|
|
335
|
+
.filter(Boolean)
|
|
336
|
+
.filter(r => !(r.is_root === 0 && memberLeafIds.has(Number(r.id))))
|
|
337
|
+
return { text: renderEntryLines(ordered) }
|
|
338
|
+
}
|
|
339
|
+
// Array query — fan out in parallel, each query runs its own hybrid search
|
|
340
|
+
// path, and results are grouped in the response so the caller sees one
|
|
341
|
+
// ranked list per angle. Collapses what would otherwise be N sequential
|
|
342
|
+
// tool calls into a single invocation.
|
|
343
|
+
if (Array.isArray(args.query)) {
|
|
344
|
+
// Dedup + fan-out cap. The cap protects the result envelope from
|
|
345
|
+
// over-eager callers (20+ near-duplicate queries N× the IO) without
|
|
346
|
+
// silently swallowing the caller's intent: when the input exceeds
|
|
347
|
+
// QUERIES_CAP, prepend a one-line note so the caller can see the
|
|
348
|
+
// truncation and re-shape their query list.
|
|
349
|
+
const QUERIES_CAP = 5
|
|
350
|
+
const dedup = [...new Set(args.query.map(q => String(q || '').trim()).filter(Boolean))]
|
|
351
|
+
if (dedup.length === 0) return { text: '' }
|
|
352
|
+
const queries = dedup.slice(0, QUERIES_CAP)
|
|
353
|
+
const dropped = dedup.length - queries.length
|
|
354
|
+
const rest = { ...args }
|
|
355
|
+
delete rest.query
|
|
356
|
+
const deadlineSec = Math.max(1, Number(process.env.MEMORY_FANOUT_DEADLINE_S) || 180)
|
|
357
|
+
const deadlineMs = deadlineSec * 1000
|
|
358
|
+
const fanOutAbort = new AbortController()
|
|
359
|
+
let deadlineTimer
|
|
360
|
+
const deadlineRace = new Promise((_res, rej) => {
|
|
361
|
+
deadlineTimer = setTimeout(() => {
|
|
362
|
+
fanOutAbort.abort(new Error(`memory fan-out deadline exceeded (${deadlineSec}s)`))
|
|
363
|
+
rej(Object.assign(new Error(`memory fan-out deadline exceeded (${deadlineSec}s)`), { _deadline: true }))
|
|
364
|
+
}, deadlineMs)
|
|
365
|
+
})
|
|
366
|
+
let settled
|
|
367
|
+
try {
|
|
368
|
+
// Pre-warm only when the embedding model is already resident. If the
|
|
369
|
+
// process is still cold, keep recall responsive and let the background
|
|
370
|
+
// warmup finish independently instead of making the first query pay the
|
|
371
|
+
// ONNX session-create cost.
|
|
372
|
+
if (isEmbeddingModelReady()) {
|
|
373
|
+
// Race against the same deadline as the fan-out itself: a stuck
|
|
374
|
+
// embedding worker would previously park here indefinitely because
|
|
375
|
+
// the timer hadn't been started yet from the fan-out's perspective.
|
|
376
|
+
await Promise.race([embedTexts(queries), deadlineRace])
|
|
377
|
+
} else if (embeddingWarmupCanStart()) {
|
|
378
|
+
void warmupEmbeddingProvider().catch((err) => {
|
|
379
|
+
log(`[memory-service] embedding warmup after cold fan-out skipped dense search: ${err?.message || err}\n`)
|
|
380
|
+
})
|
|
381
|
+
}
|
|
382
|
+
settled = await Promise.race([
|
|
383
|
+
Promise.all(queries.map(async (q) => {
|
|
384
|
+
if (fanOutAbort.signal.aborted) throw fanOutAbort.signal.reason
|
|
385
|
+
if (signal?.aborted) throw signal.reason ?? new Error('aborted')
|
|
386
|
+
const sub = await handleSearch({ ...rest, query: q }, signal)
|
|
387
|
+
return `[${q}]\n${sub.text || '(no results)'}`
|
|
388
|
+
})),
|
|
389
|
+
deadlineRace,
|
|
390
|
+
])
|
|
391
|
+
} finally {
|
|
392
|
+
clearTimeout(deadlineTimer)
|
|
393
|
+
}
|
|
394
|
+
const parts = settled
|
|
395
|
+
const header = dropped > 0
|
|
396
|
+
? `note: ${dedup.length} queries received, ${queries.length} processed, ${dropped} dropped (cap ${QUERIES_CAP})\n\n`
|
|
397
|
+
: ''
|
|
398
|
+
return { text: header + parts.join('\n\n') }
|
|
399
|
+
}
|
|
400
|
+
const query = String(args.query ?? '').trim()
|
|
401
|
+
let period = String(args.period ?? '').trim() || undefined
|
|
402
|
+
// Period and sort are caller-supplied only. Lead is responsible for
|
|
403
|
+
// mapping vague time phrases / chronological intent into the period
|
|
404
|
+
// argument before calling; the engine does not infer them from query
|
|
405
|
+
// text.
|
|
406
|
+
const RECALL_LIMIT_CAP = 100
|
|
407
|
+
const RECALL_OFFSET_CAP = 500
|
|
408
|
+
const requestedLimit = Number(args.limit)
|
|
409
|
+
const requestedOffset = Number(args.offset)
|
|
410
|
+
let limit = Math.max(1, Number.isFinite(requestedLimit) ? requestedLimit : 10)
|
|
411
|
+
let offset = Math.max(0, Number.isFinite(requestedOffset) ? requestedOffset : 0)
|
|
412
|
+
const recallCapNotes = []
|
|
413
|
+
if (Number.isFinite(requestedLimit) && requestedLimit > RECALL_LIMIT_CAP) {
|
|
414
|
+
limit = RECALL_LIMIT_CAP
|
|
415
|
+
recallCapNotes.push(`limit capped to ${RECALL_LIMIT_CAP} (requested ${requestedLimit})`)
|
|
416
|
+
} else {
|
|
417
|
+
limit = Math.min(RECALL_LIMIT_CAP, limit)
|
|
418
|
+
}
|
|
419
|
+
if (Number.isFinite(requestedOffset) && requestedOffset > RECALL_OFFSET_CAP) {
|
|
420
|
+
offset = RECALL_OFFSET_CAP
|
|
421
|
+
recallCapNotes.push(`offset capped to ${RECALL_OFFSET_CAP} (requested ${requestedOffset})`)
|
|
422
|
+
} else {
|
|
423
|
+
offset = Math.min(RECALL_OFFSET_CAP, offset)
|
|
424
|
+
}
|
|
425
|
+
const recallCapPrefix = recallCapNotes.length ? `${recallCapNotes.join('; ')}\n` : ''
|
|
426
|
+
// Recent-browsing default: a query-less recall is a "show me the latest
|
|
427
|
+
// messages" browse, not a relevance search — chronological order is the
|
|
428
|
+
// only ordering that makes sense there, so sort defaults to 'date' when
|
|
429
|
+
// no query is present (explicit args.sort still wins). Query recalls keep
|
|
430
|
+
// the importance default.
|
|
431
|
+
const hasQueryForSort = Array.isArray(args.query)
|
|
432
|
+
? args.query.some((v) => String(v || '').trim())
|
|
433
|
+
: String(args.query ?? '').trim() !== ''
|
|
434
|
+
const sort = args.sort != null ? String(args.sort) : (hasQueryForSort ? 'importance' : 'date')
|
|
435
|
+
// Chunk content is the primary recall output. Members default to true so
|
|
436
|
+
// callers receive the raw chunk leaves (the cycle1-produced semantic
|
|
437
|
+
// chunks) rather than just the root's cycle2-compressed summary line.
|
|
438
|
+
// Explicit `includeMembers:false` keeps the legacy summary-only mode.
|
|
439
|
+
const includeMembers = args.includeMembers !== false
|
|
440
|
+
// Raw leg defaults ON: recall never drains/chunks inline anymore, so raw
|
|
441
|
+
// (unchunked) rows — embedded by the always-on post-ingest/tick flush —
|
|
442
|
+
// are the only way to see content cycle1 hasn't reached yet, regardless
|
|
443
|
+
// of the recap toggle. The importance/query search leg only ranks chunked
|
|
444
|
+
// entries; readRawRowsInWindow interleaves the raw rows into the hybrid
|
|
445
|
+
// list. Explicit includeRaw:false keeps the chunked-only view.
|
|
446
|
+
const includeRaw = args.includeRaw !== false
|
|
447
|
+
const includeArchived = args.includeArchived !== false
|
|
448
|
+
const category = args.category
|
|
449
|
+
const temporal = parsePeriod(period, Boolean(query))
|
|
450
|
+
|
|
451
|
+
// Derive projectScope from caller cwd (falls back to process.cwd()).
|
|
452
|
+
// Explicit args.projectScope (string) takes priority so callers can
|
|
453
|
+
// override to 'all', 'common', or a specific slug.
|
|
454
|
+
let projectScope
|
|
455
|
+
if (typeof args.projectScope === 'string' && args.projectScope) {
|
|
456
|
+
projectScope = args.projectScope
|
|
457
|
+
} else {
|
|
458
|
+
const projectId = resolveProjectScope(typeof args.cwd === 'string' && args.cwd ? args.cwd : null)
|
|
459
|
+
projectScope = projectId !== null ? projectId : 'common'
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// R11 reviewer M4: calendar-bounded periods disable freshness decay
|
|
463
|
+
// so within-period ranking doesn't downgrade Mon entries vs Sun.
|
|
464
|
+
const CALENDAR_PERIODS = new Set(['yesterday', 'today', 'this_week', 'last_week'])
|
|
465
|
+
const isCalendarPeriod = period != null
|
|
466
|
+
&& (CALENDAR_PERIODS.has(period) || /^\d{4}-\d{2}-\d{2}/.test(period) || /^\d{1,2}:\d{2}~/.test(period))
|
|
467
|
+
const applyFreshness = !isCalendarPeriod
|
|
468
|
+
|
|
469
|
+
// period='last': no time window and no session exclusion — 'last' is a
|
|
470
|
+
// plain "walk back through ALL prior conversation, newest first" browse.
|
|
471
|
+
// No boot-timestamp cap (the old cap hid every session that ran while a
|
|
472
|
+
// long-lived daemon stayed up), no gap-bounded burst, no current-session
|
|
473
|
+
// filter: limit/offset page through history and the grouped renderer
|
|
474
|
+
// separates sessions. temporal stays unbounded (mode marker only).
|
|
475
|
+
|
|
476
|
+
if (query) {
|
|
477
|
+
const _t0 = Date.now()
|
|
478
|
+
if (signal?.aborted) throw signal.reason ?? new Error('aborted')
|
|
479
|
+
let queryVector = null
|
|
480
|
+
if (isEmbeddingModelReady()) {
|
|
481
|
+
queryVector = await embedText(query)
|
|
482
|
+
} else {
|
|
483
|
+
const now = Date.now()
|
|
484
|
+
if (now - _embeddingColdRecallLogAt > 10_000) {
|
|
485
|
+
_embeddingColdRecallLogAt = now
|
|
486
|
+
log('[recall] embedding model cold; returning lexical results while background warmup continues\n')
|
|
487
|
+
}
|
|
488
|
+
if (embeddingWarmupCanStart()) {
|
|
489
|
+
void warmupEmbeddingProvider().catch((err) => {
|
|
490
|
+
log(`[memory-service] embedding warmup after cold recall failed: ${err?.message || err}\n`)
|
|
491
|
+
})
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
if (signal?.aborted) throw signal.reason ?? new Error('aborted')
|
|
495
|
+
const _t1 = Date.now()
|
|
496
|
+
if (process.env.MIXDOG_DEBUG_MEMORY) {
|
|
497
|
+
log(`[search-time] embed=${_t1 - _t0}ms query="${query.slice(0, 60)}"\n`)
|
|
498
|
+
}
|
|
499
|
+
// Push ts and status filters into the hybrid candidate query so FTS / vec
|
|
500
|
+
// rank inside the requested window, not the whole tree. The previous post-
|
|
501
|
+
// filter approach silently emptied results when relevant matches sat
|
|
502
|
+
// outside `period` (default 30d) and could not bubble through.
|
|
503
|
+
// Recall is history-first: archived roots hold most prior work. Callers
|
|
504
|
+
// that need only live invariants can pass includeArchived:false.
|
|
505
|
+
const excludeStatuses = includeArchived ? [] : ['archived']
|
|
506
|
+
const results = await searchRelevantHybrid(db, query, {
|
|
507
|
+
limit: limit + offset,
|
|
508
|
+
queryVector: Array.isArray(queryVector) ? queryVector : null,
|
|
509
|
+
includeMembers,
|
|
510
|
+
ts_from: temporal?.startMs,
|
|
511
|
+
ts_to: temporal?.endMs,
|
|
512
|
+
applyFreshness,
|
|
513
|
+
projectScope,
|
|
514
|
+
category,
|
|
515
|
+
excludeStatuses,
|
|
516
|
+
// useHotActive was set to true here so default (no-period) calls
|
|
517
|
+
// routed through the mv_hot_active materialized view — a narrow
|
|
518
|
+
// active-roots-only pool. Live usage is dominated by vague-time
|
|
519
|
+
// queries ("recent / lately") where Lead callers omit the period
|
|
520
|
+
// filter, leaving the MV as the sole source. That hid every
|
|
521
|
+
// orphan leaf and every pending root — fresh work from the last 1-60
|
|
522
|
+
// minutes never surfaced. Now that the entries-table CTE legs run
|
|
523
|
+
// against broaden HNSW + GIN trgm partial indexes (the
|
|
524
|
+
// is_root=1 predicate was dropped in the same revision), the
|
|
525
|
+
// entries path is fast enough (1-2 ms ANN on ~10K rows, O(log N)
|
|
526
|
+
// through 1M+) to be the single source of truth. The MV is left in
|
|
527
|
+
// place for now but no longer routed to from search; cycle2 may stop
|
|
528
|
+
// refreshing it in a follow-up commit once nothing else reads it.
|
|
529
|
+
useHotActive: false,
|
|
530
|
+
})
|
|
531
|
+
let filtered = results
|
|
532
|
+
if (sort === 'date') {
|
|
533
|
+
// R11 reviewer L5: NaN guard — entries with null/undefined ts default
|
|
534
|
+
// to 0 so the comparator stays numeric and stable.
|
|
535
|
+
filtered.sort((a, b) => (Number(b.ts) || 0) - (Number(a.ts) || 0))
|
|
536
|
+
} else {
|
|
537
|
+
filtered.sort((a, b) => {
|
|
538
|
+
const sa = (v) => { const n = Number(v); return Number.isFinite(n) ? n : 0 }
|
|
539
|
+
return (sa(b.retrievalScore ?? b.rrf ?? 0) - sa(a.retrievalScore ?? a.rrf ?? 0))
|
|
540
|
+
|| (sa(b.score ?? 0) - sa(a.score ?? 0))
|
|
541
|
+
|| (sa(b.ts ?? 0) - sa(a.ts ?? 0))
|
|
542
|
+
|| (Number(a.id ?? 0) - Number(b.id ?? 0))
|
|
543
|
+
})
|
|
544
|
+
}
|
|
545
|
+
if (includeRaw) {
|
|
546
|
+
// Raw rows (chunk_root IS NULL) carry no retrievalScore, so a naive
|
|
547
|
+
// append-after-hybrid under sort=importance always lands them past
|
|
548
|
+
// slice(offset, offset+limit) once the hybrid pool exceeds one page —
|
|
549
|
+
// every page beyond the first silently drops them. Fetch a wider raw
|
|
550
|
+
// window (bounded like the hybrid candidate pool) and spread the
|
|
551
|
+
// fetched raw rows evenly across the WHOLE hybrid list before slicing,
|
|
552
|
+
// so every offset page gets its proportional share instead of only
|
|
553
|
+
// page 0. Same projectScope/ts window as the hybrid leg — filter
|
|
554
|
+
// parity (item 3) is deliberate, not accidental.
|
|
555
|
+
const RAW_FETCH = Math.min(500, Math.max(20, limit + offset))
|
|
556
|
+
const rawRows = await readRawRowsInWindow(
|
|
557
|
+
db,
|
|
558
|
+
temporal?.startMs ?? null,
|
|
559
|
+
temporal?.endMs ?? Date.now(),
|
|
560
|
+
RAW_FETCH,
|
|
561
|
+
{ projectScope, terms: sessionRecallTerms(query) },
|
|
562
|
+
)
|
|
563
|
+
const seenIds = new Set(filtered.map(r => r.id))
|
|
564
|
+
const newRaw = rawRows.filter(r => !seenIds.has(r.id))
|
|
565
|
+
if (sort === 'date') {
|
|
566
|
+
for (const r of newRaw) filtered.push(r)
|
|
567
|
+
filtered.sort((a, b) => (Number(b.ts) || 0) - (Number(a.ts) || 0))
|
|
568
|
+
} else {
|
|
569
|
+
// sort=importance: interleave raw rows at a fixed stride through the
|
|
570
|
+
// full (pre-slice) hybrid list instead of appending at the tail, so
|
|
571
|
+
// offset > 0 pages also draw from the raw pool proportionally.
|
|
572
|
+
filtered = interleaveRawRows(filtered, newRaw)
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
const coreRows = await recallCoreRows(query, { projectScope, category, limit })
|
|
576
|
+
if (coreRows.length > 0) {
|
|
577
|
+
filtered = [...coreRows, ...filtered]
|
|
578
|
+
}
|
|
579
|
+
const sliced = filtered.slice(offset, offset + limit)
|
|
580
|
+
const _t2 = Date.now()
|
|
581
|
+
if (process.env.MIXDOG_DEBUG_MEMORY) {
|
|
582
|
+
log(`[search-time] hybrid+sort+raw=${_t2 - _t1}ms rows=${filtered.length} sliced=${sliced.length}\n`)
|
|
583
|
+
}
|
|
584
|
+
// Emit a recall trace event so getTraceWithEntries() can correlate
|
|
585
|
+
// this search with the top-ranked memory entry. One event per
|
|
586
|
+
// handleSearch call (not per returned row) — cheapest meaningful link.
|
|
587
|
+
// parent_span_id left null: the agent-side span id is only known after
|
|
588
|
+
// the DB insert of the loop/tool events, which happens async on the
|
|
589
|
+
// client side and is not available here.
|
|
590
|
+
if (_traceDb && filtered.length > 0) {
|
|
591
|
+
const topHit = filtered[0]
|
|
592
|
+
const topId = topHit?.id != null ? Number(topHit.id) : null
|
|
593
|
+
if (topId !== null && Number.isFinite(topId)) {
|
|
594
|
+
insertTraceEvents(_traceDb, [{
|
|
595
|
+
ts: Date.now(),
|
|
596
|
+
kind: 'recall',
|
|
597
|
+
entry_id: topId,
|
|
598
|
+
payload: { query: query.slice(0, 200), hit_count: filtered.length },
|
|
599
|
+
}]).catch(e => log(`[trace] insertTraceEvents error: ${e?.message}\n`))
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
const out = { text: recallCapPrefix + renderEntryLines(sliced) }
|
|
603
|
+
if (process.env.MIXDOG_DEBUG_MEMORY) {
|
|
604
|
+
log(`[search-time] render+trace=${Date.now() - _t2}ms total=${Date.now() - _t0}ms textLen=${out.text.length}\n`)
|
|
605
|
+
}
|
|
606
|
+
return out
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
const filters = { limit: limit + offset }
|
|
610
|
+
if (temporal?.startMs != null) { filters.ts_from = temporal.startMs; filters.ts_to = temporal.endMs }
|
|
611
|
+
filters.projectScope = projectScope
|
|
612
|
+
if (category != null) filters.category = category
|
|
613
|
+
filters.sort = sort
|
|
614
|
+
if (!includeArchived) filters.excludeStatuses = ['archived']
|
|
615
|
+
if (includeMembers) filters.includeMembers = true
|
|
616
|
+
const rows = await retrieveEntries(db, filters)
|
|
617
|
+
// Recent-browsing raw merge: a query-less recall must show the freshest
|
|
618
|
+
// turns even when cycle1 hasn't chunked them yet. Roots lag ingest by up
|
|
619
|
+
// to a cycle interval, so on sort=date pull the raw (unchunked) window
|
|
620
|
+
// too and merge chronologically — original text first, no summaries.
|
|
621
|
+
// Query-less + includeRaw:false callers keep the roots-only view.
|
|
622
|
+
let merged = rows
|
|
623
|
+
if (sort === 'date' && args.includeRaw !== false) {
|
|
624
|
+
const rawRows = await readRawRowsInWindow(
|
|
625
|
+
db,
|
|
626
|
+
filters.ts_from ?? temporal?.startMs ?? null,
|
|
627
|
+
filters.ts_to ?? temporal?.endMs ?? Date.now(),
|
|
628
|
+
Math.min(500, Math.max(20, limit + offset)),
|
|
629
|
+
{ projectScope },
|
|
630
|
+
)
|
|
631
|
+
const seenIds = new Set(rows.map(r => Number(r.id)))
|
|
632
|
+
// Drop raw leaves already inlined as some returned root's member.
|
|
633
|
+
for (const r of rows) {
|
|
634
|
+
if (Array.isArray(r.members)) for (const m of r.members) seenIds.add(Number(m.id))
|
|
635
|
+
}
|
|
636
|
+
const newRaw = rawRows.filter(r => !seenIds.has(Number(r.id)))
|
|
637
|
+
if (newRaw.length > 0) {
|
|
638
|
+
merged = [...rows, ...newRaw]
|
|
639
|
+
merged.sort((a, b) => (Number(b.ts) || 0) - (Number(a.ts) || 0) || (Number(b.id) || 0) - (Number(a.id) || 0))
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
const sliced = merged.slice(offset, offset + limit)
|
|
643
|
+
// Multi-session grouping: a GLOBAL query-less browse ("recent work") spans
|
|
644
|
+
// sessions — render grouped per session (newest activity first) with the
|
|
645
|
+
// caller's own session marked "(current)" via the currentSessionId hint.
|
|
646
|
+
// Falls through to the flat list when everything is one session.
|
|
647
|
+
const _currentSessionHint = String(args?.currentSessionId || '').trim()
|
|
648
|
+
return { text: recallCapPrefix + renderSessionGroupedLines(sliced, { currentSessionId: _currentSessionHint }) }
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
async function dumpSessionRootChunks(args = {}) {
|
|
652
|
+
const db = getDb()
|
|
653
|
+
const sessionId = String(args.sessionId || args.session_id || '').trim()
|
|
654
|
+
if (!sessionId) return { text: '(no current session)', rows: [], chunks: [], isError: true }
|
|
655
|
+
const includeRaw = args.includeRaw !== false
|
|
656
|
+
const limit = Math.max(1, Math.min(1000, Number(args.limit) || 1000))
|
|
657
|
+
const rootRows = (await db.query(`
|
|
658
|
+
SELECT id, ts, role, content, session_id, source_turn, chunk_root, is_root,
|
|
659
|
+
element, category, summary, status, score, last_seen_at, project_id
|
|
660
|
+
FROM entries
|
|
661
|
+
WHERE session_id = $1 AND is_root = 1
|
|
662
|
+
ORDER BY COALESCE(source_turn, 2147483647) ASC, ts ASC, id ASC
|
|
663
|
+
LIMIT $2
|
|
664
|
+
`, [sessionId, limit])).rows
|
|
665
|
+
const roots = rootRows.map((r) => ({ ...r, members: [] }))
|
|
666
|
+
const rootIds = roots.map((r) => Number(r.id)).filter((id) => Number.isFinite(id))
|
|
667
|
+
const memberRows = rootIds.length > 0
|
|
668
|
+
? (await db.query(`
|
|
669
|
+
SELECT id, ts, role, content, session_id, source_turn, chunk_root, is_root, project_id
|
|
670
|
+
FROM entries
|
|
671
|
+
WHERE chunk_root = ANY($1::bigint[]) AND is_root = 0
|
|
672
|
+
ORDER BY chunk_root ASC, COALESCE(source_turn, 2147483647) ASC, ts ASC, id ASC
|
|
673
|
+
`, [rootIds])).rows
|
|
674
|
+
: []
|
|
675
|
+
const byRoot = new Map(roots.map((r) => [Number(r.id), r]))
|
|
676
|
+
for (const m of memberRows) {
|
|
677
|
+
const root = byRoot.get(Number(m.chunk_root))
|
|
678
|
+
if (root) root.members.push(m)
|
|
679
|
+
}
|
|
680
|
+
let rawRows = []
|
|
681
|
+
if (includeRaw) {
|
|
682
|
+
rawRows = (await db.query(`
|
|
683
|
+
SELECT id, ts, role, content, session_id, source_turn, chunk_root, is_root, project_id
|
|
684
|
+
FROM entries
|
|
685
|
+
WHERE session_id = $1
|
|
686
|
+
AND is_root = 0
|
|
687
|
+
AND (chunk_root IS NULL OR chunk_root = id)
|
|
688
|
+
ORDER BY COALESCE(source_turn, 2147483647) ASC, ts ASC, id ASC
|
|
689
|
+
LIMIT $2
|
|
690
|
+
`, [sessionId, limit])).rows
|
|
691
|
+
}
|
|
692
|
+
const chunks = []
|
|
693
|
+
for (const root of roots) {
|
|
694
|
+
const memberText = root.members
|
|
695
|
+
.map((m) => `${m.role === 'assistant' ? 'assistant' : m.role === 'user' ? 'user' : m.role}: ${cleanMemoryText(String(m.content ?? ''))}`)
|
|
696
|
+
.filter(Boolean)
|
|
697
|
+
.join('\n')
|
|
698
|
+
const summary = [root.element, root.summary].map((v) => String(v || '').trim()).filter(Boolean).join(' — ')
|
|
699
|
+
chunks.push({
|
|
700
|
+
id: Number(root.id),
|
|
701
|
+
kind: 'root',
|
|
702
|
+
ts: Number(root.ts) || 0,
|
|
703
|
+
sourceTurn: root.source_turn ?? null,
|
|
704
|
+
category: root.category || null,
|
|
705
|
+
summary,
|
|
706
|
+
text: memberText || cleanMemoryText(String(root.content ?? '')),
|
|
707
|
+
members: root.members,
|
|
708
|
+
})
|
|
709
|
+
}
|
|
710
|
+
for (const raw of rawRows) {
|
|
711
|
+
chunks.push({
|
|
712
|
+
id: Number(raw.id),
|
|
713
|
+
kind: 'raw',
|
|
714
|
+
chunkRoot: raw.chunk_root ?? null,
|
|
715
|
+
ts: Number(raw.ts) || 0,
|
|
716
|
+
sourceTurn: raw.source_turn ?? null,
|
|
717
|
+
category: null,
|
|
718
|
+
summary: '',
|
|
719
|
+
text: `${raw.role === 'assistant' ? 'assistant' : raw.role === 'user' ? 'user' : raw.role}: ${cleanMemoryText(String(raw.content ?? ''))}`,
|
|
720
|
+
members: [],
|
|
721
|
+
})
|
|
722
|
+
}
|
|
723
|
+
chunks.sort((a, b) => {
|
|
724
|
+
const at = Number.isFinite(Number(a.sourceTurn)) ? Number(a.sourceTurn) : 2147483647
|
|
725
|
+
const bt = Number.isFinite(Number(b.sourceTurn)) ? Number(b.sourceTurn) : 2147483647
|
|
726
|
+
return (at - bt) || ((a.ts || 0) - (b.ts || 0)) || ((a.id || 0) - (b.id || 0))
|
|
727
|
+
})
|
|
728
|
+
const text = chunks.length
|
|
729
|
+
? chunks.map((chunk, idx) => {
|
|
730
|
+
const label = chunk.kind === 'root'
|
|
731
|
+
? `# chunk ${idx + 1} root=${chunk.id}${chunk.category ? ` category=${chunk.category}` : ''}`
|
|
732
|
+
: `${chunk.chunkRoot == null ? '# raw_pending' : '# raw_terminal'} ${idx + 1} id=${chunk.id}`
|
|
733
|
+
const summary = chunk.summary ? `summary: ${chunk.summary}\n` : ''
|
|
734
|
+
return `${label}\n${summary}${chunk.text}`.trim()
|
|
735
|
+
}).join('\n\n')
|
|
736
|
+
: '(no results)'
|
|
737
|
+
return { text, rows: [...roots, ...rawRows], chunks }
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
async function entryStats() {
|
|
741
|
+
const db = getDb()
|
|
742
|
+
return await db.transaction(async (tx) => {
|
|
743
|
+
const total = (await tx.query(`SELECT COUNT(*) c FROM entries`)).rows[0].c
|
|
744
|
+
const roots = (await tx.query(`SELECT COUNT(*) c FROM entries WHERE is_root = 1`)).rows[0].c
|
|
745
|
+
const active_roots = (await tx.query(`SELECT COUNT(*) c FROM entries WHERE is_root = 1 AND status = 'active'`)).rows[0].c
|
|
746
|
+
const archived_roots = (await tx.query(`SELECT COUNT(*) c FROM entries WHERE is_root = 1 AND status = 'archived'`)).rows[0].c
|
|
747
|
+
const unchunked_leaves = (await tx.query(`SELECT COUNT(*) c FROM entries WHERE chunk_root IS NULL`)).rows[0].c
|
|
748
|
+
const cycle2_pending_roots = (await tx.query(`SELECT COUNT(*) c FROM entries WHERE is_root = 1 AND status = 'pending'`)).rows[0].c
|
|
749
|
+
const core_entries = (await tx.query(`SELECT COUNT(*) c FROM core_entries`)).rows[0].c
|
|
750
|
+
const core_embed_null = (await tx.query(`SELECT COUNT(*) c FROM core_entries WHERE embedding IS NULL`)).rows[0].c
|
|
751
|
+
const active_core_summaries = (await tx.query(`SELECT COUNT(*) c FROM entries WHERE is_root = 1 AND status = 'active' AND core_summary IS NOT NULL`)).rows[0].c
|
|
752
|
+
const active_core_summary_missing = (await tx.query(`
|
|
753
|
+
SELECT COUNT(*) c
|
|
754
|
+
FROM entries
|
|
755
|
+
WHERE is_root = 1
|
|
756
|
+
AND status = 'active'
|
|
757
|
+
AND (core_summary IS NULL OR btrim(core_summary) = '')
|
|
758
|
+
`)).rows[0].c
|
|
759
|
+
const byStatus = (await tx.query(`SELECT status, COUNT(*) c FROM entries WHERE is_root = 1 GROUP BY status`)).rows
|
|
760
|
+
const byCategory = (await tx.query(`SELECT category, COUNT(*) c FROM entries WHERE is_root = 1 AND status = 'active' GROUP BY category ORDER BY c DESC`)).rows
|
|
761
|
+
const mvRows = (await tx.query(`SELECT relispopulated FROM pg_class WHERE relname = 'mv_hot_active' LIMIT 1`)).rows
|
|
762
|
+
const mv_hot_active_populated = mvRows.length ? Boolean(mvRows[0].relispopulated) : null
|
|
763
|
+
return {
|
|
764
|
+
total, roots, active_roots, archived_roots, unchunked_leaves, cycle2_pending_roots,
|
|
765
|
+
core_entries, core_embed_null, active_core_summaries, active_core_summary_missing,
|
|
766
|
+
mv_hot_active_populated,
|
|
767
|
+
byStatus, byCategory,
|
|
768
|
+
}
|
|
769
|
+
})
|
|
770
|
+
}
|
|
771
|
+
|
|
772
|
+
return {
|
|
773
|
+
readRawRowsInWindow,
|
|
774
|
+
recallSessionRows,
|
|
775
|
+
recallCoreRows,
|
|
776
|
+
handleSearch,
|
|
777
|
+
dumpSessionRootChunks,
|
|
778
|
+
entryStats,
|
|
779
|
+
}
|
|
780
|
+
}
|