@ppagent/memory 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1187 -0
- package/dist/index.d.ts +122 -12
- package/dist/index.js +950 -357
- package/dist/{provider.resolver-ZLQ766IO.js → provider.resolver-2KS2YYNV.js} +32 -1
- package/llms.txt +866 -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,30 @@ 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
|
+
contextUsageRatio: ratio(config.contextUsageRatio ?? 0.75, "contextUsageRatio"),
|
|
42
|
+
precompressionRatio: ratio(config.precompressionRatio ?? 0.75, "precompressionRatio"),
|
|
43
|
+
compressionBatchRatio: ratio(config.compressionBatchRatio ?? 0.5, "compressionBatchRatio"),
|
|
44
|
+
compressionBatchTokenLimit: Math.max(0, Math.floor(config.compressionBatchTokenLimit ?? 0)),
|
|
45
|
+
topicSummaryMaxTokens: positiveInt(
|
|
46
|
+
config.topicSummaryMaxTokens ?? config.detailMaxTokens ?? 2048,
|
|
47
|
+
"topicSummaryMaxTokens"
|
|
48
|
+
),
|
|
49
|
+
defaultModelContextTokens: positiveInt(
|
|
50
|
+
config.defaultModelContextTokens ?? 256 * 1024,
|
|
51
|
+
"defaultModelContextTokens"
|
|
52
|
+
),
|
|
53
|
+
maxHistoryAgeMs: Math.max(0, Math.floor(config.maxHistoryAgeMs ?? 0)),
|
|
54
|
+
sessionIdleTtlMs: Math.floor(config.sessionIdleTtlMs ?? 30 * 6e4),
|
|
55
|
+
sessionSweepIntervalMs: positiveInt(
|
|
56
|
+
config.sessionSweepIntervalMs ?? 6e4,
|
|
57
|
+
"sessionSweepIntervalMs"
|
|
58
|
+
),
|
|
31
59
|
httpTimeoutMs: config.httpTimeoutMs ?? 6e4,
|
|
32
60
|
httpMaxRetries: config.httpMaxRetries ?? 2,
|
|
33
61
|
embeddingBatchSize: config.embeddingBatchSize ?? 20,
|
|
@@ -55,6 +83,18 @@ function resolveConfig(config) {
|
|
|
55
83
|
restoreConcurrency: config.restoreConcurrency ?? 8
|
|
56
84
|
};
|
|
57
85
|
}
|
|
86
|
+
function ratio(value, name) {
|
|
87
|
+
if (!Number.isFinite(value) || value <= 0 || value > 1) {
|
|
88
|
+
throw new Error(`[MemoryConfig] ${name} \u5FC5\u987B\u5728 (0, 1] \u8303\u56F4\u5185`);
|
|
89
|
+
}
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
function positiveInt(value, name) {
|
|
93
|
+
if (!Number.isFinite(value) || value <= 0) {
|
|
94
|
+
throw new Error(`[MemoryConfig] ${name} \u5FC5\u987B\u662F\u6B63\u6570`);
|
|
95
|
+
}
|
|
96
|
+
return Math.floor(value);
|
|
97
|
+
}
|
|
58
98
|
|
|
59
99
|
// src/constants.ts
|
|
60
100
|
var DEFAULT_NODE_TYPES = [
|
|
@@ -743,6 +783,7 @@ function tableDefs(dim) {
|
|
|
743
783
|
{ name: "content", type: "text" },
|
|
744
784
|
{ name: "parts", type: "text", nullable: true },
|
|
745
785
|
// 兼容存量数据
|
|
786
|
+
{ name: "payload", type: "text", nullable: true },
|
|
746
787
|
{ name: "vector", type: "vector" },
|
|
747
788
|
{ name: "usage", type: "int" },
|
|
748
789
|
{ name: "metadata", type: "text" },
|
|
@@ -765,9 +806,12 @@ function tableDefs(dim) {
|
|
|
765
806
|
{ name: "chat_id", type: "text" },
|
|
766
807
|
{ name: "title", type: "text", nullable: true },
|
|
767
808
|
// 兼容存量数据
|
|
768
|
-
{ name: "detail", type: "text" },
|
|
809
|
+
{ name: "detail", type: "text", nullable: true },
|
|
769
810
|
{ name: "summary", type: "text" },
|
|
770
|
-
{ name: "concise", type: "text" },
|
|
811
|
+
{ name: "concise", type: "text", nullable: true },
|
|
812
|
+
{ name: "tokens", type: "int", nullable: true },
|
|
813
|
+
{ name: "start_message_id", type: "text", nullable: true },
|
|
814
|
+
{ name: "end_message_id", type: "text", nullable: true },
|
|
771
815
|
{ name: "vector", type: "vector" },
|
|
772
816
|
{ name: "start_time", type: "long" },
|
|
773
817
|
{ name: "end_time", type: "long" },
|
|
@@ -777,7 +821,7 @@ function tableDefs(dim) {
|
|
|
777
821
|
],
|
|
778
822
|
indexes: [
|
|
779
823
|
{ column: "chat_id", kind: "scalar" },
|
|
780
|
-
{ column: "
|
|
824
|
+
{ column: "summary", kind: "fts" }
|
|
781
825
|
]
|
|
782
826
|
},
|
|
783
827
|
{
|
|
@@ -788,8 +832,10 @@ function tableDefs(dim) {
|
|
|
788
832
|
{ name: "chat_id", type: "text" },
|
|
789
833
|
{ name: "session_id", type: "text" },
|
|
790
834
|
{ name: "user_id", type: "text" },
|
|
835
|
+
{ name: "fact_key", type: "text", nullable: true },
|
|
791
836
|
{ name: "content", type: "text" },
|
|
792
|
-
{ name: "created_at", type: "long" }
|
|
837
|
+
{ name: "created_at", type: "long" },
|
|
838
|
+
{ name: "updated_at", type: "long", nullable: true }
|
|
793
839
|
],
|
|
794
840
|
indexes: []
|
|
795
841
|
},
|
|
@@ -868,6 +914,7 @@ function messageToRow(m) {
|
|
|
868
914
|
type: m.type,
|
|
869
915
|
content: m.content,
|
|
870
916
|
parts: m.parts ?? "[]",
|
|
917
|
+
payload: m.payload ?? null,
|
|
871
918
|
vector: m.vector,
|
|
872
919
|
usage: m.usage,
|
|
873
920
|
metadata: m.metadata,
|
|
@@ -885,8 +932,9 @@ function rowToMessage(r) {
|
|
|
885
932
|
content: r.content,
|
|
886
933
|
parts: r.parts ?? "[]",
|
|
887
934
|
// 旧数据无此列时安全降级
|
|
935
|
+
payload: r.payload ?? void 0,
|
|
888
936
|
vector: toVector(r.vector),
|
|
889
|
-
usage:
|
|
937
|
+
usage: positiveNumber(r.usage),
|
|
890
938
|
metadata: r.metadata,
|
|
891
939
|
createdAt: Number(r.created_at)
|
|
892
940
|
};
|
|
@@ -898,9 +946,13 @@ function topicToRow(t) {
|
|
|
898
946
|
user_id: t.userId,
|
|
899
947
|
chat_id: t.chatId,
|
|
900
948
|
title: t.title ?? "",
|
|
901
|
-
detail
|
|
949
|
+
// 旧库的 detail/concise 可能仍是 NOT NULL;兼容写入但业务只读取 summary。
|
|
950
|
+
detail: t.detail ?? t.summary,
|
|
902
951
|
summary: t.summary,
|
|
903
|
-
concise: t.concise,
|
|
952
|
+
concise: t.concise ?? t.summary,
|
|
953
|
+
tokens: t.tokens,
|
|
954
|
+
start_message_id: t.startMessageId ?? null,
|
|
955
|
+
end_message_id: t.endMessageId ?? null,
|
|
904
956
|
vector: t.vector,
|
|
905
957
|
start_time: t.startTime,
|
|
906
958
|
end_time: t.endTime,
|
|
@@ -916,9 +968,12 @@ function rowToTopic(r) {
|
|
|
916
968
|
userId: r.user_id,
|
|
917
969
|
chatId: r.chat_id,
|
|
918
970
|
title: r.title ?? "",
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
971
|
+
summary: r.summary || r.detail || r.concise || "",
|
|
972
|
+
tokens: positiveNumber(r.tokens),
|
|
973
|
+
startMessageId: r.start_message_id ?? void 0,
|
|
974
|
+
endMessageId: r.end_message_id ?? void 0,
|
|
975
|
+
detail: r.detail ?? void 0,
|
|
976
|
+
concise: r.concise ?? void 0,
|
|
922
977
|
vector: toVector(r.vector),
|
|
923
978
|
startTime: Number(r.start_time),
|
|
924
979
|
endTime: Number(r.end_time),
|
|
@@ -934,8 +989,10 @@ function factToRow(f) {
|
|
|
934
989
|
chat_id: f.chatId,
|
|
935
990
|
session_id: f.sessionId,
|
|
936
991
|
user_id: f.userId,
|
|
992
|
+
fact_key: f.key ?? null,
|
|
937
993
|
content: f.content,
|
|
938
|
-
created_at: f.createdAt
|
|
994
|
+
created_at: f.createdAt,
|
|
995
|
+
updated_at: f.updatedAt ?? f.createdAt
|
|
939
996
|
};
|
|
940
997
|
}
|
|
941
998
|
function rowToFact(r) {
|
|
@@ -945,8 +1002,10 @@ function rowToFact(r) {
|
|
|
945
1002
|
chatId: r.chat_id,
|
|
946
1003
|
sessionId: r.session_id,
|
|
947
1004
|
userId: r.user_id,
|
|
1005
|
+
key: r.fact_key ?? void 0,
|
|
948
1006
|
content: r.content,
|
|
949
|
-
createdAt: Number(r.created_at)
|
|
1007
|
+
createdAt: Number(r.created_at),
|
|
1008
|
+
updatedAt: Number(r.updated_at ?? r.created_at)
|
|
950
1009
|
};
|
|
951
1010
|
}
|
|
952
1011
|
function sessionToRow(s) {
|
|
@@ -1048,6 +1107,15 @@ function safeParseObject(s) {
|
|
|
1048
1107
|
return {};
|
|
1049
1108
|
}
|
|
1050
1109
|
}
|
|
1110
|
+
function positiveNumber(value) {
|
|
1111
|
+
const parsed = Number(value);
|
|
1112
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
|
|
1113
|
+
}
|
|
1114
|
+
function migrationValueEquals(current, next) {
|
|
1115
|
+
if (current == null && next == null) return true;
|
|
1116
|
+
if (typeof next === "number") return Number(current) === next;
|
|
1117
|
+
return String(current) === String(next);
|
|
1118
|
+
}
|
|
1051
1119
|
function rrfFuse(lists, keyOf) {
|
|
1052
1120
|
const acc = /* @__PURE__ */ new Map();
|
|
1053
1121
|
for (const list of lists) {
|
|
@@ -1078,7 +1146,7 @@ var MemoryStore = class {
|
|
|
1078
1146
|
if (this.providerOverride) {
|
|
1079
1147
|
this.provider = this.providerOverride;
|
|
1080
1148
|
} else {
|
|
1081
|
-
const { createProvider } = await import("./provider.resolver-
|
|
1149
|
+
const { createProvider } = await import("./provider.resolver-2KS2YYNV.js");
|
|
1082
1150
|
this.provider = await createProvider(this.config);
|
|
1083
1151
|
console.info(`[memory] \u5411\u91CF\u5B58\u50A8\u540E\u7AEF\uFF1A${this.provider.kind}`);
|
|
1084
1152
|
}
|
|
@@ -1099,6 +1167,98 @@ var MemoryStore = class {
|
|
|
1099
1167
|
if (messages.length === 0) return;
|
|
1100
1168
|
await this.provider.add(MESSAGES_TABLE, messages.map(messageToRow));
|
|
1101
1169
|
}
|
|
1170
|
+
/** 0.3.x → 0.4.x 数据回填;幂等,不调用 LLM、不删除原始消息。 */
|
|
1171
|
+
async migrateLegacyData() {
|
|
1172
|
+
const report = {
|
|
1173
|
+
provider: this.providerKind,
|
|
1174
|
+
topicsScanned: 0,
|
|
1175
|
+
topicsUpdated: 0,
|
|
1176
|
+
messagesScanned: 0,
|
|
1177
|
+
messagesUpdated: 0,
|
|
1178
|
+
factsScanned: 0,
|
|
1179
|
+
factsUpdated: 0
|
|
1180
|
+
};
|
|
1181
|
+
const topicRows = await this.provider.query(TOPICS_TABLE);
|
|
1182
|
+
report.topicsScanned = topicRows.length;
|
|
1183
|
+
const messagesBySession = /* @__PURE__ */ new Map();
|
|
1184
|
+
for (const row of topicRows) {
|
|
1185
|
+
const sessionId = String(row.session_id);
|
|
1186
|
+
let messages = messagesBySession.get(sessionId);
|
|
1187
|
+
if (!messages) {
|
|
1188
|
+
messages = await this.provider.query(MESSAGES_TABLE, {
|
|
1189
|
+
filter: [eq("session_id", sessionId)],
|
|
1190
|
+
orderBy: [
|
|
1191
|
+
{ column: "created_at", ascending: true },
|
|
1192
|
+
{ column: "message_id", ascending: true }
|
|
1193
|
+
]
|
|
1194
|
+
});
|
|
1195
|
+
messagesBySession.set(sessionId, messages);
|
|
1196
|
+
}
|
|
1197
|
+
const summary = String(row.summary || row.detail || row.concise || "");
|
|
1198
|
+
const startTime = Number(row.start_time);
|
|
1199
|
+
const endTime = Number(row.end_time);
|
|
1200
|
+
const covered = messages.filter(
|
|
1201
|
+
(message) => Number(message.created_at) >= startTime && Number(message.created_at) <= endTime
|
|
1202
|
+
);
|
|
1203
|
+
const values = {
|
|
1204
|
+
summary,
|
|
1205
|
+
tokens: positiveNumber(row.tokens) || Math.max(1, countTokens(summary)),
|
|
1206
|
+
start_message_id: row.start_message_id ?? covered.at(0)?.message_id ?? null,
|
|
1207
|
+
end_message_id: row.end_message_id ?? covered.at(-1)?.message_id ?? null,
|
|
1208
|
+
updated_at: Number(row.updated_at || Date.now())
|
|
1209
|
+
};
|
|
1210
|
+
const changed = Object.entries(values).some(
|
|
1211
|
+
([key, value]) => !migrationValueEquals(row[key], value)
|
|
1212
|
+
);
|
|
1213
|
+
if (changed) {
|
|
1214
|
+
await this.provider.update(TOPICS_TABLE, values, [eq("summary_id", String(row.summary_id))]);
|
|
1215
|
+
report.topicsUpdated++;
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
const messageRows = await this.provider.query(MESSAGES_TABLE);
|
|
1219
|
+
report.messagesScanned = messageRows.length;
|
|
1220
|
+
for (const row of messageRows) {
|
|
1221
|
+
const usage = countTokens(
|
|
1222
|
+
[row.content, row.parts ?? "[]", row.metadata ?? "{}", row.payload ?? ""].map(String).join("\n")
|
|
1223
|
+
);
|
|
1224
|
+
if (Number(row.usage) === usage) continue;
|
|
1225
|
+
await this.provider.update(MESSAGES_TABLE, { usage }, [
|
|
1226
|
+
eq("message_id", String(row.message_id))
|
|
1227
|
+
]);
|
|
1228
|
+
report.messagesUpdated++;
|
|
1229
|
+
}
|
|
1230
|
+
const factRows = await this.provider.query(FACTS_TABLE);
|
|
1231
|
+
report.factsScanned = factRows.length;
|
|
1232
|
+
for (const row of factRows) {
|
|
1233
|
+
const updatedAt = Number(row.updated_at || row.created_at);
|
|
1234
|
+
if (Number(row.updated_at) === updatedAt) continue;
|
|
1235
|
+
await this.provider.update(FACTS_TABLE, { updated_at: updatedAt }, [
|
|
1236
|
+
eq("fact_id", String(row.fact_id))
|
|
1237
|
+
]);
|
|
1238
|
+
report.factsUpdated++;
|
|
1239
|
+
}
|
|
1240
|
+
return report;
|
|
1241
|
+
}
|
|
1242
|
+
/** message_id 稳定时更新原行,否则新增;用于流式 assistant 消息最终态覆盖。 */
|
|
1243
|
+
async upsertMessages(messages) {
|
|
1244
|
+
if (messages.length === 0) return;
|
|
1245
|
+
const ids = messages.map((message) => message.messageId);
|
|
1246
|
+
const existing = await this.provider.query(MESSAGES_TABLE, {
|
|
1247
|
+
filter: [{ op: "in", field: "message_id", values: ids }],
|
|
1248
|
+
select: ["message_id"]
|
|
1249
|
+
});
|
|
1250
|
+
const existingIds = new Set(existing.map((row) => String(row.message_id)));
|
|
1251
|
+
await Promise.all(
|
|
1252
|
+
messages.filter((message) => existingIds.has(message.messageId)).map(
|
|
1253
|
+
(message) => this.provider.update(
|
|
1254
|
+
MESSAGES_TABLE,
|
|
1255
|
+
messageToRow(message),
|
|
1256
|
+
[eq("message_id", message.messageId)]
|
|
1257
|
+
)
|
|
1258
|
+
)
|
|
1259
|
+
);
|
|
1260
|
+
await this.addMessages(messages.filter((message) => !existingIds.has(message.messageId)));
|
|
1261
|
+
}
|
|
1102
1262
|
async getMessagesSince(sessionId, since, limit) {
|
|
1103
1263
|
try {
|
|
1104
1264
|
const rows = await this.provider.query(MESSAGES_TABLE, {
|
|
@@ -1110,6 +1270,12 @@ var MemoryStore = class {
|
|
|
1110
1270
|
return [];
|
|
1111
1271
|
}
|
|
1112
1272
|
}
|
|
1273
|
+
async getMessagesAfterBoundary(sessionId, endTime, endMessageId) {
|
|
1274
|
+
const messages = await this.getAllMessagesBySession(sessionId);
|
|
1275
|
+
return messages.filter(
|
|
1276
|
+
(message) => message.createdAt > endTime || message.createdAt === endTime && endMessageId != null && message.messageId.localeCompare(endMessageId) > 0
|
|
1277
|
+
);
|
|
1278
|
+
}
|
|
1113
1279
|
async getLatestMessages(sessionId, limit) {
|
|
1114
1280
|
if (limit <= 0) return [];
|
|
1115
1281
|
try {
|
|
@@ -1157,7 +1323,7 @@ var MemoryStore = class {
|
|
|
1157
1323
|
}
|
|
1158
1324
|
async hybridSearchTopics(query, vector, filter, limit = 10) {
|
|
1159
1325
|
try {
|
|
1160
|
-
const fused = (await this.hybridSearch(TOPICS_TABLE, ["
|
|
1326
|
+
const fused = (await this.hybridSearch(TOPICS_TABLE, ["summary"], query, vector, filter, limit)).slice(0, limit);
|
|
1161
1327
|
if (fused.length > 0) {
|
|
1162
1328
|
const total = fused.length;
|
|
1163
1329
|
return fused.map((r, idx) => ({
|
|
@@ -1165,25 +1331,10 @@ var MemoryStore = class {
|
|
|
1165
1331
|
score: total - idx + Math.log1p(Number(r.recall_count))
|
|
1166
1332
|
})).sort((a, b) => b.score - a.score).map((x) => rowToTopic(x.row));
|
|
1167
1333
|
}
|
|
1334
|
+
return [];
|
|
1168
1335
|
} catch {
|
|
1336
|
+
return [];
|
|
1169
1337
|
}
|
|
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
1338
|
}
|
|
1188
1339
|
/**
|
|
1189
1340
|
* 通用混合检索:FTS(BM25)与向量两路各取 limit*HYBRID_OVERFETCH 候选,RRF 融合。
|
|
@@ -1221,6 +1372,13 @@ var MemoryStore = class {
|
|
|
1221
1372
|
async addTopic(topic) {
|
|
1222
1373
|
await this.provider.add(TOPICS_TABLE, [topicToRow(topic)]);
|
|
1223
1374
|
}
|
|
1375
|
+
async updateTopic(topic) {
|
|
1376
|
+
await this.provider.update(
|
|
1377
|
+
TOPICS_TABLE,
|
|
1378
|
+
topicToRow(topic),
|
|
1379
|
+
[eq("summary_id", topic.summaryId)]
|
|
1380
|
+
);
|
|
1381
|
+
}
|
|
1224
1382
|
async updateTopicRecallCount(summaryId, count) {
|
|
1225
1383
|
await this.provider.update(
|
|
1226
1384
|
TOPICS_TABLE,
|
|
@@ -1228,6 +1386,23 @@ var MemoryStore = class {
|
|
|
1228
1386
|
[eq("summary_id", summaryId)]
|
|
1229
1387
|
);
|
|
1230
1388
|
}
|
|
1389
|
+
async incrementTopicRecallCounts(topics) {
|
|
1390
|
+
await Promise.all(
|
|
1391
|
+
topics.map((topic) => this.updateTopicRecallCount(topic.summaryId, topic.recallCount + 1))
|
|
1392
|
+
);
|
|
1393
|
+
}
|
|
1394
|
+
async getTopicsBySession(sessionId, since = 0) {
|
|
1395
|
+
try {
|
|
1396
|
+
const filter = [eq("session_id", sessionId)];
|
|
1397
|
+
if (since > 0) filter.push({ op: "gte", field: "end_time", value: since });
|
|
1398
|
+
const rows = await this.provider.query(TOPICS_TABLE, { filter });
|
|
1399
|
+
return rows.sort(
|
|
1400
|
+
(a, b) => Number(a.end_time) - Number(b.end_time) || cmpStr(a.summary_id, b.summary_id)
|
|
1401
|
+
).map(rowToTopic);
|
|
1402
|
+
} catch {
|
|
1403
|
+
return [];
|
|
1404
|
+
}
|
|
1405
|
+
}
|
|
1231
1406
|
async getRecentTopics(chatId, userId, n1, n2, n3) {
|
|
1232
1407
|
const filter = [eq("chat_id", chatId), eq("user_id", userId)];
|
|
1233
1408
|
const recallBoostMs = this.config.recallBoostMs;
|
|
@@ -1268,10 +1443,19 @@ var MemoryStore = class {
|
|
|
1268
1443
|
async deleteTopicsBySession(sessionId) {
|
|
1269
1444
|
await this.provider.deleteWhere(TOPICS_TABLE, [eq("session_id", sessionId)]);
|
|
1270
1445
|
}
|
|
1446
|
+
async deleteTopicsByIds(summaryIds) {
|
|
1447
|
+
if (summaryIds.length === 0) return;
|
|
1448
|
+
await this.provider.deleteWhere(TOPICS_TABLE, [
|
|
1449
|
+
{ op: "in", field: "summary_id", values: summaryIds }
|
|
1450
|
+
]);
|
|
1451
|
+
}
|
|
1271
1452
|
// ── facts ──────────────────────────────────────────────────────────────────
|
|
1272
1453
|
async saveFact(fact) {
|
|
1273
1454
|
await this.provider.add(FACTS_TABLE, [factToRow(fact)]);
|
|
1274
1455
|
}
|
|
1456
|
+
async updateFact(fact) {
|
|
1457
|
+
await this.provider.update(FACTS_TABLE, factToRow(fact), [eq("fact_id", fact.factId)]);
|
|
1458
|
+
}
|
|
1275
1459
|
async getAllFacts() {
|
|
1276
1460
|
try {
|
|
1277
1461
|
const rows = await this.provider.query(FACTS_TABLE);
|
|
@@ -1511,40 +1695,6 @@ var MemoryStore = class {
|
|
|
1511
1695
|
}
|
|
1512
1696
|
};
|
|
1513
1697
|
|
|
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
1698
|
// src/llm/llm.service.ts
|
|
1549
1699
|
var ENTITY_EXTRACTOR_TOOL = {
|
|
1550
1700
|
type: "function",
|
|
@@ -1617,7 +1767,10 @@ var LlmService = class {
|
|
|
1617
1767
|
* 仅在提示词明确要求 JSON 时启用;返回自然语言的调用(如 summarizeSearchResults)
|
|
1618
1768
|
* 必须传 false,否则部分端点会因 "messages 未含 json 字样" 而 400。
|
|
1619
1769
|
*/
|
|
1620
|
-
async chatCompletion(systemPrompt, userPrompt, jsonMode = true) {
|
|
1770
|
+
async chatCompletion(systemPrompt, userPrompt, jsonMode = true, maxTokens) {
|
|
1771
|
+
return (await this.chatCompletionWithUsage(systemPrompt, userPrompt, jsonMode, maxTokens)).content;
|
|
1772
|
+
}
|
|
1773
|
+
async chatCompletionWithUsage(systemPrompt, userPrompt, jsonMode = true, maxTokens) {
|
|
1621
1774
|
const url = `${this.config.llmBaseUrl.replace(/\/$/, "")}/chat/completions`;
|
|
1622
1775
|
const body = {
|
|
1623
1776
|
model: this.config.llmModel,
|
|
@@ -1628,6 +1781,7 @@ var LlmService = class {
|
|
|
1628
1781
|
]
|
|
1629
1782
|
};
|
|
1630
1783
|
if (jsonMode) body.response_format = { type: "json_object" };
|
|
1784
|
+
if (maxTokens != null) body.max_tokens = maxTokens;
|
|
1631
1785
|
const data = await postJsonWithRetry(
|
|
1632
1786
|
url,
|
|
1633
1787
|
{ Authorization: `Bearer ${this.config.llmApiKey}` },
|
|
@@ -1635,7 +1789,10 @@ var LlmService = class {
|
|
|
1635
1789
|
{ timeoutMs: this.config.httpTimeoutMs, maxRetries: this.config.httpMaxRetries },
|
|
1636
1790
|
"LLM API"
|
|
1637
1791
|
);
|
|
1638
|
-
return
|
|
1792
|
+
return {
|
|
1793
|
+
content: data.choices[0].message.content ?? "",
|
|
1794
|
+
completionTokens: data.usage?.completion_tokens
|
|
1795
|
+
};
|
|
1639
1796
|
}
|
|
1640
1797
|
// ── 带工具调用的请求 ──────────────────────────────────────────────────────────
|
|
1641
1798
|
async chatWithTools(systemPrompt, userPrompt) {
|
|
@@ -1658,27 +1815,55 @@ var LlmService = class {
|
|
|
1658
1815
|
);
|
|
1659
1816
|
return data.choices[0].message;
|
|
1660
1817
|
}
|
|
1661
|
-
// ──
|
|
1818
|
+
// ── 对话 Topic 摘要(JSON 输出)──────────────────────────────────────────────
|
|
1662
1819
|
async summarizeMessages(messages) {
|
|
1663
1820
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1664
|
-
const {
|
|
1821
|
+
const { topicSummaryMaxTokens } = this.config;
|
|
1665
1822
|
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\
|
|
1823
|
+
\u8BF7\u628A\u7ED9\u5B9A\u5BF9\u8BDD\u538B\u7F29\u6210\u4E00\u4E2A\u53EF\u957F\u671F\u590D\u7528\u7684\u4E3B\u9898\u8BB0\u5FC6\u3002
|
|
1667
1824
|
|
|
1668
1825
|
\u8981\u6C42\uFF1A
|
|
1669
1826
|
- 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
|
-
-
|
|
1827
|
+
- summary\uFF1A\u552F\u4E00\u7684\u6458\u8981\u6B63\u6587\uFF0C\u6700\u591A ${topicSummaryMaxTokens} tokens
|
|
1828
|
+
- \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
|
|
1829
|
+
- \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
|
|
1830
|
+
- 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
1831
|
|
|
1674
1832
|
\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
|
|
1833
|
+
{"title": "...", "summary": "..."}`;
|
|
1834
|
+
const formatted = messages.map((m) => formatStoredMessage(m)).join("\n");
|
|
1835
|
+
const response = await this.chatCompletionWithUsage(
|
|
1836
|
+
systemPrompt,
|
|
1837
|
+
`\u8BF7\u538B\u7F29\u4EE5\u4E0B\u5BF9\u8BDD\uFF1A
|
|
1678
1838
|
|
|
1679
|
-
${formatted}
|
|
1680
|
-
|
|
1681
|
-
|
|
1839
|
+
${formatted}`,
|
|
1840
|
+
true,
|
|
1841
|
+
topicSummaryMaxTokens + 128
|
|
1842
|
+
);
|
|
1843
|
+
const parsed = JSON.parse(response.content);
|
|
1844
|
+
return {
|
|
1845
|
+
title: parsed.title ?? "",
|
|
1846
|
+
summary: parsed.summary ?? "",
|
|
1847
|
+
tokens: response.completionTokens
|
|
1848
|
+
};
|
|
1849
|
+
}
|
|
1850
|
+
async summarizeTopics(topics) {
|
|
1851
|
+
const input = topics.map(
|
|
1852
|
+
(topic) => `[${new Date(topic.startTime).toISOString()} - ${new Date(topic.endTime).toISOString()}] ${topic.title}
|
|
1853
|
+
${topic.summary}`
|
|
1854
|
+
).join("\n\n");
|
|
1855
|
+
const response = await this.chatCompletionWithUsage(
|
|
1856
|
+
`\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 ${this.config.topicSummaryMaxTokens} tokens\u3002\u53EA\u8F93\u51FA JSON\uFF1A{"title":"...","summary":"..."}`,
|
|
1857
|
+
input,
|
|
1858
|
+
true,
|
|
1859
|
+
this.config.topicSummaryMaxTokens + 128
|
|
1860
|
+
);
|
|
1861
|
+
const parsed = JSON.parse(response.content);
|
|
1862
|
+
return {
|
|
1863
|
+
title: parsed.title ?? "\u5386\u53F2\u4E3B\u9898\u5F52\u5E76",
|
|
1864
|
+
summary: parsed.summary ?? "",
|
|
1865
|
+
tokens: response.completionTokens
|
|
1866
|
+
};
|
|
1682
1867
|
}
|
|
1683
1868
|
// ── 第二步:通过工具调用提取实体和关系 ──────────────────────────────────────
|
|
1684
1869
|
async extractEntitiesFromMessages(messages) {
|
|
@@ -1721,9 +1906,8 @@ ${formatted}`
|
|
|
1721
1906
|
]);
|
|
1722
1907
|
return {
|
|
1723
1908
|
title: summary.title,
|
|
1724
|
-
detail: summary.detail,
|
|
1725
1909
|
summary: summary.summary,
|
|
1726
|
-
|
|
1910
|
+
tokens: summary.tokens,
|
|
1727
1911
|
entities: extraction.entities,
|
|
1728
1912
|
relations: extraction.relations
|
|
1729
1913
|
};
|
|
@@ -1812,18 +1996,51 @@ ${text}`
|
|
|
1812
1996
|
${context}`, false);
|
|
1813
1997
|
}
|
|
1814
1998
|
};
|
|
1999
|
+
function formatStoredMessage(message) {
|
|
2000
|
+
const extras = [message.parts, message.metadata, message.payload].filter((value) => value && value !== "[]" && value !== "{}").join(" ");
|
|
2001
|
+
return `[${new Date(message.createdAt).toISOString()}] ${message.talkerId || "user"}: ${message.content}${extras ? `
|
|
2002
|
+
meta=${extras}` : ""}`;
|
|
2003
|
+
}
|
|
1815
2004
|
|
|
1816
2005
|
// src/manager/compress.manager.ts
|
|
1817
2006
|
import { v4 as uuidv4 } from "uuid";
|
|
2007
|
+
|
|
2008
|
+
// src/manager/semaphore.ts
|
|
2009
|
+
var Semaphore = class {
|
|
2010
|
+
count;
|
|
2011
|
+
queue = [];
|
|
2012
|
+
constructor(max) {
|
|
2013
|
+
this.count = max;
|
|
2014
|
+
}
|
|
2015
|
+
async run(fn) {
|
|
2016
|
+
await this.acquire();
|
|
2017
|
+
try {
|
|
2018
|
+
return await fn();
|
|
2019
|
+
} finally {
|
|
2020
|
+
this.release();
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
acquire() {
|
|
2024
|
+
if (this.count > 0) {
|
|
2025
|
+
this.count--;
|
|
2026
|
+
return Promise.resolve();
|
|
2027
|
+
}
|
|
2028
|
+
return new Promise((resolve) => {
|
|
2029
|
+
this.queue.push(resolve);
|
|
2030
|
+
});
|
|
2031
|
+
}
|
|
2032
|
+
release() {
|
|
2033
|
+
const next = this.queue.shift();
|
|
2034
|
+
if (next) {
|
|
2035
|
+
next();
|
|
2036
|
+
} else {
|
|
2037
|
+
this.count++;
|
|
2038
|
+
}
|
|
2039
|
+
}
|
|
2040
|
+
};
|
|
2041
|
+
|
|
2042
|
+
// src/manager/compress.manager.ts
|
|
1818
2043
|
var CompressManager = class {
|
|
1819
|
-
config;
|
|
1820
|
-
store;
|
|
1821
|
-
grafeo;
|
|
1822
|
-
llm;
|
|
1823
|
-
embed;
|
|
1824
|
-
sessionCache;
|
|
1825
|
-
semaphore;
|
|
1826
|
-
sessionChain = /* @__PURE__ */ new Map();
|
|
1827
2044
|
constructor(config, store, grafeo, llm, embed, sessionCache) {
|
|
1828
2045
|
this.config = config;
|
|
1829
2046
|
this.store = store;
|
|
@@ -1833,125 +2050,189 @@ var CompressManager = class {
|
|
|
1833
2050
|
this.sessionCache = sessionCache;
|
|
1834
2051
|
this.semaphore = new Semaphore(config.maxConcurrentCompressions);
|
|
1835
2052
|
}
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1839
|
-
|
|
2053
|
+
semaphore;
|
|
2054
|
+
sessionChain = /* @__PURE__ */ new Map();
|
|
2055
|
+
backgroundGraphs = /* @__PURE__ */ new Set();
|
|
2056
|
+
triggerCompress(sessionId, force = false, waitGraph = false, rawLimit) {
|
|
2057
|
+
const previous = this.sessionChain.get(sessionId) ?? Promise.resolve();
|
|
2058
|
+
const next = previous.then(
|
|
2059
|
+
() => this.semaphore.run(() => this.doCompress(sessionId, force, waitGraph, rawLimit))
|
|
1840
2060
|
);
|
|
1841
|
-
|
|
1842
|
-
})
|
|
2061
|
+
const settled = next.catch(() => {
|
|
2062
|
+
});
|
|
2063
|
+
this.sessionChain.set(sessionId, settled);
|
|
2064
|
+
settled.finally(() => {
|
|
2065
|
+
if (this.sessionChain.get(sessionId) === settled) this.sessionChain.delete(sessionId);
|
|
2066
|
+
});
|
|
1843
2067
|
return next;
|
|
1844
2068
|
}
|
|
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;
|
|
2069
|
+
async waitForIdle(sessionId) {
|
|
2070
|
+
await (this.sessionChain.get(sessionId) ?? Promise.resolve());
|
|
2071
|
+
}
|
|
2072
|
+
isBusy(sessionId) {
|
|
2073
|
+
return this.sessionChain.has(sessionId);
|
|
2074
|
+
}
|
|
2075
|
+
async waitForAllIdle() {
|
|
2076
|
+
while (this.sessionChain.size > 0 || this.backgroundGraphs.size > 0) {
|
|
2077
|
+
await Promise.all([
|
|
2078
|
+
...this.sessionChain.values(),
|
|
2079
|
+
...this.backgroundGraphs
|
|
2080
|
+
]);
|
|
1860
2081
|
}
|
|
1861
|
-
|
|
1862
|
-
|
|
1863
|
-
|
|
2082
|
+
}
|
|
2083
|
+
async doCompress(sessionId, force, waitGraph, rawLimitOverride) {
|
|
2084
|
+
const entry = this.sessionCache.getEntry(sessionId);
|
|
2085
|
+
if (!entry || entry.messages.length === 0) return;
|
|
2086
|
+
const rawLimit = Math.max(1, rawLimitOverride ?? this.rawLimit(entry.lastModelContextTokens));
|
|
2087
|
+
const threshold = Math.max(1, Math.floor(rawLimit * this.config.precompressionRatio));
|
|
2088
|
+
if (!force && entry.totalTokens < threshold) return;
|
|
2089
|
+
let batchTarget = Math.max(1, Math.floor(rawLimit * this.config.compressionBatchRatio));
|
|
2090
|
+
if (this.config.compressionBatchTokenLimit > 0) {
|
|
2091
|
+
batchTarget = Math.min(batchTarget, this.config.compressionBatchTokenLimit);
|
|
2092
|
+
}
|
|
2093
|
+
const messages = this.sessionCache.selectOldestMessageBatch(sessionId, batchTarget);
|
|
2094
|
+
if (messages.length === 0) return;
|
|
2095
|
+
let result;
|
|
1864
2096
|
try {
|
|
1865
|
-
|
|
1866
|
-
} catch (
|
|
1867
|
-
console.error(
|
|
2097
|
+
result = await this.llm.summarizeMessages(messages);
|
|
2098
|
+
} catch (error) {
|
|
2099
|
+
console.error(
|
|
2100
|
+
`[CompressManager] LLM compress failed for session ${sessionId}: ${formatError(error)}`
|
|
2101
|
+
);
|
|
2102
|
+
throw error;
|
|
1868
2103
|
}
|
|
2104
|
+
const summary = result.summary.trim();
|
|
2105
|
+
if (!summary) throw new Error(`LLM returned an empty summary for session ${sessionId}`);
|
|
2106
|
+
const vector = await this.embed.embedOne(summary).catch((error) => {
|
|
2107
|
+
console.error(`[CompressManager] Embed topic failed for session ${sessionId}:`, error);
|
|
2108
|
+
return [];
|
|
2109
|
+
});
|
|
2110
|
+
const first = messages[0];
|
|
2111
|
+
const last = messages[messages.length - 1];
|
|
2112
|
+
const now = Date.now();
|
|
1869
2113
|
const topic = {
|
|
1870
2114
|
summaryId: uuidv4(),
|
|
1871
2115
|
sessionId,
|
|
1872
|
-
userId,
|
|
1873
|
-
chatId,
|
|
1874
|
-
title,
|
|
1875
|
-
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
2116
|
+
userId: entry.ids.userId,
|
|
2117
|
+
chatId: entry.ids.chatId,
|
|
2118
|
+
title: result.title,
|
|
2119
|
+
summary,
|
|
2120
|
+
tokens: result.tokens != null && result.tokens > 0 ? result.tokens : Math.max(1, countTokens(summary)),
|
|
2121
|
+
startMessageId: first.messageId,
|
|
2122
|
+
endMessageId: last.messageId,
|
|
2123
|
+
startTime: first.createdAt,
|
|
2124
|
+
endTime: last.createdAt,
|
|
2125
|
+
createdAt: now,
|
|
2126
|
+
updatedAt: now,
|
|
1882
2127
|
recallCount: 0,
|
|
1883
|
-
vector
|
|
2128
|
+
vector
|
|
1884
2129
|
};
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
console.error(`[CompressManager] Rebuild history window failed:`, err);
|
|
1897
|
-
}
|
|
1898
|
-
const graphTask = this.extractAndPersistGraph(messages, sessionId, chatId, userId, endTime);
|
|
2130
|
+
await this.store.addTopic(topic);
|
|
2131
|
+
this.sessionCache.appendTopic(sessionId, topic);
|
|
2132
|
+
this.sessionCache.removeMessages(sessionId, messages.map((message) => message.messageId));
|
|
2133
|
+
await this.compactOldTopics(sessionId);
|
|
2134
|
+
const graphTask = this.extractAndPersistGraph(
|
|
2135
|
+
messages,
|
|
2136
|
+
sessionId,
|
|
2137
|
+
entry.ids.chatId,
|
|
2138
|
+
entry.ids.userId,
|
|
2139
|
+
last.createdAt
|
|
2140
|
+
);
|
|
1899
2141
|
if (waitGraph) {
|
|
1900
2142
|
await withTimeout(graphTask, this.config.graphBuildTimeoutMs, "flushChat graph build");
|
|
1901
2143
|
} else {
|
|
1902
|
-
graphTask.catch((
|
|
1903
|
-
console.error(`[CompressManager] Background graph persist failed:`,
|
|
2144
|
+
const tracked = graphTask.catch((error) => {
|
|
2145
|
+
console.error(`[CompressManager] Background graph persist failed:`, error);
|
|
1904
2146
|
});
|
|
2147
|
+
this.backgroundGraphs.add(tracked);
|
|
2148
|
+
tracked.finally(() => this.backgroundGraphs.delete(tracked));
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
async compactOldTopics(sessionId) {
|
|
2152
|
+
while (true) {
|
|
2153
|
+
const entry = this.sessionCache.getEntry(sessionId);
|
|
2154
|
+
if (!entry || entry.topicTokens <= this.config.compressedContextTokenLimit) return;
|
|
2155
|
+
const target = Math.max(1, Math.floor(this.config.compressedContextTokenLimit / 2));
|
|
2156
|
+
const selected = [];
|
|
2157
|
+
let tokens = 0;
|
|
2158
|
+
for (const topic of entry.topics) {
|
|
2159
|
+
selected.push(topic);
|
|
2160
|
+
tokens += topic.tokens;
|
|
2161
|
+
if (tokens >= target && selected.length >= 2) break;
|
|
2162
|
+
}
|
|
2163
|
+
if (selected.length < 2) {
|
|
2164
|
+
this.sessionCache.setTopics(sessionId, entry.topics, true);
|
|
2165
|
+
return;
|
|
2166
|
+
}
|
|
2167
|
+
console.info(
|
|
2168
|
+
`[CompressManager] session ${sessionId} compressed Topic context exceeds ${this.config.compressedContextTokenLimit} tokens; compacting ${selected.length} old topics.`
|
|
2169
|
+
);
|
|
2170
|
+
const result = await this.llm.summarizeTopics(selected);
|
|
2171
|
+
const summary = result.summary.trim();
|
|
2172
|
+
if (!summary) throw new Error(`LLM returned an empty Topic rollup for session ${sessionId}`);
|
|
2173
|
+
const vector = await this.embed.embedOne(summary).catch(() => []);
|
|
2174
|
+
const first = selected[0];
|
|
2175
|
+
const last = selected[selected.length - 1];
|
|
2176
|
+
const now = Date.now();
|
|
2177
|
+
const rollup = {
|
|
2178
|
+
summaryId: uuidv4(),
|
|
2179
|
+
sessionId,
|
|
2180
|
+
userId: entry.ids.userId,
|
|
2181
|
+
chatId: entry.ids.chatId,
|
|
2182
|
+
title: result.title,
|
|
2183
|
+
summary,
|
|
2184
|
+
tokens: result.tokens != null && result.tokens > 0 ? result.tokens : Math.max(1, countTokens(summary)),
|
|
2185
|
+
startMessageId: first.startMessageId,
|
|
2186
|
+
endMessageId: last.endMessageId,
|
|
2187
|
+
startTime: first.startTime,
|
|
2188
|
+
endTime: last.endTime,
|
|
2189
|
+
createdAt: now,
|
|
2190
|
+
updatedAt: now,
|
|
2191
|
+
recallCount: 0,
|
|
2192
|
+
vector
|
|
2193
|
+
};
|
|
2194
|
+
const removedIds = selected.map((topic) => topic.summaryId);
|
|
2195
|
+
await this.store.addTopic(rollup);
|
|
2196
|
+
this.sessionCache.replaceTopics(sessionId, removedIds, rollup);
|
|
2197
|
+
await this.store.deleteTopicsByIds(removedIds);
|
|
1905
2198
|
}
|
|
1906
2199
|
}
|
|
2200
|
+
rawLimit(modelContextTokens = this.config.defaultModelContextTokens) {
|
|
2201
|
+
const usable = Math.floor(modelContextTokens * this.config.contextUsageRatio);
|
|
2202
|
+
return Math.max(1, usable - this.config.compressedContextTokenLimit);
|
|
2203
|
+
}
|
|
1907
2204
|
async extractAndPersistGraph(messages, sessionId, chatId, userId, endTime) {
|
|
1908
2205
|
const { entities, relations } = await this.llm.extractEntitiesFromMessages(messages);
|
|
1909
2206
|
if (entities.length === 0 && relations.length === 0) return;
|
|
1910
2207
|
await this.persistGraph(entities, relations, sessionId, chatId, userId, endTime);
|
|
1911
2208
|
}
|
|
1912
2209
|
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
|
-
}
|
|
2210
|
+
const entities = rawEntities.map((entity) => ({
|
|
2211
|
+
name: entity.name,
|
|
2212
|
+
type: entity.type,
|
|
2213
|
+
meta: { sessionId, chatId, userId, messageTime, ...entity.meta }
|
|
1923
2214
|
}));
|
|
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
|
-
}
|
|
2215
|
+
const relations = rawRelations.map((relation) => ({
|
|
2216
|
+
from: relation.from,
|
|
2217
|
+
to: relation.to,
|
|
2218
|
+
type: relation.type,
|
|
2219
|
+
happenedAt: relation.happenedAt,
|
|
2220
|
+
meta: { sessionId, chatId, userId, messageTime, ...relation.meta }
|
|
1936
2221
|
}));
|
|
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
|
-
}
|
|
2222
|
+
const names = [
|
|
2223
|
+
.../* @__PURE__ */ new Set([
|
|
2224
|
+
...entities.map((entity) => entity.name),
|
|
2225
|
+
...relations.flatMap((relation) => [relation.from, relation.to])
|
|
2226
|
+
])
|
|
2227
|
+
];
|
|
2228
|
+
if (names.length === 0) return;
|
|
2229
|
+
const vectors = await this.embed.embed(names);
|
|
2230
|
+
const embeddings = new Map(names.map((name, index) => [name, vectors[index]]));
|
|
2231
|
+
await this.grafeo.upsertEntitiesAndRelations(entities, relations, embeddings);
|
|
1951
2232
|
}
|
|
1952
2233
|
};
|
|
1953
|
-
function formatError(
|
|
1954
|
-
return
|
|
2234
|
+
function formatError(error) {
|
|
2235
|
+
return error instanceof Error ? error.message : String(error);
|
|
1955
2236
|
}
|
|
1956
2237
|
function withTimeout(promise, timeoutMs, label) {
|
|
1957
2238
|
let timer;
|
|
@@ -1981,22 +2262,30 @@ var FactCache = class {
|
|
|
1981
2262
|
key(level, id) {
|
|
1982
2263
|
return `${level}:${id}`;
|
|
1983
2264
|
}
|
|
1984
|
-
async add(content, level, userId, chatId, sessionId = "default") {
|
|
2265
|
+
async add(content, level, userId, chatId, sessionId = "default", key) {
|
|
2266
|
+
const id = level === "user" ? userId : chatId;
|
|
2267
|
+
const cacheKey = this.key(level, id);
|
|
2268
|
+
const existing = this.cache.get(cacheKey) ?? [];
|
|
2269
|
+
const normalizedKey = key?.trim() || void 0;
|
|
2270
|
+
const existingIndex = normalizedKey ? existing.findIndex((fact2) => fact2.key === normalizedKey) : -1;
|
|
2271
|
+
const previous = existingIndex >= 0 ? existing[existingIndex] : void 0;
|
|
2272
|
+
const now = Date.now();
|
|
1985
2273
|
const fact = {
|
|
1986
|
-
factId: uuidv42(),
|
|
2274
|
+
factId: previous?.factId ?? uuidv42(),
|
|
1987
2275
|
level,
|
|
1988
2276
|
chatId,
|
|
1989
2277
|
sessionId,
|
|
1990
2278
|
userId,
|
|
2279
|
+
key: normalizedKey,
|
|
1991
2280
|
content,
|
|
1992
|
-
createdAt:
|
|
2281
|
+
createdAt: previous?.createdAt ?? now,
|
|
2282
|
+
updatedAt: now
|
|
1993
2283
|
};
|
|
1994
|
-
await this.store.
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
this.cache.set(k, existing);
|
|
2284
|
+
if (previous) await this.store.updateFact(fact);
|
|
2285
|
+
else await this.store.saveFact(fact);
|
|
2286
|
+
if (existingIndex >= 0) existing[existingIndex] = fact;
|
|
2287
|
+
else existing.push(fact);
|
|
2288
|
+
this.cache.set(cacheKey, existing);
|
|
2000
2289
|
}
|
|
2001
2290
|
get(level, id) {
|
|
2002
2291
|
return this.cache.get(this.key(level, id)) ?? [];
|
|
@@ -2024,7 +2313,9 @@ var FactCache = class {
|
|
|
2024
2313
|
toString(level, id) {
|
|
2025
2314
|
const facts = this.get(level, id);
|
|
2026
2315
|
if (facts.length === 0) return "";
|
|
2027
|
-
return facts.sort((a, b) => a.createdAt - b.createdAt).map(
|
|
2316
|
+
return facts.sort((a, b) => a.createdAt - b.createdAt).map(
|
|
2317
|
+
(f) => `${new Date(f.updatedAt ?? f.createdAt).toISOString()}\uFF1A${f.key ? `[${f.key}] ` : ""}${f.content}`
|
|
2318
|
+
).join("\n");
|
|
2028
2319
|
}
|
|
2029
2320
|
};
|
|
2030
2321
|
|
|
@@ -2184,11 +2475,8 @@ var KnowledgeManager = class {
|
|
|
2184
2475
|
const now = Date.now();
|
|
2185
2476
|
const content = opts.content;
|
|
2186
2477
|
const contentHash = createHash("sha256").update(content).digest("hex");
|
|
2187
|
-
const
|
|
2188
|
-
|
|
2189
|
-
eq("chat_id", chatId),
|
|
2190
|
-
eq("session_id", sessionId)
|
|
2191
|
-
];
|
|
2478
|
+
const dedupeScope = opts.scope ?? "session";
|
|
2479
|
+
const domainFilter = dedupeScope === "user" ? [eq("user_id", userId)] : dedupeScope === "chat" ? [eq("chat_id", chatId)] : [eq("session_id", sessionId)];
|
|
2192
2480
|
const existing = await this.store.findDocumentByHash(contentHash, domainFilter);
|
|
2193
2481
|
if (existing) return { docId: existing.docId };
|
|
2194
2482
|
const pieces = chunkMarkdown(content, {
|
|
@@ -2507,73 +2795,203 @@ function withTimeout2(promise, timeoutMs, label) {
|
|
|
2507
2795
|
var SessionCache = class {
|
|
2508
2796
|
config;
|
|
2509
2797
|
sessions = /* @__PURE__ */ new Map();
|
|
2510
|
-
|
|
2798
|
+
legacyHistoryWindows = /* @__PURE__ */ new Map();
|
|
2511
2799
|
constructor(config) {
|
|
2512
2800
|
this.config = config;
|
|
2513
2801
|
}
|
|
2514
2802
|
getEntry(sessionId) {
|
|
2803
|
+
const entry = this.sessions.get(sessionId);
|
|
2804
|
+
if (entry) entry.lastAccessAt = Date.now();
|
|
2805
|
+
return entry;
|
|
2806
|
+
}
|
|
2807
|
+
peekEntry(sessionId) {
|
|
2515
2808
|
return this.sessions.get(sessionId);
|
|
2516
2809
|
}
|
|
2517
2810
|
getOrCreateEntry(sessionId, chatId, userId) {
|
|
2518
2811
|
let entry = this.sessions.get(sessionId);
|
|
2519
2812
|
if (!entry) {
|
|
2520
|
-
entry = {
|
|
2813
|
+
entry = {
|
|
2814
|
+
messages: [],
|
|
2815
|
+
totalTokens: 0,
|
|
2816
|
+
topics: [],
|
|
2817
|
+
topicTokens: 0,
|
|
2818
|
+
ids: { chatId, userId },
|
|
2819
|
+
lastAccessAt: Date.now()
|
|
2820
|
+
};
|
|
2521
2821
|
this.sessions.set(sessionId, entry);
|
|
2822
|
+
} else {
|
|
2823
|
+
entry.ids = { chatId, userId };
|
|
2824
|
+
entry.lastAccessAt = Date.now();
|
|
2522
2825
|
}
|
|
2523
2826
|
return entry;
|
|
2524
2827
|
}
|
|
2525
|
-
|
|
2828
|
+
hydrate(sessionId, chatId, userId, messages, topics) {
|
|
2526
2829
|
const entry = this.getOrCreateEntry(sessionId, chatId, userId);
|
|
2527
|
-
entry.messages
|
|
2528
|
-
entry.totalTokens
|
|
2830
|
+
entry.messages = sortMessages(dedupeMessages(messages));
|
|
2831
|
+
entry.totalTokens = sumMessageTokens(entry.messages);
|
|
2832
|
+
this.setTopics(sessionId, topics);
|
|
2833
|
+
return entry;
|
|
2834
|
+
}
|
|
2835
|
+
upsertMessages(sessionId, messages, chatId, userId) {
|
|
2836
|
+
const entry = this.getOrCreateEntry(sessionId, chatId, userId);
|
|
2837
|
+
const byId = new Map(entry.messages.map((m) => [m.messageId, m]));
|
|
2838
|
+
for (const message of messages) byId.set(message.messageId, message);
|
|
2839
|
+
entry.messages = sortMessages([...byId.values()]);
|
|
2840
|
+
entry.totalTokens = sumMessageTokens(entry.messages);
|
|
2841
|
+
return entry;
|
|
2842
|
+
}
|
|
2843
|
+
/** @deprecated 0.3.x 单元/API 兼容;新代码使用 upsertMessages。 */
|
|
2844
|
+
addMessages(sessionId, messages, chatId, userId) {
|
|
2845
|
+
const entry = this.upsertMessages(sessionId, messages, chatId, userId);
|
|
2529
2846
|
return entry.totalTokens >= this.config.sessionTokenLimit;
|
|
2530
2847
|
}
|
|
2848
|
+
/** @deprecated 新压缩链按 messageId 精确移除。 */
|
|
2531
2849
|
clearMessages(sessionId, keepAfter) {
|
|
2532
2850
|
const entry = this.sessions.get(sessionId);
|
|
2533
2851
|
if (!entry) return;
|
|
2534
|
-
|
|
2535
|
-
|
|
2536
|
-
|
|
2537
|
-
|
|
2538
|
-
|
|
2539
|
-
|
|
2540
|
-
|
|
2852
|
+
entry.messages = keepAfter == null ? [] : entry.messages.filter((message) => message.createdAt > keepAfter);
|
|
2853
|
+
entry.totalTokens = sumMessageTokens(entry.messages);
|
|
2854
|
+
}
|
|
2855
|
+
removeMessages(sessionId, messageIds) {
|
|
2856
|
+
const entry = this.sessions.get(sessionId);
|
|
2857
|
+
if (!entry) return;
|
|
2858
|
+
const ids = new Set(messageIds);
|
|
2859
|
+
entry.messages = entry.messages.filter((m) => !ids.has(m.messageId));
|
|
2860
|
+
entry.totalTokens = sumMessageTokens(entry.messages);
|
|
2861
|
+
entry.lastAccessAt = Date.now();
|
|
2541
2862
|
}
|
|
2542
2863
|
getSessionMessages(sessionId) {
|
|
2543
|
-
return this.
|
|
2864
|
+
return this.getEntry(sessionId)?.messages ?? [];
|
|
2544
2865
|
}
|
|
2545
|
-
|
|
2546
|
-
|
|
2866
|
+
selectOldestMessageBatch(sessionId, targetTokens) {
|
|
2867
|
+
const messages = this.getSessionMessages(sessionId);
|
|
2868
|
+
if (messages.length === 0 || targetTokens <= 0) return [];
|
|
2869
|
+
const selected = [];
|
|
2870
|
+
let tokens = 0;
|
|
2871
|
+
for (const message of messages) {
|
|
2872
|
+
selected.push(message);
|
|
2873
|
+
tokens += effectiveUsage(message);
|
|
2874
|
+
if (tokens >= targetTokens) break;
|
|
2875
|
+
}
|
|
2876
|
+
return selected;
|
|
2547
2877
|
}
|
|
2548
|
-
|
|
2549
|
-
const
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
const
|
|
2553
|
-
|
|
2554
|
-
|
|
2555
|
-
|
|
2556
|
-
|
|
2557
|
-
|
|
2558
|
-
|
|
2559
|
-
|
|
2560
|
-
|
|
2561
|
-
|
|
2562
|
-
|
|
2563
|
-
|
|
2564
|
-
|
|
2565
|
-
|
|
2566
|
-
|
|
2567
|
-
|
|
2568
|
-
this.historyWindows.set(sessionId, parts.join("\n\n"));
|
|
2878
|
+
setTopics(sessionId, topics, truncate = true) {
|
|
2879
|
+
const entry = this.sessions.get(sessionId);
|
|
2880
|
+
if (!entry) return;
|
|
2881
|
+
const ordered = [...topics].sort(topicOrder);
|
|
2882
|
+
const kept = [];
|
|
2883
|
+
let used = 0;
|
|
2884
|
+
for (let i = ordered.length - 1; i >= 0; i--) {
|
|
2885
|
+
const topic = normalizeTopic(ordered[i]);
|
|
2886
|
+
if (truncate && used + topic.tokens > this.config.compressedContextTokenLimit && kept.length > 0) break;
|
|
2887
|
+
kept.push(topic);
|
|
2888
|
+
used += topic.tokens;
|
|
2889
|
+
}
|
|
2890
|
+
entry.topics = kept.reverse();
|
|
2891
|
+
entry.topicTokens = used;
|
|
2892
|
+
entry.lastAccessAt = Date.now();
|
|
2893
|
+
}
|
|
2894
|
+
appendTopic(sessionId, topic) {
|
|
2895
|
+
const entry = this.sessions.get(sessionId);
|
|
2896
|
+
if (!entry) return;
|
|
2897
|
+
this.setTopics(sessionId, [...entry.topics, topic], false);
|
|
2569
2898
|
}
|
|
2899
|
+
replaceTopics(sessionId, removedIds, replacement) {
|
|
2900
|
+
const entry = this.sessions.get(sessionId);
|
|
2901
|
+
if (!entry) return;
|
|
2902
|
+
const ids = new Set(removedIds);
|
|
2903
|
+
this.setTopics(
|
|
2904
|
+
sessionId,
|
|
2905
|
+
[...entry.topics.filter((topic) => !ids.has(topic.summaryId)), replacement],
|
|
2906
|
+
false
|
|
2907
|
+
);
|
|
2908
|
+
}
|
|
2909
|
+
getTopics(sessionId) {
|
|
2910
|
+
return this.getEntry(sessionId)?.topics ?? [];
|
|
2911
|
+
}
|
|
2912
|
+
buildCompressedContext(sessionId) {
|
|
2913
|
+
return this.getTopics(sessionId).map((topic) => topic.title ? `## ${topic.title}
|
|
2914
|
+
${topic.summary}` : topic.summary).join("\n\n");
|
|
2915
|
+
}
|
|
2916
|
+
/** @deprecated 三级窗口兼容,仅供 0.3.x 调用方过渡。 */
|
|
2917
|
+
buildHistoryWindow(sessionId, groups) {
|
|
2918
|
+
const [detailCount, summaryCount, conciseCount] = this.config.topicRatio;
|
|
2919
|
+
const values = [
|
|
2920
|
+
...groups.detail.slice(0, detailCount).map((topic) => topic.detail ?? topic.summary),
|
|
2921
|
+
...groups.summary.slice(0, summaryCount).map((topic) => topic.summary),
|
|
2922
|
+
...groups.concise.slice(0, conciseCount).map((topic) => topic.concise ?? topic.summary)
|
|
2923
|
+
];
|
|
2924
|
+
let remaining = this.config.historyWindowTokenLimit;
|
|
2925
|
+
const kept = [];
|
|
2926
|
+
for (const value of values) {
|
|
2927
|
+
const tokens = countTokens(value);
|
|
2928
|
+
if (tokens > remaining) break;
|
|
2929
|
+
kept.push(value);
|
|
2930
|
+
remaining -= tokens;
|
|
2931
|
+
}
|
|
2932
|
+
this.legacyHistoryWindows.set(sessionId, kept.join("\n\n"));
|
|
2933
|
+
}
|
|
2934
|
+
/** @deprecated MemoryManager.getHistoryWindow 返回结构化窗口。 */
|
|
2570
2935
|
getHistoryWindow(sessionId) {
|
|
2571
|
-
return this.
|
|
2936
|
+
return this.legacyHistoryWindows.get(sessionId) ?? this.buildCompressedContext(sessionId);
|
|
2572
2937
|
}
|
|
2938
|
+
/** @deprecated 仅为 0.3.x 兼容。 */
|
|
2573
2939
|
setHistoryWindow(sessionId, content) {
|
|
2574
|
-
this.
|
|
2940
|
+
this.legacyHistoryWindows.set(sessionId, content);
|
|
2941
|
+
}
|
|
2942
|
+
setModelContextTokens(sessionId, modelContextTokens) {
|
|
2943
|
+
const entry = this.sessions.get(sessionId);
|
|
2944
|
+
if (!entry) return;
|
|
2945
|
+
entry.lastModelContextTokens = modelContextTokens;
|
|
2946
|
+
entry.lastAccessAt = Date.now();
|
|
2947
|
+
}
|
|
2948
|
+
getAllSessionIds() {
|
|
2949
|
+
return [...this.sessions.keys()];
|
|
2950
|
+
}
|
|
2951
|
+
delete(sessionId) {
|
|
2952
|
+
this.sessions.delete(sessionId);
|
|
2953
|
+
this.legacyHistoryWindows.delete(sessionId);
|
|
2954
|
+
}
|
|
2955
|
+
evictIdle(now, ttlMs, isBusy) {
|
|
2956
|
+
if (ttlMs <= 0) return [];
|
|
2957
|
+
const evicted = [];
|
|
2958
|
+
for (const [sessionId, entry] of this.sessions) {
|
|
2959
|
+
if (now - entry.lastAccessAt < ttlMs || isBusy(sessionId)) continue;
|
|
2960
|
+
this.sessions.delete(sessionId);
|
|
2961
|
+
evicted.push(sessionId);
|
|
2962
|
+
}
|
|
2963
|
+
return evicted;
|
|
2575
2964
|
}
|
|
2576
2965
|
};
|
|
2966
|
+
function effectiveUsage(message) {
|
|
2967
|
+
if (Number.isFinite(message.usage) && message.usage > 0) return Math.floor(message.usage);
|
|
2968
|
+
return Math.max(
|
|
2969
|
+
1,
|
|
2970
|
+
(message.content.length + (message.parts?.length ?? 0) + message.metadata.length + (message.payload?.length ?? 0)) * 2
|
|
2971
|
+
);
|
|
2972
|
+
}
|
|
2973
|
+
function sumMessageTokens(messages) {
|
|
2974
|
+
return messages.reduce((sum, message) => sum + effectiveUsage(message), 0);
|
|
2975
|
+
}
|
|
2976
|
+
function dedupeMessages(messages) {
|
|
2977
|
+
return [...new Map(messages.map((message) => [message.messageId, message])).values()];
|
|
2978
|
+
}
|
|
2979
|
+
function sortMessages(messages) {
|
|
2980
|
+
return messages.sort(
|
|
2981
|
+
(a, b) => a.createdAt - b.createdAt || a.messageId.localeCompare(b.messageId)
|
|
2982
|
+
);
|
|
2983
|
+
}
|
|
2984
|
+
function topicOrder(a, b) {
|
|
2985
|
+
return a.endTime - b.endTime || a.summaryId.localeCompare(b.summaryId);
|
|
2986
|
+
}
|
|
2987
|
+
function normalizeTopic(topic) {
|
|
2988
|
+
const summary = topic.summary || topic.detail || topic.concise || "";
|
|
2989
|
+
return {
|
|
2990
|
+
...topic,
|
|
2991
|
+
summary,
|
|
2992
|
+
tokens: topic.tokens > 0 ? topic.tokens : Math.max(1, countTokens(summary))
|
|
2993
|
+
};
|
|
2994
|
+
}
|
|
2577
2995
|
|
|
2578
2996
|
// src/memory.manager.ts
|
|
2579
2997
|
var MemoryManager = class {
|
|
@@ -2588,7 +3006,13 @@ var MemoryManager = class {
|
|
|
2588
3006
|
knowledgeManager;
|
|
2589
3007
|
sessionMap = /* @__PURE__ */ new Map();
|
|
2590
3008
|
optimizeTimer;
|
|
3009
|
+
sessionSweepTimer;
|
|
2591
3010
|
optimizeRunning = false;
|
|
3011
|
+
optimizeTask;
|
|
3012
|
+
destroyTask;
|
|
3013
|
+
hydration = /* @__PURE__ */ new Map();
|
|
3014
|
+
pendingWrites = /* @__PURE__ */ new Map();
|
|
3015
|
+
warnedDefaultModelContext = false;
|
|
2592
3016
|
constructor(config) {
|
|
2593
3017
|
this.config = resolveConfig(config);
|
|
2594
3018
|
this.store = new MemoryStore(this.config);
|
|
@@ -2622,17 +3046,26 @@ var MemoryManager = class {
|
|
|
2622
3046
|
for (const s of allSessions) {
|
|
2623
3047
|
this.sessionMap.set(s.sessionId, this.deserializeSession(s));
|
|
2624
3048
|
}
|
|
2625
|
-
await this.restoreFromStorage();
|
|
2626
3049
|
if (this.config.autoOptimizeOnInit) {
|
|
2627
|
-
|
|
3050
|
+
this.optimizeTask = this.runBackgroundOptimize(this.config.optimizeVersionRetentionMs);
|
|
2628
3051
|
}
|
|
2629
3052
|
if (this.config.autoOptimizeIntervalMs > 0) {
|
|
2630
3053
|
const retention = Math.max(this.config.optimizeVersionRetentionMs, 6e4);
|
|
2631
3054
|
this.optimizeTimer = setInterval(() => {
|
|
2632
|
-
|
|
3055
|
+
this.optimizeTask = this.runBackgroundOptimize(retention);
|
|
2633
3056
|
}, this.config.autoOptimizeIntervalMs);
|
|
2634
3057
|
this.optimizeTimer.unref?.();
|
|
2635
3058
|
}
|
|
3059
|
+
if (this.config.sessionIdleTtlMs > 0) {
|
|
3060
|
+
this.sessionSweepTimer = setInterval(() => {
|
|
3061
|
+
this.sessionCache.evictIdle(
|
|
3062
|
+
Date.now(),
|
|
3063
|
+
this.config.sessionIdleTtlMs,
|
|
3064
|
+
(sessionId) => this.isSessionBusy(sessionId)
|
|
3065
|
+
);
|
|
3066
|
+
}, this.config.sessionSweepIntervalMs);
|
|
3067
|
+
this.sessionSweepTimer.unref?.();
|
|
3068
|
+
}
|
|
2636
3069
|
}
|
|
2637
3070
|
/** 后台压实的统一入口:防重入(上一轮未结束则跳过),失败仅告警不影响服务。*/
|
|
2638
3071
|
async runBackgroundOptimize(retentionMs) {
|
|
@@ -2660,120 +3093,141 @@ var MemoryManager = class {
|
|
|
2660
3093
|
async optimizeStorage(retentionMs) {
|
|
2661
3094
|
return this.store.optimizeStorage(retentionMs ?? this.config.optimizeVersionRetentionMs);
|
|
2662
3095
|
}
|
|
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
|
-
);
|
|
3096
|
+
async ensureSessionHydrated(sessionId, fallbackIds) {
|
|
3097
|
+
if (this.sessionCache.peekEntry(sessionId)) return;
|
|
3098
|
+
let task = this.hydration.get(sessionId);
|
|
3099
|
+
if (!task) {
|
|
3100
|
+
task = (async () => {
|
|
3101
|
+
const allTopics = await this.store.getTopicsBySession(sessionId);
|
|
3102
|
+
const latestTopic = allTopics.at(-1);
|
|
3103
|
+
const allRawAfterBoundary = latestTopic ? await this.store.getMessagesAfterBoundary(
|
|
3104
|
+
sessionId,
|
|
3105
|
+
latestTopic.endTime,
|
|
3106
|
+
latestTopic.endMessageId
|
|
3107
|
+
) : await this.store.getAllMessagesBySession(sessionId);
|
|
3108
|
+
const since = this.config.maxHistoryAgeMs > 0 ? Date.now() - this.config.maxHistoryAgeMs : 0;
|
|
3109
|
+
const rawMessages = since > 0 ? allRawAfterBoundary.filter((message) => message.createdAt >= since) : allRawAfterBoundary;
|
|
3110
|
+
const session = this.sessionMap.get(sessionId);
|
|
3111
|
+
const ids = allRawAfterBoundary.at(-1) ?? latestTopic ?? session ?? fallbackIds ?? { chatId: DEFAULT_CHAT_ID, userId: DEFAULT_USER_ID };
|
|
3112
|
+
const visibleTopics = since > 0 ? allTopics.filter((topic) => topic.endTime >= since) : allTopics;
|
|
3113
|
+
this.sessionCache.hydrate(
|
|
3114
|
+
sessionId,
|
|
3115
|
+
ids.chatId,
|
|
3116
|
+
ids.userId,
|
|
3117
|
+
rawMessages,
|
|
3118
|
+
visibleTopics
|
|
3119
|
+
);
|
|
3120
|
+
})();
|
|
3121
|
+
this.hydration.set(sessionId, task);
|
|
3122
|
+
task.finally(() => {
|
|
3123
|
+
if (this.hydration.get(sessionId) === task) this.hydration.delete(sessionId);
|
|
3124
|
+
});
|
|
3125
|
+
}
|
|
3126
|
+
await task;
|
|
2698
3127
|
}
|
|
2699
|
-
|
|
2700
|
-
|
|
2701
|
-
|
|
2702
|
-
|
|
2703
|
-
|
|
3128
|
+
isSessionBusy(sessionId) {
|
|
3129
|
+
return this.hydration.has(sessionId) || this.pendingWrites.has(sessionId) || this.compressManager.isBusy(sessionId);
|
|
3130
|
+
}
|
|
3131
|
+
waitForPendingWrites(sessionId) {
|
|
3132
|
+
return this.pendingWrites.get(sessionId) ?? Promise.resolve();
|
|
3133
|
+
}
|
|
3134
|
+
queueSessionWrite(sessionId, write) {
|
|
3135
|
+
const previous = this.pendingWrites.get(sessionId) ?? Promise.resolve();
|
|
3136
|
+
const task = previous.catch(() => {
|
|
3137
|
+
}).then(write);
|
|
3138
|
+
const settled = task.catch(() => {
|
|
3139
|
+
});
|
|
3140
|
+
this.pendingWrites.set(sessionId, settled);
|
|
3141
|
+
settled.finally(() => {
|
|
3142
|
+
if (this.pendingWrites.get(sessionId) === settled) this.pendingWrites.delete(sessionId);
|
|
3143
|
+
});
|
|
3144
|
+
return task;
|
|
3145
|
+
}
|
|
3146
|
+
updateChat(messages, opts) {
|
|
3147
|
+
if (messages.length === 0) return Promise.resolve();
|
|
3148
|
+
const sessionId = opts?.sessionId ?? messages[0]?.sessionId ?? DEFAULT_SESSION_ID;
|
|
3149
|
+
return this.queueSessionWrite(sessionId, () => this.doUpdateChat(messages, opts, sessionId));
|
|
3150
|
+
}
|
|
3151
|
+
async doUpdateChat(messages, opts, sessionId) {
|
|
3152
|
+
const userId = opts?.userId ?? messages[0]?.userId ?? DEFAULT_USER_ID;
|
|
3153
|
+
const chatId = opts?.chatId ?? messages[0]?.chatId ?? DEFAULT_CHAT_ID;
|
|
2704
3154
|
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
|
|
3155
|
+
await this.ensureSessionHydrated(sessionId, { chatId, userId });
|
|
3156
|
+
const normalized = messages.map((message) => {
|
|
3157
|
+
const parts = JSON.stringify(message.parts ?? []);
|
|
3158
|
+
const metadata = JSON.stringify(message.metadata ?? {});
|
|
3159
|
+
const payload = message.payload === void 0 ? void 0 : JSON.stringify(message.payload);
|
|
3160
|
+
const tokenInput = [message.content, parts, metadata, payload ?? ""].join("\n");
|
|
3161
|
+
return {
|
|
3162
|
+
messageId: message.messageId ?? uuidv44(),
|
|
3163
|
+
talkerId: message.talkerId ?? "user",
|
|
3164
|
+
chatId: message.chatId ?? chatId,
|
|
3165
|
+
userId: message.userId ?? userId,
|
|
3166
|
+
sessionId: message.sessionId ?? sessionId,
|
|
3167
|
+
type: message.type ?? "text",
|
|
3168
|
+
content: message.content,
|
|
3169
|
+
parts,
|
|
3170
|
+
payload,
|
|
3171
|
+
usage: message.usage ?? countTokens(tokenInput),
|
|
3172
|
+
metadata,
|
|
3173
|
+
createdAt: message.createdAt ?? now
|
|
2749
3174
|
};
|
|
2750
|
-
|
|
2751
|
-
|
|
2752
|
-
|
|
2753
|
-
|
|
2754
|
-
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
3175
|
+
});
|
|
3176
|
+
const vectors = await this.embed.embed(normalized.map((message) => message.content));
|
|
3177
|
+
const stored = normalized.map((message, index) => ({
|
|
3178
|
+
...message,
|
|
3179
|
+
vector: vectors[index]
|
|
3180
|
+
}));
|
|
3181
|
+
await this.store.upsertMessages(stored);
|
|
3182
|
+
this.sessionCache.upsertMessages(sessionId, stored, chatId, userId);
|
|
3183
|
+
const existing = this.sessionMap.get(sessionId);
|
|
3184
|
+
const session = existing ? {
|
|
3185
|
+
...this.serializeSession(existing),
|
|
3186
|
+
updatedAt: now,
|
|
3187
|
+
...opts?.sessionTitle !== void 0 && { title: opts.sessionTitle },
|
|
3188
|
+
...opts?.sessionMetadata !== void 0 && {
|
|
3189
|
+
metadata: JSON.stringify(opts.sessionMetadata)
|
|
3190
|
+
}
|
|
3191
|
+
} : {
|
|
3192
|
+
sessionId,
|
|
3193
|
+
chatId,
|
|
3194
|
+
userId,
|
|
3195
|
+
title: opts?.sessionTitle ?? "",
|
|
3196
|
+
metadata: JSON.stringify(opts?.sessionMetadata ?? {}),
|
|
3197
|
+
createdAt: now,
|
|
3198
|
+
updatedAt: now
|
|
3199
|
+
};
|
|
3200
|
+
this.sessionMap.set(sessionId, this.deserializeSession(session));
|
|
3201
|
+
if (existing) await this.store.upsertSession(session);
|
|
3202
|
+
else await this.store.insertSession(session);
|
|
3203
|
+
const entry = this.sessionCache.getEntry(sessionId);
|
|
3204
|
+
const rawLimit = this.calculateWindowUsage(
|
|
3205
|
+
entry.lastModelContextTokens ?? this.config.defaultModelContextTokens
|
|
3206
|
+
).rawTokenLimit;
|
|
3207
|
+
if (entry.totalTokens >= Math.floor(rawLimit * this.config.precompressionRatio)) {
|
|
3208
|
+
void this.compressManager.triggerCompress(sessionId, false, false, rawLimit).catch((error) => {
|
|
3209
|
+
console.error(`[MemoryManager] Background compress failed for ${sessionId}:`, error);
|
|
2759
3210
|
});
|
|
2760
3211
|
}
|
|
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
3212
|
}
|
|
2770
3213
|
async flushChat(sessionId, opts) {
|
|
2771
3214
|
const sid = sessionId ?? DEFAULT_SESSION_ID;
|
|
2772
|
-
|
|
3215
|
+
await this.waitForPendingWrites(sid);
|
|
3216
|
+
await this.ensureSessionHydrated(sid);
|
|
3217
|
+
const entry = this.sessionCache.getEntry(sid);
|
|
3218
|
+
const rawLimit = this.calculateWindowUsage(
|
|
3219
|
+
entry?.lastModelContextTokens ?? this.config.defaultModelContextTokens
|
|
3220
|
+
).rawTokenLimit;
|
|
3221
|
+
const promise = this.compressManager.triggerCompress(
|
|
3222
|
+
sid,
|
|
3223
|
+
true,
|
|
3224
|
+
opts?.waitGraph === true,
|
|
3225
|
+
rawLimit
|
|
3226
|
+
);
|
|
2773
3227
|
if (opts?.wait) await promise;
|
|
2774
3228
|
}
|
|
2775
|
-
async updateFacts(content, level, userId, chatId, sessionId) {
|
|
2776
|
-
await this.factCache.add(content, level, userId, chatId, sessionId);
|
|
3229
|
+
async updateFacts(content, level, userId, chatId, sessionId, key) {
|
|
3230
|
+
await this.factCache.add(content, level, userId, chatId, sessionId, key);
|
|
2777
3231
|
}
|
|
2778
3232
|
async updateEntity(entities, relations, context) {
|
|
2779
3233
|
if (entities.length === 0 && relations.length === 0) return;
|
|
@@ -2817,16 +3271,32 @@ var MemoryManager = class {
|
|
|
2817
3271
|
useGraph = await this.llm.judgeNeedsGraphSearch(query).catch(() => false);
|
|
2818
3272
|
}
|
|
2819
3273
|
const tasks = [
|
|
2820
|
-
this.store.hybridSearchTopics(query, vector, filter, limit).then(
|
|
2821
|
-
(topics)
|
|
3274
|
+
this.store.hybridSearchTopics(query, vector, filter, limit).then((topics) => {
|
|
3275
|
+
void this.store.incrementTopicRecallCounts(topics).catch(() => {
|
|
3276
|
+
});
|
|
3277
|
+
return topics.map((topic, index) => ({
|
|
2822
3278
|
type: "topic",
|
|
2823
|
-
content:
|
|
2824
|
-
score: 1,
|
|
3279
|
+
content: topic.summary,
|
|
3280
|
+
score: 1 / (index + 1),
|
|
2825
3281
|
meta: {
|
|
2826
|
-
summaryId:
|
|
2827
|
-
sessionId:
|
|
2828
|
-
chatId:
|
|
2829
|
-
userId:
|
|
3282
|
+
summaryId: topic.summaryId,
|
|
3283
|
+
sessionId: topic.sessionId,
|
|
3284
|
+
chatId: topic.chatId,
|
|
3285
|
+
userId: topic.userId
|
|
3286
|
+
}
|
|
3287
|
+
}));
|
|
3288
|
+
}).catch(() => []),
|
|
3289
|
+
this.store.hybridSearchMessages(query, vector, filter, limit).then(
|
|
3290
|
+
(messages) => messages.map((message, index) => ({
|
|
3291
|
+
type: "message",
|
|
3292
|
+
content: message.content,
|
|
3293
|
+
score: 1 / (index + 1),
|
|
3294
|
+
meta: {
|
|
3295
|
+
messageId: message.messageId,
|
|
3296
|
+
sessionId: message.sessionId,
|
|
3297
|
+
chatId: message.chatId,
|
|
3298
|
+
userId: message.userId,
|
|
3299
|
+
metadata: safeParseObject2(message.metadata)
|
|
2830
3300
|
}
|
|
2831
3301
|
}))
|
|
2832
3302
|
).catch(() => [])
|
|
@@ -2885,10 +3355,79 @@ var MemoryManager = class {
|
|
|
2885
3355
|
...this.factCache.get("chat", chatId)
|
|
2886
3356
|
].sort((a, b) => a.createdAt - b.createdAt);
|
|
2887
3357
|
if (merged.length === 0) return "";
|
|
2888
|
-
return merged.map(
|
|
3358
|
+
return merged.map(
|
|
3359
|
+
(f) => `${new Date(f.updatedAt ?? f.createdAt).toISOString()}\uFF1A${f.key ? `[${f.key}] ` : ""}${f.content}`
|
|
3360
|
+
).join("\n");
|
|
3361
|
+
}
|
|
3362
|
+
async getHistoryWindow(sessionId, modelContextTokens) {
|
|
3363
|
+
let modelTokens = modelContextTokens;
|
|
3364
|
+
if (modelTokens == null) {
|
|
3365
|
+
modelTokens = this.config.defaultModelContextTokens;
|
|
3366
|
+
if (!this.warnedDefaultModelContext) {
|
|
3367
|
+
this.warnedDefaultModelContext = true;
|
|
3368
|
+
console.warn(
|
|
3369
|
+
`[MemoryManager] getHistoryWindow \u672A\u4F20 modelContextTokens\uFF0C\u4F7F\u7528\u9ED8\u8BA4 ${modelTokens} tokens\u3002`
|
|
3370
|
+
);
|
|
3371
|
+
}
|
|
3372
|
+
}
|
|
3373
|
+
if (!Number.isFinite(modelTokens) || modelTokens <= 0) {
|
|
3374
|
+
throw new Error("modelContextTokens \u5FC5\u987B\u662F\u6B63\u6570");
|
|
3375
|
+
}
|
|
3376
|
+
modelTokens = Math.floor(modelTokens);
|
|
3377
|
+
await this.waitForPendingWrites(sessionId);
|
|
3378
|
+
await this.ensureSessionHydrated(sessionId);
|
|
3379
|
+
this.sessionCache.setModelContextTokens(sessionId, modelTokens);
|
|
3380
|
+
const usageLimits = this.calculateWindowUsage(modelTokens);
|
|
3381
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
3382
|
+
let entry2 = this.sessionCache.getEntry(sessionId);
|
|
3383
|
+
if (entry2.totalTokens <= usageLimits.rawTokenLimit || entry2.messages.length === 0) break;
|
|
3384
|
+
await this.compressManager.waitForIdle(sessionId);
|
|
3385
|
+
entry2 = this.sessionCache.getEntry(sessionId);
|
|
3386
|
+
if (entry2.totalTokens <= usageLimits.rawTokenLimit || entry2.messages.length === 0) break;
|
|
3387
|
+
console.warn(
|
|
3388
|
+
`[MemoryManager] session ${sessionId} raw history (${entry2.totalTokens}) exceeds current model budget (${usageLimits.rawTokenLimit}); waiting for immediate compression.`
|
|
3389
|
+
);
|
|
3390
|
+
const beforeTokens = entry2.totalTokens;
|
|
3391
|
+
await this.compressManager.triggerCompress(
|
|
3392
|
+
sessionId,
|
|
3393
|
+
true,
|
|
3394
|
+
false,
|
|
3395
|
+
usageLimits.rawTokenLimit
|
|
3396
|
+
);
|
|
3397
|
+
const after = this.sessionCache.getEntry(sessionId);
|
|
3398
|
+
if (after.messages.length > 0 && after.totalTokens >= beforeTokens) {
|
|
3399
|
+
throw new Error(`Compression made no progress for session ${sessionId}`);
|
|
3400
|
+
}
|
|
3401
|
+
}
|
|
3402
|
+
const entry = this.sessionCache.getEntry(sessionId);
|
|
3403
|
+
if (entry.totalTokens > usageLimits.rawTokenLimit) {
|
|
3404
|
+
throw new Error(`Unable to fit session ${sessionId} into the requested model context`);
|
|
3405
|
+
}
|
|
3406
|
+
return {
|
|
3407
|
+
sessionId,
|
|
3408
|
+
compressedContext: this.sessionCache.buildCompressedContext(sessionId),
|
|
3409
|
+
recentMessages: entry.messages.map(toMemoryRawMessage),
|
|
3410
|
+
usage: {
|
|
3411
|
+
...usageLimits,
|
|
3412
|
+
compressedTokens: entry.topicTokens,
|
|
3413
|
+
rawTokens: entry.totalTokens
|
|
3414
|
+
}
|
|
3415
|
+
};
|
|
2889
3416
|
}
|
|
2890
|
-
|
|
2891
|
-
|
|
3417
|
+
calculateWindowUsage(modelContextTokens) {
|
|
3418
|
+
const usableContextTokens = Math.floor(modelContextTokens * this.config.contextUsageRatio);
|
|
3419
|
+
const rawTokenLimit = usableContextTokens - this.config.compressedContextTokenLimit;
|
|
3420
|
+
if (rawTokenLimit <= 0) {
|
|
3421
|
+
throw new Error(
|
|
3422
|
+
`modelContextTokens=${modelContextTokens} \u5728 contextUsageRatio=${this.config.contextUsageRatio} \u4E0B\u4E0D\u8DB3\u4EE5\u5BB9\u7EB3 compressedContextTokenLimit=${this.config.compressedContextTokenLimit}`
|
|
3423
|
+
);
|
|
3424
|
+
}
|
|
3425
|
+
return {
|
|
3426
|
+
modelContextTokens,
|
|
3427
|
+
usableContextTokens,
|
|
3428
|
+
compressedTokenLimit: this.config.compressedContextTokenLimit,
|
|
3429
|
+
rawTokenLimit
|
|
3430
|
+
};
|
|
2892
3431
|
}
|
|
2893
3432
|
buildScopeFilter(scope, scopeId) {
|
|
2894
3433
|
if (scope === "session" && scopeId) return [eq("session_id", scopeId)];
|
|
@@ -2946,6 +3485,7 @@ var MemoryManager = class {
|
|
|
2946
3485
|
async deleteSession(sessionId) {
|
|
2947
3486
|
if (!this.sessionMap.has(sessionId)) return false;
|
|
2948
3487
|
this.sessionMap.delete(sessionId);
|
|
3488
|
+
this.sessionCache.delete(sessionId);
|
|
2949
3489
|
await this.store.deleteSession(sessionId);
|
|
2950
3490
|
await this.store.deleteMessagesBySession(sessionId).catch((err) => {
|
|
2951
3491
|
console.error(`[MemoryManager] Failed to delete messages for session ${sessionId}:`, err);
|
|
@@ -3020,8 +3560,8 @@ var MemoryManager = class {
|
|
|
3020
3560
|
return this.factCache.all().find((f) => f.chatId === chatId)?.userId;
|
|
3021
3561
|
}
|
|
3022
3562
|
/** 手动新增一条事实 */
|
|
3023
|
-
async addFact(content, level, userId, chatId, sessionId) {
|
|
3024
|
-
await this.factCache.add(content, level, userId, chatId, sessionId);
|
|
3563
|
+
async addFact(content, level, userId, chatId, sessionId, key) {
|
|
3564
|
+
await this.factCache.add(content, level, userId, chatId, sessionId, key);
|
|
3025
3565
|
}
|
|
3026
3566
|
/** 删除单条事实,返回是否命中 */
|
|
3027
3567
|
async deleteFact(factId) {
|
|
@@ -3060,15 +3600,68 @@ var MemoryManager = class {
|
|
|
3060
3600
|
return page ? this.knowledgeManager.listDocuments(filter, page) : this.knowledgeManager.listDocuments(filter);
|
|
3061
3601
|
}
|
|
3062
3602
|
destroy() {
|
|
3603
|
+
if (this.destroyTask) return this.destroyTask;
|
|
3063
3604
|
if (this.optimizeTimer) {
|
|
3064
3605
|
clearInterval(this.optimizeTimer);
|
|
3065
3606
|
this.optimizeTimer = void 0;
|
|
3066
3607
|
}
|
|
3067
|
-
this.
|
|
3068
|
-
|
|
3608
|
+
if (this.sessionSweepTimer) {
|
|
3609
|
+
clearInterval(this.sessionSweepTimer);
|
|
3610
|
+
this.sessionSweepTimer = void 0;
|
|
3611
|
+
}
|
|
3612
|
+
const writes = [...this.pendingWrites.values(), ...this.hydration.values()];
|
|
3613
|
+
this.destroyTask = Promise.all(writes).catch(() => {
|
|
3614
|
+
}).then(() => this.compressManager.waitForAllIdle()).catch(() => {
|
|
3615
|
+
}).then(() => this.optimizeTask).catch(() => {
|
|
3616
|
+
}).then(async () => {
|
|
3617
|
+
this.grafeo.close();
|
|
3618
|
+
await this.store.close().catch(() => {
|
|
3619
|
+
});
|
|
3069
3620
|
});
|
|
3621
|
+
return this.destroyTask;
|
|
3070
3622
|
}
|
|
3071
3623
|
};
|
|
3624
|
+
function toMemoryRawMessage(message) {
|
|
3625
|
+
const parts = safeParseArray(message.parts);
|
|
3626
|
+
const payload = message.payload === void 0 ? void 0 : safeParseValue(message.payload);
|
|
3627
|
+
return {
|
|
3628
|
+
messageId: message.messageId,
|
|
3629
|
+
talkerId: message.talkerId,
|
|
3630
|
+
chatId: message.chatId,
|
|
3631
|
+
userId: message.userId,
|
|
3632
|
+
sessionId: message.sessionId,
|
|
3633
|
+
type: message.type,
|
|
3634
|
+
content: message.content,
|
|
3635
|
+
...parts.length > 0 && { parts },
|
|
3636
|
+
...payload !== void 0 && { payload },
|
|
3637
|
+
usage: message.usage,
|
|
3638
|
+
metadata: safeParseObject2(message.metadata),
|
|
3639
|
+
createdAt: message.createdAt
|
|
3640
|
+
};
|
|
3641
|
+
}
|
|
3642
|
+
function safeParseArray(value) {
|
|
3643
|
+
try {
|
|
3644
|
+
const parsed = JSON.parse(value);
|
|
3645
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
3646
|
+
} catch {
|
|
3647
|
+
return [];
|
|
3648
|
+
}
|
|
3649
|
+
}
|
|
3650
|
+
function safeParseObject2(value) {
|
|
3651
|
+
try {
|
|
3652
|
+
const parsed = JSON.parse(value);
|
|
3653
|
+
return parsed != null && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
3654
|
+
} catch {
|
|
3655
|
+
return {};
|
|
3656
|
+
}
|
|
3657
|
+
}
|
|
3658
|
+
function safeParseValue(value) {
|
|
3659
|
+
try {
|
|
3660
|
+
return JSON.parse(value);
|
|
3661
|
+
} catch {
|
|
3662
|
+
return value;
|
|
3663
|
+
}
|
|
3664
|
+
}
|
|
3072
3665
|
export {
|
|
3073
3666
|
DEFAULT_NODE_TYPES,
|
|
3074
3667
|
DEFAULT_RELATION_TYPES,
|