@ppagent/memory 0.1.1 → 0.1.2

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 +159 -117
  2. package/dist/index.js +326 -157
  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,12 @@ 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
+ optimizeVersionRetentionMs: config.optimizeVersionRetentionMs ?? 0,
51
+ restoreConcurrency: config.restoreConcurrency ?? 8
46
52
  };
47
53
  }
48
54
 
@@ -162,6 +168,16 @@ import { GrafeoDB } from "@grafeo-db/js";
162
168
  import { get_encoding } from "@dqbd/tiktoken";
163
169
 
164
170
  // src/llm/http.ts
171
+ function normalizeFetchError(err, timeoutMs, errorLabel) {
172
+ if (isAbortError(err)) {
173
+ return new Error(`${errorLabel} request timed out after ${timeoutMs}ms`, { cause: err });
174
+ }
175
+ if (err instanceof Error) return err;
176
+ return new Error(String(err));
177
+ }
178
+ function isAbortError(err) {
179
+ return typeof err === "object" && err !== null && "name" in err && err.name === "AbortError";
180
+ }
165
181
  function backoffDelay(attempt) {
166
182
  return Math.min(1e3 * 2 ** attempt, 8e3);
167
183
  }
@@ -170,7 +186,9 @@ async function postJsonWithRetry(url, headers, body, opts, errorLabel = "HTTP")
170
186
  let lastErr;
171
187
  for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
172
188
  const controller = new AbortController();
173
- const timer = setTimeout(() => controller.abort(), opts.timeoutMs);
189
+ const timer = setTimeout(() => {
190
+ controller.abort(new Error(`${errorLabel} request timed out after ${opts.timeoutMs}ms`));
191
+ }, opts.timeoutMs);
174
192
  let res;
175
193
  try {
176
194
  res = await fetch(url, {
@@ -181,12 +199,12 @@ async function postJsonWithRetry(url, headers, body, opts, errorLabel = "HTTP")
181
199
  });
182
200
  } catch (err) {
183
201
  clearTimeout(timer);
184
- lastErr = err;
202
+ lastErr = normalizeFetchError(err, opts.timeoutMs, errorLabel);
185
203
  if (attempt < opts.maxRetries) {
186
204
  await sleep(backoffDelay(attempt));
187
205
  continue;
188
206
  }
189
- throw err;
207
+ throw lastErr;
190
208
  }
191
209
  clearTimeout(timer);
192
210
  if (res.ok) {
@@ -226,27 +244,45 @@ var EmbedService = class {
226
244
  const inputs = Array.isArray(texts) ? texts : [texts];
227
245
  const url = `${this.config.embeddingBaseUrl.replace(/\/$/, "")}/embeddings`;
228
246
  const batchSize = Math.max(1, this.config.embeddingBatchSize);
247
+ const concurrency = Math.max(1, this.config.embeddingConcurrency);
229
248
  const retryOpts = { timeoutMs: this.config.httpTimeoutMs, maxRetries: this.config.httpMaxRetries };
230
- const out = [];
249
+ const batches = [];
231
250
  for (let i = 0; i < inputs.length; i += batchSize) {
232
- const batch = inputs.slice(i, i + batchSize);
251
+ batches.push({ index: batches.length, texts: inputs.slice(i, i + batchSize) });
252
+ }
253
+ const batchResults = await mapLimit(batches, concurrency, async (batch) => {
233
254
  const data = await postJsonWithRetry(
234
255
  url,
235
256
  { Authorization: `Bearer ${this.config.embeddingApiKey}` },
236
- { model: this.config.embeddingModel, input: batch },
257
+ { model: this.config.embeddingModel, input: batch.texts },
237
258
  retryOpts,
238
259
  "Embedding API"
239
260
  );
240
- const sorted = data.data.sort((a, b) => a.index - b.index).map((d) => d.embedding);
241
- out.push(...sorted);
242
- }
243
- return out;
261
+ return {
262
+ index: batch.index,
263
+ vectors: data.data.sort((a, b) => a.index - b.index).map((d) => d.embedding)
264
+ };
265
+ });
266
+ return batchResults.sort((a, b) => a.index - b.index).flatMap((b) => b.vectors);
244
267
  }
245
268
  async embedOne(text) {
246
269
  const result = await this.embed([text]);
247
270
  return result[0];
248
271
  }
249
272
  };
273
+ async function mapLimit(items, limit, mapper) {
274
+ const results = new Array(items.length);
275
+ let next = 0;
276
+ const workers = Array.from({ length: Math.min(limit, items.length) }, async () => {
277
+ while (true) {
278
+ const index = next++;
279
+ if (index >= items.length) return;
280
+ results[index] = await mapper(items[index], index);
281
+ }
282
+ });
283
+ await Promise.all(workers);
284
+ return results;
285
+ }
250
286
  function cosineSimilarity(a, b) {
251
287
  let dot = 0;
252
288
  let normA = 0;
@@ -979,6 +1015,24 @@ function safeParseObject(s) {
979
1015
  return {};
980
1016
  }
981
1017
  }
1018
+ var MESSAGES_INDEXES = [
1019
+ { column: "message_id" },
1020
+ { column: "session_id" },
1021
+ { column: "content", fts: true },
1022
+ { column: "metadata", fts: true }
1023
+ // 支持 metadata 内容全文检索
1024
+ ];
1025
+ var TOPICS_INDEXES = [
1026
+ { column: "chat_id" },
1027
+ { column: "detail", fts: true }
1028
+ ];
1029
+ var SESSIONS_INDEXES = [{ column: "session_id" }];
1030
+ var DOCUMENTS_INDEXES = [{ column: "doc_id" }];
1031
+ var CHUNKS_INDEXES = [
1032
+ { column: "doc_id" },
1033
+ { column: "content", fts: true }
1034
+ // 知识片段混合检索
1035
+ ];
982
1036
  var LanceService = class {
983
1037
  config;
984
1038
  conn;
@@ -991,6 +1045,7 @@ var LanceService = class {
991
1045
  // Scalar indexes cannot be created on empty tables (LanceDB btree limitation).
992
1046
  // These flags defer creation to the first insert.
993
1047
  isNewMessagesTable = false;
1048
+ isNewTopicsTable = false;
994
1049
  isNewSessionsTable = false;
995
1050
  isNewDocumentsTable = false;
996
1051
  isNewChunksTable = false;
@@ -1004,7 +1059,6 @@ var LanceService = class {
1004
1059
  if (existingTables.includes(MESSAGES_TABLE)) {
1005
1060
  this.messagesTable = await this.conn.openTable(MESSAGES_TABLE);
1006
1061
  await this._ensurePartsColumn();
1007
- await this.ensureScalarIndex(this.messagesTable, "message_id");
1008
1062
  } else {
1009
1063
  this.messagesTable = await this.conn.createEmptyTable(
1010
1064
  MESSAGES_TABLE,
@@ -1020,6 +1074,7 @@ var LanceService = class {
1020
1074
  TOPICS_TABLE,
1021
1075
  topicsSchema(dim)
1022
1076
  );
1077
+ this.isNewTopicsTable = true;
1023
1078
  }
1024
1079
  if (existingTables.includes(FACTS_TABLE)) {
1025
1080
  this.factsTable = await this.conn.openTable(FACTS_TABLE);
@@ -1028,43 +1083,83 @@ var LanceService = class {
1028
1083
  }
1029
1084
  if (existingTables.includes(SESSIONS_TABLE)) {
1030
1085
  this.sessionsTable = await this.conn.openTable(SESSIONS_TABLE);
1031
- await this.ensureScalarIndex(this.sessionsTable, "session_id");
1032
1086
  } else {
1033
1087
  this.sessionsTable = await this.conn.createEmptyTable(SESSIONS_TABLE, sessionsSchema());
1034
1088
  this.isNewSessionsTable = true;
1035
1089
  }
1036
1090
  if (existingTables.includes(DOCUMENTS_TABLE)) {
1037
1091
  this.documentsTable = await this.conn.openTable(DOCUMENTS_TABLE);
1038
- await this.ensureScalarIndex(this.documentsTable, "doc_id");
1039
1092
  } else {
1040
1093
  this.documentsTable = await this.conn.createEmptyTable(DOCUMENTS_TABLE, documentsSchema(dim));
1041
1094
  this.isNewDocumentsTable = true;
1042
1095
  }
1043
1096
  if (existingTables.includes(CHUNKS_TABLE)) {
1044
1097
  this.chunksTable = await this.conn.openTable(CHUNKS_TABLE);
1045
- await this.ensureScalarIndex(this.chunksTable, "doc_id");
1046
1098
  } else {
1047
1099
  this.chunksTable = await this.conn.createEmptyTable(CHUNKS_TABLE, chunksSchema(dim));
1048
1100
  this.isNewChunksTable = true;
1049
1101
  }
1050
- await this.ensureFtsIndexes();
1102
+ await this.ensureIndexes(this.messagesTable, MESSAGES_INDEXES);
1103
+ await this.ensureIndexes(this.topicsTable, TOPICS_INDEXES);
1104
+ await this.ensureIndexes(this.sessionsTable, SESSIONS_INDEXES);
1105
+ await this.ensureIndexes(this.documentsTable, DOCUMENTS_INDEXES);
1106
+ await this.ensureIndexes(this.chunksTable, CHUNKS_INDEXES);
1051
1107
  }
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) {
1108
+ /**
1109
+ * 按需补齐索引:先经 listIndices 判存在,缺失的列才 createIndex(且 replace:false)。
1110
+ * createIndex 默认 replace:true 会在每次启动时全量重建索引并提交新表版本——
1111
+ * 这正是历史上「启动越来越慢 + _versions 目录膨胀」的根源,绝不可回退到无条件 createIndex。
1112
+ */
1113
+ async ensureIndexes(table, specs) {
1114
+ let indexed;
1115
+ try {
1116
+ indexed = new Set((await table.listIndices()).flatMap((i) => i.columns));
1117
+ } catch {
1118
+ indexed = /* @__PURE__ */ new Set();
1119
+ }
1120
+ for (const { column, fts } of specs) {
1121
+ if (indexed.has(column)) continue;
1062
1122
  try {
1063
- await table.createIndex(column, { config: lancedb.Index.fts() });
1123
+ await table.createIndex(
1124
+ column,
1125
+ fts ? { config: lancedb.Index.fts(), replace: false } : { replace: false }
1126
+ );
1064
1127
  } catch {
1065
1128
  }
1066
1129
  }
1067
1130
  }
1131
+ /**
1132
+ * 存储压实:逐表执行碎片合并 + 清理 retentionMs 之前的历史版本。
1133
+ * 嵌入式场景下 LanceDB 不会自动做这件事,长期运行后版本/碎片无限累积会显著拖慢启动与查询。
1134
+ */
1135
+ async optimizeStorage(retentionMs = 0) {
1136
+ const cutoff = new Date(Date.now() - Math.max(0, retentionMs));
1137
+ const targets = [
1138
+ [MESSAGES_TABLE, this.messagesTable],
1139
+ [TOPICS_TABLE, this.topicsTable],
1140
+ [FACTS_TABLE, this.factsTable],
1141
+ [SESSIONS_TABLE, this.sessionsTable],
1142
+ [DOCUMENTS_TABLE, this.documentsTable],
1143
+ [CHUNKS_TABLE, this.chunksTable]
1144
+ ];
1145
+ const results = [];
1146
+ for (const [name, table] of targets) {
1147
+ try {
1148
+ const stats = await table.optimize({ cleanupOlderThan: cutoff });
1149
+ results.push({
1150
+ table: name,
1151
+ fragmentsRemoved: stats.compaction.fragmentsRemoved,
1152
+ fragmentsAdded: stats.compaction.fragmentsAdded,
1153
+ filesRemoved: stats.compaction.filesRemoved,
1154
+ oldVersionsRemoved: stats.prune.oldVersionsRemoved,
1155
+ bytesRemoved: Number(stats.prune.bytesRemoved)
1156
+ });
1157
+ } catch (err) {
1158
+ console.warn(`[LanceService] optimize table "${name}" failed:`, err);
1159
+ }
1160
+ }
1161
+ return results;
1162
+ }
1068
1163
  /**
1069
1164
  * 为存量 messages 表添加 parts 列(如果缺失)。
1070
1165
  * LanceDB 0.14+ 支持 addColumns;旧版本会 throw,此时 rowToMessage 的 ?? "[]" 兜底。
@@ -1101,24 +1196,20 @@ var LanceService = class {
1101
1196
  } catch {
1102
1197
  }
1103
1198
  }
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
1199
  async addMessages(messages) {
1113
1200
  if (messages.length === 0) return;
1114
1201
  await this.messagesTable.add(messages.map(messageToRow));
1115
1202
  if (this.isNewMessagesTable) {
1116
- await this.ensureScalarIndex(this.messagesTable, "message_id");
1203
+ await this.ensureIndexes(this.messagesTable, MESSAGES_INDEXES);
1117
1204
  this.isNewMessagesTable = false;
1118
1205
  }
1119
1206
  }
1120
1207
  async addTopic(topic) {
1121
1208
  await this.topicsTable.add([topicToRow(topic)]);
1209
+ if (this.isNewTopicsTable) {
1210
+ await this.ensureIndexes(this.topicsTable, TOPICS_INDEXES);
1211
+ this.isNewTopicsTable = false;
1212
+ }
1122
1213
  }
1123
1214
  async updateTopicRecallCount(summaryId, count) {
1124
1215
  await this.topicsTable.update({
@@ -1251,7 +1342,7 @@ var LanceService = class {
1251
1342
  async insertSession(session) {
1252
1343
  await this.sessionsTable.add([sessionToRow(session)]);
1253
1344
  if (this.isNewSessionsTable) {
1254
- await this.ensureScalarIndex(this.sessionsTable, "session_id");
1345
+ await this.ensureIndexes(this.sessionsTable, SESSIONS_INDEXES);
1255
1346
  this.isNewSessionsTable = false;
1256
1347
  }
1257
1348
  }
@@ -1357,7 +1448,7 @@ var LanceService = class {
1357
1448
  async addDocument(doc) {
1358
1449
  await this.documentsTable.add([documentToRow(doc)]);
1359
1450
  if (this.isNewDocumentsTable) {
1360
- await this.ensureScalarIndex(this.documentsTable, "doc_id");
1451
+ await this.ensureIndexes(this.documentsTable, DOCUMENTS_INDEXES);
1361
1452
  this.isNewDocumentsTable = false;
1362
1453
  }
1363
1454
  }
@@ -1365,7 +1456,7 @@ var LanceService = class {
1365
1456
  if (chunks.length === 0) return;
1366
1457
  await this.chunksTable.add(chunks.map(chunkToRow));
1367
1458
  if (this.isNewChunksTable) {
1368
- await this.ensureScalarIndex(this.chunksTable, "doc_id");
1459
+ await this.ensureIndexes(this.chunksTable, CHUNKS_INDEXES);
1369
1460
  this.isNewChunksTable = false;
1370
1461
  }
1371
1462
  }
@@ -1488,6 +1579,40 @@ var LanceService = class {
1488
1579
  }
1489
1580
  };
1490
1581
 
1582
+ // src/manager/semaphore.ts
1583
+ var Semaphore = class {
1584
+ count;
1585
+ queue = [];
1586
+ constructor(max) {
1587
+ this.count = max;
1588
+ }
1589
+ async run(fn) {
1590
+ await this.acquire();
1591
+ try {
1592
+ return await fn();
1593
+ } finally {
1594
+ this.release();
1595
+ }
1596
+ }
1597
+ acquire() {
1598
+ if (this.count > 0) {
1599
+ this.count--;
1600
+ return Promise.resolve();
1601
+ }
1602
+ return new Promise((resolve) => {
1603
+ this.queue.push(resolve);
1604
+ });
1605
+ }
1606
+ release() {
1607
+ const next = this.queue.shift();
1608
+ if (next) {
1609
+ next();
1610
+ } else {
1611
+ this.count++;
1612
+ }
1613
+ }
1614
+ };
1615
+
1491
1616
  // src/llm/llm.service.ts
1492
1617
  var ENTITY_EXTRACTOR_TOOL = {
1493
1618
  type: "function",
@@ -1602,7 +1727,7 @@ var LlmService = class {
1602
1727
  return data.choices[0].message;
1603
1728
  }
1604
1729
  // ── 第一步:生成三级摘要(JSON 输出)──────────────────────────────────────────
1605
- async getSummary(messages) {
1730
+ async summarizeMessages(messages) {
1606
1731
  const now = (/* @__PURE__ */ new Date()).toISOString();
1607
1732
  const { detailMaxTokens, summaryMaxTokens, conciseMaxTokens } = this.config;
1608
1733
  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 +1749,7 @@ ${formatted}`);
1624
1749
  return { title: parsed.title ?? "", detail: parsed.detail, summary: parsed.summary, concise: parsed.concise };
1625
1750
  }
1626
1751
  // ── 第二步:通过工具调用提取实体和关系 ──────────────────────────────────────
1627
- async extractEntitiesWithTool(messages) {
1752
+ async extractEntitiesFromMessages(messages) {
1628
1753
  const now = (/* @__PURE__ */ new Date()).toISOString();
1629
1754
  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
1755
  \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 +1777,15 @@ ${formatted}`
1652
1777
  }
1653
1778
  }
1654
1779
  } catch (err) {
1655
- console.error("[LlmService] extractEntitiesWithTool failed:", err);
1780
+ console.error("[LlmService] extractEntitiesFromMessages failed:", err);
1656
1781
  }
1657
1782
  return { entities: [], relations: [] };
1658
1783
  }
1659
1784
  // ── 对外主接口:压缩 + 实体抽取(并发执行两步)────────────────────────────────
1660
1785
  async compress(messages) {
1661
1786
  const [summary, extraction] = await Promise.all([
1662
- this.getSummary(messages),
1663
- this.extractEntitiesWithTool(messages)
1787
+ this.summarizeMessages(messages),
1788
+ this.extractEntitiesFromMessages(messages)
1664
1789
  ]);
1665
1790
  return {
1666
1791
  title: summary.title,
@@ -1758,42 +1883,6 @@ ${context}`, false);
1758
1883
 
1759
1884
  // src/manager/compress.manager.ts
1760
1885
  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
1886
  var CompressManager = class {
1798
1887
  config;
1799
1888
  lance;
@@ -1812,17 +1901,17 @@ var CompressManager = class {
1812
1901
  this.sessionCache = sessionCache;
1813
1902
  this.semaphore = new Semaphore(config.maxConcurrentCompressions);
1814
1903
  }
1815
- triggerCompress(sessionId, force = false) {
1904
+ triggerCompress(sessionId, force = false, waitGraph = false) {
1816
1905
  const chain = this.sessionChain.get(sessionId) ?? Promise.resolve();
1817
1906
  const next = chain.then(
1818
- () => this.semaphore.run(() => this.doCompress(sessionId, force))
1907
+ () => this.semaphore.run(() => this.doCompress(sessionId, force, waitGraph))
1819
1908
  );
1820
1909
  this.sessionChain.set(sessionId, next.catch(() => {
1821
1910
  }));
1822
1911
  return next;
1823
1912
  }
1824
- async doCompress(sessionId, force) {
1825
- const messages = this.sessionCache.getSessionMessages(sessionId);
1913
+ async doCompress(sessionId, force, waitGraph) {
1914
+ const messages = this.sessionCache.getSessionMessages(sessionId).slice();
1826
1915
  if (messages.length === 0) return;
1827
1916
  const entry = this.sessionCache.getEntry(sessionId);
1828
1917
  if (!entry) return;
@@ -1830,15 +1919,15 @@ var CompressManager = class {
1830
1919
  const { chatId, userId } = entry.ids;
1831
1920
  const startTime = messages[0].createdAt;
1832
1921
  const endTime = messages[messages.length - 1].createdAt;
1833
- let compressOutput;
1922
+ let summary;
1834
1923
  try {
1835
- compressOutput = await this.llm.compress(messages);
1924
+ summary = await this.llm.summarizeMessages(messages);
1836
1925
  } catch (err) {
1837
- console.error(`[CompressManager] LLM compress failed for session ${sessionId}:`, err);
1926
+ console.error(`[CompressManager] LLM compress failed for session ${sessionId}: ${formatError(err)}`);
1838
1927
  return;
1839
1928
  }
1840
- const { title, detail, summary, concise, entities, relations } = compressOutput;
1841
- const topicTextForEmbed = [detail, summary, concise].find((t) => t.length > 0) ?? "";
1929
+ const { title, detail, summary: summaryText, concise } = summary;
1930
+ const topicTextForEmbed = [detail, summaryText, concise].find((t) => t.length > 0) ?? "";
1842
1931
  let topicVector = [];
1843
1932
  try {
1844
1933
  topicVector = await this.embed.embedOne(topicTextForEmbed);
@@ -1852,7 +1941,7 @@ var CompressManager = class {
1852
1941
  chatId,
1853
1942
  title,
1854
1943
  detail,
1855
- summary,
1944
+ summary: summaryText,
1856
1945
  concise,
1857
1946
  startTime,
1858
1947
  endTime,
@@ -1866,9 +1955,6 @@ var CompressManager = class {
1866
1955
  } catch (err) {
1867
1956
  console.error(`[CompressManager] Save topic failed:`, err);
1868
1957
  }
1869
- if (entities.length > 0 || relations.length > 0) {
1870
- await this.persistGraph(entities, relations, sessionId, chatId, userId, endTime);
1871
- }
1872
1958
  this.sessionCache.clearMessages(sessionId, endTime);
1873
1959
  const [n1, n2, n3] = this.config.topicRatio;
1874
1960
  try {
@@ -1877,6 +1963,19 @@ var CompressManager = class {
1877
1963
  } catch (err) {
1878
1964
  console.error(`[CompressManager] Rebuild history window failed:`, err);
1879
1965
  }
1966
+ const graphTask = this.extractAndPersistGraph(messages, sessionId, chatId, userId, endTime);
1967
+ if (waitGraph) {
1968
+ await withTimeout(graphTask, this.config.graphBuildTimeoutMs, "flushChat graph build");
1969
+ } else {
1970
+ graphTask.catch((err) => {
1971
+ console.error(`[CompressManager] Background graph persist failed:`, err);
1972
+ });
1973
+ }
1974
+ }
1975
+ async extractAndPersistGraph(messages, sessionId, chatId, userId, endTime) {
1976
+ const { entities, relations } = await this.llm.extractEntitiesFromMessages(messages);
1977
+ if (entities.length === 0 && relations.length === 0) return;
1978
+ await this.persistGraph(entities, relations, sessionId, chatId, userId, endTime);
1880
1979
  }
1881
1980
  async persistGraph(rawEntities, rawRelations, sessionId, chatId, userId, messageTime) {
1882
1981
  const entities = rawEntities.map((e) => ({
@@ -1919,6 +2018,16 @@ var CompressManager = class {
1919
2018
  }
1920
2019
  }
1921
2020
  };
2021
+ function formatError(err) {
2022
+ return err instanceof Error ? err.message : String(err);
2023
+ }
2024
+ function withTimeout(promise, timeoutMs, label) {
2025
+ let timer;
2026
+ const timeout = new Promise((_, reject) => {
2027
+ timer = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
2028
+ });
2029
+ return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
2030
+ }
1922
2031
 
1923
2032
  // src/manager/fact.cache.ts
1924
2033
  import { v4 as uuidv42 } from "uuid";
@@ -2178,20 +2287,29 @@ var KnowledgeManager = class {
2178
2287
  updatedAt: now
2179
2288
  };
2180
2289
  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;
2290
+ const chunkIngest = this.semaphore.run(() => this.ingestChunks(doc, pieces));
2291
+ const graphBuild = chunkIngest.then((chunks) => {
2292
+ if (!shouldBuildGraph) return;
2293
+ return this.buildDocumentGraph(doc, chunks);
2294
+ });
2295
+ if (opts.wait || opts.waitGraph) {
2296
+ await chunkIngest;
2297
+ } else {
2298
+ chunkIngest.catch((err) => {
2299
+ console.error("[KnowledgeManager] background chunk ingest failed:", err);
2300
+ });
2301
+ }
2302
+ if (opts.waitGraph) {
2303
+ await withTimeout2(graphBuild, this.config.graphBuildTimeoutMs, "document graph build");
2186
2304
  } else {
2187
- background.catch((err) => {
2188
- console.error("[KnowledgeManager] background ingest failed:", err);
2305
+ graphBuild.catch((err) => {
2306
+ console.error("[KnowledgeManager] background graph build failed:", err);
2189
2307
  });
2190
2308
  }
2191
2309
  return { docId };
2192
2310
  }
2193
- async ingestChunksAndGraph(doc, pieces, buildGraph) {
2194
- if (pieces.length === 0) return;
2311
+ async ingestChunks(doc, pieces) {
2312
+ if (pieces.length === 0) return [];
2195
2313
  const redundant = this.config.chunkRedundantIds;
2196
2314
  const embedInputs = pieces.map(
2197
2315
  (p) => p.headingPath ? `${p.headingPath}
@@ -2202,7 +2320,7 @@ ${p.content}` : p.content
2202
2320
  vectors = await this.embed.embed(embedInputs);
2203
2321
  } catch (err) {
2204
2322
  console.error("[KnowledgeManager] embed chunks failed:", err);
2205
- return;
2323
+ return [];
2206
2324
  }
2207
2325
  const chunks = pieces.map((p, i) => ({
2208
2326
  chunkId: uuidv43(),
@@ -2219,17 +2337,12 @@ ${p.content}` : p.content
2219
2337
  createdAt: doc.createdAt
2220
2338
  }));
2221
2339
  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
- }
2340
+ return chunks;
2227
2341
  }
2228
2342
  async buildDocumentGraph(doc, chunks) {
2229
- let extractedAny = false;
2230
- for (const chunk of chunks) {
2343
+ const extracted = await mapLimit2(chunks, this.config.graphExtractConcurrency, async (chunk) => {
2231
2344
  const { entities: rawEntities, relations: rawRelations } = await this.llm.extractEntitiesFromText(chunk.content);
2232
- if (rawEntities.length === 0 && rawRelations.length === 0) continue;
2345
+ if (rawEntities.length === 0 && rawRelations.length === 0) return null;
2233
2346
  const meta = {
2234
2347
  userId: doc.userId,
2235
2348
  chatId: doc.chatId,
@@ -2238,46 +2351,47 @@ ${p.content}` : p.content
2238
2351
  chunkId: chunk.chunkId,
2239
2352
  messageTime: doc.createdAt
2240
2353
  };
2241
- const entities = rawEntities.map((e) => ({
2354
+ const entities2 = rawEntities.map((e) => ({
2242
2355
  name: e.name,
2243
2356
  type: e.type,
2244
2357
  meta: { ...meta, ...e.meta }
2245
2358
  }));
2246
- const relations = rawRelations.map((r) => ({
2359
+ const relations2 = rawRelations.map((r) => ({
2247
2360
  from: r.from,
2248
2361
  to: r.to,
2249
2362
  type: r.type,
2250
2363
  happenedAt: r.happenedAt ?? void 0,
2251
2364
  meta: { ...meta, ...r.meta }
2252
2365
  }));
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
- });
2366
+ return { entities: entities2, relations: relations2 };
2367
+ });
2368
+ const entities = extracted.flatMap((item) => item?.entities ?? []);
2369
+ const relations = extracted.flatMap((item) => item?.relations ?? []);
2370
+ if (entities.length === 0 && relations.length === 0) return;
2371
+ const allNames = [
2372
+ .../* @__PURE__ */ new Set([
2373
+ ...entities.map((e) => e.name),
2374
+ ...relations.flatMap((r) => [r.from, r.to])
2375
+ ])
2376
+ ];
2377
+ if (allNames.length === 0) return;
2378
+ const embeddings = /* @__PURE__ */ new Map();
2379
+ try {
2380
+ const vecs = await this.embed.embed(allNames);
2381
+ allNames.forEach((name, i) => embeddings.set(name, vecs[i]));
2382
+ } catch (err) {
2383
+ console.error("[KnowledgeManager] embed entity names failed:", err);
2384
+ return;
2280
2385
  }
2386
+ await this.grafeo.upsertEntitiesAndRelations(
2387
+ entities,
2388
+ relations,
2389
+ embeddings,
2390
+ KIND_KNOWLEDGE
2391
+ );
2392
+ await this.lance.updateDocumentGraphFlag(doc.docId, true).catch((err) => {
2393
+ console.error(`[KnowledgeManager] Failed to update graph flag for doc ${doc.docId}:`, err);
2394
+ });
2281
2395
  }
2282
2396
  // ── 检索 ───────────────────────────────────────────────────────────────────────
2283
2397
  async searchKnowledge(opts) {
@@ -2439,6 +2553,26 @@ function inferTitle(markdown) {
2439
2553
  }
2440
2554
  return void 0;
2441
2555
  }
2556
+ async function mapLimit2(items, limit, mapper) {
2557
+ const results = new Array(items.length);
2558
+ let next = 0;
2559
+ const workers = Array.from({ length: Math.min(Math.max(1, limit), items.length) }, async () => {
2560
+ while (true) {
2561
+ const index = next++;
2562
+ if (index >= items.length) return;
2563
+ results[index] = await mapper(items[index], index);
2564
+ }
2565
+ });
2566
+ await Promise.all(workers);
2567
+ return results;
2568
+ }
2569
+ function withTimeout2(promise, timeoutMs, label) {
2570
+ let timer;
2571
+ const timeout = new Promise((_, reject) => {
2572
+ timer = setTimeout(() => reject(new Error(`${label} timed out after ${timeoutMs}ms`)), timeoutMs);
2573
+ });
2574
+ return Promise.race([promise, timeout]).finally(() => clearTimeout(timer));
2575
+ }
2442
2576
 
2443
2577
  // src/manager/session.cache.ts
2444
2578
  var SessionCache = class {
@@ -2558,27 +2692,62 @@ var MemoryManager = class {
2558
2692
  this.sessionMap.set(s.sessionId, this.deserializeSession(s));
2559
2693
  }
2560
2694
  await this.restoreFromStorage();
2695
+ if (this.config.autoOptimizeOnInit) {
2696
+ void this.optimizeStorage().then((results) => {
2697
+ const versions = results.reduce((s, r) => s + r.oldVersionsRemoved, 0);
2698
+ const fragments = results.reduce((s, r) => s + r.fragmentsRemoved, 0);
2699
+ if (versions > 0 || fragments > 0) {
2700
+ console.info(
2701
+ `[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`
2702
+ );
2703
+ }
2704
+ }).catch((err) => {
2705
+ console.warn("[MemoryManager] \u540E\u53F0\u5B58\u50A8\u538B\u5B9E\u5931\u8D25\uFF08\u4E0D\u5F71\u54CD\u670D\u52A1\uFF09:", err);
2706
+ });
2707
+ }
2708
+ }
2709
+ /**
2710
+ * 手动触发存储压实与历史版本清理(init 后台会自动执行一次;上层维护任务也可调用)。
2711
+ * @param retentionMs 保留多久内的历史版本,默认取配置 optimizeVersionRetentionMs
2712
+ */
2713
+ async optimizeStorage(retentionMs) {
2714
+ return this.lance.optimizeStorage(retentionMs ?? this.config.optimizeVersionRetentionMs);
2561
2715
  }
2562
2716
  async restoreFromStorage() {
2563
2717
  const [n1, n2, n3] = this.config.topicRatio;
2564
2718
  const sessionIds = await this.lance.getAllSessionIds();
2565
2719
  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);
2720
+ const topicsCache = /* @__PURE__ */ new Map();
2721
+ const getTopics = (chatId, userId) => {
2722
+ const key = `${chatId}\0${userId}`;
2723
+ let cached = topicsCache.get(key);
2724
+ if (!cached) {
2725
+ cached = this.lance.getRecentTopics(chatId, userId, n1, n2, n3);
2726
+ topicsCache.set(key, cached);
2575
2727
  }
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, "");
2580
- }
2581
- }
2728
+ return cached;
2729
+ };
2730
+ const sem = new Semaphore(this.config.restoreConcurrency);
2731
+ await Promise.all(
2732
+ sessionIds.map(
2733
+ (sessionId) => sem.run(async () => {
2734
+ const recentMessages = await this.lance.getLatestMessages(sessionId, 1);
2735
+ if (recentMessages.length === 0) return;
2736
+ const { chatId, userId } = recentMessages[0];
2737
+ const topicGroups = await getTopics(chatId, userId);
2738
+ const n1EndTime = topicGroups.detail.length > 0 ? topicGroups.detail[topicGroups.detail.length - 1].endTime : 0;
2739
+ const rawMessages = n1EndTime > 0 ? await this.lance.getMessagesSince(sessionId, n1EndTime) : await this.lance.getLatestMessages(sessionId, 100);
2740
+ if (rawMessages.length > 0) {
2741
+ this.sessionCache.addMessages(sessionId, rawMessages, chatId, userId);
2742
+ }
2743
+ if (topicGroups.detail.length + topicGroups.summary.length + topicGroups.concise.length > 0) {
2744
+ this.sessionCache.buildHistoryWindow(sessionId, topicGroups);
2745
+ } else {
2746
+ this.sessionCache.setHistoryWindow(sessionId, "");
2747
+ }
2748
+ })
2749
+ )
2750
+ );
2582
2751
  }
2583
2752
  async updateChat(messages, opts) {
2584
2753
  if (messages.length === 0) return;
@@ -2653,7 +2822,7 @@ var MemoryManager = class {
2653
2822
  }
2654
2823
  async flushChat(sessionId, opts) {
2655
2824
  const sid = sessionId ?? DEFAULT_SESSION_ID;
2656
- const promise = this.compressManager.triggerCompress(sid, true);
2825
+ const promise = this.compressManager.triggerCompress(sid, true, opts?.waitGraph === true);
2657
2826
  if (opts?.wait) await promise;
2658
2827
  }
2659
2828
  async updateFacts(content, level, userId, chatId, sessionId) {