{"record":{"id":"6080f96fc3098d9a","repo":"koala73/worldmonitor","slug":"recoverfrompartialfailure-no-ramp-configured","errorCode":null,"errorMessage":"[recoverFromPartialFailure] no ramp configured","messagePattern":"\\[recoverFromPartialFailure\\] no ramp configured","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"convex/broadcast/rampRunner.ts","lineNumber":371,"sourceCode":"export const recoverFromPartialFailure = internalMutation({\n  args: {\n    recovery: v.union(\n      v.literal(\"manual-finished\"),\n      v.literal(\"discard-and-rotate\"),\n    ),\n    reason: v.string(),\n    // For recovery==='manual-finished'. Fall back to persisted pending*\n    // markers when omitted; sentAt is ALWAYS operator-supplied (no progress\n    // marker captures send-completion time).\n    broadcastId: v.optional(v.string()),\n    segmentId: v.optional(v.string()),\n    sentAt: v.optional(v.number()),\n    assigned: v.optional(v.number()),\n    waveLabel: v.optional(v.string()),\n  },\n  handler: async (ctx, args) => {\n    const row = await loadConfig(ctx);\n    if (!row) throw new Error(\"[recoverFromPartialFailure] no ramp configured\");\n    if (row.lastRunStatus !== \"partial-failure\") {\n      return {\n        ok: true as const,\n        noop: true as const,\n        currentStatus: row.lastRunStatus,\n      };\n    }\n\n    const clearPending = {\n      pendingRunId: undefined,\n      pendingRunStartedAt: undefined,\n      pendingWaveLabel: undefined,\n      pendingSegmentId: undefined,\n      pendingAssigned: undefined,\n      pendingExportAt: undefined,\n      pendingBroadcastId: undefined,\n      pendingBroadcastAt: undefined,\n    } as const;","sourceCodeStart":353,"sourceCodeEnd":389,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/broadcast/rampRunner.ts#L353-L389","documentation":"recoverFromPartialFailure is the operator recovery path for a partial-failure wave. It loads the config via loadConfig and throws if no row exists, because there is no partial-failure state to recover from on a non-existent ramp.","triggerScenarios":"Calling recoverFromPartialFailure before initRamp, after abortRamp deleted the row, or in an unseeded deployment.","commonSituations":"Recovery runbook executed after the ramp was already aborted; automation targeting the wrong Convex project.","solutions":["Preflight with getRampStatus; if {configured:false} there is nothing to recover.","If abortRamp already ran, the partial-failure state was removed with the row — re-seed with initRamp if launching again.","Confirm the correct Convex deployment is targeted."],"exampleFix":"// before\nawait ctx.runMutation(internal.broadcast.rampRunner.recoverFromPartialFailure, { recovery: 'manual-finished', reason });\n\n// after\nconst status = await ctx.runQuery(internal.broadcast.rampRunner.getRampStatus, {});\nif (!status.configured) return { skipped: 'no ramp configured' };\nawait ctx.runMutation(internal.broadcast.rampRunner.recoverFromPartialFailure, { recovery: 'manual-finished', reason });","handlingStrategy":"validation","validationCode":"// Preflight before recoverFromPartialFailure\nconst status = await ctx.runQuery(internal.broadcast.rampRunner.getRampStatus, {});\nif (!status.configured) {\n  return { skipped: 'no ramp configured — nothing to recover' };\n}","typeGuard":"function isConfigured(s: { configured: boolean }): s is { configured: true; lastRunStatus?: string } {\n  return s.configured === true;\n}","tryCatchPattern":null,"preventionTips":["Preflight recovery with getRampStatus.","After abortRamp, the partial-failure state is gone with the row — re-seed if needed.","Confirm the target deployment holds the config row before invoking recovery tooling."],"tags":["convex","broadcast-ramp","state-missing","partial-failure"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}