{"record":{"id":"b6e90e6d30e508d1","repo":"JuliusBrussee/caveman","slug":"recent-stats-was-not-a-json-array","errorCode":null,"errorMessage":"recent stats was not a JSON array","messagePattern":"recent stats was not a JSON array","errorType":"exception","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"packages/cli/src/index.ts","lineNumber":15124,"sourceCode":"  const seconds = Math.ceil(timeoutMs / 1000);\n  console.error(`no request seen through the proxy in ${seconds}s`);\n  console.error(\"hint: check base URL points at the Caveman gateway and run `caveman start`\");\n  process.exit(1);\n}\n\nfunction verifyTimeoutMs(values: string[]): number {\n  const ms = Number(flagFrom(values, \"--timeout-ms\", \"\"));\n  if (Number.isFinite(ms) && ms > 0) return ms;\n  const seconds = Number(flagFrom(values, \"--timeout\", \"60\"));\n  if (Number.isFinite(seconds) && seconds > 0) return Math.round(seconds * 1000);\n  return 60_000;\n}\n\nfunction readRecentProxyRows(bin: string, recent: string): RecentProxyRow[] {\n  try {\n    const out = execFileSync(bin, [\"stats\", \"--recent\", recent, \"--json\"], { encoding: \"utf8\", env: process.env });\n    const parsed = JSON.parse(out);\n    if (!Array.isArray(parsed)) throw new Error(\"recent stats was not a JSON array\");\n    return parsed.map((row) => ({\n      ts: String(row?.ts ?? \"\"),\n      agent_slug: String(row?.agent_slug ?? \"\"),\n      provider: String(row?.provider ?? \"\"),\n      model: String(row?.model ?? \"\"),\n      endpoint: String(row?.endpoint ?? \"\"),\n      input_tokens: Number(row?.input_tokens ?? 0),\n      output_tokens: Number(row?.output_tokens ?? 0),\n      basis: String(row?.basis ?? \"inferred\"),\n    }));\n  } catch (error) {\n    const e = error as { stdout?: string; stderr?: string; status?: number; message?: string };\n    if (e.stdout) process.stdout.write(e.stdout);\n    if (e.stderr) process.stderr.write(e.stderr);\n    if (!e.stdout && !e.stderr) console.error(`failed to read recent proxy requests via ${bin}: ${e.message ?? error}`);\n    process.exit(e.status ?? 1);\n  }\n}","sourceCodeStart":15106,"sourceCodeEnd":15142,"githubUrl":"https://github.com/JuliusBrussee/caveman/blob/5184b3d11ac6a1acb7d44b9bfaa31698157cff97/packages/cli/src/index.ts#L15106-L15142","documentation":"readRecentProxyRows executes `<proxy-bin> stats --recent N --json` via execFileSync and requires the parsed stdout to be a top-level JSON array of proxy rows. If the proxy prints an error object, plain text, or a non-array document, the Array.isArray check fails and this error is thrown. The catch block around it additionally surfaces stdout/stderr/status for diagnosis.","triggerScenarios":"Proxy binary older than the CLI and lacking `stats --json` support; proxy emitting an error object instead of a row list; PATH (or the proxy env override) resolving a different binary with the same name; proxy printing null or a wrapper banner before the array.","commonSituations":"Mixed-version installs after a partial upgrade; wrapper scripts shadowing the real proxy binary; running stats on a freshly provisioned proxy before any rows exist (expected output is `[]`, anything else indicates skew).","solutions":["Run `<proxy-bin> stats --recent 5 --json` by hand and confirm it prints a JSON array","Upgrade the proxy binary to a version that supports `stats --json`","Make sure PATH / the proxy binary override points at the real caveman proxy","Treat any non-array output as a contract break between CLI and proxy versions, not as empty data"],"exampleFix":null,"handlingStrategy":"type-guard","validationCode":"// Probe the stats surface before relying on it.\nconst out = execFileSync(bin, ['stats', '--recent', '1', '--json'], { encoding: 'utf8' });\nif (!out.trim().startsWith('[')) throw new Error('proxy stats does not emit a JSON array — upgrade the proxy binary');","typeGuard":"const isRecentProxyRow = (r: any): r is RecentProxyRow =>\n  !!r && typeof r === 'object' &&\n  typeof r.ts === 'string' && typeof r.agent_slug === 'string' &&\n  Number.isFinite(r.input_tokens) && Number.isFinite(r.output_tokens);\nconst isRowArray = (v: unknown): v is RecentProxyRow[] => Array.isArray(v) && v.every(isRecentProxyRow);","tryCatchPattern":"try {\n  rows = readRecentProxyRows(bin, recent);\n} catch (e) {\n  if (String((e as Error).message).includes('not a JSON array')) {\n    // contract break with the proxy binary — check version, do not retry blindly\n  }\n  throw e;\n}","preventionTips":["Pin CLI and proxy to the same release so the stats contract holds","Compare `--version` output of both binaries before parsing stats","Treat any non-array stats output as version skew, never as empty data"],"tags":["cli","proxy","json-parsing","stats","version-skew"],"backgroundTag":"unexpected-response-format","analyzedSha":"5184b3d11ac6a1acb7d44b9bfaa31698157cff97","analyzedAt":"2026-08-18T03:14:35.516Z","contentChangedAt":"2026-08-18T03:14:35.516Z","schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}