thedotmack/claude-mem · error · Error

invalid_ops: deviceId must be 1-128 characters

Error message

invalid_ops: deviceId must be 1-128 characters

What it means

invalid("deviceId must be 1-128 characters") is thrown by SyncHub.normalizeDeviceId when the trimmed deviceId is empty or longer than 128 characters. Device IDs are primary-key-like identifiers in the devices table, so the DO rejects any value outside the 1-128 character range before touching storage.

Solutions

  1. Ensure the client sends a non-empty deviceId of 1-128 characters after trimming.
  2. Generate IDs within bounds, e.g. crypto.randomUUID() (36 chars) instead of long composite strings.
  3. Trim/validate on the client before the call and show a field-level error.
  4. If legacy IDs are too long, hash them (e.g. SHA-256 hex) to fit under 128 chars.

Example fix

// before
const deviceId = `${userId}-${browser}-${window.location.href}`; // often > 128 chars
// after
const deviceId = crypto.randomUUID(); // 36 chars, always valid
Defensive patterns

Strategy: validation

Validate before calling

function assertValidDeviceId(deviceId: string): string {
  const id = deviceId.trim();
  if (id.length === 0 || id.length > 128) {
    throw new RangeError(`deviceId must be 1-128 characters, got ${id.length}`);
  }
  return id;
}

Type guard

function isValidDeviceId(v: unknown): v is string {
  return typeof v === "string" && v.trim().length >= 1 && v.trim().length <= 128;
}

Try / catch

try {
  await registerDevice(deviceId, name);
} catch (e) {
  if (e instanceof Error && e.message.includes("deviceId must be 1-128 characters")) {
    showFieldError("deviceId", "Must be 1-128 characters");
  } else throw e;
}

Prevention

When it happens

Trigger: Registering or touching a device with deviceId "" or only whitespace (trims to empty); a deviceId longer than 128 chars (e.g. a full URL, JWT, or concatenated UUID used as an ID); null/undefined coerced into a string form that fails the length check.

Common situations: Clients sending generated IDs from UUID+hostname+timestamp concatenation; framework code passing an empty string when the ID failed to load; copying an identifier with trailing whitespace and assuming trim is done server-side (it is, but emptiness after trim fails); migrating legacy IDs that exceed the cap.

Understand the failure class

Background: "invalid id" errors: invalid identifier format — why libraries reject IDs before lookup, and how to fix them — this error's family across 37 libraries.

Related errors


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

Appendix: source

Thrown at workers/sync-hub/src/do/SyncHub.ts:857

		if (result.rowsWritten === 0) throw deviceLimitError();
	}

	private touchExistingDevice(deviceId: string, name: string | null, now = Date.now()): void {
		const normalizedId = this.normalizeDeviceId(deviceId);
		this.ctx.storage.sql.exec(
			`UPDATE devices
			 SET name = COALESCE(name, ?), last_seen = ?
			 WHERE device_id = ?`,
			name,
			now,
			normalizedId,
		);
	}

	private normalizeDeviceId(deviceId: string): string {
		const normalizedId = deviceId.trim();
		if (normalizedId.length === 0 || normalizedId.length > 128) {
			throw invalid("deviceId must be 1-128 characters");
		}
		return normalizedId;
	}

	private meta(key: string): string {
		const row = this.ctx.storage.sql.exec<{ v: string }>("SELECT v FROM meta WHERE k = ?", key).toArray()[0];
		if (!row) throw new Error(`sync-hub invariant: missing meta ${key}`);
		return row.v;
	}

	private metaOptional(key: string): string | null {
		return this.ctx.storage.sql.exec<{ v: string }>("SELECT v FROM meta WHERE k = ?", key).toArray()[0]?.v ?? null;
	}

	private setMeta(key: string, value: string): void {
		this.ctx.storage.sql.exec(
			"INSERT INTO meta (k, v) VALUES (?, ?) ON CONFLICT(k) DO UPDATE SET v=excluded.v",
			key,

View on GitHub (pinned to d8bc9755e7)