mixdog 0.9.87 → 0.9.89
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 +13 -7
- package/scripts/tool-failures.mjs +60 -24
- package/src/agents/heavy-worker/AGENT.md +3 -4
- package/src/agents/worker/AGENT.md +1 -2
- package/src/cli.mjs +8 -0
- package/src/defaults/cycle3-review-prompt.md +4 -4
- package/src/rules/agent/00-core.md +3 -0
- package/src/rules/agent/42-cycle3-agent.md +5 -6
- package/src/rules/lead/01-general.md +2 -0
- package/src/rules/shared/01-tool.md +29 -35
- package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +3 -51
- package/src/runtime/agent/orchestrator/agent-runtime/maintenance-route.mjs +59 -0
- package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +2 -2
- package/src/runtime/agent/orchestrator/agent-runtime/title-completion.mjs +67 -0
- package/src/runtime/agent/orchestrator/agent-trace-format.mjs +57 -4
- package/src/runtime/agent/orchestrator/agent-trace-io.mjs +2 -3
- package/src/runtime/agent/orchestrator/config.mjs +29 -11
- package/src/runtime/agent/orchestrator/context/collect.mjs +1 -1
- package/src/runtime/agent/orchestrator/internal-agents.mjs +1 -1
- package/src/runtime/agent/orchestrator/mcp/client.mjs +0 -24
- package/src/runtime/agent/orchestrator/providers/admission-scheduler.mjs +3 -3
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +25 -17
- package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +321 -22
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +35 -22
- package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +28 -23
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +10 -2
- package/src/runtime/agent/orchestrator/providers/grok-oauth-login.mjs +0 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +0 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +2 -5
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-native-blocks.mjs +27 -0
- package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +23 -0
- package/src/runtime/agent/orchestrator/providers/lib/stream-outcome.mjs +329 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +23 -1
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +107 -20
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +4 -3
- package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +124 -12
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +19 -2
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +110 -75
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +96 -113
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +160 -11
- package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +18 -1
- package/src/runtime/agent/orchestrator/session/lifecycle-scan.mjs +147 -117
- package/src/runtime/agent/orchestrator/session/loop/stop-hooks.mjs +88 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +50 -8
- package/src/runtime/agent/orchestrator/session/loop/termination.mjs +25 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +47 -6
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +485 -24
- package/src/runtime/agent/orchestrator/session/manager/session-close.mjs +100 -2
- package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +80 -14
- package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +142 -2
- package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +128 -32
- package/src/runtime/agent/orchestrator/session/manager.mjs +5 -1
- package/src/runtime/agent/orchestrator/session/save-session-worker.mjs +77 -4
- package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +70 -39
- package/src/runtime/agent/orchestrator/session/store/fs-probe.mjs +91 -0
- package/src/runtime/agent/orchestrator/session/store/listing.mjs +141 -64
- package/src/runtime/agent/orchestrator/session/store/live-state.mjs +253 -0
- package/src/runtime/agent/orchestrator/session/store/load-cache.mjs +211 -28
- package/src/runtime/agent/orchestrator/session/store/save-fault.mjs +313 -0
- package/src/runtime/agent/orchestrator/session/store/save-worker.mjs +622 -53
- package/src/runtime/agent/orchestrator/session/store/serialize.mjs +43 -10
- package/src/runtime/agent/orchestrator/session/store/summary-cache.mjs +53 -8
- package/src/runtime/agent/orchestrator/session/store/summary-rebuild-worker.mjs +20 -0
- package/src/runtime/agent/orchestrator/session/store/write-guards.mjs +113 -9
- package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +2 -0
- package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +476 -52
- package/src/runtime/agent/orchestrator/session/store.mjs +721 -196
- package/src/runtime/agent/orchestrator/session/token-native.mjs +2 -4
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +4 -6
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +5 -5
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +146 -90
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +6 -0
- package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +93 -17
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +5 -0
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +7 -7
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-job-spawn.mjs +17 -1
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +85 -5
- package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +52 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +54 -51
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +14 -15
- package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +132 -8
- package/src/runtime/agent/orchestrator/tools/patch/matcher.mjs +86 -3
- package/src/runtime/agent/orchestrator/tools/patch/native-server.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +210 -38
- package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +23 -1
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +87 -19
- package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +33 -8
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +12 -2
- package/src/runtime/media/lanes.mjs +2 -2
- package/src/runtime/media/renditions.mjs +101 -17
- package/src/runtime/media/renditions.test.mjs +54 -0
- package/src/runtime/media/store.mjs +111 -43
- package/src/runtime/media/store.test.mjs +53 -11
- package/src/runtime/memory/index.mjs +26 -50
- package/src/runtime/memory/lib/core-memory-candidates.mjs +2 -7
- package/src/runtime/memory/lib/core-memory-store.mjs +2 -9
- package/src/runtime/memory/lib/cycle-scheduler.mjs +10 -0
- package/src/runtime/memory/lib/embedding-provider.mjs +3 -3
- package/src/runtime/memory/lib/embedding-worker.mjs +60 -15
- package/src/runtime/memory/lib/http-router.mjs +4 -0
- package/src/runtime/memory/lib/ko-morph.mjs +49 -6
- package/src/runtime/memory/lib/memory-action-handlers.mjs +21 -18
- package/src/runtime/memory/lib/memory-cycle2.mjs +3 -3
- package/src/runtime/memory/lib/memory-cycle3.mjs +1 -3
- package/src/runtime/memory/lib/memory-recall-store.mjs +24 -0
- package/src/runtime/memory/lib/query-handlers.mjs +26 -28
- package/src/runtime/memory/tool-defs.mjs +3 -3
- package/src/runtime/shared/atomic-file.mjs +5 -1
- package/src/runtime/shared/child-guardian.mjs +73 -2
- package/src/runtime/shared/provider-api-key.mjs +22 -5
- package/src/runtime/shared/tool-execution-contract.mjs +53 -0
- package/src/session-runtime/config-helpers.mjs +6 -5
- package/src/session-runtime/lifecycle-api.mjs +16 -4
- package/src/session-runtime/media-api.mjs +31 -16
- package/src/session-runtime/prewarm.mjs +2 -24
- package/src/session-runtime/runtime-core.mjs +33 -15
- package/src/session-runtime/runtime-tunables.mjs +0 -3
- package/src/session-runtime/session-lifecycle.mjs +0 -6
- package/src/session-runtime/session-title.mjs +228 -0
- package/src/session-runtime/session-turn-api.mjs +11 -7
- package/src/session-runtime/workflow-agents-api.mjs +16 -0
- package/src/session-runtime/workflow.mjs +7 -5
- package/src/standalone/agent-tool/job-views.mjs +90 -36
- package/src/standalone/agent-tool/spawn-flow.mjs +25 -216
- package/src/standalone/agent-tool/worker-index.mjs +8 -0
- package/src/standalone/agent-tool/worker-rows.mjs +2 -1
- package/src/standalone/agent-tool.mjs +17 -17
- package/src/standalone/agent-watchdog-registry.mjs +19 -2
- package/src/standalone/channel-daemon.mjs +8 -0
- package/src/standalone/explore-tool.mjs +1 -1
- package/src/standalone/memory-runtime-proxy.mjs +4 -7
- package/src/standalone/projects.mjs +4 -1
- package/src/standalone/usage-dashboard.mjs +25 -3
- package/src/tui/App.jsx +5 -3
- package/src/tui/app/resume-picker.mjs +7 -2
- package/src/tui/app/route-pickers.mjs +11 -117
- package/src/tui/app/slash-dispatch.mjs +13 -3
- package/src/tui/app/transcript-row-estimate.mjs +1 -1
- package/src/tui/app/transcript-window.mjs +17 -93
- package/src/tui/app/use-transcript-scroll.mjs +32 -2
- package/src/tui/app/use-transcript-window.mjs +64 -114
- package/src/tui/components/Markdown.jsx +19 -2
- package/src/tui/components/Message.jsx +8 -3
- package/src/tui/components/ToolExecution.jsx +7 -3
- package/src/tui/dist/index.mjs +243 -223
- package/src/tui/engine/live-share.mjs +15 -5
- package/src/tui/engine/render-timing.mjs +2 -2
- package/src/tui/engine/session-api-ext.mjs +74 -4
- package/src/tui/engine/session-flow.mjs +3 -1
- package/src/tui/engine.mjs +2 -2
- package/src/tui/hooks/useSharedTick.mjs +0 -2
- package/src/tui/index.jsx +5 -5
- package/src/ui/statusline-agents.mjs +44 -11
- package/src/vendor/statusline/bin/statusline-route.mjs +26 -3
- package/src/workflows/default/WORKFLOW.md +3 -3
- package/src/workflows/solo/WORKFLOW.md +5 -3
- package/src/runtime/media/index.mjs +0 -18
- package/src/runtime/memory/lib/embedding-warmup.mjs +0 -68
- package/src/standalone/agent-shard/shard-child.mjs +0 -300
- package/src/standalone/agent-shard/shard-pool.mjs +0 -443
|
@@ -52,12 +52,42 @@ test('media assets are organized by kind, provider, model, and local date while
|
|
|
52
52
|
|
|
53
53
|
try {
|
|
54
54
|
const store = await import(`./store.mjs?test=${Date.now()}`);
|
|
55
|
-
const
|
|
55
|
+
const beforeMigration = store.listMediaAssets({ limit: 10 }).assets;
|
|
56
|
+
assert.equal(beforeMigration[0].file, 'legacy-image.jpg',
|
|
57
|
+
'read-only listing must not run the write-side layout migration');
|
|
58
|
+
const saved = store.saveMediaAsset({
|
|
59
|
+
kind: 'video',
|
|
60
|
+
lane: 'gemini',
|
|
61
|
+
model: 'veo:3/preview',
|
|
62
|
+
prompt: 'new clip',
|
|
63
|
+
options: { aspectRatio: '16:9' },
|
|
64
|
+
mime: 'video/mp4',
|
|
65
|
+
bytes: Buffer.from('new-video'),
|
|
66
|
+
});
|
|
67
|
+
const migrated = store.listMediaAssets({ limit: 10 }).assets;
|
|
68
|
+
const listed = ['legacy-image', 'legacy-video'].map((id) =>
|
|
69
|
+
migrated.find((entry) => entry.id === id));
|
|
56
70
|
assert.equal(listed[0].file, 'images/gemini/image-alpha/2026-07-05/legacy-image.jpg');
|
|
57
71
|
assert.equal(listed[1].file, 'videos/grok/video-beta/2026-07-05/legacy-video.mp4');
|
|
58
72
|
assert.equal(existsSync(join(assetsDir, ...listed[0].file.split('/'))), true);
|
|
59
73
|
assert.equal(existsSync(join(assetsDir, ...listed[1].file.split('/'))), true);
|
|
60
74
|
assert.equal(existsSync(join(assetsDir, 'legacy-image.jpg')), false);
|
|
75
|
+
const cacheBeforeReads = store.mediaStoreCacheStats();
|
|
76
|
+
await Promise.all([
|
|
77
|
+
store.resolveMediaFile('legacy-image'),
|
|
78
|
+
store.resolveMediaFile('legacy-video'),
|
|
79
|
+
store.resolveMediaFile('legacy-image'),
|
|
80
|
+
]);
|
|
81
|
+
const cacheAfterReads = store.mediaStoreCacheStats();
|
|
82
|
+
assert.equal(cacheAfterReads.indexDiskReads, cacheBeforeReads.indexDiskReads,
|
|
83
|
+
'a thumbnail-resolution burst must reuse the parsed media index');
|
|
84
|
+
assert.ok(cacheAfterReads.indexCacheHits >= cacheBeforeReads.indexCacheHits + 3);
|
|
85
|
+
const cachedOnlyMiss = await store.resolveMediaFile('legacy-image', {
|
|
86
|
+
variant: 'thumb',
|
|
87
|
+
generate: false,
|
|
88
|
+
});
|
|
89
|
+
assert.equal(cachedOnlyMiss.available, false,
|
|
90
|
+
'a local protocol probe must not start native rendition generation on a cache miss');
|
|
61
91
|
assert.equal(
|
|
62
92
|
(await store.readMediaAsset('legacy-video')).base64,
|
|
63
93
|
Buffer.from('video').toString('base64'),
|
|
@@ -66,7 +96,10 @@ test('media assets are organized by kind, provider, model, and local date while
|
|
|
66
96
|
// A tile-sized rendition is impossible for these stub bytes (and video
|
|
67
97
|
// needs ffmpeg): the read must REPORT the miss instead of quietly
|
|
68
98
|
// shipping the original, which is what made the remote gallery slow.
|
|
69
|
-
const missing = await store.readMediaAsset('legacy-image', {
|
|
99
|
+
const missing = await store.readMediaAsset('legacy-image', {
|
|
100
|
+
variant: 'thumb',
|
|
101
|
+
generate: false,
|
|
102
|
+
});
|
|
70
103
|
assert.equal(missing.available, false);
|
|
71
104
|
assert.equal(missing.base64, '');
|
|
72
105
|
// Callers that can afford full-size bytes (local IPC) opt in explicitly
|
|
@@ -74,20 +107,29 @@ test('media assets are organized by kind, provider, model, and local date while
|
|
|
74
107
|
const downgraded = await store.readMediaAsset('legacy-image', {
|
|
75
108
|
variant: 'thumb',
|
|
76
109
|
allowOriginal: true,
|
|
110
|
+
generate: false,
|
|
77
111
|
});
|
|
78
112
|
assert.equal(downgraded.variant, 'original');
|
|
79
113
|
assert.equal(downgraded.downgraded, true);
|
|
80
114
|
assert.equal(downgraded.base64, Buffer.from('image').toString('base64'));
|
|
81
|
-
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
prompt: 'new clip',
|
|
87
|
-
options: { aspectRatio: '16:9' },
|
|
88
|
-
mime: 'video/mp4',
|
|
89
|
-
bytes: Buffer.from('new-video'),
|
|
115
|
+
const browserThumb = Buffer.from('browser-generated-thumbnail');
|
|
116
|
+
const cached = store.cacheMediaThumbnail('legacy-video', {
|
|
117
|
+
mime: 'image/jpeg',
|
|
118
|
+
base64: browserThumb.toString('base64'),
|
|
119
|
+
durationSeconds: 7,
|
|
90
120
|
});
|
|
121
|
+
assert.equal(cached.available, true);
|
|
122
|
+
const cachedTarget = await store.resolveMediaFile('legacy-video', {
|
|
123
|
+
variant: 'thumb',
|
|
124
|
+
generate: false,
|
|
125
|
+
});
|
|
126
|
+
assert.equal(cachedTarget.available, true,
|
|
127
|
+
'a cache-only local protocol probe must serve a browser-generated rendition');
|
|
128
|
+
const cachedRead = await store.readMediaAsset('legacy-video', { variant: 'thumb' });
|
|
129
|
+
assert.equal(cachedRead.mime, 'image/jpeg');
|
|
130
|
+
assert.equal(cachedRead.base64, browserThumb.toString('base64'));
|
|
131
|
+
assert.equal(cachedRead.durationSeconds, 7);
|
|
132
|
+
|
|
91
133
|
assert.match(saved.file, /^videos\/gemini\/veo-3-preview\/\d{4}-\d{2}-\d{2}\/[0-9a-f-]+\.mp4$/);
|
|
92
134
|
assert.equal(existsSync(join(assetsDir, ...saved.file.split('/'))), true);
|
|
93
135
|
assert.equal(store.deleteMediaAsset(saved.id).removed, true);
|
|
@@ -4,6 +4,15 @@ import { __mixdogMemoryLog } from './lib/memory-log.mjs';
|
|
|
4
4
|
process.removeAllListeners('warning')
|
|
5
5
|
process.on('warning', () => {})
|
|
6
6
|
|
|
7
|
+
// V8 compile cache: the memory runtime is its own long-lived child process
|
|
8
|
+
// (spawned by the channel daemon / standalone proxy) and never passes through
|
|
9
|
+
// cli.mjs, so it pays full ESM parse cost on every boot without this. Same
|
|
10
|
+
// best-effort guard as cli.mjs.
|
|
11
|
+
try {
|
|
12
|
+
const { enableCompileCache } = await import('node:module')
|
|
13
|
+
enableCompileCache?.()
|
|
14
|
+
} catch { /* launch-speed optimization only */ }
|
|
15
|
+
|
|
7
16
|
import http from 'node:http'
|
|
8
17
|
import os from 'node:os'
|
|
9
18
|
import { performance } from 'node:perf_hooks'
|
|
@@ -55,21 +64,21 @@ import {
|
|
|
55
64
|
sessionMessageContentForIngest,
|
|
56
65
|
shouldExcludeIngestMessage,
|
|
57
66
|
} from './lib/session-ingest.mjs'
|
|
58
|
-
import { configureEmbedding, embedText, embedTexts, getEmbeddingDims, getEmbeddingDtype, getEmbeddingModelId, getKnownDimsForCurrentModel, isEmbeddingModelReady, primeEmbeddingDims, warmupEmbeddingProvider } from './lib/embedding-provider.mjs'
|
|
67
|
+
import { configureEmbedding, embedText, embedTexts, getEmbeddingDims, getEmbeddingDtype, getEmbeddingModelId, getKnownDimsForCurrentModel, isEmbeddingModelReady, primeEmbeddingDims, shutdownEmbeddingProvider, warmupEmbeddingProvider } from './lib/embedding-provider.mjs'
|
|
59
68
|
import { startLlmWorker, stopLlmWorker } from './lib/llm-worker-host.mjs'
|
|
60
69
|
import { runCycle1, runCycle2, runCycle3, runUnifiedGate, parseInterval, syncRootEmbedding, flushRawEmbeddings, applySimpleStatus, applyUpdate, applyMerge, CYCLE2_ACTIVE_TARGET_CAP } from './lib/memory-cycle.mjs'
|
|
61
70
|
import { loadConfig as loadAgentConfig } from '../agent/orchestrator/config.mjs'
|
|
62
71
|
import { initProviders } from '../agent/orchestrator/providers/registry.mjs'
|
|
63
72
|
import { makeAgentDispatch } from '../agent/orchestrator/agent-runtime/agent-dispatch.mjs'
|
|
64
73
|
import { getInFlightCycle1 } from './lib/memory-cycle1.mjs'
|
|
65
|
-
import { claimAndMarkScheduledCycle, resolveCoalesceMaxRetries, scheduleCoalescedCycleRetry } from './lib/memory-cycle-requests.mjs'
|
|
74
|
+
import { claimAndMarkScheduledCycle, markCycleRequest, resolveCoalesceMaxRetries, scheduleCoalescedCycleRetry } from './lib/memory-cycle-requests.mjs'
|
|
66
75
|
import { searchRelevantHybrid } from './lib/memory-recall-store.mjs'
|
|
67
76
|
import { fetchEntriesByIdsScoped } from './lib/memory-recall-id-patch.mjs'
|
|
68
77
|
import { retrieveEntries } from './lib/memory-retrievers.mjs'
|
|
69
78
|
import { pruneOldEntries } from './lib/memory-maintenance-store.mjs'
|
|
70
79
|
import { computeEntryScore } from './lib/memory-score.mjs'
|
|
71
80
|
import { runFullBackfill } from './lib/memory-ops-policy.mjs'
|
|
72
|
-
import { listCore, addCore, editCore, deleteCore, compactCoreIds, listCoreCandidates, promoteCoreCandidate, dismissCoreCandidate
|
|
81
|
+
import { listCore, addCore, editCore, deleteCore, compactCoreIds, listCoreCandidates, promoteCoreCandidate, dismissCoreCandidate } from './lib/core-memory-store.mjs'
|
|
73
82
|
import { resolveProjectId, resolveProjectScope } from './lib/project-id-resolver.mjs'
|
|
74
83
|
import { openTraceDatabase, closeTraceDatabase, insertTraceEvents, enqueueTraceEvents, insertAgentCalls, registerTraceExitDrain } from './lib/trace-store.mjs'
|
|
75
84
|
import { updateJsonAtomicSync, writeJsonAtomicSync } from '../shared/atomic-file.mjs'
|
|
@@ -79,8 +88,7 @@ import { parsePeriod, formatTs, coreRecallTerms, normalizeRecallProjectScope, se
|
|
|
79
88
|
import { readBody, sendJson, sendError, isLocalOrigin, normalizeCoreProjectId } from './lib/http-wire.mjs'
|
|
80
89
|
import { scheduledCycle1Signature, scheduledCycle2Signature, scheduledCycle3Signature } from './lib/cycle-signatures.mjs'
|
|
81
90
|
import { createTranscriptIngest } from './lib/transcript-ingest.mjs'
|
|
82
|
-
import {
|
|
83
|
-
import { init as initKoMorph } from './lib/ko-morph.mjs'
|
|
91
|
+
import { init as initKoMorph, reset as resetKoMorph } from './lib/ko-morph.mjs'
|
|
84
92
|
import { createCycleLlmAdapters } from './lib/cycle-llm-adapters.mjs'
|
|
85
93
|
import { createCycleScheduler } from './lib/cycle-scheduler.mjs'
|
|
86
94
|
import { createQueryHandlers } from './lib/query-handlers.mjs'
|
|
@@ -260,15 +268,6 @@ const _sessionIngest = createSessionIngestRuntime({
|
|
|
260
268
|
})
|
|
261
269
|
const { ingestSessionMessages } = _sessionIngest
|
|
262
270
|
|
|
263
|
-
// Boot-edge embedding warmup queue (extracted to lib/embedding-warmup.mjs).
|
|
264
|
-
const _embeddingWarmup = createEmbeddingWarmup({
|
|
265
|
-
canStart: embeddingWarmupCanStart,
|
|
266
|
-
warmup: warmupEmbeddingProvider,
|
|
267
|
-
getDims: getEmbeddingDims,
|
|
268
|
-
persistMeta: (metaPath, value) => writeJsonAtomicSync(metaPath, value, { lock: true }),
|
|
269
|
-
log: __mixdogMemoryLog,
|
|
270
|
-
})
|
|
271
|
-
|
|
272
271
|
// DATA_DIR-bound wrappers over the extracted pure flag helpers (see
|
|
273
272
|
// ./lib/memory-config-flags.mjs). The pg-attach check needs DATA_DIR, which is
|
|
274
273
|
// module-local here.
|
|
@@ -324,20 +323,9 @@ async function _initStore() {
|
|
|
324
323
|
if (dimsResolved) {
|
|
325
324
|
primeEmbeddingDims(dimsResolved)
|
|
326
325
|
assertSecondaryPgAttachable()
|
|
327
|
-
//
|
|
328
|
-
//
|
|
329
|
-
//
|
|
330
|
-
// lands a cold boot's first recall dense instead of lexical-fallback.
|
|
331
|
-
// Both are fire-and-forget; the boot-complete fireDeferred/initKoMorph edge
|
|
332
|
-
// stays an idempotent no-op fallback. schedule() respects canStart
|
|
333
|
-
// (secondary/env-disabled skip → fireDeferred no-op); kiwi skips secondary.
|
|
334
|
-
_embeddingWarmup.schedule(EMBEDDING_META_PATH, metaKey)
|
|
335
|
-
memoryProfile('embedding:warmup:fire')
|
|
336
|
-
_embeddingWarmup.fireDeferred()
|
|
337
|
-
if (!memorySecondaryMode()) {
|
|
338
|
-
memoryProfile('ko-morph:warmup:fire')
|
|
339
|
-
initKoMorph(DATA_DIR, __mixdogMemoryLog).catch(() => {})
|
|
340
|
-
}
|
|
326
|
+
// Known dimensions are enough to open the vector schema. Keep the ONNX
|
|
327
|
+
// model and the ~560 MB Kiwi analyzer completely off the boot path; the
|
|
328
|
+
// first relevant query starts them on demand without blocking startup.
|
|
341
329
|
const openStartedAt = performance.now()
|
|
342
330
|
db = await openDatabase(DATA_DIR, dimsResolved)
|
|
343
331
|
memoryProfile('open-db:done', { ms: (performance.now() - openStartedAt).toFixed(1), dims: dimsResolved })
|
|
@@ -352,12 +340,6 @@ async function _initStore() {
|
|
|
352
340
|
memoryProfile('embedding:cold-warmup:done', { ms: (performance.now() - warmupStartedAt).toFixed(1) })
|
|
353
341
|
dimsResolved = Number(getEmbeddingDims())
|
|
354
342
|
assertSecondaryPgAttachable()
|
|
355
|
-
// Embedding is already warm (awaited above); still fire kiwi during the PG
|
|
356
|
-
// ensure I/O-wait so the first recall's FTS path is morph-aware too.
|
|
357
|
-
if (!memorySecondaryMode()) {
|
|
358
|
-
memoryProfile('ko-morph:warmup:fire')
|
|
359
|
-
initKoMorph(DATA_DIR, __mixdogMemoryLog).catch(() => {})
|
|
360
|
-
}
|
|
361
343
|
const openStartedAt = performance.now()
|
|
362
344
|
db = await openDatabase(DATA_DIR, dimsResolved)
|
|
363
345
|
memoryProfile('open-db:done', { ms: (performance.now() - openStartedAt).toFixed(1), dims: dimsResolved })
|
|
@@ -478,6 +460,7 @@ const _cycleScheduler = createCycleScheduler({
|
|
|
478
460
|
flushRawEmbeddings,
|
|
479
461
|
getInFlightCycle1,
|
|
480
462
|
claimAndMarkScheduledCycle,
|
|
463
|
+
markCycleRequest,
|
|
481
464
|
resolveCoalesceMaxRetries,
|
|
482
465
|
scheduleCoalescedCycleRetry,
|
|
483
466
|
scheduledCycle1Signature,
|
|
@@ -535,20 +518,6 @@ async function _initRuntime() {
|
|
|
535
518
|
__mixdogMemoryLog('[memory-service] background cycle tick loop not started (secondary/env-disabled)\n')
|
|
536
519
|
}
|
|
537
520
|
_initialized = true
|
|
538
|
-
// Boot complete — continue straight into the deferred embedding warmup.
|
|
539
|
-
// Fire-and-forget on the embedding worker thread; never awaited so it does
|
|
540
|
-
// not delay init() returning or the memory-ready signal.
|
|
541
|
-
memoryProfile('embedding:warmup:fire')
|
|
542
|
-
_embeddingWarmup.fireDeferred()
|
|
543
|
-
// Boot-edge Korean morph warmup. Same fire-and-forget style: lazy async init
|
|
544
|
-
// downloads+caches the Kiwi model once under DATA_DIR/kiwi-model/<version>,
|
|
545
|
-
// then builds the WASM analyzer. On any failure ko-morph stays null and
|
|
546
|
-
// buildFtsQuery keeps the websearch_to_tsquery fallback (never throws here).
|
|
547
|
-
// Skipped in secondary mode (query path lives on the primary).
|
|
548
|
-
if (!memorySecondaryMode()) {
|
|
549
|
-
memoryProfile('ko-morph:warmup:fire')
|
|
550
|
-
initKoMorph(DATA_DIR, __mixdogMemoryLog).catch(() => {})
|
|
551
|
-
}
|
|
552
521
|
memoryProfile('runtime-init:done', { ms: (performance.now() - runtimeStartedAt).toFixed(1) })
|
|
553
522
|
}
|
|
554
523
|
|
|
@@ -569,6 +538,9 @@ const __queryHandlers = createQueryHandlers({
|
|
|
569
538
|
log: __mixdogMemoryLog,
|
|
570
539
|
resolveProjectScope,
|
|
571
540
|
embeddingWarmupCanStart,
|
|
541
|
+
ensureKoMorph: () => memorySecondaryMode()
|
|
542
|
+
? Promise.resolve(false)
|
|
543
|
+
: initKoMorph(DATA_DIR, __mixdogMemoryLog),
|
|
572
544
|
getBootTimestamp: () => _bootTimestamp,
|
|
573
545
|
getTraceDb: () => _traceDb,
|
|
574
546
|
})
|
|
@@ -598,6 +570,7 @@ const _actionHandlers = createMemoryActionHandlers({
|
|
|
598
570
|
startCycle1Run: _startCycle1Run,
|
|
599
571
|
finalizeCycle2Run: _finalizeCycle2Run,
|
|
600
572
|
finalizeCycle3Run: _finalizeCycle3Run,
|
|
573
|
+
requestCycle3Review: _cycleScheduler.requestCycle3Review,
|
|
601
574
|
getSchedulerCycle1InFlight: () => _cycleScheduler.getCycle1InFlight(),
|
|
602
575
|
getCycle2CallLlm,
|
|
603
576
|
getCycle3CallLlm,
|
|
@@ -703,9 +676,12 @@ export async function stop() {
|
|
|
703
676
|
_stopPromise = (async () => {
|
|
704
677
|
_stopCycles()
|
|
705
678
|
_memoryPortAdvertiser.reset()
|
|
706
|
-
_embeddingWarmup.reset()
|
|
707
679
|
_daemonLifecycle.reset()
|
|
708
|
-
await
|
|
680
|
+
await Promise.allSettled([
|
|
681
|
+
stopLlmWorker(),
|
|
682
|
+
shutdownEmbeddingProvider(),
|
|
683
|
+
])
|
|
684
|
+
resetKoMorph()
|
|
709
685
|
resetHttpListenErrorHandler()
|
|
710
686
|
if (_httpBoundPort != null || _httpReadyPromise) {
|
|
711
687
|
await new Promise(resolve => {
|
|
@@ -4,7 +4,7 @@ import { cachedEmbedTextBatch } from './memory-embed.mjs'
|
|
|
4
4
|
import { callAgentDispatch } from './agent-ipc.mjs'
|
|
5
5
|
import { resolveMaintenancePreset } from '../../shared/llm/index.mjs'
|
|
6
6
|
import { checkedConnect } from './pg/adapter.mjs'
|
|
7
|
-
import { __mixdogMemoryLog, CORE_DEDUP_TOP_K,
|
|
7
|
+
import { __mixdogMemoryLog, CORE_DEDUP_TOP_K, _getDb, throwIfAborted, addCore } from './core-memory-store.mjs';
|
|
8
8
|
|
|
9
9
|
//
|
|
10
10
|
// nominateCoreCandidates flags strong active entries as core-memory
|
|
@@ -225,12 +225,7 @@ export async function promoteCoreCandidate(dataDir, id, options = {}) {
|
|
|
225
225
|
if (rowPid != null && rowPid !== scope) {
|
|
226
226
|
throw new Error(`candidate id=${numId} belongs to project "${rowPid}", not the resolved scope "${scope ?? 'common'}"`)
|
|
227
227
|
}
|
|
228
|
-
// Core summary cap: candidate summaries may exceed CORE_SUMMARY_MAX. Prefer
|
|
229
|
-
// the explicit override, else compress by truncation so addCore accepts it.
|
|
230
228
|
const summary = options?.summary ?? cur.summary
|
|
231
|
-
const cappedSummary = summary && String(summary).length > CORE_SUMMARY_MAX
|
|
232
|
-
? String(summary).slice(0, CORE_SUMMARY_MAX)
|
|
233
|
-
: summary
|
|
234
229
|
const now = Date.now()
|
|
235
230
|
// Phase 1 — claim (active+candidate guarded so a concurrent archive/promote
|
|
236
231
|
// loses the race → 0 rows → nothing to promote). Embedding NOT nulled here
|
|
@@ -251,7 +246,7 @@ export async function promoteCoreCandidate(dataDir, id, options = {}) {
|
|
|
251
246
|
try {
|
|
252
247
|
entry = await addCore(
|
|
253
248
|
dataDir,
|
|
254
|
-
{ element: cur.element, summary
|
|
249
|
+
{ element: cur.element, summary, category: cur.category },
|
|
255
250
|
rowPid,
|
|
256
251
|
)
|
|
257
252
|
} catch (err) {
|
|
@@ -27,8 +27,7 @@ const VALID_CAT = new Set([
|
|
|
27
27
|
const SIM_RECALL = 0.65
|
|
28
28
|
export const CORE_DEDUP_TOP_K = 5
|
|
29
29
|
|
|
30
|
-
|
|
31
|
-
const CORE_ELEMENT_MAX = 40
|
|
30
|
+
const CORE_ELEMENT_DERIVE_LENGTH = 40
|
|
32
31
|
|
|
33
32
|
function trimOrNull(v) {
|
|
34
33
|
if (v == null) return null
|
|
@@ -38,7 +37,7 @@ function trimOrNull(v) {
|
|
|
38
37
|
|
|
39
38
|
export function normalizeCoreInput(input = {}, options = {}) {
|
|
40
39
|
const summary = trimOrNull(input.summary ?? input.content)
|
|
41
|
-
const element = trimOrNull(input.element) ?? (summary ? summary.slice(0,
|
|
40
|
+
const element = trimOrNull(input.element) ?? (summary ? summary.slice(0, CORE_ELEMENT_DERIVE_LENGTH) : null)
|
|
42
41
|
const suppliedCategory = trimOrNull(input.category)
|
|
43
42
|
const category = (suppliedCategory ?? 'fact').toLowerCase()
|
|
44
43
|
const errors = []
|
|
@@ -46,12 +45,6 @@ export function normalizeCoreInput(input = {}, options = {}) {
|
|
|
46
45
|
if (options.requireElement && !element) errors.push('element required')
|
|
47
46
|
if (options.requireSummary && !summary) errors.push('summary required')
|
|
48
47
|
if (options.requireCategory && !suppliedCategory) errors.push('category required')
|
|
49
|
-
if (element && element.length > CORE_ELEMENT_MAX) {
|
|
50
|
-
errors.push(`element too long (${element.length}/${CORE_ELEMENT_MAX} chars, remove ${element.length - CORE_ELEMENT_MAX})`)
|
|
51
|
-
}
|
|
52
|
-
if (summary && summary.length > CORE_SUMMARY_MAX) {
|
|
53
|
-
errors.push(`summary too long (${summary.length}/${CORE_SUMMARY_MAX} chars, remove ${summary.length - CORE_SUMMARY_MAX})`)
|
|
54
|
-
}
|
|
55
48
|
if (suppliedCategory && !VALID_CAT.has(category)) {
|
|
56
49
|
errors.push(`invalid category "${category}". Valid: ${[...VALID_CAT].join(', ')}`)
|
|
57
50
|
}
|
|
@@ -62,6 +62,7 @@ export function createCycleScheduler(deps) {
|
|
|
62
62
|
flushRawEmbeddings,
|
|
63
63
|
getInFlightCycle1,
|
|
64
64
|
claimAndMarkScheduledCycle,
|
|
65
|
+
markCycleRequest,
|
|
65
66
|
resolveCoalesceMaxRetries,
|
|
66
67
|
scheduleCoalescedCycleRetry,
|
|
67
68
|
scheduledCycle1Signature,
|
|
@@ -362,6 +363,14 @@ export function createCycleScheduler(deps) {
|
|
|
362
363
|
}, retryConfig, signature)
|
|
363
364
|
}
|
|
364
365
|
|
|
366
|
+
async function requestCycle3Review(reason = 'core-mutation') {
|
|
367
|
+
const config = getConfig() || readMainConfig() || {}
|
|
368
|
+
const signature = scheduledCycle3Signature(config)
|
|
369
|
+
const marked = await markCycleRequest(getDb(), 'cycle3', reason, signature)
|
|
370
|
+
if (marked) scheduleScheduledCycle3(config, signature)
|
|
371
|
+
return marked
|
|
372
|
+
}
|
|
373
|
+
|
|
365
374
|
async function _finalizeCycle2Run(result) {
|
|
366
375
|
if (result?.skippedInFlight) {
|
|
367
376
|
log('[cycle2] skipped: in flight\n')
|
|
@@ -558,6 +567,7 @@ export function createCycleScheduler(deps) {
|
|
|
558
567
|
await setCycleLastRun('cycle3', Date.now())
|
|
559
568
|
markCycleDone('cycle3', true)
|
|
560
569
|
},
|
|
570
|
+
requestCycle3Review,
|
|
561
571
|
periodicCycle1Config,
|
|
562
572
|
// lifecycle
|
|
563
573
|
startCycles,
|
|
@@ -82,11 +82,11 @@ function ensureWorker() {
|
|
|
82
82
|
return
|
|
83
83
|
}
|
|
84
84
|
if (msg.type === 'idle-dispose') {
|
|
85
|
-
cachedDims = null
|
|
86
85
|
_modelReady = false
|
|
87
86
|
_device = 'cpu'
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
const reason = String(msg.reason || 'idle timeout')
|
|
88
|
+
__mixdogMemoryLog(`[embed] ${reason} — model disposed\n`)
|
|
89
|
+
writeProfilePoint({ phase: 'post-idle', model: MODEL_ID, device: msg.device, dtype: msg.dtype, note: reason })
|
|
90
90
|
return
|
|
91
91
|
}
|
|
92
92
|
const pending = _pending.get(msg.id)
|
|
@@ -57,14 +57,15 @@ const execFileAsync = promisify(execFile)
|
|
|
57
57
|
const _envLoadCores = Number(process.env.MIXDOG_EMBED_LOAD_CORES)
|
|
58
58
|
const LOAD_AFFINITY_CORES = Number.isInteger(_envLoadCores) && _envLoadCores >= 1 ? _envLoadCores : 1
|
|
59
59
|
const MODEL_CACHE_DIR = join(resolvePluginData(), 'memory-models')
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
// stays resident for the lifetime of the worker. Set MIXDOG_EMBED_IDLE_TIMEOUT_MS
|
|
64
|
-
// to a positive value (in ms) to restore the prior dispose behaviour when GPU
|
|
65
|
-
// VRAM pressure is a concern.
|
|
60
|
+
// Reclaim the model after a short idle window. The worker remains available,
|
|
61
|
+
// so the next recall reloads on demand without keeping hundreds of MB resident
|
|
62
|
+
// throughout an otherwise idle desktop session. Set 0 to opt into keep-alive.
|
|
66
63
|
const _envIdleMs = Number(process.env.MIXDOG_EMBED_IDLE_TIMEOUT_MS)
|
|
67
|
-
const IDLE_TIMEOUT_MS = Number.isFinite(_envIdleMs) && _envIdleMs >= 0 ? _envIdleMs :
|
|
64
|
+
const IDLE_TIMEOUT_MS = Number.isFinite(_envIdleMs) && _envIdleMs >= 0 ? _envIdleMs : 3 * 60_000
|
|
65
|
+
const _envPressureMb = Number(process.env.MIXDOG_EMBED_PRESSURE_MIN_FREE_MB)
|
|
66
|
+
const PRESSURE_MIN_FREE_BYTES = (
|
|
67
|
+
Number.isFinite(_envPressureMb) && _envPressureMb >= 0 ? _envPressureMb : 1536
|
|
68
|
+
) * 1024 * 1024
|
|
68
69
|
|
|
69
70
|
// Defensive belt against giant model inputs. Callers should already bound text
|
|
70
71
|
// (see memory-embed truncateForEmbed), but the worker is the last line before
|
|
@@ -98,6 +99,15 @@ const CONTROL_ACTIONS = new Set(['configure', 'dispose', 'warmup'])
|
|
|
98
99
|
// under sustained interactive traffic. MIXDOG_EMBED_PRIORITY_SKIP_CAP overrides.
|
|
99
100
|
const _envSkipCap = Number(process.env.MIXDOG_EMBED_PRIORITY_SKIP_CAP)
|
|
100
101
|
const PRIORITY_SKIP_CAP = Number.isFinite(_envSkipCap) && _envSkipCap >= 1 ? Math.floor(_envSkipCap) : 8
|
|
102
|
+
// Bounded backlog: the queue previously grew without limit while a slow embed
|
|
103
|
+
// held the in-flight guard — a flush storm could park thousands of messages
|
|
104
|
+
// (each carrying full text payloads) in worker memory until the caller's 60s
|
|
105
|
+
// timeout killed the whole worker. Reject new INFERENCE work beyond the cap
|
|
106
|
+
// with an immediate error reply instead; control ops (configure/dispose/
|
|
107
|
+
// warmup) always queue so lifecycle invariants can never be dropped.
|
|
108
|
+
// MIXDOG_EMBED_QUEUE_MAX overrides (default 128).
|
|
109
|
+
const _envQueueMax = Number(process.env.MIXDOG_EMBED_QUEUE_MAX)
|
|
110
|
+
const QUEUE_MAX = Number.isFinite(_envQueueMax) && _envQueueMax >= 1 ? Math.floor(_envQueueMax) : 128
|
|
101
111
|
// Queue with a single priority lane. Interactive query embeds (msg.priority)
|
|
102
112
|
// jump ahead of background flush embed-batch work so an in-progress large
|
|
103
113
|
// flush cannot starve a live recall's query vector. Constraints:
|
|
@@ -106,6 +116,16 @@ const PRIORITY_SKIP_CAP = Number.isFinite(_envSkipCap) && _envSkipCap >= 1 ? Mat
|
|
|
106
116
|
// - never preempt the running ONNX call,
|
|
107
117
|
// - never overtake the same flush batch more than PRIORITY_SKIP_CAP times.
|
|
108
118
|
function enqueue(msg) {
|
|
119
|
+
if (!CONTROL_ACTIONS.has(msg?.action) && _msgQueue.length >= QUEUE_MAX) {
|
|
120
|
+
try {
|
|
121
|
+
parentPort.postMessage({
|
|
122
|
+
id: msg?.id,
|
|
123
|
+
type: 'error',
|
|
124
|
+
message: `embed worker queue full (max ${QUEUE_MAX}); retry after the embedding backlog drains`,
|
|
125
|
+
})
|
|
126
|
+
} catch { /* reply must never break the worker */ }
|
|
127
|
+
return
|
|
128
|
+
}
|
|
109
129
|
if (!msg?.priority) { _msgQueue.push(msg); return }
|
|
110
130
|
// Barrier: insertion must land after the last queued control op.
|
|
111
131
|
let lastControl = -1
|
|
@@ -135,22 +155,44 @@ function enqueue(msg) {
|
|
|
135
155
|
// reset / ext.dispose() while the prior extractor is still being torn down.
|
|
136
156
|
const GUARDED_ACTIONS = new Set(['embed', 'embed-batch', 'warmup', 'configure', 'dispose'])
|
|
137
157
|
|
|
158
|
+
function disposeLoadedExtractor(reason) {
|
|
159
|
+
if (!extractorPromise) return false
|
|
160
|
+
const loaded = extractorPromise
|
|
161
|
+
extractorPromise = null
|
|
162
|
+
const prevDevice = _device
|
|
163
|
+
_device = 'cpu'
|
|
164
|
+
loaded.then(ext => { try { ext.dispose() } catch {} }).catch(() => {})
|
|
165
|
+
__mixdogMemoryLog(`[embed-worker] ${reason} — model disposed\n`)
|
|
166
|
+
parentPort.postMessage({
|
|
167
|
+
type: 'idle-dispose',
|
|
168
|
+
reason,
|
|
169
|
+
device: prevDevice,
|
|
170
|
+
dtype: configuredDtype,
|
|
171
|
+
})
|
|
172
|
+
return true
|
|
173
|
+
}
|
|
174
|
+
|
|
138
175
|
function resetIdleTimer() {
|
|
139
176
|
if (_idleTimer) clearTimeout(_idleTimer)
|
|
140
177
|
if (IDLE_TIMEOUT_MS <= 0) return
|
|
141
178
|
_idleTimer = setTimeout(() => {
|
|
142
|
-
if (
|
|
143
|
-
extractorPromise.then(ext => { try { ext.dispose() } catch {} }).catch(() => {})
|
|
144
|
-
extractorPromise = null
|
|
145
|
-
const prevDevice = _device
|
|
146
|
-
_device = 'cpu'
|
|
147
|
-
__mixdogMemoryLog('[embed-worker] idle timeout — model disposed\n')
|
|
148
|
-
parentPort.postMessage({ type: 'idle-dispose', device: prevDevice, dtype: configuredDtype })
|
|
149
|
-
}
|
|
179
|
+
if (!_embedInFlight) disposeLoadedExtractor('idle timeout')
|
|
150
180
|
_idleTimer = null
|
|
151
181
|
}, IDLE_TIMEOUT_MS)
|
|
182
|
+
_idleTimer.unref?.()
|
|
152
183
|
}
|
|
153
184
|
|
|
185
|
+
const _pressureTimer = setInterval(() => {
|
|
186
|
+
if (PRESSURE_MIN_FREE_BYTES <= 0 || _embedInFlight || !extractorPromise) return
|
|
187
|
+
let freeBytes = Number.POSITIVE_INFINITY
|
|
188
|
+
try { freeBytes = Number(os.freemem()) } catch {}
|
|
189
|
+
if (Number.isFinite(freeBytes) && freeBytes < PRESSURE_MIN_FREE_BYTES) {
|
|
190
|
+
if (_idleTimer) { clearTimeout(_idleTimer); _idleTimer = null }
|
|
191
|
+
disposeLoadedExtractor('host memory pressure')
|
|
192
|
+
}
|
|
193
|
+
}, 5_000)
|
|
194
|
+
_pressureTimer.unref?.()
|
|
195
|
+
|
|
154
196
|
// Set this process's CPU affinity to `mask` (bitmask of allowed logical
|
|
155
197
|
// processors). Windows-only and best-effort: there is no Node API, so go
|
|
156
198
|
// through PowerShell; a process may set its own affinity without elevation.
|
|
@@ -201,6 +243,9 @@ function patchOrtThreads() {
|
|
|
201
243
|
async function loadExtractor() {
|
|
202
244
|
if (!extractorPromise) {
|
|
203
245
|
extractorPromise = (async () => {
|
|
246
|
+
if (PRESSURE_MIN_FREE_BYTES > 0 && os.freemem() < PRESSURE_MIN_FREE_BYTES) {
|
|
247
|
+
throw new Error('embedding model load deferred under host memory pressure')
|
|
248
|
+
}
|
|
204
249
|
parentPort.postMessage({ type: 'profile', record: { phase: 'baseline', model: MODEL_ID, device: _device, dtype: configuredDtype, note: 'pre-load' } })
|
|
205
250
|
patchOrtThreads()
|
|
206
251
|
const { pipeline, env } = await import('@huggingface/transformers')
|
|
@@ -230,6 +230,10 @@ export function createHttpRouter({
|
|
|
230
230
|
const body = await readBody(req)
|
|
231
231
|
const projectId = normalizeCoreProjectId(body.project_id)
|
|
232
232
|
const entry = await addCore(DATA_DIR, body, projectId)
|
|
233
|
+
if (typeof cycleScheduler?.requestCycle3Review === 'function') {
|
|
234
|
+
try { await cycleScheduler.requestCycle3Review('core-http-add') }
|
|
235
|
+
catch (err) { log?.(`[core-memory] HTTP follow-up review queue failed: ${err?.message || err}\n`) }
|
|
236
|
+
}
|
|
233
237
|
sendJson(res, { ok: true, item: entry })
|
|
234
238
|
} catch (e) { sendJson(res, { ok: false, error: e.message }, 500) }
|
|
235
239
|
return
|
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
// analyze(text) — sync; returns TokenInfo[] or null when not ready.
|
|
16
16
|
// stems(text) — sync; content-morpheme stems (NNG/NNP/VV/VA/XR/SL) or null.
|
|
17
17
|
import fs from 'node:fs'
|
|
18
|
+
import os from 'node:os'
|
|
18
19
|
import path from 'node:path'
|
|
19
20
|
import zlib from 'node:zlib'
|
|
20
21
|
|
|
@@ -49,11 +50,45 @@ let _initPromise = null
|
|
|
49
50
|
let _kiwi = null
|
|
50
51
|
let _log = () => {}
|
|
51
52
|
let _initMs = 0
|
|
53
|
+
let _idleTimer = null
|
|
54
|
+
const _envIdleMs = Number(process.env.MIXDOG_KO_MORPH_IDLE_TIMEOUT_MS)
|
|
55
|
+
const IDLE_TIMEOUT_MS = Number.isFinite(_envIdleMs) && _envIdleMs >= 0
|
|
56
|
+
? _envIdleMs
|
|
57
|
+
: 2 * 60_000
|
|
58
|
+
const _envPressureMb = Number(process.env.MIXDOG_KO_MORPH_PRESSURE_MIN_FREE_MB)
|
|
59
|
+
const PRESSURE_MIN_FREE_BYTES = (
|
|
60
|
+
Number.isFinite(_envPressureMb) && _envPressureMb >= 0 ? _envPressureMb : 1536
|
|
61
|
+
) * 1024 * 1024
|
|
52
62
|
|
|
53
63
|
export function isReady() { return _state === 'ready' && _kiwi != null }
|
|
54
64
|
export function state() { return _state }
|
|
55
65
|
function initLatencyMs() { return _initMs }
|
|
56
66
|
|
|
67
|
+
function releaseLoaded(reason = '') {
|
|
68
|
+
if (_idleTimer) clearTimeout(_idleTimer)
|
|
69
|
+
_idleTimer = null
|
|
70
|
+
const current = _kiwi
|
|
71
|
+
_kiwi = null
|
|
72
|
+
_state = 'idle'
|
|
73
|
+
_initPromise = null
|
|
74
|
+
_initMs = 0
|
|
75
|
+
try { current?.dispose?.() } catch {}
|
|
76
|
+
if (current && reason) _log(`[memory-service] kiwi morph ${reason} — analyzer released\n`)
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function touchIdleTimer() {
|
|
80
|
+
if (_idleTimer) clearTimeout(_idleTimer)
|
|
81
|
+
let delay = IDLE_TIMEOUT_MS
|
|
82
|
+
try {
|
|
83
|
+
if (PRESSURE_MIN_FREE_BYTES > 0 && os.freemem() < PRESSURE_MIN_FREE_BYTES) delay = 1
|
|
84
|
+
} catch {}
|
|
85
|
+
if (delay <= 0) return
|
|
86
|
+
_idleTimer = setTimeout(() => releaseLoaded(
|
|
87
|
+
delay <= 1 ? 'host memory pressure' : 'idle timeout',
|
|
88
|
+
), delay)
|
|
89
|
+
_idleTimer.unref?.()
|
|
90
|
+
}
|
|
91
|
+
|
|
57
92
|
function modelDir(dataDir) {
|
|
58
93
|
return path.join(dataDir, 'kiwi-model', KIWI_MODEL_VERSION)
|
|
59
94
|
}
|
|
@@ -125,9 +160,18 @@ function readModelFiles(dir) {
|
|
|
125
160
|
|
|
126
161
|
// Lazy, idempotent, never-throwing init. Fire-and-forget at boot.
|
|
127
162
|
export async function init(dataDir, log = () => {}) {
|
|
128
|
-
if (_state === 'ready') return true
|
|
129
|
-
if (_initPromise) return _initPromise
|
|
130
163
|
_log = typeof log === 'function' ? log : (() => {})
|
|
164
|
+
if (_state === 'ready') {
|
|
165
|
+
touchIdleTimer()
|
|
166
|
+
return true
|
|
167
|
+
}
|
|
168
|
+
if (_initPromise) return _initPromise
|
|
169
|
+
try {
|
|
170
|
+
if (PRESSURE_MIN_FREE_BYTES > 0 && os.freemem() < PRESSURE_MIN_FREE_BYTES) {
|
|
171
|
+
_log('[memory-service] kiwi morph load deferred under host memory pressure\n')
|
|
172
|
+
return false
|
|
173
|
+
}
|
|
174
|
+
} catch {}
|
|
131
175
|
_state = 'loading'
|
|
132
176
|
const t0 = Date.now()
|
|
133
177
|
_log(`[memory-service] kiwi morph init start (model ${KIWI_MODEL_VERSION})\n`)
|
|
@@ -151,6 +195,7 @@ export async function init(dataDir, log = () => {}) {
|
|
|
151
195
|
_state = 'ready'
|
|
152
196
|
_initMs = Date.now() - t0
|
|
153
197
|
_log(`[memory-service] kiwi morph ready in ${_initMs}ms (model ${KIWI_MODEL_VERSION}, rss≈${Math.round(process.memoryUsage().rss / 1e6)}MB)\n`)
|
|
198
|
+
touchIdleTimer()
|
|
154
199
|
return true
|
|
155
200
|
})().catch(err => {
|
|
156
201
|
_state = 'failed'
|
|
@@ -168,6 +213,7 @@ export function analyze(text) {
|
|
|
168
213
|
if (!s) return null
|
|
169
214
|
try {
|
|
170
215
|
const r = _kiwi.analyze(s)
|
|
216
|
+
touchIdleTimer()
|
|
171
217
|
return Array.isArray(r?.tokens) ? r.tokens : null
|
|
172
218
|
} catch {
|
|
173
219
|
return null
|
|
@@ -189,8 +235,5 @@ export function stems(text) {
|
|
|
189
235
|
}
|
|
190
236
|
|
|
191
237
|
export function reset() {
|
|
192
|
-
|
|
193
|
-
_initPromise = null
|
|
194
|
-
_kiwi = null
|
|
195
|
-
_initMs = 0
|
|
238
|
+
releaseLoaded()
|
|
196
239
|
}
|