dsh-auto-memory 0.5.2 → 0.6.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.md +20 -0
- package/README.zh.md +18 -1
- package/lib/client.js +50 -2
- package/lib/index.js +18 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -25,6 +25,26 @@ open a brand-new session tomorrow, ask *"what do you know about me?"*, and it
|
|
|
25
25
|
|
|
26
26
|
---
|
|
27
27
|
|
|
28
|
+
## What's new in 0.6.0 (P2)
|
|
29
|
+
|
|
30
|
+
- **Harness-sessions evaluation datasets** (`evals/datasets/`): synthetic
|
|
31
|
+
multi-session developer transcripts with gold-label facts — durable /
|
|
32
|
+
ephemeral-trap / updated labels, evidence sessions, abstention probes.
|
|
33
|
+
The coding-agent memory scenario no public benchmark covers.
|
|
34
|
+
- **L1 injection recall** (zero-API runner): 100% on both datasets at the
|
|
35
|
+
perfect-consolidation ceiling — the injection side loses nothing.
|
|
36
|
+
- **L3 consolidation quality** (one LLM call per session, same code path as
|
|
37
|
+
the plugin): **mini recall 100% / update-correctness 100% / pollution 0%;
|
|
38
|
+
standard recall 83.3% / precision 84.6% / update-correctness 100% /
|
|
39
|
+
pollution 0%** (deepseek-chat, temperature 0). Ephemeral traps were
|
|
40
|
+
correctly refused in every run.
|
|
41
|
+
- **Product fix surfaced by the eval**: consolidation now writes
|
|
42
|
+
title/description in the transcript's language (Chinese transcripts used
|
|
43
|
+
to produce English memories — measured, then fixed).
|
|
44
|
+
- Community: soft-mute (third state between keep and delete, #10), panel
|
|
45
|
+
current-workspace fix (#9), CJK bigram echo-dedup (#5), editor busy/error
|
|
46
|
+
UX (#8), GB11643 checksum before id-card redaction (#7).
|
|
47
|
+
|
|
28
48
|
## What's new in 0.5.0 (P2)
|
|
29
49
|
|
|
30
50
|
- **Three-factor ranking** (Generative Agents formula, embedding-free):
|
package/README.zh.md
CHANGED
|
@@ -24,6 +24,23 @@ dsh plugin --profile demo add dsh-auto-memory
|
|
|
24
24
|
|
|
25
25
|
---
|
|
26
26
|
|
|
27
|
+
## 0.6.0 新增(P2)
|
|
28
|
+
|
|
29
|
+
- **harness-sessions 评测数据集**(`evals/datasets/`):合成多会话开发者剧本
|
|
30
|
+
+ 金标事实(durable / ephemeral 陷阱 / updated 推翻标签、evidence 会话、
|
|
31
|
+
拒答题)——公开基准都不覆盖的 coding-agent 记忆场景。
|
|
32
|
+
- **L1 注入召回**(零 API runner):两个数据集在完美固化上界下均 100%——
|
|
33
|
+
注入侧零损耗。
|
|
34
|
+
- **L3 固化质量**(每会话一次 LLM 调用,与插件同一条固化代码路径):
|
|
35
|
+
**mini 召回 100% / 更新正确率 100% / 污染 0%;standard 召回 83.3% /
|
|
36
|
+
精确率 84.6% / 更新正确率 100% / 污染 0%**(deepseek-chat,temperature 0)。
|
|
37
|
+
ephemeral 陷阱在每轮运行中都被正确拒绝。
|
|
38
|
+
- **评测反哺的产品修复**:固化描述现在跟随会话语言(中文会话曾固化出
|
|
39
|
+
英文记忆——先测出,后修复)。
|
|
40
|
+
- 社区贡献:软停用(保留与删除之间的第三档,#10)、面板当前工作区修复
|
|
41
|
+
(#9)、中文 bigram 回声去重(#5)、编辑器 busy/错误 UX(#8)、身份证
|
|
42
|
+
校验位前置验证(#7)。
|
|
43
|
+
|
|
27
44
|
## 0.5.0 新增(P2)
|
|
28
45
|
|
|
29
46
|
- **三因子排序**(Generative Agents 公式的免 embedding 版):注入索引与
|
|
@@ -149,7 +166,7 @@ node scripts/demo.mjs # 不要 API key、不开浏览器:看 写入 → 索引
|
|
|
149
166
|
评测层已抓到过真实 bug:字节预算曾遗漏指导文本、整段超支 ~800 字节
|
|
150
167
|
(已修复并带回归)。
|
|
151
168
|
|
|
152
|
-
**
|
|
169
|
+
**93 项测试 + 三层评测(L0 确定性/L1 召回/L3 固化质量)。运行时依赖仅 `yaml`。**
|
|
153
170
|
|
|
154
171
|
## 安装
|
|
155
172
|
|
package/lib/client.js
CHANGED
|
@@ -219,6 +219,10 @@ window.__ModuleLoader__.load({
|
|
|
219
219
|
title: t("pinned"),
|
|
220
220
|
children: " 📌"
|
|
221
221
|
}),
|
|
222
|
+
memory.disabled && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
223
|
+
title: t("muted"),
|
|
224
|
+
children: " 🔇"
|
|
225
|
+
}),
|
|
222
226
|
" ",
|
|
223
227
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("code", {
|
|
224
228
|
style: {
|
|
@@ -292,6 +296,36 @@ window.__ModuleLoader__.load({
|
|
|
292
296
|
children: memory.pinned ? t("unpinAction") : t("pinAction")
|
|
293
297
|
}),
|
|
294
298
|
" ",
|
|
299
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
300
|
+
disabled: busy,
|
|
301
|
+
onClick: () => {
|
|
302
|
+
(async () => {
|
|
303
|
+
try {
|
|
304
|
+
const detail = await read({
|
|
305
|
+
name: memory.name,
|
|
306
|
+
scope: group.scope,
|
|
307
|
+
cwd: cwd(group)
|
|
308
|
+
});
|
|
309
|
+
await act(() => write({
|
|
310
|
+
cwd: cwd(group),
|
|
311
|
+
scope: group.scope,
|
|
312
|
+
name: memory.name,
|
|
313
|
+
title: memory.title,
|
|
314
|
+
description: memory.description,
|
|
315
|
+
type: memory.type,
|
|
316
|
+
body: detail.body,
|
|
317
|
+
pinned: memory.pinned,
|
|
318
|
+
disabled: !memory.disabled
|
|
319
|
+
}));
|
|
320
|
+
} catch (error) {
|
|
321
|
+
console.warn("[auto-memory] read failed, mute aborted", error);
|
|
322
|
+
await load();
|
|
323
|
+
}
|
|
324
|
+
})();
|
|
325
|
+
},
|
|
326
|
+
children: memory.disabled ? t("unmuteAction") : t("muteAction")
|
|
327
|
+
}),
|
|
328
|
+
" ",
|
|
295
329
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
296
330
|
disabled: busy,
|
|
297
331
|
onClick: () => {
|
|
@@ -449,6 +483,9 @@ window.__ModuleLoader__.load({
|
|
|
449
483
|
userScope: "用户级(所有工作区)",
|
|
450
484
|
projectScope: "项目级",
|
|
451
485
|
pinned: "置顶",
|
|
486
|
+
muted: "已停用(不注入)",
|
|
487
|
+
muteAction: "停用",
|
|
488
|
+
unmuteAction: "恢复",
|
|
452
489
|
reads: "次读取",
|
|
453
490
|
updated: "更新于",
|
|
454
491
|
injectedPreview: "下一次会话将注入的索引(点开预览)",
|
|
@@ -483,6 +520,9 @@ window.__ModuleLoader__.load({
|
|
|
483
520
|
userScope: "User scope (all workspaces)",
|
|
484
521
|
projectScope: "Project scope",
|
|
485
522
|
pinned: "pinned",
|
|
523
|
+
muted: "muted (not injected)",
|
|
524
|
+
muteAction: "Mute",
|
|
525
|
+
unmuteAction: "Unmute",
|
|
486
526
|
reads: "reads",
|
|
487
527
|
updated: "updated",
|
|
488
528
|
injectedPreview: "Index injected into the next session (click to preview)",
|
|
@@ -550,11 +590,19 @@ window.__ModuleLoader__.load({
|
|
|
550
590
|
currentCwd: () => currentCwd(ctx)
|
|
551
591
|
};
|
|
552
592
|
}
|
|
553
|
-
/** 从 sessions
|
|
593
|
+
/** 从 sessions 快照解析当前工作区 cwd(#9:多工作区并存时不再取首个)。 */
|
|
554
594
|
function currentCwd(ctx) {
|
|
555
595
|
const byId = ctx.get("sessions")?.list?.getSnapshot?.()?.byId;
|
|
556
596
|
if (byId === void 0) return void 0;
|
|
557
|
-
|
|
597
|
+
const rows = Object.values(byId).filter((session) => session.cwd !== void 0 && session.cwd.length > 0);
|
|
598
|
+
if (rows.length === 0) return void 0;
|
|
599
|
+
const running = rows.filter((session) => session.running === true);
|
|
600
|
+
const pool = running.length > 0 ? running : rows;
|
|
601
|
+
pool.sort((a, b) => {
|
|
602
|
+
const delta = (b.updatedAt ?? 0) - (a.updatedAt ?? 0);
|
|
603
|
+
return delta !== 0 ? delta : a.cwd < b.cwd ? -1 : 1;
|
|
604
|
+
});
|
|
605
|
+
return pool[0]?.cwd;
|
|
558
606
|
}
|
|
559
607
|
/** 注册侧栏入口与记忆管理面板。 */
|
|
560
608
|
function apply(ctx) {
|
package/lib/index.js
CHANGED
|
@@ -114,7 +114,7 @@ function parseMemory(raw, scope) {
|
|
|
114
114
|
try {
|
|
115
115
|
const fm = parseFrontmatter(raw);
|
|
116
116
|
if (!fm) return null;
|
|
117
|
-
const { name, description, type, title, created, updated, lastRead, reads, pinned, importance } = fm.data;
|
|
117
|
+
const { name, description, type, title, created, updated, lastRead, reads, pinned, importance, disabled } = fm.data;
|
|
118
118
|
if (typeof name !== "string" || typeof description !== "string" || description.trim().length === 0) return null;
|
|
119
119
|
const parsedType = type === void 0 ? "reference" : asMemoryType(String(type));
|
|
120
120
|
const asMs = (value) => typeof value === "number" && Number.isSafeInteger(value) && value > 0 ? value : void 0;
|
|
@@ -128,6 +128,7 @@ function parseMemory(raw, scope) {
|
|
|
128
128
|
body: fm.body.trim(),
|
|
129
129
|
scope,
|
|
130
130
|
...pinned === true ? { pinned: true } : {},
|
|
131
|
+
...disabled === true ? { disabled: true } : {},
|
|
131
132
|
...asImportance(importance) !== void 0 ? { importance: asImportance(importance) } : {},
|
|
132
133
|
createdMs: asMs(created),
|
|
133
134
|
updatedMs: asMs(updated),
|
|
@@ -146,6 +147,7 @@ function serializeMemory(record) {
|
|
|
146
147
|
description: record.description,
|
|
147
148
|
type: record.type,
|
|
148
149
|
...record.pinned === true ? { pinned: true } : {},
|
|
150
|
+
...record.disabled === true ? { disabled: true } : {},
|
|
149
151
|
...record.importance !== void 0 ? { importance: record.importance } : {},
|
|
150
152
|
...record.createdMs !== void 0 ? { created: record.createdMs } : {},
|
|
151
153
|
...record.updatedMs !== void 0 ? { updated: record.updatedMs } : {},
|
|
@@ -171,7 +173,7 @@ function mergeLifecycleMeta(existing, now) {
|
|
|
171
173
|
*/
|
|
172
174
|
function renderIndexBody(records) {
|
|
173
175
|
const now = Date.now();
|
|
174
|
-
const lines = [...records].sort((a, b) => {
|
|
176
|
+
const lines = [...records.filter((record) => record.disabled !== true)].sort((a, b) => {
|
|
175
177
|
const pa = a.pinned === true ? 0 : 1;
|
|
176
178
|
const pb = b.pinned === true ? 0 : 1;
|
|
177
179
|
if (pa !== pb) return pa - pb;
|
|
@@ -416,11 +418,13 @@ var MemoryStore = class {
|
|
|
416
418
|
await this.withLockRecovery(join(dir, INDEX_FILENAME), async () => {
|
|
417
419
|
const existing = parseMemory(await promises.readFile(file, "utf8").catch(() => ""), scope);
|
|
418
420
|
const pinned = record.pinned ?? existing?.pinned;
|
|
421
|
+
const disabled = record.disabled ?? existing?.disabled;
|
|
419
422
|
const importance = record.importance ?? existing?.importance;
|
|
420
423
|
const title = record.title ?? existing?.title;
|
|
421
424
|
written = {
|
|
422
425
|
...record,
|
|
423
426
|
...pinned === true ? { pinned: true } : {},
|
|
427
|
+
...disabled === true ? { disabled: true } : {},
|
|
424
428
|
...importance !== void 0 ? { importance } : {},
|
|
425
429
|
...title !== void 0 ? { title: sanitizeContent(title) } : {},
|
|
426
430
|
description: sanitizeContent(record.description),
|
|
@@ -667,6 +671,10 @@ function registerMemoryTools(ctx, store, enableUserScope) {
|
|
|
667
671
|
type: "boolean",
|
|
668
672
|
description: "Pin this memory: it sorts first in the index, survives budget truncation, and is never hidden by staleness eviction. Use when the user explicitly says to keep something forever; unpin by passing false"
|
|
669
673
|
},
|
|
674
|
+
disabled: {
|
|
675
|
+
type: "boolean",
|
|
676
|
+
description: "Soft-mute: keep the file but exclude it from the injected index (the memory stops reaching the model without being deleted). Use when the user says \"stop bringing this up\" or similar; unmute by passing false"
|
|
677
|
+
},
|
|
670
678
|
scope: {
|
|
671
679
|
type: "string",
|
|
672
680
|
enum: ["project", "user"],
|
|
@@ -713,6 +721,7 @@ function registerMemoryTools(ctx, store, enableUserScope) {
|
|
|
713
721
|
name,
|
|
714
722
|
title: args.title !== void 0 && args.title.trim().length > 0 ? args.title.trim().slice(0, 80) : void 0,
|
|
715
723
|
...args.pinned !== void 0 ? { pinned: args.pinned } : {},
|
|
724
|
+
...args.disabled !== void 0 ? { disabled: args.disabled } : {},
|
|
716
725
|
description: args.description.trim().slice(0, 160),
|
|
717
726
|
type: args.type,
|
|
718
727
|
body: args.body.slice(0, 2e3)
|
|
@@ -1304,6 +1313,9 @@ sessions for this user, following these rules:
|
|
|
1304
1313
|
Return ONLY a JSON array, each element exactly:
|
|
1305
1314
|
{"name":"kebab-case-id","title":"short human heading","description":"one line <=160 chars","type":"user|feedback|project|reference","body":"the fact in markdown; for feedback include **Why:** and **How to apply:** lines","scope":"project","importance":5}
|
|
1306
1315
|
Use scope "user" only for user-global preferences; default "project".
|
|
1316
|
+
Write "name" in kebab-case English; write title/description/body in the SAME
|
|
1317
|
+
language as the transcript (a Chinese transcript gets Chinese memories —
|
|
1318
|
+
the user will read them).
|
|
1307
1319
|
|
|
1308
1320
|
<transcript>
|
|
1309
1321
|
${transcript}
|
|
@@ -1451,6 +1463,7 @@ async function readGroup(dir, scope, slug) {
|
|
|
1451
1463
|
description: record.description,
|
|
1452
1464
|
type: record.type,
|
|
1453
1465
|
pinned: record.pinned === true,
|
|
1466
|
+
disabled: record.disabled === true,
|
|
1454
1467
|
reads: record.reads ?? 0,
|
|
1455
1468
|
...record.updatedMs !== void 0 ? { updatedMs: record.updatedMs } : {},
|
|
1456
1469
|
bytes: Buffer.byteLength(raw, "utf8")
|
|
@@ -1522,7 +1535,8 @@ function registerMemoryApi(ctx, store, rootDir, config) {
|
|
|
1522
1535
|
description: record.description,
|
|
1523
1536
|
type: record.type,
|
|
1524
1537
|
body: record.body,
|
|
1525
|
-
pinned: record.pinned === true
|
|
1538
|
+
pinned: record.pinned === true,
|
|
1539
|
+
disabled: record.disabled === true
|
|
1526
1540
|
});
|
|
1527
1541
|
} catch (error) {
|
|
1528
1542
|
return json({ error: String(error) }, 400);
|
|
@@ -1545,6 +1559,7 @@ function registerMemoryApi(ctx, store, rootDir, config) {
|
|
|
1545
1559
|
name,
|
|
1546
1560
|
title: typeof body.title === "string" && body.title.trim().length > 0 ? body.title.trim().slice(0, 80) : void 0,
|
|
1547
1561
|
...body.pinned !== void 0 ? { pinned: body.pinned } : {},
|
|
1562
|
+
...body.disabled === true ? { disabled: true } : {},
|
|
1548
1563
|
description: body.description.trim().slice(0, 160),
|
|
1549
1564
|
type: body.type,
|
|
1550
1565
|
body: typeof body.body === "string" ? body.body.slice(0, 2e3) : ""
|
package/package.json
CHANGED