From a1f277e30e50ac2c537fc838265309506acdde9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Rub=C3=A9n=20Cuevas?= Date: Sun, 19 Apr 2026 03:06:30 -0400 Subject: [PATCH] fix(ui): stop unsupported wiki RPC probes during startup (#67905) * UI: gate wiki method probes by advertised methods * test(ui): cover legacy wiki method fallback --- ui/src/ui/controllers/dreaming.test.ts | 151 +++++++++++++++++++++++++ ui/src/ui/controllers/dreaming.ts | 23 +++- 2 files changed, 171 insertions(+), 3 deletions(-) diff --git a/ui/src/ui/controllers/dreaming.test.ts b/ui/src/ui/controllers/dreaming.test.ts index 4fa731a68c1..49010ecc64f 100644 --- a/ui/src/ui/controllers/dreaming.test.ts +++ b/ui/src/ui/controllers/dreaming.test.ts @@ -22,6 +22,7 @@ function createState(): { state: DreamingState; request: ReturnType { it("loads and normalizes wiki import insights", async () => { const { state, request } = createState(); + state.hello = { + type: "hello-ok", + protocol: 3, + features: { methods: ["wiki.importInsights"] }, + }; state.configSnapshot = { hash: "hash-1", config: { @@ -297,6 +303,40 @@ describe("dreaming controller", () => { expect(state.wikiImportInsightsLoading).toBe(false); }); + it("falls back to config gating for wiki import insights when methods are not advertised", async () => { + const { state, request } = createState(); + state.configSnapshot = { + hash: "hash-1", + config: { + plugins: { + entries: { + "memory-wiki": { + enabled: true, + }, + }, + }, + }, + }; + request.mockResolvedValue({ + sourceType: "chatgpt", + totalItems: 1, + totalClusters: 1, + clusters: [], + }); + + await loadWikiImportInsights(state); + + expect(request).toHaveBeenCalledWith("wiki.importInsights", {}); + expect(state.wikiImportInsights).toEqual( + expect.objectContaining({ + totalItems: 1, + totalClusters: 1, + }), + ); + expect(state.wikiImportInsightsError).toBeNull(); + expect(state.wikiImportInsightsLoading).toBe(false); + }); + it("skips wiki import insights when memory-wiki is not enabled", async () => { const { state, request } = createState(); state.configSnapshot = { @@ -321,8 +361,48 @@ describe("dreaming controller", () => { expect(state.wikiImportInsightsLoading).toBe(false); }); + it("skips wiki import insights when the gateway does not advertise the method", async () => { + const { state, request } = createState(); + state.hello = { + type: "hello-ok", + protocol: 3, + features: { methods: ["doctor.memory.status"] }, + }; + state.configSnapshot = { + hash: "hash-1", + config: { + plugins: { + entries: { + "memory-wiki": { + enabled: true, + }, + }, + }, + }, + }; + state.wikiImportInsights = { + sourceType: "chatgpt", + totalItems: 1, + totalClusters: 1, + clusters: [], + }; + state.wikiImportInsightsError = "unknown method: wiki.importInsights"; + + await loadWikiImportInsights(state); + + expect(request).not.toHaveBeenCalled(); + expect(state.wikiImportInsights).toBeNull(); + expect(state.wikiImportInsightsError).toBeNull(); + expect(state.wikiImportInsightsLoading).toBe(false); + }); + it("loads and normalizes the wiki memory palace", async () => { const { state, request } = createState(); + state.hello = { + type: "hello-ok", + protocol: 3, + features: { methods: ["wiki.palace"] }, + }; state.configSnapshot = { hash: "hash-1", config: { @@ -391,6 +471,41 @@ describe("dreaming controller", () => { expect(state.wikiMemoryPalaceLoading).toBe(false); }); + it("falls back to config gating for wiki memory palace when methods are not advertised", async () => { + const { state, request } = createState(); + state.configSnapshot = { + hash: "hash-1", + config: { + plugins: { + entries: { + "memory-wiki": { + enabled: true, + }, + }, + }, + }, + }; + request.mockResolvedValue({ + totalItems: 1, + totalClaims: 2, + totalQuestions: 0, + totalContradictions: 0, + clusters: [], + }); + + await loadWikiMemoryPalace(state); + + expect(request).toHaveBeenCalledWith("wiki.palace", {}); + expect(state.wikiMemoryPalace).toEqual( + expect.objectContaining({ + totalItems: 1, + totalClaims: 2, + }), + ); + expect(state.wikiMemoryPalaceError).toBeNull(); + expect(state.wikiMemoryPalaceLoading).toBe(false); + }); + it("skips wiki memory palace when memory-wiki is not enabled", async () => { const { state, request } = createState(); state.configSnapshot = { @@ -416,6 +531,42 @@ describe("dreaming controller", () => { expect(state.wikiMemoryPalaceLoading).toBe(false); }); + it("skips wiki memory palace when the gateway does not advertise the method", async () => { + const { state, request } = createState(); + state.hello = { + type: "hello-ok", + protocol: 3, + features: { methods: ["doctor.memory.status"] }, + }; + state.configSnapshot = { + hash: "hash-1", + config: { + plugins: { + entries: { + "memory-wiki": { + enabled: true, + }, + }, + }, + }, + }; + state.wikiMemoryPalace = { + totalItems: 1, + totalClaims: 1, + totalQuestions: 0, + totalContradictions: 0, + clusters: [], + }; + state.wikiMemoryPalaceError = "unknown method: wiki.palace"; + + await loadWikiMemoryPalace(state); + + expect(request).not.toHaveBeenCalled(); + expect(state.wikiMemoryPalace).toBeNull(); + expect(state.wikiMemoryPalaceError).toBeNull(); + expect(state.wikiMemoryPalaceLoading).toBe(false); + }); + it("patches config to update global dreaming enablement", async () => { const { state, request } = createState(); state.configSnapshot = { diff --git a/ui/src/ui/controllers/dreaming.ts b/ui/src/ui/controllers/dreaming.ts index a20098ea9e8..099cb67ba18 100644 --- a/ui/src/ui/controllers/dreaming.ts +++ b/ui/src/ui/controllers/dreaming.ts @@ -1,4 +1,4 @@ -import type { GatewayBrowserClient } from "../gateway.ts"; +import type { GatewayBrowserClient, GatewayHelloOk } from "../gateway.ts"; import { isPluginEnabledInConfigSnapshot } from "../plugin-activation.ts"; import type { ConfigSnapshot } from "../types.ts"; @@ -201,6 +201,7 @@ type WikiMemoryPalacePayload = { export type DreamingState = { client: GatewayBrowserClient | null; connected: boolean; + hello: GatewayHelloOk | null; configSnapshot: ConfigSnapshot | null; applySessionKey: string; dreamingStatusLoading: boolean; @@ -236,6 +237,22 @@ function isMemoryWikiEnabled(state: DreamingState): boolean { }); } +function hasGatewayMethod(state: DreamingState, method: string): boolean | null { + const methods = state.hello?.features?.methods; + if (!Array.isArray(methods)) { + return null; + } + return methods.includes(method); +} + +function canCallMemoryWikiMethod(state: DreamingState, method: string): boolean { + const available = hasGatewayMethod(state, method); + if (available !== null) { + return available; + } + return isMemoryWikiEnabled(state); +} + function buildDreamDiaryActionSuccessMessage( method: | "doctor.memory.backfillDreamDiary" @@ -748,7 +765,7 @@ export async function loadWikiImportInsights(state: DreamingState): Promise if (!state.client || !state.connected || state.wikiMemoryPalaceLoading) { return; } - if (!isMemoryWikiEnabled(state)) { + if (!canCallMemoryWikiMethod(state, "wiki.palace")) { state.wikiMemoryPalace = null; state.wikiMemoryPalaceError = null; return;