IWMS events

The ops2 to i3 warehouse event stream · put-away, pick, pack, move, bin adjust, cycle count · with the idempotency key, the replay control and the per-event-type migration state that makes the Phase 5 cutover impossible to double count

2 bridge 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.
13 of 24 event types are still dual-write. ops2 owns their ledger, i3 shadows. Replay is refused for those types and the cutover counter is at day 4 of 7. GATE 5 needs every type in i3_owns or ops2_retired. Location register →
3 events selected · replay re-runs the classifier with the current ruleset; ledger writes are unique on (source_system, idempotency_key) so a replay adds nothing · approval required above 10 events

Migration state per event type

The Phase 5 cutover plan, held as data in iwms_event_types · exactly one of ops2 and i3 writes the ledger for any type at any instant, enforced by a check constraint, not by convention

8 i3 owns13 dual-write2 shadow1 not started
Event typeFamilyPosts ledgerFrom typeTo typeStateops2 writesi3 writesShadow sinceDays cleanTodayNote
carton.createdcartonregistry only··i3_ownsnoyes21-063893GRN owns the stock recognition
carton.movedcartonconditionalanysamei3_ownsnoyes21-063862ledger pair only when the location changes
carton.splitcartonconditionalanysamei3_ownsnoyes21-063811same-location split writes nothing
carton.openedpickingyes · pairSTOCKSTOCKi3_ownsnoyes21-06386debit carton lines, credit picking
zone.movedzoneconditionalanyanyi3_ownsnoyes21-063862writes only on a stock-type change
qc.queuedqcregistry only··i3_ownsnoyes28-063111soft flag for the QC queue
count.scancountregistry only··i3_ownsnoyes28-063118module 10 owns the variance
count.disturbancecountregistry only··i3_ownsnoyes28-06312flags a count as unreliable
picking.replenish_outpickingyes · pairSTOCKSTOCKdual_writeyesno15-07424needs 7 clean days
picking.adjustpickingyesSTOCKvariesdual_writeyesno15-0743damage, correction, found, moved out
qc.acceptqcyesRNISTOCKdual_writeyesno15-0747-
qc.reject_localqcyesRNIQUARANTINEdual_writeyesno15-0743ops2 bug: bypassed the backbone from 19-04
qc.reject_supplierqcyesRNIRTS_PENDINGdual_writeyesno15-0741-
qc.repair_localqcyesRNIQUARANTINEdual_writeyesno15-0742ops2 sent REPAIRABLE, backbone rejects it
qc.repair_supplierqcyesRNIRTS_PENDINGdual_writeyesno15-0740-
transfer.stagetransferregistry only··dual_writeyesno18-07412module 07 owns the state machine
transfer.shiptransferyes · 2NSTOCKIN_TRANSIT_RETURNdual_writeyesno18-07422posts at the TRANSIT location, not the destination
transfer.delivertransferyes · 2NIN_TRANSIT_RETURNSTOCKdual_writeyesno18-07416-
rts.stagertsyesSTOCKRTS_PENDINGdual_writeyesno20-0746-
rts.shiprtsyesRTS_PENDINGRTS_PENDINGdual_writeyesno20-0744moves to SUPPLIER_HOLD location
rts.resolvertsyesRTS_PENDINGSTOCK or WOdual_writeyesno20-0743credit or write-off, finance approves
repack.sessionrepackrecipe_transformSTOCKSTOCKshadowyesno26-0739bulk to retail composition plus spoilage
picking.remove_to_pickingpickingyes · pairSTOCKSTOCKshadowneverno27-07012ops2 never wrote a ledger row for this at all, see below
label.reprintlabelregistry only··not_startedyesno-·0no events in 24h, harmless
packmat.consumedrepackregistry only··ops2_retirednoyes02-065724packaging material, not stock
24 registered event types · ops2 writes and i3 writes can never both be yes on one row, because ck_evt_single_writer makes that state unrepresentable. That is the whole double-count defence, and it lives in the schema rather than in a runbook.

Cutover burn-down

Event types by migration state, week by week · a type only moves right after 7 consecutive clean shadow days · GATE 5 closes when the two left bands are empty

13 still owned by ops2
Week of 02-06 · 24 types not started 09-06 · 18 not started 09-06 · 6 shadow 16-06 · 10 not started 16-06 · 12 shadow 16-06 · 2 i3 owns 23-06 · 6 not started 23-06 · 11 shadow 23-06 · 7 i3 owns 30-06 · 4 not started 30-06 · 9 shadow 30-06 · 4 dual write 30-06 · 7 i3 owns 07-07 · 2 not started 07-07 · 6 shadow 07-07 · 9 dual write 07-07 · 7 i3 owns 14-07 · 2 not started 14-07 · 3 shadow 14-07 · 12 dual write 14-07 · 7 i3 owns Today · 1 not started, label/reprint, no traffic Today · 2 shadow Today · 13 dual write, ops2 still owns the ledger Today · 7 i3 owns Today · 1 ops2 retired, packmat/consumed Types i3 owns, target 24 by GATE 5
02-0609-0616-0623-0630-0607-0714-07today
Not startedShadowDual write, ops2 owns the ledgeri3 ownsops2 retiredProgress stalled at 7 owned for three weeks: the QC, transfer and RTS families all entered dual-write together and share one 7-day clock

Topic health

12 topics subscribed, all HMAC verified healthy stale idle failing
carton/created
1m ago93
carton/split
4m ago11
carton/opened
1h ago6
location/moved
1m ago62
picking/replenish
12m ago24
picking/adjust
2h ago3
qc/outcome
6m ago13
transfer/confirmed
30s ago28
transfer/bridge
2m ago22
rts/created
12m ago13
repack/session
8m ago9
count/scan
3m ago18
packmat/consumed
14m ago24
awb/printed
3h ago4
label/reprint
over 24h0
state/reset
over 24h0
inflow/count
1h ago31
carton/merge
5h ago2

Events per minute, last 60 minutes

Two-minute buckets · dispatch batches and put-away waves are visible as spikes

7 per minute
13:44 · 1 event 13:46 · 3 events 13:48 · 5 events 13:50 · 2 events 13:52 · 6 events 13:54 · 7 events 13:56 · 5 events 13:58 · 4 events 14:00 · 4 events 14:02 · 7 events 14:04 · 8 events 14:06 · 6 events 14:08 · 5 events 14:10 · 4 events 14:12 · 6 events 14:14 · 8 events 14:16 · 10 events · put-away wave after GRN INF-2319 14:18 · 7 events 14:20 · 6 events 14:22 · 8 events 14:24 · 8 events 14:26 · 5 events 14:28 · 6 events 14:30 · 4 events 14:32 · 6 events 14:34 · 7 events 14:36 · 6 events 14:38 · 5 events 14:40 · 9 events · dispatch batch 14:42 · 7 events
13:4414:1414:42

Latency by family

p5098ms
p95210ms
p99388ms
carton
p95 142ms
zone
p95 96ms
picking
p95 188ms
qc
p95 204ms
transfer
p95 246ms
repack
p95 388ms
Repack is the slow family because one session fans out into a component transform group. SLA is 500ms for the receiver acknowledgement, which is measured separately from classification.

Bridge control

State
running
Ack cursor
ops2 outbox #884,217
Last ack
14:41:08 · 0 lag
HMAC key
key_2026_07 · rotated 12-07
Overlap window
60 min for the previous key
Retention
ops2 holds until acked
Pausing stops the ack cursor advancing. ops2 retains outbox rows until acked, so the backfill on resume is real rather than a promise. The old subscriber-list emit dropped any event whose subscriber was not registered at that moment.

Event stream

Newest first · the idempotency key column is what makes replay and dedup safe · a registry-only event correctly produces zero ledger rows

418 today1 unmatched2 rejected
TimeTopicReferenceLocation · zoneIdempotency keyLedgerRuleHMACStatems
14:41:12carton/createdCTN-2048HQ_319 · RECEIVING iwms:carton_created:CTN-20480carton_single_sku · 0.99okprocessed142⋯
14:38:04transfer/confirmedTRF-4412HQ_319 → MCC ops2:transfer:TRF-4412:ship4transfer_confirm_qr · 0.97okdual_write · i3 shadow188⋯
14:36:41location/movedMC-4470319_A3 → 319_A1 iwms:carton_moved:8842110zone_move_same_type · 0.99okprocessed · registry only96⋯
14:32:08picking/replenishREPL-0441319_PICKING → PACKOUT iwms:replen:04412pick_replenish_out · 0.94okdual_write · i3 shadow188⋯
14:28:55carton/openedMC-4471319_A3 → 319_PICKING iwms:opened:MC-44712carton_open_debit · 0.99okprocessed164⋯
14:22:19qc/outcomeCTN-2041HQ_319 · QC_HOLD iwms:qc:S-1188:CTN-20412qc_outcome_accept · 0.98okdual_write · i3 shadow204⋯
14:14:00carton/splitCTN-2044319_A2 · C04 iwms:split:CTN-2044:00030unmatched · needs classificationokdrift audit opened388⋯
14:08:32rts/createdRTS-8821HQ_319 · RTS_STAGING iwms:rts:8821:stage2rts_auto_create · 0.89okdual_write · i3 shadow204⋯
14:02:11carton/createdCTN-2043HQ_319 · RECEIVING iwms:carton_created:CTN-20430not classifiedfailed · key_2026_06rejected · stored12⋯
13:48:55repack/sessionRPK-318HQ_319 · 319_PICKING iwms:repack:3186repack_spoilage · 0.86okshadow only388⋯
13:41:02count/scanCC-1108HQ_319 · all zones iwms:count:1108:scan:44110count_scan · 0.99okprocessed · registry only88⋯
13:32:44location/movedMC-4462319_A2 · C04 iwms:carton_moved:8841980zone_move_same_type · 0.99oksuppressed duplicate8⋯
13:22:11carton/splitSPL-0212319_A2 → 319_A3 iwms:split:MC-4462:00070split_same_location · 0.99okprocessed · registry only118⋯
13:05:38picking/adjustPADJ-0119319_PICKING iwms:pickadj:01191picking_adjust_damage · 0.91okdual_write · i3 shadow174⋯
12:58:04packmat/consumedPKM-8842HQ_319 · PACKOUT iwms:packmat:88420packmat_consumed · 0.99oki3 owns · ops2 retired72⋯
09:40:21carton/mergeMRG-0044319_A1 · A08 iwms:merge:MC-4401:MC-44020carton_merge · 0.95okdual_write · i3 shadow156⋯
168 of today's 418 events posted a ledger row; the other 250 were registry-only, which is correct. Under the ops2 bridge every one of the 418 would have written an inventory_ledger row, including 250 with quantity = 0 and the self-cancelling split pairs.

Why replay is safe, and when it is refused

Every ledger write carries a composite unique index on (source_system, idempotency_key). 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.

That distinction matters. ops2's bridge guards idempotency with a SELECT ... WHERE reference_type='iwms' AND reference_id=? inside the same transaction as the insert, which is a race under concurrency and a silent double-post when it loses.

Replay is refused for any event type in dual_write or not_started, because ops2 still owns that type's ledger and a replay would post into a ledger i3 does not own. The refusal is a 409 with the event type's current state in the body, not a silent no-op.

Bulk replay above 10 events writes an approval row naming the approver before anything runs. Today's two replays produced 0 new ledger rows, which is the acceptance test for GATE 5 criterion 9: replaying the last 24 hours must change nothing.

The event type ops2 never wrote a ledger row for

picking.remove_to_picking is highlighted in the migration table with never in the ops2 column, and that is not a typo.

In ops2, partial removal to the picking area calls record_movement(event_type="remove_to_picking"). That string is absent from the valid-event frozenset, so the call raises immediately. The caller catches it with a bare handler whose comment reads "movement logging is audit-only; swallow so the stock move itself is the authoritative record".

The carton is decremented. The picking row is incremented. No movement row is created, so the ledger bridge never runs, so no ledger row exists. Every partial replenishment since that feature shipped is invisible to the quantity ledger. The swallow was written to protect an audit write and it is eating a stock write.

i3's fix is structural rather than a patch: carton_events.event_type is a foreign key to this registry, so an unregistered type is a constraint violation at insert time, and the carton event plus its ledger row are written in the same transaction or neither is. There is no audit-only path to swallow.

The type sits in shadow with 0 clean days because there is nothing on the ops2 side to compare against. It goes straight to i3_owns once the historical gap is quantified by the reconciler in T-04.16.

Connected to

Cartons → Locations → HQ_319 floor → All sibling events → Shopify events → Classifier rules → Classifier stats → Drift → Audit log → Transfers → GRN and QC → RTS → Stock count → Settings →