diff --git a/src/core/facts/backstop.ts b/src/core/facts/backstop.ts index 59463d13c..0f8b6b4f8 100644 --- a/src/core/facts/backstop.ts +++ b/src/core/facts/backstop.ts @@ -191,7 +191,9 @@ export async function runFactsBackstop( // identical writes (idempotent ON CONFLICT returns existing row). idempotency_key: `facts-absorb:${ctx.sourceId}:${parsedPage.slug}:${contentHash}`, max_attempts: 3, - timeout_ms: 180_000, + // No explicit timeout_ms: let MinionQueue.add stamp the + // handler-timeouts.ts default (#3207 — a hardcoded 180s here + // overrode the map and wall-clock-killed slow-gateway absorbs). }, ); return { mode: 'queue', enqueued: true, queueDepth: 0 }; diff --git a/src/core/minions/handler-timeouts.ts b/src/core/minions/handler-timeouts.ts index 8269add1e..7c9b650e2 100644 --- a/src/core/minions/handler-timeouts.ts +++ b/src/core/minions/handler-timeouts.ts @@ -43,6 +43,9 @@ export const HANDLER_DEFAULT_TIMEOUT_MS: Readonly> = { // few writes. Generous 10-min budget (vs the tight null-default) covers a // slow gateway without the 30-min loop budget. chronicle_extract: TEN_MIN_MS, + // #3207 — facts absorb: same one-page-one-LLM-call shape as + // chronicle_extract; a slow gateway must not wall-clock-kill it. + 'facts-absorb': TEN_MIN_MS, // Per-page contextual reindex jobs process chunks sequentially with one // rate-leased LLM synopsis call per chunk; large transcript pages need more // than the standard 30-min long-job budget. diff --git a/test/facts-backstop.test.ts b/test/facts-backstop.test.ts index 69639a497..df3b537fb 100644 --- a/test/facts-backstop.test.ts +++ b/test/facts-backstop.test.ts @@ -301,3 +301,36 @@ describe('runFactsBackstop — stub guard routing (v0.34.5)', () => { } }); }); + +describe('#3207 — durable facts-absorb submit uses the handler-timeout map', () => { + test('defaultTimeoutMsFor stamps facts-absorb with the 10-min budget (matches chronicle_extract)', async () => { + const { defaultTimeoutMsFor } = await import('../src/core/minions/handler-timeouts.ts'); + expect(defaultTimeoutMsFor('facts-absorb')).toBe(10 * 60 * 1000); + expect(defaultTimeoutMsFor('facts-absorb')).toBe(defaultTimeoutMsFor('chronicle_extract')); + }); + + test('short-lived CLI durable submit inherits the map default (no hardcoded 180s override)', async () => { + const { markShortLivedCliProcess, __resetShortLivedCliForTests } = + await import('../src/core/facts/cli-process-mode.ts'); + markShortLivedCliProcess(); + try { + const page = meetingPage(); + const r = await runFactsBackstop(page, makeCtx()); + expect(r.mode).toBe('queue'); + if (r.mode === 'queue') expect(r.enqueued).toBe(true); + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const rows = await (engine as any).db.query( + `SELECT timeout_ms FROM minion_jobs WHERE name = 'facts-absorb' AND data->>'slug' = $1`, + [page.slug], + ); + expect(rows.rows.length).toBe(1); + // Submit-time stamp comes from HANDLER_DEFAULT_TIMEOUT_MS, not the old + // hardcoded 180_000 that overrode any map-only fix (the map's contract: + // "An explicit opts.timeout_ms always wins"). + expect(Number(rows.rows[0].timeout_ms)).toBe(10 * 60 * 1000); + } finally { + __resetShortLivedCliForTests(); + } + }); +});