@modusensus/dsh-mneme 0.12.0 → 0.14.0-beta.1
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 +51 -20
- package/bin/dsh-mneme-choro.mjs +9 -1
- package/docs/CONFIGURATION.md +47 -6
- 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 +788 -30
- package/lib/attachment-text.js +122 -0
- package/lib/choro-command.js +9 -2
- package/lib/client.js +4627 -1409
- package/lib/config.js +166 -1
- 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/dream/decisions.js +95 -6
- package/lib/dream/sleep.js +760 -19
- package/lib/dream/triage.js +139 -0
- package/lib/dream.js +168 -21
- 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 +22 -11
- package/lib/index.js +123 -3
- package/lib/inject.js +203 -20
- 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 +98 -4
- package/lib/peak-hours.js +57 -2
- package/lib/semantic.js +347 -60
- package/lib/service.js +268 -69
- package/lib/settings.js +128 -15
- package/lib/store.js +585 -26
- package/lib/summarize.js +38 -11
- package/lib/tools.js +35 -4
- package/lib/user-settings.js +128 -32
- package/lib/vector-index.js +76 -6
- package/lib/wiki/claims.js +267 -0
- package/lib/wiki/cli-hint.js +90 -0
- package/lib/wiki/queries.js +282 -2
- package/lib/wiki/render.js +74 -0
- package/lib/wiki/t0.js +43 -6
- package/lib/wiki/t1-jobs.js +232 -0
- package/lib/wiki/t1.js +145 -32
- package/package.json +39 -1
- package/scripts/release-docs.mjs +7 -1
- package/scripts/release-notes.mjs +4 -1
- package/src/api-standalone.js +4 -1
- package/src/api.js +788 -30
- package/src/attachment-text.js +122 -0
- package/src/choro-command.js +9 -2
- package/src/config.js +166 -1
- 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/dream/decisions.js +95 -6
- package/src/dream/sleep.js +760 -19
- package/src/dream/triage.js +139 -0
- package/src/dream.js +168 -21
- 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 +22 -11
- package/src/index.js +123 -3
- package/src/inject.js +203 -20
- 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 +98 -4
- package/src/peak-hours.js +57 -2
- package/src/semantic.js +347 -60
- package/src/service.js +268 -69
- package/src/settings.js +128 -15
- package/src/store.js +585 -26
- package/src/summarize.js +38 -11
- package/src/tools.js +35 -4
- package/src/user-settings.js +128 -32
- package/src/vector-index.js +76 -6
- package/src/wiki/claims.js +267 -0
- package/src/wiki/cli-hint.js +90 -0
- package/src/wiki/queries.js +282 -2
- package/src/wiki/render.js +74 -0
- package/src/wiki/t0.js +43 -6
- package/src/wiki/t1-jobs.js +232 -0
- package/src/wiki/t1.js +145 -32
- package/test/api-routes-gaps.test.js +63 -0
- package/test/api.test.js +385 -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 +1051 -185
- package/test/conflict-freeze.test.js +45 -1
- package/test/conflict-ledger-enforce.test.js +452 -0
- package/test/conflict-ledger.test.js +242 -0
- package/test/continuity.test.js +8 -1
- package/test/contrast.test.js +124 -12
- package/test/diary-library.test.js +187 -0
- package/test/diary.test.js +576 -0
- package/test/dream-baseline-clamp.test.js +174 -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-ready.test.js +33 -3
- package/test/entities.test.js +73 -0
- package/test/forget-corrections.test.js +70 -0
- package/test/graph-api.test.js +124 -0
- package/test/hot-memory.test.js +48 -0
- package/test/image-memory.test.js +171 -0
- package/test/inject-parent-gate.test.js +6 -3
- package/test/inject.test.js +59 -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/panel-chart-scale.test.js +90 -0
- package/test/panel-tokens.test.js +245 -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 +7 -7
- 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/release-docs.test.js +20 -0
- package/test/release-notes.test.js +25 -0
- package/test/scope-candidates.test.js +249 -0
- package/test/security-doc.test.js +10 -0
- package/test/sleep-action-set.test.js +14 -0
- package/test/sleep-heat.test.js +49 -0
- package/test/sleep.test.js +635 -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/user-settings-segments.test.js +528 -0
- package/test/vector-index.test.js +120 -6
- package/test/wiki-api.test.js +463 -1
- package/test/wiki-claims.test.js +194 -0
- package/test/wiki-cli-hint.test.js +118 -0
- package/test/wiki-provenance.test.js +202 -0
- package/test/wiki-queries.test.js +128 -0
- package/test/wiki-t0.test.js +77 -1
- package/test/wiki-t1-jobs.test.js +125 -0
- package/test/wiki-t1.test.js +226 -1
package/src/summarize.js
CHANGED
|
@@ -9,6 +9,8 @@ import { createScopeResolver } from "./scope.js";
|
|
|
9
9
|
// #493 补充 §③:断链与巩固侧同一份实现(src/fence.js)。转录里出现闭合标记时
|
|
10
10
|
// 原先能提前闭合围栏——那是把用户消息升格成指令最短的一条路。
|
|
11
11
|
import { breakFenceMarkers } from "./fence.js";
|
|
12
|
+
// Issue #611:图片块的文本口径(占位,不是空串)。三个 textOf 副本共用这一份。
|
|
13
|
+
import { contentToText, countImages } from "./attachment-text.js";
|
|
12
14
|
// #512:每轮蒸馏一个 run id(归因主体)。store 侧也能铸造,但这里必须知道它——
|
|
13
15
|
// 写记忆时要把它一路带到 saveMemoryProvenance / 落 run 行,拿回来再写就是两次往返。
|
|
14
16
|
import { randomUUID } from "node:crypto";
|
|
@@ -292,13 +294,13 @@ const SUBAGENT_MESSAGE_KINDS = new Set(["agent-message", "subagent-settled"]);
|
|
|
292
294
|
|
|
293
295
|
// 兼容严格形状 [{type:"text",text}] 与宽松形状 ["字符串", ...](lib-smoke 用例
|
|
294
296
|
// 直接传字符串数组)。只取公开文本块;reasoning 私有推理块不进蒸馏上下文。
|
|
297
|
+
//
|
|
298
|
+
// Issue #611:图片块曾经在这里一律变空串,于是图-only 消息整条被丢、图+文消息只剩
|
|
299
|
+
// 文字——静默,没有任何计数。现在改由 attachment-text 统一产占位,
|
|
300
|
+
// `[图: 屏幕截图 … 1543x983]`。抽取口径只有一份,三个调用方(本文件 /
|
|
301
|
+
// continuity / hot-memory)共用,避免同一件事写成三份会漂移的实现。
|
|
295
302
|
function textOf(content) {
|
|
296
|
-
|
|
297
|
-
if (!Array.isArray(content)) return "";
|
|
298
|
-
return content
|
|
299
|
-
.map((block) => (typeof block === "string" ? block : (block && block.type === "text" && typeof block.text === "string" ? block.text : "")))
|
|
300
|
-
.filter((s) => s)
|
|
301
|
-
.join("\n");
|
|
303
|
+
return contentToText(content);
|
|
302
304
|
}
|
|
303
305
|
|
|
304
306
|
/**
|
|
@@ -333,6 +335,10 @@ function publicTranscriptParts(event) {
|
|
|
333
335
|
parts.push({
|
|
334
336
|
role: "agent",
|
|
335
337
|
text,
|
|
338
|
+
// Issue #611:把产文本的那份 content 一起带出去。判定与文本同源,计量才能
|
|
339
|
+
// 跟着同一份判定走(见 collectMessages 里的 imageCount)——另写一套「哪些块
|
|
340
|
+
// 算公开」给计量用,就是第二份实现,迟早与这份漂移。
|
|
341
|
+
blocks: Array.isArray(message?.content) ? message.content : [],
|
|
336
342
|
// 同一条子会话消息可能同时出现在 user/message 与 agent/inbox/spliced
|
|
337
343
|
// 里(投递路径重叠),按 id(无 id 时按内容)去重,否则转录会重复。
|
|
338
344
|
key: typeof message?.id === "string" && message.id
|
|
@@ -346,7 +352,7 @@ function publicTranscriptParts(event) {
|
|
|
346
352
|
const text = textOf(data?.content);
|
|
347
353
|
if (!text.trim()) break;
|
|
348
354
|
if (SUBAGENT_MESSAGE_KINDS.has(kind)) pushAgentMessage(data);
|
|
349
|
-
else parts.push({ role: "user", text });
|
|
355
|
+
else parts.push({ role: "user", text, blocks: Array.isArray(data?.content) ? data.content : [] });
|
|
350
356
|
break;
|
|
351
357
|
}
|
|
352
358
|
case "agent/inbox/spliced": {
|
|
@@ -358,7 +364,7 @@ function publicTranscriptParts(event) {
|
|
|
358
364
|
const blocks = Array.isArray(data?.message?.content) ? data.message.content : [];
|
|
359
365
|
const text = textOf(blocks);
|
|
360
366
|
// 私有推理块(reasoning)刻意不采集:蒸馏记忆不得沉淀模型私有思考链。
|
|
361
|
-
if (text.trim()) parts.push({ role: "assistant", text });
|
|
367
|
+
if (text.trim()) parts.push({ role: "assistant", text, blocks });
|
|
362
368
|
break;
|
|
363
369
|
}
|
|
364
370
|
default:
|
|
@@ -395,6 +401,10 @@ function collectMessages(session, maxChars = 8000, language = "zh", afterSeq, th
|
|
|
395
401
|
const events = snapshotSessionEvents(session, afterSeq, throughSeq);
|
|
396
402
|
const lines = [];
|
|
397
403
|
const seenSubagentMessages = new Set();
|
|
404
|
+
// Issue #611:这一窗里被折成占位的图片张数。占位本身不丢信息,但「折了几张」
|
|
405
|
+
// 必须是个能读到的数——否则 off 档和旧版静默丢弃在观测上无法区分,下次再有人
|
|
406
|
+
// 问「图到底进来没有」还是得翻源码。计量只数窗口内事件,不受截断影响。
|
|
407
|
+
let imageCount = 0;
|
|
398
408
|
const trim = (s, n) => (typeof s === "string" && s.length > n ? `${s.slice(0, n)}…` : s);
|
|
399
409
|
const trimTranscript = (s, n) => {
|
|
400
410
|
if (typeof s !== "string" || s.length <= n) return s;
|
|
@@ -421,9 +431,20 @@ function collectMessages(session, maxChars = 8000, language = "zh", afterSeq, th
|
|
|
421
431
|
if (part.role === "agent") {
|
|
422
432
|
if (part.key && seenSubagentMessages.has(part.key)) continue;
|
|
423
433
|
if (part.key) seenSubagentMessages.add(part.key);
|
|
424
|
-
|
|
425
|
-
|
|
434
|
+
}
|
|
435
|
+
// Issue #611 计量:数「真正进了转录的图块」,所以按 part 数、并且在去重之后
|
|
436
|
+
// ——同一条子会话消息会同时出现在 user/message 与 agent/inbox/spliced 里
|
|
437
|
+
// (投递路径重叠),按事件数会把它算两遍。
|
|
438
|
+
//
|
|
439
|
+
// 不按事件数还有个更要紧的理由:被 publicTranscriptParts 挡掉的注入消息
|
|
440
|
+
// (工具结果、插件注入、陌生 source.kind)里的图根本进不了转录,按事件累加
|
|
441
|
+
// 会报「这一窗有 2 张图」而转录里一张都没有。part 是渲染与落盘共用的同一份
|
|
442
|
+
// 判定,计量跟着它走,两处不会再各有一套口径。
|
|
443
|
+
imageCount += countImages(part.blocks);
|
|
444
|
+
if (part.role === "user") {
|
|
426
445
|
lines.push(STR.transcriptUser[language](part.text));
|
|
446
|
+
} else if (part.role === "agent") {
|
|
447
|
+
lines.push(STR.transcriptAgent[language](part.text));
|
|
427
448
|
} else {
|
|
428
449
|
lines.push(STR.transcriptAssistant[language](part.text));
|
|
429
450
|
}
|
|
@@ -485,6 +506,9 @@ function collectMessages(session, maxChars = 8000, language = "zh", afterSeq, th
|
|
|
485
506
|
: [],
|
|
486
507
|
hasEvents: events.length > 0,
|
|
487
508
|
lastSeq,
|
|
509
|
+
// Issue #611:这一窗折成占位的图片张数。调用方把它写进审计 metadata,
|
|
510
|
+
// 让「图进来了几张」成为可对账的事实而不是推断。
|
|
511
|
+
imageCount,
|
|
488
512
|
// Issue #493:只数**转录正文**的字符数。minWindowChars 闸门与审计里的
|
|
489
513
|
// distillChars 量的都是「这一窗有多少可提炼内容」,不是「我们额外包了多少
|
|
490
514
|
// 提示词」——若把围栏与角色句算进去,一条「短」也会凭空越过任何阈值,
|
|
@@ -850,6 +874,7 @@ export function createSummarizer(ctx, service, config, deps = {}, sessionGate =
|
|
|
850
874
|
0
|
|
851
875
|
);
|
|
852
876
|
const minWindowChars = config.summarizeMinWindowChars ?? 0;
|
|
877
|
+
const imageCount = collected.imageCount ?? 0;
|
|
853
878
|
if (minWindowChars > 0 && distillChars < minWindowChars) {
|
|
854
879
|
commitCursor(session.id, nextSeq);
|
|
855
880
|
writeAudit({
|
|
@@ -857,7 +882,7 @@ export function createSummarizer(ctx, service, config, deps = {}, sessionGate =
|
|
|
857
882
|
model_id: route ? `${route.provider}:${route.model}` : "unknown",
|
|
858
883
|
status: "skipped",
|
|
859
884
|
error_message: "window-too-small",
|
|
860
|
-
metadata: { distillChars, minWindowChars }
|
|
885
|
+
metadata: { distillChars, minWindowChars, ...(imageCount > 0 ? { imageCount } : {}) }
|
|
861
886
|
});
|
|
862
887
|
return;
|
|
863
888
|
}
|
|
@@ -1064,6 +1089,7 @@ export function createSummarizer(ctx, service, config, deps = {}, sessionGate =
|
|
|
1064
1089
|
audit.errorMessage = failureAudit.errorMessage;
|
|
1065
1090
|
audit.metadata = {
|
|
1066
1091
|
distillChars,
|
|
1092
|
+
...(imageCount > 0 ? { imageCount } : {}),
|
|
1067
1093
|
...failureAudit.metadata,
|
|
1068
1094
|
streak,
|
|
1069
1095
|
...(maxParseRetries > 0 ? { maxRetries: maxParseRetries } : {}),
|
|
@@ -1203,6 +1229,7 @@ export function createSummarizer(ctx, service, config, deps = {}, sessionGate =
|
|
|
1203
1229
|
if (audit) {
|
|
1204
1230
|
audit.metadata = {
|
|
1205
1231
|
distillChars,
|
|
1232
|
+
...(imageCount > 0 ? { imageCount } : {}),
|
|
1206
1233
|
parsed: parsed.length,
|
|
1207
1234
|
...(parsedResult.salvaged ? { json_salvaged: true } : {}),
|
|
1208
1235
|
...(capped > 0 ? { capped } : {}),
|
package/src/tools.js
CHANGED
|
@@ -9,8 +9,16 @@ import { TOOL_GUIDE, CONTINUITY_TOOL_RULE } from "./guide.js";
|
|
|
9
9
|
import { injectChildEnabled } from "./config.js";
|
|
10
10
|
import { normalizeScopeStateDimension, normalizeScopeStateValue, normalizeScopeStateState } from "./scope-state.js";
|
|
11
11
|
import { lastUserQuery } from "./inject.js";
|
|
12
|
+
// issue #607:工具报错里的 CLI 命令同样要给绝对路径入口——profile 装的插件没有
|
|
13
|
+
// 全局 shim,光写 `dsh-mneme-choro` 用户照抄只会 command not found。
|
|
14
|
+
import { choroCliCommandFrom } from "./wiki/cli-hint.js";
|
|
15
|
+
// 知识库三工具(契约 §3.4):定义住在 kb/tools.js,本文件只决定注册不注册。
|
|
16
|
+
import { kbToolDefinitions } from "./kb/tools.js";
|
|
12
17
|
|
|
13
18
|
const TEXT_OUTPUT = (text) => [{ type: "text", text }];
|
|
19
|
+
// 工具报错里的命令串:本文件在 src/ 与 lib/ 同一层(bin/ 的父级),模块加载时推导一次。
|
|
20
|
+
// 推导不出(源码树被裁剪)就返回 null,文案退回通用说法。
|
|
21
|
+
const CHORO_CLI = choroCliCommandFrom(import.meta.url, "<repo>");
|
|
14
22
|
const incompleteChoroSearch = (pages) =>
|
|
15
23
|
`Search was incomplete because these pages could not be read (${pages.join(", ")}). Check the local checkout and page permissions.`;
|
|
16
24
|
// Per-registry tool-name registry: guards against duplicate registration on
|
|
@@ -81,7 +89,8 @@ const MEMORY_PROVENANCE = (m) => {
|
|
|
81
89
|
// choro(M1):chorography 只读查询对象(createChoroQueries 产物)。缺省 null =
|
|
82
90
|
// 装配层没接(headless/测试),三个 choro_* 工具走 hiddenTools 闸根本不注册。
|
|
83
91
|
// 它与 service 刻意分开传——chorography 的所有权模型与记忆域相反(spec §3)。
|
|
84
|
-
|
|
92
|
+
// kb:知识库装配对象(createKb 产物);缺省 null = 宿主没装它,三个 kb 工具不注册。
|
|
93
|
+
export function createTools(ctx, service, config, embedder, sessionGate = null, choro = null, kb = null) {
|
|
85
94
|
const toolsRegistry = ctx.tools;
|
|
86
95
|
let registration = REGISTERED_TOOLS.get(toolsRegistry);
|
|
87
96
|
if (!registration) {
|
|
@@ -99,7 +108,7 @@ export function createTools(ctx, service, config, embedder, sessionGate = null,
|
|
|
99
108
|
const row = choro.resolveRepo(ref);
|
|
100
109
|
if (!row) {
|
|
101
110
|
const reason = ref ? `Repository ${JSON.stringify(ref)} is not registered.` : "No repository is registered for chorography.";
|
|
102
|
-
throw new Error(`${reason} Run
|
|
111
|
+
throw new Error(`${reason} ${CHORO_CLI ? `Run \`${CHORO_CLI}\`` : "Run `dsh-mneme-choro <repo> --apply`"} to register a repository.`);
|
|
103
112
|
}
|
|
104
113
|
if (row.ambiguous) {
|
|
105
114
|
const choices = row.ambiguous.map(({ id, name }) => `${id} (${name})`).join(", ");
|
|
@@ -453,7 +462,11 @@ export function createTools(ctx, service, config, embedder, sessionGate = null,
|
|
|
453
462
|
name: "memory_forget",
|
|
454
463
|
description:
|
|
455
464
|
"Stop a memory from being auto-injected and from appearing in searches and lists without deleting it. " +
|
|
456
|
-
"The entry stays in storage; pass forgotten: false to restore it."
|
|
465
|
+
"The entry stays in storage; pass forgotten: false to restore it. " +
|
|
466
|
+
"#532: suppression is per ROW - facts fused into the same entry leave visibility together. " +
|
|
467
|
+
"When the request targets one fact among several held by a row, inspect the row first (memory_get), " +
|
|
468
|
+
"split it with memory_update if needed, or say in your reply what else the suppression took down " +
|
|
469
|
+
"(docs/FORGET.md records the supersession gap behind this).",
|
|
457
470
|
parameters: {
|
|
458
471
|
id: { type: "string", required: true },
|
|
459
472
|
forgotten: { type: "boolean", description: "Suppress (true, default) or restore (false) the entry's visibility" }
|
|
@@ -1220,7 +1233,7 @@ export function createTools(ctx, service, config, embedder, sessionGate = null,
|
|
|
1220
1233
|
throw new Error(`Page ${JSON.stringify(args?.slug ?? "")} was not found. Use choro_module_map to list page slugs.`);
|
|
1221
1234
|
}
|
|
1222
1235
|
if (page.error === "page-missing") {
|
|
1223
|
-
throw new Error(`Page ${JSON.stringify(page.slug)} is missing from disk (${page.relPath}). Check the local checkout or run
|
|
1236
|
+
throw new Error(`Page ${JSON.stringify(page.slug)} is missing from disk (${page.relPath}). Check the local checkout or run ${CHORO_CLI ? `\`${CHORO_CLI}\`` : "`dsh-mneme-choro <repo> --apply`"} to rebuild it.`);
|
|
1224
1237
|
}
|
|
1225
1238
|
if (page.error) {
|
|
1226
1239
|
throw new Error(`Page ${JSON.stringify(page.slug)} could not be read (${page.relPath}). Check the local file and its permissions.`);
|
|
@@ -1289,6 +1302,24 @@ export function createTools(ctx, service, config, embedder, sessionGate = null,
|
|
|
1289
1302
|
hiddenTools.add("choro_read_page");
|
|
1290
1303
|
hiddenTools.add("choro_search");
|
|
1291
1304
|
}
|
|
1305
|
+
// 知识库三工具(契约 §3.4):与 choro 同款反向语义。装配缺件(kb 为 null)与没开
|
|
1306
|
+
// 对模型同样不可见——可区分性归路由层(503 / 409),模型侧不需要看见半个知识库。
|
|
1307
|
+
if (config?.kbEnabled !== true || !kb) {
|
|
1308
|
+
hiddenTools.add("kb_list_collections");
|
|
1309
|
+
hiddenTools.add("kb_search");
|
|
1310
|
+
hiddenTools.add("kb_read_doc");
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
// 知识库工具定义在 kb/tools.js。**必须过 defineTool**:那里返回的是「describe 形状」
|
|
1314
|
+
// (属性上的 required:true、output.schema 用 spec 写法),直接 ctx.tools.register
|
|
1315
|
+
// 会被宿主的 JSON Schema 校验拒("required is not supported on type string")——
|
|
1316
|
+
// 这是接线时踩到的真故障,不是可选加工。判定按 parameters.type==="object" 分:
|
|
1317
|
+
// 已经是 JSON Schema 的说明调用方自己包过,再包一次会把 schema 转坏。
|
|
1318
|
+
if (typeof kbToolDefinitions === "function") {
|
|
1319
|
+
for (const def of kbToolDefinitions({ kb, cfg: config })) {
|
|
1320
|
+
tools.push(def?.parameters?.type === "object" ? def : defineTool(def));
|
|
1321
|
+
}
|
|
1322
|
+
}
|
|
1292
1323
|
|
|
1293
1324
|
for (const tool of tools) {
|
|
1294
1325
|
if (hiddenTools.has(tool.name)) {
|
package/src/user-settings.js
CHANGED
|
@@ -36,6 +36,14 @@ export function escapePromptVars(config, text) {
|
|
|
36
36
|
* 按会话身份取人格卡。返回 `{ card, source }`,`source` 是给面板解释「用的哪张卡」的
|
|
37
37
|
* 读数,不参与判定。
|
|
38
38
|
*
|
|
39
|
+
* 寻址维度(#606 起两维,子 agent 继承再加一维共三维):`sessionId`(绑会话卡,
|
|
40
|
+
* 最具体)优先于 `parentSessionId`(子 agent 继承父会话卡),最后是 `agentId`(按预设建卡)。
|
|
41
|
+
* 理由是具体性——会话比预设更具体,而 DS Bot 这类宿主里 `agentPreset` 恒为
|
|
42
|
+
* `"standard"`(`ds-bot/src/host/roster.js:72-80` 强制写入,实测 50 个会话里 43 个是
|
|
43
|
+
* 它),若让 byAgent 优先,则谁给 `standard` 建一张卡就会盖掉所有按会话建的卡。
|
|
44
|
+
* 父会话卡排在 byAgent 之前:子 agent 自己没卡时,它属于哪段对话比它属于哪个预设
|
|
45
|
+
* 更能说明「该用谁的人格」。
|
|
46
|
+
*
|
|
39
47
|
* `strict`(config.personaStrictAddressing,默认关)决定「有身份、没这张身份的卡」
|
|
40
48
|
* 与「这张卡被显式关掉」这两种情况:
|
|
41
49
|
* - 关(现状):落 default 卡。default 于是变成「所有没建卡的 agent 都能拿到它」,
|
|
@@ -44,20 +52,45 @@ export function escapePromptVars(config, text) {
|
|
|
44
52
|
* 身份」的会话(无名会话本来就是 default 卡的受众)。
|
|
45
53
|
* 默认关是有意的:这是一次口径变更,不能让老用户的注入文本在升级后静默变化。
|
|
46
54
|
*
|
|
47
|
-
* 严格模式有一格例外(#587
|
|
48
|
-
*
|
|
49
|
-
*
|
|
50
|
-
*
|
|
51
|
-
*
|
|
52
|
-
* 于是必然落进后一种处境:开了严格寻址就把唯一那张卡静默关死。
|
|
55
|
+
* 严格模式有一格例外(#587):两维**都没建过卡**时仍回落 default。两种处境必须
|
|
56
|
+
* 分开——「建了卡、只是没这一张的卡」是真该按 none 处理(否则关掉一张卡等于没关),
|
|
57
|
+
* 而「一张卡都没建」时 default 是用户手里唯一的一张卡,按 none 处理等于把它永久
|
|
58
|
+
* 废掉,且面板不报错。判据看两维合起来的键数(#606 起):只建了 bySession 卡时,
|
|
59
|
+
* 一个按 byAgent 寻址的会话同样不该被 default 兜底——用户已经做过分区了。
|
|
53
60
|
*/
|
|
54
|
-
export function resolvePersonaCard(
|
|
61
|
+
export function resolvePersonaCard(
|
|
62
|
+
settings,
|
|
63
|
+
agentId,
|
|
64
|
+
{ strict = false, sessionId = null, parentSessionId = null } = {}
|
|
65
|
+
) {
|
|
55
66
|
const store = settings.getPersona();
|
|
56
67
|
const id = typeof agentId === "string" && agentId.trim() ? agentId.trim() : "";
|
|
68
|
+
const sid = typeof sessionId === "string" && sessionId.trim() ? sessionId.trim() : "";
|
|
69
|
+
const pid =
|
|
70
|
+
typeof parentSessionId === "string" && parentSessionId.trim() ? parentSessionId.trim() : "";
|
|
71
|
+
// 第一维:绑会话卡(最具体)
|
|
72
|
+
const sessionCard = sid ? store.bySession?.[sid] : null;
|
|
73
|
+
if (sessionCard && sessionCard.enabled === true) return { card: sessionCard, source: "bySession" };
|
|
74
|
+
// 第二维(子 agent 继承):自己没卡就取父会话的卡。
|
|
75
|
+
// 为什么必须有这一维:子 agent 是新会话,用户不可能给每个派生会话都建一张卡;而
|
|
76
|
+
// strict 一旦生效,「有身份、没这张身份的卡」= 不注入人格,子 agent 会从「自称父
|
|
77
|
+
// 会话的人格」变成「不知道自己是谁」——比串味更糟。继承父会话卡才是用户能预期的
|
|
78
|
+
// 语义:同一个 agent 派生出去的手脚,仍是同一个 agent。
|
|
79
|
+
// 注意与 `src/session-gate.js` 的口径区别:那里**刻意不**用 parentSession 判「是不是
|
|
80
|
+
// 子代理」(宿主 fork 出来的主会话同样带 parentSession,用它判会把主会话误关)。
|
|
81
|
+
// 这里要问的不是「你是不是子代理」而是「你自己有没有卡」,所以 fork 出的分支继承
|
|
82
|
+
// 父卡也是正确的(它就是同一段对话的副本)。父 id 与自身 id 相同(脏数据)时跳过,
|
|
83
|
+
// 免得一次寻址查两遍同一个键。
|
|
84
|
+
const parentCard = pid && pid !== sid ? store.bySession?.[pid] : null;
|
|
85
|
+
if (parentCard && parentCard.enabled === true) return { card: parentCard, source: "parentSession" };
|
|
86
|
+
// 第三维:按预设/身份建的卡
|
|
57
87
|
const card = id ? store.byAgent[id] : null;
|
|
58
88
|
if (card && card.enabled === true) return { card, source: "byAgent" };
|
|
59
|
-
//
|
|
60
|
-
|
|
89
|
+
// 严格模式只在「确实建过卡」时才拒绝兜底:两维都空时兜底是唯一的出路。
|
|
90
|
+
const anyCard = Object.keys(store.byAgent ?? {}).length > 0 || Object.keys(store.bySession ?? {}).length > 0;
|
|
91
|
+
// 身份判定要把父会话 id 算进来:子 agent 只有父会话 id、没有自己的卡时,也是
|
|
92
|
+
// 「确实有身份」,不许被 default 兜底。
|
|
93
|
+
if ((id || sid || pid) && strict && anyCard) return { card: {}, source: "none" };
|
|
61
94
|
return { card: store.default, source: "default" };
|
|
62
95
|
}
|
|
63
96
|
|
|
@@ -78,57 +111,120 @@ export function resolveFactsCard(factsStore, agentId, { strict = false } = {}) {
|
|
|
78
111
|
}
|
|
79
112
|
|
|
80
113
|
/**
|
|
81
|
-
* 渲染整块「用户设置」。`agentId` 是会话身份(解析不到传 null → 走 default
|
|
114
|
+
* 渲染整块「用户设置」。`agentId` 是会话身份(解析不到传 null → 走 default 兜底),
|
|
115
|
+
* `sessionId` 是绑会话卡的键(#606;解析不到传 null)。
|
|
82
116
|
* `factsProvider` 是 profile-facts 域的读口(返回 `{ default: [...], byAgent: {...} }`),
|
|
83
117
|
* 可选;**开关与 provider 双闸**——profileFactsEnabled 关着时连 provider 都不调,
|
|
84
|
-
* 「关」必须是零读数而不是「读了不渲染」。返回 `{ text, source, agent, configGate }`:
|
|
118
|
+
* 「关」必须是零读数而不是「读了不渲染」。返回 `{ text, source, agent, session, configGate }`:
|
|
85
119
|
* `text` 为最终注入文本(全空时 ""),`configGate` 是 personaEnabled 的现值——面板
|
|
86
120
|
* 要靠它解释「卡开着为什么没生效」。
|
|
87
121
|
*/
|
|
88
|
-
export function renderUserSettingsBlock({
|
|
122
|
+
export function renderUserSettingsBlock({
|
|
123
|
+
settings,
|
|
124
|
+
config,
|
|
125
|
+
agentId = null,
|
|
126
|
+
sessionId = null,
|
|
127
|
+
parentSessionId = null,
|
|
128
|
+
factsProvider = null
|
|
129
|
+
}) {
|
|
89
130
|
const language = langOf(config);
|
|
90
131
|
const profile = settings.getProfile().trim();
|
|
91
132
|
const rules = settings.getRules();
|
|
92
133
|
const enabled = config?.personaEnabled === true;
|
|
134
|
+
const strict = config?.personaStrictAddressing === true;
|
|
93
135
|
const resolved = enabled
|
|
94
|
-
? resolvePersonaCard(settings, agentId, { strict
|
|
136
|
+
? resolvePersonaCard(settings, agentId, { strict, sessionId, parentSessionId })
|
|
95
137
|
: { card: {}, source: "default" };
|
|
96
138
|
const persona = resolved.card;
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
139
|
+
// 段(#600):每一行都带上它的 kind 与写回所需的定位信息,供面板按段读、按段改。
|
|
140
|
+
// 段文本是**未转义**的原样行;整串 `text` 仍走原来的「先 join 再统一转义」,
|
|
141
|
+
// 所以注入字节不变(转义只动连续花括号,换行不参与,逐段转义与整串转义等价)。
|
|
142
|
+
//
|
|
143
|
+
// `text` 是**带模板前缀的整行**(如 `- 用户画像:xxx`),不能拿去当新值回写,
|
|
144
|
+
// 否则每写一次叠一层前缀。可回写的段另带 `value`=原始值(无前缀、未转义);
|
|
145
|
+
// 没有原始值的段(表头、事实表头、截断尾注)不带 `value` 键。
|
|
146
|
+
const personaLines = [];
|
|
147
|
+
if (persona?.enabled === true) {
|
|
148
|
+
if (typeof persona.name === "string" && persona.name.trim()) {
|
|
149
|
+
const value = persona.name.trim();
|
|
150
|
+
personaLines.push({ kind: "personaName", field: "name", value, text: STR.personaNameLine[language](value) });
|
|
151
|
+
}
|
|
152
|
+
if (typeof persona.personality === "string" && persona.personality.trim()) {
|
|
153
|
+
const value = persona.personality.trim();
|
|
154
|
+
personaLines.push({ kind: "personaPersonality", field: "personality", value, text: STR.personaPersonalityLine[language](value) });
|
|
155
|
+
}
|
|
156
|
+
if (typeof persona.replyStyle === "string" && persona.replyStyle.trim()) {
|
|
157
|
+
const value = persona.replyStyle.trim();
|
|
158
|
+
personaLines.push({ kind: "personaReplyStyle", field: "replyStyle", value, text: STR.personaReplyStyleLine[language](value) });
|
|
159
|
+
}
|
|
160
|
+
// scenario / mesExample(#606):只有设置了才输出,未设置时本块逐字节不变。
|
|
161
|
+
if (typeof persona.scenario === "string" && persona.scenario.trim()) {
|
|
162
|
+
const value = persona.scenario.trim();
|
|
163
|
+
personaLines.push({ kind: "personaScenario", field: "scenario", value, text: STR.personaScenarioLine[language](value) });
|
|
164
|
+
}
|
|
165
|
+
if (typeof persona.mesExample === "string" && persona.mesExample.trim()) {
|
|
166
|
+
const value = persona.mesExample.trim();
|
|
167
|
+
personaLines.push({ kind: "personaMesExample", field: "mesExample", value, text: STR.personaMesExampleLine[language](value) });
|
|
168
|
+
}
|
|
169
|
+
}
|
|
104
170
|
// 事实段:开关关 / 无 provider / 该卡没事实三种情况都不渲染,注入文本与
|
|
105
171
|
// 未启用事实时逐字节一致(老用户零变化)。
|
|
106
172
|
const factsResolved = config?.profileFactsEnabled === true && typeof factsProvider === "function"
|
|
107
|
-
? resolveFactsCard(factsProvider(), agentId, { strict
|
|
173
|
+
? resolveFactsCard(factsProvider(), agentId, { strict })
|
|
108
174
|
: { facts: [], source: "none" };
|
|
109
175
|
const factLines = [];
|
|
110
176
|
if (factsResolved.facts.length) {
|
|
111
177
|
let chars = 0;
|
|
112
178
|
let shown = 0;
|
|
113
179
|
for (const fact of factsResolved.facts) {
|
|
114
|
-
const
|
|
180
|
+
const value = String(fact);
|
|
181
|
+
const line = STR.profileFactLine[language](value);
|
|
115
182
|
if (shown >= PROFILE_FACTS_MAX_LINES || chars + line.length > PROFILE_FACTS_MAX_CHARS) break;
|
|
116
|
-
factLines.push(line);
|
|
183
|
+
factLines.push({ kind: "fact", index: shown, value, text: line });
|
|
117
184
|
chars += line.length;
|
|
118
185
|
shown += 1;
|
|
119
186
|
}
|
|
120
187
|
const remaining = factsResolved.facts.length - shown;
|
|
121
|
-
if (remaining > 0) factLines.push(STR.profileFactsTruncated[language](remaining));
|
|
122
|
-
if (factLines.length)
|
|
188
|
+
if (remaining > 0) factLines.push({ kind: "factsTruncated", remaining, text: STR.profileFactsTruncated[language](remaining) });
|
|
189
|
+
if (factLines.length) {
|
|
190
|
+
// 表头的 n 是**总条数**(含没注入的),与「另有 N 条未注入」互补,别改成 shown。
|
|
191
|
+
factLines.unshift({
|
|
192
|
+
kind: "factsHeader",
|
|
193
|
+
shown,
|
|
194
|
+
total: factsResolved.facts.length,
|
|
195
|
+
text: STR.profileFactsHeader[language](factsResolved.facts.length)
|
|
196
|
+
});
|
|
197
|
+
}
|
|
123
198
|
}
|
|
124
199
|
const id = typeof agentId === "string" && agentId.trim() ? agentId.trim() : null;
|
|
200
|
+
const sid = typeof sessionId === "string" && sessionId.trim() ? sessionId.trim() : null;
|
|
201
|
+
const pid =
|
|
202
|
+
typeof parentSessionId === "string" && parentSessionId.trim() ? parentSessionId.trim() : null;
|
|
125
203
|
if (!profile && !rules.length && !personaLines.length && !factLines.length) {
|
|
126
|
-
|
|
204
|
+
// `parent` 是读数(面板要能说清「这张卡是从父会话继承来的」),不参与判定。
|
|
205
|
+
return {
|
|
206
|
+
text: "",
|
|
207
|
+
segments: [],
|
|
208
|
+
source: resolved.source,
|
|
209
|
+
agent: id,
|
|
210
|
+
session: sid,
|
|
211
|
+
parent: pid,
|
|
212
|
+
configGate: enabled
|
|
213
|
+
};
|
|
127
214
|
}
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
if (profile)
|
|
131
|
-
|
|
132
|
-
for (const rule of rules)
|
|
133
|
-
|
|
215
|
+
const segments = [{ kind: "header", text: STR.userSettingsHeader[language] }];
|
|
216
|
+
segments.push(...personaLines);
|
|
217
|
+
if (profile) segments.push({ kind: "profile", value: profile, text: STR.profileLine[language](profile) });
|
|
218
|
+
segments.push(...factLines);
|
|
219
|
+
for (const rule of rules) segments.push({ kind: "rule", value: rule, text: STR.ruleLine[language](rule) });
|
|
220
|
+
const lines = segments.map((s) => s.text);
|
|
221
|
+
return {
|
|
222
|
+
text: escapePromptVars(config, lines.join("\n")),
|
|
223
|
+
segments,
|
|
224
|
+
source: resolved.source,
|
|
225
|
+
agent: id,
|
|
226
|
+
session: sid,
|
|
227
|
+
parent: pid,
|
|
228
|
+
configGate: enabled
|
|
229
|
+
};
|
|
134
230
|
}
|
package/src/vector-index.js
CHANGED
|
@@ -65,35 +65,105 @@ export function createVectorIndex({ store, logger }) {
|
|
|
65
65
|
return store.searchVector(queryVector, { limit, threshold });
|
|
66
66
|
},
|
|
67
67
|
|
|
68
|
-
/**
|
|
68
|
+
/**
|
|
69
|
+
* Re-embed every row whose vector is missing OR came from another model.
|
|
70
|
+
*
|
|
71
|
+
* Returns { indexed, failed, skipped, remaining, error? } — three distinct
|
|
72
|
+
* outcomes instead of one `skipped` bucket that meant "failed", "threw" and
|
|
73
|
+
* "did not fit in the limit" all at once. #604.
|
|
74
|
+
*/
|
|
69
75
|
async rebuildIndex(embedder, { limit = 1000 } = {}) {
|
|
70
76
|
// The loop needs a single-text embedder. Native embedSingle is preferred;
|
|
71
77
|
// embed-only OpenAI-compatible clients (issue #10) are accepted too via
|
|
72
78
|
// their `embed` single-text interface, so /vector-reindex no longer
|
|
73
79
|
// silently returns 0 for them. An embedder exposing neither is ignored.
|
|
80
|
+
// #604: prefer the detailed entry point when the embedder has one. The
|
|
81
|
+
// legacy OpenAI-compatible client drops the failure reason on its normal
|
|
82
|
+
// path (embedText returns only the vector), so a provider outage and an
|
|
83
|
+
// unembeddable row both surfaced here as a bare null. The reason is what
|
|
84
|
+
// tells them apart, and the operator needs it to know whether pressing
|
|
85
|
+
// the button again is worth anything.
|
|
74
86
|
let embedOne = null;
|
|
87
|
+
let embedOneDetailed = null;
|
|
88
|
+
if (embedder && typeof embedder.embedSingleDetailed === "function") {
|
|
89
|
+
embedOneDetailed = (text) => embedder.embedSingleDetailed(text);
|
|
90
|
+
}
|
|
75
91
|
if (embedder && typeof embedder.embedSingle === "function") {
|
|
76
92
|
embedOne = (text) => embedder.embedSingle(text);
|
|
77
93
|
} else if (embedder && typeof embedder.embed === "function") {
|
|
78
94
|
embedOne = (text) => Promise.resolve(embedder.embed(text));
|
|
79
95
|
}
|
|
80
|
-
if (!embedOne)
|
|
81
|
-
|
|
96
|
+
if (!embedOne && !embedOneDetailed) {
|
|
97
|
+
return { indexed: 0, failed: 0, skipped: 0, remaining: 0, error: "no-embedder" };
|
|
98
|
+
}
|
|
99
|
+
const model = embedder?.modelHash ?? null;
|
|
100
|
+
// #604: this used to call needsEmbedding(), which only matches empty
|
|
101
|
+
// vectors. After a model switch every row already has a vector — from the
|
|
102
|
+
// previous model — so "reindex" selected nothing, embedded nothing, and
|
|
103
|
+
// reported {indexed: 0, skipped: 0} as if the index were already current.
|
|
104
|
+
// The boot sweep has always used the model-aware selector; using the same
|
|
105
|
+
// one here is what makes the button mean what it says.
|
|
106
|
+
const rows = model && typeof store.needsEmbeddingFor === "function"
|
|
107
|
+
? store.needsEmbeddingFor(model, limit)
|
|
108
|
+
: store.needsEmbedding(limit);
|
|
82
109
|
let indexed = 0;
|
|
110
|
+
let failed = 0;
|
|
111
|
+
const failureReasons = {};
|
|
112
|
+
const noteFailure = (reason) => {
|
|
113
|
+
failureReasons[reason] = (failureReasons[reason] ?? 0) + 1;
|
|
114
|
+
};
|
|
83
115
|
for (const row of rows) {
|
|
84
116
|
try {
|
|
85
117
|
const text = [row.title, row.content].filter(Boolean).join("\n");
|
|
86
|
-
|
|
118
|
+
let vector;
|
|
119
|
+
if (embedOneDetailed) {
|
|
120
|
+
const detailed = await embedOneDetailed(text);
|
|
121
|
+
vector = detailed ? detailed.vector : null;
|
|
122
|
+
if (!vector && detailed?.reason) noteFailure(`error:${detailed.reason}`);
|
|
123
|
+
} else {
|
|
124
|
+
vector = await embedOne(text);
|
|
125
|
+
}
|
|
87
126
|
if (vector && vector.length) {
|
|
88
|
-
|
|
127
|
+
// #604: the model fingerprint was omitted here, so every rebuilt
|
|
128
|
+
// row kept embedding_model NULL. needsEmbeddingFor() then treated
|
|
129
|
+
// it as stale forever: the boot sweep re-embedded it on each start,
|
|
130
|
+
// the panel counted it as unsearchable, and pressing reindex again
|
|
131
|
+
// found nothing (the vector was no longer empty). Passing the
|
|
132
|
+
// fingerprint closes the loop between the write and the selector.
|
|
133
|
+
store.setEmbedding(row.id, vector, model ?? undefined);
|
|
89
134
|
indexed++;
|
|
135
|
+
} else {
|
|
136
|
+
failed++;
|
|
137
|
+
// A detailed adapter already recorded why; the plain path has no
|
|
138
|
+
// reason to record, so name the only thing it can mean.
|
|
139
|
+
if (!embedOneDetailed) noteFailure("empty-vector");
|
|
140
|
+
logger?.warn?.(`[dsh-mneme] rebuild produced no vector for ${row.id}`);
|
|
90
141
|
}
|
|
91
142
|
} catch (error) {
|
|
143
|
+
failed++;
|
|
144
|
+
noteFailure(`error:${String(error)}`);
|
|
92
145
|
logger?.warn?.(`[dsh-mneme] rebuild embed failed for ${row.id}: ${String(error)}`);
|
|
93
146
|
}
|
|
94
147
|
}
|
|
95
148
|
if (indexed > 0 && embedder.modelHash) this.markModel(embedder.modelHash, embedder.dimension);
|
|
96
|
-
|
|
149
|
+
// remaining = still selected by the same predicate after this run. Rows
|
|
150
|
+
// that failed stay in it (they were never written), which is exactly the
|
|
151
|
+
// number the caller needs to decide whether to run again.
|
|
152
|
+
const remaining = typeof store.countNeedsEmbeddingFor === "function"
|
|
153
|
+
? store.countNeedsEmbeddingFor(model)
|
|
154
|
+
: null;
|
|
155
|
+
// `skipped` is 0 by definition here, not as a placeholder: every row the
|
|
156
|
+
// selector returned is attempted in the loop above, so nothing is left
|
|
157
|
+
// untouched. Rows that produced no vector are `failed`. The remaining
|
|
158
|
+
// bucket is the selector's own count, which is why a row can be both
|
|
159
|
+
// failed and remaining — it was tried and it is still stale.
|
|
160
|
+
//
|
|
161
|
+
// `failureReasons` mirrors the boot sweep's bucket names so the two paths
|
|
162
|
+
// read the same: `empty-vector` means this row will never embed, while an
|
|
163
|
+
// `error:` bucket means the provider or the store failed and the same
|
|
164
|
+
// press may well succeed next time. A run that only ever reports
|
|
165
|
+
// empty-vector is not a retry candidate.
|
|
166
|
+
return { indexed, failed, skipped: 0, remaining, failureReasons };
|
|
97
167
|
},
|
|
98
168
|
|
|
99
169
|
/** Index stats for the Web panel / debugging. */
|