dsh-lite-memory 0.16.4 → 0.17.0
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 +9 -9
- package/TRIGGERS.md +14 -13
- package/anchor.js +259 -24
- package/client.js +1182 -212
- package/docs/CONVENTIONS.md +143 -0
- package/docs/RELEASE-0.17.0.md +8 -0
- package/index.js +335 -93
- package/learning.js +456 -48
- package/package.json +5 -2
- package/state.js +110 -5
- package/store.js +446 -60
- package/test/anchor.test.mjs +240 -0
- package/test/client.test.mjs +176 -6
- package/test/contract.test.mjs +9 -1
- package/test/learning.test.mjs +483 -1
- package/test/palace.test.mjs +102 -0
- package/test/runtime-context.test.mjs +186 -0
- package/test/state.test.mjs +121 -2
- package/test/store.test.mjs +454 -15
- package/view.js +60 -3
package/index.js
CHANGED
|
@@ -226,6 +226,13 @@ function keepHandler(webServer, cwdForSession) {
|
|
|
226
226
|
return sendJson(res, 400, { ok: false, reason: '缺少来源' });
|
|
227
227
|
}
|
|
228
228
|
if (!found) return sendJson(res, 200, { ok: true, status: 'gone', id });
|
|
229
|
+
/** 面板要分开「当前规范」和旧版本;历史只作可折叠对照,不拼进正文。 */
|
|
230
|
+
const versions = Array.isArray(found.versions) ? found.versions
|
|
231
|
+
: Array.isArray(found.history) ? found.history : [];
|
|
232
|
+
const learning = found.learning && typeof found.learning === 'object' ? found.learning : {};
|
|
233
|
+
const enums = new Set(['applied', 'unchanged', 'superseded', 'failed']);
|
|
234
|
+
const processingStatus = enums.has(found.processingStatus) ? found.processingStatus
|
|
235
|
+
: enums.has(learning.lastStatus) ? learning.lastStatus : '';
|
|
229
236
|
return sendJson(res, 200, {
|
|
230
237
|
ok: true,
|
|
231
238
|
id: found.id,
|
|
@@ -236,6 +243,13 @@ function keepHandler(webServer, cwdForSession) {
|
|
|
236
243
|
scopeLabel: found.scopeLabel || '',
|
|
237
244
|
revision: found.revision,
|
|
238
245
|
body: found.body,
|
|
246
|
+
currentBody: typeof found.currentBody === 'string' ? found.currentBody : found.body,
|
|
247
|
+
versions,
|
|
248
|
+
sourceExcerpt: found.sourceExcerpt || learning.sourceExcerpt || '',
|
|
249
|
+
sourceRefs: Array.isArray(found.sourceRefs) ? found.sourceRefs : (Array.isArray(learning.sourceRefs) ? learning.sourceRefs : []),
|
|
250
|
+
evidence: found.evidence || (found.evidenceRef ? [found.evidenceRef] : []),
|
|
251
|
+
evidenceRef: found.evidenceRef || '',
|
|
252
|
+
processingStatus,
|
|
239
253
|
});
|
|
240
254
|
}
|
|
241
255
|
|
|
@@ -627,6 +641,9 @@ function refsOf(hits) {
|
|
|
627
641
|
revision: item.revision || 1,
|
|
628
642
|
status: item.status || 'ok',
|
|
629
643
|
origin: item.origin === 'project_evolution' ? 'project_evolution' : 'user_explicit',
|
|
644
|
+
/** 界面要按类型分开展示(偏好/约定/流程/资料),维护字段不进正文。 */
|
|
645
|
+
kind: typeof item.kind === 'string' && item.kind ? item.kind : 'note',
|
|
646
|
+
when: typeof item.when === 'string' ? item.when : '',
|
|
630
647
|
}));
|
|
631
648
|
}
|
|
632
649
|
|
|
@@ -872,6 +889,14 @@ export function apply(ctx, config) {
|
|
|
872
889
|
|
|
873
890
|
const windowOf = (agent) => windows.get(agentKey(agent)) || null;
|
|
874
891
|
|
|
892
|
+
/** 自动注入账本:普通段与实验段共用 3 条 / 1200 字节,量的是拼接后的完整文本。 */
|
|
893
|
+
const emptyAutoBudget = () => ({
|
|
894
|
+
normalText: '', experimentText: '',
|
|
895
|
+
normalCards: 0, experimentCards: 0,
|
|
896
|
+
cards: 0, cardBytes: 0, experimentBytes: 0, totalBytes: 0,
|
|
897
|
+
});
|
|
898
|
+
|
|
899
|
+
|
|
875
900
|
/** 预算按规范化 JSON 字节算:转义、中文、换行都算进模型真正收到的内容。 */
|
|
876
901
|
const jsonBytes = (value) => Buffer.byteLength(JSON.stringify(value));
|
|
877
902
|
|
|
@@ -892,7 +917,7 @@ export function apply(ctx, config) {
|
|
|
892
917
|
signature, sessionId, turn, messageIds,
|
|
893
918
|
bytes: 0, bodies: 0, reads: 0,
|
|
894
919
|
provided: new Map(), readBodies: new Map(), at: Date.now(),
|
|
895
|
-
experiment: null, contexts: new Map(),
|
|
920
|
+
experiment: null, contexts: new Map(), runtime: null, autoBudget: null,
|
|
896
921
|
};
|
|
897
922
|
windows.set(key, next);
|
|
898
923
|
return next;
|
|
@@ -917,6 +942,11 @@ export function apply(ctx, config) {
|
|
|
917
942
|
}
|
|
918
943
|
|
|
919
944
|
/** 两层都算:任何一层换库、换代或清空,之前准备的卡片都要失效。 */
|
|
945
|
+
/**
|
|
946
|
+
* 同一请求的召回快照只要内容没变就保持原样:命中次数、使用时间这类使用记录
|
|
947
|
+
* 不参与指纹,否则模型多读一次就会触发重投影。真正改变应看条目内容版本、
|
|
948
|
+
* 启停/控制代号与自进化代号。
|
|
949
|
+
*/
|
|
920
950
|
function fingerprintOf(scope) {
|
|
921
951
|
try {
|
|
922
952
|
return [scope, globalScopeView()].map((source) => {
|
|
@@ -924,7 +954,11 @@ export function apply(ctx, config) {
|
|
|
924
954
|
const meta = index.__meta || {};
|
|
925
955
|
const evolutionEpoch = index.__evolution && index.__evolution.epoch ? index.__evolution.epoch : '';
|
|
926
956
|
const controlEpoch = index.__control && index.__control.epoch ? index.__control.epoch : '';
|
|
927
|
-
|
|
957
|
+
const content = (index.entries || [])
|
|
958
|
+
.map((entry) => `${entry.id}:${entry.revision || 1}:${entry.status || 'active'}:${entry.updated || ''}:${entry.topicKey || ''}`)
|
|
959
|
+
.sort()
|
|
960
|
+
.join(',');
|
|
961
|
+
return `${index.scope}:${meta.storeId || ''}:${evolutionEpoch}:${controlEpoch}:${content}`;
|
|
928
962
|
}).join('|');
|
|
929
963
|
} catch {
|
|
930
964
|
return '';
|
|
@@ -934,7 +968,8 @@ export function apply(ctx, config) {
|
|
|
934
968
|
const globalScopeView = () => globalScope();
|
|
935
969
|
|
|
936
970
|
/** 纯内存:读两层当前投影,先判适用再排序,整卡装预算。 */
|
|
937
|
-
function projectRecall(scope, query, context, win) {
|
|
971
|
+
function projectRecall(scope, query, context, win, runtime) {
|
|
972
|
+
context = view.contextWithTask(context, runtime && runtime.task);
|
|
938
973
|
const layers = { project: [], global: [] };
|
|
939
974
|
const seen = new Set();
|
|
940
975
|
for (const source of [scope, globalScope()]) {
|
|
@@ -958,38 +993,61 @@ export function apply(ctx, config) {
|
|
|
958
993
|
records.push(group.record);
|
|
959
994
|
layerOf.set(group.record.id, group.scope === 'global' ? 'global' : 'project');
|
|
960
995
|
}
|
|
961
|
-
const ranked = view.rank(records, query, {
|
|
962
|
-
|
|
963
|
-
|
|
996
|
+
const ranked = view.rank(records, query, {
|
|
997
|
+
limit: 8, requireApplicable: true, context,
|
|
998
|
+
runtime: runtime || (win && win.runtime) || null,
|
|
999
|
+
task: runtime && runtime.task ? runtime.task : null,
|
|
1000
|
+
});
|
|
1001
|
+
/** 普通记忆与实验规则共用一份自动注入预算:总条数和最终完整文本字节一起算。 */
|
|
1002
|
+
const auto = win && win.autoBudget ? win.autoBudget : null;
|
|
964
1003
|
/** 卡片上限含包装;共享请求账本也要把标题行算进去,不能只算卡片行。 */
|
|
965
1004
|
let headerCharged = win ? win.recallHeader === true : false;
|
|
966
1005
|
const lines = [];
|
|
967
1006
|
const cards = [];
|
|
968
1007
|
for (const row of ranked) {
|
|
969
|
-
|
|
1008
|
+
const usedCards = cards.length + (auto ? (auto.experimentCards || 0) : 0);
|
|
1009
|
+
if (usedCards >= view.CARD.max) break;
|
|
970
1010
|
const card = view.cardOf(row.record, layerOf.get(row.record.id));
|
|
971
1011
|
if (card.scope === 'project') card.scopeName = projectNameOf(scope);
|
|
972
1012
|
const line = renderRecallCard(card);
|
|
973
|
-
const
|
|
974
|
-
if (
|
|
1013
|
+
const nextText = `${RECALL_HEADER}\n${[...lines, line].join('\n')}`;
|
|
1014
|
+
if (auto) {
|
|
1015
|
+
/** 实验段可能先渲染:组装成最终两段再量,超了就跳过这张,不把标题留在模型侧。 */
|
|
1016
|
+
const admitted = admitAuto(win, {
|
|
1017
|
+
normal: nextText,
|
|
1018
|
+
experiment: auto.experimentText || '',
|
|
1019
|
+
normalCards: cards.length + 1,
|
|
1020
|
+
experimentCards: auto.experimentCards || 0,
|
|
1021
|
+
});
|
|
1022
|
+
if (!admitted.ok) continue;
|
|
1023
|
+
} else {
|
|
1024
|
+
const size = Buffer.byteLength(nextText);
|
|
1025
|
+
if (size > view.CARD.bytes) continue;
|
|
1026
|
+
const cost = Buffer.byteLength(`\n${line}`) + (headerCharged ? 0 : Buffer.byteLength(RECALL_HEADER));
|
|
1027
|
+
if (win && !affordable(win, cost)) continue;
|
|
1028
|
+
if (win) win.bytes += cost;
|
|
1029
|
+
}
|
|
975
1030
|
const revision = card.revision || 1;
|
|
976
|
-
|
|
977
|
-
if (win && !
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
win.bytes += cost;
|
|
981
|
-
win.provided.set(card.id, revision);
|
|
982
|
-
if (!headerCharged) {
|
|
983
|
-
headerCharged = true;
|
|
984
|
-
win.recallHeader = true;
|
|
985
|
-
}
|
|
1031
|
+
if (win) win.provided.set(card.id, revision);
|
|
1032
|
+
if (win && !headerCharged) {
|
|
1033
|
+
headerCharged = true;
|
|
1034
|
+
win.recallHeader = true;
|
|
986
1035
|
}
|
|
987
|
-
used += size;
|
|
988
1036
|
lines.push(line);
|
|
989
1037
|
cards.push({ id: card.id, revision, scope: card.scope, title: card.title });
|
|
990
1038
|
}
|
|
1039
|
+
const finalText = `${RECALL_HEADER}\n${lines.join('\n')}`;
|
|
1040
|
+
if (auto) {
|
|
1041
|
+
/** 没有任何普通卡片时也要把已有实验段记账成当前状态。 */
|
|
1042
|
+
admitAuto(win, {
|
|
1043
|
+
normal: lines.length ? finalText : (auto.normalText || ''),
|
|
1044
|
+
experiment: auto.experimentText || '',
|
|
1045
|
+
normalCards: cards.length,
|
|
1046
|
+
experimentCards: auto.experimentCards || 0,
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
991
1049
|
if (!cards.length) return null;
|
|
992
|
-
return { text:
|
|
1050
|
+
return { text: finalText, cards, bytes: Buffer.byteLength(finalText), query };
|
|
993
1051
|
}
|
|
994
1052
|
|
|
995
1053
|
/* ---------------- 项目自进化:任务锚点、读取决策、上下文解析 ---------------- */
|
|
@@ -1037,9 +1095,62 @@ export function apply(ctx, config) {
|
|
|
1037
1095
|
.join('、');
|
|
1038
1096
|
}
|
|
1039
1097
|
|
|
1040
|
-
/**
|
|
1041
|
-
|
|
1098
|
+
/**
|
|
1099
|
+
* 自动注入只有一份预算:普通卡片与实验规则合计最多 3 条 / 1200 字节。
|
|
1100
|
+
* 这里按 provider 最终会看到的完整文本量字节(含标题行、ID、换行和两段之间的空行),
|
|
1101
|
+
* 先组装再测量,不能只量卡片正文再拼。
|
|
1102
|
+
*/
|
|
1103
|
+
function autoTextOf(win, normal, experiment) {
|
|
1104
|
+
const auto = win && win.autoBudget ? win.autoBudget : null;
|
|
1105
|
+
const n = normal === undefined ? String(auto?.normalText || '') : String(normal || '');
|
|
1106
|
+
const e = experiment === undefined ? String(auto?.experimentText || '') : String(experiment || '');
|
|
1107
|
+
return { normal: n, experiment: e, text: [e, n].filter(Boolean).join('\n\n') };
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
function autoBytesOf(win, normal, experiment) {
|
|
1111
|
+
return Buffer.byteLength(autoTextOf(win, normal, experiment).text);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
/**
|
|
1115
|
+
* 原子准入:量的是最终完整文本和总条数;通过才写入预算账本。
|
|
1116
|
+
* 任何一段超限都整段回退,不截半句前提、不把标题留在模型侧。
|
|
1117
|
+
*/
|
|
1118
|
+
function admitAuto(win, { normal, experiment, normalCards, experimentCards } = {}) {
|
|
1119
|
+
if (!win || !win.autoBudget) return { ok: false, bytes: 0 };
|
|
1120
|
+
const auto = win.autoBudget;
|
|
1121
|
+
const parts = autoTextOf(win, normal, experiment);
|
|
1122
|
+
const cards = (normalCards === undefined ? (auto.normalCards || 0) : normalCards)
|
|
1123
|
+
+ (experimentCards === undefined ? (auto.experimentCards || 0) : experimentCards);
|
|
1124
|
+
const bytes = Buffer.byteLength(parts.text);
|
|
1125
|
+
if (cards > view.CARD.max || bytes > view.CARD.bytes) return { ok: false, bytes, cards };
|
|
1126
|
+
const previousTotal = Number(auto.totalBytes || 0);
|
|
1127
|
+
if (!affordable(win, bytes - previousTotal)) return { ok: false, bytes, cards };
|
|
1128
|
+
auto.normalText = parts.normal;
|
|
1129
|
+
auto.experimentText = parts.experiment;
|
|
1130
|
+
auto.normalCards = normalCards === undefined ? (auto.normalCards || 0) : normalCards;
|
|
1131
|
+
auto.experimentCards = experimentCards === undefined ? (auto.experimentCards || 0) : experimentCards;
|
|
1132
|
+
auto.cards = cards;
|
|
1133
|
+
auto.cardBytes = Buffer.byteLength(parts.normal);
|
|
1134
|
+
auto.experimentBytes = Buffer.byteLength(parts.experiment);
|
|
1135
|
+
auto.totalBytes = bytes;
|
|
1136
|
+
win.bytes += bytes - previousTotal;
|
|
1137
|
+
return { ok: true, bytes, cards };
|
|
1138
|
+
}
|
|
1139
|
+
|
|
1140
|
+
/** 实验内容单独记账:同一请求里再次渲染替换旧段,只补差额。 */
|
|
1141
|
+
function chargeExperiment(win, text, cards = 1) {
|
|
1042
1142
|
if (!text) return '';
|
|
1143
|
+
const auto = win && win.autoBudget ? win.autoBudget : null;
|
|
1144
|
+
if (auto) {
|
|
1145
|
+
const admitted = admitAuto(win, {
|
|
1146
|
+
normal: auto.normalText || '',
|
|
1147
|
+
experiment: text,
|
|
1148
|
+
normalCards: auto.normalCards || 0,
|
|
1149
|
+
experimentCards: cards,
|
|
1150
|
+
});
|
|
1151
|
+
if (!admitted.ok) return '';
|
|
1152
|
+
return text;
|
|
1153
|
+
}
|
|
1043
1154
|
const bytes = Buffer.byteLength(`\n\n${text}`);
|
|
1044
1155
|
const previous = win ? (win.experimentBytes || 0) : 0;
|
|
1045
1156
|
if (bytes > EXPERIMENT_BYTES) return '';
|
|
@@ -1128,8 +1239,16 @@ export function apply(ctx, config) {
|
|
|
1128
1239
|
return chargeExperiment(win, '这些实验规则互相冲突,先按用户这次说的做,别叠加对立的规则。');
|
|
1129
1240
|
}
|
|
1130
1241
|
const parts = { header: EXPERIMENT_RULES_HEADER, items: [], tail: held && held.contextId ? `\ncontextId=${held.contextId}` : '' };
|
|
1242
|
+
/** 与普通记忆共用 3 张卡片 / 1200 字节:普通卡片先占掉的位子和字节,实验规则不能再拿。 */
|
|
1243
|
+
const auto = win && win.autoBudget ? win.autoBudget : null;
|
|
1244
|
+
const cardCap = auto
|
|
1245
|
+
? Math.max(0, view.CARD.max - (auto.normalCards || 0) - (auto.experimentCards || 0))
|
|
1246
|
+
: view.CARD.max;
|
|
1247
|
+
const autoCap = auto
|
|
1248
|
+
? Math.max(0, view.CARD.bytes - Buffer.byteLength(auto.normalText || '') - 2)
|
|
1249
|
+
: EXPERIMENT_BYTES;
|
|
1131
1250
|
let used = Buffer.byteLength(EXPERIMENT_RULES_HEADER) + 1;
|
|
1132
|
-
for (const record of matched.slice(0,
|
|
1251
|
+
for (const record of matched.slice(0, cardCap)) {
|
|
1133
1252
|
const body = String(record.body || '');
|
|
1134
1253
|
const short = body && body.length <= EXPERIMENT_SHORT_CHARS && body.split('\n').length <= 4;
|
|
1135
1254
|
const conditions = conditionLabel(record.conditions);
|
|
@@ -1141,7 +1260,7 @@ export function apply(ctx, config) {
|
|
|
1141
1260
|
? `- 「${record.title}」${conditions ? `(${conditions})` : ''} · ${record.id}:${body}${check}`
|
|
1142
1261
|
: `- 「${record.title}」 · ${record.id}(用途:${record.summary || record.title};要用时 memory_read 取正文)`;
|
|
1143
1262
|
const size = Buffer.byteLength(`\n${line}`);
|
|
1144
|
-
if (used + size >
|
|
1263
|
+
if (used + size > autoCap) break;
|
|
1145
1264
|
used += size;
|
|
1146
1265
|
const trialLearning = record.learning && typeof record.learning === 'object' ? record.learning : {};
|
|
1147
1266
|
const trialUntil = trialLearning.state === 'trial' && Number.isFinite(Date.parse(String(trialLearning.trialUntil || '')))
|
|
@@ -1154,71 +1273,136 @@ export function apply(ctx, config) {
|
|
|
1154
1273
|
return chargeExperiment(win, '有些实验规则的适用条件还没确认,先按用户这次说的做,别把它们当本轮依据。');
|
|
1155
1274
|
}
|
|
1156
1275
|
if (!parts.items.length) return '';
|
|
1276
|
+
/** 先按最终整段文本过预算,再登记可发布内容;没通过就一条都不留。 */
|
|
1277
|
+
const admitted = chargeExperiment(win, experimentTextFrom(parts), parts.items.length);
|
|
1278
|
+
if (!admitted) {
|
|
1279
|
+
if (held) { held.experimentParts = null; held.injectedIds = []; held.validUntil = ''; }
|
|
1280
|
+
return '';
|
|
1281
|
+
}
|
|
1157
1282
|
if (held) held.experimentParts = parts;
|
|
1158
1283
|
/** 兼容旧读取点:整体最早到期只取本轮真正注入的条目,不再被无关试验拖下水。 */
|
|
1159
1284
|
if (held) {
|
|
1160
1285
|
const deadlines = parts.items.map((item) => Date.parse(String(item.validUntil))).filter(Number.isFinite);
|
|
1161
1286
|
if (deadlines.length) held.validUntil = new Date(Math.min(...deadlines)).toISOString();
|
|
1162
1287
|
}
|
|
1163
|
-
return
|
|
1288
|
+
return admitted;
|
|
1164
1289
|
}
|
|
1165
1290
|
|
|
1166
|
-
/**
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
if (!
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1291
|
+
/**
|
|
1292
|
+
* 一次真人请求只路由一次:不管自进化开没开,都先建独立的任务上下文。
|
|
1293
|
+
* 普通记忆按这个上下文判适用;实验规则只是在这个上下文上再加一层可用性。
|
|
1294
|
+
*/
|
|
1295
|
+
function runtimeFor(scope, { sessionId, query, batchKey, messageIds, autoRoute = true }) {
|
|
1296
|
+
if (!batchKey) return null;
|
|
1297
|
+
let status = null;
|
|
1298
|
+
try { status = evolutionStatus(scope); } catch { status = null; }
|
|
1299
|
+
const enabled = Boolean(status && status.enabled === true && status.compatible !== false && scope.confirmed === true);
|
|
1300
|
+
/** 任务上下文独立于自进化:项目状态读不出来也要按当前任务判普通记忆,不能整段取消召回。 */
|
|
1301
|
+
let tasks = null;
|
|
1302
|
+
try { tasks = taskState(scope); } catch { tasks = null; }
|
|
1303
|
+
tasks = tasks && typeof tasks === 'object' ? tasks : {};
|
|
1175
1304
|
const ranges = rangesOf(scope);
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1305
|
+
/**
|
|
1306
|
+
* 工具模式不自动路由:上下文照样要有,但不能因为一句点名就改任务状态;
|
|
1307
|
+
* 真正的绑定留给 memory_search bindForRequest,绑定后状态是 proposed。
|
|
1308
|
+
*/
|
|
1309
|
+
const routed = autoRoute
|
|
1310
|
+
? anchor.routeTask(tasks, {
|
|
1311
|
+
projectId: scope.key,
|
|
1312
|
+
sessionId,
|
|
1313
|
+
batchKey,
|
|
1314
|
+
messageIds,
|
|
1315
|
+
query,
|
|
1316
|
+
registeredSeries: ranges.series,
|
|
1317
|
+
defaults: tasks.defaults,
|
|
1318
|
+
now: Date.now(),
|
|
1319
|
+
})
|
|
1320
|
+
: (() => {
|
|
1321
|
+
const sessionAnchor = tasks.sessions && tasks.sessions[sessionId] ? tasks.sessions[sessionId] : null;
|
|
1322
|
+
const episode = sessionAnchor && sessionAnchor.episodeId && tasks.episodes
|
|
1323
|
+
? tasks.episodes[sessionAnchor.episodeId] || null
|
|
1324
|
+
: null;
|
|
1325
|
+
return { tasks, anchor: sessionAnchor, episode, changed: false, decision: { kind: 'none', reason: 'tool_only' } };
|
|
1326
|
+
})();
|
|
1186
1327
|
if (routed.changed) {
|
|
1187
|
-
|
|
1188
|
-
|
|
1328
|
+
/** 落盘失败也不能取消本轮任务判断:进程内上下文仍然有效,只是不留任务历史。 */
|
|
1329
|
+
saveTaskState(scope, routed.tasks, { operationId: `task_${batchKey}` });
|
|
1189
1330
|
}
|
|
1190
1331
|
const context = view.trustedContext(scope, anchor.contextOfAnchor(routed.anchor, {}));
|
|
1191
1332
|
const thanks = anchor.isThanks(query);
|
|
1192
1333
|
const decision = anchor.decideRead({
|
|
1193
1334
|
mode: recallMode(),
|
|
1194
|
-
enabled
|
|
1195
|
-
identityConfirmed: true,
|
|
1335
|
+
enabled,
|
|
1336
|
+
identityConfirmed: scope.confirmed === true,
|
|
1196
1337
|
anchor: routed.anchor,
|
|
1197
1338
|
ranges: [...ranges.taskKinds, ...ranges.series],
|
|
1198
1339
|
thanks,
|
|
1199
1340
|
boundForRequest: false,
|
|
1200
1341
|
});
|
|
1201
1342
|
const contextId = anchor.contextIdFor(scope.key, batchKey, crypto.randomUUID().slice(0, 8));
|
|
1202
|
-
const episode = routed.
|
|
1343
|
+
const episode = routed.episode
|
|
1344
|
+
|| (routed.anchor && routed.tasks && routed.tasks.episodes ? routed.tasks.episodes[routed.anchor.episodeId] : null);
|
|
1203
1345
|
let epoch = '';
|
|
1204
|
-
try { epoch = String(
|
|
1346
|
+
try { epoch = String(status && status.epoch ? status.epoch : ''); } catch { epoch = ''; }
|
|
1347
|
+
const anchorTask = routed.anchor || {};
|
|
1348
|
+
/** 与自进化开关无关的请求上下文:普通记忆也靠它判断项目、任务与代号。 */
|
|
1349
|
+
const requestContext = anchor.buildRuntimeContext({
|
|
1350
|
+
projectId: scope.key,
|
|
1351
|
+
sessionId,
|
|
1352
|
+
batchKey,
|
|
1353
|
+
messageIds,
|
|
1354
|
+
controlEpoch: epoch,
|
|
1355
|
+
query,
|
|
1356
|
+
anchor: anchorTask,
|
|
1357
|
+
episode,
|
|
1358
|
+
});
|
|
1359
|
+
const task = {
|
|
1360
|
+
...requestContext.task,
|
|
1361
|
+
kind: anchorTask.taskKind || requestContext.task.kind || context.taskKind || '',
|
|
1362
|
+
seriesId: anchorTask.seriesId || requestContext.task.seriesId || context.seriesId || '',
|
|
1363
|
+
artifactKey: requestContext.task.artifactKey || context.artifactKey || '',
|
|
1364
|
+
phase: requestContext.task.phase || context.phase || '',
|
|
1365
|
+
};
|
|
1205
1366
|
const held = {
|
|
1206
|
-
contextId,
|
|
1207
|
-
|
|
1367
|
+
contextId, requestId: requestContext.requestId, controlEpoch: epoch,
|
|
1368
|
+
projectKey: scope.key, sessionId, batchKey, messageIds, query,
|
|
1369
|
+
context, task, anchor: routed.anchor, ranges, decision, epoch,
|
|
1370
|
+
experimental: enabled,
|
|
1208
1371
|
episodeUpdatedAt: episode ? episode.updatedAt : '', injectedIds: [],
|
|
1209
1372
|
at: Date.now(),
|
|
1210
1373
|
};
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1374
|
+
return { held, decision, ranges, enabled, status };
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
/** 实验正文只在自进化开着时登记;普通记忆共享同一个任务上下文。 */
|
|
1378
|
+
function experimentFor(scope, runtime, win) {
|
|
1379
|
+
if (!runtime || !runtime.held) return null;
|
|
1380
|
+
const held = runtime.held;
|
|
1381
|
+
if (win) win.runtime = held;
|
|
1382
|
+
if (!runtime.enabled) return null;
|
|
1383
|
+
if (!experimentStillCurrent(scope, held)) return { text: '', held, decision: held.decision };
|
|
1384
|
+
if (win) {
|
|
1385
|
+
win.experiment = held;
|
|
1386
|
+
win.contexts.set(held.contextId, held);
|
|
1387
|
+
}
|
|
1388
|
+
return { text: renderExperiment(scope, held.decision, held.context, held, win), decision: held.decision, held };
|
|
1214
1389
|
}
|
|
1215
1390
|
|
|
1216
|
-
/** 自动路径与工具共用一次解析:没有本轮上下文就退回普通检索,绝不猜一个空上下文。 */
|
|
1217
1391
|
function resolveRequestContext(execution, optionalContextId) {
|
|
1218
1392
|
const scope = scopeOf(cwdOf(execution));
|
|
1219
1393
|
const win = windowOf(execution && execution.agent);
|
|
1220
1394
|
const contextId = typeof optionalContextId === 'string' ? optionalContextId.trim() : '';
|
|
1221
1395
|
if (!contextId) {
|
|
1396
|
+
/** 没有显式上下文时,用本请求已经路由出来的任务;这层不依赖自进化开关。 */
|
|
1397
|
+
const runtime = win && win.runtime;
|
|
1398
|
+
const sameRequest = runtime
|
|
1399
|
+
&& runtime.projectKey === scope.key
|
|
1400
|
+
&& (!runtime.sessionId || runtime.sessionId === String(win.sessionId || ''))
|
|
1401
|
+
&& runtime.batchKey === anchor.requestBatchKey(win.messageIds || []);
|
|
1402
|
+
if (sameRequest) {
|
|
1403
|
+
/** 普通记忆可以直接用本请求上下文;实验正文仍要求显式 contextId,不能顺手放行。 */
|
|
1404
|
+
return { ok: true, scope, win, context: runtime.context, held: runtime, experimental: false };
|
|
1405
|
+
}
|
|
1222
1406
|
return { ok: true, scope, win, context: view.trustedContext(scope, {}), experimental: false };
|
|
1223
1407
|
}
|
|
1224
1408
|
/** 先认项目再找本地记录:别的项目的上下文要明确拒绝,不能只报过期。 */
|
|
@@ -1371,6 +1555,7 @@ export function apply(ctx, config) {
|
|
|
1371
1555
|
scope: value.scope || '',
|
|
1372
1556
|
revision: value.revision || 0,
|
|
1373
1557
|
changed: value.changed === true,
|
|
1558
|
+
recordKind: typeof value.kind === 'string' && value.kind ? value.kind : '',
|
|
1374
1559
|
mode: value.needsScope ? 'needs_scope'
|
|
1375
1560
|
: value.existingId ? 'duplicate'
|
|
1376
1561
|
: value.needsResolution ? 'needs_resolution'
|
|
@@ -1383,8 +1568,9 @@ export function apply(ctx, config) {
|
|
|
1383
1568
|
title: { type: 'string' },
|
|
1384
1569
|
summary: { type: 'string' },
|
|
1385
1570
|
when: { type: 'string' },
|
|
1571
|
+
kind: { type: 'string' },
|
|
1386
1572
|
summaryShortened: { type: 'boolean' },
|
|
1387
|
-
|
|
1573
|
+
fullTextSaved: { type: 'boolean' },
|
|
1388
1574
|
revision: { type: 'integer' },
|
|
1389
1575
|
merged: { type: 'boolean' },
|
|
1390
1576
|
changed: { type: 'boolean' },
|
|
@@ -1418,7 +1604,7 @@ export function apply(ctx, config) {
|
|
|
1418
1604
|
? `memory · 需要确认:「${value.title}」与已有条目相近${value.crossLayer ? '(在另一层)' : ''}${value.sameTopic ? '(同一主题槽位)' : ''}\n${value.similar}\n要更新它,用 id 重存;确实是另一条,加 resolution="distinct"`
|
|
1419
1605
|
: value.changed === false
|
|
1420
1606
|
? `memory · unchanged ${withTag(value, `"${value.title}"`)} · ${value.id}`
|
|
1421
|
-
: `memory · ${value.merged ? 'merged' : 'saved'} ${withTag(value, `"${value.title}"`)}${value.when ? `(${value.when})` : ''} · ${value.id}${value.similar ? ` · 相近:${value.similar}` : ''}${value.archived ? ` · 已归档「${value.archived}」腾位` : ''}${value.
|
|
1607
|
+
: `memory · ${value.merged ? 'merged' : 'saved'} ${withTag(value, `"${value.title}"`)}${value.when ? `(${value.when})` : ''} · ${value.id}${value.similar ? ` · 相近:${value.similar}` : ''}${value.archived ? ` · 已归档「${value.archived}」腾位` : ''}${value.fullTextSaved ? ' · 完整原句已另存' : ''}`,
|
|
1422
1608
|
}],
|
|
1423
1609
|
},
|
|
1424
1610
|
execute: async (args, execution) => {
|
|
@@ -1496,8 +1682,9 @@ export function apply(ctx, config) {
|
|
|
1496
1682
|
title: saved.record.title,
|
|
1497
1683
|
summary: saved.record.summary,
|
|
1498
1684
|
when: saved.record.when || '',
|
|
1685
|
+
kind: saved.record.kind || 'note',
|
|
1499
1686
|
summaryShortened: Boolean(saved.draft?.summaryShortened),
|
|
1500
|
-
|
|
1687
|
+
fullTextSaved: Boolean(saved.draft?.fullTextSaved ?? saved.draft?.fullTextInBody),
|
|
1501
1688
|
revision: saved.record.revision || 1,
|
|
1502
1689
|
merged: saved.merged,
|
|
1503
1690
|
changed: !saved.noop,
|
|
@@ -1558,7 +1745,10 @@ export function apply(ctx, config) {
|
|
|
1558
1745
|
description: `按需检索已有记忆:先搜,命中再 memory_read 读正文。用户提到「记住了什么 / 上次 / 规范 / 约定 / 路径 / 凭据怎么取」,或者眼前反复出现的报错、弹窗、异常时,先查这里有没有记过解法。跨项目的通用条目以 [global] 开头,项目条目以 [项目名] 开头。中文长句可以直接当查询。每行带 id,更新时把 id 传回 memory_save。默认只列当前有效的;已经归档或被更新替换的旧说法不混进来,要查历史就在参数里显式要 history。`,
|
|
1559
1746
|
parameters: SEARCH_PARAMS,
|
|
1560
1747
|
output: {
|
|
1561
|
-
presentationMeta: (_args, value) => ({
|
|
1748
|
+
presentationMeta: (_args, value) => ({
|
|
1749
|
+
kind: 'memory-search', count: value.count, status: value.status || 'ok', refs: value.refs || [],
|
|
1750
|
+
noTriggerReason: value.noTriggerReason || '',
|
|
1751
|
+
}),
|
|
1562
1752
|
schema: {
|
|
1563
1753
|
type: 'object',
|
|
1564
1754
|
properties: {
|
|
@@ -1567,6 +1757,7 @@ export function apply(ctx, config) {
|
|
|
1567
1757
|
text: { type: 'string' },
|
|
1568
1758
|
status: { type: 'string', enum: ['ok', 'budget_exhausted', 'memory_disabled', 'needs_context'] },
|
|
1569
1759
|
reasonCode: { type: 'string' },
|
|
1760
|
+
noTriggerReason: { type: 'string' },
|
|
1570
1761
|
refs: {
|
|
1571
1762
|
type: 'array',
|
|
1572
1763
|
items: {
|
|
@@ -1578,8 +1769,10 @@ export function apply(ctx, config) {
|
|
|
1578
1769
|
revision: { type: 'integer' },
|
|
1579
1770
|
status: { type: 'string' },
|
|
1580
1771
|
origin: { type: 'string' },
|
|
1772
|
+
kind: { type: 'string' },
|
|
1773
|
+
when: { type: 'string' },
|
|
1581
1774
|
},
|
|
1582
|
-
required: ['id', 'title', 'scope', 'revision', 'status', 'origin'],
|
|
1775
|
+
required: ['id', 'title', 'scope', 'revision', 'status', 'origin', 'kind', 'when'],
|
|
1583
1776
|
additionalProperties: false,
|
|
1584
1777
|
},
|
|
1585
1778
|
},
|
|
@@ -1618,13 +1811,16 @@ export function apply(ctx, config) {
|
|
|
1618
1811
|
}
|
|
1619
1812
|
const scope = resolved.scope;
|
|
1620
1813
|
const context = resolved.context;
|
|
1814
|
+
const gateRuntime = resolved.held || win?.runtime
|
|
1815
|
+
|| { projectId: scope.key, context, experimental: resolved.experimental === true };
|
|
1621
1816
|
/** 检索只列当前任务真能用的条目;先取全量候选,过滤后再截断,避免不可用条目挤掉后面的命中。 */
|
|
1622
1817
|
const limit = args.limit ?? 3;
|
|
1623
1818
|
const rawHits = search(scope, args.query, 50, {
|
|
1624
1819
|
context,
|
|
1625
1820
|
history: args.history === true,
|
|
1626
1821
|
allowExperimental: resolved.experimental === true,
|
|
1627
|
-
}).filter((item) => !item.ownerProjectId || item.ownerProjectId === scope.key)
|
|
1822
|
+
}).filter((item) => !item.ownerProjectId || item.ownerProjectId === scope.key)
|
|
1823
|
+
.filter((item) => item.archived || view.eligibility(item, gateRuntime, 'search').ok);
|
|
1628
1824
|
const hits = rawHits.filter((item) => item.archived || item.status === 'ok').slice(0, limit);
|
|
1629
1825
|
const withheld = rawHits.filter((item) => !item.archived && item.status !== 'ok');
|
|
1630
1826
|
const scopeName = scope.key === 'global' ? '' : projectNameOf(scope);
|
|
@@ -1647,6 +1843,7 @@ export function apply(ctx, config) {
|
|
|
1647
1843
|
: '有相关的项目记忆,但当前任务明确不适用。',
|
|
1648
1844
|
status: unresolved ? 'needs_context' : 'ok',
|
|
1649
1845
|
reasonCode: unresolved ? 'applicability_unresolved' : 'not_applicable',
|
|
1846
|
+
noTriggerReason: unresolved ? '适用条件未确认' : '当前任务不适用',
|
|
1650
1847
|
refs: [],
|
|
1651
1848
|
};
|
|
1652
1849
|
if (win) {
|
|
@@ -1659,7 +1856,7 @@ export function apply(ctx, config) {
|
|
|
1659
1856
|
}
|
|
1660
1857
|
/** 没命中是正常结果,不是额度问题;空响应同样记一次本请求开销。 */
|
|
1661
1858
|
if (!hits.length) {
|
|
1662
|
-
const empty = { count: 0, chars: 0, text: '', status: 'ok', refs: [] };
|
|
1859
|
+
const empty = { count: 0, chars: 0, text: '', status: 'ok', reasonCode: 'no_match', noTriggerReason: '没有匹配的记忆', refs: [] };
|
|
1663
1860
|
if (win) {
|
|
1664
1861
|
const bytes = jsonBytes(empty);
|
|
1665
1862
|
if (!affordable(win, bytes)) return { ...empty, status: 'budget_exhausted' };
|
|
@@ -1699,6 +1896,7 @@ export function apply(ctx, config) {
|
|
|
1699
1896
|
scope: { type: 'string' },
|
|
1700
1897
|
scopeName: { type: 'string' },
|
|
1701
1898
|
origin: { type: 'string', enum: ['user_explicit', 'project_evolution'] },
|
|
1899
|
+
kind: { type: 'string' },
|
|
1702
1900
|
status: { type: 'string', enum: ['ok', 'archived', 'needs_context', 'not_applicable', 'gone', 'budget_exhausted', 'memory_disabled'] },
|
|
1703
1901
|
body: { type: 'string' },
|
|
1704
1902
|
links: { type: 'string' },
|
|
@@ -1736,6 +1934,7 @@ export function apply(ctx, config) {
|
|
|
1736
1934
|
scope: value.scope || '',
|
|
1737
1935
|
revision: value.revision || 0,
|
|
1738
1936
|
origin: value.origin === 'project_evolution' ? 'project_evolution' : 'user_explicit',
|
|
1937
|
+
recordKind: typeof value.kind === 'string' && value.kind ? value.kind : '',
|
|
1739
1938
|
status: value.status || 'ok',
|
|
1740
1939
|
}),
|
|
1741
1940
|
},
|
|
@@ -1749,8 +1948,14 @@ export function apply(ctx, config) {
|
|
|
1749
1948
|
const allowExperimental = resolved.experimental === true;
|
|
1750
1949
|
const agent = execution?.agent;
|
|
1751
1950
|
const win = windowOf(agent);
|
|
1951
|
+
const gateRuntime = resolved.held || win?.runtime
|
|
1952
|
+
|| { projectId: scope.key, context, experimental: resolved.experimental === true };
|
|
1752
1953
|
if (args.view === 'links') {
|
|
1753
|
-
const
|
|
1954
|
+
const source = locate(scope, args.id);
|
|
1955
|
+
if (source && !view.eligibility(source.record, gateRuntime, 'read').ok) {
|
|
1956
|
+
return { id: '', scope: '', status: 'needs_context', links: '' };
|
|
1957
|
+
}
|
|
1958
|
+
const found = readLinks(scope, args.id, { context, allowExperimental, runtime: gateRuntime, now: Date.now() });
|
|
1754
1959
|
if (!found) throw new Error(`没有 ${args.id} 这条记忆`);
|
|
1755
1960
|
if (found.status !== 'ok') {
|
|
1756
1961
|
const withheld = { id: '', scope: '', status: found.status, links: '' };
|
|
@@ -1778,20 +1983,8 @@ export function apply(ctx, config) {
|
|
|
1778
1983
|
}
|
|
1779
1984
|
const record = readEntry(scope, args.id, { context, allowExperimental });
|
|
1780
1985
|
if (!record) throw new Error(`没有 ${args.id} 这条记忆`);
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
}
|
|
1784
|
-
if (record.origin === 'project_evolution' && !allowExperimental) {
|
|
1785
|
-
return { id: '', scope: record.scope, status: 'needs_context', body: '' };
|
|
1786
|
-
}
|
|
1787
|
-
/** 暂停、过期或条件不匹配的实验条目只给状态,不给标题和正文。 */
|
|
1788
|
-
if (record.origin === 'project_evolution' && !view.experimentalUsable(record)) {
|
|
1789
|
-
return { id: '', scope: record.scope, status: 'needs_context', body: '' };
|
|
1790
|
-
}
|
|
1791
|
-
const learningState = record.learning && typeof record.learning === 'object' ? record.learning : {};
|
|
1792
|
-
if (record.origin === 'project_evolution' && (learningState.state === 'paused' || learningState.blockedReason)) {
|
|
1793
|
-
return { id: '', scope: record.scope, status: 'needs_context', body: '' };
|
|
1794
|
-
}
|
|
1986
|
+
const gate = view.eligibility(record, gateRuntime, 'read');
|
|
1987
|
+
if (!gate.ok) return { id: '', scope: record.scope, status: gate.status, body: '' };
|
|
1795
1988
|
/** 条件未知或不适用的条目只报状态:正文和标题都不能绕过自动召回的门槛。 */
|
|
1796
1989
|
if (record.status !== 'ok') {
|
|
1797
1990
|
return { id: '', scope: record.scope, status: record.status === 'needs_context' ? 'needs_context' : 'not_applicable', body: '' };
|
|
@@ -1806,6 +1999,7 @@ export function apply(ctx, config) {
|
|
|
1806
1999
|
scope: record.scope,
|
|
1807
2000
|
scopeName,
|
|
1808
2001
|
status: record.status,
|
|
2002
|
+
kind: record.kind || 'note',
|
|
1809
2003
|
body,
|
|
1810
2004
|
};
|
|
1811
2005
|
if (win) {
|
|
@@ -1963,7 +2157,6 @@ export function apply(ctx, config) {
|
|
|
1963
2157
|
});
|
|
1964
2158
|
|
|
1965
2159
|
if (typeof ctx.on === 'function') ctx.on('agent/pre-step', async (payload, next) => {
|
|
1966
|
-
const startedAt = Date.now();
|
|
1967
2160
|
const agent = payload?.agent;
|
|
1968
2161
|
syncMemoryTools(agent);
|
|
1969
2162
|
let decision;
|
|
@@ -1973,6 +2166,8 @@ export function apply(ctx, config) {
|
|
|
1973
2166
|
clearPrepared(agent);
|
|
1974
2167
|
throw error;
|
|
1975
2168
|
}
|
|
2169
|
+
/** 宿主等待不算本地召回时间:额度从拿到这一步的开始算。 */
|
|
2170
|
+
const startedAt = Date.now();
|
|
1976
2171
|
try {
|
|
1977
2172
|
if (!agent || decision?.kind !== 'enter') {
|
|
1978
2173
|
clearPrepared(agent);
|
|
@@ -2018,7 +2213,8 @@ export function apply(ctx, config) {
|
|
|
2018
2213
|
}
|
|
2019
2214
|
if (fingerprint !== previous.fingerprint) {
|
|
2020
2215
|
const win = windowOf(agent);
|
|
2021
|
-
|
|
2216
|
+
if (win) win.autoBudget = emptyAutoBudget();
|
|
2217
|
+
const snapshot = projectRecall(scope, previous.query, win && win.runtime ? win.runtime.context : view.trustedContext(scope, {}), win, win && win.runtime ? win.runtime : null);
|
|
2022
2218
|
let experimentText = '';
|
|
2023
2219
|
if (win && win.experiment) {
|
|
2024
2220
|
experimentText = renderExperiment(scope, win.experiment.decision, win.experiment.context, win.experiment, win);
|
|
@@ -2042,15 +2238,16 @@ export function apply(ctx, config) {
|
|
|
2042
2238
|
/** 工具模式也要开额度窗口:只是不自动注入,工具调用同样受每请求预算约束。 */
|
|
2043
2239
|
const ids = fresh.map((message) => String(message?.id ?? '')).filter(Boolean).sort();
|
|
2044
2240
|
const win = openWindow(agent, { sessionId, turn, messageIds: ids });
|
|
2241
|
+
const budgetBefore = win ? {
|
|
2242
|
+
bytes: win.bytes, bodies: win.bodies, reads: win.reads,
|
|
2243
|
+
provided: new Map(win.provided), readBodies: new Map(win.readBodies),
|
|
2244
|
+
recallHeader: win.recallHeader, experimentBytes: win.experimentBytes,
|
|
2245
|
+
autoBudget: win.autoBudget ? JSON.parse(JSON.stringify(win.autoBudget)) : null,
|
|
2246
|
+
} : null;
|
|
2045
2247
|
/** 工具模式也要记下本轮真人原话:只用于核对绑定,不自动注入。 */
|
|
2046
2248
|
const query = view.newestUserQuery(fresh).slice(0, RECALL_QUERY_CHARS);
|
|
2047
2249
|
if (win && query) win.query = query;
|
|
2048
2250
|
|
|
2049
|
-
if (recallMode() !== 'adaptive') {
|
|
2050
|
-
clearPrepared(agent);
|
|
2051
|
-
return decision;
|
|
2052
|
-
}
|
|
2053
|
-
|
|
2054
2251
|
if (!query) {
|
|
2055
2252
|
clearPrepared(agent);
|
|
2056
2253
|
return decision;
|
|
@@ -2065,29 +2262,60 @@ export function apply(ctx, config) {
|
|
|
2065
2262
|
clearPrepared(agent);
|
|
2066
2263
|
return decision;
|
|
2067
2264
|
}
|
|
2265
|
+
const batchKey = anchor.requestBatchKey(ids);
|
|
2266
|
+
/**
|
|
2267
|
+
* 工具模式也必须先有本请求上下文:memory_search/read/links 靠同一个 contextId 判项目和任务,
|
|
2268
|
+
* 不能等到自动召回分支才建,否则工具模式永远拿不到可核对的任务。
|
|
2269
|
+
*/
|
|
2270
|
+
const runtime = runtimeFor(scope, { sessionId, query, batchKey, messageIds: ids, autoRoute: recallMode() === 'adaptive' });
|
|
2271
|
+
if (win && runtime) win.runtime = runtime.held;
|
|
2272
|
+
if (recallMode() !== 'adaptive') {
|
|
2273
|
+
clearPrepared(agent);
|
|
2274
|
+
return decision;
|
|
2275
|
+
}
|
|
2068
2276
|
const context = view.trustedContext(scope, {});
|
|
2069
2277
|
const fingerprint = fingerprintOf(scope);
|
|
2070
2278
|
const controlEpoch = controlStatus(scope).epoch || '';
|
|
2071
|
-
const batchKey = anchor.requestBatchKey(ids);
|
|
2072
2279
|
dropStaleCandidate(sessionId, batchKey);
|
|
2073
|
-
|
|
2074
|
-
const
|
|
2075
|
-
|
|
2280
|
+
/** 任务上下文独立于自进化开关:普通记忆按当前任务判适用,实验规则再叠一层可用性。 */
|
|
2281
|
+
const auto = emptyAutoBudget();
|
|
2282
|
+
if (win) win.autoBudget = auto;
|
|
2283
|
+
const experiment = runtime ? experimentFor(scope, runtime, win) : null;
|
|
2284
|
+
const recalledContext = runtime ? runtime.held.context : context;
|
|
2285
|
+
const remembered = runtime && runtime.enabled
|
|
2286
|
+
? rememberFeedback(scope, sessionId, fresh, runtime.held, turn)
|
|
2076
2287
|
: null;
|
|
2077
|
-
const snapshot = projectRecall(scope, query,
|
|
2078
|
-
|
|
2288
|
+
const snapshot = projectRecall(scope, query, recalledContext, win, runtime ? runtime.held : null);
|
|
2289
|
+
/** 没发布出去的投影要退还额度:不能因为一次超时把本请求的召回额度耗掉。 */
|
|
2290
|
+
const rollbackBudget = () => {
|
|
2291
|
+
if (!win || !budgetBefore) return;
|
|
2292
|
+
win.bytes = budgetBefore.bytes;
|
|
2293
|
+
win.bodies = budgetBefore.bodies;
|
|
2294
|
+
win.reads = budgetBefore.reads;
|
|
2295
|
+
win.provided = budgetBefore.provided;
|
|
2296
|
+
win.readBodies = budgetBefore.readBodies;
|
|
2297
|
+
win.recallHeader = budgetBefore.recallHeader;
|
|
2298
|
+
win.experimentBytes = budgetBefore.experimentBytes;
|
|
2299
|
+
win.autoBudget = budgetBefore.autoBudget || emptyAutoBudget();
|
|
2300
|
+
win.experiment = null;
|
|
2301
|
+
win.contexts.clear();
|
|
2302
|
+
};
|
|
2303
|
+
if (generation !== generations.get(agent)) { rollbackBudget(); return decision; }
|
|
2079
2304
|
/** 投影期间开关被改过:丢弃这批内容,不把关闭前的记忆交给当前请求。 */
|
|
2080
2305
|
if (!memoryEnabled(scope) || (controlStatus(scope).epoch || '') !== controlEpoch) {
|
|
2306
|
+
rollbackBudget();
|
|
2081
2307
|
clearPrepared(agent);
|
|
2082
2308
|
return decision;
|
|
2083
2309
|
}
|
|
2084
2310
|
/** 超时或被取消:这一步不留半旧卡片,下一真人请求重新投影。 */
|
|
2085
2311
|
if (signal?.aborted || Date.now() - startedAt > RECALL_DEADLINE_MS) {
|
|
2312
|
+
rollbackBudget();
|
|
2086
2313
|
clearPrepared(agent);
|
|
2087
2314
|
return decision;
|
|
2088
2315
|
}
|
|
2089
2316
|
/** 实验上下文即使本轮零注入也要留着:工具绑定后同一请求还能补上规则。 */
|
|
2090
2317
|
if (!snapshot && !experiment) {
|
|
2318
|
+
rollbackBudget();
|
|
2091
2319
|
clearPrepared(agent);
|
|
2092
2320
|
return decision;
|
|
2093
2321
|
}
|
|
@@ -2196,6 +2424,13 @@ export function apply(ctx, config) {
|
|
|
2196
2424
|
const route = routeForSession(session);
|
|
2197
2425
|
if (!route) return;
|
|
2198
2426
|
const candidate = pending.candidate;
|
|
2427
|
+
/** 候选进入整理前再走一次统一准入:项目/作品身份、作用域与反馈类型都对上才花配额。 */
|
|
2428
|
+
const admission = learning.admissionOf(candidate, {
|
|
2429
|
+
projectId: scope.key,
|
|
2430
|
+
sessionId: candidate.sessionId,
|
|
2431
|
+
anchor: pending.held?.anchor || null,
|
|
2432
|
+
}, 'collect');
|
|
2433
|
+
if (!admission || admission.ok !== true) return;
|
|
2199
2434
|
const operationId = `learn_${crypto.createHash('sha256')
|
|
2200
2435
|
.update(`${scope.key}|${candidate.episodeId}|${candidate.messageIds.join(',')}`)
|
|
2201
2436
|
.digest('hex').slice(0, 16)}`;
|
|
@@ -2254,6 +2489,12 @@ export function apply(ctx, config) {
|
|
|
2254
2489
|
}
|
|
2255
2490
|
const parsed = learning.parseOutput(text, { finish, refs: input.refs, rules: input.rules, ranges });
|
|
2256
2491
|
if (!parsed.ok) throw new Error(parsed.reason);
|
|
2492
|
+
/** 用户已经把要求说成长期规则,模型仍返回空时至少留一条规则,不让这条反馈无声消失。 */
|
|
2493
|
+
const changes = parsed.changes.length
|
|
2494
|
+
? parsed.changes
|
|
2495
|
+
: (candidate.cue?.explicitLongTerm && candidate.cue?.type === 'spec_change'
|
|
2496
|
+
? [learning.pendingChangeFrom(candidate)].filter(Boolean)
|
|
2497
|
+
: []);
|
|
2257
2498
|
const commit = commitLearning(scope, {
|
|
2258
2499
|
operationId,
|
|
2259
2500
|
epoch: begin.epoch,
|
|
@@ -2263,8 +2504,9 @@ export function apply(ctx, config) {
|
|
|
2263
2504
|
sessionId: candidate.sessionId,
|
|
2264
2505
|
messageIds: candidate.messageIds,
|
|
2265
2506
|
feedbackText: candidate.text,
|
|
2507
|
+
sourceExcerpt: candidate.sourceExcerpt || learning.sourceExcerptOf(candidate.text),
|
|
2266
2508
|
dimension: candidate.cue?.dimension || '',
|
|
2267
|
-
changes
|
|
2509
|
+
changes,
|
|
2268
2510
|
preparedTargets,
|
|
2269
2511
|
usage: learning.usageOf(chunks),
|
|
2270
2512
|
explicitLongTerm: Boolean(candidate.cue.explicitLongTerm),
|