thedotmack/claude-mem · error
sync hub pull: malformed /changes response
Error message
sync hub pull: malformed /changes response
What it means
The hub /changes response was HTTP OK and parsed as JSON, but failed the ChangesPage envelope check: page falsy, protocol_version !== 2, or ops not an array. The client speaks exactly protocol v2, so this is a hard protocol gate — a 200 response with the wrong shape is treated as a contract violation, not an empty page.
Source
Thrown at src/services/sync/SyncClient.ts:554
// 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) {
// applyOps discarded the page and reset the cursor to 0; loop to
// re-pull from the start (apply is idempotent by design).
if (pages >= this.maxPagesPerCycle) return;
continue;View on GitHub (pinned to e2d1df569a)
Solutions
- curl the /changes endpoint directly and inspect the raw JSON — check protocol_version and the ops field.
- Align client and hub versions so both speak protocol_version 2.
- Fix the hub base URL if it points at a gateway/wrapper that mangles the body.
- Disable any response-transforming proxy for the hub host.
Defensive patterns
Strategy: validation
Validate before calling
// Validate the envelope shape yourself before handing it to the client internals.
function isChangesPage(v: unknown): v is { protocol_version: number; ops: unknown[]; more: boolean } {
if (!v || typeof v !== 'object') return false;
const p = v as Record<string, unknown>;
return p.protocol_version === 2 && Array.isArray(p.ops);
}
const raw = await res.json();
if (!isChangesPage(raw)) throw new Error(`hub protocol mismatch at ${hubUrl} — got protocol_version ${raw?.protocol_version}`); Type guard
function isChangesPage(v: unknown): v is { protocol_version: number; ops: unknown[]; more: boolean } {
return !!v && typeof v === 'object' &&
(v as any).protocol_version === 2 && Array.isArray((v as any).ops) &&
typeof (v as any).more === 'boolean';
} Try / catch
try {
await syncClient.start();
} catch (e) {
if (e instanceof Error && e.message.includes('malformed /changes response')) {
// Version/endpoint mismatch: inspect raw body with curl against the configured hub URL.
logHubDiagnostics(hubUrl);
}
throw e;
} Prevention
- Pin client and hub to compatible releases; treat protocol_version as a contract in change logs.
- Never route hub traffic through response-transforming proxies or gateways.
- In hub test suites, assert the exact v2 envelope to catch regressions before deploy.
When it happens
Trigger: Hub running an older or newer protocol_version than 2; a transparent proxy or captive portal returning 200 with an HTML body that happens to parse as JSON null; a load balancer routing to a different service on the same path; a hand-rolled hub missing fields.
Common situations: Client and hub upgraded out of sync (new protocol on one side only); hub URL actually pointing at an API gateway that wraps responses; middleware stripping or rewrapping the JSON body.
Understand the failure class
- Parsing and encoding errors: unexpected token, malformed input — why parsers reject input and how to find the real culprit.
Related errors
- sync hub pull: more must be boolean
- sync hub status: response requires decimal-string epoch/head
- sync hub push: response is not JSON
- sync hub status: response is not JSON
- sync hub status: response must be an object
AI-assisted analysis of thedotmack/claude-mem@e2d1df569a (2026-08-20).
Data as JSON: /api/errors/1bfaf3dacc87ff3e.
Report an issue: GitHub.