Problem
SMSStore can attach a delivery report to the wrong outbound SMS when a modem reuses a TP-MR / +CMGS reference for the same recipient.
A delivery report currently contains only reference, recipient, and normalized status when it reaches SMSStore. SMSStore._latest_matching_message() then scans in reverse and selects the newest stored SMS whose (reference, recipient) matches. Since modem message references are finite and can repeat, a delayed receipt for an older outbound message can mark a newer same-recipient message as delivered or failed.
This creates incorrect delivery history for unattended notification/MFA integrations. It is preferable to leave a report uncorrelated than to mutate the status of a different logical SMS.
Reproduction (current main 6dd4211173857e6da55e6e2d0984c06fc56f9a7c)
PYTHONPATH=. uv run --no-project --with pytest --with pytest-asyncio --with pytest-aiohttp --with pyserial-asyncio --with aiosqlite python - <<'PY'
import asyncio
from callstack.sms.store import SMSStore
from callstack.sms.types import DeliveryReport, SMS
async def main():
store = SMSStore()
older = await store.save(SMS(
recipient="synthetic-recipient", body="older", status="sent", reference=7
))
newer = await store.save(SMS(
recipient="synthetic-recipient", body="newer", status="sent", reference=7
))
report = await store.save_delivery_report(DeliveryReport(
reference=7, recipient="synthetic-recipient", status="delivered"
))
print(report.message_id, [(m.id, m.status) for m in await store.list()])
asyncio.run(main())
PY
Observed output:
2 [(1, 'sent'), (2, 'delivered')]
The report is unconditionally attached to the newer message. This behavior is also codified in tests/test_sms_store.py::test_save_delivery_report_correlates_latest_matching_outbound_sms.
Root cause
callstack/sms/service.py::_parse_cmgr_status_report() discards the SCTS/discharge-time fields from the text-mode +CMGR delivery report.
callstack/sms/store.py::SMSStore._latest_matching_message() treats (reference, recipient) as unique and returns the last matching record.
callstack/sms/store.py::_save_delivery_report_locked() assigns that result to report.message_id and updates the chosen message's status.
Suggested fix direction
Make status correlation fail closed when the (reference, recipient) key is ambiguous. A small safe first slice could:
- Preserve parsed report timestamps where available and retain outbound submission timestamps.
- Correlate only a unique, temporally plausible candidate; otherwise persist the delivery report with
message_id=None and do not update an outbound message status.
- Keep the existing unique-match path and the public delivery-report event behavior intact.
Do not use insertion order as a proxy for modem/carrier correlation.
Acceptance criteria
Affected files
callstack/sms/service.py
callstack/sms/store.py
callstack/sms/types.py (only if correlation metadata needs extension)
tests/test_delivery_reports.py
tests/test_sms_store.py
Verification gates
git diff --check
PYTHONPATH=. uv run --no-project --with pytest --with pytest-asyncio --with pytest-aiohttp --with pyserial-asyncio --with aiosqlite pytest tests/test_delivery_reports.py tests/test_sms_store.py -q
PYTHONPATH=. uv run --no-project --with pytest --with pytest-asyncio --with pytest-aiohttp --with pyserial-asyncio --with aiosqlite pytest tests/ -q
Non-goals
- Changing modem message-reference allocation.
- Aggregating multipart segment receipts.
- Retrying delivery reports or changing SIM-slot cleanup semantics.
Problem
SMSStorecan attach a delivery report to the wrong outbound SMS when a modem reuses a TP-MR /+CMGSreference for the same recipient.A delivery report currently contains only
reference,recipient, and normalizedstatuswhen it reachesSMSStore.SMSStore._latest_matching_message()then scans in reverse and selects the newest stored SMS whose(reference, recipient)matches. Since modem message references are finite and can repeat, a delayed receipt for an older outbound message can mark a newer same-recipient message as delivered or failed.This creates incorrect delivery history for unattended notification/MFA integrations. It is preferable to leave a report uncorrelated than to mutate the status of a different logical SMS.
Reproduction (current
main6dd4211173857e6da55e6e2d0984c06fc56f9a7c)Observed output:
The report is unconditionally attached to the newer message. This behavior is also codified in
tests/test_sms_store.py::test_save_delivery_report_correlates_latest_matching_outbound_sms.Root cause
callstack/sms/service.py::_parse_cmgr_status_report()discards the SCTS/discharge-time fields from the text-mode+CMGRdelivery report.callstack/sms/store.py::SMSStore._latest_matching_message()treats(reference, recipient)as unique and returns the last matching record.callstack/sms/store.py::_save_delivery_report_locked()assigns that result toreport.message_idand updates the chosen message's status.Suggested fix direction
Make status correlation fail closed when the
(reference, recipient)key is ambiguous. A small safe first slice could:message_id=Noneand do not update an outbound message status.Do not use insertion order as a proxy for modem/carrier correlation.
Acceptance criteria
message_id=None(or an explicit documented ambiguous state), and no false outbound status mutation.Affected files
callstack/sms/service.pycallstack/sms/store.pycallstack/sms/types.py(only if correlation metadata needs extension)tests/test_delivery_reports.pytests/test_sms_store.pyVerification gates
Non-goals