thedotmack/claude-mem · error · Error

SyncApply: invalid op seq=

Error message

SyncApply: invalid op seq=${op.seq} kind=${op.kind} origin=${op.origin_device}/${op.origin_id}: body is not parseable JSON

What it means

parseBody() JSON-parses each sync op's body column. It throws when op.body is not valid JSON at all — e.g. raw text, truncated data, or binary bytes stored in the body field. The op is rejected rather than partially applied.

Solutions

  1. Check op.body for seq=${op.seq} in the sync log and fix or delete the corrupt row, then re-pull the op from the origin device.
  2. Fix the producer to always persist JSON.stringify(body) — never raw objects or formatted text.
  3. If rows were truncated by a DB migration, restore from backup and re-run sync.
  4. Add a producer-side assertion that the serialized body round-trips through JSON.parse before enqueueing.

Example fix

// before
await log.insert({ body: `user asked about sync` });
// after
await log.insert({ body: JSON.stringify({ op: 'set_title', text: 'user asked about sync' }) });
Defensive patterns

Strategy: try-catch

Validate before calling

function bodyParses(body) {
  try { JSON.parse(body); return true; } catch { return false; }
}
if (!bodyParses(op.body)) deadLetterQueue.push(op);

Type guard

function isParseableObject(body) {
  try {
    const p = JSON.parse(body);
    return typeof p === 'object' && p !== null && !Array.isArray(p);
  } catch { return false; }
}

Try / catch

try {
  applySyncOp(op);
} catch (e) {
  if (String(e.message).includes('body is not parseable JSON')) {
    logger.error({ seq: op.seq }, 'corrupt body — quarantining op');
    quarantine(op);
  } else throw e;
}

Prevention

When it happens

Trigger: The sync log row's body column contains malformed or truncated JSON — a crashed writer, manual DB edit, a queue message that was cut off, or a body stored with a non-JSON encoding.

Common situations: A migration or import script wrote plain text into body; a producer logged debug output instead of JSON.stringify'd payload; network payload was truncated before persistence; double-encoding bugs where a quoted string of JSON got mangled.

Understand the failure class

Background: JSON parse error: "Unexpected token" / "not valid JSON" / "failed to parse" — what JSON parsers are really complaining about — this error's family across 45 libraries.

Related errors


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

Appendix: source

Thrown at src/services/sync/SyncApply.ts:520

      }).catch((error) => {
        logger.error('SYNC_APPLY', 'Chroma forward of applied row failed, continuing without vector search', {},
          error instanceof Error ? error : new Error(String(error)));
      });
    }

    return result;
  }

  // -------------------------------------------------------------------------
  // Row ops
  // -------------------------------------------------------------------------

  private parseBody(op: SyncOp): Record<string, unknown> {
    let parsed: unknown;
    try {
      parsed = JSON.parse(op.body);
    } catch {
      throw invalidOp(op, 'body is not parseable JSON');
    }
    if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
      throw invalidOp(op, 'body must be a JSON object');
    }
    return parsed as Record<string, unknown>;
  }

  private findByOrigin(table: string, originDevice: string, originId: string): RowIdRev | undefined {
    return this.db.prepare(
      `SELECT id, CAST(sync_rev AS TEXT) AS sync_rev
       FROM ${table} WHERE origin_device_id = ? AND origin_local_id = ?`
    ).get(originDevice, originId) as RowIdRev | undefined;
  }

  /**
   * Canonical-v2 head ledger. It survives local row deletion and epoch replay,
   * so a stale live op cannot resurrect a tombstoned entity.
   */

View on GitHub (pinned to d8bc9755e7)