koala73/worldmonitor · error · ConvexError

activation outcome buckets must be disjoint and contain at…

Error message

activation outcome buckets must be disjoint and contain at most ${proActivationStepIdValidator.members.length} steps

What it means

ConvexError guard validating the Pro activation outcome step buckets: confirmedSteps, skippedSteps, blockedSteps, and failedSteps combined must contain no duplicates and no more entries than the total number of known activation steps. Fires when a client double-reports a step in two buckets or fabricates unknown/duplicate step ids.

Solutions

  1. Place each step id in exactly one outcome bucket per revision
  2. Use only step ids from proActivationStepIdValidator's known members
  3. Rebuild the buckets client-side from the actual per-step outcomes before submitting
Defensive patterns

Strategy: validation

When it happens

Trigger: Thrown at convex/payments/billing.ts:981 when the library encounters an invalid state.

Common situations: See trigger scenarios.


AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-08-21). Data as JSON: /api/errors/0c8ac9ed0bea28c8. Report an issue: GitHub.

Appendix: source

Thrown at convex/payments/billing.ts:997

      !Number.isSafeInteger(args.revision) ||
      args.revision < 1 ||
      args.revision > MAX_PRO_ACTIVATION_OUTCOME_REVISION
    ) {
      throw new ConvexError(
        `activation outcome revision must be an integer from 1 to ${MAX_PRO_ACTIVATION_OUTCOME_REVISION}`,
      );
    }
    const allSteps = [
      ...args.confirmedSteps,
      ...args.skippedSteps,
      ...(args.blockedSteps ?? []),
      ...args.failedSteps,
    ];
    if (
      allSteps.length > proActivationStepIdValidator.members.length ||
      new Set(allSteps).size !== allSteps.length
    ) {
      throw new ConvexError(
        `activation outcome buckets must be disjoint and contain at most ${proActivationStepIdValidator.members.length} steps`,
      );
    }
    if (args.revision <= (presentation.outcomeRevision ?? 0)) {
      return false;
    }

    const now = Date.now();
    await ctx.db.patch(presentation._id, {
      confirmedSteps: args.confirmedSteps,
      skippedSteps: args.skippedSteps,
      // Written straight from the arg, NOT coerced to []. Convex removes a
      // field patched as `undefined`, so a client too old to report blocked
      // steps leaves the field ABSENT ("could not report") instead of claiming
      // "none were blocked" — a claim that is actively false for that client,
      // which classified denials as skips. Absent is also what lets an analyst
      // exclude those rows from a denial rate. Every snapshot stays a full
      // replacement either way: an explicit [] clears, and so does absence.

View on GitHub (pinned to 7d06c8633d)