mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 12:00:44 +00:00
fix(hooks): avoid stale lancedb startup fallback
This commit is contained in:
@@ -57,6 +57,7 @@ Docs: https://docs.openclaw.ai
|
||||
- CLI sessions: persist CLI session clearing through the atomic session-store merge path, so expired Claude/Codex CLI bindings are actually removed before retrying without the stale session id. (#70298) Thanks @HFConsultant.
|
||||
- ACP/sessions_spawn: honor explicit `model` overrides for ACP child sessions instead of silently falling back to the target agent default model. (#70210) Thanks @felix-miao.
|
||||
- Diffs/viewer: re-read remote viewer access policy from live runtime config on each request, so toggling `plugins.entries.diffs.config.security.allowRemoteViewer` closes proxied viewer access immediately instead of waiting for a restart. Thanks @vincentkoc.
|
||||
- Memory/LanceDB: stop resurrecting removed live `memory-lancedb` hook config from startup snapshots, so deleting or disabling the plugin entry shuts off auto-recall and auto-capture without a restart. Thanks @vincentkoc.
|
||||
- Agents/subagents: drop bare `NO_REPLY` from the parent turn when the session still has pending spawned children, so direct-conversation surfaces such as Telegram DMs no longer rewrite the sentinel into visible fallback chatter while waiting for the child completion event. (#69942) Thanks @neeravmakwana.
|
||||
- Plugins/install: keep bundled plugin dependencies off npm install while repairing them when plugins activate from a packaged install, including Feishu/Lark, Browser, and direct bundled channel setup-entry loads.
|
||||
- CLI/channels: skip and cache bundled channel plugin, setup, and secrets load failures during read-only discovery, so one broken unused bundled channel cannot crash `openclaw status` or bootstrap secret scans.
|
||||
|
||||
@@ -511,6 +511,117 @@ describe("memory plugin e2e", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("fails closed for auto-recall when the live plugin entry is removed", async () => {
|
||||
const embeddingsCreate = vi.fn(async () => ({
|
||||
data: [{ embedding: [0.1, 0.2, 0.3] }],
|
||||
}));
|
||||
const ensureGlobalUndiciEnvProxyDispatcher = vi.fn();
|
||||
const loadLanceDbModule = vi.fn(async () => ({
|
||||
connect: vi.fn(async () => ({
|
||||
tableNames: vi.fn(async () => ["memories"]),
|
||||
openTable: vi.fn(async () => ({
|
||||
vectorSearch: vi.fn(() => ({ limit: vi.fn(() => ({ toArray: vi.fn(async () => []) })) })),
|
||||
countRows: vi.fn(async () => 0),
|
||||
add: vi.fn(async () => undefined),
|
||||
delete: vi.fn(async () => undefined),
|
||||
})),
|
||||
})),
|
||||
}));
|
||||
let configFile: Record<string, unknown> = {
|
||||
plugins: {
|
||||
entries: {
|
||||
"memory-lancedb": {
|
||||
config: {
|
||||
embedding: {
|
||||
apiKey: OPENAI_API_KEY,
|
||||
model: "text-embedding-3-small",
|
||||
},
|
||||
dbPath: getDbPath(),
|
||||
autoCapture: false,
|
||||
autoRecall: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
vi.resetModules();
|
||||
vi.doMock("openclaw/plugin-sdk/runtime-env", () => ({
|
||||
ensureGlobalUndiciEnvProxyDispatcher,
|
||||
}));
|
||||
vi.doMock("openai", () => ({
|
||||
default: class MockOpenAI {
|
||||
embeddings = { create: embeddingsCreate };
|
||||
},
|
||||
}));
|
||||
vi.doMock("./lancedb-runtime.js", () => ({
|
||||
loadLanceDbModule,
|
||||
}));
|
||||
|
||||
try {
|
||||
const { default: dynamicMemoryPlugin } = await import("./index.js");
|
||||
const on = vi.fn();
|
||||
const mockApi = {
|
||||
id: "memory-lancedb",
|
||||
name: "Memory (LanceDB)",
|
||||
source: "test",
|
||||
config: {},
|
||||
pluginConfig: {
|
||||
embedding: {
|
||||
apiKey: OPENAI_API_KEY,
|
||||
model: "text-embedding-3-small",
|
||||
},
|
||||
dbPath: getDbPath(),
|
||||
autoCapture: false,
|
||||
autoRecall: true,
|
||||
},
|
||||
runtime: {
|
||||
config: {
|
||||
loadConfig: () => configFile,
|
||||
},
|
||||
},
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
},
|
||||
registerTool: vi.fn(),
|
||||
registerCli: vi.fn(),
|
||||
registerService: vi.fn(),
|
||||
on,
|
||||
resolvePath: (p: string) => p,
|
||||
};
|
||||
|
||||
dynamicMemoryPlugin.register(mockApi as any);
|
||||
|
||||
configFile = {
|
||||
plugins: {
|
||||
entries: {},
|
||||
},
|
||||
};
|
||||
|
||||
const beforePromptBuild = on.mock.calls.find(
|
||||
([hookName]) => hookName === "before_prompt_build",
|
||||
)?.[1];
|
||||
expect(beforePromptBuild).toBeTypeOf("function");
|
||||
|
||||
const result = await beforePromptBuild?.(
|
||||
{ prompt: "what editor should i use after memory is removed?", messages: [] },
|
||||
{},
|
||||
);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
expect(embeddingsCreate).not.toHaveBeenCalled();
|
||||
expect(loadLanceDbModule).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
vi.doUnmock("openclaw/plugin-sdk/runtime-env");
|
||||
vi.doUnmock("openai");
|
||||
vi.doUnmock("./lancedb-runtime.js");
|
||||
vi.resetModules();
|
||||
}
|
||||
});
|
||||
|
||||
test("runs auto-capture through the registered agent_end hook", async () => {
|
||||
const embeddingsCreate = vi.fn(async () => ({
|
||||
data: [{ embedding: [0.1, 0.2, 0.3] }],
|
||||
@@ -744,6 +855,119 @@ describe("memory plugin e2e", () => {
|
||||
}
|
||||
});
|
||||
|
||||
test("fails closed for auto-capture when the live plugin entry is removed", async () => {
|
||||
const embeddingsCreate = vi.fn(async () => ({
|
||||
data: [{ embedding: [0.1, 0.2, 0.3] }],
|
||||
}));
|
||||
const ensureGlobalUndiciEnvProxyDispatcher = vi.fn();
|
||||
const add = vi.fn(async () => undefined);
|
||||
const loadLanceDbModule = vi.fn(async () => ({
|
||||
connect: vi.fn(async () => ({
|
||||
tableNames: vi.fn(async () => ["memories"]),
|
||||
openTable: vi.fn(async () => ({
|
||||
vectorSearch: vi.fn(() => ({ limit: vi.fn(() => ({ toArray: vi.fn(async () => []) })) })),
|
||||
countRows: vi.fn(async () => 0),
|
||||
add,
|
||||
delete: vi.fn(async () => undefined),
|
||||
})),
|
||||
})),
|
||||
}));
|
||||
let configFile: Record<string, unknown> = {
|
||||
plugins: {
|
||||
entries: {
|
||||
"memory-lancedb": {
|
||||
config: {
|
||||
embedding: {
|
||||
apiKey: OPENAI_API_KEY,
|
||||
model: "text-embedding-3-small",
|
||||
},
|
||||
dbPath: getDbPath(),
|
||||
autoCapture: true,
|
||||
autoRecall: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
vi.resetModules();
|
||||
vi.doMock("openclaw/plugin-sdk/runtime-env", () => ({
|
||||
ensureGlobalUndiciEnvProxyDispatcher,
|
||||
}));
|
||||
vi.doMock("openai", () => ({
|
||||
default: class MockOpenAI {
|
||||
embeddings = { create: embeddingsCreate };
|
||||
},
|
||||
}));
|
||||
vi.doMock("./lancedb-runtime.js", () => ({
|
||||
loadLanceDbModule,
|
||||
}));
|
||||
|
||||
try {
|
||||
const { default: dynamicMemoryPlugin } = await import("./index.js");
|
||||
const on = vi.fn();
|
||||
const mockApi = {
|
||||
id: "memory-lancedb",
|
||||
name: "Memory (LanceDB)",
|
||||
source: "test",
|
||||
config: {},
|
||||
pluginConfig: {
|
||||
embedding: {
|
||||
apiKey: OPENAI_API_KEY,
|
||||
model: "text-embedding-3-small",
|
||||
},
|
||||
dbPath: getDbPath(),
|
||||
autoCapture: true,
|
||||
autoRecall: false,
|
||||
},
|
||||
runtime: {
|
||||
config: {
|
||||
loadConfig: () => configFile,
|
||||
},
|
||||
},
|
||||
logger: {
|
||||
info: vi.fn(),
|
||||
warn: vi.fn(),
|
||||
error: vi.fn(),
|
||||
debug: vi.fn(),
|
||||
},
|
||||
registerTool: vi.fn(),
|
||||
registerCli: vi.fn(),
|
||||
registerService: vi.fn(),
|
||||
on,
|
||||
resolvePath: (p: string) => p,
|
||||
};
|
||||
|
||||
dynamicMemoryPlugin.register(mockApi as any);
|
||||
|
||||
configFile = {
|
||||
plugins: {
|
||||
entries: {},
|
||||
},
|
||||
};
|
||||
|
||||
const agentEnd = on.mock.calls.find(([hookName]) => hookName === "agent_end")?.[1];
|
||||
expect(agentEnd).toBeTypeOf("function");
|
||||
|
||||
await agentEnd?.(
|
||||
{
|
||||
success: true,
|
||||
messages: [{ role: "user", content: "I prefer Helix for editing code every day." }],
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
expect(embeddingsCreate).not.toHaveBeenCalled();
|
||||
expect(loadLanceDbModule).not.toHaveBeenCalled();
|
||||
expect(add).not.toHaveBeenCalled();
|
||||
} finally {
|
||||
vi.doUnmock("openclaw/plugin-sdk/runtime-env");
|
||||
vi.doUnmock("openai");
|
||||
vi.doUnmock("./lancedb-runtime.js");
|
||||
vi.resetModules();
|
||||
}
|
||||
});
|
||||
|
||||
test("passes configured dimensions to OpenAI embeddings API", async () => {
|
||||
const embeddingsCreate = vi.fn(async () => ({
|
||||
data: [{ embedding: [0.1, 0.2, 0.3] }],
|
||||
|
||||
@@ -10,7 +10,7 @@ import { randomUUID } from "node:crypto";
|
||||
import type * as LanceDB from "@lancedb/lancedb";
|
||||
import { Type } from "@sinclair/typebox";
|
||||
import OpenAI from "openai";
|
||||
import { resolvePluginConfigObject } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { resolveLivePluginConfigObject } from "openclaw/plugin-sdk/config-runtime";
|
||||
import { ensureGlobalUndiciEnvProxyDispatcher } from "openclaw/plugin-sdk/runtime-env";
|
||||
import { normalizeLowercaseStringOrEmpty } from "openclaw/plugin-sdk/text-runtime";
|
||||
import { definePluginEntry, type OpenClawPluginApi } from "./api.js";
|
||||
@@ -306,15 +306,19 @@ export default definePluginEntry({
|
||||
const dbPath = cfg.dbPath!;
|
||||
const resolvedDbPath = dbPath.includes("://") ? dbPath : api.resolvePath(dbPath);
|
||||
const { model, dimensions, apiKey, baseUrl } = cfg.embedding;
|
||||
const disabledHookCfg = { ...cfg, autoCapture: false, autoRecall: false };
|
||||
|
||||
const vectorDim = dimensions ?? vectorDimsForModel(model);
|
||||
const db = new MemoryDB(resolvedDbPath, vectorDim, cfg.storageOptions);
|
||||
const embeddings = new Embeddings(apiKey, model, baseUrl, dimensions);
|
||||
const resolveCurrentHookConfig = () => {
|
||||
const runtimeConfig = api.runtime.config?.loadConfig?.();
|
||||
const runtimePluginConfig = resolvePluginConfigObject(runtimeConfig, "memory-lancedb");
|
||||
const runtimePluginConfig = resolveLivePluginConfigObject(
|
||||
api.runtime.config?.loadConfig,
|
||||
"memory-lancedb",
|
||||
api.pluginConfig as Record<string, unknown>,
|
||||
);
|
||||
if (!runtimePluginConfig) {
|
||||
return cfg;
|
||||
return disabledHookCfg;
|
||||
}
|
||||
return memoryConfigSchema.parse({
|
||||
embedding: {
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { resolvePluginConfigObject, type OpenClawConfig } from "./config-runtime.js";
|
||||
import {
|
||||
resolveLivePluginConfigObject,
|
||||
resolvePluginConfigObject,
|
||||
type OpenClawConfig,
|
||||
} from "./config-runtime.js";
|
||||
|
||||
describe("resolvePluginConfigObject", () => {
|
||||
it("returns the plugin config object for a configured plugin entry", () => {
|
||||
@@ -45,3 +49,29 @@ describe("resolvePluginConfigObject", () => {
|
||||
expect(resolvePluginConfigObject(undefined, "demo-plugin")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveLivePluginConfigObject", () => {
|
||||
it("falls back to startup config only when no runtime loader exists", () => {
|
||||
expect(
|
||||
resolveLivePluginConfigObject(undefined, "demo-plugin", {
|
||||
enabled: true,
|
||||
}),
|
||||
).toEqual({
|
||||
enabled: true,
|
||||
});
|
||||
});
|
||||
|
||||
it("fails closed when the runtime loader exists but the plugin entry is missing", () => {
|
||||
const config = {
|
||||
plugins: {
|
||||
entries: {},
|
||||
},
|
||||
} as unknown as OpenClawConfig;
|
||||
|
||||
expect(
|
||||
resolveLivePluginConfigObject(() => config, "demo-plugin", {
|
||||
enabled: true,
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -34,6 +34,17 @@ export function resolvePluginConfigObject(
|
||||
: undefined;
|
||||
}
|
||||
|
||||
export function resolveLivePluginConfigObject(
|
||||
runtimeConfigLoader: (() => OpenClawConfig | undefined) | undefined,
|
||||
pluginId: string,
|
||||
startupPluginConfig?: Record<string, unknown>,
|
||||
): Record<string, unknown> | undefined {
|
||||
if (typeof runtimeConfigLoader !== "function") {
|
||||
return startupPluginConfig;
|
||||
}
|
||||
return resolvePluginConfigObject(runtimeConfigLoader(), pluginId);
|
||||
}
|
||||
|
||||
export { resolveDefaultAgentId } from "../agents/agent-scope.js";
|
||||
export {
|
||||
clearRuntimeConfigSnapshot,
|
||||
|
||||
@@ -60,10 +60,7 @@ const BUNDLED_LIVE_CONFIG_HOOK_GUARDS = {
|
||||
"resolveMemoryCorePluginConfig(startupCfg)",
|
||||
"api.runtime.config?.loadConfig?.() ?? api.config",
|
||||
],
|
||||
"extensions/memory-lancedb/index.ts": [
|
||||
'resolvePluginConfigObject(runtimeConfig, "memory-lancedb")',
|
||||
"api.runtime.config?.loadConfig?.()",
|
||||
],
|
||||
"extensions/memory-lancedb/index.ts": ["resolveLivePluginConfigObject(", '"memory-lancedb"'],
|
||||
"extensions/skill-workshop/index.ts": [
|
||||
'resolvePluginConfigObject(runtimeConfig, "skill-workshop")',
|
||||
'typeof api.runtime.config?.loadConfig === "function"',
|
||||
|
||||
Reference in New Issue
Block a user