perf: memoize handle resolution and cache the unanswered-thread scan (#6025) - #6133
Merged
Conversation
…6025) The Tribe "unanswered thread" detector ran a full timeline scan on every call: the dashboard alerts widget polls it every 120s and the Tribe page hits it again on mount, so back-to-back callers each paid for up to eight 2,000-row queries against the activity timeline plus a handle-resolution pass over every inbound turn. - Resolve counterpart handles through one memoizing resolver per scan (`createHandleResolver`), so a busy 1:1 thread's repeated handle runs the handle regexes, phone/email normalization and the Tribe/Contacts match once instead of once per message. `enrichActivityEvent` takes the resolver as an optional argument and behaves identically without it. - Cache the detection pass for 120s, keyed by detection window and holding the unsliced thread list so callers asking for different limits share one result. Caching the promise also collapses concurrent callers onto a single scan; a failed pass is never cached. Detected threads, ordering and outreach-draft behavior are unchanged. Claude-Session: https://claude.ai/code/session_01RA3pD5YM2dukQwbZ3pC6WA
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
findUnansweredTribeThreads()ran a full timeline scan on every call. The dashboard alerts widget polls it every 120s and the Tribe page hits it again on mount, so back-to-back callers each paid for up to eight 2,000-row queries againsthuman_activity_events(title, summary, participants + metadata JSONB) plus an unmemoized handle-resolution pass over every inbound turn.Two changes, both behavior-preserving:
createHandleResolver(ctx)(new, inidentityResolve.js) wrapsresolveHandlein a per-passMap.enrichActivityEvent(event, ctx, resolve)takes it as an optional third argument and is unchanged when omitted. The outreach scan builds one resolver per pass, so a busy 1:1 thread's repeated counterpart handle runs the handle regexes, phone/email normalization, Tribe index match and Contacts lookup once instead of once per message.DETECTION_CACHE_TTL_MS(120s), keyed by detection window (withinDays/staleAfterHours) and holding the unsliced thread list, so the alerts sweep andGET /api/tribe/outreach— which differ only inlimit— share one result. The cached value is the promise, so concurrent callers collapse onto a single scan; a failed pass is never cached.invalidateUnansweredThreadsCache()is exported for tests and for any caller that must see fresh state.Detected threads, sorting and outreach-draft generation are unchanged. The rejected SQL-side alternative from the issue (pairing inbound against subsequent outbound in SQL) was not attempted — that pairing lives in
groupUnansweredThreadsacross three different conversation-key shapes.Test plan
server/services/identityResolve.test.js— new test: a resolver returns the identical resolution object for a repeated handle, and enrichment through an injected resolver istoEqualthe uncached default path.server/services/tribeOutreach.test.js— new suite: one resolver is created per pass and passed to everyenrichActivityEventcall; a repeat call within the TTL issues no newlistEventsqueries and a differentlimitis served from the same cached list; the scan re-runs once the TTL lapses. Both fail against the pre-fix code (verified).cd server && npm test— 1917 files / 38,709 tests passing.Notes for review
invalidateUnansweredThreadsCache()from the timeline ingest paths) would remove that lag — deliberately left out to keep this diff to the perf fix.findUnansweredTribeThreadsis touched.suggestTribeImports(suggestTribeImports loads 2,000 full event rows with JSONB to compute handle frequency in Node memory #6026) is untouched, and no shared helper was extracted from it.Closes #6025
https://claude.ai/code/session_01RA3pD5YM2dukQwbZ3pC6WA