Environment: iOS 26.5, Message Filter app extension (IdentityLookup framework), offline filtering.
Setup
My Message Filter Extension performs offline string matching on the message body and
returns one of:
ILMessageFilterAction.allow / .junk / .transaction / .promotion
In the Messages app the filtering UI shows these folders: Messages (the main/default
folder), Transactions, Promotions, Junk. .allow is expected to surface a
message in the main Messages folder.
The documented behavior (API docs + WWDC22 "Explore SMS message filters") only describes
a static mapping from action → folder. On iOS 26.5 I'm seeing what looks like a
stateful, per-sender behavior that I cannot find documented anywhere, and I can't tell
whether it is intended or a bug.
Test methodology
All messages in every sequence are sent from the same single phone number.
Before each Case, I fully clear the receive history for that number, so every
sequence starts from a clean slate with no prior state for that sender.
The notation shows how many conversations/entries appear in each folder after each step.
Case A — first message = allow
allow → Messages: 1
promotion → Messages: 2 (allow + promotion), Promotions: 1
junk → everything collapses into Junk; Messages & Promotions become empty
every subsequent message lands in Junk regardless of the action I return
Case B — first message = allow, then transaction
allow → Messages: 1
transaction → Messages: 2 (allow + transaction), Transactions: 1
junk → everything collapses into Junk; Messages & Transactions empty
every subsequent message lands in Junk regardless of returned action
Case C — first message = transaction
transaction → Messages: 1, Transactions: 1 ← also appears in Messages
allow → Messages: 2, Transactions: 1
promotion → Messages: 3, Transactions: 1, Promotions: 1
junk → Messages: 4, Transactions: 1, Promotions: 1, Junk: 1
(NOTE: here junk does NOT collapse the thread, and there is no "sticky junk")
Case D — first message = promotion
promotion → Promotions: 1 only (does NOT appear in Messages)
allow → Messages: 2 (the earlier promotion now also appears in Messages), Promotions: 1
junk → everything collapses into Junk (sticky, same as Case A/B)
every subsequent message lands in Junk regardless of returned action
My core question: are the following two behaviors by design, or are they bugs?
(1) "Sticky junk" after allow-first / promotion-first.
In Cases A, B and D, once .junk is returned the whole sender thread collapses into
Junk, and from then on every message is forced into Junk regardless of the action my
extension returns. Is this expected/by-design, or a bug? If by design: is it permanent,
and what resets it — does the extension have any control, or is it purely user-driven
(e.g. the user moving the thread out of Junk)? What concerns me is that the system
appears to ignore my returned action entirely once this state is entered.
(2) .transaction-first behaving differently from .allow-first.
A sender whose first message is .transaction (Case C) behaves differently: the
message also appears in the main Messages folder, and a later .junk does not
collapse the thread (no sticky junk). Is this .transaction-first behavior
expected/by-design, or a bug? If by design, what is the underlying rule that makes
.transaction-first confer this state while .allow-first does not? Since the history
is cleared before each test, this is determined purely by the first action returned.
Additional clarifying questions
Is any of this per-sender state behavior documented beyond the static action → folder
mapping in the API docs / WWDC22 "Explore SMS message filters"? If so, where?
More generally, what determines whether a categorized (.transaction / .promotion)
message is also mirrored into the main Messages folder?
Thanks — I'd like my extension's return values to produce predictable categorization for
users, and right now this first-message-dependent behavior makes that hard to reason
about.
0
0
29