fix(cli): close standalone memory managers after commands (#118500)

This commit is contained in:
Peter Steinberger
2026-08-02 22:57:59 -07:00
committed by GitHub
parent d7ab20a727
commit 133323875f
3 changed files with 86 additions and 2 deletions
+68
View File
@@ -31,6 +31,7 @@ import {
resolveActiveMemoryBackendConfig,
} from "./memory-runtime.js";
import { resetStandaloneMemoryRegistrySlot } from "./memory-runtime.test-support.js";
import { hasMemoryRuntime } from "./memory-state.js";
function createRuntime() {
return {
@@ -95,6 +96,73 @@ describe("memory runtime handles", () => {
});
});
it("tracks standalone managers without activating config-only lookups and rearms reused handles", async () => {
const { registry, runtime } = createRegistry();
mocks.loadPluginRegistryHandle.mockReturnValue(registry);
expect(hasMemoryRuntime()).toBe(false);
expect(resolveActiveMemoryBackendConfig({ cfg: memoryConfig, agentId: "main" })).toEqual({
backend: "builtin",
});
expect(hasMemoryRuntime()).toBe(false);
await getActiveMemorySearchManager({ cfg: memoryConfig, agentId: "main" });
expect(hasMemoryRuntime()).toBe(true);
await closeActiveMemorySearchManagers();
expect(hasMemoryRuntime()).toBe(false);
await getActiveMemorySearchManager({ cfg: memoryConfig, agentId: "main" });
expect(hasMemoryRuntime()).toBe(true);
expect(mocks.loadPluginRegistryHandle).toHaveBeenCalledTimes(1);
await closeActiveMemorySearchManagers();
expect(runtime.closeAllMemorySearchManagers).toHaveBeenCalledTimes(2);
expect(hasMemoryRuntime()).toBe(false);
});
it("retains standalone ownership across workspace replacement and per-agent cleanup", async () => {
const main = createRegistry();
const research = createRegistry();
mocks.loadPluginRegistryHandle
.mockReturnValueOnce(main.registry)
.mockReturnValueOnce(research.registry);
await getActiveMemorySearchManager({ cfg: memoryConfig, agentId: "main" });
await getActiveMemorySearchManager({ cfg: memoryConfig, agentId: "research" });
expect(hasMemoryRuntime()).toBe(true);
await closeActiveMemorySearchManager({ cfg: memoryConfig, agentId: "main" });
expect(hasMemoryRuntime()).toBe(true);
await closeActiveMemorySearchManagers();
expect(main.runtime.closeAllMemorySearchManagers).toHaveBeenCalledTimes(1);
expect(research.runtime.closeAllMemorySearchManagers).toHaveBeenCalledTimes(1);
expect(hasMemoryRuntime()).toBe(false);
});
it("retains standalone cleanup ownership when manager acquisition or teardown fails", async () => {
const { registry, runtime } = createRegistry();
mocks.loadPluginRegistryHandle.mockReturnValue(registry);
runtime.getMemorySearchManager.mockRejectedValueOnce(
new Error("manager initialization failed"),
);
await expect(
getActiveMemorySearchManager({ cfg: memoryConfig, agentId: "main" }),
).rejects.toThrow("manager initialization failed");
expect(hasMemoryRuntime()).toBe(true);
runtime.closeAllMemorySearchManagers.mockRejectedValueOnce(
new Error("manager teardown failed"),
);
await expect(closeActiveMemorySearchManagers()).rejects.toThrow("manager teardown failed");
expect(hasMemoryRuntime()).toBe(true);
await closeActiveMemorySearchManagers();
expect(hasMemoryRuntime()).toBe(false);
});
it("keys the single slot by the requesting agent workspace", () => {
const main = createRegistry();
const research = createRegistry();
+10 -1
View File
@@ -4,7 +4,11 @@ import type { OpenClawConfig } from "../config/types.openclaw.js";
import { resolveUserPath } from "../utils.js";
import { normalizePluginsConfig } from "./config-state.js";
import { loadPluginRegistryHandle, resolvePluginRegistryLoadCacheKey } from "./loader.js";
import { getMemoryRuntime, resolveMemoryCapabilityRegistration } from "./memory-state.js";
import {
getMemoryRuntime,
resolveMemoryCapabilityRegistration,
setStandaloneMemoryManagerActive,
} from "./memory-state.js";
import type { PluginRegistry } from "./registry-types.js";
import { withPluginRuntimeRegistryScope } from "./runtime/gateway-request-scope.js";
@@ -121,6 +125,9 @@ export async function getActiveMemorySearchManager(params: {
if (!owner) {
return { manager: null, error: "memory plugin unavailable" };
}
if (owner.registry) {
setStandaloneMemoryManagerActive(true);
}
return await withMemoryRuntimeOwner(
owner,
async (runtime) => await runtime.getMemorySearchManager(params),
@@ -146,6 +153,7 @@ export async function closeActiveMemorySearchManagers(cfg?: OpenClawConfig): Pro
),
);
standaloneMemoryRegistrySlot?.retiredRuntimes.clear();
setStandaloneMemoryManagerActive(false);
}
/** Closes the plugin-backed memory search manager for one agent. */
@@ -164,6 +172,7 @@ export async function closeActiveMemorySearchManager(params: {
function resetStandaloneMemoryRegistrySlot(): void {
standaloneMemoryRegistrySlot = undefined;
setStandaloneMemoryManagerActive(false);
}
if (process.env.VITEST || process.env.NODE_ENV === "test") {
+8 -1
View File
@@ -266,8 +266,15 @@ export function getMemoryRuntime(): MemoryPluginRuntime | undefined {
return getMemoryCapability()?.capability.runtime;
}
let standaloneMemoryManagerActive = false;
// Standalone managers are intentionally absent from the active plugin registry.
export function setStandaloneMemoryManagerActive(active: boolean): void {
standaloneMemoryManagerActive = active;
}
export function hasMemoryRuntime(): boolean {
return getMemoryRuntime() !== undefined;
return standaloneMemoryManagerActive || getMemoryRuntime() !== undefined;
}
function cloneMemoryPublicArtifact(