Core conceptsSignals & memory
Signals and memory
A signal is a reading derived from records you already have, not a new source of data. Nothing here is bought or guessed: each one is computed from a contact's own history, it is deterministic, and where the history cannot honestly answer the question the signal is withheld rather than estimated.
What a signal is here
Each signal is a pure function over rows that already exist. The reads are workspace and contact scoped, the maths is deterministic, and the same history always produces the same reading. That matters because a signal changes behaviour: it can hold a send, reorder a queue, or move a thread to the top of an inbox.
Every one of these reads fails safe, and safe means toward the decision that would have been made without it. A read error on the frustration signal degrades to the ordinary readiness decision rather than suppressing everyone; a read error on the fatigue signal means no re-weighting rather than a fabricated fatigue. A signal is never allowed to invent a stronger claim than the data supports.
The signals that change what happens
- Frustration
- Derived from the contact's own bounces, spam complaints, reply classification and reply sentiment over a rolling ninety-day window, so an old complaint decays rather than suppressing forever. It is consulted on the manual SMS send, on sequence drafting, on campaign launch and on the scheduler. It can only ADD suppression: it cannot clear an existing block or enable a send.
- Channel fatigue
- The trailing run of touches a contact did not engage with, per channel. It de-weights a fatigued channel and prefers the recommended alternate when the next touch is chosen, and the alternate is still only used if it passes the consent and cap gates that already applied.
- High intent in the inbox
- A scan of received email prose for explicit buying language across eight families: pricing, cost, meeting, demo, proposal, evaluation, timeline urgency and purchase readiness. It returns the exact phrases that matched and a score between zero and one, so a thread can be ranked by its strongest single signal rather than by how many phrases it contains.
- Dial intent
- Engagement recency, opens, clicks and replies, optionally layered with external triggers such as a funding round or an executive hire, each decaying with age. It is blended with the static priority score rather than replacing it, and it contributes nothing when no external triggers are supplied.
Fatigue is measured on email only, and here is why
This is the most useful thing on the page, because the honest version is narrower than the phrase cross-channel suggests.
An email send records whether that specific touch was opened, clicked or replied to, on the same row as the send. So for every email touch the system can say truthfully whether the contact engaged with THAT touch, which is exactly what a fatigue streak needs.
No other channel records that. An outbound SMS, a LinkedIn message and a call are separate event rows, and an inbound reply is not linked to the specific outbound touch it answered. Per-touch engagement is therefore not derivable for them from the data that exists.
The detector's engaged flag is a strict boolean with no unknown state, so passing a non-email touch through it would have to call it unengaged, which would manufacture fatigue that never happened. Those touches are excluded instead. The consequence to plan around: fatigue only ever flags email fatigue, because email fatigue is the only fatigue that can be proved.
The memory a signal reads
None of this is a separate store. The memory is the record, and it is worth knowing which part of it answers which question.
- The activity timeline: calls, emails, texts and meetings logged as they happen, which is what recency and volume are counted from.
- The per-send engagement columns on an email send, which are the only honest per-touch engagement in the system.
- The consent ledger, which records what was granted or revoked on which channel and when, rather than only the contact's current state.
- The suppression store, keyed on the email address and the phone number rather than the contact row, so an opt-out survives the contact being edited, deleted or re-imported.
- Enrichment provenance, which is the memory of where a field came from.
Where a field came from
Enriched fields are merged per field rather than per provider: for each field the highest-confidence answer wins, and the answer keeps its source and its confidence alongside the value, under a confidence floor. So a firmographic on a contact can be traced to the provider that supplied it and the confidence it was supplied at, instead of arriving as an unattributed fact.
External providers are gated on their credentials, and a provider with no key is a graceful no-op rather than an error. With no provider keys configured at all, enrichment is the internal heuristics over what the record already knows, and it never fabricates a profile URL it was not given.
Behaviour on this page is read from
- src/lib/frustration-suppression.ts
- src/lib/contact-frustration-signals.ts
- src/lib/dossier/contact-fatigue-input.ts
- src/lib/cross-channel-fatigue-signal.ts
- src/lib/channel-fatigue-detector.ts
- src/lib/inbox-high-intent-signal-detector.ts
- src/lib/dialer-intent-signal-scoring.ts
- src/lib/enrichment/enrichment-waterfall.ts
- src/lib/enrichment-fallback.ts
- src/lib/suppression-store.ts
Was this page helpful?