Skip to content

Commit 74edfca

Browse files
committed
refactor(ai): Robust Hybrid Database Lifecycle and Roadmap update (#7836)
1 parent e90762c commit 74edfca

8 files changed

Lines changed: 99 additions & 72 deletions

File tree

ROADMAP.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,15 +14,15 @@ We are currently evolving our AI infrastructure to move beyond simple "tool use"
1414

1515
**Goal:** Enable agents to import and use our intelligent services directly as libraries, without the MCP protocol overhead.
1616

17-
- **Decouple Services:** Refactor `ai/mcp/server/*/services/*.mjs` to be importable as a standalone "AI SDK" (`ai/services.mjs`). This allows an agent to write a script that imports `QueryService` and uses it directly.
18-
- **Standardize Lifecycle:** Implement a `Neo.ai.ServiceBase` class that enforces the `initAsync()` and `ready()` pattern across all AI services, enabling agents to write generic "wait for readiness" logic.
17+
- **Decouple Services (Completed):** Refactored `ai/mcp/server/*/services/*.mjs` to be importable as a standalone "AI SDK" (`ai/services.mjs`).
18+
- **Standardize Lifecycle (Completed):** Enforced the `initAsync()` and `ready()` pattern across AI services, ensuring robust handling of hybrid database states (managed vs. external) and eliminating race conditions.
1919
- **The "Neo Sandbox":** Create a lightweight boilerplate/config that sets up the Neo.mjs core in a Node.js script (handling `globalThis`, `Worker` mocks if needed) so agents can instantly start scripting.
2020

2121
### 2. New "Code Execution" Capabilities
2222

2323
**Goal:** Empower agents to write "smart" scripts that perform logic on the client side, reducing token usage and increasing accuracy.
2424

25-
- **"Smart" Search Scripts:** Agents write scripts to query the Knowledge Base, then filter and rank results using custom JavaScript logic *before* returning data to the LLM context.
25+
- **"Smart" Search Scripts (Completed):** Verified with `ai/examples/smart-search.mjs`.
2626
- **Automated Refactoring Agents:** Expose the `Neo.mjs` core (Component system, Config system) to the agent sandbox. Agents can instantiate components in Node.js to verify config validity using `Neo.create()` before committing code.
2727

2828
### 3. Visibility & Marketing ("Get Visibility")
@@ -31,7 +31,7 @@ We are currently evolving our AI infrastructure to move beyond simple "tool use"
3131

3232
- **"Context Engineering" Case Study:** Publish technical content comparing raw file dumping (Context Tax) vs. our semantic inheritance chain injection.
3333
- **"The Agent OS" Branding:** Update documentation to explicitly highlight "AI-Native" capabilities and "Architecture designed for Agent Code Execution."
34-
- **"Self-Healing" Repository:** Develop a demo where an agent uses `GitHubWorkflow` + `KnowledgeBase` to autonomously read a bug report, write a reproduction test, fix the bug, and verify the fix.
34+
- **"Self-Healing" Repository (Completed):** Developed `ai/examples/self-healing.mjs` where an agent uses `GitHubWorkflow` + `KnowledgeBase` to autonomously read a bug report, query context, plan, and propose a fix.
3535

3636
## 4. Future: Decoupling the AI Tooling Ecosystem
3737

ai/examples/self-healing.mjs

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -24,25 +24,13 @@ async function main() {
2424

2525
// Start Knowledge Base
2626
await KB_LifecycleService.ready();
27+
await KB_ChromaManager.ready(); // Ensure connection is active
2728
console.log(' - Knowledge Base Service: Ready');
2829

2930
// Start Memory Core
3031
await Memory_LifecycleService.ready();
32+
await Memory_ChromaManager.ready(); // Ensure connection is active
3133
console.log(' - Memory Core Service: Ready');
32-
33-
// Verify Connections (Manual wait loop pattern)
34-
let kbConnected = false, memConnected = false;
35-
for (let i = 0; i < 20; i++) {
36-
if (!kbConnected) try { await KB_ChromaManager.client.heartbeat(); kbConnected = true; } catch(e){}
37-
if (!memConnected) try { await Memory_ChromaManager.client.heartbeat(); memConnected = true; } catch(e){}
38-
if (kbConnected && memConnected) break;
39-
await new Promise(r => setTimeout(r, 500));
40-
}
41-
42-
if (!kbConnected || !memConnected) {
43-
console.error(`❌ DB Connection Failed. KB: ${kbConnected}, Mem: ${memConnected}`);
44-
process.exit(1);
45-
}
4634

4735
// Ensure KB content is loaded (embed if needed)
4836
try { await KB_DatabaseService.embedKnowledgeBase(); } catch(e) {}

ai/mcp/server/knowledge-base/services/ChromaManager.mjs

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import {ChromaClient} from 'chromadb';
2-
import aiConfig from '../config.mjs';
3-
import logger from '../logger.mjs';
4-
import Base from '../../../../../src/core/Base.mjs';
1+
import {ChromaClient} from 'chromadb';
2+
import aiConfig from '../config.mjs';
3+
import logger from '../logger.mjs';
4+
import Base from '../../../../../src/core/Base.mjs';
5+
import DatabaseLifecycleService from './DatabaseLifecycleService.mjs';
56

67
/**
78
* Simple manager around the Chroma client that lazily caches the knowledge-base collection.
@@ -53,6 +54,7 @@ class ChromaManager extends Base {
5354
*/
5455
async initAsync() {
5556
await super.initAsync();
57+
await DatabaseLifecycleService.ready();
5658
await this.connect();
5759
}
5860

ai/mcp/server/knowledge-base/services/DatabaseLifecycleService.mjs

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import {spawn} from 'child_process';
2-
import aiConfig from '../config.mjs';
3-
import logger from '../logger.mjs';
4-
import Base from '../../../../../src/core/Base.mjs';
5-
import ChromaManager from './ChromaManager.mjs';
1+
import {spawn} from 'child_process';
2+
import aiConfig from '../config.mjs';
3+
import logger from '../logger.mjs';
4+
import Base from '../../../../../src/core/Base.mjs';
65

76
/**
87
* Manages the lifecycle of the ChromaDB process for the Knowledge Base.
@@ -42,8 +41,6 @@ class DatabaseLifecycleService extends Base {
4241
*/
4342
async initAsync() {
4443
await super.initAsync();
45-
46-
await ChromaManager.ready();
4744
await this.startDatabase();
4845
}
4946

@@ -53,6 +50,7 @@ class DatabaseLifecycleService extends Base {
5350
*/
5451
async isDbRunning() {
5552
try {
53+
const ChromaManager = (await import('./ChromaManager.mjs')).default;
5654
await ChromaManager.client.heartbeat();
5755
return true;
5856
} catch (e) {
@@ -75,7 +73,7 @@ class DatabaseLifecycleService extends Base {
7573
return result;
7674
}
7775

78-
return new Promise((resolve, reject) => {
76+
await new Promise((resolve, reject) => {
7977
const { port, path: dbPath } = aiConfig;
8078
const args = ['run', '--path', dbPath, '--port', port.toString()];
8179

@@ -87,9 +85,7 @@ class DatabaseLifecycleService extends Base {
8785
spawnedProcess.on('spawn', () => {
8886
this.chromaProcess = spawnedProcess;
8987
logger.log(`ChromaDB (Knowledge Base) process started with PID: ${this.chromaProcess.pid}`);
90-
const result = { status: 'started', pid: this.chromaProcess.pid };
91-
this.fire('processActive', { pid: this.chromaProcess.pid, managedByService: true, detail: 'started by service' });
92-
resolve(result);
88+
resolve();
9389
});
9490

9591
spawnedProcess.on('error', (err) => {
@@ -100,6 +96,28 @@ class DatabaseLifecycleService extends Base {
10096

10197
spawnedProcess.unref();
10298
});
99+
100+
await this.waitForHeartbeat();
101+
102+
const result = { status: 'started', pid: this.chromaProcess.pid };
103+
this.fire('processActive', { pid: this.chromaProcess.pid, managedByService: true, detail: 'started by service' });
104+
return result;
105+
}
106+
107+
/**
108+
* Waits for the ChromaDB server to respond to a heartbeat.
109+
* @returns {Promise<void>}
110+
*/
111+
async waitForHeartbeat() {
112+
logger.log('Waiting for ChromaDB heartbeat...');
113+
for (let i = 0; i < 30; i++) {
114+
if (await this.isDbRunning()) {
115+
logger.log('ChromaDB heartbeat detected.');
116+
return;
117+
}
118+
await new Promise(resolve => setTimeout(resolve, 500));
119+
}
120+
throw new Error('ChromaDB failed to start (timeout waiting for heartbeat).');
103121
}
104122

105123
/**

ai/mcp/server/knowledge-base/services/DatabaseService.mjs

Lines changed: 12 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,13 @@
1-
import {GoogleGenerativeAI} from '@google/generative-ai';
2-
import aiConfig from '../config.mjs';
3-
import Base from '../../../../../src/core/Base.mjs';
4-
import ChromaManager from './ChromaManager.mjs';
5-
import crypto from 'crypto';
6-
import DatabaseLifecycleService from './DatabaseLifecycleService.mjs';
7-
import dotenv from 'dotenv';
8-
import fs from 'fs-extra';
9-
import logger from '../logger.mjs';
10-
import path from 'path';
11-
import readline from 'readline';
1+
import {GoogleGenerativeAI} from '@google/generative-ai';
2+
import aiConfig from '../config.mjs';
3+
import Base from '../../../../../src/core/Base.mjs';
4+
import ChromaManager from './ChromaManager.mjs';
5+
import crypto from 'crypto';
6+
import dotenv from 'dotenv';
7+
import fs from 'fs-extra';
8+
import logger from '../logger.mjs';
9+
import path from 'path';
10+
import readline from 'readline';
1211

1312
const cwd = process.cwd();
1413
const insideNeo = process.env.npm_package_name?.includes('neo.mjs') ?? false;
@@ -74,8 +73,8 @@ class DatabaseService extends Base {
7473
async initAsync() {
7574
await super.initAsync();
7675

77-
// Wait for ChromaDB to be available
78-
await DatabaseLifecycleService.ready();
76+
// Wait for ChromaManager (which waits for LifecycleService) to be ready
77+
await ChromaManager.ready();
7978

8079
logger.info('[Startup] Checking knowledge base status...');
8180
const knowledgeBasePath = aiConfig.dataPath;

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

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
import {ChromaClient} from 'chromadb';
2-
import aiConfig from '../config.mjs';
3-
import logger from '../logger.mjs';
4-
import Base from '../../../../../src/core/Base.mjs';
1+
import {ChromaClient} from 'chromadb';
2+
import aiConfig from '../config.mjs';
3+
import logger from '../logger.mjs';
4+
import Base from '../../../../../src/core/Base.mjs';
5+
import DatabaseLifecycleService from './DatabaseLifecycleService.mjs';
56

67
/**
78
* Simple manager around the Chroma client that lazily caches frequently used collections.
@@ -58,6 +59,7 @@ class ChromaManager extends Base {
5859
*/
5960
async initAsync() {
6061
await super.initAsync();
62+
await DatabaseLifecycleService.ready();
6163
await this.connect();
6264
}
6365

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

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
import {spawn} from 'child_process';
2-
import aiConfig from '../config.mjs';
3-
import logger from '../logger.mjs';
4-
import Base from '../../../../../src/core/Base.mjs';
5-
import ChromaManager from './ChromaManager.mjs';
1+
import {spawn} from 'child_process';
2+
import aiConfig from '../config.mjs';
3+
import logger from '../logger.mjs';
4+
import Base from '../../../../../src/core/Base.mjs';
65

76
/**
87
* Manages the lifecycle of the ChromaDB process for the Memory Core.
@@ -42,8 +41,6 @@ class DatabaseLifecycleService extends Base {
4241
*/
4342
async initAsync() {
4443
await super.initAsync();
45-
46-
await ChromaManager.ready();
4744
await this.startDatabase();
4845
}
4946

@@ -53,6 +50,7 @@ class DatabaseLifecycleService extends Base {
5350
*/
5451
async isDbRunning() {
5552
try {
53+
const ChromaManager = (await import('./ChromaManager.mjs')).default;
5654
await ChromaManager.client.heartbeat();
5755
return true;
5856
} catch (e) {
@@ -77,7 +75,7 @@ class DatabaseLifecycleService extends Base {
7775

7876
logger.error('Starting ChromaDB (Memory Core) process...');
7977

80-
return new Promise((resolve, reject) => {
78+
await new Promise((resolve, reject) => {
8179
const { port, path: dbPath } = aiConfig.memoryDb;
8280
const args = ['run', '--path', dbPath, '--port', port.toString()];
8381

@@ -89,9 +87,7 @@ class DatabaseLifecycleService extends Base {
8987
spawnedProcess.on('spawn', () => {
9088
this.chromaProcess = spawnedProcess;
9189
logger.log(`ChromaDB (Memory Core) process started with PID: ${this.chromaProcess.pid}`);
92-
const result = { status: 'started', pid: this.chromaProcess.pid };
93-
this.fire('processActive', { pid: this.chromaProcess.pid, managedByService: true, detail: 'started by service' });
94-
resolve(result);
90+
resolve();
9591
});
9692

9793
spawnedProcess.on('error', (err) => {
@@ -102,6 +98,28 @@ class DatabaseLifecycleService extends Base {
10298

10399
spawnedProcess.unref();
104100
});
101+
102+
await this.waitForHeartbeat();
103+
104+
const result = { status: 'started', pid: this.chromaProcess.pid };
105+
this.fire('processActive', { pid: this.chromaProcess.pid, managedByService: true, detail: 'started by service' });
106+
return result;
107+
}
108+
109+
/**
110+
* Waits for the ChromaDB server to respond to a heartbeat.
111+
* @returns {Promise<void>}
112+
*/
113+
async waitForHeartbeat() {
114+
logger.log('Waiting for ChromaDB heartbeat...');
115+
for (let i = 0; i < 30; i++) {
116+
if (await this.isDbRunning()) {
117+
logger.log('ChromaDB heartbeat detected.');
118+
return;
119+
}
120+
await new Promise(resolve => setTimeout(resolve, 500));
121+
}
122+
throw new Error('ChromaDB failed to start (timeout waiting for heartbeat).');
105123
}
106124

107125
/**

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

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,11 @@
1-
import {GoogleGenerativeAI} from '@google/generative-ai';
2-
import aiConfig from '../config.mjs';
3-
import Base from '../../../../../src/core/Base.mjs';
4-
import crypto from 'crypto';
5-
import ChromaManager from './ChromaManager.mjs';
6-
import DatabaseLifecycleService from './DatabaseLifecycleService.mjs';
7-
import HealthService from './HealthService.mjs';
8-
import Json from '../../../../../src/util/Json.mjs';
9-
import logger from '../logger.mjs';
1+
import {GoogleGenerativeAI} from '@google/generative-ai';
2+
import aiConfig from '../config.mjs';
3+
import Base from '../../../../../src/core/Base.mjs';
4+
import crypto from 'crypto';
5+
import ChromaManager from './ChromaManager.mjs';
6+
import HealthService from './HealthService.mjs';
7+
import Json from '../../../../../src/util/Json.mjs';
8+
import logger from '../logger.mjs';
109

1110
/**
1211
* Service for handling adding, listing, and querying agent memories.
@@ -84,8 +83,9 @@ class SessionService extends Base {
8483
async initAsync() {
8584
await super.initAsync();
8685

87-
// Wait for DatabaseLifecycleService to ensure ChromaDB is available
88-
await DatabaseLifecycleService.ready();
86+
// Wait for ChromaManager to be ready (connected)
87+
// ChromaManager internally waits for DatabaseLifecycleService
88+
await ChromaManager.ready();
8989

9090
// Use ChromaManager instead of direct client access
9191
this.memoryCollection = await ChromaManager.getMemoryCollection();

0 commit comments

Comments
 (0)