mixdog 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -23
- package/package.json +33 -27
- package/scripts/_test-folder-dialog.mjs +30 -0
- package/scripts/agent-parallel-smoke.mjs +388 -0
- package/scripts/agent-tag-reuse-smoke.mjs +183 -0
- package/scripts/background-task-meta-smoke.mjs +38 -0
- package/scripts/boot-smoke.mjs +52 -9
- package/scripts/build-runtime-linux.sh +348 -0
- package/scripts/build-runtime-macos.sh +217 -0
- package/scripts/build-runtime-windows.ps1 +242 -0
- package/scripts/compact-active-turn-test.mjs +68 -0
- package/scripts/compact-smoke.mjs +859 -129
- package/scripts/compact-trigger-migration-smoke.mjs +187 -0
- package/scripts/fix-brief-fn.mjs +35 -0
- package/scripts/fix-format-tool-surface.mjs +24 -0
- package/scripts/fix-tool-exec-visible.mjs +42 -0
- package/scripts/generate-runtime-manifest.mjs +166 -0
- package/scripts/hook-bus-test.mjs +330 -0
- package/scripts/lead-workflow-smoke.mjs +33 -39
- package/scripts/live-worker-smoke.mjs +43 -37
- package/scripts/llm-trace-summary.mjs +315 -0
- package/scripts/memory-meta-concurrency-test.mjs +20 -0
- package/scripts/output-style-smoke.mjs +56 -15
- package/scripts/parent-abort-link-test.mjs +44 -0
- package/scripts/patch-agent-brief.mjs +48 -0
- package/scripts/patch-app.mjs +21 -0
- package/scripts/patch-app2.mjs +18 -0
- package/scripts/patch-dist-brief.mjs +96 -0
- package/scripts/patch-tool-exec.mjs +70 -0
- package/scripts/pretool-ask-runtime-test.mjs +54 -0
- package/scripts/provider-toolcall-test.mjs +376 -0
- package/scripts/reactive-compact-persist-smoke.mjs +124 -0
- package/scripts/sanitize-tool-pairs-test.mjs +260 -0
- package/scripts/session-context-bench.mjs +344 -0
- package/scripts/session-ingest-smoke.mjs +177 -0
- package/scripts/set-effort-config-test.mjs +41 -0
- package/scripts/smoke-runtime-negative.ps1 +106 -0
- package/scripts/smoke-runtime-negative.sh +97 -0
- package/scripts/smoke.mjs +25 -0
- package/scripts/tool-result-hook-test.mjs +48 -0
- package/scripts/tool-smoke.mjs +1223 -95
- package/scripts/toolcall-args-test.mjs +150 -0
- package/scripts/tui-background-failure-smoke.mjs +73 -0
- package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
- package/src/agents/debugger/AGENT.md +8 -0
- package/src/agents/explore/AGENT.md +4 -0
- package/src/agents/heavy-worker/AGENT.md +9 -3
- package/src/agents/maintainer/AGENT.md +4 -0
- package/src/agents/reviewer/AGENT.md +8 -0
- package/src/agents/scheduler-task/AGENT.md +12 -0
- package/src/agents/scheduler-task/agent.json +6 -0
- package/src/agents/webhook-handler/AGENT.md +12 -0
- package/src/agents/webhook-handler/agent.json +6 -0
- package/src/agents/worker/AGENT.md +9 -3
- package/src/app.mjs +77 -3
- package/src/defaults/hidden-roles.json +17 -12
- package/src/headless-role.mjs +117 -0
- package/src/help.mjs +30 -0
- package/src/hooks/lib/permission-evaluator.cjs +11 -475
- package/src/lib/keychain-cjs.cjs +9 -1
- package/src/lib/mixdog-debug.cjs +0 -7
- package/src/lib/rules-builder.cjs +240 -96
- package/src/lib/text-utils.cjs +1 -1
- package/src/mixdog-session-runtime.mjs +2325 -450
- package/src/output-styles/default.md +12 -28
- package/src/output-styles/extreme-simple.md +9 -6
- package/src/output-styles/simple.md +22 -9
- package/src/repl.mjs +118 -59
- package/src/rules/agent/00-common.md +15 -0
- package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
- package/src/rules/agent/30-explorer.md +22 -0
- package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
- package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
- package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
- package/src/rules/lead/01-general.md +9 -5
- package/src/rules/lead/04-workflow.md +51 -12
- package/src/rules/lead/lead-tool.md +6 -0
- package/src/rules/shared/01-tool.md +12 -1
- package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
- package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
- package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
- package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
- package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
- package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
- package/src/runtime/agent/orchestrator/config.mjs +174 -55
- package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
- package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
- package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
- package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
- package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +377 -243
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +146 -93
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
- package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
- package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
- package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
- package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +221 -70
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +477 -147
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -496
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
- package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +31 -11
- package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
- package/src/runtime/agent/orchestrator/session/cache/post-edit-marks.mjs +4 -4
- package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
- package/src/runtime/agent/orchestrator/session/loop.mjs +851 -674
- package/src/runtime/agent/orchestrator/session/manager.mjs +1593 -466
- package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +291 -46
- package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
- package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
- package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +34 -7
- package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -9
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
- package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
- package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
- package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
- package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
- package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
- package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
- package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
- package/src/runtime/channels/index.mjs +152 -24
- package/src/runtime/channels/lib/scheduler.mjs +18 -14
- package/src/runtime/channels/lib/session-discovery.mjs +3 -2
- package/src/runtime/channels/lib/tool-format.mjs +0 -2
- package/src/runtime/channels/lib/transcript-discovery.mjs +3 -2
- package/src/runtime/channels/lib/webhook.mjs +1 -1
- package/src/runtime/channels/tool-defs.mjs +29 -29
- package/src/runtime/memory/index.mjs +635 -107
- package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
- package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
- package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
- package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
- package/src/runtime/memory/lib/memory-cycle1.mjs +38 -17
- package/src/runtime/memory/lib/memory-cycle2.mjs +6 -7
- package/src/runtime/memory/lib/memory-cycle3.mjs +4 -4
- package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -1
- package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
- package/src/runtime/memory/lib/memory.mjs +88 -9
- package/src/runtime/memory/lib/model-profile.mjs +1 -1
- package/src/runtime/memory/lib/pg/adapter.mjs +15 -1
- package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
- package/src/runtime/memory/lib/runtime-fetcher.mjs +37 -3
- package/src/runtime/memory/lib/session-ingest.mjs +194 -0
- package/src/runtime/memory/lib/trace-store.mjs +96 -51
- package/src/runtime/memory/tool-defs.mjs +46 -37
- package/src/runtime/search/index.mjs +102 -466
- package/src/runtime/search/lib/web-tools.mjs +45 -25
- package/src/runtime/search/tool-defs.mjs +16 -23
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +4 -3
- package/src/runtime/shared/background-tasks.mjs +122 -11
- package/src/runtime/shared/child-spawn-gate.mjs +145 -0
- package/src/runtime/shared/config.mjs +7 -4
- package/src/runtime/shared/err-text.mjs +131 -4
- package/src/runtime/shared/llm/cost.mjs +2 -2
- package/src/runtime/shared/llm/http-agent.mjs +23 -7
- package/src/runtime/shared/llm/index.mjs +34 -11
- package/src/runtime/shared/llm/usage-log.mjs +4 -4
- package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
- package/src/runtime/shared/singleton-owner.mjs +104 -0
- package/src/runtime/shared/tool-execution-contract.mjs +199 -20
- package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
- package/src/runtime/shared/tool-surface.mjs +624 -98
- package/src/runtime/shared/user-data-guard.mjs +0 -2
- package/src/standalone/agent-task-status.mjs +203 -0
- package/src/standalone/agent-task-status.test.mjs +76 -0
- package/src/standalone/agent-tool.mjs +1913 -0
- package/src/standalone/channel-worker.mjs +370 -14
- package/src/standalone/explore-tool.mjs +165 -70
- package/src/standalone/folder-dialog.mjs +314 -0
- package/src/standalone/hook-bus.mjs +898 -22
- package/src/standalone/memory-runtime-proxy.mjs +320 -0
- package/src/standalone/projects.mjs +226 -0
- package/src/standalone/provider-admin.mjs +41 -24
- package/src/standalone/seeds.mjs +2 -69
- package/src/standalone/usage-dashboard.mjs +96 -8
- package/src/tui/App.jsx +4798 -2153
- package/src/tui/components/AnsiText.jsx +39 -28
- package/src/tui/components/ContextPanel.jsx +87 -29
- package/src/tui/components/Markdown.jsx +43 -77
- package/src/tui/components/MarkdownTable.jsx +9 -184
- package/src/tui/components/Message.jsx +28 -11
- package/src/tui/components/Picker.jsx +95 -56
- package/src/tui/components/PromptInput.jsx +367 -239
- package/src/tui/components/QueuedCommands.jsx +1 -1
- package/src/tui/components/SlashCommandPalette.jsx +27 -21
- package/src/tui/components/Spinner.jsx +67 -38
- package/src/tui/components/StatusLine.jsx +606 -38
- package/src/tui/components/TextEntryPanel.jsx +128 -9
- package/src/tui/components/ToolExecution.jsx +617 -368
- package/src/tui/components/TurnDone.jsx +3 -3
- package/src/tui/components/UsagePanel.jsx +3 -5
- package/src/tui/components/tool-output-format.mjs +365 -0
- package/src/tui/components/tool-output-format.test.mjs +220 -0
- package/src/tui/dist/index.mjs +8915 -2418
- package/src/tui/engine-runtime-notification.test.mjs +115 -0
- package/src/tui/engine-tool-result-text.test.mjs +75 -0
- package/src/tui/engine.mjs +1455 -279
- package/src/tui/figures.mjs +21 -40
- package/src/tui/index.jsx +75 -31
- package/src/tui/input-editing.mjs +25 -0
- package/src/tui/markdown/format-token.mjs +511 -68
- package/src/tui/markdown/format-token.test.mjs +216 -0
- package/src/tui/markdown/render-ansi.mjs +94 -0
- package/src/tui/markdown/render-ansi.test.mjs +108 -0
- package/src/tui/markdown/stream-fence.mjs +34 -0
- package/src/tui/markdown/stream-fence.test.mjs +26 -0
- package/src/tui/markdown/table-layout.mjs +250 -0
- package/src/tui/paste-attachments.mjs +0 -7
- package/src/tui/spinner-verbs.mjs +1 -2
- package/src/tui/statusline-ansi-bridge.mjs +172 -0
- package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
- package/src/tui/theme.mjs +746 -24
- package/src/tui/time-format.mjs +1 -1
- package/src/tui/transcript-tool-failures.mjs +67 -0
- package/src/tui/transcript-tool-failures.test.mjs +111 -0
- package/src/ui/ansi.mjs +1 -2
- package/src/ui/markdown.mjs +85 -26
- package/src/ui/markdown.test.mjs +70 -0
- package/src/ui/model-display.mjs +121 -0
- package/src/ui/session-stats.mjs +44 -0
- package/src/ui/statusline-context-label.test.mjs +15 -0
- package/src/ui/statusline.mjs +386 -178
- package/src/ui/tool-card.mjs +3 -16
- package/src/vendor/statusline/bin/statusline-lib.mjs +8 -4
- package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
- package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
- package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
- package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
- package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
- package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
- package/src/workflows/default/WORKFLOW.md +12 -5
- package/src/workflows/default/workflow.json +0 -1
- package/src/workflows/solo/WORKFLOW.md +15 -0
- package/src/workflows/solo/workflow.json +7 -0
- package/vendor/ink/build/output.js +6 -1
- package/src/agents/scheduler-task.md +0 -3
- package/src/agents/web-researcher/AGENT.md +0 -3
- package/src/agents/web-researcher/agent.json +0 -6
- package/src/agents/webhook-handler.md +0 -3
- package/src/rules/bridge/00-common.md +0 -5
- package/src/rules/bridge/30-explorer.md +0 -4
- package/src/rules/lead/00-tool-lead.md +0 -5
- package/src/rules/shared/00-language.md +0 -3
- package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
- package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
- package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
- package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
- package/src/standalone/bridge-tool.mjs +0 -1414
- package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
|
@@ -81,13 +81,24 @@ import {
|
|
|
81
81
|
isBootstrapComplete,
|
|
82
82
|
getMetaValue,
|
|
83
83
|
setMetaValue,
|
|
84
|
+
mergeMetaValue,
|
|
84
85
|
cleanMemoryText,
|
|
85
86
|
} from './lib/memory.mjs'
|
|
86
|
-
import {
|
|
87
|
+
import {
|
|
88
|
+
normalizeIngestRole,
|
|
89
|
+
firstTextContent,
|
|
90
|
+
stableSessionSourceRef,
|
|
91
|
+
sessionMessageContent,
|
|
92
|
+
createIngestTurnAllocator,
|
|
93
|
+
} from './lib/session-ingest.mjs'
|
|
94
|
+
import { configureEmbedding, embedText, embedTexts, getEmbeddingDims, getEmbeddingDtype, getEmbeddingModelId, getKnownDimsForCurrentModel, isEmbeddingModelReady, primeEmbeddingDims, warmupEmbeddingProvider } from './lib/embedding-provider.mjs'
|
|
87
95
|
import { startLlmWorker, stopLlmWorker } from './lib/llm-worker-host.mjs'
|
|
88
96
|
import { runCycle1, runCycle2, runCycle3, runUnifiedGate, parseInterval, syncRootEmbedding, applySimpleStatus, applyUpdate, applyMerge, CYCLE2_ACTIVE_TARGET_CAP } from './lib/memory-cycle.mjs'
|
|
97
|
+
import { loadConfig as loadAgentConfig } from '../agent/orchestrator/config.mjs'
|
|
98
|
+
import { initProviders } from '../agent/orchestrator/providers/registry.mjs'
|
|
99
|
+
import { makeAgentDispatch } from '../agent/orchestrator/agent-runtime/agent-dispatch.mjs'
|
|
89
100
|
import { getInFlightCycle1 } from './lib/memory-cycle1.mjs'
|
|
90
|
-
import { claimAndMarkScheduledCycle, makeCycleRequestSignature, scheduleCoalescedCycleRetry } from './lib/memory-cycle-requests.mjs'
|
|
101
|
+
import { claimAndMarkScheduledCycle, makeCycleRequestSignature, resolveCoalesceMaxRetries, scheduleCoalescedCycleRetry } from './lib/memory-cycle-requests.mjs'
|
|
91
102
|
import { searchRelevantHybrid } from './lib/memory-recall-store.mjs'
|
|
92
103
|
import { fetchEntriesByIdsScoped } from './lib/memory-recall-id-patch.mjs'
|
|
93
104
|
import { retrieveEntries } from './lib/memory-retrievers.mjs'
|
|
@@ -96,10 +107,12 @@ import { computeEntryScore } from './lib/memory-score.mjs'
|
|
|
96
107
|
import { runFullBackfill } from './lib/memory-ops-policy.mjs'
|
|
97
108
|
import { listCore, addCore, editCore, deleteCore, compactCoreIds, CORE_SUMMARY_MAX } from './lib/core-memory-store.mjs'
|
|
98
109
|
import { resolveProjectId, resolveProjectScope } from './lib/project-id-resolver.mjs'
|
|
99
|
-
import { openTraceDatabase, insertTraceEvents, enqueueTraceEvents,
|
|
110
|
+
import { openTraceDatabase, closeTraceDatabase, insertTraceEvents, enqueueTraceEvents, insertAgentCalls, registerTraceExitDrain } from './lib/trace-store.mjs'
|
|
100
111
|
import { updateJsonAtomicSync, writeJsonAtomicSync } from '../shared/atomic-file.mjs'
|
|
101
|
-
import { resolvePluginData } from '../shared/plugin-paths.mjs'
|
|
102
|
-
const
|
|
112
|
+
import { resolvePluginData, mixdogHome } from '../shared/plugin-paths.mjs'
|
|
113
|
+
const IS_MEMORY_ENTRY = !!process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href
|
|
114
|
+
const USE_ARG_DATA_DIR = IS_MEMORY_ENTRY || process.env.MIXDOG_WORKER_MODE === '1'
|
|
115
|
+
const DATA_DIR = process.env.MIXDOG_DATA_DIR || (USE_ARG_DATA_DIR ? process.argv[2] : '') || resolvePluginData()
|
|
103
116
|
if (!DATA_DIR) {
|
|
104
117
|
__mixdogMemoryLog('[memory-service] memory data dir not set and no explicit data dir provided\n')
|
|
105
118
|
process.exit(1)
|
|
@@ -113,6 +126,7 @@ const RUNTIME_ROOT = process.env.MIXDOG_RUNTIME_ROOT
|
|
|
113
126
|
: path.join(os.tmpdir(), 'mixdog')
|
|
114
127
|
|
|
115
128
|
let _periodicAdvertiseInstalled = false
|
|
129
|
+
let _periodicAdvertiseTimer = null
|
|
116
130
|
// Track the most recently advertised port so the periodic tick re-reads it
|
|
117
131
|
// every interval. Without this the setInterval closure binds the FIRST port
|
|
118
132
|
// (the upstream we proxied to) and keeps re-advertising the dead upstream
|
|
@@ -124,7 +138,28 @@ function parsePositivePid(value) {
|
|
|
124
138
|
return Number.isFinite(pid) && pid > 0 ? pid : null
|
|
125
139
|
}
|
|
126
140
|
|
|
127
|
-
const MEMORY_SERVER_PID = parsePositivePid(process.env.MIXDOG_SERVER_PID)
|
|
141
|
+
const MEMORY_SERVER_PID = parsePositivePid(process.env.MIXDOG_SERVER_PID) ?? process.pid
|
|
142
|
+
const MEMORY_DAEMON_MODE = process.env.MIXDOG_MEMORY_DAEMON === '1'
|
|
143
|
+
const MEMORY_IDLE_TTL_MS = Math.max(0, Number(process.env.MIXDOG_MEMORY_IDLE_TTL_MS) || 10 * 60_000)
|
|
144
|
+
let _idleShutdownTimer = null
|
|
145
|
+
|
|
146
|
+
function touchDaemonIdleTimer(reason = 'activity') {
|
|
147
|
+
if (!MEMORY_DAEMON_MODE || MEMORY_IDLE_TTL_MS <= 0) return
|
|
148
|
+
if (_idleShutdownTimer) {
|
|
149
|
+
try { clearTimeout(_idleShutdownTimer) } catch {}
|
|
150
|
+
_idleShutdownTimer = null
|
|
151
|
+
}
|
|
152
|
+
_idleShutdownTimer = setTimeout(() => {
|
|
153
|
+
__mixdogMemoryLog(`[memory-service] daemon idle TTL elapsed after ${reason}; shutting down\n`)
|
|
154
|
+
stop()
|
|
155
|
+
.then(() => process.exit(0))
|
|
156
|
+
.catch((e) => {
|
|
157
|
+
__mixdogMemoryLog(`[memory-service] daemon idle shutdown failed: ${e?.message || e}\n`)
|
|
158
|
+
process.exit(1)
|
|
159
|
+
})
|
|
160
|
+
}, MEMORY_IDLE_TTL_MS)
|
|
161
|
+
_idleShutdownTimer.unref?.()
|
|
162
|
+
}
|
|
128
163
|
|
|
129
164
|
function advertiseMemoryPort(boundPort, attempt = 0) {
|
|
130
165
|
if (!Number.isFinite(boundPort) || boundPort <= 0) return
|
|
@@ -156,18 +191,20 @@ function advertiseMemoryPort(boundPort, attempt = 0) {
|
|
|
156
191
|
}, { compact: true, fsyncDir: true })
|
|
157
192
|
if (!_periodicAdvertiseInstalled) {
|
|
158
193
|
_periodicAdvertiseInstalled = true
|
|
159
|
-
setInterval(() => {
|
|
194
|
+
_periodicAdvertiseTimer = setInterval(() => {
|
|
160
195
|
try {
|
|
161
196
|
if (_currentAdvertisedPort != null) {
|
|
162
197
|
advertiseMemoryPort(_currentAdvertisedPort)
|
|
163
198
|
}
|
|
164
199
|
} catch {}
|
|
165
|
-
}, 30_000)
|
|
200
|
+
}, 30_000)
|
|
201
|
+
_periodicAdvertiseTimer.unref?.()
|
|
166
202
|
}
|
|
167
203
|
} catch (e) {
|
|
168
204
|
const transient = e?.code === 'EPERM' || e?.code === 'EBUSY' || e?.code === 'EACCES'
|
|
169
205
|
if (transient && attempt < 3) {
|
|
170
|
-
setTimeout(() => advertiseMemoryPort(boundPort, attempt + 1), 50 * (attempt + 1))
|
|
206
|
+
const retryTimer = setTimeout(() => advertiseMemoryPort(boundPort, attempt + 1), 50 * (attempt + 1))
|
|
207
|
+
retryTimer.unref?.()
|
|
171
208
|
return
|
|
172
209
|
}
|
|
173
210
|
__mixdogMemoryLog(`[memory-service] active-instance memory_port advertise failed: ${e?.message || e}\n`)
|
|
@@ -363,8 +400,12 @@ let _startupTimeout = null
|
|
|
363
400
|
let _cycle1InFlight = null // shared cycle1 promise (outer coalesce layer)
|
|
364
401
|
let _initialized = false
|
|
365
402
|
let _initPromise = null
|
|
403
|
+
let _stopPromise = null
|
|
366
404
|
let _bootTimestamp = null
|
|
367
405
|
let _transcriptOffsets = new Map()
|
|
406
|
+
/** @type {Map<string, Promise<unknown>>} */
|
|
407
|
+
const _ingestTranscriptTails = new Map()
|
|
408
|
+
let _transcriptOffsetsPersistTail = Promise.resolve()
|
|
368
409
|
// Boot-edge background warmup. ONNX session creation on the embedding worker
|
|
369
410
|
// thread is CPU-heavy, so it must not overlap the worker's own init (DB open,
|
|
370
411
|
// schema, cycle wiring). Previously this was gated behind a fixed setTimeout —
|
|
@@ -373,6 +414,7 @@ let _transcriptOffsets = new Map()
|
|
|
373
414
|
// so it starts the instant boot's CPU-heavy work is done — no magic-number
|
|
374
415
|
// delay. MIXDOG_EMBED_WARMUP=0 disables it (model loads lazily on first use).
|
|
375
416
|
let _pendingEmbeddingWarmup = null
|
|
417
|
+
let _embeddingColdRecallLogAt = 0
|
|
376
418
|
|
|
377
419
|
const TRANSCRIPT_OFFSETS_KEY = 'state.transcript_offsets'
|
|
378
420
|
const CYCLE_LAST_RUN_KEY = 'state.cycle_last_run'
|
|
@@ -382,8 +424,50 @@ function embeddingWarmupEnabled() {
|
|
|
382
424
|
return !(raw === '0' || raw === 'false' || raw === 'off' || raw === 'no')
|
|
383
425
|
}
|
|
384
426
|
|
|
427
|
+
function envFlagEnabled(name) {
|
|
428
|
+
const raw = String(process.env[name] ?? '').trim().toLowerCase()
|
|
429
|
+
return raw === '1' || raw === 'true' || raw === 'on' || raw === 'yes'
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
function memorySecondaryMode() {
|
|
433
|
+
return envFlagEnabled('MIXDOG_MEMORY_SECONDARY')
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function embeddingWarmupCanStart() {
|
|
437
|
+
return embeddingWarmupEnabled() && !memorySecondaryMode()
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
function memoryLlmWorkerEnabled() {
|
|
441
|
+
return !memorySecondaryMode() && !envFlagEnabled('MIXDOG_MEMORY_DISABLE_LLM_WORKER')
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function memoryCyclesEnabled() {
|
|
445
|
+
return !memorySecondaryMode() && !envFlagEnabled('MIXDOG_MEMORY_DISABLE_CYCLES')
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function secondaryPgAdvertised() {
|
|
449
|
+
if (!memorySecondaryMode()) return true
|
|
450
|
+
const runtimeRoot = process.env.MIXDOG_RUNTIME_ROOT
|
|
451
|
+
? path.resolve(process.env.MIXDOG_RUNTIME_ROOT)
|
|
452
|
+
: path.join(os.tmpdir(), 'mixdog')
|
|
453
|
+
try {
|
|
454
|
+
const cur = JSON.parse(fs.readFileSync(path.join(runtimeRoot, 'active-instance.json'), 'utf8'))
|
|
455
|
+
const port = Number(cur?.pg_port)
|
|
456
|
+
const pgdata = cur?.pg_pgdata ? path.resolve(String(cur.pg_pgdata)) : ''
|
|
457
|
+
return Number.isInteger(port) && port > 0 && pgdata === path.resolve(path.join(DATA_DIR, 'pgdata'))
|
|
458
|
+
} catch {
|
|
459
|
+
return false
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function assertSecondaryPgAttachable() {
|
|
464
|
+
if (!secondaryPgAdvertised()) {
|
|
465
|
+
throw new Error('memory-service: secondary mode requires an existing primary PG instance')
|
|
466
|
+
}
|
|
467
|
+
}
|
|
468
|
+
|
|
385
469
|
function scheduleBackgroundEmbeddingWarmup(metaPath, metaKey) {
|
|
386
|
-
if (!
|
|
470
|
+
if (!embeddingWarmupCanStart()) return
|
|
387
471
|
// Queue the warmup; _initRuntime fires it once boot completes.
|
|
388
472
|
_pendingEmbeddingWarmup = () => {
|
|
389
473
|
warmupEmbeddingProvider()
|
|
@@ -397,7 +481,6 @@ function scheduleBackgroundEmbeddingWarmup(metaPath, metaKey) {
|
|
|
397
481
|
})
|
|
398
482
|
.catch(err => {
|
|
399
483
|
__mixdogMemoryLog(`[memory-service] background warmup failed: ${err?.message || err}\n`)
|
|
400
|
-
process.exit(1)
|
|
401
484
|
})
|
|
402
485
|
}
|
|
403
486
|
}
|
|
@@ -429,7 +512,7 @@ async function _initStore() {
|
|
|
429
512
|
const metaKey = {
|
|
430
513
|
provider: embeddingConfig?.provider ?? null,
|
|
431
514
|
model: getEmbeddingModelId(),
|
|
432
|
-
dtype:
|
|
515
|
+
dtype: getEmbeddingDtype(),
|
|
433
516
|
}
|
|
434
517
|
let dimsResolved = null
|
|
435
518
|
try {
|
|
@@ -450,12 +533,17 @@ async function _initStore() {
|
|
|
450
533
|
|
|
451
534
|
if (dimsResolved) {
|
|
452
535
|
primeEmbeddingDims(dimsResolved)
|
|
536
|
+
assertSecondaryPgAttachable()
|
|
453
537
|
db = await openDatabase(DATA_DIR, dimsResolved)
|
|
454
538
|
scheduleBackgroundEmbeddingWarmup(EMBEDDING_META_PATH, metaKey)
|
|
455
539
|
} else {
|
|
540
|
+
if (!embeddingWarmupCanStart()) {
|
|
541
|
+
throw new Error('memory-service: embedding dims unavailable while warmup is disabled')
|
|
542
|
+
}
|
|
456
543
|
// Cold path: meta missed AND model not registered. Sequential.
|
|
457
544
|
await warmupEmbeddingProvider()
|
|
458
545
|
dimsResolved = Number(getEmbeddingDims())
|
|
546
|
+
assertSecondaryPgAttachable()
|
|
459
547
|
db = await openDatabase(DATA_DIR, dimsResolved)
|
|
460
548
|
try {
|
|
461
549
|
writeJsonAtomicSync(EMBEDDING_META_PATH, { ...metaKey, dims: dimsResolved }, { lock: true })
|
|
@@ -467,7 +555,32 @@ async function _initStore() {
|
|
|
467
555
|
if (!await isBootstrapComplete(db)) {
|
|
468
556
|
throw new Error('memory-service: bootstrap not complete after openDatabase')
|
|
469
557
|
}
|
|
470
|
-
|
|
558
|
+
if (memoryLlmWorkerEnabled()) {
|
|
559
|
+
startLlmWorker()
|
|
560
|
+
} else {
|
|
561
|
+
__mixdogMemoryLog('[memory-service] secondary mode; skipping llm worker\n')
|
|
562
|
+
}
|
|
563
|
+
// Initialize the in-process provider registry so cycle1 can run the agent dispatch
|
|
564
|
+
// LLM locally (makeAgentDispatch → session manager → provider.send). In
|
|
565
|
+
// standalone the memory worker runs as a detached HTTP daemon whose parent
|
|
566
|
+
// has disconnected IPC, so the legacy callAgentDispatch() IPC path is dead on
|
|
567
|
+
// arrival. Mirror the channels worker boot (channels/index.mjs:
|
|
568
|
+
// loadAgentConfig() + initProviders) so the registry is populated before any
|
|
569
|
+
// cycle1 dispatch. The gate MUST match _startCycle1Run's makeAgentDispatch
|
|
570
|
+
// injection condition: cycle1 may dispatch in-process whenever cycles are
|
|
571
|
+
// enabled OR the llm worker is enabled (both exclude secondary mode), so
|
|
572
|
+
// registering only under the llm-worker gate would leave a hole where
|
|
573
|
+
// MIXDOG_MEMORY_DISABLE_LLM_WORKER=1 + cycles enabled hits an empty registry
|
|
574
|
+
// and fails with "Provider not found". Non-fatal: a failure here is logged
|
|
575
|
+
// and cycle1's own callLlm surfaces the unresolved-provider error per call.
|
|
576
|
+
if (memoryCyclesEnabled() || memoryLlmWorkerEnabled()) {
|
|
577
|
+
try {
|
|
578
|
+
const agentCfg = loadAgentConfig()
|
|
579
|
+
await initProviders(agentCfg.providers || {})
|
|
580
|
+
} catch (e) {
|
|
581
|
+
process.stderr.write(`[memory-service] initProviders failed (non-fatal): ${e instanceof Error ? e.message : String(e)}\n`)
|
|
582
|
+
}
|
|
583
|
+
}
|
|
471
584
|
_bootTimestamp = Date.now()
|
|
472
585
|
await loadTranscriptOffsets()
|
|
473
586
|
}
|
|
@@ -483,12 +596,16 @@ async function loadTranscriptOffsets() {
|
|
|
483
596
|
}
|
|
484
597
|
|
|
485
598
|
async function persistTranscriptOffsets() {
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
599
|
+
const run = _transcriptOffsetsPersistTail.catch(() => {}).then(async () => {
|
|
600
|
+
try {
|
|
601
|
+
const obj = Object.fromEntries(_transcriptOffsets)
|
|
602
|
+
await setMetaValue(db, TRANSCRIPT_OFFSETS_KEY, JSON.stringify(obj))
|
|
603
|
+
} catch (e) {
|
|
604
|
+
__mixdogMemoryLog(`[memory] persist transcript offsets failed: ${e.message}\n`)
|
|
605
|
+
}
|
|
606
|
+
})
|
|
607
|
+
_transcriptOffsetsPersistTail = run.catch(() => {})
|
|
608
|
+
return run
|
|
492
609
|
}
|
|
493
610
|
|
|
494
611
|
async function getCycleLastRun() {
|
|
@@ -522,9 +639,7 @@ async function getCycleLastRun() {
|
|
|
522
639
|
}
|
|
523
640
|
|
|
524
641
|
async function setCycleLastRun(kind, ts) {
|
|
525
|
-
|
|
526
|
-
cur[kind] = ts
|
|
527
|
-
await setMetaValue(db, CYCLE_LAST_RUN_KEY, JSON.stringify(cur))
|
|
642
|
+
await mergeMetaValue(db, CYCLE_LAST_RUN_KEY, { [kind]: ts })
|
|
528
643
|
}
|
|
529
644
|
|
|
530
645
|
// Raw-row priority lookup for narrow-window queries. Raw rows (is_root=0,
|
|
@@ -590,7 +705,7 @@ async function recallSessionRows(args = {}) {
|
|
|
590
705
|
where.push(`(${clauses.join(' OR ')})`)
|
|
591
706
|
}
|
|
592
707
|
params.push(limit)
|
|
593
|
-
|
|
708
|
+
let rows = (await db.query(`
|
|
594
709
|
SELECT id, ts, role, content, session_id, source_turn, chunk_root, is_root,
|
|
595
710
|
element, category, summary, status, score, last_seen_at, project_id
|
|
596
711
|
FROM entries
|
|
@@ -598,40 +713,133 @@ async function recallSessionRows(args = {}) {
|
|
|
598
713
|
ORDER BY ts DESC, id DESC
|
|
599
714
|
LIMIT $${params.length}
|
|
600
715
|
`, params)).rows
|
|
716
|
+
if (rows.length < limit) {
|
|
717
|
+
const seen = new Set(rows.map((row) => Number(row.id)).filter((id) => Number.isFinite(id)))
|
|
718
|
+
const fillLimit = Math.max(0, limit - rows.length)
|
|
719
|
+
const fillRows = fillLimit > 0
|
|
720
|
+
? (await db.query(`
|
|
721
|
+
SELECT id, ts, role, content, session_id, source_turn, chunk_root, is_root,
|
|
722
|
+
element, category, summary, status, score, last_seen_at, project_id
|
|
723
|
+
FROM entries
|
|
724
|
+
WHERE session_id = $1
|
|
725
|
+
AND id <> ALL($2::bigint[])
|
|
726
|
+
ORDER BY ts DESC, id DESC
|
|
727
|
+
LIMIT $3
|
|
728
|
+
`, [sessionId, [...seen], fillLimit])).rows
|
|
729
|
+
: []
|
|
730
|
+
if (fillRows.length > 0) rows = [...rows, ...fillRows]
|
|
731
|
+
}
|
|
732
|
+
if (args.includeMembers === true) {
|
|
733
|
+
const rootIds = rows
|
|
734
|
+
.filter((row) => Number(row.is_root) === 1)
|
|
735
|
+
.map((row) => Number(row.id))
|
|
736
|
+
.filter((id) => Number.isFinite(id))
|
|
737
|
+
if (rootIds.length > 0) {
|
|
738
|
+
const members = (await db.query(`
|
|
739
|
+
SELECT id, ts, role, content, session_id, source_turn, project_id, chunk_root
|
|
740
|
+
FROM entries
|
|
741
|
+
WHERE chunk_root = ANY($1::bigint[]) AND is_root = 0
|
|
742
|
+
ORDER BY chunk_root ASC, COALESCE(source_turn, 2147483647) ASC, ts ASC, id ASC
|
|
743
|
+
`, [rootIds])).rows
|
|
744
|
+
const byRoot = new Map(rootIds.map((id) => [id, []]))
|
|
745
|
+
for (const member of members) {
|
|
746
|
+
const root = Number(member.chunk_root)
|
|
747
|
+
if (byRoot.has(root)) byRoot.get(root).push(member)
|
|
748
|
+
}
|
|
749
|
+
for (const row of rows) {
|
|
750
|
+
const id = Number(row.id)
|
|
751
|
+
if (byRoot.has(id)) row.members = byRoot.get(id)
|
|
752
|
+
}
|
|
753
|
+
}
|
|
754
|
+
}
|
|
601
755
|
return { text: renderEntryLines(rows) }
|
|
602
756
|
}
|
|
603
757
|
|
|
604
758
|
async function ingestSessionMessages(args = {}) {
|
|
605
759
|
const sessionId = String(args.sessionId || args.session_id || `session-${Date.now()}`).trim()
|
|
606
760
|
const messages = Array.isArray(args.messages) ? args.messages : []
|
|
607
|
-
|
|
761
|
+
// Recall fast-track hydrates the current session before compaction; allow
|
|
762
|
+
// callers to ingest the full in-memory transcript instead of silently
|
|
763
|
+
// clipping long sessions at 500 turns. Default remains conservative.
|
|
764
|
+
const limit = Math.max(1, Math.min(5000, Number(args.limit) || 200))
|
|
608
765
|
const start = Math.max(0, messages.length - limit)
|
|
609
766
|
const projectId = resolveProjectScope(typeof args.cwd === 'string' && args.cwd ? args.cwd : null)
|
|
610
767
|
let considered = 0
|
|
611
768
|
let inserted = 0
|
|
769
|
+
// Monotonic ingest order, independent of the current (post-compaction)
|
|
770
|
+
// array index. source_turn used to be `i+1`, but after compaction shrinks /
|
|
771
|
+
// reindexes session.messages a NEWLY appended turn gets a LOW i and thus a
|
|
772
|
+
// LOW source_turn — and since dump_session_roots / recall order by
|
|
773
|
+
// source_turn first, it would sort BEFORE older pre-compaction rows. Seed a
|
|
774
|
+
// running counter from the current max source_turn for this session so every
|
|
775
|
+
// new row is assigned a turn strictly greater than all previously-ingested
|
|
776
|
+
// ones (true continuation order). Re-ingested (ON CONFLICT) rows keep their
|
|
777
|
+
// original turn and do not consume a new one.
|
|
778
|
+
let prevMaxTurn = 0
|
|
779
|
+
try {
|
|
780
|
+
const maxRow = await db.query(
|
|
781
|
+
`SELECT COALESCE(MAX(source_turn), 0) AS max_turn FROM entries WHERE session_id = $1`,
|
|
782
|
+
[sessionId],
|
|
783
|
+
)
|
|
784
|
+
prevMaxTurn = Number(maxRow.rows?.[0]?.max_turn) || 0
|
|
785
|
+
} catch { prevMaxTurn = 0 }
|
|
786
|
+
const turnAllocator = createIngestTurnAllocator(prevMaxTurn)
|
|
612
787
|
for (let i = start; i < messages.length; i += 1) {
|
|
613
788
|
const m = messages[i]
|
|
614
|
-
if (!m ||
|
|
615
|
-
const
|
|
789
|
+
if (!m || typeof m !== 'object') continue
|
|
790
|
+
const role = normalizeIngestRole(m.role)
|
|
791
|
+
// Persist the whole session conversation by role: user/assistant carry the
|
|
792
|
+
// dialogue, tool carries tool_results, system/developer carry steering
|
|
793
|
+
// context. Previously only user/assistant were kept, silently dropping
|
|
794
|
+
// recent tool/system/developer state that recall fast-track must surface.
|
|
795
|
+
if (!role) continue
|
|
796
|
+
const content = cleanMemoryText(sessionMessageContent(m))
|
|
616
797
|
if (!content || !content.trim()) continue
|
|
617
798
|
considered += 1
|
|
618
799
|
const tsMs = parseTsToMs(m.ts ?? m.timestamp ?? (Date.now() - (messages.length - i)))
|
|
619
|
-
|
|
800
|
+
// Stable per-message identity. The previous `session:${id}#${i+1}` key was
|
|
801
|
+
// positional, so after compaction shrinks/reindexes session.messages a
|
|
802
|
+
// later turn could reuse an old index and be silently skipped by
|
|
803
|
+
// ON CONFLICT DO NOTHING. stableSessionSourceRef hashes only durable
|
|
804
|
+
// fields (role, original ts if present, tool ids, content) — never the
|
|
805
|
+
// synthesized tsMs fallback or the loop index.
|
|
806
|
+
const sourceRef = stableSessionSourceRef(sessionId, m, role, content)
|
|
807
|
+
// Assign the next monotonic turn; only consume it when the row is actually
|
|
808
|
+
// inserted (a conflicting re-ingest keeps its original source_turn).
|
|
809
|
+
const assignedTurn = turnAllocator.peekNext()
|
|
620
810
|
const result = await db.query(`
|
|
621
811
|
INSERT INTO entries(ts, role, content, source_ref, session_id, source_turn, project_id)
|
|
622
812
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
|
623
813
|
ON CONFLICT DO NOTHING
|
|
624
|
-
`, [tsMs,
|
|
625
|
-
|
|
814
|
+
`, [tsMs, role, content, sourceRef, sessionId, assignedTurn, projectId])
|
|
815
|
+
const rowInserted = Number(result.rowCount ?? result.affectedRows ?? 0) || 0
|
|
816
|
+
if (rowInserted > 0) {
|
|
817
|
+
inserted += rowInserted
|
|
818
|
+
turnAllocator.next()
|
|
819
|
+
}
|
|
626
820
|
}
|
|
627
821
|
return { text: `ingest_session: considered=${considered} inserted=${inserted} session=${sessionId}` }
|
|
628
822
|
}
|
|
629
823
|
|
|
630
|
-
|
|
824
|
+
function runTranscriptIngestSerialized(transcriptPath, fn) {
|
|
825
|
+
const key = path.resolve(transcriptPath)
|
|
826
|
+
const prev = _ingestTranscriptTails.get(key) ?? Promise.resolve()
|
|
827
|
+
const run = prev.catch(() => {}).then(fn)
|
|
828
|
+
_ingestTranscriptTails.set(key, run.catch(() => {}))
|
|
829
|
+
return run
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
function snapshotTranscriptOffset(transcriptPath) {
|
|
833
|
+
const stored = _transcriptOffsets.get(transcriptPath)
|
|
834
|
+
if (!stored) return { bytes: 0, lineIndex: 0 }
|
|
835
|
+
return { bytes: Number(stored.bytes) || 0, lineIndex: Number(stored.lineIndex) || 0 }
|
|
836
|
+
}
|
|
837
|
+
|
|
838
|
+
async function ingestTranscriptFileImpl(transcriptPath, { cwd } = {}) {
|
|
631
839
|
let stat
|
|
632
840
|
try { stat = await fs.promises.stat(transcriptPath) } catch { return 0 }
|
|
633
841
|
const sessionUuid = path.basename(transcriptPath, '.jsonl')
|
|
634
|
-
const prev =
|
|
842
|
+
const prev = snapshotTranscriptOffset(transcriptPath)
|
|
635
843
|
if (stat.size < prev.bytes) {
|
|
636
844
|
prev.bytes = 0
|
|
637
845
|
prev.lineIndex = 0
|
|
@@ -721,21 +929,16 @@ async function ingestTranscriptFile(transcriptPath, { cwd } = {}) {
|
|
|
721
929
|
break
|
|
722
930
|
}
|
|
723
931
|
}
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
932
|
+
_transcriptOffsets.set(transcriptPath, {
|
|
933
|
+
bytes: lastGoodBytes,
|
|
934
|
+
lineIndex: lastGoodLineIndex,
|
|
935
|
+
})
|
|
727
936
|
await persistTranscriptOffsets()
|
|
728
937
|
return count
|
|
729
938
|
}
|
|
730
939
|
|
|
731
|
-
function
|
|
732
|
-
|
|
733
|
-
if (!Array.isArray(content)) return ''
|
|
734
|
-
for (const item of content) {
|
|
735
|
-
if (typeof item === 'string') return item
|
|
736
|
-
if (item?.type === 'text' && typeof item.text === 'string') return item.text
|
|
737
|
-
}
|
|
738
|
-
return ''
|
|
940
|
+
async function ingestTranscriptFile(transcriptPath, options = {}) {
|
|
941
|
+
return runTranscriptIngestSerialized(transcriptPath, () => ingestTranscriptFileImpl(transcriptPath, options))
|
|
739
942
|
}
|
|
740
943
|
|
|
741
944
|
function parseTsToMs(value) {
|
|
@@ -776,7 +979,7 @@ function cwdFromTranscriptPath(fp) {
|
|
|
776
979
|
}
|
|
777
980
|
|
|
778
981
|
function _initTranscriptWatcher() {
|
|
779
|
-
const projectsRoot = path.join(
|
|
982
|
+
const projectsRoot = path.join(mixdogHome(), 'projects')
|
|
780
983
|
const SAFETY_POLL_MS = 5 * 60_000
|
|
781
984
|
const DEBOUNCE_MS = 500
|
|
782
985
|
const watchedFiles = new Map()
|
|
@@ -963,6 +1166,83 @@ function _initTranscriptWatcher() {
|
|
|
963
1166
|
const CYCLE1_HEALTH_OVERDUE_MS = 5 * 60_000
|
|
964
1167
|
const CYCLE1_AUTO_RESTART_COOLDOWN_MS = 5 * 60_000
|
|
965
1168
|
|
|
1169
|
+
// In-process cycle1 LLM adapter. The memory daemon runs makeAgentDispatch()
|
|
1170
|
+
// locally (provider registry is initialized in _initStore), so cycle1 never
|
|
1171
|
+
// has to route over the dead IPC agent path (agent-ipc.mjs callAgentDispatch). The
|
|
1172
|
+
// factory is built once (role/taskType are fixed) and the returned function
|
|
1173
|
+
// is reshaped to cycle1's call signature: cycle1 invokes
|
|
1174
|
+
// `callLlm({ role, taskType, mode, preset, timeout, cwd }, userMessage)` and
|
|
1175
|
+
// expects a raw string, while makeAgentDispatch's function takes a single
|
|
1176
|
+
// `{ prompt }` object. The adapter maps the two — preset/cwd resolution is
|
|
1177
|
+
// handled inside makeAgentDispatch via role (cycle1-agent → maint.memory slot).
|
|
1178
|
+
let _cycle1AgentDispatch = null
|
|
1179
|
+
function getCycle1CallLlm() {
|
|
1180
|
+
if (!_cycle1AgentDispatch) {
|
|
1181
|
+
_cycle1AgentDispatch = makeAgentDispatch({
|
|
1182
|
+
role: 'cycle1-agent',
|
|
1183
|
+
taskType: 'maintenance',
|
|
1184
|
+
sourceType: 'memory-cycle',
|
|
1185
|
+
// cycle1 parses the full raw line-format response; the agent brief cap
|
|
1186
|
+
// (12KB) would truncate a large valid response and append prose, causing
|
|
1187
|
+
// partial parsing / omitted / invalid chunks. Opt out so the cycle1
|
|
1188
|
+
// no-truncation contract is preserved through makeAgentDispatch.
|
|
1189
|
+
brief: false,
|
|
1190
|
+
})
|
|
1191
|
+
}
|
|
1192
|
+
return async (opts = {}, userMessage) => {
|
|
1193
|
+
// Preserve cycle1's timeout contract: cycle1 derives `opts.timeout` from
|
|
1194
|
+
// config / caller deadline and expects it to bound the call. makeAgentDispatch
|
|
1195
|
+
// takes it as a per-call `idleTimeoutMs` (stale watchdog). Map it through;
|
|
1196
|
+
// omit when absent/0 so agent defaults apply.
|
|
1197
|
+
const callTimeout = Number(opts?.timeout)
|
|
1198
|
+
return _cycle1AgentDispatch({
|
|
1199
|
+
prompt: String(userMessage ?? ''),
|
|
1200
|
+
preset: opts?.preset || undefined,
|
|
1201
|
+
...(Number.isFinite(callTimeout) && callTimeout > 0 ? { idleTimeoutMs: callTimeout } : {}),
|
|
1202
|
+
})
|
|
1203
|
+
}
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
let _cycle2AgentDispatch = null
|
|
1207
|
+
function getCycle2CallLlm() {
|
|
1208
|
+
if (!_cycle2AgentDispatch) {
|
|
1209
|
+
_cycle2AgentDispatch = makeAgentDispatch({
|
|
1210
|
+
role: 'cycle2-agent',
|
|
1211
|
+
taskType: 'maintenance',
|
|
1212
|
+
sourceType: 'memory-cycle',
|
|
1213
|
+
brief: false,
|
|
1214
|
+
})
|
|
1215
|
+
}
|
|
1216
|
+
return async (opts = {}, userMessage) => {
|
|
1217
|
+
const callTimeout = Number(opts?.timeout)
|
|
1218
|
+
return _cycle2AgentDispatch({
|
|
1219
|
+
prompt: String(userMessage ?? ''),
|
|
1220
|
+
preset: opts?.preset || undefined,
|
|
1221
|
+
...(Number.isFinite(callTimeout) && callTimeout > 0 ? { idleTimeoutMs: callTimeout } : {}),
|
|
1222
|
+
})
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
let _cycle3AgentDispatch = null
|
|
1227
|
+
function getCycle3CallLlm() {
|
|
1228
|
+
if (!_cycle3AgentDispatch) {
|
|
1229
|
+
_cycle3AgentDispatch = makeAgentDispatch({
|
|
1230
|
+
role: 'cycle3-agent',
|
|
1231
|
+
taskType: 'maintenance',
|
|
1232
|
+
sourceType: 'memory-cycle',
|
|
1233
|
+
brief: false,
|
|
1234
|
+
})
|
|
1235
|
+
}
|
|
1236
|
+
return async (opts = {}, userMessage) => {
|
|
1237
|
+
const callTimeout = Number(opts?.timeout)
|
|
1238
|
+
return _cycle3AgentDispatch({
|
|
1239
|
+
prompt: String(userMessage ?? ''),
|
|
1240
|
+
preset: opts?.preset || undefined,
|
|
1241
|
+
...(Number.isFinite(callTimeout) && callTimeout > 0 ? { idleTimeoutMs: callTimeout } : {}),
|
|
1242
|
+
})
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1245
|
+
|
|
966
1246
|
async function recordCycle1Result(result) {
|
|
967
1247
|
const now = Date.now()
|
|
968
1248
|
await setCycleLastRun('cycle1_heartbeat', now)
|
|
@@ -978,6 +1258,13 @@ async function recordCycle1Result(result) {
|
|
|
978
1258
|
}
|
|
979
1259
|
|
|
980
1260
|
function _startCycle1Run(config = {}, options = {}) {
|
|
1261
|
+
// Default to the in-process agent dispatch so every cycle1 path — scheduled,
|
|
1262
|
+
// auto-restart, periodic, manual (action:cycle1), backfill drain, rebuild —
|
|
1263
|
+
// dispatches locally and the dead IPC fallback in memory-cycle1.mjs is never
|
|
1264
|
+
// reached. Explicit options.callLlm (if a caller ever passes one) wins.
|
|
1265
|
+
if (typeof options?.callLlm !== 'function') {
|
|
1266
|
+
options = { ...options, callLlm: getCycle1CallLlm() }
|
|
1267
|
+
}
|
|
981
1268
|
_cycle1InFlight = (async () => {
|
|
982
1269
|
try {
|
|
983
1270
|
const result = await runCycle1(db, config, options, DATA_DIR)
|
|
@@ -1110,34 +1397,48 @@ async function enqueueScheduledCycle3(intervalMs, _reason = 'scheduled') {
|
|
|
1110
1397
|
}
|
|
1111
1398
|
}
|
|
1112
1399
|
|
|
1113
|
-
function scheduleScheduledCycle1(config, signature) {
|
|
1400
|
+
function scheduleScheduledCycle1(config, signature, attempt = 0) {
|
|
1401
|
+
const maxRetries = resolveCoalesceMaxRetries(config, 3)
|
|
1402
|
+
if (attempt > maxRetries) {
|
|
1403
|
+
__mixdogMemoryLog('[cycle1] scheduled queue retry cap reached\n')
|
|
1404
|
+
return
|
|
1405
|
+
}
|
|
1114
1406
|
scheduleCoalescedCycleRetry(db, 'cycle1', async () => {
|
|
1115
1407
|
if (_cycle1InFlight) {
|
|
1116
|
-
scheduleScheduledCycle1(config, signature)
|
|
1408
|
+
scheduleScheduledCycle1(config, signature, attempt + 1)
|
|
1117
1409
|
return
|
|
1118
1410
|
}
|
|
1119
1411
|
const result = await _awaitCycle1Run(config, {
|
|
1120
1412
|
coalescedRetry: true,
|
|
1121
1413
|
onCoalescedSuccess: recordCycle1Result,
|
|
1122
1414
|
})
|
|
1123
|
-
if (result?.skippedInFlight) scheduleScheduledCycle1(config, signature)
|
|
1415
|
+
if (result?.skippedInFlight) scheduleScheduledCycle1(config, signature, attempt + 1)
|
|
1124
1416
|
}, config, signature)
|
|
1125
1417
|
}
|
|
1126
1418
|
|
|
1127
|
-
function scheduleScheduledCycle2(config, signature) {
|
|
1419
|
+
function scheduleScheduledCycle2(config, signature, attempt = 0) {
|
|
1420
|
+
const maxRetries = resolveCoalesceMaxRetries(config, 3)
|
|
1421
|
+
if (attempt > maxRetries) {
|
|
1422
|
+
__mixdogMemoryLog('[cycle2] scheduled queue retry cap reached\n')
|
|
1423
|
+
return
|
|
1424
|
+
}
|
|
1128
1425
|
scheduleCoalescedCycleRetry(db, 'cycle2', async () => {
|
|
1129
1426
|
if (_cycle2InFlight) {
|
|
1130
|
-
scheduleScheduledCycle2(config, signature)
|
|
1427
|
+
scheduleScheduledCycle2(config, signature, attempt + 1)
|
|
1131
1428
|
return
|
|
1132
1429
|
}
|
|
1133
1430
|
_cycle2InFlight = true
|
|
1134
1431
|
try {
|
|
1135
|
-
|
|
1432
|
+
let c2Options = {
|
|
1136
1433
|
coalescedRetry: true,
|
|
1137
1434
|
onCoalescedSuccess: _finalizeCycle2Run,
|
|
1138
|
-
}
|
|
1435
|
+
}
|
|
1436
|
+
if (typeof c2Options?.callLlm !== 'function') {
|
|
1437
|
+
c2Options = { ...c2Options, callLlm: getCycle2CallLlm() }
|
|
1438
|
+
}
|
|
1439
|
+
const result = await runCycle2(db, config, c2Options, DATA_DIR)
|
|
1139
1440
|
if (result?.skippedInFlight) {
|
|
1140
|
-
scheduleScheduledCycle2(config, signature)
|
|
1441
|
+
scheduleScheduledCycle2(config, signature, attempt + 1)
|
|
1141
1442
|
} else if (result?.coalescedRetryNoop) {
|
|
1142
1443
|
__mixdogMemoryLog('[cycle2] scheduled queue noop\n')
|
|
1143
1444
|
} else if (result?.ok === false) {
|
|
@@ -1151,21 +1452,30 @@ function scheduleScheduledCycle2(config, signature) {
|
|
|
1151
1452
|
}, config, signature)
|
|
1152
1453
|
}
|
|
1153
1454
|
|
|
1154
|
-
function scheduleScheduledCycle3(config, signature) {
|
|
1455
|
+
function scheduleScheduledCycle3(config, signature, attempt = 0) {
|
|
1155
1456
|
const retryConfig = config?.cycle3 || config
|
|
1457
|
+
const maxRetries = resolveCoalesceMaxRetries(retryConfig, 3)
|
|
1458
|
+
if (attempt > maxRetries) {
|
|
1459
|
+
__mixdogMemoryLog('[cycle3] scheduled queue retry cap reached\n')
|
|
1460
|
+
return
|
|
1461
|
+
}
|
|
1156
1462
|
scheduleCoalescedCycleRetry(db, 'cycle3', async () => {
|
|
1157
1463
|
if (_cycle3InFlight) {
|
|
1158
|
-
scheduleScheduledCycle3(config, signature)
|
|
1464
|
+
scheduleScheduledCycle3(config, signature, attempt + 1)
|
|
1159
1465
|
return
|
|
1160
1466
|
}
|
|
1161
1467
|
_cycle3InFlight = true
|
|
1162
1468
|
try {
|
|
1163
|
-
|
|
1469
|
+
let c3Options = {
|
|
1164
1470
|
coalescedRetry: true,
|
|
1165
1471
|
onCoalescedSuccess: () => setCycleLastRun('cycle3', Date.now()),
|
|
1166
|
-
}
|
|
1472
|
+
}
|
|
1473
|
+
if (typeof c3Options?.callLlm !== 'function') {
|
|
1474
|
+
c3Options = { ...c3Options, callLlm: getCycle3CallLlm() }
|
|
1475
|
+
}
|
|
1476
|
+
const result = await runCycle3(db, config, DATA_DIR, c3Options)
|
|
1167
1477
|
if (result?.skippedInFlight) {
|
|
1168
|
-
scheduleScheduledCycle3(config, signature)
|
|
1478
|
+
scheduleScheduledCycle3(config, signature, attempt + 1)
|
|
1169
1479
|
} else if (result?.coalescedRetryNoop) {
|
|
1170
1480
|
__mixdogMemoryLog('[cycle3] scheduled queue noop\n')
|
|
1171
1481
|
}
|
|
@@ -1320,8 +1630,12 @@ async function _initRuntime() {
|
|
|
1320
1630
|
// increments, so deleted rows leave permanent gaps. Fast no-op when already
|
|
1321
1631
|
// contiguous (or empty). Runs only here — never in cycle2/addCore/deleteCore.
|
|
1322
1632
|
await compactCoreIds(DATA_DIR)
|
|
1323
|
-
|
|
1324
|
-
|
|
1633
|
+
if (memoryCyclesEnabled()) {
|
|
1634
|
+
_transcriptWatcher = _initTranscriptWatcher()
|
|
1635
|
+
_startCycles()
|
|
1636
|
+
} else {
|
|
1637
|
+
__mixdogMemoryLog('[memory-service] secondary mode; skipping background cycles\n')
|
|
1638
|
+
}
|
|
1325
1639
|
_initialized = true
|
|
1326
1640
|
// Boot complete — continue straight into the deferred embedding warmup.
|
|
1327
1641
|
// Fire-and-forget on the embedding worker thread; never awaited so it does
|
|
@@ -1334,6 +1648,7 @@ function _beginRuntimeInit() {
|
|
|
1334
1648
|
if (!_initPromise) {
|
|
1335
1649
|
_initPromise = _initRuntime().catch((e) => {
|
|
1336
1650
|
__mixdogMemoryLog(`[memory-service] runtime init failed: ${e?.stack || e?.message || e}\n`)
|
|
1651
|
+
_initPromise = null
|
|
1337
1652
|
throw e
|
|
1338
1653
|
})
|
|
1339
1654
|
}
|
|
@@ -1598,15 +1913,20 @@ async function handleSearch(args, signal) {
|
|
|
1598
1913
|
})
|
|
1599
1914
|
let settled
|
|
1600
1915
|
try {
|
|
1601
|
-
// Pre-warm the
|
|
1602
|
-
//
|
|
1603
|
-
//
|
|
1604
|
-
//
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1916
|
+
// Pre-warm only when the embedding model is already resident. If the
|
|
1917
|
+
// process is still cold, keep recall responsive and let the background
|
|
1918
|
+
// warmup finish independently instead of making the first query pay the
|
|
1919
|
+
// ONNX session-create cost.
|
|
1920
|
+
if (isEmbeddingModelReady()) {
|
|
1921
|
+
// Race against the same deadline as the fan-out itself: a stuck
|
|
1922
|
+
// embedding worker would previously park here indefinitely because
|
|
1923
|
+
// the timer hadn't been started yet from the fan-out's perspective.
|
|
1924
|
+
await Promise.race([embedTexts(queries), deadlineRace])
|
|
1925
|
+
} else if (embeddingWarmupCanStart()) {
|
|
1926
|
+
void warmupEmbeddingProvider().catch((err) => {
|
|
1927
|
+
__mixdogMemoryLog(`[memory-service] embedding warmup after cold fan-out skipped dense search: ${err?.message || err}\n`)
|
|
1928
|
+
})
|
|
1929
|
+
}
|
|
1610
1930
|
settled = await Promise.race([
|
|
1611
1931
|
Promise.all(queries.map(async (q) => {
|
|
1612
1932
|
if (fanOutAbort.signal.aborted) throw fanOutAbort.signal.reason
|
|
@@ -1685,7 +2005,21 @@ async function handleSearch(args, signal) {
|
|
|
1685
2005
|
if (query) {
|
|
1686
2006
|
const _t0 = Date.now()
|
|
1687
2007
|
if (signal?.aborted) throw signal.reason ?? new Error('aborted')
|
|
1688
|
-
|
|
2008
|
+
let queryVector = null
|
|
2009
|
+
if (isEmbeddingModelReady()) {
|
|
2010
|
+
queryVector = await embedText(query)
|
|
2011
|
+
} else {
|
|
2012
|
+
const now = Date.now()
|
|
2013
|
+
if (now - _embeddingColdRecallLogAt > 10_000) {
|
|
2014
|
+
_embeddingColdRecallLogAt = now
|
|
2015
|
+
__mixdogMemoryLog('[recall] embedding model cold; returning lexical results while background warmup continues\n')
|
|
2016
|
+
}
|
|
2017
|
+
if (embeddingWarmupCanStart()) {
|
|
2018
|
+
void warmupEmbeddingProvider().catch((err) => {
|
|
2019
|
+
__mixdogMemoryLog(`[memory-service] embedding warmup after cold recall failed: ${err?.message || err}\n`)
|
|
2020
|
+
})
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
1689
2023
|
if (signal?.aborted) throw signal.reason ?? new Error('aborted')
|
|
1690
2024
|
const _t1 = Date.now()
|
|
1691
2025
|
if (process.env.MIXDOG_DEBUG_MEMORY) {
|
|
@@ -1776,7 +2110,7 @@ async function handleSearch(args, signal) {
|
|
|
1776
2110
|
// Emit a recall trace event so getTraceWithEntries() can correlate
|
|
1777
2111
|
// this search with the top-ranked memory entry. One event per
|
|
1778
2112
|
// handleSearch call (not per returned row) — cheapest meaningful link.
|
|
1779
|
-
// parent_span_id left null: the
|
|
2113
|
+
// parent_span_id left null: the agent-side span id is only known after
|
|
1780
2114
|
// the DB insert of the loop/tool events, which happens async on the
|
|
1781
2115
|
// client side and is not available here.
|
|
1782
2116
|
if (_traceDb && filtered.length > 0) {
|
|
@@ -1860,6 +2194,94 @@ function renderEntryLines(rows) {
|
|
|
1860
2194
|
return lines.join('\n')
|
|
1861
2195
|
}
|
|
1862
2196
|
|
|
2197
|
+
async function dumpSessionRootChunks(args = {}) {
|
|
2198
|
+
const sessionId = String(args.sessionId || args.session_id || '').trim()
|
|
2199
|
+
if (!sessionId) return { text: '(no current session)', rows: [], chunks: [], isError: true }
|
|
2200
|
+
const includeRaw = args.includeRaw !== false
|
|
2201
|
+
const limit = Math.max(1, Math.min(1000, Number(args.limit) || 1000))
|
|
2202
|
+
const rootRows = (await db.query(`
|
|
2203
|
+
SELECT id, ts, role, content, session_id, source_turn, chunk_root, is_root,
|
|
2204
|
+
element, category, summary, status, score, last_seen_at, project_id
|
|
2205
|
+
FROM entries
|
|
2206
|
+
WHERE session_id = $1 AND is_root = 1
|
|
2207
|
+
ORDER BY COALESCE(source_turn, 2147483647) ASC, ts ASC, id ASC
|
|
2208
|
+
LIMIT $2
|
|
2209
|
+
`, [sessionId, limit])).rows
|
|
2210
|
+
const roots = rootRows.map((r) => ({ ...r, members: [] }))
|
|
2211
|
+
const rootIds = roots.map((r) => Number(r.id)).filter((id) => Number.isFinite(id))
|
|
2212
|
+
const memberRows = rootIds.length > 0
|
|
2213
|
+
? (await db.query(`
|
|
2214
|
+
SELECT id, ts, role, content, session_id, source_turn, chunk_root, is_root, project_id
|
|
2215
|
+
FROM entries
|
|
2216
|
+
WHERE chunk_root = ANY($1::bigint[]) AND is_root = 0
|
|
2217
|
+
ORDER BY chunk_root ASC, COALESCE(source_turn, 2147483647) ASC, ts ASC, id ASC
|
|
2218
|
+
`, [rootIds])).rows
|
|
2219
|
+
: []
|
|
2220
|
+
const byRoot = new Map(roots.map((r) => [Number(r.id), r]))
|
|
2221
|
+
for (const m of memberRows) {
|
|
2222
|
+
const root = byRoot.get(Number(m.chunk_root))
|
|
2223
|
+
if (root) root.members.push(m)
|
|
2224
|
+
}
|
|
2225
|
+
let rawRows = []
|
|
2226
|
+
if (includeRaw) {
|
|
2227
|
+
rawRows = (await db.query(`
|
|
2228
|
+
SELECT id, ts, role, content, session_id, source_turn, chunk_root, is_root, project_id
|
|
2229
|
+
FROM entries
|
|
2230
|
+
WHERE session_id = $1
|
|
2231
|
+
AND is_root = 0
|
|
2232
|
+
AND (chunk_root IS NULL OR chunk_root = id)
|
|
2233
|
+
ORDER BY COALESCE(source_turn, 2147483647) ASC, ts ASC, id ASC
|
|
2234
|
+
LIMIT $2
|
|
2235
|
+
`, [sessionId, limit])).rows
|
|
2236
|
+
}
|
|
2237
|
+
const chunks = []
|
|
2238
|
+
for (const root of roots) {
|
|
2239
|
+
const memberText = root.members
|
|
2240
|
+
.map((m) => `${m.role === 'assistant' ? 'assistant' : m.role === 'user' ? 'user' : m.role}: ${cleanMemoryText(String(m.content ?? ''))}`)
|
|
2241
|
+
.filter(Boolean)
|
|
2242
|
+
.join('\n')
|
|
2243
|
+
const summary = [root.element, root.summary].map((v) => String(v || '').trim()).filter(Boolean).join(' — ')
|
|
2244
|
+
chunks.push({
|
|
2245
|
+
id: Number(root.id),
|
|
2246
|
+
kind: 'root',
|
|
2247
|
+
ts: Number(root.ts) || 0,
|
|
2248
|
+
sourceTurn: root.source_turn ?? null,
|
|
2249
|
+
category: root.category || null,
|
|
2250
|
+
summary,
|
|
2251
|
+
text: memberText || cleanMemoryText(String(root.content ?? '')),
|
|
2252
|
+
members: root.members,
|
|
2253
|
+
})
|
|
2254
|
+
}
|
|
2255
|
+
for (const raw of rawRows) {
|
|
2256
|
+
chunks.push({
|
|
2257
|
+
id: Number(raw.id),
|
|
2258
|
+
kind: 'raw',
|
|
2259
|
+
chunkRoot: raw.chunk_root ?? null,
|
|
2260
|
+
ts: Number(raw.ts) || 0,
|
|
2261
|
+
sourceTurn: raw.source_turn ?? null,
|
|
2262
|
+
category: null,
|
|
2263
|
+
summary: '',
|
|
2264
|
+
text: `${raw.role === 'assistant' ? 'assistant' : raw.role === 'user' ? 'user' : raw.role}: ${cleanMemoryText(String(raw.content ?? ''))}`,
|
|
2265
|
+
members: [],
|
|
2266
|
+
})
|
|
2267
|
+
}
|
|
2268
|
+
chunks.sort((a, b) => {
|
|
2269
|
+
const at = Number.isFinite(Number(a.sourceTurn)) ? Number(a.sourceTurn) : 2147483647
|
|
2270
|
+
const bt = Number.isFinite(Number(b.sourceTurn)) ? Number(b.sourceTurn) : 2147483647
|
|
2271
|
+
return (at - bt) || ((a.ts || 0) - (b.ts || 0)) || ((a.id || 0) - (b.id || 0))
|
|
2272
|
+
})
|
|
2273
|
+
const text = chunks.length
|
|
2274
|
+
? chunks.map((chunk, idx) => {
|
|
2275
|
+
const label = chunk.kind === 'root'
|
|
2276
|
+
? `# chunk ${idx + 1} root=${chunk.id}${chunk.category ? ` category=${chunk.category}` : ''}`
|
|
2277
|
+
: `${chunk.chunkRoot == null ? '# raw_pending' : '# raw_terminal'} ${idx + 1} id=${chunk.id}`
|
|
2278
|
+
const summary = chunk.summary ? `summary: ${chunk.summary}\n` : ''
|
|
2279
|
+
return `${label}\n${summary}${chunk.text}`.trim()
|
|
2280
|
+
}).join('\n\n')
|
|
2281
|
+
: '(no results)'
|
|
2282
|
+
return { text, rows: [...roots, ...rawRows], chunks }
|
|
2283
|
+
}
|
|
2284
|
+
|
|
1863
2285
|
async function entryStats() {
|
|
1864
2286
|
return await db.transaction(async (tx) => {
|
|
1865
2287
|
const total = (await tx.query(`SELECT COUNT(*) c FROM entries`)).rows[0].c
|
|
@@ -1895,7 +2317,10 @@ async function _handleMemCycle1(args, config, signal) {
|
|
|
1895
2317
|
const minBatchOverride = Number(args?.min_batch)
|
|
1896
2318
|
const sessionCapOverride = Number(args?.session_cap)
|
|
1897
2319
|
const batchSizeOverride = Number(args?.batch_size)
|
|
2320
|
+
const windowSizeOverride = Number(args?.window_size ?? args?.windowSize)
|
|
2321
|
+
const rowsPerSessionOverride = Number(args?.rows_per_session ?? args?.rowsPerSession ?? args?.max_rows_per_session ?? args?.maxRowsPerSession)
|
|
1898
2322
|
const concurrencyOverride = Number(args?.concurrency)
|
|
2323
|
+
const sessionIdOverride = String(args?.sessionId ?? args?.session_id ?? '').trim()
|
|
1899
2324
|
const baseCycle1 = config?.cycle1 || {}
|
|
1900
2325
|
let cycle1Config = baseCycle1
|
|
1901
2326
|
// _runCycle1Impl reads `config?.min_batch ?? config?.cycle1?.min_batch ??
|
|
@@ -1909,12 +2334,24 @@ async function _handleMemCycle1(args, config, signal) {
|
|
|
1909
2334
|
if (Number.isFinite(batchSizeOverride) && batchSizeOverride > 0) {
|
|
1910
2335
|
cycle1Config = { ...cycle1Config, batch_size: batchSizeOverride }
|
|
1911
2336
|
}
|
|
2337
|
+
if (Number.isFinite(windowSizeOverride) && windowSizeOverride > 0) {
|
|
2338
|
+
cycle1Config = { ...cycle1Config, window_size: windowSizeOverride }
|
|
2339
|
+
}
|
|
2340
|
+
if (Number.isFinite(rowsPerSessionOverride) && rowsPerSessionOverride > 0) {
|
|
2341
|
+
cycle1Config = { ...cycle1Config, rows_per_session: rowsPerSessionOverride }
|
|
2342
|
+
}
|
|
2343
|
+
if (sessionIdOverride) {
|
|
2344
|
+
cycle1Config = { ...cycle1Config, session_id: sessionIdOverride }
|
|
2345
|
+
}
|
|
1912
2346
|
if (Number.isFinite(concurrencyOverride) && concurrencyOverride > 0) {
|
|
1913
2347
|
cycle1Config = { ...cycle1Config, concurrency: Math.min(8, Math.floor(concurrencyOverride)) }
|
|
1914
2348
|
}
|
|
1915
2349
|
const callerDeadlineMs = Number(args?._callerDeadlineMs) || 0
|
|
1916
2350
|
if (signal?.aborted) throw signal.reason ?? new Error('aborted')
|
|
1917
2351
|
const cycle1Options = callerDeadlineMs > 0 ? { callerDeadlineMs, signal } : { signal }
|
|
2352
|
+
if (typeof args?._callLlm === 'function') {
|
|
2353
|
+
cycle1Options.callLlm = args._callLlm
|
|
2354
|
+
}
|
|
1918
2355
|
const result = await _awaitCycle1Run(
|
|
1919
2356
|
cycle1Config,
|
|
1920
2357
|
cycle1Options,
|
|
@@ -1928,6 +2365,7 @@ async function _handleMemCycle1(args, config, signal) {
|
|
|
1928
2365
|
const failedRows = Array.isArray(result?.failed_row_ids) ? result.failed_row_ids.length : Number(result?.quality?.failed_rows || 0)
|
|
1929
2366
|
const invalidChunks = Array.isArray(result?.invalid_chunks) ? result.invalid_chunks.length : Number(result?.quality?.invalid_chunks || 0)
|
|
1930
2367
|
return {
|
|
2368
|
+
...result,
|
|
1931
2369
|
text: `cycle1: chunks=${result.chunks} processed=${result.processed} skipped_chunks=${result.skipped}` +
|
|
1932
2370
|
` omitted=${omitted} prefiltered=${prefiltered} failed_rows=${failedRows} invalid_chunks=${invalidChunks}` +
|
|
1933
2371
|
` pending=${pendingStr} inFlight=${inFlightStr}${timedOutPart}`,
|
|
@@ -1936,7 +2374,11 @@ async function _handleMemCycle1(args, config, signal) {
|
|
|
1936
2374
|
|
|
1937
2375
|
async function _handleMemCycle2(args, config, signal) {
|
|
1938
2376
|
if (signal?.aborted) throw signal.reason ?? new Error('aborted')
|
|
1939
|
-
|
|
2377
|
+
let c2Options = { signal }
|
|
2378
|
+
if (typeof c2Options?.callLlm !== 'function') {
|
|
2379
|
+
c2Options = { ...c2Options, callLlm: getCycle2CallLlm() }
|
|
2380
|
+
}
|
|
2381
|
+
const result = await runCycle2(db, config?.cycle2 || {}, c2Options, DATA_DIR)
|
|
1940
2382
|
if (signal?.aborted) throw signal.reason ?? new Error('aborted')
|
|
1941
2383
|
await _finalizeCycle2Run(result)
|
|
1942
2384
|
const counts = {
|
|
@@ -1973,7 +2415,11 @@ async function _handleMemCycle3(args, config, signal) {
|
|
|
1973
2415
|
: (requestedMode === 'proposal' || requestedMode === 'dry-run' || requestedMode === 'dryrun')
|
|
1974
2416
|
? 'proposal'
|
|
1975
2417
|
: 'conservative'
|
|
1976
|
-
|
|
2418
|
+
let c3Options = { signal, apply: confirmed ? true : undefined, applyMode }
|
|
2419
|
+
if (typeof c3Options?.callLlm !== 'function') {
|
|
2420
|
+
c3Options = { ...c3Options, callLlm: getCycle3CallLlm() }
|
|
2421
|
+
}
|
|
2422
|
+
const result = await runCycle3(db, config || {}, DATA_DIR, c3Options)
|
|
1977
2423
|
if (signal?.aborted) throw signal.reason ?? new Error('aborted')
|
|
1978
2424
|
const parts = ['reviewed', 'kept', 'updated', 'merged', 'deleted']
|
|
1979
2425
|
.map(k => `${k}=${result?.[k] || 0}`)
|
|
@@ -1998,7 +2444,11 @@ async function _handleMemFlush(args, config, signal) {
|
|
|
1998
2444
|
if (signal?.aborted) throw signal.reason ?? new Error('aborted')
|
|
1999
2445
|
const r1 = await _awaitCycle1Run(config?.cycle1 || {}, { signal })
|
|
2000
2446
|
if (signal?.aborted) throw signal.reason ?? new Error('aborted')
|
|
2001
|
-
|
|
2447
|
+
let flushC2Options = { signal }
|
|
2448
|
+
if (typeof flushC2Options?.callLlm !== 'function') {
|
|
2449
|
+
flushC2Options = { ...flushC2Options, callLlm: getCycle2CallLlm() }
|
|
2450
|
+
}
|
|
2451
|
+
const r2 = await runCycle2(db, config?.cycle2 || {}, flushC2Options, DATA_DIR)
|
|
2002
2452
|
if (signal?.aborted) throw signal.reason ?? new Error('aborted')
|
|
2003
2453
|
await _finalizeCycle2Run(r2)
|
|
2004
2454
|
return { text: `flush: cycle1 chunks=${r1.chunks} processed=${r1.processed}, cycle2 ${JSON.stringify(r2)}` }
|
|
@@ -2102,7 +2552,11 @@ async function _handleMemRebuild(args, config, signal) {
|
|
|
2102
2552
|
// inside _awaitCycle1Run and guarantees the newly demoted rows are read.
|
|
2103
2553
|
const r1 = await _startCycle1Run(config?.cycle1 || {}, { signal })
|
|
2104
2554
|
if (signal?.aborted) throw signal.reason ?? new Error('aborted')
|
|
2105
|
-
|
|
2555
|
+
let rebuildC2Options = { signal }
|
|
2556
|
+
if (typeof rebuildC2Options?.callLlm !== 'function') {
|
|
2557
|
+
rebuildC2Options = { ...rebuildC2Options, callLlm: getCycle2CallLlm() }
|
|
2558
|
+
}
|
|
2559
|
+
const r2 = await runCycle2(db, config?.cycle2 || {}, rebuildC2Options, DATA_DIR)
|
|
2106
2560
|
await _finalizeCycle2Run(r2)
|
|
2107
2561
|
return { text: `rebuild: cycle1 chunks=${r1.chunks} processed=${r1.processed}, cycle2 ${JSON.stringify(r2)}` }
|
|
2108
2562
|
}
|
|
@@ -2152,7 +2606,11 @@ async function _handleMemBackfill(args, config, signal) {
|
|
|
2152
2606
|
},
|
|
2153
2607
|
runCycle2: async (dbArg, c2Config, c2Options, c2DataDir) => {
|
|
2154
2608
|
if (signal?.aborted) throw signal.reason ?? new Error('aborted')
|
|
2155
|
-
|
|
2609
|
+
let backfillC2Options = { ...c2Options, signal }
|
|
2610
|
+
if (typeof backfillC2Options?.callLlm !== 'function') {
|
|
2611
|
+
backfillC2Options = { ...backfillC2Options, callLlm: getCycle2CallLlm() }
|
|
2612
|
+
}
|
|
2613
|
+
const r2 = await runCycle2(dbArg, c2Config, backfillC2Options, c2DataDir)
|
|
2156
2614
|
_capturedCycle2 = r2
|
|
2157
2615
|
return r2
|
|
2158
2616
|
},
|
|
@@ -2224,6 +2682,10 @@ async function handleMemoryAction(args, signal) {
|
|
|
2224
2682
|
return ingestSessionMessages(args)
|
|
2225
2683
|
}
|
|
2226
2684
|
|
|
2685
|
+
if (action === 'dump_session_roots') {
|
|
2686
|
+
return dumpSessionRootChunks(args)
|
|
2687
|
+
}
|
|
2688
|
+
|
|
2227
2689
|
if (action === 'manage') {
|
|
2228
2690
|
const op = String(args.op ?? '').trim().toLowerCase()
|
|
2229
2691
|
if (!['add', 'edit', 'delete'].includes(op)) {
|
|
@@ -2570,7 +3032,7 @@ async function handleToolCall(name, args, signal) {
|
|
|
2570
3032
|
try {
|
|
2571
3033
|
if (name === 'search_memories') {
|
|
2572
3034
|
const result = await handleSearch(args || {}, signal)
|
|
2573
|
-
return { content: [{ type: 'text', text: result.text }], isError: result.isError || false }
|
|
3035
|
+
return { ...result, content: [{ type: 'text', text: result.text }], isError: result.isError || false }
|
|
2574
3036
|
}
|
|
2575
3037
|
if (name === 'recall') {
|
|
2576
3038
|
// recall is aiWrapped in the unified build; in standalone mode map it to
|
|
@@ -2604,11 +3066,11 @@ async function handleToolCall(name, args, signal) {
|
|
|
2604
3066
|
...(a.currentSession !== undefined ? { currentSession: a.currentSession } : {}),
|
|
2605
3067
|
}
|
|
2606
3068
|
const result = await handleSearch(searchArgs, signal)
|
|
2607
|
-
return { content: [{ type: 'text', text: result.text }], isError: result.isError || false }
|
|
3069
|
+
return { ...result, content: [{ type: 'text', text: result.text }], isError: result.isError || false }
|
|
2608
3070
|
}
|
|
2609
3071
|
if (name === 'memory') {
|
|
2610
3072
|
const result = await handleMemoryAction(args || {}, signal)
|
|
2611
|
-
return { content: [{ type: 'text', text: result.text }], isError: result.isError || false }
|
|
3073
|
+
return { ...result, content: [{ type: 'text', text: result.text }], isError: result.isError || false }
|
|
2612
3074
|
}
|
|
2613
3075
|
return { content: [{ type: 'text', text: `unknown tool: ${name}` }], isError: true }
|
|
2614
3076
|
} catch (err) {
|
|
@@ -2748,6 +3210,7 @@ let _backfillInFlight = null
|
|
|
2748
3210
|
const _ownerInFlightHttpCalls = new Map()
|
|
2749
3211
|
|
|
2750
3212
|
const httpServer = http.createServer(async (req, res) => {
|
|
3213
|
+
touchDaemonIdleTimer(`${req.method || 'HTTP'} ${req.url || '/'}`)
|
|
2751
3214
|
if (req.method === 'POST' && req.url === '/session-reset') {
|
|
2752
3215
|
_bootTimestamp = Date.now()
|
|
2753
3216
|
sendJson(res, { ok: true, bootTimestamp: _bootTimestamp })
|
|
@@ -2969,9 +3432,9 @@ const httpServer = http.createServer(async (req, res) => {
|
|
|
2969
3432
|
enqueueTraceEvents(_traceDb, body.events)
|
|
2970
3433
|
// Use `queued` — events are async; `inserted` would imply durability.
|
|
2971
3434
|
sendJson(res, { ok: true, queued: body.events.length })
|
|
2972
|
-
// Fire-and-forget into focused
|
|
2973
|
-
|
|
2974
|
-
__mixdogMemoryLog(`[trace]
|
|
3435
|
+
// Fire-and-forget into focused agent analytic tables.
|
|
3436
|
+
insertAgentCalls(_traceDb, body.events).catch(e =>
|
|
3437
|
+
__mixdogMemoryLog(`[trace] insertAgentCalls error: ${e?.message}\n`)
|
|
2975
3438
|
)
|
|
2976
3439
|
} catch (e) {
|
|
2977
3440
|
sendJson(res, { ok: false, error: e.message }, 500)
|
|
@@ -3014,11 +3477,11 @@ const httpServer = http.createServer(async (req, res) => {
|
|
|
3014
3477
|
// DEV-ONLY cycle1 chunking bench. Gated by env MIXDOG_DEV_BENCH=1 so
|
|
3015
3478
|
// production is untouched (route returns 404 when unset). Mirrors cycle1's
|
|
3016
3479
|
// exact fetch query + per-session windowing, then runs each window through
|
|
3017
|
-
// buildCycle1ChunkPrompt +
|
|
3480
|
+
// buildCycle1ChunkPrompt + callAgentDispatch + parseCycle1LineFormat. STRICT
|
|
3018
3481
|
// read-only — no UPDATE, no transaction, no commit.
|
|
3019
3482
|
if (req.method === 'POST' && req.url === '/dev/cycle1-bench') {
|
|
3020
3483
|
// Gate: env MIXDOG_DEV_BENCH=1 OR a runtime flag file, so it can be
|
|
3021
|
-
// toggled without restarting
|
|
3484
|
+
// toggled without restarting the host agent (env only reaches the worker
|
|
3022
3485
|
// on a full CC restart, not via dev-sync full-restart).
|
|
3023
3486
|
const _devBenchOn = process.env.MIXDOG_DEV_BENCH === '1'
|
|
3024
3487
|
|| (DATA_DIR && fs.existsSync(path.join(DATA_DIR, '.dev-bench-enabled')))
|
|
@@ -3044,11 +3507,14 @@ const httpServer = http.createServer(async (req, res) => {
|
|
|
3044
3507
|
: null
|
|
3045
3508
|
|
|
3046
3509
|
// Lazy-load LLM + chunking helpers so production boot pays nothing.
|
|
3047
|
-
|
|
3510
|
+
// Use the same in-process agent dispatch adapter as real cycle1 — the legacy
|
|
3511
|
+
// agent-ipc callAgentDispatch() path is dead in the detached standalone
|
|
3512
|
+
// memory daemon (no connected IPC), so the dev bench must mirror prod.
|
|
3513
|
+
const [{ buildCycle1ChunkPrompt, parseCycle1LineFormat }, { resolveMaintenancePreset }] = await Promise.all([
|
|
3048
3514
|
import('./lib/memory-cycle1.mjs'),
|
|
3049
|
-
import('./lib/agent-ipc.mjs'),
|
|
3050
3515
|
import('../shared/llm/index.mjs'),
|
|
3051
3516
|
])
|
|
3517
|
+
const benchCallLlm = getCycle1CallLlm()
|
|
3052
3518
|
|
|
3053
3519
|
const CYCLE1_MIN_BATCH = 3
|
|
3054
3520
|
const CYCLE1_SESSION_CAP = 10
|
|
@@ -3119,13 +3585,9 @@ const httpServer = http.createServer(async (req, res) => {
|
|
|
3119
3585
|
const t0 = Date.now()
|
|
3120
3586
|
let raw, error
|
|
3121
3587
|
try {
|
|
3122
|
-
raw = await
|
|
3123
|
-
role: 'cycle1-agent',
|
|
3124
|
-
taskType: 'maintenance',
|
|
3125
|
-
mode: 'cycle1',
|
|
3588
|
+
raw = await benchCallLlm({
|
|
3126
3589
|
preset,
|
|
3127
3590
|
timeout: TIMEOUT_MS,
|
|
3128
|
-
cwd: null,
|
|
3129
3591
|
}, userMessage)
|
|
3130
3592
|
} catch (e) {
|
|
3131
3593
|
error = e?.message ?? String(e)
|
|
@@ -3424,34 +3886,94 @@ export async function init() {
|
|
|
3424
3886
|
process.on('exit', releaseMemoryOwnerLock)
|
|
3425
3887
|
}
|
|
3426
3888
|
const runtimeReady = _beginRuntimeInit()
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3889
|
+
let boundPort = null
|
|
3890
|
+
if (!memorySecondaryMode()) {
|
|
3891
|
+
boundPort = await _startHttpServer()
|
|
3892
|
+
await runtimeReady
|
|
3893
|
+
advertiseMemoryPort(boundPort)
|
|
3894
|
+
} else {
|
|
3895
|
+
await runtimeReady
|
|
3896
|
+
}
|
|
3430
3897
|
if (process.env.MIXDOG_WORKER_MODE === '1' && process.send) {
|
|
3431
3898
|
__mixdogMemoryLog(`[boot-time] tag=memory-ready tMs=${Date.now()}\n`)
|
|
3432
3899
|
process.send({ type: 'ready', port: boundPort })
|
|
3433
3900
|
}
|
|
3434
3901
|
__mixdogMemoryLog(`[memory-service] init() complete (entries unified mode, version=${PLUGIN_VERSION})\n`)
|
|
3902
|
+
touchDaemonIdleTimer('init')
|
|
3435
3903
|
}
|
|
3436
3904
|
|
|
3437
3905
|
export async function stop() {
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3906
|
+
if (_stopPromise) return _stopPromise
|
|
3907
|
+
_stopPromise = (async () => {
|
|
3908
|
+
_stopCycles()
|
|
3909
|
+
if (_periodicAdvertiseTimer) {
|
|
3910
|
+
try { clearInterval(_periodicAdvertiseTimer) } catch {}
|
|
3911
|
+
_periodicAdvertiseTimer = null
|
|
3912
|
+
}
|
|
3913
|
+
_periodicAdvertiseInstalled = false
|
|
3914
|
+
_currentAdvertisedPort = null
|
|
3915
|
+
_pendingEmbeddingWarmup = null
|
|
3916
|
+
if (_idleShutdownTimer) {
|
|
3917
|
+
try { clearTimeout(_idleShutdownTimer) } catch {}
|
|
3918
|
+
_idleShutdownTimer = null
|
|
3919
|
+
}
|
|
3920
|
+
await stopLlmWorker()
|
|
3921
|
+
resetHttpListenErrorHandler()
|
|
3922
|
+
if (_httpBoundPort != null || _httpReadyPromise) {
|
|
3923
|
+
await new Promise(resolve => {
|
|
3924
|
+
try {
|
|
3925
|
+
httpServer.close(() => resolve())
|
|
3926
|
+
} catch {
|
|
3927
|
+
resolve()
|
|
3928
|
+
}
|
|
3929
|
+
})
|
|
3930
|
+
}
|
|
3931
|
+
_httpReadyPromise = null
|
|
3932
|
+
_httpBoundPort = null
|
|
3933
|
+
activePort = BASE_PORT
|
|
3934
|
+
if (_traceDb) {
|
|
3935
|
+
try { await closeTraceDatabase(DATA_DIR) } catch {}
|
|
3936
|
+
_traceDb = null
|
|
3937
|
+
}
|
|
3938
|
+
await closeDatabase(DATA_DIR)
|
|
3939
|
+
// Stop the PG postmaster after the connection pools have been drained.
|
|
3940
|
+
// closeDatabase() only ends the client pool; without this the child
|
|
3941
|
+
// postmaster keeps running after the memory service exits.
|
|
3942
|
+
if (!memorySecondaryMode()) {
|
|
3943
|
+
try {
|
|
3944
|
+
const { stopPgForShutdown } = await import('./lib/pg/supervisor.mjs')
|
|
3945
|
+
await stopPgForShutdown()
|
|
3946
|
+
} catch {}
|
|
3947
|
+
} else {
|
|
3948
|
+
__mixdogMemoryLog('[memory-service] secondary mode; leaving shared PG running\n')
|
|
3949
|
+
}
|
|
3950
|
+
db = null
|
|
3951
|
+
mainConfig = null
|
|
3952
|
+
_initialized = false
|
|
3953
|
+
_initPromise = null
|
|
3954
|
+
_bootTimestamp = null
|
|
3955
|
+
_transcriptOffsets = new Map()
|
|
3956
|
+
_cycle1InFlight = null
|
|
3957
|
+
_cycle2InFlight = false
|
|
3958
|
+
_cycle3InFlight = false
|
|
3959
|
+
_checkCyclesInFlight = false
|
|
3960
|
+
releaseLock()
|
|
3961
|
+
})().finally(() => {
|
|
3962
|
+
_stopPromise = null
|
|
3963
|
+
})
|
|
3964
|
+
return _stopPromise
|
|
3450
3965
|
}
|
|
3451
3966
|
|
|
3452
3967
|
let activePort = BASE_PORT
|
|
3453
3968
|
let _httpReadyPromise = null
|
|
3454
3969
|
let _httpBoundPort = null
|
|
3970
|
+
let _httpListenErrorHandler = null
|
|
3971
|
+
|
|
3972
|
+
function resetHttpListenErrorHandler() {
|
|
3973
|
+
if (!_httpListenErrorHandler) return
|
|
3974
|
+
try { httpServer.off('error', _httpListenErrorHandler) } catch {}
|
|
3975
|
+
_httpListenErrorHandler = null
|
|
3976
|
+
}
|
|
3455
3977
|
|
|
3456
3978
|
function _startHttpServer() {
|
|
3457
3979
|
if (_httpBoundPort != null) return Promise.resolve(_httpBoundPort)
|
|
@@ -3466,7 +3988,11 @@ function _startHttpServer() {
|
|
|
3466
3988
|
resolve(boundPort)
|
|
3467
3989
|
})
|
|
3468
3990
|
}
|
|
3469
|
-
|
|
3991
|
+
_httpListenErrorHandler = (err) => {
|
|
3992
|
+
if (_httpBoundPort != null) {
|
|
3993
|
+
__mixdogMemoryLog(`[memory-service] HTTP error: ${err?.message || err}\n`)
|
|
3994
|
+
return
|
|
3995
|
+
}
|
|
3470
3996
|
if (err.code === 'EADDRINUSE' && activePort < MAX_PORT) {
|
|
3471
3997
|
activePort++
|
|
3472
3998
|
tryListen()
|
|
@@ -3476,9 +4002,11 @@ function _startHttpServer() {
|
|
|
3476
4002
|
tryListen()
|
|
3477
4003
|
} else {
|
|
3478
4004
|
__mixdogMemoryLog(`[memory-service] HTTP fatal: ${err.message}\n`)
|
|
4005
|
+
resetHttpListenErrorHandler()
|
|
3479
4006
|
reject(err)
|
|
3480
4007
|
}
|
|
3481
|
-
}
|
|
4008
|
+
}
|
|
4009
|
+
httpServer.on('error', _httpListenErrorHandler)
|
|
3482
4010
|
tryListen()
|
|
3483
4011
|
})
|
|
3484
4012
|
return _httpReadyPromise
|
|
@@ -3582,7 +4110,7 @@ if (process.env.MIXDOG_WORKER_MODE === '1' && process.send) {
|
|
|
3582
4110
|
// server with acquireLock + StdioServerTransport. Server-main spawnWorker
|
|
3583
4111
|
// also forks this file with MIXDOG_WORKER_MODE='1'; that path uses the IPC
|
|
3584
4112
|
// handler block above and acquireLock/init() as the single memory owner.
|
|
3585
|
-
if (
|
|
4113
|
+
if (IS_MEMORY_ENTRY && process.env.MIXDOG_WORKER_MODE !== '1') {
|
|
3586
4114
|
;(async () => {
|
|
3587
4115
|
acquireLock()
|
|
3588
4116
|
process.on('exit', releaseLock)
|