{"record":{"id":"ea57a23e2a5c2bd0","repo":"koala73/worldmonitor","slug":"company-monitoring-admission-evidence-stale","errorCode":"COMPANY_MONITORING_ADMISSION_EVIDENCE_STALE","errorMessage":"COMPANY_MONITORING_ADMISSION_EVIDENCE_STALE","messagePattern":"COMPANY_MONITORING_ADMISSION_EVIDENCE_STALE","errorType":"validation","errorClass":"ConvexError","httpStatus":null,"severity":"error","filePath":"convex/companyMonitoring/admission.ts","lineNumber":116,"sourceCode":"          row.independence !== \"first_party\" ||\n          (row.provider === \"exa\" && row.matchedClaimIds.length === 0)\n        ))\n    ) {\n      throw new ConvexError(\"COMPANY_MONITORING_ADMISSION_EVIDENCE_INVALID\");\n    }\n    if (!row.queryVersion || !ADMISSION_ID.test(row.queryVersion)) {\n      throw new ConvexError(\"COMPANY_MONITORING_ADMISSION_QUERY_VERSION_INVALID\");\n    }\n  }\n  const shaped = evidence.map(evidenceShape);\n  const evidenceSnapshotDigest = await candidateEvidenceSnapshotDigest({\n    selectionPolicyVersion: candidate.selectionPolicyVersion,\n    referenceCount: candidate.referenceCount,\n    referencesTruncated: candidate.referencesTruncated,\n    evidence: shaped,\n  });\n  if (candidate.evidenceSnapshotDigest !== evidenceSnapshotDigest) {\n    throw new ConvexError(\"COMPANY_MONITORING_ADMISSION_EVIDENCE_STALE\");\n  }\n  return shaped;\n}\n\nfunction decisionVersions(candidate: Doc<\"companyMonitoringCandidates\">, modelVersion: string) {\n  return {\n    classificationSchemaVersion: COMPANY_MONITORING_CLASSIFICATION_SCHEMA_VERSION,\n    admissionPolicyVersion: COMPANY_MONITORING_ADMISSION_POLICY_VERSION,\n    sourcePolicyVersion: COMPANY_MONITORING_SOURCE_POLICY_VERSION,\n    retryPolicyVersion: COMPANY_MONITORING_RETRY_POLICY.version,\n    evidenceSelectionPolicyVersion: candidate.selectionPolicyVersion,\n    modelVersion,\n  };\n}\n\nasync function appendSystemDecision(\n  ctx: MutationCtx,\n  candidate: Doc<\"companyMonitoringCandidates\">,","sourceCodeStart":98,"sourceCodeEnd":134,"githubUrl":"https://github.com/koala73/worldmonitor/blob/eeab0a219fce0f02a00603b532dbae9041b934ac/convex/companyMonitoring/admission.ts#L98-L134","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// worker: treat stale evidence like an OCC conflict and re-select\n// before\nconst result = await ctx.runMutation(internal.companyMonitoring.admission.recordClassification, args);\n\n// after\nfor (let attempt = 0; attempt < 3; attempt++) {\n  try {\n    const result = await ctx.runMutation(internal.companyMonitoring.admission.recordClassification, args);\n    break;\n  } catch (err) {\n    if (err instanceof ConvexError && err.data === \"COMPANY_MONITORING_ADMISSION_EVIDENCE_STALE\" && attempt < 2) {\n      const fresh = await reloadCandidateAndEvidence(); // re-select, new revision + runId\n      args = { ...args, ...fresh };\n      continue;\n    }\n    throw err;\n  }\n}","handlingStrategy":"retry","validationCode":"// Cheap pre-check: confirm the candidate revision you hold is still current before the model call.\nconst candidate = await loadCandidate(ownerAccountId, companyId, occurrenceDedupeKey);\nif (!candidate || candidate.evidenceRevision !== args.expectedEvidenceRevision) {\n  return restartFromSelection(); // revision moved; a submission would be stale\n}","typeGuard":null,"tryCatchPattern":"for (let attempt = 0; attempt < 3; attempt++) {\n  try {\n    return await ctx.runMutation(internal.companyMonitoring.admission.recordClassification, args);\n  } catch (err) {\n    if (err instanceof ConvexError && err.data === \"COMPANY_MONITORING_ADMISSION_EVIDENCE_STALE\" && attempt < 2) {\n      args = await rebuildCandidateAndArgs(args); // re-select evidence, new revision + runId\n      continue;\n    }\n    throw err;\n  }\n}","preventionTips":["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"],"tags":["convex","optimistic-concurrency","digest","evidence","retry"],"backgroundTag":"optimistic-concurrency-conflict","analyzedSha":"eeab0a219fce0f02a00603b532dbae9041b934ac","analyzedAt":"2026-08-21T16:51:25.751Z","contentChangedAt":"2026-08-21T16:51:25.751Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}