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

  1. Supply all four required fields explicitly: broadcastId, segmentId, assigned, and sentAt (epoch ms).
  2. Read the persisted pending* values from the config row first and pass them through if still present.
  3. 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

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


AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12). Data as JSON: /api/errors/25ddedf258b35eab. Report an issue: GitHub.