@modusensus/dsh-mneme 0.3.6 → 0.3.7
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/README.md +0 -3
- package/lib/index.js +27 -17
- package/lib/local-embedder.js +7 -1
- package/lib/service.js +84 -1
- package/package.json +1 -1
- package/src/index.js +27 -17
- package/src/local-embedder.js +7 -1
- package/src/service.js +84 -1
package/README.md
CHANGED
|
@@ -265,9 +265,6 @@ npm run sync # 把 src/ 同步到 lib/(发布时由 prepack 钩子自动
|
|
|
265
265
|
|
|
266
266
|
> 设计文档位于仓库根 `docs/`,链接以 `../docs/` 相对路径指向(GitHub 上从本目录打开可正常跳转)。
|
|
267
267
|
|
|
268
|
-
- [记忆库设计](../docs/superpowers/specs/2026-08-13-dsh-mneme-design.md)
|
|
269
|
-
- [autoDream 设计](../docs/superpowers/specs/2026-08-13-dsh-mneme-autodream-design.md)
|
|
270
|
-
- [实施计划](../docs/superpowers/plans/2026-08-13-dsh-memory-autodream.md)
|
|
271
268
|
- [实体结构化记忆设计](docs/ENTITIES.md)
|
|
272
269
|
- [语义增强架构](docs/SEMANTIC.md)
|
|
273
270
|
- [本地模型部署指南](docs/LOCAL_MODEL.md)
|
package/lib/index.js
CHANGED
|
@@ -80,11 +80,28 @@ export const apply = (ctx, config) => {
|
|
|
80
80
|
const vectorIndex = createVectorIndex({ store, logger: ctx.logger });
|
|
81
81
|
service.setVectorIndex(vectorIndex);
|
|
82
82
|
|
|
83
|
+
// Human edits in mirror files win on every sync; merge them back first.
|
|
84
|
+
// TYPE_FILE maps each memory type to its mirror filename. Read every type's
|
|
85
|
+
// edits up front: mergeHumanEdits re-renders ALL mirror files on success, so
|
|
86
|
+
// a per-type read-then-merge loop would overwrite edits in files not yet read
|
|
87
|
+
// (e.g. preferences.md merging would clobber unsynced projects.md edits).
|
|
88
|
+
const humanEdits = new Map();
|
|
89
|
+
for (const type of Object.keys(TYPE_FILE)) {
|
|
90
|
+
humanEdits.set(type, mirror.readHumanEdits(type));
|
|
91
|
+
}
|
|
92
|
+
const applyHumanEdits = () => {
|
|
93
|
+
for (const [type, edits] of humanEdits) {
|
|
94
|
+
if (edits.length) service.mergeHumanEdits(type, edits);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
83
98
|
let embedder = null;
|
|
84
99
|
let reranker = null;
|
|
85
100
|
if (cfg.embedProvider === "openai") {
|
|
86
101
|
embedder = createEmbedder({ store, settings, logger: ctx.logger });
|
|
87
102
|
service.setEmbedder(embedder);
|
|
103
|
+
// legacy OpenAI embedder is immediately usable
|
|
104
|
+
applyHumanEdits();
|
|
88
105
|
} else {
|
|
89
106
|
try {
|
|
90
107
|
embedder = createEmbedderByProvider(cfg.embedProvider, {
|
|
@@ -97,12 +114,18 @@ export const apply = (ctx, config) => {
|
|
|
97
114
|
logger: ctx.logger
|
|
98
115
|
});
|
|
99
116
|
service.setEmbedder(embedder);
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
117
|
+
// issue #6: wait for extractor init before applying human edits, so
|
|
118
|
+
// scheduled embeddings see a ready embedder.
|
|
119
|
+
embedder.init()
|
|
120
|
+
.then(() => applyHumanEdits())
|
|
121
|
+
.catch((error) => {
|
|
122
|
+
ctx.logger?.warn?.(`[dsh-mneme] embedder init failed, search degrades to keyword: ${String(error)}`);
|
|
123
|
+
service.setEmbedder(null);
|
|
124
|
+
applyHumanEdits();
|
|
125
|
+
});
|
|
104
126
|
} catch (error) {
|
|
105
127
|
ctx.logger?.warn?.(`[dsh-mneme] embedder unavailable, search degrades to keyword: ${String(error)}`);
|
|
128
|
+
applyHumanEdits();
|
|
106
129
|
}
|
|
107
130
|
}
|
|
108
131
|
|
|
@@ -139,19 +162,6 @@ export const apply = (ctx, config) => {
|
|
|
139
162
|
commands.sync();
|
|
140
163
|
}
|
|
141
164
|
|
|
142
|
-
// Human edits in mirror files win on every sync; merge them back first.
|
|
143
|
-
// TYPE_FILE maps each memory type to its mirror filename. Read every type's
|
|
144
|
-
// edits up front: mergeHumanEdits re-renders ALL mirror files on success, so
|
|
145
|
-
// a per-type read-then-merge loop would overwrite edits in files not yet read
|
|
146
|
-
// (e.g. preferences.md merging would clobber unsynced projects.md edits).
|
|
147
|
-
const humanEdits = new Map();
|
|
148
|
-
for (const type of Object.keys(TYPE_FILE)) {
|
|
149
|
-
humanEdits.set(type, mirror.readHumanEdits(type));
|
|
150
|
-
}
|
|
151
|
-
for (const [type, edits] of humanEdits) {
|
|
152
|
-
if (edits.length) service.mergeHumanEdits(type, edits);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
165
|
// Dream scheduler: automatic consolidation + summary runs, triggered by
|
|
156
166
|
// store growth. Writes through the service fire the dream hook, which asks
|
|
157
167
|
// the scheduler to (re)schedule a run once absolute and since-last-run
|
package/lib/local-embedder.js
CHANGED
|
@@ -58,16 +58,21 @@ export class LocalEmbedder {
|
|
|
58
58
|
// Test hook: replace the pipeline factory without touching modules.
|
|
59
59
|
this.engineFactory = opts.engineFactory || defaultPipelineLoader;
|
|
60
60
|
this.extractor = null;
|
|
61
|
+
// issue #6: readiness flag for the service's scheduleEmbed gate. False until
|
|
62
|
+
// init() succeeds, so "ready" in embedder is observable even pre-init.
|
|
63
|
+
this.ready = false;
|
|
61
64
|
}
|
|
62
65
|
|
|
63
|
-
/** Load the model; throws when it cannot be loaded. */
|
|
66
|
+
/** Load the model; throws when it cannot be loaded. Idempotent. */
|
|
64
67
|
async init() {
|
|
68
|
+
if (this.extractor) return this; // already initialized: no-op
|
|
65
69
|
const options = {
|
|
66
70
|
dtype: this.useDtype,
|
|
67
71
|
device: this.device
|
|
68
72
|
};
|
|
69
73
|
if (this.cacheDir) options.cache_dir = this.cacheDir;
|
|
70
74
|
this.extractor = await this.engineFactory("feature-extraction", this.model, options);
|
|
75
|
+
this.ready = true; // service reads this to flush queued re-embeds
|
|
71
76
|
this.logger?.info?.(
|
|
72
77
|
`[dsh-mneme] local embedder ready: ${this.model} (dim=${this._dimension}, device=${this.device})`
|
|
73
78
|
);
|
|
@@ -107,6 +112,7 @@ export class LocalEmbedder {
|
|
|
107
112
|
// best-effort: some engines free resources on GC
|
|
108
113
|
}
|
|
109
114
|
this.extractor = null;
|
|
115
|
+
this.ready = false;
|
|
110
116
|
}
|
|
111
117
|
}
|
|
112
118
|
|
package/lib/service.js
CHANGED
|
@@ -37,11 +37,69 @@ export function createService({ store, mirror, config, onWrite, logger }) {
|
|
|
37
37
|
// replays them exactly once against the committed state.
|
|
38
38
|
let txDepth = 0;
|
|
39
39
|
|
|
40
|
+
// issue #6 (part 2): startup race defense. A local embedder (LocalEmbedder /
|
|
41
|
+
// Ollama) exposes an async init(), so between `setEmbedder` and init()
|
|
42
|
+
// resolving there is a window where embedSingle would throw "not initialized"
|
|
43
|
+
// and the re-embed would be silently dropped. When the embedder carries a
|
|
44
|
+
// `ready` flag we queue writes in embedPending until init sets ready=true,
|
|
45
|
+
// then flush them through the embedder's real interface. Embedders without a
|
|
46
|
+
// `ready` flag (legacy OpenAI, instantly usable) keep their old behavior.
|
|
47
|
+
let embedPending = [];
|
|
48
|
+
let embedReadyTimer = null;
|
|
49
|
+
const EMBED_PENDING_MAX = 100; // bound the queue; drop oldest beyond this
|
|
50
|
+
const EMBED_READY_POLL_MS = 100;
|
|
51
|
+
const EMBED_READY_POLL_LIMIT = 30; // ~3s ceiling; never poll forever
|
|
52
|
+
|
|
53
|
+
/** Flush the queued re-embeds once the embedder is ready. Fail-safe. */
|
|
54
|
+
function flushEmbedPending() {
|
|
55
|
+
if (!embedder || embedPending.length === 0) return;
|
|
56
|
+
const batch = embedPending.splice(0, embedPending.length);
|
|
57
|
+
for (const memory of batch) {
|
|
58
|
+
try {
|
|
59
|
+
if (!memory?.id) continue;
|
|
60
|
+
if (typeof embedder.schedule === "function") {
|
|
61
|
+
embedder.schedule(memory);
|
|
62
|
+
} else if (typeof embedder.embedSingle === "function") {
|
|
63
|
+
const text = [memory.title, memory.content].filter(Boolean).join("\n");
|
|
64
|
+
if (!text) continue;
|
|
65
|
+
embedder
|
|
66
|
+
.embedSingle(text)
|
|
67
|
+
.then((vec) => {
|
|
68
|
+
if (Array.isArray(vec) && vec.length) {
|
|
69
|
+
store.setEmbedding(memory.id, vec);
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
.catch((err) => {
|
|
73
|
+
logger?.warn?.("flushEmbedPending embedSingle failed:", err);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
} catch (err) {
|
|
77
|
+
logger?.warn?.("flushEmbedPending failed:", err);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function stopEmbedReadyPolling() {
|
|
83
|
+
if (embedReadyTimer) {
|
|
84
|
+
clearInterval(embedReadyTimer);
|
|
85
|
+
embedReadyTimer = null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
40
89
|
function scheduleEmbed(memory) {
|
|
41
90
|
try {
|
|
42
91
|
if (txDepth > 0) return; // deferred to the transaction's commit
|
|
43
92
|
if (!embedder || !memory?.id) return;
|
|
44
93
|
|
|
94
|
+
// Readiness gate: embedder exposes `ready` (async init) and is not ready
|
|
95
|
+
// yet — queue instead of firing embedSingle into a half-built extractor.
|
|
96
|
+
const hasReady = "ready" in embedder;
|
|
97
|
+
if (hasReady && embedder.ready !== true) {
|
|
98
|
+
if (embedPending.length >= EMBED_PENDING_MAX) embedPending.shift();
|
|
99
|
+
embedPending.push(memory);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
45
103
|
if (typeof embedder.schedule === "function") {
|
|
46
104
|
embedder.schedule(memory);
|
|
47
105
|
return;
|
|
@@ -681,7 +739,32 @@ export function createService({ store, mirror, config, onWrite, logger }) {
|
|
|
681
739
|
toApiList,
|
|
682
740
|
transaction,
|
|
683
741
|
setDreamHook(fn) { dreamHook = fn; },
|
|
684
|
-
setEmbedder(emb) {
|
|
742
|
+
setEmbedder(emb) {
|
|
743
|
+
embedder = emb;
|
|
744
|
+
if (!emb) {
|
|
745
|
+
// embedder removed (init failed in index.js): stop polling and drop
|
|
746
|
+
// queued re-embeds — search just degrades to keyword.
|
|
747
|
+
stopEmbedReadyPolling();
|
|
748
|
+
embedPending = [];
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
if (emb.ready === true) {
|
|
752
|
+
flushEmbedPending();
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
// Async-initializing embedder: poll `ready` until it flips, then flush.
|
|
756
|
+
if ("ready" in emb && embedReadyTimer === null) {
|
|
757
|
+
let attempts = 0;
|
|
758
|
+
embedReadyTimer = setInterval(() => {
|
|
759
|
+
attempts++;
|
|
760
|
+
if (emb.ready === true || attempts >= EMBED_READY_POLL_LIMIT) {
|
|
761
|
+
stopEmbedReadyPolling();
|
|
762
|
+
if (emb.ready === true) flushEmbedPending();
|
|
763
|
+
else embedPending = []; // init never landed: drop the queue
|
|
764
|
+
}
|
|
765
|
+
}, EMBED_READY_POLL_MS);
|
|
766
|
+
}
|
|
767
|
+
},
|
|
685
768
|
setEntityExtractor(fn) { entityExtractor = fn; },
|
|
686
769
|
setVectorIndex(vi) { vectorIndex = vi; },
|
|
687
770
|
setReranker(rn) { reranker = rn; },
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modusensus/dsh-mneme",
|
|
3
3
|
"description": "Cross-session memory plugin for DeepSeek Harness with autoDream consolidation: SQLite store, Markdown mirrors, 6 model tools, automatic injection, session summarization, user profile/rules, custom slash commands, vector (semantic) search, and a Web GUI panel",
|
|
4
|
-
"version": "0.3.
|
|
4
|
+
"version": "0.3.7",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "lib/index.js",
|
package/src/index.js
CHANGED
|
@@ -80,11 +80,28 @@ export const apply = (ctx, config) => {
|
|
|
80
80
|
const vectorIndex = createVectorIndex({ store, logger: ctx.logger });
|
|
81
81
|
service.setVectorIndex(vectorIndex);
|
|
82
82
|
|
|
83
|
+
// Human edits in mirror files win on every sync; merge them back first.
|
|
84
|
+
// TYPE_FILE maps each memory type to its mirror filename. Read every type's
|
|
85
|
+
// edits up front: mergeHumanEdits re-renders ALL mirror files on success, so
|
|
86
|
+
// a per-type read-then-merge loop would overwrite edits in files not yet read
|
|
87
|
+
// (e.g. preferences.md merging would clobber unsynced projects.md edits).
|
|
88
|
+
const humanEdits = new Map();
|
|
89
|
+
for (const type of Object.keys(TYPE_FILE)) {
|
|
90
|
+
humanEdits.set(type, mirror.readHumanEdits(type));
|
|
91
|
+
}
|
|
92
|
+
const applyHumanEdits = () => {
|
|
93
|
+
for (const [type, edits] of humanEdits) {
|
|
94
|
+
if (edits.length) service.mergeHumanEdits(type, edits);
|
|
95
|
+
}
|
|
96
|
+
};
|
|
97
|
+
|
|
83
98
|
let embedder = null;
|
|
84
99
|
let reranker = null;
|
|
85
100
|
if (cfg.embedProvider === "openai") {
|
|
86
101
|
embedder = createEmbedder({ store, settings, logger: ctx.logger });
|
|
87
102
|
service.setEmbedder(embedder);
|
|
103
|
+
// legacy OpenAI embedder is immediately usable
|
|
104
|
+
applyHumanEdits();
|
|
88
105
|
} else {
|
|
89
106
|
try {
|
|
90
107
|
embedder = createEmbedderByProvider(cfg.embedProvider, {
|
|
@@ -97,12 +114,18 @@ export const apply = (ctx, config) => {
|
|
|
97
114
|
logger: ctx.logger
|
|
98
115
|
});
|
|
99
116
|
service.setEmbedder(embedder);
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
117
|
+
// issue #6: wait for extractor init before applying human edits, so
|
|
118
|
+
// scheduled embeddings see a ready embedder.
|
|
119
|
+
embedder.init()
|
|
120
|
+
.then(() => applyHumanEdits())
|
|
121
|
+
.catch((error) => {
|
|
122
|
+
ctx.logger?.warn?.(`[dsh-mneme] embedder init failed, search degrades to keyword: ${String(error)}`);
|
|
123
|
+
service.setEmbedder(null);
|
|
124
|
+
applyHumanEdits();
|
|
125
|
+
});
|
|
104
126
|
} catch (error) {
|
|
105
127
|
ctx.logger?.warn?.(`[dsh-mneme] embedder unavailable, search degrades to keyword: ${String(error)}`);
|
|
128
|
+
applyHumanEdits();
|
|
106
129
|
}
|
|
107
130
|
}
|
|
108
131
|
|
|
@@ -139,19 +162,6 @@ export const apply = (ctx, config) => {
|
|
|
139
162
|
commands.sync();
|
|
140
163
|
}
|
|
141
164
|
|
|
142
|
-
// Human edits in mirror files win on every sync; merge them back first.
|
|
143
|
-
// TYPE_FILE maps each memory type to its mirror filename. Read every type's
|
|
144
|
-
// edits up front: mergeHumanEdits re-renders ALL mirror files on success, so
|
|
145
|
-
// a per-type read-then-merge loop would overwrite edits in files not yet read
|
|
146
|
-
// (e.g. preferences.md merging would clobber unsynced projects.md edits).
|
|
147
|
-
const humanEdits = new Map();
|
|
148
|
-
for (const type of Object.keys(TYPE_FILE)) {
|
|
149
|
-
humanEdits.set(type, mirror.readHumanEdits(type));
|
|
150
|
-
}
|
|
151
|
-
for (const [type, edits] of humanEdits) {
|
|
152
|
-
if (edits.length) service.mergeHumanEdits(type, edits);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
165
|
// Dream scheduler: automatic consolidation + summary runs, triggered by
|
|
156
166
|
// store growth. Writes through the service fire the dream hook, which asks
|
|
157
167
|
// the scheduler to (re)schedule a run once absolute and since-last-run
|
package/src/local-embedder.js
CHANGED
|
@@ -58,16 +58,21 @@ export class LocalEmbedder {
|
|
|
58
58
|
// Test hook: replace the pipeline factory without touching modules.
|
|
59
59
|
this.engineFactory = opts.engineFactory || defaultPipelineLoader;
|
|
60
60
|
this.extractor = null;
|
|
61
|
+
// issue #6: readiness flag for the service's scheduleEmbed gate. False until
|
|
62
|
+
// init() succeeds, so "ready" in embedder is observable even pre-init.
|
|
63
|
+
this.ready = false;
|
|
61
64
|
}
|
|
62
65
|
|
|
63
|
-
/** Load the model; throws when it cannot be loaded. */
|
|
66
|
+
/** Load the model; throws when it cannot be loaded. Idempotent. */
|
|
64
67
|
async init() {
|
|
68
|
+
if (this.extractor) return this; // already initialized: no-op
|
|
65
69
|
const options = {
|
|
66
70
|
dtype: this.useDtype,
|
|
67
71
|
device: this.device
|
|
68
72
|
};
|
|
69
73
|
if (this.cacheDir) options.cache_dir = this.cacheDir;
|
|
70
74
|
this.extractor = await this.engineFactory("feature-extraction", this.model, options);
|
|
75
|
+
this.ready = true; // service reads this to flush queued re-embeds
|
|
71
76
|
this.logger?.info?.(
|
|
72
77
|
`[dsh-mneme] local embedder ready: ${this.model} (dim=${this._dimension}, device=${this.device})`
|
|
73
78
|
);
|
|
@@ -107,6 +112,7 @@ export class LocalEmbedder {
|
|
|
107
112
|
// best-effort: some engines free resources on GC
|
|
108
113
|
}
|
|
109
114
|
this.extractor = null;
|
|
115
|
+
this.ready = false;
|
|
110
116
|
}
|
|
111
117
|
}
|
|
112
118
|
|
package/src/service.js
CHANGED
|
@@ -37,11 +37,69 @@ export function createService({ store, mirror, config, onWrite, logger }) {
|
|
|
37
37
|
// replays them exactly once against the committed state.
|
|
38
38
|
let txDepth = 0;
|
|
39
39
|
|
|
40
|
+
// issue #6 (part 2): startup race defense. A local embedder (LocalEmbedder /
|
|
41
|
+
// Ollama) exposes an async init(), so between `setEmbedder` and init()
|
|
42
|
+
// resolving there is a window where embedSingle would throw "not initialized"
|
|
43
|
+
// and the re-embed would be silently dropped. When the embedder carries a
|
|
44
|
+
// `ready` flag we queue writes in embedPending until init sets ready=true,
|
|
45
|
+
// then flush them through the embedder's real interface. Embedders without a
|
|
46
|
+
// `ready` flag (legacy OpenAI, instantly usable) keep their old behavior.
|
|
47
|
+
let embedPending = [];
|
|
48
|
+
let embedReadyTimer = null;
|
|
49
|
+
const EMBED_PENDING_MAX = 100; // bound the queue; drop oldest beyond this
|
|
50
|
+
const EMBED_READY_POLL_MS = 100;
|
|
51
|
+
const EMBED_READY_POLL_LIMIT = 30; // ~3s ceiling; never poll forever
|
|
52
|
+
|
|
53
|
+
/** Flush the queued re-embeds once the embedder is ready. Fail-safe. */
|
|
54
|
+
function flushEmbedPending() {
|
|
55
|
+
if (!embedder || embedPending.length === 0) return;
|
|
56
|
+
const batch = embedPending.splice(0, embedPending.length);
|
|
57
|
+
for (const memory of batch) {
|
|
58
|
+
try {
|
|
59
|
+
if (!memory?.id) continue;
|
|
60
|
+
if (typeof embedder.schedule === "function") {
|
|
61
|
+
embedder.schedule(memory);
|
|
62
|
+
} else if (typeof embedder.embedSingle === "function") {
|
|
63
|
+
const text = [memory.title, memory.content].filter(Boolean).join("\n");
|
|
64
|
+
if (!text) continue;
|
|
65
|
+
embedder
|
|
66
|
+
.embedSingle(text)
|
|
67
|
+
.then((vec) => {
|
|
68
|
+
if (Array.isArray(vec) && vec.length) {
|
|
69
|
+
store.setEmbedding(memory.id, vec);
|
|
70
|
+
}
|
|
71
|
+
})
|
|
72
|
+
.catch((err) => {
|
|
73
|
+
logger?.warn?.("flushEmbedPending embedSingle failed:", err);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
} catch (err) {
|
|
77
|
+
logger?.warn?.("flushEmbedPending failed:", err);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function stopEmbedReadyPolling() {
|
|
83
|
+
if (embedReadyTimer) {
|
|
84
|
+
clearInterval(embedReadyTimer);
|
|
85
|
+
embedReadyTimer = null;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
40
89
|
function scheduleEmbed(memory) {
|
|
41
90
|
try {
|
|
42
91
|
if (txDepth > 0) return; // deferred to the transaction's commit
|
|
43
92
|
if (!embedder || !memory?.id) return;
|
|
44
93
|
|
|
94
|
+
// Readiness gate: embedder exposes `ready` (async init) and is not ready
|
|
95
|
+
// yet — queue instead of firing embedSingle into a half-built extractor.
|
|
96
|
+
const hasReady = "ready" in embedder;
|
|
97
|
+
if (hasReady && embedder.ready !== true) {
|
|
98
|
+
if (embedPending.length >= EMBED_PENDING_MAX) embedPending.shift();
|
|
99
|
+
embedPending.push(memory);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
45
103
|
if (typeof embedder.schedule === "function") {
|
|
46
104
|
embedder.schedule(memory);
|
|
47
105
|
return;
|
|
@@ -681,7 +739,32 @@ export function createService({ store, mirror, config, onWrite, logger }) {
|
|
|
681
739
|
toApiList,
|
|
682
740
|
transaction,
|
|
683
741
|
setDreamHook(fn) { dreamHook = fn; },
|
|
684
|
-
setEmbedder(emb) {
|
|
742
|
+
setEmbedder(emb) {
|
|
743
|
+
embedder = emb;
|
|
744
|
+
if (!emb) {
|
|
745
|
+
// embedder removed (init failed in index.js): stop polling and drop
|
|
746
|
+
// queued re-embeds — search just degrades to keyword.
|
|
747
|
+
stopEmbedReadyPolling();
|
|
748
|
+
embedPending = [];
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
if (emb.ready === true) {
|
|
752
|
+
flushEmbedPending();
|
|
753
|
+
return;
|
|
754
|
+
}
|
|
755
|
+
// Async-initializing embedder: poll `ready` until it flips, then flush.
|
|
756
|
+
if ("ready" in emb && embedReadyTimer === null) {
|
|
757
|
+
let attempts = 0;
|
|
758
|
+
embedReadyTimer = setInterval(() => {
|
|
759
|
+
attempts++;
|
|
760
|
+
if (emb.ready === true || attempts >= EMBED_READY_POLL_LIMIT) {
|
|
761
|
+
stopEmbedReadyPolling();
|
|
762
|
+
if (emb.ready === true) flushEmbedPending();
|
|
763
|
+
else embedPending = []; // init never landed: drop the queue
|
|
764
|
+
}
|
|
765
|
+
}, EMBED_READY_POLL_MS);
|
|
766
|
+
}
|
|
767
|
+
},
|
|
685
768
|
setEntityExtractor(fn) { entityExtractor = fn; },
|
|
686
769
|
setVectorIndex(vi) { vectorIndex = vi; },
|
|
687
770
|
setReranker(rn) { reranker = rn; },
|