perf(memory): avoid eager provider init on empty search

This commit is contained in:
Vincent Koc 2026-03-24 13:01:04 -07:00
parent db0f957aba
commit 7330e2ce23
4 changed files with 68 additions and 20 deletions

View File

@ -19,6 +19,8 @@ export type BaseConfigSchemaResponse = {
generatedAt: string;
};
type BaseConfigSchemaStablePayload = Omit<BaseConfigSchemaResponse, "generatedAt">;
function cloneSchema<T>(value: T): T {
if (typeof structuredClone === "function") {
return structuredClone(value);
@ -54,19 +56,42 @@ function stripChannelSchema(schema: ConfigSchema): ConfigSchema {
return next;
}
export function computeBaseConfigSchemaResponse(params?: {
generatedAt?: string;
}): BaseConfigSchemaResponse {
let baseConfigSchemaStablePayload: BaseConfigSchemaStablePayload | null = null;
function computeBaseConfigSchemaStablePayload(): BaseConfigSchemaStablePayload {
if (baseConfigSchemaStablePayload) {
return {
schema: cloneSchema(baseConfigSchemaStablePayload.schema),
uiHints: cloneSchema(baseConfigSchemaStablePayload.uiHints),
version: baseConfigSchemaStablePayload.version,
};
}
const schema = OpenClawSchema.toJSONSchema({
target: "draft-07",
unrepresentable: "any",
});
schema.title = "OpenClawConfig";
const hints = applyDerivedTags(mapSensitivePaths(OpenClawSchema, "", buildBaseHints()));
return {
const stablePayload = {
schema: stripChannelSchema(schema),
uiHints: hints,
uiHints: applyDerivedTags(mapSensitivePaths(OpenClawSchema, "", buildBaseHints())),
version: VERSION,
} satisfies BaseConfigSchemaStablePayload;
baseConfigSchemaStablePayload = stablePayload;
return {
schema: cloneSchema(stablePayload.schema),
uiHints: cloneSchema(stablePayload.uiHints),
version: stablePayload.version,
};
}
export function computeBaseConfigSchemaResponse(params?: {
generatedAt?: string;
}): BaseConfigSchemaResponse {
const stablePayload = computeBaseConfigSchemaStablePayload();
return {
schema: stablePayload.schema,
uiHints: stablePayload.uiHints,
version: stablePayload.version,
generatedAt: params?.generatedAt ?? new Date().toISOString(),
};
}

View File

@ -957,6 +957,7 @@ describe("memory index", () => {
const result = await getMemorySearchManager({ cfg, agentId: "main" });
const manager = requireManager(result);
await manager.probeEmbeddingAvailability();
expect(
providerCalls.some(

View File

@ -93,17 +93,15 @@ describe("memory manager cache hydration", () => {
expect(managers).toHaveLength(12);
expect(new Set(managers).size).toBe(1);
expect(hoisted.providerCreateCalls).toBe(1);
expect(hoisted.providerCreateCalls).toBe(0);
await managers[0].close();
});
it("drains in-flight manager creation during global teardown", async () => {
it("evicts cached managers during global teardown", async () => {
const indexPath = path.join(workspaceDir, "index.sqlite");
const cfg = createMemoryConcurrencyConfig(indexPath);
hoisted.providerDelayMs = 100;
const pendingResult = RawMemoryIndexManager.get({ cfg, agentId: "main" });
await closeAllMemoryIndexManagers();
const firstManager = await pendingResult;
@ -113,7 +111,7 @@ describe("memory manager cache hydration", () => {
expect(firstManager).toBeTruthy();
expect(secondManager).toBeTruthy();
expect(Object.is(secondManager, firstManager)).toBe(false);
expect(hoisted.providerCreateCalls).toBe(2);
expect(hoisted.providerCreateCalls).toBe(0);
await secondManager?.close?.();
});

View File

@ -193,11 +193,6 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem
return pending;
}
const createPromise = (async () => {
const providerResult = await MemoryIndexManager.loadProviderResult({
cfg,
agentId,
settings,
});
const refreshed = INDEX_CACHE.get(key);
if (refreshed) {
return refreshed;
@ -208,7 +203,6 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem
agentId,
workspaceDir,
settings,
providerResult,
purpose: params.purpose,
});
INDEX_CACHE.set(key, manager);
@ -334,17 +328,21 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem
sessionKey?: string;
},
): Promise<MemorySearchResult[]> {
await this.ensureProviderInitialized();
const cleaned = query.trim();
if (!cleaned) {
return [];
}
void this.warmSession(opts?.sessionKey);
if (this.settings.sync.onSearch && (this.dirty || this.sessionsDirty)) {
void this.sync({ reason: "search" }).catch((err) => {
log.warn(`memory sync failed (search): ${String(err)}`);
});
}
const cleaned = query.trim();
if (!cleaned) {
const hasIndexedContent = this.hasIndexedContent();
if (!hasIndexedContent) {
return [];
}
await this.ensureProviderInitialized();
const minScore = opts?.minScore ?? this.settings.query.minScore;
const maxResults = opts?.maxResults ?? this.settings.query.maxResults;
const hybrid = this.settings.query.hybrid;
@ -437,6 +435,32 @@ export class MemoryIndexManager extends MemoryManagerEmbeddingOps implements Mem
.slice(0, maxResults);
}
private hasIndexedContent(): boolean {
const chunks =
(
this.db.prepare(`SELECT COUNT(*) as c FROM chunks`).get() as
| {
c: number;
}
| undefined
)?.c ?? 0;
if (chunks > 0) {
return true;
}
if (!this.fts.enabled || !this.fts.available) {
return false;
}
const ftsRows =
(
this.db.prepare(`SELECT COUNT(*) as c FROM ${FTS_TABLE}`).get() as
| {
c: number;
}
| undefined
)?.c ?? 0;
return ftsRows > 0;
}
private async searchVector(
queryVec: number[],
limit: number,