dsh-lite-memory 0.11.1 → 0.16.2
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 +44 -18
- package/anchor.js +152 -13
- package/client.js +1083 -120
- package/docs/RELEASE-0.12.0.md +15 -0
- package/docs/RELEASE-0.12.1.md +17 -0
- package/docs/RELEASE-0.12.2.md +17 -0
- package/docs/RELEASE-0.13.0.md +7 -0
- package/docs/RELEASE-0.14.0.md +7 -0
- package/docs/RELEASE-0.15.0.md +8 -0
- package/docs/RELEASE-0.16.0.md +10 -0
- package/index.js +704 -102
- package/learning.js +58 -32
- package/package.json +2 -2
- package/state.js +112 -11
- package/store.js +343 -44
- package/test/anchor.test.mjs +311 -0
- package/test/client.test.mjs +48 -6
- package/test/contract.test.mjs +214 -11
- package/test/control.test.mjs +297 -0
- package/test/evolution-read.test.mjs +196 -11
- package/test/learning.test.mjs +4 -2
- package/test/lexical.test.mjs +33 -0
- package/test/recall.test.mjs +25 -9
- package/test/state.test.mjs +21 -0
- package/test/store.test.mjs +98 -8
- package/view.js +16 -1
package/learning.js
CHANGED
|
@@ -160,25 +160,13 @@ export function normalizeConditions(conditions) {
|
|
|
160
160
|
/** 构造一次受限调用输入:无工具、无前台历史,只有身份、少量规则和反馈映射。 */
|
|
161
161
|
export function buildCallInput({ projectId, episodeId, taskKind, seriesId, phase, feedback, rules, ranges, maxBytes = LIMITS.inputBytes } = {}) {
|
|
162
162
|
const refs = { feedback: {}, rules: {} };
|
|
163
|
-
const
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
inputRules.push({
|
|
171
|
-
ref,
|
|
172
|
-
revision: record.revision || 1,
|
|
173
|
-
title: bounded(record.title, 60),
|
|
174
|
-
summary: bounded(record.summary, 120),
|
|
175
|
-
body: bounded(record.body, 360),
|
|
176
|
-
applicability: record.applicability || 'unresolved',
|
|
177
|
-
conditions: normalizeConditions(record.conditions),
|
|
178
|
-
state: record.learning?.state || record.state || 'trial',
|
|
179
|
-
});
|
|
180
|
-
index += 1;
|
|
181
|
-
}
|
|
163
|
+
const source = (Array.isArray(rules) ? rules : []).filter((record) => record && record.id);
|
|
164
|
+
/** 用户这次纠正涉及的规则先入输入:固定取前 6 条会把真正相关的旧经验挤掉。 */
|
|
165
|
+
const dimension = String(feedback?.cue?.dimension || '');
|
|
166
|
+
const prioritized = dimension
|
|
167
|
+
? [...source.filter((record) => `${record.title || ''}${record.summary || ''}${record.body || ''}`.includes(dimension)),
|
|
168
|
+
...source.filter((record) => !`${record.title || ''}${record.summary || ''}${record.body || ''}`.includes(dimension))]
|
|
169
|
+
: source;
|
|
182
170
|
refs.feedback.u0 = feedback.messageIds[0] || '';
|
|
183
171
|
const project = {
|
|
184
172
|
projectId: bounded(projectId, 80),
|
|
@@ -193,10 +181,11 @@ export function buildCallInput({ projectId, episodeId, taskKind, seriesId, phase
|
|
|
193
181
|
phase: PHASES.slice(),
|
|
194
182
|
};
|
|
195
183
|
const system = '你是项目记忆整理器,只输出 JSON。只能依据给定反馈修改已有规则或新建一条项目规则;不得调用工具,不得输出 JSON 之外的文字。没有可确定的长期变化时输出 {"changes":[]}。';
|
|
196
|
-
const
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
184
|
+
const feedbackHead = {
|
|
185
|
+
type: feedback.cue?.type || 'unclear',
|
|
186
|
+
longTerm: Boolean(feedback.cue?.explicitLongTerm),
|
|
187
|
+
};
|
|
188
|
+
const tail = {
|
|
200
189
|
allowed: { conditions: view.CONDITION_KEYS, ranges: rangesInput, maxChanges: LIMITS.maxChanges, execution_miss: '仅已有规则,currentText 写检查点', artifact_accept: '仅已有规则,记验收样例' },
|
|
201
190
|
output: {
|
|
202
191
|
changes: [{
|
|
@@ -204,19 +193,49 @@ export function buildCallInput({ projectId, episodeId, taskKind, seriesId, phase
|
|
|
204
193
|
}],
|
|
205
194
|
},
|
|
206
195
|
};
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
196
|
+
const render = (selected, feedbackText) => JSON.stringify({
|
|
197
|
+
project,
|
|
198
|
+
feedback: { ...feedbackHead, refs: [['u0', feedbackText]] },
|
|
199
|
+
rules: selected,
|
|
200
|
+
...tail,
|
|
201
|
+
});
|
|
202
|
+
/** 反馈正文按整段回退,不切开 JSON;仍放不下就是本次调用不可行。 */
|
|
203
|
+
let feedbackText = bounded(feedback.text, 600);
|
|
204
|
+
let prompt = render([], feedbackText);
|
|
205
|
+
while (Buffer.byteLength(system) + Buffer.byteLength(prompt) > maxBytes && feedbackText.length > 0) {
|
|
206
|
+
feedbackText = feedbackText.slice(0, Math.floor(feedbackText.length * 0.6));
|
|
207
|
+
prompt = render([], feedbackText);
|
|
208
|
+
}
|
|
209
|
+
if (Buffer.byteLength(system) + Buffer.byteLength(prompt) > maxBytes) {
|
|
210
|
+
return { system, prompt: '', messages: [], refs, rules: [], bytes: Buffer.byteLength(system), overLimit: true };
|
|
211
|
+
}
|
|
212
|
+
const inputRules = [];
|
|
213
|
+
for (const record of prioritized.slice(0, 6)) {
|
|
214
|
+
const ref = `r${inputRules.length}`;
|
|
215
|
+
refs.rules[ref] = record.id;
|
|
216
|
+
const row = {
|
|
217
|
+
ref,
|
|
218
|
+
revision: record.revision || 1,
|
|
219
|
+
title: bounded(record.title, 60),
|
|
220
|
+
summary: bounded(record.summary, 120),
|
|
221
|
+
body: bounded(record.body, 360),
|
|
222
|
+
applicability: record.applicability || 'unresolved',
|
|
223
|
+
conditions: normalizeConditions(record.conditions),
|
|
224
|
+
state: record.learning?.state || record.state || 'trial',
|
|
225
|
+
};
|
|
226
|
+
const next = render([...inputRules, row], feedbackText);
|
|
227
|
+
if (Buffer.byteLength(system) + Buffer.byteLength(next) > maxBytes) break;
|
|
228
|
+
inputRules.push(row);
|
|
229
|
+
prompt = next;
|
|
210
230
|
}
|
|
211
231
|
return { system, prompt, messages: [{ role: 'user', content: prompt }], refs, rules: inputRules, bytes: Buffer.byteLength(system) + Buffer.byteLength(prompt) };
|
|
212
232
|
}
|
|
213
|
-
|
|
214
233
|
/** 严格解析:截断、工具调用、额外字段、超限都整批拒绝,不做第二次修复调用。 */
|
|
215
234
|
export function parseOutput(text, { finish, refs, rules, ranges } = {}) {
|
|
216
235
|
if (finish && finish.kind !== 'stop' && finish.kind !== 'completed') return { ok: false, reason: '整理没有正常结束' };
|
|
217
236
|
const raw = String(text || '').trim();
|
|
218
237
|
if (!raw) return { ok: false, reason: '整理没有返回内容' };
|
|
219
|
-
if (raw
|
|
238
|
+
if (Buffer.byteLength(raw, 'utf8') > LIMITS.outputBytes) return { ok: false, reason: '整理输出太长' };
|
|
220
239
|
if (/<[^>]+>/.test(raw) && !raw.startsWith('{')) return { ok: false, reason: '整理返回了工具调用' };
|
|
221
240
|
let data;
|
|
222
241
|
try {
|
|
@@ -388,23 +407,29 @@ export function addCheckpoint(learning, checkpoint = {}) {
|
|
|
388
407
|
}
|
|
389
408
|
|
|
390
409
|
/** 状态只看证据强度:明确长期要求或两个独立作品才 active;条件不确定就 pending。 */
|
|
391
|
-
export function nextLearningState({ existing, change, evidence, now } = {}) {
|
|
410
|
+
export function nextLearningState({ existing, change, evidence, now, specChanged = false } = {}) {
|
|
392
411
|
const at = new Date(now || Date.now()).toISOString();
|
|
393
412
|
const learning = existing?.learning ? JSON.parse(JSON.stringify(existing.learning)) : { state: 'pending', supportedEpisodeIds: [], evidence: [] };
|
|
394
413
|
const priorState = LEARNING_STATES.includes(learning.state) ? learning.state : 'pending';
|
|
395
|
-
|
|
414
|
+
/** 规范正文或适用条件变了:旧证据不再支持新规范,只保留本次这一条。 */
|
|
415
|
+
const episodes = specChanged ? [] : (Array.isArray(learning.supportedEpisodeIds) ? learning.supportedEpisodeIds.slice() : []);
|
|
396
416
|
if (evidence?.episodeId && !episodes.includes(evidence.episodeId)) episodes.push(evidence.episodeId);
|
|
397
|
-
const anchors = Array.isArray(learning.evidence) ? learning.evidence.slice() : [];
|
|
417
|
+
const anchors = specChanged ? [] : (Array.isArray(learning.evidence) ? learning.evidence.slice() : []);
|
|
398
418
|
if (evidence?.episodeId && !anchors.some((row) => row.episodeId === evidence.episodeId)) anchors.push(evidence);
|
|
399
419
|
learning.supportedEpisodeIds = episodes.slice(-LIMITS.evidence);
|
|
400
420
|
learning.evidence = anchors.slice(-LIMITS.evidence).map((row) => ({ ...row }));
|
|
401
421
|
learning.updatedAt = at;
|
|
402
422
|
learning.lastFeedbackType = change?.feedbackType || learning.lastFeedbackType || '';
|
|
403
423
|
if (change?.currentText) learning.lastText = change.currentText;
|
|
404
|
-
if (priorState === 'paused') {
|
|
424
|
+
if (priorState === 'paused' && !specChanged && !Boolean(evidence?.explicitLongTerm)) {
|
|
405
425
|
learning.state = 'paused';
|
|
406
426
|
return learning;
|
|
407
427
|
}
|
|
428
|
+
if (priorState === 'paused') {
|
|
429
|
+
delete learning.blockedReason;
|
|
430
|
+
delete learning.pausedAt;
|
|
431
|
+
delete learning.pausedBy;
|
|
432
|
+
}
|
|
408
433
|
const conditions = normalizeConditions(change?.conditions !== undefined ? change.conditions : existing?.conditions);
|
|
409
434
|
const determinate = (change?.applicability || existing?.applicability) === 'conditional' && conditions.length > 0;
|
|
410
435
|
const explicit = Boolean(evidence?.explicitLongTerm);
|
|
@@ -425,6 +450,7 @@ export function nextLearningState({ existing, change, evidence, now } = {}) {
|
|
|
425
450
|
})) learning.state = 'trial';
|
|
426
451
|
else learning.state = 'pending';
|
|
427
452
|
if (learning.state === 'trial') learning.trialUntil = new Date((now || Date.now()) + LIMITS.trialMs).toISOString();
|
|
453
|
+
else delete learning.trialUntil;
|
|
428
454
|
if (learning.state === 'active') {
|
|
429
455
|
learning.acceptedAt = learning.acceptedAt || at;
|
|
430
456
|
delete learning.trialUntil;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-lite-memory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.2",
|
|
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/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/client.test.mjs && node test/contract.test.mjs",
|
|
12
12
|
"bench": "node bench/bench.mjs",
|
|
13
13
|
"bench:evolution": "node bench/evolution.mjs"
|
|
14
14
|
},
|
package/state.js
CHANGED
|
@@ -99,7 +99,51 @@ export function evolutionOf(state) {
|
|
|
99
99
|
};
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
-
/**
|
|
102
|
+
/**
|
|
103
|
+
* 项目记忆总开关:缺字段按开启处理,老状态不用迁移。
|
|
104
|
+
* 它管这一层的所有读取、写入、召回和学习;全局层仍按发起项目判断,不跟着关。
|
|
105
|
+
*/
|
|
106
|
+
export function controlOf(state) {
|
|
107
|
+
const raw = state && state.control;
|
|
108
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
109
|
+
return { enabled: true, epoch: '', changedAt: '', compatible: true };
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
enabled: raw.enabled !== false,
|
|
113
|
+
epoch: typeof raw.epoch === 'string' ? raw.epoch : '',
|
|
114
|
+
changedAt: typeof raw.changedAt === 'string' ? raw.changedAt : '',
|
|
115
|
+
compatible: true,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* 关总开关只是收回授权:候选、试用、生效规则、证据、默认绑定和已用额度都保留,
|
|
121
|
+
* 只清运行层(在途整理、临时锚点、旧召回快照)。重新打开也换代次,关闭前排队的任务不能补交。
|
|
122
|
+
* 自进化跟随总开关一起停:重新打开记忆后,自进化仍是关闭的,等用户单独开启。
|
|
123
|
+
*/
|
|
124
|
+
export function applyControl(draft, enabled) {
|
|
125
|
+
const now = new Date().toISOString();
|
|
126
|
+
const current = controlOf(draft);
|
|
127
|
+
const next = Boolean(enabled);
|
|
128
|
+
if (current.enabled === next) return { changed: false, enabled: next };
|
|
129
|
+
const config = { enabled: next, epoch: crypto.randomUUID(), changedAt: now };
|
|
130
|
+
draft.control = config;
|
|
131
|
+
const evolution = draft.experimental && draft.experimental.projectEvolution;
|
|
132
|
+
if (evolution && typeof evolution === 'object' && !Array.isArray(evolution)) {
|
|
133
|
+
const ledger = normalizeLearning(evolution.learning);
|
|
134
|
+
evolution.enabled = false;
|
|
135
|
+
evolution.epoch = crypto.randomUUID();
|
|
136
|
+
evolution.feedbackGeneration = Number.isInteger(evolution.feedbackGeneration) ? evolution.feedbackGeneration + 1 : 1;
|
|
137
|
+
evolution.changedAt = now;
|
|
138
|
+
/** 默认绑定属于已确认结果;临时锚点和在途观察不属于。 */
|
|
139
|
+
evolution.tasks = { ...emptyTasks(), ...(evolution.tasks && evolution.tasks.defaults ? { defaults: evolution.tasks.defaults } : {}) };
|
|
140
|
+
/** 额度只增不减:次数与回执留着,最近观察清掉。 */
|
|
141
|
+
evolution.learning = { ...ledger, recent: [] };
|
|
142
|
+
}
|
|
143
|
+
return { changed: true, enabled: next, epoch: config.epoch };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** 自进化开关只停运行层:规则、候选、试用、证据和额度都保留,恢复时接着用。 */
|
|
103
147
|
export function applyEvolution(draft, enabled) {
|
|
104
148
|
const now = new Date().toISOString();
|
|
105
149
|
const current = evolutionOf(draft);
|
|
@@ -114,10 +158,11 @@ export function applyEvolution(draft, enabled) {
|
|
|
114
158
|
changedAt: now,
|
|
115
159
|
/** 开与关都让上一轮锚点失效;已经确认的项目默认保留,它不属于临时作品状态。 */
|
|
116
160
|
tasks: { ...emptyTasks(), ...(current.tasks && current.tasks.defaults ? { defaults: current.tasks.defaults } : {}) },
|
|
117
|
-
/**
|
|
118
|
-
learning:
|
|
161
|
+
/** 只清在途观察;已学规则的来源、版本和候选都在条目里,关开不动它们。 */
|
|
162
|
+
learning: { ...normalizeLearning(current.learning), recent: [] },
|
|
119
163
|
};
|
|
120
164
|
draft.experimental = { ...(draft.experimental || {}), projectEvolution: config };
|
|
165
|
+
/** 显式关闭自进化:试用转暂停待复核,不删除候选与证据;重新打开后可逐条恢复。 */
|
|
121
166
|
if (!next) {
|
|
122
167
|
for (const entry of draft.entries) {
|
|
123
168
|
const learning = entry && entry.maintenance && entry.maintenance.learning;
|
|
@@ -128,9 +173,6 @@ export function applyEvolution(draft, enabled) {
|
|
|
128
173
|
learning.pausedBy = 'experiment_off';
|
|
129
174
|
}
|
|
130
175
|
}
|
|
131
|
-
/** 未提交的候选随关闭清掉,不保留一份看不见却还能被旧提案命中的正文。 */
|
|
132
|
-
draft.entries = draft.entries.filter((entry) => !(entry && entry.maintenance && entry.maintenance.learning && entry.maintenance.learning.state === 'pending'));
|
|
133
|
-
draft.candidates = draft.candidates.filter((row) => !row || row.origin !== 'project_evolution');
|
|
134
176
|
}
|
|
135
177
|
return { changed: true, enabled: next, epoch: config.epoch };
|
|
136
178
|
}
|
|
@@ -142,7 +184,7 @@ export const LIMITS = {
|
|
|
142
184
|
hardEntries: 1000,
|
|
143
185
|
entryBytes: 4096,
|
|
144
186
|
recordBytes: 10 * 1024,
|
|
145
|
-
totalBytes:
|
|
187
|
+
totalBytes: 1024 * 1024,
|
|
146
188
|
historyBytes: 512 * 1024,
|
|
147
189
|
stateBytes: 2 * 1024 * 1024,
|
|
148
190
|
titleChars: 60,
|
|
@@ -234,7 +276,10 @@ export function resolveRoot(cwd, options = {}) {
|
|
|
234
276
|
const explicit = options.projectRoot ? realpathOf(path.resolve(options.projectRoot)) : '';
|
|
235
277
|
if (explicit) return { root: explicit, source: 'configured', confirmed: true };
|
|
236
278
|
const configured = configuredRoot();
|
|
237
|
-
|
|
279
|
+
/** 覆盖根只对它所在的子树生效:别的目录不能被一个全局配置并成同一个项目。 */
|
|
280
|
+
if (configured && (requested === configured || requested.startsWith(`${configured}${path.sep}`))) {
|
|
281
|
+
return { root: configured, source: 'configured', confirmed: true };
|
|
282
|
+
}
|
|
238
283
|
const cacheKey = `${options.workspaceDir || ''}\n${requested}`;
|
|
239
284
|
if (rootCache.has(cacheKey)) return rootCache.get(cacheKey);
|
|
240
285
|
let resolved;
|
|
@@ -272,6 +317,53 @@ export function globalScope() {
|
|
|
272
317
|
return { key: 'global', dir: path.join(memoryRoot(), 'global'), cwd: '', rootSource: 'global', confirmed: true };
|
|
273
318
|
}
|
|
274
319
|
|
|
320
|
+
/**
|
|
321
|
+
* 管理面枚举:只读已有 state.json 的项目层,不迁移旧格式、不创建目录。
|
|
322
|
+
* 损坏或读不出的层跳过;列表按更新时间倒序,limit 防一次扫太多。
|
|
323
|
+
*/
|
|
324
|
+
export function knownProjectScopes(limit = 40) {
|
|
325
|
+
const root = memoryRoot();
|
|
326
|
+
let names;
|
|
327
|
+
try {
|
|
328
|
+
names = fs.readdirSync(root, { withFileTypes: true });
|
|
329
|
+
} catch {
|
|
330
|
+
return [];
|
|
331
|
+
}
|
|
332
|
+
const rows = [];
|
|
333
|
+
for (const item of names) {
|
|
334
|
+
if (!item.isDirectory() || item.name === 'global' || item.name === 'sessions' || item.name.startsWith('.')) continue;
|
|
335
|
+
const dir = path.join(root, item.name);
|
|
336
|
+
const file = path.join(dir, 'state.json');
|
|
337
|
+
try {
|
|
338
|
+
if (!fs.statSync(file).isFile()) continue;
|
|
339
|
+
} catch {
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
let state;
|
|
343
|
+
try {
|
|
344
|
+
state = readEnvelope(file);
|
|
345
|
+
} catch {
|
|
346
|
+
continue;
|
|
347
|
+
}
|
|
348
|
+
if (!state) continue;
|
|
349
|
+
const entries = Array.isArray(state.entries) ? state.entries : [];
|
|
350
|
+
const active = entries.filter((entry) => entry.status !== 'archived');
|
|
351
|
+
const evolution = state.experimental && state.experimental.projectEvolution ? state.experimental.projectEvolution : {};
|
|
352
|
+
rows.push({
|
|
353
|
+
key: item.name,
|
|
354
|
+
dir,
|
|
355
|
+
cwd: typeof state.cwd === 'string' ? state.cwd : '',
|
|
356
|
+
updatedAt: state.updatedAt || '',
|
|
357
|
+
entries: active.length,
|
|
358
|
+
archived: entries.length - active.length,
|
|
359
|
+
experimental: active.filter((entry) => entry.origin === 'project_evolution' || Boolean(entry.learning)).length,
|
|
360
|
+
enabled: evolution.enabled === true,
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
rows.sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)));
|
|
364
|
+
return rows.slice(0, Math.max(1, limit));
|
|
365
|
+
}
|
|
366
|
+
|
|
275
367
|
const stateFile = (scope) => path.join(scope.dir, 'state.json');
|
|
276
368
|
const prevFile = (scope) => path.join(scope.dir, 'state.prev.json');
|
|
277
369
|
const lockFile = (scope) => path.join(scope.dir, '.lock');
|
|
@@ -370,6 +462,7 @@ export function emptyState(scope) {
|
|
|
370
462
|
candidates: [],
|
|
371
463
|
receipts: [],
|
|
372
464
|
suppressions: [],
|
|
465
|
+
control: { enabled: true, epoch: crypto.randomUUID(), changedAt: now },
|
|
373
466
|
experimental: { projectEvolution: newEvolution() },
|
|
374
467
|
};
|
|
375
468
|
}
|
|
@@ -654,10 +747,12 @@ export function enforceBudget(state, scope) {
|
|
|
654
747
|
const active = state.entries.filter((entry) => entry.status === 'active');
|
|
655
748
|
const bytes = active.reduce((sum, entry) => sum + entryBytesOf(entry), 0);
|
|
656
749
|
if (active.length <= LIMITS.hardEntries && bytes <= LIMITS.totalBytes) break;
|
|
657
|
-
|
|
658
|
-
const
|
|
750
|
+
/** 用户固定过的规则不参与容量腾挪;只从没固定的条目里选最久没读的。 */
|
|
751
|
+
const movable = active.filter((entry) => entry.maintenance?.learning?.pinned !== true);
|
|
752
|
+
if (!movable.length) throw new StateError('budget_exhausted', '记忆库已满,固定住的规则不参与腾挪:先取消固定或精简正文');
|
|
753
|
+
const idle = movable.filter((entry) => now - entryLastTouch(entry) >= IDLE_MS);
|
|
754
|
+
const pool = idle.length ? idle : movable;
|
|
659
755
|
const victim = pool.reduce((oldest, entry) => (entryLastTouch(entry) < entryLastTouch(oldest) ? entry : oldest));
|
|
660
|
-
if (!victim) throw new StateError('budget_exhausted', `记忆库已满:这一条自己就超过 ${LIMITS.totalBytes} 字节正文预算,先精简正文`);
|
|
661
756
|
victim.status = 'archived';
|
|
662
757
|
victim.updatedAt = new Date().toISOString();
|
|
663
758
|
evicted.push({ id: victim.id, title: victim.current.title });
|
|
@@ -1350,6 +1445,12 @@ export function purge(scope, options = {}) {
|
|
|
1350
1445
|
at: now,
|
|
1351
1446
|
}];
|
|
1352
1447
|
draft.epoch = intent.toEpoch;
|
|
1448
|
+
/** 彻底删除也要作废本轮实验凭证:旧 contextId / 在途整理不能把刚删的规则带回来。 */
|
|
1449
|
+
const evolution = draft.experimental && draft.experimental.projectEvolution;
|
|
1450
|
+
if (evolution && evolution.enabled === true) {
|
|
1451
|
+
evolution.epoch = crypto.randomUUID();
|
|
1452
|
+
evolution.feedbackGeneration = (Number(evolution.feedbackGeneration) || 0) + 1;
|
|
1453
|
+
}
|
|
1353
1454
|
draft.generation = loaded.state.generation + 1;
|
|
1354
1455
|
draft.updatedAt = now;
|
|
1355
1456
|
validateState(draft);
|