dsh-lite-memory 0.15.0 → 0.16.4
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 +4 -4
- package/anchor.js +55 -13
- package/client.js +60 -23
- package/docs/RELEASE-0.16.0.md +10 -0
- package/index.js +140 -39
- package/package.json +1 -1
- package/state.js +7 -5
- package/store.js +61 -35
- package/test/anchor.test.mjs +90 -0
- package/test/control.test.mjs +19 -0
- package/test/evolution-read.test.mjs +133 -3
- package/test/learning.test.mjs +1 -1
- package/test/recall.test.mjs +14 -0
- package/test/store.test.mjs +87 -19
package/index.js
CHANGED
|
@@ -635,8 +635,8 @@ const SAVE_PARAMS = {
|
|
|
635
635
|
properties: {
|
|
636
636
|
id: { type: 'string', description: '要更新的记忆 id(来自搜索结果或相近提示)。同一件事更新时用它,别另建一条' },
|
|
637
637
|
title: { type: 'string', description: `标题:名词短语,用户一眼能认出是哪件事,不超过 ${LIMITS.titleChars} 字。它就是这条记忆的身份` },
|
|
638
|
-
summary: { type: 'string', description:
|
|
639
|
-
body: { type: 'string', description: `正文 markdown,整段替换旧正文,不超过 ${LIMITS.entryBytes}
|
|
638
|
+
summary: { type: 'string', description: `一句话说清要记的内容本身——用户只看这一行决定留不留,名词短语最好,超长会自动截到这个长度` },
|
|
639
|
+
body: { type: 'string', description: `正文 markdown,整段替换旧正文,不超过 ${LIMITS.entryBytes} 字节。只写原则、判断规则、触发条件和边界;不写一次性数字或代码片段,这些查当前代码或文档。` },
|
|
640
640
|
when: { type: 'string', description: `前置条件或适用场景(「在哪台机器 / 哪个环境 / 什么情况下」),不超过 ${LIMITS.whenChars} 字。同一标题但前置条件不同,会存成两条,不合并;更新时不给就保留原值` },
|
|
641
641
|
kind: { type: 'string', enum: ['preference', 'convention', 'procedure', 'pointer', 'pitfall', 'credential_hint', 'note'], description: '条目类型;更新时不给就保留原值' },
|
|
642
642
|
tags: { type: 'array', items: { type: 'string' }, description: '最多 5 个标签;更新时不给就保留原值' },
|
|
@@ -1051,6 +1051,14 @@ export function apply(ctx, config) {
|
|
|
1051
1051
|
return text;
|
|
1052
1052
|
}
|
|
1053
1053
|
|
|
1054
|
+
/** 实验正文按条目记有效期:一条到期只撤一条,不把同请求里其他规则一起清掉。 */
|
|
1055
|
+
function experimentTextFrom(parts, now = Date.now()) {
|
|
1056
|
+
if (!parts || !Array.isArray(parts.items)) return '';
|
|
1057
|
+
const items = parts.items.filter((item) => !item.validUntil || now <= Date.parse(String(item.validUntil)));
|
|
1058
|
+
if (!items.length) return '';
|
|
1059
|
+
return `${parts.header}\n${items.map((item) => item.line).join('\n')}${parts.tail || ''}`;
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1054
1062
|
/** 本轮问题与栏目名的字面相关度:只展示有交集的栏目,交集为空就整段跳过。 */
|
|
1055
1063
|
function relevantRanges(names, query) {
|
|
1056
1064
|
const text = String(query || '').toLowerCase();
|
|
@@ -1070,8 +1078,19 @@ export function apply(ctx, config) {
|
|
|
1070
1078
|
});
|
|
1071
1079
|
}
|
|
1072
1080
|
|
|
1081
|
+
/** 实验正文只在「自进化开着,且还是同一代」时可用;关掉再开不能复活旧正文。 */
|
|
1082
|
+
function experimentStillCurrent(scope, held) {
|
|
1083
|
+
let evolution;
|
|
1084
|
+
try { evolution = evolutionStatus(scope); } catch { return false; }
|
|
1085
|
+
if (!evolution || evolution.enabled !== true || evolution.compatible === false) return false;
|
|
1086
|
+
if (held && held.epoch && String(evolution.epoch || '') !== String(held.epoch)) return false;
|
|
1087
|
+
return true;
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1073
1090
|
/** 读决策 → 一段短提示:未绑定只给栏目索引,已绑定才给规则正文。 */
|
|
1074
1091
|
function renderExperiment(scope, decision, context, held, win) {
|
|
1092
|
+
if (held && typeof held === 'object') { held.validUntil = ''; held.experimentParts = null; held.injectedIds = []; }
|
|
1093
|
+
if (!experimentStillCurrent(scope, held)) return '';
|
|
1075
1094
|
if (!decision || decision.kind === 'none') return '';
|
|
1076
1095
|
if (decision.kind === 'conflict') {
|
|
1077
1096
|
return chargeExperiment(win, '这些实验规则互相冲突,先按用户这次说的做,别叠加对立的规则。');
|
|
@@ -1095,6 +1114,8 @@ export function apply(ctx, config) {
|
|
|
1095
1114
|
if (record.reviewState === 'legacy_unreviewed') continue;
|
|
1096
1115
|
const learning = record.learning && typeof record.learning === 'object' ? record.learning : {};
|
|
1097
1116
|
if (learning.state !== 'active' && learning.state !== 'trial') continue;
|
|
1117
|
+
/** 试用到期、暂停或身份失效:自动注入与检索、读取用同一档可用性,不能各判各的。 */
|
|
1118
|
+
if (!view.experimentalUsable(record, Date.now())) continue;
|
|
1098
1119
|
const status = view.statusOf(record, context);
|
|
1099
1120
|
if (status === 'needs_context') { unknown.push(record); continue; }
|
|
1100
1121
|
if (status !== 'ok') continue;
|
|
@@ -1106,8 +1127,7 @@ export function apply(ctx, config) {
|
|
|
1106
1127
|
if (conflict) {
|
|
1107
1128
|
return chargeExperiment(win, '这些实验规则互相冲突,先按用户这次说的做,别叠加对立的规则。');
|
|
1108
1129
|
}
|
|
1109
|
-
const
|
|
1110
|
-
const shown = [];
|
|
1130
|
+
const parts = { header: EXPERIMENT_RULES_HEADER, items: [], tail: held && held.contextId ? `\ncontextId=${held.contextId}` : '' };
|
|
1111
1131
|
let used = Buffer.byteLength(EXPERIMENT_RULES_HEADER) + 1;
|
|
1112
1132
|
for (const record of matched.slice(0, 3)) {
|
|
1113
1133
|
const body = String(record.body || '');
|
|
@@ -1123,17 +1143,24 @@ export function apply(ctx, config) {
|
|
|
1123
1143
|
const size = Buffer.byteLength(`\n${line}`);
|
|
1124
1144
|
if (used + size > EXPERIMENT_BYTES) break;
|
|
1125
1145
|
used += size;
|
|
1126
|
-
|
|
1127
|
-
|
|
1146
|
+
const trialLearning = record.learning && typeof record.learning === 'object' ? record.learning : {};
|
|
1147
|
+
const trialUntil = trialLearning.state === 'trial' && Number.isFinite(Date.parse(String(trialLearning.trialUntil || '')))
|
|
1148
|
+
? String(trialLearning.trialUntil) : '';
|
|
1149
|
+
parts.items.push({ id: record.id, line, validUntil: trialUntil });
|
|
1150
|
+
}
|
|
1151
|
+
if (held) held.injectedIds = parts.items.map((item) => item.id);
|
|
1152
|
+
if (!parts.items.length && unknown.length) {
|
|
1153
|
+
/** 条件没确认的条目只报数量,绝不在模型侧暴露标题。 */
|
|
1154
|
+
return chargeExperiment(win, '有些实验规则的适用条件还没确认,先按用户这次说的做,别把它们当本轮依据。');
|
|
1128
1155
|
}
|
|
1129
|
-
if (
|
|
1130
|
-
if (
|
|
1131
|
-
|
|
1132
|
-
|
|
1156
|
+
if (!parts.items.length) return '';
|
|
1157
|
+
if (held) held.experimentParts = parts;
|
|
1158
|
+
/** 兼容旧读取点:整体最早到期只取本轮真正注入的条目,不再被无关试验拖下水。 */
|
|
1159
|
+
if (held) {
|
|
1160
|
+
const deadlines = parts.items.map((item) => Date.parse(String(item.validUntil))).filter(Number.isFinite);
|
|
1161
|
+
if (deadlines.length) held.validUntil = new Date(Math.min(...deadlines)).toISOString();
|
|
1133
1162
|
}
|
|
1134
|
-
|
|
1135
|
-
const tail = held && held.contextId ? `\ncontextId=${held.contextId}` : '';
|
|
1136
|
-
return chargeExperiment(win, `${EXPERIMENT_RULES_HEADER}\n${lines.join('\n')}${tail}`);
|
|
1163
|
+
return chargeExperiment(win, experimentTextFrom(parts));
|
|
1137
1164
|
}
|
|
1138
1165
|
|
|
1139
1166
|
/** 一次真人请求只路由一次;同一批次重试复用同一 transitionId。 */
|
|
@@ -1240,7 +1267,7 @@ export function apply(ctx, config) {
|
|
|
1240
1267
|
const contextId = anchor.contextIdFor(scope.key, batchKey, crypto.randomUUID().slice(0, 8));
|
|
1241
1268
|
const created = {
|
|
1242
1269
|
contextId, projectKey: scope.key, sessionId: win.sessionId, batchKey,
|
|
1243
|
-
messageIds: win.messageIds.slice(), context: view.trustedContext(scope, {}),
|
|
1270
|
+
messageIds: win.messageIds.slice(), query: trustedQueryOf(win), context: view.trustedContext(scope, {}),
|
|
1244
1271
|
anchor: null, ranges: rangesOf(scope), decision: { kind: 'none' }, epoch: status.epoch, at: Date.now(),
|
|
1245
1272
|
};
|
|
1246
1273
|
win.experiment = created;
|
|
@@ -1248,13 +1275,27 @@ export function apply(ctx, config) {
|
|
|
1248
1275
|
return contextId;
|
|
1249
1276
|
}
|
|
1250
1277
|
|
|
1278
|
+
/** 本轮真人消息是唯一可信的任务依据;模型自己写的 query 只用于检索词,不能拿来建绑定。 */
|
|
1279
|
+
function trustedQueryOf(win) {
|
|
1280
|
+
return win && typeof win.query === 'string' ? win.query : '';
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1251
1283
|
/** 前台绑定只升到 proposed:真正执行链还没发生,不冒充 active。 */
|
|
1252
|
-
function bindSelection(execution, resolved
|
|
1284
|
+
function bindSelection(execution, resolved) {
|
|
1253
1285
|
const held = resolved.held;
|
|
1286
|
+
if (!held || typeof held !== 'object') return { ok: false, status: 'needs_context', reason: '本轮还没有可核对的用户消息,先重新问一次当前任务' };
|
|
1287
|
+
const query = held.query || trustedQueryOf(resolved.win);
|
|
1288
|
+
if (!query) return { ok: false, status: 'needs_context', reason: '本轮还没有可核对的用户消息,先重新问一次当前任务' };
|
|
1254
1289
|
const ranges = held.ranges || { series: [], taskKinds: [] };
|
|
1290
|
+
/** 自动路由已经选了范围就沿用同一个结果,工具绑定不重新猜一遍,避免两处给出不同任务。 */
|
|
1291
|
+
const routedAnchor = held.anchor && typeof held.anchor === 'object' ? held.anchor : null;
|
|
1255
1292
|
const hits = [];
|
|
1256
|
-
|
|
1257
|
-
|
|
1293
|
+
if (routedAnchor && routedAnchor.seriesId) hits.push({ seriesId: routedAnchor.seriesId });
|
|
1294
|
+
else if (routedAnchor && routedAnchor.taskKind) hits.push({ taskKind: routedAnchor.taskKind });
|
|
1295
|
+
else {
|
|
1296
|
+
for (const value of ranges.series || []) if (anchor.seriesChosenBy(query, value)) hits.push({ seriesId: value });
|
|
1297
|
+
for (const value of ranges.taskKinds || []) if (anchor.seriesChosenBy(query, value)) hits.push({ taskKind: value });
|
|
1298
|
+
}
|
|
1258
1299
|
if (hits.length !== 1) {
|
|
1259
1300
|
const names = [...(ranges.series || []), ...(ranges.taskKinds || [])].slice(0, 3);
|
|
1260
1301
|
return { ok: false, status: 'needs_context', reason: names.length ? '本轮没有匹配的记忆范围,按当前任务继续' : '本轮没有适用的记忆范围,按当前任务继续' };
|
|
@@ -1276,7 +1317,11 @@ export function apply(ctx, config) {
|
|
|
1276
1317
|
resolved.context = held.context;
|
|
1277
1318
|
const text = renderExperiment(resolved.scope, held.decision, held.context, held, resolved.win);
|
|
1278
1319
|
const preparedState = prepared.get(execution && execution.agent);
|
|
1279
|
-
if (preparedState)
|
|
1320
|
+
if (preparedState) {
|
|
1321
|
+
preparedState.experimentText = text;
|
|
1322
|
+
preparedState.experimentParts = held.experimentParts || null;
|
|
1323
|
+
preparedState.validUntil = held.validUntil || '';
|
|
1324
|
+
}
|
|
1280
1325
|
return { ok: true };
|
|
1281
1326
|
}
|
|
1282
1327
|
|
|
@@ -1297,7 +1342,17 @@ export function apply(ctx, config) {
|
|
|
1297
1342
|
if (!memoryEnabled(scope)) return '';
|
|
1298
1343
|
const epoch = controlStatus(scope).epoch || '';
|
|
1299
1344
|
if (state.controlEpoch && epoch && state.controlEpoch !== epoch) return '';
|
|
1300
|
-
|
|
1345
|
+
/** 自进化关掉或换过代:关掉前的实验正文不再输出,重开也不复活。 */
|
|
1346
|
+
let evolutionText = state.experimentParts ? experimentTextFrom(state.experimentParts) : state.experimentText;
|
|
1347
|
+
if (evolutionText) {
|
|
1348
|
+
let evolution;
|
|
1349
|
+
try { evolution = evolutionStatus(scope); } catch { evolution = null; }
|
|
1350
|
+
if (!evolution || evolution.enabled !== true || evolution.compatible === false
|
|
1351
|
+
|| (state.evolutionEpoch && String(evolution.epoch || '') !== state.evolutionEpoch)) evolutionText = '';
|
|
1352
|
+
}
|
|
1353
|
+
/** 没有逐条快照时才退回整段到期判断;有快照就只撤到期的那一条。 */
|
|
1354
|
+
if (evolutionText && !state.experimentParts && state.validUntil && Date.now() > Date.parse(String(state.validUntil))) evolutionText = '';
|
|
1355
|
+
return [evolutionText, state.text].filter(Boolean).join('\n\n');
|
|
1301
1356
|
} catch {
|
|
1302
1357
|
return '';
|
|
1303
1358
|
}
|
|
@@ -1306,7 +1361,7 @@ export function apply(ctx, config) {
|
|
|
1306
1361
|
|
|
1307
1362
|
ctx.effect(() => tools.register({
|
|
1308
1363
|
name: 'memory_save',
|
|
1309
|
-
description:
|
|
1364
|
+
description: `保存一条记忆。只在会改变以后判断时才存;先搜已有条目,能更新就不要新建。保存前先蒸馏:标题是结论,正文只写原则与触发条件,通常 2–4 句;路径、字段表、完整命令、版本号、尺寸数值不要抄进正文,改成文档或 skill 的指针。用户明确说「记住」时直接调用;用户自己的话已经是长期规则(「以后都这样」「别再 X」)时也直接调用,不要先问。更新同一件事时带 id:身份只看标题、旧标题和前置条件,像不像不授权覆盖。文本相近但标题不同时先不写,返回 needsResolution 与 1–2 条相近条目:确实是同一条就用那个 id 更新,确实是另一条才带 resolution="distinct" 再存。带 id 更新时没给的字段保持原值,正文整段替换,旧正文只留作内部撤回,不展示也不参与检索。条数到硬上限时把同层最久没被读的一条移进归档,结果里写明动了哪条,不拒绝新记忆;单条正文超限只报错、不截断。前置条件不同的同一标题是两条。${NOTES}`,
|
|
1310
1365
|
parameters: SAVE_PARAMS,
|
|
1311
1366
|
output: {
|
|
1312
1367
|
presentationMeta: (_args, value) => ({
|
|
@@ -1328,6 +1383,8 @@ export function apply(ctx, config) {
|
|
|
1328
1383
|
title: { type: 'string' },
|
|
1329
1384
|
summary: { type: 'string' },
|
|
1330
1385
|
when: { type: 'string' },
|
|
1386
|
+
summaryShortened: { type: 'boolean' },
|
|
1387
|
+
fullTextInBody: { type: 'boolean' },
|
|
1331
1388
|
revision: { type: 'integer' },
|
|
1332
1389
|
merged: { type: 'boolean' },
|
|
1333
1390
|
changed: { type: 'boolean' },
|
|
@@ -1361,7 +1418,7 @@ export function apply(ctx, config) {
|
|
|
1361
1418
|
? `memory · 需要确认:「${value.title}」与已有条目相近${value.crossLayer ? '(在另一层)' : ''}${value.sameTopic ? '(同一主题槽位)' : ''}\n${value.similar}\n要更新它,用 id 重存;确实是另一条,加 resolution="distinct"`
|
|
1362
1419
|
: value.changed === false
|
|
1363
1420
|
? `memory · unchanged ${withTag(value, `"${value.title}"`)} · ${value.id}`
|
|
1364
|
-
: `memory · ${value.merged ? 'merged' : 'saved'} ${withTag(value, `"${value.title}"`)}${value.when ? `(${value.when})` : ''} · ${value.id}${value.similar ? ` · 相近:${value.similar}` : ''}${value.archived ? ` · 已归档「${value.archived}」腾位` : ''}`,
|
|
1421
|
+
: `memory · ${value.merged ? 'merged' : 'saved'} ${withTag(value, `"${value.title}"`)}${value.when ? `(${value.when})` : ''} · ${value.id}${value.similar ? ` · 相近:${value.similar}` : ''}${value.archived ? ` · 已归档「${value.archived}」腾位` : ''}${value.fullTextInBody ? ' · 摘要超过 60 字,完整原句已存进正文' : ''}`,
|
|
1365
1422
|
}],
|
|
1366
1423
|
},
|
|
1367
1424
|
execute: async (args, execution) => {
|
|
@@ -1439,6 +1496,8 @@ export function apply(ctx, config) {
|
|
|
1439
1496
|
title: saved.record.title,
|
|
1440
1497
|
summary: saved.record.summary,
|
|
1441
1498
|
when: saved.record.when || '',
|
|
1499
|
+
summaryShortened: Boolean(saved.draft?.summaryShortened),
|
|
1500
|
+
fullTextInBody: Boolean(saved.draft?.fullTextInBody),
|
|
1442
1501
|
revision: saved.record.revision || 1,
|
|
1443
1502
|
merged: saved.merged,
|
|
1444
1503
|
changed: !saved.noop,
|
|
@@ -1507,6 +1566,7 @@ export function apply(ctx, config) {
|
|
|
1507
1566
|
chars: { type: 'integer' },
|
|
1508
1567
|
text: { type: 'string' },
|
|
1509
1568
|
status: { type: 'string', enum: ['ok', 'budget_exhausted', 'memory_disabled', 'needs_context'] },
|
|
1569
|
+
reasonCode: { type: 'string' },
|
|
1510
1570
|
refs: {
|
|
1511
1571
|
type: 'array',
|
|
1512
1572
|
items: {
|
|
@@ -1553,19 +1613,20 @@ export function apply(ctx, config) {
|
|
|
1553
1613
|
const resolved = resolveRequestContext(execution, contextId);
|
|
1554
1614
|
if (!resolved.ok) return { count: 0, chars: 0, text: resolved.reason, status: resolved.status || 'needs_context', refs: [] };
|
|
1555
1615
|
if (wantsBind) {
|
|
1556
|
-
const bound = bindSelection(execution, resolved
|
|
1616
|
+
const bound = bindSelection(execution, resolved);
|
|
1557
1617
|
if (!bound.ok) return { count: 0, chars: 0, text: bound.reason, status: bound.status || 'needs_context', refs: [] };
|
|
1558
1618
|
}
|
|
1559
1619
|
const scope = resolved.scope;
|
|
1560
1620
|
const context = resolved.context;
|
|
1561
|
-
/**
|
|
1562
|
-
const
|
|
1621
|
+
/** 检索只列当前任务真能用的条目;先取全量候选,过滤后再截断,避免不可用条目挤掉后面的命中。 */
|
|
1622
|
+
const limit = args.limit ?? 3;
|
|
1623
|
+
const rawHits = search(scope, args.query, 50, {
|
|
1563
1624
|
context,
|
|
1564
1625
|
history: args.history === true,
|
|
1565
1626
|
allowExperimental: resolved.experimental === true,
|
|
1566
|
-
}).filter((item) =>
|
|
1567
|
-
|
|
1568
|
-
|
|
1627
|
+
}).filter((item) => !item.ownerProjectId || item.ownerProjectId === scope.key);
|
|
1628
|
+
const hits = rawHits.filter((item) => item.archived || item.status === 'ok').slice(0, limit);
|
|
1629
|
+
const withheld = rawHits.filter((item) => !item.archived && item.status !== 'ok');
|
|
1569
1630
|
const scopeName = scope.key === 'global' ? '' : projectNameOf(scope);
|
|
1570
1631
|
const shown = hits.map((item) => ({
|
|
1571
1632
|
...view.toModelView(item, 'search', { status: item.status || (item.archived ? 'archived' : 'ok') }),
|
|
@@ -1575,6 +1636,27 @@ export function apply(ctx, config) {
|
|
|
1575
1636
|
const rendered = renderIndex(shown.slice(0, kept));
|
|
1576
1637
|
return { count: kept, chars: rendered.chars, text: rendered.text, status: 'ok', refs: refsOf(hits.slice(0, kept)) };
|
|
1577
1638
|
};
|
|
1639
|
+
/** 有匹配但当前用不上:只说状态,不把标题、编号或旧规则递给模型,避免它被这些字面带偏。 */
|
|
1640
|
+
if (!hits.length && withheld.length) {
|
|
1641
|
+
const unresolved = withheld.some((item) => item.status === 'needs_context');
|
|
1642
|
+
const value = {
|
|
1643
|
+
count: 0,
|
|
1644
|
+
chars: 0,
|
|
1645
|
+
text: unresolved
|
|
1646
|
+
? '有相关的项目记忆,但适用条件还没确认。先说明当前任务,或确认这条记忆的适用范围。'
|
|
1647
|
+
: '有相关的项目记忆,但当前任务明确不适用。',
|
|
1648
|
+
status: unresolved ? 'needs_context' : 'ok',
|
|
1649
|
+
reasonCode: unresolved ? 'applicability_unresolved' : 'not_applicable',
|
|
1650
|
+
refs: [],
|
|
1651
|
+
};
|
|
1652
|
+
if (win) {
|
|
1653
|
+
const bytes = jsonBytes(value);
|
|
1654
|
+
if (!affordable(win, bytes)) return { ...value, status: 'budget_exhausted', text: '' };
|
|
1655
|
+
win.bytes += bytes;
|
|
1656
|
+
win.reads += 1;
|
|
1657
|
+
}
|
|
1658
|
+
return value;
|
|
1659
|
+
}
|
|
1578
1660
|
/** 没命中是正常结果,不是额度问题;空响应同样记一次本请求开销。 */
|
|
1579
1661
|
if (!hits.length) {
|
|
1580
1662
|
const empty = { count: 0, chars: 0, text: '', status: 'ok', refs: [] };
|
|
@@ -1632,10 +1714,20 @@ export function apply(ctx, config) {
|
|
|
1632
1714
|
: value.status === 'memory_disabled'
|
|
1633
1715
|
? 'memory · 记忆检索已关闭'
|
|
1634
1716
|
: value.repeat
|
|
1635
|
-
?
|
|
1717
|
+
? 'memory · 本请求已读过这条记忆'
|
|
1636
1718
|
: value.body
|
|
1637
1719
|
? `memory · ${withTag(value, `「${value.title}」`)}${value.when ? `(${value.when})` : ''} · ${value.id}${statusLabel(value.status)}\n${value.body}`
|
|
1638
|
-
:
|
|
1720
|
+
: value.links !== undefined && value.status === 'ok'
|
|
1721
|
+
? `memory · 「${value.title}」的相关记忆\n${value.links || '(无)'}`
|
|
1722
|
+
: value.status === 'needs_context'
|
|
1723
|
+
? 'memory · 这条记忆的适用条件还没确认'
|
|
1724
|
+
: value.status === 'not_applicable'
|
|
1725
|
+
? 'memory · 这条记忆不适用于当前任务'
|
|
1726
|
+
: value.status === 'archived'
|
|
1727
|
+
? 'memory · 这条记忆已归档'
|
|
1728
|
+
: value.status === 'gone'
|
|
1729
|
+
? 'memory · 这条记忆已不存在'
|
|
1730
|
+
: 'memory · 当前不能读这条记忆',
|
|
1639
1731
|
}],
|
|
1640
1732
|
presentationMeta: (_args, value) => ({
|
|
1641
1733
|
kind: 'memory-read',
|
|
@@ -1661,7 +1753,7 @@ export function apply(ctx, config) {
|
|
|
1661
1753
|
const found = readLinks(scope, args.id, { context, allowExperimental });
|
|
1662
1754
|
if (!found) throw new Error(`没有 ${args.id} 这条记忆`);
|
|
1663
1755
|
if (found.status !== 'ok') {
|
|
1664
|
-
const withheld = { id:
|
|
1756
|
+
const withheld = { id: '', scope: '', status: found.status, links: '' };
|
|
1665
1757
|
if (win) {
|
|
1666
1758
|
const bytes = jsonBytes(withheld);
|
|
1667
1759
|
if (!affordable(win, bytes)) return { id: '', scope: '', status: 'budget_exhausted' };
|
|
@@ -1687,22 +1779,22 @@ export function apply(ctx, config) {
|
|
|
1687
1779
|
const record = readEntry(scope, args.id, { context, allowExperimental });
|
|
1688
1780
|
if (!record) throw new Error(`没有 ${args.id} 这条记忆`);
|
|
1689
1781
|
if (record.origin === 'project_evolution' && record.ownerProjectId && record.ownerProjectId !== scope.key) {
|
|
1690
|
-
return { id:
|
|
1782
|
+
return { id: '', scope: record.scope, status: 'not_applicable', body: '' };
|
|
1691
1783
|
}
|
|
1692
1784
|
if (record.origin === 'project_evolution' && !allowExperimental) {
|
|
1693
|
-
return { id:
|
|
1785
|
+
return { id: '', scope: record.scope, status: 'needs_context', body: '' };
|
|
1694
1786
|
}
|
|
1695
|
-
/**
|
|
1787
|
+
/** 暂停、过期或条件不匹配的实验条目只给状态,不给标题和正文。 */
|
|
1696
1788
|
if (record.origin === 'project_evolution' && !view.experimentalUsable(record)) {
|
|
1697
|
-
return { id:
|
|
1789
|
+
return { id: '', scope: record.scope, status: 'needs_context', body: '' };
|
|
1698
1790
|
}
|
|
1699
1791
|
const learningState = record.learning && typeof record.learning === 'object' ? record.learning : {};
|
|
1700
1792
|
if (record.origin === 'project_evolution' && (learningState.state === 'paused' || learningState.blockedReason)) {
|
|
1701
|
-
return { id:
|
|
1793
|
+
return { id: '', scope: record.scope, status: 'needs_context', body: '' };
|
|
1702
1794
|
}
|
|
1703
|
-
/**
|
|
1795
|
+
/** 条件未知或不适用的条目只报状态:正文和标题都不能绕过自动召回的门槛。 */
|
|
1704
1796
|
if (record.status !== 'ok') {
|
|
1705
|
-
return { id:
|
|
1797
|
+
return { id: '', scope: record.scope, status: record.status === 'needs_context' ? 'needs_context' : 'not_applicable', body: '' };
|
|
1706
1798
|
}
|
|
1707
1799
|
const revision = record.revision || 1;
|
|
1708
1800
|
const body = String(record.body ?? '');
|
|
@@ -1936,6 +2028,8 @@ export function apply(ctx, config) {
|
|
|
1936
2028
|
previous.text = snapshot ? snapshot.text : '';
|
|
1937
2029
|
previous.cards = snapshot ? snapshot.cards : [];
|
|
1938
2030
|
previous.experimentText = experimentText;
|
|
2031
|
+
previous.experimentParts = win && win.experiment ? (win.experiment.experimentParts || null) : null;
|
|
2032
|
+
previous.validUntil = win && win.experiment ? (win.experiment.validUntil || '') : '';
|
|
1939
2033
|
previous.fingerprint = fingerprint;
|
|
1940
2034
|
} else {
|
|
1941
2035
|
clearPrepared(agent);
|
|
@@ -1948,13 +2042,15 @@ export function apply(ctx, config) {
|
|
|
1948
2042
|
/** 工具模式也要开额度窗口:只是不自动注入,工具调用同样受每请求预算约束。 */
|
|
1949
2043
|
const ids = fresh.map((message) => String(message?.id ?? '')).filter(Boolean).sort();
|
|
1950
2044
|
const win = openWindow(agent, { sessionId, turn, messageIds: ids });
|
|
2045
|
+
/** 工具模式也要记下本轮真人原话:只用于核对绑定,不自动注入。 */
|
|
2046
|
+
const query = view.newestUserQuery(fresh).slice(0, RECALL_QUERY_CHARS);
|
|
2047
|
+
if (win && query) win.query = query;
|
|
1951
2048
|
|
|
1952
2049
|
if (recallMode() !== 'adaptive') {
|
|
1953
2050
|
clearPrepared(agent);
|
|
1954
2051
|
return decision;
|
|
1955
2052
|
}
|
|
1956
2053
|
|
|
1957
|
-
const query = view.newestUserQuery(fresh).slice(0, RECALL_QUERY_CHARS);
|
|
1958
2054
|
if (!query) {
|
|
1959
2055
|
clearPrepared(agent);
|
|
1960
2056
|
return decision;
|
|
@@ -1995,9 +2091,13 @@ export function apply(ctx, config) {
|
|
|
1995
2091
|
clearPrepared(agent);
|
|
1996
2092
|
return decision;
|
|
1997
2093
|
}
|
|
2094
|
+
let evolutionEpoch = '';
|
|
2095
|
+
try { evolutionEpoch = String(evolutionStatus(scope).epoch || ''); } catch { evolutionEpoch = ''; }
|
|
1998
2096
|
const state = {
|
|
1999
2097
|
text: snapshot ? snapshot.text : '',
|
|
2000
2098
|
experimentText: experiment ? experiment.text : '',
|
|
2099
|
+
experimentParts: experiment && experiment.held ? (experiment.held.experimentParts || null) : null,
|
|
2100
|
+
validUntil: experiment && experiment.held ? (experiment.held.validUntil || '') : '',
|
|
2001
2101
|
cards: snapshot ? snapshot.cards : [],
|
|
2002
2102
|
query,
|
|
2003
2103
|
sessionId,
|
|
@@ -2005,6 +2105,7 @@ export function apply(ctx, config) {
|
|
|
2005
2105
|
signal,
|
|
2006
2106
|
fingerprint,
|
|
2007
2107
|
controlEpoch,
|
|
2108
|
+
evolutionEpoch,
|
|
2008
2109
|
at: Date.now(),
|
|
2009
2110
|
};
|
|
2010
2111
|
prepared.set(agent, state);
|
|
@@ -2149,7 +2250,7 @@ export function apply(ctx, config) {
|
|
|
2149
2250
|
if (sawTool) throw new Error('整理返回了工具调用');
|
|
2150
2251
|
/** 运行期间用户又给了新反馈:旧结果不能覆盖新纠正,直接作废,等排队的下一次整理。 */
|
|
2151
2252
|
if ((feedbackRevisions.get(scope.key) || 0) !== candidate.feedbackRevision) {
|
|
2152
|
-
throw new Error('
|
|
2253
|
+
throw new Error('反馈已更新,这轮结果没采用');
|
|
2153
2254
|
}
|
|
2154
2255
|
const parsed = learning.parseOutput(text, { finish, refs: input.refs, rules: input.rules, ranges });
|
|
2155
2256
|
if (!parsed.ok) throw new Error(parsed.reason);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-lite-memory",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.16.4",
|
|
4
4
|
"description": "On-demand memory for the DeepSeek Harness: nothing in the system prompt, one muted line only when something is saved, hit or suggested.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "index.js",
|
package/state.js
CHANGED
|
@@ -182,9 +182,9 @@ export const LIMITS = {
|
|
|
182
182
|
entries: 200,
|
|
183
183
|
globalEntries: 200,
|
|
184
184
|
hardEntries: 1000,
|
|
185
|
-
entryBytes:
|
|
185
|
+
entryBytes: 4096,
|
|
186
186
|
recordBytes: 10 * 1024,
|
|
187
|
-
totalBytes:
|
|
187
|
+
totalBytes: 1024 * 1024,
|
|
188
188
|
historyBytes: 512 * 1024,
|
|
189
189
|
stateBytes: 2 * 1024 * 1024,
|
|
190
190
|
titleChars: 60,
|
|
@@ -747,10 +747,12 @@ export function enforceBudget(state, scope) {
|
|
|
747
747
|
const active = state.entries.filter((entry) => entry.status === 'active');
|
|
748
748
|
const bytes = active.reduce((sum, entry) => sum + entryBytesOf(entry), 0);
|
|
749
749
|
if (active.length <= LIMITS.hardEntries && bytes <= LIMITS.totalBytes) break;
|
|
750
|
-
|
|
751
|
-
const
|
|
750
|
+
/** 用户固定过的规则不参与容量腾挪;只从没固定的条目里选最久没读的。 */
|
|
751
|
+
const movable = active.filter((entry) => entry.maintenance?.learning?.pinned !== true);
|
|
752
|
+
if (!movable.length) throw new StateError('budget_exhausted', '记忆库已满,固定住的规则不参与腾挪:先取消固定或精简正文');
|
|
753
|
+
const idle = movable.filter((entry) => now - entryLastTouch(entry) >= IDLE_MS);
|
|
754
|
+
const pool = idle.length ? idle : movable;
|
|
752
755
|
const victim = pool.reduce((oldest, entry) => (entryLastTouch(entry) < entryLastTouch(oldest) ? entry : oldest));
|
|
753
|
-
if (!victim) throw new StateError('budget_exhausted', `记忆库已满:这一条自己就超过 ${LIMITS.totalBytes} 字节正文预算,先精简正文`);
|
|
754
756
|
victim.status = 'archived';
|
|
755
757
|
victim.updatedAt = new Date().toISOString();
|
|
756
758
|
evicted.push({ id: victim.id, title: victim.current.title });
|