dsh-auto-memory 0.5.1 → 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 -1
- package/README.zh.md +18 -2
- package/lib/client.js +68 -7
- package/lib/index.js +74 -7
- 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):
|
|
@@ -220,7 +240,6 @@ Override in your profile's `cordis.patch.yml` (config replaces wholesale):
|
|
|
220
240
|
resume); everything flows through standard `tool/call` / `tool/result`.
|
|
221
241
|
|
|
222
242
|
Deep dives: [design decisions](docs/design.md) ·
|
|
223
|
-
[dsh source-level research](docs/api-reports.md) ·
|
|
224
243
|
[postmortem: shipping a PR to awesome-dsh-plugin](docs/postmortem-pr-5696.md)
|
|
225
244
|
|
|
226
245
|
## Roadmap
|
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
|
|
|
@@ -187,7 +204,6 @@ dsh --profile demo # 重启 profile 生效
|
|
|
187
204
|
一切走标准 `tool/call` / `tool/result`。
|
|
188
205
|
|
|
189
206
|
深度内容:[设计决策](docs/design.md) ·
|
|
190
|
-
[dsh 源码级调研](docs/api-reports.md) ·
|
|
191
207
|
[复盘:向 awesome-dsh-plugin 提 PR](docs/postmortem-pr-5696.md)
|
|
192
208
|
|
|
193
209
|
## 路线图
|
package/lib/client.js
CHANGED
|
@@ -51,6 +51,7 @@ window.__ModuleLoader__.load({
|
|
|
51
51
|
const [groups, setGroups] = (0, react.useState)(null);
|
|
52
52
|
const [maxBytes, setMaxBytes] = (0, react.useState)(4096);
|
|
53
53
|
const [error, setError] = (0, react.useState)(false);
|
|
54
|
+
const [actionError, setActionError] = (0, react.useState)(null);
|
|
54
55
|
const [editing, setEditing] = (0, react.useState)(null);
|
|
55
56
|
const [busy, setBusy] = (0, react.useState)(false);
|
|
56
57
|
const load = (0, react.useCallback)(async () => {
|
|
@@ -68,12 +69,15 @@ window.__ModuleLoader__.load({
|
|
|
68
69
|
}, [load]);
|
|
69
70
|
const act = async (fn) => {
|
|
70
71
|
setBusy(true);
|
|
72
|
+
setActionError(null);
|
|
71
73
|
try {
|
|
72
74
|
const response = await fn();
|
|
73
|
-
if (!response.ok)
|
|
74
|
-
|
|
75
|
+
if (!response.ok) setActionError(response.status === 404 ? t("deleteFailed") : t("saveFailed"));
|
|
76
|
+
} catch {
|
|
77
|
+
setActionError(t("saveFailed"));
|
|
75
78
|
} finally {
|
|
76
79
|
setBusy(false);
|
|
80
|
+
await load();
|
|
77
81
|
}
|
|
78
82
|
};
|
|
79
83
|
const cwd = (group) => group.scope === "project" && group.current === true ? currentCwd() : void 0;
|
|
@@ -120,13 +124,19 @@ window.__ModuleLoader__.load({
|
|
|
120
124
|
style: { opacity: .75 },
|
|
121
125
|
children: t("intro")
|
|
122
126
|
}),
|
|
123
|
-
/* @__PURE__ */ (0, react_jsx_runtime.
|
|
127
|
+
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("p", { children: [/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
124
128
|
onClick: () => {
|
|
125
129
|
load();
|
|
126
130
|
},
|
|
127
131
|
disabled: busy,
|
|
128
132
|
children: t("refresh")
|
|
129
|
-
})
|
|
133
|
+
}), actionError !== null && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
134
|
+
style: {
|
|
135
|
+
marginLeft: "12px",
|
|
136
|
+
color: "#d97706"
|
|
137
|
+
},
|
|
138
|
+
children: actionError
|
|
139
|
+
})] }),
|
|
130
140
|
total === 0 && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("p", { children: t("empty") }),
|
|
131
141
|
groups.map((group) => {
|
|
132
142
|
const usage = group.indexBytes / maxBytes;
|
|
@@ -209,6 +219,10 @@ window.__ModuleLoader__.load({
|
|
|
209
219
|
title: t("pinned"),
|
|
210
220
|
children: " 📌"
|
|
211
221
|
}),
|
|
222
|
+
memory.disabled && /* @__PURE__ */ (0, react_jsx_runtime.jsx)("span", {
|
|
223
|
+
title: t("muted"),
|
|
224
|
+
children: " 🔇"
|
|
225
|
+
}),
|
|
212
226
|
" ",
|
|
213
227
|
/* @__PURE__ */ (0, react_jsx_runtime.jsxs)("code", {
|
|
214
228
|
style: {
|
|
@@ -282,6 +296,36 @@ window.__ModuleLoader__.load({
|
|
|
282
296
|
children: memory.pinned ? t("unpinAction") : t("pinAction")
|
|
283
297
|
}),
|
|
284
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
|
+
" ",
|
|
285
329
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
286
330
|
disabled: busy,
|
|
287
331
|
onClick: () => {
|
|
@@ -303,6 +347,7 @@ window.__ModuleLoader__.load({
|
|
|
303
347
|
t,
|
|
304
348
|
initial: editing.memory,
|
|
305
349
|
initialBody: editing.body,
|
|
350
|
+
busy,
|
|
306
351
|
onCancel: () => {
|
|
307
352
|
setEditing(null);
|
|
308
353
|
},
|
|
@@ -319,7 +364,7 @@ window.__ModuleLoader__.load({
|
|
|
319
364
|
});
|
|
320
365
|
}
|
|
321
366
|
/** 单条记忆编辑器(标题/摘要/正文,保存即重建索引、下次注入生效)。 */
|
|
322
|
-
function MemoryEditor({ t, initial, initialBody, onSave, onCancel }) {
|
|
367
|
+
function MemoryEditor({ t, initial, initialBody, busy, onSave, onCancel }) {
|
|
323
368
|
const [title, setTitle] = (0, react.useState)(initial.title ?? "");
|
|
324
369
|
const [description, setDescription] = (0, react.useState)(initial.description);
|
|
325
370
|
const [body, setBody] = (0, react.useState)(initialBody);
|
|
@@ -383,6 +428,7 @@ window.__ModuleLoader__.load({
|
|
|
383
428
|
style: { margin: "8px 0 0" },
|
|
384
429
|
children: [
|
|
385
430
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
431
|
+
disabled: busy,
|
|
386
432
|
onClick: () => {
|
|
387
433
|
onSave({
|
|
388
434
|
name: initial.name,
|
|
@@ -397,6 +443,7 @@ window.__ModuleLoader__.load({
|
|
|
397
443
|
}),
|
|
398
444
|
" ",
|
|
399
445
|
/* @__PURE__ */ (0, react_jsx_runtime.jsx)("button", {
|
|
446
|
+
disabled: busy,
|
|
400
447
|
onClick: onCancel,
|
|
401
448
|
children: t("cancel")
|
|
402
449
|
})
|
|
@@ -436,6 +483,9 @@ window.__ModuleLoader__.load({
|
|
|
436
483
|
userScope: "用户级(所有工作区)",
|
|
437
484
|
projectScope: "项目级",
|
|
438
485
|
pinned: "置顶",
|
|
486
|
+
muted: "已停用(不注入)",
|
|
487
|
+
muteAction: "停用",
|
|
488
|
+
unmuteAction: "恢复",
|
|
439
489
|
reads: "次读取",
|
|
440
490
|
updated: "更新于",
|
|
441
491
|
injectedPreview: "下一次会话将注入的索引(点开预览)",
|
|
@@ -470,6 +520,9 @@ window.__ModuleLoader__.load({
|
|
|
470
520
|
userScope: "User scope (all workspaces)",
|
|
471
521
|
projectScope: "Project scope",
|
|
472
522
|
pinned: "pinned",
|
|
523
|
+
muted: "muted (not injected)",
|
|
524
|
+
muteAction: "Mute",
|
|
525
|
+
unmuteAction: "Unmute",
|
|
473
526
|
reads: "reads",
|
|
474
527
|
updated: "updated",
|
|
475
528
|
injectedPreview: "Index injected into the next session (click to preview)",
|
|
@@ -537,11 +590,19 @@ window.__ModuleLoader__.load({
|
|
|
537
590
|
currentCwd: () => currentCwd(ctx)
|
|
538
591
|
};
|
|
539
592
|
}
|
|
540
|
-
/** 从 sessions
|
|
593
|
+
/** 从 sessions 快照解析当前工作区 cwd(#9:多工作区并存时不再取首个)。 */
|
|
541
594
|
function currentCwd(ctx) {
|
|
542
595
|
const byId = ctx.get("sessions")?.list?.getSnapshot?.()?.byId;
|
|
543
596
|
if (byId === void 0) return void 0;
|
|
544
|
-
|
|
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;
|
|
545
606
|
}
|
|
546
607
|
/** 注册侧栏入口与记忆管理面板。 */
|
|
547
608
|
function apply(ctx) {
|
package/lib/index.js
CHANGED
|
@@ -14,7 +14,7 @@ const USER_SCOPE_DIR = "_user";
|
|
|
14
14
|
/**
|
|
15
15
|
* 记忆存储层:类型化记忆文件 CRUD + MEMORY.md 索引维护。
|
|
16
16
|
*
|
|
17
|
-
* 关键设计决策(依据源码调研与对抗性审查,见
|
|
17
|
+
* 关键设计决策(依据源码调研与对抗性审查,见 internal research notes):
|
|
18
18
|
* 1. **必须用 node:fs 而非 ctx.fs**——默认部署的 fs-sandbox 可写根不含 $DSH_HOME,
|
|
19
19
|
* 走 ctx.fs 会抛 FS_SANDBOX_DENIED;官方先例 skill-filesystem 访问 $DSH_HOME
|
|
20
20
|
* 下的文件同样直接用 node:fs。
|
|
@@ -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;
|
|
@@ -239,12 +241,54 @@ const SECRET_PATTERNS = [
|
|
|
239
241
|
}
|
|
240
242
|
];
|
|
241
243
|
/**
|
|
244
|
+
* GB11643 身份证校验位验证:18 位号码仅当末位与加权模 11 校验相符才算真身份证。
|
|
245
|
+
* 纯长度匹配会把订单号/雪花 ID 等任意 18 位数字一并误杀(#7)。
|
|
246
|
+
*/
|
|
247
|
+
function isValidIdCard(candidate) {
|
|
248
|
+
if (!/^\d{17}[\dXx]$/.test(candidate)) return false;
|
|
249
|
+
const WEIGHTS = [
|
|
250
|
+
7,
|
|
251
|
+
9,
|
|
252
|
+
10,
|
|
253
|
+
5,
|
|
254
|
+
8,
|
|
255
|
+
4,
|
|
256
|
+
2,
|
|
257
|
+
1,
|
|
258
|
+
6,
|
|
259
|
+
3,
|
|
260
|
+
7,
|
|
261
|
+
9,
|
|
262
|
+
10,
|
|
263
|
+
5,
|
|
264
|
+
8,
|
|
265
|
+
4,
|
|
266
|
+
2
|
|
267
|
+
];
|
|
268
|
+
const CHECKS = [
|
|
269
|
+
"1",
|
|
270
|
+
"0",
|
|
271
|
+
"X",
|
|
272
|
+
"9",
|
|
273
|
+
"8",
|
|
274
|
+
"7",
|
|
275
|
+
"6",
|
|
276
|
+
"5",
|
|
277
|
+
"4",
|
|
278
|
+
"3",
|
|
279
|
+
"2"
|
|
280
|
+
];
|
|
281
|
+
let sum = 0;
|
|
282
|
+
for (let i = 0; i < 17; i++) sum += Number(candidate[i]) * WEIGHTS[i];
|
|
283
|
+
return candidate[17].toUpperCase() === CHECKS[sum % 11];
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
242
286
|
* 写入前脱敏(纯函数,可测):把高置信密钥/手机号/身份证替换为 [REDACTED:kind]。
|
|
243
|
-
* 应用于 description
|
|
287
|
+
* 应用于 description/body——所有写路径(工具/固化/面板)都经 store.write,单点拦截。
|
|
244
288
|
*/
|
|
245
289
|
function redactSecrets(text) {
|
|
246
290
|
let out = text;
|
|
247
|
-
for (const { kind, re } of SECRET_PATTERNS) out = out.replace(re, `[REDACTED:${kind}]`);
|
|
291
|
+
for (const { kind, re } of SECRET_PATTERNS) out = out.replace(re, (match) => kind === "id-card" && !isValidIdCard(match) ? match : `[REDACTED:${kind}]`);
|
|
248
292
|
return out;
|
|
249
293
|
}
|
|
250
294
|
/**
|
|
@@ -280,9 +324,19 @@ function memoryScore(record, nowMs) {
|
|
|
280
324
|
/**
|
|
281
325
|
* 描述相似度(Jaccard 词集,纯函数,可测):固化防回声的查重依据——
|
|
282
326
|
* "既有记忆的复述不是新信息",相似度过高的候选跳过而非堆积。
|
|
327
|
+
* CJK 感知:连续中文段按字符二元组(bigram)切分(检索领域标准做法)——
|
|
328
|
+
* 整段单 token 会让中文近重复仅得 ~0.4,防回声对中文完全失效(#5)。
|
|
283
329
|
*/
|
|
284
330
|
function descriptionSimilarity(a, b) {
|
|
285
|
-
const words = (text) =>
|
|
331
|
+
const words = (text) => {
|
|
332
|
+
const tokens = text.toLowerCase().split(/[^a-z0-9一-鿿]+/).filter((w) => w.length > 0);
|
|
333
|
+
const out = /* @__PURE__ */ new Set();
|
|
334
|
+
for (const token of tokens) if (/^[一-鿿]+$/.test(token)) {
|
|
335
|
+
if (token.length === 1) out.add(token);
|
|
336
|
+
else for (let i = 0; i + 1 < token.length; i++) out.add(token.slice(i, i + 2));
|
|
337
|
+
} else out.add(token);
|
|
338
|
+
return out;
|
|
339
|
+
};
|
|
286
340
|
const wa = words(a);
|
|
287
341
|
const wb = words(b);
|
|
288
342
|
if (wa.size === 0 || wb.size === 0) return 0;
|
|
@@ -364,11 +418,13 @@ var MemoryStore = class {
|
|
|
364
418
|
await this.withLockRecovery(join(dir, INDEX_FILENAME), async () => {
|
|
365
419
|
const existing = parseMemory(await promises.readFile(file, "utf8").catch(() => ""), scope);
|
|
366
420
|
const pinned = record.pinned ?? existing?.pinned;
|
|
421
|
+
const disabled = record.disabled ?? existing?.disabled;
|
|
367
422
|
const importance = record.importance ?? existing?.importance;
|
|
368
423
|
const title = record.title ?? existing?.title;
|
|
369
424
|
written = {
|
|
370
425
|
...record,
|
|
371
426
|
...pinned === true ? { pinned: true } : {},
|
|
427
|
+
...disabled === true ? { disabled: true } : {},
|
|
372
428
|
...importance !== void 0 ? { importance } : {},
|
|
373
429
|
...title !== void 0 ? { title: sanitizeContent(title) } : {},
|
|
374
430
|
description: sanitizeContent(record.description),
|
|
@@ -615,6 +671,10 @@ function registerMemoryTools(ctx, store, enableUserScope) {
|
|
|
615
671
|
type: "boolean",
|
|
616
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"
|
|
617
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
|
+
},
|
|
618
678
|
scope: {
|
|
619
679
|
type: "string",
|
|
620
680
|
enum: ["project", "user"],
|
|
@@ -661,6 +721,7 @@ function registerMemoryTools(ctx, store, enableUserScope) {
|
|
|
661
721
|
name,
|
|
662
722
|
title: args.title !== void 0 && args.title.trim().length > 0 ? args.title.trim().slice(0, 80) : void 0,
|
|
663
723
|
...args.pinned !== void 0 ? { pinned: args.pinned } : {},
|
|
724
|
+
...args.disabled !== void 0 ? { disabled: args.disabled } : {},
|
|
664
725
|
description: args.description.trim().slice(0, 160),
|
|
665
726
|
type: args.type,
|
|
666
727
|
body: args.body.slice(0, 2e3)
|
|
@@ -1252,6 +1313,9 @@ sessions for this user, following these rules:
|
|
|
1252
1313
|
Return ONLY a JSON array, each element exactly:
|
|
1253
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}
|
|
1254
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).
|
|
1255
1319
|
|
|
1256
1320
|
<transcript>
|
|
1257
1321
|
${transcript}
|
|
@@ -1399,6 +1463,7 @@ async function readGroup(dir, scope, slug) {
|
|
|
1399
1463
|
description: record.description,
|
|
1400
1464
|
type: record.type,
|
|
1401
1465
|
pinned: record.pinned === true,
|
|
1466
|
+
disabled: record.disabled === true,
|
|
1402
1467
|
reads: record.reads ?? 0,
|
|
1403
1468
|
...record.updatedMs !== void 0 ? { updatedMs: record.updatedMs } : {},
|
|
1404
1469
|
bytes: Buffer.byteLength(raw, "utf8")
|
|
@@ -1470,7 +1535,8 @@ function registerMemoryApi(ctx, store, rootDir, config) {
|
|
|
1470
1535
|
description: record.description,
|
|
1471
1536
|
type: record.type,
|
|
1472
1537
|
body: record.body,
|
|
1473
|
-
pinned: record.pinned === true
|
|
1538
|
+
pinned: record.pinned === true,
|
|
1539
|
+
disabled: record.disabled === true
|
|
1474
1540
|
});
|
|
1475
1541
|
} catch (error) {
|
|
1476
1542
|
return json({ error: String(error) }, 400);
|
|
@@ -1493,6 +1559,7 @@ function registerMemoryApi(ctx, store, rootDir, config) {
|
|
|
1493
1559
|
name,
|
|
1494
1560
|
title: typeof body.title === "string" && body.title.trim().length > 0 ? body.title.trim().slice(0, 80) : void 0,
|
|
1495
1561
|
...body.pinned !== void 0 ? { pinned: body.pinned } : {},
|
|
1562
|
+
...body.disabled === true ? { disabled: true } : {},
|
|
1496
1563
|
description: body.description.trim().slice(0, 160),
|
|
1497
1564
|
type: body.type,
|
|
1498
1565
|
body: typeof body.body === "string" ? body.body.slice(0, 2e3) : ""
|
package/package.json
CHANGED