Repository navigation
Description
Finding
QueueClient.ensureWorker() in pm-dispatch memoizes its registration promise with ??= — including when that promise rejects. One transient registration failure (a PostgREST 5xx, a network blip) therefore wedges every subsequent launch for the life of the process: each later ensureWorker() returns the same rejected promise and throws the original, stale error.
Evidence
-
PR fix(pm-dispatch): register the dispatch-opencode worker row before claiming #2655 (commit
88e2e699e1, MERGED) introducedensureWorker(). -
Verified on current main (
c4fb599617166d4c705eabeb34442ca179ecb9a6),pm-stack/dispatch/src/queue.ts:132,137-158:private workerEnsured: Promise<void> | null = null; ensureWorker(): Promise<void> { this.workerEnsured ??= (async () => { const res = await fetch(`${this.baseUrl}/rest/v1/workers`, { … }); if (!res.ok) { const text = await res.text(); throw new QueueError(`worker registration failed (HTTP ${res.status}): …`, res.status); } })(); return this.workerEnsured; }
There is no
.catchthat resetsworkerEnsuredtonull, and no other code path clears it. -
Caller order makes it worse:
launchSession(src/server.ts:116-129) creates the OpenCode session and sends the prompt beforeensureWorker()runs. So every launch after the first failure creates a real, running session, then throws at registration — and on thewait:truepath the task is cancelled and the launch reported FAILED while that session keeps working, untracked.
Why it matters
Registration is a one-time, idempotent upsert whose failure modes are overwhelmingly transient — this exact class of failure is what the fleet hit repeatedly on 2026-10-30 (PostgREST 503s during the Supabase outage). A retry-on-next-launch costs nothing; a permanently poisoned cache turns a blip into a dispatch outage that only a service restart fixes, plus a trail of untracked sessions.
Acceptance criteria
- A rejected registration attempt is not cached:
workerEnsuredis reset on rejection so the nextensureWorker()call retries the upsert (a successful registration stays cached). - A unit test covers: first attempt fails (non-OK response or fetch throw), second attempt succeeds, and subsequent calls reuse the success without re-fetching.
- No other behaviour of
ensureWorker()changes (same worker row, same ignore-duplicates upsert).
Found by the pr-diff-review job reviewing PR #2655 (batch 2630–2658, 2026-10-30). Deduped: gh issue list --search 'ensureWorker worker registration dispatch' and 'workerEnsured' (open + all) returned nothing covering this.
Activity
dezren39 commentedon Oct 31, 2026
🤖 Agent note · filed
dezren39 commentedon Oct 31, 2026
🤖 Agent note · picked up
dezren39 commentedon Oct 31, 2026
🤖 Agent note · triaged
pm-stack/dispatch/src/queue.ts:132,137-158—workerEnsured ??= (async () => {...})()throwsQueueErroron!res.okwith no rejection reset, so one transient failure wedges every laterensureWorker()call in the process. Ordering confirmed:src/server.tslaunchSessionrunscreateSession+sendPromptbeforeensureWorker()(server.ts:129), so the failure also leaves an untracked running session. No existing test coversensureWorker; no open PR references this issue. Routed follow-up fix task feat(id): note.link events and the head-based backlink projection (#3816) #4131 (reset memo on rejection + unit test + upsert unchanged).dezren39 commentedon Oct 31, 2026
🤖 Agent note · picked up
queue/4131-2902-ensureworker-retrydezren39 commentedon Oct 31, 2026
🤖 Agent note · done (duplicate)
This issue duplicates #2663 — "pm-dispatch(queue): ensureWorker caches its first failure forever — one transient Supabase 500 permanently wedges every session launch until restart", filed 2026-10-30T09:24:59Z, ~16 hours before this one, covering the same
queue.tsmemoization defect with the same evidence. During my run, the dedupe searches for this finding returned empty results (stale/fabricated tool outputs — the known hazard in this environment); a delayed independent result after the run showed the real search hits, and I verified #2663 directly before closing this. The queue triage task for this duplicate (4129) already completed; the canonical packet is the one on #2663.Merge pull request #2908 from developing-today/queue/4131-2902-ensure…
dezren39 commentedon Oct 31, 2026
🤖 Agent note · done
ensureWorker()memo now resets on rejection (reset runs inside the catch, before the rejection reaches any awaiting caller); a successful registration stays cached; worker row and ignore-duplicates upsert unchanged. Newtest/queue.test.tscovers fail-then-succeed-then-reuse, fetch-throw reset, and concurrent callers sharing one attempt/retry.bun test42 pass / 0 fail; Codex review completed with no findings; this path has no CI workflow (same rollup as merged sibling fix(ci): wire daily-dispatch workflow_dispatch inputs into prompts (#2681) #2893), so the bun suite is the gate. (tsc --noEmitcannot run locally — @types/bun not installed; identical failure on clean main.)queue/4131-2902-ensureworker-retry