koala73/worldmonitor · error · ConvexError
LEGACY_COMP_SOURCE_REQUIRES_AUDIT
LEGACY_COMP_SOURCE_REQUIRES_AUDIT
Error message
LEGACY_COMP_SOURCE_REQUIRES_AUDIT
What it means
During entitlement merge (merging an anonymous/legacy entitlement into an existing user entitlement), both sides have an ACTIVE comp (complimentary) period but the compPlanKey presence differs — meaning one comp came from a legacy source without a plan key. The system cannot safely decide which comp to keep, so it throws to force a manual audit instead of silently picking one.
Solutions
- Audit the affected entitlements manually and backfill/normalize compPlanKey (or clear one comp period) before retrying the merge.
- Identify the legacy comp grant source and update it to always set compPlanKey.
- As an operator, patch one of the two entitlements so only one active comp exists, then re-run the recompute.
Example fix
// before (data)
{ compUntil: 1735689600000, compPlanKey: undefined } // legacy comp missing plan key
// after
await ctx.db.patch(ent._id, { compPlanKey: "pro_monthly" }); // backfill, then re-run merge Defensive patterns
Strategy: try-catch
Validate before calling
const conflict = anonCompActive && existingCompActive && Boolean(anonEntitlement.compPlanKey) !== Boolean(existingEntitlement.compPlanKey); if (conflict) await auditLegacyCompSource(anonEntitlement, existingEntitlement); // resolve before merge
Try / catch
try {
await mergeEntitlements(args);
} catch (e) {
if (isConvexError(e) && e.data?.kind === "LEGACY_COMP_SOURCE_REQUIRES_AUDIT") {
enqueueEntitlementAudit(e.data, args.userId);
}
} Prevention
- Always set compPlanKey when granting comp time.
- Backfill legacy entitlements before enabling account-claim merges.
- Monitor for entitlements with active compUntil but null compPlanKey.
When it happens
Trigger: Recompute/merge runs where anonEntitlement.compUntil and existingEntitlement.compUntil are both > recomputeTimestamp AND Boolean(anonEntitlement.compPlanKey) !== Boolean(existingEntitlement.compPlanKey) — i.e. one active comp has a plan key and the other doesn't.
Common situations: A user claims an account after using an anonymous comp granted before compPlanKey was introduced; data migration mixes pre- and post-planKey comp records; a comp was granted by a legacy script that left compPlanKey null.
Understand the failure class
Background: "This is a bug, please report it": internal invariant violations, unreachable panics, and SNH errors explained — this error's family across 47 libraries.
Related errors
- COMPANY_MONITORING_ADMISSION_EVIDENCE_INVALID
- buildAuthHeaders: free-tier context has no credentials — a f
- MCP_INTERNAL_HMAC_SECRET not configured
- ${label} HTTP ${status} (${billingCode})
- EMBED_ACCESS_REQUIRED
AI-assisted analysis of koala73/worldmonitor@7d06c8633d (2026-09-15).
Data as JSON: /api/errors/dd93194277fd110e.
Report an issue: GitHub.
Appendix: source
Thrown at convex/payments/billing.ts:3829
// Move entitlement rows first, then let the shared recompute path derive
// the final paid/free state from the post-claim subscriptions. If the
// anonymous row carried a future complimentary floor, transfer it only
// when it does not undercut stronger current real-user coverage.
const recomputeTimestamp = Date.now();
if (anonEntitlement) {
const existingEntitlement = await ctx.db
.query("entitlements")
.withIndex("by_userId", (q) => q.eq("userId", realUserId))
.first();
if (existingEntitlement) {
const anonCompUntil = anonEntitlement.compUntil ?? 0;
const existingCompUntil = existingEntitlement.compUntil ?? 0;
const anonCompActive = anonCompUntil > recomputeTimestamp;
const existingCompActive = existingCompUntil > recomputeTimestamp;
if (anonCompActive && existingCompActive
&& Boolean(anonEntitlement.compPlanKey) !== Boolean(existingEntitlement.compPlanKey)) {
throw new ConvexError({ kind: "LEGACY_COMP_SOURCE_REQUIRES_AUDIT" });
}
if (anonEntitlement.compPlanKey && anonCompUntil > recomputeTimestamp) {
const existingCompIsStronger = existingEntitlement.compPlanKey
&& existingCompUntil > recomputeTimestamp
&& compareEntitlementPlans(
{ planKey: existingEntitlement.compPlanKey, validUntil: existingCompUntil },
{ planKey: anonEntitlement.compPlanKey, validUntil: anonCompUntil },
) >= 0;
await ctx.db.patch(existingEntitlement._id, {
compPlanKey: existingCompIsStronger
? existingEntitlement.compPlanKey
: anonEntitlement.compPlanKey,
compUntil: Math.max(existingCompUntil, anonCompUntil),
});
} else if (anonCompUntil > existingCompUntil && anonCompUntil > recomputeTimestamp) {
const realSubscriptions = await ctx.db
.query("subscriptions")
.withIndex("by_userId", (q) => q.eq("userId", realUserId))View on GitHub (pinned to 7d06c8633d)