CSD events

The CSD to i3 reverse-logistics stream · intake created, item received, disposition set, exchange issued, refund processed, repair opened, repair closed, reservation placed and released · with the payload, the idempotency key, the claim leg it takes, the ledger rows it produced, the replay control, the blocked lane, and what happens when CSD is down

7 of 9 topics do not exist in CSD yet. Today CSD emits exactly one typed inventory event, fireCsdReservation, from src/lib/inv-webhook.ts, and its default target is inv.minimalist-app.com which is INV1, not i2 and not i3. Every other topic below is registered here at not_started and waits on a cs-dashboard PR. A green tile on a topic the sibling cannot send would be a lie, so those tiles are violet. Bridge settings →
2 events failed HMAC verification in the last hour. Both were stored with hmac_verified=false and status=rejected, never discarded, so the incident is reconstructable. Likely a shared-secret rotation overlap; the previous key is accepted for 60 minutes via prev_key_valid_until.
Live · pauses on hover
3 events selected · replay re-runs the classifier with the current ruleset · every ledger write carries UNIQUE (source_system, idempotency_key) so duplicates are rejected by the database, not by a check-then-act in application code approval required above 10 events

Topic subscription health

9 topics registered · 2 live · 7 waiting on CSD. A topic with an expected_rate_per_day of null is not stale when it is silent, so a low-volume topic never reads as broken.

healthy stale idle, no expectation failing not emitting at source
csd.reservation.place
2m ago22
csd.reservation.release
3h ago17
csd.intake.created
never0
csd.parcel.booked
never0
csd.item.received
never0
csd.disposition.set
never0
csd.exchange.issued
never0
csd.refund.processed
never0
csd.repair.opened
never0
csd.repair.closed
never0

Migration state per topic

the double-post guard, as data
TopicFamilyPosts ledgerClaim legRequires line_idMigration stateReplayDays clean24h
csd.intake.createdintakenononeyesnot_startedrefused00
csd.parcel.bookedreceiptyesreserve onlyyesnot_startedrefused00
csd.item.receivedreceiptyesnoneyesnot_startedrefused00
csd.disposition.setdispositionyesreturn_inyesnot_startedrefused00
csd.exchange.issuedexchangeyesexchange_outyesnot_startedrefused00
csd.refund.processedrefundyesrefund_credityesnot_startedrefused00
csd.repair.openedrepairstock lane onlyrepair_outyesnot_startedrefused00
csd.repair.closedrepairstock lane onlyrepair_inyesnot_startedrefused00
csd.reservation.placereservationyescustodyyes · CSD sends noneshadowrefused422
csd.reservation.releasereservationyescustodyyes · CSD sends noneshadowrefused417
Replay is refused for every topic here because none has reached i3_owns. The refusal is a 409 carrying the topic's current state in the body, not a silent no-op. The registry CHECK ck_cet_replay makes an inconsistent row unwritable, so a topic can never be marked replayable while a sibling still owns its ledger effect.

Events per minute · last 60

live
18:14 · 1 events18:16 · 3 events18:18 · 4 events18:20 · 2 events18:22 · 5 events18:24 · 6 events18:26 · 5 events18:28 · 3 events18:30 · 4 events18:32 · 7 events18:34 · 8 events18:36 · 6 events18:38 · 5 events18:40 · 4 events18:42 · 5 events18:44 · 7 events18:46 · 10 events18:48 · 6 events18:50 · 5 events18:52 · 7 events18:54 · 8 events18:56 · 5 events18:58 · 6 events19:00 · 4 events19:02 · 5 events19:04 · 7 events19:06 · 6 events19:08 · 5 events19:10 · 9 events19:12 · 7 eventspeak 10/min
18:1418:4419:12
p50135ms
p95310ms
p99480ms

Outcome mix today

284 events
Processed with ledger rows · 142 · 50% Processed, registry only · 128 · 45% Blocked · 6 · 2.1% Unmatched · 3 · 1.1% Rejected HMAC · 2 · 0.7% 284 today
  • Posted ledger142
  • Registry only128
  • Blocked6
  • Unmatched3
  • Rejected HMAC2

Event stream

GET /api/csd/events · SSE /api/csd/events/stream · 284 today, page 1 of 12
TimeTopicCaseAnchorLegIdempotency keyLedgerRule · confSigStatusms
20:45:12disposition.set CSD-4821csd:4821:88214 return_in csd:disp:4821:88214:stock 1disposition_stock · 0.99 okprocessed198…
20:31:08exchange.issued CSD-4809csd:4809:88102 exchange_out csd:exch:4809:88102 2exchange_pair_open · 0.97 okprocessed224…
20:18:41item.received CSD-4803csd:4803:87994 custody csd:recv:4803:87994:1 1receive_to_quarantine · 0.99 okprocessed276…
19:55:03repair.opened CSD-4803csd:4803:87994 none csd:repair_open:4803:87994 0repair_customer_lane · 0.98 okprocessed142…
19:32:48intake.created CSD-4818csd:4818:88180 none csd:intake:4818 0intake_register · 0.99 okprocessed104…
19:14:00reservation.place CSD-4812missing refused csd_res:case_4812:reserve 0blocked · no line_id okblocked88…
19:02:19reservation.place CSD-4811missing refused csd_res:case_4811:reserve 0blocked · 3 SKUs, 1 line okblocked92…
18:48:55refund.processed CSD-4798csd:4798:87881 refund_credit csd:refund:4798:87881:R8821 1refund_with_receipt · 0.96 okprocessed186…
18:32:11disposition.set CSD-4791csd:4791:87702 unclaimed csd:disp:4791:87702:goodwill 0unmatched okdrift audit480…
18:20:02repair.closed CSD-4776csd:4776:87511 repair_in csd:repair_close:RPR-00305:1 2repair_stock_restock · 0.99 okprocessed292…
18:02:44parcel.booked CSD-4788csd:4788:87655 reserve csd:parcel:4788:DHL4548827193 1parcel_in_transit_return · 0.99 okprocessed152…
17:48:12exchange.issued CSD-4688csd:4688:86402 suppressed csd:exch:4688:86402 0claim_already_held · 1.00 okno double deduct62…
17:30:55disposition.set CSD-4671stored, unparsed none signature mismatch 0hmac_verified=false failrejected14…
17:14:28item.received CSD-4655stored, unparsed none signature mismatch 0hmac_verified=false failrejected12…
16:58:04intake.created CSD-4641csd:4641:85994 refused csd:intake:4641 0blocked · placeholder_sku okblocked72…
16:40:21reservation.release CSD-4629csd:4629:85702 custody csd:res:4629:85702:release:1 1reservation_release · 0.99 okprocessed156…
16:22:47intake.created CSD-4614csd:4614:85510 unclaimed csd:intake:4614 0unmatched · case_type=recurrence okdrift audit420…
142 of today's 284 events posted a ledger row; 128 were registry-only, which is correct because an intake or a case note has no stock consequence. 6 were blocked with a valid signature: three carried a case-level reservation key with no line_id, two named several SKUs on a single line, and one carried the literal placeholder SKU __NO_SKU__. All six are shapes CSD produces today, and all six would have been accepted silently by a receiver that only checked the signature.

Why replay is safe, and when it is refused

Every ledger write carries a composite unique index on (source_system, idempotency_key), and every reverse-side movement additionally claims a (anchor_key, leg) pair under its own partial unique index. A replay re-runs the classifier with the current ruleset and attempts the same inserts; the duplicates are rejected by the database, not by an application-level check-then-act.

Replay is refused for any topic whose migration_state is not i3_owns or legacy_retired, because a replay would post into a ledger a sibling still owns. The refusal is a 409 with the state in the body. Today that is all nine topics.

Bulk replay above 10 events writes an approval row naming the approver before anything runs. Today's 4 replays produced 0 new ledger rows and 0 new granted claims, which is acceptance test AC-08.12: replaying the last 24 hours must change nothing.

What happens when CSD is down

i3 keeps serving from its own state. A return_cases row in authorised or in_transit simply stops advancing and appears on the aging board, which is the correct behaviour: the parcels are still physically moving whether or not CSD is reachable.

No backfill is possible. CSD has no outbox. Its inventory client is fire-and-forget by design: fireInvWebhook catches every failure, writes it to a module-scoped invWebhookLastResult object, logs it and returns null. That object does not survive a process restart, so an event lost during an i3 outage is lost with no record on either side.

This page states that rather than promising otherwise. reverse_bridge_state.sibling_outbox_supported is false and backfill_window_days is 0. The v2 mockup's banner said "queued cases will backfill on resume", which is a promise the sibling cannot keep.

The idempotency keys i3 will not accept

CSD builds two keys today and both are case-grained:
// src/lib/inv-webhook.ts
const idem = "csd_res:" + reservationCode + ":" + eventType
// reservationCode = "case_" + caseLabel

// src/lib/ops2-push.ts
return "csd-" + routeName + "-" + caseId
Neither carries a line_id and neither carries a sequence. Three consequences, all live today:

1. A case covering three SKUs reserves one, because the caller passes skus[0] with quantity: 1.
2. A case that enters approval, is cancelled, and re-enters fires the identical key, so the second hold is deduped away and the stock is never re-held.
3. The location is hardcoded to 'HQ_319' regardless of where the stock actually is.

i3's receiver rejects those shapes with a 400 naming the missing fields, which is what the six blocked rows in the stream are. The correct key is csd:res:<case_id>:<line_id>:<place|release>:<seq>.

The first-item bug, twice

CSD already fixed this shape once, for money. Before 14 July 2026 the exchange path credited only the first selected item, so a return of earrings at AED 275 plus a necklace at AED 420 credited 275 and the customer was overcharged by 420. The fix routed refund and exchange through one helper, src/lib/return-credit.ts, whose docstring states the principle: "rule-based, not order-specific: works for any number of returned items and any bundle depth".

The identical shape is unfixed on the stock side. The reservation caller still takes skus[0]. i3 refuses to accept a single-SKU shape for a multi-SKU case at all, so the bug cannot be reproduced against the ledger even if it is reintroduced against the money.

Event stream

CSD inbox is drained

Every event received has been classified and written. Zero unmatched, zero blocked, zero rejected signatures.
Classifier rules →

No events match these filters

Filtering to topic csd.disposition.set with signature failed returns nothing: both of today's rejected events were on other topics.
Signature validation failed on the last batch. 7 CSD events rejected in the last 5 minutes and all 7 were stored with hmac_verified=false. The shared secret is mid-rotation; the previous key stays valid for another 42 minutes.
Service health →

Replay requires admin

You can read the stream, the topic health grid and every payload. Replaying an event needs role >= admin; pausing a bridge needs role >= ops_manager plus the bridge_control feature flag.
CSD bridge is paused. Paused 04:00 Dubai by ahmad for an HMAC key rotation. No events have been ingested since. Because CSD has no outbox there is no backfill window: events fired while paused are lost, so the pause is deliberately short and is announced to the CSD owner first.
GATE 2 has held for 4 of 7 consecutive days on the CSD bridge. Match rate 98.9% against a 99.5% target, every replay produced zero new rows, and the two shadow topics have 4 clean days each. Seven topics still read not_started because the CSD emitters do not exist; the gate cannot close until they do.

Connected to

Return queue → Repairs → ops2 events → All sibling events → Shopify events → IWMS events → GRN events → Classifier rules → Classifier stats → Drift → Conflicts → Webhook outbox → Rate limits → Audit log → Ops health → Settings →