thedotmack/claude-mem · error · Error
ops[ ]
Error message
ops[${index}] ${error instanceof Error ? error.message : String(error)} What it means
Aggregated per-op validation failure in SyncHubDO.pushOps: while parsing/validating ops[index], parseCanonicalOperation or the origin_device_id match check failed, and the inner message is prefixed onto ops[index] to pinpoint the offending entry. The whole batch is refused with this message via INVALID_OPS_PREFIX.
Solutions
- Read the index in ops[N] and the inner message to find the bad op
- Ensure every op's origin_device_id equals the authenticated X-Device-Id
- Drop or re-create the offending op client-side and re-push the rest
- Upgrade/downgrade the client so its op schema matches the hub's parser
Defensive patterns
Strategy: validation
Validate before calling
ops.forEach((op, i) => {
if (op.body?.origin_device_id !== deviceId) throw new Error(`op ${i} origin_device_id mismatch`);
}); Try / catch
try {
return await hub.push(deviceId, ops);
} catch (e) {
const m = String(e).match(/ops\[(\d+)\] (.+)/);
if (m) { dropOp(Number(m[1])); return retryPushWithoutBadOps(); }
throw e;
} Prevention
- Stamp every op with the same device id used for authentication
- Validate ops locally (schema + origin_device_id) before batching
- Rebuild the local op queue after client restores or version upgrades
When it happens
Trigger: Any op in the batch fails schema parsing (bad kind, malformed body, invalid revision format) or its parsed.body.origin_device_id differs from the authenticated deviceId — commonly op #N where a batch mixes devices or contains a hand-crafted op.
Common situations: Merged/replayed batches after a client restore containing ops from an old device id, schema drift after a client upgrade introducing a new op kind the hub doesn't parse, corrupted local op queue.
Understand the failure class
Background: Schema validation failed / invalid input schema: payload rejected because its shape doesn't match the expected schema — this error's family across 28 libraries.
Related errors
- canonical content
- device_id must be 1-128 characters
- deviceId must be non-empty
- name must be 1-80 characters
- ops must be an array
AI-assisted analysis of thedotmack/claude-mem@d8bc9755e7 (2026-09-17).
Data as JSON: /api/errors/29fd0ba4731b7b22.
Report an issue: GitHub.
Appendix: source
Thrown at workers/sync-hub/src/do/SyncHub.ts:386
// ---------------------------------------------------------------------
// Canonical append path and client cursor reads.
// ---------------------------------------------------------------------
async pushOps(deviceId: string, ops: PushOp[], deviceName: string | null = null): Promise<PushOutcome> {
let rows: ValidatedOp[];
try {
if (typeof deviceId !== "string" || deviceId.length === 0) throw invalid("deviceId must be non-empty");
if (!Array.isArray(ops)) throw invalid("ops must be an array");
rows = await Promise.all(ops.map(async (op, index) => {
try {
const parsed = await parseCanonicalOperation(op);
if (parsed.body.origin_device_id !== deviceId) {
throw new Error("origin_device_id does not match authenticated X-Device-Id");
}
return parsed;
} catch (error) {
throw invalid(`ops[${index}] ${error instanceof Error ? error.message : String(error)}`);
}
}));
} catch (error) {
if (error instanceof Error && error.message.startsWith(INVALID_OPS_PREFIX)) {
return { refused: true, error: error.message };
}
if (isDeviceLimitError(error)) return { refused: true, error: DEVICE_LIMIT_ERROR };
throw error;
}
const sql = this.ctx.storage.sql;
const now = Date.now();
const nowDecimal = String(now);
const headBefore = this.headSeq();
const acked: AckedOp[] = [];
try {
this.ctx.storage.transactionSync(() => {
this.touchDevice(deviceId, normalizeDeviceName(deviceName), now);View on GitHub (pinned to d8bc9755e7)