feat(iios): idempotency-command ledger + IdempotencyService (P9)

Adds the doc-mandated IiosIdempotencyCommand table + a reusable
IdempotencyService.run() that writes IN_FLIGHT at command start, replays the
cached response on a same-key/same-body retry, rejects a same-key/different-body
reuse (409), and retries a FAILED command. Tenant-scoped via
@@unique([scopeId, commandName, idempotencyKey]) (KG-02/KG-06).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-03 02:19:03 +05:30
parent dbf8f814c0
commit b8b7e0d3e8
7 changed files with 231 additions and 1 deletions
@@ -0,0 +1,23 @@
-- CreateTable
CREATE TABLE "IiosIdempotencyCommand" (
"id" TEXT NOT NULL,
"scopeId" TEXT,
"commandName" TEXT NOT NULL,
"idempotencyKey" TEXT NOT NULL,
"schemaVersion" TEXT NOT NULL DEFAULT 'v1',
"actorRefId" TEXT,
"requestHash" TEXT NOT NULL,
"responseHash" TEXT,
"response" JSONB,
"status" TEXT NOT NULL DEFAULT 'IN_FLIGHT',
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"completedAt" TIMESTAMP(3),
CONSTRAINT "IiosIdempotencyCommand_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE INDEX "IiosIdempotencyCommand_scopeId_status_idx" ON "IiosIdempotencyCommand"("scopeId", "status");
-- CreateIndex
CREATE UNIQUE INDEX "IiosIdempotencyCommand_scopeId_commandName_idempotencyKey_key" ON "IiosIdempotencyCommand"("scopeId", "commandName", "idempotencyKey");
@@ -492,6 +492,26 @@ model IiosProcessedEvent {
@@id([consumerName, eventId])
}
/// Idempotent-command ledger (P9). Suppresses duplicate externally-visible mutations:
/// same (scope, command, key) replays the cached response; a different request → conflict.
model IiosIdempotencyCommand {
id String @id @default(cuid())
scopeId String?
commandName String
idempotencyKey String
schemaVersion String @default("v1")
actorRefId String?
requestHash String
responseHash String?
response Json?
status String @default("IN_FLIGHT") // IN_FLIGHT | COMPLETED | FAILED
createdAt DateTime @default(now())
completedAt DateTime?
@@unique([scopeId, commandName, idempotencyKey])
@@index([scopeId, status])
}
// ─── P2: messaging — receipts & unread ────────────────────────────
/// Delivery/read receipt for a message interaction, per actor.