koala73/worldmonitor · error · ConvexError

COMPANY_MONITORING_ _INVALID

Error message

COMPANY_MONITORING_${field}_INVALID

What it means

Thrown by admissionIdentifier() in the company-monitoring classification admission mutation (convex/companyMonitoring/admission.ts:28). Worker-supplied identifiers are interpolated into the message, so the actual codes are COMPANY_MONITORING_ADMISSION_WORKER_ID_INVALID, COMPANY_MONITORING_ADMISSION_LEASE_INVALID, and COMPANY_MONITORING_CLASSIFICATION_RUN_ID_INVALID. An id is rejected unless it matches /^[A-Za-z0-9._:-]{1,128}$/ (non-empty, max 128 chars, restricted alphabet).

Solutions

  1. Sanitize the id right before the call: trim whitespace and re-encode into the allowed alphabet (e.g. base64url with + -> -, / -> _, padding stripped)
  2. Generate workerId/classificationRunId from the same helper the server uses (randomFence in convex/companyMonitoring/_shared.ts) so alphabets can never diverge
  3. If an upstream id is longer than 128 chars or uses a wider alphabet, hash it (sha256 hex) before sending
  4. Add a unit test asserting your worker's id generator output always matches /^[A-Za-z0-9._:-]{1,128}$/

Example fix

// before
await ctx.runMutation(internal.companyMonitoring.admission.recordClassification, {
  workerId: `worker/${process.env.POD_NAME}`,
  classificationRunId: `run-${crypto.randomUUID()}`,
  ...
});

// after
const ADMISSION_ID = /^[A-Za-z0-9._:-]{1,128}$/;
function toAdmissionId(value: string): string {
  const encoded = value.trim().replace(/[^A-Za-z0-9._:-]/g, "-").slice(0, 128);
  if (!ADMISSION_ID.test(encoded)) throw new Error(`unencodable id: ${value}`);
  return encoded;
}
await ctx.runMutation(internal.companyMonitoring.admission.recordClassification, {
  workerId: toAdmissionId(`worker-${process.env.POD_NAME}`),
  classificationRunId: toAdmissionId(`run-${crypto.randomUUID()}`),
  ...
});
Defensive patterns

Strategy: validation

Validate before calling

const ADMISSION_ID = /^[A-Za-z0-9._:-]{1,128}$/;
function toAdmissionId(value: string, field: string): string {
  const cleaned = value.trim().replace(/[^A-Za-z0-9._:-]/g, "-").slice(0, 128);
  if (!ADMISSION_ID.test(cleaned)) {
    throw new Error(`${field} cannot be encoded to a valid admission id: ${JSON.stringify(value)}`);
  }
  return cleaned;
}
// before the mutation:
const workerId = toAdmissionId(rawWorkerId, "workerId");
const leaseToken = toAdmissionId(rawLeaseToken, "leaseToken");
const classificationRunId = toAdmissionId(rawRunId, "classificationRunId");

Type guard

function isAdmissionIdentifier(value: unknown): value is string {
  return typeof value === "string" && /^[A-Za-z0-9._:-]{1,128}$/.test(value);
}

Try / catch

try {
  await ctx.runMutation(internal.companyMonitoring.admission.recordClassification, args);
} catch (err) {
  if (err instanceof ConvexError && /^COMPANY_MONITORING_.*_INVALID$/.test(String(err.data))) {
    logger.error("admission argument rejected", { code: err.data, args: redactIds(args) });
    return; // do not retry: fix the id generation first
  }
  throw err;
}

Prevention

When it happens

Trigger: Calling the internal admission mutation with a workerId, leaseToken, or classificationRunId that is empty, longer than 128 characters, or contains characters outside A-Z a-z 0-9 . _ : - . Concrete hits: a run id built as `run/${uuid}` (slash), a lease token copied from a log with a trailing newline, a workerId like "worker (pod-1)" with spaces, or an undefined value coerced to the string "undefined".

Common situations: Worker code composing ids from free-form template strings or external payloads; copying lease tokens out of logs/JSON with whitespace; a dependency upgrade changing id alphabets (e.g. base64 ids containing + or =); concatenating fields until the id exceeds 128 chars.

Related errors


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

Appendix: source

Thrown at convex/companyMonitoring/admission.ts:28

  COMPANY_MONITORING_RETRY_POLICY,
  COMPANY_MONITORING_SOURCE_POLICY_VERSION,
  evaluateCompanyMonitoringClassifierTransportFailure,
  evaluateCompanyMonitoringClassification,
} from "../../scripts/lib/company-monitoring-classification.mjs";
import { fingerprint, randomFence } from "./_shared";
import { assertValidCandidateState } from "./validators";
import {
  companyMonitoringCandidateEvidenceSnapshotDigest as candidateEvidenceSnapshotDigest,
  companyMonitoringEvidenceShape as evidenceShape,
} from "./admissionSnapshot";

const ADMISSION_LEASE_MS = 5 * 60 * 1000;
const ADMISSION_ID = /^[A-Za-z0-9._:-]{1,128}$/;
const ADMISSION_MODEL_VERSION = /^[^\u0000-\u001f\u007f]{1,200}$/u;

function admissionIdentifier(value: string, field: string) {
  if (!ADMISSION_ID.test(value)) {
    throw new ConvexError(`COMPANY_MONITORING_${field}_INVALID`);
  }
  return value;
}

function admissionModelVersion(value: string) {
  if (
    value !== value.trim() ||
    !ADMISSION_MODEL_VERSION.test(value)
  ) {
    throw new ConvexError("COMPANY_MONITORING_MODEL_VERSION_INVALID");
  }
  return value;
}

function canonicalValue(value: unknown): unknown {
  if (Array.isArray(value)) return value.map(canonicalValue);
  if (value && typeof value === "object") {
    const row = value as Record<string, unknown>;

View on GitHub (pinned to eeab0a219f)