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.
- package/package.json +23 -3
- package/scripts/build-voice-runtime-unix.sh +101 -0
- package/scripts/embedding-quality-probe.mjs +96 -0
- package/scripts/generate-voice-runtime-manifest.mjs +300 -0
- package/scripts/generate-voice-runtime-manifest.test.mjs +135 -0
- package/scripts/native-binary-arch.mjs +30 -13
- package/scripts/native-binary-arch.test.mjs +40 -4
- package/scripts/plan-voice-runtime-release.mjs +73 -0
- package/scripts/prepare-native-assets.mjs +50 -9
- package/scripts/prune-embedding-runtime.mjs +3 -3
- package/scripts/sync-voice-runtime-manifest.mjs +46 -0
- package/scripts/verify-release-assets.mjs +106 -6
- package/scripts/verify-voice-runtime-release.mjs +89 -0
- package/scripts/voice-runtime-config.json +52 -0
- package/src/app.mjs +42 -5
- package/src/headless-exec.mjs +58 -32
- package/src/headless-exec.test.mjs +74 -13
- package/src/rules/lead/01-general.md +2 -0
- package/src/rules/shared/10-tool-workflow.md +7 -4
- package/src/rules/shared/30-exploration.md +5 -1
- package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +28 -9
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +6 -1
- package/src/runtime/agent/orchestrator/config.mjs +53 -18
- package/src/runtime/agent/orchestrator/mcp/cancellation.test.mjs +139 -0
- package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +34 -5
- package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -10
- package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +5 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.mjs +98 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-fast-mode.test.mjs +90 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +67 -16
- package/src/runtime/agent/orchestrator/providers/anthropic-messages.mjs +4 -82
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +0 -5
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +62 -121
- package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.mjs +48 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-server-fallback.test.mjs +215 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +44 -48
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +54 -31
- package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +56 -2
- package/src/runtime/agent/orchestrator/providers/cursor-auth.mjs +34 -2
- package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +61 -1
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +6 -1
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +61 -15
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +120 -1
- package/src/runtime/agent/orchestrator/providers/lib/env-utils.mjs +9 -0
- package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +0 -3
- package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +187 -84
- package/src/runtime/agent/orchestrator/providers/openai-codex-identity.test.mjs +32 -9
- package/src/runtime/agent/orchestrator/providers/openai-codex-metadata.mjs +78 -33
- package/src/runtime/agent/orchestrator/providers/openai-codex-model.mjs +21 -0
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +36 -36
- package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +16 -6
- package/src/runtime/agent/orchestrator/providers/openai-compat-xai.mjs +50 -213
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +135 -126
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +20 -11
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +112 -36
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +220 -50
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +112 -9
- package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +11 -0
- package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +130 -60
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +2 -2
- package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +28 -10
- package/src/runtime/agent/orchestrator/providers/registry.mjs +122 -95
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +69 -3
- package/src/runtime/agent/orchestrator/providers/tool-stream-state.mjs +4 -22
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +7 -14
- package/src/runtime/agent/orchestrator/session/compact-policy.test.mjs +128 -0
- package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +5 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +27 -48
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +19 -14
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +9 -43
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +43 -5
- package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +15 -2
- package/src/runtime/agent/orchestrator/session/manager/pending-messages-persistence.test.mjs +325 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +599 -195
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +22 -0
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +9 -2
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +3 -0
- package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +26 -16
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +23 -11
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +118 -3
- package/src/runtime/agent/orchestrator/session/store-summary-reader.test.mjs +332 -0
- package/src/runtime/agent/orchestrator/session/store.mjs +38 -1
- package/src/runtime/agent/orchestrator/tools/builtin/advisory-lock.mjs +27 -6
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +12 -20
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +291 -128
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +46 -9
- package/src/runtime/agent/orchestrator/tools/builtin/edit-encoding-eol.test.mjs +224 -0
- package/src/runtime/agent/orchestrator/tools/builtin/edit-sequential-occupation.test.mjs +53 -3
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +255 -25
- package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +87 -0
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +132 -9
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +10 -0
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-pattern-fanout.mjs +29 -3
- package/src/runtime/agent/orchestrator/tools/builtin/lib/shell-job-records.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool-integrity.test.mjs +14 -0
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +180 -31
- package/src/runtime/agent/orchestrator/tools/builtin/local-search-telemetry.mjs +4 -1
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +32 -5
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-health.test.mjs +5 -2
- package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +10 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +38 -10
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +12 -0
- package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +31 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +11 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-windows.mjs +22 -4
- package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +6 -8
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +25 -24
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +152 -133
- package/src/runtime/agent/orchestrator/tools/builtin/task-tool.mjs +114 -42
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +129 -15
- package/src/runtime/agent/orchestrator/tools/code-graph/loose-file-target.test.mjs +72 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +7 -8
- package/src/runtime/agent/orchestrator/tools/code-graph/search-references.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +201 -19
- package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +26 -3
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +10 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-search-scope.test.mjs +55 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +115 -25
- package/src/runtime/agent/orchestrator/tools/code-graph/text-span-mask.test.mjs +268 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.mjs +296 -46
- package/src/runtime/agent/orchestrator/tools/lib/native-spawn-client.test.mjs +104 -0
- package/src/runtime/agent/orchestrator/tools/lib/shell-descendants.mjs +136 -0
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +127 -38
- package/src/runtime/agent/orchestrator/tools/patch/engine-contract-gate.test.mjs +661 -0
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +350 -9
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +418 -44
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +27 -3
- package/src/runtime/agent/orchestrator/tools/patch/patch-codec-eol.test.mjs +552 -0
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +65 -15
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +284 -109
- package/src/runtime/agent/orchestrator/tools/shell-exec-output.mjs +51 -21
- package/src/runtime/agent/orchestrator/tools/shell-exec-policy.mjs +44 -27
- package/src/runtime/agent/orchestrator/tools/shell-policy.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
- package/src/runtime/attachments/store.mjs +11 -3
- package/src/runtime/channels/data/voice-runtime-manifest.json +46 -70
- package/src/runtime/channels/lib/event-queue.mjs +51 -41
- package/src/runtime/channels/lib/scheduler.mjs +23 -28
- package/src/runtime/channels/lib/voice-runtime-fetcher.mjs +6 -17
- package/src/runtime/channels/lib/voice-runtime-fetcher.test.mjs +17 -0
- package/src/runtime/channels/lib/webhook.mjs +39 -20
- package/src/runtime/channels/lib/whisper-server.mjs +46 -1
- package/src/runtime/channels/lib/worker-main.mjs +3 -1
- package/src/runtime/media/adapters/gemini-video.mjs +20 -4
- package/src/runtime/media/jobs.mjs +13 -0
- package/src/runtime/media/renditions.mjs +7 -1
- package/src/runtime/media/store.mjs +42 -22
- package/src/runtime/memory/index.mjs +4 -9
- package/src/runtime/memory/lib/agent-ipc.mjs +1 -6
- package/src/runtime/memory/lib/archive-security.test.mjs +21 -1
- package/src/runtime/memory/lib/core-memory-store.mjs +28 -50
- package/src/runtime/memory/lib/embedding-model-config.mjs +37 -1
- package/src/runtime/memory/lib/embedding-model-config.test.mjs +33 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +34 -12
- package/src/runtime/memory/lib/embedding-worker.mjs +37 -5
- package/src/runtime/memory/lib/ko-morph.mjs +46 -15
- package/src/runtime/memory/lib/memory-action-handlers.mjs +18 -1
- package/src/runtime/memory/lib/memory-cycle1.mjs +1 -18
- package/src/runtime/memory/lib/memory-cycle2-gate.mjs +1 -18
- package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +98 -45
- package/src/runtime/memory/lib/memory-cycle2-shared.mjs +71 -0
- package/src/runtime/memory/lib/memory-cycle2-shared.test.mjs +78 -0
- package/src/runtime/memory/lib/memory-cycle2.mjs +37 -3
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
- package/src/runtime/memory/lib/memory-embedding-migration.test.mjs +50 -0
- package/src/runtime/memory/lib/memory-recall-store.mjs +9 -24
- package/src/runtime/memory/lib/memory.mjs +50 -12
- package/src/runtime/memory/lib/pg/process.mjs +1 -10
- package/src/runtime/memory/lib/pg/supervisor.mjs +1 -9
- package/src/runtime/memory/lib/query-handlers.mjs +2 -4
- package/src/runtime/memory/lib/session-ingest-runtime.mjs +2 -2
- package/src/runtime/shared/background-tasks.mjs +15 -0
- package/src/runtime/shared/background-tasks.test.mjs +78 -0
- package/src/runtime/shared/child-guardian.mjs +2 -10
- package/src/runtime/shared/pid-liveness.mjs +27 -0
- package/src/runtime/shared/pristine-execution-contract.json +0 -1
- package/src/runtime/shared/runtime-root.mjs +1 -11
- package/src/runtime/shared/service-discovery.mjs +1 -17
- package/src/runtime/shared/singleton-owner.mjs +1 -11
- package/src/runtime/shared/staged-update.mjs +2 -12
- package/src/runtime/shared/tool-status.mjs +5 -1
- package/src/runtime/shared/tool-surface.mjs +15 -7
- package/src/runtime/shared/turn-snapshot-store.mjs +168 -0
- package/src/runtime/shared/turn-snapshot.mjs +137 -13
- package/src/runtime/shared/turn-worktree-snapshot.mjs +110 -18
- package/src/runtime/shared/webhook-session-run.mjs +16 -2
- package/src/runtime/web-search/lib/http-fetch.mjs +34 -4
- package/src/runtime/web-search/lib/state.mjs +15 -15
- package/src/runtime/web-search/lib/web-tools.mjs +46 -5
- package/src/session-runtime/context-status.mjs +4 -12
- package/src/session-runtime/runtime-core.mjs +20 -1
- package/src/session-runtime/session-lifecycle.mjs +9 -0
- package/src/standalone/agent-dispatch-broker.mjs +33 -2
- package/src/standalone/agent-tool/job-task-reconcile.mjs +78 -0
- package/src/standalone/agent-tool/notify.mjs +3 -1
- package/src/standalone/agent-tool/spawn-flow.mjs +52 -70
- package/src/standalone/agent-tool/tag-registry.mjs +116 -33
- package/src/standalone/agent-tool/tool-def.mjs +2 -2
- package/src/standalone/agent-tool/worker-index.mjs +8 -11
- package/src/standalone/agent-tool/worker-rows.mjs +4 -1
- package/src/standalone/agent-tool.mjs +20 -55
- package/src/standalone/agent-watchdog-registry.mjs +28 -4
- package/src/standalone/channel-client.mjs +47 -15
- package/src/standalone/channel-transport.mjs +44 -25
- package/src/standalone/daemon.mjs +31 -2
- package/src/standalone/hook-bus/config.mjs +25 -9
- package/src/standalone/hook-bus/handlers.mjs +16 -52
- package/src/standalone/hook-bus.mjs +29 -23
- package/src/standalone/memory-runtime-proxy.mjs +1 -16
- package/src/standalone/session-client.mjs +86 -23
- package/src/standalone/session-protocol.mjs +0 -2
- package/src/standalone/session-runtime-host-health.test.mjs +5 -4
- package/src/standalone/session-runtime-host.mjs +101 -6
- package/src/standalone/session-runtime-record.mjs +1 -5
- package/src/standalone/session-runtime-worker.mjs +52 -57
- package/src/standalone/session-service.mjs +4 -1
- package/src/standalone/session-transport.mjs +56 -25
- package/src/tui/App.jsx +40 -49
- package/src/tui/app/app-view.jsx +14 -21
- package/src/tui/app/core-memory-picker.mjs +29 -16
- package/src/tui/app/create-app-pickers.mjs +15 -36
- package/src/tui/app/extension-pickers.mjs +60 -49
- package/src/tui/app/maintenance-pickers.mjs +100 -57
- package/src/tui/app/message-selector.mjs +6 -5
- package/src/tui/app/model-picker.mjs +30 -13
- package/src/tui/app/onboarding-steps.mjs +21 -12
- package/src/tui/app/panel-epoch.mjs +56 -0
- package/src/tui/app/panel-epoch.test.mjs +1009 -0
- package/src/tui/app/panel-surface.mjs +98 -0
- package/src/tui/app/project-picker.mjs +30 -29
- package/src/tui/app/prompt-submit.mjs +192 -97
- package/src/tui/app/prompt-submit.test.mjs +206 -0
- package/src/tui/app/provider-setup-picker.mjs +74 -43
- package/src/tui/app/resume-picker.mjs +11 -8
- package/src/tui/app/route-pickers.mjs +45 -28
- package/src/tui/app/settings-picker.mjs +69 -18
- package/src/tui/app/shell-layout.mjs +1 -3
- package/src/tui/app/slash-commands.mjs +0 -2
- package/src/tui/app/slash-dispatch.mjs +4 -2
- package/src/tui/app/text-entry-policy.mjs +25 -0
- package/src/tui/app/theme-effort-pickers.mjs +11 -16
- package/src/tui/app/usage-context-panels.mjs +21 -25
- package/src/tui/app/use-global-key-input.mjs +3 -2
- package/src/tui/app/use-prompt-draft-flow.mjs +6 -18
- package/src/tui/app/use-prompt-handlers.mjs +3 -2
- package/src/tui/app/use-welcome-prompt-hint.mjs +17 -3
- package/src/tui/components/Picker.jsx +1 -1
- package/src/tui/components/TextEntryPanel.jsx +7 -2
- package/src/tui/dist/index.mjs +652 -561
- package/src/tui/lib/voice-setup.mjs +1 -2
- package/src/tui/session/agent-job-feed.mjs +73 -11
- package/src/tui/session/session-api-ext.mjs +5 -6
- package/src/tui/session/session-api.mjs +1 -0
- package/src/tui/session/tool-approval.mjs +15 -5
- package/src/tui/session/tui-steering-persist.mjs +33 -19
- package/src/tui/session/tui-steering-persist.test.mjs +104 -0
- package/src/tui/session-local.mjs +0 -22
- package/scripts/.tmp-coverage-report.mjs +0 -96
- package/src/runtime/agent/orchestrator/tools/builtin/shell-monitor.mjs +0 -57
- 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
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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,
|
|
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 {
|
|
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
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
127
|
-
//
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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 = '
|
|
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
|
|
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
|
-
|
|
155
|
-
|
|
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
|
|
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', {
|
|
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 =
|
|
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(
|
|
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(
|
|
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(
|
|
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:
|
|
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 (
|
|
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
|
|
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
|
|
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
|