koala73/worldmonitor · error · ConvexError

COMPANY_MONITORING_EVIDENCE_SUBJECTS_INVALID

Error message

COMPANY_MONITORING_EVIDENCE_SUBJECTS_INVALID

What it means

Thrown by canonicalSubjects in evidence.ts when the requestedCompanyIds array is empty or exceeds COMPANY_MONITORING_LIMITS.maxCompaniesPerAccount. This is the first of three guards in canonicalSubjects that share the same error code; this one bounds the size of the evidence-ingestion subject set before any deduplication or lookup.

Source

Thrown at convex/companyMonitoring/evidence.ts:47

// companies. Reject a wider internal expansion before the first write so the
// receipt remains atomic and the mutation cannot exceed its scheduler budget.
const MAX_EXPANDED_EVIDENCE_ROWS = 25 * 25;
type EvidenceDoc = Doc<"companyMonitoringEvidence">;

function nextUpdatedAt(row: { updatedAt: number } | null | undefined, now: number) {
  return Math.max(now, (row?.updatedAt ?? now - 1) + 1);
}

async function canonicalSubjects(
  ctx: MutationCtx,
  ownerAccountId: string,
  requestedCompanyIds: string[],
) {
  if (
    requestedCompanyIds.length === 0 ||
    requestedCompanyIds.length > COMPANY_MONITORING_LIMITS.maxCompaniesPerAccount
  ) {
    throw new ConvexError("COMPANY_MONITORING_EVIDENCE_SUBJECTS_INVALID");
  }
  const subjectIds = [...new Set(requestedCompanyIds)].sort();
  if (subjectIds.length !== requestedCompanyIds.length) {
    throw new ConvexError("COMPANY_MONITORING_EVIDENCE_SUBJECTS_INVALID");
  }
  const canonical = await Promise.all(subjectIds.map(async (companyId): Promise<EvidenceSubject> => {
    const [company, claims] = await Promise.all([
      ctx.db
        .query("companyMonitoringCompanies")
        .withIndex("by_account_companyId", (q) =>
          q.eq("ownerAccountId", ownerAccountId).eq("companyId", companyId),
        )
        .unique(),
      ctx.db
        .query("companyMonitoringClaims")
        .withIndex("by_account_company", (q) =>
          q.eq("ownerAccountId", ownerAccountId).eq("companyId", companyId),
        )

View on GitHub (pinned to ffec79ac33)

Solutions

  1. Ensure the companyIds list is non-empty and at most COMPANY_MONITORING_LIMITS.maxCompaniesPerAccount before calling ingest.
  2. If the provider returned more subjects than the cap, split the ingestion into multiple calls each within the limit, or drop out-of-cap subjects deliberately.
  3. Re-read the current maxCompaniesPerAccount value from the shared contract; do not hard-code it.

Example fix

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

// after
const CAP = COMPANY_MONITORING_LIMITS.maxCompaniesPerAccount;
const safeIds = allIds.slice(0, CAP);
if (safeIds.length === 0) throw new Error("no subjects");
await ingest({ ownerAccountId, companyIds: safeIds, evidence });
Defensive patterns

Strategy: validation

Validate before calling

const CAP = COMPANY_MONITORING_LIMITS.maxCompaniesPerAccount;
if (companyIds.length === 0 || companyIds.length > CAP) {
  throw new Error("subjects out of range");
}

Type guard

function subjectsInRange(ids: string[], cap: number): boolean {
  return ids.length > 0 && ids.length <= cap;
}

Prevention

When it happens

Trigger: Calling ingestCompanyEvidenceForCompanyIds (or the ingestEvidenceForTest mutation) with an empty companyIds array or one longer than COMPANY_MONITORING_LIMITS.maxCompaniesPerAccount.

Common situations: Worker code forwarding an unbatched provider result that touched more companies than the account cap allows; a bug producing an empty subject list; exceeding the per-account company cap because the limit was lowered after companies were added.

Related errors


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