diff --git a/docs/concepts/memory-builtin.md b/docs/concepts/memory-builtin.md index e5acff309d55..40f2a2a433e2 100644 --- a/docs/concepts/memory-builtin.md +++ b/docs/concepts/memory-builtin.md @@ -85,10 +85,6 @@ OpenClaw indexes `MEMORY.md` and `memory/*.md` into chunks (~400 tokens with - **Storage maintenance:** SQLite WAL sidecars are bounded with periodic and shutdown checkpoints. - **File watching:** changes to memory files trigger a debounced reindex (1.5s). - File watching is enabled by default, including for gateways, so memory edits - become searchable without a manual reindex. Large memory trees, `extraPaths`, - or QMD collections can use many file descriptors in long-lived gateways; set - `sync.watch: false` for affected agents if that becomes a problem. - **Auto-reindex:** when the embedding provider, model, or chunking config changes, the entire index is rebuilt automatically. - **Reindex on demand:** `openclaw memory index --force` @@ -129,8 +125,8 @@ openclaw memory index --force --agent main Both standalone CLI commands and the Gateway use the same `local` provider id. Set `memorySearch.provider: "local"` when you want local embeddings. -**Stale results?** Run `openclaw memory index --force` to rebuild. Use this when -file watching is disabled or misses a change. +**Stale results?** Run `openclaw memory index --force` to rebuild. The watcher +may miss changes in rare edge cases. **sqlite-vec not loading?** OpenClaw falls back to in-process cosine similarity automatically. `openclaw memory status --deep` reports the local vector store diff --git a/docs/reference/memory-config.md b/docs/reference/memory-config.md index d4eab60926e9..6db3674ab1f4 100644 --- a/docs/reference/memory-config.md +++ b/docs/reference/memory-config.md @@ -527,9 +527,7 @@ QMD model overrides stay on the QMD side, not OpenClaw config. If you need to ov -QMD boot refreshes use a one-shot subprocess path during gateway startup. The long-lived QMD manager owns the regular file watcher and interval timers when memory search is opened for interactive use. - -Local Gateway configs can warn when memory file watching may keep too many files open. If you see open-file or watcher errors, set `sync.watch: false` for the affected agents and use manual indexing or `sync.intervalMinutes` to refresh memory. +QMD boot refreshes use a one-shot subprocess path during gateway startup. The long-lived QMD manager still owns the regular file watcher and interval timers when memory search is opened for interactive use. ### Full QMD example diff --git a/extensions/memory-core/src/memory/qmd-manager.test.ts b/extensions/memory-core/src/memory/qmd-manager.test.ts index c297edb936ee..5cae2b5fc3c0 100644 --- a/extensions/memory-core/src/memory/qmd-manager.test.ts +++ b/extensions/memory-core/src/memory/qmd-manager.test.ts @@ -569,41 +569,6 @@ describe("QmdMemoryManager", () => { await manager.close(); }); - it("logs qmd watcher errors without throwing", async () => { - cfg = { - agents: { - defaults: { - workspace: workspaceDir, - memorySearch: { - provider: "openai", - model: "mock-embed", - store: { path: path.join(workspaceDir, "index.sqlite"), vector: { enabled: false } }, - sync: { watch: true, watchDebounceMs: 25, onSessionStart: false, onSearch: false }, - }, - }, - list: [{ id: agentId, default: true, workspace: workspaceDir }], - }, - memory: { - backend: "qmd", - qmd: { - includeDefaultMemory: false, - update: { interval: "0s", debounceMs: 0, onBoot: false }, - paths: [{ path: workspaceDir, pattern: "**/*.md", name: "workspace" }], - }, - }, - } as OpenClawConfig; - - const { manager } = await createManager({ mode: "full" }); - const watcher = watchMock.mock.results[0]?.value as { - emit: (event: string, ...args: unknown[]) => boolean; - }; - - expect(watcher.emit("error", new Error("watcher error: ENOSPC"))).toBe(true); - expect(logWarnMock).toHaveBeenCalledWith("qmd watcher error: watcher error: ENOSPC"); - - await manager.close(); - }); - it("delays qmd watch sync until changed file stats settle", async () => { vi.useFakeTimers(); cfg = { diff --git a/extensions/memory-core/src/memory/qmd-manager.ts b/extensions/memory-core/src/memory/qmd-manager.ts index 66f9757cc7c7..ed902ae075a7 100644 --- a/extensions/memory-core/src/memory/qmd-manager.ts +++ b/extensions/memory-core/src/memory/qmd-manager.ts @@ -1616,10 +1616,6 @@ export class QmdMemoryManager implements MemorySearchManager { this.watcher.on("add", markDirty); this.watcher.on("change", markDirty); this.watcher.on("unlink", markDirty); - this.watcher.on("error", (err) => { - const message = err instanceof Error ? err.message : String(err); - log.warn(`qmd watcher error: ${message}`); - }); this.watcher.once("ready", () => { log.info( `qmd watcher ready for agent "${this.agentId}" paths=${watchPathList.length} durationMs=${Date.now() - startTime}`, diff --git a/src/agents/memory-search.test.ts b/src/agents/memory-search.test.ts index 16546631e976..066688bae492 100644 --- a/src/agents/memory-search.test.ts +++ b/src/agents/memory-search.test.ts @@ -291,22 +291,6 @@ describe("memory search config", () => { expect(resolveMemorySearchSyncConfig(cfg, "main")?.embeddingBatchTimeoutSeconds).toBe(600); }); - it("keeps memory watching enabled by default in gateway mode", () => { - const cfg = asConfig({ - gateway: { mode: "local" }, - agents: { - defaults: { - memorySearch: { - provider: "openai", - }, - }, - }, - }); - - expect(resolveMemorySearchConfig(cfg, "main")?.sync.watch).toBe(true); - expect(resolveMemorySearchSyncConfig(cfg, "main")?.watch).toBe(true); - }); - it("merges defaults and overrides", () => { const cfg = asConfig({ agents: { diff --git a/src/agents/memory-search.ts b/src/agents/memory-search.ts index 7ec8149b8e64..3530d19073c9 100644 --- a/src/agents/memory-search.ts +++ b/src/agents/memory-search.ts @@ -111,11 +111,6 @@ export type ResolvedMemorySearchConfig = { }; export type ResolvedMemorySearchSyncConfig = ResolvedMemorySearchConfig["sync"]; -export type MemorySearchResolvePurpose = "default" | "status" | "cli"; -export type MemorySearchResolveOptions = { - /** @deprecated No-op; kept for resolver call-site compatibility. */ - purpose?: MemorySearchResolvePurpose; -}; const DEFAULT_CHUNK_TOKENS = 400; const DEFAULT_CHUNK_OVERLAP = 80; @@ -473,7 +468,6 @@ function resolveSyncConfig( export function resolveMemorySearchConfig( cfg: OpenClawConfig, agentId: string, - _options?: MemorySearchResolveOptions, ): ResolvedMemorySearchConfig | null { const defaults = cfg.agents?.defaults?.memorySearch; const overrides = resolveAgentConfig(cfg, agentId)?.memorySearch; @@ -506,7 +500,6 @@ export function resolveMemorySearchConfig( export function resolveMemorySearchSyncConfig( cfg: OpenClawConfig, agentId: string, - _options?: MemorySearchResolveOptions, ): ResolvedMemorySearchSyncConfig | null { const defaults = cfg.agents?.defaults?.memorySearch; const overrides = resolveAgentConfig(cfg, agentId)?.memorySearch; diff --git a/src/config/schema.help.ts b/src/config/schema.help.ts index 2101a7514af7..c0178e8215d5 100644 --- a/src/config/schema.help.ts +++ b/src/config/schema.help.ts @@ -1331,7 +1331,7 @@ export const FIELD_HELP: Record = { "agents.defaults.memorySearch.sync.onSearch": "Uses lazy sync by scheduling reindex on search after content changes are detected. Keep enabled for lower idle overhead, or disable if you require pre-synced indexes before any query.", "agents.defaults.memorySearch.sync.watch": - "Watches memory files and schedules index updates from file-change events. Default: true. Disable for large memory trees, extraPaths, QMD collections, or multi-agent gateways if watcher file-descriptor usage becomes a problem.", + "Watches memory files and schedules index updates from file-change events (chokidar). Enable for near-real-time freshness; disable on very large workspaces if watch churn is too noisy.", "agents.defaults.memorySearch.sync.watchDebounceMs": "Debounce window in milliseconds for coalescing rapid file-watch events before reindex runs. Increase to reduce churn on frequently-written files, or lower for faster freshness.", "agents.defaults.memorySearch.sync.embeddingBatchTimeoutSeconds": diff --git a/src/config/types.tools.ts b/src/config/types.tools.ts index a79cb0cd31f6..fe96a51c71d6 100644 --- a/src/config/types.tools.ts +++ b/src/config/types.tools.ts @@ -523,7 +523,6 @@ export type MemorySearchConfig = { sync?: { onSessionStart?: boolean; onSearch?: boolean; - /** Watch memory files for reindexing (default: true). */ watch?: boolean; watchDebounceMs?: number; intervalMinutes?: number; diff --git a/src/config/validation.policy.test.ts b/src/config/validation.policy.test.ts index 9ace4bab34cb..9e299b2ef3cc 100644 --- a/src/config/validation.policy.test.ts +++ b/src/config/validation.policy.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it, vi } from "vitest"; -import { validateConfigObjectRaw, validateConfigObjectWithPlugins } from "./validation.js"; +import { validateConfigObjectRaw } from "./validation.js"; vi.mock("../channels/plugins/legacy-config.js", () => ({ collectChannelLegacyConfigRules: () => [], @@ -41,108 +41,6 @@ vi.mock("../secrets/unsupported-surface-policy.js", async () => { }; }); -describe("gateway memory watch config warnings", () => { - it("warns when gateway memory watching stays enabled on configured memory surfaces", () => { - const result = validateConfigObjectWithPlugins( - { - gateway: { mode: "local" }, - agents: { - defaults: { - memorySearch: { - extraPaths: ["/srv/shared-notes"], - }, - }, - }, - }, - { pluginValidation: "skip" }, - ); - - expect(result.ok).toBe(true); - expect(result.warnings).toContainEqual( - expect.objectContaining({ - path: "agents.defaults.memorySearch.sync.watch", - message: expect.stringContaining("too many files open"), - }), - ); - }); - - it("does not warn when gateway memory watching is disabled explicitly", () => { - const result = validateConfigObjectWithPlugins( - { - gateway: { mode: "local" }, - agents: { - defaults: { - memorySearch: { - extraPaths: ["/srv/shared-notes"], - sync: { watch: false }, - }, - }, - }, - }, - { pluginValidation: "skip" }, - ); - - expect(result.ok).toBe(true); - expect(result.warnings).not.toContainEqual( - expect.objectContaining({ - path: "agents.defaults.memorySearch.sync.watch", - }), - ); - }); - - it("does not warn for remote client configs", () => { - const result = validateConfigObjectWithPlugins( - { - gateway: { mode: "remote", remote: { url: "wss://gateway.example/ws" } }, - agents: { - defaults: { - memorySearch: { - extraPaths: ["/srv/shared-notes"], - sync: { watch: true }, - }, - }, - }, - }, - { pluginValidation: "skip" }, - ); - - expect(result.ok).toBe(true); - expect(result.warnings).not.toContainEqual( - expect.objectContaining({ - path: "agents.defaults.memorySearch.sync.watch", - }), - ); - }); - - it("warns for explicit per-agent watcher overrides in multi-agent gateways", () => { - const result = validateConfigObjectWithPlugins( - { - gateway: { mode: "local" }, - agents: { - defaults: { - memorySearch: { - sync: { watch: false }, - }, - }, - list: [ - { id: "main", memorySearch: { sync: { watch: false } } }, - { id: "ops", memorySearch: { sync: { watch: true } } }, - ], - }, - }, - { pluginValidation: "skip" }, - ); - - expect(result.ok).toBe(true); - expect(result.warnings).toContainEqual( - expect.objectContaining({ - path: "agents.list.1.memorySearch.sync.watch", - message: expect.stringContaining("many agents"), - }), - ); - }); -}); - function requireIssue(issues: T[], path: string): T { const issue = issues.find((entry) => entry.path === path); if (!issue) { diff --git a/src/config/validation.ts b/src/config/validation.ts index 34982933d1eb..df693d399c3b 100644 --- a/src/config/validation.ts +++ b/src/config/validation.ts @@ -46,7 +46,6 @@ import { shouldSuppressMissingCodexPluginDiagnostics } from "./codex-plugin-diag import { materializeRuntimeConfig } from "./materialize.js"; import type { OpenClawConfig, ConfigValidationIssue } from "./types.js"; import { coerceSecretRef } from "./types.secrets.js"; -import type { MemorySearchConfig } from "./types.tools.js"; import { isBuiltInModelProviderOverlayId } from "./zod-schema.core.js"; import { OpenClawSchema } from "./zod-schema.js"; @@ -872,88 +871,6 @@ function validateGatewayTailscaleAuth(config: OpenClawConfig): ConfigValidationI ]; } -function isLocalGatewayMode(config: OpenClawConfig): boolean { - return config.gateway?.mode === "local"; -} - -function isMemorySearchEnabled( - defaults: MemorySearchConfig | undefined, - override: MemorySearchConfig | undefined, -): boolean { - return override?.enabled ?? defaults?.enabled ?? true; -} - -function isMemoryWatchEnabled( - defaults: MemorySearchConfig | undefined, - override: MemorySearchConfig | undefined, -): boolean { - return override?.sync?.watch ?? defaults?.sync?.watch ?? true; -} - -function hasConfiguredMemoryWatchFdPressureSurface( - config: OpenClawConfig, - defaults: MemorySearchConfig | undefined, - override: MemorySearchConfig | undefined, -): boolean { - const hasMemorySearchConfig = Boolean(defaults || override); - const hasMultipleGatewayAgents = (config.agents?.list?.length ?? 0) > 1; - const hasQmdBackend = config.memory?.backend === "qmd"; - const hasQmdPaths = Boolean(config.memory?.qmd?.paths?.length); - const hasExtraPaths = Boolean(defaults?.extraPaths?.length || override?.extraPaths?.length); - const hasExtraQmdCollections = Boolean( - defaults?.qmd?.extraCollections?.length || override?.qmd?.extraCollections?.length, - ); - const hasSessionMemory = Boolean( - defaults?.experimental?.sessionMemory || override?.experimental?.sessionMemory, - ); - return ( - hasMemorySearchConfig || - hasMultipleGatewayAgents || - hasQmdBackend || - hasQmdPaths || - hasExtraPaths || - hasExtraQmdCollections || - hasSessionMemory - ); -} - -function memoryWatchFdPressureWarningMessage(): string { - return "Memory file watching is on for this Gateway. This keeps memory search up to date, but large memory folders, extraPaths, QMD collections, session memory, or many agents can make the Gateway keep too many files open. If you see open-file or watcher errors, set memorySearch.sync.watch: false for the affected default or agent, then use manual indexing or sync.intervalMinutes to refresh memory."; -} - -function collectGatewayMemoryWatchWarnings(config: OpenClawConfig): ConfigValidationIssue[] { - if (!isLocalGatewayMode(config)) { - return []; - } - const defaults = config.agents?.defaults?.memorySearch; - const warnings: ConfigValidationIssue[] = []; - if ( - isMemorySearchEnabled(defaults, undefined) && - isMemoryWatchEnabled(defaults, undefined) && - hasConfiguredMemoryWatchFdPressureSurface(config, defaults, undefined) - ) { - warnings.push({ - path: "agents.defaults.memorySearch.sync.watch", - message: memoryWatchFdPressureWarningMessage(), - }); - } - for (const [index, agent] of (config.agents?.list ?? []).entries()) { - const override = agent.memorySearch; - if ( - override && - isMemorySearchEnabled(defaults, override) && - isMemoryWatchEnabled(defaults, override) && - hasConfiguredMemoryWatchFdPressureSurface(config, defaults, override) - ) { - warnings.push({ - path: `agents.list.${index}.memorySearch.sync.watch`, - message: memoryWatchFdPressureWarningMessage(), - }); - } - } - return warnings; -} - /** * Validates config without applying runtime defaults. * Use this when you need the raw validated config (e.g., for writing back to file). @@ -1122,17 +1039,16 @@ function validateConfigObjectWithPluginsBase( manifestRegistry: registryInfo?.registry, }) : base.config; - const configWarnings = collectGatewayMemoryWatchWarnings(base.config); if (opts.pluginValidation === "skip") { return { ok: true, config, - warnings: configWarnings, + warnings: [], }; } const issues: ConfigValidationIssue[] = []; - const warnings: ConfigValidationIssue[] = [...configWarnings]; + const warnings: ConfigValidationIssue[] = []; const hasExplicitPluginsConfig = isRecord(raw) && Object.hasOwn(raw, "plugins"); const explicitPluginReferences = collectExplicitPluginReferences(raw);