feat: harden podman deployment orchestration
This commit is contained in:
@@ -24,6 +24,13 @@ export async function getDb(): Promise<Db> {
|
||||
return client.db();
|
||||
}
|
||||
|
||||
export async function closeMemory(): Promise<void> {
|
||||
if (!clientPromise) return;
|
||||
const client = await clientPromise;
|
||||
clientPromise = null;
|
||||
await client.close();
|
||||
}
|
||||
|
||||
export interface PodCollections {
|
||||
pods: Collection<Pod>;
|
||||
observations: Collection<EngineerContext>;
|
||||
@@ -88,6 +95,10 @@ export async function initMemory(): Promise<void> {
|
||||
['observations.podId', () => c.observations.createIndex({ podId: 1, observedAt: -1 })],
|
||||
['observations.engineerId', () => c.observations.createIndex({ engineerId: 1 })],
|
||||
['collisions.podId', () => c.collisions.createIndex({ podId: 1, detectedAt: -1 })],
|
||||
[
|
||||
'collisions.memorySignature',
|
||||
() => c.collisions.createIndex({ podId: 1, memorySignature: 1 }),
|
||||
],
|
||||
['interventions.collisionId', () => c.interventions.createIndex({ collisionId: 1 })],
|
||||
['outcomes.interventionId', () => c.outcomes.createIndex({ interventionId: 1 })],
|
||||
];
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import type { EngineerContext, Collision, Intervention, InterventionOutcome } from '@podman/shared';
|
||||
import { collections } from './db.js';
|
||||
import { enrichCollisionMemory } from './vectors.js';
|
||||
|
||||
/**
|
||||
* Continual-learning memory: persist observations, collisions, interventions,
|
||||
@@ -22,7 +23,7 @@ export async function recordObservation(ctx: EngineerContext): Promise<void> {
|
||||
|
||||
export async function recordCollision(collision: Collision): Promise<void> {
|
||||
await persist('collision', async () =>
|
||||
(await collections()).collisions.insertOne({ ...collision }),
|
||||
(await collections()).collisions.insertOne(await enrichCollisionMemory(collision)),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,10 +1,123 @@
|
||||
import type { Collision } from '@podman/shared';
|
||||
import { env } from '../env.js';
|
||||
import { getDb } from './db.js';
|
||||
|
||||
type StoredCollision = Collision & {
|
||||
memorySignature?: string;
|
||||
memoryText?: string;
|
||||
embedding?: number[];
|
||||
};
|
||||
|
||||
interface VoyageEmbeddingResponse {
|
||||
data?: Array<{ embedding?: number[] }>;
|
||||
}
|
||||
|
||||
function normalize(value: string | undefined): string {
|
||||
return (value ?? '').trim().toLowerCase();
|
||||
}
|
||||
|
||||
function signature(collision: Collision): string {
|
||||
return [normalize(collision.file), normalize(collision.symbol)].filter(Boolean).join('#');
|
||||
}
|
||||
|
||||
function memoryText(collision: Collision): string {
|
||||
return [
|
||||
`file: ${collision.file}`,
|
||||
collision.symbol ? `symbol: ${collision.symbol}` : undefined,
|
||||
`engineers: ${collision.engineers.join(', ')}`,
|
||||
`severity: ${collision.severity}`,
|
||||
collision.githubState?.unpushed ? 'unpushed local changes present' : undefined,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('\n');
|
||||
}
|
||||
|
||||
async function embed(text: string, inputType: 'document' | 'query'): Promise<number[] | null> {
|
||||
if (!env.VOYAGE_API_KEY) return null;
|
||||
try {
|
||||
const res = await fetch('https://api.voyageai.com/v1/embeddings', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
Authorization: `Bearer ${env.VOYAGE_API_KEY}`,
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
input: text,
|
||||
model: env.VOYAGE_EMBEDDING_MODEL,
|
||||
input_type: inputType,
|
||||
}),
|
||||
});
|
||||
if (!res.ok) {
|
||||
console.warn(`[memory] voyage embedding failed: ${res.status} ${await res.text()}`);
|
||||
return null;
|
||||
}
|
||||
const body = (await res.json()) as VoyageEmbeddingResponse;
|
||||
return body.data?.[0]?.embedding ?? null;
|
||||
} catch (err) {
|
||||
console.warn(`[memory] voyage embedding failed: ${(err as Error).message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export async function enrichCollisionMemory(collision: Collision): Promise<StoredCollision> {
|
||||
const text = memoryText(collision);
|
||||
const embedding = await embed(text, 'document');
|
||||
return {
|
||||
...collision,
|
||||
memorySignature: signature(collision),
|
||||
memoryText: text,
|
||||
...(embedding ? { embedding } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
async function recallByVector(collision: Collision): Promise<Collision | null> {
|
||||
const queryVector = await embed(memoryText(collision), 'query');
|
||||
if (!queryVector) return null;
|
||||
|
||||
try {
|
||||
const db = await getDb();
|
||||
const [match] = await db
|
||||
.collection<StoredCollision>('collisions')
|
||||
.aggregate<StoredCollision>([
|
||||
{
|
||||
$vectorSearch: {
|
||||
index: 'collision_embedding',
|
||||
path: 'embedding',
|
||||
queryVector,
|
||||
numCandidates: 50,
|
||||
limit: 5,
|
||||
filter: { podId: collision.podId },
|
||||
},
|
||||
},
|
||||
{ $match: { id: { $ne: collision.id } } },
|
||||
{ $project: { _id: 0, embedding: 0 } },
|
||||
])
|
||||
.toArray();
|
||||
return match ?? null;
|
||||
} catch (err) {
|
||||
console.warn(`[memory] vector recall unavailable: ${(err as Error).message}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function recallBySignature(collision: Collision): Promise<Collision | null> {
|
||||
const db = await getDb();
|
||||
const sig = signature(collision);
|
||||
const match = await db.collection<StoredCollision>('collisions').findOne(
|
||||
{
|
||||
podId: collision.podId,
|
||||
id: { $ne: collision.id },
|
||||
$or: [{ memorySignature: sig }, { file: collision.file }],
|
||||
},
|
||||
{ sort: { detectedAt: -1 }, projection: { _id: 0, embedding: 0 } },
|
||||
);
|
||||
return match ?? null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Vector-based recall of prior collision patterns (Loop A).
|
||||
* Stub: returns null until Voyage + Atlas Vector Search are wired.
|
||||
* Recall prior collision patterns. Exact Mongo recall is always available;
|
||||
* Voyage + Atlas Vector Search is used first when configured.
|
||||
*/
|
||||
export async function recallSimilar(_collision: Collision): Promise<Collision | null> {
|
||||
// TODO(memory): embed collision.file via Voyage, query Atlas vector index
|
||||
return null;
|
||||
export async function recallSimilar(collision: Collision): Promise<Collision | null> {
|
||||
return (await recallByVector(collision)) ?? recallBySignature(collision);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user