@modusensus/dsh-mneme 0.8.1 → 0.8.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 +14 -3
- package/lib/api-standalone.js +12 -0
- package/lib/api.js +12 -2
- package/lib/client.js +5 -1
- package/lib/config.js +18 -0
- package/lib/dream/decisions.js +55 -8
- package/lib/dream/sleep.js +36 -5
- package/lib/dream.js +18 -7
- package/lib/entities/extractor.js +2 -2
- package/lib/index.js +8 -0
- package/lib/inject.js +43 -1
- package/lib/lang.js +2 -2
- package/lib/local-embedder.js +21 -1
- package/lib/reranker.js +47 -8
- package/lib/runtime/model-fetch.js +361 -0
- package/lib/service.js +37 -5
- package/lib/settings.js +1 -0
- package/lib/store.js +85 -25
- package/lib/summarize.js +2 -2
- package/lib/tools.js +6 -19
- package/lib/vector-index.js +2 -8
- package/package.json +2 -1
- package/scripts/sync-test-badge.mjs +61 -0
- package/src/api-standalone.js +12 -0
- package/src/api.js +12 -2
- package/src/config.js +18 -0
- package/src/dream/decisions.js +55 -8
- package/src/dream/sleep.js +36 -5
- package/src/dream.js +18 -7
- package/src/entities/extractor.js +2 -2
- package/src/index.js +8 -0
- package/src/inject.js +43 -1
- package/src/lang.js +2 -2
- package/src/local-embedder.js +21 -1
- package/src/reranker.js +47 -8
- package/src/runtime/model-fetch.js +361 -0
- package/src/service.js +37 -5
- package/src/settings.js +1 -0
- package/src/store.js +85 -25
- package/src/summarize.js +2 -2
- package/src/tools.js +6 -19
- package/src/vector-index.js +2 -8
- package/test/api.test.js +10 -4
- package/test/dream-archive-guardrail.test.js +116 -0
- package/test/dream-coverage-degrade.test.js +86 -0
- package/test/dream.test.js +15 -11
- package/test/inject-first-round-bm25.test.js +89 -0
- package/test/inject-rotation.test.js +161 -0
- package/test/llm-message-source.test.js +177 -0
- package/test/local-embedder.test.js +20 -0
- package/test/reranker-single-logit.test.js +73 -0
- package/test/runtime-model-fetch.test.js +348 -0
- package/test/skip-invalid-restore.test.js +7 -5
- package/test/sleep-scheduler-rearm.test.js +89 -0
- package/test/sleep.test.js +2 -2
- package/test/standalone-api.test.js +34 -1
- package/test/store-vector-cache.test.js +98 -0
- package/test/tools-memory-get-schema.test.js +65 -0
package/lib/store.js
CHANGED
|
@@ -22,6 +22,9 @@ CREATE TABLE IF NOT EXISTS memories (
|
|
|
22
22
|
);
|
|
23
23
|
CREATE INDEX IF NOT EXISTS idx_memories_type ON memories(type);
|
|
24
24
|
CREATE INDEX IF NOT EXISTS idx_memories_importance ON memories(importance);
|
|
25
|
+
-- Issue #202:all()/list() 按 updated_at 排序,无索引时走临时 B 树全表排序
|
|
26
|
+
-- (5k 行实测 all() 231ms → 135ms)。IF NOT EXISTS 幂等,存量库启动即建。
|
|
27
|
+
CREATE INDEX IF NOT EXISTS idx_memories_updated ON memories(updated_at);
|
|
25
28
|
|
|
26
29
|
-- autoDream audit trail: one row per consolidation run, capturing the exact
|
|
27
30
|
-- input snapshot digest + the LLM decision list + per-id outcome + a compact
|
|
@@ -717,11 +720,51 @@ export function createStore(path) {
|
|
|
717
720
|
}
|
|
718
721
|
}
|
|
719
722
|
|
|
723
|
+
// Issue #202:embedding 解析缓存。检索路径此前每次调用都对全部带向量行
|
|
724
|
+
// JSON.parse(993 × 512 维 ≈ 48 ms/次,且 SQL 拖着 10 MB 的 TEXT),余弦本身
|
|
725
|
+
// 只占 3%。向量只经 setEmbedding 写入(单一失效点),把解析结果按 id 缓存,
|
|
726
|
+
// 命中即零解析;上限 FIFO 驱逐防大库内存无界(4000 × 512 维 double ≈ 16 MB)。
|
|
727
|
+
const embeddingCache = new Map();
|
|
728
|
+
const EMBEDDING_CACHE_MAX = 4000;
|
|
729
|
+
|
|
730
|
+
function cacheEmbedding(id, vector) {
|
|
731
|
+
if (embeddingCache.size >= EMBEDDING_CACHE_MAX && !embeddingCache.has(id)) {
|
|
732
|
+
const oldest = embeddingCache.keys().next().value;
|
|
733
|
+
embeddingCache.delete(oldest);
|
|
734
|
+
}
|
|
735
|
+
embeddingCache.set(id, vector);
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
/** Parsed embedding for one id (cache-first); undefined when none/parse
|
|
739
|
+
* failure. Single read path shared by searchVector / getEmbedding(s). */
|
|
740
|
+
function getParsedEmbedding(id) {
|
|
741
|
+
const cached = embeddingCache.get(id);
|
|
742
|
+
if (cached) return cached;
|
|
743
|
+
const row = db.prepare("SELECT embedding FROM memories WHERE id = ?").get(id);
|
|
744
|
+
if (!row?.embedding) return undefined;
|
|
745
|
+
try {
|
|
746
|
+
const v = JSON.parse(row.embedding);
|
|
747
|
+
if (Array.isArray(v) && v.length) {
|
|
748
|
+
cacheEmbedding(id, v);
|
|
749
|
+
return v;
|
|
750
|
+
}
|
|
751
|
+
} catch { /* corrupt embedding text: treated as absent */ }
|
|
752
|
+
return undefined;
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
// all()/list() 的显式列清单:从实际 schema 派生(迁移后),只排除 embedding
|
|
756
|
+
// 列——consolidation/sleep/inject 的消费方都不读向量,而它是全表里最重的一列
|
|
757
|
+
// (5k 行库 ≈ 9.4 MB TEXT),每次全表调用都白搬。经 PRAGMA 派生而非硬编码,
|
|
758
|
+
// schema 演进时自动跟随。
|
|
759
|
+
const memoryColumns = db.prepare("PRAGMA table_info(memories)").all()
|
|
760
|
+
.map((c) => c.name)
|
|
761
|
+
.filter((n) => n !== "embedding");
|
|
762
|
+
const memoryColumnList = memoryColumns.join(", ");
|
|
763
|
+
|
|
720
764
|
// Per-instance monotonic timestamp guard: consecutive writes within the same
|
|
721
765
|
// millisecond must still produce strictly increasing timestamps (test asserts
|
|
722
766
|
// updated_at != created_at). State lives in the store closure, not module scope.
|
|
723
|
-
let lastTs = "";
|
|
724
|
-
function nowIso() {
|
|
767
|
+
let lastTs = ""; function nowIso() {
|
|
725
768
|
let ts = new Date().toISOString();
|
|
726
769
|
if (lastTs && ts <= lastTs) {
|
|
727
770
|
const d = new Date(lastTs);
|
|
@@ -925,6 +968,10 @@ export function createStore(path) {
|
|
|
925
968
|
now,
|
|
926
969
|
id
|
|
927
970
|
);
|
|
971
|
+
// Issue #202:update 可直写 embedding(patch.embedding),缓存条目失效,
|
|
972
|
+
// 下次读取按库值重解析。宁滥勿缺——即便本 patch 未含 embedding,丢一条
|
|
973
|
+
// 缓存只多一次点查重解析。
|
|
974
|
+
embeddingCache.delete(id);
|
|
928
975
|
// Desired generation bumped in the same transaction as the update (peer
|
|
929
976
|
// blocker 1: crash between write and sync must still be recoverable).
|
|
930
977
|
incrementGeneration();
|
|
@@ -995,6 +1042,8 @@ export function createStore(path) {
|
|
|
995
1042
|
expectedUpdatedAt
|
|
996
1043
|
);
|
|
997
1044
|
if (result.changes === 0) return; // CAS miss: a concurrent write won
|
|
1045
|
+
// Issue #202:同 update——CAS 命中即可能改写 embedding,失效缓存条目。
|
|
1046
|
+
embeddingCache.delete(id);
|
|
998
1047
|
// Only bump desired generation on a successful CAS — a miss writes nothing.
|
|
999
1048
|
incrementGeneration();
|
|
1000
1049
|
applied = true;
|
|
@@ -1177,7 +1226,9 @@ export function createStore(path) {
|
|
|
1177
1226
|
}
|
|
1178
1227
|
|
|
1179
1228
|
function all() {
|
|
1180
|
-
|
|
1229
|
+
// Issue #202:显式列清单排除 embedding——toRow 本就不输出向量,SELECT *
|
|
1230
|
+
// 纯属把全表最重的列(9.4 MB TEXT/5k 行)拖进内存再丢弃。
|
|
1231
|
+
const rows = db.prepare(`SELECT ${memoryColumnList} FROM memories ORDER BY updated_at DESC`).all();
|
|
1181
1232
|
return rows.map(toRow);
|
|
1182
1233
|
}
|
|
1183
1234
|
|
|
@@ -1185,6 +1236,17 @@ export function createStore(path) {
|
|
|
1185
1236
|
function setEmbedding(id, vector) {
|
|
1186
1237
|
const json = Array.isArray(vector) && vector.length ? JSON.stringify(vector) : null;
|
|
1187
1238
|
db.prepare("UPDATE memories SET embedding = ? WHERE id = ?").run(json, id);
|
|
1239
|
+
// Issue #202:单一写入口即单一失效点——重写缓存条目(向量在手上不必重解析),
|
|
1240
|
+
// 清空则移除,删除过的 id 不会被下一次 searchVector 的 id 集合选中。
|
|
1241
|
+
if (json) {
|
|
1242
|
+
try {
|
|
1243
|
+
const parsed = JSON.parse(json);
|
|
1244
|
+
if (Array.isArray(parsed) && parsed.length) cacheEmbedding(id, parsed);
|
|
1245
|
+
else embeddingCache.delete(id);
|
|
1246
|
+
} catch { embeddingCache.delete(id); }
|
|
1247
|
+
} else {
|
|
1248
|
+
embeddingCache.delete(id);
|
|
1249
|
+
}
|
|
1188
1250
|
}
|
|
1189
1251
|
|
|
1190
1252
|
/** Batch fetch stored embeddings by id (v0.5.0 search-time semantic dedup).
|
|
@@ -1193,19 +1255,10 @@ export function createStore(path) {
|
|
|
1193
1255
|
function getEmbeddings(ids) {
|
|
1194
1256
|
const out = new Map();
|
|
1195
1257
|
const list = (Array.isArray(ids) ? ids : []).filter(Boolean);
|
|
1196
|
-
for (
|
|
1197
|
-
|
|
1198
|
-
const
|
|
1199
|
-
|
|
1200
|
-
WHERE embedding IS NOT NULL AND embedding != ''
|
|
1201
|
-
AND id IN (${chunk.map(() => "?").join(",")})`
|
|
1202
|
-
).all(...chunk);
|
|
1203
|
-
for (const row of rows) {
|
|
1204
|
-
try {
|
|
1205
|
-
const vec = JSON.parse(row.embedding);
|
|
1206
|
-
if (Array.isArray(vec) && vec.length) out.set(row.id, vec);
|
|
1207
|
-
} catch { /* corrupt row: skip */ }
|
|
1208
|
-
}
|
|
1258
|
+
for (const id of list) {
|
|
1259
|
+
// Issue #202:走同一解析缓存(语义去重的批量读也免重复 parse)。
|
|
1260
|
+
const vec = getParsedEmbedding(id);
|
|
1261
|
+
if (vec) out.set(id, vec);
|
|
1209
1262
|
}
|
|
1210
1263
|
return out;
|
|
1211
1264
|
}
|
|
@@ -1280,24 +1333,30 @@ export function createStore(path) {
|
|
|
1280
1333
|
function searchVector(vector, { limit = 20, includeArchived = false, threshold = 0 } = {}) {
|
|
1281
1334
|
if (!Array.isArray(vector) || !vector.length) return [];
|
|
1282
1335
|
const archivedFilter = includeArchived ? "" : "archived = 0 AND ";
|
|
1336
|
+
// Issue #202:只取 id 列(~40 KB)而非 SELECT *(~11 MB)——向量从缓存取,
|
|
1337
|
+
// 缓存未命中时按 id 点查 embedding 再解析回填。SQL 里保留 embedding 的
|
|
1338
|
+
// IS NOT NULL 过滤,保证「有向量的行」集合始终以库为准(删除/清空即时生效)。
|
|
1283
1339
|
const rows = db.prepare(
|
|
1284
|
-
`SELECT
|
|
1340
|
+
`SELECT id FROM memories
|
|
1285
1341
|
WHERE ${archivedFilter}forgotten = 0 AND embedding IS NOT NULL AND embedding != ''`
|
|
1286
1342
|
).all();
|
|
1287
1343
|
const scored = [];
|
|
1288
1344
|
for (const row of rows) {
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
v = JSON.parse(row.embedding);
|
|
1292
|
-
} catch {
|
|
1293
|
-
continue;
|
|
1294
|
-
}
|
|
1345
|
+
const v = getParsedEmbedding(row.id);
|
|
1346
|
+
if (!v) continue;
|
|
1295
1347
|
const score = cosine(vector, v);
|
|
1296
|
-
if (score >= threshold) scored.push({ row, score });
|
|
1348
|
+
if (score >= threshold) scored.push({ row: { id: row.id }, score });
|
|
1297
1349
|
}
|
|
1298
1350
|
scored.sort((a, b) => b.score - a.score);
|
|
1299
1351
|
const { limit: lim } = sanitizePage(limit, 0, 20);
|
|
1300
|
-
|
|
1352
|
+
// Top-N 回表:只对入选行取完整字段(主键点查 ≤ lim 行),评分别拖着正文走。
|
|
1353
|
+
const getFull = db.prepare("SELECT * FROM memories WHERE id = ?");
|
|
1354
|
+
const out = [];
|
|
1355
|
+
for (const { row, score } of scored.slice(0, lim)) {
|
|
1356
|
+
const full = getFull.get(row.id);
|
|
1357
|
+
if (full) out.push({ ...toRow(full), score });
|
|
1358
|
+
}
|
|
1359
|
+
return out;
|
|
1301
1360
|
}
|
|
1302
1361
|
|
|
1303
1362
|
// --- autoDream audit trail ----------------------------------------------
|
|
@@ -2245,6 +2304,7 @@ export function createStore(path) {
|
|
|
2245
2304
|
search,
|
|
2246
2305
|
setEmbedding,
|
|
2247
2306
|
getEmbeddings,
|
|
2307
|
+
getParsedEmbedding,
|
|
2248
2308
|
embeddedCount,
|
|
2249
2309
|
needsEmbedding,
|
|
2250
2310
|
searchVector,
|
package/lib/summarize.js
CHANGED
|
@@ -144,7 +144,7 @@ function collectMessages(session, maxChars = 8000, language = "zh") {
|
|
|
144
144
|
break;
|
|
145
145
|
}
|
|
146
146
|
}
|
|
147
|
-
return lines.length ? [createUserMessage({ content: [{ type: "text", text: trim(lines.join("\n"), maxChars) }] })] : [];
|
|
147
|
+
return lines.length ? [createUserMessage({ content: [{ type: "text", text: trim(lines.join("\n"), maxChars) }], source: { kind: "plugin", plugin: "dsh-mneme" } })] : [];
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
// ── 智能调速器(429 保护)─────────────────────────────────────────────
|
|
@@ -265,7 +265,7 @@ export function createSummarizer(ctx, service, config) {
|
|
|
265
265
|
model: route.model,
|
|
266
266
|
purpose: "summarization",
|
|
267
267
|
messages: [
|
|
268
|
-
{ role: "system", content: [{ type: "text", text: config.codingRetrospect ? STR.prompts.codingSummary[langOf(config)] : STR.prompts.summary[langOf(config)] }] },
|
|
268
|
+
{ role: "system", content: [{ type: "text", text: config.codingRetrospect ? STR.prompts.codingSummary[langOf(config)] : STR.prompts.summary[langOf(config)] }], source: { kind: "plugin", plugin: "dsh-mneme" } },
|
|
269
269
|
...messages
|
|
270
270
|
],
|
|
271
271
|
signal: controller.signal
|
package/lib/tools.js
CHANGED
|
@@ -39,6 +39,8 @@ const MEMORY_ITEM_SCHEMA = {
|
|
|
39
39
|
updated_at: { type: "string", required: true }
|
|
40
40
|
}
|
|
41
41
|
};
|
|
42
|
+
// 导出供回归测试断言 memory_get 与共享 schema 同源(#184 防漂移护栏)。
|
|
43
|
+
export { MEMORY_ITEM_SCHEMA };
|
|
42
44
|
|
|
43
45
|
// v0.8.0 A2:occurred 时间窗参数 → store 过滤选项(list/count 同口径);
|
|
44
46
|
// 未传参返回空对象,既有调用方不受影响。(A3 复核:此处入参仅含 occurred 边界。)
|
|
@@ -277,25 +279,10 @@ export function createTools(ctx, service, config, embedder) {
|
|
|
277
279
|
type: "object",
|
|
278
280
|
additionalProperties: false,
|
|
279
281
|
properties: {
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
id: { type: "string", required: true },
|
|
285
|
-
title: { type: "string", required: true },
|
|
286
|
-
type: { type: "string", required: true },
|
|
287
|
-
importance: { type: "integer", required: true },
|
|
288
|
-
tags: { type: "array", items: { type: "string" } },
|
|
289
|
-
content: { type: "string", required: true },
|
|
290
|
-
source: { type: "string" },
|
|
291
|
-
agent_scope: { type: "string" },
|
|
292
|
-
workspace_scope: { type: "string" },
|
|
293
|
-
sensitivity: { type: "string" },
|
|
294
|
-
occurred_at: { type: "string" },
|
|
295
|
-
created_at: { type: "string" },
|
|
296
|
-
updated_at: { type: "string" }
|
|
297
|
-
}
|
|
298
|
-
}
|
|
282
|
+
// 与 memory_search / memory_list 共用同一份 item schema——DTO 由
|
|
283
|
+
// toApiList 统一产出,内联副本曾漏掉 v0.8.1 的 scope 来源三键,
|
|
284
|
+
// 任何被标注过的行都过不了 in-process 校验(#184)。
|
|
285
|
+
memory: MEMORY_ITEM_SCHEMA
|
|
299
286
|
}
|
|
300
287
|
},
|
|
301
288
|
render: (_args, value) => {
|
package/lib/vector-index.js
CHANGED
|
@@ -56,14 +56,8 @@ export function createVectorIndex({ store, logger }) {
|
|
|
56
56
|
|
|
57
57
|
/** Read one memory's cached vector; undefined when none/parse failure. */
|
|
58
58
|
getEmbedding(id) {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
try {
|
|
62
|
-
const v = JSON.parse(row.embedding);
|
|
63
|
-
return Array.isArray(v) && v.length ? v : undefined;
|
|
64
|
-
} catch {
|
|
65
|
-
return undefined;
|
|
66
|
-
}
|
|
59
|
+
// Issue #202:走 store 的解析缓存(与 searchVector/getEmbeddings 同源)。
|
|
60
|
+
return store.getParsedEmbedding(id);
|
|
67
61
|
},
|
|
68
62
|
|
|
69
63
|
/** Cosine search over embedded rows; results carry a 0..1 score. */
|
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, 7 model tools, automatic injection, session summarization, user profile/rules, custom slash commands, vector (semantic) search, and a Web GUI panel",
|
|
4
|
-
"version": "0.8.
|
|
4
|
+
"version": "0.8.3",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
7
7
|
"type": "git",
|
|
@@ -80,6 +80,7 @@
|
|
|
80
80
|
"prepack": "npm run sync",
|
|
81
81
|
"test": "node --test test/*.test.js",
|
|
82
82
|
"test:coverage": "c8 node --test test/*.test.js",
|
|
83
|
+
"badge:sync": "node scripts/sync-test-badge.mjs",
|
|
83
84
|
"e2e": "node scripts/e2e-dsh.js",
|
|
84
85
|
"stress": "node scripts/stress-dsh.js"
|
|
85
86
|
},
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
// 发版流水线的徽章对齐(release.yml 的 badge job 调用;手动可跑 npm run badge:sync)。
|
|
2
|
+
//
|
|
3
|
+
// 做三件事:
|
|
4
|
+
// 1. 跑一遍全量测试(npm test),取最后的 `pass N` 汇总数;
|
|
5
|
+
// 2. 把双 README 的 tests 徽章(img.shields.io/badge/tests-N%20passed)与
|
|
6
|
+
// 开发命令注释(# N 个测试 / # N tests)刷成该数字;
|
|
7
|
+
// 3. 有变化就写回文件(由调用方决定是否 commit/push),无变化静默退出。
|
|
8
|
+
//
|
|
9
|
+
// 测试失败(fail>0 或进程非零退出)时以非零退出且不改任何文件——徽章永远
|
|
10
|
+
// 不可能指向一次没跑绿的套件。历史版本表里的「N 测试全绿」是各版本当时的
|
|
11
|
+
// 真实记录,刻意不在替换范围内(正则只打徽章与 `#` 注释两种形状)。
|
|
12
|
+
import { execSync } from "node:child_process";
|
|
13
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
14
|
+
import { dirname, join } from "node:path";
|
|
15
|
+
import { fileURLToPath } from "node:url";
|
|
16
|
+
|
|
17
|
+
const scriptDir = dirname(fileURLToPath(import.meta.url));
|
|
18
|
+
const repoRoot = join(scriptDir, "..", "..");
|
|
19
|
+
|
|
20
|
+
// 1) 全量测试
|
|
21
|
+
let output;
|
|
22
|
+
try {
|
|
23
|
+
output = execSync("npm test", {
|
|
24
|
+
cwd: join(repoRoot, "dsh-mneme"),
|
|
25
|
+
encoding: "utf8",
|
|
26
|
+
stdio: ["ignore", "pipe", "pipe"],
|
|
27
|
+
maxBuffer: 64 * 1024 * 1024
|
|
28
|
+
});
|
|
29
|
+
} catch (error) {
|
|
30
|
+
const tail = String(error.stdout ?? "").split("\n").slice(-25).join("\n");
|
|
31
|
+
console.error("npm test 未跑绿,徽章不更新:\n" + tail);
|
|
32
|
+
process.exit(1);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// 2) 取汇总 pass 数(spec reporter 的 `ℹ pass N`;取最后一处防逐用例行干扰)
|
|
36
|
+
const matches = [...output.matchAll(/pass (\d+)/g)];
|
|
37
|
+
const pass = matches.length ? Number(matches[matches.length - 1][1]) : 0;
|
|
38
|
+
if (!pass) {
|
|
39
|
+
console.error("无法从测试输出解析 pass 数,徽章不更新");
|
|
40
|
+
process.exit(1);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// 3) 刷双 README(徽章 + 两行开发命令注释)
|
|
44
|
+
const targets = [
|
|
45
|
+
join(repoRoot, "README.md"),
|
|
46
|
+
join(repoRoot, "dsh-mneme", "README.md")
|
|
47
|
+
];
|
|
48
|
+
let changed = 0;
|
|
49
|
+
for (const file of targets) {
|
|
50
|
+
let text = readFileSync(file, "utf8");
|
|
51
|
+
const next = text
|
|
52
|
+
.replace(/tests-\d+%20passed-/g, `tests-${pass}%20passed-`)
|
|
53
|
+
.replace(/(#\s*)\d+( 个测试)/g, `$1${pass}$2`)
|
|
54
|
+
.replace(/(#\s*)\d+( tests)/g, `$1${pass}$2`);
|
|
55
|
+
if (next !== text) {
|
|
56
|
+
writeFileSync(file, next);
|
|
57
|
+
changed += 1;
|
|
58
|
+
console.log(`updated: ${file}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
console.log(`pass=${pass}, files changed=${changed}`);
|
package/src/api-standalone.js
CHANGED
|
@@ -152,6 +152,18 @@ export function createStandaloneApi({ service, store, config = {}, logger, setti
|
|
|
152
152
|
return;
|
|
153
153
|
}
|
|
154
154
|
|
|
155
|
+
// --- GET /profile: user self-description -------------------------------
|
|
156
|
+
if (req.method === "GET" && pathname === "/profile") {
|
|
157
|
+
sendJson(res, 200, { profile: settings?.getProfile?.() ?? "" });
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// --- GET /rules: agent behavior rules -----------------------------------
|
|
162
|
+
if (req.method === "GET" && pathname === "/rules") {
|
|
163
|
+
sendJson(res, 200, { rules: settings?.getRules?.() ?? [] });
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
155
167
|
// --- GET /memories: paged + filtered list (same semantics as the
|
|
156
168
|
// internal /api/dsh-mneme/list) ------------------------------------
|
|
157
169
|
if (req.method === "GET" && pathname === "/memories") {
|
package/src/api.js
CHANGED
|
@@ -330,7 +330,17 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
330
330
|
// 按钮无放大风险。
|
|
331
331
|
maxTokens: 1024,
|
|
332
332
|
...(reasoningEffort && reasoningEffort !== "none" ? { reasoningEffort } : {}),
|
|
333
|
-
|
|
333
|
+
// role 用 user 而非 system(#189 后续):0.1.6-alpha.1 起适配器把 system
|
|
334
|
+
// 抽成顶层参数,system-only 对话会被官方 API 以
|
|
335
|
+
// 「messages: at least one message is required」整单拒绝——探测按钮 502。
|
|
336
|
+
// user 消息在任何适配器下都是一条合法对话,探测从此不怕 host 演进。
|
|
337
|
+
messages: [
|
|
338
|
+
{
|
|
339
|
+
role: "user",
|
|
340
|
+
content: [{ type: "text", text: "Reply with exactly one word: ok" }],
|
|
341
|
+
source: { kind: "plugin", plugin: "dsh-mneme" }
|
|
342
|
+
}
|
|
343
|
+
]
|
|
334
344
|
})) {
|
|
335
345
|
if (chunk.type === "text-delta" && typeof chunk.text === "string") reply += chunk.text;
|
|
336
346
|
if (chunk.type === "finish" && (chunk.reason?.kind === "error" || chunk.reason?.kind === "aborted")) {
|
|
@@ -1298,7 +1308,7 @@ export function createApi(ctx, service, settings, commands, embedder, semantic =
|
|
|
1298
1308
|
});
|
|
1299
1309
|
|
|
1300
1310
|
return {
|
|
1301
|
-
routes:
|
|
1311
|
+
routes: 30,
|
|
1302
1312
|
dispose: () => {
|
|
1303
1313
|
for (const dispose of disposers) dispose();
|
|
1304
1314
|
}
|
package/src/config.js
CHANGED
|
@@ -37,6 +37,11 @@ export const Config = z.object({
|
|
|
37
37
|
distillRateLimitRetries: z.natural().min(0).max(10).default(3),
|
|
38
38
|
distillRateLimitBaseDelayMs: z.natural().min(100).max(60000).default(1000),
|
|
39
39
|
maxInjectedItems: z.natural().min(1).max(20).default(5),
|
|
40
|
+
// Issue #205:注入位跨轮轮换。同一会话里,最近 N 个不同用户查询轮次注入过
|
|
41
|
+
// 的记忆本轮不再优先(新鲜优先、原序回填,槽位数不变);长会话中避免相邻
|
|
42
|
+
// 轮次反复注入同一条(实测 15.5h 会话 86 次注入 93% 重复)。0 = 关闭(默认,
|
|
43
|
+
// 保持既有行为)。会话边界自动重置:新会话从零开始。
|
|
44
|
+
injectRotationTurns: z.natural().min(0).max(20).default(0),
|
|
40
45
|
importanceThreshold: z.natural().min(1).max(5).default(3),
|
|
41
46
|
// 编码记忆蒸馏(codingRetrospect,opt-in,默认关)。开启时,turn/end 蒸馏
|
|
42
47
|
// 额外提取三类编码专属记忆:rejected_solution(被否决方案)/ pitfall(踩坑)/
|
|
@@ -111,6 +116,10 @@ export const Config = z.object({
|
|
|
111
116
|
// 记忆做 consolidation。大记忆量下全量快照会把 LLM 输入撑爆(636 记忆 →
|
|
112
117
|
// 677 "missing" errors、applied=0),窗口外的旧记忆不进 snapshot。
|
|
113
118
|
dreamMaxSnapshotSize: z.natural().min(1).max(1000).default(200),
|
|
119
|
+
// Issue #104 方向 2:单轮 archive 决策上限。一次性大扫除(12 条互不相关主题
|
|
120
|
+
// 被批量归档,其中 8 条疑似误伤长保留类型)是失控信号,与 update 上限同类的
|
|
121
|
+
// 全局闸门:skipInvalid 也不豁免,超限整单拒绝。正常清理可调高。
|
|
122
|
+
dreamMaxArchivePerRun: z.natural().min(1).max(200).default(8),
|
|
114
123
|
// Issue #125:候选集构造方式。"window"(默认,等同现状)只取最近
|
|
115
124
|
// dreamMaxSnapshotSize 条;"hybrid" 在此基础上并入**向量翻出的高相似组**——
|
|
116
125
|
// 本机实测 45 对「双方活跃且 sim≥0.85」里 0 对能同时进窗口(窗口覆盖率 11.7%),
|
|
@@ -176,6 +185,11 @@ export const Config = z.object({
|
|
|
176
185
|
embedModelCacheDir: z.string().default(""),
|
|
177
186
|
embedModelMirror: z.string().default("https://hf-mirror.com"),
|
|
178
187
|
|
|
188
|
+
// issue #194:模型文件下载的断点续传与重试。开启时 transformers.js 的 env.fetch 被
|
|
189
|
+
// 替换为带断点续传的实现(已收字节落盘、Range/If-Range 续传、空闲看门狗),大文件
|
|
190
|
+
// 中断后不必整份重来;关闭 = env.fetch 保持原样(线上回滚开关)。
|
|
191
|
+
resilientModelDownload: z.boolean().default(true),
|
|
192
|
+
|
|
179
193
|
// 自管运行时目录(issue #131)。空 = ~/.dsh/mneme/runtime:里面放收编或下载来的
|
|
180
194
|
// transformers + onnxruntime 闭包,插件经 src/runtime/loader.js 的三层解析加载它。
|
|
181
195
|
// 目的就是让这条重依赖不必留在宿主 profile 的依赖图里——profile 是所有插件共用的
|
|
@@ -257,6 +271,10 @@ export const Config = z.object({
|
|
|
257
271
|
rerankBatchSize: z.natural().min(1).max(64).default(8),
|
|
258
272
|
rerankMaxCandidates: z.natural().min(5).max(100).default(30),
|
|
259
273
|
rerankScoreThreshold: z.number().min(0).max(1).default(0.1),
|
|
274
|
+
// #188:量化档,与嵌入层的 useDtype 同语义(q8 = model_quantized.onnx,约为
|
|
275
|
+
// fp32 体积的 1/4)。此前重排层不传 dtype,即使缓存里已有量化文件也会去下载
|
|
276
|
+
// 1GB 级的 model.onnx。非法值由 transformers 在 init 时抛出 → 重排降级并告警。
|
|
277
|
+
rerankDtype: z.string().default("q8"),
|
|
260
278
|
|
|
261
279
|
// --- reflection: update decision + failure tracking (v0.2.1) ------------
|
|
262
280
|
reflectionUpdateEnabled: z.boolean().default(true),
|
package/src/dream/decisions.js
CHANGED
|
@@ -11,6 +11,15 @@ export const ACTIONS = new Set(["keep", "merge", "archive", "conflict", "update"
|
|
|
11
11
|
// = preferred. observation (measured) > inferred (derived) > subjective (guess).
|
|
12
12
|
const EPISTEMIC_PRIORITY = { observation: 3, inferred: 2, subjective: 1 };
|
|
13
13
|
|
|
14
|
+
// Issue #104 方向 2:archive 类型护栏的长保留类型集合——这些类型的存在意义
|
|
15
|
+
// 就是长期保存(踩坑记录、约束、用户偏好、挖掘出的模式),LLM 以「价值判断」
|
|
16
|
+
// 类理由批量归档它们时误伤损失最大。conflict 裁决的败者归档不经过 archive
|
|
17
|
+
// action(走 applyDecisions 的 conflict 分支),不受此护栏影响。
|
|
18
|
+
const ARCHIVE_GUARDED_TYPES = new Set(["preference", "pattern", "rejected_solution", "constraint", "pitfall"]);
|
|
19
|
+
// 「重复 / 过时」类理由的关键词(中英双语)。命中才允许归档长保留类型;
|
|
20
|
+
// 词表宁可放宽也不误拦——护栏只跳过单条并落明细,不整单拒绝。
|
|
21
|
+
const ARCHIVE_RATIONALE_RE = /重复|过时|陈旧|过期|已被取代|被取代|冗余|不再使用|旧版|废弃|stale|outdated|obsolete|dup(?:licate|licat)?|superseded|redundant|deprecated|expired|replaced/i;
|
|
22
|
+
|
|
14
23
|
/**
|
|
15
24
|
* Validate a dream decision list against a snapshot of eligible memories.
|
|
16
25
|
* @param decisions - LLM-produced decision list. In skipInvalid mode, invalid
|
|
@@ -128,6 +137,21 @@ export function validateDecisions(decisions, snapshot, options = {}) {
|
|
|
128
137
|
// update-specific field validation runs BEFORE claiming ids, so a failing
|
|
129
138
|
// update never pollutes the claimed set (which drives the "every id must
|
|
130
139
|
// appear in a decision" check below).
|
|
140
|
+
// Issue #104 方向 2:archive 类型护栏。长保留类型的 archive 决策必须携带
|
|
141
|
+
// 「重复 / 过时」类理由(提示词对 archive 的定义本就如此),「价值判断」类
|
|
142
|
+
// 理由(保持整洁、参考价值低……)不放行——单条跳过记 skipped(run 记
|
|
143
|
+
// degraded,明细进 dream_runs.skipped),绝不整单拒绝。未知 id 在下方通用
|
|
144
|
+
// 校验里另行报错,这里 snapshot.get 为 undefined 时跳过护栏判断。
|
|
145
|
+
if (local.length === 0 && d?.action === "archive") {
|
|
146
|
+
const guardedTypes = new Set();
|
|
147
|
+
for (const id of d.ids) {
|
|
148
|
+
const mem = snapshot.get(id);
|
|
149
|
+
if (mem && ARCHIVE_GUARDED_TYPES.has(mem.type)) guardedTypes.add(mem.type);
|
|
150
|
+
}
|
|
151
|
+
if (guardedTypes.size > 0 && !ARCHIVE_RATIONALE_RE.test(String(d.reason ?? ""))) {
|
|
152
|
+
local.push(`${at}: archive of long-retention type(s) ${[...guardedTypes].join(", ")} needs a duplicate/outdated rationale (重复/过时), got: ${JSON.stringify(String(d.reason ?? "")).slice(0, 60)}`);
|
|
153
|
+
}
|
|
154
|
+
}
|
|
131
155
|
if (local.length === 0 && d?.action === "update") {
|
|
132
156
|
// 只能更新单条
|
|
133
157
|
if (!Array.isArray(d.ids) || d.ids.length !== 1) {
|
|
@@ -216,26 +240,49 @@ export function validateDecisions(decisions, snapshot, options = {}) {
|
|
|
216
240
|
if (createCount > maxCreatePerRun) {
|
|
217
241
|
errors.push(`too many create decisions: ${createCount} > ${maxCreatePerRun}`);
|
|
218
242
|
}
|
|
243
|
+
// Issue #104 方向 2:archive 批量上限——一次性大扫除(12 条互不相关主题被
|
|
244
|
+
// 批量归档)是失控信号,与 update 上限同一类全局闸门:skipInvalid 也不豁免,
|
|
245
|
+
// 超限整单拒绝;正常的大规模清理由用户调高 dreamMaxArchivePerRun。
|
|
246
|
+
const archiveCount = survivors.filter((d) => d.action === "archive").length;
|
|
247
|
+
const maxArchivePerRun = options.maxArchivePerRun ?? 8;
|
|
248
|
+
if (archiveCount > maxArchivePerRun) {
|
|
249
|
+
errors.push(`too many archive decisions: ${archiveCount} > ${maxArchivePerRun}`);
|
|
250
|
+
}
|
|
251
|
+
// Issue #89 防洗白核心(#104 方向 1 重审后保留):全部决策都被跳过、合法
|
|
252
|
+
// 子集为空时仍整单拒绝——宽容的前提是「有合法子集可应用」;模型输出整体
|
|
253
|
+
// 非法(如全部幻觉 id)是真故障,不能被隐式 keep 洗成 ok。#104 只豁免
|
|
254
|
+
// 「有合法子集但覆盖率不足」的良性挑重点(见下方 coverageShortfall)。
|
|
255
|
+
if (options.skipInvalid === true && skipped.length > 0 && survivors.length === 0 && errors.length === 0) {
|
|
256
|
+
errors.push(`all ${skipped.length} decision(s) invalid — nothing valid survived validation`);
|
|
257
|
+
return { ok: false, errors, skipped, resolvedShortIds };
|
|
258
|
+
}
|
|
219
259
|
// v0.4.4: 隐式 keep。默认(dreamImplicitKeep !== false)下,未 claim 的
|
|
220
260
|
// snapshot 记忆自动补 {action:"keep"},而不是整体拒绝——大记忆量下 LLM 漏报
|
|
221
261
|
// 一两条就全拒(636 记忆 → 677 errors)会白白浪费整轮 run。设 false 则保留
|
|
222
262
|
// 旧的严格"全量覆盖"校验。补齐的 keep 直接 append 到 decisions,调用方
|
|
223
263
|
// (runDream/applyDecisions/audit)复用同一数组即可覆盖全部 snapshot 记忆。
|
|
224
264
|
//
|
|
225
|
-
// v0.4.4 fix
|
|
226
|
-
//
|
|
227
|
-
//
|
|
228
|
-
//
|
|
229
|
-
//
|
|
265
|
+
// v0.4.4 fix(残缺输出防洗白)→ Issue #104 方向 1 重审:显式覆盖率护栏的
|
|
266
|
+
// 初衷是拦「输出截断 → 只 claim 少量 → 其余被隐式 keep 洗白」,但它无法区分
|
|
267
|
+
// 恶性截断与良性挑重点——而提示词硬性规则本身要求「无问题的条目无需输出」,
|
|
268
|
+
// 挑重点正是要求的行为。官方路由 + 强模型的实测(heptaspirit:81 轮 failed
|
|
269
|
+
// 全是 coverage,LLM 调用本身成功、applied=0)证明这是常规路径而非边角。
|
|
270
|
+
// 处置:覆盖率不足不再 ok:false 整单拒绝,改为返回 coverageShortfall 理由、
|
|
271
|
+
// 照常补齐隐式 keep——调用方把 run 记 degraded(合法子集已应用),护栏以
|
|
272
|
+
// 状态暴露而非一票否决,与 skipInvalid 同一宽容哲学。明确异常信号(create
|
|
273
|
+
// 上限等硬错误)保持整单拒绝。要恢复旧行为可设 dreamImplicitKeep:false
|
|
274
|
+
// (严格模式,缺失即 error)或 dreamMinExplicitCoverage: 0(关护栏)。
|
|
275
|
+
// 非覆盖类硬错误(create 上限溢出、严格模式缺失 id)仍然先整单拒绝——
|
|
276
|
+
// #104 方向 1 只豁免 coverage 这一个信号,防洗白语义对它们原样保留。
|
|
230
277
|
if (errors.length > 0) {
|
|
231
278
|
return { ok: false, errors, skipped, resolvedShortIds };
|
|
232
279
|
}
|
|
233
280
|
const minCoverage = options.dreamMinExplicitCoverage ?? 0.5;
|
|
281
|
+
let coverageShortfall = null;
|
|
234
282
|
if (options.dreamImplicitKeep !== false) {
|
|
235
283
|
const coverage = snapshot.size > 0 ? claimed.size / snapshot.size : 1;
|
|
236
284
|
if (coverage < minCoverage) {
|
|
237
|
-
|
|
238
|
-
return { ok: false, errors, skipped, resolvedShortIds };
|
|
285
|
+
coverageShortfall = `explicit decision coverage ${Math.round(coverage * 100)}% < minimum ${Math.round(minCoverage * 100)}%`;
|
|
239
286
|
}
|
|
240
287
|
for (const id of snapshot.keys()) {
|
|
241
288
|
if (!claimed.has(id)) survivors.push({ action: "keep", ids: [id] });
|
|
@@ -252,7 +299,7 @@ export function validateDecisions(decisions, snapshot, options = {}) {
|
|
|
252
299
|
// 当"被跳过的非法决策数 == 隐式补齐的 keep 数"时长度回到相等但内容已变,
|
|
253
300
|
// 被跳过的决策会残留进 apply/audit。一律无条件 splice 最安全。
|
|
254
301
|
decisions.splice(0, decisions.length, ...survivors);
|
|
255
|
-
return { ok: true, errors, skipped, resolvedShortIds };
|
|
302
|
+
return { ok: true, errors, skipped, resolvedShortIds, coverageShortfall };
|
|
256
303
|
}
|
|
257
304
|
|
|
258
305
|
/** Marker thrown when a decision target changed since the run snapshot. */
|
package/src/dream/sleep.js
CHANGED
|
@@ -239,8 +239,8 @@ async function phaseConflicts(ctx, service, config, logger, runId, semantic = nu
|
|
|
239
239
|
maxTokens: 2048,
|
|
240
240
|
...(withEffort && sleepEffort ? { reasoningEffort: sleepEffort } : {}),
|
|
241
241
|
messages: [
|
|
242
|
-
{ role: "system", content: [{ type: "text", text: conflictPrompt }] },
|
|
243
|
-
{ role: "user", content: [{ type: "text", text: listText }] }
|
|
242
|
+
{ role: "system", content: [{ type: "text", text: conflictPrompt }], source: { kind: "plugin", plugin: "dsh-mneme" } },
|
|
243
|
+
{ role: "user", content: [{ type: "text", text: listText }], source: { kind: "plugin", plugin: "dsh-mneme" } }
|
|
244
244
|
]
|
|
245
245
|
}, (reason) => { conflictStreamFailure = describeStreamFailure(reason); });
|
|
246
246
|
};
|
|
@@ -390,8 +390,8 @@ async function phasePatterns(ctx, service, config, logger, runId, signal = null)
|
|
|
390
390
|
maxTokens: 2048,
|
|
391
391
|
...(withEffort && sleepEffort ? { reasoningEffort: sleepEffort } : {}),
|
|
392
392
|
messages: [
|
|
393
|
-
{ role: "system", content: [{ type: "text", text: STR.prompts.pattern[language].replace("N", String(maxPatterns)) }] },
|
|
394
|
-
{ role: "user", content: [{ type: "text", text: listText }] }
|
|
393
|
+
{ role: "system", content: [{ type: "text", text: STR.prompts.pattern[language].replace("N", String(maxPatterns)) }], source: { kind: "plugin", plugin: "dsh-mneme" } },
|
|
394
|
+
{ role: "user", content: [{ type: "text", text: listText }], source: { kind: "plugin", plugin: "dsh-mneme" } }
|
|
395
395
|
]
|
|
396
396
|
}, (reason) => { patternStreamFailure = describeStreamFailure(reason); });
|
|
397
397
|
};
|
|
@@ -596,7 +596,33 @@ export function createSleepScheduler({
|
|
|
596
596
|
const delay = Math.max(0, idleMs - (now() - lastWriteAt)) + 1000;
|
|
597
597
|
idleTimer = setTimeoutFn(async () => {
|
|
598
598
|
idleTimer = null;
|
|
599
|
-
|
|
599
|
+
// Issue #187:false = 撞上 CD(或 run 失败)——此前这里直接丢弃,"CD 到期"
|
|
600
|
+
// 成了没有监听者的时刻,要等下一次写入才救回来。改为按剩余窗口重排。
|
|
601
|
+
const ran = await maybeSchedule();
|
|
602
|
+
if (!ran) armNextWindow();
|
|
603
|
+
}, delay);
|
|
604
|
+
idleTimer.unref?.();
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
// Issue #187:撞 CD / run 失败后的补挂表——按「剩余 CD / 剩余静默」里更晚的
|
|
608
|
+
// 到点时刻重排一次,回调里重复同样的判断所以不会空转。跑成了一轮就不重排:
|
|
609
|
+
// 安静的库没有要处理的新东西,触发权交还给下一次写入的 noteWrite。
|
|
610
|
+
function armNextWindow() {
|
|
611
|
+
if (disposed || idleTimer) return;
|
|
612
|
+
if (config.sleepModeEnabled !== true) return;
|
|
613
|
+
// lastRunAt === 0 表示从未跑过(此路径不该出现,防御性返回)。
|
|
614
|
+
if (lastRunAt <= 0) return;
|
|
615
|
+
const idleMs = (config.sleepIdleMinutes ?? 5) * 60000;
|
|
616
|
+
const cdMs = (config.sleepMinIntervalHours ?? 8) * 3600000;
|
|
617
|
+
const delay = Math.max(
|
|
618
|
+
lastRunAt + cdMs - now(),
|
|
619
|
+
lastWriteAt + idleMs - now(),
|
|
620
|
+
0
|
|
621
|
+
) + 1000;
|
|
622
|
+
idleTimer = setTimeoutFn(async () => {
|
|
623
|
+
idleTimer = null;
|
|
624
|
+
const ran = await maybeSchedule();
|
|
625
|
+
if (!ran) armNextWindow();
|
|
600
626
|
}, delay);
|
|
601
627
|
idleTimer.unref?.();
|
|
602
628
|
}
|
|
@@ -665,5 +691,10 @@ export function createSleepScheduler({
|
|
|
665
691
|
}
|
|
666
692
|
}
|
|
667
693
|
|
|
694
|
+
// Issue #187(同族缺陷):构造即挂表——重启后只要没发生过写入,此前永远没有
|
|
695
|
+
// 闹钟,安静多久都不会触发。lastWriteAt 已在构造时设为当前时刻,shouldRun 的
|
|
696
|
+
// 静默条件自然生效:重启后静默满 idleMinutes 才可能起跑。
|
|
697
|
+
armIdleTimer();
|
|
698
|
+
|
|
668
699
|
return { noteWrite, maybeSchedule, shouldRun, dispose };
|
|
669
700
|
}
|