( id: string, controller: AbortController, readTimeoutSec: () => Promise<number>, logger: GenerationTimeoutLogger, )
| 129 | * instead of silently disabling the timeout. A value of `0` means "disabled". |
| 130 | */ |
| 131 | export async function armGenerationTimeout( |
| 132 | id: string, |
| 133 | controller: AbortController, |
| 134 | readTimeoutSec: () => Promise<number>, |
| 135 | logger: GenerationTimeoutLogger, |
| 136 | ): Promise<() => void> { |
| 137 | let timeoutSec: number; |
| 138 | try { |
| 139 | timeoutSec = await readTimeoutSec(); |
| 140 | } catch (err) { |
| 141 | const message = err instanceof Error ? err.message : String(err); |
| 142 | logger.warn('generate.timeout.prefs_read_failed', { id, message }); |
| 143 | throw new CodesignError( |
| 144 | `Could not read generation timeout preference: ${message}`, |
| 145 | ERROR_CODES.PREFERENCES_READ_FAIL, |
| 146 | ); |
| 147 | } |
| 148 | if (!Number.isFinite(timeoutSec) || timeoutSec < 0) { |
| 149 | logger.warn('generate.timeout.invalid_value', { id, timeoutSec }); |
| 150 | throw new CodesignError( |
| 151 | `Invalid generation timeout: ${String(timeoutSec)} (must be a non-negative finite number).`, |
| 152 | ERROR_CODES.PREFERENCES_INVALID_TIMEOUT, |
| 153 | ); |
| 154 | } |
| 155 | if (timeoutSec === 0) return () => {}; |
| 156 | |
| 157 | // Node's setTimeout caps delay at int32 (~24.8 days). Larger values overflow |
| 158 | // and fire immediately, which would abort generation instantly. |
| 159 | const TIMEOUT_MAX_MS = 2_147_483_647; |
| 160 | const ms = Math.min(timeoutSec * 1000, TIMEOUT_MAX_MS); |
| 161 | |
| 162 | const handle = setTimeout(() => { |
| 163 | logger.warn('generate.timeout.fired', { id, timeoutSec }); |
| 164 | controller.abort( |
| 165 | new CodesignError( |
| 166 | `Generation aborted after ${timeoutSec}s (Settings → Advanced → Generation timeout).`, |
| 167 | ERROR_CODES.GENERATION_TIMEOUT, |
| 168 | ), |
| 169 | ); |
| 170 | }, ms); |
| 171 | return () => clearTimeout(handle); |
| 172 | } |
| 173 | |
| 174 | /** |
| 175 | * Provider SDKs (Anthropic / OpenAI) catch an aborted fetch and rethrow their |
no test coverage detected