Skip to content

SessionTaskCompleteData round-trip turns None summary into empty string #1139

@007bsd

Description

@007bsd

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-2820SessionTaskCompleteData 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:

  1. The dataclass field declaration (line 2810): summary: str | None = None — the in-memory default is None, so the wire-default should also be None.
  2. The sibling field's parser (line 2815): success correctly uses obj.get("success") with no default and round-trips cleanly.
  3. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions