diff --git a/ui/src/ui/controllers/dreaming.test.ts b/ui/src/ui/controllers/dreaming.test.ts index a2dea66e854..5ac5a901de3 100644 --- a/ui/src/ui/controllers/dreaming.test.ts +++ b/ui/src/ui/controllers/dreaming.test.ts @@ -193,6 +193,48 @@ describe("dreaming controller", () => { }); }); + it("blocks dreaming patch when selected plugin config rejects unknown keys", async () => { + const { state, request } = createState(); + state.configSnapshot = { + hash: "hash-1", + config: { + plugins: { + slots: { + memory: "memory-lancedb", + }, + }, + }, + }; + request.mockImplementation(async (method: string) => { + if (method === "config.schema.lookup") { + return { + path: "plugins.entries.memory-lancedb.config", + schema: { + type: "object", + additionalProperties: false, + }, + children: [ + { key: "retentionDays", path: "plugins.entries.memory-lancedb.config.retentionDays" }, + ], + }; + } + if (method === "config.patch") { + return { ok: true }; + } + return {}; + }); + + const ok = await updateDreamingEnabled(state, true); + + expect(ok).toBe(false); + expect(request).toHaveBeenCalledWith("config.schema.lookup", { + path: "plugins.entries.memory-lancedb.config", + }); + expect(request).not.toHaveBeenCalledWith("config.patch", expect.anything()); + expect(state.dreamingStatusError).toContain("memory-lancedb"); + expect(state.dreamingStatusError).toContain("does not support dreaming settings"); + }); + it("reads dreaming enabled state from the selected memory slot plugin", () => { expect( resolveConfiguredDreaming({ diff --git a/ui/src/ui/controllers/dreaming.ts b/ui/src/ui/controllers/dreaming.ts index 6060555fe19..5f05cc4cb7d 100644 --- a/ui/src/ui/controllers/dreaming.ts +++ b/ui/src/ui/controllers/dreaming.ts @@ -309,11 +309,65 @@ async function writeDreamingPatch( } } +function lookupIncludesDreamingProperty(value: unknown): boolean { + const lookup = asRecord(value); + const children = Array.isArray(lookup?.children) ? lookup.children : []; + for (const child of children) { + const childRecord = asRecord(child); + if (normalizeTrimmedString(childRecord?.key) === "dreaming") { + return true; + } + } + return false; +} + +function lookupDisallowsUnknownProperties(value: unknown): boolean { + const lookup = asRecord(value); + const schema = asRecord(lookup?.schema); + return schema?.additionalProperties === false; +} + +async function ensureDreamingPathSupported( + state: DreamingState, + pluginId: string, +): Promise { + if (!state.client || !state.connected) { + return true; + } + try { + const lookup = await state.client.request("config.schema.lookup", { + path: `plugins.entries.${pluginId}.config`, + }); + if (lookupIncludesDreamingProperty(lookup)) { + return true; + } + if (lookupDisallowsUnknownProperties(lookup)) { + const message = `Selected memory plugin "${pluginId}" does not support dreaming settings.`; + state.dreamingStatusError = message; + state.lastError = message; + return false; + } + } catch { + return true; + } + return true; +} + export async function updateDreamingEnabled( state: DreamingState, enabled: boolean, ): Promise { + if (state.dreamingModeSaving) { + return false; + } + if (!state.configSnapshot?.hash) { + state.dreamingStatusError = "Config hash missing; refresh and retry."; + return false; + } const { pluginId } = resolveConfiguredDreaming(asRecord(state.configSnapshot?.config) ?? null); + if (!(await ensureDreamingPathSupported(state, pluginId))) { + return false; + } const ok = await writeDreamingPatch(state, { plugins: { entries: {