Skip to content

Commit 37a0fa0

Browse files
committed
feat: Implement Ollama-powered embedding engine with adaptive input handling, caching, and process shutdown hooks.
1 parent 85ed3b8 commit 37a0fa0

4 files changed

Lines changed: 25 additions & 5 deletions

File tree

src/core/embedding-tracker.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ export interface EmbeddingTrackerControllerOptions extends EmbeddingTrackerOptio
2525
const MIN_FILES_PER_TICK = 5;
2626
const MAX_FILES_PER_TICK = 10;
2727
const DEFAULT_FILES_PER_TICK = 8;
28-
const DEFAULT_DEBOUNCE_MS = 700;
28+
const DEFAULT_DEBOUNCE_MS = 1500;
29+
const MAX_PENDING_FILES = 50;
2930

3031
const IGNORE_PREFIXES = [
3132
".mcp_data/",
@@ -52,7 +53,7 @@ function clampFilesPerTick(value: number | undefined): number {
5253

5354
function clampDebounceMs(value: number | undefined): number {
5455
if (!Number.isFinite(value)) return DEFAULT_DEBOUNCE_MS;
55-
return Math.max(100, Math.floor(value ?? DEFAULT_DEBOUNCE_MS));
56+
return Math.max(500, Math.floor(value ?? DEFAULT_DEBOUNCE_MS));
5657
}
5758

5859
export function parseEmbeddingTrackerMode(value: string | undefined): "off" | "lazy" | "eager" {
@@ -78,6 +79,7 @@ export function startEmbeddingTracker(options: EmbeddingTrackerOptions): () => v
7879
timer = setTimeout(() => {
7980
void flushPending();
8081
}, delay);
82+
timer.unref();
8183
};
8284

8385
const flushPending = async (): Promise<void> => {
@@ -111,6 +113,7 @@ export function startEmbeddingTracker(options: EmbeddingTrackerOptions): () => v
111113
if (closed || !fileName) return;
112114
const relativePath = normalizeRelativePath(String(fileName));
113115
if (!shouldTrack(relativePath)) return;
116+
if (pendingFiles.size >= MAX_PENDING_FILES) return;
114117
pendingFiles.add(relativePath);
115118
schedule();
116119
});

src/core/embeddings.ts

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,14 @@ import { Ollama } from "ollama";
55
import { readFile, writeFile, mkdir } from "fs/promises";
66
import { join } from "path";
77

8+
const EMBED_TIMEOUT_MS = 60_000;
9+
let embedAbortController = new AbortController();
10+
11+
export function cancelAllEmbeddings(): void {
12+
embedAbortController.abort();
13+
embedAbortController = new AbortController();
14+
}
15+
816
export interface SearchDocument {
917
path: string;
1018
header: string;
@@ -120,6 +128,12 @@ function buildEmbedRequest(input: string[]): { model: string; input: string[]; o
120128
return options ? { model: EMBED_MODEL, input, options } : { model: EMBED_MODEL, input };
121129
}
122130

131+
async function embedWithTimeout(request: ReturnType<typeof buildEmbedRequest>): Promise<{ embeddings: number[][] }> {
132+
const timeoutCtrl = AbortSignal.timeout(EMBED_TIMEOUT_MS);
133+
const signal = AbortSignal.any([embedAbortController.signal, timeoutCtrl]);
134+
return ollama.embed({ ...request, signal } as Parameters<typeof ollama.embed>[0]);
135+
}
136+
123137
export function getEmbeddingBatchSize(): number {
124138
const requested = toIntegerOr(process.env.CONTEXTPLUS_EMBED_BATCH_SIZE, DEFAULT_EMBED_BATCH_SIZE);
125139
return Math.min(MAX_EMBED_BATCH_SIZE, Math.max(MIN_EMBED_BATCH_SIZE, requested));
@@ -153,7 +167,7 @@ async function embedSingleAdaptive(input: string): Promise<number[]> {
153167

154168
for (let attempt = 0; attempt <= MAX_SINGLE_INPUT_RETRIES; attempt++) {
155169
try {
156-
const response = await ollama.embed(buildEmbedRequest([candidate]));
170+
const response = await embedWithTimeout(buildEmbedRequest([candidate]));
157171
if (!response.embeddings[0]) throw new Error("Missing embedding vector in Ollama response");
158172
return response.embeddings[0];
159173
} catch (error) {
@@ -169,7 +183,7 @@ async function embedSingleAdaptive(input: string): Promise<number[]> {
169183

170184
async function embedBatchAdaptive(batch: string[]): Promise<number[][]> {
171185
try {
172-
const response = await ollama.embed(buildEmbedRequest(batch));
186+
const response = await embedWithTimeout(buildEmbedRequest(batch));
173187
if (response.embeddings.length !== batch.length) {
174188
throw new Error(`Embedding response size mismatch: expected ${batch.length}, got ${response.embeddings.length}`);
175189
}

src/core/process-lifecycle.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ const DEFAULT_PARENT_POLL_MS = 5 * 1000;
1212
const MIN_PARENT_POLL_MS = 1 * 1000;
1313

1414
export interface CleanupOptions {
15+
cancelEmbeddings?: () => void;
1516
stopTracker: () => void;
1617
closeServer: () => Promise<void> | void;
1718
closeTransport: () => Promise<void> | void;
@@ -133,6 +134,7 @@ export function startParentMonitor(options: ParentMonitorOptions): () => void {
133134
}
134135

135136
export async function runCleanup(options: CleanupOptions): Promise<void> {
137+
options.cancelEmbeddings?.();
136138
options.stopMonitors?.();
137139
options.stopTracker();
138140
await Promise.allSettled([

src/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import { createEmbeddingTrackerController } from "./core/embedding-tracker.js";
1111
import { createIdleMonitor, getIdleShutdownMs, getParentPollMs, isBrokenPipeError, runCleanup, startParentMonitor } from "./core/process-lifecycle.js";
1212
import { getContextTree } from "./tools/context-tree.js";
1313
import { getFileSkeleton } from "./tools/file-skeleton.js";
14-
import { ensureMcpDataDir } from "./core/embeddings.js";
14+
import { ensureMcpDataDir, cancelAllEmbeddings } from "./core/embeddings.js";
1515
import { semanticCodeSearch, invalidateSearchCache } from "./tools/semantic-search.js";
1616
import { semanticIdentifierSearch, invalidateIdentifierSearchCache } from "./tools/semantic-identifiers.js";
1717
import { getBlastRadius } from "./tools/blast-radius.js";
@@ -571,6 +571,7 @@ async function main() {
571571
shuttingDown = true;
572572
console.error(`Context+ MCP shutdown requested: ${reason}`);
573573
await runCleanup({
574+
cancelEmbeddings: cancelAllEmbeddings,
574575
stopTracker: trackerController.stop,
575576
closeServer,
576577
closeTransport,

0 commit comments

Comments
 (0)