From 54deba3b6c3d187308e26353df90a1febf9dc8a4 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 26 Apr 2026 09:28:00 +0000 Subject: [PATCH] docs(ahand): pin hub-keying invariant in compensation comment MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Documents that ahand-hub keys devices by `id TEXT PRIMARY KEY` (globally unique, no per-user partitioning), which makes the 23505 catch unreachable in a concurrent-register race — the loser's hub.registerDevice() already fails at step 1. Clarifies the only reachable path (crash-retry) and why the bare-deviceId hub.deleteDevice compensation is safe in that case. Closes #78 — see PR for hub code references. https://claude.ai/code/session_01LWu6p2kzp1f4JTA1Jzyb4D --- apps/server/apps/gateway/src/ahand/ahand.service.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/apps/server/apps/gateway/src/ahand/ahand.service.ts b/apps/server/apps/gateway/src/ahand/ahand.service.ts index e25af526..4476d3dd 100644 --- a/apps/server/apps/gateway/src/ahand/ahand.service.ts +++ b/apps/server/apps/gateway/src/ahand/ahand.service.ts @@ -174,7 +174,15 @@ export class AhandDevicesService { .returning(); inserted = row; } catch (e) { - // Map postgres unique constraint violation to ConflictException + // Hub-keying invariant: ahand-hub keys devices by `id TEXT PRIMARY KEY` + // (no per-user partitioning — see team9ai/aHand devices migration). In a + // concurrent-register race the loser's hub.registerDevice() at step 1 + // already returns HTTP 409, which AhandHubClient re-throws as + // ConflictException before reaching this try block, so 23505 is + // unreachable in that case. It can still fire on a retry where the + // gateway crashed after step 1 (hub OK) but before step 2 (DB OK); the + // hub.deleteDevice compensation below is safe because deviceId is + // globally unique — it can only target the caller's own hub binding. if ((e as { code?: string }).code === '23505') { // Compensate hub registration since the device is already in DB await this.hub.deleteDevice(input.hubDeviceId).catch((err) => {