dsh-layered-memory 0.8.12 → 0.9.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/README.en.md +63 -41
- package/README.md +44 -28
- package/assets/changelog/0.9.0/01-/350/256/260/345/277/206/350/212/257/347/211/207/347/272/247/350/201/224/350/217/234/345/215/225.png +0 -0
- package/assets/changelog/0.9.0/02-/345/267/245/344/275/234/345/217/260/346/200/273/350/247/210.png +0 -0
- package/assets/changelog/0.9.0/03-/350/256/260/345/277/206/345/272/223.png +0 -0
- package/assets/changelog/0.9.0/04-/346/264/236/345/257/237/346/264/273/345/212/250.png +0 -0
- package/assets/img/MemoryChip.png +0 -0
- package/assets/img/ui-dark.png +0 -0
- package/assets/img/ui-light.png +0 -0
- package/dist/client.js +2652 -2001
- package/dist/contract.d.ts +117 -1
- package/dist/hooks/recall.d.ts +2 -0
- package/dist/hooks/recall.js +2 -0
- package/dist/index.js +2 -0
- package/dist/stats.d.ts +2 -0
- package/dist/stats.js +41 -0
- package/dist/store/l1.d.ts +7 -1
- package/dist/store/l1.js +5 -1
- package/dist/store/persona.d.ts +2 -0
- package/dist/store/persona.js +9 -0
- package/dist/store/scenes.d.ts +3 -0
- package/dist/store/scenes.js +3 -2
- package/dist/store/session-modes.d.ts +14 -1
- package/dist/store/session-modes.js +37 -3
- package/dist/store/sqlite.d.ts +8 -1
- package/dist/store/sqlite.js +24 -4
- package/dist/workspace-aggregates.d.ts +44 -0
- package/dist/workspace-aggregates.js +262 -0
- package/package.json +1 -1
- package/assets/img/Modes.png +0 -0
- package/assets/img/ui-dark.jpg +0 -0
- package/assets/img/ui-light.jpg +0 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { snapshotDistillUsage } from './llm-usage.js';
|
|
2
|
+
import { META_END } from './store/scenes.js';
|
|
3
|
+
/** 检索路径单次上限(与 list-records 同值:检索缝的硬上限,触达即 truncated)。 */
|
|
4
|
+
const SEARCH_CAP = 200;
|
|
5
|
+
/** 单源取数窗口上限:活动流是「最近的资产」视角,深翻页封顶 500(防深 offset
|
|
6
|
+
* 探测窗放大成全表扫描);超出后 hasMore 自然为假,翻页止步。 */
|
|
7
|
+
const WINDOW_CAP = 500;
|
|
8
|
+
/** L1 标题截断长度(内容首行;完整内容在 content 字段)。 */
|
|
9
|
+
const TITLE_MAX = 60;
|
|
10
|
+
const FAMILIES = ['chat', 'work'];
|
|
11
|
+
function encodeCursor(offset) {
|
|
12
|
+
return Buffer.from(JSON.stringify({ o: offset }), 'utf8').toString('base64url');
|
|
13
|
+
}
|
|
14
|
+
export function decodeCursor(cursor) {
|
|
15
|
+
if (!cursor)
|
|
16
|
+
return 0;
|
|
17
|
+
try {
|
|
18
|
+
const v = JSON.parse(Buffer.from(cursor, 'base64url').toString('utf8'));
|
|
19
|
+
return typeof v.o === 'number' && Number.isInteger(v.o) && v.o >= 0 && v.o <= 1_000_000 ? v.o : 0;
|
|
20
|
+
}
|
|
21
|
+
catch {
|
|
22
|
+
return 0;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
function l1Title(content) {
|
|
26
|
+
const firstLine = content.split('\n', 1)[0] ?? '';
|
|
27
|
+
const t = firstLine.trim();
|
|
28
|
+
return t.length > TITLE_MAX ? t.slice(0, TITLE_MAX - 1) + '…' : t;
|
|
29
|
+
}
|
|
30
|
+
function l1ToItem(r) {
|
|
31
|
+
const family = r.family ?? 'chat';
|
|
32
|
+
return {
|
|
33
|
+
id: `l1:${r.id}`,
|
|
34
|
+
kind: 'l1',
|
|
35
|
+
verb: (r.version ?? 0) > 1 ? 'upd' : 'new',
|
|
36
|
+
family,
|
|
37
|
+
title: l1Title(r.content),
|
|
38
|
+
content: r.content,
|
|
39
|
+
createdAt: r.createdAt ? new Date(r.createdAt).toISOString() : null,
|
|
40
|
+
updatedAt: r.updatedAt ? new Date(r.updatedAt).toISOString() : null,
|
|
41
|
+
version: r.version ?? 0,
|
|
42
|
+
sourceSession: r.sessionId ?? null,
|
|
43
|
+
scene: r.scene_name || null,
|
|
44
|
+
l1Type: r.type || null,
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
/** 剥离 L2 场景文件头 META 块(-----META-START----- … -----META-END-----,标记与
|
|
48
|
+
* SceneStore 同源),活动流正文只留自然内容;无完整 META 块原样返回。 */
|
|
49
|
+
function stripSceneMeta(raw) {
|
|
50
|
+
const end = raw.indexOf(META_END);
|
|
51
|
+
if (end === -1)
|
|
52
|
+
return raw;
|
|
53
|
+
return raw.slice(end + META_END.length).replace(/^\s*\n/, '');
|
|
54
|
+
}
|
|
55
|
+
const ts = (iso) => (iso ? Date.parse(iso) || 0 : 0);
|
|
56
|
+
/**
|
|
57
|
+
* 混排资产活动流:L1(索引列表/检索缝)+ L2(场景摘要,页内补读正文)+
|
|
58
|
+
* L3(画像 + mtime),按 updatedAt 倒序统一排序后游标切片。
|
|
59
|
+
*/
|
|
60
|
+
export async function collectAssetActivity(stores, q) {
|
|
61
|
+
const pool = [];
|
|
62
|
+
// 每源取 offset+limit+1 条做下一页探测:任一源还有数据,池就比窗口长 1 条,
|
|
63
|
+
// nextCursor 才能为真(只取满窗口会让 pool.length ≤ offset+limit 恒成立——
|
|
64
|
+
// 纯 L1 数据源翻页结构性失效,审查 P0-1);窗口封顶 WINDOW_CAP 防深翻页放大
|
|
65
|
+
const window = Math.min(q.offset + q.limit + 1, WINDOW_CAP);
|
|
66
|
+
const sinceIso = q.sinceMs > 0 ? new Date(q.sinceMs).toISOString() : undefined;
|
|
67
|
+
const familyOpt = q.family || undefined;
|
|
68
|
+
let truncated = false;
|
|
69
|
+
if (q.kind === '' || q.kind === 'l1') {
|
|
70
|
+
let items;
|
|
71
|
+
if (q.query) {
|
|
72
|
+
// 检索唯一缝(与召回同源):ranked hits 再经 getByIds 索引点查补全
|
|
73
|
+
// 时间戳/版本(L1Hit 投影不含 updatedAt,混排排序需要),此后统一按时间排序。
|
|
74
|
+
// 已知取舍:检索按相关度截 200 后才做 since 后置过滤——窗口外的更新匹配可能
|
|
75
|
+
// 被更相关但更早的命中挤掉(小数据设置页场景接受;精确时间过滤走列表路径)
|
|
76
|
+
const hits = await stores.l1.search(q.query, Math.min(window, SEARCH_CAP), { family: familyOpt });
|
|
77
|
+
items = stores.l1.getByIds(hits.map((h) => h.id));
|
|
78
|
+
truncated = window > SEARCH_CAP;
|
|
79
|
+
}
|
|
80
|
+
else {
|
|
81
|
+
items = stores.l1.list({ family: familyOpt, since: sinceIso, limit: window, offset: 0 }).items;
|
|
82
|
+
}
|
|
83
|
+
for (const r of items) {
|
|
84
|
+
if (sinceIso && r.updatedAt && new Date(r.updatedAt).toISOString() < sinceIso)
|
|
85
|
+
continue;
|
|
86
|
+
pool.push(l1ToItem(r));
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
if (q.kind === '' || q.kind === 'l2') {
|
|
90
|
+
for (const family of FAMILIES) {
|
|
91
|
+
if (familyOpt && family !== familyOpt)
|
|
92
|
+
continue;
|
|
93
|
+
const scenes = await stores.scenes[family].list();
|
|
94
|
+
for (const s of scenes) {
|
|
95
|
+
const updatedMs = Date.parse(s.updated) || 0;
|
|
96
|
+
const createdMs = Date.parse(s.created) || 0;
|
|
97
|
+
if (q.sinceMs > 0 && (!updatedMs || updatedMs < q.sinceMs))
|
|
98
|
+
continue;
|
|
99
|
+
if (q.query) {
|
|
100
|
+
const needle = q.query.toLowerCase();
|
|
101
|
+
if (!s.path.toLowerCase().includes(needle) && !s.summary.toLowerCase().includes(needle))
|
|
102
|
+
continue;
|
|
103
|
+
}
|
|
104
|
+
pool.push({
|
|
105
|
+
id: `l2:${family}:${s.path}`,
|
|
106
|
+
kind: 'l2',
|
|
107
|
+
verb: updatedMs > createdMs ? 'upd' : 'new',
|
|
108
|
+
family,
|
|
109
|
+
title: s.path.replace(/\.md$/i, ''),
|
|
110
|
+
content: s.summary, // 页内切片后补读正文(见下)
|
|
111
|
+
createdAt: s.created && createdMs ? new Date(createdMs).toISOString() : null,
|
|
112
|
+
updatedAt: updatedMs ? new Date(updatedMs).toISOString() : null,
|
|
113
|
+
version: null,
|
|
114
|
+
sourceSession: null,
|
|
115
|
+
scene: null,
|
|
116
|
+
l1Type: null,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
if (q.kind === '' || q.kind === 'l3') {
|
|
122
|
+
for (const family of FAMILIES) {
|
|
123
|
+
if (familyOpt && family !== familyOpt)
|
|
124
|
+
continue;
|
|
125
|
+
const store = stores.persona[family];
|
|
126
|
+
const [content, mtimeMs] = await Promise.all([store.read(), store.mtime()]);
|
|
127
|
+
if (!content || !mtimeMs)
|
|
128
|
+
continue;
|
|
129
|
+
if (q.sinceMs > 0 && mtimeMs < q.sinceMs)
|
|
130
|
+
continue;
|
|
131
|
+
if (q.query && !content.toLowerCase().includes(q.query.toLowerCase()))
|
|
132
|
+
continue;
|
|
133
|
+
pool.push({
|
|
134
|
+
id: `l3:${family}`,
|
|
135
|
+
kind: 'l3',
|
|
136
|
+
verb: 'upd', // 画像只有演进没有首版语义(mtime 即最近一次蒸馏写入)
|
|
137
|
+
family,
|
|
138
|
+
title: `persona-${family}.md`,
|
|
139
|
+
content,
|
|
140
|
+
createdAt: null,
|
|
141
|
+
updatedAt: new Date(mtimeMs).toISOString(),
|
|
142
|
+
version: null,
|
|
143
|
+
sourceSession: null,
|
|
144
|
+
scene: null,
|
|
145
|
+
l1Type: null,
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
pool.sort((a, b) => ts(b.updatedAt) - ts(a.updatedAt) || (a.id < b.id ? -1 : 1));
|
|
150
|
+
const items = pool.slice(q.offset, q.offset + q.limit);
|
|
151
|
+
// L2 页内条目补读完整正文(只读窗口内 ≤limit 个文件,非全量)
|
|
152
|
+
await Promise.all(items.map(async (it) => {
|
|
153
|
+
if (it.kind !== 'l2')
|
|
154
|
+
return;
|
|
155
|
+
const raw = await stores.scenes[it.family].read(it.id.slice(`l2:${it.family}:`.length));
|
|
156
|
+
const body = raw ? stripSceneMeta(raw).trim() : '';
|
|
157
|
+
if (body)
|
|
158
|
+
it.content = body;
|
|
159
|
+
}));
|
|
160
|
+
return {
|
|
161
|
+
items,
|
|
162
|
+
nextCursor: pool.length > q.offset + q.limit ? encodeCursor(q.offset + q.limit) : null,
|
|
163
|
+
truncated,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/* ── 总览快照 ── */
|
|
167
|
+
/** 总览聚合:buildStats 既有字段 + 本周成本(token-cost 周窗口)+ 最近活动前 5 条。 */
|
|
168
|
+
export async function buildWorkspaceOverview(stores, base, week, retrieval) {
|
|
169
|
+
const recent = await collectAssetActivity(stores, {
|
|
170
|
+
query: '',
|
|
171
|
+
kind: '',
|
|
172
|
+
family: '',
|
|
173
|
+
sinceMs: 0,
|
|
174
|
+
limit: 5,
|
|
175
|
+
offset: 0,
|
|
176
|
+
});
|
|
177
|
+
return {
|
|
178
|
+
ok: true,
|
|
179
|
+
degraded: base.message !== 'running',
|
|
180
|
+
retrieval,
|
|
181
|
+
pendingExtract: base.pendingExtract,
|
|
182
|
+
l0Today: base.l0Today,
|
|
183
|
+
l1Count: base.l1Count,
|
|
184
|
+
sceneCount: base.sceneCount,
|
|
185
|
+
personaChars: base.personaChars,
|
|
186
|
+
empty: base.l1Count === 0 && base.sceneCount === 0 && base.personaChars === 0,
|
|
187
|
+
lastExtractAt: base.lastExtractAt,
|
|
188
|
+
lastL2At: base.lastL2At,
|
|
189
|
+
lastL3At: base.lastL3At,
|
|
190
|
+
week,
|
|
191
|
+
recent: recent.items,
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
/* ── 运行洞察 ── */
|
|
195
|
+
const DAY_MS = 24 * 3600_000;
|
|
196
|
+
/** UTC 日期键(与 L1 SQL substr(updated_time,1,10) 口径一致)。 */
|
|
197
|
+
function utcDay(ms) {
|
|
198
|
+
return new Date(ms).toISOString().slice(0, 10);
|
|
199
|
+
}
|
|
200
|
+
/** 洞察聚合:近 7 天逐日资产活动 + 蒸馏调用计数器 + 召回全量累计与停用分布。 */
|
|
201
|
+
export async function buildRuntimeInsights(stores, sessionInfo, modes, live) {
|
|
202
|
+
// 近 7 天(含当日)UTC 日桶;L1 走索引 GROUP BY,L2/L3 小数据内存归桶
|
|
203
|
+
const today = utcDay(Date.now());
|
|
204
|
+
const startMs = Date.parse(today + 'T00:00:00Z') - 6 * DAY_MS;
|
|
205
|
+
const buckets = new Map();
|
|
206
|
+
for (let i = 0; i < 7; i++) {
|
|
207
|
+
buckets.set(utcDay(startMs + i * DAY_MS), { l1: 0, l2: 0, l3: 0 });
|
|
208
|
+
}
|
|
209
|
+
const bump = (ms, k) => {
|
|
210
|
+
const b = buckets.get(utcDay(ms));
|
|
211
|
+
if (b)
|
|
212
|
+
b[k]++;
|
|
213
|
+
};
|
|
214
|
+
for (const row of stores.l1.countByDay(new Date(startMs).toISOString())) {
|
|
215
|
+
const b = buckets.get(row.day);
|
|
216
|
+
if (b)
|
|
217
|
+
b.l1 += row.n;
|
|
218
|
+
}
|
|
219
|
+
for (const family of FAMILIES) {
|
|
220
|
+
const scenes = await stores.scenes[family].list();
|
|
221
|
+
for (const s of scenes) {
|
|
222
|
+
const ms = Date.parse(s.updated) || 0;
|
|
223
|
+
if (ms >= startMs)
|
|
224
|
+
bump(ms, 'l2');
|
|
225
|
+
}
|
|
226
|
+
const mtimeMs = await stores.persona[family].mtime();
|
|
227
|
+
if (mtimeMs !== null && mtimeMs >= startMs)
|
|
228
|
+
bump(mtimeMs, 'l3');
|
|
229
|
+
}
|
|
230
|
+
const activityDays = Array.from(buckets.entries(), ([day, v]) => ({ day, ...v }));
|
|
231
|
+
// 蒸馏调用计数器(进程生命周期累计;llm-usage 口径)
|
|
232
|
+
const usage = snapshotDistillUsage();
|
|
233
|
+
const layerOrder = ['l1-extract', 'l1-dedup', 'l2', 'l3'];
|
|
234
|
+
const distill = layerOrder.map((layer) => ({
|
|
235
|
+
layer,
|
|
236
|
+
calls: usage.layers[layer]?.calls ?? 0,
|
|
237
|
+
failures: usage.layers[layer]?.failures ?? 0,
|
|
238
|
+
}));
|
|
239
|
+
// 召回全量累计(进程内注册表)+ 停用分布
|
|
240
|
+
const all = sessionInfo?.recallStatsAll?.() ?? [];
|
|
241
|
+
const sums = { injectedTurns: 0, hitTurns: 0, totalHits: 0, timeouts: 0, suppressedRecalls: 0 };
|
|
242
|
+
for (const s of all) {
|
|
243
|
+
sums.injectedTurns += s.injectedTurns;
|
|
244
|
+
sums.hitTurns += s.hitTurns;
|
|
245
|
+
sums.totalHits += s.totalHits;
|
|
246
|
+
sums.timeouts += s.timeouts;
|
|
247
|
+
sums.suppressedRecalls += s.suppressedRecalls;
|
|
248
|
+
}
|
|
249
|
+
const counts = modes?.countStates() ?? { off: 0, wo: 0 };
|
|
250
|
+
return {
|
|
251
|
+
ok: true,
|
|
252
|
+
activityDays,
|
|
253
|
+
distill,
|
|
254
|
+
recall: {
|
|
255
|
+
sessions: all.length,
|
|
256
|
+
...sums,
|
|
257
|
+
globalRecall: live?.get()?.recall ?? true,
|
|
258
|
+
sessionsModeOff: counts.off,
|
|
259
|
+
sessionsWriteOnly: counts.wo,
|
|
260
|
+
},
|
|
261
|
+
};
|
|
262
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-layered-memory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "L0~L3 分层蒸馏记忆插件 for DeepSeek Harness:自动捕获对话(L0)、抽取原子记忆(L1)、整合场景块(L2)、蒸馏核心画像/团队方法论(L3),并在模型步骤前自动召回注入。移植自 MemoryCore (TencentDB Agent Memory) 的管线设计。",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
package/assets/img/Modes.png
DELETED
|
Binary file
|
package/assets/img/ui-dark.jpg
DELETED
|
Binary file
|
package/assets/img/ui-light.jpg
DELETED
|
Binary file
|