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.
388 lines (387 loc) • 14.6 kB
JavaScript
/**
* Reflection Logger Service
* Task 5.3: ACE Reflection Persistence Standardization
*
* Manages reflection data flow: PostgreSQL → Redis (24h TTL) with automatic archival
*
* Performance targets:
* - Write to Redis: <100ms
* - Query spanning both: <200ms
* - Archive to PostgreSQL: <500ms (background)
*/ import { StandardError, ErrorCode } from '../lib/errors.js';
import { logger } from '../lib/logging.js';
const REDIS_TTL_SECONDS = 86400; // 24 hours
const ARCHIVE_THRESHOLD_SECONDS = 3600; // Archive when TTL < 1 hour
export class ReflectionLogger {
dbService;
redisAvailable = true;
reflectionLossCount = 0;
constructor(dbService){
this.dbService = dbService;
}
/**
* Log a reflection to Redis with 24h TTL and PostgreSQL for persistence
* Performance target: <100ms
*/ async logReflection(reflection) {
const startTime = Date.now();
// Validate reflection schema
this.validateReflection(reflection);
// Ensure timestamp is set
const reflectionWithTimestamp = {
...reflection,
timestamp: reflection.timestamp || new Date()
};
// Generate Redis key
const redisKey = this.buildRedisKey(reflectionWithTimestamp.agent_id, reflectionWithTimestamp.timestamp);
// Serialize reflection
const serialized = JSON.stringify(reflectionWithTimestamp);
try {
// Write to Redis with TTL
const redisAdapter = this.dbService.getAdapter('redis');
await redisAdapter.setex(redisKey, REDIS_TTL_SECONDS, serialized);
logger.debug('Reflection logged to Redis', {
agent_id: reflectionWithTimestamp.agent_id,
task_id: reflectionWithTimestamp.task_id,
key: redisKey,
ttl: REDIS_TTL_SECONDS
});
this.redisAvailable = true;
} catch (error) {
// Graceful degradation: Redis unavailable
this.redisAvailable = false;
this.reflectionLossCount++;
logger.warn('Redis unavailable, falling back to PostgreSQL only', {
error: error instanceof Error ? error.message : String(error),
reflection_loss_count: this.reflectionLossCount
});
}
// Always write to PostgreSQL for long-term persistence
try {
await this.writeToPostgreSQL(reflectionWithTimestamp);
} catch (error) {
throw new StandardError(ErrorCode.DATABASE_ERROR, 'Failed to persist reflection to PostgreSQL', {
reflection,
error: error instanceof Error ? error.message : String(error)
});
}
const duration = Date.now() - startTime;
logger.debug('Reflection logged', {
agent_id: reflectionWithTimestamp.agent_id,
duration_ms: duration,
redis_available: this.redisAvailable
});
if (duration >= 100) {
logger.warn('Reflection write exceeded performance target', {
duration_ms: duration,
target_ms: 100
});
}
}
/**
* Query reflections spanning both Redis (recent) and PostgreSQL (archived)
* Performance target: <200ms
*/ async queryReflections(query) {
const startTime = Date.now();
const results = [];
// Query Redis for recent reflections
if (this.redisAvailable) {
try {
const redisResults = await this.queryRedis(query);
results.push(...redisResults);
} catch (error) {
logger.warn('Redis query failed, using PostgreSQL only', {
error: error instanceof Error ? error.message : String(error)
});
this.redisAvailable = false;
}
}
// Query PostgreSQL for archived reflections
try {
const pgResults = await this.queryPostgreSQL(query);
results.push(...pgResults);
} catch (error) {
logger.error('PostgreSQL query failed', {
error: error instanceof Error ? error.message : String(error)
});
}
// Remove duplicates (prefer Redis version for recent data)
const uniqueResults = this.deduplicateResults(results);
// Sort by timestamp descending
uniqueResults.sort((a, b)=>{
const timeA = a.timestamp?.getTime() || 0;
const timeB = b.timestamp?.getTime() || 0;
return timeB - timeA;
});
const duration = Date.now() - startTime;
logger.debug('Reflections queried', {
count: uniqueResults.length,
duration_ms: duration
});
if (duration >= 200) {
logger.warn('Reflection query exceeded performance target', {
duration_ms: duration,
target_ms: 200
});
}
return uniqueResults;
}
/**
* Get aggregate statistics for reflections
*/ async getReflectionStats(agentId) {
const query = agentId ? {
agent_id: agentId
} : {};
const reflections = await this.queryReflections(query);
if (reflections.length === 0) {
return {
total_count: 0,
average_confidence: 0,
min_confidence: 0,
max_confidence: 0
};
}
const confidences = reflections.map((r)=>r.confidence);
const sum = confidences.reduce((a, b)=>a + b, 0);
const byType = {};
reflections.forEach((r)=>{
byType[r.reflection_type] = (byType[r.reflection_type] || 0) + 1;
});
return {
total_count: reflections.length,
average_confidence: sum / reflections.length,
min_confidence: Math.min(...confidences),
max_confidence: Math.max(...confidences),
by_type: byType
};
}
/**
* Archive reflections approaching TTL expiration to PostgreSQL
* Runs as background task
* Performance target: <500ms per reflection
*/ async archiveExpiredReflections() {
if (!this.redisAvailable) {
logger.debug('Redis unavailable, skipping archive check');
return 0;
}
try {
const redisAdapter = this.dbService.getAdapter('redis');
// Get all reflection keys
const keys = await redisAdapter.keys('reflection:*');
let archivedCount = 0;
for (const key of keys){
try {
// Check TTL
const ttl = await redisAdapter.ttl(key);
// Archive if TTL is approaching expiration
if (ttl > 0 && ttl < ARCHIVE_THRESHOLD_SECONDS) {
const data = await redisAdapter.get(key);
if (data) {
const reflection = JSON.parse(data);
// Ensure it's in PostgreSQL (idempotent)
await this.writeToPostgreSQL(reflection);
archivedCount++;
logger.debug('Reflection archived before expiration', {
key,
ttl,
agent_id: reflection.agent_id
});
}
}
} catch (error) {
logger.error('Failed to archive reflection', {
key,
error: error instanceof Error ? error.message : String(error)
});
}
}
return archivedCount;
} catch (error) {
logger.error('Archive scan failed', {
error: error instanceof Error ? error.message : String(error)
});
return 0;
}
}
/**
* Get reflection loss monitoring metrics
*/ getMonitoringMetrics() {
return {
redis_available: this.redisAvailable,
reflection_loss_count: this.reflectionLossCount
};
}
// ========== Private Methods ==========
validateReflection(reflection) {
const errors = [];
if (!reflection.agent_id || reflection.agent_id.trim() === '') {
errors.push('agent_id is required and must not be empty');
}
if (!reflection.task_id || reflection.task_id.trim() === '') {
errors.push('task_id is required and must not be empty');
}
if (![
'confidence',
'status',
'progress',
'error',
'decision'
].includes(reflection.reflection_type)) {
errors.push('reflection_type must be one of: confidence, status, progress, error, decision');
}
if (typeof reflection.confidence !== 'number' || reflection.confidence < 0 || reflection.confidence > 1) {
errors.push('confidence must be a number between 0 and 1');
}
if (!reflection.payload || typeof reflection.payload !== 'object') {
errors.push('payload must be an object');
}
if (errors.length > 0) {
throw new StandardError(ErrorCode.VALIDATION_ERROR, 'Reflection validation failed', {
errors,
reflection
});
}
}
buildRedisKey(agentId, timestamp) {
const isoTimestamp = timestamp.toISOString();
return `reflection:${agentId}:${isoTimestamp}`;
}
async writeToPostgreSQL(reflection) {
const pgAdapter = this.dbService.getAdapter('postgres');
const query = `
INSERT INTO reflections (
agent_id,
task_id,
reflection_type,
confidence,
payload,
timestamp
) VALUES ($1, $2, $3, $4, $5, $6)
ON CONFLICT (agent_id, task_id, timestamp)
DO UPDATE SET
reflection_type = EXCLUDED.reflection_type,
confidence = EXCLUDED.confidence,
payload = EXCLUDED.payload
`;
const params = [
reflection.agent_id,
reflection.task_id,
reflection.reflection_type,
reflection.confidence,
JSON.stringify(reflection.payload),
reflection.timestamp || new Date()
];
await pgAdapter.execute(query, params);
}
async queryRedis(query) {
const redisAdapter = this.dbService.getAdapter('redis');
// Build Redis key pattern
let pattern = 'reflection:';
if (query.agent_id) {
pattern += `${query.agent_id}:`;
} else {
pattern += '*:';
}
pattern += '*';
const keys = await redisAdapter.keys(pattern);
const results = [];
for (const key of keys){
try {
const data = await redisAdapter.get(key);
if (data) {
const reflection = JSON.parse(data);
// Apply filters
if (this.matchesQuery(reflection, query)) {
results.push(reflection);
}
}
} catch (error) {
logger.warn('Failed to parse Redis reflection', {
key,
error: error instanceof Error ? error.message : String(error)
});
}
}
return results;
}
async queryPostgreSQL(query) {
const pgAdapter = this.dbService.getAdapter('postgres');
const conditions = [];
const params = [];
let paramIndex = 1;
if (query.agent_id) {
conditions.push(`agent_id = $${paramIndex++}`);
params.push(query.agent_id);
}
if (query.task_id) {
conditions.push(`task_id = $${paramIndex++}`);
params.push(query.task_id);
}
if (query.reflection_type) {
conditions.push(`reflection_type = $${paramIndex++}`);
params.push(query.reflection_type);
}
if (query.start_date) {
conditions.push(`timestamp >= $${paramIndex++}`);
params.push(query.start_date);
}
if (query.end_date) {
conditions.push(`timestamp <= $${paramIndex++}`);
params.push(query.end_date);
}
if (query.min_confidence !== undefined) {
conditions.push(`confidence >= $${paramIndex++}`);
params.push(query.min_confidence);
}
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : '';
const sql = `
SELECT
agent_id,
task_id,
reflection_type,
confidence,
payload,
timestamp
FROM reflections
${whereClause}
ORDER BY timestamp DESC
`;
const result = await pgAdapter.query(sql, params);
return result.rows.map((row)=>({
agent_id: row.agent_id,
task_id: row.task_id,
reflection_type: row.reflection_type,
confidence: row.confidence,
payload: typeof row.payload === 'string' ? JSON.parse(row.payload) : row.payload,
timestamp: new Date(row.timestamp)
}));
}
matchesQuery(reflection, query) {
if (query.task_id && reflection.task_id !== query.task_id) {
return false;
}
if (query.reflection_type && reflection.reflection_type !== query.reflection_type) {
return false;
}
if (query.min_confidence !== undefined && reflection.confidence < query.min_confidence) {
return false;
}
if (query.start_date && reflection.timestamp && reflection.timestamp < query.start_date) {
return false;
}
if (query.end_date && reflection.timestamp && reflection.timestamp > query.end_date) {
return false;
}
return true;
}
deduplicateResults(results) {
const seen = new Map();
for (const reflection of results){
const key = `${reflection.agent_id}:${reflection.task_id}:${reflection.timestamp?.toISOString()}`;
if (!seen.has(key)) {
seen.set(key, reflection);
}
}
return Array.from(seen.values());
}
}
//# sourceMappingURL=reflection-logger.js.map