Skip to content

fix(reply): deliver final reply when queued follow-up claims session; scope dedupe to routed thread#90943

Merged
steipete merged 28 commits into
openclaw:mainfrom
sandieman2:fix/slack-thread-reply-silent-drop
Jun 14, 2026
Merged

fix(reply): deliver final reply when queued follow-up claims session; scope dedupe to routed thread#90943
steipete merged 28 commits into
openclaw:mainfrom
sandieman2:fix/slack-thread-reply-silent-drop

Conversation

@sandieman2

@sandieman2 sandieman2 commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Summary

Fix silent final-reply drops when a queued follow-up claims the same session, and make message-tool/final-reply dedupe use the actual delivered route rather than only the ambient channel.

The repaired stack now:

  • keeps the dispatch-owned final reply authoritative while queued work drains
  • preserves channel, account, target, chat type, and thread evidence through queue collection and restart paths
  • scopes dedupe to provider-normalized delivery routes
  • carries delivered message/thread receipts through embedded, Codex, and Copilot tool paths
  • accepts Slack DM aliases consistently across CLI, tools, runners, outbound delivery, and transcript mirroring
  • waits for unsettled Mattermost delivery work before releasing the reply lane
  • exposes additive gateway messaging context for harnesses without leaking private tool receipts

This consolidates the stronger route/thread handling from #90045 and covers the failure reports in #80715 and #92443.

Verification

  • Fresh maintainer autoreview: clean, no accepted/actionable findings
  • Focused Vitest run: 20 files, 786 tests passed
  • Queue rotation regression: 56 tests passed
  • pnpm build
  • pnpm plugin-sdk:api:check
  • git diff --check
  • Testbox changed-lane gate: https://github.com/openclaw/openclaw/actions/runs/27504083545
  • CLI route proof:
    • OPENCLAW_STATE_DIR=<temp> pnpm openclaw message send --channel slack --target user:U123 --message "thread route probe" --dry-run --json
    • observed normalized target U123, handled by core
  • Live Slack proof:
    • direct OpenClaw send/read/delete preserved both quote blocks, inline code styling, and the trailing marker
    • agent --deliver --reply-channel slack reported success; readback preserved the same complete rich-text structure

Real behavior proof

Behavior addressed: Final replies could disappear when queued follow-ups claimed the session, and equivalent Slack targets or cross-thread sends could be deduped against the wrong route.

Real environment tested: Current OpenClaw checkout on Node, Testbox changed-lane validation, and a real Slack workspace through both direct OpenClaw sending and agent delivery.

Exact steps or command run after this patch: Ran the focused 20-file Vitest command, the queue rotation regression, pnpm build, pnpm plugin-sdk:api:check, Testbox pnpm check:changed, the Slack user:U123 dry-run command above, a direct Slack send/read/delete, and an agent delivery/read/delete.

Evidence after fix: 786 focused tests and the 56-test queue regression passed; build and plugin API checks passed; CLI normalized user:U123 to U123; Testbox run is linked above. Redacted live output:

{
  "agentDelivery": {
    "deliverySucceeded": true,
    "deliveryStatus": {
      "status": "sent",
      "succeeded": true
    }
  },
  "slackReadback": {
    "quoteBlocks": 2,
    "inlineCodeStyle": true,
    "endMarkerPresent": true
  },
  "cleanup": {
    "temporaryMessagesDeleted": true
  }
}

Observed result after fix: Final delivery retains its owned lane, queued work waits for delivery settlement, dedupe compares the provider-resolved channel/thread route, and both live Slack delivery paths returned complete visible content.

What was not tested: No cross-OS live Slack delivery was run; Testbox covers the Linux changed-lane checks.

@openclaw-barnacle openclaw-barnacle Bot added size: S triage: needs-real-behavior-proof Candidate: external PR needs after-fix proof from a real setup. labels Jun 6, 2026
@clawsweeper

clawsweeper Bot commented Jun 6, 2026

Copy link
Copy Markdown
Contributor

Codex review: needs real behavior proof before merge. Reviewed June 14, 2026, 11:51 AM ET / 15:51 UTC.

Summary
The PR changes reply delivery routing, queued follow-up admission, route/thread-aware dedupe, Slack/Mattermost threading, Codex/Copilot message-tool context, gateway schema, and plugin SDK exports for channel reply reliability.

Reproducibility: yes. source-reproducible: the linked reports describe transcript-success/platform-delivery mismatch, latest release still has the route/thread dedupe gap, and PR-head CI reproduces a new routed ACP TypeError. I did not see live after-fix Slack or Mattermost proof.

Review metrics: 2 noteworthy metrics.

  • PR-head failed lanes: 4 failed lanes. The current head fails lint, bundled-extension lint, plugin SDK, and auto-reply dispatch validation before merge.
  • SDK/API surface movement: 1 API baseline hash changed; 4 plugin-sdk files touched; 1 gateway schema changed. The branch changes public or plugin-facing contracts, so compatibility and documentation review matter beyond unit tests.

Stored data model
Persistent data-model change detected: persistent cache schema: extensions/mattermost/src/mattermost/send.test.ts, serialized state: src/auto-reply/reply/queue/enqueue.ts, serialized state: src/auto-reply/reply/queue/state.test.ts, serialized state: src/auto-reply/reply/queue/state.ts, unknown-data-model-change: src/auto-reply/reply/queue/state.test.ts, unknown-data-model-change: src/auto-reply/reply/queue/state.ts, and 3 more. Confirm migration or upgrade compatibility proof before merge.

Merge readiness
Overall: 🧂 unranked krab
Proof: 🧂 unranked krab
Patch quality: 🧂 unranked krab
Result: blocked until stronger real behavior proof is added.

Overall follows the weaker of proof and patch quality, so missing proof can cap an otherwise strong patch.

Rank-up moves:

  • Guard loaded channel plugins without config and clear the failed dispatch, lint, and plugin SDK lanes.
  • [P1] Add redacted live Slack or Mattermost proof for queued follow-up final delivery and cross-thread dedupe behavior.
  • Get maintainer confirmation or docs for the new plugin SDK and gateway context fields.

Proof guidance:

  • [P1] Needs stronger real behavior proof before merge: The PR body provides tests, Testbox, build/API checks, and a Slack dry-run, but explicitly says no live Slack message was posted; add redacted live Slack or Mattermost proof and update the PR body to trigger re-review, or ask a maintainer to comment @clawsweeper re-review.

Mantis proof suggestion
A real Slack transport smoke would materially improve confidence for this user-visible reply-delivery and route-dedupe fix. A maintainer can ask Mantis to capture proof by posting a new PR comment that starts with the OpenClaw Mantis account mention, followed by:

slack desktop smoke: verify a Slack thread final reply still delivers after a queued same-thread follow-up and that cross-thread message-tool dedupe does not suppress a later final reply.

Risk before merge

  • [P1] PR-head CI currently fails on a routed ACP dispatch TypeError and Mattermost lint errors, so the branch is not safe to land as-is.
  • [P1] No after-fix live Slack or Mattermost transport proof is present for the user-visible silent-drop path; tests, Testbox, and dry-run normalization do not prove chat delivery or queued follow-up settlement.
  • [P1] The diff expands plugin-facing channel, SDK, and gateway context contracts, which is compatibility-sensitive for installed plugins and external SDK users.
  • [P1] fix: keep threaded Slack message sends route-bound for reply dedupe #90045 still overlaps the route-dedupe/message-tool evidence area, so maintainers should choose one canonical landing path.

Maintainer options:

  1. Repair and prove before merge (recommended)
    Guard loaded channel plugins without config, clear the failed lanes, add live redacted transport proof, and confirm the SDK contract before landing this branch.
  2. Narrow the SDK surface first
    If the new fields are only needed internally, keep them off public plugin SDK/gateway surfaces and prove the route/thread fix through existing contracts.
  3. Pick the canonical dedupe branch
    If maintainers choose the smaller overlapping branch instead, port only this PR's unique validated pieces there and pause this broad branch.

Next step before merge

  • [P1] Human review is needed because the code blocker is concrete, but the remaining gate also requires contributor/live transport proof and a maintainer SDK-contract decision.

Security
Cleared: The diff changes TypeScript runtime, protocol, SDK, generated API baseline hashes, and tests, with no workflows, dependency files, lockfiles, package scripts, or secret-handling changes found.

Review findings

  • [P1] Guard loaded channel plugins before reading config — src/auto-reply/reply/reply-threading.ts:120
Review details

Best possible solution:

Repair the current head, prove the fix in a live redacted Slack or Mattermost transport flow, and land one maintainer-approved route/thread reply fix with the intended SDK contract documented or narrowed.

Do we have a high-confidence way to reproduce the issue?

Yes, source-reproducible: the linked reports describe transcript-success/platform-delivery mismatch, latest release still has the route/thread dedupe gap, and PR-head CI reproduces a new routed ACP TypeError. I did not see live after-fix Slack or Mattermost proof.

Is this the best way to solve the issue?

No, not yet: the route/thread-aware owner boundary is plausible, but the current implementation crashes ACP routed delivery and still needs live transport proof plus maintainer approval of the expanded SDK/gateway contract.

Full review comments:

  • [P1] Guard loaded channel plugins before reading config — src/auto-reply/reply/reply-threading.ts:120
    resolveReplyDeliveryAccountId calls plugin.config.defaultAccountId after getChannelPlugin(), but loaded channel registry entries can be metadata-only and getChannelPlugin prefers them over bundled fallbacks. PR-head CI already crashes routed ACP dispatch with this TypeError before a reply can be delivered, so guard plugin.config or use a config-capable resolver before reading account defaults.
    Confidence: 0.91

Overall correctness: patch is incorrect
Overall confidence: 0.88

AGENTS.md: found and applied where relevant.

Codex review notes: model internal, reasoning high; reviewed against 2e240e772b83.

Label changes

Label justifications:

  • P1: The PR targets urgent user-visible final reply loss and Slack session stalls affecting real channel workflows.
  • merge-risk: 🚨 message-delivery: The diff changes reply admission, delivery settlement, route dedupe, and transport thread evidence that decide whether final replies are sent, suppressed, duplicated, or misrouted.
  • merge-risk: 🚨 compatibility: The diff expands plugin-facing SDK/channel/gateway context fields and assumes more of loaded channel plugin runtime shape, which can affect installed plugins and upgrades.
  • rating: 🧂 unranked krab: Overall readiness is 🧂 unranked krab; proof is 🧂 unranked krab and patch quality is 🧂 unranked krab.
  • status: 📣 needs proof: The PR needs real behavior proof before ClawSweeper can clear the contributor ask. Needs stronger real behavior proof before merge: The PR body provides tests, Testbox, build/API checks, and a Slack dry-run, but explicitly says no live Slack message was posted; add redacted live Slack or Mattermost proof and update the PR body to trigger re-review, or ask a maintainer to comment @clawsweeper re-review.
Evidence reviewed

What I checked:

  • PR-head source blocker: PR head adds resolveReplyDeliveryAccountId and reads plugin.config.defaultAccountId without guarding that the active channel plugin has a config adapter. (src/auto-reply/reply/reply-threading.ts:120, ab6587c04b76)
  • Crash call path: ACP routed dispatch calls resolveReplyDeliveryAccountId before creating the reply delivery context, so the unguarded config read can abort delivery before a reply is posted. (src/auto-reply/reply/dispatch-acp-delivery.ts:216, ab6587c04b76)
  • Loaded channel shape: Current main's loaded channel runtime shape only guarantees id, meta, messaging, capabilities, and conversationBindings; it does not guarantee config. (src/plugins/channel-registry-state.types.ts:1, 2e240e772b83)
  • Loaded plugins take precedence: Current main returns a loaded channel plugin before the bundled fallback, making the metadata-only loaded-plugin shape relevant to runtime resolution. (src/channels/plugins/registry.ts:49, 2e240e772b83)
  • PR-head validation failed: PR-head CI failed check-lint, check-additional-extension-bundled, checks-node-agentic-plugin-sdk, and checks-node-auto-reply-reply-dispatch; failed logs include the resolveReplyDeliveryAccountId TypeError and Mattermost unbound-method lint errors. (ab6587c04b76)
  • Real transport proof missing: The PR body lists focused tests, build/API checks, Testbox, and a Slack dry-run command, but explicitly says no live message was posted into a maintainer Slack workspace from this branch. (ab6587c04b76)

Likely related people:

  • steipete: GitHub path history shows repeated recent work on reply dedupe, route helpers, plugin SDK/channel contracts, Slack route normalization, and channel target resolution adjacent to this PR. (role: feature owner; confidence: high; commits: 72f6016ce596, eb66def65604, 25c0699fe909; files: src/auto-reply/reply/reply-payloads-dedupe.ts, src/channels/plugins/types.core.ts, extensions/slack/src/channel.ts)
  • vincentkoc: Recent current-main history includes messaging dedupe thread-id work and reply dedupe performance work in the same central files. (role: recent dedupe contributor; confidence: high; commits: 642e1dfcdfb1, 16b0a6202c69, e2524e043865; files: src/auto-reply/reply/reply-payloads-dedupe.ts, src/agents/embedded-agent-subscribe.tools.ts, src/channels/plugins/types.core.ts)
  • TurboTheTurtle: Recent Slack reply transcript persistence touched the same reply-payload dedupe file and visible Slack reply path that this PR is changing. (role: recent adjacent Slack reply contributor; confidence: medium; commits: 7c088045413f; files: src/auto-reply/reply/reply-payloads-dedupe.ts)
  • Dallin Romney: Current-main blame for src/auto-reply/reply/reply-threading.ts points to a broad recent QA/evidence commit, which makes this person a low-confidence routing candidate for that file only. (role: recent adjacent file contributor; confidence: low; commits: 1affe4fcdf5f; files: src/auto-reply/reply/reply-threading.ts)
What the crustacean ranks mean
  • 🦀 challenger crab: rare, exceptional readiness with strong proof, clean implementation, and convincing validation.
  • 🦞 diamond lobster: very strong readiness with only minor maintainer review expected.
  • 🐚 platinum hermit: good normal PR, likely mergeable with ordinary maintainer review.
  • 🦐 gold shrimp: useful signal, but proof or patch confidence is still limited.
  • 🦪 silver shellfish: thin signal; proof, validation, or implementation needs work.
  • 🧂 unranked krab: not merge-ready because proof is missing/unusable or there are serious correctness or safety concerns.
  • 🌊 off-meta tidepool: rating does not apply to this item.

Shiny media proof means a screenshot, video, or linked artifact directly shows the changed behavior. Runtime, network, CSP, and security claims still need visible diagnostics.

How this review workflow works
  • ClawSweeper keeps one durable marker-backed review comment per issue or PR.
  • Re-runs edit this comment so the latest verdict, findings, and automation markers stay together instead of adding duplicate bot comments.
  • A fresh review can be triggered by eligible @clawsweeper re-review comments, exact-item GitHub events, scheduled/background review runs, or manual workflow dispatch.
  • PR/issue authors and users with repository write access can comment @clawsweeper re-review or @clawsweeper re-run on an open PR or issue to request a fresh review only.
  • Maintainers can also comment @clawsweeper review to request a fresh review only.
  • Fresh-review commands do not start repair, autofix, rebase, CI repair, or automerge.
  • Maintainer-only repair and merge flows require explicit commands such as @clawsweeper autofix, @clawsweeper automerge, @clawsweeper fix ci, or @clawsweeper address review.
  • Maintainers can comment @clawsweeper explain to ask for more context, or @clawsweeper stop to stop active automation.

@clawsweeper clawsweeper Bot added rating: 🦪 silver shellfish Thin PR readiness signal; proof, validation, or implementation needs work. status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask. P1 High-priority user-facing bug, regression, or broken workflow. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. labels Jun 6, 2026
@steipete steipete self-assigned this Jun 13, 2026
sandieman2 and others added 21 commits June 14, 2026 16:26
… scope dedupe to routed thread

Two core bugs caused composed replies to be silently dropped (no delivery,
no error) when a second message arrived in the same thread mid-run:

1. dispatch-from-config: ensureDispatchReplyOperation only kept the
   dispatch-owned operation authoritative while it had no result. Once
   runReplyAgent completed the operation to drain queued follow-ups, a
   second same-thread inbound could claim the session and the first final
   reply would try to re-acquire the lane instead of finishing delivery,
   deadlocking behind the queued work. Keep the dispatch-owned operation
   authoritative through final delivery.

2. reply-payloads-dedupe: messaging-tool reply dedupe compared only the
   channel target, not the routed thread, so a send in one thread could
   suppress a later reply in a different thread. Thread the routed thread
   id through buildReplyPayloads + follow-up delivery and only fall back to
   channel-only matching for providers without a thread-aware suppression
   matcher when neither side carries thread evidence.

Adds regression tests; existing Telegram topic-suppression behavior is
preserved by gating the thread guard to providers lacking a plugin matcher.
crh-code pushed a commit to crh-code/openclaw that referenced this pull request Jun 18, 2026
… send results (openclaw#93291)

* fix(reply): preserve pending thread evidence when reconciling partial send results

extractMessagingToolSendResult re-derived threadId/threadImplicit/threadSuppressed
straight from the provider result. Mattermost is the only production provider that
implements extractToolSendResult, and for an implicitly threaded send it reports only
{ to }, so the reconciler overwrote the correct pending thread evidence with undefined.
That defeated same-thread reply suppression in reply-payloads dedupe and delivered the
agent's final reply twice in the thread, on both the native and Codex harnesses.

A partial provider result now keeps the pending thread evidence it does not speak to: a
provider-reported threadId still wins (and clears the implicit flag), but an absent one
no longer erases the pending threadId/threadImplicit/threadSuppressed.

Regression introduced by c67dc59 (openclaw#90943).

* test(reply): use a core-local stub provider instead of the bundled Mattermost import

The reconcile-thread regression test deep-imported extensions/mattermost from a
core test, which trips the core/extension package boundary (boundary-invariants
"keeps core tests off bundled extension deep imports", extension-test-boundary,
and check-tsgo-core-boundary pulling extensions/mattermost transitively).

Replace it with a core-local channel test plugin that reproduces the same
contract: an implicit-threading extractToolSend, a partial extractToolSendResult
that reports only { to, threadId? }, and no targetsMatchForReplySuppression
matcher. The test now exercises the generic reconciler contract with no
extension dependency. It still fails on pristine main and passes with the fix.

* fix(reply): reconcile thread evidence atomically

---------

Co-authored-by: Vincent Koc <[email protected]>
badgerbees pushed a commit to badgerbees/openclaw that referenced this pull request Jul 8, 2026
… scope dedupe to routed thread (openclaw#90943)

* fix(reply): deliver final reply when queued follow-up claims session; scope dedupe to routed thread

Two core bugs caused composed replies to be silently dropped (no delivery,
no error) when a second message arrived in the same thread mid-run:

1. dispatch-from-config: ensureDispatchReplyOperation only kept the
   dispatch-owned operation authoritative while it had no result. Once
   runReplyAgent completed the operation to drain queued follow-ups, a
   second same-thread inbound could claim the session and the first final
   reply would try to re-acquire the lane instead of finishing delivery,
   deadlocking behind the queued work. Keep the dispatch-owned operation
   authoritative through final delivery.

2. reply-payloads-dedupe: messaging-tool reply dedupe compared only the
   channel target, not the routed thread, so a send in one thread could
   suppress a later reply in a different thread. Thread the routed thread
   id through buildReplyPayloads + follow-up delivery and only fall back to
   channel-only matching for providers without a thread-aware suppression
   matcher when neither side carries thread evidence.

Adds regression tests; existing Telegram topic-suppression behavior is
preserved by gating the thread guard to providers lacking a plugin matcher.

* fix(reply): preserve threaded message delivery evidence

* fix(reply): dedupe final payloads by delivery route

* fix(slack): preserve native send thread evidence

* fix(reply): preserve explicit reply thread evidence

* fix(reply): align explicit reply route dedupe

* fix(reply): preserve delivery lane through final dispatch

* fix(mattermost): preserve threaded tool send routes

* chore(plugin-sdk): refresh API baseline

* fix(reply): align final delivery route dedupe

* fix(reply): gate followups on final delivery

* fix(reply): keep send receipts private

* fix(reply): infer implicit message provider

* fix(reply): align routed threading policy

* fix(reply): preserve queued delivery context

* fix(reply): hydrate queued system event routes

* fix(reply): hydrate queued execution routes

* fix(reply): scope final delivery barriers

* fix(slack): preserve DM target aliases

* fix(reply): mirror resolved source thread routes

* fix(mattermost): retain delayed delivery barrier

* fix(codex): separate message routing from tool policy

* fix(reply): consume normalized Slack DM targets once

* fix(slack): remove stale target alias

* style(reply): satisfy changed lint gates

* fix(mattermost): preserve explicit reply targets

* test: align Slack reply branch checks

* fix(reply): persist overflow summaries to admitted session

---------

Co-authored-by: Peter Steinberger <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

agents Agent runtime and tooling app: web-ui App: web-ui channel: mattermost Channel integration: mattermost channel: slack Channel integration: slack docs Improvements or additions to documentation extensions: codex extensions: copilot gateway Gateway runtime merge-risk: 🚨 compatibility 🚨 May break existing users, config, migrations, defaults, or upgrade paths. merge-risk: 🚨 message-delivery 🚨 May drop, duplicate, misroute, suppress, or wrongly target messages. P1 High-priority user-facing bug, regression, or broken workflow. proof: supplied External PR includes structured after-fix real behavior proof. rating: 🧂 unranked krab Not merge-ready due to missing proof or serious correctness/safety concerns. size: XL status: 📣 needs proof The PR needs real behavior proof before ClawSweeper can clear the contributor ask.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants