mixdog 0.8.1 → 0.9.1

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 (299) hide show
  1. package/README.md +47 -23
  2. package/package.json +34 -28
  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 -29
  62. package/src/lib/plugin-paths.cjs +1 -7
  63. package/src/lib/rules-builder.cjs +242 -98
  64. package/src/lib/text-utils.cjs +1 -1
  65. package/src/mixdog-session-runtime.mjs +2211 -446
  66. package/src/output-styles/default.md +12 -28
  67. package/src/output-styles/extreme-simple.md +9 -6
  68. package/src/output-styles/simple.md +22 -9
  69. package/src/repl.mjs +108 -59
  70. package/src/rules/agent/00-common.md +15 -0
  71. package/src/rules/{bridge → agent}/20-skip-protocol.md +1 -2
  72. package/src/rules/agent/30-explorer.md +22 -0
  73. package/src/rules/{bridge → agent}/40-cycle1-agent.md +7 -0
  74. package/src/rules/{bridge → agent}/41-cycle2-agent.md +7 -0
  75. package/src/rules/{bridge → agent}/42-cycle3-agent.md +7 -0
  76. package/src/rules/lead/01-general.md +9 -5
  77. package/src/rules/lead/04-workflow.md +51 -12
  78. package/src/rules/lead/lead-tool.md +6 -0
  79. package/src/rules/shared/01-tool.md +12 -1
  80. package/src/runtime/agent/orchestrator/activity-bus.mjs +7 -18
  81. package/src/runtime/agent/orchestrator/agent-owner.mjs +11 -0
  82. package/src/runtime/agent/orchestrator/{smart-bridge/bridge-llm.mjs → agent-runtime/agent-dispatch.mjs} +138 -111
  83. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +94 -0
  84. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/cache-strategy.mjs +32 -23
  85. package/src/runtime/agent/orchestrator/{smart-bridge → agent-runtime}/session-builder.mjs +33 -27
  86. package/src/runtime/agent/orchestrator/{bridge-trace.mjs → agent-trace.mjs} +132 -81
  87. package/src/runtime/agent/orchestrator/cache-mtime.mjs +0 -21
  88. package/src/runtime/agent/orchestrator/config.mjs +174 -55
  89. package/src/runtime/agent/orchestrator/context/collect.mjs +195 -487
  90. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +1 -1
  91. package/src/runtime/agent/orchestrator/internal-roles.mjs +77 -29
  92. package/src/runtime/agent/orchestrator/internal-tools.mjs +5 -6
  93. package/src/runtime/agent/orchestrator/mcp/client.mjs +15 -9
  94. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -1
  95. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +380 -268
  96. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +131 -93
  97. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +236 -4
  98. package/src/runtime/agent/orchestrator/providers/custom-tool-wire.mjs +49 -0
  99. package/src/runtime/agent/orchestrator/providers/gemini.mjs +58 -13
  100. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +67 -149
  101. package/src/runtime/agent/orchestrator/providers/media-normalization.mjs +132 -2
  102. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +4 -1
  103. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +45 -0
  104. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +61 -116
  105. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +25 -0
  106. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +79 -255
  107. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +203 -71
  108. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +512 -312
  109. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +343 -518
  110. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +6 -6
  111. package/src/runtime/agent/orchestrator/providers/registry.mjs +88 -51
  112. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +289 -11
  113. package/src/runtime/agent/orchestrator/providers/statusline-route-meta.mjs +41 -8
  114. package/src/runtime/agent/orchestrator/session/compact.mjs +1173 -267
  115. package/src/runtime/agent/orchestrator/session/context-utils.mjs +199 -36
  116. package/src/runtime/agent/orchestrator/session/loop.mjs +840 -612
  117. package/src/runtime/agent/orchestrator/session/manager.mjs +1574 -459
  118. package/src/runtime/agent/orchestrator/session/manager.reactive-persist.test.mjs +107 -0
  119. package/src/runtime/agent/orchestrator/session/store.mjs +67 -29
  120. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +2 -2
  121. package/src/runtime/agent/orchestrator/stall-policy.mjs +31 -16
  122. package/src/runtime/agent/orchestrator/tool-loop-guard.mjs +3 -219
  123. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +35 -8
  124. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +19 -8
  125. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -53
  126. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +60 -37
  127. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +21 -2
  128. package/src/runtime/agent/orchestrator/tools/builtin/device-paths.mjs +1 -1
  129. package/src/runtime/agent/orchestrator/tools/builtin/diagnostics-tool.mjs +0 -7
  130. package/src/runtime/agent/orchestrator/tools/builtin/glob-walk.mjs +1 -3
  131. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +36 -12
  132. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +2 -0
  133. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -2
  134. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +5 -12
  135. package/src/runtime/agent/orchestrator/tools/builtin/read-image-resize.mjs +1 -1
  136. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +4 -36
  137. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +2 -40
  138. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +148 -27
  139. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +2 -2
  140. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +43 -75
  141. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +90 -20
  142. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -5
  143. package/src/runtime/agent/orchestrator/tools/code-graph-state.mjs +86 -0
  144. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +11 -11
  145. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4106 -4019
  146. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -4
  147. package/src/runtime/agent/orchestrator/tools/patch.mjs +90 -6
  148. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +6 -4
  149. package/src/runtime/agent/orchestrator/tools/result-compression.mjs +4 -4
  150. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +8 -1
  151. package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +4 -4
  152. package/src/runtime/channels/index.mjs +149 -51
  153. package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
  154. package/src/runtime/channels/lib/config.mjs +0 -1
  155. package/src/runtime/channels/lib/drop-trace.mjs +1 -1
  156. package/src/runtime/channels/lib/executor.mjs +0 -3
  157. package/src/runtime/channels/lib/memory-client.mjs +0 -38
  158. package/src/runtime/channels/lib/output-forwarder.mjs +1 -8
  159. package/src/runtime/channels/lib/runtime-paths.mjs +0 -6
  160. package/src/runtime/channels/lib/scheduler.mjs +18 -14
  161. package/src/runtime/channels/lib/session-discovery.mjs +3 -6
  162. package/src/runtime/channels/lib/tool-format.mjs +0 -1
  163. package/src/runtime/channels/lib/transcript-discovery.mjs +4 -12
  164. package/src/runtime/channels/lib/webhook.mjs +1 -1
  165. package/src/runtime/channels/tool-defs.mjs +29 -29
  166. package/src/runtime/lib/keychain-cjs.cjs +0 -1
  167. package/src/runtime/memory/data/runtime-manifest.json +6 -7
  168. package/src/runtime/memory/index.mjs +519 -118
  169. package/src/runtime/memory/lib/agent-ipc.mjs +29 -12
  170. package/src/runtime/memory/lib/core-memory-store.mjs +2 -2
  171. package/src/runtime/memory/lib/embedding-model-config.mjs +55 -0
  172. package/src/runtime/memory/lib/embedding-provider.mjs +31 -4
  173. package/src/runtime/memory/lib/embedding-worker.mjs +19 -10
  174. package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
  175. package/src/runtime/memory/lib/memory-cycle1.mjs +28 -7
  176. package/src/runtime/memory/lib/memory-cycle2.mjs +4 -5
  177. package/src/runtime/memory/lib/memory-cycle3.mjs +2 -2
  178. package/src/runtime/memory/lib/memory-ops-policy.mjs +2 -2
  179. package/src/runtime/memory/lib/memory-session-merge.mjs +38 -0
  180. package/src/runtime/memory/lib/memory.mjs +88 -9
  181. package/src/runtime/memory/lib/model-profile.mjs +1 -1
  182. package/src/runtime/memory/lib/pg/adapter.mjs +1 -1
  183. package/src/runtime/memory/lib/pg/supervisor.mjs +12 -0
  184. package/src/runtime/memory/lib/runtime-fetcher.mjs +80 -21
  185. package/src/runtime/memory/lib/session-ingest.mjs +196 -0
  186. package/src/runtime/memory/lib/trace-store.mjs +96 -51
  187. package/src/runtime/memory/tool-defs.mjs +46 -37
  188. package/src/runtime/search/index.mjs +104 -473
  189. package/src/runtime/search/lib/config.mjs +0 -4
  190. package/src/runtime/search/lib/state.mjs +1 -15
  191. package/src/runtime/search/lib/web-tools.mjs +44 -25
  192. package/src/runtime/search/tool-defs.mjs +16 -23
  193. package/src/runtime/shared/abort-controller.mjs +1 -1
  194. package/src/runtime/shared/atomic-file.mjs +4 -3
  195. package/src/runtime/shared/background-tasks.mjs +122 -11
  196. package/src/runtime/shared/child-spawn-gate.mjs +139 -0
  197. package/src/runtime/shared/config.mjs +7 -4
  198. package/src/runtime/shared/err-text.mjs +131 -4
  199. package/src/runtime/shared/llm/cost.mjs +2 -2
  200. package/src/runtime/shared/llm/http-agent.mjs +23 -7
  201. package/src/runtime/shared/llm/index.mjs +34 -11
  202. package/src/runtime/shared/llm/usage-log.mjs +4 -4
  203. package/src/runtime/shared/markdown-frontmatter.mjs +56 -0
  204. package/src/runtime/shared/singleton-owner.mjs +104 -0
  205. package/src/runtime/shared/tool-execution-contract.mjs +199 -20
  206. package/src/runtime/shared/tool-execution-contract.test.mjs +183 -0
  207. package/src/runtime/shared/tool-surface.mjs +624 -95
  208. package/src/runtime/shared/user-data-guard.mjs +0 -2
  209. package/src/standalone/agent-task-status.mjs +203 -0
  210. package/src/standalone/agent-task-status.test.mjs +76 -0
  211. package/src/standalone/agent-tool.mjs +1913 -0
  212. package/src/standalone/channel-worker.mjs +370 -14
  213. package/src/standalone/explore-tool.mjs +165 -70
  214. package/src/standalone/folder-dialog.mjs +314 -0
  215. package/src/standalone/hook-bus.mjs +898 -22
  216. package/src/standalone/memory-runtime-proxy.mjs +320 -0
  217. package/src/standalone/projects.mjs +226 -0
  218. package/src/standalone/provider-admin.mjs +41 -24
  219. package/src/standalone/seeds.mjs +1 -78
  220. package/src/standalone/usage-dashboard.mjs +96 -8
  221. package/src/tui/App.jsx +4800 -2140
  222. package/src/tui/components/AnsiText.jsx +39 -28
  223. package/src/tui/components/ContextPanel.jsx +14 -4
  224. package/src/tui/components/Markdown.jsx +43 -77
  225. package/src/tui/components/MarkdownTable.jsx +9 -184
  226. package/src/tui/components/Message.jsx +28 -11
  227. package/src/tui/components/Picker.jsx +95 -56
  228. package/src/tui/components/PromptInput.jsx +428 -239
  229. package/src/tui/components/QueuedCommands.jsx +1 -1
  230. package/src/tui/components/SlashCommandPalette.jsx +27 -21
  231. package/src/tui/components/Spinner.jsx +67 -38
  232. package/src/tui/components/StatusLine.jsx +606 -38
  233. package/src/tui/components/TextEntryPanel.jsx +128 -9
  234. package/src/tui/components/ToolExecution.jsx +597 -362
  235. package/src/tui/components/TurnDone.jsx +3 -3
  236. package/src/tui/components/UsagePanel.jsx +3 -5
  237. package/src/tui/components/tool-output-format.mjs +499 -0
  238. package/src/tui/components/tool-output-format.test.mjs +312 -0
  239. package/src/tui/dist/index.mjs +9192 -2399
  240. package/src/tui/engine-runtime-notification.test.mjs +115 -0
  241. package/src/tui/engine-tool-result-text.test.mjs +75 -0
  242. package/src/tui/engine.mjs +1455 -279
  243. package/src/tui/figures.mjs +21 -40
  244. package/src/tui/index.jsx +75 -31
  245. package/src/tui/input-editing.mjs +25 -0
  246. package/src/tui/markdown/format-token.mjs +671 -69
  247. package/src/tui/markdown/format-token.test.mjs +312 -0
  248. package/src/tui/markdown/render-ansi.mjs +94 -0
  249. package/src/tui/markdown/render-ansi.test.mjs +108 -0
  250. package/src/tui/markdown/stream-fence.mjs +34 -0
  251. package/src/tui/markdown/stream-fence.test.mjs +26 -0
  252. package/src/tui/markdown/table-layout.mjs +250 -0
  253. package/src/tui/paste-attachments.mjs +0 -7
  254. package/src/tui/spinner-verbs.mjs +1 -2
  255. package/src/tui/statusline-ansi-bridge.mjs +172 -0
  256. package/src/tui/statusline-ansi-bridge.test.mjs +159 -0
  257. package/src/tui/theme.mjs +756 -24
  258. package/src/tui/time-format.mjs +1 -1
  259. package/src/tui/transcript-tool-failures.mjs +67 -0
  260. package/src/tui/transcript-tool-failures.test.mjs +111 -0
  261. package/src/ui/ansi.mjs +1 -2
  262. package/src/ui/markdown.mjs +85 -26
  263. package/src/ui/markdown.test.mjs +70 -0
  264. package/src/ui/model-display.mjs +121 -0
  265. package/src/ui/session-stats.mjs +44 -0
  266. package/src/ui/statusline-context-label.test.mjs +15 -0
  267. package/src/ui/statusline.mjs +386 -178
  268. package/src/ui/tool-card.mjs +2 -13
  269. package/src/vendor/statusline/bin/statusline-lib.mjs +1 -620
  270. package/src/vendor/statusline/bin/statusline-route.mjs +169 -37
  271. package/src/vendor/statusline/bin/statusline-route.test.mjs +80 -0
  272. package/src/vendor/statusline/scripts/lib/gateway-settings.mjs +3 -3
  273. package/src/vendor/statusline/src/gateway/claude-current.mjs +1 -1
  274. package/src/vendor/statusline/src/gateway/route-meta.mjs +44 -6
  275. package/src/vendor/statusline/src/gateway/session-routes.mjs +1 -1
  276. package/src/workflows/default/WORKFLOW.md +12 -5
  277. package/src/workflows/default/workflow.json +0 -1
  278. package/src/workflows/solo/WORKFLOW.md +15 -0
  279. package/src/workflows/solo/workflow.json +7 -0
  280. package/vendor/ink/build/ink.js +54 -8
  281. package/vendor/ink/build/output.js +6 -1
  282. package/src/agents/scheduler-task.md +0 -3
  283. package/src/agents/web-researcher/AGENT.md +0 -3
  284. package/src/agents/web-researcher/agent.json +0 -6
  285. package/src/agents/webhook-handler.md +0 -3
  286. package/src/hooks/lib/permission-rules.cjs +0 -170
  287. package/src/hooks/lib/settings-loader.cjs +0 -112
  288. package/src/lib/hook-pipe-path.cjs +0 -10
  289. package/src/rules/bridge/00-common.md +0 -5
  290. package/src/rules/bridge/30-explorer.md +0 -4
  291. package/src/rules/lead/00-tool-lead.md +0 -5
  292. package/src/rules/shared/00-language.md +0 -3
  293. package/src/runtime/agent/orchestrator/tools/builtin/native-edit-runner.mjs +0 -110
  294. package/src/runtime/agent/orchestrator/tools/mutation-content-cache.mjs +0 -67
  295. package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
  296. package/src/runtime/memory/lib/bridge-trace-queries.mjs +0 -120
  297. package/src/runtime/shared/llm/pid-cleanup.mjs +0 -27
  298. package/src/standalone/bridge-tool.mjs +0 -1414
  299. 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
@@ -47,6 +48,15 @@ function platformKey() {
47
48
  return `${os}-${process.arch}`
48
49
  }
49
50
 
51
+ function platformKeyCandidates() {
52
+ const primary = platformKey()
53
+ const candidates = [primary]
54
+ if (process.platform === 'win32' && process.arch === 'arm64') {
55
+ candidates.push('win32-x64')
56
+ }
57
+ return candidates
58
+ }
59
+
50
60
  // Fail-closed asset validation. A selected manifest asset is usable only if it
51
61
  // is not explicitly marked unsupported AND carries a real downloadable payload:
52
62
  // non-empty url, a well-formed 64-hex sha256, and a positive integer size.
@@ -154,6 +164,26 @@ async function downloadWithRetry(url, destPath) {
154
164
  throw lastErr
155
165
  }
156
166
 
167
+ function runtimeAssetUrlCandidates(url) {
168
+ const value = String(url || '')
169
+ const out = []
170
+ const add = (candidate) => {
171
+ if (candidate && !out.includes(candidate)) out.push(candidate)
172
+ }
173
+ add(value)
174
+ const match = /^https:\/\/github\.com\/([^/]+\/[^/]+)\/releases\/download\//i.exec(value)
175
+ if (!match) return out
176
+ const releaseRepo = match[1]
177
+ const overrides = String(process.env.MIXDOG_RUNTIME_RELEASE_REPOSITORY || process.env.RUNTIME_RELEASE_REPOSITORY || '')
178
+ .split(/[\s,;]+/u)
179
+ .map((repo) => repo.trim())
180
+ .filter(Boolean)
181
+ for (const repo of [...overrides, LEGACY_RUNTIME_RELEASE_REPOSITORY]) {
182
+ if (repo && repo !== releaseRepo) add(value.replace(`/github.com/${releaseRepo}/`, `/github.com/${repo}/`))
183
+ }
184
+ return out
185
+ }
186
+
157
187
  // ---------------------------------------------------------------------------
158
188
  // Tar entry path validation + extraction
159
189
  // ---------------------------------------------------------------------------
@@ -344,25 +374,35 @@ export async function ensureRuntime(dataDir) {
344
374
  gcRuntimeDir(runtimeBaseDir, readActiveVersion(runtimeBaseDir))
345
375
 
346
376
  const manifest = await loadManifest(key)
347
- const pkey = platformKey()
348
- const asset = manifest.assets?.[pkey]
349
- if (!asset) {
350
- // Platform/arch absent from the manifest entirely (e.g. win32-arm64).
351
- // The memory PG runtime cannot start here; fail with a single clear,
352
- // actionable message. The memory worker's init().catch reports this as
353
- // degraded and the rest of mixdog (agent, tools) keeps working without
354
- // memory.
355
- const supported = Object.keys(manifest.assets || {})
356
- .filter((k) => isUsableAsset(manifest.assets[k]))
357
- .join(', ') || '(none)'
358
- throw new Error(
359
- `[runtime-fetcher] memory runtime not available on ${pkey}: ` +
360
- `no runtime asset for this platform/arch in the manifest. ` +
361
- `Supported: ${supported}. ` +
362
- `Memory is disabled on this platform; the rest of mixdog continues to work.`
363
- )
377
+ const pkey = platformKey()
378
+ let selectedKey = null
379
+ let asset = null
380
+ for (const candidateKey of platformKeyCandidates()) {
381
+ const candidateAsset = manifest.assets?.[candidateKey]
382
+ if (isUsableAsset(candidateAsset)) {
383
+ selectedKey = candidateKey
384
+ asset = candidateAsset
385
+ break
386
+ }
364
387
  }
365
- if (!isUsableAsset(asset)) {
388
+ if (!asset) {
389
+ const primaryAsset = manifest.assets?.[pkey]
390
+ if (!primaryAsset) {
391
+ // Platform/arch absent from the manifest entirely (e.g. an exotic arch).
392
+ // The memory PG runtime cannot start here; fail with a single clear,
393
+ // actionable message. The memory worker's init().catch reports this as
394
+ // degraded and the rest of mixdog (agent, tools) keeps working without
395
+ // memory.
396
+ const supported = Object.keys(manifest.assets || {})
397
+ .filter((k) => isUsableAsset(manifest.assets[k]))
398
+ .join(', ') || '(none)'
399
+ throw new Error(
400
+ `[runtime-fetcher] memory runtime not available on ${pkey}: ` +
401
+ `no runtime asset for this platform/arch in the manifest. ` +
402
+ `Supported: ${supported}. ` +
403
+ `Memory is disabled on this platform; the rest of mixdog continues to work.`
404
+ )
405
+ }
366
406
  // Platform/arch present but explicitly marked unsupported or carrying a
367
407
  // placeholder/TBD payload (e.g. linux-arm64). Same graceful-degrade path.
368
408
  throw new Error(
@@ -372,6 +412,12 @@ export async function ensureRuntime(dataDir) {
372
412
  )
373
413
  }
374
414
 
415
+ if (selectedKey !== pkey) {
416
+ __mixdogMemoryLog(
417
+ '[runtime-fetcher] win32-arm64 has no native runtime; using win32-x64 under emulation\n'
418
+ )
419
+ }
420
+
375
421
  const { url, sha256, size } = asset
376
422
  const version = `pg${manifest.pg?.major}.${manifest.pg?.minor}+pgvector-${manifest.pgvector?.version}`
377
423
 
@@ -412,10 +458,23 @@ export async function ensureRuntime(dataDir) {
412
458
  const stagingLock = acquireStagingLock(stagingDir)
413
459
  try {
414
460
  let downloadOk = false
461
+ let lastDownloadErr = null
415
462
  try {
416
- await downloadWithRetry(url, tarPath)
417
- await verifySha256(tarPath, sha256)
418
- downloadOk = true
463
+ for (const candidateUrl of runtimeAssetUrlCandidates(url)) {
464
+ try {
465
+ if (candidateUrl !== url) {
466
+ __mixdogMemoryLog(`[runtime-fetcher] retrying runtime download from fallback release repo — ${candidateUrl}\n`)
467
+ }
468
+ await downloadWithRetry(candidateUrl, tarPath)
469
+ await verifySha256(tarPath, sha256)
470
+ downloadOk = true
471
+ break
472
+ } catch (err) {
473
+ lastDownloadErr = err
474
+ try { rmSync(tarPath, { force: true }) } catch {}
475
+ }
476
+ }
477
+ if (!downloadOk && lastDownloadErr) throw lastDownloadErr
419
478
  extractTarGz(tarPath, stagingDir, stagingDir)
420
479
  } finally {
421
480
  try { rmSync(tarPath, { force: true }) } catch {}
@@ -0,0 +1,196 @@
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 (conversation only).
9
+ // Map provider/runtime spellings onto canonical roles; only user/assistant are
10
+ // kept so recall-fasttrack memory does not duplicate protected system prefix.
11
+ const INGEST_SESSION_ROLES = new Set(['user', 'assistant'])
12
+
13
+ export function normalizeIngestRole(role) {
14
+ const raw = String(role || '').trim().toLowerCase()
15
+ if (!raw) return null
16
+ let canonical = raw
17
+ if (raw === 'tool_result' || raw === 'function' || raw === 'tool-result') canonical = 'tool'
18
+ else if (raw === 'human') canonical = 'user'
19
+ else if (raw === 'ai' || raw === 'model') canonical = 'assistant'
20
+ return INGEST_SESSION_ROLES.has(canonical) ? canonical : null
21
+ }
22
+
23
+ // Extract the first textual content block from a message content field.
24
+ export function firstTextContent(content) {
25
+ if (typeof content === 'string') return content
26
+ if (!Array.isArray(content)) return ''
27
+ for (const item of content) {
28
+ if (typeof item === 'string') return item
29
+ if (item?.type === 'text' && typeof item.text === 'string') return item.text
30
+ }
31
+ return ''
32
+ }
33
+
34
+ // Collect durable tool-call / tool-result ids for identity + pairing.
35
+ function toolIdentityIds(m) {
36
+ const ids = []
37
+ if (Array.isArray(m?.toolCalls)) {
38
+ for (const tc of m.toolCalls) { if (tc?.id) ids.push(String(tc.id)) }
39
+ }
40
+ if (m?.toolCallId) ids.push(String(m.toolCallId))
41
+ return ids
42
+ }
43
+
44
+ // Build a stable, content-derived source_ref for a session message so the
45
+ // ON CONFLICT DO NOTHING insert is idempotent across compaction reindexing.
46
+ //
47
+ // Identity hashes ONLY durable/original fields: role, tool-call/result ids, the
48
+ // ORIGINAL message timestamp (m.ts / m.timestamp) when actually present, and the
49
+ // shaped content. It never folds in a synthesized Date.now() fallback ts or the
50
+ // volatile loop index, so the same untimestamped message produces the same ref
51
+ // regardless of its position after compaction shrinks/reindexes the transcript.
52
+ // Two textually identical untimestamped plain messages intentionally dedupe to
53
+ // one row (stable dedupe preferred over positional separation).
54
+ export function stableSessionSourceRef(sessionId, m, role, content) {
55
+ const toolIds = toolIdentityIds(m)
56
+ // Only an ORIGINAL, caller-supplied timestamp counts as durable identity.
57
+ const rawTs = m?.ts ?? m?.timestamp
58
+ const originalTs = (typeof rawTs === 'number' && Number.isFinite(rawTs)) || (typeof rawTs === 'string' && rawTs.trim())
59
+ ? String(rawTs)
60
+ : ''
61
+ const identity = [role, originalTs, toolIds.join(','), content].join('\u0000')
62
+ const hash = crypto.createHash('sha256').update(identity).digest('hex').slice(0, 24)
63
+ return `session:${sessionId}:${hash}`
64
+ }
65
+
66
+ // Monotonic source_turn allocator for ingest_session. source_turn must be a
67
+ // running continuation order INDEPENDENT of the current (post-compaction)
68
+ // array index: a freshly appended turn must sort AFTER every previously
69
+ // ingested row, even though compaction reset its array index to a low value.
70
+ // Seed `prevMaxTurn` from MAX(source_turn) for the session, then call
71
+ // next() once per actually-inserted row. Re-ingested (ON CONFLICT) rows keep
72
+ // their original turn and must NOT advance the counter, so the caller only
73
+ // invokes next() when a row was truly inserted.
74
+ export function createIngestTurnAllocator(prevMaxTurn = 0) {
75
+ let turn = Number.isFinite(Number(prevMaxTurn)) ? Math.max(0, Math.floor(Number(prevMaxTurn))) : 0
76
+ return {
77
+ peekNext() { return turn + 1 },
78
+ next() { turn += 1; return turn },
79
+ current() { return turn },
80
+ }
81
+ }
82
+
83
+ // Sensitive tool-arg key matcher (mirrors compact.mjs SENSITIVE_TOOL_ARG_KEY_RE):
84
+ // api_key/authorization/auth/cookie/credential/password/secret/token, etc.
85
+ const SENSITIVE_ARG_KEY_RE = /(?:^|[_-])(?:api[_-]?key|authorization|auth|cookie|credential|passwd|password|refresh[_-]?token|secret|token)(?:$|[_-])/i
86
+ const SENSITIVE_KEY_WORD = '(?:api[_-]?key|authorization|auth|cookie|credential|passwd|password|refresh[_-]?token|secret|token)'
87
+ // Full key matcher: the sensitive WORD may carry a prefix and/or suffix segment
88
+ // joined by `_`/`-` so prefixed variants like `access_token`, `access-token`,
89
+ // `x-api-key`, and `bearer_token` are matched as whole keys (not just the bare
90
+ // word at key start). Mirrors compact.mjs SENSITIVE_TOOL_ARG_KEY_FULL.
91
+ const SENSITIVE_KEY_FULL = `(?:[A-Za-z0-9_-]*[_-])?${SENSITIVE_KEY_WORD}(?:[_-][A-Za-z0-9_-]*)?`
92
+ const TOOL_ARG_MAX_CHARS = 400
93
+
94
+ // Recursively redact sensitive values by key. Sensitive keys collapse to
95
+ // [redacted] regardless of value shape so nested secrets never survive.
96
+ export function redactToolArgValue(value, key = '', depth = 0) {
97
+ if (SENSITIVE_ARG_KEY_RE.test(String(key || ''))) return '[redacted]'
98
+ if (value == null) return value
99
+ if (typeof value === 'bigint') return String(value)
100
+ // Defense-in-depth: a non-sensitive KEY can still carry a secret embedded in
101
+ // its string VALUE (e.g. `{ headers: "authorization: Bearer ..." }`). Run raw
102
+ // key:value redaction on string values so embedded secrets never survive.
103
+ if (typeof value === 'string') return redactRawArgString(value)
104
+ if (typeof value !== 'object') return value
105
+ if (depth >= 4) return Array.isArray(value) ? `[array:${value.length}]` : '[object]'
106
+ if (Array.isArray(value)) {
107
+ return value.slice(0, 8).map((item, idx) => redactToolArgValue(item, String(idx), depth + 1))
108
+ }
109
+ const out = {}
110
+ for (const k of Object.keys(value)) out[k] = redactToolArgValue(value[k], k, depth + 1)
111
+ return out
112
+ }
113
+
114
+ // Redact `key: value` / `key=value` pairs inside a raw (non-JSON) arg string.
115
+ // Unlike a single greedy regex, this consumes the WHOLE secret value after the
116
+ // key — including spaces, `Bearer ` prefixes, quoted values with internal
117
+ // spaces, and `;`-separated cookie pairs — so no secret fragment leaks.
118
+ function redactRawArgString(text) {
119
+ const keyRe = new RegExp(`((?:^|[\\s,{(])["']?${SENSITIVE_KEY_FULL}["']?\\s*[:=]\\s*)`, 'gi')
120
+ let out = ''
121
+ let last = 0
122
+ let match
123
+ while ((match = keyRe.exec(text)) !== null) {
124
+ const prefixEnd = match.index + match[0].length
125
+ out += text.slice(last, prefixEnd)
126
+ // Determine where the secret value ends. Quoted values run to the closing
127
+ // quote; unquoted values run to the next separator that ends a field. For
128
+ // Authorization we also swallow a leading `Bearer `/`Basic ` scheme word so
129
+ // nothing after it survives.
130
+ let i = prefixEnd
131
+ const quote = text[i] === '"' || text[i] === "'" ? text[i] : ''
132
+ if (quote) {
133
+ i += 1
134
+ while (i < text.length && text[i] !== quote) i += 1
135
+ if (i < text.length) i += 1 // include closing quote
136
+ } else {
137
+ // Unquoted: stop at a comma, closing brace/paren, or newline. Spaces and
138
+ // `;` inside the value are part of the secret (Bearer tokens, cookies).
139
+ while (i < text.length && !/[,)}\n]/.test(text[i])) i += 1
140
+ }
141
+ out += '[redacted]'
142
+ last = i
143
+ keyRe.lastIndex = i
144
+ }
145
+ out += text.slice(last)
146
+ return out
147
+ }
148
+
149
+ // Produce a readable, redacted, length-capped string for tool-call arguments.
150
+ // Object/JSON args are walked key-by-key so nested secrets are caught; a raw
151
+ // non-JSON string has its key:value secret pairs redacted before truncation.
152
+ export function redactToolArgString(rawArgs) {
153
+ if (rawArgs == null) return ''
154
+ let parsed = rawArgs
155
+ if (typeof rawArgs === 'string') {
156
+ const trimmed = rawArgs.trim()
157
+ if (/^[[{]/.test(trimmed)) {
158
+ try { parsed = JSON.parse(trimmed) } catch { parsed = trimmed }
159
+ } else {
160
+ parsed = trimmed
161
+ }
162
+ }
163
+ let out
164
+ if (parsed && typeof parsed === 'object') {
165
+ try { out = JSON.stringify(redactToolArgValue(parsed)) }
166
+ catch { out = '[unserializable args]' }
167
+ } else {
168
+ out = redactRawArgString(String(parsed))
169
+ }
170
+ return out.slice(0, TOOL_ARG_MAX_CHARS)
171
+ }
172
+
173
+ // Build a readable, role-aware content string for a session message so the
174
+ // structured handoff preserves assistant tool_calls and tool_result bodies
175
+ // (not just plain text). Keeps valid tool-call/tool-result pairing legible by
176
+ // tagging each with its toolCallId, while redacting sensitive argument values.
177
+ export function sessionMessageContent(m) {
178
+ const parts = []
179
+ const base = firstTextContent(m?.content)
180
+ if (base && base.trim()) parts.push(base.trim())
181
+ if (m?.role === 'assistant' && Array.isArray(m.toolCalls) && m.toolCalls.length) {
182
+ for (const tc of m.toolCalls.slice(0, 8)) {
183
+ const name = tc?.name || tc?.function?.name || tc?.id || 'tool'
184
+ const id = tc?.id ? ` id=${tc.id}` : ''
185
+ const argStr = redactToolArgString(tc?.arguments ?? tc?.function?.arguments)
186
+ parts.push(`[tool_call ${name}${id}]${argStr ? ` ${argStr}` : ''}`)
187
+ }
188
+ }
189
+ const isTool = m?.role === 'tool' || normalizeIngestRole(m?.role) === 'tool'
190
+ if (isTool && m?.toolCallId && parts.length) {
191
+ parts[0] = `[tool_result id=${m.toolCallId}] ${parts[0]}`
192
+ } else if (isTool && m?.toolCallId) {
193
+ parts.push(`[tool_result id=${m.toolCallId}]`)
194
+ }
195
+ return parts.join('\n')
196
+ }