From f5a48efac57b3e8580d6a93338be993b2376de8c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 28 Apr 2026 01:51:30 +0100 Subject: [PATCH] fix(status): report custom memory plugin status --- CHANGELOG.md | 1 + docs/cli/status.md | 3 +- src/commands/status-overview-rows.test.ts | 18 +++++ src/commands/status-overview-rows.ts | 1 + src/commands/status.command-sections.ts | 3 +- src/commands/status.scan.shared.test.ts | 85 ++++++++++++++++++++++- src/commands/status.scan.shared.ts | 60 +++++++++++----- 7 files changed, 151 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 163435951d0..7a73df3514f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- CLI/status: show skipped fast-path memory checks as `not checked` and report active custom memory plugin runtime status from `status --json --all` without requiring built-in `agents.defaults.memorySearch`, so plugins such as memory-lancedb-pro and memory-cms no longer look unavailable when their own runtime is healthy. Fixes #56968. Thanks @Tony-ooo and @aderius. - Memory/LanceDB: let embedding config use provider-backed auth profiles, environment credentials, or provider config without a separate plugin `embedding.apiKey`, so OAuth-capable embedding providers can power auto-recall/capture. Fixes #68950. Thanks @malshaalan-ai. - Plugins/hooks: time out never-settling `agent_end` observation hooks after 30 seconds and log the plugin failure, so hung embedding endpoints no longer leave memory capture silently pending forever. Fixes #65544. Thanks @ghoc0099. - Gateway/config: serve runtime config schemas from the current plugin metadata snapshot and generated bundled channel schema metadata instead of rebuilding plugin channel config modules on every `config.get`/`config.schema`, preventing idle plugin-discovery CPU churn after upgrades. Fixes #73088. Thanks @sleitor and @geovansb. diff --git a/docs/cli/status.md b/docs/cli/status.md index 93782f27e16..b9281899303 100644 --- a/docs/cli/status.md +++ b/docs/cli/status.md @@ -20,7 +20,8 @@ openclaw status --usage Notes: - `--deep` runs live probes (WhatsApp Web + Telegram + Discord + Slack + Signal). -- Plain `openclaw status` stays on the fast read-only path. Heavy security audit, plugin compatibility, and memory-vector probes are left to `openclaw status --all`, `openclaw status --deep`, `openclaw security audit`, and `openclaw memory status --deep`. +- Plain `openclaw status` stays on the fast read-only path and marks memory as `not checked` instead of unavailable when it skips memory inspection. Heavy security audit, plugin compatibility, and memory-vector probes are left to `openclaw status --all`, `openclaw status --deep`, `openclaw security audit`, and `openclaw memory status --deep`. +- `status --json --all` reports memory details from the active memory plugin runtime selected by `plugins.slots.memory`. Custom memory plugins can leave built-in `agents.defaults.memorySearch.enabled` disabled and still report their own files, chunks, vector, and FTS state. - `--usage` prints normalized provider usage windows as `X% left`. - Session status output separates `Execution:` from `Runtime:`. `Execution` is the sandbox path (`direct`, `docker/*`), while `Runtime` tells you whether the session is using `OpenClaw Pi Default`, `OpenAI Codex`, a CLI backend, or an ACP backend such as `codex (acp/acpx)`. See [Agent runtimes](/concepts/agent-runtimes) for the provider/model/runtime distinction. - MiniMax's raw `usage_percent` / `usagePercent` fields are remaining quota, so OpenClaw inverts them before display; count-based fields win when present. `model_remains` responses prefer the chat-model entry, derive the window label from timestamps when needed, and include the model name in the plan label. diff --git a/src/commands/status-overview-rows.test.ts b/src/commands/status-overview-rows.test.ts index 44719ffddf5..356fe80e353 100644 --- a/src/commands/status-overview-rows.test.ts +++ b/src/commands/status-overview-rows.test.ts @@ -24,6 +24,24 @@ describe("status-overview-rows", () => { ); }); + it("marks skipped memory inspection as not checked in fast status output", () => { + expect( + buildStatusCommandOverviewRows( + createStatusCommandOverviewRowsParams({ + memory: null, + memoryPlugin: { enabled: true, slot: "memory-lancedb-pro" }, + }), + ), + ).toEqual( + expect.arrayContaining([ + { + Item: "Memory", + Value: "muted(enabled (plugin memory-lancedb-pro) · not checked)", + }, + ]), + ); + }); + it("builds status-all overview rows from the shared surface", () => { expect( buildStatusAllOverviewRows({ diff --git a/src/commands/status-overview-rows.ts b/src/commands/status-overview-rows.ts index 0c680826721..d78db9e69fd 100644 --- a/src/commands/status-overview-rows.ts +++ b/src/commands/status-overview-rows.ts @@ -89,6 +89,7 @@ export function buildStatusCommandOverviewRows( resolveMemoryVectorState: params.resolveMemoryVectorState, resolveMemoryFtsState: params.resolveMemoryFtsState, resolveMemoryCacheSummary: params.resolveMemoryCacheSummary, + memoryUnavailableLabel: "not checked", }); const pluginCompatibilityValue = buildStatusPluginCompatibilityValue({ notices: params.pluginCompatibility, diff --git a/src/commands/status.command-sections.ts b/src/commands/status.command-sections.ts index 4145ab04050..ffae54fda5e 100644 --- a/src/commands/status.command-sections.ts +++ b/src/commands/status.command-sections.ts @@ -144,6 +144,7 @@ export function buildStatusMemoryValue( ok: (value: string) => string; warn: (value: string) => string; muted: (value: string) => string; + memoryUnavailableLabel?: string; } & StatusMemoryStateResolvers, ) { if (!params.memoryPlugin.enabled) { @@ -152,7 +153,7 @@ export function buildStatusMemoryValue( } if (!params.memory) { const slot = params.memoryPlugin.slot ? `plugin ${params.memoryPlugin.slot}` : "plugin"; - return params.muted(`enabled (${slot}) · unavailable`); + return params.muted(`enabled (${slot}) · ${params.memoryUnavailableLabel ?? "unavailable"}`); } const parts: string[] = []; const dirtySuffix = params.memory.dirty ? ` · ${params.warn("dirty")}` : ""; diff --git a/src/commands/status.scan.shared.test.ts b/src/commands/status.scan.shared.test.ts index 267a169c014..dc882181066 100644 --- a/src/commands/status.scan.shared.test.ts +++ b/src/commands/status.scan.shared.test.ts @@ -1,5 +1,8 @@ import { beforeEach, describe, expect, it, vi } from "vitest"; -import { resolveGatewayProbeSnapshot } from "./status.scan.shared.js"; +import { + resolveGatewayProbeSnapshot, + resolveSharedMemoryStatusSnapshot, +} from "./status.scan.shared.js"; const mocks = vi.hoisted(() => ({ buildGatewayConnectionDetailsWithResolvers: vi.fn(), @@ -140,3 +143,83 @@ describe("resolveGatewayProbeSnapshot", () => { expect(result.gatewayProbeAuthWarning).toBeUndefined(); }); }); + +describe("resolveSharedMemoryStatusSnapshot", () => { + it("asks custom memory-slot runtimes for status without requiring built-in memorySearch", async () => { + const manager = { + probeVectorAvailability: vi.fn(async () => true), + status: vi.fn(() => ({ + backend: "builtin" as const, + provider: "memory-lancedb-pro", + files: 66, + chunks: 128, + vector: { enabled: true, available: true }, + fts: { enabled: true, available: true }, + })), + close: vi.fn(async () => {}), + }; + const resolveMemoryConfig = vi.fn(() => null); + const getMemorySearchManager = vi.fn(async () => ({ manager })); + const requireDefaultStore = vi.fn(() => `/tmp/openclaw-missing-memory-${process.pid}.sqlite`); + + const result = await resolveSharedMemoryStatusSnapshot({ + cfg: { + plugins: { + slots: { memory: "memory-lancedb-pro" }, + }, + agents: { + defaults: { + memorySearch: { enabled: false }, + }, + }, + }, + agentStatus: { defaultId: "main" }, + memoryPlugin: { enabled: true, slot: "memory-lancedb-pro" }, + resolveMemoryConfig, + getMemorySearchManager, + requireDefaultStore, + }); + + expect(resolveMemoryConfig).not.toHaveBeenCalled(); + expect(requireDefaultStore).not.toHaveBeenCalled(); + expect(getMemorySearchManager).toHaveBeenCalledWith({ + cfg: expect.objectContaining({ + plugins: expect.objectContaining({ + slots: { memory: "memory-lancedb-pro" }, + }), + }), + agentId: "main", + purpose: "status", + }); + expect(manager.probeVectorAvailability).toHaveBeenCalled(); + expect(manager.status).toHaveBeenCalled(); + expect(manager.close).toHaveBeenCalled(); + expect(result).toEqual({ + agentId: "main", + backend: "builtin", + provider: "memory-lancedb-pro", + files: 66, + chunks: 128, + vector: { enabled: true, available: true }, + fts: { enabled: true, available: true }, + }); + }); + + it("keeps default memory-core on the cold-start store shortcut", async () => { + const resolveMemoryConfig = vi.fn(() => null); + const getMemorySearchManager = vi.fn(async () => ({ manager: null })); + + const result = await resolveSharedMemoryStatusSnapshot({ + cfg: {}, + agentStatus: { defaultId: "main" }, + memoryPlugin: { enabled: true, slot: "memory-core" }, + resolveMemoryConfig, + getMemorySearchManager, + requireDefaultStore: () => `/tmp/openclaw-missing-memory-${process.pid}.sqlite`, + }); + + expect(result).toBeNull(); + expect(resolveMemoryConfig).not.toHaveBeenCalled(); + expect(getMemorySearchManager).not.toHaveBeenCalled(); + }); +}); diff --git a/src/commands/status.scan.shared.ts b/src/commands/status.scan.shared.ts index 0c15b752285..5d4ff372e2a 100644 --- a/src/commands/status.scan.shared.ts +++ b/src/commands/status.scan.shared.ts @@ -55,6 +55,20 @@ export type GatewayProbeSnapshot = { }; }; +type StatusMemorySearchManager = { + probeVectorAvailability(): Promise; + status(): MemoryProviderStatus; + close?(): Promise; +}; + +type StatusMemorySearchManagerResolver = (params: { + cfg: OpenClawConfig; + agentId: string; + purpose: "status"; +}) => Promise<{ + manager: StatusMemorySearchManager | null; +}>; + export function hasExplicitMemorySearchConfig(cfg: OpenClawConfig, agentId: string): boolean { if ( cfg.agents?.defaults && @@ -168,17 +182,7 @@ export async function resolveSharedMemoryStatusSnapshot(params: { agentStatus: { defaultId?: string | null }; memoryPlugin: MemoryPluginStatus; resolveMemoryConfig: (cfg: OpenClawConfig, agentId: string) => { store: { path: string } } | null; - getMemorySearchManager: (params: { - cfg: OpenClawConfig; - agentId: string; - purpose: "status"; - }) => Promise<{ - manager: { - probeVectorAvailability(): Promise; - status(): MemoryProviderStatus; - close?(): Promise; - } | null; - }>; + getMemorySearchManager: StatusMemorySearchManagerResolver; requireDefaultStore?: (agentId: string) => string | null; }): Promise { const { cfg, agentStatus, memoryPlugin } = params; @@ -186,6 +190,11 @@ export async function resolveSharedMemoryStatusSnapshot(params: { return null; } const agentId = agentStatus.defaultId ?? "main"; + + if (memoryPlugin.slot !== defaultSlotIdForKey("memory")) { + return await resolveMemoryManagerStatusSnapshot(params, agentId); + } + const defaultStorePath = params.requireDefaultStore?.(agentId); if ( defaultStorePath && @@ -203,14 +212,31 @@ export async function resolveSharedMemoryStatusSnapshot(params: { if (!shouldInspectStore) { return null; } - const { manager } = await params.getMemorySearchManager({ cfg, agentId, purpose: "status" }); + return await resolveMemoryManagerStatusSnapshot(params, agentId); +} + +async function resolveMemoryManagerStatusSnapshot( + params: { + cfg: OpenClawConfig; + getMemorySearchManager: StatusMemorySearchManagerResolver; + }, + agentId: string, +): Promise { + const { manager } = await params.getMemorySearchManager({ + cfg: params.cfg, + agentId, + purpose: "status", + }); if (!manager) { return null; } try { - await manager.probeVectorAvailability(); - } catch {} - const status = manager.status(); - await manager.close?.().catch(() => {}); - return { agentId, ...status }; + try { + await manager.probeVectorAvailability(); + } catch {} + const status = manager.status(); + return { agentId, ...status }; + } finally { + await manager.close?.().catch(() => {}); + } }