mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 13:10:43 +00:00
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 heada6a1967316. - Required merge gates passed before the squash merge. Prepared head SHA:a6a1967316Review: https://github.com/openclaw/openclaw/pull/76393#issuecomment-4365255585 Co-authored-by: Neerav Makwana <261249544+neeravmakwana@users.noreply.github.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: clawsweeper <274271284+clawsweeper[bot]@users.noreply.github.com>
This commit is contained in:
@@ -43,6 +43,7 @@ Docs: https://docs.openclaw.ai
|
||||
- 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.
|
||||
- Gateway/chat history: preserve oversized transcript turns as explicit omitted-message placeholders while avoiding large JSONL parse stalls. Thanks @Marvinthebored and @vincentkoc.
|
||||
- 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.
|
||||
- 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.
|
||||
- Gateway: preserve stack diagnostics when `chat.send` or agent attachment parsing/staging fails, improving image-send failure triage. Refs #63432. (#75135) Thanks @keen0206.
|
||||
- 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.
|
||||
- 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.
|
||||
|
||||
@@ -4,6 +4,11 @@ const resolveRuntimePluginRegistryMock =
|
||||
vi.fn<typeof import("./loader.js").resolveRuntimePluginRegistry>();
|
||||
const getLoadedRuntimePluginRegistryMock =
|
||||
vi.fn<typeof import("./active-runtime-registry.js").getLoadedRuntimePluginRegistry>();
|
||||
const ensureStandaloneRuntimePluginRegistryLoadedMock = vi.hoisted(() =>
|
||||
vi.fn<
|
||||
typeof import("./runtime/standalone-runtime-registry-loader.js").ensureStandaloneRuntimePluginRegistryLoaded
|
||||
>(),
|
||||
);
|
||||
const applyPluginAutoEnableMock =
|
||||
vi.fn<typeof import("../config/plugin-auto-enable.js").applyPluginAutoEnable>();
|
||||
const getMemoryRuntimeMock = vi.fn<typeof import("./memory-state.js").getMemoryRuntime>();
|
||||
@@ -30,6 +35,10 @@ vi.mock("./active-runtime-registry.js", () => ({
|
||||
getLoadedRuntimePluginRegistry: getLoadedRuntimePluginRegistryMock,
|
||||
}));
|
||||
|
||||
vi.mock("./runtime/standalone-runtime-registry-loader.js", () => ({
|
||||
ensureStandaloneRuntimePluginRegistryLoaded: ensureStandaloneRuntimePluginRegistryLoadedMock,
|
||||
}));
|
||||
|
||||
vi.mock("./memory-state.js", () => ({
|
||||
getMemoryRuntime: () => getMemoryRuntimeMock(),
|
||||
}));
|
||||
@@ -61,12 +70,25 @@ function createMemoryRuntimeFixture() {
|
||||
};
|
||||
}
|
||||
|
||||
function expectMemoryRuntimeLoaded(rawConfig: unknown, autoEnabledConfig: unknown) {
|
||||
function expectMemoryRuntimeLoaded(
|
||||
rawConfig: unknown,
|
||||
autoEnabledConfig: unknown,
|
||||
pluginIds: readonly string[] = ["memory-core"],
|
||||
) {
|
||||
void rawConfig;
|
||||
void autoEnabledConfig;
|
||||
expect(getLoadedRuntimePluginRegistryMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
requiredPluginIds: ["memory-core"],
|
||||
requiredPluginIds: pluginIds,
|
||||
}),
|
||||
);
|
||||
expect(ensureStandaloneRuntimePluginRegistryLoadedMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
requiredPluginIds: pluginIds,
|
||||
loadOptions: expect.objectContaining({
|
||||
onlyPluginIds: pluginIds,
|
||||
workspaceDir: "/resolved-workspace",
|
||||
}),
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -84,7 +106,10 @@ function setAutoEnabledMemoryRuntime() {
|
||||
changes: [],
|
||||
autoEnabledReasons: {},
|
||||
});
|
||||
getMemoryRuntimeMock.mockReturnValueOnce(undefined).mockReturnValue(runtime);
|
||||
getMemoryRuntimeMock
|
||||
.mockReturnValueOnce(undefined)
|
||||
.mockReturnValueOnce(undefined)
|
||||
.mockReturnValue(runtime);
|
||||
return { rawConfig, autoEnabledConfig, runtime };
|
||||
}
|
||||
|
||||
@@ -92,6 +117,7 @@ function expectNoMemoryRuntimeBootstrap() {
|
||||
expect(applyPluginAutoEnableMock).not.toHaveBeenCalled();
|
||||
expect(resolveRuntimePluginRegistryMock).not.toHaveBeenCalled();
|
||||
expect(getLoadedRuntimePluginRegistryMock).not.toHaveBeenCalled();
|
||||
expect(ensureStandaloneRuntimePluginRegistryLoadedMock).not.toHaveBeenCalled();
|
||||
}
|
||||
|
||||
async function expectAutoEnabledMemoryRuntimeCase(params: {
|
||||
@@ -130,6 +156,7 @@ describe("memory runtime auto-enable loading", () => {
|
||||
} = await import("./memory-runtime.js"));
|
||||
resolveRuntimePluginRegistryMock.mockReset();
|
||||
getLoadedRuntimePluginRegistryMock.mockReset();
|
||||
ensureStandaloneRuntimePluginRegistryLoadedMock.mockReset();
|
||||
applyPluginAutoEnableMock.mockReset();
|
||||
getMemoryRuntimeMock.mockReset();
|
||||
resolveAgentWorkspaceDirMock.mockReset();
|
||||
@@ -179,18 +206,17 @@ describe("memory runtime auto-enable loading", () => {
|
||||
changes: [],
|
||||
autoEnabledReasons: {},
|
||||
});
|
||||
getMemoryRuntimeMock.mockReturnValueOnce(undefined).mockReturnValue(runtime);
|
||||
getMemoryRuntimeMock
|
||||
.mockReturnValueOnce(undefined)
|
||||
.mockReturnValueOnce(undefined)
|
||||
.mockReturnValue(runtime);
|
||||
|
||||
await getActiveMemorySearchManager({
|
||||
cfg: rawConfig as never,
|
||||
agentId: "main",
|
||||
});
|
||||
|
||||
expect(getLoadedRuntimePluginRegistryMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
requiredPluginIds: ["memory-lancedb"],
|
||||
}),
|
||||
);
|
||||
expectMemoryRuntimeLoaded(rawConfig, rawConfig, ["memory-lancedb"]);
|
||||
});
|
||||
|
||||
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", () => {
|
||||
expect(applyPluginAutoEnableMock).not.toHaveBeenCalled();
|
||||
expect(resolveRuntimePluginRegistryMock).not.toHaveBeenCalled();
|
||||
expect(getLoadedRuntimePluginRegistryMock).not.toHaveBeenCalled();
|
||||
expect(ensureStandaloneRuntimePluginRegistryLoadedMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not standalone-load the memory plugin when plugins are globally disabled", async () => {
|
||||
const rawConfig = {
|
||||
plugins: {
|
||||
enabled: false,
|
||||
},
|
||||
};
|
||||
getMemoryRuntimeMock.mockReturnValue(undefined);
|
||||
|
||||
await expect(
|
||||
getActiveMemorySearchManager({
|
||||
cfg: rawConfig as never,
|
||||
agentId: "main",
|
||||
}),
|
||||
).resolves.toEqual({ manager: null, error: "memory plugin unavailable" });
|
||||
|
||||
expectNoMemoryRuntimeBootstrap();
|
||||
});
|
||||
|
||||
it.each([
|
||||
{
|
||||
name: "denied",
|
||||
plugins: {
|
||||
deny: ["memory-core"],
|
||||
slots: {
|
||||
memory: "memory-core",
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "entry-disabled",
|
||||
plugins: {
|
||||
entries: {
|
||||
"memory-core": { enabled: false },
|
||||
},
|
||||
slots: {
|
||||
memory: "memory-core",
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const)("does not standalone-load a $name memory slot plugin", async ({ plugins }) => {
|
||||
getMemoryRuntimeMock.mockReturnValue(undefined);
|
||||
|
||||
await expect(
|
||||
getActiveMemorySearchManager({
|
||||
cfg: { plugins } as never,
|
||||
agentId: "main",
|
||||
}),
|
||||
).resolves.toEqual({ manager: null, error: "memory plugin unavailable" });
|
||||
|
||||
expectNoMemoryRuntimeBootstrap();
|
||||
});
|
||||
|
||||
it("does not standalone-load plugins when the memory runtime is already registered", () => {
|
||||
const rawConfig = {
|
||||
plugins: {
|
||||
slots: {
|
||||
memory: "memory-core",
|
||||
},
|
||||
},
|
||||
};
|
||||
const runtime = createMemoryRuntimeFixture();
|
||||
getLoadedRuntimePluginRegistryMock.mockReturnValue({} as never);
|
||||
getMemoryRuntimeMock.mockReturnValueOnce(undefined).mockReturnValue(runtime);
|
||||
|
||||
resolveActiveMemoryBackendConfig({
|
||||
cfg: rawConfig as never,
|
||||
agentId: "main",
|
||||
});
|
||||
|
||||
expect(getLoadedRuntimePluginRegistryMock).toHaveBeenCalled();
|
||||
expect(ensureStandaloneRuntimePluginRegistryLoadedMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it.each([
|
||||
|
||||
@@ -1,11 +1,31 @@
|
||||
import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||
import type { OpenClawConfig } from "../config/types.openclaw.js";
|
||||
import { resolveUserPath } from "../utils.js";
|
||||
import { getLoadedRuntimePluginRegistry } from "./active-runtime-registry.js";
|
||||
import { normalizePluginsConfig } from "./config-state.js";
|
||||
import { getMemoryRuntime } from "./memory-state.js";
|
||||
import { ensureStandaloneRuntimePluginRegistryLoaded } from "./runtime/standalone-runtime-registry-loader.js";
|
||||
|
||||
function resolveMemoryRuntimePluginIds(config: OpenClawConfig): string[] {
|
||||
const memorySlot = normalizePluginsConfig(config.plugins).slots.memory;
|
||||
return typeof memorySlot === "string" && memorySlot.trim().length > 0 ? [memorySlot] : [];
|
||||
const plugins = normalizePluginsConfig(config.plugins);
|
||||
const memorySlot = plugins.slots.memory;
|
||||
if (!plugins.enabled || typeof memorySlot !== "string" || memorySlot.trim().length === 0) {
|
||||
return [];
|
||||
}
|
||||
const pluginId = memorySlot.trim();
|
||||
if (plugins.deny.includes(pluginId) || plugins.entries[pluginId]?.enabled === false) {
|
||||
return [];
|
||||
}
|
||||
return [pluginId];
|
||||
}
|
||||
|
||||
function resolveMemoryRuntimeWorkspaceDir(cfg: OpenClawConfig): string | undefined {
|
||||
const agentId = resolveDefaultAgentId(cfg);
|
||||
const dir = resolveAgentWorkspaceDir(cfg, agentId);
|
||||
if (typeof dir !== "string" || !dir.trim()) {
|
||||
return undefined;
|
||||
}
|
||||
return resolveUserPath(dir);
|
||||
}
|
||||
|
||||
function ensureMemoryRuntime(cfg?: OpenClawConfig) {
|
||||
@@ -18,6 +38,18 @@ function ensureMemoryRuntime(cfg?: OpenClawConfig) {
|
||||
return getMemoryRuntime();
|
||||
}
|
||||
getLoadedRuntimePluginRegistry({ requiredPluginIds: onlyPluginIds });
|
||||
if (getMemoryRuntime()) {
|
||||
return getMemoryRuntime();
|
||||
}
|
||||
const workspaceDir = resolveMemoryRuntimeWorkspaceDir(cfg);
|
||||
ensureStandaloneRuntimePluginRegistryLoaded({
|
||||
requiredPluginIds: onlyPluginIds,
|
||||
loadOptions: {
|
||||
config: cfg,
|
||||
onlyPluginIds,
|
||||
workspaceDir,
|
||||
},
|
||||
});
|
||||
return getMemoryRuntime();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user