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/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 {
|
|
@@ -365,8 +635,8 @@ const SAVE_PARAMS = {
|
|
|
365
635
|
properties: {
|
|
366
636
|
id: { type: 'string', description: '要更新的记忆 id(来自搜索结果或相近提示)。同一件事更新时用它,别另建一条' },
|
|
367
637
|
title: { type: 'string', description: `标题:名词短语,用户一眼能认出是哪件事,不超过 ${LIMITS.titleChars} 字。它就是这条记忆的身份` },
|
|
368
|
-
summary: { type: 'string', description:
|
|
369
|
-
body: { type: 'string', description: `正文 markdown,整段替换旧正文,不超过 ${LIMITS.entryBytes}
|
|
638
|
+
summary: { type: 'string', description: `一句话说清要记的内容本身——用户只看这一行决定留不留,名词短语最好,超长会自动截到这个长度` },
|
|
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,16 +1051,55 @@ export function apply(ctx, config) {
|
|
|
740
1051
|
return text;
|
|
741
1052
|
}
|
|
742
1053
|
|
|
1054
|
+
/** 实验正文按条目记有效期:一条到期只撤一条,不把同请求里其他规则一起清掉。 */
|
|
1055
|
+
function experimentTextFrom(parts, now = Date.now()) {
|
|
1056
|
+
if (!parts || !Array.isArray(parts.items)) return '';
|
|
1057
|
+
const items = parts.items.filter((item) => !item.validUntil || now <= Date.parse(String(item.validUntil)));
|
|
1058
|
+
if (!items.length) return '';
|
|
1059
|
+
return `${parts.header}\n${items.map((item) => item.line).join('\n')}${parts.tail || ''}`;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
/** 本轮问题与栏目名的字面相关度:只展示有交集的栏目,交集为空就整段跳过。 */
|
|
1063
|
+
function relevantRanges(names, query) {
|
|
1064
|
+
const text = String(query || '').toLowerCase();
|
|
1065
|
+
if (!text) return [];
|
|
1066
|
+
const tokens = new Set();
|
|
1067
|
+
for (const word of text.match(/[a-z][a-z0-9_-]{2,}/g) || []) tokens.add(word);
|
|
1068
|
+
for (const run of text.match(/[\u4e00-\u9fff]+/g) || []) {
|
|
1069
|
+
for (let i = 0; i + 2 <= run.length; i += 1) tokens.add(run.slice(i, i + 2));
|
|
1070
|
+
}
|
|
1071
|
+
if (!tokens.size) return [];
|
|
1072
|
+
return (Array.isArray(names) ? names : []).filter((name) => {
|
|
1073
|
+
const value = String(name || '').toLowerCase();
|
|
1074
|
+
if (!value) return false;
|
|
1075
|
+
if (text.includes(value)) return true;
|
|
1076
|
+
for (const token of tokens) if (value.includes(token)) return true;
|
|
1077
|
+
return false;
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
/** 实验正文只在「自进化开着,且还是同一代」时可用;关掉再开不能复活旧正文。 */
|
|
1082
|
+
function experimentStillCurrent(scope, held) {
|
|
1083
|
+
let evolution;
|
|
1084
|
+
try { evolution = evolutionStatus(scope); } catch { return false; }
|
|
1085
|
+
if (!evolution || evolution.enabled !== true || evolution.compatible === false) return false;
|
|
1086
|
+
if (held && held.epoch && String(evolution.epoch || '') !== String(held.epoch)) return false;
|
|
1087
|
+
return true;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
743
1090
|
/** 读决策 → 一段短提示:未绑定只给栏目索引,已绑定才给规则正文。 */
|
|
744
1091
|
function renderExperiment(scope, decision, context, held, win) {
|
|
1092
|
+
if (held && typeof held === 'object') { held.validUntil = ''; held.experimentParts = null; held.injectedIds = []; }
|
|
1093
|
+
if (!experimentStillCurrent(scope, held)) return '';
|
|
745
1094
|
if (!decision || decision.kind === 'none') return '';
|
|
746
1095
|
if (decision.kind === 'conflict') {
|
|
747
1096
|
return chargeExperiment(win, '这些实验规则互相冲突,先按用户这次说的做,别叠加对立的规则。');
|
|
748
1097
|
}
|
|
749
1098
|
if (decision.kind === 'index' || decision.kind === 'clarify') {
|
|
750
|
-
const names = Array.isArray(decision.ranges) ? decision.ranges.filter(Boolean).slice(0, 3)
|
|
1099
|
+
const names = relevantRanges(Array.isArray(decision.ranges) ? decision.ranges.filter(Boolean) : [], held && held.query).slice(0, 3);
|
|
1100
|
+
/** 没有适用栏目就什么也不注入:不拿无关范围冒充当前任务,也不催用户选栏目。 */
|
|
1101
|
+
if (!names.length) return '';
|
|
751
1102
|
const tail = held && held.contextId ? `\ncontextId=${held.contextId}` : '';
|
|
752
|
-
if (!names.length) return chargeExperiment(win, `先问用户这期做什么栏目,别自己猜。${tail}`);
|
|
753
1103
|
return chargeExperiment(win, `${EXPERIMENT_INDEX_HEADER}\n${names.map((name) => `- ${name}`).join('\n')}${tail}`);
|
|
754
1104
|
}
|
|
755
1105
|
let index;
|
|
@@ -760,9 +1110,12 @@ export function apply(ctx, config) {
|
|
|
760
1110
|
let conflict = false;
|
|
761
1111
|
for (const record of index.entries) {
|
|
762
1112
|
if (record.origin !== 'project_evolution') continue;
|
|
1113
|
+
if (record.ownerProjectId && record.ownerProjectId !== scope.key) continue;
|
|
763
1114
|
if (record.reviewState === 'legacy_unreviewed') continue;
|
|
764
1115
|
const learning = record.learning && typeof record.learning === 'object' ? record.learning : {};
|
|
765
1116
|
if (learning.state !== 'active' && learning.state !== 'trial') continue;
|
|
1117
|
+
/** 试用到期、暂停或身份失效:自动注入与检索、读取用同一档可用性,不能各判各的。 */
|
|
1118
|
+
if (!view.experimentalUsable(record, Date.now())) continue;
|
|
766
1119
|
const status = view.statusOf(record, context);
|
|
767
1120
|
if (status === 'needs_context') { unknown.push(record); continue; }
|
|
768
1121
|
if (status !== 'ok') continue;
|
|
@@ -774,33 +1127,40 @@ export function apply(ctx, config) {
|
|
|
774
1127
|
if (conflict) {
|
|
775
1128
|
return chargeExperiment(win, '这些实验规则互相冲突,先按用户这次说的做,别叠加对立的规则。');
|
|
776
1129
|
}
|
|
777
|
-
const
|
|
778
|
-
const shown = [];
|
|
1130
|
+
const parts = { header: EXPERIMENT_RULES_HEADER, items: [], tail: held && held.contextId ? `\ncontextId=${held.contextId}` : '' };
|
|
779
1131
|
let used = Buffer.byteLength(EXPERIMENT_RULES_HEADER) + 1;
|
|
780
1132
|
for (const record of matched.slice(0, 3)) {
|
|
781
1133
|
const body = String(record.body || '');
|
|
782
1134
|
const short = body && body.length <= EXPERIMENT_SHORT_CHARS && body.split('\n').length <= 4;
|
|
783
1135
|
const conditions = conditionLabel(record.conditions);
|
|
784
|
-
const
|
|
785
|
-
|
|
1136
|
+
const checkpoint = record.learning && Array.isArray(record.learning.checkpoints) ? record.learning.checkpoints[0] : null;
|
|
1137
|
+
const check = checkpoint && checkpoint.text
|
|
1138
|
+
? `;核对:${String(checkpoint.text).slice(0, 40)}`
|
|
786
1139
|
: '';
|
|
787
1140
|
const line = short
|
|
788
|
-
? `- 「${record.title}」${conditions ? `(${conditions})` : ''} · ${record.id}
|
|
789
|
-
: `- 「${record.title}」 · ${record.id}
|
|
1141
|
+
? `- 「${record.title}」${conditions ? `(${conditions})` : ''} · ${record.id}:${body}${check}`
|
|
1142
|
+
: `- 「${record.title}」 · ${record.id}(用途:${record.summary || record.title};要用时 memory_read 取正文)`;
|
|
790
1143
|
const size = Buffer.byteLength(`\n${line}`);
|
|
791
1144
|
if (used + size > EXPERIMENT_BYTES) break;
|
|
792
1145
|
used += size;
|
|
793
|
-
|
|
794
|
-
|
|
1146
|
+
const trialLearning = record.learning && typeof record.learning === 'object' ? record.learning : {};
|
|
1147
|
+
const trialUntil = trialLearning.state === 'trial' && Number.isFinite(Date.parse(String(trialLearning.trialUntil || '')))
|
|
1148
|
+
? String(trialLearning.trialUntil) : '';
|
|
1149
|
+
parts.items.push({ id: record.id, line, validUntil: trialUntil });
|
|
1150
|
+
}
|
|
1151
|
+
if (held) held.injectedIds = parts.items.map((item) => item.id);
|
|
1152
|
+
if (!parts.items.length && unknown.length) {
|
|
1153
|
+
/** 条件没确认的条目只报数量,绝不在模型侧暴露标题。 */
|
|
1154
|
+
return chargeExperiment(win, '有些实验规则的适用条件还没确认,先按用户这次说的做,别把它们当本轮依据。');
|
|
795
1155
|
}
|
|
796
|
-
if (
|
|
797
|
-
if (
|
|
798
|
-
|
|
799
|
-
|
|
1156
|
+
if (!parts.items.length) return '';
|
|
1157
|
+
if (held) held.experimentParts = parts;
|
|
1158
|
+
/** 兼容旧读取点:整体最早到期只取本轮真正注入的条目,不再被无关试验拖下水。 */
|
|
1159
|
+
if (held) {
|
|
1160
|
+
const deadlines = parts.items.map((item) => Date.parse(String(item.validUntil))).filter(Number.isFinite);
|
|
1161
|
+
if (deadlines.length) held.validUntil = new Date(Math.min(...deadlines)).toISOString();
|
|
800
1162
|
}
|
|
801
|
-
|
|
802
|
-
const tail = held && held.contextId ? `\ncontextId=${held.contextId}` : '';
|
|
803
|
-
return chargeExperiment(win, `${EXPERIMENT_RULES_HEADER}\n${lines.join('\n')}${tail}`);
|
|
1163
|
+
return chargeExperiment(win, experimentTextFrom(parts));
|
|
804
1164
|
}
|
|
805
1165
|
|
|
806
1166
|
/** 一次真人请求只路由一次;同一批次重试复用同一 transitionId。 */
|
|
@@ -840,9 +1200,11 @@ export function apply(ctx, config) {
|
|
|
840
1200
|
});
|
|
841
1201
|
const contextId = anchor.contextIdFor(scope.key, batchKey, crypto.randomUUID().slice(0, 8));
|
|
842
1202
|
const episode = routed.tasks && routed.tasks.episodes ? routed.tasks.episodes[routed.anchor.episodeId] : null;
|
|
1203
|
+
let epoch = '';
|
|
1204
|
+
try { epoch = String(evolutionStatus(scope).epoch || ''); } catch { epoch = ''; }
|
|
843
1205
|
const held = {
|
|
844
|
-
contextId, projectKey: scope.key, sessionId, batchKey, messageIds,
|
|
845
|
-
context, anchor: routed.anchor, ranges, decision,
|
|
1206
|
+
contextId, projectKey: scope.key, sessionId, batchKey, messageIds, query,
|
|
1207
|
+
context, anchor: routed.anchor, ranges, decision, epoch,
|
|
846
1208
|
episodeUpdatedAt: episode ? episode.updatedAt : '', injectedIds: [],
|
|
847
1209
|
at: Date.now(),
|
|
848
1210
|
};
|
|
@@ -866,42 +1228,77 @@ export function apply(ctx, config) {
|
|
|
866
1228
|
if (!win) return { ok: false, status: 'needs_context', reason: '这个请求的上下文已经过期,先重新问一次当前任务' };
|
|
867
1229
|
const held = win.contexts.get(contextId);
|
|
868
1230
|
if (!held) return { ok: false, status: 'needs_context', reason: '这个上下文已经过期,先重新问一次当前任务' };
|
|
1231
|
+
/** 凭据只在当轮、当次开关代次内有效:换轮、关闭、重开或过期都不再放行旧正文。 */
|
|
1232
|
+
const expired = (reason) => {
|
|
1233
|
+
win.contexts.delete(contextId);
|
|
1234
|
+
if (win.experiment && win.experiment.contextId === contextId) win.experiment = null;
|
|
1235
|
+
return { ok: false, status: 'needs_context', reason };
|
|
1236
|
+
};
|
|
1237
|
+
if (held.projectKey !== scope.key) return expired('这个上下文属于另一个项目');
|
|
1238
|
+
if (held.sessionId && held.sessionId !== String(win.sessionId || '')) return expired('这个上下文已经过期,先重新问一次当前任务');
|
|
1239
|
+
if (!held.batchKey || held.batchKey !== anchor.requestBatchKey(win.messageIds || [])) {
|
|
1240
|
+
return expired('这个上下文已经过期,先重新问一次当前任务');
|
|
1241
|
+
}
|
|
1242
|
+
let evolution;
|
|
1243
|
+
try { evolution = evolutionStatus(scope); } catch { return expired('当前项目状态读不出来,先重新问一次当前任务'); }
|
|
1244
|
+
if (!evolution || evolution.enabled !== true || evolution.compatible === false) {
|
|
1245
|
+
return expired('项目自进化已经关闭,先重新问一次当前任务');
|
|
1246
|
+
}
|
|
1247
|
+
if (held.epoch && evolution.epoch !== held.epoch) {
|
|
1248
|
+
return expired('项目自进化重新开过,先重新问一次当前任务');
|
|
1249
|
+
}
|
|
869
1250
|
return { ok: true, scope, win, context: held.context, experimental: true, held, contextId };
|
|
870
1251
|
}
|
|
871
1252
|
|
|
872
|
-
/**
|
|
1253
|
+
/** 工具模式没有自动索引时,按本轮真人消息现开一个请求上下文,只按问题与记忆范围的字面关系判断。 */
|
|
873
1254
|
function toolContextFor(execution) {
|
|
874
1255
|
const win = windowOf(execution && execution.agent);
|
|
875
1256
|
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
1257
|
const scope = scopeOf(cwdOf(execution));
|
|
880
1258
|
if (scope.confirmed !== true) return '';
|
|
1259
|
+
let status;
|
|
1260
|
+
try { status = evolutionStatus(scope); } catch { return ''; }
|
|
1261
|
+
if (!status || status.enabled !== true || status.compatible === false) return '';
|
|
881
1262
|
const batchKey = anchor.requestBatchKey(win.messageIds);
|
|
882
|
-
const held = win.experiment && win.experiment.batchKey === batchKey
|
|
1263
|
+
const held = win.experiment && win.experiment.batchKey === batchKey && win.experiment.epoch === status.epoch
|
|
1264
|
+
? win.experiment
|
|
1265
|
+
: null;
|
|
883
1266
|
if (held) return held.contextId;
|
|
884
1267
|
const contextId = anchor.contextIdFor(scope.key, batchKey, crypto.randomUUID().slice(0, 8));
|
|
885
1268
|
const created = {
|
|
886
1269
|
contextId, projectKey: scope.key, sessionId: win.sessionId, batchKey,
|
|
887
|
-
messageIds: win.messageIds.slice(), context: view.trustedContext(scope, {}),
|
|
888
|
-
anchor: null, ranges: rangesOf(scope), decision: { kind: 'none' }, at: Date.now(),
|
|
1270
|
+
messageIds: win.messageIds.slice(), query: trustedQueryOf(win), context: view.trustedContext(scope, {}),
|
|
1271
|
+
anchor: null, ranges: rangesOf(scope), decision: { kind: 'none' }, epoch: status.epoch, at: Date.now(),
|
|
889
1272
|
};
|
|
890
1273
|
win.experiment = created;
|
|
891
1274
|
win.contexts.set(contextId, created);
|
|
892
1275
|
return contextId;
|
|
893
1276
|
}
|
|
894
1277
|
|
|
1278
|
+
/** 本轮真人消息是唯一可信的任务依据;模型自己写的 query 只用于检索词,不能拿来建绑定。 */
|
|
1279
|
+
function trustedQueryOf(win) {
|
|
1280
|
+
return win && typeof win.query === 'string' ? win.query : '';
|
|
1281
|
+
}
|
|
1282
|
+
|
|
895
1283
|
/** 前台绑定只升到 proposed:真正执行链还没发生,不冒充 active。 */
|
|
896
|
-
function bindSelection(execution, resolved
|
|
1284
|
+
function bindSelection(execution, resolved) {
|
|
897
1285
|
const held = resolved.held;
|
|
1286
|
+
if (!held || typeof held !== 'object') return { ok: false, status: 'needs_context', reason: '本轮还没有可核对的用户消息,先重新问一次当前任务' };
|
|
1287
|
+
const query = held.query || trustedQueryOf(resolved.win);
|
|
1288
|
+
if (!query) return { ok: false, status: 'needs_context', reason: '本轮还没有可核对的用户消息,先重新问一次当前任务' };
|
|
898
1289
|
const ranges = held.ranges || { series: [], taskKinds: [] };
|
|
1290
|
+
/** 自动路由已经选了范围就沿用同一个结果,工具绑定不重新猜一遍,避免两处给出不同任务。 */
|
|
1291
|
+
const routedAnchor = held.anchor && typeof held.anchor === 'object' ? held.anchor : null;
|
|
899
1292
|
const hits = [];
|
|
900
|
-
|
|
901
|
-
|
|
1293
|
+
if (routedAnchor && routedAnchor.seriesId) hits.push({ seriesId: routedAnchor.seriesId });
|
|
1294
|
+
else if (routedAnchor && routedAnchor.taskKind) hits.push({ taskKind: routedAnchor.taskKind });
|
|
1295
|
+
else {
|
|
1296
|
+
for (const value of ranges.series || []) if (anchor.seriesChosenBy(query, value)) hits.push({ seriesId: value });
|
|
1297
|
+
for (const value of ranges.taskKinds || []) if (anchor.seriesChosenBy(query, value)) hits.push({ taskKind: value });
|
|
1298
|
+
}
|
|
902
1299
|
if (hits.length !== 1) {
|
|
903
1300
|
const names = [...(ranges.series || []), ...(ranges.taskKinds || [])].slice(0, 3);
|
|
904
|
-
return { ok: false, status: 'needs_context', reason: names.length ?
|
|
1301
|
+
return { ok: false, status: 'needs_context', reason: names.length ? '本轮没有匹配的记忆范围,按当前任务继续' : '本轮没有适用的记忆范围,按当前任务继续' };
|
|
905
1302
|
}
|
|
906
1303
|
let tasks;
|
|
907
1304
|
try { tasks = taskState(resolved.scope); } catch { return { ok: false, status: 'needs_context', reason: '任务状态还没读出来' }; }
|
|
@@ -920,7 +1317,11 @@ export function apply(ctx, config) {
|
|
|
920
1317
|
resolved.context = held.context;
|
|
921
1318
|
const text = renderExperiment(resolved.scope, held.decision, held.context, held, resolved.win);
|
|
922
1319
|
const preparedState = prepared.get(execution && execution.agent);
|
|
923
|
-
if (preparedState)
|
|
1320
|
+
if (preparedState) {
|
|
1321
|
+
preparedState.experimentText = text;
|
|
1322
|
+
preparedState.experimentParts = held.experimentParts || null;
|
|
1323
|
+
preparedState.validUntil = held.validUntil || '';
|
|
1324
|
+
}
|
|
924
1325
|
return { ok: true };
|
|
925
1326
|
}
|
|
926
1327
|
|
|
@@ -936,7 +1337,22 @@ export function apply(ctx, config) {
|
|
|
936
1337
|
if (state.signal && signal && state.signal !== signal) return '';
|
|
937
1338
|
const sessionId = agent?.session?.id;
|
|
938
1339
|
if (state.sessionId !== (typeof sessionId === 'string' ? sessionId : '')) return '';
|
|
939
|
-
|
|
1340
|
+
/** 开关/自进化换代后,关掉前准备好的正文一律不再输出。 */
|
|
1341
|
+
const scope = scopeOf(cwdOf({ agent }));
|
|
1342
|
+
if (!memoryEnabled(scope)) return '';
|
|
1343
|
+
const epoch = controlStatus(scope).epoch || '';
|
|
1344
|
+
if (state.controlEpoch && epoch && state.controlEpoch !== epoch) return '';
|
|
1345
|
+
/** 自进化关掉或换过代:关掉前的实验正文不再输出,重开也不复活。 */
|
|
1346
|
+
let evolutionText = state.experimentParts ? experimentTextFrom(state.experimentParts) : state.experimentText;
|
|
1347
|
+
if (evolutionText) {
|
|
1348
|
+
let evolution;
|
|
1349
|
+
try { evolution = evolutionStatus(scope); } catch { evolution = null; }
|
|
1350
|
+
if (!evolution || evolution.enabled !== true || evolution.compatible === false
|
|
1351
|
+
|| (state.evolutionEpoch && String(evolution.epoch || '') !== state.evolutionEpoch)) evolutionText = '';
|
|
1352
|
+
}
|
|
1353
|
+
/** 没有逐条快照时才退回整段到期判断;有快照就只撤到期的那一条。 */
|
|
1354
|
+
if (evolutionText && !state.experimentParts && state.validUntil && Date.now() > Date.parse(String(state.validUntil))) evolutionText = '';
|
|
1355
|
+
return [evolutionText, state.text].filter(Boolean).join('\n\n');
|
|
940
1356
|
} catch {
|
|
941
1357
|
return '';
|
|
942
1358
|
}
|
|
@@ -945,7 +1361,7 @@ export function apply(ctx, config) {
|
|
|
945
1361
|
|
|
946
1362
|
ctx.effect(() => tools.register({
|
|
947
1363
|
name: 'memory_save',
|
|
948
|
-
description:
|
|
1364
|
+
description: `保存一条记忆。只在会改变以后判断时才存;先搜已有条目,能更新就不要新建。保存前先蒸馏:标题是结论,正文只写原则与触发条件,不抄原始输出里的具体数值。用户明确说「记住」时直接调用;用户自己的话已经是长期规则(「以后都这样」「别再 X」)时也直接调用,不要先问。更新同一件事时带 id:身份只看标题、旧标题和前置条件,像不像不授权覆盖。文本相近但标题不同时先不写,返回 needsResolution 与 1–2 条相近条目:确实是同一条就用那个 id 更新,确实是另一条才带 resolution="distinct" 再存。带 id 更新时没给的字段保持原值,正文整段替换,旧正文只留作内部撤回,不展示也不参与检索。条数到硬上限时把同层最久没被读的一条移进归档,结果里写明动了哪条,不拒绝新记忆;单条正文超限只报错、不截断。前置条件不同的同一标题是两条。${NOTES}`,
|
|
949
1365
|
parameters: SAVE_PARAMS,
|
|
950
1366
|
output: {
|
|
951
1367
|
presentationMeta: (_args, value) => ({
|
|
@@ -967,10 +1383,13 @@ export function apply(ctx, config) {
|
|
|
967
1383
|
title: { type: 'string' },
|
|
968
1384
|
summary: { type: 'string' },
|
|
969
1385
|
when: { type: 'string' },
|
|
1386
|
+
summaryShortened: { type: 'boolean' },
|
|
1387
|
+
fullTextInBody: { type: 'boolean' },
|
|
970
1388
|
revision: { type: 'integer' },
|
|
971
1389
|
merged: { type: 'boolean' },
|
|
972
1390
|
changed: { type: 'boolean' },
|
|
973
1391
|
scope: { type: 'string' },
|
|
1392
|
+
scopeName: { type: 'string' },
|
|
974
1393
|
needsResolution: { type: 'boolean' },
|
|
975
1394
|
needsScope: { type: 'boolean' },
|
|
976
1395
|
reason: { type: 'string' },
|
|
@@ -988,20 +1407,30 @@ export function apply(ctx, config) {
|
|
|
988
1407
|
render: (_args, value) => [{
|
|
989
1408
|
type: 'text',
|
|
990
1409
|
text: value.needsScope
|
|
991
|
-
? value.reason === '
|
|
1410
|
+
? value.reason === 'memory_disabled'
|
|
1411
|
+
? 'memory · 本项目记忆已关闭,先在面板打开'
|
|
1412
|
+
: value.reason === 'unconfirmed_project'
|
|
992
1413
|
? `memory · 当前目录还不能确认属于哪个项目:「${value.title}」\n先确认项目根目录,或显式 scope="global"`
|
|
993
1414
|
: `memory · 归属还没定:「${value.title}」\n补 memoryClass(personal_preference / project_fact / environment_fact / portable_procedure)或显式 scope;设备事实还要给设备条件`
|
|
994
1415
|
: value.existingId
|
|
995
|
-
? `memory · 已有相同记忆:「${value.title}」 · ${value.existingId}(${value.existingScope}
|
|
1416
|
+
? `memory · 已有相同记忆:「${value.title}」 · ${value.existingId}(${value.existingScope})\n要改成另一条,加 resolution="distinct"`
|
|
996
1417
|
: value.needsResolution
|
|
997
1418
|
? `memory · 需要确认:「${value.title}」与已有条目相近${value.crossLayer ? '(在另一层)' : ''}${value.sameTopic ? '(同一主题槽位)' : ''}\n${value.similar}\n要更新它,用 id 重存;确实是另一条,加 resolution="distinct"`
|
|
998
1419
|
: value.changed === false
|
|
999
|
-
? `memory · unchanged${
|
|
1000
|
-
: `memory · ${value.merged ? 'merged' : 'saved'}${
|
|
1420
|
+
? `memory · unchanged ${withTag(value, `"${value.title}"`)} · ${value.id}`
|
|
1421
|
+
: `memory · ${value.merged ? 'merged' : 'saved'} ${withTag(value, `"${value.title}"`)}${value.when ? `(${value.when})` : ''} · ${value.id}${value.similar ? ` · 相近:${value.similar}` : ''}${value.archived ? ` · 已归档「${value.archived}」腾位` : ''}${value.fullTextInBody ? ' · 摘要超过 60 字,完整原句已存进正文' : ''}`,
|
|
1001
1422
|
}],
|
|
1002
1423
|
},
|
|
1003
1424
|
execute: async (args, execution) => {
|
|
1004
1425
|
const projectScope = scopeOf(cwdOf(execution));
|
|
1426
|
+
const projectScopeName = projectNameOf(projectScope);
|
|
1427
|
+
if (!memoryEnabled(projectScope)) {
|
|
1428
|
+
return {
|
|
1429
|
+
id: '', title: String(args.title || ''), summary: '', when: '', revision: 0,
|
|
1430
|
+
merged: false, changed: false, scope: '', needsScope: true, reason: 'memory_disabled',
|
|
1431
|
+
needsResolution: false, similarIds: '', similar: '', archived: '',
|
|
1432
|
+
};
|
|
1433
|
+
}
|
|
1005
1434
|
const saved = saveEntry(projectScope, { ...args, writeMode: args.mode || 'patch' });
|
|
1006
1435
|
if (saved.needsScope) {
|
|
1007
1436
|
return {
|
|
@@ -1031,6 +1460,7 @@ export function apply(ctx, config) {
|
|
|
1031
1460
|
merged: false,
|
|
1032
1461
|
changed: false,
|
|
1033
1462
|
scope: saved.existing.scope,
|
|
1463
|
+
scopeName: saved.existing.scope === 'global' ? '' : projectScopeName,
|
|
1034
1464
|
needsScope: false,
|
|
1035
1465
|
existingId: saved.existing.id,
|
|
1036
1466
|
existingScope: saved.existing.scope,
|
|
@@ -1066,10 +1496,13 @@ export function apply(ctx, config) {
|
|
|
1066
1496
|
title: saved.record.title,
|
|
1067
1497
|
summary: saved.record.summary,
|
|
1068
1498
|
when: saved.record.when || '',
|
|
1499
|
+
summaryShortened: Boolean(saved.draft?.summaryShortened),
|
|
1500
|
+
fullTextInBody: Boolean(saved.draft?.fullTextInBody),
|
|
1069
1501
|
revision: saved.record.revision || 1,
|
|
1070
1502
|
merged: saved.merged,
|
|
1071
1503
|
changed: !saved.noop,
|
|
1072
1504
|
scope: scope.key === 'global' ? 'global' : 'project',
|
|
1505
|
+
scopeName: scope.key === 'global' ? '' : projectScopeName,
|
|
1073
1506
|
needsScope: false,
|
|
1074
1507
|
needsResolution: false,
|
|
1075
1508
|
similarIds: saved.similarList.map((item) => item.id).join('、'),
|
|
@@ -1107,6 +1540,7 @@ export function apply(ctx, config) {
|
|
|
1107
1540
|
},
|
|
1108
1541
|
execute: async (args, execution) => {
|
|
1109
1542
|
const scope = scopeOf(cwdOf(execution));
|
|
1543
|
+
if (!memoryEnabled(scope)) return { asked: false, reason: '本项目记忆已关闭' };
|
|
1110
1544
|
const gate = suggestionGate(scope, args);
|
|
1111
1545
|
if (!gate.ok) return { asked: false, reason: gate.reason };
|
|
1112
1546
|
const noted = noteSuggestion(scope, gate.target ? { ...args, targetId: gate.target.id } : args);
|
|
@@ -1121,7 +1555,7 @@ export function apply(ctx, config) {
|
|
|
1121
1555
|
|
|
1122
1556
|
ctx.effect(() => tools.register({
|
|
1123
1557
|
name: 'memory_search',
|
|
1124
|
-
description: `按需检索已有记忆:先搜,命中再 memory_read 读正文。用户提到「记住了什么 / 上次 / 规范 / 约定 / 路径 / 凭据怎么取」,或者眼前反复出现的报错、弹窗、异常时,先查这里有没有记过解法。跨项目的通用条目以 [
|
|
1558
|
+
description: `按需检索已有记忆:先搜,命中再 memory_read 读正文。用户提到「记住了什么 / 上次 / 规范 / 约定 / 路径 / 凭据怎么取」,或者眼前反复出现的报错、弹窗、异常时,先查这里有没有记过解法。跨项目的通用条目以 [global] 开头,项目条目以 [项目名] 开头。中文长句可以直接当查询。每行带 id,更新时把 id 传回 memory_save。默认只列当前有效的;已经归档或被更新替换的旧说法不混进来,要查历史就在参数里显式要 history。`,
|
|
1125
1559
|
parameters: SEARCH_PARAMS,
|
|
1126
1560
|
output: {
|
|
1127
1561
|
presentationMeta: (_args, value) => ({ kind: 'memory-search', count: value.count, status: value.status || 'ok', refs: value.refs || [] }),
|
|
@@ -1132,6 +1566,7 @@ export function apply(ctx, config) {
|
|
|
1132
1566
|
chars: { type: 'integer' },
|
|
1133
1567
|
text: { type: 'string' },
|
|
1134
1568
|
status: { type: 'string', enum: ['ok', 'budget_exhausted', 'memory_disabled', 'needs_context'] },
|
|
1569
|
+
reasonCode: { type: 'string' },
|
|
1135
1570
|
refs: {
|
|
1136
1571
|
type: 'array',
|
|
1137
1572
|
items: {
|
|
@@ -1142,8 +1577,9 @@ export function apply(ctx, config) {
|
|
|
1142
1577
|
scope: { type: 'string' },
|
|
1143
1578
|
revision: { type: 'integer' },
|
|
1144
1579
|
status: { type: 'string' },
|
|
1580
|
+
origin: { type: 'string' },
|
|
1145
1581
|
},
|
|
1146
|
-
required: ['id', 'title', 'scope', 'revision', 'status'],
|
|
1582
|
+
required: ['id', 'title', 'scope', 'revision', 'status', 'origin'],
|
|
1147
1583
|
additionalProperties: false,
|
|
1148
1584
|
},
|
|
1149
1585
|
},
|
|
@@ -1165,7 +1601,7 @@ export function apply(ctx, config) {
|
|
|
1165
1601
|
}],
|
|
1166
1602
|
},
|
|
1167
1603
|
execute: async (args, execution) => {
|
|
1168
|
-
if (recallMode() === 'off') return { count: 0, chars: 0, text: '', status: 'memory_disabled', refs: [] };
|
|
1604
|
+
if (recallMode() === 'off' || !memoryEnabledFor(execution)) return { count: 0, chars: 0, text: '', status: 'memory_disabled', refs: [] };
|
|
1169
1605
|
const agent = execution?.agent;
|
|
1170
1606
|
const win = windowOf(agent);
|
|
1171
1607
|
if (win && !affordable(win, 0)) return { count: 0, chars: 0, text: '', status: 'budget_exhausted', refs: [] };
|
|
@@ -1177,21 +1613,50 @@ export function apply(ctx, config) {
|
|
|
1177
1613
|
const resolved = resolveRequestContext(execution, contextId);
|
|
1178
1614
|
if (!resolved.ok) return { count: 0, chars: 0, text: resolved.reason, status: resolved.status || 'needs_context', refs: [] };
|
|
1179
1615
|
if (wantsBind) {
|
|
1180
|
-
const bound = bindSelection(execution, resolved
|
|
1616
|
+
const bound = bindSelection(execution, resolved);
|
|
1181
1617
|
if (!bound.ok) return { count: 0, chars: 0, text: bound.reason, status: bound.status || 'needs_context', refs: [] };
|
|
1182
1618
|
}
|
|
1183
1619
|
const scope = resolved.scope;
|
|
1184
1620
|
const context = resolved.context;
|
|
1185
|
-
|
|
1621
|
+
/** 检索只列当前任务真能用的条目;先取全量候选,过滤后再截断,避免不可用条目挤掉后面的命中。 */
|
|
1622
|
+
const limit = args.limit ?? 3;
|
|
1623
|
+
const rawHits = search(scope, args.query, 50, {
|
|
1186
1624
|
context,
|
|
1187
1625
|
history: args.history === true,
|
|
1188
1626
|
allowExperimental: resolved.experimental === true,
|
|
1189
|
-
});
|
|
1190
|
-
const
|
|
1627
|
+
}).filter((item) => !item.ownerProjectId || item.ownerProjectId === scope.key);
|
|
1628
|
+
const hits = rawHits.filter((item) => item.archived || item.status === 'ok').slice(0, limit);
|
|
1629
|
+
const withheld = rawHits.filter((item) => !item.archived && item.status !== 'ok');
|
|
1630
|
+
const scopeName = scope.key === 'global' ? '' : projectNameOf(scope);
|
|
1631
|
+
const shown = hits.map((item) => ({
|
|
1632
|
+
...view.toModelView(item, 'search', { status: item.status || (item.archived ? 'archived' : 'ok') }),
|
|
1633
|
+
scopeName: item.scope === 'global' ? '' : scopeName,
|
|
1634
|
+
}));
|
|
1191
1635
|
const valueOf = (kept) => {
|
|
1192
1636
|
const rendered = renderIndex(shown.slice(0, kept));
|
|
1193
1637
|
return { count: kept, chars: rendered.chars, text: rendered.text, status: 'ok', refs: refsOf(hits.slice(0, kept)) };
|
|
1194
1638
|
};
|
|
1639
|
+
/** 有匹配但当前用不上:只说状态,不把标题、编号或旧规则递给模型,避免它被这些字面带偏。 */
|
|
1640
|
+
if (!hits.length && withheld.length) {
|
|
1641
|
+
const unresolved = withheld.some((item) => item.status === 'needs_context');
|
|
1642
|
+
const value = {
|
|
1643
|
+
count: 0,
|
|
1644
|
+
chars: 0,
|
|
1645
|
+
text: unresolved
|
|
1646
|
+
? '有相关的项目记忆,但适用条件还没确认。先说明当前任务,或确认这条记忆的适用范围。'
|
|
1647
|
+
: '有相关的项目记忆,但当前任务明确不适用。',
|
|
1648
|
+
status: unresolved ? 'needs_context' : 'ok',
|
|
1649
|
+
reasonCode: unresolved ? 'applicability_unresolved' : 'not_applicable',
|
|
1650
|
+
refs: [],
|
|
1651
|
+
};
|
|
1652
|
+
if (win) {
|
|
1653
|
+
const bytes = jsonBytes(value);
|
|
1654
|
+
if (!affordable(win, bytes)) return { ...value, status: 'budget_exhausted', text: '' };
|
|
1655
|
+
win.bytes += bytes;
|
|
1656
|
+
win.reads += 1;
|
|
1657
|
+
}
|
|
1658
|
+
return value;
|
|
1659
|
+
}
|
|
1195
1660
|
/** 没命中是正常结果,不是额度问题;空响应同样记一次本请求开销。 */
|
|
1196
1661
|
if (!hits.length) {
|
|
1197
1662
|
const empty = { count: 0, chars: 0, text: '', status: 'ok', refs: [] };
|
|
@@ -1232,6 +1697,7 @@ export function apply(ctx, config) {
|
|
|
1232
1697
|
when: { type: 'string' },
|
|
1233
1698
|
revision: { type: 'integer' },
|
|
1234
1699
|
scope: { type: 'string' },
|
|
1700
|
+
scopeName: { type: 'string' },
|
|
1235
1701
|
origin: { type: 'string', enum: ['user_explicit', 'project_evolution'] },
|
|
1236
1702
|
status: { type: 'string', enum: ['ok', 'archived', 'needs_context', 'not_applicable', 'gone', 'budget_exhausted', 'memory_disabled'] },
|
|
1237
1703
|
body: { type: 'string' },
|
|
@@ -1248,10 +1714,20 @@ export function apply(ctx, config) {
|
|
|
1248
1714
|
: value.status === 'memory_disabled'
|
|
1249
1715
|
? 'memory · 记忆检索已关闭'
|
|
1250
1716
|
: value.repeat
|
|
1251
|
-
?
|
|
1717
|
+
? 'memory · 本请求已读过这条记忆'
|
|
1252
1718
|
: value.body
|
|
1253
|
-
? `memory ·
|
|
1254
|
-
:
|
|
1719
|
+
? `memory · ${withTag(value, `「${value.title}」`)}${value.when ? `(${value.when})` : ''} · ${value.id}${statusLabel(value.status)}\n${value.body}`
|
|
1720
|
+
: value.links !== undefined && value.status === 'ok'
|
|
1721
|
+
? `memory · 「${value.title}」的相关记忆\n${value.links || '(无)'}`
|
|
1722
|
+
: value.status === 'needs_context'
|
|
1723
|
+
? 'memory · 这条记忆的适用条件还没确认'
|
|
1724
|
+
: value.status === 'not_applicable'
|
|
1725
|
+
? 'memory · 这条记忆不适用于当前任务'
|
|
1726
|
+
: value.status === 'archived'
|
|
1727
|
+
? 'memory · 这条记忆已归档'
|
|
1728
|
+
: value.status === 'gone'
|
|
1729
|
+
? 'memory · 这条记忆已不存在'
|
|
1730
|
+
: 'memory · 当前不能读这条记忆',
|
|
1255
1731
|
}],
|
|
1256
1732
|
presentationMeta: (_args, value) => ({
|
|
1257
1733
|
kind: 'memory-read',
|
|
@@ -1264,10 +1740,11 @@ export function apply(ctx, config) {
|
|
|
1264
1740
|
}),
|
|
1265
1741
|
},
|
|
1266
1742
|
execute: async (args, execution) => {
|
|
1267
|
-
if (recallMode() === 'off') return { id: '', scope: '', status: 'memory_disabled' };
|
|
1743
|
+
if (recallMode() === 'off' || !memoryEnabledFor(execution)) return { id: '', scope: '', status: 'memory_disabled' };
|
|
1268
1744
|
const resolved = resolveRequestContext(execution, args.contextId);
|
|
1269
1745
|
if (!resolved.ok) return { id: '', scope: '', status: resolved.status || 'needs_context' };
|
|
1270
1746
|
const scope = resolved.scope;
|
|
1747
|
+
const scopeName = scope.key === 'global' ? '' : projectNameOf(scope);
|
|
1271
1748
|
const context = resolved.context;
|
|
1272
1749
|
const allowExperimental = resolved.experimental === true;
|
|
1273
1750
|
const agent = execution?.agent;
|
|
@@ -1275,11 +1752,22 @@ export function apply(ctx, config) {
|
|
|
1275
1752
|
if (args.view === 'links') {
|
|
1276
1753
|
const found = readLinks(scope, args.id, { context, allowExperimental });
|
|
1277
1754
|
if (!found) throw new Error(`没有 ${args.id} 这条记忆`);
|
|
1278
|
-
|
|
1755
|
+
if (found.status !== 'ok') {
|
|
1756
|
+
const withheld = { id: '', scope: '', status: found.status, links: '' };
|
|
1757
|
+
if (win) {
|
|
1758
|
+
const bytes = jsonBytes(withheld);
|
|
1759
|
+
if (!affordable(win, bytes)) return { id: '', scope: '', status: 'budget_exhausted' };
|
|
1760
|
+
win.bytes += bytes;
|
|
1761
|
+
win.reads += 1;
|
|
1762
|
+
}
|
|
1763
|
+
return withheld;
|
|
1764
|
+
}
|
|
1765
|
+
const rows = found.links.filter((link) => link.status === 'invalid' || link.status === 'ok')
|
|
1766
|
+
.map((link) => (link.status === 'invalid'
|
|
1279
1767
|
? `- ${link.id} · 已失效`
|
|
1280
|
-
: `-
|
|
1768
|
+
: `- ${link.scope === 'global' ? '[global] ' : (scopeName ? `[${scopeName}] ` : '')}「${link.title}」 · ${link.id}`));
|
|
1281
1769
|
const text = rows.join('\n') || '(无)';
|
|
1282
|
-
const value = { id: found.id, scope: found.scope, status: found.status, links: text };
|
|
1770
|
+
const value = { id: found.id, scope: found.scope, scopeName, status: found.status, links: text };
|
|
1283
1771
|
if (win) {
|
|
1284
1772
|
const bytes = jsonBytes(value);
|
|
1285
1773
|
if (!affordable(win, bytes)) return { id: '', scope: '', status: 'budget_exhausted' };
|
|
@@ -1290,16 +1778,23 @@ export function apply(ctx, config) {
|
|
|
1290
1778
|
}
|
|
1291
1779
|
const record = readEntry(scope, args.id, { context, allowExperimental });
|
|
1292
1780
|
if (!record) throw new Error(`没有 ${args.id} 这条记忆`);
|
|
1781
|
+
if (record.origin === 'project_evolution' && record.ownerProjectId && record.ownerProjectId !== scope.key) {
|
|
1782
|
+
return { id: '', scope: record.scope, status: 'not_applicable', body: '' };
|
|
1783
|
+
}
|
|
1293
1784
|
if (record.origin === 'project_evolution' && !allowExperimental) {
|
|
1294
|
-
return { id:
|
|
1785
|
+
return { id: '', scope: record.scope, status: 'needs_context', body: '' };
|
|
1786
|
+
}
|
|
1787
|
+
/** 暂停、过期或条件不匹配的实验条目只给状态,不给标题和正文。 */
|
|
1788
|
+
if (record.origin === 'project_evolution' && !view.experimentalUsable(record)) {
|
|
1789
|
+
return { id: '', scope: record.scope, status: 'needs_context', body: '' };
|
|
1295
1790
|
}
|
|
1296
|
-
/** 暂停或条件不匹配的实验条目只给状态,不给正文。 */
|
|
1297
1791
|
const learningState = record.learning && typeof record.learning === 'object' ? record.learning : {};
|
|
1298
1792
|
if (record.origin === 'project_evolution' && (learningState.state === 'paused' || learningState.blockedReason)) {
|
|
1299
|
-
return { id:
|
|
1793
|
+
return { id: '', scope: record.scope, status: 'needs_context', body: '' };
|
|
1300
1794
|
}
|
|
1301
|
-
|
|
1302
|
-
|
|
1795
|
+
/** 条件未知或不适用的条目只报状态:正文和标题都不能绕过自动召回的门槛。 */
|
|
1796
|
+
if (record.status !== 'ok') {
|
|
1797
|
+
return { id: '', scope: record.scope, status: record.status === 'needs_context' ? 'needs_context' : 'not_applicable', body: '' };
|
|
1303
1798
|
}
|
|
1304
1799
|
const revision = record.revision || 1;
|
|
1305
1800
|
const body = String(record.body ?? '');
|
|
@@ -1309,6 +1804,7 @@ export function apply(ctx, config) {
|
|
|
1309
1804
|
when: record.when || '',
|
|
1310
1805
|
revision,
|
|
1311
1806
|
scope: record.scope,
|
|
1807
|
+
scopeName,
|
|
1312
1808
|
status: record.status,
|
|
1313
1809
|
body,
|
|
1314
1810
|
};
|
|
@@ -1346,20 +1842,23 @@ export function apply(ctx, config) {
|
|
|
1346
1842
|
id: { type: 'string' },
|
|
1347
1843
|
title: { type: 'string' },
|
|
1348
1844
|
scope: { type: 'string' },
|
|
1349
|
-
|
|
1845
|
+
scopeName: { type: 'string' },
|
|
1846
|
+
mode: { type: 'string', enum: ['archive', 'purged', 'needs_resolution', 'purge_pending', 'memory_disabled'] },
|
|
1350
1847
|
},
|
|
1351
1848
|
required: ['id', 'mode'],
|
|
1352
1849
|
additionalProperties: false,
|
|
1353
1850
|
},
|
|
1354
1851
|
render: (_args, value) => [{
|
|
1355
1852
|
type: 'text',
|
|
1356
|
-
text: value.mode === '
|
|
1853
|
+
text: value.mode === 'memory_disabled'
|
|
1854
|
+
? 'memory · 本项目记忆已关闭,先在面板打开'
|
|
1855
|
+
: value.mode === 'purged'
|
|
1357
1856
|
? `memory · forgotten "${value.title}" · ${value.id}`
|
|
1358
1857
|
: value.mode === 'purge_pending'
|
|
1359
1858
|
? `memory · 清除未完成 · ${value.id},重启后会继续`
|
|
1360
1859
|
: value.mode === 'needs_resolution'
|
|
1361
1860
|
? `memory · 先确认是归档还是彻底忘掉 · ${value.id}`
|
|
1362
|
-
: `memory · archived${
|
|
1861
|
+
: `memory · archived ${withTag(value, `"${value.title}"`)} · ${value.id},可找回`,
|
|
1363
1862
|
}],
|
|
1364
1863
|
presentationMeta: (_args, value) => ({
|
|
1365
1864
|
kind: 'memory-forget',
|
|
@@ -1371,6 +1870,8 @@ export function apply(ctx, config) {
|
|
|
1371
1870
|
},
|
|
1372
1871
|
execute: async (args, execution) => {
|
|
1373
1872
|
const scope = scopeOf(cwdOf(execution));
|
|
1873
|
+
const projectScopeName = projectNameOf(scope);
|
|
1874
|
+
if (!memoryEnabled(scope)) return { id: String(args.id || ''), title: '', scope: '', mode: 'memory_disabled' };
|
|
1374
1875
|
const located = locate(scope, args.id);
|
|
1375
1876
|
if (!located && !args.mode) throw new Error(`没有 ${args.id} 这条记忆`);
|
|
1376
1877
|
if (!args.mode) {
|
|
@@ -1392,6 +1893,7 @@ export function apply(ctx, config) {
|
|
|
1392
1893
|
title: located ? located.record.title : '',
|
|
1393
1894
|
mode: 'purge_pending',
|
|
1394
1895
|
scope: layer.key === 'global' ? 'global' : 'project',
|
|
1896
|
+
scopeName: layer.key === 'global' ? '' : projectScopeName,
|
|
1395
1897
|
};
|
|
1396
1898
|
}
|
|
1397
1899
|
throw again;
|
|
@@ -1402,11 +1904,13 @@ export function apply(ctx, config) {
|
|
|
1402
1904
|
}
|
|
1403
1905
|
if (!dropped) throw new Error(`没有 ${args.id} 这条记忆`);
|
|
1404
1906
|
const layerKey = dropped.scopeKey || (located ? located.scope.key : scope.key);
|
|
1907
|
+
if (dropped.mode === 'purged') revokeLearningUse(layerKey);
|
|
1405
1908
|
return {
|
|
1406
1909
|
id: dropped.id,
|
|
1407
1910
|
title: dropped.title || (located ? located.record.title : ''),
|
|
1408
1911
|
mode: dropped.mode,
|
|
1409
1912
|
scope: layerKey === 'global' ? 'global' : 'project',
|
|
1913
|
+
scopeName: layerKey === 'global' ? '' : projectScopeName,
|
|
1410
1914
|
};
|
|
1411
1915
|
},
|
|
1412
1916
|
}), 'lite-memory: memory_forget');
|
|
@@ -1445,9 +1949,23 @@ export function apply(ctx, config) {
|
|
|
1445
1949
|
}), 'lite-memory: recall context');
|
|
1446
1950
|
}
|
|
1447
1951
|
|
|
1952
|
+
/** 执行入口再拦一次:工具若已出现在旧快照里,调用时也不放行。 */
|
|
1953
|
+
if (typeof ctx.on === 'function') ctx.on('agent/created', (payload) => {
|
|
1954
|
+
const agent = payload?.agent;
|
|
1955
|
+
if (!agent?.ctx?.tools) return;
|
|
1956
|
+
try {
|
|
1957
|
+
agent.ctx.effect(() => agent.ctx.tools.guard((execution) => {
|
|
1958
|
+
if (!MEMORY_TOOLS.includes(execution?.name)) return undefined;
|
|
1959
|
+
return memoryOnForAgent(agent) ? undefined : '本项目记忆已关闭,先在面板打开';
|
|
1960
|
+
}), 'lite-memory: tool guard');
|
|
1961
|
+
} catch { /* older host without guards */ }
|
|
1962
|
+
syncMemoryTools(agent);
|
|
1963
|
+
});
|
|
1964
|
+
|
|
1448
1965
|
if (typeof ctx.on === 'function') ctx.on('agent/pre-step', async (payload, next) => {
|
|
1449
1966
|
const startedAt = Date.now();
|
|
1450
1967
|
const agent = payload?.agent;
|
|
1968
|
+
syncMemoryTools(agent);
|
|
1451
1969
|
let decision;
|
|
1452
1970
|
try {
|
|
1453
1971
|
decision = await next();
|
|
@@ -1488,6 +2006,10 @@ export function apply(ctx, config) {
|
|
|
1488
2006
|
}
|
|
1489
2007
|
previous.signal = signal;
|
|
1490
2008
|
const scope = scopeOf(cwdOf({ agent }));
|
|
2009
|
+
if (!memoryEnabled(scope)) {
|
|
2010
|
+
clearPrepared(agent);
|
|
2011
|
+
return decision;
|
|
2012
|
+
}
|
|
1491
2013
|
const fingerprint = fingerprintOf(scope);
|
|
1492
2014
|
/** 指纹拿不到就当作已失效:不拿旧任务内容硬撑这一步。 */
|
|
1493
2015
|
if (!fingerprint) {
|
|
@@ -1506,6 +2028,8 @@ export function apply(ctx, config) {
|
|
|
1506
2028
|
previous.text = snapshot ? snapshot.text : '';
|
|
1507
2029
|
previous.cards = snapshot ? snapshot.cards : [];
|
|
1508
2030
|
previous.experimentText = experimentText;
|
|
2031
|
+
previous.experimentParts = win && win.experiment ? (win.experiment.experimentParts || null) : null;
|
|
2032
|
+
previous.validUntil = win && win.experiment ? (win.experiment.validUntil || '') : '';
|
|
1509
2033
|
previous.fingerprint = fingerprint;
|
|
1510
2034
|
} else {
|
|
1511
2035
|
clearPrepared(agent);
|
|
@@ -1518,13 +2042,15 @@ export function apply(ctx, config) {
|
|
|
1518
2042
|
/** 工具模式也要开额度窗口:只是不自动注入,工具调用同样受每请求预算约束。 */
|
|
1519
2043
|
const ids = fresh.map((message) => String(message?.id ?? '')).filter(Boolean).sort();
|
|
1520
2044
|
const win = openWindow(agent, { sessionId, turn, messageIds: ids });
|
|
2045
|
+
/** 工具模式也要记下本轮真人原话:只用于核对绑定,不自动注入。 */
|
|
2046
|
+
const query = view.newestUserQuery(fresh).slice(0, RECALL_QUERY_CHARS);
|
|
2047
|
+
if (win && query) win.query = query;
|
|
1521
2048
|
|
|
1522
2049
|
if (recallMode() !== 'adaptive') {
|
|
1523
2050
|
clearPrepared(agent);
|
|
1524
2051
|
return decision;
|
|
1525
2052
|
}
|
|
1526
2053
|
|
|
1527
|
-
const query = view.newestUserQuery(fresh).slice(0, RECALL_QUERY_CHARS);
|
|
1528
2054
|
if (!query) {
|
|
1529
2055
|
clearPrepared(agent);
|
|
1530
2056
|
return decision;
|
|
@@ -1535,36 +2061,51 @@ export function apply(ctx, config) {
|
|
|
1535
2061
|
return decision;
|
|
1536
2062
|
}
|
|
1537
2063
|
const scope = scopeOf(cwd);
|
|
2064
|
+
if (!memoryEnabled(scope)) {
|
|
2065
|
+
clearPrepared(agent);
|
|
2066
|
+
return decision;
|
|
2067
|
+
}
|
|
1538
2068
|
const context = view.trustedContext(scope, {});
|
|
1539
2069
|
const fingerprint = fingerprintOf(scope);
|
|
1540
|
-
const
|
|
2070
|
+
const controlEpoch = controlStatus(scope).epoch || '';
|
|
2071
|
+
const batchKey = anchor.requestBatchKey(ids);
|
|
2072
|
+
dropStaleCandidate(sessionId, batchKey);
|
|
2073
|
+
const experiment = experimentFor(scope, { sessionId, query, batchKey, messageIds: ids, win });
|
|
1541
2074
|
const remembered = experiment && experiment.held
|
|
1542
|
-
? rememberFeedback(scope, sessionId, fresh, experiment.held)
|
|
2075
|
+
? rememberFeedback(scope, sessionId, fresh, experiment.held, turn)
|
|
1543
2076
|
: null;
|
|
1544
|
-
|
|
2077
|
+
const snapshot = projectRecall(scope, query, context, win);
|
|
2078
|
+
if (generation !== generations.get(agent)) return decision;
|
|
2079
|
+
/** 投影期间开关被改过:丢弃这批内容,不把关闭前的记忆交给当前请求。 */
|
|
2080
|
+
if (!memoryEnabled(scope) || (controlStatus(scope).epoch || '') !== controlEpoch) {
|
|
1545
2081
|
clearPrepared(agent);
|
|
1546
2082
|
return decision;
|
|
1547
2083
|
}
|
|
1548
|
-
const snapshot = projectRecall(scope, query, context, win);
|
|
1549
|
-
if (generation !== generations.get(agent)) return decision;
|
|
1550
2084
|
/** 超时或被取消:这一步不留半旧卡片,下一真人请求重新投影。 */
|
|
1551
2085
|
if (signal?.aborted || Date.now() - startedAt > RECALL_DEADLINE_MS) {
|
|
1552
2086
|
clearPrepared(agent);
|
|
1553
2087
|
return decision;
|
|
1554
2088
|
}
|
|
1555
|
-
|
|
2089
|
+
/** 实验上下文即使本轮零注入也要留着:工具绑定后同一请求还能补上规则。 */
|
|
2090
|
+
if (!snapshot && !experiment) {
|
|
1556
2091
|
clearPrepared(agent);
|
|
1557
2092
|
return decision;
|
|
1558
2093
|
}
|
|
2094
|
+
let evolutionEpoch = '';
|
|
2095
|
+
try { evolutionEpoch = String(evolutionStatus(scope).epoch || ''); } catch { evolutionEpoch = ''; }
|
|
1559
2096
|
const state = {
|
|
1560
2097
|
text: snapshot ? snapshot.text : '',
|
|
1561
2098
|
experimentText: experiment ? experiment.text : '',
|
|
2099
|
+
experimentParts: experiment && experiment.held ? (experiment.held.experimentParts || null) : null,
|
|
2100
|
+
validUntil: experiment && experiment.held ? (experiment.held.validUntil || '') : '',
|
|
1562
2101
|
cards: snapshot ? snapshot.cards : [],
|
|
1563
2102
|
query,
|
|
1564
2103
|
sessionId,
|
|
1565
2104
|
turn,
|
|
1566
2105
|
signal,
|
|
1567
2106
|
fingerprint,
|
|
2107
|
+
controlEpoch,
|
|
2108
|
+
evolutionEpoch,
|
|
1568
2109
|
at: Date.now(),
|
|
1569
2110
|
};
|
|
1570
2111
|
prepared.set(agent, state);
|
|
@@ -1586,8 +2127,19 @@ export function apply(ctx, config) {
|
|
|
1586
2127
|
|
|
1587
2128
|
/* ---------------- 项目自进化:一次短整理,先扣频次,再走提交门禁 ---------------- */
|
|
1588
2129
|
|
|
1589
|
-
/**
|
|
1590
|
-
function
|
|
2130
|
+
/** 只清不属于当前这批人类消息的旧候选:新一轮开始后,上一轮没整理完的反馈不能再落盘。 */
|
|
2131
|
+
function dropStaleCandidate(sessionId, batchKey) {
|
|
2132
|
+
const prior = feedbackCandidates.get(sessionId);
|
|
2133
|
+
if (prior && prior.candidate && prior.candidate.batchKey !== batchKey) {
|
|
2134
|
+
feedbackCandidates.delete(sessionId);
|
|
2135
|
+
}
|
|
2136
|
+
}
|
|
2137
|
+
|
|
2138
|
+
/**
|
|
2139
|
+
* 反馈只来自本轮真人消息;初始任务、工具成功、沉默都不算。
|
|
2140
|
+
* 解析目标由整理器完成:不在这里暂停所有已注入规则,避免一次 BGM 纠正打掉开头和字幕。
|
|
2141
|
+
*/
|
|
2142
|
+
function rememberFeedback(scope, sessionId, messages, held, turn) {
|
|
1591
2143
|
let candidate;
|
|
1592
2144
|
try {
|
|
1593
2145
|
candidate = learning.candidateFrom({
|
|
@@ -1597,21 +2149,22 @@ export function apply(ctx, config) {
|
|
|
1597
2149
|
if (!candidate) return null;
|
|
1598
2150
|
candidate.cwd = scope.cwd;
|
|
1599
2151
|
candidate.episodeUpdatedAt = held.episodeUpdatedAt || '';
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
2152
|
+
candidate.batchKey = held.batchKey || '';
|
|
2153
|
+
candidate.turn = String(turn || '');
|
|
2154
|
+
const revision = (feedbackRevisions.get(scope.key) || 0) + 1;
|
|
2155
|
+
feedbackRevisions.set(scope.key, revision);
|
|
2156
|
+
candidate.feedbackRevision = revision;
|
|
2157
|
+
const prior = feedbackCandidates.get(sessionId);
|
|
2158
|
+
feedbackCandidates.set(sessionId, {
|
|
2159
|
+
scope,
|
|
2160
|
+
candidate,
|
|
2161
|
+
held,
|
|
2162
|
+
session: prior && prior.session ? prior.session : null,
|
|
2163
|
+
queued: Boolean(prior && prior.queued),
|
|
2164
|
+
at: Date.now(),
|
|
2165
|
+
});
|
|
1613
2166
|
while (feedbackCandidates.size > 8) feedbackCandidates.delete(feedbackCandidates.keys().next().value);
|
|
1614
|
-
return { candidate
|
|
2167
|
+
return { candidate };
|
|
1615
2168
|
}
|
|
1616
2169
|
|
|
1617
2170
|
/** 只认前台最近一次真实请求的路线;拿不到就宁可这次不整理。 */
|
|
@@ -1695,6 +2248,10 @@ export function apply(ctx, config) {
|
|
|
1695
2248
|
else if (chunk && chunk.type === 'finish' && chunk.reason) finish = chunk.reason;
|
|
1696
2249
|
}
|
|
1697
2250
|
if (sawTool) throw new Error('整理返回了工具调用');
|
|
2251
|
+
/** 运行期间用户又给了新反馈:旧结果不能覆盖新纠正,直接作废,等排队的下一次整理。 */
|
|
2252
|
+
if ((feedbackRevisions.get(scope.key) || 0) !== candidate.feedbackRevision) {
|
|
2253
|
+
throw new Error('反馈已更新,这次整理作废');
|
|
2254
|
+
}
|
|
1698
2255
|
const parsed = learning.parseOutput(text, { finish, refs: input.refs, rules: input.rules, ranges });
|
|
1699
2256
|
if (!parsed.ok) throw new Error(parsed.reason);
|
|
1700
2257
|
const commit = commitLearning(scope, {
|
|
@@ -1737,6 +2294,42 @@ export function apply(ctx, config) {
|
|
|
1737
2294
|
}
|
|
1738
2295
|
}
|
|
1739
2296
|
|
|
2297
|
+
/** 启动一次整理;结束后再看同项目是否还排着一条候选。 */
|
|
2298
|
+
function startOrganizer(scope, session, pending) {
|
|
2299
|
+
const job = { controller: learningAborts.get(scope.key) || null };
|
|
2300
|
+
learningJobs.set(scope.key, job);
|
|
2301
|
+
void organize(scope, session, pending)
|
|
2302
|
+
.catch(() => { /* 整理失败不影响会话 */ })
|
|
2303
|
+
.finally(() => {
|
|
2304
|
+
if (learningJobs.get(scope.key) === job) learningJobs.delete(scope.key);
|
|
2305
|
+
drainOrganizer(scope.key);
|
|
2306
|
+
});
|
|
2307
|
+
}
|
|
2308
|
+
|
|
2309
|
+
/** 彻底删除后中止该层在途整理并丢掉候选:不让旧结果复活已删规则。 */
|
|
2310
|
+
function revokeLearningUse(layerKey) {
|
|
2311
|
+
for (const [scopeKey, controller] of learningAborts) {
|
|
2312
|
+
if (layerKey !== 'global' && scopeKey !== layerKey) continue;
|
|
2313
|
+
try { controller.abort(); } catch { /* already settled */ }
|
|
2314
|
+
}
|
|
2315
|
+
for (const [sessionId, pending] of feedbackCandidates) {
|
|
2316
|
+
if (layerKey !== 'global' && pending?.scope?.key !== layerKey) continue;
|
|
2317
|
+
feedbackCandidates.delete(sessionId);
|
|
2318
|
+
}
|
|
2319
|
+
}
|
|
2320
|
+
|
|
2321
|
+
/** 忙碌时保留一条最新候选,不丢用户刚说的纠正;同一项目仍只跑一个整理。 */
|
|
2322
|
+
function drainOrganizer(scopeKey) {
|
|
2323
|
+
if (learningJobs.has(scopeKey)) return;
|
|
2324
|
+
for (const [sessionId, pending] of feedbackCandidates) {
|
|
2325
|
+
if (!pending || !pending.scope || pending.scope.key !== scopeKey) continue;
|
|
2326
|
+
feedbackCandidates.delete(sessionId);
|
|
2327
|
+
if (!pending.session) return;
|
|
2328
|
+
startOrganizer(pending.scope, pending.session, pending);
|
|
2329
|
+
return;
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
|
|
1740
2333
|
/** 只在正常完成的人类回合后调度,一次一个项目;永不阻塞事件派发。 */
|
|
1741
2334
|
function scheduleOrganizer(session, event) {
|
|
1742
2335
|
const reason = event && event.data ? event.data.reason : null;
|
|
@@ -1745,14 +2338,18 @@ export function apply(ctx, config) {
|
|
|
1745
2338
|
if (!sessionId) return;
|
|
1746
2339
|
const pending = feedbackCandidates.get(sessionId);
|
|
1747
2340
|
if (!pending) return;
|
|
2341
|
+
const eventTurn = event.data && event.data.turn !== undefined ? String(event.data.turn) : '';
|
|
2342
|
+
if (eventTurn && pending.candidate && pending.candidate.turn && eventTurn !== pending.candidate.turn) {
|
|
2343
|
+
feedbackCandidates.delete(sessionId);
|
|
2344
|
+
return;
|
|
2345
|
+
}
|
|
2346
|
+
pending.session = session;
|
|
2347
|
+
if (learningJobs.has(pending.scope.key)) {
|
|
2348
|
+
pending.queued = true;
|
|
2349
|
+
return;
|
|
2350
|
+
}
|
|
1748
2351
|
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); });
|
|
2352
|
+
startOrganizer(pending.scope, session, pending);
|
|
1756
2353
|
}
|
|
1757
2354
|
|
|
1758
2355
|
if (typeof ctx.on === 'function') ctx.on('agent/disposed', (payload) => {
|
|
@@ -1773,6 +2370,11 @@ export function apply(ctx, config) {
|
|
|
1773
2370
|
if (!signal) return;
|
|
1774
2371
|
const agent = signalAgents.get(signal);
|
|
1775
2372
|
if (agent) clearPrepared(agent);
|
|
2373
|
+
/** 被取消或被中断的回合不保留反馈候选,下一句「谢谢」不能把没说完的纠正写进去。 */
|
|
2374
|
+
if (signal.aborted === true && agent) {
|
|
2375
|
+
const sessionId = String(agent?.session?.id ?? '');
|
|
2376
|
+
if (sessionId) feedbackCandidates.delete(sessionId);
|
|
2377
|
+
}
|
|
1776
2378
|
signalAgents.delete(signal);
|
|
1777
2379
|
});
|
|
1778
2380
|
|