⚡ perf: Build the Memory Message Copy Only When Something Reads It (#15164)

buildMessages formatted every history row twice per turn — a prompt
copy and a memory copy — then discarded the entire memory payload
unless some row carried fileContext, which is the rare case. The
memory copy has exactly two consumers: that payload, and the canonical
recount of a row, where it is content-identical to the prompt copy
unless the row itself has fileContext. So the prompt copy is now the
recount surface for context-free rows, a fileContext row builds its
memory copy at recount time, and the full memory payload is assembled
in a deferred pass — same formatting, same per-row merge order — only
once a row has proven the payload will be kept. The common turn
formats each row once instead of twice and no longer allocates a
payload it throws away.

Also forwards the run's useLegacyContent to formatAgentMessages as the
new legacyContent option, inert on the current SDK release: once the
SDK change ships, text history is emitted pre-flattened so the
per-request legacy projection stops cloning every message and the
context meter's identity-based count reuse holds across the
projection.
This commit is contained in:
Danny Avila 2026-08-24 10:33:47 -04:00 • committed by GitHub
parent 6f3d303985
commit 5a8700643c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 86 additions and 13 deletions

View file

@ -1693,12 +1693,15 @@ class AgentClient extends BaseClient {
let hasFileContext = false;
let promptTokenTotal = 0;
const encoding = this.getEncoding();
const formattedMessages = orderedMessages.map((message, i) => {
const formattedMessage = formatMessage({
message,
userName: this.options?.name,
assistantName: this.options?.modelLabel,
});
/**
* Rebuilds the memory-side copy of one source row: the same formatting and
* per-message merges as the prompt copy, minus the fileContext prepend.
* Only materialized when something actually consumes it — the canonical
* recount of a fileContext row, or the memory payload once any row proves
* to carry fileContext — instead of unconditionally formatting every row
* twice per turn.
*/
const buildMemoryFormattedMessage = (message) => {
const memoryFormattedMessage = formatMessage({
message,
userName: this.options?.name,
@ -1706,9 +1709,28 @@ class AgentClient extends BaseClient {
});
const sourceMessageId = message.messageId ?? message.id;
if (typeof sourceMessageId === 'string' && sourceMessageId.length > 0) {
formattedMessage.messageId = sourceMessageId;
memoryFormattedMessage.messageId = sourceMessageId;
}
if (Array.isArray(message.quotes) && message.quotes.length > 0) {
prependQuotes(memoryFormattedMessage, message.quotes);
}
const turnFiles = this.message_file_map?.[message.messageId] ?? message.files;
applyAttachmentOnlyText(memoryFormattedMessage, turnFiles);
return memoryFormattedMessage;
};
/** Memory copies built for canonical recounts, reused by the memory payload pass. */
const memoryFormattedMessages = [];
const formattedMessages = orderedMessages.map((message, i) => {
const formattedMessage = formatMessage({
message,
userName: this.options?.name,
assistantName: this.options?.modelLabel,
});
const sourceMessageId = message.messageId ?? message.id;
if (typeof sourceMessageId === 'string' && sourceMessageId.length > 0) {
formattedMessage.messageId = sourceMessageId;
}
/**
* Bind file context to the message it belongs to. Historical attachments
@ -1732,7 +1754,6 @@ class AgentClient extends BaseClient {
*/
if (Array.isArray(message.quotes) && message.quotes.length > 0) {
prependQuotes(formattedMessage, message.quotes);
prependQuotes(memoryFormattedMessage, message.quotes);
}
/**
@ -1746,9 +1767,6 @@ class AgentClient extends BaseClient {
*/
const turnFiles = this.message_file_map?.[message.messageId] ?? message.files;
applyAttachmentOnlyText(formattedMessage, turnFiles);
applyAttachmentOnlyText(memoryFormattedMessage, turnFiles);
memoryPayload.push(memoryFormattedMessage);
const dbTokenCount = Number(orderedMessages[i].tokenCount);
const hasDbTokenCount = Number.isFinite(dbTokenCount) && dbTokenCount > 0;
@ -1766,7 +1784,15 @@ class AgentClient extends BaseClient {
let canonicalTokenCount = hasDbTokenCount ? dbTokenCount : 0;
if (needsCanonicalTokenCount) {
canonicalTokenCount = countFormattedMessageTokens(memoryFormattedMessage, encoding);
/** Without fileContext the memory copy is content-identical to the
* prompt copy, so the prompt copy is the counting surface; with it,
* the canonical count must exclude the prepended context. */
let countSurface = formattedMessage;
if (message.fileContext) {
memoryFormattedMessages[i] = buildMemoryFormattedMessage(message);
countSurface = memoryFormattedMessages[i];
}
canonicalTokenCount = countFormattedMessageTokens(countSurface, encoding);
}
const promptMessageTokenCount = message.fileContext
@ -1917,6 +1943,13 @@ class AgentClient extends BaseClient {
}
}
}
if (hasFileContext) {
for (let i = 0; i < orderedMessages.length; i++) {
memoryPayload.push(
memoryFormattedMessages[i] ?? buildMemoryFormattedMessage(orderedMessages[i]),
);
}
}
this.memoryPayload = hasFileContext ? memoryPayload : null;
messages = orderedMessages;
promptTokens = promptTokenTotal;
@ -3288,13 +3321,15 @@ class AgentClient extends BaseClient {
manualSkillPrimes,
alwaysApplySkillPrimes,
});
const useLegacyContent = this.options.agent?.useLegacyContent === true;
const formatOptions =
needsReasoningContentFormat || freshSkillPrimeNames.size > 0
needsReasoningContentFormat || freshSkillPrimeNames.size > 0 || useLegacyContent
? {
...(needsReasoningContentFormat ? { preserveReasoningContent: true } : {}),
...(freshSkillPrimeNames.size > 0
? { skipSkillBodyNames: freshSkillPrimeNames }
: {}),
...(useLegacyContent ? { legacyContent: true } : {}),
}
: undefined;
let {

View file

@ -3783,6 +3783,44 @@ describe('AgentClient - titleConvo', () => {
expect(client.memoryPayload[0].content).toBe('What is written here?');
});
it('recounts a quote-bearing history row from quote-merged content and keeps the memory payload unbuilt without file context', async () => {
const { countFormattedMessageTokens } = require('@librechat/api');
countFormattedMessageTokens.mockImplementation(({ content }) => {
const text = Array.isArray(content)
? content.map((part) => part.text ?? part[ContentTypes.TEXT] ?? '').join('\n')
: String(content ?? '');
return text.includes('quoted excerpt') ? 77 : 11;
});
const result = await client.buildMessages(
[
{
messageId: 'msg-1',
parentMessageId: null,
sender: 'User',
text: 'Discuss this.',
isCreatedByUser: true,
tokenCount: 5,
quotes: ['quoted excerpt'],
},
{
messageId: 'msg-2',
parentMessageId: 'msg-1',
sender: 'Assistant',
text: 'Sure.',
isCreatedByUser: false,
tokenCount: 3,
},
],
'msg-2',
{},
);
expect(result.tokenCountMap['msg-1']).toBe(77);
expect(result.tokenCountMap['msg-2']).toBe(3);
expect(client.memoryPayload).toBeNull();
});
it('does not duplicate a file that is both request context and scoped context', async () => {
const sharedFile = makeTextFile('shared-file', 'shared.txt', 'Shared duplicate context');