import { mkdir, open, readFile, rename, unlink, type FileHandle } from 'node:fs/promises' import { join } from 'node:path' const DEFAULT_MAXIMUM_FILE_BYTES = 256 * 1024 const DEFAULT_MAXIMUM_FILES = 4 const DEFAULT_MAXIMUM_RECORDS = 1_000 export const MAXIMUM_PENDING_DESKTOP_DIAGNOSTIC_WRITES = 32 const DIAGNOSTIC_FILE_NAME = 'desktop-diagnostics.ndjson' const rendererExitReasons = new Set([ 'clean-exit', 'abnormal-exit', 'killed', 'crashed', 'oom', 'launch-failed', 'integrity-failure' ]) const allowedStages = new Set([ 'renderer', 'startup', 'create', 'status', 'connection-test', 'native-snapshot', 'compact', 'run', 'connect', 'disconnect' ]) const allowedCodes = new Set([ 'desktop.renderer.gone', 'desktop.renderer.load-failed', 'desktop.startup.failed', 'runtime.operation.failed', 'runtime.run.failed', 'runtime.mcp.failed', 'remote.connection.network', 'remote.connection.host-invalidated', 'remote.connection.host-identity', 'remote.connection.authentication', 'remote.connection.installation', 'remote.connection.protocol', 'remote.connection.daemon-status', 'remote.connection.platform', 'remote.connection.capability', 'remote.connection.shutdown', 'remote.connection.lost' ]) const safeErrorTypes = new Set([ 'AbortError', 'AggregateError', 'Error', 'EvalError', 'RangeError', 'ReferenceError', 'RemoteAgentConnectionError', 'StartupPrerequisiteError', 'SyntaxError', 'TimeoutError', 'TypeError', 'URIError' ]) export type DesktopDiagnosticComponent = | 'desktop' | 'runtime' | 'remote-agent' export type DesktopMcpDiagnosticMetadata = Readonly<{ phase: 'request' | 'authentication' | 'session' | 'initialize' | 'tool-discovery' | 'connect' | 'disconnect' category: 'http-rejection' | 'handler-failure' | 'transport-failure' | 'initialization-failure' | 'cleanup-failure' | 'cancelled' elapsedMs: number status?: number correlationId?: string attempt?: 1 | 2 kind?: 'builtin' | 'custom' }> function normalizeMcpMetadata(value: unknown): DesktopMcpDiagnosticMetadata | undefined { if (!value || typeof value !== 'object') return undefined const candidate = value as DesktopMcpDiagnosticMetadata if (!['request', 'authentication', 'session', 'initialize', 'tool-discovery', 'connect', 'disconnect'].includes(candidate.phase) || !['http-rejection', 'handler-failure', 'transport-failure', 'initialization-failure', 'cleanup-failure', 'cancelled'].includes(candidate.category) || !Number.isSafeInteger(candidate.elapsedMs) || candidate.elapsedMs < 0) return undefined return Object.freeze({ phase: candidate.phase, category: candidate.category, elapsedMs: candidate.elapsedMs, ...(candidate.attempt === 1 || candidate.attempt === 2 ? { attempt: candidate.attempt } : {}), ...(candidate.kind === 'builtin' || candidate.kind === 'custom' ? { kind: candidate.kind } : {}), ...(Number.isInteger(candidate.status) && candidate.status! >= 100 && candidate.status! <= 599 ? { status: candidate.status } : {}), ...(typeof candidate.correlationId === 'string' && /^sha256:[a-f0-9]{64}$/.test(candidate.correlationId) ? { correlationId: candidate.correlationId } : {}) }) } export type DesktopDiagnosticFailure = Readonly<{ component: DesktopDiagnosticComponent stage: string code: string error: unknown reason?: string exitCode?: number mcp?: DesktopMcpDiagnosticMetadata }> export type DesktopDiagnosticFailureObserver = ( failure: DesktopDiagnosticFailure ) => void export type DesktopDiagnosticRecord = Readonly<{ timestamp: string component: DesktopDiagnosticComponent stage: string code: string errorType: string message: string reason?: string exitCode?: number mcp?: DesktopMcpDiagnosticMetadata }> export type DesktopDiagnosticsOptions = Readonly<{ maximumFileBytes?: number maximumFiles?: number maximumRecords?: number now?: () => Date }> function safeErrorType(error: unknown): string { if (!(error instanceof Error)) { return 'NonError' } let name: unknown try { name = error.name } catch { return 'Error' } return typeof name === 'string' && safeErrorTypes.has(name) ? name : 'Error' } function fixedMessage( component: DesktopDiagnosticComponent, stage: string ): string { if (component === 'desktop') { if (stage === 'renderer') return 'Desktop renderer failed' return 'Desktop startup failed' } if (component === 'remote-agent') { return stage === 'disconnect' ? 'Remote connection was lost' : 'Remote connection failed' } return stage === 'run' ? 'Runtime request failed' : 'Runtime operation failed' } function safeComponent(component: unknown): DesktopDiagnosticComponent { switch (component) { case 'runtime': case 'remote-agent': return component default: return 'desktop' } } function safeStage(stage: unknown): string { return typeof stage === 'string' && allowedStages.has(stage) ? stage : 'unknown' } function safeCode(code: unknown): string { return typeof code === 'string' && allowedCodes.has(code) ? code : 'diagnostic.failure' } function safeStoredErrorType(errorType: unknown): string { return typeof errorType === 'string' && (errorType === 'NonError' || safeErrorTypes.has(errorType)) ? errorType : 'Error' } export function normalizeDesktopDiagnosticRecord( value: unknown ): DesktopDiagnosticRecord | undefined { if (typeof value !== 'object' || value === null) { return undefined } const candidate = value as Partial if (typeof candidate.timestamp !== 'string') { return undefined } const timestamp = new Date(candidate.timestamp) if (Number.isNaN(timestamp.valueOf())) { return undefined } const component = safeComponent(candidate.component) const stage = safeStage(candidate.stage) const code = safeCode(candidate.code) const mcp = component === 'runtime' && code === 'runtime.mcp.failed' ? normalizeMcpMetadata(candidate.mcp) : undefined const rendererGone = component === 'desktop' && stage === 'renderer' && code === 'desktop.renderer.gone' return Object.freeze({ timestamp: timestamp.toISOString(), component, stage, code, errorType: safeStoredErrorType(candidate.errorType), message: fixedMessage(component, stage), ...(mcp ? { mcp } : {}), ...(rendererGone && typeof candidate.reason === 'string' && rendererExitReasons.has(candidate.reason) ? { reason: candidate.reason } : {}), ...(rendererGone && Number.isSafeInteger(candidate.exitCode) ? { exitCode: candidate.exitCode } : {}) }) } function ignoreMissing(error: unknown): void { if ( !(error instanceof Error) || !('code' in error) || error.code !== 'ENOENT' ) { throw error } } export class DesktopDiagnostics { readonly #directoryPath: string readonly #maximumFileBytes: number readonly #maximumFiles: number readonly #maximumRecords: number readonly #now: () => Date #handle: FileHandle | undefined #currentBytes = 0 #initialized = false #accepting = true #pendingWrites = 0 #queue: Promise = Promise.resolve() constructor( directoryPath: string, options: DesktopDiagnosticsOptions = {} ) { this.#directoryPath = directoryPath this.#maximumFileBytes = options.maximumFileBytes ?? DEFAULT_MAXIMUM_FILE_BYTES this.#maximumFiles = options.maximumFiles ?? DEFAULT_MAXIMUM_FILES this.#maximumRecords = options.maximumRecords ?? DEFAULT_MAXIMUM_RECORDS this.#now = options.now ?? (() => new Date()) if ( !Number.isSafeInteger(this.#maximumFileBytes) || this.#maximumFileBytes < 256 || !Number.isSafeInteger(this.#maximumFiles) || this.#maximumFiles < 1 || this.#maximumFiles > 16 || !Number.isSafeInteger(this.#maximumRecords) || this.#maximumRecords < 1 || this.#maximumRecords > 10_000 ) { throw new RangeError('Invalid desktop diagnostic bounds') } } initialize(): Promise { return this.#enqueue(async () => this.#ensureInitialized()) } get pendingWriteCount(): number { return this.#pendingWrites } recordFailure( failure: DesktopDiagnosticFailure ): Promise { if (!this.#accepting) { return Promise.resolve() } let record: DesktopDiagnosticRecord try { const normalized = normalizeDesktopDiagnosticRecord({ timestamp: this.#now().toISOString(), component: failure.component, stage: failure.stage, code: failure.code, errorType: safeErrorType(failure.error), reason: failure.reason, exitCode: failure.exitCode, mcp: failure.mcp }) if (normalized === undefined) { return Promise.reject( new Error('Desktop diagnostic record is invalid') ) } record = normalized } catch (error) { return Promise.reject(error) } if ( this.#pendingWrites >= MAXIMUM_PENDING_DESKTOP_DIAGNOSTIC_WRITES ) { return Promise.resolve() } this.#pendingWrites += 1 return this.#enqueue(async () => { try { await this.#ensureInitialized() const encoded = Buffer.from( `${JSON.stringify(record)}\n`, 'utf8' ) if (encoded.byteLength > this.#maximumFileBytes) { throw new RangeError( 'Desktop diagnostic record exceeds file bound' ) } if ( this.#currentBytes > 0 && this.#currentBytes + encoded.byteLength > this.#maximumFileBytes ) { await this.#rotate() } await this.#handle!.write(encoded) this.#currentBytes += encoded.byteLength } finally { this.#pendingWrites -= 1 } }) } readRecent( limit = this.#maximumRecords ): Promise { const requestedLimit = Number.isFinite(limit) ? Math.trunc(limit) : this.#maximumRecords const boundedLimit = Math.min( Math.max(0, requestedLimit), this.#maximumRecords ) if (boundedLimit === 0) { return Promise.resolve([]) } return this.#enqueue(async () => { await this.#ensureInitialized() await this.#handle!.sync() const records: DesktopDiagnosticRecord[] = [] for (let index = this.#maximumFiles - 1; index >= 0; index -= 1) { const contents = await this.#readFile(index) if (contents === undefined) { continue } for (const line of contents.split('\n')) { if (line.length === 0) { continue } try { const parsed: unknown = JSON.parse(line) const record = normalizeDesktopDiagnosticRecord(parsed) if (record !== undefined) { records.push(record) } } catch { // Ignore a final partial or externally damaged record. } } } return Object.freeze(records.slice(-boundedLimit)) }) } async exportRecent( limit = this.#maximumRecords ): Promise { const records = await this.readRecent(limit) return Buffer.from( records.map((record) => JSON.stringify(record)).join('\n') + (records.length > 0 ? '\n' : ''), 'utf8' ) } flush(): Promise { return this.#enqueue(async () => { if (this.#handle) { await this.#handle.sync() } }) } dispose(): Promise { if (!this.#accepting) { return this.#queue } this.#accepting = false return this.#enqueue(async () => { const handle = this.#handle this.#handle = undefined this.#initialized = false if (handle) { await handle.sync() await handle.close() } }) } #enqueue(operation: () => Promise): Promise { const result = this.#queue.then(operation) this.#queue = result.then( () => undefined, () => undefined ) return result } async #ensureInitialized(): Promise { if (this.#initialized) { return } await mkdir(this.#directoryPath, { recursive: true }) const currentPath = this.#filePath(0) this.#handle = await open(currentPath, 'a+') this.#currentBytes = (await this.#handle.stat()).size this.#initialized = true if (this.#currentBytes > this.#maximumFileBytes) { await this.#handle.close() this.#handle = undefined await unlink(currentPath) this.#handle = await open(currentPath, 'a+') this.#currentBytes = 0 } } async #rotate(): Promise { const handle = this.#handle this.#handle = undefined if (handle) { await handle.close() } await unlink(this.#filePath(this.#maximumFiles - 1)).catch( ignoreMissing ) for ( let index = this.#maximumFiles - 2; index >= 0; index -= 1 ) { await rename( this.#filePath(index), this.#filePath(index + 1) ).catch(ignoreMissing) } this.#handle = await open(this.#filePath(0), 'a+') this.#currentBytes = 0 } async #readFile(index: number): Promise { try { return await readFile(this.#filePath(index), 'utf8') } catch (error) { if ( error instanceof Error && 'code' in error && error.code === 'ENOENT' ) { return undefined } throw error } } #filePath(index: number): string { return join( this.#directoryPath, index === 0 ? DIAGNOSTIC_FILE_NAME : `${DIAGNOSTIC_FILE_NAME}.${index}` ) } }