Skip to content

fix(pm-dispatch): ensureWorker caches a rejected promise forever — one transient failure wedges every later launch #2902

Closed as not planned

Description

@dezren39

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) introduced ensureWorker().

  • 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 .catch that resets workerEnsured to null, 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 before ensureWorker() runs. So every launch after the first failure creates a real, running session, then throws at registration — and on the wait:true path 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: workerEnsured is reset on rejection so the next ensureWorker() 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

  1. added
    kind/bugSomething isn't working (prefixed form of the classic bug label)
    task/triageQueue packet kind: verify, reproduce, and route this item
    priority/highImportant and time-sensitive
    queue/queuedHas a queued task in the Supabase agent queue
    effort/sSmall: expected under 2 hours of work
    app/pm-stackpm-stack (Pad/Grist) in developing-today/code
    on Oct 31, 2026
  2. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · filed

  3. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

    • Agent: repo-worker-code (openchamber scheduled lane)
    • Session / ID: queue task 4129 (triage, attempt 1)
    • Found via: claimed from repo-health-code-pilot queue 2026-10-31
    • Task: Triage the ensureWorker rejected-promise memo report against origin/main
  4. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/queuedHas a queued task in the Supabase agent queue
    on Oct 31, 2026
  5. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · triaged

  6. added
    work/assignedQueue worker lifecycle state
    queue/claimedQueue worker lifecycle state
    and removed
    queue/claimedQueue worker lifecycle state
    work/assignedQueue worker lifecycle state
    on Oct 31, 2026
  7. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · picked up

    • Agent: queue worker (repo-worker-code), Lane B shift
    • Session / ID: queue task 4131 (repo-queue-worker-shift-b, 2026-10-30 20:04 CDT run)
    • Found via: queue claim
    • Task: Fix fix(pm-dispatch): ensureWorker caches a rejected promise forever — one transient failure wedges every later launch #2902 — reset the ensureWorker memo on rejection in pm-stack/dispatch/src/queue.ts so a transient registration failure retries on the next launch (success stays cached), plus the fail-then-succeed-then-reuse unit test.
    • Expectation: effort s; done = branch + PR with the fix and test, merged when CI green and Codex clean
    • Links: branch queue/4131-2902-ensureworker-retry
  8. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 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.ts memoization 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.

  9. added a commit that references this issue on Oct 31, 2026
  10. dezren39 commented on Oct 31, 2026

    @dezren39
    MemberAuthor

    🤖 Agent note · done

    • Agent: queue worker (repo-worker-code), Lane B shift
    • Session / ID: queue task 4131 (repo-queue-worker-shift-b, 2026-10-30 20:04 CDT run)
    • Found via: queue claim
    • Task: Fixed — 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. New test/queue.test.ts covers fail-then-succeed-then-reuse, fetch-throw reset, and concurrent callers sharing one attempt/retry.
    • Expectation: done = PR merged — PR fix(pm-dispatch): reset ensureWorker memo on rejection (#2902) #2908 merged as f0a7b0a; issue auto-closed. Verified on laptop: bun test 42 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 --noEmit cannot run locally — @types/bun not installed; identical failure on clean main.)
    • Links: PR fix(pm-dispatch): reset ensureWorker memo on rejection (#2902) #2908 · branch queue/4131-2902-ensureworker-retry
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    app/pm-stackpm-stack (Pad/Grist) in developing-today/codeeffort/sSmall: expected under 2 hours of workkind/bugSomething isn't working (prefixed form of the classic bug label)priority/highImportant and time-sensitivesource/pr-diff-reviewFiled by the pr-diff-review jobtask/triageQueue packet kind: verify, reproduce, and route this item

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions