koala73/worldmonitor · error · Error

[initRamp] rampCurve entries must be positive integers

Error message

[initRamp] rampCurve entries must be positive integers

What it means

Thrown by initRamp when any entry in args.rampCurve fails Number.isInteger or is <= 0. Each curve entry is a per-wave recipient count and must be a positive integer — fractional, zero, or negative counts are meaningless for audience sizing and would break the reservoir-sampling math downstream.

Source

Thrown at convex/broadcast/rampRunner.ts:169

    seedLastWaveBroadcastId: v.optional(v.string()),
    seedLastWaveSentAt: v.optional(v.number()),
    seedLastWaveLabel: v.optional(v.string()),
    seedLastWaveSegmentId: v.optional(v.string()),
    seedLastWaveAssigned: v.optional(v.number()),
    // Locale filter — when true, pickWaveAction excludes contacts with
    // non-English locale (canonical via `users.localePrimary`, fallback
    // via email-TLD heuristic for legacy waitlist registrants without a
    // users row). Default false to preserve byte-identical behavior for
    // existing ramps. Operators opt in for wave-8+ deliberately. Run
    // `_dryRunNonEnglishExclusion` first to inspect impact before flipping.
    excludeNonEnglish: v.optional(v.boolean()),
  },
  handler: async (ctx, args) => {
    if (args.rampCurve.length === 0) {
      throw new Error("[initRamp] rampCurve must be non-empty");
    }
    if (args.rampCurve.some((n) => !Number.isInteger(n) || n <= 0)) {
      throw new Error("[initRamp] rampCurve entries must be positive integers");
    }
    const offset = args.waveLabelOffset ?? 0;
    const hasSeedBroadcast = !!args.seedLastWaveBroadcastId;
    const hasSeedSentAt = typeof args.seedLastWaveSentAt === "number";
    if (hasSeedBroadcast !== hasSeedSentAt) {
      throw new Error(
        "[initRamp] seedLastWaveBroadcastId and seedLastWaveSentAt must be provided together.",
      );
    }
    if (offset > 0 && !hasSeedBroadcast) {
      throw new Error(
        `[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.`,
      );
    }
    const existing = await ctx.db
      .query("broadcastRampConfig")
      .withIndex("by_key", (q) => q.eq("key", RAMP_KEY))
      .first();

View on GitHub (pinned to ffec79ac33)

Solutions

  1. Ensure every entry in rampCurve is a positive integer (>= 1).
  2. Round/ceil computed counts to integers and clamp to a minimum of 1 before passing.
  3. Validate the curve with a pre-check: curve.every(n => Number.isInteger(n) && n > 0).

Example fix

// before
await initRamp(ctx, { rampCurve: [100, 0, 500.5], waveLabelPrefix: "w" });
// after
await initRamp(ctx, { rampCurve: [100, 50, 501], waveLabelPrefix: "w" });
Defensive patterns

Strategy: validation

Validate before calling

if (!rampCurve.every(n => Number.isInteger(n) && n > 0)) {
  throw new Error("rampCurve entries must be positive integers");
}
await initRamp(ctx, { rampCurve, waveLabelPrefix });

Type guard

function isPositiveIntegerCurve(c: unknown): c is number[] {
  return Array.isArray(c) && c.every(n => Number.isInteger(n) && (n as number) > 0);
}

Prevention

When it happens

Trigger: Calling initRamp with rampCurve containing a zero (e.g. [100, 0, 500]), a negative number, a fractional value like 100.5, or a value that is not a number (NaN coerced).

Common situations: Curve read from a JSON config that had a null/0 placeholder; a computed curve where a percentage multiplied down to a fraction; a templating bug inserted a non-numeric value.

Related errors


AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12). Data as JSON: /api/errors/4c0e109478563872. Report an issue: GitHub.