Polish repeat-campaign rule: post-time window, attribution, logs (#87) #38

Merged
backmeupplz merged 4 commits from ticket-87-campaign-polish into main 2026-10-12 18:05:49 +00:00
backmeupplz commented 2026-10-12 17:43:34 +00:00 (Migrated from github.com)

Follow-up polish for the #81 repeat-campaign rule (agentboard #87). Laya model, the 0.80 gate and option-B semantics are unchanged.

  1. Post-time window. Copies are matched on Telegram's original post time (ctx.msg.date), not processing time. An edit keeps the copy's original time and its handled state, so an edit of an already-attempted copy is never retried. Memory eviction still uses when a copy was seen, so a delayed backlog can't evict copies early or revive old ones.
  2. Attribution. Stats record source: "cache" only for the current copy when it was a cache hit. Earlier campaign copies are jev. The spam_deleted.rule field is now repeat_campaign when the campaign widened the deletion, otherwise cache or model.
  3. repeat_campaign_detected.copyCount logs the campaign size (all copies in the window), not just the copies deleted now.
  4. Cap. The ledger groups copies by normalized text and evicts only whole least-recently-seen texts (20 per actor), so the cap can never drop part of a campaign. Copies of one text are bounded by the 10-minute window.
  5. Seed failures. spamCache.seed now runs outside the classifier try block. A failure logs spam_cache_seed_failed and never becomes classification_failed.
  6. Tests. Ledger unit tests cover the post-time window, edit state and the cap. New bot-level tests cover a cache-hit copy counting towards a campaign, a cache-hit trigger with model-kept earlier copies (per-copy stats source), separation between chats with each delete's chat_id, the post-time window, never retrying an edited handled copy, and seed failure isolation.

bun run check passes locally (typecheck + 492 pass / 285 skipped DB/live tests / 0 fail). Three of the new bot tests fail on main. The chat-separation test is regression coverage.

Review follow-ups (Astra, all rounds CLEAN)

  • A campaign needs its ≥3 distinct posts to fit inside one <10-minute post-time window with the current copy (linear two-pointer sweep). An edit can no longer stretch a campaign to ~20 minutes.
  • Copies are retained for 2× the window after being seen, so processing backlogs and late edits keep their state. Matching still uses post times.
  • Handled copies drop their audit snapshot. A handled copy edited out of eligibility and back keeps its handled state (short-lived tombstone), so a failed delete is not retried.
  • repeat_campaign_detected is logged only when something is pending.
  • The 20-text cap trade-off is documented: losing a text takes 20 other distinct 40+ character texts from one actor inside the window.
  • Accepted limitation: handled state lasts until 2× the window after the copy was first seen, so at most one bounded retry of a failed delete is possible after that.
Follow-up polish for the #81 repeat-campaign rule (agentboard #87). Laya model, the 0.80 gate and option-B semantics are unchanged. 1. **Post-time window.** Copies are matched on Telegram's original post time (`ctx.msg.date`), not processing time. An edit keeps the copy's original time and its handled state, so an edit of an already-attempted copy is never retried. Memory eviction still uses when a copy was seen, so a delayed backlog can't evict copies early or revive old ones. 2. **Attribution.** Stats record `source: "cache"` only for the current copy when it was a cache hit. Earlier campaign copies are `jev`. The `spam_deleted.rule` field is now `repeat_campaign` when the campaign widened the deletion, otherwise `cache` or `model`. 3. **`repeat_campaign_detected.copyCount`** logs the campaign size (all copies in the window), not just the copies deleted now. 4. **Cap.** The ledger groups copies by normalized text and evicts only whole least-recently-seen texts (20 per actor), so the cap can never drop part of a campaign. Copies of one text are bounded by the 10-minute window. 5. **Seed failures.** `spamCache.seed` now runs outside the classifier try block. A failure logs `spam_cache_seed_failed` and never becomes `classification_failed`. 6. **Tests.** Ledger unit tests cover the post-time window, edit state and the cap. New bot-level tests cover a cache-hit copy counting towards a campaign, a cache-hit trigger with model-kept earlier copies (per-copy stats source), separation between chats with each delete's `chat_id`, the post-time window, never retrying an edited handled copy, and seed failure isolation. `bun run check` passes locally (typecheck + 492 pass / 285 skipped DB/live tests / 0 fail). Three of the new bot tests fail on `main`. The chat-separation test is regression coverage. ### Review follow-ups (Astra, all rounds CLEAN) - A campaign needs its ≥3 distinct posts to fit inside one <10-minute post-time window with the current copy (linear two-pointer sweep). An edit can no longer stretch a campaign to ~20 minutes. - Copies are retained for 2× the window after being seen, so processing backlogs and late edits keep their state. Matching still uses post times. - Handled copies drop their audit snapshot. A handled copy edited out of eligibility and back keeps its handled state (short-lived tombstone), so a failed delete is not retried. - `repeat_campaign_detected` is logged only when something is pending. - The 20-text cap trade-off is documented: losing a text takes 20 other distinct 40+ character texts from one actor inside the window. - Accepted limitation: handled state lasts until 2× the window after the copy was first seen, so at most one bounded retry of a failed delete is possible after that.
Sign in to join this conversation.
No description provided.