dsh-prime-memory 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.en.md +28 -0
- package/CHANGELOG.ja.md +30 -0
- package/CHANGELOG.ko.md +30 -0
- package/CHANGELOG.md +1220 -0
- package/ENGINEERING-NOTES.md +452 -0
- package/INSTALL.en.md +92 -0
- package/INSTALL.ja.md +92 -0
- package/INSTALL.ko.md +92 -0
- package/INSTALL.md +92 -0
- package/LICENSE +21 -0
- package/README.en.md +458 -0
- package/README.ja.md +306 -0
- package/README.ko.md +306 -0
- package/README.md +425 -0
- package/assets/changelog/0.8.10/01-write-only-pill.png +0 -0
- package/assets/changelog/0.8.9/01-panel.png +0 -0
- package/assets/changelog/0.8.9/02-halo.png +0 -0
- package/assets/changelog/0.8.9/03-layer-segmented-panel.png +0 -0
- package/assets/changelog/0.8.9/04-layer-l1-panel.png +0 -0
- package/assets/img/EmbeddingSource.png +0 -0
- package/assets/img/Hero.png +0 -0
- package/assets/img/Layers.png +0 -0
- package/assets/img/MemoryTools.png +0 -0
- package/assets/img/Modes.png +0 -0
- package/assets/img/ToolTrajectory.png +0 -0
- package/assets/img/ui-dark.jpg +0 -0
- package/assets/img/ui-light.jpg +0 -0
- package/assets/readme/bench-dialog.svg +70 -0
- package/assets/readme/bench-workflow.svg +79 -0
- package/assets/readme/flow.svg +189 -0
- package/assets/readme/storage.svg +115 -0
- package/cordis.patch.yml +16 -0
- package/dist/bench-control.d.ts +34 -0
- package/dist/bench-control.js +16 -0
- package/dist/client.js +4293 -0
- package/dist/config.d.ts +683 -0
- package/dist/config.js +129 -0
- package/dist/contract.d.ts +820 -0
- package/dist/contract.js +1 -0
- package/dist/embedding-worker.cjs +176 -0
- package/dist/graph/apply.d.ts +37 -0
- package/dist/graph/apply.js +270 -0
- package/dist/graph/constraints.d.ts +47 -0
- package/dist/graph/constraints.js +38 -0
- package/dist/graph/search.d.ts +16 -0
- package/dist/graph/search.js +115 -0
- package/dist/graph/types.d.ts +142 -0
- package/dist/graph/types.js +14 -0
- package/dist/hooks/capture.d.ts +32 -0
- package/dist/hooks/capture.js +194 -0
- package/dist/hooks/recall.d.ts +63 -0
- package/dist/hooks/recall.js +429 -0
- package/dist/index.d.ts +534 -0
- package/dist/index.js +344 -0
- package/dist/llm-usage.d.ts +26 -0
- package/dist/llm-usage.js +37 -0
- package/dist/llm.d.ts +153 -0
- package/dist/llm.js +530 -0
- package/dist/pipeline/graph.d.ts +35 -0
- package/dist/pipeline/graph.js +104 -0
- package/dist/pipeline/l1.d.ts +19 -0
- package/dist/pipeline/l1.js +271 -0
- package/dist/pipeline/l2.d.ts +13 -0
- package/dist/pipeline/l2.js +83 -0
- package/dist/pipeline/l3.d.ts +15 -0
- package/dist/pipeline/l3.js +78 -0
- package/dist/pipeline/rebuild.d.ts +61 -0
- package/dist/pipeline/rebuild.js +307 -0
- package/dist/pipeline/ruminate.d.ts +89 -0
- package/dist/pipeline/ruminate.js +298 -0
- package/dist/pipeline/runner.d.ts +167 -0
- package/dist/pipeline/runner.js +638 -0
- package/dist/pipeline/trigger.d.ts +40 -0
- package/dist/pipeline/trigger.js +75 -0
- package/dist/prompts/graph-projection.d.ts +70 -0
- package/dist/prompts/graph-projection.js +167 -0
- package/dist/prompts/l1-dedup.d.ts +22 -0
- package/dist/prompts/l1-dedup.js +251 -0
- package/dist/prompts/l1-extraction.d.ts +22 -0
- package/dist/prompts/l1-extraction.js +457 -0
- package/dist/prompts/persona.d.ts +23 -0
- package/dist/prompts/persona.js +240 -0
- package/dist/prompts/scene.d.ts +32 -0
- package/dist/prompts/scene.js +414 -0
- package/dist/runtime-package-lock.json +982 -0
- package/dist/settings.d.ts +50 -0
- package/dist/settings.js +355 -0
- package/dist/stats.d.ts +109 -0
- package/dist/stats.js +929 -0
- package/dist/store/bm25.d.ts +19 -0
- package/dist/store/bm25.js +63 -0
- package/dist/store/cost-ledger.d.ts +75 -0
- package/dist/store/cost-ledger.js +171 -0
- package/dist/store/download-queue.d.ts +79 -0
- package/dist/store/download-queue.js +424 -0
- package/dist/store/embedding-source.d.ts +118 -0
- package/dist/store/embedding-source.js +443 -0
- package/dist/store/embedding.d.ts +90 -0
- package/dist/store/embedding.js +206 -0
- package/dist/store/graph-store.d.ts +94 -0
- package/dist/store/graph-store.js +641 -0
- package/dist/store/l0.d.ts +40 -0
- package/dist/store/l0.js +197 -0
- package/dist/store/l1.d.ts +93 -0
- package/dist/store/l1.js +297 -0
- package/dist/store/local-embedding.d.ts +89 -0
- package/dist/store/local-embedding.js +227 -0
- package/dist/store/model-catalog.d.ts +48 -0
- package/dist/store/model-catalog.js +81 -0
- package/dist/store/occupancy.d.ts +30 -0
- package/dist/store/occupancy.js +134 -0
- package/dist/store/pending.d.ts +36 -0
- package/dist/store/pending.js +103 -0
- package/dist/store/persona.d.ts +15 -0
- package/dist/store/persona.js +60 -0
- package/dist/store/recall-dedupe.d.ts +26 -0
- package/dist/store/recall-dedupe.js +138 -0
- package/dist/store/runtime-installer.d.ts +59 -0
- package/dist/store/runtime-installer.js +243 -0
- package/dist/store/scenes.d.ts +24 -0
- package/dist/store/scenes.js +160 -0
- package/dist/store/search-utils.d.ts +38 -0
- package/dist/store/search-utils.js +100 -0
- package/dist/store/session-modes.d.ts +35 -0
- package/dist/store/session-modes.js +144 -0
- package/dist/store/sqlite.d.ts +246 -0
- package/dist/store/sqlite.js +1491 -0
- package/dist/store/state.d.ts +41 -0
- package/dist/store/state.js +72 -0
- package/dist/token-cost.d.ts +23 -0
- package/dist/token-cost.js +185 -0
- package/dist/tools/index.d.ts +34 -0
- package/dist/tools/index.js +758 -0
- package/dist/types.d.ts +139 -0
- package/dist/types.js +38 -0
- package/dist/util/context-occupancy.d.ts +68 -0
- package/dist/util/context-occupancy.js +92 -0
- package/dist/util/filelog.d.ts +6 -0
- package/dist/util/filelog.js +108 -0
- package/dist/util/io.d.ts +18 -0
- package/dist/util/io.js +97 -0
- package/dist/util/recall-budget.d.ts +31 -0
- package/dist/util/recall-budget.js +84 -0
- package/dist/util/sanitize.d.ts +11 -0
- package/dist/util/sanitize.js +67 -0
- package/dist/util/text.d.ts +16 -0
- package/dist/util/text.js +61 -0
- package/dist/util/tokenizer.d.ts +9 -0
- package/dist/util/tokenizer.js +50 -0
- package/dsh.plugin.json +22 -0
- package/package.json +118 -0
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 图谱节点检索(纯函数,无 I/O):字段加权重叠计分 + 可解释命中输出。
|
|
3
|
+
*
|
|
4
|
+
* 与 L1 检索(FTS5/向量)不同,图谱节点量级是"可重建投影"的百~千级,在此
|
|
5
|
+
* 直接内存打分即可,不建倒排索引(>2k 节点再考虑预建——见规划可延后清单)。
|
|
6
|
+
*
|
|
7
|
+
* 设计要点:
|
|
8
|
+
* - 分词与 L0/L1 共用 util/text.ts 的 tokenize(jieba 词 ∪ 拉丁词 ∪ CJK 二元组),
|
|
9
|
+
* 查询与文档两侧同源;
|
|
10
|
+
* - 字段加权:name×6 / aliases×5 / tags×5 / currentState×4 / facts×4 /
|
|
11
|
+
* relations×3 / type×2——名称是实体最强身份信号,关系词最弱(邻接噪声);
|
|
12
|
+
* - facts 只拼 active(superseded 是历史,不该让旧状态把节点拉进结果);
|
|
13
|
+
* - relations 只取 active 边(superseded 的旧关系同理);
|
|
14
|
+
* - "仅关系词命中"的结果过滤丢弃:只命中关系名的节点通常是邻接噪声
|
|
15
|
+
* (type-only 命中仍保留,保证按"项目/工具"等类型搜索可用);
|
|
16
|
+
* - score 只用于排序,不是事实置信度;无词法命中返回空,不兜底。
|
|
17
|
+
*/
|
|
18
|
+
import { tokenize } from '../util/text.js';
|
|
19
|
+
import { GRAPH_SEARCH_LIMIT_MAX } from './types.js';
|
|
20
|
+
/** 字段权重(检索命中计分;名称最高、关系词最低)。 */
|
|
21
|
+
export const GRAPH_FIELD_WEIGHTS = {
|
|
22
|
+
name: 6,
|
|
23
|
+
aliases: 5,
|
|
24
|
+
tags: 5,
|
|
25
|
+
currentState: 4,
|
|
26
|
+
facts: 4,
|
|
27
|
+
relations: 3,
|
|
28
|
+
type: 2,
|
|
29
|
+
};
|
|
30
|
+
/** matchedFields 的中文标签(matchReason 组装)。 */
|
|
31
|
+
const FIELD_LABELS = {
|
|
32
|
+
name: '名称',
|
|
33
|
+
aliases: '别名',
|
|
34
|
+
tags: '标签',
|
|
35
|
+
currentState: '当前状态',
|
|
36
|
+
facts: '事实',
|
|
37
|
+
relations: '关系',
|
|
38
|
+
type: '类型',
|
|
39
|
+
};
|
|
40
|
+
const FIELD_ORDER = ['name', 'aliases', 'tags', 'currentState', 'facts', 'relations', 'type'];
|
|
41
|
+
/** fact 值渲染进检索文本(数组值空格连接)。 */
|
|
42
|
+
function factValueText(value) {
|
|
43
|
+
return Array.isArray(value) ? value.join(' ') : value;
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* 图谱节点检索:返回按 score 降序的命中(最多 limit 条;limit 钳制 1~20)。
|
|
47
|
+
* 候选节点 = active | disputed(superseded/archived 不进检索)。
|
|
48
|
+
*/
|
|
49
|
+
export function searchGraphNodes(nodes, edges, query, limit) {
|
|
50
|
+
const tokens = [...new Set(tokenize(query))];
|
|
51
|
+
const cappedLimit = Math.min(Math.max(Math.floor(limit) || 5, 1), GRAPH_SEARCH_LIMIT_MAX);
|
|
52
|
+
if (tokens.length === 0)
|
|
53
|
+
return [];
|
|
54
|
+
// nodeId → active 关系词 token 集(先建邻接表,防逐节点全量扫边)
|
|
55
|
+
const relationTokensByNode = new Map();
|
|
56
|
+
for (const edge of edges) {
|
|
57
|
+
if (edge.status !== 'active')
|
|
58
|
+
continue;
|
|
59
|
+
let set = relationTokensByNode.get(edge.fromNodeId);
|
|
60
|
+
if (!set) {
|
|
61
|
+
set = new Set();
|
|
62
|
+
relationTokensByNode.set(edge.fromNodeId, set);
|
|
63
|
+
}
|
|
64
|
+
for (const t of tokenize(edge.relation))
|
|
65
|
+
set.add(t);
|
|
66
|
+
let set2 = relationTokensByNode.get(edge.toNodeId);
|
|
67
|
+
if (!set2) {
|
|
68
|
+
set2 = new Set();
|
|
69
|
+
relationTokensByNode.set(edge.toNodeId, set2);
|
|
70
|
+
}
|
|
71
|
+
for (const t of tokenize(edge.relation))
|
|
72
|
+
set2.add(t);
|
|
73
|
+
}
|
|
74
|
+
const results = [];
|
|
75
|
+
for (const node of nodes) {
|
|
76
|
+
if (node.status !== 'active' && node.status !== 'disputed')
|
|
77
|
+
continue;
|
|
78
|
+
const fieldTokens = {
|
|
79
|
+
name: new Set(tokenize(node.name)),
|
|
80
|
+
aliases: new Set(node.aliases.flatMap((a) => tokenize(a))),
|
|
81
|
+
tags: new Set((node.tags ?? []).flatMap((t) => tokenize(t))),
|
|
82
|
+
currentState: new Set(tokenize(node.currentState)),
|
|
83
|
+
facts: new Set(node.facts
|
|
84
|
+
.filter((f) => f.status === 'active')
|
|
85
|
+
.flatMap((f) => tokenize(`${f.key} ${factValueText(f.value)}`))),
|
|
86
|
+
relations: relationTokensByNode.get(node.id) ?? new Set(),
|
|
87
|
+
type: new Set(tokenize(node.type)),
|
|
88
|
+
};
|
|
89
|
+
let score = 0;
|
|
90
|
+
const matchedFields = [];
|
|
91
|
+
const matchedTokens = new Set();
|
|
92
|
+
for (const field of FIELD_ORDER) {
|
|
93
|
+
let hits = 0;
|
|
94
|
+
for (const t of tokens) {
|
|
95
|
+
if (fieldTokens[field].has(t)) {
|
|
96
|
+
hits++;
|
|
97
|
+
matchedTokens.add(t);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
if (hits > 0) {
|
|
101
|
+
score += GRAPH_FIELD_WEIGHTS[field] * hits;
|
|
102
|
+
matchedFields.push(field);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
// 无命中跳过;仅关系词命中 = 邻接噪声,过滤丢弃(type-only 保留)
|
|
106
|
+
if (matchedFields.length === 0)
|
|
107
|
+
continue;
|
|
108
|
+
if (matchedFields.length === 1 && matchedFields[0] === 'relations')
|
|
109
|
+
continue;
|
|
110
|
+
const matchReason = `命中${matchedFields.map((f) => FIELD_LABELS[f]).join('/')}「${[...matchedTokens].slice(0, 4).join('、')}」`;
|
|
111
|
+
results.push({ node, score, matchedFields, matchReason });
|
|
112
|
+
}
|
|
113
|
+
results.sort((a, b) => b.score - a.score || (a.node.updatedAt < b.node.updatedAt ? 1 : a.node.updatedAt > b.node.updatedAt ? -1 : a.node.id < b.node.id ? -1 : 1));
|
|
114
|
+
return results.slice(0, cappedLimit);
|
|
115
|
+
}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 知识图谱域模型与投影结果类型。
|
|
3
|
+
*
|
|
4
|
+
* 定位:图谱是 L1 记录的可重建投影,不是独立知识库——记录"发生过什么",
|
|
5
|
+
* 图谱回答"人物/项目/组织/工具/地点现在是什么状态"。每个节点/fact/边都必须
|
|
6
|
+
* 携带 sourceRecordIds 回链到 L1 记录,任何图谱结论都能沿
|
|
7
|
+
* `节点/fact → sourceRecordIds → L1 记录 → JSONL 事实源` 追溯。
|
|
8
|
+
*
|
|
9
|
+
* 图谱三表(graph_nodes/graph_edges/graph_projection_jobs)可随时 drop 重造
|
|
10
|
+
* (JSONL → L1 → 投影单向重建),因此这里的类型不承担磁盘契约职责——字段演进
|
|
11
|
+
* 只要求投影作业重跑,不像 l1_records 的 DDL 那样逐字节冻结。
|
|
12
|
+
*/
|
|
13
|
+
import type { MemoryFamily, MemoryRecord } from '../types.js';
|
|
14
|
+
export type GraphNodeType = 'person' | 'project' | 'organization' | 'tool' | 'place';
|
|
15
|
+
export type GraphRecordStatus = 'active' | 'superseded' | 'disputed' | 'archived';
|
|
16
|
+
export interface GraphFact {
|
|
17
|
+
id: string;
|
|
18
|
+
key: string;
|
|
19
|
+
value: string | string[];
|
|
20
|
+
status: GraphRecordStatus;
|
|
21
|
+
validFrom?: string;
|
|
22
|
+
validTo?: string;
|
|
23
|
+
confidence: number;
|
|
24
|
+
sourceRecordIds: string[];
|
|
25
|
+
createdAt: string;
|
|
26
|
+
updatedAt: string;
|
|
27
|
+
}
|
|
28
|
+
export interface GraphNode {
|
|
29
|
+
id: string;
|
|
30
|
+
name: string;
|
|
31
|
+
type: GraphNodeType;
|
|
32
|
+
aliases: string[];
|
|
33
|
+
/** 语义角色标签(检索发现与展示用),只做附加元数据,不替代稳定 type。 */
|
|
34
|
+
tags?: string[];
|
|
35
|
+
/** 由全部 active facts 重建的状态串(key: value 逐行)。 */
|
|
36
|
+
currentState: string;
|
|
37
|
+
facts: GraphFact[];
|
|
38
|
+
status: GraphRecordStatus;
|
|
39
|
+
confidence: number;
|
|
40
|
+
sourceRecordIds: string[];
|
|
41
|
+
/** 来源记录的族并集(族过滤的唯一依据):纯档会话只见本族衍生节点,
|
|
42
|
+
* auto 会话不过滤。与记录侧 family 三级兜底同源(family ?? type 前缀推导)。 */
|
|
43
|
+
families: MemoryFamily[];
|
|
44
|
+
createdAt: string;
|
|
45
|
+
updatedAt: string;
|
|
46
|
+
}
|
|
47
|
+
export interface GraphEdge {
|
|
48
|
+
id: string;
|
|
49
|
+
fromNodeId: string;
|
|
50
|
+
toNodeId: string;
|
|
51
|
+
relation: string;
|
|
52
|
+
status: GraphRecordStatus;
|
|
53
|
+
validFrom?: string;
|
|
54
|
+
validTo?: string;
|
|
55
|
+
confidence: number;
|
|
56
|
+
sourceRecordIds: string[];
|
|
57
|
+
createdAt: string;
|
|
58
|
+
updatedAt: string;
|
|
59
|
+
}
|
|
60
|
+
/** 模型返回的节点提案(ref 是提案内临时引用,边靠它连线)。 */
|
|
61
|
+
export interface GraphNodeProjection {
|
|
62
|
+
ref: string;
|
|
63
|
+
name: string;
|
|
64
|
+
type: GraphNodeType;
|
|
65
|
+
aliases?: string[];
|
|
66
|
+
tags?: string[];
|
|
67
|
+
/** 单行状态描述(入库为 key='状态' 的 fact)。 */
|
|
68
|
+
state?: string;
|
|
69
|
+
facts?: Array<{
|
|
70
|
+
key: string;
|
|
71
|
+
value: string | string[];
|
|
72
|
+
sourceRecordIds?: string[];
|
|
73
|
+
}>;
|
|
74
|
+
validFrom?: string;
|
|
75
|
+
validTo?: string;
|
|
76
|
+
status?: GraphRecordStatus;
|
|
77
|
+
confidence?: number;
|
|
78
|
+
sourceRecordIds: string[];
|
|
79
|
+
}
|
|
80
|
+
export interface GraphEdgeProjection {
|
|
81
|
+
fromRef: string;
|
|
82
|
+
toRef: string;
|
|
83
|
+
relation: string;
|
|
84
|
+
validFrom?: string;
|
|
85
|
+
validTo?: string;
|
|
86
|
+
status?: GraphRecordStatus;
|
|
87
|
+
confidence?: number;
|
|
88
|
+
sourceRecordIds: string[];
|
|
89
|
+
}
|
|
90
|
+
export interface GraphProjectionResult {
|
|
91
|
+
reason: string;
|
|
92
|
+
nodes: GraphNodeProjection[];
|
|
93
|
+
edges: GraphEdgeProjection[];
|
|
94
|
+
}
|
|
95
|
+
/** 投影作业(持久化状态机 pending → running → completed/failed/dead)。 */
|
|
96
|
+
export interface GraphProjectionJob {
|
|
97
|
+
id: string;
|
|
98
|
+
/** 本批唯一事实来源(L1 record id);claim 后按 id 现查记录。 */
|
|
99
|
+
sourceRecordIds: string[];
|
|
100
|
+
/** 排序优先级:数值越大越先投(新蒸馏记录 > 存量补投影,见 GRAPH_PRIORITY_NEW)。 */
|
|
101
|
+
priority: number;
|
|
102
|
+
projectorVersion: number;
|
|
103
|
+
status: 'pending' | 'running' | 'completed' | 'failed' | 'dead';
|
|
104
|
+
attempts: number;
|
|
105
|
+
/** 指数退避的最早重试时刻(epoch ms;null = 立即可取)。 */
|
|
106
|
+
nextAttemptAt: number | null;
|
|
107
|
+
error?: string;
|
|
108
|
+
createdAt: string;
|
|
109
|
+
updatedAt: string;
|
|
110
|
+
}
|
|
111
|
+
/** 图谱检索单条命中(score 只用于排序,不是事实置信度)。 */
|
|
112
|
+
export interface GraphNodeSearchResult {
|
|
113
|
+
node: GraphNode;
|
|
114
|
+
score: number;
|
|
115
|
+
/** 命中发生在哪些字段(按权重降序;空数组不会出现——无命中不返回)。 */
|
|
116
|
+
matchedFields: Array<'name' | 'aliases' | 'tags' | 'currentState' | 'facts' | 'relations' | 'type'>;
|
|
117
|
+
/** 中文可解释匹配说明(面板/工具展示用)。 */
|
|
118
|
+
matchReason: string;
|
|
119
|
+
}
|
|
120
|
+
/** 投影上下文(claim 时装配,LLM 调用与 complete 共用同一形状)。 */
|
|
121
|
+
export interface GraphProjectionContext {
|
|
122
|
+
jobId: string;
|
|
123
|
+
projectorVersion: number;
|
|
124
|
+
/** 本批唯一事实来源(L1 记录)。 */
|
|
125
|
+
records: MemoryRecord[];
|
|
126
|
+
existingNodes: GraphNode[];
|
|
127
|
+
existingEdges: GraphEdge[];
|
|
128
|
+
}
|
|
129
|
+
/** 投影版本号:提示词/校验语义升级时 +1,旧版本 job 全部作废重投影。 */
|
|
130
|
+
export declare const GRAPH_PROJECTOR_VERSION = 1;
|
|
131
|
+
/** 单 job 认领的记录数上限(批内上下文规模可控)。 */
|
|
132
|
+
export declare const GRAPH_JOB_BATCH = 8;
|
|
133
|
+
/** 新蒸馏记录的投影优先级(存量补投影 ≤9999,永不倒挂)。 */
|
|
134
|
+
export declare const GRAPH_PRIORITY_NEW = 10000;
|
|
135
|
+
/** 存量补投影的优先级(启动/周期补齐;恒低于新蒸馏)。 */
|
|
136
|
+
export declare const GRAPH_PRIORITY_BACKFILL = 100;
|
|
137
|
+
/** 单 job 最大尝试次数:claim 时 +1,达到即转 dead(不再重试)。 */
|
|
138
|
+
export declare const GRAPH_JOB_MAX_ATTEMPTS = 3;
|
|
139
|
+
/** 失败指数退避基数:第 n 次失败后 nextAttemptAt = now + base × 2^(n-1)。 */
|
|
140
|
+
export declare const GRAPH_JOB_BACKOFF_BASE_MS = 60000;
|
|
141
|
+
/** 单次检索返回上限的硬钳制(工具与 RPC 两侧共用同一钳制值)。 */
|
|
142
|
+
export declare const GRAPH_SEARCH_LIMIT_MAX = 20;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/** 投影版本号:提示词/校验语义升级时 +1,旧版本 job 全部作废重投影。 */
|
|
2
|
+
export const GRAPH_PROJECTOR_VERSION = 1;
|
|
3
|
+
/** 单 job 认领的记录数上限(批内上下文规模可控)。 */
|
|
4
|
+
export const GRAPH_JOB_BATCH = 8;
|
|
5
|
+
/** 新蒸馏记录的投影优先级(存量补投影 ≤9999,永不倒挂)。 */
|
|
6
|
+
export const GRAPH_PRIORITY_NEW = 10000;
|
|
7
|
+
/** 存量补投影的优先级(启动/周期补齐;恒低于新蒸馏)。 */
|
|
8
|
+
export const GRAPH_PRIORITY_BACKFILL = 100;
|
|
9
|
+
/** 单 job 最大尝试次数:claim 时 +1,达到即转 dead(不再重试)。 */
|
|
10
|
+
export const GRAPH_JOB_MAX_ATTEMPTS = 3;
|
|
11
|
+
/** 失败指数退避基数:第 n 次失败后 nextAttemptAt = now + base × 2^(n-1)。 */
|
|
12
|
+
export const GRAPH_JOB_BACKOFF_BASE_MS = 60_000;
|
|
13
|
+
/** 单次检索返回上限的硬钳制(工具与 RPC 两侧共用同一钳制值)。 */
|
|
14
|
+
export const GRAPH_SEARCH_LIMIT_MAX = 20;
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
2
|
+
import type { SessionEvent } from '@deepseek-ai/dsh-session';
|
|
3
|
+
import type { MemoryConfig } from '../config.js';
|
|
4
|
+
import type { MemoryRunner } from '../pipeline/runner.js';
|
|
5
|
+
import type { SessionModeStore } from '../store/session-modes.js';
|
|
6
|
+
import type { L0Store } from '../store/l0.js';
|
|
7
|
+
import type { LiveSettingsHandle } from '../settings.js';
|
|
8
|
+
import type { MemoryLogger } from '../types.js';
|
|
9
|
+
export declare function isCaptureRelevant(type: string): boolean;
|
|
10
|
+
/**
|
|
11
|
+
* 按会话缓冲轮次事件。turn 被消费后剩余前缀为空即删除 Map 条目——
|
|
12
|
+
* 条目随会话数累积是慢泄漏(每会话残留一个数组引用,宿主长跑不释放)。
|
|
13
|
+
*/
|
|
14
|
+
export declare class CaptureBuffers {
|
|
15
|
+
private readonly map;
|
|
16
|
+
/** 活跃缓冲条目数(诊断/冒烟用)。 */
|
|
17
|
+
get size(): number;
|
|
18
|
+
push(sid: string, event: SessionEvent): void;
|
|
19
|
+
/** 取出该 turn 的全部事件(不含 turn/start 自身);无匹配 start 时返回整个缓冲。 */
|
|
20
|
+
takeTurn(sid: string, turn: number): SessionEvent[];
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* 注册 L0 捕获。返回 L0 串行链的冲刷函数(dispose 序在关库前 await,
|
|
24
|
+
* 排队中的 turn 消息先落盘);capture 关闭时返回 undefined。
|
|
25
|
+
*/
|
|
26
|
+
export declare function registerCapture(ctx: Context, cfg: MemoryConfig, runner: MemoryRunner, l0: L0Store, logger: MemoryLogger, live: LiveSettingsHandle, modes: SessionModeStore): (() => Promise<void>) | undefined;
|
|
27
|
+
/**
|
|
28
|
+
* 缓冲上限裁剪(防御性;RELEVANT_TYPES 过滤后基本不可达)。
|
|
29
|
+
* 铁律:进行中轮次(turn/start 之后、turn/end 之前)的事件绝不裁——
|
|
30
|
+
* 只裁最早一个未闭合 turn/start 之前的已完成前缀。
|
|
31
|
+
*/
|
|
32
|
+
export declare function trimBuffer(buf: SessionEvent[]): void;
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* L0 捕获 Hook:订阅 session/event,按轮次缓冲 user/assistant 消息,
|
|
3
|
+
* turn/end 时清洗并交给 Runner 落盘 + 触发蒸馏。
|
|
4
|
+
*
|
|
5
|
+
* 冷启动保护:插件激活时间之前的事件不捕获(防止恢复会话时倾倒全部历史)。
|
|
6
|
+
*/
|
|
7
|
+
import { randomBytes } from 'node:crypto';
|
|
8
|
+
import { blocksToText } from '../util/text.js';
|
|
9
|
+
import { sanitizeText, shouldCaptureL0, stripCodeBlocks } from '../util/sanitize.js';
|
|
10
|
+
/**
|
|
11
|
+
* 需要进缓冲的事件类型。流式 chunk(text-delta/reasoning 等)一秒钟可达数百条,
|
|
12
|
+
* 缓冲它们会把 MAX_BUFFER 撑爆、把轮次头部(turn/start + user 消息)裁掉——
|
|
13
|
+
* 2026-08-16 真实事故:长回复轮次丢失 user 消息。
|
|
14
|
+
*/
|
|
15
|
+
const RELEVANT_TYPES = new Set(['user/message', 'assistant/message', 'turn/start', 'turn/end']);
|
|
16
|
+
export function isCaptureRelevant(type) {
|
|
17
|
+
return RELEVANT_TYPES.has(type);
|
|
18
|
+
}
|
|
19
|
+
const MAX_BUFFER = 500;
|
|
20
|
+
/**
|
|
21
|
+
* 按会话缓冲轮次事件。turn 被消费后剩余前缀为空即删除 Map 条目——
|
|
22
|
+
* 条目随会话数累积是慢泄漏(每会话残留一个数组引用,宿主长跑不释放)。
|
|
23
|
+
*/
|
|
24
|
+
export class CaptureBuffers {
|
|
25
|
+
map = new Map();
|
|
26
|
+
/** 活跃缓冲条目数(诊断/冒烟用)。 */
|
|
27
|
+
get size() {
|
|
28
|
+
return this.map.size;
|
|
29
|
+
}
|
|
30
|
+
push(sid, event) {
|
|
31
|
+
let buf = this.map.get(sid);
|
|
32
|
+
if (!buf) {
|
|
33
|
+
buf = [];
|
|
34
|
+
this.map.set(sid, buf);
|
|
35
|
+
}
|
|
36
|
+
buf.push(event);
|
|
37
|
+
trimBuffer(buf);
|
|
38
|
+
}
|
|
39
|
+
/** 取出该 turn 的全部事件(不含 turn/start 自身);无匹配 start 时返回整个缓冲。 */
|
|
40
|
+
takeTurn(sid, turn) {
|
|
41
|
+
const buf = this.map.get(sid);
|
|
42
|
+
if (!buf)
|
|
43
|
+
return [];
|
|
44
|
+
const startIdx = findTurnStart(buf, turn);
|
|
45
|
+
const turnEvents = startIdx === -1 ? buf : buf.slice(startIdx + 1);
|
|
46
|
+
const rest = startIdx === -1 ? [] : buf.slice(0, startIdx);
|
|
47
|
+
if (rest.length === 0)
|
|
48
|
+
this.map.delete(sid);
|
|
49
|
+
else
|
|
50
|
+
this.map.set(sid, rest);
|
|
51
|
+
return turnEvents;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* 注册 L0 捕获。返回 L0 串行链的冲刷函数(dispose 序在关库前 await,
|
|
56
|
+
* 排队中的 turn 消息先落盘);capture 关闭时返回 undefined。
|
|
57
|
+
*/
|
|
58
|
+
export function registerCapture(ctx, cfg, runner, l0, logger, live, modes) {
|
|
59
|
+
if (!cfg.capture.enabled)
|
|
60
|
+
return;
|
|
61
|
+
const startFloor = Date.now();
|
|
62
|
+
const buffers = new CaptureBuffers();
|
|
63
|
+
// L0 即时落盘链:turn/end 立刻写,不被蒸馏队列(慢 LLM 调用)阻塞;
|
|
64
|
+
// 串行化保证同轮次顺序与单次写入(进程退出时排队中的 L0 不再依赖蒸馏完成)
|
|
65
|
+
let l0Queue = Promise.resolve();
|
|
66
|
+
ctx.on('session/event', (session, event) => {
|
|
67
|
+
try {
|
|
68
|
+
const s = live.get();
|
|
69
|
+
if (!s.enabled || !s.capture)
|
|
70
|
+
return;
|
|
71
|
+
const sid = String(session.id ?? session);
|
|
72
|
+
// off 档:本会话对记忆系统完全隐身(不缓冲、不写 L0、不蒸馏)
|
|
73
|
+
if (modes.get(sid) === 'off')
|
|
74
|
+
return;
|
|
75
|
+
if (!isCaptureRelevant(event.type))
|
|
76
|
+
return;
|
|
77
|
+
if (event.time < startFloor) {
|
|
78
|
+
// 冷启动保护拦截的 user 消息记 info(罕见,但正是"整轮只剩 assistant"现象的线索)
|
|
79
|
+
if (event.type === 'user/message') {
|
|
80
|
+
logger.info(`[memory] L0 跳过早于插件启动的 user 消息(冷启动保护,早 ${startFloor - event.time}ms)`);
|
|
81
|
+
}
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
buffers.push(sid, event);
|
|
85
|
+
if (event.type === 'turn/end') {
|
|
86
|
+
const turn = event.data.turn;
|
|
87
|
+
const turnEvents = buffers.takeTurn(sid, turn);
|
|
88
|
+
const messages = turnEventsToMessages(turnEvents, cfg, logger);
|
|
89
|
+
if (messages.length > 0) {
|
|
90
|
+
const roles = messages.reduce((acc, m) => {
|
|
91
|
+
acc[m.role] = (acc[m.role] ?? 0) + 1;
|
|
92
|
+
return acc;
|
|
93
|
+
}, {});
|
|
94
|
+
// 轮末档位生效(中途切档:本轮按 turn/end 时的档位蒸馏)
|
|
95
|
+
const mode = modes.get(sid);
|
|
96
|
+
if (mode === 'off') {
|
|
97
|
+
logger.info(`[memory] turn=${turn} 结束时档位为关闭,本轮不落盘不蒸馏(session=${sid})`);
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
logger.info(`[memory] L0 捕获 turn=${turn} ${messages.length} 条(${Object.entries(roles)
|
|
101
|
+
.map(([k, v]) => `${k}=${v}`)
|
|
102
|
+
.join('/')} ,session=${sid},mode=${mode})`);
|
|
103
|
+
// L0 立刻落盘(不等蒸馏)
|
|
104
|
+
const n = messages.length;
|
|
105
|
+
l0Queue = l0Queue
|
|
106
|
+
.then(() => l0.append(sid, messages))
|
|
107
|
+
.then(() => logger.info(`[memory] L0 落盘 ${n} 条`))
|
|
108
|
+
.catch((err) => logger.warn(`[memory] L0 落盘失败: ${err instanceof Error ? err.message : String(err)}`));
|
|
109
|
+
runner.enqueue(sid, messages, mode);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
logger.warn(`[memory] session/event 处理失败: ${err instanceof Error ? err.message : String(err)}`);
|
|
115
|
+
}
|
|
116
|
+
});
|
|
117
|
+
// 冲刷 = 等待串行链排空(链上每环自带 catch,永不 reject)。
|
|
118
|
+
// 注:极小概率在 await 期间又入队的新消息会落到链尾——其 JSONL 事实源照写、
|
|
119
|
+
// DB 写入由 upsert 内部兜底(关库后 warn),下次重建自愈。
|
|
120
|
+
return () => l0Queue;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* 缓冲上限裁剪(防御性;RELEVANT_TYPES 过滤后基本不可达)。
|
|
124
|
+
* 铁律:进行中轮次(turn/start 之后、turn/end 之前)的事件绝不裁——
|
|
125
|
+
* 只裁最早一个未闭合 turn/start 之前的已完成前缀。
|
|
126
|
+
*/
|
|
127
|
+
export function trimBuffer(buf) {
|
|
128
|
+
if (buf.length <= MAX_BUFFER)
|
|
129
|
+
return;
|
|
130
|
+
let openStart = -1;
|
|
131
|
+
let closed = true;
|
|
132
|
+
for (let i = 0; i < buf.length; i++) {
|
|
133
|
+
const t = buf[i].type;
|
|
134
|
+
if (t === 'turn/start' && closed) {
|
|
135
|
+
openStart = i;
|
|
136
|
+
closed = false;
|
|
137
|
+
}
|
|
138
|
+
else if (t === 'turn/end') {
|
|
139
|
+
closed = true;
|
|
140
|
+
openStart = -1;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
const floorIdx = openStart === -1 ? Math.max(0, buf.length - MAX_BUFFER) : openStart;
|
|
144
|
+
if (floorIdx > 0)
|
|
145
|
+
buf.splice(0, floorIdx);
|
|
146
|
+
}
|
|
147
|
+
/** 找到与 turn/end 同 turn 的最后一个 turn/start 的索引(含自身)。 */
|
|
148
|
+
function findTurnStart(buf, turn) {
|
|
149
|
+
for (let i = buf.length - 1; i >= 0; i--) {
|
|
150
|
+
const e = buf[i];
|
|
151
|
+
if (e.type === 'turn/start' && e.data.turn === turn)
|
|
152
|
+
return i;
|
|
153
|
+
}
|
|
154
|
+
return -1;
|
|
155
|
+
}
|
|
156
|
+
/** 把轮次事件转成 L0 消息(仅真实 user 消息 + assistant 消息,清洗过滤)。 */
|
|
157
|
+
function turnEventsToMessages(events, cfg, logger) {
|
|
158
|
+
const out = [];
|
|
159
|
+
for (const event of events) {
|
|
160
|
+
if (event.type === 'user/message') {
|
|
161
|
+
const msg = event.data;
|
|
162
|
+
// 只捕获真实用户输入(source.kind === 'user'),跳过插件注入上下文
|
|
163
|
+
if (msg.source?.kind !== 'user') {
|
|
164
|
+
logger.info(`[memory] L0 跳过非用户来源消息(source.kind=${msg.source?.kind ?? 'none'})`);
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
const content = sanitizeText(blocksToText(msg.content));
|
|
168
|
+
if (shouldCaptureL0(content)) {
|
|
169
|
+
out.push(makeMessage('user', content, event.time, cfg.capture.maxMessageChars));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
else if (event.type === 'assistant/message') {
|
|
173
|
+
const data = event.data;
|
|
174
|
+
let content = sanitizeText(blocksToText(data.message?.content));
|
|
175
|
+
if (cfg.capture.stripCodeBlocks)
|
|
176
|
+
content = stripCodeBlocks(content);
|
|
177
|
+
if (shouldCaptureL0(content)) {
|
|
178
|
+
out.push(makeMessage('assistant', content, event.time, cfg.capture.maxMessageChars));
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
if (out.length > 0) {
|
|
183
|
+
logger.debug?.(`[memory] 轮次消息 ${events.length} 事件 → ${out.length} 条(清洗过滤后)`);
|
|
184
|
+
}
|
|
185
|
+
return out;
|
|
186
|
+
}
|
|
187
|
+
function makeMessage(role, content, timestamp, maxChars) {
|
|
188
|
+
return {
|
|
189
|
+
id: `msg_${Date.now()}_${randomBytes(3).toString('hex')}`,
|
|
190
|
+
role,
|
|
191
|
+
content: content.slice(0, maxChars),
|
|
192
|
+
timestamp,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 召回 Hook(消息侧注入版,ADR-0001):
|
|
3
|
+
* 1. agent/pre-step(waterfall,prepend 注册):有新的用户来源消息到达的步骤
|
|
4
|
+
* (轮首 claim 或 steering 插话)→ 检索 L1 → 以合成消息形式注入到用户消息之前。
|
|
5
|
+
* 注入消息携带插件来源(form: 'recall'),对用户可见、随会话历史持久累积——
|
|
6
|
+
* 这就是"记忆生效"的显式提示(dsh-time-context 同款官方范式)。
|
|
7
|
+
* 纯工具步透传;召回超时(recall.timeoutMs 总预算)跳过本轮,绝不阻塞对话。
|
|
8
|
+
* 2. agent/created:在 agent 作用域注册动态上下文 provider(L3 画像 + L2 场景导航 +
|
|
9
|
+
* 工具指南——系统提示只保留稳定内容;指南三条件门控:工具开启 && 有内容)。
|
|
10
|
+
*
|
|
11
|
+
* 注入内容使用 <relevant-memories> 标签包裹(模型侧语义边界 + 助手回显剥离锚点),
|
|
12
|
+
* 召回预算(单条/整轮)超限截断并引导模型用记忆工具查全文。
|
|
13
|
+
* L0 防污染零成本:capture 侧只收 source.kind === 'user' 的消息,注入消息天然被排除。
|
|
14
|
+
*
|
|
15
|
+
* 注意:PromptContext.text 是同步函数,画像/场景导航这类异步文件读取必须走内存缓存,
|
|
16
|
+
* 由定时刷新 + 管线更新后的主动失效来更新。
|
|
17
|
+
*/
|
|
18
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
19
|
+
import type { MemoryConfig } from '../config.js';
|
|
20
|
+
import type { LiveSettingsHandle } from '../settings.js';
|
|
21
|
+
import type { L1Store } from '../store/l1.js';
|
|
22
|
+
import type { PersonaStore } from '../store/persona.js';
|
|
23
|
+
import type { SceneStore } from '../store/scenes.js';
|
|
24
|
+
import type { SessionModeStore } from '../store/session-modes.js';
|
|
25
|
+
import type { MemoryLogger } from '../types.js';
|
|
26
|
+
import { type OccupancyLedger } from '../util/context-occupancy.js';
|
|
27
|
+
/**
|
|
28
|
+
* 从会话消息构建召回查询(纯函数):末尾 N 条 + 总长截断,空输入返回空串。
|
|
29
|
+
* 全史拼接会让 MATCH 表达式随会话长度线性膨胀(整会话累计二次方成本)。
|
|
30
|
+
*/
|
|
31
|
+
export declare function buildRecallQuery(messages: Array<{
|
|
32
|
+
content: unknown;
|
|
33
|
+
}>, tailMessages?: number, maxChars?: number): string;
|
|
34
|
+
/** 单会话召回统计(悬浮卡信息区数据源;每轮 O(1) 记账,agent/disposed 清理)。
|
|
35
|
+
* 口径声明:这是"注入统计"而非 bench 的离线 recall@k——运行时没有 ground truth,
|
|
36
|
+
* 命中率 = hitTurns / injectedTurns。去重语义:全量压制轮计入 hitTurns
|
|
37
|
+
* (相关记忆已在模型上下文里,本质是命中而非未命中),injectedTurns 仍计全部
|
|
38
|
+
* 发生过检索的轮次(保住分母语义与悬浮卡口径连续性)。 */
|
|
39
|
+
import type { RecallSessionStats } from '../contract.js';
|
|
40
|
+
export type { RecallSessionStats } from '../contract.js';
|
|
41
|
+
/** 新建零值统计(首次出现的会话)。 */
|
|
42
|
+
export declare function emptyRecallStats(now?: number): RecallSessionStats;
|
|
43
|
+
export interface RecallHooks {
|
|
44
|
+
/** 管线更新后调用:画像/场景缓存立即失效并异步刷新。 */
|
|
45
|
+
invalidateProfile(): void;
|
|
46
|
+
/** 会话召回统计只读视图(未发生过检索的会话返回 undefined)。 */
|
|
47
|
+
stats(sessionId: string): RecallSessionStats | undefined;
|
|
48
|
+
/** 记忆占用账本只读出口:内存优先,miss 时从流水复生(重启后历史会话);从未注入返回 null。 */
|
|
49
|
+
occupancy(sessionId: string): OccupancyLedger | null;
|
|
50
|
+
/**
|
|
51
|
+
* 稳定区份额估算(旧会话回填):按当前画像/导航/指南组词折算 token,
|
|
52
|
+
* 纯读不记账——旧会话系统提示里"现在大约坐着多少稳定区"的最佳可得估计。
|
|
53
|
+
*/
|
|
54
|
+
estimateProfileTokens(agentId: string): number;
|
|
55
|
+
/** 召回份额回填:live 会话 surface 现扫本插件注入,miss 时读盘上日志兜底;
|
|
56
|
+
* 均不可得返回 null。 */
|
|
57
|
+
estimateRecallTokens(sessionId: string): Promise<number | null>;
|
|
58
|
+
}
|
|
59
|
+
export declare function registerRecall(ctx: Context, cfg: MemoryConfig, stores: {
|
|
60
|
+
l1: L1Store;
|
|
61
|
+
scenes: Record<'chat' | 'work', SceneStore>;
|
|
62
|
+
persona: Record<'chat' | 'work', PersonaStore>;
|
|
63
|
+
}, logger: MemoryLogger, live: LiveSettingsHandle, modes: SessionModeStore, dataDir: string): RecallHooks;
|