Skip to content

fix(bridge): preserve original timestamp on retry-redelivered messages - #149

Open
malyszm wants to merge 2 commits into
verygoodplugins:mainfrom
malyszm:fix/retry-redelivery-timestamp
Open

fix(bridge): preserve original timestamp on retry-redelivered messages#149
malyszm wants to merge 2 commits into
verygoodplugins:mainfrom
malyszm:fix/retry-redelivery-timestamp

Conversation

@malyszm

@malyszm malyszm commented Jul 1, 2026

Copy link
Copy Markdown

Summary

  • Incoming messages that fail to decrypt on first delivery (common after the bridge has been offline a while — an outage, or the periodic linked-device session expiry, when the local Signal session lacks the current sender key) are re-sent by WhatsApp after a retry receipt. The re-sent stanza's t is the resend time, not the original send time, so those messages were stored at reconnect-time — corrupting recency-ordered views (last-message-per-chat and chat sort order) until they aged out.
  • Fix: cache the original timestamp from events.UndecryptableMessage (the first delivery carries the true t) and reuse it when the decrypted retry lands in handleMessage. Guarded to only override when strictly earlier; entries consumed on use with a soft size cap. Applied to the content and reaction store paths and the chat's last-message time.

Type of change

  • fix — bug fix

Scope check

  • This change fits the ROADMAP.md "in scope" list (reliability fix for the Go bridge — reconnect/session-recovery correctness)
  • PR is focused on one concern
  • PR is ≤ ~300 LOC (~140)

Linked issues

Testing

  • Added or updated tests — unit tests for the timestamp-cache helpers (earliest-wins, consume-on-take, empty/zero guards)
  • Ran golangci-lint run and go build ./... (Go changes) — go build ./..., go vet ./..., gofmt, and go test ./... all clean locally (golangci-lint deferred to CI)
  • Manually exercised the affected code path — reproduced after a multi-week offline gap: several incoming messages that failed initial decryption were stored with the reconnect time; with this change the cached original timestamp is used instead (the bridge logs Using original pre-retry timestamp for <id>… when it fires)

Docs

  • No user-visible surface or env/tool/schema change — internal timestamp-correctness fix, no docs update needed.

Risk / rollback

  • Low. The cache only ever lowers a message's timestamp toward its true send time, and only when an earlier value was observed for that exact message ID; otherwise behavior is unchanged. Memory is bounded (consumed on use + soft cap). Revert is a single commit.

@malyszm
malyszm requested a review from jack-arturo as a code owner July 1, 2026 14:08
@malyszm
malyszm force-pushed the fix/retry-redelivery-timestamp branch from 2717226 to 34f1059 Compare July 1, 2026 14:23
When the bridge has been offline for a while (an outage, or the periodic
linked-device session expiry), incoming messages can fail to decrypt on first
delivery because the local Signal session lacks the current sender key. whatsmeow
sends a retry receipt and WhatsApp re-sends the message, but the re-sent stanza's
`t` attribute is the resend time, not the original send time. The bridge stored
that resend time, so messages actually sent hours or days earlier were stamped at
reconnect time — corrupting recency-ordered views (last-message-per-chat, chat
sort order) for the affected messages until they aged out.

The original timestamp is recoverable: the first (undecryptable) delivery carries
it, and whatsmeow surfaces it via events.UndecryptableMessage moments before the
retry resend arrives. Cache the original timestamp by message ID when the
UndecryptableMessage fires, then reuse it when the decrypted retry lands in
handleMessage — guarded to only override when strictly earlier, entries consumed
on use, with a soft size cap so a burst of never-retried messages can't grow the
map unbounded. Applied to both the content and reaction store paths and to the
chat's last-message time.

Adds unit tests for the timestamp-cache helpers.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@malyszm
malyszm force-pushed the fix/retry-redelivery-timestamp branch from 34f1059 to 73f2a0c Compare July 1, 2026 14:27
@jack-arturo

Copy link
Copy Markdown
Member

@codex can you give this a quick look before we merge?

jack-arturo
jack-arturo previously approved these changes Jul 16, 2026

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

Copy link
Copy Markdown

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: 73f2a0cd6e

ℹ️ 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 thread whatsapp-bridge/main.go
extractMediaInfo was still fed the raw resend-time msg.Info.Timestamp, so
for retry-redelivered media the webhook MediaFilename used the resend time
while downloadMedia rebuilds the on-disk name from the stored (original)
timestamp — metadata pointed at a filename that never exists. Pass the
same retry-corrected msgTimestamp used for storage.

Addresses Codex review feedback on verygoodplugins#149.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@malyszm

malyszm commented Jul 29, 2026

Copy link
Copy Markdown
Author

Addressed the Codex feedback in a094d4f — extractMediaInfo now gets the same retry-corrected timestamp that gets stored, so MediaFilename matches the on-disk name downloadMedia rebuilds. @jack-arturo should be good to merge now.

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants