feat(iios): idempotency ledger on outbound egress (409 on key reuse) (P9)

OutboundService.send is now opt-in idempotent: an explicit idempotencyKey routes
the send through IdempotencyService.run (Slice 6), so reusing a key with a
different target/payload returns a 409 conflict and a same-key/same-request retry
replays the cached command. The inner findUnique early-return stays as a backstop
so a FAILED-then-retried send never collides on the command's unique key. Keyless
sends keep the old behavior (no ledger row).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-03 18:44:22 +05:30
parent cdba0f0179
commit a933e2e3af
8 changed files with 79 additions and 44 deletions
@@ -7,6 +7,7 @@ import { ActorResolver } from '../identity/actor.resolver';
import { AiJobService } from '../ai/ai.service';
import { AiBudgetGuard } from '../ai/ai-budget.guard';
import { OutboundService } from '../adapters/outbound.service';
import { IdempotencyService } from '../idempotency/idempotency.service';
import { CapabilityBroker } from '../capability/capability.broker';
import { CapabilityProviderRegistry } from '../capability/capability.registry';
import { CalendarService } from './calendar.service';
@@ -22,7 +23,7 @@ const alice: MessagePrincipal = { userId: 'alice', orgId: 'org_demo', appId: 'po
const START = '2026-07-02T10:00:00.000Z';
const ai = () => new AiJobService(asService, makeFakePorts(), makeFakeInference(), new AiBudgetGuard(asService), actors);
const cal = (ports?: FakePorts) => new CalendarService(asService, ports ?? makeFakePorts(), actors, ai(), new OutboundService(asService, new CapabilityBroker(makeFakePorts(), new CapabilityProviderRegistry())));
const cal = (ports?: FakePorts) => new CalendarService(asService, ports ?? makeFakePorts(), actors, ai(), new OutboundService(asService, new CapabilityBroker(makeFakePorts(), new CapabilityProviderRegistry()), new IdempotencyService(asService)));
async function scheduledMeeting(attendees: { userId: string; visibility?: 'FULL' | 'NONE' }[]) {
const m = await cal().scheduleDirect(alice, { meetingType: 'INTERNAL', title: 'Board sync', startAt: START, attendees });