Skip to content

feat(lifecycle): persistent inbound dedup across gateway restarts#29957

Closed
nohat wants to merge 17 commits intoopenclaw:mainfrom
nohat:lifecycle/persistent-dedup-temp
Closed

feat(lifecycle): persistent inbound dedup across gateway restarts#29957
nohat wants to merge 17 commits intoopenclaw:mainfrom
nohat:lifecycle/persistent-dedup-temp

Conversation

@nohat
Copy link
Contributor

@nohat nohat commented Feb 28, 2026

Summary

Stack: merge after #29956 (the diff will be correct once #29956 is merged; until then this shows the combined diff)

Single commit: removes disablePersistentDedupe flag so acceptTurn() uses INSERT OR IGNORE with unique index on dedupe_key.

  • Dedup survives gateway restarts; pruned turns free their dedupe keys
  • Minimal change on top of the turn-tracking PR

Closes #14431
Related: #28941

Test plan

  • pnpm build passes
  • pnpm test passes (dedup persistence tests)
  • Manual: send message, restart gateway, resend same message — verify dedup rejects it

🤖 Generated with Claude Code

nohat and others added 17 commits February 28, 2026 09:34
Replace unbounded file-based delivery queue with queryable SQLite
message_outbox table. Adds TTL/expiry for stale entries, delivery
outcome retention, and one-time legacy file queue import on startup.

Closes openclaw#23777, openclaw#16555, openclaw#29128
…pat layer

Write-ahead delivery pattern: enqueue outbox entry before sending, ack on
success, retry on failure. Continuous outbox worker replaces one-shot
recovery. Plugin channels get durable delivery guarantees via v1/v2
adapter compat layer.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…l and separate ackDelivery errors in recovery
Every inbound message creates a durable turn record in message_turns.
Turn worker detects orphaned turns (accepted but never completed after
crash) and recovers them. Abort commands mark turns as aborted,
preventing re-delivery. Outbox entries are linked to turns for
coordinated finalization.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@openclaw-barnacle openclaw-barnacle bot added channel: bluebubbles Channel integration: bluebubbles channel: discord Channel integration: discord channel: googlechat Channel integration: googlechat channel: imessage Channel integration: imessage channel: line Channel integration: line channel: matrix Channel integration: matrix channel: mattermost Channel integration: mattermost channel: msteams Channel integration: msteams channel: nextcloud-talk Channel integration: nextcloud-talk channel: nostr Channel integration: nostr channel: signal Channel integration: signal channel: slack Channel integration: slack channel: telegram Channel integration: telegram labels Feb 28, 2026
@openclaw-barnacle
Copy link

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

6 similar comments
@openclaw-barnacle
Copy link

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

15 similar comments
@openclaw-barnacle
Copy link

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

@openclaw-barnacle
Copy link

Closing this PR because it looks dirty (too many unrelated or unexpected changes). This usually happens when a branch picks up unrelated commits or a merge went sideways. Please recreate the PR from a clean branch.

Copy link

@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: 32c87109ac

ℹ️ 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 +204 to +205
sendPayload: async (payload, overrides) =>
outbound.sendFinal({

Choose a reason for hiding this comment

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

P1 Badge Keep legacy outbound adapter fallback paths

createPluginHandler now always routes channel-data sends through outbound.sendFinal(...), but dynamically loaded third-party plugins built against the previous SDK may still expose only sendPayload/sendText/sendMedia. In that case this call becomes a runtime TypeError as soon as a payload with channelData is emitted, which breaks outbound delivery for existing plugins after upgrade; keep a runtime fallback for adapters that don't implement sendFinal yet.

Useful? React with 👍 / 👎.

@greptile-apps
Copy link
Contributor

greptile-apps bot commented Feb 28, 2026

Greptile Summary

This PR enables persistent inbound message deduplication across gateway restarts by removing the disablePersistentDedupe flag. The change is minimal (6 lines removed) and builds on the turn-tracking infrastructure from PR #29956.

Key changes:

  • Removes the disablePersistentDedupe = true flag from acceptTurn() function
  • Enables INSERT OR IGNORE with unique index on dedupe_key column
  • Adds comprehensive tests covering deduplication persistence, restart simulation, and pruning behavior

How it works:

  • acceptTurn() now always computes dedupeKey from buildInboundDedupeKey(ctx) (provider + accountId + sessionKey + peerId + threadId + messageId)
  • Database has partial unique index on dedupe_key column (only when NOT NULL)
  • Duplicate messages return accepted: false and are logged but not processed
  • Pruned turns free their dedupe keys, allowing re-acceptance after cleanup
  • Fallback to in-memory dedup cache if database write fails (fail-open with warning)

Testing:
All scenarios covered including first accept, duplicate rejection, cross-restart persistence, messages without dedupe keys, and pruning behavior.

Confidence Score: 5/5

  • This PR is safe to merge with minimal risk
  • The change is minimal (6 lines), well-tested with comprehensive coverage, and has defensive fallback behavior. The implementation correctly uses SQLite's INSERT OR IGNORE with a partial unique index, and the integration with existing code (dispatch.ts, recovery workers) is correct. No logical errors, security issues, or performance concerns identified.
  • No files require special attention - the actual PR changes only 2 files (turns.ts and turns.test.ts). The 66 other files shown in the diff are from the stacked PR feat(lifecycle): inbound turn tracking, orphan recovery, and abort coordination #29956 which should be reviewed separately.

Last reviewed commit: 32c8710

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

Labels

channel: bluebubbles Channel integration: bluebubbles channel: discord Channel integration: discord channel: feishu Channel integration: feishu channel: googlechat Channel integration: googlechat channel: imessage Channel integration: imessage channel: irc channel: line Channel integration: line channel: matrix Channel integration: matrix channel: mattermost Channel integration: mattermost channel: msteams Channel integration: msteams channel: nextcloud-talk Channel integration: nextcloud-talk channel: nostr Channel integration: nostr channel: signal Channel integration: signal channel: slack Channel integration: slack channel: telegram Channel integration: telegram channel: tlon Channel integration: tlon channel: twitch Channel integration: twitch channel: whatsapp-web Channel integration: whatsapp-web channel: zalo Channel integration: zalo channel: zalouser Channel integration: zalouser commands Command implementations gateway Gateway runtime size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Feishu inbound dedup cache lost on SIGUSR1 restart, causing duplicate message processing

1 participant