{"record":{"id":"1d4d7f40ebbe8e78","repo":"koala73/worldmonitor","slug":"initramp-seedlastwavebroadcastid-and-seedlastwav","errorCode":null,"errorMessage":"[initRamp] seedLastWaveBroadcastId and seedLastWaveSentAt must be provided together.","messagePattern":"\\[initRamp\\] seedLastWaveBroadcastId and seedLastWaveSentAt must be provided together\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"convex/broadcast/rampRunner.ts","lineNumber":175,"sourceCode":"    // non-English locale (canonical via `users.localePrimary`, fallback\n    // via email-TLD heuristic for legacy waitlist registrants without a\n    // users row). Default false to preserve byte-identical behavior for\n    // existing ramps. Operators opt in for wave-8+ deliberately. Run\n    // `_dryRunNonEnglishExclusion` first to inspect impact before flipping.\n    excludeNonEnglish: v.optional(v.boolean()),\n  },\n  handler: async (ctx, args) => {\n    if (args.rampCurve.length === 0) {\n      throw new Error(\"[initRamp] rampCurve must be non-empty\");\n    }\n    if (args.rampCurve.some((n) => !Number.isInteger(n) || n <= 0)) {\n      throw new Error(\"[initRamp] rampCurve entries must be positive integers\");\n    }\n    const offset = args.waveLabelOffset ?? 0;\n    const hasSeedBroadcast = !!args.seedLastWaveBroadcastId;\n    const hasSeedSentAt = typeof args.seedLastWaveSentAt === \"number\";\n    if (hasSeedBroadcast !== hasSeedSentAt) {\n      throw new Error(\n        \"[initRamp] seedLastWaveBroadcastId and seedLastWaveSentAt must be provided together.\",\n      );\n    }\n    if (offset > 0 && !hasSeedBroadcast) {\n      throw new Error(\n        `[initRamp] waveLabelOffset=${offset} signals resumption after manual waves; seedLastWaveBroadcastId + seedLastWaveSentAt are required so the first cron tick can apply the kill-gate against the prior wave. Pass them, or set waveLabelOffset=0 to start a fresh ramp.`,\n      );\n    }\n    const existing = await ctx.db\n      .query(\"broadcastRampConfig\")\n      .withIndex(\"by_key\", (q) => q.eq(\"key\", RAMP_KEY))\n      .first();\n    if (existing) {\n      throw new Error(\n        `[initRamp] ramp already configured (active=${existing.active}, tier=${existing.currentTier}). Run abortRamp first if reconfiguring.`,\n      );\n    }\n    await ctx.db.insert(\"broadcastRampConfig\", {","sourceCodeStart":157,"sourceCodeEnd":193,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/broadcast/rampRunner.ts#L157-L193","documentation":"Thrown by initRamp when exactly one of seedLastWaveBroadcastId and seedLastWaveSentAt is provided. These two fields are a required pair because the kill-gate logic needs both the broadcast id (to fetch bounce/complaint stats) and the sent-at timestamp (to scope the stats window). Providing only one yields an inconsistent seed state.","triggerScenarios":"Calling initRamp with seedLastWaveBroadcastId set but seedLastWaveSentAt omitted (or vice versa); a config builder that populated one field from a prior-wave record but forgot the other.","commonSituations":"Operator manually filled in the broadcast id from the Resend dashboard but didn't look up the sent timestamp; a script pulled fields from an incomplete prior-wave record.","solutions":["Provide both seedLastWaveBroadcastId AND seedLastWaveSentAt together, or omit both entirely.","Look up the prior wave's record to obtain both values consistently."],"exampleFix":"// before\nawait initRamp(ctx, {\n  rampCurve: [100, 250], waveLabelPrefix: \"w\",\n  seedLastWaveBroadcastId: \"abc\", // seedLastWaveSentAt missing\n});\n// after — provide both (or neither)\nawait initRamp(ctx, {\n  rampCurve: [100, 250], waveLabelPrefix: \"w\",\n  seedLastWaveBroadcastId: \"abc\",\n  seedLastWaveSentAt: Date.parse(\"2024-10-15T12:00:00Z\"),\n});","handlingStrategy":"validation","validationCode":"const hasBroadcast = !!args.seedLastWaveBroadcastId;\nconst hasSentAt = typeof args.seedLastWaveSentAt === \"number\";\nif (hasBroadcast !== hasSentAt) {\n  throw new Error(\"Provide both seedLastWaveBroadcastId and seedLastWaveSentAt, or neither\");\n}\nawait initRamp(ctx, args);","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Always pass seed fields as a pair sourced from the same prior-wave record.","Add a config-lint check that the two seed fields are both present or both absent."],"tags":["convex","broadcast","ramp","validation","config","paired-args"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}