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
|
@@ -9,7 +9,6 @@ process.removeAllListeners('warning')
|
|
|
9
9
|
process.on('warning', () => {})
|
|
10
10
|
|
|
11
11
|
import http from 'node:http'
|
|
12
|
-
import crypto from 'node:crypto'
|
|
13
12
|
import os from 'node:os'
|
|
14
13
|
import fs from 'node:fs'
|
|
15
14
|
import path from 'node:path'
|
|
@@ -17,47 +16,9 @@ import { fileURLToPath, pathToFileURL } from 'node:url'
|
|
|
17
16
|
|
|
18
17
|
const PLUGIN_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..', '..', '..')
|
|
19
18
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
} catch { return '0.0.1' }
|
|
24
|
-
}
|
|
25
|
-
const PLUGIN_VERSION = readPluginVersion()
|
|
26
|
-
const PROMOTION_FINGERPRINT_ROOTS = ['src/memory']
|
|
27
|
-
function collectPromotionFingerprintFiles() {
|
|
28
|
-
const out = []
|
|
29
|
-
const walk = (relDir) => {
|
|
30
|
-
let entries = []
|
|
31
|
-
try { entries = fs.readdirSync(path.join(PLUGIN_ROOT, relDir), { withFileTypes: true }) }
|
|
32
|
-
catch { return }
|
|
33
|
-
for (const ent of entries) {
|
|
34
|
-
const rel = `${relDir}/${ent.name}`.replace(/\\/g, '/')
|
|
35
|
-
if (ent.isDirectory()) {
|
|
36
|
-
walk(rel)
|
|
37
|
-
} else if (ent.isFile() && rel.endsWith('.mjs')) {
|
|
38
|
-
out.push(rel)
|
|
39
|
-
}
|
|
40
|
-
}
|
|
41
|
-
}
|
|
42
|
-
for (const root of PROMOTION_FINGERPRINT_ROOTS) walk(root)
|
|
43
|
-
return out.sort()
|
|
44
|
-
}
|
|
45
|
-
function readPromotionCodeFingerprint() {
|
|
46
|
-
const hash = crypto.createHash('sha256')
|
|
47
|
-
const files = collectPromotionFingerprintFiles()
|
|
48
|
-
for (const rel of files) {
|
|
49
|
-
hash.update(rel)
|
|
50
|
-
hash.update('\0')
|
|
51
|
-
try {
|
|
52
|
-
hash.update(fs.readFileSync(path.join(PLUGIN_ROOT, rel)))
|
|
53
|
-
} catch {
|
|
54
|
-
hash.update('missing')
|
|
55
|
-
}
|
|
56
|
-
hash.update('\0')
|
|
57
|
-
}
|
|
58
|
-
return `src/memory:${files.length}:${hash.digest('hex').slice(0, 16)}`
|
|
59
|
-
}
|
|
60
|
-
const BOOT_PROMOTION_CODE_FINGERPRINT = readPromotionCodeFingerprint()
|
|
19
|
+
import { readPluginVersion, readPromotionCodeFingerprint } from './lib/promotion-fingerprint.mjs'
|
|
20
|
+
const PLUGIN_VERSION = readPluginVersion(PLUGIN_ROOT)
|
|
21
|
+
const BOOT_PROMOTION_CODE_FINGERPRINT = readPromotionCodeFingerprint(PLUGIN_ROOT)
|
|
61
22
|
|
|
62
23
|
try { os.setPriority(os.constants.priority.PRIORITY_BELOW_NORMAL) } catch {}
|
|
63
24
|
try {
|
|
@@ -90,6 +51,8 @@ import {
|
|
|
90
51
|
stableSessionSourceRef,
|
|
91
52
|
sessionMessageContent,
|
|
92
53
|
createIngestTurnAllocator,
|
|
54
|
+
sessionMessageContentForIngest,
|
|
55
|
+
shouldExcludeIngestMessage,
|
|
93
56
|
} from './lib/session-ingest.mjs'
|
|
94
57
|
import { configureEmbedding, embedText, embedTexts, getEmbeddingDims, getEmbeddingDtype, getEmbeddingModelId, getKnownDimsForCurrentModel, isEmbeddingModelReady, primeEmbeddingDims, warmupEmbeddingProvider } from './lib/embedding-provider.mjs'
|
|
95
58
|
import { startLlmWorker, stopLlmWorker } from './lib/llm-worker-host.mjs'
|
|
@@ -98,18 +61,22 @@ import { loadConfig as loadAgentConfig } from '../agent/orchestrator/config.mjs'
|
|
|
98
61
|
import { initProviders } from '../agent/orchestrator/providers/registry.mjs'
|
|
99
62
|
import { makeAgentDispatch } from '../agent/orchestrator/agent-runtime/agent-dispatch.mjs'
|
|
100
63
|
import { getInFlightCycle1 } from './lib/memory-cycle1.mjs'
|
|
101
|
-
import {
|
|
64
|
+
import { drainSessionCycle1 } from '../agent/orchestrator/session/compact.mjs'
|
|
65
|
+
import { claimAndMarkScheduledCycle, resolveCoalesceMaxRetries, scheduleCoalescedCycleRetry } from './lib/memory-cycle-requests.mjs'
|
|
102
66
|
import { searchRelevantHybrid } from './lib/memory-recall-store.mjs'
|
|
103
67
|
import { fetchEntriesByIdsScoped } from './lib/memory-recall-id-patch.mjs'
|
|
104
68
|
import { retrieveEntries } from './lib/memory-retrievers.mjs'
|
|
105
69
|
import { pruneOldEntries } from './lib/memory-maintenance-store.mjs'
|
|
106
70
|
import { computeEntryScore } from './lib/memory-score.mjs'
|
|
107
71
|
import { runFullBackfill } from './lib/memory-ops-policy.mjs'
|
|
108
|
-
import { listCore, addCore, editCore, deleteCore, compactCoreIds, CORE_SUMMARY_MAX } from './lib/core-memory-store.mjs'
|
|
72
|
+
import { listCore, addCore, editCore, deleteCore, compactCoreIds, listCoreCandidates, promoteCoreCandidate, dismissCoreCandidate, CORE_SUMMARY_MAX } from './lib/core-memory-store.mjs'
|
|
109
73
|
import { resolveProjectId, resolveProjectScope } from './lib/project-id-resolver.mjs'
|
|
110
74
|
import { openTraceDatabase, closeTraceDatabase, insertTraceEvents, enqueueTraceEvents, insertAgentCalls, registerTraceExitDrain } from './lib/trace-store.mjs'
|
|
111
75
|
import { updateJsonAtomicSync, writeJsonAtomicSync } from '../shared/atomic-file.mjs'
|
|
112
76
|
import { resolvePluginData, mixdogHome } from '../shared/plugin-paths.mjs'
|
|
77
|
+
import { parsePeriod, formatTs, coreRecallTerms, normalizeRecallProjectScope, sessionRecallTerms, interleaveRawRows, renderEntryLines } from './lib/recall-format.mjs'
|
|
78
|
+
import { readBody, sendJson, sendError, isLocalOrigin, normalizeCoreProjectId } from './lib/http-wire.mjs'
|
|
79
|
+
import { scheduledCycle1Signature, scheduledCycle2Signature, scheduledCycle3Signature } from './lib/cycle-signatures.mjs'
|
|
113
80
|
const IS_MEMORY_ENTRY = !!process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href
|
|
114
81
|
const USE_ARG_DATA_DIR = IS_MEMORY_ENTRY || process.env.MIXDOG_WORKER_MODE === '1'
|
|
115
82
|
const DATA_DIR = process.env.MIXDOG_DATA_DIR || (USE_ARG_DATA_DIR ? process.argv[2] : '') || resolvePluginData()
|
|
@@ -398,6 +365,13 @@ let _startupTimeout = null
|
|
|
398
365
|
// getting the real stats and others getting `skippedInFlight: true` from
|
|
399
366
|
// the inner guard.
|
|
400
367
|
let _cycle1InFlight = null // shared cycle1 promise (outer coalesce layer)
|
|
368
|
+
// Per-session recall-drain in-flight map. Multiple terminals share one
|
|
369
|
+
// memory daemon (active-instance.json memory_port); concurrent recall calls
|
|
370
|
+
// for the SAME session must share one drain promise (dedup), while DIFFERENT
|
|
371
|
+
// sessions drain in parallel (cycle1's own advisory-lock/coalesce guard in
|
|
372
|
+
// memory-cycle1.mjs still serializes the actual DB work). A drain never
|
|
373
|
+
// blocks recall for sessions other than its own.
|
|
374
|
+
const _recallDrainInFlight = new Map() // sessionId -> Promise
|
|
401
375
|
let _initialized = false
|
|
402
376
|
let _initPromise = null
|
|
403
377
|
let _stopPromise = null
|
|
@@ -645,57 +619,78 @@ async function setCycleLastRun(kind, ts) {
|
|
|
645
619
|
// Raw-row priority lookup for narrow-window queries. Raw rows (is_root=0,
|
|
646
620
|
// chunk_root IS NULL) are inserted immediately by ingestTranscriptFile before
|
|
647
621
|
// cycle1 runs, so they always carry the freshest turns in the DB.
|
|
648
|
-
async function readRawRowsInWindow(db, tsFromMs, tsToMs, hardLimit = 10, { projectScope } = {}) {
|
|
622
|
+
async function readRawRowsInWindow(db, tsFromMs, tsToMs, hardLimit = 10, { projectScope, sessionId } = {}) {
|
|
649
623
|
try {
|
|
650
|
-
|
|
624
|
+
// Composable WHERE assembly (mirrors retrieveEntries' filter semantics so
|
|
625
|
+
// raw and chunked legs stay in filter parity: projectScope AND sessionId
|
|
626
|
+
// apply identically to both pools).
|
|
627
|
+
const where = ['chunk_root IS NULL', 'is_root = 0', 'ts >= $1', 'ts <= $2']
|
|
628
|
+
const params = [tsFromMs ?? 0, tsToMs ?? Date.now()]
|
|
651
629
|
if (projectScope === 'common') {
|
|
652
|
-
|
|
653
|
-
element, category, summary, status, score, last_seen_at, project_id
|
|
654
|
-
FROM entries
|
|
655
|
-
WHERE chunk_root IS NULL AND is_root = 0
|
|
656
|
-
AND ts >= $1 AND ts <= $2
|
|
657
|
-
AND project_id IS NULL
|
|
658
|
-
ORDER BY ts DESC
|
|
659
|
-
LIMIT $3`
|
|
660
|
-
params = [tsFromMs ?? 0, tsToMs ?? Date.now(), hardLimit]
|
|
630
|
+
where.push('project_id IS NULL')
|
|
661
631
|
} else if (projectScope && projectScope !== 'all') {
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
sql = `SELECT id, ts, role, content, session_id, source_turn, chunk_root, is_root,
|
|
632
|
+
params.push(projectScope)
|
|
633
|
+
where.push(`(project_id IS NULL OR project_id = $${params.length})`)
|
|
634
|
+
}
|
|
635
|
+
const sid = String(sessionId || '').trim()
|
|
636
|
+
if (sid) {
|
|
637
|
+
params.push(sid)
|
|
638
|
+
where.push(`session_id = $${params.length}`)
|
|
639
|
+
}
|
|
640
|
+
params.push(hardLimit)
|
|
641
|
+
const sql = `SELECT id, ts, role, content, session_id, source_turn, chunk_root, is_root,
|
|
673
642
|
element, category, summary, status, score, last_seen_at, project_id
|
|
674
643
|
FROM entries
|
|
675
|
-
WHERE
|
|
676
|
-
AND ts >= $1 AND ts <= $2
|
|
644
|
+
WHERE ${where.join(' AND ')}
|
|
677
645
|
ORDER BY ts DESC
|
|
678
|
-
LIMIT
|
|
679
|
-
params = [tsFromMs ?? 0, tsToMs ?? Date.now(), hardLimit]
|
|
680
|
-
}
|
|
646
|
+
LIMIT $${params.length}`
|
|
681
647
|
const rows = (await db.query(sql, params)).rows
|
|
682
648
|
return rows.map(r => ({ ...r, retrievalScore: 0, rrf: 0 }))
|
|
683
649
|
} catch { return [] }
|
|
684
650
|
}
|
|
685
651
|
|
|
686
|
-
function sessionRecallTerms(query) {
|
|
687
|
-
return [...new Set(String(query || '').toLowerCase().match(/[\p{L}\p{N}_./:-]{2,}/gu) || [])]
|
|
688
|
-
.filter((term) => !CORE_RECALL_STOPWORDS.has(term))
|
|
689
|
-
.slice(0, 12)
|
|
690
|
-
}
|
|
691
|
-
|
|
692
652
|
async function recallSessionRows(args = {}) {
|
|
693
653
|
const sessionId = String(args.sessionId || args.session_id || '').trim()
|
|
694
654
|
if (!sessionId) return { text: '(no current session)' }
|
|
695
655
|
const limit = Math.max(1, Math.min(100, Number(args.limit) || 20))
|
|
696
656
|
const terms = sessionRecallTerms(args.query)
|
|
697
657
|
const params = [sessionId]
|
|
698
|
-
|
|
658
|
+
// Roots + not-yet-chunked leaves only. Once cycle1 turns raw leaves into
|
|
659
|
+
// (root, members) pairs, selecting every row unfiltered emitted the root's
|
|
660
|
+
// summary AND its own member rows in the same browse — duplicate content.
|
|
661
|
+
// A committed member (is_root=0 with a chunk_root) is always reachable via
|
|
662
|
+
// its root's `members` expansion below, so it never needs to be selected
|
|
663
|
+
// directly here.
|
|
664
|
+
const where = ['session_id = $1', '(is_root = 1 OR chunk_root IS NULL OR chunk_root = id)']
|
|
665
|
+
// Current-turn cutoff: the newest unchunked row is very often the calling
|
|
666
|
+
// turn's OWN recall request/tool-args, still being written when this query
|
|
667
|
+
// runs. Exclude it from a bare (no-query) browse so the in-flight turn
|
|
668
|
+
// doesn't self-echo; a query browse (explicit search intent) keeps it.
|
|
669
|
+
// Only treat the newest unchunked turn as "in-flight" when its latest row
|
|
670
|
+
// is fresh (within FRESHNESS_MS of now) — an older newest-unchunked-turn
|
|
671
|
+
// is completed history (cycle1 just hasn't gotten to it, or drain timed
|
|
672
|
+
// out) and must stay visible, not be silently hidden every browse.
|
|
673
|
+
const IN_FLIGHT_TURN_FRESHNESS_MS = 5 * 60 * 1000
|
|
674
|
+
let excludeSourceTurnId = null
|
|
675
|
+
if (terms.length === 0) {
|
|
676
|
+
try {
|
|
677
|
+
const r = await db.query(
|
|
678
|
+
`SELECT source_turn t, MAX(ts) last_ts FROM entries
|
|
679
|
+
WHERE session_id = $1 AND chunk_root IS NULL
|
|
680
|
+
GROUP BY source_turn ORDER BY source_turn DESC LIMIT 1`,
|
|
681
|
+
[sessionId],
|
|
682
|
+
)
|
|
683
|
+
const t = r.rows?.[0]?.t
|
|
684
|
+
const lastTs = Number(r.rows?.[0]?.last_ts)
|
|
685
|
+
if (t != null && Number.isFinite(lastTs) && (Date.now() - lastTs) <= IN_FLIGHT_TURN_FRESHNESS_MS) {
|
|
686
|
+
excludeSourceTurnId = Number(t)
|
|
687
|
+
}
|
|
688
|
+
} catch {}
|
|
689
|
+
}
|
|
690
|
+
if (Number.isFinite(excludeSourceTurnId)) {
|
|
691
|
+
params.push(excludeSourceTurnId)
|
|
692
|
+
where.push(`NOT (chunk_root IS NULL AND source_turn = $${params.length})`)
|
|
693
|
+
}
|
|
699
694
|
if (terms.length > 0) {
|
|
700
695
|
const textExpr = `lower(coalesce(content, '') || ' ' || coalesce(element, '') || ' ' || coalesce(summary, ''))`
|
|
701
696
|
const clauses = terms.map((term) => {
|
|
@@ -716,16 +711,22 @@ async function recallSessionRows(args = {}) {
|
|
|
716
711
|
if (rows.length < limit) {
|
|
717
712
|
const seen = new Set(rows.map((row) => Number(row.id)).filter((id) => Number.isFinite(id)))
|
|
718
713
|
const fillLimit = Math.max(0, limit - rows.length)
|
|
714
|
+
const fillWhere = ['session_id = $1', 'id <> ALL($2::bigint[])', '(is_root = 1 OR chunk_root IS NULL OR chunk_root = id)']
|
|
715
|
+
const fillParams = [sessionId, [...seen]]
|
|
716
|
+
if (Number.isFinite(excludeSourceTurnId)) {
|
|
717
|
+
fillParams.push(excludeSourceTurnId)
|
|
718
|
+
fillWhere.push(`NOT (chunk_root IS NULL AND source_turn = $${fillParams.length})`)
|
|
719
|
+
}
|
|
720
|
+
fillParams.push(fillLimit)
|
|
719
721
|
const fillRows = fillLimit > 0
|
|
720
722
|
? (await db.query(`
|
|
721
723
|
SELECT id, ts, role, content, session_id, source_turn, chunk_root, is_root,
|
|
722
724
|
element, category, summary, status, score, last_seen_at, project_id
|
|
723
725
|
FROM entries
|
|
724
|
-
WHERE
|
|
725
|
-
AND id <> ALL($2::bigint[])
|
|
726
|
+
WHERE ${fillWhere.join(' AND ')}
|
|
726
727
|
ORDER BY ts DESC, id DESC
|
|
727
|
-
LIMIT
|
|
728
|
-
`,
|
|
728
|
+
LIMIT $${fillParams.length}
|
|
729
|
+
`, fillParams)).rows
|
|
729
730
|
: []
|
|
730
731
|
if (fillRows.length > 0) rows = [...rows, ...fillRows]
|
|
731
732
|
}
|
|
@@ -792,7 +793,17 @@ async function ingestSessionMessages(args = {}) {
|
|
|
792
793
|
// are dropped so recall-fasttrack summaries stay conversation-focused and
|
|
793
794
|
// do not re-inject content already in the protected system prefix.
|
|
794
795
|
if (!role) continue
|
|
795
|
-
|
|
796
|
+
// Exclude synthetic / non-conversation rows entirely (reference-files
|
|
797
|
+
// injections, compaction summaries, protected-context `.` acks, internal
|
|
798
|
+
// runtime nudges). These are mechanical noise, not conversation.
|
|
799
|
+
if (shouldExcludeIngestMessage(m)) continue
|
|
800
|
+
// Pure-conversation shaping: only the human/model prose text. The ingest
|
|
801
|
+
// shaper NEVER inlines tool_call / tool_result traces and strips the
|
|
802
|
+
// deterministic manager.mjs user-turn prefix envelopes (# Session /
|
|
803
|
+
// # Additional context / # Prefetch / # Task) so only the real human
|
|
804
|
+
// prompt remains. cleanMemoryText then removes the <system-reminder> block
|
|
805
|
+
// and residual transcript noise.
|
|
806
|
+
const content = cleanMemoryText(sessionMessageContentForIngest(m))
|
|
796
807
|
if (!content || !content.trim()) continue
|
|
797
808
|
considered += 1
|
|
798
809
|
const tsMs = parseTsToMs(m.ts ?? m.timestamp ?? (Date.now() - (messages.length - i)))
|
|
@@ -1178,7 +1189,7 @@ let _cycle1AgentDispatch = null
|
|
|
1178
1189
|
function getCycle1CallLlm() {
|
|
1179
1190
|
if (!_cycle1AgentDispatch) {
|
|
1180
1191
|
_cycle1AgentDispatch = makeAgentDispatch({
|
|
1181
|
-
|
|
1192
|
+
agent: 'cycle1-agent',
|
|
1182
1193
|
taskType: 'maintenance',
|
|
1183
1194
|
sourceType: 'memory-cycle',
|
|
1184
1195
|
// cycle1 parses the full raw line-format response; the agent brief cap
|
|
@@ -1206,7 +1217,7 @@ let _cycle2AgentDispatch = null
|
|
|
1206
1217
|
function getCycle2CallLlm() {
|
|
1207
1218
|
if (!_cycle2AgentDispatch) {
|
|
1208
1219
|
_cycle2AgentDispatch = makeAgentDispatch({
|
|
1209
|
-
|
|
1220
|
+
agent: 'cycle2-agent',
|
|
1210
1221
|
taskType: 'maintenance',
|
|
1211
1222
|
sourceType: 'memory-cycle',
|
|
1212
1223
|
brief: false,
|
|
@@ -1226,7 +1237,7 @@ let _cycle3AgentDispatch = null
|
|
|
1226
1237
|
function getCycle3CallLlm() {
|
|
1227
1238
|
if (!_cycle3AgentDispatch) {
|
|
1228
1239
|
_cycle3AgentDispatch = makeAgentDispatch({
|
|
1229
|
-
|
|
1240
|
+
agent: 'cycle3-agent',
|
|
1230
1241
|
taskType: 'maintenance',
|
|
1231
1242
|
sourceType: 'memory-cycle',
|
|
1232
1243
|
brief: false,
|
|
@@ -1254,6 +1265,62 @@ async function recordCycle1Result(result) {
|
|
|
1254
1265
|
if (!skipped && !coalescedNoop && !allFailed) {
|
|
1255
1266
|
await setCycleLastRun('cycle1', now)
|
|
1256
1267
|
}
|
|
1268
|
+
if (!skipped && !coalescedNoop) markCycleDone('cycle1', !allFailed, allFailed ? 'all rows skipped' : null)
|
|
1269
|
+
}
|
|
1270
|
+
|
|
1271
|
+
// ── Cycle health state (no new commands; consumed by /health + statusline) ──
|
|
1272
|
+
// In-memory per-cycle run ledger + a small state file the TUI statusline can
|
|
1273
|
+
// read without HTTP. "Silently stalled cycles" (2026-07: cycle2 poison
|
|
1274
|
+
// rotation sat unnoticed for months) become visible three ways: /health
|
|
1275
|
+
// fields, a WARN log with cooldown, and the L2 "Memory" segment.
|
|
1276
|
+
const CYCLE_STATE_FILE = path.join(DATA_DIR, 'memory-cycle-state.json')
|
|
1277
|
+
const _cycleHealth = {
|
|
1278
|
+
cycle1: { last_success_at: 0, last_error_at: 0, last_error: null, consecutive_failures: 0 },
|
|
1279
|
+
cycle2: { last_success_at: 0, last_error_at: 0, last_error: null, consecutive_failures: 0 },
|
|
1280
|
+
cycle3: { last_success_at: 0, last_error_at: 0, last_error: null, consecutive_failures: 0 },
|
|
1281
|
+
}
|
|
1282
|
+
let _cycleRunning = null // { cycle, started_at }
|
|
1283
|
+
let _cycleBacklogSnapshot = { unchunked: 0, cycle2_pending: 0, at: 0 }
|
|
1284
|
+
let _lastBacklogWarnAt = 0
|
|
1285
|
+
const BACKLOG_WARN_COOLDOWN_MS = 10 * 60_000
|
|
1286
|
+
const BACKLOG_WARN_PENDING = 500
|
|
1287
|
+
const BACKLOG_WARN_FAILURES = 5
|
|
1288
|
+
|
|
1289
|
+
function _writeCycleStateFile() {
|
|
1290
|
+
try {
|
|
1291
|
+
fs.writeFileSync(CYCLE_STATE_FILE, JSON.stringify({
|
|
1292
|
+
running: _cycleRunning,
|
|
1293
|
+
backlog: _cycleBacklogSnapshot,
|
|
1294
|
+
cycles: _cycleHealth,
|
|
1295
|
+
updatedAt: Date.now(),
|
|
1296
|
+
}))
|
|
1297
|
+
} catch { /* best-effort; statusline just shows nothing */ }
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1300
|
+
function markCycleRunning(cycle) {
|
|
1301
|
+
_cycleRunning = { cycle, started_at: Date.now() }
|
|
1302
|
+
_writeCycleStateFile()
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
function markCycleDone(cycle, ok, err = null) {
|
|
1306
|
+
const h = _cycleHealth[cycle]
|
|
1307
|
+
if (h) {
|
|
1308
|
+
const now = Date.now()
|
|
1309
|
+
if (ok) { h.last_success_at = now; h.consecutive_failures = 0; h.last_error = null }
|
|
1310
|
+
else { h.last_error_at = now; h.consecutive_failures += 1; h.last_error = String(err || 'unknown').slice(0, 200) }
|
|
1311
|
+
if (!ok && h.consecutive_failures >= BACKLOG_WARN_FAILURES) {
|
|
1312
|
+
_warnCycleHealth(`${cycle} failing repeatedly (consecutive=${h.consecutive_failures}, last="${h.last_error}")`)
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
if (_cycleRunning?.cycle === cycle) _cycleRunning = null
|
|
1316
|
+
_writeCycleStateFile()
|
|
1317
|
+
}
|
|
1318
|
+
|
|
1319
|
+
function _warnCycleHealth(msg) {
|
|
1320
|
+
const now = Date.now()
|
|
1321
|
+
if (now - _lastBacklogWarnAt < BACKLOG_WARN_COOLDOWN_MS) return
|
|
1322
|
+
_lastBacklogWarnAt = now
|
|
1323
|
+
__mixdogMemoryLog(`[cycle-health] WARN ${msg}\n`)
|
|
1257
1324
|
}
|
|
1258
1325
|
|
|
1259
1326
|
function _startCycle1Run(config = {}, options = {}) {
|
|
@@ -1264,6 +1331,7 @@ function _startCycle1Run(config = {}, options = {}) {
|
|
|
1264
1331
|
if (typeof options?.callLlm !== 'function') {
|
|
1265
1332
|
options = { ...options, callLlm: getCycle1CallLlm() }
|
|
1266
1333
|
}
|
|
1334
|
+
markCycleRunning('cycle1')
|
|
1267
1335
|
_cycle1InFlight = (async () => {
|
|
1268
1336
|
try {
|
|
1269
1337
|
const result = await runCycle1(db, config, options, DATA_DIR)
|
|
@@ -1276,7 +1344,11 @@ function _startCycle1Run(config = {}, options = {}) {
|
|
|
1276
1344
|
await recordCycle1Result(result)
|
|
1277
1345
|
}
|
|
1278
1346
|
return result
|
|
1347
|
+
} catch (err) {
|
|
1348
|
+
markCycleDone('cycle1', false, err?.message || err)
|
|
1349
|
+
throw err
|
|
1279
1350
|
} finally {
|
|
1351
|
+
if (_cycleRunning?.cycle === 'cycle1') { _cycleRunning = null; _writeCycleStateFile() }
|
|
1280
1352
|
if (_cycle1InFlight === promise) _cycle1InFlight = null
|
|
1281
1353
|
}
|
|
1282
1354
|
})()
|
|
@@ -1303,7 +1375,14 @@ async function _awaitCycle1Run(config = {}, options = {}) {
|
|
|
1303
1375
|
let timer
|
|
1304
1376
|
const deadlinePromise = new Promise((resolve) => {
|
|
1305
1377
|
timer = setTimeout(() => {
|
|
1306
|
-
|
|
1378
|
+
// Do NOT clear _cycle1InFlight here — the underlying runCycle1 is still
|
|
1379
|
+
// running in the background and owns that slot; its own `finally`
|
|
1380
|
+
// (_startCycle1Run above) clears it when the real work settles. Clearing
|
|
1381
|
+
// it early made a later unbounded drain (deadlineMs:0, e.g. the query
|
|
1382
|
+
// recall path) re-enter _startCycle1Run and spawn a SECOND concurrent
|
|
1383
|
+
// cycle1 run instead of awaiting the one already in flight, defeating
|
|
1384
|
+
// drain-to-zero silently. The caller here just stops WAITING at the
|
|
1385
|
+
// deadline; state ownership stays with the promise itself.
|
|
1307
1386
|
resolve({
|
|
1308
1387
|
processed: 0,
|
|
1309
1388
|
chunks: 0,
|
|
@@ -1339,34 +1418,6 @@ function periodicCycle1Config() {
|
|
|
1339
1418
|
}
|
|
1340
1419
|
}
|
|
1341
1420
|
|
|
1342
|
-
function scheduledCycle1Signature(config) {
|
|
1343
|
-
return makeCycleRequestSignature('cycle1', config, {
|
|
1344
|
-
preset: undefined,
|
|
1345
|
-
concurrency: undefined,
|
|
1346
|
-
maxConcurrent: undefined,
|
|
1347
|
-
})
|
|
1348
|
-
}
|
|
1349
|
-
|
|
1350
|
-
function scheduledCycle2Signature(config) {
|
|
1351
|
-
return makeCycleRequestSignature('cycle2', config, {
|
|
1352
|
-
cascadePreset: undefined,
|
|
1353
|
-
concurrency: undefined,
|
|
1354
|
-
})
|
|
1355
|
-
}
|
|
1356
|
-
|
|
1357
|
-
function scheduledCycle3ApplyMode(config) {
|
|
1358
|
-
const raw = String(config?.cycle3?.applyMode || 'conservative').trim().toLowerCase()
|
|
1359
|
-
return (raw === 'proposal' || raw === 'dry-run' || raw === 'dryrun') ? 'proposal' : 'conservative'
|
|
1360
|
-
}
|
|
1361
|
-
|
|
1362
|
-
function scheduledCycle3Signature(config) {
|
|
1363
|
-
const retryConfig = config?.cycle3 || config
|
|
1364
|
-
return makeCycleRequestSignature('cycle3', retryConfig, {
|
|
1365
|
-
applyMode: scheduledCycle3ApplyMode(config),
|
|
1366
|
-
apply: undefined,
|
|
1367
|
-
})
|
|
1368
|
-
}
|
|
1369
|
-
|
|
1370
1421
|
async function enqueueScheduledCycle(kind, intervalMs, signature) {
|
|
1371
1422
|
const claim = await claimAndMarkScheduledCycle(db, kind, intervalMs, signature, { reason: 'scheduled' })
|
|
1372
1423
|
return claim.claimed === true
|
|
@@ -1427,6 +1478,7 @@ function scheduleScheduledCycle2(config, signature, attempt = 0) {
|
|
|
1427
1478
|
return
|
|
1428
1479
|
}
|
|
1429
1480
|
_cycle2InFlight = true
|
|
1481
|
+
markCycleRunning('cycle2')
|
|
1430
1482
|
try {
|
|
1431
1483
|
let c2Options = {
|
|
1432
1484
|
coalescedRetry: true,
|
|
@@ -1445,8 +1497,10 @@ function scheduleScheduledCycle2(config, signature, attempt = 0) {
|
|
|
1445
1497
|
}
|
|
1446
1498
|
} catch (err) {
|
|
1447
1499
|
__mixdogMemoryLog(`[cycle2] scheduled queue failed: ${err?.message || err}\n`)
|
|
1500
|
+
markCycleDone('cycle2', false, err?.message || err)
|
|
1448
1501
|
} finally {
|
|
1449
1502
|
_cycle2InFlight = false
|
|
1503
|
+
if (_cycleRunning?.cycle === 'cycle2') { _cycleRunning = null; _writeCycleStateFile() }
|
|
1450
1504
|
}
|
|
1451
1505
|
}, config, signature)
|
|
1452
1506
|
}
|
|
@@ -1464,10 +1518,11 @@ function scheduleScheduledCycle3(config, signature, attempt = 0) {
|
|
|
1464
1518
|
return
|
|
1465
1519
|
}
|
|
1466
1520
|
_cycle3InFlight = true
|
|
1521
|
+
markCycleRunning('cycle3')
|
|
1467
1522
|
try {
|
|
1468
1523
|
let c3Options = {
|
|
1469
1524
|
coalescedRetry: true,
|
|
1470
|
-
onCoalescedSuccess: () => setCycleLastRun('cycle3', Date.now()),
|
|
1525
|
+
onCoalescedSuccess: async () => { await setCycleLastRun('cycle3', Date.now()); markCycleDone('cycle3', true) },
|
|
1471
1526
|
}
|
|
1472
1527
|
if (typeof c3Options?.callLlm !== 'function') {
|
|
1473
1528
|
c3Options = { ...c3Options, callLlm: getCycle3CallLlm() }
|
|
@@ -1480,8 +1535,10 @@ function scheduleScheduledCycle3(config, signature, attempt = 0) {
|
|
|
1480
1535
|
}
|
|
1481
1536
|
} catch (err) {
|
|
1482
1537
|
__mixdogMemoryLog(`[cycle3] scheduled queue failed: ${err?.message || err}\n`)
|
|
1538
|
+
markCycleDone('cycle3', false, err?.message || err)
|
|
1483
1539
|
} finally {
|
|
1484
1540
|
_cycle3InFlight = false
|
|
1541
|
+
if (_cycleRunning?.cycle === 'cycle3') { _cycleRunning = null; _writeCycleStateFile() }
|
|
1485
1542
|
}
|
|
1486
1543
|
}, retryConfig, signature)
|
|
1487
1544
|
}
|
|
@@ -1495,9 +1552,11 @@ async function _finalizeCycle2Run(result) {
|
|
|
1495
1552
|
await setCycleLastRun('cycle2', Date.now())
|
|
1496
1553
|
await setCycleLastRun('cycle2_last_error', '')
|
|
1497
1554
|
__mixdogMemoryLog('[cycle2] completed\n')
|
|
1555
|
+
markCycleDone('cycle2', true)
|
|
1498
1556
|
} else {
|
|
1499
1557
|
await setCycleLastRun('cycle2_last_error', result.error || 'unknown error')
|
|
1500
1558
|
__mixdogMemoryLog(`[cycle2] failed: ${result.error}\n`)
|
|
1559
|
+
markCycleDone('cycle2', false, result.error || 'unknown error')
|
|
1501
1560
|
}
|
|
1502
1561
|
}
|
|
1503
1562
|
|
|
@@ -1574,6 +1633,36 @@ async function checkCycles() {
|
|
|
1574
1633
|
if (now - last.cycle3 >= cycle3Ms) {
|
|
1575
1634
|
await enqueueScheduledCycle3(cycle3Ms, 'scheduled')
|
|
1576
1635
|
}
|
|
1636
|
+
|
|
1637
|
+
// ── Backlog watch + self-kick (cycle-health) ─────────────────────────────
|
|
1638
|
+
// Cheap counts each tick (60s): surface a WARN when the pipeline is
|
|
1639
|
+
// building a backlog, and if a cycle has not SUCCEEDED for 2h+ while its
|
|
1640
|
+
// backlog is over threshold, kick one unscheduled run now instead of
|
|
1641
|
+
// waiting for the interval — a stalled cycle otherwise hides behind
|
|
1642
|
+
// "the schedule ran it" (attempt != success; see 2026-07 cycle2 stall).
|
|
1643
|
+
try {
|
|
1644
|
+
const unchunked = Number((await db.query(
|
|
1645
|
+
`SELECT COUNT(*) c FROM entries WHERE chunk_root IS NULL AND NULLIF(btrim(session_id), '') IS NOT NULL`,
|
|
1646
|
+
)).rows[0]?.c ?? 0)
|
|
1647
|
+
const cycle2Pending = Number((await db.query(
|
|
1648
|
+
`SELECT COUNT(*) c FROM entries WHERE is_root = 1 AND status = 'pending'`,
|
|
1649
|
+
)).rows[0]?.c ?? 0)
|
|
1650
|
+
_cycleBacklogSnapshot = { unchunked, cycle2_pending: cycle2Pending, at: now }
|
|
1651
|
+
_writeCycleStateFile()
|
|
1652
|
+
if (unchunked > BACKLOG_WARN_PENDING || cycle2Pending > BACKLOG_WARN_PENDING) {
|
|
1653
|
+
_warnCycleHealth(`backlog unchunked=${unchunked} cycle2_pending=${cycle2Pending}`)
|
|
1654
|
+
}
|
|
1655
|
+
const SELF_KICK_STALE_MS = 2 * 3600_000
|
|
1656
|
+
const c1Stale = (_cycleHealth.cycle1.last_success_at || last.cycle1 || 0) < now - SELF_KICK_STALE_MS
|
|
1657
|
+
const c2Stale = (_cycleHealth.cycle2.last_success_at || last.cycle2 || 0) < now - SELF_KICK_STALE_MS
|
|
1658
|
+
if (c1Stale && unchunked > BACKLOG_WARN_PENDING) {
|
|
1659
|
+
_warnCycleHealth(`cycle1 self-kick: no success 2h+, unchunked=${unchunked}`)
|
|
1660
|
+
await enqueueScheduledCycle1(0, 'self-kick')
|
|
1661
|
+
} else if (c2Stale && cycle2Pending > BACKLOG_WARN_PENDING) {
|
|
1662
|
+
_warnCycleHealth(`cycle2 self-kick: no success 2h+, pending=${cycle2Pending}`)
|
|
1663
|
+
await enqueueScheduledCycle2(0, 'self-kick')
|
|
1664
|
+
}
|
|
1665
|
+
} catch { /* counts are best-effort; never fail the tick */ }
|
|
1577
1666
|
}
|
|
1578
1667
|
let _cycle2InFlight = false
|
|
1579
1668
|
let _cycle3InFlight = false
|
|
@@ -1654,114 +1743,6 @@ function _beginRuntimeInit() {
|
|
|
1654
1743
|
return _initPromise
|
|
1655
1744
|
}
|
|
1656
1745
|
|
|
1657
|
-
|
|
1658
|
-
function parsePeriod(period, hasQuery) {
|
|
1659
|
-
if (!period && hasQuery) period = '30d'
|
|
1660
|
-
if (!period) return null
|
|
1661
|
-
if (period === 'all') return null
|
|
1662
|
-
if (period === 'last') return { mode: 'last' }
|
|
1663
|
-
// Calendar-day windows: 'today' anchors at local midnight rather than
|
|
1664
|
-
// rolling 24h. Without this, a query asking 'today' at 01:30 would silently
|
|
1665
|
-
// include yesterday's last 22.5h of activity, mislabelling them as
|
|
1666
|
-
// 'today's work'. 'yesterday' is the previous calendar day.
|
|
1667
|
-
if (period === 'today') {
|
|
1668
|
-
const start = new Date()
|
|
1669
|
-
start.setHours(0, 0, 0, 0)
|
|
1670
|
-
return { startMs: start.getTime(), endMs: Date.now() }
|
|
1671
|
-
}
|
|
1672
|
-
if (period === 'yesterday') {
|
|
1673
|
-
const start = new Date()
|
|
1674
|
-
start.setDate(start.getDate() - 1)
|
|
1675
|
-
start.setHours(0, 0, 0, 0)
|
|
1676
|
-
const end = new Date(start)
|
|
1677
|
-
end.setHours(23, 59, 59, 999)
|
|
1678
|
-
return { startMs: start.getTime(), endMs: end.getTime() }
|
|
1679
|
-
}
|
|
1680
|
-
if (period === 'this_week' || period === 'last_week') {
|
|
1681
|
-
// R6 P9: calendar Mon-Sun previous/current week. Mon-start ISO
|
|
1682
|
-
// convention. Replaces R5 rolling 7-14d range which was empty for
|
|
1683
|
-
// sessions where "last week" decisions actually fell on Mon (4/27) of
|
|
1684
|
-
// this week. Precise calendar bounds match natural-language intuition.
|
|
1685
|
-
const d = new Date()
|
|
1686
|
-
d.setHours(0, 0, 0, 0)
|
|
1687
|
-
const dayOfWeek = d.getDay()
|
|
1688
|
-
const daysSinceMon = (dayOfWeek + 6) % 7
|
|
1689
|
-
const thisWeekMon = new Date(d)
|
|
1690
|
-
thisWeekMon.setDate(d.getDate() - daysSinceMon)
|
|
1691
|
-
if (period === 'this_week') {
|
|
1692
|
-
return { startMs: thisWeekMon.getTime(), endMs: Date.now() }
|
|
1693
|
-
}
|
|
1694
|
-
const lastWeekMon = new Date(thisWeekMon)
|
|
1695
|
-
lastWeekMon.setDate(thisWeekMon.getDate() - 7)
|
|
1696
|
-
const lastWeekSunEnd = new Date(thisWeekMon.getTime() - 1)
|
|
1697
|
-
return { startMs: lastWeekMon.getTime(), endMs: lastWeekSunEnd.getTime() }
|
|
1698
|
-
}
|
|
1699
|
-
const relMatch = period.match(/^(\d+)(m|h|d)$/)
|
|
1700
|
-
if (relMatch) {
|
|
1701
|
-
const n = parseInt(relMatch[1])
|
|
1702
|
-
const unit = relMatch[2]
|
|
1703
|
-
const now = new Date()
|
|
1704
|
-
if (unit === 'm') {
|
|
1705
|
-
// Minute granularity is for "resume from the previous turn / pick
|
|
1706
|
-
// up where we left off" style recall — sub-hour windows where 1h
|
|
1707
|
-
// is too coarse. n=0 is invalid (the regex requires \d+ which
|
|
1708
|
-
// matches "0" but a zero-width window returns no rows; leave that
|
|
1709
|
-
// as caller-supplied no-op).
|
|
1710
|
-
const start = new Date(now.getTime() - n * 60_000)
|
|
1711
|
-
return { startMs: start.getTime(), endMs: now.getTime() }
|
|
1712
|
-
}
|
|
1713
|
-
if (unit === 'h') {
|
|
1714
|
-
const start = new Date(now.getTime() - n * 3600_000)
|
|
1715
|
-
return { startMs: start.getTime(), endMs: now.getTime() }
|
|
1716
|
-
}
|
|
1717
|
-
const start = new Date(now)
|
|
1718
|
-
start.setDate(start.getDate() - n)
|
|
1719
|
-
return { startMs: start.getTime(), endMs: now.getTime() }
|
|
1720
|
-
}
|
|
1721
|
-
const rangeMatch = period.match(/^(\d{4}-\d{2}-\d{2})~(\d{4}-\d{2}-\d{2})$/)
|
|
1722
|
-
if (rangeMatch) {
|
|
1723
|
-
return {
|
|
1724
|
-
startMs: Date.parse(rangeMatch[1] + 'T00:00:00'),
|
|
1725
|
-
endMs: Date.parse(rangeMatch[2] + 'T23:59:59.999'),
|
|
1726
|
-
}
|
|
1727
|
-
}
|
|
1728
|
-
const dateMatch = period.match(/^(\d{4}-\d{2}-\d{2})$/)
|
|
1729
|
-
if (dateMatch) {
|
|
1730
|
-
return {
|
|
1731
|
-
startMs: Date.parse(dateMatch[1] + 'T00:00:00'),
|
|
1732
|
-
endMs: Date.parse(dateMatch[1] + 'T23:59:59.999'),
|
|
1733
|
-
exact: true,
|
|
1734
|
-
}
|
|
1735
|
-
}
|
|
1736
|
-
return null
|
|
1737
|
-
}
|
|
1738
|
-
|
|
1739
|
-
function formatTs(tsMs) {
|
|
1740
|
-
const n = Number(tsMs)
|
|
1741
|
-
if (Number.isFinite(n) && n > 1e12) {
|
|
1742
|
-
return new Date(n).toLocaleString('sv-SE').slice(0, 16)
|
|
1743
|
-
}
|
|
1744
|
-
return String(tsMs ?? '').slice(0, 16)
|
|
1745
|
-
}
|
|
1746
|
-
|
|
1747
|
-
const CORE_RECALL_STOPWORDS = new Set([
|
|
1748
|
-
'about', 'after', 'again', 'before', 'check', 'color', 'decision', 'decided',
|
|
1749
|
-
'earlier', 'memory', 'previous', 'routing', 'stored', 'tell', 'what',
|
|
1750
|
-
])
|
|
1751
|
-
|
|
1752
|
-
function coreRecallTerms(query) {
|
|
1753
|
-
return [...new Set(String(query || '').toLowerCase().match(/[\p{L}\p{N}_-]{4,}/gu) || [])]
|
|
1754
|
-
.filter((term) => !CORE_RECALL_STOPWORDS.has(term))
|
|
1755
|
-
.slice(0, 8)
|
|
1756
|
-
}
|
|
1757
|
-
|
|
1758
|
-
function normalizeRecallProjectScope(projectScope) {
|
|
1759
|
-
const raw = String(projectScope || 'common').trim()
|
|
1760
|
-
if (!raw || raw.toLowerCase() === 'common') return null
|
|
1761
|
-
if (raw.toLowerCase() === 'all') return '*'
|
|
1762
|
-
return raw
|
|
1763
|
-
}
|
|
1764
|
-
|
|
1765
1746
|
async function recallCoreRows(query, { projectScope, category, limit } = {}) {
|
|
1766
1747
|
const terms = coreRecallTerms(query)
|
|
1767
1748
|
if (terms.length === 0) return []
|
|
@@ -1815,10 +1796,120 @@ async function recallCoreRows(query, { projectScope, category, limit } = {}) {
|
|
|
1815
1796
|
}))
|
|
1816
1797
|
}
|
|
1817
1798
|
|
|
1799
|
+
// Session-scoped full drain before a recall(query) search. Chunking is
|
|
1800
|
+
// partitioned by session_id (memory-cycle1.mjs:409-431 selected_sessions
|
|
1801
|
+
// GROUP BY session_id + ROW_NUMBER PARTITION BY session_id), so this drains
|
|
1802
|
+
// ONLY the calling session's backlog via the same session_id override
|
|
1803
|
+
// (memory-cycle1.mjs:394) — other sessions' pending rows are left for the
|
|
1804
|
+
// periodic cycle1 sweep. Reuses recall-fasttrack's drainSessionCycle1
|
|
1805
|
+
// (session/compact.mjs:504) which loops cycle1 until raw rows hit 0 or a
|
|
1806
|
+
// no-progress pass. Safety limits: a 500-row hard cap (both in the pending
|
|
1807
|
+
// pre-check and as the per-call cycle1 batch ceiling) and cooperative
|
|
1808
|
+
// abort-signal propagation through handleMemoryAction — no time budget.
|
|
1809
|
+
// Window 20 rows per classifier prompt × concurrency 5 (haiku — cost is
|
|
1810
|
+
// negligible; 500 rows ≈ 25 prompts ≈ 5 waves).
|
|
1811
|
+
//
|
|
1812
|
+
// Multi-terminal safety: the memory daemon is one shared instance across
|
|
1813
|
+
// terminals (active-instance.json memory_port). Concurrent recall(query)
|
|
1814
|
+
// calls for the SAME session share one in-flight drain promise (dedup);
|
|
1815
|
+
// different sessions drain independently in parallel (cycle1's own
|
|
1816
|
+
// advisory-lock/coalesce guard in memory-cycle1.mjs still serializes actual
|
|
1817
|
+
// DB writes). A drain only gates recall for its OWN session — every other
|
|
1818
|
+
// terminal's recall (different session, or no session) proceeds unblocked.
|
|
1819
|
+
const RECALL_DRAIN_HARD_CAP_ROWS = 500
|
|
1820
|
+
async function _drainSessionForRecallQuery(sessionId, signal, deadlineMs = 0) {
|
|
1821
|
+
if (!sessionId || signal?.aborted) return
|
|
1822
|
+
const existing = _recallDrainInFlight.get(sessionId)
|
|
1823
|
+
if (existing) { try { await existing } catch {} return }
|
|
1824
|
+
let pendingCount = 0
|
|
1825
|
+
try {
|
|
1826
|
+
const r = await db.query(
|
|
1827
|
+
`SELECT COUNT(*) c FROM entries WHERE chunk_root IS NULL AND session_id = $1`,
|
|
1828
|
+
[sessionId],
|
|
1829
|
+
)
|
|
1830
|
+
pendingCount = Number(r.rows?.[0]?.c ?? 0)
|
|
1831
|
+
} catch { return }
|
|
1832
|
+
if (pendingCount <= 0) return
|
|
1833
|
+
const runTool = async (_name, toolArgs) => {
|
|
1834
|
+
const result = await handleMemoryAction(toolArgs, signal)
|
|
1835
|
+
return result?.text ?? ''
|
|
1836
|
+
}
|
|
1837
|
+
const dumpArgs = {
|
|
1838
|
+
action: 'dump_session_roots',
|
|
1839
|
+
sessionId,
|
|
1840
|
+
includeRaw: true,
|
|
1841
|
+
limit: Math.min(RECALL_DRAIN_HARD_CAP_ROWS, Math.max(1, pendingCount)),
|
|
1842
|
+
}
|
|
1843
|
+
const drainPromise = (async () => {
|
|
1844
|
+
try {
|
|
1845
|
+
return await drainSessionCycle1(runTool, {
|
|
1846
|
+
sessionId,
|
|
1847
|
+
dumpArgs,
|
|
1848
|
+
deadlineMs, // 0 = no time budget (query path); bare browse passes a
|
|
1849
|
+
// short bound so a browse call never blocks minutes on LLM chunking.
|
|
1850
|
+
maxPasses: 20, // safety stopper; drainSessionCycle1 also self-stops on no-progress
|
|
1851
|
+
cycleArgs: {
|
|
1852
|
+
min_batch: 1,
|
|
1853
|
+
session_cap: 1,
|
|
1854
|
+
batch_size: RECALL_DRAIN_HARD_CAP_ROWS,
|
|
1855
|
+
rows_per_session: RECALL_DRAIN_HARD_CAP_ROWS,
|
|
1856
|
+
window_size: 20,
|
|
1857
|
+
concurrency: 5,
|
|
1858
|
+
},
|
|
1859
|
+
})
|
|
1860
|
+
} catch (err) {
|
|
1861
|
+
__mixdogMemoryLog(`[recall] session drain aborted/failed (sess=${sessionId}): ${err?.message || err}\n`)
|
|
1862
|
+
return null
|
|
1863
|
+
}
|
|
1864
|
+
})()
|
|
1865
|
+
_recallDrainInFlight.set(sessionId, drainPromise)
|
|
1866
|
+
try {
|
|
1867
|
+
await drainPromise
|
|
1868
|
+
} finally {
|
|
1869
|
+
if (_recallDrainInFlight.get(sessionId) === drainPromise) _recallDrainInFlight.delete(sessionId)
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
// Bare-browse drain deadline: best-effort only. A browse call ("show me
|
|
1874
|
+
// recent context") must never block on LLM chunking for minutes the way a
|
|
1875
|
+
// query recall is allowed to — the raw-row merge already covers unchunked
|
|
1876
|
+
// content, so the drain here is a nice-to-have upgrade to chunked/scored
|
|
1877
|
+
// rows, not a correctness requirement. Failures/timeouts are silently
|
|
1878
|
+
// tolerated; the raw fallback in the caller applies regardless.
|
|
1879
|
+
const RECALL_BROWSE_DRAIN_DEADLINE_MS = 1800
|
|
1880
|
+
|
|
1818
1881
|
async function handleSearch(args, signal) {
|
|
1819
1882
|
// Cooperative abort check: throw early if the caller already aborted
|
|
1820
1883
|
// (IPC cancel handler signals the AbortController before re-entry).
|
|
1821
1884
|
if (signal?.aborted) throw signal.reason ?? new Error('aborted')
|
|
1885
|
+
// Drain this session's unchunked backlog to zero BEFORE searching, so a
|
|
1886
|
+
// recall never returns raw/unclassified transcript tail instead of the
|
|
1887
|
+
// chunked summary. Runs whenever a session is scoped, query or not — a
|
|
1888
|
+
// bare "show recent" browse hits raw unchunked rows just as often as a
|
|
1889
|
+
// query recall does. Query path keeps the unbounded (deadlineMs:0) drain
|
|
1890
|
+
// since chunked accuracy matters more than latency there; bare browse
|
|
1891
|
+
// gets a short best-effort deadline — it must never block on LLM
|
|
1892
|
+
// chunking for minutes, the raw-row merge covers the fallback either way.
|
|
1893
|
+
{
|
|
1894
|
+
const _drainSessionId = String(args?.sessionId || args?.session_id || '').trim()
|
|
1895
|
+
const _drainHasQuery = Array.isArray(args?.query)
|
|
1896
|
+
? args.query.some((v) => String(v || '').trim())
|
|
1897
|
+
: String(args?.query ?? '').trim() !== ''
|
|
1898
|
+
if (_drainSessionId) {
|
|
1899
|
+
await _drainSessionForRecallQuery(
|
|
1900
|
+
_drainSessionId,
|
|
1901
|
+
signal,
|
|
1902
|
+
_drainHasQuery ? 0 : RECALL_BROWSE_DRAIN_DEADLINE_MS,
|
|
1903
|
+
)
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
// #id lookup normalization: search_memories and memory action:'search'
|
|
1907
|
+
// callers pass a single `id` (or an id array under that same key), not
|
|
1908
|
+
// the `ids` array below. Normalize once here so every dispatch path gets
|
|
1909
|
+
// exact-id lookup, not just callers who already knew to use `ids`.
|
|
1910
|
+
if (!Array.isArray(args.ids) && args.id != null) {
|
|
1911
|
+
args = { ...args, ids: Array.isArray(args.id) ? args.id : [args.id] }
|
|
1912
|
+
}
|
|
1822
1913
|
if (args?.currentSession === true || args?.sessionId || args?.session_id) {
|
|
1823
1914
|
return await recallSessionRows(args)
|
|
1824
1915
|
}
|
|
@@ -1830,7 +1921,7 @@ async function handleSearch(args, signal) {
|
|
|
1830
1921
|
if (Array.isArray(args.ids) && args.ids.length > 0) {
|
|
1831
1922
|
const ids = args.ids
|
|
1832
1923
|
.map(v => Number(v))
|
|
1833
|
-
.filter(v => Number.
|
|
1924
|
+
.filter(v => Number.isInteger(v) && v > 0)
|
|
1834
1925
|
if (ids.length === 0) return { text: '(no valid ids)' }
|
|
1835
1926
|
const includeArchived = args.includeArchived !== false
|
|
1836
1927
|
const category = args.category
|
|
@@ -1935,8 +2026,6 @@ async function handleSearch(args, signal) {
|
|
|
1935
2026
|
})),
|
|
1936
2027
|
deadlineRace,
|
|
1937
2028
|
])
|
|
1938
|
-
} catch (err) {
|
|
1939
|
-
throw err
|
|
1940
2029
|
} finally {
|
|
1941
2030
|
clearTimeout(deadlineTimer)
|
|
1942
2031
|
}
|
|
@@ -1972,7 +2061,15 @@ async function handleSearch(args, signal) {
|
|
|
1972
2061
|
offset = Math.min(RECALL_OFFSET_CAP, offset)
|
|
1973
2062
|
}
|
|
1974
2063
|
const recallCapPrefix = recallCapNotes.length ? `${recallCapNotes.join('; ')}\n` : ''
|
|
1975
|
-
|
|
2064
|
+
// Recent-browsing default: a query-less recall is a "show me the latest
|
|
2065
|
+
// messages" browse, not a relevance search — chronological order is the
|
|
2066
|
+
// only ordering that makes sense there, so sort defaults to 'date' when
|
|
2067
|
+
// no query is present (explicit args.sort still wins). Query recalls keep
|
|
2068
|
+
// the importance default.
|
|
2069
|
+
const hasQueryForSort = Array.isArray(args.query)
|
|
2070
|
+
? args.query.some((v) => String(v || '').trim())
|
|
2071
|
+
: String(args.query ?? '').trim() !== ''
|
|
2072
|
+
const sort = args.sort != null ? String(args.sort) : (hasQueryForSort ? 'importance' : 'date')
|
|
1976
2073
|
// Chunk content is the primary recall output. Members default to true so
|
|
1977
2074
|
// callers receive the raw chunk leaves (the cycle1-produced semantic
|
|
1978
2075
|
// chunks) rather than just the root's cycle2-compressed summary line.
|
|
@@ -2071,13 +2168,16 @@ async function handleSearch(args, signal) {
|
|
|
2071
2168
|
})
|
|
2072
2169
|
}
|
|
2073
2170
|
if (includeRaw) {
|
|
2074
|
-
//
|
|
2075
|
-
//
|
|
2076
|
-
//
|
|
2077
|
-
//
|
|
2078
|
-
// window
|
|
2079
|
-
//
|
|
2080
|
-
|
|
2171
|
+
// Raw rows (chunk_root IS NULL) carry no retrievalScore, so a naive
|
|
2172
|
+
// append-after-hybrid under sort=importance always lands them past
|
|
2173
|
+
// slice(offset, offset+limit) once the hybrid pool exceeds one page —
|
|
2174
|
+
// every page beyond the first silently drops them. Fetch a wider raw
|
|
2175
|
+
// window (bounded like the hybrid candidate pool) and spread the
|
|
2176
|
+
// fetched raw rows evenly across the WHOLE hybrid list before slicing,
|
|
2177
|
+
// so every offset page gets its proportional share instead of only
|
|
2178
|
+
// page 0. Same projectScope/ts window as the hybrid leg — filter
|
|
2179
|
+
// parity (item 3) is deliberate, not accidental.
|
|
2180
|
+
const RAW_FETCH = Math.min(500, Math.max(20, limit + offset))
|
|
2081
2181
|
const rawRows = await readRawRowsInWindow(
|
|
2082
2182
|
db,
|
|
2083
2183
|
temporal?.startMs ?? null,
|
|
@@ -2091,10 +2191,10 @@ async function handleSearch(args, signal) {
|
|
|
2091
2191
|
for (const r of newRaw) filtered.push(r)
|
|
2092
2192
|
filtered.sort((a, b) => (Number(b.ts) || 0) - (Number(a.ts) || 0))
|
|
2093
2193
|
} else {
|
|
2094
|
-
// sort=importance:
|
|
2095
|
-
//
|
|
2096
|
-
//
|
|
2097
|
-
|
|
2194
|
+
// sort=importance: interleave raw rows at a fixed stride through the
|
|
2195
|
+
// full (pre-slice) hybrid list instead of appending at the tail, so
|
|
2196
|
+
// offset > 0 pages also draw from the raw pool proportionally.
|
|
2197
|
+
filtered = interleaveRawRows(filtered, newRaw)
|
|
2098
2198
|
}
|
|
2099
2199
|
}
|
|
2100
2200
|
const coreRows = await recallCoreRows(query, { projectScope, category, limit })
|
|
@@ -2133,7 +2233,16 @@ async function handleSearch(args, signal) {
|
|
|
2133
2233
|
|
|
2134
2234
|
const filters = { limit: limit + offset }
|
|
2135
2235
|
if (temporal?.startMs != null) { filters.ts_from = temporal.startMs; filters.ts_to = temporal.endMs }
|
|
2136
|
-
|
|
2236
|
+
// period='last' used to hard-cap ts_to at _bootTimestamp-1 unconditionally,
|
|
2237
|
+
// which silently hid the CURRENT session's own content from a query-less
|
|
2238
|
+
// recent browse (the whole point of "what did we just talk about"). Keep
|
|
2239
|
+
// the boot cap only for session-less global browsing, where 'last' means
|
|
2240
|
+
// "the previously completed session" and there's no session-scoped drain
|
|
2241
|
+
// above to have already surfaced the in-flight rows. When a sessionId is
|
|
2242
|
+
// present the drain above already pulled that session's raw backlog to
|
|
2243
|
+
// zero, so the current session's rows are safe (and wanted) to include.
|
|
2244
|
+
const _hasScopedSession = Boolean(String(args?.sessionId || args?.session_id || '').trim())
|
|
2245
|
+
if (temporal?.mode === 'last' && _bootTimestamp && !_hasScopedSession) {
|
|
2137
2246
|
filters.ts_to = _bootTimestamp - 1
|
|
2138
2247
|
}
|
|
2139
2248
|
filters.projectScope = projectScope
|
|
@@ -2142,55 +2251,33 @@ async function handleSearch(args, signal) {
|
|
|
2142
2251
|
if (!includeArchived) filters.excludeStatuses = ['archived']
|
|
2143
2252
|
if (includeMembers) filters.includeMembers = true
|
|
2144
2253
|
const rows = await retrieveEntries(db, filters)
|
|
2145
|
-
|
|
2146
|
-
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
const
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
const content = cleanMemoryText(String(m.content ?? '')).slice(0, 1000)
|
|
2169
|
-
lines.push(`[${mTs}] ${role}: ${content} #${m.id}`)
|
|
2170
|
-
}
|
|
2171
|
-
} else {
|
|
2172
|
-
if (lines.length >= RECALL_LINE_CAP) { _capped = true; break }
|
|
2173
|
-
// No chunks (root not yet chunked by cycle1, or orphan leaf): emit
|
|
2174
|
-
// the row itself in the same shape. element/summary fall back to
|
|
2175
|
-
// raw content when both are absent.
|
|
2176
|
-
const ts = formatTs(r.ts)
|
|
2177
|
-
const element = r.element ?? ''
|
|
2178
|
-
const summary = r.summary ?? ''
|
|
2179
|
-
// Standalone leaf rows (is_root=0, no parent chunks_root resolved
|
|
2180
|
-
// into a `members` list) carry their u/a role just like inline
|
|
2181
|
-
// chunk members — surface it so the format stays consistent across
|
|
2182
|
-
// the two emission paths.
|
|
2183
|
-
const rolePrefix = r.is_root === 0 && r.role
|
|
2184
|
-
? (r.role === 'user' ? 'u: ' : r.role === 'assistant' ? 'a: ' : `${r.role}: `)
|
|
2185
|
-
: ''
|
|
2186
|
-
const body = element || summary
|
|
2187
|
-
? `${element}${summary ? ' — ' + summary : ''}`
|
|
2188
|
-
: cleanMemoryText(String(r.content ?? '')).slice(0, 1000)
|
|
2189
|
-
lines.push(`[${ts}] ${rolePrefix}${body.slice(0, 1000)} #${r.id}`)
|
|
2254
|
+
// Recent-browsing raw merge: a query-less recall must show the freshest
|
|
2255
|
+
// turns even when cycle1 hasn't chunked them yet. Roots lag ingest by up
|
|
2256
|
+
// to a cycle interval, so on sort=date pull the raw (unchunked) window
|
|
2257
|
+
// too and merge chronologically — original text first, no summaries.
|
|
2258
|
+
// Query-less + includeRaw:false callers keep the roots-only view.
|
|
2259
|
+
let merged = rows
|
|
2260
|
+
if (sort === 'date' && args.includeRaw !== false) {
|
|
2261
|
+
const rawRows = await readRawRowsInWindow(
|
|
2262
|
+
db,
|
|
2263
|
+
temporal?.startMs ?? null,
|
|
2264
|
+
temporal?.endMs ?? (filters.ts_to ?? Date.now()),
|
|
2265
|
+
Math.min(500, Math.max(20, limit + offset)),
|
|
2266
|
+
{ projectScope },
|
|
2267
|
+
)
|
|
2268
|
+
const seenIds = new Set(rows.map(r => Number(r.id)))
|
|
2269
|
+
// Drop raw leaves already inlined as some returned root's member.
|
|
2270
|
+
for (const r of rows) {
|
|
2271
|
+
if (Array.isArray(r.members)) for (const m of r.members) seenIds.add(Number(m.id))
|
|
2272
|
+
}
|
|
2273
|
+
const newRaw = rawRows.filter(r => !seenIds.has(Number(r.id)))
|
|
2274
|
+
if (newRaw.length > 0) {
|
|
2275
|
+
merged = [...rows, ...newRaw]
|
|
2276
|
+
merged.sort((a, b) => (Number(b.ts) || 0) - (Number(a.ts) || 0) || (Number(b.id) || 0) - (Number(a.id) || 0))
|
|
2190
2277
|
}
|
|
2191
2278
|
}
|
|
2192
|
-
|
|
2193
|
-
return
|
|
2279
|
+
const sliced = merged.slice(offset, offset + limit)
|
|
2280
|
+
return { text: recallCapPrefix + renderEntryLines(sliced) }
|
|
2194
2281
|
}
|
|
2195
2282
|
|
|
2196
2283
|
async function dumpSessionRootChunks(args = {}) {
|
|
@@ -2824,11 +2911,57 @@ async function handleMemoryAction(args, signal) {
|
|
|
2824
2911
|
|
|
2825
2912
|
if (action === 'core') {
|
|
2826
2913
|
const op = String(args.op ?? '').trim().toLowerCase()
|
|
2827
|
-
if (!['add', 'edit', 'delete', 'list'].includes(op)) {
|
|
2828
|
-
return { text: 'core requires op: "add" | "edit" | "delete" | "list"', isError: true }
|
|
2914
|
+
if (!['add', 'edit', 'delete', 'list', 'candidates', 'promote', 'dismiss'].includes(op)) {
|
|
2915
|
+
return { text: 'core requires op: "add" | "edit" | "delete" | "list" | "candidates" | "promote" | "dismiss"', isError: true }
|
|
2829
2916
|
}
|
|
2830
2917
|
const dataDir = (typeof DATA_DIR === 'string' ? DATA_DIR : resolvePluginData())
|
|
2831
2918
|
if (!dataDir) return { text: 'core: memory data dir is not initialized', isError: true }
|
|
2919
|
+
// Core-candidate promotion pipeline (proposal mode). The candidate flag
|
|
2920
|
+
// lives on generated `entries`, which carry a project_id, so these ops MUST
|
|
2921
|
+
// be project-scoped just like add/edit/delete — an unscoped listing/promote
|
|
2922
|
+
// would leak candidates across projects. project_id resolution mirrors the
|
|
2923
|
+
// block below: 'common'/null → COMMON (project_id NULL), '*' → all pools
|
|
2924
|
+
// (candidates op only, same escape hatch as op:'list'). UI calls exactly
|
|
2925
|
+
// these op names.
|
|
2926
|
+
if (op === 'candidates' || op === 'promote' || op === 'dismiss') {
|
|
2927
|
+
const hasPid = Object.prototype.hasOwnProperty.call(args, 'project_id')
|
|
2928
|
+
const scope = (() => {
|
|
2929
|
+
if (!hasPid || args.project_id == null) return null
|
|
2930
|
+
const s = String(args.project_id).trim()
|
|
2931
|
+
if (s === '' || s.toLowerCase() === 'common') return null
|
|
2932
|
+
if (s === '*') return '*'
|
|
2933
|
+
return s
|
|
2934
|
+
})()
|
|
2935
|
+
try {
|
|
2936
|
+
if (op === 'candidates') {
|
|
2937
|
+
const list = await listCoreCandidates(dataDir, scope)
|
|
2938
|
+
if (list.length === 0) return { text: 'core candidates: none' }
|
|
2939
|
+
return {
|
|
2940
|
+
text: list.map(c =>
|
|
2941
|
+
// project=<pool> lets the UI thread project_id into the follow-up
|
|
2942
|
+
// promote/dismiss call — matters under project_id:'*' listing where
|
|
2943
|
+
// rows span pools. Uses the same COMMON/slug convention as op:'list'.
|
|
2944
|
+
`id=${c.id} project=${c.project_id == null ? 'COMMON' : c.project_id} [${c.category}] score=${c.score == null ? '-' : c.score.toFixed(2)} ${c.element} — ${String(c.summary || '').slice(0, 200)} (${c.reason})`,
|
|
2945
|
+
).join('\n'),
|
|
2946
|
+
}
|
|
2947
|
+
}
|
|
2948
|
+
// promote/dismiss operate on a single id but are scope-guarded: the
|
|
2949
|
+
// candidate must belong to the resolved scope (or COMMON), never '*'.
|
|
2950
|
+
if (scope === '*') {
|
|
2951
|
+
return { text: `core ${op}: project_id "*" only valid for op="candidates"`, isError: true }
|
|
2952
|
+
}
|
|
2953
|
+
if (op === 'promote') {
|
|
2954
|
+
const entry = await promoteCoreCandidate(dataDir, args.id, { ...args, scope })
|
|
2955
|
+
const mergeNote = entry.merged_with ? ` (merged into core id=${entry.merged_with}, sim=${entry.sim})` : ''
|
|
2956
|
+
return { text: `core promoted candidate id=${args.id} → core id=${entry.id}${mergeNote}: [${entry.category}] ${entry.element}` }
|
|
2957
|
+
}
|
|
2958
|
+
// dismiss
|
|
2959
|
+
const removed = await dismissCoreCandidate(dataDir, args.id, { scope })
|
|
2960
|
+
return { text: `core candidate dismissed (id=${removed.id}): [${removed.category}] ${removed.element}` }
|
|
2961
|
+
} catch (e) {
|
|
2962
|
+
return { text: `core ${op} failed: ${e.message}`, isError: true }
|
|
2963
|
+
}
|
|
2964
|
+
}
|
|
2832
2965
|
// Local trim helper — the manage-block trimOrNull at :1807 is scoped to
|
|
2833
2966
|
// that branch and unreachable from here.
|
|
2834
2967
|
// Normalize project_id: 'common' (case-insensitive) or null → null (COMMON pool); non-empty string → slug.
|
|
@@ -3095,37 +3228,6 @@ function createHttpMcpServer() {
|
|
|
3095
3228
|
return s
|
|
3096
3229
|
}
|
|
3097
3230
|
|
|
3098
|
-
function readBody(req) {
|
|
3099
|
-
return new Promise((resolve, reject) => {
|
|
3100
|
-
const chunks = []
|
|
3101
|
-
req.on('data', c => chunks.push(c))
|
|
3102
|
-
req.on('end', () => {
|
|
3103
|
-
const raw = Buffer.concat(chunks).toString('utf8').trim()
|
|
3104
|
-
if (!raw) { resolve({}); return }
|
|
3105
|
-
try { resolve(JSON.parse(raw)) }
|
|
3106
|
-
catch (error) {
|
|
3107
|
-
const e = new Error(`invalid JSON body: ${error.message}`)
|
|
3108
|
-
e.statusCode = 400
|
|
3109
|
-
reject(e)
|
|
3110
|
-
}
|
|
3111
|
-
})
|
|
3112
|
-
req.on('error', reject)
|
|
3113
|
-
})
|
|
3114
|
-
}
|
|
3115
|
-
|
|
3116
|
-
function sendJson(res, data, status = 200) {
|
|
3117
|
-
const body = JSON.stringify(data)
|
|
3118
|
-
res.writeHead(status, {
|
|
3119
|
-
'Content-Type': 'application/json; charset=utf-8',
|
|
3120
|
-
'Content-Length': Buffer.byteLength(body),
|
|
3121
|
-
})
|
|
3122
|
-
res.end(body)
|
|
3123
|
-
}
|
|
3124
|
-
|
|
3125
|
-
function sendError(res, msg, status = 500) {
|
|
3126
|
-
sendJson(res, { error: msg }, status)
|
|
3127
|
-
}
|
|
3128
|
-
|
|
3129
3231
|
async function awaitRuntimeReadyForHttp(res) {
|
|
3130
3232
|
if (_initialized) return true
|
|
3131
3233
|
if (!_initPromise) {
|
|
@@ -3141,29 +3243,6 @@ async function awaitRuntimeReadyForHttp(res) {
|
|
|
3141
3243
|
}
|
|
3142
3244
|
}
|
|
3143
3245
|
|
|
3144
|
-
// Origin/Referer guard for /admin/* mutation routes. Memory-service binds
|
|
3145
|
-
// 127.0.0.1, but browser DNS-rebinding or a stray cross-origin fetch could
|
|
3146
|
-
// still reach destructive endpoints (purge, backfill, entry mutations).
|
|
3147
|
-
// Server-to-server callers (setup-server, hooks) issue raw http.request
|
|
3148
|
-
// without a browser Origin/Referer, so absent headers pass; any non-loopback
|
|
3149
|
-
// Origin/Referer is rejected. Mirrors setup-server.mjs isAllowedOrigin.
|
|
3150
|
-
function isLocalOrigin(req) {
|
|
3151
|
-
const LOOP = /^https?:\/\/(localhost|127\.0\.0\.1|\[::1\])(:\d+)?(\/|$)/i
|
|
3152
|
-
const origin = req.headers.origin || ''
|
|
3153
|
-
const referer = req.headers.referer || ''
|
|
3154
|
-
if (origin && !LOOP.test(origin)) return false
|
|
3155
|
-
if (referer && !LOOP.test(referer)) return false
|
|
3156
|
-
return true
|
|
3157
|
-
}
|
|
3158
|
-
|
|
3159
|
-
function normalizeCoreProjectId(value, { allowStar = false } = {}) {
|
|
3160
|
-
if (value == null) return null
|
|
3161
|
-
const s = String(value).trim()
|
|
3162
|
-
if (!s || s.toLowerCase() === 'common') return null
|
|
3163
|
-
if (allowStar && s === '*') return '*'
|
|
3164
|
-
return s
|
|
3165
|
-
}
|
|
3166
|
-
|
|
3167
3246
|
async function buildSessionCoreMemoryPayload(cwd) {
|
|
3168
3247
|
const projectId = resolveProjectScope(typeof cwd === 'string' && cwd ? cwd : null)
|
|
3169
3248
|
const generatedScopeClause = projectId !== null
|
|
@@ -3246,6 +3325,9 @@ const httpServer = http.createServer(async (req, res) => {
|
|
|
3246
3325
|
active_core_summaries: stats.active_core_summaries,
|
|
3247
3326
|
active_core_summary_missing: stats.active_core_summary_missing,
|
|
3248
3327
|
mv_hot_active_populated: stats.mv_hot_active_populated,
|
|
3328
|
+
cycle_running: _cycleRunning,
|
|
3329
|
+
cycle_health: _cycleHealth,
|
|
3330
|
+
cycle_backlog: _cycleBacklogSnapshot,
|
|
3249
3331
|
})
|
|
3250
3332
|
} catch (e) { sendError(res, e.message) }
|
|
3251
3333
|
return
|
|
@@ -3920,8 +4002,46 @@ export async function stop() {
|
|
|
3920
4002
|
// postmaster keeps running after the memory service exits.
|
|
3921
4003
|
if (!memorySecondaryMode()) {
|
|
3922
4004
|
try {
|
|
3923
|
-
|
|
3924
|
-
|
|
4005
|
+
// Conservative check: only skip stopPgForShutdown when the owner
|
|
4006
|
+
// record is unambiguously (a) a memory-runtime-daemon owner record
|
|
4007
|
+
// (kind check — guards against a stale/foreign pid reusing this pid
|
|
4008
|
+
// number for an unrelated process) and (b) that pid is alive AND not
|
|
4009
|
+
// this process. Any read/parse failure or ambiguous state falls back
|
|
4010
|
+
// to stopping PG (previous unconditional behavior) rather than
|
|
4011
|
+
// risking an orphaned PG postmaster.
|
|
4012
|
+
const anotherOwnerAlive = await (async () => {
|
|
4013
|
+
try {
|
|
4014
|
+
const { readSingletonOwner } = await import('../shared/singleton-owner.mjs')
|
|
4015
|
+
const ownerPath = path.join(DATA_DIR, 'memory-runtime-owner.json')
|
|
4016
|
+
const { owner, alive } = readSingletonOwner(ownerPath)
|
|
4017
|
+
if (!alive) return false
|
|
4018
|
+
if (owner?.kind !== 'memory-runtime-daemon') return false
|
|
4019
|
+
const ownerPid = Number(owner?.pid)
|
|
4020
|
+
if (!Number.isInteger(ownerPid) || ownerPid === process.pid) return false
|
|
4021
|
+
// Best-effort process-name check (mirrors supervisor.mjs's
|
|
4022
|
+
// isPostgresPid pattern) — confirms the pid is actually a node
|
|
4023
|
+
// process before trusting it as a live sibling memory owner.
|
|
4024
|
+
// Falls back to true (trust the owner file) when the platform
|
|
4025
|
+
// check is unavailable or inconclusive.
|
|
4026
|
+
try {
|
|
4027
|
+
if (process.platform === 'win32') {
|
|
4028
|
+
const { execFileSync } = await import('node:child_process')
|
|
4029
|
+
const out = execFileSync('tasklist', ['/FI', `PID eq ${ownerPid}`, '/FO', 'CSV', '/NH'], { encoding: 'utf8', windowsHide: true })
|
|
4030
|
+
if (!String(out || '').toLowerCase().includes('node')) return false
|
|
4031
|
+
} else if (process.platform === 'linux') {
|
|
4032
|
+
const comm = fs.readFileSync(`/proc/${ownerPid}/comm`, 'utf8').trim()
|
|
4033
|
+
if (!comm.includes('node')) return false
|
|
4034
|
+
}
|
|
4035
|
+
} catch { /* inconclusive — trust the owner file (already alive+kind-matched) */ }
|
|
4036
|
+
return true
|
|
4037
|
+
} catch { return false }
|
|
4038
|
+
})()
|
|
4039
|
+
if (anotherOwnerAlive) {
|
|
4040
|
+
__mixdogMemoryLog('[memory-service] shutdown: another live memory owner holds memory-runtime-owner.json — leaving PG running\n')
|
|
4041
|
+
} else {
|
|
4042
|
+
const { stopPgForShutdown } = await import('./lib/pg/supervisor.mjs')
|
|
4043
|
+
await stopPgForShutdown()
|
|
4044
|
+
}
|
|
3925
4045
|
} catch {}
|
|
3926
4046
|
} else {
|
|
3927
4047
|
__mixdogMemoryLog('[memory-service] secondary mode; leaving shared PG running\n')
|