koala73/worldmonitor · error · ConvexError

COMPANY_MONITORING_ACCOUNT_INACTIVE

Error message

COMPANY_MONITORING_ACCOUNT_INACTIVE

What it means

Thrown by scheduleAccountWorkHandler when the owner account is missing, its lifecycle is not 'entitled', or it has a terminalReason. Scan scheduling is only permitted for fully-entitled, non-terminal accounts, so this guard stops new work from being queued for accounts that can no longer consume it.

Source

Thrown at convex/companyMonitoring/orchestration.ts:285

  await ctx.db.patch(account._id, {
    nextExaScanDueAt,
    nextXScanDueAt,
    updatedAt: Date.now(),
  });
}

async function scheduleAccountWorkHandler(
  ctx: MutationCtx,
  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),
        )

View on GitHub (pinned to ffec79ac33)

Solutions

  1. Before scheduling, confirm the account is lifecycle 'entitled' with no terminalReason.
  2. Ensure account teardown cancels queued scheduler callbacks (runAfter) for that account.
  3. Treat this error as terminal for the scheduling path: do not retry blindly.

Example fix

// before
await queueAccountSourceWork(ctx, ownerAccountId, source, companyIds);

// after
const account = await fetchAccount(ownerAccountId);
if (!account || account.lifecycle !== "entitled" || account.terminalReason) {
  return { status: "inactive" };
}
await queueAccountSourceWork(ctx, ownerAccountId, source, companyIds);
Defensive patterns

Strategy: validation

Validate before calling

const account = await fetchAccount(ownerAccountId);
if (!account || account.lifecycle !== "entitled" || account.terminalReason) {
  return { status: "inactive" };
}

Type guard

function isSchedulableAccount(a: { lifecycle: string; terminalReason?: string } | null): boolean {
  return Boolean(a) && a.lifecycle === "entitled" && !a.terminalReason;
}

Prevention

When it happens

Trigger: Calling ensureAccountWork/scheduleAccountWork for an ownerAccountId whose account row is gone, suspended/cancelled (lifecycle != 'entitled'), or marked terminal.

Common situations: A queued follow-up mutation firing after the account was suspended; account teardown did not drain pending scheduler callbacks; billing lapse marking the account terminal.

Related errors


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