| 1 | import { addBreadcrumb } from "./breadcrumbs"; |
| 2 | import { desktopHost, type RendererDiagnosticPayload } from "./desktopHost"; |
| 3 | |
| 4 | export type TranscriptDiagnosticStage = |
| 5 | | "none" |
| 6 | | "baseline_read" |
| 7 | | "baseline_validate" |
| 8 | | "snapshot_install" |
| 9 | | "delta_read" |
| 10 | | "delta_validate" |
| 11 | | "delta_apply"; |
| 12 | |
| 13 | export type TranscriptDiagnosticReason = |
| 14 | | "transport_rejected" |
| 15 | | "protocol_version" |
| 16 | | "snapshot_missing" |
| 17 | | "history_not_ready" |
| 18 | | "revision_regressed" |
| 19 | | "revision_gap" |
| 20 | | "business_gap" |
| 21 | | "frame_cut_mismatch" |
| 22 | | "sampling_identity_missing" |
| 23 | | "sampling_gap" |
| 24 | | "settlement_not_committed" |
| 25 | | "settlement_identity_mismatch" |
| 26 | | "resync_required" |
| 27 | | "consumer_error" |
| 28 | | "service_stopping" |
| 29 | | "unknown"; |
| 30 | |
| 31 | export type TranscriptDiagnostic = RendererDiagnosticPayload & { |
| 32 | kind: "transcript"; |
| 33 | event: "failure" | "summary" | "recovered" | "stopped"; |
| 34 | stage: TranscriptDiagnosticStage; |
| 35 | reason: TranscriptDiagnosticReason; |
| 36 | transport: "local" | "remote"; |
| 37 | errorType: "classified" | "error" | "string" | "object" | "unknown"; |
| 38 | }; |
| 39 | |
| 40 | const active = new Map<number, string>(); |
| 41 | let nextOwner = 1; |
| 42 | const crashSnapshotHost = globalThis as typeof globalThis & { __reasonixTranscriptDiagnostics?: string }; |
| 43 | |
| 44 | function formatDiagnostic(event: TranscriptDiagnostic): string { |
| 45 | return `${event.event} stage=${event.stage} reason=${event.reason} type=${event.errorType} transport=${event.transport} revision=${event.revision} commit=${event.commit} attempts=${event.attempts} failures=${event.failures} duration_ms=${event.durationMs}`; |
| 46 | } |
| 47 | |
| 48 | function syncCrashSnapshot(): void { |
| 49 | crashSnapshotHost.__reasonixTranscriptDiagnostics = [...active.values()].join("\n"); |
| 50 | } |
| 51 | |
| 52 | export function newTranscriptDiagnosticOwner(): number { |
| 53 | return nextOwner++; |
| 54 | } |
| 55 | |
| 56 | export function publishTranscriptDiagnostic(owner: number, event: TranscriptDiagnostic, visible = true): void { |
| 57 | const message = formatDiagnostic(event); |
| 58 | if (event.event === "failure" || event.event === "summary") active.set(owner, message); |
| 59 | else active.delete(owner); |
| 60 | syncCrashSnapshot(); |
| 61 | if (!visible) return; |
| 62 | addBreadcrumb("transcript.v2", message); |
| 63 | try { |
| 64 | void desktopHost().native.recordRendererDiagnostic(event).catch(() => undefined); |
| 65 | } catch { |
| 66 | // Diagnostics are optional; a broken host must not interrupt following. |
| 67 | } |
| 68 | } |
| 69 | |
| 70 | export function clearTranscriptDiagnostic(owner: number): void { |
| 71 | active.delete(owner); |
| 72 | syncCrashSnapshot(); |
| 73 | } |
| 74 |