@ppagent/memory 0.4.4 → 0.4.5
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 +163 -8
- package/dist/index.d.ts +5 -1
- package/dist/index.js +288 -39
- package/llms.txt +17 -12
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -52,8 +52,16 @@ function resolveConfig(config) {
|
|
|
52
52
|
),
|
|
53
53
|
contextUsageRatio: ratio(config.contextUsageRatio ?? 0.75, "contextUsageRatio"),
|
|
54
54
|
precompressionRatio: ratio(config.precompressionRatio ?? 0.75, "precompressionRatio"),
|
|
55
|
-
compressionBatchRatio: ratio(config.compressionBatchRatio ?? 0.
|
|
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
|
+
),
|
|
57
65
|
topicSummaryMaxTokens: positiveInt(
|
|
58
66
|
config.topicSummaryMaxTokens ?? config.detailMaxTokens ?? 2048,
|
|
59
67
|
"topicSummaryMaxTokens"
|
|
@@ -101,6 +109,12 @@ function ratio(value, name) {
|
|
|
101
109
|
}
|
|
102
110
|
return value;
|
|
103
111
|
}
|
|
112
|
+
function nonNegativeInt(value, name) {
|
|
113
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
114
|
+
throw new Error(`[MemoryConfig] ${name} \u5FC5\u987B\u662F\u975E\u8D1F\u6570`);
|
|
115
|
+
}
|
|
116
|
+
return Math.floor(value);
|
|
117
|
+
}
|
|
104
118
|
function positiveInt(value, name) {
|
|
105
119
|
if (!Number.isFinite(value) || value <= 0) {
|
|
106
120
|
throw new Error(`[MemoryConfig] ${name} \u5FC5\u987B\u662F\u6B63\u6570`);
|
|
@@ -134,6 +148,136 @@ function countTokens(text) {
|
|
|
134
148
|
return encoder.encode(text).length;
|
|
135
149
|
}
|
|
136
150
|
|
|
151
|
+
// src/history-image.ts
|
|
152
|
+
var OMIT_IMAGE = Symbol("omit-history-image");
|
|
153
|
+
var IMAGE_PART_TYPES = /* @__PURE__ */ new Set([
|
|
154
|
+
"image",
|
|
155
|
+
"image-data",
|
|
156
|
+
"image-url",
|
|
157
|
+
"image-file-id",
|
|
158
|
+
"image-file-reference",
|
|
159
|
+
"input-image",
|
|
160
|
+
"output-image"
|
|
161
|
+
]);
|
|
162
|
+
function sanitizeRawHistoryFields(message) {
|
|
163
|
+
const partsResult = sanitizeValue(message.parts ?? []);
|
|
164
|
+
const payloadResult = sanitizeValue(message.payload);
|
|
165
|
+
const contextResult = sanitizeValue(message.contextPayload);
|
|
166
|
+
const metadataResult = sanitizeValue(message.metadata ?? {});
|
|
167
|
+
const payload = optionalSanitizedValue(payloadResult);
|
|
168
|
+
const contextPayload = optionalSanitizedValue(contextResult);
|
|
169
|
+
return {
|
|
170
|
+
parts: Array.isArray(partsResult.value) ? partsResult.value : [],
|
|
171
|
+
...payload !== void 0 && { payload },
|
|
172
|
+
...contextPayload !== void 0 && { contextPayload },
|
|
173
|
+
metadata: isRecord(metadataResult.value) ? metadataResult.value : {},
|
|
174
|
+
removedImages: partsResult.removedImages + payloadResult.removedImages + contextResult.removedImages + metadataResult.removedImages
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
function sanitizeStoredMessageImages(message) {
|
|
178
|
+
const parsed = {
|
|
179
|
+
content: message.content,
|
|
180
|
+
parts: parseArray(message.parts),
|
|
181
|
+
...message.payload !== void 0 && { payload: parseValue(message.payload) },
|
|
182
|
+
...message.contextPayload !== void 0 && {
|
|
183
|
+
contextPayload: parseValue(message.contextPayload)
|
|
184
|
+
},
|
|
185
|
+
metadata: parseObject(message.metadata)
|
|
186
|
+
};
|
|
187
|
+
const sanitized = sanitizeRawHistoryFields(parsed);
|
|
188
|
+
if (sanitized.removedImages === 0) {
|
|
189
|
+
return { message, changed: false, removedImages: 0 };
|
|
190
|
+
}
|
|
191
|
+
const parts = JSON.stringify(sanitized.parts);
|
|
192
|
+
const payload = sanitized.payload === void 0 ? void 0 : JSON.stringify(sanitized.payload);
|
|
193
|
+
const contextPayload = sanitized.contextPayload === void 0 ? void 0 : JSON.stringify(sanitized.contextPayload);
|
|
194
|
+
const metadata = JSON.stringify(sanitized.metadata);
|
|
195
|
+
const usage2 = countTokens(
|
|
196
|
+
[message.content, parts, metadata, payload ?? "", contextPayload ?? ""].join("\n")
|
|
197
|
+
);
|
|
198
|
+
return {
|
|
199
|
+
message: {
|
|
200
|
+
...message,
|
|
201
|
+
parts,
|
|
202
|
+
payload,
|
|
203
|
+
contextPayload,
|
|
204
|
+
usage: usage2,
|
|
205
|
+
metadata
|
|
206
|
+
},
|
|
207
|
+
changed: true,
|
|
208
|
+
removedImages: sanitized.removedImages
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
function sanitizeValue(value) {
|
|
212
|
+
if (typeof value === "string" && /^data:image\//i.test(value.trim())) {
|
|
213
|
+
return { value: OMIT_IMAGE, removedImages: 1, changed: true };
|
|
214
|
+
}
|
|
215
|
+
if (value == null || typeof value !== "object") {
|
|
216
|
+
return { value, removedImages: 0, changed: false };
|
|
217
|
+
}
|
|
218
|
+
if (isImageNode(value)) {
|
|
219
|
+
return { value: OMIT_IMAGE, removedImages: 1, changed: true };
|
|
220
|
+
}
|
|
221
|
+
if (Array.isArray(value)) {
|
|
222
|
+
const out2 = [];
|
|
223
|
+
let removedImages2 = 0;
|
|
224
|
+
let changed2 = false;
|
|
225
|
+
for (const item of value) {
|
|
226
|
+
const sanitized = sanitizeValue(item);
|
|
227
|
+
removedImages2 += sanitized.removedImages;
|
|
228
|
+
changed2 ||= sanitized.changed;
|
|
229
|
+
if (sanitized.value !== OMIT_IMAGE) out2.push(sanitized.value);
|
|
230
|
+
}
|
|
231
|
+
return { value: changed2 ? out2 : value, removedImages: removedImages2, changed: changed2 };
|
|
232
|
+
}
|
|
233
|
+
const out = {};
|
|
234
|
+
let removedImages = 0;
|
|
235
|
+
let changed = false;
|
|
236
|
+
for (const [key, item] of Object.entries(value)) {
|
|
237
|
+
const sanitized = sanitizeValue(item);
|
|
238
|
+
removedImages += sanitized.removedImages;
|
|
239
|
+
changed ||= sanitized.changed;
|
|
240
|
+
if (sanitized.value !== OMIT_IMAGE) out[key] = sanitized.value;
|
|
241
|
+
}
|
|
242
|
+
return { value: changed ? out : value, removedImages, changed };
|
|
243
|
+
}
|
|
244
|
+
function isImageNode(value) {
|
|
245
|
+
if (!isRecord(value)) return false;
|
|
246
|
+
const type = normalizeType(value.type);
|
|
247
|
+
if (IMAGE_PART_TYPES.has(type)) return true;
|
|
248
|
+
const mediaType = String(value.mediaType ?? value.mimeType ?? "").trim().toLowerCase();
|
|
249
|
+
return mediaType.startsWith("image/") && (type === "file" || "data" in value || "url" in value || "fileId" in value || "providerReference" in value);
|
|
250
|
+
}
|
|
251
|
+
function normalizeType(value) {
|
|
252
|
+
return typeof value === "string" ? value.trim().toLowerCase().replaceAll("_", "-") : "";
|
|
253
|
+
}
|
|
254
|
+
function optionalSanitizedValue(result) {
|
|
255
|
+
if (result.value === OMIT_IMAGE) return void 0;
|
|
256
|
+
if (result.changed && isEmptyContainer(result.value)) return void 0;
|
|
257
|
+
return result.value;
|
|
258
|
+
}
|
|
259
|
+
function isEmptyContainer(value) {
|
|
260
|
+
return Array.isArray(value) ? value.length === 0 : isRecord(value) && Object.keys(value).length === 0;
|
|
261
|
+
}
|
|
262
|
+
function isRecord(value) {
|
|
263
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
264
|
+
}
|
|
265
|
+
function parseValue(value) {
|
|
266
|
+
try {
|
|
267
|
+
return JSON.parse(value);
|
|
268
|
+
} catch {
|
|
269
|
+
return value;
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
function parseArray(value) {
|
|
273
|
+
const parsed = parseValue(value);
|
|
274
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
275
|
+
}
|
|
276
|
+
function parseObject(value) {
|
|
277
|
+
const parsed = parseValue(value);
|
|
278
|
+
return isRecord(parsed) ? parsed : {};
|
|
279
|
+
}
|
|
280
|
+
|
|
137
281
|
// src/db/memory.store.ts
|
|
138
282
|
var MESSAGES_TABLE = "messages";
|
|
139
283
|
var TOPICS_TABLE = "topics";
|
|
@@ -596,17 +740,28 @@ var MemoryStore = class {
|
|
|
596
740
|
const messageRows = await this.provider.query(MESSAGES_TABLE);
|
|
597
741
|
report.messagesScanned = messageRows.length;
|
|
598
742
|
for (const row of messageRows) {
|
|
743
|
+
const sanitized = sanitizeStoredMessageImages(rowToMessage(row)).message;
|
|
599
744
|
const usage2 = countTokens(
|
|
600
745
|
[
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
746
|
+
sanitized.content,
|
|
747
|
+
sanitized.parts,
|
|
748
|
+
sanitized.metadata,
|
|
749
|
+
sanitized.payload ?? "",
|
|
750
|
+
sanitized.contextPayload ?? ""
|
|
606
751
|
].map(String).join("\n")
|
|
607
752
|
);
|
|
608
|
-
|
|
609
|
-
|
|
753
|
+
const values = {
|
|
754
|
+
parts: sanitized.parts,
|
|
755
|
+
payload: sanitized.payload ?? null,
|
|
756
|
+
context_payload: sanitized.contextPayload ?? null,
|
|
757
|
+
metadata: sanitized.metadata,
|
|
758
|
+
usage: usage2
|
|
759
|
+
};
|
|
760
|
+
const changed = Object.entries(values).some(
|
|
761
|
+
([key, value]) => !migrationValueEquals(row[key], value)
|
|
762
|
+
);
|
|
763
|
+
if (!changed) continue;
|
|
764
|
+
await this.provider.update(MESSAGES_TABLE, values, [
|
|
610
765
|
eq("message_id", String(row.message_id))
|
|
611
766
|
]);
|
|
612
767
|
report.messagesUpdated++;
|
package/dist/index.d.ts
CHANGED
|
@@ -421,10 +421,14 @@ interface MemoryConfig {
|
|
|
421
421
|
contextUsageRatio?: number;
|
|
422
422
|
/** 原始消息达到其可用预算的此比例时后台预压缩,默认 0.75。 */
|
|
423
423
|
precompressionRatio?: number;
|
|
424
|
-
/**
|
|
424
|
+
/** 单次压缩目标占当前未压缩消息 token 总量的比例,默认 0.7(压 7 留 3)。 */
|
|
425
425
|
compressionBatchRatio?: number;
|
|
426
426
|
/** 单次压缩硬上限;0 表示只受 compressionBatchRatio 控制。 */
|
|
427
427
|
compressionBatchTokenLimit?: number;
|
|
428
|
+
/** 压缩模型返回非法 JSON、缺字段或空摘要时的最大重试次数,默认 2(即最多 3 次)。 */
|
|
429
|
+
compressionMaxRetries?: number;
|
|
430
|
+
/** 压缩结果级重试的指数退避基数(毫秒),默认 500;0 表示不等待。 */
|
|
431
|
+
compressionRetryBaseDelayMs?: number;
|
|
428
432
|
/** 单条 Topic 摘要硬上限,实际长度由重要性决定,默认 2048。 */
|
|
429
433
|
topicSummaryMaxTokens?: number;
|
|
430
434
|
/** 未传 getHistoryWindow 模型窗口时使用,默认 256K。 */
|
package/dist/index.js
CHANGED
|
@@ -48,8 +48,16 @@ function resolveConfig(config) {
|
|
|
48
48
|
),
|
|
49
49
|
contextUsageRatio: ratio(config.contextUsageRatio ?? 0.75, "contextUsageRatio"),
|
|
50
50
|
precompressionRatio: ratio(config.precompressionRatio ?? 0.75, "precompressionRatio"),
|
|
51
|
-
compressionBatchRatio: ratio(config.compressionBatchRatio ?? 0.
|
|
51
|
+
compressionBatchRatio: ratio(config.compressionBatchRatio ?? 0.7, "compressionBatchRatio"),
|
|
52
52
|
compressionBatchTokenLimit: Math.max(0, Math.floor(config.compressionBatchTokenLimit ?? 0)),
|
|
53
|
+
compressionMaxRetries: nonNegativeInt(
|
|
54
|
+
config.compressionMaxRetries ?? 2,
|
|
55
|
+
"compressionMaxRetries"
|
|
56
|
+
),
|
|
57
|
+
compressionRetryBaseDelayMs: nonNegativeInt(
|
|
58
|
+
config.compressionRetryBaseDelayMs ?? 500,
|
|
59
|
+
"compressionRetryBaseDelayMs"
|
|
60
|
+
),
|
|
53
61
|
topicSummaryMaxTokens: positiveInt(
|
|
54
62
|
config.topicSummaryMaxTokens ?? config.detailMaxTokens ?? 2048,
|
|
55
63
|
"topicSummaryMaxTokens"
|
|
@@ -97,6 +105,12 @@ function ratio(value, name) {
|
|
|
97
105
|
}
|
|
98
106
|
return value;
|
|
99
107
|
}
|
|
108
|
+
function nonNegativeInt(value, name) {
|
|
109
|
+
if (!Number.isFinite(value) || value < 0) {
|
|
110
|
+
throw new Error(`[MemoryConfig] ${name} \u5FC5\u987B\u662F\u975E\u8D1F\u6570`);
|
|
111
|
+
}
|
|
112
|
+
return Math.floor(value);
|
|
113
|
+
}
|
|
100
114
|
function positiveInt(value, name) {
|
|
101
115
|
if (!Number.isFinite(value) || value <= 0) {
|
|
102
116
|
throw new Error(`[MemoryConfig] ${name} \u5FC5\u987B\u662F\u6B63\u6570`);
|
|
@@ -255,6 +269,18 @@ import { GrafeoDB } from "@grafeo-db/js";
|
|
|
255
269
|
import { get_encoding } from "@dqbd/tiktoken";
|
|
256
270
|
|
|
257
271
|
// src/llm/http.ts
|
|
272
|
+
var HttpRequestError = class extends Error {
|
|
273
|
+
retryable;
|
|
274
|
+
attempts;
|
|
275
|
+
status;
|
|
276
|
+
constructor(message, options) {
|
|
277
|
+
super(message, options.cause === void 0 ? void 0 : { cause: options.cause });
|
|
278
|
+
this.name = "HttpRequestError";
|
|
279
|
+
this.retryable = options.retryable;
|
|
280
|
+
this.attempts = options.attempts;
|
|
281
|
+
this.status = options.status;
|
|
282
|
+
}
|
|
283
|
+
};
|
|
258
284
|
function normalizeFetchError(err, timeoutMs, errorLabel) {
|
|
259
285
|
if (isAbortError(err)) {
|
|
260
286
|
return new Error(`${errorLabel} request timed out after ${timeoutMs}ms`, { cause: err });
|
|
@@ -291,15 +317,36 @@ async function postJsonWithRetry(url, headers, body, opts, errorLabel = "HTTP")
|
|
|
291
317
|
await sleep(backoffDelay(attempt));
|
|
292
318
|
continue;
|
|
293
319
|
}
|
|
294
|
-
throw lastErr
|
|
320
|
+
throw new HttpRequestError(lastErr.message, {
|
|
321
|
+
retryable: true,
|
|
322
|
+
attempts: attempt + 1,
|
|
323
|
+
cause: lastErr
|
|
324
|
+
});
|
|
295
325
|
}
|
|
296
326
|
clearTimeout(timer);
|
|
297
327
|
if (res.ok) {
|
|
298
|
-
|
|
328
|
+
try {
|
|
329
|
+
return await res.json();
|
|
330
|
+
} catch (err) {
|
|
331
|
+
lastErr = new HttpRequestError(`${errorLabel} returned invalid JSON`, {
|
|
332
|
+
retryable: true,
|
|
333
|
+
attempts: attempt + 1,
|
|
334
|
+
cause: err
|
|
335
|
+
});
|
|
336
|
+
if (attempt < opts.maxRetries) {
|
|
337
|
+
await sleep(backoffDelay(attempt));
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
throw lastErr;
|
|
341
|
+
}
|
|
299
342
|
}
|
|
300
343
|
const text = await res.text().catch(() => "");
|
|
301
|
-
lastErr = new Error(`${errorLabel} error ${res.status}: ${text}`);
|
|
302
344
|
const retryable = res.status >= 500 || res.status === 429;
|
|
345
|
+
lastErr = new HttpRequestError(`${errorLabel} error ${res.status}: ${text}`, {
|
|
346
|
+
retryable,
|
|
347
|
+
attempts: attempt + 1,
|
|
348
|
+
status: res.status
|
|
349
|
+
});
|
|
303
350
|
if (retryable && attempt < opts.maxRetries) {
|
|
304
351
|
await sleep(backoffDelay(attempt));
|
|
305
352
|
continue;
|
|
@@ -802,6 +849,136 @@ function eq(field, value) {
|
|
|
802
849
|
return { op: "eq", field, value };
|
|
803
850
|
}
|
|
804
851
|
|
|
852
|
+
// src/history-image.ts
|
|
853
|
+
var OMIT_IMAGE = Symbol("omit-history-image");
|
|
854
|
+
var IMAGE_PART_TYPES = /* @__PURE__ */ new Set([
|
|
855
|
+
"image",
|
|
856
|
+
"image-data",
|
|
857
|
+
"image-url",
|
|
858
|
+
"image-file-id",
|
|
859
|
+
"image-file-reference",
|
|
860
|
+
"input-image",
|
|
861
|
+
"output-image"
|
|
862
|
+
]);
|
|
863
|
+
function sanitizeRawHistoryFields(message) {
|
|
864
|
+
const partsResult = sanitizeValue(message.parts ?? []);
|
|
865
|
+
const payloadResult = sanitizeValue(message.payload);
|
|
866
|
+
const contextResult = sanitizeValue(message.contextPayload);
|
|
867
|
+
const metadataResult = sanitizeValue(message.metadata ?? {});
|
|
868
|
+
const payload = optionalSanitizedValue(payloadResult);
|
|
869
|
+
const contextPayload = optionalSanitizedValue(contextResult);
|
|
870
|
+
return {
|
|
871
|
+
parts: Array.isArray(partsResult.value) ? partsResult.value : [],
|
|
872
|
+
...payload !== void 0 && { payload },
|
|
873
|
+
...contextPayload !== void 0 && { contextPayload },
|
|
874
|
+
metadata: isRecord(metadataResult.value) ? metadataResult.value : {},
|
|
875
|
+
removedImages: partsResult.removedImages + payloadResult.removedImages + contextResult.removedImages + metadataResult.removedImages
|
|
876
|
+
};
|
|
877
|
+
}
|
|
878
|
+
function sanitizeStoredMessageImages(message) {
|
|
879
|
+
const parsed = {
|
|
880
|
+
content: message.content,
|
|
881
|
+
parts: parseArray(message.parts),
|
|
882
|
+
...message.payload !== void 0 && { payload: parseValue(message.payload) },
|
|
883
|
+
...message.contextPayload !== void 0 && {
|
|
884
|
+
contextPayload: parseValue(message.contextPayload)
|
|
885
|
+
},
|
|
886
|
+
metadata: parseObject(message.metadata)
|
|
887
|
+
};
|
|
888
|
+
const sanitized = sanitizeRawHistoryFields(parsed);
|
|
889
|
+
if (sanitized.removedImages === 0) {
|
|
890
|
+
return { message, changed: false, removedImages: 0 };
|
|
891
|
+
}
|
|
892
|
+
const parts = JSON.stringify(sanitized.parts);
|
|
893
|
+
const payload = sanitized.payload === void 0 ? void 0 : JSON.stringify(sanitized.payload);
|
|
894
|
+
const contextPayload = sanitized.contextPayload === void 0 ? void 0 : JSON.stringify(sanitized.contextPayload);
|
|
895
|
+
const metadata = JSON.stringify(sanitized.metadata);
|
|
896
|
+
const usage = countTokens(
|
|
897
|
+
[message.content, parts, metadata, payload ?? "", contextPayload ?? ""].join("\n")
|
|
898
|
+
);
|
|
899
|
+
return {
|
|
900
|
+
message: {
|
|
901
|
+
...message,
|
|
902
|
+
parts,
|
|
903
|
+
payload,
|
|
904
|
+
contextPayload,
|
|
905
|
+
usage,
|
|
906
|
+
metadata
|
|
907
|
+
},
|
|
908
|
+
changed: true,
|
|
909
|
+
removedImages: sanitized.removedImages
|
|
910
|
+
};
|
|
911
|
+
}
|
|
912
|
+
function sanitizeValue(value) {
|
|
913
|
+
if (typeof value === "string" && /^data:image\//i.test(value.trim())) {
|
|
914
|
+
return { value: OMIT_IMAGE, removedImages: 1, changed: true };
|
|
915
|
+
}
|
|
916
|
+
if (value == null || typeof value !== "object") {
|
|
917
|
+
return { value, removedImages: 0, changed: false };
|
|
918
|
+
}
|
|
919
|
+
if (isImageNode(value)) {
|
|
920
|
+
return { value: OMIT_IMAGE, removedImages: 1, changed: true };
|
|
921
|
+
}
|
|
922
|
+
if (Array.isArray(value)) {
|
|
923
|
+
const out2 = [];
|
|
924
|
+
let removedImages2 = 0;
|
|
925
|
+
let changed2 = false;
|
|
926
|
+
for (const item of value) {
|
|
927
|
+
const sanitized = sanitizeValue(item);
|
|
928
|
+
removedImages2 += sanitized.removedImages;
|
|
929
|
+
changed2 ||= sanitized.changed;
|
|
930
|
+
if (sanitized.value !== OMIT_IMAGE) out2.push(sanitized.value);
|
|
931
|
+
}
|
|
932
|
+
return { value: changed2 ? out2 : value, removedImages: removedImages2, changed: changed2 };
|
|
933
|
+
}
|
|
934
|
+
const out = {};
|
|
935
|
+
let removedImages = 0;
|
|
936
|
+
let changed = false;
|
|
937
|
+
for (const [key, item] of Object.entries(value)) {
|
|
938
|
+
const sanitized = sanitizeValue(item);
|
|
939
|
+
removedImages += sanitized.removedImages;
|
|
940
|
+
changed ||= sanitized.changed;
|
|
941
|
+
if (sanitized.value !== OMIT_IMAGE) out[key] = sanitized.value;
|
|
942
|
+
}
|
|
943
|
+
return { value: changed ? out : value, removedImages, changed };
|
|
944
|
+
}
|
|
945
|
+
function isImageNode(value) {
|
|
946
|
+
if (!isRecord(value)) return false;
|
|
947
|
+
const type = normalizeType(value.type);
|
|
948
|
+
if (IMAGE_PART_TYPES.has(type)) return true;
|
|
949
|
+
const mediaType = String(value.mediaType ?? value.mimeType ?? "").trim().toLowerCase();
|
|
950
|
+
return mediaType.startsWith("image/") && (type === "file" || "data" in value || "url" in value || "fileId" in value || "providerReference" in value);
|
|
951
|
+
}
|
|
952
|
+
function normalizeType(value) {
|
|
953
|
+
return typeof value === "string" ? value.trim().toLowerCase().replaceAll("_", "-") : "";
|
|
954
|
+
}
|
|
955
|
+
function optionalSanitizedValue(result) {
|
|
956
|
+
if (result.value === OMIT_IMAGE) return void 0;
|
|
957
|
+
if (result.changed && isEmptyContainer(result.value)) return void 0;
|
|
958
|
+
return result.value;
|
|
959
|
+
}
|
|
960
|
+
function isEmptyContainer(value) {
|
|
961
|
+
return Array.isArray(value) ? value.length === 0 : isRecord(value) && Object.keys(value).length === 0;
|
|
962
|
+
}
|
|
963
|
+
function isRecord(value) {
|
|
964
|
+
return !!value && typeof value === "object" && !Array.isArray(value);
|
|
965
|
+
}
|
|
966
|
+
function parseValue(value) {
|
|
967
|
+
try {
|
|
968
|
+
return JSON.parse(value);
|
|
969
|
+
} catch {
|
|
970
|
+
return value;
|
|
971
|
+
}
|
|
972
|
+
}
|
|
973
|
+
function parseArray(value) {
|
|
974
|
+
const parsed = parseValue(value);
|
|
975
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
976
|
+
}
|
|
977
|
+
function parseObject(value) {
|
|
978
|
+
const parsed = parseValue(value);
|
|
979
|
+
return isRecord(parsed) ? parsed : {};
|
|
980
|
+
}
|
|
981
|
+
|
|
805
982
|
// src/db/memory.store.ts
|
|
806
983
|
var MESSAGES_TABLE = "messages";
|
|
807
984
|
var TOPICS_TABLE = "topics";
|
|
@@ -1264,17 +1441,28 @@ var MemoryStore = class {
|
|
|
1264
1441
|
const messageRows = await this.provider.query(MESSAGES_TABLE);
|
|
1265
1442
|
report.messagesScanned = messageRows.length;
|
|
1266
1443
|
for (const row of messageRows) {
|
|
1444
|
+
const sanitized = sanitizeStoredMessageImages(rowToMessage(row)).message;
|
|
1267
1445
|
const usage = countTokens(
|
|
1268
1446
|
[
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1447
|
+
sanitized.content,
|
|
1448
|
+
sanitized.parts,
|
|
1449
|
+
sanitized.metadata,
|
|
1450
|
+
sanitized.payload ?? "",
|
|
1451
|
+
sanitized.contextPayload ?? ""
|
|
1274
1452
|
].map(String).join("\n")
|
|
1275
1453
|
);
|
|
1276
|
-
|
|
1277
|
-
|
|
1454
|
+
const values = {
|
|
1455
|
+
parts: sanitized.parts,
|
|
1456
|
+
payload: sanitized.payload ?? null,
|
|
1457
|
+
context_payload: sanitized.contextPayload ?? null,
|
|
1458
|
+
metadata: sanitized.metadata,
|
|
1459
|
+
usage
|
|
1460
|
+
};
|
|
1461
|
+
const changed = Object.entries(values).some(
|
|
1462
|
+
([key, value]) => !migrationValueEquals(row[key], value)
|
|
1463
|
+
);
|
|
1464
|
+
if (!changed) continue;
|
|
1465
|
+
await this.provider.update(MESSAGES_TABLE, values, [
|
|
1278
1466
|
eq("message_id", String(row.message_id))
|
|
1279
1467
|
]);
|
|
1280
1468
|
report.messagesUpdated++;
|
|
@@ -2095,7 +2283,7 @@ function stripToolCallContent(value) {
|
|
|
2095
2283
|
if (Array.isArray(value)) {
|
|
2096
2284
|
return value.map((item) => stripToolCallContent(item)).filter((item) => item !== OMIT_COMPRESSION_VALUE);
|
|
2097
2285
|
}
|
|
2098
|
-
if (!
|
|
2286
|
+
if (!isRecord2(value)) return value;
|
|
2099
2287
|
if (isToolCallBlock(value)) return OMIT_COMPRESSION_VALUE;
|
|
2100
2288
|
const sanitized = {};
|
|
2101
2289
|
for (const [key, child] of Object.entries(value)) {
|
|
@@ -2120,7 +2308,7 @@ function isToolCallBlock(value) {
|
|
|
2120
2308
|
function normalizeToolKey(value) {
|
|
2121
2309
|
return value.replace(/[\s_-]/g, "").toLowerCase();
|
|
2122
2310
|
}
|
|
2123
|
-
function
|
|
2311
|
+
function isRecord2(value) {
|
|
2124
2312
|
return value !== null && typeof value === "object";
|
|
2125
2313
|
}
|
|
2126
2314
|
|
|
@@ -2221,7 +2409,10 @@ var CompressManager = class {
|
|
|
2221
2409
|
const rawLimit = Math.max(1, rawLimitOverride ?? this.rawLimit(entry.lastModelContextTokens));
|
|
2222
2410
|
const threshold = Math.max(1, Math.floor(rawLimit * this.config.precompressionRatio));
|
|
2223
2411
|
if (!force && entry.totalTokens < threshold) return;
|
|
2224
|
-
let batchTarget = Math.max(
|
|
2412
|
+
let batchTarget = Math.max(
|
|
2413
|
+
1,
|
|
2414
|
+
Math.floor(entry.totalTokens * this.config.compressionBatchRatio)
|
|
2415
|
+
);
|
|
2225
2416
|
if (this.config.compressionBatchTokenLimit > 0) {
|
|
2226
2417
|
batchTarget = Math.min(batchTarget, this.config.compressionBatchTokenLimit);
|
|
2227
2418
|
}
|
|
@@ -2229,7 +2420,11 @@ var CompressManager = class {
|
|
|
2229
2420
|
if (messages.length === 0) return;
|
|
2230
2421
|
let result;
|
|
2231
2422
|
try {
|
|
2232
|
-
result = await this.
|
|
2423
|
+
result = await this.summarizeWithRetry(
|
|
2424
|
+
sessionId,
|
|
2425
|
+
"raw-message compression",
|
|
2426
|
+
() => this.llm.summarizeMessages(messages)
|
|
2427
|
+
);
|
|
2233
2428
|
} catch (error) {
|
|
2234
2429
|
console.error(
|
|
2235
2430
|
`[CompressManager] LLM compress failed for session ${sessionId}: ${formatError(error)}`
|
|
@@ -2237,7 +2432,6 @@ var CompressManager = class {
|
|
|
2237
2432
|
throw error;
|
|
2238
2433
|
}
|
|
2239
2434
|
const summary = result.summary.trim();
|
|
2240
|
-
if (!summary) throw new Error(`LLM returned an empty summary for session ${sessionId}`);
|
|
2241
2435
|
const vector = await this.embed.embedOne(summary).catch((error) => {
|
|
2242
2436
|
console.error(`[CompressManager] Embed topic failed for session ${sessionId}:`, error);
|
|
2243
2437
|
return [];
|
|
@@ -2319,9 +2513,12 @@ var CompressManager = class {
|
|
|
2319
2513
|
Math.max(1, tokenLimit - unselectedTokens)
|
|
2320
2514
|
)
|
|
2321
2515
|
);
|
|
2322
|
-
const result = await this.
|
|
2516
|
+
const result = await this.summarizeWithRetry(
|
|
2517
|
+
sessionId,
|
|
2518
|
+
"Topic compaction",
|
|
2519
|
+
() => this.llm.summarizeTopics(selected, maxSummaryTokens)
|
|
2520
|
+
);
|
|
2323
2521
|
const summary = result.summary.trim();
|
|
2324
|
-
if (!summary) throw new Error(`LLM returned an empty Topic rollup for session ${sessionId}`);
|
|
2325
2522
|
const vector = persist ? await this.embed.embedOne(summary).catch(() => []) : [];
|
|
2326
2523
|
const first = selected[0];
|
|
2327
2524
|
const last = selected[selected.length - 1];
|
|
@@ -2394,6 +2591,37 @@ var CompressManager = class {
|
|
|
2394
2591
|
const embeddings = new Map(names.map((name, index) => [name, vectors[index]]));
|
|
2395
2592
|
await this.grafeo.upsertEntitiesAndRelations(entities, relations, embeddings);
|
|
2396
2593
|
}
|
|
2594
|
+
/**
|
|
2595
|
+
* HTTP transport already owns bounded retries for network/timeout/5xx/429.
|
|
2596
|
+
* This outer layer retries semantic compression failures only: malformed model
|
|
2597
|
+
* content, missing fields and empty summaries. That avoids retry multiplication.
|
|
2598
|
+
*/
|
|
2599
|
+
async summarizeWithRetry(sessionId, label, operation) {
|
|
2600
|
+
let lastError;
|
|
2601
|
+
for (let attempt = 0; attempt <= this.config.compressionMaxRetries; attempt++) {
|
|
2602
|
+
try {
|
|
2603
|
+
const result = await operation();
|
|
2604
|
+
if (typeof result?.summary !== "string" || !result.summary.trim()) {
|
|
2605
|
+
throw new Error(`${label} returned an empty summary`);
|
|
2606
|
+
}
|
|
2607
|
+
return result;
|
|
2608
|
+
} catch (error) {
|
|
2609
|
+
lastError = error;
|
|
2610
|
+
if (error instanceof HttpRequestError || attempt >= this.config.compressionMaxRetries) {
|
|
2611
|
+
throw error;
|
|
2612
|
+
}
|
|
2613
|
+
const delayMs = Math.min(
|
|
2614
|
+
this.config.compressionRetryBaseDelayMs * 2 ** attempt,
|
|
2615
|
+
8e3
|
|
2616
|
+
);
|
|
2617
|
+
console.warn(
|
|
2618
|
+
`[CompressManager] ${label} failed for session ${sessionId}; retrying ${attempt + 1}/${this.config.compressionMaxRetries} after ${delayMs}ms: ` + formatError(error)
|
|
2619
|
+
);
|
|
2620
|
+
if (delayMs > 0) await delay(delayMs);
|
|
2621
|
+
}
|
|
2622
|
+
}
|
|
2623
|
+
throw lastError;
|
|
2624
|
+
}
|
|
2397
2625
|
};
|
|
2398
2626
|
function sumTopicTokens(topics) {
|
|
2399
2627
|
return topics.reduce((sum, topic) => sum + Math.max(1, topic.tokens), 0);
|
|
@@ -2404,6 +2632,9 @@ function topicOrder(a, b) {
|
|
|
2404
2632
|
function formatError(error) {
|
|
2405
2633
|
return error instanceof Error ? error.message : String(error);
|
|
2406
2634
|
}
|
|
2635
|
+
function delay(ms) {
|
|
2636
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
2637
|
+
}
|
|
2407
2638
|
function withTimeout(promise, timeoutMs, label) {
|
|
2408
2639
|
let timer;
|
|
2409
2640
|
const timeout = new Promise((_, reject) => {
|
|
@@ -3039,9 +3270,14 @@ var SessionCache = class {
|
|
|
3039
3270
|
if (messages.length === 0 || targetTokens <= 0) return [];
|
|
3040
3271
|
const selected = [];
|
|
3041
3272
|
let tokens = 0;
|
|
3042
|
-
|
|
3273
|
+
const selectableCount = messages.length > 1 ? messages.length - 1 : 1;
|
|
3274
|
+
for (const message of messages.slice(0, selectableCount)) {
|
|
3275
|
+
const nextTokens = tokens + effectiveUsage(message);
|
|
3276
|
+
if (selected.length > 0 && Math.abs(targetTokens - tokens) <= Math.abs(targetTokens - nextTokens)) {
|
|
3277
|
+
break;
|
|
3278
|
+
}
|
|
3043
3279
|
selected.push(message);
|
|
3044
|
-
tokens
|
|
3280
|
+
tokens = nextTokens;
|
|
3045
3281
|
if (tokens >= targetTokens) break;
|
|
3046
3282
|
}
|
|
3047
3283
|
return selected;
|
|
@@ -3286,11 +3522,20 @@ var MemoryManager = class {
|
|
|
3286
3522
|
task = (async () => {
|
|
3287
3523
|
const allTopics = await this.store.getTopicsBySession(sessionId);
|
|
3288
3524
|
const latestTopic = allTopics.at(-1);
|
|
3289
|
-
const
|
|
3525
|
+
const loadedRawAfterBoundary = latestTopic ? await this.store.getMessagesAfterBoundary(
|
|
3290
3526
|
sessionId,
|
|
3291
3527
|
latestTopic.endTime,
|
|
3292
3528
|
latestTopic.endMessageId
|
|
3293
3529
|
) : await this.store.getAllMessagesBySession(sessionId);
|
|
3530
|
+
const sanitizedRows = loadedRawAfterBoundary.map(sanitizeStoredMessageImages);
|
|
3531
|
+
const repaired = sanitizedRows.filter((row) => row.changed);
|
|
3532
|
+
if (repaired.length > 0) {
|
|
3533
|
+
await this.store.upsertMessages(repaired.map((row) => row.message));
|
|
3534
|
+
console.info(
|
|
3535
|
+
`[MemoryManager] stripped ${repaired.reduce((sum, row) => sum + row.removedImages, 0)} historical image block(s) from ${repaired.length} message(s) in session ${sessionId}.`
|
|
3536
|
+
);
|
|
3537
|
+
}
|
|
3538
|
+
const allRawAfterBoundary = sanitizedRows.map((row) => row.message);
|
|
3294
3539
|
const since = this.config.maxHistoryAgeMs > 0 ? Date.now() - this.config.maxHistoryAgeMs : 0;
|
|
3295
3540
|
const rawMessages = since > 0 ? allRawAfterBoundary.filter((message) => message.createdAt >= since) : allRawAfterBoundary;
|
|
3296
3541
|
const session = this.sessionMap.get(sessionId);
|
|
@@ -3340,10 +3585,11 @@ var MemoryManager = class {
|
|
|
3340
3585
|
const now = Date.now();
|
|
3341
3586
|
await this.ensureSessionHydrated(sessionId, { chatId, userId });
|
|
3342
3587
|
const normalized = messages.map((message) => {
|
|
3343
|
-
const
|
|
3344
|
-
const
|
|
3345
|
-
const
|
|
3346
|
-
const
|
|
3588
|
+
const sanitized = sanitizeRawHistoryFields(message);
|
|
3589
|
+
const parts = JSON.stringify(sanitized.parts);
|
|
3590
|
+
const metadata = JSON.stringify(sanitized.metadata);
|
|
3591
|
+
const payload = sanitized.payload === void 0 ? void 0 : JSON.stringify(sanitized.payload);
|
|
3592
|
+
const contextPayload = sanitized.contextPayload === void 0 ? void 0 : JSON.stringify(sanitized.contextPayload);
|
|
3347
3593
|
const tokenInput = [message.content, parts, metadata, payload ?? "", contextPayload ?? ""].join("\n");
|
|
3348
3594
|
return {
|
|
3349
3595
|
messageId: message.messageId ?? uuidv44(),
|
|
@@ -3356,7 +3602,9 @@ var MemoryManager = class {
|
|
|
3356
3602
|
parts,
|
|
3357
3603
|
payload,
|
|
3358
3604
|
contextPayload,
|
|
3359
|
-
usage
|
|
3605
|
+
// Host-provided usage describes the original payload. Once image bytes are
|
|
3606
|
+
// stripped, recalculate against the actual persisted history budget.
|
|
3607
|
+
usage: sanitized.removedImages > 0 ? countTokens(tokenInput) : message.usage ?? countTokens(tokenInput),
|
|
3360
3608
|
metadata,
|
|
3361
3609
|
createdAt: message.createdAt ?? now
|
|
3362
3610
|
};
|
|
@@ -3895,23 +4143,24 @@ var MemoryManager = class {
|
|
|
3895
4143
|
}
|
|
3896
4144
|
};
|
|
3897
4145
|
function toMemoryRawMessage(message) {
|
|
3898
|
-
const
|
|
3899
|
-
const
|
|
3900
|
-
const
|
|
4146
|
+
const sanitized = sanitizeStoredMessageImages(message).message;
|
|
4147
|
+
const parts = safeParseArray(sanitized.parts);
|
|
4148
|
+
const payload = sanitized.payload === void 0 ? void 0 : safeParseValue(sanitized.payload);
|
|
4149
|
+
const contextPayload = sanitized.contextPayload === void 0 ? void 0 : safeParseValue(sanitized.contextPayload);
|
|
3901
4150
|
return {
|
|
3902
|
-
messageId:
|
|
3903
|
-
talkerId:
|
|
3904
|
-
chatId:
|
|
3905
|
-
userId:
|
|
3906
|
-
sessionId:
|
|
3907
|
-
type:
|
|
3908
|
-
content:
|
|
4151
|
+
messageId: sanitized.messageId,
|
|
4152
|
+
talkerId: sanitized.talkerId,
|
|
4153
|
+
chatId: sanitized.chatId,
|
|
4154
|
+
userId: sanitized.userId,
|
|
4155
|
+
sessionId: sanitized.sessionId,
|
|
4156
|
+
type: sanitized.type,
|
|
4157
|
+
content: sanitized.content,
|
|
3909
4158
|
...parts.length > 0 && { parts },
|
|
3910
4159
|
...payload !== void 0 && { payload },
|
|
3911
4160
|
...contextPayload !== void 0 && { contextPayload },
|
|
3912
|
-
usage:
|
|
3913
|
-
metadata: safeParseObject2(
|
|
3914
|
-
createdAt:
|
|
4161
|
+
usage: sanitized.usage,
|
|
4162
|
+
metadata: safeParseObject2(sanitized.metadata),
|
|
4163
|
+
createdAt: sanitized.createdAt
|
|
3915
4164
|
};
|
|
3916
4165
|
}
|
|
3917
4166
|
function buildCompressionRevision(topics, messages) {
|
package/llms.txt
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @ppagent/memory
|
|
2
2
|
|
|
3
|
-
> `@ppagent/memory` is an independent TypeScript/Node.js long-term memory package for AI agents. It stores
|
|
3
|
+
> `@ppagent/memory` is an independent TypeScript/Node.js long-term memory package for AI agents. It stores semantic raw conversation history in a pluggable vector store (LanceDB or SQLite/sqlite-vec, auto-detected per platform), strips historical image payloads, builds importance-aware Topic summaries, returns dynamically budgeted Topics plus recent uncompressed messages as one context window, extracts and searches entity relationships with Grafeo, supports cached user/chat facts, and ingests Markdown documents as a searchable knowledge base.
|
|
4
4
|
|
|
5
5
|
This file is written for AI coding agents and assistants. Use it as the primary package guide when generating code that depends on `@ppagent/memory`.
|
|
6
6
|
|
|
@@ -154,8 +154,10 @@ const memory = new MemoryManager({
|
|
|
154
154
|
topicCompactionSyncRatio: 1.25,
|
|
155
155
|
contextUsageRatio: 0.75,
|
|
156
156
|
precompressionRatio: 0.75,
|
|
157
|
-
compressionBatchRatio: 0.
|
|
157
|
+
compressionBatchRatio: 0.7,
|
|
158
158
|
compressionBatchTokenLimit: 0,
|
|
159
|
+
compressionMaxRetries: 2,
|
|
160
|
+
compressionRetryBaseDelayMs: 500,
|
|
159
161
|
topicSummaryMaxTokens: 2048,
|
|
160
162
|
defaultModelContextTokens: 256 * 1024,
|
|
161
163
|
maxHistoryAgeMs: 0,
|
|
@@ -327,7 +329,7 @@ Embedding fallback behavior:
|
|
|
327
329
|
HTTP and retry fields:
|
|
328
330
|
|
|
329
331
|
- `httpTimeoutMs?: number` - timeout per LLM or embedding HTTP attempt. Default: `60000`.
|
|
330
|
-
- `httpMaxRetries?: number` - retry count after the first attempt for network errors, timeout/abort, HTTP `429`,
|
|
332
|
+
- `httpMaxRetries?: number` - retry count after the first attempt for network errors, timeout/abort, HTTP `429`, HTTP `5xx`, and invalid JSON in a successful HTTP response. Default: `2`.
|
|
331
333
|
- `embeddingBatchSize?: number` - max texts per embedding request. Default: `20`.
|
|
332
334
|
- `embeddingConcurrency?: number` - concurrent embedding batches. Default: `2`.
|
|
333
335
|
|
|
@@ -338,8 +340,10 @@ Conversation memory fields:
|
|
|
338
340
|
- `topicCompactionSyncRatio?: number` - a cold-start Topic overage blocks `getHistoryWindow` only after it exceeds this multiple of the effective Topic budget, unless Topic plus raw history already exceeds the usable window. Default: `1.25`.
|
|
339
341
|
- `contextUsageRatio?: number` - fraction of the current model context available to conversation history. Default: `0.75`.
|
|
340
342
|
- `precompressionRatio?: number` - start background raw-message compression when raw usage reaches this fraction of its dynamic budget. Default: `0.75`.
|
|
341
|
-
- `compressionBatchRatio?: number` - target fraction of the raw-message
|
|
343
|
+
- `compressionBatchRatio?: number` - target fraction of the current uncompressed raw-message tokens compressed from the oldest edge in one batch. Default: `0.7`, leaving the newest approximately `0.3` verbatim; message boundaries are atomic.
|
|
342
344
|
- `compressionBatchTokenLimit?: number` - optional hard limit for one compression batch. Default: `0` (ratio only).
|
|
345
|
+
- `compressionMaxRetries?: number` - result-level retry count after the first compression-model attempt when the returned content is malformed, missing a summary, or empty. Default: `2`. Transport failures do not multiply this count because the HTTP layer already owns bounded retries.
|
|
346
|
+
- `compressionRetryBaseDelayMs?: number` - exponential-backoff base for result-level compression retries. Default: `500`; set `0` to retry immediately.
|
|
343
347
|
- `topicSummaryMaxTokens?: number` - hard maximum for one Topic summary. The LLM uses less for low-value chat and more for facts, experience, decisions, preferences, constraints, and reusable knowledge. Default: `2048`.
|
|
344
348
|
- `defaultModelContextTokens?: number` - used when `getHistoryWindow` receives no model size; a warning is logged. Default: `262144`.
|
|
345
349
|
- `maxHistoryAgeMs?: number` - maximum Topic and raw-message age restored during lazy cold start. Default: `0` (all history).
|
|
@@ -374,9 +378,10 @@ The package intentionally separates durable base writes from expensive graph con
|
|
|
374
378
|
`updateChat(messages, opts)`:
|
|
375
379
|
|
|
376
380
|
- Registers its write synchronously, so a caller may intentionally fire-and-forget it and a following `getHistoryWindow` will still wait for that write.
|
|
377
|
-
-
|
|
378
|
-
- `
|
|
379
|
-
-
|
|
381
|
+
- Recursively strips known historical image blocks (`image*` content parts and `file` parts with `image/*` media types) from `parts`, `payload`, `contextPayload`, and `metadata`, while preserving adjacent text, tool protocol, and non-image files. Legacy image rows are lazily repaired and written back when a session is first hydrated; the manual migration command performs the same repair.
|
|
382
|
+
- Calculates tokens over the sanitized `content`, `parts`, `payload`, `contextPayload`, and `metadata`, embeds only the searchable text, and upserts raw messages by stable `messageId` before resolving. If image removal changes a host payload, any host-supplied usage is recalculated against the stored form.
|
|
383
|
+
- `contextPayload` is a replay-only host payload: its non-image structure is stored and returned and counts toward the raw budget, but it is excluded from embedding, FTS, Topic-summary input, and conversation graph extraction.
|
|
384
|
+
- Raw-message token usage counts the complete sanitized stored `metadata`. When a raw batch is summarized, common tool-call/tool-result fields are removed only from the temporary LLM compression input; persisted non-image metadata and uncompressed `recentMessages` remain unchanged.
|
|
380
385
|
- Creates or updates the session record.
|
|
381
386
|
- Updates the in-memory session cache.
|
|
382
387
|
- Once `getHistoryWindow` has supplied the current model size, reaching the precompression threshold starts background compression.
|
|
@@ -394,7 +399,7 @@ The package intentionally separates durable base writes from expensive graph con
|
|
|
394
399
|
|
|
395
400
|
- Lazily hydrates the session from persistent Topics plus raw messages after the newest Topic boundary.
|
|
396
401
|
- Waits for registered message writes. If the hard raw budget is exceeded, it also waits for or starts compression until the returned context fits.
|
|
397
|
-
- Returns `{ compressionRevision, compressedContext, recentMessages, usage }`. `recentMessages` preserves the `RawMessage` input shape, including `messageId`, `parts`, `payload`, `contextPayload`, `metadata`, and `createdAt`. `compressionRevision` changes when the returned Topic/raw compression boundary changes and is intended for host observability rather than optimistic locking.
|
|
402
|
+
- Returns `{ compressionRevision, compressedContext, recentMessages, usage }`. `recentMessages` preserves the sanitized `RawMessage` input shape, including `messageId`, non-image `parts`, `payload`, `contextPayload`, `metadata`, and `createdAt`. `compressionRevision` changes when the returned Topic/raw compression boundary changes and is intended for host observability rather than optimistic locking.
|
|
398
403
|
- The effective Topic budget is `min(compressedContextTokenLimit, usableContextTokens * compressedContextRatio)`; the remaining usable history budget is reserved for raw messages.
|
|
399
404
|
- Cold hydration always restores all persisted Topics and never silently truncates them. Each rollup summarizes the oldest approximately half of the current Topic tokens; a single oversized Topic is re-summarized by itself.
|
|
400
405
|
- A small Topic-only overage returns immediately and schedules a transient background rollup for the next read. It blocks only when the overage exceeds `topicCompactionSyncRatio` or Topic plus raw history cannot fit the usable window.
|
|
@@ -424,9 +429,9 @@ Use `wait: true` when the next line of code must immediately call `searchKnowled
|
|
|
424
429
|
- `sessionId?: string` - session id.
|
|
425
430
|
- `type?: "text" | "image" | "file"` - default: `"text"`.
|
|
426
431
|
- `content: string` - required plain text used for embedding and retrieval.
|
|
427
|
-
- `parts?: ContentPart[]` - optional multimodal content parts; stored serialized.
|
|
428
|
-
- `payload?: unknown` - host-framework message payload stored and returned
|
|
429
|
-
- `contextPayload?: unknown` - replay-only host context stored and returned
|
|
432
|
+
- `parts?: ContentPart[]` - optional multimodal content parts; non-image parts are stored serialized.
|
|
433
|
+
- `payload?: unknown` - host-framework message payload stored and returned after recursive historical-image stripping.
|
|
434
|
+
- `contextPayload?: unknown` - replay-only host context stored and returned after recursive historical-image stripping. Its remaining content counts toward the raw history budget but is excluded from retrieval, compression summaries, and graph extraction.
|
|
430
435
|
- `usage?: number` - token count; estimated with `tiktoken` if omitted.
|
|
431
436
|
- `metadata?: Record<string, unknown>` - custom metadata stored as JSON.
|
|
432
437
|
- `createdAt?: number` - Unix milliseconds; default is current time.
|
|
@@ -622,7 +627,7 @@ Returns the complete model-history window:
|
|
|
622
627
|
|
|
623
628
|
- `compressionRevision?: string` - lightweight identifier for the current Topic/raw compression boundary. Background or blocking compression changes it; ordinary raw appends are not guaranteed to do so.
|
|
624
629
|
- `compressedContext: string` - chronological Topic summaries for the current model-size budget. A soft cold-start overage may be returned once while its transient rollup runs in the background.
|
|
625
|
-
- `recentMessages: MemoryRawMessage[]` - all currently uncompressed raw messages in chronological order, preserving
|
|
630
|
+
- `recentMessages: MemoryRawMessage[]` - all currently uncompressed raw messages in chronological order, preserving sanitized non-image host payload, replay-only context payload, and metadata.
|
|
626
631
|
- `usage` - model size, usable history size, compressed usage, and dynamic raw-message budget/usage.
|
|
627
632
|
|
|
628
633
|
Pass the active model's context size on every read. Omitting it uses `defaultModelContextTokens` (256K by default) and logs a warning.
|