koala73/worldmonitor · error · ConvexError

COMPANY_MONITORING_MODEL_VERSION_INVALID

COMPANY_MONITORING_MODEL_VERSION_INVALID

Error message

COMPANY_MONITORING_MODEL_VERSION_INVALID

What it means

Thrown by admissionModelVersion() (convex/companyMonitoring/admission.ts:38) for both the modelVersion and requestedModelVersion arguments of the classification admission mutation. A version string is rejected when it differs from its own trim() (leading/trailing whitespace) or fails /^[^\u0000-\u001f\u007f]{1,200}$/u — i.e. empty, longer than 200 characters, or containing control characters.

Solutions

  1. Trim the version and validate it against the same regex before invoking the mutation: value === value.trim() && /^[^\u0000-\u001f\u007f]{1,200}$/u.test(value)
  2. Fail fast at worker startup when the model version env var is missing or empty, so the mutation never sees ""
  3. Cap the version string to a short logical tag (e.g. "gpt-4o-2024-08-06") instead of full build metadata
  4. Add a preflight assertion in the worker config loader that runs the server regex on every deploy

Example fix

// before
const modelVersion = process.env.MODEL_VERSION; // may be "gpt-4o\n" or ""
await ctx.runMutation(internal.companyMonitoring.admission.recordClassification, { modelVersion, ... });

// after
const MODEL_VERSION_RE = /^[^\u0000-\u001f\u007f]{1,200}$/u;
function admissionModelVersion(value: string): string {
  const trimmed = value.trim();
  if (value !== trimmed || !MODEL_VERSION_RE.test(trimmed)) {
    throw new Error(`MODEL_VERSION invalid: ${JSON.stringify(value)}`);
  }
  return trimmed;
}
await ctx.runMutation(internal.companyMonitoring.admission.recordClassification, {
  modelVersion: admissionModelVersion(process.env.MODEL_VERSION ?? ""),
  ...
});
Defensive patterns

Strategy: validation

Validate before calling

const MODEL_VERSION_RE = /^[^\u0000-\u001f\u007f]{1,200}$/u;
function assertModelVersion(value: string): string {
  const trimmed = value.trim();
  if (value !== trimmed || !MODEL_VERSION_RE.test(trimmed)) {
    throw new Error(`model version invalid: ${JSON.stringify(value)}`);
  }
  return trimmed;
}
const modelVersion = assertModelVersion(process.env.MODEL_VERSION ?? "");
const requestedModelVersion = assertModelVersion(config.requestedModelVersion);

Type guard

function isAdmissionModelVersion(value: unknown): value is string {
  return (
    typeof value === "string" &&
    value === value.trim() &&
    /^[^\u0000-\u001f\u007f]{1,200}$/u.test(value)
  );
}

Try / catch

try {
  await ctx.runMutation(internal.companyMonitoring.admission.recordClassification, args);
} catch (err) {
  if (err instanceof ConvexError && err.data === "COMPANY_MONITORING_MODEL_VERSION_INVALID") {
    failFastAtStartup(`MODEL_VERSION malformed: ${JSON.stringify(args.modelVersion)}`);
  }
  throw err;
}

Prevention

When it happens

Trigger: Passing a model version read from an env var or config file that carries a trailing \n; passing "" because the env var was never set and the default is empty; a version string like "claude-3-5-sonnet\t20241022"; an extremely long version built from git describe plus build metadata exceeding 200 chars.

Common situations: MODEL_VERSION sourced from dotenv/CI secrets that silently include newlines; a deploy that changes version format (semver to full build fingerprint) pushing length past 200; copy-pasting a version from a release page with a non-breaking space or control character; upgrade scripts that leave the variable unset.

Related errors


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

Appendix: source

Thrown at convex/companyMonitoring/admission.ts:38

} 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>;
    return Object.fromEntries(
      Object.keys(row).sort().map((key) => [key, canonicalValue(row[key])]),
    );
  }
  return value;
}

function admissionTerminalReason(decision: string) {
  if (decision === "publish") return "admitted" as const;
  if (decision === "reject") return "rejected" as const;

View on GitHub (pinned to eeab0a219f)