clawdie-ai/src/split-brain-status.ts

212 lines
6.4 KiB
TypeScript

import fs from 'fs';
import path from 'path';
import pg from 'pg';
import {
MEMORY_DB_URL,
PROJECT_ROOT,
SKILLS_DB_URL,
} from './config.js';
import {
BUILTIN_KNOWLEDGE_ARTIFACT_SQL,
BUILTIN_KNOWLEDGE_METADATA_JSON,
readBuiltinKnowledgeMetadata,
} from './builtin-knowledge-artifact.js';
export type DbAvailability = 'available' | 'missing';
export type ArtifactPresence = 'present' | 'missing';
export type SkillsArtifactStatus =
| 'ready'
| 'missing'
| 'incomplete'
| 'unavailable'
| 'unknown';
export interface SplitBrainStatus {
skillsDb: DbAvailability;
memoryDb: DbAvailability;
skillsArtifactSql: ArtifactPresence;
skillsArtifactMetadata: ArtifactPresence;
skillsArtifact: SkillsArtifactStatus;
skillsArtifactVersion: string;
skillsArtifactDbVersion: string;
skillsRuntimeLookup: ArtifactPresence;
skillsArtifactRows: number;
skillsDocumentRows: number;
skillsChunkRows: number;
memoryRows: number;
memoryChunkRows: number;
memoryEmbeddingRows: number;
}
const { Client } = pg;
async function verifyDb(connectionString: string): Promise<DbAvailability> {
try {
const client = new Client({
connectionString,
connectionTimeoutMillis: 5000,
});
await client.connect();
try {
await client.query('SELECT 1');
return 'available';
} finally {
await client.end();
}
} catch {
return 'missing';
}
}
export async function collectSplitBrainStatus(): Promise<SplitBrainStatus> {
const metadata = readBuiltinKnowledgeMetadata();
const status: SplitBrainStatus = {
skillsDb: 'missing',
memoryDb: 'missing',
skillsArtifactSql: fs.existsSync(BUILTIN_KNOWLEDGE_ARTIFACT_SQL)
? 'present'
: 'missing',
skillsArtifactMetadata: fs.existsSync(BUILTIN_KNOWLEDGE_METADATA_JSON)
? 'present'
: 'missing',
skillsArtifact: 'unknown',
skillsArtifactVersion: String(metadata?.artifact_version ?? 'unknown'),
skillsArtifactDbVersion: 'unknown',
skillsRuntimeLookup: fs.existsSync(path.join(PROJECT_ROOT, 'src', 'skills-pg.ts'))
? 'present'
: 'missing',
skillsArtifactRows: 0,
skillsDocumentRows: 0,
skillsChunkRows: 0,
memoryRows: 0,
memoryChunkRows: 0,
memoryEmbeddingRows: 0,
};
const [skillsDb, memoryDb] = await Promise.all([
verifyDb(SKILLS_DB_URL),
verifyDb(MEMORY_DB_URL),
]);
status.skillsDb = skillsDb;
status.memoryDb = memoryDb;
if (status.skillsDb === 'available') {
try {
const client = new Client({
connectionString: SKILLS_DB_URL,
connectionTimeoutMillis: 5000,
});
await client.connect();
try {
const [artifactRows, documentRows, chunkRows, latestArtifact] =
await Promise.all([
client.query<{ count: string }>(
'SELECT count(*) FROM builtin_knowledge_artifacts',
),
client.query<{ count: string }>(
'SELECT count(*) FROM builtin_knowledge_documents',
),
client.query<{ count: string }>(
'SELECT count(*) FROM builtin_knowledge_chunks',
),
client.query<{ artifact_version: string }>(
'SELECT artifact_version FROM builtin_knowledge_artifacts ORDER BY created_at DESC LIMIT 1',
),
]);
status.skillsArtifactRows = Number(artifactRows.rows[0]?.count ?? 0);
status.skillsDocumentRows = Number(documentRows.rows[0]?.count ?? 0);
status.skillsChunkRows = Number(chunkRows.rows[0]?.count ?? 0);
status.skillsArtifactDbVersion =
latestArtifact.rows[0]?.artifact_version ?? 'unknown';
const expectedDocuments = Number(metadata?.document_count ?? 0);
const expectedChunks = Number(metadata?.chunk_count ?? 0);
if (
status.skillsArtifactSql === 'missing' ||
status.skillsArtifactMetadata === 'missing' ||
status.skillsArtifactRows < 1
) {
status.skillsArtifact = 'missing';
} else if (
status.skillsDocumentRows < Math.max(expectedDocuments, 1) ||
status.skillsChunkRows < Math.max(expectedChunks, 1) ||
(status.skillsArtifactVersion !== 'unknown' &&
status.skillsArtifactDbVersion !== 'unknown' &&
status.skillsArtifactVersion !== status.skillsArtifactDbVersion)
) {
status.skillsArtifact = 'incomplete';
} else {
status.skillsArtifact = 'ready';
}
} finally {
await client.end();
}
} catch {
status.skillsArtifact = 'unknown';
}
} else {
status.skillsArtifact = 'unavailable';
}
if (status.memoryDb === 'available') {
try {
const client = new Client({
connectionString: MEMORY_DB_URL,
connectionTimeoutMillis: 5000,
});
await client.connect();
try {
const [memoryRows, memoryChunkRows, memoryEmbeddingRows] =
await Promise.all([
client.query<{ count: string }>('SELECT count(*) FROM memories'),
client.query<{ count: string }>('SELECT count(*) FROM memory_chunks'),
client.query<{ count: string }>('SELECT count(*) FROM memory_embeddings'),
]);
status.memoryRows = Number(memoryRows.rows[0]?.count ?? 0);
status.memoryChunkRows = Number(memoryChunkRows.rows[0]?.count ?? 0);
status.memoryEmbeddingRows = Number(memoryEmbeddingRows.rows[0]?.count ?? 0);
} finally {
await client.end();
}
} catch {
// Leave counts at zero; availability is already tracked above.
}
}
return status;
}
export function deriveSplitBrainReadiness(
status: SplitBrainStatus,
): 'ready' | 'degraded' {
return status.skillsDb === 'available' &&
status.memoryDb === 'available' &&
status.skillsArtifact === 'ready' &&
status.skillsRuntimeLookup === 'present'
? 'ready'
: 'degraded';
}
export function collectSplitBrainIssues(status: SplitBrainStatus): string[] {
const issues: string[] = [];
if (status.skillsDb !== 'available') {
issues.push('skills db is unavailable');
}
if (status.memoryDb !== 'available') {
issues.push('memory db is unavailable');
}
if (status.skillsArtifact !== 'ready') {
issues.push(`built-in knowledge artifact is ${status.skillsArtifact}`);
}
if (status.skillsRuntimeLookup !== 'present') {
issues.push('skills runtime lookup is missing');
}
return issues;
}