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.
Files changed (170) hide show
  1. package/README.md +0 -17
  2. package/package.json +8 -7
  3. package/scripts/build-tui.mjs +6 -6
  4. package/scripts/{channel-daemon-stub.mjs → daemon-stub.mjs} +18 -12
  5. package/src/defaults/skills/setup/SKILL.md +1 -1
  6. package/src/lib/keychain-cjs.cjs +1 -1
  7. package/src/lib/mixdog-debug.cjs +2 -3
  8. package/src/rules/lead/01-general.md +1 -2
  9. package/src/rules/lead/lead-tool.md +0 -3
  10. package/src/rules/shared/01-tool.md +11 -10
  11. package/src/runtime/agent/orchestrator/agent-trace-io.mjs +4 -13
  12. package/src/runtime/agent/orchestrator/mcp/client.mjs +81 -33
  13. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +3 -6
  14. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -2
  15. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +8 -6
  16. package/src/runtime/agent/orchestrator/session/compact.mjs +1 -1
  17. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +22 -4
  18. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +20 -2
  19. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +4 -3
  20. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +40 -25
  21. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +2 -3
  22. package/src/runtime/agent/orchestrator/session/manager/prefetch-bridge.mjs +12 -6
  23. package/src/runtime/agent/orchestrator/session/manager/session-lock.mjs +38 -2
  24. package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +10 -3
  25. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +4 -0
  26. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +59 -42
  27. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +3 -2
  28. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +12 -22
  29. package/src/runtime/agent/orchestrator/tools/builtin/binary-file.mjs +92 -19
  30. package/src/runtime/agent/orchestrator/tools/builtin/fs-reachability.mjs +2 -2
  31. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +2 -0
  32. package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +4 -4
  33. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +80 -30
  34. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +41 -22
  35. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +6 -2
  36. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +55 -11
  37. package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +12 -1
  38. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +55 -18
  39. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -0
  40. package/src/runtime/agent/orchestrator/tools/env-scrub.mjs +1 -2
  41. package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +2 -2
  42. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +1 -1
  43. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +67 -21
  44. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +2 -1
  45. package/src/runtime/agent/orchestrator/tools/patch.mjs +1 -1
  46. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +7 -1
  47. package/src/runtime/channels/lib/config.mjs +33 -33
  48. package/src/runtime/channels/lib/inbound-handler.mjs +7 -7
  49. package/src/runtime/channels/lib/interaction-handlers.mjs +6 -6
  50. package/src/runtime/channels/lib/memory-client.mjs +31 -157
  51. package/src/runtime/channels/lib/output-forwarder.mjs +16 -16
  52. package/src/runtime/channels/lib/owned-runtime.mjs +60 -60
  53. package/src/runtime/channels/lib/owner-heartbeat.mjs +1 -1
  54. package/src/runtime/channels/lib/{backend-dispatch.mjs → provider-dispatch.mjs} +6 -6
  55. package/src/runtime/channels/lib/runtime-paths.mjs +4 -30
  56. package/src/runtime/channels/lib/status-snapshot.mjs +3 -3
  57. package/src/runtime/channels/lib/tool-dispatch.mjs +4 -4
  58. package/src/runtime/channels/lib/worker-bootstrap.mjs +1 -1
  59. package/src/runtime/channels/lib/worker-ipc.mjs +7 -7
  60. package/src/runtime/channels/lib/worker-main.mjs +38 -38
  61. package/src/runtime/channels/{backends → providers}/discord-access.mjs +1 -1
  62. package/src/runtime/channels/{backends → providers}/discord-gateway.mjs +1 -1
  63. package/src/runtime/channels/{backends → providers}/discord.mjs +3 -3
  64. package/src/runtime/channels/{backends → providers}/telegram.mjs +12 -12
  65. package/src/runtime/channels/tool-defs.mjs +1 -1
  66. package/src/runtime/memory/index.mjs +72 -211
  67. package/src/runtime/memory/lib/agent-ipc.mjs +9 -8
  68. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +2 -2
  69. package/src/runtime/memory/lib/http-router.mjs +1 -6
  70. package/src/runtime/memory/lib/memory-config-flags.mjs +1 -1
  71. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +4 -1
  72. package/src/runtime/memory/lib/memory-daemon-lifecycle.mjs +5 -0
  73. package/src/runtime/memory/lib/memory-log.mjs +1 -1
  74. package/src/runtime/memory/lib/memory-process-lock.mjs +5 -60
  75. package/src/runtime/memory/lib/memory.mjs +1 -1
  76. package/src/runtime/memory/lib/session-ingest.mjs +1 -1
  77. package/src/runtime/search/index.mjs +2 -2
  78. package/src/runtime/shared/abort-race.mjs +70 -0
  79. package/src/runtime/shared/child-spawn-gate.mjs +101 -63
  80. package/src/runtime/shared/config.mjs +11 -24
  81. package/src/runtime/shared/llm/index.mjs +1 -1
  82. package/src/runtime/shared/owner-fair-gate.mjs +134 -0
  83. package/src/runtime/shared/resource-admission.mjs +6 -5
  84. package/src/runtime/shared/tool-execution-owner.mjs +12 -0
  85. package/src/runtime/shared/tool-result-summary.mjs +1 -1
  86. package/src/runtime/shared/tool-surface.mjs +91 -22
  87. package/src/runtime/shared/tool-workload-gates.mjs +29 -0
  88. package/src/runtime/shared/turn-snapshot.mjs +17 -2
  89. package/src/session-runtime/channel-config-api.mjs +5 -5
  90. package/src/session-runtime/config-lifecycle.mjs +27 -27
  91. package/src/session-runtime/lifecycle-api.mjs +22 -3
  92. package/src/session-runtime/memory-runtime-prewarm.mjs +15 -0
  93. package/src/session-runtime/prewarm.mjs +1 -1
  94. package/src/session-runtime/provider-models.mjs +1 -1
  95. package/src/session-runtime/remote-control.mjs +6 -7
  96. package/src/session-runtime/runtime-core.mjs +52 -28
  97. package/src/session-runtime/self-update.mjs +1 -1
  98. package/src/session-runtime/session-lifecycle.mjs +36 -15
  99. package/src/session-runtime/session-title.mjs +1 -1
  100. package/src/session-runtime/session-turn-api.mjs +55 -15
  101. package/src/session-runtime/settings-api.mjs +7 -7
  102. package/src/standalone/agent-dispatch-broker.mjs +1 -1
  103. package/src/standalone/channel-admin.mjs +42 -53
  104. package/src/standalone/{channel-daemon-client.mjs → channel-client.mjs} +16 -18
  105. package/src/standalone/{channel-daemon-transport.mjs → channel-transport.mjs} +45 -46
  106. package/src/standalone/channel-worker.mjs +124 -647
  107. package/src/standalone/{backend-daemon.mjs → daemon.mjs} +187 -132
  108. package/src/standalone/memory-runtime-proxy.mjs +58 -1
  109. package/src/standalone/{engine-daemon-client.mjs → session-client.mjs} +151 -86
  110. package/src/standalone/session-protocol.mjs +8 -4
  111. package/src/standalone/session-runtime-pool.mjs +393 -0
  112. package/src/standalone/session-runtime-record.mjs +20 -0
  113. package/src/standalone/session-runtime-worker.mjs +203 -0
  114. package/src/standalone/{engine-daemon-service.mjs → session-service.mjs} +170 -157
  115. package/src/standalone/{engine-daemon-transport.mjs → session-transport.mjs} +139 -61
  116. package/src/standalone/session-wire.mjs +55 -0
  117. package/src/tui/App.jsx +4 -4
  118. package/src/tui/app/app-format.mjs +1 -1
  119. package/src/tui/app/channel-pickers.mjs +41 -41
  120. package/src/tui/app/input-parsers.mjs +2 -2
  121. package/src/tui/app/onboarding-steps.mjs +2 -2
  122. package/src/tui/app/project-picker.mjs +8 -8
  123. package/src/tui/app/prompt-submit.mjs +8 -8
  124. package/src/tui/app/settings-picker.mjs +25 -25
  125. package/src/tui/app/use-prompt-queue-history.mjs +1 -1
  126. package/src/tui/app/use-transcript-scroll.mjs +1 -1
  127. package/src/tui/components/ToolExecution.jsx +1 -1
  128. package/src/tui/components/prompt-input/restore-policy.mjs +1 -1
  129. package/src/tui/components/tool-execution/surface-detail.mjs +1 -1
  130. package/src/tui/dist/index.mjs +154 -127
  131. package/src/tui/hooks/useSession.mjs +16 -0
  132. package/src/tui/index.jsx +11 -11
  133. package/src/tui/{engine → session}/agent-envelope.mjs +3 -3
  134. package/src/tui/{engine → session}/agent-job-feed.mjs +4 -4
  135. package/src/tui/{engine → session}/boot-profile.mjs +2 -2
  136. package/src/tui/{engine → session}/context-state.mjs +3 -3
  137. package/src/tui/{engine → session}/labels.mjs +2 -2
  138. package/src/tui/{engine → session}/live-share.mjs +7 -7
  139. package/src/tui/{engine → session}/notice-text.mjs +2 -2
  140. package/src/tui/{engine → session}/notification-plan.mjs +3 -3
  141. package/src/tui/session/oauth-flows.mjs +140 -0
  142. package/src/tui/{engine → session}/prompt-history.mjs +3 -3
  143. package/src/tui/{engine → session}/queue-helpers.mjs +2 -2
  144. package/src/tui/{engine → session}/render-timing.mjs +2 -2
  145. package/src/tui/{engine → session}/session-api-ext.mjs +30 -17
  146. package/src/tui/{engine → session}/session-api.mjs +7 -60
  147. package/src/tui/{engine → session}/session-flow.mjs +1 -1
  148. package/src/tui/{engine → session}/session-stats.mjs +3 -3
  149. package/src/tui/{engine → session}/tool-approval.mjs +3 -3
  150. package/src/tui/{engine → session}/tool-call-fields.mjs +2 -2
  151. package/src/tui/{engine → session}/tool-card-results.mjs +4 -4
  152. package/src/tui/{engine → session}/tool-result-status.mjs +5 -5
  153. package/src/tui/{engine → session}/tool-result-text.mjs +3 -3
  154. package/src/tui/{engine → session}/transcript-spill.mjs +34 -34
  155. package/src/tui/{engine → session}/turn.mjs +29 -107
  156. package/src/tui/{engine-local-session.mjs → session-local.mjs} +59 -75
  157. package/src/tui/session.mjs +22 -0
  158. package/src/ui/statusline-segments.mjs +1 -1
  159. package/src/ui/statusline.mjs +1 -1
  160. package/vendor/ink/build/components/App.js +10 -1
  161. package/src/session-runtime/memory-daemon-probe.mjs +0 -61
  162. package/src/standalone/engine-daemon-protocol.mjs +0 -32
  163. package/src/tui/engine/turn-watchdog.mjs +0 -92
  164. package/src/tui/engine.mjs +0 -22
  165. package/src/tui/hooks/useEngine.mjs +0 -16
  166. /package/src/runtime/agent/orchestrator/session/compact/{engine.mjs → runner.mjs} +0 -0
  167. /package/src/runtime/channels/{backends → providers}/discord-attachments.mjs +0 -0
  168. /package/src/tui/{engine → session}/agent-response-tail.mjs +0 -0
  169. /package/src/tui/{engine → session}/frame-batched-store.mjs +0 -0
  170. /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
- process.removeAllListeners('warning')
5
- process.on('warning', () => {})
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
- const USE_ARG_DATA_DIR = IS_MEMORY_ENTRY || process.env.MIXDOG_WORKER_MODE === '1'
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 backend daemon. Memory
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
- function sendMemoryWorkerMessage(message) {
591
- return safeIpcSend(process, message, {
592
- onError: (error) => {
593
- const code = error?.code ? ` code=${error.code}` : ''
594
- __mixdogMemoryLog(`[memory-worker] IPC send failed${code}: ${error?.message || error}\n`)
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
- __mixdogMemoryLog(`[boot-time] tag=memory-ready tMs=${Date.now()}\n`)
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 memory service exits.
680
+ // postmaster keeps running after the unified daemon exits.
673
681
  if (!memorySecondaryMode()) {
674
682
  try {
675
- // Conservative check: only skip stopPgForShutdown when the owner
676
- // record is unambiguously (a) a memory-runtime-daemon owner record
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
- if (process.env.MIXDOG_WORKER_MODE === '1' && process.send) {
782
- // SIGTERM/SIGINT handler for worker mode: call stop() (fsyncs,
783
- // removes port file) then exit(0). Prevents taskkill /F from bypassing
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
- const transport = new StdioServerTransport()
912
- await mcp.connect(transport)
913
- await new Promise((resolve) => { mcp.onclose = resolve })
914
- await stop()
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 backend
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 backend restart.
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(), 'channel-daemon.json'), 'utf8'))
41
- const port = Number(raw?.port)
42
- if (!Number.isInteger(port) || port <= 0 || port >= 65536 || !raw?.token || !isPidAlive(raw?.pid)) {
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(raw.token) }
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 backend provider/session graph.
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: backend daemon unavailable')
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 backend's
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 daemon. Keep the factory
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 daemon runs as a detached cross-process HTTP
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 || fileURLToPath(new URL('../../../..', import.meta.url))
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-daemon stderr logger. The write is captured at import time so
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
- // Extracted from index.mjs — single/multi-instance process-lock + owner-election
2
- // helpers. Pure functions parameterized on lock-file paths so index.mjs retains
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
- export function tryAcquireMemoryOwnerLock(ownerLockFile, log = () => {}) {
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 guard. In multi-terminal mode the lock owner is a *peer*
121
- // memory worker serving recall for another CC session. killPreviousServer
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 backend discord.mjs:724) predate the
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 backend discord.mjs:724
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 backend = 'native-web-search'
289
+ const provider = 'native-web-search'
290
290
  const searchCacheKey = buildCacheKey('search', {
291
- provider: backend,
291
+ provider,
292
292
  ...cacheArgs,
293
293
  })
294
294
  const cachedSearch = getCachedEntry(cacheState, searchCacheKey)