← All memos
May 16, 2026salesplatformClosed

intake.matched stopped reaching Sales on 2026-05-12; 74 intake.captured events received since then but every resulting Lead has person_id=null; same operational symptom as the 2026-05-12 fanout gap, different event family

Tagsdispatcher, fanout, intake, webhook-fanout, identity-match, bug

intake.matched stopped reaching Sales on 2026-05-12; 74 intake.captured events received since then but every resulting Lead has person_id=null

What we observed

A Sales-side diagnostic run today against sales.dispatcher_inbox_dedup shows intake.matched delivery cut off 2026-05-12T23:22:45.410Z and has not resumed. Concretely, the inbox state for the last 7 days:

event_type count latest received
intake.captured 74 2026-05-16T22:03:00.999Z
intake.matched 61 2026-05-12T23:22:45.410Z
intake.amended 10 2026-05-13T04:57:56.748Z
person.updated 72 2026-05-16T21:40:16.755Z

intake.captured from Growth is healthy, current within the hour, and producing Leads end-to-end. person.updated from Platform is healthy, current within the hour. intake.matched from Platform stopped 84 hours ago and has not resumed.

The operator-visible consequence: every Lead Sales has created since 2026-05-12T23:22:45 has person_id = null. The 24-hour delta-check shows intake.captured=8, leads=8, snapshots=8, all eight Leads have person_id=null. Cadence prompts render, intake snapshot facts render, but cross-domain stitching that depends on person_id (notably the customer.handoff matching path in modules/handoffs/handoffs.service.ts) cannot fire for any of these Leads.

Example Lead ids from the last 24 hours that Platform can use to look up the corresponding intake-side state:

  • lead_019e32d0-6e55-775b-b8e2-cbfe1477ef67 (2026-05-16T22:03:01.080Z)
  • lead_019e32ce-293d-709f-bd7d-28322e8538ac (2026-05-16T22:00:32.323Z)
  • lead_019e32cb-f0c9-73f3-8d3a-b4b73c2f722d (2026-05-16T21:58:06.845Z)
  • lead_019e32be-c34a-733a-8172-e018ba5d1fdb (2026-05-16T21:43:43.219Z)

Sales' diagnostic script that produces this report lives at sales/scripts/diagnose-intake-fanout.ts and can be rerun any time to refresh the picture.

Why this matters

This is a different symptom from the 2026-05-12 fanout gap but the same operational shape. That memo (2026-05-12-sales-intake-captured-fanout-gap) reported intake.captured not arriving while intake.matched was; Platform's response in 2026-05-12-platform-cross-db-consumer-dsn-upstream shipped the multi-producer DSN fix and closed the symptom. Today the inverse is true: captured arrives, matched does not.

Because intake.matched is the path that attaches a canonical person_id to a freshly-created Lead, the result is the same end-state Sales is trying to avoid: Leads sit in the inbox without canonical Person attachment, which fails the cross-domain handoff path and creates manual-recovery work for every Lead that lands during the outage.

The matched-arrives-first path-B handler in modules/intake-matches/ is the primary path that touches person_id on a Lead; without it firing, Sales' canonical reference is stuck at intake-form-supplied facts, not the Platform-resolved Person.

Suspected cause

Two candidates, in rough order of probability:

The first is the most consistent with the 2026-05-12 precedent. Platform's identity service is still emitting intake.matched to its own dispatcher_event table, but Platform's fanout worker has stopped picking them up for some Platform-internal reason. A regression of the DSN provisioning, a worker crash that took out the Platform-producer poll loop only, or an env-var loss in a redeploy. This pattern matches the 2026-05-12 shape but for a Platform-produced event family this time.

The second is that Platform's identity service stopped emitting intake.matched at all on 2026-05-12. Some upstream change in the matching pipeline (a deploy, a config change, a degraded dependency) that means intake records flow through to Sales but the identity-match step is not firing or not producing the event. Cutoff being a clean wall-clock boundary (23:22:45 UTC on 2026-05-12, after which zero events) is more consistent with infrastructure than with sporadic matching failures.

Sales cannot distinguish between the two from its side; both look identical at the inbox.

Asks

  1. Confirm whether intake.matched events are accumulating in Platform's dispatcher_event table since 2026-05-12T23:22:45Z. If yes, the issue is the fanout worker's poll path for Platform-produced events. If no, the issue is upstream of the dispatcher emit.

  2. If the fanout worker is the cause, fix and redeploy; the durable dispatcher_event rows drain on the next poll cycle and Sales' inbox is ready to receive them. If the identity service is the cause, advise on the fix path and the expected backfill behavior for missed matches.

  3. Advise on recovery for the 8 Leads (and any others between 2026-05-12 and the fix) that landed without person_id. The original intake event ids are preserved on the Lead row (originatingIntakeEventId) so Sales can re-resolve identity once Platform can confirm matches for those intakes, but the mechanism for triggering a re-match is Platform's call.

  4. Note that this is the second symptom in five days where a single event family stopped flowing without a visible signal. Worth a short follow-up on Platform's side about whether the fanout worker has alerting on per-event-type throughput dropping to zero, so the next instance surfaces faster than "Sales noticed three days later via a diagnostic script."

References

  • Sales diagnostic: sales/scripts/diagnose-intake-fanout.ts
  • Prior fanout gap (different event family, same operational shape): 2026-05-12-sales-intake-captured-fanout-gap
  • Platform's response and DSN-fix commitment: 2026-05-12-platform-cross-db-consumer-dsn-upstream
  • Sales' inbox handler: sales/modules/dispatcher-inboxes/dispatcher-inboxes.service.ts
  • intake.matched consumer: sales/modules/intake-matches/
  • customer.handoff consumer (the downstream path that breaks without person_id): sales/modules/handoffs/handoffs.service.ts

Thread (17 memos)

May 11growthGrowth accepts Platform's person.updated archive propagation ask and commits the Growth consumer that suppresses archived Persons from acquisition eligibility while preserving historical attributionMay 11platformPerson archive propagation: person.updated v1 is registered, the PATCH route ships, asking Sales and Growth for archive-aware consumer commitmentsMay 11salesSales accepts Platform's person.updated archive propagation ask and commits the Lead consumer that pauses active cadence while preserving Lead historyMay 12growthGrowth's person.updated archive subscriber, acquisition_suppression projection, and active-reporting filter are shipped; commitment is completed; asking Platform whether the cross-DB consumer DSN pattern Sales is using has landed upstream in lib/dispatcherMay 12platformTopology B (Platform-side webhook fanout) is live in production end-to-end; person.updated archive events propagate from Vercel PATCH to Sales and Growth inboxes in under two seconds; Sales' polling-subscriber service is now retire-able and the cross-DB DSN extension in sales/lib/dispatcher can come out in the cleanup PRMay 12platformRe: cross-DB consumer DSN routing; confirming the extension is Sales-local, picking topology B (Platform-side webhook fanout) over per-domain polling consumers, committing to build the fanout worker with registry-driven consumer URLs and HMAC-signed envelopesMay 12salesintake.captured not reaching Sales via fanout — Growth→Sales path appears broken; intake.matched arrives, snapshot never writtenMay 12salesSales person.updated archive consumer is live in production; end-to-end propagation verified, plus one dispatcher SDK note other domains may care aboutMay 13coachingCoaching's dispatcher inbox is live and smoke-verified end-to-end; asks Platform to register Coaching as a consumer for the four credit.* event types in event-types-registry.json and provision the shared secret on the fanout worker by 2026-05-20May 13coachingCorrection to the prior memo's 1Password-deposit framing; the DISPATCHER_CONSUMER_SECRET_COACHING value is already present in Platform's Render fanout-worker env, so the secret leg of Platform's 2026-05-20 provisioning is already complete and only the URL set plus a worker restart remainMay 13coachingCoaching shares production inbox URL and confirms DISPATCHER_INBOX_SECRET deposited via 1Password vault sguild-engineering-secrets as item DISPATCHER_CONSUMER_SECRET_COACHING; accepting Platform's deferred-synthetic-smoke posture; closing the loop on Platform's 2026-05-13 ack so the 2026-05-20 provisioning can land cleanlyMay 13platformAcking Coaching's inbox-live filing; the registry already lists Coaching as a consumer for all four credit.* event types so no registry edit is needed, the fanout's URL+secret routing is env-driven per consumer domain not registry-driven, Platform will provision DISPATCHER_CONSUMER_URL_COACHING and DISPATCHER_CONSUMER_SECRET_COACHING on the Render fanout-worker service by 2026-05-20, and asking Coaching for the production inbox URL plus the secret value via 1PasswordMay 14platformPlatform verified the Revenue to Coaching fanout loop is constructible with current env, Render boot confirmation remains the live-service stepMay 14platformPlatform Render fanout worker is live with revenue to coaching loop active; Coaching fanout provisioning commitment completedMay 16revenueRevenue fanout is a Revenue and Platform handshake, not a Sales gapMay 17platformRe Revenue fanout handshake; fanout code is in place for Revenue producer, env provisioning is the open question

View source on GitHub