thedotmack/claude-mem · error

sync hub push: acked[${index}] must be an object

Error message

sync hub push: acked[${index}] must be an object

What it means

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.

Source

Thrown at src/services/sync/CloudSync.ts:990

    try {
      parsed = await res.json();
    } catch {
      throw new Error('sync hub push: response is not JSON');
    }
    const acked = (parsed as { acked?: unknown } | null)?.acked;
    if (!Array.isArray(acked)) {
      throw new Error('sync hub push: response missing acked array');
    }
    const headSeq = (parsed as { head_seq?: unknown }).head_seq;
    const projectedSeq = (parsed as { projected_seq?: unknown }).projected_seq;
    if (typeof headSeq !== 'string' || typeof projectedSeq !== 'string') {
      throw new Error('sync hub push: response requires decimal-string head_seq/projected_seq');
    }
    assertCanonicalDecimal(headSeq);
    assertCanonicalDecimal(projectedSeq);
    const validatedAcked = acked.map((value, index): AckedOp => {
      if (!value || typeof value !== 'object' || Array.isArray(value)) {
        throw new Error(`sync hub push: acked[${index}] must be an object`);
      }
      const item = value as Record<string, unknown>;
      if (
        typeof item.id !== 'string'
        || typeof item.kind !== 'string'
        || (item.origin_local_id !== null && typeof item.origin_local_id !== 'string')
        || typeof item.entity_rev !== 'string'
        || typeof item.operation_sha256 !== 'string'
        || typeof item.seq !== 'string'
      ) {
        throw new Error(`sync hub push: malformed acked[${index}]`);
      }
      assertCanonicalDecimal(item.entity_rev, { positive: true });
      assertCanonicalDecimal(item.seq, { positive: true });
      if (typeof item.origin_local_id === 'string') assertCanonicalDecimal(item.origin_local_id);
      return item as unknown as AckedOp;
    });
    return {

View on GitHub (pinned to d768ba3643)

Solutions

  1. 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.
  2. 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.
  3. 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).
  4. Upgrade the client and hub together so the wire contract agrees on object-shaped ack entries.

Example fix

// before (hub returning string acks): { "acked": ["seq-1", "seq-2"], ... }
// after (object acks): { "acked": [ { "id": "...", "kind": "observation", "origin_local_id": null, "entity_rev": "1", "operation_sha256": "...", "seq": "1" } ], ... }
Defensive patterns

Strategy: try-catch

Validate before calling

// Before calling cloudSync.push(pushed), you cannot pre-validate the hub body
// (it is produced server-side). Validate your *input* instead:
function assertWireOps(pushed: WireOp[]): void {
  for (const op of pushed) {
    if (typeof op.operation_sha256 !== 'string') throw new Error('push: operation_sha256 must be string');
    parseCanonicalOperation(op); // throws on bad body
  }
}

Type guard

function isPlainObject(v: unknown): v is Record<string, unknown> {
  return typeof v === 'object' && v !== null && !Array.isArray(v);
}

Try / catch

try {
  const resp = await cloudSync.push(pushed);
} catch (e) {
  if (e instanceof Error && e.message.startsWith('sync hub push:')) {
    logger.error('SYNC', 'hub response shape rejected', { msg: e.message }, e);
    // do not stamp outbox; leave ops for next push cycle
    return;
  }
  throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


AI-assisted analysis of thedotmack/claude-mem@d768ba3643 (2026-08-12). Data as JSON: /api/errors/98ddc9679fc72f0c. Report an issue: GitHub.