From 931820db6903f40d73a90de0e128e64292f98eb2 Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 30 Aug 2026 15:43:11 -0400 Subject: [PATCH 1/2] perf: evolve compaction guidance on warm turns --- api/server/controllers/agents/client.js | 82 +++++----- api/server/controllers/agents/client.test.js | 145 ++++++++++++++---- packages/api/src/agents/compaction.spec.ts | 40 ++++- packages/api/src/agents/compaction.ts | 42 ++++- .../src/methods/conversation.spec.ts | 17 ++ packages/data-schemas/src/schema/convo.ts | 1 + packages/data-schemas/src/types/compaction.ts | 7 +- 7 files changed, 255 insertions(+), 79 deletions(-) diff --git a/api/server/controllers/agents/client.js b/api/server/controllers/agents/client.js index 042fbcd3b9b..3b129419479 100644 --- a/api/server/controllers/agents/client.js +++ b/api/server/controllers/agents/client.js @@ -132,7 +132,7 @@ const { createSkillContentDigest, normalizeAgentEventActorDiscoveredTools, createCompactionSemanticIndexProjection, - restoreCompactionSemanticIndex, + restoreCompactionSemanticIndexSnapshot, MAX_AGENT_CONTEXT_SKILLS, } = require('@librechat/api'); const { @@ -291,9 +291,9 @@ class AgentClient extends BaseClient { this.eventActorSkillPrimeResult = undefined; this.eventActorDiscoveredToolNames = undefined; this.eventActorSummary = undefined; - /** Advisory compaction guidance retained across graph reconstruction. - * @type {import('@librechat/agents').CompactionSemanticIndex | undefined} */ - this.compactionSemanticIndex = undefined; + /** Advisory compaction guidance retained and evolved across graph reconstruction. + * @type {import('@librechat/agents').CompactionSemanticIndexSnapshot | undefined} */ + this.compactionSemanticIndexSnapshot = undefined; /** @type {AgentRun} */ this.run; @@ -1711,7 +1711,9 @@ class AgentClient extends BaseClient { discoveredToolNames = normalizeAgentEventActorDiscoveredTools(state.discoveredToolNames); summary = normalizeEventActorSummary(state.summary); contextMeta = normalizeEventActorContextMeta(state.contextMeta); - compactionSemanticIndex = restoreCompactionSemanticIndex(state.compactionSemanticIndex); + compactionSemanticIndex = restoreCompactionSemanticIndexSnapshot( + state.compactionSemanticIndex, + ); } catch { return undefined; } @@ -1739,7 +1741,7 @@ class AgentClient extends BaseClient { this.eventActorDiscoveredToolNames = discoveredToolNames; this.eventActorSummary = summary; this.contextMeta = contextMeta; - this.compactionSemanticIndex = compactionSemanticIndex; + this.compactionSemanticIndexSnapshot = compactionSemanticIndex; const context = await this.getEventActorContext(storedManifest, discoveredToolNames); const skillBodies = new Map(skillPrimeResult?.skills ?? []); const rootAgentContext = this.eventActorAgentContextSources?.[0]; @@ -1799,7 +1801,7 @@ class AgentClient extends BaseClient { const summary = getLatestEventActorSummary(this.contentParts) ?? this.eventActorSummary; this.eventActorSummary = summary; const compactionSemanticIndex = createCompactionSemanticIndexProjection( - this.compactionSemanticIndex, + this.compactionSemanticIndexSnapshot, ); return { fingerprint: createInitializedAgentContextFingerprint({ @@ -1834,7 +1836,7 @@ class AgentClient extends BaseClient { * non-checkpointed state from durable history, never from that stale head. */ this.eventActorSummary = undefined; this.contextMeta = undefined; - this.compactionSemanticIndex = undefined; + this.compactionSemanticIndexSnapshot = undefined; } /** Always pass mapMethod; getMessagesForConversation applies it only to messages with addedConvo flag */ const orderedMessages = this.constructor.getMessagesForConversation({ @@ -3709,7 +3711,7 @@ class AgentClient extends BaseClient { discoveredTools, activityPhaseSnapshot: this.activityPhaseWiring?.snapshot?.(), compactionSemanticIndex: createCompactionSemanticIndexProjection( - this.compactionSemanticIndex, + this.compactionSemanticIndexSnapshot, ), }; if (this.eventActorInvocationId != null) { @@ -3886,47 +3888,43 @@ class AgentClient extends BaseClient { this.options.agent, ...(this.agentConfigs?.values() ?? []), ]); - const deriveCompactionSemanticIndex = this.eventActorContinuation !== 'warm'; const messageFormatOptions = { ...(needsReasoningContentFormat ? { preserveReasoningContent: true } : {}), ...(freshSkillPrimeNames.size > 0 ? { skipSkillBodyNames: freshSkillPrimeNames } : {}), ...(useLegacyContent ? { legacyContent: true } : {}), }; - let semanticIntentToolNames; - if (deriveCompactionSemanticIndex) { - semanticIntentToolNames = new Set(); - const semanticIntentBlockedToolNames = new Set(); - for (const agent of reachableAgents) { - for (const toolName of agent.semanticIntentToolNames ?? []) { - semanticIntentToolNames.add(toolName); - } - for (const toolName of agent.semanticIntentBlockedToolNames ?? []) { - semanticIntentBlockedToolNames.add(toolName); - } + const semanticIntentToolNames = new Set(); + const semanticIntentBlockedToolNames = new Set(); + for (const agent of reachableAgents) { + for (const toolName of agent.semanticIntentToolNames ?? []) { + semanticIntentToolNames.add(toolName); } - for (const toolName of semanticIntentBlockedToolNames) { - semanticIntentToolNames.delete(toolName); + for (const toolName of agent.semanticIntentBlockedToolNames ?? []) { + semanticIntentBlockedToolNames.add(toolName); } } + for (const toolName of semanticIntentBlockedToolNames) { + semanticIntentToolNames.delete(toolName); + } const hasMessageFormatOptions = needsReasoningContentFormat || freshSkillPrimeNames.size > 0 || useLegacyContent; - const formatOptions = - hasMessageFormatOptions || deriveCompactionSemanticIndex - ? { - ...messageFormatOptions, - ...(deriveCompactionSemanticIndex - ? { compactionSemanticIndex: { intentToolNames: semanticIntentToolNames } } - : {}), - } - : undefined; + const formatOptions = { + ...messageFormatOptions, + compactionSemanticIndex: { + ...(this.eventActorContinuation === 'warm' && this.compactionSemanticIndexSnapshot != null + ? { baseSnapshot: this.compactionSemanticIndexSnapshot } + : {}), + intentToolNames: semanticIntentToolNames, + }, + }; let { messages: initialMessages, indexTokenCountMap, summary: initialSummary, boundaryTokenAdjustment, - compactionSemanticIndex, + compactionSemanticIndexSnapshot, } = formatAgentMessages( - deriveCompactionSemanticIndex ? payload : stripActivityLabelParts(payload), + payload, this.indexTokenCountMap, toolSet, skillPrimeResult?.skills, @@ -3934,14 +3932,13 @@ class AgentClient extends BaseClient { ); if (this.eventActorContinuation !== 'warm') { this.eventActorSummary = initialSummary; - this.compactionSemanticIndex = compactionSemanticIndex; } + this.compactionSemanticIndexSnapshot = + compactionSemanticIndexSnapshot ?? + (this.eventActorContinuation === 'warm' ? this.compactionSemanticIndexSnapshot : undefined); const continuationSummary = this.eventActorContinuation === 'warm' ? this.eventActorSummary : initialSummary; - const continuationCompactionSemanticIndex = - this.eventActorContinuation === 'warm' - ? this.compactionSemanticIndex - : compactionSemanticIndex; + const continuationCompactionSemanticIndex = this.compactionSemanticIndexSnapshot?.entries; if (boundaryTokenAdjustment) { logger.debug( `[AgentClient] Boundary token adjustment: ${boundaryTokenAdjustment.original} → ${boundaryTokenAdjustment.adjusted} (${boundaryTokenAdjustment.remainingChars}/${boundaryTokenAdjustment.totalChars} chars)`, @@ -4566,7 +4563,8 @@ class AgentClient extends BaseClient { } const tokenCounter = await createCachedTokenCounter(this.getEncoding()); - this.compactionSemanticIndex = restoreCompactionSemanticIndex(compactionSemanticIndex); + this.compactionSemanticIndexSnapshot = + restoreCompactionSemanticIndexSnapshot(compactionSemanticIndex); const agents = collectReachableAgents([ this.options.agent, ...(this.agentConfigs?.size > 0 ? this.agentConfigs.values() : []), @@ -4698,9 +4696,9 @@ class AgentClient extends BaseClient { // batches keep claiming slots and generating group headers. activityLabel, activityPhase, - ...(this.compactionSemanticIndex == null + ...(this.compactionSemanticIndexSnapshot == null ? {} - : { compactionSemanticIndex: this.compactionSemanticIndex }), + : { compactionSemanticIndex: this.compactionSemanticIndexSnapshot.entries }), // Replay deferred tools discovered before the pause. With `messages: []` the // discovery scan finds nothing, so these names restore the schemas to the // rebuilt model binding. Undefined/empty for non-deferred turns is a no-op. diff --git a/api/server/controllers/agents/client.test.js b/api/server/controllers/agents/client.test.js index 6949449fa40..94773ed7456 100644 --- a/api/server/controllers/agents/client.test.js +++ b/api/server/controllers/agents/client.test.js @@ -92,6 +92,7 @@ describe('AgentClient - event actor history adapter', () => { const fingerprint = { algorithm: 'sha256', version: 1, digest: 'context' }; const compactionSemanticIndex = { version: 1, + providedEntryCount: 1, entries: [ { type: 'activity_phase', @@ -187,7 +188,10 @@ describe('AgentClient - event actor history adapter', () => { tokenCount: 12, }); expect(client.contextMeta).toEqual({ calibrationRatio: 1.25, encoding: 'o200k_base' }); - expect(client.compactionSemanticIndex).toEqual(compactionSemanticIndex.entries); + expect(client.compactionSemanticIndexSnapshot).toEqual({ + entries: compactionSemanticIndex.entries, + providedEntryCount: 1, + }); }); it('falls back when a durable Skill resolves to a different revision', async () => { @@ -301,7 +305,10 @@ describe('AgentClient - event actor history adapter', () => { }, ]; client.contextMeta = { calibrationRatio: 1.3, encoding: 'o200k_base' }; - client.compactionSemanticIndex = compactionSemanticIndex.entries; + client.compactionSemanticIndexSnapshot = { + entries: compactionSemanticIndex.entries, + providedEntryCount: 1, + }; client.getEventActorAgents = jest.fn(() => []); client.getEventActorMemorySnapshots = jest.fn().mockResolvedValue([]); @@ -874,16 +881,19 @@ describe('AgentClient - interrupt discovery persistence', () => { client.conversationId = streamId; client.responseMessageId = 'response-discovered-pause'; client.jobCreatedAt = job.createdAt; - client.compactionSemanticIndex = [ - { - type: 'activity_phase', - sourceMessageId: 'assistant-before-pause', - sourceContentIndex: 1, - revision: 1, - status: 'committed', - text: 'Prepared the change', - }, - ]; + client.compactionSemanticIndexSnapshot = { + entries: [ + { + type: 'activity_phase', + sourceMessageId: 'assistant-before-pause', + sourceContentIndex: 1, + revision: 1, + status: 'committed', + text: 'Prepared the change', + }, + ], + providedEntryCount: 1, + }; await client.handleRunInterrupt( { @@ -906,7 +916,8 @@ describe('AgentClient - interrupt discovery persistence', () => { expect(paused?.metadata.discoveredTools).toEqual(['save_issue_mcp_linear']); expect(paused?.metadata.compactionSemanticIndex).toEqual({ version: 1, - entries: client.compactionSemanticIndex, + entries: client.compactionSemanticIndexSnapshot.entries, + providedEntryCount: 1, }); }); @@ -1781,7 +1792,10 @@ describe('AgentClient - startup telemetry', () => { indexTokenCountMap: {}, summary: undefined, boundaryTokenAdjustment: undefined, - compactionSemanticIndex, + compactionSemanticIndexSnapshot: { + entries: compactionSemanticIndex, + providedEntryCount: compactionSemanticIndex.length, + }, }); const processStream = jest.fn().mockResolvedValue(); mockCreateRun.mockResolvedValueOnce({ @@ -1930,6 +1944,55 @@ describe('AgentClient - startup telemetry', () => { ); }); + it('evolves a persisted semantic snapshot from only the warm payload', () => { + const { formatAgentMessages } = jest.requireActual('@librechat/agents'); + const baseEntry = { + type: 'activity_phase', + sourceMessageId: 'assistant-history', + sourceContentIndex: 1, + revision: 1, + status: 'committed', + text: 'Verified the release state', + }; + const baseSnapshot = { entries: [baseEntry], providedEntryCount: 7 }; + const payload = [ + { + role: 'assistant', + messageId: 'assistant-event', + content: [ + { + type: ContentTypes.ACTIVITY_LABEL, + activity_label: 'Applied the warm event', + activity_label_type: 'phase', + activity_start_index: 0, + pending: false, + }, + { type: ContentTypes.TEXT, text: 'The warm event completed.' }, + ], + }, + ]; + + const baseline = formatAgentMessages(payload); + const evolved = formatAgentMessages(payload, undefined, undefined, undefined, { + compactionSemanticIndex: { baseSnapshot }, + }); + + expect(evolved.messages.map((message) => message.toDict())).toEqual( + baseline.messages.map((message) => message.toDict()), + ); + expect(evolved.compactionSemanticIndexSnapshot).toEqual({ + entries: expect.arrayContaining([ + baseEntry, + expect.objectContaining({ + type: 'activity_phase', + sourceMessageId: 'assistant-event', + text: 'Applied the warm event', + }), + ]), + providedEntryCount: 8, + }); + }); + it('propagates final model callback policy errors instead of persisting a generic error part', async () => { jest.clearAllMocks(); let policyError; @@ -1990,15 +2053,43 @@ describe('AgentClient - startup telemetry', () => { ); }); - it('rehydrates a warm event actor from its fork and injects only the new event message', async () => { + it('evolves warm compaction guidance while injecting only the new event message', async () => { jest.clearAllMocks(); const history = { _getType: () => 'human', content: 'old turn' }; const currentEvent = { _getType: () => 'human', content: 'new event' }; + const baseCompactionSemanticIndexSnapshot = { + entries: [ + { + type: 'activity_phase', + sourceMessageId: 'assistant-history', + sourceContentIndex: 1, + revision: 1, + status: 'committed', + text: 'Verified the release state', + }, + ], + providedEntryCount: 1, + }; + const evolvedCompactionSemanticIndexSnapshot = { + entries: [ + ...baseCompactionSemanticIndexSnapshot.entries, + { + type: 'activity_phase', + sourceMessageId: 'assistant-event', + sourceContentIndex: 0, + revision: 1, + status: 'committed', + text: 'Applied the warm event', + }, + ], + providedEntryCount: 2, + }; mockFormatAgentMessages.mockReturnValueOnce({ messages: [history, currentEvent], indexTokenCountMap: { 0: 11, 1: 22 }, summary: undefined, boundaryTokenAdjustment: undefined, + compactionSemanticIndexSnapshot: evolvedCompactionSemanticIndexSnapshot, }); let client; const processStream = jest.fn(async () => { @@ -2047,16 +2138,7 @@ describe('AgentClient - startup telemetry', () => { client.eventActorDiscoveredToolNames = ['deferred_tool']; client.eventActorSummary = { text: 'summary of earlier turns', tokenCount: 40 }; client.contextMeta = { calibrationRatio: 1.25, encoding: client.getEncoding() }; - client.compactionSemanticIndex = [ - { - type: 'activity_phase', - sourceMessageId: 'assistant-history', - sourceContentIndex: 1, - revision: 1, - status: 'committed', - text: 'Verified the release state', - }, - ]; + client.compactionSemanticIndexSnapshot = baseCompactionSemanticIndexSnapshot; client.recordCollectedUsage = jest.fn().mockResolvedValue(); await client.chatCompletion({ payload: [] }); @@ -2074,12 +2156,19 @@ describe('AgentClient - startup telemetry', () => { indexTokenCountMap: {}, initialSummary: { text: 'summary of earlier turns', tokenCount: 40 }, calibrationRatio: 1.25, - compactionSemanticIndex: client.compactionSemanticIndex, + compactionSemanticIndex: evolvedCompactionSemanticIndexSnapshot.entries, + }), + ); + expect(mockFormatAgentMessages.mock.calls[0][4]).toEqual( + expect.objectContaining({ + compactionSemanticIndex: expect.objectContaining({ + baseSnapshot: baseCompactionSemanticIndexSnapshot, + }), }), ); - expect(mockFormatAgentMessages.mock.calls[0][4]).toBeUndefined(); expect(mockFormatAgentMessages).toHaveBeenCalledTimes(1); - expect(mockStripActivityLabelParts).toHaveBeenCalledTimes(1); + expect(mockStripActivityLabelParts).not.toHaveBeenCalled(); + expect(client.compactionSemanticIndexSnapshot).toBe(evolvedCompactionSemanticIndexSnapshot); expect(processStream).toHaveBeenCalledWith( { messages: [currentEvent] }, expect.objectContaining({ diff --git a/packages/api/src/agents/compaction.spec.ts b/packages/api/src/agents/compaction.spec.ts index dd5f32fc355..60dab9b4f86 100644 --- a/packages/api/src/agents/compaction.spec.ts +++ b/packages/api/src/agents/compaction.spec.ts @@ -6,10 +6,11 @@ import { MAX_COMPACTION_SEMANTIC_INDEX_TEXT_LENGTH, } from '@librechat/data-schemas'; import type { ICompactionSemanticIndexProjection } from '@librechat/data-schemas'; -import type { CompactionSemanticIndex } from '@librechat/agents'; +import type { CompactionSemanticIndex, CompactionSemanticIndexSnapshot } from '@librechat/agents'; import { createCompactionSemanticIndexProjection, restoreCompactionSemanticIndex, + restoreCompactionSemanticIndexSnapshot, } from './compaction'; const index = [ @@ -53,6 +54,7 @@ describe('compaction semantic index continuation projection', () => { expect(projection).toEqual({ version: 1, + providedEntryCount: 2, entries: [ index[0], { @@ -64,6 +66,36 @@ describe('compaction semantic index continuation projection', () => { expect(restoreCompactionSemanticIndex(projection)).toEqual(projection?.entries); }); + it('preserves cumulative omission counts across JSON persistence', () => { + const snapshot = { + entries: index, + providedEntryCount: 17, + } satisfies CompactionSemanticIndexSnapshot; + const projection = createCompactionSemanticIndexProjection(snapshot); + + expect(projection).toEqual({ + version: 1, + entries: [index[0], { ...index[1], text: '' }], + providedEntryCount: 17, + }); + expect(restoreCompactionSemanticIndexSnapshot(JSON.parse(JSON.stringify(projection)))).toEqual({ + entries: projection?.entries, + providedEntryCount: 17, + }); + }); + + it('defaults legacy projections to their retained entry count', () => { + const legacyProjection = { + version: 1, + entries: [index[0]], + } satisfies ICompactionSemanticIndexProjection; + + expect(restoreCompactionSemanticIndexSnapshot(legacyProjection)).toEqual({ + entries: legacyProjection.entries, + providedEntryCount: 1, + }); + }); + it('fails closed for malformed or oversized continuation state', () => { const malformed = { version: 1, @@ -77,10 +109,16 @@ describe('compaction semantic index continuation projection', () => { version: 1, entries: [null], } as never; + const impossibleCount = { + version: 1, + entries: index, + providedEntryCount: 1, + } as ICompactionSemanticIndexProjection; expect(restoreCompactionSemanticIndex(malformed)).toBeUndefined(); expect(restoreCompactionSemanticIndex(oversized)).toBeUndefined(); expect(restoreCompactionSemanticIndex(corrupt)).toBeUndefined(); + expect(restoreCompactionSemanticIndexSnapshot(impossibleCount)).toBeUndefined(); expect(createCompactionSemanticIndexProjection(oversized.entries)).toBeUndefined(); }); diff --git a/packages/api/src/agents/compaction.ts b/packages/api/src/agents/compaction.ts index 2f1c442f50f..ff21a391f13 100644 --- a/packages/api/src/agents/compaction.ts +++ b/packages/api/src/agents/compaction.ts @@ -10,7 +10,11 @@ import type { ICompactionSemanticIndexProjection, TCompactionSemanticIndexEntry, } from '@librechat/data-schemas'; -import type { CompactionSemanticIndex, CompactionSemanticIndexEntry } from '@librechat/agents'; +import type { + CompactionSemanticIndex, + CompactionSemanticIndexEntry, + CompactionSemanticIndexSnapshot, +} from '@librechat/agents'; function snapshotEntry( entry: CompactionSemanticIndexEntry, @@ -67,14 +71,28 @@ function snapshotEntry( return { type, toolCallId, ...common }; } +function isCompactionSemanticIndexSnapshot( + input: CompactionSemanticIndex | CompactionSemanticIndexSnapshot, +): input is CompactionSemanticIndexSnapshot { + return !Array.isArray(input); +} + export function createCompactionSemanticIndexProjection( - index: CompactionSemanticIndex | undefined, + input: CompactionSemanticIndex | CompactionSemanticIndexSnapshot | undefined, ): ICompactionSemanticIndexProjection | undefined { + if (input == null) { + return undefined; + } + const isSnapshot = isCompactionSemanticIndexSnapshot(input); + const index = isSnapshot ? input.entries : input; + const providedEntryCount = isSnapshot ? input.providedEntryCount : input.length; if ( - index == null || !Array.isArray(index) || index.length === 0 || - index.length > MAX_COMPACTION_SEMANTIC_INDEX_ENTRIES + index.length > MAX_COMPACTION_SEMANTIC_INDEX_ENTRIES || + providedEntryCount == null || + !Number.isSafeInteger(providedEntryCount) || + providedEntryCount < index.length ) { return undefined; } @@ -89,12 +107,13 @@ export function createCompactionSemanticIndexProjection( return { version: COMPACTION_SEMANTIC_INDEX_PROJECTION_VERSION, entries, + providedEntryCount, }; } -export function restoreCompactionSemanticIndex( +export function restoreCompactionSemanticIndexSnapshot( projection: ICompactionSemanticIndexProjection | null | undefined, -): CompactionSemanticIndex | undefined { +): CompactionSemanticIndexSnapshot | undefined { if (!isCompactionSemanticIndexProjection(projection)) { return undefined; } @@ -106,5 +125,14 @@ export function restoreCompactionSemanticIndex( } entries.push(Object.freeze(snapshot)); } - return Object.freeze(entries); + return Object.freeze({ + entries: Object.freeze(entries), + providedEntryCount: projection.providedEntryCount ?? entries.length, + }); +} + +export function restoreCompactionSemanticIndex( + projection: ICompactionSemanticIndexProjection | null | undefined, +): CompactionSemanticIndex | undefined { + return restoreCompactionSemanticIndexSnapshot(projection)?.entries; } diff --git a/packages/data-schemas/src/methods/conversation.spec.ts b/packages/data-schemas/src/methods/conversation.spec.ts index 5b24bbc0437..a73a08c88b3 100644 --- a/packages/data-schemas/src/methods/conversation.spec.ts +++ b/packages/data-schemas/src/methods/conversation.spec.ts @@ -4680,6 +4680,7 @@ describe('Conversation Operations', () => { const contextMeta = { calibrationRatio: 1.25, encoding: 'o200k_base' }; const compactionSemanticIndex = { version: 1 as const, + providedEntryCount: 9, entries: [ { type: 'activity_phase' as const, @@ -4831,6 +4832,22 @@ describe('Conversation Operations', () => { }, }), ).rejects.toThrow('Event actor compaction semantic index is invalid'); + + await expect( + methods.commitAgentEventActorState({ + user: 'actor-context-user', + conversationId, + invocationId: 'invalid-compaction-count', + expectedEpoch: 0, + action: { toolName: 'submit_move' }, + checkpoint: actorCheckpoint, + compactionSemanticIndex: { + version: 1, + entries: compactionSemanticIndex.entries, + providedEntryCount: 0, + }, + }), + ).rejects.toThrow('Event actor compaction semantic index is invalid'); }); it('retains exact legacy settled receipts until delivery-ledger migration', async () => { diff --git a/packages/data-schemas/src/schema/convo.ts b/packages/data-schemas/src/schema/convo.ts index 61f119f2f1f..de7ac5a1d66 100644 --- a/packages/data-schemas/src/schema/convo.ts +++ b/packages/data-schemas/src/schema/convo.ts @@ -202,6 +202,7 @@ const convoSchema: Schema = new Schema( message: `Compaction semantic index exceeds ${MAX_COMPACTION_SEMANTIC_INDEX_ENTRIES} entries`, }, }, + providedEntryCount: { type: Number, min: 0, default: undefined }, }, _id: false, default: undefined, diff --git a/packages/data-schemas/src/types/compaction.ts b/packages/data-schemas/src/types/compaction.ts index f72424e91cb..4bfee111364 100644 --- a/packages/data-schemas/src/types/compaction.ts +++ b/packages/data-schemas/src/types/compaction.ts @@ -39,6 +39,8 @@ export type TCompactionSemanticIndexEntry = export interface ICompactionSemanticIndexProjection { version: typeof COMPACTION_SEMANTIC_INDEX_PROJECTION_VERSION; entries: TCompactionSemanticIndexEntry[]; + /** Cumulative entries supplied before bounded retention. Absent on legacy snapshots. */ + providedEntryCount?: number; } function isBoundedIdentity(value: string | undefined): value is string { @@ -89,7 +91,10 @@ export function isCompactionSemanticIndexProjection( projection?.version !== COMPACTION_SEMANTIC_INDEX_PROJECTION_VERSION || !Array.isArray(projection.entries) || projection.entries.length === 0 || - projection.entries.length > MAX_COMPACTION_SEMANTIC_INDEX_ENTRIES + projection.entries.length > MAX_COMPACTION_SEMANTIC_INDEX_ENTRIES || + (projection.providedEntryCount !== undefined && + (!Number.isSafeInteger(projection.providedEntryCount) || + projection.providedEntryCount < projection.entries.length)) ) { return false; } From f110ac06a258ca1c9a7fc643032bc2daa7033a0b Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Sun, 30 Aug 2026 15:51:41 -0400 Subject: [PATCH 2/2] style: sort compaction adapter imports --- packages/api/src/agents/compaction.spec.ts | 2 +- packages/api/src/agents/compaction.ts | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/api/src/agents/compaction.spec.ts b/packages/api/src/agents/compaction.spec.ts index 60dab9b4f86..afe5b59f5ad 100644 --- a/packages/api/src/agents/compaction.spec.ts +++ b/packages/api/src/agents/compaction.spec.ts @@ -5,8 +5,8 @@ import { MAX_COMPACTION_SEMANTIC_INDEX_SOURCE_CONTENT_INDEX, MAX_COMPACTION_SEMANTIC_INDEX_TEXT_LENGTH, } from '@librechat/data-schemas'; -import type { ICompactionSemanticIndexProjection } from '@librechat/data-schemas'; import type { CompactionSemanticIndex, CompactionSemanticIndexSnapshot } from '@librechat/agents'; +import type { ICompactionSemanticIndexProjection } from '@librechat/data-schemas'; import { createCompactionSemanticIndexProjection, restoreCompactionSemanticIndex, diff --git a/packages/api/src/agents/compaction.ts b/packages/api/src/agents/compaction.ts index ff21a391f13..0783c122cf0 100644 --- a/packages/api/src/agents/compaction.ts +++ b/packages/api/src/agents/compaction.ts @@ -6,15 +6,15 @@ import { MAX_COMPACTION_SEMANTIC_INDEX_TEXT_LENGTH, isCompactionSemanticIndexProjection, } from '@librechat/data-schemas'; -import type { - ICompactionSemanticIndexProjection, - TCompactionSemanticIndexEntry, -} from '@librechat/data-schemas'; import type { CompactionSemanticIndex, CompactionSemanticIndexEntry, CompactionSemanticIndexSnapshot, } from '@librechat/agents'; +import type { + ICompactionSemanticIndexProjection, + TCompactionSemanticIndexEntry, +} from '@librechat/data-schemas'; function snapshotEntry( entry: CompactionSemanticIndexEntry,