fix(memory-lancedb): retry failed runtime initialization

This commit is contained in:
Peter Steinberger
2026-04-22 21:20:03 +01:00
parent eae0039aa4
commit ee63b9ee49
5 changed files with 230 additions and 10 deletions

View File

@@ -33,6 +33,7 @@ Docs: https://docs.openclaw.ai
- 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.
- 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.
- Memory/LanceDB: retry initialization after a failed LanceDB load and report unsupported Intel macOS native runtime clearly instead of caching the failure or repeatedly attempting an install that cannot work.
- CLI/Claude: hash only static extra system prompt parts when deciding whether to reuse a CLI session, so per-message inbound metadata no longer resets Claude CLI conversations on every turn. (#70122) Thanks @zijunl.
- Hooks/Slack: standardize shared message hook routing fields (`threadId` / `replyToId`) and stop Slack outbound delivery from re-running `message_sending` inside the channel adapter, so plugins like thread-ownership make one outbound routing decision per reply. Thanks @vincentkoc.
- Auto-reply/media: share one run-scoped reply media context between streamed block delivery and final payload filtering, so a local `MEDIA:` attachment is staged once and duplicate media sends are suppressed reliably. (#68111) Thanks @ayeshakhalid192007-dev.

View File

@@ -60,6 +60,8 @@ function createRuntimeLoader(
env?: NodeJS.ProcessEnv;
importBundled?: () => Promise<LanceDbModule>;
importResolved?: (resolvedPath: string) => Promise<LanceDbModule>;
platform?: NodeJS.Platform;
arch?: NodeJS.Architecture;
resolveRuntimeEntry?: (params: {
runtimeDir: string;
manifest: RuntimeManifest;
@@ -74,6 +76,8 @@ function createRuntimeLoader(
) {
return createLanceDbRuntimeLoader({
env: overrides.env ?? ({} as NodeJS.ProcessEnv),
platform: overrides.platform,
arch: overrides.arch,
resolveStateDir: () => "/tmp/openclaw-state",
runtimeManifest: TEST_RUNTIME_MANIFEST,
importBundled:
@@ -832,6 +836,100 @@ describe("memory plugin e2e", () => {
}
});
test("clears failed database initialization so later tool calls can retry", async () => {
const embeddingsCreate = vi.fn(async () => ({
data: [{ embedding: [0.1, 0.2, 0.3] }],
}));
const ensureGlobalUndiciEnvProxyDispatcher = vi.fn();
const toArray = vi.fn(async () => []);
const limit = vi.fn(() => ({ toArray }));
const vectorSearch = vi.fn(() => ({ limit }));
const loadLanceDbModule = vi
.fn()
.mockRejectedValueOnce(new Error("temporary LanceDB install failure"))
.mockResolvedValueOnce({
connect: vi.fn(async () => ({
tableNames: vi.fn(async () => ["memories"]),
openTable: vi.fn(async () => ({
vectorSearch,
countRows: vi.fn(async () => 0),
add: vi.fn(async () => undefined),
delete: vi.fn(async () => undefined),
})),
})),
});
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 registeredTools: any[] = [];
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: false,
},
runtime: {},
logger: {
info: vi.fn(),
warn: vi.fn(),
error: vi.fn(),
debug: vi.fn(),
},
registerTool: (tool: any, opts: any) => {
registeredTools.push({ tool, opts });
},
registerCli: vi.fn(),
registerService: vi.fn(),
on: vi.fn(),
resolvePath: (p: string) => p,
};
dynamicMemoryPlugin.register(mockApi as any);
const recallTool = registeredTools.find((t) => t.opts?.name === "memory_recall")?.tool;
if (!recallTool) {
throw new Error("memory_recall tool was not registered");
}
await expect(recallTool.execute("test-call-retry-1", { query: "hello" })).rejects.toThrow(
"temporary LanceDB install failure",
);
await expect(
recallTool.execute("test-call-retry-2", { query: "hello again" }),
).resolves.toMatchObject({
details: { count: 0 },
});
expect(loadLanceDbModule).toHaveBeenCalledTimes(2);
expect(embeddingsCreate).toHaveBeenCalledTimes(2);
} finally {
vi.doUnmock("openclaw/plugin-sdk/runtime-env");
vi.doUnmock("openai");
vi.doUnmock("./lancedb-runtime.js");
vi.resetModules();
}
});
test("config schema accepts storageOptions with string values", async () => {
const { default: memoryPlugin } = await import("./index.js");
@@ -1067,6 +1165,23 @@ describe("lancedb runtime loader", () => {
expect(installRuntime).not.toHaveBeenCalled();
});
test("fails clearly on Intel macOS instead of attempting an unsupported native install", async () => {
const installRuntime = vi.fn(
async ({ runtimeDir }: { runtimeDir: string }) =>
`${runtimeDir}/node_modules/@lancedb/lancedb/index.js`,
);
const loader = createRuntimeLoader({
platform: "darwin",
arch: "x64",
installRuntime,
});
await expect(loader.load()).rejects.toThrow(
"memory-lancedb: LanceDB runtime is unavailable on darwin-x64.",
);
expect(installRuntime).not.toHaveBeenCalled();
});
test("clears the cached failure so later calls can retry the install", async () => {
const runtimeModule = createMockModule();
const installRuntime = vi

View File

@@ -71,7 +71,10 @@ class MemoryDB {
return this.initPromise;
}
this.initPromise = this.doInitialize();
this.initPromise = this.doInitialize().catch((error) => {
this.initPromise = null;
throw error;
});
return this.initPromise;
}

View File

@@ -28,6 +28,8 @@ type ReadPackageJson = (manifestPath: string) => PackageJsonWithDependencies | n
type LanceDbRuntimeLoaderDeps = {
env: NodeJS.ProcessEnv;
platform: NodeJS.Platform;
arch: NodeJS.Architecture;
resolveStateDir: (env?: NodeJS.ProcessEnv, homedir?: () => string) => string;
runtimeManifest: RuntimeManifest;
importBundled: () => Promise<LanceDbModule>;
@@ -218,11 +220,31 @@ function buildLoadFailureMessage(prefix: string, error: unknown): string {
return `memory-lancedb: ${prefix}. ${String(error)}`;
}
function isUnsupportedNativePlatform(params: {
platform: NodeJS.Platform;
arch: NodeJS.Architecture;
}): boolean {
return params.platform === "darwin" && params.arch === "x64";
}
function buildUnsupportedNativePlatformMessage(params: {
platform: NodeJS.Platform;
arch: NodeJS.Architecture;
}): string {
return [
`memory-lancedb: LanceDB runtime is unavailable on ${params.platform}-${params.arch}.`,
"The bundled @lancedb/lancedb dependency does not publish a native package for this platform.",
"Disable memory-lancedb or switch to a supported memory backend/platform.",
].join(" ");
}
export function createLanceDbRuntimeLoader(overrides: Partial<LanceDbRuntimeLoaderDeps> = {}): {
load: (logger?: LanceDbRuntimeLogger) => Promise<LanceDbModule>;
} {
const deps: LanceDbRuntimeLoaderDeps = {
env: overrides.env ?? process.env,
platform: overrides.platform ?? process.platform,
arch: overrides.arch ?? process.arch,
resolveStateDir: overrides.resolveStateDir ?? resolveStateDir,
runtimeManifest: overrides.runtimeManifest ?? MEMORY_LANCEDB_RUNTIME_MANIFEST,
importBundled: overrides.importBundled ?? (() => import("@lancedb/lancedb")),
@@ -240,6 +262,15 @@ export function createLanceDbRuntimeLoader(overrides: Partial<LanceDbRuntimeLoad
try {
return await deps.importBundled();
} catch (bundledError) {
if (isUnsupportedNativePlatform({ platform: deps.platform, arch: deps.arch })) {
throw new Error(
buildUnsupportedNativePlatformMessage({
platform: deps.platform,
arch: deps.arch,
}),
{ cause: bundledError },
);
}
const runtimeDir = resolveRuntimeDir(
deps.resolveStateDir(deps.env, () =>
deps.env.HOME?.trim() ? deps.env.HOME : os.homedir(),

View File

@@ -65,6 +65,7 @@ test -d "$package_root/dist/extensions/telegram"
test -d "$package_root/dist/extensions/discord"
test -d "$package_root/dist/extensions/slack"
test -d "$package_root/dist/extensions/feishu"
test -d "$package_root/dist/extensions/memory-lancedb"
if [ -d "$package_root/dist/extensions/$CHANNEL/node_modules" ]; then
echo "$CHANNEL runtime deps should not be preinstalled in package" >&2
@@ -156,6 +157,35 @@ if (mode === "feishu") {
},
};
}
if (mode === "memory-lancedb") {
config.plugins = {
...(config.plugins || {}),
enabled: true,
allow: [...new Set([...(config.plugins?.allow || []), "memory-lancedb"])],
slots: {
...(config.plugins?.slots || {}),
memory: "memory-lancedb",
},
entries: {
...(config.plugins?.entries || {}),
"memory-lancedb": {
...(config.plugins?.entries?.["memory-lancedb"] || {}),
enabled: true,
config: {
...(config.plugins?.entries?.["memory-lancedb"]?.config || {}),
embedding: {
...(config.plugins?.entries?.["memory-lancedb"]?.config?.embedding || {}),
apiKey: process.env.OPENAI_API_KEY,
model: "text-embedding-3-small",
},
dbPath: "~/.openclaw/memory/lancedb-e2e",
autoCapture: false,
autoRecall: false,
},
},
},
};
}
fs.mkdirSync(path.dirname(configPath), { recursive: true });
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
@@ -206,6 +236,10 @@ wait_for_gateway_health() {
assert_channel_status() {
local channel="$1"
if [ "$channel" = "memory-lancedb" ]; then
echo "memory-lancedb plugin activation verified by dependency sentinel"
return 0
fi
local out="/tmp/openclaw-channel-status-$channel.json"
openclaw gateway call channels.status \
--url "ws://127.0.0.1:$PORT" \
@@ -635,7 +669,6 @@ export OPENAI_API_KEY="sk-openclaw-bundled-channel-update-e2e"
export OPENCLAW_NO_ONBOARD=1
export OPENCLAW_UPDATE_PACKAGE_SPEC=""
BASELINE_VERSION="${OPENCLAW_BUNDLED_CHANNEL_UPDATE_BASELINE_VERSION:?missing baseline version}"
TOKEN="bundled-channel-update-token"
PORT="18790"
@@ -736,6 +769,35 @@ config.channels = {
enabled: mode === "feishu",
},
};
if (mode === "memory-lancedb") {
config.plugins = {
...(config.plugins || {}),
enabled: true,
allow: [...new Set([...(config.plugins?.allow || []), "memory-lancedb"])],
slots: {
...(config.plugins?.slots || {}),
memory: "memory-lancedb",
},
entries: {
...(config.plugins?.entries || {}),
"memory-lancedb": {
...(config.plugins?.entries?.["memory-lancedb"] || {}),
enabled: true,
config: {
...(config.plugins?.entries?.["memory-lancedb"]?.config || {}),
embedding: {
...(config.plugins?.entries?.["memory-lancedb"]?.config?.embedding || {}),
apiKey: process.env.OPENAI_API_KEY,
model: "text-embedding-3-small",
},
dbPath: "~/.openclaw/memory/lancedb-update-e2e",
autoCapture: false,
autoRecall: false,
},
},
},
};
}
fs.mkdirSync(path.dirname(configPath), { recursive: true });
fs.writeFileSync(configPath, `${JSON.stringify(config, null, 2)}\n`, "utf8");
@@ -857,8 +919,8 @@ run_update_and_capture() {
fi
}
echo "Installing known-bad baseline $BASELINE_VERSION..."
npm install -g "openclaw@$BASELINE_VERSION" --omit=optional --no-fund --no-audit >/tmp/openclaw-update-baseline-install.log 2>&1
echo "Installing current candidate as update baseline..."
npm install -g "$package_tgz" --no-fund --no-audit >/tmp/openclaw-update-baseline-install.log 2>&1
command -v openclaw >/dev/null
baseline_root="$(package_root)"
test -d "$baseline_root/dist/extensions/telegram"
@@ -871,16 +933,14 @@ set +e
openclaw doctor --non-interactive >/tmp/openclaw-baseline-doctor.log 2>&1
baseline_doctor_status=$?
set -e
if [ "$baseline_doctor_status" -eq 0 ] || ! grep -Eq "grammy|ERR_MODULE_NOT_FOUND|Cannot find module" /tmp/openclaw-baseline-doctor.log; then
echo "expected baseline doctor to fail on missing Telegram runtime deps" >&2
cat /tmp/openclaw-baseline-doctor.log >&2
exit 1
fi
echo "baseline doctor exited with $baseline_doctor_status"
remove_runtime_dep telegram grammy
assert_no_dep_available telegram grammy
echo "Updating from baseline to current candidate; candidate doctor must repair Telegram deps..."
run_update_and_capture telegram /tmp/openclaw-update-telegram.json
cat /tmp/openclaw-update-telegram.json
assert_update_ok /tmp/openclaw-update-telegram.json "$BASELINE_VERSION"
assert_update_ok /tmp/openclaw-update-telegram.json "$candidate_version"
assert_dep_available telegram grammy
echo "Mutating installed package: remove Telegram deps, then update-mode doctor repairs them..."
@@ -920,6 +980,15 @@ cat /tmp/openclaw-update-feishu.json
assert_update_ok /tmp/openclaw-update-feishu.json "$candidate_version"
assert_dep_available feishu @larksuiteoapi/node-sdk
echo "Mutating config to memory-lancedb and rerunning same-version update path..."
write_config memory-lancedb
remove_runtime_dep memory-lancedb @lancedb/lancedb
assert_no_dep_available memory-lancedb @lancedb/lancedb
run_update_and_capture memory-lancedb /tmp/openclaw-update-memory-lancedb.json
cat /tmp/openclaw-update-memory-lancedb.json
assert_update_ok /tmp/openclaw-update-memory-lancedb.json "$candidate_version"
assert_dep_available memory-lancedb @lancedb/lancedb
echo "bundled channel runtime deps Docker update E2E passed"
EOF
then
@@ -937,6 +1006,7 @@ if [ "$RUN_CHANNEL_SCENARIOS" != "0" ]; then
run_channel_scenario discord discord-api-types
run_channel_scenario slack @slack/web-api
run_channel_scenario feishu @larksuiteoapi/node-sdk
run_channel_scenario memory-lancedb @lancedb/lancedb
fi
if [ "$RUN_UPDATE_SCENARIO" != "0" ]; then
run_update_scenario