thedotmack/claude-mem · error

sync hub pull ${res.status}: ${body}

Error message

sync hub pull ${res.status}: ${body}

What it means

SyncClient's HTTP pull loop received a non-OK status from the sync hub's /changes endpoint; the error embeds the status code and the first 200 bytes of the body. The X-Sync-Mode header hint is processed before this throw, so poll-mode state is preserved across transient outages by design. This is the generic transport-level failure for hub pulls.

Source

Thrown at src/services/sync/SyncClient.ts:550

            // Plain short request — never a held connection (directive #4).
            signal: AbortSignal.timeout(Math.max(1, Math.min(this.requestTimeoutMs, remaining))),
          }
        );
        // Kill-switch mode hint (plan Phase 5 task 2), read BEFORE the
        // ok-check — the header rides error responses too. Asymmetric on
        // purpose: header PRESENCE means poll regardless of status, but
        // header ABSENCE only means "cleared" on an OK response. An error
        // response without the header (a degraded auth upstream 503ing
        // everything mid-incident — incidents correlate) is ambiguous and
        // must not exit poll mode, or the client would resume socket
        // churn for the whole outage.
        const syncMode = res.headers.get('X-Sync-Mode');
        if (syncMode !== null || res.ok) {
          this.onSyncModeHint(syncMode);
        }
        if (!res.ok) {
          const body = (await res.text().catch(() => '')).slice(0, 200);
          throw new Error(`sync hub pull ${res.status}: ${body}`);
        }
        const page = await res.json() as ChangesPage | null;
        if (!page || page.protocol_version !== 2 || !Array.isArray(page.ops)) {
          throw new Error('sync hub pull: malformed /changes response');
        }
        const epoch = assertCanonicalDecimal(page.epoch);
        assertCanonicalDecimal(page.head_seq);
        if (typeof page.more !== 'boolean') throw new Error('sync hub pull: more must be boolean');
        if (this.stopped) return;

        const decodedOps = decodeChanges(page.ops);
        const result = this.apply.applyOps(decodedOps, {
          epoch,
          requireContiguous: true,
        });
        pages++;

        if (result.epochReset) {

View on GitHub (pinned to e2d1df569a)

Solutions

  1. Check the embedded status: 401/403 means re-authenticate/refresh the sync credentials; 429 means back off (the client already retries with backoff).
  2. Verify the hub base URL and network path (curl the /changes endpoint with the same credentials).
  3. For 5xx, confirm hub health and retry once the incident clears — poll mode survives the outage automatically.
  4. If a proxy intercepts responses, bypass it or add the hub host to NO_PROXY.
Defensive patterns

Strategy: retry

Validate before calling

// Pre-flight the hub before entering the pull loop.
async function hubReachable(baseUrl: string, token: string): Promise<boolean> {
  const res = await fetch(`${baseUrl}/changes?cursor=0&limit=1`, {
    headers: { Authorization: `Bearer ${token}` },
  });
  return res.ok || res.status === 429; // 429 means alive but throttled
}

Try / catch

try {
  await pullPage();
} catch (e) {
  if (e instanceof Error && /sync hub pull (401|403)/.test(e.message)) {
    await refreshCredentials(); // hard stop is wrong for auth: fix creds, then retry
  } else if (e instanceof Error && e.message.startsWith('sync hub pull')) {
    await sleep(backoffMs()); // 5xx/429: exponential backoff, poll mode survives
  } else throw e;
}

Prevention

When it happens

Trigger: GET /changes returning 401/403 (expired or revoked sync credentials), 503 (degraded auth upstream mid-incident), 429 (rate limited), 404 (wrong hub base URL), or any 5xx while the hub is restarting; corporate proxy or captive portal intercepting the request.

Common situations: Auth token expired overnight and every pull now 401s; hub deployed behind a load balancer that 503s during rollout; DNS pointing at the wrong environment; local firewall blocking the hub host.

Understand the failure class

Background: "API error: {status}" and "HTTP 401/403/404/429/5xx" errors: non-2xx HTTP responses explained — this error's family across 27 libraries.

Related errors


AI-assisted analysis of thedotmack/claude-mem@e2d1df569a (2026-08-20). Data as JSON: /api/errors/3a4587a3fe22da34. Report an issue: GitHub.