email-security: a message that joins a campaign late, and the EMAIL_VERDICT that says so - #391
Open
maximelb wants to merge 2 commits into
Open
email-security: a message that joins a campaign late, and the EMAIL_VERDICT that says so#391maximelb wants to merge 2 commits into
maximelb wants to merge 2 commits into
Conversation
…vent that says so Clustering runs during ingest, so two copies of one attack that arrive together can each be stored attributed to nothing. A retro-join pass re-asks the question for 24 hours, and an EMAIL_VERDICT with campaign_joined_late: true reports the answer so a response that has already run can learn it missed something. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
lcbill
previously approved these changes
Sep 8, 2026
maximelb
marked this pull request as ready for review
September 8, 2026 11:42
Contributor
Author
|
/lc-review |
…t, and decided_at has two branches Review found both claims too broad: the queue is not "every recently-delivered ungrouped message" (most mail satisfies the two-key rule and would swamp it), and revision/decided_at is the join's clock only on an unrevised message. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
lcbill
approved these changes
Sep 8, 2026
Contributor
Author
|
/lc-review |
2 similar comments
Contributor
Author
|
/lc-review |
Contributor
Author
|
/lc-review |
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.
HOLD — merge with the prod release
This documents behaviour that is not in production yet (
go-mailsec#139+legion_mailsec#95, deployed to the experimental datacenter first). Do not merge until that ships to prod.Why
Campaign clustering runs while a message is being ingested, so two copies of one attack that arrive in the same instant each look for a campaign-mate before the other has been written down. Both are stored attributed to nothing, and a campaign that forms around a third copy later leaves one of them out — so a campaign-wide quarantine misses it and the campaign's
member_countunder-reports the spread.A bounded retro-join pass now re-asks that question for 24 hours after delivery, and reports the answer as an
EMAIL_VERDICTcarryingcampaign_joined_late: true.Both facts are customer-visible and neither was documented: a customer whose campaign grew after they swept it needs to know why, and a customer writing a D&R rule needs the field to gate on.
What changed
campaigns.md— a new "Messages that join after the fact" section, placed straight after "A campaign takes two messages" because it is the same subject continued: what happens to the message that had nobody to pair with yet. It states the two numbers a customer can observe (24 hours from delivery, re-asked at most every ten minutes), what they will see change (campaign_id,cluster_reason,member_count, and that a sweep from that point reaches the message), and the one case that is never re-asked — a message with no usable cluster keys, because no answer could change.automation.md— a new "A message that joins a campaign late" subsection under the existingEMAIL_VERDICTreference, plus two rows in that reference's field table (campaign_joined_late,cluster_reason).The subsection exists because the
revisionblock on this event needs explaining or it will be misread. It is a restatement, not a decision:revision/seqis the message's current revision sequence —0when the engine's verdict has never been overridden, which is the usual case — so a consumer already de-duplicating on(msg_uuid, seq)reads this as a decision it holds, now carrying a campaign. Without saying so, that consumer would either drop the event or treat it as a new decision, and both are wrong.revision/decided_atis when the join happened, because that is the only thing the event reports as new. The original decision's clock is unchanged in the store and in the event that carried it.It also states why this is a flag and not a new event type: a rule already written against
EMAIL_VERDICTkeeps working and simply starts seeing the campaign, which is the one fact a campaign-wide response needs. A new type would leave every one of those rules blind to it.A copy-pasteable D&R rule is included, and a tip saying why a customer would act on it at all — this event is how a response that has already run learns it missed something.
Anchors
Manually checked, both directions:
campaigns.md->automation.md#a-message-that-joins-a-campaign-late— the heading exists (### A message that joins a campaign late, automation.md:60).campaign_joined_latetable row ->#a-message-that-joins-a-campaign-late— same-page anchor, same heading.Scope
Nothing else was corrected. The body-similarity section is already current (it predates this branch), and the pre-existing text about clustering, sweeps and
member_countis unchanged.