{"record":{"id":"949f18f22c632977","repo":"thedotmack/claude-mem","slug":"device-limit-exceeded","errorCode":"device_limit_exceeded","errorMessage":"device_limit_exceeded","messagePattern":"device_limit_exceeded","errorType":"error_code","errorClass":"Error","httpStatus":null,"severity":"error","filePath":"workers/sync-hub/src/do/SyncHub.ts","lineNumber":839,"sourceCode":"\tprivate projectedSeq(): string { return this.meta(\"projected_seq\"); }\n\n\tprivate touchDevice(deviceId: string, name: string | null, now = Date.now()): void {\n\t\tconst normalizedId = this.normalizeDeviceId(deviceId);\n\t\tconst result = this.ctx.storage.sql.exec(\n\t\t\t`INSERT INTO devices (device_id, name, last_seen)\n\t\t\t SELECT ?, ?, ?\n\t\t\t WHERE EXISTS (SELECT 1 FROM devices WHERE device_id = ?)\n\t\t\t    OR (SELECT COUNT(*) FROM devices) < ?\n\t\t\t ON CONFLICT(device_id) DO UPDATE SET\n\t\t\t name=COALESCE(devices.name, excluded.name),\n\t\t\t last_seen=excluded.last_seen`,\n\t\t\tnormalizedId,\n\t\t\tname,\n\t\t\tnow,\n\t\t\tnormalizedId,\n\t\t\tMAX_DEVICES_PER_USER,\n\t\t);\n\t\tif (result.rowsWritten === 0) throw deviceLimitError();\n\t}\n\n\tprivate touchExistingDevice(deviceId: string, name: string | null, now = Date.now()): void {\n\t\tconst normalizedId = this.normalizeDeviceId(deviceId);\n\t\tthis.ctx.storage.sql.exec(\n\t\t\t`UPDATE devices\n\t\t\t SET name = COALESCE(name, ?), last_seen = ?\n\t\t\t WHERE device_id = ?`,\n\t\t\tname,\n\t\t\tnow,\n\t\t\tnormalizedId,\n\t\t);\n\t}\n\n\tprivate normalizeDeviceId(deviceId: string): string {\n\t\tconst normalizedId = deviceId.trim();\n\t\tif (normalizedId.length === 0 || normalizedId.length > 128) {\n\t\t\tthrow invalid(\"deviceId must be 1-128 characters\");","sourceCodeStart":821,"sourceCodeEnd":857,"githubUrl":"https://github.com/thedotmack/claude-mem/blob/d8bc9755e74915e5c3b999181e10a67c889bce2a/workers/sync-hub/src/do/SyncHub.ts#L821-L857","documentation":"deviceLimitError() with code device_limit_exceeded is thrown when registering a new device would exceed MAX_DEVICES_PER_USER for that user. The INSERT includes the limit in the SQL guard and if rowsWritten === 0 the insert was rejected because the per-user device quota is full. This is a quota/policy error, not a corruption error.","triggerScenarios":"Calling the device-registration path (registerDevice) when the user already has MAX_DEVICES_PER_USER rows in the devices table; a new deviceId insert whose guarded COUNT/limit condition fails so rowsWritten comes back 0.","commonSituations":"Users logging in from many browsers/phones until the cap is hit; stale devices never removed because touchExistingDevice updates but never prunes; tests seeding devices without cleanup between runs; limit lowered in config while existing rows exceed it.","solutions":["Remove/deregister an unused device for the user, then retry registration.","Reuse an existing deviceId so the code path touches/updates it instead of inserting a new one.","Raise MAX_DEVICES_PER_USER if the quota is legitimately too small.","Add pruning of stale devices (last-seen based) before registration attempts.","Surface a user-facing 'device limit reached' message rather than retrying blindly."],"exampleFix":"// before\nawait hub.fetch(\"/register-device\", { method: \"POST\", body: JSON.stringify({ deviceId }) }); // throws at limit\n// after\nconst devices = await listDevices();\nif (devices.length >= MAX_DEVICES_PER_USER) await removeDevice(devices[0].id);\nawait hub.fetch(\"/register-device\", { method: \"POST\", body: JSON.stringify({ deviceId }) });","handlingStrategy":"try-catch","validationCode":"const count = await getDeviceCount(userId);\nif (count >= MAX_DEVICES_PER_USER) {\n  // free a slot or surface a quota UI before attempting registration\n}","typeGuard":null,"tryCatchPattern":"try {\n  await registerDevice(deviceId, name);\n} catch (e) {\n  if (e instanceof Error && (e as any).code === \"device_limit_exceeded\") {\n    await replaceOldestDevice(deviceId); // deregister then register\n  } else throw e;\n}","preventionTips":["Track the user's device count client-side and prompt for replacement at the cap.","Prune stale devices (no recent touch) on login.","Reuse an existing deviceId per browser/app install instead of minting new ones each session.","Check the configured MAX_DEVICES_PER_USER after config changes and reconcile existing rows."],"tags":["quota","limit-exceeded","devices","sync"],"backgroundTag":"quota-exceeded","analyzedSha":"d8bc9755e74915e5c3b999181e10a67c889bce2a","analyzedAt":"2026-09-17T16:40:26.182Z","contentChangedAt":"2026-09-17T16:40:26.182Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}