Summary
SessionTaskCompleteData.from_dict() reads a missing/null summary field as the empty string "" instead of None, so the round-trip from_dict(to_dict(x)) is not the identity for SessionTaskCompleteData(success=None, summary=None). Any code that compares the parsed event to the original, or that distinguishes "no summary provided" from "empty summary", sees incorrect data.
Affected versions
main at commit dd2dcbc439256acfb9feb2cff07c0b9c820091b8. The class is generated; the same shape ships in every Python release built from this codegen.
Affected source
python/copilot/generated/session_events.py:2807-2820 — SessionTaskCompleteData definition and from_dict
- The bug is on line 2816:
summary = from_union([from_none, from_str], obj.get("summary", ""))
Compare line 2815 (correct):
success = from_union([from_none, from_bool], obj.get("success"))
The obj.get("summary", "") default coerces a missing key to "", which from_union([from_none, from_str], ...) then accepts as a valid str. Without the default, obj.get("summary") returns None, and from_union correctly resolves through from_none.
Reproduction
from copilot.generated.session_events import SessionTaskCompleteData
original = SessionTaskCompleteData(success=None, summary=None)
print("original: ", original)
print("to_dict(): ", original.to_dict())
roundtrip = SessionTaskCompleteData.from_dict(original.to_dict())
print("roundtrip: ", roundtrip)
print("equal? ", roundtrip == original)
Expected vs actual output
Expected:
original: SessionTaskCompleteData(success=None, summary=None)
to_dict(): {}
roundtrip: SessionTaskCompleteData(success=None, summary=None)
equal? True
Actual:
original: SessionTaskCompleteData(success=None, summary=None)
to_dict(): {}
roundtrip: SessionTaskCompleteData(success=None, summary='')
equal? False
Root cause
The obj.get("summary", "") default on line 2816 is inconsistent with both:
- The dataclass field declaration (line 2810):
summary: str | None = None — the in-memory default is None, so the wire-default should also be None.
- The sibling field's parser (line 2815):
success correctly uses obj.get("success") with no default and round-trips cleanly.
- The serializer (lines 2822-2826, omits
summary when None): the asymmetry between to_dict() (omit on None) and from_dict() (default to "") is what breaks the round-trip.
This is the only occurrence of the obj.get("...", "") pattern in session_events.py (grep -c 'obj.get("[a-zA-Z]*", "")' returns 1), so the fix is local to one line.
Impact
- The dataclass advertises
summary: str | None = None, so callers may pattern-match on None to mean "no summary." After a round-trip they see "" instead, breaking that branch.
- Any code caching or comparing events (e.g., session replay, audit logs, deduplication) sees
original != round_tripped for events with no summary.
SessionTaskCompleteData is dispatched at session_events.py:4480 from the SESSION_TASK_COMPLETE event type — so any session that emits a task-complete event without a summary triggers the bug end-to-end.
Summary
SessionTaskCompleteData.from_dict()reads a missing/nullsummaryfield as the empty string""instead ofNone, so the round-tripfrom_dict(to_dict(x))is not the identity forSessionTaskCompleteData(success=None, summary=None). Any code that compares the parsed event to the original, or that distinguishes "no summary provided" from "empty summary", sees incorrect data.Affected versions
mainat commitdd2dcbc439256acfb9feb2cff07c0b9c820091b8. The class is generated; the same shape ships in every Python release built from this codegen.Affected source
python/copilot/generated/session_events.py:2807-2820—SessionTaskCompleteDatadefinition andfrom_dictThe
obj.get("summary", "")default coerces a missing key to"", whichfrom_union([from_none, from_str], ...)then accepts as a validstr. Without the default,obj.get("summary")returnsNone, andfrom_unioncorrectly resolves throughfrom_none.Reproduction
Expected vs actual output
Expected:
Actual:
Root cause
The
obj.get("summary", "")default on line 2816 is inconsistent with both:summary: str | None = None— the in-memory default isNone, so the wire-default should also beNone.successcorrectly usesobj.get("success")with no default and round-trips cleanly.summarywhenNone): the asymmetry betweento_dict()(omit on None) andfrom_dict()(default to "") is what breaks the round-trip.This is the only occurrence of the
obj.get("...", "")pattern insession_events.py(grep -c 'obj.get("[a-zA-Z]*", "")'returns 1), so the fix is local to one line.Impact
summary: str | None = None, so callers may pattern-match onNoneto mean "no summary." After a round-trip they see""instead, breaking that branch.original != round_trippedfor events with no summary.SessionTaskCompleteDatais dispatched atsession_events.py:4480from theSESSION_TASK_COMPLETEevent type — so any session that emits a task-complete event without a summary triggers the bug end-to-end.