{"record":{"id":"5327feddd25ee194","repo":"koala73/worldmonitor","slug":"markpickcomplete-run-args-runid-is-run-sta","errorCode":null,"errorMessage":"[_markPickComplete] run ${args.runId} is ${run.status}, expected picking","messagePattern":"\\[_markPickComplete\\] run (.+?) is (.+?), expected picking","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"convex/broadcast/waveRuns.ts","lineNumber":604,"sourceCode":"/**\n * Transition a `picking`-status run to `segment-created` after pickWaveAction\n * has finished sampling, persisting, and creating the Resend segment.\n */\nexport const _markPickComplete = internalMutation({\n  args: {\n    runId: v.string(),\n    segmentId: v.string(),\n    totalCount: v.number(),\n    underfilled: v.boolean(),\n  },\n  handler: async (ctx, args) => {\n    const run = await ctx.db\n      .query(\"waveRuns\")\n      .withIndex(\"by_runId\", (q) => q.eq(\"runId\", args.runId))\n      .unique();\n    if (!run) throw new Error(`[_markPickComplete] no run ${args.runId}`);\n    if (run.status !== \"picking\") {\n      throw new Error(\n        `[_markPickComplete] run ${args.runId} is ${run.status}, expected picking`,\n      );\n    }\n    const now = Date.now();\n    await ctx.db.patch(run._id, {\n      status: \"segment-created\",\n      segmentId: args.segmentId,\n      totalCount: args.totalCount,\n      underfilled: args.underfilled,\n      updatedAt: now,\n    });\n    return { ok: true };\n  },\n});\n\n/**\n * Record a pick-phase failure. Lease policy depends on substatus:\n *   - 'empty-pool' clears the lease (terminal no-op; operator may retry next cycle)","sourceCodeStart":586,"sourceCodeEnd":622,"githubUrl":"https://github.com/koala73/worldmonitor/blob/ffec79ac339946fd2d24e85845da5755dcaa534b/convex/broadcast/waveRuns.ts#L586-L622","documentation":"Thrown by _markPickComplete when the waveRuns row exists but its status is not 'picking' — the expected state at the end of the pick phase. This is a state-machine invariant violation: the run has already transitioned (e.g. to 'segment-created', 'failed', or was discarded) and cannot be advanced again. The error reports the actual status found.","triggerScenarios":"_markPickComplete is called twice for the same runId (duplicate scheduled action, retry). The run was already marked failed by _markPickFailed. The operator ran discardWaveRun or resumeStalledWaveRun which transitioned the status. A replayed/delayed scheduled action fires after the run already advanced.","commonSituations":"A scheduled action fires twice (Convex at-least-once delivery). The operator manually advanced or discarded the run while pickWaveAction was still in flight. A retry of pickWaveAction after a partial failure re-attempts _markPickComplete.","solutions":["Treat this as idempotent-rejection: if the run is already 'segment-created', the pick phase already completed and no action is needed — log and exit.","If the status is a failure substatus, do not retry _markPickComplete; the run needs operator intervention (discardWaveRun or resumeStalledWaveRun).","Prevent duplicate scheduling: ensure pickWaveAction does not schedule _markPickComplete more than once per run."],"exampleFix":null,"handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try {\n  await ctx.runMutation(internal.broadcast.waveRuns._markPickComplete, { runId, segmentId, totalCount, underfilled });\n} catch (err) {\n  if (String(err).includes(\"expected picking\")) {\n    // run already advanced — check current status and treat as idempotent if already 'segment-created'\n    const run = await ctx.runQuery(internal.broadcast.waveRuns._getRun, { runId });\n    if (run?.status === \"segment-created\") return; // already done, safe to continue pipeline\n  }\n  throw err;\n}","preventionTips":["Ensure pickWaveAction schedules _markPickComplete exactly once per run.","Handle the 'expected picking' error as idempotent rejection: if the run is already 'segment-created', continue; otherwise halt.","Avoid retrying pickWaveAction wholesale after a partial failure without checking the run's current status."],"tags":["convex","state-machine","wave-runs","idempotency","internal-mutation"],"backgroundTag":null,"analyzedSha":"ffec79ac339946fd2d24e85845da5755dcaa534b","analyzedAt":"2026-08-12T11:24:56.012Z","schemaVersion":2},"datasetVersion":"2026-08-13T09:17:06.757Z"}