Calendar Day
Thursday, July 9, 2026 (PR 1 of 3)
Planned Effort
5 story points (Medium) — sprint item #5
Companion PRs: Thursday PR 2 (dispatch simplify #6) stacks on this; PR 3 (session decorator #7) is independent.
Problem
models/session.py MessageDict (lines 29–52) is a single flat TypedDict: role: RoleLiteral required plus 22 NotRequired fields spanning all five roles. mypy cannot distinguish roles, so msg["thinking"] on a user message type-checks yet is absent at runtime.
Already done and not this issue:
MessageDict remains a flat, monotonically-widening union — the type-safety gap is unresolved.
Goal
One merged PR that splits MessageDict into role-specific TypedDicts unioned on role, updates consumers to narrow on the discriminant, and preserves serialized JSON shapes for API/export parity.
Scope
Touch points
models/session.py — introduce:
BaseMessageDict (common fields: uuid, timestamp, type, …)
UserMessageDict, AssistantMessageDict, SystemMessageDict, ResultMessageDict, ProgressMessageDict
MessageDict = Union[...] discriminated on role
- Consumer narrowing in:
utils/jsonl_parser.py
utils/tool_dispatch.py
api/sessions.py
utils/md_exporter.py, utils/json_exporter.py
- Type-level regression test — mypy rejects role-inappropriate field access (e.g.
thinking on user message).
Out of scope
Acceptance Criteria
Verification
cd C:\Users\Jasen\CppAliance\claude-code-chat-browser
.\.venv\Scripts\Activate.ps1
mypy -p models -p utils -p api
pytest tests/test_jsonl_parser.py tests/test_real_session_fixtures.py tests/test_api_routes.py -q
pytest -q
ruff check .
Calendar Day
Thursday, July 9, 2026 (PR 1 of 3)
Planned Effort
5 story points (Medium) — sprint item #5
Companion PRs: Thursday PR 2 (dispatch simplify #6) stacks on this; PR 3 (session decorator #7) is independent.
Problem
models/session.pyMessageDict(lines 29–52) is a single flatTypedDict:role: RoleLiteralrequired plus 22NotRequiredfields spanning all five roles. mypy cannot distinguish roles, somsg["thinking"]on a user message type-checks yet is absent at runtime.Already done and not this issue:
rolenarrowed toRoleLiteral(PR feat: narrow MessageDict.role to RoleLiteral #89).SessionMetadataDicttightened to all-required fields (PR claude-code-chat-browser: SessionMetadataDict — tighten all-NotRequired fields for mypy shape enforcement #106).MessageDictremains a flat, monotonically-widening union — the type-safety gap is unresolved.Goal
One merged PR that splits
MessageDictinto role-specific TypedDicts unioned onrole, updates consumers to narrow on the discriminant, and preserves serialized JSON shapes for API/export parity.Scope
Touch points
models/session.py— introduce:BaseMessageDict(common fields:uuid,timestamp,type, …)UserMessageDict,AssistantMessageDict,SystemMessageDict,ResultMessageDict,ProgressMessageDictMessageDict = Union[...]discriminated onroleutils/jsonl_parser.pyutils/tool_dispatch.pyapi/sessions.pyutils/md_exporter.py,utils/json_exporter.pythinkingon user message).Out of scope
_TOOL_RESULT_DISPATCHrefactor (Thursday PR 2 — lands after this).SessionMetadataDictchanges (already done in PR claude-code-chat-browser: SessionMetadataDict — tighten all-NotRequired fields for mypy shape enforcement #106).Acceptance Criteria
MessageDictrefactored into role-specific TypedDicts +Uniononrole.[tool.mypy] strict = truepasses with no new ignores.tests/test_real_session_fixtures.pyand export tests pass.pytest,mypy --strict, andruffpass.Verification