@ppagent/memory 0.4.0 → 0.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +14 -0
- package/dist/index.d.ts +34 -3
- package/dist/index.js +271 -76
- package/llms.txt +15 -7
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -42,6 +42,14 @@ function resolveConfig(config) {
|
|
|
42
42
|
summaryMaxTokens: config.summaryMaxTokens ?? 512,
|
|
43
43
|
conciseMaxTokens: config.conciseMaxTokens ?? 128,
|
|
44
44
|
compressedContextTokenLimit,
|
|
45
|
+
compressedContextRatio: ratio(
|
|
46
|
+
config.compressedContextRatio ?? 0.1,
|
|
47
|
+
"compressedContextRatio"
|
|
48
|
+
),
|
|
49
|
+
topicCompactionSyncRatio: atLeastOne(
|
|
50
|
+
config.topicCompactionSyncRatio ?? 1.25,
|
|
51
|
+
"topicCompactionSyncRatio"
|
|
52
|
+
),
|
|
45
53
|
contextUsageRatio: ratio(config.contextUsageRatio ?? 0.75, "contextUsageRatio"),
|
|
46
54
|
precompressionRatio: ratio(config.precompressionRatio ?? 0.75, "precompressionRatio"),
|
|
47
55
|
compressionBatchRatio: ratio(config.compressionBatchRatio ?? 0.5, "compressionBatchRatio"),
|
|
@@ -99,6 +107,12 @@ function positiveInt(value, name) {
|
|
|
99
107
|
}
|
|
100
108
|
return Math.floor(value);
|
|
101
109
|
}
|
|
110
|
+
function atLeastOne(value, name) {
|
|
111
|
+
if (!Number.isFinite(value) || value < 1) {
|
|
112
|
+
throw new Error(`[MemoryConfig] ${name} \u5FC5\u987B\u662F\u5927\u4E8E\u7B49\u4E8E 1 \u7684\u6709\u9650\u6570`);
|
|
113
|
+
}
|
|
114
|
+
return value;
|
|
115
|
+
}
|
|
102
116
|
|
|
103
117
|
// src/db/store.types.ts
|
|
104
118
|
function eq(field, value) {
|
package/dist/index.d.ts
CHANGED
|
@@ -248,6 +248,16 @@ interface MemoryContextWindowUsage {
|
|
|
248
248
|
rawTokenLimit: number;
|
|
249
249
|
rawTokens: number;
|
|
250
250
|
}
|
|
251
|
+
type MemoryBlockingCompressionReason = "pending" | "raw" | "topics";
|
|
252
|
+
interface MemoryBlockingCompressionEvent {
|
|
253
|
+
sessionId: string;
|
|
254
|
+
phase: "start" | "end";
|
|
255
|
+
reason: MemoryBlockingCompressionReason;
|
|
256
|
+
}
|
|
257
|
+
interface GetHistoryWindowOptions {
|
|
258
|
+
/** 仅当前读取确实被压缩阻塞时调用;回调异常不会影响记忆读取。 */
|
|
259
|
+
onBlockingCompression?: (event: MemoryBlockingCompressionEvent) => void;
|
|
260
|
+
}
|
|
251
261
|
/** 固定预算的压缩记忆 + 近期未压缩原始消息。 */
|
|
252
262
|
interface MemoryContextWindow {
|
|
253
263
|
sessionId: string;
|
|
@@ -387,8 +397,12 @@ interface MemoryConfig {
|
|
|
387
397
|
embeddingBatchSize?: number;
|
|
388
398
|
/** embedding 批次并发数(默认 2)。*/
|
|
389
399
|
embeddingConcurrency?: number;
|
|
390
|
-
/** 压缩 Topic
|
|
400
|
+
/** 压缩 Topic 的全局存储上限,默认 16K。 */
|
|
391
401
|
compressedContextTokenLimit?: number;
|
|
402
|
+
/** Topic 最多占本次可用模型上下文的比例,默认 0.10。 */
|
|
403
|
+
compressedContextRatio?: number;
|
|
404
|
+
/** Topic 超过本次有效预算多少倍后必须同步归并,默认 1.25。 */
|
|
405
|
+
topicCompactionSyncRatio?: number;
|
|
392
406
|
/** 模型上下文中允许历史记忆使用的比例,默认 0.75。 */
|
|
393
407
|
contextUsageRatio?: number;
|
|
394
408
|
/** 原始消息达到其可用预算的此比例时后台预压缩,默认 0.75。 */
|
|
@@ -659,6 +673,7 @@ declare class MemoryManager {
|
|
|
659
673
|
private destroyTask?;
|
|
660
674
|
private readonly hydration;
|
|
661
675
|
private readonly pendingWrites;
|
|
676
|
+
private readonly topicCompactions;
|
|
662
677
|
private warnedDefaultModelContext;
|
|
663
678
|
constructor(config: MemoryConfig);
|
|
664
679
|
init(): Promise<void>;
|
|
@@ -695,7 +710,9 @@ declare class MemoryManager {
|
|
|
695
710
|
* 返回 user 级(userId) ∪ chat 级(chatId),按时间正序拼接。
|
|
696
711
|
*/
|
|
697
712
|
getFactsForContext(userId: string, chatId: string): Promise<string>;
|
|
698
|
-
getHistoryWindow(sessionId: string, modelContextTokens?: number): Promise<MemoryContextWindow>;
|
|
713
|
+
getHistoryWindow(sessionId: string, modelContextTokens?: number, options?: GetHistoryWindowOptions): Promise<MemoryContextWindow>;
|
|
714
|
+
private compactTopicsForBudget;
|
|
715
|
+
private notifyBlockingCompression;
|
|
699
716
|
private calculateWindowUsage;
|
|
700
717
|
private buildScopeFilter;
|
|
701
718
|
private deserializeSession;
|
|
@@ -836,6 +853,20 @@ declare class MemoryManager {
|
|
|
836
853
|
destroy(): Promise<void>;
|
|
837
854
|
}
|
|
838
855
|
|
|
856
|
+
interface MemoryContextBudget {
|
|
857
|
+
modelContextTokens: number;
|
|
858
|
+
usableContextTokens: number;
|
|
859
|
+
compressedTokenLimit: number;
|
|
860
|
+
rawTokenLimit: number;
|
|
861
|
+
}
|
|
862
|
+
/**
|
|
863
|
+
* 统一计算一次 history window 的 Topic / raw 预算。
|
|
864
|
+
*
|
|
865
|
+
* Topic 同时受全局质量上限和当前模型比例约束;最后保留至少 1 token raw 空间,
|
|
866
|
+
* 避免小窗口模型被固定 Topic 上限完全挤空。
|
|
867
|
+
*/
|
|
868
|
+
declare function calculateContextBudget(config: Pick<ResolvedConfig, "contextUsageRatio" | "compressedContextRatio" | "compressedContextTokenLimit">, modelContextTokens: number): MemoryContextBudget;
|
|
869
|
+
|
|
839
870
|
interface MemoryMigrationReport {
|
|
840
871
|
provider: string;
|
|
841
872
|
topicsScanned: number;
|
|
@@ -974,4 +1005,4 @@ type RelationType = (typeof DEFAULT_RELATION_TYPES)[number];
|
|
|
974
1005
|
declare const KIND_CONVERSATION = "conversation";
|
|
975
1006
|
declare const KIND_KNOWLEDGE = "knowledge";
|
|
976
1007
|
|
|
977
|
-
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, KIND_CONVERSATION, KIND_KNOWLEDGE, type KnowledgeSearchOptions, type KnowledgeSearchResult, type MemoryConfig, 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 };
|
|
1008
|
+
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 };
|
package/dist/index.js
CHANGED
|
@@ -38,6 +38,14 @@ function resolveConfig(config) {
|
|
|
38
38
|
summaryMaxTokens: config.summaryMaxTokens ?? 512,
|
|
39
39
|
conciseMaxTokens: config.conciseMaxTokens ?? 128,
|
|
40
40
|
compressedContextTokenLimit,
|
|
41
|
+
compressedContextRatio: ratio(
|
|
42
|
+
config.compressedContextRatio ?? 0.1,
|
|
43
|
+
"compressedContextRatio"
|
|
44
|
+
),
|
|
45
|
+
topicCompactionSyncRatio: atLeastOne(
|
|
46
|
+
config.topicCompactionSyncRatio ?? 1.25,
|
|
47
|
+
"topicCompactionSyncRatio"
|
|
48
|
+
),
|
|
41
49
|
contextUsageRatio: ratio(config.contextUsageRatio ?? 0.75, "contextUsageRatio"),
|
|
42
50
|
precompressionRatio: ratio(config.precompressionRatio ?? 0.75, "precompressionRatio"),
|
|
43
51
|
compressionBatchRatio: ratio(config.compressionBatchRatio ?? 0.5, "compressionBatchRatio"),
|
|
@@ -95,6 +103,41 @@ function positiveInt(value, name) {
|
|
|
95
103
|
}
|
|
96
104
|
return Math.floor(value);
|
|
97
105
|
}
|
|
106
|
+
function atLeastOne(value, name) {
|
|
107
|
+
if (!Number.isFinite(value) || value < 1) {
|
|
108
|
+
throw new Error(`[MemoryConfig] ${name} \u5FC5\u987B\u662F\u5927\u4E8E\u7B49\u4E8E 1 \u7684\u6709\u9650\u6570`);
|
|
109
|
+
}
|
|
110
|
+
return value;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
// src/context-budget.ts
|
|
114
|
+
function calculateContextBudget(config, modelContextTokens) {
|
|
115
|
+
if (!Number.isFinite(modelContextTokens) || modelContextTokens <= 0) {
|
|
116
|
+
throw new Error("modelContextTokens \u5FC5\u987B\u662F\u6B63\u6570");
|
|
117
|
+
}
|
|
118
|
+
const normalizedModelTokens = Math.floor(modelContextTokens);
|
|
119
|
+
const usableContextTokens = Math.floor(normalizedModelTokens * config.contextUsageRatio);
|
|
120
|
+
if (usableContextTokens < 2) {
|
|
121
|
+
throw new Error(
|
|
122
|
+
`modelContextTokens=${normalizedModelTokens} \u5728 contextUsageRatio=${config.contextUsageRatio} \u4E0B\u4E0D\u8DB3\u4EE5\u540C\u65F6\u4FDD\u7559 Topic \u4E0E\u539F\u59CB\u6D88\u606F\u9884\u7B97`
|
|
123
|
+
);
|
|
124
|
+
}
|
|
125
|
+
const ratioLimit = Math.max(
|
|
126
|
+
1,
|
|
127
|
+
Math.floor(usableContextTokens * config.compressedContextRatio)
|
|
128
|
+
);
|
|
129
|
+
const compressedTokenLimit = Math.min(
|
|
130
|
+
config.compressedContextTokenLimit,
|
|
131
|
+
ratioLimit,
|
|
132
|
+
usableContextTokens - 1
|
|
133
|
+
);
|
|
134
|
+
return {
|
|
135
|
+
modelContextTokens: normalizedModelTokens,
|
|
136
|
+
usableContextTokens,
|
|
137
|
+
compressedTokenLimit,
|
|
138
|
+
rawTokenLimit: usableContextTokens - compressedTokenLimit
|
|
139
|
+
};
|
|
140
|
+
}
|
|
98
141
|
|
|
99
142
|
// src/constants.ts
|
|
100
143
|
var DEFAULT_NODE_TYPES = [
|
|
@@ -1847,16 +1890,16 @@ ${formatted}`,
|
|
|
1847
1890
|
tokens: response.completionTokens
|
|
1848
1891
|
};
|
|
1849
1892
|
}
|
|
1850
|
-
async summarizeTopics(topics) {
|
|
1893
|
+
async summarizeTopics(topics, maxSummaryTokens = this.config.topicSummaryMaxTokens) {
|
|
1851
1894
|
const input = topics.map(
|
|
1852
1895
|
(topic) => `[${new Date(topic.startTime).toISOString()} - ${new Date(topic.endTime).toISOString()}] ${topic.title}
|
|
1853
1896
|
${topic.summary}`
|
|
1854
1897
|
).join("\n\n");
|
|
1855
1898
|
const response = await this.chatCompletionWithUsage(
|
|
1856
|
-
`\u4F60\u662F\u8BB0\u5FC6\u5F52\u5E76\u52A9\u624B\u3002\u628A\u4E00\u7EC4\u6309\u65F6\u95F4\u6392\u5217\u7684\u65E7\u4E3B\u9898\u5F52\u5E76\u6210\u4E00\u4E2A\u4E3B\u9898\u3002\u4FDD\u7559\u7A33\u5B9A\u504F\u597D\u3001\u4E8B\u5B9E\u3001\u51B3\u5B9A\u53CA\u7406\u7531\u3001\u957F\u671F\u7EA6\u675F\u3001\u963B\u585E\u4E0E\u53EF\u590D\u7528\u7ECF\u9A8C\uFF1B\u5220\u9664\u91CD\u590D\u5185\u5BB9\u3002summary \u6700\u591A ${
|
|
1899
|
+
`\u4F60\u662F\u8BB0\u5FC6\u5F52\u5E76\u52A9\u624B\u3002\u628A\u4E00\u7EC4\u6309\u65F6\u95F4\u6392\u5217\u7684\u65E7\u4E3B\u9898\u5F52\u5E76\u6210\u4E00\u4E2A\u4E3B\u9898\u3002\u4FDD\u7559\u7A33\u5B9A\u504F\u597D\u3001\u4E8B\u5B9E\u3001\u51B3\u5B9A\u53CA\u7406\u7531\u3001\u957F\u671F\u7EA6\u675F\u3001\u963B\u585E\u4E0E\u53EF\u590D\u7528\u7ECF\u9A8C\uFF1B\u5220\u9664\u91CD\u590D\u5185\u5BB9\u3002summary \u6700\u591A ${maxSummaryTokens} tokens\u3002\u53EA\u8F93\u51FA JSON\uFF1A{"title":"...","summary":"..."}`,
|
|
1857
1900
|
input,
|
|
1858
1901
|
true,
|
|
1859
|
-
|
|
1902
|
+
maxSummaryTokens + 128
|
|
1860
1903
|
);
|
|
1861
1904
|
const parsed = JSON.parse(response.content);
|
|
1862
1905
|
return {
|
|
@@ -2054,9 +2097,22 @@ var CompressManager = class {
|
|
|
2054
2097
|
sessionChain = /* @__PURE__ */ new Map();
|
|
2055
2098
|
backgroundGraphs = /* @__PURE__ */ new Set();
|
|
2056
2099
|
triggerCompress(sessionId, force = false, waitGraph = false, rawLimit) {
|
|
2100
|
+
return this.enqueueSessionTask(
|
|
2101
|
+
sessionId,
|
|
2102
|
+
() => this.doCompress(sessionId, force, waitGraph, rawLimit)
|
|
2103
|
+
);
|
|
2104
|
+
}
|
|
2105
|
+
/** 按给定预算收敛 Topic;persist=false 时只缓存当前模型使用的临时视图。 */
|
|
2106
|
+
triggerTopicCompaction(sessionId, tokenLimit, persist) {
|
|
2107
|
+
return this.enqueueSessionTask(
|
|
2108
|
+
sessionId,
|
|
2109
|
+
() => this.compactOldTopics(sessionId, Math.max(1, Math.floor(tokenLimit)), persist)
|
|
2110
|
+
);
|
|
2111
|
+
}
|
|
2112
|
+
enqueueSessionTask(sessionId, task) {
|
|
2057
2113
|
const previous = this.sessionChain.get(sessionId) ?? Promise.resolve();
|
|
2058
2114
|
const next = previous.then(
|
|
2059
|
-
() => this.semaphore.run(
|
|
2115
|
+
() => this.semaphore.run(task)
|
|
2060
2116
|
);
|
|
2061
2117
|
const settled = next.catch(() => {
|
|
2062
2118
|
});
|
|
@@ -2130,7 +2186,11 @@ var CompressManager = class {
|
|
|
2130
2186
|
await this.store.addTopic(topic);
|
|
2131
2187
|
this.sessionCache.appendTopic(sessionId, topic);
|
|
2132
2188
|
this.sessionCache.removeMessages(sessionId, messages.map((message) => message.messageId));
|
|
2133
|
-
await this.compactOldTopics(
|
|
2189
|
+
await this.compactOldTopics(
|
|
2190
|
+
sessionId,
|
|
2191
|
+
this.config.compressedContextTokenLimit,
|
|
2192
|
+
true
|
|
2193
|
+
);
|
|
2134
2194
|
const graphTask = this.extractAndPersistGraph(
|
|
2135
2195
|
messages,
|
|
2136
2196
|
sessionId,
|
|
@@ -2148,29 +2208,42 @@ var CompressManager = class {
|
|
|
2148
2208
|
tracked.finally(() => this.backgroundGraphs.delete(tracked));
|
|
2149
2209
|
}
|
|
2150
2210
|
}
|
|
2151
|
-
async compactOldTopics(sessionId) {
|
|
2211
|
+
async compactOldTopics(sessionId, tokenLimit, persist) {
|
|
2212
|
+
let transientTopics = persist ? void 0 : this.sessionCache.getTopicView(sessionId, tokenLimit) ?? [...this.sessionCache.getEntry(sessionId)?.topics ?? []];
|
|
2152
2213
|
while (true) {
|
|
2153
2214
|
const entry = this.sessionCache.getEntry(sessionId);
|
|
2154
|
-
if (!entry
|
|
2155
|
-
const
|
|
2215
|
+
if (!entry) return;
|
|
2216
|
+
const topics = persist ? entry.topics : transientTopics;
|
|
2217
|
+
const topicTokens = sumTopicTokens(topics);
|
|
2218
|
+
if (topicTokens <= tokenLimit) {
|
|
2219
|
+
if (!persist) this.sessionCache.setTopicView(sessionId, tokenLimit, topics);
|
|
2220
|
+
return;
|
|
2221
|
+
}
|
|
2222
|
+
const target = Math.max(1, Math.floor(topicTokens / 2));
|
|
2156
2223
|
const selected = [];
|
|
2157
|
-
let
|
|
2158
|
-
for (const topic of
|
|
2224
|
+
let selectedTokens = 0;
|
|
2225
|
+
for (const topic of topics) {
|
|
2159
2226
|
selected.push(topic);
|
|
2160
|
-
|
|
2161
|
-
if (
|
|
2162
|
-
}
|
|
2163
|
-
if (selected.length < 2) {
|
|
2164
|
-
this.sessionCache.setTopics(sessionId, entry.topics, true);
|
|
2165
|
-
return;
|
|
2227
|
+
selectedTokens += topic.tokens;
|
|
2228
|
+
if (selectedTokens >= target && (selected.length >= 2 || topics.length === 1)) break;
|
|
2166
2229
|
}
|
|
2230
|
+
if (selected.length === 0) return;
|
|
2167
2231
|
console.info(
|
|
2168
|
-
`[CompressManager] session ${sessionId} compressed Topic context exceeds ${
|
|
2232
|
+
`[CompressManager] session ${sessionId} compressed Topic context exceeds ${tokenLimit} tokens; compacting ${selected.length} old topics${persist ? " persistently" : " for a transient model view"}.`
|
|
2169
2233
|
);
|
|
2170
|
-
const
|
|
2234
|
+
const unselectedTokens = topicTokens - selectedTokens;
|
|
2235
|
+
const maxSummaryTokens = Math.max(
|
|
2236
|
+
1,
|
|
2237
|
+
Math.min(
|
|
2238
|
+
this.config.topicSummaryMaxTokens,
|
|
2239
|
+
selectedTokens - 1,
|
|
2240
|
+
Math.max(1, tokenLimit - unselectedTokens)
|
|
2241
|
+
)
|
|
2242
|
+
);
|
|
2243
|
+
const result = await this.llm.summarizeTopics(selected, maxSummaryTokens);
|
|
2171
2244
|
const summary = result.summary.trim();
|
|
2172
2245
|
if (!summary) throw new Error(`LLM returned an empty Topic rollup for session ${sessionId}`);
|
|
2173
|
-
const vector = await this.embed.embedOne(summary).catch(() => []);
|
|
2246
|
+
const vector = persist ? await this.embed.embedOne(summary).catch(() => []) : [];
|
|
2174
2247
|
const first = selected[0];
|
|
2175
2248
|
const last = selected[selected.length - 1];
|
|
2176
2249
|
const now = Date.now();
|
|
@@ -2181,7 +2254,7 @@ var CompressManager = class {
|
|
|
2181
2254
|
chatId: entry.ids.chatId,
|
|
2182
2255
|
title: result.title,
|
|
2183
2256
|
summary,
|
|
2184
|
-
tokens:
|
|
2257
|
+
tokens: Math.max(1, countTokens(summary)),
|
|
2185
2258
|
startMessageId: first.startMessageId,
|
|
2186
2259
|
endMessageId: last.endMessageId,
|
|
2187
2260
|
startTime: first.startTime,
|
|
@@ -2191,15 +2264,27 @@ var CompressManager = class {
|
|
|
2191
2264
|
recallCount: 0,
|
|
2192
2265
|
vector
|
|
2193
2266
|
};
|
|
2267
|
+
if (rollup.tokens >= selectedTokens) {
|
|
2268
|
+
throw new Error(
|
|
2269
|
+
`Topic compaction made no progress for session ${sessionId}: ${selectedTokens} -> ${rollup.tokens} tokens`
|
|
2270
|
+
);
|
|
2271
|
+
}
|
|
2194
2272
|
const removedIds = selected.map((topic) => topic.summaryId);
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2273
|
+
if (persist) {
|
|
2274
|
+
await this.store.addTopic(rollup);
|
|
2275
|
+
this.sessionCache.replaceTopics(sessionId, removedIds, rollup);
|
|
2276
|
+
await this.store.deleteTopicsByIds(removedIds);
|
|
2277
|
+
} else {
|
|
2278
|
+
const ids = new Set(removedIds);
|
|
2279
|
+
transientTopics = [
|
|
2280
|
+
...topics.filter((topic) => !ids.has(topic.summaryId)),
|
|
2281
|
+
rollup
|
|
2282
|
+
].sort(topicOrder);
|
|
2283
|
+
}
|
|
2198
2284
|
}
|
|
2199
2285
|
}
|
|
2200
2286
|
rawLimit(modelContextTokens = this.config.defaultModelContextTokens) {
|
|
2201
|
-
|
|
2202
|
-
return Math.max(1, usable - this.config.compressedContextTokenLimit);
|
|
2287
|
+
return calculateContextBudget(this.config, modelContextTokens).rawTokenLimit;
|
|
2203
2288
|
}
|
|
2204
2289
|
async extractAndPersistGraph(messages, sessionId, chatId, userId, endTime) {
|
|
2205
2290
|
const { entities, relations } = await this.llm.extractEntitiesFromMessages(messages);
|
|
@@ -2231,6 +2316,12 @@ var CompressManager = class {
|
|
|
2231
2316
|
await this.grafeo.upsertEntitiesAndRelations(entities, relations, embeddings);
|
|
2232
2317
|
}
|
|
2233
2318
|
};
|
|
2319
|
+
function sumTopicTokens(topics) {
|
|
2320
|
+
return topics.reduce((sum, topic) => sum + Math.max(1, topic.tokens), 0);
|
|
2321
|
+
}
|
|
2322
|
+
function topicOrder(a, b) {
|
|
2323
|
+
return a.endTime - b.endTime || a.summaryId.localeCompare(b.summaryId);
|
|
2324
|
+
}
|
|
2234
2325
|
function formatError(error) {
|
|
2235
2326
|
return error instanceof Error ? error.message : String(error);
|
|
2236
2327
|
}
|
|
@@ -2795,6 +2886,7 @@ function withTimeout2(promise, timeoutMs, label) {
|
|
|
2795
2886
|
var SessionCache = class {
|
|
2796
2887
|
config;
|
|
2797
2888
|
sessions = /* @__PURE__ */ new Map();
|
|
2889
|
+
topicViews = /* @__PURE__ */ new Map();
|
|
2798
2890
|
legacyHistoryWindows = /* @__PURE__ */ new Map();
|
|
2799
2891
|
constructor(config) {
|
|
2800
2892
|
this.config = config;
|
|
@@ -2829,7 +2921,7 @@ var SessionCache = class {
|
|
|
2829
2921
|
const entry = this.getOrCreateEntry(sessionId, chatId, userId);
|
|
2830
2922
|
entry.messages = sortMessages(dedupeMessages(messages));
|
|
2831
2923
|
entry.totalTokens = sumMessageTokens(entry.messages);
|
|
2832
|
-
this.setTopics(sessionId, topics);
|
|
2924
|
+
this.setTopics(sessionId, topics, false);
|
|
2833
2925
|
return entry;
|
|
2834
2926
|
}
|
|
2835
2927
|
upsertMessages(sessionId, messages, chatId, userId) {
|
|
@@ -2878,7 +2970,7 @@ var SessionCache = class {
|
|
|
2878
2970
|
setTopics(sessionId, topics, truncate = true) {
|
|
2879
2971
|
const entry = this.sessions.get(sessionId);
|
|
2880
2972
|
if (!entry) return;
|
|
2881
|
-
const ordered = [...topics].sort(
|
|
2973
|
+
const ordered = [...topics].sort(topicOrder2);
|
|
2882
2974
|
const kept = [];
|
|
2883
2975
|
let used = 0;
|
|
2884
2976
|
for (let i = ordered.length - 1; i >= 0; i--) {
|
|
@@ -2890,6 +2982,7 @@ var SessionCache = class {
|
|
|
2890
2982
|
entry.topics = kept.reverse();
|
|
2891
2983
|
entry.topicTokens = used;
|
|
2892
2984
|
entry.lastAccessAt = Date.now();
|
|
2985
|
+
this.topicViews.delete(sessionId);
|
|
2893
2986
|
}
|
|
2894
2987
|
appendTopic(sessionId, topic) {
|
|
2895
2988
|
const entry = this.sessions.get(sessionId);
|
|
@@ -2909,8 +3002,19 @@ var SessionCache = class {
|
|
|
2909
3002
|
getTopics(sessionId) {
|
|
2910
3003
|
return this.getEntry(sessionId)?.topics ?? [];
|
|
2911
3004
|
}
|
|
2912
|
-
|
|
2913
|
-
return this.
|
|
3005
|
+
getTopicView(sessionId, tokenLimit) {
|
|
3006
|
+
return this.topicViews.get(sessionId)?.get(tokenLimit);
|
|
3007
|
+
}
|
|
3008
|
+
setTopicView(sessionId, tokenLimit, topics) {
|
|
3009
|
+
let views = this.topicViews.get(sessionId);
|
|
3010
|
+
if (!views) {
|
|
3011
|
+
views = /* @__PURE__ */ new Map();
|
|
3012
|
+
this.topicViews.set(sessionId, views);
|
|
3013
|
+
}
|
|
3014
|
+
views.set(tokenLimit, [...topics].map(normalizeTopic).sort(topicOrder2));
|
|
3015
|
+
}
|
|
3016
|
+
buildCompressedContext(sessionId, topics) {
|
|
3017
|
+
return (topics ?? this.getTopics(sessionId)).map((topic) => topic.title ? `## ${topic.title}
|
|
2914
3018
|
${topic.summary}` : topic.summary).join("\n\n");
|
|
2915
3019
|
}
|
|
2916
3020
|
/** @deprecated 三级窗口兼容,仅供 0.3.x 调用方过渡。 */
|
|
@@ -2950,6 +3054,7 @@ ${topic.summary}` : topic.summary).join("\n\n");
|
|
|
2950
3054
|
}
|
|
2951
3055
|
delete(sessionId) {
|
|
2952
3056
|
this.sessions.delete(sessionId);
|
|
3057
|
+
this.topicViews.delete(sessionId);
|
|
2953
3058
|
this.legacyHistoryWindows.delete(sessionId);
|
|
2954
3059
|
}
|
|
2955
3060
|
evictIdle(now, ttlMs, isBusy) {
|
|
@@ -2958,6 +3063,7 @@ ${topic.summary}` : topic.summary).join("\n\n");
|
|
|
2958
3063
|
for (const [sessionId, entry] of this.sessions) {
|
|
2959
3064
|
if (now - entry.lastAccessAt < ttlMs || isBusy(sessionId)) continue;
|
|
2960
3065
|
this.sessions.delete(sessionId);
|
|
3066
|
+
this.topicViews.delete(sessionId);
|
|
2961
3067
|
evicted.push(sessionId);
|
|
2962
3068
|
}
|
|
2963
3069
|
return evicted;
|
|
@@ -2981,7 +3087,7 @@ function sortMessages(messages) {
|
|
|
2981
3087
|
(a, b) => a.createdAt - b.createdAt || a.messageId.localeCompare(b.messageId)
|
|
2982
3088
|
);
|
|
2983
3089
|
}
|
|
2984
|
-
function
|
|
3090
|
+
function topicOrder2(a, b) {
|
|
2985
3091
|
return a.endTime - b.endTime || a.summaryId.localeCompare(b.summaryId);
|
|
2986
3092
|
}
|
|
2987
3093
|
function normalizeTopic(topic) {
|
|
@@ -3012,6 +3118,7 @@ var MemoryManager = class {
|
|
|
3012
3118
|
destroyTask;
|
|
3013
3119
|
hydration = /* @__PURE__ */ new Map();
|
|
3014
3120
|
pendingWrites = /* @__PURE__ */ new Map();
|
|
3121
|
+
topicCompactions = /* @__PURE__ */ new Map();
|
|
3015
3122
|
warnedDefaultModelContext = false;
|
|
3016
3123
|
constructor(config) {
|
|
3017
3124
|
this.config = resolveConfig(config);
|
|
@@ -3126,7 +3233,7 @@ var MemoryManager = class {
|
|
|
3126
3233
|
await task;
|
|
3127
3234
|
}
|
|
3128
3235
|
isSessionBusy(sessionId) {
|
|
3129
|
-
return this.hydration.has(sessionId) || this.pendingWrites.has(sessionId) || this.compressManager.isBusy(sessionId);
|
|
3236
|
+
return this.hydration.has(sessionId) || this.pendingWrites.has(sessionId) || [...this.topicCompactions.keys()].some((key) => key.startsWith(`${sessionId}:`)) || this.compressManager.isBusy(sessionId);
|
|
3130
3237
|
}
|
|
3131
3238
|
waitForPendingWrites(sessionId) {
|
|
3132
3239
|
return this.pendingWrites.get(sessionId) ?? Promise.resolve();
|
|
@@ -3359,7 +3466,7 @@ var MemoryManager = class {
|
|
|
3359
3466
|
(f) => `${new Date(f.updatedAt ?? f.createdAt).toISOString()}\uFF1A${f.key ? `[${f.key}] ` : ""}${f.content}`
|
|
3360
3467
|
).join("\n");
|
|
3361
3468
|
}
|
|
3362
|
-
async getHistoryWindow(sessionId, modelContextTokens) {
|
|
3469
|
+
async getHistoryWindow(sessionId, modelContextTokens, options) {
|
|
3363
3470
|
let modelTokens = modelContextTokens;
|
|
3364
3471
|
if (modelTokens == null) {
|
|
3365
3472
|
modelTokens = this.config.defaultModelContextTokens;
|
|
@@ -3378,56 +3485,136 @@ var MemoryManager = class {
|
|
|
3378
3485
|
await this.ensureSessionHydrated(sessionId);
|
|
3379
3486
|
this.sessionCache.setModelContextTokens(sessionId, modelTokens);
|
|
3380
3487
|
const usageLimits = this.calculateWindowUsage(modelTokens);
|
|
3381
|
-
|
|
3382
|
-
|
|
3383
|
-
if (
|
|
3384
|
-
|
|
3385
|
-
|
|
3386
|
-
|
|
3387
|
-
|
|
3388
|
-
|
|
3389
|
-
|
|
3390
|
-
|
|
3391
|
-
|
|
3488
|
+
let blockingReason;
|
|
3489
|
+
const beginBlocking = (reason) => {
|
|
3490
|
+
if (blockingReason) return;
|
|
3491
|
+
blockingReason = reason;
|
|
3492
|
+
this.notifyBlockingCompression(options, { sessionId, phase: "start", reason });
|
|
3493
|
+
};
|
|
3494
|
+
try {
|
|
3495
|
+
for (let attempt = 0; attempt < 100; attempt++) {
|
|
3496
|
+
let entry2 = this.sessionCache.getEntry(sessionId);
|
|
3497
|
+
if (entry2.totalTokens <= usageLimits.rawTokenLimit || entry2.messages.length === 0) break;
|
|
3498
|
+
if (this.compressManager.isBusy(sessionId)) {
|
|
3499
|
+
beginBlocking("pending");
|
|
3500
|
+
await this.compressManager.waitForIdle(sessionId);
|
|
3501
|
+
entry2 = this.sessionCache.getEntry(sessionId);
|
|
3502
|
+
if (entry2.totalTokens <= usageLimits.rawTokenLimit || entry2.messages.length === 0) break;
|
|
3503
|
+
}
|
|
3504
|
+
beginBlocking("raw");
|
|
3505
|
+
console.warn(
|
|
3506
|
+
`[MemoryManager] session ${sessionId} raw history (${entry2.totalTokens}) exceeds current model budget (${usageLimits.rawTokenLimit}); waiting for immediate compression.`
|
|
3507
|
+
);
|
|
3508
|
+
const beforeTokens = entry2.totalTokens;
|
|
3509
|
+
await this.compressManager.triggerCompress(
|
|
3510
|
+
sessionId,
|
|
3511
|
+
true,
|
|
3512
|
+
false,
|
|
3513
|
+
usageLimits.rawTokenLimit
|
|
3514
|
+
);
|
|
3515
|
+
const after = this.sessionCache.getEntry(sessionId);
|
|
3516
|
+
if (after.messages.length > 0 && after.totalTokens >= beforeTokens) {
|
|
3517
|
+
throw new Error(`Compression made no progress for session ${sessionId}`);
|
|
3518
|
+
}
|
|
3519
|
+
}
|
|
3520
|
+
let entry = this.sessionCache.getEntry(sessionId);
|
|
3521
|
+
if (entry.totalTokens > usageLimits.rawTokenLimit) {
|
|
3522
|
+
throw new Error(`Unable to fit session ${sessionId} into the requested model context`);
|
|
3523
|
+
}
|
|
3524
|
+
let topicView = this.sessionCache.getTopicView(
|
|
3392
3525
|
sessionId,
|
|
3393
|
-
|
|
3394
|
-
false,
|
|
3395
|
-
usageLimits.rawTokenLimit
|
|
3526
|
+
usageLimits.compressedTokenLimit
|
|
3396
3527
|
);
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
|
|
3528
|
+
if (!topicView && entry.topicTokens > usageLimits.compressedTokenLimit) {
|
|
3529
|
+
const affectsCurrentWindow = entry.topicTokens + entry.totalTokens > usageLimits.usableContextTokens;
|
|
3530
|
+
const materiallyOverLimit = entry.topicTokens > Math.floor(
|
|
3531
|
+
usageLimits.compressedTokenLimit * this.config.topicCompactionSyncRatio
|
|
3532
|
+
);
|
|
3533
|
+
if (affectsCurrentWindow || materiallyOverLimit) {
|
|
3534
|
+
if (this.compressManager.isBusy(sessionId)) beginBlocking("pending");
|
|
3535
|
+
else beginBlocking("topics");
|
|
3536
|
+
await this.compactTopicsForBudget(sessionId, usageLimits.compressedTokenLimit);
|
|
3537
|
+
entry = this.sessionCache.getEntry(sessionId);
|
|
3538
|
+
topicView = this.sessionCache.getTopicView(
|
|
3539
|
+
sessionId,
|
|
3540
|
+
usageLimits.compressedTokenLimit
|
|
3541
|
+
);
|
|
3542
|
+
} else {
|
|
3543
|
+
void this.compactTopicsForBudget(
|
|
3544
|
+
sessionId,
|
|
3545
|
+
usageLimits.compressedTokenLimit
|
|
3546
|
+
).catch((error) => {
|
|
3547
|
+
console.error(`[MemoryManager] Background Topic compaction failed for ${sessionId}:`, error);
|
|
3548
|
+
});
|
|
3549
|
+
}
|
|
3550
|
+
}
|
|
3551
|
+
const topics = topicView ?? entry.topics;
|
|
3552
|
+
const compressedTokens = sumTopicTokens2(topics);
|
|
3553
|
+
if (compressedTokens + entry.totalTokens > usageLimits.usableContextTokens) {
|
|
3554
|
+
throw new Error(`Unable to fit session ${sessionId} into the requested model context`);
|
|
3555
|
+
}
|
|
3556
|
+
return {
|
|
3557
|
+
sessionId,
|
|
3558
|
+
compressedContext: this.sessionCache.buildCompressedContext(sessionId, topics),
|
|
3559
|
+
recentMessages: entry.messages.map(toMemoryRawMessage),
|
|
3560
|
+
usage: {
|
|
3561
|
+
...usageLimits,
|
|
3562
|
+
compressedTokens,
|
|
3563
|
+
rawTokens: entry.totalTokens
|
|
3564
|
+
}
|
|
3565
|
+
};
|
|
3566
|
+
} finally {
|
|
3567
|
+
if (blockingReason) {
|
|
3568
|
+
this.notifyBlockingCompression(options, {
|
|
3569
|
+
sessionId,
|
|
3570
|
+
phase: "end",
|
|
3571
|
+
reason: blockingReason
|
|
3572
|
+
});
|
|
3400
3573
|
}
|
|
3401
3574
|
}
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
|
|
3405
|
-
|
|
3406
|
-
return
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3411
|
-
|
|
3412
|
-
|
|
3413
|
-
|
|
3575
|
+
}
|
|
3576
|
+
compactTopicsForBudget(sessionId, effectiveLimit) {
|
|
3577
|
+
const key = `${sessionId}:${effectiveLimit}`;
|
|
3578
|
+
const existing = this.topicCompactions.get(key);
|
|
3579
|
+
if (existing) return existing;
|
|
3580
|
+
const task = (async () => {
|
|
3581
|
+
let entry = this.sessionCache.getEntry(sessionId);
|
|
3582
|
+
if (!entry) return;
|
|
3583
|
+
if (entry.topicTokens > this.config.compressedContextTokenLimit) {
|
|
3584
|
+
await this.compressManager.triggerTopicCompaction(
|
|
3585
|
+
sessionId,
|
|
3586
|
+
this.config.compressedContextTokenLimit,
|
|
3587
|
+
true
|
|
3588
|
+
);
|
|
3589
|
+
entry = this.sessionCache.getEntry(sessionId);
|
|
3590
|
+
}
|
|
3591
|
+
if (entry && entry.topicTokens > effectiveLimit && !this.sessionCache.getTopicView(sessionId, effectiveLimit)) {
|
|
3592
|
+
await this.compressManager.triggerTopicCompaction(sessionId, effectiveLimit, false);
|
|
3414
3593
|
}
|
|
3594
|
+
})();
|
|
3595
|
+
this.topicCompactions.set(key, task);
|
|
3596
|
+
const clear = () => {
|
|
3597
|
+
if (this.topicCompactions.get(key) === task) this.topicCompactions.delete(key);
|
|
3415
3598
|
};
|
|
3599
|
+
void task.then(clear, clear);
|
|
3600
|
+
return task;
|
|
3416
3601
|
}
|
|
3417
|
-
|
|
3418
|
-
const
|
|
3419
|
-
|
|
3420
|
-
|
|
3421
|
-
|
|
3422
|
-
|
|
3423
|
-
|
|
3602
|
+
notifyBlockingCompression(options, event) {
|
|
3603
|
+
const callback = options?.onBlockingCompression;
|
|
3604
|
+
if (!callback) return;
|
|
3605
|
+
try {
|
|
3606
|
+
const returned = callback(event);
|
|
3607
|
+
if (returned && typeof returned.then === "function") {
|
|
3608
|
+
void Promise.resolve(returned).catch((error) => {
|
|
3609
|
+
console.warn("[MemoryManager] onBlockingCompression callback rejected:", error);
|
|
3610
|
+
});
|
|
3611
|
+
}
|
|
3612
|
+
} catch (error) {
|
|
3613
|
+
console.warn("[MemoryManager] onBlockingCompression callback failed:", error);
|
|
3424
3614
|
}
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
compressedTokenLimit: this.config.compressedContextTokenLimit,
|
|
3429
|
-
rawTokenLimit
|
|
3430
|
-
};
|
|
3615
|
+
}
|
|
3616
|
+
calculateWindowUsage(modelContextTokens) {
|
|
3617
|
+
return calculateContextBudget(this.config, modelContextTokens);
|
|
3431
3618
|
}
|
|
3432
3619
|
buildScopeFilter(scope, scopeId) {
|
|
3433
3620
|
if (scope === "session" && scopeId) return [eq("session_id", scopeId)];
|
|
@@ -3609,7 +3796,11 @@ var MemoryManager = class {
|
|
|
3609
3796
|
clearInterval(this.sessionSweepTimer);
|
|
3610
3797
|
this.sessionSweepTimer = void 0;
|
|
3611
3798
|
}
|
|
3612
|
-
const writes = [
|
|
3799
|
+
const writes = [
|
|
3800
|
+
...this.pendingWrites.values(),
|
|
3801
|
+
...this.hydration.values(),
|
|
3802
|
+
...this.topicCompactions.values()
|
|
3803
|
+
];
|
|
3613
3804
|
this.destroyTask = Promise.all(writes).catch(() => {
|
|
3614
3805
|
}).then(() => this.compressManager.waitForAllIdle()).catch(() => {
|
|
3615
3806
|
}).then(() => this.optimizeTask).catch(() => {
|
|
@@ -3639,6 +3830,9 @@ function toMemoryRawMessage(message) {
|
|
|
3639
3830
|
createdAt: message.createdAt
|
|
3640
3831
|
};
|
|
3641
3832
|
}
|
|
3833
|
+
function sumTopicTokens2(topics) {
|
|
3834
|
+
return topics.reduce((sum, topic) => sum + Math.max(1, topic.tokens), 0);
|
|
3835
|
+
}
|
|
3642
3836
|
function safeParseArray(value) {
|
|
3643
3837
|
try {
|
|
3644
3838
|
const parsed = JSON.parse(value);
|
|
@@ -3668,5 +3862,6 @@ export {
|
|
|
3668
3862
|
KIND_CONVERSATION,
|
|
3669
3863
|
KIND_KNOWLEDGE,
|
|
3670
3864
|
MemoryManager,
|
|
3671
|
-
MemoryStore
|
|
3865
|
+
MemoryStore,
|
|
3866
|
+
calculateContextBudget
|
|
3672
3867
|
};
|
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 complete raw conversation messages in a pluggable vector store (LanceDB or SQLite/sqlite-vec, auto-detected per platform), builds importance-aware Topic summaries, returns
|
|
3
|
+
> `@ppagent/memory` is an independent TypeScript/Node.js long-term memory package for AI agents. It stores complete raw conversation messages in a pluggable vector store (LanceDB or SQLite/sqlite-vec, auto-detected per platform), 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
|
|
|
@@ -150,6 +150,8 @@ const memory = new MemoryManager({
|
|
|
150
150
|
embeddingConcurrency: 2,
|
|
151
151
|
|
|
152
152
|
compressedContextTokenLimit: 16 * 1024,
|
|
153
|
+
compressedContextRatio: 0.10,
|
|
154
|
+
topicCompactionSyncRatio: 1.25,
|
|
153
155
|
contextUsageRatio: 0.75,
|
|
154
156
|
precompressionRatio: 0.75,
|
|
155
157
|
compressionBatchRatio: 0.5,
|
|
@@ -331,7 +333,9 @@ HTTP and retry fields:
|
|
|
331
333
|
|
|
332
334
|
Conversation memory fields:
|
|
333
335
|
|
|
334
|
-
- `compressedContextTokenLimit?: number` -
|
|
336
|
+
- `compressedContextTokenLimit?: number` - global storage cap for compressed Topics. Default: `16384`; startup logs a warning above `32768`.
|
|
337
|
+
- `compressedContextRatio?: number` - maximum fraction of the usable model-history window assigned to Topics. Default: `0.10`. The effective Topic budget is the smaller of this ratio and `compressedContextTokenLimit`.
|
|
338
|
+
- `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`.
|
|
335
339
|
- `contextUsageRatio?: number` - fraction of the current model context available to conversation history. Default: `0.75`.
|
|
336
340
|
- `precompressionRatio?: number` - start background raw-message compression when raw usage reaches this fraction of its dynamic budget. Default: `0.75`.
|
|
337
341
|
- `compressionBatchRatio?: number` - target fraction of the raw-message budget compressed in one batch. Default: `0.5`.
|
|
@@ -384,13 +388,15 @@ The package intentionally separates durable base writes from expensive graph con
|
|
|
384
388
|
- `waitGraph: true` waits for graph extraction/persistence too.
|
|
385
389
|
- When `waitGraph: true`, graph waiting is bounded by `graphBuildTimeoutMs` and may throw a timeout error.
|
|
386
390
|
|
|
387
|
-
`getHistoryWindow(sessionId, modelContextTokens?)`:
|
|
391
|
+
`getHistoryWindow(sessionId, modelContextTokens?, options?)`:
|
|
388
392
|
|
|
389
393
|
- Lazily hydrates the session from persistent Topics plus raw messages after the newest Topic boundary.
|
|
390
394
|
- Waits for registered message writes. If the hard raw budget is exceeded, it also waits for or starts compression until the returned context fits.
|
|
391
395
|
- Returns `{ compressedContext, recentMessages, usage }`. `recentMessages` preserves the `RawMessage` input shape, including `messageId`, `parts`, `payload`, `metadata`, and `createdAt`.
|
|
392
|
-
- Topic budget is
|
|
393
|
-
-
|
|
396
|
+
- The effective Topic budget is `min(compressedContextTokenLimit, usableContextTokens * compressedContextRatio)`; the remaining usable history budget is reserved for raw messages.
|
|
397
|
+
- 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.
|
|
398
|
+
- 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.
|
|
399
|
+
- `options.onBlockingCompression` receives `{ phase: "start" | "end", reason }` only when this read actually waits for compression. Silent precompression and non-blocking Topic rollups do not invoke it. Callback failures are contained.
|
|
394
400
|
|
|
395
401
|
`addDocument(opts)`:
|
|
396
402
|
|
|
@@ -607,15 +613,17 @@ Context:
|
|
|
607
613
|
- `chatId?: string`
|
|
608
614
|
- `userId?: string`
|
|
609
615
|
|
|
610
|
-
### `getHistoryWindow(sessionId, modelContextTokens?): Promise<MemoryContextWindow>`
|
|
616
|
+
### `getHistoryWindow(sessionId, modelContextTokens?, options?): Promise<MemoryContextWindow>`
|
|
611
617
|
|
|
612
618
|
Returns the complete model-history window:
|
|
613
619
|
|
|
614
|
-
- `compressedContext: string` - chronological Topic summaries
|
|
620
|
+
- `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.
|
|
615
621
|
- `recentMessages: MemoryRawMessage[]` - all currently uncompressed raw messages in chronological order, preserving the host payload and metadata.
|
|
616
622
|
- `usage` - model size, usable history size, compressed usage, and dynamic raw-message budget/usage.
|
|
617
623
|
|
|
618
624
|
Pass the active model's context size on every read. Omitting it uses `defaultModelContextTokens` (256K by default) and logs a warning.
|
|
625
|
+
|
|
626
|
+
Pass `options.onBlockingCompression` when the host needs a foreground-wait UI signal. It is deliberately not a general compression-progress callback.
|
|
619
627
|
|
|
620
628
|
### `getRecentMessages(sessionId, limit): Promise<StoredMessage[]>`
|
|
621
629
|
|