fix: narrow reply-root dedupe to queued reruns#47920
fix: narrow reply-root dedupe to queued reruns#47920livingghost wants to merge 2 commits intoopenclaw:mainfrom
Conversation
Track recent reply roots across queued and routed sends so the same inbound thread does not trigger duplicate agent replies. Surface the reply-root key into message hooks so plugin-side send guards can keep using the same identifier.
Avoid dropping fresh inbound messages that happen to share a reply root, prefer explicit reply targets over thread roots, and only mark a reply root as sent after an outbound delivery actually occurs.
|
Closing and recreating this PR with the repository template body. |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 78264ed959
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| const replyRootId = run.replyRootId?.trim(); | ||
| if (replyRootId) { | ||
| return { kind: "reply-root", value: replyRootId }; |
There was a problem hiding this comment.
Deduplicate queued runs by message id before thread root
Prioritizing replyRootId here causes distinct thread messages to be collapsed as duplicates: get-reply-run.ts now fills replyRootId from inbound reply context, and for Slack that value is the thread root (thread_ts) for every message in the thread (extensions/slack/src/threading.ts), so in active collect/followup queues a second user turn with a different messageId is rejected as already queued. This drops legitimate user input instead of only suppressing true redeliveries.
Useful? React with 👍 / 👎.
Greptile SummaryThis PR narrows reply-root deduplication to only suppress queued/followup re-runs (not fresh inbound messages), deduplicates queued runs by explicit reply targets before falling back to message IDs, and gates the "recently sent" mark on a confirmed outbound delivery result rather than on enqueueing. The changes are well-structured, with a new Key areas to pay attention to:
Confidence Score: 3/5
|
| const recentReplyRootKey = buildRecentSentReplyRootKey({ | ||
| scopeKey: params.replyRootScopeKey ?? params.sessionKey, | ||
| agentId: params.replyRootAgentId ?? resolvedAgentId, | ||
| channel: channelId, | ||
| to, | ||
| accountId: accountId ?? undefined, | ||
| threadId: threadId ?? null, | ||
| replyRootId: params.replyRootId, | ||
| }); |
There was a problem hiding this comment.
Channel key mismatch silently disables dedupe for aliased channels
recentReplyRootKey is built using channelId (the normalized result of normalizeChannelId(channel)), which lowercases and resolves aliases (e.g., "google-chat" → "googlechat", "imsg" → "imessage"). The check side in agent-runner.ts and followup-runner.ts calls buildRecentSentReplyRootKeyForRun(run), which uses the raw run.originatingChannel for that same field.
When originatingChannel contains an alias, the mark key and the check key contain different strings and will never match — reply-root deduplication silently does nothing for those channels.
The existing dedup code in enqueue.ts (buildRecentMessageIdKey) consistently uses raw originatingChannel without normalization to avoid exactly this class of mismatch. Consider passing params.channel (the raw value) instead of the normalized channelId when building recentReplyRootKey, so the mark and check sides stay in sync.
Prompt To Fix With AI
This is a comment left during a code review.
Path: src/auto-reply/reply/route-reply.ts
Line: 173-181
Comment:
**Channel key mismatch silently disables dedupe for aliased channels**
`recentReplyRootKey` is built using `channelId` (the **normalized** result of `normalizeChannelId(channel)`), which lowercases and resolves aliases (e.g., `"google-chat"` → `"googlechat"`, `"imsg"` → `"imessage"`). The *check* side in `agent-runner.ts` and `followup-runner.ts` calls `buildRecentSentReplyRootKeyForRun(run)`, which uses the **raw** `run.originatingChannel` for that same field.
When `originatingChannel` contains an alias, the mark key and the check key contain different strings and will never match — reply-root deduplication silently does nothing for those channels.
The existing dedup code in `enqueue.ts` (`buildRecentMessageIdKey`) consistently uses raw `originatingChannel` without normalization to avoid exactly this class of mismatch. Consider passing `params.channel` (the raw value) instead of the normalized `channelId` when building `recentReplyRootKey`, so the mark and check sides stay in sync.
How can I resolve this? If you propose a fix, please make it concise.
Summary
Test plan