{"record":{"id":"be29f97a63d19ced","repo":"thedotmack/claude-mem","slug":"sync-hub-push-res-status-body","errorCode":null,"errorMessage":"sync hub push ${res.status}: ${body}","messagePattern":"sync hub push (.+?): (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/services/sync/CloudSync.ts","lineNumber":969,"sourceCode":"      },\n      body: requestBody,\n      signal: AbortSignal.timeout(this.requestTimeoutMs),\n    });\n    // Mode hint BEFORE the ok-check: the kill-switch header rides error\n    // responses too, and a client that only learned the mode from happy\n    // paths would keep hammering the socket through an incident.\n    // Asymmetric on purpose (SyncClient.onSyncModeHint contract): header\n    // PRESENCE is emitted regardless of status; header ABSENCE is only\n    // emitted (as null = \"cleared\") from an OK response — absence on an\n    // error response is ambiguous (a degraded auth upstream 503s without\n    // the funnel) and must not read as \"switch cleared\".\n    const syncMode = res.headers.get('X-Sync-Mode');\n    if (syncMode !== null || res.ok) {\n      this.emitSyncMode(syncMode);\n    }\n    if (!res.ok) {\n      const body = (await res.text().catch(() => '')).slice(0, 200);\n      throw new Error(`sync hub push ${res.status}: ${body}`);\n    }\n    let parsed: unknown;\n    try {\n      parsed = await res.json();\n    } catch {\n      throw new Error('sync hub push: response is not JSON');\n    }\n    const acked = (parsed as { acked?: unknown } | null)?.acked;\n    if (!Array.isArray(acked)) {\n      throw new Error('sync hub push: response missing acked array');\n    }\n    const headSeq = (parsed as { head_seq?: unknown }).head_seq;\n    const projectedSeq = (parsed as { projected_seq?: unknown }).projected_seq;\n    if (typeof headSeq !== 'string' || typeof projectedSeq !== 'string') {\n      throw new Error('sync hub push: response requires decimal-string head_seq/projected_seq');\n    }\n    assertCanonicalDecimal(headSeq);\n    assertCanonicalDecimal(projectedSeq);","sourceCodeStart":951,"sourceCodeEnd":987,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d768ba364302d12b76e69e4f021f0bb1d2d50ed6/src/services/sync/CloudSync.ts#L951-L987","documentation":"Thrown by the push response handler when the POST to ${hubUrl}/v1/sync/ops returned a non-OK status. The body is read (up to 200 chars) and included. X-Sync-Mode header is emitted on presence or OK, but absence on an error response is deliberately NOT emitted as a clear (per the documented asymmetric contract) so a degraded-auth 503 does not read as 'switch cleared'.","triggerScenarios":"The hub rejected the push: 401/403 (token/user/device invalid), 409 (sequence conflict / epoch mismatch), 413 (body too large, though 115 guards client-side), 422 (malformed ops), 429 (rate limit), 5xx (hub outage).","commonSituations":"Token expired between status and push; out-of-sync epoch/head_seq causing a 409; hub rate-limiting a chatty client; partial hub outage; client pushing ops the hub considers malformed after a protocol change.","solutions":["Read the status code and body in the message — 401/403 → re-auth; 409 → re-sync from status; 429 → back off; 5xx → retry with backoff.","Re-run probeHubStatus to refresh epoch/head_seq before retrying the push, in case of a 409 conflict.","Verify the token is still valid and X-User-Id/X-Device-Id are correct.","For 429, reduce push frequency / batch more aggressively.","For 5xx, retry with exponential backoff and surface a hub-outage indication to the user."],"exampleFix":"// before: push on a stale epoch hits 409 and aborts\nawait cloudSync.pushOps(ops);\n// after: refresh status then retry on conflict\ntry { await cloudSync.pushOps(ops); }\ncatch (e) { if (/push 409/.test(e.message)) { await cloudSync.status(); await backoff(); await cloudSync.pushOps(ops); } else throw e; }","handlingStrategy":"retry","validationCode":null,"typeGuard":"function isPushHttpError(e: unknown): boolean {\n  return e instanceof Error && /^sync hub push \\d{3}:/i.test(e.message);\n}","tryCatchPattern":"try { await cloudSync.pushOps(ops); }\ncatch (e) {\n  if (isPushHttpError(e)) {\n    const code = Number(e.message.match(/push (\\d{3})/)?.[1]);\n    if (code === 409) { await cloudSync.status(); await sleep(500); await cloudSync.pushOps(ops); }\n    else if (code === 429 || code >= 500) { await sleep(backoffMs); await cloudSync.pushOps(ops); }\n    else { throw e; }\n    return;\n  }\n  throw e;\n}","preventionTips":["Refresh the token before it expires to avoid 401/403 on push.","Re-probe status to refresh epoch/head_seq before retrying on 409 conflicts.","Back off pushes on 429; batch more to reduce request count.","Rely on hub dedup (origin_device, kind, origin_id, rev) — re-pushing un-acked ops is safe."],"tags":["cloud-sync","push","http","hub","auth"],"backgroundTag":null,"analyzedSha":"d768ba364302d12b76e69e4f021f0bb1d2d50ed6","analyzedAt":"2026-08-12T23:52:55.241Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}