@modusensus/dsh-mneme 0.2.1 → 0.2.3
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 +7 -5
- package/lib/api.js +9 -1
- package/lib/dream/decisions.js +30 -27
- package/lib/index.js +5 -0
- package/lib/service.js +13 -5
- package/lib/store.js +22 -7
- package/lib/tools.js +3 -2
- package/lib/vector-index.js +1 -1
- package/package.json +1 -1
- package/src/api.js +9 -1
- package/src/dream/decisions.js +30 -27
- package/src/index.js +5 -0
- package/src/service.js +13 -5
- package/src/store.js +22 -7
- package/src/tools.js +3 -2
- package/src/vector-index.js +1 -1
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://www.npmjs.com/package/@modusensus/dsh-mneme)
|
|
6
6
|
[](LICENSE)
|
|
7
7
|
[](https://github.com/awesome-dsh-plugin/awesome-dsh-plugin)
|
|
8
|
-
[](https://github.com/modusensus/dsh-mneme)
|
|
9
9
|
|
|
10
10
|
> 给 DeepSeek Harness 的跨会话记忆插件:让 Agent 记住你、记住项目、自动整理记忆。**Mneme**(Μνήμη)——希腊记忆女神 Mnemosyne 之名,掌管记忆与梦境,正如 autoDream 在后台巩固记忆。
|
|
11
11
|
|
|
@@ -38,13 +38,15 @@
|
|
|
38
38
|
### autoDream 自动记忆整理 🧠
|
|
39
39
|
|
|
40
40
|
- **触发**:记忆数 > 10 或总字符 > 5000 时,异步自动触发(不阻塞写入)
|
|
41
|
-
- **决策清单式整理**:LLM 输出 `keep` / `merge` / `archive` / `conflict` 决策清单,服务端校验后逐条应用
|
|
41
|
+
- **决策清单式整理**:LLM 输出 `keep` / `merge` / `archive` / `conflict` / `update` 决策清单,服务端校验后逐条应用
|
|
42
42
|
- `merge`:合并主题相近的条目,保留信息最完整者
|
|
43
43
|
- `archive`:归档过时/冗余条目(可恢复,不物理删除)
|
|
44
44
|
- `conflict`:裁决矛盾信息,胜者保留、败者归档并追加溯源注释
|
|
45
|
+
- `update`(v0.2.1):直接修正单条记忆的过时/错误内容(单 id / 必须实际变化 / 非 summary / 24h 保护 / 每次 ≤2)
|
|
46
|
+
- **失败追踪(v0.2.1)**:用户纠正记忆时写入 `failure_memories` 表(旧值/新值),为后续自进化积累数据
|
|
45
47
|
- **摘要生成**:整理后生成"记忆库总览"(单一实例),作为下次会话的优先注入
|
|
46
48
|
- **Fail-safe**:非法 LLM 输出(未知 id / 非法 action / 跨类型合并 / 越界 importance)拒绝整单,绝不破坏记忆库
|
|
47
|
-
- **裁决审计**:每次运行写入 `dream_runs` 审计表(输入快照 sha256 digest + 完整输入快照 + 决策清单 + 逐 id 去向 + receipt),可离线回放;merge / conflict
|
|
49
|
+
- **裁决审计**:每次运行写入 `dream_runs` 审计表(输入快照 sha256 digest + 完整输入快照 + 决策清单 + 逐 id 去向 + receipt),可离线回放;merge / conflict / update 幂等应用,重放/并发重复执行无累积副作用;update 记录 `_before` 快照
|
|
48
50
|
|
|
49
51
|
### Web 记忆面板
|
|
50
52
|
|
|
@@ -204,7 +206,7 @@ src/
|
|
|
204
206
|
lib/
|
|
205
207
|
├── client.js # Web 面板(手写 ModuleLoader bundle)
|
|
206
208
|
└── *.js # src 的同步分发产物
|
|
207
|
-
test/ #
|
|
209
|
+
test/ # 212 个 node:test 测试(含审计与三轴线压测不变量)
|
|
208
210
|
scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压测 · sync-lib.js 同步
|
|
209
211
|
```
|
|
210
212
|
|
|
@@ -213,7 +215,7 @@ scripts/ # e2e-dsh.js 端到端演示 · stress-dsh.js 三轴线压
|
|
|
213
215
|
```bash
|
|
214
216
|
cd dsh-mneme
|
|
215
217
|
npm install # 安装 peer 依赖(以 devDependencies 形式,用于本地测试)
|
|
216
|
-
npm test # 运行
|
|
218
|
+
npm test # 运行 212 个测试
|
|
217
219
|
npm run stress # 三轴线压测:长会话检索 / 冲突仲裁 / 多 Agent 并发(离线 mock LLM)
|
|
218
220
|
npm run sync # 把 src/ 同步到 lib/(发布时由 prepack 钩子自动执行)
|
|
219
221
|
```
|
package/lib/api.js
CHANGED
|
@@ -71,7 +71,15 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
71
71
|
const url = new URL(req.url, "http://localhost");
|
|
72
72
|
const q = url.searchParams.get("q") ?? "";
|
|
73
73
|
const limit = Number(url.searchParams.get("limit") ?? 20);
|
|
74
|
-
// mode
|
|
74
|
+
// mode selects the recall strategy (defaults to auto):
|
|
75
|
+
// auto (default) keyword first, vector fills remaining slots
|
|
76
|
+
// hybrid vector first, keyword fills remaining slots; scores of
|
|
77
|
+
// memories hit by both sides are weight-blended
|
|
78
|
+
// vector vector only, falls back to keyword when the vector path
|
|
79
|
+
// is unavailable (no embedder or a throwing one)
|
|
80
|
+
// keyword literal text only; never queries the embedder
|
|
81
|
+
// rerank=false disables the cross-encoder reorder for this request;
|
|
82
|
+
// the response `mode` field reports which path actually produced rows.
|
|
75
83
|
const mode = url.searchParams.get("mode") ?? "auto";
|
|
76
84
|
const rerank = url.searchParams.get("rerank") !== "false";
|
|
77
85
|
const query = q.trim();
|
package/lib/dream/decisions.js
CHANGED
|
@@ -30,6 +30,36 @@ export function validateDecisions(decisions, snapshot, options = {}) {
|
|
|
30
30
|
errors.push(`${at}: ${d.action} needs non-empty ids`);
|
|
31
31
|
continue;
|
|
32
32
|
}
|
|
33
|
+
// update-specific field validation runs BEFORE claiming ids, so a failing
|
|
34
|
+
// update never pollutes the claimed set (which drives the "every id must
|
|
35
|
+
// appear in a decision" check below).
|
|
36
|
+
if (d.action === "update") {
|
|
37
|
+
// 只能更新单条
|
|
38
|
+
if (!Array.isArray(d.ids) || d.ids.length !== 1) {
|
|
39
|
+
errors.push(`${at}: update must target exactly one id`);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
// 必须产生实际变化
|
|
43
|
+
const mem = snapshot.get(d.ids[0]);
|
|
44
|
+
const hasChange = (d.title !== undefined && d.title !== mem?.title)
|
|
45
|
+
|| (d.content !== undefined && d.content !== mem?.content)
|
|
46
|
+
|| (d.importance !== undefined && d.importance !== mem?.importance);
|
|
47
|
+
if (!hasChange) {
|
|
48
|
+
errors.push(`${at}: update must change at least one field`);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
// 不能更新 summary
|
|
52
|
+
if (mem?.type === "summary") {
|
|
53
|
+
errors.push(`${at}: cannot update summary via update action`);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
// 保护期:新建记忆不可立即被 update(可配置)
|
|
57
|
+
const ageHours = (Date.now() - new Date(mem?.created_at).getTime()) / 3600000;
|
|
58
|
+
if (ageHours < minAgeHours) {
|
|
59
|
+
errors.push(`${at}: memory too young (< ${minAgeHours}h)`);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
33
63
|
for (const id of ids) {
|
|
34
64
|
const mem = snapshot.get(id);
|
|
35
65
|
if (!mem) {
|
|
@@ -59,33 +89,6 @@ export function validateDecisions(decisions, snapshot, options = {}) {
|
|
|
59
89
|
errors.push(`${at}: merge ids span multiple types (${[...mergeTypes].join(", ")})`);
|
|
60
90
|
}
|
|
61
91
|
}
|
|
62
|
-
if (d.action === "update") {
|
|
63
|
-
// 只能更新单条
|
|
64
|
-
if (!Array.isArray(d.ids) || d.ids.length !== 1) {
|
|
65
|
-
errors.push(`${at}: update must target exactly one id`);
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
// 必须产生实际变化
|
|
69
|
-
const mem = snapshot.get(d.ids[0]);
|
|
70
|
-
const hasChange = (d.title !== undefined && d.title !== mem?.title)
|
|
71
|
-
|| (d.content !== undefined && d.content !== mem?.content)
|
|
72
|
-
|| (d.importance !== undefined && d.importance !== mem?.importance);
|
|
73
|
-
if (!hasChange) {
|
|
74
|
-
errors.push(`${at}: update must change at least one field`);
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
// 不能更新 summary
|
|
78
|
-
if (mem?.type === "summary") {
|
|
79
|
-
errors.push(`${at}: cannot update summary via update action`);
|
|
80
|
-
continue;
|
|
81
|
-
}
|
|
82
|
-
// 保护期:新建记忆不可立即被 update(可配置)
|
|
83
|
-
const ageHours = (Date.now() - new Date(mem?.created_at).getTime()) / 3600000;
|
|
84
|
-
if (ageHours < minAgeHours) {
|
|
85
|
-
errors.push(`${at}: memory too young (< ${minAgeHours}h)`);
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
92
|
}
|
|
90
93
|
// Cap update churn: too many edits in one cycle signals a runaway model
|
|
91
94
|
const updateCount = decisions.filter((d) => d.action === "update").length;
|
package/lib/index.js
CHANGED
|
@@ -36,6 +36,11 @@ export const apply = (ctx, config) => {
|
|
|
36
36
|
mkdirSync(memoryDir, { recursive: true });
|
|
37
37
|
|
|
38
38
|
const store = createStore(join(memoryDir, "memory.db"));
|
|
39
|
+
// Prune reflection failure rows older than 90 days on boot (best-effort, so
|
|
40
|
+
// the failure table never grows unbounded).
|
|
41
|
+
try {
|
|
42
|
+
store.deleteOldFailures(new Date(Date.now() - 90 * 86400000).toISOString());
|
|
43
|
+
} catch { /* non-fatal */ }
|
|
39
44
|
const mirror = createMirror(memoryDir);
|
|
40
45
|
const service = createService({ store, mirror, config: cfg });
|
|
41
46
|
|
package/lib/service.js
CHANGED
|
@@ -284,17 +284,25 @@ export function createService({ store, mirror, config, onWrite }) {
|
|
|
284
284
|
syncMirror();
|
|
285
285
|
notifyWrite();
|
|
286
286
|
},
|
|
287
|
-
update: (id, p) => {
|
|
287
|
+
update: (id, p, ctx = {}) => {
|
|
288
288
|
const old = store.getById(id);
|
|
289
289
|
const updated = store.update(id, p);
|
|
290
|
-
// Record a user correction
|
|
291
|
-
// reflection failure tracker is enabled
|
|
292
|
-
// actual = what it was before
|
|
293
|
-
|
|
290
|
+
// Record a user correction when any meaningful field changed and the
|
|
291
|
+
// reflection failure tracker is enabled. expected = what it became,
|
|
292
|
+
// actual = what it was before; query (when provided) captures the
|
|
293
|
+
// user's original intent so later reflection can reason about recall.
|
|
294
|
+
const hasMeaningfulChange = old && updated && (
|
|
295
|
+
old.content !== updated.content ||
|
|
296
|
+
old.title !== updated.title ||
|
|
297
|
+
old.importance !== updated.importance
|
|
298
|
+
);
|
|
299
|
+
if (hasMeaningfulChange && config.reflectionFailureTracking) {
|
|
294
300
|
store.saveFailure({
|
|
295
301
|
id: randomUUID(),
|
|
302
|
+
query: ctx.query ?? null,
|
|
296
303
|
expected: updated.content,
|
|
297
304
|
actual: old.content,
|
|
305
|
+
before: { title: old.title, content: old.content, importance: old.importance },
|
|
298
306
|
failure_type: "user_correction",
|
|
299
307
|
memory_id: id
|
|
300
308
|
});
|
package/lib/store.js
CHANGED
|
@@ -42,13 +42,16 @@ CREATE TABLE IF NOT EXISTS dream_runs (
|
|
|
42
42
|
CREATE INDEX IF NOT EXISTS idx_dream_runs_created ON dream_runs(created_at);
|
|
43
43
|
|
|
44
44
|
-- failure_memories: records user corrections / reflection failures. Captures
|
|
45
|
-
-- what a memory was (
|
|
45
|
+
-- what a memory was (actual) vs what the user changed it to (expected)
|
|
46
46
|
-- so later reflection passes can mine recurring correction patterns.
|
|
47
|
+
-- before holds a JSON snapshot of the pre-change title/content/importance,
|
|
48
|
+
-- so a title-only or importance-only correction is still traceable.
|
|
47
49
|
CREATE TABLE IF NOT EXISTS failure_memories (
|
|
48
50
|
id TEXT PRIMARY KEY,
|
|
49
51
|
query TEXT,
|
|
50
52
|
expected TEXT,
|
|
51
53
|
actual TEXT,
|
|
54
|
+
before TEXT,
|
|
52
55
|
failure_type TEXT NOT NULL,
|
|
53
56
|
memory_id TEXT,
|
|
54
57
|
created_at TEXT NOT NULL
|
|
@@ -405,13 +408,14 @@ export function createStore(path) {
|
|
|
405
408
|
* Like the dream audit trail this is bookkeeping: it never triggers write
|
|
406
409
|
* hooks, so reflection mining of failures cannot loop back into the writer.
|
|
407
410
|
*/
|
|
408
|
-
function saveFailure({ id, query, expected, actual, failure_type, memory_id }) {
|
|
411
|
+
function saveFailure({ id, query, expected, actual, before, failure_type, memory_id }) {
|
|
409
412
|
const now = nowIso();
|
|
413
|
+
const beforeJson = before && typeof before === "object" ? JSON.stringify(before) : (before ?? null);
|
|
410
414
|
db.prepare(
|
|
411
|
-
`INSERT INTO failure_memories (id, query, expected, actual, failure_type, memory_id, created_at)
|
|
412
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
413
|
-
).run(id ?? randomUUID(), query ?? null, expected ?? null, actual ?? null, failure_type, memory_id ?? null, now);
|
|
414
|
-
return { id, query, expected, actual, failure_type, memory_id, created_at: now };
|
|
415
|
+
`INSERT INTO failure_memories (id, query, expected, actual, before, failure_type, memory_id, created_at)
|
|
416
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
417
|
+
).run(id ?? randomUUID(), query ?? null, expected ?? null, actual ?? null, beforeJson, failure_type, memory_id ?? null, now);
|
|
418
|
+
return { id, query, expected, actual, before: before ?? null, failure_type, memory_id, created_at: now };
|
|
415
419
|
}
|
|
416
420
|
|
|
417
421
|
function listFailures({ limit = 50, offset = 0, since, memory_id, failure_type } = {}) {
|
|
@@ -423,7 +427,17 @@ export function createStore(path) {
|
|
|
423
427
|
const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
424
428
|
const lim = Number.isInteger(limit) && limit > 0 ? limit : 50;
|
|
425
429
|
const off = Number.isInteger(offset) && offset > 0 ? offset : 0;
|
|
426
|
-
return db.prepare(`SELECT * FROM failure_memories ${where} ORDER BY created_at DESC, id LIMIT ? OFFSET ?`).all(...params, lim, off)
|
|
430
|
+
return db.prepare(`SELECT * FROM failure_memories ${where} ORDER BY created_at DESC, id LIMIT ? OFFSET ?`).all(...params, lim, off)
|
|
431
|
+
.map((row) => {
|
|
432
|
+
let before;
|
|
433
|
+
try { before = row.before ? JSON.parse(row.before) : null; } catch { before = null; }
|
|
434
|
+
return { ...row, before };
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/** Delete failure rows older than `before` (ISO string). Returns count removed. */
|
|
439
|
+
function deleteOldFailures(before) {
|
|
440
|
+
return db.prepare("DELETE FROM failure_memories WHERE created_at < ?").run(before).changes;
|
|
427
441
|
}
|
|
428
442
|
|
|
429
443
|
function getFailureStats({ since } = {}) {
|
|
@@ -459,6 +473,7 @@ export function createStore(path) {
|
|
|
459
473
|
saveFailure,
|
|
460
474
|
listFailures,
|
|
461
475
|
getFailureStats,
|
|
476
|
+
deleteOldFailures,
|
|
462
477
|
close() {
|
|
463
478
|
db.close();
|
|
464
479
|
}
|
package/lib/tools.js
CHANGED
|
@@ -133,7 +133,8 @@ export function createTools(ctx, service, config, embedder) {
|
|
|
133
133
|
content: { type: "string" },
|
|
134
134
|
type: { type: "string", enum: ["preference", "project", "decision", "history"] },
|
|
135
135
|
tags: { type: "array", items: { type: "string" } },
|
|
136
|
-
importance: { type: "integer", description: "1-5" }
|
|
136
|
+
importance: { type: "integer", description: "1-5" },
|
|
137
|
+
reason: { type: "string", description: "Optional context for the correction (what the user actually said/wanted), recorded for reflection" }
|
|
137
138
|
},
|
|
138
139
|
output: {
|
|
139
140
|
schema: {
|
|
@@ -160,7 +161,7 @@ export function createTools(ctx, service, config, embedder) {
|
|
|
160
161
|
type: args.type,
|
|
161
162
|
tags: args.tags,
|
|
162
163
|
importance: args.importance
|
|
163
|
-
});
|
|
164
|
+
}, { query: args.reason });
|
|
164
165
|
return { memory: { id: memory.id, title: memory.title, content: memory.content } };
|
|
165
166
|
}
|
|
166
167
|
}),
|
package/lib/vector-index.js
CHANGED
|
@@ -73,7 +73,7 @@ export function createVectorIndex({ store, logger }) {
|
|
|
73
73
|
|
|
74
74
|
/** Re-embed every row missing an embedding. Returns indexed count. */
|
|
75
75
|
async rebuildIndex(embedder, { limit = 1000 } = {}) {
|
|
76
|
-
if (!embedder || typeof embedder.
|
|
76
|
+
if (!embedder || typeof embedder.embedSingle !== "function") return { indexed: 0, skipped: 0 };
|
|
77
77
|
const rows = store.needsEmbedding(limit);
|
|
78
78
|
let indexed = 0;
|
|
79
79
|
for (const row of rows) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modusensus/dsh-mneme",
|
|
3
3
|
"description": "Cross-session memory plugin for DeepSeek Harness with autoDream consolidation: SQLite store, Markdown mirrors, 6 model tools, automatic injection, session summarization, user profile/rules, custom slash commands, vector (semantic) search, and a Web GUI panel",
|
|
4
|
-
"version": "0.2.
|
|
4
|
+
"version": "0.2.3",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "lib/index.js",
|
package/src/api.js
CHANGED
|
@@ -71,7 +71,15 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
71
71
|
const url = new URL(req.url, "http://localhost");
|
|
72
72
|
const q = url.searchParams.get("q") ?? "";
|
|
73
73
|
const limit = Number(url.searchParams.get("limit") ?? 20);
|
|
74
|
-
// mode
|
|
74
|
+
// mode selects the recall strategy (defaults to auto):
|
|
75
|
+
// auto (default) keyword first, vector fills remaining slots
|
|
76
|
+
// hybrid vector first, keyword fills remaining slots; scores of
|
|
77
|
+
// memories hit by both sides are weight-blended
|
|
78
|
+
// vector vector only, falls back to keyword when the vector path
|
|
79
|
+
// is unavailable (no embedder or a throwing one)
|
|
80
|
+
// keyword literal text only; never queries the embedder
|
|
81
|
+
// rerank=false disables the cross-encoder reorder for this request;
|
|
82
|
+
// the response `mode` field reports which path actually produced rows.
|
|
75
83
|
const mode = url.searchParams.get("mode") ?? "auto";
|
|
76
84
|
const rerank = url.searchParams.get("rerank") !== "false";
|
|
77
85
|
const query = q.trim();
|
package/src/dream/decisions.js
CHANGED
|
@@ -30,6 +30,36 @@ export function validateDecisions(decisions, snapshot, options = {}) {
|
|
|
30
30
|
errors.push(`${at}: ${d.action} needs non-empty ids`);
|
|
31
31
|
continue;
|
|
32
32
|
}
|
|
33
|
+
// update-specific field validation runs BEFORE claiming ids, so a failing
|
|
34
|
+
// update never pollutes the claimed set (which drives the "every id must
|
|
35
|
+
// appear in a decision" check below).
|
|
36
|
+
if (d.action === "update") {
|
|
37
|
+
// 只能更新单条
|
|
38
|
+
if (!Array.isArray(d.ids) || d.ids.length !== 1) {
|
|
39
|
+
errors.push(`${at}: update must target exactly one id`);
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
// 必须产生实际变化
|
|
43
|
+
const mem = snapshot.get(d.ids[0]);
|
|
44
|
+
const hasChange = (d.title !== undefined && d.title !== mem?.title)
|
|
45
|
+
|| (d.content !== undefined && d.content !== mem?.content)
|
|
46
|
+
|| (d.importance !== undefined && d.importance !== mem?.importance);
|
|
47
|
+
if (!hasChange) {
|
|
48
|
+
errors.push(`${at}: update must change at least one field`);
|
|
49
|
+
continue;
|
|
50
|
+
}
|
|
51
|
+
// 不能更新 summary
|
|
52
|
+
if (mem?.type === "summary") {
|
|
53
|
+
errors.push(`${at}: cannot update summary via update action`);
|
|
54
|
+
continue;
|
|
55
|
+
}
|
|
56
|
+
// 保护期:新建记忆不可立即被 update(可配置)
|
|
57
|
+
const ageHours = (Date.now() - new Date(mem?.created_at).getTime()) / 3600000;
|
|
58
|
+
if (ageHours < minAgeHours) {
|
|
59
|
+
errors.push(`${at}: memory too young (< ${minAgeHours}h)`);
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
33
63
|
for (const id of ids) {
|
|
34
64
|
const mem = snapshot.get(id);
|
|
35
65
|
if (!mem) {
|
|
@@ -59,33 +89,6 @@ export function validateDecisions(decisions, snapshot, options = {}) {
|
|
|
59
89
|
errors.push(`${at}: merge ids span multiple types (${[...mergeTypes].join(", ")})`);
|
|
60
90
|
}
|
|
61
91
|
}
|
|
62
|
-
if (d.action === "update") {
|
|
63
|
-
// 只能更新单条
|
|
64
|
-
if (!Array.isArray(d.ids) || d.ids.length !== 1) {
|
|
65
|
-
errors.push(`${at}: update must target exactly one id`);
|
|
66
|
-
continue;
|
|
67
|
-
}
|
|
68
|
-
// 必须产生实际变化
|
|
69
|
-
const mem = snapshot.get(d.ids[0]);
|
|
70
|
-
const hasChange = (d.title !== undefined && d.title !== mem?.title)
|
|
71
|
-
|| (d.content !== undefined && d.content !== mem?.content)
|
|
72
|
-
|| (d.importance !== undefined && d.importance !== mem?.importance);
|
|
73
|
-
if (!hasChange) {
|
|
74
|
-
errors.push(`${at}: update must change at least one field`);
|
|
75
|
-
continue;
|
|
76
|
-
}
|
|
77
|
-
// 不能更新 summary
|
|
78
|
-
if (mem?.type === "summary") {
|
|
79
|
-
errors.push(`${at}: cannot update summary via update action`);
|
|
80
|
-
continue;
|
|
81
|
-
}
|
|
82
|
-
// 保护期:新建记忆不可立即被 update(可配置)
|
|
83
|
-
const ageHours = (Date.now() - new Date(mem?.created_at).getTime()) / 3600000;
|
|
84
|
-
if (ageHours < minAgeHours) {
|
|
85
|
-
errors.push(`${at}: memory too young (< ${minAgeHours}h)`);
|
|
86
|
-
continue;
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
92
|
}
|
|
90
93
|
// Cap update churn: too many edits in one cycle signals a runaway model
|
|
91
94
|
const updateCount = decisions.filter((d) => d.action === "update").length;
|
package/src/index.js
CHANGED
|
@@ -36,6 +36,11 @@ export const apply = (ctx, config) => {
|
|
|
36
36
|
mkdirSync(memoryDir, { recursive: true });
|
|
37
37
|
|
|
38
38
|
const store = createStore(join(memoryDir, "memory.db"));
|
|
39
|
+
// Prune reflection failure rows older than 90 days on boot (best-effort, so
|
|
40
|
+
// the failure table never grows unbounded).
|
|
41
|
+
try {
|
|
42
|
+
store.deleteOldFailures(new Date(Date.now() - 90 * 86400000).toISOString());
|
|
43
|
+
} catch { /* non-fatal */ }
|
|
39
44
|
const mirror = createMirror(memoryDir);
|
|
40
45
|
const service = createService({ store, mirror, config: cfg });
|
|
41
46
|
|
package/src/service.js
CHANGED
|
@@ -284,17 +284,25 @@ export function createService({ store, mirror, config, onWrite }) {
|
|
|
284
284
|
syncMirror();
|
|
285
285
|
notifyWrite();
|
|
286
286
|
},
|
|
287
|
-
update: (id, p) => {
|
|
287
|
+
update: (id, p, ctx = {}) => {
|
|
288
288
|
const old = store.getById(id);
|
|
289
289
|
const updated = store.update(id, p);
|
|
290
|
-
// Record a user correction
|
|
291
|
-
// reflection failure tracker is enabled
|
|
292
|
-
// actual = what it was before
|
|
293
|
-
|
|
290
|
+
// Record a user correction when any meaningful field changed and the
|
|
291
|
+
// reflection failure tracker is enabled. expected = what it became,
|
|
292
|
+
// actual = what it was before; query (when provided) captures the
|
|
293
|
+
// user's original intent so later reflection can reason about recall.
|
|
294
|
+
const hasMeaningfulChange = old && updated && (
|
|
295
|
+
old.content !== updated.content ||
|
|
296
|
+
old.title !== updated.title ||
|
|
297
|
+
old.importance !== updated.importance
|
|
298
|
+
);
|
|
299
|
+
if (hasMeaningfulChange && config.reflectionFailureTracking) {
|
|
294
300
|
store.saveFailure({
|
|
295
301
|
id: randomUUID(),
|
|
302
|
+
query: ctx.query ?? null,
|
|
296
303
|
expected: updated.content,
|
|
297
304
|
actual: old.content,
|
|
305
|
+
before: { title: old.title, content: old.content, importance: old.importance },
|
|
298
306
|
failure_type: "user_correction",
|
|
299
307
|
memory_id: id
|
|
300
308
|
});
|
package/src/store.js
CHANGED
|
@@ -42,13 +42,16 @@ CREATE TABLE IF NOT EXISTS dream_runs (
|
|
|
42
42
|
CREATE INDEX IF NOT EXISTS idx_dream_runs_created ON dream_runs(created_at);
|
|
43
43
|
|
|
44
44
|
-- failure_memories: records user corrections / reflection failures. Captures
|
|
45
|
-
-- what a memory was (
|
|
45
|
+
-- what a memory was (actual) vs what the user changed it to (expected)
|
|
46
46
|
-- so later reflection passes can mine recurring correction patterns.
|
|
47
|
+
-- before holds a JSON snapshot of the pre-change title/content/importance,
|
|
48
|
+
-- so a title-only or importance-only correction is still traceable.
|
|
47
49
|
CREATE TABLE IF NOT EXISTS failure_memories (
|
|
48
50
|
id TEXT PRIMARY KEY,
|
|
49
51
|
query TEXT,
|
|
50
52
|
expected TEXT,
|
|
51
53
|
actual TEXT,
|
|
54
|
+
before TEXT,
|
|
52
55
|
failure_type TEXT NOT NULL,
|
|
53
56
|
memory_id TEXT,
|
|
54
57
|
created_at TEXT NOT NULL
|
|
@@ -405,13 +408,14 @@ export function createStore(path) {
|
|
|
405
408
|
* Like the dream audit trail this is bookkeeping: it never triggers write
|
|
406
409
|
* hooks, so reflection mining of failures cannot loop back into the writer.
|
|
407
410
|
*/
|
|
408
|
-
function saveFailure({ id, query, expected, actual, failure_type, memory_id }) {
|
|
411
|
+
function saveFailure({ id, query, expected, actual, before, failure_type, memory_id }) {
|
|
409
412
|
const now = nowIso();
|
|
413
|
+
const beforeJson = before && typeof before === "object" ? JSON.stringify(before) : (before ?? null);
|
|
410
414
|
db.prepare(
|
|
411
|
-
`INSERT INTO failure_memories (id, query, expected, actual, failure_type, memory_id, created_at)
|
|
412
|
-
VALUES (?, ?, ?, ?, ?, ?, ?)`
|
|
413
|
-
).run(id ?? randomUUID(), query ?? null, expected ?? null, actual ?? null, failure_type, memory_id ?? null, now);
|
|
414
|
-
return { id, query, expected, actual, failure_type, memory_id, created_at: now };
|
|
415
|
+
`INSERT INTO failure_memories (id, query, expected, actual, before, failure_type, memory_id, created_at)
|
|
416
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`
|
|
417
|
+
).run(id ?? randomUUID(), query ?? null, expected ?? null, actual ?? null, beforeJson, failure_type, memory_id ?? null, now);
|
|
418
|
+
return { id, query, expected, actual, before: before ?? null, failure_type, memory_id, created_at: now };
|
|
415
419
|
}
|
|
416
420
|
|
|
417
421
|
function listFailures({ limit = 50, offset = 0, since, memory_id, failure_type } = {}) {
|
|
@@ -423,7 +427,17 @@ export function createStore(path) {
|
|
|
423
427
|
const where = clauses.length ? `WHERE ${clauses.join(" AND ")}` : "";
|
|
424
428
|
const lim = Number.isInteger(limit) && limit > 0 ? limit : 50;
|
|
425
429
|
const off = Number.isInteger(offset) && offset > 0 ? offset : 0;
|
|
426
|
-
return db.prepare(`SELECT * FROM failure_memories ${where} ORDER BY created_at DESC, id LIMIT ? OFFSET ?`).all(...params, lim, off)
|
|
430
|
+
return db.prepare(`SELECT * FROM failure_memories ${where} ORDER BY created_at DESC, id LIMIT ? OFFSET ?`).all(...params, lim, off)
|
|
431
|
+
.map((row) => {
|
|
432
|
+
let before;
|
|
433
|
+
try { before = row.before ? JSON.parse(row.before) : null; } catch { before = null; }
|
|
434
|
+
return { ...row, before };
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/** Delete failure rows older than `before` (ISO string). Returns count removed. */
|
|
439
|
+
function deleteOldFailures(before) {
|
|
440
|
+
return db.prepare("DELETE FROM failure_memories WHERE created_at < ?").run(before).changes;
|
|
427
441
|
}
|
|
428
442
|
|
|
429
443
|
function getFailureStats({ since } = {}) {
|
|
@@ -459,6 +473,7 @@ export function createStore(path) {
|
|
|
459
473
|
saveFailure,
|
|
460
474
|
listFailures,
|
|
461
475
|
getFailureStats,
|
|
476
|
+
deleteOldFailures,
|
|
462
477
|
close() {
|
|
463
478
|
db.close();
|
|
464
479
|
}
|
package/src/tools.js
CHANGED
|
@@ -133,7 +133,8 @@ export function createTools(ctx, service, config, embedder) {
|
|
|
133
133
|
content: { type: "string" },
|
|
134
134
|
type: { type: "string", enum: ["preference", "project", "decision", "history"] },
|
|
135
135
|
tags: { type: "array", items: { type: "string" } },
|
|
136
|
-
importance: { type: "integer", description: "1-5" }
|
|
136
|
+
importance: { type: "integer", description: "1-5" },
|
|
137
|
+
reason: { type: "string", description: "Optional context for the correction (what the user actually said/wanted), recorded for reflection" }
|
|
137
138
|
},
|
|
138
139
|
output: {
|
|
139
140
|
schema: {
|
|
@@ -160,7 +161,7 @@ export function createTools(ctx, service, config, embedder) {
|
|
|
160
161
|
type: args.type,
|
|
161
162
|
tags: args.tags,
|
|
162
163
|
importance: args.importance
|
|
163
|
-
});
|
|
164
|
+
}, { query: args.reason });
|
|
164
165
|
return { memory: { id: memory.id, title: memory.title, content: memory.content } };
|
|
165
166
|
}
|
|
166
167
|
}),
|
package/src/vector-index.js
CHANGED
|
@@ -73,7 +73,7 @@ export function createVectorIndex({ store, logger }) {
|
|
|
73
73
|
|
|
74
74
|
/** Re-embed every row missing an embedding. Returns indexed count. */
|
|
75
75
|
async rebuildIndex(embedder, { limit = 1000 } = {}) {
|
|
76
|
-
if (!embedder || typeof embedder.
|
|
76
|
+
if (!embedder || typeof embedder.embedSingle !== "function") return { indexed: 0, skipped: 0 };
|
|
77
77
|
const rows = store.needsEmbedding(limit);
|
|
78
78
|
let indexed = 0;
|
|
79
79
|
for (const row of rows) {
|