{"record":{"id":"85857d3f8df37644","repo":"koala73/worldmonitor","slug":"assignandexportwave-pool-empty-all-registratio","errorCode":null,"errorMessage":"[assignAndExportWave] pool empty — all registrations are suppressed/paid/already-stamped. Nothing to send.","messagePattern":"\\[assignAndExportWave\\] pool empty — all registrations are suppressed/paid/already-stamped\\. Nothing to send\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"warning","filePath":"convex/broadcast/audienceWaveExport.ts","lineNumber":350,"sourceCode":"        { cursor, numItems: REGISTRATIONS_PAGE_SIZE },\n      );\n      for (const row of page.page) {\n        const email = row.normalizedEmail;\n        if (!email || email.length === 0) continue;\n        if (suppressedSet.has(email)) continue;\n        if (paidSet.has(email)) continue;\n        if (row.proLaunchWave) continue;\n        reservoir.offer(email);\n      }\n      if (page.isDone) break;\n      cursor = page.continueCursor;\n    }\n\n    const picked = reservoir.values();\n    const poolSize = reservoir.totalSeen();\n\n    if (picked.length === 0) {\n      throw new Error(\n        `[assignAndExportWave] pool empty — all registrations are suppressed/paid/already-stamped. Nothing to send.`,\n      );\n    }\n\n    // Step 3: create the Resend segment FIRST so we never stamp a\n    // contact until we know it has a destination to land in. If\n    // segment creation fails, the picked rows are still unstamped and\n    // remain available for the next wave's pick — no data loss, no\n    // stranded contacts.\n    const segmentName = `pro-launch-${waveLabel}`;\n    const segmentId = await createSegment(apiKey, segmentName);\n\n    // Step 4: push picked contacts to the segment, then stamp ONLY on\n    // successful push outcomes (created / linkedExisting /\n    // alreadyInSegment). This ordering is load-bearing — see the\n    // file docstring's \"Atomicity\" section.\n    //\n    //   - push succeeds  → stamp Convex → contact won't be re-picked","sourceCodeStart":332,"sourceCodeEnd":368,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/broadcast/audienceWaveExport.ts#L332-L368","documentation":"Thrown by assignAndExportWave when the reservoir picked zero eligible emails after streaming all registrations. Every candidate was filtered out because it was suppressed, paid, or already stamped with a proLaunchWave. This signals the waitlist is drained for this ramp tier and there is nothing to send.","triggerScenarios":"All registrations are in the suppressed set, the paid set, or already have proLaunchWave set; the waitlist has been fully processed for prior waves; suppression list grew to cover the entire remaining pool.","commonSituations":"End of a launch ramp — all registrants already received their wave; an over-broad suppression rule filtered everyone; paid users dominate the small remaining pool; a prior wave stamped the last unstamped rows.","solutions":["Treat this as end-of-ramp — reduce or stop further wave scheduling for this audience.","Inspect the suppression list and paid set sizes to confirm the pool is genuinely exhausted (not a misconfiguration).","If suppression is unexpectedly large, audit the suppression source for an over-broad rule.","Do not retry with the same parameters — the pool state will not change without new registrations."],"exampleFix":"// before\nawait assignAndExportWave(ctx, { waveLabel: \"wave-99\", count: 500 }); // pool empty\n// after — guard before calling; check remaining pool size\nconst remaining = await ctx.runQuery(internal.broadcast.audienceWaveExport._countEligible, {});\nif (remaining === 0) {\n  console.log(\"Waitlist exhausted — ramp complete.\");\n  return;\n}","handlingStrategy":"validation","validationCode":"// Pre-check eligible pool size before exporting\nconst remaining = await ctx.runQuery(internal.broadcast.audienceWaveExport._countEligible, {});\nif (remaining === 0) {\n  console.log(\"Waitlist exhausted — ramp complete.\");\n  return;\n}\nawait assignAndExportWave(ctx, { waveLabel, count });","typeGuard":null,"tryCatchPattern":"try {\n  await assignAndExportWave(ctx, { waveLabel, count });\n} catch (e) {\n  if (e.message.includes(\"pool empty\")) {\n    // end of ramp — stop scheduling further waves\n  } else throw e;\n}","preventionTips":["Monitor remaining eligible pool size as the ramp progresses.","Treat pool-empty as end-of-ramp, not a transient retry.","Audit the suppression list if the pool empties unexpectedly early."],"tags":["convex","broadcast","resend","wave-export","empty-pool","suppression"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}