({ db, getMainWindow }: RegisterGenerateIpcDeps)
| 312 | * aborts every in-flight generation — call from the app `before-quit` hook. |
| 313 | */ |
| 314 | export function registerGenerateIpc({ db, getMainWindow }: RegisterGenerateIpcDeps): () => void { |
| 315 | const logIpc = getLogger('main:ipc'); |
| 316 | |
| 317 | // Cache of the last NormalizedProviderError seen per run, so recordFinalError |
| 318 | // can attach it to the final (non-transient) row. Without this, the row the |
| 319 | // user actually reports lacks upstream_request_id / status — those fields |
| 320 | // lived only on the hidden transient sibling row emitted by retry.ts. |
| 321 | // Implementation + LRU eviction lives in ../provider-context.ts. |
| 322 | const providerContext = createProviderContextStore(50); |
| 323 | |
| 324 | const recordFinalError = (scope: string, runId: string, err: unknown): void => { |
| 325 | if (db === null) return; |
| 326 | const code = err instanceof CodesignError ? (err.code as string) : 'PROVIDER_UPSTREAM_ERROR'; |
| 327 | const stack = err instanceof Error ? err.stack : undefined; |
| 328 | const message = err instanceof Error ? err.message : String(err); |
| 329 | const context = providerContext.consume(runId); |
| 330 | recordDiagnosticEvent(db, { |
| 331 | level: 'error', |
| 332 | code, |
| 333 | scope, |
| 334 | runId, |
| 335 | fingerprint: computeFingerprint({ errorCode: code, stack, message }), |
| 336 | message, |
| 337 | stack, |
| 338 | transient: false, |
| 339 | ...(context !== undefined ? { context } : {}), |
| 340 | }); |
| 341 | }; |
| 342 | |
| 343 | /** Adapter so `core` can log step events through the same scoped electron-log |
| 344 | * sink the IPC handler uses. Keeps a single timeline per generation in the |
| 345 | * log file without forcing `core` to depend on electron-log. |
| 346 | * |
| 347 | * Only `provider.error` (retry in flight, transient=true) is persisted from |
| 348 | * this adapter; the `provider.error.final` event is NOT recorded because the |
| 349 | * outer handler's catch block calls `recordFinalError` — recording both |
| 350 | * would double-count the same failure with two distinct fingerprints. */ |
| 351 | const coreLoggerFor = (id: string): CoreLogger => ({ |
| 352 | info: (event, data) => logIpc.info(event, { generationId: id, ...(data ?? {}) }), |
| 353 | warn: (event, data) => { |
| 354 | logIpc.warn(event, { generationId: id, ...(data ?? {}) }); |
| 355 | if (event === 'provider.error' && db !== null) { |
| 356 | const code = 'PROVIDER_UPSTREAM_ERROR'; |
| 357 | const upstream = |
| 358 | data !== undefined && typeof data['upstream_message'] === 'string' |
| 359 | ? (data['upstream_message'] as string) |
| 360 | : event; |
| 361 | // Fingerprint basis: errorCode + synthetic frame containing the two |
| 362 | // fields that truly differentiate provider errors — upstream_status |
| 363 | // and upstream_code. JSON-stringifying `data` and passing it as |
| 364 | // `stack` would produce an identical 8-hex for every provider error |
| 365 | // because `extractTopFrames` requires lines starting with "at ". |
| 366 | const status = |
| 367 | typeof data?.['upstream_status'] === 'number' ? data['upstream_status'] : '?'; |
| 368 | const upstreamCode = |
| 369 | typeof data?.['upstream_code'] === 'string' ? data['upstream_code'] : 'unknown'; |
| 370 | const syntheticFrame = ` at provider (${status}:${upstreamCode})`; |
| 371 | if (data !== undefined) providerContext.remember(id, data); |
no test coverage detected