Skip to content

Preserve multiline bodies for direct +CMT SMS URCs #27

Description

@Justinabox

Summary

Direct-delivery SMS URCs (+CMT) preserve only the first body line. The executor reads exactly one follow-up line for +CMT: and dispatches it as the body; any additional body lines are consumed later while idle and ignored as non-URC lines.

This is separate from #17 / PR #20, which cover multiline bodies in stored +CMGR/+CMGL responses. The default SMS initialization currently uses direct delivery (AT+CNMI=2,2,0,1,0), so direct +CMT is on the main inbound path.

Affected files / lines

  • callstack/protocol/urc.py:37-46MULTILINE_PREFIXES = ("+CMT:",) marks direct SMS as needing a follow-up, but only one follow-up line is represented.
  • callstack/protocol/executor.py:141-151 and :282-294 — the reader/collector reads a single follow-up line for a multiline URC, dispatches it, then treats later body lines as idle non-URCs.
  • callstack/protocol/urc.py:104-106_RawSMSNotification receives only that one followup string.
  • callstack/sms/service.py:201-214 — the direct CMT path saves/emits only event.body.
  • tests/test_urc.py:106-118 and tests/test_sms_service.py:195-214 — coverage exercises only a single-line direct CMT body.

Evidence

Baseline is healthy:

git diff --check -> exit 0
PYTHONPATH=. uv run --no-project --with pytest --with pytest-asyncio --with pytest-aiohttp --with pyserial-asyncio --with aiosqlite pytest tests/ -q
311 passed in 4.03s

Minimal safe reproduction with MockTransport and the real executor/URC/SMS pipeline:

PYTHONPATH=. uv run --no-project --with pyserial-asyncio python /tmp/callstack_issue_scout_repros.py

Relevant output:

Direct CMT emitted body: 'first line'
Direct CMT stored bodies: ["'first line'"]
Direct CMT leftover queued lines: 0

The second body line was consumed by the reader loop and dropped rather than appended to the SMS body.

Expected behavior

A direct-delivery SMS body containing line breaks should be stored and emitted with all body lines intact (for example first line\nsecond line), or the direct-delivery path should be changed to a framing mode that can reliably preserve multiline text.

Actual behavior

Only the first body line is saved/emitted; later lines are silently ignored.

Duplicate check

No existing direct-+CMT issue/PR found with these searches:

gh issue list --repo Justinabox/Callstack --state all --search 'direct CMT multiline SMS body line ignored in:title,body'
gh issue list --repo Justinabox/Callstack --state all --search 'CMT followup multiline SMS direct delivery in:title,body'
gh pr list --repo Justinabox/Callstack --state all --search 'direct CMT multiline SMS followup'

Notable existing related work avoided as duplicate:

Suggested fix direction

  • Add a regression test that feeds a +CMT header plus multiple body lines through ATCommandExecutor.start_reader() using MockTransport.
  • Decide a robust framing strategy for direct +CMT bodies. Options:
    • Prefer notification mode (+CMTI) and fetch via +CMGR, where response final codes frame the full body.
    • If keeping direct +CMT, collect continuation lines until a safe boundary/timeout and preserve them with \n.
  • Ensure non-SMS idle lines still do not get accidentally appended to SMS bodies.

Acceptance criteria

  • Direct +CMT inbound messages with embedded newlines are stored and emitted intact.
  • The regression test fails on current main and passes after the fix.
  • Existing single-line direct CMT behavior and stored-message parsing continue to pass.
  • No new race is introduced in command/URC demuxing.

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_urc.py tests/test_sms_service.py tests/test_executor.py -q
PYTHONPATH=. uv run --no-project --with pytest --with pytest-asyncio --with pytest-aiohttp --with pyserial-asyncio --with aiosqlite pytest tests/ -q

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions