@ppagent/memory 0.3.1 → 0.4.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/dist/cli.js +1201 -0
- package/dist/index.d.ts +153 -12
- package/dist/index.js +1146 -358
- package/dist/{provider.resolver-ZLQ766IO.js → provider.resolver-2KS2YYNV.js} +32 -1
- package/llms.txt +874 -820
- package/package.json +5 -2
package/dist/index.js
CHANGED
|
@@ -16,6 +16,15 @@ function resolveConfig(config) {
|
|
|
16
16
|
console.warn("[MemoryConfig] embeddingApiKey \u672A\u914D\u7F6E\uFF0C\u81EA\u52A8\u56DE\u9000\u4F7F\u7528 llmApiKey\u3002");
|
|
17
17
|
embeddingApiKey = config.llmApiKey;
|
|
18
18
|
}
|
|
19
|
+
const compressedContextTokenLimit = positiveInt(
|
|
20
|
+
config.compressedContextTokenLimit ?? config.historyWindowTokenLimit ?? 16 * 1024,
|
|
21
|
+
"compressedContextTokenLimit"
|
|
22
|
+
);
|
|
23
|
+
if (compressedContextTokenLimit > 32 * 1024) {
|
|
24
|
+
console.warn(
|
|
25
|
+
`[MemoryConfig] compressedContextTokenLimit=${compressedContextTokenLimit} \u8D85\u8FC7 32K\uFF0C\u4F1A\u5360\u7528\u8F83\u591A\u6A21\u578B\u4E0A\u4E0B\u6587\u5E76\u589E\u52A0\u5E38\u9A7B\u5185\u5B58\u3002`
|
|
26
|
+
);
|
|
27
|
+
}
|
|
19
28
|
return {
|
|
20
29
|
...config,
|
|
21
30
|
embeddingBaseUrl,
|
|
@@ -23,11 +32,38 @@ function resolveConfig(config) {
|
|
|
23
32
|
provider: config.provider ?? "auto",
|
|
24
33
|
sqlitePath: config.sqlitePath ?? path.join(path.dirname(config.lancedbPath), "memory.sqlite3"),
|
|
25
34
|
sessionTokenLimit: config.sessionTokenLimit ?? 16386,
|
|
26
|
-
historyWindowTokenLimit: config.historyWindowTokenLimit ??
|
|
35
|
+
historyWindowTokenLimit: config.historyWindowTokenLimit ?? compressedContextTokenLimit,
|
|
27
36
|
topicRatio: config.topicRatio ?? [1, 5, 20],
|
|
28
37
|
detailMaxTokens: config.detailMaxTokens ?? 2048,
|
|
29
38
|
summaryMaxTokens: config.summaryMaxTokens ?? 512,
|
|
30
39
|
conciseMaxTokens: config.conciseMaxTokens ?? 128,
|
|
40
|
+
compressedContextTokenLimit,
|
|
41
|
+
compressedContextRatio: ratio(
|
|
42
|
+
config.compressedContextRatio ?? 0.1,
|
|
43
|
+
"compressedContextRatio"
|
|
44
|
+
),
|
|
45
|
+
topicCompactionSyncRatio: atLeastOne(
|
|
46
|
+
config.topicCompactionSyncRatio ?? 1.25,
|
|
47
|
+
"topicCompactionSyncRatio"
|
|
48
|
+
),
|
|
49
|
+
contextUsageRatio: ratio(config.contextUsageRatio ?? 0.75, "contextUsageRatio"),
|
|
50
|
+
precompressionRatio: ratio(config.precompressionRatio ?? 0.75, "precompressionRatio"),
|
|
51
|
+
compressionBatchRatio: ratio(config.compressionBatchRatio ?? 0.5, "compressionBatchRatio"),
|
|
52
|
+
compressionBatchTokenLimit: Math.max(0, Math.floor(config.compressionBatchTokenLimit ?? 0)),
|
|
53
|
+
topicSummaryMaxTokens: positiveInt(
|
|
54
|
+
config.topicSummaryMaxTokens ?? config.detailMaxTokens ?? 2048,
|
|
55
|
+
"topicSummaryMaxTokens"
|
|
56
|
+
),
|
|
57
|
+
defaultModelContextTokens: positiveInt(
|
|
58
|
+
config.defaultModelContextTokens ?? 256 * 1024,
|
|
59
|
+
"defaultModelContextTokens"
|
|
60
|
+
),
|
|
61
|
+
maxHistoryAgeMs: Math.max(0, Math.floor(config.maxHistoryAgeMs ?? 0)),
|
|
62
|
+
sessionIdleTtlMs: Math.floor(config.sessionIdleTtlMs ?? 30 * 6e4),
|
|
63
|
+
sessionSweepIntervalMs: positiveInt(
|
|
64
|
+
config.sessionSweepIntervalMs ?? 6e4,
|
|
65
|
+
"sessionSweepIntervalMs"
|
|
66
|
+
),
|
|
31
67
|
httpTimeoutMs: config.httpTimeoutMs ?? 6e4,
|
|
32
68
|
httpMaxRetries: config.httpMaxRetries ?? 2,
|
|
33
69
|
embeddingBatchSize: config.embeddingBatchSize ?? 20,
|
|
@@ -55,6 +91,53 @@ function resolveConfig(config) {
|
|
|
55
91
|
restoreConcurrency: config.restoreConcurrency ?? 8
|
|
56
92
|
};
|
|
57
93
|
}
|
|
94
|
+
function ratio(value, name) {
|
|
95
|
+
if (!Number.isFinite(value) || value <= 0 || value > 1) {
|
|
96
|
+
throw new Error(`[MemoryConfig] ${name} \u5FC5\u987B\u5728 (0, 1] \u8303\u56F4\u5185`);
|
|
97
|
+
}
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
function positiveInt(value, name) {
|
|
101
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
102
|
+
throw new Error(`[MemoryConfig] ${name} \u5FC5\u987B\u662F\u6B63\u6570`);
|
|
103
|
+
}
|
|
104
|
+
return Math.floor(value);
|
|
105
|
+
}
|
|
106
|
+
function atLeastOne(value, name) {
|
|
107
|
+
if (!Number.isFinite(value) || value < 1) {
|
|
108
|
+
throw new Error(`[MemoryConfig] ${name} \u5FC5\u987B\u662F\u5927\u4E8E\u7B49\u4E8E 1 \u7684\u6709\u9650\u6570`);
|
|
109
|
+
}
|
|
110
|
+
return value;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// src/context-budget.ts
|
|
114
|
+
function calculateContextBudget(config, modelContextTokens) {
|
|
115
|
+
if (!Number.isFinite(modelContextTokens) || modelContextTokens <= 0) {
|
|
116
|
+
throw new Error("modelContextTokens \u5FC5\u987B\u662F\u6B63\u6570");
|
|
117
|
+
}
|
|
118
|
+
const normalizedModelTokens = Math.floor(modelContextTokens);
|
|
119
|
+
const usableContextTokens = Math.floor(normalizedModelTokens * config.contextUsageRatio);
|
|
120
|
+
if (usableContextTokens < 2) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`modelContextTokens=${normalizedModelTokens} \u5728 contextUsageRatio=${config.contextUsageRatio} \u4E0B\u4E0D\u8DB3\u4EE5\u540C\u65F6\u4FDD\u7559 Topic \u4E0E\u539F\u59CB\u6D88\u606F\u9884\u7B97`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
const ratioLimit = Math.max(
|
|
126
|
+
1,
|
|
127
|
+
Math.floor(usableContextTokens * config.compressedContextRatio)
|
|
128
|
+
);
|
|
129
|
+
const compressedTokenLimit = Math.min(
|
|
130
|
+
config.compressedContextTokenLimit,
|
|
131
|
+
ratioLimit,
|
|
132
|
+
usableContextTokens - 1
|
|
133
|
+
);
|
|
134
|
+
return {
|
|
135
|
+
modelContextTokens: normalizedModelTokens,
|
|
136
|
+
usableContextTokens,
|
|
137
|
+
compressedTokenLimit,
|
|
138
|
+
rawTokenLimit: usableContextTokens - compressedTokenLimit
|
|
139
|
+
};
|
|
140
|
+
}
|
|
58
141
|
|
|
59
142
|
// src/constants.ts
|
|
60
143
|
var DEFAULT_NODE_TYPES = [
|
|
@@ -743,6 +826,7 @@ function tableDefs(dim) {
|
|
|
743
826
|
{ name: "content", type: "text" },
|
|
744
827
|
{ name: "parts", type: "text", nullable: true },
|
|
745
828
|
// 兼容存量数据
|
|
829
|
+
{ name: "payload", type: "text", nullable: true },
|
|
746
830
|
{ name: "vector", type: "vector" },
|
|
747
831
|
{ name: "usage", type: "int" },
|
|
748
832
|
{ name: "metadata", type: "text" },
|
|
@@ -765,9 +849,12 @@ function tableDefs(dim) {
|
|
|
765
849
|
{ name: "chat_id", type: "text" },
|
|
766
850
|
{ name: "title", type: "text", nullable: true },
|
|
767
851
|
// 兼容存量数据
|
|
768
|
-
{ name: "detail", type: "text" },
|
|
852
|
+
{ name: "detail", type: "text", nullable: true },
|
|
769
853
|
{ name: "summary", type: "text" },
|
|
770
|
-
{ name: "concise", type: "text" },
|
|
854
|
+
{ name: "concise", type: "text", nullable: true },
|
|
855
|
+
{ name: "tokens", type: "int", nullable: true },
|
|
856
|
+
{ name: "start_message_id", type: "text", nullable: true },
|
|
857
|
+
{ name: "end_message_id", type: "text", nullable: true },
|
|
771
858
|
{ name: "vector", type: "vector" },
|
|
772
859
|
{ name: "start_time", type: "long" },
|
|
773
860
|
{ name: "end_time", type: "long" },
|
|
@@ -777,7 +864,7 @@ function tableDefs(dim) {
|
|
|
777
864
|
],
|
|
778
865
|
indexes: [
|
|
779
866
|
{ column: "chat_id", kind: "scalar" },
|
|
780
|
-
{ column: "
|
|
867
|
+
{ column: "summary", kind: "fts" }
|
|
781
868
|
]
|
|
782
869
|
},
|
|
783
870
|
{
|
|
@@ -788,8 +875,10 @@ function tableDefs(dim) {
|
|
|
788
875
|
{ name: "chat_id", type: "text" },
|
|
789
876
|
{ name: "session_id", type: "text" },
|
|
790
877
|
{ name: "user_id", type: "text" },
|
|
878
|
+
{ name: "fact_key", type: "text", nullable: true },
|
|
791
879
|
{ name: "content", type: "text" },
|
|
792
|
-
{ name: "created_at", type: "long" }
|
|
880
|
+
{ name: "created_at", type: "long" },
|
|
881
|
+
{ name: "updated_at", type: "long", nullable: true }
|
|
793
882
|
],
|
|
794
883
|
indexes: []
|
|
795
884
|
},
|
|
@@ -868,6 +957,7 @@ function messageToRow(m) {
|
|
|
868
957
|
type: m.type,
|
|
869
958
|
content: m.content,
|
|
870
959
|
parts: m.parts ?? "[]",
|
|
960
|
+
payload: m.payload ?? null,
|
|
871
961
|
vector: m.vector,
|
|
872
962
|
usage: m.usage,
|
|
873
963
|
metadata: m.metadata,
|
|
@@ -885,8 +975,9 @@ function rowToMessage(r) {
|
|
|
885
975
|
content: r.content,
|
|
886
976
|
parts: r.parts ?? "[]",
|
|
887
977
|
// 旧数据无此列时安全降级
|
|
978
|
+
payload: r.payload ?? void 0,
|
|
888
979
|
vector: toVector(r.vector),
|
|
889
|
-
usage:
|
|
980
|
+
usage: positiveNumber(r.usage),
|
|
890
981
|
metadata: r.metadata,
|
|
891
982
|
createdAt: Number(r.created_at)
|
|
892
983
|
};
|
|
@@ -898,9 +989,13 @@ function topicToRow(t) {
|
|
|
898
989
|
user_id: t.userId,
|
|
899
990
|
chat_id: t.chatId,
|
|
900
991
|
title: t.title ?? "",
|
|
901
|
-
detail
|
|
992
|
+
// 旧库的 detail/concise 可能仍是 NOT NULL;兼容写入但业务只读取 summary。
|
|
993
|
+
detail: t.detail ?? t.summary,
|
|
902
994
|
summary: t.summary,
|
|
903
|
-
concise: t.concise,
|
|
995
|
+
concise: t.concise ?? t.summary,
|
|
996
|
+
tokens: t.tokens,
|
|
997
|
+
start_message_id: t.startMessageId ?? null,
|
|
998
|
+
end_message_id: t.endMessageId ?? null,
|
|
904
999
|
vector: t.vector,
|
|
905
1000
|
start_time: t.startTime,
|
|
906
1001
|
end_time: t.endTime,
|
|
@@ -916,9 +1011,12 @@ function rowToTopic(r) {
|
|
|
916
1011
|
userId: r.user_id,
|
|
917
1012
|
chatId: r.chat_id,
|
|
918
1013
|
title: r.title ?? "",
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
1014
|
+
summary: r.summary || r.detail || r.concise || "",
|
|
1015
|
+
tokens: positiveNumber(r.tokens),
|
|
1016
|
+
startMessageId: r.start_message_id ?? void 0,
|
|
1017
|
+
endMessageId: r.end_message_id ?? void 0,
|
|
1018
|
+
detail: r.detail ?? void 0,
|
|
1019
|
+
concise: r.concise ?? void 0,
|
|
922
1020
|
vector: toVector(r.vector),
|
|
923
1021
|
startTime: Number(r.start_time),
|
|
924
1022
|
endTime: Number(r.end_time),
|
|
@@ -934,8 +1032,10 @@ function factToRow(f) {
|
|
|
934
1032
|
chat_id: f.chatId,
|
|
935
1033
|
session_id: f.sessionId,
|
|
936
1034
|
user_id: f.userId,
|
|
1035
|
+
fact_key: f.key ?? null,
|
|
937
1036
|
content: f.content,
|
|
938
|
-
created_at: f.createdAt
|
|
1037
|
+
created_at: f.createdAt,
|
|
1038
|
+
updated_at: f.updatedAt ?? f.createdAt
|
|
939
1039
|
};
|
|
940
1040
|
}
|
|
941
1041
|
function rowToFact(r) {
|
|
@@ -945,8 +1045,10 @@ function rowToFact(r) {
|
|
|
945
1045
|
chatId: r.chat_id,
|
|
946
1046
|
sessionId: r.session_id,
|
|
947
1047
|
userId: r.user_id,
|
|
1048
|
+
key: r.fact_key ?? void 0,
|
|
948
1049
|
content: r.content,
|
|
949
|
-
createdAt: Number(r.created_at)
|
|
1050
|
+
createdAt: Number(r.created_at),
|
|
1051
|
+
updatedAt: Number(r.updated_at ?? r.created_at)
|
|
950
1052
|
};
|
|
951
1053
|
}
|
|
952
1054
|
function sessionToRow(s) {
|
|
@@ -1048,6 +1150,15 @@ function safeParseObject(s) {
|
|
|
1048
1150
|
return {};
|
|
1049
1151
|
}
|
|
1050
1152
|
}
|
|
1153
|
+
function positiveNumber(value) {
|
|
1154
|
+
const parsed = Number(value);
|
|
1155
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
|
1156
|
+
}
|
|
1157
|
+
function migrationValueEquals(current, next) {
|
|
1158
|
+
if (current == null && next == null) return true;
|
|
1159
|
+
if (typeof next === "number") return Number(current) === next;
|
|
1160
|
+
return String(current) === String(next);
|
|
1161
|
+
}
|
|
1051
1162
|
function rrfFuse(lists, keyOf) {
|
|
1052
1163
|
const acc = /* @__PURE__ */ new Map();
|
|
1053
1164
|
for (const list of lists) {
|
|
@@ -1078,7 +1189,7 @@ var MemoryStore = class {
|
|
|
1078
1189
|
if (this.providerOverride) {
|
|
1079
1190
|
this.provider = this.providerOverride;
|
|
1080
1191
|
} else {
|
|
1081
|
-
const { createProvider } = await import("./provider.resolver-
|
|
1192
|
+
const { createProvider } = await import("./provider.resolver-2KS2YYNV.js");
|
|
1082
1193
|
this.provider = await createProvider(this.config);
|
|
1083
1194
|
console.info(`[memory] \u5411\u91CF\u5B58\u50A8\u540E\u7AEF\uFF1A${this.provider.kind}`);
|
|
1084
1195
|
}
|
|
@@ -1099,6 +1210,98 @@ var MemoryStore = class {
|
|
|
1099
1210
|
if (messages.length === 0) return;
|
|
1100
1211
|
await this.provider.add(MESSAGES_TABLE, messages.map(messageToRow));
|
|
1101
1212
|
}
|
|
1213
|
+
/** 0.3.x → 0.4.x 数据回填;幂等,不调用 LLM、不删除原始消息。 */
|
|
1214
|
+
async migrateLegacyData() {
|
|
1215
|
+
const report = {
|
|
1216
|
+
provider: this.providerKind,
|
|
1217
|
+
topicsScanned: 0,
|
|
1218
|
+
topicsUpdated: 0,
|
|
1219
|
+
messagesScanned: 0,
|
|
1220
|
+
messagesUpdated: 0,
|
|
1221
|
+
factsScanned: 0,
|
|
1222
|
+
factsUpdated: 0
|
|
1223
|
+
};
|
|
1224
|
+
const topicRows = await this.provider.query(TOPICS_TABLE);
|
|
1225
|
+
report.topicsScanned = topicRows.length;
|
|
1226
|
+
const messagesBySession = /* @__PURE__ */ new Map();
|
|
1227
|
+
for (const row of topicRows) {
|
|
1228
|
+
const sessionId = String(row.session_id);
|
|
1229
|
+
let messages = messagesBySession.get(sessionId);
|
|
1230
|
+
if (!messages) {
|
|
1231
|
+
messages = await this.provider.query(MESSAGES_TABLE, {
|
|
1232
|
+
filter: [eq("session_id", sessionId)],
|
|
1233
|
+
orderBy: [
|
|
1234
|
+
{ column: "created_at", ascending: true },
|
|
1235
|
+
{ column: "message_id", ascending: true }
|
|
1236
|
+
]
|
|
1237
|
+
});
|
|
1238
|
+
messagesBySession.set(sessionId, messages);
|
|
1239
|
+
}
|
|
1240
|
+
const summary = String(row.summary || row.detail || row.concise || "");
|
|
1241
|
+
const startTime = Number(row.start_time);
|
|
1242
|
+
const endTime = Number(row.end_time);
|
|
1243
|
+
const covered = messages.filter(
|
|
1244
|
+
(message) => Number(message.created_at) >= startTime && Number(message.created_at) <= endTime
|
|
1245
|
+
);
|
|
1246
|
+
const values = {
|
|
1247
|
+
summary,
|
|
1248
|
+
tokens: positiveNumber(row.tokens) || Math.max(1, countTokens(summary)),
|
|
1249
|
+
start_message_id: row.start_message_id ?? covered.at(0)?.message_id ?? null,
|
|
1250
|
+
end_message_id: row.end_message_id ?? covered.at(-1)?.message_id ?? null,
|
|
1251
|
+
updated_at: Number(row.updated_at || Date.now())
|
|
1252
|
+
};
|
|
1253
|
+
const changed = Object.entries(values).some(
|
|
1254
|
+
([key, value]) => !migrationValueEquals(row[key], value)
|
|
1255
|
+
);
|
|
1256
|
+
if (changed) {
|
|
1257
|
+
await this.provider.update(TOPICS_TABLE, values, [eq("summary_id", String(row.summary_id))]);
|
|
1258
|
+
report.topicsUpdated++;
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
const messageRows = await this.provider.query(MESSAGES_TABLE);
|
|
1262
|
+
report.messagesScanned = messageRows.length;
|
|
1263
|
+
for (const row of messageRows) {
|
|
1264
|
+
const usage = countTokens(
|
|
1265
|
+
[row.content, row.parts ?? "[]", row.metadata ?? "{}", row.payload ?? ""].map(String).join("\n")
|
|
1266
|
+
);
|
|
1267
|
+
if (Number(row.usage) === usage) continue;
|
|
1268
|
+
await this.provider.update(MESSAGES_TABLE, { usage }, [
|
|
1269
|
+
eq("message_id", String(row.message_id))
|
|
1270
|
+
]);
|
|
1271
|
+
report.messagesUpdated++;
|
|
1272
|
+
}
|
|
1273
|
+
const factRows = await this.provider.query(FACTS_TABLE);
|
|
1274
|
+
report.factsScanned = factRows.length;
|
|
1275
|
+
for (const row of factRows) {
|
|
1276
|
+
const updatedAt = Number(row.updated_at || row.created_at);
|
|
1277
|
+
if (Number(row.updated_at) === updatedAt) continue;
|
|
1278
|
+
await this.provider.update(FACTS_TABLE, { updated_at: updatedAt }, [
|
|
1279
|
+
eq("fact_id", String(row.fact_id))
|
|
1280
|
+
]);
|
|
1281
|
+
report.factsUpdated++;
|
|
1282
|
+
}
|
|
1283
|
+
return report;
|
|
1284
|
+
}
|
|
1285
|
+
/** message_id 稳定时更新原行,否则新增;用于流式 assistant 消息最终态覆盖。 */
|
|
1286
|
+
async upsertMessages(messages) {
|
|
1287
|
+
if (messages.length === 0) return;
|
|
1288
|
+
const ids = messages.map((message) => message.messageId);
|
|
1289
|
+
const existing = await this.provider.query(MESSAGES_TABLE, {
|
|
1290
|
+
filter: [{ op: "in", field: "message_id", values: ids }],
|
|
1291
|
+
select: ["message_id"]
|
|
1292
|
+
});
|
|
1293
|
+
const existingIds = new Set(existing.map((row) => String(row.message_id)));
|
|
1294
|
+
await Promise.all(
|
|
1295
|
+
messages.filter((message) => existingIds.has(message.messageId)).map(
|
|
1296
|
+
(message) => this.provider.update(
|
|
1297
|
+
MESSAGES_TABLE,
|
|
1298
|
+
messageToRow(message),
|
|
1299
|
+
[eq("message_id", message.messageId)]
|
|
1300
|
+
)
|
|
1301
|
+
)
|
|
1302
|
+
);
|
|
1303
|
+
await this.addMessages(messages.filter((message) => !existingIds.has(message.messageId)));
|
|
1304
|
+
}
|
|
1102
1305
|
async getMessagesSince(sessionId, since, limit) {
|
|
1103
1306
|
try {
|
|
1104
1307
|
const rows = await this.provider.query(MESSAGES_TABLE, {
|
|
@@ -1110,6 +1313,12 @@ var MemoryStore = class {
|
|
|
1110
1313
|
return [];
|
|
1111
1314
|
}
|
|
1112
1315
|
}
|
|
1316
|
+
async getMessagesAfterBoundary(sessionId, endTime, endMessageId) {
|
|
1317
|
+
const messages = await this.getAllMessagesBySession(sessionId);
|
|
1318
|
+
return messages.filter(
|
|
1319
|
+
(message) => message.createdAt > endTime || message.createdAt === endTime && endMessageId != null && message.messageId.localeCompare(endMessageId) > 0
|
|
1320
|
+
);
|
|
1321
|
+
}
|
|
1113
1322
|
async getLatestMessages(sessionId, limit) {
|
|
1114
1323
|
if (limit <= 0) return [];
|
|
1115
1324
|
try {
|
|
@@ -1157,7 +1366,7 @@ var MemoryStore = class {
|
|
|
1157
1366
|
}
|
|
1158
1367
|
async hybridSearchTopics(query, vector, filter, limit = 10) {
|
|
1159
1368
|
try {
|
|
1160
|
-
const fused = (await this.hybridSearch(TOPICS_TABLE, ["
|
|
1369
|
+
const fused = (await this.hybridSearch(TOPICS_TABLE, ["summary"], query, vector, filter, limit)).slice(0, limit);
|
|
1161
1370
|
if (fused.length > 0) {
|
|
1162
1371
|
const total = fused.length;
|
|
1163
1372
|
return fused.map((r, idx) => ({
|
|
@@ -1165,25 +1374,10 @@ var MemoryStore = class {
|
|
|
1165
1374
|
score: total - idx + Math.log1p(Number(r.recall_count))
|
|
1166
1375
|
})).sort((a, b) => b.score - a.score).map((x) => rowToTopic(x.row));
|
|
1167
1376
|
}
|
|
1377
|
+
return [];
|
|
1168
1378
|
} catch {
|
|
1379
|
+
return [];
|
|
1169
1380
|
}
|
|
1170
|
-
const msgs = await this.hybridSearchMessages(query, vector, filter, limit);
|
|
1171
|
-
return msgs.map((r) => ({
|
|
1172
|
-
summaryId: r.messageId,
|
|
1173
|
-
sessionId: r.sessionId,
|
|
1174
|
-
userId: r.userId,
|
|
1175
|
-
chatId: r.chatId,
|
|
1176
|
-
title: "",
|
|
1177
|
-
detail: r.content,
|
|
1178
|
-
summary: r.content,
|
|
1179
|
-
concise: r.content,
|
|
1180
|
-
startTime: r.createdAt,
|
|
1181
|
-
endTime: r.createdAt,
|
|
1182
|
-
createdAt: r.createdAt,
|
|
1183
|
-
updatedAt: r.createdAt,
|
|
1184
|
-
recallCount: 0,
|
|
1185
|
-
vector: r.vector
|
|
1186
|
-
}));
|
|
1187
1381
|
}
|
|
1188
1382
|
/**
|
|
1189
1383
|
* 通用混合检索:FTS(BM25)与向量两路各取 limit*HYBRID_OVERFETCH 候选,RRF 融合。
|
|
@@ -1221,6 +1415,13 @@ var MemoryStore = class {
|
|
|
1221
1415
|
async addTopic(topic) {
|
|
1222
1416
|
await this.provider.add(TOPICS_TABLE, [topicToRow(topic)]);
|
|
1223
1417
|
}
|
|
1418
|
+
async updateTopic(topic) {
|
|
1419
|
+
await this.provider.update(
|
|
1420
|
+
TOPICS_TABLE,
|
|
1421
|
+
topicToRow(topic),
|
|
1422
|
+
[eq("summary_id", topic.summaryId)]
|
|
1423
|
+
);
|
|
1424
|
+
}
|
|
1224
1425
|
async updateTopicRecallCount(summaryId, count) {
|
|
1225
1426
|
await this.provider.update(
|
|
1226
1427
|
TOPICS_TABLE,
|
|
@@ -1228,6 +1429,23 @@ var MemoryStore = class {
|
|
|
1228
1429
|
[eq("summary_id", summaryId)]
|
|
1229
1430
|
);
|
|
1230
1431
|
}
|
|
1432
|
+
async incrementTopicRecallCounts(topics) {
|
|
1433
|
+
await Promise.all(
|
|
1434
|
+
topics.map((topic) => this.updateTopicRecallCount(topic.summaryId, topic.recallCount + 1))
|
|
1435
|
+
);
|
|
1436
|
+
}
|
|
1437
|
+
async getTopicsBySession(sessionId, since = 0) {
|
|
1438
|
+
try {
|
|
1439
|
+
const filter = [eq("session_id", sessionId)];
|
|
1440
|
+
if (since > 0) filter.push({ op: "gte", field: "end_time", value: since });
|
|
1441
|
+
const rows = await this.provider.query(TOPICS_TABLE, { filter });
|
|
1442
|
+
return rows.sort(
|
|
1443
|
+
(a, b) => Number(a.end_time) - Number(b.end_time) || cmpStr(a.summary_id, b.summary_id)
|
|
1444
|
+
).map(rowToTopic);
|
|
1445
|
+
} catch {
|
|
1446
|
+
return [];
|
|
1447
|
+
}
|
|
1448
|
+
}
|
|
1231
1449
|
async getRecentTopics(chatId, userId, n1, n2, n3) {
|
|
1232
1450
|
const filter = [eq("chat_id", chatId), eq("user_id", userId)];
|
|
1233
1451
|
const recallBoostMs = this.config.recallBoostMs;
|
|
@@ -1268,10 +1486,19 @@ var MemoryStore = class {
|
|
|
1268
1486
|
async deleteTopicsBySession(sessionId) {
|
|
1269
1487
|
await this.provider.deleteWhere(TOPICS_TABLE, [eq("session_id", sessionId)]);
|
|
1270
1488
|
}
|
|
1489
|
+
async deleteTopicsByIds(summaryIds) {
|
|
1490
|
+
if (summaryIds.length === 0) return;
|
|
1491
|
+
await this.provider.deleteWhere(TOPICS_TABLE, [
|
|
1492
|
+
{ op: "in", field: "summary_id", values: summaryIds }
|
|
1493
|
+
]);
|
|
1494
|
+
}
|
|
1271
1495
|
// ── facts ──────────────────────────────────────────────────────────────────
|
|
1272
1496
|
async saveFact(fact) {
|
|
1273
1497
|
await this.provider.add(FACTS_TABLE, [factToRow(fact)]);
|
|
1274
1498
|
}
|
|
1499
|
+
async updateFact(fact) {
|
|
1500
|
+
await this.provider.update(FACTS_TABLE, factToRow(fact), [eq("fact_id", fact.factId)]);
|
|
1501
|
+
}
|
|
1275
1502
|
async getAllFacts() {
|
|
1276
1503
|
try {
|
|
1277
1504
|
const rows = await this.provider.query(FACTS_TABLE);
|
|
@@ -1511,40 +1738,6 @@ var MemoryStore = class {
|
|
|
1511
1738
|
}
|
|
1512
1739
|
};
|
|
1513
1740
|
|
|
1514
|
-
// src/manager/semaphore.ts
|
|
1515
|
-
var Semaphore = class {
|
|
1516
|
-
count;
|
|
1517
|
-
queue = [];
|
|
1518
|
-
constructor(max) {
|
|
1519
|
-
this.count = max;
|
|
1520
|
-
}
|
|
1521
|
-
async run(fn) {
|
|
1522
|
-
await this.acquire();
|
|
1523
|
-
try {
|
|
1524
|
-
return await fn();
|
|
1525
|
-
} finally {
|
|
1526
|
-
this.release();
|
|
1527
|
-
}
|
|
1528
|
-
}
|
|
1529
|
-
acquire() {
|
|
1530
|
-
if (this.count > 0) {
|
|
1531
|
-
this.count--;
|
|
1532
|
-
return Promise.resolve();
|
|
1533
|
-
}
|
|
1534
|
-
return new Promise((resolve) => {
|
|
1535
|
-
this.queue.push(resolve);
|
|
1536
|
-
});
|
|
1537
|
-
}
|
|
1538
|
-
release() {
|
|
1539
|
-
const next = this.queue.shift();
|
|
1540
|
-
if (next) {
|
|
1541
|
-
next();
|
|
1542
|
-
} else {
|
|
1543
|
-
this.count++;
|
|
1544
|
-
}
|
|
1545
|
-
}
|
|
1546
|
-
};
|
|
1547
|
-
|
|
1548
1741
|
// src/llm/llm.service.ts
|
|
1549
1742
|
var ENTITY_EXTRACTOR_TOOL = {
|
|
1550
1743
|
type: "function",
|
|
@@ -1617,7 +1810,10 @@ var LlmService = class {
|
|
|
1617
1810
|
* 仅在提示词明确要求 JSON 时启用;返回自然语言的调用(如 summarizeSearchResults)
|
|
1618
1811
|
* 必须传 false,否则部分端点会因 "messages 未含 json 字样" 而 400。
|
|
1619
1812
|
*/
|
|
1620
|
-
async chatCompletion(systemPrompt, userPrompt, jsonMode = true) {
|
|
1813
|
+
async chatCompletion(systemPrompt, userPrompt, jsonMode = true, maxTokens) {
|
|
1814
|
+
return (await this.chatCompletionWithUsage(systemPrompt, userPrompt, jsonMode, maxTokens)).content;
|
|
1815
|
+
}
|
|
1816
|
+
async chatCompletionWithUsage(systemPrompt, userPrompt, jsonMode = true, maxTokens) {
|
|
1621
1817
|
const url = `${this.config.llmBaseUrl.replace(/\/$/, "")}/chat/completions`;
|
|
1622
1818
|
const body = {
|
|
1623
1819
|
model: this.config.llmModel,
|
|
@@ -1628,6 +1824,7 @@ var LlmService = class {
|
|
|
1628
1824
|
]
|
|
1629
1825
|
};
|
|
1630
1826
|
if (jsonMode) body.response_format = { type: "json_object" };
|
|
1827
|
+
if (maxTokens != null) body.max_tokens = maxTokens;
|
|
1631
1828
|
const data = await postJsonWithRetry(
|
|
1632
1829
|
url,
|
|
1633
1830
|
{ Authorization: `Bearer ${this.config.llmApiKey}` },
|
|
@@ -1635,7 +1832,10 @@ var LlmService = class {
|
|
|
1635
1832
|
{ timeoutMs: this.config.httpTimeoutMs, maxRetries: this.config.httpMaxRetries },
|
|
1636
1833
|
"LLM API"
|
|
1637
1834
|
);
|
|
1638
|
-
return
|
|
1835
|
+
return {
|
|
1836
|
+
content: data.choices[0].message.content ?? "",
|
|
1837
|
+
completionTokens: data.usage?.completion_tokens
|
|
1838
|
+
};
|
|
1639
1839
|
}
|
|
1640
1840
|
// ── 带工具调用的请求 ──────────────────────────────────────────────────────────
|
|
1641
1841
|
async chatWithTools(systemPrompt, userPrompt) {
|
|
@@ -1658,27 +1858,55 @@ var LlmService = class {
|
|
|
1658
1858
|
);
|
|
1659
1859
|
return data.choices[0].message;
|
|
1660
1860
|
}
|
|
1661
|
-
// ──
|
|
1861
|
+
// ── 对话 Topic 摘要(JSON 输出)──────────────────────────────────────────────
|
|
1662
1862
|
async summarizeMessages(messages) {
|
|
1663
1863
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1664
|
-
const {
|
|
1864
|
+
const { topicSummaryMaxTokens } = this.config;
|
|
1665
1865
|
const systemPrompt = `\u4F60\u662F\u4E00\u4E2A\u5BF9\u8BDD\u8BB0\u5FC6\u7BA1\u7406\u52A9\u624B\u3002\u5F53\u524D\u7CFB\u7EDF\u65F6\u95F4\uFF1A${now}\u3002
|
|
1666
|
-
\u8BF7\
|
|
1866
|
+
\u8BF7\u628A\u7ED9\u5B9A\u5BF9\u8BDD\u538B\u7F29\u6210\u4E00\u4E2A\u53EF\u957F\u671F\u590D\u7528\u7684\u4E3B\u9898\u8BB0\u5FC6\u3002
|
|
1667
1867
|
|
|
1668
1868
|
\u8981\u6C42\uFF1A
|
|
1669
1869
|
- title\uFF1A\u4E3B\u9898\u6807\u9898\uFF0C\u4E00\u53E5\u8BDD\uFF08\u5EFA\u8BAE\u4E0D\u8D85\u8FC7 20 \u5B57\uFF09\uFF0C\u9AD8\u5EA6\u6982\u62EC\u8FD9\u6BB5\u5BF9\u8BDD\u7684\u4E3B\u9898\uFF0C\u7528\u4E8E\u5217\u8868\u5C55\u793A
|
|
1670
|
-
-
|
|
1671
|
-
-
|
|
1672
|
-
-
|
|
1870
|
+
- summary\uFF1A\u552F\u4E00\u7684\u6458\u8981\u6B63\u6587\uFF0C\u6700\u591A ${topicSummaryMaxTokens} tokens
|
|
1871
|
+
- \u6839\u636E\u91CD\u8981\u6027\u52A8\u6001\u51B3\u5B9A\u7BC7\u5E45\uFF1A\u7528\u6237\u7A33\u5B9A\u504F\u597D\u3001\u660E\u786E\u4E8B\u5B9E\u3001\u51B3\u5B9A\u4E0E\u7406\u7531\u3001\u957F\u671F\u7EA6\u675F\u3001\u672A\u89E3\u51B3\u963B\u585E\u3001\u53EF\u590D\u7528\u7ECF\u9A8C\u8981\u8BE6\u7EC6\u4FDD\u7559\uFF1B\u91CD\u590D\u8868\u8FF0\u3001\u5BD2\u6684\u548C\u4E00\u6B21\u6027\u8FC7\u7A0B\u53EF\u6781\u77ED\u6216\u7701\u7565
|
|
1872
|
+
- \u4FDD\u7559\u8C01\u5728\u4F55\u65F6\u505A\u4E86\u4EC0\u4E48\u3001\u5173\u952E\u6807\u8BC6\u7B26\u3001\u6570\u503C\u3001\u7ED3\u8BBA\u3001\u5931\u8D25\u539F\u56E0\u548C\u540E\u7EED\u52A8\u4F5C\uFF1B\u4E0D\u8981\u865A\u6784
|
|
1873
|
+
- metadata\u3001\u7ED3\u6784\u5316 parts \u548C payload \u4E2D\u5F71\u54CD\u8BED\u4E49\u7684\u4FE1\u606F\u540C\u6837\u5C5E\u4E8E\u8BB0\u5FC6\u5185\u5BB9
|
|
1673
1874
|
|
|
1674
1875
|
\u8F93\u51FA\u5FC5\u987B\u662F\u5408\u6CD5\u7684 JSON \u5BF9\u8C61\uFF0C\u4E0D\u8981\u5305\u542B\u4EFB\u4F55 markdown \u6807\u8BB0\uFF0C\u683C\u5F0F\u5982\u4E0B\uFF1A
|
|
1675
|
-
{"title": "...", "
|
|
1676
|
-
const formatted = messages.map((m) =>
|
|
1677
|
-
const
|
|
1876
|
+
{"title": "...", "summary": "..."}`;
|
|
1877
|
+
const formatted = messages.map((m) => formatStoredMessage(m)).join("\n");
|
|
1878
|
+
const response = await this.chatCompletionWithUsage(
|
|
1879
|
+
systemPrompt,
|
|
1880
|
+
`\u8BF7\u538B\u7F29\u4EE5\u4E0B\u5BF9\u8BDD\uFF1A
|
|
1678
1881
|
|
|
1679
|
-
${formatted}
|
|
1680
|
-
|
|
1681
|
-
|
|
1882
|
+
${formatted}`,
|
|
1883
|
+
true,
|
|
1884
|
+
topicSummaryMaxTokens + 128
|
|
1885
|
+
);
|
|
1886
|
+
const parsed = JSON.parse(response.content);
|
|
1887
|
+
return {
|
|
1888
|
+
title: parsed.title ?? "",
|
|
1889
|
+
summary: parsed.summary ?? "",
|
|
1890
|
+
tokens: response.completionTokens
|
|
1891
|
+
};
|
|
1892
|
+
}
|
|
1893
|
+
async summarizeTopics(topics, maxSummaryTokens = this.config.topicSummaryMaxTokens) {
|
|
1894
|
+
const input = topics.map(
|
|
1895
|
+
(topic) => `[${new Date(topic.startTime).toISOString()} - ${new Date(topic.endTime).toISOString()}] ${topic.title}
|
|
1896
|
+
${topic.summary}`
|
|
1897
|
+
).join("\n\n");
|
|
1898
|
+
const response = await this.chatCompletionWithUsage(
|
|
1899
|
+
`\u4F60\u662F\u8BB0\u5FC6\u5F52\u5E76\u52A9\u624B\u3002\u628A\u4E00\u7EC4\u6309\u65F6\u95F4\u6392\u5217\u7684\u65E7\u4E3B\u9898\u5F52\u5E76\u6210\u4E00\u4E2A\u4E3B\u9898\u3002\u4FDD\u7559\u7A33\u5B9A\u504F\u597D\u3001\u4E8B\u5B9E\u3001\u51B3\u5B9A\u53CA\u7406\u7531\u3001\u957F\u671F\u7EA6\u675F\u3001\u963B\u585E\u4E0E\u53EF\u590D\u7528\u7ECF\u9A8C\uFF1B\u5220\u9664\u91CD\u590D\u5185\u5BB9\u3002summary \u6700\u591A ${maxSummaryTokens} tokens\u3002\u53EA\u8F93\u51FA JSON\uFF1A{"title":"...","summary":"..."}`,
|
|
1900
|
+
input,
|
|
1901
|
+
true,
|
|
1902
|
+
maxSummaryTokens + 128
|
|
1903
|
+
);
|
|
1904
|
+
const parsed = JSON.parse(response.content);
|
|
1905
|
+
return {
|
|
1906
|
+
title: parsed.title ?? "\u5386\u53F2\u4E3B\u9898\u5F52\u5E76",
|
|
1907
|
+
summary: parsed.summary ?? "",
|
|
1908
|
+
tokens: response.completionTokens
|
|
1909
|
+
};
|
|
1682
1910
|
}
|
|
1683
1911
|
// ── 第二步:通过工具调用提取实体和关系 ──────────────────────────────────────
|
|
1684
1912
|
async extractEntitiesFromMessages(messages) {
|
|
@@ -1721,9 +1949,8 @@ ${formatted}`
|
|
|
1721
1949
|
]);
|
|
1722
1950
|
return {
|
|
1723
1951
|
title: summary.title,
|
|
1724
|
-
detail: summary.detail,
|
|
1725
1952
|
summary: summary.summary,
|
|
1726
|
-
|
|
1953
|
+
tokens: summary.tokens,
|
|
1727
1954
|
entities: extraction.entities,
|
|
1728
1955
|
relations: extraction.relations
|
|
1729
1956
|
};
|
|
@@ -1812,18 +2039,51 @@ ${text}`
|
|
|
1812
2039
|
${context}`, false);
|
|
1813
2040
|
}
|
|
1814
2041
|
};
|
|
2042
|
+
function formatStoredMessage(message) {
|
|
2043
|
+
const extras = [message.parts, message.metadata, message.payload].filter((value) => value && value !== "[]" && value !== "{}").join(" ");
|
|
2044
|
+
return `[${new Date(message.createdAt).toISOString()}] ${message.talkerId || "user"}: ${message.content}${extras ? `
|
|
2045
|
+
meta=${extras}` : ""}`;
|
|
2046
|
+
}
|
|
1815
2047
|
|
|
1816
2048
|
// src/manager/compress.manager.ts
|
|
1817
2049
|
import { v4 as uuidv4 } from "uuid";
|
|
2050
|
+
|
|
2051
|
+
// src/manager/semaphore.ts
|
|
2052
|
+
var Semaphore = class {
|
|
2053
|
+
count;
|
|
2054
|
+
queue = [];
|
|
2055
|
+
constructor(max) {
|
|
2056
|
+
this.count = max;
|
|
2057
|
+
}
|
|
2058
|
+
async run(fn) {
|
|
2059
|
+
await this.acquire();
|
|
2060
|
+
try {
|
|
2061
|
+
return await fn();
|
|
2062
|
+
} finally {
|
|
2063
|
+
this.release();
|
|
2064
|
+
}
|
|
2065
|
+
}
|
|
2066
|
+
acquire() {
|
|
2067
|
+
if (this.count > 0) {
|
|
2068
|
+
this.count--;
|
|
2069
|
+
return Promise.resolve();
|
|
2070
|
+
}
|
|
2071
|
+
return new Promise((resolve) => {
|
|
2072
|
+
this.queue.push(resolve);
|
|
2073
|
+
});
|
|
2074
|
+
}
|
|
2075
|
+
release() {
|
|
2076
|
+
const next = this.queue.shift();
|
|
2077
|
+
if (next) {
|
|
2078
|
+
next();
|
|
2079
|
+
} else {
|
|
2080
|
+
this.count++;
|
|
2081
|
+
}
|
|
2082
|
+
}
|
|
2083
|
+
};
|
|
2084
|
+
|
|
2085
|
+
// src/manager/compress.manager.ts
|
|
1818
2086
|
var CompressManager = class {
|
|
1819
|
-
config;
|
|
1820
|
-
store;
|
|
1821
|
-
grafeo;
|
|
1822
|
-
llm;
|
|
1823
|
-
embed;
|
|
1824
|
-
sessionCache;
|
|
1825
|
-
semaphore;
|
|
1826
|
-
sessionChain = /* @__PURE__ */ new Map();
|
|
1827
2087
|
constructor(config, store, grafeo, llm, embed, sessionCache) {
|
|
1828
2088
|
this.config = config;
|
|
1829
2089
|
this.store = store;
|
|
@@ -1833,125 +2093,237 @@ var CompressManager = class {
|
|
|
1833
2093
|
this.sessionCache = sessionCache;
|
|
1834
2094
|
this.semaphore = new Semaphore(config.maxConcurrentCompressions);
|
|
1835
2095
|
}
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
2096
|
+
semaphore;
|
|
2097
|
+
sessionChain = /* @__PURE__ */ new Map();
|
|
2098
|
+
backgroundGraphs = /* @__PURE__ */ new Set();
|
|
2099
|
+
triggerCompress(sessionId, force = false, waitGraph = false, rawLimit) {
|
|
2100
|
+
return this.enqueueSessionTask(
|
|
2101
|
+
sessionId,
|
|
2102
|
+
() => this.doCompress(sessionId, force, waitGraph, rawLimit)
|
|
1840
2103
|
);
|
|
1841
|
-
|
|
1842
|
-
|
|
2104
|
+
}
|
|
2105
|
+
/** 按给定预算收敛 Topic;persist=false 时只缓存当前模型使用的临时视图。 */
|
|
2106
|
+
triggerTopicCompaction(sessionId, tokenLimit, persist) {
|
|
2107
|
+
return this.enqueueSessionTask(
|
|
2108
|
+
sessionId,
|
|
2109
|
+
() => this.compactOldTopics(sessionId, Math.max(1, Math.floor(tokenLimit)), persist)
|
|
2110
|
+
);
|
|
2111
|
+
}
|
|
2112
|
+
enqueueSessionTask(sessionId, task) {
|
|
2113
|
+
const previous = this.sessionChain.get(sessionId) ?? Promise.resolve();
|
|
2114
|
+
const next = previous.then(
|
|
2115
|
+
() => this.semaphore.run(task)
|
|
2116
|
+
);
|
|
2117
|
+
const settled = next.catch(() => {
|
|
2118
|
+
});
|
|
2119
|
+
this.sessionChain.set(sessionId, settled);
|
|
2120
|
+
settled.finally(() => {
|
|
2121
|
+
if (this.sessionChain.get(sessionId) === settled) this.sessionChain.delete(sessionId);
|
|
2122
|
+
});
|
|
1843
2123
|
return next;
|
|
1844
2124
|
}
|
|
1845
|
-
async
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
1854
|
-
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
} catch (err) {
|
|
1858
|
-
console.error(`[CompressManager] LLM compress failed for session ${sessionId}: ${formatError(err)}`);
|
|
1859
|
-
return;
|
|
2125
|
+
async waitForIdle(sessionId) {
|
|
2126
|
+
await (this.sessionChain.get(sessionId) ?? Promise.resolve());
|
|
2127
|
+
}
|
|
2128
|
+
isBusy(sessionId) {
|
|
2129
|
+
return this.sessionChain.has(sessionId);
|
|
2130
|
+
}
|
|
2131
|
+
async waitForAllIdle() {
|
|
2132
|
+
while (this.sessionChain.size > 0 || this.backgroundGraphs.size > 0) {
|
|
2133
|
+
await Promise.all([
|
|
2134
|
+
...this.sessionChain.values(),
|
|
2135
|
+
...this.backgroundGraphs
|
|
2136
|
+
]);
|
|
1860
2137
|
}
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
2138
|
+
}
|
|
2139
|
+
async doCompress(sessionId, force, waitGraph, rawLimitOverride) {
|
|
2140
|
+
const entry = this.sessionCache.getEntry(sessionId);
|
|
2141
|
+
if (!entry || entry.messages.length === 0) return;
|
|
2142
|
+
const rawLimit = Math.max(1, rawLimitOverride ?? this.rawLimit(entry.lastModelContextTokens));
|
|
2143
|
+
const threshold = Math.max(1, Math.floor(rawLimit * this.config.precompressionRatio));
|
|
2144
|
+
if (!force && entry.totalTokens < threshold) return;
|
|
2145
|
+
let batchTarget = Math.max(1, Math.floor(rawLimit * this.config.compressionBatchRatio));
|
|
2146
|
+
if (this.config.compressionBatchTokenLimit > 0) {
|
|
2147
|
+
batchTarget = Math.min(batchTarget, this.config.compressionBatchTokenLimit);
|
|
2148
|
+
}
|
|
2149
|
+
const messages = this.sessionCache.selectOldestMessageBatch(sessionId, batchTarget);
|
|
2150
|
+
if (messages.length === 0) return;
|
|
2151
|
+
let result;
|
|
1864
2152
|
try {
|
|
1865
|
-
|
|
1866
|
-
} catch (
|
|
1867
|
-
console.error(
|
|
2153
|
+
result = await this.llm.summarizeMessages(messages);
|
|
2154
|
+
} catch (error) {
|
|
2155
|
+
console.error(
|
|
2156
|
+
`[CompressManager] LLM compress failed for session ${sessionId}: ${formatError(error)}`
|
|
2157
|
+
);
|
|
2158
|
+
throw error;
|
|
1868
2159
|
}
|
|
2160
|
+
const summary = result.summary.trim();
|
|
2161
|
+
if (!summary) throw new Error(`LLM returned an empty summary for session ${sessionId}`);
|
|
2162
|
+
const vector = await this.embed.embedOne(summary).catch((error) => {
|
|
2163
|
+
console.error(`[CompressManager] Embed topic failed for session ${sessionId}:`, error);
|
|
2164
|
+
return [];
|
|
2165
|
+
});
|
|
2166
|
+
const first = messages[0];
|
|
2167
|
+
const last = messages[messages.length - 1];
|
|
2168
|
+
const now = Date.now();
|
|
1869
2169
|
const topic = {
|
|
1870
2170
|
summaryId: uuidv4(),
|
|
1871
2171
|
sessionId,
|
|
1872
|
-
userId,
|
|
1873
|
-
chatId,
|
|
1874
|
-
title,
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
2172
|
+
userId: entry.ids.userId,
|
|
2173
|
+
chatId: entry.ids.chatId,
|
|
2174
|
+
title: result.title,
|
|
2175
|
+
summary,
|
|
2176
|
+
tokens: result.tokens != null && result.tokens > 0 ? result.tokens : Math.max(1, countTokens(summary)),
|
|
2177
|
+
startMessageId: first.messageId,
|
|
2178
|
+
endMessageId: last.messageId,
|
|
2179
|
+
startTime: first.createdAt,
|
|
2180
|
+
endTime: last.createdAt,
|
|
2181
|
+
createdAt: now,
|
|
2182
|
+
updatedAt: now,
|
|
1882
2183
|
recallCount: 0,
|
|
1883
|
-
vector
|
|
2184
|
+
vector
|
|
1884
2185
|
};
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
2186
|
+
await this.store.addTopic(topic);
|
|
2187
|
+
this.sessionCache.appendTopic(sessionId, topic);
|
|
2188
|
+
this.sessionCache.removeMessages(sessionId, messages.map((message) => message.messageId));
|
|
2189
|
+
await this.compactOldTopics(
|
|
2190
|
+
sessionId,
|
|
2191
|
+
this.config.compressedContextTokenLimit,
|
|
2192
|
+
true
|
|
2193
|
+
);
|
|
2194
|
+
const graphTask = this.extractAndPersistGraph(
|
|
2195
|
+
messages,
|
|
2196
|
+
sessionId,
|
|
2197
|
+
entry.ids.chatId,
|
|
2198
|
+
entry.ids.userId,
|
|
2199
|
+
last.createdAt
|
|
2200
|
+
);
|
|
1899
2201
|
if (waitGraph) {
|
|
1900
2202
|
await withTimeout(graphTask, this.config.graphBuildTimeoutMs, "flushChat graph build");
|
|
1901
2203
|
} else {
|
|
1902
|
-
graphTask.catch((
|
|
1903
|
-
console.error(`[CompressManager] Background graph persist failed:`,
|
|
2204
|
+
const tracked = graphTask.catch((error) => {
|
|
2205
|
+
console.error(`[CompressManager] Background graph persist failed:`, error);
|
|
1904
2206
|
});
|
|
2207
|
+
this.backgroundGraphs.add(tracked);
|
|
2208
|
+
tracked.finally(() => this.backgroundGraphs.delete(tracked));
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2211
|
+
async compactOldTopics(sessionId, tokenLimit, persist) {
|
|
2212
|
+
let transientTopics = persist ? void 0 : this.sessionCache.getTopicView(sessionId, tokenLimit) ?? [...this.sessionCache.getEntry(sessionId)?.topics ?? []];
|
|
2213
|
+
while (true) {
|
|
2214
|
+
const entry = this.sessionCache.getEntry(sessionId);
|
|
2215
|
+
if (!entry) return;
|
|
2216
|
+
const topics = persist ? entry.topics : transientTopics;
|
|
2217
|
+
const topicTokens = sumTopicTokens(topics);
|
|
2218
|
+
if (topicTokens <= tokenLimit) {
|
|
2219
|
+
if (!persist) this.sessionCache.setTopicView(sessionId, tokenLimit, topics);
|
|
2220
|
+
return;
|
|
2221
|
+
}
|
|
2222
|
+
const target = Math.max(1, Math.floor(topicTokens / 2));
|
|
2223
|
+
const selected = [];
|
|
2224
|
+
let selectedTokens = 0;
|
|
2225
|
+
for (const topic of topics) {
|
|
2226
|
+
selected.push(topic);
|
|
2227
|
+
selectedTokens += topic.tokens;
|
|
2228
|
+
if (selectedTokens >= target && (selected.length >= 2 || topics.length === 1)) break;
|
|
2229
|
+
}
|
|
2230
|
+
if (selected.length === 0) return;
|
|
2231
|
+
console.info(
|
|
2232
|
+
`[CompressManager] session ${sessionId} compressed Topic context exceeds ${tokenLimit} tokens; compacting ${selected.length} old topics${persist ? " persistently" : " for a transient model view"}.`
|
|
2233
|
+
);
|
|
2234
|
+
const unselectedTokens = topicTokens - selectedTokens;
|
|
2235
|
+
const maxSummaryTokens = Math.max(
|
|
2236
|
+
1,
|
|
2237
|
+
Math.min(
|
|
2238
|
+
this.config.topicSummaryMaxTokens,
|
|
2239
|
+
selectedTokens - 1,
|
|
2240
|
+
Math.max(1, tokenLimit - unselectedTokens)
|
|
2241
|
+
)
|
|
2242
|
+
);
|
|
2243
|
+
const result = await this.llm.summarizeTopics(selected, maxSummaryTokens);
|
|
2244
|
+
const summary = result.summary.trim();
|
|
2245
|
+
if (!summary) throw new Error(`LLM returned an empty Topic rollup for session ${sessionId}`);
|
|
2246
|
+
const vector = persist ? await this.embed.embedOne(summary).catch(() => []) : [];
|
|
2247
|
+
const first = selected[0];
|
|
2248
|
+
const last = selected[selected.length - 1];
|
|
2249
|
+
const now = Date.now();
|
|
2250
|
+
const rollup = {
|
|
2251
|
+
summaryId: uuidv4(),
|
|
2252
|
+
sessionId,
|
|
2253
|
+
userId: entry.ids.userId,
|
|
2254
|
+
chatId: entry.ids.chatId,
|
|
2255
|
+
title: result.title,
|
|
2256
|
+
summary,
|
|
2257
|
+
tokens: Math.max(1, countTokens(summary)),
|
|
2258
|
+
startMessageId: first.startMessageId,
|
|
2259
|
+
endMessageId: last.endMessageId,
|
|
2260
|
+
startTime: first.startTime,
|
|
2261
|
+
endTime: last.endTime,
|
|
2262
|
+
createdAt: now,
|
|
2263
|
+
updatedAt: now,
|
|
2264
|
+
recallCount: 0,
|
|
2265
|
+
vector
|
|
2266
|
+
};
|
|
2267
|
+
if (rollup.tokens >= selectedTokens) {
|
|
2268
|
+
throw new Error(
|
|
2269
|
+
`Topic compaction made no progress for session ${sessionId}: ${selectedTokens} -> ${rollup.tokens} tokens`
|
|
2270
|
+
);
|
|
2271
|
+
}
|
|
2272
|
+
const removedIds = selected.map((topic) => topic.summaryId);
|
|
2273
|
+
if (persist) {
|
|
2274
|
+
await this.store.addTopic(rollup);
|
|
2275
|
+
this.sessionCache.replaceTopics(sessionId, removedIds, rollup);
|
|
2276
|
+
await this.store.deleteTopicsByIds(removedIds);
|
|
2277
|
+
} else {
|
|
2278
|
+
const ids = new Set(removedIds);
|
|
2279
|
+
transientTopics = [
|
|
2280
|
+
...topics.filter((topic) => !ids.has(topic.summaryId)),
|
|
2281
|
+
rollup
|
|
2282
|
+
].sort(topicOrder);
|
|
2283
|
+
}
|
|
1905
2284
|
}
|
|
1906
2285
|
}
|
|
2286
|
+
rawLimit(modelContextTokens = this.config.defaultModelContextTokens) {
|
|
2287
|
+
return calculateContextBudget(this.config, modelContextTokens).rawTokenLimit;
|
|
2288
|
+
}
|
|
1907
2289
|
async extractAndPersistGraph(messages, sessionId, chatId, userId, endTime) {
|
|
1908
2290
|
const { entities, relations } = await this.llm.extractEntitiesFromMessages(messages);
|
|
1909
2291
|
if (entities.length === 0 && relations.length === 0) return;
|
|
1910
2292
|
await this.persistGraph(entities, relations, sessionId, chatId, userId, endTime);
|
|
1911
2293
|
}
|
|
1912
2294
|
async persistGraph(rawEntities, rawRelations, sessionId, chatId, userId, messageTime) {
|
|
1913
|
-
const entities = rawEntities.map((
|
|
1914
|
-
name:
|
|
1915
|
-
type:
|
|
1916
|
-
meta: {
|
|
1917
|
-
sessionId,
|
|
1918
|
-
chatId,
|
|
1919
|
-
userId,
|
|
1920
|
-
messageTime,
|
|
1921
|
-
...e.meta
|
|
1922
|
-
}
|
|
2295
|
+
const entities = rawEntities.map((entity) => ({
|
|
2296
|
+
name: entity.name,
|
|
2297
|
+
type: entity.type,
|
|
2298
|
+
meta: { sessionId, chatId, userId, messageTime, ...entity.meta }
|
|
1923
2299
|
}));
|
|
1924
|
-
const relations = rawRelations.map((
|
|
1925
|
-
from:
|
|
1926
|
-
to:
|
|
1927
|
-
type:
|
|
1928
|
-
happenedAt:
|
|
1929
|
-
meta: {
|
|
1930
|
-
sessionId,
|
|
1931
|
-
chatId,
|
|
1932
|
-
userId,
|
|
1933
|
-
messageTime,
|
|
1934
|
-
...r.meta
|
|
1935
|
-
}
|
|
2300
|
+
const relations = rawRelations.map((relation) => ({
|
|
2301
|
+
from: relation.from,
|
|
2302
|
+
to: relation.to,
|
|
2303
|
+
type: relation.type,
|
|
2304
|
+
happenedAt: relation.happenedAt,
|
|
2305
|
+
meta: { sessionId, chatId, userId, messageTime, ...relation.meta }
|
|
1936
2306
|
}));
|
|
1937
|
-
const
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
await this.grafeo.upsertEntitiesAndRelations(entities, relations, embeddings);
|
|
1948
|
-
} catch (err) {
|
|
1949
|
-
console.error(`[CompressManager] Upsert entities/relations failed:`, err);
|
|
1950
|
-
}
|
|
2307
|
+
const names = [
|
|
2308
|
+
.../* @__PURE__ */ new Set([
|
|
2309
|
+
...entities.map((entity) => entity.name),
|
|
2310
|
+
...relations.flatMap((relation) => [relation.from, relation.to])
|
|
2311
|
+
])
|
|
2312
|
+
];
|
|
2313
|
+
if (names.length === 0) return;
|
|
2314
|
+
const vectors = await this.embed.embed(names);
|
|
2315
|
+
const embeddings = new Map(names.map((name, index) => [name, vectors[index]]));
|
|
2316
|
+
await this.grafeo.upsertEntitiesAndRelations(entities, relations, embeddings);
|
|
1951
2317
|
}
|
|
1952
2318
|
};
|
|
1953
|
-
function
|
|
1954
|
-
return
|
|
2319
|
+
function sumTopicTokens(topics) {
|
|
2320
|
+
return topics.reduce((sum, topic) => sum + Math.max(1, topic.tokens), 0);
|
|
2321
|
+
}
|
|
2322
|
+
function topicOrder(a, b) {
|
|
2323
|
+
return a.endTime - b.endTime || a.summaryId.localeCompare(b.summaryId);
|
|
2324
|
+
}
|
|
2325
|
+
function formatError(error) {
|
|
2326
|
+
return error instanceof Error ? error.message : String(error);
|
|
1955
2327
|
}
|
|
1956
2328
|
function withTimeout(promise, timeoutMs, label) {
|
|
1957
2329
|
let timer;
|
|
@@ -1981,22 +2353,30 @@ var FactCache = class {
|
|
|
1981
2353
|
key(level, id) {
|
|
1982
2354
|
return `${level}:${id}`;
|
|
1983
2355
|
}
|
|
1984
|
-
async add(content, level, userId, chatId, sessionId = "default") {
|
|
2356
|
+
async add(content, level, userId, chatId, sessionId = "default", key) {
|
|
2357
|
+
const id = level === "user" ? userId : chatId;
|
|
2358
|
+
const cacheKey = this.key(level, id);
|
|
2359
|
+
const existing = this.cache.get(cacheKey) ?? [];
|
|
2360
|
+
const normalizedKey = key?.trim() || void 0;
|
|
2361
|
+
const existingIndex = normalizedKey ? existing.findIndex((fact2) => fact2.key === normalizedKey) : -1;
|
|
2362
|
+
const previous = existingIndex >= 0 ? existing[existingIndex] : void 0;
|
|
2363
|
+
const now = Date.now();
|
|
1985
2364
|
const fact = {
|
|
1986
|
-
factId: uuidv42(),
|
|
2365
|
+
factId: previous?.factId ?? uuidv42(),
|
|
1987
2366
|
level,
|
|
1988
2367
|
chatId,
|
|
1989
2368
|
sessionId,
|
|
1990
2369
|
userId,
|
|
2370
|
+
key: normalizedKey,
|
|
1991
2371
|
content,
|
|
1992
|
-
createdAt:
|
|
2372
|
+
createdAt: previous?.createdAt ?? now,
|
|
2373
|
+
updatedAt: now
|
|
1993
2374
|
};
|
|
1994
|
-
await this.store.
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
this.cache.set(k, existing);
|
|
2375
|
+
if (previous) await this.store.updateFact(fact);
|
|
2376
|
+
else await this.store.saveFact(fact);
|
|
2377
|
+
if (existingIndex >= 0) existing[existingIndex] = fact;
|
|
2378
|
+
else existing.push(fact);
|
|
2379
|
+
this.cache.set(cacheKey, existing);
|
|
2000
2380
|
}
|
|
2001
2381
|
get(level, id) {
|
|
2002
2382
|
return this.cache.get(this.key(level, id)) ?? [];
|
|
@@ -2024,7 +2404,9 @@ var FactCache = class {
|
|
|
2024
2404
|
toString(level, id) {
|
|
2025
2405
|
const facts = this.get(level, id);
|
|
2026
2406
|
if (facts.length === 0) return "";
|
|
2027
|
-
return facts.sort((a, b) => a.createdAt - b.createdAt).map(
|
|
2407
|
+
return facts.sort((a, b) => a.createdAt - b.createdAt).map(
|
|
2408
|
+
(f) => `${new Date(f.updatedAt ?? f.createdAt).toISOString()}\uFF1A${f.key ? `[${f.key}] ` : ""}${f.content}`
|
|
2409
|
+
).join("\n");
|
|
2028
2410
|
}
|
|
2029
2411
|
};
|
|
2030
2412
|
|
|
@@ -2184,11 +2566,8 @@ var KnowledgeManager = class {
|
|
|
2184
2566
|
const now = Date.now();
|
|
2185
2567
|
const content = opts.content;
|
|
2186
2568
|
const contentHash = createHash("sha256").update(content).digest("hex");
|
|
2187
|
-
const
|
|
2188
|
-
|
|
2189
|
-
eq("chat_id", chatId),
|
|
2190
|
-
eq("session_id", sessionId)
|
|
2191
|
-
];
|
|
2569
|
+
const dedupeScope = opts.scope ?? "session";
|
|
2570
|
+
const domainFilter = dedupeScope === "user" ? [eq("user_id", userId)] : dedupeScope === "chat" ? [eq("chat_id", chatId)] : [eq("session_id", sessionId)];
|
|
2192
2571
|
const existing = await this.store.findDocumentByHash(contentHash, domainFilter);
|
|
2193
2572
|
if (existing) return { docId: existing.docId };
|
|
2194
2573
|
const pieces = chunkMarkdown(content, {
|
|
@@ -2507,73 +2886,218 @@ function withTimeout2(promise, timeoutMs, label) {
|
|
|
2507
2886
|
var SessionCache = class {
|
|
2508
2887
|
config;
|
|
2509
2888
|
sessions = /* @__PURE__ */ new Map();
|
|
2510
|
-
|
|
2889
|
+
topicViews = /* @__PURE__ */ new Map();
|
|
2890
|
+
legacyHistoryWindows = /* @__PURE__ */ new Map();
|
|
2511
2891
|
constructor(config) {
|
|
2512
2892
|
this.config = config;
|
|
2513
2893
|
}
|
|
2514
2894
|
getEntry(sessionId) {
|
|
2895
|
+
const entry = this.sessions.get(sessionId);
|
|
2896
|
+
if (entry) entry.lastAccessAt = Date.now();
|
|
2897
|
+
return entry;
|
|
2898
|
+
}
|
|
2899
|
+
peekEntry(sessionId) {
|
|
2515
2900
|
return this.sessions.get(sessionId);
|
|
2516
2901
|
}
|
|
2517
2902
|
getOrCreateEntry(sessionId, chatId, userId) {
|
|
2518
2903
|
let entry = this.sessions.get(sessionId);
|
|
2519
2904
|
if (!entry) {
|
|
2520
|
-
entry = {
|
|
2905
|
+
entry = {
|
|
2906
|
+
messages: [],
|
|
2907
|
+
totalTokens: 0,
|
|
2908
|
+
topics: [],
|
|
2909
|
+
topicTokens: 0,
|
|
2910
|
+
ids: { chatId, userId },
|
|
2911
|
+
lastAccessAt: Date.now()
|
|
2912
|
+
};
|
|
2521
2913
|
this.sessions.set(sessionId, entry);
|
|
2914
|
+
} else {
|
|
2915
|
+
entry.ids = { chatId, userId };
|
|
2916
|
+
entry.lastAccessAt = Date.now();
|
|
2522
2917
|
}
|
|
2523
2918
|
return entry;
|
|
2524
2919
|
}
|
|
2525
|
-
|
|
2920
|
+
hydrate(sessionId, chatId, userId, messages, topics) {
|
|
2921
|
+
const entry = this.getOrCreateEntry(sessionId, chatId, userId);
|
|
2922
|
+
entry.messages = sortMessages(dedupeMessages(messages));
|
|
2923
|
+
entry.totalTokens = sumMessageTokens(entry.messages);
|
|
2924
|
+
this.setTopics(sessionId, topics, false);
|
|
2925
|
+
return entry;
|
|
2926
|
+
}
|
|
2927
|
+
upsertMessages(sessionId, messages, chatId, userId) {
|
|
2526
2928
|
const entry = this.getOrCreateEntry(sessionId, chatId, userId);
|
|
2527
|
-
entry.messages.
|
|
2528
|
-
|
|
2929
|
+
const byId = new Map(entry.messages.map((m) => [m.messageId, m]));
|
|
2930
|
+
for (const message of messages) byId.set(message.messageId, message);
|
|
2931
|
+
entry.messages = sortMessages([...byId.values()]);
|
|
2932
|
+
entry.totalTokens = sumMessageTokens(entry.messages);
|
|
2933
|
+
return entry;
|
|
2934
|
+
}
|
|
2935
|
+
/** @deprecated 0.3.x 单元/API 兼容;新代码使用 upsertMessages。 */
|
|
2936
|
+
addMessages(sessionId, messages, chatId, userId) {
|
|
2937
|
+
const entry = this.upsertMessages(sessionId, messages, chatId, userId);
|
|
2529
2938
|
return entry.totalTokens >= this.config.sessionTokenLimit;
|
|
2530
2939
|
}
|
|
2940
|
+
/** @deprecated 新压缩链按 messageId 精确移除。 */
|
|
2531
2941
|
clearMessages(sessionId, keepAfter) {
|
|
2532
2942
|
const entry = this.sessions.get(sessionId);
|
|
2533
2943
|
if (!entry) return;
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2944
|
+
entry.messages = keepAfter == null ? [] : entry.messages.filter((message) => message.createdAt > keepAfter);
|
|
2945
|
+
entry.totalTokens = sumMessageTokens(entry.messages);
|
|
2946
|
+
}
|
|
2947
|
+
removeMessages(sessionId, messageIds) {
|
|
2948
|
+
const entry = this.sessions.get(sessionId);
|
|
2949
|
+
if (!entry) return;
|
|
2950
|
+
const ids = new Set(messageIds);
|
|
2951
|
+
entry.messages = entry.messages.filter((m) => !ids.has(m.messageId));
|
|
2952
|
+
entry.totalTokens = sumMessageTokens(entry.messages);
|
|
2953
|
+
entry.lastAccessAt = Date.now();
|
|
2541
2954
|
}
|
|
2542
2955
|
getSessionMessages(sessionId) {
|
|
2543
|
-
return this.
|
|
2956
|
+
return this.getEntry(sessionId)?.messages ?? [];
|
|
2544
2957
|
}
|
|
2545
|
-
|
|
2546
|
-
|
|
2958
|
+
selectOldestMessageBatch(sessionId, targetTokens) {
|
|
2959
|
+
const messages = this.getSessionMessages(sessionId);
|
|
2960
|
+
if (messages.length === 0 || targetTokens <= 0) return [];
|
|
2961
|
+
const selected = [];
|
|
2962
|
+
let tokens = 0;
|
|
2963
|
+
for (const message of messages) {
|
|
2964
|
+
selected.push(message);
|
|
2965
|
+
tokens += effectiveUsage(message);
|
|
2966
|
+
if (tokens >= targetTokens) break;
|
|
2967
|
+
}
|
|
2968
|
+
return selected;
|
|
2547
2969
|
}
|
|
2548
|
-
|
|
2549
|
-
const
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
const
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
this.
|
|
2970
|
+
setTopics(sessionId, topics, truncate = true) {
|
|
2971
|
+
const entry = this.sessions.get(sessionId);
|
|
2972
|
+
if (!entry) return;
|
|
2973
|
+
const ordered = [...topics].sort(topicOrder2);
|
|
2974
|
+
const kept = [];
|
|
2975
|
+
let used = 0;
|
|
2976
|
+
for (let i = ordered.length - 1; i >= 0; i--) {
|
|
2977
|
+
const topic = normalizeTopic(ordered[i]);
|
|
2978
|
+
if (truncate && used + topic.tokens > this.config.compressedContextTokenLimit && kept.length > 0) break;
|
|
2979
|
+
kept.push(topic);
|
|
2980
|
+
used += topic.tokens;
|
|
2981
|
+
}
|
|
2982
|
+
entry.topics = kept.reverse();
|
|
2983
|
+
entry.topicTokens = used;
|
|
2984
|
+
entry.lastAccessAt = Date.now();
|
|
2985
|
+
this.topicViews.delete(sessionId);
|
|
2986
|
+
}
|
|
2987
|
+
appendTopic(sessionId, topic) {
|
|
2988
|
+
const entry = this.sessions.get(sessionId);
|
|
2989
|
+
if (!entry) return;
|
|
2990
|
+
this.setTopics(sessionId, [...entry.topics, topic], false);
|
|
2991
|
+
}
|
|
2992
|
+
replaceTopics(sessionId, removedIds, replacement) {
|
|
2993
|
+
const entry = this.sessions.get(sessionId);
|
|
2994
|
+
if (!entry) return;
|
|
2995
|
+
const ids = new Set(removedIds);
|
|
2996
|
+
this.setTopics(
|
|
2997
|
+
sessionId,
|
|
2998
|
+
[...entry.topics.filter((topic) => !ids.has(topic.summaryId)), replacement],
|
|
2999
|
+
false
|
|
3000
|
+
);
|
|
3001
|
+
}
|
|
3002
|
+
getTopics(sessionId) {
|
|
3003
|
+
return this.getEntry(sessionId)?.topics ?? [];
|
|
3004
|
+
}
|
|
3005
|
+
getTopicView(sessionId, tokenLimit) {
|
|
3006
|
+
return this.topicViews.get(sessionId)?.get(tokenLimit);
|
|
3007
|
+
}
|
|
3008
|
+
setTopicView(sessionId, tokenLimit, topics) {
|
|
3009
|
+
let views = this.topicViews.get(sessionId);
|
|
3010
|
+
if (!views) {
|
|
3011
|
+
views = /* @__PURE__ */ new Map();
|
|
3012
|
+
this.topicViews.set(sessionId, views);
|
|
3013
|
+
}
|
|
3014
|
+
views.set(tokenLimit, [...topics].map(normalizeTopic).sort(topicOrder2));
|
|
3015
|
+
}
|
|
3016
|
+
buildCompressedContext(sessionId, topics) {
|
|
3017
|
+
return (topics ?? this.getTopics(sessionId)).map((topic) => topic.title ? `## ${topic.title}
|
|
3018
|
+
${topic.summary}` : topic.summary).join("\n\n");
|
|
3019
|
+
}
|
|
3020
|
+
/** @deprecated 三级窗口兼容,仅供 0.3.x 调用方过渡。 */
|
|
3021
|
+
buildHistoryWindow(sessionId, groups) {
|
|
3022
|
+
const [detailCount, summaryCount, conciseCount] = this.config.topicRatio;
|
|
3023
|
+
const values = [
|
|
3024
|
+
...groups.detail.slice(0, detailCount).map((topic) => topic.detail ?? topic.summary),
|
|
3025
|
+
...groups.summary.slice(0, summaryCount).map((topic) => topic.summary),
|
|
3026
|
+
...groups.concise.slice(0, conciseCount).map((topic) => topic.concise ?? topic.summary)
|
|
3027
|
+
];
|
|
3028
|
+
let remaining = this.config.historyWindowTokenLimit;
|
|
3029
|
+
const kept = [];
|
|
3030
|
+
for (const value of values) {
|
|
3031
|
+
const tokens = countTokens(value);
|
|
3032
|
+
if (tokens > remaining) break;
|
|
3033
|
+
kept.push(value);
|
|
3034
|
+
remaining -= tokens;
|
|
3035
|
+
}
|
|
3036
|
+
this.legacyHistoryWindows.set(sessionId, kept.join("\n\n"));
|
|
2569
3037
|
}
|
|
3038
|
+
/** @deprecated MemoryManager.getHistoryWindow 返回结构化窗口。 */
|
|
2570
3039
|
getHistoryWindow(sessionId) {
|
|
2571
|
-
return this.
|
|
3040
|
+
return this.legacyHistoryWindows.get(sessionId) ?? this.buildCompressedContext(sessionId);
|
|
2572
3041
|
}
|
|
3042
|
+
/** @deprecated 仅为 0.3.x 兼容。 */
|
|
2573
3043
|
setHistoryWindow(sessionId, content) {
|
|
2574
|
-
this.
|
|
3044
|
+
this.legacyHistoryWindows.set(sessionId, content);
|
|
3045
|
+
}
|
|
3046
|
+
setModelContextTokens(sessionId, modelContextTokens) {
|
|
3047
|
+
const entry = this.sessions.get(sessionId);
|
|
3048
|
+
if (!entry) return;
|
|
3049
|
+
entry.lastModelContextTokens = modelContextTokens;
|
|
3050
|
+
entry.lastAccessAt = Date.now();
|
|
3051
|
+
}
|
|
3052
|
+
getAllSessionIds() {
|
|
3053
|
+
return [...this.sessions.keys()];
|
|
3054
|
+
}
|
|
3055
|
+
delete(sessionId) {
|
|
3056
|
+
this.sessions.delete(sessionId);
|
|
3057
|
+
this.topicViews.delete(sessionId);
|
|
3058
|
+
this.legacyHistoryWindows.delete(sessionId);
|
|
3059
|
+
}
|
|
3060
|
+
evictIdle(now, ttlMs, isBusy) {
|
|
3061
|
+
if (ttlMs <= 0) return [];
|
|
3062
|
+
const evicted = [];
|
|
3063
|
+
for (const [sessionId, entry] of this.sessions) {
|
|
3064
|
+
if (now - entry.lastAccessAt < ttlMs || isBusy(sessionId)) continue;
|
|
3065
|
+
this.sessions.delete(sessionId);
|
|
3066
|
+
this.topicViews.delete(sessionId);
|
|
3067
|
+
evicted.push(sessionId);
|
|
3068
|
+
}
|
|
3069
|
+
return evicted;
|
|
2575
3070
|
}
|
|
2576
3071
|
};
|
|
3072
|
+
function effectiveUsage(message) {
|
|
3073
|
+
if (Number.isFinite(message.usage) && message.usage > 0) return Math.floor(message.usage);
|
|
3074
|
+
return Math.max(
|
|
3075
|
+
1,
|
|
3076
|
+
(message.content.length + (message.parts?.length ?? 0) + message.metadata.length + (message.payload?.length ?? 0)) * 2
|
|
3077
|
+
);
|
|
3078
|
+
}
|
|
3079
|
+
function sumMessageTokens(messages) {
|
|
3080
|
+
return messages.reduce((sum, message) => sum + effectiveUsage(message), 0);
|
|
3081
|
+
}
|
|
3082
|
+
function dedupeMessages(messages) {
|
|
3083
|
+
return [...new Map(messages.map((message) => [message.messageId, message])).values()];
|
|
3084
|
+
}
|
|
3085
|
+
function sortMessages(messages) {
|
|
3086
|
+
return messages.sort(
|
|
3087
|
+
(a, b) => a.createdAt - b.createdAt || a.messageId.localeCompare(b.messageId)
|
|
3088
|
+
);
|
|
3089
|
+
}
|
|
3090
|
+
function topicOrder2(a, b) {
|
|
3091
|
+
return a.endTime - b.endTime || a.summaryId.localeCompare(b.summaryId);
|
|
3092
|
+
}
|
|
3093
|
+
function normalizeTopic(topic) {
|
|
3094
|
+
const summary = topic.summary || topic.detail || topic.concise || "";
|
|
3095
|
+
return {
|
|
3096
|
+
...topic,
|
|
3097
|
+
summary,
|
|
3098
|
+
tokens: topic.tokens > 0 ? topic.tokens : Math.max(1, countTokens(summary))
|
|
3099
|
+
};
|
|
3100
|
+
}
|
|
2577
3101
|
|
|
2578
3102
|
// src/memory.manager.ts
|
|
2579
3103
|
var MemoryManager = class {
|
|
@@ -2588,7 +3112,14 @@ var MemoryManager = class {
|
|
|
2588
3112
|
knowledgeManager;
|
|
2589
3113
|
sessionMap = /* @__PURE__ */ new Map();
|
|
2590
3114
|
optimizeTimer;
|
|
3115
|
+
sessionSweepTimer;
|
|
2591
3116
|
optimizeRunning = false;
|
|
3117
|
+
optimizeTask;
|
|
3118
|
+
destroyTask;
|
|
3119
|
+
hydration = /* @__PURE__ */ new Map();
|
|
3120
|
+
pendingWrites = /* @__PURE__ */ new Map();
|
|
3121
|
+
topicCompactions = /* @__PURE__ */ new Map();
|
|
3122
|
+
warnedDefaultModelContext = false;
|
|
2592
3123
|
constructor(config) {
|
|
2593
3124
|
this.config = resolveConfig(config);
|
|
2594
3125
|
this.store = new MemoryStore(this.config);
|
|
@@ -2622,17 +3153,26 @@ var MemoryManager = class {
|
|
|
2622
3153
|
for (const s of allSessions) {
|
|
2623
3154
|
this.sessionMap.set(s.sessionId, this.deserializeSession(s));
|
|
2624
3155
|
}
|
|
2625
|
-
await this.restoreFromStorage();
|
|
2626
3156
|
if (this.config.autoOptimizeOnInit) {
|
|
2627
|
-
|
|
3157
|
+
this.optimizeTask = this.runBackgroundOptimize(this.config.optimizeVersionRetentionMs);
|
|
2628
3158
|
}
|
|
2629
3159
|
if (this.config.autoOptimizeIntervalMs > 0) {
|
|
2630
3160
|
const retention = Math.max(this.config.optimizeVersionRetentionMs, 6e4);
|
|
2631
3161
|
this.optimizeTimer = setInterval(() => {
|
|
2632
|
-
|
|
3162
|
+
this.optimizeTask = this.runBackgroundOptimize(retention);
|
|
2633
3163
|
}, this.config.autoOptimizeIntervalMs);
|
|
2634
3164
|
this.optimizeTimer.unref?.();
|
|
2635
3165
|
}
|
|
3166
|
+
if (this.config.sessionIdleTtlMs > 0) {
|
|
3167
|
+
this.sessionSweepTimer = setInterval(() => {
|
|
3168
|
+
this.sessionCache.evictIdle(
|
|
3169
|
+
Date.now(),
|
|
3170
|
+
this.config.sessionIdleTtlMs,
|
|
3171
|
+
(sessionId) => this.isSessionBusy(sessionId)
|
|
3172
|
+
);
|
|
3173
|
+
}, this.config.sessionSweepIntervalMs);
|
|
3174
|
+
this.sessionSweepTimer.unref?.();
|
|
3175
|
+
}
|
|
2636
3176
|
}
|
|
2637
3177
|
/** 后台压实的统一入口:防重入(上一轮未结束则跳过),失败仅告警不影响服务。*/
|
|
2638
3178
|
async runBackgroundOptimize(retentionMs) {
|
|
@@ -2660,120 +3200,141 @@ var MemoryManager = class {
|
|
|
2660
3200
|
async optimizeStorage(retentionMs) {
|
|
2661
3201
|
return this.store.optimizeStorage(retentionMs ?? this.config.optimizeVersionRetentionMs);
|
|
2662
3202
|
}
|
|
2663
|
-
async
|
|
2664
|
-
|
|
2665
|
-
|
|
2666
|
-
if (
|
|
2667
|
-
|
|
2668
|
-
|
|
2669
|
-
|
|
2670
|
-
|
|
2671
|
-
|
|
2672
|
-
|
|
2673
|
-
|
|
2674
|
-
|
|
2675
|
-
|
|
2676
|
-
|
|
2677
|
-
|
|
2678
|
-
|
|
2679
|
-
|
|
2680
|
-
|
|
2681
|
-
|
|
2682
|
-
|
|
2683
|
-
|
|
2684
|
-
|
|
2685
|
-
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2692
|
-
|
|
2693
|
-
|
|
2694
|
-
}
|
|
2695
|
-
})
|
|
2696
|
-
)
|
|
2697
|
-
);
|
|
3203
|
+
async ensureSessionHydrated(sessionId, fallbackIds) {
|
|
3204
|
+
if (this.sessionCache.peekEntry(sessionId)) return;
|
|
3205
|
+
let task = this.hydration.get(sessionId);
|
|
3206
|
+
if (!task) {
|
|
3207
|
+
task = (async () => {
|
|
3208
|
+
const allTopics = await this.store.getTopicsBySession(sessionId);
|
|
3209
|
+
const latestTopic = allTopics.at(-1);
|
|
3210
|
+
const allRawAfterBoundary = latestTopic ? await this.store.getMessagesAfterBoundary(
|
|
3211
|
+
sessionId,
|
|
3212
|
+
latestTopic.endTime,
|
|
3213
|
+
latestTopic.endMessageId
|
|
3214
|
+
) : await this.store.getAllMessagesBySession(sessionId);
|
|
3215
|
+
const since = this.config.maxHistoryAgeMs > 0 ? Date.now() - this.config.maxHistoryAgeMs : 0;
|
|
3216
|
+
const rawMessages = since > 0 ? allRawAfterBoundary.filter((message) => message.createdAt >= since) : allRawAfterBoundary;
|
|
3217
|
+
const session = this.sessionMap.get(sessionId);
|
|
3218
|
+
const ids = allRawAfterBoundary.at(-1) ?? latestTopic ?? session ?? fallbackIds ?? { chatId: DEFAULT_CHAT_ID, userId: DEFAULT_USER_ID };
|
|
3219
|
+
const visibleTopics = since > 0 ? allTopics.filter((topic) => topic.endTime >= since) : allTopics;
|
|
3220
|
+
this.sessionCache.hydrate(
|
|
3221
|
+
sessionId,
|
|
3222
|
+
ids.chatId,
|
|
3223
|
+
ids.userId,
|
|
3224
|
+
rawMessages,
|
|
3225
|
+
visibleTopics
|
|
3226
|
+
);
|
|
3227
|
+
})();
|
|
3228
|
+
this.hydration.set(sessionId, task);
|
|
3229
|
+
task.finally(() => {
|
|
3230
|
+
if (this.hydration.get(sessionId) === task) this.hydration.delete(sessionId);
|
|
3231
|
+
});
|
|
3232
|
+
}
|
|
3233
|
+
await task;
|
|
2698
3234
|
}
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
3235
|
+
isSessionBusy(sessionId) {
|
|
3236
|
+
return this.hydration.has(sessionId) || this.pendingWrites.has(sessionId) || [...this.topicCompactions.keys()].some((key) => key.startsWith(`${sessionId}:`)) || this.compressManager.isBusy(sessionId);
|
|
3237
|
+
}
|
|
3238
|
+
waitForPendingWrites(sessionId) {
|
|
3239
|
+
return this.pendingWrites.get(sessionId) ?? Promise.resolve();
|
|
3240
|
+
}
|
|
3241
|
+
queueSessionWrite(sessionId, write) {
|
|
3242
|
+
const previous = this.pendingWrites.get(sessionId) ?? Promise.resolve();
|
|
3243
|
+
const task = previous.catch(() => {
|
|
3244
|
+
}).then(write);
|
|
3245
|
+
const settled = task.catch(() => {
|
|
3246
|
+
});
|
|
3247
|
+
this.pendingWrites.set(sessionId, settled);
|
|
3248
|
+
settled.finally(() => {
|
|
3249
|
+
if (this.pendingWrites.get(sessionId) === settled) this.pendingWrites.delete(sessionId);
|
|
3250
|
+
});
|
|
3251
|
+
return task;
|
|
3252
|
+
}
|
|
3253
|
+
updateChat(messages, opts) {
|
|
3254
|
+
if (messages.length === 0) return Promise.resolve();
|
|
3255
|
+
const sessionId = opts?.sessionId ?? messages[0]?.sessionId ?? DEFAULT_SESSION_ID;
|
|
3256
|
+
return this.queueSessionWrite(sessionId, () => this.doUpdateChat(messages, opts, sessionId));
|
|
3257
|
+
}
|
|
3258
|
+
async doUpdateChat(messages, opts, sessionId) {
|
|
3259
|
+
const userId = opts?.userId ?? messages[0]?.userId ?? DEFAULT_USER_ID;
|
|
3260
|
+
const chatId = opts?.chatId ?? messages[0]?.chatId ?? DEFAULT_CHAT_ID;
|
|
2704
3261
|
const now = Date.now();
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
|
|
2711
|
-
|
|
2712
|
-
|
|
2713
|
-
|
|
2714
|
-
|
|
2715
|
-
|
|
2716
|
-
|
|
2717
|
-
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2722
|
-
|
|
2723
|
-
|
|
2724
|
-
}
|
|
2725
|
-
const stored = withUsage.map((m, i) => ({
|
|
2726
|
-
messageId: m.messageId,
|
|
2727
|
-
talkerId: m.talkerId,
|
|
2728
|
-
chatId: m.chatId,
|
|
2729
|
-
userId: m.userId,
|
|
2730
|
-
sessionId: m.sessionId,
|
|
2731
|
-
type: m.type,
|
|
2732
|
-
content: m.content,
|
|
2733
|
-
parts: m.parts,
|
|
2734
|
-
usage: m.usage,
|
|
2735
|
-
metadata: m.metadata,
|
|
2736
|
-
vector: vectors[i],
|
|
2737
|
-
createdAt: m.createdAt
|
|
2738
|
-
}));
|
|
2739
|
-
await this.store.addMessages(stored);
|
|
2740
|
-
if (!this.sessionMap.has(sessionId)) {
|
|
2741
|
-
const session = {
|
|
2742
|
-
sessionId,
|
|
2743
|
-
chatId,
|
|
2744
|
-
userId,
|
|
2745
|
-
title: opts?.sessionTitle ?? "",
|
|
2746
|
-
metadata: JSON.stringify(opts?.sessionMetadata ?? {}),
|
|
2747
|
-
createdAt: now,
|
|
2748
|
-
updatedAt: now
|
|
3262
|
+
await this.ensureSessionHydrated(sessionId, { chatId, userId });
|
|
3263
|
+
const normalized = messages.map((message) => {
|
|
3264
|
+
const parts = JSON.stringify(message.parts ?? []);
|
|
3265
|
+
const metadata = JSON.stringify(message.metadata ?? {});
|
|
3266
|
+
const payload = message.payload === void 0 ? void 0 : JSON.stringify(message.payload);
|
|
3267
|
+
const tokenInput = [message.content, parts, metadata, payload ?? ""].join("\n");
|
|
3268
|
+
return {
|
|
3269
|
+
messageId: message.messageId ?? uuidv44(),
|
|
3270
|
+
talkerId: message.talkerId ?? "user",
|
|
3271
|
+
chatId: message.chatId ?? chatId,
|
|
3272
|
+
userId: message.userId ?? userId,
|
|
3273
|
+
sessionId: message.sessionId ?? sessionId,
|
|
3274
|
+
type: message.type ?? "text",
|
|
3275
|
+
content: message.content,
|
|
3276
|
+
parts,
|
|
3277
|
+
payload,
|
|
3278
|
+
usage: message.usage ?? countTokens(tokenInput),
|
|
3279
|
+
metadata,
|
|
3280
|
+
createdAt: message.createdAt ?? now
|
|
2749
3281
|
};
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
3282
|
+
});
|
|
3283
|
+
const vectors = await this.embed.embed(normalized.map((message) => message.content));
|
|
3284
|
+
const stored = normalized.map((message, index) => ({
|
|
3285
|
+
...message,
|
|
3286
|
+
vector: vectors[index]
|
|
3287
|
+
}));
|
|
3288
|
+
await this.store.upsertMessages(stored);
|
|
3289
|
+
this.sessionCache.upsertMessages(sessionId, stored, chatId, userId);
|
|
3290
|
+
const existing = this.sessionMap.get(sessionId);
|
|
3291
|
+
const session = existing ? {
|
|
3292
|
+
...this.serializeSession(existing),
|
|
3293
|
+
updatedAt: now,
|
|
3294
|
+
...opts?.sessionTitle !== void 0 && { title: opts.sessionTitle },
|
|
3295
|
+
...opts?.sessionMetadata !== void 0 && {
|
|
3296
|
+
metadata: JSON.stringify(opts.sessionMetadata)
|
|
3297
|
+
}
|
|
3298
|
+
} : {
|
|
3299
|
+
sessionId,
|
|
3300
|
+
chatId,
|
|
3301
|
+
userId,
|
|
3302
|
+
title: opts?.sessionTitle ?? "",
|
|
3303
|
+
metadata: JSON.stringify(opts?.sessionMetadata ?? {}),
|
|
3304
|
+
createdAt: now,
|
|
3305
|
+
updatedAt: now
|
|
3306
|
+
};
|
|
3307
|
+
this.sessionMap.set(sessionId, this.deserializeSession(session));
|
|
3308
|
+
if (existing) await this.store.upsertSession(session);
|
|
3309
|
+
else await this.store.insertSession(session);
|
|
3310
|
+
const entry = this.sessionCache.getEntry(sessionId);
|
|
3311
|
+
const rawLimit = this.calculateWindowUsage(
|
|
3312
|
+
entry.lastModelContextTokens ?? this.config.defaultModelContextTokens
|
|
3313
|
+
).rawTokenLimit;
|
|
3314
|
+
if (entry.totalTokens >= Math.floor(rawLimit * this.config.precompressionRatio)) {
|
|
3315
|
+
void this.compressManager.triggerCompress(sessionId, false, false, rawLimit).catch((error) => {
|
|
3316
|
+
console.error(`[MemoryManager] Background compress failed for ${sessionId}:`, error);
|
|
2759
3317
|
});
|
|
2760
3318
|
}
|
|
2761
|
-
(async () => {
|
|
2762
|
-
const overLimit = this.sessionCache.addMessages(sessionId, stored, chatId, userId);
|
|
2763
|
-
if (overLimit) {
|
|
2764
|
-
await this.compressManager.triggerCompress(sessionId).catch((err) => {
|
|
2765
|
-
console.error("[MemoryManager] Background compress failed:", err);
|
|
2766
|
-
});
|
|
2767
|
-
}
|
|
2768
|
-
})();
|
|
2769
3319
|
}
|
|
2770
3320
|
async flushChat(sessionId, opts) {
|
|
2771
3321
|
const sid = sessionId ?? DEFAULT_SESSION_ID;
|
|
2772
|
-
|
|
3322
|
+
await this.waitForPendingWrites(sid);
|
|
3323
|
+
await this.ensureSessionHydrated(sid);
|
|
3324
|
+
const entry = this.sessionCache.getEntry(sid);
|
|
3325
|
+
const rawLimit = this.calculateWindowUsage(
|
|
3326
|
+
entry?.lastModelContextTokens ?? this.config.defaultModelContextTokens
|
|
3327
|
+
).rawTokenLimit;
|
|
3328
|
+
const promise = this.compressManager.triggerCompress(
|
|
3329
|
+
sid,
|
|
3330
|
+
true,
|
|
3331
|
+
opts?.waitGraph === true,
|
|
3332
|
+
rawLimit
|
|
3333
|
+
);
|
|
2773
3334
|
if (opts?.wait) await promise;
|
|
2774
3335
|
}
|
|
2775
|
-
async updateFacts(content, level, userId, chatId, sessionId) {
|
|
2776
|
-
await this.factCache.add(content, level, userId, chatId, sessionId);
|
|
3336
|
+
async updateFacts(content, level, userId, chatId, sessionId, key) {
|
|
3337
|
+
await this.factCache.add(content, level, userId, chatId, sessionId, key);
|
|
2777
3338
|
}
|
|
2778
3339
|
async updateEntity(entities, relations, context) {
|
|
2779
3340
|
if (entities.length === 0 && relations.length === 0) return;
|
|
@@ -2817,16 +3378,32 @@ var MemoryManager = class {
|
|
|
2817
3378
|
useGraph = await this.llm.judgeNeedsGraphSearch(query).catch(() => false);
|
|
2818
3379
|
}
|
|
2819
3380
|
const tasks = [
|
|
2820
|
-
this.store.hybridSearchTopics(query, vector, filter, limit).then(
|
|
2821
|
-
(topics)
|
|
3381
|
+
this.store.hybridSearchTopics(query, vector, filter, limit).then((topics) => {
|
|
3382
|
+
void this.store.incrementTopicRecallCounts(topics).catch(() => {
|
|
3383
|
+
});
|
|
3384
|
+
return topics.map((topic, index) => ({
|
|
2822
3385
|
type: "topic",
|
|
2823
|
-
content:
|
|
2824
|
-
score: 1,
|
|
3386
|
+
content: topic.summary,
|
|
3387
|
+
score: 1 / (index + 1),
|
|
3388
|
+
meta: {
|
|
3389
|
+
summaryId: topic.summaryId,
|
|
3390
|
+
sessionId: topic.sessionId,
|
|
3391
|
+
chatId: topic.chatId,
|
|
3392
|
+
userId: topic.userId
|
|
3393
|
+
}
|
|
3394
|
+
}));
|
|
3395
|
+
}).catch(() => []),
|
|
3396
|
+
this.store.hybridSearchMessages(query, vector, filter, limit).then(
|
|
3397
|
+
(messages) => messages.map((message, index) => ({
|
|
3398
|
+
type: "message",
|
|
3399
|
+
content: message.content,
|
|
3400
|
+
score: 1 / (index + 1),
|
|
2825
3401
|
meta: {
|
|
2826
|
-
|
|
2827
|
-
sessionId:
|
|
2828
|
-
chatId:
|
|
2829
|
-
userId:
|
|
3402
|
+
messageId: message.messageId,
|
|
3403
|
+
sessionId: message.sessionId,
|
|
3404
|
+
chatId: message.chatId,
|
|
3405
|
+
userId: message.userId,
|
|
3406
|
+
metadata: safeParseObject2(message.metadata)
|
|
2830
3407
|
}
|
|
2831
3408
|
}))
|
|
2832
3409
|
).catch(() => [])
|
|
@@ -2885,10 +3462,159 @@ var MemoryManager = class {
|
|
|
2885
3462
|
...this.factCache.get("chat", chatId)
|
|
2886
3463
|
].sort((a, b) => a.createdAt - b.createdAt);
|
|
2887
3464
|
if (merged.length === 0) return "";
|
|
2888
|
-
return merged.map(
|
|
3465
|
+
return merged.map(
|
|
3466
|
+
(f) => `${new Date(f.updatedAt ?? f.createdAt).toISOString()}\uFF1A${f.key ? `[${f.key}] ` : ""}${f.content}`
|
|
3467
|
+
).join("\n");
|
|
3468
|
+
}
|
|
3469
|
+
async getHistoryWindow(sessionId, modelContextTokens, options) {
|
|
3470
|
+
let modelTokens = modelContextTokens;
|
|
3471
|
+
if (modelTokens == null) {
|
|
3472
|
+
modelTokens = this.config.defaultModelContextTokens;
|
|
3473
|
+
if (!this.warnedDefaultModelContext) {
|
|
3474
|
+
this.warnedDefaultModelContext = true;
|
|
3475
|
+
console.warn(
|
|
3476
|
+
`[MemoryManager] getHistoryWindow \u672A\u4F20 modelContextTokens\uFF0C\u4F7F\u7528\u9ED8\u8BA4 ${modelTokens} tokens\u3002`
|
|
3477
|
+
);
|
|
3478
|
+
}
|
|
3479
|
+
}
|
|
3480
|
+
if (!Number.isFinite(modelTokens) || modelTokens <= 0) {
|
|
3481
|
+
throw new Error("modelContextTokens \u5FC5\u987B\u662F\u6B63\u6570");
|
|
3482
|
+
}
|
|
3483
|
+
modelTokens = Math.floor(modelTokens);
|
|
3484
|
+
await this.waitForPendingWrites(sessionId);
|
|
3485
|
+
await this.ensureSessionHydrated(sessionId);
|
|
3486
|
+
this.sessionCache.setModelContextTokens(sessionId, modelTokens);
|
|
3487
|
+
const usageLimits = this.calculateWindowUsage(modelTokens);
|
|
3488
|
+
let blockingReason;
|
|
3489
|
+
const beginBlocking = (reason) => {
|
|
3490
|
+
if (blockingReason) return;
|
|
3491
|
+
blockingReason = reason;
|
|
3492
|
+
this.notifyBlockingCompression(options, { sessionId, phase: "start", reason });
|
|
3493
|
+
};
|
|
3494
|
+
try {
|
|
3495
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
3496
|
+
let entry2 = this.sessionCache.getEntry(sessionId);
|
|
3497
|
+
if (entry2.totalTokens <= usageLimits.rawTokenLimit || entry2.messages.length === 0) break;
|
|
3498
|
+
if (this.compressManager.isBusy(sessionId)) {
|
|
3499
|
+
beginBlocking("pending");
|
|
3500
|
+
await this.compressManager.waitForIdle(sessionId);
|
|
3501
|
+
entry2 = this.sessionCache.getEntry(sessionId);
|
|
3502
|
+
if (entry2.totalTokens <= usageLimits.rawTokenLimit || entry2.messages.length === 0) break;
|
|
3503
|
+
}
|
|
3504
|
+
beginBlocking("raw");
|
|
3505
|
+
console.warn(
|
|
3506
|
+
`[MemoryManager] session ${sessionId} raw history (${entry2.totalTokens}) exceeds current model budget (${usageLimits.rawTokenLimit}); waiting for immediate compression.`
|
|
3507
|
+
);
|
|
3508
|
+
const beforeTokens = entry2.totalTokens;
|
|
3509
|
+
await this.compressManager.triggerCompress(
|
|
3510
|
+
sessionId,
|
|
3511
|
+
true,
|
|
3512
|
+
false,
|
|
3513
|
+
usageLimits.rawTokenLimit
|
|
3514
|
+
);
|
|
3515
|
+
const after = this.sessionCache.getEntry(sessionId);
|
|
3516
|
+
if (after.messages.length > 0 && after.totalTokens >= beforeTokens) {
|
|
3517
|
+
throw new Error(`Compression made no progress for session ${sessionId}`);
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
let entry = this.sessionCache.getEntry(sessionId);
|
|
3521
|
+
if (entry.totalTokens > usageLimits.rawTokenLimit) {
|
|
3522
|
+
throw new Error(`Unable to fit session ${sessionId} into the requested model context`);
|
|
3523
|
+
}
|
|
3524
|
+
let topicView = this.sessionCache.getTopicView(
|
|
3525
|
+
sessionId,
|
|
3526
|
+
usageLimits.compressedTokenLimit
|
|
3527
|
+
);
|
|
3528
|
+
if (!topicView && entry.topicTokens > usageLimits.compressedTokenLimit) {
|
|
3529
|
+
const affectsCurrentWindow = entry.topicTokens + entry.totalTokens > usageLimits.usableContextTokens;
|
|
3530
|
+
const materiallyOverLimit = entry.topicTokens > Math.floor(
|
|
3531
|
+
usageLimits.compressedTokenLimit * this.config.topicCompactionSyncRatio
|
|
3532
|
+
);
|
|
3533
|
+
if (affectsCurrentWindow || materiallyOverLimit) {
|
|
3534
|
+
if (this.compressManager.isBusy(sessionId)) beginBlocking("pending");
|
|
3535
|
+
else beginBlocking("topics");
|
|
3536
|
+
await this.compactTopicsForBudget(sessionId, usageLimits.compressedTokenLimit);
|
|
3537
|
+
entry = this.sessionCache.getEntry(sessionId);
|
|
3538
|
+
topicView = this.sessionCache.getTopicView(
|
|
3539
|
+
sessionId,
|
|
3540
|
+
usageLimits.compressedTokenLimit
|
|
3541
|
+
);
|
|
3542
|
+
} else {
|
|
3543
|
+
void this.compactTopicsForBudget(
|
|
3544
|
+
sessionId,
|
|
3545
|
+
usageLimits.compressedTokenLimit
|
|
3546
|
+
).catch((error) => {
|
|
3547
|
+
console.error(`[MemoryManager] Background Topic compaction failed for ${sessionId}:`, error);
|
|
3548
|
+
});
|
|
3549
|
+
}
|
|
3550
|
+
}
|
|
3551
|
+
const topics = topicView ?? entry.topics;
|
|
3552
|
+
const compressedTokens = sumTopicTokens2(topics);
|
|
3553
|
+
if (compressedTokens + entry.totalTokens > usageLimits.usableContextTokens) {
|
|
3554
|
+
throw new Error(`Unable to fit session ${sessionId} into the requested model context`);
|
|
3555
|
+
}
|
|
3556
|
+
return {
|
|
3557
|
+
sessionId,
|
|
3558
|
+
compressedContext: this.sessionCache.buildCompressedContext(sessionId, topics),
|
|
3559
|
+
recentMessages: entry.messages.map(toMemoryRawMessage),
|
|
3560
|
+
usage: {
|
|
3561
|
+
...usageLimits,
|
|
3562
|
+
compressedTokens,
|
|
3563
|
+
rawTokens: entry.totalTokens
|
|
3564
|
+
}
|
|
3565
|
+
};
|
|
3566
|
+
} finally {
|
|
3567
|
+
if (blockingReason) {
|
|
3568
|
+
this.notifyBlockingCompression(options, {
|
|
3569
|
+
sessionId,
|
|
3570
|
+
phase: "end",
|
|
3571
|
+
reason: blockingReason
|
|
3572
|
+
});
|
|
3573
|
+
}
|
|
3574
|
+
}
|
|
2889
3575
|
}
|
|
2890
|
-
|
|
2891
|
-
|
|
3576
|
+
compactTopicsForBudget(sessionId, effectiveLimit) {
|
|
3577
|
+
const key = `${sessionId}:${effectiveLimit}`;
|
|
3578
|
+
const existing = this.topicCompactions.get(key);
|
|
3579
|
+
if (existing) return existing;
|
|
3580
|
+
const task = (async () => {
|
|
3581
|
+
let entry = this.sessionCache.getEntry(sessionId);
|
|
3582
|
+
if (!entry) return;
|
|
3583
|
+
if (entry.topicTokens > this.config.compressedContextTokenLimit) {
|
|
3584
|
+
await this.compressManager.triggerTopicCompaction(
|
|
3585
|
+
sessionId,
|
|
3586
|
+
this.config.compressedContextTokenLimit,
|
|
3587
|
+
true
|
|
3588
|
+
);
|
|
3589
|
+
entry = this.sessionCache.getEntry(sessionId);
|
|
3590
|
+
}
|
|
3591
|
+
if (entry && entry.topicTokens > effectiveLimit && !this.sessionCache.getTopicView(sessionId, effectiveLimit)) {
|
|
3592
|
+
await this.compressManager.triggerTopicCompaction(sessionId, effectiveLimit, false);
|
|
3593
|
+
}
|
|
3594
|
+
})();
|
|
3595
|
+
this.topicCompactions.set(key, task);
|
|
3596
|
+
const clear = () => {
|
|
3597
|
+
if (this.topicCompactions.get(key) === task) this.topicCompactions.delete(key);
|
|
3598
|
+
};
|
|
3599
|
+
void task.then(clear, clear);
|
|
3600
|
+
return task;
|
|
3601
|
+
}
|
|
3602
|
+
notifyBlockingCompression(options, event) {
|
|
3603
|
+
const callback = options?.onBlockingCompression;
|
|
3604
|
+
if (!callback) return;
|
|
3605
|
+
try {
|
|
3606
|
+
const returned = callback(event);
|
|
3607
|
+
if (returned && typeof returned.then === "function") {
|
|
3608
|
+
void Promise.resolve(returned).catch((error) => {
|
|
3609
|
+
console.warn("[MemoryManager] onBlockingCompression callback rejected:", error);
|
|
3610
|
+
});
|
|
3611
|
+
}
|
|
3612
|
+
} catch (error) {
|
|
3613
|
+
console.warn("[MemoryManager] onBlockingCompression callback failed:", error);
|
|
3614
|
+
}
|
|
3615
|
+
}
|
|
3616
|
+
calculateWindowUsage(modelContextTokens) {
|
|
3617
|
+
return calculateContextBudget(this.config, modelContextTokens);
|
|
2892
3618
|
}
|
|
2893
3619
|
buildScopeFilter(scope, scopeId) {
|
|
2894
3620
|
if (scope === "session" && scopeId) return [eq("session_id", scopeId)];
|
|
@@ -2946,6 +3672,7 @@ var MemoryManager = class {
|
|
|
2946
3672
|
async deleteSession(sessionId) {
|
|
2947
3673
|
if (!this.sessionMap.has(sessionId)) return false;
|
|
2948
3674
|
this.sessionMap.delete(sessionId);
|
|
3675
|
+
this.sessionCache.delete(sessionId);
|
|
2949
3676
|
await this.store.deleteSession(sessionId);
|
|
2950
3677
|
await this.store.deleteMessagesBySession(sessionId).catch((err) => {
|
|
2951
3678
|
console.error(`[MemoryManager] Failed to delete messages for session ${sessionId}:`, err);
|
|
@@ -3020,8 +3747,8 @@ var MemoryManager = class {
|
|
|
3020
3747
|
return this.factCache.all().find((f) => f.chatId === chatId)?.userId;
|
|
3021
3748
|
}
|
|
3022
3749
|
/** 手动新增一条事实 */
|
|
3023
|
-
async addFact(content, level, userId, chatId, sessionId) {
|
|
3024
|
-
await this.factCache.add(content, level, userId, chatId, sessionId);
|
|
3750
|
+
async addFact(content, level, userId, chatId, sessionId, key) {
|
|
3751
|
+
await this.factCache.add(content, level, userId, chatId, sessionId, key);
|
|
3025
3752
|
}
|
|
3026
3753
|
/** 删除单条事实,返回是否命中 */
|
|
3027
3754
|
async deleteFact(factId) {
|
|
@@ -3060,20 +3787,81 @@ var MemoryManager = class {
|
|
|
3060
3787
|
return page ? this.knowledgeManager.listDocuments(filter, page) : this.knowledgeManager.listDocuments(filter);
|
|
3061
3788
|
}
|
|
3062
3789
|
destroy() {
|
|
3790
|
+
if (this.destroyTask) return this.destroyTask;
|
|
3063
3791
|
if (this.optimizeTimer) {
|
|
3064
3792
|
clearInterval(this.optimizeTimer);
|
|
3065
3793
|
this.optimizeTimer = void 0;
|
|
3066
3794
|
}
|
|
3067
|
-
this.
|
|
3068
|
-
|
|
3795
|
+
if (this.sessionSweepTimer) {
|
|
3796
|
+
clearInterval(this.sessionSweepTimer);
|
|
3797
|
+
this.sessionSweepTimer = void 0;
|
|
3798
|
+
}
|
|
3799
|
+
const writes = [
|
|
3800
|
+
...this.pendingWrites.values(),
|
|
3801
|
+
...this.hydration.values(),
|
|
3802
|
+
...this.topicCompactions.values()
|
|
3803
|
+
];
|
|
3804
|
+
this.destroyTask = Promise.all(writes).catch(() => {
|
|
3805
|
+
}).then(() => this.compressManager.waitForAllIdle()).catch(() => {
|
|
3806
|
+
}).then(() => this.optimizeTask).catch(() => {
|
|
3807
|
+
}).then(async () => {
|
|
3808
|
+
this.grafeo.close();
|
|
3809
|
+
await this.store.close().catch(() => {
|
|
3810
|
+
});
|
|
3069
3811
|
});
|
|
3812
|
+
return this.destroyTask;
|
|
3070
3813
|
}
|
|
3071
3814
|
};
|
|
3815
|
+
function toMemoryRawMessage(message) {
|
|
3816
|
+
const parts = safeParseArray(message.parts);
|
|
3817
|
+
const payload = message.payload === void 0 ? void 0 : safeParseValue(message.payload);
|
|
3818
|
+
return {
|
|
3819
|
+
messageId: message.messageId,
|
|
3820
|
+
talkerId: message.talkerId,
|
|
3821
|
+
chatId: message.chatId,
|
|
3822
|
+
userId: message.userId,
|
|
3823
|
+
sessionId: message.sessionId,
|
|
3824
|
+
type: message.type,
|
|
3825
|
+
content: message.content,
|
|
3826
|
+
...parts.length > 0 && { parts },
|
|
3827
|
+
...payload !== void 0 && { payload },
|
|
3828
|
+
usage: message.usage,
|
|
3829
|
+
metadata: safeParseObject2(message.metadata),
|
|
3830
|
+
createdAt: message.createdAt
|
|
3831
|
+
};
|
|
3832
|
+
}
|
|
3833
|
+
function sumTopicTokens2(topics) {
|
|
3834
|
+
return topics.reduce((sum, topic) => sum + Math.max(1, topic.tokens), 0);
|
|
3835
|
+
}
|
|
3836
|
+
function safeParseArray(value) {
|
|
3837
|
+
try {
|
|
3838
|
+
const parsed = JSON.parse(value);
|
|
3839
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
3840
|
+
} catch {
|
|
3841
|
+
return [];
|
|
3842
|
+
}
|
|
3843
|
+
}
|
|
3844
|
+
function safeParseObject2(value) {
|
|
3845
|
+
try {
|
|
3846
|
+
const parsed = JSON.parse(value);
|
|
3847
|
+
return parsed != null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
3848
|
+
} catch {
|
|
3849
|
+
return {};
|
|
3850
|
+
}
|
|
3851
|
+
}
|
|
3852
|
+
function safeParseValue(value) {
|
|
3853
|
+
try {
|
|
3854
|
+
return JSON.parse(value);
|
|
3855
|
+
} catch {
|
|
3856
|
+
return value;
|
|
3857
|
+
}
|
|
3858
|
+
}
|
|
3072
3859
|
export {
|
|
3073
3860
|
DEFAULT_NODE_TYPES,
|
|
3074
3861
|
DEFAULT_RELATION_TYPES,
|
|
3075
3862
|
KIND_CONVERSATION,
|
|
3076
3863
|
KIND_KNOWLEDGE,
|
|
3077
3864
|
MemoryManager,
|
|
3078
|
-
MemoryStore
|
|
3865
|
+
MemoryStore,
|
|
3866
|
+
calculateContextBudget
|
|
3079
3867
|
};
|