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-46 — MULTILINE_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
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/+CMGLresponses. The default SMS initialization currently uses direct delivery (AT+CNMI=2,2,0,1,0), so direct+CMTis on the main inbound path.Affected files / lines
callstack/protocol/urc.py:37-46—MULTILINE_PREFIXES = ("+CMT:",)marks direct SMS as needing a follow-up, but only one follow-up line is represented.callstack/protocol/executor.py:141-151and: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—_RawSMSNotificationreceives only that onefollowupstring.callstack/sms/service.py:201-214— the direct CMT path saves/emits onlyevent.body.tests/test_urc.py:106-118andtests/test_sms_service.py:195-214— coverage exercises only a single-line direct CMT body.Evidence
Baseline is healthy:
Minimal safe reproduction with
MockTransportand the real executor/URC/SMS pipeline:Relevant output:
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-
+CMTissue/PR found with these searches:Notable existing related work avoided as duplicate:
+CMGR/+CMGLstored-message parser truncation, not the direct+CMTURC framing path.Suggested fix direction
+CMTheader plus multiple body lines throughATCommandExecutor.start_reader()usingMockTransport.+CMTbodies. Options:+CMTI) and fetch via+CMGR, where response final codes frame the full body.+CMT, collect continuation lines until a safe boundary/timeout and preserve them with\n.Acceptance criteria
+CMTinbound messages with embedded newlines are stored and emitted intact.mainand passes after the fix.Verification gates