fix(feed): dedup rule-driven counterparty/series rows on the timeline#1920
Merged
Conversation
A retroactive (or sync-time) assign_counterparty / assign_series rule
wrote both a `rule_applied` row (attributed to the rule) and a
membership side-effect row (`counterparty_assigned` / `series_assigned`,
attributed to the SystemActor "Breadbox"). The rule-source dedup in
EnrichAnnotations only covered tag/category kinds, so the membership row
escaped it and the same application surfaced as two adjacent feed events
("Netflix Counterparty ran a rule …" + "Breadbox assigned … to Netflix").
Mirror the category/tag pattern for membership kinds:
- The rule-write paths (AssignCounterpartyFromRuleTx / AssignSeriesFromRuleTx)
now stamp `source: "rule"` into the membership payload via a threaded
`source` arg; user- and agent-authored assignments pass "" and survive.
- isRuleSourceDuplicate drops counterparty_assigned/_unlinked and
series_assigned/_unlinked rows carrying source="rule".
- formatRuleSummary phrases the surviving rule_applied row for the
counterparty / series action fields instead of falling back to "applied".
The result: one rule_applied event, no duplicate "Breadbox …" row.
Fixes #1915
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01CvAru8AVLa95DGfZg4M1wj
08f52d9 to
91c4268
Compare
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.
What & why
Fixes #1915 — on the home Feed, a single retroactive (or sync-time)
assign_counterparty/assign_seriesrule showed up as two events: the canonicalrule_appliedrow and a duplicate membership row attributed to the system actor "Breadbox":Root cause
When a rule fires
assign_counterparty/assign_series, two annotations are written inside the same transaction:rule_applied— attributed to the rule (the canonical audit record), andcounterparty_assigned/series_assigned) — attributed toSystemActor()("Breadbox").The rule-source dedup in
EnrichAnnotations(isRuleSourceDuplicate) already collapses this pair for tag/category kinds by dropping the side-effect row when its payload carriessource: "rule". The membership kinds were added after that dedup landed and were never wired in — the membership write path didn't stampsource: "rule", and the dedup switch didn't list those kinds. So the side-effect row escaped dedup and rendered as a second feed event with the wrong-looking "Breadbox" actor.The fix (mirrors the existing tag/category pattern)
AssignCounterpartyFromRuleTx/AssignSeriesFromRuleTx) now stampsource: "rule"into the membership payload via a threadedsourceargument. User- and agent-authored assignments pass""and continue to surface as first-class events.isRuleSourceDuplicatenow dropscounterparty_assigned/counterparty_unlinked/series_assigned/series_unlinkedrows carryingsource: "rule".formatRuleSummaryphrases the survivingrule_appliedrow for thecounterparty/seriesaction fields (instead of the generic "applied") so the per-transaction timeline stays readable.This fixes both surfaces that share
EnrichAnnotations— the home Feed and the per-transaction activity timeline.Evidence
This is a deduplication-logic bug, so the proof is a deterministic before/after of the same end-to-end path the screenshot exercises (seed 3 Netflix charges → create an
assign_counterpartyrule → apply retroactively → readListFeedEvents).Before the fix (dedup switch reverted to its old tag/category-only form) — the exact two rows from the issue:
After the fix — collapses to one event:
New regression tests added:
TestApplyRuleRetroactively_AssignCounterparty_FeedSingleEvent(integration) — the end-to-end feed assertion above.TestEnrichAnnotations_DropsRuleSourcedMembership(unit) — rule-source membership rows are deduped; surviving summary readsRule "Netflix Counterparty" set the counterparty retroactively.TestEnrichAnnotations_KeepsUserAuthoredMembership(unit) — user/agent-authored membership rows still survive.Validation run locally:
go build ./...,go vet ./...— clean.go test ./...(unit) — green for touched packages (the unrelatedinternal/agentprocess-reaping test fails identically on a clean checkout — pre-existing/env-specific).go test -tags integration -p 1 ./internal/service/ ./internal/sync/— green.Notes
docs/activity-timeline.mddedup contract updated to list the membership kinds and to note "stampsource: "rule"and add the kind toisRuleSourceDuplicate" when introducing a new rule-write path.rule_applied). Collapsing the duplicate removes the stray system-actor "Breadbox" row, which is what made the attribution look off in the issue.🤖 Generated with Claude Code
https://claude.ai/code/session_01CvAru8AVLa95DGfZg4M1wj
Generated by Claude Code