dsh-lite-memory 0.18.19 → 0.18.26
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/TRIGGERS.md +1 -1
- package/client.js +136 -28
- package/docs/CONVENTIONS.md +10 -1
- package/docs/RELEASE-0.18.16.md +6 -0
- package/docs/RELEASE-0.18.17.md +6 -0
- package/docs/RELEASE-0.18.18.md +7 -0
- package/docs/RELEASE-0.18.19.md +7 -0
- package/docs/RELEASE-0.18.20.md +4 -0
- package/docs/RELEASE-0.18.21.md +4 -0
- package/docs/RELEASE-0.18.22.md +5 -0
- package/docs/RELEASE-0.18.23.md +5 -0
- package/docs/RELEASE-0.18.24.md +3 -0
- package/docs/RELEASE-0.18.25.md +5 -0
- package/docs/RELEASE-0.18.26.md +5 -0
- package/index.js +168 -35
- package/learning.js +3 -2
- package/package.json +1 -1
- package/state.js +11 -6
- package/store.js +84 -13
- package/test/client.test.mjs +25 -1
- package/test/contract.test.mjs +130 -1
- package/test/control.test.mjs +22 -0
- package/test/evolution-read.test.mjs +2 -0
- package/test/learning.test.mjs +3 -2
- package/test/lexical.test.mjs +22 -0
- package/test/recall.test.mjs +53 -0
- package/test/state.test.mjs +8 -5
- package/test/store.test.mjs +50 -3
- package/view.js +129 -16
package/index.js
CHANGED
|
@@ -29,6 +29,8 @@ import {
|
|
|
29
29
|
evolutionStatus,
|
|
30
30
|
controlStatus,
|
|
31
31
|
setMemoryEnabled,
|
|
32
|
+
setMemoryControl,
|
|
33
|
+
setRecallMode,
|
|
32
34
|
isExperimentalRecord,
|
|
33
35
|
setEvolution,
|
|
34
36
|
taskState,
|
|
@@ -83,6 +85,9 @@ function cwdOf(execution) {
|
|
|
83
85
|
return process.cwd();
|
|
84
86
|
}
|
|
85
87
|
|
|
88
|
+
/** 插件配置的默认触发方式,面板要按项目实际生效值显示。 */
|
|
89
|
+
let hostRecallMode = 'adaptive';
|
|
90
|
+
|
|
86
91
|
/** 本项目记忆总开关:读不出来时不擅自关掉,按开处理。 */
|
|
87
92
|
function memoryEnabled(scope) {
|
|
88
93
|
try {
|
|
@@ -168,6 +173,12 @@ function noteRecall(scope, sessionId, receipt) {
|
|
|
168
173
|
while (recallReceipts.size > 64) recallReceipts.delete(recallReceipts.keys().next().value);
|
|
169
174
|
}
|
|
170
175
|
|
|
176
|
+
/** 投影没发出去就撤销回执:面板不能显示一条模型从没见过的提醒。 */
|
|
177
|
+
function clearRecall(scope, sessionId) {
|
|
178
|
+
if (!scope || typeof sessionId !== 'string' || !sessionId) return;
|
|
179
|
+
recallReceipts.delete(`${scope.key}\u0000${sessionId}`);
|
|
180
|
+
}
|
|
181
|
+
|
|
171
182
|
function recallReceiptOf(scope, sessionId) {
|
|
172
183
|
if (!scope || typeof sessionId !== 'string' || !sessionId) return null;
|
|
173
184
|
return recallReceipts.get(`${scope.key}\u0000${sessionId}`) || null;
|
|
@@ -290,6 +301,8 @@ function keepHandler(webServer, cwdForSession) {
|
|
|
290
301
|
scope: found.scope,
|
|
291
302
|
scopeLabel: found.scopeLabel || '',
|
|
292
303
|
revision: found.revision,
|
|
304
|
+
applicability: found.applicability || 'all_in_scope',
|
|
305
|
+
conditions: Array.isArray(found.conditions) ? found.conditions : [],
|
|
293
306
|
body: found.body,
|
|
294
307
|
currentBody: typeof found.currentBody === 'string' ? found.currentBody : found.body,
|
|
295
308
|
versions,
|
|
@@ -328,8 +341,15 @@ function keepHandler(webServer, cwdForSession) {
|
|
|
328
341
|
return sendJson(res, 409, { ok: false, reason: '这条记忆的版本不清楚,先刷新再改' });
|
|
329
342
|
}
|
|
330
343
|
if (!title.trim()) return sendJson(res, 400, { ok: false, reason: '标题不能为空' });
|
|
344
|
+
/** 面板只让人确认「适用范围」这一件事实,不开放条件编辑;取值由 view 再校验。 */
|
|
345
|
+
const applicability = typeof body.applicability === 'string' ? body.applicability : undefined;
|
|
346
|
+
const conditions = Array.isArray(body.conditions) ? body.conditions : undefined;
|
|
331
347
|
try {
|
|
332
|
-
const result = saveEntry(scope, {
|
|
348
|
+
const result = saveEntry(scope, {
|
|
349
|
+
id, title, body: text, expectedRevision, scope: 'project', origin: 'user_explicit',
|
|
350
|
+
...(applicability !== undefined ? { applicability } : {}),
|
|
351
|
+
...(conditions !== undefined ? { conditions } : {}),
|
|
352
|
+
});
|
|
333
353
|
if (!result || !result.record) {
|
|
334
354
|
const reason = result && result.reasonCode === 'memory_disabled' ? '本项目记忆已关闭'
|
|
335
355
|
: result && result.needsResolution ? '改动和另一条太像,先去那条上改'
|
|
@@ -448,7 +468,7 @@ function keepHandler(webServer, cwdForSession) {
|
|
|
448
468
|
selection: record.selection || null,
|
|
449
469
|
links: Array.isArray(record.links) ? record.links.slice(0, 24).map((link) => ({ type: link.type || 'related', id: link.id })) : [],
|
|
450
470
|
revision: Number(record.revision) || 1,
|
|
451
|
-
origin: record.origin
|
|
471
|
+
origin: originOut(record.origin),
|
|
452
472
|
updated: record.updated || '',
|
|
453
473
|
lastUsed: record.lastUsed || '',
|
|
454
474
|
bytes: Number(record.bytes) || 0,
|
|
@@ -478,10 +498,14 @@ function keepHandler(webServer, cwdForSession) {
|
|
|
478
498
|
const currentControl = (() => {
|
|
479
499
|
try { return controlStatus(scope); } catch { return { enabled: true, epoch: '' }; }
|
|
480
500
|
})();
|
|
501
|
+
const modeOf = (target) => {
|
|
502
|
+
try { return { effectiveMode: controlStatus(target).mode || hostRecallMode }; } catch { return { effectiveMode: hostRecallMode }; }
|
|
503
|
+
};
|
|
504
|
+
currentControl.effectiveMode = modeOf(scope).effectiveMode;
|
|
481
505
|
const selectedControl = selectedScope.key === scope.key
|
|
482
506
|
? currentControl
|
|
483
507
|
: (() => {
|
|
484
|
-
try { return controlStatus(selectedScope); } catch { return { enabled: true, epoch: '' }; }
|
|
508
|
+
try { return { ...controlStatus(selectedScope), ...modeOf(selectedScope) }; } catch { return { enabled: true, epoch: '', effectiveMode: recallMode() }; }
|
|
485
509
|
})();
|
|
486
510
|
const selectedEvolution = selectedScope.key === scope.key
|
|
487
511
|
? currentEvolution
|
|
@@ -529,8 +553,11 @@ function keepHandler(webServer, cwdForSession) {
|
|
|
529
553
|
if (!scope.confirmed) return sendJson(res, 409, { ok: false, reason: '项目根还没确认,先重新打开这个项目' });
|
|
530
554
|
const operationId = typeof body.operationId === 'string' ? body.operationId : '';
|
|
531
555
|
try {
|
|
532
|
-
const result = typeof body.enabled === 'boolean'
|
|
533
|
-
?
|
|
556
|
+
const result = typeof body.enabled === 'boolean' || typeof body.mode === 'string'
|
|
557
|
+
? setMemoryControl(scope, {
|
|
558
|
+
...(typeof body.enabled === 'boolean' ? { enabled: body.enabled } : {}),
|
|
559
|
+
...(typeof body.mode === 'string' ? { mode: body.mode } : {}),
|
|
560
|
+
}, { operationId })
|
|
534
561
|
: { ok: true, ...controlStatus(scope) };
|
|
535
562
|
if (typeof body.enabled === 'boolean' && result && result.ok !== false && body.enabled === false) {
|
|
536
563
|
const controller = learningAborts.get(scope.key);
|
|
@@ -730,13 +757,26 @@ function keepHandler(webServer, cwdForSession) {
|
|
|
730
757
|
}
|
|
731
758
|
|
|
732
759
|
/** 给界面的事件引用:只有定位与版次,不带正文,避免把维护字段当内容画出来。 */
|
|
760
|
+
/** 保存失败的统一回执:字段与成功回执同形,渲染只报这一次没写成。 */
|
|
761
|
+
function saveFailure(message, args) {
|
|
762
|
+
return {
|
|
763
|
+
title: String(args && args.title || '').trim(), summary: '', when: '', revision: 0,
|
|
764
|
+
merged: false, changed: false, scope: '', needsResolution: false, needsScope: false,
|
|
765
|
+
archived: '', error: String(message || '').replace(/^memory · /, ''),
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
function originOut(value) {
|
|
770
|
+
return value === 'project_evolution' || value === 'model_drafted' ? value : 'user_explicit';
|
|
771
|
+
}
|
|
772
|
+
|
|
733
773
|
function refsOf(hits) {
|
|
734
774
|
return hits.map((item) => ({
|
|
735
775
|
title: item.title || '',
|
|
736
776
|
scope: item.scope || 'project',
|
|
737
777
|
revision: item.revision || 1,
|
|
738
778
|
status: item.status || 'ok',
|
|
739
|
-
origin: item.origin
|
|
779
|
+
origin: originOut(item.origin),
|
|
740
780
|
/** 界面要按类型分开展示(偏好/约定/流程/资料),维护字段不进正文。 */
|
|
741
781
|
kind: typeof item.kind === 'string' && item.kind ? item.kind : 'note',
|
|
742
782
|
when: typeof item.when === 'string' ? item.when : '',
|
|
@@ -764,7 +804,7 @@ const SAVE_PARAMS = {
|
|
|
764
804
|
enum: ['distinct'],
|
|
765
805
|
description: '明确表示「这确实是另一条」。返回 needsResolution 时,只有确认不是同一件事才带上它再存一次',
|
|
766
806
|
},
|
|
767
|
-
scope: { type: 'string', enum: ['auto', 'project', 'global'], description: '存哪一层:auto
|
|
807
|
+
scope: { type: 'string', enum: ['auto', 'project', 'global'], description: '存哪一层:auto 按记忆类别判定(默认);只有用户这一次明确说「以后所有项目都这样」才用 global,模型自己判断通用不是理由;明确说只在这个项目用 project。本机路径/端口和项目事实即使写 global 也会留在项目层,降级原因见 scopeNote;只在本机成立的命令/流程声明 machine 或 runtime 条件后可进全局层' },
|
|
768
808
|
memoryClass: { type: 'string', enum: ['personal_preference', 'project_fact', 'environment_fact', 'portable_procedure'], description: '归属提案:个人偏好 / 项目事实 / 设备事实 / 可搬流程。服务端据此判定存哪层;设备事实必须同时给设备条件' },
|
|
769
809
|
durability: { type: 'string', enum: ['durable', 'task'], description: '一般认为 durable;只是一次性任务参数用 task,服务端会拒绝落盘' },
|
|
770
810
|
evidenceRef: { type: 'string', description: '归属判定的依据(哪条消息、哪个文件、哪次观察);用户没说范围时用来判断能不能存' },
|
|
@@ -975,7 +1015,8 @@ function statusLabel(status) {
|
|
|
975
1015
|
*/
|
|
976
1016
|
export const RECALL_MODES = ['adaptive', 'tool_only', 'off'];
|
|
977
1017
|
const RECALL_QUERY_CHARS = 400;
|
|
978
|
-
|
|
1018
|
+
/** 单轮投影的耗时上限:慢机器上宁可这轮不注入,也不把回合拖住。测试可临时放宽。 */
|
|
1019
|
+
let recallDeadlineMs = 150;
|
|
979
1020
|
const RECALL_HEADER = '已有记忆可能相关(需要细节用 memory_read 带标题取正文,别凭标题猜):';
|
|
980
1021
|
|
|
981
1022
|
/** 卡片只放够判断要不要读的几行;必要前提整条在场,装不下就整卡省略。 */
|
|
@@ -1020,6 +1061,16 @@ export function apply(ctx, config) {
|
|
|
1020
1061
|
|
|
1021
1062
|
/** 升级不覆盖用户的选择:默认 adaptive,别的模式只由明确配置切换。 */
|
|
1022
1063
|
const recallMode = () => (RECALL_MODES.includes(config?.mode) ? config.mode : 'adaptive');
|
|
1064
|
+
hostRecallMode = recallMode();
|
|
1065
|
+
/** 项目自己的选择优先;没有写过就跟随插件配置,升级不改用户选择。 */
|
|
1066
|
+
const modeFor = (scope) => {
|
|
1067
|
+
try {
|
|
1068
|
+
const mode = controlStatus(scope).mode;
|
|
1069
|
+
return RECALL_MODES.includes(mode) ? mode : recallMode();
|
|
1070
|
+
} catch {
|
|
1071
|
+
return recallMode();
|
|
1072
|
+
}
|
|
1073
|
+
};
|
|
1023
1074
|
/** 已准备好的快照只挂在 Agent 上,弱引用随手回收,不跨会话串。 */
|
|
1024
1075
|
const prepared = new WeakMap();
|
|
1025
1076
|
/** turn-stopping 只回 signal,不带头 agent;这里记 signal → agent 供收尾清理。 */
|
|
@@ -1190,6 +1241,7 @@ export function apply(ctx, config) {
|
|
|
1190
1241
|
/** 卡片上限含包装;共享请求账本也要把标题行算进去,不能只算卡片行。 */
|
|
1191
1242
|
let headerCharged = win ? win.recallHeader === true : false;
|
|
1192
1243
|
const lines = [];
|
|
1244
|
+
const linesMeta = [];
|
|
1193
1245
|
const cards = [];
|
|
1194
1246
|
for (const row of ranked) {
|
|
1195
1247
|
const usedCards = cards.length + (auto ? (auto.experimentCards || 0) : 0);
|
|
@@ -1222,6 +1274,8 @@ export function apply(ctx, config) {
|
|
|
1222
1274
|
}
|
|
1223
1275
|
lines.push(line);
|
|
1224
1276
|
cards.push({ revision, scope: card.scope, title: card.title });
|
|
1277
|
+
/** 卡片行与到期时间一起留到最后输出:中间隔了系统提示组装,不能拿投影时的判断当现在。 */
|
|
1278
|
+
linesMeta.push({ line, validUntil: row.record.validUntil || '' });
|
|
1225
1279
|
}
|
|
1226
1280
|
const finalText = `${RECALL_HEADER}\n${lines.join('\n')}`;
|
|
1227
1281
|
if (auto) {
|
|
@@ -1243,6 +1297,8 @@ export function apply(ctx, config) {
|
|
|
1243
1297
|
}
|
|
1244
1298
|
let waiting = near.some((row) => row.status !== 'ok' || row.record.reviewState === 'legacy_unreviewed');
|
|
1245
1299
|
let nearMatched = near.length;
|
|
1300
|
+
/** 冲突与「条件未确认」是两种原因:前者要人先裁决,后者只差一句任务说明。 */
|
|
1301
|
+
let conflictNear = false;
|
|
1246
1302
|
if (!cards.length && !ranked.length) {
|
|
1247
1303
|
/** 整组被条件或冲突挡下的候选没进 records,单独按同一查询算;只计数,不投放。 */
|
|
1248
1304
|
for (const group of groups) {
|
|
@@ -1252,16 +1308,18 @@ export function apply(ctx, config) {
|
|
|
1252
1308
|
});
|
|
1253
1309
|
if (!hits.length) continue;
|
|
1254
1310
|
waiting = true;
|
|
1311
|
+
if (group.status === 'conflict') conflictNear = true;
|
|
1255
1312
|
nearMatched += hits.length;
|
|
1256
1313
|
}
|
|
1257
1314
|
}
|
|
1258
1315
|
const reason = cards.length ? 'hit'
|
|
1259
1316
|
: ranked.length ? 'budget'
|
|
1260
|
-
:
|
|
1261
|
-
:
|
|
1317
|
+
: conflictNear ? 'conflict'
|
|
1318
|
+
: waiting ? 'needs_context'
|
|
1319
|
+
: (gateRuntime && gateRuntime.enabled ? 'no_overlap' : 'needs_context');
|
|
1262
1320
|
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 };
|
|
1321
|
+
if (!cards.length) return { text: '', cards: [], lines: [], bytes: 0, query, reason, matched };
|
|
1322
|
+
return { text: finalText, cards, lines: linesMeta, bytes: Buffer.byteLength(finalText), query, reason, matched };
|
|
1265
1323
|
}
|
|
1266
1324
|
|
|
1267
1325
|
/* ---------------- 项目自进化:任务锚点、读取决策、上下文解析 ---------------- */
|
|
@@ -1462,7 +1520,16 @@ export function apply(ctx, config) {
|
|
|
1462
1520
|
? Math.max(0, view.CARD.bytes - Buffer.byteLength(auto.normalText || '') - 2)
|
|
1463
1521
|
: EXPERIMENT_BYTES;
|
|
1464
1522
|
let used = Buffer.byteLength(EXPERIMENT_RULES_HEADER) + 1;
|
|
1465
|
-
|
|
1523
|
+
/** 实验规则也要过本轮相关度:和当前问题没有交集就不占卡片,不能“来自自进化”就天然优先。 */
|
|
1524
|
+
const query = held && held.query ? String(held.query) : '';
|
|
1525
|
+
const relevant = query
|
|
1526
|
+
? matched.map((record) => ({ record, score: view.matchScore(record, query).score }))
|
|
1527
|
+
/** 条件明确且已满足的规则按绑定注入;无条件的全项目规则必须和本轮问题有交集。 */
|
|
1528
|
+
.filter((row) => row.score > 0 || (Array.isArray(row.record.conditions) && row.record.conditions.length > 0))
|
|
1529
|
+
.sort((a, b) => b.score - a.score)
|
|
1530
|
+
.map((row) => row.record)
|
|
1531
|
+
: matched;
|
|
1532
|
+
for (const record of relevant.slice(0, cardCap)) {
|
|
1466
1533
|
const body = String(record.body || '');
|
|
1467
1534
|
const short = body && body.length <= EXPERIMENT_SHORT_CHARS && body.split('\n').length <= 4;
|
|
1468
1535
|
const conditions = conditionLabel(record.conditions);
|
|
@@ -1752,7 +1819,23 @@ export function apply(ctx, config) {
|
|
|
1752
1819
|
}
|
|
1753
1820
|
/** 没有逐条快照时才退回整段到期判断;有快照就只撤到期的那一条。 */
|
|
1754
1821
|
if (evolutionText && !state.experimentParts && state.validUntil && Date.now() > Date.parse(String(state.validUntil))) evolutionText = '';
|
|
1755
|
-
|
|
1822
|
+
/** 普通卡片也在输出前逐条复核到期:投影到真正发给模型之间可能已经过了 validUntil。 */
|
|
1823
|
+
let normalText = state.text;
|
|
1824
|
+
if (Array.isArray(state.lines)) {
|
|
1825
|
+
const now = Date.now();
|
|
1826
|
+
const live = state.lines.filter((row) => !row.validUntil || now <= Date.parse(String(row.validUntil)));
|
|
1827
|
+
normalText = live.length ? `${RECALL_HEADER}\n${live.map((row) => row.line).join('\n')}` : '';
|
|
1828
|
+
/** 投影时还在、发出时已过期:回执跟着真实输出改,别在面板上说「提醒了 2 条」而正文是空的。 */
|
|
1829
|
+
if (state.scope && live.length !== state.cards.length) {
|
|
1830
|
+
noteRecall(state.scope, state.sessionId, {
|
|
1831
|
+
reason: live.length ? 'hit' : 'expired',
|
|
1832
|
+
matched: state.matched || state.cards.length,
|
|
1833
|
+
cards: live.length,
|
|
1834
|
+
chars: live.length ? view.bytesOf(normalText) : 0,
|
|
1835
|
+
});
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
return [evolutionText, normalText].filter(Boolean).join('\n\n');
|
|
1756
1839
|
} catch {
|
|
1757
1840
|
return '';
|
|
1758
1841
|
}
|
|
@@ -1771,7 +1854,8 @@ export function apply(ctx, config) {
|
|
|
1771
1854
|
revision: value.revision || 0,
|
|
1772
1855
|
changed: value.changed === true,
|
|
1773
1856
|
recordKind: typeof value.kind === 'string' && value.kind ? value.kind : '',
|
|
1774
|
-
mode: value.
|
|
1857
|
+
mode: value.error ? 'failed'
|
|
1858
|
+
: value.needsScope ? 'needs_scope'
|
|
1775
1859
|
: value.existingTitle ? 'duplicate'
|
|
1776
1860
|
: value.needsResolution ? 'needs_resolution'
|
|
1777
1861
|
: value.changed === false ? 'unchanged' : value.merged ? 'updated' : 'created',
|
|
@@ -1800,16 +1884,20 @@ export function apply(ctx, config) {
|
|
|
1800
1884
|
sameTopic: { type: 'boolean' },
|
|
1801
1885
|
similarTitles: { type: 'string' },
|
|
1802
1886
|
similar: { type: 'string' },
|
|
1887
|
+
scopeNote: { type: 'string' },
|
|
1803
1888
|
supersededTitle: { type: 'string' },
|
|
1804
1889
|
archived: { type: 'string' },
|
|
1805
1890
|
pendingConflicts: { type: 'integer' },
|
|
1891
|
+
error: { type: 'string' },
|
|
1806
1892
|
},
|
|
1807
1893
|
required: ['title', 'summary', 'when', 'revision', 'merged', 'changed', 'scope', 'needsResolution', 'archived'],
|
|
1808
1894
|
additionalProperties: false,
|
|
1809
1895
|
},
|
|
1810
1896
|
render: (_args, value) => [{
|
|
1811
1897
|
type: 'text',
|
|
1812
|
-
text: value.
|
|
1898
|
+
text: value.error
|
|
1899
|
+
? `memory · ${value.error}`
|
|
1900
|
+
: value.needsScope
|
|
1813
1901
|
? value.reason === 'memory_disabled'
|
|
1814
1902
|
? `memory · ${memoryOffCopy(projectScope)}`
|
|
1815
1903
|
: value.reason === 'unconfirmed_project'
|
|
@@ -1821,7 +1909,7 @@ export function apply(ctx, config) {
|
|
|
1821
1909
|
? `memory · 需要确认:「${value.title}」与已有条目相近${value.crossLayer ? '(在另一层)' : ''}${value.sameTopic ? '(同一主题槽位)' : ''}\n${value.similar}\n要更新它:用同一个标题${value.crossScope === 'global' ? '、并带 scope="global"' : ''}再存一次;确实是另一条,加 resolution="distinct"`
|
|
1822
1910
|
: value.changed === false
|
|
1823
1911
|
? `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}」` : ''}`,
|
|
1912
|
+
: `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}」` : ''}${value.scopeNote ? ` · ${value.scopeNote}` : ''}`,
|
|
1825
1913
|
}],
|
|
1826
1914
|
},
|
|
1827
1915
|
execute: async (args, execution) => {
|
|
@@ -1831,7 +1919,6 @@ export function apply(ctx, config) {
|
|
|
1831
1919
|
return {
|
|
1832
1920
|
title: String(args.title || ''), summary: '', when: '', revision: 0,
|
|
1833
1921
|
merged: false, changed: false, scope: '', needsScope: true, reason: 'memory_disabled',
|
|
1834
|
-
recoveryPending: controlStatus(projectScope).recoveryPending,
|
|
1835
1922
|
needsResolution: false, similarTitles: '', similar: '', archived: '',
|
|
1836
1923
|
};
|
|
1837
1924
|
}
|
|
@@ -1839,17 +1926,18 @@ export function apply(ctx, config) {
|
|
|
1839
1926
|
const supersedesId = supersedes && !/^mem_[a-z0-9]+$/.test(supersedes)
|
|
1840
1927
|
? (() => { try { return refIdOf(projectScope, { title: supersedes }, '记忆'); } catch { return ''; } })()
|
|
1841
1928
|
: supersedes;
|
|
1842
|
-
|
|
1929
|
+
/** 工具写入的正文由模型写,来源标成模型整理,不能冒充用户原话。 */
|
|
1930
|
+
const draft = { ...args, writeMode: args.mode || 'patch', origin: 'model_drafted' };
|
|
1843
1931
|
if (supersedes) { if (supersedesId) draft.supersedes = supersedesId; else delete draft.supersedes; }
|
|
1844
1932
|
/** 更新句柄也用标题:id 只是底层实现,模型抄标题就行。 */
|
|
1845
1933
|
const updateTitle = String(args.updateTitle || '').trim();
|
|
1846
1934
|
if (updateTitle) {
|
|
1847
1935
|
const candidates = args.scope ? [] : titleCandidates(projectScope, updateTitle);
|
|
1848
1936
|
if (candidates.length > 1) {
|
|
1849
|
-
return {
|
|
1937
|
+
return saveFailure(`「${updateTitle}」在项目层和全局层各有一条:改全局加 scope="global",改项目加 scope="project"`, args);
|
|
1850
1938
|
}
|
|
1851
1939
|
const updateId = (() => { try { return refIdOf(projectScope, { title: updateTitle, scope: args.scope }, '记忆'); } catch { return ''; } })();
|
|
1852
|
-
if (!updateId) return {
|
|
1940
|
+
if (!updateId) return saveFailure(`没有「${updateTitle}」这条记忆;要新增就不用 updateTitle`, args);
|
|
1853
1941
|
draft.id = updateId;
|
|
1854
1942
|
}
|
|
1855
1943
|
/** 关联目标同样按标题解析成内部 id 再落盘。 */
|
|
@@ -1857,9 +1945,9 @@ export function apply(ctx, config) {
|
|
|
1857
1945
|
const resolved = [];
|
|
1858
1946
|
for (const link of args.links) {
|
|
1859
1947
|
const title = typeof link?.title === 'string' ? link.title.trim() : '';
|
|
1860
|
-
if (!title) return
|
|
1948
|
+
if (!title) return saveFailure('关联要写成 {type, title}:给被关联那条记忆的标题', args);
|
|
1861
1949
|
const targetId = (() => { try { return refIdOf(projectScope, { title }, '记忆'); } catch { return ''; } })();
|
|
1862
|
-
if (!targetId) return {
|
|
1950
|
+
if (!targetId) return saveFailure(`没有「${title}」这条记忆,关联没写成`, args);
|
|
1863
1951
|
resolved.push({ type: link.type || 'related', id: targetId });
|
|
1864
1952
|
}
|
|
1865
1953
|
draft.links = resolved;
|
|
@@ -1902,6 +1990,13 @@ export function apply(ctx, config) {
|
|
|
1902
1990
|
};
|
|
1903
1991
|
}
|
|
1904
1992
|
const scope = saved.chosenScope === 'global' || (args.scope === 'global' && !saved.chosenScope) ? globalScope() : projectScope;
|
|
1993
|
+
/** 显式要了全局却被硬冲突挡回项目层时说清原因:否则模型只能看到 scope=project,无从纠。 */
|
|
1994
|
+
const scopeNote = (() => {
|
|
1995
|
+
if (args.scope !== 'global' || scope.key === 'global') return '';
|
|
1996
|
+
if (saved.reasonCode === 'machine_bound_scope') return '按本机痕迹留在项目层:正文或依据里写了本机路径/端口,改成别的项目仍成立的通用规则再存全局;只在本机适用的流程可补 machine 条件后存全局';
|
|
1997
|
+
if (saved.reasonCode === 'model_global_project_fact') return '按项目事实留在项目层:换成别的项目就不成立;确实是通用流程就改 memoryClass';
|
|
1998
|
+
return '';
|
|
1999
|
+
})();
|
|
1905
2000
|
if (saved.needsResolution) {
|
|
1906
2001
|
return {
|
|
1907
2002
|
title: saved.draft.title,
|
|
@@ -1919,6 +2014,7 @@ export function apply(ctx, config) {
|
|
|
1919
2014
|
similarTitles: saved.similarList.map((item) => item.title).join('、'),
|
|
1920
2015
|
similar: saved.similarList.map((item) => describeCandidate(item, { sameTopic: saved.sameTopic })).join(';'),
|
|
1921
2016
|
archived: '',
|
|
2017
|
+
...(scopeNote ? { scopeNote } : {}),
|
|
1922
2018
|
pendingConflicts: Number(saved.pendingConflicts) || 0,
|
|
1923
2019
|
};
|
|
1924
2020
|
}
|
|
@@ -1938,6 +2034,7 @@ export function apply(ctx, config) {
|
|
|
1938
2034
|
needsScope: false,
|
|
1939
2035
|
needsResolution: false,
|
|
1940
2036
|
similarTitles: saved.similarList.map((item) => item.title).join('、'),
|
|
2037
|
+
...(scopeNote ? { scopeNote } : {}),
|
|
1941
2038
|
supersededTitle: supersedes && supersedesId ? supersedes : '',
|
|
1942
2039
|
archived: (saved.archived || []).map((item) => item.title).join('、'),
|
|
1943
2040
|
pendingConflicts: Number(saved.pendingConflicts) || 0,
|
|
@@ -2040,7 +2137,7 @@ export function apply(ctx, config) {
|
|
|
2040
2137
|
}],
|
|
2041
2138
|
},
|
|
2042
2139
|
execute: async (args, execution) => {
|
|
2043
|
-
if (
|
|
2140
|
+
if (!memoryEnabledFor(execution) || modeFor(scopeOf(cwdOf(execution))) === 'off') return { count: 0, chars: 0, text: '', status: 'memory_disabled', refs: [] };
|
|
2044
2141
|
const agent = execution?.agent;
|
|
2045
2142
|
const win = windowOf(agent);
|
|
2046
2143
|
if (win && !affordableTool(win, 0)) return { count: 0, chars: 0, text: '', status: 'budget_exhausted', refs: [] };
|
|
@@ -2143,7 +2240,7 @@ export function apply(ctx, config) {
|
|
|
2143
2240
|
revision: { type: 'integer' },
|
|
2144
2241
|
scope: { type: 'string' },
|
|
2145
2242
|
scopeName: { type: 'string' },
|
|
2146
|
-
origin: { type: 'string', enum: ['user_explicit', 'project_evolution'] },
|
|
2243
|
+
origin: { type: 'string', enum: ['user_explicit', 'model_drafted', 'project_evolution'] },
|
|
2147
2244
|
kind: { type: 'string' },
|
|
2148
2245
|
status: { type: 'string', enum: ['ok', 'archived', 'needs_context', 'not_applicable', 'gone', 'budget_exhausted', 'memory_disabled'] },
|
|
2149
2246
|
body: { type: 'string' },
|
|
@@ -2180,13 +2277,13 @@ export function apply(ctx, config) {
|
|
|
2180
2277
|
title: value.title || '',
|
|
2181
2278
|
scope: value.scope || '',
|
|
2182
2279
|
revision: value.revision || 0,
|
|
2183
|
-
origin: value.origin
|
|
2280
|
+
origin: originOut(value.origin),
|
|
2184
2281
|
recordKind: typeof value.kind === 'string' && value.kind ? value.kind : '',
|
|
2185
2282
|
status: value.status || 'ok',
|
|
2186
2283
|
}),
|
|
2187
2284
|
},
|
|
2188
2285
|
execute: async (args, execution) => {
|
|
2189
|
-
if (
|
|
2286
|
+
if (!memoryEnabledFor(execution) || modeFor(scopeOf(cwdOf(execution))) === 'off') return { scope: '', status: 'memory_disabled' };
|
|
2190
2287
|
const resolved = resolveRequestContext(execution, args.contextId);
|
|
2191
2288
|
if (!resolved.ok) return { scope: '', status: resolved.status || 'needs_context' };
|
|
2192
2289
|
const scope = resolved.scope;
|
|
@@ -2222,7 +2319,7 @@ export function apply(ctx, config) {
|
|
|
2222
2319
|
? '- 有一条已失效'
|
|
2223
2320
|
: `- ${link.scope === 'global' ? '[global] ' : (scopeName ? `[${scopeName}] ` : '')}「${link.title}」`));
|
|
2224
2321
|
const text = rows.join('\n') || '(无)';
|
|
2225
|
-
const value = { scope: found.scope, scopeName, status: found.status, links: text };
|
|
2322
|
+
const value = { title: found.title || '', scope: found.scope, scopeName, status: found.status, links: text };
|
|
2226
2323
|
if (win) {
|
|
2227
2324
|
const bytes = jsonBytes(value);
|
|
2228
2325
|
if (!affordableTool(win, bytes)) return { scope: '', status: 'budget_exhausted' };
|
|
@@ -2258,6 +2355,8 @@ export function apply(ctx, config) {
|
|
|
2258
2355
|
scopeName,
|
|
2259
2356
|
status: record.status,
|
|
2260
2357
|
kind: record.kind || 'note',
|
|
2358
|
+
/** 来源随正文回执一起给:模型整理稿要能标出来,但不会因此变成全局。 */
|
|
2359
|
+
origin: originOut(record.origin),
|
|
2261
2360
|
body,
|
|
2262
2361
|
};
|
|
2263
2362
|
if (win) {
|
|
@@ -2488,6 +2587,7 @@ export function apply(ctx, config) {
|
|
|
2488
2587
|
if (snapshot || experimentText) {
|
|
2489
2588
|
previous.text = snapshot ? snapshot.text : '';
|
|
2490
2589
|
previous.cards = snapshot ? snapshot.cards : [];
|
|
2590
|
+
previous.lines = snapshot ? snapshot.lines : [];
|
|
2491
2591
|
previous.experimentText = experimentText;
|
|
2492
2592
|
previous.experimentParts = win && win.experiment ? (win.experiment.experimentParts || null) : null;
|
|
2493
2593
|
previous.validUntil = win && win.experiment ? (win.experiment.validUntil || '') : '';
|
|
@@ -2510,7 +2610,10 @@ export function apply(ctx, config) {
|
|
|
2510
2610
|
autoBudget: win.autoBudget ? JSON.parse(JSON.stringify(win.autoBudget)) : null,
|
|
2511
2611
|
} : null;
|
|
2512
2612
|
/** 工具模式也要记下本轮真人原话:只用于核对绑定,不自动注入。 */
|
|
2513
|
-
const
|
|
2613
|
+
const said = view.newestUserQuery(fresh).slice(0, RECALL_QUERY_CHARS);
|
|
2614
|
+
/** 「继续」这类续说没有检索词:带上同会话上一轮原话,否则这一轮必然召回为空。 */
|
|
2615
|
+
const carried = sameSession && previous?.query && view.isContinuationQuery(said) ? previous.query : '';
|
|
2616
|
+
const query = (carried ? `${said} ${carried}` : said).trim().slice(0, RECALL_QUERY_CHARS);
|
|
2514
2617
|
if (win && query) win.query = query;
|
|
2515
2618
|
|
|
2516
2619
|
if (!query) {
|
|
@@ -2533,10 +2636,10 @@ export function apply(ctx, config) {
|
|
|
2533
2636
|
* 工具模式也必须先有本请求上下文:memory_search/read/links 靠同一个 contextId 判项目和任务,
|
|
2534
2637
|
* 不能等到自动召回分支才建,否则工具模式永远拿不到可核对的任务。
|
|
2535
2638
|
*/
|
|
2536
|
-
const runtime = runtimeFor(scope, { sessionId, query, batchKey, messageIds: ids, autoRoute:
|
|
2639
|
+
const runtime = runtimeFor(scope, { sessionId, query, batchKey, messageIds: ids, autoRoute: modeFor(scope) === 'adaptive' });
|
|
2537
2640
|
if (win && runtime) win.runtime = runtime.held;
|
|
2538
2641
|
rememberRuntime(scope, sessionId, runtime && runtime.held);
|
|
2539
|
-
if (
|
|
2642
|
+
if (modeFor(scope) !== 'adaptive') {
|
|
2540
2643
|
noteRecall(scope, sessionId, { reason: 'tools_only' });
|
|
2541
2644
|
clearPrepared(agent);
|
|
2542
2645
|
return decision;
|
|
@@ -2553,7 +2656,30 @@ export function apply(ctx, config) {
|
|
|
2553
2656
|
const remembered = runtime && runtime.enabled
|
|
2554
2657
|
? rememberFeedback(scope, sessionId, fresh, runtime.held, turn)
|
|
2555
2658
|
: null;
|
|
2556
|
-
|
|
2659
|
+
let snapshot = projectRecall(scope, query, recalledContext, win, runtime ? runtime.held : null);
|
|
2660
|
+
/**
|
|
2661
|
+
* 预防型记忆约束的是「下一步动作」,词面常写在上一轮助手的话和工具调用里,用户这句话反而没提。
|
|
2662
|
+
* 第一次检索一张卡都没有时,用最近的实词/工具名补一次有界查询,预算仍是同一份。
|
|
2663
|
+
*/
|
|
2664
|
+
if (!snapshot.cards.length && !carried) {
|
|
2665
|
+
const humanTail = (() => {
|
|
2666
|
+
const messages = Array.isArray(decision.messages) ? decision.messages : [];
|
|
2667
|
+
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
2668
|
+
if (messages[i]?.source?.kind === 'user') return messages.slice(0, i);
|
|
2669
|
+
}
|
|
2670
|
+
return messages;
|
|
2671
|
+
})();
|
|
2672
|
+
const trail = view.actionTerms(humanTail, { limit: 12 }).slice(0, 240);
|
|
2673
|
+
if (trail) {
|
|
2674
|
+
const actionQuery = `${trail} ${query}`.trim().slice(0, RECALL_QUERY_CHARS);
|
|
2675
|
+
const retry = projectRecall(scope, actionQuery, recalledContext, win, runtime ? runtime.held : null);
|
|
2676
|
+
/** 补查是为了给出提醒:补不到就保留第一次的「为什么没提醒」,不拿空结果盖掉。 */
|
|
2677
|
+
if (retry.cards.length) {
|
|
2678
|
+
snapshot = retry;
|
|
2679
|
+
if (win) win.query = actionQuery;
|
|
2680
|
+
}
|
|
2681
|
+
}
|
|
2682
|
+
}
|
|
2557
2683
|
noteRecall(scope, sessionId, {
|
|
2558
2684
|
reason: snapshot.reason, matched: snapshot.matched, cards: snapshot.cards.length, chars: snapshot.bytes,
|
|
2559
2685
|
});
|
|
@@ -2572,16 +2698,18 @@ export function apply(ctx, config) {
|
|
|
2572
2698
|
win.experiment = null;
|
|
2573
2699
|
win.contexts.clear();
|
|
2574
2700
|
};
|
|
2575
|
-
if (generation !== generations.get(agent)) { rollbackBudget(); return decision; }
|
|
2701
|
+
if (generation !== generations.get(agent)) { rollbackBudget(); if (snapshot.cards.length) clearRecall(scope, sessionId); return decision; }
|
|
2576
2702
|
/** 投影期间开关被改过:丢弃这批内容,不把关闭前的记忆交给当前请求。 */
|
|
2577
2703
|
if (!memoryEnabled(scope) || (controlStatus(scope).epoch || '') !== controlEpoch) {
|
|
2578
2704
|
rollbackBudget();
|
|
2705
|
+
if (snapshot.cards.length) clearRecall(scope, sessionId);
|
|
2579
2706
|
clearPrepared(agent);
|
|
2580
2707
|
return decision;
|
|
2581
2708
|
}
|
|
2582
2709
|
/** 超时或被取消:这一步不留半旧卡片,下一真人请求重新投影。 */
|
|
2583
|
-
if (signal?.aborted || Date.now() - startedAt >
|
|
2710
|
+
if (signal?.aborted || Date.now() - startedAt > recallDeadlineMs) {
|
|
2584
2711
|
rollbackBudget();
|
|
2712
|
+
if (snapshot.cards.length) clearRecall(scope, sessionId);
|
|
2585
2713
|
clearPrepared(agent);
|
|
2586
2714
|
return decision;
|
|
2587
2715
|
}
|
|
@@ -2599,6 +2727,9 @@ export function apply(ctx, config) {
|
|
|
2599
2727
|
experimentParts: experiment && experiment.held ? (experiment.held.experimentParts || null) : null,
|
|
2600
2728
|
validUntil: experiment && experiment.held ? (experiment.held.validUntil || '') : '',
|
|
2601
2729
|
cards: snapshot ? snapshot.cards : [],
|
|
2730
|
+
lines: snapshot ? snapshot.lines : [],
|
|
2731
|
+
matched: snapshot ? snapshot.matched : 0,
|
|
2732
|
+
scope,
|
|
2602
2733
|
query,
|
|
2603
2734
|
sessionId,
|
|
2604
2735
|
turn,
|
|
@@ -2614,7 +2745,7 @@ export function apply(ctx, config) {
|
|
|
2614
2745
|
if (signal) signalAgents.set(signal, agent);
|
|
2615
2746
|
const key = agentKey(agent);
|
|
2616
2747
|
if (key && signal && typeof signal.addEventListener === 'function') {
|
|
2617
|
-
const onAbort = () => clearPrepared(agent);
|
|
2748
|
+
const onAbort = () => { clearPrepared(agent); clearRecall(scope, sessionId); };
|
|
2618
2749
|
signal.addEventListener('abort', onAbort, { once: true });
|
|
2619
2750
|
abortCleanups.set(key, () => signal.removeEventListener('abort', onAbort));
|
|
2620
2751
|
}
|
|
@@ -2914,6 +3045,8 @@ export function apply(ctx, config) {
|
|
|
2914
3045
|
mode: recallMode,
|
|
2915
3046
|
preparedOf: (agent) => prepared.get(agent) || null,
|
|
2916
3047
|
windowOf: (agent) => windowOf(agent),
|
|
3048
|
+
/** 测试专用:CI 冷启动偶尔超过 150ms,逻辑测试不该被耗时上限判失败。 */
|
|
3049
|
+
setDeadlineMs: (ms) => { if (Number.isFinite(ms) && ms > 0) recallDeadlineMs = ms; },
|
|
2917
3050
|
};
|
|
2918
3051
|
return () => {
|
|
2919
3052
|
delete globalThis.__liteMemoryRecall;
|
package/learning.js
CHANGED
|
@@ -44,7 +44,8 @@ const UNCLEAR = /不太对|再想想|感觉不对|说不上|有点怪|不对劲/
|
|
|
44
44
|
* 点名项目范围才算全项目授权;「以后都」不等于「所有项目」。
|
|
45
45
|
* 「所有口播」「一律」这种覆盖全部对象的说法也算,但只限当前项目层。
|
|
46
46
|
*/
|
|
47
|
-
const PROJECT_WIDE = /(
|
|
47
|
+
const PROJECT_WIDE = /(这个项目|本项目|所有项目|全项目|整个项目|全库|每个项目)/;
|
|
48
|
+
/** 「所有口播」「一律」限定的是对象,不是整个项目:没有可信范围时只算待确认。 */
|
|
48
49
|
|
|
49
50
|
const has = (pattern, text) => pattern.test(text);
|
|
50
51
|
|
|
@@ -380,7 +381,7 @@ export function pendingChangeFrom(feedback = {}) {
|
|
|
380
381
|
? (expected.applicability === 'all_in_scope'
|
|
381
382
|
? 'all_in_scope'
|
|
382
383
|
: expectedConditions.length ? 'conditional' : 'unresolved')
|
|
383
|
-
: /(
|
|
384
|
+
: /(这个项目|本项目|所有项目|全项目|整个项目|全库|每个项目)/.test(currentText)
|
|
384
385
|
? 'all_in_scope'
|
|
385
386
|
: taskKind ? 'conditional' : 'unresolved';
|
|
386
387
|
const conditions = expected
|
package/package.json
CHANGED
package/state.js
CHANGED
|
@@ -106,10 +106,12 @@ export function evolutionOf(state) {
|
|
|
106
106
|
export function controlOf(state) {
|
|
107
107
|
const raw = state && state.control;
|
|
108
108
|
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) {
|
|
109
|
-
return { enabled: true, epoch: '', changedAt: '', recoveryPending: false, compatible: true };
|
|
109
|
+
return { enabled: true, mode: '', epoch: '', changedAt: '', recoveryPending: false, compatible: true };
|
|
110
110
|
}
|
|
111
111
|
return {
|
|
112
112
|
enabled: raw.enabled !== false && raw.recoveryPending !== true,
|
|
113
|
+
/** 空字符串表示跟随插件配置;只有明确写过的项目才覆盖。 */
|
|
114
|
+
mode: raw.mode === 'adaptive' || raw.mode === 'tool_only' || raw.mode === 'off' ? raw.mode : '',
|
|
113
115
|
epoch: typeof raw.epoch === 'string' ? raw.epoch : '',
|
|
114
116
|
changedAt: typeof raw.changedAt === 'string' ? raw.changedAt : '',
|
|
115
117
|
recoveryPending: raw.recoveryPending === true,
|
|
@@ -122,12 +124,15 @@ export function controlOf(state) {
|
|
|
122
124
|
* 只清运行层(在途整理、临时锚点、旧召回快照)。重新打开也换代次,关闭前排队的任务不能补交。
|
|
123
125
|
* 自进化跟随总开关一起停:重新打开记忆后,自进化仍是关闭的,等用户单独开启。
|
|
124
126
|
*/
|
|
125
|
-
export function applyControl(draft, enabled) {
|
|
127
|
+
export function applyControl(draft, enabled, mode) {
|
|
126
128
|
const now = new Date().toISOString();
|
|
127
129
|
const current = controlOf(draft);
|
|
128
|
-
const next =
|
|
129
|
-
|
|
130
|
-
|
|
130
|
+
const next = typeof enabled === 'boolean' ? enabled : current.enabled;
|
|
131
|
+
const nextMode = mode === 'adaptive' || mode === 'tool_only' || mode === 'off'
|
|
132
|
+
? mode
|
|
133
|
+
: current.mode;
|
|
134
|
+
if (current.enabled === next && current.mode === nextMode) return { changed: false, enabled: next, mode: nextMode };
|
|
135
|
+
const config = { enabled: next, mode: nextMode, epoch: crypto.randomUUID(), changedAt: now };
|
|
131
136
|
draft.control = config;
|
|
132
137
|
const evolution = draft.experimental && draft.experimental.projectEvolution;
|
|
133
138
|
if (evolution && typeof evolution === 'object' && !Array.isArray(evolution)) {
|
|
@@ -141,7 +146,7 @@ export function applyControl(draft, enabled) {
|
|
|
141
146
|
/** 额度只增不减:次数与回执留着,最近观察清掉。 */
|
|
142
147
|
evolution.learning = { ...ledger, recent: [] };
|
|
143
148
|
}
|
|
144
|
-
return { changed: true, enabled: next, epoch: config.epoch };
|
|
149
|
+
return { changed: true, enabled: next, mode: nextMode, epoch: config.epoch };
|
|
145
150
|
}
|
|
146
151
|
|
|
147
152
|
/**
|