Skip to content

fix(embedded): scope messaging dedupe suppression to originating target#26889

Closed
kevinwildenradt wants to merge 62 commits intoopenclaw:mainfrom
kevinwildenradt:codex/fix-messaging-dedupe-target-scope
Closed

fix(embedded): scope messaging dedupe suppression to originating target#26889
kevinwildenradt wants to merge 62 commits intoopenclaw:mainfrom
kevinwildenradt:codex/fix-messaging-dedupe-target-scope

Conversation

@kevinwildenradt
Copy link
Copy Markdown

@kevinwildenradt kevinwildenradt commented Feb 25, 2026

Summary

Fix false duplicate suppression in embedded subscribe when the message tool sends to a different destination than the current conversation.

Problem

In multi-chat flows (notably BlueBubbles), a run can send text via the message tool to chat B while still answering chat A.
If the assistant answer matches the tool-sent text, current dedupe logic may suppress the reply in chat A, making the agent appear stalled.

Root Cause

Duplicate suppression for block replies (text_end and message_end paths) used normalized sent text without checking whether the message tool send target matched the current run’s originating target.

Changes

  • Pass routing context into embedded subscribe:
    • messageProvider
    • originatingTo
    • accountId
  • Gate duplicate suppression with shouldSuppressMessagingToolReplies(...) so suppression only applies in-scope.
  • Keep backward-compatible behavior when routing context is missing (legacy suppression behavior).
  • Add regression tests for cross-target sends:
    • message_end should not be suppressed
    • text_end should not be suppressed

Repro (before)

  1. Start a run from BlueBubbles chat A.
  2. Tool call sends message text X to chat B.
  3. Assistant emits final text X.
  4. Reply in chat A can be suppressed.

Behavior After Fix

Suppression only occurs when the message tool target matches the run’s originating target. Cross-target sends no longer silence the origin chat reply.

Validation

  • Added/updated unit tests in:
    • src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.suppresses-message-end-block-replies-message-tool.test.ts
  • Local tests were not executed in this environment originally because pnpm/corepack was unavailable; please rely on CI and/or run the targeted command locally.

Greptile Summary

Scopes messaging tool duplicate suppression to only apply when the message tool target matches the originating conversation target. Previously, if an agent sent a message to chat B while answering chat A, and the text matched, the reply in chat A would incorrectly be suppressed.

Key changes:

  • Added routing context (messageProvider, originatingTo, accountId) to subscribeEmbeddedPiSession parameters
  • Introduced shouldSuppressMessagingToolReplies helper that checks if any sent target matches the originating target
  • Modified duplicate suppression logic in both emitBlockChunk (text_end path) and handleMessageEnd (message_end path) to only suppress when targets match
  • Maintains backward compatibility by defaulting to legacy behavior when routing context is missing
  • Added regression tests for cross-target scenarios

Confidence Score: 5/5

  • Safe to merge - well-tested bug fix with proper backward compatibility
  • The fix correctly scopes duplicate suppression to same-target sends, includes comprehensive test coverage for both text_end and message_end paths, maintains backward compatibility when routing context is unavailable, and follows existing code patterns. The logic is sound and the implementation is clean.
  • No files require special attention

Last reviewed commit: 02cf8bd

(2/5) Greptile learns from your feedback when you react with thumbs up/down!

@openclaw-barnacle openclaw-barnacle bot added agents Agent runtime and tooling size: S labels Feb 25, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 02cf8bd0ef

ℹ️ 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".

@openclaw-barnacle openclaw-barnacle bot added gateway Gateway runtime size: M and removed size: S labels Feb 26, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1de25fb8ce

ℹ️ 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".

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9bc990f7af

ℹ️ 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".

aidenwildenbadt pushed a commit to aidenwildenbadt/openclaw that referenced this pull request Mar 3, 2026
# Conflicts:
#	src/agents/pi-embedded-subscribe.tools.extract.test.ts
#	src/auto-reply/reply/reply-payloads.ts
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 9532e60eb2

ℹ️ 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".

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0b7ee11a02

ℹ️ 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 target = parseTelegramTarget(params.targetKey);
const rawTargetThreadId =
typeof params.targetThreadId === "string" ? params.targetThreadId.trim() : undefined;
if (rawTargetThreadId && !/^\d+$/.test(rawTargetThreadId)) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Accept scoped Telegram thread IDs in dedupe matching

The new malformed-thread guard treats any targetThreadId that is not plain digits as off-target, but Telegram routing already accepts scoped thread IDs in the form <chatId>:<threadId> (parseTelegramThreadId in src/telegram/outbound-params.ts). Fresh evidence is that this commit introduced the strict ^\d+$ check, so valid scoped thread IDs are now rejected here. When a tool send uses that valid format, dedupe matching incorrectly returns false and the origin reply is emitted again as a duplicate.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — addressed in c4268c5b4 (fix(reply): accept scoped telegram thread ids in dedupe).

targetsMatchForSuppression(...) now parses explicit Telegram threadId with the shared parseTelegramThreadId(...) helper, so valid scoped ids like <chatId>:<threadId> are treated as explicit topic ids (matching runtime routing semantics).

Behavior now:

  • threadId="-100123:77" matches topic 77 and allows suppression.
  • threadId="-100123:88" remains off-target and does not suppress.
  • malformed explicit values still return off-target (no chat-level fallback).

Coverage added in src/auto-reply/reply/reply-payloads.test.ts for both scoped-match and scoped-cross-topic cases.

Validation:

  • pnpm vitest run --config vitest.config.ts src/auto-reply/reply/reply-payloads.test.ts
  • pnpm check

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: c4268c5b4a

ℹ️ 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 targetNormalizationProvider = isGenericMessageProvider ? provider : targetProvider;
const targetKey = normalizeTargetForProvider(targetNormalizationProvider, target.to);
const targetKey = normalizeTargetForProvider(targetProvider, target.to);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Compare suppression targets after route resolution

The new target-scoped dedupe check compares originatingTo against normalizeTargetForProvider(..., target.to) only, but target.to is captured from tool arguments before runtime target resolution. In valid flows where the message tool sends to the current chat via a resolvable alias (for example a directory name/handle), runMessageAction can resolve and rewrite the outbound target in resolveActionTarget(...), yet this suppression check still sees the unresolved alias and returns false. That causes same-chat sends to be treated as off-target and emits a duplicate block reply instead of suppressing it.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — addressed in d077b1b12 (fix(embedded): use resolved send target for dedupe scope).

I updated embedded subscribe tracking so target-scoped dedupe can use post-resolution routing data instead of only the pre-execution tool args:

  • Added extractMessagingToolSendFromResult(...) to extract send route metadata from tool result payloads (details or JSON text payload).
  • In handleToolExecutionEnd(...), pending messaging target metadata is now enriched from resolved result metadata (fallback to post-call args), then committed for suppression matching.

This preserves scoped dedupe when a message send uses an alias/handle that resolves to the current chat at runtime.

Coverage added:

  • src/agents/pi-embedded-subscribe.handlers.tools.test.ts
    • updates pending messaging target from resolved tool result payload.
  • src/agents/pi-embedded-subscribe.tools.extract.test.ts
    • extracts normalized send target from JSON tool output/details.

Validation:

  • pnpm vitest run --config vitest.config.ts src/agents/pi-embedded-subscribe.tools.extract.test.ts src/agents/pi-embedded-subscribe.handlers.tools.test.ts src/auto-reply/reply/reply-payloads.test.ts
  • pnpm check

aidenwildenbadt pushed a commit to aidenwildenbadt/openclaw that referenced this pull request Mar 3, 2026
# Conflicts:
#	src/agents/pi-embedded-subscribe.handlers.messages.ts
#	src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.suppresses-message-end-block-replies-message-tool.test.ts
#	src/agents/pi-embedded-subscribe.ts
#	src/auto-reply/reply/reply-payloads.test.ts
#	src/auto-reply/reply/reply-payloads.ts
@kevinwildenradt
Copy link
Copy Markdown
Author

Addressed current CI failure (secrets / pnpm-audit-prod) in aa51117.\n\nWhat changed:\n- Bumped extension peer floor for OpenClaw to patched range:\n - extensions/googlechat/package.json: openclaw >=2026.3.2\n - extensions/memory-core/package.json: openclaw >=2026.3.2\n- Refreshed pnpm lock entries so those importers resolve openclaw 2026.3.2 instead of 2026.3.1.\n\nValidation run locally:\n- pnpm audit --prod --audit-level=high (passes; only 1 low remains)\n- pnpm check\n\nNo review threads were resolved in this update.

@openclaw-barnacle openclaw-barnacle bot added channel: googlechat Channel integration: googlechat extensions: memory-core Extension: memory-core labels Mar 3, 2026
Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: aa5111746b

ℹ️ 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".

Comment on lines +207 to +208
if (origin.messageThreadId && !targetThreadId) {
return true;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Treat explicit Telegram threadId 0 as off-target

targetsMatchForSuppression currently treats targetThreadId with a falsy check, so an explicit threadId that parses to 0 is handled as “no thread” and falls into the auto-threading match path. In Telegram runs originating from a topic, this can suppress the origin reply even when the tool send did not stay in that topic (for example, an explicit threadId: "0" that downstream send logic treats as invalid/off-topic). This regresses target-scoped dedupe by turning an explicit thread choice into chat-level fallback matching.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in cf1013727 (fix(reply): treat explicit telegram threadId 0 as explicit target).\n\ntargetsMatchForSuppression(...) now treats an explicit threadId as explicit even when it parses to 0, so topic-origin flows no longer fall through to chat-level auto-thread fallback for threadId: "0".\n\nBehavior now:\n- topic origin + explicit threadId: "0" => off-target (no suppression)\n- topic origin + missing threadId => existing auto-thread fallback unchanged\n\nAlso fixed current CI failure in src/plugins/commands.test.ts by aligning expected normalized command metadata (acceptsArgs: false).\n\nValidation run locally:\n- pnpm exec vitest run --config vitest.config.ts src/plugins/commands.test.ts src/auto-reply/reply/reply-payloads.test.ts

@kevinwildenradt
Copy link
Copy Markdown
Author

kevinwildenradt commented Mar 3, 2026

Pushed structural dedupe cleanup in commit 4feb765 to reduce edge-case regressions across follow-up reviews.

What changed:

  • Added a canonical MessagingToolSentRecord model (textNormalized + resolved target + target source).
  • Added shared suppression gate shouldSuppressMessagingToolBlockReply(...) in src/agents/pi-embedded-subscribe.dedupe.ts.
  • Switched both suppression paths to use that same gate:
    • stream chunk path (emitBlockChunk)
    • message_end block-reply path
  • Updated tool_execution_end to persist resolved target metadata even when start args omitted explicit target (result/adjusted-args extraction path).

Validation:

  • Targeted vitest suites for subscribe dedupe + extraction + commands all pass.
  • pnpm check passes locally.

Review threads remain unresolved as requested.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 4feb7655f0

ℹ️ 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".

return true;
}

const sentTargets = collectSentTargets(params.sentRecords);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Scope suppression targets to the duplicated message text

This function checks hasDuplicate for the current normalizedText, but then evaluates routing scope against collectSentTargets(params.sentRecords) from the entire run. In mixed runs, a different message sent to the origin target can make shouldSuppressMessagingToolReplies(...) return true and suppress a reply whose duplicated text was actually only sent off-target. To avoid cross-target suppression in this case, target matching should only consider records whose textNormalized matches the candidate reply text.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in 677134971 (fix(embedded): scope dedupe target checks to matching text records).

I updated shouldSuppressMessagingToolBlockReply(...) to scope routing checks to matching-text records only:

  • compute matchingRecords for the candidate normalizedText
  • evaluate shouldSuppressMessagingToolReplies(...) using targets extracted from those matching records only

This prevents in-scope sends of different text from suppressing a duplicated off-target text.

Coverage added in src/agents/pi-embedded-subscribe.dedupe.test.ts:

  • scopes routing checks to records for the duplicated text

Validation:

  • pnpm exec vitest run --config vitest.config.ts src/agents/pi-embedded-subscribe.dedupe.test.ts src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.suppresses-message-end-block-replies-message-tool.test.ts
  • pnpm check

// Keep fallback suppression only for matching text from non-explicit targets.
return params.sentRecords.some(
(record) =>
record.textNormalized === params.normalizedText && record.targetSource !== "explicit",
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Skip fallback suppression for inferred records with targets

The final fallback suppresses any matching record where targetSource !== "explicit", but "inferred" records can still have a concrete resolved target (for example when handleToolExecutionEnd derives one from afterToolCallArgs/result without a pending explicit target). If that inferred target is off-target, routing checks correctly return false and this fallback then suppresses anyway, reintroducing false suppression. The fallback should be restricted to records that truly lack target metadata rather than all non-explicit sources.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 677134971 as part of the same dedupe-gate update.

I narrowed the fallback path so it only suppresses matching-text records that truly lack target metadata (!record.target).

Previously fallback used targetSource !== "explicit", which could incorrectly include inferred records that already had resolved off-target metadata. With the new logic:

  • inferred records with concrete off-target target metadata no longer fallback-suppress
  • targetless matching records still preserve legacy inferred-target fallback behavior

Coverage added in src/agents/pi-embedded-subscribe.dedupe.test.ts:

  • does not fallback-suppress inferred records when they include off-target metadata
  • keeps fallback suppression for matching records without target metadata

Validation:

  • targeted vitest command above + pnpm check both pass locally.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in 677134971 as part of the same dedupe-gate update.

I narrowed the fallback path so it only suppresses matching-text records that truly lack target metadata (!record.target).

Previously fallback used targetSource !== "explicit", which could incorrectly include inferred records that already had resolved off-target metadata. With the new logic:

  • inferred records with concrete off-target target metadata no longer fallback-suppress
  • targetless matching records still preserve legacy inferred-target fallback behavior

Coverage added in src/agents/pi-embedded-subscribe.dedupe.test.ts:

  • does not fallback-suppress inferred records when they include off-target metadata
  • keeps fallback suppression for matching records without target metadata

Validation:

  • targeted vitest command above + pnpm check both pass locally.

Copy link
Copy Markdown

@chatgpt-codex-connector chatgpt-codex-connector bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 677134971b

ℹ️ 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".

Comment on lines +428 to +430
if (!isToolError && committedTarget) {
ctx.state.messagingToolSentTargets.push(committedTarget);
ctx.trimMessagingToolSent();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Limit target commits to real messaging send actions

This branch records committedTarget for any successful messaging tool call, even when the tool action was not a send. For message actions like poll, the tool result payload still includes channel/to (see src/infra/outbound/message.ts), so extractMessagingToolSendFromResult(...) infers a target and this code appends it to messagingToolSentTargets. Downstream suppression (shouldSuppressMessagingToolReplies) can then treat the run as already replied and drop the final assistant reply entirely, causing silent post-action responses for non-send actions.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — fixed in f38f5fe86 (fix(embedded): ignore non-send message actions in dedupe target tracking).

I updated handleToolExecutionEnd(...) so target extraction/commit is gated by isMessagingSendCall, not just isMessagingTool(...):

  • derive isMessagingSendCall from isMessagingToolSendAction(...) using both start and adjusted args
  • only extract/commit committedTarget when that call is a send action

This prevents non-send message actions (for example poll) from contributing dedupe targets via result payloads.

Regression coverage added:

  • src/agents/pi-embedded-subscribe.handlers.tools.test.ts
    • does not commit targets from non-send message actions

Validation:

  • pnpm exec vitest run --config vitest.config.ts src/agents/pi-embedded-subscribe.handlers.tools.test.ts src/agents/pi-embedded-subscribe.dedupe.test.ts src/agents/pi-embedded-subscribe.subscribe-embedded-pi-session.suppresses-message-end-block-replies-message-tool.test.ts
  • pnpm check

@Takhoffman
Copy link
Copy Markdown
Contributor

Superseded by #33786.

The synthesis PR integrates the relevant shared dedupe/routing semantics and tests while avoiding unrelated churn.

@Takhoffman
Copy link
Copy Markdown
Contributor

Thanks again for this work.

This PR was triaged and merged using an AI-assisted review/synthesis workflow.

This landed indirectly via the synthesized PR #33786, and your contribution is credited in the changelog and as a co-author on the merge commit.

Closing this PR as superseded by #33786 (reason: superseded). If anything here looks incorrect or incomplete, reply to reopen and we can reassess.

@Takhoffman Takhoffman closed this Mar 4, 2026
@Takhoffman
Copy link
Copy Markdown
Contributor

Correction for accuracy: the synthesized work is credited in the changelog, but the squash merge commit for #33786 does not include explicit Co-authored-by trailers.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling channel: feishu Channel integration: feishu channel: googlechat Channel integration: googlechat extensions: memory-core Extension: memory-core gateway Gateway runtime size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants