What happens
On the self-host Node runtime (src/server.ts), the queue's job consumer closure captures env:
let env: Env;
const consume = async (message: JobMessage): Promise<void> => {
try {
await processJob(env, message);
...
env is not actually assigned until much later in main(), after the queue backend is built, migrations run, and AI providers are configured (~250 lines / several awaited steps later).
Both queue backends (pg-queue.ts's init(), sqlite-queue.ts's construction) deliberately self-heal any foreground-priority job (github-webhook, agent-regate-pr, agent-regate-sweep, recapture-preview, ...) that was left over-deferred across a restart, by calling releaseStaleForegroundDeferrals() once at boot in addition to on its normal periodic sweep. If that boot-time call finds an eligible job, it releases it and calls kickAll(), which fires an un-awaited pump() → processOne() → consume(message) chain.
Since this all happens before env is assigned in main(), processJob(env, message) can be invoked with env still undefined — the first property read off it (e.g. env.DB) throws TypeError: Cannot read properties of undefined (reading '...'). This gets caught by the queue's normal error handling and logged as an unremarkable job_error audit event, indistinguishable from a normal transient failure. It self-heals on the very next retry (by which point env is set), so nothing looks broken after the fact — but the root cause (a boot-ordering race) is completely masked.
Trigger condition
Only manifests when, at the moment of a container restart/redeploy, there is at least one foreground-priority job sitting pending with a future run_after that's also eligible for release (age-stale past FOREGROUND_LIVENESS_MAX_DEFER_MS, default 10 minutes, OR its rate-limit condition just cleared) — e.g. a contributor-PR-review job that got deferred by GitHub rate-limiting shortly before the restart. FOREGROUND_LIVENESS_ENABLED defaults to true, so this is live by default on any self-host instance.
Fix
src/server.ts's consume closure now awaits an envReady promise (resolved right after env is fully assigned) before calling processJob, so a boot-time release waits rather than dereferencing env early. Confined entirely to server.ts (shared by both queue backends), so neither pg-queue.ts nor sqlite-queue.ts needed changes.
server.ts is in codecov.yml's ignore list (self-host process entry, validated by the Docker boot smoke test, not unit-coverable without booting a real server) — full local gate (typecheck, unsharded coverage, test:ci, npm audit) is green.
What happens
On the self-host Node runtime (
src/server.ts), the queue's job consumer closure capturesenv:envis not actually assigned until much later inmain(), after the queue backend is built, migrations run, and AI providers are configured (~250 lines / several awaited steps later).Both queue backends (
pg-queue.ts'sinit(),sqlite-queue.ts's construction) deliberately self-heal any foreground-priority job (github-webhook,agent-regate-pr,agent-regate-sweep,recapture-preview, ...) that was left over-deferred across a restart, by callingreleaseStaleForegroundDeferrals()once at boot in addition to on its normal periodic sweep. If that boot-time call finds an eligible job, it releases it and callskickAll(), which fires an un-awaitedpump()→processOne()→consume(message)chain.Since this all happens before
envis assigned inmain(),processJob(env, message)can be invoked withenvstillundefined— the first property read off it (e.g.env.DB) throwsTypeError: Cannot read properties of undefined (reading '...'). This gets caught by the queue's normal error handling and logged as an unremarkablejob_erroraudit event, indistinguishable from a normal transient failure. It self-heals on the very next retry (by which pointenvis set), so nothing looks broken after the fact — but the root cause (a boot-ordering race) is completely masked.Trigger condition
Only manifests when, at the moment of a container restart/redeploy, there is at least one foreground-priority job sitting
pendingwith a futurerun_afterthat's also eligible for release (age-stale pastFOREGROUND_LIVENESS_MAX_DEFER_MS, default 10 minutes, OR its rate-limit condition just cleared) — e.g. a contributor-PR-review job that got deferred by GitHub rate-limiting shortly before the restart.FOREGROUND_LIVENESS_ENABLEDdefaults to true, so this is live by default on any self-host instance.Fix
src/server.ts'sconsumeclosure now awaits anenvReadypromise (resolved right afterenvis fully assigned) before callingprocessJob, so a boot-time release waits rather than dereferencingenvearly. Confined entirely toserver.ts(shared by both queue backends), so neitherpg-queue.tsnorsqlite-queue.tsneeded changes.server.tsis incodecov.yml's ignore list (self-host process entry, validated by the Docker boot smoke test, not unit-coverable without booting a real server) — full local gate (typecheck, unsharded coverage,test:ci,npm audit) is green.