mixdog 0.9.0 → 0.9.2
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 +10 -3
- package/scripts/_bench-cwc.json +20 -0
- package/scripts/agent-loop-policy-test.mjs +37 -0
- package/scripts/agent-parallel-smoke.mjs +54 -10
- package/scripts/background-task-meta-smoke.mjs +1 -1
- package/scripts/bench-run.mjs +262 -0
- package/scripts/compact-smoke.mjs +12 -0
- package/scripts/compact-trigger-migration-smoke.mjs +67 -1
- package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
- package/scripts/internal-comms-bench.mjs +727 -0
- package/scripts/internal-comms-smoke.mjs +75 -0
- package/scripts/lead-workflow-smoke.mjs +4 -4
- package/scripts/live-worker-smoke.mjs +9 -9
- package/scripts/output-style-bench.mjs +285 -0
- package/scripts/output-style-smoke.mjs +13 -10
- package/scripts/patch-replay.mjs +90 -0
- package/scripts/provider-stream-stall-test.mjs +276 -0
- package/scripts/provider-toolcall-test.mjs +599 -1
- package/scripts/routing-corpus.mjs +281 -0
- package/scripts/session-bench.mjs +1526 -0
- package/scripts/session-diag.mjs +595 -0
- package/scripts/session-ingest-smoke.mjs +2 -2
- package/scripts/task-bench.mjs +207 -0
- package/scripts/tool-failures.mjs +6 -6
- package/scripts/tool-smoke.mjs +306 -66
- package/scripts/toolcall-args-test.mjs +81 -0
- package/src/agents/debugger/AGENT.md +4 -4
- package/src/agents/heavy-worker/AGENT.md +4 -2
- package/src/agents/reviewer/AGENT.md +4 -4
- package/src/agents/worker/AGENT.md +4 -2
- package/src/app.mjs +10 -6
- package/src/defaults/{hidden-roles.json → agents.json} +7 -7
- package/src/examples/schedules/SCHEDULE.example.md +32 -0
- package/src/examples/webhooks/WEBHOOK.example.md +40 -0
- package/src/headless-role.mjs +14 -14
- package/src/help.mjs +1 -0
- package/src/lib/mixdog-debug.cjs +0 -22
- package/src/lib/plugin-paths.cjs +1 -7
- package/src/lib/rules-builder.cjs +34 -56
- package/src/mixdog-session-runtime.mjs +710 -319
- package/src/output-styles/default.md +12 -7
- package/src/output-styles/minimal.md +25 -0
- package/src/output-styles/oneline.md +21 -0
- package/src/output-styles/simple.md +10 -9
- package/src/repl.mjs +12 -4
- package/src/rules/agent/00-common.md +7 -5
- package/src/rules/agent/30-explorer.md +7 -8
- package/src/rules/lead/01-general.md +3 -1
- package/src/rules/lead/lead-tool.md +7 -0
- package/src/rules/shared/01-tool.md +17 -12
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
- package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
- package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
- package/src/runtime/agent/orchestrator/config.mjs +3 -0
- package/src/runtime/agent/orchestrator/context/collect.mjs +131 -67
- package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
- package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
- package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -16
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +359 -106
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +63 -51
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +27 -20
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +210 -21
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +86 -30
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +254 -280
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +191 -50
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +11 -5
- package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +265 -1
- package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
- package/src/runtime/agent/orchestrator/session/loop.mjs +394 -132
- package/src/runtime/agent/orchestrator/session/manager.mjs +217 -170
- package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
- package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -32
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +1 -44
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +0 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
- package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +13 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +18 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -1
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4076 -3985
- package/src/runtime/agent/orchestrator/tools/patch.mjs +116 -2
- package/src/runtime/channels/backends/discord.mjs +99 -9
- package/src/runtime/channels/backends/telegram.mjs +501 -0
- package/src/runtime/channels/index.mjs +441 -1254
- package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
- package/src/runtime/channels/lib/config.mjs +54 -3
- package/src/runtime/channels/lib/drop-trace.mjs +1 -1
- package/src/runtime/channels/lib/executor.mjs +0 -3
- package/src/runtime/channels/lib/format.mjs +4 -2
- package/src/runtime/channels/lib/memory-client.mjs +0 -38
- package/src/runtime/channels/lib/output-forwarder.mjs +77 -71
- package/src/runtime/channels/lib/runtime-paths.mjs +29 -6
- package/src/runtime/channels/lib/scheduler.mjs +1 -1
- package/src/runtime/channels/lib/session-discovery.mjs +0 -4
- package/src/runtime/channels/lib/telegram-format.mjs +283 -0
- package/src/runtime/channels/lib/tool-format.mjs +1 -2
- package/src/runtime/channels/lib/transcript-discovery.mjs +20 -11
- package/src/runtime/channels/lib/webhook.mjs +59 -31
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/lib/keychain-cjs.cjs +0 -1
- package/src/runtime/memory/data/runtime-manifest.json +6 -7
- package/src/runtime/memory/index.mjs +187 -43
- package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
- package/src/runtime/memory/lib/core-memory-store.mjs +1 -1
- package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2.mjs +9 -6
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
- package/src/runtime/memory/lib/memory-ops-policy.mjs +0 -1
- package/src/runtime/memory/lib/memory.mjs +101 -4
- package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
- package/src/runtime/memory/lib/runtime-fetcher.mjs +43 -18
- package/src/runtime/memory/lib/session-ingest.mjs +116 -7
- package/src/runtime/memory/lib/trace-store.mjs +69 -22
- package/src/runtime/memory/tool-defs.mjs +6 -3
- package/src/runtime/search/index.mjs +2 -7
- package/src/runtime/search/lib/config.mjs +0 -4
- package/src/runtime/search/lib/state.mjs +1 -15
- package/src/runtime/search/lib/web-tools.mjs +0 -1
- package/src/runtime/shared/channel-notification-routing.mjs +12 -0
- package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
- package/src/runtime/shared/child-spawn-gate.mjs +0 -6
- package/src/runtime/shared/config.mjs +9 -0
- package/src/runtime/shared/llm/http-agent.mjs +12 -5
- package/src/runtime/shared/schedules-store.mjs +21 -19
- package/src/runtime/shared/tool-surface.mjs +98 -13
- package/src/runtime/shared/transcript-writer.mjs +129 -0
- package/src/runtime/shared/update-checker.mjs +214 -0
- package/src/standalone/agent-tool.mjs +255 -109
- package/src/standalone/channel-admin.mjs +133 -40
- package/src/standalone/channel-worker.mjs +8 -291
- package/src/standalone/explore-tool.mjs +2 -2
- package/src/standalone/memory-runtime-proxy.mjs +3 -1
- package/src/standalone/provider-admin.mjs +11 -0
- package/src/standalone/seeds.mjs +1 -11
- package/src/standalone/usage-dashboard.mjs +1 -1
- package/src/tui/App.jsx +2137 -750
- package/src/tui/components/ConfirmBar.jsx +47 -0
- package/src/tui/components/ContextPanel.jsx +5 -3
- package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
- package/src/tui/components/Markdown.jsx +22 -98
- package/src/tui/components/Message.jsx +14 -35
- package/src/tui/components/Picker.jsx +87 -12
- package/src/tui/components/PromptInput.jsx +146 -9
- package/src/tui/components/QueuedCommands.jsx +1 -1
- package/src/tui/components/SlashCommandPalette.jsx +8 -5
- package/src/tui/components/Spinner.jsx +7 -7
- package/src/tui/components/StatusLine.jsx +40 -21
- package/src/tui/components/TextEntryPanel.jsx +51 -7
- package/src/tui/components/ToolExecution.jsx +177 -100
- package/src/tui/components/TurnDone.jsx +4 -4
- package/src/tui/components/UsagePanel.jsx +1 -1
- package/src/tui/components/tool-output-format.mjs +312 -40
- package/src/tui/components/tool-output-format.test.mjs +180 -1
- package/src/tui/display-width.mjs +69 -0
- package/src/tui/display-width.test.mjs +35 -0
- package/src/tui/dist/index.mjs +7324 -2393
- package/src/tui/engine.mjs +287 -126
- package/src/tui/index.jsx +117 -7
- package/src/tui/keyboard-protocol.mjs +42 -0
- package/src/tui/lib/voice-recorder.mjs +453 -0
- package/src/tui/markdown/format-token.mjs +354 -142
- package/src/tui/markdown/format-token.test.mjs +155 -17
- package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
- package/src/tui/markdown/render-ansi.test.mjs +1 -1
- package/src/tui/markdown/streaming-markdown.mjs +167 -0
- package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
- package/src/tui/markdown/table-layout.mjs +9 -9
- package/src/tui/paste-attachments.mjs +0 -11
- package/src/tui/prompt-history-store.mjs +129 -0
- package/src/tui/prompt-history-store.test.mjs +52 -0
- package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
- package/src/tui/theme.mjs +41 -647
- package/src/tui/themes/base.mjs +86 -0
- package/src/tui/themes/basic.mjs +85 -0
- package/src/tui/themes/catppuccin.mjs +72 -0
- package/src/tui/themes/dracula.mjs +70 -0
- package/src/tui/themes/everforest.mjs +71 -0
- package/src/tui/themes/gruvbox.mjs +71 -0
- package/src/tui/themes/index.mjs +71 -0
- package/src/tui/themes/indigo.mjs +78 -0
- package/src/tui/themes/kanagawa.mjs +80 -0
- package/src/tui/themes/light.mjs +81 -0
- package/src/tui/themes/nord.mjs +72 -0
- package/src/tui/themes/onedark.mjs +16 -0
- package/src/tui/themes/rosepine.mjs +70 -0
- package/src/tui/themes/teal.mjs +81 -0
- package/src/tui/themes/tokyonight.mjs +79 -0
- package/src/tui/themes/utils.mjs +106 -0
- package/src/tui/themes/warm.mjs +79 -0
- package/src/tui/transcript-tool-failures.mjs +13 -2
- package/src/ui/markdown.mjs +1 -1
- package/src/ui/model-display.mjs +2 -2
- package/src/ui/statusline.mjs +26 -27
- package/src/vendor/statusline/bin/statusline-lib.mjs +0 -623
- package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
- package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
- package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
- package/src/workflows/default/WORKFLOW.md +39 -12
- package/src/workflows/sequential/WORKFLOW.md +46 -0
- package/src/workflows/solo/WORKFLOW.md +7 -0
- package/vendor/ink/build/display-width.js +62 -0
- package/vendor/ink/build/ink.js +154 -20
- package/vendor/ink/build/measure-text.js +4 -1
- package/vendor/ink/build/output.js +115 -9
- package/vendor/ink/build/render-node-to-output.js +4 -1
- package/vendor/ink/build/render.js +4 -0
- package/src/hooks/lib/permission-rules.cjs +0 -170
- package/src/hooks/lib/settings-loader.cjs +0 -112
- package/src/lib/hook-pipe-path.cjs +0 -10
- package/src/output-styles/extreme-simple.md +0 -20
- package/src/rules/lead/04-workflow.md +0 -51
- package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
- package/src/workflows/default/workflow.json +0 -13
- package/src/workflows/solo/workflow.json +0 -7
|
@@ -90,6 +90,8 @@ import {
|
|
|
90
90
|
stableSessionSourceRef,
|
|
91
91
|
sessionMessageContent,
|
|
92
92
|
createIngestTurnAllocator,
|
|
93
|
+
sessionMessageContentForIngest,
|
|
94
|
+
shouldExcludeIngestMessage,
|
|
93
95
|
} from './lib/session-ingest.mjs'
|
|
94
96
|
import { configureEmbedding, embedText, embedTexts, getEmbeddingDims, getEmbeddingDtype, getEmbeddingModelId, getKnownDimsForCurrentModel, isEmbeddingModelReady, primeEmbeddingDims, warmupEmbeddingProvider } from './lib/embedding-provider.mjs'
|
|
95
97
|
import { startLlmWorker, stopLlmWorker } from './lib/llm-worker-host.mjs'
|
|
@@ -98,6 +100,7 @@ import { loadConfig as loadAgentConfig } from '../agent/orchestrator/config.mjs'
|
|
|
98
100
|
import { initProviders } from '../agent/orchestrator/providers/registry.mjs'
|
|
99
101
|
import { makeAgentDispatch } from '../agent/orchestrator/agent-runtime/agent-dispatch.mjs'
|
|
100
102
|
import { getInFlightCycle1 } from './lib/memory-cycle1.mjs'
|
|
103
|
+
import { drainSessionCycle1 } from '../agent/orchestrator/session/compact.mjs'
|
|
101
104
|
import { claimAndMarkScheduledCycle, makeCycleRequestSignature, resolveCoalesceMaxRetries, scheduleCoalescedCycleRetry } from './lib/memory-cycle-requests.mjs'
|
|
102
105
|
import { searchRelevantHybrid } from './lib/memory-recall-store.mjs'
|
|
103
106
|
import { fetchEntriesByIdsScoped } from './lib/memory-recall-id-patch.mjs'
|
|
@@ -398,6 +401,13 @@ let _startupTimeout = null
|
|
|
398
401
|
// getting the real stats and others getting `skippedInFlight: true` from
|
|
399
402
|
// the inner guard.
|
|
400
403
|
let _cycle1InFlight = null // shared cycle1 promise (outer coalesce layer)
|
|
404
|
+
// Per-session recall-drain in-flight map. Multiple terminals share one
|
|
405
|
+
// memory daemon (active-instance.json memory_port); concurrent recall calls
|
|
406
|
+
// for the SAME session must share one drain promise (dedup), while DIFFERENT
|
|
407
|
+
// sessions drain in parallel (cycle1's own advisory-lock/coalesce guard in
|
|
408
|
+
// memory-cycle1.mjs still serializes the actual DB work). A drain never
|
|
409
|
+
// blocks recall for sessions other than its own.
|
|
410
|
+
const _recallDrainInFlight = new Map() // sessionId -> Promise
|
|
401
411
|
let _initialized = false
|
|
402
412
|
let _initPromise = null
|
|
403
413
|
let _stopPromise = null
|
|
@@ -683,6 +693,28 @@ async function readRawRowsInWindow(db, tsFromMs, tsToMs, hardLimit = 10, { proje
|
|
|
683
693
|
} catch { return [] }
|
|
684
694
|
}
|
|
685
695
|
|
|
696
|
+
// Spread raw (unchunked) rows evenly across a hybrid-ranked list at a fixed
|
|
697
|
+
// stride so every offset page draws its proportional share of raw rows,
|
|
698
|
+
// instead of them all landing after the hybrid page-0 window (the old
|
|
699
|
+
// append-only behaviour). Stride is derived from the ratio of hybrid:raw
|
|
700
|
+
// counts so a small raw set doesn't get pushed arbitrarily deep, and a large
|
|
701
|
+
// raw set doesn't crowd out every hybrid hit near the top.
|
|
702
|
+
function interleaveRawRows(hybridRows, rawRows) {
|
|
703
|
+
if (!Array.isArray(rawRows) || rawRows.length === 0) return hybridRows
|
|
704
|
+
const out = []
|
|
705
|
+
const stride = Math.max(1, Math.round(hybridRows.length / (rawRows.length + 1)))
|
|
706
|
+
let rawIdx = 0
|
|
707
|
+
for (let i = 0; i < hybridRows.length; i += 1) {
|
|
708
|
+
out.push(hybridRows[i])
|
|
709
|
+
if ((i + 1) % stride === 0 && rawIdx < rawRows.length) {
|
|
710
|
+
out.push(rawRows[rawIdx])
|
|
711
|
+
rawIdx += 1
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
while (rawIdx < rawRows.length) out.push(rawRows[rawIdx++])
|
|
715
|
+
return out
|
|
716
|
+
}
|
|
717
|
+
|
|
686
718
|
function sessionRecallTerms(query) {
|
|
687
719
|
return [...new Set(String(query || '').toLowerCase().match(/[\p{L}\p{N}_./:-]{2,}/gu) || [])]
|
|
688
720
|
.filter((term) => !CORE_RECALL_STOPWORDS.has(term))
|
|
@@ -788,12 +820,21 @@ async function ingestSessionMessages(args = {}) {
|
|
|
788
820
|
const m = messages[i]
|
|
789
821
|
if (!m || typeof m !== 'object') continue
|
|
790
822
|
const role = normalizeIngestRole(m.role)
|
|
791
|
-
//
|
|
792
|
-
//
|
|
793
|
-
//
|
|
794
|
-
// recent tool/system/developer state that recall fast-track must surface.
|
|
823
|
+
// ingest_session persists user/assistant only; system/developer/tool rows
|
|
824
|
+
// are dropped so recall-fasttrack summaries stay conversation-focused and
|
|
825
|
+
// do not re-inject content already in the protected system prefix.
|
|
795
826
|
if (!role) continue
|
|
796
|
-
|
|
827
|
+
// Exclude synthetic / non-conversation rows entirely (reference-files
|
|
828
|
+
// injections, compaction summaries, protected-context `.` acks, internal
|
|
829
|
+
// runtime nudges). These are mechanical noise, not conversation.
|
|
830
|
+
if (shouldExcludeIngestMessage(m)) continue
|
|
831
|
+
// Pure-conversation shaping: only the human/model prose text. The ingest
|
|
832
|
+
// shaper NEVER inlines tool_call / tool_result traces and strips the
|
|
833
|
+
// deterministic manager.mjs user-turn prefix envelopes (# Session /
|
|
834
|
+
// # Additional context / # Prefetch / # Task) so only the real human
|
|
835
|
+
// prompt remains. cleanMemoryText then removes the <system-reminder> block
|
|
836
|
+
// and residual transcript noise.
|
|
837
|
+
const content = cleanMemoryText(sessionMessageContentForIngest(m))
|
|
797
838
|
if (!content || !content.trim()) continue
|
|
798
839
|
considered += 1
|
|
799
840
|
const tsMs = parseTsToMs(m.ts ?? m.timestamp ?? (Date.now() - (messages.length - i)))
|
|
@@ -1179,7 +1220,7 @@ let _cycle1AgentDispatch = null
|
|
|
1179
1220
|
function getCycle1CallLlm() {
|
|
1180
1221
|
if (!_cycle1AgentDispatch) {
|
|
1181
1222
|
_cycle1AgentDispatch = makeAgentDispatch({
|
|
1182
|
-
|
|
1223
|
+
agent: 'cycle1-agent',
|
|
1183
1224
|
taskType: 'maintenance',
|
|
1184
1225
|
sourceType: 'memory-cycle',
|
|
1185
1226
|
// cycle1 parses the full raw line-format response; the agent brief cap
|
|
@@ -1207,7 +1248,7 @@ let _cycle2AgentDispatch = null
|
|
|
1207
1248
|
function getCycle2CallLlm() {
|
|
1208
1249
|
if (!_cycle2AgentDispatch) {
|
|
1209
1250
|
_cycle2AgentDispatch = makeAgentDispatch({
|
|
1210
|
-
|
|
1251
|
+
agent: 'cycle2-agent',
|
|
1211
1252
|
taskType: 'maintenance',
|
|
1212
1253
|
sourceType: 'memory-cycle',
|
|
1213
1254
|
brief: false,
|
|
@@ -1227,7 +1268,7 @@ let _cycle3AgentDispatch = null
|
|
|
1227
1268
|
function getCycle3CallLlm() {
|
|
1228
1269
|
if (!_cycle3AgentDispatch) {
|
|
1229
1270
|
_cycle3AgentDispatch = makeAgentDispatch({
|
|
1230
|
-
|
|
1271
|
+
agent: 'cycle3-agent',
|
|
1231
1272
|
taskType: 'maintenance',
|
|
1232
1273
|
sourceType: 'memory-cycle',
|
|
1233
1274
|
brief: false,
|
|
@@ -1816,10 +1857,94 @@ async function recallCoreRows(query, { projectScope, category, limit } = {}) {
|
|
|
1816
1857
|
}))
|
|
1817
1858
|
}
|
|
1818
1859
|
|
|
1860
|
+
// Session-scoped full drain before a recall(query) search. Chunking is
|
|
1861
|
+
// partitioned by session_id (memory-cycle1.mjs:409-431 selected_sessions
|
|
1862
|
+
// GROUP BY session_id + ROW_NUMBER PARTITION BY session_id), so this drains
|
|
1863
|
+
// ONLY the calling session's backlog via the same session_id override
|
|
1864
|
+
// (memory-cycle1.mjs:394) — other sessions' pending rows are left for the
|
|
1865
|
+
// periodic cycle1 sweep. Reuses recall-fasttrack's drainSessionCycle1
|
|
1866
|
+
// (session/compact.mjs:504) which loops cycle1 until raw rows hit 0 or a
|
|
1867
|
+
// no-progress pass. Safety limits: a 2,000-row hard cap (both in the pending
|
|
1868
|
+
// pre-check and as the per-call cycle1 batch ceiling) and cooperative
|
|
1869
|
+
// abort-signal propagation through handleMemoryAction — no time budget.
|
|
1870
|
+
//
|
|
1871
|
+
// Multi-terminal safety: the memory daemon is one shared instance across
|
|
1872
|
+
// terminals (active-instance.json memory_port). Concurrent recall(query)
|
|
1873
|
+
// calls for the SAME session share one in-flight drain promise (dedup);
|
|
1874
|
+
// different sessions drain independently in parallel (cycle1's own
|
|
1875
|
+
// advisory-lock/coalesce guard in memory-cycle1.mjs still serializes actual
|
|
1876
|
+
// DB writes). A drain only gates recall for its OWN session — every other
|
|
1877
|
+
// terminal's recall (different session, or no session) proceeds unblocked.
|
|
1878
|
+
const RECALL_DRAIN_HARD_CAP_ROWS = 2000
|
|
1879
|
+
async function _drainSessionForRecallQuery(sessionId, signal) {
|
|
1880
|
+
if (!sessionId || signal?.aborted) return
|
|
1881
|
+
const existing = _recallDrainInFlight.get(sessionId)
|
|
1882
|
+
if (existing) { try { await existing } catch {} return }
|
|
1883
|
+
let pendingCount = 0
|
|
1884
|
+
try {
|
|
1885
|
+
const r = await db.query(
|
|
1886
|
+
`SELECT COUNT(*) c FROM entries WHERE chunk_root IS NULL AND session_id = $1`,
|
|
1887
|
+
[sessionId],
|
|
1888
|
+
)
|
|
1889
|
+
pendingCount = Number(r.rows?.[0]?.c ?? 0)
|
|
1890
|
+
} catch { return }
|
|
1891
|
+
if (pendingCount <= 0) return
|
|
1892
|
+
const runTool = async (_name, toolArgs) => {
|
|
1893
|
+
const result = await handleMemoryAction(toolArgs, signal)
|
|
1894
|
+
return result?.text ?? ''
|
|
1895
|
+
}
|
|
1896
|
+
const dumpArgs = {
|
|
1897
|
+
action: 'dump_session_roots',
|
|
1898
|
+
sessionId,
|
|
1899
|
+
includeRaw: true,
|
|
1900
|
+
limit: Math.min(RECALL_DRAIN_HARD_CAP_ROWS, Math.max(1000, pendingCount)),
|
|
1901
|
+
}
|
|
1902
|
+
const drainPromise = (async () => {
|
|
1903
|
+
try {
|
|
1904
|
+
return await drainSessionCycle1(runTool, {
|
|
1905
|
+
sessionId,
|
|
1906
|
+
dumpArgs,
|
|
1907
|
+
deadlineMs: 0, // no time budget — drive the session's backlog to 0
|
|
1908
|
+
maxPasses: 20, // safety stopper; drainSessionCycle1 also self-stops on no-progress
|
|
1909
|
+
cycleArgs: {
|
|
1910
|
+
min_batch: 1,
|
|
1911
|
+
session_cap: 1,
|
|
1912
|
+
batch_size: RECALL_DRAIN_HARD_CAP_ROWS,
|
|
1913
|
+
rows_per_session: RECALL_DRAIN_HARD_CAP_ROWS,
|
|
1914
|
+
window_size: 20,
|
|
1915
|
+
concurrency: 4,
|
|
1916
|
+
},
|
|
1917
|
+
})
|
|
1918
|
+
} catch (err) {
|
|
1919
|
+
__mixdogMemoryLog(`[recall] session drain aborted/failed (sess=${sessionId}): ${err?.message || err}\n`)
|
|
1920
|
+
return null
|
|
1921
|
+
}
|
|
1922
|
+
})()
|
|
1923
|
+
_recallDrainInFlight.set(sessionId, drainPromise)
|
|
1924
|
+
try {
|
|
1925
|
+
await drainPromise
|
|
1926
|
+
} finally {
|
|
1927
|
+
if (_recallDrainInFlight.get(sessionId) === drainPromise) _recallDrainInFlight.delete(sessionId)
|
|
1928
|
+
}
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1819
1931
|
async function handleSearch(args, signal) {
|
|
1820
1932
|
// Cooperative abort check: throw early if the caller already aborted
|
|
1821
1933
|
// (IPC cancel handler signals the AbortController before re-entry).
|
|
1822
1934
|
if (signal?.aborted) throw signal.reason ?? new Error('aborted')
|
|
1935
|
+
// Drain this session's unchunked backlog to zero BEFORE searching, so a
|
|
1936
|
+
// query recall never returns raw/unclassified transcript tail instead of
|
|
1937
|
+
// the chunked summary. Only runs when both a session and a query are
|
|
1938
|
+
// present; a bare recent-browsing call (no query) skips this entirely.
|
|
1939
|
+
{
|
|
1940
|
+
const _drainSessionId = String(args?.sessionId || args?.session_id || '').trim()
|
|
1941
|
+
const _drainHasQuery = Array.isArray(args?.query)
|
|
1942
|
+
? args.query.some((v) => String(v || '').trim())
|
|
1943
|
+
: String(args?.query ?? '').trim() !== ''
|
|
1944
|
+
if (_drainSessionId && _drainHasQuery) {
|
|
1945
|
+
await _drainSessionForRecallQuery(_drainSessionId, signal)
|
|
1946
|
+
}
|
|
1947
|
+
}
|
|
1823
1948
|
if (args?.currentSession === true || args?.sessionId || args?.session_id) {
|
|
1824
1949
|
return await recallSessionRows(args)
|
|
1825
1950
|
}
|
|
@@ -1936,8 +2061,6 @@ async function handleSearch(args, signal) {
|
|
|
1936
2061
|
})),
|
|
1937
2062
|
deadlineRace,
|
|
1938
2063
|
])
|
|
1939
|
-
} catch (err) {
|
|
1940
|
-
throw err
|
|
1941
2064
|
} finally {
|
|
1942
2065
|
clearTimeout(deadlineTimer)
|
|
1943
2066
|
}
|
|
@@ -2072,13 +2195,16 @@ async function handleSearch(args, signal) {
|
|
|
2072
2195
|
})
|
|
2073
2196
|
}
|
|
2074
2197
|
if (includeRaw) {
|
|
2075
|
-
//
|
|
2076
|
-
//
|
|
2077
|
-
//
|
|
2078
|
-
//
|
|
2079
|
-
// window
|
|
2080
|
-
//
|
|
2081
|
-
|
|
2198
|
+
// Raw rows (chunk_root IS NULL) carry no retrievalScore, so a naive
|
|
2199
|
+
// append-after-hybrid under sort=importance always lands them past
|
|
2200
|
+
// slice(offset, offset+limit) once the hybrid pool exceeds one page —
|
|
2201
|
+
// every page beyond the first silently drops them. Fetch a wider raw
|
|
2202
|
+
// window (bounded like the hybrid candidate pool) and spread the
|
|
2203
|
+
// fetched raw rows evenly across the WHOLE hybrid list before slicing,
|
|
2204
|
+
// so every offset page gets its proportional share instead of only
|
|
2205
|
+
// page 0. Same projectScope/ts window as the hybrid leg — filter
|
|
2206
|
+
// parity (item 3) is deliberate, not accidental.
|
|
2207
|
+
const RAW_FETCH = Math.min(500, Math.max(20, limit + offset))
|
|
2082
2208
|
const rawRows = await readRawRowsInWindow(
|
|
2083
2209
|
db,
|
|
2084
2210
|
temporal?.startMs ?? null,
|
|
@@ -2092,10 +2218,10 @@ async function handleSearch(args, signal) {
|
|
|
2092
2218
|
for (const r of newRaw) filtered.push(r)
|
|
2093
2219
|
filtered.sort((a, b) => (Number(b.ts) || 0) - (Number(a.ts) || 0))
|
|
2094
2220
|
} else {
|
|
2095
|
-
// sort=importance:
|
|
2096
|
-
//
|
|
2097
|
-
//
|
|
2098
|
-
|
|
2221
|
+
// sort=importance: interleave raw rows at a fixed stride through the
|
|
2222
|
+
// full (pre-slice) hybrid list instead of appending at the tail, so
|
|
2223
|
+
// offset > 0 pages also draw from the raw pool proportionally.
|
|
2224
|
+
filtered = interleaveRawRows(filtered, newRaw)
|
|
2099
2225
|
}
|
|
2100
2226
|
}
|
|
2101
2227
|
const coreRows = await recallCoreRows(query, { projectScope, category, limit })
|
|
@@ -3666,26 +3792,6 @@ const httpServer = http.createServer(async (req, res) => {
|
|
|
3666
3792
|
return
|
|
3667
3793
|
}
|
|
3668
3794
|
|
|
3669
|
-
if (req.method === 'POST' && req.url === '/session-start/recap') {
|
|
3670
|
-
try {
|
|
3671
|
-
const body = await readBody(req)
|
|
3672
|
-
const projectId = resolveProjectScope(typeof body.cwd === 'string' && body.cwd ? body.cwd : null)
|
|
3673
|
-
const rows = projectId !== null
|
|
3674
|
-
? (await db.query(`
|
|
3675
|
-
SELECT id, ts, summary FROM entries
|
|
3676
|
-
WHERE is_root = 1 AND (project_id IS NULL OR project_id = $1)
|
|
3677
|
-
ORDER BY ts DESC, id DESC LIMIT 20
|
|
3678
|
-
`, [projectId])).rows
|
|
3679
|
-
: (await db.query(`
|
|
3680
|
-
SELECT id, ts, summary FROM entries
|
|
3681
|
-
WHERE is_root = 1
|
|
3682
|
-
ORDER BY ts DESC, id DESC LIMIT 20
|
|
3683
|
-
`)).rows
|
|
3684
|
-
sendJson(res, { ok: true, projectId, rows })
|
|
3685
|
-
} catch (e) { sendError(res, e.message) }
|
|
3686
|
-
return
|
|
3687
|
-
}
|
|
3688
|
-
|
|
3689
3795
|
if (req.method === 'POST' && req.url === '/api/tool') {
|
|
3690
3796
|
if (!isLocalOrigin(req)) {
|
|
3691
3797
|
sendJson(res, { content: [{ type: 'text', text: 'forbidden: cross-origin' }], isError: true }, 403)
|
|
@@ -3941,8 +4047,46 @@ export async function stop() {
|
|
|
3941
4047
|
// postmaster keeps running after the memory service exits.
|
|
3942
4048
|
if (!memorySecondaryMode()) {
|
|
3943
4049
|
try {
|
|
3944
|
-
|
|
3945
|
-
|
|
4050
|
+
// Conservative check: only skip stopPgForShutdown when the owner
|
|
4051
|
+
// record is unambiguously (a) a memory-runtime-daemon owner record
|
|
4052
|
+
// (kind check — guards against a stale/foreign pid reusing this pid
|
|
4053
|
+
// number for an unrelated process) and (b) that pid is alive AND not
|
|
4054
|
+
// this process. Any read/parse failure or ambiguous state falls back
|
|
4055
|
+
// to stopping PG (previous unconditional behavior) rather than
|
|
4056
|
+
// risking an orphaned PG postmaster.
|
|
4057
|
+
const anotherOwnerAlive = await (async () => {
|
|
4058
|
+
try {
|
|
4059
|
+
const { readSingletonOwner } = await import('../shared/singleton-owner.mjs')
|
|
4060
|
+
const ownerPath = path.join(DATA_DIR, 'memory-runtime-owner.json')
|
|
4061
|
+
const { owner, alive } = readSingletonOwner(ownerPath)
|
|
4062
|
+
if (!alive) return false
|
|
4063
|
+
if (owner?.kind !== 'memory-runtime-daemon') return false
|
|
4064
|
+
const ownerPid = Number(owner?.pid)
|
|
4065
|
+
if (!Number.isInteger(ownerPid) || ownerPid === process.pid) return false
|
|
4066
|
+
// Best-effort process-name check (mirrors supervisor.mjs's
|
|
4067
|
+
// isPostgresPid pattern) — confirms the pid is actually a node
|
|
4068
|
+
// process before trusting it as a live sibling memory owner.
|
|
4069
|
+
// Falls back to true (trust the owner file) when the platform
|
|
4070
|
+
// check is unavailable or inconclusive.
|
|
4071
|
+
try {
|
|
4072
|
+
if (process.platform === 'win32') {
|
|
4073
|
+
const { execFileSync } = await import('node:child_process')
|
|
4074
|
+
const out = execFileSync('tasklist', ['/FI', `PID eq ${ownerPid}`, '/FO', 'CSV', '/NH'], { encoding: 'utf8', windowsHide: true })
|
|
4075
|
+
if (!String(out || '').toLowerCase().includes('node')) return false
|
|
4076
|
+
} else if (process.platform === 'linux') {
|
|
4077
|
+
const comm = fs.readFileSync(`/proc/${ownerPid}/comm`, 'utf8').trim()
|
|
4078
|
+
if (!comm.includes('node')) return false
|
|
4079
|
+
}
|
|
4080
|
+
} catch { /* inconclusive — trust the owner file (already alive+kind-matched) */ }
|
|
4081
|
+
return true
|
|
4082
|
+
} catch { return false }
|
|
4083
|
+
})()
|
|
4084
|
+
if (anotherOwnerAlive) {
|
|
4085
|
+
__mixdogMemoryLog('[memory-service] shutdown: another live memory owner holds memory-runtime-owner.json — leaving PG running\n')
|
|
4086
|
+
} else {
|
|
4087
|
+
const { stopPgForShutdown } = await import('./lib/pg/supervisor.mjs')
|
|
4088
|
+
await stopPgForShutdown()
|
|
4089
|
+
}
|
|
3946
4090
|
} catch {}
|
|
3947
4091
|
} else {
|
|
3948
4092
|
__mixdogMemoryLog('[memory-service] secondary mode; leaving shared PG running\n')
|
|
@@ -40,7 +40,7 @@ function nextCallId() {
|
|
|
40
40
|
* unavailable (worker not forked) or the parent reports an error.
|
|
41
41
|
*
|
|
42
42
|
* @param {object} opts agent-dispatch construction options
|
|
43
|
-
* @param {string} [opts.
|
|
43
|
+
* @param {string} [opts.agent]
|
|
44
44
|
* @param {string} [opts.taskType]
|
|
45
45
|
* @param {string} [opts.mode]
|
|
46
46
|
* @param {string} [opts.preset] preset id/name (passed at call time)
|
|
@@ -86,7 +86,7 @@ export function callAgentDispatch(opts = {}, prompt) {
|
|
|
86
86
|
callId,
|
|
87
87
|
tool: 'agent_dispatch',
|
|
88
88
|
params: {
|
|
89
|
-
|
|
89
|
+
agent: opts.agent || null,
|
|
90
90
|
taskType: opts.taskType || null,
|
|
91
91
|
mode: opts.mode || null,
|
|
92
92
|
preset: opts.preset || null,
|
|
@@ -170,7 +170,7 @@ async function _llmJudgeMerge(existing, incoming) {
|
|
|
170
170
|
`INCOMING: ${incoming.element} — ${String(incoming.summary || '')}`
|
|
171
171
|
try {
|
|
172
172
|
const raw = await callAgentDispatch({
|
|
173
|
-
|
|
173
|
+
agent: 'cycle2-agent',
|
|
174
174
|
taskType: 'maintenance',
|
|
175
175
|
mode: 'core-merge-judge',
|
|
176
176
|
preset: resolveMaintenancePreset('memory'),
|
|
@@ -520,7 +520,7 @@ async function _runCycle1Impl(db, config = {}, options = {}, _dataDir = null) {
|
|
|
520
520
|
const _tLlm = Date.now()
|
|
521
521
|
try {
|
|
522
522
|
raw = await llmCall({
|
|
523
|
-
|
|
523
|
+
agent: 'cycle1-agent',
|
|
524
524
|
taskType: 'maintenance',
|
|
525
525
|
mode: 'cycle1',
|
|
526
526
|
preset,
|
|
@@ -47,7 +47,7 @@ function resourceDir() {
|
|
|
47
47
|
|
|
48
48
|
async function invokeLlm(prompt, mode, preset, timeout, llmCall = callAgentDispatch) {
|
|
49
49
|
return await llmCall({
|
|
50
|
-
|
|
50
|
+
agent: 'cycle2-agent',
|
|
51
51
|
taskType: 'maintenance',
|
|
52
52
|
mode,
|
|
53
53
|
preset,
|
|
@@ -347,7 +347,7 @@ async function _llmJudgePair(summaryA, summaryB, siblingContext = [], options =
|
|
|
347
347
|
`Two memory entries below. Are they restating the same principle? Reply ONE WORD: merge or distinct.\n\nA: ${summaryA}\nB: ${summaryB}${siblings}`
|
|
348
348
|
try {
|
|
349
349
|
const raw = await llmCall({
|
|
350
|
-
|
|
350
|
+
agent: 'cycle2-agent',
|
|
351
351
|
taskType: 'maintenance',
|
|
352
352
|
mode: 'cycle2-phase_merge_judge',
|
|
353
353
|
preset: 'HAIKU',
|
|
@@ -808,7 +808,7 @@ async function sonnetCascade(candidates, rulesDigest, options = {}) {
|
|
|
808
808
|
let raw
|
|
809
809
|
try {
|
|
810
810
|
raw = await llmCall({
|
|
811
|
-
|
|
811
|
+
agent: 'cycle2-agent',
|
|
812
812
|
taskType: 'maintenance',
|
|
813
813
|
mode: 'cycle2-cascade',
|
|
814
814
|
preset,
|
|
@@ -1305,13 +1305,16 @@ async function _runCycle2Impl(db, config = {}, options = {}, dataDir = null) {
|
|
|
1305
1305
|
)
|
|
1306
1306
|
}
|
|
1307
1307
|
} else if (rows.length > 0) {
|
|
1308
|
-
// Parse failure — bump error_count
|
|
1308
|
+
// Parse failure — bump error_count and advance reviewed_at so the
|
|
1309
|
+
// failing row sorts to the back of reviewed_at ASC NULLS FIRST (see
|
|
1310
|
+
// ~:1067-1073), matching CYCLE1_OMITTED_COOLDOWN_MS-style cooldown
|
|
1311
|
+
// semantics instead of spinning forever on the same row.
|
|
1309
1312
|
for (const r of rows) {
|
|
1310
1313
|
throwIfAborted(signal)
|
|
1311
1314
|
try {
|
|
1312
1315
|
await db.query(
|
|
1313
|
-
`UPDATE entries SET error_count = COALESCE(error_count, 0) + 1 WHERE id = $
|
|
1314
|
-
[r.id],
|
|
1316
|
+
`UPDATE entries SET error_count = COALESCE(error_count, 0) + 1, reviewed_at = $1 WHERE id = $2`,
|
|
1317
|
+
[sweepCursor, r.id],
|
|
1315
1318
|
)
|
|
1316
1319
|
} catch {}
|
|
1317
1320
|
}
|
|
@@ -136,9 +136,18 @@ export async function init(db, dims) {
|
|
|
136
136
|
await db.exec(`CREATE INDEX IF NOT EXISTS idx_entries_phase_sweep ON entries(status, is_root, error_count, reviewed_at, id)`)
|
|
137
137
|
await db.exec(`CREATE INDEX IF NOT EXISTS idx_entries_promoted_at ON entries(promoted_at) WHERE promoted_at IS NOT NULL`)
|
|
138
138
|
await db.exec(`CREATE INDEX IF NOT EXISTS idx_entries_tsv ON entries USING GIN (search_tsv)`)
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
139
|
+
// Recall CTEs (memory-recall-store.mjs dense/trgm legs) intentionally match
|
|
140
|
+
// BOTH root and leaf/chunk rows, so their SQL has NO `is_root = 1` predicate
|
|
141
|
+
// (only `embedding IS NOT NULL` / content|element|summary text filters).
|
|
142
|
+
// The old root-only PARTIAL indexes therefore could not be used by those
|
|
143
|
+
// queries — the planner fell back to a Seq Scan + top-N heapsort over every
|
|
144
|
+
// embedding (verified via EXPLAIN ANALYZE). Broaden the predicates to match
|
|
145
|
+
// the query shape so HNSW/GIN are actually used. A `summary` trgm index is
|
|
146
|
+
// added because the trgm leg also filters on `summary` but had no index.
|
|
147
|
+
await db.exec(`CREATE INDEX IF NOT EXISTS idx_entries_content_trgm ON entries USING GIN (content gin_trgm_ops)`)
|
|
148
|
+
await db.exec(`CREATE INDEX IF NOT EXISTS idx_entries_element_trgm ON entries USING GIN (element gin_trgm_ops) WHERE element IS NOT NULL`)
|
|
149
|
+
await db.exec(`CREATE INDEX IF NOT EXISTS idx_entries_summary_trgm ON entries USING GIN (summary gin_trgm_ops) WHERE summary IS NOT NULL`)
|
|
150
|
+
await db.exec(`CREATE INDEX IF NOT EXISTS idx_entries_embedding_hnsw ON entries USING hnsw (embedding halfvec_cosine_ops) WHERE embedding IS NOT NULL`)
|
|
142
151
|
|
|
143
152
|
// BEFORE INSERT/UPDATE trigger keeps score in sync with category + last_seen_at
|
|
144
153
|
// automatically; cycle code no longer needs to UPDATE entries SET score = ...
|
|
@@ -333,13 +342,101 @@ export async function validateEmbeddingDims(db, dimCount) {
|
|
|
333
342
|
}
|
|
334
343
|
}
|
|
335
344
|
|
|
345
|
+
// One-time migration: broaden the recall indexes that were created as root-only
|
|
346
|
+
// PARTIAL indexes (`WHERE is_root = 1 ...`) so they match the recall CTE query
|
|
347
|
+
// predicates (which do NOT restrict is_root — recall matches leaf/chunk rows
|
|
348
|
+
// too). A stale root-only index is unusable by those queries and forces a Seq
|
|
349
|
+
// Scan. This runs on every boot (via ensureCurrentSchemaExtensions), so it is
|
|
350
|
+
// carefully guarded: it only DROP+CREATEs an index when the CURRENT definition
|
|
351
|
+
// still contains the `is_root = 1` predicate. Once broadened, every check is a
|
|
352
|
+
// cheap catalog read and no rebuild happens. Best-effort: catalog-read or DDL
|
|
353
|
+
// failures are logged and swallowed so a boot is never blocked by this.
|
|
354
|
+
async function _migrateRecallIndexesIfStale(db) {
|
|
355
|
+
// Each entry: [indexName, newDefTailPredicate] where the presence of
|
|
356
|
+
// "is_root" in the live indexdef signals the stale root-only shape.
|
|
357
|
+
const targets = [
|
|
358
|
+
{ name: 'idx_entries_content_trgm', create: `CREATE INDEX idx_entries_content_trgm ON entries USING GIN (content gin_trgm_ops)` },
|
|
359
|
+
{ name: 'idx_entries_element_trgm', create: `CREATE INDEX idx_entries_element_trgm ON entries USING GIN (element gin_trgm_ops) WHERE element IS NOT NULL` },
|
|
360
|
+
{ name: 'idx_entries_embedding_hnsw', create: `CREATE INDEX idx_entries_embedding_hnsw ON entries USING hnsw (embedding halfvec_cosine_ops) WHERE embedding IS NOT NULL` },
|
|
361
|
+
]
|
|
362
|
+
try {
|
|
363
|
+
for (const t of targets) {
|
|
364
|
+
let def = null
|
|
365
|
+
try {
|
|
366
|
+
const r = await db.query(`SELECT indexdef FROM pg_indexes WHERE indexname = $1`, [t.name])
|
|
367
|
+
def = r.rows?.[0]?.indexdef ?? null
|
|
368
|
+
} catch { def = null }
|
|
369
|
+
// Missing index → post-loop IF-NOT-EXISTS ensures below self-heal trgm
|
|
370
|
+
// indexes; embedding_hnsw is also re-ensured by ensureCurrentSchemaExtensions.
|
|
371
|
+
if (def == null) continue
|
|
372
|
+
// Already broadened (no is_root predicate) → no-op, no rebuild.
|
|
373
|
+
if (!/is_root/i.test(def)) continue
|
|
374
|
+
try {
|
|
375
|
+
await db.exec(`DROP INDEX IF EXISTS ${t.name}`)
|
|
376
|
+
await db.exec(t.create)
|
|
377
|
+
__mixdogMemoryLog(`[memory] migrated stale root-only index ${t.name} → broadened to match recall query predicates\n`)
|
|
378
|
+
} catch (err) {
|
|
379
|
+
__mixdogMemoryLog(`[memory] recall index migration for ${t.name} failed: ${err?.message || err}\n`)
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
// Self-heal trgm recall indexes on already-bootstrapped DBs: init() will
|
|
383
|
+
// not re-run, and a missing index is not handled by the stale-shape loop
|
|
384
|
+
// above (def == null → continue). These IF-NOT-EXISTS ensures recreate any
|
|
385
|
+
// trgm index that never existed or whose migrate-create failed. No-op (cheap
|
|
386
|
+
// catalog check) when the broadened index is already present.
|
|
387
|
+
try {
|
|
388
|
+
await db.exec(`CREATE INDEX IF NOT EXISTS idx_entries_content_trgm ON entries USING GIN (content gin_trgm_ops)`)
|
|
389
|
+
} catch (err) {
|
|
390
|
+
__mixdogMemoryLog(`[memory] idx_entries_content_trgm ensure failed: ${err?.message || err}\n`)
|
|
391
|
+
}
|
|
392
|
+
try {
|
|
393
|
+
await db.exec(`CREATE INDEX IF NOT EXISTS idx_entries_element_trgm ON entries USING GIN (element gin_trgm_ops) WHERE element IS NOT NULL`)
|
|
394
|
+
} catch (err) {
|
|
395
|
+
__mixdogMemoryLog(`[memory] idx_entries_element_trgm ensure failed: ${err?.message || err}\n`)
|
|
396
|
+
}
|
|
397
|
+
try {
|
|
398
|
+
await db.exec(`CREATE INDEX IF NOT EXISTS idx_entries_summary_trgm ON entries USING GIN (summary gin_trgm_ops) WHERE summary IS NOT NULL`)
|
|
399
|
+
} catch (err) {
|
|
400
|
+
__mixdogMemoryLog(`[memory] idx_entries_summary_trgm ensure failed: ${err?.message || err}\n`)
|
|
401
|
+
}
|
|
402
|
+
} catch (err) {
|
|
403
|
+
__mixdogMemoryLog(`[memory] _migrateRecallIndexesIfStale failed: ${err?.message || err}\n`)
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
|
|
336
407
|
export async function ensureCurrentSchemaExtensions(db, dims) {
|
|
408
|
+
// One-time cleanup: attachment-only placeholder rows ('(attachment)' user
|
|
409
|
+
// content, e.g. Discord backend discord.mjs:724) predate the
|
|
410
|
+
// shouldExcludeIngestMessage() ingest-time filter (session-ingest.mjs).
|
|
411
|
+
// Delete any already-persisted rows so they stop polluting recall/cycle1.
|
|
412
|
+
// Idempotent (no-op once cleaned); best-effort so a failure never blocks boot.
|
|
413
|
+
try {
|
|
414
|
+
const cleaned = await db.query(
|
|
415
|
+
`DELETE FROM entries WHERE content = '(attachment)' AND role = 'user'`,
|
|
416
|
+
)
|
|
417
|
+
const n = Number(cleaned?.rowCount ?? 0)
|
|
418
|
+
if (n > 0) {
|
|
419
|
+
__mixdogMemoryLog(`[memory] ensureCurrentSchemaExtensions: removed ${n} attachment-only placeholder rows\n`)
|
|
420
|
+
}
|
|
421
|
+
} catch (err) {
|
|
422
|
+
__mixdogMemoryLog(`[memory] attachment-placeholder cleanup failed: ${err?.message || err}\n`)
|
|
423
|
+
}
|
|
337
424
|
// core_entries gained an embedding column for cross-table semantic dedup
|
|
338
425
|
// between user-curated rows and cycle2-promoted entries. ALTER + index are
|
|
339
426
|
// idempotent and define the current runtime schema.
|
|
340
427
|
if (Number.isInteger(dims) && dims > 0) {
|
|
341
428
|
await db.exec(`ALTER TABLE core_entries ADD COLUMN IF NOT EXISTS embedding halfvec(${dims})`)
|
|
342
|
-
|
|
429
|
+
// One-time migration for EXISTING deployments (bootstrap-complete DBs never
|
|
430
|
+
// re-run init(), so the broadened index definitions there would otherwise
|
|
431
|
+
// never reach them). This path runs on EVERY boot, so we must NOT
|
|
432
|
+
// unconditionally DROP+CREATE an HNSW index (that would rebuild it on every
|
|
433
|
+
// startup). Only rebuild when the current index still carries the stale
|
|
434
|
+
// root-only `is_root = 1` predicate; once broadened, the check is a no-op.
|
|
435
|
+
await _migrateRecallIndexesIfStale(db)
|
|
436
|
+
// Residual (low risk, no action needed): cycle2's root-active embedding
|
|
437
|
+
// scans (memory-cycle2.mjs) now share this broader all-embedding HNSW
|
|
438
|
+
// instead of a root-only partial index; acceptable at current scale.
|
|
439
|
+
await db.exec(`CREATE INDEX IF NOT EXISTS idx_entries_embedding_hnsw ON entries USING hnsw (embedding halfvec_cosine_ops) WHERE embedding IS NOT NULL`)
|
|
343
440
|
await db.exec(`CREATE INDEX IF NOT EXISTS core_entries_embedding_hnsw ON core_entries USING hnsw (embedding halfvec_cosine_ops) WHERE embedding IS NOT NULL`)
|
|
344
441
|
}
|
|
345
442
|
await db.exec(`ALTER TABLE entries ADD COLUMN IF NOT EXISTS core_summary text`)
|