mixdog 0.9.1 → 0.9.3
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 +9 -1
- 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/anthropic-maxtokens-test.mjs +119 -0
- package/scripts/background-task-meta-smoke.mjs +1 -1
- package/scripts/bench-run.mjs +262 -0
- package/scripts/build-tui.mjs +13 -1
- package/scripts/compact-smoke.mjs +12 -0
- package/scripts/compact-trigger-migration-smoke.mjs +67 -1
- package/scripts/explore-bench.mjs +124 -0
- package/scripts/hook-bus-test.mjs +191 -0
- 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/path-suffix-test.mjs +57 -0
- package/scripts/provider-stream-stall-test.mjs +276 -0
- package/scripts/provider-toolcall-test.mjs +599 -1
- package/scripts/recall-bench.mjs +207 -0
- package/scripts/routing-corpus.mjs +281 -0
- package/scripts/session-bench.mjs +1526 -0
- package/scripts/session-diag.mjs +595 -0
- package/scripts/task-bench.mjs +207 -0
- package/scripts/tool-failures.mjs +6 -6
- package/scripts/tool-smoke.mjs +310 -67
- package/scripts/toolcall-args-test.mjs +81 -0
- package/src/agents/debugger/AGENT.md +4 -4
- package/src/agents/heavy-worker/AGENT.md +20 -9
- package/src/agents/reviewer/AGENT.md +4 -4
- package/src/agents/worker/AGENT.md +17 -9
- 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/rules-builder.cjs +32 -54
- package/src/mixdog-session-runtime.mjs +1040 -2036
- 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 +17 -7
- package/src/rules/agent/00-common.md +7 -5
- package/src/rules/agent/30-explorer.md +8 -12
- package/src/rules/lead/01-general.md +3 -1
- package/src/rules/lead/lead-tool.md +13 -2
- package/src/rules/shared/01-tool.md +23 -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 +182 -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 +100 -18
- 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-max-tokens.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +313 -84
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +104 -38
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +28 -33
- 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/lib/usage-primitives.mjs +32 -0
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +227 -31
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +83 -6
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +229 -115
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +213 -33
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +49 -17
- package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +22 -17
- 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/compact-debug.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +262 -0
- package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +38 -0
- package/src/runtime/agent/orchestrator/session/loop/env.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/loop/hidden-agents.mjs +21 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +49 -0
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +63 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +100 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +218 -0
- package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +101 -0
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +35 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +513 -1000
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +227 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +137 -0
- package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +232 -1152
- 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/builtin/arg-guard.mjs +194 -24
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +162 -0
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
- 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-diagnostics.mjs +42 -2
- 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 +14 -5
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +23 -2
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +50 -39
- 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 +70 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +303 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +43 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +382 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +497 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +295 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +158 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/lang-predicates.mjs +128 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +66 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +44 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1192 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +81 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +280 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +347 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +39 -4189
- package/src/runtime/agent/orchestrator/tools/patch.mjs +119 -5
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
- package/src/runtime/channels/backends/discord.mjs +99 -9
- package/src/runtime/channels/backends/telegram.mjs +501 -0
- package/src/runtime/channels/index.mjs +437 -1439
- package/src/runtime/channels/lib/boot-profile.mjs +23 -0
- package/src/runtime/channels/lib/config.mjs +54 -2
- package/src/runtime/channels/lib/crash-log.mjs +106 -0
- package/src/runtime/channels/lib/format.mjs +4 -2
- package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
- package/src/runtime/channels/lib/output-forwarder.mjs +88 -67
- package/src/runtime/channels/lib/runtime-paths.mjs +29 -0
- package/src/runtime/channels/lib/scheduler.mjs +1 -1
- package/src/runtime/channels/lib/telegram-format.mjs +280 -0
- package/src/runtime/channels/lib/tool-format.mjs +1 -1
- package/src/runtime/channels/lib/transcript-discovery.mjs +19 -1
- package/src/runtime/channels/lib/webhook.mjs +59 -31
- package/src/runtime/channels/lib/whisper-language.mjs +42 -0
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/memory/index.mjs +465 -345
- package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
- package/src/runtime/memory/lib/core-memory-store.mjs +352 -2
- package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
- package/src/runtime/memory/lib/http-wire.mjs +57 -0
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2.mjs +65 -9
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
- package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
- package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
- package/src/runtime/memory/lib/memory.mjs +121 -4
- package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
- package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
- package/src/runtime/memory/lib/recall-format.mjs +183 -0
- package/src/runtime/memory/lib/session-ingest.mjs +107 -0
- package/src/runtime/memory/lib/trace-store.mjs +69 -22
- package/src/runtime/memory/tool-defs.mjs +10 -7
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/background-tasks.mjs +2 -3
- package/src/runtime/shared/buffered-appender.mjs +149 -0
- 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/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/task-notification-envelope.mjs +98 -0
- package/src/runtime/shared/task-notification-envelope.test.mjs +107 -0
- package/src/runtime/shared/tool-execution-contract.mjs +2 -2
- package/src/runtime/shared/tool-surface.mjs +98 -13
- package/src/runtime/shared/transcript-writer.mjs +156 -0
- package/src/runtime/shared/update-checker.mjs +214 -0
- package/src/session-runtime/config-helpers.mjs +209 -0
- package/src/session-runtime/effort.mjs +128 -0
- package/src/session-runtime/fs-utils.mjs +10 -0
- package/src/session-runtime/model-capabilities.mjs +130 -0
- package/src/session-runtime/output-styles.mjs +124 -0
- package/src/session-runtime/plugin-mcp.mjs +114 -0
- package/src/session-runtime/session-text.mjs +100 -0
- package/src/session-runtime/statusline-route.mjs +35 -0
- package/src/session-runtime/tool-catalog.mjs +720 -0
- package/src/session-runtime/workflow.mjs +358 -0
- package/src/standalone/agent-tool.mjs +302 -117
- package/src/standalone/channel-admin.mjs +133 -40
- package/src/standalone/channel-worker.mjs +10 -292
- package/src/standalone/explore-tool.mjs +12 -5
- package/src/standalone/hook-bus.mjs +165 -8
- package/src/standalone/memory-runtime-proxy.mjs +3 -1
- package/src/standalone/opencode-go-login.mjs +121 -0
- package/src/standalone/provider-admin.mjs +25 -3
- package/src/standalone/usage-dashboard.mjs +1 -1
- package/src/tui/App.jsx +2883 -778
- 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 +355 -22
- 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 +183 -101
- package/src/tui/components/TurnDone.jsx +4 -4
- package/src/tui/components/UsagePanel.jsx +1 -1
- package/src/tui/components/tool-output-format.mjs +161 -23
- package/src/tui/components/tool-output-format.test.mjs +87 -0
- package/src/tui/display-width.mjs +69 -0
- package/src/tui/display-width.test.mjs +35 -0
- package/src/tui/dist/index.mjs +6731 -2333
- package/src/tui/engine/agent-envelope.mjs +296 -0
- package/src/tui/engine/boot-profile.mjs +21 -0
- package/src/tui/engine/labels.mjs +67 -0
- package/src/tui/engine/notice-text.mjs +112 -0
- package/src/tui/engine/queue-helpers.mjs +161 -0
- package/src/tui/engine/session-stats.mjs +46 -0
- package/src/tui/engine/tool-call-fields.mjs +23 -0
- package/src/tui/engine/tool-result-text.mjs +126 -0
- package/src/tui/engine.mjs +569 -948
- package/src/tui/index.jsx +117 -7
- package/src/tui/input-editing.mjs +58 -8
- package/src/tui/input-editing.selection.test.mjs +75 -0
- package/src/tui/keyboard-protocol.mjs +42 -0
- package/src/tui/lib/voice-recorder.mjs +469 -0
- package/src/tui/markdown/format-token.mjs +128 -76
- package/src/tui/markdown/format-token.test.mjs +61 -19
- 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 +36 -9
- package/src/tui/paste-fix.test.mjs +119 -0
- 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 -657
- 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 +80 -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 +75 -27
- 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 +46 -12
- package/src/workflows/sequential/WORKFLOW.md +51 -0
- package/src/workflows/solo/WORKFLOW.md +12 -1
- package/vendor/ink/build/display-width.js +62 -0
- package/vendor/ink/build/ink.js +100 -12
- 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/output-styles/extreme-simple.md +0 -20
- package/src/rules/lead/04-workflow.md +0 -51
- package/src/workflows/default/workflow.json +0 -13
- package/src/workflows/solo/workflow.json +0 -7
|
@@ -18,11 +18,35 @@ export function buildCategoryFilterClause(offset, categories, { tableAlias = ''
|
|
|
18
18
|
return { clause: `AND (${inner})`, params: [...cats] }
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
// Shared, param-less predicate excluding promoted/promoting core-candidate
|
|
22
|
+
// roots AND member rows under such a root. Single source of truth so the hybrid
|
|
23
|
+
// recall path (buildRecallScopeFilter) and the query-less browse path
|
|
24
|
+
// (retrieveEntries) can't diverge. tableAlias='' → bare `entries` column refs.
|
|
25
|
+
export function buildPromotedExclusionClauses(tableAlias = '') {
|
|
26
|
+
const p = `${tableAlias || 'entries'}.`
|
|
27
|
+
return [
|
|
28
|
+
// Promoted core-candidate roots have been absorbed into user-curated
|
|
29
|
+
// core_entries and archived by promoteCoreCandidate. Their content now
|
|
30
|
+
// lives in the {{USER_CORE}} slot, so surfacing the stale generated root
|
|
31
|
+
// would double-serve the same fact. 'promoting' (mid-flight or crashed
|
|
32
|
+
// promote awaiting recovery) is excluded too — its root is already archived
|
|
33
|
+
// and finalizes to 'promoted'. Member rows whose chunk_root points at a
|
|
34
|
+
// promoted/promoting root are excluded via EXISTS-on-root (the flag lives
|
|
35
|
+
// only on the root; members keep NULL). Constant predicates — no bind param.
|
|
36
|
+
`(${p}core_candidate_status IS NULL OR ${p}core_candidate_status NOT IN ('promoted', 'promoting'))`,
|
|
37
|
+
`NOT (${p}is_root = 0 AND ${p}chunk_root IS NOT NULL AND ${p}chunk_root <> ${p}id AND EXISTS (
|
|
38
|
+
SELECT 1 FROM entries r WHERE r.id = ${p}chunk_root AND r.is_root = 1 AND r.core_candidate_status IN ('promoted', 'promoting')
|
|
39
|
+
))`,
|
|
40
|
+
]
|
|
41
|
+
}
|
|
42
|
+
|
|
21
43
|
export function buildRecallScopeFilter(offset, options = {}, tableAlias = '') {
|
|
22
44
|
const outerRef = tableAlias || 'entries'
|
|
23
45
|
const p = `${outerRef}.`
|
|
24
46
|
const clauses = [
|
|
25
47
|
`NOT (${p}is_root = 0 AND ${p}chunk_root IS NOT DISTINCT FROM ${p}id AND ${p}status IS NOT DISTINCT FROM 'archived')`,
|
|
48
|
+
// Exclude promoted/promoting roots + their members (shared predicate).
|
|
49
|
+
...buildPromotedExclusionClauses(tableAlias),
|
|
26
50
|
]
|
|
27
51
|
const params = []
|
|
28
52
|
let next = offset
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { recallReadQuery } from './memory-recall-read-query.mjs'
|
|
2
2
|
|
|
3
|
+
import { buildPromotedExclusionClauses } from './memory-recall-scope-filter.mjs'
|
|
4
|
+
|
|
3
5
|
const VALID_CATEGORIES_SET = new Set([
|
|
4
6
|
'rule', 'constraint', 'decision', 'fact', 'goal', 'preference', 'task', 'issue',
|
|
5
7
|
])
|
|
@@ -72,6 +74,12 @@ export async function retrieveEntries(db, filters = {}) {
|
|
|
72
74
|
where.push(`chunk_root IS NULL`)
|
|
73
75
|
}
|
|
74
76
|
|
|
77
|
+
// Exclude promoted/promoting core-candidate roots AND members under such a
|
|
78
|
+
// root — the query-less browse path (includeArchived) would otherwise surface
|
|
79
|
+
// rows the hybrid recall path already filters. Shared predicate (param-less)
|
|
80
|
+
// keeps this in lock-step with buildRecallScopeFilter.
|
|
81
|
+
where.push(...buildPromotedExclusionClauses())
|
|
82
|
+
|
|
75
83
|
const limit = Math.max(1, Math.min(500, Number(filters.limit ?? 50)))
|
|
76
84
|
const offset = Math.max(0, Number(filters.offset ?? 0))
|
|
77
85
|
const sort = String(filters.sort ?? 'importance').trim().toLowerCase()
|
|
@@ -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,19 +342,127 @@ 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`)
|
|
346
443
|
|
|
347
444
|
|
|
348
445
|
|
|
446
|
+
// Core-candidate promotion pipeline (proposal mode): active entries the
|
|
447
|
+
// cycle2 nomination pass flags as strong core-memory candidates. Never
|
|
448
|
+
// auto-inserted into core_entries — a user approves each via the
|
|
449
|
+
// action:'core' op:'promote' handler. Columns are nullable and the ALTERs
|
|
450
|
+
// are idempotent (ADD COLUMN IF NOT EXISTS), safe to re-run every boot.
|
|
451
|
+
// core_candidate_status: NULL (not a candidate) | 'candidate' | 'promoting'
|
|
452
|
+
// (mid-flight promote, recoverable) | 'promoted' | 'dismissed'
|
|
453
|
+
// core_candidate_at: ms timestamp of last nomination/state change
|
|
454
|
+
// 'dismissed'/'promoted' are terminal for a given root so the pass never
|
|
455
|
+
// re-nominates the same entry.
|
|
456
|
+
await db.exec(`ALTER TABLE entries ADD COLUMN IF NOT EXISTS core_candidate_status text`)
|
|
457
|
+
await db.exec(`ALTER TABLE entries ADD COLUMN IF NOT EXISTS core_candidate_at bigint`)
|
|
458
|
+
// No index on core_candidate_status by design (round-2 finding #4): the only
|
|
459
|
+
// readers are listCoreCandidates (user picker, on-demand) and
|
|
460
|
+
// nominateCoreCandidates (once per cycle2, hourly). Both are rare and the
|
|
461
|
+
// entries table is small enough that a seq scan is fine — an index isn't
|
|
462
|
+
// worth the boot-time AccessExclusive build lock on the hot entries table.
|
|
463
|
+
// Drop it if a previous deploy created it (idempotent no-op otherwise).
|
|
464
|
+
await db.exec(`DROP INDEX IF EXISTS idx_entries_core_candidate`)
|
|
465
|
+
|
|
349
466
|
// Dedupe core_entries before creating the unique index — keeps the row with
|
|
350
467
|
// the most recent updated_at (id breaks ties), drops the rest.
|
|
351
468
|
const dedupe = await db.query(`
|
|
@@ -105,31 +105,50 @@ const _checkedConnect = checkedConnect
|
|
|
105
105
|
// native PG db shim
|
|
106
106
|
// ---------------------------------------------------------------------------
|
|
107
107
|
|
|
108
|
-
function makeCompatDb(pgPool, schema) {
|
|
108
|
+
function makeCompatDb(pgPool, schema, dataDir) {
|
|
109
109
|
const db = {
|
|
110
110
|
// query: use pool directly for single-statement queries
|
|
111
111
|
query: async (sql, params) => {
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
112
|
+
return await withPgRetry(dataDir, schema, async (pool) => {
|
|
113
|
+
const client = await _checkedConnect(pool, schema)
|
|
114
|
+
try {
|
|
115
|
+
return await client.query(sql, params)
|
|
116
|
+
} finally {
|
|
117
|
+
client.release()
|
|
118
|
+
}
|
|
119
|
+
})
|
|
118
120
|
},
|
|
119
121
|
|
|
120
122
|
// exec: multi-statement SQL (semicolon-separated); single client for session state
|
|
121
123
|
exec: async (sql) => {
|
|
122
|
-
|
|
124
|
+
// Not retried: exec runs arbitrary multi-statement SQL where a partial
|
|
125
|
+
// failure mid-sequence is unobservable from here — replaying the whole
|
|
126
|
+
// string after recovery risks double-applying already-committed
|
|
127
|
+
// statements. On ECONNREFUSED, trigger recovery in the background (so
|
|
128
|
+
// the NEXT call gets a fresh pool) and propagate the original error.
|
|
129
|
+
const pool = instances.get(`${resolve(dataDir)}|${schema}`)?.pool ?? pgPool
|
|
123
130
|
try {
|
|
124
|
-
await
|
|
125
|
-
|
|
126
|
-
|
|
131
|
+
const client = await _checkedConnect(pool, schema)
|
|
132
|
+
try {
|
|
133
|
+
await client.query(sql)
|
|
134
|
+
} finally {
|
|
135
|
+
client.release()
|
|
136
|
+
}
|
|
137
|
+
} catch (err) {
|
|
138
|
+
if (err?.code === 'ECONNREFUSED') _recoverPgConnection(dataDir, schema).catch(() => {})
|
|
139
|
+
throw err
|
|
127
140
|
}
|
|
128
141
|
},
|
|
129
142
|
|
|
130
143
|
// transaction: check out one client, BEGIN, run callback(tx), COMMIT or ROLLBACK
|
|
131
144
|
transaction: async (fn) => {
|
|
132
|
-
|
|
145
|
+
// Not retried — same rationale as exec(): a COMMIT that fails with
|
|
146
|
+
// ECONNREFUSED leaves the transaction's applied/rolled-back state
|
|
147
|
+
// unknown to this process; blindly replaying fn() could double-apply
|
|
148
|
+
// side effects. Recover in the background for the next caller and
|
|
149
|
+
// propagate the original error immediately.
|
|
150
|
+
const pool = instances.get(`${resolve(dataDir)}|${schema}`)?.pool ?? pgPool
|
|
151
|
+
const client = await _checkedConnect(pool, schema)
|
|
133
152
|
try {
|
|
134
153
|
await client.query('BEGIN')
|
|
135
154
|
const tx = {
|
|
@@ -141,6 +160,7 @@ function makeCompatDb(pgPool, schema) {
|
|
|
141
160
|
return result
|
|
142
161
|
} catch (err) {
|
|
143
162
|
try { await client.query('ROLLBACK') } catch {}
|
|
163
|
+
if (err?.code === 'ECONNREFUSED') _recoverPgConnection(dataDir, schema).catch(() => {})
|
|
144
164
|
throw err
|
|
145
165
|
} finally {
|
|
146
166
|
client.release()
|
|
@@ -150,12 +170,116 @@ function makeCompatDb(pgPool, schema) {
|
|
|
150
170
|
// close: drain pool
|
|
151
171
|
close: () => pgPool.end(),
|
|
152
172
|
|
|
153
|
-
// Internal access for callers that need raw pool
|
|
154
|
-
|
|
173
|
+
// Internal access for callers that need raw pool. Getter (not a fixed
|
|
174
|
+
// field) so callers that hold onto `db` across a recovery cycle (e.g.
|
|
175
|
+
// trace-store's checkedConnect/pool.connect() call sites) transparently
|
|
176
|
+
// see the refreshed pool after withPgRetry swaps the cached instance —
|
|
177
|
+
// otherwise db._pool would keep pointing at the dead pre-recovery pool.
|
|
178
|
+
get _pool() {
|
|
179
|
+
return instances.get(`${resolve(dataDir)}|${schema}`)?.pool ?? pgPool
|
|
180
|
+
},
|
|
155
181
|
}
|
|
156
182
|
return db
|
|
157
183
|
}
|
|
158
184
|
|
|
185
|
+
// ---------------------------------------------------------------------------
|
|
186
|
+
// Self-healing: pool query fails with ECONNREFUSED (target pg_port down while
|
|
187
|
+
// the server process itself stays alive) → discard the stale pool via
|
|
188
|
+
// closePgInstance and re-run ensurePgInstance so supervisor-pg restarts/
|
|
189
|
+
// re-attaches PG, then retry the failed operation exactly once against the
|
|
190
|
+
// fresh pool.
|
|
191
|
+
//
|
|
192
|
+
// Guards against runaway restart loops:
|
|
193
|
+
// - _recoverInFlight: at most one recovery coroutine per dataDir at a time;
|
|
194
|
+
// concurrent callers await the same promise instead of racing restarts.
|
|
195
|
+
// - _lastRecoverAt / RECOVER_COOLDOWN_MS: after a recovery attempt (success
|
|
196
|
+
// or failure) further ECONNREFUSED hits within the cooldown window
|
|
197
|
+
// surface the original error instead of re-triggering PG restart.
|
|
198
|
+
// ---------------------------------------------------------------------------
|
|
199
|
+
|
|
200
|
+
const RECOVER_COOLDOWN_MS = 15_000
|
|
201
|
+
const _lastRecoverAt = new Map() // dataDirKey → epoch ms of last recovery attempt
|
|
202
|
+
const _recoverInFlight = new Map() // dataDirKey → Promise<boolean>
|
|
203
|
+
|
|
204
|
+
function _instanceKeysForDataDir(dataDirKey) {
|
|
205
|
+
const prefix = `${dataDirKey}|`
|
|
206
|
+
return Array.from(instances.keys()).filter(k => k.startsWith(prefix))
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
async function _recoverPgConnection(dataDir, schema) {
|
|
210
|
+
const dataDirKey = resolve(dataDir)
|
|
211
|
+
// In-flight check FIRST: a concurrent recovery already running for this
|
|
212
|
+
// dataDir must be awaited by every caller (even ones arriving inside the
|
|
213
|
+
// cooldown window that starts once that recovery begins) — otherwise a
|
|
214
|
+
// caller that lands between the in-flight recovery's start and its cooldown
|
|
215
|
+
// stamp would see neither in-flight nor cooldown and could trigger a second
|
|
216
|
+
// redundant restart cycle.
|
|
217
|
+
if (_recoverInFlight.has(dataDirKey)) return _recoverInFlight.get(dataDirKey)
|
|
218
|
+
const now = Date.now()
|
|
219
|
+
const last = _lastRecoverAt.get(dataDirKey) || 0
|
|
220
|
+
if (now - last < RECOVER_COOLDOWN_MS) {
|
|
221
|
+
// Cooldown active — do not hammer PG restart on every failing query.
|
|
222
|
+
return false
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const p = (async () => {
|
|
226
|
+
_lastRecoverAt.set(dataDirKey, Date.now())
|
|
227
|
+
__mixdogMemoryLog(`[pg-adapter] ECONNREFUSED on pool query — recovering PG for dataDir=${dataDirKey}\n`)
|
|
228
|
+
// memory + trace schemas share one PG cluster/port; discard every cached
|
|
229
|
+
// pool for this dataDir so a dead-port pool is never reused after restart.
|
|
230
|
+
// Track every schema whose pool was actually closed here so ALL of them
|
|
231
|
+
// get re-ensured below (not just the schema of the caller that happened
|
|
232
|
+
// to trigger this recovery) — otherwise a sibling schema's cached `db`
|
|
233
|
+
// handle is left pointing at an ended pool (its `_pool` getter falls back
|
|
234
|
+
// to the stale `pgPool` closure var) and its next query throws a
|
|
235
|
+
// "Cannot use a pool after calling end" TypeError instead of recovering.
|
|
236
|
+
const closedSchemas = new Set()
|
|
237
|
+
for (const key of _instanceKeysForDataDir(dataDirKey)) {
|
|
238
|
+
const sch = key.slice(dataDirKey.length + 1)
|
|
239
|
+
try { await closePgInstance(dataDir, { schema: sch }) } catch {}
|
|
240
|
+
closedSchemas.add(sch)
|
|
241
|
+
}
|
|
242
|
+
closedSchemas.add(schema) // the triggering schema, even if it had no cached instance yet
|
|
243
|
+
try {
|
|
244
|
+
for (const sch of closedSchemas) {
|
|
245
|
+
await ensurePgInstance(dataDir, { schema: sch })
|
|
246
|
+
}
|
|
247
|
+
__mixdogMemoryLog(`[pg-adapter] PG reconnect recovery complete for dataDir=${dataDirKey}\n`)
|
|
248
|
+
return true
|
|
249
|
+
} catch (e) {
|
|
250
|
+
__mixdogMemoryLog(`[pg-adapter] PG reconnect recovery failed for dataDir=${dataDirKey}: ${e?.message || e}\n`)
|
|
251
|
+
return false
|
|
252
|
+
}
|
|
253
|
+
})()
|
|
254
|
+
_recoverInFlight.set(dataDirKey, p)
|
|
255
|
+
try {
|
|
256
|
+
return await p
|
|
257
|
+
} finally {
|
|
258
|
+
_recoverInFlight.delete(dataDirKey)
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Run `fn(pool)` against the current pool for (dataDir, schema); on
|
|
264
|
+
* ECONNREFUSED, recover PG (see _recoverPgConnection) and retry once against
|
|
265
|
+
* the refreshed pool. Non-ECONNREFUSED errors and cooldown/in-flight misses
|
|
266
|
+
* propagate the original error unchanged.
|
|
267
|
+
*/
|
|
268
|
+
async function withPgRetry(dataDir, schema, fn) {
|
|
269
|
+
const key = `${resolve(dataDir)}|${schema}`
|
|
270
|
+
const pool0 = instances.get(key)?.pool
|
|
271
|
+
try {
|
|
272
|
+
return await fn(pool0)
|
|
273
|
+
} catch (err) {
|
|
274
|
+
if (err?.code !== 'ECONNREFUSED') throw err
|
|
275
|
+
const recovered = await _recoverPgConnection(dataDir, schema)
|
|
276
|
+
if (!recovered) throw err
|
|
277
|
+
const pool1 = instances.get(key)?.pool
|
|
278
|
+
if (!pool1) throw err
|
|
279
|
+
return await fn(pool1)
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
159
283
|
// ---------------------------------------------------------------------------
|
|
160
284
|
// Instance bootstrap — extensions + schemas (idempotent)
|
|
161
285
|
// ---------------------------------------------------------------------------
|
|
@@ -299,7 +423,7 @@ export async function ensurePgInstance(dataDir, opts = {}) {
|
|
|
299
423
|
await bootstrapInstance(pgPool, resolve(dataDir))
|
|
300
424
|
|
|
301
425
|
// 5. Build the compat db shim.
|
|
302
|
-
const db = makeCompatDb(pgPool, schema)
|
|
426
|
+
const db = makeCompatDb(pgPool, schema, dataDir)
|
|
303
427
|
|
|
304
428
|
const result = { db, pool: pgPool, host, port, runtimeDir, pgdataDir }
|
|
305
429
|
instances.set(key, result)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import fs from 'node:fs'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import crypto from 'node:crypto'
|
|
4
|
+
|
|
5
|
+
// Plugin version + promotion code fingerprint. Extracted from index.mjs
|
|
6
|
+
// (behavior-preserving). Callers pass PLUGIN_ROOT so this stays free of any
|
|
7
|
+
// entry-point path resolution.
|
|
8
|
+
|
|
9
|
+
export function readPluginVersion(pluginRoot) {
|
|
10
|
+
try {
|
|
11
|
+
return JSON.parse(fs.readFileSync(path.join(pluginRoot, 'package.json'), 'utf8')).version || '0.0.1'
|
|
12
|
+
} catch { return '0.0.1' }
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const PROMOTION_FINGERPRINT_ROOTS = ['src/memory']
|
|
16
|
+
|
|
17
|
+
function collectPromotionFingerprintFiles(pluginRoot) {
|
|
18
|
+
const out = []
|
|
19
|
+
const walk = (relDir) => {
|
|
20
|
+
let entries = []
|
|
21
|
+
try { entries = fs.readdirSync(path.join(pluginRoot, relDir), { withFileTypes: true }) }
|
|
22
|
+
catch { return }
|
|
23
|
+
for (const ent of entries) {
|
|
24
|
+
const rel = `${relDir}/${ent.name}`.replace(/\\/g, '/')
|
|
25
|
+
if (ent.isDirectory()) {
|
|
26
|
+
walk(rel)
|
|
27
|
+
} else if (ent.isFile() && rel.endsWith('.mjs')) {
|
|
28
|
+
out.push(rel)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
for (const root of PROMOTION_FINGERPRINT_ROOTS) walk(root)
|
|
33
|
+
return out.sort()
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function readPromotionCodeFingerprint(pluginRoot) {
|
|
37
|
+
const hash = crypto.createHash('sha256')
|
|
38
|
+
const files = collectPromotionFingerprintFiles(pluginRoot)
|
|
39
|
+
for (const rel of files) {
|
|
40
|
+
hash.update(rel)
|
|
41
|
+
hash.update('\0')
|
|
42
|
+
try {
|
|
43
|
+
hash.update(fs.readFileSync(path.join(pluginRoot, rel)))
|
|
44
|
+
} catch {
|
|
45
|
+
hash.update('missing')
|
|
46
|
+
}
|
|
47
|
+
hash.update('\0')
|
|
48
|
+
}
|
|
49
|
+
return `src/memory:${files.length}:${hash.digest('hex').slice(0, 16)}`
|
|
50
|
+
}
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import { cleanMemoryText } from './memory.mjs'
|
|
2
|
+
|
|
3
|
+
// Recall query/format helpers extracted verbatim from index.mjs
|
|
4
|
+
// (behavior-preserving). Pure string/date logic plus row rendering.
|
|
5
|
+
|
|
6
|
+
export function parsePeriod(period, hasQuery) {
|
|
7
|
+
if (!period && hasQuery) period = '30d'
|
|
8
|
+
if (!period) return null
|
|
9
|
+
if (period === 'all') return null
|
|
10
|
+
if (period === 'last') return { mode: 'last' }
|
|
11
|
+
// Calendar-day windows: 'today' anchors at local midnight rather than
|
|
12
|
+
// rolling 24h. Without this, a query asking 'today' at 01:30 would silently
|
|
13
|
+
// include yesterday's last 22.5h of activity, mislabelling them as
|
|
14
|
+
// 'today's work'. 'yesterday' is the previous calendar day.
|
|
15
|
+
if (period === 'today') {
|
|
16
|
+
const start = new Date()
|
|
17
|
+
start.setHours(0, 0, 0, 0)
|
|
18
|
+
return { startMs: start.getTime(), endMs: Date.now() }
|
|
19
|
+
}
|
|
20
|
+
if (period === 'yesterday') {
|
|
21
|
+
const start = new Date()
|
|
22
|
+
start.setDate(start.getDate() - 1)
|
|
23
|
+
start.setHours(0, 0, 0, 0)
|
|
24
|
+
const end = new Date(start)
|
|
25
|
+
end.setHours(23, 59, 59, 999)
|
|
26
|
+
return { startMs: start.getTime(), endMs: end.getTime() }
|
|
27
|
+
}
|
|
28
|
+
if (period === 'this_week' || period === 'last_week') {
|
|
29
|
+
// R6 P9: calendar Mon-Sun previous/current week. Mon-start ISO
|
|
30
|
+
// convention. Replaces R5 rolling 7-14d range which was empty for
|
|
31
|
+
// sessions where "last week" decisions actually fell on Mon (4/27) of
|
|
32
|
+
// this week. Precise calendar bounds match natural-language intuition.
|
|
33
|
+
const d = new Date()
|
|
34
|
+
d.setHours(0, 0, 0, 0)
|
|
35
|
+
const dayOfWeek = d.getDay()
|
|
36
|
+
const daysSinceMon = (dayOfWeek + 6) % 7
|
|
37
|
+
const thisWeekMon = new Date(d)
|
|
38
|
+
thisWeekMon.setDate(d.getDate() - daysSinceMon)
|
|
39
|
+
if (period === 'this_week') {
|
|
40
|
+
return { startMs: thisWeekMon.getTime(), endMs: Date.now() }
|
|
41
|
+
}
|
|
42
|
+
const lastWeekMon = new Date(thisWeekMon)
|
|
43
|
+
lastWeekMon.setDate(thisWeekMon.getDate() - 7)
|
|
44
|
+
const lastWeekSunEnd = new Date(thisWeekMon.getTime() - 1)
|
|
45
|
+
return { startMs: lastWeekMon.getTime(), endMs: lastWeekSunEnd.getTime() }
|
|
46
|
+
}
|
|
47
|
+
const relMatch = period.match(/^(\d+)(m|h|d)$/)
|
|
48
|
+
if (relMatch) {
|
|
49
|
+
const n = parseInt(relMatch[1])
|
|
50
|
+
const unit = relMatch[2]
|
|
51
|
+
const now = new Date()
|
|
52
|
+
if (unit === 'm') {
|
|
53
|
+
// Minute granularity is for "resume from the previous turn / pick
|
|
54
|
+
// up where we left off" style recall — sub-hour windows where 1h
|
|
55
|
+
// is too coarse. n=0 is invalid (the regex requires \d+ which
|
|
56
|
+
// matches "0" but a zero-width window returns no rows; leave that
|
|
57
|
+
// as caller-supplied no-op).
|
|
58
|
+
const start = new Date(now.getTime() - n * 60_000)
|
|
59
|
+
return { startMs: start.getTime(), endMs: now.getTime() }
|
|
60
|
+
}
|
|
61
|
+
if (unit === 'h') {
|
|
62
|
+
const start = new Date(now.getTime() - n * 3600_000)
|
|
63
|
+
return { startMs: start.getTime(), endMs: now.getTime() }
|
|
64
|
+
}
|
|
65
|
+
const start = new Date(now)
|
|
66
|
+
start.setDate(start.getDate() - n)
|
|
67
|
+
return { startMs: start.getTime(), endMs: now.getTime() }
|
|
68
|
+
}
|
|
69
|
+
const rangeMatch = period.match(/^(\d{4}-\d{2}-\d{2})~(\d{4}-\d{2}-\d{2})$/)
|
|
70
|
+
if (rangeMatch) {
|
|
71
|
+
return {
|
|
72
|
+
startMs: Date.parse(rangeMatch[1] + 'T00:00:00'),
|
|
73
|
+
endMs: Date.parse(rangeMatch[2] + 'T23:59:59.999'),
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
const dateMatch = period.match(/^(\d{4}-\d{2}-\d{2})$/)
|
|
77
|
+
if (dateMatch) {
|
|
78
|
+
return {
|
|
79
|
+
startMs: Date.parse(dateMatch[1] + 'T00:00:00'),
|
|
80
|
+
endMs: Date.parse(dateMatch[1] + 'T23:59:59.999'),
|
|
81
|
+
exact: true,
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return null
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function formatTs(tsMs) {
|
|
88
|
+
const n = Number(tsMs)
|
|
89
|
+
if (Number.isFinite(n) && n > 1e12) {
|
|
90
|
+
return new Date(n).toLocaleString('sv-SE').slice(0, 16)
|
|
91
|
+
}
|
|
92
|
+
return String(tsMs ?? '').slice(0, 16)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export const CORE_RECALL_STOPWORDS = new Set([
|
|
96
|
+
'about', 'after', 'again', 'before', 'check', 'color', 'decision', 'decided',
|
|
97
|
+
'earlier', 'memory', 'previous', 'routing', 'stored', 'tell', 'what',
|
|
98
|
+
])
|
|
99
|
+
|
|
100
|
+
export function coreRecallTerms(query) {
|
|
101
|
+
return [...new Set(String(query || '').toLowerCase().match(/[\p{L}\p{N}_-]{4,}/gu) || [])]
|
|
102
|
+
.filter((term) => !CORE_RECALL_STOPWORDS.has(term))
|
|
103
|
+
.slice(0, 8)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function normalizeRecallProjectScope(projectScope) {
|
|
107
|
+
const raw = String(projectScope || 'common').trim()
|
|
108
|
+
if (!raw || raw.toLowerCase() === 'common') return null
|
|
109
|
+
if (raw.toLowerCase() === 'all') return '*'
|
|
110
|
+
return raw
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function sessionRecallTerms(query) {
|
|
114
|
+
return [...new Set(String(query || '').toLowerCase().match(/[\p{L}\p{N}_./:-]{2,}/gu) || [])]
|
|
115
|
+
.filter((term) => !CORE_RECALL_STOPWORDS.has(term))
|
|
116
|
+
.slice(0, 12)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
export function interleaveRawRows(hybridRows, rawRows) {
|
|
120
|
+
if (!Array.isArray(rawRows) || rawRows.length === 0) return hybridRows
|
|
121
|
+
const out = []
|
|
122
|
+
const stride = Math.max(1, Math.round(hybridRows.length / (rawRows.length + 1)))
|
|
123
|
+
let rawIdx = 0
|
|
124
|
+
for (let i = 0; i < hybridRows.length; i += 1) {
|
|
125
|
+
out.push(hybridRows[i])
|
|
126
|
+
if ((i + 1) % stride === 0 && rawIdx < rawRows.length) {
|
|
127
|
+
out.push(rawRows[rawIdx])
|
|
128
|
+
rawIdx += 1
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
while (rawIdx < rawRows.length) out.push(rawRows[rawIdx++])
|
|
132
|
+
return out
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function renderEntryLines(rows) {
|
|
136
|
+
if (!rows || rows.length === 0) return '(no results)'
|
|
137
|
+
const lines = []
|
|
138
|
+
// Bound total emitted lines (roots x members) so a many-member recall can't
|
|
139
|
+
// inject unbounded output. Per-line content is already capped at 1000 chars;
|
|
140
|
+
// this caps the line COUNT. Narrow the query (limit/period/projectScope) for more.
|
|
141
|
+
const RECALL_LINE_CAP = 200
|
|
142
|
+
let _capped = false
|
|
143
|
+
outer:
|
|
144
|
+
for (const r of rows) {
|
|
145
|
+
const hasMembers = Array.isArray(r.members) && r.members.length > 0
|
|
146
|
+
if (hasMembers) {
|
|
147
|
+
// Chunks present: emit each member as its own line. Root row is a
|
|
148
|
+
// grouping artifact for retrieval — the caller wants the chunk
|
|
149
|
+
// content (cycle1 raw), not the cycle2-compressed summary.
|
|
150
|
+
for (const m of r.members) {
|
|
151
|
+
if (lines.length >= RECALL_LINE_CAP) { _capped = true; break outer }
|
|
152
|
+
const mTs = formatTs(m.ts)
|
|
153
|
+
const role = m.role === 'user' ? 'u' : m.role === 'assistant' ? 'a' : (m.role || '?')
|
|
154
|
+
const content = cleanMemoryText(String(m.content ?? '')).slice(0, 1000)
|
|
155
|
+
lines.push(`[${mTs}] ${role}: ${content} #${m.id}`)
|
|
156
|
+
}
|
|
157
|
+
} else {
|
|
158
|
+
if (lines.length >= RECALL_LINE_CAP) { _capped = true; break }
|
|
159
|
+
// No chunks (root not yet chunked by cycle1, or orphan leaf): emit
|
|
160
|
+
// the row itself in the same shape. element/summary fall back to
|
|
161
|
+
// raw content when both are absent.
|
|
162
|
+
const ts = formatTs(r.ts)
|
|
163
|
+
const element = r.element ?? ''
|
|
164
|
+
const summary = r.summary ?? ''
|
|
165
|
+
// Standalone leaf rows (is_root=0, no parent chunks_root resolved
|
|
166
|
+
// into a `members` list) carry their u/a role just like inline
|
|
167
|
+
// chunk members — surface it so the format stays consistent across
|
|
168
|
+
// the two emission paths.
|
|
169
|
+
const rolePrefix = r.is_root === 0 && r.role
|
|
170
|
+
? (r.role === 'user' ? 'u: ' : r.role === 'assistant' ? 'a: ' : `${r.role}: `)
|
|
171
|
+
: ''
|
|
172
|
+
const body = element || summary
|
|
173
|
+
? `${element}${summary ? ' — ' + summary : ''}`
|
|
174
|
+
: cleanMemoryText(String(r.content ?? '')).slice(0, 1000)
|
|
175
|
+
// Unchunked raw leaf (cycle1 hasn't classified it yet): mark it so
|
|
176
|
+
// callers can tell fresh-but-unprocessed rows from chunked memory.
|
|
177
|
+
const pendingMark = (r.is_root === 0 && r.chunk_root == null) ? ' [pending]' : ''
|
|
178
|
+
lines.push(`[${ts}] ${rolePrefix}${body.slice(0, 1000)}${pendingMark} #${r.id}`)
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
if (_capped) lines.push(`[recall truncated — showing first ${RECALL_LINE_CAP} lines; narrow the query (limit/period/projectScope) for the rest]`)
|
|
182
|
+
return lines.join('\n')
|
|
183
|
+
}
|