@modusensus/dsh-mneme 0.11.0 → 0.13.0
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 +54 -21
- package/docs/CONFIGURATION.md +45 -4
- package/docs/DIRECTORIES.md +114 -0
- package/docs/DISTILL.md +64 -0
- package/docs/DSH-GO.md +97 -0
- package/docs/ENTITIES.md +5 -3
- package/docs/FORGET.md +77 -0
- package/docs/KB.md +110 -0
- package/docs/SLEEP.md +28 -3
- package/docs/STORAGE.md +23 -0
- package/lib/api-standalone.js +4 -1
- package/lib/api.js +744 -26
- package/lib/attachment-text.js +122 -0
- package/lib/choro-command.js +9 -2
- package/lib/client.js +3752 -819
- package/lib/config.js +171 -2
- package/lib/continuity.js +7 -6
- package/lib/diary/auto.js +190 -0
- package/lib/diary/compose.js +440 -0
- package/lib/diary/dir.js +59 -0
- package/lib/diary/export.js +97 -0
- package/lib/diary/index.js +26 -0
- package/lib/diary/library.js +189 -0
- package/lib/diary/qq-inbox.js +142 -0
- package/lib/diary/rollover.js +61 -0
- package/lib/diary/session-provider.js +122 -0
- package/lib/document.js +9 -3
- package/lib/dream/clustering.js +6 -1
- package/lib/dream/decisions.js +56 -5
- package/lib/dream/sleep.js +616 -15
- package/lib/dream.js +45 -13
- package/lib/dsh-go/host.js +162 -0
- package/lib/dsh-go/panel.js +2091 -0
- package/lib/dsh-go/text.js +342 -0
- package/lib/dsh-go/views/common.js +68 -0
- package/lib/dsh-go/views/entity.js +48 -0
- package/lib/dsh-go/views/memory.js +198 -0
- package/lib/dsh-go/views/root.js +30 -0
- package/lib/dsh-go/views/settings.js +398 -0
- package/lib/dsh-go/views/status.js +51 -0
- package/lib/embedding.js +42 -8
- package/lib/entities/extractor.js +36 -7
- package/lib/hot-memory.js +8 -9
- package/lib/index.js +131 -5
- package/lib/inject.js +192 -19
- package/lib/kb/index.js +309 -0
- package/lib/kb/routes.js +220 -0
- package/lib/kb/tools.js +148 -0
- package/lib/lang.js +115 -4
- package/lib/peak-hours.js +57 -2
- package/lib/profile-facts.js +356 -0
- package/lib/semantic.js +347 -60
- package/lib/service.js +302 -70
- package/lib/settings.js +117 -14
- package/lib/store.js +830 -55
- package/lib/summarize.js +38 -11
- package/lib/tools.js +173 -25
- package/lib/user-settings.js +175 -23
- package/lib/vector-index.js +76 -6
- package/lib/wiki/cli-hint.js +90 -0
- package/lib/wiki/queries.js +264 -10
- package/lib/wiki/t1-jobs.js +232 -0
- package/lib/wiki/t1.js +7 -1
- package/package.json +25 -1
- package/scripts/sync-test-badge.mjs +1 -1
- package/scripts/test-count-sync.mjs +9 -5
- package/src/api-standalone.js +4 -1
- package/src/api.js +744 -26
- package/src/attachment-text.js +122 -0
- package/src/choro-command.js +9 -2
- package/src/config.js +171 -2
- package/src/continuity.js +7 -6
- package/src/diary/auto.js +190 -0
- package/src/diary/compose.js +440 -0
- package/src/diary/dir.js +59 -0
- package/src/diary/export.js +97 -0
- package/src/diary/index.js +26 -0
- package/src/diary/library.js +189 -0
- package/src/diary/qq-inbox.js +142 -0
- package/src/diary/rollover.js +61 -0
- package/src/diary/session-provider.js +122 -0
- package/src/document.js +9 -3
- package/src/dream/clustering.js +6 -1
- package/src/dream/decisions.js +56 -5
- package/src/dream/sleep.js +616 -15
- package/src/dream.js +45 -13
- package/src/dsh-go/host.js +162 -0
- package/src/dsh-go/panel.js +2091 -0
- package/src/dsh-go/text.js +342 -0
- package/src/dsh-go/views/common.js +68 -0
- package/src/dsh-go/views/entity.js +48 -0
- package/src/dsh-go/views/memory.js +198 -0
- package/src/dsh-go/views/root.js +30 -0
- package/src/dsh-go/views/settings.js +398 -0
- package/src/dsh-go/views/status.js +51 -0
- package/src/embedding.js +42 -8
- package/src/entities/extractor.js +36 -7
- package/src/hot-memory.js +8 -9
- package/src/index.js +131 -5
- package/src/inject.js +192 -19
- package/src/kb/index.js +309 -0
- package/src/kb/routes.js +220 -0
- package/src/kb/tools.js +148 -0
- package/src/lang.js +115 -4
- package/src/peak-hours.js +57 -2
- package/src/profile-facts.js +356 -0
- package/src/semantic.js +347 -60
- package/src/service.js +302 -70
- package/src/settings.js +117 -14
- package/src/store.js +830 -55
- package/src/summarize.js +38 -11
- package/src/tools.js +173 -25
- package/src/user-settings.js +175 -23
- package/src/vector-index.js +76 -6
- package/src/wiki/cli-hint.js +90 -0
- package/src/wiki/queries.js +264 -10
- package/src/wiki/t1-jobs.js +232 -0
- package/src/wiki/t1.js +7 -1
- package/test/api-routes-gaps.test.js +63 -0
- package/test/api.test.js +377 -4
- package/test/attachment-text.test.js +140 -0
- package/test/bm25-corpus-cache.test.js +145 -0
- package/test/capacity-warning.test.js +262 -0
- package/test/client.test.js +908 -138
- package/test/config.test.js +79 -0
- package/test/conflict-freeze.test.js +45 -1
- package/test/continuity.test.js +8 -1
- package/test/diary-library.test.js +187 -0
- package/test/diary.test.js +576 -0
- package/test/document.test.js +32 -0
- package/test/dream-maxtokens-fallback.test.js +3 -3
- package/test/dream.test.js +62 -1
- package/test/dsh-go-host.test.js +134 -0
- package/test/dsh-go-panel-actions.test.js +854 -0
- package/test/dsh-go-panel.test.js +251 -0
- package/test/embedding-cache-f32.test.js +276 -0
- package/test/embedding-ready.test.js +33 -3
- package/test/entities.test.js +73 -0
- package/test/forget-corrections.test.js +70 -0
- package/test/image-memory.test.js +171 -0
- package/test/inject-pin-explicit.test.js +300 -0
- package/test/inject-pin-vector-crowd.test.js +225 -0
- package/test/issue-617-step1-query-and-fusion.test.js +361 -0
- package/test/kb-api-wiring.test.js +128 -0
- package/test/kb-collections.test.js +272 -0
- package/test/kb-routes.test.js +379 -0
- package/test/mint-source.test.js +26 -0
- package/test/peak-hours-observability.test.js +70 -0
- package/test/persona-api.test.js +3 -3
- package/test/persona-by-session.test.js +568 -0
- package/test/persona-settings.test.js +38 -7
- package/test/profile-facts.test.js +492 -0
- package/test/reflection-update-gate.test.js +129 -0
- package/test/reindex-backfill.test.js +334 -0
- package/test/reindex-scheduler.test.js +369 -0
- package/test/scope-candidates.test.js +249 -0
- package/test/session-gate.test.js +1 -1
- package/test/sleep-action-set.test.js +14 -0
- package/test/sleep-heat.test.js +49 -0
- package/test/sleep.test.js +598 -0
- package/test/standalone-api.test.js +24 -0
- package/test/store.test.js +20 -0
- package/test/summarize.test.js +26 -0
- package/test/test-count-sync.test.js +14 -3
- package/test/tool-state-ignore.test.js +65 -0
- package/test/tools-dto-schema-matrix.test.js +23 -2
- package/test/tools.test.js +144 -7
- package/test/user-settings-segments.test.js +528 -0
- package/test/vector-index.test.js +120 -6
- package/test/wiki-api.test.js +476 -1
- package/test/wiki-cli-hint.test.js +118 -0
- package/test/wiki-queries.test.js +91 -1
- package/test/wiki-t1-jobs.test.js +125 -0
- package/test/wiki-tools.test.js +131 -76
|
@@ -125,6 +125,340 @@ test("rows with unknown provenance (NULL embedding_model) count as stale", async
|
|
|
125
125
|
store.close();
|
|
126
126
|
});
|
|
127
127
|
|
|
128
|
+
test("#604: a permanently failing row is tried once, not once per batch", async () => {
|
|
129
|
+
// A failed row is never written, so it stays stale and the next pick() hands
|
|
130
|
+
// it straight back. Measured on the old loop: 9 of 20 budget slots burned on
|
|
131
|
+
// one bad row, and the summary still said "capped" — as if the budget had run
|
|
132
|
+
// out with other work waiting behind it.
|
|
133
|
+
const store = createStore(":memory:");
|
|
134
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
135
|
+
for (let i = 0; i < 12; i++) {
|
|
136
|
+
service.saveWithDedupe({ type: "preference", title: `记忆${i}`, content: `内容${i}` });
|
|
137
|
+
}
|
|
138
|
+
const bad = "记忆11\n内容11";
|
|
139
|
+
const calls = [];
|
|
140
|
+
const embedder = {
|
|
141
|
+
embedSingle: async (text) => {
|
|
142
|
+
calls.push(text);
|
|
143
|
+
return text === bad ? null : [1, 0, 0]; // legacy adapters signal failure by returning null
|
|
144
|
+
},
|
|
145
|
+
modelHash: "h#1",
|
|
146
|
+
dimension: 3
|
|
147
|
+
};
|
|
148
|
+
const vectorIndex = createVectorIndex({ store });
|
|
149
|
+
|
|
150
|
+
const summary = await backfillMissingEmbeddings({
|
|
151
|
+
store, embedder, vectorIndex,
|
|
152
|
+
logger: { info() {} },
|
|
153
|
+
maxTotal: 20, batchSize: 10, rateLimitMs: 0
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const badCalls = calls.filter((t) => t === bad).length;
|
|
157
|
+
assert.equal(badCalls, 1, "the bad row is attempted exactly once");
|
|
158
|
+
assert.equal(summary.indexed, 11, "every healthy row still gets embedded");
|
|
159
|
+
assert.equal(summary.failed, 1, "one failure counted, not nine");
|
|
160
|
+
assert.equal(summary.finishReason, "stalled", "queue is not empty but nothing is left to try");
|
|
161
|
+
assert.deepEqual(summary.failureReasons, { "empty-vector": 1 });
|
|
162
|
+
store.close();
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
test("#604: maxTotal: 0 with a permanently failing row terminates", async () => {
|
|
166
|
+
// The old loop used `unlimited || done < maxTotal`, so with maxTotal: 0 the
|
|
167
|
+
// counter never bounded anything and a failing row kept the queue non-empty
|
|
168
|
+
// forever. Measured: still running after 25s, embedSingle called in a tight
|
|
169
|
+
// loop. This must return, and return quickly.
|
|
170
|
+
const store = createStore(":memory:");
|
|
171
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
172
|
+
for (let i = 0; i < 4; i++) {
|
|
173
|
+
service.saveWithDedupe({ type: "preference", title: `行${i}`, content: `C${i}` });
|
|
174
|
+
}
|
|
175
|
+
let calls = 0;
|
|
176
|
+
const embedder = {
|
|
177
|
+
embedSingle: async () => { calls++; throw new Error("provider down"); },
|
|
178
|
+
modelHash: "h#1",
|
|
179
|
+
dimension: 3
|
|
180
|
+
};
|
|
181
|
+
const vectorIndex = createVectorIndex({ store });
|
|
182
|
+
|
|
183
|
+
const summary = await backfillMissingEmbeddings({
|
|
184
|
+
store, embedder, vectorIndex,
|
|
185
|
+
logger: { info() {} },
|
|
186
|
+
maxTotal: 0, batchSize: 10, rateLimitMs: 0
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
assert.equal(calls, 4, "each row attempted once, then the run stops");
|
|
190
|
+
assert.equal(summary.indexed, 0);
|
|
191
|
+
assert.equal(summary.failed, 4);
|
|
192
|
+
// 这里抛错,按失败种类归 `errored`(provider 不可达,值得重试)而不是
|
|
193
|
+
// `stalled`(死行,重试无意义)。本条用例真正要锁的是「它会停」——
|
|
194
|
+
// 调用次数与退出行为不因分档而变,重试策略由调度器决定。
|
|
195
|
+
assert.equal(summary.finishReason, "errored");
|
|
196
|
+
assert.equal(summary.stalled, false);
|
|
197
|
+
assert.equal(summary.stillStale, true, "the rows are still stale — they will be retried next boot");
|
|
198
|
+
assert.deepEqual(summary.failureReasons, { "error:Error: provider down": 4 });
|
|
199
|
+
store.close();
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
test("#604: a dead prefix at the queue head does not starve the healthy tail", async () => {
|
|
203
|
+
// pick() is ORDER BY updated_at DESC and a failed row is never written, so a
|
|
204
|
+
// block of permanently failing rows sits at the head forever. With a fixed
|
|
205
|
+
// batchSize window the filter removed the whole batch every round and the run
|
|
206
|
+
// ended `stalled` without trying a single healthy row behind them — measured
|
|
207
|
+
// on this code: okCalls 0, indexed 0, 20 rows still stale.
|
|
208
|
+
const store = createStore(":memory:");
|
|
209
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
210
|
+
// Written first ⇒ older updated_at ⇒ BEHIND the failing block under DESC.
|
|
211
|
+
for (let i = 0; i < 10; i++) {
|
|
212
|
+
service.saveWithDedupe({ type: "preference", title: `好行${i}`, content: `good-${i}` });
|
|
213
|
+
}
|
|
214
|
+
// Written last ⇒ newest updated_at ⇒ the queue head.
|
|
215
|
+
for (let i = 0; i < 10; i++) {
|
|
216
|
+
service.saveWithDedupe({ type: "preference", title: `坏行${i}`, content: `bad-${i}` });
|
|
217
|
+
}
|
|
218
|
+
let okCalls = 0;
|
|
219
|
+
let badCalls = 0;
|
|
220
|
+
const embedder = {
|
|
221
|
+
embedSingle: async (text) => {
|
|
222
|
+
if (String(text).includes("bad-")) { badCalls++; return null; }
|
|
223
|
+
okCalls++; return [1, 0, 0];
|
|
224
|
+
},
|
|
225
|
+
modelHash: "h#1",
|
|
226
|
+
dimension: 3
|
|
227
|
+
};
|
|
228
|
+
const vectorIndex = createVectorIndex({ store });
|
|
229
|
+
|
|
230
|
+
const summary = await backfillMissingEmbeddings({
|
|
231
|
+
store, embedder, vectorIndex,
|
|
232
|
+
logger: { info() {} },
|
|
233
|
+
maxTotal: 500, batchSize: 10, rateLimitMs: 0
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
assert.equal(okCalls, 10, "every healthy row behind the dead prefix is attempted");
|
|
237
|
+
assert.equal(summary.indexed, 10, "and actually embedded");
|
|
238
|
+
assert.equal(badCalls, 10, "each failing row is still tried exactly once");
|
|
239
|
+
assert.equal(summary.failed, 10);
|
|
240
|
+
assert.equal(store.needsEmbeddingFor("h#1", 100).length, 10, "only the failing rows remain stale");
|
|
241
|
+
store.close();
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test("#604: a dead prefix deeper than the pick window reports 'stalled', not 'capped'", async () => {
|
|
245
|
+
// The window widening above has a bound (maxWindow = 500). A dead prefix at
|
|
246
|
+
// least that deep fills every round's window, so `rows` is never empty, the
|
|
247
|
+
// loop exits on the budget, and `stalled` never gets set — the run used to
|
|
248
|
+
// report "capped" with indexed 0, and the scheduler re-ran it 21 times:
|
|
249
|
+
// 10,500 provider calls, nothing embedded. "capped" must mean work is left
|
|
250
|
+
// AND the budget stopped us; a run that embedded nothing made no progress
|
|
251
|
+
// and has to say so, so the next boot becomes the retry point.
|
|
252
|
+
const store = createStore(":memory:");
|
|
253
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
254
|
+
for (let i = 0; i < 10; i++) {
|
|
255
|
+
service.saveWithDedupe({ type: "preference", title: `好行${i}`, content: `good-${i}` });
|
|
256
|
+
}
|
|
257
|
+
for (let i = 0; i < 500; i++) {
|
|
258
|
+
service.saveWithDedupe({ type: "preference", title: `坏行${i}`, content: `bad-${i}` });
|
|
259
|
+
}
|
|
260
|
+
let okCalls = 0;
|
|
261
|
+
const embedder = {
|
|
262
|
+
embedSingle: async (text) => {
|
|
263
|
+
if (String(text).includes("bad-")) return null;
|
|
264
|
+
okCalls++; return [1, 0, 0];
|
|
265
|
+
},
|
|
266
|
+
modelHash: "h#1",
|
|
267
|
+
dimension: 3
|
|
268
|
+
};
|
|
269
|
+
const vectorIndex = createVectorIndex({ store });
|
|
270
|
+
|
|
271
|
+
const summary = await backfillMissingEmbeddings({
|
|
272
|
+
store, embedder, vectorIndex,
|
|
273
|
+
logger: { info() {} },
|
|
274
|
+
maxTotal: 500, batchSize: 10, rateLimitMs: 0
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
assert.equal(summary.indexed, 0, "the dead prefix consumed the whole budget");
|
|
278
|
+
assert.equal(summary.failed, 500);
|
|
279
|
+
assert.equal(
|
|
280
|
+
summary.finishReason, "stalled",
|
|
281
|
+
"no progress means stalled — 'capped' would make the scheduler burn the same rows again"
|
|
282
|
+
);
|
|
283
|
+
assert.equal(okCalls, 0, "the healthy tail is simply out of reach this run");
|
|
284
|
+
store.close();
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
test("#604: a whole batch of transient failures reports 'errored', not 'stalled'", async () => {
|
|
288
|
+
// 10 条全是好行,但这一轮恰好全撞上 503。indexed === 0,如果只按「没进展」
|
|
289
|
+
// 判 stalled,重试就被推到下次启动——而 provider 一秒后已经好了。
|
|
290
|
+
// 判据在 failureReasons:抛错落 `error:` 桶(瞬时),null 向量落
|
|
291
|
+
// `empty-vector`(对该行是永久性)。
|
|
292
|
+
const store = createStore(":memory:");
|
|
293
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
294
|
+
for (let i = 0; i < 10; i++) {
|
|
295
|
+
service.saveWithDedupe({ type: "preference", title: `好行${i}`, content: `good-${i}` });
|
|
296
|
+
}
|
|
297
|
+
const vectorIndex = createVectorIndex({ store });
|
|
298
|
+
let calls = 0;
|
|
299
|
+
const embedder = {
|
|
300
|
+
embedSingle: async () => {
|
|
301
|
+
calls++;
|
|
302
|
+
throw new Error("503 Service Unavailable");
|
|
303
|
+
},
|
|
304
|
+
modelHash: "h#1", dimension: 3
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
const summary = await backfillMissingEmbeddings({
|
|
308
|
+
store, embedder, vectorIndex, logger: { info() {} },
|
|
309
|
+
maxTotal: 500, batchSize: 10, rateLimitMs: 0
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
assert.equal(summary.indexed, 0);
|
|
313
|
+
assert.equal(
|
|
314
|
+
summary.finishReason, "errored",
|
|
315
|
+
"a throw is a provider problem, not a dead row — the scheduler retries it on a short leash"
|
|
316
|
+
);
|
|
317
|
+
assert.equal(summary.stalled, false, "'stalled' must keep meaning 'no retry will help'");
|
|
318
|
+
assert.ok(
|
|
319
|
+
Object.keys(summary.failureReasons).some((r) => r.startsWith("error:")),
|
|
320
|
+
"the transient reason is named, not folded into a generic bucket"
|
|
321
|
+
);
|
|
322
|
+
assert.equal(store.needsEmbeddingFor("h#1", 100).length, 10, "nothing was lost");
|
|
323
|
+
store.close();
|
|
324
|
+
});
|
|
325
|
+
|
|
326
|
+
test("#604: permanent failures still report 'stalled' even mixed with the same count", async () => {
|
|
327
|
+
// 与上一条同样的形状(10 条、一条没嵌上),唯一区别是失败形态:null 而不是抛错。
|
|
328
|
+
// 两条并列,锁住「分档看的是失败种类,不是数量」。
|
|
329
|
+
const store = createStore(":memory:");
|
|
330
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
331
|
+
for (let i = 0; i < 10; i++) {
|
|
332
|
+
service.saveWithDedupe({ type: "preference", title: `坏行${i}`, content: `bad-${i}` });
|
|
333
|
+
}
|
|
334
|
+
const vectorIndex = createVectorIndex({ store });
|
|
335
|
+
const embedder = {
|
|
336
|
+
embedSingle: async () => null,
|
|
337
|
+
modelHash: "h#1", dimension: 3
|
|
338
|
+
};
|
|
339
|
+
|
|
340
|
+
const summary = await backfillMissingEmbeddings({
|
|
341
|
+
store, embedder, vectorIndex, logger: { info() {} },
|
|
342
|
+
maxTotal: 500, batchSize: 10, rateLimitMs: 0
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
assert.equal(summary.indexed, 0);
|
|
346
|
+
assert.equal(summary.finishReason, "stalled", "nothing transient here: retrying now is pointless");
|
|
347
|
+
assert.equal(summary.stalled, true);
|
|
348
|
+
assert.deepEqual(summary.failureReasons, { "empty-vector": 10 });
|
|
349
|
+
store.close();
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test("#604: a detailed adapter turns a provider outage into 'errored' on the legacy path", async () => {
|
|
353
|
+
// 这台机器实际跑的是 legacy OpenAI 兼容适配器(embedProvider 默认 openai)。
|
|
354
|
+
// 它的 embedSingle() 只回向量、把原因丢掉,所以 provider 挂掉与「这行坏了」
|
|
355
|
+
// 在旧路径上是同一个 null。embedSingleDetailed() 把原因带出来,回填才能分档。
|
|
356
|
+
const store = createStore(":memory:");
|
|
357
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
358
|
+
for (let i = 0; i < 10; i++) {
|
|
359
|
+
service.saveWithDedupe({ type: "preference", title: `行${i}`, content: `legacy-${i}` });
|
|
360
|
+
}
|
|
361
|
+
const vectorIndex = createVectorIndex({ store });
|
|
362
|
+
let detailedCalls = 0;
|
|
363
|
+
const embedder = {
|
|
364
|
+
// Deliberately present and returning null: if the backfill ever prefers it
|
|
365
|
+
// over embedSingleDetailed, the run degrades to `stalled` and this fails.
|
|
366
|
+
embedSingle: async () => null,
|
|
367
|
+
embedSingleDetailed: async () => {
|
|
368
|
+
detailedCalls++;
|
|
369
|
+
return { vector: null, reason: "http-503" };
|
|
370
|
+
},
|
|
371
|
+
modelHash: "h#1", dimension: 3
|
|
372
|
+
};
|
|
373
|
+
|
|
374
|
+
const summary = await backfillMissingEmbeddings({
|
|
375
|
+
store, embedder, vectorIndex, logger: { info() {} },
|
|
376
|
+
maxTotal: 500, batchSize: 10, rateLimitMs: 0
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
assert.equal(detailedCalls, 10, "the detailed adapter is the one that gets called");
|
|
380
|
+
assert.equal(summary.indexed, 0);
|
|
381
|
+
assert.equal(
|
|
382
|
+
summary.finishReason, "errored",
|
|
383
|
+
"an http-503 is the provider's problem, so the scheduler retries it in-boot"
|
|
384
|
+
);
|
|
385
|
+
assert.deepEqual(summary.failureReasons, { "error:http-503": 10 });
|
|
386
|
+
assert.equal(store.needsEmbeddingFor("h#1", 100).length, 10, "nothing was lost");
|
|
387
|
+
store.close();
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
test("#604: a detailed adapter keeps an unembeddable row permanent", async () => {
|
|
391
|
+
// 同一个详细适配器、同样的条数,只把 reason 换成永久的那一类。分档必须跟着
|
|
392
|
+
// 原因走,而不是跟着「有没有 detailed 方法」走。
|
|
393
|
+
const store = createStore(":memory:");
|
|
394
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
395
|
+
for (let i = 0; i < 10; i++) {
|
|
396
|
+
service.saveWithDedupe({ type: "preference", title: `行${i}`, content: `bad-${i}` });
|
|
397
|
+
}
|
|
398
|
+
const vectorIndex = createVectorIndex({ store });
|
|
399
|
+
const embedder = {
|
|
400
|
+
embedSingle: async () => null,
|
|
401
|
+
embedSingleDetailed: async () => ({ vector: null, reason: "response-no-vector" }),
|
|
402
|
+
modelHash: "h#1", dimension: 3
|
|
403
|
+
};
|
|
404
|
+
|
|
405
|
+
const summary = await backfillMissingEmbeddings({
|
|
406
|
+
store, embedder, vectorIndex, logger: { info() {} },
|
|
407
|
+
maxTotal: 500, batchSize: 10, rateLimitMs: 0
|
|
408
|
+
});
|
|
409
|
+
|
|
410
|
+
assert.equal(summary.finishReason, "stalled");
|
|
411
|
+
assert.deepEqual(summary.failureReasons, { "error:response-no-vector": 10 });
|
|
412
|
+
store.close();
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
test("#604: a missing config field is permanent, so it must not buy error retries", async () => {
|
|
416
|
+
// config-incomplete 也以 error: 开头,但它每次都会以同样方式失败。若按前缀
|
|
417
|
+
// 一律算瞬时,配置不全的机器会在每次启动时空跑 1+3 轮。
|
|
418
|
+
const store = createStore(":memory:");
|
|
419
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
420
|
+
for (let i = 0; i < 10; i++) {
|
|
421
|
+
service.saveWithDedupe({ type: "preference", title: `行${i}`, content: `cfg-${i}` });
|
|
422
|
+
}
|
|
423
|
+
const vectorIndex = createVectorIndex({ store });
|
|
424
|
+
const embedder = {
|
|
425
|
+
embedSingle: async () => null,
|
|
426
|
+
embedSingleDetailed: async () => ({ vector: null, reason: "config-incomplete:apiKey" }),
|
|
427
|
+
modelHash: "h#1", dimension: 3
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
const summary = await backfillMissingEmbeddings({
|
|
431
|
+
store, embedder, vectorIndex, logger: { info() {} },
|
|
432
|
+
maxTotal: 500, batchSize: 10, rateLimitMs: 0
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
assert.equal(
|
|
436
|
+
summary.finishReason, "stalled",
|
|
437
|
+
"an incomplete config is not an outage; retrying it only burns the error budget"
|
|
438
|
+
);
|
|
439
|
+
assert.equal(summary.stalled, true);
|
|
440
|
+
store.close();
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
test("#604: a run with no failures can reach 'drained' even with a nonzero budget", async () => {
|
|
444
|
+
const store = createStore(":memory:");
|
|
445
|
+
const service = createService({ store, mirror: null, config: {} });
|
|
446
|
+
for (let i = 0; i < 3; i++) {
|
|
447
|
+
service.saveWithDedupe({ type: "preference", title: `行${i}`, content: `C${i}` });
|
|
448
|
+
}
|
|
449
|
+
const embedder = { embedSingle: async () => [1, 0, 0], modelHash: "h#1", dimension: 3 };
|
|
450
|
+
const vectorIndex = createVectorIndex({ store });
|
|
451
|
+
|
|
452
|
+
const summary = await backfillMissingEmbeddings({
|
|
453
|
+
store, embedder, vectorIndex, logger: { info() {} },
|
|
454
|
+
maxTotal: 100, batchSize: 10, rateLimitMs: 0
|
|
455
|
+
});
|
|
456
|
+
assert.equal(summary.finishReason, "drained");
|
|
457
|
+
assert.equal(summary.stalled, false);
|
|
458
|
+
assert.equal(summary.failureReasons, null, "no failure map when nothing failed");
|
|
459
|
+
store.close();
|
|
460
|
+
});
|
|
461
|
+
|
|
128
462
|
test("setEmbedding records provenance and clearing the vector clears it", async () => {
|
|
129
463
|
const store = createStore(":memory:");
|
|
130
464
|
const service = createService({ store, mirror: null, config: {} });
|