UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

518 lines (517 loc) 18.3 kB
import { d as onDiagnosticEvent } from "./diagnostic-events-Chmz2PSy.js"; //#region src/logging/diagnostic-stability.ts const DEFAULT_DIAGNOSTIC_STABILITY_CAPACITY = 1e3; const DEFAULT_DIAGNOSTIC_STABILITY_LIMIT = 50; const MAX_DIAGNOSTIC_STABILITY_LIMIT = DEFAULT_DIAGNOSTIC_STABILITY_CAPACITY; const LIVENESS_EVENT_LOOP_DELAY_WARN_MS = 1e3; const SAFE_REASON_CODE = /^[A-Za-z0-9_.:-]{1,120}$/u; function createState(capacity = DEFAULT_DIAGNOSTIC_STABILITY_CAPACITY) { return { records: Array.from({ length: capacity }), capacity, nextIndex: 0, count: 0, dropped: 0, unsubscribe: null }; } function getDiagnosticStabilityState() { const globalStore = globalThis; globalStore["__openclawDiagnosticStabilityState"] ??= createState(); return globalStore["__openclawDiagnosticStabilityState"]; } function copyMemory(memory) { return { ...memory }; } function copyReasonCode(reason) { if (!reason || !SAFE_REASON_CODE.test(reason)) return; return reason; } function assignReasonCode(record, reason) { const reasonCode = copyReasonCode(reason); if (reasonCode) record.reason = reasonCode; } function resolveDiagnosticLivenessRecordLevel(event) { const hasBlockingWork = event.waiting > 0 || event.queued > 0; const hasSustainedEventLoopDelay = (event.eventLoopDelayP99Ms ?? 0) >= LIVENESS_EVENT_LOOP_DELAY_WARN_MS; return hasBlockingWork || event.active > 0 && hasSustainedEventLoopDelay ? "warning" : "info"; } function isRecord(record) { return record !== void 0; } function sanitizeDiagnosticEvent(event) { const record = { seq: event.seq, ts: event.ts, type: event.type }; switch (event.type) { case "model.usage": record.channel = event.channel; record.provider = event.provider; record.model = event.model; record.usage = { ...event.usage }; record.context = event.context ? { ...event.context } : void 0; record.costUsd = event.costUsd; record.durationMs = event.durationMs; break; case "webhook.received": record.channel = event.channel; break; case "webhook.processed": record.channel = event.channel; record.durationMs = event.durationMs; break; case "webhook.error": record.channel = event.channel; break; case "message.queued": record.channel = event.channel; record.source = event.source; record.queueDepth = event.queueDepth; break; case "message.received": record.channel = event.channel; record.source = event.source; break; case "message.dispatch.started": record.channel = event.channel; record.source = event.source; break; case "message.dispatch.completed": record.channel = event.channel; record.source = event.source; record.durationMs = event.durationMs; record.outcome = event.outcome; assignReasonCode(record, event.reason); break; case "message.processed": record.channel = event.channel; record.durationMs = event.durationMs; record.outcome = event.outcome; assignReasonCode(record, event.reason); break; case "message.delivery.started": record.channel = event.channel; record.deliveryKind = event.deliveryKind; break; case "message.delivery.completed": record.channel = event.channel; record.deliveryKind = event.deliveryKind; record.durationMs = event.durationMs; record.resultCount = event.resultCount; record.outcome = "completed"; break; case "message.delivery.error": record.channel = event.channel; record.deliveryKind = event.deliveryKind; record.durationMs = event.durationMs; record.outcome = "error"; assignReasonCode(record, event.errorCategory); break; case "talk.event": record.talkEventType = event.talkEventType; record.mode = event.mode; record.transport = event.transport; record.brain = event.brain; record.provider = event.provider; record.final = event.final; record.durationMs = event.durationMs; record.bytes = event.byteLength; break; case "session.state": record.outcome = event.state; assignReasonCode(record, event.reason); record.queueDepth = event.queueDepth; break; case "session.long_running": case "session.stalled": case "session.stuck": record.outcome = event.state; if (event.type === "session.stuck") record.level = "warning"; assignReasonCode(record, event.reason); record.ageMs = event.ageMs; record.queueDepth = event.queueDepth; if (event.activeWorkKind) record.activeWorkKind = event.activeWorkKind; if (event.activeToolName) record.toolName = event.activeToolName; break; case "session.recovery.requested": record.outcome = event.state; record.action = event.allowActiveAbort ? "abort" : "recover"; record.ageMs = event.ageMs; record.queueDepth = event.queueDepth; if (event.activeWorkKind) record.activeWorkKind = event.activeWorkKind; assignReasonCode(record, event.reason); break; case "session.recovery.completed": record.outcome = event.status; record.action = event.action; record.ageMs = event.ageMs; record.queueDepth = event.queueDepth; record.count = event.released; if (event.activeWorkKind) record.activeWorkKind = event.activeWorkKind; assignReasonCode(record, event.outcomeReason ?? event.reason); break; case "session.turn.created": record.source = event.agentId; record.channel = event.channel; record.outcome = event.trigger; break; case "queue.lane.enqueue": record.source = event.lane; record.queueSize = event.queueSize; break; case "queue.lane.dequeue": record.source = event.lane; record.queueSize = event.queueSize; record.waitMs = event.waitMs; break; case "run.attempt": record.count = event.attempt; break; case "run.progress": assignReasonCode(record, event.reason); break; case "context.assembled": record.channel = event.channel; record.provider = event.provider; record.model = event.model; record.count = event.messageCount; record.bytes = event.promptChars; record.context = event.contextTokenBudget !== void 0 ? { limit: event.contextTokenBudget } : void 0; record.bytes = event.promptChars; break; case "diagnostic.heartbeat": record.webhooks = { ...event.webhooks }; record.active = event.active; record.waiting = event.waiting; record.queued = event.queued; break; case "diagnostic.liveness.warning": record.level = resolveDiagnosticLivenessRecordLevel(event); record.durationMs = event.intervalMs; record.count = event.reasons.length; assignReasonCode(record, event.reasons[0]); record.eventLoopDelayP99Ms = event.eventLoopDelayP99Ms; record.eventLoopDelayMaxMs = event.eventLoopDelayMaxMs; record.eventLoopUtilization = event.eventLoopUtilization; record.cpuCoreRatio = event.cpuCoreRatio; record.active = event.active; record.waiting = event.waiting; record.queued = event.queued; record.phase = event.phase; if (event.activeWorkLabels?.length) record.source = event.activeWorkLabels[0]; else if (event.queuedWorkLabels?.length) record.source = event.queuedWorkLabels[0]; break; case "diagnostic.phase.completed": record.phase = event.name; record.durationMs = event.durationMs; record.cpuCoreRatio = event.cpuCoreRatio; break; case "tool.loop": record.toolName = event.toolName; record.level = event.level; record.action = event.action; record.detector = event.detector; record.count = event.count; record.pairedToolName = event.pairedToolName; break; case "tool.execution.started": record.toolName = event.toolName; record.source = event.toolSource; record.pluginId = event.toolOwner; break; case "tool.execution.completed": record.toolName = event.toolName; record.source = event.toolSource; record.pluginId = event.toolOwner; record.durationMs = event.durationMs; break; case "tool.execution.error": record.toolName = event.toolName; record.source = event.toolSource; record.pluginId = event.toolOwner; record.durationMs = event.durationMs; assignReasonCode(record, event.errorCategory); break; case "tool.execution.blocked": record.toolName = event.toolName; record.source = event.toolSource; record.pluginId = event.toolOwner; record.outcome = "blocked"; assignReasonCode(record, event.deniedReason); break; case "skill.used": record.toolName = event.toolName; record.source = event.skillSource; record.action = event.activation; record.target = event.skillName; break; case "exec.process.completed": record.target = event.target; record.mode = event.mode; record.outcome = event.outcome; record.durationMs = event.durationMs; record.commandLength = event.commandLength; record.exitCode = event.exitCode; record.timedOut = event.timedOut; record.failureKind = event.failureKind; assignReasonCode(record, event.failureKind); break; case "run.started": record.provider = event.provider; record.model = event.model; record.channel = event.channel; break; case "run.completed": record.provider = event.provider; record.model = event.model; record.channel = event.channel; record.durationMs = event.durationMs; record.outcome = event.outcome; assignReasonCode(record, event.errorCategory); break; case "harness.run.started": record.source = event.harnessId; record.pluginId = event.pluginId; record.provider = event.provider; record.model = event.model; record.channel = event.channel; break; case "harness.run.completed": record.source = event.harnessId; record.pluginId = event.pluginId; record.provider = event.provider; record.model = event.model; record.channel = event.channel; record.durationMs = event.durationMs; record.outcome = event.outcome; record.count = event.itemLifecycle?.completedCount; break; case "harness.run.error": record.source = event.harnessId; record.pluginId = event.pluginId; record.provider = event.provider; record.model = event.model; record.channel = event.channel; record.durationMs = event.durationMs; record.outcome = "error"; record.action = event.phase; assignReasonCode(record, event.errorCategory); break; case "model.call.started": record.provider = event.provider; record.model = event.model; break; case "model.call.completed": record.provider = event.provider; record.model = event.model; record.durationMs = event.durationMs; record.requestBytes = event.requestPayloadBytes; record.responseBytes = event.responseStreamBytes; record.timeToFirstByteMs = event.timeToFirstByteMs; break; case "model.call.error": record.provider = event.provider; record.model = event.model; record.durationMs = event.durationMs; record.requestBytes = event.requestPayloadBytes; record.responseBytes = event.responseStreamBytes; record.timeToFirstByteMs = event.timeToFirstByteMs; record.failureKind = event.failureKind; record.memory = event.memory ? copyMemory(event.memory) : void 0; assignReasonCode(record, event.errorCategory); break; case "log.record": record.level = event.level; record.source = event.loggerName; break; case "diagnostic.memory.sample": record.memory = copyMemory(event.memory); break; case "diagnostic.memory.pressure": record.level = event.level; assignReasonCode(record, event.reason); record.memory = copyMemory(event.memory); record.thresholdBytes = event.thresholdBytes; record.rssGrowthBytes = event.rssGrowthBytes; record.windowMs = event.windowMs; break; case "payload.large": record.surface = event.surface; record.action = event.action; record.bytes = event.bytes; record.limitBytes = event.limitBytes; record.count = event.count; record.channel = event.channel; record.pluginId = event.pluginId; assignReasonCode(record, event.reason); break; case "telemetry.exporter": record.source = event.exporter; record.target = event.signal; record.outcome = event.status; assignReasonCode(record, event.reason ?? event.errorCategory); break; case "diagnostic.async_queue.dropped": record.droppedEvents = event.droppedEvents; record.droppedTrustedEvents = event.droppedTrustedEvents; record.droppedUntrustedEvents = event.droppedUntrustedEvents; record.droppedPriorityEvents = event.droppedPriorityEvents; record.queueLength = event.queueLength; record.maxQueueLength = event.maxQueueLength; record.drainBatchSize = event.drainBatchSize; break; case "model.failover": record.provider = event.fromProvider; record.model = event.fromModel; assignReasonCode(record, event.reason); break; } return record; } function appendRecord(record) { const state = getDiagnosticStabilityState(); state.records[state.nextIndex] = record; state.nextIndex = (state.nextIndex + 1) % state.capacity; if (state.count < state.capacity) { state.count += 1; return; } state.dropped += 1; } function listRecords() { const state = getDiagnosticStabilityState(); if (state.count === 0) return []; if (state.count < state.capacity) return state.records.slice(0, state.count).filter(isRecord); return [...state.records.slice(state.nextIndex), ...state.records.slice(0, state.nextIndex)].filter(isRecord); } function summarizeRecords(records) { const byType = {}; let latestMemory; let maxRssBytes; let maxHeapUsedBytes; let pressureCount = 0; const payloadLarge = { count: 0, rejected: 0, truncated: 0, chunked: 0, bySurface: {} }; for (const record of records) { byType[record.type] = (byType[record.type] ?? 0) + 1; if (record.memory) { latestMemory = record.memory; maxRssBytes = maxRssBytes === void 0 ? record.memory.rssBytes : Math.max(maxRssBytes, record.memory.rssBytes); maxHeapUsedBytes = maxHeapUsedBytes === void 0 ? record.memory.heapUsedBytes : Math.max(maxHeapUsedBytes, record.memory.heapUsedBytes); } if (record.type === "diagnostic.memory.pressure") pressureCount += 1; if (record.type === "payload.large") { payloadLarge.count += 1; if (record.action === "rejected") payloadLarge.rejected += 1; else if (record.action === "truncated") payloadLarge.truncated += 1; else if (record.action === "chunked") payloadLarge.chunked += 1; const surface = record.surface ?? "unknown"; payloadLarge.bySurface[surface] = (payloadLarge.bySurface[surface] ?? 0) + 1; } } return { byType, ...latestMemory || pressureCount > 0 ? { memory: { latest: latestMemory, maxRssBytes, maxHeapUsedBytes, pressureCount } } : {}, ...payloadLarge.count > 0 ? { payloadLarge } : {} }; } function selectRecords(records, options) { const { limit, type, sinceSeq } = normalizeDiagnosticStabilityQuery(options); const filtered = records.filter((record) => { if (type && record.type !== type) return false; if (sinceSeq !== void 0 && record.seq <= sinceSeq) return false; return true; }); return { filtered, events: filtered.slice(Math.max(0, filtered.length - limit)) }; } function parseOptionalNonNegativeInteger(value, field) { if (value === void 0 || value === null || value === "") return; const parsed = typeof value === "number" ? value : typeof value === "string" ? Number(value) : NaN; if (!Number.isInteger(parsed) || parsed < 0) throw new Error(`${field} must be a non-negative integer`); return parsed; } function parseOptionalType(value) { if (value === void 0 || value === null || value === "") return; if (typeof value !== "string" || value.trim() === "") throw new Error("type must be a non-empty string"); return value.trim(); } function normalizeLimit(limit, defaultLimit = DEFAULT_DIAGNOSTIC_STABILITY_LIMIT) { const parsed = parseOptionalNonNegativeInteger(limit, "limit"); if (parsed === void 0) return defaultLimit; if (parsed < 1 || parsed > 1e3) throw new Error(`limit must be between 1 and ${MAX_DIAGNOSTIC_STABILITY_LIMIT}`); return parsed; } /** Normalizes user-facing snapshot query options. */ function normalizeDiagnosticStabilityQuery(input = {}, options) { return { limit: normalizeLimit(input.limit, options?.defaultLimit), type: parseOptionalType(input.type), sinceSeq: parseOptionalNonNegativeInteger(input.sinceSeq, "sinceSeq") }; } /** Starts the process-wide diagnostic event recorder if it is not already active. */ function startDiagnosticStabilityRecorder() { const state = getDiagnosticStabilityState(); if (state.unsubscribe) return; state.unsubscribe = onDiagnosticEvent((event) => { appendRecord(sanitizeDiagnosticEvent(event)); }); } /** Stops the process-wide diagnostic event recorder. */ function stopDiagnosticStabilityRecorder() { const state = getDiagnosticStabilityState(); state.unsubscribe?.(); state.unsubscribe = null; } /** Returns a sanitized stability snapshot from the process-wide ring buffer. */ function getDiagnosticStabilitySnapshot(options) { const state = getDiagnosticStabilityState(); const { filtered, events } = selectRecords(listRecords(), options); return { generatedAt: (/* @__PURE__ */ new Date()).toISOString(), capacity: state.capacity, count: filtered.length, dropped: state.dropped, firstSeq: filtered[0]?.seq, lastSeq: filtered.at(-1)?.seq, events, summary: summarizeRecords(filtered) }; } /** Applies filtering/limits to an existing snapshot without mutating its source records. */ function selectDiagnosticStabilitySnapshot(snapshot, options) { const { filtered, events } = selectRecords(snapshot.events, options); return { ...snapshot, count: filtered.length, firstSeq: filtered[0]?.seq, lastSeq: filtered.at(-1)?.seq, events, summary: summarizeRecords(filtered) }; } /** Resets recorder state and subscriptions for isolated tests. */ function resetDiagnosticStabilityRecorderForTest() { const state = getDiagnosticStabilityState(); state.unsubscribe?.(); const next = createState(state.capacity); const globalStore = globalThis; globalStore["__openclawDiagnosticStabilityState"] = next; } //#endregion export { selectDiagnosticStabilitySnapshot as a, resetDiagnosticStabilityRecorderForTest as i, getDiagnosticStabilitySnapshot as n, startDiagnosticStabilityRecorder as o, normalizeDiagnosticStabilityQuery as r, stopDiagnosticStabilityRecorder as s, MAX_DIAGNOSTIC_STABILITY_LIMIT as t };