koala73/worldmonitor · error · ConvexError
quietHoursStart and quietHoursEnd must differ (same value =
Error message
quietHoursStart and quietHoursEnd must differ (same value = no quiet window)
What it means
Thrown in setQuietHours when quiet hours are effectively enabled (args.quietHoursEnabled ?? existing?.quietHoursEnabled ?? false is true) and the effective start and end hours resolve to the same value. An identical start/end denotes a zero-length window, which is treated as 'no quiet window' and rejected rather than silently stored. The check uses effective values — args override existing — so it can fire even when only one of the two is supplied.
Source
Thrown at convex/alertRules.ts:461
if (!identity) throw new ConvexError("UNAUTHENTICATED");
const userId = identity.subject;
await assertProEntitlement(ctx, userId);
validateQuietHoursArgs(args);
const existing = await ctx.db
.query("alertRules")
.withIndex("by_user_variant", (q) =>
q.eq("userId", userId).eq("variant", args.variant),
)
.unique();
// Only enforce start !== end when quiet hours are effectively enabled
const effectiveEnabled = args.quietHoursEnabled ?? existing?.quietHoursEnabled ?? false;
if (effectiveEnabled) {
const effectiveStart = args.quietHoursStart ?? existing?.quietHoursStart;
const effectiveEnd = args.quietHoursEnd ?? existing?.quietHoursEnd;
if (effectiveStart !== undefined && effectiveEnd !== undefined && effectiveStart === effectiveEnd) {
throw new ConvexError("quietHoursStart and quietHoursEnd must differ (same value = no quiet window)");
}
}
// resolveEffectivePair supplies sensitivity:'critical' on fresh insert (compatible
// by construction under the tightened rule). We DO NOT call assertCompatibleDeliveryMode here — quiet-hours
// mutations don't touch the (digestMode, sensitivity) pair, so blocking unrelated
// quiet-hours updates on pre-migration forbidden rows would surface as confusing
// generic 500s ('set-quiet-hours' HTTP action has no INCOMPATIBLE_DELIVERY
// passthrough). The relay coerce-at-read protects delivery for in-flight forbidden
// rows; the migration drains them.
// See docs/archive/plans/forbid-realtime-all-events.md + PR #3461 Greptile P1.
const pair = resolveEffectivePair({ existing: existing ?? undefined });
const now = Date.now();
const patch = {
quietHoursEnabled: args.quietHoursEnabled,
quietHoursStart: args.quietHoursStart,
quietHoursEnd: args.quietHoursEnd,View on GitHub (pinned to ffec79ac33)
Solutions
- Ensure the start and end hours differ when enabling quiet hours.
- When patching only one of start/end, verify the other (stored) value differs from the new one before submitting.
- If the user genuinely wants quiet hours disabled, set quietHoursEnabled:false instead of equal start/end.
Example fix
// before
await setQuietHours({ variant, quietHoursEnabled: true, quietHoursStart: 22, quietHoursEnd: 22 });
// after
await setQuietHours({ variant, quietHoursEnabled: true, quietHoursStart: 22, quietHoursEnd: 7 }); Defensive patterns
Strategy: validation
Validate before calling
function willQuietHoursBeEnabled(args, existing) {
return args.quietHoursEnabled ?? existing?.quietHoursEnabled ?? false;
}
function effectiveStartEnd(args, existing) {
return {
start: args.quietHoursStart ?? existing?.quietHoursStart,
end: args.quietHoursEnd ?? existing?.quietHoursEnd,
};
}
// before submit:
if (willQuietHoursBeEnabled(args, existing)) {
const { start, end } = effectiveStartEnd(args, existing);
if (start !== undefined && end !== undefined && start === end) {
// surface 'start and end must differ'
}
} Type guard
function isValidQuietWindow(args, existing): boolean {
const enabled = args.quietHoursEnabled ?? existing?.quietHoursEnabled ?? false;
if (!enabled) return true;
const start = args.quietHoursStart ?? existing?.quietHoursStart;
const end = args.quietHoursEnd ?? existing?.quietHoursEnd;
if (start === undefined || end === undefined) return true;
return start !== end;
} Try / catch
try {
await setQuietHours(args);
} catch (e) {
if (e instanceof ConvexError && /must differ/.test(String(e.message))) {
// prompt user to pick a distinct end hour
} else throw e;
} Prevention
- When patching only start or only end, read the stored counterpart and assert they differ before submitting.
- Disable the submit button in the UI when both fields resolve equal and quiet hours are enabled.
- If the user wants no quiet window, set quietHoursEnabled:false rather than equal start/end.
When it happens
Trigger: Calling setQuietHours with quietHoursEnabled:true and quietHoursStart === quietHoursEnd (both 7, both 22, etc.); calling with quietHoursEnabled:true and only quietHoursStart supplied when the stored quietHoursEnd equals the new start; calling with quietHoursEnabled:true and only quietHoursEnd supplied when the stored quietHoursStart equals the new end.
Common situations: UI defaulting both fields to the same hour; a copy/clone operation that duplicates the start into end; a rounding step that collapses both to the same value; partial update that happens to collide with the stored counterpart.
Related errors
- INCOMPATIBLE_DELIVERY
- quietHoursStart must be an integer 0–23
- quietHoursEnd must be an integer 0–23
- quietHoursTimezone must be a valid IANA timezone (e.g. Ameri
- COUNTRIES_LIMIT_EXCEEDED
AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12).
Data as JSON: /api/errors/8f34623b65defe43.
Report an issue: GitHub.