claude-flow-novice
Version:
Claude Flow Novice - Advanced orchestration platform for multi-agent AI workflows with CFN Loop architecture Includes Local RuVector Accelerator and all CFN skills for complete functionality.
284 lines (283 loc) • 11.5 kB
JavaScript
/**
* Skill Execution Logger - Phase 7.2
*
* Dual logging system that logs skill executions to:
* 1. SQLite (Skills DB) - ALL skills
* 2. PostgreSQL (Phase 4 DB) - ONLY Phase4-generated skills
*
* Features:
* - Automatic skill ID lookup and caching
* - Phase4 skill detection based on generated_by='phase4' or phase4_pattern_id
* - Graceful PostgreSQL fallback (non-blocking)
* - Connection pooling for performance
* - Environment variable configuration
* - <50ms logging performance target
*
* Usage:
* const logger = new SkillExecutionLogger();
* await logger.logSkillExecution({
* agentId: 'backend-dev-1',
* agentType: 'backend-developer',
* skillName: 'jwt-authentication',
* executionTimeMs: 12,
* exitCode: 0
* });
* await logger.close();
*/ import Database from 'better-sqlite3';
import { Pool } from 'pg';
import { existsSync } from 'fs';
import path from 'path';
// ============================================================================
// SkillExecutionLogger Class
// ============================================================================
export class SkillExecutionLogger {
sqliteDb;
postgresPool;
config;
skillCache = new Map();
POSTGRES_TIMEOUT_MS = 5000;
/**
* Initialize logger with optional configuration
* Falls back to environment variables if not provided
*/ constructor(config){
// Merge config with environment variables and defaults
this.config = {
sqliteDbPath: config?.sqliteDbPath || process.env.CFN_SKILLS_DB_PATH || './.claude/skills-database/skills.db',
postgresHost: config?.postgresHost || process.env.PHASE4_POSTGRES_HOST,
postgresPort: config?.postgresPort || parseInt(process.env.PHASE4_POSTGRES_PORT || '5432', 10),
postgresDb: config?.postgresDb || process.env.PHASE4_POSTGRES_DB || 'workflow_codification',
postgresUser: config?.postgresUser || process.env.PHASE4_POSTGRES_USER || 'postgres',
postgresPass: config?.postgresPass || process.env.PHASE4_POSTGRES_PASS || '',
enablePostgres: config?.enablePostgres !== undefined ? config.enablePostgres : process.env.ENABLE_PHASE4_LOGGING === 'true'
};
// Initialize SQLite connection
const dbPath = path.resolve(this.config.sqliteDbPath);
if (!existsSync(dbPath)) {
throw new Error(`Skills database not found at: ${dbPath}`);
}
this.sqliteDb = new Database(dbPath);
// Initialize PostgreSQL connection pool (if enabled)
if (this.config.enablePostgres && this.config.postgresHost) {
try {
const poolConfig = {
host: this.config.postgresHost,
port: this.config.postgresPort,
database: this.config.postgresDb,
user: this.config.postgresUser,
password: this.config.postgresPass,
max: 10,
idleTimeoutMillis: 30000,
connectionTimeoutMillis: this.POSTGRES_TIMEOUT_MS
};
this.postgresPool = new Pool(poolConfig);
// Test connection (don't fail if it doesn't work)
this.postgresPool.query('SELECT 1').then(()=>{
console.log('[SkillExecutionLogger] PostgreSQL connection pool established');
}).catch((err)=>{
console.warn(`[SkillExecutionLogger] PostgreSQL connection failed, will fallback to SQLite only:`, err.message);
});
} catch (err) {
console.warn(`[SkillExecutionLogger] Failed to create PostgreSQL pool, using SQLite only:`, err instanceof Error ? err.message : String(err));
this.postgresPool = undefined;
}
}
}
/**
* Log skill execution to appropriate databases
* - SQLite: ALL skills
* - PostgreSQL: ONLY Phase4-generated skills (if enabled)
*/ async logSkillExecution(metrics) {
const startTime = Date.now();
try {
// Step 1: Get skill ID and metadata
const skillId = metrics.skillId || await this.getSkillIdByName(metrics.skillName);
if (!skillId) {
throw new Error(`Skill not found: ${metrics.skillName}`);
}
// Step 2: Get skill metadata (with caching)
const skillMetadata = await this.getSkillMetadata(metrics.skillName, skillId);
// Step 3: Log to SQLite (always, critical operation)
await this.logToSQLite(metrics, skillId, skillMetadata);
// Step 4: Log to PostgreSQL (if Phase4 skill and PostgreSQL enabled)
if (skillMetadata.isPhase4Generated && this.postgresPool) {
// Non-blocking PostgreSQL log (don't wait, don't fail)
this.logToPostgreSQL(metrics, skillId, skillMetadata).catch((err)=>{
console.warn(`[SkillExecutionLogger] PostgreSQL logging failed for skill ${metrics.skillName}:`, err.message);
});
}
const duration = Date.now() - startTime;
if (duration > 50) {
console.warn(`[SkillExecutionLogger] Logging took ${duration}ms (target: <50ms) for skill ${metrics.skillName}`);
}
} catch (err) {
console.error(`[SkillExecutionLogger] Failed to log skill execution:`, err instanceof Error ? err.message : String(err));
throw err;
}
}
/**
* Log execution to SQLite (Skills DB)
* This is the primary, critical logging operation
*/ async logToSQLite(metrics, skillId, skillMetadata) {
try {
const stmt = this.sqliteDb.prepare(`
INSERT INTO skill_usage_log (
agent_id,
agent_type,
skill_id,
task_id,
phase,
loaded_at,
execution_time_ms,
confidence_before,
confidence_after,
success_indicator,
approval_level,
phase4_generated,
exit_code
) VALUES (?, ?, ?, ?, ?, datetime('now'), ?, ?, ?, ?, ?, ?, ?)
`);
stmt.run(metrics.agentId, metrics.agentType, skillId, metrics.taskId || null, metrics.phase || null, metrics.executionTimeMs, metrics.confidenceBefore || null, metrics.confidenceAfter || null, metrics.exitCode === 0 ? 1 : 0, skillMetadata.approvalLevel, skillMetadata.isPhase4Generated ? 1 : 0, metrics.exitCode);
} catch (err) {
throw new Error(`SQLite logging failed: ${err instanceof Error ? err.message : String(err)}`);
}
}
/**
* Log execution to PostgreSQL (Phase 4 DB)
* This is a non-critical, best-effort operation
*
* PostgreSQL schema:
* CREATE TABLE skill_executions (
* id SERIAL PRIMARY KEY,
* skill_id INTEGER NOT NULL,
* team_id VARCHAR(100) NOT NULL,
* task_id VARCHAR(100),
* execution_time_ms INTEGER,
* exit_code INTEGER,
* cost_avoided_usd DECIMAL(10,4),
* tokens_avoided INTEGER,
* timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP
* );
*/ async logToPostgreSQL(metrics, skillId, skillMetadata) {
if (!this.postgresPool) {
return; // Silently skip if pool not available
}
try {
// Use phase4_pattern_id as the skill_id in PostgreSQL
const phase4SkillId = skillMetadata.phase4PatternId || skillId;
const query = `
INSERT INTO skill_executions (
skill_id,
team_id,
task_id,
execution_time_ms,
exit_code,
cost_avoided_usd,
tokens_avoided,
timestamp
) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW())
`;
const values = [
phase4SkillId,
metrics.agentType,
metrics.taskId || null,
metrics.executionTimeMs,
metrics.exitCode,
metrics.costAvoidedUsd || null,
metrics.tokensAvoided || null
];
// Execute with timeout
await Promise.race([
this.postgresPool.query(query, values),
new Promise((_, reject)=>setTimeout(()=>reject(new Error('PostgreSQL query timeout')), this.POSTGRES_TIMEOUT_MS))
]);
} catch (err) {
// Don't throw, just log warning
console.warn(`[SkillExecutionLogger] PostgreSQL insert failed:`, err instanceof Error ? err.message : String(err));
}
}
/**
* Get skill ID by name with caching
*/ async getSkillIdByName(skillName) {
// Check cache first
if (this.skillCache.has(skillName)) {
return this.skillCache.get(skillName).skillId;
}
// Query database
try {
const row = this.sqliteDb.prepare(`
SELECT
id,
approval_level,
generated_by,
phase4_pattern_id
FROM skills
WHERE name = ?
`).get(skillName);
if (!row) {
return null;
}
// Cache the result
this.skillCache.set(skillName, {
skillId: row.id,
approvalLevel: row.approval_level,
isPhase4Generated: row.generated_by === 'phase4' || row.phase4_pattern_id !== null,
phase4PatternId: row.phase4_pattern_id
});
return row.id;
} catch (err) {
throw new Error(`Failed to lookup skill ID: ${err instanceof Error ? err.message : String(err)}`);
}
}
/**
* Get skill metadata (approval level, Phase4 status) with caching
*/ async getSkillMetadata(skillName, skillId) {
// Check cache first
if (this.skillCache.has(skillName)) {
return this.skillCache.get(skillName);
}
// Query database
try {
const row = this.sqliteDb.prepare(`
SELECT
approval_level,
generated_by,
phase4_pattern_id
FROM skills
WHERE id = ?
`).get(skillId);
if (!row) {
throw new Error(`Skill not found with ID: ${skillId}`);
}
// Cache the result
const metadata = {
skillId,
approvalLevel: row.approval_level,
isPhase4Generated: row.generated_by === 'phase4' || row.phase4_pattern_id !== null,
phase4PatternId: row.phase4_pattern_id
};
this.skillCache.set(skillName, metadata);
return metadata;
} catch (err) {
throw new Error(`Failed to get skill metadata: ${err instanceof Error ? err.message : String(err)}`);
}
}
/**
* Close database connections and cleanup
*/ async close() {
try {
// Close SQLite
if (this.sqliteDb) {
this.sqliteDb.close();
}
// Close PostgreSQL pool
if (this.postgresPool) {
await this.postgresPool.end();
}
// Clear cache
this.skillCache.clear();
} catch (err) {
console.error(`[SkillExecutionLogger] Error during cleanup:`, err instanceof Error ? err.message : String(err));
}
}
}
//# sourceMappingURL=skill-execution-logger.js.map