@modusensus/dsh-mneme 0.6.8 → 0.6.9
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/.github/workflows/test.yml +32 -0
- package/.release-notes-v0.6.9.md +13 -0
- package/CHANGELOG.md +89 -0
- package/README.md +148 -392
- package/SECURITY.md +544 -0
- package/docs/devlog/2026-08-14-dsh-mneme-dev-log.md +247 -0
- package/docs/devlog/2026-08-15-dsh-mneme-audit-stress-dev-log.md +145 -0
- package/docs/devlog/2026-08-15-dsh-mneme-pipeline-dev-log.md +56 -0
- package/docs/devlog/2026-08-15-dsh-mneme-reflection-dev-log.md +77 -0
- package/docs/devlog/2026-08-15-dsh-mneme-review-fixes-dev-log.md +64 -0
- package/docs/devlog/2026-08-15-dsh-mneme-semantic-dev-log.md +90 -0
- package/dsh-mneme/CHANGELOG.md +248 -0
- package/dsh-mneme/LICENSE +21 -0
- package/dsh-mneme/README.md +465 -0
- package/dsh-mneme/docs/AGENT_MEMORY_RESEARCH.md +183 -0
- package/dsh-mneme/docs/ENTITIES.md +245 -0
- package/dsh-mneme/docs/LOCAL_MODEL.md +141 -0
- package/dsh-mneme/docs/MIGRATION.md +127 -0
- package/dsh-mneme/docs/SEMANTIC.md +256 -0
- package/dsh-mneme/docs/SLEEP.md +163 -0
- package/{lib → dsh-mneme/lib}/config.js +10 -0
- package/{src → dsh-mneme/lib}/dream/decisions.js +143 -94
- package/{lib → dsh-mneme/lib}/dream.js +32 -9
- package/dsh-mneme/package-lock.json +1936 -0
- package/dsh-mneme/package.json +80 -0
- package/{src → dsh-mneme/src}/config.js +10 -0
- package/{lib → dsh-mneme/src}/dream/decisions.js +143 -94
- package/{src → dsh-mneme/src}/dream.js +32 -9
- package/{test → dsh-mneme/test}/dream.test.js +157 -0
- package/package.json +58 -80
- package//346/250/252/345/271/205.png +0 -0
- /package/{cordis.patch.yml → dsh-mneme/cordis.patch.yml} +0 -0
- /package/{lib → dsh-mneme/lib}/api.js +0 -0
- /package/{lib → dsh-mneme/lib}/client.js +0 -0
- /package/{lib → dsh-mneme/lib}/commands.js +0 -0
- /package/{lib → dsh-mneme/lib}/dream/clustering.js +0 -0
- /package/{lib → dsh-mneme/lib}/dream/sleep.js +0 -0
- /package/{lib → dsh-mneme/lib}/dream/tag-extractor.js +0 -0
- /package/{lib → dsh-mneme/lib}/embedding.js +0 -0
- /package/{lib → dsh-mneme/lib}/entities/extractor.js +0 -0
- /package/{lib → dsh-mneme/lib}/hot-memory.js +0 -0
- /package/{lib → dsh-mneme/lib}/index.js +0 -0
- /package/{lib → dsh-mneme/lib}/inject.js +0 -0
- /package/{lib → dsh-mneme/lib}/local-embedder.js +0 -0
- /package/{lib → dsh-mneme/lib}/mirror.js +0 -0
- /package/{lib → dsh-mneme/lib}/parser/tag.js +0 -0
- /package/{lib → dsh-mneme/lib}/parser/wiki-link.js +0 -0
- /package/{lib → dsh-mneme/lib}/quality-filter.js +0 -0
- /package/{lib → dsh-mneme/lib}/reranker.js +0 -0
- /package/{lib → dsh-mneme/lib}/search/adaptive.js +0 -0
- /package/{lib → dsh-mneme/lib}/search/bm25.js +0 -0
- /package/{lib → dsh-mneme/lib}/search/tag-boost.js +0 -0
- /package/{lib → dsh-mneme/lib}/service.js +0 -0
- /package/{lib → dsh-mneme/lib}/settings.js +0 -0
- /package/{lib → dsh-mneme/lib}/store.js +0 -0
- /package/{lib → dsh-mneme/lib}/summarize.js +0 -0
- /package/{lib → dsh-mneme/lib}/tools.js +0 -0
- /package/{lib → dsh-mneme/lib}/vector-index.js +0 -0
- /package/{scripts → dsh-mneme/scripts}/benchmark-embed.js +0 -0
- /package/{scripts → dsh-mneme/scripts}/benchmark-recall.js +0 -0
- /package/{scripts → dsh-mneme/scripts}/benchmark-rerank.js +0 -0
- /package/{scripts → dsh-mneme/scripts}/e2e-dsh.js +0 -0
- /package/{scripts → dsh-mneme/scripts}/stress-dsh.js +0 -0
- /package/{scripts → dsh-mneme/scripts}/sync-lib.js +0 -0
- /package/{src → dsh-mneme/src}/api.js +0 -0
- /package/{src → dsh-mneme/src}/commands.js +0 -0
- /package/{src → dsh-mneme/src}/dream/clustering.js +0 -0
- /package/{src → dsh-mneme/src}/dream/sleep.js +0 -0
- /package/{src → dsh-mneme/src}/dream/tag-extractor.js +0 -0
- /package/{src → dsh-mneme/src}/embedding.js +0 -0
- /package/{src → dsh-mneme/src}/entities/extractor.js +0 -0
- /package/{src → dsh-mneme/src}/hot-memory.js +0 -0
- /package/{src → dsh-mneme/src}/index.js +0 -0
- /package/{src → dsh-mneme/src}/inject.js +0 -0
- /package/{src → dsh-mneme/src}/local-embedder.js +0 -0
- /package/{src → dsh-mneme/src}/mirror.js +0 -0
- /package/{src → dsh-mneme/src}/parser/tag.js +0 -0
- /package/{src → dsh-mneme/src}/parser/wiki-link.js +0 -0
- /package/{src → dsh-mneme/src}/quality-filter.js +0 -0
- /package/{src → dsh-mneme/src}/reranker.js +0 -0
- /package/{src → dsh-mneme/src}/search/adaptive.js +0 -0
- /package/{src → dsh-mneme/src}/search/bm25.js +0 -0
- /package/{src → dsh-mneme/src}/search/tag-boost.js +0 -0
- /package/{src → dsh-mneme/src}/service.js +0 -0
- /package/{src → dsh-mneme/src}/settings.js +0 -0
- /package/{src → dsh-mneme/src}/store.js +0 -0
- /package/{src → dsh-mneme/src}/summarize.js +0 -0
- /package/{src → dsh-mneme/src}/tools.js +0 -0
- /package/{src → dsh-mneme/src}/vector-index.js +0 -0
- /package/{test → dsh-mneme/test}/api.test.js +0 -0
- /package/{test → dsh-mneme/test}/audit.test.js +0 -0
- /package/{test → dsh-mneme/test}/benchmark.test.js +0 -0
- /package/{test → dsh-mneme/test}/boundary-v0625.test.js +0 -0
- /package/{test → dsh-mneme/test}/client.test.js +0 -0
- /package/{test → dsh-mneme/test}/clustering.test.js +0 -0
- /package/{test → dsh-mneme/test}/commands.test.js +0 -0
- /package/{test → dsh-mneme/test}/config.test.js +0 -0
- /package/{test → dsh-mneme/test}/conflict-freeze.test.js +0 -0
- /package/{test → dsh-mneme/test}/directory.test.js +0 -0
- /package/{test → dsh-mneme/test}/entities.test.js +0 -0
- /package/{test → dsh-mneme/test}/epistemic.test.js +0 -0
- /package/{test → dsh-mneme/test}/fnew-0112.test.js +0 -0
- /package/{test → dsh-mneme/test}/fnew-03.test.js +0 -0
- /package/{test → dsh-mneme/test}/graph-api.test.js +0 -0
- /package/{test → dsh-mneme/test}/helpers/dream-mock.js +0 -0
- /package/{test → dsh-mneme/test}/hot-memory.test.js +0 -0
- /package/{test → dsh-mneme/test}/inject.test.js +0 -0
- /package/{test → dsh-mneme/test}/llm-audit.test.js +0 -0
- /package/{test → dsh-mneme/test}/local-embedder.test.js +0 -0
- /package/{test → dsh-mneme/test}/mirror-dirty.test.js +0 -0
- /package/{test → dsh-mneme/test}/mirror-edit-digest.test.js +0 -0
- /package/{test → dsh-mneme/test}/mirror-generation.test.js +0 -0
- /package/{test → dsh-mneme/test}/mirror.test.js +0 -0
- /package/{test → dsh-mneme/test}/normalize-decisions.test.js +0 -0
- /package/{test → dsh-mneme/test}/peer-blockers.test.js +0 -0
- /package/{test → dsh-mneme/test}/policy-epoch.test.js +0 -0
- /package/{test → dsh-mneme/test}/provenance.test.js +0 -0
- /package/{test → dsh-mneme/test}/quality-filter.test.js +0 -0
- /package/{test → dsh-mneme/test}/reasoning-effort.test.js +0 -0
- /package/{test → dsh-mneme/test}/recall-evals.test.js +0 -0
- /package/{test → dsh-mneme/test}/recall-layer.test.js +0 -0
- /package/{test → dsh-mneme/test}/receipt-chain.test.js +0 -0
- /package/{test → dsh-mneme/test}/reflection.test.js +0 -0
- /package/{test → dsh-mneme/test}/reranker.test.js +0 -0
- /package/{test → dsh-mneme/test}/search-fusion.test.js +0 -0
- /package/{test → dsh-mneme/test}/semantic.test.js +0 -0
- /package/{test → dsh-mneme/test}/service-search.test.js +0 -0
- /package/{test → dsh-mneme/test}/service.test.js +0 -0
- /package/{test → dsh-mneme/test}/settings.test.js +0 -0
- /package/{test → dsh-mneme/test}/sleep.test.js +0 -0
- /package/{test → dsh-mneme/test}/store.test.js +0 -0
- /package/{test → dsh-mneme/test}/stress.test.js +0 -0
- /package/{test → dsh-mneme/test}/summarize.test.js +0 -0
- /package/{test → dsh-mneme/test}/tag-boost.test.js +0 -0
- /package/{test → dsh-mneme/test}/tag.test.js +0 -0
- /package/{test → dsh-mneme/test}/tools.test.js +0 -0
- /package/{test → dsh-mneme/test}/vector-index.test.js +0 -0
- /package/{test → dsh-mneme/test}/wiki-link.test.js +0 -0
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@modusensus/dsh-mneme",
|
|
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.6.9",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"repository": {
|
|
7
|
+
"type": "git",
|
|
8
|
+
"url": "git+https://github.com/modusensus/dsh-mneme.git"
|
|
9
|
+
},
|
|
10
|
+
"homepage": "https://github.com/modusensus/dsh-mneme#readme",
|
|
11
|
+
"bugs": {
|
|
12
|
+
"url": "https://github.com/modusensus/dsh-mneme/issues"
|
|
13
|
+
},
|
|
14
|
+
"type": "module",
|
|
15
|
+
"main": "lib/index.js",
|
|
16
|
+
"exports": {
|
|
17
|
+
".": {
|
|
18
|
+
"default": "./lib/index.js"
|
|
19
|
+
},
|
|
20
|
+
"./client": {
|
|
21
|
+
"default": "./lib/client.js"
|
|
22
|
+
},
|
|
23
|
+
"./package.json": "./package.json"
|
|
24
|
+
},
|
|
25
|
+
"files": [
|
|
26
|
+
"lib",
|
|
27
|
+
"src",
|
|
28
|
+
"scripts",
|
|
29
|
+
"test",
|
|
30
|
+
"cordis.patch.yml"
|
|
31
|
+
],
|
|
32
|
+
"dsh": {
|
|
33
|
+
"client": {
|
|
34
|
+
"inject": [
|
|
35
|
+
"slots",
|
|
36
|
+
"locale",
|
|
37
|
+
"layout",
|
|
38
|
+
"connection"
|
|
39
|
+
],
|
|
40
|
+
"platform": "web"
|
|
41
|
+
},
|
|
42
|
+
"bundle": {
|
|
43
|
+
"patch": "./cordis.patch.yml"
|
|
44
|
+
}
|
|
45
|
+
},
|
|
46
|
+
"peerDependencies": {
|
|
47
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
48
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
|
|
49
|
+
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
|
|
50
|
+
"@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.6",
|
|
51
|
+
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
|
|
52
|
+
"@deepseek-ai/schemastery": "^3.18.1"
|
|
53
|
+
},
|
|
54
|
+
"devDependencies": {
|
|
55
|
+
"@deepseek-ai/cordis": "^4.0.1",
|
|
56
|
+
"@deepseek-ai/dsh-host-webserver": "^0.1.0-rc.6",
|
|
57
|
+
"@deepseek-ai/dsh-llm": "^0.1.0-rc.6",
|
|
58
|
+
"@deepseek-ai/dsh-system-prompt": "^0.1.0-rc.6",
|
|
59
|
+
"@deepseek-ai/dsh-tools": "^0.1.0-rc.6",
|
|
60
|
+
"@deepseek-ai/schemastery": "^3.18.1",
|
|
61
|
+
"c8": "^12.0.0"
|
|
62
|
+
},
|
|
63
|
+
"scripts": {
|
|
64
|
+
"sync": "node scripts/sync-lib.js",
|
|
65
|
+
"prepack": "npm run sync",
|
|
66
|
+
"test": "node --test test/*.test.js",
|
|
67
|
+
"test:coverage": "c8 node --test test/*.test.js",
|
|
68
|
+
"e2e": "node scripts/e2e-dsh.js",
|
|
69
|
+
"stress": "node scripts/stress-dsh.js"
|
|
70
|
+
},
|
|
71
|
+
"dependencies": {
|
|
72
|
+
"@huggingface/transformers": "^4.2.0"
|
|
73
|
+
},
|
|
74
|
+
"overrides": {
|
|
75
|
+
"adm-zip": "0.6.0"
|
|
76
|
+
},
|
|
77
|
+
"c8": {
|
|
78
|
+
"reporter": ["text", "lcov"]
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -49,6 +49,16 @@ export const Config = z.object({
|
|
|
49
49
|
// → 整单拒绝,防止残缺输出被隐式 keep 洗白成 ok 后再被真实 apply。0-1,
|
|
50
50
|
// 默认 0.5(至少显式覆盖一半 snapshot)。
|
|
51
51
|
dreamMinExplicitCoverage: z.number().min(0).max(1).default(0.5),
|
|
52
|
+
// 跳过非法决策(Issue #26 P0,默认开):跨类型 merge 等"单条非法"决策不再
|
|
53
|
+
// 让整批校验失败 → 跳过该决策、应用合法子集,run 记为 degraded(applied>0)。
|
|
54
|
+
// 关闭后恢复旧的"任意非法即整单拒绝"(applied=0)。防洗白语义不受影响——
|
|
55
|
+
// 显式覆盖率不足/update 超量等全局错误仍整单拒绝。
|
|
56
|
+
dreamSkipInvalid: z.boolean().default(true),
|
|
57
|
+
// 允许跨类型合并(Issue #26 P1,默认关):类型有语义边界(preference 注入
|
|
58
|
+
// 权重更高、decision/project 注入上下文不同),跨类型合并会丢类型信息,故
|
|
59
|
+
// 默认禁止并在 skipInvalid 下被跳过;显式开启后放宽该检查,类型边界由用户
|
|
60
|
+
// 自行承担(需与 dreamSkipInvalid 配合:开启后跨类型 merge 视为合法、可应用)。
|
|
61
|
+
allowCrossTypeMerge: z.boolean().default(false),
|
|
52
62
|
// Rule version for dream adjudication: when this bumps, older dream_runs
|
|
53
63
|
// degrade to historical evidence (their receipts no longer drive live
|
|
54
64
|
// decisions). Default 0 = no versioning in use yet.
|
|
@@ -7,120 +7,163 @@ const EPISTEMIC_PRIORITY = { observation: 3, inferred: 2, subjective: 1 };
|
|
|
7
7
|
|
|
8
8
|
/**
|
|
9
9
|
* Validate a dream decision list against a snapshot of eligible memories.
|
|
10
|
-
*
|
|
10
|
+
*
|
|
11
|
+
* Issue #26 (skipInvalid): 模型几乎必然为了语义相关性而产出跨类型 merge
|
|
12
|
+
* (type 不同,硬性禁止),这类"单条非法"此前让整批校验 ok:false、整单拒绝
|
|
13
|
+
* (applied=0、白耗一次 LLM 调用)。`options.skipInvalid` 开启后,逐条非法的
|
|
14
|
+
* 决策被跳过(从 decisions 中移除、不 claim 任何 id、记录到 `skipped`),
|
|
15
|
+
* 只有全局性错误(update/create 超量、显式覆盖率不足——
|
|
16
|
+
* 这些是"模型失控/输出被截断"信号)仍整单拒绝,防洗白语义不变。
|
|
17
|
+
* `options.allowCrossTypeMerge`(Issue #26 P1)显式放宽跨类型合并检查——
|
|
18
|
+
* 默认 false 保持现有类型边界,true 时跨类型 merge 被允许(类型边界由
|
|
19
|
+
* 用户自行承担)。
|
|
20
|
+
*
|
|
21
|
+
* @param decisions - LLM-produced decision list. In skipInvalid mode, invalid
|
|
22
|
+
* entries are spliced out in place so the caller's downstream apply/audit
|
|
23
|
+
* operates on the surviving subset only.
|
|
11
24
|
* @param snapshot - Map<id, memory> of eligible (non-archived, non-summary) entries.
|
|
12
|
-
* @returns {{ok: boolean, errors: string[]}}
|
|
25
|
+
* @returns {{ok: boolean, errors: string[], skipped?: Array<{index, action, ids, error}>}}
|
|
13
26
|
*/
|
|
14
27
|
export function validateDecisions(decisions, snapshot, options = {}) {
|
|
15
28
|
const errors = [];
|
|
29
|
+
const skipped = [];
|
|
16
30
|
const maxUpdatePerRun = options.maxUpdatePerRun ?? 2;
|
|
17
31
|
const minAgeHours = options.minAgeHours ?? 24;
|
|
32
|
+
const skipInvalid = options.skipInvalid === true;
|
|
18
33
|
if (!Array.isArray(decisions) || decisions.length === 0) {
|
|
19
34
|
return { ok: false, errors: ["decision list must be a non-empty array"] };
|
|
20
35
|
}
|
|
21
36
|
const claimed = new Set();
|
|
37
|
+
const survivors = [];
|
|
22
38
|
for (const [index, d] of decisions.entries()) {
|
|
23
39
|
const at = `decision[${index}]`;
|
|
40
|
+
const local = [];
|
|
41
|
+
const ids = d && d.action === "conflict" ? [d.winner, d.loser] : (d?.ids ?? []);
|
|
24
42
|
if (!d || typeof d !== "object" || !ACTIONS.has(d.action)) {
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
if (typeof d.type !== "string" || !d.type.trim()) {
|
|
50
|
-
errors.push(`${at}: create needs non-empty type`);
|
|
51
|
-
}
|
|
52
|
-
continue;
|
|
53
|
-
} else if (!Array.isArray(d.ids) || d.ids.length === 0) {
|
|
54
|
-
errors.push(`${at}: ${d.action} needs non-empty ids`);
|
|
55
|
-
continue;
|
|
56
|
-
}
|
|
57
|
-
// update-specific field validation runs BEFORE claiming ids, so a failing
|
|
58
|
-
// update never pollutes the claimed set (which drives the "every id must
|
|
59
|
-
// appear in a decision" check below).
|
|
60
|
-
if (d.action === "update") {
|
|
61
|
-
// 只能更新单条
|
|
62
|
-
if (!Array.isArray(d.ids) || d.ids.length !== 1) {
|
|
63
|
-
errors.push(`${at}: update must target exactly one id`);
|
|
64
|
-
continue;
|
|
65
|
-
}
|
|
66
|
-
// 必须产生实际变化
|
|
67
|
-
const mem = snapshot.get(d.ids[0]);
|
|
68
|
-
const hasChange = (d.title !== undefined && d.title !== mem?.title)
|
|
69
|
-
|| (d.content !== undefined && d.content !== mem?.content)
|
|
70
|
-
|| (d.importance !== undefined && d.importance !== mem?.importance);
|
|
71
|
-
if (!hasChange) {
|
|
72
|
-
errors.push(`${at}: update must change at least one field`);
|
|
73
|
-
continue;
|
|
43
|
+
local.push(`${at}: invalid action ${JSON.stringify(d?.action)}`);
|
|
44
|
+
} else {
|
|
45
|
+
if (d.action === "conflict") {
|
|
46
|
+
if (!d.winner || !d.loser || d.winner === d.loser) {
|
|
47
|
+
local.push(`${at}: conflict needs distinct winner and loser`);
|
|
48
|
+
}
|
|
49
|
+
} else if (d.action === "create") {
|
|
50
|
+
// Mint a fresh memory (sleep pattern discovery). Claims no existing id,
|
|
51
|
+
// so it skips the claiming loop below; evidence is optional provenance
|
|
52
|
+
// (already filtered to real ids by the caller) and is stored in content.
|
|
53
|
+
if (typeof d.title !== "string" || !d.title.trim()) {
|
|
54
|
+
local.push(`${at}: create needs non-empty title`);
|
|
55
|
+
}
|
|
56
|
+
if (typeof d.content !== "string" || !d.content.trim()) {
|
|
57
|
+
local.push(`${at}: create needs non-empty content`);
|
|
58
|
+
}
|
|
59
|
+
if (d.importance !== undefined && (!Number.isInteger(d.importance) || d.importance < 1 || d.importance > 5)) {
|
|
60
|
+
local.push(`${at}: create importance must be an integer 1-5 when provided`);
|
|
61
|
+
}
|
|
62
|
+
if (typeof d.type !== "string" || !d.type.trim()) {
|
|
63
|
+
local.push(`${at}: create needs non-empty type`);
|
|
64
|
+
}
|
|
65
|
+
} else if (!Array.isArray(d.ids) || d.ids.length === 0) {
|
|
66
|
+
local.push(`${at}: ${d.action} needs non-empty ids`);
|
|
74
67
|
}
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
68
|
+
// update-specific field validation runs BEFORE claiming ids, so a failing
|
|
69
|
+
// update never pollutes the claimed set (which drives the "every id must
|
|
70
|
+
// appear in a decision" check below).
|
|
71
|
+
if (d.action === "update") {
|
|
72
|
+
// 只能更新单条
|
|
73
|
+
if (!Array.isArray(d.ids) || d.ids.length !== 1) {
|
|
74
|
+
local.push(`${at}: update must target exactly one id`);
|
|
75
|
+
} else {
|
|
76
|
+
// 必须产生实际变化
|
|
77
|
+
const mem = snapshot.get(d.ids[0]);
|
|
78
|
+
const hasChange = (d.title !== undefined && d.title !== mem?.title)
|
|
79
|
+
|| (d.content !== undefined && d.content !== mem?.content)
|
|
80
|
+
|| (d.importance !== undefined && d.importance !== mem?.importance);
|
|
81
|
+
if (!hasChange) {
|
|
82
|
+
local.push(`${at}: update must change at least one field`);
|
|
83
|
+
}
|
|
84
|
+
// 不能更新 summary
|
|
85
|
+
if (mem?.type === "summary") {
|
|
86
|
+
local.push(`${at}: cannot update summary via update action`);
|
|
87
|
+
}
|
|
88
|
+
// 保护期:新建记忆不可立即被 update(可配置)
|
|
89
|
+
const ageHours = (Date.now() - new Date(mem?.created_at).getTime()) / 3600000;
|
|
90
|
+
if (ageHours < minAgeHours) {
|
|
91
|
+
local.push(`${at}: memory too young (< ${minAgeHours}h)`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
79
94
|
}
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
95
|
+
if (d.action !== "create") {
|
|
96
|
+
const seen = new Set();
|
|
97
|
+
for (const id of ids) {
|
|
98
|
+
const mem = snapshot.get(id);
|
|
99
|
+
if (!mem) {
|
|
100
|
+
local.push(`${at}: unknown id ${JSON.stringify(id)}`);
|
|
101
|
+
} else if (mem.archived || mem.type === "summary") {
|
|
102
|
+
local.push(`${at}: id ${JSON.stringify(id)} is archived or summary (not eligible)`);
|
|
103
|
+
}
|
|
104
|
+
// 单决策内重复 id(同一决策引用同一记忆两次)→ 非法;跨决策重复由
|
|
105
|
+
// 全局 claimed 检测(只含已通过校验的幸存决策的 claim)。
|
|
106
|
+
if (seen.has(id)) {
|
|
107
|
+
local.push(`${at}: duplicate id ${JSON.stringify(id)} within one decision`);
|
|
108
|
+
}
|
|
109
|
+
if (claimed.has(id)) {
|
|
110
|
+
local.push(`${at}: id ${JSON.stringify(id)} claimed by multiple decisions`);
|
|
111
|
+
}
|
|
112
|
+
seen.add(id);
|
|
113
|
+
}
|
|
114
|
+
if (d.action === "merge") {
|
|
115
|
+
if (!d.keepSource || !d.ids.includes(d.keepSource)) {
|
|
116
|
+
local.push(`${at}: merge keepSource must be one of ids`);
|
|
117
|
+
}
|
|
118
|
+
if (typeof d.title !== "string" || !d.title.trim() || typeof d.content !== "string" || !d.content.trim()) {
|
|
119
|
+
local.push(`${at}: merge needs non-empty title and content`);
|
|
120
|
+
}
|
|
121
|
+
if (d.importance !== undefined && (!Number.isInteger(d.importance) || d.importance < 1 || d.importance > 5)) {
|
|
122
|
+
local.push(`${at}: merge importance must be an integer 1-5 when provided`);
|
|
123
|
+
}
|
|
124
|
+
// Merging across types would blur preference/project/decision boundaries
|
|
125
|
+
// in the injected context; the snapshot carries each entry's type.
|
|
126
|
+
// Issue #26 (P1): 默认禁止跨类型合并(类型有语义作用——preference 注入
|
|
127
|
+
// 权重更高、decision/project 注入上下文不同,合并会丢类型边界)。用户显式
|
|
128
|
+
// 开启 allowCrossTypeMerge 后放宽该检查,类型边界由用户自行承担。
|
|
129
|
+
const mergeTypes = new Set(d.ids.map((id) => snapshot.get(id)?.type));
|
|
130
|
+
if (mergeTypes.size > 1 && options.allowCrossTypeMerge !== true) {
|
|
131
|
+
local.push(`${at}: merge ids span multiple types (${[...mergeTypes].join(", ")})`);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
85
134
|
}
|
|
86
135
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
136
|
+
if (local.length > 0) {
|
|
137
|
+
if (skipInvalid) {
|
|
138
|
+
// 单条非法 → 跳过该决策,不 claim id(其目标记忆留给其它合法决策/
|
|
139
|
+
// 隐式 keep),并记录到 skipped 供调用方日志/审计。信息性跳过绝不
|
|
140
|
+
// 写入全局 errors,否则会误触发下方的整单拒绝。
|
|
141
|
+
skipped.push({
|
|
142
|
+
index,
|
|
143
|
+
action: d?.action,
|
|
144
|
+
ids,
|
|
145
|
+
error: local.join("; ")
|
|
146
|
+
});
|
|
147
|
+
} else {
|
|
148
|
+
errors.push(...local);
|
|
96
149
|
}
|
|
97
|
-
|
|
150
|
+
continue;
|
|
98
151
|
}
|
|
99
|
-
if (d.action
|
|
100
|
-
|
|
101
|
-
errors.push(`${at}: merge keepSource must be one of ids`);
|
|
102
|
-
}
|
|
103
|
-
if (typeof d.title !== "string" || !d.title.trim() || typeof d.content !== "string" || !d.content.trim()) {
|
|
104
|
-
errors.push(`${at}: merge needs non-empty title and content`);
|
|
105
|
-
}
|
|
106
|
-
if (d.importance !== undefined && (!Number.isInteger(d.importance) || d.importance < 1 || d.importance > 5)) {
|
|
107
|
-
errors.push(`${at}: merge importance must be an integer 1-5 when provided`);
|
|
108
|
-
}
|
|
109
|
-
// Merging across types would blur preference/project/decision boundaries
|
|
110
|
-
// in the injected context; the snapshot carries each entry's type.
|
|
111
|
-
const mergeTypes = new Set(d.ids.map((id) => snapshot.get(id)?.type));
|
|
112
|
-
if (mergeTypes.size > 1) {
|
|
113
|
-
errors.push(`${at}: merge ids span multiple types (${[...mergeTypes].join(", ")})`);
|
|
114
|
-
}
|
|
152
|
+
if (d.action !== "create") {
|
|
153
|
+
for (const id of ids) claimed.add(id);
|
|
115
154
|
}
|
|
155
|
+
survivors.push(d);
|
|
116
156
|
}
|
|
117
|
-
// Cap update churn: too many edits in one cycle signals a runaway model
|
|
118
|
-
|
|
157
|
+
// Cap update churn: too many edits in one cycle signals a runaway model.
|
|
158
|
+
// These are GLOBAL signals — in skipInvalid mode they still reject the whole
|
|
159
|
+
// batch, because a model that floods updates/creates is broken, not just
|
|
160
|
+
// slightly schema-drifting.
|
|
161
|
+
const updateCount = survivors.filter((d) => d.action === "update").length;
|
|
119
162
|
if (updateCount > maxUpdatePerRun) {
|
|
120
163
|
errors.push(`too many update decisions: ${updateCount} > ${maxUpdatePerRun}`);
|
|
121
164
|
}
|
|
122
165
|
// Cap pattern minting per run (sleepMaxPatternPerRun passes through here).
|
|
123
|
-
const createCount =
|
|
166
|
+
const createCount = survivors.filter((d) => d.action === "create").length;
|
|
124
167
|
const maxCreatePerRun = options.maxCreatePerRun ?? 5;
|
|
125
168
|
if (createCount > maxCreatePerRun) {
|
|
126
169
|
errors.push(`too many create decisions: ${createCount} > ${maxCreatePerRun}`);
|
|
@@ -137,25 +180,31 @@ export function validateDecisions(decisions, snapshot, options = {}) {
|
|
|
137
180
|
// snapshot(claimed.size / snapshot.size < dreamMinExplicitCoverage)时整单拒绝,
|
|
138
181
|
// 而不是用 keep 把绝大部分 snapshot 全部"通过"。
|
|
139
182
|
if (errors.length > 0) {
|
|
140
|
-
return { ok: false, errors };
|
|
183
|
+
return { ok: false, errors, skipped };
|
|
141
184
|
}
|
|
142
185
|
const minCoverage = options.dreamMinExplicitCoverage ?? 0.5;
|
|
143
186
|
if (options.dreamImplicitKeep !== false) {
|
|
144
187
|
const coverage = snapshot.size > 0 ? claimed.size / snapshot.size : 1;
|
|
145
188
|
if (coverage < minCoverage) {
|
|
146
189
|
errors.push(`explicit decision coverage ${Math.round(coverage * 100)}% < minimum ${Math.round(minCoverage * 100)}%`);
|
|
147
|
-
return { ok: false, errors };
|
|
190
|
+
return { ok: false, errors, skipped };
|
|
148
191
|
}
|
|
149
192
|
for (const id of snapshot.keys()) {
|
|
150
|
-
if (!claimed.has(id))
|
|
193
|
+
if (!claimed.has(id)) survivors.push({ action: "keep", ids: [id] });
|
|
151
194
|
}
|
|
152
195
|
} else {
|
|
153
196
|
for (const id of snapshot.keys()) {
|
|
154
197
|
if (!claimed.has(id)) errors.push(`memory ${JSON.stringify(id)} missing from decisions`);
|
|
155
198
|
}
|
|
156
|
-
if (errors.length > 0) return { ok: false, errors };
|
|
199
|
+
if (errors.length > 0) return { ok: false, errors, skipped };
|
|
200
|
+
}
|
|
201
|
+
// 调用方下游(apply/audit)复用同一 decisions 引用:就地同步为 survivors——
|
|
202
|
+
// 在 skipInvalid 模式下去掉被跳过的非法决策;在隐式 keep 下追加补齐的 keep。
|
|
203
|
+
// 内容一致时(无跳过、无补齐)为 no-op。
|
|
204
|
+
if (survivors.length !== decisions.length) {
|
|
205
|
+
decisions.splice(0, decisions.length, ...survivors);
|
|
157
206
|
}
|
|
158
|
-
return { ok: true, errors };
|
|
207
|
+
return { ok: true, errors, skipped };
|
|
159
208
|
}
|
|
160
209
|
|
|
161
210
|
/** Marker thrown when a decision target changed since the run snapshot. */
|
|
@@ -720,18 +720,27 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
720
720
|
logger?.warn?.(`dsh-mneme dream: no json array in llm output (raw length ${decisionText?.length ?? 0})`);
|
|
721
721
|
return finish({ ok: false, error: "no json array in llm output", summary: false });
|
|
722
722
|
}
|
|
723
|
-
const { ok, errors } = validateDecisions(decisions, snapshot, {
|
|
723
|
+
const { ok, errors, skipped = [] } = validateDecisions(decisions, snapshot, {
|
|
724
724
|
maxUpdatePerRun: config.reflectionUpdateMaxPerRun,
|
|
725
725
|
minAgeHours: config.reflectionUpdateMinAgeHours,
|
|
726
726
|
// v0.4.4 fix:显式透传,用户配 dreamImplicitKeep:false 时严格模式必须
|
|
727
727
|
// 真正生效,dreamMinExplicitCoverage 决定隐式 keep 下的覆盖率下限。
|
|
728
728
|
dreamImplicitKeep: config.dreamImplicitKeep,
|
|
729
|
-
dreamMinExplicitCoverage: config.dreamMinExplicitCoverage
|
|
729
|
+
dreamMinExplicitCoverage: config.dreamMinExplicitCoverage,
|
|
730
|
+
// Issue #26 (P0):跨类型 merge 等"单条非法"决策不再拖垮整批——默认开启
|
|
731
|
+
// skipInvalid,非法决策跳过、合法子集照常应用(run 记为 degraded)。
|
|
732
|
+
// dreamSkipInvalid:false 可恢复旧的"任意非法即整单拒绝"。
|
|
733
|
+
skipInvalid: config.dreamSkipInvalid !== false,
|
|
734
|
+
// Issue #26 (P1):显式开启 allowCrossTypeMerge 后放宽跨类型合并检查。
|
|
735
|
+
allowCrossTypeMerge: config.allowCrossTypeMerge === true
|
|
730
736
|
});
|
|
731
737
|
if (!ok) {
|
|
732
738
|
logger?.warn?.(`dsh-mneme dream: invalid decisions: ${errors.join("; ")}`);
|
|
733
739
|
return finish({ ok: false, error: `invalid decisions: ${errors.length} errors`, summary: false });
|
|
734
740
|
}
|
|
741
|
+
for (const s of skipped) {
|
|
742
|
+
logger?.warn?.(`dsh-mneme dream: skipping invalid decision[${s.index}] (${s.action}): ${s.error}`);
|
|
743
|
+
}
|
|
735
744
|
|
|
736
745
|
// Capture pre-update snapshots so the audit records what each update changed.
|
|
737
746
|
const updateSnapshots = {};
|
|
@@ -804,9 +813,11 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
804
813
|
);
|
|
805
814
|
// Outcome is derived from the ACTUALLY committed sub-steps, never from the
|
|
806
815
|
// raw LLM decision list — a merge whose archive step rolled back must not
|
|
807
|
-
// claim "merge-archived" (item ②). Conflicts/failures ride along
|
|
808
|
-
// audit row records why the run diverged
|
|
809
|
-
|
|
816
|
+
// claim "merge-archived" (item ②). Conflicts/failures/skipped ride along
|
|
817
|
+
// so the audit row records why the run diverged (Issue #26: skipped =
|
|
818
|
+
// decisions dropped by skipInvalid because they were individually invalid,
|
|
819
|
+
// e.g. cross-type merge).
|
|
820
|
+
const outcome = { ...buildOutcome(committed), conflicts, failures, skipped };
|
|
810
821
|
// Frozen conflicts were not adjudicated: mark both sides pending in the
|
|
811
822
|
// per-id outcome so the audit row shows they were parked, not skipped.
|
|
812
823
|
if (frozenIds.length) {
|
|
@@ -903,9 +914,12 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
903
914
|
// run. ok:false keeps the scheduler from moving the baseline.
|
|
904
915
|
// ok — either real changes landed, or a fresh summary was stored
|
|
905
916
|
// (all-keep + summary is a substantive summary refresh).
|
|
906
|
-
// degraded — real consolidation landed but the
|
|
907
|
-
//
|
|
908
|
-
//
|
|
917
|
+
// degraded — real consolidation landed but the run did not produce its
|
|
918
|
+
// full output: the summary came back empty/missing, or
|
|
919
|
+
// (Issue #26) some individually-invalid decisions were
|
|
920
|
+
// skipped by skipInvalid. The valid subset was absorbed (ok
|
|
921
|
+
// for the baseline — a future run won't re-fail on the same
|
|
922
|
+
// permanently-invalid pairs), but the run is marked, not faked.
|
|
909
923
|
let status;
|
|
910
924
|
let okResult;
|
|
911
925
|
if (partial) {
|
|
@@ -914,6 +928,13 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
914
928
|
} else if (noChange) {
|
|
915
929
|
status = summaryStored ? "ok" : "noop";
|
|
916
930
|
okResult = summaryStored;
|
|
931
|
+
} else if (skipped.length > 0) {
|
|
932
|
+
// Issue #26: valid subset landed but at least one decision was dropped as
|
|
933
|
+
// invalid (e.g. cross-type merge). degraded (not ok) — never a fake ok.
|
|
934
|
+
// autoTag still runs on degraded runs, so it is not blocked behind a
|
|
935
|
+
// "success" that a skipped-invalid run can never reach.
|
|
936
|
+
status = "degraded";
|
|
937
|
+
okResult = true;
|
|
917
938
|
} else {
|
|
918
939
|
status = summaryStored ? "ok" : "degraded";
|
|
919
940
|
okResult = true;
|
|
@@ -926,8 +947,10 @@ export function createDreamScheduler({ onRun, thresholdCount = 10, thresholdChar
|
|
|
926
947
|
outcome,
|
|
927
948
|
conflicts,
|
|
928
949
|
failures,
|
|
950
|
+
skipped,
|
|
929
951
|
frozen: frozenCount,
|
|
930
|
-
summary: summaryStored
|
|
952
|
+
summary: summaryStored,
|
|
953
|
+
error: skipped.length > 0 ? `skipped ${skipped.length} invalid decision(s)` : undefined
|
|
931
954
|
});
|
|
932
955
|
}
|
|
933
956
|
|