dsh-lite-memory 0.16.4 → 0.18.1
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 +10 -10
- package/TRIGGERS.md +14 -13
- package/anchor.js +259 -24
- package/client.js +1334 -215
- package/docs/BENCH.md +10 -8
- package/docs/CONVENTIONS.md +143 -0
- package/docs/RELEASE-0.17.0.md +8 -0
- package/docs/RELEASE-0.18.0.md +35 -0
- package/docs/RELEASE-0.18.1.md +18 -0
- package/index.js +395 -105
- package/learning.js +456 -48
- package/package.json +9 -3
- package/state.js +119 -7
- package/store.js +627 -75
- package/test/anchor.test.mjs +240 -0
- package/test/client.test.mjs +216 -6
- package/test/contract.test.mjs +20 -9
- package/test/crossing.test.mjs +22 -1
- package/test/evolution-read.test.mjs +23 -0
- package/test/ha.test.mjs +136 -0
- package/test/learning.test.mjs +483 -1
- package/test/palace.test.mjs +102 -0
- package/test/recall.test.mjs +13 -0
- package/test/record.test.mjs +130 -0
- package/test/runtime-context.test.mjs +186 -0
- package/test/safety.test.mjs +119 -0
- package/test/state.test.mjs +121 -2
- package/test/store.test.mjs +454 -15
- package/view.js +71 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-lite-memory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.18.1",
|
|
4
4
|
"description": "On-demand memory for the DeepSeek Harness: nothing in the system prompt, one muted line only when something is saved, hit or suggested.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
"dsh": ">=0.1.5"
|
|
9
9
|
},
|
|
10
10
|
"scripts": {
|
|
11
|
-
"test": "node test/store.test.mjs && node test/state.test.mjs && node test/view.test.mjs && node test/anchor.test.mjs && node test/recall.test.mjs && node test/evolution-read.test.mjs && node test/lexical.test.mjs && node test/control.test.mjs && node test/learning.test.mjs && node test/scenario.test.mjs && node test/crossing.test.mjs && node test/client.test.mjs && node test/contract.test.mjs",
|
|
11
|
+
"test": "node test/store.test.mjs && node test/state.test.mjs && node test/view.test.mjs && node test/anchor.test.mjs && node test/recall.test.mjs && node test/evolution-read.test.mjs && node test/lexical.test.mjs && node test/control.test.mjs && node test/learning.test.mjs && node test/scenario.test.mjs && node test/crossing.test.mjs && node test/runtime-context.test.mjs && node test/client.test.mjs && node test/palace.test.mjs && node test/contract.test.mjs && node test/safety.test.mjs && node test/ha.test.mjs && node test/record.test.mjs",
|
|
12
12
|
"bench": "node bench/bench.mjs",
|
|
13
13
|
"bench:evolution": "node bench/evolution.mjs"
|
|
14
14
|
},
|
|
@@ -33,6 +33,9 @@
|
|
|
33
33
|
"README.md",
|
|
34
34
|
"TRIGGERS.md",
|
|
35
35
|
"docs",
|
|
36
|
+
"!docs/competitive-notes.md",
|
|
37
|
+
"!docs/ha-audit.md",
|
|
38
|
+
"!docs/ROADMAP.md",
|
|
36
39
|
"LICENSE",
|
|
37
40
|
"bench",
|
|
38
41
|
"test"
|
|
@@ -63,6 +66,9 @@
|
|
|
63
66
|
"bugs": {
|
|
64
67
|
"url": "https://github.com/regrevia/dsh-lite-memory/issues"
|
|
65
68
|
},
|
|
66
|
-
"author":
|
|
69
|
+
"author": {
|
|
70
|
+
"name": "regrevia",
|
|
71
|
+
"url": "https://github.com/regrevia"
|
|
72
|
+
},
|
|
67
73
|
"license": "MIT"
|
|
68
74
|
}
|
package/state.js
CHANGED
|
@@ -260,7 +260,7 @@ function gitRoot(cwd) {
|
|
|
260
260
|
return { root: '', reason: 'failed' };
|
|
261
261
|
} catch (error) {
|
|
262
262
|
const text = `${error && error.stderr ? error.stderr : ''}${error && error.message ? error.message : ''}`;
|
|
263
|
-
if (/not a git repository
|
|
263
|
+
if (/not a git repository|不是[\s\S]*git/i.test(text)) return { root: '', reason: 'not_git' };
|
|
264
264
|
return { root: '', reason: 'failed' };
|
|
265
265
|
}
|
|
266
266
|
}
|
|
@@ -366,6 +366,43 @@ export function knownProjectScopes(limit = 40) {
|
|
|
366
366
|
|
|
367
367
|
const stateFile = (scope) => path.join(scope.dir, 'state.json');
|
|
368
368
|
const prevFile = (scope) => path.join(scope.dir, 'state.prev.json');
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
* 损坏的 state.json 先留一份原始字节,再谈恢复:只复制、不改名、不删除,
|
|
372
|
+
* 同一份坏字节重复遇到时复用已有副本,避免每次读都堆一个新文件。
|
|
373
|
+
*/
|
|
374
|
+
function quarantineCorrupt(scope, file) {
|
|
375
|
+
let raw;
|
|
376
|
+
try {
|
|
377
|
+
raw = fs.readFileSync(file);
|
|
378
|
+
} catch {
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
let names;
|
|
382
|
+
try {
|
|
383
|
+
names = fs.readdirSync(scope.dir);
|
|
384
|
+
} catch {
|
|
385
|
+
return null;
|
|
386
|
+
}
|
|
387
|
+
for (const name of names) {
|
|
388
|
+
if (!name.startsWith('state.corrupt-') || !name.endsWith('.json')) continue;
|
|
389
|
+
const candidate = path.join(scope.dir, name);
|
|
390
|
+
try {
|
|
391
|
+
if (fs.readFileSync(candidate).equals(raw)) return candidate;
|
|
392
|
+
} catch {
|
|
393
|
+
/* 再试下一个副本 */
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
const stamp = new Date().toISOString().replace(/[-:]/g, '').replace('T', '-').replace(/\..+/, '');
|
|
397
|
+
let copy = path.join(scope.dir, `state.corrupt-${stamp}.json`);
|
|
398
|
+
if (fs.existsSync(copy)) copy = path.join(scope.dir, `state.corrupt-${stamp}-${crypto.randomBytes(3).toString('hex')}.json`);
|
|
399
|
+
try {
|
|
400
|
+
writeDurable(copy, raw);
|
|
401
|
+
} catch {
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
404
|
+
return copy;
|
|
405
|
+
}
|
|
369
406
|
const lockFile = (scope) => path.join(scope.dir, '.lock');
|
|
370
407
|
const migrationIntentFile = (scope) => path.join(scope.dir, 'migration.intent.json');
|
|
371
408
|
const purgeIntentFile = (scope) => path.join(scope.dir, 'purge.intent.json');
|
|
@@ -389,6 +426,40 @@ function sleepSync(ms) {
|
|
|
389
426
|
|
|
390
427
|
const lockDepth = new Map();
|
|
391
428
|
|
|
429
|
+
/** pid 还在不在:EPERM 说明进程存在但不属于当前用户,按「活着」处理,绝不回收。 */
|
|
430
|
+
function pidAlive(pid) {
|
|
431
|
+
if (!Number.isInteger(pid) || pid <= 0) return false;
|
|
432
|
+
try {
|
|
433
|
+
process.kill(pid, 0);
|
|
434
|
+
return true;
|
|
435
|
+
} catch (error) {
|
|
436
|
+
return error.code === 'EPERM';
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* 崩溃残留的锁:token 里的 pid 已经不在、且文件内容没被换过,才允许删。
|
|
442
|
+
* 活进程(含当前进程)的锁一律不动,迟到的新锁也不会被误删。
|
|
443
|
+
*/
|
|
444
|
+
function reclaimStaleLock(file) {
|
|
445
|
+
let owner;
|
|
446
|
+
try {
|
|
447
|
+
owner = fs.readFileSync(file, 'utf8').trim();
|
|
448
|
+
} catch {
|
|
449
|
+
return false;
|
|
450
|
+
}
|
|
451
|
+
if (!owner) return false;
|
|
452
|
+
const pid = Number.parseInt(owner.split(':')[0], 10);
|
|
453
|
+
if (!Number.isInteger(pid) || pid === process.pid || pidAlive(pid)) return false;
|
|
454
|
+
try {
|
|
455
|
+
if (fs.readFileSync(file, 'utf8').trim() !== owner) return false;
|
|
456
|
+
fs.rmSync(file, { force: true });
|
|
457
|
+
return true;
|
|
458
|
+
} catch {
|
|
459
|
+
return false;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
392
463
|
/**
|
|
393
464
|
* 跨进程一把锁:.lock 用 wx 创建,写 owner token,只释放自己那把。
|
|
394
465
|
* 同进程内允许重入(读—改—写在同一个锁窗口里连续调用)。
|
|
@@ -414,6 +485,8 @@ export function withScopeLock(scope, fn) {
|
|
|
414
485
|
fd = fs.openSync(file, 'wx');
|
|
415
486
|
} catch (error) {
|
|
416
487
|
if (error.code !== 'EEXIST') throw new StateError('unavailable', `记忆库加锁失败(${error.code || 'IO'}):${file}`);
|
|
488
|
+
/** 锁主人已经不在了(崩溃/被 kill):回收后立刻重试,不把用户卡在 500ms 的 busy 上。 */
|
|
489
|
+
if (reclaimStaleLock(file)) continue;
|
|
417
490
|
if (Date.now() > deadline) {
|
|
418
491
|
throw new StateError('busy', `记忆库正忙(另一个进程在写),稍后再试;若确认没有写入进程,删掉 ${file}`);
|
|
419
492
|
}
|
|
@@ -462,13 +535,17 @@ export function emptyState(scope) {
|
|
|
462
535
|
candidates: [],
|
|
463
536
|
receipts: [],
|
|
464
537
|
suppressions: [],
|
|
538
|
+
conflicts: [],
|
|
465
539
|
control: { enabled: true, epoch: crypto.randomUUID(), changedAt: now },
|
|
466
540
|
experimental: { projectEvolution: newEvolution() },
|
|
467
541
|
};
|
|
468
542
|
}
|
|
469
543
|
|
|
544
|
+
/** 坏文件不自动覆盖(可能还能人工找回),但错误里必须给出下一步,不能只说「坏了」。 */
|
|
470
545
|
function fail(file, why, code = 'corrupt') {
|
|
471
|
-
|
|
546
|
+
const name = path.basename(file);
|
|
547
|
+
const prev = name === 'state.json' ? 'state.prev.json' : `${name}.prev`;
|
|
548
|
+
return new StateError(code, `记忆状态${why},已停止读写:${file}。恢复:把 ${name} 改名成 ${name}.bad 后重试,会自动从上一份完好的 ${prev} 恢复;${prev} 也不在就删掉整个目录重建(旧文件可人工找回)`);
|
|
472
549
|
}
|
|
473
550
|
|
|
474
551
|
function validEntry(entry) {
|
|
@@ -493,7 +570,7 @@ export function validateState(data) {
|
|
|
493
570
|
if (typeof data[key] !== 'string' || !data[key]) throw new StateError('invalid', `记忆状态缺少 ${key}`);
|
|
494
571
|
}
|
|
495
572
|
if (!Number.isInteger(data.generation) || data.generation < 0) throw new StateError('invalid', '记忆状态 generation 不对');
|
|
496
|
-
for (const key of ['entries', 'candidates', 'receipts', 'suppressions']) {
|
|
573
|
+
for (const key of ['entries', 'candidates', 'receipts', 'suppressions', 'conflicts']) {
|
|
497
574
|
if (!Array.isArray(data[key])) throw new StateError('invalid', `记忆状态 ${key} 不是数组`);
|
|
498
575
|
}
|
|
499
576
|
const ids = new Set();
|
|
@@ -589,9 +666,12 @@ function readEnvelope(file) {
|
|
|
589
666
|
if (upgraded.experimental === undefined) {
|
|
590
667
|
upgraded.experimental = { projectEvolution: { enabled: false, epoch: '', policyVersion: EVOLUTION_POLICY_VERSION, feedbackGeneration: 0, learning: emptyLearning() } };
|
|
591
668
|
}
|
|
669
|
+
if (!Array.isArray(upgraded.conflicts)) upgraded.conflicts = [];
|
|
592
670
|
validateState(upgraded);
|
|
593
671
|
return upgraded;
|
|
594
672
|
}
|
|
673
|
+
/** 旧库没有冲突队列:读时补空数组。 */
|
|
674
|
+
if (!Array.isArray(data.payload.conflicts)) data.payload.conflicts = [];
|
|
595
675
|
validateState(data.payload);
|
|
596
676
|
return data.payload;
|
|
597
677
|
}
|
|
@@ -645,7 +725,7 @@ export function cleanStateResidue(scope) {
|
|
|
645
725
|
}
|
|
646
726
|
}
|
|
647
727
|
|
|
648
|
-
function writeDurable(file, text) {
|
|
728
|
+
export function writeDurable(file, text) {
|
|
649
729
|
const dir = path.dirname(file);
|
|
650
730
|
fs.mkdirSync(dir, { recursive: true });
|
|
651
731
|
const tmp = tempOf(file);
|
|
@@ -1078,8 +1158,39 @@ export function load(scope, options = {}) {
|
|
|
1078
1158
|
try {
|
|
1079
1159
|
data = readEnvelope(current);
|
|
1080
1160
|
} catch (error) {
|
|
1081
|
-
|
|
1082
|
-
|
|
1161
|
+
/**
|
|
1162
|
+
* 损坏的 current 不当空库覆盖:先把原始字节留成 state.corrupt-<时间戳>.json,
|
|
1163
|
+
* 再试 state.prev.json;prev 过了同一套校验才采用。两处都不可用就报出可执行的恢复路径。
|
|
1164
|
+
*/
|
|
1165
|
+
if (!error || error.code !== 'corrupt') throw error;
|
|
1166
|
+
const copy = quarantineCorrupt(scope, current);
|
|
1167
|
+
const prev = prevFile(scope);
|
|
1168
|
+
let recovered = null;
|
|
1169
|
+
try {
|
|
1170
|
+
recovered = readEnvelope(prev);
|
|
1171
|
+
} catch {
|
|
1172
|
+
recovered = null;
|
|
1173
|
+
}
|
|
1174
|
+
if (recovered) {
|
|
1175
|
+
return {
|
|
1176
|
+
state: recovered,
|
|
1177
|
+
exists: true,
|
|
1178
|
+
recovered: true,
|
|
1179
|
+
recovery: {
|
|
1180
|
+
corruptFile: copy,
|
|
1181
|
+
prevFile: prev,
|
|
1182
|
+
message: `记忆状态已损坏,已从上一版恢复(${prev});损坏副本保留在 ${copy}`,
|
|
1183
|
+
},
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
const hint = new StateError('corrupt', [
|
|
1187
|
+
`记忆状态已损坏,上一版也不可用,已停止读写:${current}`,
|
|
1188
|
+
`损坏副本已保留:${copy || '(原文件读不出来,未生成副本)'}`,
|
|
1189
|
+
`上一版路径:${prev}`,
|
|
1190
|
+
`恢复步骤:先备份 ${current} 和 ${prev};确认 ${prev} 可独立复原后把它复制成 state.json,或把损坏的 state.json 移走后重试;不要直接覆盖原文件`,
|
|
1191
|
+
].join(';'));
|
|
1192
|
+
hint.recovery = { corruptFile: copy, prevFile: prev };
|
|
1193
|
+
throw hint;
|
|
1083
1194
|
}
|
|
1084
1195
|
if (!data) {
|
|
1085
1196
|
const hasLegacy = LEGACY_MARKERS.some((name) => fs.existsSync(path.join(scope.dir, name)));
|
|
@@ -1153,7 +1264,8 @@ export function transact(scope, options, work) {
|
|
|
1153
1264
|
}].slice(-LIMITS.receipts);
|
|
1154
1265
|
}
|
|
1155
1266
|
validateState(draft);
|
|
1156
|
-
|
|
1267
|
+
/** 从 prev 恢复出来的这一轮,current 是坏的:不要把它当「上一版」写进 prev,先让好文件顶回去。 */
|
|
1268
|
+
const published = publish(scope, draft, loaded.exists && !loaded.recovered);
|
|
1157
1269
|
return { changed: true, state: draft, result: outcome.result, archived: evicted, durability: published.durability };
|
|
1158
1270
|
});
|
|
1159
1271
|
}
|