koala73/worldmonitor · error · Error
[recoverFromPartialFailure:manual-finished] missing required
Error message
[recoverFromPartialFailure:manual-finished] missing required field(s): ${missing.join(", ")}. Operator must supply (or rely on persisted pending* state from a prior runner). Persisted state: pendingBroadcastId=${row.pendingBroadcastId ?? "-"}, pendingSegmentId=${row.pendingSegmentId ?? "-"}, pendingAssigned=${row.pendingAssigned ?? "-"}, pendingWaveLabel=${row.pendingWaveLabel ?? "-"}. What it means
The 'manual-finished' recovery branch must record the just-sent wave's metadata to advance the ramp. It resolves each field from operator args with persisted pending* markers as fallback, then requires broadcastId, segmentId, assigned, and sentAt. If any remain unresolved it throws, listing the missing fields and the persisted fallback state, so the operator knows exactly what to supply. sentAt has no fallback and is always operator-supplied.
Source
Thrown at convex/broadcast/rampRunner.ts:414
const assigned = args.assigned ?? row.pendingAssigned;
const nextTier = row.currentTier + 1;
if (nextTier >= row.rampCurve.length) {
throw new Error(
`[recoverFromPartialFailure:manual-finished] currentTier=${row.currentTier} would advance past rampCurve.length=${row.rampCurve.length}. Curve is complete; nothing to recover.`,
);
}
const waveLabel =
args.waveLabel ??
row.pendingWaveLabel ??
`${row.waveLabelPrefix}-${nextTier + row.waveLabelOffset}`;
const missing: string[] = [];
if (!broadcastId) missing.push("broadcastId");
if (!segmentId) missing.push("segmentId");
if (assigned === undefined) missing.push("assigned");
if (args.sentAt === undefined) missing.push("sentAt");
if (missing.length > 0) {
throw new Error(
`[recoverFromPartialFailure:manual-finished] missing required field(s): ${missing.join(", ")}. ` +
`Operator must supply (or rely on persisted pending* state from a prior runner). Persisted state: ` +
`pendingBroadcastId=${row.pendingBroadcastId ?? "-"}, pendingSegmentId=${row.pendingSegmentId ?? "-"}, ` +
`pendingAssigned=${row.pendingAssigned ?? "-"}, pendingWaveLabel=${row.pendingWaveLabel ?? "-"}.`,
);
}
await ctx.db.patch(row._id, {
currentTier: nextTier,
lastWaveLabel: waveLabel,
lastWaveBroadcastId: broadcastId,
lastWaveSegmentId: segmentId,
lastWaveAssigned: assigned,
lastWaveSentAt: args.sentAt,
lastRunStatus: `succeeded-via-manual-recovery: ${args.reason.slice(0, 200)}`,
lastRunAt: Date.now(),
lastRunError: undefined,
...clearPending,View on GitHub (pinned to ffec79ac33)
Solutions
- Supply all four required fields explicitly: broadcastId, segmentId, assigned, and sentAt (epoch ms).
- Read the persisted pending* values from the config row first and pass them through if still present.
- If the broadcast never actually sent, use recovery:'discard-and-rotate' instead of 'manual-finished'.
Example fix
// before
await ctx.runMutation(internal.broadcast.rampRunner.recoverFromPartialFailure, {
recovery: 'manual-finished', reason,
});
// after — supply all required fields (sentAt has no persisted fallback)
await ctx.runMutation(internal.broadcast.rampRunner.recoverFromPartialFailure, {
recovery: 'manual-finished', reason,
broadcastId: row.pendingBroadcastId,
segmentId: row.pendingSegmentId,
assigned: row.pendingAssigned,
sentAt: Date.parse(confirmedSendIso),
}); Defensive patterns
Strategy: validation
Validate before calling
// Validate all required manual-finished fields before calling recover
const row = await ctx.db.query('broadcastRampConfig').withIndex('by_key', (q) => q.eq('key', 'current')).first();
const broadcastId = args.broadcastId ?? row?.pendingBroadcastId;
const segmentId = args.segmentId ?? row?.pendingSegmentId;
const assigned = args.assigned ?? row?.pendingAssigned;
const missing = [
!broadcastId && 'broadcastId',
!segmentId && 'segmentId',
assigned === undefined && 'assigned',
args.sentAt === undefined && 'sentAt',
].filter(Boolean);
if (missing.length) throw new Error('Missing: ' + missing.join(', ')); Type guard
interface ManualFinishedArgs {
broadcastId: string;
segmentId: string;
assigned: number;
sentAt: number;
}
function isCompleteManualFinished(a: Partial<ManualFinishedArgs>): a is ManualFinishedArgs {
return typeof a.broadcastId === 'string' && typeof a.segmentId === 'string' && typeof a.assigned === 'number' && typeof a.sentAt === 'number';
} Prevention
- Remember sentAt has no persisted fallback — always supply it for manual-finished.
- Read pending* markers first and pass them through when present.
- Use discard-and-rotate, not manual-finished, when the broadcast never actually sent.
When it happens
Trigger: Calling recoverFromPartialFailure({recovery:'manual-finished'}) without supplying broadcastId/segmentId/assigned/sentAt AND the persisted pending* markers are absent (e.g. the run failed before _recordPendingExport/_recordPendingBroadcast ran, or were cleared).
Common situations: The runner crashed before persisting any pending markers; an operator manually cleared pending state then tried manual-finished without re-supplying all fields; forgetting sentAt which has no persisted fallback.
Related errors
- [recoverFromPartialFailure:manual-finished] currentTier=${ro
- [pauseRamp] no ramp configured
- [resumeRamp] no ramp configured
- [clearPartialFailure] no ramp configured
- [clearPartialFailure] refused: pending progress markers pres
AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12).
Data as JSON: /api/errors/25ddedf258b35eab.
Report an issue: GitHub.