@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/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 ?? 10240,
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: "detail", kind: "fts" }
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: Number(r.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: t.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
- detail: r.detail,
920
- summary: r.summary,
921
- concise: r.concise,
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-ZLQ766IO.js");
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, ["detail"], query, vector, filter, limit)).slice(0, limit);
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 data.choices[0].message.content ?? "";
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
- // ── 第一步:生成三级摘要(JSON 输出)──────────────────────────────────────────
1861
+ // ── 对话 Topic 摘要(JSON 输出)──────────────────────────────────────────────
1662
1862
  async summarizeMessages(messages) {
1663
1863
  const now = (/* @__PURE__ */ new Date()).toISOString();
1664
- const { detailMaxTokens, summaryMaxTokens, conciseMaxTokens } = this.config;
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\u5BF9\u7ED9\u5B9A\u7684\u5BF9\u8BDD\u5185\u5BB9\u751F\u6210\u4E00\u4E2A\u4E3B\u9898\u6807\u9898\u548C\u4E09\u7EA7\u6458\u8981\u3002
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
- - detail\uFF1A\u8BE6\u7EC6\u6458\u8981\uFF0C\u6700\u591A ${detailMaxTokens} tokens\uFF0C\u4FDD\u7559\u5173\u952E\u4E8B\u5B9E\u3001\u4EBA\u7269\u548C\u65F6\u95F4
1671
- - summary\uFF1A\u4E2D\u7B49\u6458\u8981\uFF0C\u6700\u591A ${summaryMaxTokens} tokens
1672
- - concise\uFF1A\u4E00\u4E24\u53E5\u8BDD\u7684\u7B80\u6D01\u6458\u8981\uFF0C\u6700\u591A ${conciseMaxTokens} tokens
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": "...", "detail": "...", "summary": "...", "concise": "..."}`;
1676
- const formatted = messages.map((m) => `[${new Date(m.createdAt).toISOString()}] ${m.talkerId || "user"}: ${m.content}`).join("\n");
1677
- const raw = await this.chatCompletion(systemPrompt, `\u8BF7\u5BF9\u4EE5\u4E0B\u5BF9\u8BDD\u751F\u6210\u4E3B\u9898\u6807\u9898\u548C\u4E09\u7EA7\u6458\u8981\uFF1A
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
- const parsed = JSON.parse(raw);
1681
- return { title: parsed.title ?? "", detail: parsed.detail, summary: parsed.summary, concise: parsed.concise };
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
- concise: summary.concise,
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
- triggerCompress(sessionId, force = false, waitGraph = false) {
1837
- const chain = this.sessionChain.get(sessionId) ?? Promise.resolve();
1838
- const next = chain.then(
1839
- () => this.semaphore.run(() => this.doCompress(sessionId, force, waitGraph))
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
- this.sessionChain.set(sessionId, next.catch(() => {
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 doCompress(sessionId, force, waitGraph) {
1846
- const messages = this.sessionCache.getSessionMessages(sessionId).slice();
1847
- if (messages.length === 0) return;
1848
- const entry = this.sessionCache.getEntry(sessionId);
1849
- if (!entry) return;
1850
- if (!force && entry.totalTokens < this.config.sessionTokenLimit) return;
1851
- const { chatId, userId } = entry.ids;
1852
- const startTime = messages[0].createdAt;
1853
- const endTime = messages[messages.length - 1].createdAt;
1854
- let summary;
1855
- try {
1856
- summary = await this.llm.summarizeMessages(messages);
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
- const { title, detail, summary: summaryText, concise } = summary;
1862
- const topicTextForEmbed = [detail, summaryText, concise].find((t) => t.length > 0) ?? "";
1863
- let topicVector = [];
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
- topicVector = await this.embed.embedOne(topicTextForEmbed);
1866
- } catch (err) {
1867
- console.error(`[CompressManager] Embed topic failed:`, err);
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
- detail,
1876
- summary: summaryText,
1877
- concise,
1878
- startTime,
1879
- endTime,
1880
- createdAt: Date.now(),
1881
- updatedAt: Date.now(),
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: topicVector
2184
+ vector
1884
2185
  };
1885
- try {
1886
- await this.store.addTopic(topic);
1887
- } catch (err) {
1888
- console.error(`[CompressManager] Save topic failed:`, err);
1889
- }
1890
- this.sessionCache.clearMessages(sessionId, endTime);
1891
- const [n1, n2, n3] = this.config.topicRatio;
1892
- try {
1893
- const topicGroups = await this.store.getRecentTopics(chatId, userId, n1, n2, n3);
1894
- this.sessionCache.buildHistoryWindow(sessionId, topicGroups);
1895
- } catch (err) {
1896
- console.error(`[CompressManager] Rebuild history window failed:`, err);
1897
- }
1898
- const graphTask = this.extractAndPersistGraph(messages, sessionId, chatId, userId, endTime);
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((err) => {
1903
- console.error(`[CompressManager] Background graph persist failed:`, err);
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((e) => ({
1914
- name: e.name,
1915
- type: e.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((r) => ({
1925
- from: r.from,
1926
- to: r.to,
1927
- type: r.type,
1928
- happenedAt: r.happenedAt ?? void 0,
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 allNames = [.../* @__PURE__ */ new Set([...entities.map((e) => e.name), ...relations.flatMap((r) => [r.from, r.to])])];
1938
- const embeddings = /* @__PURE__ */ new Map();
1939
- try {
1940
- const vecs = await this.embed.embed(allNames);
1941
- allNames.forEach((name, i) => embeddings.set(name, vecs[i]));
1942
- } catch (err) {
1943
- console.error(`[CompressManager] Embed entity names failed:`, err);
1944
- return;
1945
- }
1946
- try {
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 formatError(err) {
1954
- return err instanceof Error ? err.message : String(err);
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: Date.now()
2372
+ createdAt: previous?.createdAt ?? now,
2373
+ updatedAt: now
1993
2374
  };
1994
- await this.store.saveFact(fact);
1995
- const id = level === "user" ? userId : chatId;
1996
- const k = this.key(level, id);
1997
- const existing = this.cache.get(k) ?? [];
1998
- existing.push(fact);
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((f) => `${new Date(f.createdAt).toISOString()}\uFF1A${f.content}`).join("\n");
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 domainFilter = [
2188
- eq("user_id", userId),
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
- historyWindows = /* @__PURE__ */ new Map();
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 = { messages: [], totalTokens: 0, ids: { chatId, userId } };
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
- addMessages(sessionId, messages, chatId, userId) {
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.push(...messages);
2528
- entry.totalTokens += messages.reduce((sum, m) => sum + m.usage, 0);
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
- if (keepAfter != null) {
2535
- entry.messages = entry.messages.filter((m) => m.createdAt > keepAfter);
2536
- entry.totalTokens = entry.messages.reduce((sum, m) => sum + m.usage, 0);
2537
- } else {
2538
- entry.messages = [];
2539
- entry.totalTokens = 0;
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.sessions.get(sessionId)?.messages ?? [];
2956
+ return this.getEntry(sessionId)?.messages ?? [];
2544
2957
  }
2545
- getAllSessionIds() {
2546
- return [...this.sessions.keys()];
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
- buildHistoryWindow(sessionId, topicGroups) {
2549
- const { topicRatio, historyWindowTokenLimit } = this.config;
2550
- const [n1, n2, n3] = topicRatio;
2551
- let remaining = historyWindowTokenLimit;
2552
- const parts = [];
2553
- const addTopics = (topics, field, max) => {
2554
- let count = 0;
2555
- for (const t of topics) {
2556
- if (count >= max || remaining <= 0) break;
2557
- const text = t[field];
2558
- const tokens = countTokens(text);
2559
- if (tokens > remaining) break;
2560
- parts.push(text);
2561
- remaining -= tokens;
2562
- count++;
2563
- }
2564
- };
2565
- addTopics(topicGroups.detail, "detail", n1);
2566
- addTopics(topicGroups.summary, "summary", n2);
2567
- addTopics(topicGroups.concise, "concise", n3);
2568
- this.historyWindows.set(sessionId, parts.join("\n\n"));
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.historyWindows.get(sessionId) ?? "";
3040
+ return this.legacyHistoryWindows.get(sessionId) ?? this.buildCompressedContext(sessionId);
2572
3041
  }
3042
+ /** @deprecated 仅为 0.3.x 兼容。 */
2573
3043
  setHistoryWindow(sessionId, content) {
2574
- this.historyWindows.set(sessionId, content);
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
- void this.runBackgroundOptimize(this.config.optimizeVersionRetentionMs);
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
- void this.runBackgroundOptimize(retention);
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 restoreFromStorage() {
2664
- const [n1, n2, n3] = this.config.topicRatio;
2665
- const sessionIds = await this.store.getAllSessionIds();
2666
- if (sessionIds.length === 0) return;
2667
- const topicsCache = /* @__PURE__ */ new Map();
2668
- const getTopics = (chatId, userId) => {
2669
- const key = `${chatId}\0${userId}`;
2670
- let cached = topicsCache.get(key);
2671
- if (!cached) {
2672
- cached = this.store.getRecentTopics(chatId, userId, n1, n2, n3);
2673
- topicsCache.set(key, cached);
2674
- }
2675
- return cached;
2676
- };
2677
- const sem = new Semaphore(this.config.restoreConcurrency);
2678
- await Promise.all(
2679
- sessionIds.map(
2680
- (sessionId) => sem.run(async () => {
2681
- const recentMessages = await this.store.getLatestMessages(sessionId, 1);
2682
- if (recentMessages.length === 0) return;
2683
- const { chatId, userId } = recentMessages[0];
2684
- const topicGroups = await getTopics(chatId, userId);
2685
- const n1EndTime = topicGroups.detail.length > 0 ? topicGroups.detail[topicGroups.detail.length - 1].endTime : 0;
2686
- const rawMessages = n1EndTime > 0 ? await this.store.getMessagesSince(sessionId, n1EndTime) : await this.store.getLatestMessages(sessionId, 100);
2687
- if (rawMessages.length > 0) {
2688
- this.sessionCache.addMessages(sessionId, rawMessages, chatId, userId);
2689
- }
2690
- if (topicGroups.detail.length + topicGroups.summary.length + topicGroups.concise.length > 0) {
2691
- this.sessionCache.buildHistoryWindow(sessionId, topicGroups);
2692
- } else {
2693
- this.sessionCache.setHistoryWindow(sessionId, "");
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
- async updateChat(messages, opts) {
2700
- if (messages.length === 0) return;
2701
- const userId = opts?.userId ?? DEFAULT_USER_ID;
2702
- const chatId = opts?.chatId ?? DEFAULT_CHAT_ID;
2703
- const sessionId = opts?.sessionId ?? DEFAULT_SESSION_ID;
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
- const withUsage = messages.map((m) => ({
2706
- messageId: m.messageId ?? uuidv44(),
2707
- talkerId: m.talkerId ?? "user",
2708
- chatId: m.chatId ?? chatId,
2709
- userId: m.userId ?? userId,
2710
- sessionId: m.sessionId ?? sessionId,
2711
- type: m.type ?? "text",
2712
- content: m.content,
2713
- parts: JSON.stringify(m.parts ?? []),
2714
- usage: m.usage ?? countTokens(m.content),
2715
- metadata: JSON.stringify(m.metadata ?? {}),
2716
- createdAt: m.createdAt ?? now
2717
- }));
2718
- let vectors;
2719
- try {
2720
- vectors = await this.embed.embed(withUsage.map((m) => m.content));
2721
- } catch (err) {
2722
- console.error("[MemoryManager] Embedding failed:", err);
2723
- throw err;
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
- this.sessionMap.set(sessionId, this.deserializeSession(session));
2751
- this.store.insertSession(session).catch((err) => {
2752
- console.error("[MemoryManager] Failed to insert session:", err);
2753
- });
2754
- } else {
2755
- const view = this.sessionMap.get(sessionId);
2756
- view.updatedAt = now;
2757
- this.store.upsertSession(this.serializeSession(view)).catch((err) => {
2758
- console.error(`[MemoryManager] Failed to upsert session ${sessionId}:`, err);
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
- const promise = this.compressManager.triggerCompress(sid, true, opts?.waitGraph === true);
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) => topics.map((t) => ({
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: t.detail,
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
- summaryId: t.summaryId,
2827
- sessionId: t.sessionId,
2828
- chatId: t.chatId,
2829
- userId: t.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((f) => `${new Date(f.createdAt).toISOString()}\uFF1A${f.content}`).join("\n");
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
- getHistoryWindow(sessionId) {
2891
- return this.sessionCache.getHistoryWindow(sessionId);
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.grafeo.close();
3068
- void this.store.close().catch(() => {
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
  };