# Code Review: `qc_trace/schemas/` -- Schemas and Transforms

**Date:** 2026-02-07
**Reviewer:** Claude Opus 4.6
**Scope:** `qc_trace/schemas/unified.py`, `claude_code/`, `codex_cli/`, `gemini_cli/`, `cursor/`

---

## Executive Summary

The schemas directory implements a normalize-on-read architecture: four source-specific schema modules (Claude Code, Codex CLI, Gemini CLI, Cursor IDE) each define TypedDict/dataclass representations of their raw formats, plus a `transform.py` that converts raw data into a shared `NormalizedMessage` dataclass. The design is sound and the code is generally well-structured. The main issues are: (1) significant dead code in the source schemas, especially types that are defined but never imported or used outside their own module; (2) inconsistencies across transforms in how they populate `NormalizedMessage` fields; and (3) a bug in codex_cli's function_call_output parsing where the schema says `output` is a nested dict but the transform treats it as a string.

---

## 1. Unified Schema (`unified.py`)

**File:** `/Users/sagar/work/all-things-quickcall/trace/qc_trace/schemas/unified.py` (187 lines)

### Medium: Duplicated Literal definitions for MessageType

**Lines 85-114.** `ConversationMessageType`, `ObservabilityMessageType`, and `MessageType` are three separate `Literal` types. The first two are never referenced anywhere in the codebase -- only `MessageType` is used (by `NormalizedMessage.msg_type`). The three-way split creates a maintenance burden: if a new type is added to `MessageType`, a developer might forget to update the categorical Literals. Since the categorical ones are unused, they are dead code.

```python
# Lines 85-96 -- never imported or referenced outside this file
ConversationMessageType = Literal[...]
ObservabilityMessageType = Literal[...]
```

### Low: `SourceType` not enforced by transforms

**Line 116.** `SourceType = Literal["claude_code", "codex_cli", "gemini_cli", "cursor"]` is defined but transforms hardcode string literals (e.g., `source="claude_code"`) rather than referencing `SourceType`. This is technically fine (mypy checks literals against the annotation), but a helper constant or enum could prevent typos in future source additions.

### Low: Observability dataclasses are Claude-Code-specific

**Lines 40-81.** `HookInfo`, `ProgressData`, `SystemEventData`, and `QueueOperationData` are only populated by the Claude Code transform. No other source uses these fields. They are part of the unified schema but are effectively single-source types masquerading as generic ones. If another source has progress/system events in the future, these dataclasses may need redesign. This is not a bug but a design smell worth noting.

---

## 2. Claude Code (`claude_code/`)

### 2.1 `v1.py` (233 lines)

**File:** `/Users/sagar/work/all-things-quickcall/trace/qc_trace/schemas/claude_code/v1.py`

#### Medium: Dataclasses defined but never constructed

**Lines 124-224.** `ClaudeUserMessage` and `ClaudeAssistantMessage` are full `@dataclass` definitions with all fields. However, the transform (`transform.py`) operates entirely on `dict[str, Any]` -- it never constructs these dataclasses. They serve purely as documentation of the raw schema shape. This is an acceptable pattern for a "frozen schema" but it means the dataclass field defaults, types, and structure are never validated at runtime. The `__init__.py` exports these types, but `grep` shows no external consumer imports them.

#### Low: `ClaudeMessage` union (line 224) is unused

`ClaudeMessage = ClaudeUserMessage | ClaudeAssistantMessage` is exported in `__init__.py` but is never imported by any other module (confirmed by grep). The same applies to `ClaudeContentBlock` (line 57).

#### Low: Comment at lines 227-233 says "skipped types" exist

The comment documents types that exist in session files but are not modeled. This is good documentation, but these types *are* actually handled by `transform.py` (e.g., `progress`, `summary`, `file-history-snapshot`, `system`, `queue-operation`). The comment is stale and misleading -- it should note these types are handled in the transform but not modeled as schema dataclasses.

### 2.2 `transform.py` (477 lines)

**File:** `/Users/sagar/work/all-things-quickcall/trace/qc_trace/schemas/claude_code/transform.py`

#### Medium: Re-import of `re` inside function body

**Line 393.** `_transform_queue_operation` does `import re as regex` inside the function body, despite `re` already being imported at the module level (line 3). This is a clear oversight -- the module-level `re` should be used directly.

```python
# Line 3: already imported
import re

# Line 393: redundant re-import with alias
import re as regex
```

#### Medium: Duplicate tool-result extraction logic

**Lines 108-138 vs. 258-288.** `_transform_user_message` and `_transform_result_message` both contain nearly identical code for extracting tool results from content blocks. The only meaningful difference is that `_transform_result_message` checks `block.get("is_error")` for status while `_transform_user_message` hardcodes `status="success"`. This is a copy-paste pattern that should be extracted into a shared helper.

#### Medium: Token double-attribution for assistant messages

**Lines 210-220 and 228-243.** When an assistant message contains both text and tool_use blocks, the same `tokens` object is attached to *both* the assistant message *and* every tool_call message. This means summing `tokens.input` across all messages from a session will overcount tokens for any assistant turn that includes tool calls.

#### Low: Missing `model` field on tool_result messages

Claude Code tool results (lines 120-137, 269-286) never set `model`. This is consistent with the other transforms but worth noting -- the model is known from the preceding assistant message but not threaded through.

### 2.3 `__init__.py`

**File:** `/Users/sagar/work/all-things-quickcall/trace/qc_trace/schemas/claude_code/__init__.py`

#### Low: Exports types with no external consumers

All v1 types (`ClaudeMessage`, `ClaudeTextBlock`, `ClaudeThinkingBlock`, etc.) are exported but only `transform_claude_v1` is imported by external code (`collector.py`, `test_transforms.py`). The TypedDict exports are unused outside this package.

---

## 3. Codex CLI (`codex_cli/`)

### 3.1 `v1.py` (1266 lines)

**File:** `/Users/sagar/work/all-things-quickcall/trace/qc_trace/schemas/codex_cli/v1.py`

#### Assessment: Size is justified but comes with dead code

The 1266-line file is large because the Codex CLI protocol (`protocol.rs`) defines **50+ EventMsg variants** plus response item types, session metadata, and configuration. Each variant gets its own TypedDict (1-5 fields each). The file is essentially a mechanical 1:1 translation of the Rust protocol definitions into Python TypedDict declarations. The size is justified by the complexity of the source protocol.

However, the transform (`transform.py`, 348 lines) only handles a small fraction of these types:
- `session_meta` (metadata, no output)
- `turn_context` (metadata, no output)
- `event_msg` with `type` in: `token_count`, `turn_aborted`, `user_message`, `agent_message`
- `response_item` with `type` in: `message`, `reasoning`, `function_call`, `function_call_output`

This means the vast majority of the 50+ event TypedDicts and several response item types are **defined but never used in transformation**. They exist purely as schema documentation.

**Types never referenced outside v1.py (partial list):**
- All `Codex*EventPayload` types for: `exec_command_*`, `patch_apply_*`, `mcp_*`, `collab_*`, `web_search_*`, `plan_*`, `undo_*`, `review_*`, `deprecation_*`, `stream_error`, `background_event`, `view_image_*`, `item_started/completed`, `agent_message_content_delta`, `reasoning_content_delta`, `reasoning_raw_content_delta`, `shutdown_complete`, `skills_update_available`, and many more.
- Sandbox policy types: `CodexSandboxPolicyDangerFullAccess`, `CodexSandboxPolicyReadOnly`, `CodexSandboxPolicyExternalSandbox`
- `CodexCompactedItemPayload`, `CodexGhostSnapshotPayload`, `CodexCompactionPayload`
- `CodexCustomToolCallPayload`, `CodexCustomToolCallOutputPayload`
- `CodexLocalShellCallPayload`, `CodexWebSearchCallPayload` (exported in `__init__.py` but never imported externally)
- All `CodexReview*`, `CodexPlan*`, `CodexDeprecation*`, `CodexViewImage*` types

#### High: `function_call_output` schema/transform mismatch

**v1.py lines 378-391 vs. transform.py line 275.** The schema defines `CodexFunctionCallOutputPayload.output` as type `CodexFunctionCallOutputPayloadInner` (a TypedDict with `output: str`, `success: bool`, `metadata: dict`). However, the transform at line 275 does:

```python
output = payload.get("output", "")
```

This treats `output` as a string. But per the schema, `payload["output"]` is a dict with nested `.output` and `.success` fields. One of two things is true:
1. The schema is wrong and the real data has `output` as a string at the top level, OR
2. The transform has a bug and should be doing `payload.get("output", {}).get("output", "")` and using `.get("success", True)` for status instead of the naive "error" substring check.

The naive `"error" in output.lower()` status heuristic (line 277) is also fragile -- it will false-positive on outputs that mention "error" in explanatory text (e.g., "I fixed the error handling code"). The schema has `success: bool` that should be used instead.

#### Low: `CodexDynamicToolSpec` (line 186) placed in Session Meta section

Minor organizational issue. `CodexDynamicToolSpec` is used inside `CodexSessionMetaPayload` but it's a tool-related type that could be grouped with other tool types.

### 3.2 `transform.py` (348 lines)

**File:** `/Users/sagar/work/all-things-quickcall/trace/qc_trace/schemas/codex_cli/transform.py`

#### Medium: `CodexTransformContext` tracks git fields that are never populated

**Lines 55-56.** `git_branch`, `git_commit`, and `repo_url` are set in `update_from_turn_context` but `turn_context` payloads (per the schema at line 305-319 of v1.py) don't have `branch`, `commit`, or `repo_url` fields. These would need to come from the `session_meta`'s `git` field, but `update_from_session_meta` (line 58) doesn't extract git info. The git context fields are always `None`.

#### Medium: `extract_session_id` regex may not match real filenames

**Line 24.** The regex `r"rollout-[^-]+-([0-9a-f-]{36})\.jsonl$"` expects exactly one non-hyphen segment between "rollout-" and the UUID. But the documented filename format is `rollout-2026-02-04T16-44-26-019c285c-...`, where the ISO timestamp contains multiple hyphens. The `[^-]+` will match only "2026" and then fail because the rest of the timestamp is not a UUID. The fallback (lines 28-31) saves it by returning the full filename, but the primary regex is never matching real data.

#### Medium: Most event types are silently dropped

**Line 208.** The `_transform_event_msg` function returns `[]` for all event types except `token_count`, `turn_aborted`, `user_message`, and `agent_message`. Events like `error`, `exec_command_begin/end`, `mcp_tool_call_begin/end`, `task_started`, `task_complete` are all silently discarded. This is a design choice, but unlike Claude Code's transform which handles all types, Codex drops significant observability data. An `exec_command_begin` event contains the actual shell command being run -- arguably as important as a tool_call.

#### Low: `uuid.uuid4()` imported but could collide across runs

**Lines 5, 37.** `generate_id()` creates random UUIDs for messages that lack IDs. This means the same Codex session processed twice will produce different message IDs, making deduplication or idempotent ingestion impossible. Claude Code uses UUIDs from the source data; Gemini uses source IDs; Codex generates random ones. This inconsistency could cause issues for any system that expects stable IDs.

### 3.3 `__init__.py`

**File:** `/Users/sagar/work/all-things-quickcall/trace/qc_trace/schemas/codex_cli/__init__.py`

#### Low: Selective export hides most v1 types

The `__init__.py` exports 30 of the 60+ types defined in v1.py. The selection criteria is not documented. Types like `CodexErrorInfo`, `CodexSessionConfiguredEventPayload`, `CodexMcpToolCallBeginEventPayload` are not exported but may be useful for downstream consumers.

---

## 4. Gemini CLI (`gemini_cli/`)

### 4.1 `v1.py` (610 lines)

**File:** `/Users/sagar/work/all-things-quickcall/trace/qc_trace/schemas/gemini_cli/v1.py`

#### Medium: `GeminiBaseMessageRecord` is dead code

**Line 334.** `GeminiBaseMessageRecord` defines base fields (`id`, `timestamp`, `content`, `displayContent`) but is never used as a base class, never imported, never exported. The individual message records (`GeminiUserMessageRecord`, etc.) each re-declare these fields independently. This is likely a design remnant -- TypedDicts don't support inheritance cleanly in Python, so the base was abandoned but not removed.

#### Medium: Dual representation -- TypedDicts AND dataclasses for messages

**Lines 334-424 vs. 509-570.** The file defines *both* TypedDict records (`GeminiUserMessageRecord`, `GeminiGeminiMessageRecord`, etc.) and dataclass equivalents (`GeminiUserMessage`, `GeminiAssistantMessage`, etc.). The TypedDicts describe the raw JSON shape; the dataclasses are labeled "convenience dataclasses." However, neither the transform nor any external code constructs the dataclass versions. They are pure dead code.

Similarly, `GeminiSession` (lines 573-596) has a `from_record` classmethod but is never instantiated anywhere in the codebase (confirmed by grep).

#### Medium: `GeminiToolResult` TypedDict (lines 604-610) shadows unified ToolResult

**Line 604.** `GeminiToolResult` is a TypedDict defined in `v1.py` and exported from `__init__.py`. The unified schema also has a `ToolResult` dataclass. The Gemini-specific one is never used by the transform (which uses the unified `ToolResult`). Its existence is confusing -- a reader might wonder whether the transform should be using it.

#### Low: Backward-compatibility aliases are premature

**Lines 163-164, 193-194, 326.** `GeminiTokens = GeminiTokensSummary`, `GeminiThought = GeminiTimestampedThought`, and `GeminiToolCall = GeminiToolCallRecord` are aliases "for backward compatibility." But this is a v1 schema that has never had a different name -- there's nothing to be backward-compatible with. These aliases add confusion without value.

#### Low: Tool argument TypedDicts are comprehensive but unused

**Lines 228-297.** `GeminiWriteFileArgs`, `GeminiEditArgs`, `GeminiShellArgs`, etc. are defined for various Gemini tools. The transform never inspects tool arguments at this level of detail -- it passes `tc.get("args", {})` directly as a dict. These types are documentation-only.

### 4.2 `transform.py` (253 lines)

**File:** `/Users/sagar/work/all-things-quickcall/trace/qc_trace/schemas/gemini_cli/transform.py`

#### Medium: `warning` messages silently dropped

**Line 89.** The `_transform_message` function handles `user`, `gemini`, `error`, and `info` types but has no handler for `warning`. The schema defines `GeminiWarningMessageRecord` and `GeminiMessageType` includes `"warning"`. Warning messages will hit the final `return []` and be silently discarded. Given that `info` and `error` both have handlers, `warning` should too.

#### Low: Content type not validated

**Lines 105, 133.** `content=msg.get("content")` is passed directly. For user messages, content can be `str | Any` (per the TypedDict). If `content` is a list (e.g., multipart content with images), it will be stored as-is in `NormalizedMessage.content` which expects `str | None`. This could cause issues downstream.

#### Low: No `session_context` populated

The Gemini transform does not populate `session_context`. The `collector.py` handles this externally, which is the right pattern, but it means the transform is not self-sufficient -- the same message processed through the transform alone vs. through the collector will have different completeness.

### 4.3 `__init__.py`

**File:** `/Users/sagar/work/all-things-quickcall/trace/qc_trace/schemas/gemini_cli/__init__.py`

#### Low: Exports dead types

`GeminiToolResult`, `GeminiToolCall`, `GeminiTokens`, `GeminiThought`, convenience dataclasses (`GeminiUserMessage`, etc.), and tool arg types are all exported but have no external consumers.

---

## 5. Cursor (`cursor/`)

### 5.1 `v1.py` (368 lines)

**File:** `/Users/sagar/work/all-things-quickcall/trace/qc_trace/schemas/cursor/v1.py`

#### Low: Schema covers much more than the transform uses

The schema defines types for: global state DB (`CursorDailyStats`, `CursorPendingMemory`, `CursorServerConfig`), workspace state (`CursorComposerEntry`, `CursorComposerData`, `CursorAiServicePrompt`), AI tracking DB (`CursorAiCodeHash`, `CursorConversationSummary`, `CursorScoredCommit`), MCP metadata, IDE state, and agent transcripts. The transform only uses `CursorAgentTranscript`, `CursorTranscriptMessage`, `CursorToolInvocation`, and `CursorComposerEntry`. The rest are schema documentation for other parts of the system (e.g., `cursor_parser.py` uses some).

This is not a problem per se -- the schema file is the canonical reference for all Cursor data structures -- but it explains why the file is 368 lines while the transform is only 265.

### 5.2 `transform.py` (265 lines)

**File:** `/Users/sagar/work/all-things-quickcall/trace/qc_trace/schemas/cursor/transform.py`

#### Medium: `uuid` imported but only used for fallback in `transform_composer_metadata`

**Lines 5, 243.** `import uuid` is at the top of the file but only used once: `str(uuid.uuid4())` as a fallback if `composerId` is missing. Additionally, `datetime` and `timezone` are imported at the top (line 6) and again inside the function body (line 249). This is the same pattern as the Claude Code `re` re-import.

```python
# Line 6: already imported
from datetime import datetime, timezone

# Line 249: redundant re-import inside function
from datetime import datetime, timezone
```

#### Medium: `os.path.getmtime` for timestamps is fragile

**Lines 55-58.** The cursor transform uses the file's modification time as a fallback timestamp for all messages. This means copying/moving the transcript file changes all message timestamps. Other transforms get timestamps from the data itself. Cursor transcripts appear to lack timestamps, so this is the best available option, but the fragility should be documented.

#### Medium: Tool result `call_id` does not link back to the tool call

**Line 217.** Tool result messages use `call_id=tool_id` where `tool_id = f"{session_id}-tool-{msg_index}"`. But the corresponding tool call uses a different `msg_index` value (the tool call's index vs. the tool result's index). This means `tool_result.call_id` will never equal `tool_call.tool_call.id`, making it impossible to correlate tool calls with their results. In contrast, Claude Code and Gemini use the same ID (`tool_use_id` or `tc.get("id")`) for both the call and result.

#### Low: No model field populated

Cursor transcript messages never set `model` on assistant messages. The model information may be available in `CursorComposerEntry` or `CursorConversationSummary` but is not threaded through.

#### Low: No tokens populated

Cursor transforms never populate token usage. The `TokenUsage` defaults to all zeros. This is expected since plain-text transcripts don't contain token counts, but it means Cursor sessions will look anomalous in any dashboard that displays token usage.

### 5.3 `__init__.py`

**File:** `/Users/sagar/work/all-things-quickcall/trace/qc_trace/schemas/cursor/__init__.py`

Clean. Exports both schema types and transform functions. External consumers (`cursor_parser.py`, `collector.py`) do import from this module.

---

## 6. Cross-Cutting Findings

### High: `function_call_output` schema vs. transform mismatch (Codex)

Already detailed in section 3.2. The schema says `output` is a nested TypedDict; the transform treats it as a string. One of them is wrong.

### High: Inconsistent `NormalizedMessage` field population across transforms

| Field | Claude Code | Codex CLI | Gemini CLI | Cursor |
|-------|------------|-----------|------------|--------|
| `id` | From source UUID | `uuid.uuid4()` (random) | From source UUID | Synthetic `{session}-{index}` |
| `model` | Yes (assistant, tool_call) | Yes (assistant, tool_call) | Yes (assistant only) | Never |
| `tokens` | Yes (assistant, tool_call -- double-counted) | Yes (assistant, tool_call) | Yes (assistant only) | Never |
| `thinking` | Yes | Yes (often `[encrypted]`) | Yes (formatted with subjects) | Yes |
| `raw_line_number` | Always set | Always set | Never set (JSON, not JSONL) | Always `None` |
| `session_context` | Set by collector | Set by collector | Set by collector | Set by collector |
| `progress_data` | Yes | Never | Never | Never |
| `system_event_data` | Yes | Never | Never | Never |
| `queue_operation_data` | Yes | Never | Never | Never |

Key concerns:
- **Token double-counting** in Claude Code (same tokens on both assistant + tool_call messages from the same turn)
- **Non-deterministic IDs** in Codex (random UUIDs break idempotent processing)
- **Missing model** in Cursor (no way to know which model was used)
- **Gemini raw_line_number** is never set because the format is JSON not JSONL -- this is correct behavior, not a bug, but the field name `raw_line_number` is misleading for JSON sources

### Medium: Three different `extract_session_id` functions

Each of Claude Code, Codex CLI, and Cursor defines its own `extract_session_id` with similar structure (regex match on file path, fallback to filename). These could share a common pattern or base function, but the path formats are different enough that sharing would add complexity without much value. Noting as a minor redundancy.

### Medium: Repeated NormalizedMessage construction boilerplate

Every transform function manually specifies `source="claude_code"`, `source_schema_version=1`, `raw_file_path=file_path`, `raw_line_number=line_num` on every `NormalizedMessage` construction. A factory function like `_make_message(session_id, file_path, line_num, **overrides)` per transform module would reduce boilerplate and prevent field omission errors.

### Low: `schemas/__init__.py` is minimal

**File:** `/Users/sagar/work/all-things-quickcall/trace/qc_trace/schemas/__init__.py` (6 lines)

Only exports `NormalizedMessage`, `TokenUsage`, and the `cursor` submodule. Does not re-export the other three source modules or their transforms. This is inconsistent -- either all sources should be exported or none should.

---

## 7. Summary of Findings by Severity

### Critical
None.

### High (3)
1. **Codex `function_call_output` schema/transform mismatch** -- `v1.py` says `output` is a nested dict with `success: bool`; `transform.py` treats it as a plain string and uses naive "error" substring matching. (`codex_cli/v1.py:378-391`, `codex_cli/transform.py:274-277`)
2. **Token double-counting in Claude Code** -- Same `TokenUsage` object attached to both assistant message and each tool_call message from the same turn. (`claude_code/transform.py:210,237`)
3. **Non-deterministic IDs in Codex transform** -- `uuid.uuid4()` means reprocessing the same file yields different message IDs, breaking idempotent ingestion. (`codex_cli/transform.py:37`)

### Medium (10)
1. **Duplicate tool-result extraction** in Claude Code transform. (`claude_code/transform.py:108-138` vs `258-288`)
2. **Re-import of `re` as `regex`** inside function body. (`claude_code/transform.py:393`)
3. **Re-import of `datetime`** inside function body in Cursor transform. (`cursor/transform.py:249`)
4. **Codex `extract_session_id` primary regex never matches** real filenames. (`codex_cli/transform.py:24`)
5. **Codex git fields never populated** in `CodexTransformContext`. (`codex_cli/transform.py:55-56,71-73`)
6. **Codex silently drops most event types** including errors, exec commands, MCP calls. (`codex_cli/transform.py:208`)
7. **Gemini `warning` messages silently dropped.** (`gemini_cli/transform.py:89`)
8. **Cursor tool_result `call_id` doesn't link** to corresponding tool_call ID. (`cursor/transform.py:217`)
9. **`GeminiBaseMessageRecord` is dead code.** (`gemini_cli/v1.py:334`)
10. **Dual TypedDict + dataclass representations** in Gemini with dataclasses unused. (`gemini_cli/v1.py:509-570`)

### Low (12)
1. **`ConversationMessageType` and `ObservabilityMessageType`** Literal types unused. (`unified.py:85-96`)
2. **`ClaudeMessage` union type** unused outside module. (`claude_code/v1.py:224`)
3. **Stale comment** about "skipped types" in Claude Code schema. (`claude_code/v1.py:227-233`)
4. **`GeminiToolResult` TypedDict** shadows unified `ToolResult` and is unused. (`gemini_cli/v1.py:604-610`)
5. **Backward-compat aliases** premature (`GeminiTokens`, `GeminiThought`, `GeminiToolCall`). (`gemini_cli/v1.py:163,193,326`)
6. **Tool argument TypedDicts** in Gemini defined but unused by transform. (`gemini_cli/v1.py:228-297`)
7. **Cursor `os.path.getmtime`** for timestamps is fragile. (`cursor/transform.py:55-58`)
8. **Cursor never sets `model`** on messages. (`cursor/transform.py` -- all messages)
9. **Cursor never sets tokens.** (`cursor/transform.py` -- all messages)
10. **`schemas/__init__.py`** inconsistent exports. (`schemas/__init__.py`)
11. **Codex `__init__.py`** selectively exports with undocumented criteria. (`codex_cli/__init__.py`)
12. **Gemini `__init__.py`** exports many dead types. (`gemini_cli/__init__.py`)

---

## 8. Recommendations

### Priority 1 -- Fix bugs
- Verify codex `function_call_output` actual data shape and fix either the schema or the transform.
- Fix Claude Code token double-counting (only attach tokens to the assistant message, not to each tool_call).
- Make Codex IDs deterministic (e.g., `f"{session_id}-{line_num}"`).

### Priority 2 -- Improve consistency
- Add `warning` handler to Gemini transform (trivial, mirror `_transform_info_message`).
- Fix Cursor tool_result `call_id` to reference the corresponding tool_call's ID.
- Fix the Codex `extract_session_id` regex or remove it in favor of the fallback.

### Priority 3 -- Reduce dead code
- Remove `GeminiBaseMessageRecord`, convenience dataclasses, `GeminiSession`, `GeminiToolResult`.
- Remove `ConversationMessageType` and `ObservabilityMessageType` from unified.py.
- Remove backward-compat aliases that have no backward compatibility to maintain.
- Consider splitting codex_cli/v1.py: core types used by the transform in one file, full protocol reference types in another.

### Priority 4 -- Reduce boilerplate
- Add a `_make_message()` factory per transform module.
- Extract shared tool-result parsing in Claude Code transform.
- Remove redundant `import re as regex` and `from datetime import datetime, timezone` re-imports.
