koala73/worldmonitor · error · Error

[_persistPickedBatch] chunk too large: ${contacts.length} >

Error message

[_persistPickedBatch] chunk too large: ${contacts.length} > ${PERSIST_CHUNK_SIZE}

What it means

Thrown by _persistPickedBatch when the contacts array length exceeds PERSIST_CHUNK_SIZE (defined as 500 in waveRuns.ts:77). The mutation exists specifically to stay under Convex's per-mutation write limit, so it refuses oversized chunks rather than attempting a write that Convex would reject. The caller (pickWaveAction) is responsible for slicing into 500-row chunks.

Source

Thrown at convex/broadcast/waveRuns.ts:564

      createdAt: now,
      updatedAt: now,
    });
    return { ok: true, runId: args.runId };
  },
});

/**
 * Insert a chunk of picked-contact rows. Called repeatedly from
 * `pickWaveAction` to stay under Convex per-mutation write limits.
 */
export const _persistPickedBatch = internalMutation({
  args: {
    runId: v.string(),
    contacts: v.array(v.string()), // normalizedEmails
  },
  handler: async (ctx, { runId, contacts }) => {
    if (contacts.length > PERSIST_CHUNK_SIZE) {
      throw new Error(
        `[_persistPickedBatch] chunk too large: ${contacts.length} > ${PERSIST_CHUNK_SIZE}`,
      );
    }
    const now = Date.now();
    for (const email of contacts) {
      await ctx.db.insert("wavePickedContacts", {
        runId,
        normalizedEmail: email,
        status: "pending",
      });
    }
    // Bump updatedAt so the in-flight guard's lastActivityAt fallback sees fresh activity.
    const run = await ctx.db
      .query("waveRuns")
      .withIndex("by_runId", (q) => q.eq("runId", runId))
      .unique();
    if (run) await ctx.db.patch(run._id, { updatedAt: now });
    return { inserted: contacts.length };

View on GitHub (pinned to ffec79ac33)

Solutions

  1. Ensure the caller slices contacts into chunks of at most PERSIST_CHUNK_SIZE (500): `for (let i = 0; i < picked.length; i += PERSIST_CHUNK_SIZE) { const chunk = picked.slice(i, i + PERSIST_CHUNK_SIZE); await runMutation(..., {contacts: chunk}); }`.
  2. If you intentionally changed PERSIST_CHUNK_SIZE, update every call site that slices by it.
  3. Never call _persistPickedBatch with an unsliced array from a new action.

Example fix

// before — caller passes unsliced batch
await ctx.runMutation(internal.broadcast.waveRuns._persistPickedBatch, { runId, contacts: picked });
// after — caller slices into 500-row chunks
const PERSIST_CHUNK_SIZE = 500;
for (let i = 0; i < picked.length; i += PERSIST_CHUNK_SIZE) {
  const chunk = picked.slice(i, i + PERSIST_CHUNK_SIZE);
  await ctx.runMutation(internal.broadcast.waveRuns._persistPickedBatch, { runId, contacts: chunk });
}
Defensive patterns

Strategy: validation

Validate before calling

const PERSIST_CHUNK_SIZE = 500;
function assertChunkSize(contacts: string[]): void {
  if (contacts.length > PERSIST_CHUNK_SIZE) {
    throw new Error(`Batch too large: ${contacts.length} > ${PERSIST_CHUNK_SIZE}. Slice the caller's array.`);
  }
}
// slice before calling: for (let i = 0; i < picked.length; i += PERSIST_CHUNK_SIZE) { ... }

Prevention

When it happens

Trigger: pickWaveAction calls runMutation('_persistPickedBatch', {contacts}) with an array longer than 500 because the slicing loop (waveRuns.ts:869-870) was modified or bypassed. A different caller invokes _persistPickedBatch directly with an unsliced batch.

Common situations: A developer changes PERSIST_CHUNK_SIZE to a smaller number but forgets to update the slicing loop, or vice versa. A new code path calls _persistPickedBatch without slicing. A test passes a large fixture array directly.

Related errors


AI-assisted analysis of koala73/worldmonitor@ffec79ac33 (2026-08-12). Data as JSON: /api/errors/f9fad5ed9c5b49da. Report an issue: GitHub.