dsh-prime-memory 0.26.31 → 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/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(0.5, 0.5^(Δ天/半衰期));半衰期 ≤0 = 关 → null。 */
172
- export declare function agingWeightOf(updatedAtMs: number | undefined, halfLifeDays: number, now?: number): number | null;
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
- return graph.buildPreview(seedId, mode);
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(0.5, 0.5^(Δ天/半衰期));半衰期 ≤0 = 关 → null。 */
2433
- export function agingWeightOf(updatedAtMs, halfLifeDays, now = Date.now()) {
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(0.5, Math.pow(0.5, ageDays / halfLifeDays));
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
- let pendingJobs = 0;
520
- let jobsTotal = 0;
521
- let deadJobs = 0;
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
- pendingJobs = this.db
524
- .prepare("SELECT COUNT(1) AS c FROM graph_projection_jobs WHERE status IN ('pending','running','failed')")
525
- .get().c;
526
- jobsTotal = this.db.prepare('SELECT COUNT(1) AS c FROM graph_projection_jobs').get().c;
527
- deadJobs = this.db.prepare("SELECT COUNT(1) AS c FROM graph_projection_jobs WHERE status = 'dead'").get().c;
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} 预览 job 计数失败(计数回零): ${err instanceof Error ? err.message : String(err)}`);
610
+ this.logger?.warn(`${TAG} job 计数失败(计数回零): ${err instanceof Error ? err.message : String(err)}`);
611
+ return { pendingJobs: 0, jobsTotal: 0, deadJobs: 0 };
531
612
  }
532
- const r = buildGraphPreview(nodes, edges, { seedId, pendingJobs, jobsTotal, deadJobs, missing: this.countMissing() });
533
- if (mode === 'full')
534
- return { ...r, full: buildFullView(nodes, edges) };
535
- return r;
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.31",
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 分层蒸馏记忆插件:自动捕获对话、抽取原子记忆、整合场景块、蒸馏核心画像,并在模型步骤前自动召回注入。"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-prime-memory",
3
- "version": "0.26.31",
3
+ "version": "0.26.32",
4
4
  "publishConfig": {
5
5
  "registry": "https://registry.npmjs.org",
6
6
  "access": "public",