koala73/worldmonitor · error · Error
[_recordPendingExport] lease lost: expected runId=${args.run
Error message
[_recordPendingExport] lease lost: expected runId=${args.runId}, found ${row.pendingRunId ?? "<cleared>"}. Refusing to persist export progress — operator/another run owns the state. What it means
_recordPendingExport validates that broadcastRampConfig.pendingRunId still equals the caller's runId before persisting export progress. A mismatch means the lease was taken over — another run claimed it, or an operator called forceReleaseLease. The handler throws to avoid overwriting another owner's progress, and the error surfaces to Convex auto-Sentry so ops can investigate.
Source
Thrown at convex/broadcast/rampRunner.ts:720
* metadata if the action dies between this point and a successful
* `_recordWaveSent`.
*
* Lease-validating: throws if the lease has changed (operator
* `forceReleaseLease` mid-flight, or a different run claimed). The throw
* bubbles to Convex auto-Sentry; the runner stops without advancing.
*/
export const _recordPendingExport = internalMutation({
args: {
runId: v.string(),
waveLabel: v.string(),
segmentId: v.string(),
assigned: v.number(),
},
handler: async (ctx, args) => {
const row = await loadConfig(ctx);
if (!row) throw new Error("[_recordPendingExport] no ramp configured");
if (row.pendingRunId !== args.runId) {
throw new Error(
`[_recordPendingExport] lease lost: expected runId=${args.runId}, found ${row.pendingRunId ?? "<cleared>"}. Refusing to persist export progress — operator/another run owns the state.`,
);
}
await ctx.db.patch(row._id, {
pendingWaveLabel: args.waveLabel,
pendingSegmentId: args.segmentId,
pendingAssigned: args.assigned,
pendingExportAt: Date.now(),
});
return { ok: true as const };
},
});
/**
* 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 aView on GitHub (pinned to ffec79ac33)
Solutions
- Do not retry blindly — investigate which runId currently holds the lease and whether its wave completed.
- If the current holder is stale/stuck, use forceReleaseLease then recoverFromPartialFailure as appropriate.
- Prevent concurrency by ensuring the cron and manual triggers are mutually exclusive.
Defensive patterns
Strategy: try-catch
Validate before calling
// Runner-side: verify lease ownership before persisting
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' }; // do not persist; do not retry
} 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._recordPendingExport, { runId, ... });
} catch (e) {
if ((e as Error).message.includes('lease lost')) {
// ownership moved — stop this run; investigate current holder
return { aborted: 'lease-lost', detail: (e as Error).message };
}
throw e;
} Prevention
- Make the cron and any manual trigger mutually exclusive to avoid overlapping claims.
- Treat lease-lost as terminal for the losing run — never blind-retry.
- Use forceReleaseLease deliberately; it should be the only non-runner path that clears the lease.
When it happens
Trigger: Two concurrent runs (cron + manual, or a Convex runtime retry) where a second _claimTierForRun overwrote pendingRunId; an operator forceReleaseLease'd mid-flight; recoverFromPartialFailure cleared the lease.
Common situations: Cron fired while a manual run was still executing; a transient Convex retry re-entered the action after the lease was released; operator and automation both acted on a stalled run.
Related errors
- [_recordPendingBroadcast] lease lost: expected runId=${args.
- [_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/e316ed16f36cfa3f.
Report an issue: GitHub.