fix: refine contacts cumulative stats + refine timeout (SQL/backend) - #2883
Open
malek10xdev wants to merge 7 commits into
Open
fix: refine contacts cumulative stats + refine timeout (SQL/backend)#2883malek10xdev wants to merge 7 commits into
malek10xdev wants to merge 7 commits into
Conversation
refinedpersons stats previously overwrote to the last cycle and pointsofcontact grew unbounded (every re-mine re-inserted duplicates, full-table scans timed out past Kong's ~60s upstream). Rework: - private.messages becomes a permanent per-user ledger with a refined_at marker; messages are never drained, and a message is never re-counted. - private.pointsofcontact becomes transient staging (purged after refine); inserts become idempotent via a new unique index. - refinedpersons accumulates (counts += delta, recency GREATEST, seniority LEAST, tags distinct-union, temperature computed inline matching the trigger). - partition-aware indexes on (user_id, message_id) / (user_id, person_id) and on messages (user_id, message_id) so the POC join stops using a nested-loop seq scan; orphan POC rows from previous cycles are purged once. Verified against old semantics on identical data (0 stat diffs) and at 60k msgs / 10k people (backfill ~13s, incumbent no-op ~70ms, incremental ~2.3s).
… timeout - INSERT_POC_BULK_SQL now ON CONFLICT (user_id, message_id, person_id) DO NOTHING so re-mined folders stop duplicating rows. - single-row person upsert gained the same IS DISTINCT FROM only-if-changed guard as the bulk variant, cutting redundant UPDATE writes/realtime events. - refineContacts races a 55s timeout (under Kong's ~60s) and logs clearly; Pipeline.complete keeps refine awaited (mining-complete email reads refined stats) and logs a structured completion error instead of a misleading 'email notification, refine contacts' message.
The contacts channel subscribed to all person changes, so background/passive mining flooded the client and the realtime tenant (dropping profile/credits events). The postgres_changes filter set is fixed at subscribe time, so: - add buildPersonChangeFilters(): UPDATE+DELETE always, INSERT only while a foreground mining is active; - rebuild the channel when activeMiningTask flips so the server-side filter stops delivering INSERTs outside foreground mining (keep-last; new contacts appear on reload/page entry).
…oid collision PR #2882 introduces 20260904000000_passive_mining_incremental_config.sql, so the refine ledger migration must move to 20260904000001 to keep migration ordering deterministic when both merge to main.
ctid is only unique per partition on a HASH(user_id)-partitioned table, so joining the batched orphan purge on poc.ctid could match valid rows at the same physical position in another partition and delete them. Join on the globally unique (id, user_id) PK instead. 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
The frontend person-INSERT gating (buildPersonChangeFilters + channel rebuild) is extracted to feat/contacts-realtime-insert-gating so the refine-persons SQL/backend fixes can land independently. 🤖 Generated with Codebuff Co-Authored-By: Codebuff <noreply@codebuff.com>
Coverage Report✅ Passed Commit: b3f01a2 Summary
All files
No coverage changes
Generated by Test Coverage Reporter for commit b3f01a2 |
This was referenced Sep 11, 2026
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Split from #2881 (now closed): this PR keeps the SQL/backend half — the /contacts realtime INSERT gating moves to #2884, where it belongs with the frontend channel changes.
Fixes two bugs from the client demo against QA:
refine contactsSQL timed out —refine_personsscanned the whole (growing)pointsofcontacttable with no(user_id, ...)index, blew Kong's ~60s upstream timeout, and only reflected the last mining cycle.Key changes
refine_persons rewrite (SQL)
messagesbecomes a permanent per-user ledger (refined_atmarker) — never drained, never re-counted;pointsofcontactbecomes transient staging (purged after refine) with idempotent inserts (ON CONFLICT ... DO NOTHING).refinedpersonsnow accumulates (counts+= delta, recency/seniorityGREATEST/LEAST, tags distinct-union; temperature computed inline matching the trigger). Stats are cumulative, no more double-counting.pointsofcontact (user_id, message_id),(user_id, person_id), andmessages (user_id, message_id)— the POC join drops the nested-loop seq scan; one-time purge of ~2.5M orphaned POC rows.(id, user_id)PK, notctid— ctid is only unique per partition on the HASH(user_id)-partitioned table, so cross-partition ctid collisions could have deleted valid rows.Backend writes
ON CONFLICT (user_id, message_id, person_id) DO NOTHING) — also a prerequisite for feat(mining): incremental passive mining via per-source UID watermark #2882's at-least-once watermark semantics (cancel/uidvalidity re-fetch re-inserts POC rows).IS DISTINCT FROMchange-guard on the single-row person upsert (theRETURNING+ fallback re-select keeps person-id resolution correct), cutting redundant UPDATE writes/realtime events.Testing
tscbuild clean, 605/605 unit tests green (after merging latestmain, incl. fix(mining): surface real OAuth 401 cause in passive-mining; improve reauth classification + error messages #2880), prettier/eslint clean.Migration notes for QA/prod
20260904000001_refine_persons_ledger_accumulate.sql, then run one fullSELECT private.refine_persons('<user_id>')so residual unrefined messages are accounted for.pointsofcontact/messagesduring the deploy window.refine_personsruns for the same user could double-count (the backend 55s timeout does not cancel server-side work) — pre-existing race class, not introduced here.leadminer.io(self-hosted Supabase config) — separate PR.Resolves #2867.