Summary
ATCommandExecutor._collect_response() treats an expected result as successful when the expected token appears anywhere in a response line. For the common expect=["OK"] path, an SMS body or modem data line containing OK can terminate collection before the real final OK arrives. A line starting with ERROR can similarly be treated as a modem failure even when it is user message content.
This can silently drop valid inbound SMS content and leave unread response lines to poison the next command.
Evidence
Affected code:
callstack/protocol/executor.py:290-292 uses if any(e in line for e in expect) for success matching.
callstack/protocol/executor.py:294-296 treats line.startswith(e) for final errors, so message bodies beginning with ERROR are also ambiguous.
callstack/sms/service.py:277-282 calls AT+CMGR with expect=["OK"] and parses only resp.lines.
callstack/sms/service.py:332-345 expects the line after +CMGR to be the message body.
Reproduction on current main (a687aece24a1):
PYTHONPATH=. uv run --no-project --with pytest --with pytest-asyncio --with pyserial-asyncio python -c 'import asyncio
from callstack.events.bus import EventBus
from callstack.protocol.urc import URCDispatcher
from callstack.protocol.executor import ATCommandExecutor
from callstack.transport.mock import MockTransport
from callstack.sms.service import SMSService
async def main():
bus=EventBus(); t=MockTransport(); ex=ATCommandExecutor(t, URCDispatcher(bus)); svc=SMSService(ex,bus)
t.feed("+CMGR: \\\"REC UNREAD\\\",\\\"+155****1234\\\",\\\"\\\",\\\"24/12/25,14:30:00+04\\\"", "OK computer", "OK")
sms=await svc.read_message(0)
print({"sms_is_none": sms is None, "body": None if sms is None else sms.body})
asyncio.run(main())'
# {'sms_is_none': True, 'body': None}
Repository health checks from this scout run:
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
# 291 passed in 3.94s
Duplicate checks:
gh issue list --repo Justinabox/Callstack --state all --search 'executor OK substring SMS body truncates in:title,body'
# no matches
gh issue list --repo Justinabox/Callstack --state all --search 'ATResponse expect in line contains OK in:title,body'
# no matches
Expected behavior
The executor should collect data lines until a real final result code is received. User-controlled SMS bodies such as OK computer, OK, or ERROR: door open should be returned as data, not interpreted as modem final results.
Actual behavior
A data line containing OK satisfies the success expectation and returns early. In the AT+CMGR path above, the body line is mistaken for the final result, so _parse_single_message() sees no body line after the header and returns None.
Suggested fix direction
- Separate final-result matching from prompt/intermediate matching.
- For standard final codes, require exact
OK, exact ERROR, or prefixed +CME ERROR / +CMS ERROR according to AT result-code syntax.
- Keep prompt handling (
expect=[">"]) and intermediate send references (+CMGS:) explicit so SMS send still works.
- Add regression tests at the executor level and through
SMSService.read_message().
Acceptance criteria
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_executor.py tests/test_sms_service.py -q
PYTHONPATH=. uv run --no-project --with pytest --with pytest-asyncio --with pytest-aiohttp --with pyserial-asyncio --with aiosqlite pytest tests/ -q
Summary
ATCommandExecutor._collect_response()treats an expected result as successful when the expected token appears anywhere in a response line. For the commonexpect=["OK"]path, an SMS body or modem data line containingOKcan terminate collection before the real finalOKarrives. A line starting withERRORcan similarly be treated as a modem failure even when it is user message content.This can silently drop valid inbound SMS content and leave unread response lines to poison the next command.
Evidence
Affected code:
callstack/protocol/executor.py:290-292usesif any(e in line for e in expect)for success matching.callstack/protocol/executor.py:294-296treatsline.startswith(e)for final errors, so message bodies beginning withERRORare also ambiguous.callstack/sms/service.py:277-282callsAT+CMGRwithexpect=["OK"]and parses onlyresp.lines.callstack/sms/service.py:332-345expects the line after+CMGRto be the message body.Reproduction on current
main(a687aece24a1):Repository health checks from this scout run:
Duplicate checks:
Expected behavior
The executor should collect data lines until a real final result code is received. User-controlled SMS bodies such as
OK computer,OK, orERROR: door openshould be returned as data, not interpreted as modem final results.Actual behavior
A data line containing
OKsatisfies the success expectation and returns early. In theAT+CMGRpath above, the body line is mistaken for the final result, so_parse_single_message()sees no body line after the header and returnsNone.Suggested fix direction
OK, exactERROR, or prefixed+CME ERROR/+CMS ERRORaccording to AT result-code syntax.expect=[">"]) and intermediate send references (+CMGS:) explicit so SMS send still works.SMSService.read_message().Acceptance criteria
AT+CMGRresponses with bodies containingOKare parsed as normal messages.AT+CMGRresponses with bodies beginning withERRORare parsed as normal messages when followed by finalOK.OK.success=Falseon exactERROR,+CME ERROR..., or+CMS ERROR....expect=[">"]remains green.Verification gates