dsh-lite-memory 0.17.0 → 0.18.19
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 +15 -15
- package/TRIGGERS.md +1 -1
- package/client.js +343 -44
- package/docs/BENCH.md +10 -8
- package/docs/RELEASE-0.18.0.md +35 -0
- package/docs/RELEASE-0.18.1.md +18 -0
- package/docs/RELEASE-0.18.10.md +25 -0
- package/docs/RELEASE-0.18.11.md +48 -0
- package/docs/RELEASE-0.18.12.md +27 -0
- package/docs/RELEASE-0.18.13.md +13 -0
- package/docs/RELEASE-0.18.14.md +13 -0
- package/docs/RELEASE-0.18.15.md +5 -0
- package/docs/RELEASE-0.18.2.md +21 -0
- package/docs/RELEASE-0.18.3.md +16 -0
- package/docs/RELEASE-0.18.4.md +15 -0
- package/docs/RELEASE-0.18.5.md +21 -0
- package/docs/RELEASE-0.18.6.md +18 -0
- package/docs/RELEASE-0.18.7.md +23 -0
- package/docs/RELEASE-0.18.8.md +18 -0
- package/docs/RELEASE-0.18.9.md +24 -0
- package/index.js +403 -131
- package/learning.js +65 -6
- package/package.json +7 -4
- package/state.js +56 -8
- package/store.js +361 -33
- package/test/client.test.mjs +79 -17
- package/test/contract.test.mjs +231 -93
- package/test/control.test.mjs +2 -2
- package/test/crossing.test.mjs +22 -1
- package/test/eval-seed.test.mjs +113 -0
- package/test/evolution-read.test.mjs +33 -10
- package/test/ha.test.mjs +186 -0
- package/test/layer-read.test.mjs +293 -0
- package/test/learning.test.mjs +80 -0
- package/test/palace.test.mjs +2 -0
- package/test/recall.test.mjs +80 -9
- package/test/record.test.mjs +130 -0
- package/test/safety.test.mjs +119 -0
- package/test/state.test.mjs +57 -1
- package/test/store.test.mjs +49 -5
- package/test/view.test.mjs +3 -0
- package/view.js +97 -13
package/learning.js
CHANGED
|
@@ -40,6 +40,11 @@ const NEGATIVE = /不要|不用|别用|别再|停用|取消这条|这条不要|
|
|
|
40
40
|
const CORRECTION = /改成|改为|换成|重做|重来|再来|下次|不对|错了|不是这样|又没|怎么又|还是没|没按|不好|不行|不满意|不要|别/;
|
|
41
41
|
const EXECUTION_MISS = /之前说过|已经说过|不是说好|怎么又|又没|还是没|没按|又忘|重复/;
|
|
42
42
|
const UNCLEAR = /不太对|再想想|感觉不对|说不上|有点怪|不对劲/;
|
|
43
|
+
/**
|
|
44
|
+
* 点名项目范围才算全项目授权;「以后都」不等于「所有项目」。
|
|
45
|
+
* 「所有口播」「一律」这种覆盖全部对象的说法也算,但只限当前项目层。
|
|
46
|
+
*/
|
|
47
|
+
const PROJECT_WIDE = /(这个项目|本项目|所有项目|全项目|整个项目|全库|每个项目|所有(?!项目)|一律)/;
|
|
43
48
|
|
|
44
49
|
const has = (pattern, text) => pattern.test(text);
|
|
45
50
|
|
|
@@ -182,6 +187,8 @@ export function candidateFrom({ messages, anchor, sessionId, projectId, now } =
|
|
|
182
187
|
if (!context.projectId) return null;
|
|
183
188
|
/** 没有作品绑定时,规范纠正同样按项目候选收集;单次局部改动交给整理器判空,不再用电平词一刀切。 */
|
|
184
189
|
if (cue.type !== 'spec_change') return null;
|
|
190
|
+
/** 作用域在收集时就固定:只有原句点名项目范围才给全项目,否则先待确认。 */
|
|
191
|
+
const applicability = PROJECT_WIDE.test(text) ? 'all_in_scope' : 'unresolved';
|
|
185
192
|
return {
|
|
186
193
|
projectId: context.projectId,
|
|
187
194
|
sessionId: context.sessionId,
|
|
@@ -195,7 +202,7 @@ export function candidateFrom({ messages, anchor, sessionId, projectId, now } =
|
|
|
195
202
|
text,
|
|
196
203
|
sourceExcerpt,
|
|
197
204
|
cue,
|
|
198
|
-
expectedTarget: { scope: 'project', projectId, applicability
|
|
205
|
+
expectedTarget: { scope: 'project', projectId, applicability },
|
|
199
206
|
episodeUpdatedAt: '',
|
|
200
207
|
at,
|
|
201
208
|
};
|
|
@@ -356,13 +363,36 @@ export function pendingChangeFrom(feedback = {}) {
|
|
|
356
363
|
if (!currentText) return null;
|
|
357
364
|
const first = (lines[0] || currentText).split(/[,。!?;]/)[0].trim();
|
|
358
365
|
const topicKey = String(feedback.cue?.dimension || first || currentText).slice(0, 60);
|
|
366
|
+
/**
|
|
367
|
+
* 兜底不是授权:只在文本自己说到全项目时才按全项目收,否则保留它原本的适用范围
|
|
368
|
+
* (有任务种类就挂条件,没有就待确认),不能让一句「以后开头先给结论」变成全库规则。
|
|
369
|
+
*/
|
|
370
|
+
const expected = feedback.expectedTarget && typeof feedback.expectedTarget === 'object' ? feedback.expectedTarget : null;
|
|
371
|
+
const taskKind = String(feedback.cue?.taskKind || expected?.taskKind || '').trim();
|
|
372
|
+
const expectedConditions = ['taskKind', 'seriesId', 'phase']
|
|
373
|
+
.filter((key) => String(expected?.[key] || '').trim())
|
|
374
|
+
.map((key) => ({ key, value: String(expected[key]).trim() }));
|
|
375
|
+
/**
|
|
376
|
+
* 兜底不重新推导作用域:候选收集时已经固定 expectedTarget 就照抄(只能沿用或收窄),
|
|
377
|
+
* 没有 expectedTarget 的旧调用才退回原句判断。
|
|
378
|
+
*/
|
|
379
|
+
const applicability = expected
|
|
380
|
+
? (expected.applicability === 'all_in_scope'
|
|
381
|
+
? 'all_in_scope'
|
|
382
|
+
: expectedConditions.length ? 'conditional' : 'unresolved')
|
|
383
|
+
: /(所有|全都|一律|任何|默认|整个项目|这个项目|本项目)/.test(currentText)
|
|
384
|
+
? 'all_in_scope'
|
|
385
|
+
: taskKind ? 'conditional' : 'unresolved';
|
|
386
|
+
const conditions = expected
|
|
387
|
+
? (applicability === 'conditional' ? expectedConditions : [])
|
|
388
|
+
: applicability === 'conditional' ? [{ key: 'taskKind', value: taskKind }] : [];
|
|
359
389
|
return {
|
|
360
390
|
target: 'new',
|
|
361
391
|
feedbackType: 'spec_change',
|
|
362
392
|
topicKey,
|
|
363
393
|
currentText,
|
|
364
|
-
applicability
|
|
365
|
-
conditions
|
|
394
|
+
applicability,
|
|
395
|
+
conditions,
|
|
366
396
|
evidence: ['u0'],
|
|
367
397
|
persistence: 'project_candidate',
|
|
368
398
|
/** 摘录从候选一路带到提交路径,模型返回空时也不丢来源。 */
|
|
@@ -504,7 +534,7 @@ export function buildCallInput({ projectId, episodeId, taskKind, seriesId, phase
|
|
|
504
534
|
return { system, prompt, messages: [{ role: 'user', content: prompt }], refs, rules: inputRules, bytes: Buffer.byteLength(system) + Buffer.byteLength(prompt), sourceExcerpt, messageIds };
|
|
505
535
|
}
|
|
506
536
|
/** 严格解析:截断、工具调用、额外字段、超限都整批拒绝,不做第二次修复调用。 */
|
|
507
|
-
export function parseOutput(text, { finish, refs, rules, ranges } = {}) {
|
|
537
|
+
export function parseOutput(text, { finish, refs, rules, ranges, expectedTarget } = {}) {
|
|
508
538
|
if (finish && finish.kind !== 'stop' && finish.kind !== 'completed') return { ok: false, reason: '整理没有正常结束' };
|
|
509
539
|
const raw = String(text || '').trim();
|
|
510
540
|
if (!raw) return { ok: false, reason: '整理没有返回内容' };
|
|
@@ -522,14 +552,14 @@ export function parseOutput(text, { finish, refs, rules, ranges } = {}) {
|
|
|
522
552
|
if (data.changes.length > LIMITS.maxChanges) return { ok: false, reason: '一次改动太多' };
|
|
523
553
|
const changes = [];
|
|
524
554
|
for (const change of data.changes) {
|
|
525
|
-
const checked = validateChange(change, { refs, rules, ranges });
|
|
555
|
+
const checked = validateChange(change, { refs, rules, ranges, expectedTarget });
|
|
526
556
|
if (!checked.ok) return checked;
|
|
527
557
|
changes.push(checked.change);
|
|
528
558
|
}
|
|
529
559
|
return { ok: true, changes };
|
|
530
560
|
}
|
|
531
561
|
|
|
532
|
-
export function validateChange(change, { refs, rules, ranges } = {}) {
|
|
562
|
+
export function validateChange(change, { refs, rules, ranges, expectedTarget } = {}) {
|
|
533
563
|
if (!change || typeof change !== 'object' || Array.isArray(change)) return { ok: false, reason: '变化不是对象' };
|
|
534
564
|
for (const key of Object.keys(change)) if (!ALLOWED_KEYS.has(key)) return { ok: false, reason: `变化里有未知字段:${key}` };
|
|
535
565
|
const byRef = new Map((Array.isArray(rules) ? rules : []).map((row) => [row.ref, row]));
|
|
@@ -590,6 +620,35 @@ export function validateChange(change, { refs, rules, ranges } = {}) {
|
|
|
590
620
|
if (!allowed) return { ok: false, reason: '条件值不在本项目的栏目或阶段里' };
|
|
591
621
|
}
|
|
592
622
|
}
|
|
623
|
+
/** 收集时固定的作用域只能沿用或收窄:整理器不能把待确认候选抬成全项目规则。 */
|
|
624
|
+
const expected = expectedTarget && typeof expectedTarget === 'object' ? expectedTarget : null;
|
|
625
|
+
const expectedConditions = ['taskKind', 'seriesId', 'phase']
|
|
626
|
+
.filter((key) => String(expected?.[key] || '').trim())
|
|
627
|
+
.map((key) => ({ key, value: String(expected[key]).trim() }));
|
|
628
|
+
const expectedApplicability = expected
|
|
629
|
+
? (expected.applicability === 'all_in_scope'
|
|
630
|
+
? 'all_in_scope'
|
|
631
|
+
: expectedConditions.length ? 'conditional' : 'unresolved')
|
|
632
|
+
: '';
|
|
633
|
+
if (expectedApplicability && applicability === 'all_in_scope' && expectedApplicability !== 'all_in_scope') {
|
|
634
|
+
return { ok: false, reason: '适用范围不能从待确认抬成全项目' };
|
|
635
|
+
}
|
|
636
|
+
if (expected && expected.scope === 'episode' && applicability === 'all_in_scope') {
|
|
637
|
+
return { ok: false, reason: '作品规则不能抬成全项目' };
|
|
638
|
+
}
|
|
639
|
+
const effectiveConditions = conditions !== undefined ? conditions : normalizeConditions(existing?.conditions);
|
|
640
|
+
if (applicability === 'all_in_scope' && effectiveConditions.length) {
|
|
641
|
+
return { ok: false, reason: '全项目规则不能带条件' };
|
|
642
|
+
}
|
|
643
|
+
if (applicability === 'conditional' && !conditions && !existing && !sampleOnly) {
|
|
644
|
+
return { ok: false, reason: '条件适用必须给出条件' };
|
|
645
|
+
}
|
|
646
|
+
if (conditions && expectedConditions.length) {
|
|
647
|
+
const has = new Set(conditions.map((row) => `${row.key}=${row.value}`));
|
|
648
|
+
if (expectedConditions.some((row) => !has.has(`${row.key}=${row.value}`))) {
|
|
649
|
+
return { ok: false, reason: '条件不能删掉收集时的任务范围' };
|
|
650
|
+
}
|
|
651
|
+
}
|
|
593
652
|
const evidence = Array.isArray(change.evidence) ? change.evidence.map((value) => String(value)) : [];
|
|
594
653
|
if (!evidence.length || evidence.length > LIMITS.evidence) return { ok: false, reason: '证据数量不对' };
|
|
595
654
|
for (const ref of evidence) if (!refs?.feedback?.[ref]) return { ok: false, reason: '证据不在本次反馈里' };
|
package/package.json
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-lite-memory",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "On-demand memory for the DeepSeek Harness: nothing
|
|
3
|
+
"version": "0.18.19",
|
|
4
|
+
"description": "On-demand memory for the DeepSeek Harness: nothing resident, at most three small cards on a real hit, bodies read on demand.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
|
7
7
|
"engines": {
|
|
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/runtime-context.test.mjs && node test/client.test.mjs && node test/palace.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/layer-read.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 && node test/eval-seed.test.mjs",
|
|
12
12
|
"bench": "node bench/bench.mjs",
|
|
13
13
|
"bench:evolution": "node bench/evolution.mjs"
|
|
14
14
|
},
|
|
@@ -66,6 +66,9 @@
|
|
|
66
66
|
"bugs": {
|
|
67
67
|
"url": "https://github.com/regrevia/dsh-lite-memory/issues"
|
|
68
68
|
},
|
|
69
|
-
"author":
|
|
69
|
+
"author": {
|
|
70
|
+
"name": "regrevia",
|
|
71
|
+
"url": "https://github.com/regrevia"
|
|
72
|
+
},
|
|
70
73
|
"license": "MIT"
|
|
71
74
|
}
|
package/state.js
CHANGED
|
@@ -106,12 +106,13 @@ export function evolutionOf(state) {
|
|
|
106
106
|
export function controlOf(state) {
|
|
107
107
|
const raw = state && state.control;
|
|
108
108
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
109
|
-
return { enabled: true, epoch: '', changedAt: '', compatible: true };
|
|
109
|
+
return { enabled: true, epoch: '', changedAt: '', recoveryPending: false, compatible: true };
|
|
110
110
|
}
|
|
111
111
|
return {
|
|
112
|
-
enabled: raw.enabled !== false,
|
|
112
|
+
enabled: raw.enabled !== false && raw.recoveryPending !== true,
|
|
113
113
|
epoch: typeof raw.epoch === 'string' ? raw.epoch : '',
|
|
114
114
|
changedAt: typeof raw.changedAt === 'string' ? raw.changedAt : '',
|
|
115
|
+
recoveryPending: raw.recoveryPending === true,
|
|
115
116
|
compatible: true,
|
|
116
117
|
};
|
|
117
118
|
}
|
|
@@ -143,13 +144,39 @@ export function applyControl(draft, enabled) {
|
|
|
143
144
|
return { changed: true, enabled: next, epoch: config.epoch };
|
|
144
145
|
}
|
|
145
146
|
|
|
147
|
+
/**
|
|
148
|
+
* 旧策略生成的实验规则没记下作用域指纹,无法证明它该在哪些任务里出现。
|
|
149
|
+
* 只把它们转成待复核:正文、来源、版本原样保留,不删除也不改写内容。
|
|
150
|
+
*/
|
|
151
|
+
function quarantineLegacyRules(draft, now) {
|
|
152
|
+
let touched = 0;
|
|
153
|
+
for (const entry of Array.isArray(draft.entries) ? draft.entries : []) {
|
|
154
|
+
if (!entry || entry.origin !== 'project_evolution') continue;
|
|
155
|
+
const maintenance = entry.maintenance || (entry.maintenance = {});
|
|
156
|
+
if (maintenance.reviewState === 'legacy_unreviewed') continue;
|
|
157
|
+
if (maintenance.learning && maintenance.learning.specDigest) continue;
|
|
158
|
+
maintenance.reviewState = 'legacy_unreviewed';
|
|
159
|
+
maintenance.legacyReview = { reason: 'scope_unverified', at: now };
|
|
160
|
+
const learning = maintenance.learning;
|
|
161
|
+
if (learning && (learning.state === 'trial' || learning.state === 'active')) {
|
|
162
|
+
learning.state = 'paused';
|
|
163
|
+
learning.blockedReason = 'needs_scope_review';
|
|
164
|
+
learning.pausedAt = now;
|
|
165
|
+
learning.pausedBy = 'legacy_scope';
|
|
166
|
+
}
|
|
167
|
+
touched += 1;
|
|
168
|
+
}
|
|
169
|
+
return touched;
|
|
170
|
+
}
|
|
171
|
+
|
|
146
172
|
/** 自进化开关只停运行层:规则、候选、试用、证据和额度都保留,恢复时接着用。 */
|
|
147
173
|
export function applyEvolution(draft, enabled) {
|
|
148
174
|
const now = new Date().toISOString();
|
|
149
175
|
const current = evolutionOf(draft);
|
|
150
176
|
if (!current.compatible) throw new StateError('invalid', '实验配置版本不认,先不要改动开关');
|
|
151
177
|
const next = Boolean(enabled);
|
|
152
|
-
|
|
178
|
+
const quarantined = next ? quarantineLegacyRules(draft, now) : 0;
|
|
179
|
+
if (current.enabled === next) return { changed: quarantined > 0, enabled: next, quarantined };
|
|
153
180
|
const config = {
|
|
154
181
|
enabled: next,
|
|
155
182
|
epoch: crypto.randomUUID(),
|
|
@@ -174,7 +201,7 @@ export function applyEvolution(draft, enabled) {
|
|
|
174
201
|
}
|
|
175
202
|
}
|
|
176
203
|
}
|
|
177
|
-
return { changed: true, enabled: next, epoch: config.epoch };
|
|
204
|
+
return { changed: true, enabled: next, epoch: config.epoch, quarantined };
|
|
178
205
|
}
|
|
179
206
|
|
|
180
207
|
/** 容量与预算都归这里管:写入路径只报「要写什么」,能不能落地由 state 决定。 */
|
|
@@ -535,13 +562,17 @@ export function emptyState(scope) {
|
|
|
535
562
|
candidates: [],
|
|
536
563
|
receipts: [],
|
|
537
564
|
suppressions: [],
|
|
565
|
+
conflicts: [],
|
|
538
566
|
control: { enabled: true, epoch: crypto.randomUUID(), changedAt: now },
|
|
539
567
|
experimental: { projectEvolution: newEvolution() },
|
|
540
568
|
};
|
|
541
569
|
}
|
|
542
570
|
|
|
571
|
+
/** 坏文件不自动覆盖(可能还能人工找回),但错误里必须给出下一步,不能只说「坏了」。 */
|
|
543
572
|
function fail(file, why, code = 'corrupt') {
|
|
544
|
-
|
|
573
|
+
const name = path.basename(file);
|
|
574
|
+
const prev = name === 'state.json' ? 'state.prev.json' : `${name}.prev`;
|
|
575
|
+
return new StateError(code, `记忆状态${why},已停止读写:${file}。恢复:把 ${name} 改名成 ${name}.bad 后重试,会自动从上一份完好的 ${prev} 恢复;${prev} 也不在就删掉整个目录重建(旧文件可人工找回)`);
|
|
545
576
|
}
|
|
546
577
|
|
|
547
578
|
function validEntry(entry) {
|
|
@@ -566,7 +597,7 @@ export function validateState(data) {
|
|
|
566
597
|
if (typeof data[key] !== 'string' || !data[key]) throw new StateError('invalid', `记忆状态缺少 ${key}`);
|
|
567
598
|
}
|
|
568
599
|
if (!Number.isInteger(data.generation) || data.generation < 0) throw new StateError('invalid', '记忆状态 generation 不对');
|
|
569
|
-
for (const key of ['entries', 'candidates', 'receipts', 'suppressions']) {
|
|
600
|
+
for (const key of ['entries', 'candidates', 'receipts', 'suppressions', 'conflicts']) {
|
|
570
601
|
if (!Array.isArray(data[key])) throw new StateError('invalid', `记忆状态 ${key} 不是数组`);
|
|
571
602
|
}
|
|
572
603
|
const ids = new Set();
|
|
@@ -662,9 +693,12 @@ function readEnvelope(file) {
|
|
|
662
693
|
if (upgraded.experimental === undefined) {
|
|
663
694
|
upgraded.experimental = { projectEvolution: { enabled: false, epoch: '', policyVersion: EVOLUTION_POLICY_VERSION, feedbackGeneration: 0, learning: emptyLearning() } };
|
|
664
695
|
}
|
|
696
|
+
if (!Array.isArray(upgraded.conflicts)) upgraded.conflicts = [];
|
|
665
697
|
validateState(upgraded);
|
|
666
698
|
return upgraded;
|
|
667
699
|
}
|
|
700
|
+
/** 旧库没有冲突队列:读时补空数组。 */
|
|
701
|
+
if (!Array.isArray(data.payload.conflicts)) data.payload.conflicts = [];
|
|
668
702
|
validateState(data.payload);
|
|
669
703
|
return data.payload;
|
|
670
704
|
}
|
|
@@ -1135,6 +1169,20 @@ export function migrateLegacy(scope, options = {}) {
|
|
|
1135
1169
|
});
|
|
1136
1170
|
}
|
|
1137
1171
|
|
|
1172
|
+
/** 恢复出来的状态把总开关留在关闭态并记下原因,避免用一份旧快照静默恢复授权。 */
|
|
1173
|
+
function withRecoveryMarker(payload) {
|
|
1174
|
+
const raw = payload && payload.control && typeof payload.control === 'object' && !Array.isArray(payload.control) ? payload.control : {};
|
|
1175
|
+
return {
|
|
1176
|
+
...payload,
|
|
1177
|
+
control: {
|
|
1178
|
+
enabled: false,
|
|
1179
|
+
epoch: typeof raw.epoch === 'string' ? raw.epoch : '',
|
|
1180
|
+
changedAt: new Date().toISOString(),
|
|
1181
|
+
recoveryPending: true,
|
|
1182
|
+
},
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1138
1186
|
/**
|
|
1139
1187
|
* 读当前状态:只有「完全没有」才算新库;坏文件、未知版本、迁移痕迹都报错,
|
|
1140
1188
|
* 绝不用空库覆盖。
|
|
@@ -1166,7 +1214,7 @@ export function load(scope, options = {}) {
|
|
|
1166
1214
|
}
|
|
1167
1215
|
if (recovered) {
|
|
1168
1216
|
return {
|
|
1169
|
-
state: recovered,
|
|
1217
|
+
state: withRecoveryMarker(recovered),
|
|
1170
1218
|
exists: true,
|
|
1171
1219
|
recovered: true,
|
|
1172
1220
|
recovery: {
|
|
@@ -1197,7 +1245,7 @@ export function load(scope, options = {}) {
|
|
|
1197
1245
|
const prev = readEnvelope(prevFile(scope));
|
|
1198
1246
|
if (prev) {
|
|
1199
1247
|
// current 丢了但 prev 完好:恢复 prev,并标记需要重写 current。
|
|
1200
|
-
return { state: prev, exists: false, recovered: true };
|
|
1248
|
+
return { state: withRecoveryMarker(prev), exists: false, recovered: true };
|
|
1201
1249
|
}
|
|
1202
1250
|
return { state: emptyState(scope), exists: false, recovered: false };
|
|
1203
1251
|
}
|