Skip to content

fix(call_log): take a synced call's direction from its creator, not the direction fields - #1295

Merged
jlucaso1 merged 2 commits into
mainfrom
claude/issue-1294-resolution
Aug 12, 2026
Merged

fix(call_log): take a synced call's direction from its creator, not the direction fields#1295
jlucaso1 merged 2 commits into
mainfrom
claude/issue-1294-resolution

Conversation

@jlucaso1

@jlucaso1 jlucaso1 commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Closes #1294.

Summary

A call placed from the account's own handset arrived as from_me: false. The report is right that the direction is backwards, and right that record.is_incoming reads literally in the captures — but the cause is not that the crate picked the wrong one of two fields. It is that both fields are written differently depending on which client authored the mutation, so no fixed reading of either is right for all of them.

The fix is the route the issue suggests at the end, and the one the official client actually takes: derive the direction from the call creator.

Protocol evidence

Bundle 2.3000.1044659339 — the version whatspec.lock.json currently pins, so this is the same build the generated tree describes. The appstate IR only models the literal call_log index part, so this came from the raw bundle.

The writer, WAWebCallLogSync.getCallLogMutation:

var c = (e = n.callCreatorJid) == null ? void 0 : e.toJid(), d = c;
d == null && (d = n.fromMe ? getMeDevicePnOrThrow().toJid() : n.peerJid.toJid());
var m = n.fromMe ? "1" : "0", p = n.callId,
    f = {, isIncoming: n.fromMe,, callCreatorJid: c,};
return buildPendingMutation({, indexArgs: [d, p, m], value: {callLogAction: {callLogRecord: f}},})

So WA Web sets the index's fourth part and record.isIncoming from the same local fromMe. Its isIncoming is genuinely inverted against its own name — which is what the crate's old doc block described, and it was accurate about this writer.

The reader, WAWebVoipActionWriteCallLogSync.generateCallLogFromCallSyncRecord, destructures the record as callCreatorJid, callId, callLinkToken, callResult, groupJid, isCallLink, isVideo, silenceReason, startTimeisIncoming is not among them. It never sees the index either, since applyMutations is handed only the record. Direction comes from WAWebVoipBackendCallLogTargetResolver.getCallLogTargetDetails:

var f = yield c({callDestinationWid: asUserWidOrThrow(n)}),   // n = callCreatorWid
    g = isMeAccount(f);

return {msgKeyId: , fromMe: g, callCreatorUserWid: f, chatId: m, participant: p, viewMode: _}

fromMe is isMeAccount(callCreatorWid) — nothing else. The helper c() resolves the creator through the LID↔PN mapping first.

That closes the loop with the report: the two captures have creator == me with the direction fields saying 0/false, which is contradictory under a fromMe reading and consistent under a literal isIncoming one. Under the creator rule they are simply outbound, and so is a WA Web-authored record with the same bytes. The fix does not depend on settling which writer is right, which is its main advantage over inverting the parse: inverting would fix phone-authored records and break WA Web-authored ones, and both reach a companion because app state fans a companion's mutations out to every other device.

Changes

fix(call_log)dispatch_call_log_mutation takes an is_own_jid predicate and sets from_me from the call creator. The index's fourth part is no longer parsed. Both doc blocks corrected: record.is_incoming is not a safe fallback under either name.

fix(send) — self-detection now keys on the addressing mode. is_own_jid compared with is_same_user_as, which ignores the server, so a peer LID whose digits spell our phone number read as us. The rule was already written down twice here: is_same_chat_as's test says it exists to guard "the is_same_user_as looseness that ignores server", and lid_recipient_without_own_lid_is_not_self_dm records WA Web's isMeAccount keying on isSameAccountAndAddressingMode. Caught in review; I had reused the loose helper without checking which of the two it was.

Fixed at the source rather than at the call site, so the privacy-token path gets it too — a false self-match there withholds a token that was due, the same bug wearing different clothes. The comparison moved into is_own_identity, a free function over the two identities, so it is unit-testable without standing up a client. features::contacts had an inline copy of the old comparison and now calls is_own_jid, so three sites share one rule instead of two.

Cost

Nothing added to the hot path. The predicate is FnOnce and is consulted only after the mutation is claimed as a call log, so the other app-state mutation kinds — the bulk of a full sync — do not pay for the device snapshot; previously this dispatcher did no identity work at all, and it still does none for them. Per call log it is one cached Arc<Device> snapshot and at most two comparisons, replacing a string match on the index part. No new allocations, and is_same_chat_as is no more expensive than is_same_user_as on a mismatch (it short-circuits on the user first).

Binary size on the first commit: total size, .text and dependency count all unchanged, llvm-lines whatsapp-rust −99.

Behavior change worth flagging

An unreadable fourth index part used to drop the whole mutation, on the reasoning that guessing the direction would mislabel the call. Nothing reads that part now, so the reason is gone and dropping a usable call log over it would be a loss. Such a mutation is dispatched, with the direction still derived from the creator. an_unreadable_direction_part_no_longer_drops_the_call pins it; the other index parts are still required, because the event is built from them.

Checked and not changed

  • Inverting the parse of index[3], the issue's primary suggestion. Correct for the reported captures, wrong for WA Web-authored ones, where that part really is fromMe. The creator comparison is right for both.
  • record.is_incoming as a fallback when the creator is missing: it isn't one. The creator is the only field the direction can come from, and the index always carries it — that is what WA Web's d == null && (d = fromMe ? me : peerJid) fallback exists for. A mutation with no readable creator produces no event, as before.
  • The CallLogSync shape. from_me keeps its name and meaning; only how it is computed changed, so consumers need no migration — the ones that were filing calls backwards start being right.

Validation

cargo fmt --all
cargo clippy -p whatsapp-rust --lib --all-features -- -D warnings
cargo test -p whatsapp-rust --lib    # 1653 pass
cargo test -p wacore --lib           # 1420 pass

Call-log coverage: a call this account placed reads as from_me under either of our identities, with both direction fields set the way the phone sends them for an outbound call (the combination that produced the bug); a call the peer placed does not, whatever those fields claim, including the WA Web spelling that is byte-identical to the outbound fixture and told apart only by the creator; an unreadable fourth part no longer drops the call; an index missing a creator still produces no event. Identity coverage: a_peer_addressed_in_the_other_namespace_is_not_us covers both namespace directions and the case where no LID is known.

Semver Checks (informational) is red on this branch and on main alike — the failures are waproto fields dropped by the whatspec regeneration in #1293, which this PR does not touch. Same failure on #1293 itself, already merged, with everything else green.

Workspace clippy and the full matrix left to CI.

…he direction fields

A call placed from the account's own handset arrived as `from_me: false`.

Both fields that claim to carry direction are written differently depending
on which client authored the mutation. WA Web's writer sets both from its
local `fromMe` — `indexArgs: [d, p, m]` with `m = n.fromMe ? "1" : "0"`, and
`isIncoming: n.fromMe` — so its `isIncoming` is inverted against its own
name. Mutations authored by the phone carry both literally as `isIncoming`,
so an outbound call arrives as `0`/`false`. App state fans a companion's
mutations out to every other device, so both flavors reach this client and
no fixed reading of either field is right for both.

WA Web's reader does not read either one. `generateCallLogFromCallSyncRecord`
destructures the record without touching `isIncoming`, and takes direction
from `getCallLogTargetDetails`, which returns `fromMe: isMeAccount(
callCreatorWid)` — the call creator compared against this account. Do the
same, reusing `Client::is_own_jid`, which already compares a JID against
both of the device's identities and whose doc already claimed to be the
single source of truth for the message and call paths.

The predicate is passed as `FnOnce` and consulted only after the mutation is
claimed, so the other app-state mutation kinds do not pay for the device
snapshot. The index's fourth part is no longer parsed at all, which also
means a value we cannot read there no longer drops an otherwise usable call
log.
@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Repository UI (base), Organization UI (inherited)

Review profile: ASSERTIVE

Plan: Pro Plus

Run ID: 330379bf-a074-4ce9-899b-94a5fa2f0703

📥 Commits

Reviewing files that changed from the base of the PR and between b879bca and 49df384.

📒 Files selected for processing (4)
  • src/features/call_log.rs
  • src/features/contacts.rs
  • src/send/tctoken_lifecycle.rs
  • wacore/src/types/events.rs

📝 Walkthrough

Summary by CodeRabbit

  • Bug Fixes
    • Improved call log direction detection by accurately identifying calls made by the current account.
    • Added support for matching phone-number and device-based account identifiers, including device suffixes.
    • Call log updates with malformed direction values are now processed instead of being rejected.
    • Improved recognition of the current account when retrieving profile pictures.
  • Documentation
    • Clarified how call direction is determined when synced records contain conflicting values.

Walkthrough

Call-log direction now comes from creator JID ownership. Dispatch receives the client ownership predicate, supports PN/LID matching, and accepts malformed fourth index values without rejecting otherwise valid mutations. Profile-picture handling, documentation, and tests use the updated ownership semantics.

Changes

Call direction handling

Layer / File(s) Summary
Ownership predicate contract
src/send/tctoken_lifecycle.rs, src/features/contacts.rs
Client::is_own_jid is crate-visible and uses PN/LID-aware matching. Profile-picture retrieval uses the same method.
Dispatch direction classification
src/client/app_state.rs, src/features/call_log.rs
Call-log dispatch receives the ownership predicate and derives direction from the parsed creator JID. It no longer rejects unreadable fourth index components.
Direction and malformed-index coverage
src/features/call_log.rs
Tests cover local PN/LID identities, peer identities, conflicting direction fields, and malformed indexes.
Event direction documentation
wacore/src/types/events.rs
Documentation defines creator-JID-based from_me semantics and records conflicting interpretations of record.is_incoming.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Client
  participant AppState
  participant CallLog
  participant EventBus
  Client->>AppState: Dispatch call-log mutation
  AppState->>CallLog: Pass is_own_jid predicate
  CallLog->>Client: Check creator JID ownership
  CallLog->>EventBus: Emit classified call event
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly states that synced call direction now comes from the creator instead of the direction fields.
Description check ✅ Passed The description directly explains the call-direction fix, identity matching changes, behavior changes, tests, and validation.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/issue-1294-resolution

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@greptile-apps

greptile-apps Bot commented Aug 12, 2026

Copy link
Copy Markdown

Greptile Summary

The PR derives synchronized call direction from the call creator rather than inconsistent direction fields.

  • Passes the client’s shared own-identity predicate into call-log mutation dispatch.
  • Makes own-identity matching namespace-aware and reuses it in contact and TC-token paths.
  • Adds regression coverage for PN/LID identities, conflicting direction fields, and unreadable direction index values.

Confidence Score: 5/5

The PR appears safe to merge.

No blocking failure remains.

Important Files Changed

Filename Overview
src/client/app_state.rs Supplies the client’s own-identity predicate when dispatching synchronized call-log mutations.
src/features/call_log.rs Replaces direction-field parsing with creator identity comparison and adds focused regression tests.
src/features/contacts.rs Reuses the shared own-identity helper when deciding whether to request a trusted-contact token.
src/send/tctoken_lifecycle.rs Centralizes namespace-aware PN/LID self-detection and verifies device and cross-namespace behavior.
wacore/src/types/events.rs Updates CallLogSync documentation to describe creator-derived direction semantics.

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart LR
  A[Decoded call_log mutation] --> B[Parse creator JID from index]
  B --> C[Compare creator with own PN and LID]
  C -->|Match| D[from_me = true]
  C -->|No match| E[from_me = false]
  D --> F[Dispatch CallLogSync]
  E --> F
Loading

Reviews (2): Last reviewed commit: "fix(send): key self-detection on the add..." | Re-trigger Greptile

greptile-apps[bot]
greptile-apps Bot previously approved these changes Aug 12, 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: b879bcac51

ℹ️ 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 src/features/call_log.rs
Comment thread src/features/call_log.rs Outdated
@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown

📦 Binary size report

Metric main PR Δ
bin size (stripped) 10.16 MiB 10.16 MiB +192 B (+0.00%) 🔺
bin .text 8.15 MiB 8.15 MiB +256 B (+0.00%) 🔺
bin allocated (text+data+bss) 10.16 MiB 10.16 MiB -64 B (-0.00%) 🔽
llvm-lines wacore 538,872 538,872 0
llvm-lines wacore copies 17,689 17,689 0
llvm-lines whatsapp-rust lib 770,023 770,018 -5 (-0.00%) 🔽
llvm-lines whatsapp-rust lib copies 24,006 24,010 +4 (+0.02%) 🔺
deps crates (Cargo.lock) 463 463 0
.text per crate
Crate main PR Δ
.text whatsapp_rust 1.86 MiB 1.86 MiB +227 B (+0.01%) 🔺
.text wacore 695.95 KiB 695.57 KiB -391 B (-0.05%) 🔽
.text wacore_binary 81.61 KiB 81.61 KiB 0
.text wacore_libsignal 178.98 KiB 178.98 KiB 0
.text wacore_appstate 22.37 KiB 22.37 KiB 0
.text wacore_noise 20.94 KiB 20.94 KiB 0
.text waproto 1.81 MiB 1.81 MiB 0
.text whatsapp_rust_sqlite_storage 540.57 KiB 540.57 KiB 0
.text whatsapp_rust_tokio_transport 40.49 KiB 40.49 KiB 0
.text whatsapp_rust_ureq_http_client 12.68 KiB 12.68 KiB 0
.text std 1002.87 KiB 1002.87 KiB 0
.text other deps 1.90 MiB 1.90 MiB +391 B (+0.02%) 🔺

Baseline: 874328f37 (latest main run) · Head: e153902cb · Graphs

…gits

`is_own_jid` compared with `is_same_user_as`, which ignores the server. A
LID is an assigned number in its own namespace, so one can spell a phone
number belonging to somebody else — and that peer was reading as us.

The repo already had the rule written down twice: `is_same_chat_as` exists
to guard "the `is_same_user_as` looseness that ignores server", and
`lid_recipient_without_own_lid_is_not_self_dm` records that WA Web's
`isMeAccount` keys on `isSameAccountAndAddressingMode`. Self-detection now
follows it, which matters more since the synced call log started deriving a
call's direction from this: a false match there files a call the peer placed
as one we placed, and on the privacy-token path it withholds a token that
was due.

The comparison moves into `is_own_identity`, a free function over the two
identities, so it is unit-testable without a client. `features::contacts`
had an inline copy of the old comparison and now calls `is_own_jid` instead,
so the three sites share one rule rather than two.
@greptile-apps
greptile-apps Bot dismissed their stale review August 12, 2026 14:32

Dismissed because a newer commit was pushed; Greptile will re-review the current head.

@jlucaso1
jlucaso1 merged commit ed7723a into main Aug 12, 2026
26 of 27 checks passed
@jlucaso1
jlucaso1 deleted the claude/issue-1294-resolution branch August 12, 2026 14:39
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.

CallLogSync::from_me is inverted: the index part and record.is_incoming both carry isIncoming

2 participants