koala73/worldmonitor · error · Error
[_recordPendingBroadcast] lease lost: expected runId=${args.
Error message
[_recordPendingBroadcast] lease lost: expected runId=${args.runId}, found ${row.pendingRunId ?? "<cleared>"}. Refusing to persist broadcast progress — operator/another run owns the state. What it means
_recordPendingBroadcast validates pendingRunId === runId before persisting broadcast progress. A mismatch means lease ownership changed (another run or operator forceReleaseLease). It throws rather than overwriting another owner's state; the throw bubbles to Convex auto-Sentry for investigation.
Source
Thrown at convex/broadcast/rampRunner.ts:752
/**
* Persist post-`createProLaunchBroadcast` progress. Called by the runner
* AFTER `createProLaunchBroadcast` returns successfully. Lets
* `recoverFromPartialFailure` recover the broadcastId without
* operator-supplied metadata if the action dies between this point and a
* successful `_recordWaveSent`.
*
* Lease-validating: same semantics as `_recordPendingExport`.
*/
export const _recordPendingBroadcast = internalMutation({
args: {
runId: v.string(),
broadcastId: v.string(),
},
handler: async (ctx, args) => {
const row = await loadConfig(ctx);
if (!row) throw new Error("[_recordPendingBroadcast] no ramp configured");
if (row.pendingRunId !== args.runId) {
throw new Error(
`[_recordPendingBroadcast] lease lost: expected runId=${args.runId}, found ${row.pendingRunId ?? "<cleared>"}. Refusing to persist broadcast progress — operator/another run owns the state.`,
);
}
await ctx.db.patch(row._id, {
pendingBroadcastId: args.broadcastId,
pendingBroadcastAt: Date.now(),
});
return { ok: true as const };
},
});
/**
* Internal mutation that the action calls to atomically advance the tier +
* record a successful wave-send. Validates that the lease still belongs to
* this runId AND clears all pending-progress markers.
*/
export const _recordWaveSent = internalMutation({
args: {View on GitHub (pinned to ffec79ac33)
Solutions
- Investigate the current lease holder and whether its broadcast actually sent before doing anything.
- If stuck, forceReleaseLease then recoverFromPartialFailure (manual-finished if sent, discard-and-rotate if not).
- Eliminate concurrent triggers (make cron and manual mutually exclusive).
Defensive patterns
Strategy: try-catch
Validate before calling
// Runner-side: verify lease before persisting broadcast progress
const row = await ctx.db.query('broadcastRampConfig').withIndex('by_key', (q) => q.eq('key', 'current')).first();
if (!row || row.pendingRunId !== runId) {
return { aborted: 'lease-lost' };
} Type guard
function leaseIsOurs(row: { pendingRunId?: string } | null, runId: string): row is { pendingRunId: string } {
return row !== null && row.pendingRunId === runId;
} Try / catch
try {
await ctx.runMutation(internal.broadcast.rampRunner._recordPendingBroadcast, { runId, broadcastId });
} catch (e) {
if ((e as Error).message.includes('lease lost')) {
return { aborted: 'lease-lost', detail: (e as Error).message };
}
throw e;
} Prevention
- Prevent concurrent cron + manual triggers.
- Treat lease-lost as terminal; reconcile via recoverFromPartialFailure.
- Only forceReleaseLease when you have confirmed no live run is progressing.
When it happens
Trigger: Concurrent runs where a second claim overwrote pendingRunId; operator forceReleaseLease between createProLaunchBroadcast and this record; recoverFromPartialFailure cleared the lease.
Common situations: Cron overlapped a manual run; Convex runtime retry re-entered after release; operator and automation both touched a stalled run.
Related errors
- [_recordPendingExport] lease lost: expected runId=${args.run
- [_recordWaveSent] tier moved underneath us: expected ${args.
- [_recordWaveSent] lease lost: expected runId=${args.runId},
- [_recordPendingExport] no ramp configured
- [_recordPendingBroadcast] no ramp configured
AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12).
Data as JSON: /api/errors/e28790e95500eb28.
Report an issue: GitHub.