koala73/worldmonitor · error · ConvexError

COMPANY_MONITORING_EVIDENCE_EXPANSION_INVALID

Error message

COMPANY_MONITORING_EVIDENCE_EXPANSION_INVALID

What it means

Thrown by ingestCompanyEvidenceForCompanyIds when the normalized (expanded) evidence list exceeds MAX_EXPANDED_EVIDENCE_ROWS (625, i.e. 25 companies * 25 rows). normalizeCompanyEvidence can multiply the input because a single provider row may be routed to multiple subject companies; this guard rejects an over-broad expansion before any write so the receipt stays atomic within the scheduler budget.

Source

Thrown at convex/companyMonitoring/evidence.ts:381

  if (input.evidence.length === 0 || input.evidence.length > MAX_INGESTION_ROWS) {
    throw new ConvexError("COMPANY_MONITORING_EVIDENCE_BATCH_INVALID");
  }
  const account = await ctx.db
    .query("companyMonitoringAccounts")
    .withIndex("by_logicalAccountId", (q) => q.eq("logicalAccountId", input.ownerAccountId))
    .unique();
  if (!account || account.lifecycle !== "entitled" || account.terminalReason) {
    throw new ConvexError("COMPANY_MONITORING_ACCOUNT_INACTIVE");
  }
  const subjects = await canonicalSubjects(ctx, input.ownerAccountId, input.companyIds);
  const normalized = await normalizeCompanyEvidence({
    ownerAccountId: input.ownerAccountId,
    subjects,
    evidence: input.evidence,
    now: Date.now(),
  });
  if (normalized.evidence.length > MAX_EXPANDED_EVIDENCE_ROWS) {
    throw new ConvexError("COMPANY_MONITORING_EVIDENCE_EXPANSION_INVALID");
  }
  const now = Date.now();
  const affected = new Map<string, Map<string, "evidence_unavailable" | undefined>>();
  const rememberOccurrence = (
    companyId: string,
    occurrenceDedupeKey: string,
    fallbackLossReason?: "evidence_unavailable",
  ) => {
    const occurrences = affected.get(companyId) ?? new Map();
    if (!occurrences.has(occurrenceDedupeKey) || fallbackLossReason === undefined) {
      occurrences.set(occurrenceDedupeKey, fallbackLossReason);
    }
    affected.set(companyId, occurrences);
  };
  for (const evidence of normalized.evidence) {
    const existing = await ctx.db
      .query("companyMonitoringEvidence")
      .withIndex("by_account_company_locator", (q) =>

View on GitHub (pinned to ffec79ac33)

Solutions

  1. Tighten the provider query/claim matching so fewer rows route to each company.
  2. Split the ingestion so each call covers fewer subjects or fewer evidence rows.
  3. If the expansion is legitimately large, raise MAX_EXPANDED_EVIDENCE_ROWS only after confirming the scheduler budget holds.

Example fix

// before
await ingest({ ownerAccountId, companyIds: allSubjects, evidence });

// after
// split subjects so expansion stays under the cap
for (const subjectBatch of chunk(allSubjects, 5)) {
  await ingest({ ownerAccountId, companyIds: subjectBatch, evidence });
}
Defensive patterns

Strategy: validation

Validate before calling

// estimate worst-case expansion = subjects * rows; keep under 625
const MAX_EXPANSION = 625;
const estimated = companyIds.length * evidence.length;
if (estimated > MAX_EXPANSION) {
  // split ingestion
}

Type guard

function expansionSafe(subjects: number, rows: number, cap = 625): boolean {
  return subjects * rows <= cap;
}

Prevention

When it happens

Trigger: Provider evidence that, after normalization and per-company routing, expands to more than 625 rows in a single ingestion call.

Common situations: A provider returned rows touching the full cohort (25 companies) with 25+ routed matches each; a query that over-matches claims; the cohort size or claim set grew since the cap was set.

Related errors


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