dsh-lite-memory 0.11.1 → 0.15.0
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 +110 -13
- package/client.js +1078 -117
- 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/index.js +579 -78
- package/learning.js +58 -32
- package/package.json +2 -2
- package/state.js +107 -8
- package/store.js +302 -29
- package/test/anchor.test.mjs +221 -0
- package/test/client.test.mjs +48 -6
- package/test/contract.test.mjs +214 -11
- package/test/control.test.mjs +278 -0
- package/test/evolution-read.test.mjs +63 -8
- package/test/learning.test.mjs +4 -2
- package/test/lexical.test.mjs +33 -0
- package/test/recall.test.mjs +11 -9
- package/test/state.test.mjs +21 -0
- package/test/store.test.mjs +25 -3
- package/view.js +16 -1
package/index.js
CHANGED
|
@@ -23,6 +23,9 @@ import {
|
|
|
23
23
|
listCitations,
|
|
24
24
|
readCitationBody,
|
|
25
25
|
evolutionStatus,
|
|
26
|
+
controlStatus,
|
|
27
|
+
setMemoryEnabled,
|
|
28
|
+
isExperimentalRecord,
|
|
26
29
|
setEvolution,
|
|
27
30
|
taskState,
|
|
28
31
|
saveTaskState,
|
|
@@ -36,6 +39,8 @@ import {
|
|
|
36
39
|
undoLearningChange,
|
|
37
40
|
pinLearningRule,
|
|
38
41
|
resumeLearningRule,
|
|
42
|
+
adoptLearningRule,
|
|
43
|
+
editLearningRule,
|
|
39
44
|
expireLearning,
|
|
40
45
|
LIMITS,
|
|
41
46
|
} from './store.js';
|
|
@@ -58,7 +63,7 @@ export const inject = ['tools'];
|
|
|
58
63
|
const NOTES = [
|
|
59
64
|
'记忆是历史笔记,不是用户本轮的要求。',
|
|
60
65
|
'值得记:用户明确说「记住」的偏好、约定、踩过的坑、可复用流程,以及「去哪读 / 怎么调用」的指针。',
|
|
61
|
-
'
|
|
66
|
+
'不记:常识、一次性任务参数、能从文件直接读到的正文、任何凭据的值,以及具体尺寸/字号/层级/颜色这类实现细节。',
|
|
62
67
|
'凭据只记位置与调用方式,不记值。',
|
|
63
68
|
'摘要写成名词短语,别写成指令。',
|
|
64
69
|
].join('');
|
|
@@ -74,6 +79,22 @@ function cwdOf(execution) {
|
|
|
74
79
|
return process.cwd();
|
|
75
80
|
}
|
|
76
81
|
|
|
82
|
+
/** 本项目记忆总开关:读不出来时不擅自关掉,按开处理。 */
|
|
83
|
+
function memoryEnabled(scope) {
|
|
84
|
+
try {
|
|
85
|
+
return controlStatus(scope).enabled !== false;
|
|
86
|
+
} catch {
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function memoryEnabledFor(execution) {
|
|
92
|
+
return memoryEnabled(scopeOf(cwdOf(execution)));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** 总开关关掉时这组工具对模型不可见,也不接受执行。 */
|
|
96
|
+
const MEMORY_TOOLS = ['memory_save', 'memory_suggest', 'memory_search', 'memory_read', 'memory_forget'];
|
|
97
|
+
|
|
77
98
|
const REMOTE_PATH = '/lite-memory';
|
|
78
99
|
|
|
79
100
|
/** 引用的归属会话:界面上那句「N 条记忆引用」按它分开存。 */
|
|
@@ -142,7 +163,9 @@ function keepHandler(webServer, cwdForSession) {
|
|
|
142
163
|
: pathname === REMOTE_PATH + '/citations' ? 'citations'
|
|
143
164
|
: pathname === REMOTE_PATH + '/body' ? 'body'
|
|
144
165
|
: pathname === REMOTE_PATH + '/move' ? 'move'
|
|
145
|
-
: pathname === REMOTE_PATH + '/
|
|
166
|
+
: pathname === REMOTE_PATH + '/overview' ? 'overview'
|
|
167
|
+
: pathname === REMOTE_PATH + '/control' ? 'control'
|
|
168
|
+
: pathname === REMOTE_PATH + '/evolution' ? 'evolution'
|
|
146
169
|
: pathname === REMOTE_PATH + '/resume-move' ? 'resume-move' : '';
|
|
147
170
|
if (!action) return sendJson(res, 404, { ok: false, reason: '未知操作' });
|
|
148
171
|
let body;
|
|
@@ -161,8 +184,28 @@ function keepHandler(webServer, cwdForSession) {
|
|
|
161
184
|
if (!id) return sendJson(res, 400, { ok: false, reason: '缺少 id' });
|
|
162
185
|
const sessionId = typeof body.sessionId === 'string' ? body.sessionId : '';
|
|
163
186
|
const cwd = typeof body.cwd === 'string' ? body.cwd : '';
|
|
187
|
+
const scopeKey = typeof body.scopeKey === 'string' ? body.scopeKey : '';
|
|
164
188
|
let found;
|
|
165
|
-
if (
|
|
189
|
+
if (scopeKey) {
|
|
190
|
+
/**
|
|
191
|
+
* 面板可以读枚举出来的项目层,但键必须来自服务端列表;
|
|
192
|
+
* 不接受客户端拼路径,会话也只用来确认当前身份。
|
|
193
|
+
*/
|
|
194
|
+
if (!cwdForSession(sessionId)) return sendJson(res, 409, { ok: false, reason: '这个会话还没有项目目录' });
|
|
195
|
+
const listed = state.knownProjectScopes().find((row) => row.key === scopeKey);
|
|
196
|
+
const target = scopeKey === 'global' ? globalScope()
|
|
197
|
+
: listed ? { key: listed.key, dir: listed.dir, cwd: listed.cwd || '', requestedCwd: listed.cwd || '', rootSource: 'configured', confirmed: true }
|
|
198
|
+
: null;
|
|
199
|
+
if (!target) return sendJson(res, 404, { ok: false, reason: '没有这个项目' });
|
|
200
|
+
const located = locate(target, id, { readOnly: true });
|
|
201
|
+
if (!located) return sendJson(res, 200, { ok: true, status: 'gone', id });
|
|
202
|
+
found = {
|
|
203
|
+
...located.record,
|
|
204
|
+
scope: located.scope.key === 'global' ? 'global' : 'project',
|
|
205
|
+
scopeLabel: located.scope.key === 'global' ? '全局' : path.basename(located.scope.cwd || '') || located.scope.key,
|
|
206
|
+
body: located.record.body,
|
|
207
|
+
};
|
|
208
|
+
} else if (sessionId) {
|
|
166
209
|
try {
|
|
167
210
|
found = readCitationBody(sessionId, id);
|
|
168
211
|
} catch {
|
|
@@ -171,7 +214,7 @@ function keepHandler(webServer, cwdForSession) {
|
|
|
171
214
|
/** 不在本会话引用里就当已消失:不给跨会话探正文,界面也只显示「已删除」。 */
|
|
172
215
|
if (!found) return sendJson(res, 200, { ok: true, status: 'gone', id });
|
|
173
216
|
} else if (cwd) {
|
|
174
|
-
const located = locate(scopeOf(cwd), id);
|
|
217
|
+
const located = locate(scopeOf(cwd), id, { readOnly: true });
|
|
175
218
|
if (!located) return sendJson(res, 200, { ok: true, status: 'gone', id });
|
|
176
219
|
found = {
|
|
177
220
|
...located.record,
|
|
@@ -196,6 +239,207 @@ function keepHandler(webServer, cwdForSession) {
|
|
|
196
239
|
});
|
|
197
240
|
}
|
|
198
241
|
|
|
242
|
+
/**
|
|
243
|
+
* 面板总览:当前会话项目、可切换的项目层与全局层,都从服务端枚举。
|
|
244
|
+
* 只读;正文不进列表,点开单条再按 scopeKey 取。
|
|
245
|
+
*/
|
|
246
|
+
if (action === 'overview') {
|
|
247
|
+
const sessionId = typeof body.sessionId === 'string' ? body.sessionId : '';
|
|
248
|
+
if (!sessionId) return sendJson(res, 400, { ok: false, reason: '缺少 sessionId' });
|
|
249
|
+
const cwd = typeof cwdForSession === 'function' ? cwdForSession(sessionId) : '';
|
|
250
|
+
if (!cwd) return sendJson(res, 409, { ok: false, reason: '这个会话还没有项目目录' });
|
|
251
|
+
const scope = scopeOf(cwd);
|
|
252
|
+
if (!scope.confirmed) return sendJson(res, 409, { ok: false, reason: '项目根还没确认,先重新打开这个项目' });
|
|
253
|
+
let currentState;
|
|
254
|
+
try {
|
|
255
|
+
currentState = state.load(scope).state;
|
|
256
|
+
} catch (error) {
|
|
257
|
+
return sendJson(res, 409, { ok: false, reason: error instanceof Error ? error.message : '这个项目的记忆读不出来' });
|
|
258
|
+
}
|
|
259
|
+
const currentEvolution = (() => {
|
|
260
|
+
try {
|
|
261
|
+
return learningSnapshot(scope, { readOnly: true });
|
|
262
|
+
} catch {
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
})();
|
|
266
|
+
const labelOf = (target) => target.key === 'global' ? '全局' : (path.basename(target.cwd || '') || target.key);
|
|
267
|
+
const listed = state.knownProjectScopes();
|
|
268
|
+
const scopes = listed.map((row) => ({
|
|
269
|
+
key: row.key,
|
|
270
|
+
kind: 'project',
|
|
271
|
+
label: path.basename(row.cwd || '') || row.key,
|
|
272
|
+
entries: row.entries,
|
|
273
|
+
archived: row.archived,
|
|
274
|
+
experimental: row.experimental,
|
|
275
|
+
enabled: row.enabled,
|
|
276
|
+
updatedAt: row.updatedAt,
|
|
277
|
+
}));
|
|
278
|
+
if (!scopes.some((row) => row.key === scope.key)) {
|
|
279
|
+
const active = currentState.entries.filter((entry) => entry.status !== 'archived');
|
|
280
|
+
scopes.unshift({
|
|
281
|
+
key: scope.key,
|
|
282
|
+
kind: 'project',
|
|
283
|
+
label: labelOf(scope),
|
|
284
|
+
entries: active.length,
|
|
285
|
+
archived: currentState.entries.length - active.length,
|
|
286
|
+
experimental: active.filter((entry) => isExperimentalRecord(entry)).length,
|
|
287
|
+
enabled: currentState.experimental?.projectEvolution?.enabled === true,
|
|
288
|
+
updatedAt: currentState.updatedAt || '',
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
let globalState = null;
|
|
292
|
+
try {
|
|
293
|
+
globalState = state.load(globalScope()).state;
|
|
294
|
+
} catch {
|
|
295
|
+
globalState = null;
|
|
296
|
+
}
|
|
297
|
+
if (globalState) {
|
|
298
|
+
const active = globalState.entries.filter((entry) => entry.status !== 'archived');
|
|
299
|
+
scopes.push({
|
|
300
|
+
key: 'global',
|
|
301
|
+
kind: 'global',
|
|
302
|
+
label: '全局',
|
|
303
|
+
entries: active.length,
|
|
304
|
+
archived: globalState.entries.length - active.length,
|
|
305
|
+
experimental: 0,
|
|
306
|
+
enabled: false,
|
|
307
|
+
updatedAt: globalState.updatedAt || '',
|
|
308
|
+
});
|
|
309
|
+
} else {
|
|
310
|
+
scopes.push({ key: 'global', kind: 'global', label: '全局', entries: 0, archived: 0, experimental: 0, enabled: false, updatedAt: '' });
|
|
311
|
+
}
|
|
312
|
+
const requested = typeof body.scopeKey === 'string' ? body.scopeKey : '';
|
|
313
|
+
let selectedScope = scope;
|
|
314
|
+
if (requested === 'global') selectedScope = globalScope();
|
|
315
|
+
else if (requested) {
|
|
316
|
+
if (requested === scope.key) selectedScope = scope;
|
|
317
|
+
else {
|
|
318
|
+
const row = listed.find((item) => item.key === requested);
|
|
319
|
+
if (!row) return sendJson(res, 404, { ok: false, reason: '没有这个项目' });
|
|
320
|
+
selectedScope = { key: row.key, dir: row.dir, cwd: row.cwd || '', requestedCwd: row.cwd || '', rootSource: 'configured', confirmed: true };
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
let index;
|
|
324
|
+
try {
|
|
325
|
+
index = loadIndex(selectedScope, { readOnly: true });
|
|
326
|
+
} catch (error) {
|
|
327
|
+
return sendJson(res, 409, { ok: false, reason: error instanceof Error ? error.message : '这个项目的记忆读不出来' });
|
|
328
|
+
}
|
|
329
|
+
const records = index.entries.filter((entry) => entry.status !== 'archived').map((record) => ({
|
|
330
|
+
id: record.id,
|
|
331
|
+
title: record.title || '',
|
|
332
|
+
summary: record.summary || '',
|
|
333
|
+
kind: record.kind || 'note',
|
|
334
|
+
when: record.when || '',
|
|
335
|
+
tags: Array.isArray(record.tags) ? record.tags.slice(0, 12) : [],
|
|
336
|
+
topicKey: record.topicKey || '',
|
|
337
|
+
triggers: Array.isArray(record.triggers) ? record.triggers.slice(0, 8) : [],
|
|
338
|
+
applicability: record.applicability || 'all_in_scope',
|
|
339
|
+
conditions: Array.isArray(record.conditions) ? record.conditions.slice(0, 8) : [],
|
|
340
|
+
selection: record.selection || null,
|
|
341
|
+
links: Array.isArray(record.links) ? record.links.slice(0, 24).map((link) => ({ type: link.type || 'related', id: link.id })) : [],
|
|
342
|
+
revision: Number(record.revision) || 1,
|
|
343
|
+
origin: record.origin || 'user_explicit',
|
|
344
|
+
updated: record.updated || '',
|
|
345
|
+
lastUsed: record.lastUsed || '',
|
|
346
|
+
bytes: Number(record.bytes) || 0,
|
|
347
|
+
experimental: isExperimentalRecord(record),
|
|
348
|
+
learning: record.learning ? {
|
|
349
|
+
state: record.learning.state || '',
|
|
350
|
+
pinned: record.learning.pinned === true,
|
|
351
|
+
samples: Array.isArray(record.learning.samples) ? record.learning.samples.length : 0,
|
|
352
|
+
checkpoints: Array.isArray(record.learning.checkpoints) ? record.learning.checkpoints.length : 0,
|
|
353
|
+
evidence: Array.isArray(record.learning.evidence) ? record.learning.evidence.length : 0,
|
|
354
|
+
versions: Array.isArray(record.learning.versions) ? record.learning.versions.length : 0,
|
|
355
|
+
} : null,
|
|
356
|
+
}));
|
|
357
|
+
const archives = Array.isArray(index.archived) ? index.archived : [];
|
|
358
|
+
const archived = archives.slice(0, 80).map((record) => ({
|
|
359
|
+
id: record.id,
|
|
360
|
+
title: record.title || '',
|
|
361
|
+
revision: Number(record.revision) || 1,
|
|
362
|
+
updated: record.updated || '',
|
|
363
|
+
}));
|
|
364
|
+
const tags = new Set();
|
|
365
|
+
let links = 0;
|
|
366
|
+
for (const record of records) {
|
|
367
|
+
for (const tag of record.tags) tags.add(tag);
|
|
368
|
+
links += record.links.length;
|
|
369
|
+
}
|
|
370
|
+
const currentControl = (() => {
|
|
371
|
+
try { return controlStatus(scope); } catch { return { enabled: true, epoch: '' }; }
|
|
372
|
+
})();
|
|
373
|
+
const selectedControl = selectedScope.key === scope.key
|
|
374
|
+
? currentControl
|
|
375
|
+
: (() => {
|
|
376
|
+
try { return controlStatus(selectedScope); } catch { return { enabled: true, epoch: '' }; }
|
|
377
|
+
})();
|
|
378
|
+
const selectedEvolution = selectedScope.key === scope.key
|
|
379
|
+
? currentEvolution
|
|
380
|
+
: (() => {
|
|
381
|
+
try {
|
|
382
|
+
return learningSnapshot(selectedScope, { readOnly: true });
|
|
383
|
+
} catch {
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
})();
|
|
387
|
+
return sendJson(res, 200, {
|
|
388
|
+
ok: true,
|
|
389
|
+
current: { key: scope.key, label: labelOf(scope), control: currentControl, evolution: currentEvolution },
|
|
390
|
+
scopes,
|
|
391
|
+
selected: {
|
|
392
|
+
key: selectedScope.key,
|
|
393
|
+
kind: selectedScope.key === 'global' ? 'global' : 'project',
|
|
394
|
+
label: labelOf(selectedScope),
|
|
395
|
+
updatedAt: records.reduce((latest, record) => record.updated > latest ? record.updated : latest, ''),
|
|
396
|
+
control: selectedControl,
|
|
397
|
+
evolution: selectedEvolution,
|
|
398
|
+
records,
|
|
399
|
+
archived,
|
|
400
|
+
stats: {
|
|
401
|
+
total: records.length,
|
|
402
|
+
experimental: records.filter((record) => record.experimental).length,
|
|
403
|
+
links,
|
|
404
|
+
tags: tags.size,
|
|
405
|
+
archived: archives.length,
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* 本项目记忆总开关:关闭后这层不再召回、检索、读写、学习,也不继承全局记忆。
|
|
413
|
+
* 自进化设置原样保留,但有效状态随总开关暂停。
|
|
414
|
+
*/
|
|
415
|
+
if (action === 'control') {
|
|
416
|
+
const sessionId = typeof body.sessionId === 'string' ? body.sessionId : '';
|
|
417
|
+
if (!sessionId) return sendJson(res, 400, { ok: false, reason: '缺少 sessionId' });
|
|
418
|
+
const cwd = typeof cwdForSession === 'function' ? cwdForSession(sessionId) : '';
|
|
419
|
+
if (!cwd) return sendJson(res, 409, { ok: false, reason: '这个会话还没有项目目录' });
|
|
420
|
+
const scope = scopeOf(cwd);
|
|
421
|
+
if (!scope.confirmed) return sendJson(res, 409, { ok: false, reason: '项目根还没确认,先重新打开这个项目' });
|
|
422
|
+
const operationId = typeof body.operationId === 'string' ? body.operationId : '';
|
|
423
|
+
try {
|
|
424
|
+
const result = typeof body.enabled === 'boolean'
|
|
425
|
+
? setMemoryEnabled(scope, body.enabled, { operationId })
|
|
426
|
+
: { ok: true, ...controlStatus(scope) };
|
|
427
|
+
if (typeof body.enabled === 'boolean' && result && result.ok !== false && body.enabled === false) {
|
|
428
|
+
const controller = learningAborts.get(scope.key);
|
|
429
|
+
if (controller) {
|
|
430
|
+
try { controller.abort(); } catch { /* already gone */ }
|
|
431
|
+
learningAborts.delete(scope.key);
|
|
432
|
+
}
|
|
433
|
+
}
|
|
434
|
+
return sendJson(res, result.ok === false ? 409 : 200, {
|
|
435
|
+
...result,
|
|
436
|
+
project: path.basename(scope.cwd || '') || scope.key,
|
|
437
|
+
});
|
|
438
|
+
} catch (error) {
|
|
439
|
+
return sendJson(res, 409, { ok: false, reason: error instanceof Error ? error.message : '记忆开关没有改动' });
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
199
443
|
/**
|
|
200
444
|
* 项目实验开关:作用域只由服务端按 cwd 判,身份不确定就不写。
|
|
201
445
|
* 不传 enabled 是查状态;传布尔才是切换,切换在项目锁里旋转 epoch。
|
|
@@ -213,8 +457,11 @@ function keepHandler(webServer, cwdForSession) {
|
|
|
213
457
|
const learningAction = typeof body.learningAction === 'string' ? body.learningAction : '';
|
|
214
458
|
if (learningAction) {
|
|
215
459
|
try {
|
|
460
|
+
if (learningAction !== 'snapshot' && !memoryEnabled(scope)) {
|
|
461
|
+
return sendJson(res, 409, { ok: false, code: 'memory_disabled', reason: '本项目记忆已关闭' });
|
|
462
|
+
}
|
|
216
463
|
if (learningAction === 'snapshot') {
|
|
217
|
-
|
|
464
|
+
/** 查看不改盘:到期状态在写入事务里清理,读路径不写文件。 */
|
|
218
465
|
const request = typeof body.sessionId === 'string' ? body.sessionId : '';
|
|
219
466
|
return sendJson(res, 200, {
|
|
220
467
|
ok: true,
|
|
@@ -238,12 +485,30 @@ function keepHandler(webServer, cwdForSession) {
|
|
|
238
485
|
});
|
|
239
486
|
return sendJson(res, result && result.ok !== false ? 200 : 409, { ...result });
|
|
240
487
|
}
|
|
488
|
+
if (learningAction === 'adopt') {
|
|
489
|
+
const result = adoptLearningRule(scope, id, {
|
|
490
|
+
expectedRevision: Number(body.expectedRevision || 0),
|
|
491
|
+
title: typeof body.title === 'string' ? body.title : '',
|
|
492
|
+
body: typeof body.body === 'string' ? body.body : '',
|
|
493
|
+
operationId: operationId || `adopt_learn_${id}_${Date.now()}`,
|
|
494
|
+
});
|
|
495
|
+
return sendJson(res, result && result.ok !== false ? 200 : 409, { ...result });
|
|
496
|
+
}
|
|
497
|
+
if (learningAction === 'edit') {
|
|
498
|
+
const result = editLearningRule(scope, id, {
|
|
499
|
+
expectedRevision: Number(body.expectedRevision || 0),
|
|
500
|
+
title: typeof body.title === 'string' ? body.title : '',
|
|
501
|
+
body: typeof body.body === 'string' ? body.body : '',
|
|
502
|
+
operationId: operationId || `edit_learn_${id}_${Date.now()}`,
|
|
503
|
+
});
|
|
504
|
+
return sendJson(res, result && result.ok !== false ? 200 : 409, { ...result });
|
|
505
|
+
}
|
|
241
506
|
if (learningAction === 'pause') {
|
|
242
507
|
const ids = Array.isArray(body.ids) ? body.ids.filter((value) => typeof value === 'string').slice(0, 12) : [];
|
|
243
508
|
const result = pauseLearningRules(scope, ids, { operationId: operationId || `pause_learn_${Date.now()}`, by: 'management', reason: 'paused_by_user' });
|
|
244
509
|
return sendJson(res, result && result.ok !== false ? 200 : 409, { ...result });
|
|
245
510
|
}
|
|
246
|
-
if (learningAction === 'delete') {
|
|
511
|
+
if (learningAction === 'delete' || learningAction === 'discard') {
|
|
247
512
|
const result = removeLearningRule(scope, id, {
|
|
248
513
|
expectedRevision: Number(body.expectedRevision || 0),
|
|
249
514
|
operationId: operationId || `delete_learn_${id}_${Date.now()}`,
|
|
@@ -294,6 +559,11 @@ function keepHandler(webServer, cwdForSession) {
|
|
|
294
559
|
: typeof body.suggestionId === 'string' ? body.suggestionId : '';
|
|
295
560
|
const sessionId = typeof body.sessionId === 'string' ? body.sessionId : '';
|
|
296
561
|
|
|
562
|
+
/** 总开关关闭时面板只能看:保留、拒绝、搬移、撤销都不落盘。 */
|
|
563
|
+
if (!controlStatus(scope).enabled) {
|
|
564
|
+
return sendJson(res, 409, { ok: false, reason: '本项目记忆已关闭,先在面板打开', code: 'memory_disabled' });
|
|
565
|
+
}
|
|
566
|
+
|
|
297
567
|
if (action === 'move' || action === 'resume-move') {
|
|
298
568
|
const operationId = typeof body.operationId === 'string' ? body.operationId : '';
|
|
299
569
|
try {
|
|
@@ -366,7 +636,7 @@ const SAVE_PARAMS = {
|
|
|
366
636
|
id: { type: 'string', description: '要更新的记忆 id(来自搜索结果或相近提示)。同一件事更新时用它,别另建一条' },
|
|
367
637
|
title: { type: 'string', description: `标题:名词短语,用户一眼能认出是哪件事,不超过 ${LIMITS.titleChars} 字。它就是这条记忆的身份` },
|
|
368
638
|
summary: { type: 'string', description: `一句话说清要记的内容本身——用户只看这一行决定留不留,名词短语,不超过 ${LIMITS.summaryChars} 字` },
|
|
369
|
-
body: { type: 'string', description: `正文 markdown,整段替换旧正文,不超过 ${LIMITS.entryBytes}
|
|
639
|
+
body: { type: 'string', description: `正文 markdown,整段替换旧正文,不超过 ${LIMITS.entryBytes} 字节。只写原则、判断规则、触发条件和边界;不写像素、字号、层级、颜色、一次性数字或代码片段,这些查当前代码或文档。` },
|
|
370
640
|
when: { type: 'string', description: `前置条件或适用场景(「在哪台机器 / 哪个环境 / 什么情况下」),不超过 ${LIMITS.whenChars} 字。同一标题但前置条件不同,会存成两条,不合并;更新时不给就保留原值` },
|
|
371
641
|
kind: { type: 'string', enum: ['preference', 'convention', 'procedure', 'pointer', 'pitfall', 'credential_hint', 'note'], description: '条目类型;更新时不给就保留原值' },
|
|
372
642
|
tags: { type: 'array', items: { type: 'string' }, description: '最多 5 个标签;更新时不给就保留原值' },
|
|
@@ -491,8 +761,21 @@ const FORGET_PARAMS = {
|
|
|
491
761
|
additionalProperties: false,
|
|
492
762
|
};
|
|
493
763
|
|
|
494
|
-
|
|
495
|
-
|
|
764
|
+
/** 记忆归属标记:全局 `[global]`,项目用项目目录名(如 `[DSH]`)。 */
|
|
765
|
+
function projectNameOf(scope) {
|
|
766
|
+
const base = scope && scope.cwd ? path.basename(scope.cwd) : '';
|
|
767
|
+
return String(base || '').replace(/[^\w.\-\u4e00-\u9fff]+/g, '-').replace(/^-+|-+$/g, '').slice(0, 16);
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
function scopeTag(value) {
|
|
771
|
+
if (value && value.scope === 'global') return '[global]';
|
|
772
|
+
if (value && value.scopeName) return `[${value.scopeName}]`;
|
|
773
|
+
return '';
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function withTag(value, text) {
|
|
777
|
+
const tag = scopeTag(value);
|
|
778
|
+
return tag ? `${tag} ${text}` : text;
|
|
496
779
|
}
|
|
497
780
|
|
|
498
781
|
function statusLabel(status) {
|
|
@@ -515,7 +798,8 @@ const RECALL_HEADER = '已有记忆可能相关(需要细节用 memory_read
|
|
|
515
798
|
|
|
516
799
|
/** 卡片只放够判断要不要读的几行;必要前提整条在场,装不下就整卡省略。 */
|
|
517
800
|
function renderRecallCard(card) {
|
|
518
|
-
const
|
|
801
|
+
const tag = card.scope === 'global' ? '[global]' : card.scopeName ? `[${card.scopeName}]` : '';
|
|
802
|
+
const parts = [`- ${tag ? `${tag} ` : ''}「${card.title}」 · ${card.id}`];
|
|
519
803
|
if (card.summary) parts.push(`|${card.summary}`);
|
|
520
804
|
if (card.conditions && card.conditions.length) parts.push(`|前提:${card.conditions.join('、')}`);
|
|
521
805
|
if (card.selection) parts.push(`|只适用于 ${card.selection}`);
|
|
@@ -527,6 +811,29 @@ export function apply(ctx, config) {
|
|
|
527
811
|
const tools = ctx.tools;
|
|
528
812
|
const getService = (name) => (typeof ctx.get === 'function' ? ctx.get(name) : undefined);
|
|
529
813
|
|
|
814
|
+
/** 关掉记忆的会话看不到这组工具;每次进入新步骤前按当前开关同步一次。 */
|
|
815
|
+
const hiddenTools = new WeakMap();
|
|
816
|
+
const memoryOnForAgent = (agent) => {
|
|
817
|
+
try {
|
|
818
|
+
return memoryEnabled(scopeOf(cwdOf({ agent })));
|
|
819
|
+
} catch {
|
|
820
|
+
return true;
|
|
821
|
+
}
|
|
822
|
+
};
|
|
823
|
+
const syncMemoryTools = (agent) => {
|
|
824
|
+
const scoped = agent?.ctx?.tools;
|
|
825
|
+
if (!scoped) return;
|
|
826
|
+
const dispose = hiddenTools.get(agent);
|
|
827
|
+
if (memoryOnForAgent(agent)) {
|
|
828
|
+
if (!dispose) return;
|
|
829
|
+
hiddenTools.delete(agent);
|
|
830
|
+
try { dispose(); } catch { /* scope already gone */ }
|
|
831
|
+
return;
|
|
832
|
+
}
|
|
833
|
+
if (dispose) return;
|
|
834
|
+
try { hiddenTools.set(agent, scoped.restrict({ deny: MEMORY_TOOLS })); } catch { /* no global tools to mask */ }
|
|
835
|
+
};
|
|
836
|
+
|
|
530
837
|
/* ---------------- 自动召回:策略、冻结快照、任务窗口与请求预算 ---------------- */
|
|
531
838
|
|
|
532
839
|
/** 升级不覆盖用户的选择:默认 adaptive,别的模式只由明确配置切换。 */
|
|
@@ -543,6 +850,8 @@ export function apply(ctx, config) {
|
|
|
543
850
|
const seenHumans = new Map();
|
|
544
851
|
/** 本轮真人反馈先记在这里,等 turn/end 再决定要不要整理;不跨会话。 */
|
|
545
852
|
const feedbackCandidates = new Map();
|
|
853
|
+
/** 项目内反馈代次:每来一次新的可整理反馈就加一,旧整理结果必须作废。 */
|
|
854
|
+
const feedbackRevisions = new Map();
|
|
546
855
|
/** 一个项目同一时间只整理一次;控制器放在模块级供开关路由中止。 */
|
|
547
856
|
const learningJobs = new Map();
|
|
548
857
|
|
|
@@ -614,7 +923,8 @@ export function apply(ctx, config) {
|
|
|
614
923
|
const index = loadIndex(source);
|
|
615
924
|
const meta = index.__meta || {};
|
|
616
925
|
const evolutionEpoch = index.__evolution && index.__evolution.epoch ? index.__evolution.epoch : '';
|
|
617
|
-
|
|
926
|
+
const controlEpoch = index.__control && index.__control.epoch ? index.__control.epoch : '';
|
|
927
|
+
return `${index.scope}:${meta.storeId || ''}:${index.generation}:${index.epoch}:${evolutionEpoch}:${controlEpoch}`;
|
|
618
928
|
}).join('|');
|
|
619
929
|
} catch {
|
|
620
930
|
return '';
|
|
@@ -658,6 +968,7 @@ export function apply(ctx, config) {
|
|
|
658
968
|
for (const row of ranked) {
|
|
659
969
|
if (cards.length >= view.CARD.max) break;
|
|
660
970
|
const card = view.cardOf(row.record, layerOf.get(row.record.id));
|
|
971
|
+
if (card.scope === 'project') card.scopeName = projectNameOf(scope);
|
|
661
972
|
const line = renderRecallCard(card);
|
|
662
973
|
const size = Buffer.byteLength(`\n${line}`);
|
|
663
974
|
if (used + size > view.CARD.bytes) continue;
|
|
@@ -683,7 +994,7 @@ export function apply(ctx, config) {
|
|
|
683
994
|
|
|
684
995
|
/* ---------------- 项目自进化:任务锚点、读取决策、上下文解析 ---------------- */
|
|
685
996
|
|
|
686
|
-
const EXPERIMENT_INDEX_HEADER = '
|
|
997
|
+
const EXPERIMENT_INDEX_HEADER = '项目里这些记忆范围可能适用当前任务(未绑定);需要细节再用 memory_search 带上 contextId:';
|
|
687
998
|
const EXPERIMENT_RULES_HEADER = '项目实验规则(只在当前任务条件匹配时适用;细节用 memory_read 取正文,别凭标题猜):';
|
|
688
999
|
const EXPERIMENT_BYTES = 1200;
|
|
689
1000
|
const EXPERIMENT_SHORT_CHARS = 240;
|
|
@@ -740,6 +1051,25 @@ export function apply(ctx, config) {
|
|
|
740
1051
|
return text;
|
|
741
1052
|
}
|
|
742
1053
|
|
|
1054
|
+
/** 本轮问题与栏目名的字面相关度:只展示有交集的栏目,交集为空就整段跳过。 */
|
|
1055
|
+
function relevantRanges(names, query) {
|
|
1056
|
+
const text = String(query || '').toLowerCase();
|
|
1057
|
+
if (!text) return [];
|
|
1058
|
+
const tokens = new Set();
|
|
1059
|
+
for (const word of text.match(/[a-z][a-z0-9_-]{2,}/g) || []) tokens.add(word);
|
|
1060
|
+
for (const run of text.match(/[\u4e00-\u9fff]+/g) || []) {
|
|
1061
|
+
for (let i = 0; i + 2 <= run.length; i += 1) tokens.add(run.slice(i, i + 2));
|
|
1062
|
+
}
|
|
1063
|
+
if (!tokens.size) return [];
|
|
1064
|
+
return (Array.isArray(names) ? names : []).filter((name) => {
|
|
1065
|
+
const value = String(name || '').toLowerCase();
|
|
1066
|
+
if (!value) return false;
|
|
1067
|
+
if (text.includes(value)) return true;
|
|
1068
|
+
for (const token of tokens) if (value.includes(token)) return true;
|
|
1069
|
+
return false;
|
|
1070
|
+
});
|
|
1071
|
+
}
|
|
1072
|
+
|
|
743
1073
|
/** 读决策 → 一段短提示:未绑定只给栏目索引,已绑定才给规则正文。 */
|
|
744
1074
|
function renderExperiment(scope, decision, context, held, win) {
|
|
745
1075
|
if (!decision || decision.kind === 'none') return '';
|
|
@@ -747,9 +1077,10 @@ export function apply(ctx, config) {
|
|
|
747
1077
|
return chargeExperiment(win, '这些实验规则互相冲突,先按用户这次说的做,别叠加对立的规则。');
|
|
748
1078
|
}
|
|
749
1079
|
if (decision.kind === 'index' || decision.kind === 'clarify') {
|
|
750
|
-
const names = Array.isArray(decision.ranges) ? decision.ranges.filter(Boolean).slice(0, 3)
|
|
1080
|
+
const names = relevantRanges(Array.isArray(decision.ranges) ? decision.ranges.filter(Boolean) : [], held && held.query).slice(0, 3);
|
|
1081
|
+
/** 没有适用栏目就什么也不注入:不拿无关范围冒充当前任务,也不催用户选栏目。 */
|
|
1082
|
+
if (!names.length) return '';
|
|
751
1083
|
const tail = held && held.contextId ? `\ncontextId=${held.contextId}` : '';
|
|
752
|
-
if (!names.length) return chargeExperiment(win, `先问用户这期做什么栏目,别自己猜。${tail}`);
|
|
753
1084
|
return chargeExperiment(win, `${EXPERIMENT_INDEX_HEADER}\n${names.map((name) => `- ${name}`).join('\n')}${tail}`);
|
|
754
1085
|
}
|
|
755
1086
|
let index;
|
|
@@ -760,6 +1091,7 @@ export function apply(ctx, config) {
|
|
|
760
1091
|
let conflict = false;
|
|
761
1092
|
for (const record of index.entries) {
|
|
762
1093
|
if (record.origin !== 'project_evolution') continue;
|
|
1094
|
+
if (record.ownerProjectId && record.ownerProjectId !== scope.key) continue;
|
|
763
1095
|
if (record.reviewState === 'legacy_unreviewed') continue;
|
|
764
1096
|
const learning = record.learning && typeof record.learning === 'object' ? record.learning : {};
|
|
765
1097
|
if (learning.state !== 'active' && learning.state !== 'trial') continue;
|
|
@@ -781,12 +1113,13 @@ export function apply(ctx, config) {
|
|
|
781
1113
|
const body = String(record.body || '');
|
|
782
1114
|
const short = body && body.length <= EXPERIMENT_SHORT_CHARS && body.split('\n').length <= 4;
|
|
783
1115
|
const conditions = conditionLabel(record.conditions);
|
|
784
|
-
const
|
|
785
|
-
|
|
1116
|
+
const checkpoint = record.learning && Array.isArray(record.learning.checkpoints) ? record.learning.checkpoints[0] : null;
|
|
1117
|
+
const check = checkpoint && checkpoint.text
|
|
1118
|
+
? `;核对:${String(checkpoint.text).slice(0, 40)}`
|
|
786
1119
|
: '';
|
|
787
1120
|
const line = short
|
|
788
|
-
? `- 「${record.title}」${conditions ? `(${conditions})` : ''} · ${record.id}
|
|
789
|
-
: `- 「${record.title}」 · ${record.id}
|
|
1121
|
+
? `- 「${record.title}」${conditions ? `(${conditions})` : ''} · ${record.id}:${body}${check}`
|
|
1122
|
+
: `- 「${record.title}」 · ${record.id}(用途:${record.summary || record.title};要用时 memory_read 取正文)`;
|
|
790
1123
|
const size = Buffer.byteLength(`\n${line}`);
|
|
791
1124
|
if (used + size > EXPERIMENT_BYTES) break;
|
|
792
1125
|
used += size;
|
|
@@ -840,9 +1173,11 @@ export function apply(ctx, config) {
|
|
|
840
1173
|
});
|
|
841
1174
|
const contextId = anchor.contextIdFor(scope.key, batchKey, crypto.randomUUID().slice(0, 8));
|
|
842
1175
|
const episode = routed.tasks && routed.tasks.episodes ? routed.tasks.episodes[routed.anchor.episodeId] : null;
|
|
1176
|
+
let epoch = '';
|
|
1177
|
+
try { epoch = String(evolutionStatus(scope).epoch || ''); } catch { epoch = ''; }
|
|
843
1178
|
const held = {
|
|
844
|
-
contextId, projectKey: scope.key, sessionId, batchKey, messageIds,
|
|
845
|
-
context, anchor: routed.anchor, ranges, decision,
|
|
1179
|
+
contextId, projectKey: scope.key, sessionId, batchKey, messageIds, query,
|
|
1180
|
+
context, anchor: routed.anchor, ranges, decision, epoch,
|
|
846
1181
|
episodeUpdatedAt: episode ? episode.updatedAt : '', injectedIds: [],
|
|
847
1182
|
at: Date.now(),
|
|
848
1183
|
};
|
|
@@ -866,26 +1201,47 @@ export function apply(ctx, config) {
|
|
|
866
1201
|
if (!win) return { ok: false, status: 'needs_context', reason: '这个请求的上下文已经过期,先重新问一次当前任务' };
|
|
867
1202
|
const held = win.contexts.get(contextId);
|
|
868
1203
|
if (!held) return { ok: false, status: 'needs_context', reason: '这个上下文已经过期,先重新问一次当前任务' };
|
|
1204
|
+
/** 凭据只在当轮、当次开关代次内有效:换轮、关闭、重开或过期都不再放行旧正文。 */
|
|
1205
|
+
const expired = (reason) => {
|
|
1206
|
+
win.contexts.delete(contextId);
|
|
1207
|
+
if (win.experiment && win.experiment.contextId === contextId) win.experiment = null;
|
|
1208
|
+
return { ok: false, status: 'needs_context', reason };
|
|
1209
|
+
};
|
|
1210
|
+
if (held.projectKey !== scope.key) return expired('这个上下文属于另一个项目');
|
|
1211
|
+
if (held.sessionId && held.sessionId !== String(win.sessionId || '')) return expired('这个上下文已经过期,先重新问一次当前任务');
|
|
1212
|
+
if (!held.batchKey || held.batchKey !== anchor.requestBatchKey(win.messageIds || [])) {
|
|
1213
|
+
return expired('这个上下文已经过期,先重新问一次当前任务');
|
|
1214
|
+
}
|
|
1215
|
+
let evolution;
|
|
1216
|
+
try { evolution = evolutionStatus(scope); } catch { return expired('当前项目状态读不出来,先重新问一次当前任务'); }
|
|
1217
|
+
if (!evolution || evolution.enabled !== true || evolution.compatible === false) {
|
|
1218
|
+
return expired('项目自进化已经关闭,先重新问一次当前任务');
|
|
1219
|
+
}
|
|
1220
|
+
if (held.epoch && evolution.epoch !== held.epoch) {
|
|
1221
|
+
return expired('项目自进化重新开过,先重新问一次当前任务');
|
|
1222
|
+
}
|
|
869
1223
|
return { ok: true, scope, win, context: held.context, experimental: true, held, contextId };
|
|
870
1224
|
}
|
|
871
1225
|
|
|
872
|
-
/**
|
|
1226
|
+
/** 工具模式没有自动索引时,按本轮真人消息现开一个请求上下文,只按问题与记忆范围的字面关系判断。 */
|
|
873
1227
|
function toolContextFor(execution) {
|
|
874
1228
|
const win = windowOf(execution && execution.agent);
|
|
875
1229
|
if (!win || !win.sessionId || !Array.isArray(win.messageIds) || win.messageIds.length === 0) return '';
|
|
876
|
-
let status;
|
|
877
|
-
try { status = evolutionStatus(scopeOf(cwdOf(execution))); } catch { return ''; }
|
|
878
|
-
if (!status || status.enabled !== true || status.compatible === false) return '';
|
|
879
1230
|
const scope = scopeOf(cwdOf(execution));
|
|
880
1231
|
if (scope.confirmed !== true) return '';
|
|
1232
|
+
let status;
|
|
1233
|
+
try { status = evolutionStatus(scope); } catch { return ''; }
|
|
1234
|
+
if (!status || status.enabled !== true || status.compatible === false) return '';
|
|
881
1235
|
const batchKey = anchor.requestBatchKey(win.messageIds);
|
|
882
|
-
const held = win.experiment && win.experiment.batchKey === batchKey
|
|
1236
|
+
const held = win.experiment && win.experiment.batchKey === batchKey && win.experiment.epoch === status.epoch
|
|
1237
|
+
? win.experiment
|
|
1238
|
+
: null;
|
|
883
1239
|
if (held) return held.contextId;
|
|
884
1240
|
const contextId = anchor.contextIdFor(scope.key, batchKey, crypto.randomUUID().slice(0, 8));
|
|
885
1241
|
const created = {
|
|
886
1242
|
contextId, projectKey: scope.key, sessionId: win.sessionId, batchKey,
|
|
887
1243
|
messageIds: win.messageIds.slice(), context: view.trustedContext(scope, {}),
|
|
888
|
-
anchor: null, ranges: rangesOf(scope), decision: { kind: 'none' }, at: Date.now(),
|
|
1244
|
+
anchor: null, ranges: rangesOf(scope), decision: { kind: 'none' }, epoch: status.epoch, at: Date.now(),
|
|
889
1245
|
};
|
|
890
1246
|
win.experiment = created;
|
|
891
1247
|
win.contexts.set(contextId, created);
|
|
@@ -901,7 +1257,7 @@ export function apply(ctx, config) {
|
|
|
901
1257
|
for (const value of ranges.taskKinds || []) if (query.includes(value)) hits.push({ taskKind: value });
|
|
902
1258
|
if (hits.length !== 1) {
|
|
903
1259
|
const names = [...(ranges.series || []), ...(ranges.taskKinds || [])].slice(0, 3);
|
|
904
|
-
return { ok: false, status: 'needs_context', reason: names.length ?
|
|
1260
|
+
return { ok: false, status: 'needs_context', reason: names.length ? '本轮没有匹配的记忆范围,按当前任务继续' : '本轮没有适用的记忆范围,按当前任务继续' };
|
|
905
1261
|
}
|
|
906
1262
|
let tasks;
|
|
907
1263
|
try { tasks = taskState(resolved.scope); } catch { return { ok: false, status: 'needs_context', reason: '任务状态还没读出来' }; }
|
|
@@ -936,6 +1292,11 @@ export function apply(ctx, config) {
|
|
|
936
1292
|
if (state.signal && signal && state.signal !== signal) return '';
|
|
937
1293
|
const sessionId = agent?.session?.id;
|
|
938
1294
|
if (state.sessionId !== (typeof sessionId === 'string' ? sessionId : '')) return '';
|
|
1295
|
+
/** 开关/自进化换代后,关掉前准备好的正文一律不再输出。 */
|
|
1296
|
+
const scope = scopeOf(cwdOf({ agent }));
|
|
1297
|
+
if (!memoryEnabled(scope)) return '';
|
|
1298
|
+
const epoch = controlStatus(scope).epoch || '';
|
|
1299
|
+
if (state.controlEpoch && epoch && state.controlEpoch !== epoch) return '';
|
|
939
1300
|
return [state.experimentText, state.text].filter(Boolean).join('\n\n');
|
|
940
1301
|
} catch {
|
|
941
1302
|
return '';
|
|
@@ -945,7 +1306,7 @@ export function apply(ctx, config) {
|
|
|
945
1306
|
|
|
946
1307
|
ctx.effect(() => tools.register({
|
|
947
1308
|
name: 'memory_save',
|
|
948
|
-
description:
|
|
1309
|
+
description: `保存一条记忆。只在会改变以后判断时才存;先搜已有条目,能更新就不要新建。保存前先蒸馏:标题是结论,正文只写原则与触发条件,不抄原始输出里的具体数值。用户明确说「记住」时直接调用;用户自己的话已经是长期规则(「以后都这样」「别再 X」)时也直接调用,不要先问。更新同一件事时带 id:身份只看标题、旧标题和前置条件,像不像不授权覆盖。文本相近但标题不同时先不写,返回 needsResolution 与 1–2 条相近条目:确实是同一条就用那个 id 更新,确实是另一条才带 resolution="distinct" 再存。带 id 更新时没给的字段保持原值,正文整段替换,旧正文只留作内部撤回,不展示也不参与检索。条数到硬上限时把同层最久没被读的一条移进归档,结果里写明动了哪条,不拒绝新记忆;单条正文超限只报错、不截断。前置条件不同的同一标题是两条。${NOTES}`,
|
|
949
1310
|
parameters: SAVE_PARAMS,
|
|
950
1311
|
output: {
|
|
951
1312
|
presentationMeta: (_args, value) => ({
|
|
@@ -971,6 +1332,7 @@ export function apply(ctx, config) {
|
|
|
971
1332
|
merged: { type: 'boolean' },
|
|
972
1333
|
changed: { type: 'boolean' },
|
|
973
1334
|
scope: { type: 'string' },
|
|
1335
|
+
scopeName: { type: 'string' },
|
|
974
1336
|
needsResolution: { type: 'boolean' },
|
|
975
1337
|
needsScope: { type: 'boolean' },
|
|
976
1338
|
reason: { type: 'string' },
|
|
@@ -988,20 +1350,30 @@ export function apply(ctx, config) {
|
|
|
988
1350
|
render: (_args, value) => [{
|
|
989
1351
|
type: 'text',
|
|
990
1352
|
text: value.needsScope
|
|
991
|
-
? value.reason === '
|
|
1353
|
+
? value.reason === 'memory_disabled'
|
|
1354
|
+
? 'memory · 本项目记忆已关闭,先在面板打开'
|
|
1355
|
+
: value.reason === 'unconfirmed_project'
|
|
992
1356
|
? `memory · 当前目录还不能确认属于哪个项目:「${value.title}」\n先确认项目根目录,或显式 scope="global"`
|
|
993
1357
|
: `memory · 归属还没定:「${value.title}」\n补 memoryClass(personal_preference / project_fact / environment_fact / portable_procedure)或显式 scope;设备事实还要给设备条件`
|
|
994
1358
|
: value.existingId
|
|
995
|
-
? `memory · 已有相同记忆:「${value.title}」 · ${value.existingId}(${value.existingScope}
|
|
1359
|
+
? `memory · 已有相同记忆:「${value.title}」 · ${value.existingId}(${value.existingScope})\n要改成另一条,加 resolution="distinct"`
|
|
996
1360
|
: value.needsResolution
|
|
997
1361
|
? `memory · 需要确认:「${value.title}」与已有条目相近${value.crossLayer ? '(在另一层)' : ''}${value.sameTopic ? '(同一主题槽位)' : ''}\n${value.similar}\n要更新它,用 id 重存;确实是另一条,加 resolution="distinct"`
|
|
998
1362
|
: value.changed === false
|
|
999
|
-
? `memory · unchanged${
|
|
1000
|
-
: `memory · ${value.merged ? 'merged' : 'saved'}${
|
|
1363
|
+
? `memory · unchanged ${withTag(value, `"${value.title}"`)} · ${value.id}`
|
|
1364
|
+
: `memory · ${value.merged ? 'merged' : 'saved'} ${withTag(value, `"${value.title}"`)}${value.when ? `(${value.when})` : ''} · ${value.id}${value.similar ? ` · 相近:${value.similar}` : ''}${value.archived ? ` · 已归档「${value.archived}」腾位` : ''}`,
|
|
1001
1365
|
}],
|
|
1002
1366
|
},
|
|
1003
1367
|
execute: async (args, execution) => {
|
|
1004
1368
|
const projectScope = scopeOf(cwdOf(execution));
|
|
1369
|
+
const projectScopeName = projectNameOf(projectScope);
|
|
1370
|
+
if (!memoryEnabled(projectScope)) {
|
|
1371
|
+
return {
|
|
1372
|
+
id: '', title: String(args.title || ''), summary: '', when: '', revision: 0,
|
|
1373
|
+
merged: false, changed: false, scope: '', needsScope: true, reason: 'memory_disabled',
|
|
1374
|
+
needsResolution: false, similarIds: '', similar: '', archived: '',
|
|
1375
|
+
};
|
|
1376
|
+
}
|
|
1005
1377
|
const saved = saveEntry(projectScope, { ...args, writeMode: args.mode || 'patch' });
|
|
1006
1378
|
if (saved.needsScope) {
|
|
1007
1379
|
return {
|
|
@@ -1031,6 +1403,7 @@ export function apply(ctx, config) {
|
|
|
1031
1403
|
merged: false,
|
|
1032
1404
|
changed: false,
|
|
1033
1405
|
scope: saved.existing.scope,
|
|
1406
|
+
scopeName: saved.existing.scope === 'global' ? '' : projectScopeName,
|
|
1034
1407
|
needsScope: false,
|
|
1035
1408
|
existingId: saved.existing.id,
|
|
1036
1409
|
existingScope: saved.existing.scope,
|
|
@@ -1070,6 +1443,7 @@ export function apply(ctx, config) {
|
|
|
1070
1443
|
merged: saved.merged,
|
|
1071
1444
|
changed: !saved.noop,
|
|
1072
1445
|
scope: scope.key === 'global' ? 'global' : 'project',
|
|
1446
|
+
scopeName: scope.key === 'global' ? '' : projectScopeName,
|
|
1073
1447
|
needsScope: false,
|
|
1074
1448
|
needsResolution: false,
|
|
1075
1449
|
similarIds: saved.similarList.map((item) => item.id).join('、'),
|
|
@@ -1107,6 +1481,7 @@ export function apply(ctx, config) {
|
|
|
1107
1481
|
},
|
|
1108
1482
|
execute: async (args, execution) => {
|
|
1109
1483
|
const scope = scopeOf(cwdOf(execution));
|
|
1484
|
+
if (!memoryEnabled(scope)) return { asked: false, reason: '本项目记忆已关闭' };
|
|
1110
1485
|
const gate = suggestionGate(scope, args);
|
|
1111
1486
|
if (!gate.ok) return { asked: false, reason: gate.reason };
|
|
1112
1487
|
const noted = noteSuggestion(scope, gate.target ? { ...args, targetId: gate.target.id } : args);
|
|
@@ -1121,7 +1496,7 @@ export function apply(ctx, config) {
|
|
|
1121
1496
|
|
|
1122
1497
|
ctx.effect(() => tools.register({
|
|
1123
1498
|
name: 'memory_search',
|
|
1124
|
-
description: `按需检索已有记忆:先搜,命中再 memory_read 读正文。用户提到「记住了什么 / 上次 / 规范 / 约定 / 路径 / 凭据怎么取」,或者眼前反复出现的报错、弹窗、异常时,先查这里有没有记过解法。跨项目的通用条目以 [
|
|
1499
|
+
description: `按需检索已有记忆:先搜,命中再 memory_read 读正文。用户提到「记住了什么 / 上次 / 规范 / 约定 / 路径 / 凭据怎么取」,或者眼前反复出现的报错、弹窗、异常时,先查这里有没有记过解法。跨项目的通用条目以 [global] 开头,项目条目以 [项目名] 开头。中文长句可以直接当查询。每行带 id,更新时把 id 传回 memory_save。默认只列当前有效的;已经归档或被更新替换的旧说法不混进来,要查历史就在参数里显式要 history。`,
|
|
1125
1500
|
parameters: SEARCH_PARAMS,
|
|
1126
1501
|
output: {
|
|
1127
1502
|
presentationMeta: (_args, value) => ({ kind: 'memory-search', count: value.count, status: value.status || 'ok', refs: value.refs || [] }),
|
|
@@ -1142,8 +1517,9 @@ export function apply(ctx, config) {
|
|
|
1142
1517
|
scope: { type: 'string' },
|
|
1143
1518
|
revision: { type: 'integer' },
|
|
1144
1519
|
status: { type: 'string' },
|
|
1520
|
+
origin: { type: 'string' },
|
|
1145
1521
|
},
|
|
1146
|
-
required: ['id', 'title', 'scope', 'revision', 'status'],
|
|
1522
|
+
required: ['id', 'title', 'scope', 'revision', 'status', 'origin'],
|
|
1147
1523
|
additionalProperties: false,
|
|
1148
1524
|
},
|
|
1149
1525
|
},
|
|
@@ -1165,7 +1541,7 @@ export function apply(ctx, config) {
|
|
|
1165
1541
|
}],
|
|
1166
1542
|
},
|
|
1167
1543
|
execute: async (args, execution) => {
|
|
1168
|
-
if (recallMode() === 'off') return { count: 0, chars: 0, text: '', status: 'memory_disabled', refs: [] };
|
|
1544
|
+
if (recallMode() === 'off' || !memoryEnabledFor(execution)) return { count: 0, chars: 0, text: '', status: 'memory_disabled', refs: [] };
|
|
1169
1545
|
const agent = execution?.agent;
|
|
1170
1546
|
const win = windowOf(agent);
|
|
1171
1547
|
if (win && !affordable(win, 0)) return { count: 0, chars: 0, text: '', status: 'budget_exhausted', refs: [] };
|
|
@@ -1182,12 +1558,19 @@ export function apply(ctx, config) {
|
|
|
1182
1558
|
}
|
|
1183
1559
|
const scope = resolved.scope;
|
|
1184
1560
|
const context = resolved.context;
|
|
1185
|
-
|
|
1561
|
+
/** 检索只列当前任务真能用的条目;条件未知或不适用就只留状态,不给索引行。 */
|
|
1562
|
+
const hits = search(scope, args.query, (args.limit ?? 3) + 3, {
|
|
1186
1563
|
context,
|
|
1187
1564
|
history: args.history === true,
|
|
1188
1565
|
allowExperimental: resolved.experimental === true,
|
|
1189
|
-
})
|
|
1190
|
-
|
|
1566
|
+
}).filter((item) => (item.archived || item.status === 'ok')
|
|
1567
|
+
&& (!item.ownerProjectId || item.ownerProjectId === scope.key))
|
|
1568
|
+
.slice(0, args.limit ?? 3);
|
|
1569
|
+
const scopeName = scope.key === 'global' ? '' : projectNameOf(scope);
|
|
1570
|
+
const shown = hits.map((item) => ({
|
|
1571
|
+
...view.toModelView(item, 'search', { status: item.status || (item.archived ? 'archived' : 'ok') }),
|
|
1572
|
+
scopeName: item.scope === 'global' ? '' : scopeName,
|
|
1573
|
+
}));
|
|
1191
1574
|
const valueOf = (kept) => {
|
|
1192
1575
|
const rendered = renderIndex(shown.slice(0, kept));
|
|
1193
1576
|
return { count: kept, chars: rendered.chars, text: rendered.text, status: 'ok', refs: refsOf(hits.slice(0, kept)) };
|
|
@@ -1232,6 +1615,7 @@ export function apply(ctx, config) {
|
|
|
1232
1615
|
when: { type: 'string' },
|
|
1233
1616
|
revision: { type: 'integer' },
|
|
1234
1617
|
scope: { type: 'string' },
|
|
1618
|
+
scopeName: { type: 'string' },
|
|
1235
1619
|
origin: { type: 'string', enum: ['user_explicit', 'project_evolution'] },
|
|
1236
1620
|
status: { type: 'string', enum: ['ok', 'archived', 'needs_context', 'not_applicable', 'gone', 'budget_exhausted', 'memory_disabled'] },
|
|
1237
1621
|
body: { type: 'string' },
|
|
@@ -1248,9 +1632,9 @@ export function apply(ctx, config) {
|
|
|
1248
1632
|
: value.status === 'memory_disabled'
|
|
1249
1633
|
? 'memory · 记忆检索已关闭'
|
|
1250
1634
|
: value.repeat
|
|
1251
|
-
? `memory · 本请求已读过「${value.title}」 · ${value.id}
|
|
1635
|
+
? `memory · 本请求已读过「${value.title}」 · ${value.id}`
|
|
1252
1636
|
: value.body
|
|
1253
|
-
? `memory ·
|
|
1637
|
+
? `memory · ${withTag(value, `「${value.title}」`)}${value.when ? `(${value.when})` : ''} · ${value.id}${statusLabel(value.status)}\n${value.body}`
|
|
1254
1638
|
: `memory · 「${value.title}」的相关记忆\n${value.links || '(无)'}`,
|
|
1255
1639
|
}],
|
|
1256
1640
|
presentationMeta: (_args, value) => ({
|
|
@@ -1264,10 +1648,11 @@ export function apply(ctx, config) {
|
|
|
1264
1648
|
}),
|
|
1265
1649
|
},
|
|
1266
1650
|
execute: async (args, execution) => {
|
|
1267
|
-
if (recallMode() === 'off') return { id: '', scope: '', status: 'memory_disabled' };
|
|
1651
|
+
if (recallMode() === 'off' || !memoryEnabledFor(execution)) return { id: '', scope: '', status: 'memory_disabled' };
|
|
1268
1652
|
const resolved = resolveRequestContext(execution, args.contextId);
|
|
1269
1653
|
if (!resolved.ok) return { id: '', scope: '', status: resolved.status || 'needs_context' };
|
|
1270
1654
|
const scope = resolved.scope;
|
|
1655
|
+
const scopeName = scope.key === 'global' ? '' : projectNameOf(scope);
|
|
1271
1656
|
const context = resolved.context;
|
|
1272
1657
|
const allowExperimental = resolved.experimental === true;
|
|
1273
1658
|
const agent = execution?.agent;
|
|
@@ -1275,11 +1660,22 @@ export function apply(ctx, config) {
|
|
|
1275
1660
|
if (args.view === 'links') {
|
|
1276
1661
|
const found = readLinks(scope, args.id, { context, allowExperimental });
|
|
1277
1662
|
if (!found) throw new Error(`没有 ${args.id} 这条记忆`);
|
|
1278
|
-
|
|
1663
|
+
if (found.status !== 'ok') {
|
|
1664
|
+
const withheld = { id: found.id, scope: found.scope, status: found.status, links: '' };
|
|
1665
|
+
if (win) {
|
|
1666
|
+
const bytes = jsonBytes(withheld);
|
|
1667
|
+
if (!affordable(win, bytes)) return { id: '', scope: '', status: 'budget_exhausted' };
|
|
1668
|
+
win.bytes += bytes;
|
|
1669
|
+
win.reads += 1;
|
|
1670
|
+
}
|
|
1671
|
+
return withheld;
|
|
1672
|
+
}
|
|
1673
|
+
const rows = found.links.filter((link) => link.status === 'invalid' || link.status === 'ok')
|
|
1674
|
+
.map((link) => (link.status === 'invalid'
|
|
1279
1675
|
? `- ${link.id} · 已失效`
|
|
1280
|
-
: `-
|
|
1676
|
+
: `- ${link.scope === 'global' ? '[global] ' : (scopeName ? `[${scopeName}] ` : '')}「${link.title}」 · ${link.id}`));
|
|
1281
1677
|
const text = rows.join('\n') || '(无)';
|
|
1282
|
-
const value = { id: found.id, scope: found.scope, status: found.status, links: text };
|
|
1678
|
+
const value = { id: found.id, scope: found.scope, scopeName, status: found.status, links: text };
|
|
1283
1679
|
if (win) {
|
|
1284
1680
|
const bytes = jsonBytes(value);
|
|
1285
1681
|
if (!affordable(win, bytes)) return { id: '', scope: '', status: 'budget_exhausted' };
|
|
@@ -1290,15 +1686,22 @@ export function apply(ctx, config) {
|
|
|
1290
1686
|
}
|
|
1291
1687
|
const record = readEntry(scope, args.id, { context, allowExperimental });
|
|
1292
1688
|
if (!record) throw new Error(`没有 ${args.id} 这条记忆`);
|
|
1689
|
+
if (record.origin === 'project_evolution' && record.ownerProjectId && record.ownerProjectId !== scope.key) {
|
|
1690
|
+
return { id: args.id, title: record.title, when: record.when || '', revision: record.revision || 1, scope: record.scope, status: 'not_applicable', body: '' };
|
|
1691
|
+
}
|
|
1293
1692
|
if (record.origin === 'project_evolution' && !allowExperimental) {
|
|
1294
1693
|
return { id: record.id, title: record.title, when: record.when || '', revision: record.revision || 1, scope: record.scope, status: 'needs_context', body: '' };
|
|
1295
1694
|
}
|
|
1296
|
-
/**
|
|
1695
|
+
/** 暂停、过期或条件不匹配的实验条目只给状态,不给正文。 */
|
|
1696
|
+
if (record.origin === 'project_evolution' && !view.experimentalUsable(record)) {
|
|
1697
|
+
return { id: record.id, title: record.title, when: record.when || '', revision: record.revision || 1, scope: record.scope, status: 'needs_context', body: '' };
|
|
1698
|
+
}
|
|
1297
1699
|
const learningState = record.learning && typeof record.learning === 'object' ? record.learning : {};
|
|
1298
1700
|
if (record.origin === 'project_evolution' && (learningState.state === 'paused' || learningState.blockedReason)) {
|
|
1299
1701
|
return { id: record.id, title: record.title, when: record.when || '', revision: record.revision || 1, scope: record.scope, status: 'needs_context', body: '' };
|
|
1300
1702
|
}
|
|
1301
|
-
|
|
1703
|
+
/** 条件未知或不适用的条目只报状态:正文绝不能绕过自动召回的门槛。 */
|
|
1704
|
+
if (record.status !== 'ok') {
|
|
1302
1705
|
return { id: record.id, title: record.title, when: record.when || '', revision: record.revision || 1, scope: record.scope, status: record.status === 'needs_context' ? 'needs_context' : 'not_applicable', body: '' };
|
|
1303
1706
|
}
|
|
1304
1707
|
const revision = record.revision || 1;
|
|
@@ -1309,6 +1712,7 @@ export function apply(ctx, config) {
|
|
|
1309
1712
|
when: record.when || '',
|
|
1310
1713
|
revision,
|
|
1311
1714
|
scope: record.scope,
|
|
1715
|
+
scopeName,
|
|
1312
1716
|
status: record.status,
|
|
1313
1717
|
body,
|
|
1314
1718
|
};
|
|
@@ -1346,20 +1750,23 @@ export function apply(ctx, config) {
|
|
|
1346
1750
|
id: { type: 'string' },
|
|
1347
1751
|
title: { type: 'string' },
|
|
1348
1752
|
scope: { type: 'string' },
|
|
1349
|
-
|
|
1753
|
+
scopeName: { type: 'string' },
|
|
1754
|
+
mode: { type: 'string', enum: ['archive', 'purged', 'needs_resolution', 'purge_pending', 'memory_disabled'] },
|
|
1350
1755
|
},
|
|
1351
1756
|
required: ['id', 'mode'],
|
|
1352
1757
|
additionalProperties: false,
|
|
1353
1758
|
},
|
|
1354
1759
|
render: (_args, value) => [{
|
|
1355
1760
|
type: 'text',
|
|
1356
|
-
text: value.mode === '
|
|
1761
|
+
text: value.mode === 'memory_disabled'
|
|
1762
|
+
? 'memory · 本项目记忆已关闭,先在面板打开'
|
|
1763
|
+
: value.mode === 'purged'
|
|
1357
1764
|
? `memory · forgotten "${value.title}" · ${value.id}`
|
|
1358
1765
|
: value.mode === 'purge_pending'
|
|
1359
1766
|
? `memory · 清除未完成 · ${value.id},重启后会继续`
|
|
1360
1767
|
: value.mode === 'needs_resolution'
|
|
1361
1768
|
? `memory · 先确认是归档还是彻底忘掉 · ${value.id}`
|
|
1362
|
-
: `memory · archived${
|
|
1769
|
+
: `memory · archived ${withTag(value, `"${value.title}"`)} · ${value.id},可找回`,
|
|
1363
1770
|
}],
|
|
1364
1771
|
presentationMeta: (_args, value) => ({
|
|
1365
1772
|
kind: 'memory-forget',
|
|
@@ -1371,6 +1778,8 @@ export function apply(ctx, config) {
|
|
|
1371
1778
|
},
|
|
1372
1779
|
execute: async (args, execution) => {
|
|
1373
1780
|
const scope = scopeOf(cwdOf(execution));
|
|
1781
|
+
const projectScopeName = projectNameOf(scope);
|
|
1782
|
+
if (!memoryEnabled(scope)) return { id: String(args.id || ''), title: '', scope: '', mode: 'memory_disabled' };
|
|
1374
1783
|
const located = locate(scope, args.id);
|
|
1375
1784
|
if (!located && !args.mode) throw new Error(`没有 ${args.id} 这条记忆`);
|
|
1376
1785
|
if (!args.mode) {
|
|
@@ -1392,6 +1801,7 @@ export function apply(ctx, config) {
|
|
|
1392
1801
|
title: located ? located.record.title : '',
|
|
1393
1802
|
mode: 'purge_pending',
|
|
1394
1803
|
scope: layer.key === 'global' ? 'global' : 'project',
|
|
1804
|
+
scopeName: layer.key === 'global' ? '' : projectScopeName,
|
|
1395
1805
|
};
|
|
1396
1806
|
}
|
|
1397
1807
|
throw again;
|
|
@@ -1402,11 +1812,13 @@ export function apply(ctx, config) {
|
|
|
1402
1812
|
}
|
|
1403
1813
|
if (!dropped) throw new Error(`没有 ${args.id} 这条记忆`);
|
|
1404
1814
|
const layerKey = dropped.scopeKey || (located ? located.scope.key : scope.key);
|
|
1815
|
+
if (dropped.mode === 'purged') revokeLearningUse(layerKey);
|
|
1405
1816
|
return {
|
|
1406
1817
|
id: dropped.id,
|
|
1407
1818
|
title: dropped.title || (located ? located.record.title : ''),
|
|
1408
1819
|
mode: dropped.mode,
|
|
1409
1820
|
scope: layerKey === 'global' ? 'global' : 'project',
|
|
1821
|
+
scopeName: layerKey === 'global' ? '' : projectScopeName,
|
|
1410
1822
|
};
|
|
1411
1823
|
},
|
|
1412
1824
|
}), 'lite-memory: memory_forget');
|
|
@@ -1445,9 +1857,23 @@ export function apply(ctx, config) {
|
|
|
1445
1857
|
}), 'lite-memory: recall context');
|
|
1446
1858
|
}
|
|
1447
1859
|
|
|
1860
|
+
/** 执行入口再拦一次:工具若已出现在旧快照里,调用时也不放行。 */
|
|
1861
|
+
if (typeof ctx.on === 'function') ctx.on('agent/created', (payload) => {
|
|
1862
|
+
const agent = payload?.agent;
|
|
1863
|
+
if (!agent?.ctx?.tools) return;
|
|
1864
|
+
try {
|
|
1865
|
+
agent.ctx.effect(() => agent.ctx.tools.guard((execution) => {
|
|
1866
|
+
if (!MEMORY_TOOLS.includes(execution?.name)) return undefined;
|
|
1867
|
+
return memoryOnForAgent(agent) ? undefined : '本项目记忆已关闭,先在面板打开';
|
|
1868
|
+
}), 'lite-memory: tool guard');
|
|
1869
|
+
} catch { /* older host without guards */ }
|
|
1870
|
+
syncMemoryTools(agent);
|
|
1871
|
+
});
|
|
1872
|
+
|
|
1448
1873
|
if (typeof ctx.on === 'function') ctx.on('agent/pre-step', async (payload, next) => {
|
|
1449
1874
|
const startedAt = Date.now();
|
|
1450
1875
|
const agent = payload?.agent;
|
|
1876
|
+
syncMemoryTools(agent);
|
|
1451
1877
|
let decision;
|
|
1452
1878
|
try {
|
|
1453
1879
|
decision = await next();
|
|
@@ -1488,6 +1914,10 @@ export function apply(ctx, config) {
|
|
|
1488
1914
|
}
|
|
1489
1915
|
previous.signal = signal;
|
|
1490
1916
|
const scope = scopeOf(cwdOf({ agent }));
|
|
1917
|
+
if (!memoryEnabled(scope)) {
|
|
1918
|
+
clearPrepared(agent);
|
|
1919
|
+
return decision;
|
|
1920
|
+
}
|
|
1491
1921
|
const fingerprint = fingerprintOf(scope);
|
|
1492
1922
|
/** 指纹拿不到就当作已失效:不拿旧任务内容硬撑这一步。 */
|
|
1493
1923
|
if (!fingerprint) {
|
|
@@ -1535,24 +1965,33 @@ export function apply(ctx, config) {
|
|
|
1535
1965
|
return decision;
|
|
1536
1966
|
}
|
|
1537
1967
|
const scope = scopeOf(cwd);
|
|
1968
|
+
if (!memoryEnabled(scope)) {
|
|
1969
|
+
clearPrepared(agent);
|
|
1970
|
+
return decision;
|
|
1971
|
+
}
|
|
1538
1972
|
const context = view.trustedContext(scope, {});
|
|
1539
1973
|
const fingerprint = fingerprintOf(scope);
|
|
1540
|
-
const
|
|
1974
|
+
const controlEpoch = controlStatus(scope).epoch || '';
|
|
1975
|
+
const batchKey = anchor.requestBatchKey(ids);
|
|
1976
|
+
dropStaleCandidate(sessionId, batchKey);
|
|
1977
|
+
const experiment = experimentFor(scope, { sessionId, query, batchKey, messageIds: ids, win });
|
|
1541
1978
|
const remembered = experiment && experiment.held
|
|
1542
|
-
? rememberFeedback(scope, sessionId, fresh, experiment.held)
|
|
1979
|
+
? rememberFeedback(scope, sessionId, fresh, experiment.held, turn)
|
|
1543
1980
|
: null;
|
|
1544
|
-
|
|
1981
|
+
const snapshot = projectRecall(scope, query, context, win);
|
|
1982
|
+
if (generation !== generations.get(agent)) return decision;
|
|
1983
|
+
/** 投影期间开关被改过:丢弃这批内容,不把关闭前的记忆交给当前请求。 */
|
|
1984
|
+
if (!memoryEnabled(scope) || (controlStatus(scope).epoch || '') !== controlEpoch) {
|
|
1545
1985
|
clearPrepared(agent);
|
|
1546
1986
|
return decision;
|
|
1547
1987
|
}
|
|
1548
|
-
const snapshot = projectRecall(scope, query, context, win);
|
|
1549
|
-
if (generation !== generations.get(agent)) return decision;
|
|
1550
1988
|
/** 超时或被取消:这一步不留半旧卡片,下一真人请求重新投影。 */
|
|
1551
1989
|
if (signal?.aborted || Date.now() - startedAt > RECALL_DEADLINE_MS) {
|
|
1552
1990
|
clearPrepared(agent);
|
|
1553
1991
|
return decision;
|
|
1554
1992
|
}
|
|
1555
|
-
|
|
1993
|
+
/** 实验上下文即使本轮零注入也要留着:工具绑定后同一请求还能补上规则。 */
|
|
1994
|
+
if (!snapshot && !experiment) {
|
|
1556
1995
|
clearPrepared(agent);
|
|
1557
1996
|
return decision;
|
|
1558
1997
|
}
|
|
@@ -1565,6 +2004,7 @@ export function apply(ctx, config) {
|
|
|
1565
2004
|
turn,
|
|
1566
2005
|
signal,
|
|
1567
2006
|
fingerprint,
|
|
2007
|
+
controlEpoch,
|
|
1568
2008
|
at: Date.now(),
|
|
1569
2009
|
};
|
|
1570
2010
|
prepared.set(agent, state);
|
|
@@ -1586,8 +2026,19 @@ export function apply(ctx, config) {
|
|
|
1586
2026
|
|
|
1587
2027
|
/* ---------------- 项目自进化:一次短整理,先扣频次,再走提交门禁 ---------------- */
|
|
1588
2028
|
|
|
1589
|
-
/**
|
|
1590
|
-
function
|
|
2029
|
+
/** 只清不属于当前这批人类消息的旧候选:新一轮开始后,上一轮没整理完的反馈不能再落盘。 */
|
|
2030
|
+
function dropStaleCandidate(sessionId, batchKey) {
|
|
2031
|
+
const prior = feedbackCandidates.get(sessionId);
|
|
2032
|
+
if (prior && prior.candidate && prior.candidate.batchKey !== batchKey) {
|
|
2033
|
+
feedbackCandidates.delete(sessionId);
|
|
2034
|
+
}
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
/**
|
|
2038
|
+
* 反馈只来自本轮真人消息;初始任务、工具成功、沉默都不算。
|
|
2039
|
+
* 解析目标由整理器完成:不在这里暂停所有已注入规则,避免一次 BGM 纠正打掉开头和字幕。
|
|
2040
|
+
*/
|
|
2041
|
+
function rememberFeedback(scope, sessionId, messages, held, turn) {
|
|
1591
2042
|
let candidate;
|
|
1592
2043
|
try {
|
|
1593
2044
|
candidate = learning.candidateFrom({
|
|
@@ -1597,21 +2048,22 @@ export function apply(ctx, config) {
|
|
|
1597
2048
|
if (!candidate) return null;
|
|
1598
2049
|
candidate.cwd = scope.cwd;
|
|
1599
2050
|
candidate.episodeUpdatedAt = held.episodeUpdatedAt || '';
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
2051
|
+
candidate.batchKey = held.batchKey || '';
|
|
2052
|
+
candidate.turn = String(turn || '');
|
|
2053
|
+
const revision = (feedbackRevisions.get(scope.key) || 0) + 1;
|
|
2054
|
+
feedbackRevisions.set(scope.key, revision);
|
|
2055
|
+
candidate.feedbackRevision = revision;
|
|
2056
|
+
const prior = feedbackCandidates.get(sessionId);
|
|
2057
|
+
feedbackCandidates.set(sessionId, {
|
|
2058
|
+
scope,
|
|
2059
|
+
candidate,
|
|
2060
|
+
held,
|
|
2061
|
+
session: prior && prior.session ? prior.session : null,
|
|
2062
|
+
queued: Boolean(prior && prior.queued),
|
|
2063
|
+
at: Date.now(),
|
|
2064
|
+
});
|
|
1613
2065
|
while (feedbackCandidates.size > 8) feedbackCandidates.delete(feedbackCandidates.keys().next().value);
|
|
1614
|
-
return { candidate
|
|
2066
|
+
return { candidate };
|
|
1615
2067
|
}
|
|
1616
2068
|
|
|
1617
2069
|
/** 只认前台最近一次真实请求的路线;拿不到就宁可这次不整理。 */
|
|
@@ -1695,6 +2147,10 @@ export function apply(ctx, config) {
|
|
|
1695
2147
|
else if (chunk && chunk.type === 'finish' && chunk.reason) finish = chunk.reason;
|
|
1696
2148
|
}
|
|
1697
2149
|
if (sawTool) throw new Error('整理返回了工具调用');
|
|
2150
|
+
/** 运行期间用户又给了新反馈:旧结果不能覆盖新纠正,直接作废,等排队的下一次整理。 */
|
|
2151
|
+
if ((feedbackRevisions.get(scope.key) || 0) !== candidate.feedbackRevision) {
|
|
2152
|
+
throw new Error('反馈已更新,这次整理作废');
|
|
2153
|
+
}
|
|
1698
2154
|
const parsed = learning.parseOutput(text, { finish, refs: input.refs, rules: input.rules, ranges });
|
|
1699
2155
|
if (!parsed.ok) throw new Error(parsed.reason);
|
|
1700
2156
|
const commit = commitLearning(scope, {
|
|
@@ -1737,6 +2193,42 @@ export function apply(ctx, config) {
|
|
|
1737
2193
|
}
|
|
1738
2194
|
}
|
|
1739
2195
|
|
|
2196
|
+
/** 启动一次整理;结束后再看同项目是否还排着一条候选。 */
|
|
2197
|
+
function startOrganizer(scope, session, pending) {
|
|
2198
|
+
const job = { controller: learningAborts.get(scope.key) || null };
|
|
2199
|
+
learningJobs.set(scope.key, job);
|
|
2200
|
+
void organize(scope, session, pending)
|
|
2201
|
+
.catch(() => { /* 整理失败不影响会话 */ })
|
|
2202
|
+
.finally(() => {
|
|
2203
|
+
if (learningJobs.get(scope.key) === job) learningJobs.delete(scope.key);
|
|
2204
|
+
drainOrganizer(scope.key);
|
|
2205
|
+
});
|
|
2206
|
+
}
|
|
2207
|
+
|
|
2208
|
+
/** 彻底删除后中止该层在途整理并丢掉候选:不让旧结果复活已删规则。 */
|
|
2209
|
+
function revokeLearningUse(layerKey) {
|
|
2210
|
+
for (const [scopeKey, controller] of learningAborts) {
|
|
2211
|
+
if (layerKey !== 'global' && scopeKey !== layerKey) continue;
|
|
2212
|
+
try { controller.abort(); } catch { /* already settled */ }
|
|
2213
|
+
}
|
|
2214
|
+
for (const [sessionId, pending] of feedbackCandidates) {
|
|
2215
|
+
if (layerKey !== 'global' && pending?.scope?.key !== layerKey) continue;
|
|
2216
|
+
feedbackCandidates.delete(sessionId);
|
|
2217
|
+
}
|
|
2218
|
+
}
|
|
2219
|
+
|
|
2220
|
+
/** 忙碌时保留一条最新候选,不丢用户刚说的纠正;同一项目仍只跑一个整理。 */
|
|
2221
|
+
function drainOrganizer(scopeKey) {
|
|
2222
|
+
if (learningJobs.has(scopeKey)) return;
|
|
2223
|
+
for (const [sessionId, pending] of feedbackCandidates) {
|
|
2224
|
+
if (!pending || !pending.scope || pending.scope.key !== scopeKey) continue;
|
|
2225
|
+
feedbackCandidates.delete(sessionId);
|
|
2226
|
+
if (!pending.session) return;
|
|
2227
|
+
startOrganizer(pending.scope, pending.session, pending);
|
|
2228
|
+
return;
|
|
2229
|
+
}
|
|
2230
|
+
}
|
|
2231
|
+
|
|
1740
2232
|
/** 只在正常完成的人类回合后调度,一次一个项目;永不阻塞事件派发。 */
|
|
1741
2233
|
function scheduleOrganizer(session, event) {
|
|
1742
2234
|
const reason = event && event.data ? event.data.reason : null;
|
|
@@ -1745,14 +2237,18 @@ export function apply(ctx, config) {
|
|
|
1745
2237
|
if (!sessionId) return;
|
|
1746
2238
|
const pending = feedbackCandidates.get(sessionId);
|
|
1747
2239
|
if (!pending) return;
|
|
2240
|
+
const eventTurn = event.data && event.data.turn !== undefined ? String(event.data.turn) : '';
|
|
2241
|
+
if (eventTurn && pending.candidate && pending.candidate.turn && eventTurn !== pending.candidate.turn) {
|
|
2242
|
+
feedbackCandidates.delete(sessionId);
|
|
2243
|
+
return;
|
|
2244
|
+
}
|
|
2245
|
+
pending.session = session;
|
|
2246
|
+
if (learningJobs.has(pending.scope.key)) {
|
|
2247
|
+
pending.queued = true;
|
|
2248
|
+
return;
|
|
2249
|
+
}
|
|
1748
2250
|
feedbackCandidates.delete(sessionId);
|
|
1749
|
-
|
|
1750
|
-
if (learningJobs.has(scope.key)) return;
|
|
1751
|
-
const job = { controller: learningAborts.get(scope.key) || null };
|
|
1752
|
-
learningJobs.set(scope.key, job);
|
|
1753
|
-
void organize(scope, session, pending)
|
|
1754
|
-
.catch(() => { /* 整理失败不影响会话 */ })
|
|
1755
|
-
.finally(() => { if (learningJobs.get(scope.key) === job) learningJobs.delete(scope.key); });
|
|
2251
|
+
startOrganizer(pending.scope, session, pending);
|
|
1756
2252
|
}
|
|
1757
2253
|
|
|
1758
2254
|
if (typeof ctx.on === 'function') ctx.on('agent/disposed', (payload) => {
|
|
@@ -1773,6 +2269,11 @@ export function apply(ctx, config) {
|
|
|
1773
2269
|
if (!signal) return;
|
|
1774
2270
|
const agent = signalAgents.get(signal);
|
|
1775
2271
|
if (agent) clearPrepared(agent);
|
|
2272
|
+
/** 被取消或被中断的回合不保留反馈候选,下一句「谢谢」不能把没说完的纠正写进去。 */
|
|
2273
|
+
if (signal.aborted === true && agent) {
|
|
2274
|
+
const sessionId = String(agent?.session?.id ?? '');
|
|
2275
|
+
if (sessionId) feedbackCandidates.delete(sessionId);
|
|
2276
|
+
}
|
|
1776
2277
|
signalAgents.delete(signal);
|
|
1777
2278
|
});
|
|
1778
2279
|
|