JuliusBrussee/caveman · error · Error

result.error?.message ?? `sync import failed

Error message

result.error?.message ?? `sync import failed (${response.status})`

What it means

Thrown when the sync import POST completes but the server does not report a completed import: either `response.ok` is false or the parsed body's `status` is not "completed". If the server supplied `error.message`, that message is surfaced instead of the generic HTTP-status fallback.

Solutions

  1. Read the surfaced `error.message` for the server-side rejection reason.
  2. For 401 responses, re-login (`caveman login`) and retry the sync.
  3. Upgrade/align CLI and gateway versions so the import payload and status contract match.
  4. The sync watermark is only advanced after success, so simply retrying after fixing the cause re-sends the same rows safely.
Defensive patterns

Strategy: retry

Validate before calling

// Confirm auth and payload shape before syncing
const res = await fetch(`${baseURL}/healthz`);
if (!res.ok) throw new Error("gateway unhealthy — sync would fail");

Type guard

function isCompletedImport(v: unknown): v is { status: "completed" } {
  return (v as { status?: unknown })?.status === "completed";
}

Try / catch

try {
  await syncImport();
} catch (e) {
  console.error(`Sync import failed: ${e.message}`); // server error.message takes precedence
  // watermark is not advanced on failure, so a plain retry is safe
  await sleep(5000);
  await syncImport();
}

Prevention

When it happens

Trigger: `caveman sync` when the import endpoint rejects the batch (validation, auth, 5xx) or returns a body with status other than "completed" (e.g. "partial", "failed").

Common situations: Spend-store rows the server refuses (schema drift between CLI and gateway); expired access token (401); gateway returning partial-completed statuses; proxy interference mangling the response.

Related errors


AI-assisted analysis of JuliusBrussee/caveman@3ee70a1026 (2026-09-20). Data as JSON: /api/errors/641c55ced390796e. Report an issue: GitHub.

Appendix: source

Thrown at packages/cli/src/index.ts:10375

    const ts = typeof r.ts === "string" ? r.ts : "";
    // The proxy writes ClickHouse-layout UTC timestamps ("YYYY-MM-DD HH:MM:SS.mmm").
    const ms = Date.parse(ts.includes("T") ? ts : ts.replace(" ", "T") + "Z");
    if (!Number.isFinite(ms)) return [];
    return directiveInstalls.filter((d) => ms >= d.installedAtMs).map((d) => d.id);
  };
  const body = rows.map((r) => syncRequestSpan(r, rowDirectives(r))).join("\n") + "\n";
  const response = await fetch(`${cfg.baseURL}/api/v1/imports?format=caveman-jsonl`, {
    method: "POST",
    headers: {
      authorization: `Bearer ${cfg.token}`,
      "content-type": "application/octet-stream",
      "x-cave-csrf": "cli",
    },
    body,
  });
  const result = (await response.json().catch(() => ({}))) as { status?: string; error?: { message?: string } };
  if (!response.ok || result.status !== "completed") {
    throw new Error(result.error?.message ?? `sync import failed (${response.status})`);
  }

  const num = (v: unknown) => (typeof v === "bigint" ? Number(v) : typeof v === "number" && Number.isFinite(v) ? v : 0);
  const maxId = rows.reduce((m, r) => Math.max(m, num(r.id)), since);
  writeSyncWatermark(key, maxId);
  const tokensSaved = rows.reduce((sum, r) => sum + Math.max(0, num(r.compression_tokens_before) - num(r.compression_tokens_after)), 0);
  const tokenBases = new Set(rows.map((r) => (typeof r.compression_token_count_basis === "string" ? r.compression_token_count_basis : "")).filter(Boolean));
  const tokenCountBasis = tokenBases.size === 0 ? "unavailable" : tokenBases.size === 1 ? [...tokenBases][0] ?? "unavailable" : "mixed";
  const cachedInputTokens = rows.reduce((sum, r) => sum + Math.max(0, num(r.cached_input_tokens)), 0);
  const cacheCreationInputTokens = rows.reduce((sum, r) => sum + Math.max(0, num(r.cache_creation_input_tokens)), 0);
  const headlineCompressionRefused = cacheCreationInputTokens > cachedInputTokens;
  const savingsUSD = rows.reduce((sum, r) => sum + num(r.savings_usd), 0);
  return {
    kind: "synced",
    spans: rows.length,
    tokensSaved,
    tokenCountBasis,
    cachedInputTokens,

View on GitHub (pinned to 3ee70a1026)