feat: add memory management actions and local filters in memory settings (#1467)

* Add MVP memory management actions

* Fix memory settings locale coverage

* Polish memory management interactions

* Add memory search and type filters

* Refine memory settings review feedback

* docs: simplify memory settings review setup

* fix: restore memory updater compatibility helpers

* fix: address memory settings review feedback

* docs: soften memory sample review wording

---------

Co-authored-by: Willem Jiang <willem.jiang@gmail.com>
Co-authored-by: JeffJiang <for-eleven@hotmail.com>
This commit is contained in:
Admire
2026-03-29 13:14:45 +08:00
committed by GitHub
parent 481494b9c0
commit 7eb3a150b5
18 changed files with 1025 additions and 130 deletions
@@ -1,9 +1,28 @@
"use client";
import { Trash2Icon } from "lucide-react";
import Link from "next/link";
import { useDeferredValue, useState } from "react";
import { toast } from "sonner";
import { Streamdown } from "streamdown";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from "@/components/ui/dialog";
import { Input } from "@/components/ui/input";
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
import { useI18n } from "@/core/i18n/hooks";
import { useMemory } from "@/core/memory/hooks";
import {
useClearMemory,
useDeleteMemoryFact,
useMemory,
} from "@/core/memory/hooks";
import type { UserMemory } from "@/core/memory/types";
import { streamdownPlugins } from "@/core/streamdown/plugins";
import { pathOfThread } from "@/core/threads/utils";
@@ -11,6 +30,20 @@ import { formatTimeAgo } from "@/core/utils/datetime";
import { SettingsSection } from "./settings-section";
type MemoryViewFilter = "all" | "facts" | "summaries";
type MemoryFact = UserMemory["facts"][number];
type MemorySection = {
title: string;
summary: string;
updatedAt?: string;
};
type MemorySectionGroup = {
title: string;
sections: MemorySection[];
};
function confidenceToLevelKey(confidence: unknown): {
key: "veryHigh" | "high" | "normal" | "unknown";
value?: number;
@@ -19,41 +52,82 @@ function confidenceToLevelKey(confidence: unknown): {
return { key: "unknown" };
}
// Clamp to [0, 1] since confidence is expected to be a probability-like score.
const value = Math.min(1, Math.max(0, confidence));
// 3 levels:
// - veryHigh: [0.85, 1]
// - high: [0.65, 0.85)
// - normal: [0, 0.65)
if (value >= 0.85) return { key: "veryHigh", value };
if (value >= 0.65) return { key: "high", value };
return { key: "normal", value };
}
function formatMemorySection(
title: string,
summary: string,
updatedAt: string | undefined,
section: MemorySection,
t: ReturnType<typeof useI18n>["t"],
): string {
const content =
summary.trim() ||
section.summary.trim() ||
`<span class="text-muted-foreground">${t.settings.memory.markdown.empty}</span>`;
return [
`### ${title}`,
`### ${section.title}`,
content,
"",
updatedAt &&
`> ${t.settings.memory.markdown.updatedAt}: \`${formatTimeAgo(updatedAt)}\``,
section.updatedAt &&
`> ${t.settings.memory.markdown.updatedAt}: \`${formatTimeAgo(section.updatedAt)}\``,
]
.filter(Boolean)
.join("\n");
}
function memoryToMarkdown(
function buildMemorySectionGroups(
memory: UserMemory,
t: ReturnType<typeof useI18n>["t"],
): MemorySectionGroup[] {
return [
{
title: t.settings.memory.markdown.userContext,
sections: [
{
title: t.settings.memory.markdown.work,
summary: memory.user.workContext.summary,
updatedAt: memory.user.workContext.updatedAt,
},
{
title: t.settings.memory.markdown.personal,
summary: memory.user.personalContext.summary,
updatedAt: memory.user.personalContext.updatedAt,
},
{
title: t.settings.memory.markdown.topOfMind,
summary: memory.user.topOfMind.summary,
updatedAt: memory.user.topOfMind.updatedAt,
},
],
},
{
title: t.settings.memory.markdown.historyBackground,
sections: [
{
title: t.settings.memory.markdown.recentMonths,
summary: memory.history.recentMonths.summary,
updatedAt: memory.history.recentMonths.updatedAt,
},
{
title: t.settings.memory.markdown.earlierContext,
summary: memory.history.earlierContext.summary,
updatedAt: memory.history.earlierContext.updatedAt,
},
{
title: t.settings.memory.markdown.longTermBackground,
summary: memory.history.longTermBackground.summary,
updatedAt: memory.history.longTermBackground.updatedAt,
},
],
},
];
}
function summariesToMarkdown(
memory: UserMemory,
sectionGroups: MemorySectionGroup[],
t: ReturnType<typeof useI18n>["t"],
) {
const parts: string[] = [];
@@ -62,83 +136,14 @@ function memoryToMarkdown(
`- **${t.common.lastUpdated}**: \`${formatTimeAgo(memory.lastUpdated)}\``,
);
parts.push(`\n## ${t.settings.memory.markdown.userContext}`);
parts.push(
formatMemorySection(
t.settings.memory.markdown.work,
memory.user.workContext.summary,
memory.user.workContext.updatedAt,
t,
),
);
parts.push(
formatMemorySection(
t.settings.memory.markdown.personal,
memory.user.personalContext.summary,
memory.user.personalContext.updatedAt,
t,
),
);
parts.push(
formatMemorySection(
t.settings.memory.markdown.topOfMind,
memory.user.topOfMind.summary,
memory.user.topOfMind.updatedAt,
t,
),
);
parts.push(`\n## ${t.settings.memory.markdown.historyBackground}`);
parts.push(
formatMemorySection(
t.settings.memory.markdown.recentMonths,
memory.history.recentMonths.summary,
memory.history.recentMonths.updatedAt,
t,
),
);
parts.push(
formatMemorySection(
t.settings.memory.markdown.earlierContext,
memory.history.earlierContext.summary,
memory.history.earlierContext.updatedAt,
t,
),
);
parts.push(
formatMemorySection(
t.settings.memory.markdown.longTermBackground,
memory.history.longTermBackground.summary,
memory.history.longTermBackground.updatedAt,
t,
),
);
parts.push(`\n## ${t.settings.memory.markdown.facts}`);
if (memory.facts.length === 0) {
parts.push(
`<span class="text-muted-foreground">${t.settings.memory.markdown.empty}</span>`,
);
} else {
parts.push(
[
`| ${t.settings.memory.markdown.table.category} | ${t.settings.memory.markdown.table.confidence} | ${t.settings.memory.markdown.table.content} | ${t.settings.memory.markdown.table.source} | ${t.settings.memory.markdown.table.createdAt} |`,
"|---|---|---|---|---|",
...memory.facts.map((f) => {
const { key, value } = confidenceToLevelKey(f.confidence);
const levelLabel =
t.settings.memory.markdown.table.confidenceLevel[key];
const confidenceText =
typeof value === "number" ? `${levelLabel}` : levelLabel;
return `| ${upperFirst(f.category)} | ${confidenceText} | ${f.content} | [${t.settings.memory.markdown.table.view}](${pathOfThread(f.source)}) | ${formatTimeAgo(f.createdAt)} |`;
}),
].join("\n"),
);
for (const group of sectionGroups) {
parts.push(`\n## ${group.title}`);
for (const section of group.sections) {
parts.push(formatMemorySection(section, t));
}
}
const markdown = parts.join("\n\n");
// Ensure every level-2 heading (##) is preceded by a horizontal rule.
const lines = markdown.split("\n");
const out: string[] = [];
let i = 0;
@@ -155,36 +160,355 @@ function memoryToMarkdown(
return out.join("\n");
}
export function MemorySettingsPage() {
const { t } = useI18n();
const { memory, isLoading, error } = useMemory();
function isMemorySummaryEmpty(memory: UserMemory) {
return (
<SettingsSection
title={t.settings.memory.title}
description={t.settings.memory.description}
>
{isLoading ? (
<div className="text-muted-foreground text-sm">{t.common.loading}</div>
) : error ? (
<div>Error: {error.message}</div>
) : !memory ? (
<div className="text-muted-foreground text-sm">
{t.settings.memory.empty}
</div>
) : (
<div className="rounded-lg border p-4">
<Streamdown
className="size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0"
{...streamdownPlugins}
>
{memoryToMarkdown(memory, t)}
</Streamdown>
</div>
)}
</SettingsSection>
memory.user.workContext.summary.trim() === "" &&
memory.user.personalContext.summary.trim() === "" &&
memory.user.topOfMind.summary.trim() === "" &&
memory.history.recentMonths.summary.trim() === "" &&
memory.history.earlierContext.summary.trim() === "" &&
memory.history.longTermBackground.summary.trim() === ""
);
}
function truncateFactPreview(content: string, maxLength = 140) {
const normalized = content.replace(/\s+/g, " ").trim();
if (normalized.length <= maxLength) {
return normalized;
}
const ellipsis = "...";
if (maxLength <= ellipsis.length) {
return normalized.slice(0, maxLength);
}
return `${normalized.slice(0, maxLength - ellipsis.length)}${ellipsis}`;
}
function upperFirst(str: string) {
return str.charAt(0).toUpperCase() + str.slice(1);
}
export function MemorySettingsPage() {
const { t } = useI18n();
const { memory, isLoading, error } = useMemory();
const clearMemory = useClearMemory();
const deleteMemoryFact = useDeleteMemoryFact();
const [clearDialogOpen, setClearDialogOpen] = useState(false);
const [factToDelete, setFactToDelete] = useState<MemoryFact | null>(null);
const [query, setQuery] = useState("");
const [filter, setFilter] = useState<MemoryViewFilter>("all");
const deferredQuery = useDeferredValue(query);
const normalizedQuery = deferredQuery.trim().toLowerCase();
const clearAllLabel = t.settings.memory.clearAll ?? "Clear all memory";
const clearAllConfirmTitle =
t.settings.memory.clearAllConfirmTitle ?? "Clear all memory?";
const clearAllConfirmDescription =
t.settings.memory.clearAllConfirmDescription ??
"This will remove all saved summaries and facts. This action cannot be undone.";
const clearAllSuccess =
t.settings.memory.clearAllSuccess ?? "All memory cleared";
const factDeleteConfirmTitle =
t.settings.memory.factDeleteConfirmTitle ?? "Delete this fact?";
const factDeleteConfirmDescription =
t.settings.memory.factDeleteConfirmDescription ??
"This fact will be removed from memory immediately. This action cannot be undone.";
const factDeleteSuccess =
t.settings.memory.factDeleteSuccess ?? "Fact deleted";
const noFacts = t.settings.memory.noFacts ?? "No saved facts yet.";
const summaryReadOnly =
t.settings.memory.summaryReadOnly ??
"Summary sections are read-only for now. You can currently clear all memory or delete individual facts.";
const memoryFullyEmpty =
t.settings.memory.memoryFullyEmpty ?? "No memory saved yet.";
const factPreviewLabel =
t.settings.memory.factPreviewLabel ?? "Fact to delete";
const searchPlaceholder =
t.settings.memory.searchPlaceholder ?? "Search memory";
const filterAll = t.settings.memory.filterAll ?? "All";
const filterFacts = t.settings.memory.filterFacts ?? "Facts";
const filterSummaries = t.settings.memory.filterSummaries ?? "Summaries";
const noMatches =
t.settings.memory.noMatches ?? "No matching memory found";
const sectionGroups = memory ? buildMemorySectionGroups(memory, t) : [];
const filteredSectionGroups = sectionGroups
.map((group) => ({
...group,
sections: group.sections.filter((section) =>
normalizedQuery
? `${section.title} ${section.summary}`
.toLowerCase()
.includes(normalizedQuery)
: true,
),
}))
.filter((group) => group.sections.length > 0);
const filteredFacts = memory
? memory.facts.filter((fact) =>
normalizedQuery
? `${fact.content} ${fact.category}`
.toLowerCase()
.includes(normalizedQuery)
: true,
)
: [];
const showSummaries = filter !== "facts";
const showFacts = filter !== "summaries";
const shouldRenderSummariesBlock =
showSummaries && (filteredSectionGroups.length > 0 || !normalizedQuery);
const shouldRenderFactsBlock =
showFacts &&
(filteredFacts.length > 0 || !normalizedQuery || filter === "facts");
const hasMatchingVisibleContent =
!memory ||
(showSummaries && filteredSectionGroups.length > 0) ||
(showFacts && filteredFacts.length > 0);
async function handleClearMemory() {
try {
await clearMemory.mutateAsync();
toast.success(clearAllSuccess);
setClearDialogOpen(false);
} catch (err) {
toast.error(err instanceof Error ? err.message : String(err));
}
}
async function handleDeleteFact() {
if (!factToDelete) return;
try {
await deleteMemoryFact.mutateAsync(factToDelete.id);
toast.success(factDeleteSuccess);
setFactToDelete(null);
} catch (err) {
toast.error(err instanceof Error ? err.message : String(err));
}
}
return (
<>
<SettingsSection
title={t.settings.memory.title}
description={t.settings.memory.description}
>
{isLoading ? (
<div className="text-muted-foreground text-sm">{t.common.loading}</div>
) : error ? (
<div>Error: {error.message}</div>
) : !memory ? (
<div className="text-muted-foreground text-sm">
{t.settings.memory.empty}
</div>
) : (
<div className="space-y-4">
{isMemorySummaryEmpty(memory) && memory.facts.length === 0 ? (
<div className="text-muted-foreground rounded-lg border border-dashed p-4 text-sm">
{memoryFullyEmpty}
</div>
) : null}
<div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
<div className="flex flex-1 flex-col gap-3 sm:flex-row sm:items-center">
<Input
value={query}
onChange={(event) => setQuery(event.target.value)}
placeholder={searchPlaceholder}
className="sm:max-w-xs"
/>
<ToggleGroup
type="single"
value={filter}
onValueChange={(value) => {
if (value) setFilter(value as MemoryViewFilter);
}}
variant="outline"
>
<ToggleGroupItem value="all">{filterAll}</ToggleGroupItem>
<ToggleGroupItem value="facts">{filterFacts}</ToggleGroupItem>
<ToggleGroupItem value="summaries">
{filterSummaries}
</ToggleGroupItem>
</ToggleGroup>
</div>
<Button
variant="destructive"
onClick={() => setClearDialogOpen(true)}
disabled={clearMemory.isPending}
>
{clearMemory.isPending ? t.common.loading : clearAllLabel}
</Button>
</div>
{!hasMatchingVisibleContent && normalizedQuery ? (
<div className="text-muted-foreground rounded-lg border border-dashed p-4 text-sm">
{noMatches}
</div>
) : null}
{shouldRenderSummariesBlock ? (
<div className="rounded-lg border p-4">
<div className="text-muted-foreground mb-4 text-sm">
{summaryReadOnly}
</div>
<Streamdown
className="size-full [&>*:first-child]:mt-0 [&>*:last-child]:mb-0"
{...streamdownPlugins}
>
{summariesToMarkdown(memory, filteredSectionGroups, t)}
</Streamdown>
</div>
) : null}
{shouldRenderFactsBlock ? (
<div className="rounded-lg border p-4">
<div className="mb-4">
<h3 className="text-base font-medium">
{t.settings.memory.markdown.facts}
</h3>
</div>
{filteredFacts.length === 0 ? (
<div className="text-muted-foreground text-sm">
{normalizedQuery ? noMatches : noFacts}
</div>
) : (
<div className="space-y-3">
{filteredFacts.map((fact) => {
const { key } = confidenceToLevelKey(fact.confidence);
const confidenceText =
t.settings.memory.markdown.table.confidenceLevel[key];
return (
<div
key={fact.id}
className="flex flex-col gap-3 rounded-md border p-3 sm:flex-row sm:items-start sm:justify-between"
>
<div className="min-w-0 space-y-2">
<div className="flex flex-wrap gap-x-4 gap-y-1 text-sm">
<span>
<span className="text-muted-foreground">
{t.settings.memory.markdown.table.category}:
</span>{" "}
{upperFirst(fact.category)}
</span>
<span>
<span className="text-muted-foreground">
{t.settings.memory.markdown.table.confidence}:
</span>{" "}
{confidenceText}
</span>
<span>
<span className="text-muted-foreground">
{t.settings.memory.markdown.table.createdAt}:
</span>{" "}
{formatTimeAgo(fact.createdAt)}
</span>
</div>
<p className="break-words text-sm">{fact.content}</p>
<Link
href={pathOfThread(fact.source)}
className="text-primary text-sm underline-offset-4 hover:underline"
>
{t.settings.memory.markdown.table.view}
</Link>
</div>
<Button
variant="ghost"
size="icon"
className="text-destructive hover:text-destructive shrink-0"
onClick={() => setFactToDelete(fact)}
disabled={deleteMemoryFact.isPending}
title={t.common.delete}
aria-label={t.common.delete}
>
<Trash2Icon className="h-4 w-4" />
</Button>
</div>
);
})}
</div>
)}
</div>
) : null}
</div>
)}
</SettingsSection>
<Dialog open={clearDialogOpen} onOpenChange={setClearDialogOpen}>
<DialogContent>
<DialogHeader>
<DialogTitle>{clearAllConfirmTitle}</DialogTitle>
<DialogDescription>
{clearAllConfirmDescription}
</DialogDescription>
</DialogHeader>
<DialogFooter>
<Button
variant="outline"
onClick={() => setClearDialogOpen(false)}
disabled={clearMemory.isPending}
>
{t.common.cancel}
</Button>
<Button
variant="destructive"
onClick={() => void handleClearMemory()}
disabled={clearMemory.isPending}
>
{clearMemory.isPending ? t.common.loading : clearAllLabel}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog
open={factToDelete !== null}
onOpenChange={(open) => {
if (!open) {
setFactToDelete(null);
}
}}
>
<DialogContent>
<DialogHeader>
<DialogTitle>{factDeleteConfirmTitle}</DialogTitle>
<DialogDescription>
{factDeleteConfirmDescription}
</DialogDescription>
</DialogHeader>
{factToDelete ? (
<div className="bg-muted rounded-md border p-3 text-sm">
<div className="text-muted-foreground mb-1 font-medium">
{factPreviewLabel}
</div>
<p className="break-words">
{truncateFactPreview(factToDelete.content)}
</p>
</div>
) : null}
<DialogFooter>
<Button
variant="outline"
onClick={() => setFactToDelete(null)}
disabled={deleteMemoryFact.isPending}
>
{t.common.cancel}
</Button>
<Button
variant="destructive"
onClick={() => void handleDeleteFact()}
disabled={deleteMemoryFact.isPending}
>
{deleteMemoryFact.isPending ? t.common.loading : t.common.delete}
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
);
}
+3 -2
View File
@@ -4,7 +4,6 @@ function getBaseOrigin() {
if (typeof window !== "undefined") {
return window.location.origin;
}
return undefined;
}
@@ -13,7 +12,9 @@ export function getBackendBaseURL() {
return new URL(
env.NEXT_PUBLIC_BACKEND_BASE_URL,
getBaseOrigin(),
).toString();
)
.toString()
.replace(/\/+$/, "");
} else {
return "";
}
+19
View File
@@ -311,6 +311,25 @@ export const enUS: Translations = {
"DeerFlow automatically learns from your conversations in the background. These memories help DeerFlow understand you better and deliver a more personalized experience.",
empty: "No memory data to display.",
rawJson: "Raw JSON",
clearAll: "Clear all memory",
clearAllConfirmTitle: "Clear all memory?",
clearAllConfirmDescription:
"This will remove all saved summaries and facts. This action cannot be undone.",
clearAllSuccess: "All memory cleared",
factDeleteConfirmTitle: "Delete this fact?",
factDeleteConfirmDescription:
"This fact will be removed from memory immediately. This action cannot be undone.",
factDeleteSuccess: "Fact deleted",
noFacts: "No saved facts yet.",
summaryReadOnly:
"Summary sections are read-only for now. You can currently clear all memory or delete individual facts.",
memoryFullyEmpty: "No memory saved yet.",
factPreviewLabel: "Fact to delete",
searchPlaceholder: "Search memory",
filterAll: "All",
filterFacts: "Facts",
filterSummaries: "Summaries",
noMatches: "No matching memory found.",
markdown: {
overview: "Overview",
userContext: "User context",
+17 -1
View File
@@ -247,7 +247,23 @@ export interface Translations {
description: string;
empty: string;
rawJson: string;
markdown: {
clearAll: string;
clearAllConfirmTitle: string;
clearAllConfirmDescription: string;
clearAllSuccess: string;
factDeleteConfirmTitle: string;
factDeleteConfirmDescription: string;
factDeleteSuccess: string;
noFacts: string;
summaryReadOnly: string;
memoryFullyEmpty: string;
factPreviewLabel: string;
searchPlaceholder: string;
filterAll: string;
filterFacts: string;
filterSummaries: string;
noMatches: string;
markdown: {
overview: string;
userContext: string;
work: string;
+18
View File
@@ -298,6 +298,24 @@ export const zhCN: Translations = {
"DeerFlow 会在后台不断从你的对话中自动学习。这些记忆能帮助 DeerFlow 更好地理解你,并提供更个性化的体验。",
empty: "暂无可展示的记忆数据。",
rawJson: "原始 JSON",
clearAll: "清空全部记忆",
clearAllConfirmTitle: "要清空全部记忆吗?",
clearAllConfirmDescription:
"这会删除所有已保存的摘要和事实。此操作无法撤销。",
clearAllSuccess: "已清空全部记忆",
factDeleteConfirmTitle: "要删除这条事实吗?",
factDeleteConfirmDescription:
"这条事实会立即从记忆中删除。此操作无法撤销。",
factDeleteSuccess: "事实已删除",
noFacts: "还没有保存的事实。",
summaryReadOnly: "摘要分区当前仍为只读。现在你可以清空全部记忆或删除单条事实。",
memoryFullyEmpty: "还没有保存任何记忆。",
factPreviewLabel: "即将删除的事实",
searchPlaceholder: "搜索记忆",
filterAll: "全部",
filterFacts: "事实",
filterSummaries: "摘要",
noMatches: "没有找到匹配的记忆。",
markdown: {
overview: "概览",
userContext: "用户上下文",
+34 -4
View File
@@ -2,8 +2,38 @@ import { getBackendBaseURL } from "../config";
import type { UserMemory } from "./types";
export async function loadMemory() {
const memory = await fetch(`${getBackendBaseURL()}/api/memory`);
const json = await memory.json();
return json as UserMemory;
async function readMemoryResponse(
response: Response,
fallbackMessage: string,
): Promise<UserMemory> {
if (!response.ok) {
const errorData = (await response.json().catch(() => ({}))) as {
detail?: string;
};
throw new Error(errorData.detail ?? `${fallbackMessage}: ${response.statusText}`);
}
return response.json() as Promise<UserMemory>;
}
export async function loadMemory(): Promise<UserMemory> {
const response = await fetch(`${getBackendBaseURL()}/api/memory`);
return readMemoryResponse(response, "Failed to fetch memory");
}
export async function clearMemory(): Promise<UserMemory> {
const response = await fetch(`${getBackendBaseURL()}/api/memory`, {
method: "DELETE",
});
return readMemoryResponse(response, "Failed to clear memory");
}
export async function deleteMemoryFact(factId: string): Promise<UserMemory> {
const response = await fetch(
`${getBackendBaseURL()}/api/memory/facts/${encodeURIComponent(factId)}`,
{
method: "DELETE",
},
);
return readMemoryResponse(response, "Failed to delete memory fact");
}
+25 -2
View File
@@ -1,6 +1,7 @@
import { useQuery } from "@tanstack/react-query";
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { loadMemory } from "./api";
import { clearMemory, deleteMemoryFact, loadMemory } from "./api";
import type { UserMemory } from "./types";
export function useMemory() {
const { data, isLoading, error } = useQuery({
@@ -9,3 +10,25 @@ export function useMemory() {
});
return { memory: data ?? null, isLoading, error };
}
export function useClearMemory() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: () => clearMemory(),
onSuccess: (memory) => {
queryClient.setQueryData<UserMemory>(["memory"], memory);
},
});
}
export function useDeleteMemoryFact() {
const queryClient = useQueryClient();
return useMutation({
mutationFn: (factId: string) => deleteMemoryFact(factId),
onSuccess: (memory) => {
queryClient.setQueryData<UserMemory>(["memory"], memory);
},
});
}