From b5362e9b28714ff666389f0f2d22c05dff99767e Mon Sep 17 00:00:00 2001 From: Danny Avila Date: Mon, 22 Jun 2026 14:17:54 -0400 Subject: [PATCH] =?UTF-8?q?=F0=9F=A7=A0=20fix:=20Persist=20memory=20capabi?= =?UTF-8?q?lity=20on=20saved=20agents;=20honor=20registration=20flag?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add Tools.memory to the v1 systemTools allowlist so filterAuthorizedTools no longer silently drops the memory marker when an agent with the Memory capability is created/updated/duplicated through the builder (previously the capability only worked for ephemeral chats, not persisted agents). - agentHasInlineMemoryTools now honors an explicit memoryToolsRegistered boolean before falling back to the raw `memory` marker, so an initialized config whose registration was denied (memoryAvailable false) is not given keyed memory context just because the marker survives in tools. --- .../agents/filterAuthorizedTools.spec.js | 4 +-- api/server/controllers/agents/v1.js | 1 + packages/api/src/agents/memory.spec.ts | 25 +++++++++++++++++++ packages/api/src/agents/memory.ts | 8 ++++-- 4 files changed, 34 insertions(+), 4 deletions(-) diff --git a/api/server/controllers/agents/filterAuthorizedTools.spec.js b/api/server/controllers/agents/filterAuthorizedTools.spec.js index 89835fac06..677bccdfe0 100644 --- a/api/server/controllers/agents/filterAuthorizedTools.spec.js +++ b/api/server/controllers/agents/filterAuthorizedTools.spec.js @@ -183,12 +183,12 @@ describe('MCP Tool Authorization', () => { test('should keep system tools without querying MCP registry', async () => { const result = await filterAuthorizedTools({ - tools: ['execute_code', 'file_search', 'web_search'], + tools: ['execute_code', 'file_search', 'web_search', 'memory'], userId, availableTools: {}, }); - expect(result).toEqual(['execute_code', 'file_search', 'web_search']); + expect(result).toEqual(['execute_code', 'file_search', 'web_search', 'memory']); expect(mockGetAllServerConfigs).not.toHaveBeenCalled(); }); diff --git a/api/server/controllers/agents/v1.js b/api/server/controllers/agents/v1.js index b7f7a9bd29..eb0fd001d0 100644 --- a/api/server/controllers/agents/v1.js +++ b/api/server/controllers/agents/v1.js @@ -56,6 +56,7 @@ const systemTools = { [Tools.execute_code]: true, [Tools.file_search]: true, [Tools.web_search]: true, + [Tools.memory]: true, }; const MAX_SEARCH_LEN = 100; diff --git a/packages/api/src/agents/memory.spec.ts b/packages/api/src/agents/memory.spec.ts index f1a8f33e38..124cf346ff 100644 --- a/packages/api/src/agents/memory.spec.ts +++ b/packages/api/src/agents/memory.spec.ts @@ -8,6 +8,7 @@ import { createDeleteMemoryTool, getRequestMemories, invalidateRequestMemories, + agentHasInlineMemoryTools, } from './memory'; jest.mock('~/stream/GenerationJobManager'); @@ -663,6 +664,30 @@ describe('createMemoryTool tokenLimit enforcement', () => { }); }); +describe('agentHasInlineMemoryTools', () => { + it('returns false for a nullish agent', () => { + expect(agentHasInlineMemoryTools(null)).toBe(false); + expect(agentHasInlineMemoryTools(undefined)).toBe(false); + }); + + it('honors an explicit memoryToolsRegistered flag over the raw marker', () => { + /** Initialized config whose registration was denied (memoryAvailable false) + * but whose raw `memory` marker survived in tools must not be treated as + * memory-enabled. */ + expect(agentHasInlineMemoryTools({ memoryToolsRegistered: false, tools: ['memory'] })).toBe( + false, + ); + expect(agentHasInlineMemoryTools({ memoryToolsRegistered: true, tools: [] })).toBe(true); + }); + + it('falls back to the raw memory marker when no flag is present', () => { + expect(agentHasInlineMemoryTools({ tools: ['memory'] })).toBe(true); + expect(agentHasInlineMemoryTools({ tools: [{ name: 'memory' }] })).toBe(true); + expect(agentHasInlineMemoryTools({ tools: ['execute_code'] })).toBe(false); + expect(agentHasInlineMemoryTools({ tools: [] })).toBe(false); + }); +}); + describe('getRequestMemories caching', () => { it('memoizes per request, then re-fetches after invalidation', async () => { const getFormattedMemories = jest diff --git a/packages/api/src/agents/memory.ts b/packages/api/src/agents/memory.ts index aa53907d5a..e6b3dbebb3 100644 --- a/packages/api/src/agents/memory.ts +++ b/packages/api/src/agents/memory.ts @@ -447,8 +447,12 @@ export function agentHasInlineMemoryTools(agent: InlineMemoryAgent): boolean { if (!agent) { return false; } - if (agent.memoryToolsRegistered === true) { - return true; + /** An initialized config carries an explicit boolean: honor it so an agent + * whose registration was denied (`false`) is not treated as memory-enabled + * just because the raw `memory` marker survives in `tools`. Fall back to the + * marker only for the raw agent, where the flag is absent. */ + if (typeof agent.memoryToolsRegistered === 'boolean') { + return agent.memoryToolsRegistered; } return (agent.tools ?? []).some( (entry) =>