(prompt: string)
| 373 | if (!Number.isFinite(timeout) || timeout <= 0) { |
| 374 | throw new Error(`--timeout must be a positive number of seconds (got ${values.timeout})`); |
| 375 | } |
| 376 | |
| 377 | // Default to the log's current end, so a watcher started now waits for what happens NEXT |
| 378 | // rather than firing immediately on the conversation it was started in the middle of. |
| 379 | // |
| 380 | // This first call needs the same unreachable handling as the poll loop. Left bare it threw |
| 381 | // to the top-level catch and exited 1 — a *usage* code — so a harness starting a watcher |
| 382 | // against a dead engine could not tell "the engine is gone" from "you typed it wrong". That |
| 383 | // is the distinction the exit codes exist for, defeated at the one moment it matters most. |
| 384 | // Narrowing does not survive into a closure, so pin it once the guard above has run. |
| 385 | const kildId = id; |
| 386 | // Wake latency, and the gap between tolerated retries. Tunable because a harness that |
| 387 | // wants a snappier wake should not have to fork the CLI to get one. |
| 388 | const interval = values.interval === undefined ? WATCH_POLL_MS : Number(values.interval) * 1000; |
| 389 | if (!Number.isFinite(interval) || interval <= 0) { |
| 390 | throw new Error(`--interval must be a positive number of seconds (got ${values.interval})`); |
| 391 | } |
| 392 | const started = Date.now(); |
| 393 | const deadline = started + timeout * 1000; |
| 394 | function sleep(): Promise<unknown> { |
| 395 | return new Promise((resolve) => setTimeout(resolve, interval)); |
| 396 | } |
| 397 | let failures = 0; |
| 398 | /** Whether the engine has answered even once. "Quiet" is a claim ABOUT the engine — that it |
| 399 | * responded and had nothing — so it cannot be reported by a watcher that never heard from |
| 400 | * it. Without this, a window that closed after nothing but failures exited 2, asserting the |
| 401 | * opposite of what was observed. */ |
| 402 | let answered = false; |
| 403 | |
| 404 | /** |
| 405 | * Ask once. Returns the batch, or null when the failure was tolerated and the caller should |
| 406 | * wait and try again; exits `unreachable` once tolerance runs out. |
| 407 | * |
| 408 | * ONE place that knows how much transient failure is acceptable, used by both the bootstrap |
| 409 | * fetch and the poll loop. They had separate copies of this decision and immediately drifted: |
| 410 | * the loop retried with no delay at all, so three "tolerated" failures burned in about eight |
| 411 | * milliseconds and a merely-restarting engine was declared dead — the exact opposite of what |
| 412 | * the tolerance exists for — while the bootstrap fetch tolerated nothing whatsoever. |
| 413 | */ |
| 414 | async function poll( |
| 415 | since: number | undefined, |
| 416 | ): Promise<Awaited<ReturnType<typeof kildMessages>> | null> { |
| 417 | try { |
| 418 | // The bound and the deadline are one question, asked once: never wait past the window. |
| 419 | const batch = await kildMessages( |
| 420 | kildId, |
| 421 | since, |
| 422 | watchRequestTimeout(interval, deadline - Date.now()), |
| 423 | ); |
| 424 | failures = 0; |
| 425 | answered = true; |
| 426 | return batch; |
| 427 | } catch (err) { |
| 428 | if (++failures < WATCH_TOLERATED_FAILURES) return null; |
| 429 | console.error(`kild: engine unreachable after ${failures} attempts: ${errText(err)}`); |
| 430 | process.exit(WATCH_EXIT.unreachable); |
no test coverage detected