mixdog 0.8.1 → 0.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (280) hide show
  1. package/README.md +47 -23
  2. package/package.json +33 -27
  3. package/scripts/_test-folder-dialog.mjs +30 -0
  4. package/scripts/agent-parallel-smoke.mjs +388 -0
  5. package/scripts/agent-tag-reuse-smoke.mjs +183 -0
  6. package/scripts/background-task-meta-smoke.mjs +38 -0
  7. package/scripts/boot-smoke.mjs +52 -9
  8. package/scripts/build-runtime-linux.sh +348 -0
  9. package/scripts/build-runtime-macos.sh +217 -0
  10. package/scripts/build-runtime-windows.ps1 +242 -0
  11. package/scripts/compact-active-turn-test.mjs +68 -0
  12. package/scripts/compact-smoke.mjs +859 -129
  13. package/scripts/compact-trigger-migration-smoke.mjs +187 -0
  14. package/scripts/fix-brief-fn.mjs +35 -0
  15. package/scripts/fix-format-tool-surface.mjs +24 -0
  16. package/scripts/fix-tool-exec-visible.mjs +42 -0
  17. package/scripts/generate-runtime-manifest.mjs +166 -0
  18. package/scripts/hook-bus-test.mjs +330 -0
  19. package/scripts/lead-workflow-smoke.mjs +33 -39
  20. package/scripts/live-worker-smoke.mjs +43 -37
  21. package/scripts/llm-trace-summary.mjs +315 -0
  22. package/scripts/memory-meta-concurrency-test.mjs +20 -0
  23. package/scripts/output-style-smoke.mjs +56 -15
  24. package/scripts/parent-abort-link-test.mjs +44 -0
  25. package/scripts/patch-agent-brief.mjs +48 -0
  26. package/scripts/patch-app.mjs +21 -0
  27. package/scripts/patch-app2.mjs +18 -0
  28. package/scripts/patch-dist-brief.mjs +96 -0
  29. package/scripts/patch-tool-exec.mjs +70 -0
  30. package/scripts/pretool-ask-runtime-test.mjs +54 -0
  31. package/scripts/provider-toolcall-test.mjs +376 -0
  32. package/scripts/reactive-compact-persist-smoke.mjs +124 -0
  33. package/scripts/sanitize-tool-pairs-test.mjs +260 -0
  34. package/scripts/session-context-bench.mjs +205 -33
  35. package/scripts/session-ingest-smoke.mjs +177 -0
  36. package/scripts/set-effort-config-test.mjs +41 -0
  37. package/scripts/smoke-runtime-negative.ps1 +106 -0
  38. package/scripts/smoke-runtime-negative.sh +97 -0
  39. package/scripts/smoke.mjs +25 -0
  40. package/scripts/tool-result-hook-test.mjs +48 -0
  41. package/scripts/tool-smoke.mjs +1222 -90
  42. package/scripts/toolcall-args-test.mjs +150 -0
  43. package/scripts/tui-background-failure-smoke.mjs +73 -0
  44. package/scripts/usage-metrics-epoch-smoke.mjs +114 -0
  45. package/src/agents/debugger/AGENT.md +8 -0
  46. package/src/agents/explore/AGENT.md +4 -0
  47. package/src/agents/heavy-worker/AGENT.md +9 -3
  48. package/src/agents/maintainer/AGENT.md +4 -0
  49. package/src/agents/reviewer/AGENT.md +8 -0
  50. package/src/agents/scheduler-task/AGENT.md +12 -0
  51. package/src/agents/scheduler-task/agent.json +6 -0
  52. package/src/agents/webhook-handler/AGENT.md +12 -0
  53. package/src/agents/webhook-handler/agent.json +6 -0
  54. package/src/agents/worker/AGENT.md +9 -3
  55. package/src/app.mjs +77 -3
  56. package/src/defaults/hidden-roles.json +17 -12
  57. package/src/headless-role.mjs +117 -0
  58. package/src/help.mjs +30 -0
  59. package/src/hooks/lib/permission-evaluator.cjs +11 -475
  60. package/src/lib/keychain-cjs.cjs +9 -1
  61. package/src/lib/mixdog-debug.cjs +0 -7
  62. package/src/lib/rules-builder.cjs +240 -96
  63. package/src/lib/text-utils.cjs +1 -1
  64. package/src/mixdog-session-runtime.mjs +2212 -446
  65. package/src/output-styles/default.md +12 -28
  66. package/src/output-styles/extreme-simple.md +9 -6
  67. package/src/output-styles/simple.md +22 -9
  68. package/src/repl.mjs +110 -59
  69. package/src/rules/agent/00-common.md +15 -0
  70. package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
  71. package/src/rules/agent/30-explorer.md +22 -0
  72. package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
  73. package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
  74. package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
  75. package/src/rules/lead/01-general.md +9 -5
  76. package/src/rules/lead/04-workflow.md +51 -12
  77. package/src/rules/lead/lead-tool.md +6 -0
  78. package/src/rules/shared/01-tool.md +12 -1
  79. package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
  80. package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
  81. package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
  82. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
  83. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
  84. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
  85. package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
  86. package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
  87. package/src/runtime/agent/orchestrator/config.mjs +174 -55
  88. package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
  89. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
  90. package/src/runtime/agent/orchestrator/internal-roles.mjs +76 -28
  91. package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
  92. package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
  93. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
  94. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +377 -243
  95. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +146 -93
  96. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
  97. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
  98. package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
  99. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
  100. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
  101. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
  102. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
  103. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
  104. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
  106. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +221 -70
  107. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +477 -147
  108. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -496
  109. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
  110. package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
  111. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +31 -11
  112. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
  113. package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
  114. package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
  115. package/src/runtime/agent/orchestrator/session/loop.mjs +840 -612
  116. package/src/runtime/agent/orchestrator/session/manager.mjs +1574 -459
  117. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
  118. package/src/runtime/agent/orchestrator/session/store.mjs +67 -29
  119. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  120. package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
  121. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
  122. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +34 -7
  123. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -0
  124. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -9
  125. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
  126. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
  127. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
  128. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
  129. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
  130. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
  131. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
  132. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
  133. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
  134. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
  135. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
  136. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
  137. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
  138. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
  139. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
  140. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
  141. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
  142. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
  143. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
  144. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
  145. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
  146. package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
  147. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
  148. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
  149. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
  150. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
  151. package/src/runtime/channels/index.mjs +152 -24
  152. package/src/runtime/channels/lib/scheduler.mjs +18 -14
  153. package/src/runtime/channels/lib/session-discovery.mjs +3 -2
  154. package/src/runtime/channels/lib/transcript-discovery.mjs +3 -2
  155. package/src/runtime/channels/lib/webhook.mjs +1 -1
  156. package/src/runtime/channels/tool-defs.mjs +29 -29
  157. package/src/runtime/memory/index.mjs +520 -98
  158. package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
  159. package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
  160. package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
  161. package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
  162. package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
  163. package/src/runtime/memory/lib/memory-cycle1.mjs +28 -7
  164. package/src/runtime/memory/lib/memory-cycle2.mjs +4 -5
  165. package/src/runtime/memory/lib/memory-cycle3.mjs +2 -2
  166. package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -1
  167. package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
  168. package/src/runtime/memory/lib/memory.mjs +88 -9
  169. package/src/runtime/memory/lib/model-profile.mjs +1 -1
  170. package/src/runtime/memory/lib/pg/adapter.mjs +1 -1
  171. package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
  172. package/src/runtime/memory/lib/runtime-fetcher.mjs +37 -3
  173. package/src/runtime/memory/lib/session-ingest.mjs +194 -0
  174. package/src/runtime/memory/lib/trace-store.mjs +96 -51
  175. package/src/runtime/memory/tool-defs.mjs +46 -37
  176. package/src/runtime/search/index.mjs +102 -466
  177. package/src/runtime/search/lib/web-tools.mjs +45 -25
  178. package/src/runtime/search/tool-defs.mjs +16 -23
  179. package/src/runtime/shared/abort-controller.mjs +1 -1
  180. package/src/runtime/shared/atomic-file.mjs +4 -3
  181. package/src/runtime/shared/background-tasks.mjs +122 -11
  182. package/src/runtime/shared/child-spawn-gate.mjs +145 -0
  183. package/src/runtime/shared/config.mjs +7 -4
  184. package/src/runtime/shared/err-text.mjs +131 -4
  185. package/src/runtime/shared/llm/cost.mjs +2 -2
  186. package/src/runtime/shared/llm/http-agent.mjs +23 -7
  187. package/src/runtime/shared/llm/index.mjs +34 -11
  188. package/src/runtime/shared/llm/usage-log.mjs +4 -4
  189. package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
  190. package/src/runtime/shared/singleton-owner.mjs +104 -0
  191. package/src/runtime/shared/tool-execution-contract.mjs +199 -20
  192. package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
  193. package/src/runtime/shared/tool-surface.mjs +624 -95
  194. package/src/runtime/shared/user-data-guard.mjs +0 -2
  195. package/src/standalone/agent-task-status.mjs +203 -0
  196. package/src/standalone/agent-task-status.test.mjs +76 -0
  197. package/src/standalone/agent-tool.mjs +1913 -0
  198. package/src/standalone/channel-worker.mjs +370 -14
  199. package/src/standalone/explore-tool.mjs +165 -70
  200. package/src/standalone/folder-dialog.mjs +314 -0
  201. package/src/standalone/hook-bus.mjs +898 -22
  202. package/src/standalone/memory-runtime-proxy.mjs +320 -0
  203. package/src/standalone/projects.mjs +226 -0
  204. package/src/standalone/provider-admin.mjs +41 -24
  205. package/src/standalone/seeds.mjs +2 -69
  206. package/src/standalone/usage-dashboard.mjs +96 -8
  207. package/src/tui/App.jsx +4788 -2151
  208. package/src/tui/components/AnsiText.jsx +39 -28
  209. package/src/tui/components/ContextPanel.jsx +14 -4
  210. package/src/tui/components/Markdown.jsx +43 -77
  211. package/src/tui/components/MarkdownTable.jsx +9 -184
  212. package/src/tui/components/Message.jsx +28 -11
  213. package/src/tui/components/Picker.jsx +95 -56
  214. package/src/tui/components/PromptInput.jsx +367 -239
  215. package/src/tui/components/QueuedCommands.jsx +1 -1
  216. package/src/tui/components/SlashCommandPalette.jsx +27 -21
  217. package/src/tui/components/Spinner.jsx +67 -38
  218. package/src/tui/components/StatusLine.jsx +606 -38
  219. package/src/tui/components/TextEntryPanel.jsx +128 -9
  220. package/src/tui/components/ToolExecution.jsx +592 -362
  221. package/src/tui/components/TurnDone.jsx +3 -3
  222. package/src/tui/components/UsagePanel.jsx +3 -5
  223. package/src/tui/components/tool-output-format.mjs +365 -0
  224. package/src/tui/components/tool-output-format.test.mjs +220 -0
  225. package/src/tui/dist/index.mjs +8826 -2390
  226. package/src/tui/engine-runtime-notification.test.mjs +115 -0
  227. package/src/tui/engine-tool-result-text.test.mjs +75 -0
  228. package/src/tui/engine.mjs +1455 -279
  229. package/src/tui/figures.mjs +21 -40
  230. package/src/tui/index.jsx +75 -31
  231. package/src/tui/input-editing.mjs +25 -0
  232. package/src/tui/markdown/format-token.mjs +511 -68
  233. package/src/tui/markdown/format-token.test.mjs +216 -0
  234. package/src/tui/markdown/render-ansi.mjs +94 -0
  235. package/src/tui/markdown/render-ansi.test.mjs +108 -0
  236. package/src/tui/markdown/stream-fence.mjs +34 -0
  237. package/src/tui/markdown/stream-fence.test.mjs +26 -0
  238. package/src/tui/markdown/table-layout.mjs +250 -0
  239. package/src/tui/paste-attachments.mjs +0 -7
  240. package/src/tui/spinner-verbs.mjs +1 -2
  241. package/src/tui/statusline-ansi-bridge.mjs +172 -0
  242. package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
  243. package/src/tui/theme.mjs +746 -24
  244. package/src/tui/time-format.mjs +1 -1
  245. package/src/tui/transcript-tool-failures.mjs +67 -0
  246. package/src/tui/transcript-tool-failures.test.mjs +111 -0
  247. package/src/ui/ansi.mjs +1 -2
  248. package/src/ui/markdown.mjs +85 -26
  249. package/src/ui/markdown.test.mjs +70 -0
  250. package/src/ui/model-display.mjs +121 -0
  251. package/src/ui/session-stats.mjs +44 -0
  252. package/src/ui/statusline-context-label.test.mjs +15 -0
  253. package/src/ui/statusline.mjs +386 -178
  254. package/src/ui/tool-card.mjs +2 -13
  255. package/src/vendor/statusline/bin/statusline-lib.mjs +8 -4
  256. package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
  257. package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
  258. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
  259. package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
  260. package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
  261. package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
  262. package/src/workflows/default/WORKFLOW.md +12 -5
  263. package/src/workflows/default/workflow.json +0 -1
  264. package/src/workflows/solo/WORKFLOW.md +15 -0
  265. package/src/workflows/solo/workflow.json +7 -0
  266. package/vendor/ink/build/output.js +6 -1
  267. package/src/agents/scheduler-task.md +0 -3
  268. package/src/agents/web-researcher/AGENT.md +0 -3
  269. package/src/agents/web-researcher/agent.json +0 -6
  270. package/src/agents/webhook-handler.md +0 -3
  271. package/src/rules/bridge/00-common.md +0 -5
  272. package/src/rules/bridge/30-explorer.md +0 -4
  273. package/src/rules/lead/00-tool-lead.md +0 -5
  274. package/src/rules/shared/00-language.md +0 -3
  275. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  276. package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
  277. package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
  278. package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
  279. package/src/standalone/bridge-tool.mjs +0 -1414
  280. package/src/tui/runtime/shared/process-shutdown.mjs +0 -1
@@ -254,23 +254,77 @@ export async function init(db, dims) {
254
254
  )
255
255
  }
256
256
 
257
- // Validate that the halfvec column dimension stored in the DB matches
258
- // dimCount from the current model config. Call after schema is confirmed
259
- // complete and before any embedding operations.
260
- export async function validateEmbeddingDims(db, dimCount) {
257
+ async function getEmbeddingColumnDims(db, tableName) {
261
258
  const r = await db.query(`
262
- SELECT atttypmod
259
+ SELECT a.atttypmod
263
260
  FROM pg_attribute a
264
261
  JOIN pg_class c ON c.oid = a.attrelid
265
- WHERE c.relname = 'entries'
262
+ JOIN pg_namespace n ON n.oid = c.relnamespace
263
+ WHERE n.nspname = current_schema()
264
+ AND c.relname = $1
266
265
  AND a.attname = 'embedding'
267
266
  AND a.attnum > 0
268
267
  AND NOT a.attisdropped
269
- `)
268
+ `, [tableName])
270
269
  const row = r.rows[0]
271
- if (!row) return // column absent — pre-schema DB; bootstrapSchema will handle
270
+ return row ? Number(row.atttypmod) : null
271
+ }
272
+
273
+ async function ensureHotActiveSearchObjects(db) {
274
+ await db.exec(`
275
+ CREATE MATERIALIZED VIEW IF NOT EXISTS mv_hot_active AS
276
+ SELECT id, element, summary, category, status, score, last_seen_at, promoted_at,
277
+ project_id, embedding, search_tsv
278
+ FROM entries
279
+ WHERE is_root = 1 AND status = 'active' AND embedding IS NOT NULL
280
+ WITH NO DATA
281
+ `)
282
+ await db.exec(`CREATE UNIQUE INDEX IF NOT EXISTS mv_hot_active_id ON mv_hot_active(id)`)
283
+ await db.exec(`CREATE INDEX IF NOT EXISTS mv_hot_active_hnsw ON mv_hot_active USING hnsw (embedding halfvec_cosine_ops)`)
284
+ await db.exec(`CREATE INDEX IF NOT EXISTS mv_hot_active_tsv ON mv_hot_active USING GIN (search_tsv)`)
285
+ await db.exec(`CREATE INDEX IF NOT EXISTS mv_hot_active_score ON mv_hot_active(score DESC)`)
286
+ }
287
+
288
+ async function resetEmbeddingColumnsForDims(db, dimCount) {
289
+ const entriesDims = await getEmbeddingColumnDims(db, 'entries')
290
+ const coreDims = await getEmbeddingColumnDims(db, 'core_entries')
291
+ const needsEntriesReset = entriesDims != null && entriesDims !== dimCount
292
+ const needsCoreReset = coreDims != null && coreDims !== dimCount
293
+ if (!needsEntriesReset && !needsCoreReset) return false
294
+
295
+ __mixdogMemoryLog(
296
+ `[memory] embedding dimension changed; resetting vectors for halfvec(${dimCount}) ` +
297
+ `(entries=${entriesDims ?? 'missing'}, core_entries=${coreDims ?? 'missing'})\n`,
298
+ )
299
+
300
+ await db.exec(`DROP MATERIALIZED VIEW IF EXISTS mv_hot_active CASCADE`)
301
+ await db.exec(`DROP INDEX IF EXISTS idx_entries_embedding_hnsw`)
302
+ await db.exec(`DROP INDEX IF EXISTS core_entries_embedding_hnsw`)
303
+
304
+ if (needsEntriesReset) {
305
+ await db.exec(`ALTER TABLE entries ALTER COLUMN embedding TYPE halfvec(${dimCount}) USING NULL::halfvec(${dimCount})`)
306
+ await db.exec(`UPDATE entries SET summary_hash = NULL WHERE summary_hash IS NOT NULL`)
307
+ }
308
+ if (needsCoreReset) {
309
+ await db.exec(`ALTER TABLE core_entries ALTER COLUMN embedding TYPE halfvec(${dimCount}) USING NULL::halfvec(${dimCount})`)
310
+ }
311
+
312
+ await db.exec(`DROP TABLE IF EXISTS memory.embedding_cache`)
313
+ await db.query(
314
+ `INSERT INTO meta(key, value) VALUES ($1, $2::jsonb)
315
+ ON CONFLICT(key) DO UPDATE SET value = EXCLUDED.value`,
316
+ ['embedding.current_dims', JSON.stringify(dimCount)],
317
+ )
318
+ return true
319
+ }
320
+
321
+ // Validate that the halfvec column dimension stored in the DB matches
322
+ // dimCount from the current model config. Call after schema is confirmed
323
+ // complete and before any embedding operations.
324
+ export async function validateEmbeddingDims(db, dimCount) {
325
+ const colDims = await getEmbeddingColumnDims(db, 'entries')
326
+ if (colDims == null) return // column absent — pre-schema DB; bootstrapSchema will handle
272
327
  // pgvector halfvec stores dimension as atttypmod directly (unlike varchar which uses dims+4).
273
- const colDims = row.atttypmod
274
328
  if (colDims !== dimCount) {
275
329
  throw new Error(
276
330
  `Embedding dimension mismatch: DB column halfvec(${colDims}) vs model config ${dimCount} dims. ` +
@@ -285,6 +339,7 @@ export async function ensureCurrentSchemaExtensions(db, dims) {
285
339
  // idempotent and define the current runtime schema.
286
340
  if (Number.isInteger(dims) && dims > 0) {
287
341
  await db.exec(`ALTER TABLE core_entries ADD COLUMN IF NOT EXISTS embedding halfvec(${dims})`)
342
+ await db.exec(`CREATE INDEX IF NOT EXISTS idx_entries_embedding_hnsw ON entries USING hnsw (embedding halfvec_cosine_ops) WHERE is_root = 1 AND embedding IS NOT NULL`)
288
343
  await db.exec(`CREATE INDEX IF NOT EXISTS core_entries_embedding_hnsw ON core_entries USING hnsw (embedding halfvec_cosine_ops) WHERE embedding IS NOT NULL`)
289
344
  }
290
345
  await db.exec(`ALTER TABLE entries ADD COLUMN IF NOT EXISTS core_summary text`)
@@ -319,6 +374,16 @@ export async function ensureCurrentSchemaExtensions(db, dims) {
319
374
  __mixdogMemoryLog(`[memory] ensureCurrentSchemaExtensions: core_entries_unique_proj_elem creation failed — duplicate rows must be deduplicated before this index can be created: ${err?.message || err}\n`)
320
375
  throw err
321
376
  }
377
+
378
+ if (Number.isInteger(dims) && dims > 0) {
379
+ await db.query(
380
+ `INSERT INTO meta(key, value) VALUES ($1, $2::jsonb)
381
+ ON CONFLICT(key) DO UPDATE SET value = EXCLUDED.value`,
382
+ ['embedding.current_dims', JSON.stringify(dims)],
383
+ )
384
+ }
385
+
386
+ await ensureHotActiveSearchObjects(db)
322
387
  }
323
388
 
324
389
  export async function openDatabase(dataDir, dims) {
@@ -346,6 +411,9 @@ export async function openDatabase(dataDir, dims) {
346
411
  }
347
412
  })
348
413
  }
414
+ if (await isBootstrapComplete(db)) {
415
+ await resetEmbeddingColumnsForDims(db, Number(dims))
416
+ }
349
417
  await ensureCurrentSchemaExtensions(db, Number(dims))
350
418
  await validateEmbeddingDims(db, Number(dims))
351
419
 
@@ -412,6 +480,17 @@ export async function setMetaValue(db, key, value) {
412
480
  )
413
481
  }
414
482
 
483
+ // Shallow-merge patch into an existing JSON object meta row (jsonb ||). Avoids
484
+ // read-modify-write lost updates when concurrent writers touch different keys.
485
+ export async function mergeMetaValue(db, key, patch) {
486
+ const patchJson = typeof patch === 'string' ? patch : JSON.stringify(patch ?? {})
487
+ await db.query(
488
+ `INSERT INTO meta(key, value) VALUES ($1, $2::jsonb)
489
+ ON CONFLICT(key) DO UPDATE SET value = COALESCE(meta.value, '{}'::jsonb) || EXCLUDED.value`,
490
+ [key, patchJson],
491
+ )
492
+ }
493
+
415
494
  export function embeddingToSql(arr) {
416
495
  if (!arr || !Array.isArray(arr)) return null
417
496
  return `[${arr.map((n) => Number(n).toFixed(6)).join(',')}]`
@@ -4,7 +4,7 @@
4
4
  * Writes one JSONL line per measurement point to
5
5
  * `history/model-profile.jsonl` (mirrors `retrieval-trace.jsonl`). Meant for
6
6
  * the embedding provider. Fire-and-forget: any I/O error is swallowed so
7
- * instrumentation never breaks a live bridge or memory-cycle call.
7
+ * instrumentation never breaks a live agent or memory-cycle call.
8
8
  *
9
9
  * Schema:
10
10
  * {
@@ -86,7 +86,7 @@ async function _initClient(client, schema) {
86
86
  }
87
87
 
88
88
  // Wrapper around pool.connect() that runs per-client init before returning.
89
- // Exported so external callers (e.g. trace-store.insertBridgeCalls) can obtain
89
+ // Exported so external callers (e.g. trace-store.insertAgentCalls) can obtain
90
90
  // a connection with search_path already set; raw _pool.connect() leaves it at
91
91
  // the PG default and unqualified table lookups resolve in the wrong schema.
92
92
  export async function checkedConnect(pgPool, schema) {
@@ -64,6 +64,15 @@ const LOCK_POLL_MS = 100;
64
64
  const LOCK_WARN_MS = 5_000;
65
65
  const LOCK_WAIT_CODES = new Set(['EEXIST', 'EPERM', 'EACCES', 'EBUSY']);
66
66
 
67
+ function envFlagEnabled(name) {
68
+ const raw = String(process.env[name] ?? '').trim().toLowerCase();
69
+ return raw === '1' || raw === 'true' || raw === 'on' || raw === 'yes';
70
+ }
71
+
72
+ function attachOnlyMode() {
73
+ return envFlagEnabled('MIXDOG_PG_ATTACH_ONLY') || envFlagEnabled('MIXDOG_MEMORY_SECONDARY');
74
+ }
75
+
67
76
  // ── File lock (O_EXCL pattern from dispatch-persist.mjs / run-mcp.mjs) ───────
68
77
 
69
78
  /**
@@ -356,6 +365,9 @@ async function _doEnsure(dataDir) {
356
365
  source: 'prelock',
357
366
  });
358
367
  if (prelockReuse) return prelockReuse;
368
+ if (attachOnlyMode()) {
369
+ throw new Error('secondary memory runtime requires an existing PG instance')
370
+ }
359
371
 
360
372
  // ── Acquire spawn lock to serialize initdb races ─────────────────────────
361
373
  const acquired = await acquireSpawnLock(dataDir, () => tryReusePgInstance({
@@ -37,6 +37,7 @@ const BUNDLED_MANIFEST_PATH = fileURLToPath(new URL('../data/runtime-manifest.js
37
37
 
38
38
  // GitHub raw URL fallback — used only when no cached or bundled manifest exists.
39
39
  const MANIFEST_URL = 'https://raw.githubusercontent.com/tribgames/mixdog/main/src/runtime/memory/data/runtime-manifest.json'
40
+ const LEGACY_RUNTIME_RELEASE_REPOSITORY = 'trib-plugin/mixdog'
40
41
 
41
42
  // ---------------------------------------------------------------------------
42
43
  // Platform key
@@ -154,6 +155,26 @@ async function downloadWithRetry(url, destPath) {
154
155
  throw lastErr
155
156
  }
156
157
 
158
+ function runtimeAssetUrlCandidates(url) {
159
+ const value = String(url || '')
160
+ const out = []
161
+ const add = (candidate) => {
162
+ if (candidate && !out.includes(candidate)) out.push(candidate)
163
+ }
164
+ add(value)
165
+ const match = /^https:\/\/github\.com\/([^/]+\/[^/]+)\/releases\/download\//i.exec(value)
166
+ if (!match) return out
167
+ const releaseRepo = match[1]
168
+ const overrides = String(process.env.MIXDOG_RUNTIME_RELEASE_REPOSITORY || process.env.RUNTIME_RELEASE_REPOSITORY || '')
169
+ .split(/[\s,;]+/u)
170
+ .map((repo) => repo.trim())
171
+ .filter(Boolean)
172
+ for (const repo of [...overrides, LEGACY_RUNTIME_RELEASE_REPOSITORY]) {
173
+ if (repo && repo !== releaseRepo) add(value.replace(`/github.com/${releaseRepo}/`, `/github.com/${repo}/`))
174
+ }
175
+ return out
176
+ }
177
+
157
178
  // ---------------------------------------------------------------------------
158
179
  // Tar entry path validation + extraction
159
180
  // ---------------------------------------------------------------------------
@@ -412,10 +433,23 @@ export async function ensureRuntime(dataDir) {
412
433
  const stagingLock = acquireStagingLock(stagingDir)
413
434
  try {
414
435
  let downloadOk = false
436
+ let lastDownloadErr = null
415
437
  try {
416
- await downloadWithRetry(url, tarPath)
417
- await verifySha256(tarPath, sha256)
418
- downloadOk = true
438
+ for (const candidateUrl of runtimeAssetUrlCandidates(url)) {
439
+ try {
440
+ if (candidateUrl !== url) {
441
+ __mixdogMemoryLog(`[runtime-fetcher] retrying runtime download from fallback release repo — ${candidateUrl}\n`)
442
+ }
443
+ await downloadWithRetry(candidateUrl, tarPath)
444
+ await verifySha256(tarPath, sha256)
445
+ downloadOk = true
446
+ break
447
+ } catch (err) {
448
+ lastDownloadErr = err
449
+ try { rmSync(tarPath, { force: true }) } catch {}
450
+ }
451
+ }
452
+ if (!downloadOk && lastDownloadErr) throw lastDownloadErr
419
453
  extractTarGz(tarPath, stagingDir, stagingDir)
420
454
  } finally {
421
455
  try { rmSync(tarPath, { force: true }) } catch {}
@@ -0,0 +1,194 @@
1
+ import crypto from 'node:crypto'
2
+
3
+ // Side-effect-free helpers for ingest_session (recall fast-track hydration).
4
+ // Extracted from memory/index.mjs so the pure logic (stable identity, sensitive
5
+ // redaction, role/content shaping) can be unit-tested without importing the
6
+ // MCP server entrypoint and its heavy boot-time side effects.
7
+
8
+ // Roles we persist from an in-memory session transcript. Map provider/runtime
9
+ // role spellings onto the canonical set; unknown roles are dropped.
10
+ const INGEST_SESSION_ROLES = new Set(['user', 'assistant', 'tool', 'system', 'developer'])
11
+
12
+ export function normalizeIngestRole(role) {
13
+ const raw = String(role || '').trim().toLowerCase()
14
+ if (!raw) return null
15
+ if (raw === 'tool_result' || raw === 'function' || raw === 'tool-result') return 'tool'
16
+ if (raw === 'human') return 'user'
17
+ if (raw === 'ai' || raw === 'model') return 'assistant'
18
+ return INGEST_SESSION_ROLES.has(raw) ? raw : null
19
+ }
20
+
21
+ // Extract the first textual content block from a message content field.
22
+ export function firstTextContent(content) {
23
+ if (typeof content === 'string') return content
24
+ if (!Array.isArray(content)) return ''
25
+ for (const item of content) {
26
+ if (typeof item === 'string') return item
27
+ if (item?.type === 'text' && typeof item.text === 'string') return item.text
28
+ }
29
+ return ''
30
+ }
31
+
32
+ // Collect durable tool-call / tool-result ids for identity + pairing.
33
+ function toolIdentityIds(m) {
34
+ const ids = []
35
+ if (Array.isArray(m?.toolCalls)) {
36
+ for (const tc of m.toolCalls) { if (tc?.id) ids.push(String(tc.id)) }
37
+ }
38
+ if (m?.toolCallId) ids.push(String(m.toolCallId))
39
+ return ids
40
+ }
41
+
42
+ // Build a stable, content-derived source_ref for a session message so the
43
+ // ON CONFLICT DO NOTHING insert is idempotent across compaction reindexing.
44
+ //
45
+ // Identity hashes ONLY durable/original fields: role, tool-call/result ids, the
46
+ // ORIGINAL message timestamp (m.ts / m.timestamp) when actually present, and the
47
+ // shaped content. It never folds in a synthesized Date.now() fallback ts or the
48
+ // volatile loop index, so the same untimestamped message produces the same ref
49
+ // regardless of its position after compaction shrinks/reindexes the transcript.
50
+ // Two textually identical untimestamped plain messages intentionally dedupe to
51
+ // one row (stable dedupe preferred over positional separation).
52
+ export function stableSessionSourceRef(sessionId, m, role, content) {
53
+ const toolIds = toolIdentityIds(m)
54
+ // Only an ORIGINAL, caller-supplied timestamp counts as durable identity.
55
+ const rawTs = m?.ts ?? m?.timestamp
56
+ const originalTs = (typeof rawTs === 'number' && Number.isFinite(rawTs)) || (typeof rawTs === 'string' && rawTs.trim())
57
+ ? String(rawTs)
58
+ : ''
59
+ const identity = [role, originalTs, toolIds.join(','), content].join('\u0000')
60
+ const hash = crypto.createHash('sha256').update(identity).digest('hex').slice(0, 24)
61
+ return `session:${sessionId}:${hash}`
62
+ }
63
+
64
+ // Monotonic source_turn allocator for ingest_session. source_turn must be a
65
+ // running continuation order INDEPENDENT of the current (post-compaction)
66
+ // array index: a freshly appended turn must sort AFTER every previously
67
+ // ingested row, even though compaction reset its array index to a low value.
68
+ // Seed `prevMaxTurn` from MAX(source_turn) for the session, then call
69
+ // next() once per actually-inserted row. Re-ingested (ON CONFLICT) rows keep
70
+ // their original turn and must NOT advance the counter, so the caller only
71
+ // invokes next() when a row was truly inserted.
72
+ export function createIngestTurnAllocator(prevMaxTurn = 0) {
73
+ let turn = Number.isFinite(Number(prevMaxTurn)) ? Math.max(0, Math.floor(Number(prevMaxTurn))) : 0
74
+ return {
75
+ peekNext() { return turn + 1 },
76
+ next() { turn += 1; return turn },
77
+ current() { return turn },
78
+ }
79
+ }
80
+
81
+ // Sensitive tool-arg key matcher (mirrors compact.mjs SENSITIVE_TOOL_ARG_KEY_RE):
82
+ // api_key/authorization/auth/cookie/credential/password/secret/token, etc.
83
+ const SENSITIVE_ARG_KEY_RE = /(?:^|[_-])(?:api[_-]?key|authorization|auth|cookie|credential|passwd|password|refresh[_-]?token|secret|token)(?:$|[_-])/i
84
+ const SENSITIVE_KEY_WORD = '(?:api[_-]?key|authorization|auth|cookie|credential|passwd|password|refresh[_-]?token|secret|token)'
85
+ // Full key matcher: the sensitive WORD may carry a prefix and/or suffix segment
86
+ // joined by `_`/`-` so prefixed variants like `access_token`, `access-token`,
87
+ // `x-api-key`, and `bearer_token` are matched as whole keys (not just the bare
88
+ // word at key start). Mirrors compact.mjs SENSITIVE_TOOL_ARG_KEY_FULL.
89
+ const SENSITIVE_KEY_FULL = `(?:[A-Za-z0-9_-]*[_-])?${SENSITIVE_KEY_WORD}(?:[_-][A-Za-z0-9_-]*)?`
90
+ const TOOL_ARG_MAX_CHARS = 400
91
+
92
+ // Recursively redact sensitive values by key. Sensitive keys collapse to
93
+ // [redacted] regardless of value shape so nested secrets never survive.
94
+ export function redactToolArgValue(value, key = '', depth = 0) {
95
+ if (SENSITIVE_ARG_KEY_RE.test(String(key || ''))) return '[redacted]'
96
+ if (value == null) return value
97
+ if (typeof value === 'bigint') return String(value)
98
+ // Defense-in-depth: a non-sensitive KEY can still carry a secret embedded in
99
+ // its string VALUE (e.g. `{ headers: "authorization: Bearer ..." }`). Run raw
100
+ // key:value redaction on string values so embedded secrets never survive.
101
+ if (typeof value === 'string') return redactRawArgString(value)
102
+ if (typeof value !== 'object') return value
103
+ if (depth >= 4) return Array.isArray(value) ? `[array:${value.length}]` : '[object]'
104
+ if (Array.isArray(value)) {
105
+ return value.slice(0, 8).map((item, idx) => redactToolArgValue(item, String(idx), depth + 1))
106
+ }
107
+ const out = {}
108
+ for (const k of Object.keys(value)) out[k] = redactToolArgValue(value[k], k, depth + 1)
109
+ return out
110
+ }
111
+
112
+ // Redact `key: value` / `key=value` pairs inside a raw (non-JSON) arg string.
113
+ // Unlike a single greedy regex, this consumes the WHOLE secret value after the
114
+ // key — including spaces, `Bearer ` prefixes, quoted values with internal
115
+ // spaces, and `;`-separated cookie pairs — so no secret fragment leaks.
116
+ function redactRawArgString(text) {
117
+ const keyRe = new RegExp(`((?:^|[\\s,{(])["']?${SENSITIVE_KEY_FULL}["']?\\s*[:=]\\s*)`, 'gi')
118
+ let out = ''
119
+ let last = 0
120
+ let match
121
+ while ((match = keyRe.exec(text)) !== null) {
122
+ const prefixEnd = match.index + match[0].length
123
+ out += text.slice(last, prefixEnd)
124
+ // Determine where the secret value ends. Quoted values run to the closing
125
+ // quote; unquoted values run to the next separator that ends a field. For
126
+ // Authorization we also swallow a leading `Bearer `/`Basic ` scheme word so
127
+ // nothing after it survives.
128
+ let i = prefixEnd
129
+ const quote = text[i] === '"' || text[i] === "'" ? text[i] : ''
130
+ if (quote) {
131
+ i += 1
132
+ while (i < text.length && text[i] !== quote) i += 1
133
+ if (i < text.length) i += 1 // include closing quote
134
+ } else {
135
+ // Unquoted: stop at a comma, closing brace/paren, or newline. Spaces and
136
+ // `;` inside the value are part of the secret (Bearer tokens, cookies).
137
+ while (i < text.length && !/[,)}\n]/.test(text[i])) i += 1
138
+ }
139
+ out += '[redacted]'
140
+ last = i
141
+ keyRe.lastIndex = i
142
+ }
143
+ out += text.slice(last)
144
+ return out
145
+ }
146
+
147
+ // Produce a readable, redacted, length-capped string for tool-call arguments.
148
+ // Object/JSON args are walked key-by-key so nested secrets are caught; a raw
149
+ // non-JSON string has its key:value secret pairs redacted before truncation.
150
+ export function redactToolArgString(rawArgs) {
151
+ if (rawArgs == null) return ''
152
+ let parsed = rawArgs
153
+ if (typeof rawArgs === 'string') {
154
+ const trimmed = rawArgs.trim()
155
+ if (/^[[{]/.test(trimmed)) {
156
+ try { parsed = JSON.parse(trimmed) } catch { parsed = trimmed }
157
+ } else {
158
+ parsed = trimmed
159
+ }
160
+ }
161
+ let out
162
+ if (parsed && typeof parsed === 'object') {
163
+ try { out = JSON.stringify(redactToolArgValue(parsed)) }
164
+ catch { out = '[unserializable args]' }
165
+ } else {
166
+ out = redactRawArgString(String(parsed))
167
+ }
168
+ return out.slice(0, TOOL_ARG_MAX_CHARS)
169
+ }
170
+
171
+ // Build a readable, role-aware content string for a session message so the
172
+ // structured handoff preserves assistant tool_calls and tool_result bodies
173
+ // (not just plain text). Keeps valid tool-call/tool-result pairing legible by
174
+ // tagging each with its toolCallId, while redacting sensitive argument values.
175
+ export function sessionMessageContent(m) {
176
+ const parts = []
177
+ const base = firstTextContent(m?.content)
178
+ if (base && base.trim()) parts.push(base.trim())
179
+ if (m?.role === 'assistant' && Array.isArray(m.toolCalls) && m.toolCalls.length) {
180
+ for (const tc of m.toolCalls.slice(0, 8)) {
181
+ const name = tc?.name || tc?.function?.name || tc?.id || 'tool'
182
+ const id = tc?.id ? ` id=${tc.id}` : ''
183
+ const argStr = redactToolArgString(tc?.arguments ?? tc?.function?.arguments)
184
+ parts.push(`[tool_call ${name}${id}]${argStr ? ` ${argStr}` : ''}`)
185
+ }
186
+ }
187
+ const isTool = m?.role === 'tool' || normalizeIngestRole(m?.role) === 'tool'
188
+ if (isTool && m?.toolCallId && parts.length) {
189
+ parts[0] = `[tool_result id=${m.toolCallId}] ${parts[0]}`
190
+ } else if (isTool && m?.toolCallId) {
191
+ parts.push(`[tool_result id=${m.toolCallId}]`)
192
+ }
193
+ return parts.join('\n')
194
+ }