Skip to content

Commit 150daee

Browse files
committed
feat(MemoryCore): Add Ollama process lifecycle management and cleanup (#9724)
1 parent 8a4221a commit 150daee

3 files changed

Lines changed: 128 additions & 13 deletions

File tree

ai/mcp/server/memory-core/managers/SQLiteVectorManager.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,8 @@ class SQLiteVectorManager extends Base {
379379

380380
const rows = self.db.prepare(sql).all(...queryArgs);
381381

382+
let returnData = { ids: [[]], metadatas: [[]], documents: [[]], distances: [[]] };
383+
382384
for (const row of rows) {
383385
returnData.ids[0].push(row.id);
384386
returnData.metadatas[0].push(JSON.parse(row.metadata));

ai/mcp/server/memory-core/openapi.yaml

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -643,9 +643,12 @@ paths:
643643
content:
644644
application/json:
645645
schema:
646-
type: array
647-
items:
648-
type: object
646+
type: object
647+
properties:
648+
neighbors:
649+
type: array
650+
items:
651+
type: object
649652
'500':
650653
description: Internal server error.
651654
content:
@@ -676,9 +679,12 @@ paths:
676679
content:
677680
application/json:
678681
schema:
679-
type: array
680-
items:
681-
type: object
682+
type: object
683+
properties:
684+
nodes:
685+
type: array
686+
items:
687+
type: object
682688
'500':
683689
description: Internal server error.
684690
content:

ai/mcp/server/memory-core/services/DatabaseLifecycleService.mjs

Lines changed: 114 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,12 @@ class DatabaseLifecycleService extends Base {
3434
* @protected
3535
*/
3636
chromaProcess: null,
37+
/**
38+
* Holds the child process object for the Ollama server.
39+
* @member {ChildProcess|null} ollamaProcess=null
40+
* @protected
41+
*/
42+
ollamaProcess: null,
3743
/**
3844
* @member {Boolean} singleton=true
3945
* @protected
@@ -63,6 +69,71 @@ class DatabaseLifecycleService extends Base {
6369
}
6470
}
6571

72+
/**
73+
* Checks if the Ollama daemon is running.
74+
* @returns {Promise<boolean>}
75+
*/
76+
async isOllamaRunning() {
77+
try {
78+
const {host} = aiConfig.ollama;
79+
const res = await fetch(`${host}/api/version`);
80+
return res.ok;
81+
} catch (e) {
82+
return false;
83+
}
84+
}
85+
86+
/**
87+
* Starts the Ollama daemon if it is not already running.
88+
* @returns {Promise<void>}
89+
*/
90+
async startOllama() {
91+
if (await this.isOllamaRunning()) {
92+
logger.log('Ollama daemon is already running (Memory Core).');
93+
return;
94+
}
95+
96+
logger.error('Starting Ollama daemon (Memory Core)...');
97+
98+
await new Promise((resolve, reject) => {
99+
const spawnedProcess = spawn('ollama', ['serve'], {
100+
detached: true,
101+
stdio : 'ignore'
102+
});
103+
104+
spawnedProcess.on('spawn', () => {
105+
this.ollamaProcess = spawnedProcess;
106+
logger.log(`Ollama daemon started with PID: ${this.ollamaProcess.pid}`);
107+
108+
if (!this.cleanupHandler) {
109+
this.cleanupHandler = this.cleanup.bind(this);
110+
process.on('exit', this.cleanupHandler);
111+
process.on('SIGINT', this.cleanupHandler);
112+
process.on('SIGTERM', this.cleanupHandler);
113+
}
114+
resolve();
115+
});
116+
117+
spawnedProcess.on('error', (err) => {
118+
logger.error('Failed to start Ollama daemon:', err);
119+
this.ollamaProcess = null;
120+
reject(err);
121+
});
122+
123+
spawnedProcess.unref();
124+
});
125+
126+
logger.log('Waiting for Ollama heartbeat...');
127+
for (let i = 0; i < 30; i++) {
128+
if (await this.isOllamaRunning()) {
129+
logger.log('Ollama heartbeat detected.');
130+
return;
131+
}
132+
await new Promise(resolve => setTimeout(resolve, 500));
133+
}
134+
throw new Error('Ollama failed to start (timeout waiting for heartbeat).');
135+
}
136+
66137
/**
67138
* Manages the database lifecycle based on the provided action.
68139
* @param {Object} params
@@ -85,25 +156,39 @@ class DatabaseLifecycleService extends Base {
85156
*/
86157
async startDatabase() {
87158
try {
159+
if (aiConfig.engine === 'neo' || aiConfig.engine === 'both') {
160+
if (aiConfig.neoEmbeddingProvider === 'ollama') {
161+
await this.startOllama();
162+
}
163+
}
164+
165+
if (aiConfig.engine === 'neo') {
166+
return {status: 'neo_engine_active', pid: null, detail: 'SQLite-Vec native engine started.'};
167+
}
168+
88169
if (this.chromaProcess && !this.chromaProcess.killed) {
89-
return {status: 'already_running', pid: this.chromaProcess.pid, detail: 'Server was started by this process.'};
170+
return {
171+
status: 'already_running',
172+
pid : this.chromaProcess.pid,
173+
detail: 'Server was started by this process.'
174+
};
90175
}
91176

92177
if (await this.isDbRunning()) {
93178
const result = {status: 'already_running', pid: null, detail: 'Server was started externally.'};
94-
this.fire('processActive', { pid: null, managedByService: false, detail: result.detail });
179+
this.fire('processActive', {pid: null, managedByService: false, detail: result.detail});
95180
return result;
96181
}
97182

98183
logger.error('Starting ChromaDB (Memory Core) process...');
99184

100185
await new Promise((resolve, reject) => {
101186
const {port, path: dbPath} = aiConfig.memoryDb;
102-
const args = ['run', '--path', dbPath, '--port', port.toString()];
187+
const args = ['run', '--path', dbPath, '--port', port.toString()];
103188

104189
const spawnedProcess = spawn('chroma', args, {
105190
detached: true,
106-
stdio: 'ignore'
191+
stdio : 'ignore'
107192
});
108193

109194
spawnedProcess.on('spawn', () => {
@@ -131,7 +216,11 @@ class DatabaseLifecycleService extends Base {
131216
await this.waitForHeartbeat();
132217

133218
const result = {status: 'started', pid: this.chromaProcess.pid};
134-
this.fire('processActive', {pid: this.chromaProcess.pid, managedByService: true, detail: 'started by service'});
219+
this.fire('processActive', {
220+
pid : this.chromaProcess.pid,
221+
managedByService: true,
222+
detail : 'started by service'
223+
});
135224
return result;
136225
} catch (error) {
137226
logger.error('[DatabaseLifecycleService] Error starting database:', error);
@@ -160,6 +249,15 @@ class DatabaseLifecycleService extends Base {
160249
}
161250
}
162251

252+
if (this.ollamaProcess) {
253+
try {
254+
process.kill(-this.ollamaProcess.pid, 'SIGTERM');
255+
this.ollamaProcess = null;
256+
} catch (e) {
257+
// Ignore errors
258+
}
259+
}
260+
163261
// If this was a signal (not a normal exit), we need to exit explicitly
164262
if (typeof signalOrCode === 'string') {
165263
process.exit(0);
@@ -188,6 +286,15 @@ class DatabaseLifecycleService extends Base {
188286
*/
189287
async stopDatabase() {
190288
try {
289+
if (this.ollamaProcess && !this.ollamaProcess.killed) {
290+
logger.log(`Ollama process with PID: ${this.ollamaProcess.pid} has been stopped.`);
291+
try {
292+
process.kill(-this.ollamaProcess.pid, 'SIGTERM');
293+
} catch (e) {
294+
}
295+
this.ollamaProcess = null;
296+
}
297+
191298
if (!this.chromaProcess || this.chromaProcess.killed) {
192299
return {status: 'not_running', detail: 'No process was started by this server.'};
193300
}
@@ -206,8 +313,8 @@ class DatabaseLifecycleService extends Base {
206313
this.cleanupHandler = null;
207314
}
208315

209-
const result = { status: 'stopped' };
210-
this.fire('processStopped', { pid, managedByService: true });
316+
const result = {status: 'stopped'};
317+
this.fire('processStopped', {pid, managedByService: true});
211318
resolve(result);
212319
});
213320

0 commit comments

Comments
 (0)