mixdog 0.9.147 → 0.9.149

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 (265) hide show
  1. package/package.json +23 -3
  2. package/scripts/build-voice-runtime-unix.sh +101 -0
  3. package/scripts/embedding-quality-probe.mjs +96 -0
  4. package/scripts/generate-voice-runtime-manifest.mjs +300 -0
  5. package/scripts/generate-voice-runtime-manifest.test.mjs +135 -0
  6. package/scripts/native-binary-arch.mjs +30 -13
  7. package/scripts/native-binary-arch.test.mjs +40 -4
  8. package/scripts/plan-voice-runtime-release.mjs +73 -0
  9. package/scripts/prepare-native-assets.mjs +50 -9
  10. package/scripts/prune-embedding-runtime.mjs +3 -3
  11. package/scripts/sync-voice-runtime-manifest.mjs +46 -0
  12. package/scripts/verify-release-assets.mjs +106 -6
  13. package/scripts/verify-voice-runtime-release.mjs +89 -0
  14. package/scripts/voice-runtime-config.json +52 -0
  15. package/src/app.mjs +42 -5
  16. package/src/headless-exec.mjs +58 -32
  17. package/src/headless-exec.test.mjs +74 -13
  18. package/src/rules/lead/01-general.md +2 -0
  19. package/src/rules/shared/10-tool-workflow.md +7 -4
  20. package/src/rules/shared/30-exploration.md +5 -1
  21. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +28 -9
  22. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +6 -1
  23. package/src/runtime/agent/orchestrator/config.mjs +53 -18
  24. package/src/runtime/agent/orchestrator/mcp/cancellation.test.mjs +139 -0
  25. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +34 -5
  26. package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -10
  27. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +5 -0
  28. package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +98 -0
  29. package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.test.mjs +90 -0
  30. package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +67 -16
  31. package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +4 -82
  32. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +0 -5
  33. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +62 -121
  34. package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.mjs +48 -0
  35. package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.test.mjs +215 -0
  36. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +44 -48
  37. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +54 -31
  38. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +56 -2
  39. package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +34 -2
  40. package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +61 -1
  41. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +6 -1
  42. package/src/runtime/agent/orchestrator/providers/gemini.mjs +61 -15
  43. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +120 -1
  44. package/src/runtime/agent/orchestrator/providers/lib/env-utils.mjs +9 -0
  45. package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +0 -3
  46. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +187 -84
  47. package/src/runtime/agent/orchestrator/providers/openai-codex-identity.test.mjs +32 -9
  48. package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +78 -33
  49. package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +21 -0
  50. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +36 -36
  51. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +16 -6
  52. package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +50 -213
  53. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +135 -126
  54. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +20 -11
  55. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +112 -36
  56. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +220 -50
  57. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +112 -9
  58. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +11 -0
  59. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +130 -60
  60. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -2
  61. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +28 -10
  62. package/src/runtime/agent/orchestrator/providers/registry.mjs +122 -95
  63. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +69 -3
  64. package/src/runtime/agent/orchestrator/providers/tool-stream-state.mjs +4 -22
  65. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +7 -14
  66. package/src/runtime/agent/orchestrator/session/compact-policy.test.mjs +128 -0
  67. package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +5 -0
  68. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +27 -48
  69. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +19 -14
  70. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +9 -43
  71. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +5 -1
  72. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +43 -5
  73. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +15 -2
  74. package/src/runtime/agent/orchestrator/session/manager/pending-messages-persistence.test.mjs +325 -0
  75. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +599 -195
  76. package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +22 -0
  77. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +9 -2
  78. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +2 -0
  79. package/src/runtime/agent/orchestrator/session/manager.mjs +3 -0
  80. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +26 -16
  81. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +23 -11
  82. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +118 -3
  83. package/src/runtime/agent/orchestrator/session/store-summary-reader.test.mjs +332 -0
  84. package/src/runtime/agent/orchestrator/session/store.mjs +38 -1
  85. package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +27 -6
  86. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -20
  87. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +291 -128
  88. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +46 -9
  89. package/src/runtime/agent/orchestrator/tools/builtin/edit-encoding-eol.test.mjs +224 -0
  90. package/src/runtime/agent/orchestrator/tools/builtin/edit-sequential-occupation.test.mjs +53 -3
  91. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +255 -25
  92. package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +87 -0
  93. package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +1 -1
  94. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +132 -9
  95. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +10 -0
  96. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-pattern-fanout.mjs +29 -3
  97. package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +1 -1
  98. package/src/runtime/agent/orchestrator/tools/builtin/list-tool-integrity.test.mjs +14 -0
  99. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +180 -31
  100. package/src/runtime/agent/orchestrator/tools/builtin/local-search-telemetry.mjs +4 -1
  101. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +32 -5
  102. package/src/runtime/agent/orchestrator/tools/builtin/native-search-health.test.mjs +5 -2
  103. package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +10 -1
  104. package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +38 -10
  105. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +12 -0
  106. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +31 -1
  107. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +11 -1
  108. package/src/runtime/agent/orchestrator/tools/builtin/read-windows.mjs +22 -4
  109. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +6 -8
  110. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +25 -24
  111. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +152 -133
  112. package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +114 -42
  113. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +129 -15
  114. package/src/runtime/agent/orchestrator/tools/code-graph/loose-file-target.test.mjs +72 -0
  115. package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +7 -8
  116. package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +4 -4
  117. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +201 -19
  118. package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +26 -3
  119. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +10 -1
  120. package/src/runtime/agent/orchestrator/tools/code-graph/symbol-search-scope.test.mjs +55 -0
  121. package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +115 -25
  122. package/src/runtime/agent/orchestrator/tools/code-graph/text-span-mask.test.mjs +268 -0
  123. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +3 -3
  124. package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +296 -46
  125. package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.test.mjs +104 -0
  126. package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +136 -0
  127. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +127 -38
  128. package/src/runtime/agent/orchestrator/tools/patch/engine-contract-gate.test.mjs +661 -0
  129. package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +350 -9
  130. package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +418 -44
  131. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +27 -3
  132. package/src/runtime/agent/orchestrator/tools/patch/patch-codec-eol.test.mjs +552 -0
  133. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +65 -15
  134. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  135. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +284 -109
  136. package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +51 -21
  137. package/src/runtime/agent/orchestrator/tools/shell-exec-policy.mjs +44 -27
  138. package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +6 -1
  139. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
  140. package/src/runtime/attachments/store.mjs +11 -3
  141. package/src/runtime/channels/data/voice-runtime-manifest.json +46 -70
  142. package/src/runtime/channels/lib/event-queue.mjs +51 -41
  143. package/src/runtime/channels/lib/scheduler.mjs +23 -28
  144. package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -17
  145. package/src/runtime/channels/lib/voice-runtime-fetcher.test.mjs +17 -0
  146. package/src/runtime/channels/lib/webhook.mjs +39 -20
  147. package/src/runtime/channels/lib/whisper-server.mjs +46 -1
  148. package/src/runtime/channels/lib/worker-main.mjs +3 -1
  149. package/src/runtime/media/adapters/gemini-video.mjs +20 -4
  150. package/src/runtime/media/jobs.mjs +13 -0
  151. package/src/runtime/media/renditions.mjs +7 -1
  152. package/src/runtime/media/store.mjs +42 -22
  153. package/src/runtime/memory/index.mjs +4 -9
  154. package/src/runtime/memory/lib/agent-ipc.mjs +1 -6
  155. package/src/runtime/memory/lib/archive-security.test.mjs +21 -1
  156. package/src/runtime/memory/lib/core-memory-store.mjs +28 -50
  157. package/src/runtime/memory/lib/embedding-model-config.mjs +37 -1
  158. package/src/runtime/memory/lib/embedding-model-config.test.mjs +33 -0
  159. package/src/runtime/memory/lib/embedding-provider.mjs +34 -12
  160. package/src/runtime/memory/lib/embedding-worker.mjs +37 -5
  161. package/src/runtime/memory/lib/ko-morph.mjs +46 -15
  162. package/src/runtime/memory/lib/memory-action-handlers.mjs +18 -1
  163. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -18
  164. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +1 -18
  165. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +98 -45
  166. package/src/runtime/memory/lib/memory-cycle2-shared.mjs +71 -0
  167. package/src/runtime/memory/lib/memory-cycle2-shared.test.mjs +78 -0
  168. package/src/runtime/memory/lib/memory-cycle2.mjs +37 -3
  169. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
  170. package/src/runtime/memory/lib/memory-embedding-migration.test.mjs +50 -0
  171. package/src/runtime/memory/lib/memory-recall-store.mjs +9 -24
  172. package/src/runtime/memory/lib/memory.mjs +50 -12
  173. package/src/runtime/memory/lib/pg/process.mjs +1 -10
  174. package/src/runtime/memory/lib/pg/supervisor.mjs +1 -9
  175. package/src/runtime/memory/lib/query-handlers.mjs +2 -4
  176. package/src/runtime/memory/lib/session-ingest-runtime.mjs +2 -2
  177. package/src/runtime/shared/background-tasks.mjs +15 -0
  178. package/src/runtime/shared/background-tasks.test.mjs +78 -0
  179. package/src/runtime/shared/child-guardian.mjs +2 -10
  180. package/src/runtime/shared/pid-liveness.mjs +27 -0
  181. package/src/runtime/shared/pristine-execution-contract.json +0 -1
  182. package/src/runtime/shared/runtime-root.mjs +1 -11
  183. package/src/runtime/shared/service-discovery.mjs +1 -17
  184. package/src/runtime/shared/singleton-owner.mjs +1 -11
  185. package/src/runtime/shared/staged-update.mjs +2 -12
  186. package/src/runtime/shared/tool-status.mjs +5 -1
  187. package/src/runtime/shared/tool-surface.mjs +15 -7
  188. package/src/runtime/shared/turn-snapshot-store.mjs +168 -0
  189. package/src/runtime/shared/turn-snapshot.mjs +137 -13
  190. package/src/runtime/shared/turn-worktree-snapshot.mjs +110 -18
  191. package/src/runtime/shared/webhook-session-run.mjs +16 -2
  192. package/src/runtime/web-search/lib/http-fetch.mjs +34 -4
  193. package/src/runtime/web-search/lib/state.mjs +15 -15
  194. package/src/runtime/web-search/lib/web-tools.mjs +46 -5
  195. package/src/session-runtime/context-status.mjs +4 -12
  196. package/src/session-runtime/runtime-core.mjs +20 -1
  197. package/src/session-runtime/session-lifecycle.mjs +9 -0
  198. package/src/standalone/agent-dispatch-broker.mjs +33 -2
  199. package/src/standalone/agent-tool/job-task-reconcile.mjs +78 -0
  200. package/src/standalone/agent-tool/notify.mjs +3 -1
  201. package/src/standalone/agent-tool/spawn-flow.mjs +52 -70
  202. package/src/standalone/agent-tool/tag-registry.mjs +116 -33
  203. package/src/standalone/agent-tool/tool-def.mjs +2 -2
  204. package/src/standalone/agent-tool/worker-index.mjs +8 -11
  205. package/src/standalone/agent-tool/worker-rows.mjs +4 -1
  206. package/src/standalone/agent-tool.mjs +20 -55
  207. package/src/standalone/agent-watchdog-registry.mjs +28 -4
  208. package/src/standalone/channel-client.mjs +47 -15
  209. package/src/standalone/channel-transport.mjs +44 -25
  210. package/src/standalone/daemon.mjs +31 -2
  211. package/src/standalone/hook-bus/config.mjs +25 -9
  212. package/src/standalone/hook-bus/handlers.mjs +16 -52
  213. package/src/standalone/hook-bus.mjs +29 -23
  214. package/src/standalone/memory-runtime-proxy.mjs +1 -16
  215. package/src/standalone/session-client.mjs +86 -23
  216. package/src/standalone/session-protocol.mjs +0 -2
  217. package/src/standalone/session-runtime-host-health.test.mjs +5 -4
  218. package/src/standalone/session-runtime-host.mjs +101 -6
  219. package/src/standalone/session-runtime-record.mjs +1 -5
  220. package/src/standalone/session-runtime-worker.mjs +52 -57
  221. package/src/standalone/session-service.mjs +4 -1
  222. package/src/standalone/session-transport.mjs +56 -25
  223. package/src/tui/App.jsx +40 -49
  224. package/src/tui/app/app-view.jsx +14 -21
  225. package/src/tui/app/core-memory-picker.mjs +29 -16
  226. package/src/tui/app/create-app-pickers.mjs +15 -36
  227. package/src/tui/app/extension-pickers.mjs +60 -49
  228. package/src/tui/app/maintenance-pickers.mjs +100 -57
  229. package/src/tui/app/message-selector.mjs +6 -5
  230. package/src/tui/app/model-picker.mjs +30 -13
  231. package/src/tui/app/onboarding-steps.mjs +21 -12
  232. package/src/tui/app/panel-epoch.mjs +56 -0
  233. package/src/tui/app/panel-epoch.test.mjs +1009 -0
  234. package/src/tui/app/panel-surface.mjs +98 -0
  235. package/src/tui/app/project-picker.mjs +30 -29
  236. package/src/tui/app/prompt-submit.mjs +192 -97
  237. package/src/tui/app/prompt-submit.test.mjs +206 -0
  238. package/src/tui/app/provider-setup-picker.mjs +74 -43
  239. package/src/tui/app/resume-picker.mjs +11 -8
  240. package/src/tui/app/route-pickers.mjs +45 -28
  241. package/src/tui/app/settings-picker.mjs +69 -18
  242. package/src/tui/app/shell-layout.mjs +1 -3
  243. package/src/tui/app/slash-commands.mjs +0 -2
  244. package/src/tui/app/slash-dispatch.mjs +4 -2
  245. package/src/tui/app/text-entry-policy.mjs +25 -0
  246. package/src/tui/app/theme-effort-pickers.mjs +11 -16
  247. package/src/tui/app/usage-context-panels.mjs +21 -25
  248. package/src/tui/app/use-global-key-input.mjs +3 -2
  249. package/src/tui/app/use-prompt-draft-flow.mjs +6 -18
  250. package/src/tui/app/use-prompt-handlers.mjs +3 -2
  251. package/src/tui/app/use-welcome-prompt-hint.mjs +17 -3
  252. package/src/tui/components/Picker.jsx +1 -1
  253. package/src/tui/components/TextEntryPanel.jsx +7 -2
  254. package/src/tui/dist/index.mjs +652 -561
  255. package/src/tui/lib/voice-setup.mjs +1 -2
  256. package/src/tui/session/agent-job-feed.mjs +73 -11
  257. package/src/tui/session/session-api-ext.mjs +5 -6
  258. package/src/tui/session/session-api.mjs +1 -0
  259. package/src/tui/session/tool-approval.mjs +15 -5
  260. package/src/tui/session/tui-steering-persist.mjs +33 -19
  261. package/src/tui/session/tui-steering-persist.test.mjs +104 -0
  262. package/src/tui/session-local.mjs +0 -22
  263. package/scripts/.tmp-coverage-report.mjs +0 -96
  264. package/src/runtime/agent/orchestrator/tools/builtin/shell-monitor.mjs +0 -57
  265. package/src/runtime/agent/orchestrator/tools/builtin/shell-monitor.test.mjs +0 -57
@@ -71,8 +71,11 @@ function indexSignature(path) {
71
71
  try {
72
72
  const info = statSync(path);
73
73
  return `${info.mtimeMs}:${info.ctimeMs}:${info.size}`;
74
- } catch {
75
- return 'missing';
74
+ } catch (error) {
75
+ // Only an absent file is "missing". Reporting EACCES/EIO as missing let the
76
+ // read path cache an empty catalog for a store that is actually unreadable.
77
+ if (error?.code === 'ENOENT') return 'missing';
78
+ throw error;
76
79
  }
77
80
  }
78
81
 
@@ -91,14 +94,22 @@ function readIndex() {
91
94
  indexCacheHits += 1;
92
95
  return indexCacheAssets;
93
96
  }
97
+ let raw;
94
98
  try {
95
- const raw = readFileSync(path, 'utf8');
96
- const parsed = JSON.parse(raw);
97
- indexDiskReads += 1;
98
- return rememberIndex(path, signature, Array.isArray(parsed?.assets) ? parsed.assets : []);
99
- } catch {
100
- return rememberIndex(path, signature, []);
99
+ raw = readFileSync(path, 'utf8');
100
+ } catch (error) {
101
+ // A missing index file is a genuinely empty catalog (first run). Every
102
+ // OTHER read failure is not: answering with an empty list made the next
103
+ // save/delete write that emptiness back over every existing asset entry.
104
+ if (error?.code === 'ENOENT') return rememberIndex(path, signature, []);
105
+ throw error;
101
106
  }
107
+ const parsed = JSON.parse(raw);
108
+ if (!parsed || !Array.isArray(parsed.assets)) {
109
+ throw new Error(`media index is malformed: ${path}`);
110
+ }
111
+ indexDiskReads += 1;
112
+ return rememberIndex(path, signature, parsed.assets);
102
113
  }
103
114
 
104
115
  function writeIndex(assets) {
@@ -107,6 +118,20 @@ function writeIndex(assets) {
107
118
  rememberIndex(path, indexSignature(path), assets);
108
119
  }
109
120
 
121
+ /**
122
+ * The one catalog read-modify-write. Every mutation (migrate, save, duration
123
+ * backfill, delete) holds index.lock across a fresh readIndex() and writes back
124
+ * only what the callback returns, so a concurrent CLI/desktop generation can
125
+ * never lose an entry — and a read that FAILS (rather than being absent)
126
+ * propagates out of the lock instead of writing an empty catalog over it.
127
+ */
128
+ function mutateIndex(mutate) {
129
+ withFileLockSync(indexLockPath(), () => {
130
+ const next = mutate(readIndex());
131
+ if (next) writeIndex(next);
132
+ });
133
+ }
134
+
110
135
  export function mediaStoreCacheStats() {
111
136
  return {
112
137
  indexDiskReads,
@@ -158,8 +183,7 @@ function storedAssetPath(file) {
158
183
 
159
184
  /** Move flat v1 assets into kind/provider/model/date folders without losing old indexes. */
160
185
  function migrateAssetLayout() {
161
- withFileLockSync(indexLockPath(), () => {
162
- const current = readIndex();
186
+ mutateIndex((current) => {
163
187
  let changed = false;
164
188
  const assets = current.map((entry) => {
165
189
  const targetFile = organizedAssetFile(entry);
@@ -181,7 +205,7 @@ function migrateAssetLayout() {
181
205
  return entry;
182
206
  }
183
207
  });
184
- if (changed) writeIndex(assets);
208
+ return changed ? assets : null;
185
209
  });
186
210
  }
187
211
 
@@ -213,9 +237,7 @@ export function saveMediaAsset({ kind, lane, model, prompt, options = {}, mime,
213
237
  // The gallery is NOT capped (user). An entry-count trim used to unlink the
214
238
  // oldest files here, which is data loss nobody asked for: a generated asset
215
239
  // leaves this store only through an explicit delete.
216
- withFileLockSync(indexLockPath(), () => {
217
- writeIndex([entry, ...readIndex()]);
218
- });
240
+ mutateIndex((assets) => [entry, ...assets]);
219
241
  // The first gallery paint after a generation would otherwise be the one that
220
242
  // pays for the tile rendition; build it now, off the caller's path.
221
243
  void ensureRendition({
@@ -248,12 +270,11 @@ function getMediaAsset(id) {
248
270
  * survives a restart without decoding the clip again. */
249
271
  function rememberDuration(entry, durationSeconds) {
250
272
  if (!durationSeconds || entry.durationSeconds === durationSeconds) return;
251
- withFileLockSync(indexLockPath(), () => {
252
- const assets = readIndex();
273
+ mutateIndex((assets) => {
253
274
  const row = assets.find((item) => item.id === entry.id);
254
- if (!row || row.durationSeconds === durationSeconds) return;
275
+ if (!row || row.durationSeconds === durationSeconds) return null;
255
276
  row.durationSeconds = durationSeconds;
256
- writeIndex(assets);
277
+ return assets;
257
278
  });
258
279
  entry.durationSeconds = durationSeconds;
259
280
  }
@@ -406,15 +427,14 @@ function openWithOs(path) {
406
427
  export function deleteMediaAsset(id) {
407
428
  ensureDirs();
408
429
  let removed = false;
409
- withFileLockSync(indexLockPath(), () => {
410
- const assets = readIndex();
430
+ mutateIndex((assets) => {
411
431
  const entry = assets.find((row) => row.id === id);
412
- if (!entry) return;
432
+ if (!entry) return null;
413
433
  const path = storedAssetPath(entry.file);
414
434
  try { if (path) unlinkSync(path); } catch {}
415
435
  removeRenditions(renditionsDir(), id);
416
- writeIndex(assets.filter((row) => row.id !== id));
417
436
  removed = true;
437
+ return assets.filter((row) => row.id !== id);
418
438
  });
419
439
  return { id, removed };
420
440
  }
@@ -65,7 +65,7 @@ import { retrieveEntries } from './lib/memory-retrievers.mjs'
65
65
  import { pruneOldEntries } from './lib/memory-maintenance-store.mjs'
66
66
  import { computeEntryScore } from './lib/memory-score.mjs'
67
67
  import { runFullBackfill } from './lib/memory-ops-policy.mjs'
68
- import { listCore, addCore, editCore, deleteCore, compactCoreIds, listCoreCandidates, promoteCoreCandidate, dismissCoreCandidate } from './lib/core-memory-store.mjs'
68
+ import { listCore, addCore, editCore, deleteCore, listCoreCandidates, promoteCoreCandidate, dismissCoreCandidate } from './lib/core-memory-store.mjs'
69
69
  import { refreshCoreMemoryFile } from './lib/core-memory-file.mjs'
70
70
  import { resolveProjectId, resolveProjectScope } from './lib/project-id-resolver.mjs'
71
71
  import { openTraceDatabase, closeTraceDatabase, insertTraceEvents, enqueueTraceEvents, insertAgentCalls, registerTraceExitDrain } from './lib/trace-store.mjs'
@@ -275,6 +275,7 @@ async function _initStore() {
275
275
  provider: embeddingConfig?.provider ?? null,
276
276
  model: getEmbeddingModelId(),
277
277
  dtype: getEmbeddingDtype(),
278
+ format: 'mixdog-embedding-v2',
278
279
  }
279
280
  let dimsResolved = null
280
281
  try {
@@ -300,7 +301,7 @@ async function _initStore() {
300
301
  // model and the ~560 MB Kiwi analyzer completely off the boot path; the
301
302
  // first relevant query starts them on demand without blocking startup.
302
303
  const openStartedAt = performance.now()
303
- db = await openDatabase(DATA_DIR, dimsResolved)
304
+ db = await openDatabase(DATA_DIR, dimsResolved, metaKey)
304
305
  memoryProfile('open-db:done', { ms: (performance.now() - openStartedAt).toFixed(1), dims: dimsResolved })
305
306
  } else {
306
307
  if (!embeddingWarmupCanStart()) {
@@ -314,7 +315,7 @@ async function _initStore() {
314
315
  dimsResolved = Number(getEmbeddingDims())
315
316
  assertSecondaryPgAttachable()
316
317
  const openStartedAt = performance.now()
317
- db = await openDatabase(DATA_DIR, dimsResolved)
318
+ db = await openDatabase(DATA_DIR, dimsResolved, metaKey)
318
319
  memoryProfile('open-db:done', { ms: (performance.now() - openStartedAt).toFixed(1), dims: dimsResolved })
319
320
  try {
320
321
  writeJsonAtomicSync(EMBEDDING_META_PATH, { ...metaKey, dims: dimsResolved }, { lock: true })
@@ -451,12 +452,6 @@ async function _initRuntime() {
451
452
  memoryProfile('runtime-init:start')
452
453
  await _initStore()
453
454
  memoryProfile('runtime-init:init-store-ready', { ms: (performance.now() - runtimeStartedAt).toFixed(1) })
454
- // Restore the core_entries.id == 1..N invariant once per boot: SERIAL only
455
- // increments, so deleted rows leave permanent gaps. Fast no-op when already
456
- // contiguous (or empty). Runs only here — never in cycle2/addCore/deleteCore.
457
- const compactStartedAt = performance.now()
458
- await compactCoreIds(DATA_DIR)
459
- memoryProfile('core-ids:compact:done', { ms: (performance.now() - compactStartedAt).toFixed(1) })
460
455
  // First boot migrates existing PG core data; later boots reconcile any
461
456
  // crash window between a committed PG mutation and its atomic file refresh.
462
457
  // The snapshot is not part of memory readiness.
@@ -10,6 +10,7 @@ import http from 'node:http'
10
10
  import { readFileSync } from 'node:fs'
11
11
  import { join } from 'node:path'
12
12
  import { resolveRuntimeRoot } from '../../shared/runtime-root.mjs'
13
+ import { isPidAlive } from '../../shared/pid-liveness.mjs'
13
14
 
14
15
  const brokerAgent = new http.Agent({
15
16
  keepAlive: true,
@@ -27,12 +28,6 @@ function runtimeRoot() {
27
28
  return resolveRuntimeRoot()
28
29
  }
29
30
 
30
- function isPidAlive(value) {
31
- const pid = Number(value)
32
- if (!Number.isInteger(pid) || pid <= 0) return false
33
- try { process.kill(pid, 0); return true } catch (error) { return error?.code === 'EPERM' }
34
- }
35
-
36
31
  function readBrokerDiscovery() {
37
32
  try {
38
33
  const raw = JSON.parse(readFileSync(join(runtimeRoot(), 'daemon.json'), 'utf8'))
@@ -6,11 +6,31 @@ import { tmpdir } from 'node:os'
6
6
  import { join } from 'node:path'
7
7
  import { gzipSync } from 'node:zlib'
8
8
 
9
- import { _extractTar, _verifyKiwiModelArchive } from './ko-morph.mjs'
9
+ import {
10
+ _extractTar,
11
+ _kiwiBuildArgs,
12
+ _resolveKoMorphIdleTimeout,
13
+ _verifyKiwiModelArchive,
14
+ } from './ko-morph.mjs'
10
15
  import { _validateRuntimeTarEntries } from './runtime-fetcher.mjs'
11
16
 
12
17
  const REQUIRED = ['combiningRule.txt', 'default.dict', 'extract.mdl', 'sj.knlm', 'sj.morph']
13
18
 
19
+ test('Kiwi recall build excludes optional dictionaries and releases after two minutes', () => {
20
+ const modelFiles = { 'sj.knlm': new Uint8Array([1]) }
21
+ assert.deepEqual(_kiwiBuildArgs(modelFiles), {
22
+ modelFiles,
23
+ modelType: 'knlm',
24
+ loadDefaultDict: true,
25
+ loadMultiDict: false,
26
+ loadTypoDict: false,
27
+ typos: 'none',
28
+ })
29
+ assert.equal(_resolveKoMorphIdleTimeout(undefined), 120_000)
30
+ assert.equal(_resolveKoMorphIdleTimeout('0'), 0)
31
+ assert.equal(_resolveKoMorphIdleTimeout('45000'), 45_000)
32
+ })
33
+
14
34
  function tarEntry(name, body = Buffer.alloc(0), type = '0') {
15
35
  const bytes = Buffer.from(body)
16
36
  const header = Buffer.alloc(512)
@@ -120,53 +120,11 @@ export async function backfillCoreEmbeddings(dataDir, options = {}) {
120
120
  return await _backfillNullEmbeddings(db, options)
121
121
  }
122
122
 
123
- // Boot-time invariant restoration: core_entries.id must always be the
124
- // contiguous sequence 1..N. SERIAL only ever increments, so deleting a row
125
- // leaves a permanent gap (e.g. 1,2,4,5 after deleting 3). This closes those
126
- // gaps by resequencing every row globally (across all project_id pools) to
127
- // 1..N in id order, then realigns the serial so the next INSERT continues from
128
- // N+1. Deterministic invariant restore — no heuristic, no fallback branch.
129
- export async function compactCoreIds(dataDir) {
130
- const db = _getDb(dataDir)
131
- // Fast no-op guard: a contiguous 1..N table has COUNT == MAX(id). This also
132
- // covers the empty table (n=0, mx=0) — return before any write.
133
- const g = await db.query(`SELECT COUNT(*) AS n, COALESCE(MAX(id),0) AS mx FROM core_entries`)
134
- const n = Number(g.rows[0].n)
135
- const mx = Number(g.rows[0].mx)
136
- if (n === mx) return 0
137
-
138
- // BEGIN transaction on ONE checked-out client — the pool wrapper db.query
139
- // releases the client per call, so BEGIN/COMMIT must run on one client
140
- // (same pattern as addCore).
141
- const client = await checkedConnect(db._pool, 'memory')
142
- try {
143
- await client.query('BEGIN')
144
- // (a) Vacate the low range so shifted ids can't collide with the 1..N
145
- // target. Offset by current MAX(id): every id becomes id+mx, which is
146
- // strictly greater than N (N <= mx), so the resequence below is safe.
147
- await client.query(`UPDATE core_entries SET id = id + $1`, [mx])
148
- // (b) Resequence ALL rows globally to 1..N preserving id order.
149
- await client.query(`
150
- WITH ordered AS (
151
- SELECT id, ROW_NUMBER() OVER (ORDER BY id) AS rn FROM core_entries
152
- )
153
- UPDATE core_entries c SET id = o.rn FROM ordered o WHERE c.id = o.id`)
154
- // (c) Realign the serial so the next INSERT continues from N+1. Guarded by
155
- // the n===mx return above, so MAX(id) is always > 0 here — setval(...,0,true)
156
- // is invalid and is never reached.
157
- await client.query(
158
- `SELECT setval(pg_get_serial_sequence('core_entries','id'),
159
- (SELECT COALESCE(MAX(id),0) FROM core_entries), true)`)
160
- await client.query('COMMIT')
161
- } catch (err) {
162
- try { await client.query('ROLLBACK') } catch {}
163
- throw err
164
- } finally {
165
- client.release()
166
- }
167
- __mixdogMemoryLog(`[core-memory] compacted ${n} core id(s)\n`)
168
- return n
169
- }
123
+ // NOTE: boot-time core id compaction was REMOVED. Resequencing core_entries.id
124
+ // to 1..N rewrote primary keys that are handed out as stable `core:N`
125
+ // references, so after a restart a retained reference pointed at a different
126
+ // fact. Gaps left by deletes are the correct, permanent behavior of a surrogate
127
+ // key.
170
128
 
171
129
  async function _findTopKCore(db, projectId, embedding, excludeId, { forUpdate = false } = {}) {
172
130
  if (!embedding) return []
@@ -351,10 +309,17 @@ export async function editCore(dataDir, id, patch) {
351
309
  const textChanged = newElement !== cur.element || newSummary !== cur.summary
352
310
  const projectChanged = newProjectId !== (cur.project_id ?? null)
353
311
  if (!textChanged && !projectChanged) {
354
- await db.query(
355
- `UPDATE core_entries SET category = $1, updated_at = $2 WHERE id = $3`,
356
- [newCategory, now, numId],
312
+ // Optimistic guard on the snapshot this patch was computed against: a blind
313
+ // UPDATE by id overwrote whatever a concurrent edit had just written.
314
+ const res = await db.query(
315
+ `UPDATE core_entries SET category = $1, updated_at = $2
316
+ WHERE id = $3 AND element = $4 AND summary = $5
317
+ AND updated_at IS NOT DISTINCT FROM $6::bigint`,
318
+ [newCategory, now, numId, cur.element, cur.summary, cur.updated_at],
357
319
  )
320
+ if (Number(res.rowCount ?? 0) === 0) {
321
+ throw new Error(`core entry id=${numId} changed concurrently — re-read and retry`)
322
+ }
358
323
  return { ...cur, element: newElement, summary: newSummary, category: newCategory, updated_at: now }
359
324
  }
360
325
 
@@ -371,6 +336,19 @@ export async function editCore(dataDir, id, patch) {
371
336
  for (const poolKey of new Set(poolKeys)) {
372
337
  await client.query(`SELECT pg_advisory_xact_lock(hashtext($1))`, [poolKey])
373
338
  }
339
+ // Revalidate under the lock. `cur` (and the embedding derived from it) was
340
+ // read before any locking, so without this check two concurrent edits both
341
+ // computed a patch from the same snapshot and the slower one silently
342
+ // overwrote the newer content.
343
+ const fresh = (await client.query(`SELECT * FROM core_entries WHERE id = $1 FOR UPDATE`, [numId])).rows[0]
344
+ if (!fresh) throw new Error(`no entry with id=${numId}`)
345
+ if (fresh.element !== cur.element
346
+ || fresh.summary !== cur.summary
347
+ || fresh.category !== cur.category
348
+ || (fresh.project_id ?? null) !== (cur.project_id ?? null)
349
+ || Number(fresh.updated_at ?? 0) !== Number(cur.updated_at ?? 0)) {
350
+ throw new Error(`core entry id=${numId} changed concurrently — re-read and retry`)
351
+ }
374
352
  const candidates = await _findTopKCore(client, newProjectId, embedding, numId, { forUpdate: true })
375
353
  const mergeTarget = await _resolveMergeTarget(candidates, { element: newElement, summary: newSummary })
376
354
  if (mergeTarget) {
@@ -1,17 +1,34 @@
1
- const DEFAULT_MODEL_ID = 'ibm-granite/granite-embedding-97m-multilingual-r2'
1
+ const DEFAULT_MODEL_ID = 'onnx-community/harrier-oss-v1-270m-ONNX'
2
+ const HARRIER_QUERY_PREFIX = 'Instruct: Given a web search query, retrieve relevant passages that answer the query\nQuery: '
2
3
 
3
4
  const MODEL_PROFILES = Object.freeze({
4
5
  [DEFAULT_MODEL_ID]: Object.freeze({
6
+ dims: 640,
7
+ defaultDtype: 'q4',
8
+ defaultDevice: 'cpu',
9
+ modelFileName: 'model',
10
+ outputName: 'sentence_embedding',
11
+ pooling: 'last_token',
12
+ queryPrefix: HARRIER_QUERY_PREFIX,
13
+ supportedDtypes: Object.freeze(['fp32', 'fp16', 'q8', 'q4']),
14
+ }),
15
+ 'ibm-granite/granite-embedding-97m-multilingual-r2': Object.freeze({
5
16
  dims: 384,
6
17
  defaultDtype: 'fp32',
7
18
  defaultDevice: 'cpu',
8
19
  modelFileName: 'model_quint8_avx2',
20
+ outputName: '',
21
+ pooling: 'mean',
22
+ queryPrefix: '',
9
23
  supportedDtypes: Object.freeze(['fp32']),
10
24
  }),
11
25
  'Xenova/bge-m3': Object.freeze({
12
26
  dims: 1024,
13
27
  defaultDtype: 'q4',
14
28
  defaultDevice: 'auto',
29
+ outputName: '',
30
+ pooling: 'mean',
31
+ queryPrefix: '',
15
32
  supportedDtypes: Object.freeze(['fp32', 'fp16', 'q8', 'q4']),
16
33
  }),
17
34
  })
@@ -53,3 +70,22 @@ export function getEmbeddingModelLoadOptions(modelId = getConfiguredEmbeddingMod
53
70
  const profile = getEmbeddingModelProfile(modelId)
54
71
  return profile?.modelFileName ? { model_file_name: profile.modelFileName } : {}
55
72
  }
73
+
74
+ export function getEmbeddingPooling(modelId = getConfiguredEmbeddingModelId()) {
75
+ return getEmbeddingModelProfile(modelId)?.pooling || 'mean'
76
+ }
77
+
78
+ export function getEmbeddingOutputName(modelId = getConfiguredEmbeddingModelId()) {
79
+ return getEmbeddingModelProfile(modelId)?.outputName || ''
80
+ }
81
+
82
+ export function normalizeEmbeddingInputType(inputType) {
83
+ return clean(inputType).toLowerCase() === 'query' ? 'query' : 'document'
84
+ }
85
+
86
+ export function prepareEmbeddingInput(text, inputType = 'document', modelId = getConfiguredEmbeddingModelId()) {
87
+ const cleanText = clean(text)
88
+ if (!cleanText || normalizeEmbeddingInputType(inputType) !== 'query') return cleanText
89
+ const prefix = getEmbeddingModelProfile(modelId)?.queryPrefix || ''
90
+ return `${prefix}${cleanText}`
91
+ }
@@ -0,0 +1,33 @@
1
+ import assert from 'node:assert/strict'
2
+ import test from 'node:test'
3
+
4
+ import {
5
+ getEmbeddingModelLoadOptions,
6
+ getEmbeddingOutputName,
7
+ getEmbeddingPooling,
8
+ getKnownEmbeddingDims,
9
+ normalizeEmbeddingDtype,
10
+ normalizeEmbeddingInputType,
11
+ prepareEmbeddingInput,
12
+ } from './embedding-model-config.mjs'
13
+
14
+ const HARRIER = 'onnx-community/harrier-oss-v1-270m-ONNX'
15
+
16
+ test('Harrier uses its fixed ONNX profile and last-token pooling', () => {
17
+ assert.equal(getKnownEmbeddingDims(HARRIER), 640)
18
+ assert.equal(normalizeEmbeddingDtype(HARRIER, ''), 'q4')
19
+ assert.deepEqual(getEmbeddingModelLoadOptions(HARRIER), { model_file_name: 'model' })
20
+ assert.equal(getEmbeddingOutputName(HARRIER), 'sentence_embedding')
21
+ assert.equal(getEmbeddingPooling(HARRIER), 'last_token')
22
+ })
23
+
24
+ test('Harrier applies its retrieval instruction only to queries', () => {
25
+ const text = 'Windows 음성 런타임의 메모리 사용량'
26
+ assert.equal(normalizeEmbeddingInputType('QUERY'), 'query')
27
+ assert.equal(normalizeEmbeddingInputType('passage'), 'document')
28
+ assert.equal(prepareEmbeddingInput(text, 'document', HARRIER), text)
29
+ assert.equal(
30
+ prepareEmbeddingInput(text, 'query', HARRIER),
31
+ `Instruct: Given a web search query, retrieve relevant passages that answer the query\nQuery: ${text}`,
32
+ )
33
+ })
@@ -14,6 +14,7 @@ import {
14
14
  getDefaultEmbeddingDtype,
15
15
  getKnownEmbeddingDims,
16
16
  normalizeEmbeddingDtype,
17
+ normalizeEmbeddingInputType,
17
18
  } from './embedding-model-config.mjs'
18
19
 
19
20
  const MODEL_ID = getConfiguredEmbeddingModelId()
@@ -51,6 +52,10 @@ function getCachedEmbedding(key) {
51
52
  return queryEmbeddingCache.get(key)
52
53
  }
53
54
 
55
+ function embeddingCacheKey(text, inputType) {
56
+ return `${MODEL_ID}\n${normalizeEmbeddingInputType(inputType)}\n${text}`
57
+ }
58
+
54
59
  function ensureWorker() {
55
60
  if (worker) return worker
56
61
  const now = Date.now()
@@ -143,20 +148,31 @@ function ensureWorker() {
143
148
  }
144
149
 
145
150
  const EMBED_WORKER_TIMEOUT_MS = 60_000
151
+ const EMBED_WORKER_COLD_TIMEOUT_MS = 180_000
146
152
 
147
- function sendToWorker(action, extra = {}, timeoutMs = EMBED_WORKER_TIMEOUT_MS) {
153
+ function embeddingWorkerTimeout(action) {
154
+ return !_modelReady && ['warmup', 'embed', 'embed-batch'].includes(action)
155
+ ? EMBED_WORKER_COLD_TIMEOUT_MS
156
+ : EMBED_WORKER_TIMEOUT_MS
157
+ }
158
+
159
+ function sendToWorker(action, extra = {}, timeoutMs = embeddingWorkerTimeout(action)) {
148
160
  const w = ensureWorker()
149
161
  const id = ++_msgId
150
162
  return new Promise((resolve, reject) => {
151
163
  const timer = setTimeout(() => {
152
164
  _pending.delete(id)
153
165
  __mixdogMemoryLog(`[embed] worker ${action} timed out — terminating worker\n`)
154
- if (worker) {
155
- const stuck = worker
166
+ // Retire the worker this request was actually posted to. Reading the
167
+ // module-level `worker` here killed whatever worker happened to be
168
+ // current — after a restart that is a healthy replacement, so one stuck
169
+ // request cascaded into terminating its successor.
170
+ if (worker === w) {
156
171
  worker = null
172
+ _modelReady = false
157
173
  _restartCount++
158
- stuck.terminate().catch(() => {})
159
174
  }
175
+ w.terminate().catch(() => {})
160
176
  reject(new Error(`embed worker ${action} timed out after ${timeoutMs}ms`))
161
177
  }, timeoutMs)
162
178
  _pending.set(id, {
@@ -246,13 +262,18 @@ export function warmupEmbeddingProvider() {
246
262
  export async function embedText(text, options = {}) {
247
263
  const clean = String(text ?? '').trim()
248
264
  if (!clean) return []
249
- const cacheKey = `${MODEL_ID}\n${clean}`
265
+ const inputType = normalizeEmbeddingInputType(options?.inputType)
266
+ const cacheKey = embeddingCacheKey(clean, inputType)
250
267
  const cached = getCachedEmbedding(cacheKey)
251
268
  if (cached) return [...cached]
252
269
 
253
270
  // Interactive query embeds pass { priority: true } so the worker can
254
271
  // queue-jump them ahead of background flush embed-batch work.
255
- const result = await sendToWorker('embed', { text: clean, priority: options?.priority === true })
272
+ const result = await sendToWorker('embed', {
273
+ text: clean,
274
+ inputType,
275
+ priority: options?.priority === true,
276
+ })
256
277
  if (!result.dims) throw new Error(`embed result missing dims (model=${MODEL_ID})`)
257
278
  const resultDims = result.dims
258
279
  if (cachedDims && resultDims !== cachedDims) {
@@ -288,20 +309,21 @@ export async function embedText(text, options = {}) {
288
309
  * single or batch path. Worker still serializes the single ONNX run, but
289
310
  * one batched run replaces N sequential runs.
290
311
  */
291
- export async function embedTexts(texts) {
312
+ export async function embedTexts(texts, options = {}) {
292
313
  if (!Array.isArray(texts)) throw new Error('embedTexts requires an array')
314
+ const inputType = normalizeEmbeddingInputType(options?.inputType)
293
315
  const cleaned = texts.map(t => String(t ?? '').trim())
294
316
  const missing = []
295
317
  for (const t of cleaned) {
296
318
  if (!t) continue
297
- const key = `${MODEL_ID}\n${t}`
319
+ const key = embeddingCacheKey(t, inputType)
298
320
  if (!queryEmbeddingCache.has(key)) missing.push(t)
299
321
  }
300
322
  if (missing.length === 0) return cleaned.map(t => {
301
323
  if (!t) return []
302
- return [...queryEmbeddingCache.get(`${MODEL_ID}\n${t}`)]
324
+ return [...queryEmbeddingCache.get(embeddingCacheKey(t, inputType))]
303
325
  })
304
- const result = await sendToWorker('embed-batch', { texts: missing })
326
+ const result = await sendToWorker('embed-batch', { texts: missing, inputType })
305
327
  if (!result.dims) throw new Error(`embed-batch result missing dims (model=${MODEL_ID})`)
306
328
  const resultDims = result.dims
307
329
  if (cachedDims && resultDims !== cachedDims) {
@@ -318,12 +340,12 @@ export async function embedTexts(texts) {
318
340
  if (!Array.isArray(vec) || vec.length !== cachedDims) {
319
341
  throw new Error(`embed-batch vector length mismatch at idx ${i}: expected ${cachedDims}, got ${vec?.length}`)
320
342
  }
321
- cacheEmbedding(`${MODEL_ID}\n${missing[i]}`, vec)
343
+ cacheEmbedding(embeddingCacheKey(missing[i], inputType), vec)
322
344
  }
323
345
  _embedCallCount += missing.length
324
346
  return cleaned.map(t => {
325
347
  if (!t) return []
326
- const cached = queryEmbeddingCache.get(`${MODEL_ID}\n${t}`)
348
+ const cached = queryEmbeddingCache.get(embeddingCacheKey(t, inputType))
327
349
  return cached ? [...cached] : []
328
350
  })
329
351
  }
@@ -13,7 +13,11 @@ import {
13
13
  getDefaultEmbeddingDevice,
14
14
  getDefaultEmbeddingDtype,
15
15
  getEmbeddingModelLoadOptions,
16
+ getEmbeddingOutputName,
17
+ getEmbeddingPooling,
16
18
  normalizeEmbeddingDtype,
19
+ normalizeEmbeddingInputType,
20
+ prepareEmbeddingInput,
17
21
  } from './embedding-model-config.mjs'
18
22
 
19
23
  const MODEL_ID = getConfiguredEmbeddingModelId()
@@ -40,6 +44,7 @@ process.stderr.write = __forwardWorkerWrite
40
44
  const DEFAULT_DEVICE = getDefaultEmbeddingDevice(MODEL_ID)
41
45
  const DEFAULT_DTYPE = getDefaultEmbeddingDtype(MODEL_ID)
42
46
  const MODEL_LOAD_OPTIONS = getEmbeddingModelLoadOptions(MODEL_ID)
47
+ const MODEL_OUTPUT_NAME = getEmbeddingOutputName(MODEL_ID)
43
48
  const INTRA_OP_THREADS = 1
44
49
  const INTER_OP_THREADS = 1
45
50
  // Session-create graph optimization. ORT defaults to 'all' (full node fusion),
@@ -77,12 +82,16 @@ const _envWorkerMaxChars = Number(process.env.MIXDOG_EMBED_MAX_CHARS)
77
82
  const WORKER_MAX_CHARS = (Number.isFinite(_envWorkerMaxChars) && _envWorkerMaxChars > 0)
78
83
  ? Math.floor(_envWorkerMaxChars)
79
84
  : 8000
80
- const EXTRACT_OPTS = { pooling: 'mean', normalize: true, truncation: true }
85
+ const EXTRACT_OPTS = { pooling: getEmbeddingPooling(MODEL_ID), normalize: true, truncation: true }
81
86
  function capEmbedText(text) {
82
87
  if (typeof text !== 'string') return ''
83
88
  return text.length > WORKER_MAX_CHARS ? text.slice(0, WORKER_MAX_CHARS) : text
84
89
  }
85
90
 
91
+ function prepareWorkerText(text, inputType) {
92
+ return capEmbedText(prepareEmbeddingInput(text, inputType, MODEL_ID))
93
+ }
94
+
86
95
  let extractorPromise = null
87
96
  let configuredDtype = DEFAULT_DTYPE
88
97
  let _device = 'cpu'
@@ -244,7 +253,7 @@ async function loadExtractor() {
244
253
  extractorPromise = (async () => {
245
254
  parentPort.postMessage({ type: 'profile', record: { phase: 'baseline', model: MODEL_ID, device: _device, dtype: configuredDtype, note: 'pre-load' } })
246
255
  patchOrtThreads()
247
- const { pipeline, env } = await import('@huggingface/transformers')
256
+ const { AutoModel, AutoTokenizer, pipeline, env } = await import('@huggingface/transformers')
248
257
  try { env.backends.onnx.logLevel = 'fatal' } catch {}
249
258
  env.allowLocalModels = false
250
259
  try { mkdirSync(MODEL_CACHE_DIR, { recursive: true }) } catch {}
@@ -277,7 +286,28 @@ async function loadExtractor() {
277
286
  let priorityLowered = false
278
287
  try { os.setPriority(0, os.constants.priority.PRIORITY_BELOW_NORMAL); priorityLowered = true } catch {}
279
288
  try {
280
- if (preferGpu) {
289
+ if (MODEL_OUTPUT_NAME) {
290
+ const device = preferGpu ? 'dml' : 'cpu'
291
+ const [tokenizer, model] = await Promise.all([
292
+ AutoTokenizer.from_pretrained(MODEL_ID),
293
+ AutoModel.from_pretrained(MODEL_ID, { ...opts, device }),
294
+ ])
295
+ extractor = async (input, extractOptions = {}) => {
296
+ const modelInputs = await tokenizer(input, {
297
+ padding: true,
298
+ truncation: extractOptions.truncation !== false,
299
+ })
300
+ const outputs = await model(modelInputs)
301
+ let result = outputs?.[MODEL_OUTPUT_NAME]
302
+ if (!result?.data?.length) {
303
+ throw new Error(`embedding output '${MODEL_OUTPUT_NAME}' missing (model=${MODEL_ID})`)
304
+ }
305
+ if (extractOptions.normalize) result = result.normalize(2, -1)
306
+ return result
307
+ }
308
+ extractor.dispose = () => model.dispose()
309
+ _device = device
310
+ } else if (preferGpu) {
281
311
  extractor = await pipeline('feature-extraction', MODEL_ID, { ...opts, device: 'dml' })
282
312
  _device = 'dml'
283
313
  } else {
@@ -334,7 +364,8 @@ async function processMessage(msg) {
334
364
  break
335
365
  }
336
366
  const t0 = Date.now()
337
- const output = await extractor(texts.map(capEmbedText), EXTRACT_OPTS)
367
+ const inputType = normalizeEmbeddingInputType(msg.inputType)
368
+ const output = await extractor(texts.map((text) => prepareWorkerText(text, inputType)), EXTRACT_OPTS)
338
369
  const wallMs = Date.now() - t0
339
370
  if (!output.data?.length) throw new Error(`embed-batch output missing data (model=${MODEL_ID})`)
340
371
  const total = output.data.length
@@ -356,7 +387,8 @@ async function processMessage(msg) {
356
387
  resetIdleTimer()
357
388
  const extractor = await loadExtractor()
358
389
  const t0 = Date.now()
359
- const output = await extractor(capEmbedText(msg.text), EXTRACT_OPTS)
390
+ const inputType = normalizeEmbeddingInputType(msg.inputType)
391
+ const output = await extractor(prepareWorkerText(msg.text, inputType), EXTRACT_OPTS)
360
392
  const wallMs = Date.now() - t0
361
393
  if (!output.data?.length) throw new Error(`embed output missing data (model=${MODEL_ID})`)
362
394
  const dims = output.data.length