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 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
- **78 项测试(含确定性评测层)。运行时依赖仅 `yaml`。安装体积 15 kB。**
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 快照取一个可用的工作区 cwd(官方 ui-reference 同款读取面)。 */
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
- for (const session of Object.values(byId)) if (session.cwd !== void 0 && session.cwd.length > 0) return session.cwd;
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-auto-memory",
3
- "version": "0.5.2",
3
+ "version": "0.6.0",
4
4
  "description": "Claude Code style auto-memory plugin for DeepSeek Harness: typed memory files + MEMORY.md index auto-injected into the system prompt",
5
5
  "type": "module",
6
6
  "license": "MIT",