thedotmack/claude-mem · error · Error

sync hub push invariant: request exceeds

Error message

sync hub push invariant: request exceeds ${MAX_BODY_BYTES} encoded bytes

What it means

pushOps enforces a hard limit on the encoded push request body (MAX_BODY_BYTES) before POSTing ops to the hub. If JSON.stringify of the batch exceeds the limit, the client refuses to send it, since the hub would reject oversized bodies. This is a client-side guard of an internal invariant about batch size.

Solutions

  1. Reduce the ops batch size — chunk the WireOp[] into batches under MAX_BODY_BYTES and push sequentially.
  2. Flush the outbox more frequently so batches stay small.
  3. Inspect the outbox for abnormally large ops (e.g. huge content payloads) and trim them at the source.
  4. If legitimate payloads need more room, raise MAX_BODY_BYTES consistently on client and hub.

Example fix

// before
await client.pushOps(allOps);
// after
for (const chunk of chunkOps(allOps, MAX_OPS_PER_BATCH)) {
  await client.pushOps(chunk);
}
Defensive patterns

Strategy: validation

Validate before calling

const body = JSON.stringify({ protocol_version: 2, ops });
if (Buffer.byteLength(body, 'utf8') <= 4 * 1024 * 1024) await client.pushOps(ops);

Try / catch

try { await client.pushOps(ops); } catch (e) { if (e.message.includes('exceeds') && e.message.includes('bytes')) { /* split batch and retry */ } }

Prevention

When it happens

Trigger: Calling the push path with a WireOp[] batch whose JSON body exceeds MAX_BODY_BYTES — e.g. many queued ops accumulated in sync_content_outbox flushed in one request.

Common situations: Long offline periods let the outbox grow; a large batch is drained in one push; deletes or large content updates inflate op payloads; MAX_BODY_BYTES was lowered on a hub/proxy upgrade while the client still batches aggressively.

Understand the failure class

Background: payload too large / request exceeds maximum size: why libraries cap bytes and how to fix oversize payloads — this error's family across 50 libraries.

Related errors


AI-assisted analysis of thedotmack/claude-mem@d8bc9755e7 (2026-09-17). Data as JSON: /api/errors/9818d4e5743804e1. Report an issue: GitHub.

Appendix: source

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

        this.db.prepare(`
          UPDATE ${table} SET synced_at = NULL
          WHERE id = ? AND origin_device_id IS NULL AND (synced_at IS NULL OR synced_at < 0)
        `).run(row.origin_local_id);
      }
    });
    tx();
    logger.error('CLOUD_SYNC', 'Dropped stale content outbox snapshot; local row left unsynced for resnapshot', {
      kind: row.kind,
      originLocalId: row.origin_local_id,
      entityRev: row.entity_rev,
      reason,
    });
  }

  private async pushOps(ops: WireOp[]): Promise<PushResponse> {
    const requestBody = JSON.stringify({ protocol_version: 2, ops });
    if (Buffer.byteLength(requestBody, 'utf8') > MAX_BODY_BYTES) {
      throw new Error(`sync hub push invariant: request exceeds ${MAX_BODY_BYTES} encoded bytes`);
    }
    const res = await this.fetchImpl(`${this.hubUrl}/v1/sync/ops`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'Authorization': `Bearer ${this.token}`,
        'X-User-Id': this.userId,
        'X-Device-Id': this.deviceId,
        ...(this.deviceName ? { 'X-Device-Name': this.deviceName } : {}),
      },
      body: requestBody,
      signal: AbortSignal.timeout(this.requestTimeoutMs),
    });
    // Mode hint BEFORE the ok-check: the kill-switch header rides error
    // responses too, and a client that only learned the mode from happy
    // paths would keep hammering the socket through an incident.
    // Asymmetric on purpose (SyncClient.onSyncModeHint contract): header
    // PRESENCE is emitted regardless of status; header ABSENCE is only

View on GitHub (pinned to d8bc9755e7)