{"record":{"id":"25ddedf258b35eab","repo":"koala73/worldmonitor","slug":"recoverfrompartialfailure-manual-finished-missin","errorCode":null,"errorMessage":"[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 ?? \"-\"}.","messagePattern":"\\[recoverFromPartialFailure:manual-finished\\] missing required field\\(s\\): (.+?)\\. Operator must supply \\(or rely on persisted pending\\* state from a prior runner\\)\\. Persisted state: pendingBroadcastId=(.+?), pendingSegmentId=(.+?), pendingAssigned=(.+?), pendingWaveLabel=(.+?)\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"convex/broadcast/rampRunner.ts","lineNumber":414,"sourceCode":"      const assigned = args.assigned ?? row.pendingAssigned;\n      const nextTier = row.currentTier + 1;\n      if (nextTier >= row.rampCurve.length) {\n        throw new Error(\n          `[recoverFromPartialFailure:manual-finished] currentTier=${row.currentTier} would advance past rampCurve.length=${row.rampCurve.length}. Curve is complete; nothing to recover.`,\n        );\n      }\n      const waveLabel =\n        args.waveLabel ??\n        row.pendingWaveLabel ??\n        `${row.waveLabelPrefix}-${nextTier + row.waveLabelOffset}`;\n\n      const missing: string[] = [];\n      if (!broadcastId) missing.push(\"broadcastId\");\n      if (!segmentId) missing.push(\"segmentId\");\n      if (assigned === undefined) missing.push(\"assigned\");\n      if (args.sentAt === undefined) missing.push(\"sentAt\");\n      if (missing.length > 0) {\n        throw new Error(\n          `[recoverFromPartialFailure:manual-finished] missing required field(s): ${missing.join(\", \")}. ` +\n            `Operator must supply (or rely on persisted pending* state from a prior runner). Persisted state: ` +\n            `pendingBroadcastId=${row.pendingBroadcastId ?? \"-\"}, pendingSegmentId=${row.pendingSegmentId ?? \"-\"}, ` +\n            `pendingAssigned=${row.pendingAssigned ?? \"-\"}, pendingWaveLabel=${row.pendingWaveLabel ?? \"-\"}.`,\n        );\n      }\n\n      await ctx.db.patch(row._id, {\n        currentTier: nextTier,\n        lastWaveLabel: waveLabel,\n        lastWaveBroadcastId: broadcastId,\n        lastWaveSegmentId: segmentId,\n        lastWaveAssigned: assigned,\n        lastWaveSentAt: args.sentAt,\n        lastRunStatus: `succeeded-via-manual-recovery: ${args.reason.slice(0, 200)}`,\n        lastRunAt: Date.now(),\n        lastRunError: undefined,\n        ...clearPending,","sourceCodeStart":396,"sourceCodeEnd":432,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/broadcast/rampRunner.ts#L396-L432","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","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'."],"exampleFix":"// before\nawait ctx.runMutation(internal.broadcast.rampRunner.recoverFromPartialFailure, {\n  recovery: 'manual-finished', reason,\n});\n\n// after — supply all required fields (sentAt has no persisted fallback)\nawait ctx.runMutation(internal.broadcast.rampRunner.recoverFromPartialFailure, {\n  recovery: 'manual-finished', reason,\n  broadcastId: row.pendingBroadcastId,\n  segmentId: row.pendingSegmentId,\n  assigned: row.pendingAssigned,\n  sentAt: Date.parse(confirmedSendIso),\n});","handlingStrategy":"validation","validationCode":"// Validate all required manual-finished fields before calling recover\nconst row = await ctx.db.query('broadcastRampConfig').withIndex('by_key', (q) => q.eq('key', 'current')).first();\nconst broadcastId = args.broadcastId ?? row?.pendingBroadcastId;\nconst segmentId = args.segmentId ?? row?.pendingSegmentId;\nconst assigned = args.assigned ?? row?.pendingAssigned;\nconst missing = [\n  !broadcastId && 'broadcastId',\n  !segmentId && 'segmentId',\n  assigned === undefined && 'assigned',\n  args.sentAt === undefined && 'sentAt',\n].filter(Boolean);\nif (missing.length) throw new Error('Missing: ' + missing.join(', '));","typeGuard":"interface ManualFinishedArgs {\n  broadcastId: string;\n  segmentId: string;\n  assigned: number;\n  sentAt: number;\n}\nfunction isCompleteManualFinished(a: Partial<ManualFinishedArgs>): a is ManualFinishedArgs {\n  return typeof a.broadcastId === 'string' && typeof a.segmentId === 'string' && typeof a.assigned === 'number' && typeof a.sentAt === 'number';\n}","tryCatchPattern":null,"preventionTips":["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."],"tags":["convex","broadcast-ramp","partial-failure","validation","operator-error"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}