dsh-prime-memory 0.26.30 → 0.26.32
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/CHANGELOG.md +12 -0
- package/dist/client.js +1522 -8
- package/dist/config.js +6 -0
- package/dist/contract.d.ts +66 -1
- package/dist/graph/constraints.d.ts +14 -0
- package/dist/graph/constraints.js +14 -0
- package/dist/graph/preview.d.ts +22 -0
- package/dist/graph/preview.js +25 -7
- package/dist/graph/record-links.d.ts +61 -0
- package/dist/graph/record-links.js +167 -0
- package/dist/graph/select.d.ts +30 -0
- package/dist/graph/select.js +124 -0
- package/dist/pipeline/intra-dedupe.d.ts +13 -0
- package/dist/pipeline/intra-dedupe.js +15 -0
- package/dist/pipeline/l1.js +2 -6
- package/dist/settings.js +3 -0
- package/dist/stats.d.ts +3 -2
- package/dist/stats.js +81 -5
- package/dist/store/graph-store.d.ts +37 -1
- package/dist/store/graph-store.js +120 -14
- package/dsh.plugin.json +1 -1
- package/package.json +1 -1
|
@@ -176,6 +176,21 @@ export function clusterByVector(items, vectors, tau) {
|
|
|
176
176
|
});
|
|
177
177
|
return finish(items, clusters);
|
|
178
178
|
}
|
|
179
|
+
/**
|
|
180
|
+
* 把「被吸收 → 代表」关系映射成 receipt 输入项。
|
|
181
|
+
*
|
|
182
|
+
* 为什么用既有 `kind='merge'` + `candidateIds=[代表 id]` 而不新增表列/词表即可:
|
|
183
|
+
* 吸收本质上就是「本条被并入另一条」,`merge` 正是这个词;`candidateIds` 载代表项 id,
|
|
184
|
+
* 于是「本条被批内哪条吸收」只需一次 `(runId, recordId)` 回溯(inputDigest 含代表 id)。
|
|
185
|
+
* 关闭特性时 `absorbedBy` 为空 ⇒ 不产生任何额外行(凭证与改动前逐字节一致)。
|
|
186
|
+
*/
|
|
187
|
+
export function receiptItemsForAbsorbed(absorbedBy) {
|
|
188
|
+
return Object.entries(absorbedBy).map(([absorbedId, repId]) => ({
|
|
189
|
+
recordId: absorbedId,
|
|
190
|
+
candidateIds: [repId],
|
|
191
|
+
action: 'merge',
|
|
192
|
+
}));
|
|
193
|
+
}
|
|
179
194
|
/**
|
|
180
195
|
* 一次完成:精确层 → (可选)向量层,并把“代表项→向量”与“被吸收→代表”两表一并交回。
|
|
181
196
|
*
|
package/dist/pipeline/l1.js
CHANGED
|
@@ -16,7 +16,7 @@ import { normTags, normUsageScenario, readCognitiveHall, readWing } from '../met
|
|
|
16
16
|
import { acceptTitle } from '../util/md-name.js';
|
|
17
17
|
import { clampPriority } from '../util/priority.js';
|
|
18
18
|
import { formatBatchConflictPrompt, getConflictDetectionSystemPrompt } from '../prompts/l1-dedup.js';
|
|
19
|
-
import { preDedupeBatch } from './intra-dedupe.js';
|
|
19
|
+
import { preDedupeBatch, receiptItemsForAbsorbed } from './intra-dedupe.js';
|
|
20
20
|
import { resolveSourceAnchors, withSourceAnchors } from './anchors.js';
|
|
21
21
|
import { stampValidity, validToSourceOf } from '../store/validity.js';
|
|
22
22
|
import { familyForType, normPersistence, normScope, resolveRecordFamily, resolveRecordScope, WING_FALLBACK } from '../types.js';
|
|
@@ -542,11 +542,7 @@ routeHealth) {
|
|
|
542
542
|
// 唯一零痕迹分支,批内吸收不能重蹈)。用既有 `kind='merge'` + candidateIds=[代表 id]:
|
|
543
543
|
// 不新增表列、不扩词表,回答“本条被批内哪条吸收”只需一次 (runId, recordId) 回溯。
|
|
544
544
|
// 关闭特性时 intraAbsorbedBy 为空 ⇒ 凭证与改动前逐字节一致。
|
|
545
|
-
const absorbedItems =
|
|
546
|
-
recordId: absorbedId,
|
|
547
|
-
candidateIds: [repId],
|
|
548
|
-
action: 'merge',
|
|
549
|
-
}));
|
|
545
|
+
const absorbedItems = receiptItemsForAbsorbed(intraAbsorbedBy);
|
|
550
546
|
const rows = buildReceipts(runId, new Date().toISOString(), [...items, ...absorbedItems]);
|
|
551
547
|
persistReceiptsSafely((rows) => store.recordReceipts(rows), rows, logger);
|
|
552
548
|
// §F 追踪捎带:六值决策词表聚合(与凭证同源,runner 侧并入 distill_run 事件)
|
package/dist/settings.js
CHANGED
|
@@ -90,6 +90,8 @@ const ALWAYS_ON = {
|
|
|
90
90
|
memportEnabled: false,
|
|
91
91
|
// 时效分级门2 灰度开关:默认关(注入与改前逐字一致)
|
|
92
92
|
validityTiering: false,
|
|
93
|
+
// 记忆库侧栏入口:默认开(用户装了插件就能看到)
|
|
94
|
+
explorerEnabled: true,
|
|
93
95
|
// md 投射护栏运行时覆盖:0 = 跟随部署 cfg.memExport.maxRecords(面板上可改)
|
|
94
96
|
mdExportMaxRecords: 0,
|
|
95
97
|
// 回填并行度运行时覆盖:0 = 跟随部署 cfg.backfill.concurrency(概览页滑块可改)
|
|
@@ -339,6 +341,7 @@ function resolveSettings(value) {
|
|
|
339
341
|
mdSyncEnabled: v.mdSyncEnabled === true,
|
|
340
342
|
memportEnabled: v.memportEnabled === true,
|
|
341
343
|
validityTiering: v.validityTiering === true,
|
|
344
|
+
explorerEnabled: v.explorerEnabled !== false, // 默认 true,只有显式 false 才关
|
|
342
345
|
// md 投射护栏钳 0~10000(与 live schema 同上限);0 = 跟随部署默认
|
|
343
346
|
mdExportMaxRecords: Math.min(10_000, Math.max(0, num(v.mdExportMaxRecords))),
|
|
344
347
|
// 回填并行度钳 0~8(与 live schema 同上限);0 = 跟随部署 cfg.backfill.concurrency
|
package/dist/stats.d.ts
CHANGED
|
@@ -168,8 +168,9 @@ export interface EndpointDeps {
|
|
|
168
168
|
/** 端点分发表(导出供测试直调:可精确注入 rebuild/ruminate 等可选控制器,验证 deps 接线)。 */
|
|
169
169
|
export declare function handleEndpoint(endpoint: string, payload: unknown, deps: EndpointDeps): Promise<unknown>;
|
|
170
170
|
/** 浏览器卡片字段(比 MemoryRecord 精简,去掉大 metadata;Hall 从 metadata 提取)。 */
|
|
171
|
-
/** 老化权重(与读路径 applyDecayWeight 同式):max(
|
|
172
|
-
|
|
171
|
+
/** 老化权重(与读路径 applyDecayWeight 同式):max(floor, 0.5^(Δ天/半衰期));半衰期 ≤0 = 关 → null。
|
|
172
|
+
* `floor` 缺省 0.5(召回/列表旧口径不变);星图传 0.2 以拉开更细的新年差异。 */
|
|
173
|
+
export declare function agingWeightOf(updatedAtMs: number | undefined, halfLifeDays: number, now?: number, floor?: number): number | null;
|
|
173
174
|
export declare function hitToUiRecord(r: {
|
|
174
175
|
id: string;
|
|
175
176
|
content: string;
|
package/dist/stats.js
CHANGED
|
@@ -109,6 +109,7 @@ export const MEMORY_ENDPOINTS = [
|
|
|
109
109
|
'dsh-memory/graph-search',
|
|
110
110
|
'dsh-memory/graph-node-get',
|
|
111
111
|
'dsh-memory/graph-preview',
|
|
112
|
+
'dsh-memory/record-links',
|
|
112
113
|
'dsh-memory/graph-cleanup-dead',
|
|
113
114
|
'dsh-memory/scenes',
|
|
114
115
|
'dsh-memory/persona',
|
|
@@ -1415,6 +1416,7 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
1415
1416
|
mdSyncEnabled: live?.get()?.mdSyncEnabled === true,
|
|
1416
1417
|
memportEnabled: live?.get()?.memportEnabled === true,
|
|
1417
1418
|
validityTiering: live?.get()?.validityTiering === true,
|
|
1419
|
+
explorerEnabled: live?.get()?.explorerEnabled !== false, // 默认 true
|
|
1418
1420
|
mdExportMaxRecords: live?.get()?.mdExportMaxRecords ?? 0,
|
|
1419
1421
|
backfillConcurrency: live?.get()?.backfillConcurrency ?? 0,
|
|
1420
1422
|
}),
|
|
@@ -1461,7 +1463,7 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
1461
1463
|
const patch = (payload ?? {});
|
|
1462
1464
|
const clean = {};
|
|
1463
1465
|
// 布尔开关组:memoryMutate(高权限写删门)与主开关同列;conflictFreeze(§C 人工冲突裁决)
|
|
1464
|
-
for (const key of ['enabled', 'capture', 'distill', 'recall', 'memoryMutate', 'conflictFreeze', 'skillsEnabled', 'graphEnabled', 'keywordEnabled', 'redistillEnabled', 'mdSyncEnabled', 'memportEnabled', 'toolRoom', 'toolGraph', 'toolRuminate', 'toolConflict', 'toolMutate', 'toolSkills']) {
|
|
1466
|
+
for (const key of ['enabled', 'capture', 'distill', 'recall', 'memoryMutate', 'conflictFreeze', 'skillsEnabled', 'graphEnabled', 'keywordEnabled', 'redistillEnabled', 'mdSyncEnabled', 'memportEnabled', 'explorerEnabled', 'toolRoom', 'toolGraph', 'toolRuminate', 'toolConflict', 'toolMutate', 'toolSkills']) {
|
|
1465
1467
|
if (typeof patch[key] === 'boolean')
|
|
1466
1468
|
clean[key] = patch[key];
|
|
1467
1469
|
}
|
|
@@ -2117,7 +2119,80 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
2117
2119
|
const zero = { seeds: [], stats: { nodes: 0, edges: 0, causalEdges: 0, pendingJobs: 0, jobsTotal: 0 } };
|
|
2118
2120
|
return mode === 'full' ? { ...zero, full: { nodes: [], edges: [], truncated: false } } : zero;
|
|
2119
2121
|
}
|
|
2120
|
-
|
|
2122
|
+
// 老化 + 出处两条展示通道:节点无自己的老化/场景——都属于来源记录。
|
|
2123
|
+
// 一次 sourceMeta() 同时供两算:aging 取最新存活来源的 agingWeight(下限 0.2 拉开亮度);
|
|
2124
|
+
// source 取主场景名(provenance fallback,B 方案落库后退为兜底)。无值→不挂键(可选键零漂移)。
|
|
2125
|
+
let agingOf;
|
|
2126
|
+
let sourceOf;
|
|
2127
|
+
if (mode === 'full') {
|
|
2128
|
+
const halfLife = stores.l1.decayHalfLife;
|
|
2129
|
+
const meta = graph.sourceMeta();
|
|
2130
|
+
sourceOf = (id) => meta.get(id)?.scene ?? null;
|
|
2131
|
+
if (halfLife > 0) {
|
|
2132
|
+
agingOf = (id) => {
|
|
2133
|
+
const m = meta.get(id);
|
|
2134
|
+
// 星图专用下限 0.2:让 0.2~1 的亮度差异可见(召回/列表仍用缺省 0.5,不受影响)。
|
|
2135
|
+
return m == null ? null : agingWeightOf(m.ms, halfLife, undefined, 0.2);
|
|
2136
|
+
};
|
|
2137
|
+
}
|
|
2138
|
+
}
|
|
2139
|
+
return graph.buildPreview(seedId, mode, agingOf, sourceOf);
|
|
2140
|
+
}
|
|
2141
|
+
case 'dsh-memory/record-links': {
|
|
2142
|
+
// 记忆库入口的关联跳转(记录 → 实体 → 关系 → 其它记录)。图侧一次 loadGraph 供三算
|
|
2143
|
+
// (见 GraphStore.buildExplorer),记录侧在这里装配:UiRecord 的 agingWeight / usage /
|
|
2144
|
+
// maturity 必须与 list-records **同口径**,否则同一个入口与设置页会给出两套数。
|
|
2145
|
+
const p = (payload ?? {});
|
|
2146
|
+
const recordId = typeof p.recordId === 'string' ? p.recordId.trim().slice(0, 200) : '';
|
|
2147
|
+
// hops/limit 不在这里重复钳制——钳制判据单源在 graph/record-links.ts。
|
|
2148
|
+
const hops = typeof p.hops === 'number' ? p.hops : undefined;
|
|
2149
|
+
const limit = typeof p.limit === 'number' ? p.limit : undefined;
|
|
2150
|
+
const graph = stores.graph;
|
|
2151
|
+
const emptyResp = {
|
|
2152
|
+
record: null,
|
|
2153
|
+
entities: [],
|
|
2154
|
+
neighbors: [],
|
|
2155
|
+
records: [],
|
|
2156
|
+
supersede: { prevIds: [], nextId: null },
|
|
2157
|
+
truncated: false,
|
|
2158
|
+
};
|
|
2159
|
+
// 图谱未启用 / 记录不存在 / 该记录尚未投影:都是正常态而非错误态,统一回空结构不抛。
|
|
2160
|
+
if (!graph || !recordId)
|
|
2161
|
+
return emptyResp;
|
|
2162
|
+
const { links } = graph.buildExplorer(recordId, { hops, limit });
|
|
2163
|
+
const rawById = new Map(stores.l1.getByIds([recordId, ...links.neighborRecordIds]).map((r) => [r.id, r]));
|
|
2164
|
+
const toUi = (id) => {
|
|
2165
|
+
const r = rawById.get(id);
|
|
2166
|
+
if (!r)
|
|
2167
|
+
return null;
|
|
2168
|
+
return hitToUiRecord(r, {
|
|
2169
|
+
agingWeight: agingWeightOf(r.updatedAt, stores.l1.decayHalfLife),
|
|
2170
|
+
usage: stores.usage?.get(r.id) ?? null,
|
|
2171
|
+
userRelevance: stores.feedback?.meanOf(r.id) ?? null,
|
|
2172
|
+
userMark: stores.feedback?.lastOf(r.id) ?? null,
|
|
2173
|
+
});
|
|
2174
|
+
};
|
|
2175
|
+
const record = toUi(recordId);
|
|
2176
|
+
const records = [];
|
|
2177
|
+
for (const id of links.neighborRecordIds) {
|
|
2178
|
+
const u = toUi(id);
|
|
2179
|
+
if (u)
|
|
2180
|
+
records.push(u);
|
|
2181
|
+
}
|
|
2182
|
+
// 取代链方向由 marker 的位置决定:dsh_superseded **写在退场的旧行上**、by 指向新行。
|
|
2183
|
+
// 故 nextId 读本行的 marker(单值)、prevIds 走反向查(数组,LIKE 全扫缝)。
|
|
2184
|
+
const nextId = readSupersedeMarker(rawById.get(recordId)?.metadata)?.by ?? null;
|
|
2185
|
+
const prevIds = stores.l1.findSupersededTargets(recordId);
|
|
2186
|
+
if (!record && links.entities.length === 0)
|
|
2187
|
+
return { ...emptyResp, records, truncated: links.truncated };
|
|
2188
|
+
return {
|
|
2189
|
+
record,
|
|
2190
|
+
entities: links.entities,
|
|
2191
|
+
neighbors: links.neighbors,
|
|
2192
|
+
records,
|
|
2193
|
+
supersede: { prevIds, nextId },
|
|
2194
|
+
truncated: links.truncated,
|
|
2195
|
+
};
|
|
2121
2196
|
}
|
|
2122
2197
|
case 'dsh-memory/graph-cleanup-dead': {
|
|
2123
2198
|
const graph = stores.graph;
|
|
@@ -2429,12 +2504,13 @@ export async function handleEndpoint(endpoint, payload, deps) {
|
|
|
2429
2504
|
}
|
|
2430
2505
|
}
|
|
2431
2506
|
/** 浏览器卡片字段(比 MemoryRecord 精简,去掉大 metadata;Hall 从 metadata 提取)。 */
|
|
2432
|
-
/** 老化权重(与读路径 applyDecayWeight 同式):max(
|
|
2433
|
-
|
|
2507
|
+
/** 老化权重(与读路径 applyDecayWeight 同式):max(floor, 0.5^(Δ天/半衰期));半衰期 ≤0 = 关 → null。
|
|
2508
|
+
* `floor` 缺省 0.5(召回/列表旧口径不变);星图传 0.2 以拉开更细的新年差异。 */
|
|
2509
|
+
export function agingWeightOf(updatedAtMs, halfLifeDays, now = Date.now(), floor = 0.5) {
|
|
2434
2510
|
if (!(halfLifeDays > 0))
|
|
2435
2511
|
return null;
|
|
2436
2512
|
const ageDays = Math.max(0, (now - (updatedAtMs ?? 0)) / 86_400_000);
|
|
2437
|
-
return Math.max(
|
|
2513
|
+
return Math.max(floor, Math.pow(0.5, ageDays / halfLifeDays));
|
|
2438
2514
|
}
|
|
2439
2515
|
export function hitToUiRecord(r, extra) {
|
|
2440
2516
|
// 退场判据 marker 单源(门1 解耦,与 `isRetired`/`retiredL1Sql` 同口径)。
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { DatabaseSync } from 'node:sqlite';
|
|
2
2
|
import { buildFullView } from '../graph/preview.js';
|
|
3
3
|
import type { GraphPreviewResult } from '../graph/preview.js';
|
|
4
|
+
import type { RecordLinksResult } from '../graph/record-links.js';
|
|
5
|
+
import type { ExplorerSelection } from '../graph/select.js';
|
|
4
6
|
import type { GraphEdge, GraphNode, GraphNodeSearchResult, GraphProjectionJob, GraphProjectionResult } from '../graph/types.js';
|
|
5
7
|
import type { MemoryFamily, MemoryLogger, MemoryRecord } from '../types.js';
|
|
6
8
|
/**
|
|
@@ -123,10 +125,44 @@ export declare class GraphStore {
|
|
|
123
125
|
* 降级(db 未就绪)返**全零结构**不抛——预览是展示面,永不拖垮端点。
|
|
124
126
|
* mode==='full' 时响应附 `full` 轻量块;**缺省时该键缺席**(可选键零漂移纪律,
|
|
125
127
|
* 不是 undefined 占位——消费方 `'full' in res` 与 JSON 形状双判据都干净)。
|
|
128
|
+
* `agingOf`/`sourceOf` 仅在 mode==='full' 时生效(星图亮度与出处通道):本层不认识 L1,
|
|
129
|
+
* 由端点层查来源记录后注入判据(nodeId → aging | null / 场景名 | null)。
|
|
126
130
|
*/
|
|
127
|
-
buildPreview(seedId?: string, mode?: 'full'): GraphPreviewResult & {
|
|
131
|
+
buildPreview(seedId?: string, mode?: 'full', agingOf?: (id: string) => number | null, sourceOf?: (id: string) => string | null): GraphPreviewResult & {
|
|
128
132
|
full?: ReturnType<typeof buildFullView>;
|
|
129
133
|
};
|
|
134
|
+
/**
|
|
135
|
+
* 节点 → 存活来源记录的最新时间 + 主场景标签。
|
|
136
|
+
* `graph-preview(mode:'full')` 老化与出处两条展示通道的数据源:
|
|
137
|
+
* - `ms`:最新存活来源的 `updated_time`(老化属于记录不属于投影节点,端点层据此算 agingWeight);
|
|
138
|
+
* - `scene`:来源记录中出现最多的非空 `scene_name`(provenance fallback:人物实体靠场景回溯“在哪提到的”,
|
|
139
|
+
* 如“字节Code M岗位”;无场景则 null,不猜)。B 方案(投影期落库 context)上线后本项退为兜底。
|
|
140
|
+
* 只认有值的行:全来源无有效时间戳的节点**不进表**。降级返空表不抛。
|
|
141
|
+
*/
|
|
142
|
+
sourceMeta(): Map<string, {
|
|
143
|
+
ms: number;
|
|
144
|
+
scene: string | null;
|
|
145
|
+
}>;
|
|
146
|
+
/**
|
|
147
|
+
* 投影作业三档计数(预览与记忆库视图共用同一分母)。
|
|
148
|
+
* db 未就绪或计数失败一律回零并 warn——计数是展示面,不值得拖垮端点。
|
|
149
|
+
*/
|
|
150
|
+
private jobCounts;
|
|
151
|
+
/**
|
|
152
|
+
* 记忆库视图存储缝(契约 `dsh-memory/record-links` 的图侧数据源)。
|
|
153
|
+
*
|
|
154
|
+
* **一次 `loadGraph()` 供三算**(种子榜 / 元素选择 / 关联反查):三者必须看到
|
|
155
|
+
* 同一份图,否则会出现“画布上有这个节点、关联清单里查不到”或反之——那比慢
|
|
156
|
+
* 一点严重点。记录侧(UiRecord 装配与取代链)在 stats 端点层做,本方法不碰 l1 表。
|
|
157
|
+
*/
|
|
158
|
+
buildExplorer(recordId?: string, opts?: {
|
|
159
|
+
hops?: number;
|
|
160
|
+
limit?: number;
|
|
161
|
+
}): {
|
|
162
|
+
preview: GraphPreviewResult;
|
|
163
|
+
view: ExplorerSelection;
|
|
164
|
+
links: RecordLinksResult;
|
|
165
|
+
};
|
|
130
166
|
/**
|
|
131
167
|
* 清理历史 dead 作业行(纯审计痕迹删除,不影响已重入队的记录)。
|
|
132
168
|
* olderThanMs: 只删早于 now-olderThanMs 的;缺省删全部 dead。
|
|
@@ -21,6 +21,8 @@ import { randomBytes } from 'node:crypto';
|
|
|
21
21
|
import { applyGraphProjection } from '../graph/apply.js';
|
|
22
22
|
import { expandHitRecordIds, searchGraphNodes } from '../graph/search.js';
|
|
23
23
|
import { buildFullView, buildGraphPreview } from '../graph/preview.js';
|
|
24
|
+
import { buildRecordLinks } from '../graph/record-links.js';
|
|
25
|
+
import { selectExplorerNodes } from '../graph/select.js';
|
|
24
26
|
import { GRAPH_JOB_BACKOFF_BASE_MS, GRAPH_JOB_BATCH, GRAPH_JOB_MAX_ATTEMPTS, GRAPH_JOB_TRANSIENT_MAX_ATTEMPTS, GRAPH_PRIORITY_BACKFILL, GRAPH_PRIORITY_NEW, GRAPH_PROJECTOR_VERSION, GRAPH_VEC_SEED_CAP, } from '../graph/types.js';
|
|
25
27
|
import { normPersistence } from '../types.js';
|
|
26
28
|
import { classifyValidity, isExitedState, validToSourceOf } from './validity.js';
|
|
@@ -510,29 +512,133 @@ export class GraphStore {
|
|
|
510
512
|
* 降级(db 未就绪)返**全零结构**不抛——预览是展示面,永不拖垮端点。
|
|
511
513
|
* mode==='full' 时响应附 `full` 轻量块;**缺省时该键缺席**(可选键零漂移纪律,
|
|
512
514
|
* 不是 undefined 占位——消费方 `'full' in res` 与 JSON 形状双判据都干净)。
|
|
515
|
+
* `agingOf`/`sourceOf` 仅在 mode==='full' 时生效(星图亮度与出处通道):本层不认识 L1,
|
|
516
|
+
* 由端点层查来源记录后注入判据(nodeId → aging | null / 场景名 | null)。
|
|
513
517
|
*/
|
|
514
|
-
buildPreview(seedId, mode) {
|
|
518
|
+
buildPreview(seedId, mode, agingOf, sourceOf) {
|
|
515
519
|
const zero = { seeds: [], stats: { nodes: 0, edges: 0, causalEdges: 0, pendingJobs: 0, jobsTotal: 0 } };
|
|
516
520
|
if (!this.db)
|
|
517
521
|
return mode === 'full' ? { ...zero, full: { nodes: [], edges: [], truncated: false } } : zero;
|
|
518
522
|
const { nodes, edges } = this.loadGraph();
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
523
|
+
const { pendingJobs, jobsTotal, deadJobs } = this.jobCounts();
|
|
524
|
+
const r = buildGraphPreview(nodes, edges, { seedId, pendingJobs, jobsTotal, deadJobs, missing: this.countMissing() });
|
|
525
|
+
if (mode === 'full')
|
|
526
|
+
return { ...r, full: buildFullView(nodes, edges, { agingOf, sourceOf }) };
|
|
527
|
+
return r;
|
|
528
|
+
}
|
|
529
|
+
/**
|
|
530
|
+
* 节点 → 存活来源记录的最新时间 + 主场景标签。
|
|
531
|
+
* `graph-preview(mode:'full')` 老化与出处两条展示通道的数据源:
|
|
532
|
+
* - `ms`:最新存活来源的 `updated_time`(老化属于记录不属于投影节点,端点层据此算 agingWeight);
|
|
533
|
+
* - `scene`:来源记录中出现最多的非空 `scene_name`(provenance fallback:人物实体靠场景回溯“在哪提到的”,
|
|
534
|
+
* 如“字节Code M岗位”;无场景则 null,不猜)。B 方案(投影期落库 context)上线后本项退为兜底。
|
|
535
|
+
* 只认有值的行:全来源无有效时间戳的节点**不进表**。降级返空表不抛。
|
|
536
|
+
*/
|
|
537
|
+
sourceMeta() {
|
|
538
|
+
const out = new Map();
|
|
539
|
+
if (!this.db)
|
|
540
|
+
return out;
|
|
541
|
+
try {
|
|
542
|
+
const { nodes } = this.loadGraph();
|
|
543
|
+
const sources = new Set();
|
|
544
|
+
for (const n of nodes)
|
|
545
|
+
for (const id of n.sourceRecordIds)
|
|
546
|
+
sources.add(id);
|
|
547
|
+
if (sources.size === 0)
|
|
548
|
+
return out;
|
|
549
|
+
const updatedById = new Map();
|
|
550
|
+
const sceneById = new Map();
|
|
551
|
+
for (const chunk of chunkOf([...sources])) {
|
|
552
|
+
const ph = chunk.map(() => '?').join(',');
|
|
553
|
+
for (const row of this.db
|
|
554
|
+
.prepare(`SELECT record_id, updated_time, scene_name FROM l1_records WHERE record_id IN (${ph})`)
|
|
555
|
+
.all(...chunk)) {
|
|
556
|
+
const ms = Date.parse(String(row.updated_time ?? ''));
|
|
557
|
+
if (Number.isFinite(ms))
|
|
558
|
+
updatedById.set(row.record_id, ms);
|
|
559
|
+
const sc = String(row.scene_name ?? '').trim();
|
|
560
|
+
if (sc)
|
|
561
|
+
sceneById.set(row.record_id, sc);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
for (const n of nodes) {
|
|
565
|
+
let latest = -1;
|
|
566
|
+
const tally = new Map();
|
|
567
|
+
for (const id of n.sourceRecordIds) {
|
|
568
|
+
const ms = updatedById.get(id);
|
|
569
|
+
if (ms != null)
|
|
570
|
+
latest = Math.max(latest, ms);
|
|
571
|
+
const sc = sceneById.get(id);
|
|
572
|
+
if (sc)
|
|
573
|
+
tally.set(sc, (tally.get(sc) ?? 0) + 1);
|
|
574
|
+
}
|
|
575
|
+
if (latest < 0)
|
|
576
|
+
continue; // 无有效时间戳:不挂(aging 与 scene 同进退)
|
|
577
|
+
let scene = null;
|
|
578
|
+
let best = 0;
|
|
579
|
+
for (const [s, c] of tally)
|
|
580
|
+
if (c > best) {
|
|
581
|
+
best = c;
|
|
582
|
+
scene = s;
|
|
583
|
+
}
|
|
584
|
+
out.set(n.id, { ms: latest, scene });
|
|
585
|
+
}
|
|
586
|
+
return out;
|
|
587
|
+
}
|
|
588
|
+
catch (err) {
|
|
589
|
+
this.logger?.warn(`${TAG} 来源元信息读取失败(老化/出处通道整轮缺省): ${err instanceof Error ? err.message : String(err)}`);
|
|
590
|
+
return new Map();
|
|
591
|
+
}
|
|
592
|
+
}
|
|
593
|
+
/**
|
|
594
|
+
* 投影作业三档计数(预览与记忆库视图共用同一分母)。
|
|
595
|
+
* db 未就绪或计数失败一律回零并 warn——计数是展示面,不值得拖垮端点。
|
|
596
|
+
*/
|
|
597
|
+
jobCounts() {
|
|
598
|
+
const db = this.db;
|
|
599
|
+
if (!db)
|
|
600
|
+
return { pendingJobs: 0, jobsTotal: 0, deadJobs: 0 };
|
|
522
601
|
try {
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
602
|
+
const count = (where) => db.prepare(`SELECT COUNT(1) AS c FROM graph_projection_jobs${where}`).get().c;
|
|
603
|
+
return {
|
|
604
|
+
pendingJobs: count(` WHERE status IN ('pending','running','failed')`),
|
|
605
|
+
jobsTotal: count(''),
|
|
606
|
+
deadJobs: count(` WHERE status = 'dead'`),
|
|
607
|
+
};
|
|
528
608
|
}
|
|
529
609
|
catch (err) {
|
|
530
|
-
this.logger?.warn(`${TAG}
|
|
610
|
+
this.logger?.warn(`${TAG} job 计数失败(计数回零): ${err instanceof Error ? err.message : String(err)}`);
|
|
611
|
+
return { pendingJobs: 0, jobsTotal: 0, deadJobs: 0 };
|
|
531
612
|
}
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
613
|
+
}
|
|
614
|
+
/**
|
|
615
|
+
* 记忆库视图存储缝(契约 `dsh-memory/record-links` 的图侧数据源)。
|
|
616
|
+
*
|
|
617
|
+
* **一次 `loadGraph()` 供三算**(种子榜 / 元素选择 / 关联反查):三者必须看到
|
|
618
|
+
* 同一份图,否则会出现“画布上有这个节点、关联清单里查不到”或反之——那比慢
|
|
619
|
+
* 一点严重点。记录侧(UiRecord 装配与取代链)在 stats 端点层做,本方法不碰 l1 表。
|
|
620
|
+
*/
|
|
621
|
+
buildExplorer(recordId, opts = {}) {
|
|
622
|
+
const emptyStats = { nodes: 0, edges: 0, causalEdges: 0, pendingJobs: 0, jobsTotal: 0 };
|
|
623
|
+
if (!this.db) {
|
|
624
|
+
return {
|
|
625
|
+
preview: { seeds: [], stats: emptyStats },
|
|
626
|
+
view: {
|
|
627
|
+
nodes: [], edges: [], truncated: false,
|
|
628
|
+
counts: { skeleton: 0, fresh: 0, tail: 0 }, totalVisible: 0, totalEdges: 0,
|
|
629
|
+
},
|
|
630
|
+
links: { entities: [], neighbors: [], neighborRecordIds: [], truncated: false },
|
|
631
|
+
};
|
|
632
|
+
}
|
|
633
|
+
const { nodes, edges } = this.loadGraph();
|
|
634
|
+
const jc = this.jobCounts();
|
|
635
|
+
return {
|
|
636
|
+
preview: buildGraphPreview(nodes, edges, {
|
|
637
|
+
pendingJobs: jc.pendingJobs, jobsTotal: jc.jobsTotal, deadJobs: jc.deadJobs, missing: this.countMissing(),
|
|
638
|
+
}),
|
|
639
|
+
view: selectExplorerNodes(nodes, edges),
|
|
640
|
+
links: buildRecordLinks(nodes, edges, recordId ?? '', opts),
|
|
641
|
+
};
|
|
536
642
|
}
|
|
537
643
|
/**
|
|
538
644
|
* 清理历史 dead 作业行(纯审计痕迹删除,不影响已重入队的记录)。
|
package/dsh.plugin.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"id": "dsh-prime-memory",
|
|
3
3
|
"name": "dsh-prime-memory",
|
|
4
|
-
"version": "0.26.
|
|
4
|
+
"version": "0.26.32",
|
|
5
5
|
"description": {
|
|
6
6
|
"en": "Layered distilled memory plugin (L0 capture → L1 atomic memory → L2 scenes → L3 persona) with automatic recall injection before each model step.",
|
|
7
7
|
"zh": "L0~L3 分层蒸馏记忆插件:自动捕获对话、抽取原子记忆、整合场景块、蒸馏核心画像,并在模型步骤前自动召回注入。"
|