{"record":{"id":"98ddc9679fc72f0c","repo":"thedotmack/claude-mem","slug":"sync-hub-push-acked-index-must-be-an-object","errorCode":null,"errorMessage":"sync hub push: acked[${index}] must be an object","messagePattern":"sync hub push: acked\\[(.+?)\\] must be an object","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/services/sync/CloudSync.ts","lineNumber":990,"sourceCode":"    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'\n        || typeof item.operation_sha256 !== 'string'\n        || typeof item.seq !== 'string'\n      ) {\n        throw new Error(`sync hub push: malformed acked[${index}]`);\n      }\n      assertCanonicalDecimal(item.entity_rev, { positive: true });\n      assertCanonicalDecimal(item.seq, { positive: true });\n      if (typeof item.origin_local_id === 'string') assertCanonicalDecimal(item.origin_local_id);\n      return item as unknown as AckedOp;\n    });\n    return {","sourceCodeStart":972,"sourceCodeEnd":1008,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d768ba364302d12b76e69e4f021f0bb1d2d50ed6/src/services/sync/CloudSync.ts#L972-L1008","documentation":"Thrown while parsing the JSON body of a successful push to the sync hub. Each element of the `acked` array must be a plain object; this fires when an element is null, a primitive (string/number/boolean), or an Array. It is the first of two shape checks on acked entries (the second, [121], checks field types), so hitting it means the hub returned a structurally wrong ack list, not merely a mistyped field.","triggerScenarios":"A POST to the sync hub push endpoint returns HTTP 200 with an `acked` array containing a non-object element (e.g. `acked: [\"123\", null, []]` or a string-encoded entry). Reachable only inside the push response parser after `acked` is confirmed to be an array and `head_seq`/`projected_seq` are validated decimal strings.","commonSituations":"Hub version skew (server serializes acks as bare strings or wraps them differently than the client expects), a misbehaving proxy/CDN that rewrites the JSON body, or a test stub returning `acked: [\"seq-1\"]`. Also seen when the hub begins returning a new envelope shape before the client is upgraded.","solutions":["Inspect the raw hub response body (add a temporary log of `parsed` right before the `acked.map` call) to see the actual non-object element.","Confirm the sync hub software version matches the client CloudSync.ts version; a hub that serializes ack tuples as arrays or strings instead of objects indicates version skew.","If you control the hub, ensure every `acked` entry is a JSON object with the AckedOp shape (id, kind, origin_local_id, entity_rev, operation_sha256, seq).","Upgrade the client and hub together so the wire contract agrees on object-shaped ack entries."],"exampleFix":"// before (hub returning string acks): { \"acked\": [\"seq-1\", \"seq-2\"], ... }\n// after (object acks): { \"acked\": [ { \"id\": \"...\", \"kind\": \"observation\", \"origin_local_id\": null, \"entity_rev\": \"1\", \"operation_sha256\": \"...\", \"seq\": \"1\" } ], ... }","handlingStrategy":"try-catch","validationCode":"// Before calling cloudSync.push(pushed), you cannot pre-validate the hub body\n// (it is produced server-side). Validate your *input* instead:\nfunction assertWireOps(pushed: WireOp[]): void {\n  for (const op of pushed) {\n    if (typeof op.operation_sha256 !== 'string') throw new Error('push: operation_sha256 must be string');\n    parseCanonicalOperation(op); // throws on bad body\n  }\n}","typeGuard":"function isPlainObject(v: unknown): v is Record<string, unknown> {\n  return typeof v === 'object' && v !== null && !Array.isArray(v);\n}","tryCatchPattern":"try {\n  const resp = await cloudSync.push(pushed);\n} catch (e) {\n  if (e instanceof Error && e.message.startsWith('sync hub push:')) {\n    logger.error('SYNC', 'hub response shape rejected', { msg: e.message }, e);\n    // do not stamp outbox; leave ops for next push cycle\n    return;\n  }\n  throw e;\n}","preventionTips":["Pin hub and client versions together so the acked wire schema matches.","Log the raw parsed response once when integrating a new hub to confirm object-shaped acks.","Never hand-craft push-response mocks with non-object ack entries; mirror the AckedOp shape."],"tags":["sync","cloud-sync","hub-response","validation","wire-contract"],"backgroundTag":null,"analyzedSha":"d768ba364302d12b76e69e4f021f0bb1d2d50ed6","analyzedAt":"2026-08-12T23:52:55.241Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}