🧩 fix: Bring memory tool to parity with other ephemeral tools

- Add `memory` to the model-spec schema/type and honor `modelSpec.memory`
  in both ephemeral paths (load.ts, added.ts) and the frontend spec
  application, so admins can pre-enable Memory from a model spec exactly
  like webSearch/fileSearch/executeCode.
- Add LAST_MEMORY_TOGGLE_ to the timestamped-storage cleanup list so stale
  per-conversation memory toggles are purged on startup like the others.
- Hide the agent-builder Memory toggle for users who disabled memory in
  personalization (memories === false), mirroring the chat badge's opt-out
  gate, so the setting isn't shown as inert/misleading.
This commit is contained in:
Danny Avila 2026-06-24 16:49:34 -04:00
parent b5362e9b28
commit 672a03b052
7 changed files with 31 additions and 4 deletions

View file

@ -20,7 +20,13 @@ import {
getIconKey,
cn,
} from '~/utils';
import { useLocalize, useVisibleTools, useHasAccess, useHasMemoryAccess } from '~/hooks';
import {
useLocalize,
useVisibleTools,
useHasAccess,
useHasMemoryAccess,
useAuthContext,
} from '~/hooks';
import { ToolSelectDialog, MCPToolSelectDialog } from '~/components/Tools';
import useAgentCapabilities from '~/hooks/Agents/useAgentCapabilities';
import { useListSkillsQuery, useGetAgentFiles } from '~/data-provider';
@ -114,8 +120,12 @@ export default function AgentConfig() {
permissionType: PermissionTypes.SKILLS,
permission: Permissions.USE,
});
const { user } = useAuthContext();
const hasMemoryAccess = useHasMemoryAccess();
const showMemory = hasMemoryAccess && memoryEnabled;
/** Mirror the chat memory badge's opt-out gate: a user who disabled memory in
* personalization can't use the inline tools, so the builder toggle is inert
* for them and must be hidden too. */
const showMemory = hasMemoryAccess && memoryEnabled && user?.personalization?.memories !== false;
const showSkills = hasSkillsAccess && skillsEnabled;
const { data: skillsData } = useListSkillsQuery({ limit: 100 }, { enabled: showSkills });
const skillsMap = useMemo(() => {

View file

@ -132,6 +132,19 @@ describe('timestamps', () => {
expect(localStorage.getItem(regularKey)).toBe('value');
});
it('should purge stale memory toggle entries', () => {
const key = `${LocalStorageKeys.LAST_MEMORY_TOGGLE_}convo-321`;
const oldTimestamp = Date.now() - 3 * 24 * 60 * 60 * 1000; // 3 days ago
localStorage.setItem(key, 'true');
localStorage.setItem(`${key}_TIMESTAMP`, oldTimestamp.toString());
cleanupTimestampedStorage();
expect(localStorage.getItem(key)).toBeNull();
expect(localStorage.getItem(`${key}_TIMESTAMP`)).toBeNull();
});
});
describe('migrateExistingEntries', () => {

View file

@ -368,6 +368,7 @@ export function applyModelSpecEphemeralAgent({
web_search: modelSpec.webSearch ?? false,
file_search: modelSpec.fileSearch ?? false,
execute_code: modelSpec.executeCode ?? false,
memory: modelSpec.memory ?? false,
artifacts: modelSpec.artifacts === true ? 'default' : modelSpec.artifacts || '',
};

View file

@ -16,6 +16,7 @@ const TIMESTAMPED_KEYS = [
LocalStorageKeys.LAST_WEB_SEARCH_TOGGLE_,
LocalStorageKeys.LAST_FILE_SEARCH_TOGGLE_,
LocalStorageKeys.LAST_ARTIFACTS_TOGGLE_,
LocalStorageKeys.LAST_MEMORY_TOGGLE_,
LocalStorageKeys.PIN_MCP_,
];

View file

@ -181,7 +181,7 @@ export async function loadAddedAgent(
if (ephemeralAgent?.web_search === true || modelSpec?.webSearch === true) {
tools.push(Tools.web_search);
}
if (ephemeralAgent?.memory === true) {
if (ephemeralAgent?.memory === true || modelSpec?.memory === true) {
tools.push(Tools.memory);
}

View file

@ -73,7 +73,7 @@ export async function loadEphemeralAgent(
if (ephemeralAgent?.web_search === true || modelSpec?.webSearch === true) {
tools.push(Tools.web_search);
}
if (ephemeralAgent?.memory === true) {
if (ephemeralAgent?.memory === true || modelSpec?.memory === true) {
tools.push(Tools.memory);
}

View file

@ -44,6 +44,7 @@ export type TModelSpec = {
webSearch?: boolean;
fileSearch?: boolean;
executeCode?: boolean;
memory?: boolean;
artifacts?: string | boolean;
mcpServers?: string[];
skills?: boolean | string[];
@ -76,6 +77,7 @@ export const tModelSpecSchema = z.object({
webSearch: z.boolean().optional(),
fileSearch: z.boolean().optional(),
executeCode: z.boolean().optional(),
memory: z.boolean().optional(),
artifacts: z.union([z.string(), z.boolean()]).optional(),
mcpServers: z.array(z.string()).optional(),
skills: z.union([z.boolean(), z.array(z.string())]).optional(),