Skip to content

Commit a4c511a

Browse files
committed
fix(memory): keep keyword search available when embeddings time out
1 parent ff143e0 commit a4c511a

2 files changed

Lines changed: 41 additions & 17 deletions

File tree

src/memory/embeddings.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,26 @@ const DEFAULT_OPENAI_MODEL = 'text-embedding-3-small';
77
const DEFAULT_GEMINI_MODEL = 'gemini-embedding-001';
88
const DEFAULT_OLLAMA_MODEL = 'nomic-embed-text';
99
const EMBEDDING_BATCH_SIZE = 64;
10+
const EMBEDDING_TIMEOUT_MS = 15_000;
1011

1112
type ResolvedProvider = Exclude<EmbeddingProviderId, 'auto' | 'none'>;
1213

14+
function withTimeout<T>(promise: Promise<T>, ms: number, message: string): Promise<T> {
15+
return new Promise<T>((resolve, reject) => {
16+
const timer = setTimeout(() => reject(new Error(message)), ms);
17+
promise.then(
18+
(value) => {
19+
clearTimeout(timer);
20+
resolve(value);
21+
},
22+
(error) => {
23+
clearTimeout(timer);
24+
reject(error);
25+
},
26+
);
27+
});
28+
}
29+
1330
function resolveProvider(preferred: EmbeddingProviderId): ResolvedProvider | null {
1431
if (preferred === 'openai' && process.env.OPENAI_API_KEY) {
1532
return 'openai';
@@ -43,7 +60,7 @@ async function embedInBatches(
4360
const vectors: number[][] = [];
4461
for (let i = 0; i < texts.length; i += EMBEDDING_BATCH_SIZE) {
4562
const batch = texts.slice(i, i + EMBEDDING_BATCH_SIZE);
46-
const result = await embedBatch(batch);
63+
const result = await withTimeout(embedBatch(batch), EMBEDDING_TIMEOUT_MS, 'Embedding API timed out');
4764
vectors.push(...result);
4865
}
4966
return vectors;
@@ -106,6 +123,6 @@ export async function embedSingleQuery(
106123
if (!client) {
107124
return null;
108125
}
109-
const vectors = await client.embed([query]);
126+
const vectors = await withTimeout(client.embed([query]), EMBEDDING_TIMEOUT_MS, 'Embedding query timed out');
110127
return vectors[0] ?? null;
111128
}

src/memory/index.ts

Lines changed: 22 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -87,25 +87,32 @@ export class MemoryManager {
8787

8888
try {
8989
this.db = await MemoryDatabase.create(`${this.store.getMemoryDir()}/index.sqlite`);
90-
const fingerprint = client ? `${client.provider}:${client.model}` : 'none:none';
91-
if (this.db.getProviderFingerprint() !== fingerprint) {
92-
this.db.clearEmbeddings();
93-
this.db.setProviderFingerprint(fingerprint);
94-
}
95-
96-
this.indexer = new MemoryIndexer(this.store, this.db, {
97-
chunkTokens: this.config.chunkTokens,
98-
overlapTokens: this.config.chunkOverlapTokens,
99-
watchDebounceMs: this.config.watchDebounceMs,
100-
embeddingClient: client,
101-
indexSessions: this.config.indexSessions,
102-
});
103-
this.indexer.startWatching();
104-
await this.indexer.sync({ force: false });
10590
} catch (error) {
10691
this.initError = error instanceof Error ? error.message : String(error);
10792
this.db = null;
10893
this.indexer = null;
94+
return;
95+
}
96+
97+
const fingerprint = client ? `${client.provider}:${client.model}` : 'none:none';
98+
if (this.db.getProviderFingerprint() !== fingerprint) {
99+
this.db.clearEmbeddings();
100+
this.db.setProviderFingerprint(fingerprint);
101+
}
102+
103+
this.indexer = new MemoryIndexer(this.store, this.db, {
104+
chunkTokens: this.config.chunkTokens,
105+
overlapTokens: this.config.chunkOverlapTokens,
106+
watchDebounceMs: this.config.watchDebounceMs,
107+
embeddingClient: client,
108+
indexSessions: this.config.indexSessions,
109+
});
110+
this.indexer.startWatching();
111+
112+
try {
113+
await this.indexer.sync({ force: false });
114+
} catch (error) {
115+
this.initError = error instanceof Error ? error.message : String(error);
109116
}
110117
}
111118

0 commit comments

Comments
 (0)