koala73/worldmonitor · error · ConvexError

COMPANY_MONITORING_ADMISSION_EVIDENCE_STALE

COMPANY_MONITORING_ADMISSION_EVIDENCE_STALE

Error message

COMPANY_MONITORING_ADMISSION_EVIDENCE_STALE

What it means

Thrown by referencedEvidence() (convex/companyMonitoring/admission.ts:116) when the digest recomputed from the live evidence rows (selectionPolicyVersion, referenceCount, referencesTruncated, shaped evidence) does not equal candidate.evidenceSnapshotDigest. It is an optimistic-concurrency fence: the evidence behind the candidate changed after the snapshot was taken, so the classification inputs are stale.

Solutions

  1. Discard the stale attempt and restart the flow at evidence selection: build a fresh candidate and classify the new evidenceRevision
  2. Always read expectedEvidenceRevision from the current candidate doc immediately before submitting, not from a cached earlier read
  3. Keep model latency plus retry budget inside the evidence stability window so snapshots do not age out
  4. If it fires without any concurrent writes, audit for digest-input drift: evidenceShape or the digest inputs changed between deploy versions

Example fix

// worker: treat stale evidence like an OCC conflict and re-select
// before
const result = await ctx.runMutation(internal.companyMonitoring.admission.recordClassification, args);

// after
for (let attempt = 0; attempt < 3; attempt++) {
  try {
    const result = await ctx.runMutation(internal.companyMonitoring.admission.recordClassification, args);
    break;
  } catch (err) {
    if (err instanceof ConvexError && err.data === "COMPANY_MONITORING_ADMISSION_EVIDENCE_STALE" && attempt < 2) {
      const fresh = await reloadCandidateAndEvidence(); // re-select, new revision + runId
      args = { ...args, ...fresh };
      continue;
    }
    throw err;
  }
}
Defensive patterns

Strategy: retry

Validate before calling

// Cheap pre-check: confirm the candidate revision you hold is still current before the model call.
const candidate = await loadCandidate(ownerAccountId, companyId, occurrenceDedupeKey);
if (!candidate || candidate.evidenceRevision !== args.expectedEvidenceRevision) {
  return restartFromSelection(); // revision moved; a submission would be stale
}

Try / catch

for (let attempt = 0; attempt < 3; attempt++) {
  try {
    return await ctx.runMutation(internal.companyMonitoring.admission.recordClassification, args);
  } catch (err) {
    if (err instanceof ConvexError && err.data === "COMPANY_MONITORING_ADMISSION_EVIDENCE_STALE" && attempt < 2) {
      args = await rebuildCandidateAndArgs(args); // re-select evidence, new revision + runId
      continue;
    }
    throw err;
  }
}

Prevention

When it happens

Trigger: Evidence rows were inserted, patched, or expired between candidate creation and classification submission; referenceCount/referencesTruncated edited on the candidate without recomputing the digest; selection policy version changed under an old candidate; concurrent workers classifying the same occurrence after a re-scan bumped the evidence revision.

Common situations: A second scan completing while a slow model call is in flight; manual edits to evidence or candidate docs in the dashboard; redeploying with a new selection policy while old candidates are pending; retrying a classification run after the evidence window rolled over.

Related errors


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

Appendix: source

Thrown at convex/companyMonitoring/admission.ts:116

          row.independence !== "first_party" ||
          (row.provider === "exa" && row.matchedClaimIds.length === 0)
        ))
    ) {
      throw new ConvexError("COMPANY_MONITORING_ADMISSION_EVIDENCE_INVALID");
    }
    if (!row.queryVersion || !ADMISSION_ID.test(row.queryVersion)) {
      throw new ConvexError("COMPANY_MONITORING_ADMISSION_QUERY_VERSION_INVALID");
    }
  }
  const shaped = evidence.map(evidenceShape);
  const evidenceSnapshotDigest = await candidateEvidenceSnapshotDigest({
    selectionPolicyVersion: candidate.selectionPolicyVersion,
    referenceCount: candidate.referenceCount,
    referencesTruncated: candidate.referencesTruncated,
    evidence: shaped,
  });
  if (candidate.evidenceSnapshotDigest !== evidenceSnapshotDigest) {
    throw new ConvexError("COMPANY_MONITORING_ADMISSION_EVIDENCE_STALE");
  }
  return shaped;
}

function decisionVersions(candidate: Doc<"companyMonitoringCandidates">, modelVersion: string) {
  return {
    classificationSchemaVersion: COMPANY_MONITORING_CLASSIFICATION_SCHEMA_VERSION,
    admissionPolicyVersion: COMPANY_MONITORING_ADMISSION_POLICY_VERSION,
    sourcePolicyVersion: COMPANY_MONITORING_SOURCE_POLICY_VERSION,
    retryPolicyVersion: COMPANY_MONITORING_RETRY_POLICY.version,
    evidenceSelectionPolicyVersion: candidate.selectionPolicyVersion,
    modelVersion,
  };
}

async function appendSystemDecision(
  ctx: MutationCtx,
  candidate: Doc<"companyMonitoringCandidates">,

View on GitHub (pinned to eeab0a219f)