dsh-lite-memory 0.17.0 → 0.18.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +15 -15
- package/TRIGGERS.md +1 -1
- package/client.js +343 -44
- package/docs/BENCH.md +10 -8
- package/docs/RELEASE-0.18.0.md +35 -0
- package/docs/RELEASE-0.18.1.md +18 -0
- package/docs/RELEASE-0.18.10.md +25 -0
- package/docs/RELEASE-0.18.11.md +48 -0
- package/docs/RELEASE-0.18.12.md +27 -0
- package/docs/RELEASE-0.18.13.md +13 -0
- package/docs/RELEASE-0.18.14.md +13 -0
- package/docs/RELEASE-0.18.15.md +5 -0
- package/docs/RELEASE-0.18.2.md +21 -0
- package/docs/RELEASE-0.18.3.md +16 -0
- package/docs/RELEASE-0.18.4.md +15 -0
- package/docs/RELEASE-0.18.5.md +21 -0
- package/docs/RELEASE-0.18.6.md +18 -0
- package/docs/RELEASE-0.18.7.md +23 -0
- package/docs/RELEASE-0.18.8.md +18 -0
- package/docs/RELEASE-0.18.9.md +24 -0
- package/index.js +403 -131
- package/learning.js +65 -6
- package/package.json +7 -4
- package/state.js +56 -8
- package/store.js +361 -33
- package/test/client.test.mjs +79 -17
- package/test/contract.test.mjs +231 -93
- package/test/control.test.mjs +2 -2
- package/test/crossing.test.mjs +22 -1
- package/test/eval-seed.test.mjs +113 -0
- package/test/evolution-read.test.mjs +33 -10
- package/test/ha.test.mjs +186 -0
- package/test/layer-read.test.mjs +293 -0
- package/test/learning.test.mjs +80 -0
- package/test/palace.test.mjs +2 -0
- package/test/recall.test.mjs +80 -9
- package/test/record.test.mjs +130 -0
- package/test/safety.test.mjs +119 -0
- package/test/state.test.mjs +57 -1
- package/test/store.test.mjs +49 -5
- package/test/view.test.mjs +3 -0
- package/view.js +97 -13
package/index.js
CHANGED
|
@@ -8,6 +8,8 @@ import {
|
|
|
8
8
|
forget,
|
|
9
9
|
undoChange,
|
|
10
10
|
locate,
|
|
11
|
+
layersFor,
|
|
12
|
+
runtimeLayers,
|
|
11
13
|
findAnywhere,
|
|
12
14
|
renderIndex,
|
|
13
15
|
loadIndex,
|
|
@@ -22,6 +24,8 @@ import {
|
|
|
22
24
|
recordCitations,
|
|
23
25
|
listCitations,
|
|
24
26
|
readCitationBody,
|
|
27
|
+
readCitationBodyByTitle,
|
|
28
|
+
locateByTitle,
|
|
25
29
|
evolutionStatus,
|
|
26
30
|
controlStatus,
|
|
27
31
|
setMemoryEnabled,
|
|
@@ -88,6 +92,17 @@ function memoryEnabled(scope) {
|
|
|
88
92
|
}
|
|
89
93
|
}
|
|
90
94
|
|
|
95
|
+
/** 关着的原因决定下一步:恢复出来的状态要用户重新开启,而不是让人以为是自己关的。 */
|
|
96
|
+
function memoryOffCopy(scope) {
|
|
97
|
+
try {
|
|
98
|
+
return controlStatus(scope).recoveryPending
|
|
99
|
+
? '本项目记忆刚从损坏里恢复,需要你重新开启'
|
|
100
|
+
: '本项目记忆已关闭,先在面板打开';
|
|
101
|
+
} catch {
|
|
102
|
+
return '本项目记忆已关闭,先在面板打开';
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
91
106
|
function memoryEnabledFor(execution) {
|
|
92
107
|
return memoryEnabled(scopeOf(cwdOf(execution)));
|
|
93
108
|
}
|
|
@@ -136,6 +151,28 @@ function readJsonBody(req, limit = 8192) {
|
|
|
136
151
|
});
|
|
137
152
|
}
|
|
138
153
|
|
|
154
|
+
/** 每个会话最近一次自动召回的判断依据;只留判断,不留正文。 */
|
|
155
|
+
const recallReceipts = new Map();
|
|
156
|
+
|
|
157
|
+
function noteRecall(scope, sessionId, receipt) {
|
|
158
|
+
if (!scope || typeof sessionId !== 'string' || !sessionId) return;
|
|
159
|
+
recallReceipts.set(`${scope.key}\u0000${sessionId}`, {
|
|
160
|
+
at: new Date().toISOString(),
|
|
161
|
+
reason: String(receipt.reason || ''),
|
|
162
|
+
matched: Number(receipt.matched) || 0,
|
|
163
|
+
cards: Number(receipt.cards) || 0,
|
|
164
|
+
chars: Number(receipt.chars) || 0,
|
|
165
|
+
limit: view.CARD.max,
|
|
166
|
+
bytes: view.CARD.bytes,
|
|
167
|
+
});
|
|
168
|
+
while (recallReceipts.size > 64) recallReceipts.delete(recallReceipts.keys().next().value);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function recallReceiptOf(scope, sessionId) {
|
|
172
|
+
if (!scope || typeof sessionId !== 'string' || !sessionId) return null;
|
|
173
|
+
return recallReceipts.get(`${scope.key}\u0000${sessionId}`) || null;
|
|
174
|
+
}
|
|
175
|
+
|
|
139
176
|
/**
|
|
140
177
|
* 桌面壳与本地 Web 是同一个界面:认 host 自己的 origin,也认 app 内建协议。
|
|
141
178
|
*/
|
|
@@ -162,6 +199,7 @@ function keepHandler(webServer, cwdForSession) {
|
|
|
162
199
|
: pathname === REMOTE_PATH + '/keep' ? 'keep'
|
|
163
200
|
: pathname === REMOTE_PATH + '/citations' ? 'citations'
|
|
164
201
|
: pathname === REMOTE_PATH + '/body' ? 'body'
|
|
202
|
+
: pathname === REMOTE_PATH + '/entry' ? 'entry'
|
|
165
203
|
: pathname === REMOTE_PATH + '/move' ? 'move'
|
|
166
204
|
: pathname === REMOTE_PATH + '/overview' ? 'overview'
|
|
167
205
|
: pathname === REMOTE_PATH + '/control' ? 'control'
|
|
@@ -181,10 +219,18 @@ function keepHandler(webServer, cwdForSession) {
|
|
|
181
219
|
*/
|
|
182
220
|
if (action === 'body') {
|
|
183
221
|
const id = typeof body.id === 'string' ? body.id : '';
|
|
184
|
-
|
|
222
|
+
/** 界面只有标题:按标题照样能读,id 留着兼容旧引用文件。 */
|
|
223
|
+
const titleRef = typeof body.title === 'string' ? body.title.trim() : '';
|
|
224
|
+
const revisionRef = Number.isInteger(body.revision) ? body.revision : undefined;
|
|
225
|
+
if (!id && !titleRef) return sendJson(res, 400, { ok: false, reason: '缺少标题' });
|
|
226
|
+
/** 找不到时只回调用方给的那个句柄:按 id 问就回 id,按标题问就回标题。 */
|
|
227
|
+
const goneReply = () => sendJson(res, 200, titleRef
|
|
228
|
+
? { ok: true, status: 'gone', title: titleRef }
|
|
229
|
+
: { ok: true, status: 'gone', id });
|
|
185
230
|
const sessionId = typeof body.sessionId === 'string' ? body.sessionId : '';
|
|
186
231
|
const cwd = typeof body.cwd === 'string' ? body.cwd : '';
|
|
187
232
|
const scopeKey = typeof body.scopeKey === 'string' ? body.scopeKey : '';
|
|
233
|
+
const scopeRef = body.scope === 'global' || body.scope === 'project' ? body.scope : '';
|
|
188
234
|
let found;
|
|
189
235
|
if (scopeKey) {
|
|
190
236
|
/**
|
|
@@ -197,8 +243,9 @@ function keepHandler(webServer, cwdForSession) {
|
|
|
197
243
|
: listed ? { key: listed.key, dir: listed.dir, cwd: listed.cwd || '', requestedCwd: listed.cwd || '', rootSource: 'configured', confirmed: true }
|
|
198
244
|
: null;
|
|
199
245
|
if (!target) return sendJson(res, 404, { ok: false, reason: '没有这个项目' });
|
|
200
|
-
const located = locate(target, id, { readOnly: true })
|
|
201
|
-
|
|
246
|
+
const located = id ? locate(target, id, { readOnly: true })
|
|
247
|
+
: locateByTitle(target, titleRef, { readOnly: true, revision: revisionRef });
|
|
248
|
+
if (!located) return goneReply();
|
|
202
249
|
found = {
|
|
203
250
|
...located.record,
|
|
204
251
|
scope: located.scope.key === 'global' ? 'global' : 'project',
|
|
@@ -207,15 +254,17 @@ function keepHandler(webServer, cwdForSession) {
|
|
|
207
254
|
};
|
|
208
255
|
} else if (sessionId) {
|
|
209
256
|
try {
|
|
210
|
-
found = readCitationBody(sessionId, id)
|
|
257
|
+
found = id ? readCitationBody(sessionId, id)
|
|
258
|
+
: readCitationBodyByTitle(sessionId, titleRef, { revision: revisionRef, scope: scopeRef });
|
|
211
259
|
} catch {
|
|
212
260
|
found = null;
|
|
213
261
|
}
|
|
214
262
|
/** 不在本会话引用里就当已消失:不给跨会话探正文,界面也只显示「已删除」。 */
|
|
215
|
-
if (!found) return
|
|
263
|
+
if (!found) return goneReply();
|
|
216
264
|
} else if (cwd) {
|
|
217
|
-
const located = locate(scopeOf(cwd), id, { readOnly: true })
|
|
218
|
-
|
|
265
|
+
const located = id ? locate(scopeOf(cwd), id, { readOnly: true })
|
|
266
|
+
: locateByTitle(scopeOf(cwd), titleRef, { readOnly: true, revision: revisionRef });
|
|
267
|
+
if (!located) return goneReply();
|
|
219
268
|
found = {
|
|
220
269
|
...located.record,
|
|
221
270
|
scope: located.scope.key === 'global' ? 'global' : 'project',
|
|
@@ -225,7 +274,7 @@ function keepHandler(webServer, cwdForSession) {
|
|
|
225
274
|
} else {
|
|
226
275
|
return sendJson(res, 400, { ok: false, reason: '缺少来源' });
|
|
227
276
|
}
|
|
228
|
-
if (!found) return
|
|
277
|
+
if (!found) return goneReply();
|
|
229
278
|
/** 面板要分开「当前规范」和旧版本;历史只作可折叠对照,不拼进正文。 */
|
|
230
279
|
const versions = Array.isArray(found.versions) ? found.versions
|
|
231
280
|
: Array.isArray(found.history) ? found.history : [];
|
|
@@ -235,7 +284,6 @@ function keepHandler(webServer, cwdForSession) {
|
|
|
235
284
|
: enums.has(learning.lastStatus) ? learning.lastStatus : '';
|
|
236
285
|
return sendJson(res, 200, {
|
|
237
286
|
ok: true,
|
|
238
|
-
id: found.id,
|
|
239
287
|
status: found.status,
|
|
240
288
|
title: found.title,
|
|
241
289
|
when: found.when || '',
|
|
@@ -253,6 +301,52 @@ function keepHandler(webServer, cwdForSession) {
|
|
|
253
301
|
});
|
|
254
302
|
}
|
|
255
303
|
|
|
304
|
+
/**
|
|
305
|
+
* 面板改一条普通记忆:只改当前项目里的标题与正文,别层只读。
|
|
306
|
+
* 必须带读到的版本,写前再对一次;中间被别人改过就拒绝,不覆盖。
|
|
307
|
+
*/
|
|
308
|
+
if (action === 'entry') {
|
|
309
|
+
const edit = typeof body.edit === 'string' ? body.edit : '';
|
|
310
|
+
if (edit && edit !== 'save') return sendJson(res, 400, { ok: false, reason: '不认识这个操作' });
|
|
311
|
+
const sessionId = typeof body.sessionId === 'string' ? body.sessionId : '';
|
|
312
|
+
if (!sessionId) return sendJson(res, 400, { ok: false, reason: '缺少 sessionId' });
|
|
313
|
+
const cwd = typeof cwdForSession === 'function' ? cwdForSession(sessionId) : '';
|
|
314
|
+
if (!cwd) return sendJson(res, 409, { ok: false, reason: '这个会话还没有项目目录' });
|
|
315
|
+
const scope = scopeOf(cwd);
|
|
316
|
+
if (!scope.confirmed) return sendJson(res, 409, { ok: false, reason: '项目根还没确认,先重新打开这个项目' });
|
|
317
|
+
const scopeKey = typeof body.scopeKey === 'string' ? body.scopeKey : '';
|
|
318
|
+
if (scopeKey && scopeKey !== scope.key) {
|
|
319
|
+
return sendJson(res, 409, { ok: false, reason: '别的项目只读,回到那个项目里再改' });
|
|
320
|
+
}
|
|
321
|
+
if (!memoryEnabled(scope)) return sendJson(res, 409, { ok: false, code: 'memory_disabled', reason: '本项目记忆已关闭' });
|
|
322
|
+
const id = typeof body.id === 'string' ? body.id : '';
|
|
323
|
+
const title = typeof body.title === 'string' ? body.title : '';
|
|
324
|
+
const text = typeof body.body === 'string' ? body.body : '';
|
|
325
|
+
const expectedRevision = Number(body.expectedRevision);
|
|
326
|
+
if (!id) return sendJson(res, 400, { ok: false, reason: '缺少这条记忆' });
|
|
327
|
+
if (!Number.isInteger(expectedRevision) || expectedRevision < 1) {
|
|
328
|
+
return sendJson(res, 409, { ok: false, reason: '这条记忆的版本不清楚,先刷新再改' });
|
|
329
|
+
}
|
|
330
|
+
if (!title.trim()) return sendJson(res, 400, { ok: false, reason: '标题不能为空' });
|
|
331
|
+
try {
|
|
332
|
+
const result = saveEntry(scope, { id, title, body: text, expectedRevision, scope: 'project', source: 'user_explicit' });
|
|
333
|
+
if (!result || !result.record) {
|
|
334
|
+
const reason = result && result.reasonCode === 'memory_disabled' ? '本项目记忆已关闭'
|
|
335
|
+
: result && result.needsResolution ? '改动和另一条太像,先去那条上改'
|
|
336
|
+
: '这次没有改动';
|
|
337
|
+
return sendJson(res, 409, { ok: false, reason });
|
|
338
|
+
}
|
|
339
|
+
return sendJson(res, 200, { ok: true, title: result.record.title, revision: result.record.revision, changed: result.noop !== true });
|
|
340
|
+
} catch (error) {
|
|
341
|
+
const conflict = error && error.code === 'conflict';
|
|
342
|
+
return sendJson(res, conflict ? 409 : 400, {
|
|
343
|
+
ok: false,
|
|
344
|
+
retry: conflict,
|
|
345
|
+
reason: error instanceof Error ? error.message : '这次没有改动',
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
|
|
256
350
|
/**
|
|
257
351
|
* 面板总览:当前会话项目、可切换的项目层与全局层,都从服务端枚举。
|
|
258
352
|
* 只读;正文不进列表,点开单条再按 scopeKey 取。
|
|
@@ -400,7 +494,7 @@ function keepHandler(webServer, cwdForSession) {
|
|
|
400
494
|
})();
|
|
401
495
|
return sendJson(res, 200, {
|
|
402
496
|
ok: true,
|
|
403
|
-
current: { key: scope.key, label: labelOf(scope), control: currentControl, evolution: currentEvolution },
|
|
497
|
+
current: { key: scope.key, label: labelOf(scope), control: currentControl, evolution: currentEvolution, recall: recallReceiptOf(scope, sessionId) },
|
|
404
498
|
scopes,
|
|
405
499
|
selected: {
|
|
406
500
|
key: selectedScope.key,
|
|
@@ -575,7 +669,7 @@ function keepHandler(webServer, cwdForSession) {
|
|
|
575
669
|
|
|
576
670
|
/** 总开关关闭时面板只能看:保留、拒绝、搬移、撤销都不落盘。 */
|
|
577
671
|
if (!controlStatus(scope).enabled) {
|
|
578
|
-
return sendJson(res, 409, { ok: false, reason:
|
|
672
|
+
return sendJson(res, 409, { ok: false, reason: memoryOffCopy(scope), code: 'memory_disabled' });
|
|
579
673
|
}
|
|
580
674
|
|
|
581
675
|
if (action === 'move' || action === 'resume-move') {
|
|
@@ -596,16 +690,19 @@ function keepHandler(webServer, cwdForSession) {
|
|
|
596
690
|
}
|
|
597
691
|
|
|
598
692
|
if (action === 'undo') {
|
|
599
|
-
|
|
600
|
-
if (!
|
|
693
|
+
const undoTitle = typeof body.title === 'string' ? body.title.trim() : '';
|
|
694
|
+
if (!id && !undoTitle) return sendJson(res, 400, { ok: false, reason: '缺少标题' });
|
|
695
|
+
const located = id ? locate(scope, id)
|
|
696
|
+
: locateByTitle(scope, undoTitle, { revision: Number.isInteger(body.revision) ? body.revision : undefined });
|
|
697
|
+
if (!located) return sendJson(res, 404, { ok: false, reason: '没有这条记忆' });
|
|
601
698
|
/** 撤销是精确动作:必须带当时看到的版本;缺版本就不猜,免得撤掉别人后来的修改。 */
|
|
602
699
|
if (!Number.isInteger(body.revision) || body.revision < 1) {
|
|
603
700
|
return sendJson(res, 400, { ok: false, reason: '缺少版本' });
|
|
604
701
|
}
|
|
605
702
|
try {
|
|
606
|
-
const undone = undoChange(scope, id, { expectedRevision: body.revision });
|
|
703
|
+
const undone = undoChange(scope, located.record.id, { expectedRevision: body.revision });
|
|
607
704
|
if (!undone) return sendJson(res, 404, { ok: false, reason: '没有这条记忆' });
|
|
608
|
-
return sendJson(res, 200, { ok: true,
|
|
705
|
+
return sendJson(res, 200, { ok: true, title: undone.title, revision: undone.revision, mode: undone.mode });
|
|
609
706
|
} catch (error) {
|
|
610
707
|
return sendJson(res, 409, { ok: false, reason: error instanceof Error ? error.message : '撤销失败' });
|
|
611
708
|
}
|
|
@@ -635,7 +732,6 @@ function keepHandler(webServer, cwdForSession) {
|
|
|
635
732
|
/** 给界面的事件引用:只有定位与版次,不带正文,避免把维护字段当内容画出来。 */
|
|
636
733
|
function refsOf(hits) {
|
|
637
734
|
return hits.map((item) => ({
|
|
638
|
-
id: item.id,
|
|
639
735
|
title: item.title || '',
|
|
640
736
|
scope: item.scope || 'project',
|
|
641
737
|
revision: item.revision || 1,
|
|
@@ -644,19 +740,25 @@ function refsOf(hits) {
|
|
|
644
740
|
/** 界面要按类型分开展示(偏好/约定/流程/资料),维护字段不进正文。 */
|
|
645
741
|
kind: typeof item.kind === 'string' && item.kind ? item.kind : 'note',
|
|
646
742
|
when: typeof item.when === 'string' ? item.when : '',
|
|
743
|
+
/** 界面要能说「N 天前更新」,只给时间不给正文。 */
|
|
744
|
+
updated: typeof item.updated === 'string' ? item.updated : '',
|
|
647
745
|
}));
|
|
648
746
|
}
|
|
649
747
|
|
|
650
748
|
const SAVE_PARAMS = {
|
|
651
749
|
type: 'object',
|
|
652
750
|
properties: {
|
|
653
|
-
|
|
751
|
+
updateTitle: { type: 'string', description: '要更新的那条记忆的标题(原样抄搜索结果里的标题)。同一件事更新时用它,别另建一条' },
|
|
654
752
|
title: { type: 'string', description: `标题:名词短语,用户一眼能认出是哪件事,不超过 ${LIMITS.titleChars} 字。它就是这条记忆的身份` },
|
|
655
753
|
summary: { type: 'string', description: `一句话说清要记的内容本身——用户只看这一行决定留不留,名词短语最好,超长会自动截到这个长度` },
|
|
656
754
|
body: { type: 'string', description: `正文 markdown,整段替换旧正文,不超过 ${LIMITS.entryBytes} 字节。只写原则、判断规则、触发条件和边界;不写一次性数字或代码片段,这些查当前代码或文档。` },
|
|
657
755
|
when: { type: 'string', description: `前置条件或适用场景(「在哪台机器 / 哪个环境 / 什么情况下」),不超过 ${LIMITS.whenChars} 字。同一标题但前置条件不同,会存成两条,不合并;更新时不给就保留原值` },
|
|
658
756
|
kind: { type: 'string', enum: ['preference', 'convention', 'procedure', 'pointer', 'pitfall', 'credential_hint', 'note'], description: '条目类型;更新时不给就保留原值' },
|
|
659
757
|
tags: { type: 'array', items: { type: 'string' }, description: '最多 5 个标签;更新时不给就保留原值' },
|
|
758
|
+
supersedes: {
|
|
759
|
+
type: 'string',
|
|
760
|
+
description: '要取代的那条记忆的标题(原样抄搜索结果里的标题):新内容写成新条目,旧条目标为已被取代并保留在历史里;同层才有效,已被取代过的不能再取代',
|
|
761
|
+
},
|
|
660
762
|
resolution: {
|
|
661
763
|
type: 'string',
|
|
662
764
|
enum: ['distinct'],
|
|
@@ -668,13 +770,12 @@ const SAVE_PARAMS = {
|
|
|
668
770
|
evidenceRef: { type: 'string', description: '归属判定的依据(哪条消息、哪个文件、哪次观察);用户没说范围时用来判断能不能存' },
|
|
669
771
|
sameTopicAs: {
|
|
670
772
|
type: 'object',
|
|
671
|
-
description: '显式绑定到已有主题槽位:{scope,
|
|
773
|
+
description: '显式绑定到已有主题槽位:{scope, title}。标题对不上就报错,不写',
|
|
672
774
|
properties: {
|
|
673
775
|
scope: { type: 'string', enum: ['project', 'global'] },
|
|
674
|
-
|
|
675
|
-
revision: { type: 'integer' },
|
|
776
|
+
title: { type: 'string' },
|
|
676
777
|
},
|
|
677
|
-
required: ['scope', '
|
|
778
|
+
required: ['scope', 'title'],
|
|
678
779
|
additionalProperties: false,
|
|
679
780
|
},
|
|
680
781
|
mode: {
|
|
@@ -718,9 +819,9 @@ const SAVE_PARAMS = {
|
|
|
718
819
|
type: 'object',
|
|
719
820
|
properties: {
|
|
720
821
|
type: { type: 'string', enum: view.LINK_TYPES, description: '默认 related' },
|
|
721
|
-
|
|
822
|
+
title: { type: 'string', description: '被关联那条记忆的标题(原样抄搜索结果里的标题)' },
|
|
722
823
|
},
|
|
723
|
-
required: ['
|
|
824
|
+
required: ['title'],
|
|
724
825
|
additionalProperties: false,
|
|
725
826
|
},
|
|
726
827
|
},
|
|
@@ -737,7 +838,7 @@ const SAVE_PARAMS = {
|
|
|
737
838
|
|
|
738
839
|
const SUGGEST_PARAMS = {
|
|
739
840
|
...SAVE_PARAMS,
|
|
740
|
-
properties: Object.fromEntries(Object.entries(SAVE_PARAMS.properties).filter(([key]) => !['scope', 'id', 'resolution', 'memoryClass', 'durability', 'evidenceRef', 'sameTopicAs'].includes(key))),
|
|
841
|
+
properties: Object.fromEntries(Object.entries(SAVE_PARAMS.properties).filter(([key]) => !['scope', 'id', 'updateTitle', 'resolution', 'memoryClass', 'durability', 'evidenceRef', 'sameTopicAs'].includes(key))),
|
|
741
842
|
};
|
|
742
843
|
|
|
743
844
|
const SEARCH_PARAMS = {
|
|
@@ -756,28 +857,69 @@ const SEARCH_PARAMS = {
|
|
|
756
857
|
const READ_PARAMS = {
|
|
757
858
|
type: 'object',
|
|
758
859
|
properties: {
|
|
759
|
-
|
|
860
|
+
title: { type: 'string', description: '记忆标题,来自搜索结果或召回卡' },
|
|
760
861
|
view: { type: 'string', enum: ['full', 'links'], description: '默认 full 读正文;links 只看直接相关的记忆清单,不取正文' },
|
|
862
|
+
scope: { type: 'string', enum: ['project', 'global'], description: '只在指定层解析标题;默认先当前项目再全局' },
|
|
761
863
|
contextId: { type: 'string', description: '上一轮自动索引或工具结果给出的任务上下文 id;读实验规则时带上' },
|
|
762
864
|
},
|
|
763
|
-
required: ['id'],
|
|
764
865
|
additionalProperties: false,
|
|
765
866
|
};
|
|
766
867
|
|
|
767
868
|
const FORGET_PARAMS = {
|
|
768
869
|
type: 'object',
|
|
769
870
|
properties: {
|
|
770
|
-
|
|
871
|
+
title: { type: 'string', description: '要忘掉的那条记忆的标题' },
|
|
872
|
+
scope: { type: 'string', enum: ['project', 'global'], description: '只在指定层解析标题;默认先当前项目再全局' },
|
|
771
873
|
mode: {
|
|
772
874
|
type: 'string',
|
|
773
875
|
enum: ['archive', 'purge'],
|
|
774
876
|
description: 'archive 移入归档可找回(只是不想再提);purge 彻底忘掉,正文与受管副本清除且不可复活。没点明是哪种就两种都别做,先问用户',
|
|
775
877
|
},
|
|
776
878
|
},
|
|
777
|
-
required: ['id'],
|
|
778
879
|
additionalProperties: false,
|
|
779
880
|
};
|
|
780
881
|
|
|
882
|
+
/** 对外只认标题:id 留作内部地址,模型和界面都不用碰。 */
|
|
883
|
+
function refIdOf(scope, args, label = '记忆') {
|
|
884
|
+
const id = typeof args.id === 'string' ? args.id.trim() : '';
|
|
885
|
+
if (id) return id;
|
|
886
|
+
const title = typeof args.title === 'string' ? args.title.trim() : '';
|
|
887
|
+
if (!title) throw new Error(`要读或要忘一条${label},先给标题`);
|
|
888
|
+
/** 显式指定层时只解析那一层:同名条目不会被顺手改掉,项目层也不会命中全局。 */
|
|
889
|
+
const explicit = args && args.scope === 'global' ? globalScope() : args && args.scope === 'project' ? scope : null;
|
|
890
|
+
const sources = explicit ? [explicit] : runtimeLayers(scope);
|
|
891
|
+
/** 运行时只解析当前项目与全局:别的项目正文不进本会话。 */
|
|
892
|
+
for (const target of sources) {
|
|
893
|
+
let index;
|
|
894
|
+
try { index = loadIndex(target); } catch { continue; }
|
|
895
|
+
const hit = (index.entries || []).find((entry) => entry && entry.status !== 'archived'
|
|
896
|
+
&& String(entry.title || '').trim() === title);
|
|
897
|
+
if (hit) return hit.id;
|
|
898
|
+
}
|
|
899
|
+
/** 忘掉已归档的条目也要能按标题解析到;归档不会因此重新参与召回。 */
|
|
900
|
+
for (const target of sources) {
|
|
901
|
+
let index;
|
|
902
|
+
try { index = loadIndex(target); } catch { continue; }
|
|
903
|
+
const hit = (index.archived || []).find((entry) => entry
|
|
904
|
+
&& String(entry.title || '').trim() === title);
|
|
905
|
+
if (hit) return hit.id;
|
|
906
|
+
}
|
|
907
|
+
throw new Error(`没有「${title}」这条记忆;先用 memory_search 对一下标题`);
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
/** 同名条目可能分布在项目层和全局层:更新前先报候选,不能先匹配。 */
|
|
911
|
+
function titleCandidates(scope, title) {
|
|
912
|
+
const out = [];
|
|
913
|
+
for (const target of runtimeLayers(scope)) {
|
|
914
|
+
let index;
|
|
915
|
+
try { index = loadIndex(target); } catch { continue; }
|
|
916
|
+
const hit = (index.entries || []).find((entry) => entry && entry.status !== 'archived'
|
|
917
|
+
&& String(entry.title || '').trim() === title);
|
|
918
|
+
if (hit) out.push({ id: hit.id, scope: target.key === 'global' ? 'global' : 'project' });
|
|
919
|
+
}
|
|
920
|
+
return out;
|
|
921
|
+
}
|
|
922
|
+
|
|
781
923
|
/** 记忆归属标记:全局 `[global]`,项目用项目目录名(如 `[DSH]`)。 */
|
|
782
924
|
function projectNameOf(scope) {
|
|
783
925
|
const base = scope && scope.cwd ? path.basename(scope.cwd) : '';
|
|
@@ -795,6 +937,29 @@ function withTag(value, text) {
|
|
|
795
937
|
return tag ? `${tag} ${text}` : text;
|
|
796
938
|
}
|
|
797
939
|
|
|
940
|
+
/** 候选要能自己解释「什么时候的、为什么像」,模型才好判断是更新还是另一条。 */
|
|
941
|
+
function humanAge(iso) {
|
|
942
|
+
const at = Date.parse(String(iso || ''));
|
|
943
|
+
if (!Number.isFinite(at)) return '';
|
|
944
|
+
const days = Math.floor((Date.now() - at) / 86400000);
|
|
945
|
+
if (days <= 0) return '今天';
|
|
946
|
+
if (days === 1) return '昨天';
|
|
947
|
+
if (days < 30) return `${days} 天前`;
|
|
948
|
+
if (days < 365) return `${Math.floor(days / 30)} 个月前`;
|
|
949
|
+
return `${Math.floor(days / 365)} 年前`;
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
function describeCandidate(item, options = {}) {
|
|
953
|
+
const age = humanAge(item.updated);
|
|
954
|
+
const why = options.sameTopic ? '' : item.reason === 'similar_summary' ? '摘要相近' : '标题相近';
|
|
955
|
+
const parts = [`「${item.title}」`];
|
|
956
|
+
if (age) parts.push(`${age}更新`);
|
|
957
|
+
if (item.when) parts.push(item.when);
|
|
958
|
+
if (why) parts.push(why);
|
|
959
|
+
if (item.summary) parts.push(item.summary);
|
|
960
|
+
return parts.join(' · ');
|
|
961
|
+
}
|
|
962
|
+
|
|
798
963
|
function statusLabel(status) {
|
|
799
964
|
if (status === 'archived') return ' · 已归档';
|
|
800
965
|
if (status === 'needs_context') return ' · 适用范围待确认';
|
|
@@ -811,12 +976,12 @@ function statusLabel(status) {
|
|
|
811
976
|
export const RECALL_MODES = ['adaptive', 'tool_only', 'off'];
|
|
812
977
|
const RECALL_QUERY_CHARS = 400;
|
|
813
978
|
const RECALL_DEADLINE_MS = 150;
|
|
814
|
-
const RECALL_HEADER = '已有记忆可能相关(需要细节用 memory_read
|
|
979
|
+
const RECALL_HEADER = '已有记忆可能相关(需要细节用 memory_read 带标题取正文,别凭标题猜):';
|
|
815
980
|
|
|
816
981
|
/** 卡片只放够判断要不要读的几行;必要前提整条在场,装不下就整卡省略。 */
|
|
817
982
|
function renderRecallCard(card) {
|
|
818
983
|
const tag = card.scope === 'global' ? '[global]' : card.scopeName ? `[${card.scopeName}]` : '';
|
|
819
|
-
const parts = [`- ${tag ? `${tag} ` : ''}「${card.title}
|
|
984
|
+
const parts = [`- ${tag ? `${tag} ` : ''}「${card.title}」`];
|
|
820
985
|
if (card.summary) parts.push(`|${card.summary}`);
|
|
821
986
|
if (card.conditions && card.conditions.length) parts.push(`|前提:${card.conditions.join('、')}`);
|
|
822
987
|
if (card.selection) parts.push(`|只适用于 ${card.selection}`);
|
|
@@ -907,6 +1072,27 @@ export function apply(ctx, config) {
|
|
|
907
1072
|
return win.bytes + bytes <= view.REQUEST_BYTES;
|
|
908
1073
|
}
|
|
909
1074
|
|
|
1075
|
+
/**
|
|
1076
|
+
* 工具自己取的记忆另算一笔:自动注入已经用满时,模型按标题来读正文不该被连带挡掉,
|
|
1077
|
+
* 否则用户看到的就是「额度已用完」这种没法处理的死路。
|
|
1078
|
+
*/
|
|
1079
|
+
function affordableTool(win, bytes, bodies = 0) {
|
|
1080
|
+
if (!win) return true;
|
|
1081
|
+
if (win.reads >= view.REQUEST_LIMITS.reads) return false;
|
|
1082
|
+
if (bodies && win.bodies + bodies > view.REQUEST_LIMITS.bodies) return false;
|
|
1083
|
+
return (win.toolBytes || 0) + bytes <= view.TOOL_BYTES;
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
/** 续写轮没有新消息:沿用上一次能核对的请求上下文。 */
|
|
1087
|
+
const lastRuntime = new Map();
|
|
1088
|
+
const rememberRuntime = (scope, sessionId, held) => {
|
|
1089
|
+
if (!held || !sessionId) return;
|
|
1090
|
+
const key = `${scope.key}\u0000${sessionId}`;
|
|
1091
|
+
lastRuntime.delete(key);
|
|
1092
|
+
lastRuntime.set(key, held);
|
|
1093
|
+
if (lastRuntime.size > 20) lastRuntime.delete(lastRuntime.keys().next().value);
|
|
1094
|
+
};
|
|
1095
|
+
|
|
910
1096
|
function openWindow(agent, { sessionId, turn, messageIds }) {
|
|
911
1097
|
const key = agentKey(agent);
|
|
912
1098
|
if (!key) return null;
|
|
@@ -915,7 +1101,7 @@ export function apply(ctx, config) {
|
|
|
915
1101
|
if (existing && existing.signature === signature) return existing;
|
|
916
1102
|
const next = {
|
|
917
1103
|
signature, sessionId, turn, messageIds,
|
|
918
|
-
bytes: 0, bodies: 0, reads: 0,
|
|
1104
|
+
bytes: 0, bodies: 0, reads: 0, toolBytes: 0,
|
|
919
1105
|
provided: new Map(), readBodies: new Map(), at: Date.now(),
|
|
920
1106
|
experiment: null, contexts: new Map(), runtime: null, autoBudget: null,
|
|
921
1107
|
};
|
|
@@ -986,17 +1172,18 @@ export function apply(ctx, config) {
|
|
|
986
1172
|
}
|
|
987
1173
|
}
|
|
988
1174
|
/** 同一槽位只让胜出的规范进去;冲突或条件未知的整组不进自动召回。 */
|
|
1175
|
+
const groups = [...view.resolveTopics(layers.project, layers.global, context).values()];
|
|
989
1176
|
const records = [];
|
|
990
1177
|
const layerOf = new Map();
|
|
991
|
-
for (const group of
|
|
1178
|
+
for (const group of groups) {
|
|
992
1179
|
if (group.status !== 'ok' || !group.record) continue;
|
|
993
1180
|
records.push(group.record);
|
|
994
1181
|
layerOf.set(group.record.id, group.scope === 'global' ? 'global' : 'project');
|
|
995
1182
|
}
|
|
1183
|
+
const gateRuntime = runtime || (win && win.runtime) || null;
|
|
1184
|
+
const task = runtime && runtime.task ? runtime.task : null;
|
|
996
1185
|
const ranked = view.rank(records, query, {
|
|
997
|
-
limit: 8, requireApplicable: true, context,
|
|
998
|
-
runtime: runtime || (win && win.runtime) || null,
|
|
999
|
-
task: runtime && runtime.task ? runtime.task : null,
|
|
1186
|
+
limit: 8, requireApplicable: true, context, runtime: gateRuntime, task,
|
|
1000
1187
|
});
|
|
1001
1188
|
/** 普通记忆与实验规则共用一份自动注入预算:总条数和最终完整文本字节一起算。 */
|
|
1002
1189
|
const auto = win && win.autoBudget ? win.autoBudget : null;
|
|
@@ -1034,7 +1221,7 @@ export function apply(ctx, config) {
|
|
|
1034
1221
|
win.recallHeader = true;
|
|
1035
1222
|
}
|
|
1036
1223
|
lines.push(line);
|
|
1037
|
-
cards.push({
|
|
1224
|
+
cards.push({ revision, scope: card.scope, title: card.title });
|
|
1038
1225
|
}
|
|
1039
1226
|
const finalText = `${RECALL_HEADER}\n${lines.join('\n')}`;
|
|
1040
1227
|
if (auto) {
|
|
@@ -1046,8 +1233,35 @@ export function apply(ctx, config) {
|
|
|
1046
1233
|
experimentCards: auto.experimentCards || 0,
|
|
1047
1234
|
});
|
|
1048
1235
|
}
|
|
1049
|
-
|
|
1050
|
-
|
|
1236
|
+
/** 没卡片也要给出判断依据:面板要能回答「为什么这次没提醒我」。 */
|
|
1237
|
+
let near = [];
|
|
1238
|
+
if (!cards.length && !ranked.length) {
|
|
1239
|
+
/** 自动召回只认完全适用;再放宽一层看有没有字面匹配,好把「条件未确认」和「没有匹配」分开。 */
|
|
1240
|
+
near = view.rank(records, query, {
|
|
1241
|
+
limit: 8, requireApplicable: false, context, runtime: gateRuntime, task,
|
|
1242
|
+
});
|
|
1243
|
+
}
|
|
1244
|
+
let waiting = near.some((row) => row.status !== 'ok' || row.record.reviewState === 'legacy_unreviewed');
|
|
1245
|
+
let nearMatched = near.length;
|
|
1246
|
+
if (!cards.length && !ranked.length) {
|
|
1247
|
+
/** 整组被条件或冲突挡下的候选没进 records,单独按同一查询算;只计数,不投放。 */
|
|
1248
|
+
for (const group of groups) {
|
|
1249
|
+
if (group.status !== 'conflict' && group.status !== 'needs_context') continue;
|
|
1250
|
+
const hits = view.rank(group.records || [], query, {
|
|
1251
|
+
limit: 1, requireApplicable: false, context, runtime: gateRuntime, task,
|
|
1252
|
+
});
|
|
1253
|
+
if (!hits.length) continue;
|
|
1254
|
+
waiting = true;
|
|
1255
|
+
nearMatched += hits.length;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
const reason = cards.length ? 'hit'
|
|
1259
|
+
: ranked.length ? 'budget'
|
|
1260
|
+
: waiting ? 'needs_context'
|
|
1261
|
+
: (gateRuntime && gateRuntime.enabled ? 'no_overlap' : 'needs_context');
|
|
1262
|
+
const matched = ranked.length || nearMatched;
|
|
1263
|
+
if (!cards.length) return { text: '', cards: [], bytes: 0, query, reason, matched };
|
|
1264
|
+
return { text: finalText, cards, bytes: Buffer.byteLength(finalText), query, reason, matched };
|
|
1051
1265
|
}
|
|
1052
1266
|
|
|
1053
1267
|
/* ---------------- 项目自进化:任务锚点、读取决策、上下文解析 ---------------- */
|
|
@@ -1257,8 +1471,8 @@ export function apply(ctx, config) {
|
|
|
1257
1471
|
? `;核对:${String(checkpoint.text).slice(0, 40)}`
|
|
1258
1472
|
: '';
|
|
1259
1473
|
const line = short
|
|
1260
|
-
? `- 「${record.title}」${conditions ? `(${conditions})` : ''}
|
|
1261
|
-
: `- 「${record.title}
|
|
1474
|
+
? `- 「${record.title}」${conditions ? `(${conditions})` : ''}:${body}${check}`
|
|
1475
|
+
: `- 「${record.title}」(用途:${record.summary || record.title};要用时 memory_read 带标题取正文)`;
|
|
1262
1476
|
const size = Buffer.byteLength(`\n${line}`);
|
|
1263
1477
|
if (used + size > autoCap) break;
|
|
1264
1478
|
used += size;
|
|
@@ -1394,11 +1608,13 @@ export function apply(ctx, config) {
|
|
|
1394
1608
|
const contextId = typeof optionalContextId === 'string' ? optionalContextId.trim() : '';
|
|
1395
1609
|
if (!contextId) {
|
|
1396
1610
|
/** 没有显式上下文时,用本请求已经路由出来的任务;这层不依赖自进化开关。 */
|
|
1397
|
-
const
|
|
1611
|
+
const live = win && win.runtime ? win.runtime : null;
|
|
1612
|
+
const runtime = live
|
|
1613
|
+
|| (win && win.sessionId ? lastRuntime.get(`${scope.key}\u0000${win.sessionId}`) : null);
|
|
1398
1614
|
const sameRequest = runtime
|
|
1399
1615
|
&& runtime.projectKey === scope.key
|
|
1400
1616
|
&& (!runtime.sessionId || runtime.sessionId === String(win.sessionId || ''))
|
|
1401
|
-
&& runtime.batchKey === anchor.requestBatchKey(win.messageIds || []);
|
|
1617
|
+
&& (runtime === live ? runtime.batchKey === anchor.requestBatchKey(win.messageIds || []) : true);
|
|
1402
1618
|
if (sameRequest) {
|
|
1403
1619
|
/** 普通记忆可以直接用本请求上下文;实验正文仍要求显式 contextId,不能顺手放行。 */
|
|
1404
1620
|
return { ok: true, scope, win, context: runtime.context, held: runtime, experimental: false };
|
|
@@ -1545,26 +1761,24 @@ export function apply(ctx, config) {
|
|
|
1545
1761
|
|
|
1546
1762
|
ctx.effect(() => tools.register({
|
|
1547
1763
|
name: 'memory_save',
|
|
1548
|
-
description: `保存一条记忆。只在会改变以后判断时才存;先搜已有条目,能更新就不要新建。保存前先蒸馏:标题是结论,正文只写原则与触发条件,通常 2–4 句;路径、字段表、完整命令、版本号、尺寸数值不要抄进正文,改成文档或 skill 的指针。用户明确说「记住」时直接调用;用户自己的话已经是长期规则(「以后都这样」「别再 X
|
|
1764
|
+
description: `保存一条记忆。只在会改变以后判断时才存;先搜已有条目,能更新就不要新建。保存前先蒸馏:标题是结论,正文只写原则与触发条件,通常 2–4 句;路径、字段表、完整命令、版本号、尺寸数值不要抄进正文,改成文档或 skill 的指针。用户明确说「记住」时直接调用;用户自己的话已经是长期规则(「以后都这样」「别再 X」)时也直接调用,不要先问。更新同一件事时用同一个标题:身份只看标题、旧标题和前置条件,像不像不授权覆盖。文本相近但标题不同时先不写,返回 needsResolution 与 1–2 条相近条目:确实是同一条就改成那个标题重存,确实是另一条才带 resolution="distinct" 再存。按标题更新时没给的字段保持原值,正文整段替换,旧正文只留作内部撤回,不展示也不参与检索。条数到硬上限时把同层最久没被读的一条移进归档,结果里写明动了哪条,不拒绝新记忆;单条正文超限只报错、不截断。前置条件不同的同一标题是两条。${NOTES}`,
|
|
1549
1765
|
parameters: SAVE_PARAMS,
|
|
1550
1766
|
output: {
|
|
1551
1767
|
presentationMeta: (_args, value) => ({
|
|
1552
1768
|
kind: 'memory-save',
|
|
1553
|
-
id: value.id || '',
|
|
1554
1769
|
title: value.title || '',
|
|
1555
1770
|
scope: value.scope || '',
|
|
1556
1771
|
revision: value.revision || 0,
|
|
1557
1772
|
changed: value.changed === true,
|
|
1558
1773
|
recordKind: typeof value.kind === 'string' && value.kind ? value.kind : '',
|
|
1559
1774
|
mode: value.needsScope ? 'needs_scope'
|
|
1560
|
-
: value.
|
|
1775
|
+
: value.existingTitle ? 'duplicate'
|
|
1561
1776
|
: value.needsResolution ? 'needs_resolution'
|
|
1562
1777
|
: value.changed === false ? 'unchanged' : value.merged ? 'updated' : 'created',
|
|
1563
1778
|
}),
|
|
1564
1779
|
schema: {
|
|
1565
1780
|
type: 'object',
|
|
1566
1781
|
properties: {
|
|
1567
|
-
id: { type: 'string' },
|
|
1568
1782
|
title: { type: 'string' },
|
|
1569
1783
|
summary: { type: 'string' },
|
|
1570
1784
|
when: { type: 'string' },
|
|
@@ -1579,32 +1793,35 @@ export function apply(ctx, config) {
|
|
|
1579
1793
|
needsResolution: { type: 'boolean' },
|
|
1580
1794
|
needsScope: { type: 'boolean' },
|
|
1581
1795
|
reason: { type: 'string' },
|
|
1582
|
-
|
|
1796
|
+
existingTitle: { type: 'string' },
|
|
1583
1797
|
existingScope: { type: 'string' },
|
|
1584
1798
|
crossLayer: { type: 'boolean' },
|
|
1799
|
+
crossScope: { type: 'string', enum: ['global', 'project'] },
|
|
1585
1800
|
sameTopic: { type: 'boolean' },
|
|
1586
|
-
|
|
1801
|
+
similarTitles: { type: 'string' },
|
|
1587
1802
|
similar: { type: 'string' },
|
|
1803
|
+
supersededTitle: { type: 'string' },
|
|
1588
1804
|
archived: { type: 'string' },
|
|
1805
|
+
pendingConflicts: { type: 'integer' },
|
|
1589
1806
|
},
|
|
1590
|
-
required: ['
|
|
1807
|
+
required: ['title', 'summary', 'when', 'revision', 'merged', 'changed', 'scope', 'needsResolution', 'archived'],
|
|
1591
1808
|
additionalProperties: false,
|
|
1592
1809
|
},
|
|
1593
1810
|
render: (_args, value) => [{
|
|
1594
1811
|
type: 'text',
|
|
1595
1812
|
text: value.needsScope
|
|
1596
1813
|
? value.reason === 'memory_disabled'
|
|
1597
|
-
?
|
|
1814
|
+
? `memory · ${memoryOffCopy(projectScope)}`
|
|
1598
1815
|
: value.reason === 'unconfirmed_project'
|
|
1599
1816
|
? `memory · 当前目录还不能确认属于哪个项目:「${value.title}」\n先确认项目根目录,或显式 scope="global"`
|
|
1600
1817
|
: `memory · 归属还没定:「${value.title}」\n补 memoryClass(personal_preference / project_fact / environment_fact / portable_procedure)或显式 scope;设备事实还要给设备条件`
|
|
1601
|
-
: value.
|
|
1602
|
-
? `memory · 已有相同记忆:「${value.title}
|
|
1818
|
+
: value.existingTitle
|
|
1819
|
+
? `memory · 已有相同记忆:「${value.title}」(${value.existingScope})\n要改成另一条,加 resolution="distinct"`
|
|
1603
1820
|
: value.needsResolution
|
|
1604
|
-
? `memory · 需要确认:「${value.title}」与已有条目相近${value.crossLayer ? '(在另一层)' : ''}${value.sameTopic ? '(同一主题槽位)' : ''}\n${value.similar}\n
|
|
1821
|
+
? `memory · 需要确认:「${value.title}」与已有条目相近${value.crossLayer ? '(在另一层)' : ''}${value.sameTopic ? '(同一主题槽位)' : ''}\n${value.similar}\n要更新它:用同一个标题${value.crossScope === 'global' ? '、并带 scope="global"' : ''}再存一次;确实是另一条,加 resolution="distinct"`
|
|
1605
1822
|
: value.changed === false
|
|
1606
|
-
? `memory · unchanged ${withTag(value, `"${value.title}"`)}
|
|
1607
|
-
: `memory · ${value.merged ? 'merged' : 'saved'} ${withTag(value, `"${value.title}"`)}${value.when ? `(${value.when})` : ''}
|
|
1823
|
+
? `memory · unchanged ${withTag(value, `"${value.title}"`)}`
|
|
1824
|
+
: `memory · ${value.merged ? 'merged' : 'saved'} ${withTag(value, `"${value.title}"`)}${value.when ? `(${value.when})` : ''}${value.similar ? ` · 相近:${value.similar}` : ''}${value.archived ? ` · 已归档「${value.archived}」腾位` : ''}${value.fullTextSaved ? ' · 完整原句已另存' : ''}${value.supersededTitle ? ` · 已取代「${value.supersededTitle}」` : ''}`,
|
|
1608
1825
|
}],
|
|
1609
1826
|
},
|
|
1610
1827
|
execute: async (args, execution) => {
|
|
@@ -1612,15 +1829,44 @@ export function apply(ctx, config) {
|
|
|
1612
1829
|
const projectScopeName = projectNameOf(projectScope);
|
|
1613
1830
|
if (!memoryEnabled(projectScope)) {
|
|
1614
1831
|
return {
|
|
1615
|
-
|
|
1832
|
+
title: String(args.title || ''), summary: '', when: '', revision: 0,
|
|
1616
1833
|
merged: false, changed: false, scope: '', needsScope: true, reason: 'memory_disabled',
|
|
1617
|
-
|
|
1834
|
+
recoveryPending: controlStatus(projectScope).recoveryPending,
|
|
1835
|
+
needsResolution: false, similarTitles: '', similar: '', archived: '',
|
|
1618
1836
|
};
|
|
1619
1837
|
}
|
|
1620
|
-
const
|
|
1838
|
+
const supersedes = String(args.supersedes || '').trim();
|
|
1839
|
+
const supersedesId = supersedes && !/^mem_[a-z0-9]+$/.test(supersedes)
|
|
1840
|
+
? (() => { try { return refIdOf(projectScope, { title: supersedes }, '记忆'); } catch { return ''; } })()
|
|
1841
|
+
: supersedes;
|
|
1842
|
+
const draft = { ...args, writeMode: args.mode || 'patch' };
|
|
1843
|
+
if (supersedes) { if (supersedesId) draft.supersedes = supersedesId; else delete draft.supersedes; }
|
|
1844
|
+
/** 更新句柄也用标题:id 只是底层实现,模型抄标题就行。 */
|
|
1845
|
+
const updateTitle = String(args.updateTitle || '').trim();
|
|
1846
|
+
if (updateTitle) {
|
|
1847
|
+
const candidates = args.scope ? [] : titleCandidates(projectScope, updateTitle);
|
|
1848
|
+
if (candidates.length > 1) {
|
|
1849
|
+
return { error: `memory · 「${updateTitle}」在项目层和全局层各有一条:改全局加 scope="global",改项目加 scope="project"` };
|
|
1850
|
+
}
|
|
1851
|
+
const updateId = (() => { try { return refIdOf(projectScope, { title: updateTitle, scope: args.scope }, '记忆'); } catch { return ''; } })();
|
|
1852
|
+
if (!updateId) return { error: `memory · 没有「${updateTitle}」这条记忆;要新增就不用 updateTitle` };
|
|
1853
|
+
draft.id = updateId;
|
|
1854
|
+
}
|
|
1855
|
+
/** 关联目标同样按标题解析成内部 id 再落盘。 */
|
|
1856
|
+
if (Array.isArray(args.links) && args.links.length) {
|
|
1857
|
+
const resolved = [];
|
|
1858
|
+
for (const link of args.links) {
|
|
1859
|
+
const title = typeof link?.title === 'string' ? link.title.trim() : '';
|
|
1860
|
+
if (!title) return { error: 'memory · 关联要写成 {type, title}:给被关联那条记忆的标题' };
|
|
1861
|
+
const targetId = (() => { try { return refIdOf(projectScope, { title }, '记忆'); } catch { return ''; } })();
|
|
1862
|
+
if (!targetId) return { error: `memory · 没有「${title}」这条记忆,关联没写成` };
|
|
1863
|
+
resolved.push({ type: link.type || 'related', id: targetId });
|
|
1864
|
+
}
|
|
1865
|
+
draft.links = resolved;
|
|
1866
|
+
}
|
|
1867
|
+
const saved = saveEntry(projectScope, draft);
|
|
1621
1868
|
if (saved.needsScope) {
|
|
1622
1869
|
return {
|
|
1623
|
-
id: '',
|
|
1624
1870
|
title: saved.draft?.title || '',
|
|
1625
1871
|
summary: saved.draft?.summary || '',
|
|
1626
1872
|
when: saved.draft?.when || '',
|
|
@@ -1631,14 +1877,13 @@ export function apply(ctx, config) {
|
|
|
1631
1877
|
needsScope: true,
|
|
1632
1878
|
reason: saved.reasonCode,
|
|
1633
1879
|
needsResolution: false,
|
|
1634
|
-
|
|
1880
|
+
similarTitles: '',
|
|
1635
1881
|
similar: '',
|
|
1636
1882
|
archived: '',
|
|
1637
1883
|
};
|
|
1638
1884
|
}
|
|
1639
1885
|
if (saved.existing) {
|
|
1640
1886
|
return {
|
|
1641
|
-
id: saved.existing.id,
|
|
1642
1887
|
title: saved.existing.title || saved.draft?.title || '',
|
|
1643
1888
|
summary: '',
|
|
1644
1889
|
when: '',
|
|
@@ -1648,10 +1893,10 @@ export function apply(ctx, config) {
|
|
|
1648
1893
|
scope: saved.existing.scope,
|
|
1649
1894
|
scopeName: saved.existing.scope === 'global' ? '' : projectScopeName,
|
|
1650
1895
|
needsScope: false,
|
|
1651
|
-
|
|
1896
|
+
existingTitle: saved.existing.title || saved.draft?.title || '',
|
|
1652
1897
|
existingScope: saved.existing.scope,
|
|
1653
1898
|
needsResolution: false,
|
|
1654
|
-
|
|
1899
|
+
similarTitles: '',
|
|
1655
1900
|
similar: '',
|
|
1656
1901
|
archived: '',
|
|
1657
1902
|
};
|
|
@@ -1659,7 +1904,6 @@ export function apply(ctx, config) {
|
|
|
1659
1904
|
const scope = saved.chosenScope === 'global' || (args.scope === 'global' && !saved.chosenScope) ? globalScope() : projectScope;
|
|
1660
1905
|
if (saved.needsResolution) {
|
|
1661
1906
|
return {
|
|
1662
|
-
id: '',
|
|
1663
1907
|
title: saved.draft.title,
|
|
1664
1908
|
summary: saved.draft.summary,
|
|
1665
1909
|
when: saved.draft.when,
|
|
@@ -1670,15 +1914,16 @@ export function apply(ctx, config) {
|
|
|
1670
1914
|
needsScope: false,
|
|
1671
1915
|
needsResolution: true,
|
|
1672
1916
|
crossLayer: Boolean(saved.crossLayer),
|
|
1917
|
+
crossScope: saved.crossScope === 'global' ? 'global' : 'project',
|
|
1673
1918
|
sameTopic: Boolean(saved.sameTopic),
|
|
1674
|
-
|
|
1675
|
-
similar: saved.similarList.map((item) =>
|
|
1919
|
+
similarTitles: saved.similarList.map((item) => item.title).join('、'),
|
|
1920
|
+
similar: saved.similarList.map((item) => describeCandidate(item, { sameTopic: saved.sameTopic })).join(';'),
|
|
1676
1921
|
archived: '',
|
|
1922
|
+
pendingConflicts: Number(saved.pendingConflicts) || 0,
|
|
1677
1923
|
};
|
|
1678
1924
|
}
|
|
1679
1925
|
recordCitations(scope, sessionIdOf(execution), [saved.record]);
|
|
1680
1926
|
return {
|
|
1681
|
-
id: saved.record.id,
|
|
1682
1927
|
title: saved.record.title,
|
|
1683
1928
|
summary: saved.record.summary,
|
|
1684
1929
|
when: saved.record.when || '',
|
|
@@ -1692,9 +1937,11 @@ export function apply(ctx, config) {
|
|
|
1692
1937
|
scopeName: scope.key === 'global' ? '' : projectScopeName,
|
|
1693
1938
|
needsScope: false,
|
|
1694
1939
|
needsResolution: false,
|
|
1695
|
-
|
|
1940
|
+
similarTitles: saved.similarList.map((item) => item.title).join('、'),
|
|
1941
|
+
supersededTitle: supersedes && supersedesId ? supersedes : '',
|
|
1696
1942
|
archived: (saved.archived || []).map((item) => item.title).join('、'),
|
|
1697
|
-
|
|
1943
|
+
pendingConflicts: Number(saved.pendingConflicts) || 0,
|
|
1944
|
+
...(saved.similar ? { similar: `「${saved.similar.title}」` } : {}),
|
|
1698
1945
|
};
|
|
1699
1946
|
},
|
|
1700
1947
|
}), 'lite-memory: memory_save');
|
|
@@ -1712,7 +1959,6 @@ export function apply(ctx, config) {
|
|
|
1712
1959
|
title: { type: 'string' },
|
|
1713
1960
|
id: { type: 'string' },
|
|
1714
1961
|
reason: { type: 'string' },
|
|
1715
|
-
targetId: { type: 'string' },
|
|
1716
1962
|
targetTitle: { type: 'string' },
|
|
1717
1963
|
},
|
|
1718
1964
|
required: ['asked'],
|
|
@@ -1721,7 +1967,7 @@ export function apply(ctx, config) {
|
|
|
1721
1967
|
render: (_args, value) => [{
|
|
1722
1968
|
type: 'text',
|
|
1723
1969
|
text: value.asked
|
|
1724
|
-
? `memory · suggested "${value.title}"
|
|
1970
|
+
? `memory · suggested "${value.title}"${value.targetTitle ? ` · 确认后更新「${value.targetTitle}」` : ''}`
|
|
1725
1971
|
: `memory · skipped${value.reason ? `\n${value.reason}` : ''}`,
|
|
1726
1972
|
}],
|
|
1727
1973
|
},
|
|
@@ -1735,14 +1981,14 @@ export function apply(ctx, config) {
|
|
|
1735
1981
|
asked: true,
|
|
1736
1982
|
title: noted.title,
|
|
1737
1983
|
id: noted.id,
|
|
1738
|
-
...(noted.
|
|
1984
|
+
...(noted.targetTitle ? { targetTitle: noted.targetTitle } : {}),
|
|
1739
1985
|
};
|
|
1740
1986
|
},
|
|
1741
1987
|
}), 'lite-memory: memory_suggest');
|
|
1742
1988
|
|
|
1743
1989
|
ctx.effect(() => tools.register({
|
|
1744
1990
|
name: 'memory_search',
|
|
1745
|
-
description: `按需检索已有记忆:先搜,命中再 memory_read 读正文。用户提到「记住了什么 / 上次 / 规范 / 约定 / 路径 / 凭据怎么取」,或者眼前反复出现的报错、弹窗、异常时,先查这里有没有记过解法。跨项目的通用条目以 [global] 开头,项目条目以 [项目名]
|
|
1991
|
+
description: `按需检索已有记忆:先搜,命中再 memory_read 读正文。用户提到「记住了什么 / 上次 / 规范 / 约定 / 路径 / 凭据怎么取」,或者眼前反复出现的报错、弹窗、异常时,先查这里有没有记过解法。跨项目的通用条目以 [global] 开头,项目条目以 [项目名] 开头。中文长句可以直接当查询。每行给标题与适用范围;要更新就用那个标题重存。默认只列当前有效的;已经归档或被更新替换的旧说法不混进来,要查历史就在参数里显式要 history。`,
|
|
1746
1992
|
parameters: SEARCH_PARAMS,
|
|
1747
1993
|
output: {
|
|
1748
1994
|
presentationMeta: (_args, value) => ({
|
|
@@ -1763,7 +2009,6 @@ export function apply(ctx, config) {
|
|
|
1763
2009
|
items: {
|
|
1764
2010
|
type: 'object',
|
|
1765
2011
|
properties: {
|
|
1766
|
-
id: { type: 'string' },
|
|
1767
2012
|
title: { type: 'string' },
|
|
1768
2013
|
scope: { type: 'string' },
|
|
1769
2014
|
revision: { type: 'integer' },
|
|
@@ -1771,8 +2016,9 @@ export function apply(ctx, config) {
|
|
|
1771
2016
|
origin: { type: 'string' },
|
|
1772
2017
|
kind: { type: 'string' },
|
|
1773
2018
|
when: { type: 'string' },
|
|
2019
|
+
updated: { type: 'string' }, // ui-copy-ignore
|
|
1774
2020
|
},
|
|
1775
|
-
required: ['
|
|
2021
|
+
required: ['title', 'scope', 'revision', 'status', 'origin', 'kind', 'when'],
|
|
1776
2022
|
additionalProperties: false,
|
|
1777
2023
|
},
|
|
1778
2024
|
},
|
|
@@ -1783,7 +2029,7 @@ export function apply(ctx, config) {
|
|
|
1783
2029
|
render: (_args, value) => [{
|
|
1784
2030
|
type: 'text',
|
|
1785
2031
|
text: value.status === 'budget_exhausted'
|
|
1786
|
-
? 'memory ·
|
|
2032
|
+
? 'memory · 这条消息已停止检索,下一条消息再查'
|
|
1787
2033
|
: value.status === 'memory_disabled'
|
|
1788
2034
|
? 'memory · 记忆检索已关闭'
|
|
1789
2035
|
: value.status === 'needs_context'
|
|
@@ -1797,7 +2043,7 @@ export function apply(ctx, config) {
|
|
|
1797
2043
|
if (recallMode() === 'off' || !memoryEnabledFor(execution)) return { count: 0, chars: 0, text: '', status: 'memory_disabled', refs: [] };
|
|
1798
2044
|
const agent = execution?.agent;
|
|
1799
2045
|
const win = windowOf(agent);
|
|
1800
|
-
if (win && !
|
|
2046
|
+
if (win && !affordableTool(win, 0)) return { count: 0, chars: 0, text: '', status: 'budget_exhausted', refs: [] };
|
|
1801
2047
|
const wantsBind = args.bindForRequest === true;
|
|
1802
2048
|
const contextId = wantsBind && !args.contextId ? toolContextFor(execution) : args.contextId;
|
|
1803
2049
|
if (wantsBind && !contextId) {
|
|
@@ -1848,8 +2094,9 @@ export function apply(ctx, config) {
|
|
|
1848
2094
|
};
|
|
1849
2095
|
if (win) {
|
|
1850
2096
|
const bytes = jsonBytes(value);
|
|
1851
|
-
if (!
|
|
2097
|
+
if (!affordableTool(win, bytes)) return { ...value, status: 'budget_exhausted', text: '' };
|
|
1852
2098
|
win.bytes += bytes;
|
|
2099
|
+
win.toolBytes += bytes;
|
|
1853
2100
|
win.reads += 1;
|
|
1854
2101
|
}
|
|
1855
2102
|
return value;
|
|
@@ -1859,18 +2106,20 @@ export function apply(ctx, config) {
|
|
|
1859
2106
|
const empty = { count: 0, chars: 0, text: '', status: 'ok', reasonCode: 'no_match', noTriggerReason: '没有匹配的记忆', refs: [] };
|
|
1860
2107
|
if (win) {
|
|
1861
2108
|
const bytes = jsonBytes(empty);
|
|
1862
|
-
if (!
|
|
2109
|
+
if (!affordableTool(win, bytes)) return { ...empty, status: 'budget_exhausted' };
|
|
1863
2110
|
win.bytes += bytes;
|
|
2111
|
+
win.toolBytes += bytes;
|
|
1864
2112
|
win.reads += 1;
|
|
1865
2113
|
}
|
|
1866
2114
|
return empty;
|
|
1867
2115
|
}
|
|
1868
2116
|
if (win) {
|
|
1869
2117
|
let kept = shown.length;
|
|
1870
|
-
while (kept > 0 && !
|
|
2118
|
+
while (kept > 0 && !affordableTool(win, jsonBytes(valueOf(kept)))) kept -= 1;
|
|
1871
2119
|
if (kept === 0) return { count: 0, chars: 0, text: '', status: 'budget_exhausted', refs: [] };
|
|
1872
2120
|
const value = valueOf(kept);
|
|
1873
2121
|
win.bytes += jsonBytes(value);
|
|
2122
|
+
win.toolBytes += jsonBytes(value);
|
|
1874
2123
|
win.reads += 1;
|
|
1875
2124
|
recordCitations(scope, sessionIdOf(execution), hits.slice(0, kept));
|
|
1876
2125
|
return value;
|
|
@@ -1883,13 +2132,12 @@ export function apply(ctx, config) {
|
|
|
1883
2132
|
|
|
1884
2133
|
ctx.effect(() => tools.register({
|
|
1885
2134
|
name: 'memory_read',
|
|
1886
|
-
description: '
|
|
2135
|
+
description: '读一条记忆的正文。搜索结果里只有一行摘要,需要细节时按标题读。索引里还有、正文文件丢了会直接报错,不会返回空正文。',
|
|
1887
2136
|
parameters: READ_PARAMS,
|
|
1888
2137
|
output: {
|
|
1889
2138
|
schema: {
|
|
1890
2139
|
type: 'object',
|
|
1891
2140
|
properties: {
|
|
1892
|
-
id: { type: 'string' },
|
|
1893
2141
|
title: { type: 'string' },
|
|
1894
2142
|
when: { type: 'string' },
|
|
1895
2143
|
revision: { type: 'integer' },
|
|
@@ -1902,23 +2150,23 @@ export function apply(ctx, config) {
|
|
|
1902
2150
|
links: { type: 'string' },
|
|
1903
2151
|
repeat: { type: 'boolean' },
|
|
1904
2152
|
},
|
|
1905
|
-
required: ['
|
|
2153
|
+
required: ['scope', 'status'],
|
|
1906
2154
|
additionalProperties: false,
|
|
1907
2155
|
},
|
|
1908
2156
|
render: (_args, value) => [{
|
|
1909
2157
|
type: 'text',
|
|
1910
2158
|
text: value.status === 'budget_exhausted'
|
|
1911
|
-
? 'memory ·
|
|
2159
|
+
? 'memory · 这条消息已停止读记忆,下一条消息再读'
|
|
1912
2160
|
: value.status === 'memory_disabled'
|
|
1913
2161
|
? 'memory · 记忆检索已关闭'
|
|
1914
2162
|
: value.repeat
|
|
1915
2163
|
? 'memory · 本请求已读过这条记忆'
|
|
1916
2164
|
: value.body
|
|
1917
|
-
? `memory · ${withTag(value, `「${value.title}」`)}${value.when ? `(${value.when})` : ''}
|
|
2165
|
+
? `memory · ${withTag(value, `「${value.title}」`)}${value.when ? `(${value.when})` : ''}${statusLabel(value.status)}\n${value.body}`
|
|
1918
2166
|
: value.links !== undefined && value.status === 'ok'
|
|
1919
2167
|
? `memory · 「${value.title}」的相关记忆\n${value.links || '(无)'}`
|
|
1920
2168
|
: value.status === 'needs_context'
|
|
1921
|
-
? 'memory ·
|
|
2169
|
+
? 'memory · 这条记忆的适用条件还没确认,先说明当前任务再读'
|
|
1922
2170
|
: value.status === 'not_applicable'
|
|
1923
2171
|
? 'memory · 这条记忆不适用于当前任务'
|
|
1924
2172
|
: value.status === 'archived'
|
|
@@ -1929,7 +2177,6 @@ export function apply(ctx, config) {
|
|
|
1929
2177
|
}],
|
|
1930
2178
|
presentationMeta: (_args, value) => ({
|
|
1931
2179
|
kind: 'memory-read',
|
|
1932
|
-
id: value.id || '',
|
|
1933
2180
|
title: value.title || '',
|
|
1934
2181
|
scope: value.scope || '',
|
|
1935
2182
|
revision: value.revision || 0,
|
|
@@ -1939,11 +2186,13 @@ export function apply(ctx, config) {
|
|
|
1939
2186
|
}),
|
|
1940
2187
|
},
|
|
1941
2188
|
execute: async (args, execution) => {
|
|
1942
|
-
if (recallMode() === 'off' || !memoryEnabledFor(execution)) return {
|
|
2189
|
+
if (recallMode() === 'off' || !memoryEnabledFor(execution)) return { scope: '', status: 'memory_disabled' };
|
|
1943
2190
|
const resolved = resolveRequestContext(execution, args.contextId);
|
|
1944
|
-
if (!resolved.ok) return {
|
|
2191
|
+
if (!resolved.ok) return { scope: '', status: resolved.status || 'needs_context' };
|
|
1945
2192
|
const scope = resolved.scope;
|
|
1946
2193
|
const scopeName = scope.key === 'global' ? '' : projectNameOf(scope);
|
|
2194
|
+
/** 明确指定层时只在那一层解析标题,同名条目不会串到别的层。 */
|
|
2195
|
+
const readScope = args.scope === 'global' ? globalScope() : scope;
|
|
1947
2196
|
const context = resolved.context;
|
|
1948
2197
|
const allowExperimental = resolved.experimental === true;
|
|
1949
2198
|
const agent = execution?.agent;
|
|
@@ -1951,48 +2200,57 @@ export function apply(ctx, config) {
|
|
|
1951
2200
|
const gateRuntime = resolved.held || win?.runtime
|
|
1952
2201
|
|| { projectId: scope.key, context, experimental: resolved.experimental === true };
|
|
1953
2202
|
if (args.view === 'links') {
|
|
1954
|
-
const
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
}
|
|
1958
|
-
const found = readLinks(
|
|
1959
|
-
if (!found) throw new Error(
|
|
2203
|
+
const refId = refIdOf(readScope, args);
|
|
2204
|
+
const source = locate(readScope, refId);
|
|
2205
|
+
const gateOk = !source || view.eligibility(source.record, gateRuntime, 'read').ok;
|
|
2206
|
+
if (!gateOk) return { scope: '', status: 'needs_context', links: '' };
|
|
2207
|
+
const found = readLinks(readScope, refId, { context, allowExperimental, runtime: gateRuntime, now: Date.now() });
|
|
2208
|
+
if (!found) throw new Error(`没有「${String(args.title || args.id || '').trim()}」这条记忆`);
|
|
1960
2209
|
if (found.status !== 'ok') {
|
|
1961
|
-
const withheld = {
|
|
2210
|
+
const withheld = { scope: '', status: found.status, links: '' };
|
|
1962
2211
|
if (win) {
|
|
1963
2212
|
const bytes = jsonBytes(withheld);
|
|
1964
|
-
if (!
|
|
2213
|
+
if (!affordableTool(win, bytes)) return { scope: '', status: 'budget_exhausted' };
|
|
1965
2214
|
win.bytes += bytes;
|
|
2215
|
+
win.toolBytes += bytes;
|
|
1966
2216
|
win.reads += 1;
|
|
1967
2217
|
}
|
|
1968
2218
|
return withheld;
|
|
1969
2219
|
}
|
|
1970
2220
|
const rows = found.links.filter((link) => link.status === 'invalid' || link.status === 'ok')
|
|
1971
2221
|
.map((link) => (link.status === 'invalid'
|
|
1972
|
-
?
|
|
1973
|
-
: `- ${link.scope === 'global' ? '[global] ' : (scopeName ? `[${scopeName}] ` : '')}「${link.title}
|
|
2222
|
+
? '- 有一条已失效'
|
|
2223
|
+
: `- ${link.scope === 'global' ? '[global] ' : (scopeName ? `[${scopeName}] ` : '')}「${link.title}」`));
|
|
1974
2224
|
const text = rows.join('\n') || '(无)';
|
|
1975
|
-
const value = {
|
|
2225
|
+
const value = { scope: found.scope, scopeName, status: found.status, links: text };
|
|
1976
2226
|
if (win) {
|
|
1977
2227
|
const bytes = jsonBytes(value);
|
|
1978
|
-
if (!
|
|
2228
|
+
if (!affordableTool(win, bytes)) return { scope: '', status: 'budget_exhausted' };
|
|
1979
2229
|
win.bytes += bytes;
|
|
2230
|
+
win.toolBytes += bytes;
|
|
1980
2231
|
win.reads += 1;
|
|
1981
2232
|
}
|
|
1982
2233
|
return value;
|
|
1983
2234
|
}
|
|
1984
|
-
const record = readEntry(
|
|
1985
|
-
|
|
2235
|
+
const record = readEntry(readScope, refIdOf(readScope, args), {
|
|
2236
|
+
layers: args.scope ? [readScope] : undefined,
|
|
2237
|
+
context,
|
|
2238
|
+
allowExperimental,
|
|
2239
|
+
/** 只有真正把正文交出去的读取才算用过:条件未知/被取代/过期不更新使用记录。 */
|
|
2240
|
+
usage: (item) => view.eligibility(item, gateRuntime, 'read').ok,
|
|
2241
|
+
});
|
|
2242
|
+
if (!record) throw new Error(`没有「${String(args.title || args.id || '').trim()}」这条记忆`);
|
|
2243
|
+
/** 归档条目还能按标题找到:只回状态,不交正文,也不把它当成「不存在」。 */
|
|
2244
|
+
if (record.status === 'archived') return { title: record.title, scope: record.scope, status: 'archived', body: '' };
|
|
1986
2245
|
const gate = view.eligibility(record, gateRuntime, 'read');
|
|
1987
|
-
if (!gate.ok) return {
|
|
1988
|
-
/**
|
|
1989
|
-
if (record.status !== 'ok') {
|
|
1990
|
-
return {
|
|
2246
|
+
if (!gate.ok) return { scope: record.scope, status: gate.status, body: '' };
|
|
2247
|
+
/** 条件未知或不适用的条目只报状态:标题只能定位,不能证明适用,正文一律不先返回。 */
|
|
2248
|
+
if (record.status !== 'ok' && record.status !== 'archived') {
|
|
2249
|
+
return { scope: record.scope, status: record.status === 'needs_context' ? 'needs_context' : 'not_applicable', body: '' };
|
|
1991
2250
|
}
|
|
1992
2251
|
const revision = record.revision || 1;
|
|
1993
2252
|
const body = String(record.body ?? '');
|
|
1994
2253
|
const value = {
|
|
1995
|
-
id: record.id,
|
|
1996
2254
|
title: record.title,
|
|
1997
2255
|
when: record.when || '',
|
|
1998
2256
|
revision,
|
|
@@ -2003,19 +2261,21 @@ export function apply(ctx, config) {
|
|
|
2003
2261
|
body,
|
|
2004
2262
|
};
|
|
2005
2263
|
if (win) {
|
|
2006
|
-
if (!
|
|
2264
|
+
if (!affordableTool(win, 0)) return { scope: '', status: 'budget_exhausted' };
|
|
2007
2265
|
const seenRevision = win.readBodies.get(record.id);
|
|
2008
2266
|
if (seenRevision === revision) {
|
|
2009
2267
|
const receipt = { ...value, body: '', repeat: true };
|
|
2010
2268
|
const bytes = jsonBytes(receipt);
|
|
2011
|
-
if (!
|
|
2269
|
+
if (!affordableTool(win, bytes)) return { scope: '', status: 'budget_exhausted' };
|
|
2012
2270
|
win.bytes += bytes;
|
|
2271
|
+
win.toolBytes += bytes;
|
|
2013
2272
|
win.reads += 1;
|
|
2014
2273
|
return receipt;
|
|
2015
2274
|
}
|
|
2016
2275
|
const bytes = jsonBytes(value);
|
|
2017
|
-
if (!
|
|
2276
|
+
if (!affordableTool(win, bytes, 1)) return { scope: '', status: 'budget_exhausted' };
|
|
2018
2277
|
win.bytes += bytes;
|
|
2278
|
+
win.toolBytes += bytes;
|
|
2019
2279
|
win.bodies += 1;
|
|
2020
2280
|
win.reads += 1;
|
|
2021
2281
|
win.readBodies.set(record.id, revision);
|
|
@@ -2033,13 +2293,12 @@ export function apply(ctx, config) {
|
|
|
2033
2293
|
schema: {
|
|
2034
2294
|
type: 'object',
|
|
2035
2295
|
properties: {
|
|
2036
|
-
id: { type: 'string' },
|
|
2037
2296
|
title: { type: 'string' },
|
|
2038
2297
|
scope: { type: 'string' },
|
|
2039
2298
|
scopeName: { type: 'string' },
|
|
2040
2299
|
mode: { type: 'string', enum: ['archive', 'purged', 'needs_resolution', 'purge_pending', 'memory_disabled'] },
|
|
2041
2300
|
},
|
|
2042
|
-
required: ['
|
|
2301
|
+
required: ['mode'],
|
|
2043
2302
|
additionalProperties: false,
|
|
2044
2303
|
},
|
|
2045
2304
|
render: (_args, value) => [{
|
|
@@ -2047,16 +2306,15 @@ export function apply(ctx, config) {
|
|
|
2047
2306
|
text: value.mode === 'memory_disabled'
|
|
2048
2307
|
? 'memory · 本项目记忆已关闭,先在面板打开'
|
|
2049
2308
|
: value.mode === 'purged'
|
|
2050
|
-
? `memory · forgotten "${value.title}"
|
|
2309
|
+
? `memory · forgotten "${value.title}"`
|
|
2051
2310
|
: value.mode === 'purge_pending'
|
|
2052
|
-
? `memory · 清除未完成 ·
|
|
2311
|
+
? `memory · 清除未完成 · 「${value.title || '这条记忆'}」,重启后会继续`
|
|
2053
2312
|
: value.mode === 'needs_resolution'
|
|
2054
|
-
? `memory · 先确认是归档还是彻底忘掉 ·
|
|
2055
|
-
: `memory · archived ${withTag(value, `"${value.title}"`)}
|
|
2313
|
+
? `memory · 先确认是归档还是彻底忘掉 · 「${value.title || '这条记忆'}」`
|
|
2314
|
+
: `memory · archived ${withTag(value, `"${value.title}"`)},可找回`,
|
|
2056
2315
|
}],
|
|
2057
2316
|
presentationMeta: (_args, value) => ({
|
|
2058
2317
|
kind: 'memory-forget',
|
|
2059
|
-
id: value.id || '',
|
|
2060
2318
|
title: value.title || '',
|
|
2061
2319
|
scope: value.scope || '',
|
|
2062
2320
|
mode: value.mode,
|
|
@@ -2065,25 +2323,33 @@ export function apply(ctx, config) {
|
|
|
2065
2323
|
execute: async (args, execution) => {
|
|
2066
2324
|
const scope = scopeOf(cwdOf(execution));
|
|
2067
2325
|
const projectScopeName = projectNameOf(scope);
|
|
2068
|
-
if (!memoryEnabled(scope)) return {
|
|
2069
|
-
const
|
|
2070
|
-
if (!
|
|
2326
|
+
if (!memoryEnabled(scope)) return { title: '', scope: '', mode: 'memory_disabled' };
|
|
2327
|
+
const forgetTitle = String(args.title || args.id || '').trim();
|
|
2328
|
+
if (!args.scope && args.mode) {
|
|
2329
|
+
const candidates = titleCandidates(scope, forgetTitle);
|
|
2330
|
+
if (candidates.length > 1) {
|
|
2331
|
+
throw new Error(`「${forgetTitle}」在项目层和全局层各有一条:删全局加 scope="global",删项目加 scope="project"`);
|
|
2332
|
+
}
|
|
2333
|
+
}
|
|
2334
|
+
const onlyLayer = args.scope === 'global' ? globalScope() : args.scope === 'project' ? scope : null;
|
|
2335
|
+
const refId = refIdOf(scope, args);
|
|
2336
|
+
const located = locate(scope, refId, onlyLayer ? { layers: [onlyLayer] } : {});
|
|
2337
|
+
if (!located && !args.mode) throw new Error(`没有「${String(args.title || args.id || '').trim()}」这条记忆`);
|
|
2071
2338
|
if (!args.mode) {
|
|
2072
|
-
return {
|
|
2339
|
+
return { title: located.record.title || '', mode: 'needs_resolution', scope: located.scope.key === 'global' ? 'global' : 'project' };
|
|
2073
2340
|
}
|
|
2074
2341
|
let dropped;
|
|
2075
2342
|
try {
|
|
2076
|
-
dropped = forget(scope, args.
|
|
2343
|
+
dropped = forget(scope, refId, onlyLayer ? { mode: args.mode, layers: [onlyLayer] } : { mode: args.mode });
|
|
2077
2344
|
} catch (error) {
|
|
2078
2345
|
if (error && error.code === 'purge_pending') {
|
|
2079
2346
|
const layer = located ? located.scope : scope;
|
|
2080
2347
|
try {
|
|
2081
2348
|
state.resumePurge(layer);
|
|
2082
|
-
dropped = forget(scope, args.
|
|
2349
|
+
dropped = forget(scope, refId, onlyLayer ? { mode: args.mode, layers: [onlyLayer] } : { mode: args.mode });
|
|
2083
2350
|
} catch (again) {
|
|
2084
2351
|
if (again && again.code === 'purge_pending') {
|
|
2085
2352
|
return {
|
|
2086
|
-
id: args.id,
|
|
2087
2353
|
title: located ? located.record.title : '',
|
|
2088
2354
|
mode: 'purge_pending',
|
|
2089
2355
|
scope: layer.key === 'global' ? 'global' : 'project',
|
|
@@ -2096,11 +2362,10 @@ export function apply(ctx, config) {
|
|
|
2096
2362
|
throw error;
|
|
2097
2363
|
}
|
|
2098
2364
|
}
|
|
2099
|
-
if (!dropped) throw new Error(
|
|
2365
|
+
if (!dropped) throw new Error(`没有「${String(args.title || args.id || '').trim()}」这条记忆`);
|
|
2100
2366
|
const layerKey = dropped.scopeKey || (located ? located.scope.key : scope.key);
|
|
2101
2367
|
if (dropped.mode === 'purged') revokeLearningUse(layerKey);
|
|
2102
2368
|
return {
|
|
2103
|
-
id: dropped.id,
|
|
2104
2369
|
title: dropped.title || (located ? located.record.title : ''),
|
|
2105
2370
|
mode: dropped.mode,
|
|
2106
2371
|
scope: layerKey === 'global' ? 'global' : 'project',
|
|
@@ -2239,7 +2504,7 @@ export function apply(ctx, config) {
|
|
|
2239
2504
|
const ids = fresh.map((message) => String(message?.id ?? '')).filter(Boolean).sort();
|
|
2240
2505
|
const win = openWindow(agent, { sessionId, turn, messageIds: ids });
|
|
2241
2506
|
const budgetBefore = win ? {
|
|
2242
|
-
bytes: win.bytes, bodies: win.bodies, reads: win.reads,
|
|
2507
|
+
bytes: win.bytes, bodies: win.bodies, reads: win.reads, toolBytes: win.toolBytes || 0,
|
|
2243
2508
|
provided: new Map(win.provided), readBodies: new Map(win.readBodies),
|
|
2244
2509
|
recallHeader: win.recallHeader, experimentBytes: win.experimentBytes,
|
|
2245
2510
|
autoBudget: win.autoBudget ? JSON.parse(JSON.stringify(win.autoBudget)) : null,
|
|
@@ -2259,6 +2524,7 @@ export function apply(ctx, config) {
|
|
|
2259
2524
|
}
|
|
2260
2525
|
const scope = scopeOf(cwd);
|
|
2261
2526
|
if (!memoryEnabled(scope)) {
|
|
2527
|
+
noteRecall(scope, sessionId, { reason: 'memory_off' });
|
|
2262
2528
|
clearPrepared(agent);
|
|
2263
2529
|
return decision;
|
|
2264
2530
|
}
|
|
@@ -2269,7 +2535,9 @@ export function apply(ctx, config) {
|
|
|
2269
2535
|
*/
|
|
2270
2536
|
const runtime = runtimeFor(scope, { sessionId, query, batchKey, messageIds: ids, autoRoute: recallMode() === 'adaptive' });
|
|
2271
2537
|
if (win && runtime) win.runtime = runtime.held;
|
|
2538
|
+
rememberRuntime(scope, sessionId, runtime && runtime.held);
|
|
2272
2539
|
if (recallMode() !== 'adaptive') {
|
|
2540
|
+
noteRecall(scope, sessionId, { reason: 'tools_only' });
|
|
2273
2541
|
clearPrepared(agent);
|
|
2274
2542
|
return decision;
|
|
2275
2543
|
}
|
|
@@ -2286,12 +2554,16 @@ export function apply(ctx, config) {
|
|
|
2286
2554
|
? rememberFeedback(scope, sessionId, fresh, runtime.held, turn)
|
|
2287
2555
|
: null;
|
|
2288
2556
|
const snapshot = projectRecall(scope, query, recalledContext, win, runtime ? runtime.held : null);
|
|
2557
|
+
noteRecall(scope, sessionId, {
|
|
2558
|
+
reason: snapshot.reason, matched: snapshot.matched, cards: snapshot.cards.length, chars: snapshot.bytes,
|
|
2559
|
+
});
|
|
2289
2560
|
/** 没发布出去的投影要退还额度:不能因为一次超时把本请求的召回额度耗掉。 */
|
|
2290
2561
|
const rollbackBudget = () => {
|
|
2291
2562
|
if (!win || !budgetBefore) return;
|
|
2292
2563
|
win.bytes = budgetBefore.bytes;
|
|
2293
2564
|
win.bodies = budgetBefore.bodies;
|
|
2294
2565
|
win.reads = budgetBefore.reads;
|
|
2566
|
+
win.toolBytes = budgetBefore.toolBytes;
|
|
2295
2567
|
win.provided = budgetBefore.provided;
|
|
2296
2568
|
win.readBodies = budgetBefore.readBodies;
|
|
2297
2569
|
win.recallHeader = budgetBefore.recallHeader;
|
|
@@ -2314,7 +2586,7 @@ export function apply(ctx, config) {
|
|
|
2314
2586
|
return decision;
|
|
2315
2587
|
}
|
|
2316
2588
|
/** 实验上下文即使本轮零注入也要留着:工具绑定后同一请求还能补上规则。 */
|
|
2317
|
-
if (!snapshot && !experiment) {
|
|
2589
|
+
if ((!snapshot || !snapshot.cards.length) && !experiment) {
|
|
2318
2590
|
rollbackBudget();
|
|
2319
2591
|
clearPrepared(agent);
|
|
2320
2592
|
return decision;
|
|
@@ -2322,7 +2594,7 @@ export function apply(ctx, config) {
|
|
|
2322
2594
|
let evolutionEpoch = '';
|
|
2323
2595
|
try { evolutionEpoch = String(evolutionStatus(scope).epoch || ''); } catch { evolutionEpoch = ''; }
|
|
2324
2596
|
const state = {
|
|
2325
|
-
text: snapshot ? snapshot.text : '',
|
|
2597
|
+
text: snapshot && snapshot.cards.length ? snapshot.text : '',
|
|
2326
2598
|
experimentText: experiment ? experiment.text : '',
|
|
2327
2599
|
experimentParts: experiment && experiment.held ? (experiment.held.experimentParts || null) : null,
|
|
2328
2600
|
validUntil: experiment && experiment.held ? (experiment.held.validUntil || '') : '',
|
|
@@ -2487,7 +2759,7 @@ export function apply(ctx, config) {
|
|
|
2487
2759
|
if ((feedbackRevisions.get(scope.key) || 0) !== candidate.feedbackRevision) {
|
|
2488
2760
|
throw new Error('反馈已更新,这轮结果没采用');
|
|
2489
2761
|
}
|
|
2490
|
-
const parsed = learning.parseOutput(text, { finish, refs: input.refs, rules: input.rules, ranges });
|
|
2762
|
+
const parsed = learning.parseOutput(text, { finish, refs: input.refs, rules: input.rules, ranges, expectedTarget: candidate.expectedTarget });
|
|
2491
2763
|
if (!parsed.ok) throw new Error(parsed.reason);
|
|
2492
2764
|
/** 用户已经把要求说成长期规则,模型仍返回空时至少留一条规则,不让这条反馈无声消失。 */
|
|
2493
2765
|
const changes = parsed.changes.length
|