{"record":{"id":"9da987c84741c550","repo":"koala73/worldmonitor","slug":"recoverfrompartialfailure-manual-finished-curren","errorCode":null,"errorMessage":"[recoverFromPartialFailure:manual-finished] currentTier=${row.currentTier} would advance past rampCurve.length=${row.rampCurve.length}. Curve is complete; nothing to recover.","messagePattern":"\\[recoverFromPartialFailure:manual-finished\\] currentTier=(.+?) would advance past rampCurve\\.length=(.+?)\\. Curve is complete; nothing to recover\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"convex/broadcast/rampRunner.ts","lineNumber":399,"sourceCode":"      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;\n\n    if (args.recovery === \"manual-finished\") {\n      // Resolve each field: operator-supplied wins, persisted pending* is the\n      // fallback. sentAt is operator-only (no fallback).\n      const broadcastId = args.broadcastId ?? row.pendingBroadcastId;\n      const segmentId = args.segmentId ?? row.pendingSegmentId;\n      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 ?? \"-\"}, ` +","sourceCodeStart":381,"sourceCodeEnd":417,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/broadcast/rampRunner.ts#L381-L417","documentation":"In the 'manual-finished' recovery branch, recoverFromPartialFailure computes nextTier = currentTier + 1 to advance the ramp. If nextTier is already >= rampCurve.length, the ramp curve is fully consumed and there is no further tier to advance to — recovering would write an out-of-bounds tier. The handler throws rather than silently no-op'ing or corrupting the pointer.","triggerScenarios":"Calling recoverFromPartialFailure({recovery:'manual-finished'}) when the last partial failure occurred on the final tier of the rampCurve and currentTier+1 would exceed the curve length.","commonSituations":"The final wave partially failed; the operator manually confirmed it sent and tries to mark it finished, but the curve has no room to advance; the ramp already reached completion elsewhere.","solutions":["Treat the ramp as complete: if the wave genuinely sent, record it directly (lastWave* fields) rather than advancing the tier, or run abortRamp.","Verify currentTier against rampCurve.length via getRampStatus before calling recovery.","If more waves are genuinely needed, abortRamp and initRamp with an extended rampCurve + correct waveLabelOffset/seed fields."],"exampleFix":"// before\nawait ctx.runMutation(internal.broadcast.rampRunner.recoverFromPartialFailure, { recovery: 'manual-finished', reason });\n\n// after\nconst s = await ctx.runQuery(internal.broadcast.rampRunner.getRampStatus, {});\nif (s.configured && s.currentTier + 1 >= s.rampCurve.length) {\n  throw new Error('Ramp curve complete — nothing to advance. Record manually or abortRamp.');\n}\nawait ctx.runMutation(internal.broadcast.rampRunner.recoverFromPartialFailure, { recovery: 'manual-finished', reason });","handlingStrategy":"validation","validationCode":"// Before manual-finished recovery, ensure the curve has room to advance\nconst s = await ctx.runQuery(internal.broadcast.rampRunner.getRampStatus, {});\nif (s.configured && s.currentTier + 1 >= s.rampCurve.length) {\n  throw new Error('Ramp curve complete — nothing to recover/advance. Record manually or abortRamp.');\n}","typeGuard":"function curveHasRoom(s: { configured: boolean; currentTier?: number; rampCurve?: number[] }): boolean {\n  return s.configured === true && typeof s.currentTier === 'number' && Array.isArray(s.rampCurve) && s.currentTier + 1 < s.rampCurve.length;\n}","tryCatchPattern":null,"preventionTips":["Check currentTier vs rampCurve.length before manual-finished recovery.","If the final wave sent, record it directly rather than advancing past the curve.","Extend rampCurve via abort+init if more waves are genuinely needed."],"tags":["convex","broadcast-ramp","partial-failure","bounds-check","operator-error"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}