koala73/worldmonitor · error · ConvexError

NOTICE_NOT_FOUND

NOTICE_NOT_FOUND

Error message

NOTICE_NOT_FOUND

What it means

Thrown by acknowledgeNotice when the requested apiPlanLimitNotices row does not exist or exists but belongs to a different userId. As with revokeApiKey, missing and foreign-owned are collapsed into one error to avoid leaking notice ids across users. This guards the acknowledge mutation so a user can only acknowledge their own plan-limit notices.

Source

Thrown at convex/apiPlanLimitNotices.ts:431

        .collect();
      notices.push(...rows.filter((notice) => notice.acknowledgedAt === undefined));
    }
    return notices.sort((a, b) => {
      const severity = (state: ApiPlanLimitNoticeState) =>
        state === "over_limit" ? 3 : state === "sustained_burst" ? 2 : 1;
      const severityDiff = severity(b.state) - severity(a.state);
      return severityDiff || b.lastSeenAt - a.lastSeenAt;
    });
  },
});

export const acknowledgeNotice = mutation({
  args: { noticeId: v.id("apiPlanLimitNotices") },
  handler: async (ctx, args) => {
    const userId = await requireUserId(ctx);
    const notice = await ctx.db.get(args.noticeId);
    if (!notice || notice.userId !== userId) {
      throw new ConvexError("NOTICE_NOT_FOUND");
    }
    await ctx.db.patch(args.noticeId, { acknowledgedAt: Date.now() });
    return { ok: true };
  },
});

export const listEmailDue = internalQuery({
  args: {
    now: v.number(),
    limit: v.optional(v.number()),
  },
  handler: async (ctx, args) => {
    const max = args.limit ?? 100;
    // Scope to `current` in the INDEX so a backlog of superseded (current:false)
    // pending/failed rows -- which sort first by oldest lastSeenAt -- can never
    // consume the take() budget and starve genuinely-due live notices.
    const pending = await ctx.db
      .query("apiPlanLimitNotices")

View on GitHub (pinned to ffec79ac33)

Solutions

  1. Refresh the notices list and acknowledge a current, owned noticeId.
  2. Treat NOTICE_NOT_FOUND as a no-op success if the notice is already gone/acknowledged.
  3. Validate the noticeId is a valid Convex id for apiPlanLimitNotices.

Example fix

// before
await acknowledgeNotice(ctx, { noticeId: staleId }); // NOTICE_NOT_FOUND
// after — refresh and guard
const notices = await listNotices(ctx, {});
const target = notices.find(n => n.id === requestedId && !n.acknowledgedAt);
if (!target) return { ok: true }; // nothing to acknowledge
await acknowledgeNotice(ctx, { noticeId: target.id });
Defensive patterns

Strategy: validation

Validate before calling

const notices = await listNotices(ctx, {});
const target = notices.find(n => n.id === noticeId && !n.acknowledgedAt);
if (!target) return { ok: true };
await acknowledgeNotice(ctx, { noticeId: target.id });

Try / catch

try {
  await acknowledgeNotice(ctx, { noticeId });
} catch (e) {
  if (e instanceof ConvexError && e.message === "NOTICE_NOT_FOUND") {
    // notice gone/superseded — nothing to acknowledge
  } else throw e;
}

Prevention

When it happens

Trigger: Calling acknowledgeNotice with a noticeId that was deleted, never existed, or belongs to another user; passing a stale noticeId from an outdated UI list; passing a malformed/truncated Convex id.

Common situations: The notice was superseded (current:false) and pruned between the list render and the click; a stale UI rendered an old id; concurrent acknowledgment from another device already mutated state; cross-user id leak.

Related errors


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