dsh-lite-memory 0.18.1 → 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 +13 -13
- package/TRIGGERS.md +1 -1
- package/client.js +184 -34
- 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 +347 -123
- package/learning.js +65 -6
- package/package.json +3 -3
- package/state.js +47 -6
- package/store.js +180 -18
- package/test/client.test.mjs +47 -25
- package/test/contract.test.mjs +221 -86
- package/test/control.test.mjs +2 -2
- package/test/eval-seed.test.mjs +113 -0
- package/test/evolution-read.test.mjs +10 -10
- package/test/ha.test.mjs +55 -5
- 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 +67 -9
- package/test/state.test.mjs +57 -1
- package/test/store.test.mjs +49 -5
- package/test/view.test.mjs +3 -0
- package/view.js +85 -12
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,
|
|
@@ -652,7 +748,7 @@ function refsOf(hits) {
|
|
|
652
748
|
const SAVE_PARAMS = {
|
|
653
749
|
type: 'object',
|
|
654
750
|
properties: {
|
|
655
|
-
|
|
751
|
+
updateTitle: { type: 'string', description: '要更新的那条记忆的标题(原样抄搜索结果里的标题)。同一件事更新时用它,别另建一条' },
|
|
656
752
|
title: { type: 'string', description: `标题:名词短语,用户一眼能认出是哪件事,不超过 ${LIMITS.titleChars} 字。它就是这条记忆的身份` },
|
|
657
753
|
summary: { type: 'string', description: `一句话说清要记的内容本身——用户只看这一行决定留不留,名词短语最好,超长会自动截到这个长度` },
|
|
658
754
|
body: { type: 'string', description: `正文 markdown,整段替换旧正文,不超过 ${LIMITS.entryBytes} 字节。只写原则、判断规则、触发条件和边界;不写一次性数字或代码片段,这些查当前代码或文档。` },
|
|
@@ -661,7 +757,7 @@ const SAVE_PARAMS = {
|
|
|
661
757
|
tags: { type: 'array', items: { type: 'string' }, description: '最多 5 个标签;更新时不给就保留原值' },
|
|
662
758
|
supersedes: {
|
|
663
759
|
type: 'string',
|
|
664
|
-
description: '
|
|
760
|
+
description: '要取代的那条记忆的标题(原样抄搜索结果里的标题):新内容写成新条目,旧条目标为已被取代并保留在历史里;同层才有效,已被取代过的不能再取代',
|
|
665
761
|
},
|
|
666
762
|
resolution: {
|
|
667
763
|
type: 'string',
|
|
@@ -674,13 +770,12 @@ const SAVE_PARAMS = {
|
|
|
674
770
|
evidenceRef: { type: 'string', description: '归属判定的依据(哪条消息、哪个文件、哪次观察);用户没说范围时用来判断能不能存' },
|
|
675
771
|
sameTopicAs: {
|
|
676
772
|
type: 'object',
|
|
677
|
-
description: '显式绑定到已有主题槽位:{scope,
|
|
773
|
+
description: '显式绑定到已有主题槽位:{scope, title}。标题对不上就报错,不写',
|
|
678
774
|
properties: {
|
|
679
775
|
scope: { type: 'string', enum: ['project', 'global'] },
|
|
680
|
-
|
|
681
|
-
revision: { type: 'integer' },
|
|
776
|
+
title: { type: 'string' },
|
|
682
777
|
},
|
|
683
|
-
required: ['scope', '
|
|
778
|
+
required: ['scope', 'title'],
|
|
684
779
|
additionalProperties: false,
|
|
685
780
|
},
|
|
686
781
|
mode: {
|
|
@@ -724,9 +819,9 @@ const SAVE_PARAMS = {
|
|
|
724
819
|
type: 'object',
|
|
725
820
|
properties: {
|
|
726
821
|
type: { type: 'string', enum: view.LINK_TYPES, description: '默认 related' },
|
|
727
|
-
|
|
822
|
+
title: { type: 'string', description: '被关联那条记忆的标题(原样抄搜索结果里的标题)' },
|
|
728
823
|
},
|
|
729
|
-
required: ['
|
|
824
|
+
required: ['title'],
|
|
730
825
|
additionalProperties: false,
|
|
731
826
|
},
|
|
732
827
|
},
|
|
@@ -743,7 +838,7 @@ const SAVE_PARAMS = {
|
|
|
743
838
|
|
|
744
839
|
const SUGGEST_PARAMS = {
|
|
745
840
|
...SAVE_PARAMS,
|
|
746
|
-
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))),
|
|
747
842
|
};
|
|
748
843
|
|
|
749
844
|
const SEARCH_PARAMS = {
|
|
@@ -762,28 +857,69 @@ const SEARCH_PARAMS = {
|
|
|
762
857
|
const READ_PARAMS = {
|
|
763
858
|
type: 'object',
|
|
764
859
|
properties: {
|
|
765
|
-
|
|
860
|
+
title: { type: 'string', description: '记忆标题,来自搜索结果或召回卡' },
|
|
766
861
|
view: { type: 'string', enum: ['full', 'links'], description: '默认 full 读正文;links 只看直接相关的记忆清单,不取正文' },
|
|
862
|
+
scope: { type: 'string', enum: ['project', 'global'], description: '只在指定层解析标题;默认先当前项目再全局' },
|
|
767
863
|
contextId: { type: 'string', description: '上一轮自动索引或工具结果给出的任务上下文 id;读实验规则时带上' },
|
|
768
864
|
},
|
|
769
|
-
required: ['id'],
|
|
770
865
|
additionalProperties: false,
|
|
771
866
|
};
|
|
772
867
|
|
|
773
868
|
const FORGET_PARAMS = {
|
|
774
869
|
type: 'object',
|
|
775
870
|
properties: {
|
|
776
|
-
|
|
871
|
+
title: { type: 'string', description: '要忘掉的那条记忆的标题' },
|
|
872
|
+
scope: { type: 'string', enum: ['project', 'global'], description: '只在指定层解析标题;默认先当前项目再全局' },
|
|
777
873
|
mode: {
|
|
778
874
|
type: 'string',
|
|
779
875
|
enum: ['archive', 'purge'],
|
|
780
876
|
description: 'archive 移入归档可找回(只是不想再提);purge 彻底忘掉,正文与受管副本清除且不可复活。没点明是哪种就两种都别做,先问用户',
|
|
781
877
|
},
|
|
782
878
|
},
|
|
783
|
-
required: ['id'],
|
|
784
879
|
additionalProperties: false,
|
|
785
880
|
};
|
|
786
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
|
+
|
|
787
923
|
/** 记忆归属标记:全局 `[global]`,项目用项目目录名(如 `[DSH]`)。 */
|
|
788
924
|
function projectNameOf(scope) {
|
|
789
925
|
const base = scope && scope.cwd ? path.basename(scope.cwd) : '';
|
|
@@ -816,7 +952,7 @@ function humanAge(iso) {
|
|
|
816
952
|
function describeCandidate(item, options = {}) {
|
|
817
953
|
const age = humanAge(item.updated);
|
|
818
954
|
const why = options.sameTopic ? '' : item.reason === 'similar_summary' ? '摘要相近' : '标题相近';
|
|
819
|
-
const parts = [
|
|
955
|
+
const parts = [`「${item.title}」`];
|
|
820
956
|
if (age) parts.push(`${age}更新`);
|
|
821
957
|
if (item.when) parts.push(item.when);
|
|
822
958
|
if (why) parts.push(why);
|
|
@@ -840,12 +976,12 @@ function statusLabel(status) {
|
|
|
840
976
|
export const RECALL_MODES = ['adaptive', 'tool_only', 'off'];
|
|
841
977
|
const RECALL_QUERY_CHARS = 400;
|
|
842
978
|
const RECALL_DEADLINE_MS = 150;
|
|
843
|
-
const RECALL_HEADER = '已有记忆可能相关(需要细节用 memory_read
|
|
979
|
+
const RECALL_HEADER = '已有记忆可能相关(需要细节用 memory_read 带标题取正文,别凭标题猜):';
|
|
844
980
|
|
|
845
981
|
/** 卡片只放够判断要不要读的几行;必要前提整条在场,装不下就整卡省略。 */
|
|
846
982
|
function renderRecallCard(card) {
|
|
847
983
|
const tag = card.scope === 'global' ? '[global]' : card.scopeName ? `[${card.scopeName}]` : '';
|
|
848
|
-
const parts = [`- ${tag ? `${tag} ` : ''}「${card.title}
|
|
984
|
+
const parts = [`- ${tag ? `${tag} ` : ''}「${card.title}」`];
|
|
849
985
|
if (card.summary) parts.push(`|${card.summary}`);
|
|
850
986
|
if (card.conditions && card.conditions.length) parts.push(`|前提:${card.conditions.join('、')}`);
|
|
851
987
|
if (card.selection) parts.push(`|只适用于 ${card.selection}`);
|
|
@@ -936,6 +1072,17 @@ export function apply(ctx, config) {
|
|
|
936
1072
|
return win.bytes + bytes <= view.REQUEST_BYTES;
|
|
937
1073
|
}
|
|
938
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
|
+
|
|
939
1086
|
/** 续写轮没有新消息:沿用上一次能核对的请求上下文。 */
|
|
940
1087
|
const lastRuntime = new Map();
|
|
941
1088
|
const rememberRuntime = (scope, sessionId, held) => {
|
|
@@ -954,7 +1101,7 @@ export function apply(ctx, config) {
|
|
|
954
1101
|
if (existing && existing.signature === signature) return existing;
|
|
955
1102
|
const next = {
|
|
956
1103
|
signature, sessionId, turn, messageIds,
|
|
957
|
-
bytes: 0, bodies: 0, reads: 0,
|
|
1104
|
+
bytes: 0, bodies: 0, reads: 0, toolBytes: 0,
|
|
958
1105
|
provided: new Map(), readBodies: new Map(), at: Date.now(),
|
|
959
1106
|
experiment: null, contexts: new Map(), runtime: null, autoBudget: null,
|
|
960
1107
|
};
|
|
@@ -1025,17 +1172,18 @@ export function apply(ctx, config) {
|
|
|
1025
1172
|
}
|
|
1026
1173
|
}
|
|
1027
1174
|
/** 同一槽位只让胜出的规范进去;冲突或条件未知的整组不进自动召回。 */
|
|
1175
|
+
const groups = [...view.resolveTopics(layers.project, layers.global, context).values()];
|
|
1028
1176
|
const records = [];
|
|
1029
1177
|
const layerOf = new Map();
|
|
1030
|
-
for (const group of
|
|
1178
|
+
for (const group of groups) {
|
|
1031
1179
|
if (group.status !== 'ok' || !group.record) continue;
|
|
1032
1180
|
records.push(group.record);
|
|
1033
1181
|
layerOf.set(group.record.id, group.scope === 'global' ? 'global' : 'project');
|
|
1034
1182
|
}
|
|
1183
|
+
const gateRuntime = runtime || (win && win.runtime) || null;
|
|
1184
|
+
const task = runtime && runtime.task ? runtime.task : null;
|
|
1035
1185
|
const ranked = view.rank(records, query, {
|
|
1036
|
-
limit: 8, requireApplicable: true, context,
|
|
1037
|
-
runtime: runtime || (win && win.runtime) || null,
|
|
1038
|
-
task: runtime && runtime.task ? runtime.task : null,
|
|
1186
|
+
limit: 8, requireApplicable: true, context, runtime: gateRuntime, task,
|
|
1039
1187
|
});
|
|
1040
1188
|
/** 普通记忆与实验规则共用一份自动注入预算:总条数和最终完整文本字节一起算。 */
|
|
1041
1189
|
const auto = win && win.autoBudget ? win.autoBudget : null;
|
|
@@ -1073,7 +1221,7 @@ export function apply(ctx, config) {
|
|
|
1073
1221
|
win.recallHeader = true;
|
|
1074
1222
|
}
|
|
1075
1223
|
lines.push(line);
|
|
1076
|
-
cards.push({
|
|
1224
|
+
cards.push({ revision, scope: card.scope, title: card.title });
|
|
1077
1225
|
}
|
|
1078
1226
|
const finalText = `${RECALL_HEADER}\n${lines.join('\n')}`;
|
|
1079
1227
|
if (auto) {
|
|
@@ -1085,8 +1233,35 @@ export function apply(ctx, config) {
|
|
|
1085
1233
|
experimentCards: auto.experimentCards || 0,
|
|
1086
1234
|
});
|
|
1087
1235
|
}
|
|
1088
|
-
|
|
1089
|
-
|
|
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 };
|
|
1090
1265
|
}
|
|
1091
1266
|
|
|
1092
1267
|
/* ---------------- 项目自进化:任务锚点、读取决策、上下文解析 ---------------- */
|
|
@@ -1296,8 +1471,8 @@ export function apply(ctx, config) {
|
|
|
1296
1471
|
? `;核对:${String(checkpoint.text).slice(0, 40)}`
|
|
1297
1472
|
: '';
|
|
1298
1473
|
const line = short
|
|
1299
|
-
? `- 「${record.title}」${conditions ? `(${conditions})` : ''}
|
|
1300
|
-
: `- 「${record.title}
|
|
1474
|
+
? `- 「${record.title}」${conditions ? `(${conditions})` : ''}:${body}${check}`
|
|
1475
|
+
: `- 「${record.title}」(用途:${record.summary || record.title};要用时 memory_read 带标题取正文)`;
|
|
1301
1476
|
const size = Buffer.byteLength(`\n${line}`);
|
|
1302
1477
|
if (used + size > autoCap) break;
|
|
1303
1478
|
used += size;
|
|
@@ -1586,26 +1761,24 @@ export function apply(ctx, config) {
|
|
|
1586
1761
|
|
|
1587
1762
|
ctx.effect(() => tools.register({
|
|
1588
1763
|
name: 'memory_save',
|
|
1589
|
-
description: `保存一条记忆。只在会改变以后判断时才存;先搜已有条目,能更新就不要新建。保存前先蒸馏:标题是结论,正文只写原则与触发条件,通常 2–4 句;路径、字段表、完整命令、版本号、尺寸数值不要抄进正文,改成文档或 skill 的指针。用户明确说「记住」时直接调用;用户自己的话已经是长期规则(「以后都这样」「别再 X
|
|
1764
|
+
description: `保存一条记忆。只在会改变以后判断时才存;先搜已有条目,能更新就不要新建。保存前先蒸馏:标题是结论,正文只写原则与触发条件,通常 2–4 句;路径、字段表、完整命令、版本号、尺寸数值不要抄进正文,改成文档或 skill 的指针。用户明确说「记住」时直接调用;用户自己的话已经是长期规则(「以后都这样」「别再 X」)时也直接调用,不要先问。更新同一件事时用同一个标题:身份只看标题、旧标题和前置条件,像不像不授权覆盖。文本相近但标题不同时先不写,返回 needsResolution 与 1–2 条相近条目:确实是同一条就改成那个标题重存,确实是另一条才带 resolution="distinct" 再存。按标题更新时没给的字段保持原值,正文整段替换,旧正文只留作内部撤回,不展示也不参与检索。条数到硬上限时把同层最久没被读的一条移进归档,结果里写明动了哪条,不拒绝新记忆;单条正文超限只报错、不截断。前置条件不同的同一标题是两条。${NOTES}`,
|
|
1590
1765
|
parameters: SAVE_PARAMS,
|
|
1591
1766
|
output: {
|
|
1592
1767
|
presentationMeta: (_args, value) => ({
|
|
1593
1768
|
kind: 'memory-save',
|
|
1594
|
-
id: value.id || '',
|
|
1595
1769
|
title: value.title || '',
|
|
1596
1770
|
scope: value.scope || '',
|
|
1597
1771
|
revision: value.revision || 0,
|
|
1598
1772
|
changed: value.changed === true,
|
|
1599
1773
|
recordKind: typeof value.kind === 'string' && value.kind ? value.kind : '',
|
|
1600
1774
|
mode: value.needsScope ? 'needs_scope'
|
|
1601
|
-
: value.
|
|
1775
|
+
: value.existingTitle ? 'duplicate'
|
|
1602
1776
|
: value.needsResolution ? 'needs_resolution'
|
|
1603
1777
|
: value.changed === false ? 'unchanged' : value.merged ? 'updated' : 'created',
|
|
1604
1778
|
}),
|
|
1605
1779
|
schema: {
|
|
1606
1780
|
type: 'object',
|
|
1607
1781
|
properties: {
|
|
1608
|
-
id: { type: 'string' },
|
|
1609
1782
|
title: { type: 'string' },
|
|
1610
1783
|
summary: { type: 'string' },
|
|
1611
1784
|
when: { type: 'string' },
|
|
@@ -1620,34 +1793,35 @@ export function apply(ctx, config) {
|
|
|
1620
1793
|
needsResolution: { type: 'boolean' },
|
|
1621
1794
|
needsScope: { type: 'boolean' },
|
|
1622
1795
|
reason: { type: 'string' },
|
|
1623
|
-
|
|
1796
|
+
existingTitle: { type: 'string' },
|
|
1624
1797
|
existingScope: { type: 'string' },
|
|
1625
1798
|
crossLayer: { type: 'boolean' },
|
|
1799
|
+
crossScope: { type: 'string', enum: ['global', 'project'] },
|
|
1626
1800
|
sameTopic: { type: 'boolean' },
|
|
1627
|
-
|
|
1801
|
+
similarTitles: { type: 'string' },
|
|
1628
1802
|
similar: { type: 'string' },
|
|
1629
|
-
|
|
1803
|
+
supersededTitle: { type: 'string' },
|
|
1630
1804
|
archived: { type: 'string' },
|
|
1631
1805
|
pendingConflicts: { type: 'integer' },
|
|
1632
1806
|
},
|
|
1633
|
-
required: ['
|
|
1807
|
+
required: ['title', 'summary', 'when', 'revision', 'merged', 'changed', 'scope', 'needsResolution', 'archived'],
|
|
1634
1808
|
additionalProperties: false,
|
|
1635
1809
|
},
|
|
1636
1810
|
render: (_args, value) => [{
|
|
1637
1811
|
type: 'text',
|
|
1638
1812
|
text: value.needsScope
|
|
1639
1813
|
? value.reason === 'memory_disabled'
|
|
1640
|
-
?
|
|
1814
|
+
? `memory · ${memoryOffCopy(projectScope)}`
|
|
1641
1815
|
: value.reason === 'unconfirmed_project'
|
|
1642
1816
|
? `memory · 当前目录还不能确认属于哪个项目:「${value.title}」\n先确认项目根目录,或显式 scope="global"`
|
|
1643
1817
|
: `memory · 归属还没定:「${value.title}」\n补 memoryClass(personal_preference / project_fact / environment_fact / portable_procedure)或显式 scope;设备事实还要给设备条件`
|
|
1644
|
-
: value.
|
|
1818
|
+
: value.existingTitle
|
|
1645
1819
|
? `memory · 已有相同记忆:「${value.title}」(${value.existingScope})\n要改成另一条,加 resolution="distinct"`
|
|
1646
1820
|
: value.needsResolution
|
|
1647
|
-
? `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"`
|
|
1648
1822
|
: value.changed === false
|
|
1649
1823
|
? `memory · unchanged ${withTag(value, `"${value.title}"`)}`
|
|
1650
|
-
: `memory · ${value.merged ? 'merged' : 'saved'} ${withTag(value, `"${value.title}"`)}${value.when ? `(${value.when})` : ''}${value.similar ? ` · 相近:${value.similar}` : ''}${value.archived ? ` · 已归档「${value.archived}」腾位` : ''}${value.fullTextSaved ? ' · 完整原句已另存' : ''}${value.
|
|
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}」` : ''}`,
|
|
1651
1825
|
}],
|
|
1652
1826
|
},
|
|
1653
1827
|
execute: async (args, execution) => {
|
|
@@ -1655,15 +1829,44 @@ export function apply(ctx, config) {
|
|
|
1655
1829
|
const projectScopeName = projectNameOf(projectScope);
|
|
1656
1830
|
if (!memoryEnabled(projectScope)) {
|
|
1657
1831
|
return {
|
|
1658
|
-
|
|
1832
|
+
title: String(args.title || ''), summary: '', when: '', revision: 0,
|
|
1659
1833
|
merged: false, changed: false, scope: '', needsScope: true, reason: 'memory_disabled',
|
|
1660
|
-
|
|
1834
|
+
recoveryPending: controlStatus(projectScope).recoveryPending,
|
|
1835
|
+
needsResolution: false, similarTitles: '', similar: '', archived: '',
|
|
1661
1836
|
};
|
|
1662
1837
|
}
|
|
1663
|
-
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);
|
|
1664
1868
|
if (saved.needsScope) {
|
|
1665
1869
|
return {
|
|
1666
|
-
id: '',
|
|
1667
1870
|
title: saved.draft?.title || '',
|
|
1668
1871
|
summary: saved.draft?.summary || '',
|
|
1669
1872
|
when: saved.draft?.when || '',
|
|
@@ -1674,14 +1877,13 @@ export function apply(ctx, config) {
|
|
|
1674
1877
|
needsScope: true,
|
|
1675
1878
|
reason: saved.reasonCode,
|
|
1676
1879
|
needsResolution: false,
|
|
1677
|
-
|
|
1880
|
+
similarTitles: '',
|
|
1678
1881
|
similar: '',
|
|
1679
1882
|
archived: '',
|
|
1680
1883
|
};
|
|
1681
1884
|
}
|
|
1682
1885
|
if (saved.existing) {
|
|
1683
1886
|
return {
|
|
1684
|
-
id: saved.existing.id,
|
|
1685
1887
|
title: saved.existing.title || saved.draft?.title || '',
|
|
1686
1888
|
summary: '',
|
|
1687
1889
|
when: '',
|
|
@@ -1691,10 +1893,10 @@ export function apply(ctx, config) {
|
|
|
1691
1893
|
scope: saved.existing.scope,
|
|
1692
1894
|
scopeName: saved.existing.scope === 'global' ? '' : projectScopeName,
|
|
1693
1895
|
needsScope: false,
|
|
1694
|
-
|
|
1896
|
+
existingTitle: saved.existing.title || saved.draft?.title || '',
|
|
1695
1897
|
existingScope: saved.existing.scope,
|
|
1696
1898
|
needsResolution: false,
|
|
1697
|
-
|
|
1899
|
+
similarTitles: '',
|
|
1698
1900
|
similar: '',
|
|
1699
1901
|
archived: '',
|
|
1700
1902
|
};
|
|
@@ -1702,7 +1904,6 @@ export function apply(ctx, config) {
|
|
|
1702
1904
|
const scope = saved.chosenScope === 'global' || (args.scope === 'global' && !saved.chosenScope) ? globalScope() : projectScope;
|
|
1703
1905
|
if (saved.needsResolution) {
|
|
1704
1906
|
return {
|
|
1705
|
-
id: '',
|
|
1706
1907
|
title: saved.draft.title,
|
|
1707
1908
|
summary: saved.draft.summary,
|
|
1708
1909
|
when: saved.draft.when,
|
|
@@ -1713,8 +1914,9 @@ export function apply(ctx, config) {
|
|
|
1713
1914
|
needsScope: false,
|
|
1714
1915
|
needsResolution: true,
|
|
1715
1916
|
crossLayer: Boolean(saved.crossLayer),
|
|
1917
|
+
crossScope: saved.crossScope === 'global' ? 'global' : 'project',
|
|
1716
1918
|
sameTopic: Boolean(saved.sameTopic),
|
|
1717
|
-
|
|
1919
|
+
similarTitles: saved.similarList.map((item) => item.title).join('、'),
|
|
1718
1920
|
similar: saved.similarList.map((item) => describeCandidate(item, { sameTopic: saved.sameTopic })).join(';'),
|
|
1719
1921
|
archived: '',
|
|
1720
1922
|
pendingConflicts: Number(saved.pendingConflicts) || 0,
|
|
@@ -1722,7 +1924,6 @@ export function apply(ctx, config) {
|
|
|
1722
1924
|
}
|
|
1723
1925
|
recordCitations(scope, sessionIdOf(execution), [saved.record]);
|
|
1724
1926
|
return {
|
|
1725
|
-
id: saved.record.id,
|
|
1726
1927
|
title: saved.record.title,
|
|
1727
1928
|
summary: saved.record.summary,
|
|
1728
1929
|
when: saved.record.when || '',
|
|
@@ -1736,8 +1937,8 @@ export function apply(ctx, config) {
|
|
|
1736
1937
|
scopeName: scope.key === 'global' ? '' : projectScopeName,
|
|
1737
1938
|
needsScope: false,
|
|
1738
1939
|
needsResolution: false,
|
|
1739
|
-
|
|
1740
|
-
|
|
1940
|
+
similarTitles: saved.similarList.map((item) => item.title).join('、'),
|
|
1941
|
+
supersededTitle: supersedes && supersedesId ? supersedes : '',
|
|
1741
1942
|
archived: (saved.archived || []).map((item) => item.title).join('、'),
|
|
1742
1943
|
pendingConflicts: Number(saved.pendingConflicts) || 0,
|
|
1743
1944
|
...(saved.similar ? { similar: `「${saved.similar.title}」` } : {}),
|
|
@@ -1758,7 +1959,6 @@ export function apply(ctx, config) {
|
|
|
1758
1959
|
title: { type: 'string' },
|
|
1759
1960
|
id: { type: 'string' },
|
|
1760
1961
|
reason: { type: 'string' },
|
|
1761
|
-
targetId: { type: 'string' },
|
|
1762
1962
|
targetTitle: { type: 'string' },
|
|
1763
1963
|
},
|
|
1764
1964
|
required: ['asked'],
|
|
@@ -1767,7 +1967,7 @@ export function apply(ctx, config) {
|
|
|
1767
1967
|
render: (_args, value) => [{
|
|
1768
1968
|
type: 'text',
|
|
1769
1969
|
text: value.asked
|
|
1770
|
-
? `memory · suggested "${value.title}"${value.
|
|
1970
|
+
? `memory · suggested "${value.title}"${value.targetTitle ? ` · 确认后更新「${value.targetTitle}」` : ''}`
|
|
1771
1971
|
: `memory · skipped${value.reason ? `\n${value.reason}` : ''}`,
|
|
1772
1972
|
}],
|
|
1773
1973
|
},
|
|
@@ -1781,14 +1981,14 @@ export function apply(ctx, config) {
|
|
|
1781
1981
|
asked: true,
|
|
1782
1982
|
title: noted.title,
|
|
1783
1983
|
id: noted.id,
|
|
1784
|
-
...(noted.
|
|
1984
|
+
...(noted.targetTitle ? { targetTitle: noted.targetTitle } : {}),
|
|
1785
1985
|
};
|
|
1786
1986
|
},
|
|
1787
1987
|
}), 'lite-memory: memory_suggest');
|
|
1788
1988
|
|
|
1789
1989
|
ctx.effect(() => tools.register({
|
|
1790
1990
|
name: 'memory_search',
|
|
1791
|
-
description: `按需检索已有记忆:先搜,命中再 memory_read 读正文。用户提到「记住了什么 / 上次 / 规范 / 约定 / 路径 / 凭据怎么取」,或者眼前反复出现的报错、弹窗、异常时,先查这里有没有记过解法。跨项目的通用条目以 [global] 开头,项目条目以 [项目名]
|
|
1991
|
+
description: `按需检索已有记忆:先搜,命中再 memory_read 读正文。用户提到「记住了什么 / 上次 / 规范 / 约定 / 路径 / 凭据怎么取」,或者眼前反复出现的报错、弹窗、异常时,先查这里有没有记过解法。跨项目的通用条目以 [global] 开头,项目条目以 [项目名] 开头。中文长句可以直接当查询。每行给标题与适用范围;要更新就用那个标题重存。默认只列当前有效的;已经归档或被更新替换的旧说法不混进来,要查历史就在参数里显式要 history。`,
|
|
1792
1992
|
parameters: SEARCH_PARAMS,
|
|
1793
1993
|
output: {
|
|
1794
1994
|
presentationMeta: (_args, value) => ({
|
|
@@ -1809,7 +2009,6 @@ export function apply(ctx, config) {
|
|
|
1809
2009
|
items: {
|
|
1810
2010
|
type: 'object',
|
|
1811
2011
|
properties: {
|
|
1812
|
-
id: { type: 'string' },
|
|
1813
2012
|
title: { type: 'string' },
|
|
1814
2013
|
scope: { type: 'string' },
|
|
1815
2014
|
revision: { type: 'integer' },
|
|
@@ -1819,7 +2018,7 @@ export function apply(ctx, config) {
|
|
|
1819
2018
|
when: { type: 'string' },
|
|
1820
2019
|
updated: { type: 'string' }, // ui-copy-ignore
|
|
1821
2020
|
},
|
|
1822
|
-
required: ['
|
|
2021
|
+
required: ['title', 'scope', 'revision', 'status', 'origin', 'kind', 'when'],
|
|
1823
2022
|
additionalProperties: false,
|
|
1824
2023
|
},
|
|
1825
2024
|
},
|
|
@@ -1830,7 +2029,7 @@ export function apply(ctx, config) {
|
|
|
1830
2029
|
render: (_args, value) => [{
|
|
1831
2030
|
type: 'text',
|
|
1832
2031
|
text: value.status === 'budget_exhausted'
|
|
1833
|
-
? 'memory ·
|
|
2032
|
+
? 'memory · 这条消息已停止检索,下一条消息再查'
|
|
1834
2033
|
: value.status === 'memory_disabled'
|
|
1835
2034
|
? 'memory · 记忆检索已关闭'
|
|
1836
2035
|
: value.status === 'needs_context'
|
|
@@ -1844,7 +2043,7 @@ export function apply(ctx, config) {
|
|
|
1844
2043
|
if (recallMode() === 'off' || !memoryEnabledFor(execution)) return { count: 0, chars: 0, text: '', status: 'memory_disabled', refs: [] };
|
|
1845
2044
|
const agent = execution?.agent;
|
|
1846
2045
|
const win = windowOf(agent);
|
|
1847
|
-
if (win && !
|
|
2046
|
+
if (win && !affordableTool(win, 0)) return { count: 0, chars: 0, text: '', status: 'budget_exhausted', refs: [] };
|
|
1848
2047
|
const wantsBind = args.bindForRequest === true;
|
|
1849
2048
|
const contextId = wantsBind && !args.contextId ? toolContextFor(execution) : args.contextId;
|
|
1850
2049
|
if (wantsBind && !contextId) {
|
|
@@ -1895,8 +2094,9 @@ export function apply(ctx, config) {
|
|
|
1895
2094
|
};
|
|
1896
2095
|
if (win) {
|
|
1897
2096
|
const bytes = jsonBytes(value);
|
|
1898
|
-
if (!
|
|
2097
|
+
if (!affordableTool(win, bytes)) return { ...value, status: 'budget_exhausted', text: '' };
|
|
1899
2098
|
win.bytes += bytes;
|
|
2099
|
+
win.toolBytes += bytes;
|
|
1900
2100
|
win.reads += 1;
|
|
1901
2101
|
}
|
|
1902
2102
|
return value;
|
|
@@ -1906,18 +2106,20 @@ export function apply(ctx, config) {
|
|
|
1906
2106
|
const empty = { count: 0, chars: 0, text: '', status: 'ok', reasonCode: 'no_match', noTriggerReason: '没有匹配的记忆', refs: [] };
|
|
1907
2107
|
if (win) {
|
|
1908
2108
|
const bytes = jsonBytes(empty);
|
|
1909
|
-
if (!
|
|
2109
|
+
if (!affordableTool(win, bytes)) return { ...empty, status: 'budget_exhausted' };
|
|
1910
2110
|
win.bytes += bytes;
|
|
2111
|
+
win.toolBytes += bytes;
|
|
1911
2112
|
win.reads += 1;
|
|
1912
2113
|
}
|
|
1913
2114
|
return empty;
|
|
1914
2115
|
}
|
|
1915
2116
|
if (win) {
|
|
1916
2117
|
let kept = shown.length;
|
|
1917
|
-
while (kept > 0 && !
|
|
2118
|
+
while (kept > 0 && !affordableTool(win, jsonBytes(valueOf(kept)))) kept -= 1;
|
|
1918
2119
|
if (kept === 0) return { count: 0, chars: 0, text: '', status: 'budget_exhausted', refs: [] };
|
|
1919
2120
|
const value = valueOf(kept);
|
|
1920
2121
|
win.bytes += jsonBytes(value);
|
|
2122
|
+
win.toolBytes += jsonBytes(value);
|
|
1921
2123
|
win.reads += 1;
|
|
1922
2124
|
recordCitations(scope, sessionIdOf(execution), hits.slice(0, kept));
|
|
1923
2125
|
return value;
|
|
@@ -1930,13 +2132,12 @@ export function apply(ctx, config) {
|
|
|
1930
2132
|
|
|
1931
2133
|
ctx.effect(() => tools.register({
|
|
1932
2134
|
name: 'memory_read',
|
|
1933
|
-
description: '
|
|
2135
|
+
description: '读一条记忆的正文。搜索结果里只有一行摘要,需要细节时按标题读。索引里还有、正文文件丢了会直接报错,不会返回空正文。',
|
|
1934
2136
|
parameters: READ_PARAMS,
|
|
1935
2137
|
output: {
|
|
1936
2138
|
schema: {
|
|
1937
2139
|
type: 'object',
|
|
1938
2140
|
properties: {
|
|
1939
|
-
id: { type: 'string' },
|
|
1940
2141
|
title: { type: 'string' },
|
|
1941
2142
|
when: { type: 'string' },
|
|
1942
2143
|
revision: { type: 'integer' },
|
|
@@ -1949,13 +2150,13 @@ export function apply(ctx, config) {
|
|
|
1949
2150
|
links: { type: 'string' },
|
|
1950
2151
|
repeat: { type: 'boolean' },
|
|
1951
2152
|
},
|
|
1952
|
-
required: ['
|
|
2153
|
+
required: ['scope', 'status'],
|
|
1953
2154
|
additionalProperties: false,
|
|
1954
2155
|
},
|
|
1955
2156
|
render: (_args, value) => [{
|
|
1956
2157
|
type: 'text',
|
|
1957
2158
|
text: value.status === 'budget_exhausted'
|
|
1958
|
-
? 'memory ·
|
|
2159
|
+
? 'memory · 这条消息已停止读记忆,下一条消息再读'
|
|
1959
2160
|
: value.status === 'memory_disabled'
|
|
1960
2161
|
? 'memory · 记忆检索已关闭'
|
|
1961
2162
|
: value.repeat
|
|
@@ -1976,7 +2177,6 @@ export function apply(ctx, config) {
|
|
|
1976
2177
|
}],
|
|
1977
2178
|
presentationMeta: (_args, value) => ({
|
|
1978
2179
|
kind: 'memory-read',
|
|
1979
|
-
id: value.id || '',
|
|
1980
2180
|
title: value.title || '',
|
|
1981
2181
|
scope: value.scope || '',
|
|
1982
2182
|
revision: value.revision || 0,
|
|
@@ -1986,11 +2186,13 @@ export function apply(ctx, config) {
|
|
|
1986
2186
|
}),
|
|
1987
2187
|
},
|
|
1988
2188
|
execute: async (args, execution) => {
|
|
1989
|
-
if (recallMode() === 'off' || !memoryEnabledFor(execution)) return {
|
|
2189
|
+
if (recallMode() === 'off' || !memoryEnabledFor(execution)) return { scope: '', status: 'memory_disabled' };
|
|
1990
2190
|
const resolved = resolveRequestContext(execution, args.contextId);
|
|
1991
|
-
if (!resolved.ok) return {
|
|
2191
|
+
if (!resolved.ok) return { scope: '', status: resolved.status || 'needs_context' };
|
|
1992
2192
|
const scope = resolved.scope;
|
|
1993
2193
|
const scopeName = scope.key === 'global' ? '' : projectNameOf(scope);
|
|
2194
|
+
/** 明确指定层时只在那一层解析标题,同名条目不会串到别的层。 */
|
|
2195
|
+
const readScope = args.scope === 'global' ? globalScope() : scope;
|
|
1994
2196
|
const context = resolved.context;
|
|
1995
2197
|
const allowExperimental = resolved.experimental === true;
|
|
1996
2198
|
const agent = execution?.agent;
|
|
@@ -1998,48 +2200,57 @@ export function apply(ctx, config) {
|
|
|
1998
2200
|
const gateRuntime = resolved.held || win?.runtime
|
|
1999
2201
|
|| { projectId: scope.key, context, experimental: resolved.experimental === true };
|
|
2000
2202
|
if (args.view === 'links') {
|
|
2001
|
-
const
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
}
|
|
2005
|
-
const found = readLinks(
|
|
2006
|
-
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()}」这条记忆`);
|
|
2007
2209
|
if (found.status !== 'ok') {
|
|
2008
|
-
const withheld = {
|
|
2210
|
+
const withheld = { scope: '', status: found.status, links: '' };
|
|
2009
2211
|
if (win) {
|
|
2010
2212
|
const bytes = jsonBytes(withheld);
|
|
2011
|
-
if (!
|
|
2213
|
+
if (!affordableTool(win, bytes)) return { scope: '', status: 'budget_exhausted' };
|
|
2012
2214
|
win.bytes += bytes;
|
|
2215
|
+
win.toolBytes += bytes;
|
|
2013
2216
|
win.reads += 1;
|
|
2014
2217
|
}
|
|
2015
2218
|
return withheld;
|
|
2016
2219
|
}
|
|
2017
2220
|
const rows = found.links.filter((link) => link.status === 'invalid' || link.status === 'ok')
|
|
2018
2221
|
.map((link) => (link.status === 'invalid'
|
|
2019
|
-
?
|
|
2020
|
-
: `- ${link.scope === 'global' ? '[global] ' : (scopeName ? `[${scopeName}] ` : '')}「${link.title}
|
|
2222
|
+
? '- 有一条已失效'
|
|
2223
|
+
: `- ${link.scope === 'global' ? '[global] ' : (scopeName ? `[${scopeName}] ` : '')}「${link.title}」`));
|
|
2021
2224
|
const text = rows.join('\n') || '(无)';
|
|
2022
|
-
const value = {
|
|
2225
|
+
const value = { scope: found.scope, scopeName, status: found.status, links: text };
|
|
2023
2226
|
if (win) {
|
|
2024
2227
|
const bytes = jsonBytes(value);
|
|
2025
|
-
if (!
|
|
2228
|
+
if (!affordableTool(win, bytes)) return { scope: '', status: 'budget_exhausted' };
|
|
2026
2229
|
win.bytes += bytes;
|
|
2230
|
+
win.toolBytes += bytes;
|
|
2027
2231
|
win.reads += 1;
|
|
2028
2232
|
}
|
|
2029
2233
|
return value;
|
|
2030
2234
|
}
|
|
2031
|
-
const record = readEntry(
|
|
2032
|
-
|
|
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: '' };
|
|
2033
2245
|
const gate = view.eligibility(record, gateRuntime, 'read');
|
|
2034
|
-
if (!gate.ok) return {
|
|
2035
|
-
/**
|
|
2036
|
-
if (record.status !== 'ok') {
|
|
2037
|
-
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: '' };
|
|
2038
2250
|
}
|
|
2039
2251
|
const revision = record.revision || 1;
|
|
2040
2252
|
const body = String(record.body ?? '');
|
|
2041
2253
|
const value = {
|
|
2042
|
-
id: record.id,
|
|
2043
2254
|
title: record.title,
|
|
2044
2255
|
when: record.when || '',
|
|
2045
2256
|
revision,
|
|
@@ -2050,19 +2261,21 @@ export function apply(ctx, config) {
|
|
|
2050
2261
|
body,
|
|
2051
2262
|
};
|
|
2052
2263
|
if (win) {
|
|
2053
|
-
if (!
|
|
2264
|
+
if (!affordableTool(win, 0)) return { scope: '', status: 'budget_exhausted' };
|
|
2054
2265
|
const seenRevision = win.readBodies.get(record.id);
|
|
2055
2266
|
if (seenRevision === revision) {
|
|
2056
2267
|
const receipt = { ...value, body: '', repeat: true };
|
|
2057
2268
|
const bytes = jsonBytes(receipt);
|
|
2058
|
-
if (!
|
|
2269
|
+
if (!affordableTool(win, bytes)) return { scope: '', status: 'budget_exhausted' };
|
|
2059
2270
|
win.bytes += bytes;
|
|
2271
|
+
win.toolBytes += bytes;
|
|
2060
2272
|
win.reads += 1;
|
|
2061
2273
|
return receipt;
|
|
2062
2274
|
}
|
|
2063
2275
|
const bytes = jsonBytes(value);
|
|
2064
|
-
if (!
|
|
2276
|
+
if (!affordableTool(win, bytes, 1)) return { scope: '', status: 'budget_exhausted' };
|
|
2065
2277
|
win.bytes += bytes;
|
|
2278
|
+
win.toolBytes += bytes;
|
|
2066
2279
|
win.bodies += 1;
|
|
2067
2280
|
win.reads += 1;
|
|
2068
2281
|
win.readBodies.set(record.id, revision);
|
|
@@ -2080,13 +2293,12 @@ export function apply(ctx, config) {
|
|
|
2080
2293
|
schema: {
|
|
2081
2294
|
type: 'object',
|
|
2082
2295
|
properties: {
|
|
2083
|
-
id: { type: 'string' },
|
|
2084
2296
|
title: { type: 'string' },
|
|
2085
2297
|
scope: { type: 'string' },
|
|
2086
2298
|
scopeName: { type: 'string' },
|
|
2087
2299
|
mode: { type: 'string', enum: ['archive', 'purged', 'needs_resolution', 'purge_pending', 'memory_disabled'] },
|
|
2088
2300
|
},
|
|
2089
|
-
required: ['
|
|
2301
|
+
required: ['mode'],
|
|
2090
2302
|
additionalProperties: false,
|
|
2091
2303
|
},
|
|
2092
2304
|
render: (_args, value) => [{
|
|
@@ -2103,7 +2315,6 @@ export function apply(ctx, config) {
|
|
|
2103
2315
|
}],
|
|
2104
2316
|
presentationMeta: (_args, value) => ({
|
|
2105
2317
|
kind: 'memory-forget',
|
|
2106
|
-
id: value.id || '',
|
|
2107
2318
|
title: value.title || '',
|
|
2108
2319
|
scope: value.scope || '',
|
|
2109
2320
|
mode: value.mode,
|
|
@@ -2112,25 +2323,33 @@ export function apply(ctx, config) {
|
|
|
2112
2323
|
execute: async (args, execution) => {
|
|
2113
2324
|
const scope = scopeOf(cwdOf(execution));
|
|
2114
2325
|
const projectScopeName = projectNameOf(scope);
|
|
2115
|
-
if (!memoryEnabled(scope)) return {
|
|
2116
|
-
const
|
|
2117
|
-
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()}」这条记忆`);
|
|
2118
2338
|
if (!args.mode) {
|
|
2119
|
-
return {
|
|
2339
|
+
return { title: located.record.title || '', mode: 'needs_resolution', scope: located.scope.key === 'global' ? 'global' : 'project' };
|
|
2120
2340
|
}
|
|
2121
2341
|
let dropped;
|
|
2122
2342
|
try {
|
|
2123
|
-
dropped = forget(scope, args.
|
|
2343
|
+
dropped = forget(scope, refId, onlyLayer ? { mode: args.mode, layers: [onlyLayer] } : { mode: args.mode });
|
|
2124
2344
|
} catch (error) {
|
|
2125
2345
|
if (error && error.code === 'purge_pending') {
|
|
2126
2346
|
const layer = located ? located.scope : scope;
|
|
2127
2347
|
try {
|
|
2128
2348
|
state.resumePurge(layer);
|
|
2129
|
-
dropped = forget(scope, args.
|
|
2349
|
+
dropped = forget(scope, refId, onlyLayer ? { mode: args.mode, layers: [onlyLayer] } : { mode: args.mode });
|
|
2130
2350
|
} catch (again) {
|
|
2131
2351
|
if (again && again.code === 'purge_pending') {
|
|
2132
2352
|
return {
|
|
2133
|
-
id: args.id,
|
|
2134
2353
|
title: located ? located.record.title : '',
|
|
2135
2354
|
mode: 'purge_pending',
|
|
2136
2355
|
scope: layer.key === 'global' ? 'global' : 'project',
|
|
@@ -2143,11 +2362,10 @@ export function apply(ctx, config) {
|
|
|
2143
2362
|
throw error;
|
|
2144
2363
|
}
|
|
2145
2364
|
}
|
|
2146
|
-
if (!dropped) throw new Error(
|
|
2365
|
+
if (!dropped) throw new Error(`没有「${String(args.title || args.id || '').trim()}」这条记忆`);
|
|
2147
2366
|
const layerKey = dropped.scopeKey || (located ? located.scope.key : scope.key);
|
|
2148
2367
|
if (dropped.mode === 'purged') revokeLearningUse(layerKey);
|
|
2149
2368
|
return {
|
|
2150
|
-
id: dropped.id,
|
|
2151
2369
|
title: dropped.title || (located ? located.record.title : ''),
|
|
2152
2370
|
mode: dropped.mode,
|
|
2153
2371
|
scope: layerKey === 'global' ? 'global' : 'project',
|
|
@@ -2286,7 +2504,7 @@ export function apply(ctx, config) {
|
|
|
2286
2504
|
const ids = fresh.map((message) => String(message?.id ?? '')).filter(Boolean).sort();
|
|
2287
2505
|
const win = openWindow(agent, { sessionId, turn, messageIds: ids });
|
|
2288
2506
|
const budgetBefore = win ? {
|
|
2289
|
-
bytes: win.bytes, bodies: win.bodies, reads: win.reads,
|
|
2507
|
+
bytes: win.bytes, bodies: win.bodies, reads: win.reads, toolBytes: win.toolBytes || 0,
|
|
2290
2508
|
provided: new Map(win.provided), readBodies: new Map(win.readBodies),
|
|
2291
2509
|
recallHeader: win.recallHeader, experimentBytes: win.experimentBytes,
|
|
2292
2510
|
autoBudget: win.autoBudget ? JSON.parse(JSON.stringify(win.autoBudget)) : null,
|
|
@@ -2306,6 +2524,7 @@ export function apply(ctx, config) {
|
|
|
2306
2524
|
}
|
|
2307
2525
|
const scope = scopeOf(cwd);
|
|
2308
2526
|
if (!memoryEnabled(scope)) {
|
|
2527
|
+
noteRecall(scope, sessionId, { reason: 'memory_off' });
|
|
2309
2528
|
clearPrepared(agent);
|
|
2310
2529
|
return decision;
|
|
2311
2530
|
}
|
|
@@ -2318,6 +2537,7 @@ export function apply(ctx, config) {
|
|
|
2318
2537
|
if (win && runtime) win.runtime = runtime.held;
|
|
2319
2538
|
rememberRuntime(scope, sessionId, runtime && runtime.held);
|
|
2320
2539
|
if (recallMode() !== 'adaptive') {
|
|
2540
|
+
noteRecall(scope, sessionId, { reason: 'tools_only' });
|
|
2321
2541
|
clearPrepared(agent);
|
|
2322
2542
|
return decision;
|
|
2323
2543
|
}
|
|
@@ -2334,12 +2554,16 @@ export function apply(ctx, config) {
|
|
|
2334
2554
|
? rememberFeedback(scope, sessionId, fresh, runtime.held, turn)
|
|
2335
2555
|
: null;
|
|
2336
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
|
+
});
|
|
2337
2560
|
/** 没发布出去的投影要退还额度:不能因为一次超时把本请求的召回额度耗掉。 */
|
|
2338
2561
|
const rollbackBudget = () => {
|
|
2339
2562
|
if (!win || !budgetBefore) return;
|
|
2340
2563
|
win.bytes = budgetBefore.bytes;
|
|
2341
2564
|
win.bodies = budgetBefore.bodies;
|
|
2342
2565
|
win.reads = budgetBefore.reads;
|
|
2566
|
+
win.toolBytes = budgetBefore.toolBytes;
|
|
2343
2567
|
win.provided = budgetBefore.provided;
|
|
2344
2568
|
win.readBodies = budgetBefore.readBodies;
|
|
2345
2569
|
win.recallHeader = budgetBefore.recallHeader;
|
|
@@ -2362,7 +2586,7 @@ export function apply(ctx, config) {
|
|
|
2362
2586
|
return decision;
|
|
2363
2587
|
}
|
|
2364
2588
|
/** 实验上下文即使本轮零注入也要留着:工具绑定后同一请求还能补上规则。 */
|
|
2365
|
-
if (!snapshot && !experiment) {
|
|
2589
|
+
if ((!snapshot || !snapshot.cards.length) && !experiment) {
|
|
2366
2590
|
rollbackBudget();
|
|
2367
2591
|
clearPrepared(agent);
|
|
2368
2592
|
return decision;
|
|
@@ -2370,7 +2594,7 @@ export function apply(ctx, config) {
|
|
|
2370
2594
|
let evolutionEpoch = '';
|
|
2371
2595
|
try { evolutionEpoch = String(evolutionStatus(scope).epoch || ''); } catch { evolutionEpoch = ''; }
|
|
2372
2596
|
const state = {
|
|
2373
|
-
text: snapshot ? snapshot.text : '',
|
|
2597
|
+
text: snapshot && snapshot.cards.length ? snapshot.text : '',
|
|
2374
2598
|
experimentText: experiment ? experiment.text : '',
|
|
2375
2599
|
experimentParts: experiment && experiment.held ? (experiment.held.experimentParts || null) : null,
|
|
2376
2600
|
validUntil: experiment && experiment.held ? (experiment.held.validUntil || '') : '',
|
|
@@ -2535,7 +2759,7 @@ export function apply(ctx, config) {
|
|
|
2535
2759
|
if ((feedbackRevisions.get(scope.key) || 0) !== candidate.feedbackRevision) {
|
|
2536
2760
|
throw new Error('反馈已更新,这轮结果没采用');
|
|
2537
2761
|
}
|
|
2538
|
-
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 });
|
|
2539
2763
|
if (!parsed.ok) throw new Error(parsed.reason);
|
|
2540
2764
|
/** 用户已经把要求说成长期规则,模型仍返回空时至少留一条规则,不让这条反馈无声消失。 */
|
|
2541
2765
|
const changes = parsed.changes.length
|