import * as plugins from './plugins.js'; import type { IControllerFailureDetail } from '../ts_interfaces/index.js'; /** * In-memory journal of the failures whose wire response is deliberately generic. * * The controller answers an unclassified failure with one fixed sentence so no internal detail * reaches a client that is not the owner. That would leave the cause nowhere, so every such * failure is recorded here under a short opaque reference, logged with the same reference, and * read back by the authenticated owner UI. The journal is process-local, bounded, and never * persisted: it is diagnostics for the running process, not application data. */ /** How many failures are retained; older records are dropped. */ export const controllerFailureJournalLimit = 100; const maximumOperationLength = 128; const maximumMessageLength = 2_000; const maximumStackLength = 8_000; const truncate = (valueArg: string, maximumArg: number): string => ( valueArg.length <= maximumArg ? valueArg : `${valueArg.slice(0, maximumArg)}…` ); export class ControllerFailureJournal { private readonly records: IControllerFailureDetail[] = []; /** * Records one failure and returns the reference the generic response carries. The reference is * random rather than sequential so it reveals nothing about failure volume or ordering. */ public record(inputArg: { operation: string; cause: unknown }): string { const reference = plugins.crypto.randomBytes(6).toString('base64url'); const cause = inputArg.cause; const record: IControllerFailureDetail = { reference, at: Date.now(), operation: truncate(inputArg.operation, maximumOperationLength), name: cause instanceof Error ? cause.name : typeof cause, message: truncate( cause instanceof Error ? cause.message : String(cause), maximumMessageLength, ), stack: truncate( cause instanceof Error && typeof cause.stack === 'string' ? cause.stack : '', maximumStackLength, ), }; this.records.push(record); while (this.records.length > controllerFailureJournalLimit) this.records.shift(); return reference; } /** The retained failures, oldest first. */ public list(): IControllerFailureDetail[] { return this.records.map((record) => ({ ...record })); } public find(referenceArg: string): IControllerFailureDetail | undefined { const record = this.records.find((candidate) => candidate.reference === referenceArg); return record === undefined ? undefined : { ...record }; } public clear(): void { this.records.length = 0; } } /** * One journal per process: the generic failure mapper and the private MCP host are module-level * funnels, and a failure reference has to resolve regardless of which of them recorded it. */ export const controllerFailureJournal = new ControllerFailureJournal();