JuliusBrussee/caveman · warning

${key} must be a string

Error message

${key} must be a string

What it means

In validateTraceSearch(), the optional date-window arguments `from` and `to` must be strings when present. Numbers (Unix timestamps as ints), Date objects stringified by a client into something else, null, or arrays throw with the key named. There is no format enforcement at this layer beyond the string type — the backend interprets the values.

Source

Thrown at packages/cli/src/agent-mcp.ts:511

    if (typeof monitor.id !== "string" || monitor.id.trim() === "") throw new Error("filter monitor.id is required");
    if (typeof monitor.verdict !== "string" || !["pass", "fail", "error"].includes(monitor.verdict)) {
      throw new Error("filter monitor.verdict must be pass, fail, or error");
    }
  }
}

function optionalEnum(args: JSONObject, key: string, values: string[]): void {
  const value = args[key];
  if (value !== undefined && (typeof value !== "string" || !values.includes(value))) {
    throw new Error(`${key} must be one of ${values.join(", ")}`);
  }
}

function validateTraceSearch(args: JSONObject): void {
  validateTraceFilters(args);
  for (const key of ["from", "to"]) {
    const value = args[key];
    if (value !== undefined && typeof value !== "string") throw new Error(`${key} must be a string`);
  }
  optionalEnum(args, "date_field", ["occurred", "updated"]);
  optionalEnum(args, "group_by", ["session", "workflow", "model", "member"]);
  const pageSize = args.page_size;
  if (pageSize !== undefined && (!Number.isSafeInteger(pageSize) || (pageSize as number) < 1 || (pageSize as number) > 500)) {
    throw new Error("page_size must be an integer from 1 to 500");
  }
  const sort = args.sort;
  if (sort !== undefined) {
    if (!sort || typeof sort !== "object" || Array.isArray(sort)) throw new Error("sort must be an object");
    const unknown = Object.keys(sort).filter((key) => key !== "by" && key !== "dir");
    if (unknown.length > 0) throw new Error(`sort has unknown key(s): ${unknown.sort().join(", ")}`);
    optionalEnum(sort, "by", ["timestamp", "total_cost_usd", "latency_ms", "total_tokens"]);
    optionalEnum(sort, "dir", ["asc", "desc"]);
  }
  const cursor = args.cursor;
  if (cursor !== undefined) {
    if (!cursor || typeof cursor !== "object" || Array.isArray(cursor)) throw new Error("cursor must be an object");

View on GitHub (pinned to 27d5a3981a)

Solutions

  1. Send ISO-8601 strings: from: "2026-08-01T00:00:00Z".
  2. Omit from/to entirely for an unbounded window — never null.
  3. Convert epochs explicitly: new Date(ms).toISOString().

Example fix

// before
{ from: 1754006400, to: null }

// after
{ from: new Date(1754006400000).toISOString() }
Defensive patterns

Strategy: type-guard

Validate before calling

for (const k of ["from", "to"]) {
  const v = args[k];
  if (v !== undefined && typeof v !== "string") throw new Error(`${k} must be a string`);
}

Type guard

function isDateString(v: unknown): v is string {
  return typeof v === "string" && !Number.isNaN(Date.parse(v));
}

Prevention

When it happens

Trigger: from: 1700000000 (epoch seconds as number); from: null used to mean "no bound"; from: ["2024-01-01"] wrapped in an array by a generic serializer.

Common situations: Programmatic callers converting dates to epoch ints; optional-field handling that sends null instead of omitting; a model emitting a number where an ISO string was expected.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@27d5a3981a (2026-08-15). Data as JSON: /api/errors/2c3dc1701ee7c69b. Report an issue: GitHub.