@ppagent/memory 0.4.4 → 0.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -51,9 +51,27 @@ function resolveConfig(config) {
51
51
  "topicCompactionSyncRatio"
52
52
  ),
53
53
  contextUsageRatio: ratio(config.contextUsageRatio ?? 0.75, "contextUsageRatio"),
54
- precompressionRatio: ratio(config.precompressionRatio ?? 0.75, "precompressionRatio"),
55
- compressionBatchRatio: ratio(config.compressionBatchRatio ?? 0.5, "compressionBatchRatio"),
54
+ precompressionRatio: ratio(config.precompressionRatio ?? 0.6, "precompressionRatio"),
55
+ compressionBatchRatio: ratio(config.compressionBatchRatio ?? 0.7, "compressionBatchRatio"),
56
56
  compressionBatchTokenLimit: Math.max(0, Math.floor(config.compressionBatchTokenLimit ?? 0)),
57
+ compressionMaxRetries: nonNegativeInt(
58
+ config.compressionMaxRetries ?? 2,
59
+ "compressionMaxRetries"
60
+ ),
61
+ compressionRetryBaseDelayMs: nonNegativeInt(
62
+ config.compressionRetryBaseDelayMs ?? 500,
63
+ "compressionRetryBaseDelayMs"
64
+ ),
65
+ backgroundCompressionMaxRetries: nonNegativeInt(
66
+ config.backgroundCompressionMaxRetries ?? 2,
67
+ "backgroundCompressionMaxRetries"
68
+ ),
69
+ backgroundCompressionRetryBaseDelayMs: nonNegativeInt(
70
+ config.backgroundCompressionRetryBaseDelayMs ?? 1e3,
71
+ "backgroundCompressionRetryBaseDelayMs"
72
+ ),
73
+ onCompressionEvent: config.onCompressionEvent ?? (() => {
74
+ }),
57
75
  topicSummaryMaxTokens: positiveInt(
58
76
  config.topicSummaryMaxTokens ?? config.detailMaxTokens ?? 2048,
59
77
  "topicSummaryMaxTokens"
@@ -90,6 +108,8 @@ function resolveConfig(config) {
90
108
  graphExtractConcurrency: config.graphExtractConcurrency ?? 2,
91
109
  graphBuildTimeoutMs: config.graphBuildTimeoutMs ?? 12e4,
92
110
  autoOptimizeOnInit: config.autoOptimizeOnInit ?? true,
111
+ autoMigrateLegacyDataOnInit: config.autoMigrateLegacyDataOnInit ?? true,
112
+ autoResumeCompressionOnInit: config.autoResumeCompressionOnInit ?? true,
93
113
  autoOptimizeIntervalMs: config.autoOptimizeIntervalMs ?? 6 * 36e5,
94
114
  optimizeVersionRetentionMs: config.optimizeVersionRetentionMs ?? 0,
95
115
  restoreConcurrency: config.restoreConcurrency ?? 8
@@ -101,6 +121,12 @@ function ratio(value, name) {
101
121
  }
102
122
  return value;
103
123
  }
124
+ function nonNegativeInt(value, name) {
125
+ if (!Number.isFinite(value) || value < 0) {
126
+ throw new Error(`[MemoryConfig] ${name} \u5FC5\u987B\u662F\u975E\u8D1F\u6570`);
127
+ }
128
+ return Math.floor(value);
129
+ }
104
130
  function positiveInt(value, name) {
105
131
  if (!Number.isFinite(value) || value <= 0) {
106
132
  throw new Error(`[MemoryConfig] ${name} \u5FC5\u987B\u662F\u6B63\u6570`);
@@ -134,6 +160,136 @@ function countTokens(text) {
134
160
  return encoder.encode(text).length;
135
161
  }
136
162
 
163
+ // src/history-image.ts
164
+ var OMIT_IMAGE = Symbol("omit-history-image");
165
+ var IMAGE_PART_TYPES = /* @__PURE__ */ new Set([
166
+ "image",
167
+ "image-data",
168
+ "image-url",
169
+ "image-file-id",
170
+ "image-file-reference",
171
+ "input-image",
172
+ "output-image"
173
+ ]);
174
+ function sanitizeRawHistoryFields(message) {
175
+ const partsResult = sanitizeValue(message.parts ?? []);
176
+ const payloadResult = sanitizeValue(message.payload);
177
+ const contextResult = sanitizeValue(message.contextPayload);
178
+ const metadataResult = sanitizeValue(message.metadata ?? {});
179
+ const payload = optionalSanitizedValue(payloadResult);
180
+ const contextPayload = optionalSanitizedValue(contextResult);
181
+ return {
182
+ parts: Array.isArray(partsResult.value) ? partsResult.value : [],
183
+ ...payload !== void 0 && { payload },
184
+ ...contextPayload !== void 0 && { contextPayload },
185
+ metadata: isRecord(metadataResult.value) ? metadataResult.value : {},
186
+ removedImages: partsResult.removedImages + payloadResult.removedImages + contextResult.removedImages + metadataResult.removedImages
187
+ };
188
+ }
189
+ function sanitizeStoredMessageImages(message) {
190
+ const parsed = {
191
+ content: message.content,
192
+ parts: parseArray(message.parts),
193
+ ...message.payload !== void 0 && { payload: parseValue(message.payload) },
194
+ ...message.contextPayload !== void 0 && {
195
+ contextPayload: parseValue(message.contextPayload)
196
+ },
197
+ metadata: parseObject(message.metadata)
198
+ };
199
+ const sanitized = sanitizeRawHistoryFields(parsed);
200
+ if (sanitized.removedImages === 0) {
201
+ return { message, changed: false, removedImages: 0 };
202
+ }
203
+ const parts = JSON.stringify(sanitized.parts);
204
+ const payload = sanitized.payload === void 0 ? void 0 : JSON.stringify(sanitized.payload);
205
+ const contextPayload = sanitized.contextPayload === void 0 ? void 0 : JSON.stringify(sanitized.contextPayload);
206
+ const metadata = JSON.stringify(sanitized.metadata);
207
+ const usage2 = countTokens(
208
+ [message.content, parts, metadata, payload ?? "", contextPayload ?? ""].join("\n")
209
+ );
210
+ return {
211
+ message: {
212
+ ...message,
213
+ parts,
214
+ payload,
215
+ contextPayload,
216
+ usage: usage2,
217
+ metadata
218
+ },
219
+ changed: true,
220
+ removedImages: sanitized.removedImages
221
+ };
222
+ }
223
+ function sanitizeValue(value) {
224
+ if (typeof value === "string" && /^data:image\//i.test(value.trim())) {
225
+ return { value: OMIT_IMAGE, removedImages: 1, changed: true };
226
+ }
227
+ if (value == null || typeof value !== "object") {
228
+ return { value, removedImages: 0, changed: false };
229
+ }
230
+ if (isImageNode(value)) {
231
+ return { value: OMIT_IMAGE, removedImages: 1, changed: true };
232
+ }
233
+ if (Array.isArray(value)) {
234
+ const out2 = [];
235
+ let removedImages2 = 0;
236
+ let changed2 = false;
237
+ for (const item of value) {
238
+ const sanitized = sanitizeValue(item);
239
+ removedImages2 += sanitized.removedImages;
240
+ changed2 ||= sanitized.changed;
241
+ if (sanitized.value !== OMIT_IMAGE) out2.push(sanitized.value);
242
+ }
243
+ return { value: changed2 ? out2 : value, removedImages: removedImages2, changed: changed2 };
244
+ }
245
+ const out = {};
246
+ let removedImages = 0;
247
+ let changed = false;
248
+ for (const [key, item] of Object.entries(value)) {
249
+ const sanitized = sanitizeValue(item);
250
+ removedImages += sanitized.removedImages;
251
+ changed ||= sanitized.changed;
252
+ if (sanitized.value !== OMIT_IMAGE) out[key] = sanitized.value;
253
+ }
254
+ return { value: changed ? out : value, removedImages, changed };
255
+ }
256
+ function isImageNode(value) {
257
+ if (!isRecord(value)) return false;
258
+ const type = normalizeType(value.type);
259
+ if (IMAGE_PART_TYPES.has(type)) return true;
260
+ const mediaType = String(value.mediaType ?? value.mimeType ?? "").trim().toLowerCase();
261
+ return mediaType.startsWith("image/") && (type === "file" || "data" in value || "url" in value || "fileId" in value || "providerReference" in value);
262
+ }
263
+ function normalizeType(value) {
264
+ return typeof value === "string" ? value.trim().toLowerCase().replaceAll("_", "-") : "";
265
+ }
266
+ function optionalSanitizedValue(result) {
267
+ if (result.value === OMIT_IMAGE) return void 0;
268
+ if (result.changed && isEmptyContainer(result.value)) return void 0;
269
+ return result.value;
270
+ }
271
+ function isEmptyContainer(value) {
272
+ return Array.isArray(value) ? value.length === 0 : isRecord(value) && Object.keys(value).length === 0;
273
+ }
274
+ function isRecord(value) {
275
+ return !!value && typeof value === "object" && !Array.isArray(value);
276
+ }
277
+ function parseValue(value) {
278
+ try {
279
+ return JSON.parse(value);
280
+ } catch {
281
+ return value;
282
+ }
283
+ }
284
+ function parseArray(value) {
285
+ const parsed = parseValue(value);
286
+ return Array.isArray(parsed) ? parsed : [];
287
+ }
288
+ function parseObject(value) {
289
+ const parsed = parseValue(value);
290
+ return isRecord(parsed) ? parsed : {};
291
+ }
292
+
137
293
  // src/db/memory.store.ts
138
294
  var MESSAGES_TABLE = "messages";
139
295
  var TOPICS_TABLE = "topics";
@@ -596,17 +752,28 @@ var MemoryStore = class {
596
752
  const messageRows = await this.provider.query(MESSAGES_TABLE);
597
753
  report.messagesScanned = messageRows.length;
598
754
  for (const row of messageRows) {
755
+ const sanitized = sanitizeStoredMessageImages(rowToMessage(row)).message;
599
756
  const usage2 = countTokens(
600
757
  [
601
- row.content,
602
- row.parts ?? "[]",
603
- row.metadata ?? "{}",
604
- row.payload ?? "",
605
- row.context_payload ?? ""
758
+ sanitized.content,
759
+ sanitized.parts,
760
+ sanitized.metadata,
761
+ sanitized.payload ?? "",
762
+ sanitized.contextPayload ?? ""
606
763
  ].map(String).join("\n")
607
764
  );
608
- if (Number(row.usage) === usage2) continue;
609
- await this.provider.update(MESSAGES_TABLE, { usage: usage2 }, [
765
+ const values = {
766
+ parts: sanitized.parts,
767
+ payload: sanitized.payload ?? null,
768
+ context_payload: sanitized.contextPayload ?? null,
769
+ metadata: sanitized.metadata,
770
+ usage: usage2
771
+ };
772
+ const changed = Object.entries(values).some(
773
+ ([key, value]) => !migrationValueEquals(row[key], value)
774
+ );
775
+ if (!changed) continue;
776
+ await this.provider.update(MESSAGES_TABLE, values, [
610
777
  eq("message_id", String(row.message_id))
611
778
  ]);
612
779
  report.messagesUpdated++;
package/dist/index.d.ts CHANGED
@@ -2,6 +2,26 @@ type MessageType = "text" | "image" | "file";
2
2
  type FactLevel = "user" | "chat";
3
3
  type SearchMode = "fast" | "auto" | "all";
4
4
  type SearchScope = "session" | "chat" | "user" | "all";
5
+ type CompressionRole = "user" | "assistant" | "tool" | "system";
6
+ type MemoryCompressionKind = "raw" | "topics";
7
+ type MemoryCompressionMode = "background" | "blocking" | "manual" | "startup";
8
+ type MemoryCompressionPhase = "start" | "retry" | "success" | "failure";
9
+ interface MemoryCompressionEvent {
10
+ sessionId: string;
11
+ kind: MemoryCompressionKind;
12
+ mode: MemoryCompressionMode;
13
+ phase: MemoryCompressionPhase;
14
+ /** 当前尝试序号,从 1 开始。 */
15
+ attempt: number;
16
+ /** 本层最多尝试次数。 */
17
+ maxAttempts: number;
18
+ inputTokens?: number;
19
+ selectedTokens?: number;
20
+ outputTokens?: number;
21
+ durationMs?: number;
22
+ retryKind?: "semantic" | "task";
23
+ error?: string;
24
+ }
5
25
  /**
6
26
  * 分页参数(可选)。
7
27
  * - offset:起始偏移,从 0 开始(负数按 0 处理)。
@@ -67,6 +87,13 @@ interface RawMessage {
67
87
  * Topic 摘要或知识图谱抽取。适合保存 provider-ready 工具协议消息等大体积细节。
68
88
  */
69
89
  contextPayload?: unknown;
90
+ /**
91
+ * 同一个完整对话轮次(含 user、assistant 与 tool 链)应使用相同 id。
92
+ * 压缩只会在 group 边界切分;省略时按 compressionRole/talkerId 兼容推断。
93
+ */
94
+ compressionGroupId?: string;
95
+ /** 消息在对话轮次中的角色,用于旧数据缺少显式 group id 时推断完整边界。 */
96
+ compressionRole?: CompressionRole;
70
97
  usage?: number;
71
98
  metadata?: Record<string, unknown>;
72
99
  createdAt?: number;
@@ -245,6 +272,8 @@ interface MemoryRawMessage {
245
272
  parts?: ContentPart[];
246
273
  payload?: unknown;
247
274
  contextPayload?: unknown;
275
+ compressionGroupId?: string;
276
+ compressionRole?: CompressionRole;
248
277
  usage: number;
249
278
  metadata?: Record<string, unknown>;
250
279
  createdAt: number;
@@ -419,12 +448,22 @@ interface MemoryConfig {
419
448
  topicCompactionSyncRatio?: number;
420
449
  /** 模型上下文中允许历史记忆使用的比例,默认 0.75。 */
421
450
  contextUsageRatio?: number;
422
- /** 原始消息达到其可用预算的此比例时后台预压缩,默认 0.75。 */
451
+ /** 原始消息达到其可用预算的此比例时后台预压缩,默认 0.6。 */
423
452
  precompressionRatio?: number;
424
- /** 单次压缩目标占原始消息预算的比例,默认 0.5。 */
453
+ /** 单次压缩目标占当前未压缩消息 token 总量的比例,默认 0.7(压 7 留 3)。 */
425
454
  compressionBatchRatio?: number;
426
455
  /** 单次压缩硬上限;0 表示只受 compressionBatchRatio 控制。 */
427
456
  compressionBatchTokenLimit?: number;
457
+ /** 压缩模型返回非法 JSON、缺字段或空摘要时的最大重试次数,默认 2(即最多 3 次)。 */
458
+ compressionMaxRetries?: number;
459
+ /** 压缩结果级重试的指数退避基数(毫秒),默认 500;0 表示不等待。 */
460
+ compressionRetryBaseDelayMs?: number;
461
+ /** 后台压缩整任务失败后的最大重试次数,默认 2(即最多执行 3 轮任务)。 */
462
+ backgroundCompressionMaxRetries?: number;
463
+ /** 后台压缩整任务重试的指数退避基数(毫秒),默认 1000;0 表示立即重试。 */
464
+ backgroundCompressionRetryBaseDelayMs?: number;
465
+ /** 压缩生命周期事件;回调异常会被隔离,不影响记忆读写。 */
466
+ onCompressionEvent?: (event: MemoryCompressionEvent) => void | Promise<void>;
428
467
  /** 单条 Topic 摘要硬上限,实际长度由重要性决定,默认 2048。 */
429
468
  topicSummaryMaxTokens?: number;
430
469
  /** 未传 getHistoryWindow 模型窗口时使用,默认 256K。 */
@@ -490,6 +529,10 @@ interface MemoryConfig {
490
529
  * 不阻塞启动;嵌入式单进程使用时建议保持开启,否则 LanceDB 版本目录会无限膨胀、启动越来越慢。
491
530
  */
492
531
  autoOptimizeOnInit?: boolean;
532
+ /** init 后是否在后台幂等执行一次 0.4 历史数据迁移(含历史图片剥离),默认 true。 */
533
+ autoMigrateLegacyDataOnInit?: boolean;
534
+ /** init 后是否后台扫描未完成的原始窗口并恢复自动压缩,默认 true。 */
535
+ autoResumeCompressionOnInit?: boolean;
493
536
  /**
494
537
  * 运行期定期压实的间隔(毫秒,默认 6 小时;设为 0 或负数关闭定时任务)。
495
538
  * 长驻服务不重启时版本仍会随写入累积,靠该定时任务周期性回收。
@@ -684,13 +727,19 @@ declare class MemoryManager {
684
727
  private sessionSweepTimer?;
685
728
  private optimizeRunning;
686
729
  private optimizeTask?;
730
+ private startupMaintenanceTask?;
687
731
  private destroyTask?;
688
732
  private readonly hydration;
689
733
  private readonly pendingWrites;
690
734
  private readonly topicCompactions;
735
+ private readonly backgroundCompressions;
691
736
  private warnedDefaultModelContext;
692
737
  constructor(config: MemoryConfig);
693
738
  init(): Promise<void>;
739
+ private runStartupMaintenance;
740
+ private getPersistedRawTailTokens;
741
+ private migrateLegacyDataOnce;
742
+ private legacyMigrationMarkerPath;
694
743
  /** 后台压实的统一入口:防重入(上一轮未结束则跳过),失败仅告警不影响服务。*/
695
744
  private runBackgroundOptimize;
696
745
  /**
@@ -702,6 +751,8 @@ declare class MemoryManager {
702
751
  private isSessionBusy;
703
752
  private waitForPendingWrites;
704
753
  private queueSessionWrite;
754
+ private scheduleBackgroundCompression;
755
+ private waitForBackgroundCompressionIdle;
705
756
  updateChat(messages: RawMessage[], opts?: UpdateChatOptions): Promise<void>;
706
757
  private doUpdateChat;
707
758
  flushChat(sessionId?: string, opts?: {
@@ -1019,4 +1070,4 @@ type RelationType = (typeof DEFAULT_RELATION_TYPES)[number];
1019
1070
  declare const KIND_CONVERSATION = "conversation";
1020
1071
  declare const KIND_KNOWLEDGE = "knowledge";
1021
1072
 
1022
- export { type AddDocumentOptions, type BuildGraphMode, type Chunk, type ChunkHit, type ChunkPiece, type CompressOutput, type ContentPart, DEFAULT_NODE_TYPES, DEFAULT_RELATION_TYPES, type Document, type DomainIds, type Entity, type EntityMeta, type Fact, type FactLevel, type Filter, type FilterCondition, type GetHistoryWindowOptions, KIND_CONVERSATION, KIND_KNOWLEDGE, type KnowledgeSearchOptions, type KnowledgeSearchResult, type MemoryBlockingCompressionEvent, type MemoryBlockingCompressionReason, type MemoryConfig, type MemoryContextBudget, type MemoryContextWindow, type MemoryContextWindowUsage, MemoryManager, type MemoryMigrationReport, type MemoryRawMessage, MemoryStore, type MessageType, type NodeType, type PageParams, type Paginated, type ProviderCapabilities, type ProviderKind, type RawMessage, type Relation, type RelationType, type SearchMode, type SearchOptions, type SearchResult, type SearchScope, type Session, type SessionEntry, type SessionSearchOptions, type SessionView, type StorageOptimizeResult, type StoredMessage, type Topic, type UpdateChatOptions, type UpdateEntityOptions, type UpdateSessionOptions, type VectorStoreProvider, calculateContextBudget };
1073
+ export { type AddDocumentOptions, type BuildGraphMode, type Chunk, type ChunkHit, type ChunkPiece, type CompressOutput, type CompressionRole, type ContentPart, DEFAULT_NODE_TYPES, DEFAULT_RELATION_TYPES, type Document, type DomainIds, type Entity, type EntityMeta, type Fact, type FactLevel, type Filter, type FilterCondition, type GetHistoryWindowOptions, KIND_CONVERSATION, KIND_KNOWLEDGE, type KnowledgeSearchOptions, type KnowledgeSearchResult, type MemoryBlockingCompressionEvent, type MemoryBlockingCompressionReason, type MemoryCompressionEvent, type MemoryCompressionKind, type MemoryCompressionMode, type MemoryCompressionPhase, type MemoryConfig, type MemoryContextBudget, type MemoryContextWindow, type MemoryContextWindowUsage, MemoryManager, type MemoryMigrationReport, type MemoryRawMessage, MemoryStore, type MessageType, type NodeType, type PageParams, type Paginated, type ProviderCapabilities, type ProviderKind, type RawMessage, type Relation, type RelationType, type SearchMode, type SearchOptions, type SearchResult, type SearchScope, type Session, type SessionEntry, type SessionSearchOptions, type SessionView, type StorageOptimizeResult, type StoredMessage, type Topic, type UpdateChatOptions, type UpdateEntityOptions, type UpdateSessionOptions, type VectorStoreProvider, calculateContextBudget };