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
- Discard the stale attempt and restart the flow at evidence selection: build a fresh candidate and classify the new evidenceRevision
- Always read expectedEvidenceRevision from the current candidate doc immediately before submitting, not from a cached earlier read
- Keep model latency plus retry budget inside the evidence stability window so snapshots do not age out
- 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
- Read expectedEvidenceRevision from the candidate doc immediately before submitting
- Keep model latency inside the evidence stability window so snapshots do not age out
- After any deploy that changes evidenceShape or digest inputs, expect old candidates to fail — let them roll over
- Never retry a stale submission with the same run id and revision; rebuild first
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
- COMPANY_MONITORING_ADMISSION_EVIDENCE_INVALID
- COMPANY_MONITORING_ADMISSION_EVIDENCE_MISSING
- COMPANY_MONITORING_ADMISSION_QUERY_VERSION_INVALID
- COMPANY_MONITORING_CLASSIFICATION_REPLAY_CONFLICT
- digestHour must be an integer 0–23
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)