@@ -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