mixdog 0.9.3 → 0.9.5
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/README.md +112 -38
- package/package.json +10 -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/r5-orchestrated-task.json +7 -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-r13-clientmeta.json +464 -0
- package/scripts/bench/round-r14-betafeatures.json +466 -0
- package/scripts/bench/round-r15-fulldefault.json +462 -0
- package/scripts/bench/round-r16-sessionid.json +466 -0
- package/scripts/bench/round-r17-wirebytes.json +456 -0
- package/scripts/bench/round-r18-prewarm.json +468 -0
- package/scripts/bench/round-r19-clean.json +472 -0
- package/scripts/bench/round-r20-prewarm-clean.json +475 -0
- package/scripts/bench/round-r21-delta-retry.json +473 -0
- package/scripts/bench/round-r22-full-probe.json +693 -0
- package/scripts/bench/round-r23-itemprobe.json +701 -0
- package/scripts/bench/round-r24-shapefix.json +677 -0
- package/scripts/bench/round-r25-serial.json +464 -0
- package/scripts/bench/round-r26-parallel3.json +671 -0
- package/scripts/bench/round-r27-parallel10.json +894 -0
- package/scripts/bench/round-r28-parallel10-stagger.json +882 -0
- package/scripts/bench/round-r29-parallel10-stagger166.json +886 -0
- package/scripts/bench/round-r30-instid.json +253 -0
- package/scripts/bench/round-r31-upgradeprobe.json +256 -0
- package/scripts/bench/round-r32-vs-codex-lead.json +254 -0
- package/scripts/bench/round-r33-vs-codex-codex.json +115 -0
- package/scripts/bench/round-r34-orchestrated.json +120 -0
- package/scripts/bench/round-r35-orchestrated-codex.json +61 -0
- package/scripts/bench/round-r36-orchestrated-capped.json +128 -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 +251 -32
- package/scripts/freevar-smoke.mjs +95 -0
- package/scripts/internal-comms-bench.mjs +3 -4
- package/scripts/internal-comms-smoke.mjs +10 -9
- package/scripts/model-catalog-audit.mjs +209 -0
- package/scripts/model-list-sanitize-test.mjs +37 -0
- 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-bench.mjs +76 -13
- package/scripts/recall-quality-cases.json +12 -0
- package/scripts/recall-usecase-cases.json +18 -0
- package/scripts/session-bench.mjs +152 -6
- package/scripts/tool-smoke.mjs +25 -65
- 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 +9 -8
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +2 -1
- package/src/agents/scheduler-task/AGENT.md +2 -3
- package/src/agents/webhook-handler/AGENT.md +2 -3
- package/src/agents/worker/AGENT.md +10 -7
- package/src/app.mjs +12 -1
- package/src/headless-role.mjs +7 -1
- package/src/lib/rules-builder.cjs +4 -0
- package/src/mixdog-session-runtime.mjs +647 -2056
- package/src/output-styles/default.md +30 -9
- package/src/output-styles/{oneline.md → extreme-minimal.md} +5 -4
- package/src/output-styles/minimal.md +8 -6
- package/src/output-styles/simple.md +21 -7
- package/src/rules/agent/00-common.md +6 -3
- package/src/rules/agent/30-explorer.md +16 -5
- package/src/rules/lead/01-general.md +5 -5
- package/src/rules/lead/lead-brief.md +15 -0
- package/src/rules/lead/lead-tool.md +6 -15
- package/src/rules/shared/01-tool.md +17 -21
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +8 -3
- 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 +105 -11
- package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +356 -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 +740 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-login.mjs +193 -0
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +349 -2131
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +143 -1002
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +229 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-events.mjs +67 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +465 -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/providers/provider-catalog-cache.mjs +80 -0
- 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 +275 -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 +278 -402
- 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/stall-policy.mjs +20 -1
- 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 +35 -44
- 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 +27 -318
- package/src/runtime/channels/backends/telegram.mjs +8 -12
- package/src/runtime/channels/index.mjs +247 -701
- package/src/runtime/channels/lib/backend-dispatch.mjs +46 -0
- package/src/runtime/channels/lib/config.mjs +37 -149
- package/src/runtime/channels/lib/event-pipeline.mjs +22 -5
- package/src/runtime/channels/lib/event-queue.mjs +78 -13
- package/src/runtime/channels/lib/inbound-routing.mjs +74 -0
- package/src/runtime/channels/lib/interaction-workflows.mjs +5 -113
- 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/scheduler.mjs +27 -113
- 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 +313 -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 +43 -616
- package/src/runtime/channels/tool-defs.mjs +11 -130
- package/src/runtime/memory/index.mjs +210 -1948
- package/src/runtime/memory/lib/core-memory-store.mjs +5 -1
- 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/ko-morph.mjs +195 -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 +69 -12
- package/src/runtime/memory/lib/memory-text-utils.mjs +46 -0
- package/src/runtime/memory/lib/pg/supervisor.mjs +1 -1
- package/src/runtime/memory/lib/query-handlers.mjs +802 -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/config.mjs +14 -4
- package/src/runtime/shared/launcher-control.mjs +2 -2
- package/src/runtime/shared/markdown-frontmatter.mjs +19 -0
- package/src/runtime/shared/schedules-store.mjs +13 -3
- package/src/runtime/shared/tool-execution-contract.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 +119 -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 +205 -0
- package/src/session-runtime/quick-search-models.mjs +47 -0
- package/src/session-runtime/session-hooks.mjs +93 -0
- package/src/session-runtime/settings-api.mjs +352 -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/session-runtime/workflow.mjs +1 -1
- 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 +138 -669
- package/src/standalone/channel-admin.mjs +102 -90
- package/src/standalone/channel-worker.mjs +4 -7
- package/src/standalone/explore-tool.mjs +64 -14
- 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 +1059 -8110
- package/src/tui/app/app-format.mjs +213 -0
- package/src/tui/app/channel-pickers.mjs +508 -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 +356 -0
- package/src/tui/app/model-options.mjs +334 -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 +489 -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 +677 -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 +512 -0
- package/src/tui/app/use-transcript-window.mjs +607 -0
- package/src/tui/components/ConfirmBar.jsx +10 -7
- package/src/tui/components/Picker.jsx +64 -15
- package/src/tui/components/PromptInput.jsx +33 -102
- package/src/tui/components/SlashCommandPalette.jsx +8 -1
- package/src/tui/components/StatusLine.jsx +69 -15
- 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 +13553 -12384
- 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 +170 -574
- package/src/tui/figures.mjs +5 -0
- package/src/tui/index.jsx +65 -1
- 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 +77 -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 +16 -18
- package/src/workflows/sequential/WORKFLOW.md +16 -18
- package/vendor/ink/build/display-width.js +19 -3
- package/vendor/ink/build/ink.js +112 -7
- 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/channels/lib/holidays.mjs +0 -138
- 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
|
@@ -213,6 +213,155 @@ export async function flushEmbeddingDirty(db, options = {}) {
|
|
|
213
213
|
}
|
|
214
214
|
}
|
|
215
215
|
|
|
216
|
+
// Embeds raw transcript rows (is_root = 0, non-chunk-member) so the dense
|
|
217
|
+
// recall leg can rank them alongside root/summary entries. Mirrors
|
|
218
|
+
// flushEmbeddingDirty's SKIP LOCKED batch-claim structure but sources the
|
|
219
|
+
// embed text from `content` (raw rows have no element/summary) and honors
|
|
220
|
+
// `limit` as a total cap across the whole call rather than an unbounded loop.
|
|
221
|
+
//
|
|
222
|
+
// Stale-embedding safety when recap toggles back ON: cycle1's promotion
|
|
223
|
+
// UPDATE (memory-cycle1.mjs chunk commit) SETs summary/element, which fires
|
|
224
|
+
// trg_entries_embedding_invalidate (memory.mjs — BEFORE UPDATE OF content,
|
|
225
|
+
// summary, element; NEW.is_root=1 AND summary IS DISTINCT FROM OLD.summary)
|
|
226
|
+
// and NULLs the raw-content embedding written here; cycle1's end-of-cycle
|
|
227
|
+
// flushEmbeddingDirty (is_root=1 AND embedding IS NULL) then re-embeds the
|
|
228
|
+
// root from its fresh element/summary. Member leaves keep their content
|
|
229
|
+
// embedding — the dense recall leg resolves members to their chunk root
|
|
230
|
+
// (memory-recall-store.mjs member→root resolution), so that is leaf-level
|
|
231
|
+
// dense matching, not staleness.
|
|
232
|
+
export async function flushRawEmbeddings(db, options = {}) {
|
|
233
|
+
const { limit = 200, signal } = options ?? {}
|
|
234
|
+
throwIfAborted(signal)
|
|
235
|
+
await ensureEmbCacheTable(db)
|
|
236
|
+
throwIfAborted(signal)
|
|
237
|
+
|
|
238
|
+
let attempted = 0
|
|
239
|
+
let embedded = 0
|
|
240
|
+
let cursor = 0
|
|
241
|
+
|
|
242
|
+
while (attempted < limit) {
|
|
243
|
+
throwIfAborted(signal)
|
|
244
|
+
const batchCap = Math.min(BATCH_SIZE, limit - attempted)
|
|
245
|
+
const client = await db._pool.connect()
|
|
246
|
+
let ids
|
|
247
|
+
try {
|
|
248
|
+
throwIfAborted(signal)
|
|
249
|
+
await client.query('BEGIN')
|
|
250
|
+
const res = await client.query(
|
|
251
|
+
`SELECT id FROM memory.entries
|
|
252
|
+
WHERE is_root = 0 AND chunk_root IS NULL AND embedding IS NULL
|
|
253
|
+
AND NULLIF(btrim(session_id), '') IS NOT NULL
|
|
254
|
+
AND content IS NOT NULL
|
|
255
|
+
AND id > $2
|
|
256
|
+
ORDER BY id
|
|
257
|
+
LIMIT $1
|
|
258
|
+
FOR UPDATE SKIP LOCKED`,
|
|
259
|
+
[batchCap, cursor],
|
|
260
|
+
)
|
|
261
|
+
ids = res.rows.map(r => Number(r.id))
|
|
262
|
+
throwIfAborted(signal)
|
|
263
|
+
} catch (err) {
|
|
264
|
+
try { await client.query('ROLLBACK') } catch {}
|
|
265
|
+
client.release()
|
|
266
|
+
if (signal?.aborted) throw signal.reason ?? err
|
|
267
|
+
__mixdogMemoryLog(`[embed] raw flush SKIP LOCKED claim failed: ${err.message}\n`)
|
|
268
|
+
break
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
if (ids.length === 0) {
|
|
272
|
+
try { await client.query('COMMIT') } catch {}
|
|
273
|
+
client.release()
|
|
274
|
+
break
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
cursor = ids[ids.length - 1]
|
|
278
|
+
attempted += ids.length
|
|
279
|
+
let batchDone = false
|
|
280
|
+
try {
|
|
281
|
+
throwIfAborted(signal)
|
|
282
|
+
const writtenIds = await syncRawBatchEmbeddings(client, ids, { signal })
|
|
283
|
+
embedded += writtenIds.length
|
|
284
|
+
batchDone = true
|
|
285
|
+
} catch (err) {
|
|
286
|
+
if (signal?.aborted) throw signal.reason ?? err
|
|
287
|
+
__mixdogMemoryLog(`[embed] raw batch failed (ids=${ids[0]}..${ids[ids.length-1]}): ${err.message}\n`)
|
|
288
|
+
} finally {
|
|
289
|
+
try {
|
|
290
|
+
if (batchDone) await client.query('COMMIT')
|
|
291
|
+
else await client.query('ROLLBACK')
|
|
292
|
+
} catch {}
|
|
293
|
+
client.release()
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
if (ids.length < batchCap) break
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return { attempted, embedded }
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
// Batch-embed raw transcript rows: content is the embed source text (raw
|
|
303
|
+
// rows have no element/summary). Mirrors syncBatchEmbeddings' shape.
|
|
304
|
+
async function syncRawBatchEmbeddings(db, ids, options = {}) {
|
|
305
|
+
const signal = options?.signal
|
|
306
|
+
throwIfAborted(signal)
|
|
307
|
+
const rows = (await db.query(
|
|
308
|
+
`SELECT id, content FROM memory.entries
|
|
309
|
+
WHERE id = ANY($1::bigint[]) AND is_root = 0 AND chunk_root IS NULL`,
|
|
310
|
+
[ids],
|
|
311
|
+
)).rows
|
|
312
|
+
throwIfAborted(signal)
|
|
313
|
+
if (rows.length === 0) return []
|
|
314
|
+
|
|
315
|
+
const dimsRow = (await db.query(`SELECT value FROM memory.meta WHERE key = 'embedding.current_dims'`)).rows[0]
|
|
316
|
+
throwIfAborted(signal)
|
|
317
|
+
const expected = Number(dimsRow?.value ?? 0)
|
|
318
|
+
|
|
319
|
+
const texts = rows.map(row => (typeof row.content === 'string' ? row.content.trim() : ''))
|
|
320
|
+
const vectors = await cachedEmbedTextBatch(db, texts.filter(Boolean), { signal })
|
|
321
|
+
throwIfAborted(signal)
|
|
322
|
+
let vecIdx = 0
|
|
323
|
+
const updates = [] // { id, content, vector }
|
|
324
|
+
for (let i = 0; i < rows.length; i++) {
|
|
325
|
+
throwIfAborted(signal)
|
|
326
|
+
const row = rows[i]
|
|
327
|
+
if (!texts[i]) continue
|
|
328
|
+
const vector = vectors[vecIdx++]
|
|
329
|
+
if (!Array.isArray(vector) || vector.length === 0) continue
|
|
330
|
+
if (Number.isFinite(expected) && expected > 0 && vector.length !== expected) {
|
|
331
|
+
__mixdogMemoryLog(`[embed] raw dim mismatch (id=${row.id} got=${vector.length} expected=${expected})\n`)
|
|
332
|
+
continue
|
|
333
|
+
}
|
|
334
|
+
updates.push({ id: Number(row.id), content: row.content, vector })
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
if (updates.length === 0) return []
|
|
338
|
+
|
|
339
|
+
const valClauses = updates.map((u, i) => {
|
|
340
|
+
const base = i * 3
|
|
341
|
+
return `($${base+1}::bigint, $${base+2}::halfvec, $${base+3}::text)`
|
|
342
|
+
})
|
|
343
|
+
const params = []
|
|
344
|
+
for (const u of updates) {
|
|
345
|
+
params.push(u.id, embeddingToSql(u.vector), u.content)
|
|
346
|
+
}
|
|
347
|
+
const res = await db.query(
|
|
348
|
+
`UPDATE memory.entries AS e
|
|
349
|
+
SET embedding = t.vector
|
|
350
|
+
FROM (VALUES ${valClauses.join(',')}) AS t(id, vector, content)
|
|
351
|
+
WHERE e.id = t.id AND e.is_root = 0 AND e.chunk_root IS NULL
|
|
352
|
+
AND e.embedding IS NULL
|
|
353
|
+
AND e.content IS NOT DISTINCT FROM t.content
|
|
354
|
+
RETURNING e.id`,
|
|
355
|
+
params,
|
|
356
|
+
)
|
|
357
|
+
throwIfAborted(signal)
|
|
358
|
+
const writtenIds = (res.rows || []).map(r => Number(r.id))
|
|
359
|
+
if (writtenIds.length < updates.length) {
|
|
360
|
+
__mixdogMemoryLog(`[embed-sync] ${updates.length - writtenIds.length} raw stale-write(s) skipped — content changed since read\n`)
|
|
361
|
+
}
|
|
362
|
+
return writtenIds
|
|
363
|
+
}
|
|
364
|
+
|
|
216
365
|
// Batch-embed all ids in one DB round-trip for fetching, one API call per unique text,
|
|
217
366
|
// then one VALUES UPDATE for all results.
|
|
218
367
|
// Returns the list of ids that were actually UPDATEd with a fresh embedding.
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
// Extracted from index.mjs — single/multi-instance process-lock + owner-election
|
|
2
|
+
// helpers. Pure functions parameterized on lock-file paths so index.mjs retains
|
|
3
|
+
// ownership of DATA_DIR-derived constants and the daemon logger.
|
|
4
|
+
import fs from 'node:fs'
|
|
5
|
+
import { execFileSync } from 'node:child_process'
|
|
6
|
+
|
|
7
|
+
export function parsePositivePid(value) {
|
|
8
|
+
const pid = Number(value)
|
|
9
|
+
return Number.isFinite(pid) && pid > 0 ? pid : null
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function isPidAliveLocal(pid) {
|
|
13
|
+
if (!Number.isFinite(pid) || pid <= 0) return false
|
|
14
|
+
try { process.kill(pid, 0); return true }
|
|
15
|
+
catch (e) { return e.code !== 'ESRCH' }
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function tryAcquireMemoryOwnerLock(ownerLockFile, log = () => {}) {
|
|
19
|
+
// Returns true on success (this process now owns memory worker for the data
|
|
20
|
+
// dir), false when a live peer holds the lock. Stale locks (dead PID) are
|
|
21
|
+
// unlinked and retried atomically. Throws on unexpected fs errors so callers
|
|
22
|
+
// surface lock-system corruption rather than silently downgrading.
|
|
23
|
+
//
|
|
24
|
+
// EPERM/EBUSY/EACCES at openSync are transient — AV scanners (SignKorea /
|
|
25
|
+
// SKCert / ezPDFWS etc) briefly lock newly-created files during inspection.
|
|
26
|
+
// The 0.1.x baseline threw immediately and the worker promoted to
|
|
27
|
+
// permanentlyDegraded, killing memory tools for the rest of the session.
|
|
28
|
+
// Treat the AV error codes as retryable with bounded backoff (~750ms total)
|
|
29
|
+
// before giving up and rethrowing.
|
|
30
|
+
for (let attempt = 0; attempt < 5; attempt++) {
|
|
31
|
+
try {
|
|
32
|
+
const fd = fs.openSync(ownerLockFile, 'wx')
|
|
33
|
+
fs.writeSync(fd, String(process.pid))
|
|
34
|
+
fs.closeSync(fd)
|
|
35
|
+
return true
|
|
36
|
+
} catch (e) {
|
|
37
|
+
if (e.code === 'EEXIST') {
|
|
38
|
+
let ownerPid = NaN
|
|
39
|
+
try { ownerPid = Number(fs.readFileSync(ownerLockFile, 'utf8').trim()) } catch {}
|
|
40
|
+
if (isPidAliveLocal(ownerPid)) return false
|
|
41
|
+
// Stale lock: dead owner — unlink and retry exclusive create.
|
|
42
|
+
try { fs.unlinkSync(ownerLockFile) } catch {}
|
|
43
|
+
continue
|
|
44
|
+
}
|
|
45
|
+
const transient = e.code === 'EPERM' || e.code === 'EBUSY' || e.code === 'EACCES'
|
|
46
|
+
if (transient && attempt < 4) {
|
|
47
|
+
// Sync busy-wait acceptable here: this runs on memory worker boot
|
|
48
|
+
// path, once per process; the parent handler is not blocked.
|
|
49
|
+
const end = Date.now() + 50 * (attempt + 1)
|
|
50
|
+
while (Date.now() < end) {}
|
|
51
|
+
continue
|
|
52
|
+
}
|
|
53
|
+
throw e
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return false
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function releaseMemoryOwnerLock(ownerLockFile) {
|
|
60
|
+
try {
|
|
61
|
+
const ownerPid = Number(fs.readFileSync(ownerLockFile, 'utf8').trim())
|
|
62
|
+
if (ownerPid === process.pid) fs.unlinkSync(ownerLockFile)
|
|
63
|
+
} catch {}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function killPreviousServer(pid, log = () => {}) {
|
|
67
|
+
if (pid <= 0 || pid === process.pid) return false
|
|
68
|
+
if (process.platform === 'win32') {
|
|
69
|
+
try {
|
|
70
|
+
execFileSync('taskkill', ['/F', '/T', '/PID', String(pid)], { encoding: 'utf8', timeout: 5000, windowsHide: true })
|
|
71
|
+
log(`[memory-service] Killed previous server PID ${pid}\n`)
|
|
72
|
+
return true
|
|
73
|
+
} catch (e) {
|
|
74
|
+
// Exit code 128 = process not found; treat stale lock as already-dead = success.
|
|
75
|
+
// Status 128 reliably means "process not found" regardless of locale; no text match needed.
|
|
76
|
+
// Status 1 with English text match handles edge cases on some Windows versions.
|
|
77
|
+
const notFoundText = /not found|no running instance/i.test(e.stdout || '')
|
|
78
|
+
|| /not found|no running instance/i.test(e.stderr || '')
|
|
79
|
+
|| /not found|no running instance/i.test(e.message || '')
|
|
80
|
+
const alreadyDead = e.status === 128 || (e.status === 1 && notFoundText)
|
|
81
|
+
if (alreadyDead) {
|
|
82
|
+
log(`[memory-service] PID ${pid} already dead (stale lock), proceeding\n`)
|
|
83
|
+
return true
|
|
84
|
+
}
|
|
85
|
+
log(`[memory-service] taskkill failed for PID ${pid}: ${e.message}\n`)
|
|
86
|
+
return false
|
|
87
|
+
}
|
|
88
|
+
} else {
|
|
89
|
+
// Pre-flight: if the process is already gone, treat stale lock as success.
|
|
90
|
+
try {
|
|
91
|
+
process.kill(pid, 0)
|
|
92
|
+
} catch (e) {
|
|
93
|
+
if (e.code === 'ESRCH') {
|
|
94
|
+
log(`[memory-service] PID ${pid} already dead (stale lock), proceeding\n`)
|
|
95
|
+
return true
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
try { process.kill(pid, 'SIGTERM') } catch {}
|
|
99
|
+
try { process.kill(pid, 'SIGKILL') } catch {}
|
|
100
|
+
// Poll for death up to 2s
|
|
101
|
+
const deadline = Date.now() + 2000
|
|
102
|
+
while (Date.now() < deadline) {
|
|
103
|
+
try {
|
|
104
|
+
process.kill(pid, 0)
|
|
105
|
+
} catch (e) {
|
|
106
|
+
if (e.code === 'ESRCH') {
|
|
107
|
+
log(`[memory-service] Killed previous server PID ${pid}\n`)
|
|
108
|
+
return true
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
// Synchronous 50ms sleep via shared buffer spin
|
|
112
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 50)
|
|
113
|
+
}
|
|
114
|
+
log(`[memory-service] PID ${pid} still alive after SIGKILL\n`)
|
|
115
|
+
return false
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function acquireLock(lockFile, log = () => {}) {
|
|
120
|
+
// Multi-instance guard. In multi-terminal mode the lock owner is a *peer*
|
|
121
|
+
// memory worker serving recall for another CC session. killPreviousServer
|
|
122
|
+
// would taskkill /F that healthy peer mid-flight, then this fork-proxy
|
|
123
|
+
// mode wouldn't even need a lock anyway. Skip the entire kill-the-previous
|
|
124
|
+
// protocol; fork-proxy detection in init() takes priority. If neither
|
|
125
|
+
// proxy nor lock-owner path applies (race window during simultaneous
|
|
126
|
+
// boot), the worker simply continues without the lock — server-main /
|
|
127
|
+
// PG / port-listen handle the actual conflict cases.
|
|
128
|
+
if (process.env.MIXDOG_MULTI_INSTANCE === '1') return
|
|
129
|
+
try {
|
|
130
|
+
if (fs.existsSync(lockFile)) {
|
|
131
|
+
const lockedPid = Number(fs.readFileSync(lockFile, 'utf8').trim())
|
|
132
|
+
if (lockedPid > 0 && lockedPid !== process.pid) {
|
|
133
|
+
const killed = killPreviousServer(lockedPid, log)
|
|
134
|
+
if (!killed) {
|
|
135
|
+
log(`[memory-service] Could not kill previous server PID ${lockedPid}, aborting\n`)
|
|
136
|
+
process.exit(1)
|
|
137
|
+
}
|
|
138
|
+
try { fs.unlinkSync(lockFile) } catch {}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const fd = fs.openSync(lockFile, fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_EXCL, 0o600)
|
|
142
|
+
try {
|
|
143
|
+
fs.writeSync(fd, String(process.pid))
|
|
144
|
+
} finally {
|
|
145
|
+
fs.closeSync(fd)
|
|
146
|
+
}
|
|
147
|
+
} catch (e) {
|
|
148
|
+
if (e.code === 'EEXIST') {
|
|
149
|
+
log(`[memory-service] Lock file exists (EEXIST) — another instance is already running, exiting\n`)
|
|
150
|
+
process.exit(0)
|
|
151
|
+
}
|
|
152
|
+
log(`[memory-service] Lock acquisition failed: ${e.message}\n`)
|
|
153
|
+
process.exit(1)
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export function releaseLock(lockFile) {
|
|
158
|
+
try {
|
|
159
|
+
const content = fs.readFileSync(lockFile, 'utf8').trim()
|
|
160
|
+
if (Number(content) === process.pid) fs.unlinkSync(lockFile)
|
|
161
|
+
} catch {}
|
|
162
|
+
}
|
|
@@ -4,7 +4,7 @@ function __mixdogMemoryLog(...args) {
|
|
|
4
4
|
return __mixdogMemoryStderrWrite(...args);
|
|
5
5
|
}
|
|
6
6
|
|
|
7
|
-
import { buildFtsQuery } from './memory-text-utils.mjs'
|
|
7
|
+
import { buildFtsQuery, buildFtsPrefixQuery } from './memory-text-utils.mjs'
|
|
8
8
|
import { VALID_CATEGORY, embeddingToSql } from './memory.mjs'
|
|
9
9
|
import { freshnessFactor } from './memory-score.mjs'
|
|
10
10
|
import { buildRecallScopeFilter } from './memory-recall-scope-filter.mjs'
|
|
@@ -15,7 +15,34 @@ import { recallReadQuery } from './memory-recall-read-query.mjs'
|
|
|
15
15
|
const _MV_HOT_ACTIVE_TTL_MS = 60_000
|
|
16
16
|
const _mvHotActiveCache = new WeakMap() // db → { populated: boolean, ts: number }
|
|
17
17
|
const SEMANTIC_ONLY_MIN_SIM = 0.72
|
|
18
|
+
// Cross-language semantic rescue: a query in one language against rows in
|
|
19
|
+
// another rarely clears SEMANTIC_ONLY_MIN_SIM on a small (384-dim) embedding
|
|
20
|
+
// model, yet the TOP dense ranks are still overwhelmingly relevant. Accept
|
|
21
|
+
// semantic-only rows that are both top-ranked in the dense leg AND above a
|
|
22
|
+
// softer floor, instead of dropping everything below 0.72.
|
|
23
|
+
const SEMANTIC_TOP_RANK_MAX = 3
|
|
24
|
+
// Floor for the top-rank rescue. 384-dim MiniLM-class embeddings put UNRELATED
|
|
25
|
+
// text at ~0.65-0.75 sim (measured: gibberish query "zzqqxx ... 999" scored
|
|
26
|
+
// 0.749 against real rows), so anything below ~0.70 is noise regardless of
|
|
27
|
+
// rank; cross-language relevant pairs measured 0.67-0.74. 0.70 keeps most of
|
|
28
|
+
// the cross-language rescue while rejecting pure-noise top ranks.
|
|
29
|
+
const SEMANTIC_TOP_RANK_MIN_SIM = 0.70
|
|
18
30
|
const SHORT_QUERY_TOKEN_MAX = 2
|
|
31
|
+
// Dense-similarity weighting for retrievalScore. Lexical RRF sums 1/(K+rank)
|
|
32
|
+
// per matching leg (K=60), so a row hitting 2 lexical legs at rank 1 scores
|
|
33
|
+
// rrf ≈ 2/(60+1) = 0.0328 — enough to bury a single semantically-close dense
|
|
34
|
+
// hit that never shares lexical terms. We add a normalized dense-similarity
|
|
35
|
+
// term so a strong dense-only match can outrank stale multi-leg lexical hits:
|
|
36
|
+
// denseTerm = W_DENSE * max(0, sim - SIM_FLOOR) / (1 - SIM_FLOOR) * freshness
|
|
37
|
+
// At sim=0.85: (0.85-0.60)/(1-0.60) = 0.625, so denseTerm = 0.04 * 0.625 = 0.025
|
|
38
|
+
// with freshness=1. That alone doesn't beat 0.0328, but combined with the
|
|
39
|
+
// row's own rrf contribution (a dense-only row still has 1/(60+rank) from its
|
|
40
|
+
// dense rank, e.g. rank 1 → 0.0164) the total ≈ 0.0164 + 0.025 = 0.0414,
|
|
41
|
+
// which clears the 0.0328 two-leg lexical baseline. W_DENSE=0.04 was chosen
|
|
42
|
+
// as the smallest round value satisfying this at sim=0.85; SIM_FLOOR=0.60
|
|
43
|
+
// matches the noise floor used elsewhere for MiniLM-class embeddings.
|
|
44
|
+
const SIM_FLOOR = 0.60
|
|
45
|
+
const W_DENSE = 0.04
|
|
19
46
|
|
|
20
47
|
function buildExactTerms(query) {
|
|
21
48
|
const clean = String(query ?? '').replace(/\s+/g, ' ').trim()
|
|
@@ -62,7 +89,12 @@ function hasFullQueryTextMatch(query, row) {
|
|
|
62
89
|
function hasQueryTokenCoverage(row, queryTokenCount) {
|
|
63
90
|
if (queryTokenCount <= SHORT_QUERY_TOKEN_MAX) return true
|
|
64
91
|
const hits = Number(row?.exact_hits)
|
|
65
|
-
|
|
92
|
+
// Majority coverage, not full coverage. Requiring EVERY query token to hit
|
|
93
|
+
// made mixed-language queries ("usage spike 원인") return nothing when one
|
|
94
|
+
// token has no lexical counterpart in the row — ~60% of tokens matching is
|
|
95
|
+
// strong enough lexical evidence alongside the semantic leg.
|
|
96
|
+
const required = Math.max(2, Math.ceil(queryTokenCount * 0.6))
|
|
97
|
+
return Number.isFinite(hits) && hits >= required
|
|
66
98
|
}
|
|
67
99
|
|
|
68
100
|
function exactTextBoost(query, row, exactHits) {
|
|
@@ -213,7 +245,13 @@ export async function searchRelevantHybrid(db, query, options = {}) {
|
|
|
213
245
|
? embeddingToSql(options.queryVector)
|
|
214
246
|
: null
|
|
215
247
|
|
|
216
|
-
|
|
248
|
+
// Prefer the Kiwi morph prefix-form query (to_tsquery, ':*' prefix match on
|
|
249
|
+
// content-morpheme stems) when kiwi is ready; else fall back to the plain
|
|
250
|
+
// websearch_to_tsquery path. ftsPrefixMode drives which tsquery ctor the
|
|
251
|
+
// sparse CTE uses (to_tsquery vs websearch_to_tsquery).
|
|
252
|
+
const ftsPrefix = clean.length >= 3 ? buildFtsPrefixQuery(clean) : null
|
|
253
|
+
const ftsQuery = ftsPrefix ? ftsPrefix.query : (clean.length >= 3 ? (buildFtsQuery(clean) ?? null) : null)
|
|
254
|
+
const ftsPrefixMode = Boolean(ftsPrefix)
|
|
217
255
|
const exactTerms = buildExactTerms(clean)
|
|
218
256
|
const queryTokenCount = countQueryTokens(clean)
|
|
219
257
|
const minExactHits = exactTerms.length >= 8 ? 3 : exactTerms.length >= 4 ? 2 : 1
|
|
@@ -312,23 +350,29 @@ dense AS (SELECT NULL::bigint AS id, NULL::float8 AS sim, NULL::bigint AS dense_
|
|
|
312
350
|
|
|
313
351
|
// sparse CTE: active only when ftsQuery is non-null.
|
|
314
352
|
// useHotActive → queries mv_hot_active GIN index (mv_hot_active_tsv).
|
|
353
|
+
// tsqExpr: to_tsquery for the Kiwi morph prefix-form query ('stem:* & ...'),
|
|
354
|
+
// else websearch_to_tsquery for the plain fallback token string. Both parse
|
|
355
|
+
// $2 under the 'simple' config to match search_tsv's simple-config lexemes.
|
|
356
|
+
const tsqExpr = ftsPrefixMode
|
|
357
|
+
? `to_tsquery('simple', $2)`
|
|
358
|
+
: `websearch_to_tsquery('simple', $2)`
|
|
315
359
|
const sparseCte = ftsQuery ? (useHotActive ? `
|
|
316
360
|
sparse AS (
|
|
317
361
|
SELECT id,
|
|
318
|
-
ts_rank_cd(search_tsv,
|
|
319
|
-
ROW_NUMBER() OVER (ORDER BY ts_rank_cd(search_tsv,
|
|
362
|
+
ts_rank_cd(search_tsv, ${tsqExpr}) AS lex,
|
|
363
|
+
ROW_NUMBER() OVER (ORDER BY ts_rank_cd(search_tsv, ${tsqExpr}) DESC) AS sparse_rank
|
|
320
364
|
FROM mv_hot_active
|
|
321
|
-
WHERE search_tsv @@
|
|
365
|
+
WHERE search_tsv @@ ${tsqExpr}
|
|
322
366
|
${mvFilterClause}
|
|
323
367
|
ORDER BY lex DESC
|
|
324
368
|
LIMIT $4
|
|
325
369
|
),` : `
|
|
326
370
|
sparse AS (
|
|
327
371
|
SELECT id,
|
|
328
|
-
ts_rank_cd(search_tsv,
|
|
329
|
-
ROW_NUMBER() OVER (ORDER BY ts_rank_cd(search_tsv,
|
|
372
|
+
ts_rank_cd(search_tsv, ${tsqExpr}) AS lex,
|
|
373
|
+
ROW_NUMBER() OVER (ORDER BY ts_rank_cd(search_tsv, ${tsqExpr}) DESC) AS sparse_rank
|
|
330
374
|
FROM entries
|
|
331
|
-
WHERE search_tsv @@
|
|
375
|
+
WHERE search_tsv @@ ${tsqExpr}
|
|
332
376
|
${filterClause}
|
|
333
377
|
ORDER BY lex DESC
|
|
334
378
|
LIMIT $4
|
|
@@ -472,14 +516,21 @@ LEFT JOIN exact x ON x.id = c.id`
|
|
|
472
516
|
+ (exactRank ? 1 / (K + exactRank) : 0)
|
|
473
517
|
const freshness = applyFreshness ? freshnessFactor(row.ts, nowMs) : 1.0
|
|
474
518
|
const boost = exactTextBoost(clean, row, row.exact_hits)
|
|
475
|
-
|
|
519
|
+
const sim = Number(row.dense_sim)
|
|
520
|
+
const denseTerm = Number.isFinite(sim)
|
|
521
|
+
? W_DENSE * Math.max(0, sim - SIM_FLOOR) / (1 - SIM_FLOOR) * freshness
|
|
522
|
+
: 0
|
|
523
|
+
return { id, row, rrf, freshness, retrievalScore: (rrf * freshness) + boost + denseTerm }
|
|
476
524
|
})
|
|
477
525
|
let semanticOnlyDropped = 0
|
|
478
526
|
let weakTextDropped = 0
|
|
479
527
|
const scored = scoredAll.filter(({ row }) => {
|
|
480
528
|
const hasTextSupport = row.sparse_rank != null || row.trgm_rank != null || row.exact_rank != null
|
|
481
529
|
const sim = Number(row.dense_sim)
|
|
482
|
-
const
|
|
530
|
+
const denseRankNum = row.dense_rank != null ? Number(row.dense_rank) : null
|
|
531
|
+
const hasSemanticSupport = (Number.isFinite(sim) && sim >= SEMANTIC_ONLY_MIN_SIM)
|
|
532
|
+
|| (Number.isFinite(sim) && sim >= SEMANTIC_TOP_RANK_MIN_SIM
|
|
533
|
+
&& denseRankNum != null && denseRankNum <= SEMANTIC_TOP_RANK_MAX)
|
|
483
534
|
if (!hasTextSupport) {
|
|
484
535
|
if (hasSemanticSupport) return true
|
|
485
536
|
semanticOnlyDropped += 1
|
|
@@ -495,7 +546,13 @@ LEFT JOIN exact x ON x.id = c.id`
|
|
|
495
546
|
if (hasFullPhrase) return true
|
|
496
547
|
if (queryTokenCount <= SHORT_QUERY_TOKEN_MAX) return true
|
|
497
548
|
if (row.sparse_rank != null) return true
|
|
498
|
-
|
|
549
|
+
// Semantic support as a second signal only applies when the dense leg
|
|
550
|
+
// actually ran. With a cold embedding model (no queryVector) semantic
|
|
551
|
+
// support is unattainable, and requiring it silently zeroed every
|
|
552
|
+
// multi-token lexical recall until warmup finished. Token coverage alone
|
|
553
|
+
// carries the filter in that degraded mode.
|
|
554
|
+
const semanticLegActive = vecSql != null
|
|
555
|
+
if ((!semanticLegActive || hasSemanticSupport) && hasQueryTokenCoverage(row, queryTokenCount)) return true
|
|
499
556
|
weakTextDropped += 1
|
|
500
557
|
return false
|
|
501
558
|
})
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
cleanMemoryText,
|
|
3
3
|
} from './memory-extraction.mjs'
|
|
4
|
+
import { isReady as koMorphReady, stems as koMorphStems } from './ko-morph.mjs'
|
|
4
5
|
|
|
5
6
|
const MEMORY_TOKEN_STOPWORDS = new Set([
|
|
6
7
|
'a', 'an', 'and', 'are', 'as', 'at', 'be', 'but', 'by', 'did', 'do', 'does', 'for', 'from',
|
|
@@ -55,4 +56,49 @@ export function buildFtsQuery(text) {
|
|
|
55
56
|
return ftsTokens.map(t => t.replace(/["']/g, '')).filter(t => t.length > 0).join(' ')
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
const HANGUL_RE = /[\uAC00-\uD7AF]/
|
|
60
|
+
|
|
61
|
+
// Sanitize a single lexeme for embedding inside a to_tsquery string. Strips the
|
|
62
|
+
// tsquery operator characters so a raw token can never inject syntax.
|
|
63
|
+
function sanitizeLexeme(t) {
|
|
64
|
+
return String(t ?? '').replace(/[&|!():*'"\\\s]+/g, '')
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Prefix-form (to_tsquery) builder. Returns null when kiwi morph is not ready,
|
|
68
|
+
// signalling the caller to keep the websearch_to_tsquery fallback path. When
|
|
69
|
+
// ready: Korean tokens → Kiwi content-morpheme stems (NNG/NNP/VV/VA/XR/SL),
|
|
70
|
+
// non-Korean tokens kept as-is; every lexeme gets a ':*' prefix match and the
|
|
71
|
+
// set is '&'-joined to preserve the current AND semantics.
|
|
72
|
+
//
|
|
73
|
+
// Returns { query, prefix:true } on success, or null.
|
|
74
|
+
export function buildFtsPrefixQuery(text) {
|
|
75
|
+
if (!koMorphReady()) return null
|
|
76
|
+
const tokens = tokenizeMemoryText(text)
|
|
77
|
+
if (tokens.length === 0) return null
|
|
78
|
+
const ftsTokens = [...new Set(tokens)].filter(t => t.length >= 3 || (t.length === 2 && HANGUL_RE.test(t)))
|
|
79
|
+
if (ftsTokens.length === 0) return null
|
|
80
|
+
|
|
81
|
+
const lexemes = []
|
|
82
|
+
const seen = new Set()
|
|
83
|
+
for (const tok of ftsTokens) {
|
|
84
|
+
if (HANGUL_RE.test(tok)) {
|
|
85
|
+
// Analyze against the ORIGINAL token (pre suffix-strip) so Kiwi sees the
|
|
86
|
+
// real inflection; normalizeMemoryToken already ran, so also feed the
|
|
87
|
+
// stripped form — union of both keeps recall high.
|
|
88
|
+
const st = koMorphStems(tok)
|
|
89
|
+
const forms = (st && st.length > 0) ? st : [tok]
|
|
90
|
+
for (const f of forms) {
|
|
91
|
+
const lex = sanitizeLexeme(f)
|
|
92
|
+
if (lex.length >= 1 && !seen.has(lex)) { seen.add(lex); lexemes.push(lex) }
|
|
93
|
+
}
|
|
94
|
+
} else {
|
|
95
|
+
const lex = sanitizeLexeme(tok)
|
|
96
|
+
if (lex.length >= 1 && !seen.has(lex)) { seen.add(lex); lexemes.push(lex) }
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
if (lexemes.length === 0) return null
|
|
100
|
+
const query = lexemes.map(l => `${l}:*`).join(' & ')
|
|
101
|
+
return { query, prefix: true }
|
|
102
|
+
}
|
|
103
|
+
|
|
58
104
|
|
|
@@ -194,7 +194,7 @@ function patchActiveInstance(fields) {
|
|
|
194
194
|
else merged[k] = v;
|
|
195
195
|
}
|
|
196
196
|
return merged;
|
|
197
|
-
}, { compact: true, fsyncDir: true });
|
|
197
|
+
}, { compact: true, fsyncDir: true, renameFallback: 'truncate' });
|
|
198
198
|
} catch (e) {
|
|
199
199
|
__mixdogMemoryLog(`[supervisor-pg] patchActiveInstance failed: ${e?.message}\n`);
|
|
200
200
|
}
|