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
- Refresh the notices list and acknowledge a current, owned noticeId.
- Treat NOTICE_NOT_FOUND as a no-op success if the notice is already gone/acknowledged.
- 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
- Refresh notices before acknowledging.
- Treat NOTICE_NOT_FOUND as success in idempotent acknowledge flows.
- Filter out acknowledged notices from the UI list.
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.