mixdog 0.9.95 → 0.9.96

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 (178) hide show
  1. package/LICENSES/Apache-2.0.txt +201 -0
  2. package/LICENSES/MIT.txt +56 -0
  3. package/LICENSES/codex-NOTICE.txt +6 -0
  4. package/NOTICE.md +46 -9
  5. package/package.json +12 -9
  6. package/scripts/.tmp-coverage-report.mjs +96 -0
  7. package/scripts/build-tui.mjs +17 -1
  8. package/scripts/fixtures/patch-replay-corpus.json +98 -0
  9. package/src/defaults/mixdog-config.template.json +5 -8
  10. package/src/rules/agent/30-explorer.md +7 -4
  11. package/src/rules/shared/01-tool.md +20 -29
  12. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +18 -2
  13. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +31 -1
  14. package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +6 -17
  15. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +12 -1
  16. package/src/runtime/agent/orchestrator/config.mjs +246 -67
  17. package/src/runtime/agent/orchestrator/mcp/client.mjs +26 -11
  18. package/src/runtime/agent/orchestrator/mcp/reconnect-singleflight.mjs +23 -0
  19. package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +113 -14
  20. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +5 -1
  21. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +32 -11
  22. package/src/runtime/agent/orchestrator/providers/gemini.mjs +89 -2
  23. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +12 -1
  24. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +19 -1
  25. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +144 -39
  26. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -8
  27. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +15 -1
  28. package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +1 -1
  29. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +3 -4
  30. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +67 -7
  31. package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +291 -0
  32. package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +21 -0
  33. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +28 -4
  34. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +4 -33
  35. package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +13 -0
  36. package/src/runtime/agent/orchestrator/session/loop/termination.mjs +2 -2
  37. package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +0 -24
  38. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +1 -27
  39. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +6 -10
  40. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +5 -0
  41. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +142 -84
  42. package/src/runtime/agent/orchestrator/session/manager/runtime-loaders.mjs +4 -0
  43. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +31 -1
  44. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +29 -9
  45. package/src/runtime/agent/orchestrator/session/manager.mjs +2 -0
  46. package/src/runtime/agent/orchestrator/session/result-classification.mjs +65 -0
  47. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +86 -8
  48. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +12 -0
  49. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +3 -33
  50. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +4 -5
  51. package/src/runtime/agent/orchestrator/stall-policy.mjs +1 -1
  52. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +2 -2
  53. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +35 -1
  54. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +118 -18
  55. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +8 -9
  56. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +2 -2
  57. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +2 -2
  58. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +6 -15
  59. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +10 -26
  60. package/src/runtime/agent/orchestrator/tools/builtin/read-image.mjs +1 -1
  61. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +3 -3
  62. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +1 -23
  63. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +23 -9
  64. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +7 -0
  65. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +17 -39
  66. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +73 -0
  67. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +1 -1
  68. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +0 -2
  69. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +3 -8
  70. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +30 -0
  71. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +5 -5
  72. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +87 -15
  73. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +6 -15
  74. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +2 -2
  75. package/src/runtime/agent/orchestrator/tools/lib/pwsh-standby-pool.mjs +83 -13
  76. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +39 -1
  77. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +29 -1
  78. package/src/runtime/agent/orchestrator/tools/patch/parsing.mjs +16 -0
  79. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +108 -12
  80. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +10 -10
  81. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +56 -8
  82. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +2 -2
  83. package/src/runtime/channels/lib/config.mjs +2 -2
  84. package/src/runtime/channels/lib/inbound-handler.mjs +1 -1
  85. package/src/runtime/channels/lib/scheduler.mjs +2 -2
  86. package/src/runtime/memory/index.mjs +5 -38
  87. package/src/runtime/memory/lib/agent-ipc.mjs +145 -81
  88. package/src/runtime/memory/lib/compact-vector-cache.mjs +88 -0
  89. package/src/runtime/memory/lib/core-memory-store.mjs +18 -1
  90. package/src/runtime/memory/lib/cycle-llm-adapters.mjs +12 -33
  91. package/src/runtime/memory/lib/embedding-provider.mjs +47 -26
  92. package/src/runtime/memory/lib/embedding-worker.mjs +30 -34
  93. package/src/runtime/memory/lib/http-router.mjs +8 -0
  94. package/src/runtime/memory/lib/ko-morph.mjs +3 -20
  95. package/src/runtime/memory/lib/memory-action-handlers.mjs +2 -1
  96. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -0
  97. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +4 -2
  98. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +1 -0
  99. package/src/runtime/memory/lib/memory-cycle3.mjs +3 -1
  100. package/src/runtime/memory/lib/pg/process.mjs +32 -9
  101. package/src/runtime/memory/tool-defs.mjs +1 -5
  102. package/src/runtime/search/tool-defs.mjs +2 -2
  103. package/src/runtime/shared/agent-route-config.mjs +119 -0
  104. package/src/runtime/shared/atomic-file.mjs +80 -4
  105. package/src/runtime/shared/channel-notification-routing.mjs +2 -2
  106. package/src/runtime/shared/child-guardian.mjs +204 -108
  107. package/src/runtime/shared/child-spawn-gate.mjs +60 -31
  108. package/src/runtime/shared/config.mjs +130 -44
  109. package/src/runtime/shared/resource-admission.mjs +146 -97
  110. package/src/runtime/shared/stream-progress.mjs +6 -0
  111. package/src/runtime/shared/tool-surface.mjs +0 -5
  112. package/src/runtime/shared/turn-snapshot.mjs +31 -5
  113. package/src/runtime/shared/turn-worktree-snapshot.mjs +14 -2
  114. package/src/session-runtime/config-helpers.mjs +39 -37
  115. package/src/session-runtime/model-route-api.mjs +7 -5
  116. package/src/session-runtime/provider-auth-api.mjs +0 -7
  117. package/src/session-runtime/runtime-core.mjs +11 -11
  118. package/src/session-runtime/runtime-tunables.mjs +4 -4
  119. package/src/session-runtime/session-lifecycle.mjs +9 -2
  120. package/src/session-runtime/session-turn-api.mjs +41 -9
  121. package/src/session-runtime/settings-api.mjs +5 -17
  122. package/src/session-runtime/tool-defs.mjs +3 -3
  123. package/src/session-runtime/workflow-agents-api.mjs +55 -81
  124. package/src/session-runtime/workflow.mjs +14 -24
  125. package/src/standalone/agent-dispatch-broker.mjs +198 -0
  126. package/src/standalone/agent-tool/helpers.mjs +2 -2
  127. package/src/standalone/agent-tool/spawn-flow.mjs +2 -0
  128. package/src/standalone/agent-tool/spawn-preset.mjs +11 -21
  129. package/src/standalone/agent-tool/tool-def.mjs +0 -14
  130. package/src/standalone/backend-daemon.mjs +97 -36
  131. package/src/standalone/channel-admin.mjs +12 -12
  132. package/src/standalone/channel-daemon-client.mjs +17 -1
  133. package/src/standalone/channel-daemon-transport.mjs +90 -8
  134. package/src/standalone/engine-daemon-client.mjs +380 -205
  135. package/src/standalone/engine-daemon-protocol.mjs +3 -4
  136. package/src/standalone/engine-daemon-service.mjs +378 -134
  137. package/src/standalone/engine-daemon-transport.mjs +268 -77
  138. package/src/standalone/explore-tool.mjs +187 -70
  139. package/src/standalone/fair-call-scheduler.mjs +264 -0
  140. package/src/standalone/session-protocol.mjs +188 -0
  141. package/src/tui/App.jsx +30 -17
  142. package/src/tui/app/app-view.jsx +5 -2
  143. package/src/tui/app/core-memory-picker.mjs +19 -19
  144. package/src/tui/app/doctor.mjs +5 -11
  145. package/src/tui/app/message-selector.mjs +103 -0
  146. package/src/tui/app/onboarding-steps.mjs +1 -2
  147. package/src/tui/app/project-picker.mjs +80 -56
  148. package/src/tui/app/prompt-submit.mjs +46 -41
  149. package/src/tui/app/resume-picker.mjs +2 -2
  150. package/src/tui/app/route-pickers.mjs +3 -1
  151. package/src/tui/app/settings-picker.mjs +6 -8
  152. package/src/tui/app/transcript-window.mjs +38 -0
  153. package/src/tui/app/use-prompt-draft-flow.mjs +5 -7
  154. package/src/tui/app/use-prompt-handlers.mjs +86 -21
  155. package/src/tui/app/use-prompt-hint.mjs +3 -2
  156. package/src/tui/app/use-prompt-queue-history.mjs +10 -3
  157. package/src/tui/app/use-transcript-scroll.mjs +9 -2
  158. package/src/tui/app/use-transcript-window.mjs +50 -17
  159. package/src/tui/components/PromptInput.jsx +71 -38
  160. package/src/tui/components/Spinner.jsx +1 -1
  161. package/src/tui/components/prompt-input/edit-helpers.mjs +9 -11
  162. package/src/tui/components/prompt-input/escape-policy.mjs +42 -0
  163. package/src/tui/components/prompt-input/immediate-render.mjs +0 -10
  164. package/src/tui/components/prompt-input/interrupt-policy.mjs +10 -0
  165. package/src/tui/components/prompt-input/restore-policy.mjs +10 -0
  166. package/src/tui/dist/index.mjs +920 -1592
  167. package/src/tui/engine/live-share.mjs +34 -0
  168. package/src/tui/engine/session-api-ext.mjs +1 -4
  169. package/src/tui/engine/session-api.mjs +103 -33
  170. package/src/tui/engine/session-flow.mjs +7 -1
  171. package/src/tui/engine/tool-card-results.mjs +4 -2
  172. package/src/tui/engine/tool-result-text.mjs +10 -0
  173. package/src/tui/engine/turn.mjs +9 -69
  174. package/src/tui/engine-local-session.mjs +47 -31
  175. package/src/tui/index.jsx +1 -2
  176. package/src/tui/spinner-meta.mjs +4 -4
  177. package/src/tui/spinner-verbs.mjs +51 -3
  178. package/src/standalone/engine-daemon-local-bridge.mjs +0 -20
@@ -171,6 +171,7 @@ export function createHttpRouter({
171
171
  try {
172
172
  const db = getDb()
173
173
  const stats = await entryStats()
174
+ const memory = process.memoryUsage()
174
175
  sendJson(res, {
175
176
  status: 'ok',
176
177
  worker_pid: process.pid,
@@ -192,6 +193,13 @@ export function createHttpRouter({
192
193
  cycle_running: cycleScheduler.getCycleRunning(),
193
194
  cycle_health: cycleScheduler.getCycleHealth(),
194
195
  cycle_backlog: cycleScheduler.getCycleBacklogSnapshot(),
196
+ memory: {
197
+ rssBytes: memory.rss,
198
+ heapTotalBytes: memory.heapTotal,
199
+ heapUsedBytes: memory.heapUsed,
200
+ externalBytes: memory.external,
201
+ arrayBufferBytes: memory.arrayBuffers,
202
+ },
195
203
  })
196
204
  } catch (e) { sendError(res, e.message) }
197
205
  return
@@ -15,7 +15,6 @@
15
15
  // analyze(text) — sync; returns TokenInfo[] or null when not ready.
16
16
  // stems(text) — sync; content-morpheme stems (NNG/NNP/VV/VA/XR/SL) or null.
17
17
  import fs from 'node:fs'
18
- import os from 'node:os'
19
18
  import path from 'node:path'
20
19
  import zlib from 'node:zlib'
21
20
 
@@ -54,11 +53,7 @@ let _idleTimer = null
54
53
  const _envIdleMs = Number(process.env.MIXDOG_KO_MORPH_IDLE_TIMEOUT_MS)
55
54
  const IDLE_TIMEOUT_MS = Number.isFinite(_envIdleMs) && _envIdleMs >= 0
56
55
  ? _envIdleMs
57
- : 2 * 60_000
58
- const _envPressureMb = Number(process.env.MIXDOG_KO_MORPH_PRESSURE_MIN_FREE_MB)
59
- const PRESSURE_MIN_FREE_BYTES = (
60
- Number.isFinite(_envPressureMb) && _envPressureMb >= 0 ? _envPressureMb : 1536
61
- ) * 1024 * 1024
56
+ : 60_000
62
57
 
63
58
  export function isReady() { return _state === 'ready' && _kiwi != null }
64
59
  export function state() { return _state }
@@ -78,14 +73,8 @@ function releaseLoaded(reason = '') {
78
73
 
79
74
  function touchIdleTimer() {
80
75
  if (_idleTimer) clearTimeout(_idleTimer)
81
- let delay = IDLE_TIMEOUT_MS
82
- try {
83
- if (PRESSURE_MIN_FREE_BYTES > 0 && os.freemem() < PRESSURE_MIN_FREE_BYTES) delay = 1
84
- } catch {}
85
- if (delay <= 0) return
86
- _idleTimer = setTimeout(() => releaseLoaded(
87
- delay <= 1 ? 'host memory pressure' : 'idle timeout',
88
- ), delay)
76
+ if (IDLE_TIMEOUT_MS <= 0) return
77
+ _idleTimer = setTimeout(() => releaseLoaded('idle timeout'), IDLE_TIMEOUT_MS)
89
78
  _idleTimer.unref?.()
90
79
  }
91
80
 
@@ -166,12 +155,6 @@ export async function init(dataDir, log = () => {}) {
166
155
  return true
167
156
  }
168
157
  if (_initPromise) return _initPromise
169
- try {
170
- if (PRESSURE_MIN_FREE_BYTES > 0 && os.freemem() < PRESSURE_MIN_FREE_BYTES) {
171
- _log('[memory-service] kiwi morph load deferred under host memory pressure\n')
172
- return false
173
- }
174
- } catch {}
175
158
  _state = 'loading'
176
159
  const t0 = Date.now()
177
160
  _log(`[memory-service] kiwi morph init start (model ${KIWI_MODEL_VERSION})\n`)
@@ -33,6 +33,7 @@ import {
33
33
  promoteCoreCandidate,
34
34
  dismissCoreCandidate,
35
35
  normalizeCoreInput,
36
+ normalizeCoreOp,
36
37
  } from './core-memory-store.mjs'
37
38
  import { resolveProjectId, resolveProjectScope } from './project-id-resolver.mjs'
38
39
  import { resolvePluginData } from '../../shared/plugin-paths.mjs'
@@ -602,7 +603,7 @@ export function createMemoryActionHandlers({
602
603
  }
603
604
 
604
605
  if (action === 'core') {
605
- const op = String(args.op ?? '').trim().toLowerCase()
606
+ const op = normalizeCoreOp(args.op)
606
607
  if (!['add', 'edit', 'delete', 'list', 'candidates', 'promote', 'dismiss'].includes(op)) {
607
608
  return { text: 'core requires op: "add" | "edit" | "delete" | "list" | "candidates" | "promote" | "dismiss"', isError: true }
608
609
  }
@@ -634,6 +634,7 @@ async function _runCycle1Impl(db, config = {}, options = {}, _dataDir = null) {
634
634
  timeout,
635
635
  // Pin cwd to null so every memory cycle call hits the same agent cache shard.
636
636
  cwd: null,
637
+ signal,
637
638
  }, userMessage)
638
639
  throwIfAborted(signal)
639
640
  __mixdogMemoryLog(`[cycle1-time] window=${windowIdx} llmMs=${Date.now() - _tLlm}\n`)
@@ -32,7 +32,7 @@ const isShiftFollowToken = (tok) => {
32
32
  return ALL_PRIMARY_VERBS.has(v) || v === 'why' || v === 'core'
33
33
  }
34
34
 
35
- async function invokeLlm(prompt, mode, preset, timeout, llmCall = callAgentDispatch) {
35
+ async function invokeLlm(prompt, mode, preset, timeout, llmCall = callAgentDispatch, signal = null) {
36
36
  return await llmCall({
37
37
  agent: 'cycle2-agent',
38
38
  taskType: 'maintenance',
@@ -40,6 +40,7 @@ async function invokeLlm(prompt, mode, preset, timeout, llmCall = callAgentDispa
40
40
  preset,
41
41
  timeout,
42
42
  cwd: null,
43
+ signal,
43
44
  }, prompt)
44
45
  }
45
46
 
@@ -357,7 +358,7 @@ export async function runUnifiedGate(db, rows, activeContext, config = {}, optio
357
358
  const callOnce = async (extraTag) => {
358
359
  throwIfAborted(signal)
359
360
  const p = extraTag ? `${prompt}\n\n[retry:${extraTag}]` : prompt
360
- const raw = await invokeLlm(p, mode, preset, timeout, options.callLlm)
361
+ const raw = await invokeLlm(p, mode, preset, timeout, options.callLlm, signal)
361
362
  throwIfAborted(signal)
362
363
  return raw
363
364
  }
@@ -518,6 +519,7 @@ export async function sonnetCascade(candidates, rulesDigest, options = {}) {
518
519
  preset,
519
520
  timeout: 600000,
520
521
  cwd: null,
522
+ signal,
521
523
  }, prompt)
522
524
  } catch (err) {
523
525
  if (signal?.aborted) throw signal.reason ?? err
@@ -322,6 +322,7 @@ async function _llmJudgePair(summaryA, summaryB, siblingContext = [], options =
322
322
  preset: options.preset || resolveMaintenancePreset('memory'),
323
323
  timeout: 30000,
324
324
  cwd: null,
325
+ signal,
325
326
  }, prompt)
326
327
  throwIfAborted(signal)
327
328
  return String(raw ?? '').trim().toLowerCase().startsWith('merge')
@@ -36,7 +36,7 @@ function resourceDir() {
36
36
  return process.env.MIXDOG_ROOT || fileURLToPath(new URL('../../../..', import.meta.url))
37
37
  }
38
38
 
39
- async function invokeLlm(prompt, mode, preset, timeout, llmCall = callAgentDispatch) {
39
+ async function invokeLlm(prompt, mode, preset, timeout, llmCall = callAgentDispatch, signal = null) {
40
40
  return await llmCall({
41
41
  agent: 'cycle3-agent',
42
42
  taskType: 'maintenance',
@@ -44,6 +44,7 @@ async function invokeLlm(prompt, mode, preset, timeout, llmCall = callAgentDispa
44
44
  preset,
45
45
  timeout,
46
46
  cwd: null,
47
+ signal,
47
48
  }, prompt)
48
49
  }
49
50
 
@@ -55,6 +56,7 @@ export async function invokeCycle3Maintenance(prompt, options = {}) {
55
56
  preset,
56
57
  Number(options.timeout ?? 600000),
57
58
  options.callLlm,
59
+ options.signal,
58
60
  )
59
61
  }
60
62
 
@@ -107,6 +107,7 @@ async function findFreePort(preferred) {
107
107
  // ---------------------------------------------------------------------------
108
108
 
109
109
  const MIXDOG_CONF_V2_MARKER = '# mixdog overrides v2 — bgwriter / checkpoint distribution'
110
+ const MIXDOG_CONF_V3_MARKER = '# mixdog overrides v3 — bounded local-app resources'
110
111
 
111
112
  // Lines emitted into postgresql.conf for v2. effective_io_concurrency requires
112
113
  // posix_fadvise; PostgreSQL rejects non-zero values on Windows and macOS.
@@ -124,6 +125,21 @@ function buildV2Block() {
124
125
  return lines.join('\n') + '\n'
125
126
  }
126
127
 
128
+ function buildV3Block() {
129
+ return [
130
+ '',
131
+ MIXDOG_CONF_V3_MARKER,
132
+ // The only long-lived pools are memory(max 5) + trace(max 5). Keep ample
133
+ // recovery/bootstrap headroom without reserving backend slots for 100
134
+ // connections this single-user local store can never use.
135
+ 'max_connections = 32',
136
+ // Mixdog queries are short indexed lookups / bounded batches. LLVM compile
137
+ // setup adds latency and transient memory; parallel query workers remain
138
+ // untouched for genuinely large scans.
139
+ 'jit = off',
140
+ ].join('\n') + '\n'
141
+ }
142
+
127
143
  // Migration: an earlier v2 emitted effective_io_concurrency on every POSIX
128
144
  // platform. Strip it anywhere except Linux so old macOS clusters can start.
129
145
  function stripPlatformInvalidLines(conf) {
@@ -136,18 +152,25 @@ function ensureConfV2(pgdataDir) {
136
152
  if (!existsSync(confPath)) return false
137
153
  const original = readFileSync(confPath, 'utf8')
138
154
  let conf = stripPlatformInvalidLines(original)
139
- const stripped = conf !== original
140
- const hasMarker = conf.includes(MIXDOG_CONF_V2_MARKER)
141
- if (hasMarker && !stripped) return false
142
- if (!hasMarker) conf = conf + buildV2Block()
155
+ let changed = conf !== original
156
+ if (!conf.includes(MIXDOG_CONF_V2_MARKER)) {
157
+ conf += buildV2Block()
158
+ changed = true
159
+ }
160
+ if (!conf.includes(MIXDOG_CONF_V3_MARKER)) {
161
+ conf += buildV3Block()
162
+ changed = true
163
+ }
164
+ if (!changed) return false
143
165
  writeFileSync(confPath, conf)
144
166
  return true
145
167
  }
146
168
 
147
- // Public reconcile: idempotent conf v2 append + pg_ctl reload on a running
148
- // instance. Single source of truth for v2 application — supervisor and other
169
+ // Public reconcile: idempotent current override append + pg_ctl reload on a
170
+ // running instance. Single source of truth — supervisor and other
149
171
  // callers route through this rather than re-implementing the conf/reload pair.
150
- // All v2 settings are reload-applicable (no restart required).
172
+ // Reload-compatible settings apply immediately; postmaster settings such as
173
+ // max_connections apply on the next normal PG start (never restart live work).
151
174
  export function reconcileConfV2(runtimeDir, pgdataDir) {
152
175
  const applied = ensureConfV2(pgdataDir)
153
176
  if (!applied) return { applied: false, reloaded: false }
@@ -156,7 +179,7 @@ export function reconcileConfV2(runtimeDir, pgdataDir) {
156
179
  })
157
180
  const reloaded = reload.status === 0
158
181
  if (reloaded) {
159
- __mixdogMemoryLog('[pg-process] postgresql.conf v2 overrides applied via pg_ctl reload\n')
182
+ __mixdogMemoryLog('[pg-process] postgresql.conf overrides reconciled via pg_ctl reload\n')
160
183
  } else {
161
184
  __mixdogMemoryLog(`[pg-process] pg_ctl reload after v2 append failed (non-fatal): ${reload.stderr?.toString() || ''}\n`)
162
185
  }
@@ -252,7 +275,7 @@ export async function startPg({
252
275
  'wal_compression = on', // smaller WAL segments; fewer bytes for Defender to scan per segment
253
276
  'wal_init_zero = off', // skips zero-fill on new WAL segment; cuts Defender contact at segment creation
254
277
  'wal_recycle = off', // disables WAL rename/recycle loop; eliminates rename-storm EPERM pattern in pgdata
255
- ].join('\n') + '\n' + buildV2Block()
278
+ ].join('\n') + '\n' + buildV2Block() + buildV3Block()
256
279
 
257
280
  try {
258
281
  const existing = readFileSync(confPath, 'utf8')
@@ -10,11 +10,7 @@ export const TOOL_DEFS = [
10
10
  name: 'memory',
11
11
  title: 'Memory Cycle',
12
12
  annotations: { title: 'Memory Cycle', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false },
13
- description: [
14
- 'Core-memory mutation/status; recall retrieves.',
15
- 'Mutations use action=core with op; status uses action=status.',
16
- 'Store only durable compact ENGLISH facts/rules/preferences.',
17
- ].join(' '),
13
+ description: 'Core-memory mutation/status.',
18
14
  inputSchema: {
19
15
  type: 'object',
20
16
  properties: {
@@ -8,7 +8,7 @@ export const TOOL_DEFS = [
8
8
  {
9
9
  name: 'search',
10
10
  title: 'Mixdog Web Search',
11
- description: `Web/docs/current-info search. ${TOOL_SYNC_EXECUTION_CONTRACT} Cached; query supports array fan-out.`,
11
+ description: `Cached web/docs/current-info search. ${TOOL_SYNC_EXECUTION_CONTRACT}`,
12
12
  inputSchema: {
13
13
  type: 'object',
14
14
  properties: {
@@ -27,7 +27,7 @@ export const TOOL_DEFS = [
27
27
  {
28
28
  name: 'web_fetch',
29
29
  title: 'Mixdog Web Fetch',
30
- description: 'Use after search. Fetch page/docs body from URL.',
30
+ description: 'Fetch page/docs body from URL.',
31
31
  inputSchema: {
32
32
  type: 'object',
33
33
  properties: {
@@ -0,0 +1,119 @@
1
+ const LEGACY_AGENT_ROUTE_SLOTS = Object.freeze({
2
+ explore: Object.freeze({ workflow: 'explorer', maintenance: 'explore' }),
3
+ maintainer: Object.freeze({ alias: 'maintenance', workflow: 'memory', maintenance: 'memory' }),
4
+ worker: Object.freeze({ workflow: 'agent' }),
5
+ });
6
+
7
+ const REDUNDANT_WORKFLOW_PRESET_IDS = new Set([
8
+ 'workflow-agent',
9
+ 'workflow-explorer',
10
+ 'workflow-memory',
11
+ ]);
12
+
13
+ function record(value) {
14
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
15
+ }
16
+
17
+ export function isCompleteAgentRoute(value) {
18
+ return !!value
19
+ && typeof value === 'object'
20
+ && !Array.isArray(value)
21
+ && !!String(value.provider || '').trim()
22
+ && !!String(value.model || '').trim();
23
+ }
24
+
25
+ function generatedPresetAgentId(preset) {
26
+ const id = String(preset?.id || '').trim().toLowerCase();
27
+ if (id === 'workflow-agent') return 'worker';
28
+ if (id === 'workflow-explorer') return 'explore';
29
+ if (id === 'workflow-memory') return 'maintainer';
30
+ if (!id.startsWith('workflow-agent-')) return '';
31
+ const suffix = id.slice('workflow-agent-'.length);
32
+ if (suffix === 'explorer' || suffix === 'explore') return 'explore';
33
+ if (suffix === 'maintenance' || suffix === 'memory' || suffix === 'maintainer') return 'maintainer';
34
+ return suffix;
35
+ }
36
+
37
+ function routeFromGeneratedPreset(preset) {
38
+ const provider = String(preset?.provider || '').trim();
39
+ const model = String(preset?.model || '').trim();
40
+ if (!provider || !model) return null;
41
+ const route = { provider, model };
42
+ const effort = String(preset?.effort || '').trim();
43
+ if (effort) route.effort = effort;
44
+ if (preset?.fast === true) route.fast = true;
45
+ return route;
46
+ }
47
+
48
+ export function configuredAgentRouteCandidates(config, agentId) {
49
+ const id = String(agentId || '').trim();
50
+ if (!id) return [];
51
+ const agents = record(config?.agents);
52
+ const legacy = LEGACY_AGENT_ROUTE_SLOTS[id] || null;
53
+ const candidates = [agents[id]];
54
+ if (legacy?.alias) candidates.push(agents[legacy.alias]);
55
+ if (legacy?.workflow) candidates.push(record(config?.workflowRoutes)[legacy.workflow]);
56
+ if (legacy?.maintenance) candidates.push(record(config?.maintenance)[legacy.maintenance]);
57
+ return candidates.filter(Boolean);
58
+ }
59
+
60
+ export function canonicalizeAgentRoutes(config = {}) {
61
+ const sourceAgents = record(config.agents);
62
+ const agents = Object.fromEntries(
63
+ Object.entries(sourceAgents).filter(([, route]) => isCompleteAgentRoute(route)),
64
+ );
65
+ for (const id of Object.keys(LEGACY_AGENT_ROUTE_SLOTS)) {
66
+ if (agents[id]) continue;
67
+ const candidate = configuredAgentRouteCandidates({ ...config, agents: sourceAgents }, id)
68
+ .find((route) => isCompleteAgentRoute(route));
69
+ if (candidate) agents[id] = candidate;
70
+ }
71
+ delete agents.maintenance;
72
+ // Older settings saves generated one preset per fixed/custom agent. Promote
73
+ // a preset only when no higher-priority canonical/legacy route exists, then
74
+ // the redundant preset can be removed without discarding user assignments.
75
+ for (const preset of Array.isArray(config?.presets) ? config.presets : []) {
76
+ const id = generatedPresetAgentId(preset);
77
+ if (!id || agents[id]) continue;
78
+ const route = routeFromGeneratedPreset(preset);
79
+ if (route) agents[id] = route;
80
+ }
81
+ return agents;
82
+ }
83
+
84
+ export function isRedundantGeneratedRoutePreset(preset, defaultPreset = null) {
85
+ const id = String(preset?.id || '').trim();
86
+ if (!id || id === String(defaultPreset || '').trim()) return false;
87
+ return REDUNDANT_WORKFLOW_PRESET_IDS.has(id) || id.startsWith('workflow-agent-');
88
+ }
89
+
90
+ export function canonicalizeAgentRouteStorage(config = {}) {
91
+ const {
92
+ workflowRoutes: _legacyWorkflowRoutes,
93
+ ...rest
94
+ } = config || {};
95
+ const maintenance = { ...record(config?.maintenance) };
96
+ delete maintenance.explore;
97
+ delete maintenance.memory;
98
+ const presets = Array.isArray(config?.presets)
99
+ ? config.presets.filter((preset) => !isRedundantGeneratedRoutePreset(preset, config?.default))
100
+ : [];
101
+ return {
102
+ ...rest,
103
+ agents: canonicalizeAgentRoutes(config),
104
+ maintenance,
105
+ presets,
106
+ };
107
+ }
108
+
109
+ export function agentRouteStorageNeedsMigration(config = {}) {
110
+ const agents = record(config?.agents);
111
+ const maintenance = record(config?.maintenance);
112
+ return Object.prototype.hasOwnProperty.call(config || {}, 'workflowRoutes')
113
+ || Object.prototype.hasOwnProperty.call(agents, 'maintenance')
114
+ || Object.prototype.hasOwnProperty.call(maintenance, 'explore')
115
+ || Object.prototype.hasOwnProperty.call(maintenance, 'memory')
116
+ || Object.values(agents).some((route) => !isCompleteAgentRoute(route))
117
+ || (Array.isArray(config?.presets)
118
+ && config.presets.some((preset) => isRedundantGeneratedRoutePreset(preset, config?.default)));
119
+ }
@@ -28,6 +28,55 @@ import { AsyncLocalStorage } from 'node:async_hooks';
28
28
 
29
29
  const _execFileAsync = promisify(execFile);
30
30
 
31
+ // Async atomic writes from many daemon sessions share one disk. Keep enough
32
+ // parallelism for SSD throughput, then use additive-increase/multiplicative-
33
+ // decrease when latency shows the filesystem is saturated. This gate never
34
+ // touches synchronous compatibility paths and never revokes in-flight writes.
35
+ const _ioCeiling = Math.max(1, Math.floor(Number(process.env.MIXDOG_FILE_IO_MAX_CONCURRENCY) || 16));
36
+ let _ioLimit = Math.min(4, _ioCeiling);
37
+ let _ioActive = 0;
38
+ let _ioFastStreak = 0;
39
+ const _ioQueue = [];
40
+
41
+ function _drainAdaptiveFileIo() {
42
+ while (_ioActive < _ioLimit && _ioQueue.length > 0) {
43
+ const item = _ioQueue.shift();
44
+ _ioActive += 1;
45
+ const startedAt = Date.now();
46
+ Promise.resolve()
47
+ .then(item.run)
48
+ .then(item.resolve, item.reject)
49
+ .finally(() => {
50
+ const elapsed = Date.now() - startedAt;
51
+ _ioActive = Math.max(0, _ioActive - 1);
52
+ if (elapsed >= 250) {
53
+ _ioLimit = Math.max(1, Math.ceil(_ioLimit / 2));
54
+ _ioFastStreak = 0;
55
+ } else if (elapsed <= 40 && _ioLimit < _ioCeiling) {
56
+ _ioFastStreak += 1;
57
+ if (_ioFastStreak >= _ioLimit * 4) {
58
+ _ioLimit += 1;
59
+ _ioFastStreak = 0;
60
+ }
61
+ } else {
62
+ _ioFastStreak = 0;
63
+ }
64
+ _drainAdaptiveFileIo();
65
+ });
66
+ }
67
+ }
68
+
69
+ function _runAdaptiveFileIo(run) {
70
+ return new Promise((resolve, reject) => {
71
+ _ioQueue.push({ run, resolve, reject });
72
+ _drainAdaptiveFileIo();
73
+ });
74
+ }
75
+
76
+ export function adaptiveFileIoSnapshot() {
77
+ return { active: _ioActive, queued: _ioQueue.length, limit: _ioLimit, ceiling: _ioCeiling };
78
+ }
79
+
31
80
  const RETRY_CODES = new Set(['EPERM', 'EACCES', 'EBUSY', 'EEXIST']);
32
81
  const LOCK_WAIT_CODES = new Set(['EEXIST', 'EPERM', 'EACCES', 'EBUSY']);
33
82
  const DEFAULT_BACKOFFS_MS = Object.freeze([25, 50, 100, 200, 400, 800, 1200, 1600]);
@@ -79,6 +128,28 @@ function _describeLockHolder(lockPath) {
79
128
  }
80
129
  }
81
130
 
131
+ // Lock-wait watchdog. A cross-process file lock is invisible in a profile: the
132
+ // only symptom is "everything stopped". One bounded line per path names the
133
+ // path and the wait, so the next stall is diagnosed from the log instead of a
134
+ // live repro.
135
+ const LOCK_WAIT_WARN_MS = 500;
136
+ const LOCK_WAIT_WARN_INTERVAL_MS = 10_000;
137
+ const _lockWaitWarnedAt = new Map();
138
+ function _reportLockWait(lockPath, waitedMs, mode) {
139
+ if (waitedMs < LOCK_WAIT_WARN_MS) return;
140
+ const now = Date.now();
141
+ if (now - (_lockWaitWarnedAt.get(lockPath) || 0) < LOCK_WAIT_WARN_INTERVAL_MS) return;
142
+ _lockWaitWarnedAt.set(lockPath, now);
143
+ if (_lockWaitWarnedAt.size > 64) {
144
+ for (const [path, at] of _lockWaitWarnedAt) {
145
+ if (now - at >= LOCK_WAIT_WARN_INTERVAL_MS * 6) _lockWaitWarnedAt.delete(path);
146
+ }
147
+ }
148
+ try {
149
+ process.stderr.write(`[atomic-file] ${mode} lock wait ${waitedMs}ms: ${lockPath}\n`);
150
+ } catch { /* diagnostics only */ }
151
+ }
152
+
82
153
  export function renameWithRetrySync(src, dst, opts = {}) {
83
154
  const backoffs = Array.isArray(opts.backoffs) && opts.backoffs.length > 0
84
155
  ? opts.backoffs
@@ -129,6 +200,7 @@ export function withFileLockSync(lockPath, fn, opts = {}) {
129
200
  }
130
201
  const deadline = Date.now() + timeoutMs;
131
202
  mkdirSync(dirname(lockPath), { recursive: true });
203
+ const waitStartedAt = Date.now();
132
204
  let attempt = 0;
133
205
  let lastErr = null;
134
206
  while (true) {
@@ -168,6 +240,7 @@ export function withFileLockSync(lockPath, fn, opts = {}) {
168
240
  // `<pid> <ts> <token>` is a superset of the old `<pid> <ts>`; a
169
241
  // tokenless (old-format) lock is still read as pid-authoritative.
170
242
  try { writeFileSync(fd, `${process.pid} ${Date.now()} ${_OWNER_TOKEN}\n`, 'utf8'); } catch {}
243
+ _reportLockWait(lockPath, Date.now() - waitStartedAt, 'sync');
171
244
  // For secret-bearing critical sections, the lock file sits beside
172
245
  // the secret in the same (shared-home) directory; clamp it owner-only
173
246
  // too. Fail-closed: an unenforceable ACL aborts before fn() runs.
@@ -362,6 +435,7 @@ async function _withOsFileLock(lockPath, fn, opts = {}) {
362
435
  const staleMs = Number.isFinite(opts.staleMs) ? opts.staleMs : 30000;
363
436
  const deadline = Date.now() + timeoutMs;
364
437
  mkdirSync(dirname(lockPath), { recursive: true });
438
+ const waitStartedAt = Date.now();
365
439
  let attempt = 0;
366
440
  let lastErr = null;
367
441
  while (true) {
@@ -393,6 +467,7 @@ async function _withOsFileLock(lockPath, fn, opts = {}) {
393
467
  continue;
394
468
  }
395
469
  try { writeFileSync(fd, `${process.pid} ${Date.now()} ${_OWNER_TOKEN}\n`, 'utf8'); } catch {}
470
+ _reportLockWait(lockPath, Date.now() - waitStartedAt, 'async');
396
471
  _osHeldPaths.add(lockPath);
397
472
  try {
398
473
  // Async ACL enforcement (promisified execFile) so a secret-bearing
@@ -738,10 +813,11 @@ export async function writeFileAtomicAsync(filePath, data, opts = {}) {
738
813
  throw err;
739
814
  }
740
815
  };
741
- if (opts.lock === true) {
742
- return withFileLock(`${filePath}.lock`, run, opts);
743
- }
744
- return run();
816
+ return _runAdaptiveFileIo(() => (
817
+ opts.lock === true
818
+ ? withFileLock(`${filePath}.lock`, run, opts)
819
+ : run()
820
+ ));
745
821
  }
746
822
 
747
823
  export function writeJsonAtomicAsync(filePath, value, opts = {}) {
@@ -7,8 +7,8 @@ export function channelNotificationModelContent(params = {}) {
7
7
  }
8
8
 
9
9
  export function shouldMirrorChannelNotificationToPending(meta = {}) {
10
- // CC parity (reference messageQueueManager.ts): every inbound channel/
11
- // schedule notification flows through the single queue — when no live
10
+ // Every inbound channel/schedule notification flows through the single
11
+ // queue — when no live
12
12
  // listener handled it (engine not subscribed yet, pane disposed, headless),
13
13
  // it is mirrored into the session pending queue for the next turn instead
14
14
  // of being dropped. Silent-to-agent stays excluded; the upstream content