From 38ad2292d1e69d1ddd07bf6c9ec758fea66fb755 Mon Sep 17 00:00:00 2001 From: kristinhe Date: Wed, 4 Nov 2026 15:58:03 +0800 Subject: [PATCH] fix(memhub): clarify memory mode switch effects Co-authored-by: Cursor --- .../apps/web/src/MemoryModeSettings.test.tsx | 69 +++++++++++-------- memhub/apps/web/src/MemoryModeSettings.tsx | 12 ++-- .../i18n/src/gui-i18n-catalogs.test.ts | 5 +- memhub/packages/i18n/src/gui-i18n-mode.ts | 13 +++- .../src/memory-mode-operation.test.ts | 30 ++++++-- .../operations/src/memory-mode-operation.ts | 13 ++-- 6 files changed, 96 insertions(+), 46 deletions(-) diff --git a/memhub/apps/web/src/MemoryModeSettings.test.tsx b/memhub/apps/web/src/MemoryModeSettings.test.tsx index 4abd50a..7566a09 100644 --- a/memhub/apps/web/src/MemoryModeSettings.test.tsx +++ b/memhub/apps/web/src/MemoryModeSettings.test.tsx @@ -1,5 +1,6 @@ // @vitest-environment jsdom +import { legacyMemoryModePlan } from "@one-memory/operations"; import { cleanup, render, @@ -64,31 +65,9 @@ function prepare(mode: "lite" | "pro" | "ultra" = "pro") { running: [], }); vi.mocked(getMemoryProcessing).mockResolvedValue({} as never); - vi.mocked(planMemoryMode).mockImplementation(async (target) => ({ - current: mode, - target, - effective_target: target, - capabilities: { - content_plane: target === "lite" ? "raw" : "memory", - generation_required: target !== "lite", - graph_enabled: target === "ultra", - evolve_available: target === "ultra", - }, - effects: [ - ...(target === "lite" ? (["higher_mode_memories_hidden"] as const) : []), - ...(mode === "lite" && target !== "lite" - ? (["earlier_raw_not_extracted", "lite_search_stops"] as const) - : []), - ...(target === "ultra" && mode !== "ultra" - ? (["existing_memories_added_to_graph"] as const) - : []), - ...(mode === "ultra" && target !== "ultra" - ? (["evolve_stopped"] as const) - : []), - ], - warnings: [], - blockers: [], - })); + vi.mocked(planMemoryMode).mockImplementation(async (target) => + legacyMemoryModePlan(mode, target, { hasActiveModel: true }), + ); } async function chooseMode( @@ -227,6 +206,20 @@ it("uses the isolated legacy plan only for a route-level old-Core 404", async () expect(await screen.findByText(/已有记忆继续保留/)).toBeTruthy(); }); +it("keeps downgrade effects when an old Core falls back from ultra to pro", async () => { + prepare("ultra"); + vi.mocked(planMemoryMode).mockRejectedValue(new Error("404")); + vi.mocked(memoryModePlanUnsupported).mockReturnValue(true); + const user = userEvent.setup(); + render(); + + await chooseMode(user, /智能记忆(pro)/); + + const dialog = screen.getByRole("dialog"); + expect(within(dialog).getAllByText("已有数据")).toHaveLength(1); + expect(within(dialog).getByText("每天自动整合")).toBeTruthy(); +}); + it("explains model cost, search scope and the next step before switching to ultra", async () => { prepare("lite"); const user = userEvent.setup(); @@ -238,6 +231,8 @@ it("explains model cost, search scope and the next step before switching to ultr expect(screen.getByText("搜索范围")).toBeTruthy(); expect(screen.getByText("切换后")).toBeTruthy(); expect(screen.getByText(/已经处理完的旧对话不会补做/)).toBeTruthy(); + expect(screen.getByText("知识图谱")).toBeTruthy(); + expect(screen.getAllByText("已有数据")).toHaveLength(1); expect(screen.getByText(/切换本身不会立即整合/)).toBeTruthy(); }); @@ -248,9 +243,27 @@ it("distinguishes regular memories from higher-level Ultra data when switching t await chooseMode(user, /智能记忆(pro)/); - expect(screen.getByText(/普通沉淀记忆仍可使用/)).toBeTruthy(); - expect(screen.getByText(/更高层记忆和图谱数据继续保留/)).toBeTruthy(); - expect(screen.getByText(/每天自动整合会停止/)).toBeTruthy(); + const dialog = screen.getByRole("dialog"); + expect(within(dialog).getAllByText("已有数据")).toHaveLength(1); + expect(within(dialog).getByText(/普通沉淀记忆仍可使用/)).toBeTruthy(); + expect(within(dialog).getByText(/更高层记忆和图谱数据继续保留/)).toBeTruthy(); + expect(within(dialog).getByText("每天自动整合")).toBeTruthy(); + expect( + within(dialog).getByText("每天自动整合会停止;已生成的整合结果继续保留。"), + ).toBeTruthy(); +}); + +it("does not show automatic consolidation when switching from lite to pro", async () => { + prepare("lite"); + const user = userEvent.setup(); + render(); + + await chooseMode(user, /智能记忆(pro)/); + + const dialog = screen.getByRole("dialog"); + expect(within(dialog).getAllByText("已有数据")).toHaveLength(1); + expect(within(dialog).getByText(/已经处理完的旧对话不会补做/)).toBeTruthy(); + expect(within(dialog).queryByText("每天自动整合")).toBeNull(); }); it("focuses the Evolve settings after switching to ultra", async () => { diff --git a/memhub/apps/web/src/MemoryModeSettings.tsx b/memhub/apps/web/src/MemoryModeSettings.tsx index 452b361..c09634d 100644 --- a/memhub/apps/web/src/MemoryModeSettings.tsx +++ b/memhub/apps/web/src/MemoryModeSettings.tsx @@ -85,18 +85,20 @@ function planRows(plan: MemoryModePlan) { if (plan.effects.includes("higher_mode_memories_hidden")) rows.push({ label: "已有数据", - content: "已有记忆继续保留,但高级记忆在当前模式下不显示、不参与召回。", + content: + plan.effective_target === "pro" + ? "普通沉淀记忆仍可使用;深度记忆新增的更高层记忆和图谱数据继续保留,但在智能记忆下不显示、不参与召回。原始对话不会删除。" + : "已有记忆继续保留,但在基础记忆下不显示、不参与召回;原始对话不会删除。", }); if (plan.effects.includes("existing_memories_added_to_graph")) rows.push({ - label: "已有数据", + label: "知识图谱", content: "已有记忆会在后台加入知识图谱,期间仍可使用。", }); if (plan.effects.includes("evolve_stopped")) rows.push({ - label: "已有数据", - content: - "普通沉淀记忆仍可使用;更高层记忆和图谱数据继续保留,但不参与当前召回;每天自动整合会停止。", + label: "每天自动整合", + content: "每天自动整合会停止,已经生成的数据不会删除。", }); if (plan.capabilities.evolve_available) rows.push({ diff --git a/memhub/packages/i18n/src/gui-i18n-catalogs.test.ts b/memhub/packages/i18n/src/gui-i18n-catalogs.test.ts index 22d0a6e..e6399af 100644 --- a/memhub/packages/i18n/src/gui-i18n-catalogs.test.ts +++ b/memhub/packages/i18n/src/gui-i18n-catalogs.test.ts @@ -275,9 +275,10 @@ describe("Mode catalog", () => { "搜索沉淀记忆,并用知识图谱补充关联。", "搜索沉淀后的记忆。", "已经处理完的旧对话不会补做;原始对话全部保留。", - "已有记忆继续保留,但高级记忆在当前模式下不显示、不参与召回。", + "知识图谱", + "普通沉淀记忆仍可使用;深度记忆新增的更高层记忆和图谱数据继续保留,但在智能记忆下不显示、不参与召回。原始对话不会删除。", "已有记忆会在后台加入知识图谱,期间仍可使用。", - "普通沉淀记忆仍可使用;更高层记忆和图谱数据继续保留,但不参与当前召回;每天自动整合会停止。", + "每天自动整合会停止,已经生成的数据不会删除。", "记忆维护正在进行,完成后才能切换模式。", "模式切换影响暂时无法读取,请重试。", ] as const; diff --git a/memhub/packages/i18n/src/gui-i18n-mode.ts b/memhub/packages/i18n/src/gui-i18n-mode.ts index bc27a13..c31427b 100644 --- a/memhub/packages/i18n/src/gui-i18n-mode.ts +++ b/memhub/packages/i18n/src/gui-i18n-mode.ts @@ -47,7 +47,7 @@ const modeChineseMessages = { "以前生成的高级记忆会继续保留,但在 lite 下不会显示或参与记忆召回。": "高级记忆会保留,但在 lite 下不显示、不参与召回。", "每天自动整合会停止,已经生成的数据不会删除。": - "自动整合会停止,已有数据保留。", + "每天自动整合会停止;已生成的整合结果继续保留。", "已有高级记忆会在后台加入知识图谱,期间可以继续使用 MemHub。": "已有高级记忆将在后台加入知识图谱。", "知识图谱仍在补建,不会重复启动。": "知识图谱仍在补建,不会重复启动。", @@ -97,6 +97,7 @@ const modeChineseMessages = { 模型调用与费用: "模型调用与费用", 搜索范围: "搜索范围", 已有数据: "已有数据", + 知识图谱: "知识图谱", 切换后: "切换后", "之后只保存原始对话,不再生成新记忆。": "之后只保存原始对话,不再生成新记忆。", @@ -295,6 +296,7 @@ export const modeEnglishMessages: ModeMessages = { 模型调用与费用: "Model calls and cost", 搜索范围: "Search scope", 已有数据: "Existing data", + 知识图谱: "Knowledge graph", 切换后: "After switching", "之后只保存原始对话,不再生成新记忆。": "New conversations are saved as original text only; no new memories are created.", @@ -498,6 +500,7 @@ export const modeSpanishMessages: ModeMessages = { 模型调用与费用: "Llamadas al modelo y coste", 搜索范围: "Ámbito de búsqueda", 已有数据: "Datos existentes", + 知识图谱: "Grafo de conocimiento", 切换后: "Después del cambio", "之后只保存原始对话,不再生成新记忆。": "Las conversaciones nuevas se guardan solo como texto original; no se crean memorias nuevas.", @@ -693,6 +696,7 @@ export const modeTraditionalChineseMessages: ModeMessages = { 模型调用与费用: "模型呼叫與費用", 搜索范围: "搜尋範圍", 已有数据: "現有資料", + 知识图谱: "知識圖譜", 切换后: "切換後", "之后只保存原始对话,不再生成新记忆。": "之後只儲存原始對話,不再產生新記憶。", @@ -843,6 +847,7 @@ export const modeJapaneseMessages: ModeMessages = { 模型调用与费用: "モデル呼び出しと料金", 搜索范围: "検索範囲", 已有数据: "既存データ", + 知识图谱: "ナレッジグラフ", 切换后: "切り替え後", "之后只保存原始对话,不再生成新记忆。": "以降は元の会話のみを保存し、新しい記憶は生成しません。", @@ -1043,6 +1048,7 @@ export const modeGermanMessages: ModeMessages = { 模型调用与费用: "Modellaufrufe und Kosten", 搜索范围: "Suchbereich", 已有数据: "Vorhandene Daten", + 知识图谱: "Wissensgraph", 切换后: "Nach dem Wechsel", "之后只保存原始对话,不再生成新记忆。": "Neue Unterhaltungen werden nur noch im Original gespeichert; es werden keine neuen Erinnerungen erstellt.", @@ -1246,6 +1252,7 @@ export const modePortugueseMessages: ModeMessages = { 模型调用与费用: "Chamadas ao modelo e custos", 搜索范围: "Âmbito da pesquisa", 已有数据: "Dados existentes", + 知识图谱: "Grafo de conhecimento", 切换后: "Após a mudança", "之后只保存原始对话,不再生成新记忆。": "As novas conversas passam a ser guardadas apenas como texto original; não são criadas novas memórias.", @@ -1451,6 +1458,7 @@ export const modeFrenchMessages: ModeMessages = { 模型调用与费用: "Appels au modèle et coût", 搜索范围: "Périmètre de recherche", 已有数据: "Données existantes", + 知识图谱: "Graphe de connaissances", 切换后: "Après le changement", "之后只保存原始对话,不再生成新记忆。": "Les nouvelles conversations sont uniquement enregistrées dans leur forme originale ; aucune nouvelle mémoire n’est créée.", @@ -1651,6 +1659,7 @@ export const modeKoreanMessages: ModeMessages = { 模型调用与费用: "모델 호출 및 비용", 搜索范围: "검색 범위", 已有数据: "기존 데이터", + 知识图谱: "지식 그래프", 切换后: "전환 후", "之后只保存原始对话,不再生成新记忆。": "이후 새 대화는 원본만 저장되며 새 메모리는 생성되지 않습니다.", @@ -1850,6 +1859,7 @@ export const modeRussianMessages: ModeMessages = { 模型调用与费用: "Вызовы модели и расходы", 搜索范围: "Область поиска", 已有数据: "Существующие данные", + 知识图谱: "Граф знаний", 切换后: "После переключения", "之后只保存原始对话,不再生成新记忆。": "Новые диалоги будут сохраняться только в исходном виде; новые воспоминания создаваться не будут.", @@ -2052,6 +2062,7 @@ export const modeIndonesianMessages: ModeMessages = { 模型调用与费用: "Panggilan model dan biaya", 搜索范围: "Cakupan pencarian", 已有数据: "Data yang ada", + 知识图谱: "Graf pengetahuan", 切换后: "Setelah beralih", "之后只保存原始对话,不再生成新记忆。": "Percakapan baru hanya disimpan sebagai teks asli; tidak ada memori baru yang dibuat.", diff --git a/memhub/packages/operations/src/memory-mode-operation.test.ts b/memhub/packages/operations/src/memory-mode-operation.test.ts index a96e0ed..c3e6b52 100644 --- a/memhub/packages/operations/src/memory-mode-operation.test.ts +++ b/memhub/packages/operations/src/memory-mode-operation.test.ts @@ -32,7 +32,31 @@ describe("memory mode plan", () => { expect(parseMemoryModePlan({ ...valid, blockers: ["unknown"] })).toBeNull(); }); - it("isolates the old-Core matrix in one helper", () => { + it.each([ + ["lite", "pro", ["earlier_raw_not_extracted", "lite_search_stops"]], + [ + "lite", + "ultra", + [ + "earlier_raw_not_extracted", + "lite_search_stops", + "existing_memories_added_to_graph", + ], + ], + ["pro", "lite", ["higher_mode_memories_hidden"]], + ["pro", "ultra", ["existing_memories_added_to_graph"]], + ["ultra", "lite", ["higher_mode_memories_hidden", "evolve_stopped"]], + ["ultra", "pro", ["higher_mode_memories_hidden", "evolve_stopped"]], + ] as const)( + "keeps the old-Core %s to %s effects aligned with the plan route", + (current, target, effects) => { + expect( + legacyMemoryModePlan(current, target, { hasActiveModel: true }).effects, + ).toEqual(effects); + }, + ); + + it("keeps the old-Core model blocker and target capabilities", () => { expect( legacyMemoryModePlan("lite", "ultra", { hasActiveModel: false }), ).toMatchObject({ @@ -42,10 +66,6 @@ describe("memory mode plan", () => { graph_enabled: true, }, blockers: ["active_model_required"], - effects: expect.arrayContaining([ - "earlier_raw_not_extracted", - "existing_memories_added_to_graph", - ]), }); }); }); diff --git a/memhub/packages/operations/src/memory-mode-operation.ts b/memhub/packages/operations/src/memory-mode-operation.ts index b17da39..4b7042b 100644 --- a/memhub/packages/operations/src/memory-mode-operation.ts +++ b/memhub/packages/operations/src/memory-mode-operation.ts @@ -106,12 +106,15 @@ export function legacyMemoryModePlan( options: { hasActiveModel: boolean }, ): MemoryModePlan { const effects: MemoryModeChange["effects"] = []; - if (target === "lite") { - effects.push("higher_mode_memories_hidden"); - if (current === "ultra") effects.push("evolve_stopped"); - } else if (current === "lite") { + const rank: Record = { + lite: 0, + pro: 1, + ultra: 2, + }; + if (current !== target && current === "lite") effects.push("earlier_raw_not_extracted", "lite_search_stops"); - } + if (rank[target] < rank[current]) effects.push("higher_mode_memories_hidden"); + if (current !== target && current === "ultra") effects.push("evolve_stopped"); if (target === "ultra" && current !== "ultra") effects.push("existing_memories_added_to_graph"); return {