From 01a9e1d398fd12970e936335811f13ee913e29c1 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 18 Jul 2026 12:16:49 +0100 Subject: [PATCH] feat(memory): default cross-conversation recall for personal installs (#110597) * feat(memory): default private recall for personal installs * fix(memory): repair remember-across CI checks --- docs/concepts/active-memory.md | 20 ++++--- docs/reference/memory-config.md | 11 ++-- extensions/active-memory/index.test.ts | 19 +++++- extensions/active-memory/session-policy.ts | 19 ++---- .../src/host/backend-config.test.ts | 3 +- .../src/host/backend-config.ts | 3 +- .../memory-host-sdk/src/host/config-utils.ts | 33 ++++++++++- scripts/plugin-sdk-surface-report.mjs | 6 +- src/agents/memory-search.test.ts | 58 ++++++++++++++++++- src/agents/memory-search.ts | 4 +- src/commands/doctor-memory-search.test.ts | 12 ++-- src/commands/doctor-memory-search.ts | 23 +++----- src/commands/doctor-state-integrity.test.ts | 5 ++ src/config/schema.help.models.ts | 2 +- src/memory-host-sdk/host/config-utils.ts | 2 + .../memory-core-host-runtime-core.ts | 1 + 16 files changed, 162 insertions(+), 59 deletions(-) create mode 100644 src/memory-host-sdk/host/config-utils.ts diff --git a/docs/concepts/active-memory.md b/docs/concepts/active-memory.md index bd45f94554a..57d3f01ec30 100644 --- a/docs/concepts/active-memory.md +++ b/docs/concepts/active-memory.md @@ -35,11 +35,13 @@ private conversations with one per-agent setting: } ``` -The setting is off by default. When enabled, OpenClaw indexes that agent's -session transcripts and runs an Active Memory retrieval pass before eligible -private replies. The pass can read relevant transcript excerpts from the same -agent's other private conversations. It excludes the conversation already -being answered. +The setting defaults on for personal installs: global `session.dmScope` must be +unset or `"main"`, and no binding may override `session.dmScope`. Any configured +DM isolation defaults it off. An explicit `true` or `false` always wins. When +enabled, OpenClaw indexes that agent's session transcripts and runs an Active +Memory retrieval pass before eligible private replies. The pass can read +relevant transcript excerpts from the same agent's other private conversations. +It excludes the conversation already being answered. The privacy boundary is fixed: @@ -156,8 +158,8 @@ personalization would be surprising. Active Memory has two activation paths: 1. **Remember across conversations** automatically targets agents whose - `memorySearch.rememberAcrossConversations` setting is enabled, but only for - private direct or persistent explicit UI conversations. + effective `memorySearch.rememberAcrossConversations` setting is enabled, but + only for private direct or persistent explicit UI conversations. 2. **Advanced Active Memory** targets agent IDs listed in `plugins.entries.active-memory.config.agents` and applies the plugin's chat type and chat ID controls. @@ -727,8 +729,8 @@ while warm-up finishes. If active memory is not showing up where you expect: 1. Confirm the plugin is enabled under `plugins.entries.active-memory.enabled`. -2. For Remember across conversations, confirm the agent's - `memorySearch.rememberAcrossConversations` setting is `true`, run +2. For Remember across conversations, confirm the agent's effective + `memorySearch.rememberAcrossConversations` setting is enabled, run `openclaw doctor` to verify the current memory provider supports protected transcript recall, and confirm `config.toolsAllow` includes `memory_search` when explicitly configured. For advanced Active Memory, confirm the agent ID diff --git a/docs/reference/memory-config.md b/docs/reference/memory-config.md index 33f6e447eba..672b883784b 100644 --- a/docs/reference/memory-config.md +++ b/docs/reference/memory-config.md @@ -44,9 +44,9 @@ transcript persistence, and safe rollout guidance. ## Remember across conversations -| Key | Type | Default | Description | -| ----------------------------- | --------- | ------- | ------------------------------------------------------------------------------ | -| `rememberAcrossConversations` | `boolean` | `false` | Use relevant context from this agent's other recognized private conversations. | +| Key | Type | Default | Description | +| ----------------------------- | --------- | ---------------------------------------------------------- | ------------------------------------------------------------------------------ | +| `rememberAcrossConversations` | `boolean` | On for personal installs; off with configured DM isolation | Use relevant context from this agent's other recognized private conversations. | Configure it per agent when only a trusted personal agent should use cross-conversation transcript recall: @@ -67,7 +67,10 @@ cross-conversation transcript recall: ``` The value follows normal `agents.defaults.memorySearch` inheritance with a -per-agent override. Enabling it implies session transcript indexing and +per-agent override. When unset, it defaults on only if global +`session.dmScope` is unset or `"main"` and no binding has a `session.dmScope` +override. Any configured DM isolation defaults it off. An explicit `true` or +`false` always wins. Enabling it implies session transcript indexing and adds `sessions` to the agent's resolved memory sources. With QMD, it also enables that agent's session export; no separate `memory.qmd.sessions.enabled` setting is required for this mode. diff --git a/extensions/active-memory/index.test.ts b/extensions/active-memory/index.test.ts index 0fa561aa96a..c9b3489a3e6 100644 --- a/extensions/active-memory/index.test.ts +++ b/extensions/active-memory/index.test.ts @@ -15,6 +15,7 @@ import { appendSessionTranscriptMessageByIdentity } from "openclaw/plugin-sdk/se import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest"; import { applyCliRuntimeRecallTimeoutDefault } from "./config.js"; import plugin, { testing } from "./index.js"; +import { hasRememberAcrossConversationsAgent } from "./session-policy.js"; // Match only lone surrogates so valid supplementary-plane characters remain allowed. const UNPAIRED_SURROGATE_RE = @@ -505,6 +506,7 @@ describe("active-memory plugin", () => { }).clear(); runEmbeddedAgent.mockReset(); configFile = { + session: { dmScope: "per-peer" }, plugins: { entries: { "active-memory": { @@ -521,6 +523,7 @@ describe("active-memory plugin", () => { logging: true, }); api.config = { + session: { dmScope: "per-peer" }, agents: { defaults: { model: { @@ -628,6 +631,19 @@ describe("active-memory plugin", () => { expect(hookOptions.before_prompt_build?.timeoutMs).toBe(153_000); }); + it("does not synthesize a main agent when every configured agent opts out", () => { + expect( + hasRememberAcrossConversationsAgent({ + agents: { + list: [ + { id: "personal", memorySearch: { rememberAcrossConversations: false } }, + { id: "support", memorySearch: { rememberAcrossConversations: false } }, + ], + }, + }), + ).toBe(false); + }); + it("keeps the outer hook timeout at the live-config ceiling", () => { api.pluginConfig = { agents: ["main"], @@ -1016,14 +1032,13 @@ describe("active-memory plugin", () => { } }); - it("runs product recall without an explicit Active Memory config entry", async () => { + it("runs product recall by default for a personal install without Active Memory config", async () => { configFile = { agents: { list: [ { id: "personal", model: { primary: "github-copilot/gpt-5.4-mini" }, - memorySearch: { rememberAcrossConversations: true }, }, ], }, diff --git a/extensions/active-memory/session-policy.ts b/extensions/active-memory/session-policy.ts index 087644dc33e..02cd296eb09 100644 --- a/extensions/active-memory/session-policy.ts +++ b/extensions/active-memory/session-policy.ts @@ -1,5 +1,6 @@ import crypto from "node:crypto"; import type { OpenClawConfig } from "openclaw/plugin-sdk/config-contracts"; +import { resolveRememberAcrossConversations } from "openclaw/plugin-sdk/memory-core-host-runtime-core"; import { normalizePluginsConfig, resolvePluginConfigObject, @@ -127,23 +128,13 @@ function isActiveMemoryPluginEnabled(cfg: OpenClawConfig): boolean { } function hasRememberAcrossConversationsAgent(cfg: OpenClawConfig): boolean { - return ( - cfg.agents?.defaults?.memorySearch?.rememberAcrossConversations === true || - cfg.agents?.list?.some((agent) => agent.memorySearch?.rememberAcrossConversations === true) === - true - ); + const configuredAgentIds = cfg.agents?.list?.map((agent) => agent.id) ?? []; + const agentIds = configuredAgentIds.length > 0 ? configuredAgentIds : ["main"]; + return agentIds.some((agentId) => resolveRememberAcrossConversations(cfg, agentId)); } function shouldRememberAcrossConversations(cfg: OpenClawConfig, agentId: string): boolean { - const normalizedAgentId = agentId.trim().toLowerCase(); - const agent = cfg.agents?.list?.find( - (candidate) => candidate.id?.trim().toLowerCase() === normalizedAgentId, - ); - return ( - agent?.memorySearch?.rememberAcrossConversations ?? - cfg.agents?.defaults?.memorySearch?.rememberAcrossConversations ?? - false - ); + return resolveRememberAcrossConversations(cfg, agentId); } function updateActiveMemoryGlobalEnabledInConfig( diff --git a/packages/memory-host-sdk/src/host/backend-config.test.ts b/packages/memory-host-sdk/src/host/backend-config.test.ts index b0e11a9183e..e6f5927cf5f 100644 --- a/packages/memory-host-sdk/src/host/backend-config.test.ts +++ b/packages/memory-host-sdk/src/host/backend-config.test.ts @@ -132,9 +132,10 @@ describe("resolveMemoryBackendConfig", () => { expect(requireQmdCollection(resolved, "memory-root-main").pattern).toBe("MEMORY.md"); }); - it("keeps QMD session export off by default", () => { + it("keeps QMD session export off by default when DM isolation is configured", () => { const cfg = { agents: { defaults: { workspace: "/tmp/memory-test" } }, + session: { dmScope: "per-peer" }, memory: { backend: "qmd", qmd: {} }, } as OpenClawConfig; diff --git a/packages/memory-host-sdk/src/host/backend-config.ts b/packages/memory-host-sdk/src/host/backend-config.ts index a09f446d1e2..c9f0699ac9e 100644 --- a/packages/memory-host-sdk/src/host/backend-config.ts +++ b/packages/memory-host-sdk/src/host/backend-config.ts @@ -495,8 +495,7 @@ export function resolveMemoryBackendConfig(params: { sessions: resolveSessionConfig( { ...qmdCfg?.sessions, - enabled: - qmdCfg?.sessions?.enabled === true || memorySearch?.rememberAcrossConversations === true, + enabled: qmdCfg?.sessions?.enabled === true || memorySearch?.rememberAcrossConversations, }, workspaceDir, // Remember-only export is search-only for trusted recall; ordinary diff --git a/packages/memory-host-sdk/src/host/config-utils.ts b/packages/memory-host-sdk/src/host/config-utils.ts index 1c4112d28b1..f697835df80 100644 --- a/packages/memory-host-sdk/src/host/config-utils.ts +++ b/packages/memory-host-sdk/src/host/config-utils.ts @@ -18,6 +18,7 @@ export { splitShellArgs } from "./openclaw-runtime-io.js"; /** Chat shape used by memory send-policy matching. */ type ChatType = "direct" | "group" | "channel"; +type DmScope = "main" | "per-peer" | "per-channel-peer" | "per-account-channel-peer"; /** Memory backend selected by user config. */ export type MemoryBackend = "builtin" | "qmd"; /** Citation injection behavior for memory search results. */ @@ -156,6 +157,10 @@ export type OpenClawConfig = { }; list?: AgentConfig[]; }; + session?: { + dmScope?: DmScope; + }; + bindings?: unknown[]; memory?: MemoryConfig; models?: { providers?: Record< @@ -169,6 +174,31 @@ export type OpenClawConfig = { }; }; +export function resolveRememberAcrossConversations(cfg: OpenClawConfig, agentId: string): boolean { + const defaults = cfg.agents?.defaults?.memorySearch; + const overrides = resolveAgentConfig(cfg, agentId)?.memorySearch; + const explicit = overrides?.rememberAcrossConversations ?? defaults?.rememberAcrossConversations; + if (explicit !== undefined) { + return explicit; + } + // Recall is per-agent/private-shaped, not per-sender. Any DM isolation signals a + // multi-user install, where silently recalling across senders would leak context. + return ( + (cfg.session?.dmScope === undefined || cfg.session.dmScope === "main") && + !cfg.bindings?.some((binding) => { + if (!binding || typeof binding !== "object") { + return false; + } + const session = (binding as { session?: unknown }).session; + return ( + Boolean(session) && + typeof session === "object" && + (session as { dmScope?: unknown }).dmScope !== undefined + ); + }) + ); +} + /** Root memory filename used in agent workspaces. */ export const MEMORY_HOST_ROOT_FILENAME = "MEMORY.md"; @@ -341,8 +371,7 @@ export function resolveMemoryHostSearchPathConfig( ]); return { enabled, - rememberAcrossConversations: - overrides?.rememberAcrossConversations ?? defaults?.rememberAcrossConversations ?? false, + rememberAcrossConversations: resolveRememberAcrossConversations(cfg, agentId), extraPaths: uniqueStrings(rawPaths), }; } diff --git a/scripts/plugin-sdk-surface-report.mjs b/scripts/plugin-sdk-surface-report.mjs index 16af5621b8f..e4448e93b8e 100644 --- a/scripts/plugin-sdk-surface-report.mjs +++ b/scripts/plugin-sdk-surface-report.mjs @@ -266,9 +266,10 @@ export function readPluginSdkSurfaceBudgets(env = process.env) { // +3: question-gateway-runtime resolver plus request/result types. // +1: async memory prompt preparation registration. // +1: canonical memory host event normalization for SQLite storage. + // +1: centralized remember-across-conversations effective-default resolver. // +4: gateway-backed harness question runner, claim/cancel helpers, and caller type. // Harvest: internal question runtime exports -2. - 8156, + 8157, env, ), publicFunctionExports: readPluginSdkSurfaceBudgetEnv( @@ -305,9 +306,10 @@ export function readPluginSdkSurfaceBudgets(env = process.env) { // +1: question-gateway-runtime resolver. // +1: async memory prompt preparation registration. // +1: canonical memory host event normalization for SQLite storage. + // +1: centralized remember-across-conversations effective-default resolver. // +3: gateway-backed harness question runner and claim/cancel helpers. // Harvest: internal question runtime callable -1. - 4538, + 4539, env, ), publicDeprecatedExports: readPluginSdkSurfaceBudgetEnv( diff --git a/src/agents/memory-search.test.ts b/src/agents/memory-search.test.ts index fe2a3fd549d..5b0150c59ac 100644 --- a/src/agents/memory-search.test.ts +++ b/src/agents/memory-search.test.ts @@ -1,6 +1,7 @@ // Verifies memory-search config resolution across providers, sync, and batching. import { afterEach, beforeEach, describe, expect, it } from "vitest"; import type { OpenClawConfig } from "../config/config.js"; +import { resolveRememberAcrossConversations } from "../memory-host-sdk/host/config-utils.js"; import { clearEmbeddingProviders, listRegisteredEmbeddingProviders, @@ -234,9 +235,63 @@ describe("memory search config", () => { expect(resolved).toBeNull(); }); - it("keeps cross-conversation recall off by default", () => { + it.each([ + { name: "unset with main scope", cfg: {}, expected: true }, + { + name: "unset with per-channel-peer scope", + cfg: { session: { dmScope: "per-channel-peer" } }, + expected: false, + }, + { + name: "unset with main scope and a binding override", + cfg: { + session: { dmScope: "main" }, + bindings: [ + { + agentId: "main", + match: { channel: "telegram" }, + session: { dmScope: "per-peer" }, + }, + ], + }, + expected: false, + }, + { + name: "explicit false with main scope", + cfg: { + session: { dmScope: "main" }, + agents: { defaults: { memorySearch: { rememberAcrossConversations: false } } }, + }, + expected: false, + }, + { + name: "explicit true with per-peer scope", + cfg: { + session: { dmScope: "per-peer" }, + agents: { defaults: { memorySearch: { rememberAcrossConversations: true } } }, + }, + expected: true, + }, + ])("resolves remember-across-conversations for $name", ({ cfg, expected }) => { + expect(resolveRememberAcrossConversations(asConfig(cfg as OpenClawConfig), "main")).toBe( + expected, + ); + }); + + it("enables cross-conversation recall by default for a personal install", () => { const resolved = resolveMemorySearchConfig(asConfig({}), "main"); + expect(resolved?.rememberAcrossConversations).toBe(true); + expect(resolved?.experimental.sessionMemory).toBe(true); + expect(resolved?.sources).toEqual(["memory", "sessions"]); + }); + + it("keeps cross-conversation recall off by default for isolated DMs", () => { + const resolved = resolveMemorySearchConfig( + asConfig({ session: { dmScope: "per-channel-peer" } }), + "main", + ); + expect(resolved?.rememberAcrossConversations).toBe(false); expect(resolved?.experimental.sessionMemory).toBe(false); expect(resolved?.sources).toEqual(["memory"]); @@ -887,6 +942,7 @@ describe("memory search config", () => { id: "main", default: true, memorySearch: { + rememberAcrossConversations: false, experimental: { sessionMemory: false }, }, }, diff --git a/src/agents/memory-search.ts b/src/agents/memory-search.ts index f4b380f0b62..d2a7821fd9e 100644 --- a/src/agents/memory-search.ts +++ b/src/agents/memory-search.ts @@ -15,6 +15,7 @@ import { } from "@openclaw/normalization-core/string-normalization"; import type { OpenClawConfig, MemorySearchConfig } from "../config/config.js"; import type { SecretInput } from "../config/types.secrets.js"; +import { resolveRememberAcrossConversations } from "../memory-host-sdk/host/config-utils.js"; import { isMemoryMultimodalEnabled, normalizeMemoryMultimodalSettings, @@ -224,8 +225,7 @@ function mergeConfig( agentId: string, ): ResolvedMemorySearchConfig { const enabled = overrides?.enabled ?? defaults?.enabled ?? true; - const rememberAcrossConversations = - overrides?.rememberAcrossConversations ?? defaults?.rememberAcrossConversations ?? false; + const rememberAcrossConversations = resolveRememberAcrossConversations(cfg, agentId); const configuredSessionMemory = overrides?.experimental?.sessionMemory ?? defaults?.experimental?.sessionMemory ?? false; const sessionMemory = rememberAcrossConversations || configuredSessionMemory; diff --git a/src/commands/doctor-memory-search.test.ts b/src/commands/doctor-memory-search.test.ts index d847a4dfef5..6db2f1da252 100644 --- a/src/commands/doctor-memory-search.test.ts +++ b/src/commands/doctor-memory-search.test.ts @@ -483,6 +483,7 @@ describe("noteMemorySearchHealth", () => { remote: {}, }); const cfgWithLancedb = { + session: { dmScope: "per-peer" }, plugins: { slots: { memory: "memory-lancedb" }, entries: { "memory-lancedb": { enabled: true, config: { dbPath: ".openclaw/memory" } } }, @@ -504,6 +505,7 @@ describe("noteMemorySearchHealth", () => { remote: {}, }); const cfgWithSlotOnly = { + session: { dmScope: "per-peer" }, plugins: { slots: { memory: "memory-lancedb" } }, } as unknown as OpenClawConfig; @@ -521,6 +523,7 @@ describe("noteMemorySearchHealth", () => { remote: {}, }); const cfgWithDisabledLancedb = { + session: { dmScope: "per-peer" }, plugins: { slots: { memory: "memory-lancedb" }, entries: { "memory-lancedb": { enabled: false } }, @@ -541,6 +544,7 @@ describe("noteMemorySearchHealth", () => { remote: {}, }); const cfgWithPlaceholderEntry = { + session: { dmScope: "per-peer" }, plugins: { slots: { memory: "memory-lancedb" }, entries: { "memory-lancedb": {} }, @@ -704,7 +708,7 @@ describe("noteMemorySearchHealth", () => { await noteMemorySearchHealth(qmdCfg, { skipQmdBinaryProbe: true }); expect(firstNoteMessage()).toBe( - 'Remember across conversations is enabled for agent "personal", but the current memory provider does not support protected private transcript recall. Turn off Remember across conversations or use that provider\'s own recall path; advanced Active Memory can still use its recall tools.', + 'Remember across conversations is effectively enabled for agent "personal", but the current memory provider does not support protected private transcript recall. Set memorySearch.rememberAcrossConversations to false or use that provider\'s own recall path; advanced Active Memory can still use its recall tools.', ); }, ); @@ -726,7 +730,7 @@ describe("noteMemorySearchHealth", () => { await noteMemorySearchHealth(qmdCfg, { skipQmdBinaryProbe: true }); expect(firstNoteMessage()).toBe( - 'Remember across conversations is enabled for agent "personal", but the Active Memory plugin is disabled. Enable the plugin or turn off Remember across conversations.', + 'Remember across conversations is effectively enabled for agent "personal", but the Active Memory plugin is disabled. Enable the plugin or set memorySearch.rememberAcrossConversations to false.', ); }); @@ -774,7 +778,7 @@ describe("noteMemorySearchHealth", () => { await noteMemorySearchHealth(qmdCfg, { skipQmdBinaryProbe: true }); expect(firstNoteMessage()).toBe( - 'Remember across conversations is enabled for agent "personal", but Active Memory does not allow memory_search. Add memory_search to the plugin toolsAllow list or turn off Remember across conversations.', + 'Remember across conversations is effectively enabled for agent "personal", but Active Memory does not allow memory_search. Add memory_search to the plugin toolsAllow list or set memorySearch.rememberAcrossConversations to false.', ); }); @@ -792,7 +796,7 @@ describe("noteMemorySearchHealth", () => { await noteMemorySearchHealth(qmdCfg, { skipQmdBinaryProbe: true }); expect(firstNoteMessage()).toBe( - 'Remember across conversations is enabled for agent "personal", but memory search is disabled. Enable memory search or turn off Remember across conversations.', + 'Remember across conversations is effectively enabled for agent "personal", but memory search is disabled. Enable memory search or set memorySearch.rememberAcrossConversations to false.', ); }); diff --git a/src/commands/doctor-memory-search.ts b/src/commands/doctor-memory-search.ts index f232f583445..87ec82e6bb3 100644 --- a/src/commands/doctor-memory-search.ts +++ b/src/commands/doctor-memory-search.ts @@ -30,6 +30,7 @@ import { checkQmdBinaryAvailability, resolveQmdBinaryUnavailableReason, } from "../memory-host-sdk/engine-qmd.js"; +import { resolveRememberAcrossConversations } from "../memory-host-sdk/host/config-utils.js"; import { hasConfiguredMemorySecretInput } from "../memory-host-sdk/secret.js"; import { auditDreamingArtifacts, @@ -444,20 +445,12 @@ function listRememberAcrossConversationAgentIds( cfg: OpenClawConfig, defaultAgentId: string, ): string[] { - const defaultsEnabled = cfg.agents?.defaults?.memorySearch?.rememberAcrossConversations === true; const configuredAgents = cfg.agents?.list ?? []; const enabledAgentIds = configuredAgents - .filter( - (agent) => - (agent.memorySearch?.rememberAcrossConversations ?? defaultsEnabled) && - Boolean(agent.id?.trim()), - ) + .filter((agent) => resolveRememberAcrossConversations(cfg, agent.id)) .map((agent) => agent.id.trim()); - const defaultEntry = configuredAgents.find( - (agent) => agent.id.trim().toLowerCase() === defaultAgentId.trim().toLowerCase(), - ); if ( - (defaultEntry?.memorySearch?.rememberAcrossConversations ?? defaultsEnabled) && + resolveRememberAcrossConversations(cfg, defaultAgentId) && !enabledAgentIds.some( (agentId) => agentId.toLowerCase() === defaultAgentId.trim().toLowerCase(), ) @@ -514,18 +507,18 @@ function noteRememberAcrossConversationsHealth(params: { for (const agentId of agentIds) { if (!activeMemoryAvailable) { params.noteFn( - `Remember across conversations is enabled for agent "${agentId}", but the Active Memory plugin is disabled. Enable the plugin or turn off Remember across conversations.`, + `Remember across conversations is effectively enabled for agent "${agentId}", but the Active Memory plugin is disabled. Enable the plugin or set memorySearch.rememberAcrossConversations to false.`, "Memory search", ); } if (activeMemoryAvailable && !conversationRecallSupport.providerSupported) { params.noteFn( - `Remember across conversations is enabled for agent "${agentId}", but the current memory provider does not support protected private transcript recall. Turn off Remember across conversations or use that provider's own recall path; advanced Active Memory can still use its recall tools.`, + `Remember across conversations is effectively enabled for agent "${agentId}", but the current memory provider does not support protected private transcript recall. Set memorySearch.rememberAcrossConversations to false or use that provider's own recall path; advanced Active Memory can still use its recall tools.`, "Memory search", ); } else if (activeMemoryAvailable && !conversationRecallSupport.memorySearchAllowed) { params.noteFn( - `Remember across conversations is enabled for agent "${agentId}", but Active Memory does not allow memory_search. Add memory_search to the plugin toolsAllow list or turn off Remember across conversations.`, + `Remember across conversations is effectively enabled for agent "${agentId}", but Active Memory does not allow memory_search. Add memory_search to the plugin toolsAllow list or set memorySearch.rememberAcrossConversations to false.`, "Memory search", ); } @@ -534,7 +527,7 @@ function noteRememberAcrossConversationsHealth(params: { !resolveMemorySearchConfig(params.cfg, agentId) ) { params.noteFn( - `Remember across conversations is enabled for agent "${agentId}", but memory search is disabled. Enable memory search or turn off Remember across conversations.`, + `Remember across conversations is effectively enabled for agent "${agentId}", but memory search is disabled. Enable memory search or set memorySearch.rememberAcrossConversations to false.`, "Memory search", ); } @@ -586,7 +579,7 @@ export async function noteMemorySearchHealth( if (!resolved) { noteFn( recallHealth.defaultAgentEnabled - ? `Remember across conversations is enabled for agent "${agentId}", but memory search is disabled. Enable memory search or turn off Remember across conversations.` + ? `Remember across conversations is effectively enabled for agent "${agentId}", but memory search is disabled. Enable memory search or set memorySearch.rememberAcrossConversations to false.` : "Memory search is explicitly disabled (enabled: false).", "Memory search", ); diff --git a/src/commands/doctor-state-integrity.test.ts b/src/commands/doctor-state-integrity.test.ts index 784cc25bf97..3ca6f6dd290 100644 --- a/src/commands/doctor-state-integrity.test.ts +++ b/src/commands/doctor-state-integrity.test.ts @@ -233,6 +233,11 @@ describe("structured state integrity findings", () => { async function runOrphanTranscriptCheckWithQmdSessions(enabled: boolean, homeDir: string) { const cfg: OpenClawConfig = { + agents: { + defaults: { + memorySearch: { rememberAcrossConversations: false }, + }, + }, memory: { backend: "qmd", qmd: { diff --git a/src/config/schema.help.models.ts b/src/config/schema.help.models.ts index 7aaf4feceb3..d3a72fb70c3 100644 --- a/src/config/schema.help.models.ts +++ b/src/config/schema.help.models.ts @@ -225,7 +225,7 @@ export const MODEL_FIELD_HELP: Record = { "agents.defaults.memorySearch.enabled": "Master toggle for memory search indexing and retrieval behavior on this agent profile. Keep enabled for semantic recall, and disable when you want fully stateless responses.", "agents.defaults.memorySearch.rememberAcrossConversations": - "Use relevant context from this agent's other private conversations through protected transcript recall. Groups and channels stay separate. Enable this only for a personal or fully trusted agent.", + 'Use relevant context from this agent\'s other private conversations through protected transcript recall. Defaults on only when global session.dmScope is unset or "main" and no binding overrides DM scope; any configured DM isolation defaults it off. An explicit true or false always wins.', "agents.defaults.memorySearch.sources": 'Chooses which sources are indexed: "memory" reads MEMORY.md + memory files, and "sessions" includes transcript history. Keep ["memory"] unless you need recall from prior chat transcripts.', "agents.defaults.memorySearch.extraPaths": diff --git a/src/memory-host-sdk/host/config-utils.ts b/src/memory-host-sdk/host/config-utils.ts new file mode 100644 index 00000000000..a3ad1cb0400 --- /dev/null +++ b/src/memory-host-sdk/host/config-utils.ts @@ -0,0 +1,2 @@ +/** Core-facing memory host config helpers. */ +export { resolveRememberAcrossConversations } from "../../../packages/memory-host-sdk/src/host/config-utils.js"; diff --git a/src/plugin-sdk/memory-core-host-runtime-core.ts b/src/plugin-sdk/memory-core-host-runtime-core.ts index 2a731dd3cbf..1fd8df39fe6 100644 --- a/src/plugin-sdk/memory-core-host-runtime-core.ts +++ b/src/plugin-sdk/memory-core-host-runtime-core.ts @@ -1,5 +1,6 @@ // Memory core host runtime exports bridge memory host runtime-core APIs into the SDK. export { SILENT_REPLY_TOKEN } from "../../packages/memory-host-sdk/src/runtime-core.js"; +export { resolveRememberAcrossConversations } from "../../packages/memory-host-sdk/src/host/config-utils.js"; export { DEFAULT_AGENT_COMPACTION_RESERVE_TOKENS_FLOOR } from "../agents/agent-settings.js"; export { asToolParamsRecord,