koala73/worldmonitor · error · ConvexError

INVALID_COMPANY_MONITORING_COHORT

Error message

INVALID_COMPANY_MONITORING_COHORT

What it means

Thrown by scheduleAccountWorkHandler when the deduplicated requestedCompanyIds list is empty or longer than COMPANY_MONITORING_SCAN_COHORT_LIMIT (25). Scan cohorts are bounded so a single work item's obligations and provider query stay within lease and budget limits.

Source

Thrown at convex/companyMonitoring/orchestration.ts:294

  args: { ownerAccountId: string; source: Source; companyIds: string[] },
  mode: "strict" | "missing_only" = "strict",
) {
  const now = Date.now();
  const account = await ctx.db
    .query("companyMonitoringAccounts")
    .withIndex("by_logicalAccountId", (q) => q.eq("logicalAccountId", args.ownerAccountId))
    .unique();
  if (!account || account.lifecycle !== "entitled" || account.terminalReason) {
    throw new ConvexError("COMPANY_MONITORING_ACCOUNT_INACTIVE");
  }
  requireProviderClaimPolicy(account, args.source);

  const requestedCompanyIds = [...new Set(args.companyIds)].sort();
  if (
    requestedCompanyIds.length === 0 ||
    requestedCompanyIds.length > COMPANY_MONITORING_SCAN_COHORT_LIMIT
  ) {
    throw new ConvexError("INVALID_COMPANY_MONITORING_COHORT");
  }

  const rows = await Promise.all(requestedCompanyIds.map(async (companyId) => {
    const [company, obligation] = await Promise.all([
      ctx.db
        .query("companyMonitoringCompanies")
        .withIndex("by_account_companyId", (q) =>
          q.eq("ownerAccountId", args.ownerAccountId).eq("companyId", companyId),
        )
        .unique(),
      ctx.db
        .query("companyMonitoringScanObligations")
        .withIndex("by_account_company_source", (q) =>
          q
            .eq("ownerAccountId", args.ownerAccountId)
            .eq("companyId", companyId)
            .eq("source", args.source),
        )

View on GitHub (pinned to ffec79ac33)

Solutions

  1. Chunk company ids into cohorts of at most COMPANY_MONITORING_SCAN_COHORT_LIMIT (25) and schedule one work item per chunk.
  2. Skip scheduling when the list is empty.
  3. Read the limit from the exported constant rather than hard-coding 25.

Example fix

// before
await schedule({ ownerAccountId, source, companyIds: allIds });

// after
const LIMIT = 25;
for (let i = 0; i < allIds.length; i += LIMIT) {
  await schedule({ ownerAccountId, source, companyIds: allIds.slice(i, i + LIMIT) });
}
Defensive patterns

Strategy: validation

Validate before calling

const LIMIT = COMPANY_MONITORING_SCAN_COHORT_LIMIT;
const unique = [...new Set(companyIds)];
if (unique.length === 0 || unique.length > LIMIT) {
  throw new Error("cohort out of range");
}

Type guard

function cohortValid(ids: string[], limit = 25): boolean {
  const u = new Set(ids);
  return u.size > 0 && u.size <= limit;
}

Prevention

When it happens

Trigger: Calling account-work scheduling with no company ids or with more than 25 distinct company ids in one call.

Common situations: A 'scan all' action that does not chunk by cohort limit; an empty list from a filter that matched nothing; cohort limit lowered after code paths were written against a larger value.

Related errors


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