{"record":{"id":"64fdbef91eeb2f3f","repo":"thedotmack/claude-mem","slug":"sync-hub-push-response-missing-acked-array","errorCode":null,"errorMessage":"sync hub push: response missing acked array","messagePattern":"sync hub push: response missing acked array","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/services/sync/CloudSync.ts","lineNumber":979,"sourceCode":"    // 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);\n    const validatedAcked = acked.map((value, index): AckedOp => {\n      if (!value || typeof value !== 'object' || Array.isArray(value)) {\n        throw new Error(`sync hub push: acked[${index}] must be an object`);\n      }\n      const item = value as Record<string, unknown>;\n      if (\n        typeof item.id !== 'string'\n        || typeof item.kind !== 'string'\n        || (item.origin_local_id !== null && typeof item.origin_local_id !== 'string')\n        || typeof item.entity_rev !== 'string'","sourceCodeStart":961,"sourceCodeEnd":997,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/e2d1df569a8f04075d40e92461128ece7cf04c82/src/services/sync/CloudSync.ts#L961-L997","documentation":"The push response parsed as JSON but its acked field is not an array (missing, null, or an object). Protocol v2 requires the response to carry the complete acked array plus head_seq/projected_seq; validation runs before stampAcked so nothing is written locally, and flush() backs off and retries the same ops safely. This indicates a hub that does not implement the v2 response envelope — usually version skew.","triggerScenarios":"Hub build predating protocol v2 returning a different response shape (e.g. {ok:true}); a field rename on the hub; a proxy returning its own JSON (health-check style) with 200; a partially deployed hub.","commonSituations":"Client upgraded ahead of the hub; hub rolled back mid-incident; load balancer routing /v1/sync/ops to the wrong service; test stub answering with a placeholder body.","solutions":["curl -X POST the ops endpoint with an empty ops array and inspect the exact response shape","Deploy a hub build that returns { acked: [...], head_seq: \"...\", projected_seq: \"...\" } for protocol v2","Verify the load balancer routes /v1/sync/* to the sync hub, not another backend","Pin client and hub versions together in deployment so the wire contract stays matched"],"exampleFix":null,"handlingStrategy":"type-guard","validationCode":"// Validate a push response envelope in hub contract tests:\nfunction validPushEnvelope(v: unknown): boolean {\n  if (!v || typeof v !== 'object') return false;\n  const r = v as Record<string, unknown>;\n  return Array.isArray(r.acked)\n    && typeof r.head_seq === 'string'\n    && typeof r.projected_seq === 'string';\n}","typeGuard":"function isPushResponseShape(v: unknown): v is { acked: unknown[]; head_seq: string; projected_seq: string } {\n  return !!v && typeof v === 'object' && !Array.isArray(v)\n    && Array.isArray((v as Record<string, unknown>).acked)\n    && typeof (v as Record<string, unknown>).head_seq === 'string'\n    && typeof (v as Record<string, unknown>).projected_seq === 'string';\n}","tryCatchPattern":"// Internal; surfaces via lastError after backoff:\nif (/missing acked array/.test(sync.status().lastError ?? '')) {\n  // hub is not speaking the v2 response contract: check hub version/deployment before retrying\n}","preventionTips":["Run hub response-envelope contract tests in CI against the deployed client build","Pin hub and client releases together; never partial-upgrade one side","Make load balancer health checks hit a hub-specific route so /v1/sync/ops never lands elsewhere"],"tags":["sync","hub","protocol-validation","response-shape"],"backgroundTag":"schema-validation-failed","analyzedSha":"e2d1df569a8f04075d40e92461128ece7cf04c82","analyzedAt":"2026-08-20T23:58:13.836Z","contentChangedAt":"2026-08-20T23:58:13.836Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}