UNPKG

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.

237 lines (236 loc) 8.11 kB
/** * Idempotent Write Utility * * Provides content-based deduplication for metrics logging using SHA256 hashing. * Part of Task 2.3: Unified Metrics and Execution Logging * * Features: * - Content-based idempotency keys (prevents duplicate metrics) * - TTL-based cleanup (24-hour retention) * - Atomic write tracking across databases */ import * as crypto from 'crypto'; import { createLogger } from './logging.js'; const logger = createLogger('idempotent-write'); /** * Default TTL for idempotency keys (24 hours) */ const DEFAULT_TTL_MS = 24 * 60 * 60 * 1000; /** * Create idempotency key from execution metrics * * Key is based on: agent_id + task_id + timestamp + duration_ms * This combination ensures uniqueness while allowing retries to be detected * * @param metrics - Execution metrics to create key from * @returns SHA256 hash as idempotency key */ export function createIdempotentKey(metrics) { // Normalize timestamp to ISO string for consistent hashing const timestampStr = metrics.timestamp instanceof Date ? metrics.timestamp.toISOString() : new Date(metrics.timestamp).toISOString(); // Build content string for hashing const content = [ metrics.agent_id, metrics.task_id, timestampStr, metrics.duration_ms.toString(), metrics.status ].join(':'); // Generate SHA256 hash const hash = crypto.createHash('sha256').update(content).digest('hex'); logger.debug('Created idempotency key', { agent_id: metrics.agent_id, task_id: metrics.task_id, key: hash.substring(0, 16) + '...' }); return hash; } /** * Check if metrics have already been written * * @param key - Idempotency key to check * @param db - Database adapter * @returns True if already written, false otherwise */ export async function hasBeenWritten(key, db) { try { // Query idempotency_keys table const result = await db.get(`idempotency_keys:${key}`); if (!result) { logger.debug('Idempotency key not found', { key: key.substring(0, 16) + '...' }); return false; } // Check if key has expired const expiresAt = new Date(result.expires_at); const now = new Date(); if (expiresAt < now) { logger.debug('Idempotency key expired', { key: key.substring(0, 16) + '...', expired_at: expiresAt.toISOString() }); return false; } logger.debug('Idempotency key found', { key: key.substring(0, 16) + '...', written_at: result.written_at }); return true; } catch (error) { logger.error('Failed to check idempotency key', { key: key.substring(0, 16) + '...', error: error instanceof Error ? error.message : String(error) }); // On error, assume not written to allow retry return false; } } /** * Mark metrics as written * * @param key - Idempotency key * @param db - Database adapter * @param metricsId - Optional metrics ID to link * @param ttlMs - Time-to-live in milliseconds (default: 24 hours) */ export async function markWritten(key, db, metricsId, ttlMs = DEFAULT_TTL_MS) { try { const now = new Date(); const expiresAt = new Date(now.getTime() + ttlMs); const idempotencyRecord = { key, metrics_id: metricsId, written_at: now, expires_at: expiresAt }; // Insert into idempotency_keys table await db.insert('idempotency_keys', idempotencyRecord); logger.debug('Marked idempotency key as written', { key: key.substring(0, 16) + '...', metrics_id: metricsId, expires_at: expiresAt.toISOString() }); } catch (error) { logger.error('Failed to mark idempotency key', { key: key.substring(0, 16) + '...', error: error instanceof Error ? error.message : String(error) }); throw error; } } /** * Batch check for written metrics * * @param keys - Array of idempotency keys * @param db - Database adapter * @returns Map of key -> boolean (written status) */ export async function batchCheckWritten(keys, db) { const results = new Map(); try { // Check all keys in parallel const checks = keys.map(async (key)=>{ const written = await hasBeenWritten(key, db); return { key, written }; }); const checkResults = await Promise.all(checks); // Build result map checkResults.forEach(({ key, written })=>{ results.set(key, written); }); logger.debug('Batch idempotency check complete', { total_keys: keys.length, already_written: Array.from(results.values()).filter((v)=>v).length }); return results; } catch (error) { logger.error('Failed to batch check idempotency keys', { total_keys: keys.length, error: error instanceof Error ? error.message : String(error) }); throw error; } } /** * Batch mark metrics as written * * @param entries - Array of { key, metricsId? } entries * @param db - Database adapter * @param ttlMs - Time-to-live in milliseconds (default: 24 hours) */ export async function batchMarkWritten(entries, db, ttlMs = DEFAULT_TTL_MS) { try { const now = new Date(); const expiresAt = new Date(now.getTime() + ttlMs); const records = entries.map(({ key, metricsId })=>({ key, metrics_id: metricsId, written_at: now, expires_at: expiresAt })); // Batch insert await db.insertMany('idempotency_keys', records); logger.debug('Batch marked idempotency keys as written', { total_keys: entries.length, expires_at: expiresAt.toISOString() }); } catch (error) { logger.error('Failed to batch mark idempotency keys', { total_keys: entries.length, error: error instanceof Error ? error.message : String(error) }); throw error; } } /** * Cleanup expired idempotency keys * * @param db - Database adapter * @returns Number of keys deleted */ export async function cleanupExpiredKeys(db) { try { const now = new Date(); // Query for expired keys const expiredKeys = await db.query('idempotency_keys', [ { field: 'expires_at', operator: 'lt', value: now } ]); // Delete expired keys let deletedCount = 0; for (const key of expiredKeys){ await db.delete('idempotency_keys', key.key); deletedCount++; } logger.info('Cleaned up expired idempotency keys', { deleted_count: deletedCount }); return deletedCount; } catch (error) { logger.error('Failed to cleanup expired idempotency keys', { error: error instanceof Error ? error.message : String(error) }); throw error; } } /** * Validate cost accuracy (within $0.001 precision) * * @param cost - Cost value to validate * @returns True if cost is accurate to $0.001 */ export function validateCostAccuracy(cost) { const rounded = parseFloat(cost.toFixed(3)); return Math.abs(cost - rounded) < 0.0001; } /** * Round cost to $0.001 precision * * Uses standard "round half up" behavior for consistent rounding. * * @param cost - Cost value to round * @returns Cost rounded to 3 decimal places */ export function roundCost(cost) { // Use Math.round for proper "round half up" behavior // Multiply by 1000, round, then divide by 1000 for $0.001 precision return Math.round(cost * 1000) / 1000; } //# sourceMappingURL=idempotent-write.js.map