Files
openclaw/extensions
Peter Steinberger 1855f35d7d fix(whatsapp): preserve inbound messages across restarts (#110418)
* refactor(whatsapp): replay durable inbound through the shared drain

Accept-before-processing stays; the private startup replay loop and inline
complete/release bookkeeping move to the core drain. Drain-owned entries keep
their durable id so downstream failures release for replay, dedupe-claim
contention stays retryable instead of tombstoning, and undeliverable claims
still send configured read receipts. Rows tombstone at dispatch return
(pre-migration timing), not at adoption (#108656).

* fix(whatsapp): finish durable ingress drain adoption with serialized conversation lanes

Core durable ingress intentionally serializes claims by remote JID. Same-conversation bursts now merge as queued followups in the core reply lane after adoption; channel debouncing remains for control and retry paths, with merged fan-out and gated settlement.

Built on the durable drain branch base by @obviyus.

* fix(whatsapp): fail closed on persistence failure and guard prepared-inbound races

Landing autoreview caught two holes: the persistence-failure fallback
dispatched live, bypassing drain dedupe and lane serialization now that the
replay guard is deleted (duplicate replies / session races); and duplicate
pending rows could orphan or clobber preparedInboundByDurableId entries. The
append now retries transient failures with short backoff then drops loudly
(fail closed — the fallback traded a correctness race for availability
against an already-broken store); redeliveries neither clobber the first
delivery's in-flight preparation nor keep orphan entries. The old
fallback-contract test is repurposed to prove retry-then-durable-delivery.

* fix(whatsapp): dispose the durable drain when the close drain times out

The close timeout abandoned the graceful drain path before its finally could
dispose, so a successor monitor could pump the same account queue against a
still-live drain. The timeout wrapper now owns disposal; dispose is
idempotent so the graceful path's cleanup is unaffected.

* style(whatsapp): keep ingress internals unexported and drop a dead harness helper

* refactor(whatsapp): move durable payload serialization to its own module

The dead-export gate refused serializer/error exports whose only external
consumers were tests. The payload contract (Long-timestamp-preserving
serialize/deserialize + the permanent-error class) now lives in
durable-payload.ts with durable-receive as its production consumer; the dead
WhatsAppRetryableInboundError class is deleted (any non-permanent error is
retryable to the drain classifier, so test-support throws a plain Error).

* fix(whatsapp): guard prepared-map ownership and bound its growth

A duplicate pending delivery's finishPreparation deleted the first
delivery's kept entry (ownership guard now requires the deleter to be the
installer), and queue pruning could evict pending rows whose prepared-map
entries then lingered forever on a blocked lane (map now evicts oldest-first
at a bound above the queue's pending cap; dispatch already re-normalizes the
journaled payload when its entry is gone). Regression covers the duplicate
case.

* style(whatsapp): prune split leftovers and satisfy consistent-return lint

* style(whatsapp): satisfy type-aware lint across the drain monitor

Closed verdict shape for the retry result (queue enqueue's metadata generic
leaks any into the union), Promise.resolve-wrapped abandonment aggregators,
braced timeout executor, and the drain merged to a single const (lazy
closures above only run post-start, so const-after-use is TDZ-safe).

---------

Co-authored-by: Ayaan Zaidi <hi@obviy.us>
2026-07-18 06:14:33 +01:00
..