mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-26 04:15:48 -06:00
docs(channels): settle replay-guard vs ingress-drain layering contract (#109799)
* docs(channels): settle replay-guard vs ingress-drain layering contract in code comments and SDK docs * docs: refresh SDK channel docs map
This commit is contained in:
committed by
GitHub
parent
d9b86cb76c
commit
2920ec1fab
@@ -7360,6 +7360,7 @@ Do not edit it by hand; run `pnpm docs:map:gen`.
|
||||
- H2: What your plugin owns
|
||||
- H2: Message adapter
|
||||
- H3: Inbound ingress (experimental)
|
||||
- H3: Durable ingress and replay dedupe
|
||||
- H3: Typing indicators
|
||||
- H3: Media source params
|
||||
- H3: Native payload shaping
|
||||
|
||||
@@ -94,6 +94,28 @@ the resolved state or decision. See
|
||||
[Channel ingress API](/plugins/sdk-channel-ingress) for the API design,
|
||||
ownership boundary, and test expectations.
|
||||
|
||||
### Durable ingress and replay dedupe
|
||||
|
||||
Channels adopting the durable ingress drain follow the Telegram reference
|
||||
pattern: enqueue the raw transport envelope at a single receive chokepoint
|
||||
(no normalization at receive time), gate the transport ack on the durable
|
||||
append for webhook transports, derive one serialized lane per conversation,
|
||||
and mark the event complete at dispatch adoption. The queue's primary key is
|
||||
`(queue_name, event_id)` and completion tombstones the row instead of
|
||||
deleting it, so a late platform redelivery of the same `event_id` is rejected
|
||||
durably for the tombstone retention window.
|
||||
|
||||
That tombstone is the layering rule for replay guards
|
||||
(`openclaw/plugin-sdk/persistent-dedupe`): a drained channel keeps a separate
|
||||
replay guard only when the guard's identity or retention exceeds the queue's
|
||||
— a logical message key that differs from the transport delivery id (Telegram
|
||||
dedupes `chat_id:message_id` because debounce merges can re-surface a message
|
||||
under a fresh `update_id`), or a longer window than the channel's tombstone
|
||||
retention. If your guard key would equal the drain `event_id`, delete the
|
||||
guard when adopting the drain and size `completedTtlMs`/`completedMaxEntries`
|
||||
to cover the old guard window instead. Non-dedupe protections (age fences,
|
||||
outbound echo caches) are unrelated to this rule and stay.
|
||||
|
||||
### Typing indicators
|
||||
|
||||
If your channel supports typing indicators outside inbound replies, expose
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
// iMessage inbound replay protection: brings the channel in line with the
|
||||
// other channels (whatsapp/discord/signal/...) by deduping inbound messages on
|
||||
// a stable identity, plus an age fence that suppresses stale backlog Apple
|
||||
// delivers in a burst after a bridge/Push recovery.
|
||||
// iMessage inbound replay protection: GUID dedupe on a stable identity, plus
|
||||
// an age fence that suppresses stale backlog Apple delivers in a burst after a
|
||||
// bridge/Push recovery.
|
||||
//
|
||||
// Why both:
|
||||
// Why both, and what survives ingress-drain adoption:
|
||||
// - The GUID dedupe stops a message that was already dispatched from being
|
||||
// dispatched again when imsg re-emits a recent row on reconnect.
|
||||
// - Dedupe cannot catch a message that was *never seen* (the gateway was down
|
||||
// when it was sent). Apple writes that backlog into chat.db with a fresh
|
||||
// ROWID but the original (old) send date, so it arrives on the live watch as
|
||||
// a "new" row. The age fence is what recognizes it as stale.
|
||||
// dispatched again when imsg re-emits a recent row on reconnect. It is the
|
||||
// transitional layer: if this channel adopts the durable ingress drain with
|
||||
// event_id = GUID, the queue tombstone owns this job and the dedupe goes.
|
||||
// - The age fence is NOT a dedupe and stays regardless. It catches messages
|
||||
// the gateway *never saw* (sent while down): Apple writes that backlog into
|
||||
// chat.db with a fresh ROWID/GUID but the original old send date, so no
|
||||
// dedupe or tombstone can recognize it — only the send-date fence does.
|
||||
import { createHash } from "node:crypto";
|
||||
import { createChannelReplayGuard } from "openclaw/plugin-sdk/persistent-dedupe";
|
||||
import type { IMessagePayload } from "./types.js";
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
// Telegram plugin module implements message dispatch dedupe behavior.
|
||||
// Telegram dispatch dedupe: a PERMANENT second layer above the ingress spool,
|
||||
// not a leftover to delete on drain adoption. The spool tombstones transport
|
||||
// update_ids; debounce/media-group flushes merge N update_ids into one
|
||||
// dispatched turn, so a constituent message re-arriving under a *fresh*
|
||||
// update_id is invisible to the update_id tombstone. This guard keys the
|
||||
// logical (chat_id, message_id) — the only identity that catches that replay.
|
||||
import path from "node:path";
|
||||
import type { Message } from "grammy/types";
|
||||
import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime";
|
||||
|
||||
@@ -678,7 +678,19 @@ export function createClaimableDedupe(
|
||||
};
|
||||
}
|
||||
|
||||
/** Create an event-keyed replay guard whose claims own their settlement handles. */
|
||||
/**
|
||||
* Create an event-keyed replay guard whose claims own their settlement handles.
|
||||
*
|
||||
* Layering contract vs the durable ingress drain (`src/channels/message/ingress-queue.ts`):
|
||||
* the drain already rejects duplicate event ids durably — `complete()` tombstones the row
|
||||
* and enqueue is `ON CONFLICT DO NOTHING` for the tombstone retention window. A replay
|
||||
* guard on a drained channel is justified only when its identity or retention exceeds the
|
||||
* queue's: a *logical* message key that differs from the transport delivery id (Telegram:
|
||||
* `chat_id:message_id` vs `update_id` — debounce/media-group merges can re-surface a
|
||||
* constituent message under a fresh update_id only the guard sees), or a window longer
|
||||
* than the channel's tombstone retention. If the guard key would equal the drain event_id
|
||||
* and retention fits the tombstone window, delete the guard when adopting the drain.
|
||||
*/
|
||||
export function createChannelReplayGuard<TEvent>(
|
||||
params: ChannelReplayGuardParams<TEvent>,
|
||||
): ChannelReplayGuard<TEvent> {
|
||||
|
||||
Reference in New Issue
Block a user