From 8cd1bdd345a81bd30d475f1147c7f903937853fb Mon Sep 17 00:00:00 2001 From: Gustavo Madeira Santana Date: Mon, 16 Mar 2026 10:27:44 +0000 Subject: [PATCH] Status: stabilize startup memory probes --- scripts/check-cli-startup-memory.mjs | 10 +- src/commands/status.scan.test.ts | 138 ++++++++++++++++++++++++++- src/commands/status.scan.ts | 24 +++++ 3 files changed, 168 insertions(+), 4 deletions(-) diff --git a/scripts/check-cli-startup-memory.mjs b/scripts/check-cli-startup-memory.mjs index fcbd63d8d11..ce452d1a7ab 100644 --- a/scripts/check-cli-startup-memory.mjs +++ b/scripts/check-cli-startup-memory.mjs @@ -63,11 +63,12 @@ const cases = [ ]; function parseMaxRssMb(stderr) { - const match = stderr.match(new RegExp(`^${MAX_RSS_MARKER}(\\d+)\\s*$`, "m")); - if (!match) { + const matches = [...stderr.matchAll(new RegExp(`^${MAX_RSS_MARKER}(\\d+)\\s*$`, "gm"))]; + const lastMatch = matches.at(-1); + if (!lastMatch) { return null; } - return Number(match[1]) / 1024; + return Number(lastMatch[1]) / 1024; } function buildBenchEnv() { @@ -98,6 +99,9 @@ function buildBenchEnv() { // one-shot compile cache overhead, which varies across runner builds. env.NODE_DISABLE_COMPILE_CACHE = "1"; } + // Keep the benchmark on a single process so RSS reflects the actual command + // path rather than the warning-suppression respawn wrapper. + env.OPENCLAW_NO_RESPAWN = "1"; return env; } diff --git a/src/commands/status.scan.test.ts b/src/commands/status.scan.test.ts index 6e778070c09..edb77ae4fcf 100644 --- a/src/commands/status.scan.test.ts +++ b/src/commands/status.scan.test.ts @@ -8,6 +8,7 @@ const mocks = vi.hoisted(() => ({ getUpdateCheckResult: vi.fn(), getAgentLocalStatuses: vi.fn(), getStatusSummary: vi.fn(), + getMemorySearchManager: vi.fn(), buildGatewayConnectionDetails: vi.fn(), probeGateway: vi.fn(), resolveGatewayProbeAuthResolution: vi.fn(), @@ -53,7 +54,7 @@ vi.mock("../infra/os-summary.js", () => ({ vi.mock("./status.scan.deps.runtime.js", () => ({ getTailnetHostname: vi.fn(), - getMemorySearchManager: vi.fn(), + getMemorySearchManager: mocks.getMemorySearchManager, })); vi.mock("../gateway/call.js", () => ({ @@ -196,6 +197,141 @@ describe("scanStatus", () => { expect(mocks.ensurePluginRegistryLoaded).not.toHaveBeenCalled(); }); + it("skips memory backend inspection for default memory-core with no existing store", async () => { + mocks.readBestEffortConfig.mockResolvedValue({ + session: {}, + gateway: {}, + }); + mocks.resolveCommandSecretRefsViaGateway.mockResolvedValue({ + resolvedConfig: { + session: {}, + gateway: {}, + }, + diagnostics: [], + }); + mocks.getUpdateCheckResult.mockResolvedValue({ + installKind: "git", + git: null, + registry: null, + }); + mocks.getAgentLocalStatuses.mockResolvedValue({ + defaultId: "main", + agents: [], + }); + mocks.getStatusSummary.mockResolvedValue({ + linkChannel: undefined, + sessions: { count: 0, paths: [], defaults: {}, recent: [] }, + }); + mocks.buildGatewayConnectionDetails.mockReturnValue({ + url: "ws://127.0.0.1:18789", + urlSource: "default", + }); + mocks.resolveGatewayProbeAuthResolution.mockReturnValue({ + auth: {}, + warning: undefined, + }); + mocks.probeGateway.mockResolvedValue({ + ok: false, + url: "ws://127.0.0.1:18789", + connectLatencyMs: null, + error: "timeout", + close: null, + health: null, + status: null, + presence: null, + configSnapshot: null, + }); + + await scanStatus({ json: true }, {} as never); + + expect(mocks.getMemorySearchManager).not.toHaveBeenCalled(); + }); + + it("inspects memory backend when memory search is explicitly configured", async () => { + mocks.readBestEffortConfig.mockResolvedValue({ + session: {}, + gateway: {}, + agents: { + defaults: { + memorySearch: { + provider: "local", + local: { modelPath: "/tmp/model.gguf" }, + fallback: "none", + }, + }, + }, + }); + mocks.resolveCommandSecretRefsViaGateway.mockResolvedValue({ + resolvedConfig: { + session: {}, + gateway: {}, + agents: { + defaults: { + memorySearch: { + provider: "local", + local: { modelPath: "/tmp/model.gguf" }, + fallback: "none", + }, + }, + }, + }, + diagnostics: [], + }); + mocks.getUpdateCheckResult.mockResolvedValue({ + installKind: "git", + git: null, + registry: null, + }); + mocks.getAgentLocalStatuses.mockResolvedValue({ + defaultId: "main", + agents: [], + }); + mocks.getStatusSummary.mockResolvedValue({ + linkChannel: undefined, + sessions: { count: 0, paths: [], defaults: {}, recent: [] }, + }); + mocks.buildGatewayConnectionDetails.mockReturnValue({ + url: "ws://127.0.0.1:18789", + urlSource: "default", + }); + mocks.resolveGatewayProbeAuthResolution.mockReturnValue({ + auth: {}, + warning: undefined, + }); + mocks.probeGateway.mockResolvedValue({ + ok: false, + url: "ws://127.0.0.1:18789", + connectLatencyMs: null, + error: "timeout", + close: null, + health: null, + status: null, + presence: null, + configSnapshot: null, + }); + mocks.getMemorySearchManager.mockResolvedValue({ + manager: { + probeVectorAvailability: vi.fn(async () => true), + status: vi.fn(() => ({ files: 0, chunks: 0, dirty: false })), + close: vi.fn(async () => {}), + }, + }); + + await scanStatus({ json: true }, {} as never); + + expect(mocks.getMemorySearchManager).toHaveBeenCalledWith({ + cfg: expect.objectContaining({ + agents: expect.objectContaining({ + defaults: expect.objectContaining({ + memorySearch: expect.any(Object), + }), + }), + }), + agentId: "main", + purpose: "status", + }); + }); + it("preloads configured channel plugins for status --json when channel config exists", async () => { mocks.readBestEffortConfig.mockResolvedValue({ session: {}, diff --git a/src/commands/status.scan.ts b/src/commands/status.scan.ts index bbe10301624..6c2bd67f3dd 100644 --- a/src/commands/status.scan.ts +++ b/src/commands/status.scan.ts @@ -1,3 +1,5 @@ +import { existsSync } from "node:fs"; +import { resolveMemorySearchConfig } from "../agents/memory-search.js"; import { hasPotentialConfiguredChannels } from "../channels/config-presence.js"; import { resolveCommandSecretRefsViaGateway } from "../cli/command-secret-gateway.js"; import { getStatusCommandSecretTargetIds } from "../cli/command-secret-targets.js"; @@ -33,6 +35,19 @@ type MemoryPluginStatus = { reason?: string; }; +function hasExplicitMemorySearchConfig(cfg: OpenClawConfig, agentId: string): boolean { + if ( + cfg.agents?.defaults && + Object.prototype.hasOwnProperty.call(cfg.agents.defaults, "memorySearch") + ) { + return true; + } + const agents = Array.isArray(cfg.agents?.list) ? cfg.agents.list : []; + return agents.some( + (agent) => agent?.id === agentId && Object.prototype.hasOwnProperty.call(agent, "memorySearch"), + ); +} + type DeferredResult = { ok: true; value: T } | { ok: false; error: unknown }; type GatewayProbeSnapshot = { @@ -190,6 +205,15 @@ async function resolveMemoryStatusSnapshot(params: { return null; } const agentId = agentStatus.defaultId ?? "main"; + const resolvedMemory = resolveMemorySearchConfig(cfg, agentId); + if (!resolvedMemory) { + return null; + } + const shouldInspectStore = + hasExplicitMemorySearchConfig(cfg, agentId) || existsSync(resolvedMemory.store.path); + if (!shouldInspectStore) { + return null; + } const { getMemorySearchManager } = await loadStatusScanDepsRuntimeModule(); const { manager } = await getMemorySearchManager({ cfg, agentId, purpose: "status" }); if (!manager) {