{"record":{"id":"e9727c4f5a77917d","repo":"koala73/worldmonitor","slug":"rundailyramp-rampcurve-nexttier-is-undefined","errorCode":null,"errorMessage":"[runDailyRamp] rampCurve[${nextTier}] is undefined despite bounds check — config corruption?","messagePattern":"\\[runDailyRamp\\] rampCurve\\[(.+?)\\] is undefined despite bounds check — config corruption\\?","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"critical","filePath":"convex/broadcast/rampRunner.ts","lineNumber":1025,"sourceCode":"      }\n    }\n\n    // ──── Step 2: figure out which tier to send next ────\n    const nextTier = row.currentTier + 1;\n    if (nextTier >= row.rampCurve.length) {\n      console.log(\"[runDailyRamp] ramp curve complete — deactivating\");\n      await ctx.runMutation(\n        internal.broadcast.rampRunner._recordRunOutcome,\n        { status: \"ramp-complete\", deactivate: true },\n      );\n      return { status: \"ramp-complete\" };\n    }\n    // Bounds-checked above; explicit guard quiets noUncheckedIndexedAccess\n    // and protects against a future code change that breaks the\n    // bounds check above without realising this index is now unsafe.\n    const count = row.rampCurve[nextTier];\n    if (count === undefined) {\n      throw new Error(\n        `[runDailyRamp] rampCurve[${nextTier}] is undefined despite bounds check — config corruption?`,\n      );\n    }\n    const waveLabel = `${row.waveLabelPrefix}-${nextTier + row.waveLabelOffset}`;\n\n    // ──── Step 3: in-flight guard via the new wave-loading state machine ────\n    // PR 2 (post-launch-stabilization plan, 2026-04-29) replaces the\n    // monolithic assignAndExportWave + createProLaunchBroadcast + sendProLaunchBroadcast\n    // chain with a self-driving multi-step pipeline that fits within the\n    // Convex 10-min action runtime budget at any wave size. The state lives\n    // on `waveRuns` + `wavePickedContacts`. See `convex/broadcast/waveRuns.ts`.\n    //\n    // The legacy `_claimTierForRun` + `_recordPendingExport` + `_recordPendingBroadcast`\n    // + `_recordWaveSent` + `_recordRunOutcome` mutations remain in this\n    // module so the operator-recovery commands (`recoverFromPartialFailure`,\n    // `clearPartialFailure`, `forceReleaseLease`) keep working on any\n    // legacy partial-failure rows. New runs go through the state machine.\n    //","sourceCodeStart":1007,"sourceCodeEnd":1043,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/broadcast/rampRunner.ts#L1007-L1043","documentation":"In runDailyRamp, after a bounds check confirms nextTier < rampCurve.length, the code indexes rampCurve[nextTier] and guards against undefined as a defensive measure (it quiets noUncheckedIndexedAccess and protects against a future code change breaking the bounds check). If this throw fires, the rampCurve array was mutated/shortened between the bounds check and the index, or the persisted config is internally inconsistent.","triggerScenarios":"The broadcastRampConfig.rampCurve array was concurrently shortened (another mutation) between the length check and the index read; a corrupted/hand-edited config row has a rampCurve with holes or shorter than its recorded length; a Convex OCC retry saw a stale row.","commonSituations":"An operator mutation rewrote rampCurve mid-tick; a buggy script wrote an inconsistent row; extremely rare race on config rewrite.","solutions":["Inspect the broadcastRampConfig row's rampCurve and currentTier directly — confirm internal consistency.","If corrupted, abortRamp and re-seed via initRamp with a valid rampCurve.","Add a guard around any code path that mutates rampCurve post-init (it should be immutable after initRamp)."],"exampleFix":null,"handlingStrategy":"validation","validationCode":"// Validate config consistency before the runner indexes rampCurve\nconst row = await ctx.db.query('broadcastRampConfig').withIndex('by_key', (q) => q.eq('key', 'current')).first();\nif (!row) throw new Error('no ramp configured');\nconst nextTier = row.currentTier + 1;\nif (nextTier >= row.rampCurve.length || row.rampCurve[nextTier] === undefined) {\n  throw new Error(`rampCurve inconsistent at nextTier=${nextTier} (length=${row.rampCurve.length}) — config corruption; abort+reseed.`);\n}","typeGuard":"function rampCurveIsConsistent(row: { currentTier: number; rampCurve: number[] }): boolean {\n  return Array.isArray(row.rampCurve) && row.rampCurve.length > 0 && row.rampCurve.every((n) => typeof n === 'number' && Number.isInteger(n) && n > 0);\n}","tryCatchPattern":null,"preventionTips":["Treat rampCurve as immutable after initRamp; never mutate it on the live config.","If reconfiguration is needed, abortRamp then initRamp fresh — do not patch rampCurve in place.","Add a table-level invariant test that broadcastRampConfig.rampCurve has no holes."],"tags":["convex","broadcast-ramp","invariant","config-corruption","defensive"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}