@ppagent/memory 0.1.1 → 0.1.4

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.
Files changed (4) hide show
  1. package/dist/index.d.ts +173 -117
  2. package/dist/index.js +355 -159
  3. package/llms.txt +845 -0
  4. package/package.json +10 -8
package/dist/index.js CHANGED
@@ -25,9 +25,10 @@ function resolveConfig(config) {
25
25
  detailMaxTokens: config.detailMaxTokens ?? 2048,
26
26
  summaryMaxTokens: config.summaryMaxTokens ?? 512,
27
27
  conciseMaxTokens: config.conciseMaxTokens ?? 128,
28
- httpTimeoutMs: config.httpTimeoutMs ?? 3e4,
28
+ httpTimeoutMs: config.httpTimeoutMs ?? 6e4,
29
29
  httpMaxRetries: config.httpMaxRetries ?? 2,
30
30
  embeddingBatchSize: config.embeddingBatchSize ?? 20,
31
+ embeddingConcurrency: config.embeddingConcurrency ?? 2,
31
32
  maxConcurrentCompressions: config.maxConcurrentCompressions ?? 3,
32
33
  entitySimilarityThreshold: config.entitySimilarityThreshold ?? 0.92,
33
34
  defaultSearchLimit: config.defaultSearchLimit ?? 10,
@@ -42,7 +43,13 @@ function resolveConfig(config) {
42
43
  knowledgeGraphTriggerScore: config.knowledgeGraphTriggerScore ?? 0.78,
43
44
  knowledgeGraphEntityTopK: config.knowledgeGraphEntityTopK ?? 10,
44
45
  knowledgeGraphAnchorTopK: config.knowledgeGraphAnchorTopK ?? 3,
45
- knowledgeGraphHopLimit: config.knowledgeGraphHopLimit ?? 10
46
+ knowledgeGraphHopLimit: config.knowledgeGraphHopLimit ?? 10,
47
+ graphExtractConcurrency: config.graphExtractConcurrency ?? 2,
48
+ graphBuildTimeoutMs: config.graphBuildTimeoutMs ?? 12e4,
49
+ autoOptimizeOnInit: config.autoOptimizeOnInit ?? true,
50
+ autoOptimizeIntervalMs: config.autoOptimizeIntervalMs ?? 6 * 36e5,
51
+ optimizeVersionRetentionMs: config.optimizeVersionRetentionMs ?? 0,
52
+ restoreConcurrency: config.restoreConcurrency ?? 8
46
53
  };
47
54
  }
48
55
 
@@ -162,6 +169,16 @@ import { GrafeoDB } from "@grafeo-db/js";
162
169
  import { get_encoding } from "@dqbd/tiktoken";
163
170
 
164
171
  // src/llm/http.ts
172
+ function normalizeFetchError(err, timeoutMs, errorLabel) {
173
+ if (isAbortError(err)) {
174
+ return new Error(`${errorLabel} request timed out after ${timeoutMs}ms`, { cause: err });
175
+ }
176
+ if (err instanceof Error) return err;
177
+ return new Error(String(err));
178
+ }
179
+ function isAbortError(err) {
180
+ return typeof err === "object" && err !== null && "name" in err && err.name === "AbortError";
181
+ }
165
182
  function backoffDelay(attempt) {
166
183
  return Math.min(1e3 * 2 ** attempt, 8e3);
167
184
  }
@@ -170,7 +187,9 @@ async function postJsonWithRetry(url, headers, body, opts, errorLabel = "HTTP")
170
187
  let lastErr;
171
188
  for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
172
189
  const controller = new AbortController();
173
- const timer = setTimeout(() => controller.abort(), opts.timeoutMs);
190
+ const timer = setTimeout(() => {
191
+ controller.abort(new Error(`${errorLabel} request timed out after ${opts.timeoutMs}ms`));
192
+ }, opts.timeoutMs);
174
193
  let res;
175
194
  try {
176
195
  res = await fetch(url, {
@@ -181,12 +200,12 @@ async function postJsonWithRetry(url, headers, body, opts, errorLabel = "HTTP")
181
200
  });
182
201
  } catch (err) {
183
202
  clearTimeout(timer);
184
- lastErr = err;
203
+ lastErr = normalizeFetchError(err, opts.timeoutMs, errorLabel);
185
204
  if (attempt < opts.maxRetries) {
186
205
  await sleep(backoffDelay(attempt));
187
206
  continue;
188
207
  }
189
- throw err;
208
+ throw lastErr;
190
209
  }
191
210
  clearTimeout(timer);
192
211
  if (res.ok) {
@@ -226,27 +245,45 @@ var EmbedService = class {
226
245
  const inputs = Array.isArray(texts) ? texts : [texts];
227
246
  const url = `${this.config.embeddingBaseUrl.replace(/\/$/, "")}/embeddings`;
228
247
  const batchSize = Math.max(1, this.config.embeddingBatchSize);
248
+ const concurrency = Math.max(1, this.config.embeddingConcurrency);
229
249
  const retryOpts = { timeoutMs: this.config.httpTimeoutMs, maxRetries: this.config.httpMaxRetries };
230
- const out = [];
250
+ const batches = [];
231
251
  for (let i = 0; i < inputs.length; i += batchSize) {
232
- const batch = inputs.slice(i, i + batchSize);
252
+ batches.push({ index: batches.length, texts: inputs.slice(i, i + batchSize) });
253
+ }
254
+ const batchResults = await mapLimit(batches, concurrency, async (batch) => {
233
255
  const data = await postJsonWithRetry(
234
256
  url,
235
257
  { Authorization: `Bearer ${this.config.embeddingApiKey}` },
236
- { model: this.config.embeddingModel, input: batch },
258
+ { model: this.config.embeddingModel, input: batch.texts },
237
259
  retryOpts,
238
260
  "Embedding API"
239
261
  );
240
- const sorted = data.data.sort((a, b) => a.index - b.index).map((d) => d.embedding);
241
- out.push(...sorted);
242
- }
243
- return out;
262
+ return {
263
+ index: batch.index,
264
+ vectors: data.data.sort((a, b) => a.index - b.index).map((d) => d.embedding)
265
+ };
266
+ });
267
+ return batchResults.sort((a, b) => a.index - b.index).flatMap((b) => b.vectors);
244
268
  }
245
269
  async embedOne(text) {
246
270
  const result = await this.embed([text]);
247
271
  return result[0];
248
272
  }
249
273
  };
274
+ async function mapLimit(items, limit, mapper) {
275
+ const results = new Array(items.length);
276
+ let next = 0;
277
+ const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
278
+ while (true) {
279
+ const index = next++;
280
+ if (index >= items.length) return;
281
+ results[index] = await mapper(items[index], index);
282
+ }
283
+ });
284
+ await Promise.all(workers);
285
+ return results;
286
+ }
250
287
  function cosineSimilarity(a, b) {
251
288
  let dot = 0;
252
289
  let normA = 0;
@@ -979,6 +1016,24 @@ function safeParseObject(s) {
979
1016
  return {};
980
1017
  }
981
1018
  }
1019
+ var MESSAGES_INDEXES = [
1020
+ { column: "message_id" },
1021
+ { column: "session_id" },
1022
+ { column: "content", fts: true },
1023
+ { column: "metadata", fts: true }
1024
+ // 支持 metadata 内容全文检索
1025
+ ];
1026
+ var TOPICS_INDEXES = [
1027
+ { column: "chat_id" },
1028
+ { column: "detail", fts: true }
1029
+ ];
1030
+ var SESSIONS_INDEXES = [{ column: "session_id" }];
1031
+ var DOCUMENTS_INDEXES = [{ column: "doc_id" }];
1032
+ var CHUNKS_INDEXES = [
1033
+ { column: "doc_id" },
1034
+ { column: "content", fts: true }
1035
+ // 知识片段混合检索
1036
+ ];
982
1037
  var LanceService = class {
983
1038
  config;
984
1039
  conn;
@@ -991,6 +1046,7 @@ var LanceService = class {
991
1046
  // Scalar indexes cannot be created on empty tables (LanceDB btree limitation).
992
1047
  // These flags defer creation to the first insert.
993
1048
  isNewMessagesTable = false;
1049
+ isNewTopicsTable = false;
994
1050
  isNewSessionsTable = false;
995
1051
  isNewDocumentsTable = false;
996
1052
  isNewChunksTable = false;
@@ -1004,7 +1060,6 @@ var LanceService = class {
1004
1060
  if (existingTables.includes(MESSAGES_TABLE)) {
1005
1061
  this.messagesTable = await this.conn.openTable(MESSAGES_TABLE);
1006
1062
  await this._ensurePartsColumn();
1007
- await this.ensureScalarIndex(this.messagesTable, "message_id");
1008
1063
  } else {
1009
1064
  this.messagesTable = await this.conn.createEmptyTable(
1010
1065
  MESSAGES_TABLE,
@@ -1020,6 +1075,7 @@ var LanceService = class {
1020
1075
  TOPICS_TABLE,
1021
1076
  topicsSchema(dim)
1022
1077
  );
1078
+ this.isNewTopicsTable = true;
1023
1079
  }
1024
1080
  if (existingTables.includes(FACTS_TABLE)) {
1025
1081
  this.factsTable = await this.conn.openTable(FACTS_TABLE);
@@ -1028,43 +1084,83 @@ var LanceService = class {
1028
1084
  }
1029
1085
  if (existingTables.includes(SESSIONS_TABLE)) {
1030
1086
  this.sessionsTable = await this.conn.openTable(SESSIONS_TABLE);
1031
- await this.ensureScalarIndex(this.sessionsTable, "session_id");
1032
1087
  } else {
1033
1088
  this.sessionsTable = await this.conn.createEmptyTable(SESSIONS_TABLE, sessionsSchema());
1034
1089
  this.isNewSessionsTable = true;
1035
1090
  }
1036
1091
  if (existingTables.includes(DOCUMENTS_TABLE)) {
1037
1092
  this.documentsTable = await this.conn.openTable(DOCUMENTS_TABLE);
1038
- await this.ensureScalarIndex(this.documentsTable, "doc_id");
1039
1093
  } else {
1040
1094
  this.documentsTable = await this.conn.createEmptyTable(DOCUMENTS_TABLE, documentsSchema(dim));
1041
1095
  this.isNewDocumentsTable = true;
1042
1096
  }
1043
1097
  if (existingTables.includes(CHUNKS_TABLE)) {
1044
1098
  this.chunksTable = await this.conn.openTable(CHUNKS_TABLE);
1045
- await this.ensureScalarIndex(this.chunksTable, "doc_id");
1046
1099
  } else {
1047
1100
  this.chunksTable = await this.conn.createEmptyTable(CHUNKS_TABLE, chunksSchema(dim));
1048
1101
  this.isNewChunksTable = true;
1049
1102
  }
1050
- await this.ensureFtsIndexes();
1103
+ await this.ensureIndexes(this.messagesTable, MESSAGES_INDEXES);
1104
+ await this.ensureIndexes(this.topicsTable, TOPICS_INDEXES);
1105
+ await this.ensureIndexes(this.sessionsTable, SESSIONS_INDEXES);
1106
+ await this.ensureIndexes(this.documentsTable, DOCUMENTS_INDEXES);
1107
+ await this.ensureIndexes(this.chunksTable, CHUNKS_INDEXES);
1051
1108
  }
1052
- async ensureFtsIndexes() {
1053
- const ftsTargets = [
1054
- { table: this.messagesTable, column: "content" },
1055
- { table: this.messagesTable, column: "metadata" },
1056
- // 支持 metadata 内容全文检索
1057
- { table: this.topicsTable, column: "detail" },
1058
- { table: this.chunksTable, column: "content" }
1059
- // 知识片段混合检索
1060
- ];
1061
- for (const { table, column } of ftsTargets) {
1109
+ /**
1110
+ * 按需补齐索引:先经 listIndices 判存在,缺失的列才 createIndex(且 replace:false)。
1111
+ * createIndex 默认 replace:true 会在每次启动时全量重建索引并提交新表版本——
1112
+ * 这正是历史上「启动越来越慢 + _versions 目录膨胀」的根源,绝不可回退到无条件 createIndex。
1113
+ */
1114
+ async ensureIndexes(table, specs) {
1115
+ let indexed;
1116
+ try {
1117
+ indexed = new Set((await table.listIndices()).flatMap((i) => i.columns));
1118
+ } catch {
1119
+ indexed = /* @__PURE__ */ new Set();
1120
+ }
1121
+ for (const { column, fts } of specs) {
1122
+ if (indexed.has(column)) continue;
1062
1123
  try {
1063
- await table.createIndex(column, { config: lancedb.Index.fts() });
1124
+ await table.createIndex(
1125
+ column,
1126
+ fts ? { config: lancedb.Index.fts(), replace: false } : { replace: false }
1127
+ );
1064
1128
  } catch {
1065
1129
  }
1066
1130
  }
1067
1131
  }
1132
+ /**
1133
+ * 存储压实:逐表执行碎片合并 + 清理 retentionMs 之前的历史版本。
1134
+ * 嵌入式场景下 LanceDB 不会自动做这件事,长期运行后版本/碎片无限累积会显著拖慢启动与查询。
1135
+ */
1136
+ async optimizeStorage(retentionMs = 0) {
1137
+ const cutoff = new Date(Date.now() - Math.max(0, retentionMs));
1138
+ const targets = [
1139
+ [MESSAGES_TABLE, this.messagesTable],
1140
+ [TOPICS_TABLE, this.topicsTable],
1141
+ [FACTS_TABLE, this.factsTable],
1142
+ [SESSIONS_TABLE, this.sessionsTable],
1143
+ [DOCUMENTS_TABLE, this.documentsTable],
1144
+ [CHUNKS_TABLE, this.chunksTable]
1145
+ ];
1146
+ const results = [];
1147
+ for (const [name, table] of targets) {
1148
+ try {
1149
+ const stats = await table.optimize({ cleanupOlderThan: cutoff });
1150
+ results.push({
1151
+ table: name,
1152
+ fragmentsRemoved: stats.compaction.fragmentsRemoved,
1153
+ fragmentsAdded: stats.compaction.fragmentsAdded,
1154
+ filesRemoved: stats.compaction.filesRemoved,
1155
+ oldVersionsRemoved: stats.prune.oldVersionsRemoved,
1156
+ bytesRemoved: Number(stats.prune.bytesRemoved)
1157
+ });
1158
+ } catch (err) {
1159
+ console.warn(`[LanceService] optimize table "${name}" failed:`, err);
1160
+ }
1161
+ }
1162
+ return results;
1163
+ }
1068
1164
  /**
1069
1165
  * 为存量 messages 表添加 parts 列(如果缺失)。
1070
1166
  * LanceDB 0.14+ 支持 addColumns;旧版本会 throw,此时 rowToMessage 的 ?? "[]" 兜底。
@@ -1101,24 +1197,20 @@ var LanceService = class {
1101
1197
  } catch {
1102
1198
  }
1103
1199
  }
1104
- // LanceDB btree scalar indexes require at least one row.
1105
- // For existing tables (opened in init) this is safe; for new tables we defer via isNew*Table flags.
1106
- async ensureScalarIndex(table, column) {
1107
- try {
1108
- await table.createIndex(column);
1109
- } catch {
1110
- }
1111
- }
1112
1200
  async addMessages(messages) {
1113
1201
  if (messages.length === 0) return;
1114
1202
  await this.messagesTable.add(messages.map(messageToRow));
1115
1203
  if (this.isNewMessagesTable) {
1116
- await this.ensureScalarIndex(this.messagesTable, "message_id");
1204
+ await this.ensureIndexes(this.messagesTable, MESSAGES_INDEXES);
1117
1205
  this.isNewMessagesTable = false;
1118
1206
  }
1119
1207
  }
1120
1208
  async addTopic(topic) {
1121
1209
  await this.topicsTable.add([topicToRow(topic)]);
1210
+ if (this.isNewTopicsTable) {
1211
+ await this.ensureIndexes(this.topicsTable, TOPICS_INDEXES);
1212
+ this.isNewTopicsTable = false;
1213
+ }
1122
1214
  }
1123
1215
  async updateTopicRecallCount(summaryId, count) {
1124
1216
  await this.topicsTable.update({
@@ -1160,9 +1252,13 @@ var LanceService = class {
1160
1252
  }
1161
1253
  }
1162
1254
  async getLatestMessages(sessionId, limit) {
1255
+ if (limit <= 0) return [];
1163
1256
  try {
1164
- const rows = await this.messagesTable.query().where(eqFilter("session_id", sessionId)).limit(limit * 3).toArray();
1165
- return rows.sort((a, b) => Number(b.created_at) - Number(a.created_at)).slice(0, limit).reverse().map(rowToMessage);
1257
+ const rows = await this.messagesTable.query().where(eqFilter("session_id", sessionId)).orderBy([
1258
+ { columnName: "created_at", ascending: false },
1259
+ { columnName: "message_id", ascending: false }
1260
+ ]).limit(limit).toArray();
1261
+ return rows.reverse().map(rowToMessage);
1166
1262
  } catch {
1167
1263
  return [];
1168
1264
  }
@@ -1251,7 +1347,7 @@ var LanceService = class {
1251
1347
  async insertSession(session) {
1252
1348
  await this.sessionsTable.add([sessionToRow(session)]);
1253
1349
  if (this.isNewSessionsTable) {
1254
- await this.ensureScalarIndex(this.sessionsTable, "session_id");
1350
+ await this.ensureIndexes(this.sessionsTable, SESSIONS_INDEXES);
1255
1351
  this.isNewSessionsTable = false;
1256
1352
  }
1257
1353
  }
@@ -1357,7 +1453,7 @@ var LanceService = class {
1357
1453
  async addDocument(doc) {
1358
1454
  await this.documentsTable.add([documentToRow(doc)]);
1359
1455
  if (this.isNewDocumentsTable) {
1360
- await this.ensureScalarIndex(this.documentsTable, "doc_id");
1456
+ await this.ensureIndexes(this.documentsTable, DOCUMENTS_INDEXES);
1361
1457
  this.isNewDocumentsTable = false;
1362
1458
  }
1363
1459
  }
@@ -1365,7 +1461,7 @@ var LanceService = class {
1365
1461
  if (chunks.length === 0) return;
1366
1462
  await this.chunksTable.add(chunks.map(chunkToRow));
1367
1463
  if (this.isNewChunksTable) {
1368
- await this.ensureScalarIndex(this.chunksTable, "doc_id");
1464
+ await this.ensureIndexes(this.chunksTable, CHUNKS_INDEXES);
1369
1465
  this.isNewChunksTable = false;
1370
1466
  }
1371
1467
  }
@@ -1488,6 +1584,40 @@ var LanceService = class {
1488
1584
  }
1489
1585
  };
1490
1586
 
1587
+ // src/manager/semaphore.ts
1588
+ var Semaphore = class {
1589
+ count;
1590
+ queue = [];
1591
+ constructor(max) {
1592
+ this.count = max;
1593
+ }
1594
+ async run(fn) {
1595
+ await this.acquire();
1596
+ try {
1597
+ return await fn();
1598
+ } finally {
1599
+ this.release();
1600
+ }
1601
+ }
1602
+ acquire() {
1603
+ if (this.count > 0) {
1604
+ this.count--;
1605
+ return Promise.resolve();
1606
+ }
1607
+ return new Promise((resolve) => {
1608
+ this.queue.push(resolve);
1609
+ });
1610
+ }
1611
+ release() {
1612
+ const next = this.queue.shift();
1613
+ if (next) {
1614
+ next();
1615
+ } else {
1616
+ this.count++;
1617
+ }
1618
+ }
1619
+ };
1620
+
1491
1621
  // src/llm/llm.service.ts
1492
1622
  var ENTITY_EXTRACTOR_TOOL = {
1493
1623
  type: "function",
@@ -1602,7 +1732,7 @@ var LlmService = class {
1602
1732
  return data.choices[0].message;
1603
1733
  }
1604
1734
  // ── 第一步:生成三级摘要(JSON 输出)──────────────────────────────────────────
1605
- async getSummary(messages) {
1735
+ async summarizeMessages(messages) {
1606
1736
  const now = (/* @__PURE__ */ new Date()).toISOString();
1607
1737
  const { detailMaxTokens, summaryMaxTokens, conciseMaxTokens } = this.config;
1608
1738
  const systemPrompt = `\u4F60\u662F\u4E00\u4E2A\u5BF9\u8BDD\u8BB0\u5FC6\u7BA1\u7406\u52A9\u624B\u3002\u5F53\u524D\u7CFB\u7EDF\u65F6\u95F4\uFF1A${now}\u3002
@@ -1624,7 +1754,7 @@ ${formatted}`);
1624
1754
  return { title: parsed.title ?? "", detail: parsed.detail, summary: parsed.summary, concise: parsed.concise };
1625
1755
  }
1626
1756
  // ── 第二步:通过工具调用提取实体和关系 ──────────────────────────────────────
1627
- async extractEntitiesWithTool(messages) {
1757
+ async extractEntitiesFromMessages(messages) {
1628
1758
  const now = (/* @__PURE__ */ new Date()).toISOString();
1629
1759
  const systemPrompt = `\u4F60\u662F\u4E00\u4E2A\u5B9E\u4F53\u5173\u7CFB\u62BD\u53D6\u52A9\u624B\u3002\u5F53\u524D\u7CFB\u7EDF\u65F6\u95F4\uFF1A${now}\u3002
1630
1760
  \u68C0\u67E5\u7ED9\u5B9A\u7684\u5BF9\u8BDD\u5185\u5BB9\uFF0C\u5224\u65AD\u5176\u4E2D\u662F\u5426\u5B58\u5728\u503C\u5F97\u8BB0\u5F55\u7684\u547D\u540D\u5B9E\u4F53\uFF08\u4EBA\u7269\u3001\u7EC4\u7EC7\u3001\u9879\u76EE\u3001\u5730\u70B9\u3001\u6280\u672F\u7B49\uFF09\u548C\u5B83\u4EEC\u4E4B\u95F4\u7684\u5173\u7CFB\u3002
@@ -1652,15 +1782,15 @@ ${formatted}`
1652
1782
  }
1653
1783
  }
1654
1784
  } catch (err) {
1655
- console.error("[LlmService] extractEntitiesWithTool failed:", err);
1785
+ console.error("[LlmService] extractEntitiesFromMessages failed:", err);
1656
1786
  }
1657
1787
  return { entities: [], relations: [] };
1658
1788
  }
1659
1789
  // ── 对外主接口:压缩 + 实体抽取(并发执行两步)────────────────────────────────
1660
1790
  async compress(messages) {
1661
1791
  const [summary, extraction] = await Promise.all([
1662
- this.getSummary(messages),
1663
- this.extractEntitiesWithTool(messages)
1792
+ this.summarizeMessages(messages),
1793
+ this.extractEntitiesFromMessages(messages)
1664
1794
  ]);
1665
1795
  return {
1666
1796
  title: summary.title,
@@ -1758,42 +1888,6 @@ ${context}`, false);
1758
1888
 
1759
1889
  // src/manager/compress.manager.ts
1760
1890
  import { v4 as uuidv4 } from "uuid";
1761
-
1762
- // src/manager/semaphore.ts
1763
- var Semaphore = class {
1764
- count;
1765
- queue = [];
1766
- constructor(max) {
1767
- this.count = max;
1768
- }
1769
- async run(fn) {
1770
- await this.acquire();
1771
- try {
1772
- return await fn();
1773
- } finally {
1774
- this.release();
1775
- }
1776
- }
1777
- acquire() {
1778
- if (this.count > 0) {
1779
- this.count--;
1780
- return Promise.resolve();
1781
- }
1782
- return new Promise((resolve) => {
1783
- this.queue.push(resolve);
1784
- });
1785
- }
1786
- release() {
1787
- const next = this.queue.shift();
1788
- if (next) {
1789
- next();
1790
- } else {
1791
- this.count++;
1792
- }
1793
- }
1794
- };
1795
-
1796
- // src/manager/compress.manager.ts
1797
1891
  var CompressManager = class {
1798
1892
  config;
1799
1893
  lance;
@@ -1812,17 +1906,17 @@ var CompressManager = class {
1812
1906
  this.sessionCache = sessionCache;
1813
1907
  this.semaphore = new Semaphore(config.maxConcurrentCompressions);
1814
1908
  }
1815
- triggerCompress(sessionId, force = false) {
1909
+ triggerCompress(sessionId, force = false, waitGraph = false) {
1816
1910
  const chain = this.sessionChain.get(sessionId) ?? Promise.resolve();
1817
1911
  const next = chain.then(
1818
- () => this.semaphore.run(() => this.doCompress(sessionId, force))
1912
+ () => this.semaphore.run(() => this.doCompress(sessionId, force, waitGraph))
1819
1913
  );
1820
1914
  this.sessionChain.set(sessionId, next.catch(() => {
1821
1915
  }));
1822
1916
  return next;
1823
1917
  }
1824
- async doCompress(sessionId, force) {
1825
- const messages = this.sessionCache.getSessionMessages(sessionId);
1918
+ async doCompress(sessionId, force, waitGraph) {
1919
+ const messages = this.sessionCache.getSessionMessages(sessionId).slice();
1826
1920
  if (messages.length === 0) return;
1827
1921
  const entry = this.sessionCache.getEntry(sessionId);
1828
1922
  if (!entry) return;
@@ -1830,15 +1924,15 @@ var CompressManager = class {
1830
1924
  const { chatId, userId } = entry.ids;
1831
1925
  const startTime = messages[0].createdAt;
1832
1926
  const endTime = messages[messages.length - 1].createdAt;
1833
- let compressOutput;
1927
+ let summary;
1834
1928
  try {
1835
- compressOutput = await this.llm.compress(messages);
1929
+ summary = await this.llm.summarizeMessages(messages);
1836
1930
  } catch (err) {
1837
- console.error(`[CompressManager] LLM compress failed for session ${sessionId}:`, err);
1931
+ console.error(`[CompressManager] LLM compress failed for session ${sessionId}: ${formatError(err)}`);
1838
1932
  return;
1839
1933
  }
1840
- const { title, detail, summary, concise, entities, relations } = compressOutput;
1841
- const topicTextForEmbed = [detail, summary, concise].find((t) => t.length > 0) ?? "";
1934
+ const { title, detail, summary: summaryText, concise } = summary;
1935
+ const topicTextForEmbed = [detail, summaryText, concise].find((t) => t.length > 0) ?? "";
1842
1936
  let topicVector = [];
1843
1937
  try {
1844
1938
  topicVector = await this.embed.embedOne(topicTextForEmbed);
@@ -1852,7 +1946,7 @@ var CompressManager = class {
1852
1946
  chatId,
1853
1947
  title,
1854
1948
  detail,
1855
- summary,
1949
+ summary: summaryText,
1856
1950
  concise,
1857
1951
  startTime,
1858
1952
  endTime,
@@ -1866,9 +1960,6 @@ var CompressManager = class {
1866
1960
  } catch (err) {
1867
1961
  console.error(`[CompressManager] Save topic failed:`, err);
1868
1962
  }
1869
- if (entities.length > 0 || relations.length > 0) {
1870
- await this.persistGraph(entities, relations, sessionId, chatId, userId, endTime);
1871
- }
1872
1963
  this.sessionCache.clearMessages(sessionId, endTime);
1873
1964
  const [n1, n2, n3] = this.config.topicRatio;
1874
1965
  try {
@@ -1877,6 +1968,19 @@ var CompressManager = class {
1877
1968
  } catch (err) {
1878
1969
  console.error(`[CompressManager] Rebuild history window failed:`, err);
1879
1970
  }
1971
+ const graphTask = this.extractAndPersistGraph(messages, sessionId, chatId, userId, endTime);
1972
+ if (waitGraph) {
1973
+ await withTimeout(graphTask, this.config.graphBuildTimeoutMs, "flushChat graph build");
1974
+ } else {
1975
+ graphTask.catch((err) => {
1976
+ console.error(`[CompressManager] Background graph persist failed:`, err);
1977
+ });
1978
+ }
1979
+ }
1980
+ async extractAndPersistGraph(messages, sessionId, chatId, userId, endTime) {
1981
+ const { entities, relations } = await this.llm.extractEntitiesFromMessages(messages);
1982
+ if (entities.length === 0 && relations.length === 0) return;
1983
+ await this.persistGraph(entities, relations, sessionId, chatId, userId, endTime);
1880
1984
  }
1881
1985
  async persistGraph(rawEntities, rawRelations, sessionId, chatId, userId, messageTime) {
1882
1986
  const entities = rawEntities.map((e) => ({
@@ -1919,6 +2023,16 @@ var CompressManager = class {
1919
2023
  }
1920
2024
  }
1921
2025
  };
2026
+ function formatError(err) {
2027
+ return err instanceof Error ? err.message : String(err);
2028
+ }
2029
+ function withTimeout(promise, timeoutMs, label) {
2030
+ let timer;
2031
+ const timeout = new Promise((_, reject) => {
2032
+ timer = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
2033
+ });
2034
+ return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
2035
+ }
1922
2036
 
1923
2037
  // src/manager/fact.cache.ts
1924
2038
  import { v4 as uuidv42 } from "uuid";
@@ -2178,20 +2292,29 @@ var KnowledgeManager = class {
2178
2292
  updatedAt: now
2179
2293
  };
2180
2294
  await this.lance.addDocument(doc);
2181
- const background = this.semaphore.run(
2182
- () => this.ingestChunksAndGraph(doc, pieces, shouldBuildGraph)
2183
- );
2184
- if (opts.wait) {
2185
- await background;
2295
+ const chunkIngest = this.semaphore.run(() => this.ingestChunks(doc, pieces));
2296
+ const graphBuild = chunkIngest.then((chunks) => {
2297
+ if (!shouldBuildGraph) return;
2298
+ return this.buildDocumentGraph(doc, chunks);
2299
+ });
2300
+ if (opts.wait || opts.waitGraph) {
2301
+ await chunkIngest;
2186
2302
  } else {
2187
- background.catch((err) => {
2188
- console.error("[KnowledgeManager] background ingest failed:", err);
2303
+ chunkIngest.catch((err) => {
2304
+ console.error("[KnowledgeManager] background chunk ingest failed:", err);
2305
+ });
2306
+ }
2307
+ if (opts.waitGraph) {
2308
+ await withTimeout2(graphBuild, this.config.graphBuildTimeoutMs, "document graph build");
2309
+ } else {
2310
+ graphBuild.catch((err) => {
2311
+ console.error("[KnowledgeManager] background graph build failed:", err);
2189
2312
  });
2190
2313
  }
2191
2314
  return { docId };
2192
2315
  }
2193
- async ingestChunksAndGraph(doc, pieces, buildGraph) {
2194
- if (pieces.length === 0) return;
2316
+ async ingestChunks(doc, pieces) {
2317
+ if (pieces.length === 0) return [];
2195
2318
  const redundant = this.config.chunkRedundantIds;
2196
2319
  const embedInputs = pieces.map(
2197
2320
  (p) => p.headingPath ? `${p.headingPath}
@@ -2202,7 +2325,7 @@ ${p.content}` : p.content
2202
2325
  vectors = await this.embed.embed(embedInputs);
2203
2326
  } catch (err) {
2204
2327
  console.error("[KnowledgeManager] embed chunks failed:", err);
2205
- return;
2328
+ return [];
2206
2329
  }
2207
2330
  const chunks = pieces.map((p, i) => ({
2208
2331
  chunkId: uuidv43(),
@@ -2219,17 +2342,12 @@ ${p.content}` : p.content
2219
2342
  createdAt: doc.createdAt
2220
2343
  }));
2221
2344
  await this.lance.addChunks(chunks);
2222
- if (buildGraph) {
2223
- await this.buildDocumentGraph(doc, chunks).catch((err) => {
2224
- console.error("[KnowledgeManager] build graph failed:", err);
2225
- });
2226
- }
2345
+ return chunks;
2227
2346
  }
2228
2347
  async buildDocumentGraph(doc, chunks) {
2229
- let extractedAny = false;
2230
- for (const chunk of chunks) {
2348
+ const extracted = await mapLimit2(chunks, this.config.graphExtractConcurrency, async (chunk) => {
2231
2349
  const { entities: rawEntities, relations: rawRelations } = await this.llm.extractEntitiesFromText(chunk.content);
2232
- if (rawEntities.length === 0 && rawRelations.length === 0) continue;
2350
+ if (rawEntities.length === 0 && rawRelations.length === 0) return null;
2233
2351
  const meta = {
2234
2352
  userId: doc.userId,
2235
2353
  chatId: doc.chatId,
@@ -2238,46 +2356,47 @@ ${p.content}` : p.content
2238
2356
  chunkId: chunk.chunkId,
2239
2357
  messageTime: doc.createdAt
2240
2358
  };
2241
- const entities = rawEntities.map((e) => ({
2359
+ const entities2 = rawEntities.map((e) => ({
2242
2360
  name: e.name,
2243
2361
  type: e.type,
2244
2362
  meta: { ...meta, ...e.meta }
2245
2363
  }));
2246
- const relations = rawRelations.map((r) => ({
2364
+ const relations2 = rawRelations.map((r) => ({
2247
2365
  from: r.from,
2248
2366
  to: r.to,
2249
2367
  type: r.type,
2250
2368
  happenedAt: r.happenedAt ?? void 0,
2251
2369
  meta: { ...meta, ...r.meta }
2252
2370
  }));
2253
- const allNames = [
2254
- .../* @__PURE__ */ new Set([
2255
- ...entities.map((e) => e.name),
2256
- ...relations.flatMap((r) => [r.from, r.to])
2257
- ])
2258
- ];
2259
- if (allNames.length === 0) continue;
2260
- const embeddings = /* @__PURE__ */ new Map();
2261
- try {
2262
- const vecs = await this.embed.embed(allNames);
2263
- allNames.forEach((name, i) => embeddings.set(name, vecs[i]));
2264
- } catch (err) {
2265
- console.error("[KnowledgeManager] embed entity names failed:", err);
2266
- continue;
2267
- }
2268
- await this.grafeo.upsertEntitiesAndRelations(
2269
- entities,
2270
- relations,
2271
- embeddings,
2272
- KIND_KNOWLEDGE
2273
- );
2274
- extractedAny = true;
2275
- }
2276
- if (extractedAny) {
2277
- await this.lance.updateDocumentGraphFlag(doc.docId, true).catch((err) => {
2278
- console.error(`[KnowledgeManager] Failed to update graph flag for doc ${doc.docId}:`, err);
2279
- });
2371
+ return { entities: entities2, relations: relations2 };
2372
+ });
2373
+ const entities = extracted.flatMap((item) => item?.entities ?? []);
2374
+ const relations = extracted.flatMap((item) => item?.relations ?? []);
2375
+ if (entities.length === 0 && relations.length === 0) return;
2376
+ const allNames = [
2377
+ .../* @__PURE__ */ new Set([
2378
+ ...entities.map((e) => e.name),
2379
+ ...relations.flatMap((r) => [r.from, r.to])
2380
+ ])
2381
+ ];
2382
+ if (allNames.length === 0) return;
2383
+ const embeddings = /* @__PURE__ */ new Map();
2384
+ try {
2385
+ const vecs = await this.embed.embed(allNames);
2386
+ allNames.forEach((name, i) => embeddings.set(name, vecs[i]));
2387
+ } catch (err) {
2388
+ console.error("[KnowledgeManager] embed entity names failed:", err);
2389
+ return;
2280
2390
  }
2391
+ await this.grafeo.upsertEntitiesAndRelations(
2392
+ entities,
2393
+ relations,
2394
+ embeddings,
2395
+ KIND_KNOWLEDGE
2396
+ );
2397
+ await this.lance.updateDocumentGraphFlag(doc.docId, true).catch((err) => {
2398
+ console.error(`[KnowledgeManager] Failed to update graph flag for doc ${doc.docId}:`, err);
2399
+ });
2281
2400
  }
2282
2401
  // ── 检索 ───────────────────────────────────────────────────────────────────────
2283
2402
  async searchKnowledge(opts) {
@@ -2439,6 +2558,26 @@ function inferTitle(markdown) {
2439
2558
  }
2440
2559
  return void 0;
2441
2560
  }
2561
+ async function mapLimit2(items, limit, mapper) {
2562
+ const results = new Array(items.length);
2563
+ let next = 0;
2564
+ const workers = Array.from({ length: Math.min(Math.max(1, limit), items.length) }, async () => {
2565
+ while (true) {
2566
+ const index = next++;
2567
+ if (index >= items.length) return;
2568
+ results[index] = await mapper(items[index], index);
2569
+ }
2570
+ });
2571
+ await Promise.all(workers);
2572
+ return results;
2573
+ }
2574
+ function withTimeout2(promise, timeoutMs, label) {
2575
+ let timer;
2576
+ const timeout = new Promise((_, reject) => {
2577
+ timer = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
2578
+ });
2579
+ return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
2580
+ }
2442
2581
 
2443
2582
  // src/manager/session.cache.ts
2444
2583
  var SessionCache = class {
@@ -2524,6 +2663,8 @@ var MemoryManager = class {
2524
2663
  compressManager;
2525
2664
  knowledgeManager;
2526
2665
  sessionMap = /* @__PURE__ */ new Map();
2666
+ optimizeTimer;
2667
+ optimizeRunning = false;
2527
2668
  constructor(config) {
2528
2669
  this.config = resolveConfig(config);
2529
2670
  this.lance = new LanceService(this.config);
@@ -2558,27 +2699,78 @@ var MemoryManager = class {
2558
2699
  this.sessionMap.set(s.sessionId, this.deserializeSession(s));
2559
2700
  }
2560
2701
  await this.restoreFromStorage();
2702
+ if (this.config.autoOptimizeOnInit) {
2703
+ void this.runBackgroundOptimize(this.config.optimizeVersionRetentionMs);
2704
+ }
2705
+ if (this.config.autoOptimizeIntervalMs > 0) {
2706
+ const retention = Math.max(this.config.optimizeVersionRetentionMs, 6e4);
2707
+ this.optimizeTimer = setInterval(() => {
2708
+ void this.runBackgroundOptimize(retention);
2709
+ }, this.config.autoOptimizeIntervalMs);
2710
+ this.optimizeTimer.unref?.();
2711
+ }
2712
+ }
2713
+ /** 后台压实的统一入口:防重入(上一轮未结束则跳过),失败仅告警不影响服务。*/
2714
+ async runBackgroundOptimize(retentionMs) {
2715
+ if (this.optimizeRunning) return;
2716
+ this.optimizeRunning = true;
2717
+ try {
2718
+ const results = await this.optimizeStorage(retentionMs);
2719
+ const versions = results.reduce((s, r) => s + r.oldVersionsRemoved, 0);
2720
+ const fragments = results.reduce((s, r) => s + r.fragmentsRemoved, 0);
2721
+ if (versions > 0 || fragments > 0) {
2722
+ console.info(
2723
+ `[MemoryManager] \u540E\u53F0\u5B58\u50A8\u538B\u5B9E\u5B8C\u6210\uFF1A\u6E05\u7406\u5386\u53F2\u7248\u672C ${versions} \u4E2A\uFF0C\u5408\u5E76\u788E\u7247 ${fragments} \u4E2A`
2724
+ );
2725
+ }
2726
+ } catch (err) {
2727
+ console.warn("[MemoryManager] \u540E\u53F0\u5B58\u50A8\u538B\u5B9E\u5931\u8D25\uFF08\u4E0D\u5F71\u54CD\u670D\u52A1\uFF09:", err);
2728
+ } finally {
2729
+ this.optimizeRunning = false;
2730
+ }
2731
+ }
2732
+ /**
2733
+ * 手动触发存储压实与历史版本清理(init 后台会自动执行一次;上层维护任务也可调用)。
2734
+ * @param retentionMs 保留多久内的历史版本,默认取配置 optimizeVersionRetentionMs
2735
+ */
2736
+ async optimizeStorage(retentionMs) {
2737
+ return this.lance.optimizeStorage(retentionMs ?? this.config.optimizeVersionRetentionMs);
2561
2738
  }
2562
2739
  async restoreFromStorage() {
2563
2740
  const [n1, n2, n3] = this.config.topicRatio;
2564
2741
  const sessionIds = await this.lance.getAllSessionIds();
2565
2742
  if (sessionIds.length === 0) return;
2566
- for (const sessionId of sessionIds) {
2567
- const recentMessages = await this.lance.getLatestMessages(sessionId, 1);
2568
- if (recentMessages.length === 0) continue;
2569
- const { chatId, userId } = recentMessages[0];
2570
- const topicGroups = await this.lance.getRecentTopics(chatId, userId, n1, n2, n3);
2571
- const n1EndTime = topicGroups.detail.length > 0 ? topicGroups.detail[topicGroups.detail.length - 1].endTime : 0;
2572
- const rawMessages = n1EndTime > 0 ? await this.lance.getMessagesSince(sessionId, n1EndTime) : await this.lance.getLatestMessages(sessionId, 100);
2573
- if (rawMessages.length > 0) {
2574
- this.sessionCache.addMessages(sessionId, rawMessages, chatId, userId);
2575
- }
2576
- if (topicGroups.detail.length + topicGroups.summary.length + topicGroups.concise.length > 0) {
2577
- this.sessionCache.buildHistoryWindow(sessionId, topicGroups);
2578
- } else {
2579
- this.sessionCache.setHistoryWindow(sessionId, "");
2743
+ const topicsCache = /* @__PURE__ */ new Map();
2744
+ const getTopics = (chatId, userId) => {
2745
+ const key = `${chatId}\0${userId}`;
2746
+ let cached = topicsCache.get(key);
2747
+ if (!cached) {
2748
+ cached = this.lance.getRecentTopics(chatId, userId, n1, n2, n3);
2749
+ topicsCache.set(key, cached);
2580
2750
  }
2581
- }
2751
+ return cached;
2752
+ };
2753
+ const sem = new Semaphore(this.config.restoreConcurrency);
2754
+ await Promise.all(
2755
+ sessionIds.map(
2756
+ (sessionId) => sem.run(async () => {
2757
+ const recentMessages = await this.lance.getLatestMessages(sessionId, 1);
2758
+ if (recentMessages.length === 0) return;
2759
+ const { chatId, userId } = recentMessages[0];
2760
+ const topicGroups = await getTopics(chatId, userId);
2761
+ const n1EndTime = topicGroups.detail.length > 0 ? topicGroups.detail[topicGroups.detail.length - 1].endTime : 0;
2762
+ const rawMessages = n1EndTime > 0 ? await this.lance.getMessagesSince(sessionId, n1EndTime) : await this.lance.getLatestMessages(sessionId, 100);
2763
+ if (rawMessages.length > 0) {
2764
+ this.sessionCache.addMessages(sessionId, rawMessages, chatId, userId);
2765
+ }
2766
+ if (topicGroups.detail.length + topicGroups.summary.length + topicGroups.concise.length > 0) {
2767
+ this.sessionCache.buildHistoryWindow(sessionId, topicGroups);
2768
+ } else {
2769
+ this.sessionCache.setHistoryWindow(sessionId, "");
2770
+ }
2771
+ })
2772
+ )
2773
+ );
2582
2774
  }
2583
2775
  async updateChat(messages, opts) {
2584
2776
  if (messages.length === 0) return;
@@ -2653,7 +2845,7 @@ var MemoryManager = class {
2653
2845
  }
2654
2846
  async flushChat(sessionId, opts) {
2655
2847
  const sid = sessionId ?? DEFAULT_SESSION_ID;
2656
- const promise = this.compressManager.triggerCompress(sid, true);
2848
+ const promise = this.compressManager.triggerCompress(sid, true, opts?.waitGraph === true);
2657
2849
  if (opts?.wait) await promise;
2658
2850
  }
2659
2851
  async updateFacts(content, level, userId, chatId, sessionId) {
@@ -2944,6 +3136,10 @@ var MemoryManager = class {
2944
3136
  return page ? this.knowledgeManager.listDocuments(filter, page) : this.knowledgeManager.listDocuments(filter);
2945
3137
  }
2946
3138
  destroy() {
3139
+ if (this.optimizeTimer) {
3140
+ clearInterval(this.optimizeTimer);
3141
+ this.optimizeTimer = void 0;
3142
+ }
2947
3143
  this.grafeo.close();
2948
3144
  }
2949
3145
  };