{"record":{"id":"9818d4e5743804e1","repo":"thedotmack/claude-mem","slug":"sync-hub-push-invariant-request-exceeds-max-bod","errorCode":null,"errorMessage":"sync hub push invariant: request exceeds ${MAX_BODY_BYTES} encoded bytes","messagePattern":"sync hub push invariant: request exceeds (.+?) encoded bytes","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"src/services/sync/CloudSync.ts","lineNumber":941,"sourceCode":"    }\n  }\n\n  /** POST one batch to the hub and stamp/delete on ack. */\n  private async sendOps(ops: WireOp[]): Promise<void> {\n    const response = await this.pushOps(ops);\n    // stop() while the POST was in flight: the DB may already be closing, so\n    // skip the stamp. The hub dedupes on (origin_device, kind, origin_id,\n    // rev), so re-pushing these ops on next start is harmless.\n    if (this.stopped) return;\n    this.validatePushResponse(response, ops);\n    this.stampAcked(response.acked, ops);\n    this.emitHeadSeq(response.head_seq);\n  }\n\n  private async pushOps(ops: WireOp[]): Promise<PushResponse> {\n    const requestBody = JSON.stringify({ protocol_version: 2, ops });\n    if (Buffer.byteLength(requestBody, 'utf8') > MAX_BODY_BYTES) {\n      throw new Error(`sync hub push invariant: request exceeds ${MAX_BODY_BYTES} encoded bytes`);\n    }\n    const res = await this.fetchImpl(`${this.hubUrl}/v1/sync/ops`, {\n      method: 'POST',\n      headers: {\n        'Content-Type': 'application/json',\n        'Authorization': `Bearer ${this.token}`,\n        'X-User-Id': this.userId,\n        'X-Device-Id': this.deviceId,\n        ...(this.deviceName ? { 'X-Device-Name': this.deviceName } : {}),\n      },\n      body: requestBody,\n      signal: AbortSignal.timeout(this.requestTimeoutMs),\n    });\n    // Mode hint BEFORE the ok-check: the kill-switch header rides error\n    // responses too, and a client that only learned the mode from happy\n    // paths would keep hammering the socket through an incident.\n    // Asymmetric on purpose (SyncClient.onSyncModeHint contract): header\n    // PRESENCE is emitted regardless of status; header ABSENCE is only","sourceCodeStart":923,"sourceCodeEnd":959,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d768ba364302d12b76e69e4f021f0bb1d2d50ed6/src/services/sync/CloudSync.ts#L923-L959","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","solutions":["Inspect the ops being pushed for an abnormally large payload (huge observation/summary text) and cap individual op size upstream.","Re-check pushRequestBytes logic — ensure it measures the same encoding (utf8 byte length) the guard uses.","Reduce the batch size so serialized output stays comfortably under 4MB.","If a single op legitimately exceeds the cap, chunk the content or store it out-of-band and sync a reference.","Treat this as a defect to fix in the batching path, not a runtime condition to retry."],"exampleFix":"// before: batcher counts ops, not bytes, so one giant op slips through\nbatch.push(op); // no size check\n// after: batcher enforces the same byte budget as pushOps\nif (pushRequestBytes(buf + JSON.stringify(op), batch.length+1) > MAX_BODY_BYTES) flush();\nbatch.push(op);","handlingStrategy":"validation","validationCode":"// Enforce the same 4MB cap when building the batch, not just when sending it\nconst MAX_BODY_BYTES = 4_000_000;\nfunction fitsPushBudget(ops: unknown[]): boolean {\n  return Buffer.byteLength(JSON.stringify({ protocol_version: 2, ops }), 'utf8') <= MAX_BODY_BYTES;\n}","typeGuard":"function isPushBodyTooLarge(e: unknown): boolean {\n  return e instanceof Error && /sync hub push invariant: request exceeds.*encoded bytes/i.test(e.message);\n}","tryCatchPattern":"// This is a code defect, not a runtime retry condition\nif (isPushBodyTooLarge(e)) { log.error('batcher size accounting is broken', e); /* split batch + re-flush */ }","preventionTips":["Measure batch byte size with the same Buffer.byteLength(JSON.stringify(...)) the guard uses.","Cap individual op payload size before adding to a batch.","Keep the batcher's estimator in sync with the WireOp serialized shape.","Add a unit test asserting no legal batch exceeds MAX_BODY_BYTES."],"tags":["cloud-sync","push","invariant","payload-size","batching"],"backgroundTag":null,"analyzedSha":"d768ba364302d12b76e69e4f021f0bb1d2d50ed6","analyzedAt":"2026-08-12T23:52:55.241Z","schemaVersion":2},"datasetVersion":"2026-08-13T04:17:16.726Z"}