dsh-prime-memory 0.14.0-beta.3 → 0.14.0-beta.5
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/dist/client.js +60 -8
- package/dist/conflict-service.d.ts +4 -2
- package/dist/conflict-service.js +15 -4
- package/dist/contract.d.ts +70 -2
- package/dist/pipeline/anchors.d.ts +15 -0
- package/dist/pipeline/anchors.js +19 -0
- package/dist/pipeline/l1.js +31 -10
- package/dist/stats.d.ts +1 -1
- package/dist/stats.js +132 -25
- package/dist/store/l1-snapshot.d.ts +32 -0
- package/dist/store/l1-snapshot.js +35 -0
- package/dist/store/l1.d.ts +35 -1
- package/dist/store/l1.js +58 -2
- package/dist/store/sqlite.d.ts +43 -0
- package/dist/store/sqlite.js +121 -4
- package/dist/store/supersede.d.ts +71 -0
- package/dist/store/supersede.js +74 -0
- package/dist/tools/index.js +47 -18
- package/package.json +1 -1
package/dist/client.js
CHANGED
|
@@ -606,7 +606,7 @@ var __defProp = Object.defineProperty;
|
|
|
606
606
|
// client/src/tabs/ConflictsTab.tsx
|
|
607
607
|
var import_jsx_runtime = require("react/jsx-runtime");
|
|
608
608
|
var POLL_MS = 1e4;
|
|
609
|
-
var GONE = "
|
|
609
|
+
var GONE = "(该记录正文不可得:已不在主表,或被更早的清理清掉了)";
|
|
610
610
|
function ConflictsTab(props) {
|
|
611
611
|
const rpc = props.rpc;
|
|
612
612
|
const [view, setView] = (0, import_react2.useState)(null);
|
|
@@ -639,7 +639,7 @@ var __defProp = Object.defineProperty;
|
|
|
639
639
|
将要退场的记忆:
|
|
640
640
|
「${doomed}」
|
|
641
641
|
|
|
642
|
-
|
|
642
|
+
它会移出检索面(不再被召回),但记录仍保留 —— 可在「记忆」页的「已退场」区恢复。裁决结论本身不可覆盖。`
|
|
643
643
|
);
|
|
644
644
|
if (!ok) return;
|
|
645
645
|
}
|
|
@@ -3214,24 +3214,25 @@ var __defProp = Object.defineProperty;
|
|
|
3214
3214
|
const ids = Array.from(sel);
|
|
3215
3215
|
if (ids.length === 0) return;
|
|
3216
3216
|
if (!hiPriv) {
|
|
3217
|
-
setError("
|
|
3217
|
+
setError("高权限模式未开启:请在右上「高权限:关」或概览页开关中开启后,再退场记忆。");
|
|
3218
3218
|
return;
|
|
3219
3219
|
}
|
|
3220
3220
|
if (ids.length > DELETE_LIMIT) {
|
|
3221
|
-
setError("
|
|
3221
|
+
setError("一次最多退场 " + DELETE_LIMIT + " 条(当前勾选 " + ids.length + " 条),请分批操作。");
|
|
3222
3222
|
return;
|
|
3223
3223
|
}
|
|
3224
|
-
if (!window.confirm("
|
|
3224
|
+
if (!window.confirm("退场勾选的 " + ids.length + " 条记忆?\n\n它们会移出检索面(不再被召回),但记录仍保留 —— 可在下方「已退场」区恢复。")) return;
|
|
3225
3225
|
rpc("dsh-memory/records-delete", { ids }).then((r) => {
|
|
3226
3226
|
if (r && r.ok) {
|
|
3227
3227
|
setSel(/* @__PURE__ */ new Set());
|
|
3228
3228
|
if (expandedId && ids.indexOf(expandedId) >= 0) setExpandedId(null);
|
|
3229
3229
|
fetchPage(last, 0, false);
|
|
3230
|
-
|
|
3230
|
+
if (showRetired) loadRetired();
|
|
3231
|
+
} else if (r) setError(r.error ? r.error.message : "退场失败");
|
|
3231
3232
|
}).catch((e) => setError(String(e && e.message || e)));
|
|
3232
3233
|
};
|
|
3233
3234
|
const deleteRecord = (id) => {
|
|
3234
|
-
if (!window.confirm("
|
|
3235
|
+
if (!window.confirm("退场该条记忆?\n\n它会移出检索面(不再被召回),但记录仍保留 —— 可在下方「已退场」区恢复。")) return;
|
|
3235
3236
|
rpc("dsh-memory/records-delete", { ids: [id] }).then((r) => {
|
|
3236
3237
|
if (r && r.ok) {
|
|
3237
3238
|
setSel((prev) => {
|
|
@@ -3241,9 +3242,42 @@ var __defProp = Object.defineProperty;
|
|
|
3241
3242
|
});
|
|
3242
3243
|
if (expandedId === id) setExpandedId(null);
|
|
3243
3244
|
fetchPage(last, 0, false);
|
|
3244
|
-
|
|
3245
|
+
if (showRetired) loadRetired();
|
|
3246
|
+
} else if (r) setError(r.error ? r.error.message : "退场失败");
|
|
3245
3247
|
}).catch((e) => setError(String(e && e.message || e)));
|
|
3246
3248
|
};
|
|
3249
|
+
const [retired, setRetired] = (0, import_react15.useState)([]);
|
|
3250
|
+
const [retiredTotal, setRetiredTotal] = (0, import_react15.useState)(0);
|
|
3251
|
+
const [showRetired, setShowRetired] = (0, import_react15.useState)(false);
|
|
3252
|
+
const [retiredBusy, setRetiredBusy] = (0, import_react15.useState)(false);
|
|
3253
|
+
const loadRetired = (0, import_react15.useCallback)(() => {
|
|
3254
|
+
setRetiredBusy(true);
|
|
3255
|
+
rpc("dsh-memory/records-retired", { limit: 100, offset: 0 }).then((r) => {
|
|
3256
|
+
if (r && r.ok) {
|
|
3257
|
+
setRetired(r.value.items);
|
|
3258
|
+
setRetiredTotal(r.value.total);
|
|
3259
|
+
} else if (r) setError(r.error ? r.error.message : "已退场列表加载失败");
|
|
3260
|
+
}).catch((e) => setError(String(e && e.message || e))).finally(() => setRetiredBusy(false));
|
|
3261
|
+
}, [rpc]);
|
|
3262
|
+
(0, import_react15.useEffect)(() => {
|
|
3263
|
+
if (showRetired) loadRetired();
|
|
3264
|
+
}, [showRetired, loadRetired]);
|
|
3265
|
+
const restoreRecords = (ids) => {
|
|
3266
|
+
if (ids.length === 0) return;
|
|
3267
|
+
setRetiredBusy(true);
|
|
3268
|
+
rpc("dsh-memory/records-restore", { ids }).then((r) => {
|
|
3269
|
+
if (r && r.ok) {
|
|
3270
|
+
loadRetired();
|
|
3271
|
+
fetchPage(last, 0, false);
|
|
3272
|
+
} else if (r) setError(r.error ? r.error.message : "恢复失败");
|
|
3273
|
+
}).catch((e) => setError(String(e && e.message || e))).finally(() => setRetiredBusy(false));
|
|
3274
|
+
};
|
|
3275
|
+
const RETIRE_REASON_LABEL = {
|
|
3276
|
+
conflict: "裁决退场",
|
|
3277
|
+
superseded: "被取代",
|
|
3278
|
+
manual: "人工退场",
|
|
3279
|
+
unknown: "已退场"
|
|
3280
|
+
};
|
|
3247
3281
|
const countText = total !== null ? "共 " + total + " 条" : items.length + " 条" + (hasMore ? "+" : "");
|
|
3248
3282
|
const selCount = sel.size;
|
|
3249
3283
|
return /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { children: [
|
|
@@ -3405,6 +3439,24 @@ var __defProp = Object.defineProperty;
|
|
|
3405
3439
|
m.id
|
|
3406
3440
|
);
|
|
3407
3441
|
}),
|
|
3442
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { style: { ...S.flexRow, marginTop: 12 }, children: [
|
|
3443
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(NButton, { onClick: () => setShowRetired((v) => !v), children: (showRetired ? "收起" : "展开") + "「已退场」(可恢复)" }),
|
|
3444
|
+
showRetired && retiredBusy ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { style: S.muted, children: "加载中…" }) : null,
|
|
3445
|
+
showRetired && !retiredBusy && retiredTotal > 0 ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { style: S.muted, children: "共 " + retiredTotal + " 条可恢复" }) : null
|
|
3446
|
+
] }),
|
|
3447
|
+
showRetired ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { style: { marginTop: 8 }, children: retired.length === 0 ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { style: S.hint, children: retiredBusy ? " " : "没有已退场的记忆。" }) : /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { children: [
|
|
3448
|
+
retired.map((m) => /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { className: "dsh-mem-card", style: S.card, children: [
|
|
3449
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { style: S.cardHead, children: [
|
|
3450
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { style: S.muted, children: RETIRE_REASON_LABEL[m.retiredReason] || m.retiredReason }),
|
|
3451
|
+
m.verdict ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { style: S.muted, children: "结论 " + m.verdict }) : null,
|
|
3452
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { style: S.grow }),
|
|
3453
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("span", { style: S.muted, children: fmtTime(m.retiredAt) }),
|
|
3454
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(NButton, { disabled: retiredBusy, title: "恢复到检索面", onClick: () => restoreRecords([m.id]), children: "恢复" })
|
|
3455
|
+
] }),
|
|
3456
|
+
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { style: S.content, children: m.content })
|
|
3457
|
+
] }, m.id)),
|
|
3458
|
+
retiredTotal > retired.length ? /* @__PURE__ */ (0, import_jsx_runtime15.jsx)("p", { style: S.hint, children: "共 " + retiredTotal + " 条,此处显示 " + retired.length + " 条" }) : null
|
|
3459
|
+
] }) }) : null,
|
|
3408
3460
|
hasMore ? /* @__PURE__ */ (0, import_jsx_runtime15.jsxs)("div", { style: S.flexRow, children: [
|
|
3409
3461
|
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)("div", { style: S.grow }),
|
|
3410
3462
|
/* @__PURE__ */ (0, import_jsx_runtime15.jsx)(
|
|
@@ -12,7 +12,7 @@ import type { L1Store } from './store/l1.js';
|
|
|
12
12
|
import type { ConflictPairView, ConflictsResponse as ConflictsView, ConflictResolveResponse as ConflictResolutionView } from './contract.js';
|
|
13
13
|
export type { ConflictPairView, ConflictsView, ConflictResolutionView };
|
|
14
14
|
export interface ConflictResolveDeps {
|
|
15
|
-
l1: Pick<L1Store, 'listConflictPending' | 'resolveConflictPending' | '
|
|
15
|
+
l1: Pick<L1Store, 'listConflictPending' | 'resolveConflictPending' | 'retire' | 'syncGraphDisputed'>;
|
|
16
16
|
/** `conflictFreeze.enabled`。未开启时队列恒空,直接给出提示而非静默无操作。 */
|
|
17
17
|
conflictFreezeEnabled: boolean;
|
|
18
18
|
}
|
|
@@ -21,9 +21,11 @@ export interface ConflictResolveDeps {
|
|
|
21
21
|
*
|
|
22
22
|
* 顺序刻意如此:
|
|
23
23
|
* ① **先打 `resolved_at` 再退场 loser**。反过来的话,退场成功但打标失败会留下
|
|
24
|
-
* "
|
|
24
|
+
* "记录已退场、队列里那条仍在待裁决"的状态——人再点一次才发现无据可依。
|
|
25
25
|
* 打标用 `WHERE resolved_at = ''`,天然防重复裁决:第二次调用拿到 0 行即中止。
|
|
26
26
|
* ② 退场后才**重算**图谱 `disputed`(派生字段必须由当前事实重算,见 `syncDisputed`)。
|
|
27
|
+
* ③ 退场是**软删**(`retire`,可恢复),不是物理删除:主表行留着,`valid_to` 闭合 +
|
|
28
|
+
* 写取代标记,FTS/向量行撤掉。故"判错了"可以再恢复——裁决不可覆盖,但可以反悔。
|
|
27
29
|
*/
|
|
28
30
|
export declare function resolveConflictPair(deps: ConflictResolveDeps, pairId: string, outcome: string): Promise<ConflictResolutionView>;
|
|
29
31
|
/** 裁决结果的人类可读渲染(工具路径用)。schema 产出的是可选字段,故按部分取值渲染。 */
|
package/dist/conflict-service.js
CHANGED
|
@@ -7,9 +7,11 @@ function view(partial) {
|
|
|
7
7
|
*
|
|
8
8
|
* 顺序刻意如此:
|
|
9
9
|
* ① **先打 `resolved_at` 再退场 loser**。反过来的话,退场成功但打标失败会留下
|
|
10
|
-
* "
|
|
10
|
+
* "记录已退场、队列里那条仍在待裁决"的状态——人再点一次才发现无据可依。
|
|
11
11
|
* 打标用 `WHERE resolved_at = ''`,天然防重复裁决:第二次调用拿到 0 行即中止。
|
|
12
12
|
* ② 退场后才**重算**图谱 `disputed`(派生字段必须由当前事实重算,见 `syncDisputed`)。
|
|
13
|
+
* ③ 退场是**软删**(`retire`,可恢复),不是物理删除:主表行留着,`valid_to` 闭合 +
|
|
14
|
+
* 写取代标记,FTS/向量行撤掉。故"判错了"可以再恢复——裁决不可覆盖,但可以反悔。
|
|
13
15
|
*/
|
|
14
16
|
export async function resolveConflictPair(deps, pairId, outcome) {
|
|
15
17
|
const clean = outcome.trim();
|
|
@@ -41,8 +43,17 @@ export async function resolveConflictPair(deps, pairId, outcome) {
|
|
|
41
43
|
// 并发/重复调用:这一对在本次读取与本次写入之间被裁决了
|
|
42
44
|
return view({ pair_id: pairId, outcome: clean, notice: '该对已被裁决,本次未生效(裁决不可覆盖)。' });
|
|
43
45
|
}
|
|
44
|
-
if (removedId)
|
|
45
|
-
|
|
46
|
+
if (removedId) {
|
|
47
|
+
// **软删**(退场),不是物理删除:主表行保留 + `valid_to` 闭合 + 写取代标记,
|
|
48
|
+
// FTS/向量行撤掉使其退出检索面。于是"判错了"可以再恢复,而不必去
|
|
49
|
+
// `records/*.jsonl` 事实源里手工捞——那是本功能上线前唯一的后悔药。
|
|
50
|
+
deps.l1.retire([removedId], {
|
|
51
|
+
at: resolvedAt,
|
|
52
|
+
reason: 'conflict',
|
|
53
|
+
verdict: clean,
|
|
54
|
+
pairId,
|
|
55
|
+
});
|
|
56
|
+
}
|
|
46
57
|
// 图谱 disputed 重算:此刻仍未裁决的对才是争议集,已了结的节点自动复原 active
|
|
47
58
|
const ids = new Set();
|
|
48
59
|
for (const p of deps.l1.listConflictPending()) {
|
|
@@ -59,7 +70,7 @@ export function renderConflictResolution(v) {
|
|
|
59
70
|
const outcome = v.outcome ?? '';
|
|
60
71
|
const label = outcome === 'winner' ? '判定 LLM 建议的胜方为真' : outcome === 'loser' ? '判定败方为真' : '两者都保留(判为各自独立的事实)';
|
|
61
72
|
const removed = v.removed_record_id
|
|
62
|
-
? `\n退场记录:${v.removed_record_id}(
|
|
73
|
+
? `\n退场记录:${v.removed_record_id}(已移出检索面,**可恢复**——记忆列表里能找回)`
|
|
63
74
|
: '\n未移除任何记录。';
|
|
64
75
|
return `已裁决待裁决对 ${v.pair_id ?? ''}\n结论:${outcome}(${label})\n裁决时刻:${v.resolved_at ?? ''}${removed}`;
|
|
65
76
|
}
|
package/dist/contract.d.ts
CHANGED
|
@@ -610,14 +610,76 @@ export interface ListRecordsResponse {
|
|
|
610
610
|
/** 场景筛选下拉选项(仅 offset===0 时附带)。 */
|
|
611
611
|
scenes?: string[];
|
|
612
612
|
}
|
|
613
|
-
/** dsh-memory/records-delete(
|
|
613
|
+
/** dsh-memory/records-delete(面板高权限退场指定记忆;须 memoryMutate 开启)。 */
|
|
614
614
|
export interface RecordsDeleteRequest {
|
|
615
|
-
/**
|
|
615
|
+
/** 要**退场(软删)**的 L1 record id 列表(≤200)。不是物理删除,可恢复。 */
|
|
616
616
|
ids: string[];
|
|
617
617
|
}
|
|
618
618
|
export interface RecordsDeleteResponse {
|
|
619
619
|
deleted: number;
|
|
620
620
|
}
|
|
621
|
+
/**
|
|
622
|
+
* 已退场记录的一条(在浏览卡片之上补退场信息)。
|
|
623
|
+
*
|
|
624
|
+
* `retiredReason` 用宽松 `string` 而非字面量联合:契约要能在**客户端**那一档
|
|
625
|
+
* (`types: []`)独立编译,不该反向依赖宿主 store 的类型;严格联合留在 store 侧,
|
|
626
|
+
* 面板只做展示。新增原因时面板不认识也能照常显示,不会编译失败。
|
|
627
|
+
*/
|
|
628
|
+
export interface RetiredRecordView extends UiRecord {
|
|
629
|
+
/** 退场时刻(ISO 8601)。 */
|
|
630
|
+
retiredAt: string;
|
|
631
|
+
/** 退场原因:`conflict`(裁决) / `superseded`(去重取代) / `manual`(人工)。 */
|
|
632
|
+
retiredReason: string;
|
|
633
|
+
/** 裁决结论(仅 reason=conflict)。 */
|
|
634
|
+
verdict?: string;
|
|
635
|
+
/** 取代它的新记录 id(仅 reason=superseded)。 */
|
|
636
|
+
supersededBy?: string;
|
|
637
|
+
}
|
|
638
|
+
/** dsh-memory/records-retired(已退场列表;面板据此展示"可恢复"区)。 */
|
|
639
|
+
export interface RecordsRetiredRequest {
|
|
640
|
+
limit?: number;
|
|
641
|
+
offset?: number;
|
|
642
|
+
}
|
|
643
|
+
export interface RecordsRetiredResponse {
|
|
644
|
+
items: RetiredRecordView[];
|
|
645
|
+
total: number;
|
|
646
|
+
}
|
|
647
|
+
/** dsh-memory/records-restore(把已退场记录送回检索面)。 */
|
|
648
|
+
export interface RecordsRestoreRequest {
|
|
649
|
+
/** 要恢复的 record id 列表(≤200)。 */
|
|
650
|
+
ids: string[];
|
|
651
|
+
}
|
|
652
|
+
export interface RecordsRestoreResponse {
|
|
653
|
+
restored: number;
|
|
654
|
+
/** 成功补回向量的条数(嵌入不可用时可能小于 `restored`)。 */
|
|
655
|
+
vectorsWritten: number;
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* dsh-memory/cleanup-retired(物理清理已退场记录)。
|
|
659
|
+
*
|
|
660
|
+
* **默认干跑**:`dryRun` 省略即视为 `true`,只报"将要清理多少条"。
|
|
661
|
+
* 真要物理删除必须显式 `dryRun:false` —— 这是本插件唯一不可逆的动作。
|
|
662
|
+
* 即便显式执行,也要先落快照并校验通过,否则中止(`aborted:true`)。
|
|
663
|
+
*/
|
|
664
|
+
export interface CleanupRetiredRequest {
|
|
665
|
+
/** 限定要清理的 id;省略 = 全部已退场记录。 */
|
|
666
|
+
ids?: string[];
|
|
667
|
+
/** 默认 true(干跑)。显式 false 才真正删除。 */
|
|
668
|
+
dryRun?: boolean;
|
|
669
|
+
}
|
|
670
|
+
export interface CleanupRetiredResponse {
|
|
671
|
+
dryRun: boolean;
|
|
672
|
+
/** 本次涉及(干跑)或实际处理(真跑)的条数。 */
|
|
673
|
+
targets: number;
|
|
674
|
+
/** 真正物理删除的条数(干跑恒为 0;中止恒为 0)。 */
|
|
675
|
+
purged: number;
|
|
676
|
+
/** 门禁未通过而中止。 */
|
|
677
|
+
aborted: boolean;
|
|
678
|
+
/** 快照目录(真跑时非空)。 */
|
|
679
|
+
dir: string;
|
|
680
|
+
/** 中止原因(仅 aborted 时非空)。 */
|
|
681
|
+
diffs: string[];
|
|
682
|
+
}
|
|
621
683
|
/** dsh-memory/graph-search(图谱节点检索;紧凑节点卡)。 */
|
|
622
684
|
export interface GraphSearchRequest {
|
|
623
685
|
/** 自然语言查询(≤4096 字符;空查询返回空)。 */
|
|
@@ -903,6 +965,9 @@ export interface DshMemoryRequestMap {
|
|
|
903
965
|
'dsh-memory/embedding-runtime-cancel': Record<string, never>;
|
|
904
966
|
'dsh-memory/embedding-reindex': Record<string, never>;
|
|
905
967
|
'dsh-memory/embedding-reindex-cancel': Record<string, never>;
|
|
968
|
+
'dsh-memory/records-retired': RecordsRetiredRequest;
|
|
969
|
+
'dsh-memory/records-restore': RecordsRestoreRequest;
|
|
970
|
+
'dsh-memory/cleanup-retired': CleanupRetiredRequest;
|
|
906
971
|
}
|
|
907
972
|
export interface DshMemoryResponseMap {
|
|
908
973
|
'dsh-memory/stats': StatsResponse;
|
|
@@ -938,6 +1003,9 @@ export interface DshMemoryResponseMap {
|
|
|
938
1003
|
'dsh-memory/embedding-runtime-cancel': EmbeddingCancelResponse;
|
|
939
1004
|
'dsh-memory/embedding-reindex': EmbeddingReindexStartResponse;
|
|
940
1005
|
'dsh-memory/embedding-reindex-cancel': EmbeddingCancelResponse;
|
|
1006
|
+
'dsh-memory/records-retired': RecordsRetiredResponse;
|
|
1007
|
+
'dsh-memory/records-restore': RecordsRestoreResponse;
|
|
1008
|
+
'dsh-memory/cleanup-retired': CleanupRetiredResponse;
|
|
941
1009
|
}
|
|
942
1010
|
/** 全部端点名(client 调用与 host case 表的共用字面量来源)。 */
|
|
943
1011
|
export type DshMemoryEndpoint = keyof DshMemoryResponseMap;
|
|
@@ -47,3 +47,18 @@ export declare function withSourceAnchors(metadata: Record<string, unknown> | un
|
|
|
47
47
|
* 宁可报告"无锚点",也不把半截坐标喂给下游的证据读取器。
|
|
48
48
|
*/
|
|
49
49
|
export declare function readSourceAnchors(metadata: unknown): ConversationAnchor[] | undefined;
|
|
50
|
+
/**
|
|
51
|
+
* 锚点的人类可读标签:**UI 契约(`UiRecord.sourceAnchors`)要的字符串形态**。
|
|
52
|
+
*
|
|
53
|
+
* 格式即 `t<turn>` / `t<turn> s<step>`(与 `reconcile.ts` 的证据行同一写法,
|
|
54
|
+
* 见 contract.ts 对 `sourceAnchors` 的说明)。`step` 缺失就**不写**——
|
|
55
|
+
* 不得用 0 或相邻事件的 step 补齐(红线 2:坐标永不推算)。
|
|
56
|
+
*
|
|
57
|
+
* 刻意**不带 sessionId**:该字段用于跨会话归并排序,而 UI 一行里只有
|
|
58
|
+
* "这条记忆出在会话内哪个位置",带上 id 反而把可用信息挤掉。
|
|
59
|
+
*/
|
|
60
|
+
export declare function anchorLabel(anchor: ConversationAnchor): string;
|
|
61
|
+
/** 读侧一步到位:`l1_records.metadata_json` → UI 契约的标签数组。
|
|
62
|
+
* 无锚点返回**空数组**(`UiRecord.sourceAnchors` 的契约语义是"空数组 = 无锚点",
|
|
63
|
+
* 与 `readSourceAnchors` 的 `undefined` 区分开,故此处完成转换)。 */
|
|
64
|
+
export declare function sourceAnchorLabels(metadata: unknown): string[];
|
package/dist/pipeline/anchors.js
CHANGED
|
@@ -88,3 +88,22 @@ export function readSourceAnchors(metadata) {
|
|
|
88
88
|
}
|
|
89
89
|
return out.length > 0 ? out : undefined;
|
|
90
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* 锚点的人类可读标签:**UI 契约(`UiRecord.sourceAnchors`)要的字符串形态**。
|
|
93
|
+
*
|
|
94
|
+
* 格式即 `t<turn>` / `t<turn> s<step>`(与 `reconcile.ts` 的证据行同一写法,
|
|
95
|
+
* 见 contract.ts 对 `sourceAnchors` 的说明)。`step` 缺失就**不写**——
|
|
96
|
+
* 不得用 0 或相邻事件的 step 补齐(红线 2:坐标永不推算)。
|
|
97
|
+
*
|
|
98
|
+
* 刻意**不带 sessionId**:该字段用于跨会话归并排序,而 UI 一行里只有
|
|
99
|
+
* "这条记忆出在会话内哪个位置",带上 id 反而把可用信息挤掉。
|
|
100
|
+
*/
|
|
101
|
+
export function anchorLabel(anchor) {
|
|
102
|
+
return anchor.step === undefined ? `t${anchor.turn}` : `t${anchor.turn} s${anchor.step}`;
|
|
103
|
+
}
|
|
104
|
+
/** 读侧一步到位:`l1_records.metadata_json` → UI 契约的标签数组。
|
|
105
|
+
* 无锚点返回**空数组**(`UiRecord.sourceAnchors` 的契约语义是"空数组 = 无锚点",
|
|
106
|
+
* 与 `readSourceAnchors` 的 `undefined` 区分开,故此处完成转换)。 */
|
|
107
|
+
export function sourceAnchorLabels(metadata) {
|
|
108
|
+
return (readSourceAnchors(metadata) ?? []).map(anchorLabel);
|
|
109
|
+
}
|
package/dist/pipeline/l1.js
CHANGED
|
@@ -272,7 +272,11 @@ anchorMap) {
|
|
|
272
272
|
relatedIds.add(c.id);
|
|
273
273
|
}
|
|
274
274
|
const byId = new Map(store.getByIds([...relatedIds]).map((r) => [r.id, r]));
|
|
275
|
-
|
|
275
|
+
/**
|
|
276
|
+
* update/merge 取代掉的旧记录 → **取代它的**新记录 id。
|
|
277
|
+
* 用 Map 而非 Set:退场标记要带 `by`,否则"被谁取代"只能靠时间猜。
|
|
278
|
+
*/
|
|
279
|
+
const supersededBy = new Map();
|
|
276
280
|
const added = [];
|
|
277
281
|
/** §C 本轮新冻结的冲突对(应用完新增记录后统一落盘)。 */
|
|
278
282
|
const frozen = [];
|
|
@@ -323,11 +327,14 @@ anchorMap) {
|
|
|
323
327
|
}
|
|
324
328
|
continue;
|
|
325
329
|
}
|
|
326
|
-
// update / merge
|
|
330
|
+
// update / merge:目标记录**退场(软删)**,合并结果作为新记录追加(版本 +1)
|
|
327
331
|
// 候选召回按族隔离,合并产物保持新记忆的族标签
|
|
328
332
|
const targets = (decision.target_ids ?? []).filter((id) => byId.has(id));
|
|
333
|
+
// 同一目标被多条新记录取代时保留**首个**取代者:与 retire 的幂等语义一致
|
|
334
|
+
// (已退场记录不重复写标记),故先到先得而不是被后者覆盖
|
|
329
335
|
for (const id of targets)
|
|
330
|
-
|
|
336
|
+
if (!supersededBy.has(id))
|
|
337
|
+
supersededBy.set(id, m.record_id);
|
|
331
338
|
const targetVersion = targets.reduce((max, id) => Math.max(max, byId.get(id)?.version ?? 0), 0);
|
|
332
339
|
const mergedTs = (decision.merged_timestamps ?? [])
|
|
333
340
|
.map((t) => Date.parse(t))
|
|
@@ -356,13 +363,27 @@ anchorMap) {
|
|
|
356
363
|
});
|
|
357
364
|
}
|
|
358
365
|
await store.appendNew(added);
|
|
359
|
-
if (
|
|
360
|
-
|
|
361
|
-
|
|
366
|
+
if (supersededBy.size > 0) {
|
|
367
|
+
// **软删**(取代):不是物理删除——主表行保留 + `valid_to` 闭合 + 取代标记,
|
|
368
|
+
// FTS/向量撤出检索面。于是"合并错了"也能恢复,而不必去 `records/*.jsonl` 手工捞。
|
|
369
|
+
// 按取代者分组落盘(一次事务一组),避免逐条开事务。
|
|
370
|
+
const retiredAt = new Date(now).toISOString();
|
|
371
|
+
const byNewRecord = new Map();
|
|
372
|
+
for (const [targetId, newId] of supersededBy) {
|
|
373
|
+
const arr = byNewRecord.get(newId) ?? [];
|
|
374
|
+
arr.push(targetId);
|
|
375
|
+
byNewRecord.set(newId, arr);
|
|
376
|
+
}
|
|
377
|
+
for (const [newId, ids] of byNewRecord) {
|
|
378
|
+
store.retire(ids, { at: retiredAt, reason: 'superseded', by: newId });
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
// §C 自动裁决的执行面:LLM 的 loser 从检索面退场(winner 存活),同为**软删**。
|
|
362
382
|
// 排在 appendNew 之后——若 loser 恰是**本轮新记忆**(LLM 判定新记忆更差),
|
|
363
|
-
// 也必须先让它进库再退场,以保证"本轮新增"与"
|
|
364
|
-
if (autoLosers.size > 0)
|
|
365
|
-
|
|
383
|
+
// 也必须先让它进库再退场,以保证"本轮新增"与"本轮退场"的账面一致。
|
|
384
|
+
if (autoLosers.size > 0) {
|
|
385
|
+
store.retire([...autoLosers], { at: new Date(now).toISOString(), reason: 'conflict', verdict: 'auto' });
|
|
386
|
+
}
|
|
366
387
|
// ── §C 冻结对落盘(排在 appendNew 之后) ──
|
|
367
388
|
// 顺序有讲究:先让新记忆真正进 L1,再登记"它和谁构成待裁决对"。反过来的话,
|
|
368
389
|
// 落盘失败会留下一条指向**不存在记录**的裁决请求,人工打开队列只会看到悬空 id。
|
|
@@ -404,7 +425,7 @@ anchorMap) {
|
|
|
404
425
|
states[f].memoriesSinceL3 += addedByFamily[f];
|
|
405
426
|
}
|
|
406
427
|
markExtracted(states, mode, lastScene);
|
|
407
|
-
logger.info(`[memory] L1 抽取完成(mode=${mode}):消息 ${pending.length} 条,抽取 ${extracted.length} 条,去重后新增 ${added.length} 条(
|
|
428
|
+
logger.info(`[memory] L1 抽取完成(mode=${mode}):消息 ${pending.length} 条,抽取 ${extracted.length} 条,去重后新增 ${added.length} 条(取代退场 ${supersededBy.size} 条,chat=${addedByFamily.chat}/work=${addedByFamily.work}),累计 chat=${states.chat.totalExtracted}/work=${states.work.totalExtracted}`);
|
|
408
429
|
return { stored: added.length, skipped: false, sceneName: lastScene, newRecords: added };
|
|
409
430
|
}
|
|
410
431
|
/** auto 档取最近活跃的族 checkpoint(情境链/计数锚点)。 */
|
package/dist/stats.d.ts
CHANGED
|
@@ -22,7 +22,7 @@ export interface MemoryStatusSource {
|
|
|
22
22
|
pending(): number;
|
|
23
23
|
}
|
|
24
24
|
/**
|
|
25
|
-
* 端点全集运行时清单(
|
|
25
|
+
* 端点全集运行时清单(36 个,与 tests/contract-keys.test.ts 的 ENDPOINTS 及
|
|
26
26
|
* contract.ts 类型映射表三方对齐,漂移由键集 diff 测试暴露)。
|
|
27
27
|
* 注意:本清单同时是 HTTP 前缀路由 `/dsh-memory/rpc/<短名>` 的**放行白名单**
|
|
28
28
|
* (见下方 SHORT_ENDPOINTS),漏一条 = 该端点在面板里静默消失(404 被客户端
|
package/dist/stats.js
CHANGED
|
@@ -21,14 +21,14 @@ import { buildRouteChain, decideSendableEffort, LAYER_DEFAULT_BUDGETS, layerChai
|
|
|
21
21
|
import { projectDistillChain, validateDistillChain } from './settings.js';
|
|
22
22
|
import { RECEIPTS_QUERY_LIMIT_MAX, dimensionOf, toReceiptView } from './store/receipts.js';
|
|
23
23
|
import { resolveConflictPair, listConflictPairs } from './conflict-service.js';
|
|
24
|
-
|
|
25
|
-
import {
|
|
24
|
+
import { sourceAnchorLabels } from './pipeline/anchors.js';
|
|
25
|
+
import { readSupersedeMarker } from './store/supersede.js';
|
|
26
26
|
import { errDetail } from './util/filelog.js';
|
|
27
27
|
import { snapshotTokenCost } from './token-cost.js';
|
|
28
28
|
const require = createRequire(import.meta.url);
|
|
29
29
|
export const PLUGIN_VERSION = require('../package.json').version;
|
|
30
30
|
/**
|
|
31
|
-
* 端点全集运行时清单(
|
|
31
|
+
* 端点全集运行时清单(36 个,与 tests/contract-keys.test.ts 的 ENDPOINTS 及
|
|
32
32
|
* contract.ts 类型映射表三方对齐,漂移由键集 diff 测试暴露)。
|
|
33
33
|
* 注意:本清单同时是 HTTP 前缀路由 `/dsh-memory/rpc/<短名>` 的**放行白名单**
|
|
34
34
|
* (见下方 SHORT_ENDPOINTS),漏一条 = 该端点在面板里静默消失(404 被客户端
|
|
@@ -72,6 +72,9 @@ export const MEMORY_ENDPOINTS = [
|
|
|
72
72
|
'dsh-memory/embedding-runtime-cancel',
|
|
73
73
|
'dsh-memory/embedding-reindex',
|
|
74
74
|
'dsh-memory/embedding-reindex-cancel',
|
|
75
|
+
'dsh-memory/records-retired',
|
|
76
|
+
'dsh-memory/records-restore',
|
|
77
|
+
'dsh-memory/cleanup-retired',
|
|
75
78
|
];
|
|
76
79
|
/** HTTP 路由前缀(客户端 fetch `/dsh-memory/rpc/<短方法名>`)。 */
|
|
77
80
|
const RPC_ROUTE_PREFIX = '/dsh-memory/rpc';
|
|
@@ -491,6 +494,7 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
491
494
|
distillMode: '', directBaseURL: '', directApiKey: '',
|
|
492
495
|
embedRemoteBaseURL: '', embedRemoteApiKey: '', embedRemoteModel: '', embedRemoteDimensions: 0,
|
|
493
496
|
memoryMutate: false,
|
|
497
|
+
conflictFreeze: live?.get()?.conflictFreeze === true,
|
|
494
498
|
}),
|
|
495
499
|
// 静态部署上限(cordis.patch.yml):运行时开关与它取 AND
|
|
496
500
|
ceilings: { capture: cfg.capture.enabled, distill: cfg.extract.enabled, recall: cfg.recall.enabled },
|
|
@@ -528,8 +532,8 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
528
532
|
throw new Error('开关通道未初始化');
|
|
529
533
|
const patch = (payload ?? {});
|
|
530
534
|
const clean = {};
|
|
531
|
-
// 布尔开关组:memoryMutate(高权限写删门)
|
|
532
|
-
for (const key of ['enabled', 'capture', 'distill', 'recall', 'memoryMutate']) {
|
|
535
|
+
// 布尔开关组:memoryMutate(高权限写删门)与主开关同列;conflictFreeze(§C 人工冲突裁决)
|
|
536
|
+
for (const key of ['enabled', 'capture', 'distill', 'recall', 'memoryMutate', 'conflictFreeze']) {
|
|
533
537
|
if (typeof patch[key] === 'boolean')
|
|
534
538
|
clean[key] = patch[key];
|
|
535
539
|
}
|
|
@@ -729,11 +733,17 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
729
733
|
// 未开启冻结时同样走返回体(enabled:false + notice)而非抛错,理由同上。
|
|
730
734
|
case 'dsh-memory/conflicts': {
|
|
731
735
|
const p = (payload ?? {});
|
|
732
|
-
|
|
736
|
+
// 冻结开关只有**一个**事实源:与去重管线同一套 effectiveCfg 解析
|
|
737
|
+
// (live.conflictFreeze 覆盖静态 cfg.conflictFreeze.enabled)。此前这里直接读
|
|
738
|
+
// cfg.conflictFreeze.enabled —— 面板开关写的是 live,而部署静态值恒 false,
|
|
739
|
+
// 于是开关已开、settings.yaml 已落 true,本页仍报"矛盾冻结未开启"。
|
|
740
|
+
return listConflictPairs({ l1: stores.l1, conflictFreezeEnabled: effectiveCfg(cfg, live).conflictFreeze?.enabled === true }, { limit: Number(p.limit) || undefined });
|
|
733
741
|
}
|
|
734
742
|
// ── §C 矛盾冻结裁决(task_25):与 memory_resolve_conflict 工具共用同一形状 ──
|
|
735
743
|
// 端点层同样不给"提示文案"出口的例外只有一条:**队列未开启**不是调用错误而是
|
|
736
744
|
// 部署状态,故它走返回体(带 notice)而非抛错;pair_id/outcome 缺参才抛。
|
|
745
|
+
// 开关判定与上面 conflicts 同源(effectiveCfg):读端与写端必须看同一份状态,
|
|
746
|
+
// 否则会出现"列表说开着、裁决说没开"的自相矛盾。
|
|
737
747
|
case 'dsh-memory/conflict-resolve': {
|
|
738
748
|
const p = (payload ?? {});
|
|
739
749
|
const pairId = typeof p.pairId === 'string' ? p.pairId.trim() : '';
|
|
@@ -742,10 +752,15 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
742
752
|
throw new Error('需要 pairId(待裁决对的 pair_id)');
|
|
743
753
|
if (!outcome)
|
|
744
754
|
throw new Error('需要 outcome(winner | loser | both)');
|
|
745
|
-
return await resolveConflictPair({ l1: stores.l1, conflictFreezeEnabled: cfg.conflictFreeze?.enabled === true }, pairId, outcome);
|
|
755
|
+
return await resolveConflictPair({ l1: stores.l1, conflictFreezeEnabled: effectiveCfg(cfg, live).conflictFreeze?.enabled === true }, pairId, outcome);
|
|
746
756
|
}
|
|
747
757
|
case 'dsh-memory/records-delete': {
|
|
748
|
-
// 面板高权限删除指定记忆;写入删权限门(memoryMutate)
|
|
758
|
+
// 面板高权限删除指定记忆;写入删权限门(memoryMutate)防御。
|
|
759
|
+
//
|
|
760
|
+
// **软删**(退场),不是物理删除:与裁决 / 取代共用同一原语。面板上的"删除"
|
|
761
|
+
// 因此可撤销;真要抹掉数据只能走 `cleanup-retired`(先落快照 + 校验通过才删)。
|
|
762
|
+
// 这样 `deleteL1Batch` 在整个代码里**只有一个调用方**(exportThenPurge),
|
|
763
|
+
// "物理删除必须先有可信导出物"就成了结构性事实,而不是一句约定。
|
|
749
764
|
if (!live?.get().memoryMutate) {
|
|
750
765
|
throw new Error('记忆写删未开放:请在记忆库面板开启高权限模式');
|
|
751
766
|
}
|
|
@@ -753,9 +768,101 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
753
768
|
const ids = (Array.isArray(p.ids) ? p.ids : []).filter((x) => typeof x === 'string').slice(0, 200);
|
|
754
769
|
if (ids.length === 0)
|
|
755
770
|
throw new Error('ids 缺失');
|
|
756
|
-
|
|
757
|
-
deps.logger.info(`[memory]
|
|
758
|
-
return { deleted:
|
|
771
|
+
const n = stores.l1.retire(ids, { at: new Date().toISOString(), reason: 'manual' });
|
|
772
|
+
deps.logger.info(`[memory] 高权限退场(软删)记忆 ${n} 条(${ids.join(',')})`);
|
|
773
|
+
return { deleted: n };
|
|
774
|
+
}
|
|
775
|
+
// ── 记忆退场(软删)与清理:已退场列表 / 恢复 / 物理清理 ──
|
|
776
|
+
// 读方向**不开**权限门(与 conflicts 一致:看得见才知道要不要恢复);
|
|
777
|
+
// 恢复与物理清理由 memoryMutate 门控。
|
|
778
|
+
case 'dsh-memory/records-retired': {
|
|
779
|
+
const p = (payload ?? {});
|
|
780
|
+
const limit = Math.min(Math.max(Math.floor(Number(p.limit)) || 50, 1), 200);
|
|
781
|
+
const offset = Math.min(Math.max(Math.floor(Number(p.offset)) || 0, 0), 1_000_000);
|
|
782
|
+
const { items, total } = stores.l1.listRetired({ limit, offset });
|
|
783
|
+
const resp = {
|
|
784
|
+
items: items.map((r) => {
|
|
785
|
+
const mark = readSupersedeMarker(r.metadata);
|
|
786
|
+
const view = hitToUiRecord(r);
|
|
787
|
+
return {
|
|
788
|
+
...view,
|
|
789
|
+
// 标记缺失时退回 `valid_to`(软删的两条判据任一成立即算已退场)
|
|
790
|
+
retiredAt: mark?.at ?? (r.validTo !== undefined ? new Date(r.validTo).toISOString() : ''),
|
|
791
|
+
retiredReason: mark?.reason ?? 'unknown',
|
|
792
|
+
...(mark?.verdict ? { verdict: mark.verdict } : {}),
|
|
793
|
+
...(mark?.by ? { supersededBy: mark.by } : {}),
|
|
794
|
+
};
|
|
795
|
+
}),
|
|
796
|
+
total,
|
|
797
|
+
};
|
|
798
|
+
return resp;
|
|
799
|
+
}
|
|
800
|
+
case 'dsh-memory/records-restore': {
|
|
801
|
+
if (!live?.get().memoryMutate) {
|
|
802
|
+
throw new Error('记忆写删未开放:请在记忆库面板开启高权限模式');
|
|
803
|
+
}
|
|
804
|
+
const p = (payload ?? {});
|
|
805
|
+
const ids = (Array.isArray(p.ids) ? p.ids : [])
|
|
806
|
+
.filter((x) => typeof x === 'string' && x !== '')
|
|
807
|
+
.slice(0, 200);
|
|
808
|
+
if (ids.length === 0)
|
|
809
|
+
throw new Error('ids 缺失');
|
|
810
|
+
const r = await stores.l1.restore(ids);
|
|
811
|
+
deps.logger.info(`[memory] 恢复已退场记忆 ${r.restored} 条(补向量 ${r.vectorsWritten} 条)`);
|
|
812
|
+
const resp = r;
|
|
813
|
+
return resp;
|
|
814
|
+
}
|
|
815
|
+
case 'dsh-memory/cleanup-retired': {
|
|
816
|
+
if (!live?.get().memoryMutate) {
|
|
817
|
+
throw new Error('记忆写删未开放:请在记忆库面板开启高权限模式');
|
|
818
|
+
}
|
|
819
|
+
const p = (payload ?? {});
|
|
820
|
+
// **默认干跑**:省略 `dryRun` 即视为 true。物理删除是本插件唯一不可逆的动作,
|
|
821
|
+
// 必须由调用方显式要求才做(与"所有破坏性动作必须默认可回滚"同一条纪律)。
|
|
822
|
+
const dryRun = p.dryRun !== false;
|
|
823
|
+
const explicit = (Array.isArray(p.ids) ? p.ids : [])
|
|
824
|
+
.filter((x) => typeof x === 'string' && x !== '')
|
|
825
|
+
.slice(0, 500);
|
|
826
|
+
let targets;
|
|
827
|
+
if (explicit.length > 0) {
|
|
828
|
+
// 显式 id 也要**复核**是否真的处于已退场态:防止调用方用一个 id 列表
|
|
829
|
+
// 把活动记忆绕过软删直接物理抹掉(那等于给了一条硬删后门)。
|
|
830
|
+
targets = explicit.filter((id) => stores.l1.getByIds([id]).some((r) => r.validTo !== undefined));
|
|
831
|
+
}
|
|
832
|
+
else {
|
|
833
|
+
targets = [];
|
|
834
|
+
for (let offset = 0;; offset += 200) {
|
|
835
|
+
const page = stores.l1.listRetired({ limit: 200, offset });
|
|
836
|
+
targets.push(...page.items.map((r) => r.id));
|
|
837
|
+
if (page.items.length < 200)
|
|
838
|
+
break;
|
|
839
|
+
}
|
|
840
|
+
}
|
|
841
|
+
if (dryRun) {
|
|
842
|
+
const resp = {
|
|
843
|
+
dryRun: true,
|
|
844
|
+
targets: targets.length,
|
|
845
|
+
purged: 0,
|
|
846
|
+
aborted: false,
|
|
847
|
+
dir: '',
|
|
848
|
+
diffs: [],
|
|
849
|
+
};
|
|
850
|
+
return resp;
|
|
851
|
+
}
|
|
852
|
+
if (targets.length === 0) {
|
|
853
|
+
const resp = { dryRun: false, targets: 0, purged: 0, aborted: false, dir: '', diffs: [] };
|
|
854
|
+
return resp;
|
|
855
|
+
}
|
|
856
|
+
const r = await stores.l1.purgeRetired(targets, 'cleanup-retired');
|
|
857
|
+
const resp = {
|
|
858
|
+
dryRun: false,
|
|
859
|
+
targets: targets.length,
|
|
860
|
+
purged: r.purged,
|
|
861
|
+
aborted: r.aborted,
|
|
862
|
+
dir: r.dir,
|
|
863
|
+
diffs: r.diffs,
|
|
864
|
+
};
|
|
865
|
+
return resp;
|
|
759
866
|
}
|
|
760
867
|
// ── 知识图谱(面板图谱视图;graph 未装配时返空不报错) ──
|
|
761
868
|
case 'dsh-memory/graph-search': {
|
|
@@ -1058,11 +1165,16 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
1058
1165
|
return { cancelled: embedManager.cancelRuntimeInstall() };
|
|
1059
1166
|
}
|
|
1060
1167
|
case 'dsh-memory/embedding-reindex': {
|
|
1168
|
+
// 手动触发重建(契约:`EmbeddingReindexStartResponse`,受理即返回,进度照旧轮询
|
|
1169
|
+
// embedding-state-get 的 reindex 字段)。此前只登记了 -cancel:start 既不在白名单
|
|
1170
|
+
// 也没有 case,于是 startReindex() 成了够不着的死代码,端点在面板上静默 404
|
|
1171
|
+
// (契约门禁 tests/contract-keys.test.ts 早已为此标红)。
|
|
1172
|
+
// 拒绝语义交给 startReindex 自己抛(已卸载/在跑/切源占锁/源未就绪),不在端点层复述。
|
|
1061
1173
|
if (!embedManager)
|
|
1062
|
-
throw new Error('嵌入管理器未初始化');
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
return
|
|
1174
|
+
throw new Error('嵌入管理器未初始化(存储不可用)');
|
|
1175
|
+
const r = embedManager.startReindex();
|
|
1176
|
+
deps.logger.info('[memory] 收到嵌入重建指令(设置页按钮)');
|
|
1177
|
+
return r;
|
|
1066
1178
|
}
|
|
1067
1179
|
case 'dsh-memory/embedding-reindex-cancel': {
|
|
1068
1180
|
if (!embedManager)
|
|
@@ -1087,19 +1199,14 @@ function hitToUiRecord(r) {
|
|
|
1087
1199
|
createdAt: r.createdAt ? new Date(r.createdAt).toISOString() : null,
|
|
1088
1200
|
updatedAt: r.updatedAt ? new Date(r.updatedAt).toISOString() : null,
|
|
1089
1201
|
version: r.version ?? 0,
|
|
1090
|
-
|
|
1202
|
+
// R7 来源锚点:契约字段是**标签数组**(`t12 s3`),由 metadata 的保留键读出。
|
|
1203
|
+
// 此前这里仍在填已废弃的 `sourceMessageIds`——`l1_records` 从不存那一列,
|
|
1204
|
+
// 该字段恒为 `[]`(死字段),而契约早已换成 `sourceAnchors`,于是
|
|
1205
|
+
// `sourceAnchors` 永远缺失、来源行在 UI 上从未显示过。
|
|
1206
|
+
sourceAnchors: sourceAnchorLabels(r.metadata),
|
|
1091
1207
|
score: r.score ?? null,
|
|
1092
1208
|
};
|
|
1093
1209
|
}
|
|
1094
|
-
/**
|
|
1095
|
-
* 锚点的展示形态:`t12 s3` / 无 step 时 `t12`。
|
|
1096
|
-
*
|
|
1097
|
-
* 只做**可读化**,不携带 sessionId——单条记录的来源会话由记录自身语义决定,
|
|
1098
|
-
* 把 sessionId 塞进这一行会把 12 个字符的坐标变成 40 个字符。
|
|
1099
|
-
*/
|
|
1100
|
-
function formatAnchor(a) {
|
|
1101
|
-
return typeof a.step === 'number' ? `t${a.turn} s${a.step}` : `t${a.turn}`;
|
|
1102
|
-
}
|
|
1103
1210
|
/**
|
|
1104
1211
|
* 从文件尾反向分块读取最后 N 行:不整读全文件(轮转上限 2MB,整读会
|
|
1105
1212
|
* 阻塞事件循环数毫秒)。原始 Buffer 拼接后再解码——分块边界可能切在
|