dsh-lite-memory 0.16.4 → 0.18.1
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 +10 -10
- package/TRIGGERS.md +14 -13
- package/anchor.js +259 -24
- package/client.js +1334 -215
- package/docs/BENCH.md +10 -8
- package/docs/CONVENTIONS.md +143 -0
- package/docs/RELEASE-0.17.0.md +8 -0
- package/docs/RELEASE-0.18.0.md +35 -0
- package/docs/RELEASE-0.18.1.md +18 -0
- package/index.js +395 -105
- package/learning.js +456 -48
- package/package.json +9 -3
- package/state.js +119 -7
- package/store.js +627 -75
- package/test/anchor.test.mjs +240 -0
- package/test/client.test.mjs +216 -6
- package/test/contract.test.mjs +20 -9
- package/test/crossing.test.mjs +22 -1
- package/test/evolution-read.test.mjs +23 -0
- package/test/ha.test.mjs +136 -0
- package/test/learning.test.mjs +483 -1
- package/test/palace.test.mjs +102 -0
- package/test/recall.test.mjs +13 -0
- package/test/record.test.mjs +130 -0
- package/test/runtime-context.test.mjs +186 -0
- package/test/safety.test.mjs +119 -0
- package/test/state.test.mjs +121 -2
- package/test/store.test.mjs +454 -15
- package/view.js +71 -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,11 @@ 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 : '',
|
|
647
|
+
/** 界面要能说「N 天前更新」,只给时间不给正文。 */
|
|
648
|
+
updated: typeof item.updated === 'string' ? item.updated : '',
|
|
630
649
|
}));
|
|
631
650
|
}
|
|
632
651
|
|
|
@@ -640,6 +659,10 @@ const SAVE_PARAMS = {
|
|
|
640
659
|
when: { type: 'string', description: `前置条件或适用场景(「在哪台机器 / 哪个环境 / 什么情况下」),不超过 ${LIMITS.whenChars} 字。同一标题但前置条件不同,会存成两条,不合并;更新时不给就保留原值` },
|
|
641
660
|
kind: { type: 'string', enum: ['preference', 'convention', 'procedure', 'pointer', 'pitfall', 'credential_hint', 'note'], description: '条目类型;更新时不给就保留原值' },
|
|
642
661
|
tags: { type: 'array', items: { type: 'string' }, description: '最多 5 个标签;更新时不给就保留原值' },
|
|
662
|
+
supersedes: {
|
|
663
|
+
type: 'string',
|
|
664
|
+
description: '要取代的记忆 id:新内容写成新条目,旧条目标为已被取代并保留在历史里;同层才有效,已被取代过的不能再取代',
|
|
665
|
+
},
|
|
643
666
|
resolution: {
|
|
644
667
|
type: 'string',
|
|
645
668
|
enum: ['distinct'],
|
|
@@ -778,6 +801,29 @@ function withTag(value, text) {
|
|
|
778
801
|
return tag ? `${tag} ${text}` : text;
|
|
779
802
|
}
|
|
780
803
|
|
|
804
|
+
/** 候选要能自己解释「什么时候的、为什么像」,模型才好判断是更新还是另一条。 */
|
|
805
|
+
function humanAge(iso) {
|
|
806
|
+
const at = Date.parse(String(iso || ''));
|
|
807
|
+
if (!Number.isFinite(at)) return '';
|
|
808
|
+
const days = Math.floor((Date.now() - at) / 86400000);
|
|
809
|
+
if (days <= 0) return '今天';
|
|
810
|
+
if (days === 1) return '昨天';
|
|
811
|
+
if (days < 30) return `${days} 天前`;
|
|
812
|
+
if (days < 365) return `${Math.floor(days / 30)} 个月前`;
|
|
813
|
+
return `${Math.floor(days / 365)} 年前`;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
function describeCandidate(item, options = {}) {
|
|
817
|
+
const age = humanAge(item.updated);
|
|
818
|
+
const why = options.sameTopic ? '' : item.reason === 'similar_summary' ? '摘要相近' : '标题相近';
|
|
819
|
+
const parts = [`${item.id}「${item.title}」`];
|
|
820
|
+
if (age) parts.push(`${age}更新`);
|
|
821
|
+
if (item.when) parts.push(item.when);
|
|
822
|
+
if (why) parts.push(why);
|
|
823
|
+
if (item.summary) parts.push(item.summary);
|
|
824
|
+
return parts.join(' · ');
|
|
825
|
+
}
|
|
826
|
+
|
|
781
827
|
function statusLabel(status) {
|
|
782
828
|
if (status === 'archived') return ' · 已归档';
|
|
783
829
|
if (status === 'needs_context') return ' · 适用范围待确认';
|
|
@@ -872,6 +918,14 @@ export function apply(ctx, config) {
|
|
|
872
918
|
|
|
873
919
|
const windowOf = (agent) => windows.get(agentKey(agent)) || null;
|
|
874
920
|
|
|
921
|
+
/** 自动注入账本:普通段与实验段共用 3 条 / 1200 字节,量的是拼接后的完整文本。 */
|
|
922
|
+
const emptyAutoBudget = () => ({
|
|
923
|
+
normalText: '', experimentText: '',
|
|
924
|
+
normalCards: 0, experimentCards: 0,
|
|
925
|
+
cards: 0, cardBytes: 0, experimentBytes: 0, totalBytes: 0,
|
|
926
|
+
});
|
|
927
|
+
|
|
928
|
+
|
|
875
929
|
/** 预算按规范化 JSON 字节算:转义、中文、换行都算进模型真正收到的内容。 */
|
|
876
930
|
const jsonBytes = (value) => Buffer.byteLength(JSON.stringify(value));
|
|
877
931
|
|
|
@@ -882,6 +936,16 @@ export function apply(ctx, config) {
|
|
|
882
936
|
return win.bytes + bytes <= view.REQUEST_BYTES;
|
|
883
937
|
}
|
|
884
938
|
|
|
939
|
+
/** 续写轮没有新消息:沿用上一次能核对的请求上下文。 */
|
|
940
|
+
const lastRuntime = new Map();
|
|
941
|
+
const rememberRuntime = (scope, sessionId, held) => {
|
|
942
|
+
if (!held || !sessionId) return;
|
|
943
|
+
const key = `${scope.key}\u0000${sessionId}`;
|
|
944
|
+
lastRuntime.delete(key);
|
|
945
|
+
lastRuntime.set(key, held);
|
|
946
|
+
if (lastRuntime.size > 20) lastRuntime.delete(lastRuntime.keys().next().value);
|
|
947
|
+
};
|
|
948
|
+
|
|
885
949
|
function openWindow(agent, { sessionId, turn, messageIds }) {
|
|
886
950
|
const key = agentKey(agent);
|
|
887
951
|
if (!key) return null;
|
|
@@ -892,7 +956,7 @@ export function apply(ctx, config) {
|
|
|
892
956
|
signature, sessionId, turn, messageIds,
|
|
893
957
|
bytes: 0, bodies: 0, reads: 0,
|
|
894
958
|
provided: new Map(), readBodies: new Map(), at: Date.now(),
|
|
895
|
-
experiment: null, contexts: new Map(),
|
|
959
|
+
experiment: null, contexts: new Map(), runtime: null, autoBudget: null,
|
|
896
960
|
};
|
|
897
961
|
windows.set(key, next);
|
|
898
962
|
return next;
|
|
@@ -917,6 +981,11 @@ export function apply(ctx, config) {
|
|
|
917
981
|
}
|
|
918
982
|
|
|
919
983
|
/** 两层都算:任何一层换库、换代或清空,之前准备的卡片都要失效。 */
|
|
984
|
+
/**
|
|
985
|
+
* 同一请求的召回快照只要内容没变就保持原样:命中次数、使用时间这类使用记录
|
|
986
|
+
* 不参与指纹,否则模型多读一次就会触发重投影。真正改变应看条目内容版本、
|
|
987
|
+
* 启停/控制代号与自进化代号。
|
|
988
|
+
*/
|
|
920
989
|
function fingerprintOf(scope) {
|
|
921
990
|
try {
|
|
922
991
|
return [scope, globalScopeView()].map((source) => {
|
|
@@ -924,7 +993,11 @@ export function apply(ctx, config) {
|
|
|
924
993
|
const meta = index.__meta || {};
|
|
925
994
|
const evolutionEpoch = index.__evolution && index.__evolution.epoch ? index.__evolution.epoch : '';
|
|
926
995
|
const controlEpoch = index.__control && index.__control.epoch ? index.__control.epoch : '';
|
|
927
|
-
|
|
996
|
+
const content = (index.entries || [])
|
|
997
|
+
.map((entry) => `${entry.id}:${entry.revision || 1}:${entry.status || 'active'}:${entry.updated || ''}:${entry.topicKey || ''}`)
|
|
998
|
+
.sort()
|
|
999
|
+
.join(',');
|
|
1000
|
+
return `${index.scope}:${meta.storeId || ''}:${evolutionEpoch}:${controlEpoch}:${content}`;
|
|
928
1001
|
}).join('|');
|
|
929
1002
|
} catch {
|
|
930
1003
|
return '';
|
|
@@ -934,7 +1007,8 @@ export function apply(ctx, config) {
|
|
|
934
1007
|
const globalScopeView = () => globalScope();
|
|
935
1008
|
|
|
936
1009
|
/** 纯内存:读两层当前投影,先判适用再排序,整卡装预算。 */
|
|
937
|
-
function projectRecall(scope, query, context, win) {
|
|
1010
|
+
function projectRecall(scope, query, context, win, runtime) {
|
|
1011
|
+
context = view.contextWithTask(context, runtime && runtime.task);
|
|
938
1012
|
const layers = { project: [], global: [] };
|
|
939
1013
|
const seen = new Set();
|
|
940
1014
|
for (const source of [scope, globalScope()]) {
|
|
@@ -958,38 +1032,61 @@ export function apply(ctx, config) {
|
|
|
958
1032
|
records.push(group.record);
|
|
959
1033
|
layerOf.set(group.record.id, group.scope === 'global' ? 'global' : 'project');
|
|
960
1034
|
}
|
|
961
|
-
const ranked = view.rank(records, query, {
|
|
962
|
-
|
|
963
|
-
|
|
1035
|
+
const ranked = view.rank(records, query, {
|
|
1036
|
+
limit: 8, requireApplicable: true, context,
|
|
1037
|
+
runtime: runtime || (win && win.runtime) || null,
|
|
1038
|
+
task: runtime && runtime.task ? runtime.task : null,
|
|
1039
|
+
});
|
|
1040
|
+
/** 普通记忆与实验规则共用一份自动注入预算:总条数和最终完整文本字节一起算。 */
|
|
1041
|
+
const auto = win && win.autoBudget ? win.autoBudget : null;
|
|
964
1042
|
/** 卡片上限含包装;共享请求账本也要把标题行算进去,不能只算卡片行。 */
|
|
965
1043
|
let headerCharged = win ? win.recallHeader === true : false;
|
|
966
1044
|
const lines = [];
|
|
967
1045
|
const cards = [];
|
|
968
1046
|
for (const row of ranked) {
|
|
969
|
-
|
|
1047
|
+
const usedCards = cards.length + (auto ? (auto.experimentCards || 0) : 0);
|
|
1048
|
+
if (usedCards >= view.CARD.max) break;
|
|
970
1049
|
const card = view.cardOf(row.record, layerOf.get(row.record.id));
|
|
971
1050
|
if (card.scope === 'project') card.scopeName = projectNameOf(scope);
|
|
972
1051
|
const line = renderRecallCard(card);
|
|
973
|
-
const
|
|
974
|
-
if (
|
|
1052
|
+
const nextText = `${RECALL_HEADER}\n${[...lines, line].join('\n')}`;
|
|
1053
|
+
if (auto) {
|
|
1054
|
+
/** 实验段可能先渲染:组装成最终两段再量,超了就跳过这张,不把标题留在模型侧。 */
|
|
1055
|
+
const admitted = admitAuto(win, {
|
|
1056
|
+
normal: nextText,
|
|
1057
|
+
experiment: auto.experimentText || '',
|
|
1058
|
+
normalCards: cards.length + 1,
|
|
1059
|
+
experimentCards: auto.experimentCards || 0,
|
|
1060
|
+
});
|
|
1061
|
+
if (!admitted.ok) continue;
|
|
1062
|
+
} else {
|
|
1063
|
+
const size = Buffer.byteLength(nextText);
|
|
1064
|
+
if (size > view.CARD.bytes) continue;
|
|
1065
|
+
const cost = Buffer.byteLength(`\n${line}`) + (headerCharged ? 0 : Buffer.byteLength(RECALL_HEADER));
|
|
1066
|
+
if (win && !affordable(win, cost)) continue;
|
|
1067
|
+
if (win) win.bytes += cost;
|
|
1068
|
+
}
|
|
975
1069
|
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
|
-
}
|
|
1070
|
+
if (win) win.provided.set(card.id, revision);
|
|
1071
|
+
if (win && !headerCharged) {
|
|
1072
|
+
headerCharged = true;
|
|
1073
|
+
win.recallHeader = true;
|
|
986
1074
|
}
|
|
987
|
-
used += size;
|
|
988
1075
|
lines.push(line);
|
|
989
1076
|
cards.push({ id: card.id, revision, scope: card.scope, title: card.title });
|
|
990
1077
|
}
|
|
1078
|
+
const finalText = `${RECALL_HEADER}\n${lines.join('\n')}`;
|
|
1079
|
+
if (auto) {
|
|
1080
|
+
/** 没有任何普通卡片时也要把已有实验段记账成当前状态。 */
|
|
1081
|
+
admitAuto(win, {
|
|
1082
|
+
normal: lines.length ? finalText : (auto.normalText || ''),
|
|
1083
|
+
experiment: auto.experimentText || '',
|
|
1084
|
+
normalCards: cards.length,
|
|
1085
|
+
experimentCards: auto.experimentCards || 0,
|
|
1086
|
+
});
|
|
1087
|
+
}
|
|
991
1088
|
if (!cards.length) return null;
|
|
992
|
-
return { text:
|
|
1089
|
+
return { text: finalText, cards, bytes: Buffer.byteLength(finalText), query };
|
|
993
1090
|
}
|
|
994
1091
|
|
|
995
1092
|
/* ---------------- 项目自进化:任务锚点、读取决策、上下文解析 ---------------- */
|
|
@@ -1037,9 +1134,62 @@ export function apply(ctx, config) {
|
|
|
1037
1134
|
.join('、');
|
|
1038
1135
|
}
|
|
1039
1136
|
|
|
1040
|
-
/**
|
|
1041
|
-
|
|
1137
|
+
/**
|
|
1138
|
+
* 自动注入只有一份预算:普通卡片与实验规则合计最多 3 条 / 1200 字节。
|
|
1139
|
+
* 这里按 provider 最终会看到的完整文本量字节(含标题行、ID、换行和两段之间的空行),
|
|
1140
|
+
* 先组装再测量,不能只量卡片正文再拼。
|
|
1141
|
+
*/
|
|
1142
|
+
function autoTextOf(win, normal, experiment) {
|
|
1143
|
+
const auto = win && win.autoBudget ? win.autoBudget : null;
|
|
1144
|
+
const n = normal === undefined ? String(auto?.normalText || '') : String(normal || '');
|
|
1145
|
+
const e = experiment === undefined ? String(auto?.experimentText || '') : String(experiment || '');
|
|
1146
|
+
return { normal: n, experiment: e, text: [e, n].filter(Boolean).join('\n\n') };
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
function autoBytesOf(win, normal, experiment) {
|
|
1150
|
+
return Buffer.byteLength(autoTextOf(win, normal, experiment).text);
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
/**
|
|
1154
|
+
* 原子准入:量的是最终完整文本和总条数;通过才写入预算账本。
|
|
1155
|
+
* 任何一段超限都整段回退,不截半句前提、不把标题留在模型侧。
|
|
1156
|
+
*/
|
|
1157
|
+
function admitAuto(win, { normal, experiment, normalCards, experimentCards } = {}) {
|
|
1158
|
+
if (!win || !win.autoBudget) return { ok: false, bytes: 0 };
|
|
1159
|
+
const auto = win.autoBudget;
|
|
1160
|
+
const parts = autoTextOf(win, normal, experiment);
|
|
1161
|
+
const cards = (normalCards === undefined ? (auto.normalCards || 0) : normalCards)
|
|
1162
|
+
+ (experimentCards === undefined ? (auto.experimentCards || 0) : experimentCards);
|
|
1163
|
+
const bytes = Buffer.byteLength(parts.text);
|
|
1164
|
+
if (cards > view.CARD.max || bytes > view.CARD.bytes) return { ok: false, bytes, cards };
|
|
1165
|
+
const previousTotal = Number(auto.totalBytes || 0);
|
|
1166
|
+
if (!affordable(win, bytes - previousTotal)) return { ok: false, bytes, cards };
|
|
1167
|
+
auto.normalText = parts.normal;
|
|
1168
|
+
auto.experimentText = parts.experiment;
|
|
1169
|
+
auto.normalCards = normalCards === undefined ? (auto.normalCards || 0) : normalCards;
|
|
1170
|
+
auto.experimentCards = experimentCards === undefined ? (auto.experimentCards || 0) : experimentCards;
|
|
1171
|
+
auto.cards = cards;
|
|
1172
|
+
auto.cardBytes = Buffer.byteLength(parts.normal);
|
|
1173
|
+
auto.experimentBytes = Buffer.byteLength(parts.experiment);
|
|
1174
|
+
auto.totalBytes = bytes;
|
|
1175
|
+
win.bytes += bytes - previousTotal;
|
|
1176
|
+
return { ok: true, bytes, cards };
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
/** 实验内容单独记账:同一请求里再次渲染替换旧段,只补差额。 */
|
|
1180
|
+
function chargeExperiment(win, text, cards = 1) {
|
|
1042
1181
|
if (!text) return '';
|
|
1182
|
+
const auto = win && win.autoBudget ? win.autoBudget : null;
|
|
1183
|
+
if (auto) {
|
|
1184
|
+
const admitted = admitAuto(win, {
|
|
1185
|
+
normal: auto.normalText || '',
|
|
1186
|
+
experiment: text,
|
|
1187
|
+
normalCards: auto.normalCards || 0,
|
|
1188
|
+
experimentCards: cards,
|
|
1189
|
+
});
|
|
1190
|
+
if (!admitted.ok) return '';
|
|
1191
|
+
return text;
|
|
1192
|
+
}
|
|
1043
1193
|
const bytes = Buffer.byteLength(`\n\n${text}`);
|
|
1044
1194
|
const previous = win ? (win.experimentBytes || 0) : 0;
|
|
1045
1195
|
if (bytes > EXPERIMENT_BYTES) return '';
|
|
@@ -1128,8 +1278,16 @@ export function apply(ctx, config) {
|
|
|
1128
1278
|
return chargeExperiment(win, '这些实验规则互相冲突,先按用户这次说的做,别叠加对立的规则。');
|
|
1129
1279
|
}
|
|
1130
1280
|
const parts = { header: EXPERIMENT_RULES_HEADER, items: [], tail: held && held.contextId ? `\ncontextId=${held.contextId}` : '' };
|
|
1281
|
+
/** 与普通记忆共用 3 张卡片 / 1200 字节:普通卡片先占掉的位子和字节,实验规则不能再拿。 */
|
|
1282
|
+
const auto = win && win.autoBudget ? win.autoBudget : null;
|
|
1283
|
+
const cardCap = auto
|
|
1284
|
+
? Math.max(0, view.CARD.max - (auto.normalCards || 0) - (auto.experimentCards || 0))
|
|
1285
|
+
: view.CARD.max;
|
|
1286
|
+
const autoCap = auto
|
|
1287
|
+
? Math.max(0, view.CARD.bytes - Buffer.byteLength(auto.normalText || '') - 2)
|
|
1288
|
+
: EXPERIMENT_BYTES;
|
|
1131
1289
|
let used = Buffer.byteLength(EXPERIMENT_RULES_HEADER) + 1;
|
|
1132
|
-
for (const record of matched.slice(0,
|
|
1290
|
+
for (const record of matched.slice(0, cardCap)) {
|
|
1133
1291
|
const body = String(record.body || '');
|
|
1134
1292
|
const short = body && body.length <= EXPERIMENT_SHORT_CHARS && body.split('\n').length <= 4;
|
|
1135
1293
|
const conditions = conditionLabel(record.conditions);
|
|
@@ -1141,7 +1299,7 @@ export function apply(ctx, config) {
|
|
|
1141
1299
|
? `- 「${record.title}」${conditions ? `(${conditions})` : ''} · ${record.id}:${body}${check}`
|
|
1142
1300
|
: `- 「${record.title}」 · ${record.id}(用途:${record.summary || record.title};要用时 memory_read 取正文)`;
|
|
1143
1301
|
const size = Buffer.byteLength(`\n${line}`);
|
|
1144
|
-
if (used + size >
|
|
1302
|
+
if (used + size > autoCap) break;
|
|
1145
1303
|
used += size;
|
|
1146
1304
|
const trialLearning = record.learning && typeof record.learning === 'object' ? record.learning : {};
|
|
1147
1305
|
const trialUntil = trialLearning.state === 'trial' && Number.isFinite(Date.parse(String(trialLearning.trialUntil || '')))
|
|
@@ -1154,71 +1312,138 @@ export function apply(ctx, config) {
|
|
|
1154
1312
|
return chargeExperiment(win, '有些实验规则的适用条件还没确认,先按用户这次说的做,别把它们当本轮依据。');
|
|
1155
1313
|
}
|
|
1156
1314
|
if (!parts.items.length) return '';
|
|
1315
|
+
/** 先按最终整段文本过预算,再登记可发布内容;没通过就一条都不留。 */
|
|
1316
|
+
const admitted = chargeExperiment(win, experimentTextFrom(parts), parts.items.length);
|
|
1317
|
+
if (!admitted) {
|
|
1318
|
+
if (held) { held.experimentParts = null; held.injectedIds = []; held.validUntil = ''; }
|
|
1319
|
+
return '';
|
|
1320
|
+
}
|
|
1157
1321
|
if (held) held.experimentParts = parts;
|
|
1158
1322
|
/** 兼容旧读取点:整体最早到期只取本轮真正注入的条目,不再被无关试验拖下水。 */
|
|
1159
1323
|
if (held) {
|
|
1160
1324
|
const deadlines = parts.items.map((item) => Date.parse(String(item.validUntil))).filter(Number.isFinite);
|
|
1161
1325
|
if (deadlines.length) held.validUntil = new Date(Math.min(...deadlines)).toISOString();
|
|
1162
1326
|
}
|
|
1163
|
-
return
|
|
1327
|
+
return admitted;
|
|
1164
1328
|
}
|
|
1165
1329
|
|
|
1166
|
-
/**
|
|
1167
|
-
|
|
1168
|
-
|
|
1169
|
-
|
|
1170
|
-
|
|
1171
|
-
if (!
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
1330
|
+
/**
|
|
1331
|
+
* 一次真人请求只路由一次:不管自进化开没开,都先建独立的任务上下文。
|
|
1332
|
+
* 普通记忆按这个上下文判适用;实验规则只是在这个上下文上再加一层可用性。
|
|
1333
|
+
*/
|
|
1334
|
+
function runtimeFor(scope, { sessionId, query, batchKey, messageIds, autoRoute = true }) {
|
|
1335
|
+
if (!batchKey) return null;
|
|
1336
|
+
let status = null;
|
|
1337
|
+
try { status = evolutionStatus(scope); } catch { status = null; }
|
|
1338
|
+
const enabled = Boolean(status && status.enabled === true && status.compatible !== false && scope.confirmed === true);
|
|
1339
|
+
/** 任务上下文独立于自进化:项目状态读不出来也要按当前任务判普通记忆,不能整段取消召回。 */
|
|
1340
|
+
let tasks = null;
|
|
1341
|
+
try { tasks = taskState(scope); } catch { tasks = null; }
|
|
1342
|
+
tasks = tasks && typeof tasks === 'object' ? tasks : {};
|
|
1175
1343
|
const ranges = rangesOf(scope);
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1344
|
+
/**
|
|
1345
|
+
* 工具模式不自动路由:上下文照样要有,但不能因为一句点名就改任务状态;
|
|
1346
|
+
* 真正的绑定留给 memory_search bindForRequest,绑定后状态是 proposed。
|
|
1347
|
+
*/
|
|
1348
|
+
const routed = autoRoute
|
|
1349
|
+
? anchor.routeTask(tasks, {
|
|
1350
|
+
projectId: scope.key,
|
|
1351
|
+
sessionId,
|
|
1352
|
+
batchKey,
|
|
1353
|
+
messageIds,
|
|
1354
|
+
query,
|
|
1355
|
+
registeredSeries: ranges.series,
|
|
1356
|
+
defaults: tasks.defaults,
|
|
1357
|
+
now: Date.now(),
|
|
1358
|
+
})
|
|
1359
|
+
: (() => {
|
|
1360
|
+
const sessionAnchor = tasks.sessions && tasks.sessions[sessionId] ? tasks.sessions[sessionId] : null;
|
|
1361
|
+
const episode = sessionAnchor && sessionAnchor.episodeId && tasks.episodes
|
|
1362
|
+
? tasks.episodes[sessionAnchor.episodeId] || null
|
|
1363
|
+
: null;
|
|
1364
|
+
return { tasks, anchor: sessionAnchor, episode, changed: false, decision: { kind: 'none', reason: 'tool_only' } };
|
|
1365
|
+
})();
|
|
1186
1366
|
if (routed.changed) {
|
|
1187
|
-
|
|
1188
|
-
|
|
1367
|
+
/** 落盘失败也不能取消本轮任务判断:进程内上下文仍然有效,只是不留任务历史。 */
|
|
1368
|
+
saveTaskState(scope, routed.tasks, { operationId: `task_${batchKey}` });
|
|
1189
1369
|
}
|
|
1190
1370
|
const context = view.trustedContext(scope, anchor.contextOfAnchor(routed.anchor, {}));
|
|
1191
1371
|
const thanks = anchor.isThanks(query);
|
|
1192
1372
|
const decision = anchor.decideRead({
|
|
1193
1373
|
mode: recallMode(),
|
|
1194
|
-
enabled
|
|
1195
|
-
identityConfirmed: true,
|
|
1374
|
+
enabled,
|
|
1375
|
+
identityConfirmed: scope.confirmed === true,
|
|
1196
1376
|
anchor: routed.anchor,
|
|
1197
1377
|
ranges: [...ranges.taskKinds, ...ranges.series],
|
|
1198
1378
|
thanks,
|
|
1199
1379
|
boundForRequest: false,
|
|
1200
1380
|
});
|
|
1201
1381
|
const contextId = anchor.contextIdFor(scope.key, batchKey, crypto.randomUUID().slice(0, 8));
|
|
1202
|
-
const episode = routed.
|
|
1382
|
+
const episode = routed.episode
|
|
1383
|
+
|| (routed.anchor && routed.tasks && routed.tasks.episodes ? routed.tasks.episodes[routed.anchor.episodeId] : null);
|
|
1203
1384
|
let epoch = '';
|
|
1204
|
-
try { epoch = String(
|
|
1385
|
+
try { epoch = String(status && status.epoch ? status.epoch : ''); } catch { epoch = ''; }
|
|
1386
|
+
const anchorTask = routed.anchor || {};
|
|
1387
|
+
/** 与自进化开关无关的请求上下文:普通记忆也靠它判断项目、任务与代号。 */
|
|
1388
|
+
const requestContext = anchor.buildRuntimeContext({
|
|
1389
|
+
projectId: scope.key,
|
|
1390
|
+
sessionId,
|
|
1391
|
+
batchKey,
|
|
1392
|
+
messageIds,
|
|
1393
|
+
controlEpoch: epoch,
|
|
1394
|
+
query,
|
|
1395
|
+
anchor: anchorTask,
|
|
1396
|
+
episode,
|
|
1397
|
+
});
|
|
1398
|
+
const task = {
|
|
1399
|
+
...requestContext.task,
|
|
1400
|
+
kind: anchorTask.taskKind || requestContext.task.kind || context.taskKind || '',
|
|
1401
|
+
seriesId: anchorTask.seriesId || requestContext.task.seriesId || context.seriesId || '',
|
|
1402
|
+
artifactKey: requestContext.task.artifactKey || context.artifactKey || '',
|
|
1403
|
+
phase: requestContext.task.phase || context.phase || '',
|
|
1404
|
+
};
|
|
1205
1405
|
const held = {
|
|
1206
|
-
contextId,
|
|
1207
|
-
|
|
1406
|
+
contextId, requestId: requestContext.requestId, controlEpoch: epoch,
|
|
1407
|
+
projectKey: scope.key, sessionId, batchKey, messageIds, query,
|
|
1408
|
+
context, task, anchor: routed.anchor, ranges, decision, epoch,
|
|
1409
|
+
experimental: enabled,
|
|
1208
1410
|
episodeUpdatedAt: episode ? episode.updatedAt : '', injectedIds: [],
|
|
1209
1411
|
at: Date.now(),
|
|
1210
1412
|
};
|
|
1211
|
-
|
|
1212
|
-
|
|
1213
|
-
|
|
1413
|
+
return { held, decision, ranges, enabled, status };
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
/** 实验正文只在自进化开着时登记;普通记忆共享同一个任务上下文。 */
|
|
1417
|
+
function experimentFor(scope, runtime, win) {
|
|
1418
|
+
if (!runtime || !runtime.held) return null;
|
|
1419
|
+
const held = runtime.held;
|
|
1420
|
+
if (win) win.runtime = held;
|
|
1421
|
+
if (!runtime.enabled) return null;
|
|
1422
|
+
if (!experimentStillCurrent(scope, held)) return { text: '', held, decision: held.decision };
|
|
1423
|
+
if (win) {
|
|
1424
|
+
win.experiment = held;
|
|
1425
|
+
win.contexts.set(held.contextId, held);
|
|
1426
|
+
}
|
|
1427
|
+
return { text: renderExperiment(scope, held.decision, held.context, held, win), decision: held.decision, held };
|
|
1214
1428
|
}
|
|
1215
1429
|
|
|
1216
|
-
/** 自动路径与工具共用一次解析:没有本轮上下文就退回普通检索,绝不猜一个空上下文。 */
|
|
1217
1430
|
function resolveRequestContext(execution, optionalContextId) {
|
|
1218
1431
|
const scope = scopeOf(cwdOf(execution));
|
|
1219
1432
|
const win = windowOf(execution && execution.agent);
|
|
1220
1433
|
const contextId = typeof optionalContextId === 'string' ? optionalContextId.trim() : '';
|
|
1221
1434
|
if (!contextId) {
|
|
1435
|
+
/** 没有显式上下文时,用本请求已经路由出来的任务;这层不依赖自进化开关。 */
|
|
1436
|
+
const live = win && win.runtime ? win.runtime : null;
|
|
1437
|
+
const runtime = live
|
|
1438
|
+
|| (win && win.sessionId ? lastRuntime.get(`${scope.key}\u0000${win.sessionId}`) : null);
|
|
1439
|
+
const sameRequest = runtime
|
|
1440
|
+
&& runtime.projectKey === scope.key
|
|
1441
|
+
&& (!runtime.sessionId || runtime.sessionId === String(win.sessionId || ''))
|
|
1442
|
+
&& (runtime === live ? runtime.batchKey === anchor.requestBatchKey(win.messageIds || []) : true);
|
|
1443
|
+
if (sameRequest) {
|
|
1444
|
+
/** 普通记忆可以直接用本请求上下文;实验正文仍要求显式 contextId,不能顺手放行。 */
|
|
1445
|
+
return { ok: true, scope, win, context: runtime.context, held: runtime, experimental: false };
|
|
1446
|
+
}
|
|
1222
1447
|
return { ok: true, scope, win, context: view.trustedContext(scope, {}), experimental: false };
|
|
1223
1448
|
}
|
|
1224
1449
|
/** 先认项目再找本地记录:别的项目的上下文要明确拒绝,不能只报过期。 */
|
|
@@ -1371,6 +1596,7 @@ export function apply(ctx, config) {
|
|
|
1371
1596
|
scope: value.scope || '',
|
|
1372
1597
|
revision: value.revision || 0,
|
|
1373
1598
|
changed: value.changed === true,
|
|
1599
|
+
recordKind: typeof value.kind === 'string' && value.kind ? value.kind : '',
|
|
1374
1600
|
mode: value.needsScope ? 'needs_scope'
|
|
1375
1601
|
: value.existingId ? 'duplicate'
|
|
1376
1602
|
: value.needsResolution ? 'needs_resolution'
|
|
@@ -1383,8 +1609,9 @@ export function apply(ctx, config) {
|
|
|
1383
1609
|
title: { type: 'string' },
|
|
1384
1610
|
summary: { type: 'string' },
|
|
1385
1611
|
when: { type: 'string' },
|
|
1612
|
+
kind: { type: 'string' },
|
|
1386
1613
|
summaryShortened: { type: 'boolean' },
|
|
1387
|
-
|
|
1614
|
+
fullTextSaved: { type: 'boolean' },
|
|
1388
1615
|
revision: { type: 'integer' },
|
|
1389
1616
|
merged: { type: 'boolean' },
|
|
1390
1617
|
changed: { type: 'boolean' },
|
|
@@ -1399,7 +1626,9 @@ export function apply(ctx, config) {
|
|
|
1399
1626
|
sameTopic: { type: 'boolean' },
|
|
1400
1627
|
similarIds: { type: 'string' },
|
|
1401
1628
|
similar: { type: 'string' },
|
|
1629
|
+
supersededId: { type: 'string' },
|
|
1402
1630
|
archived: { type: 'string' },
|
|
1631
|
+
pendingConflicts: { type: 'integer' },
|
|
1403
1632
|
},
|
|
1404
1633
|
required: ['id', 'title', 'summary', 'when', 'revision', 'merged', 'changed', 'scope', 'needsResolution', 'archived'],
|
|
1405
1634
|
additionalProperties: false,
|
|
@@ -1413,12 +1642,12 @@ export function apply(ctx, config) {
|
|
|
1413
1642
|
? `memory · 当前目录还不能确认属于哪个项目:「${value.title}」\n先确认项目根目录,或显式 scope="global"`
|
|
1414
1643
|
: `memory · 归属还没定:「${value.title}」\n补 memoryClass(personal_preference / project_fact / environment_fact / portable_procedure)或显式 scope;设备事实还要给设备条件`
|
|
1415
1644
|
: value.existingId
|
|
1416
|
-
? `memory · 已有相同记忆:「${value.title}
|
|
1645
|
+
? `memory · 已有相同记忆:「${value.title}」(${value.existingScope})\n要改成另一条,加 resolution="distinct"`
|
|
1417
1646
|
: value.needsResolution
|
|
1418
1647
|
? `memory · 需要确认:「${value.title}」与已有条目相近${value.crossLayer ? '(在另一层)' : ''}${value.sameTopic ? '(同一主题槽位)' : ''}\n${value.similar}\n要更新它,用 id 重存;确实是另一条,加 resolution="distinct"`
|
|
1419
1648
|
: value.changed === false
|
|
1420
|
-
? `memory · unchanged ${withTag(value, `"${value.title}"`)}
|
|
1421
|
-
: `memory · ${value.merged ? 'merged' : 'saved'} ${withTag(value, `"${value.title}"`)}${value.when ? `(${value.when})` : ''}
|
|
1649
|
+
? `memory · unchanged ${withTag(value, `"${value.title}"`)}`
|
|
1650
|
+
: `memory · ${value.merged ? 'merged' : 'saved'} ${withTag(value, `"${value.title}"`)}${value.when ? `(${value.when})` : ''}${value.similar ? ` · 相近:${value.similar}` : ''}${value.archived ? ` · 已归档「${value.archived}」腾位` : ''}${value.fullTextSaved ? ' · 完整原句已另存' : ''}${value.supersededId ? ' · 已取代旧说法' : ''}`,
|
|
1422
1651
|
}],
|
|
1423
1652
|
},
|
|
1424
1653
|
execute: async (args, execution) => {
|
|
@@ -1486,8 +1715,9 @@ export function apply(ctx, config) {
|
|
|
1486
1715
|
crossLayer: Boolean(saved.crossLayer),
|
|
1487
1716
|
sameTopic: Boolean(saved.sameTopic),
|
|
1488
1717
|
similarIds: saved.similarList.map((item) => item.id).join('、'),
|
|
1489
|
-
similar: saved.similarList.map((item) =>
|
|
1718
|
+
similar: saved.similarList.map((item) => describeCandidate(item, { sameTopic: saved.sameTopic })).join(';'),
|
|
1490
1719
|
archived: '',
|
|
1720
|
+
pendingConflicts: Number(saved.pendingConflicts) || 0,
|
|
1491
1721
|
};
|
|
1492
1722
|
}
|
|
1493
1723
|
recordCitations(scope, sessionIdOf(execution), [saved.record]);
|
|
@@ -1496,8 +1726,9 @@ export function apply(ctx, config) {
|
|
|
1496
1726
|
title: saved.record.title,
|
|
1497
1727
|
summary: saved.record.summary,
|
|
1498
1728
|
when: saved.record.when || '',
|
|
1729
|
+
kind: saved.record.kind || 'note',
|
|
1499
1730
|
summaryShortened: Boolean(saved.draft?.summaryShortened),
|
|
1500
|
-
|
|
1731
|
+
fullTextSaved: Boolean(saved.draft?.fullTextSaved ?? saved.draft?.fullTextInBody),
|
|
1501
1732
|
revision: saved.record.revision || 1,
|
|
1502
1733
|
merged: saved.merged,
|
|
1503
1734
|
changed: !saved.noop,
|
|
@@ -1506,8 +1737,10 @@ export function apply(ctx, config) {
|
|
|
1506
1737
|
needsScope: false,
|
|
1507
1738
|
needsResolution: false,
|
|
1508
1739
|
similarIds: saved.similarList.map((item) => item.id).join('、'),
|
|
1740
|
+
supersededId: String(args.supersedes || ''),
|
|
1509
1741
|
archived: (saved.archived || []).map((item) => item.title).join('、'),
|
|
1510
|
-
|
|
1742
|
+
pendingConflicts: Number(saved.pendingConflicts) || 0,
|
|
1743
|
+
...(saved.similar ? { similar: `「${saved.similar.title}」` } : {}),
|
|
1511
1744
|
};
|
|
1512
1745
|
},
|
|
1513
1746
|
}), 'lite-memory: memory_save');
|
|
@@ -1534,7 +1767,7 @@ export function apply(ctx, config) {
|
|
|
1534
1767
|
render: (_args, value) => [{
|
|
1535
1768
|
type: 'text',
|
|
1536
1769
|
text: value.asked
|
|
1537
|
-
? `memory · suggested "${value.title}"
|
|
1770
|
+
? `memory · suggested "${value.title}"${value.targetId ? ` · 确认后更新「${value.targetTitle}」` : ''}`
|
|
1538
1771
|
: `memory · skipped${value.reason ? `\n${value.reason}` : ''}`,
|
|
1539
1772
|
}],
|
|
1540
1773
|
},
|
|
@@ -1558,7 +1791,10 @@ export function apply(ctx, config) {
|
|
|
1558
1791
|
description: `按需检索已有记忆:先搜,命中再 memory_read 读正文。用户提到「记住了什么 / 上次 / 规范 / 约定 / 路径 / 凭据怎么取」,或者眼前反复出现的报错、弹窗、异常时,先查这里有没有记过解法。跨项目的通用条目以 [global] 开头,项目条目以 [项目名] 开头。中文长句可以直接当查询。每行带 id,更新时把 id 传回 memory_save。默认只列当前有效的;已经归档或被更新替换的旧说法不混进来,要查历史就在参数里显式要 history。`,
|
|
1559
1792
|
parameters: SEARCH_PARAMS,
|
|
1560
1793
|
output: {
|
|
1561
|
-
presentationMeta: (_args, value) => ({
|
|
1794
|
+
presentationMeta: (_args, value) => ({
|
|
1795
|
+
kind: 'memory-search', count: value.count, status: value.status || 'ok', refs: value.refs || [],
|
|
1796
|
+
noTriggerReason: value.noTriggerReason || '',
|
|
1797
|
+
}),
|
|
1562
1798
|
schema: {
|
|
1563
1799
|
type: 'object',
|
|
1564
1800
|
properties: {
|
|
@@ -1567,6 +1803,7 @@ export function apply(ctx, config) {
|
|
|
1567
1803
|
text: { type: 'string' },
|
|
1568
1804
|
status: { type: 'string', enum: ['ok', 'budget_exhausted', 'memory_disabled', 'needs_context'] },
|
|
1569
1805
|
reasonCode: { type: 'string' },
|
|
1806
|
+
noTriggerReason: { type: 'string' },
|
|
1570
1807
|
refs: {
|
|
1571
1808
|
type: 'array',
|
|
1572
1809
|
items: {
|
|
@@ -1578,8 +1815,11 @@ export function apply(ctx, config) {
|
|
|
1578
1815
|
revision: { type: 'integer' },
|
|
1579
1816
|
status: { type: 'string' },
|
|
1580
1817
|
origin: { type: 'string' },
|
|
1818
|
+
kind: { type: 'string' },
|
|
1819
|
+
when: { type: 'string' },
|
|
1820
|
+
updated: { type: 'string' }, // ui-copy-ignore
|
|
1581
1821
|
},
|
|
1582
|
-
required: ['id', 'title', 'scope', 'revision', 'status', 'origin'],
|
|
1822
|
+
required: ['id', 'title', 'scope', 'revision', 'status', 'origin', 'kind', 'when'],
|
|
1583
1823
|
additionalProperties: false,
|
|
1584
1824
|
},
|
|
1585
1825
|
},
|
|
@@ -1618,13 +1858,16 @@ export function apply(ctx, config) {
|
|
|
1618
1858
|
}
|
|
1619
1859
|
const scope = resolved.scope;
|
|
1620
1860
|
const context = resolved.context;
|
|
1861
|
+
const gateRuntime = resolved.held || win?.runtime
|
|
1862
|
+
|| { projectId: scope.key, context, experimental: resolved.experimental === true };
|
|
1621
1863
|
/** 检索只列当前任务真能用的条目;先取全量候选,过滤后再截断,避免不可用条目挤掉后面的命中。 */
|
|
1622
1864
|
const limit = args.limit ?? 3;
|
|
1623
1865
|
const rawHits = search(scope, args.query, 50, {
|
|
1624
1866
|
context,
|
|
1625
1867
|
history: args.history === true,
|
|
1626
1868
|
allowExperimental: resolved.experimental === true,
|
|
1627
|
-
}).filter((item) => !item.ownerProjectId || item.ownerProjectId === scope.key)
|
|
1869
|
+
}).filter((item) => !item.ownerProjectId || item.ownerProjectId === scope.key)
|
|
1870
|
+
.filter((item) => item.archived || view.eligibility(item, gateRuntime, 'search').ok);
|
|
1628
1871
|
const hits = rawHits.filter((item) => item.archived || item.status === 'ok').slice(0, limit);
|
|
1629
1872
|
const withheld = rawHits.filter((item) => !item.archived && item.status !== 'ok');
|
|
1630
1873
|
const scopeName = scope.key === 'global' ? '' : projectNameOf(scope);
|
|
@@ -1647,6 +1890,7 @@ export function apply(ctx, config) {
|
|
|
1647
1890
|
: '有相关的项目记忆,但当前任务明确不适用。',
|
|
1648
1891
|
status: unresolved ? 'needs_context' : 'ok',
|
|
1649
1892
|
reasonCode: unresolved ? 'applicability_unresolved' : 'not_applicable',
|
|
1893
|
+
noTriggerReason: unresolved ? '适用条件未确认' : '当前任务不适用',
|
|
1650
1894
|
refs: [],
|
|
1651
1895
|
};
|
|
1652
1896
|
if (win) {
|
|
@@ -1659,7 +1903,7 @@ export function apply(ctx, config) {
|
|
|
1659
1903
|
}
|
|
1660
1904
|
/** 没命中是正常结果,不是额度问题;空响应同样记一次本请求开销。 */
|
|
1661
1905
|
if (!hits.length) {
|
|
1662
|
-
const empty = { count: 0, chars: 0, text: '', status: 'ok', refs: [] };
|
|
1906
|
+
const empty = { count: 0, chars: 0, text: '', status: 'ok', reasonCode: 'no_match', noTriggerReason: '没有匹配的记忆', refs: [] };
|
|
1663
1907
|
if (win) {
|
|
1664
1908
|
const bytes = jsonBytes(empty);
|
|
1665
1909
|
if (!affordable(win, bytes)) return { ...empty, status: 'budget_exhausted' };
|
|
@@ -1699,6 +1943,7 @@ export function apply(ctx, config) {
|
|
|
1699
1943
|
scope: { type: 'string' },
|
|
1700
1944
|
scopeName: { type: 'string' },
|
|
1701
1945
|
origin: { type: 'string', enum: ['user_explicit', 'project_evolution'] },
|
|
1946
|
+
kind: { type: 'string' },
|
|
1702
1947
|
status: { type: 'string', enum: ['ok', 'archived', 'needs_context', 'not_applicable', 'gone', 'budget_exhausted', 'memory_disabled'] },
|
|
1703
1948
|
body: { type: 'string' },
|
|
1704
1949
|
links: { type: 'string' },
|
|
@@ -1716,11 +1961,11 @@ export function apply(ctx, config) {
|
|
|
1716
1961
|
: value.repeat
|
|
1717
1962
|
? 'memory · 本请求已读过这条记忆'
|
|
1718
1963
|
: value.body
|
|
1719
|
-
? `memory · ${withTag(value, `「${value.title}」`)}${value.when ? `(${value.when})` : ''}
|
|
1964
|
+
? `memory · ${withTag(value, `「${value.title}」`)}${value.when ? `(${value.when})` : ''}${statusLabel(value.status)}\n${value.body}`
|
|
1720
1965
|
: value.links !== undefined && value.status === 'ok'
|
|
1721
1966
|
? `memory · 「${value.title}」的相关记忆\n${value.links || '(无)'}`
|
|
1722
1967
|
: value.status === 'needs_context'
|
|
1723
|
-
? 'memory ·
|
|
1968
|
+
? 'memory · 这条记忆的适用条件还没确认,先说明当前任务再读'
|
|
1724
1969
|
: value.status === 'not_applicable'
|
|
1725
1970
|
? 'memory · 这条记忆不适用于当前任务'
|
|
1726
1971
|
: value.status === 'archived'
|
|
@@ -1736,6 +1981,7 @@ export function apply(ctx, config) {
|
|
|
1736
1981
|
scope: value.scope || '',
|
|
1737
1982
|
revision: value.revision || 0,
|
|
1738
1983
|
origin: value.origin === 'project_evolution' ? 'project_evolution' : 'user_explicit',
|
|
1984
|
+
recordKind: typeof value.kind === 'string' && value.kind ? value.kind : '',
|
|
1739
1985
|
status: value.status || 'ok',
|
|
1740
1986
|
}),
|
|
1741
1987
|
},
|
|
@@ -1749,8 +1995,14 @@ export function apply(ctx, config) {
|
|
|
1749
1995
|
const allowExperimental = resolved.experimental === true;
|
|
1750
1996
|
const agent = execution?.agent;
|
|
1751
1997
|
const win = windowOf(agent);
|
|
1998
|
+
const gateRuntime = resolved.held || win?.runtime
|
|
1999
|
+
|| { projectId: scope.key, context, experimental: resolved.experimental === true };
|
|
1752
2000
|
if (args.view === 'links') {
|
|
1753
|
-
const
|
|
2001
|
+
const source = locate(scope, args.id);
|
|
2002
|
+
if (source && !view.eligibility(source.record, gateRuntime, 'read').ok) {
|
|
2003
|
+
return { id: '', scope: '', status: 'needs_context', links: '' };
|
|
2004
|
+
}
|
|
2005
|
+
const found = readLinks(scope, args.id, { context, allowExperimental, runtime: gateRuntime, now: Date.now() });
|
|
1754
2006
|
if (!found) throw new Error(`没有 ${args.id} 这条记忆`);
|
|
1755
2007
|
if (found.status !== 'ok') {
|
|
1756
2008
|
const withheld = { id: '', scope: '', status: found.status, links: '' };
|
|
@@ -1778,20 +2030,8 @@ export function apply(ctx, config) {
|
|
|
1778
2030
|
}
|
|
1779
2031
|
const record = readEntry(scope, args.id, { context, allowExperimental });
|
|
1780
2032
|
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
|
-
}
|
|
2033
|
+
const gate = view.eligibility(record, gateRuntime, 'read');
|
|
2034
|
+
if (!gate.ok) return { id: '', scope: record.scope, status: gate.status, body: '' };
|
|
1795
2035
|
/** 条件未知或不适用的条目只报状态:正文和标题都不能绕过自动召回的门槛。 */
|
|
1796
2036
|
if (record.status !== 'ok') {
|
|
1797
2037
|
return { id: '', scope: record.scope, status: record.status === 'needs_context' ? 'needs_context' : 'not_applicable', body: '' };
|
|
@@ -1806,6 +2046,7 @@ export function apply(ctx, config) {
|
|
|
1806
2046
|
scope: record.scope,
|
|
1807
2047
|
scopeName,
|
|
1808
2048
|
status: record.status,
|
|
2049
|
+
kind: record.kind || 'note',
|
|
1809
2050
|
body,
|
|
1810
2051
|
};
|
|
1811
2052
|
if (win) {
|
|
@@ -1853,12 +2094,12 @@ export function apply(ctx, config) {
|
|
|
1853
2094
|
text: value.mode === 'memory_disabled'
|
|
1854
2095
|
? 'memory · 本项目记忆已关闭,先在面板打开'
|
|
1855
2096
|
: value.mode === 'purged'
|
|
1856
|
-
? `memory · forgotten "${value.title}"
|
|
2097
|
+
? `memory · forgotten "${value.title}"`
|
|
1857
2098
|
: value.mode === 'purge_pending'
|
|
1858
|
-
? `memory · 清除未完成 ·
|
|
2099
|
+
? `memory · 清除未完成 · 「${value.title || '这条记忆'}」,重启后会继续`
|
|
1859
2100
|
: value.mode === 'needs_resolution'
|
|
1860
|
-
? `memory · 先确认是归档还是彻底忘掉 ·
|
|
1861
|
-
: `memory · archived ${withTag(value, `"${value.title}"`)}
|
|
2101
|
+
? `memory · 先确认是归档还是彻底忘掉 · 「${value.title || '这条记忆'}」`
|
|
2102
|
+
: `memory · archived ${withTag(value, `"${value.title}"`)},可找回`,
|
|
1862
2103
|
}],
|
|
1863
2104
|
presentationMeta: (_args, value) => ({
|
|
1864
2105
|
kind: 'memory-forget',
|
|
@@ -1875,7 +2116,7 @@ export function apply(ctx, config) {
|
|
|
1875
2116
|
const located = locate(scope, args.id);
|
|
1876
2117
|
if (!located && !args.mode) throw new Error(`没有 ${args.id} 这条记忆`);
|
|
1877
2118
|
if (!args.mode) {
|
|
1878
|
-
return { id: args.id, mode: 'needs_resolution', scope: located.scope.key === 'global' ? 'global' : 'project' };
|
|
2119
|
+
return { id: args.id, title: located.record.title || '', mode: 'needs_resolution', scope: located.scope.key === 'global' ? 'global' : 'project' };
|
|
1879
2120
|
}
|
|
1880
2121
|
let dropped;
|
|
1881
2122
|
try {
|
|
@@ -1963,7 +2204,6 @@ export function apply(ctx, config) {
|
|
|
1963
2204
|
});
|
|
1964
2205
|
|
|
1965
2206
|
if (typeof ctx.on === 'function') ctx.on('agent/pre-step', async (payload, next) => {
|
|
1966
|
-
const startedAt = Date.now();
|
|
1967
2207
|
const agent = payload?.agent;
|
|
1968
2208
|
syncMemoryTools(agent);
|
|
1969
2209
|
let decision;
|
|
@@ -1973,6 +2213,8 @@ export function apply(ctx, config) {
|
|
|
1973
2213
|
clearPrepared(agent);
|
|
1974
2214
|
throw error;
|
|
1975
2215
|
}
|
|
2216
|
+
/** 宿主等待不算本地召回时间:额度从拿到这一步的开始算。 */
|
|
2217
|
+
const startedAt = Date.now();
|
|
1976
2218
|
try {
|
|
1977
2219
|
if (!agent || decision?.kind !== 'enter') {
|
|
1978
2220
|
clearPrepared(agent);
|
|
@@ -2018,7 +2260,8 @@ export function apply(ctx, config) {
|
|
|
2018
2260
|
}
|
|
2019
2261
|
if (fingerprint !== previous.fingerprint) {
|
|
2020
2262
|
const win = windowOf(agent);
|
|
2021
|
-
|
|
2263
|
+
if (win) win.autoBudget = emptyAutoBudget();
|
|
2264
|
+
const snapshot = projectRecall(scope, previous.query, win && win.runtime ? win.runtime.context : view.trustedContext(scope, {}), win, win && win.runtime ? win.runtime : null);
|
|
2022
2265
|
let experimentText = '';
|
|
2023
2266
|
if (win && win.experiment) {
|
|
2024
2267
|
experimentText = renderExperiment(scope, win.experiment.decision, win.experiment.context, win.experiment, win);
|
|
@@ -2042,15 +2285,16 @@ export function apply(ctx, config) {
|
|
|
2042
2285
|
/** 工具模式也要开额度窗口:只是不自动注入,工具调用同样受每请求预算约束。 */
|
|
2043
2286
|
const ids = fresh.map((message) => String(message?.id ?? '')).filter(Boolean).sort();
|
|
2044
2287
|
const win = openWindow(agent, { sessionId, turn, messageIds: ids });
|
|
2288
|
+
const budgetBefore = win ? {
|
|
2289
|
+
bytes: win.bytes, bodies: win.bodies, reads: win.reads,
|
|
2290
|
+
provided: new Map(win.provided), readBodies: new Map(win.readBodies),
|
|
2291
|
+
recallHeader: win.recallHeader, experimentBytes: win.experimentBytes,
|
|
2292
|
+
autoBudget: win.autoBudget ? JSON.parse(JSON.stringify(win.autoBudget)) : null,
|
|
2293
|
+
} : null;
|
|
2045
2294
|
/** 工具模式也要记下本轮真人原话:只用于核对绑定,不自动注入。 */
|
|
2046
2295
|
const query = view.newestUserQuery(fresh).slice(0, RECALL_QUERY_CHARS);
|
|
2047
2296
|
if (win && query) win.query = query;
|
|
2048
2297
|
|
|
2049
|
-
if (recallMode() !== 'adaptive') {
|
|
2050
|
-
clearPrepared(agent);
|
|
2051
|
-
return decision;
|
|
2052
|
-
}
|
|
2053
|
-
|
|
2054
2298
|
if (!query) {
|
|
2055
2299
|
clearPrepared(agent);
|
|
2056
2300
|
return decision;
|
|
@@ -2065,29 +2309,61 @@ export function apply(ctx, config) {
|
|
|
2065
2309
|
clearPrepared(agent);
|
|
2066
2310
|
return decision;
|
|
2067
2311
|
}
|
|
2312
|
+
const batchKey = anchor.requestBatchKey(ids);
|
|
2313
|
+
/**
|
|
2314
|
+
* 工具模式也必须先有本请求上下文:memory_search/read/links 靠同一个 contextId 判项目和任务,
|
|
2315
|
+
* 不能等到自动召回分支才建,否则工具模式永远拿不到可核对的任务。
|
|
2316
|
+
*/
|
|
2317
|
+
const runtime = runtimeFor(scope, { sessionId, query, batchKey, messageIds: ids, autoRoute: recallMode() === 'adaptive' });
|
|
2318
|
+
if (win && runtime) win.runtime = runtime.held;
|
|
2319
|
+
rememberRuntime(scope, sessionId, runtime && runtime.held);
|
|
2320
|
+
if (recallMode() !== 'adaptive') {
|
|
2321
|
+
clearPrepared(agent);
|
|
2322
|
+
return decision;
|
|
2323
|
+
}
|
|
2068
2324
|
const context = view.trustedContext(scope, {});
|
|
2069
2325
|
const fingerprint = fingerprintOf(scope);
|
|
2070
2326
|
const controlEpoch = controlStatus(scope).epoch || '';
|
|
2071
|
-
const batchKey = anchor.requestBatchKey(ids);
|
|
2072
2327
|
dropStaleCandidate(sessionId, batchKey);
|
|
2073
|
-
|
|
2074
|
-
const
|
|
2075
|
-
|
|
2328
|
+
/** 任务上下文独立于自进化开关:普通记忆按当前任务判适用,实验规则再叠一层可用性。 */
|
|
2329
|
+
const auto = emptyAutoBudget();
|
|
2330
|
+
if (win) win.autoBudget = auto;
|
|
2331
|
+
const experiment = runtime ? experimentFor(scope, runtime, win) : null;
|
|
2332
|
+
const recalledContext = runtime ? runtime.held.context : context;
|
|
2333
|
+
const remembered = runtime && runtime.enabled
|
|
2334
|
+
? rememberFeedback(scope, sessionId, fresh, runtime.held, turn)
|
|
2076
2335
|
: null;
|
|
2077
|
-
const snapshot = projectRecall(scope, query,
|
|
2078
|
-
|
|
2336
|
+
const snapshot = projectRecall(scope, query, recalledContext, win, runtime ? runtime.held : null);
|
|
2337
|
+
/** 没发布出去的投影要退还额度:不能因为一次超时把本请求的召回额度耗掉。 */
|
|
2338
|
+
const rollbackBudget = () => {
|
|
2339
|
+
if (!win || !budgetBefore) return;
|
|
2340
|
+
win.bytes = budgetBefore.bytes;
|
|
2341
|
+
win.bodies = budgetBefore.bodies;
|
|
2342
|
+
win.reads = budgetBefore.reads;
|
|
2343
|
+
win.provided = budgetBefore.provided;
|
|
2344
|
+
win.readBodies = budgetBefore.readBodies;
|
|
2345
|
+
win.recallHeader = budgetBefore.recallHeader;
|
|
2346
|
+
win.experimentBytes = budgetBefore.experimentBytes;
|
|
2347
|
+
win.autoBudget = budgetBefore.autoBudget || emptyAutoBudget();
|
|
2348
|
+
win.experiment = null;
|
|
2349
|
+
win.contexts.clear();
|
|
2350
|
+
};
|
|
2351
|
+
if (generation !== generations.get(agent)) { rollbackBudget(); return decision; }
|
|
2079
2352
|
/** 投影期间开关被改过:丢弃这批内容,不把关闭前的记忆交给当前请求。 */
|
|
2080
2353
|
if (!memoryEnabled(scope) || (controlStatus(scope).epoch || '') !== controlEpoch) {
|
|
2354
|
+
rollbackBudget();
|
|
2081
2355
|
clearPrepared(agent);
|
|
2082
2356
|
return decision;
|
|
2083
2357
|
}
|
|
2084
2358
|
/** 超时或被取消:这一步不留半旧卡片,下一真人请求重新投影。 */
|
|
2085
2359
|
if (signal?.aborted || Date.now() - startedAt > RECALL_DEADLINE_MS) {
|
|
2360
|
+
rollbackBudget();
|
|
2086
2361
|
clearPrepared(agent);
|
|
2087
2362
|
return decision;
|
|
2088
2363
|
}
|
|
2089
2364
|
/** 实验上下文即使本轮零注入也要留着:工具绑定后同一请求还能补上规则。 */
|
|
2090
2365
|
if (!snapshot && !experiment) {
|
|
2366
|
+
rollbackBudget();
|
|
2091
2367
|
clearPrepared(agent);
|
|
2092
2368
|
return decision;
|
|
2093
2369
|
}
|
|
@@ -2196,6 +2472,13 @@ export function apply(ctx, config) {
|
|
|
2196
2472
|
const route = routeForSession(session);
|
|
2197
2473
|
if (!route) return;
|
|
2198
2474
|
const candidate = pending.candidate;
|
|
2475
|
+
/** 候选进入整理前再走一次统一准入:项目/作品身份、作用域与反馈类型都对上才花配额。 */
|
|
2476
|
+
const admission = learning.admissionOf(candidate, {
|
|
2477
|
+
projectId: scope.key,
|
|
2478
|
+
sessionId: candidate.sessionId,
|
|
2479
|
+
anchor: pending.held?.anchor || null,
|
|
2480
|
+
}, 'collect');
|
|
2481
|
+
if (!admission || admission.ok !== true) return;
|
|
2199
2482
|
const operationId = `learn_${crypto.createHash('sha256')
|
|
2200
2483
|
.update(`${scope.key}|${candidate.episodeId}|${candidate.messageIds.join(',')}`)
|
|
2201
2484
|
.digest('hex').slice(0, 16)}`;
|
|
@@ -2254,6 +2537,12 @@ export function apply(ctx, config) {
|
|
|
2254
2537
|
}
|
|
2255
2538
|
const parsed = learning.parseOutput(text, { finish, refs: input.refs, rules: input.rules, ranges });
|
|
2256
2539
|
if (!parsed.ok) throw new Error(parsed.reason);
|
|
2540
|
+
/** 用户已经把要求说成长期规则,模型仍返回空时至少留一条规则,不让这条反馈无声消失。 */
|
|
2541
|
+
const changes = parsed.changes.length
|
|
2542
|
+
? parsed.changes
|
|
2543
|
+
: (candidate.cue?.explicitLongTerm && candidate.cue?.type === 'spec_change'
|
|
2544
|
+
? [learning.pendingChangeFrom(candidate)].filter(Boolean)
|
|
2545
|
+
: []);
|
|
2257
2546
|
const commit = commitLearning(scope, {
|
|
2258
2547
|
operationId,
|
|
2259
2548
|
epoch: begin.epoch,
|
|
@@ -2263,8 +2552,9 @@ export function apply(ctx, config) {
|
|
|
2263
2552
|
sessionId: candidate.sessionId,
|
|
2264
2553
|
messageIds: candidate.messageIds,
|
|
2265
2554
|
feedbackText: candidate.text,
|
|
2555
|
+
sourceExcerpt: candidate.sourceExcerpt || learning.sourceExcerptOf(candidate.text),
|
|
2266
2556
|
dimension: candidate.cue?.dimension || '',
|
|
2267
|
-
changes
|
|
2557
|
+
changes,
|
|
2268
2558
|
preparedTargets,
|
|
2269
2559
|
usage: learning.usageOf(chunks),
|
|
2270
2560
|
explicitLongTerm: Boolean(candidate.cue.explicitLongTerm),
|