Skip to content

Commit b6cbd92

Browse files
neeravmakwanacursoragentclawsweeper[bot]
authored
fix(cli): load memory plugin for doctor/status when registry is cold (#76393)
Summary: - The PR adds a scoped standalone memory-slot plugin load for doctor/status memory resolution, updates memory-runtime regression tests, and adds a changelog fix entry. - Reproducibility: yes. source-reproducible: current main's doctor/status path reads getMemoryRuntime after on ... registers that runtime only during plugin activation. I did not run a live macOS LaunchAgent reproduction. Automerge notes: - PR branch already contained follow-up commit before automerge: fix(cli): load memory plugin for doctor/status when registry is cold Validation: - ClawSweeper review passed for head a6a1967. - Required merge gates passed before the squash merge. Prepared head SHA: a6a1967 Review: #76393 (comment) Co-authored-by: Neerav Makwana <[email protected]> Co-authored-by: Cursor <[email protected]> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
1 parent 39bc94e commit b6cbd92

3 files changed

Lines changed: 144 additions & 11 deletions

File tree

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ Docs: https://docs.openclaw.ai
4343
- Agents/sessions: preserve delivered trailing assistant replies during session-file repair so Telegram/WebChat history is not rewritten to drop already-delivered responses. Fixes #76329. Thanks @obviyus.
4444
- Gateway/chat history: preserve oversized transcript turns as explicit omitted-message placeholders while avoiding large JSONL parse stalls. Thanks @Marvinthebored and @vincentkoc.
4545
- Gateway/models: keep read-only model-list responses on registry-compatible fallbacks and metadata defaults, so empty or minimal persisted model files do not hide built-ins or custom model capabilities. Thanks @Marvinthebored.
46+
- CLI/doctor: load the configured memory-slot plugin when resolving memory diagnostics so bundled `memory-core` no longer triggers a false “no active memory plugin” warning on standalone `doctor` / `status` runs. Fixes #76367. Thanks @neeravmakwana.
4647
- Gateway: preserve stack diagnostics when `chat.send` or agent attachment parsing/staging fails, improving image-send failure triage. Refs #63432. (#75135) Thanks @keen0206.
4748
- Heartbeats/Codex: stop sending the legacy `HEARTBEAT_OK` prompt instruction when heartbeat turns have the structured `heartbeat_respond` tool, while keeping the text sentinel for legacy automatic heartbeat replies. Thanks @pashpashpash.
4849
- Agent runtimes: fail explicit plugin runtime selections honestly when the requested harness is unavailable instead of silently falling back to the embedded PI runtime. Thanks @pashpashpash.

src/plugins/memory-runtime.test.ts

Lines changed: 109 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ const resolveRuntimePluginRegistryMock =
44
vi.fn<typeof import("./loader.js").resolveRuntimePluginRegistry>();
55
const getLoadedRuntimePluginRegistryMock =
66
vi.fn<typeof import("./active-runtime-registry.js").getLoadedRuntimePluginRegistry>();
7+
const ensureStandaloneRuntimePluginRegistryLoadedMock = vi.hoisted(() =>
8+
vi.fn<
9+
typeof import("./runtime/standalone-runtime-registry-loader.js").ensureStandaloneRuntimePluginRegistryLoaded
10+
>(),
11+
);
712
const applyPluginAutoEnableMock =
813
vi.fn<typeof import("../config/plugin-auto-enable.js").applyPluginAutoEnable>();
914
const getMemoryRuntimeMock = vi.fn<typeof import("./memory-state.js").getMemoryRuntime>();
@@ -30,6 +35,10 @@ vi.mock("./active-runtime-registry.js", () => ({
3035
getLoadedRuntimePluginRegistry: getLoadedRuntimePluginRegistryMock,
3136
}));
3237

38+
vi.mock("./runtime/standalone-runtime-registry-loader.js", () => ({
39+
ensureStandaloneRuntimePluginRegistryLoaded: ensureStandaloneRuntimePluginRegistryLoadedMock,
40+
}));
41+
3342
vi.mock("./memory-state.js", () => ({
3443
getMemoryRuntime: () => getMemoryRuntimeMock(),
3544
}));
@@ -61,12 +70,25 @@ function createMemoryRuntimeFixture() {
6170
};
6271
}
6372

64-
function expectMemoryRuntimeLoaded(rawConfig: unknown, autoEnabledConfig: unknown) {
73+
function expectMemoryRuntimeLoaded(
74+
rawConfig: unknown,
75+
autoEnabledConfig: unknown,
76+
pluginIds: readonly string[] = ["memory-core"],
77+
) {
6578
void rawConfig;
6679
void autoEnabledConfig;
6780
expect(getLoadedRuntimePluginRegistryMock).toHaveBeenCalledWith(
6881
expect.objectContaining({
69-
requiredPluginIds: ["memory-core"],
82+
requiredPluginIds: pluginIds,
83+
}),
84+
);
85+
expect(ensureStandaloneRuntimePluginRegistryLoadedMock).toHaveBeenCalledWith(
86+
expect.objectContaining({
87+
requiredPluginIds: pluginIds,
88+
loadOptions: expect.objectContaining({
89+
onlyPluginIds: pluginIds,
90+
workspaceDir: "/resolved-workspace",
91+
}),
7092
}),
7193
);
7294
}
@@ -84,14 +106,18 @@ function setAutoEnabledMemoryRuntime() {
84106
changes: [],
85107
autoEnabledReasons: {},
86108
});
87-
getMemoryRuntimeMock.mockReturnValueOnce(undefined).mockReturnValue(runtime);
109+
getMemoryRuntimeMock
110+
.mockReturnValueOnce(undefined)
111+
.mockReturnValueOnce(undefined)
112+
.mockReturnValue(runtime);
88113
return { rawConfig, autoEnabledConfig, runtime };
89114
}
90115

91116
function expectNoMemoryRuntimeBootstrap() {
92117
expect(applyPluginAutoEnableMock).not.toHaveBeenCalled();
93118
expect(resolveRuntimePluginRegistryMock).not.toHaveBeenCalled();
94119
expect(getLoadedRuntimePluginRegistryMock).not.toHaveBeenCalled();
120+
expect(ensureStandaloneRuntimePluginRegistryLoadedMock).not.toHaveBeenCalled();
95121
}
96122

97123
async function expectAutoEnabledMemoryRuntimeCase(params: {
@@ -130,6 +156,7 @@ describe("memory runtime auto-enable loading", () => {
130156
} = await import("./memory-runtime.js"));
131157
resolveRuntimePluginRegistryMock.mockReset();
132158
getLoadedRuntimePluginRegistryMock.mockReset();
159+
ensureStandaloneRuntimePluginRegistryLoadedMock.mockReset();
133160
applyPluginAutoEnableMock.mockReset();
134161
getMemoryRuntimeMock.mockReset();
135162
resolveAgentWorkspaceDirMock.mockReset();
@@ -179,18 +206,17 @@ describe("memory runtime auto-enable loading", () => {
179206
changes: [],
180207
autoEnabledReasons: {},
181208
});
182-
getMemoryRuntimeMock.mockReturnValueOnce(undefined).mockReturnValue(runtime);
209+
getMemoryRuntimeMock
210+
.mockReturnValueOnce(undefined)
211+
.mockReturnValueOnce(undefined)
212+
.mockReturnValue(runtime);
183213

184214
await getActiveMemorySearchManager({
185215
cfg: rawConfig as never,
186216
agentId: "main",
187217
});
188218

189-
expect(getLoadedRuntimePluginRegistryMock).toHaveBeenCalledWith(
190-
expect.objectContaining({
191-
requiredPluginIds: ["memory-lancedb"],
192-
}),
193-
);
219+
expectMemoryRuntimeLoaded(rawConfig, rawConfig, ["memory-lancedb"]);
194220
});
195221

196222
it("does not fall back to broad plugin loading when the memory slot is disabled", async () => {
@@ -218,6 +244,80 @@ describe("memory runtime auto-enable loading", () => {
218244
expect(applyPluginAutoEnableMock).not.toHaveBeenCalled();
219245
expect(resolveRuntimePluginRegistryMock).not.toHaveBeenCalled();
220246
expect(getLoadedRuntimePluginRegistryMock).not.toHaveBeenCalled();
247+
expect(ensureStandaloneRuntimePluginRegistryLoadedMock).not.toHaveBeenCalled();
248+
});
249+
250+
it("does not standalone-load the memory plugin when plugins are globally disabled", async () => {
251+
const rawConfig = {
252+
plugins: {
253+
enabled: false,
254+
},
255+
};
256+
getMemoryRuntimeMock.mockReturnValue(undefined);
257+
258+
await expect(
259+
getActiveMemorySearchManager({
260+
cfg: rawConfig as never,
261+
agentId: "main",
262+
}),
263+
).resolves.toEqual({ manager: null, error: "memory plugin unavailable" });
264+
265+
expectNoMemoryRuntimeBootstrap();
266+
});
267+
268+
it.each([
269+
{
270+
name: "denied",
271+
plugins: {
272+
deny: ["memory-core"],
273+
slots: {
274+
memory: "memory-core",
275+
},
276+
},
277+
},
278+
{
279+
name: "entry-disabled",
280+
plugins: {
281+
entries: {
282+
"memory-core": { enabled: false },
283+
},
284+
slots: {
285+
memory: "memory-core",
286+
},
287+
},
288+
},
289+
] as const)("does not standalone-load a $name memory slot plugin", async ({ plugins }) => {
290+
getMemoryRuntimeMock.mockReturnValue(undefined);
291+
292+
await expect(
293+
getActiveMemorySearchManager({
294+
cfg: { plugins } as never,
295+
agentId: "main",
296+
}),
297+
).resolves.toEqual({ manager: null, error: "memory plugin unavailable" });
298+
299+
expectNoMemoryRuntimeBootstrap();
300+
});
301+
302+
it("does not standalone-load plugins when the memory runtime is already registered", () => {
303+
const rawConfig = {
304+
plugins: {
305+
slots: {
306+
memory: "memory-core",
307+
},
308+
},
309+
};
310+
const runtime = createMemoryRuntimeFixture();
311+
getLoadedRuntimePluginRegistryMock.mockReturnValue({} as never);
312+
getMemoryRuntimeMock.mockReturnValueOnce(undefined).mockReturnValue(runtime);
313+
314+
resolveActiveMemoryBackendConfig({
315+
cfg: rawConfig as never,
316+
agentId: "main",
317+
});
318+
319+
expect(getLoadedRuntimePluginRegistryMock).toHaveBeenCalled();
320+
expect(ensureStandaloneRuntimePluginRegistryLoadedMock).not.toHaveBeenCalled();
221321
});
222322

223323
it.each([

src/plugins/memory-runtime.ts

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,31 @@
1+
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
12
import type { OpenClawConfig } from "../config/types.openclaw.js";
3+
import { resolveUserPath } from "../utils.js";
24
import { getLoadedRuntimePluginRegistry } from "./active-runtime-registry.js";
35
import { normalizePluginsConfig } from "./config-state.js";
46
import { getMemoryRuntime } from "./memory-state.js";
7+
import { ensureStandaloneRuntimePluginRegistryLoaded } from "./runtime/standalone-runtime-registry-loader.js";
58

69
function resolveMemoryRuntimePluginIds(config: OpenClawConfig): string[] {
7-
const memorySlot = normalizePluginsConfig(config.plugins).slots.memory;
8-
return typeof memorySlot === "string" && memorySlot.trim().length > 0 ? [memorySlot] : [];
10+
const plugins = normalizePluginsConfig(config.plugins);
11+
const memorySlot = plugins.slots.memory;
12+
if (!plugins.enabled || typeof memorySlot !== "string" || memorySlot.trim().length === 0) {
13+
return [];
14+
}
15+
const pluginId = memorySlot.trim();
16+
if (plugins.deny.includes(pluginId) || plugins.entries[pluginId]?.enabled === false) {
17+
return [];
18+
}
19+
return [pluginId];
20+
}
21+
22+
function resolveMemoryRuntimeWorkspaceDir(cfg: OpenClawConfig): string | undefined {
23+
const agentId = resolveDefaultAgentId(cfg);
24+
const dir = resolveAgentWorkspaceDir(cfg, agentId);
25+
if (typeof dir !== "string" || !dir.trim()) {
26+
return undefined;
27+
}
28+
return resolveUserPath(dir);
929
}
1030

1131
function ensureMemoryRuntime(cfg?: OpenClawConfig) {
@@ -18,6 +38,18 @@ function ensureMemoryRuntime(cfg?: OpenClawConfig) {
1838
return getMemoryRuntime();
1939
}
2040
getLoadedRuntimePluginRegistry({ requiredPluginIds: onlyPluginIds });
41+
if (getMemoryRuntime()) {
42+
return getMemoryRuntime();
43+
}
44+
const workspaceDir = resolveMemoryRuntimeWorkspaceDir(cfg);
45+
ensureStandaloneRuntimePluginRegistryLoaded({
46+
requiredPluginIds: onlyPluginIds,
47+
loadOptions: {
48+
config: cfg,
49+
onlyPluginIds,
50+
workspaceDir,
51+
},
52+
});
2153
return getMemoryRuntime();
2254
}
2355

0 commit comments

Comments
 (0)