Summary
Public event serialization appends a Z suffix to every event timestamp, but the base Event dataclass defaults to datetime.now() without a timezone. That means normal runtime events created by SMS/USSD/call/modem code are local-time naive datetimes that get labeled as UTC in callstack monitor --json and any future realtime/event-stream consumers using serialize_event().
For unattended Raspberry Pi deployments, this can misorder SMS/call/delivery-report timelines across hosts, log shippers, dashboards, and incident investigations. It also makes the new monitor/realtime foundation less trustworthy before WebSocket/event-feed work expands.
Evidence
Affected code:
callstack/events/types.py:15-18 defines Event.timestamp with default_factory=datetime.now, producing a naive local timestamp.
callstack/events/serialize.py:33-37 converts aware timestamps to UTC, but for naive timestamps it still returns f"{timestamp.isoformat()}Z".
- Runtime event constructors such as
IncomingSMSEvent(...), USSDResponseEvent(...), RingEvent(...), ModemDisconnectedEvent(...), etc. inherit that default unless callers explicitly pass an aware timestamp.
- Tests in
tests/test_event_serialization.py pass an explicit UTC-aware timestamp, so they do not cover the default runtime path.
No-hardware reproduction on current main (e53d94da997d305a2403322b985b13d8fbff2fce):
PYTHONPATH=. uv run --no-project python -c 'from callstack.events.types import IncomingSMSEvent; from callstack.events.serialize import serialize_event; e=IncomingSMSEvent(sender="+155****0123", body="secret"); print(e.timestamp); print(e.timestamp.tzinfo); print(serialize_event(e)["timestamp"])'
Actual output:
2026-06-27 12:01:08.738956
None
2026-06-27T12:01:08.738956Z
The tzinfo is None, but the serialized value is marked with Z (UTC).
Repository health 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
# 620 passed in 5.95s
Duplicate checks performed:
gh issue list --repo Justinabox/Callstack --state all --search "UTC timestamp event serialize_event Z naive datetime"
# no matches
gh issue list --repo Justinabox/Callstack --state all --search "monitor timestamp timezone local UTC"
# only broad planning issue #9
gh issue list --repo Justinabox/Callstack --state all --search "datetime.now timezone.utc Event timestamp"
# no matches
gh pr list --repo Justinabox/Callstack --state all --search "timestamp timezone event UTC Z"
# no matches
Expected behavior
Serialized public event timestamps should either:
- be truly UTC-aware instants before appending
Z, or
- avoid claiming UTC for naive local timestamps.
For a PBX/SMS gateway, the preferred behavior is likely UTC-aware event creation by default so local monitor output, future WebSocket events, and persisted/forwarded observability data all share one canonical timeline.
Actual behavior
Default runtime events are naive local datetimes, and serialize_event() labels them as UTC with a Z suffix.
Suggested fix direction
- Change
Event.timestamp to default to an aware UTC value, e.g. datetime.now(timezone.utc).
- Keep
serialize_event() converting aware timestamps to UTC before rendering.
- Decide how to handle existing caller-provided naive timestamps; fail closed with a clear error in serialization or explicitly treat them as local/UTC with documented behavior. For public event streams, rejecting or normalizing naive timestamps is safer than silently mislabeling them.
- Add regression coverage for events created without an explicit timestamp, not only explicit
timezone.utc fixtures.
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_event_serialization.py tests/test_cli.py -q
PYTHONPATH=. uv run --no-project --with pytest --with pytest-asyncio --with pytest-aiohttp --with pyserial-asyncio --with aiosqlite pytest tests/ -q
Summary
Public event serialization appends a
Zsuffix to every event timestamp, but the baseEventdataclass defaults todatetime.now()without a timezone. That means normal runtime events created by SMS/USSD/call/modem code are local-time naive datetimes that get labeled as UTC incallstack monitor --jsonand any future realtime/event-stream consumers usingserialize_event().For unattended Raspberry Pi deployments, this can misorder SMS/call/delivery-report timelines across hosts, log shippers, dashboards, and incident investigations. It also makes the new monitor/realtime foundation less trustworthy before WebSocket/event-feed work expands.
Evidence
Affected code:
callstack/events/types.py:15-18definesEvent.timestampwithdefault_factory=datetime.now, producing a naive local timestamp.callstack/events/serialize.py:33-37converts aware timestamps to UTC, but for naive timestamps it still returnsf"{timestamp.isoformat()}Z".IncomingSMSEvent(...),USSDResponseEvent(...),RingEvent(...),ModemDisconnectedEvent(...), etc. inherit that default unless callers explicitly pass an aware timestamp.tests/test_event_serialization.pypass an explicit UTC-aware timestamp, so they do not cover the default runtime path.No-hardware reproduction on current
main(e53d94da997d305a2403322b985b13d8fbff2fce):PYTHONPATH=. uv run --no-project python -c 'from callstack.events.types import IncomingSMSEvent; from callstack.events.serialize import serialize_event; e=IncomingSMSEvent(sender="+155****0123", body="secret"); print(e.timestamp); print(e.timestamp.tzinfo); print(serialize_event(e)["timestamp"])'Actual output:
The
tzinfoisNone, but the serialized value is marked withZ(UTC).Repository health from this scout run:
Duplicate checks performed:
Expected behavior
Serialized public event timestamps should either:
Z, orFor a PBX/SMS gateway, the preferred behavior is likely UTC-aware event creation by default so local monitor output, future WebSocket events, and persisted/forwarded observability data all share one canonical timeline.
Actual behavior
Default runtime events are naive local datetimes, and
serialize_event()labels them as UTC with aZsuffix.Suggested fix direction
Event.timestampto default to an aware UTC value, e.g.datetime.now(timezone.utc).serialize_event()converting aware timestamps to UTC before rendering.timezone.utcfixtures.Acceptance criteria
serialize_event()no longer appendsZto a naive timestamp without first normalizing/rejecting it deliberately.Event/IncomingSMSEventtimestamp serialization.Verification gates