fix(integration-events): dedupe Slack thread-parent across messages+threads trees#175
Conversation
… trees A Slack thread PARENT materializes under BOTH the flat messages/<ts> record AND the threads/<ts> root (thread_ts == parent ts) — two distinct files with distinct revisions, one logical message. slackLogicalChangeFingerprint keyed the threads-root copy as `...:thread:<ts>:...` and the messages copy as `...:message:<ts>:...`, so the two copies produced different injection-dedupe keys and the same message was injected twice (and re-injected as each tree's record was re-committed). Operator observed the same message echoing into the channel repeatedly (one msg logged 59x received / 3x injected). Fix: collapse the thread ROOT (no reply segment) to the message identity so both materializations share one dedupe key and inject once. Thread REPLIES keep their distinct `:reply:<ts>` identity. Regression test added; 74/74 bridge tests pass. Note: an agent still receives ONE copy of its own outbound post (mirror reads it back down); fully suppressing self-echo needs a self-bot-user signal — follow-up. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
|
Your free trial PR review limit of 300 PRs has been reached. Please upgrade your plan to continue using CodeAnt AI. |
|
Warning You have reached your daily quota limit. Please wait up to 24 hours and I will start processing your requests again! |
|
ℹ️ pr-reviewer: review only — no file changes were applied to the PR (nothing to commit after review). The notes below are advisory and were not pushed. Reviewed PR #175 against the current checkout. I did not find a validated breakage requiring code changes. Local validation run: Final PR state observed: Addressed comments
|
Symptom
The operator saw the same Slack message echoing into the channel repeatedly. In
integration-events.log, one message (1780946577_526789) appeared 59× — 25received, 22skipped(de-duped), but 3injected(the visible echoes). The bot's own posts echoed too (compounded once the mounts wentmirrorand read the bot's own writebacks back down).Root cause
A Slack thread parent (
thread_ts == parent ts) materializes under two trees: the flatmessages/<ts>/meta.jsonrecord AND thethreads/<ts>/meta.jsonroot — two distinct files, distinct revisions, but one logical message.slackLogicalChangeFingerprintkeyed them differently:messages/<ts>/meta.json→slack:channels:<ch>:message:<ts>:meta.jsonthreads/<ts>/meta.json→slack:channels:<ch>:thread:<ts>:meta.jsonDifferent fingerprints → different injection-dedupe keys → the same message injects once per tree, and re-injects as each tree's record is re-committed (revision churn). (
event.typeis the constantrelayfile.changedand the separate messages/threads mounts yield the same dedupe path-tail, so the fingerprint was the only differentiator.)Fix
Collapse the thread root (a
threads/<ts>path with noreplies/segment) to the message identity inslackLogicalChangeFingerprint, so both materializations of the parent share one dedupe key and inject once. Thread replies keep their distinct:reply:<ts>identity and still inject.One-line change in the fingerprint + a regression test.
Test
slack thread parent materialized under both messages and threads trees injects once— parent under both trees → 1 injection; a reply → still injects. 74/74 bridge tests pass (node --test src/main/__tests__/integration-event-bridge.test.ts).Residual (follow-up, not in this PR)
An agent still receives one copy of its own outbound post, because
mirrormounts read the bot's own messages back down and the bridge has no self-identity signal to suppress them. Fully killing self-echo needs the workspace's own bot-user id at the bridge — a separate change.Source-only (bridge); activates on the next Pear rebuild+restart.
🤖 Generated with Claude Code