mirror of
https://github.com/danny-avila/LibreChat.git
synced 2026-08-03 22:32:42 +00:00
🧠 fix: Invalidate request memory cache after inline writes
Inline set_memory/delete_memory now invalidate the request-scoped getFormattedMemories cache on a successful write, so a later tool round in the same response is seeded with the post-write usage total instead of the stale pre-write one (multi-round writes no longer collectively exceed tokenLimit, and a set after a delete is not over-counted). The within-round sharing across multiple memory-enabled agents is preserved.
This commit is contained in:
parent
9f22c0c804
commit
4754c981aa
2 changed files with 83 additions and 2 deletions
|
|
@ -2,7 +2,13 @@ import { Types } from 'mongoose';
|
|||
import { Run, Providers } from '@librechat/agents';
|
||||
import type { IUser } from '@librechat/data-schemas';
|
||||
import type { Response } from 'express';
|
||||
import { processMemory, createMemoryTool } from './memory';
|
||||
import {
|
||||
processMemory,
|
||||
createMemoryTool,
|
||||
createDeleteMemoryTool,
|
||||
getRequestMemories,
|
||||
invalidateRequestMemories,
|
||||
} from './memory';
|
||||
|
||||
jest.mock('~/stream/GenerationJobManager');
|
||||
|
||||
|
|
@ -622,4 +628,57 @@ describe('createMemoryTool tokenLimit enforcement', () => {
|
|||
|
||||
expect(setMemory).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('fires onWrite after a successful set, but not when the write fails', async () => {
|
||||
const onWrite = jest.fn();
|
||||
const okTool = createMemoryTool({
|
||||
userId: 'user-1',
|
||||
setMemory: jest.fn().mockResolvedValue({ ok: true }),
|
||||
onWrite,
|
||||
});
|
||||
await okTool.invoke({ key: 'k1', value: 'a fact' });
|
||||
expect(onWrite).toHaveBeenCalledTimes(1);
|
||||
|
||||
onWrite.mockClear();
|
||||
const failTool = createMemoryTool({
|
||||
userId: 'user-1',
|
||||
setMemory: jest.fn().mockResolvedValue({ ok: false }),
|
||||
onWrite,
|
||||
});
|
||||
await failTool.invoke({ key: 'k1', value: 'a fact' });
|
||||
expect(onWrite).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('fires onWrite after a successful delete', async () => {
|
||||
const onWrite = jest.fn();
|
||||
const tool = createDeleteMemoryTool({
|
||||
userId: 'user-1',
|
||||
deleteMemory: jest.fn().mockResolvedValue({ ok: true }),
|
||||
onWrite,
|
||||
});
|
||||
|
||||
await tool.invoke({ key: 'k1' });
|
||||
|
||||
expect(onWrite).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRequestMemories caching', () => {
|
||||
it('memoizes per request, then re-fetches after invalidation', async () => {
|
||||
const getFormattedMemories = jest
|
||||
.fn()
|
||||
.mockResolvedValue({ withKeys: '', withoutKeys: '', totalTokens: 10 });
|
||||
const req = {};
|
||||
|
||||
await getRequestMemories({ req, userId: 'user-1', getFormattedMemories });
|
||||
await getRequestMemories({ req, userId: 'user-1', getFormattedMemories });
|
||||
/** A second memory-enabled agent in the same run reuses the first fetch. */
|
||||
expect(getFormattedMemories).toHaveBeenCalledTimes(1);
|
||||
|
||||
/** A successful inline write invalidates the cache so a later tool round in
|
||||
* the same response re-reads the post-write usage total. */
|
||||
invalidateRequestMemories(req);
|
||||
await getRequestMemories({ req, userId: 'user-1', getFormattedMemories });
|
||||
expect(getFormattedMemories).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -122,6 +122,7 @@ export const createMemoryTool = ({
|
|||
charLimit,
|
||||
tokenLimit,
|
||||
totalTokens = 0,
|
||||
onWrite,
|
||||
}: {
|
||||
userId: string | ObjectId;
|
||||
setMemory: MemoryMethods['setMemory'];
|
||||
|
|
@ -129,6 +130,7 @@ export const createMemoryTool = ({
|
|||
charLimit?: number;
|
||||
tokenLimit?: number;
|
||||
totalTokens?: number;
|
||||
onWrite?: () => void;
|
||||
}): DynamicStructuredTool => {
|
||||
/** Running token total, advanced after each successful write. Writes are
|
||||
* serialized through `writeChain` so multiple `set_memory` calls in one
|
||||
|
|
@ -227,6 +229,7 @@ export const createMemoryTool = ({
|
|||
currentTotalTokens = newTotalTokens;
|
||||
writtenTokensByKey.set(key, tokenCount);
|
||||
}
|
||||
onWrite?.();
|
||||
logger.debug(`Memory set for key "${key}" (${tokenCount} tokens) for user "${userId}"`);
|
||||
return [`Memory set for key "${key}" (${tokenCount} tokens)`, artifact];
|
||||
}
|
||||
|
|
@ -273,10 +276,12 @@ export const createDeleteMemoryTool = ({
|
|||
userId,
|
||||
deleteMemory,
|
||||
validKeys,
|
||||
onWrite,
|
||||
}: {
|
||||
userId: string | ObjectId;
|
||||
deleteMemory: MemoryMethods['deleteMemory'];
|
||||
validKeys?: string[];
|
||||
onWrite?: () => void;
|
||||
}): DynamicStructuredTool => {
|
||||
return tool(
|
||||
async ({ key }) => {
|
||||
|
|
@ -299,6 +304,7 @@ export const createDeleteMemoryTool = ({
|
|||
|
||||
const result = await deleteMemory({ userId, key });
|
||||
if (result.ok) {
|
||||
onWrite?.();
|
||||
logger.debug(`Memory deleted for key "${key}" for user "${userId}"`);
|
||||
return [`Memory deleted for key "${key}"`, artifact];
|
||||
}
|
||||
|
|
@ -474,6 +480,16 @@ export function getRequestMemories({
|
|||
return cached;
|
||||
}
|
||||
|
||||
/**
|
||||
* Drops the cached memories for a request so the next {@link getRequestMemories}
|
||||
* re-fetches. Inline `set_memory`/`delete_memory` writes call this on success so
|
||||
* a later tool round in the same response is seeded with the post-write usage
|
||||
* total instead of a stale pre-write one.
|
||||
*/
|
||||
export function invalidateRequestMemories(req: object): void {
|
||||
requestMemoriesCache.delete(req);
|
||||
}
|
||||
|
||||
/**
|
||||
* Re-checks the run-level memory gate at tool-execution time: the agents
|
||||
* `memory` capability is enabled, memory is configured, the user hasn't opted
|
||||
|
|
@ -553,7 +569,12 @@ export async function buildInlineMemoryTool({
|
|||
if (!allowed) {
|
||||
return null;
|
||||
}
|
||||
return createDeleteMemoryTool({ userId, deleteMemory: memoryMethods.deleteMemory, validKeys });
|
||||
return createDeleteMemoryTool({
|
||||
userId,
|
||||
deleteMemory: memoryMethods.deleteMemory,
|
||||
validKeys,
|
||||
onWrite: () => invalidateRequestMemories(req),
|
||||
});
|
||||
}
|
||||
|
||||
const allowed = await isMemoryToolAllowed({
|
||||
|
|
@ -591,6 +612,7 @@ export async function buildInlineMemoryTool({
|
|||
charLimit,
|
||||
tokenLimit,
|
||||
totalTokens,
|
||||
onWrite: () => invalidateRequestMemories(req),
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue