180 lines
4.2 KiB
TypeScript
180 lines
4.2 KiB
TypeScript
import type { ClawdbotConfig } from "../config/config.js";
|
|
|
|
export type DiagnosticSessionState = "idle" | "processing" | "waiting";
|
|
|
|
type DiagnosticBaseEvent = {
|
|
ts: number;
|
|
seq: number;
|
|
};
|
|
|
|
export type DiagnosticUsageEvent = DiagnosticBaseEvent & {
|
|
type: "model.usage";
|
|
sessionKey?: string;
|
|
sessionId?: string;
|
|
channel?: string;
|
|
provider?: string;
|
|
model?: string;
|
|
usage: {
|
|
input?: number;
|
|
output?: number;
|
|
cacheRead?: number;
|
|
cacheWrite?: number;
|
|
promptTokens?: number;
|
|
total?: number;
|
|
};
|
|
context?: {
|
|
limit?: number;
|
|
used?: number;
|
|
};
|
|
costUsd?: number;
|
|
durationMs?: number;
|
|
};
|
|
|
|
export type DiagnosticWebhookReceivedEvent = DiagnosticBaseEvent & {
|
|
type: "webhook.received";
|
|
channel: string;
|
|
updateType?: string;
|
|
chatId?: number | string;
|
|
};
|
|
|
|
export type DiagnosticWebhookProcessedEvent = DiagnosticBaseEvent & {
|
|
type: "webhook.processed";
|
|
channel: string;
|
|
updateType?: string;
|
|
chatId?: number | string;
|
|
durationMs?: number;
|
|
};
|
|
|
|
export type DiagnosticWebhookErrorEvent = DiagnosticBaseEvent & {
|
|
type: "webhook.error";
|
|
channel: string;
|
|
updateType?: string;
|
|
chatId?: number | string;
|
|
error: string;
|
|
};
|
|
|
|
export type DiagnosticMessageQueuedEvent = DiagnosticBaseEvent & {
|
|
type: "message.queued";
|
|
sessionKey?: string;
|
|
sessionId?: string;
|
|
channel?: string;
|
|
source: string;
|
|
queueDepth?: number;
|
|
};
|
|
|
|
export type DiagnosticMessageProcessedEvent = DiagnosticBaseEvent & {
|
|
type: "message.processed";
|
|
channel: string;
|
|
messageId?: number | string;
|
|
chatId?: number | string;
|
|
sessionKey?: string;
|
|
sessionId?: string;
|
|
durationMs?: number;
|
|
outcome: "completed" | "skipped" | "error";
|
|
reason?: string;
|
|
error?: string;
|
|
};
|
|
|
|
export type DiagnosticSessionStateEvent = DiagnosticBaseEvent & {
|
|
type: "session.state";
|
|
sessionKey?: string;
|
|
sessionId?: string;
|
|
prevState?: DiagnosticSessionState;
|
|
state: DiagnosticSessionState;
|
|
reason?: string;
|
|
queueDepth?: number;
|
|
};
|
|
|
|
export type DiagnosticSessionStuckEvent = DiagnosticBaseEvent & {
|
|
type: "session.stuck";
|
|
sessionKey?: string;
|
|
sessionId?: string;
|
|
state: DiagnosticSessionState;
|
|
ageMs: number;
|
|
queueDepth?: number;
|
|
};
|
|
|
|
export type DiagnosticLaneEnqueueEvent = DiagnosticBaseEvent & {
|
|
type: "queue.lane.enqueue";
|
|
lane: string;
|
|
queueSize: number;
|
|
};
|
|
|
|
export type DiagnosticLaneDequeueEvent = DiagnosticBaseEvent & {
|
|
type: "queue.lane.dequeue";
|
|
lane: string;
|
|
queueSize: number;
|
|
waitMs: number;
|
|
};
|
|
|
|
export type DiagnosticRunAttemptEvent = DiagnosticBaseEvent & {
|
|
type: "run.attempt";
|
|
sessionKey?: string;
|
|
sessionId?: string;
|
|
runId: string;
|
|
attempt: number;
|
|
};
|
|
|
|
export type DiagnosticHeartbeatEvent = DiagnosticBaseEvent & {
|
|
type: "diagnostic.heartbeat";
|
|
webhooks: {
|
|
received: number;
|
|
processed: number;
|
|
errors: number;
|
|
};
|
|
active: number;
|
|
waiting: number;
|
|
queued: number;
|
|
};
|
|
|
|
export type DiagnosticEventPayload =
|
|
| DiagnosticUsageEvent
|
|
| DiagnosticWebhookReceivedEvent
|
|
| DiagnosticWebhookProcessedEvent
|
|
| DiagnosticWebhookErrorEvent
|
|
| DiagnosticMessageQueuedEvent
|
|
| DiagnosticMessageProcessedEvent
|
|
| DiagnosticSessionStateEvent
|
|
| DiagnosticSessionStuckEvent
|
|
| DiagnosticLaneEnqueueEvent
|
|
| DiagnosticLaneDequeueEvent
|
|
| DiagnosticRunAttemptEvent
|
|
| DiagnosticHeartbeatEvent;
|
|
|
|
export type DiagnosticEventInput = DiagnosticEventPayload extends infer Event
|
|
? Event extends DiagnosticEventPayload
|
|
? Omit<Event, "seq" | "ts">
|
|
: never
|
|
: never;
|
|
let seq = 0;
|
|
const listeners = new Set<(evt: DiagnosticEventPayload) => void>();
|
|
|
|
export function isDiagnosticsEnabled(config?: ClawdbotConfig): boolean {
|
|
return config?.diagnostics?.enabled === true;
|
|
}
|
|
|
|
export function emitDiagnosticEvent(event: DiagnosticEventInput) {
|
|
const enriched = {
|
|
...event,
|
|
seq: (seq += 1),
|
|
ts: Date.now(),
|
|
} satisfies DiagnosticEventPayload;
|
|
for (const listener of listeners) {
|
|
try {
|
|
listener(enriched);
|
|
} catch {
|
|
// Ignore listener failures.
|
|
}
|
|
}
|
|
}
|
|
|
|
export function onDiagnosticEvent(listener: (evt: DiagnosticEventPayload) => void): () => void {
|
|
listeners.add(listener);
|
|
return () => listeners.delete(listener);
|
|
}
|
|
|
|
export function resetDiagnosticEventsForTest(): void {
|
|
seq = 0;
|
|
listeners.clear();
|
|
}
|