feat(backend): persist memory to MongoDB
Replace the in-memory store with a real MongoDB-backed store. Adds memory/db.ts
(client singleton, typed collections, startup ping + indexes) and rewrites
record{Observation,Collision,Intervention,Outcome} to insert into Mongo
(best-effort — failures log, don't crash). Server connects on startup and
exposes GET /api/memory/stats for verification.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,51 @@
|
|||||||
|
import { MongoClient, type Db, type Collection } from 'mongodb';
|
||||||
|
import type { EngineerContext, Collision, Intervention, InterventionOutcome } from '@podman/shared';
|
||||||
|
import { env } from '../env.js';
|
||||||
|
|
||||||
|
let clientPromise: Promise<MongoClient> | null = null;
|
||||||
|
|
||||||
|
function getClient(): Promise<MongoClient> {
|
||||||
|
if (!clientPromise) {
|
||||||
|
const client = new MongoClient(env.MONGODB_URI, { serverSelectionTimeoutMS: 3000 });
|
||||||
|
clientPromise = client.connect();
|
||||||
|
}
|
||||||
|
return clientPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** The PodMan database (name comes from the MONGODB_URI path, e.g. `podman`). */
|
||||||
|
export async function getDb(): Promise<Db> {
|
||||||
|
const client = await getClient();
|
||||||
|
return client.db();
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface PodCollections {
|
||||||
|
observations: Collection<EngineerContext>;
|
||||||
|
collisions: Collection<Collision>;
|
||||||
|
interventions: Collection<Intervention>;
|
||||||
|
outcomes: Collection<InterventionOutcome>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function collections(): Promise<PodCollections> {
|
||||||
|
const db = await getDb();
|
||||||
|
return {
|
||||||
|
observations: db.collection<EngineerContext>('observations'),
|
||||||
|
collisions: db.collection<Collision>('collisions'),
|
||||||
|
interventions: db.collection<Intervention>('interventions'),
|
||||||
|
outcomes: db.collection<InterventionOutcome>('outcomes'),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Connect, verify reachability, and create helpful indexes. Call once on startup. */
|
||||||
|
export async function initMemory(): Promise<void> {
|
||||||
|
const db = await getDb();
|
||||||
|
await db.command({ ping: 1 });
|
||||||
|
const c = await collections();
|
||||||
|
await Promise.all([
|
||||||
|
c.observations.createIndex({ podId: 1, observedAt: -1 }),
|
||||||
|
c.observations.createIndex({ engineerId: 1 }),
|
||||||
|
c.collisions.createIndex({ podId: 1, detectedAt: -1 }),
|
||||||
|
c.interventions.createIndex({ collisionId: 1 }),
|
||||||
|
c.outcomes.createIndex({ interventionId: 1 }),
|
||||||
|
]);
|
||||||
|
console.log(`[memory] mongo connected -> ${db.databaseName}`);
|
||||||
|
}
|
||||||
+29
-31
@@ -1,47 +1,45 @@
|
|||||||
import type { EngineerContext, Collision, Intervention } from '@podman/shared';
|
import type { EngineerContext, Collision, Intervention, InterventionOutcome } from '@podman/shared';
|
||||||
import type { InterventionOutcome } from '@podman/shared';
|
import { collections } from './db.js';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Continual-learning memory: persist observations + intervention outcomes to
|
* Continual-learning memory: persist observations, collisions, interventions,
|
||||||
* MongoDB Atlas and embed file/feature notes into Voyage vectors so later
|
* and outcomes to MongoDB so later sessions get sharper. Writes are best-effort
|
||||||
* sessions are sharper ("more useful the more you use it").
|
* — a Mongo hiccup logs a warning rather than crashing the agent/server.
|
||||||
*/
|
*/
|
||||||
export interface PodMemory {
|
async function persist(name: string, fn: () => Promise<unknown>): Promise<void> {
|
||||||
recordObservation(ctx: EngineerContext): Promise<void>;
|
try {
|
||||||
recordOutcome(intervention: Intervention, accepted: boolean): Promise<void>;
|
await fn();
|
||||||
|
} catch (err) {
|
||||||
|
console.warn(`[memory] ${name} persist failed: ${(err as Error).message}`);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/** In-memory stub so the rest of the pipeline can run before Atlas is wired. */
|
|
||||||
export function createInMemoryStore(): PodMemory {
|
|
||||||
const observations: EngineerContext[] = [];
|
|
||||||
return {
|
|
||||||
async recordObservation(ctx) {
|
|
||||||
observations.push(ctx);
|
|
||||||
},
|
|
||||||
async recordOutcome() {
|
|
||||||
/* no-op until Atlas is wired */
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Standalone helpers used by the PodMan orchestrator and HTTP server.
|
|
||||||
const _observations: EngineerContext[] = [];
|
|
||||||
const _collisions: Collision[] = [];
|
|
||||||
const _interventions: Intervention[] = [];
|
|
||||||
const _outcomes: InterventionOutcome[] = [];
|
|
||||||
|
|
||||||
export async function recordObservation(ctx: EngineerContext): Promise<void> {
|
export async function recordObservation(ctx: EngineerContext): Promise<void> {
|
||||||
_observations.push(ctx);
|
await persist('observation', async () => (await collections()).observations.insertOne({ ...ctx }));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function recordCollision(collision: Collision): Promise<void> {
|
export async function recordCollision(collision: Collision): Promise<void> {
|
||||||
_collisions.push(collision);
|
await persist('collision', async () => (await collections()).collisions.insertOne({ ...collision }));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function recordIntervention(intervention: Intervention): Promise<void> {
|
export async function recordIntervention(intervention: Intervention): Promise<void> {
|
||||||
_interventions.push(intervention);
|
await persist('intervention', async () =>
|
||||||
|
(await collections()).interventions.insertOne({ ...intervention }),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function recordOutcome(outcome: InterventionOutcome): Promise<void> {
|
export async function recordOutcome(outcome: InterventionOutcome): Promise<void> {
|
||||||
_outcomes.push(outcome);
|
await persist('outcome', async () => (await collections()).outcomes.insertOne({ ...outcome }));
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Document counts per collection — used by the /api/memory/stats endpoint. */
|
||||||
|
export async function memoryStats(): Promise<Record<string, number>> {
|
||||||
|
const c = await collections();
|
||||||
|
const [observations, collisions, interventions, outcomes] = await Promise.all([
|
||||||
|
c.observations.estimatedDocumentCount(),
|
||||||
|
c.collisions.estimatedDocumentCount(),
|
||||||
|
c.interventions.estimatedDocumentCount(),
|
||||||
|
c.outcomes.estimatedDocumentCount(),
|
||||||
|
]);
|
||||||
|
return { observations, collisions, interventions, outcomes };
|
||||||
}
|
}
|
||||||
|
|||||||
+15
-2
@@ -5,7 +5,8 @@ import { WebSocketServer } from 'ws';
|
|||||||
import { AccessToken } from 'livekit-server-sdk';
|
import { AccessToken } from 'livekit-server-sdk';
|
||||||
import { env } from './env.js';
|
import { env } from './env.js';
|
||||||
import { createSyncPr } from './github/client.js';
|
import { createSyncPr } from './github/client.js';
|
||||||
import { recordOutcome } from './memory/store.js';
|
import { recordOutcome, memoryStats } from './memory/store.js';
|
||||||
|
import { initMemory } from './memory/db.js';
|
||||||
import type { InterventionOutcome } from '@podman/shared';
|
import type { InterventionOutcome } from '@podman/shared';
|
||||||
|
|
||||||
const app = express();
|
const app = express();
|
||||||
@@ -44,6 +45,15 @@ app.post('/api/outcome', async (req, res) => {
|
|||||||
res.json({ ok: true });
|
res.json({ ok: true });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Memory counts — quick way to confirm Mongo persistence is working.
|
||||||
|
app.get('/api/memory/stats', async (_req, res) => {
|
||||||
|
try {
|
||||||
|
res.json(await memoryStats());
|
||||||
|
} catch (e) {
|
||||||
|
res.status(500).json({ error: (e as Error).message });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
const http = createServer(app);
|
const http = createServer(app);
|
||||||
|
|
||||||
// ws relay: the agent pushes collision/intervention JSON here; PWAs subscribed by pod receive it.
|
// ws relay: the agent pushes collision/intervention JSON here; PWAs subscribed by pod receive it.
|
||||||
@@ -58,4 +68,7 @@ wss.on('connection', (ws) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
http.listen(env.PORT, '0.0.0.0', () => console.log(`[server] :${env.PORT}`));
|
http.listen(env.PORT, '0.0.0.0', () => {
|
||||||
|
console.log(`[server] :${env.PORT}`);
|
||||||
|
initMemory().catch((e) => console.warn(`[memory] init failed: ${(e as Error).message}`));
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user