thedotmack/claude-mem · error

sync hub push invariant: request exceeds ${MAX_BODY_BYTES} e

Error message

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

What it means

Thrown by pushOps before the HTTP request is sent, when JSON.stringify({protocol_version:2, ops}) exceeds MAX_BODY_BYTES (4,000,000). This is a client-side invariant guard: the batch-sizing logic upstream (pushRequestBytes checks during batching) should keep every push under the cap, so hitting this throw means the batcher and the actual serialized size disagree — a code/path bug rather than normal operation.

Source

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

    }
  }

  /** POST one batch to the hub and stamp/delete on ack. */
  private async sendOps(ops: WireOp[]): Promise<void> {
    const response = await this.pushOps(ops);
    // stop() while the POST was in flight: the DB may already be closing, so
    // skip the stamp. The hub dedupes on (origin_device, kind, origin_id,
    // rev), so re-pushing these ops on next start is harmless.
    if (this.stopped) return;
    this.validatePushResponse(response, ops);
    this.stampAcked(response.acked, ops);
    this.emitHeadSeq(response.head_seq);
  }

  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 d768ba3643)

Solutions

  1. Inspect the ops being pushed for an abnormally large payload (huge observation/summary text) and cap individual op size upstream.
  2. Re-check pushRequestBytes logic — ensure it measures the same encoding (utf8 byte length) the guard uses.
  3. Reduce the batch size so serialized output stays comfortably under 4MB.
  4. If a single op legitimately exceeds the cap, chunk the content or store it out-of-band and sync a reference.
  5. Treat this as a defect to fix in the batching path, not a runtime condition to retry.

Example fix

// before: batcher counts ops, not bytes, so one giant op slips through
batch.push(op); // no size check
// after: batcher enforces the same byte budget as pushOps
if (pushRequestBytes(buf + JSON.stringify(op), batch.length+1) > MAX_BODY_BYTES) flush();
batch.push(op);
Defensive patterns

Strategy: validation

Validate before calling

// Enforce the same 4MB cap when building the batch, not just when sending it
const MAX_BODY_BYTES = 4_000_000;
function fitsPushBudget(ops: unknown[]): boolean {
  return Buffer.byteLength(JSON.stringify({ protocol_version: 2, ops }), 'utf8') <= MAX_BODY_BYTES;
}

Type guard

function isPushBodyTooLarge(e: unknown): boolean {
  return e instanceof Error && /sync hub push invariant: request exceeds.*encoded bytes/i.test(e.message);
}

Try / catch

// This is a code defect, not a runtime retry condition
if (isPushBodyTooLarge(e)) { log.error('batcher size accounting is broken', e); /* split batch + re-flush */ }

Prevention

When it happens

Trigger: A batch of WireOp ops was assembled whose serialized JSON exceeds 4MB; the upstream batch split logic (lines ~740, ~820 using pushRequestBytes > MAX_BODY_BYTES) failed to bound the batch, or ops were added after the size check.

Common situations: A single op with an enormous embedded document/observation payload that alone exceeds 4MB; a bug in the batcher's size accounting (e.g. counting ops but not JSON overhead); a change to WireOp shape that inflated serialized size beyond the estimator.

Related errors


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