Status: stabilize startup memory probes

This commit is contained in:
Gustavo Madeira Santana
2026-03-16 10:27:44 +00:00
parent 1cf544ffbc
commit 8cd1bdd345
3 changed files with 168 additions and 4 deletions

View File

@@ -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: {},

View File

@@ -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<T> = { 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) {