{"record":{"id":"2d7dcaf9c3a2d600","repo":"koala73/worldmonitor","slug":"initramp-wavelabeloffset-offset-signals-resum","errorCode":null,"errorMessage":"[initRamp] waveLabelOffset=${offset} signals resumption after manual waves; seedLastWaveBroadcastId + seedLastWaveSentAt are required so the first cron tick can apply the kill-gate against the prior wave. Pass them, or set waveLabelOffset=0 to start a fresh ramp.","messagePattern":"\\[initRamp\\] waveLabelOffset=(.+?) signals resumption after manual waves; seedLastWaveBroadcastId \\+ seedLastWaveSentAt are required so the first cron tick can apply the kill-gate against the prior wave\\. Pass them, or set waveLabelOffset=0 to start a fresh ramp\\.","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"convex/broadcast/rampRunner.ts","lineNumber":180,"sourceCode":"    excludeNonEnglish: v.optional(v.boolean()),\n  },\n  handler: async (ctx, args) => {\n    if (args.rampCurve.length === 0) {\n      throw new Error(\"[initRamp] rampCurve must be non-empty\");\n    }\n    if (args.rampCurve.some((n) => !Number.isInteger(n) || n <= 0)) {\n      throw new Error(\"[initRamp] rampCurve entries must be positive integers\");\n    }\n    const offset = args.waveLabelOffset ?? 0;\n    const hasSeedBroadcast = !!args.seedLastWaveBroadcastId;\n    const hasSeedSentAt = typeof args.seedLastWaveSentAt === \"number\";\n    if (hasSeedBroadcast !== hasSeedSentAt) {\n      throw new Error(\n        \"[initRamp] seedLastWaveBroadcastId and seedLastWaveSentAt must be provided together.\",\n      );\n    }\n    if (offset > 0 && !hasSeedBroadcast) {\n      throw new Error(\n        `[initRamp] waveLabelOffset=${offset} signals resumption after manual waves; seedLastWaveBroadcastId + seedLastWaveSentAt are required so the first cron tick can apply the kill-gate against the prior wave. Pass them, or set waveLabelOffset=0 to start a fresh ramp.`,\n      );\n    }\n    const existing = await ctx.db\n      .query(\"broadcastRampConfig\")\n      .withIndex(\"by_key\", (q) => q.eq(\"key\", RAMP_KEY))\n      .first();\n    if (existing) {\n      throw new Error(\n        `[initRamp] ramp already configured (active=${existing.active}, tier=${existing.currentTier}). Run abortRamp first if reconfiguring.`,\n      );\n    }\n    await ctx.db.insert(\"broadcastRampConfig\", {\n      key: RAMP_KEY,\n      active: true,\n      rampCurve: args.rampCurve,\n      currentTier: -1,\n      waveLabelPrefix: args.waveLabelPrefix,","sourceCodeStart":162,"sourceCodeEnd":198,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/broadcast/rampRunner.ts#L162-L198","documentation":"Thrown by initRamp when waveLabelOffset > 0 (signaling the ramp resumes after one or more manually-sent waves) but the seed pair (seedLastWaveBroadcastId + seedLastWaveSentAt) is not provided. The first cron tick must apply the kill-gate against the prior wave, which requires the seed broadcast id and timestamp. Without them, the kill-gate would be silently skipped — a dangerous failure mode.","triggerScenarios":"Calling initRamp with waveLabelOffset set to a positive number (e.g. 7, meaning waves 0-6 were sent manually) but without seedLastWaveBroadcastId and seedLastWaveSentAt; resuming a ramp after manual waves and forgetting to seed the prior-wave state.","commonSituations":"Operator sent several waves manually before enabling the auto-ramp, then called initRamp with the offset but didn't pass the seed values; a resume-ramp runbook step was skipped.","solutions":["Provide seedLastWaveBroadcastId and seedLastWaveSentAt when waveLabelOffset > 0, so the first cron tick can evaluate the kill-gate against the prior wave.","Alternatively, set waveLabelOffset=0 to start a fresh ramp with no prior waves (no seed needed).","Follow the resume-ramp runbook: capture the last manual wave's broadcast id and sent timestamp before calling initRamp."],"exampleFix":"// before\nawait initRamp(ctx, {\n  rampCurve: [500, 1000], waveLabelPrefix: \"w\",\n  waveLabelOffset: 7, // signals resumption, but no seed\n});\n// after — seed the prior wave\nawait initRamp(ctx, {\n  rampCurve: [500, 1000], waveLabelPrefix: \"w\",\n  waveLabelOffset: 7,\n  seedLastWaveBroadcastId: \"last-broadcast-id\",\n  seedLastWaveSentAt: Date.parse(\"2024-10-14T00:00:00Z\"),\n});","handlingStrategy":"validation","validationCode":"const offset = args.waveLabelOffset ?? 0;\nif (offset > 0 && (!args.seedLastWaveBroadcastId || typeof args.seedLastWaveSentAt !== \"number\")) {\n  throw new Error(\"waveLabelOffset > 0 requires seedLastWaveBroadcastId + seedLastWaveSentAt\");\n}\nawait initRamp(ctx, args);","typeGuard":null,"tryCatchPattern":null,"preventionTips":["Follow the resume-ramp runbook: capture the last manual wave's id and timestamp before init.","For fresh ramps, leave waveLabelOffset at 0 to avoid the seed requirement.","Validate the resume/seed invariant in a config-lint step."],"tags":["convex","broadcast","ramp","validation","kill-gate","config"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}