mixdog 0.9.96 → 0.9.97
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 +0 -17
- package/package.json +8 -7
- package/scripts/build-tui.mjs +6 -6
- package/scripts/{channel-daemon-stub.mjs → daemon-stub.mjs} +18 -12
- package/src/defaults/skills/setup/SKILL.md +1 -1
- package/src/lib/keychain-cjs.cjs +1 -1
- package/src/lib/mixdog-debug.cjs +2 -3
- package/src/rules/lead/01-general.md +1 -2
- package/src/rules/lead/lead-tool.md +0 -3
- package/src/rules/shared/01-tool.md +11 -10
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -13
- package/src/runtime/agent/orchestrator/mcp/client.mjs +81 -33
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +3 -6
- package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +8 -6
- package/src/runtime/agent/orchestrator/session/compact.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +22 -4
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +20 -2
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +4 -3
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +40 -25
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -3
- package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +12 -6
- package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +38 -2
- package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +10 -3
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +4 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +59 -42
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +3 -2
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +12 -22
- package/src/runtime/agent/orchestrator/tools/builtin/binary-file.mjs +92 -19
- package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +2 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +80 -30
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +41 -22
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +6 -2
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +55 -11
- package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +12 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +55 -18
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -0
- package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +67 -21
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +7 -1
- package/src/runtime/channels/lib/config.mjs +33 -33
- package/src/runtime/channels/lib/inbound-handler.mjs +7 -7
- package/src/runtime/channels/lib/interaction-handlers.mjs +6 -6
- package/src/runtime/channels/lib/memory-client.mjs +31 -157
- package/src/runtime/channels/lib/output-forwarder.mjs +16 -16
- package/src/runtime/channels/lib/owned-runtime.mjs +60 -60
- package/src/runtime/channels/lib/owner-heartbeat.mjs +1 -1
- package/src/runtime/channels/lib/{backend-dispatch.mjs → provider-dispatch.mjs} +6 -6
- package/src/runtime/channels/lib/runtime-paths.mjs +4 -30
- package/src/runtime/channels/lib/status-snapshot.mjs +3 -3
- package/src/runtime/channels/lib/tool-dispatch.mjs +4 -4
- package/src/runtime/channels/lib/worker-bootstrap.mjs +1 -1
- package/src/runtime/channels/lib/worker-ipc.mjs +7 -7
- package/src/runtime/channels/lib/worker-main.mjs +38 -38
- package/src/runtime/channels/{backends → providers}/discord-access.mjs +1 -1
- package/src/runtime/channels/{backends → providers}/discord-gateway.mjs +1 -1
- package/src/runtime/channels/{backends → providers}/discord.mjs +3 -3
- package/src/runtime/channels/{backends → providers}/telegram.mjs +12 -12
- package/src/runtime/channels/tool-defs.mjs +1 -1
- package/src/runtime/memory/index.mjs +72 -211
- package/src/runtime/memory/lib/agent-ipc.mjs +9 -8
- package/src/runtime/memory/lib/cycle-llm-adapters.mjs +2 -2
- package/src/runtime/memory/lib/http-router.mjs +1 -6
- package/src/runtime/memory/lib/memory-config-flags.mjs +1 -1
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +4 -1
- package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +5 -0
- package/src/runtime/memory/lib/memory-log.mjs +1 -1
- package/src/runtime/memory/lib/memory-process-lock.mjs +5 -60
- package/src/runtime/memory/lib/memory.mjs +1 -1
- package/src/runtime/memory/lib/session-ingest.mjs +1 -1
- package/src/runtime/search/index.mjs +2 -2
- package/src/runtime/shared/abort-race.mjs +70 -0
- package/src/runtime/shared/child-spawn-gate.mjs +101 -63
- package/src/runtime/shared/config.mjs +11 -24
- package/src/runtime/shared/llm/index.mjs +1 -1
- package/src/runtime/shared/owner-fair-gate.mjs +134 -0
- package/src/runtime/shared/resource-admission.mjs +6 -5
- package/src/runtime/shared/tool-execution-owner.mjs +12 -0
- package/src/runtime/shared/tool-result-summary.mjs +1 -1
- package/src/runtime/shared/tool-surface.mjs +91 -22
- package/src/runtime/shared/tool-workload-gates.mjs +29 -0
- package/src/runtime/shared/turn-snapshot.mjs +17 -2
- package/src/session-runtime/channel-config-api.mjs +5 -5
- package/src/session-runtime/config-lifecycle.mjs +27 -27
- package/src/session-runtime/lifecycle-api.mjs +22 -3
- package/src/session-runtime/memory-runtime-prewarm.mjs +15 -0
- package/src/session-runtime/prewarm.mjs +1 -1
- package/src/session-runtime/provider-models.mjs +1 -1
- package/src/session-runtime/remote-control.mjs +6 -7
- package/src/session-runtime/runtime-core.mjs +52 -28
- package/src/session-runtime/self-update.mjs +1 -1
- package/src/session-runtime/session-lifecycle.mjs +36 -15
- package/src/session-runtime/session-title.mjs +1 -1
- package/src/session-runtime/session-turn-api.mjs +55 -15
- package/src/session-runtime/settings-api.mjs +7 -7
- package/src/standalone/agent-dispatch-broker.mjs +1 -1
- package/src/standalone/channel-admin.mjs +42 -53
- package/src/standalone/{channel-daemon-client.mjs → channel-client.mjs} +16 -18
- package/src/standalone/{channel-daemon-transport.mjs → channel-transport.mjs} +45 -46
- package/src/standalone/channel-worker.mjs +124 -647
- package/src/standalone/{backend-daemon.mjs → daemon.mjs} +187 -132
- package/src/standalone/memory-runtime-proxy.mjs +58 -1
- package/src/standalone/{engine-daemon-client.mjs → session-client.mjs} +151 -86
- package/src/standalone/session-protocol.mjs +8 -4
- package/src/standalone/session-runtime-pool.mjs +393 -0
- package/src/standalone/session-runtime-record.mjs +20 -0
- package/src/standalone/session-runtime-worker.mjs +203 -0
- package/src/standalone/{engine-daemon-service.mjs → session-service.mjs} +170 -157
- package/src/standalone/{engine-daemon-transport.mjs → session-transport.mjs} +139 -61
- package/src/standalone/session-wire.mjs +55 -0
- package/src/tui/App.jsx +4 -4
- package/src/tui/app/app-format.mjs +1 -1
- package/src/tui/app/channel-pickers.mjs +41 -41
- package/src/tui/app/input-parsers.mjs +2 -2
- package/src/tui/app/onboarding-steps.mjs +2 -2
- package/src/tui/app/project-picker.mjs +8 -8
- package/src/tui/app/prompt-submit.mjs +8 -8
- package/src/tui/app/settings-picker.mjs +25 -25
- package/src/tui/app/use-prompt-queue-history.mjs +1 -1
- package/src/tui/app/use-transcript-scroll.mjs +1 -1
- package/src/tui/components/ToolExecution.jsx +1 -1
- package/src/tui/components/prompt-input/restore-policy.mjs +1 -1
- package/src/tui/components/tool-execution/surface-detail.mjs +1 -1
- package/src/tui/dist/index.mjs +154 -127
- package/src/tui/hooks/useSession.mjs +16 -0
- package/src/tui/index.jsx +11 -11
- package/src/tui/{engine → session}/agent-envelope.mjs +3 -3
- package/src/tui/{engine → session}/agent-job-feed.mjs +4 -4
- package/src/tui/{engine → session}/boot-profile.mjs +2 -2
- package/src/tui/{engine → session}/context-state.mjs +3 -3
- package/src/tui/{engine → session}/labels.mjs +2 -2
- package/src/tui/{engine → session}/live-share.mjs +7 -7
- package/src/tui/{engine → session}/notice-text.mjs +2 -2
- package/src/tui/{engine → session}/notification-plan.mjs +3 -3
- package/src/tui/session/oauth-flows.mjs +140 -0
- package/src/tui/{engine → session}/prompt-history.mjs +3 -3
- package/src/tui/{engine → session}/queue-helpers.mjs +2 -2
- package/src/tui/{engine → session}/render-timing.mjs +2 -2
- package/src/tui/{engine → session}/session-api-ext.mjs +30 -17
- package/src/tui/{engine → session}/session-api.mjs +7 -60
- package/src/tui/{engine → session}/session-flow.mjs +1 -1
- package/src/tui/{engine → session}/session-stats.mjs +3 -3
- package/src/tui/{engine → session}/tool-approval.mjs +3 -3
- package/src/tui/{engine → session}/tool-call-fields.mjs +2 -2
- package/src/tui/{engine → session}/tool-card-results.mjs +4 -4
- package/src/tui/{engine → session}/tool-result-status.mjs +5 -5
- package/src/tui/{engine → session}/tool-result-text.mjs +3 -3
- package/src/tui/{engine → session}/transcript-spill.mjs +34 -34
- package/src/tui/{engine → session}/turn.mjs +29 -107
- package/src/tui/{engine-local-session.mjs → session-local.mjs} +59 -75
- package/src/tui/session.mjs +22 -0
- package/src/ui/statusline-segments.mjs +1 -1
- package/src/ui/statusline.mjs +1 -1
- package/vendor/ink/build/components/App.js +10 -1
- package/src/session-runtime/memory-daemon-probe.mjs +0 -61
- package/src/standalone/engine-daemon-protocol.mjs +0 -32
- package/src/tui/engine/turn-watchdog.mjs +0 -92
- package/src/tui/engine.mjs +0 -22
- package/src/tui/hooks/useEngine.mjs +0 -16
- /package/src/runtime/agent/orchestrator/session/compact/{engine.mjs → runner.mjs} +0 -0
- /package/src/runtime/channels/{backends → providers}/discord-attachments.mjs +0 -0
- /package/src/tui/{engine → session}/agent-response-tail.mjs +0 -0
- /package/src/tui/{engine → session}/frame-batched-store.mjs +0 -0
- /package/src/tui/{engine → session}/tui-steering-persist.mjs +0 -0
|
@@ -1,13 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env bun
|
|
2
2
|
import { __mixdogMemoryLog } from './lib/memory-log.mjs';
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
// V8 compile cache: the memory runtime is its own long-lived child process
|
|
8
|
-
// (spawned by the channel daemon / standalone proxy) and never passes through
|
|
9
|
-
// cli.mjs, so it pays full ESM parse cost on every boot without this. Same
|
|
10
|
-
// best-effort guard as cli.mjs.
|
|
4
|
+
// V8 compile cache: the memory runtime is a separate long-lived child in
|
|
5
|
+
// product mode. The standalone MCP entry uses the same module directly.
|
|
11
6
|
try {
|
|
12
7
|
const { enableCompileCache } = await import('node:module')
|
|
13
8
|
enableCompileCache?.()
|
|
@@ -26,8 +21,6 @@ import { readPluginVersion, readPromotionCodeFingerprint } from './lib/promotion
|
|
|
26
21
|
const PLUGIN_VERSION = readPluginVersion(PLUGIN_ROOT)
|
|
27
22
|
const BOOT_PROMOTION_CODE_FINGERPRINT = readPromotionCodeFingerprint(PLUGIN_ROOT)
|
|
28
23
|
|
|
29
|
-
try { os.setPriority(os.constants.priority.PRIORITY_BELOW_NORMAL) } catch {}
|
|
30
|
-
|
|
31
24
|
import { Server } from '@modelcontextprotocol/sdk/server/index.js'
|
|
32
25
|
import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
|
|
33
26
|
import { StreamableHTTPServerTransport } from '@modelcontextprotocol/sdk/server/streamableHttp.js'
|
|
@@ -104,8 +97,14 @@ import {
|
|
|
104
97
|
assertSecondaryPgAttachable as _assertSecondaryPgAttachable,
|
|
105
98
|
} from './lib/memory-config-flags.mjs'
|
|
106
99
|
const IS_MEMORY_ENTRY = !!process.argv[1] && import.meta.url === pathToFileURL(path.resolve(process.argv[1])).href
|
|
107
|
-
|
|
100
|
+
if (IS_MEMORY_ENTRY) {
|
|
101
|
+
process.removeAllListeners('warning')
|
|
102
|
+
process.on('warning', () => {})
|
|
103
|
+
try { os.setPriority(os.constants.priority.PRIORITY_BELOW_NORMAL) } catch {}
|
|
104
|
+
}
|
|
105
|
+
const USE_ARG_DATA_DIR = IS_MEMORY_ENTRY
|
|
108
106
|
const DATA_DIR = process.env.MIXDOG_DATA_DIR || (USE_ARG_DATA_DIR ? process.argv[2] : '') || resolvePluginData()
|
|
107
|
+
const INTEGRATED_DAEMON_HOST = process.env.MIXDOG_DAEMON_HOST === '1' && !IS_MEMORY_ENTRY
|
|
109
108
|
if (!DATA_DIR) {
|
|
110
109
|
__mixdogMemoryLog('[memory-service] memory data dir not set and no explicit data dir provided\n')
|
|
111
110
|
process.exit(1)
|
|
@@ -126,9 +125,6 @@ function memoryProfile(event, fields = {}) {
|
|
|
126
125
|
import {
|
|
127
126
|
parsePositivePid,
|
|
128
127
|
isPidAliveLocal,
|
|
129
|
-
tryAcquireMemoryOwnerLock as _tryAcquireMemoryOwnerLock,
|
|
130
|
-
releaseMemoryOwnerLock as _releaseMemoryOwnerLock,
|
|
131
|
-
killPreviousServer as _killPreviousServer,
|
|
132
128
|
acquireLock as _acquireLock,
|
|
133
129
|
releaseLock as _releaseLock,
|
|
134
130
|
} from './lib/memory-process-lock.mjs'
|
|
@@ -169,19 +165,6 @@ const _daemonLifecycle = createMemoryDaemonLifecycle({
|
|
|
169
165
|
const { touchDaemonIdleTimer, registerClient, deregisterClient } = _daemonLifecycle
|
|
170
166
|
|
|
171
167
|
const LOCK_FILE = path.join(DATA_DIR, '.memory-service.lock')
|
|
172
|
-
// Owner-election lock. Separate from LOCK_FILE so single-instance mode keeps
|
|
173
|
-
// its kill-the-previous protocol while multi-instance fork-proxy workers use
|
|
174
|
-
// atomic CAS for takeover. Created via fs.openSync(path,'wx') — node guarantees
|
|
175
|
-
// EEXIST when another process won the race.
|
|
176
|
-
const OWNER_LOCK_FILE = path.join(DATA_DIR, '.memory-owner.lock')
|
|
177
|
-
|
|
178
|
-
function tryAcquireMemoryOwnerLock() {
|
|
179
|
-
return _tryAcquireMemoryOwnerLock(OWNER_LOCK_FILE, __mixdogMemoryLog)
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
function releaseMemoryOwnerLock() {
|
|
183
|
-
return _releaseMemoryOwnerLock(OWNER_LOCK_FILE)
|
|
184
|
-
}
|
|
185
168
|
|
|
186
169
|
const BASE_PORT = 3350
|
|
187
170
|
const MAX_PORT = 3357
|
|
@@ -190,10 +173,6 @@ let _traceDb = null
|
|
|
190
173
|
|
|
191
174
|
const MEMORY_INSTRUCTIONS_TEXT = ''
|
|
192
175
|
|
|
193
|
-
function killPreviousServer(pid) {
|
|
194
|
-
return _killPreviousServer(pid, __mixdogMemoryLog)
|
|
195
|
-
}
|
|
196
|
-
|
|
197
176
|
function acquireLock() {
|
|
198
177
|
return _acquireLock(LOCK_FILE, __mixdogMemoryLog)
|
|
199
178
|
}
|
|
@@ -352,7 +331,7 @@ async function _initStore() {
|
|
|
352
331
|
} else {
|
|
353
332
|
__mixdogMemoryLog('[memory-service] secondary mode; skipping llm worker\n')
|
|
354
333
|
}
|
|
355
|
-
// Provider/session/agent modules live once in the
|
|
334
|
+
// Provider/session/agent modules live once in the daemon. Memory
|
|
356
335
|
// cycles cross the authenticated loopback broker instead of initializing a
|
|
357
336
|
// second registry in this process.
|
|
358
337
|
_bootTimestamp = Date.now()
|
|
@@ -587,31 +566,61 @@ export { TOOL_DEFS, handleToolCall, buildSessionCoreMemoryPayload }
|
|
|
587
566
|
export { MEMORY_INSTRUCTIONS_TEXT as instructions }
|
|
588
567
|
export { acquireLock, releaseLock }
|
|
589
568
|
export { cwdFromTranscriptPath }
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
569
|
+
|
|
570
|
+
/** In-process write surface used by the unified channel/session daemon. */
|
|
571
|
+
export async function appendEntry(data = {}) {
|
|
572
|
+
await init()
|
|
573
|
+
const role = String(data.role ?? 'user')
|
|
574
|
+
const content = String(data.content ?? '')
|
|
575
|
+
const cleaned = cleanMemoryText(content)
|
|
576
|
+
if (!cleaned || !cleaned.trim()) return { error: content ? 'empty after clean' : 'content required' }
|
|
577
|
+
const sourceRef = String(data.sourceRef ?? `manual:${Date.now()}-${process.pid}`)
|
|
578
|
+
const sessionId = data.sessionId ?? null
|
|
579
|
+
const tsMs = parseTsToMs(data.ts ?? Date.now())
|
|
580
|
+
const projectId = resolveProjectScope(typeof data.cwd === 'string' && data.cwd ? data.cwd : null)
|
|
581
|
+
const result = await db.query(`
|
|
582
|
+
INSERT INTO entries(ts, role, content, source_ref, session_id, project_id)
|
|
583
|
+
VALUES ($1, $2, $3, $4, $5, $6)
|
|
584
|
+
ON CONFLICT DO NOTHING
|
|
585
|
+
RETURNING id
|
|
586
|
+
`, [tsMs, role, cleaned, sourceRef, sessionId, projectId])
|
|
587
|
+
const insertedId = result.rows[0]?.id ?? null
|
|
588
|
+
return {
|
|
589
|
+
ok: true,
|
|
590
|
+
id: insertedId !== null ? Number(insertedId) : null,
|
|
591
|
+
changes: Number(result.rowCount ?? result.affectedRows ?? 0),
|
|
592
|
+
}
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
/** In-process transcript ingest surface used by the unified daemon. */
|
|
596
|
+
export async function ingestTranscript(filePath, { cwd } = {}) {
|
|
597
|
+
await init()
|
|
598
|
+
if (!filePath) return { error: 'filePath required' }
|
|
599
|
+
const ingested = await ingestTranscriptFile(filePath, { cwd })
|
|
600
|
+
return { ok: true, ingested }
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/** Direct trace sink for agent orchestration running in this daemon process. */
|
|
604
|
+
export async function recordTraceEvents(events = []) {
|
|
605
|
+
await init()
|
|
606
|
+
if (!Array.isArray(events)) throw new TypeError('events must be an array')
|
|
607
|
+
if (events.length > 500) throw new RangeError('too many events (max 500)')
|
|
608
|
+
if (!_traceDb) {
|
|
609
|
+
_traceDb = await openTraceDatabase(DATA_DIR)
|
|
610
|
+
registerTraceExitDrain(_traceDb)
|
|
611
|
+
}
|
|
612
|
+
enqueueTraceEvents(_traceDb, events)
|
|
613
|
+
void insertAgentCalls(_traceDb, events).catch((error) => {
|
|
614
|
+
__mixdogMemoryLog(`[trace] insertAgentCalls error: ${error?.message}\n`)
|
|
596
615
|
})
|
|
616
|
+
return { ok: true, queued: events.length }
|
|
597
617
|
}
|
|
598
618
|
export async function init() {
|
|
599
619
|
if (_initialized) return
|
|
600
620
|
__mixdogMemoryLog(`[boot-time] tag=memory-init-start tMs=${Date.now()}\n`)
|
|
601
|
-
if (process.env.MIXDOG_WORKER_MODE === '1' && process.send) {
|
|
602
|
-
// Single-worker daemon: acquire the owner lock (which reclaims a crashed
|
|
603
|
-
// predecessor's stale, dead-PID lock). If a LIVE peer still holds it — an
|
|
604
|
-
// anomaly, since server-main forks exactly one memory worker — exit so
|
|
605
|
-
// server-main respawns us instead of running a second owner.
|
|
606
|
-
if (!tryAcquireMemoryOwnerLock()) {
|
|
607
|
-
__mixdogMemoryLog('[memory-service] live peer holds owner lock — exiting for respawn\n')
|
|
608
|
-
process.exit(0)
|
|
609
|
-
}
|
|
610
|
-
process.on('exit', releaseMemoryOwnerLock)
|
|
611
|
-
}
|
|
612
621
|
const runtimeReady = _beginRuntimeInit()
|
|
613
622
|
let boundPort = null
|
|
614
|
-
if (!memorySecondaryMode()) {
|
|
623
|
+
if (!memorySecondaryMode() && !INTEGRATED_DAEMON_HOST) {
|
|
615
624
|
boundPort = await _startHttpServer()
|
|
616
625
|
advertiseMemoryPort(boundPort)
|
|
617
626
|
try {
|
|
@@ -630,11 +639,10 @@ export async function init() {
|
|
|
630
639
|
} else {
|
|
631
640
|
await runtimeReady
|
|
632
641
|
}
|
|
642
|
+
__mixdogMemoryLog(`[memory-service] init() complete (entries unified mode, version=${PLUGIN_VERSION})\n`)
|
|
633
643
|
if (process.env.MIXDOG_WORKER_MODE === '1' && process.send) {
|
|
634
|
-
|
|
635
|
-
sendMemoryWorkerMessage({ type: 'ready', port: boundPort })
|
|
644
|
+
safeIpcSend(process, { type: 'ready', port: boundPort })
|
|
636
645
|
}
|
|
637
|
-
__mixdogMemoryLog(`[memory-service] init() complete (entries unified mode, version=${PLUGIN_VERSION})\n`)
|
|
638
646
|
touchDaemonIdleTimer('init')
|
|
639
647
|
}
|
|
640
648
|
|
|
@@ -669,49 +677,11 @@ export async function stop() {
|
|
|
669
677
|
await closeDatabase(DATA_DIR)
|
|
670
678
|
// Stop the PG postmaster after the connection pools have been drained.
|
|
671
679
|
// closeDatabase() only ends the client pool; without this the child
|
|
672
|
-
// postmaster keeps running after the
|
|
680
|
+
// postmaster keeps running after the unified daemon exits.
|
|
673
681
|
if (!memorySecondaryMode()) {
|
|
674
682
|
try {
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
// (kind check — guards against a stale/foreign pid reusing this pid
|
|
678
|
-
// number for an unrelated process) and (b) that pid is alive AND not
|
|
679
|
-
// this process. Any read/parse failure or ambiguous state falls back
|
|
680
|
-
// to stopping PG (previous unconditional behavior) rather than
|
|
681
|
-
// risking an orphaned PG postmaster.
|
|
682
|
-
const anotherOwnerAlive = await (async () => {
|
|
683
|
-
try {
|
|
684
|
-
const { readSingletonOwner } = await import('../shared/singleton-owner.mjs')
|
|
685
|
-
const ownerPath = path.join(DATA_DIR, 'memory-runtime-owner.json')
|
|
686
|
-
const { owner, alive } = readSingletonOwner(ownerPath)
|
|
687
|
-
if (!alive) return false
|
|
688
|
-
if (owner?.kind !== 'memory-runtime-daemon') return false
|
|
689
|
-
const ownerPid = Number(owner?.pid)
|
|
690
|
-
if (!Number.isInteger(ownerPid) || ownerPid === process.pid) return false
|
|
691
|
-
// Best-effort process-name check (mirrors supervisor.mjs's
|
|
692
|
-
// isPostgresPid pattern) — confirms the pid is actually a node
|
|
693
|
-
// process before trusting it as a live sibling memory owner.
|
|
694
|
-
// Falls back to true (trust the owner file) when the platform
|
|
695
|
-
// check is unavailable or inconclusive.
|
|
696
|
-
try {
|
|
697
|
-
if (process.platform === 'win32') {
|
|
698
|
-
const { execFileSync } = await import('node:child_process')
|
|
699
|
-
const out = execFileSync('tasklist', ['/FI', `PID eq ${ownerPid}`, '/FO', 'CSV', '/NH'], { encoding: 'utf8', windowsHide: true })
|
|
700
|
-
if (!String(out || '').toLowerCase().includes('node')) return false
|
|
701
|
-
} else if (process.platform === 'linux') {
|
|
702
|
-
const comm = fs.readFileSync(`/proc/${ownerPid}/comm`, 'utf8').trim()
|
|
703
|
-
if (!comm.includes('node')) return false
|
|
704
|
-
}
|
|
705
|
-
} catch { /* inconclusive — trust the owner file (already alive+kind-matched) */ }
|
|
706
|
-
return true
|
|
707
|
-
} catch { return false }
|
|
708
|
-
})()
|
|
709
|
-
if (anotherOwnerAlive) {
|
|
710
|
-
__mixdogMemoryLog('[memory-service] shutdown: another live memory owner holds memory-runtime-owner.json — leaving PG running\n')
|
|
711
|
-
} else {
|
|
712
|
-
const { stopPgForShutdown } = await import('./lib/pg/supervisor.mjs')
|
|
713
|
-
await stopPgForShutdown()
|
|
714
|
-
}
|
|
683
|
+
const { stopPgForShutdown } = await import('./lib/pg/supervisor.mjs')
|
|
684
|
+
await stopPgForShutdown()
|
|
715
685
|
} catch {}
|
|
716
686
|
} else {
|
|
717
687
|
__mixdogMemoryLog('[memory-service] secondary mode; leaving shared PG running\n')
|
|
@@ -778,120 +748,9 @@ function _startHttpServer() {
|
|
|
778
748
|
return _httpReadyPromise
|
|
779
749
|
}
|
|
780
750
|
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
// graceful shutdown and leaving pgdata in an inconsistent checkpoint state.
|
|
785
|
-
let _stopInFlight = false
|
|
786
|
-
let _syncPgStopRequested = false
|
|
787
|
-
const _workerSignalHandler = (sig) => {
|
|
788
|
-
if (_stopInFlight) {
|
|
789
|
-
__mixdogMemoryLog(`[memory-worker] ${sig} — stop already in flight, ignoring\n`)
|
|
790
|
-
return
|
|
791
|
-
}
|
|
792
|
-
_stopInFlight = true
|
|
793
|
-
_syncPgStopRequested = true
|
|
794
|
-
__mixdogMemoryLog(`[memory-worker] received ${sig} — calling stop() for clean shutdown\n`)
|
|
795
|
-
const _exitTimer = setTimeout(() => {
|
|
796
|
-
__mixdogMemoryLog(`[memory-worker] stop() timed out after 6000ms — forcing exit(2)\n`)
|
|
797
|
-
process.exit(2)
|
|
798
|
-
}, 6000)
|
|
799
|
-
stop().then(() => {
|
|
800
|
-
clearTimeout(_exitTimer)
|
|
801
|
-
__mixdogMemoryLog(`[memory-worker] stop() complete — exiting cleanly\n`)
|
|
802
|
-
process.exit(0)
|
|
803
|
-
}).catch((e) => {
|
|
804
|
-
clearTimeout(_exitTimer)
|
|
805
|
-
__mixdogMemoryLog(`[memory-worker] stop() error on ${sig}: ${e && (e.message || e)}\n`)
|
|
806
|
-
process.exit(1)
|
|
807
|
-
})
|
|
808
|
-
}
|
|
809
|
-
process.on('SIGTERM', () => _workerSignalHandler('SIGTERM'))
|
|
810
|
-
process.on('SIGINT', () => _workerSignalHandler('SIGINT'))
|
|
811
|
-
|
|
812
|
-
// Windows-safe last resort: SIGTERM may TerminateProcess before the async
|
|
813
|
-
// stop() path runs, orphaning PG mid-write. Best-effort sync pg_ctl stop on
|
|
814
|
-
// exit (no-op after a completed graceful stop). Skip in secondary mode — we
|
|
815
|
-
// do not own the shared PG there.
|
|
816
|
-
if (!memorySecondaryMode()) process.on('exit', () => {
|
|
817
|
-
// Do not stop shared PG on an unexpected Memory crash. The singleton proxy
|
|
818
|
-
// will respawn a daemon that can attach to the still-healthy cluster. The
|
|
819
|
-
// sync stop is only a last resort after an explicit shutdown signal began.
|
|
820
|
-
if (_syncPgStopRequested) {
|
|
821
|
-
try { stopPgForShutdownSync() } catch {}
|
|
822
|
-
}
|
|
823
|
-
})
|
|
824
|
-
|
|
825
|
-
// callId → AbortController for in-flight IPC calls (cancel handler uses this).
|
|
826
|
-
const _inFlightCalls = new Map()
|
|
827
|
-
|
|
828
|
-
process.on('message', async (msg) => {
|
|
829
|
-
// Handle parent-initiated graceful shutdown IPC message.
|
|
830
|
-
if (msg.type === 'shutdown') {
|
|
831
|
-
__mixdogMemoryLog('[memory-worker] received IPC shutdown — calling stop()\n')
|
|
832
|
-
_workerSignalHandler('IPC:shutdown')
|
|
833
|
-
return
|
|
834
|
-
}
|
|
835
|
-
if (msg.type === 'cancel' && msg.callId) {
|
|
836
|
-
const entry = _inFlightCalls.get(msg.callId)
|
|
837
|
-
if (entry) {
|
|
838
|
-
// Mark cancelled so the in-flight call's result/error branch below
|
|
839
|
-
// does not double-respond after the AbortController fires.
|
|
840
|
-
entry.cancelled = true
|
|
841
|
-
entry.ac.abort()
|
|
842
|
-
_inFlightCalls.delete(msg.callId)
|
|
843
|
-
sendMemoryWorkerMessage({ type: 'result', callId: msg.callId, error: 'cancelled' })
|
|
844
|
-
}
|
|
845
|
-
return
|
|
846
|
-
}
|
|
847
|
-
if (msg.type !== 'call' || !msg.callId) return
|
|
848
|
-
const entry = { ac: new AbortController(), cancelled: false }
|
|
849
|
-
_inFlightCalls.set(msg.callId, entry)
|
|
850
|
-
try {
|
|
851
|
-
let result
|
|
852
|
-
try {
|
|
853
|
-
result = await handleToolCall(msg.name, msg.args || {}, entry.ac.signal)
|
|
854
|
-
} finally {
|
|
855
|
-
_inFlightCalls.delete(msg.callId)
|
|
856
|
-
}
|
|
857
|
-
if (!entry.cancelled) sendMemoryWorkerMessage({ type: 'result', callId: msg.callId, result })
|
|
858
|
-
} catch (e) {
|
|
859
|
-
if (!entry.cancelled) sendMemoryWorkerMessage({ type: 'result', callId: msg.callId, error: e.message })
|
|
860
|
-
}
|
|
861
|
-
})
|
|
862
|
-
init().catch(e => {
|
|
863
|
-
let detail
|
|
864
|
-
try {
|
|
865
|
-
const parts = []
|
|
866
|
-
if (e?.name) parts.push(`name=${e.name}`)
|
|
867
|
-
if (e?.code) parts.push(`code=${e.code}`)
|
|
868
|
-
if (e?.errno) parts.push(`errno=${e.errno}`)
|
|
869
|
-
if (e?.syscall) parts.push(`syscall=${e.syscall}`)
|
|
870
|
-
if (e?.path) parts.push(`path=${e.path}`)
|
|
871
|
-
if (e?.message) parts.push(`message=${e.message}`)
|
|
872
|
-
let stringified = null
|
|
873
|
-
try { stringified = JSON.stringify(e, Object.getOwnPropertyNames(e || {})) } catch {}
|
|
874
|
-
if (stringified && stringified !== '{}' && stringified !== '"{}"') parts.push(`json=${stringified}`)
|
|
875
|
-
if (e?.stack) parts.push(`\nstack=\n${e.stack}`)
|
|
876
|
-
if (parts.length === 0) parts.push(`raw=${typeof e}:${String(e)}`)
|
|
877
|
-
detail = parts.join(' | ')
|
|
878
|
-
} catch (logErr) {
|
|
879
|
-
detail = `(error formatting failed: ${logErr?.message}) raw=${String(e)}`
|
|
880
|
-
}
|
|
881
|
-
__mixdogMemoryLog(`[memory-worker] init failed: ${detail}\n`)
|
|
882
|
-
// Signal degraded state to parent before exiting so it records the failure
|
|
883
|
-
// rather than treating this as a normal pre-ready crash.
|
|
884
|
-
sendMemoryWorkerMessage({ type: 'ready', degraded: true, error: detail.slice(0, 800) })
|
|
885
|
-
process.exit(1)
|
|
886
|
-
})
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
// Standalone MCP launcher path. When this module is the entry script AND no
|
|
890
|
-
// MIXDOG_WORKER_MODE flag is set, we own stdio and bring up the full MCP
|
|
891
|
-
// server with acquireLock + StdioServerTransport. Server-main spawnWorker
|
|
892
|
-
// also forks this file with MIXDOG_WORKER_MODE='1'; that path uses the IPC
|
|
893
|
-
// handler block above and acquireLock/init() as the single memory owner.
|
|
894
|
-
if (IS_MEMORY_ENTRY && process.env.MIXDOG_WORKER_MODE !== '1') {
|
|
751
|
+
// Standalone MCP launcher path. Product mode imports this module into the
|
|
752
|
+
// unified daemon; running the module directly owns stdio for MCP clients.
|
|
753
|
+
if (IS_MEMORY_ENTRY) {
|
|
895
754
|
;(async () => {
|
|
896
755
|
acquireLock()
|
|
897
756
|
process.on('exit', releaseLock)
|
|
@@ -908,10 +767,12 @@ if (IS_MEMORY_ENTRY && process.env.MIXDOG_WORKER_MODE !== '1') {
|
|
|
908
767
|
process.on('SIGINT', stopFromSignal)
|
|
909
768
|
process.on('SIGTERM', stopFromSignal)
|
|
910
769
|
await init()
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
770
|
+
if (!(process.env.MIXDOG_WORKER_MODE === '1' && process.send)) {
|
|
771
|
+
const transport = new StdioServerTransport()
|
|
772
|
+
await mcp.connect(transport)
|
|
773
|
+
await new Promise((resolve) => { mcp.onclose = resolve })
|
|
774
|
+
await stop()
|
|
775
|
+
}
|
|
915
776
|
})().catch((err) => {
|
|
916
777
|
__mixdogMemoryLog(`[memory-service] startup failed: ${err.stack || err.message}\n`)
|
|
917
778
|
process.exit(1)
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* Agent-dispatch client for the memory process.
|
|
3
3
|
*
|
|
4
4
|
* PG/embedding/recall remain isolated in the memory process, while the heavy
|
|
5
|
-
* provider/session graph lives exactly once in the machine-global
|
|
5
|
+
* provider/session graph lives exactly once in the machine-global daemon.
|
|
6
6
|
* daemon. Calls use its authenticated loopback broker, so the memory process
|
|
7
|
-
* can outlive its original fork parent and reconnect after a
|
|
7
|
+
* can outlive its original fork parent and reconnect after a daemon restart.
|
|
8
8
|
*/
|
|
9
9
|
import http from 'node:http'
|
|
10
10
|
import { readFileSync } from 'node:fs'
|
|
@@ -37,12 +37,13 @@ function isPidAlive(value) {
|
|
|
37
37
|
|
|
38
38
|
function readBrokerDiscovery() {
|
|
39
39
|
try {
|
|
40
|
-
const raw = JSON.parse(readFileSync(join(runtimeRoot(), '
|
|
41
|
-
const
|
|
42
|
-
|
|
40
|
+
const raw = JSON.parse(readFileSync(join(runtimeRoot(), 'daemon.json'), 'utf8'))
|
|
41
|
+
const endpoint = raw?.endpoints?.channel
|
|
42
|
+
const port = Number(endpoint?.port)
|
|
43
|
+
if (!Number.isInteger(port) || port <= 0 || port >= 65536 || !endpoint?.token || !isPidAlive(raw?.pid)) {
|
|
43
44
|
return null
|
|
44
45
|
}
|
|
45
|
-
return { port, token: String(
|
|
46
|
+
return { port, token: String(endpoint.token) }
|
|
46
47
|
} catch {
|
|
47
48
|
return null
|
|
48
49
|
}
|
|
@@ -129,7 +130,7 @@ function cancelBrokerCall(discovery, callId, reason) {
|
|
|
129
130
|
}
|
|
130
131
|
|
|
131
132
|
/**
|
|
132
|
-
* Dispatch through the singleton
|
|
133
|
+
* Dispatch through the singleton daemon provider/session graph.
|
|
133
134
|
*
|
|
134
135
|
* @param {object} opts agent-dispatch construction options
|
|
135
136
|
* @param {string} [opts.agent]
|
|
@@ -144,7 +145,7 @@ function cancelBrokerCall(discovery, callId, reason) {
|
|
|
144
145
|
export async function callAgentDispatch(opts = {}, prompt) {
|
|
145
146
|
const discovery = readBrokerDiscovery()
|
|
146
147
|
if (!discovery) {
|
|
147
|
-
throw new Error('agent-broker:
|
|
148
|
+
throw new Error('agent-broker: daemon unavailable')
|
|
148
149
|
}
|
|
149
150
|
const callId = nextCallId()
|
|
150
151
|
const timeoutMs = Math.max(1000, Number(opts.timeout ?? 600000))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Cycle LLM dispatch adapters, extracted from index.mjs (pass 3).
|
|
2
2
|
//
|
|
3
|
-
// Each cycleN scheduler/handler routes its LLM work through the
|
|
3
|
+
// Each cycleN scheduler/handler routes its LLM work through the provider's
|
|
4
4
|
// process-singleton agent broker. The memory process owns PG/embeddings only;
|
|
5
5
|
// provider/session modules are not duplicated here. The factory fixes the
|
|
6
6
|
// cycle role/taskType and reshapes to the cycle call signature:
|
|
@@ -24,7 +24,7 @@ export function createCycleLlmAdapters({ callAgentDispatch }) {
|
|
|
24
24
|
// treat these as FACTORIES: call once to obtain the callLlm adapter.
|
|
25
25
|
// Returning the adapter directly here made `getCycle1CallLlm()` execute an
|
|
26
26
|
// LLM dispatch with no arguments — empty prompt → agent-dispatch throw →
|
|
27
|
-
// unhandled rejection that killed the memory
|
|
27
|
+
// unhandled rejection that killed the memory runtime. Keep the factory
|
|
28
28
|
// contract: each getter returns the (memoized) adapter function.
|
|
29
29
|
const cycle1 = buildAdapter('cycle1-agent')
|
|
30
30
|
const cycle2 = buildAdapter('cycle2-agent')
|
|
@@ -121,10 +121,7 @@ export function createHttpRouter({
|
|
|
121
121
|
const _ownerInFlightHttpCalls = new Map()
|
|
122
122
|
|
|
123
123
|
const requestHandler = async (req, res) => {
|
|
124
|
-
touchDaemonIdleTimer(`${req.method || 'HTTP'} ${req.url || '/'}`)
|
|
125
|
-
// Connected-client lifecycle. Proxies register on first use and
|
|
126
|
-
// deregister on CLI shutdown; the daemon reaps itself shortly after the
|
|
127
|
-
// last client leaves (see registerClient/deregisterClient in index.mjs).
|
|
124
|
+
touchDaemonIdleTimer?.(`${req.method || 'HTTP'} ${req.url || '/'}`)
|
|
128
125
|
if (req.method === 'POST' && (req.url === '/client/register' || req.url === '/client/deregister')) {
|
|
129
126
|
if (!isLocalOrigin(req)) {
|
|
130
127
|
sendJson(res, { ok: false, error: 'forbidden: cross-origin' }, 403)
|
|
@@ -136,8 +133,6 @@ export function createHttpRouter({
|
|
|
136
133
|
if (req.url === '/client/register') {
|
|
137
134
|
const accepted = registerClient?.(clientPid)
|
|
138
135
|
if (accepted === false) {
|
|
139
|
-
// Daemon is draining — distinct signal so the proxy respawns a fresh
|
|
140
|
-
// daemon and retries its pending RPC instead of binding to this one.
|
|
141
136
|
sendJson(res, { ok: false, draining: true, error: 'memory worker draining' }, 503)
|
|
142
137
|
return
|
|
143
138
|
}
|
|
@@ -14,7 +14,7 @@ export function readMainConfig() {
|
|
|
14
14
|
|
|
15
15
|
// Recap toggle lives in the `agent` config section (agent.recap.enabled,
|
|
16
16
|
// default true) — the same file the session runtime persists via
|
|
17
|
-
// saveConfigAndAdopt. The memory
|
|
17
|
+
// saveConfigAndAdopt. The memory runtime is hosted by the unified daemon
|
|
18
18
|
// worker, so it re-reads this section from disk each cycle tick (poll-on-use)
|
|
19
19
|
// rather than relying on IPC.
|
|
20
20
|
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// memory-cycle2.mjs). Logging shim, abort check, and resource-dir resolution.
|
|
3
3
|
// No cycle2 business logic; safe to import from any cycle2 sub-module.
|
|
4
4
|
import { fileURLToPath } from 'url'
|
|
5
|
+
import { join } from 'path'
|
|
5
6
|
|
|
6
7
|
import { __mixdogMemoryLog } from './memory-log.mjs'
|
|
7
8
|
export { __mixdogMemoryLog }
|
|
@@ -11,5 +12,7 @@ export function throwIfAborted(signal) {
|
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
export function resourceDir() {
|
|
14
|
-
return process.env.MIXDOG_ROOT
|
|
15
|
+
return process.env.MIXDOG_ROOT
|
|
16
|
+
? join(process.env.MIXDOG_ROOT, 'src')
|
|
17
|
+
: fileURLToPath(new URL('../../..', import.meta.url))
|
|
15
18
|
}
|
|
@@ -24,6 +24,11 @@ export function createMemoryDaemonLifecycle({
|
|
|
24
24
|
_idleShutdownTimer = null
|
|
25
25
|
}
|
|
26
26
|
_idleShutdownTimer = setTimeout(() => {
|
|
27
|
+
pruneDeadClients()
|
|
28
|
+
if (_connectedClients.size > 0) {
|
|
29
|
+
touchDaemonIdleTimer('live client retained')
|
|
30
|
+
return
|
|
31
|
+
}
|
|
27
32
|
log(`[memory-service] daemon idle TTL elapsed after ${reason}; shutting down\n`)
|
|
28
33
|
stop()
|
|
29
34
|
.then(() => process.exit(0))
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Shared memory-
|
|
1
|
+
// Shared memory-runtime stderr logger. The write is captured at import time so
|
|
2
2
|
// memory output stays visible even when a TUI log guard swaps
|
|
3
3
|
// process.stderr.write later; MIXDOG_QUIET_MEMORY_LOG mutes it entirely.
|
|
4
4
|
const __mixdogMemoryStderrWrite = process.stderr.write.bind(process.stderr);
|
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// ownership of DATA_DIR-derived constants and the daemon logger.
|
|
1
|
+
// Process lock for the standalone memory MCP entry. Product mode is hosted
|
|
2
|
+
// A direct entry (including the isolated daemon child) owns this lock.
|
|
4
3
|
import fs from 'node:fs'
|
|
5
4
|
import { execFileSync } from 'node:child_process'
|
|
6
5
|
|
|
@@ -15,55 +14,7 @@ export function isPidAliveLocal(pid) {
|
|
|
15
14
|
catch (e) { return e.code !== 'ESRCH' }
|
|
16
15
|
}
|
|
17
16
|
|
|
18
|
-
|
|
19
|
-
// Returns true on success (this process now owns memory worker for the data
|
|
20
|
-
// dir), false when a live peer holds the lock. Stale locks (dead PID) are
|
|
21
|
-
// unlinked and retried atomically. Throws on unexpected fs errors so callers
|
|
22
|
-
// surface lock-system corruption rather than silently downgrading.
|
|
23
|
-
//
|
|
24
|
-
// EPERM/EBUSY/EACCES at openSync are transient — AV scanners (SignKorea /
|
|
25
|
-
// SKCert / ezPDFWS etc) briefly lock newly-created files during inspection.
|
|
26
|
-
// The 0.1.x baseline threw immediately and the worker promoted to
|
|
27
|
-
// permanentlyDegraded, killing memory tools for the rest of the session.
|
|
28
|
-
// Treat the AV error codes as retryable with bounded backoff (~750ms total)
|
|
29
|
-
// before giving up and rethrowing.
|
|
30
|
-
for (let attempt = 0; attempt < 5; attempt++) {
|
|
31
|
-
try {
|
|
32
|
-
const fd = fs.openSync(ownerLockFile, 'wx')
|
|
33
|
-
fs.writeSync(fd, String(process.pid))
|
|
34
|
-
fs.closeSync(fd)
|
|
35
|
-
return true
|
|
36
|
-
} catch (e) {
|
|
37
|
-
if (e.code === 'EEXIST') {
|
|
38
|
-
let ownerPid = NaN
|
|
39
|
-
try { ownerPid = Number(fs.readFileSync(ownerLockFile, 'utf8').trim()) } catch {}
|
|
40
|
-
if (isPidAliveLocal(ownerPid)) return false
|
|
41
|
-
// Stale lock: dead owner — unlink and retry exclusive create.
|
|
42
|
-
try { fs.unlinkSync(ownerLockFile) } catch {}
|
|
43
|
-
continue
|
|
44
|
-
}
|
|
45
|
-
const transient = e.code === 'EPERM' || e.code === 'EBUSY' || e.code === 'EACCES'
|
|
46
|
-
if (transient && attempt < 4) {
|
|
47
|
-
// Sync busy-wait acceptable here: this runs on memory worker boot
|
|
48
|
-
// path, once per process; the parent handler is not blocked.
|
|
49
|
-
const end = Date.now() + 50 * (attempt + 1)
|
|
50
|
-
while (Date.now() < end) {}
|
|
51
|
-
continue
|
|
52
|
-
}
|
|
53
|
-
throw e
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
return false
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export function releaseMemoryOwnerLock(ownerLockFile) {
|
|
60
|
-
try {
|
|
61
|
-
const ownerPid = Number(fs.readFileSync(ownerLockFile, 'utf8').trim())
|
|
62
|
-
if (ownerPid === process.pid) fs.unlinkSync(ownerLockFile)
|
|
63
|
-
} catch {}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
export function killPreviousServer(pid, log = () => {}) {
|
|
17
|
+
function killPreviousServer(pid, log = () => {}) {
|
|
67
18
|
if (pid <= 0 || pid === process.pid) return false
|
|
68
19
|
if (process.platform === 'win32') {
|
|
69
20
|
try {
|
|
@@ -117,14 +68,8 @@ export function killPreviousServer(pid, log = () => {}) {
|
|
|
117
68
|
}
|
|
118
69
|
|
|
119
70
|
export function acquireLock(lockFile, log = () => {}) {
|
|
120
|
-
// Multi-instance
|
|
121
|
-
//
|
|
122
|
-
// would taskkill /F that healthy peer mid-flight, then this fork-proxy
|
|
123
|
-
// mode wouldn't even need a lock anyway. Skip the entire kill-the-previous
|
|
124
|
-
// protocol; fork-proxy detection in init() takes priority. If neither
|
|
125
|
-
// proxy nor lock-owner path applies (race window during simultaneous
|
|
126
|
-
// boot), the worker simply continues without the lock — server-main /
|
|
127
|
-
// PG / port-listen handle the actual conflict cases.
|
|
71
|
+
// Multi-instance mode is coordinated by the unified daemon, so a direct MCP
|
|
72
|
+
// entry does not evict an already-running peer from another client.
|
|
128
73
|
if (process.env.MIXDOG_MULTI_INSTANCE === '1') return
|
|
129
74
|
try {
|
|
130
75
|
if (fs.existsSync(lockFile)) {
|
|
@@ -417,7 +417,7 @@ async function _migrateRecallIndexesIfStale(db) {
|
|
|
417
417
|
|
|
418
418
|
export async function ensureCurrentSchemaExtensions(db, dims) {
|
|
419
419
|
// One-time cleanup: attachment-only placeholder rows ('(attachment)' user
|
|
420
|
-
// content, e.g. Discord
|
|
420
|
+
// content, e.g. Discord provider discord.mjs:724) predate the
|
|
421
421
|
// shouldExcludeIngestMessage() ingest-time filter (session-ingest.mjs).
|
|
422
422
|
// Delete any already-persisted rows so they stop polluting recall/cycle1.
|
|
423
423
|
// Idempotent (no-op once cleaned); best-effort so a failure never blocks boot.
|
|
@@ -305,7 +305,7 @@ export function shouldExcludeIngestMessage(m) {
|
|
|
305
305
|
if (role === 'user' && typeof raw === 'string' && /^Reference files:\s*/i.test(raw.trimStart())) {
|
|
306
306
|
return true
|
|
307
307
|
}
|
|
308
|
-
// Attachment-only placeholder rows (e.g. Discord
|
|
308
|
+
// Attachment-only placeholder rows (e.g. Discord provider discord.mjs:724
|
|
309
309
|
// `"(attachment)"` fallback when a message carries no text, only files).
|
|
310
310
|
// Carries zero retrievable content — excluded so recall isn't polluted
|
|
311
311
|
// with bare "(attachment)" memory rows.
|
|
@@ -286,9 +286,9 @@ function normalizeNativeSearchPayload(result, args, startedAt) {
|
|
|
286
286
|
|
|
287
287
|
async function _searchCore(args, { cacheState, nativeSearch, signal }) {
|
|
288
288
|
const cacheArgs = searchArgsForCacheKey(args)
|
|
289
|
-
const
|
|
289
|
+
const provider = 'native-web-search'
|
|
290
290
|
const searchCacheKey = buildCacheKey('search', {
|
|
291
|
-
provider
|
|
291
|
+
provider,
|
|
292
292
|
...cacheArgs,
|
|
293
293
|
})
|
|
294
294
|
const cachedSearch = getCachedEntry(cacheState, searchCacheKey)
|